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

124
ent/fund_rate.go Normal file
View File

@@ -0,0 +1,124 @@
package ent
import (
"fmt"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/orm"
utils2 "github.com/banbox/banbot/utils"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/utils"
"github.com/banbox/banstagy/com"
"math"
"path/filepath"
"sort"
"strconv"
)
/*
永续合约资金费率结算后一段时间,如果资金费率<-0.0035,未来5分钟有较大概率下降
*/
type FRate struct {
Time int64
Data []*com.CodeVal
}
func runFundEnter() *errs.Error {
exgId := "binance"
exchange, err := exg.GetWith(exgId, banexg.MarketLinear, banexg.MarketSwap)
if err != nil {
return err
}
_, err = orm.LoadMarkets(exchange, false)
if err != nil {
return err
}
dumpDir := filepath.Join(config.GetDataDir(), "dump")
err_ := utils2.EnsureDir(dumpDir, 0755)
if err_ != nil {
return errs.New(errs.CodeIOWriteFail, err_)
}
dumpPath := filepath.Join(dumpDir, exgId+"_fundingRate.json")
his := make(map[string]map[int64]float64)
err_ = utils.ReadJsonFile(dumpPath, &his, utils.JsonNumDefault)
if err_ != nil {
return errs.New(errs.CodeIOReadFail, err_)
}
dates := make(map[int64]*FRate)
for code, timeMap := range his {
for dateMS, val := range timeMap {
sta, ok := dates[dateMS]
if !ok {
sta = &FRate{Time: dateMS}
dates[dateMS] = sta
}
sta.Data = append(sta.Data, &com.CodeVal{
Code: code,
Val: val,
})
}
}
arr := make([]*FRate, 0, len(dates))
for _, it := range dates {
arr = append(arr, it)
}
sort.Slice(arr, func(i, j int) bool {
return arr[i].Time < arr[j].Time
})
// 按时间戳,对资金费率绝对值较大的,筛选K线并记录价格
rows := make([][]string, 0)
barNum := 20
backOff := 5
head := []string{"time", "code", "rate", "price"}
for i := 0; i < barNum; i++ {
sid := i - backOff
if sid >= 0 {
sid += 1
}
head = append(head, "p"+strconv.Itoa(sid))
}
rows = append(rows, head)
for _, dt := range arr {
sort.Slice(dt.Data, func(i, j int) bool {
return math.Abs(dt.Data[i].Val) > math.Abs(dt.Data[j].Val)
})
startMS := dt.Time - int64(backOff)*60000
for _, it := range dt.Data {
if math.Abs(it.Val) < 0.0005 {
break
}
exs, err := orm.GetExSymbol(exchange, it.Code)
if err != nil {
return err
}
_, bars, err := orm.GetOHLCV(exs, "1m", startMS, 0, barNum, false)
if err != nil {
return err
}
if len(bars) < barNum {
continue
}
row := make([]string, 0, 4+barNum)
initPrice := bars[backOff].Open
row = append(row, btime.ToDateStr(dt.Time, core.DefaultDateFmt), it.Code,
strconv.FormatFloat(it.Val, 'f', 6, 64),
fmt.Sprintf("%.4e", initPrice))
var pct float64
for i, b := range bars {
if i < backOff {
pct = (1 - b.Open/initPrice) * 100
} else {
pct = (b.Close/initPrice - 1) * 100
}
row = append(row, strconv.FormatFloat(pct, 'f', 4, 64))
}
rows = append(rows, row)
}
}
outPath := filepath.Join(dumpDir, exgId+"_fr.csv")
return utils2.WriteCsvFile(outPath, rows, false)
}

137
ent/his_sharpe.go Normal file
View File

