package backtest import ( "math" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" "sig-pub/pkg/utils/collect" "sig-pub/pkg/utils/conver" "time" ) type Account struct { Cash float64 Positions []*Position Trades []*Trade // 交易订单利润分布,回测结果echart折线显示,评估止盈止损策略效果 CloseTrades []*Trade MaxPosPct float64 // 最大仓位占比 MaxExposurePct float64 // 最大总敞口占比 MaxLots float64 // 最大手数/数量 (optional) Simulator *Simulator Stat *TradeStat // 交易统计 Profit float64 } func NewAccount(cash float64, sim *Simulator) *Account { return &Account{ Cash: cash, Simulator: sim, MaxPosPct: 1.0, MaxExposurePct: 1.0, Stat: &TradeStat{}, } } 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 { switch p.Side { case types.SideLong: equity += (price - p.EntryPx) * p.Qty case types.SideShort: 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 } func (a *Account) PositionCost() float64 { var sum float64 for _, p := range a.Positions { sum += math.Abs(p.Qty * p.EntryPx) } return sum } // CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct) func (a *Account) CanOpen(side types.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 types.Side, qty float64, price float64, ts int64) (t *Trade, ok bool) { if !a.CanOpen(side, qty, price) { return t, false } trade, ok := a.Simulator.ExecuteMarket(side, qty, price, ts) if !ok { return } // apply cash/position, short side also need earnest money cost := trade.Price*qty + trade.Fee if cost > a.Cash { return trade, false } a.Cash -= cost // open/add position pos := &Position{TradeId: trade.Id, Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: ts, PeakPx: trade.Price, Fee: trade.Fee} a.Positions = append(a.Positions, pos) a.Trades = append(a.Trades, trade) { a.Stat.TotalTrades++ a.Stat.Fee += trade.Fee } return trade, true } // ClosePosition 根据索引平仓(全部平仓该仓位) func (a *Account) ClosePosition(index int, pos *Position, kline types.Kline, ts int64, cause string) (t *Trade, ok bool) { // determine close side (opposite) var closeSide types.Side if pos.Side == types.SideLong { closeSide = types.SideShort } else { closeSide = types.SideLong } closePrice := decimals.MustToFloat64(kline.Close) t, ok = a.Simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, ts) if !ok { return t, false } t.CloseCause = cause // apply cash change // calc profit var receive, profit float64 if pos.Side == types.SideLong { profit = t.Qty*(t.Price-pos.EntryPx) - t.Fee - pos.Fee receive = t.Price*pos.Qty - t.Fee } else { profit = (pos.EntryPx - t.Price) * t.Qty receive = pos.EntryPx*pos.Qty + profit - t.Fee profit = profit - t.Fee - pos.Fee } a.Cash += receive a.Profit += profit if profit > 0 { a.Stat.WinningTrades++ } else { a.Stat.LosingTrades++ } a.Stat.Fee += t.Fee // remove position a.Positions = append(a.Positions[:index], a.Positions[index+1:]...) a.CloseTrades = append(a.CloseTrades, t) trade, ok := collect.Find(a.Trades, func(t *Trade) bool { return t.Id == pos.TradeId }) if ok { trade.Pnl = profit trade.ClosePrice = closePrice trade.CloseFee = t.Fee trade.CloseTs = kline.Ts trade.CloseCause = cause trade.HoldTime = conver.TimeDurationFormat(time.Duration(trade.CloseTs-trade.Ts)*time.Millisecond, ".") } return t, true }