package llmjob import ( "fmt" "github.com/banbox/banbot/config" "github.com/banbox/banbot/core" "github.com/banbox/banbot/strat" ta "github.com/banbox/banta" "math" "sort" "sync" ) /* 需要多品种综合市场信息,品种数不可少于30 此策略有综合市场信息并限制开单数量,回测和实盘开单预期不一致,但开单数量基本符合 openMaxs中的前两个减少会降低收益 待改进: zone1信号在22年有几次略大回撤 */ // ZoneTradeState 用于存储每个交易区间的状态信息 type ZoneTradeState struct { LastTimes [3]int64 // 3个买入信号的最后交易时间 LastSellTime int64 // 最后卖出时间 atrBase float64 score float64 upFlag float64 volRate float64 } func J3782(pol *config.RunPolicyConfig) *strat.TradeStrat { // 定义可调超参数 maPeriod := pol.DefInt("maPeriod", 100, core.PNorm(20, 200)) baseRetra := pol.Def("baseRetra", 0.08, core.PNorm(0.05, 0.2)) retraRate := pol.Def("retraRate", 1.8, core.PNorm(1.5, 2.5)) profitRate := pol.Def("profitRate", 0.03, core.PNorm(0.015, 0.06)) tradeDelay := pol.Def("tradeDelay", 40, core.PNorm(10, 100)) slRate := pol.Def("slRate", 63, core.PNorm(5, 35)) openMaxs := [3]int{2, 8, 13} // 同一个bar最大同时允许开首单的数量 holdMaxs := [3]int{25, 35, 50} // 最大同时持仓数 buyWeis := [3]float64{1, 1.5, 3} // 不同信号开仓倍率 maxJobTrades := [3]int{1, 3, 4} // 单个品种最大开仓数量 // 计算各回撤比例 retracement2 := baseRetra retracement3 := baseRetra * retraRate retracement4 := baseRetra * retraRate * 1.33 lenAtr := 20 baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) rates := make([]float64, 0, 10) allOpens := 0 openNums := [3]int{0, 0, 0} // 同一个bar开单数量 holdNums := [3]int{0, 0, 0} // 总计持仓数量 openUnits := 0.0 var pairLock sync.Mutex addThres, firstRate := 1.0, 1.0 avgAtrBase := 2.5 maxAllowUnit := 3.0 // 这里默认为品种数 rangeVolRate := 1.0 // 全局成交量倍率 checkMaxPair := func(level int, openNum int) (int, float64) { if openNum >= maxJobTrades[level] { return -1, 0 } costRate := buyWeis[level] pairLock.Lock() if openUnits < maxAllowUnit && openNums[level] < openMaxs[level] && holdNums[level] < holdMaxs[level] { openNums[level] += 1 holdNums[level] += 1 allOpens += 1 openUnits += costRate } else { level = -1 } pairLock.Unlock() return level, costRate } return &strat.TradeStrat{ WarmupNum: maPeriod, BatchInfo: true, BatchInOut: true, RunTimeFrames: []string{"15m"}, OnStartUp: func(s *strat.StratJob) { // 初始化状态信息 s.More = &ZoneTradeState{ LastTimes: [3]int64{}, } }, OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { return []*strat.PairSub{ {"_cur_", "4h", baseAtrLen * 2}, } }, OnBar: func(s *strat.StratJob) { e := s.Env state, _ := s.More.(*ZoneTradeState) // 计算MA100 ma100 := ta.SMA(e.Close, maPeriod) ma100Val := ma100.Get(0) // 计算交易价格水平 curRate := max(addThres, 1+state.score/10) itemRate := max(0.7, min(math.Sqrt(state.atrBase/avgAtrBase), 1.4)) buyPrice2 := ma100Val * (1 - retracement2*curRate*itemRate*firstRate) buyPrice3 := ma100Val * (1 - retracement3*curRate*itemRate) buyPrice4 := ma100Val * (1 - retracement4*curRate*itemRate) sellPrice := ma100Val * (1 + profitRate) gapRange := (buyPrice2 - buyPrice4) / 2 maVol := ta.EMA(e.Volume, lenAtr) volBase := ta.Lowest(ta.Highest(maVol, lenAtr), baseAtrLen).Get(0) state.volRate = maVol.Get(0) / volBase // 获取当前价格 lowPrice := e.Low.Get(0) highPrice := e.High.Get(0) // 检查交易间隔条件 currentBarTime := e.TimeStop lastEnt := max(state.LastTimes[0], state.LastTimes[1], state.LastTimes[2]) canTrade := e.BarCount(lastEnt) >= 10 canTrade2 := canTrade && e.BarCount(state.LastTimes[0]) >= tradeDelay canTrade3 := canTrade && e.BarCount(state.LastTimes[1]) >= tradeDelay canTrade4 := canTrade && e.BarCount(state.LastTimes[2]) >= tradeDelay canSell := e.BarCount(state.LastSellTime) >= tradeDelay // 获取当前总开仓数量,与原始策略的strategy.opentrades保持一致 openTrades := len(s.LongOrders) + len(s.ShortOrders) buyIdx, costRate := -1, 0.0 if lowPrice <= buyPrice4 && canTrade4 { buyIdx, costRate = checkMaxPair(2, openTrades) } else if lowPrice <= buyPrice3 && canTrade3 { buyIdx, costRate = checkMaxPair(1, openTrades) } else if lowPrice <= buyPrice2 && canTrade2 { buyIdx, costRate = checkMaxPair(0, openTrades) } if buyIdx >= 0 && costRate > 0 { callNum := allOpens //if buyIdx == 0 { // // 对信号1,累计开单较多时,下移入场价 // pairLock.Lock() // callNum = max(callNum, int(math.Round(float64(holdNums[0])*0.5))-3) // pairLock.Unlock() //} // 同一个bar开单多次,从第三次开始,使用ln(num)下移入场价 currRate := 1 + max(0, math.Log10(float64(callNum-2))) // 用倒抛物线映射成交量倍率,作为入场价距离调整系数 volRate := 1 + max(0, 0.4-math.Pow(1.4*(rangeVolRate-1.5), 2)) rangeRate := math.Pow(volRate*currRate, 0.3) - 1 limitPrice := float64(0) if (rangeVolRate > 1.2 || callNum > 2) && rangeRate > 0.01 { limitPrice = e.Close.Get(0) - rangeRate*gapRange } tag := fmt.Sprintf("zone%d", buyIdx+1) s.OpenOrder(&strat.EnterReq{ Tag: tag, CostRate: costRate, Limit: limitPrice, Log: true, StopBars: 3, }) state.LastTimes[buyIdx] = currentBarTime } // 检查卖出条件 if s.OrderNum > 0 { curPrice := e.Close.Get(0) slRange := slRate * itemRate * curPrice / 100 avgPrice := s.PositionAvgPrice(core.OdDirtLong, "") if curPrice+slRange <= avgPrice { s.CloseOrders(&strat.ExitReq{ Tag: "StopLoss", Log: true, }) state.LastSellTime = currentBarTime } else { exitPrice := avgPrice*(1+profitRate)*0.4 + sellPrice*0.6 if openTrades > 0 && highPrice >= exitPrice && canSell { s.CloseOrders(&strat.ExitReq{ Tag: "SellAll", Log: true, }) state.LastSellTime = currentBarTime } } } }, OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { // 多指标综合计算大周期行情下跌分数 score := float64(0) if ta.RSI(e.Close, 14).Get(0) < 30 { score += 1 } roc := ta.ROC(e.Close, 20).Get(0) if roc < -20 { score += 2 } else if roc < -10 { score += 1 } plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14) adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) if minusDi.Get(0) > plusDi.Get(0) { if adx > 40 { score += 2 } else if adx > 25 { score += 1 } } atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) rates = append(rates, atrBase/(e.Close.Get(0)*0.01)) //openRate := 0.006 * closePrice / atrBase //if openRate < 1 { // openRate = math.Sqrt(openRate) //} //openRate = max(0.7, min(1.5, openRate)) ma7 := ta.SMA(e.Close, 7).Get(0) ma20 := ta.SMA(e.Close, 20) ma20V := ma20.Get(0) ma99 := ta.SMA(e.Close, 99).Get(0) if ma99 > ma20V { score += 1 } state, _ := s.More.(*ZoneTradeState) state.score = score state.upFlag = 0 state.atrBase = atrBase / (e.Close.Get(0) * 0.01) if ma20V > ma99 && ma20V > ma20.Get(2) { state.upFlag = 1 } else if ma7 < min(ma20V, ma99) { state.upFlag = -1 } }, OnBatchJobs: func(jobs []*strat.StratJob) { pairLock.Lock() openUnits = 0 allOpens = 0 openNums = [3]int{0, 0, 0} holdNums = [3]int{0, 0, 0} volRates := make([]float64, 0, len(jobs)) for _, j := range jobs { for _, od := range j.LongOrders { if od.EnterTag == "zone1" { holdNums[0] += 1 } else if od.EnterTag == "zone2" { holdNums[1] += 1 } else if od.EnterTag == "zone3" { holdNums[2] += 1 } } m, _ := j.More.(*ZoneTradeState) volRates = append(volRates, m.volRate) } pairLock.Unlock() sort.Float64s(volRates) rangeVolRate = max(1, math.Pow(volRates[len(volRates)/2]/2, 0.3)) }, OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) { if len(jobs) < 3 { return } // 多品种综合计算整体市场下行激烈程度 downNum, downHeavyNum := float64(0), float64(0) atrRates := make([]float64, 0, len(jobs)) for _, j := range jobs { state, _ := j.Job.More.(*ZoneTradeState) if state.score >= 5 { downHeavyNum += 1 } if state.score >= 3 { downNum += 1 } atrRates = append(atrRates, state.atrBase) } jobNum := float64(len(jobs)) downRate := downNum / jobNum downHeavyRate := downHeavyNum / jobNum sort.Float64s(atrRates) avgAtrBase = atrRates[len(jobs)/2] if downHeavyRate >= 0.8 { // 极端下跌 addThres, firstRate = 1.6, 1.2 maxAllowUnit = jobNum * 0.5 } else if downRate >= 0.85 && downHeavyRate >= 0.6 { // 严重下跌 addThres, firstRate = 1.45, 1.15 maxAllowUnit = jobNum * 0.55 } else if downRate >= 0.7 && downRate <= 0.85 { // 市场中度下跌 addThres, firstRate = 1.3, 1.1 maxAllowUnit = jobNum * 0.6 } else if downRate >= 0.5 && downRate <= 0.7 { // 轻度下跌 addThres, firstRate = 1.15, 1.05 maxAllowUnit = jobNum * 0.7 } else { // 轻度或未下跌 maxAllowUnit = jobNum addThres, firstRate = 1, 1 } }, } }