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

311 lines
9.4 KiB
Go
Raw 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"
"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
}
},
}
}