Browse Source

new strategys

main
strange 10 months ago
parent
commit
a056b2336e
  1. 123
      internal/trading/backtest/account.go
  2. 197
      internal/trading/backtest/backtest.go
  3. 171
      internal/trading/backtest/close_manager.go
  4. 78
      internal/trading/backtest/context.go
  5. 35
      internal/trading/backtest/simulator.go
  6. 72
      internal/trading/backtest/types.go
  7. 3
      internal/trading/sig/account_position.go
  8. 2
      internal/trading/sig/trading_plan.go
  9. 12
      internal/trading/trading_service.go
  10. 35
      pkg/indicator/atr.go
  11. 1
      pkg/indicator/indicator_registry.go
  12. 64
      pkg/strategy/cross_star.go
  13. 4
      pkg/strategy/gold_x.go
  14. 29
      pkg/strategy/sig_strategy_params.go
  15. 2
      pkg/strategy/sig_strategy_registry.go
  16. 146
      pkg/strategy/super_trend.go
  17. 6
      pkg/strategy/super_trend_yin_yang.go
  18. 24
      pkg/trade/close_strategy.go
  19. 4
      pkg/trade/risk_strategy.go
  20. 28
      pkg/types/kline.go
  21. 14
      pkg/types/signal.go
  22. 9
      pkg/utils/collect/collect.go

123
internal/trading/backtest/account.go

