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

159
arb/common.go Normal file
View File

@@ -0,0 +1,159 @@
package arb
import (
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/orm"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banstagy/com"
"gonum.org/v1/gonum/floats"
"math"
"slices"
"sort"
)
func init() {
strat.AddStratGroup("arb", map[string]strat.FuncMakeStrat{
"stat": MeanRegressStat,
})
}
type spikeTuple struct {
A *orm.ExSymbol
B *orm.ExSymbol
Score float64 // 分数,越高越好
BigSlope float64 // 大趋势平均斜率
PftRate float64 // (hh-ll)/minChg平均倍率
RawXBig int // 价格比率与大周期均线交叉次数
LastDiff float64 // 最新fast与大趋势偏移,是PftRate倍数,越小越好
}
/*
发现两个品种价格比率套利机会。
通过此函数发现的机会实际上是交易量很小,价格变动百分比较大的小品种的波动。如XEM,最小变动0.6%,不如直接交易这个品种
要求:EMA1333较稳定,(hh-ll)/maxChg较大,ratio和大均线交叉次数较多
建议:pftRate>0.4,rawXBig>200,score>0.3
*/
func findSpikeTuples(endMS int64, tf string, barNum, hlWindow, itemMax int) ([]*spikeTuple, *errs.Error) {
// 获取所有标的的K线
exgId := exg.Default.Info().ID
exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear)
exsList, barList, _, err := getExsBars(exg.Default, exsMap, tf, 0, endMS, barNum)
if err != nil {
return nil, err
}
// 计算周期内关键指标
minChg := 0.0005 * 4 * 4 // 变动至少是两次开平仓手续费的4倍
bigSlopeDist := 120 // 计算大周期斜率的距离
items := make([]*spikeTuple, 0, len(exsList)*2)
halfWindow := hlWindow / 2
if hlWindow%2 > 0 {
halfWindow += 1
}
for i := 0; i < len(exsList); i++ {
barsA := barList[i]
for j := i + 1; j < len(exsList); j++ {
barsB := barList[j]
barsRes := getAlignBars(barsA, barsB)
a, b := barsRes[0], barsRes[1]
if len(a) < barNum/2 {
continue
}
var ma = com.NewEMA(0.08)
//var maSlow = common.NewEMA(0.03)
var maBig = com.NewEMA(0.0015)
var rawXBig = com.NewCrossLog()
//var rawXfa = common.NewCrossLog()
//var vmax, vmin = float64(0), float64(0)
var raw = make([]float64, 0, len(a))
var bbRates = make([]float64, 0, len(a)/3)
var spikeNum = 0
var maBigs = make([]float64, 0, len(a))
var bigSlopes = make([]float64, 0, 100)
for m := 0; m < len(a); m++ {
ratio := a[m].Close / b[m].Close
raw = append(raw, ratio)
maVal := ma.Update(ratio)
maBigV := maBig.Update(ratio)
if math.IsNaN(maBigV) {
continue
}
maBigs = append(maBigs, maBigV)
if len(maBigs)%bigSlopeDist == 0 && len(maBigs) > 0 {
// 采样大周期斜率
bigSlopes = append(bigSlopes, math.Abs(maBigV/maBigs[len(maBigs)-bigSlopeDist]-1))
}
if m%halfWindow == 0 && len(raw) > hlWindow {
// 计算hh-ll的盈利空间
near5 := raw[len(raw)-hlWindow:]
bbRates = append(bbRates, (slices.Max(near5)-slices.Min(near5))/maVal/minChg)
}
// 计算raw与big均线交叉次数
if ratio != maBigV {
rawXBig.Update(ratio - maBigV)
fxs := rawXBig.Get()
if fxs == 1 {
// 只记录上穿,避免交叉太多
spikeNum += 1
}
}
// 计算比率在均线附近振幅可盈利的次数
//rawXfa.Update(ratio - maVal)
//rxf := rawXfa.Get()
//if rxf == 1 || rxf == -1 {
// pft := (vmax-vmin)/maVal > minChg
// if pft {
// spikeNum += 1
// }
// vmin = maVal
// vmax = maVal
//}
//if ratio > maVal {
// vmax = max(vmax, ratio)
//} else {
// vmin = min(vmin, ratio)
//}
}
// 平均大周期斜率
avgSlope := floats.Sum(bigSlopes) * 50 / float64(len(bigSlopes))
slopeRate := math.Pow(1-min(1, avgSlope), 1.5)
// 平均盈利空间
sort.Slice(bbRates, func(i, j int) bool {
return bbRates[i] < bbRates[j]
})
bbRates = bbRates[:len(bbRates)/2]
avgPftRate := floats.Sum(bbRates) / float64(len(bbRates))
// 与大周期交叉的分数,交叉少于200权重降低
hitScore := math.Log(float64(spikeNum)/200 + 1)
val := hitScore * avgPftRate * slopeRate
// 计算fast与大周期偏移
lastDiff := math.Abs(ma.Val/maBig.Val-1) / minChg / avgPftRate
items = append(items, &spikeTuple{
A: exsList[i],
B: exsList[j],
Score: val,
BigSlope: avgSlope,
PftRate: avgPftRate,
RawXBig: spikeNum,
LastDiff: lastDiff,
})
}
}
sort.Slice(items, func(i, j int) bool {
return items[i].Score > items[j].Score
})
tops := items[:min(1000, int(float64(len(items))*0.1))]
res := make([]*spikeTuple, 0, len(tops)/2)
hits := make(map[string]int)
for _, t := range tops {
numA, _ := hits[t.A.Symbol]
numB, _ := hits[t.B.Symbol]
if max(numA, numB) < itemMax {
hits[t.A.Symbol] = numA + 1
hits[t.B.Symbol] = numB + 1
res = append(res, t)
}
}
return res, nil
}

