init
This commit is contained in:
159
arb/common.go
Normal file
159
arb/common.go
Normal 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
52
arb/stat_group.go
Normal 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
271
arb/stat_job.go
Normal 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
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
|
||||
}
|
||||
}
|
||||
348
arb/stat_prof.go
Normal file
348
arb/stat_prof.go
Normal 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
203
arb/stat_showTwo.go
Normal 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
31
arb/stat_spike.go
Normal 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
391
arb/stat_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user