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), }) } } } } }, } }