52
arb/stat_group.go Normal file
View File

@@ -0,0 +1,52 @@
package arb
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/orm"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
"sort"
)
func StatGroupPairs() *errs.Error {
exgId := "binance"
endMS := config.TimeRange.EndMS
// 生成待订阅标的列表
exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear)
limit := 720
exsList, barList, _, err := getExsBars(exg.Default, exsMap, "1h", 0, endMS, limit)
if err != nil {
return err
}
arr := make([]*core.StrFloat, 0, len(exsList))
for i, exs := range exsList {
var sumVal float64
bars := barList[i]
if float64(len(bars)) < float64(limit)*0.9 {
continue
}
for _, b := range bars {
sumVal += b.Volume * b.Close
}
arr = append(arr, &core.StrFloat{
Str: exs.Symbol,
Val: sumVal,
})
}
// 按成交量降序,取前30%
sort.Slice(arr, func(i, j int) bool {
return arr[i].Val > arr[j].Val
})
cutNum := int(float64(len(arr)) * 0.2)
cutPairs := make([]string, 0, cutNum)
for i := 0; i < cutNum; i++ {
base, _, _, _ := core.SplitSymbol(arr[i].Str)
cutPairs = append(cutPairs, base)
}
log.Info("cut 20% of pairs", zap.Int("num", cutNum), zap.Int("total", len(arr)), zap.Strings("items", cutPairs))
return nil
}

271
arb/stat_job.go Normal file
View File

@@ -0,0 +1,271 @@
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})
}
}
}
},
}
}

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
}
}

348
arb/stat_prof.go Normal file
View File

