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 }