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

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