272 lines
7.6 KiB
Go
272 lines
7.6 KiB
Go
package arb
|
|
|
|
import (
|
|
"github.com/banbox/banbot/btime"
|
|
"github.com/banbox/banbot/config"
|
|
"github.com/banbox/banbot/core"
|
|
"github.com/banbox/banbot/strat"
|
|
"github.com/banbox/banexg/log"
|
|
"github.com/banbox/banexg/utils"
|
|
ta "github.com/banbox/banta"
|
|
"go.uber.org/zap"
|
|
"maps"
|
|
"math"
|
|
"math/rand"
|
|
"path/filepath"
|
|
)
|
|
|
|
func init() {
|
|
strat.AddStratGroup("arb", map[string]strat.FuncMakeStrat{
|
|
"gp": ArbGroupJob,
|
|
})
|
|
}
|
|
|
|
/*
|
|
strengths widRatios
|
|
rate: 0 -0.002491 0.859934
|
|
rate: 14234 -0.000915 0.946673
|
|
rate: 28468 -0.000089 0.997275
|
|
rate: 42702 0.000818 1.048731
|
|
rate: 56936 0.002571 1.166192
|
|
rate: 71170 0.007989 1.619902
|
|
*/
|
|
|
|
func ArbGroupJob(pol *config.RunPolicyConfig) *strat.TradeStrat {
|
|
lenFast := 6
|
|
lenSlow := 20
|
|
lenMid := 96
|
|
lenStd := 96
|
|
bollWid := 0.0043
|
|
lastSplitMS := int64(0)
|
|
tfMSecs := int64(60000)
|
|
dayMS := int64(utils.TFToSecs("1d") * 1000)
|
|
env, _ := ta.NewBarEnv(core.ExgName, core.Market, "", "1m")
|
|
var rate *ta.Series
|
|
groups := make(map[*strat.StratJob]float64)
|
|
hits := make(map[string]float64)
|
|
warmNum := 120
|
|
var widths []float64
|
|
var maxRate float64
|
|
var position float64
|
|
var posTag string
|
|
var rates []*TimeFlt
|
|
dumpDir := filepath.Join(config.GetDataDir(), "dump")
|
|
return &strat.TradeStrat{
|
|
WarmupNum: warmNum,
|
|
RunTimeFrames: []string{"1m"},
|
|
BatchInOut: true,
|
|
OnBar: func(s *strat.StratJob) {
|
|
},
|
|
OnBatchJobs: func(jobs []*strat.StratJob) {
|
|
var curMS int64
|
|
newNum := 0
|
|
hitDup := maps.Clone(hits)
|
|
for _, j := range jobs {
|
|
curMS = max(curMS, j.Env.TimeStop)
|
|
if _, ok := hitDup[j.Symbol.Symbol]; ok {
|
|
delete(hitDup, j.Symbol.Symbol)
|
|
} else {
|
|
newNum += 1
|
|
}
|
|
}
|
|
alignDayMS := utils.AlignTfMSecs(curMS, dayMS)
|
|
if alignDayMS > lastSplitMS || newNum > 0 || len(hitDup) > 0 {
|
|
// 每天刷新品种分组
|
|
for j := range groups {
|
|
j.CloseOrders(&strat.ExitReq{Tag: "switch", Log: true})
|
|
}
|
|
offsetMS := alignDayMS - dayMS
|
|
dateStr := btime.ToDateStr(offsetMS, "20060102")
|
|
dumpPath := filepath.Join(dumpDir, dateStr+".html")
|
|
if len(rates) > 1000 {
|
|
log.Info("try dump rates", zap.String("date", dateStr), zap.Int("num", len(rates)))
|
|
err := dumpRateChart(rates, "1m", dumpPath, dateStr)
|
|
if err != nil {
|
|
log.Error("dumpRateChart fail", zap.Error(err))
|
|
}
|
|
}
|
|
rates = nil
|
|
rand.Shuffle(len(jobs), func(i, j int) {
|
|
jobs[i], jobs[j] = jobs[j], jobs[i]
|
|
})
|
|
env.Reset()
|
|
rate = env.NewSeries(nil)
|
|
groups = make(map[*strat.StratJob]float64)
|
|
hits = make(map[string]float64)
|
|
for i, j := range jobs {
|
|
if i%2 == 0 {
|
|
groups[j] = -1
|
|
} else {
|
|
groups[j] = 1
|
|
}
|
|
hits[j.Symbol.Symbol] = j.Env.Close.Get(lenMid)
|
|
}
|
|
env.TimeStop = curMS - int64(lenMid)*tfMSecs
|
|
env.TimeStart = env.TimeStop - tfMSecs
|
|
for i := warmNum; i >= 0; i-- {
|
|
var price1, price2 float64
|
|
var num1, num2 float64
|
|
for j, tag := range groups {
|
|
base := hits[j.Symbol.Symbol]
|
|
ratec := j.Env.Close.Get(i) / base
|
|
if tag < 0 {
|
|
price1 += ratec
|
|
num1 += 1
|
|
} else {
|
|
price2 += ratec
|
|
num2 += 1
|
|
}
|
|
}
|
|
price1 /= num1
|
|
price2 /= num2
|
|
env.TimeStart += tfMSecs
|
|
env.TimeStop += tfMSecs
|
|
rate.Append(price1 / price2)
|
|
_ = ta.EMA(rate, lenFast)
|
|
_ = ta.EMA(rate, lenSlow)
|
|
maMid := ta.EMA(rate, lenMid).Get(0)
|
|
dev := ta.StdDev(rate, lenStd)
|
|
stdDev := ta.SMA(dev, lenStd).Get(0) * 1.7
|
|
bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5
|
|
widths = append(widths, bollWidAvg)
|
|
}
|
|
lastSplitMS = alignDayMS
|
|
}
|
|
var price1, price2 float64
|
|
var num1, num2 float64
|
|
for j, tag := range groups {
|
|
base := hits[j.Symbol.Symbol]
|
|
ratec := j.Env.Close.Get(0) / base
|
|
if tag < 0 {
|
|
price1 += ratec
|
|
num1 += 1
|
|
} else {
|
|
price2 += ratec
|
|
num2 += 1
|
|
}
|
|
}
|
|
price1 /= num1
|
|
price2 /= num2
|
|
curRate := price1 / price2
|
|
env.TimeStart += tfMSecs
|
|
env.TimeStop += tfMSecs
|
|
rate.Append(curRate)
|
|
rates = append(rates, &TimeFlt{
|
|
Stamp: curMS,
|
|
Val: curRate,
|
|
})
|
|
maFast := ta.EMA(rate, lenFast)
|
|
maSlow := ta.EMA(rate, lenSlow) //66
|
|
maMidS := ta.EMA(rate, lenMid)
|
|
maMid := maMidS.Get(0)
|
|
dev := ta.StdDev(rate, lenStd)
|
|
stdDev := ta.SMA(dev, lenStd).Get(0) * 1.7
|
|
bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5
|
|
widths = append(widths, bollWidAvg)
|
|
zScore := (curRate - maMid) / bollWidAvg
|
|
strength := maMid/maMidS.Get(warmNum) - 1
|
|
widDelta := bollWidAvg - widths[len(widths)-60]
|
|
widRatio := bollWidAvg / widths[len(widths)-warmNum]
|
|
fastXSlow := maFast.Cross(maSlow)
|
|
fastXMid := maFast.Cross(maMidS)
|
|
|
|
absStrength := math.Abs(strength)
|
|
mkt := ""
|
|
if absStrength > 0.01 || math.Abs(zScore) > 4 {
|
|
// D: 结构性跳变,暂停交易
|
|
mkt = "D"
|
|
} else if widRatio > 1.5 && widDelta > 0 || bollWidAvg >= 0.008 {
|
|
// C: 高波动突破
|
|
mkt = "C"
|
|
} else if absStrength >= 0.003 || widRatio > 1.3 && widDelta > 0 {
|
|
// B: 趋势市场
|
|
mkt = "B"
|
|
} else if absStrength < 0.002 && widRatio < 1.2 && widDelta <= 0 {
|
|
// A: 低波动区间
|
|
mkt = "A"
|
|
} else {
|
|
//if absStrength >= 0.001 && absStrength < 0.002 && widRatio < 1.3
|
|
// E: 低频弱趋势,中性,混合策略
|
|
mkt = "E"
|
|
}
|
|
dirt := -1.0
|
|
if strength > 0 {
|
|
dirt = 1
|
|
}
|
|
if math.Abs(zScore) > maxRate*dirt {
|
|
maxRate = zScore
|
|
}
|
|
allow1, allow2 := false, false
|
|
if mkt == "A" {
|
|
allow1 = true
|
|
} else if mkt == "B" || mkt == "C" {
|
|
// B明显趋势 或 C波动扩大方向明确
|
|
allow2 = true
|
|
} else if mkt == "E" {
|
|
allow1, allow2 = true, true
|
|
}
|
|
openDirt := 0.0
|
|
openTag := ""
|
|
dateStr := btime.ToDateStr(curMS, core.DefaultDateFmt)
|
|
if allow1 {
|
|
expPft := math.Abs(curRate - maMid)
|
|
if math.Abs(maxRate) >= 1.5 && expPft > 0.002 && float64(fastXSlow)*dirt == -1 {
|
|
openDirt = -dirt
|
|
openTag = "rev"
|
|
log.Info("open rev", zap.String("date", dateStr), zap.Float64("z", zScore),
|
|
zap.Float64("width", bollWidAvg), zap.Float64("widDelta", widDelta))
|
|
}
|
|
}
|
|
if allow2 && float64(fastXMid)*dirt == 1 {
|
|
if absStrength >= 0.003 || widRatio > 1.3 && widDelta > 0 {
|
|
openDirt = dirt
|
|
openTag = "follow"
|
|
log.Info("open trend", zap.String("date", dateStr), zap.Float64("z", zScore),
|
|
zap.Float64("width", bollWidAvg), zap.Float64("widDelta", widDelta),
|
|
zap.Float64("strength", strength), zap.Float64("widRatio", widRatio))
|
|
}
|
|
}
|
|
if position != openDirt && openDirt != 0 {
|
|
if position != 0 {
|
|
log.Info("close pos", zap.String("date", dateStr), zap.String("tag", "reverse"), zap.Float64("dirt", position))
|
|
for j := range groups {
|
|
j.CloseOrders(&strat.ExitReq{Tag: "reverse"})
|
|
}
|
|
}
|
|
for j, tag := range groups {
|
|
j.OpenOrder(&strat.EnterReq{
|
|
Tag: openTag,
|
|
Short: tag != openDirt,
|
|
})
|
|
}
|
|
log.Info("open pos", zap.String("date", dateStr), zap.String("tag", openTag), zap.Float64("dirt", openDirt))
|
|
position = openDirt
|
|
posTag = openTag
|
|
} else if position != 0 {
|
|
exitTag := ""
|
|
if posTag == "rev" {
|
|
if position*dirt > 0 {
|
|
exitTag = "tpRev"
|
|
} else if math.Abs(zScore) >= 3 {
|
|
exitTag = "slRev"
|
|
}
|
|
} else if posTag == "follow" {
|
|
if position*dirt < 0 && (math.Abs(zScore) > 0.5 || widDelta < 0) {
|
|
exitTag = "slFol"
|
|
} else if position*dirt > 0 && math.Abs(zScore) >= 1 {
|
|
exitTag = "tpFol"
|
|
}
|
|
}
|
|
if exitTag != "" {
|
|
log.Info("close pos", zap.String("date", dateStr), zap.String("tag", exitTag), zap.Float64("dirt", position))
|
|
position = 0
|
|
posTag = ""
|
|
for j := range groups {
|
|
j.CloseOrders(&strat.ExitReq{Tag: exitTag})
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|