Files
banstagy/llmjob/3762.go
2026-08-24 20:53:21 +08:00

271 lines
8.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package llmjob
import (
"fmt"
"math"
"sort"
"sync"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"github.com/banbox/banexg/utils"
ta "github.com/banbox/banta"
"go.uber.org/zap"
)
/*
超跌反弹策略。30个品种为宜;已增加暴跌抄底机制。
开单越多,后续抄底价指数下移
做空部分没有利润,不稳定,小亏,禁用
*/
type State3762 struct {
NoLongUntil int64
BWidth float64
MidPrice float64
SlRange float64
volRate float64
}
func J3762(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义策略参数
p := pol.DefInt("p", 20, core.PNorm(5, 50))
d := pol.Def("d", 30.0, core.PNorm(5, 50))
mult := pol.Def("mult", 1.5, core.PNorm(1.0, 3.0))
slRate := pol.Def("slRate", 25, core.PNorm(0, 250)) //default: 25
// 某品种入场时,下移其他品种的倍率(相对BWidth)
// 0.3开单太多亏损,0.5开单不足亏损
downOtherRate := 0.33
dayMS := int64(utils.TFToSecs("1d") * 1000)
longFillNum := float64(0)
fillLock := sync.Mutex{}
var allJobs []*strat.StratJob
// 基于成交量计算的区间倍率,成交量太大时,适当调低做多和止损价格
var rangeVolRate = float64(1)
allowPairs := 1 // 允许挂单的订单数,默认为品种数,随开单减少,防止交易所故障导致下单过多
openPairs := 0 // 实际有订单的品种数(挂单+持仓)
lenAtr := 20
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
calcBuyPrice := func(m *State3762) float64 {
return m.MidPrice - m.BWidth*(1+math.Pow(longFillNum, 1.5)*downOtherRate)*rangeVolRate
}
return &strat.TradeStrat{
WarmupNum: baseAtrLen * 2, // 预热K线数量,确保有足够数据计算指标
RunTimeFrames: []string{"15m"},
BatchInOut: true,
OnStartUp: func(s *strat.StratJob) {
s.More = &State3762{}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
m, _ := s.More.(*State3762)
// 计算基础布林带指标
sma := ta.SMA(e.Close, p)
ma7 := ta.SMA(e.Close, 7).Get(0)
ma30 := ta.SMA(e.Close, 30).Get(0)
multCur := mult
if ma7 < ma30 {
// 熊市下移下限
multCur *= 1.35
}
stddev := ta.StdDev(e.Close, p).Get(0) * multCur
upperBB := sma.Get(0) + stddev
lowerBB := sma.Get(0) - stddev
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
maVol := ta.EMA(e.Volume, lenAtr)
volBase := ta.Lowest(ta.Highest(maVol, lenAtr), baseAtrLen).Get(0)
m.volRate = maVol.Get(0) / volBase
volH3Rate := ta.Highest(maVol, 3).Get(0) / volBase
// 计算百分比偏离轨道
atrRt := d
if ma7 < ma30 {
// 熊市下移下限
atrRt *= 1.35
}
upex2 := sma.Get(0) + atrBase*atrRt
dnex2 := sma.Get(0) - atrBase*atrRt
// 计算最终轨道
upexPrice := max(upperBB, (upex2+upperBB)/2)
dnexPrice := min(lowerBB, (dnex2+lowerBB)/2)
m.BWidth = (upexPrice - dnexPrice) / 2
m.MidPrice = (upexPrice + dnexPrice) / 2
//lowPrice, highPrice := dnexPrice, upexPrice
lowPrice := calcBuyPrice(m)
// 获取当前价格
closePrice := e.Close.Get(0)
// 平仓条件:阳线
if len(s.LongOrders) > 0 && s.LongOrders[0].Status >= ormo.InOutStatusPartEnter {
doExit := closePrice > e.Open.Get(0)
if volH3Rate > 5 {
// 放巨量入场的订单,等待阳线后一个K线出场
doExit = e.Close.Get(1) > e.Open.Get(1)
}
if doExit {
s.CloseOrders(&strat.ExitReq{
Tag: "ExitLong",
Dirt: core.OdDirtLong,
Log: true,
})
}
}
if math.IsNaN(lowPrice) || lowPrice <= 0 {
return
}
slRange := min(atrBase*slRate, closePrice*0.25) * min(3, rangeVolRate)
m.SlRange = slRange
// 执行交易逻辑
//if !s.IsWarmUp && e.TimeStop >= 1760130900000 {
// line := fmt.Sprintf("%s upp: %.6f, low: %.6f upex2: %.6f, dnex2: %.6f dnex: %.6f slRange: %.6f",
// e.Symbol, upperBB, lowerBB, upex2, dnex2, lowPrice, slRange)
// s.Strat.WriteOutput(line, true)
//}
if len(s.LongOrders) == 0 {
// 使用限价单在下轨价格做多,不依赖当前价格是否突破轨道
if e.TimeStop >= m.NoLongUntil && openPairs < allowPairs {
fillLock.Lock()
openPairs += 1
fillLock.Unlock()
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Limit: lowPrice, // 以下轨价格作为限价单
StopLossVal: slRange,
Log: true,
})
}
} else {
for _, od := range s.LongOrders {
if od.Status == ormo.InOutStatusInit {
err := od.SetEnterLimit(lowPrice)
if err != nil {
log.Error("SetEnterLimit fail", zap.Error(err))
} else {
err = od.SetStopLoss(&ormo.ExitTrigger{
Price: lowPrice - slRange,
})
if err != nil {
log.Error("SetStopLoss fail", zap.Error(err))
}
}
} else if od.Status == ormo.InOutStatusFullEnter {
barNum := e.BarCount(od.Enter.UpdateAt)
if barNum > 2 && od.ProfitRate < 0 {
// 持仓2个k线,出现亏损,平仓
s.CloseOrders(&strat.ExitReq{
Tag: "ExitBad",
Dirt: core.OdDirtLong,
Log: true,
})
m.NoLongUntil = e.TimeStop + dayMS
}
}
}
}
},
OnBatchJobs: func(jobs []*strat.StratJob) {
fillLock.Lock()
longFillNum = 0
openPairs = 0
volRates := make([]float64, 0, len(jobs))
for _, s := range jobs {
if len(s.LongOrders) > 0 {
openPairs += 1
for _, od := range s.LongOrders {
if od.Status >= ormo.InOutStatusPartEnter {
longFillNum += 1
}
}
}
m, _ := s.More.(*State3762)
volRates = append(volRates, m.volRate)
}
fillLock.Unlock()
allJobs = jobs
sort.Float64s(volRates)
rangeVolRate = max(1, math.Pow(volRates[len(volRates)/2]/2, 0.3))
allowPairs = int(math.Round(math.Pow(0.8, longFillNum) * float64(len(allJobs))))
//jobs[0].Strat.WriteOutput(fmt.Sprintf("%.3f", rangeVolRate), true)
},
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
if chgType == core.OdChgExitFill {
if od.Enter.Amount > 0 {
m, _ := s.More.(*State3762)
m.NoLongUntil = od.ExitAt + dayMS
fillLock.Lock()
longFillNum -= 1
openPairs -= 1
fillLock.Unlock()
}
} else if chgType == core.OdChgEnterFill {
fillLock.Lock()
longFillNum += 1
openPairs = 0 // 这里应当重新计算,避免并发和回调导致错误
for _, job := range allJobs {
if len(job.LongOrders) > 0 {
openPairs += 1
}
}
fillLock.Unlock()
// 降低允许开仓的品种数,避免交易所故障导致下单太多
allowPairs = int(math.Round(math.Pow(0.85, longFillNum) * float64(len(allJobs))))
needClose := openPairs - allowPairs // 需要关闭的订单数
if needClose > 0 {
// 排序确保多次回测一致
sort.Slice(allJobs, func(i, j int) bool {
return allJobs[i].Symbol.Symbol < allJobs[j].Symbol.Symbol
})
}
odMgr := biz.GetOdMgr(s.Account)
// 移动其他品种的入场价格,避免同时开仓太多
for _, job := range allJobs {
if job == s || len(job.LongOrders) == 0 {
continue
}
m, _ := job.More.(*State3762)
lowPrice := calcBuyPrice(m)
for _, odr := range job.LongOrders {
if odr.Status <= ormo.InOutStatusPartEnter {
if lowPrice <= 0 || needClose > 0 {
needClose -= 1
job.CloseOrders(&strat.ExitReq{
Tag: "cancel",
OrderID: odr.ID,
Force: true,
})
odMgr.ProcessOrders(job)
} else {
err := odr.SetEnterLimit(lowPrice)
if err == nil {
odr.EnterTag = fmt.Sprintf("long%d", int(longFillNum))
odr.SetStopLoss(&ormo.ExitTrigger{
Price: max(lowPrice-m.SlRange, lowPrice*0.1),
})
}
}
} else {
odr.SetStopLoss(&ormo.ExitTrigger{
Price: max(lowPrice-m.SlRange, odr.Enter.Price*0.1),
})
}
}
}
}
},
}
}