@@ -0,0 +1,348 @@
package arb
import (
"fmt"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/orm"
"github.com/banbox/banbot/strat"
"github.com/banbox/banbot/utils"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"github.com/banbox/banstagy/com"
"go.uber.org/zap"
"gonum.org/v1/gonum/floats"
"math"
"sort"
"strconv"
"strings"
)
type IJV struct {
I int
J int
V float64
}
func ProfTuples() *errs.Error {
exgId := "binance"
tf := "1m"
endMS := config.TimeRange.EndMS
// 生成待订阅标的列表
jobs := make(map[string]*strat.StratJob)
exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear)
exsList, barList, jobs, err := getExsBars(exg.Default, exsMap, "1h", 0, endMS, 720)
if err != nil {
return err
}
// 先从1h级别过滤,筛选比率较稳定的,取前10%,最多2k个
maLen := 17
stepMa := 8 //对应480分钟
ts := make([]*IJV, 0, len(exsList)*2)
for i := 0; i < len(exsList); i++ {
barsA := barList[i]
for j := i + 1; j < len(exsList); j++ {
barsB := barList[j]
barsRes := getAlignBars(barsA, barsB)
if len(barsRes) == 0 {
continue
}
a, b := barsRes[0], barsRes[1]
if len(a) <= 100 {
continue
}
slopes := make([]float64, 0, 90)
sum := float64(0)
avgs := make([]float64, 0, len(a))
for m := 0; m < len(a); m++ {
sum += a[m].Close / b[m].Close
if m >= maLen {
sum -= a[m-maLen].Close / b[m-maLen].Close
avg := sum / float64(maLen)
avgs = append(avgs, avg)
if len(avgs)%stepMa == 1 && len(avgs) > stepMa {
slopes = append(slopes, math.Abs(avg/avgs[len(avgs)-stepMa-1]-1)*100)
}
}
}
if len(slopes) < 20 {
continue
}
avgSlope := floats.Sum(slopes) / float64(len(slopes))
if avgSlope < 0.05 {
continue
}
ts = append(ts, &IJV{I: i, J: j, V: avgSlope})
}
}
sort.Slice(ts, func(i, j int) bool {
return ts[i].V < ts[j].V
})
tops := ts[:min(2000, int(float64(len(ts))*0.1))]
log.Info("cut top tuples", zap.Int("num", len(tops)), zap.Int("all", len(ts)))
var tuples = make([][]string, 0, len(tops))
its := make([]*com.CodeVal, 0, len(tops))
for _, t := range tops {
ca, cb := exsList[t.I].Symbol, exsList[t.J].Symbol
tuples = append(tuples, []string{ca, cb})
codeA, _, _, _ := core.SplitSymbol(ca)
codeB, _, _, _ := core.SplitSymbol(cb)
if codeA > codeB {
codeA, codeB = codeB, codeA
}
its = append(its, &com.CodeVal{Code: codeA + "/" + codeB, Val: t.V})
}
printCodeVals(its)
sta := newStatTwo(jobs, tuples)
barNum := 0
// 输出csv信息
var datas = make(map[string][]float64)
// 每行表示一对;每列表示一个时间帧
var rows = make(map[string][]float64)
colCount := 0 // 列数量,即时间采样数量
// 进度条
totalSecs := (endMS - config.TimeRange.StartMS) / 1000
pbar := utils.NewPrgBar(int(totalSecs), "Run")
defer pbar.Close()
onStaBatch := func(arr []*Tuple) {
if barNum%500 != 0 && barNum < 1800 {
return
}
colCount += 1
for _, it := range arr {
code := it.env.Symbol
if math.IsNaN(it.maSlope) {
continue
}
absVal := math.Abs(it.maSlope * 100)
old, _ := datas[code]
datas[code] = append(old, absVal)
col, _ := rows[code]
if len(col) < colCount {
col = append(col, make([]float64, colCount-len(col))...)
}
rows[code] = append(col, absVal)
}
}
// Kline回调
onBar := func(b *orm.InfoKline, nexts []*orm.InfoKline) {
if b.Time > sta.barMS {
if !b.IsWarmUp && sta.barMS > 0 {
pbar.Add(int((b.Time - sta.barMS) / 1000))
}
barNum += 1
sta.barMS = b.Time
arr := sta.popTuples()
if !b.IsWarmUp {
onStaBatch(arr)
}
}
sta.OnPrice(b.Symbol, b.Close)
}
var replayErr *errs.Error
verCh := make(chan int, 1)
onData := func(evt *orm.DataSeries, _ []*orm.DataSeries) {
bar, err_ := orm.AsKline(evt)
if err_ != nil {
replayErr = errs.New(errs.CodeRunTime, err_)
select {
case verCh <- -1:
default:
}
return
}
onBar(bar, nil)
}
err = biz.RunHistSeries(&biz.RunHistSeriesArgs{
ExsList: exsList,
Start: config.TimeRange.StartMS,
End: config.TimeRange.EndMS,
VerCh: verCh,
OnData: onData,
TfWarms: map[string]int{tf: 0}, // 无需预热,必须所有bar一同运行
})
if err != nil {
return err
}
if replayErr != nil {
return replayErr
}
// 计算平均大趋势斜率
var items = make([]*com.CodeVal, 0, len(datas))
var avgs = make(map[string]float64)
for code, arr := range datas {
avgVal := floats.Sum(arr) / float64(len(arr))
items = append(items, &com.CodeVal{Code: code, Val: avgVal})
avgs[code] = avgVal
}
printCodeVals(items)
// 生成csv文件
outRows := make([][]string, 0)
for key, row := range rows {
if len(row) < colCount {
row = append(row, make([]float64, colCount-len(row))...)
rows[key] = row
}
out := make([]string, 0, colCount+2)
out = append(out, key, strconv.FormatFloat(avgs[key], 'f', 2, 64))
for _, v := range row {
out = append(out, strconv.FormatFloat(v, 'f', 2, 64))
}
outRows = append(outRows, out)
}
return utils.WriteCsvFile("D:/slopes.csv", outRows, false)
}
func getExsBars(exchange banexg.BanExchange, exsMap map[int32]*orm.ExSymbol, tf string, startMS, endMS int64, limit int) ([]*orm.ExSymbol, [][]*banexg.Kline, map[string]*strat.StratJob, *errs.Error) {
_, err := orm.LoadMarkets(exg.Default, false)
if err != nil {
return nil, nil, nil, err
}
err = orm.BulkDownOHLCV(exchange, exsMap, tf, startMS, endMS, limit, nil)
if err != nil {
return nil, nil, nil, err
}
exsList := make([]*orm.ExSymbol, 0, len(exsMap))
barList := make([][]*banexg.Kline, 0, len(exsMap))
jobs := make(map[string]*strat.StratJob)
for _, exs := range exsMap {
_, quote, _, ident := core.SplitSymbol(exs.Symbol)
if quote != "USDT" || ident != "" {
continue
}
_, bars, err := orm.GetOHLCV(exs, tf, startMS, endMS, limit, false)
if err != nil {
return nil, nil, nil, err
}
exsList = append(exsList, exs)
barList = append(barList, bars)
jobs[exs.Symbol] = &strat.StratJob{Symbol: exs}
}
return exsList, barList, jobs, nil
}
func getAlignBars(items ...[]*banexg.Kline) [][]*banexg.Kline {
if len(items) == 0 {
return nil
}
if len(items) == 1 {
return items
}
first := items[0]
if len(first) == 0 {
return nil
}
headLen, headStart, headStop := len(first), first[0].Time, first[len(first)-1].Time
allAlign, minLen := true, headLen
for _, a := range items {
if len(a) == 0 {
return nil
}
if len(a) != headLen || a[0].Time != headStart || a[len(a)-1].Time != headStop {
allAlign = false
}
minLen = min(minLen, len(a))
}
if allAlign {
return items
}
var resList = make([][]*banexg.Kline, len(items))
var indexs = make([]int, len(items))
started := false
reachEnd := false
for {
stampCounts := make(map[int64]int)
maxNum, mostTime, maxTime := 0, int64(0), int64(0)
for i, idx := range indexs {
b := items[i][idx]
num, _ := stampCounts[b.Time]
stampCounts[b.Time] = num + 1
if num >= maxNum {
maxNum, mostTime = num+1, b.Time
}
maxTime = max(maxTime, b.Time)
}
idxChange := false
if len(stampCounts) > 1 {
// 至少2个不同时间戳,将较旧的移动
if maxNum <= len(indexs)/2 {
mostTime = maxTime
}
for i, idx := range indexs {
arr := items[i]
for arr[idx].Time < mostTime {
idx += 1
if idx >= minLen {
reachEnd = true
break
}
}
if idx > indexs[i] {
idxChange = true
indexs[i] = idx
}
}
} else if !started {
started = true
}
if started && maxNum >= len(indexs)/2 {
// 已开始,maxNum超过一半时有效
for i, idx := range indexs {
b := items[i][idx]
res := resList[i]
if b.Time > mostTime {
b = res[len(res)-1]
} else if b.Time < mostTime {
panic("bad logic")
} else {
indexs[i] = idx + 1
idxChange = true
if idx+1 >= minLen {
reachEnd = true
}
}
resList[i] = append(res, b)
}
}
if !idxChange {
for i, idx := range indexs {
arr := items[i]
if arr[idx].Time <= mostTime {
idx += 1
indexs[i] = idx
if idx >= minLen {
reachEnd = true
break
}
}
}
}
if reachEnd {
break
}
}
if len(resList[0]) == 0 {
return nil
}
return resList
}
func printCodeVals(items []*com.CodeVal) {
sort.Slice(items, func(i, j int) bool {
return items[i].Val < items[j].Val
})
var b strings.Builder
for _, it := range items {
b.WriteString(it.Code)
b.WriteString(": ")
b.WriteString(strconv.FormatFloat(it.Val, 'f', 2, 64))
b.WriteString(", ")
}
fmt.Printf(b.String())
fmt.Printf("\n")
}

