160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
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
|
||
}
|