@ -4,20 +4,30 @@ import (
"math" "math"
"sig-pub/pkg/types" "sig-pub/pkg/types"
"sig-pub/pkg/types/decimals" "sig-pub/pkg/types/decimals"
"sig-pub/pkg/utils/collect"
"sig-pub/pkg/utils/conver"
"time"
) )
type Account struct { type Account struct {
Cash float64 Cash float64
Positions []*Position Positions []*Position
Trades []Trade Trades []*Trade // 交易订单利润分布,回测结果echart折线显示,评估止盈止损策略效果
CloseTrades []*Trade
MaxPosPct float64 // 最大仓位占比 MaxPosPct float64 // 最大仓位占比
MaxExposurePct float64 // 最大总敞口占比 MaxExposurePct float64 // 最大总敞口占比
MaxLots float64 // 最大手数/数量 (optional) MaxLots float64 // 最大手数/数量 (optional)
Simulator *Simulator Simulator *Simulator
Stat *TradeStat // 交易统计
Profit float64
} }
func NewAccount(cash float64, sim *Simulator) *Account { func NewAccount(cash float64, sim *Simulator) *Account {
return &Account{Cash: cash, Simulator: sim, MaxPosPct: 1.0, MaxExposurePct: 1.0} return &Account{
Cash: cash, Simulator: sim, MaxPosPct: 1.0, MaxExposurePct: 1.0,
Stat: &TradeStat{},
}
} }
func (a *Account) SetRiskLimits(maxPosPct, maxExposurePct float64) { func (a *Account) SetRiskLimits(maxPosPct, maxExposurePct float64) {
@ -33,9 +43,10 @@ func (a *Account) SetRiskLimits(maxPosPct, maxExposurePct float64) {
func (a *Account) CurrentEquity(price float64) float64 { func (a *Account) CurrentEquity(price float64) float64 {
equity := a.Cash equity := a.Cash
for _, p := range a.Positions { for _, p := range a.Positions {
if p.Side == types.SideBuy { switch p.Side {
case types.SideLong:
equity += (price - p.EntryPx) * p.Qty equity += (price - p.EntryPx) * p.Qty
} else if p.Side == types.SideSell { case types.SideShort:
equity += (p.EntryPx - price) * p.Qty equity += (p.EntryPx - price) * p.Qty
} }
} }
@ -51,6 +62,14 @@ func (a *Account) CurrentExposure(price float64) float64 {
return sum return sum
} }
func (a *Account) PositionCost() float64 {
var sum float64
for _, p := range a.Positions {
sum += math.Abs(p.Qty * p.EntryPx)
}
return sum
}
// CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct) // CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct)
func (a *Account) CanOpen(side types.Side, qty, price float64) bool { func (a *Account) CanOpen(side types.Side, qty, price float64) bool {
if qty <= 0 || price <= 0 { if qty <= 0 || price <= 0 {
@ -77,66 +96,82 @@ func (a *Account) CanOpen(side types.Side, qty, price float64) bool {
} }
// ApplyMarketOrder 直接用市价下单(简化),qty为基础货币数量 // ApplyMarketOrder 直接用市价下单(简化),qty为基础货币数量
func (a *Account) ApplyMarketOrder(side types.Side, qty float64, klineTs int64, kline types.Kline) (t Trade, ok bool) { func (a *Account) ApplyMarketOrder(side types.Side, qty float64, price float64, ts int64) (t *Trade, ok bool) {
// risk check before executing
price := decimals.MustToFloat64(kline.Close)
if !a.CanOpen(side, qty, price) { if !a.CanOpen(side, qty, price) {
return t, false return t, false
} }
trade := a.Simulator.ExecuteMarket(side, qty, kline, klineTs) trade, ok := a.Simulator.ExecuteMarket(side, qty, price, ts)
// apply cash/position if !ok {
if side == types.SideBuy { return
cost := trade.Price*qty + trade.Fee
if cost > a.Cash {
return trade, false
}
a.Cash -= cost
// 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 == types.SideSell {
// simplify: allow short by increasing cash
receive := trade.Price*qty - trade.Fee
a.Cash += receive
pos := &Position{Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: klineTs, PeakPx: trade.Price}
a.Positions = append(a.Positions, pos)
} }
// apply cash/position, short side also need earnest money
cost := trade.Price*qty + trade.Fee
if cost > a.Cash {
return trade, false
}
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.Trades = append(a.Trades, trade)
{
a.Stat.TotalTrades++
a.Stat.Fee += trade.Fee
}
return trade, true return trade, true
} }
// ClosePosition 根据索引平仓(全部平仓该仓位) // ClosePosition 根据索引平仓(全部平仓该仓位)
func (a *Account) ClosePosition(index int, kline types.Kline, ts int64, cause string) (t Trade, ok bool) { func (a *Account) ClosePosition(index int, pos *Position, kline types.Kline, ts int64, cause string) (t *Trade, ok bool) {
if index < 0 || index >= len(a.Positions) {
return t, false
}
pos := a.Positions[index]
if pos == nil {
return t, false
}
// determine close side (opposite) // determine close side (opposite)
var closeSide types.Side var closeSide types.Side
if pos.Side == types.SideBuy { if pos.Side == types.SideLong {
closeSide = types.SideSell closeSide = types.SideShort
} else { } else {
closeSide = types.SideBuy closeSide = types.SideLong
} }
t = a.Simulator.ExecuteMarket(closeSide, pos.Qty, kline, ts) closePrice := decimals.MustToFloat64(kline.Close)
t, ok = a.Simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, ts)
if !ok {
return t, false
}
t.CloseCause = cause t.CloseCause = cause
// apply cash change // apply cash change
if closeSide == types.SideSell { // calc profit
// selling a long position -> receive cash var receive, profit float64
receive := t.Price*pos.Qty - t.Fee if pos.Side == types.SideLong {
a.Cash += receive profit = t.Qty*(t.Price-pos.EntryPx) - t.Fee - pos.Fee
receive = t.Price*pos.Qty - t.Fee
} else {
profit = (pos.EntryPx - t.Price) * t.Qty
receive = pos.EntryPx*pos.Qty + profit - t.Fee
profit = profit - t.Fee - pos.Fee
}
a.Cash += receive
a.Profit += profit
if profit > 0 {
a.Stat.WinningTrades++
} else { } else {
// buying to close a short -> pay cash a.Stat.LosingTrades++
cost := t.Price*pos.Qty + t.Fee
a.Cash -= cost
} }
a.Stat.Fee += t.Fee
// remove position // remove position
a.Positions = append(a.Positions[:index], a.Positions[index+1:]...) a.Positions = append(a.Positions[:index], a.Positions[index+1:]...)
a.Trades = append(a.Trades, t) a.CloseTrades = append(a.CloseTrades, t)
trade, ok := collect.Find(a.Trades, func(t *Trade) bool { return t.Id == pos.TradeId })
if 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, ".")
}
return t, true return t, true
} }

197
internal/trading/backtest/backtest.go

@ -11,6 +11,7 @@ import (
"sig-pub/pkg/trade" "sig-pub/pkg/trade"
"sig-pub/pkg/types" "sig-pub/pkg/types"
"sig-pub/pkg/types/decimals" "sig-pub/pkg/types/decimals"
"sig-pub/pkg/utils/collect"
"sig-pub/pkg/utils/times" "sig-pub/pkg/utils/times"
"sig-pub/pkg/zlog" "sig-pub/pkg/zlog"
@ -20,28 +21,19 @@ import (
type Backtest struct { type Backtest struct {
exchangeClient pb.ExchangeServiceClient exchangeClient pb.ExchangeServiceClient
indReg *indicator.IndicatorRegistry indReg *indicator.IndicatorRegistry
sigStrategyReg *strategy.SigStrategyRegistry
account *Account
riskStrategy *trade.RiskStrategy riskStrategy *trade.RiskStrategy
closeManager *CloseManager
} }
func NewBacktest(exchangeClient pb.ExchangeServiceClient, indReg *indicator.IndicatorRegistry, sigStrategyReg *strategy.SigStrategyRegistry) *Backtest { 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{ return &Backtest{
exchangeClient: exchangeClient, exchangeClient: exchangeClient,
indReg: indReg, indReg: indReg,
riskStrategy: trade.NewRiskStrategy(), 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) { 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 plan := tradingPlan.Plan
exchange := pb.ExchangeType(plan.Exchange) exchange := pb.ExchangeType(plan.Exchange)
interval := types.Interval(plan.Interval) interval := types.Interval(plan.Interval)
@ -54,6 +46,12 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP
return return
} }
// 回测账户
account := NewAccount(10000, NewSimulator(0.0005, 0.0008))
// 平仓管理器
closeManager := NewCloseManager(0.02, 0)
closeManager.SetDynamicParams(0.1, 0.02, 0)
seriesRange := &pb.SeriesRange{ seriesRange := &pb.SeriesRange{
Exchange: exchange, Exchange: exchange,
InstId: instId, InstId: instId,
@ -67,25 +65,31 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP
} }
// fetch history klines via stream // fetch history klines via stream
req := &pb.ReqHistoryKlineStream{Series: seriesRange} req := &pb.ReqHistoryKlineStream{Series: seriesRange}
stream, err := b.exchangeClient.HistoryKlineStream(context.Background(), req, grpc.UseCompressor("snappy")) stream, err := b.exchangeClient.HistoryKlineStream(ctx, req, grpc.UseCompressor("snappy"))
if err != nil { if err != nil {
return return
} }
recvTimes, total := 0, 0
watch := times.NewWatch() watch := times.NewWatch()
recvTimes, recvTotal := 0, 0
var lastK *types.Kline var lastK *types.Kline
var msg *pb.RspHistoryKlineStream
for { for {
msg, err0 := stream.Recv() select {
if err0 == io.EOF { case <-ctx.Done():
err = ctx.Err()
return
default:
}
msg, err = stream.Recv()
if err == io.EOF {
break break
} }
if err0 != nil { if err != nil {
err = err0
return return
} }
recvTimes++ recvTimes++
total += len(msg.Klines) recvTotal += len(msg.Klines)
for _, k := range msg.Klines { for _, k := range msg.Klines {
kline := new(types.Kline) kline := new(types.Kline)
kline.ParsePBKline(seriesRange.Exchange, k) kline.ParsePBKline(seriesRange.Exchange, k)
@ -100,166 +104,35 @@ func (b *Backtest) RunTradingPlan(ctx context.Context, tradingPlan *sig.TradingP
} }
// 平仓策略 // 平仓策略
b.closeManager.OnKline(*kline, b.account) closeManager.OnKline(*kline, account)
sigSide := tradingPlan.Update(strategy.StrategyTypeSig) sigSide := tradingPlan.Update(strategy.StrategyTypeSig)
if sigSide.IsValid() { if sigSide.IsValid() {
b.onSigSideSignal(sigSide, *kline) b.onSigSideSignalWithAccount(sigSide, *kline, account, closeManager)
} }
} }
} }
// build result _ = lastK
res := &BacktestResult{} zlog.Debugf("recv=%d, total=%d, use %s", recvTimes, recvTotal, watch.ElapsedFmt("."))
res.StartTs = 0 collect.SortDesc(account.Trades, func(t *Trade) float64 { return t.Pnl })
res.EndTs = 0 exposure := account.Cash + account.PositionCost()
acct := b.account _ = exposure
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 return
} }
// onSigSideSignal 交易策略发出交易信号 // onSigSideSignalWithAccount 交易策略发出交易信号(使用指定的账户和平仓管理器)
func (b *Backtest) onSigSideSignal(sigSide types.Side, k types.Kline) { func (b *Backtest) onSigSideSignalWithAccount(sigSide types.Side, k types.Kline, account *Account, closeManager *CloseManager) {
// risk check before executing
side := b.riskStrategy.SideAssess(sigSide) side := b.riskStrategy.SideAssess(sigSide)
if !side.IsValid() { if !side.IsValid() {
zlog.Debugf("risk strategy filter sig side: %s", sigSide.String()) zlog.Debugf("risk strategy filter sig side: %s", sigSide.String())
return return
} }
zlog.Debugf("apply market order: ts=%d, side=%s", k.Ts, side.String()) // zlog.Debugf("apply market order: ts=%d, side=%s", k.Ts, side.String())
b.account.ApplyMarketOrder(side, 0.01, k.Ts, k) price := decimals.MustToFloat64(k.Close)
account.ApplyMarketOrder(side, 0.01, price, k.Ts)
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 closeManager.CloseBySignal(sigSide, account, k)
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
} }

171
internal/trading/backtest/close_manager.go

@ -1,11 +1,14 @@
package backtest package backtest
import ( import (
"sig-pub/pkg/trade"
"sig-pub/pkg/types" "sig-pub/pkg/types"
"sig-pub/pkg/types/decimals" "sig-pub/pkg/types/decimals"
) )
// CloseManager 管理持仓平仓逻辑:stoploss/takeprofit 与 基于信号的平仓 // CloseManager 管理持仓平仓逻辑:stoploss/takeprofit 与 基于信号的平仓
// 1.交易信号和持单方向相反时是否进行平仓
// 2.计算止盈止损时是否包含手续费
type CloseManager struct { type CloseManager struct {
StopLossPct float64 // static stoploss StopLossPct float64 // static stoploss
TakeProfitPct float64 // static take profit TakeProfitPct float64 // static take profit
@ -24,9 +27,13 @@ func (m *CloseManager) SetDynamicParams(trailingPct, minProfitToTrail, profitRet
m.ProfitRetracePct = profitRetracePct m.ProfitRetracePct = profitRetracePct
} }
func (m *CloseManager) Init(param trade.CloseStrategyParam) {
}
// OnKline 根据最新 kline 检查是否触发 stoploss 或 takeprofit,触发则平仓(市价) // 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 { if acct == nil {
return 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 // collect indices to close to avoid modifying slice during iteration
type closeTask struct { type closeTask struct {
idx int tradeId int64
cause string cause string
} }
var toClose []closeTask var toClose []closeTask
priceHigh := decimals.MustToFloat64(k.High) closePrice := decimals.MustToFloat64(k.Close)
priceLow := decimals.MustToFloat64(k.Low)
for i, p := range acct.Positions { for _, p := range acct.Positions {
if p == nil { if p == nil {
continue continue
} }
entry := p.EntryPx entry := p.EntryPx
// update peak px // update peak px
high := decimals.MustToFloat64(k.High) switch p.Side {
low := decimals.MustToFloat64(k.Low) case types.SideLong:
if p.Side == types.SideBuy { if closePrice > p.PeakPx {
if high > p.PeakPx { p.PeakPx = closePrice
p.PeakPx = high
} }
} else if p.Side == types.SideSell { case types.SideShort:
if low < p.PeakPx { if closePrice < p.PeakPx {
p.PeakPx = low p.PeakPx = closePrice
} }
} }
if p.Side == types.SideBuy { // 固定止盈止损
if p.Side == types.SideLong {
// stoploss // stoploss
if m.StopLossPct > 0 && priceLow <= entry*(1-m.StopLossPct) { if m.StopLossPct > 0 && closePrice <= entry*(1-m.StopLossPct) {
toClose = append(toClose, closeTask{idx: i, cause: "stoploss"}) acct.Stat.StoplossTimes++
toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "stoploss"})
continue continue
} }
// takeprofit // takeprofit
if m.TakeProfitPct > 0 && priceHigh >= entry*(1+m.TakeProfitPct) { // if m.TakeProfitPct > 0 && priceHigh >= entry*(1+m.TakeProfitPct) {
toClose = append(toClose, closeTask{idx: i, cause: "takeprofit"}) // acct.Stat.TakeprofitTimes++
continue // toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "takeprofit"})
} // continue
// }
// dynamic trailing stop based on peak price // dynamic trailing stop based on peak price
if m.TrailingPct > 0 && m.MinProfitToTrail > 0 { if m.TrailingPct > 0 && m.MinProfitToTrail > 0 {
// peak profit fraction // peak profit fraction
peakProfit := (p.PeakPx - entry) / entry peakProfit := (p.PeakPx - entry) / entry
if peakProfit >= m.MinProfitToTrail { if peakProfit >= m.MinProfitToTrail { // 最高盈利百分比
// trailing level // trailing level
trailLevel := p.PeakPx * (1 - m.TrailingPct) // trailLevel := p.PeakPx * (1 - m.TrailingPct)
if priceLow <= trailLevel { // if closePrice <= trailLevel {
toClose = append(toClose, closeTask{idx: i, cause: "trailing"}) trail := (p.PeakPx - entry) * (1 - m.TrailingPct)
if (closePrice - entry) < trail {
acct.Stat.TrailingTimes++
toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "trailing"})
continue continue
} }
} }
} }
// profit retrace rule: if peakProfit>0 and current retrace > ProfitRetracePct // profit retrace rule: if peakProfit>0 and current retrace > ProfitRetracePct
if m.ProfitRetracePct > 0 { // if m.ProfitRetracePct > 0 {
peakProfit := (p.PeakPx - entry) / entry // peakProfit := (p.PeakPx - entry) / entry
curProfit := (priceHigh - entry) / entry // curProfit := (closePrice - entry) / entry
if peakProfit > 0 { // if peakProfit > 0 {
retrace := (peakProfit - curProfit) / peakProfit // retrace := (peakProfit - curProfit) / peakProfit
if retrace >= m.ProfitRetracePct { // if retrace >= m.ProfitRetracePct {
toClose = append(toClose, closeTask{idx: i, cause: "retrace"}) // acct.Stat.RetraceTimes++
continue // toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "retrace"})
} // continue
} // }
} // }
} else if p.Side == types.SideSell { // }
} else if p.Side == types.SideShort {
// short: stoploss if high >= entry*(1+stop), takeprofit if low <= entry*(1-tp) // short: stoploss if high >= entry*(1+stop), takeprofit if low <= entry*(1-tp)
if m.StopLossPct > 0 && priceHigh >= entry*(1+m.StopLossPct) { if m.StopLossPct > 0 && closePrice >= entry*(1+m.StopLossPct) {
toClose = append(toClose, closeTask{idx: i, cause: "stoploss"}) acct.Stat.StoplossTimes++
continue toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "stoploss"})
}
if m.TakeProfitPct > 0 && priceLow <= entry*(1-m.TakeProfitPct) {
toClose = append(toClose, closeTask{idx: i, cause: "takeprofit"})
continue 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) // update trailing for short based on PeakPx (lower is better for short)
if m.TrailingPct > 0 && m.MinProfitToTrail > 0 { if m.TrailingPct > 0 && m.MinProfitToTrail > 0 {
peakProfit := (entry - p.PeakPx) / entry peakProfit := (entry - p.PeakPx) / entry
if peakProfit >= m.MinProfitToTrail { if peakProfit >= m.MinProfitToTrail {
trailLevel := p.PeakPx * (1 + m.TrailingPct) // trailLevel := p.PeakPx * (1 + m.TrailingPct)
if priceHigh >= trailLevel { // if closePrice >= trailLevel {
toClose = append(toClose, closeTask{idx: i, cause: "trailing"}) trail := (entry - p.PeakPx) * (1 - m.TrailingPct)
continue if (entry - closePrice) < trail {
} acct.Stat.TrailingTimes++
} toClose = append(toClose, closeTask{tradeId: p.TradeId, cause: "trailing"})
}
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"})
continue 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) // close collected positions (process from high index to low to safely remove)
for j := len(toClose) - 1; j >= 0; j-- { for j := len(toClose) - 1; j >= 0; j-- {
idx := toClose[j].idx tradeId := toClose[j].tradeId
cause := toClose[j].cause cause := toClose[j].cause
if idx < 0 || idx >= len(acct.Positions) { var pos *Position
continue var index int
for i, p := range acct.Positions {
if p.TradeId == tradeId {
pos = p
index = i
break
}
} }
// perform market close: side opposite if pos == nil {
pos := acct.Positions[idx] return
var closeSide types.Side
if pos.Side == types.SideBuy {
closeSide = types.SideSell
} else {
closeSide = types.SideBuy
} }
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 { if ok {
trades = append(trades, tr) trades = append(trades, tr)
} }
_ = closeSide // closeSide kept for clarity if we later need it
} }
return return
} }
// CloseBySignal 根据策略信号尝试平掉相反方向的仓位。例如策略返回 SELL 时,尝试平掉所有 BUY 持仓 // 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 { if acct == nil {
return return
} }
@ -164,22 +184,23 @@ func (m *CloseManager) CloseBySignal(sigSide types.Side, acct *Account, k types.
type closeTask struct { type closeTask struct {
idx int idx int
cause string cause string
pos *Position
} }
var toClose []closeTask var toClose []closeTask
for i, p := range acct.Positions { for i, p := range acct.Positions {
if p == nil { if p == nil {
continue continue
} }
if sigSide == types.SideBuy && p.Side == types.SideSell { if sigSide == types.SideLong && p.Side == types.SideShort {
toClose = append(toClose, closeTask{idx: i, cause: "signal"}) toClose = append(toClose, closeTask{idx: i, cause: "signal", pos: p})
} else if sigSide == types.SideSell && p.Side == types.SideBuy { } else if sigSide == types.SideShort && p.Side == types.SideLong {
toClose = append(toClose, closeTask{idx: i, cause: "signal"}) toClose = append(toClose, closeTask{idx: i, cause: "signal", pos: p})
} }
} }
for j := len(toClose) - 1; j >= 0; j-- { for j := len(toClose) - 1; j >= 0; j-- {
idx := toClose[j].idx idx := toClose[j].idx
cause := toClose[j].cause 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 { if ok {
trades = append(trades, tr) trades = append(trades, tr)
} }

78
internal/trading/backtest/context.go

@ -1,78 +0,0 @@
package backtest
import (
"sig-pub/pkg/indicator"
"sig-pub/pkg/types"
"sig-pub/pkg/types/series"
)
// SigStrategyContext 是一个轻量的策略上下文,用于回测时把历史k线提供给策略
type SigStrategyContext struct {
klines []types.Kline // 时间升序: oldest ... newest
offset int16 // offset applied when indicators request
indReg *indicator.IndicatorRegistry
}
func NewSigStrategyContext(klines []types.Kline, indReg *indicator.IndicatorRegistry) *SigStrategyContext {
return &SigStrategyContext{klines: klines, indReg: indReg}
}
func (c *SigStrategyContext) Get(offset int16) (k types.Kline) {
// offset relative to current (0 = latest)
idx := len(c.klines) - 1 - int(offset+c.offset)
if idx < 0 {
// return zero kline if out of range
return types.Kline{}
}
return c.klines[idx]
}
func (c *SigStrategyContext) Series(offset, count int16) (klines series.Klines) {
// return slice in descending time order as expected by series.Klines
var ret series.Klines
for i := int16(0); i < count; i++ {
k := c.Get(offset + i)
ret = append(ret, k)
}
return ret
}
// WindowIndicatorSeriesLocal 实现 indicator.IIndicatorSeries
type WindowIndicatorSeriesLocal struct {
window int16
ind indicator.IWindowIndicator
ctx *SigStrategyContext
}
func NewWindowIndicatorSeriesLocal(window int16, ind indicator.IWindowIndicator, ctx *SigStrategyContext) *WindowIndicatorSeriesLocal {
return &WindowIndicatorSeriesLocal{window: window, ind: ind, ctx: ctx}
}
func (w *WindowIndicatorSeriesLocal) Get(offset int16) (vector float64) {
// tell indicator to use offset by shifting internal offset then restore
prev := w.ctx.offset
w.ctx.offset += offset
vector = w.ind.Calculate(w.ctx, w.window)
w.ctx.offset = prev
return
}
func (w *WindowIndicatorSeriesLocal) Series(offset, count int16) (matrix series.Floats) {
prev := w.ctx.offset
w.ctx.offset += offset
for i := int16(0); i < count; i++ {
v := w.ind.Calculate(w.ctx, w.window)
matrix.Push(v)
w.ctx.offset++
}
w.ctx.offset = prev
return
}
func (c *SigStrategyContext) IndicatorW(name string, window int16) indicator.IIndicatorSeries {
ind, ok := c.indReg.IndicatorW(name)
if !ok {
panic("indicator not found: " + name)
}
return NewWindowIndicatorSeriesLocal(window, ind, c)
}

35
internal/trading/backtest/simulator.go

@ -9,6 +9,8 @@ import (
type Simulator struct { type Simulator struct {
FeePct float64 // e.g. 0.0005 = 0.05% FeePct float64 // e.g. 0.0005 = 0.05%
SlippagePct float64 // e.g. 0.001 = 0.1% SlippagePct float64 // e.g. 0.001 = 0.1%
TradeId int64
} }
func NewSimulator(feePct, slippagePct float64) *Simulator { func NewSimulator(feePct, slippagePct float64) *Simulator {
@ -16,42 +18,49 @@ func NewSimulator(feePct, slippagePct float64) *Simulator {
} }
// ExecuteMarket 执行市价单,使用kline信息决定成交价(使用close以及滑点) // ExecuteMarket 执行市价单,使用kline信息决定成交价(使用close以及滑点)
func (s *Simulator) ExecuteMarket(side types.Side, qty float64, k types.Kline, ts int64) (trade Trade) { func (s *Simulator) ExecuteMarket(side types.Side, qty float64, closePrice float64, ts int64) (trade *Trade, ok bool) {
// base price use close // base price use close
base := decimals.MustToFloat64(k.Close) base := closePrice
slippage := s.SlippagePct slippage := s.SlippagePct
if side == types.SideSell { switch side {
// sell: worse price lower case types.SideLong:
base = base * (1 - slippage)
} else {
// buy: worse price higher // buy: worse price higher
base = base * (1 + slippage) base = base * (1 + slippage)
case types.SideShort:
// sell: worse price lower
base = base * (1 - slippage)
default:
return
} }
fee := math.Abs(base*qty) * s.FeePct fee := math.Abs(base*qty) * s.FeePct
trade = Trade{Side: side, Qty: qty, Price: base, Fee: fee, Ts: ts} trade = &Trade{Side: side, Qty: qty, Price: base, Fee: fee, Ts: ts}
s.TradeId++
trade.Id = s.TradeId
ok = true
return return
} }
// ExecuteLimit 简单实现: 如果limit价格被kline的high/low包含则成交 // ExecuteLimit 简单实现: 如果limit价格被kline的high/low包含则成交
func (s *Simulator) ExecuteLimit(side types.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) (trade Trade, filled bool) {
h := decimals.MustToFloat64(k.High) h := decimals.MustToFloat64(k.High)
l := decimals.MustToFloat64(k.Low) l := decimals.MustToFloat64(k.Low)
if side == types.SideBuy { switch side {
case types.SideLong:
// buy limit: filled if low <= price // buy limit: filled if low <= price
if l <= limitPx { if l <= limitPx {
// assume filled at min(limitPx, open) // assume filled at min(limitPx, open)
px := math.Min(limitPx, decimals.MustToFloat64(k.Open)) px := math.Min(limitPx, decimals.MustToFloat64(k.Open))
fee := math.Abs(px*qty) * s.FeePct fee := math.Abs(px*qty) * s.FeePct
trade = Trade{Side: side, Qty: qty, Price: px * (1 + s.SlippagePct), Fee: fee, Ts: ts} trade = Trade{Side: side, Qty: qty, Price: px * (1 + s.SlippagePct), Fee: fee, Ts: ts}
return true, trade return trade, true
} }
} else if side == types.SideSell { case types.SideShort:
if h >= limitPx { if h >= limitPx {
px := math.Max(limitPx, decimals.MustToFloat64(k.Open)) px := math.Max(limitPx, decimals.MustToFloat64(k.Open))
fee := math.Abs(px*qty) * s.FeePct fee := math.Abs(px*qty) * s.FeePct
trade = Trade{Side: side, Qty: qty, Price: px * (1 - s.SlippagePct), Fee: fee, Ts: ts} trade = Trade{Side: side, Qty: qty, Price: px * (1 - s.SlippagePct), Fee: fee, Ts: ts}
return true, trade return trade, true
} }
} }
return false, trade return trade, false
} }

72
internal/trading/backtest/types.go

@ -1,21 +1,27 @@
package backtest package backtest
import "sig-pub/pkg/types" import (
"sig-pub/pkg/types"
)
type Side = types.Side type Side = types.Side
type Position struct { type Position struct {
Side Side TradeId int64
Qty float64 Side Side // 交易方向
EntryPx float64 Qty float64 // 交易量
EntryTs int64 EntryPx float64 // 入场价格
EntryTs int64 // 入场时间
PeakPx float64 // highest (for long) or lowest (for short) observed price since entry 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 { type Trade struct {
Id int64 // 交易id Id int64 // 交易id
Side types.Side // 交易方向 Side types.Side // 交易方向
Status int32 // 1.交易中 2.持仓中 3.已平仓
Qty float64 // 交易量 Qty float64 // 交易量
Price float64 // 开仓价格 Price float64 // 开仓价格
Fee float64 // 开仓手续费 Fee float64 // 开仓手续费
@ -24,6 +30,8 @@ type Trade struct {
CloseFee float64 // 平仓手续费 CloseFee float64 // 平仓手续费
CloseTs int64 // 平仓时间 CloseTs int64 // 平仓时间
CloseCause string // 平仓原因 ["stoploss", "takeprofit", "trailing", "retrace", "signal"](“止损”、“止盈”、“动态跟踪”、“回撤”、“信号”) 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 { type BacktestResult struct {
@ -34,3 +42,55 @@ type BacktestResult struct {
Cash float64 Cash float64
Equity float64 Equity float64
} }
type TradeStat struct {
// 消耗
Fee float64 // 总手续费
StoplossTimes int
TakeprofitTimes int
TrailingTimes int
RetraceTimes int
// Trade Statistics (交易统计)
WinRate float64 `json:"win_rate"` // 胜率
TotalTrades int64 `json:"total_trades"` // 交易次数
WinningTrades int64 `json:"winning_trades"` // 盈利次数
LosingTrades int64 `json:"losing_trades"` // 亏损次数
// // Capital (本金)
// Principal float64 `json:"principal"` // 本金
// MaxCapitalUsedAmount float64 `json:"max_capital_used_amount"` // 最大资金使用金额
// MaxCapitalUsedAmountTime int64 `json:"max_capital_used_amount_time"` // 最大资金使用金额时间
// MaxCapitalUsedAmountRatio float64 `json:"max_capital_used_amount_ratio"` // 最大资金使用金额比率
// MaxCapitalUsedRatioTime string `json:"max_capital_used_ratio_time"` // 最大资金使用金额比率时间
// // Equity (权益)
// MaxEquityDuringBacktest float64 `json:"max_equity_during_backtest"` // 回测期间最大权益
// MinEquityDuringBacktest float64 `json:"min_equity_during_backtest"` // 回测期间最小权益
// FinalEquity float64 `json:"final_equity"` // 最终权益
// Profit float64 `json:"profit"` // 收益
// // Returns (收益率)
// ReturnRate float64 `json:"return_rate"` // 收益率
// CumulativeReturnRate float64 `json:"cumulative_return_rate"` // 累积收益率
// AnnualizedSimpleReturnRate float64 `json:"annualized_simple_return_rate"` // 年化单利收益率
// MonthlySimpleReturnRate float64 `json:"monthly_simple_return_rate"` // 月化单利收益率
// AnnualizedCompoundReturnRate float64 `json:"annualized_compound_return_rate"` // 年化复利收益率
// MonthlyCompoundReturnRate float64 `json:"monthly_compound_return_rate"` // 月化复利收益率
// // Risk Metrics (风险指标)
// SharpeRatio float64 `json:"sharpe_ratio"` // 夏普比率
// SortinoRatio float64 `json:"sortino_ratio"` // 索提诺比率
// EquityVolatility float64 `json:"equity_volatility"` // 权益离散度
// EquityVolatilityCoefficient float64 `json:"equity_volatility_coefficient"` // 权益离散系数
// PrincipalRiskRate float64 `json:"principal_risk_rate"` // 本金风险率
// AnnualizedReturnRiskRatio float64 `json:"annualized_return_risk_ratio"` // 年化收益风险比率
// // Drawdown (回撤)
// MaxDrawdownAmount float64 `json:"max_drawdown_amount"` // 权益/盈亏最大回撤
// MaxDrawdownTime string `json:"max_drawdown_time"` // 权益/盈亏最大回撤时间
// MaxDrawdownRatio float64 `json:"max_drawdown_ratio"` // 权益/盈亏最大回撤比率
// MaxDrawdownRatioTime string `json:"max_drawdown_ratio_time"` // 权益/盈亏最大回撤比率时间
// LongestDaysWithoutNewHigh int64 `json:"longest_days_without_new_high"` // 权益/盈亏最长未创新高天数
// LongestPeriodWithoutNewHigh string `json:"longest_period_without_new_high"` // 权益/盈亏最长未创新高时间段
}

3
internal/trading/sig/account_position.go

@ -1,3 +0,0 @@
package sig
// 账户持仓管理 -> riskManager 风险管理

2
internal/trading/sig/trading_plan.go

@ -15,8 +15,6 @@ type TradingPlan struct {
sigStrategy strategy.ISigStrategy sigStrategy strategy.ISigStrategy
sigStrategyContext strategy.ISigStrategyContext sigStrategyContext strategy.ISigStrategyContext
// publisher publish.Publisher[int32, any]
// signalKey map[string]int32
} }
func NewTradingPlan(plan entity.TradePlan, indicatorReg *indicator.IndicatorRegistry) *TradingPlan { func NewTradingPlan(plan entity.TradePlan, indicatorReg *indicator.IndicatorRegistry) *TradingPlan {

12
internal/trading/trading_service.go

@ -202,6 +202,7 @@ func (svc *TradingService) IndicatorSeries(indicatorName string, window uint32,
} }
// StrategySeries 简单策略信号测试 // StrategySeries 简单策略信号测试
// todo 去掉 HistoryIndicatorContext, 像backtest使用stream来一个算一个
func (svc *TradingService) StrategySeries(req *pb.ReqStrategySeries, rsp *pb.RspStrategySeries) (err error) { func (svc *TradingService) StrategySeries(req *pb.ReqStrategySeries, rsp *pb.RspStrategySeries) (err error) {
// sigStrategy // sigStrategy
sigStrategy, ok := svc.strategyReg.NewSigStrategy(req.SigStrategy) sigStrategy, ok := svc.strategyReg.NewSigStrategy(req.SigStrategy)
@ -227,7 +228,14 @@ func (svc *TradingService) StrategySeries(req *pb.ReqStrategySeries, rsp *pb.Rsp
// return // return
// } // }
// recover todo out of range // recover todo out of range
sr := req.Series
exchange := sr.Exchange
series := sig.NewKlineSeries(exchange, sr.InstId, interval)
count, totalK := 0, 0 count, totalK := 0, 0
indctx := sig.NewIndicatorContext(series)
_ = indctx
indicatorContext := sig.NewHistoryIndicatorContext(svc.exchangeClient) indicatorContext := sig.NewHistoryIndicatorContext(svc.exchangeClient)
req.Series.Window += indicator.MaxWindow req.Series.Window += indicator.MaxWindow
if totalK, err = indicatorContext.Init(req.Series); err != nil { if totalK, err = indicatorContext.Init(req.Series); err != nil {
@ -239,8 +247,8 @@ func (svc *TradingService) StrategySeries(req *pb.ReqStrategySeries, rsp *pb.Rsp
for i := count - 1; i >= 0; i-- { for i := count - 1; i >= 0; i-- {
strategyContext.SetOffset(int16(i)) strategyContext.SetOffset(int16(i))
sigSide := sigStrategy.Update(strategyContext) sigSide := sigStrategy.Update(strategyContext)
if sigSide == types.SideBuy || sigSide == types.SideSell { if sigSide == types.SideLong || sigSide == types.SideShort {
side := lang.Ternary(sigSide == types.SideBuy, pb.Side_BUY, pb.Side_SELL) side := lang.Ternary(sigSide == types.SideLong, pb.Side_BUY, pb.Side_SELL)
signalK := strategyContext.Get(0) signalK := strategyContext.Get(0)
rsp.Signal = append(rsp.Signal, side) rsp.Signal = append(rsp.Signal, side)
rsp.Times = append(rsp.Times, signalK.Ts) rsp.Times = append(rsp.Times, signalK.Ts)

35
pkg/indicator/atr.go

@ -0,0 +1,35 @@
package indicator
import "sig-pub/pkg/types/series"
// ATR = SMA(TR, N)
// 平均真实波幅 (ATR) atr define: https://www.investopedia.com/terms/a/atr.asp
type ATR struct {
}
// indicator interface
func (c *ATR) Name() string {
return "atr"
}
// Calculate 计算单根k线rsi指标
func (c *ATR) Calculate(ctx IIndicatorContext, window int16) (vector float64) {
klineSeries := ctx.Series(0, int16(window)+1)
highs := klineSeries.High()
lows := klineSeries.Low()
closes := klineSeries.Close()
trs := make([]float64, 0, window)
for i := range window {
high := highs[i]
low := lows[i]
close1 := closes[i+1]
// 计算TR
tr := max(high-low, high-close1, low-close1)
trs = append(trs, tr)
}
// 计算TR平均值得到ATR
seriesTR := series.NewFloats(trs...)
atr := seriesTR.Avg()
return atr
}

1
pkg/indicator/indicator_registry.go

@ -20,6 +20,7 @@ func (r *IndicatorRegistry) Init() (err error) {
// indicator regist // indicator regist
r.MustRegistIndicatorW(&RSI{}) r.MustRegistIndicatorW(&RSI{})
r.MustRegistIndicatorW(&SMA{}) r.MustRegistIndicatorW(&SMA{})
r.MustRegistIndicatorW(&ATR{})
return return
} }

64
pkg/strategy/cross_star.go

@ -0,0 +1,64 @@
package strategy
import (
"math"
"sig-pub/pkg/types"
"sig-pub/pkg/zlog"
)
// CrossStar
type CrossStar struct {
ISigStrategy
IIntervalSigStrategy
rate float64
rate2 float64
}
func (s *CrossStar) New() ISigStrategy {
return &CrossStar{}
}
func (s *CrossStar) Meta() StrategyMeta {
return StrategyMeta{
Name: "CrossStar",
Desc: "十字星策略",
Args: []Param{
{Name: "rate", Type: ParamTypeUFloat, Desc: "上线影线与基线比例"},
{Name: "rate2", Type: ParamTypeUFloat, Desc: "上线影线之间比例"},
},
}
}
func (s *CrossStar) Init(param StrategyParam) (err error) { // 校验参数, 并根据参数初始化策略
if s.rate, err = param.GetFloat64E("rate"); err != nil {
return
}
if s.rate2, err = param.GetFloat64E("rate2"); err != nil {
return
}
return
}
func (s *CrossStar) MaxWindow() int {
return int(1)
}
func (s *CrossStar) Update(ctx ISigStrategyContext) (side types.Side) {
// O 109744.8 H 110600 L 109507.5 C 109686.8
k0 := ctx.Get(0)
open, close, high, low := k0.OpenF64(), k0.CloseF64(), k0.HighF64(), k0.LowF64()
base := math.Abs(open - close) // 58
rup := (high - max(open, close)) / base // 855.2 / 2 427.6
rdown := (min(open, close) - low) / base // 179.3 / 2 89.65
if k0.Ts == 1761833700000 {
zlog.Debugf("base=%.4f, rup=%.4f, rdown=%.4f", base, rup, rdown)
}
if rup > s.rate && rup/rdown > s.rate2 {
return types.SideLong
}
if rdown > s.rate && rdown/rup > s.rate2 {
return types.SideShort
}
return
}

4
pkg/strategy/gold_x.go

@ -54,10 +54,10 @@ func (s *GoldX) Update(ctx ISigStrategyContext) (side types.Side) {
crossover := s14[0] > s28[0] && s14[1] < s28[1] // 上穿 crossover := s14[0] > s28[0] && s14[1] < s28[1] // 上穿
crossunder := s14[0] < s28[0] && s14[1] > s28[1] // 下穿 crossunder := s14[0] < s28[0] && s14[1] > s28[1] // 下穿
if crossover { if crossover {
return types.SideBuy return types.SideLong
} }
if crossunder { if crossunder {
return types.SideSell return types.SideShort
} }
return return
} }

29
pkg/strategy/sig_strategy_params.go

@ -94,6 +94,18 @@ func (s *StrategyParam) Get(key string) (v string, ok bool) {
return return
} }
func (s *StrategyParam) GetE(key string) (v string, err error) {
if len(*s) == 0 {
return
}
v, ok := (*s)[key]
if !ok {
err = fmt.Errorf("param %s not provided", key)
return
}
return
}
func (s *StrategyParam) GetInt(key string) (r int, ok bool) { func (s *StrategyParam) GetInt(key string) (r int, ok bool) {
v, ok := s.Get(key) v, ok := s.Get(key)
if !ok { if !ok {
@ -107,9 +119,8 @@ func (s *StrategyParam) GetInt(key string) (r int, ok bool) {
} }
func (s *StrategyParam) GetInt16E(key string) (r int16, err error) { func (s *StrategyParam) GetInt16E(key string) (r int16, err error) {
v, ok := s.Get(key) v, err := s.GetE(key)
if !ok { if err != nil {
err = fmt.Errorf("param %s not provided", key)
return return
} }
r, err = cast.ToInt16E(v) r, err = cast.ToInt16E(v)
@ -117,14 +128,18 @@ func (s *StrategyParam) GetInt16E(key string) (r int16, err error) {
} }
func (s *StrategyParam) GetFloat64(key string) (r float64, ok bool) { func (s *StrategyParam) GetFloat64(key string) (r float64, ok bool) {
v, ok := s.Get(key) r, err := s.GetFloat64E(key)
if !ok { if err != nil {
return return
} }
r, err := cast.ToFloat64E(v) return r, true
if ok = err == nil; !ok { }
func (s *StrategyParam) GetFloat64E(key string) (r float64, err error) {
v, err := s.GetE(key)
if err != nil {
return return
} }
r, err = cast.ToFloat64E(v)
return return
} }

2
pkg/strategy/sig_strategy_registry.go

@ -21,6 +21,8 @@ func NewSigStrategyRegistry() *SigStrategyRegistry {
func (r *SigStrategyRegistry) Init() (err error) { func (r *SigStrategyRegistry) Init() (err error) {
// indicator regist // indicator regist
r.MustRegistStrategy(&GoldX{}) r.MustRegistStrategy(&GoldX{})
r.MustRegistStrategy(&SupertrendBOSWaves{})
r.MustRegistStrategy(&CrossStar{})
return return
} }

146
pkg/strategy/super_trend.go

@ -0,0 +1,146 @@
package strategy
import (
"sig-pub/pkg/types"
"sig-pub/pkg/utils/lang"
)
type SupertrendBOSWaves struct {
ISigStrategy
atrLength int16
atrMult float64
// radiusStrength float64
// smoothness int16
prevDirection int
// anchorPrice float64
// anchorBar int
// velocity float64
// barCount int
}
func (s *SupertrendBOSWaves) New() ISigStrategy {
return &SupertrendBOSWaves{}
}
func (s *SupertrendBOSWaves) Meta() StrategyMeta {
return StrategyMeta{
Name: "SupertrendBOSWaves",
Desc: "曲线半径超级趋势 [BOSWaves] https://www.tradingview.com/script/v0Fr7PAb-Curved-Radius-Supertrend-BOSWaves/",
Args: []Param{
{Name: "atrLength", Type: ParamTypeUInt, Desc: "atr指标长度,14"},
{Name: "atrMult", Type: ParamTypeUFloat, Desc: "atr倍数,2"},
{Name: "radiusStrength", Type: ParamTypeUFloat, Desc: `
Controls curve acceleration strength.\n\n" +
"Recommended values by timeframe:\n" +
"• 1-5min (Scalping): 0.08-0.12\n" +
"• 15min: 0.12-0.15\n" +
"• 1H: 0.15-0.18\n" +
"• 4H: 0.18-0.22\n" +
"• Daily: 0.20-0.25\n" +
"• Weekly: 0.25-0.30\n\n" +
"Lower = Tighter curves (responsive)\n" +
"Higher = Wider curves (smoother)
`},
{Name: "smoothness", Type: ParamTypeUInt, Desc: "Smoothing applied to curved band. Higher = smoother curves, less noise."},
},
}
}
func (s *SupertrendBOSWaves) Init(param StrategyParam) (err error) { // 校验参数, 并根据参数初始化策略
if s.atrLength, err = param.GetInt16E("atrLength"); err != nil {
return
}
if s.atrMult, err = param.GetFloat64E("atrMult"); err != nil {
return
}
// if s.radiusStrength, err = param.GetFloat64E("radiusStrength"); err != nil {
// return
// }
// if s.smoothness, err = param.GetInt16E("smoothness"); err != nil {
// return
// }
return
}
func (s *SupertrendBOSWaves) MaxWindow() int {
return int(s.atrLength + 1)
}
func (s *SupertrendBOSWaves) Update(ctx ISigStrategyContext) (side types.Side) {
k0 := ctx.Get(0)
high, low, close := k0.HighF64(), k0.LowF64(), k0.CloseF64()
atr := ctx.IndicatorW("atr", s.atrLength).Get(0)
src := (high + low) / 2
// src := k0.HL2()
upperBand := src + (s.atrMult * atr)
lowerBand := src - (s.atrMult * atr)
supertrend := lowerBand
direction := 1
// Standard supertrend logic
prevSupertrend := supertrend
if direction == 1 {
supertrend = lang.Ternary(close < prevSupertrend, upperBand, max(lowerBand, prevSupertrend))
} else {
supertrend = lang.Ternary(close > prevSupertrend, lowerBand, min(upperBand, prevSupertrend))
}
s.prevDirection = direction
if close < supertrend {
direction = -1
}
if close > supertrend {
direction = 1
}
// ============================================================================
// Curved Radius Implementation
// ============================================================================
// Detect trend change - set new anchor
trendChanged := s.prevDirection != 0 && direction != s.prevDirection
buySignal := trendChanged && direction == 1
sellSignal := trendChanged && direction == -1
if buySignal {
return types.SideLong
}
if sellSignal {
return types.SideShort
}
// if trendChanged {
// s.anchorPrice = supertrend
// s.anchorBar = 0 //bar_index
// s.velocity = 0.0
// s.barCount = 0
// }
// // Increment bar counter
// s.barCount = s.barCount + 1
// // Calculate curved offset using acceleration creating a parabolic curve
// // todo if not na(anchorPrice)
// if trendChanged {
// // Acceleration increases with each bar (quadratic growth)
// s.velocity = s.velocity + (s.radiusStrength * float64(s.barCount))
// }
// // Apply velocity in direction of trend
// if direction == 1 {
// // Uptrend - curve upward with acceleration
// supertrend = s.anchorPrice + s.velocity
// } else {
// // Downtrend - curve downward with acceleration
// supertrend = s.anchorPrice - s.velocity
// }
// Apply smoothing to create flowing curves
// curvedBand = ta.sma(supertrend, s.smoothness)
return
}

6
pkg/strategy/super_trend_yin_yang.go

@ -0,0 +1,6 @@
package strategy
// SuperTrendYinYang
// https://www.tradingview.com/script/9Mm3qKME-Machine-Learning-SuperTrend-Strategy-TP-SL-YinYangAlgorithms/
type SuperTrendTpSlYinYang struct {
}

24
pkg/trade/close_strategy.go

@ -0,0 +1,24 @@
package trade
// 平仓策略参数
type CloseStrategyParam struct {
StopLossPct float64 // 固定止损 static stoploss
TakeProfitPct float64 // 固定止盈 static take profit
TrailMinProfit float64 // 启动移动止损的最小盈利阈值(例如达到 1% 后才开始追踪) minimum profit (fraction) before trailing activates (e.g. 0.01 = 1%);
TrailingPct float64 // 移动止损百分比(例如 0.02 表示从最高价回撤 2% 时触发追踪止损) trailing stop percent (e.g. 0.02 = 2%);
ProfitRetracePct float64 // close when profit retraces more than this fraction of peak profit;基于最高利润回撤触发平仓(例如从最高利润回撤超过 30% 则平仓)。
}
type CloseStrategy struct {
}
func NewCloseStrategy(param CloseStrategyParam) {
}
// OnPriceUpdate 当价格更新判断是否关闭仓位
func (s *CloseStrategy) OnPriceUpdate(price float64, position *string) (closePos bool) {
return
}

4
pkg/trade/risk_strategy.go

@ -17,7 +17,9 @@ func NewRiskStrategy() *RiskStrategy {
} }
// SideAssess 收到信号时进行评估, 返回过滤后的交易信号 // SideAssess 收到信号时进行评估, 返回过滤后的交易信号
// todo 对交易方向进行信心分数评估, 后续开仓仓位 // 对交易方向进行信心分数评估, 后续开仓仓位
// 1.当前持有反方向单时, 不进行开仓
// 2.当前持有同方向单时, 根据信心分数评估是否加仓
func (s *RiskStrategy) SideAssess(signalSide types.Side) (side types.Side) { func (s *RiskStrategy) SideAssess(signalSide types.Side) (side types.Side) {
return signalSide return signalSide

28
pkg/types/kline.go

@ -60,6 +60,34 @@ func (k *Kline) ToPBKline() (kline *pb.Kline) {
return return
} }
func (k *Kline) OpenF64() float64 {
return decimals.MustToFloat64(k.Open)
}
func (k *Kline) CloseF64() float64 {
return decimals.MustToFloat64(k.Close)
}
func (k *Kline) HighF64() float64 {
return decimals.MustToFloat64(k.High)
}
func (k *Kline) LowF64() float64 {
return decimals.MustToFloat64(k.Low)
}
func (k *Kline) VolF64() float64 {
return decimals.MustToFloat64(k.Vol)
}
func (k *Kline) VolQtyF64() float64 {
return decimals.MustToFloat64(k.VolQuote)
}
func (k *Kline) HL2() float64 {
return (k.HighF64() + k.LowF64()) / 2
}
// ChannelKline k线订阅消息 // ChannelKline k线订阅消息
type ChannelKline struct { type ChannelKline struct {
ExgInstId string `json:"instId"` // 交易所交易产品id,如 BTC_USDT_SWAP ExgInstId string `json:"instId"` // 交易所交易产品id,如 BTC_USDT_SWAP

14
pkg/types/signal.go

@ -3,21 +3,21 @@ package types
type Side int32 type Side int32
const ( const (
SideBuy Side = 1 // BUY SideLong Side = 1 // LONG
SideSell Side = 2 // SELL SideShort Side = 2 // SHORT
) )
func (side Side) IsValid() bool { func (side Side) IsValid() bool {
return side == SideBuy || side == SideSell return side == SideLong || side == SideShort
} }
func (side Side) String() string { func (side Side) String() string {
switch side { switch side {
default: default:
return "" return ""
case SideBuy: case SideLong:
return "BUY" return "LONG"
case SideSell: case SideShort:
return "SELL" return "SHORT"
} }
} }

9
pkg/utils/collect/collect.go

@ -41,6 +41,15 @@ func Filter[T any](slice []T, predicate func(int, T) bool) []T {
return res return res
} }
func Find[T any](slice []T, predicate func(T) bool) (r T, ok bool) {
for _, item := range slice {
if predicate(item) {
return item, true
}
}
return
}
// SortAsc 切片升序排序 // SortAsc 切片升序排序
func SortAsc[T any, C cmp.Ordered](slice []T, compare func(T) C) { func SortAsc[T any, C cmp.Ordered](slice []T, compare func(T) C) {
if len(slice) < 2 { if len(slice) < 2 {

Loading…
Cancel
Save