|
|
|
|
@ -1,11 +1,14 @@
|
|
|
|
|
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
|
|
|
|
|
@ -24,9 +27,13 @@ func (m *CloseManager) SetDynamicParams(trailingPct, minProfitToTrail, profitRet
|
|
|
|
|
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) { |
|
|
|
|
func (m *CloseManager) OnKline(k types.Kline, acct *Account) (trades []*Trade) { |
|
|
|
|
if acct == nil { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
@ -36,127 +43,140 @@ func (m *CloseManager) OnKline(k types.Kline, acct *Account) (trades []Trade) {
|
|
|
|
|
|
|
|
|
|
// collect indices to close to avoid modifying slice during iteration
|
|
|
|
|
type closeTask struct { |
|
|
|
|
idx int |
|
|
|
|
cause string |
|
|
|
|
tradeId int64 |
|
|
|
|
cause string |
|
|
|
|
} |
|
|
|
|
var toClose []closeTask |
|
|
|
|
priceHigh := decimals.MustToFloat64(k.High) |
|
|
|
|
priceLow := decimals.MustToFloat64(k.Low) |
|
|
|
|
closePrice := decimals.MustToFloat64(k.Close) |
|
|
|
|
|
|
|
|
|
for i, p := range acct.Positions { |
|
|
|
|
for _, p := range acct.Positions { |
|
|
|
|
if p == nil { |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
entry := p.EntryPx |
|
|
|
|
// update peak px
|
|
|
|
|
high := decimals.MustToFloat64(k.High) |
|
|
|
|
low := decimals.MustToFloat64(k.Low) |
|
|
|
|
if p.Side == types.SideBuy { |
|
|
|
|
if high > p.PeakPx { |
|
|
|
|
p.PeakPx = high |
|
|
|
|
switch p.Side { |
|
|
|
|
case types.SideLong: |
|
|
|
|
if closePrice > p.PeakPx { |
|
|
|
|
p.PeakPx = closePrice |
|
|
|
|
} |
|
|
|
|
} else if p.Side == types.SideSell { |
|
|
|
|
if low < p.PeakPx { |
|
|
|
|
p.PeakPx = low |
|
|
|
|
case types.SideShort: |
|
|
|
|
if closePrice < p.PeakPx { |
|
|
|
|
p.PeakPx = closePrice |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if p.Side == types.SideBuy { |
|
|
|
|
// 固定止盈止损
|
|
|
|
|
if p.Side == types.SideLong { |
|
|
|
|
// stoploss
|
|
|
|
|
if m.StopLossPct > 0 && priceLow <= entry*(1-m.StopLossPct) { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "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) { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "takeprofit"}) |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
// 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 { |
|
|
|
|
if peakProfit >= m.MinProfitToTrail { // 最高盈利百分比
|
|
|
|
|
// trailing level
|
|
|
|
|
trailLevel := p.PeakPx * (1 - m.TrailingPct) |
|
|
|
|
if priceLow <= trailLevel { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "trailing"}) |
|
|
|
|
// 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 := (priceHigh - entry) / entry |
|
|
|
|
if peakProfit > 0 { |
|
|
|
|
retrace := (peakProfit - curProfit) / peakProfit |
|
|
|
|
if retrace >= m.ProfitRetracePct { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "retrace"}) |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else if p.Side == types.SideSell { |
|
|
|
|
// 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 && priceHigh >= entry*(1+m.StopLossPct) { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "stoploss"}) |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
if m.TakeProfitPct > 0 && priceLow <= entry*(1-m.TakeProfitPct) { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "takeprofit"}) |
|
|
|
|
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 priceHigh >= trailLevel { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "trailing"}) |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if m.ProfitRetracePct > 0 { |
|
|
|
|
peakProfit := (entry - p.PeakPx) / entry |
|
|
|
|
curProfit := (entry - priceLow) / entry |
|
|
|
|
if peakProfit > 0 { |
|
|
|
|
retrace := (peakProfit - curProfit) / peakProfit |
|
|
|
|
if retrace >= m.ProfitRetracePct { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "retrace"}) |
|
|
|
|
// 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-- { |
|
|
|
|
idx := toClose[j].idx |
|
|
|
|
tradeId := toClose[j].tradeId |
|
|
|
|
cause := toClose[j].cause |
|
|
|
|
if idx < 0 || idx >= len(acct.Positions) { |
|
|
|
|
continue |
|
|
|
|
var pos *Position |
|
|
|
|
var index int |
|
|
|
|
for i, p := range acct.Positions { |
|
|
|
|
if p.TradeId == tradeId { |
|
|
|
|
pos = p |
|
|
|
|
index = i |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// perform market close: side opposite
|
|
|
|
|
pos := acct.Positions[idx] |
|
|
|
|
var closeSide types.Side |
|
|
|
|
if pos.Side == types.SideBuy { |
|
|
|
|
closeSide = types.SideSell |
|
|
|
|
} else { |
|
|
|
|
closeSide = types.SideBuy |
|
|
|
|
if pos == nil { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
tr, ok := acct.ClosePosition(idx, k, k.Ts, cause) |
|
|
|
|
|
|
|
|
|
// perform market close: side opposite
|
|
|
|
|
tr, ok := acct.ClosePosition(index, pos, k, k.Ts, cause) |
|
|
|
|
if ok { |
|
|
|
|
trades = append(trades, tr) |
|
|
|
|
} |
|
|
|
|
_ = closeSide // closeSide kept for clarity if we later need it
|
|
|
|
|
} |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// CloseBySignal 根据策略信号尝试平掉相反方向的仓位。例如策略返回 SELL 时,尝试平掉所有 BUY 持仓
|
|
|
|
|
func (m *CloseManager) CloseBySignal(sigSide types.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 |
|
|
|
|
} |
|
|
|
|
@ -164,22 +184,23 @@ func (m *CloseManager) CloseBySignal(sigSide types.Side, acct *Account, k types.
|
|
|
|
|
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.SideBuy && p.Side == types.SideSell { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "signal"}) |
|
|
|
|
} else if sigSide == types.SideSell && p.Side == types.SideBuy { |
|
|
|
|
toClose = append(toClose, closeTask{idx: i, cause: "signal"}) |
|
|
|
|
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, k, k.Ts, cause) |
|
|
|
|
tr, ok := acct.ClosePosition(idx, toClose[j].pos, k, k.Ts, cause) |
|
|
|
|
if ok { |
|
|
|
|
trades = append(trades, tr) |
|
|
|
|
} |
|
|
|
|
|