Browse Source

backtest trade

main
strange 10 months ago
parent
commit
3d7a91eb71
  1. 4
      config/exchange.toml
  2. 194
      internal/trading/backtest/account.go
  3. 209
      internal/trading/backtest/close_manager.go
  4. 42
      internal/trading/backtest/sig_strategy_backtester.go
  5. 25
      internal/trading/backtest/trade_simulator.go
  6. 239
      internal/trading/backtest/trading_plan_backtester.go
  7. 42
      internal/trading/backtest/types.go
  8. 2
      internal/trading/trading_grpc_server.go
  9. 29
      internal/trading/trading_service.go
  10. 26
      pkg/data/entity/trade_plan.go
  11. 95
      pkg/trade/close_strategy.go
  12. 5
      pkg/trade/risk_strategy.go
  13. 44
      pkg/trade/trade_account.go
  14. 45
      pkg/trade/trade_strategy.go
  15. 94
      pkg/trade/types.go
  16. 36
      pkg/types/interval.go

4
config/exchange.toml

@ -18,8 +18,8 @@ receiveBuffer = 4096
marketSubscribeLimit = 16
consumeBatch = 1024
consumeLater = 2000 # 时间到达later或者数据累计到batch触发consume
httpProxy = "http://192.168.1.5:7890"
# httpProxy = "http://10.255.183.209:7890"
# httpProxy = "http://192.168.1.5:7890"
httpProxy = "http://10.255.183.209:7890"
# 模拟盘API交易地址如下:
# REST:https://www.okx.com

194
internal/trading/backtest/account.go

