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

160 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package arb
import (
"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
}