This commit is contained in:
2026-08-24 20:53:21 +08:00
commit 6e78bba16a
287 changed files with 32443 additions and 0 deletions

373
arb/stat_mean.go Normal file
View File

@@ -0,0 +1,373 @@
package arb
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"github.com/banbox/banta"
"go.uber.org/zap"
"math"
"sort"
"strconv"
"sync"
)
/*
统计套利:对两个交易对的价格比率下注
价格比率较稳定的:
快速穿越大趋势跟随开单
超出上下轨,回弹交叉开单
价格比率明显单边趋势:
在标准差30%以内交叉,跟随大趋势方向开单
强劲单边跟随:
平时价格比率小幅上下波动,但偶尔有强劲单边价格剧烈运动:GLM/USDT:USDT, XEM/USDT:USDT 20241001
瞬时极端波动:
某段时间,价格比率快速上下极端波动,GLM/USDT:USDT, XEM/USDT:USDT 20241025
*/
func MeanRegressStat(p *config.RunPolicyConfig) *strat.TradeStrat {
var sta *StatTwo
var jobs = make(map[string]*strat.StratJob)
return &strat.TradeStrat{
WarmupNum: 1000,
BatchInOut: true,
OnStartUp: func(s *strat.StratJob) {
jobs[s.Symbol.Symbol] = s
},
OnBar: func(s *strat.StratJob) {
if sta == nil {
sta = newStatTwo(jobs, nil)
}
e := s.Env
if e.TimeStart > sta.barMS {
sta.barMS = e.TimeStart
}
sta.OnPrice(s.Symbol.Symbol, e.Close.Get(0))
},
OnBatchJobs: func(jobs []*strat.StratJob) {
sta.onStampDone()
},
}
}
const (
slopeDist = 150
maSlopeDist = 500
)
type StatTwo struct {
barMS int64 // 收到最新bar的时间戳
prices map[string]float64 // 当前时间戳下,所有品种的价格
jobs map[string]*strat.StratJob // 所有的任务
cacheSize int // Tuple中保留历史数据的最小长度
lock sync.Mutex
Tuples map[string]map[string]*Tuple
}
type Tuple struct {
A *strat.StratJob
B *strat.StratJob
env *banta.BarEnv
raw *banta.Series
maFast *banta.Series
maSlow *banta.Series
maBig *banta.Series
rawSbBig *banta.Series // sma(raw-maBig, 500)
faXSl int // fast slow交叉:1快线上穿慢线;-1快线下穿慢线
rawXBig int // raw big交叉
maSlope float64 // ma斜率(和500个之前)
slope float64 // 价格比率斜率(和150个之前)
sort float64 // 排序分数,降序
stdDev float64 // 标准差
allowType int // 允许开单类型: 1突破布林带回弹;2快速穿越大趋势线;3中心交叉跟随趋势
allowDirt int // 允许开单方向: 1做多A做空B; -1做空A做多B
openType int // 开单类型:从allowType赋值
openRate float64 // 开单的价格比率,0表示未开单
openAt int64 // 开单的时间戳
}
func newStatTwo(jobs map[string]*strat.StratJob, tuples [][]string) *StatTwo {
tupleMap := make(map[string]map[string]*Tuple)
res := &StatTwo{
cacheSize: 2000,
jobs: jobs,
Tuples: tupleMap,
}
if len(tuples) == 0 {
codes := make([]string, 0, len(jobs))
for key := range jobs {
codes = append(codes, key)
}
for i := 0; i < len(codes); i++ {
for j := i + 1; j < len(codes); j++ {
tuples = append(tuples, []string{codes[i], codes[j]})
}
}
}
if len(tuples) > 7000 {
log.Warn("too many tuples may cause OOM!", zap.Int("num", len(tuples)))
}
for _, va := range tuples {
a, b := va[0], va[1]
if a > b {
a, b = b, a
}
subMap, ok := tupleMap[a]
if !ok {
subMap = make(map[string]*Tuple)
tupleMap[a] = subMap
}
tup, ok := subMap[b]
if !ok {
baseA, _, _, _ := core.SplitSymbol(a)
baseB, _, _, _ := core.SplitSymbol(b)
symbol := fmt.Sprintf("%s/%s", baseA, baseB)
env, _ := banta.NewBarEnv(core.ExgName, core.Market, symbol, "1m")
env.MaxCache = res.cacheSize
raw := env.NewSeries(nil)
A, _ := jobs[a]
B, _ := jobs[b]
tup = &Tuple{
A: A,
B: B,
raw: raw,
env: env,
}
subMap[b] = tup
}
}
return res
}
func (s *StatTwo) OnPrice(symbol string, price float64) {
s.lock.Lock()
s.prices[symbol] = price
s.lock.Unlock()
}
func (s *StatTwo) popTuples() []*Tuple {
s.lock.Lock()
tasks := make(map[string]map[string]bool)
for codeA := range s.prices {
subMap := make(map[string]bool)
for codeB := range s.prices {
if codeA >= codeB {
continue
}
subMap[codeB] = true
}
tasks[codeA] = subMap
}
var res = make([]*Tuple, 0, len(s.prices)*2)
for codeA, subMap := range tasks {
for codeB := range subMap {
ratio := s.prices[codeA] / s.prices[codeB]
t := s.tuple(codeA, codeB, ratio)
if t == nil {
continue
}
res = append(res, t)
}
}
s.prices = make(map[string]float64)
s.lock.Unlock()
return res
}
func (s *StatTwo) onStampDone() {
tuples := s.popTuples()
// 筛选两种开单类型
size := len(tuples) / 3
list1 := make([]*Tuple, 0, size)
list2 := make([]*Tuple, 0, size)
list3 := make([]*Tuple, 0, size)
for _, t := range tuples {
t.calcInOuts()
if t.allowType == 1 {
list1 = append(list1, t)
} else if t.allowType == 2 {
list2 = append(list2, t)
} else if t.allowType == 3 {
list3 = append(list3, t)
}
}
var list = list1
if len(list) == 0 {
if len(list2) == 0 {
if len(list3) == 0 {
return
}
list = list3
} else {
list = list2
}
}
// 按分数降序,取前三个中没有开单的
var it *Tuple
sort.Slice(list, func(i, j int) bool {
return list[i].sort > list[j].sort
})
gap := len(list) / 2
list = append(list[gap:], list[:gap]...)
for i, v := range list {
if v.openType == 0 && v.A.OrderNum == 0 && v.B.OrderNum == 0 {
it = v
break
}
if i == 2 {
break
}
}
if it == nil {
return
}
// 下单
shortA := it.allowDirt < 0
errA := it.A.OpenOrder(&strat.EnterReq{
Tag: strconv.Itoa(it.allowType),
Short: shortA,
})
if errA == nil {
errB := it.B.OpenOrder(&strat.EnterReq{
Tag: strconv.Itoa(it.allowType),
Short: !shortA,
})
if errB != nil {
it.A.Entrys = nil
} else {
it.openType = it.allowType
it.openRate = it.raw.Get(0)
it.openAt = it.env.TimeStop
}
}
//dateStr := btime.ToDateStr(s.barMS, core.DefaultDateFmt)
//fmt.Printf("%s %s - %s type=%v sort=%.3f slope=%.3f bigSlope=%.3f\n", dateStr, it.A.Symbol.Symbol,
// it.B.Symbol.Symbol, it.allowType, it.sort, it.slope, it.maSlope)
}
func (s *StatTwo) tuple(a, b string, ratio float64) *Tuple {
if a > b {
a, b = b, a
ratio = 1 / ratio
}
subMap, ok := s.Tuples[a]
if !ok {
return nil
}
res, ok := subMap[b]
if ok {
res.update(s.barMS, ratio)
return res
}
return nil
}
func (t *Tuple) update(stamp int64, ratio float64) {
t.env.TimeStart = stamp
t.env.TimeStop = stamp + t.env.TFMSecs
t.env.BarNum += 1
t.raw.Append(ratio)
t.maFast = banta.EMA(t.raw, 24)
t.maSlow = banta.EMA(t.raw, 66)
t.maBig = banta.EMA(t.raw, 1333)
t.rawSbBig = banta.SMA(t.raw.Sub(t.maBig), 500)
t.faXSl = t.maFast.Cross(t.maSlow)
t.rawXBig = t.raw.Cross(t.maBig)
sdev := banta.StdDev(t.raw, 1333)
t.stdDev = banta.SMA(sdev, 800).Get(0) * 1.7
if t.raw.Len() > slopeDist {
// 计算近期斜率,优先用基于历史150偏移计算
// 当基于均线历史150偏移的绝对变动区间更小时,基于均线计算
old := t.raw.Get(slopeDist)
oldMa := t.maBig.Get(slopeDist)
slope := ratio/old - 1
slopeMa := ratio/oldMa - 1
if math.Abs(slopeMa) < math.Abs(slope) {
slope = slopeMa
}
t.slope = slope
}
if t.maBig.Len() > maSlopeDist {
t.allowType = 0
t.sort = 0
t.maSlope = t.maBig.Get(0)/t.maBig.Get(maSlopeDist) - 1
}
}
func (t *Tuple) calcInOuts() {
if t.maBig.Len() < maSlopeDist {
return
}
//fastVal, fastVal30 := t.maFast.Get(0), t.maFast.Get(30)
//if t.rawXBig == -1 || t.rawXBig == 1 {
// // 快速穿越大趋势,开单
// fastSlope := math.Abs(fastVal/fastVal30 - 1)
// distRate := t.rawSbBig.Get(0) / stdDev
// if fastSlope > 0.01 && distRate*float64(t.rawXBig) < 0 && math.Abs(distRate) > 0.7 {
// // 要求穿过大趋势线时,具备较高的斜率,且过去500个平均与大周期的差超过70%标准差
// t.allowType = 2
// t.allowDirt = t.rawXBig
// t.sort = math.Abs(distRate)
// }
//}
maSlopeAbs := math.Abs(t.maSlope)
//diff := math.Abs(t.slope - t.maSlope)
//if maSlopeAbs < min(math.Abs(t.slope), 0.02) && diff > 0.015 {
// // 突破布林带上下轨,回弹时开单
// bigVal := t.maBig.Get(0)
// slowPrev := t.maSlow.Get(60)
// // 按maSlope进行惩罚,极端0.02时削减64%
// sortV := diff * math.Pow(1-maSlopeAbs*20, 2)
// if t.faXSl == -1 && max(fastVal, fastVal30) > bigVal+stdDev {
// // 均线上方,突破上界,快线下穿慢线
// if fastVal > slowPrev {
// t.allowType = 1
// t.allowDirt = t.faXSl
// t.sort = sortV
// }
// } else if t.faXSl == 1 && min(fastVal, fastVal30) < bigVal-stdDev {
// // 均线下方,突破下界,快线上穿慢线
// if fastVal < slowPrev {
// t.allowType = 1
// t.allowDirt = t.faXSl
// t.sort = sortV
// }
// }
//}
nearToCenter := math.Abs(t.raw.Get(0)-t.maBig.Get(0)) < t.stdDev*0.3
if maSlopeAbs > 0.02 && nearToCenter && (t.faXSl == 1 || t.faXSl == -1) && t.maSlope*float64(t.faXSl) > 0 {
// 明显大趋势,在中心附近交叉,按大趋势方向开单
t.allowType = 3
t.allowDirt = t.faXSl
t.sort = maSlopeAbs
}
closeOd := false
if t.openType == 2 {
// 根据穿越大趋势开单:回穿大趋势平仓、快线慢线交叉平仓
if t.faXSl*t.allowDirt == -1 || t.rawXBig*t.allowDirt == -1 {
closeOd = true
}
} else if t.openType == 1 && t.faXSl*t.allowDirt == -1 {
// 根据突破上下轨回弹开单,(盈利达到标准差30% || 持有时间超过700个bar):快线慢线交叉平仓
pft30 := (t.raw.Get(0)-t.openRate)*float64(t.allowDirt) > t.stdDev*0.3
//costBarNum := int((t.env.TimeStop - t.openAt) / t.env.TFMSecs)
if pft30 { //|| costBarNum > 1000 {
closeOd = true
}
} else if t.openType == 3 && t.faXSl*t.allowDirt == -1 {
// 中心附近交叉,跟随趋势的,再次交叉时平仓
closeOd = true
}
if closeOd {
t.A.CloseOrders(&strat.ExitReq{Tag: "exit"})
t.B.CloseOrders(&strat.ExitReq{Tag: "exit"})
t.openType = 0
t.openRate = 0
t.openAt = 0
}
}