Browse Source

backtest

main
strange 10 months ago
parent
commit
bbac5c3e6c
  1. 23
      internal/trading/backtest/account.go
  2. 75
      internal/trading/backtest/backtest.go
  3. 23
      internal/trading/backtest/close_manager.go
  4. 11
      internal/trading/backtest/simulator.go
  5. 8
      internal/trading/backtest/types.go
  6. 19
      pkg/trade/risk_strategy.go
  7. 4
      pkg/types/signal.go

23
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

75
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,19 +99,56 @@ 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)
}
}
}
// 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
}
zlog.Debugf("recv=%d, total=%d, use %s", recvTimes, total, watch.ElapsedFmt("."))
// 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: 回测的交易产品/周期/时间区间
@ -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

23
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"})
}
}

11
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

8
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
@ -16,7 +14,7 @@ type Position struct {
type Trade struct {
Id int64 // 交易id
Side pb.Side // 交易方向
Side types.Side // 交易方向
Status int32 // 1.交易中 2.持仓中 3.已平仓
Qty float64 // 交易量
Price float64 // 开仓价格

19
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
}

4
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:

Loading…
Cancel
Save