@ -1,48 +1,43 @@
package backtest
import (
"math"
"fmt"
"sig-pub/pkg/trade"
"sig-pub/pkg/types"
"sig-pub/pkg/types/decimals"
"sig-pub/pkg/utils/collect"
"sig-pub/pkg/utils/conver"
"sig-pub/pkg/utils/lang"
"time"
)
type Account struct {
Cash float64
Positions []*Position
Trades []*Trade // 交易订单利润分布,回测结果echart折线显示,评估止盈止损策略效果
CloseTrades []*Trade
MaxPosPct float64 // 最大仓位占比
MaxExposurePct float64 // 最大总敞口占比
MaxLots float64 // 最大手数/数量 (optional)
Simulator *Simulator
type BacktestAccount struct {
trade.ITradeAccount
cash float64
positions map[int64]*trade.Position
trades map[int64]*trade.Trade
closeTrades map[int64]*trade.Trade
simulator *TradeSimulator
Stat *TradeStat // 交易统计
Profit float64
profit float64
winningTrades int
losingTrades int
fee float64
}
func NewAccount(cash float64, sim *Simulator) *Account {
return &Account{
Cash: cash, Simulator: sim, MaxPosPct: 1.0, MaxExposurePct: 1.0,
Stat: &TradeStat{},
}
}
func (a *Account) SetRiskLimits(maxPosPct, maxExposurePct float64) {
if maxPosPct > 0 {
a.MaxPosPct = maxPosPct
}
if maxExposurePct > 0 {
a.MaxExposurePct = maxExposurePct
func NewBacktestAccount(cash float64, simulator *TradeSimulator) *BacktestAccount {
return &BacktestAccount{
cash: cash,
positions: make(map[int64]*trade.Position),
trades: make(map[int64]*trade.Trade),
closeTrades: make(map[int64]*trade.Trade),
simulator: simulator,
}
}
// CurrentEquity 根据当前价格对仓位进行 mark-to-market,返回账户净值
func (a *Account) CurrentEquity(price float64) float64 {
equity := a.Cash
for _, p := range a.Positions {
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
@ -54,88 +49,80 @@ func (a *Account) CurrentEquity(price float64) float64 {
}
// CurrentExposure 返回当前仓位的名义总敞口(绝对值)
func (a *Account) CurrentExposure(price float64) float64 {
func (a *BacktestAccount) CurrentExposure() float64 {
var sum float64
for _, p := range a.Positions {
sum += math.Abs(p.Qty * price)
}
return sum
}
func (a *Account) PositionCost() float64 {
var sum float64
for _, p := range a.Positions {
sum += math.Abs(p.Qty * p.EntryPx)
for _, p := range a.positions {
sum += (p.Qty * p.EntryPx)
}
return sum
}
// CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct)
func (a *Account) 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
}
// ApplyMarketOrder 直接用市价下单(简化),qty为基础货币数量
func (a *Account) ApplyMarketOrder(side types.Side, qty float64, price float64, ts int64) (t *Trade, ok bool) {
if !a.CanOpen(side, qty, price) {
return t, false
}
trade, ok := a.Simulator.ExecuteMarket(side, qty, price, ts)
// 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) OpenPositions() map[int64]*trade.Position {
return a.positions
}
// 获取未平仓交易单数
func (a *BacktestAccount) CountOpenPositions() int {
return len(a.positions)
}
// 订单下单
func (a *BacktestAccount) TradeOrder(ta trade.TradeArg) (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 := trade.Price*qty + trade.Fee
if cost > a.Cash {
return trade, false
cost := order.Price*order.Qty + order.Fee
if cost > a.cash {
ok = false
return
}
a.Cash -= cost
a.cash -= cost
// open/add position
pos := &Position{TradeId: trade.Id, Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: ts, PeakPx: trade.Price, Fee: trade.Fee}
a.Positions = append(a.Positions, pos)
a.Trades = append(a.Trades, trade)
{
a.Stat.TotalTrades++
a.Stat.Fee += trade.Fee
}
return trade, true
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
}
// ClosePosition 根据索引平仓(全部平仓该仓位)
func (a *Account) ClosePosition(index int, pos *Position, kline types.Kline, ts int64, cause string) (t *Trade, ok bool) {
// determine close side (opposite)
var closeSide types.Side
if pos.Side == types.SideLong {
closeSide = types.SideShort
} else {
closeSide = types.SideLong
}
// 将仓位进行平仓
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, ts)
t, ok := a.simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, kline.Ts)
if !ok {
return t, false
err = fmt.Errorf("close position error: %#v", pos)
return
}
t.CloseCause = cause
@ -150,28 +137,27 @@ func (a *Account) ClosePosition(index int, pos *Position, kline types.Kline, ts
receive = pos.EntryPx*pos.Qty + profit - t.Fee
profit = profit - t.Fee - pos.Fee
}
a.Cash += receive
a.Profit += profit
a.cash += receive
a.profit += profit
if profit > 0 {
a.Stat.WinningTrades++
a.winningTrades++
} else {
a.Stat.LosingTrades++
a.losingTrades++
}
a.Stat.Fee += t.Fee
a.fee += t.Fee
// remove position
a.Positions = append(a.Positions[:index], a.Positions[index+1:]...)
a.CloseTrades = append(a.CloseTrades, t)
delete(a.positions, pos.TradeId)
a.closeTrades[t.Id] = t
trade, ok := collect.Find(a.Trades, func(t *Trade) bool { return t.Id == pos.TradeId })
if ok {
if trade, ok := a.trades[pos.TradeId]; ok {
trade.Pnl = profit
trade.ClosePrice = closePrice
trade.CloseFee = t.Fee
trade.CloseTs = kline.Ts
trade.CloseCause = cause
trade.HoldTime = conver.TimeDurationFormat(time.Duration(trade.CloseTs-trade.Ts)*time.Millisecond, ".")
trade.HoldTime = conver.TimeDurationFormat(time.Duration(trade.CloseTs-trade.Time)*time.Millisecond, ".")
}
return t, true
return
}

209
internal/trading/backtest/close_manager.go

@ -1,209 +0,0 @@
package backtest
import (
"sig-pub/pkg/trade"
"sig-pub/pkg/types"
"sig-pub/pkg/types/decimals"
)
// CloseManager 管理持仓平仓逻辑:stoploss/takeprofit 与 基于信号的平仓
// 1.交易信号和持单方向相反时是否进行平仓
// 2.计算止盈止损时是否包含手续费
type CloseManager struct {
StopLossPct float64 // static stoploss
TakeProfitPct float64 // static take profit
TrailingPct float64 // trailing stop percent (e.g. 0.02 = 2%);移动止损百分比(例如 0.02 表示从最高价回撤 2% 时触发追踪止损)。
MinProfitToTrail float64 // minimum profit (fraction) before trailing activates (e.g. 0.01 = 1%);启动移动止损的最小盈利阈值(例如达到 1% 后才开始追踪)。
ProfitRetracePct float64 // close when profit retraces more than this fraction of peak profit;基于最高利润回撤触发平仓(例如从最高利润回撤超过 30% 则平仓)。
}
func NewCloseManager(stopLossPct, takeProfitPct float64) *CloseManager {
return &CloseManager{StopLossPct: stopLossPct, TakeProfitPct: takeProfitPct}
}
func (m *CloseManager) SetDynamicParams(trailingPct, minProfitToTrail, profitRetracePct float64) {
m.TrailingPct = trailingPct
m.MinProfitToTrail = minProfitToTrail
m.ProfitRetracePct = profitRetracePct
}
func (m *CloseManager) Init(param trade.CloseStrategyParam) {
}
// OnKline 根据最新 kline 检查是否触发 stoploss 或 takeprofit,触发则平仓(市价)
// 返回发生的平仓成交记录
func (m *CloseManager) OnKline(k types.Kline, acct *Account) (trades []*Trade) {
if acct == nil {
return
}
if (m.StopLossPct <= 0) && (m.TakeProfitPct <= 0) {
return
}
// collect indices to close to avoid modifying slice during iteration
type closeTask struct {
tradeId int64
cause string
}
var toClose []closeTask
closePrice := decimals.MustToFloat64(k.Close)
for _, p := range acct.Positions {
if p == nil {
continue
}
entry := p.EntryPx
// update peak px
switch p.Side {
case types.SideLong:
if closePrice > p.PeakPx {
p.PeakPx = closePrice
}
case types.SideShort:
if closePrice < p.PeakPx {
p.PeakPx = closePrice
}
}
// 固定止盈止损
if p.Side == types.SideLong {
// stoploss
if m.StopLossPct > 0 && closePrice <= entry*(1-m.StopLossPct) {
acct.Stat.StoplossTimes++
toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "stoploss"})
continue
}
// takeprofit
// if m.TakeProfitPct > 0 && priceHigh >= entry*(1+m.TakeProfitPct) {
// acct.Stat.TakeprofitTimes++
// toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "takeprofit"})
// continue
// }
// dynamic trailing stop based on peak price
if m.TrailingPct > 0 && m.MinProfitToTrail > 0 {
// peak profit fraction
peakProfit := (p.PeakPx - entry) / entry
if peakProfit >= m.MinProfitToTrail { // 最高盈利百分比
// trailing level
// trailLevel := p.PeakPx * (1 - m.TrailingPct)
// if closePrice <= trailLevel {
trail := (p.PeakPx - entry) * (1 - m.TrailingPct)
if (closePrice - entry) < trail {
acct.Stat.TrailingTimes++
toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "trailing"})
continue
}
}
}
// profit retrace rule: if peakProfit>0 and current retrace > ProfitRetracePct
// if m.ProfitRetracePct > 0 {
// peakProfit := (p.PeakPx - entry) / entry
// curProfit := (closePrice - entry) / entry
// if peakProfit > 0 {
// retrace := (peakProfit - curProfit) / peakProfit
// if retrace >= m.ProfitRetracePct {
// acct.Stat.RetraceTimes++
// toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "retrace"})
// continue
// }
// }
// }
} else if p.Side == types.SideShort {
// short: stoploss if high >= entry*(1+stop), takeprofit if low <= entry*(1-tp)
if m.StopLossPct > 0 && closePrice >= entry*(1+m.StopLossPct) {
acct.Stat.StoplossTimes++
toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "stoploss"})
continue
}
// if m.TakeProfitPct > 0 && priceLow <= entry*(1-m.TakeProfitPct) {
// acct.Stat.TakeprofitTimes++
// toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "takeprofit"})
// continue
// }
// update trailing for short based on PeakPx (lower is better for short)
if m.TrailingPct > 0 && m.MinProfitToTrail > 0 {
peakProfit := (entry - p.PeakPx) / entry
if peakProfit >= m.MinProfitToTrail {
// trailLevel := p.PeakPx * (1 + m.TrailingPct)
// if closePrice >= trailLevel {
trail := (entry - p.PeakPx) * (1 - m.TrailingPct)
if (entry - closePrice) < trail {
acct.Stat.TrailingTimes++
toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "trailing"})
continue
}
}
}
// if m.ProfitRetracePct > 0 {
// peakProfit := (entry - p.PeakPx) / entry
// curProfit := (entry - closePrice) / entry
// if peakProfit > 0 {
// retrace := (peakProfit - curProfit) / peakProfit
// if retrace >= m.ProfitRetracePct {
// acct.Stat.RetraceTimes++
// toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "retrace"})
// continue
// }
// }
// }
}
}
// close collected positions (process from high index to low to safely remove)
for j := len(toClose) - 1; j >= 0; j-- {
tradeId := toClose[j].tradeId
cause := toClose[j].cause
var pos *Position
var index int
for i, p := range acct.Positions {
if p.TradeId == tradeId {
pos = p
index = i
break
}
}
if pos == nil {
return
}
// perform market close: side opposite
tr, ok := acct.ClosePosition(index, pos, k, k.Ts, cause)
if ok {
trades = append(trades, tr)
}
}
return
}
// CloseBySignal 根据策略信号尝试平掉相反方向的仓位。例如策略返回 SELL 时,尝试平掉所有 BUY 持仓
func (m *CloseManager) CloseBySignal(sigSide types.Side, acct *Account, k types.Kline) (trades []*Trade) {
if acct == nil {
return
}
// determine which positions to close: positions with opposite side to sigSide
type closeTask struct {
idx int
cause string
pos *Position
}
var toClose []closeTask
for i, p := range acct.Positions {
if p == nil {
continue
}
if sigSide == types.SideLong && p.Side == types.SideShort {
toClose = append(toClose, closeTask{idx: i, cause: "signal", pos: p})
} else if sigSide == types.SideShort && p.Side == types.SideLong {
toClose = append(toClose, closeTask{idx: i, cause: "signal", pos: p})
}
}
for j := len(toClose) - 1; j >= 0; j-- {
idx := toClose[j].idx
cause := toClose[j].cause
tr, ok := acct.ClosePosition(idx, toClose[j].pos, k, k.Ts, cause)
if ok {
trades = append(trades, tr)
}
}
return
}

42
internal/trading/backtest/sig_strategy_backtester.go

