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

349 lines
8.6 KiB
Go
Raw Permalink 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 (
"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")
}