package backtest import ( "math" "sig-pub/pkg/trade" "sig-pub/pkg/types" "sort" ) type BacktestAccount struct { trade.ITradeAccount cash float64 initialCash float64 positions map[int64]*trade.Position trades map[int64]*Trade closeTrades map[int64]*Trade simulator *TradeSimulator profit float64 winningTrades int losingTrades int fee float64 maxDrawdown [4]float64 // 最大回撤 } func NewBacktestAccount(cash float64, simulator *TradeSimulator) *BacktestAccount { return &BacktestAccount{ // keep initial cash for return calculations initialCash: cash, cash: cash, positions: make(map[int64]*trade.Position), trades: make(map[int64]*Trade), closeTrades: make(map[int64]*Trade), simulator: simulator, maxDrawdown: [4]float64{cash, cash, cash, cash}, } } // initialCash stores the starting capital for return calculations // (placed here to avoid changing exported API) func (a *BacktestAccount) InitialCash() float64 { return a.initialCash } // SharpeRatio computes an annualized Sharpe ratio based on closed trades. // rfAnnual is the annual risk-free rate expressed as a decimal (e.g. 0.01 for 1%). // Method: // - For each closed trade, we compute a period return = trade.Pnl / initialCash. // - Period lengths are derived from successive trade close timestamps (ms). // - Excess returns = periodReturn - rfAnnual * periodYears. // - Sharpe = mean(excess) / stddev(excess) * sqrt(periodsPerYear) // This provides a reasonable approximation when equity snapshots are not available. func (a *BacktestAccount) SharpeRatio(rfAnnual float64) float64 { if a.initialCash <= 0 { return 0 } n := len(a.closeTrades) if n < 2 { return 0 } trades := make([]*Trade, 0, n) for _, t := range a.closeTrades { trades = append(trades, t) } sort.Slice(trades, func(i, j int) bool { return trades[i].CloseTime < trades[j].CloseTime }) // returns per closed trade (relative to initial capital) returns := make([]float64, 0, n) // periods in seconds between closes; length will be n-1 initially periodsSec := make([]float64, 0, n-1) for i, t := range trades { returns = append(returns, t.Pnl/a.initialCash) if i > 0 { // CloseTs is in milliseconds in this codebase dtSec := float64(t.CloseTime-trades[i-1].CloseTime) / 1000.0 if dtSec <= 0 { dtSec = 1.0 } periodsSec = append(periodsSec, dtSec) } } if len(periodsSec) == 0 { return 0 } // average period (seconds) used to approximate period length for the first return sumDt := 0.0 for _, d := range periodsSec { sumDt += d } avgDt := sumDt / float64(len(periodsSec)) // Build final periods slice aligned with returns length periods := make([]float64, 0, n) periods = append(periods, avgDt) periods = append(periods, periodsSec...) const secsYear = 365.0 * 24.0 * 3600.0 excess := make([]float64, len(returns)) for i := range returns { years := periods[i] / secsYear excess[i] = returns[i] - rfAnnual*years } meanEx := mean(excess) sd := stddev(excess) if sd == 0 { return 0 } // approximate number of periods per year periodsPerYear := secsYear / avgDt return meanEx / sd * math.Sqrt(periodsPerYear) } func mean(x []float64) float64 { if len(x) == 0 { return 0 } s := 0.0 for _, v := range x { s += v } return s / float64(len(x)) } func stddev(x []float64) float64 { if len(x) <= 1 { return 0 } m := mean(x) s := 0.0 for _, v := range x { d := v - m s += d * d } // population or sample? use sample (n-1) return math.Sqrt(s / float64(len(x)-1)) } // 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 equity += price * p.Qty case types.SideShort: // equity += (p.EntryPx - price) * p.Qty equity += (p.EntryPx - price + p.EntryPx) * 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) GetOpenTrades() map[int64]*trade.Position { return a.positions } // 获取未平仓交易单数 func (a *BacktestAccount) CountOpenPositions() int { return len(a.positions) } // 订单下单 func (a *BacktestAccount) TradeOrder(ta trade.TradeTicket) (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.ClosePrice = closePrice // trade.CloseFee = t.Fee // trade.CloseTime = kline.Ts // trade.CloseCause = cause // trade.Pnl = profit // trade.HoldTime = conver.TimeDurationFormat(time.Duration(trade.CloseTime-trade.Time)*time.Millisecond, ".") // trade.PeakPx = pos.PeakPx // trade.Cash = a.CurrentEquity(closePrice) // } // // 记录最大回撤 [high, low, high, low] // currentEquity := a.CurrentEquity(closePrice) // if currentEquity < a.maxDrawdown[1] { // a.maxDrawdown[1] = currentEquity // } // if currentEquity > a.maxDrawdown[0] { // if a.maxDrawdown[0]-a.maxDrawdown[1] > a.maxDrawdown[2]-a.maxDrawdown[3] { // a.maxDrawdown[2], a.maxDrawdown[3] = a.maxDrawdown[0], a.maxDrawdown[1] // } // a.maxDrawdown[0] = currentEquity // a.maxDrawdown[1] = currentEquity // } return }