init
This commit is contained in:
373
arb/stat_mean.go
Normal file
373
arb/stat_mean.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user