@ -25,7 +25,7 @@ type SigStrategyBacktester struct {
exchangeClient pb.ExchangeServiceClient
// 回测过程中订阅k线
intervalSubscribe map[types.Interval][]func(interval types.Interval, k *types.Kline) (err error)
intervalSubscribe map[types.Interval][]func(interval types.Interval, k types.Kline) (err error)
}
func NewSigStrategyBacktester(
@ -39,22 +39,25 @@ func NewSigStrategyBacktester(
sigStrategy: sigStrategy,
indicatorReg: indicatorReg,
exchangeClient: exchangeServiceClient,
intervalSubscribe: make(map[types.Interval][]func(interval types.Interval, k *types.Kline) (err error)),
intervalSubscribe: make(map[types.Interval][]func(interval types.Interval, k types.Kline) (err error)),
}
}
// SubKline 在回测过程中订阅k线
func (b *SigStrategyBacktester) SubKline(interval types.Interval, recv func(interval types.Interval, k *types.Kline) (err error)) {
func (b *SigStrategyBacktester) SubKline(interval types.Interval, recv func(interval types.Interval, k types.Kline) (err error)) {
b.intervalSubscribe[interval] = append(b.intervalSubscribe[interval], recv)
}
// Backtest 基于历史数据回测信号策略
func (b *SigStrategyBacktester) Backtest(ctx context.Context, sr *pb.SeriesRange, recvSignal func(sigSide types.Side, k types.Kline) (err error)) (err error) {
func (b *SigStrategyBacktester) Backtest(ctx context.Context, sr *pb.SeriesRange, cleanIntervalSeries *types.IntervalState[*sig.KlineSeries], recvSignal func(sigSide types.Side, k types.Kline) (err error)) (err error) {
if cleanIntervalSeries == nil {
cleanIntervalSeries = types.NewIntervalState[*sig.KlineSeries]()
}
switch b.sigStrategyType {
case strategy.SigStrategyTypeSingle:
err = b.singleStrategySeries(ctx, b.sigStrategy.(strategy.ISingleSigStrategy), sr, recvSignal)
err = b.singleStrategySeries(ctx, b.sigStrategy.(strategy.ISingleSigStrategy), sr, cleanIntervalSeries, recvSignal)
case strategy.SigStrategyTypeInterval:
err = b.intervalStrategySeries(ctx, b.sigStrategy.(strategy.IIntervalSigStrategy), sr, recvSignal)
err = b.intervalStrategySeries(ctx, b.sigStrategy.(strategy.IIntervalSigStrategy), sr, cleanIntervalSeries, recvSignal)
default:
err = fmt.Errorf("unknown sig strategy type %v", b.sigStrategyType)
}
@ -62,9 +65,9 @@ func (b *SigStrategyBacktester) Backtest(ctx context.Context, sr *pb.SeriesRange
}
// singleStrategySeries 单周期策略
func (b *SigStrategyBacktester) singleStrategySeries(ctx context.Context, sigStrategy strategy.ISingleSigStrategy, sr *pb.SeriesRange, recvSignal func(sigSide types.Side, k types.Kline) (err error)) (err error) {
func (b *SigStrategyBacktester) singleStrategySeries(ctx context.Context, sigStrategy strategy.ISingleSigStrategy, sr *pb.SeriesRange, intervalKlineSeries *types.IntervalState[*sig.KlineSeries], recvSignal func(sigSide types.Side, k types.Kline) (err error)) (err error) {
interval := types.Interval(sr.Interval)
kSeries := sig.NewKlineSeries(sr.Exchange, sr.InstId, interval)
kSeries := intervalKlineSeries.ComputeIfAbsent(interval, func() *sig.KlineSeries { return sig.NewKlineSeries(sr.Exchange, sr.InstId, interval) })
indicatorContext := sig.NewIndicatorContext(kSeries)
strategyContext := sig.NewStrategyContext(indicatorContext, b.indicatorReg)
requiredSeries := int(sigStrategy.RequiredSeries())
@ -72,9 +75,6 @@ func (b *SigStrategyBacktester) singleStrategySeries(ctx context.Context, sigStr
requiredIntervalSeries := types.NewIntervalState[int16]()
requiredIntervalSeries.Set(interval, int16(max(1, requiredSeries)))
intervalKlineSeries := types.NewIntervalState[*sig.KlineSeries]()
intervalKlineSeries.Set(interval, kSeries)
err = b.multiIntervalSeries(ctx, sr, requiredIntervalSeries, intervalKlineSeries, func(driver bool, interval types.Interval, k *types.Kline) (err error) {
if !driver {
return
@ -95,11 +95,9 @@ func (b *SigStrategyBacktester) singleStrategySeries(ctx context.Context, sigStr
}
// intervalStrategySeries 多周期策略
func (b *SigStrategyBacktester) intervalStrategySeries(ctx context.Context, intervalSigStrategy strategy.IIntervalSigStrategy, sr *pb.SeriesRange, recvSignal func(sigSide types.Side, k types.Kline) (err error)) (err error) {
func (b *SigStrategyBacktester) intervalStrategySeries(ctx context.Context, intervalSigStrategy strategy.IIntervalSigStrategy, sr *pb.SeriesRange, intervalKlineSeries *types.IntervalState[*sig.KlineSeries], recvSignal func(sigSide types.Side, k types.Kline) (err error)) (err error) {
// 各周期所需k线数量
requiredIntervalSeries := intervalSigStrategy.RequiredIntervalSeries()
// 各周期 series
intervalKlineSeries := types.NewIntervalState[*sig.KlineSeries]()
// 策略上下文
intervalStrategyContext := sig.NewIntervalStrategyContext(intervalKlineSeries, b.indicatorReg)
err = b.multiIntervalSeries(ctx, sr, requiredIntervalSeries, intervalKlineSeries, func(driver bool, interval types.Interval, k *types.Kline) (err error) {
@ -166,11 +164,7 @@ func (b *SigStrategyBacktester) multiIntervalSeries(ctx context.Context, sr *pb.
stopCh := make(chan struct{})
stopChClosed := atomic.Bool{}
for _, interval := range otherIntervals {
kSeries := intervalKlineSeries.Get(interval)
if kSeries == nil {
kSeries = sig.NewKlineSeries(sr.Exchange, sr.InstId, interval)
intervalKlineSeries.Set(interval, kSeries)
}
kSeries := intervalKlineSeries.ComputeIfAbsent(interval, func() *sig.KlineSeries { return sig.NewKlineSeries(sr.Exchange, sr.InstId, interval) })
go func(interval types.Interval, kSeries *sig.KlineSeries) {
syncCh := otherIntervalSyncCh.Get(interval)
intervalAdder := types.SupportedIntervals[interval]
@ -208,7 +202,7 @@ func (b *SigStrategyBacktester) multiIntervalSeries(ctx context.Context, sr *pb.
// 回调周期订阅
if subs, ok := b.intervalSubscribe[interval]; ok {
for _, subFn := range subs {
if err = subFn(interval, k); err != nil {
if err = subFn(interval, *k); err != nil {
return
}
}
@ -232,11 +226,7 @@ func (b *SigStrategyBacktester) multiIntervalSeries(ctx context.Context, sr *pb.
}(interval, kSeries)
}
// 驱动周期数据拉取
driverSeries := intervalKlineSeries.Get(driverInterval)
if driverSeries == nil {
driverSeries = sig.NewKlineSeries(sr.Exchange, sr.InstId, driverInterval)
intervalKlineSeries.Set(driverInterval, driverSeries)
}
driverSeries := intervalKlineSeries.ComputeIfAbsent(driverInterval, func() *sig.KlineSeries { return sig.NewKlineSeries(sr.Exchange, sr.InstId, driverInterval) })
sr.WindowExtra = max(sr.WindowExtra, uint32(max(0, requiredIntervalSeries.Get(driverInterval)-1)))
err0 := b.fetchHistoryKlineSeries(ctx, sr, func(k *types.Kline) (err error) {
if lastTs, serial := driverSeries.Update(k); !serial {
@ -270,7 +260,7 @@ func (b *SigStrategyBacktester) multiIntervalSeries(ctx context.Context, sr *pb.
// 回调周期订阅
if subs, ok := b.intervalSubscribe[driverInterval]; ok {
for _, subFn := range subs {
if err = subFn(driverInterval, k); err != nil {
if err = subFn(driverInterval, *k); err != nil {
return
}
}

25
internal/trading/backtest/simulator.go → internal/trading/backtest/trade_simulator.go

@ -2,23 +2,24 @@ package backtest
import (
"math"
"sig-pub/pkg/trade"
"sig-pub/pkg/types"
"sig-pub/pkg/types/decimals"
)
type Simulator struct {
type TradeSimulator struct {
FeePct float64 // e.g. 0.0005 = 0.05%
SlippagePct float64 // e.g. 0.001 = 0.1%
TradeId int64
}
func NewSimulator(feePct, slippagePct float64) *Simulator {
return &Simulator{FeePct: feePct, SlippagePct: slippagePct}
func NewTradeSimulator(feePct, slippagePct float64) *TradeSimulator {
return &TradeSimulator{FeePct: feePct, SlippagePct: slippagePct}
}
// ExecuteMarket 执行市价单,使用kline信息决定成交价(使用close以及滑点)
func (s *Simulator) ExecuteMarket(side types.Side, qty float64, closePrice float64, ts int64) (trade *Trade, ok bool) {
func (s *TradeSimulator) ExecuteMarket(side types.Side, qty float64, closePrice float64, ts int64) (trd *trade.Trade, ok bool) {
// base price use close
base := closePrice
slippage := s.SlippagePct
@ -33,15 +34,15 @@ func (s *Simulator) ExecuteMarket(side types.Side, qty float64, closePrice float
return
}
fee := math.Abs(base*qty) * s.FeePct
trade = &Trade{Side: side, Qty: qty, Price: base, Fee: fee, Ts: ts}
trd = &trade.Trade{Side: side, Qty: qty, Price: base, Fee: fee, Time: ts}
s.TradeId++
trade.Id = s.TradeId
trd.Id = s.TradeId
ok = true
return
}
// ExecuteLimit 简单实现: 如果limit价格被kline的high/low包含则成交
func (s *Simulator) ExecuteLimit(side types.Side, qty float64, limitPx float64, k types.Kline, ts int64) (trade Trade, filled bool) {
func (s *TradeSimulator) ExecuteLimit(side types.Side, qty float64, limitPx float64, k types.Kline, ts int64) (trd *trade.Trade, filled bool) {
h := decimals.MustToFloat64(k.High)
l := decimals.MustToFloat64(k.Low)
switch side {
@ -51,16 +52,16 @@ func (s *Simulator) ExecuteLimit(side types.Side, qty float64, limitPx float64,
// assume filled at min(limitPx, open)
px := math.Min(limitPx, decimals.MustToFloat64(k.Open))
fee := math.Abs(px*qty) * s.FeePct
trade = Trade{Side: side, Qty: qty, Price: px * (1 + s.SlippagePct), Fee: fee, Ts: ts}
return trade, true
trd = &trade.Trade{Side: side, Qty: qty, Price: px * (1 + s.SlippagePct), Fee: fee, Time: ts}
return trd, true
}
case types.SideShort:
if h >= limitPx {
px := math.Max(limitPx, decimals.MustToFloat64(k.Open))
fee := math.Abs(px*qty) * s.FeePct
trade = Trade{Side: side, Qty: qty, Price: px * (1 - s.SlippagePct), Fee: fee, Ts: ts}
return trade, true
trd = &trade.Trade{Side: side, Qty: qty, Price: px * (1 - s.SlippagePct), Fee: fee, Time: ts}
return trd, true
}
}
return trade, false
return nil, false
}

239
internal/trading/backtest/trading_plan_backtester.go

@ -2,61 +2,48 @@ package backtest
import (
"context"
"errors"
"fmt"
"io"
"sig-pub/api/pb"
"sig-pub/internal/trading/sig"
"sig-pub/pkg/data/entity"
"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/collect"
"sig-pub/pkg/utils/times"
"sig-pub/pkg/zlog"
"github.com/bytedance/sonic"
"google.golang.org/grpc"
)
type BacktestStat struct {
// total_return 年化收益
// sharpe_ratio 夏普比率
// max_drawdown 最大回撤
// num_trades 单数
// win_rate 胜率
}
type TradeAccount struct {
trade.ITradeAccount
cash float64
}
func NewTradeAccount(cash float64) *TradeAccount {
return &TradeAccount{
cash: cash,
}
}
type TradingPlanBacktester struct {
exchangeClient pb.ExchangeServiceClient
indicatorReg *indicator.IndicatorRegistry
sigStrategyReg *strategy.SigStrategyRegistry
exchangeClient pb.ExchangeServiceClient
account *BacktestAccount
sigStrategyType strategy.SigStrategyType
sigStrategy strategy.ISigStrategy
closeStrategy *trade.CloseStrategy
riskStrategy *trade.RiskStrategy
tradeStrategy *trade.TradeStrategy
}
func NewTradingPlanBacktester(exchangeClient pb.ExchangeServiceClient, indicatorReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry) *TradingPlanBacktester {
func NewTradingPlanBacktester(indicatorReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry, exchangeClient pb.ExchangeServiceClient) *TradingPlanBacktester {
return &TradingPlanBacktester{
exchangeClient: exchangeClient,
indicatorReg: indicatorReg,
sigStrategyReg: sigStrategyReg,
exchangeClient: exchangeClient,
}
}
// 核心引擎,模拟交易、持仓跟踪、费用计算
func (b *TradingPlanBacktester) Backtest(ctx context.Context, cash float64, plan entity.TradePlan, sr *pb.SeriesRange) (err error) {
func (b *TradingPlanBacktester) Init(cash float64, plan entity.TradePlan) (err error) {
// trade account
simulator := NewTradeSimulator(0.0005, 0.0008)
b.account = NewBacktestAccount(cash, simulator)
// sig strategy
sigStrategyType, sigStrategy, ok := b.sigStrategyReg.NewSigStrategy(plan.SigStrategy)
ok := false
b.sigStrategyType, b.sigStrategy, ok = b.sigStrategyReg.NewSigStrategy(plan.SigStrategy)
if !ok {
err = fmt.Errorf("sig strategy not exists %s", plan.SigStrategy)
return
@ -65,7 +52,7 @@ func (b *TradingPlanBacktester) Backtest(ctx context.Context, cash float64, plan
if err = sonic.UnmarshalString(plan.SigStrategyParam, &sigStrategyParam); err != nil {
return
}
if err = sigStrategy.Init(sigStrategyParam); err != nil {
if err = b.sigStrategy.Init(sigStrategyParam); err != nil {
return
}
closeStrategyParam, tradeStrategyParam, riskStrategyParam := new(trade.CloseStrategyParam),
@ -80,146 +67,124 @@ func (b *TradingPlanBacktester) Backtest(ctx context.Context, cash float64, plan
return
}
// 平仓策略
closeStrategy, err := trade.NewCloseStrategy(*closeStrategyParam)
b.closeStrategy, err = trade.NewCloseStrategy(*closeStrategyParam)
if err != nil {
return
}
// 风险管理策略
riskStrategy, err := trade.NewRiskStrategy(*riskStrategyParam)
b.riskStrategy, err = trade.NewRiskStrategy(*riskStrategyParam)
if err != nil {
return
}
tradeAccount := NewTradeAccount(cash)
_ = tradeAccount
// 平仓管理器
closeManager := NewCloseManager(0.02, 0)
closeManager.SetDynamicParams(0.1, 0.02, 0)
// 回测账户
account := NewAccount(10000, NewSimulator(0.0005, 0.0008))
sigStrategyBacktester := NewSigStrategyBacktester(sigStrategyType, sigStrategy, b.indicatorReg, b.exchangeClient)
// 平仓策略订阅1分钟曲线
sigStrategyBacktester.SubKline(types.Interval1m, func(interval types.Interval, k *types.Kline) (err error) {
closeManager.OnKline(*k, account)
// 交易策略
b.tradeStrategy, err = trade.NewTradeStrategy(*tradeStrategyParam)
if err != nil {
return
})
// 下单
err = sigStrategyBacktester.Backtest(ctx, sr, func(sigSide types.Side, k types.Kline) (err error) {
b.onSigSideSignalWithAccount(sigSide, k, account, closeManager, riskStrategy)
return b.onSideSingal(sigSide, k, closeStrategy)
})
}
return
}
// onSideSingal 出现买卖信号
func (b *TradingPlanBacktester) onSideSingal(sigSide types.Side, k types.Kline, closeStrategy *trade.CloseStrategy) (err error) {
// 核心引擎,模拟交易、持仓跟踪、费用计算
func (b *TradingPlanBacktester) Backtest(ctx context.Context, sr *pb.SeriesRange) (err error) {
// 交易信号回测器
sigStrategyBacktester := NewSigStrategyBacktester(b.sigStrategyType, b.sigStrategy, b.indicatorReg, b.exchangeClient)
onInterval1m, sigTimes := 0, 0
sigStrategyBacktester.SubKline(types.Interval1m, func(interval types.Interval, k types.Kline) (err error) {
onInterval1m++
// 检查仓位平仓
return b.closeByKlineInterval1m(k)
})
err = sigStrategyBacktester.Backtest(ctx, sr, nil, func(sigSide types.Side, k types.Kline) (err error) {
sigTimes++
// 根据交易信号检查仓位平仓
if err = b.closeBySigSingal(sigSide, k); err != nil {
return
}
// onSigSideSignalWithAccount 交易策略发出交易信号(使用指定的账户和平仓管理器)
func (b *TradingPlanBacktester) onSigSideSignalWithAccount(sigSide types.Side, k types.Kline, account *Account, closeManager *CloseManager, riskStrategy *trade.RiskStrategy) {
// risk check before executing
side := riskStrategy.SideAssess(sigSide)
if !side.IsValid() {
zlog.Debugf("risk strategy filter sig side: %s", sigSide.String())
// 交易下单
return b.onSideSingal(sigSide, k)
})
if err != nil {
return
}
// zlog.Debugf("apply market order: ts=%d, side=%s", k.Ts, side.String())
price := decimals.MustToFloat64(k.Close)
account.ApplyMarketOrder(side, 0.01, price, k.Ts)
// 根据信号方向平掉相反方向的仓位:如果信号是买入,平掉所有卖出仓位;如果信号是卖出,平掉所有买入仓位
closeManager.CloseBySignal(sigSide, account, k)
// todo 回测报告
var trades []*trade.Trade
for _, trade := range b.account.trades {
if trade.ClosePrice > 0 {
trades = append(trades, trade)
}
func (b *TradingPlanBacktester) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingPlan, stime, etime int64, sigKlineSeries *sig.KlineSeries) (err error) {
plan := tradingPlan.Plan
exchange := pb.ExchangeType(plan.Exchange)
interval := types.Interval(plan.Interval)
instId := plan.InstId
sigStrategy := tradingPlan.GetSigStrategy()
maxWindow := int(sigStrategy.(strategy.ISingleSigStrategy).RequiredSeries())
if maxWindow < 0 || maxWindow > indicator.MaxWindow {
err = fmt.Errorf("invalid window %d 0-%d, planId=%d", maxWindow, indicator.MaxWindow, plan.Id)
}
collect.SortDesc(trades, func(t *trade.Trade) float64 { return t.Pnl })
exposure := b.account.cash + b.account.CurrentExposure()
_ = exposure
return
}
// 回测账户
account := NewAccount(10000, NewSimulator(0.0005, 0.0008))
// 平仓管理器
closeManager := NewCloseManager(0.02, 0)
closeManager.SetDynamicParams(0.1, 0.02, 0)
seriesRange := &pb.SeriesRange{
Exchange: exchange,
InstId: instId,
Interval: string(interval),
Before: stime,
After: etime,
Open: false,
Live: false,
Desc: false,
WindowExtra: uint32(maxWindow),
}
// fetch history klines via stream
req := &pb.ReqHistoryKlineStream{Series: seriesRange}
stream, err := b.exchangeClient.HistoryKlineStream(ctx, req, grpc.UseCompressor("snappy"))
if err != nil {
// closeByKlineInterval1m k线更新时检查平仓
func (b *TradingPlanBacktester) closeByKlineInterval1m(k types.Kline) (err error) {
var posErrs []error
positions := b.account.OpenPositions()
for _, pos := range positions {
closePos, cause := b.closeStrategy.OnKline(k, pos)
if closePos {
errc := b.account.ClosePosition(pos, k, cause)
if errc != nil {
posErrs = append(posErrs, errc)
zlog.Errorf("close position error: k=%#v, err=%v", k, errc)
}
}
}
if len(posErrs) > 0 {
err = errors.Join(posErrs...)
return
}
return
}
watch := times.NewWatch()
recvTimes, recvTotal := 0, 0
var lastK *types.Kline
var msg *pb.RspHistoryKlineStream
for {
select {
case <-ctx.Done():
err = ctx.Err()
// closeBySigSingal 交易信号出现时检查平仓
func (b *TradingPlanBacktester) closeBySigSingal(sigSide types.Side, kline types.Kline) (err error) {
var posErrs []error
positions := b.account.OpenPositions()
for _, pos := range positions {
closePos, cause := b.closeStrategy.OnSigStrategySingal(sigSide, pos)
if closePos {
errc := b.account.ClosePosition(pos, kline, cause)
if errc != nil {
posErrs = append(posErrs, errc)
zlog.Error("close position error: ", errc)
}
}
}
if len(posErrs) > 0 {
err = errors.Join(posErrs...)
return
default:
}
msg, err = stream.Recv()
if err == io.EOF {
break
return
}
// onSideSingal 出现买卖信号
func (b *TradingPlanBacktester) onSideSingal(sigSide types.Side, k types.Kline) (err error) {
// 买卖信号交易风险分析
doTrade, causes, err := b.riskStrategy.SigRiskAnalyze(sigSide)
if err != nil {
return
}
recvTimes++
recvTotal += 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)
if !doTrade {
_ = causes // todo 记录信号不交易原因分析 log db analyze
return
}
length := sigKlineSeries.Length()
if length <= maxWindow {
continue
}
// 平仓策略
closeManager.OnKline(*kline, account)
sigSide := tradingPlan.Update(strategy.StrategyTypeSig)
if sigSide.IsValid() {
b.onSigSideSignalWithAccount(sigSide, *kline, account, closeManager, nil)
tradeArg, err := b.tradeStrategy.SigTrade(sigSide, k)
if err != nil {
return
}
ok, cause, err := b.account.TradeOrder(tradeArg)
if err != nil {
return
}
if !ok {
_ = cause // todo 记录不交易原因
}
_ = lastK
zlog.Debugf("recv=%d, total=%d, use %s", recvTimes, recvTotal, watch.ElapsedFmt("."))
collect.SortDesc(account.Trades, func(t *Trade) float64 { return t.Pnl })
exposure := account.Cash + account.PositionCost()
_ = exposure
return
}

42
internal/trading/backtest/types.go

@ -1,46 +1,22 @@
package backtest
import (
"sig-pub/pkg/types"
"sig-pub/pkg/trade"
)
type Side = types.Side
type Position struct {
TradeId int64
Side Side // 交易方向
Qty float64 // 交易量
EntryPx float64 // 入场价格
EntryTs int64 // 入场时间
PeakPx float64 // highest (for long) or lowest (for short) observed price since entry
Status int32 // 1.交易中 2.持仓中 3.已平仓
Fee float64 // 手续费
Close bool // 是否已平仓
}
type Trade struct {
Id int64 // 交易id
Side types.Side // 交易方向
Qty float64 // 交易量
Price float64 // 开仓价格
Fee float64 // 开仓手续费
Ts int64 // 开仓时间
ClosePrice float64 // 平仓价格
CloseFee float64 // 平仓手续费
CloseTs int64 // 平仓时间
CloseCause string // 平仓原因 ["stoploss", "takeprofit", "trailing", "retrace", "signal"](“止损”、“止盈”、“动态跟踪”、“回撤”、“信号”)
Pnl float64 // 盈利/亏损 pnl = (t.ClosePrice-t.Price)*t.Qty - t.Fee - t.CloseFee
HoldTime string // 持仓时间
}
type BacktestResult struct {
StartTs int64
EndTs int64
Trades []Trade
Positions []Position
Trades []*trade.Trade
Positions []*trade.Position
Cash float64
Equity float64
// total_return 年化收益
// sharpe_ratio 夏普比率
// max_drawdown 最大回撤
// num_trades 单数
// win_rate 胜率
}
type TradeStat struct {

2
internal/trading/trading_grpc_server.go

@ -47,7 +47,7 @@ func (svr *TradingGrpcServer) Backtest(ctx context.Context, req *pb.ReqBacktest)
if err != nil {
return
}
err = svr.tradingService.Backtest(req.PlanId, stime.UnixMilli(), etime.UnixMilli())
err = svr.tradingService.Backtest(ctx, req.PlanId, stime.UnixMilli(), etime.UnixMilli())
if err != nil {
return
}

29
internal/trading/trading_service.go

@ -104,7 +104,7 @@ func (svc *TradingService) getTradingPlan(plan *entity.TradePlan, sigKlineSeries
return
}
if _, ok := types.SupportedIntervals[interval]; !ok {
err = fmt.Errorf("unsupport interval %d", plan.Exchange)
err = fmt.Errorf("unsupport interval %s", plan.Interval)
return
}
@ -260,7 +260,7 @@ func (svc *TradingService) StrategySeries(ctx context.Context, req *pb.ReqStrate
// 使用回测器回测信号
backtester := backtest.NewSigStrategyBacktester(sigStrategyType, sigStrategy, svc.indicatorReg, svc.exchangeClient)
err = backtester.Backtest(ctx, req.Series, func(sigSide types.Side, k types.Kline) (err error) {
err = backtester.Backtest(ctx, req.Series, nil, func(sigSide types.Side, k types.Kline) (err error) {
side := lang.Ternary(sigSide == types.SideLong, pb.Side_BUY, pb.Side_SELL)
rsp.Signal = append(rsp.Signal, side)
rsp.Times = append(rsp.Times, k.Ts)
@ -270,24 +270,33 @@ func (svc *TradingService) StrategySeries(ctx context.Context, req *pb.ReqStrate
}
// Backtest 回测交易计划
func (svc *TradingService) Backtest(planId, stime, etime int64) (err error) {
func (svc *TradingService) Backtest(ctx context.Context, planId, stime, etime int64) (err error) {
plan, err := svc.tradingDataPersist.GetTradePlanById(planId)
if err != nil {
return
}
exchange := pb.ExchangeType(plan.Exchange)
interval := types.Interval(plan.Interval)
sigKlineSeries := sig.NewKlineSeries(exchange, plan.InstId, interval)
tradingPlan, err := svc.getTradingPlan(plan, sigKlineSeries)
if err != nil {
if _, ok := types.SupportedIntervals[interval]; !ok {
err = fmt.Errorf("unsupport interval %s", interval)
return
}
test := backtest.NewTradingPlanBacktester(svc.exchangeClient, svc.indicatorReg, svc.strategyReg)
err = test.RunTradingPlan(context.Background(), tradingPlan, stime, etime, sigKlineSeries)
if err != nil {
sr := &pb.SeriesRange{
Exchange: exchange,
InstId: plan.InstId,
Interval: plan.Interval,
Before: stime,
After: etime,
Open: false,
Live: false,
Desc: false,
}
tester := backtest.NewTradingPlanBacktester(svc.indicatorReg, svc.strategyReg, svc.exchangeClient)
if err = tester.Init(10000, *plan); err != nil {
return
}
err = tester.Backtest(ctx, sr)
return
}

26
pkg/data/entity/trade_plan.go

@ -2,19 +2,19 @@ package entity
// TradePlan 交易计划
type TradePlan struct {
Id int64 `gorm:"column:id;primaryKey" json:"id"` // id
UserId int64 `gorm:"column:user_id" json:"userId"` // 用户id
Status int8 `gorm:"column:status" json:"status"` // 状态:0禁用,1启用
Exchange int8 `gorm:"column:exchange" json:"exchange"` // 交易所
InstId string `gorm:"column:inst_id" json:"instId"` // 交易产品id
Interval string `gorm:"column:interval" json:"interval"` // 交易周期
SigStrategy string `gorm:"column:sig_strategy" json:"sigStrategy"` // 交易信号策略
SigStrategyParam string `gorm:"column:sig_strategy_param" json:"sigStrategyParam"` // 交易信号策略参数
CloseStrategyParam string `gorm:"column:close_strategy_param" json:"closeStrategyParam"` // 退出策略名称参数
TradeStrategyParam string `gorm:"column:trade_strategy_param" json:"tradeStrategyParam"` // 下单仓位管理策略参数
RiskStrategyParam string `gorm:"column:risk_strategy_param" json:"riskStrategyParam"` // 风险管理策略参数
UpdateBy string `gorm:"column:update_by" json:"updateBy"` // 更新人
UpdateTime int64 `gorm:"column:update_time" json:"updateTime"` // 更新时间戳毫秒
Id int64 ` json:"id" gorm:"column:id;primaryKey"` // id
UserId int64 ` json:"userId" gorm:"column:user_id"` // 用户id
Status int8 ` json:"status" gorm:"column:status"` // 状态:0禁用,1启用
Exchange int8 ` json:"exchange" gorm:"column:exchange"` // 交易所
InstId string ` json:"instId" gorm:"column:inst_id"` // 交易产品id
Interval string ` json:"interval" gorm:"column:interval"` // 交易周期
SigStrategy string ` json:"sigStrategy" gorm:"column:sig_strategy"` // 交易信号策略
SigStrategyParam string ` json:"sigStrategyParam" gorm:"column:sig_strategy_param"` // 交易信号策略参数
CloseStrategyParam string ` json:"closeStrategyParam" gorm:"column:close_strategy_param"` // 退出策略名称参数
TradeStrategyParam string ` json:"tradeStrategyParam" gorm:"column:trade_strategy_param"` // 下单仓位管理策略参数
RiskStrategyParam string ` json:"riskStrategyParam" gorm:"column:risk_strategy_param"` // 风险管理策略参数
UpdateBy string ` json:"updateBy" gorm:"column:update_by"` // 更新人
UpdateTime int64 ` json:"updateTime" gorm:"column:update_time"` // 更新时间戳毫秒
}
func (TradePlan) TableName() string {

95
pkg/trade/close_strategy.go

@ -17,9 +17,7 @@ type ICloseStrategy interface {
type CloseStrategyParam struct {
StopLossPct float64 `json:"stopLossPct"` // 固定止损 static stoploss
TakeProfitPct float64 `json:"takeProfitPct"` // 固定止盈 static take profit
TrailMinProfit float64 `json:"trailMinProfit"` // 启动移动止损的最小盈利阈值(例如达到 1% 后才开始追踪) minimum profit (fraction) before trailing activates (e.g. 0.01 = 1%);
TrailingPct float64 `json:"trailingPct"` // 移动止损百分比(例如 0.02 表示从最高价回撤 2% 时触发追踪止损) trailing stop percent (e.g. 0.02 = 2%);
ProfitRetracePct float64 `json:"profitRetracePct"` // close when profit retraces more than this fraction of peak profit;基于最高利润回撤触发平仓(例如从最高利润回撤超过 30% 则平仓)。
ProfitRetracePcts [][]float64 `json:"profitRetracePcts"` // 基于最高利润回撤触发平仓 (例如 [[0.01, 0.3], [0.02, 0.2]] 最高利润超过1%时30%回撤则触发平仓,最高利润超过2%时20%回撤就触发平仓)
CloseOnSideReverse bool `json:"closeOnSideReverse"` // 交易信号和持单方向相反时是否进行平仓
Fee bool `json:"fee"` // 计算止盈止损时是否包含手续费
}
@ -41,13 +39,6 @@ func NewCloseStrategy(param CloseStrategyParam) (cs *CloseStrategy, err error) {
// Update 当k线更新判断是否关闭仓位
func (s *CloseStrategy) OnKline(k types.Kline, pos *Position) (closePos bool, cause Cause) {
closePrice := decimals.MustToFloat64(k.Close)
// update peak px
// if pos.Side == types.SideLong && closePrice > pos.PeakPx {
// pos.PeakPx = closePrice
// }
// if pos.Side == types.SideShort && closePrice < pos.PeakPx {
// pos.PeakPx = closePrice
// }
return s.OnPrice(closePrice, pos)
}
@ -56,26 +47,92 @@ func (s *CloseStrategy) OnPrice(price float64, pos *Position) (closePos bool, ca
if !pos.Side.IsValid() {
return
}
// update peak px
peakPx, ok := pos.GetStateF64("peakPx")
updatePeakPx := false
if !ok {
updatePeakPx = true
} else {
if pos.Side == types.SideLong && price > peakPx {
updatePeakPx = true
}
if pos.Side == types.SideShort && price < peakPx {
updatePeakPx = true
}
}
if updatePeakPx {
peakPx = price
pos.SetState("peakPx", price)
}
entry := pos.EntryPx
// side long:
if pos.Side == types.SideLong {
// 固定止损
if s.StopLossPct > 0 && price <= pos.EntryPx*(1-s.StopLossPct) {
return true, CauseStoploss
if s.StopLossPct > 0 && price <= entry*(1-s.StopLossPct) {
return true, CauseCloseStoploss
}
// 固定止盈
if s.TakeProfitPct > 0 && price >= pos.EntryPx*(1+s.TakeProfitPct) {
return true, CauseTakeprofit
if s.TakeProfitPct > 0 && price >= entry*(1+s.TakeProfitPct) {
return true, CauseCloseTakeprofit
}
// 基于最高利润动态止盈
if len(s.ProfitRetracePcts) > 0 {
// peak profit fraction
peakProfit := (peakPx - entry) / entry
minProfitToTrail, trailingPct := float64(0), float64(0)
for _, profit := range s.ProfitRetracePcts {
if len(profit) != 2 {
continue
}
_minProfitToTrail := profit[0] // 启动最高利润回撤的最小盈利阈值
_trailingPct := profit[1] // 基于最高利润回撤触发平仓
if peakProfit >= _minProfitToTrail {
if _minProfitToTrail > minProfitToTrail {
minProfitToTrail = _minProfitToTrail
trailingPct = _trailingPct
}
}
}
if minProfitToTrail > 0 && trailingPct > 0 {
trail := peakPx * (1 - trailingPct)
if price <= trail {
return true, CauseCloseTrailing
}
}
}
// todo dynamic trailing
return
}
// side short:
if s.StopLossPct > 0 && price >= pos.EntryPx*(1+s.StopLossPct) {
return true, CauseStoploss
return true, CauseCloseStoploss
}
if s.TakeProfitPct > 0 && price <= pos.EntryPx*(1-s.TakeProfitPct) {
return true, CauseTakeprofit
return true, CauseCloseTakeprofit
}
// 基于最高利润动态止盈
if len(s.ProfitRetracePcts) > 0 {
// peak profit fraction
peakProfit := (entry - peakPx) / entry
minProfitToTrail, trailingPct := float64(0), float64(0)
for _, profit := range s.ProfitRetracePcts {
if len(profit) != 2 {
continue
}
_minProfitToTrail := profit[0] // 启动最高利润回撤的最小盈利阈值
_trailingPct := profit[1] // 基于最高利润回撤触发平仓
if peakProfit >= _minProfitToTrail && _minProfitToTrail >= minProfitToTrail {
minProfitToTrail = _minProfitToTrail
trailingPct = _trailingPct
}
}
if minProfitToTrail > 0 && trailingPct > 0 {
trail := peakPx * (1 + trailingPct)
if price >= trail {
return true, CauseCloseTrailing
}
}
}
return
}
@ -85,5 +142,5 @@ func (s *CloseStrategy) OnSigStrategySingal(sigSide types.Side, pos *Position) (
if !s.CloseOnSideReverse {
return
}
return sigSide != pos.Side, CauseStoploss
return sigSide != pos.Side, CauseCloseReverseSingal
}

5
pkg/trade/risk_strategy.go

@ -11,6 +11,7 @@ type IRickStrategy interface {
type RiskStrategyParam struct {
SkipOnSideOpposite bool // 当前持有反方向单时
SkipOnSideSame bool // 当前持有相同方向单时
}
// RiskStrategy 风险管理策略
@ -30,7 +31,7 @@ func NewRiskStrategy(param RiskStrategyParam) (rs *RiskStrategy, err error) {
// 对交易方向进行信心分数评估, 后续开仓仓位
// 1.当前持有反方向单时, 不进行开仓
// 2.当前持有同方向单时, 根据信心分数评估是否加仓
func (s *RiskStrategy) SideAssess(signalSide types.Side) (side types.Side) {
func (s *RiskStrategy) SigRiskAnalyze(signalSide types.Side) (doTrade bool, causes []Cause, err error) {
return signalSide
return true, nil, nil
}

44
pkg/trade/trade_account.go

@ -1,38 +1,32 @@
package trade
import (
"sig-pub/pkg/data/entity"
"sig-pub/pkg/types"
"github.com/govalues/decimal"
)
// sig -> close strategy
// sig -> risk strategy -> trade strategy -> tarde account
type ITradeAccount interface {
// 根据当前价格对仓位进行 mark-to-market,返回账户净值
GetCurrentEquity() decimal.Decimal
// 返回当前仓位的名义总敞口(绝对值)
GetCurrentExposure() decimal.Decimal
// 获取可交易空闲资金
GetCash() decimal.Decimal
// 获取未平仓交易单
ListOpenPosition() []*Position
// 获取交易订单
GetTradeOrder(tradeId int64) Trade
OpenPositions() []*Position
// 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct)
CanOpen(instId string, side types.Side, qty, price float64) bool
// 获取未平仓交易单数
CountOpenPositions() int
// 直接用市价下单(简化),qty为基础货币数量
ApplyMarketOrder(instId string, side types.Side, qty float64, price float64, ts int64) (t *entity.TradeOrder, ok bool)
// 订单下单
TradeOrder(ta TradeArg) (ok bool, cause Cause, err error)
// 将仓位进行平仓
ClosePosition()
ClosePosition(*Position, Cause) (err error)
// // 根据当前价格对仓位进行 mark-to-market,返回账户净值
// GetCurrentEquity() decimal.Decimal
// // 返回当前仓位的名义总敞口(绝对值)
// GetCurrentExposure() decimal.Decimal
// // 获取可交易空闲资金
// GetCash() decimal.Decimal
// // 获取交易订单
// GetTradeOrder(tradeId int64) Trade
// // 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct)
// CanOpen(instId string, side types.Side, qty, price float64) bool
// // 直接用市价下单(简化),qty为基础货币数量
// ApplyMarketOrder(instId string, side types.Side, qty float64, price float64, ts int64) (t *entity.TradeOrder, ok bool)
}
type OkxTradeAccount struct {

45
pkg/trade/trade_strategy.go

@ -1,6 +1,11 @@
package trade
import "github.com/govalues/decimal"
import (
"sig-pub/pkg/types"
"sig-pub/pkg/types/decimals"
"github.com/govalues/decimal"
)
// ITradeStrategy 下单策略
// 根据购买信号和账户信息生成下单参数
@ -19,4 +24,42 @@ type ITradeStrategyContext interface {
}
type TradeStrategyParam struct {
MaxPosPct float64 // 单笔交易最大仓位占比
MaxExposurePct float64 // 最大总敞口占比
MaxLots float64 // 最大手数/数量 (optional)
}
type TradeStrategy struct {
param TradeStrategyParam
}
func NewTradeStrategy(param TradeStrategyParam) (*TradeStrategy, error) {
return &TradeStrategy{
param: param,
}, nil
}
func (s *TradeStrategy) SigTrade(side types.Side, k types.Kline) (ta TradeArg, err error) {
price := decimals.MustToFloat64(k.Close)
time := k.Interval.MustAddMul(k.Ts, 1)
ta = TradeArg{
Side: side,
Price: price,
Leverage: 1,
Qty: 0.01,
KInterval: string(k.Interval),
KTime: k.Ts,
Time: time,
}
return
}
type TradeArg struct {
Side types.Side // 开仓方向
Price float64 // 开仓价格
Leverage int32 // 杠杆倍数
Qty float64 // 交易量 qty为基础货币数量
KInterval string // 交易k线周期
KTime int64 // 交易k线时间
Time int64 // 交易时间
}

94
pkg/trade/types.go

@ -1,18 +1,32 @@
package trade
import "sig-pub/pkg/types"
import (
"sig-pub/pkg/types"
// Position 持仓仓位
type Position struct {
TradeId int64 // 交易订单id
Status int32 // 1.交易中 2.持仓中 3.已平仓
Side types.Side // 交易方向
Qty float64 // 交易量
EntryPx float64 // 入场价格
EntryTs int64 // 入场时间
PeakPx float64 // highest (for long) or lowest (for short) observed price since entry
Fee float64 // 手续费
FeeRate float64 // 手续费率
"github.com/spf13/cast"
)
type Cause int32
const (
// _ Cause = iota
CauseCloseStoploss Cause = 1001 // 平仓:固定止损
CauseCloseTakeprofit Cause = 1002 // 平仓:固定止盈
CauseCloseReverseSingal Cause = 1003 // 平仓:策略反向信号
CauseCloseTrailing Cause = 1004 // 平仓:移动止损基于最高利润点回撤百分比
CauseRiskAlreadyTrade Cause = 2001 // 风控:已有持仓
CauseRiskSideAlreadyTrade Cause = 2002 // 风控:相同方向已有持仓
)
func (c Cause) String() string {
switch c {
default:
return ""
case CauseCloseStoploss:
return "stoploss"
case CauseCloseTakeprofit:
return "takeprofit"
}
}
type Trade struct {
@ -21,31 +35,53 @@ type Trade struct {
Qty float64 // 交易量
Price float64 // 开仓价格
Fee float64 // 开仓手续费
Ts int64 // 开仓时间
Time int64 // 开仓时间
ClosePrice float64 // 平仓价格
CloseFee float64 // 平仓手续费
CloseTs int64 // 平仓时间
CloseCause string // 平仓原因 ["stoploss", "takeprofit", "trailing", "retrace", "signal"](“止损”、“止盈”、“动态跟踪”、“回撤”、“信号”)
CloseCause Cause // 平仓原因 ["stoploss", "takeprofit", "trailing", "retrace", "signal"](“止损”、“止盈”、“动态跟踪”、“回撤”、“信号”)
Pnl float64 // 盈利/亏损 pnl = (t.ClosePrice-t.Price)*t.Qty - t.Fee - t.CloseFee
HoldTime string // 持仓时间
}
type Cause int32
// Position 持仓仓位
type Position struct {
TradeId int64 // 交易订单id
Status int32 // 1.交易中 2.持仓中 3.已平仓
Side types.Side // 交易方向
Qty float64 // 交易量
EntryPx float64 // 入场价格
EntryTs int64 // 入场时间
PeakPx float64 // highest (for long) or lowest (for short) observed price since entry
Fee float64 // 手续费
FeeRate float64 // 手续费率
State map[string]any // 持仓持久化状态
}
const (
_ Cause = iota
CauseStoploss // 固定止损
CauseTakeprofit // 固定止盈
CauseReverseSingal // 策略反向信号
)
func (pos *Position) SetState(k string, v any) {
if pos.State == nil {
pos.State = make(map[string]any)
}
pos.State[k] = v
}
func (c Cause) String() string {
switch c {
default:
return ""
case CauseStoploss:
return "stoploss"
case CauseTakeprofit:
return "takeprofit"
func (pos *Position) GetState(k string) (v any, ok bool) {
if pos.State == nil {
return
}
v, ok = pos.State[k]
return
}
func (pos *Position) GetStateF64(k string) (f float64, ok bool) {
if pos.State == nil {
return
}
v, ok := pos.State[k]
if !ok {
return
}
f, err := cast.ToFloat64E(v)
ok = err == nil
return
}

36
pkg/types/interval.go

@ -127,30 +127,50 @@ func IntervalsSort(intervals []Interval) {
}
type IntervalState[T any] struct {
state []T
state []intervalStateEntry[T]
}
type intervalStateEntry[T any] struct {
V T
Exists bool
}
func NewIntervalState[T any]() *IntervalState[T] {
return &IntervalState[T]{
state: make([]T, intervalIotaMax+1),
state: make([]intervalStateEntry[T], intervalIotaMax+1),
}
}
func (s *IntervalState[T]) Get(interval Interval) T {
i := intervalIotas[interval]
return s.state[i]
return s.state[i].V
}
func (s *IntervalState[T]) setI(i int, v T) {
s.state[i].V = v
s.state[i].Exists = true
}
func (s *IntervalState[T]) Set(interval Interval, v T) {
i := intervalIotas[interval]
s.state[i] = v
s.setI(i, v)
}
func (s *IntervalState[T]) ComputeIfAbsent(interval Interval, computeV func() T) (r T) {
i := intervalIotas[interval]
if s.state[i].Exists {
return s.state[i].V
}
v := computeV()
s.setI(i, v)
return v
}
func (s *IntervalState[T]) Range(f func(interval Interval, v T)) {
for i, interval := range iotasIntervals {
index := i + 1 // 0保留
v := s.state[index]
f(interval, v)
f(interval, v.V)
}
}
@ -158,7 +178,7 @@ func (s *IntervalState[T]) RangeBreak(f func(interval Interval, v T) bool) {
for i, interval := range iotasIntervals {
index := i + 1 // 0保留
v := s.state[index]
if !f(interval, v) {
if !f(interval, v.V) {
break
}
}
@ -167,7 +187,7 @@ func (s *IntervalState[T]) RangeBreak(f func(interval Interval, v T) bool) {
func (s *IntervalState[T]) SetIf(interval Interval, v T, cond func(old T) bool) {
i := intervalIotas[interval]
old := s.state[i]
if cond(old) {
s.state[i] = v
if cond(old.V) {
s.setI(i, v)
}
}

Loading…
Cancel
Save