diff --git a/internal/trading/backtest/account.go b/internal/trading/backtest/account.go index b094c5f..0c1aaf2 100644 --- a/internal/trading/backtest/account.go +++ b/internal/trading/backtest/account.go @@ -2,7 +2,6 @@ package backtest import ( "math" - "sig-pub/api/pb" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" ) @@ -34,9 +33,9 @@ func (a *Account) SetRiskLimits(maxPosPct, maxExposurePct float64) { func (a *Account) CurrentEquity(price float64) float64 { equity := a.Cash for _, p := range a.Positions { - if p.Side == pb.Side_BUY { + if p.Side == types.SideBuy { equity += (price - p.EntryPx) * p.Qty - } else if p.Side == pb.Side_SELL { + } else if p.Side == types.SideSell { equity += (p.EntryPx - price) * p.Qty } } @@ -53,7 +52,7 @@ func (a *Account) CurrentExposure(price float64) float64 { } // CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct) -func (a *Account) CanOpen(side pb.Side, qty, price float64) bool { +func (a *Account) CanOpen(side types.Side, qty, price float64) bool { if qty <= 0 || price <= 0 { return false } @@ -78,7 +77,7 @@ func (a *Account) CanOpen(side pb.Side, qty, price float64) bool { } // ApplyMarketOrder 直接用市价下单(简化),qty为基础货币数量 -func (a *Account) ApplyMarketOrder(side pb.Side, qty float64, klineTs int64, kline types.Kline) (t Trade, ok bool) { +func (a *Account) ApplyMarketOrder(side types.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) { @@ -86,7 +85,7 @@ func (a *Account) ApplyMarketOrder(side pb.Side, qty float64, klineTs int64, kli } trade := a.Simulator.ExecuteMarket(side, qty, kline, klineTs) // apply cash/position - if side == pb.Side_BUY { + if side == types.SideBuy { cost := trade.Price*qty + trade.Fee if cost > a.Cash { return trade, false @@ -95,7 +94,7 @@ func (a *Account) ApplyMarketOrder(side pb.Side, qty float64, klineTs int64, kli // 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 { + } else if side == types.SideSell { // simplify: allow short by increasing cash receive := trade.Price*qty - trade.Fee a.Cash += receive @@ -116,17 +115,17 @@ func (a *Account) ClosePosition(index int, kline types.Kline, ts int64, cause st return t, false } // determine close side (opposite) - var closeSide pb.Side - if pos.Side == pb.Side_BUY { - closeSide = pb.Side_SELL + var closeSide types.Side + if pos.Side == types.SideBuy { + closeSide = types.SideSell } else { - closeSide = pb.Side_BUY + closeSide = types.SideBuy } t = a.Simulator.ExecuteMarket(closeSide, pos.Qty, kline, ts) t.CloseCause = cause // apply cash change - if closeSide == pb.Side_SELL { + if closeSide == types.SideSell { // selling a long position -> receive cash receive := t.Price*pos.Qty - t.Fee a.Cash += receive diff --git a/internal/trading/backtest/backtest.go b/internal/trading/backtest/backtest.go index 337081c..a979e70 100644 --- a/internal/trading/backtest/backtest.go +++ b/internal/trading/backtest/backtest.go @@ -8,6 +8,7 @@ import ( "sig-pub/internal/trading/sig" "sig-pub/pkg/indicator" "sig-pub/pkg/strategy" + "sig-pub/pkg/trade" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" "sig-pub/pkg/utils/times" @@ -20,10 +21,22 @@ type Backtest struct { exchangeClient pb.ExchangeServiceClient indReg *indicator.IndicatorRegistry sigStrategyReg *strategy.SigStrategyRegistry + + account *Account + riskStrategy *trade.RiskStrategy + closeManager *CloseManager } func NewBacktest(exchangeClient pb.ExchangeServiceClient, indReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry) *Backtest { - return &Backtest{exchangeClient: exchangeClient, indReg: indReg} + account := NewAccount(10000, NewSimulator(0.0005, 0.0008)) + cm := NewCloseManager(0.02, 0.03) + return &Backtest{ + exchangeClient: exchangeClient, + indReg: indReg, + riskStrategy: trade.NewRiskStrategy(), + account: account, + closeManager: cm, + } } func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingPlan, stime, etime int64, sigKlineSeries *sig.KlineSeries) (err error) { @@ -61,6 +74,7 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP recvTimes, total := 0, 0 watch := times.NewWatch() + var lastK *types.Kline for { msg, err0 := stream.Recv() if err0 == io.EOF { @@ -75,7 +89,7 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP for _, k := range msg.Klines { kline := new(types.Kline) kline.ParsePBKline(seriesRange.Exchange, k) - + lastK = kline if lastTs, serial := sigKlineSeries.Update(kline); !serial { err = fmt.Errorf("kline not series: %s(%s), interval=%s, lastTs=%d", instId, exchange, interval, lastTs) return @@ -85,20 +99,57 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP continue } - signalSide := tradingPlan.Update(strategy.StrategyTypeSig) - switch signalSide { - case types.SideBuy, types.SideSell: - k, _ := sigKlineSeries.Get(0) - _ = k - // zlog.Infof("sigSide: ts=%d, %s", k.Ts, sigSide) + // 平仓策略 + b.closeManager.OnKline(*kline, b.account) + + sigSide := tradingPlan.Update(strategy.StrategyTypeSig) + if sigSide.IsValid() { + b.onSigSideSignal(sigSide, *kline) } } } - zlog.Debugf("recv=%d, total=%d, use %s", recvTimes, total, watch.ElapsedFmt(".")) + // build result + res := &BacktestResult{} + res.StartTs = 0 + res.EndTs = 0 + acct := b.account + res.Trades = acct.Trades + for _, p := range acct.Positions { + res.Positions = append(res.Positions, *p) + } + res.Cash = acct.Cash + // estimate equity using last close price + if lastK != nil { + last := decimals.MustToFloat64(lastK.Close) + equity := acct.Cash + // naive mark-to-market of positions + for _, p := range acct.Positions { + if p.Side == types.SideBuy { + equity += (last - p.EntryPx) * p.Qty + } else { + equity += (p.EntryPx - last) * p.Qty + } + } + res.Equity = equity + } + zlog.Debugf("recv=%d, total=%d, use %s, ret=%#v", recvTimes, total, watch.ElapsedFmt("."), res) return } +// onSigSideSignal 交易策略发出交易信号 +func (b *Backtest) onSigSideSignal(sigSide types.Side, k types.Kline) { + side := b.riskStrategy.SideAssess(sigSide) + if !side.IsValid() { + zlog.Debugf("risk strategy filter sig side: %s", sigSide.String()) + return + } + zlog.Debugf("apply market order: ts=%d, side=%s", k.Ts, side.String()) + b.account.ApplyMarketOrder(side, 0.01, k.Ts, k) + + b.closeManager.CloseBySignal(types.SideBuy, b.account, k) +} + // Run 执行回测 // seriesRange: 回测的交易产品/周期/时间区间 // sigStrategy: 已创建的策略实例(将调用 New() 并 Init) @@ -178,9 +229,9 @@ func (b *Backtest) Run0(ctx context.Context, seriesRange *pb.SeriesRange, sigStr if side == types.SideBuy || side == types.SideSell { // signal-based close: close opposite positions first if cm != nil { - cm.CloseBySignal(pb.Side_BUY, acct, klines[i]) + cm.CloseBySignal(types.SideBuy, acct, klines[i]) } - if _, ok := acct.ApplyMarketOrder(pb.Side_BUY, qty, klines[i].Ts, klines[i]); ok { + if _, ok := acct.ApplyMarketOrder(types.SideBuy, qty, klines[i].Ts, klines[i]); ok { // trade recorded } else { zlog.Debugf("order rejected or insufficient cash at ts=%d", klines[i].Ts) @@ -202,7 +253,7 @@ func (b *Backtest) Run0(ctx context.Context, seriesRange *pb.SeriesRange, sigStr equity := acct.Cash // naive mark-to-market of positions for _, p := range acct.Positions { - if p.Side == pb.Side_BUY { + if p.Side == types.SideBuy { equity += (last - p.EntryPx) * p.Qty } else { equity += (p.EntryPx - last) * p.Qty diff --git a/internal/trading/backtest/close_manager.go b/internal/trading/backtest/close_manager.go index 002c883..db800ee 100644 --- a/internal/trading/backtest/close_manager.go +++ b/internal/trading/backtest/close_manager.go @@ -1,7 +1,6 @@ package backtest import ( - "sig-pub/api/pb" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" ) @@ -52,16 +51,16 @@ func (m *CloseManager) OnKline(k types.Kline, acct *Account) (trades []Trade) { // update peak px high := decimals.MustToFloat64(k.High) low := decimals.MustToFloat64(k.Low) - if p.Side == pb.Side_BUY { + if p.Side == types.SideBuy { if high > p.PeakPx { p.PeakPx = high } - } else if p.Side == pb.Side_SELL { + } else if p.Side == types.SideSell { if low < p.PeakPx { p.PeakPx = low } } - if p.Side == pb.Side_BUY { + if p.Side == types.SideBuy { // stoploss if m.StopLossPct > 0 && priceLow <= entry*(1-m.StopLossPct) { toClose = append(toClose, closeTask{idx: i, cause: "stoploss"}) @@ -97,7 +96,7 @@ func (m *CloseManager) OnKline(k types.Kline, acct *Account) (trades []Trade) { } } } - } else if p.Side == pb.Side_SELL { + } else if p.Side == types.SideSell { // short: stoploss if high >= entry*(1+stop), takeprofit if low <= entry*(1-tp) if m.StopLossPct > 0 && priceHigh >= entry*(1+m.StopLossPct) { toClose = append(toClose, closeTask{idx: i, cause: "stoploss"}) @@ -141,11 +140,11 @@ func (m *CloseManager) OnKline(k types.Kline, acct *Account) (trades []Trade) { } // perform market close: side opposite pos := acct.Positions[idx] - var closeSide pb.Side - if pos.Side == pb.Side_BUY { - closeSide = pb.Side_SELL + var closeSide types.Side + if pos.Side == types.SideBuy { + closeSide = types.SideSell } else { - closeSide = pb.Side_BUY + closeSide = types.SideBuy } tr, ok := acct.ClosePosition(idx, k, k.Ts, cause) if ok { @@ -157,7 +156,7 @@ func (m *CloseManager) OnKline(k types.Kline, acct *Account) (trades []Trade) { } // CloseBySignal 根据策略信号尝试平掉相反方向的仓位。例如策略返回 SELL 时,尝试平掉所有 BUY 持仓 -func (m *CloseManager) CloseBySignal(sigSide pb.Side, acct *Account, k types.Kline) (trades []Trade) { +func (m *CloseManager) CloseBySignal(sigSide types.Side, acct *Account, k types.Kline) (trades []Trade) { if acct == nil { return } @@ -171,9 +170,9 @@ func (m *CloseManager) CloseBySignal(sigSide pb.Side, acct *Account, k types.Kli if p == nil { continue } - if sigSide == pb.Side_BUY && p.Side == pb.Side_SELL { + if sigSide == types.SideBuy && p.Side == types.SideSell { toClose = append(toClose, closeTask{idx: i, cause: "signal"}) - } else if sigSide == pb.Side_SELL && p.Side == pb.Side_BUY { + } else if sigSide == types.SideSell && p.Side == types.SideBuy { toClose = append(toClose, closeTask{idx: i, cause: "signal"}) } } diff --git a/internal/trading/backtest/simulator.go b/internal/trading/backtest/simulator.go index b2af07b..92b3dd0 100644 --- a/internal/trading/backtest/simulator.go +++ b/internal/trading/backtest/simulator.go @@ -2,7 +2,6 @@ package backtest import ( "math" - "sig-pub/api/pb" "sig-pub/pkg/types" "sig-pub/pkg/types/decimals" ) @@ -17,11 +16,11 @@ func NewSimulator(feePct, slippagePct float64) *Simulator { } // ExecuteMarket 执行市价单,使用kline信息决定成交价(使用close以及滑点) -func (s *Simulator) ExecuteMarket(side pb.Side, qty float64, k types.Kline, ts int64) (trade Trade) { +func (s *Simulator) ExecuteMarket(side types.Side, qty float64, k types.Kline, ts int64) (trade Trade) { // base price use close base := decimals.MustToFloat64(k.Close) slippage := s.SlippagePct - if side == pb.Side_SELL { + if side == types.SideSell { // sell: worse price lower base = base * (1 - slippage) } else { @@ -34,10 +33,10 @@ func (s *Simulator) ExecuteMarket(side pb.Side, qty float64, k types.Kline, ts i } // ExecuteLimit 简单实现: 如果limit价格被kline的high/low包含则成交 -func (s *Simulator) ExecuteLimit(side pb.Side, qty float64, limitPx float64, k types.Kline, ts int64) (filled bool, trade Trade) { +func (s *Simulator) ExecuteLimit(side types.Side, qty float64, limitPx float64, k types.Kline, ts int64) (filled bool, trade Trade) { h := decimals.MustToFloat64(k.High) l := decimals.MustToFloat64(k.Low) - if side == pb.Side_BUY { + if side == types.SideBuy { // buy limit: filled if low <= price if l <= limitPx { // assume filled at min(limitPx, open) @@ -46,7 +45,7 @@ func (s *Simulator) ExecuteLimit(side pb.Side, qty float64, limitPx float64, k t trade = Trade{Side: side, Qty: qty, Price: px * (1 + s.SlippagePct), Fee: fee, Ts: ts} return true, trade } - } else if side == pb.Side_SELL { + } else if side == types.SideSell { if h >= limitPx { px := math.Max(limitPx, decimals.MustToFloat64(k.Open)) fee := math.Abs(px*qty) * s.FeePct diff --git a/internal/trading/backtest/types.go b/internal/trading/backtest/types.go index 40cfb53..ff7d6e5 100644 --- a/internal/trading/backtest/types.go +++ b/internal/trading/backtest/types.go @@ -1,10 +1,8 @@ package backtest -import ( - "sig-pub/api/pb" -) +import "sig-pub/pkg/types" -type Side = pb.Side +type Side = types.Side type Position struct { Side Side @@ -15,17 +13,17 @@ type Position struct { } type Trade struct { - Id int64 // 交易id - Side pb.Side // 交易方向 - Status int32 // 1.交易中 2.持仓中 3.已平仓 - Qty float64 // 交易量 - Price float64 // 开仓价格 - Fee float64 // 开仓手续费 - Ts int64 // 开仓时间 - ClosePrice float64 // 平仓价格 - CloseFee float64 // 平仓手续费 - CloseTs int64 // 平仓时间 - CloseCause string // 平仓原因 ["stoploss", "takeprofit", "trailing", "retrace", "signal"](“止损”、“止盈”、“动态跟踪”、“回撤”、“信号”) + Id int64 // 交易id + Side types.Side // 交易方向 + Status int32 // 1.交易中 2.持仓中 3.已平仓 + Qty float64 // 交易量 + Price float64 // 开仓价格 + Fee float64 // 开仓手续费 + Ts int64 // 开仓时间 + ClosePrice float64 // 平仓价格 + CloseFee float64 // 平仓手续费 + CloseTs int64 // 平仓时间 + CloseCause string // 平仓原因 ["stoploss", "takeprofit", "trailing", "retrace", "signal"](“止损”、“止盈”、“动态跟踪”、“回撤”、“信号”) } type BacktestResult struct { diff --git a/pkg/trade/risk_strategy.go b/pkg/trade/risk_strategy.go index ff20ed6..ab4473a 100644 --- a/pkg/trade/risk_strategy.go +++ b/pkg/trade/risk_strategy.go @@ -1,8 +1,6 @@ package trade import ( - "sig-pub/api/pb" - "sig-pub/pkg/indicator" "sig-pub/pkg/types" ) @@ -10,18 +8,17 @@ type IRickStrategy interface { OnSingle(signalSide types.Side) } -// RickStrategy 风险管理策略 -type RickStrategy struct { - indicatorCtx indicator.IIndicatorContext +// RiskStrategy 风险管理策略 +type RiskStrategy struct { } -// OnSingle 收到信号时进行评估, 返回过滤后的交易信号 +func NewRiskStrategy() *RiskStrategy { + return &RiskStrategy{} +} + +// SideAssess 收到信号时进行评估, 返回过滤后的交易信号 // todo 对交易方向进行信心分数评估, 后续开仓仓位 -func (s *RickStrategy) OnSingle(signalSide pb.Side) (side pb.Side) { - k := s.indicatorCtx.Get(0) - price := k.Close - ts := k.Ts - _, _ = price, ts +func (s *RiskStrategy) SideAssess(signalSide types.Side) (side types.Side) { return signalSide } diff --git a/pkg/types/signal.go b/pkg/types/signal.go index 59f77bd..efe20b0 100644 --- a/pkg/types/signal.go +++ b/pkg/types/signal.go @@ -7,6 +7,10 @@ const ( SideSell Side = 2 // SELL ) +func (side Side) IsValid() bool { + return side == SideBuy || side == SideSell +} + func (side Side) String() string { switch side { default: