package backtest import ( "math" "sig-pub/api/pb" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" ) type Account struct { Cash float64 Positions []*Position Trades []Trade MaxPosPct float64 // 最大仓位占比 MaxExposurePct float64 // 最大总敞口占比 MaxLots float64 // 最大手数/数量 (optional) Simulator *Simulator } func NewAccount(cash float64, sim *Simulator) *Account { return &Account{Cash: cash, Simulator: sim, MaxPosPct: 1.0, MaxExposurePct: 1.0} } func (a *Account) SetRiskLimits(maxPosPct, maxExposurePct float64) { if maxPosPct > 0 { a.MaxPosPct = maxPosPct } if maxExposurePct > 0 { a.MaxExposurePct = maxExposurePct } } // CurrentEquity 根据当前价格对仓位进行 mark-to-market,返回账户净值 func (a *Account) CurrentEquity(price float64) float64 { equity := a.Cash for _, p := range a.Positions { if p.Side == pb.Side_BUY { equity += (price - p.EntryPx) * p.Qty } else if p.Side == pb.Side_SELL { equity += (p.EntryPx - price) * p.Qty } } return equity } // CurrentExposure 返回当前仓位的名义总敞口(绝对值) func (a *Account) CurrentExposure(price float64) float64 { var sum float64 for _, p := range a.Positions { sum += math.Abs(p.Qty * price) } return sum } // CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct) func (a *Account) CanOpen(side pb.Side, qty, price float64) bool { if qty <= 0 || price <= 0 { return false } equity := a.CurrentEquity(price) if equity <= 0 { return false } notional := math.Abs(qty * price) // 单仓位限制 if a.MaxPosPct > 0 { if notional > a.MaxPosPct*equity { return false } } // 总敞口限制 if a.MaxExposurePct > 0 { if a.CurrentExposure(price)+notional > a.MaxExposurePct*equity { return false } } return true } // ApplyMarketOrder 直接用市价下单(简化),qty为基础货币数量 func (a *Account) ApplyMarketOrder(side pb.Side, qty float64, klineTs int64, kline types.Kline) (t Trade, ok bool) { // risk check before executing price := decimals.MustToFloat64(kline.Close) if !a.CanOpen(side, qty, price) { return t, false } trade := a.Simulator.ExecuteMarket(side, qty, kline, klineTs) // apply cash/position if side == pb.Side_BUY { cost := trade.Price*qty + trade.Fee if cost > a.Cash { return trade, false } a.Cash -= cost // open/add position pos := &Position{Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: klineTs, PeakPx: trade.Price} a.Positions = append(a.Positions, pos) } else if side == pb.Side_SELL { // simplify: allow short by increasing cash receive := trade.Price*qty - trade.Fee a.Cash += receive pos := &Position{Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: klineTs, PeakPx: trade.Price} a.Positions = append(a.Positions, pos) } a.Trades = append(a.Trades, trade) return trade, true } // ClosePosition 根据索引平仓(全部平仓该仓位) func (a *Account) ClosePosition(index int, kline types.Kline, ts int64, cause string) (t Trade, ok bool) { if index < 0 || index >= len(a.Positions) { return t, false } pos := a.Positions[index] if pos == nil { return t, false } // determine close side (opposite) var closeSide pb.Side if pos.Side == pb.Side_BUY { closeSide = pb.Side_SELL } else { closeSide = pb.Side_BUY } t = a.Simulator.ExecuteMarket(closeSide, pos.Qty, kline, ts) t.Cause = cause // apply cash change if closeSide == pb.Side_SELL { // selling a long position -> receive cash receive := t.Price*pos.Qty - t.Fee a.Cash += receive } else { // buying to close a short -> pay cash cost := t.Price*pos.Qty + t.Fee a.Cash -= cost } // remove position a.Positions = append(a.Positions[:index], a.Positions[index+1:]...) a.Trades = append(a.Trades, t) return t, true }