package backtest import ( "fmt" "sig-pub/pkg/trade" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" "sig-pub/pkg/utils/conver" "sig-pub/pkg/utils/lang" "time" ) type BacktestAccount struct { trade.ITradeAccount cash float64 positions map[int64]*trade.Position trades map[int64]*trade.Trade closeTrades map[int64]*trade.Trade simulator *TradeSimulator profit float64 winningTrades int losingTrades int fee float64 } func NewBacktestAccount(cash float64, simulator *TradeSimulator) *BacktestAccount { return &BacktestAccount{ cash: cash, positions: make(map[int64]*trade.Position), trades: make(map[int64]*trade.Trade), closeTrades: make(map[int64]*trade.Trade), simulator: simulator, } } // CurrentEquity 根据当前价格对仓位进行 mark-to-market,返回账户净值 func (a *BacktestAccount) 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 *BacktestAccount) CurrentExposure() float64 { var sum float64 for _, p := range a.positions { sum += (p.Qty * p.EntryPx) } return sum } // CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct) // func (a *BacktestAccount) 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 // } // 获取未平仓交易单 func (a *BacktestAccount) OpenPositions() map[int64]*trade.Position { return a.positions } // 获取未平仓交易单数 func (a *BacktestAccount) CountOpenPositions() int { return len(a.positions) } // 订单下单 func (a *BacktestAccount) TradeOrder(ta trade.TradeArg) (ok bool, cause trade.Cause, err error) { order, ok := a.simulator.ExecuteMarket(ta.Side, ta.Qty, ta.Price, ta.Time) if !ok { return } // apply cash/position, short side also need earnest money cost := order.Price*order.Qty + order.Fee if cost > a.cash { ok = false return } a.cash -= cost // open/add position pos := &trade.Position{TradeId: order.Id, Side: order.Side, Qty: order.Qty, EntryPx: order.Price, EntryTs: order.Time, PeakPx: order.Price, Fee: order.Fee} a.positions[order.Id] = pos a.trades[order.Id] = order ok = true return } // 将仓位进行平仓 func (a *BacktestAccount) ClosePosition(pos *trade.Position, kline types.Kline, cause trade.Cause) (err error) { closeSide := lang.Ternary(pos.Side == types.SideLong, types.SideShort, types.SideLong) closePrice := decimals.MustToFloat64(kline.Close) t, ok := a.simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, kline.Ts) if !ok { err = fmt.Errorf("close position error: %#v", pos) return } 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.winningTrades++ } else { a.losingTrades++ } a.fee += t.Fee // remove position delete(a.positions, pos.TradeId) a.closeTrades[t.Id] = t if trade, ok := a.trades[pos.TradeId]; 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.Time)*time.Millisecond, ".") } return }