203
arb/stat_showTwo.go Normal file
View File

@@ -0,0 +1,203 @@
package arb
import (
_ "embed"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/opt"
"github.com/banbox/banbot/orm"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/utils"
"github.com/banbox/banta"
"math"
"strings"
)
func GenTwoPairRatio() *errs.Error {
//LineExamples()
//return nil
//groups := []string{"1000PEPE/USDT:USDT", "1000BONK/USDT:USDT"}
groups := []string{
"LPT,ATH,MOODENG,RNDR,APT,TRUMP,DOT,HYPE,BAKE,SPX,BOME,TUT,PUMP,BERA,OG,EOS,MERL,TAO,NMR,FET,TIA,FHE,ZORA,LINK,POL,TRX,1000PEPE,ETHFI,PROVE,TREE",
"RED,SUI,VIRTUAL,NEIRO,GALA,TON,PAXG,XNY,CRV,NAORIS,XRP,NEAR,AVAX,ETC,XLM,MATIC,AAVE,SEI,1000SHIB,DAM,SPK,MEME,OP,FIL,KAITO,FARTCOIN,EIGEN,BCH,IP,XPL",
}
//startMS := config.TimeRange.StartMS
endMS, _ := btime.ParseTimeMS("20250921")
tf := "15m"
core.Market = "linear"
tfMSecs := int64(utils.TFToSecs(tf) * 1000)
barLimit := 30000
startMS := endMS - tfMSecs*int64(barLimit)
barsArr := make([][]*TimeFlt, 0, len(groups))
allPairs := strings.Split(strings.Join(groups, ","), ",")
allPairs, err := config.ParsePairs(allPairs...)
if err != nil {
return err
}
exsMap := make(map[int32]*orm.ExSymbol)
for _, pair := range allPairs {
exs, err := orm.GetExSymbolCur(pair)
if err != nil {
return err
}
exsMap[exs.ID] = exs
}
err = orm.BulkDownOHLCV(exg.Default, exsMap, tf, startMS, endMS, 0, nil)
if err != nil {
return err
}
for _, group := range groups {
pairs := strings.Split(group, ",")
prices, err := GetGroupPrices(pairs, tf, startMS, endMS)
if err != nil {
return err
}
barsArr = append(barsArr, prices)
}
rateArr := make([]*TimeFlt, len(barsArr[0]))
for i, va := range barsArr[0] {
rateArr[i] = &TimeFlt{
Stamp: va.Stamp,
Val: va.Val / barsArr[1][i].Val,
}
}
path := "D:/ratios.html"
title := "price ratio"
return dumpRateChart(rateArr, tf, path, title)
}
// GetGroupPrices 计算分组内多个品种的平均价格
func GetGroupPrices(pairs []string, tf string, startMS, endMS int64) ([]*TimeFlt, *errs.Error) {
var groupBar [][]*banexg.Kline
pairs, err := config.ParsePairs(pairs...)
if err != nil {
return nil, err
}
tfMSecs := int64(utils.TFToSecs(tf) * 1000)
barLimit := (endMS - startMS) / tfMSecs
for _, pair := range pairs {
exs, err := orm.GetExSymbolCur(pair)
if err != nil {
return nil, err
}
_, bars, err := orm.GetOHLCV(exs, tf, startMS, endMS, 0, false)
if err != nil {
return nil, err
}
if float64(len(bars)) > float64(barLimit)*0.8 {
groupBar = append(groupBar, bars)
}
}
// 对分组内品种计算价格归一化均值
groupBar = getAlignBars(groupBar...)
if len(groupBar) == 0 {
return nil, errs.NewMsg(errs.CodeParamInvalid, "align bars fail for %v", pairs)
}
for _, gp := range groupBar {
base := gp[0].Close
gp[0].Close = 1
for i := 1; i < len(gp); i++ {
gp[i].Close /= base
}
}
barNum := len(groupBar[0])
bars := make([]*TimeFlt, 0, barNum)
for j := 0; j < barNum; j++ {
var sumVal float64
for _, gp := range groupBar {
sumVal += gp[j].Close
}
bars = append(bars, &TimeFlt{
Stamp: groupBar[0][j].Time,
Val: sumVal / float64(len(groupBar)),
})
}
return bars, nil
}
type TimeFlt struct {
Stamp int64
Val float64
}
func dumpRateChart(rates []*TimeFlt, tf string, path, title string) *errs.Error {
if len(rates) == 0 {
return nil
}
var labels []string
var arrRaw []float64
var arrFast []float64
var arrSlow []float64
var arrMdMa []float64
var arrBigMa []float64
var arrBigUp []float64
var arrBigDn []float64
var arrHH, arrLL []float64
bollWid := 0.0043
showNum := 1440
stepFlt := float64(len(rates)) / float64(showNum)
stepSize := int(math.Round(stepFlt))
tfMSecs := int64(utils.TFToSecs(tf) * 1000)
lenFast := int(stepFlt * 5.77)
lenSlow := int(stepFlt * 31.25)
lenMid := int(stepFlt * 96.5)
lenBig := int(stepFlt * 320.2)
lenStd := int(stepFlt * 96.3)
lenHL := int(stepFlt * 14.42)
var idx = 0
env, _ := banta.NewBarEnv(core.ExgName, core.Market, "", tf)
env.TimeStart = rates[idx].Stamp
env.TimeStop = rates[idx].Stamp + tfMSecs
rate := env.NewSeries(nil)
rowId := 0
dateFmt := "01-02 15:04"
for idx < len(rates) {
item := rates[idx]
env.TimeStart += tfMSecs
env.TimeStop += tfMSecs
env.BarNum += 1
rowId += 1
ratio := item.Val
rate.Append(ratio)
maFast := banta.EMA(rate, lenFast)
maSlow := banta.EMA(rate, lenSlow) //66
maMid := banta.EMA(rate, lenMid).Get(0)
maBig := banta.EMA(rate, lenBig)
dev := banta.StdDev(rate, lenStd)
stdDev := banta.SMA(dev, lenStd).Get(0) * 1.7
bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5
hh := banta.Highest(rate, lenHL).Get(0)
ll := banta.Lowest(rate, lenHL).Get(0)
bigVal := maBig.Get(0)
if rowId%stepSize == 0 && !math.IsNaN(maMid) {
labels = append(labels, btime.ToDateStr(item.Stamp, dateFmt))
arrRaw = append(arrRaw, ratio)
arrFast = append(arrFast, maFast.Get(0))
arrSlow = append(arrSlow, maSlow.Get(0))
arrMdMa = append(arrMdMa, maMid)
arrBigMa = append(arrBigMa, bigVal)
arrBigUp = append(arrBigUp, maMid+bollWidAvg)
arrBigDn = append(arrBigDn, maMid-bollWidAvg)
arrHH = append(arrHH, hh)
arrLL = append(arrLL, ll)
}
idx += 1
}
return opt.DumpChart(path, title, labels, 5, nil, []*opt.ChartDs{
{Label: "raw", Data: arrRaw},
{Label: "ma5", Data: arrFast, Hidden: true},
{Label: "ma10", Data: arrSlow, Hidden: true},
{Label: "mid", Data: arrMdMa},
{Label: "big", Data: arrBigMa, Hidden: true},
{Label: "upp", Data: arrBigUp, Hidden: false},
{Label: "dnn", Data: arrBigDn, Hidden: false},
//{Label: "hh", Data: arrHH, Hidden: true},
//{Label: "ll", Data: arrLL, Hidden: true},
})
}