@@ -0,0 +1,137 @@
package ent
import (
"errors"
"flag"
"fmt"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/opt"
"github.com/banbox/banbot/orm"
"github.com/banbox/banbot/orm/ormo"
utils2 "github.com/banbox/banexg/utils"
"strconv"
)
func hisSharpe(args []string) error {
var configPaths config.ArrString
var inPath string
var sub = flag.NewFlagSet("fac", flag.ExitOnError)
sub.Var(&configPaths, "config", "config path to use, Multiple -config options may be used")
sub.StringVar(&inPath, "in", "", "path for all pairs orders e.g.: orders.gob")
err_ := sub.Parse(args)
if err_ != nil {
return err_
}
core.SetRunMode(core.RunModeBackTest)
err := biz.SetupComsExg(&config.CmdArgs{
Configs: configPaths,
})
if err != nil {
return err
}
var startMs = config.TimeRange.StartMS
var endMS = config.TimeRange.EndMS
if len(config.StakeCurrency) == 0 {
return errors.New("`stake_currency` in yml is required")
}
var tfSecs int
var orders []*ormo.InOutOrder
if inPath == "" {
return errors.New("-in is required")
} else {
inPath = config.ParsePath(inPath)
}
orders, err = ormo.LoadOrdersGob(inPath)
if err != nil {
return err
}
var exsMap = make(map[int32]*orm.ExSymbol)
var pairMap = make(map[string]int32)
var totalCost float64
var validOdNum int
if len(orders) > 0 {
startMs = orders[0].RealEnterMS()
endMS = orders[0].RealExitMS()
for _, od := range orders {
curStart := od.RealEnterMS()
if curStart < startMs && curStart > 0 {
startMs = curStart
}
curEnd := od.RealExitMS()
if curEnd > endMS {
endMS = curEnd
}
tfSecs = max(tfSecs, utils2.TFToSecs(od.Timeframe))
curCost := od.EnterCost()
if curCost > 0 {
totalCost += curCost
validOdNum += 1
}
if _, ok := pairMap[od.Symbol]; !ok {
exs, err := orm.GetExSymbolCur(od.Symbol)
if err != nil {
return err
}
exsMap[exs.ID] = exs
pairMap[od.Symbol] = exs.ID
}
}
} else {
return errors.New("orders is empty")
}
if validOdNum == 0 {
return errors.New("no valid orders")
}
if tfSecs == 0 {
tfSecs = utils2.TFToSecs("1d")
}
tf := "1d"
// 滚动测试因子
rangeStart, _ := btime.ParseTimeMS("2022-11-19")
rangeEnd, _ := btime.ParseTimeMS("2024-11-18")
pairOrders, err := opt.CutOrdersInRange(orders, rangeStart, rangeEnd)
if err != nil {
return err
}
pairOrders = map[string][]*ormo.InOutOrder{
"WIF/USDT:USDT": pairOrders["WIF/USDT:USDT"],
}
pairInfos, err := opt.CalcPairStats(pairOrders, rangeStart, rangeEnd, tf)
if err != nil {
return err
}
initFund := config.WalletAmounts["USDT"]
rangeSecs := (rangeEnd - rangeStart) / 1000
for _, sta := range pairInfos {
reals := make([]float64, 0, len(sta.Returns))
curFund := initFund
for _, v := range sta.Returns {
curFund += v
reals = append(reals, curFund)
}
sharp, _, err := opt.CalcMeasuresByReal(reals, rangeSecs, "", 0, 0)
if err != nil {
return err
}
labels := make([]string, len(reals))
for i := range reals {
labels[i] = strconv.Itoa(i)
}
g := opt.Chart{
Labels: labels,
Datasets: []*opt.ChartDs{
{
Label: "pow(x, 1.5)",
Data: reals,
BorderColor: "rgb(75, 192, 192)",
},
},
}
g.DumpFile("D:/dump_pairs2.html")
fmt.Printf("%s sharpe: %v", sta.Symbol, sharp)
}
return nil
}

76
ent/main.go Normal file
View File

