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.
 
 

265 lines
7.0 KiB

package backtest
import (
"context"
"fmt"
"io"
"sig-pub/api/pb"
"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"
"sig-pub/pkg/zlog"
"google.golang.org/grpc"
)
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 {
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) {
var sim *Simulator
_ = sim
plan := tradingPlan.Plan
exchange := pb.ExchangeType(plan.Exchange)
interval := types.Interval(plan.Interval)
instId := plan.InstId
sigStrategy := tradingPlan.GetSigStrategy()
maxWindow := sigStrategy.MaxWindow()
if maxWindow < 0 || maxWindow > indicator.MaxWindow {
err = fmt.Errorf("invalid window %d 0-%d, planId=%d", maxWindow, indicator.MaxWindow, plan.Id)
return
}
seriesRange := &pb.SeriesRange{
Exchange: exchange,
InstId: instId,
Interval: string(interval),
Before: stime,
After: etime,
Open: false,
Live: false,
Desc: false,
Window: uint32(maxWindow),
}
// 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
}
recvTimes, total := 0, 0
watch := times.NewWatch()
var lastK *types.Kline
for {
msg, err0 := stream.Recv()
if err0 == io.EOF {
break
}
if err0 != nil {
err = err0
return
}
recvTimes++
total += len(msg.Klines)
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
}
length := sigKlineSeries.Length()
if length <= maxWindow {
continue
}
// 平仓策略
b.closeManager.OnKline(*kline, b.account)
sigSide := tradingPlan.Update(strategy.StrategyTypeSig)
if sigSide.IsValid() {
b.onSigSideSignal(sigSide, *kline)
}
}
}
// 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)
// params: 策略参数
func (b *Backtest) Run0(ctx context.Context, seriesRange *pb.SeriesRange, sigStrategy strategy.ISigStrategy, params strategy.StrategyParam, 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, nil)
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 == types.SideBuy || side == types.SideSell {
// signal-based close: close opposite positions first
if cm != nil {
cm.CloseBySignal(types.SideBuy, acct, klines[i])
}
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)
}
}
}
// 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 == types.SideBuy {
equity += (last - p.EntryPx) * p.Qty
} else {
equity += (p.EntryPx - last) * p.Qty
}
}
res.Equity = equity
}
return
}