You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
3.6 KiB
138 lines
3.6 KiB
package backtest |
|
|
|
import ( |
|
"context" |
|
"io" |
|
"sig-pub/api/pb" |
|
"sig-pub/pkg/indicator" |
|
"sig-pub/pkg/strategy" |
|
"sig-pub/pkg/types" |
|
"sig-pub/pkg/types/decimals" |
|
"sig-pub/pkg/zlog" |
|
|
|
"google.golang.org/grpc" |
|
) |
|
|
|
type Backtest struct { |
|
exchangeClient pb.ExchangeServiceClient |
|
indReg *indicator.IndicatorRegistry |
|
sim *Simulator |
|
} |
|
|
|
func NewBacktest(exchangeClient pb.ExchangeServiceClient, indReg *indicator.IndicatorRegistry, sim *Simulator) *Backtest { |
|
return &Backtest{exchangeClient: exchangeClient, indReg: indReg, sim: sim} |
|
} |
|
|
|
// Run 执行回测 |
|
// seriesRange: 回测的交易产品/周期/时间区间 |
|
// sigStrategy: 已创建的策略实例(将调用 New() 并 Init) |
|
// params: 策略参数 |
|
func (b *Backtest) Run(ctx context.Context, seriesRange *pb.SeriesRange, sigStrategy strategy.ISigStrategy, params strategy.SigStrategyParam, initialCash float64) (res BacktestResult, err error) { |
|
// prepare strategy |
|
strat := sigStrategy.New() |
|
if err = strat.Init(params); err != nil { |
|
return |
|
} |
|
|
|
// fetch history klines via stream |
|
req := &pb.ReqHistoryKlineStream{Series: seriesRange} |
|
stream, err := b.exchangeClient.HistoryKlineStream(context.Background(), req, grpc.UseCompressor("snappy")) |
|
if err != nil { |
|
return |
|
} |
|
|
|
var klines []types.Kline |
|
var firstTs, lastTs int64 |
|
for { |
|
msg, err0 := stream.Recv() |
|
if err0 == io.EOF { |
|
break |
|
} |
|
if err0 != nil { |
|
err = err0 |
|
return |
|
} |
|
for _, k := range msg.Klines { |
|
kk := new(types.Kline) |
|
kk.ParsePBKline(seriesRange.Exchange, k) |
|
klines = append(klines, *kk) |
|
if firstTs == 0 { |
|
firstTs = kk.Ts |
|
} |
|
lastTs = kk.Ts |
|
} |
|
} |
|
|
|
// prepare account and set risk limits from params if provided |
|
acct := NewAccount(initialCash, b.sim) |
|
if v, ok := params.GetFloat64("max_pos_pct"); ok && v > 0 { |
|
acct.MaxPosPct = v |
|
} |
|
if v, ok := params.GetFloat64("max_exposure_pct"); ok && v > 0 { |
|
acct.MaxExposurePct = v |
|
} |
|
|
|
// prepare close manager from params |
|
var cm *CloseManager |
|
if sl, ok := params.GetFloat64("stoploss_pct"); ok || true { |
|
tp, _ := params.GetFloat64("takeprofit_pct") |
|
cm = NewCloseManager(sl, tp) |
|
} |
|
|
|
// iterate klines in chronological order |
|
for i := 0; i < len(klines); i++ { |
|
// build context with klines up to i |
|
window := klines[:i+1] |
|
ctxSig := NewSigStrategyContext(window, b.indReg) |
|
// first, evaluate stoploss/takeprofit on this kline |
|
if cm != nil { |
|
cm.OnKline(klines[i], acct) |
|
} |
|
side := strat.Update(ctxSig) |
|
// simple position sizing: param 'size' as fraction of cash; else use fixed qty 1 |
|
sizePct, ok := params.GetFloat64("size") |
|
var qty float64 |
|
if ok && sizePct > 0 { |
|
price := decimals.MustToFloat64(klines[i].Close) |
|
qty = (acct.Cash * sizePct) / price |
|
} else { |
|
qty = 1 |
|
} |
|
|
|
if side == pb.Side_BUY || side == pb.Side_SELL { |
|
// signal-based close: close opposite positions first |
|
if cm != nil { |
|
cm.CloseBySignal(side, acct, klines[i]) |
|
} |
|
if _, ok := acct.ApplyMarketOrder(side, qty, klines[i].Ts, klines[i]); ok { |
|
// trade recorded |
|
} else { |
|
zlog.Debugf("order rejected or insufficient cash at ts=%d", klines[i].Ts) |
|
} |
|
} |
|
} |
|
|
|
// build result |
|
res.StartTs = firstTs |
|
res.EndTs = lastTs |
|
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 len(klines) > 0 { |
|
last := decimals.MustToFloat64(klines[len(klines)-1].Close) |
|
equity := acct.Cash |
|
// naive mark-to-market of positions |
|
for _, p := range acct.Positions { |
|
if p.Side == pb.Side_BUY { |
|
equity += (last - p.EntryPx) * p.Qty |
|
} else { |
|
equity += (p.EntryPx - last) * p.Qty |
|
} |
|
} |
|
res.Equity = equity |
|
} |
|
return |
|
}
|
|
|