31
arb/stat_spike.go Normal file
View File

@@ -0,0 +1,31 @@
package arb
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banexg/errs"
)
/*
捕捉价格比率的瞬时剧烈波动
*/
func FindTupleSpikes() *errs.Error {
tf := "1m"
endMS := config.TimeRange.EndMS
items, err := findSpikeTuples(endMS, tf, 2000, 5, 5)
if err != nil {
return err
}
fmt.Println("tuple,score,slope,pftRate,rawXBig")
for _, t := range items {
codeA, _, _, _ := core.SplitSymbol(t.A.Symbol)
codeB, _, _, _ := core.SplitSymbol(t.B.Symbol)
if codeA > codeB {
codeA, codeB = codeB, codeA
}
fmt.Printf("%s/%s,%.2f,%.4f,%.3f,%v\n", codeA, codeB, t.Score, t.BigSlope, t.PftRate, t.RawXBig)
}
return nil
}

391
arb/stat_test.go Normal file
View File

@@ -0,0 +1,391 @@
package arb
import (
"fmt"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/data"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/goods"
"github.com/banbox/banbot/orm"
utils2 "github.com/banbox/banbot/utils"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"github.com/banbox/banexg/utils"
"github.com/sasha-s/go-deadlock"
"go.uber.org/zap"
"math/rand"
"path/filepath"
"sort"
"strings"
"testing"
"time"
)
func TestPairPick(t *testing.T) {
all := "ETH,BTC,SOL,DOGE,XRP,WLFI,MYX,ENA,BNB,1000PEPE,LINK,SUI,AVAX,ADA,PUMP,WLD,SOMI,HYPE,BIO,FARTCOIN,NMR,M,PENGU,PYTH,AAVE,LTC,HIFI,TA,WIF,ARB,RNDR,TRUMP,1000BONK,BCH,DOT,UNI,IP,BAKE,MATIC,FIL,EOS,TRX,NEAR,ONDO,XPL,1000SHIB,DOLO,PROMPT,CRV,OP,MEME,ETHFI,HBAR,XLM,ETC,TIA,LDO,SEI,TREE,EIGEN,TAO,XNY,SPK,APT,DAM,W,NAORIS,INJ,SAPIEN,JELLYJELLY,MOODENG,VIRTUAL,RED,TST,QTUM,MERL,TON,F,TUT,PNUT,LPT,SOLV,PENDLE,CFX,POL,KAITO,BERA,ZORA,GALA,FHE,PAXG,ONT,JUP,OG,FET,PUMPBTC,SKL,GAL,ATOM,NEIRO,BOME,MAV,PROVE,ATH,SPX"
pickA, pickB := 30, 30
coins := strings.Split(all, ",")
rand.Shuffle(len(coins), func(i, j int) { coins[i], coins[j] = coins[j], coins[i] })
groupA := coins[:pickA]
groupB := coins[pickA : pickA+pickB]
fmt.Println(strings.Join(groupA, ","))
fmt.Println(strings.Join(groupB, ","))
}
// 从高频逐笔数据中提取每隔5秒的价格
func TestExtractPrices(t *testing.T) {
core.SetRunMode(core.RunModeBackTest)
err := biz.SetupComsExg(&config.CmdArgs{})
if err != nil {
panic(err)
}
loader, err := data.NewWsDataLoader()
if err != nil {
panic(err)
}
dataDir := config.GetDataDir()
outPath := filepath.Join(dataDir, "dump", "group1.gob")
group1 := "LPT,ATH,MOODENG,RNDR,APT,TRUMP,DOT,HYPE,BAKE,SPX,BOME,TUT,PUMP,BERA,OG,EOS,MERL,TAO,NMR,FET,TIA,FHE,ZORA,LINK,POL,TRX,1000PEPE,ETHFI,PROVE,TREE"
//group2 := "RED,SUI,VIRTUAL,NEIRO,GALA,TON,PAXG,XNY,CRV,NAORIS,XRP,NEAR,AVAX,ETC,XLM,MATIC,AAVE,SEI,1000SHIB,DAM,SPK,MEME,OP,FIL,KAITO,FARTCOIN,EIGEN,BCH,IP,XPL"
pairs := strings.Split(group1, ",")
pairs, err = config.ParsePairs(pairs...)
if err != nil {
panic(err)
}
result := make(map[string][]float64)
if utils2.Exists(outPath) {
err = utils2.DecodeGobFile(outPath, &result)
if err != nil {
panic(err)
}
cache := make(map[string]int)
num := 0
for k, v := range result {
cache[k] = len(v)
if len(v) > 0 {
num += 1
}
}
log.Info("load cache", zap.Int("num", num), zap.Any("keys", cache))
}
lock := deadlock.Mutex{}
year, mon, day := time.Now().Add(-time.Hour * 24).Date()
endTime := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC)
year, mon, day = endTime.AddDate(0, 0, -15).Date()
startTime := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC)
gapMS := int64(5000) // 采样间隔ms
startMS := startTime.UnixMilli()
endMS := endTime.UnixMilli()
size := int((endMS - startMS) / gapMS)
pBar := utils2.NewPrgBar(len(pairs)*size, "run")
defer pBar.Close()
err = utils2.ParallelRun(pairs, 4, func(i int, pair string) *errs.Error {
lock.Lock()
arr, ok := result[pair]
lock.Unlock()
if ok && len(arr) > 0 {
pBar.Add(size)
return nil
}
arr = make([]float64, size+10)
lastIdx := 0
saveNum := 0
curTime := startTime
success := true
for curTime.Before(endTime) {
trades, err := loader.LoadTrades(&data.WsSymbol{
ExgId: "binance",
Market: banexg.MarketLinear,
WsType: core.WsSubTrade,
Symbol: pair,
Date: curTime.Format(core.DateFmt),
Hour: curTime.Hour(),
})
if err != nil {
log.Error("down trades fail", zap.Error(err))
success = false
break
}
curTime = curTime.Add(time.Hour)
for _, td := range trades {
curIdx := int((td.Timestamp - startMS) / gapMS)
if curIdx > lastIdx {
prevVal := arr[lastIdx]
for lastIdx+1 < curIdx {
lastIdx += 1
arr[lastIdx] = prevVal
}
lastIdx = curIdx
arr[curIdx] = td.Price
}
}
if lastIdx > saveNum {
pBar.Add(lastIdx - saveNum)
saveNum = lastIdx
}
}
if success {
lock.Lock()
result[pair] = arr
lock.Unlock()
}
return nil
})
err = utils2.EncodeGob(outPath, result)
if err != nil {
panic(err)
}
}
// 测试随机两组品种的平均价格比率(逐秒级别)
func TestDayBatchRates(t *testing.T) {
// 读取两组30个交易品种的每隔5s的价格序列,计算平均价比率,研究套利空间
tf := "5s"
dataDir := config.GetDataDir()
outPath := filepath.Join(dataDir, "dump", "group1.gob")
group1 := make(map[string][]float64)
err := utils2.DecodeGobFile(outPath, &group1)
if err != nil {
panic(err)
}
outPath = filepath.Join(dataDir, "dump", "group2.gob")
group2 := make(map[string][]float64)
err = utils2.DecodeGobFile(outPath, &group2)
if err != nil {
panic(err)
}
tfSecs := utils.TFToSecs(tf)
daySize := utils.TFToSecs("1d") / tfSecs
dataLen := 0
for _, arr := range group1 {
dataLen = len(arr)
break
}
tfMSecs := int64(tfSecs * 1000)
year, mon, day := time.Now().Add(-time.Hour * 24).Date()
endMS := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC).UnixMilli()
startMS := endMS - int64(dataLen)*tfMSecs
dumpDir := filepath.Join(config.GetDataDir(), "dump")
for offset := 0; offset+daySize < dataLen; offset += daySize {
offsetMS := startMS + int64(offset)*tfMSecs
batchA := normInRange(group1, offset, offset+daySize, offsetMS, tfMSecs)
batchB := normInRange(group2, offset, offset+daySize, offsetMS, tfMSecs)
rateArr := make([]*TimeFlt, len(batchA))
for i, va := range batchA {
rateArr[i] = &TimeFlt{
Stamp: va.Stamp,
Val: va.Val / batchB[i].Val,
}
}
dateStr := btime.ToDateStr(offsetMS, "20060102")
dumpPath := filepath.Join(dumpDir, dateStr+".html")
log.Info("dump rate chart", zap.String("title", dateStr))
err = dumpRateChart(rateArr, tf, dumpPath, dateStr)
if err != nil {
panic(err)
}
}
}
func normInRange(dataMap map[string][]float64, start, end int, offsetMS, stepMS int64) []*TimeFlt {
batchA := make(map[string][]float64)
minLen := -1
// 对每个品种的价格序列进行归一化
for key, arr := range dataMap {
vals := arr[start:end]
res := make([]float64, len(vals))
first := float64(0)
for i, v := range vals {
if v > 0 {
if first > 0 {
res[i] = v / first
} else {
res[i] = 1
first = v
}
}
}
batchA[key] = res
if minLen < 0 {
minLen = len(res)
} else {
minLen = min(minLen, len(res))
}
}
// 直接对所有品种价格序列取平均值
res := make([]*TimeFlt, minLen)
for i := 0; i < minLen; i++ {
sumVal := float64(0)
validNum := 0
for _, arr := range batchA {
val := arr[i]
if val > 0 {
sumVal += val
validNum += 1
}
}
item := &TimeFlt{
Stamp: offsetMS + int64(i)*stepMS,
}
if validNum > 0 {
item.Val = sumVal / float64(validNum)
}
res[i] = item
}
return res
}
/*
每7日取成交量最高top60
随机分为两组计算平均价格比率
获取7日成交量倒序前30%的品种列表
每天计算:
计算最近14天1h的相关性矩阵
按相关性从高到低取top60
奇数位和偶数位分为两组
对比了按相关性从高到低取top60和随机分配top60,差别不大,简单起见,使用随机方法
*/
func TestDailyGroupArb(t *testing.T) {
core.SetRunMode(core.RunModeBackTest)
err := biz.SetupComsExg(&config.CmdArgs{})
if err != nil {
panic(err)
}
groupSize := 30
dayTf := "1d"
dayMSecs := int64(utils.TFToSecs("1d") * 1000)
year, mon, day := time.Now().Add(-time.Hour * 24).Date()
endMS := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC).UnixMilli()
startMS := endMS - dayMSecs*28
minListMS := endMS - dayMSecs*180
curMS := startMS
exsMap := orm.GetExSymbols(core.ExgName, core.Market)
pairs := make([]string, 0, len(exsMap))
for _, exs := range exsMap {
if exs.ListMs > minListMS {
// 忽略上市不久的品种
continue
}
pairs = append(pairs, exs.Symbol)
}
err = orm.BulkDownOHLCV(exg.Default, exsMap, "1h", startMS, endMS, 0, nil)
if err != nil {
panic(err)
}
var pre30Pairs []string
var pre30Ms int64
dumpDir := filepath.Join(config.GetDataDir(), "dump")
for curMS < endMS {
if len(pre30Pairs) == 0 || (curMS-pre30Ms)/dayMSecs >= 7 {
// 获取14日成交量倒序前30%的品种列表
pre30Pairs = nil
symbolVols, err := goods.GetSymbolVols(pairs, dayTf, 14, curMS, false)
if err != nil {
panic(err)
}
sort.Slice(symbolVols, func(i, j int) bool {
return symbolVols[i].Vol > symbolVols[j].Vol
})
cutNum := int(float64(len(symbolVols)) * 0.23)
for i := 0; i < cutNum; i++ {
pre30Pairs = append(pre30Pairs, symbolVols[i].Symbol)
}
pre30Ms = curMS
dateStr := btime.ToDateStr(curMS, core.DateFmt)
log.Info("cut pre 30% pairs by vols", zap.Int("num", len(pre30Pairs)), zap.String("date", dateStr))
}
// 按成交量取前面的,然后随机挑选。
coins := pre30Pairs[:groupSize*2]
rand.Shuffle(len(coins), func(i, j int) { coins[i], coins[j] = coins[j], coins[i] })
var pairsA, pairsB []string
downMap := make(map[int32]*orm.ExSymbol)
for i := 0; i < groupSize*2; i++ {
pair := coins[i]
if i%2 == 0 {
pairsA = append(pairsA, pair)
} else {
pairsB = append(pairsB, pair)
}
exs, _ := orm.GetExSymbolCur(pair)
downMap[exs.ID] = exs
}
// 对A和B两组计算1m加权价格
curEndMS := curMS + dayMSecs
err = orm.BulkDownOHLCV(exg.Default, downMap, "1m", curMS, curEndMS, 0, nil)
if err != nil {
panic(err)
}
pricesA, err := GetGroupPrices(pairsA, "1m", curMS, curEndMS)
if err != nil {
panic(err)
}
pricesB, err := GetGroupPrices(pairsB, "1m", curMS, curEndMS)
if err != nil {
panic(err)
}
rateArr := make([]*TimeFlt, len(pricesA))
for i, va := range pricesA {
rateArr[i] = &TimeFlt{
Stamp: va.Stamp,
Val: va.Val / pricesB[i].Val,
}
}
dateStr := btime.ToDateStr(curMS, "20060102")
dumpPath := filepath.Join(dumpDir, dateStr+"_r2.html")
log.Info("dump rate chart", zap.String("title", dateStr))
err = dumpRateChart(rateArr, "1m", dumpPath, dateStr)
if err != nil {
panic(err)
}
curMS += dayMSecs
}
}
func getPairCorrs(curMS int64, pairs []string) []string {
// 计算最近14天1h的相关性矩阵
dayMSecs := int64(utils.TFToSecs("1d") * 1000)
hourMSecs := int64(utils.TFToSecs("1h") * 1000)
pre14MS := curMS - dayMSecs*14
names := make([]string, 0, len(pairs))
dataArr := make([][]float64, 0, len(pairs))
klineNum := int((curMS - pre14MS) / hourMSecs)
err := orm.FastBulkOHLCV(exg.Default, pairs, "1h", pre14MS, curMS, 0, func(s string, s2 string, klines []*banexg.Kline, infos []*orm.AdjInfo) {
if len(klines) == klineNum {
prices := make([]float64, 0, len(klines))
for _, b := range klines {
prices = append(prices, b.Close)
}
names = append(names, s)
dataArr = append(dataArr, prices[:klineNum])
}
})
if err != nil {
panic(err)
}
_, avgs, err_ := utils2.CalcCorrMat(klineNum, dataArr, true)
if err_ != nil {
panic(err_)
}
// 基于平均相似度降序
arr := make([]*goods.IdVal, 0, len(avgs))
lefts := make(map[int]bool)
for i, avg := range avgs {
arr = append(arr, &goods.IdVal{Id: i, Val: avg})
lefts[i] = true
}
sort.Slice(arr, func(i, j int) bool {
return arr[i].Val < arr[j].Val
})
var resPairs []string
for _, v := range arr {
resPairs = append(resPairs, names[v.Id])
}
return resPairs
}