@@ -0,0 +1,76 @@
package ent
import (
"flag"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/entry"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"github.com/banbox/banstagy/arb"
"github.com/spf13/cobra"
"os"
)
func init() {
entry.AddCommand("tool", &cobra.Command{
Use: "hisSharpe",
DisableFlagParsing: true,
RunE: func(_ *cobra.Command, args []string) error {
return hisSharpe(args)
},
})
}
func RunMyEntry(args []string) *errs.Error {
if len(args) == 0 {
log.Warn("sub command is require for `my`")
return nil
}
var ag = &config.CmdArgs{}
var f = flag.NewFlagSet("web", flag.ExitOnError)
f.StringVar(&ag.LogLevel, "level", "info", "log level")
f.StringVar(&ag.TimeZone, "tz", "", "timezone")
f.StringVar(&ag.DataDir, "datadir", "", "Path to data dir.")
f.StringVar(&ag.ConfigData, "config-data", "", "yaml config string")
f.Var(&ag.Configs, "config", "config path to use, Multiple -config options may be used")
if args == nil {
args = os.Args
}
err_ := f.Parse(args[1:])
if err_ != nil {
return errs.New(errs.CodeRunTime, err_)
}
err := biz.SetupComsExg(&config.CmdArgs{
LogLevel: ag.LogLevel,
DataDir: ag.DataDir,
TimeZone: ag.TimeZone,
Configs: ag.Configs,
ConfigData: ag.ConfigData,
})
if err != nil {
return err
}
switch args[0] {
case "fundRate":
err = runFundEnter()
case "moneyFlow":
err = maxVolTest()
case "maxChg":
err = maxChgTest()
case "twoPairRatio":
err = arb.GenTwoPairRatio()
case "profTuple":
err = arb.ProfTuples()
case "tupleSpike":
err = arb.FindTupleSpikes()
case "statGroup":
err = arb.StatGroupPairs()
case "balance":
err = showBalance()
default:
log.Warn("unknown sub command for `my`")
}
return err
}

195
ent/max_chg.go Normal file
View File

@@ -0,0 +1,195 @@
package ent
import (
"fmt"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm"
"github.com/banbox/banbot/utils"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
utils2 "github.com/banbox/banexg/utils"
"github.com/banbox/banstagy/com"
"github.com/banbox/banta"
"math"
"path/filepath"
"sort"
"strconv"
)
type chgSta struct {
env *banta.BarEnv
volMa *com.Ema
chg float64 // 变动百分比
stability float64 // 稳定性分数
adx float64
rsi float64
er float64
price float64
weight float64
nexts []*orm.InfoKline
num int
}
func maxChgTest() *errs.Error {
exgId := "binance"
tf := "1m"
maAlpha := 0.03
viewNextNum := 5
tfMSecs := int64(utils2.TFToSecs(tf) * 1000)
exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear)
exsList := make([]*orm.ExSymbol, 0, len(exsMap))
envs := make(map[string]*chgSta)
staList := make([]*chgSta, 0, len(exsMap))
topStas := make([]*chgSta, 0, len(exsMap)/4)
for _, exs := range exsMap {
_, quote, _, _ := core.SplitSymbol(exs.Symbol)
if quote != "USDT" {
continue
}
exsList = append(exsList, exs)
env := &chgSta{
env: &banta.BarEnv{
TimeFrame: tf,
TFMSecs: tfMSecs,
Exchange: exs.Exchange,
MarketType: exs.Market,
Symbol: exs.Symbol,
MaxCache: 1000,
},
volMa: com.NewEMA(maAlpha),
}
envs[exs.Symbol] = env
staList = append(staList, env)
}
barNum := 0
barTimeMS := int64(0)
var rows [][]string
var head = []string{"time", "code", "chg", "weight", "price", "adx", "rsi", "er"}
for i := 0; i < viewNextNum; i++ {
head = append(head, "p"+strconv.Itoa(i+1))
}
rows = append(rows, head)
updateWeights := func() {
total := float64(0)
for _, e := range envs {
total += e.volMa.Val
}
if math.IsNaN(total) || total == 0 {
return
}
for _, e := range envs {
e.weight = (e.volMa.Val / total) * 100
}
sort.Slice(staList, func(i, j int) bool {
return staList[i].weight > staList[j].weight
})
topStas = staList[:150]
}
checkBatch := func() {
sort.Slice(topStas, func(i, j int) bool {
return math.Abs(topStas[i].chg) > math.Abs(topStas[j].chg)
})
for i := 0; i < 3; i++ {
e := topStas[i]
row := []string{
btime.ToDateStr(barTimeMS, core.DefaultDateFmt),
e.env.Symbol,
strconv.FormatFloat(e.chg, 'f', 2, 64),
strconv.FormatFloat(e.weight, 'f', 2, 64),
fmt.Sprintf("%.3e", e.price),
strconv.FormatFloat(e.adx, 'f', 2, 64),
strconv.FormatFloat(e.rsi, 'f', 2, 64),
strconv.FormatFloat(e.er, 'f', 2, 64),
}
for j := 0; j < viewNextNum; j++ {
rate := (e.nexts[j].Close/e.price - 1) * 100
row = append(row, strconv.FormatFloat(rate, 'f', 2, 64))
}
rows = append(rows, row)
}
}
totalSecs := (config.TimeRange.EndMS - config.TimeRange.StartMS) / 1000
pbar := utils.NewPrgBar(int(totalSecs), "Run")
defer pbar.Close()
onBar := func(b *orm.InfoKline, nexts []*orm.InfoKline) {
if b.Time > barTimeMS {
if !b.IsWarmUp && barTimeMS > 0 {
pbar.Add(int((b.Time - barTimeMS) / 1000))
}
barNum += 1
barTimeMS = b.Time
if barNum%20 == 1 {
// 每隔20个bar,更新全局权重
updateWeights()
}
if !b.IsWarmUp && barNum%5 == 0 {
// 每隔5个bar,计算一次变化
checkBatch()
}
}
sta := envs[b.Symbol]
sta.num += 1
e := sta.env
e.OnBar(b.Time, b.Open, b.High, b.Low, b.Close, b.Volume,
b.Quote, b.BuyVolume, b.TradeNum)
avgPrice := (b.High + b.Low + b.Close) / 3
sta.volMa.Update(b.Volume * avgPrice)
sta.chg = (b.Close/b.Open - 1) * 100
sta.adx = banta.ADX(e.High, e.Low, e.Close, 3).Get(0)
sta.rsi = banta.RSI(e.Close, 3).Get(0)
sta.er = banta.ER(e.Close, 3).Get(0)
if b.IsWarmUp {
return
}
sta.nexts = nexts
sta.price = b.Close
}
var replayErr *errs.Error
verCh := make(chan int, 1)
onData := func(evt *orm.DataSeries, nexts []*orm.DataSeries) {
bar, err_ := orm.AsKline(evt)
if err_ != nil {
replayErr = errs.New(errs.CodeRunTime, err_)
select {
case verCh <- -1:
default:
}
return
}
futureBars := make([]*orm.InfoKline, 0, len(nexts))
for _, next := range nexts {
futureBar, convErr := orm.AsKline(next)
if convErr != nil {
replayErr = errs.New(errs.CodeRunTime, convErr)
select {
case verCh <- -1:
default:
}
return
}
futureBars = append(futureBars, futureBar)
}
onBar(bar, futureBars)
}
err := biz.RunHistSeries(&biz.RunHistSeriesArgs{
ExsList: exsList,
Start: config.TimeRange.StartMS,
End: config.TimeRange.EndMS,
ViewNextNum: viewNextNum,
TfWarms: map[string]int{tf: 100},
VerCh: verCh,
OnData: onData,
})
if err == nil && replayErr != nil {
err = replayErr
}
outPath := filepath.Join(config.GetDataDir(), "dump", "maxChg.csv")
err2 := utils.WriteCsvFile(outPath, rows, false)
if err == nil {
err = err2
}
return err
}

