Files
banstagy/arb/stat_mean.go
2026-08-24 20:53:21 +08:00

374 lines
10 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 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
}
}