源码索引 / CURRENT IMPLEMENTATION
cmd/trade-demo/market.go
158 行 · 点击行号可定位;函数链接随声明位置更新,不手填行号。
跳到函数 / 声明
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"sync"
"time"
spine "pysolate-spine"
)
type event struct {
Time string `json:"time"`
ElapsedMS float64 `json:"elapsed_ms"`
Sequence int `json:"seq"`
Event string `json:"event"`
Tool string `json:"tool,omitempty"`
Data any `json:"data,omitempty"`
}
type trace struct {
mu sync.Mutex
start time.Time
sequence int
writer io.Writer
}
func newTrace(w io.Writer) *trace { return &trace{start: time.Now(), writer: w} }
func (t *trace) log(kind, tool string, data any) {
t.mu.Lock()
defer t.mu.Unlock()
t.write(kind, tool, data)
}
func (t *trace) write(kind, tool string, data any) {
t.sequence++
now := time.Now()
e := event{Time: now.UTC().Format(time.RFC3339Nano), ElapsedMS: float64(now.Sub(t.start).Microseconds()) / 1000, Sequence: t.sequence, Event: kind, Tool: tool, Data: data}
// This CLI's log writer is stderr; traces never alter runtime decisions.
_ = json.NewEncoder(t.writer).Encode(e)
}
func (t *trace) closeSource(ch chan string) {
t.mu.Lock()
defer t.mu.Unlock()
close(ch)
t.write("source.closed", "", nil)
}
type order struct {
ID int `json:"id"`
Symbol string `json:"symbol"`
Quantity int `json:"quantity"`
Price int `json:"price"`
Cost int `json:"cost"`
}
type market struct {
mu sync.Mutex
symbol string
price, initialCash, remainingCash int
allowOrder bool
delay time.Duration
orders []order
attempts int
calls map[string]int
trace *trace
}
func newMarket(symbol string, price, cash int, allow bool, delay time.Duration, t *trace) *market {
return &market{symbol: symbol, price: price, initialCash: cash, remainingCash: cash, allowOrder: allow, delay: delay, calls: map[string]int{}, trace: t}
}
func (m *market) wrap(name string, fn spine.Tool) spine.Tool {
return func(ctx context.Context, args json.RawMessage) (any, error) {
m.mu.Lock()
m.calls[name]++
m.mu.Unlock()
m.trace.log("tool.start", name, args)
value, err := fn(ctx, args)
if err != nil {
m.trace.log("tool.error", name, map[string]any{"error": err.Error()})
} else {
m.trace.log("tool.end", name, map[string]any{"result": value})
}
return value, err
}
}
func (m *market) waitRead(ctx context.Context) error {
if m.delay == 0 {
return ctx.Err()
}
timer := time.NewTimer(m.delay)
defer timer.Stop()
select {
case <-timer.C:
return ctx.Err()
case <-ctx.Done():
return ctx.Err()
}
}
func (m *market) tools() spine.Manifest {
return spine.Manifest{
"quote": {Call: m.wrap("quote", func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
Symbol string `json:"symbol"`
}
if err := json.Unmarshal(args, &a); err != nil {
return nil, err
}
if a.Symbol != m.symbol {
return nil, fmt.Errorf("unknown simulated symbol: %s", a.Symbol)
}
if err := m.waitRead(ctx); err != nil {
return nil, err
}
return m.price, nil
}), AllowEarlyRead: true},
"cash": {Call: m.wrap("cash", func(ctx context.Context, _ json.RawMessage) (any, error) {
if err := m.waitRead(ctx); err != nil {
return nil, err
}
// A frozen snapshot is eligible for early reads even if a later buy changes the account.
return m.initialCash, nil
}), AllowEarlyRead: true},
"buy": {Call: m.wrap("buy", func(ctx context.Context, args json.RawMessage) (any, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.attempts++
if err := ctx.Err(); err != nil {
return nil, err
}
if !m.allowOrder {
m.trace.log("order.denied", "buy", map[string]any{"reason": "Host approval missing", "account_changed": false})
return nil, fmt.Errorf("simulated order requires Host -allow-order")
}
var a struct {
Symbol string `json:"symbol"`
Quantity int `json:"quantity"`
}
if err := json.Unmarshal(args, &a); err != nil {
return nil, err
}
if a.Symbol != m.symbol || a.Quantity <= 0 {
return nil, fmt.Errorf("invalid simulated order")
}
// Recheck the current account, independently of Python's control flow. Division avoids overflow.
if a.Quantity > m.remainingCash/m.price {
m.trace.log("order.denied", "buy", map[string]any{"reason": "insufficient current cash", "account_changed": false})
return nil, fmt.Errorf("insufficient current cash")
}
m.trace.log("order.authorized", "buy", map[string]any{"simulation": true})
o := order{ID: len(m.orders) + 1, Symbol: a.Symbol, Quantity: a.Quantity, Price: m.price, Cost: a.Quantity * m.price}
m.remainingCash -= o.Cost
m.orders = append(m.orders, o)
m.trace.log("order.committed", "buy", map[string]any{"simulation": true, "order": o, "remaining_cash": m.remainingCash})
return o, nil
})},
}
}