182
ent/max_vol.go Normal file
View File

@@ -0,0 +1,182 @@
package ent
import (
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm"
"github.com/banbox/banbot/utils"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banstagy/com"
"math"
"path/filepath"
"sort"
"strconv"
)
type volSta struct {
code string
longVol *com.Ema
shortVol *com.Ema
longRate float64
shortRate float64
weight float64
price float64
nexts []*orm.InfoKline
num int
}
func maxVolTest() *errs.Error {
exgId := "binance"
tf := "1h"
maAlpha := 0.03
viewNextNum := 3
exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear)
exsList := make([]*orm.ExSymbol, 0, len(exsMap))
envs := make(map[string]*volSta)
envList := make([]*volSta, 0, len(exsMap))
for _, exs := range exsMap {
_, quote, _, _ := core.SplitSymbol(exs.Symbol)
if quote != "USDT" {
continue
}
exsList = append(exsList, exs)
env := &volSta{
code: exs.Symbol,
longVol: com.NewEMA(maAlpha),
shortVol: com.NewEMA(maAlpha),
}
envs[exs.Symbol] = env
envList = append(envList, env)
}
barNum := 0
barTimeMS := int64(0)
updateWeights := func() {
total := float64(0)
for _, e := range envs {
total += e.longVol.Val + e.shortVol.Val
}
if math.IsNaN(total) {
return
}
for _, e := range envs {
wei := (e.longVol.Val + e.shortVol.Val) / total
e.weight = math.Pow(wei, 0.5)
}
}
var rows [][]string
var head = []string{"time", "code", "longRt", "shortRt"}
for i := 0; i < viewNextNum; i++ {
head = append(head, "p"+strconv.Itoa(i+1))
}
rows = append(rows, head)
checkBatch := func() {
sort.Slice(envList, func(i, j int) bool {
return envList[i].longRate > envList[j].shortRate
})
for i := 0; i < 3; i++ {
e := envList[i]
row := []string{
btime.ToDateStr(barTimeMS, core.DefaultDateFmt),
e.code,
strconv.FormatFloat(e.longRate, 'f', 3, 64),
strconv.FormatFloat(e.shortRate, 'f', 3, 64),
}
for j := 0; j < viewNextNum; j++ {
rate := (e.nexts[j].Close/e.price - 1) * 100
row = append(row, strconv.FormatFloat(rate, 'f', 2, 64))
}
rows = append(rows, row)
}
}
totalSecs := (config.TimeRange.EndMS - config.TimeRange.StartMS) / 1000
pbar := utils.NewPrgBar(int(totalSecs), "Run")
defer pbar.Close()
onBar := func(b *orm.InfoKline, nexts []*orm.InfoKline) {
if b.Time > barTimeMS {
if !b.IsWarmUp && barTimeMS > 0 {
pbar.Add(int((b.Time - barTimeMS) / 1000))
}
barNum += 1
barTimeMS = b.Time
if barNum%20 == 1 {
// 每隔20个bar,更新全局权重
updateWeights()
}
if !b.IsWarmUp && barNum%5 == 0 {
// 每隔5个bar,计算一次变化
checkBatch()
}
}
sta := envs[b.Symbol]
sta.num += 1
lineTot := (b.High - b.Low) - math.Abs(b.Open-b.Close)
total := (b.High - b.Low) + lineTot + core.AmtDust
longTot := lineTot
if b.Open < b.Close {
longTot += math.Abs(b.Open - b.Close)
}
avgPrice := (b.High + b.Low + b.Close) / 3
quoteVol := avgPrice * b.Volume
longRt := longTot / total
longVol := quoteVol * longRt
shortVol := quoteVol - longVol
sta.longVol.Update(longVol)
sta.shortVol.Update(shortVol)
if b.IsWarmUp {
return
}
// 暂时不考虑多空直接比例
sta.longRate = longVol / sta.longVol.Val * sta.weight * math.Pow(longRt, 2)
sta.shortRate = shortVol / sta.shortVol.Val * sta.weight * math.Pow(1-longRt, 2)
sta.nexts = nexts
sta.price = b.Close
}
var replayErr *errs.Error
verCh := make(chan int, 1)
onData := func(evt *orm.DataSeries, nexts []*orm.DataSeries) {
bar, err_ := orm.AsKline(evt)
if err_ != nil {
replayErr = errs.New(errs.CodeRunTime, err_)
select {
case verCh <- -1:
default:
}
return
}
futureBars := make([]*orm.InfoKline, 0, len(nexts))
for _, next := range nexts {
futureBar, convErr := orm.AsKline(next)
if convErr != nil {
replayErr = errs.New(errs.CodeRunTime, convErr)
select {
case verCh <- -1:
default:
}
return
}
futureBars = append(futureBars, futureBar)
}
onBar(bar, futureBars)
}
err := biz.RunHistSeries(&biz.RunHistSeriesArgs{
ExsList: exsList,
Start: config.TimeRange.StartMS,
End: config.TimeRange.EndMS,
ViewNextNum: viewNextNum,
TfWarms: map[string]int{tf: 100},
VerCh: verCh,
OnData: onData,
})
if err == nil && replayErr != nil {
err = replayErr
}
outPath := filepath.Join(config.GetDataDir(), "dump", "moneyFlow.csv")
err2 := utils.WriteCsvFile(outPath, rows, false)
if err == nil {
err = err2
}
return err
}

31
ent/tools.go Normal file
View File

@@ -0,0 +1,31 @@
package ent
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/exg"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/utils"
)
func showBalance() *errs.Error {
var account string
for acc := range config.Accounts {
account = acc
break
}
exchange := exg.Default
res, err := exchange.FetchBalance(map[string]interface{}{
banexg.ParamAccount: account,
})
if err != nil {
return err
}
resstr, err_ := utils.MarshalString(res.Info)
if err_ != nil {
return errs.New(errs.CodeMarshalFail, err_)
}
fmt.Println(resstr)
return nil
}