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

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.idea
.omx
tmp
bot
code
*.exe
*.profile
*.db
*.gob
*.log
.noai

922
adv/pairs.go Normal file
View File

@@ -0,0 +1,922 @@
package adv
import (
"errors"
"flag"
"fmt"
"io"
"math"
"net/http"
"path/filepath"
"slices"
"sort"
"strings"
"time"
"gonum.org/v1/gonum/floats"
"go.uber.org/zap"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/entry"
"github.com/banbox/banbot/opt"
"github.com/banbox/banbot/orm"
"github.com/banbox/banbot/orm/ormo"
utils2 "github.com/banbox/banbot/utils"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"github.com/banbox/banexg/utils"
"github.com/spf13/cobra"
)
func init() {
runConfig := func(args *config.CmdArgs, run func(*config.CmdArgs) *errs.Error) error {
core.SetRunMode(core.RunModeOther)
args.Init()
return run(args)
}
bindCommon := func(command *cobra.Command, args *config.CmdArgs) {
flags := command.Flags()
flags.StringArrayVar((*[]string)(&args.Configs), "config", nil, "config path; may be repeated")
flags.StringVar(&args.ConfigData, "config-data", "", "inline YAML config")
flags.StringVar(&args.Logfile, "logfile", "", "log file path")
flags.StringVar(&args.DataDir, "datadir", "", "path to the data directory")
flags.BoolVar(&args.NoDefault, "no-default", false, "ignore config.yml and config.local.yml")
flags.StringVar(&args.LogLevel, "level", "info", "logging level")
flags.IntVar(&args.MaxPoolSize, "max-pool-size", 0, "maximum database pool size")
flags.BoolVar(&core.NetDisable, "net-off", false, "disable network requests")
}
newConfigCommand := func(name, help string, run func(*config.CmdArgs) *errs.Error,
bind func(*cobra.Command, *config.CmdArgs)) *cobra.Command {
args := &config.CmdArgs{}
command := &cobra.Command{
Use: name,
Short: help,
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
return runConfig(args, run)
},
}
bindCommon(command, args)
if bind != nil {
bind(command, args)
}
return command
}
dayRets := newConfigCommand("dayRets", "calc return rate for pairs by months", CalcPairDayPerfs,
func(command *cobra.Command, args *config.CmdArgs) {
command.Flags().StringVar(&args.InPath, "in", "", "input file or directory")
})
entry.AddCommand("tool", dayRets)
entry.AddCommand("tool", newConfigCommand("updMkt", "update coin markets by month", UpdateCoinMarkets, nil))
entry.AddCommand("tool", newConfigCommand("buildFac", "build factor samples", BuildFacSamples, nil))
entry.AddCommand("tool", &cobra.Command{
Use: "testFac",
Short: "test factors",
DisableFlagParsing: true,
RunE: func(_ *cobra.Command, args []string) error {
return TestFactors(args)
},
})
}
var urlCoinMarket = "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/listings/historical?convertId=2781&date=%s&limit=%d&start=%d"
var coinSlugMap = map[string]string{
"BTCDOM": "",
"1000XEC": "ecash",
"LUNA2": "terra-luna-v2",
"DODOX": "dodo",
"BEAMX": "onbeam",
"1000RATS": "",
"RONIN": "ronin",
"1000SHIB": "shiba-inu",
"1000LUNC": "terra-luna",
"1000FLOKI": "floki-inu",
"1000PEPE": "pepe",
"1000BONK": "bonk1",
}
type PairReturns struct {
StartMS int64
EndMS int64
Pairs map[string]*ItemReturns
}
type ItemReturns struct {
StartMS int64
EndMS int64
Arr []float64
}
type CoinMarket struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
ListMS int64 `json:"list_ms"`
Slug string `json:"slug"`
FirstMS int64 `json:"first_ms"`
LastMS int64 `json:"last_ms"`
MarketHis []*MarketSlice `json:"market_his"`
}
type MarketSlice struct {
TimeMS int64 `json:"time_ms"`
CirculatingSupply float64 `json:"circulating_supply"`
TotalSupply float64 `json:"total_supply"`
Price float64 `json:"price"`
MarketCap float64 `json:"market_cap"`
}
type CoinMarketHis struct {
Coins map[string]*CoinMarket
Times []int64
}
type CoinMarketResponse struct {
Data []struct {
ID int `json:"id"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Slug string `json:"slug"`
DateAdded string `json:"dateAdded"`
CirculatingSupply float64 `json:"circulatingSupply"`
TotalSupply float64 `json:"totalSupply"`
Quotes []struct {
Name string `json:"name"`
Price float64 `json:"price"`
MarketCap float64 `json:"marketCap"`
} `json:"quotes"`
} `json:"data"`
Status struct {
ErrorCode string `json:"error_code"`
ErrorMessage string `json:"error_message"`
} `json:"status"`
}
/*
某个品种在某个时间的历史收益,市场、未来回报等信息
*/
type PairSample struct {
Offset int // 值int是在ItemReturns.Arr的位置索引
Pair string
Code string
Price float64
MarketCap float64
Vols []float64
}
type FacHis struct {
Times []int64
Pairs map[string]*ItemReturns
Data map[int64]map[string]*PairSample // time: pair: sample
}
type FacState struct {
Ret *ItemReturns
Sample *PairSample
Groups map[string]string
Label string
Tmp float64 // 因子计算临时使用
OutScore float64 // 输出分数
OutID int // 输出分数排序序号
}
type GpState struct {
count float64 // 该块中FacState的数量
sumScore float64 // 该块的平均分数
}
/*
CalcPairDayPerfs 从订单计算每个品种的每日收益率
*/
func CalcPairDayPerfs(args *config.CmdArgs) *errs.Error {
err := biz.SetupComs(args)
if err != nil {
return err
}
orders, err := ormo.LoadOrdersGob(args.InPath)
if err != nil {
return err
}
pairOrders := make(map[string][]*ormo.InOutOrder)
minTimeMS, maxTimeMS := btime.TimeMS(), int64(0)
totalCost := float64(0)
for _, o := range orders {
odList, _ := pairOrders[o.Symbol]
pairOrders[o.Symbol] = append(odList, o)
minTimeMS = min(minTimeMS, o.Enter.CreateAt)
maxTimeMS = max(maxTimeMS, o.Exit.CreateAt)
totalCost += o.EnterCost() / o.Leverage
}
// 假设初始金额能同时开50笔
initStake := totalCost / float64(len(orders)) * 50
// 解析开始时间和结束时间,对齐到月的1号
startYear, startMon, _ := btime.MSToTime(minTimeMS).Date()
endYear, endMon, endDay := btime.MSToTime(maxTimeMS).Date()
endAddMon := 0
if endDay > 1 {
endAddMon = 1
}
startMS := time.Date(startYear, startMon, 1, 0, 0, 0, 0, time.UTC).UnixMilli()
endTime := time.Date(endYear, endMon, 1, 0, 0, 0, 0, time.UTC)
endMS := endTime.AddDate(0, endAddMon, 0).UnixMilli()
symbolMap := orm.GetExSymbolMap(core.ExgName, core.Market)
dayMSecs := int64(utils.TFToSecs("1d") * 1000)
retMap := make(map[string]*ItemReturns)
var addNum int
for pair, odList := range pairOrders {
sort.Slice(odList, func(i, j int) bool {
a, b := odList[i], odList[j]
return a.Enter.CreateAt < b.Enter.CreateAt
})
exs := symbolMap[pair]
_, bars, err := orm.GetOHLCV(exs, "1d", startMS, endMS, 0, false)
if err != nil {
return err
}
var closes []float64
if len(bars) > 0 {
bars, addNum = utils2.FillOHLCVLacks(bars, startMS, endMS, dayMSecs)
if addNum > 0 {
log.Info("fill ohlcv lack", zap.String("pair", pair), zap.Int("num", addNum))
}
closes = make([]float64, len(bars))
for i, b := range bars {
closes[i] = b.Close
}
}
// 计算每日回报率
returns, dayOff, dayEnd := ormo.CalcUnitReturns(odList, closes, startMS, endMS, dayMSecs)
retRates := make([]float64, len(returns))
for i, ret := range returns {
retRates[i] = ret / initStake
}
retMap[pair] = &ItemReturns{
StartMS: startMS + int64(dayOff)*dayMSecs,
EndMS: startMS + int64(dayEnd)*dayMSecs,
Arr: retRates[dayOff:dayEnd],
}
}
outPath := filepath.Join(filepath.Dir(args.InPath), "out.gob")
return utils2.EncodeGob(outPath, &PairReturns{
StartMS: startMS,
EndMS: endMS,
Pairs: retMap,
})
}
func loadHisMarkets() (*CoinMarketHis, string, *errs.Error) {
path := filepath.Join(config.GetDataDir(), "cryptoMarkets.gob")
var data = &CoinMarketHis{
Coins: make(map[string]*CoinMarket),
Times: make([]int64, 0),
}
err := utils2.DecodeGobFile(path, data)
if err != nil {
return data, path, err
}
return data, path, nil
}
/*
UpdateCoinMarkets 更新BanDataDir目录下的cryptoMarkets.gob全币种市场信息。每个月更新一次。
*/
func UpdateCoinMarkets(args *config.CmdArgs) *errs.Error {
err := biz.SetupComs(args)
if err != nil {
return err
}
data, path, err := loadHisMarkets()
if err != nil {
log.Warn("load cryptoMarkets error", zap.Error(err))
}
// 确定开始时间
startTime := time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC)
if len(data.Times) > 0 {
startTime = time.UnixMilli(data.Times[len(data.Times)-1]).UTC().AddDate(0, 1, 0)
}
lastTimeMS := startTime.AddDate(0, -1, 0).UnixMilli()
batchSize := 2000
isLast := false
// 循环获取每个月的数据
for {
if startTime.After(time.Now()) {
pyear, pmon, pday := time.Now().UTC().Date()
startTime = time.Date(pyear, pmon, pday, 0, 0, 0, 0, time.UTC).AddDate(0, 0, -1)
isLast = true
}
// 获取该月所有币种数据
monthData := make(map[string]*MarketSlice)
start := 1
fetchNum, fixLack := 0, 0
timeMS := startTime.UnixMilli()
for {
url := fmt.Sprintf(urlCoinMarket, startTime.Format("2006-01-02"), batchSize, start)
time.Sleep(time.Second * 1)
resp, err_ := http.Get(url)
log.Info("request", zap.String("url", url))
if err_ != nil {
return errs.New(errs.CodeNetFail, err_)
}
body, err_ := io.ReadAll(resp.Body)
resp.Body.Close()
if err_ != nil {
return errs.New(errs.CodeNetFail, err_)
}
var rsp CoinMarketResponse
if err_ = utils.Unmarshal(body, &rsp, utils.JsonNumAuto); err_ != nil {
return errs.New(errs.CodeUnmarshalFail, err_)
}
if rsp.Status.ErrorCode != "0" {
return errs.NewMsg(errs.CodeRunTime, "code: %v, msg: %v", rsp.Status.ErrorCode, rsp.Status.ErrorMessage)
}
if len(rsp.Data) == 0 {
break
}
for _, coin := range rsp.Data {
slice := &MarketSlice{
TimeMS: timeMS,
CirculatingSupply: coin.CirculatingSupply,
TotalSupply: coin.TotalSupply,
}
if len(coin.Quotes) > 0 {
slice.Price = coin.Quotes[0].Price
slice.MarketCap = coin.Quotes[0].MarketCap
}
monthData[coin.Slug] = slice
// 更新或创建币种信息
if cm, exists := data.Coins[coin.Slug]; exists {
if len(cm.MarketHis) == 0 || timeMS > cm.MarketHis[len(cm.MarketHis)-1].TimeMS {
// 检查是否有缺失的月份数据需要补充
if !isLast {
if cm.LastMS < lastTimeMS {
fixLack += 1
lastSlice := cm.MarketHis[len(cm.MarketHis)-1]
// 从上次更新的下个月开始,直到上个月为止,补充所有缺失的月份
curTime := time.UnixMilli(cm.LastMS).UTC().AddDate(0, 1, 0)
for curTime.UnixMilli() <= lastTimeMS {
newSlice := &MarketSlice{
TimeMS: curTime.UnixMilli(),
CirculatingSupply: lastSlice.CirculatingSupply,
TotalSupply: lastSlice.TotalSupply,
Price: lastSlice.Price,
MarketCap: lastSlice.MarketCap,
}
cm.MarketHis = append(cm.MarketHis, newSlice)
cm.LastMS = curTime.UnixMilli()
curTime = curTime.AddDate(0, 1, 0)
}
}
// 添加当前月份的数据
cm.MarketHis = append(cm.MarketHis, slice)
cm.LastMS = timeMS
}
if cm.Name != coin.Name {
cm.Name = coin.Name
}
if cm.Symbol != coin.Symbol {
cm.Symbol = coin.Symbol
}
fetchNum += 1
}
} else if !isLast {
listTime, _ := time.Parse("2006-01-02T15:04:05.000Z", coin.DateAdded)
data.Coins[coin.Slug] = &CoinMarket{
Name: coin.Name,
Symbol: coin.Symbol,
ListMS: listTime.UnixMilli(),
Slug: coin.Slug,
FirstMS: timeMS,
LastMS: timeMS,
MarketHis: []*MarketSlice{slice},
}
fetchNum += 1
}
}
// 这里可能返回数量不足batchSize个,但不代表结束了,继续请求返回为空才表示结束
start += batchSize
}
// 添加时间戳
data.Times = append(data.Times, timeMS)
// 保存数据
if err := utils2.EncodeGob(path, data); err != nil {
log.Error("save market data failed", zap.Error(err))
return err
} else {
log.Info("update coin markets ok", zap.String("date", startTime.Format("2006-01-02")),
zap.Int("fetch_num", fetchNum), zap.Int("fixLack", fixLack))
}
// 移动到下一个月
lastTimeMS = timeMS
startTime = startTime.AddDate(0, 1, 0)
if isLast {
break
}
}
return nil
}
/*
BuildFacSamples 构建因子定期采样历史
*/
func BuildFacSamples(args *config.CmdArgs) *errs.Error {
err := biz.SetupComs(args)
if err != nil {
return err
}
data, _, err := loadHisMarkets()
if err != nil {
return err
}
retData := &PairReturns{Pairs: make(map[string]*ItemReturns)}
retPath := "E:\\trade\\go\\bandata\\backtest\\383f7dc32a_all_pairs\\out.gob"
if !utils2.Exists(retPath) {
args.InPath = filepath.Join(filepath.Dir(retPath), "orders.gob")
err = CalcPairDayPerfs(args)
if err != nil {
return err
}
}
err = utils2.DecodeGobFile(retPath, retData)
if err != nil {
return err
}
if len(retData.Pairs) == 0 {
return errs.NewMsg(errs.CodeRunTime, "no Pairs for PairReturns")
}
res := &FacHis{Pairs: retData.Pairs, Data: make(map[int64]map[string]*PairSample)}
startMS := retData.StartMS
// 构建Symbol到CoinMarket的映射
var b strings.Builder
symbolToCoin := make(map[string]*CoinMarket)
badCoins := make(map[string]bool)
skipCoins := make(map[string]bool)
dupCoins := make(map[string][]string)
for _, coin := range data.Coins {
if old, ok := symbolToCoin[coin.Symbol]; ok {
dups, _ := dupCoins[coin.Symbol]
if len(dups) == 0 {
dups = append(dups, old.Slug)
}
dupCoins[coin.Symbol] = append(dups, coin.Slug)
continue
}
symbolToCoin[coin.Symbol] = coin
b.WriteString(coin.Symbol)
b.WriteString(", ")
}
symbolMap := orm.GetExSymbolMap(core.ExgName, core.Market)
log.Info("all coins", zap.String("v", b.String()))
if len(dupCoins) > 0 {
log.Info("dupCoins", zap.String("v", utils2.MapToStr(dupCoins, true, 0)))
}
// 设置月份间隔(这里设置为3个月)
monNum := 2
// 将startMS转换为月初
startTime := time.UnixMilli(startMS).UTC()
startTime = time.Date(startTime.Year(), startTime.Month(), 1, 0, 0, 0, 0, time.UTC)
startTime = startTime.AddDate(0, monNum, 0)
endTime := time.UnixMilli(retData.EndMS).UTC()
// 从startTime开始遍历时间点
for curTime := startTime; !curTime.After(endTime); curTime = curTime.AddDate(0, monNum, 0) {
timeMS := curTime.UnixMilli()
timeMap := make(map[string]*PairSample)
prevTime := curTime.AddDate(0, -monNum, 0)
nextTime := curTime.AddDate(0, monNum, 0)
// 对每个pair的returns进行检查
for pair, itemRet := range retData.Pairs {
// 检查时间范围是否符合要求
if itemRet.StartMS > prevTime.UnixMilli() || itemRet.EndMS < nextTime.UnixMilli() {
continue
}
// 计算当前时间点在Arr中的索引
offset := int((timeMS - itemRet.StartMS) / (24 * 3600 * 1000)) // 转换为天数
if offset < 0 || offset >= len(itemRet.Arr) {
continue
}
// 查找对应的CoinMarket
baseCode, _, _, _ := core.SplitSymbol(pair)
coin, ok := symbolToCoin[baseCode]
if !ok {
if slug, ok := coinSlugMap[baseCode]; ok {
if slug == "" {
skipCoins[baseCode] = true
continue
}
coin = data.Coins[slug]
} else {
badCoins[baseCode] = true
continue
}
}
// 查找当前时间点对应的MarketSlice
var mktSlice *MarketSlice
for _, slice := range coin.MarketHis {
if slice.TimeMS <= timeMS {
mktSlice = slice
} else {
break
}
}
if mktSlice == nil {
// 太新,跳过
continue
}
// 获取成交量数据
exs := symbolMap[pair]
if exs == nil {
return errs.NewMsg(errs.CodeRunTime, "ExSymbol not exist: %v", pair)
}
// 计算2周和1个月的成交量
barStartMS := prevTime.UnixMilli()
_, bars, err := orm.GetOHLCV(exs, "1d", barStartMS, timeMS, 0, false)
if err != nil {
log.Warn("get volume failed", zap.String("pair", pair), zap.Error(err))
continue
}
// 计算成交量
vols := make([]float64, len(bars))
for i, bar := range bars {
vols[i] = bar.Close * bar.Volume
}
// 构建PairSample
sample := &PairSample{
Offset: offset,
Pair: pair,
Code: baseCode,
Price: mktSlice.Price,
MarketCap: mktSlice.MarketCap,
Vols: vols,
}
timeMap[pair] = sample
}
if len(timeMap) > 0 {
res.Data[timeMS] = timeMap
res.Times = append(res.Times, timeMS)
}
}
if len(badCoins) > 0 {
log.Warn("unknown coins", zap.String("v", utils2.MapToStr(badCoins, false, 0)))
}
if len(skipCoins) > 0 {
log.Warn("skip coins", zap.String("v", utils2.MapToStr(skipCoins, false, 0)))
}
// 保存结果
name := fmt.Sprintf("facHis_%dM.gob", monNum)
outPath := filepath.Join(config.GetDataDir(), name)
return utils2.EncodeGob(outPath, res)
}
func TestFactors(args []string) error {
parser := flag.NewFlagSet("", flag.ExitOnError)
var monNum, outNum, offNum int
var facStr, outBy string
parser.IntVar(&monNum, "mon", 2, "month for period")
parser.IntVar(&outNum, "out", 3, "output group num")
parser.IntVar(&offNum, "off", 0, "skip group num in head")
parser.StringVar(&facStr, "fac", "", "factors to test")
parser.StringVar(&outBy, "outBy", "", "calc output score by")
err_ := parser.Parse(args)
if err_ != nil {
return err_
}
facArr := utils2.SplitSolid(facStr, ",", true)
if len(facArr) == 0 {
return errors.New("-fac is required")
}
dayNum := monNum * 30
fac := &FacHis{Pairs: make(map[string]*ItemReturns), Data: make(map[int64]map[string]*PairSample)}
facPath := filepath.Join(config.GetDataDir(), fmt.Sprintf("facHis_%dM.gob", monNum))
err := utils2.DecodeGobFile(facPath, fac)
if err != nil {
return err
}
groupMeas := make(map[string][]*GpState)
for i, stampMS := range fac.Times {
if i <= offNum {
continue
}
pairSamples := fac.Data[stampMS]
items := make([]*FacState, 0)
for pair, smp := range pairSamples {
ret := fac.Pairs[pair]
items = append(items, &FacState{
Ret: ret,
Sample: smp,
Groups: make(map[string]string),
})
}
// 计算每个采样所属的因子分组
for _, factor := range facArr {
var pos []int
var tags []string
if factor == "marketCap" {
pos, tags = facMarketCap(items)
} else if factor == "vol1M" {
// 优于vol2w和marketCap
pos, tags = facVol1M(items)
} else if factor == "volStd" {
// 成交量波动较低的
pos, tags = facVolStd(items)
} else if factor == "mont1" {
// 3M下,跟mont2不相上下
pos, tags = facMont1(items, dayNum)
} else if factor == "mont2" {
pos, tags = facMont2(items, dayNum)
} else if factor == "mont1y" {
pos, tags = facMont1Y(items, dayNum)
} else if factor == "sharpe1y" {
pos, tags = facSharpe1Y(items, dayNum)
} else if factor == "score1y" {
pos, tags = facScore1Y(items, dayNum)
} else {
return errors.New("unknown factor: " + factor)
}
if len(pos)+1 != len(tags) {
return errors.New("invalid factor: " + factor)
}
start := 0
for i, end := range pos {
tag := tags[i]
for j := start; j < end; j++ {
items[j].Groups[factor] = tag
}
start = end
}
tag := tags[len(tags)-1]
for j := start; j < len(items); j++ {
items[j].Groups[factor] = tag
}
}
// 组合因子分组
groups := make(map[string][]*FacState)
for _, sta := range items {
var b strings.Builder
for _, f := range facArr {
b.WriteString(sta.Groups[f] + "+")
}
sta.Label = strings.TrimRight(b.String(), "+")
gpItems, _ := groups[sta.Label]
groups[sta.Label] = append(gpItems, sta)
end := sta.Sample.Offset + dayNum
if end > len(sta.Ret.Arr) {
end = len(sta.Ret.Arr)
}
// 计算未来分数,并进行分组打分
futures := sta.Ret.Arr[sta.Sample.Offset:end]
if outBy == "profit" {
sta.OutScore = floats.Sum(futures)
} else if outBy == "sharpe" {
sta.OutScore, err_ = utils2.SharpeRatioBy(futures, 0.02, 365, true)
if err_ != nil {
return err_
}
} else if outBy == "smart" || outBy == "" {
sharpe, err_ := utils2.SharpeRatioBy(futures, 0.02, 365, true)
if err_ != nil {
return err_
}
profit := floats.Sum(futures)
sta.OutScore = math.Log2(profit) * sharpe
} else {
return errors.New("unknown outBy: " + outBy)
}
}
// 按输出分数降序
sort.Slice(items, func(i, j int) bool {
return items[i].OutScore < items[j].OutScore
})
for i, it := range items {
it.OutID = i
}
// 计算每个目标块应包含的FacState数量
blockNum := int(math.Ceil(float64(len(items)) / float64(outNum)))
for key, gpList := range groups {
// 遍历分组中的每个FacState,根据其OutID确定所属块
blocks, ok := groupMeas[key]
if !ok {
blocks = make([]*GpState, outNum)
for i := 0; i < outNum; i++ {
blocks[i] = &GpState{}
}
groupMeas[key] = blocks
}
for _, state := range gpList {
blockID := state.OutID / blockNum // 确定目标块ID
if blockID >= outNum {
blockID = outNum - 1 // 处理边界情况
}
// 更新该块的统计信息
block := blocks[blockID]
block.count++
block.sumScore += utils2.NanInfTo(state.OutScore, 0)
}
}
}
countDsList := make([]*opt.ChartDs, len(groupMeas)) // 用于绘制数量分布图
scoreDsList := make([]*opt.ChartDs, len(groupMeas)) // 用于绘制平均分数图
// 生成x轴标签
xLabels := make([]string, outNum)
for i := 0; i < outNum; i++ {
xLabels[i] = fmt.Sprintf("Block%d", i+1)
}
keys := utils2.KeysOfMap(groupMeas)
slices.Sort(keys)
for i, key := range keys {
counts := make([]float64, outNum)
for j := 0; j < outNum; j++ {
counts[j] = groupMeas[key][j].count
}
countDsList[i] = &opt.ChartDs{
Label: key,
Data: counts,
}
scores := make([]float64, outNum)
for j := 0; j < outNum; j++ {
bk := groupMeas[key][j]
scores[j] = bk.sumScore / bk.count
}
scoreDsList[i] = &opt.ChartDs{
Label: key,
Data: scores,
}
}
facNames := strings.Join(facArr, "+")
// 生成数量分布折线图
countPath := filepath.Join(config.GetDataDir(), fmt.Sprintf("fac_count_%dM.html", monNum))
err = opt.DumpChart(countPath, "Distribution: "+facNames, xLabels, 5, nil, countDsList)
if err != nil {
return err
}
// 生成平均分数折线图
scorePath := filepath.Join(config.GetDataDir(), fmt.Sprintf("fac_score_%dM.html", monNum))
err = opt.DumpChart(scorePath, "Average Scores: "+facNames, xLabels, 5, nil, scoreDsList)
if err != nil {
return err
}
log.Info(fmt.Sprintf("generate in BanDataDir: fac_score_%dM.html fac_count_%dM.html", monNum, monNum))
return nil
}
func facMarketCap(items []*FacState) ([]int, []string) {
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Sample.MarketCap - a.Sample.MarketCap) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}
func facVol1M(items []*FacState) ([]int, []string) {
for _, it := range items {
start := max(0, len(it.Sample.Vols)-30)
it.Tmp = floats.Sum(it.Sample.Vols[start:])
}
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Tmp - a.Tmp) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}
func facVolStd(items []*FacState) ([]int, []string) {
for _, it := range items {
arr := it.Sample.Vols
data := make([]float64, 0, len(arr))
for i, v := range arr[1:] {
data = append(data, v/arr[i])
}
it.Tmp = utils2.StdDevVolatility(data, 1)
}
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Tmp - a.Tmp) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}
func facMont1(items []*FacState, unitDays int) ([]int, []string) {
for _, it := range items {
start := max(0, it.Sample.Offset-unitDays)
it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset])
}
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Tmp - a.Tmp) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}
func facMont2(items []*FacState, unitDays int) ([]int, []string) {
for _, it := range items {
start := max(0, it.Sample.Offset-unitDays*2)
it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset])
}
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Tmp - a.Tmp) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}
func facMont1Y(items []*FacState, unitDays int) ([]int, []string) {
for _, it := range items {
start := max(0, it.Sample.Offset-unitDays*6)
it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset])
}
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Tmp - a.Tmp) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}
func facSharpe1Y(items []*FacState, unitDays int) ([]int, []string) {
for _, it := range items {
start := max(0, it.Sample.Offset-unitDays*6)
arr := it.Ret.Arr[start:it.Sample.Offset]
sharpe, err := utils2.SharpeRatioBy(arr, 0.02, 365, true)
if err != nil {
log.Info("calc sharpe fail", zap.Error(err))
}
it.Tmp = sharpe
}
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Tmp - a.Tmp) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}
func facScore1Y(items []*FacState, unitDays int) ([]int, []string) {
for _, it := range items {
start := max(0, it.Sample.Offset-unitDays*6)
arr := it.Ret.Arr[start:it.Sample.Offset]
sharpe, err_ := utils2.SharpeRatioBy(arr, 0.02, 365, true)
if err_ != nil {
log.Error("calc sharpe fail", zap.Error(err_))
}
profit := floats.Sum(arr)
it.Tmp = math.Log2(profit) * sharpe
}
slices.SortFunc(items, func(a, b *FacState) int {
return int((b.Tmp - a.Tmp) / 1000)
})
totalNum := float64(len(items))
bigNum := int(totalNum * 0.3)
midEnd := int(totalNum*0.4) + bigNum
return []int{bigNum, midEnd}, []string{"big", "mid", "sml"}
}

159
arb/common.go Normal file
View File

@@ -0,0 +1,159 @@
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
}

52
arb/stat_group.go Normal file
View File

@@ -0,0 +1,52 @@
package arb
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/orm"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
"sort"
)
func StatGroupPairs() *errs.Error {
exgId := "binance"
endMS := config.TimeRange.EndMS
// 生成待订阅标的列表
exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear)
limit := 720
exsList, barList, _, err := getExsBars(exg.Default, exsMap, "1h", 0, endMS, limit)
if err != nil {
return err
}
arr := make([]*core.StrFloat, 0, len(exsList))
for i, exs := range exsList {
var sumVal float64
bars := barList[i]
if float64(len(bars)) < float64(limit)*0.9 {
continue
}
for _, b := range bars {
sumVal += b.Volume * b.Close
}
arr = append(arr, &core.StrFloat{
Str: exs.Symbol,
Val: sumVal,
})
}
// 按成交量降序,取前30%
sort.Slice(arr, func(i, j int) bool {
return arr[i].Val > arr[j].Val
})
cutNum := int(float64(len(arr)) * 0.2)
cutPairs := make([]string, 0, cutNum)
for i := 0; i < cutNum; i++ {
base, _, _, _ := core.SplitSymbol(arr[i].Str)
cutPairs = append(cutPairs, base)
}
log.Info("cut 20% of pairs", zap.Int("num", cutNum), zap.Int("total", len(arr)), zap.Strings("items", cutPairs))
return nil
}

271
arb/stat_job.go Normal file
View File

@@ -0,0 +1,271 @@
package arb
import (
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"github.com/banbox/banexg/utils"
ta "github.com/banbox/banta"
"go.uber.org/zap"
"maps"
"math"
"math/rand"
"path/filepath"
)
func init() {
strat.AddStratGroup("arb", map[string]strat.FuncMakeStrat{
"gp": ArbGroupJob,
})
}
/*
strengths widRatios
rate: 0 -0.002491 0.859934
rate: 14234 -0.000915 0.946673
rate: 28468 -0.000089 0.997275
rate: 42702 0.000818 1.048731
rate: 56936 0.002571 1.166192
rate: 71170 0.007989 1.619902
*/
func ArbGroupJob(pol *config.RunPolicyConfig) *strat.TradeStrat {
lenFast := 6
lenSlow := 20
lenMid := 96
lenStd := 96
bollWid := 0.0043
lastSplitMS := int64(0)
tfMSecs := int64(60000)
dayMS := int64(utils.TFToSecs("1d") * 1000)
env, _ := ta.NewBarEnv(core.ExgName, core.Market, "", "1m")
var rate *ta.Series
groups := make(map[*strat.StratJob]float64)
hits := make(map[string]float64)
warmNum := 120
var widths []float64
var maxRate float64
var position float64
var posTag string
var rates []*TimeFlt
dumpDir := filepath.Join(config.GetDataDir(), "dump")
return &strat.TradeStrat{
WarmupNum: warmNum,
RunTimeFrames: []string{"1m"},
BatchInOut: true,
OnBar: func(s *strat.StratJob) {
},
OnBatchJobs: func(jobs []*strat.StratJob) {
var curMS int64
newNum := 0
hitDup := maps.Clone(hits)
for _, j := range jobs {
curMS = max(curMS, j.Env.TimeStop)
if _, ok := hitDup[j.Symbol.Symbol]; ok {
delete(hitDup, j.Symbol.Symbol)
} else {
newNum += 1
}
}
alignDayMS := utils.AlignTfMSecs(curMS, dayMS)
if alignDayMS > lastSplitMS || newNum > 0 || len(hitDup) > 0 {
// 每天刷新品种分组
for j := range groups {
j.CloseOrders(&strat.ExitReq{Tag: "switch", Log: true})
}
offsetMS := alignDayMS - dayMS
dateStr := btime.ToDateStr(offsetMS, "20060102")
dumpPath := filepath.Join(dumpDir, dateStr+".html")
if len(rates) > 1000 {
log.Info("try dump rates", zap.String("date", dateStr), zap.Int("num", len(rates)))
err := dumpRateChart(rates, "1m", dumpPath, dateStr)
if err != nil {
log.Error("dumpRateChart fail", zap.Error(err))
}
}
rates = nil
rand.Shuffle(len(jobs), func(i, j int) {
jobs[i], jobs[j] = jobs[j], jobs[i]
})
env.Reset()
rate = env.NewSeries(nil)
groups = make(map[*strat.StratJob]float64)
hits = make(map[string]float64)
for i, j := range jobs {
if i%2 == 0 {
groups[j] = -1
} else {
groups[j] = 1
}
hits[j.Symbol.Symbol] = j.Env.Close.Get(lenMid)
}
env.TimeStop = curMS - int64(lenMid)*tfMSecs
env.TimeStart = env.TimeStop - tfMSecs
for i := warmNum; i >= 0; i-- {
var price1, price2 float64
var num1, num2 float64
for j, tag := range groups {
base := hits[j.Symbol.Symbol]
ratec := j.Env.Close.Get(i) / base
if tag < 0 {
price1 += ratec
num1 += 1
} else {
price2 += ratec
num2 += 1
}
}
price1 /= num1
price2 /= num2
env.TimeStart += tfMSecs
env.TimeStop += tfMSecs
rate.Append(price1 / price2)
_ = ta.EMA(rate, lenFast)
_ = ta.EMA(rate, lenSlow)
maMid := ta.EMA(rate, lenMid).Get(0)
dev := ta.StdDev(rate, lenStd)
stdDev := ta.SMA(dev, lenStd).Get(0) * 1.7
bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5
widths = append(widths, bollWidAvg)
}
lastSplitMS = alignDayMS
}
var price1, price2 float64
var num1, num2 float64
for j, tag := range groups {
base := hits[j.Symbol.Symbol]
ratec := j.Env.Close.Get(0) / base
if tag < 0 {
price1 += ratec
num1 += 1
} else {
price2 += ratec
num2 += 1
}
}
price1 /= num1
price2 /= num2
curRate := price1 / price2
env.TimeStart += tfMSecs
env.TimeStop += tfMSecs
rate.Append(curRate)
rates = append(rates, &TimeFlt{
Stamp: curMS,
Val: curRate,
})
maFast := ta.EMA(rate, lenFast)
maSlow := ta.EMA(rate, lenSlow) //66
maMidS := ta.EMA(rate, lenMid)
maMid := maMidS.Get(0)
dev := ta.StdDev(rate, lenStd)
stdDev := ta.SMA(dev, lenStd).Get(0) * 1.7
bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5
widths = append(widths, bollWidAvg)
zScore := (curRate - maMid) / bollWidAvg
strength := maMid/maMidS.Get(warmNum) - 1
widDelta := bollWidAvg - widths[len(widths)-60]
widRatio := bollWidAvg / widths[len(widths)-warmNum]
fastXSlow := maFast.Cross(maSlow)
fastXMid := maFast.Cross(maMidS)
absStrength := math.Abs(strength)
mkt := ""
if absStrength > 0.01 || math.Abs(zScore) > 4 {
// D: 结构性跳变,暂停交易
mkt = "D"
} else if widRatio > 1.5 && widDelta > 0 || bollWidAvg >= 0.008 {
// C: 高波动突破
mkt = "C"
} else if absStrength >= 0.003 || widRatio > 1.3 && widDelta > 0 {
// B: 趋势市场
mkt = "B"
} else if absStrength < 0.002 && widRatio < 1.2 && widDelta <= 0 {
// A: 低波动区间
mkt = "A"
} else {
//if absStrength >= 0.001 && absStrength < 0.002 && widRatio < 1.3
// E: 低频弱趋势,中性,混合策略
mkt = "E"
}
dirt := -1.0
if strength > 0 {
dirt = 1
}
if math.Abs(zScore) > maxRate*dirt {
maxRate = zScore
}
allow1, allow2 := false, false
if mkt == "A" {
allow1 = true
} else if mkt == "B" || mkt == "C" {
// B明显趋势 或 C波动扩大方向明确
allow2 = true
} else if mkt == "E" {
allow1, allow2 = true, true
}
openDirt := 0.0
openTag := ""
dateStr := btime.ToDateStr(curMS, core.DefaultDateFmt)
if allow1 {
expPft := math.Abs(curRate - maMid)
if math.Abs(maxRate) >= 1.5 && expPft > 0.002 && float64(fastXSlow)*dirt == -1 {
openDirt = -dirt
openTag = "rev"
log.Info("open rev", zap.String("date", dateStr), zap.Float64("z", zScore),
zap.Float64("width", bollWidAvg), zap.Float64("widDelta", widDelta))
}
}
if allow2 && float64(fastXMid)*dirt == 1 {
if absStrength >= 0.003 || widRatio > 1.3 && widDelta > 0 {
openDirt = dirt
openTag = "follow"
log.Info("open trend", zap.String("date", dateStr), zap.Float64("z", zScore),
zap.Float64("width", bollWidAvg), zap.Float64("widDelta", widDelta),
zap.Float64("strength", strength), zap.Float64("widRatio", widRatio))
}
}
if position != openDirt && openDirt != 0 {
if position != 0 {
log.Info("close pos", zap.String("date", dateStr), zap.String("tag", "reverse"), zap.Float64("dirt", position))
for j := range groups {
j.CloseOrders(&strat.ExitReq{Tag: "reverse"})
}
}
for j, tag := range groups {
j.OpenOrder(&strat.EnterReq{
Tag: openTag,
Short: tag != openDirt,
})
}
log.Info("open pos", zap.String("date", dateStr), zap.String("tag", openTag), zap.Float64("dirt", openDirt))
position = openDirt
posTag = openTag
} else if position != 0 {
exitTag := ""
if posTag == "rev" {
if position*dirt > 0 {
exitTag = "tpRev"
} else if math.Abs(zScore) >= 3 {
exitTag = "slRev"
}
} else if posTag == "follow" {
if position*dirt < 0 && (math.Abs(zScore) > 0.5 || widDelta < 0) {
exitTag = "slFol"
} else if position*dirt > 0 && math.Abs(zScore) >= 1 {
exitTag = "tpFol"
}
}
if exitTag != "" {
log.Info("close pos", zap.String("date", dateStr), zap.String("tag", exitTag), zap.Float64("dirt", position))
position = 0
posTag = ""
for j := range groups {
j.CloseOrders(&strat.ExitReq{Tag: exitTag})
}
}
}
},
}
}

373
arb/stat_mean.go Normal file
View File

@@ -0,0 +1,373 @@
package arb
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"github.com/banbox/banta"
"go.uber.org/zap"
"math"
"sort"
"strconv"
"sync"
)
/*
统计套利:对两个交易对的价格比率下注
价格比率较稳定的:
快速穿越大趋势跟随开单
超出上下轨,回弹交叉开单
价格比率明显单边趋势:
在标准差30%以内交叉,跟随大趋势方向开单
强劲单边跟随:
平时价格比率小幅上下波动,但偶尔有强劲单边价格剧烈运动:GLM/USDT:USDT, XEM/USDT:USDT 20241001
瞬时极端波动:
某段时间,价格比率快速上下极端波动,GLM/USDT:USDT, XEM/USDT:USDT 20241025
*/
func MeanRegressStat(p *config.RunPolicyConfig) *strat.TradeStrat {
var sta *StatTwo
var jobs = make(map[string]*strat.StratJob)
return &strat.TradeStrat{
WarmupNum: 1000,
BatchInOut: true,
OnStartUp: func(s *strat.StratJob) {
jobs[s.Symbol.Symbol] = s
},
OnBar: func(s *strat.StratJob) {
if sta == nil {
sta = newStatTwo(jobs, nil)
}
e := s.Env
if e.TimeStart > sta.barMS {
sta.barMS = e.TimeStart
}
sta.OnPrice(s.Symbol.Symbol, e.Close.Get(0))
},
OnBatchJobs: func(jobs []*strat.StratJob) {
sta.onStampDone()
},
}
}
const (
slopeDist = 150
maSlopeDist = 500
)
type StatTwo struct {
barMS int64 // 收到最新bar的时间戳
prices map[string]float64 // 当前时间戳下,所有品种的价格
jobs map[string]*strat.StratJob // 所有的任务
cacheSize int // Tuple中保留历史数据的最小长度
lock sync.Mutex
Tuples map[string]map[string]*Tuple
}
type Tuple struct {
A *strat.StratJob
B *strat.StratJob
env *banta.BarEnv
raw *banta.Series
maFast *banta.Series
maSlow *banta.Series
maBig *banta.Series
rawSbBig *banta.Series // sma(raw-maBig, 500)
faXSl int // fast slow交叉:1快线上穿慢线;-1快线下穿慢线
rawXBig int // raw big交叉
maSlope float64 // ma斜率(和500个之前)
slope float64 // 价格比率斜率(和150个之前)
sort float64 // 排序分数,降序
stdDev float64 // 标准差
allowType int // 允许开单类型: 1突破布林带回弹;2快速穿越大趋势线;3中心交叉跟随趋势
allowDirt int // 允许开单方向: 1做多A做空B; -1做空A做多B
openType int // 开单类型:从allowType赋值
openRate float64 // 开单的价格比率,0表示未开单
openAt int64 // 开单的时间戳
}
func newStatTwo(jobs map[string]*strat.StratJob, tuples [][]string) *StatTwo {
tupleMap := make(map[string]map[string]*Tuple)
res := &StatTwo{
cacheSize: 2000,
jobs: jobs,
Tuples: tupleMap,
}
if len(tuples) == 0 {
codes := make([]string, 0, len(jobs))
for key := range jobs {
codes = append(codes, key)
}
for i := 0; i < len(codes); i++ {
for j := i + 1; j < len(codes); j++ {
tuples = append(tuples, []string{codes[i], codes[j]})
}
}
}
if len(tuples) > 7000 {
log.Warn("too many tuples may cause OOM!", zap.Int("num", len(tuples)))
}
for _, va := range tuples {
a, b := va[0], va[1]
if a > b {
a, b = b, a
}
subMap, ok := tupleMap[a]
if !ok {
subMap = make(map[string]*Tuple)
tupleMap[a] = subMap
}
tup, ok := subMap[b]
if !ok {
baseA, _, _, _ := core.SplitSymbol(a)
baseB, _, _, _ := core.SplitSymbol(b)
symbol := fmt.Sprintf("%s/%s", baseA, baseB)
env, _ := banta.NewBarEnv(core.ExgName, core.Market, symbol, "1m")
env.MaxCache = res.cacheSize
raw := env.NewSeries(nil)
A, _ := jobs[a]
B, _ := jobs[b]
tup = &Tuple{
A: A,
B: B,
raw: raw,
env: env,
}
subMap[b] = tup
}
}
return res
}
func (s *StatTwo) OnPrice(symbol string, price float64) {
s.lock.Lock()
s.prices[symbol] = price
s.lock.Unlock()
}
func (s *StatTwo) popTuples() []*Tuple {
s.lock.Lock()
tasks := make(map[string]map[string]bool)
for codeA := range s.prices {
subMap := make(map[string]bool)
for codeB := range s.prices {
if codeA >= codeB {
continue
}
subMap[codeB] = true
}
tasks[codeA] = subMap
}
var res = make([]*Tuple, 0, len(s.prices)*2)
for codeA, subMap := range tasks {
for codeB := range subMap {
ratio := s.prices[codeA] / s.prices[codeB]
t := s.tuple(codeA, codeB, ratio)
if t == nil {
continue
}
res = append(res, t)
}
}
s.prices = make(map[string]float64)
s.lock.Unlock()
return res
}
func (s *StatTwo) onStampDone() {
tuples := s.popTuples()
// 筛选两种开单类型
size := len(tuples) / 3
list1 := make([]*Tuple, 0, size)
list2 := make([]*Tuple, 0, size)
list3 := make([]*Tuple, 0, size)
for _, t := range tuples {
t.calcInOuts()
if t.allowType == 1 {
list1 = append(list1, t)
} else if t.allowType == 2 {
list2 = append(list2, t)
} else if t.allowType == 3 {
list3 = append(list3, t)
}
}
var list = list1
if len(list) == 0 {
if len(list2) == 0 {
if len(list3) == 0 {
return
}
list = list3
} else {
list = list2
}
}
// 按分数降序,取前三个中没有开单的
var it *Tuple
sort.Slice(list, func(i, j int) bool {
return list[i].sort > list[j].sort
})
gap := len(list) / 2
list = append(list[gap:], list[:gap]...)
for i, v := range list {
if v.openType == 0 && v.A.OrderNum == 0 && v.B.OrderNum == 0 {
it = v
break
}
if i == 2 {
break
}
}
if it == nil {
return
}
// 下单
shortA := it.allowDirt < 0
errA := it.A.OpenOrder(&strat.EnterReq{
Tag: strconv.Itoa(it.allowType),
Short: shortA,
})
if errA == nil {
errB := it.B.OpenOrder(&strat.EnterReq{
Tag: strconv.Itoa(it.allowType),
Short: !shortA,
})
if errB != nil {
it.A.Entrys = nil
} else {
it.openType = it.allowType
it.openRate = it.raw.Get(0)
it.openAt = it.env.TimeStop
}
}
//dateStr := btime.ToDateStr(s.barMS, core.DefaultDateFmt)
//fmt.Printf("%s %s - %s type=%v sort=%.3f slope=%.3f bigSlope=%.3f\n", dateStr, it.A.Symbol.Symbol,
// it.B.Symbol.Symbol, it.allowType, it.sort, it.slope, it.maSlope)
}
func (s *StatTwo) tuple(a, b string, ratio float64) *Tuple {
if a > b {
a, b = b, a
ratio = 1 / ratio
}
subMap, ok := s.Tuples[a]
if !ok {
return nil
}
res, ok := subMap[b]
if ok {
res.update(s.barMS, ratio)
return res
}
return nil
}
func (t *Tuple) update(stamp int64, ratio float64) {
t.env.TimeStart = stamp
t.env.TimeStop = stamp + t.env.TFMSecs
t.env.BarNum += 1
t.raw.Append(ratio)
t.maFast = banta.EMA(t.raw, 24)
t.maSlow = banta.EMA(t.raw, 66)
t.maBig = banta.EMA(t.raw, 1333)
t.rawSbBig = banta.SMA(t.raw.Sub(t.maBig), 500)
t.faXSl = t.maFast.Cross(t.maSlow)
t.rawXBig = t.raw.Cross(t.maBig)
sdev := banta.StdDev(t.raw, 1333)
t.stdDev = banta.SMA(sdev, 800).Get(0) * 1.7
if t.raw.Len() > slopeDist {
// 计算近期斜率,优先用基于历史150偏移计算
// 当基于均线历史150偏移的绝对变动区间更小时,基于均线计算
old := t.raw.Get(slopeDist)
oldMa := t.maBig.Get(slopeDist)
slope := ratio/old - 1
slopeMa := ratio/oldMa - 1
if math.Abs(slopeMa) < math.Abs(slope) {
slope = slopeMa
}
t.slope = slope
}
if t.maBig.Len() > maSlopeDist {
t.allowType = 0
t.sort = 0
t.maSlope = t.maBig.Get(0)/t.maBig.Get(maSlopeDist) - 1
}
}
func (t *Tuple) calcInOuts() {
if t.maBig.Len() < maSlopeDist {
return
}
//fastVal, fastVal30 := t.maFast.Get(0), t.maFast.Get(30)
//if t.rawXBig == -1 || t.rawXBig == 1 {
// // 快速穿越大趋势,开单
// fastSlope := math.Abs(fastVal/fastVal30 - 1)
// distRate := t.rawSbBig.Get(0) / stdDev
// if fastSlope > 0.01 && distRate*float64(t.rawXBig) < 0 && math.Abs(distRate) > 0.7 {
// // 要求穿过大趋势线时,具备较高的斜率,且过去500个平均与大周期的差超过70%标准差
// t.allowType = 2
// t.allowDirt = t.rawXBig
// t.sort = math.Abs(distRate)
// }
//}
maSlopeAbs := math.Abs(t.maSlope)
//diff := math.Abs(t.slope - t.maSlope)
//if maSlopeAbs < min(math.Abs(t.slope), 0.02) && diff > 0.015 {
// // 突破布林带上下轨,回弹时开单
// bigVal := t.maBig.Get(0)
// slowPrev := t.maSlow.Get(60)
// // 按maSlope进行惩罚,极端0.02时削减64%
// sortV := diff * math.Pow(1-maSlopeAbs*20, 2)
// if t.faXSl == -1 && max(fastVal, fastVal30) > bigVal+stdDev {
// // 均线上方,突破上界,快线下穿慢线
// if fastVal > slowPrev {
// t.allowType = 1
// t.allowDirt = t.faXSl
// t.sort = sortV
// }
// } else if t.faXSl == 1 && min(fastVal, fastVal30) < bigVal-stdDev {
// // 均线下方,突破下界,快线上穿慢线
// if fastVal < slowPrev {
// t.allowType = 1
// t.allowDirt = t.faXSl
// t.sort = sortV
// }
// }
//}
nearToCenter := math.Abs(t.raw.Get(0)-t.maBig.Get(0)) < t.stdDev*0.3
if maSlopeAbs > 0.02 && nearToCenter && (t.faXSl == 1 || t.faXSl == -1) && t.maSlope*float64(t.faXSl) > 0 {
// 明显大趋势,在中心附近交叉,按大趋势方向开单
t.allowType = 3
t.allowDirt = t.faXSl
t.sort = maSlopeAbs
}
closeOd := false
if t.openType == 2 {
// 根据穿越大趋势开单:回穿大趋势平仓、快线慢线交叉平仓
if t.faXSl*t.allowDirt == -1 || t.rawXBig*t.allowDirt == -1 {
closeOd = true
}
} else if t.openType == 1 && t.faXSl*t.allowDirt == -1 {
// 根据突破上下轨回弹开单,(盈利达到标准差30% || 持有时间超过700个bar):快线慢线交叉平仓
pft30 := (t.raw.Get(0)-t.openRate)*float64(t.allowDirt) > t.stdDev*0.3
//costBarNum := int((t.env.TimeStop - t.openAt) / t.env.TFMSecs)
if pft30 { //|| costBarNum > 1000 {
closeOd = true
}
} else if t.openType == 3 && t.faXSl*t.allowDirt == -1 {
// 中心附近交叉,跟随趋势的,再次交叉时平仓
closeOd = true
}
if closeOd {
t.A.CloseOrders(&strat.ExitReq{Tag: "exit"})
t.B.CloseOrders(&strat.ExitReq{Tag: "exit"})
t.openType = 0
t.openRate = 0
t.openAt = 0
}
}

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

203
arb/stat_showTwo.go Normal file
View File

@@ -0,0 +1,203 @@
package arb
import (
_ "embed"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/opt"
"github.com/banbox/banbot/orm"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/utils"
"github.com/banbox/banta"
"math"
"strings"
)
func GenTwoPairRatio() *errs.Error {
//LineExamples()
//return nil
//groups := []string{"1000PEPE/USDT:USDT", "1000BONK/USDT:USDT"}
groups := []string{
"LPT,ATH,MOODENG,RNDR,APT,TRUMP,DOT,HYPE,BAKE,SPX,BOME,TUT,PUMP,BERA,OG,EOS,MERL,TAO,NMR,FET,TIA,FHE,ZORA,LINK,POL,TRX,1000PEPE,ETHFI,PROVE,TREE",
"RED,SUI,VIRTUAL,NEIRO,GALA,TON,PAXG,XNY,CRV,NAORIS,XRP,NEAR,AVAX,ETC,XLM,MATIC,AAVE,SEI,1000SHIB,DAM,SPK,MEME,OP,FIL,KAITO,FARTCOIN,EIGEN,BCH,IP,XPL",
}
//startMS := config.TimeRange.StartMS
endMS, _ := btime.ParseTimeMS("20250921")
tf := "15m"
core.Market = "linear"
tfMSecs := int64(utils.TFToSecs(tf) * 1000)
barLimit := 30000
startMS := endMS - tfMSecs*int64(barLimit)
barsArr := make([][]*TimeFlt, 0, len(groups))
allPairs := strings.Split(strings.Join(groups, ","), ",")
allPairs, err := config.ParsePairs(allPairs...)
if err != nil {
return err
}
exsMap := make(map[int32]*orm.ExSymbol)
for _, pair := range allPairs {
exs, err := orm.GetExSymbolCur(pair)
if err != nil {
return err
}
exsMap[exs.ID] = exs
}
err = orm.BulkDownOHLCV(exg.Default, exsMap, tf, startMS, endMS, 0, nil)
if err != nil {
return err
}
for _, group := range groups {
pairs := strings.Split(group, ",")
prices, err := GetGroupPrices(pairs, tf, startMS, endMS)
if err != nil {
return err
}
barsArr = append(barsArr, prices)
}
rateArr := make([]*TimeFlt, len(barsArr[0]))
for i, va := range barsArr[0] {
rateArr[i] = &TimeFlt{
Stamp: va.Stamp,
Val: va.Val / barsArr[1][i].Val,
}
}
path := "D:/ratios.html"
title := "price ratio"
return dumpRateChart(rateArr, tf, path, title)
}
// GetGroupPrices 计算分组内多个品种的平均价格
func GetGroupPrices(pairs []string, tf string, startMS, endMS int64) ([]*TimeFlt, *errs.Error) {
var groupBar [][]*banexg.Kline
pairs, err := config.ParsePairs(pairs...)
if err != nil {
return nil, err
}
tfMSecs := int64(utils.TFToSecs(tf) * 1000)
barLimit := (endMS - startMS) / tfMSecs
for _, pair := range pairs {
exs, err := orm.GetExSymbolCur(pair)
if err != nil {
return nil, err
}
_, bars, err := orm.GetOHLCV(exs, tf, startMS, endMS, 0, false)
if err != nil {
return nil, err
}
if float64(len(bars)) > float64(barLimit)*0.8 {
groupBar = append(groupBar, bars)
}
}
// 对分组内品种计算价格归一化均值
groupBar = getAlignBars(groupBar...)
if len(groupBar) == 0 {
return nil, errs.NewMsg(errs.CodeParamInvalid, "align bars fail for %v", pairs)
}
for _, gp := range groupBar {
base := gp[0].Close
gp[0].Close = 1
for i := 1; i < len(gp); i++ {
gp[i].Close /= base
}
}
barNum := len(groupBar[0])
bars := make([]*TimeFlt, 0, barNum)
for j := 0; j < barNum; j++ {
var sumVal float64
for _, gp := range groupBar {
sumVal += gp[j].Close
}
bars = append(bars, &TimeFlt{
Stamp: groupBar[0][j].Time,
Val: sumVal / float64(len(groupBar)),
})
}
return bars, nil
}
type TimeFlt struct {
Stamp int64
Val float64
}
func dumpRateChart(rates []*TimeFlt, tf string, path, title string) *errs.Error {
if len(rates) == 0 {
return nil
}
var labels []string
var arrRaw []float64
var arrFast []float64
var arrSlow []float64
var arrMdMa []float64
var arrBigMa []float64
var arrBigUp []float64
var arrBigDn []float64
var arrHH, arrLL []float64
bollWid := 0.0043
showNum := 1440
stepFlt := float64(len(rates)) / float64(showNum)
stepSize := int(math.Round(stepFlt))
tfMSecs := int64(utils.TFToSecs(tf) * 1000)
lenFast := int(stepFlt * 5.77)
lenSlow := int(stepFlt * 31.25)
lenMid := int(stepFlt * 96.5)
lenBig := int(stepFlt * 320.2)
lenStd := int(stepFlt * 96.3)
lenHL := int(stepFlt * 14.42)
var idx = 0
env, _ := banta.NewBarEnv(core.ExgName, core.Market, "", tf)
env.TimeStart = rates[idx].Stamp
env.TimeStop = rates[idx].Stamp + tfMSecs
rate := env.NewSeries(nil)
rowId := 0
dateFmt := "01-02 15:04"
for idx < len(rates) {
item := rates[idx]
env.TimeStart += tfMSecs
env.TimeStop += tfMSecs
env.BarNum += 1
rowId += 1
ratio := item.Val
rate.Append(ratio)
maFast := banta.EMA(rate, lenFast)
maSlow := banta.EMA(rate, lenSlow) //66
maMid := banta.EMA(rate, lenMid).Get(0)
maBig := banta.EMA(rate, lenBig)
dev := banta.StdDev(rate, lenStd)
stdDev := banta.SMA(dev, lenStd).Get(0) * 1.7
bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5
hh := banta.Highest(rate, lenHL).Get(0)
ll := banta.Lowest(rate, lenHL).Get(0)
bigVal := maBig.Get(0)
if rowId%stepSize == 0 && !math.IsNaN(maMid) {
labels = append(labels, btime.ToDateStr(item.Stamp, dateFmt))
arrRaw = append(arrRaw, ratio)
arrFast = append(arrFast, maFast.Get(0))
arrSlow = append(arrSlow, maSlow.Get(0))
arrMdMa = append(arrMdMa, maMid)
arrBigMa = append(arrBigMa, bigVal)
arrBigUp = append(arrBigUp, maMid+bollWidAvg)
arrBigDn = append(arrBigDn, maMid-bollWidAvg)
arrHH = append(arrHH, hh)
arrLL = append(arrLL, ll)
}
idx += 1
}
return opt.DumpChart(path, title, labels, 5, nil, []*opt.ChartDs{
{Label: "raw", Data: arrRaw},
{Label: "ma5", Data: arrFast, Hidden: true},
{Label: "ma10", Data: arrSlow, Hidden: true},
{Label: "mid", Data: arrMdMa},
{Label: "big", Data: arrBigMa, Hidden: true},
{Label: "upp", Data: arrBigUp, Hidden: false},
{Label: "dnn", Data: arrBigDn, Hidden: false},
//{Label: "hh", Data: arrHH, Hidden: true},
//{Label: "ll", Data: arrLL, Hidden: true},
})
}

31
arb/stat_spike.go Normal file
View File

@@ -0,0 +1,31 @@
package arb
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banexg/errs"
)
/*
捕捉价格比率的瞬时剧烈波动
*/
func FindTupleSpikes() *errs.Error {
tf := "1m"
endMS := config.TimeRange.EndMS
items, err := findSpikeTuples(endMS, tf, 2000, 5, 5)
if err != nil {
return err
}
fmt.Println("tuple,score,slope,pftRate,rawXBig")
for _, t := range items {
codeA, _, _, _ := core.SplitSymbol(t.A.Symbol)
codeB, _, _, _ := core.SplitSymbol(t.B.Symbol)
if codeA > codeB {
codeA, codeB = codeB, codeA
}
fmt.Printf("%s/%s,%.2f,%.4f,%.3f,%v\n", codeA, codeB, t.Score, t.BigSlope, t.PftRate, t.RawXBig)
}
return nil
}

391
arb/stat_test.go Normal file
View File

@@ -0,0 +1,391 @@
package arb
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/data"
"github.com/banbox/banbot/exg"
"github.com/banbox/banbot/goods"
"github.com/banbox/banbot/orm"
utils2 "github.com/banbox/banbot/utils"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"github.com/banbox/banexg/utils"
"github.com/sasha-s/go-deadlock"
"go.uber.org/zap"
"math/rand"
"path/filepath"
"sort"
"strings"
"testing"
"time"
)
func TestPairPick(t *testing.T) {
all := "ETH,BTC,SOL,DOGE,XRP,WLFI,MYX,ENA,BNB,1000PEPE,LINK,SUI,AVAX,ADA,PUMP,WLD,SOMI,HYPE,BIO,FARTCOIN,NMR,M,PENGU,PYTH,AAVE,LTC,HIFI,TA,WIF,ARB,RNDR,TRUMP,1000BONK,BCH,DOT,UNI,IP,BAKE,MATIC,FIL,EOS,TRX,NEAR,ONDO,XPL,1000SHIB,DOLO,PROMPT,CRV,OP,MEME,ETHFI,HBAR,XLM,ETC,TIA,LDO,SEI,TREE,EIGEN,TAO,XNY,SPK,APT,DAM,W,NAORIS,INJ,SAPIEN,JELLYJELLY,MOODENG,VIRTUAL,RED,TST,QTUM,MERL,TON,F,TUT,PNUT,LPT,SOLV,PENDLE,CFX,POL,KAITO,BERA,ZORA,GALA,FHE,PAXG,ONT,JUP,OG,FET,PUMPBTC,SKL,GAL,ATOM,NEIRO,BOME,MAV,PROVE,ATH,SPX"
pickA, pickB := 30, 30
coins := strings.Split(all, ",")
rand.Shuffle(len(coins), func(i, j int) { coins[i], coins[j] = coins[j], coins[i] })
groupA := coins[:pickA]
groupB := coins[pickA : pickA+pickB]
fmt.Println(strings.Join(groupA, ","))
fmt.Println(strings.Join(groupB, ","))
}
// 从高频逐笔数据中提取每隔5秒的价格
func TestExtractPrices(t *testing.T) {
core.SetRunMode(core.RunModeBackTest)
err := biz.SetupComsExg(&config.CmdArgs{})
if err != nil {
panic(err)
}
loader, err := data.NewWsDataLoader()
if err != nil {
panic(err)
}
dataDir := config.GetDataDir()
outPath := filepath.Join(dataDir, "dump", "group1.gob")
group1 := "LPT,ATH,MOODENG,RNDR,APT,TRUMP,DOT,HYPE,BAKE,SPX,BOME,TUT,PUMP,BERA,OG,EOS,MERL,TAO,NMR,FET,TIA,FHE,ZORA,LINK,POL,TRX,1000PEPE,ETHFI,PROVE,TREE"
//group2 := "RED,SUI,VIRTUAL,NEIRO,GALA,TON,PAXG,XNY,CRV,NAORIS,XRP,NEAR,AVAX,ETC,XLM,MATIC,AAVE,SEI,1000SHIB,DAM,SPK,MEME,OP,FIL,KAITO,FARTCOIN,EIGEN,BCH,IP,XPL"
pairs := strings.Split(group1, ",")
pairs, err = config.ParsePairs(pairs...)
if err != nil {
panic(err)
}
result := make(map[string][]float64)
if utils2.Exists(outPath) {
err = utils2.DecodeGobFile(outPath, &result)
if err != nil {
panic(err)
}
cache := make(map[string]int)
num := 0
for k, v := range result {
cache[k] = len(v)
if len(v) > 0 {
num += 1
}
}
log.Info("load cache", zap.Int("num", num), zap.Any("keys", cache))
}
lock := deadlock.Mutex{}
year, mon, day := time.Now().Add(-time.Hour * 24).Date()
endTime := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC)
year, mon, day = endTime.AddDate(0, 0, -15).Date()
startTime := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC)
gapMS := int64(5000) // 采样间隔ms
startMS := startTime.UnixMilli()
endMS := endTime.UnixMilli()
size := int((endMS - startMS) / gapMS)
pBar := utils2.NewPrgBar(len(pairs)*size, "run")
defer pBar.Close()
err = utils2.ParallelRun(pairs, 4, func(i int, pair string) *errs.Error {
lock.Lock()
arr, ok := result[pair]
lock.Unlock()
if ok && len(arr) > 0 {
pBar.Add(size)
return nil
}
arr = make([]float64, size+10)
lastIdx := 0
saveNum := 0
curTime := startTime
success := true
for curTime.Before(endTime) {
trades, err := loader.LoadTrades(&data.WsSymbol{
ExgId: "binance",
Market: banexg.MarketLinear,
WsType: core.WsSubTrade,
Symbol: pair,
Date: curTime.Format(core.DateFmt),
Hour: curTime.Hour(),
})
if err != nil {
log.Error("down trades fail", zap.Error(err))
success = false
break
}
curTime = curTime.Add(time.Hour)
for _, td := range trades {
curIdx := int((td.Timestamp - startMS) / gapMS)
if curIdx > lastIdx {
prevVal := arr[lastIdx]
for lastIdx+1 < curIdx {
lastIdx += 1
arr[lastIdx] = prevVal
}
lastIdx = curIdx
arr[curIdx] = td.Price
}
}
if lastIdx > saveNum {
pBar.Add(lastIdx - saveNum)
saveNum = lastIdx
}
}
if success {
lock.Lock()
result[pair] = arr
lock.Unlock()
}
return nil
})
err = utils2.EncodeGob(outPath, result)
if err != nil {
panic(err)
}
}
// 测试随机两组品种的平均价格比率(逐秒级别)
func TestDayBatchRates(t *testing.T) {
// 读取两组30个交易品种的每隔5s的价格序列,计算平均价比率,研究套利空间
tf := "5s"
dataDir := config.GetDataDir()
outPath := filepath.Join(dataDir, "dump", "group1.gob")
group1 := make(map[string][]float64)
err := utils2.DecodeGobFile(outPath, &group1)
if err != nil {
panic(err)
}
outPath = filepath.Join(dataDir, "dump", "group2.gob")
group2 := make(map[string][]float64)
err = utils2.DecodeGobFile(outPath, &group2)
if err != nil {
panic(err)
}
tfSecs := utils.TFToSecs(tf)
daySize := utils.TFToSecs("1d") / tfSecs
dataLen := 0
for _, arr := range group1 {
dataLen = len(arr)
break
}
tfMSecs := int64(tfSecs * 1000)
year, mon, day := time.Now().Add(-time.Hour * 24).Date()
endMS := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC).UnixMilli()
startMS := endMS - int64(dataLen)*tfMSecs
dumpDir := filepath.Join(config.GetDataDir(), "dump")
for offset := 0; offset+daySize < dataLen; offset += daySize {
offsetMS := startMS + int64(offset)*tfMSecs
batchA := normInRange(group1, offset, offset+daySize, offsetMS, tfMSecs)
batchB := normInRange(group2, offset, offset+daySize, offsetMS, tfMSecs)
rateArr := make([]*TimeFlt, len(batchA))
for i, va := range batchA {
rateArr[i] = &TimeFlt{
Stamp: va.Stamp,
Val: va.Val / batchB[i].Val,
}
}
dateStr := btime.ToDateStr(offsetMS, "20060102")
dumpPath := filepath.Join(dumpDir, dateStr+".html")
log.Info("dump rate chart", zap.String("title", dateStr))
err = dumpRateChart(rateArr, tf, dumpPath, dateStr)
if err != nil {
panic(err)
}
}
}
func normInRange(dataMap map[string][]float64, start, end int, offsetMS, stepMS int64) []*TimeFlt {
batchA := make(map[string][]float64)
minLen := -1
// 对每个品种的价格序列进行归一化
for key, arr := range dataMap {
vals := arr[start:end]
res := make([]float64, len(vals))
first := float64(0)
for i, v := range vals {
if v > 0 {
if first > 0 {
res[i] = v / first
} else {
res[i] = 1
first = v
}
}
}
batchA[key] = res
if minLen < 0 {
minLen = len(res)
} else {
minLen = min(minLen, len(res))
}
}
// 直接对所有品种价格序列取平均值
res := make([]*TimeFlt, minLen)
for i := 0; i < minLen; i++ {
sumVal := float64(0)
validNum := 0
for _, arr := range batchA {
val := arr[i]
if val > 0 {
sumVal += val
validNum += 1
}
}
item := &TimeFlt{
Stamp: offsetMS + int64(i)*stepMS,
}
if validNum > 0 {
item.Val = sumVal / float64(validNum)
}
res[i] = item
}
return res
}
/*
每7日取成交量最高top60
随机分为两组计算平均价格比率
获取7日成交量倒序前30%的品种列表
每天计算:
计算最近14天1h的相关性矩阵
按相关性从高到低取top60
奇数位和偶数位分为两组
对比了按相关性从高到低取top60和随机分配top60,差别不大,简单起见,使用随机方法
*/
func TestDailyGroupArb(t *testing.T) {
core.SetRunMode(core.RunModeBackTest)
err := biz.SetupComsExg(&config.CmdArgs{})
if err != nil {
panic(err)
}
groupSize := 30
dayTf := "1d"
dayMSecs := int64(utils.TFToSecs("1d") * 1000)
year, mon, day := time.Now().Add(-time.Hour * 24).Date()
endMS := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC).UnixMilli()
startMS := endMS - dayMSecs*28
minListMS := endMS - dayMSecs*180
curMS := startMS
exsMap := orm.GetExSymbols(core.ExgName, core.Market)
pairs := make([]string, 0, len(exsMap))
for _, exs := range exsMap {
if exs.ListMs > minListMS {
// 忽略上市不久的品种
continue
}
pairs = append(pairs, exs.Symbol)
}
err = orm.BulkDownOHLCV(exg.Default, exsMap, "1h", startMS, endMS, 0, nil)
if err != nil {
panic(err)
}
var pre30Pairs []string
var pre30Ms int64
dumpDir := filepath.Join(config.GetDataDir(), "dump")
for curMS < endMS {
if len(pre30Pairs) == 0 || (curMS-pre30Ms)/dayMSecs >= 7 {
// 获取14日成交量倒序前30%的品种列表
pre30Pairs = nil
symbolVols, err := goods.GetSymbolVols(pairs, dayTf, 14, curMS, false)
if err != nil {
panic(err)
}
sort.Slice(symbolVols, func(i, j int) bool {
return symbolVols[i].Vol > symbolVols[j].Vol
})
cutNum := int(float64(len(symbolVols)) * 0.23)
for i := 0; i < cutNum; i++ {
pre30Pairs = append(pre30Pairs, symbolVols[i].Symbol)
}
pre30Ms = curMS
dateStr := btime.ToDateStr(curMS, core.DateFmt)
log.Info("cut pre 30% pairs by vols", zap.Int("num", len(pre30Pairs)), zap.String("date", dateStr))
}
// 按成交量取前面的,然后随机挑选。
coins := pre30Pairs[:groupSize*2]
rand.Shuffle(len(coins), func(i, j int) { coins[i], coins[j] = coins[j], coins[i] })
var pairsA, pairsB []string
downMap := make(map[int32]*orm.ExSymbol)
for i := 0; i < groupSize*2; i++ {
pair := coins[i]
if i%2 == 0 {
pairsA = append(pairsA, pair)
} else {
pairsB = append(pairsB, pair)
}
exs, _ := orm.GetExSymbolCur(pair)
downMap[exs.ID] = exs
}
// 对A和B两组计算1m加权价格
curEndMS := curMS + dayMSecs
err = orm.BulkDownOHLCV(exg.Default, downMap, "1m", curMS, curEndMS, 0, nil)
if err != nil {
panic(err)
}
pricesA, err := GetGroupPrices(pairsA, "1m", curMS, curEndMS)
if err != nil {
panic(err)
}
pricesB, err := GetGroupPrices(pairsB, "1m", curMS, curEndMS)
if err != nil {
panic(err)
}
rateArr := make([]*TimeFlt, len(pricesA))
for i, va := range pricesA {
rateArr[i] = &TimeFlt{
Stamp: va.Stamp,
Val: va.Val / pricesB[i].Val,
}
}
dateStr := btime.ToDateStr(curMS, "20060102")
dumpPath := filepath.Join(dumpDir, dateStr+"_r2.html")
log.Info("dump rate chart", zap.String("title", dateStr))
err = dumpRateChart(rateArr, "1m", dumpPath, dateStr)
if err != nil {
panic(err)
}
curMS += dayMSecs
}
}
func getPairCorrs(curMS int64, pairs []string) []string {
// 计算最近14天1h的相关性矩阵
dayMSecs := int64(utils.TFToSecs("1d") * 1000)
hourMSecs := int64(utils.TFToSecs("1h") * 1000)
pre14MS := curMS - dayMSecs*14
names := make([]string, 0, len(pairs))
dataArr := make([][]float64, 0, len(pairs))
klineNum := int((curMS - pre14MS) / hourMSecs)
err := orm.FastBulkOHLCV(exg.Default, pairs, "1h", pre14MS, curMS, 0, func(s string, s2 string, klines []*banexg.Kline, infos []*orm.AdjInfo) {
if len(klines) == klineNum {
prices := make([]float64, 0, len(klines))
for _, b := range klines {
prices = append(prices, b.Close)
}
names = append(names, s)
dataArr = append(dataArr, prices[:klineNum])
}
})
if err != nil {
panic(err)
}
_, avgs, err_ := utils2.CalcCorrMat(klineNum, dataArr, true)
if err_ != nil {
panic(err_)
}
// 基于平均相似度降序
arr := make([]*goods.IdVal, 0, len(avgs))
lefts := make(map[int]bool)
for i, avg := range avgs {
arr = append(arr, &goods.IdVal{Id: i, Val: avg})
lefts[i] = true
}
sort.Slice(arr, func(i, j int) bool {
return arr[i].Val < arr[j].Val
})
var resPairs []string
for _, v := range arr {
resPairs = append(resPairs, names[v.Id])
}
return resPairs
}

91
com/common.go Normal file
View File

@@ -0,0 +1,91 @@
package com
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/utils"
ta "github.com/banbox/banta"
"os"
"path/filepath"
"slices"
)
const (
MaSMA = "sma"
MaEMA = "ema"
MaRMA = "rma"
MaWMA = "wma"
MaVWMA = "vwma"
MaHMA = "hma"
MaKAMA = "kama"
MaALMA = "alma"
)
var MATypes = []string{MaSMA, MaEMA, MaRMA, MaWMA, MaVWMA, MaHMA, MaKAMA, MaALMA}
var maMap = make(map[string]bool)
func init() {
for _, name := range MATypes {
maMap[name] = true
}
}
func DefMaType(p *config.RunPolicyConfig, key, defVal string, allows ...string) string {
if len(allows) == 0 {
allows = MATypes
} else {
for _, k := range allows {
if _, ok := maMap[k]; !ok {
panic(fmt.Sprintf("unsupport maType: %s", k))
}
}
}
if _, ok := maMap[defVal]; !ok {
panic(fmt.Sprintf("unsupport maType: %s", defVal))
}
defValInt := slices.Index(allows, defVal)
if defValInt < 0 {
defValInt = 0
allows = append([]string{defVal}, allows...)
}
pVal := p.DefInt(key, defValInt, core.PUniform(0, float64(len(allows)-1)))
return allows[pVal]
}
func GetMA(price, vol *ta.Series, maType string, maLen int) *ta.Series {
switch maType {
case MaSMA:
return ta.SMA(price, maLen)
case MaEMA:
return ta.EMA(price, maLen)
case MaRMA:
return ta.RMA(price, maLen)
case MaWMA:
return ta.WMA(price, maLen)
case MaVWMA:
return ta.VWMA(price, vol, maLen)
case MaHMA:
return ta.HMA(price, maLen)
case MaKAMA:
return ta.KAMA(price, maLen)
case MaALMA:
return ta.ALMA(price, maLen, 6, 0.85)
default:
panic(fmt.Errorf("unknown maType: " + maType))
}
}
func GetTmpPath(name string) (string, error) {
path, err := os.Getwd()
if err != nil {
return "", err
}
tmpDir := filepath.Join(path, "tmp")
err = utils.EnsureDir(tmpDir, 0644)
if err != nil {
return "", err
}
return filepath.Join(tmpDir, name), nil
}

38
com/cross.go Normal file
View File

@@ -0,0 +1,38 @@
package com
import "math"
func NewCrossLog() *CrossLog {
return &CrossLog{}
}
type CrossLog struct {
PrevVal float64
Sign int // 最近一次交叉符号:1上穿,-1下穿
XAt int // 最近一次交叉序号
Num int // 当前序号
}
func (l *CrossLog) Update(sub float64) {
l.Num += 1
if sub == 0 {
return
}
if math.IsNaN(l.PrevVal) || l.PrevVal == 0 {
l.PrevVal = sub
} else {
if factor := l.PrevVal * sub; factor < 0 {
l.PrevVal = sub
l.XAt = l.Num
if sub > 0 {
l.Sign = 1
} else {
l.Sign = -1
}
}
}
}
func (l *CrossLog) Get() int {
return l.Sign * (l.Num - l.XAt + 1)
}

44
com/eff_trail_stop.go Normal file
View File

@@ -0,0 +1,44 @@
package com
import (
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/utils"
"math"
)
type EFTRS struct {
SLLong float64
SLShort float64
EFLen int
}
func NewEFTRS(efLen int) *EFTRS {
return &EFTRS{EFLen: efLen}
}
func (t *EFTRS) Calc(s *strat.StratJob, atr float64) (float64, float64) {
e := s.Env
c, c1 := e.Close.Get(0), e.Close.Get(1)
totSum := utils.ArrSum(e.Close.Sub(e.Close.Back(1)).Abs().Range(0, t.EFLen))
effRate := math.Abs(c-e.Close.Get(t.EFLen)) / totSum
slLong := e.Low.Get(0) - atr
slShort := e.High.Get(0) + atr
if len(s.LongOrders) > 0 {
if c > c1 {
slLong = t.SLLong + effRate*(c-c1)
} else {
slLong = t.SLLong
}
}
if len(s.ShortOrders) > 0 {
if c < c1 {
slShort = t.SLShort - effRate*(c1-c)
} else {
slShort = t.SLShort
}
}
t.SLLong = slLong
t.SLShort = slShort
return slLong, slShort
}

28
com/ema.go Normal file
View File

@@ -0,0 +1,28 @@
package com
import "math"
type Ema struct {
Alpha float64
Val float64
}
func NewEMA(alpha float64) *Ema {
return &Ema{Alpha: alpha}
}
func (e *Ema) Update(val float64) float64 {
if math.IsNaN(val) || math.IsInf(val, 0) {
return math.NaN()
}
if e.Val == 0 {
e.Val = val
} else {
e.Val = e.Val*(1-e.Alpha) + val*e.Alpha
}
return e.Val
}
func (e *Ema) Reset() {
e.Val = 0
}

135
com/exm.go Normal file
View File

@@ -0,0 +1,135 @@
package com
import (
"math"
"slices"
)
type ExmItem struct {
BarNum int
TimeMS int64
Value float64
Valid bool
}
type ExmTrace struct {
Hist []*ExmItem
Expect int // 1寻找极大值,-1寻找极小值
PrevVals []float64 // 前一个值
PrevDist float64 // 前两个极值的距离
distList []float64 // 前面极值距离,保留最近3个
}
/*
Update
val: 当前指标值
pNum: 前一个索引
ptime:前一个时间戳
*/
func (m *ExmTrace) Update(val float64, pNum int, ptime int64) bool {
if math.IsNaN(val) {
return false
}
if len(m.PrevVals) < 2 {
// 至少积累2个
if len(m.PrevVals) > 0 && m.PrevVals[len(m.PrevVals)-1] == val {
// 和前一个值相同,跳过
return false
}
m.PrevVals = append(m.PrevVals, val)
return false
}
// 前1个,前2个
p1, p2 := m.PrevVals[1], m.PrevVals[0]
if p1 == val {
// 当前值无效
return false
}
// 更新
m.PrevVals = append(m.PrevVals, val)
m.PrevVals = m.PrevVals[len(m.PrevVals)-2:]
pSub := p1 - p2
sub := p1 - val
var last *ExmItem
if len(m.Hist) > 0 {
last = m.Hist[len(m.Hist)-1]
}
getMinDist := func() float64 {
if last == nil {
return math.Abs(p1) * 0.3
}
if m.PrevDist != 0 {
return m.PrevDist * 0.3
}
return max(math.Abs(last.Value), math.Abs(p1)) * 0.3
}
if pSub*sub <= 0 {
// 非极大值或极小值
if last != nil && !last.Valid && (val-last.Value)*float64(m.Expect) > getMinDist() {
// 检查最后一个是否有效
last.Valid = true
if len(m.Hist) > 1 {
dist := math.Abs(m.Hist[len(m.Hist)-2].Value - m.Hist[len(m.Hist)-1].Value)
m.distList = append(m.distList, dist)
if len(m.distList) > 3 {
m.distList = m.distList[len(m.distList)-3:]
}
m.PrevDist = slices.Max(m.distList)
}
return true
}
return false
}
valid := 0
if sub > 0 {
if m.Expect == 0 || m.Expect == 1 {
// 新增极大值
if last == nil || p1-last.Value >= getMinDist() {
valid = -1
}
} else if p1 > last.Value {
// 更新极大值
valid = -1
}
} else if sub < 0 {
if m.Expect == 0 || m.Expect == -1 {
// 新增极小值
if last == nil || last.Value-p1 >= getMinDist() {
valid = 1
}
} else if p1 < last.Value {
// 更新极小值
valid = 1
}
}
if valid != 0 {
exm := &ExmItem{
TimeMS: ptime,
BarNum: pNum,
Value: p1,
}
if m.Expect != valid {
m.Hist = append(m.Hist, exm)
//log.Info("add exm", zap.String("date", btime.ToDateStr(ptime, "")),
// zap.Float64("val", p1))
} else {
m.Hist[len(m.Hist)-1] = exm
//log.Info("update exm", zap.String("date", btime.ToDateStr(ptime, "")),
// zap.Float64("val", p1))
}
if len(m.Hist) > 2 {
dist := math.Abs(m.Hist[len(m.Hist)-3].Value - m.Hist[len(m.Hist)-2].Value)
if len(m.distList) == 0 || dist != m.distList[len(m.distList)-1] {
m.distList = append(m.distList, dist)
if len(m.distList) > 3 {
m.distList = m.distList[len(m.distList)-3:]
}
m.PrevDist = slices.Max(m.distList)
}
}
m.Expect = valid
m.PrevVals = nil
return true
}
return false
}

41
com/inds.go Normal file
View File

@@ -0,0 +1,41 @@
package com
import "github.com/banbox/banta"
/*
IchimokuCloud
sml: 9, mid: 26, big: 52, disp: 26
return [smlMa, midMa, spanA, spanB]
*/
func IchimokuCloud(e *banta.BarEnv, sml, mid, big, disp int) *banta.Series {
res := e.Close.To("_ichimokuCld", big*100000+mid*1000+disp*10+sml)
if res.Cached() {
return res
}
// earlier
h := e.High.Back(disp - 1)
l := e.Low.Back(disp - 1)
hhSml := banta.Highest(h, sml).Get(0)
llSml := banta.Lowest(l, sml).Get(0)
hhMid := banta.Highest(h, mid).Get(0)
llMid := banta.Lowest(l, mid).Get(0)
hhBig := banta.Highest(h, big).Get(0)
llBig := banta.Lowest(l, big).Get(0)
smlMa := (hhSml + llSml) / 2
midMa := (hhMid + llMid) / 2
bigMa := (hhBig + llBig) / 2
spanA := (smlMa + midMa) / 2
// Current
h, l = e.High, e.Low
hhSml = banta.Highest(h, sml).Get(0)
llSml = banta.Lowest(l, sml).Get(0)
hhMid = banta.Highest(h, mid).Get(0)
llMid = banta.Lowest(l, mid).Get(0)
smlMa2 := (hhSml + llSml) / 2
midMa2 := (hhMid + llMid) / 2
return res.Append([]float64{smlMa2, midMa2, spanA, bigMa})
}

65
com/slop.go Normal file
View File

@@ -0,0 +1,65 @@
package com
import (
"math"
"slices"
)
type Slop struct {
lastCalc int
sampleNum int
Dirt int // 大周期斜率方向:1向上,0不明确,-1向下
rate float64 // 斜率阈值,推荐0.25
Data []float64
Top float64
Btm float64
}
func NewSlop(sampleNum int, rate float64) *Slop {
return &Slop{
Dirt: 0,
sampleNum: sampleNum,
rate: rate,
Top: math.NaN(),
Btm: math.NaN(),
}
}
func (s *Slop) Update(barNum int, slop float64) {
s.Data = append(s.Data, math.Abs(slop))
if len(s.Data) < s.sampleNum {
return
}
if !math.IsNaN(s.Top) {
s.Dirt = 0
if slop >= s.Top {
s.Dirt = 1
} else if slop <= s.Btm {
s.Dirt = -1
}
}
if barNum-s.lastCalc < s.sampleNum/2 {
return
}
if len(s.Data) > s.sampleNum {
s.Data = s.Data[len(s.Data)-s.sampleNum:]
}
thres := CutScore(s.Data, s.rate)
s.Top = thres
s.Btm = -thres
s.lastCalc = barNum
s.Dirt = 0
if slop >= s.Top {
s.Dirt = 1
} else if slop <= s.Btm {
s.Dirt = -1
}
}
func CutScore(vals []float64, rate float64) float64 {
arr := make([]float64, len(vals))
copy(arr, vals)
slices.Sort(arr)
idx := int(float64(len(vals)) * rate)
return arr[idx]
}

85
com/trailing_stop.go Normal file
View File

@@ -0,0 +1,85 @@
package com
import (
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
type TRS struct {
MaxLow float64
MinHigh float64
SLLong float64 // 止损的tr倍率,有持仓时,逐渐减小
SLShort float64 // 止损的tr倍率,有持仓时,逐渐减小
InitSLTR float64
minSLTR float64
stepSLTR float64
}
func NewTRS(initSLTR, minRate, costStep float64) *TRS {
var minSLTR = initSLTR * minRate
var stepSLTR = (initSLTR - minSLTR) / costStep
return &TRS{
InitSLTR: initSLTR,
minSLTR: minSLTR,
stepSLTR: stepSLTR,
}
}
func (m *TRS) TrialLong(price float64) {
m.SLLong = m.InitSLTR
m.MaxLow = price
}
func (m *TRS) TrialShort(price float64) {
m.SLShort = m.InitSLTR
m.MinHigh = price
}
func (m *TRS) setTRS(e *ta.BarEnv, longNum, shortNum int) {
h, l := e.High.Get(0), e.Low.Get(0)
if longNum == 0 {
if m.SLLong != m.InitSLTR {
m.MaxLow = 0
}
} else {
m.MaxLow = max(m.MaxLow, l)
if m.SLLong > m.minSLTR+0.01 {
m.SLLong -= m.stepSLTR
}
}
if shortNum == 0 {
if m.SLShort != m.InitSLTR {
m.MinHigh = 0
}
} else {
m.MinHigh = min(m.MinHigh, h)
if m.SLShort > m.minSLTR+0.01 {
m.SLShort -= m.stepSLTR
}
}
}
func (m *TRS) Calc(s *strat.StratJob, atr float64) (float64, float64) {
m.setTRS(s.Env, len(s.LongOrders), len(s.ShortOrders))
var stopLong, stopShort float64
if m.MaxLow > 0 {
stopLong = m.MaxLow - atr*m.SLLong
}
if m.MinHigh > 0 {
stopShort = m.MinHigh + atr*m.SLShort
}
return stopLong, stopShort
}
func (m *TRS) CalcSL(s *strat.StratJob, atr *ta.Series, lenAtr, lenAtrB int, pow, rate float64) (float64, float64) {
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), lenAtrB).Get(0)
atrRate := math.Pow(atr.Get(0)/atrBase, pow)
return m.Calc(s, atrBase*atrRate*rate)
}
func (m *TRS) CalcSL2(s *strat.StratJob, lenAtr, lenAtrB int, pow, rate float64) (float64, float64) {
e := s.Env
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
return m.CalcSL(s, atr, lenAtr, lenAtrB, pow, rate)
}

6
com/types.go Normal file
View File

@@ -0,0 +1,6 @@
package com
type CodeVal struct {
Code string
Val float64
}

51
com/utils.go Normal file
View File

@@ -0,0 +1,51 @@
package com
import (
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
)
func FastOpen(s *strat.StratJob, dirt, slLong, slShort, limit float64) {
openTag := "long"
stopLoss := slLong
if dirt < 0 {
openTag = "short"
stopLoss = slShort
}
if !s.CanOpen(dirt < 0) {
return
}
err := s.OpenOrder(&strat.EnterReq{
Tag: openTag,
Short: dirt < 0,
Limit: limit,
StopLoss: stopLoss,
})
if err != nil {
log.Error("open fail", zap.Error(err))
}
}
func MakePairInfos(warmNum int) func(s *strat.StratJob) []*strat.PairSub {
return func(s *strat.StratJob) []*strat.PairSub {
var bigTF string
switch s.TimeFrame {
case "1d":
bigTF = "1w"
case "4h":
bigTF = "1d"
case "1h":
bigTF = "8h"
case "15m":
bigTF = "2h"
case "5m":
bigTF = "1h"
case "1m":
bigTF = "15m"
}
return []*strat.PairSub{
{Pair: "_cur_", TimeFrame: bigTF, WarmupNum: warmNum},
}
}
}

79
demo/bar_agg.go Normal file
View File

@@ -0,0 +1,79 @@
package demo
import (
"fmt"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/strat"
"github.com/banbox/banbot/utils"
"github.com/banbox/banexg"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
ta "github.com/banbox/banta"
"go.uber.org/zap"
"path/filepath"
"strings"
)
type AggMore struct {
bars []*banexg.Kline
barsBig []*banexg.Kline
}
func getBar(e *ta.BarEnv) *banexg.Kline {
return &banexg.Kline{
Time: e.TimeStart,
Open: e.Open.Get(0),
High: e.High.Get(0),
Low: e.Low.Get(0),
Close: e.Close.Get(0),
Volume: e.Volume.Get(0),
}
}
func dumpKline(bars []*banexg.Kline, name string) *errs.Error {
if len(bars) == 0 {
return nil
}
outDir := config.GetDataDir()
rows := utils.KlineToStr(bars, btime.UTCLocale)
path := filepath.Join(outDir, "tmp", fmt.Sprintf("%s.csv", name))
return utils.WriteCsvFile(path, rows, false)
}
func BarAgg(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 1,
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "1h", 1},
}
},
OnStartUp: func(s *strat.StratJob) {
s.More = &AggMore{}
},
OnBar: func(s *strat.StratJob) {
b := getBar(s.Env)
m, _ := s.More.(*AggMore)
m.bars = append(m.bars, b)
},
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
b := getBar(e)
m, _ := s.More.(*AggMore)
m.barsBig = append(m.barsBig, b)
},
OnShutDown: func(s *strat.StratJob) {
m, _ := s.More.(*AggMore)
code := strings.ReplaceAll(s.Symbol.Symbol, "/", "_")
code = strings.ReplaceAll(code, ":", "_")
err := dumpKline(m.bars, fmt.Sprintf("%s_%s", code, s.TimeFrame))
if err != nil {
log.Error("dump klines fail", zap.Error(err))
}
err = dumpKline(m.barsBig, fmt.Sprintf("%s_1h", code))
if err != nil {
log.Error("dump big klines fail", zap.Error(err))
}
},
}
}

40
demo/bar_stat.go Normal file
View File

@@ -0,0 +1,40 @@
package demo
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
"slices"
)
type Stat struct {
rates []float64
}
func BarStat(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 50,
OnStartUp: func(s *strat.StratJob) {
s.More = &Stat{}
},
OnBar: func(s *strat.StratJob) {
m, _ := s.More.(*Stat)
// rate := genCurFutProbs(s.Env)[0]
m.rates = append(m.rates, 0)
},
OnShutDown: func(s *strat.StratJob) {
m, _ := s.More.(*Stat)
slices.Sort(m.rates)
var sum float64
var outs []string
for _, v := range m.rates {
sum += v
outs = append(outs, fmt.Sprintf("%.2f", v*100))
}
log.Info("", zap.Float64("avg", sum/float64(len(m.rates))),
zap.Strings("arr", outs))
},
}
}

48
demo/dca.go Normal file
View File

@@ -0,0 +1,48 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
)
type DCAState struct {
LastInvestTime int64 // 上次定投时间
}
func DCA(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
intvDays := pol.DefInt("intvDays", 7, core.PNorm(1, 365))
intvMSecs := int64(intvDays) * 86400000
return &strat.TradeStrat{
WarmupNum: 1, // 定投策略不需要太多预热数据
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的状态
s.More = &DCAState{
LastInvestTime: 0,
}
},
OnBar: func(s *strat.StratJob) {
state, _ := s.More.(*DCAState)
currentTime := s.Env.TimeStop
// 检查是否到了定投时间间隔
if state.LastInvestTime > 0 && currentTime-state.LastInvestTime < intvMSecs {
return // 还未到定投时间
}
// 执行定投买入
err := s.OpenOrder(&strat.EnterReq{
Tag: "buy",
Log: true,
})
if err == nil {
// 更新上次定投时间
state.LastInvestTime = currentTime
}
},
}
}

71
demo/demo.go Normal file
View File

@@ -0,0 +1,71 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func Demo(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
fastPeriod := pol.DefInt("fastPeriod", 5, core.PNorm(3, 20))
maRate := pol.Def("maRate", 4.0, core.PNorm(1.5, 9.0))
// 计算慢速均线周期
slowPeriod := int(float64(fastPeriod) * maRate)
return &strat.TradeStrat{
WarmupNum: slowPeriod + 10,
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
maFast := ta.SMA(e.Close, fastPeriod) // 快速移动平均线
maSlow := ta.SMA(e.Close, slowPeriod) // 慢速移动平均线
maCrossState := maFast.Cross(maSlow) // 判断均线交叉状态
// 金叉:快线上穿慢线
if maCrossState == 1 {
// 先平掉空头持仓
if len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "golden_cross_close_short",
Dirt: core.OdDirtShort,
})
}
// 再开多头仓位
if len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "golden_cross_long",
Short: false,
})
}
}
// 死叉:快线下穿慢线
if maCrossState == -1 {
// 先平掉多头持仓
if len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "death_cross_close_long",
Dirt: core.OdDirtLong,
})
}
// 再开空头仓位
if len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "death_cross_short",
Short: true,
})
}
}
},
}
}
func init() {
strat.StratMake["demo"] = Demo
}

View File

@@ -0,0 +1,43 @@
package freqtrade
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
/*
Strategy 004
author@: Gerald Lonlas
github@: https://github.com/freqtrade/freqtrade-strategies/blob/main/user_data/strategies/Strategy004.py
*/
func Strategy004(p *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 100,
EachMaxLong: 1,
OnBar: func(s *strat.StratJob) {
e := s.Env
adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0)
adxSlow := ta.ADX(e.High, e.Low, e.Close, 35).Get(0)
cci := ta.CCI(e.Close, 14).Get(0)
fastD, _, fastK := ta.KDJBy(e.High, e.Low, e.Close, 5, 3, 3, "sma")
fastDBig, _, fastKBig := ta.KDJBy(e.High, e.Low, e.Close, 50, 3, 3, "sma")
ema := ta.EMA(e.Close, 5).Get(0)
KDX := fastK.Cross(fastD)
adxOk := adx > 50 || adxSlow > 26
fastKDOk := fastK.Get(1) < 20 && fastD.Get(1) < 20
fastKDBigOk := fastKBig.Get(1) < 30 && fastDBig.Get(1) < 30
fastKDgt70 := fastK.Get(0) > 70 || fastD.Get(0) > 70
if adxOk && cci < -100 && fastKDOk && fastKDBigOk && KDX == 1 {
s.OpenOrder(&strat.EnterReq{
Tag: "long",
StopLossVal: ema * 0.1,
})
} else if adxSlow < 25 && fastKDgt70 && fastK.Get(1) < fastD.Get(1) && e.Close.Get(0) > ema {
s.CloseOrders(&strat.ExitReq{Tag: "exit"})
}
},
}
}

View File

@@ -0,0 +1,26 @@
package freqtrade
/*
Strategy 005
author@: Gerald Lonlas
github@: https://github.com/freqtrade/freqtrade-strategies/blob/main/user_data/strategies/Strategy005.py
*/
//func Strategy005(p *config.RunPolicyConfig) *strat.TradeStrat {
// buyVolAvg := int(p.Def("buyVolAvg", 150, core.PNorm(50, 300)))
// buyRsi := p.Def("buyRsi", 26, core.PNorm(1, 100))
// buyFastD := p.Def("buyFastD", 1, core.PNorm(1, 100))
// buyFishNo := p.Def("buyFishNo", 5, core.PNorm(1, 100))
// sellRsi := p.Def("sellRsi", 74, core.PNorm(1, 100))
// sellMiDi := p.Def("sellMiDi", 4, core.PNorm(1, 100))
// sellFiRsiN := p.Def("sellFiRsiN", 30, core.PNorm(1, 100))
// return &strat.TradeStrat{
// WarmupNum: 100,
// OnBar: func(s *strat.StratJob) {
// e := s.Env
// macdCols := ta.MACD(e.Close, 12, 26, 9).Cols
// macd, macdSig := macdCols[0], macdCols[1]
//
// },
// }
//}

11
demo/freqtrade/common.go Normal file
View File

@@ -0,0 +1,11 @@
package freqtrade
import "github.com/banbox/banbot/strat"
func init() {
strat.AddStratGroup("freqtrade", map[string]strat.FuncMakeStrat{
"Strategy001": Strategy001,
"Strategy004": Strategy004,
})
}

View File

@@ -0,0 +1,46 @@
package freqtrade
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
/*
Strategy 001
author@: Gerald Lonlas
github@: https://github.com/freqtrade/freqtrade-strategies/blob/main/user_data/strategies/Strategy001.py
*/
func Strategy001(p *config.RunPolicyConfig) *strat.TradeStrat {
lenSml := int(p.Def("lenSml", 20, core.PNorm(10, 30)))
midRate := p.Def("midRate", 2.5, core.PNorm(1.5, 5))
bigRate := p.Def("bigRate", 2, core.PNorm(1.5, 3))
lenMid := int(float64(lenSml) * midRate)
lenBig := int(float64(lenMid) * bigRate)
return &strat.TradeStrat{
WarmupNum: 200,
EachMaxLong: 1,
OnBar: func(s *strat.StratJob) {
e := s.Env
maSml := ta.EMA(e.Close, lenSml)
maMid := ta.EMA(e.Close, lenMid)
maBig := ta.EMA(e.Close, lenBig)
haO, _, _, haC := ta.HeikinAshi(e)
haOpen := haO.Get(0)
haClose := haC.Get(0)
smlXmid := maSml.Cross(maMid)
midXBig := maMid.Cross(maBig)
if smlXmid == 1 && haClose > maSml.Get(0) && haOpen < haClose {
s.OpenOrder(&strat.EnterReq{
Tag: "long",
StopLossVal: haClose * 0.1,
})
} else if midXBig == 1 && haClose < maSml.Get(0) && haOpen > haClose {
s.CloseOrders(&strat.ExitReq{Tag: "exit"})
}
},
}
}

3
demo/here.java Normal file
View File

@@ -0,0 +1,3 @@
public static void main (String[] args) {
System.out.println("Hello World2!");
}

43
demo/hft.go Normal file
View File

@@ -0,0 +1,43 @@
package demo
import (
"fmt"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
)
func hft(p *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WsSubs: map[string]string{
core.WsSubDepth: "",
core.WsSubTrade: "",
core.WsSubKLine: "",
},
OnBar: func(s *strat.StratJob) {
e := s.Env
log.Info(fmt.Sprintf("OnBar %v: %v", e.TimeStop, e.Close.Get(0)))
},
OnWsKline: func(s *strat.StratJob, pair string, k *banexg.Kline) {
log.Info(fmt.Sprintf("OnWsKline %v: %v", k.Time, k.Close))
s.OpenOrder(&strat.EnterReq{Tag: "long"})
_, _, err := biz.GetOdMgr(s.Account).ProcessOrders(s)
if err != nil {
log.Error("process order fail", zap.Error(err))
}
},
OnWsTrades: func(s *strat.StratJob, pair string, trades []*banexg.Trade) {
last := trades[len(trades)-1]
log.Info(fmt.Sprintf("OnWsTrades %v %v, %v", last.Timestamp, last.Price, last.Amount))
},
OnWsDepth: func(s *strat.StratJob, dep *banexg.OrderBook) {
bp1, bm1 := dep.Bids.Price[0], dep.Bids.Size[0]
ap1, am1 := dep.Asks.Price[0], dep.Asks.Size[0]
log.Info(fmt.Sprintf("OnWsDepth %v %v, %v,, %v, %v", dep.TimeStamp, bp1, bm1, ap1, am1))
},
}
}

19
demo/hold.go Normal file
View File

@@ -0,0 +1,19 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/strat"
)
func Hold(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
EachMaxLong: 1,
OnBar: func(s *strat.StratJob) {
if len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "hold",
})
}
},
}
}

37
demo/holdSl.go Normal file
View File

@@ -0,0 +1,37 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func HoldSL(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 20,
EachMaxLong: 1,
OnBar: func(s *strat.StratJob) {
e := s.Env
ma5 := ta.VWMA(e.Close, e.Volume, 5)
ma20 := ta.VWMA(e.Close, e.Volume, 10)
maCrx := ma5.Cross(ma20)
atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0)
for _, od := range s.LongOrders {
od.SetStopLoss(&ormo.ExitTrigger{
Price: e.Close.Get(0) - atr*0.5,
})
}
if maCrx == -1 && len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{Tag: "exit", Dirt: core.OdDirtLong})
} else if maCrx == 1 || len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "long",
StopLossVal: atr * 0.5,
})
}
},
}
}

48
demo/limits.go Normal file
View File

@@ -0,0 +1,48 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
)
func DemoV1(pol *config.RunPolicyConfig) *strat.TradeStrat {
dirt := -1.0
limitRate := 0.001
slRate := 0.005
return &strat.TradeStrat{
WarmupNum: 30,
OnBar: func(s *strat.StratJob) {
if s.OrderNum == 0 {
price := s.Env.Close.Get(0)
limit := price * (1 - dirt*limitRate)
stopLoss := price * (1 - dirt*slRate)
err := s.OpenOrder(&strat.EnterReq{
Tag: "enter",
Limit: limit,
Short: dirt < 0,
StopLoss: stopLoss,
})
if err != nil {
log.Error("open fail", zap.Error(err))
} else {
log.Info("new order", zap.Float64("limit", limit), zap.Float64("stopLoss", stopLoss))
}
}
},
OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq {
price := s.Env.Close.Get(0)
stopLoss := price * (1 - dirt*slRate)
sl := od.GetStopLoss()
if sl != nil && (stopLoss-sl.Price)*dirt > 0 {
// 有盈利,调整跟踪止损
log.Info("set stoploss", zap.Float64("val", stopLoss))
sl.Price = stopLoss
od.DirtyInfo = true
}
return nil
},
}
}

31
demo/ma_er.go Normal file
View File

@@ -0,0 +1,31 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func DemoER(pol *config.RunPolicyConfig) *strat.TradeStrat {
smlLen := int(pol.Def("smlLen", 9, core.PNorm(3, 10)))
bigLen := int(pol.Def("bigLen", 23, core.PNorm(10, 40)))
erUpp := pol.Def("erUpp", 0.13, core.PNorm(0.1, 0.7))
return &strat.TradeStrat{
WarmupNum: 100,
OnBar: func(s *strat.StratJob) {
e := s.Env
ma5 := ta.SMA(e.Close, smlLen)
ma20 := ta.SMA(e.Close, bigLen)
maCrx := ma5.Cross(ma20)
er := ta.ER(e.Close, 50).Get(0)
if maCrx == 1 && er < erUpp {
s.OpenOrder(&strat.EnterReq{Tag: "open"})
} else if maCrx == -1 {
s.CloseOrders(&strat.ExitReq{Tag: "exit"})
}
},
}
}

86
demo/made.go Normal file
View File

@@ -0,0 +1,86 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func Made(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
fastMAPeriod := pol.DefInt("fast_ma_period", 5, core.PUniform(3, 10))
slowMAPeriod := pol.DefInt("slow_ma_period", 20, core.PUniform(10, 40))
atrPeriod := pol.DefInt("atr_period", 14, core.PUniform(7, 21))
slRate := pol.Def("sl_rate", 2.0, core.PUniform(1.0, 4.0))
return &strat.TradeStrat{
WarmupNum: 100,
EachMaxLong: 1,
EachMaxShort: 1,
DrawDownExit: true, // 启用回撤止盈
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
maFast := ta.SMA(e.Close, fastMAPeriod)
maSlow := ta.SMA(e.Close, slowMAPeriod)
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
// 计算均线交叉信号
maCrossOver := maFast.Cross(maSlow) // 1 表示上穿
maCrossUnder := maSlow.Cross(maFast) // 1 表示下穿
// 获取当前价格和ATR值
currentPrice := e.Close.Get(0)
currentATR := atr.Get(0)
// 多头入场信号:快线上穿慢线(金叉)
if maCrossOver == 1 {
if s.GetOrderNum(core.OdDirtShort) > 0 {
s.CloseOrders(&strat.ExitReq{Dirt: core.OdDirtShort, Tag: "exit_short"})
}
if s.GetOrderNum(core.OdDirtLong) == 0 {
stopLossPrice := currentPrice - currentATR*slRate
s.OpenOrder(&strat.EnterReq{
Tag: "long",
StopLoss: stopLossPrice,
StopLossTag: "atr_sl_long",
})
}
}
// 空头入场信号:快线下穿慢线(死叉)
if maCrossUnder == 1 {
if s.GetOrderNum(core.OdDirtLong) > 0 {
s.CloseOrders(&strat.ExitReq{Dirt: core.OdDirtLong, Tag: "exit_long"})
}
if s.GetOrderNum(core.OdDirtShort) == 0 {
stopLossPrice := currentPrice + currentATR*slRate
s.OpenOrder(&strat.EnterReq{
Tag: "short",
Short: true,
StopLoss: stopLossPrice,
StopLossTag: "atr_sl_short",
})
}
}
// 多头出场信号:快线下穿慢线(死叉)
if maCrossUnder == 1 && s.GetOrderNum(core.OdDirtLong) > 0 {
s.CloseOrders(&strat.ExitReq{
Dirt: core.OdDirtLong,
Tag: "exit_long",
})
}
// 空头出场信号:快线上穿慢线(金叉)
if maCrossOver == 1 && s.GetOrderNum(core.OdDirtShort) > 0 {
s.CloseOrders(&strat.ExitReq{
Dirt: core.OdDirtShort,
Tag: "exit_short",
})
}
},
}
}

26
demo/main.go Normal file
View File

@@ -0,0 +1,26 @@
package demo
import (
"github.com/banbox/banbot/strat"
)
func init() {
strat.AddStratGroup("demo", map[string]strat.FuncMakeStrat{
"dca": DCA,
"tmp": tmp,
"01": DemoV1,
"agg": BarAgg,
"stat": BarStat,
"demo": Demo,
"demo_er": DemoER,
"riskRw": fixRiskReward,
"hold": Hold,
"holdSL": HoldSL,
"show": Show,
"tmp1": Tmp1,
"made": Made,
"test": Test,
"hft": hft,
"TrailStop": TrailStop,
})
}

97
demo/pairs_test.go Normal file
View File

@@ -0,0 +1,97 @@
package demo
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/exg"
"github.com/banbox/banbot/goods"
"github.com/banbox/banbot/orm"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
"math/rand"
"slices"
"sort"
"testing"
"time"
)
func genVolPairs(timeMS int64) []string {
err := biz.SetupComs(&config.CmdArgs{})
if err != nil {
panic(err)
}
_, err = orm.LoadMarkets(exg.Default, false)
if err != nil {
panic(err)
}
ft := goods.VolumePairFilter{
BaseFilter: goods.BaseFilter{Name: "VolumePairFilter"},
BackPeriod: "3d",
}
core.SetRunMode(core.RunModeBackTest)
core.SetRunEnv(core.RunEnvProd)
btime.CurTimeMS = timeMS
config.PairMgr.Cron = ""
pairs, err := ft.GenSymbols(btime.UTCStamp())
if err != nil {
panic(err)
}
res := make([]string, 0, len(pairs))
for _, pair := range pairs {
_, quote, _, _ := core.SplitSymbol(pair)
if _, ok := config.StakeCurrencyMap[quote]; ok {
res = append(res, pair)
}
}
return res
}
func TestRandPairs(t *testing.T) {
timeMS, _ := btime.ParseTimeMS("2021-11-01 00:00")
pairs := genVolPairs(timeMS)
total := len(pairs)
half := pairs[:total/3]
for i := range half {
j := rand.Intn(i + 1)
if i != j {
half[i], half[j] = half[j], half[i]
}
}
cutNum := 50
if len(half) > cutNum {
half = half[:cutNum]
}
log.Info("pairs", zap.Int("total", total), zap.Strings("arr", half))
}
func TestPairsLocs(t *testing.T) {
pairs := genVolPairs(time.Now().UnixMilli())
//pairs48 := []string{"1000FLOKI/USDT:USDT", "1000LUNC/USDT:USDT", "1000PEPE/USDT:USDT", "1000SHIB/USDT:USDT", "1000XEC/USDT:USDT", "ADA/USDT:USDT", "APT/USDT:USDT", "ARB/USDT:USDT", "AVAX/USDT:USDT", "BADGER/USDT:USDT", "BCH/USDT:USDT", "BNB/USDT:USDT", "BOME/USDT:USDT", "BTC/USDT:USDT", "CFX/USDT:USDT", "DOGE/USDT:USDT", "DOT/USDT:USDT", "ETC/USDT:USDT", "ETH/USDT:USDT", "ETHFI/USDT:USDT", "FET/USDT:USDT", "FIL/USDT:USDT", "FTM/USDT:USDT", "GALA/USDT:USDT", "HIFI/USDT:USDT", "ICP/USDT:USDT", "ID/USDT:USDT", "JUP/USDT:USDT", "LINK/USDT:USDT", "LTC/USDT:USDT", "LUNA2/USDT:USDT", "MATIC/USDT:USDT", "NEAR/USDT:USDT", "ONDO/USDT:USDT", "OP/USDT:USDT", "ORDI/USDT:USDT", "PENDLE/USDT:USDT", "PIXEL/USDT:USDT", "POLYX/USDT:USDT", "RSR/USDT:USDT", "SAND/USDT:USDT", "SOL/USDT:USDT", "STX/USDT:USDT", "SUI/USDT:USDT", "TOKEN/USDT:USDT", "TON/USDT:USDT", "WIF/USDT:USDT", "WLD/USDT:USDT", "XRP/USDT:USDT", "YGG/USDT:USDT"}
pairs170 := []string{"1000BONK/USDT:USDT", "1000FLOKI/USDT:USDT", "1000LUNC/USDT:USDT", "1000PEPE/USDT:USDT", "1000RATS/USDT:USDT", "1000XEC/USDT:USDT", "1INCH/USDT:USDT", "AAVE/USDT:USDT", "ACH/USDT:USDT", "ADA/USDT:USDT", "AGIX/USDT:USDT", "ALGO/USDT:USDT", "ANKR/USDT:USDT", "APE/USDT:USDT", "API3/USDT:USDT", "APT/USDT:USDT", "AR/USDT:USDT", "ARB/USDT:USDT", "ARPA/USDT:USDT", "ASTR/USDT:USDT", "ATOM/USDT:USDT", "AUCTION/USDT:USDT", "AVAX/USDT:USDT", "AXL/USDT:USDT", "AXS/USDT:USDT", "BADGER/USDT:USDT", "BAKE/USDT:USDT", "BCH/USDT:USDT", "BEAMX/USDT:USDT", "BEL/USDT:USDT", "BICO/USDT:USDT", "BLUR/USDT:USDT", "BNB/USDT:USDT", "BNX/USDT:USDT", "BOND/USDT:USDT", "BSV/USDT:USDT", "BTC/USDT:USDT", "CAKE/USDT:USDT", "CELO/USDT:USDT", "CFX/USDT:USDT", "CHR/USDT:USDT", "CHZ/USDT:USDT", "COMP/USDT:USDT", "CYBER/USDT:USDT", "DOGE/USDT:USDT", "DOT/USDT:USDT", "DUSK/USDT:USDT", "DYM/USDT:USDT", "EGLD/USDT:USDT", "ENA/USDT:USDT", "ENJ/USDT:USDT", "EOS/USDT:USDT", "ETC/USDT:USDT", "ETH/USDT:USDT", "ETHW/USDT:USDT", "FET/USDT:USDT", "FIL/USDT:USDT", "FLM/USDT:USDT", "FLOW/USDT:USDT", "FRONT/USDT:USDT", "FTM/USDT:USDT", "GAL/USDT:USDT", "GALA/USDT:USDT", "GAS/USDT:USDT", "GLM/USDT:USDT", "GMT/USDT:USDT", "GMX/USDT:USDT", "GRT/USDT:USDT", "HBAR/USDT:USDT", "HIFI/USDT:USDT", "HOOK/USDT:USDT", "HOT/USDT:USDT", "ICP/USDT:USDT", "ID/USDT:USDT", "ILV/USDT:USDT", "IMX/USDT:USDT", "INJ/USDT:USDT", "IOTA/USDT:USDT", "IOTX/USDT:USDT", "JASMY/USDT:USDT", "JOE/USDT:USDT", "JTO/USDT:USDT", "JUP/USDT:USDT", "KAS/USDT:USDT", "KAVA/USDT:USDT", "KEY/USDT:USDT", "KNC/USDT:USDT", "KSM/USDT:USDT", "LDO/USDT:USDT", "LEVER/USDT:USDT", "LINA/USDT:USDT", "LINK/USDT:USDT", "LPT/USDT:USDT", "LQTY/USDT:USDT", "LRC/USDT:USDT", "LTC/USDT:USDT", "LUNA2/USDT:USDT", "MAGIC/USDT:USDT", "MANA/USDT:USDT", "MANTA/USDT:USDT", "MASK/USDT:USDT", "MATIC/USDT:USDT", "MAV/USDT:USDT", "MEME/USDT:USDT", "METIS/USDT:USDT", "MINA/USDT:USDT", "MKR/USDT:USDT", "MTL/USDT:USDT", "MYRO/USDT:USDT", "NEO/USDT:USDT", "NKN/USDT:USDT", "OCEAN/USDT:USDT", "OM/USDT:USDT", "ONDO/USDT:USDT", "ONE/USDT:USDT", "OP/USDT:USDT", "ORDI/USDT:USDT", "PENDLE/USDT:USDT", "PEOPLE/USDT:USDT", "POLYX/USDT:USDT", "PYTH/USDT:USDT", "QNT/USDT:USDT", "QTUM/USDT:USDT", "RDNT/USDT:USDT", "REN/USDT:USDT", "RIF/USDT:USDT", "RLC/USDT:USDT", "RSR/USDT:USDT", "RUNE/USDT:USDT", "RVN/USDT:USDT", "SEI/USDT:USDT", "SKL/USDT:USDT", "SNX/USDT:USDT", "SPELL/USDT:USDT", "SSV/USDT:USDT", "STG/USDT:USDT", "STMX/USDT:USDT", "STORJ/USDT:USDT", "STX/USDT:USDT", "SUI/USDT:USDT", "SXP/USDT:USDT", "T/USDT:USDT", "THETA/USDT:USDT", "TIA/USDT:USDT", "TOKEN/USDT:USDT", "TON/USDT:USDT", "TRU/USDT:USDT", "TRX/USDT:USDT", "UMA/USDT:USDT", "VET/USDT:USDT", "WAVES/USDT:USDT", "WAXP/USDT:USDT", "WIF/USDT:USDT", "WOO/USDT:USDT", "XAI/USDT:USDT", "XLM/USDT:USDT", "XRP/USDT:USDT", "XTZ/USDT:USDT", "XVG/USDT:USDT", "YFI/USDT:USDT", "YGG/USDT:USDT", "ZEC/USDT:USDT", "ZEN/USDT:USDT", "ZETA/USDT:USDT", "ZIL/USDT:USDT", "ZRX/USDT:USDT"}
fails := make([]string, 0, 10)
idArr := make([]int, 0, 100)
for _, p := range pairs170 {
idx := slices.Index(pairs, p)
if idx < 0 {
fails = append(fails, p)
} else {
idArr = append(idArr, idx)
}
}
log.Warn("pairs", zap.Int("total", len(pairs)), zap.Strings("unknown", fails))
sort.Slice(idArr, func(i, j int) bool {
return idArr[i] < idArr[j]
})
step := 10
end, num := step, 0
for _, v := range idArr {
if v >= end {
log.Info("range", zap.Int("to", end), zap.Int("num", num))
num = 0
end += step
}
num += 1
}
}

63
demo/risk_reward.go Normal file
View File

@@ -0,0 +1,63 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func fixRiskReward(p *config.RunPolicyConfig) *strat.TradeStrat {
rewardRate := 4.0
return &strat.TradeStrat{
EachMaxLong: 1,
WarmupNum: 30,
OnBar: func(s *strat.StratJob) {
e := s.Env
atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0)
rsi := ta.RSI50(e.Close, 14).Get(0)
ma := ta.SMA(e.Close, 5).Get(0)
maMid := ta.SMA(e.Close, 10).Get(0)
maBig := ta.SMA(e.Close, 40).Get(0)
c := e.Close.Get(0)
if s.OrderNum == 0 && maMid > maBig && rsi > 15 && c < ma {
s.OpenOrder(&strat.EnterReq{
Tag: "long",
StopLossVal: atr,
StopLossTag: "init",
})
}
},
OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq {
sl := od.GetStopLoss()
if sl.Tag == "take" {
// 已设置止盈
return nil
}
if sl.Tag == "init" {
// 检查是否回本,如果是,设置止损到成本线
if od.ProfitRate > 0 {
// 保存初始止损空间
od.SetInfo("initSL", sl.Range)
feeRate := od.Enter.FeeQuote * 2 / od.EnterCost()
slPrice := od.Enter.Average * (1 + feeRate)
od.SetStopLoss(&ormo.ExitTrigger{
Price: slPrice,
Tag: "break_even",
})
}
} else {
//已设置成本止损,检查是否触发止盈
slRange := od.GetInfoFloat64("initSL")
if od.Profit > slRange*rewardRate {
// 触发止盈线,设置止盈线止损
od.SetStopLoss(&ormo.ExitTrigger{
Price: od.Enter.Average + slRange*rewardRate,
Tag: "take",
})
}
}
return nil
},
}
}

25
demo/sample.go Normal file
View File

@@ -0,0 +1,25 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func Sample(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 100,
OnBar: func(s *strat.StratJob) {
e := s.Env
ma5 := ta.SMA(e.Close, 5)
ma20 := ta.SMA(e.Close, 20)
maCrx := ma5.Cross(ma20)
if maCrx == 1 {
s.OpenOrder(&strat.EnterReq{Tag: "open"})
} else if maCrx == -1 {
s.CloseOrders(&strat.ExitReq{Tag: "exit"})
}
},
}
}

45
demo/show.go Normal file
View File

@@ -0,0 +1,45 @@
package demo
import (
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
"strings"
"sync"
)
func Show(pol *config.RunPolicyConfig) *strat.TradeStrat {
pairMap := make(map[string]int)
var copied []string
var lastMS int64
var lockPair sync.Mutex
return &strat.TradeStrat{
EachMaxLong: 1,
OnStartUp: func(s *strat.StratJob) {
lockPair.Lock()
base, _, _, _ := core.SplitSymbol(s.Symbol.Symbol)
pairMap[base] = 0
lockPair.Unlock()
},
OnBar: func(s *strat.StratJob) {
curMS := s.Env.TimeStop
if curMS > lastMS {
if len(copied) > 0 {
date := btime.ToDateStr(lastMS, core.DefaultDateFmt)
lockPair.Lock()
totalNum := len(pairMap)
lockPair.Unlock()
log.Info("OnBar", zap.String("date", date), zap.Int("got", len(copied)),
zap.Int("total", totalNum), zap.String("copied", strings.Join(copied, ",")))
}
lastMS = curMS
copied = nil
}
base, _, _, _ := core.SplitSymbol(s.Symbol.Symbol)
copied = append(copied, base)
},
}
}

45
demo/test.go Normal file
View File

@@ -0,0 +1,45 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func Test(pol *config.RunPolicyConfig) *strat.TradeStrat {
//- 超参(极简)
highestDay := pol.DefInt("highest day", 20, core.PNorm(10, 40))
atrN := pol.DefInt("atrn", 14, core.PNorm(10, 30))
slNATR := pol.Def("sl natr", 2.0, core.PNorm(0.8, 4.0))
tpNATR := pol.Def("tp natr", 2.5, core.PNorm(1.0, 5.0))
//预热:技术指标所需长度的最大值 + 余量
warm := max(highestDay+1, atrN+1, 20) + 2
return &strat.TradeStrat{
WarmupNum: warm,
RunTimeFrames: []string{"15m"},
HedgeOff: true,
OnBar: func(s *strat.StratJob) {
e := s.Env
if e.Close.Len() <= warm {
return
}
last := e.Close.Get(0)
hhPrev := ta.Highest(e.High, highestDay).Get(1)
atr := ta.ATR(e.High, e.Low, e.Close, atrN).Get(0)
//仅在没有任何多单(含未成交挂单)时考虑新挂单倮京沋塑
if len(s.LongOrders) == 0 {
//只在价格位于前高下方时挂“突破买入"的 stop 触发单
if last < hhPrev && atr > 0 {
entry := hhPrev
_ = s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Stop: entry,
StopLossVal: slNATR * atr,
TakeProfitVal: tpNATR * atr,
})
}
}
},
}
}

35
demo/tmp.go Normal file
View File

@@ -0,0 +1,35 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
)
func tmp(p *config.RunPolicyConfig) *strat.TradeStrat {
price := 2.236
return &strat.TradeStrat{
OnBar: func(s *strat.StratJob) {
s.OpenOrder(&strat.EnterReq{
Tag: "BUY",
OrderType: core.OrderTypeLimit,
Limit: price,
Amount: 4.65000,
TakeProfit: price * 1.01,
TakeProfitLimit: price * 1.012,
StopLoss: price * 0.99,
Log: true,
})
s.CloseOrders(&strat.ExitReq{
Tag: "all",
})
ods, err := strat.GetHistOrders(ormo.GetHistOrdersArgs{
Limit: 3,
})
log.Info("get hist", zap.Int("num", len(ods)), zap.Error(err))
},
}
}

29
demo/tmp1.go Normal file
View File

@@ -0,0 +1,29 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
"math"
)
func Tmp1(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 100,
OnBar: func(s *strat.StratJob) {
e := s.Env
flag := math.Round(math.Mod(float64(e.TimeStop/e.TFMSecs), 2))
if flag == 1 {
s.OpenOrder(&strat.EnterReq{Tag: "long"})
if len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{Tag: "exitS", Dirt: core.OdDirtShort})
}
} else {
s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true})
if len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{Tag: "exitL", Dirt: core.OdDirtLong})
}
}
},
}
}

27
demo/trailing_stop.go Normal file
View File

@@ -0,0 +1,27 @@
package demo
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func TrailStop(p *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 100,
TimeFrames: "1h",
OnBar: func(s *strat.StratJob) {
e := s.Env
atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0)
atrPct := atr * 100 / e.Close.Get(0)
k, d, _ := ta.KDJ(e.High, e.Low, e.Close, 9, 3, 3)
kdCross := k.Cross(d)
if len(s.LongOrders) == 0 && kdCross == 1 {
s.OpenOrder(&strat.EnterReq{
Tag: "long",
CallbackPct: min(10, 5*atrPct),
})
}
},
}
}

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
}

172
fac/best.go Normal file
View File

@@ -0,0 +1,172 @@
package fac
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/opt"
"github.com/banbox/banbot/utils"
"github.com/banbox/banexg/log"
utils2 "github.com/banbox/banexg/utils"
"go.uber.org/zap"
"gonum.org/v1/gonum/floats"
)
func init() {
opt.FactorMap["best"] = best
opt.FactorMap["best3"] = best3
opt.FactorMap["bigVol"] = bigVols
opt.FactorMap["sharpe"] = sharpe
}
func bigVols(arg opt.FacArgs) ([]string, error) {
allNum := len(arg.Pairs)
cutNum := min(int(float64(allNum)/10), 30)
// 只取成交量高的一半品种
volPeriodMS := int64(utils2.TFToSecs("30d") * 1000)
volBarNum := int(volPeriodMS / arg.TFMSecs)
vols := make([]float64, 0, allNum)
for _, sta := range arg.Pairs {
start := max(0, len(sta.KLines)-volBarNum)
bars := sta.KLines[start:]
volSum := float64(0)
for _, b := range bars {
volSum += b.Volume * b.Close
}
vols = append(vols, volSum)
}
volIdxArr := utils.ArgSortDesc(vols)[:allNum/2]
var result = make([]string, 0, cutNum)
for _, idx := range volIdxArr {
result = append(result, arg.Pairs[idx].Symbol)
if len(result) >= cutNum {
break
}
}
return result, nil
}
func best(arg opt.FacArgs) ([]string, error) {
return bestOf(arg, 0)
}
func best3(arg opt.FacArgs) ([]string, error) {
return bestOf(arg, 0.25)
}
func bestOf(arg opt.FacArgs, offRate float64) ([]string, error) {
allNum := len(arg.Pairs)
initFund := float64(0)
for _, v := range config.WalletAmounts {
initFund = v
break
}
cutNum := min(int(float64(allNum)/10), 30)
// 只取成交量高的一半品种
volPeriodMS := int64(utils2.TFToSecs("30d") * 1000)
volBarNum := int(volPeriodMS / arg.TFMSecs)
vols := make([]float64, 0, allNum)
scores := make([]float64, 0, allNum)
for _, sta := range arg.Pairs {
start := max(0, len(sta.KLines)-volBarNum)
bars := sta.KLines[start:]
volSum := float64(0)
for _, b := range bars {
volSum += b.Volume * b.Close
}
vols = append(vols, volSum)
profitPct := floats.Sum(sta.Returns) * 100 / initFund
reals := make([]float64, 0, len(sta.Returns))
curFund := initFund
for _, v := range sta.Returns {
curFund += v
reals = append(reals, curFund)
}
drawRate, _ := utils.CalcDrawDown(reals, opt.ShowNum)
score := opt.CalcBtScore(profitPct, drawRate*100)
scores = append(scores, score)
}
volIdxArr := utils.ArgSortDesc(vols)[:allNum/2]
scoreIdxArr := utils.ArgSortDesc(scores)
volIdMap := utils.ArrToMap(volIdxArr, func(t2 int) int {
return t2
})
offset := int(float64(len(scoreIdxArr)) * offRate)
var result = make([]string, 0, cutNum)
for _, idx := range scoreIdxArr[offset:] {
if _, ok := volIdMap[idx]; ok {
result = append(result, arg.Pairs[idx].Symbol)
if len(result) >= cutNum {
break
}
}
}
return result, nil
}
func sharpe(arg opt.FacArgs) ([]string, error) {
allNum := len(arg.Pairs)
initFund := float64(0)
for _, v := range config.WalletAmounts {
initFund = v
break
}
cutNum := min(int(float64(allNum)/10), 30)
// 只取成交量高的一半品种
volPeriodMS := int64(utils2.TFToSecs("30d") * 1000)
volBarNum := int(volPeriodMS / arg.TFMSecs)
vols := make([]float64, 0, allNum)
scores := make([]float64, 0, allNum)
totOdNum := 0
rangeSecs := (arg.EndMS - arg.StartMS) / 1000
for _, sta := range arg.Pairs {
if len(sta.KLines) == 0 || sta.KLines[0].Time > arg.StartMS {
// 跳过回顾周期内数据不全的品种
vols = append(vols, 0)
scores = append(scores, -99)
continue
}
start := max(0, len(sta.KLines)-volBarNum)
bars := sta.KLines[start:]
volSum := float64(0)
for _, b := range bars {
volSum += b.Volume * b.Close
}
vols = append(vols, volSum)
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 nil, err
}
scores = append(scores, sharp)
totOdNum += len(sta.Orders)
}
validNum := len(vols)
volIdxArr := utils.ArgSortDesc(vols)[:validNum/2]
scoreIdxArr := utils.ArgSortDesc(scores)
volIdMap := utils.ArrToMap(volIdxArr, func(t2 int) int {
return t2
})
avgOdNum := totOdNum / len(arg.Pairs)
var result = make([]string, 0, cutNum)
var raw = make([]string, 0, allNum)
for _, idx := range scoreIdxArr {
//raw[arg.Pairs[idx].Symbol] = scores[idx]
raw = append(raw, arg.Pairs[idx].Symbol)
if _, ok := volIdMap[idx]; ok {
sta := arg.Pairs[idx]
if len(sta.Orders)*4 < avgOdNum {
continue
}
result = append(result, sta.Symbol)
if len(result) >= cutNum {
break
}
}
}
log.Info("by sharpe", zap.Strings("pairs", raw))
return result, nil
}

88
filters/exp_pos.go Normal file
View File

@@ -0,0 +1,88 @@
package filters
import (
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/goods"
"github.com/banbox/banexg/errs"
"github.com/banbox/banexg/log"
"go.uber.org/zap"
"math"
"strings"
)
func init() {
goods.RegisterFilter("ExpPosFilter", func(base goods.BaseFilter) goods.IFilter {
return &ExpPosFilter{
BaseFilter: base,
Exp: 3,
CutRate: 1,
}
})
}
type ExpPosFilter struct {
goods.BaseFilter
Rate float64 `yaml:"rate" mapstructure:"rate,omitempty"`
// 输出的数量
Limit int `yaml:"limit" mapstructure:"limit,omitempty"`
// 偏斜因子 (k)。> 1.0 的值会使分布偏向较小的索引。默认2
Exp float64 `yaml:"exp" mapstructure:"exp,omitempty"`
// 允许的最大索引占总数的百分比 (例如 0.5 表示 50%)默认1
CutRate float64 `yaml:"cut_rate" mapstructure:"cut_rate,omitempty"`
}
func (e *ExpPosFilter) Filter(pairs []string, timeMS int64) ([]string, *errs.Error) {
outNum := float64(len(pairs))
if e.Rate > 0 {
outNum *= e.Rate
}
outNumV := int(outNum)
if e.Limit > 0 && e.Limit < outNumV {
outNumV = e.Limit
}
maxId := float64(len(pairs)) * e.CutRate
outIds := calcRankings(int(maxId), outNumV, e.Exp)
outPairs := make([]string, 0, len(outIds))
hit := make(map[int]bool)
codes := make([]string, 0, len(outIds))
for _, id := range outIds {
if _, ok := hit[id]; ok {
continue
}
hit[id] = true
outPairs = append(outPairs, pairs[id])
base, _, _, _ := core.SplitSymbol(pairs[id])
codes = append(codes, base)
}
dateStr := btime.ToDateStr(btime.TimeMS(), core.DefaultDateFmt)
log.Info("ExpPosFilter", zap.String("date", dateStr), zap.Int("inNum", len(pairs)), zap.Ints("ids", outIds),
zap.String("code", strings.Join(codes, ",")))
return outPairs, nil
}
func calcRankings(totalNum int, pickNum int, skewFactor float64) []int {
if pickNum <= 1 {
return []int{0}
}
rankings := make([]int, pickNum)
for i := 1; i <= pickNum; i++ {
// 2. 将源索引 (i) 归一化到 [0.0, 1.0] 范围
proportion := float64(i) / float64(pickNum)
// 3. 应用幂函数 (k) 来“偏斜”分布
// 这使得 0.1 -> 0.01, 0.5 -> 0.25 (当 k=2.0 时)
// 导致更多早期项目聚集在较低的索引上
skewedProportion := math.Pow(proportion, skewFactor)
// 4. 将偏斜后的比例 [0.0, 1.0] 映射到最终的索引范围 [0, totalNum-1]
finalRank := skewedProportion * float64(totalNum-1)
// 转换为整数索引
rankings[i-1] = int(math.Round(finalRank))
}
return rankings
}

302
flux/grid/common.go Normal file
View File

@@ -0,0 +1,302 @@
package grid
import (
"fmt"
"math"
"slices"
"strconv"
"strings"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
"github.com/banbox/banbot/utils"
"github.com/banbox/banexg/log"
ta "github.com/banbox/banta"
)
/*
CheckPos在每个bar的close上检查位置,是否需要补仓或止盈。
bar中间的high/low忽略。所以尽量在1m周期执行,避免一个bar的high/low突破网格,close又收回来导致未执行的问题
*/
func init() {
strat.AddStratGroup("grid", map[string]strat.FuncMakeStrat{
"inv": InvGrid,
"long": LongGrid,
})
}
func NewGrid(initRate, maxAdd int, slRate float64, debug bool) *Grid {
return &Grid{
InitRate: float64(initRate),
MaxAdd: maxAdd,
SLRate: slRate,
Debug: debug,
}
}
type Grid struct {
InitRate float64 // 初次打开网格投入倍数
MaxAdd int // 最大加仓次数
SLRate float64 // 止损相对Unit的倍数
Dirt float64 // 当前是做多网格还是做空;1做多,-1做空
HoldSize float64 // 持有仓位倍率;相对单位开仓大小
Unit float64 // 网格价格单位
OneAmt float64 // 一份的数量,初次入场时计算
EntPrice float64 // 入场价格
Pos float64 // 网格位置,整数,-6/-5/-4/-3/-2/-1/0/1/2/3/4/5/6
OldPos float64 // 上次网格位置
openNeed bool // 是否需要打开加仓单
takeNeed bool // 是否需要追加止盈单
Debug bool
barLogs []string
barMS int64
}
func (m *Grid) UpdatePos(e *ta.BarEnv, price float64) {
offRaw := (price - m.EntPrice) / m.Unit
rate := math.Floor(math.Abs(offRaw) + 0.1)
if offRaw < 0 {
rate = -rate
}
if rate != m.Pos {
m.Pos = rate
m.takeNeed = true
m.openNeed = true
if m.Debug {
base := m.EntPrice + m.Pos*m.Unit
m.log(e, "upper: %.2f lower: %.2f", base+m.Unit, base-m.Unit)
}
}
}
func (m *Grid) Open(s *strat.StratJob) {
c := s.Env.Close.Get(0)
var tag = "short"
if m.Dirt > 0 {
tag = "long"
}
m.EntPrice = 0
m.Pos = 0
m.OldPos = 0
m.HoldSize = 0
m.OneAmt = s.Strat.GetStakeAmount(s) / c
m.openNeed = true
m.takeNeed = true
s.OpenOrder(&strat.EnterReq{
Tag: tag,
Amount: m.InitRate * m.OneAmt,
Short: m.Dirt < 0,
StopLossVal: m.Unit * m.SLRate,
})
if m.Debug {
m.log(s.Env, "init open, dir: %v, size: %v, grid: %.3f, pct: %.1f",
m.Dirt, m.InitRate, m.Unit, m.Unit*100/c)
}
}
func (m *Grid) CheckPos(s *strat.StratJob) {
e := s.Env
m.UpdatePos(e, e.Close.Get(0))
if !m.openNeed && !m.takeNeed {
return
}
odList := s.GetOrders(m.Dirt)
// 取消未入场的触发订单
cancels := make([]string, 0)
var holds = make(map[string]int)
for _, od := range odList {
if od.Status < ormo.InOutStatusPartEnter {
s.CloseOrders(&strat.ExitReq{Tag: "cancel", OrderID: od.ID, Force: true})
if m.Debug {
cancels = append(cancels, strconv.FormatFloat(od.Enter.Price, 'f', 4, 64))
}
} else if m.Debug {
num := int(math.Round(od.HoldAmount() / m.OneAmt))
row := strconv.FormatFloat(od.Enter.Average, 'f', 2, 64)
old, _ := holds[row]
holds[row] = old + num
}
}
if m.HoldSize < 0.1 {
if m.Debug {
m.log(e, "hold reset\n")
}
s.CloseOrders(&strat.ExitReq{Tag: "clear", Force: true})
return
}
posChg := (m.Pos - m.OldPos) * m.Dirt
m.OldPos = m.Pos
jumpNum := math.Abs(posChg) - 1
var jumpStr string
if jumpNum > 0 {
// 市场剧烈变化,此时最新价格已偏移多个网格
extAmt := m.OneAmt * jumpNum
tag := "jadd"
if posChg > 0 {
// 剧烈盈利,卖出额外持仓
tag = "jexit"
s.CloseOrders(&strat.ExitReq{
Tag: tag,
Amount: extAmt,
FilledOnly: true,
Force: true,
})
} else {
// 剧烈亏损,买入额外持仓
s.OpenOrder(&strat.EnterReq{
Tag: tag,
Amount: extAmt,
Short: m.Dirt < 0,
StopLoss: odList[0].GetStopLoss().Price,
})
}
if m.Debug {
jumpStr = fmt.Sprintf("jump %v %.0f ", tag, jumpNum)
}
}
// 设置最新的止盈止损等待触发
trigPrice := m.EntPrice + m.Pos*m.Unit
openPrice, closePrice := trigPrice-m.Unit, trigPrice+m.Unit
if m.Dirt < 0 {
openPrice, closePrice = closePrice, openPrice
}
var openInfo string
if m.openNeed {
if -m.Pos*m.Dirt >= float64(m.MaxAdd) {
// 达到最大加仓深度
if m.Debug {
openInfo = "open=nil "
}
} else {
sl := odList[0].GetStopLoss()
stopLoss := e.Close.Get(0) - m.Dirt*m.Unit*m.SLRate
if sl != nil {
stopLoss = sl.Price
} else {
log.Info("hit sl nil")
}
s.OpenOrder(&strat.EnterReq{
Tag: "add",
Limit: openPrice,
Amount: m.OneAmt,
Short: m.Dirt < 0,
StopLoss: stopLoss,
})
if m.Debug {
openInfo = fmt.Sprintf("open=%.4f ", openPrice)
}
}
}
m.openNeed = false
if m.takeNeed {
s.SetAllTakeProfit(m.Dirt, &ormo.ExitTrigger{
Price: closePrice,
Rate: 1 / m.HoldSize,
Tag: "exit",
})
//s.CloseOrders(&strat.ExitReq{
// Tag: "exit",
// Limit: closePrice,
// Amount: m.OneAmt,
// Force: true,
//})
}
m.takeNeed = false
if m.Debug {
var takes = make(map[string]int)
for _, od := range odList {
tp := od.GetTakeProfit()
if tp != nil {
tpNum := math.Round(od.HoldAmount() / m.OneAmt)
if tp.Rate > 0 && tp.Rate < 1 {
tpNum *= tp.Rate
}
tpPrice := strconv.FormatFloat(tp.Price, 'f', 2, 64)
old, _ := takes[tpPrice]
takes[tpPrice] = old + int(math.Round(tpNum))
}
}
var cancel string
if len(cancels) > 0 {
cancel = "cancel: [" + strings.Join(cancels, ", ") + "] "
}
hold := printAmts(holds, "hold: ")
take := printAmts(takes, "take: ")
m.log(e, cancel+jumpStr+openInfo+hold+take)
}
}
func (m *Grid) OnOrderChange(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
if chgType == strat.OdChgEnterFill && od.Enter.Filled > core.AmtDust {
// 加仓入场完成事件
e := s.Env
if m.EntPrice == 0 {
// 设置第一个下跌买入和第一个盈利卖出
m.EntPrice = od.Enter.Average
sl := od.GetStopLoss()
stoploss := float64(0)
if sl != nil {
stoploss = sl.Price
}
upgaps := make([]string, 0, 5)
dngaps := make([]string, 0, 5)
for i := 0; i < 5; i++ {
dist := m.Unit * float64(i+1)
upgaps = append(upgaps, strconv.FormatFloat(m.EntPrice+dist, 'f', 4, 64))
dngaps = append(dngaps, strconv.FormatFloat(m.EntPrice-dist, 'f', 4, 64))
}
if m.Debug {
m.log(e, "p=%.4f sl=%.4f", m.EntPrice, stoploss)
m.log(e, "up: %v", strings.Join(upgaps, " "))
m.log(e, "dn: %v", strings.Join(dngaps, " "))
}
}
addNum := math.Round(od.Enter.Filled / m.OneAmt)
m.HoldSize += addNum
m.openNeed = addNum > 0
if m.Debug {
m.log(e, "[enter] %.4f %.0f hold %.0f",
od.Enter.Average, addNum, m.HoldSize)
}
} else if chgType == strat.OdChgExitFill && od.Exit.Filled > core.AmtDust {
e := s.Env
exitNum := math.Round(od.Exit.Filled / m.OneAmt)
m.HoldSize -= exitNum
m.takeNeed = exitNum > 0
if m.Debug {
m.log(e, "[exit] %v %.4f -> %.4f amt %.0f hold %.0f",
od.ExitTag, od.Enter.Average, od.Exit.Average, exitNum, m.HoldSize)
}
}
}
func (m *Grid) log(e *ta.BarEnv, tpl string, args ...any) {
if e.TimeStart > m.barMS {
content := strings.Join(m.barLogs, "\n\t")
fmt.Printf(content)
fmt.Printf("\n")
date := btime.ToDateStr(e.TimeStart, core.DefaultDateFmt)
m.barLogs = []string{date + fmt.Sprintf(" h: %.2f l: %.2f c: %.2f",
e.High.Get(0), e.Low.Get(0), e.Close.Get(0))}
m.barMS = e.TimeStart
}
m.barLogs = append(m.barLogs, fmt.Sprintf(tpl, args...))
}
func printAmts(amts map[string]int, prefix string) string {
if len(amts) == 0 {
return ""
}
ents := utils.KeysOfMap(amts)
slices.Sort(ents)
for i, k := range ents {
if amts[k] > 1 {
ents[i] += " * " + strconv.Itoa(amts[k])
}
}
return prefix + "[" + strings.Join(ents, ", ") + "] "
}

81
flux/grid/inv.go Normal file
View File

@@ -0,0 +1,81 @@
package grid
import (
"math"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
/*
多头网格,下跌买入,上涨卖出。适合波动性较强,方向不明的市场。
以大周期atrBase的一定比例作为网格的单位大小
初始倍率: InitRate
初次开仓条件:
大周期ER20 < 0.3
小周期MA5 < MA20 看多网格;否则看空网格
持有仓位:
*/
type GridV1 struct {
*Grid
bigER float64 // 大周期ER系数
}
func InvGrid(pol *config.RunPolicyConfig) *strat.TradeStrat {
lenAtr := 20
baseAtrLen := int(float64(lenAtr) * 4.3)
unitRate := pol.Def("unitRate", 2, core.PNorm(0.5, 5))
initRate := int(pol.Def("initRate", 4, core.PNorm(0.7, 7.3)))
maxAdd := 5
return &strat.TradeStrat{
WarmupNum: 100,
StopEnterBars: 9999999,
TimeFrames: "1m",
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "4h", 100},
}
},
OnStartUp: func(s *strat.StratJob) {
s.More = &GridV1{Grid: NewGrid(initRate, maxAdd, 6, false)}
},
OnBar: func(s *strat.StratJob) {
m, _ := s.More.(*GridV1)
if s.OrderNum == 0 {
if m.Unit == 0 || m.bigER >= 0.1 || s.IsWarmUp {
return
}
m.Open(s)
} else {
m.CheckPos(s)
}
},
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
m, _ := s.More.(*GridV1)
m.bigER = ta.ER(e.Close, 20).Get(0)
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
ma5 := ta.SMA(e.Close, 10).Get(0)
ma20 := ta.SMA(e.Close, 50).Get(0)
if s.OrderNum == 0 && !math.IsNaN(atrBase) {
// 未持仓时更新网格单元大小
m.Unit = atrBase * unitRate
m.Dirt = core.OdDirtShort
if ma5 < ma20 {
m.Dirt = core.OdDirtLong
}
}
},
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
m, _ := s.More.(*GridV1)
m.OnOrderChange(s, od, chgType)
},
}
}

83
flux/grid/long.go Normal file
View File

@@ -0,0 +1,83 @@
package grid
import (
"math"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
/*
多头网格,下跌买入,上涨卖出。适合波动性较强,方向不明的市场。
以大周期atrBase的一定比例作为网格的单位大小
初始倍率: InitRate
初次开仓条件:
大周期ER20 < 0.3
小周期MA5 < MA20 看多网格;否则看空网格
持有仓位:
*/
type GridLong struct {
*Grid
bigER float64 // 大周期ER系数
}
func LongGrid(pol *config.RunPolicyConfig) *strat.TradeStrat {
lenAtr := 20
baseAtrLen := int(float64(lenAtr) * 4.3)
unitRate := pol.Def("unitRate", 2, core.PNorm(0.5, 5))
initRate := int(pol.Def("initRate", 4, core.PNorm(0.7, 7.3)))
maxAdd := 5
return &strat.TradeStrat{
WarmupNum: 100,
StopEnterBars: 9999999,
TimeFrames: "1m",
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "4h", 100},
}
},
OnStartUp: func(s *strat.StratJob) {
g := NewGrid(initRate, maxAdd, 6, false)
s.More = &GridLong{Grid: g}
},
OnBar: func(s *strat.StratJob) {
m, _ := s.More.(*GridLong)
if s.OrderNum == 0 {
if m.Unit == 0 || s.IsWarmUp {
return
}
m.Open(s)
} else {
m.CheckPos(s)
}
},
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
m, _ := s.More.(*GridLong)
m.bigER = ta.ER(e.Close, 20).Get(0)
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
//ma5 := ta.SMA(e.Close, 10).Get(0)
//ma20 := ta.SMA(e.Close, 50).Get(0)
if s.OrderNum == 0 && !math.IsNaN(atrBase) {
// 未持仓时更新网格单元大小
m.Unit = atrBase * unitRate
m.Dirt = core.OdDirtLong
//m.Dirt = core.OdDirtShort
//if ma5 < ma20 {
// m.Dirt = core.OdDirtLong
//}
}
},
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
m, _ := s.More.(*GridLong)
m.OnOrderChange(s, od, chgType)
},
}
}

147
go.mod Normal file
View File

@@ -0,0 +1,147 @@
module github.com/banbox/banstagy
go 1.24.0
toolchain go1.24.9
replace github.com/banbox/banbot => ../banbot
replace github.com/banbox/banexg => ../banexg
replace github.com/banbox/banta => ../banta
require (
github.com/banbox/banbot v0.2.31
github.com/banbox/banexg v0.2.60
github.com/banbox/banta v0.3.7
github.com/sasha-s/go-deadlock v0.3.6
go.uber.org/zap v1.27.0
)
require (
github.com/anyongjin/go-bayesopt v1.0.2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/banbox/bntp v0.1.0 // indirect
github.com/banbox/cron/v3 v3.1.2 // indirect
github.com/beevik/ntp v1.4.3 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino v0.6.0 // indirect
github.com/cloudwego/eino-ext/components/model/openai v0.1.1 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250918130948-16e3a249e721 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/eino-contrib/jsonschema v1.0.2 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/fasthttp/websocket v1.5.12 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/flopp/go-findfont v0.1.0 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/getkin/kin-openapi v0.118.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect
github.com/go-telegram/bot v1.17.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofiber/contrib/websocket v1.3.4 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/perimeterx/marshmallow v1.1.4 // indirect
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/image v0.32.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.39.1 // indirect
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/c-bata/goptuna v0.9.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/gofiber/fiber/v2 v2.52.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
//github.com/pkujhd/goloader v0.0.0-20240113094056-ff3a1e01ffcb // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/schollz/progressbar/v3 v3.18.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.9 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.64.0 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/excelize/v2 v2.10.0 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
gonum.org/v1/gonum v0.16.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

435
go.sum Normal file
View File

@@ -0,0 +1,435 @@
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anyongjin/go-bayesopt v1.0.2 h1:845NHAdxk4x1FxeOS5McoaWO33ZfE2AR434j9lnEHAM=
github.com/anyongjin/go-bayesopt v1.0.2/go.mod h1:KB64n3O9sPBql6pPnOKh0w8bnOSG1TIMNb+9c/9iQs0=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/banbox/bntp v0.1.0 h1:kZROpbb3OME8XM8QeM+2SeQWQrHRDFN05okr8OITKAU=
github.com/banbox/bntp v0.1.0/go.mod h1:w0WP7GROKQo07wHFBm/FVgH4xlZuOYCoro0ky4iTC3M=
github.com/banbox/cron/v3 v3.1.2 h1:gBNnz/OBxIMU7iBCtObZKhb85rzbaXr8AZLZb40CgeM=
github.com/banbox/cron/v3 v3.1.2/go.mod h1:SZzzT03qsZ1y8/Pt2VqKb9Av/HfnuSargOQRmql7n18=
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/mockey v1.2.14 h1:KZaFgPdiUwW+jOWFieo3Lr7INM1P+6adO3hxZhDswY8=
github.com/bytedance/mockey v1.2.14/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/c-bata/goptuna v0.9.0 h1:JUO13AVxM4YmO9/NMoaNWegfwaM4wAlkrQ+oN/fgfrg=
github.com/c-bata/goptuna v0.9.0/go.mod h1:X2UFBRhqjEI+x2xw/LbaGRaFa/TRBH9w8QjknAaEavo=
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.6.0 h1:pobGKMOfcQHVNhD9UT/HrvO0eYG6FC2ML/NKY2Eb9+Q=
github.com/cloudwego/eino v0.6.0/go.mod h1:JNapfU+QUrFFpboNDrNOFvmz0m9wjBFHHCr77RH6a50=
github.com/cloudwego/eino-ext/components/model/openai v0.1.1 h1:VRdUDcnfi/T8F0jcuovhdADU9Io/oMqiKpY2ZJTBc1o=
github.com/cloudwego/eino-ext/components/model/openai v0.1.1/go.mod h1:VwAXEY1ik2K9KFPZvymnkfBQQKgLHbpg90yg+7hrTt8=
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250918130948-16e3a249e721 h1:5Hd8GxNEmu+ppTGCRBU6kLKfCQNXPMwi31xA83PzEqo=
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250918130948-16e3a249e721/go.mod h1:fHn/6OqPPY1iLLx9wzz+MEVT5Dl9gwuZte1oLEnCoYw=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/eino-contrib/jsonschema v1.0.2 h1:HaxruBMUdnXa7Lg/lX8g0Hk71ZIfdTZXmBQz0e3esr8=
github.com/eino-contrib/jsonschema v1.0.2/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU=
github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM=
github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY=
github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg=
github.com/gofiber/contrib/websocket v1.3.4/go.mod h1:kTFBPC6YENCnKfKx0BoOFjgXxdz7E85/STdkmZPEmPs=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0 h1:nIohpHs1ViKR0SVgW/cbBstHjmnqFZDM9RqgX9m9Xu8=
github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE=
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw=
github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

189
llmjob/128.go Normal file
View File

@@ -0,0 +1,189 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func J128(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
longEntryThreshold := pol.Def("long_entry_threshold", 60.0, core.PNorm(50.0, 70.0))
thresholdRange := pol.Def("threshold_range", 14.0, core.PNorm(10.0, 25.0))
smaPeriod := pol.DefInt("sma_period", 3, core.PNorm(2, 8))
atrPeriod := pol.DefInt("atr_period", 14, core.PNorm(7, 21))
tp1ATR := pol.Def("tp1_atr", 3.0, core.PNorm(1.5, 5.0))
tp2ATR := pol.Def("tp2_atr", 8.0, core.PNorm(5.0, 12.0))
tp3ATR := pol.Def("tp3_atr", 14.0, core.PNorm(10.0, 20.0))
tp1Percent := pol.Def("tp1_percent", 12.0, core.PNorm(5.0, 25.0))
tp2Percent := pol.Def("tp2_percent", 12.0, core.PNorm(5.0, 25.0))
tp3Percent := pol.Def("tp3_percent", 12.0, core.PNorm(5.0, 25.0))
useTakeProfit := pol.Def("use_take_profit", 0.0, core.PNorm(0.0, 1.0)) // 0=false, 1=true
tradingDirection := pol.Def("trading_direction", 2.0, core.PNorm(0.0, 2.0)) // 0=Long, 1=Short, 2=Both
// 计算衍生参数
shortEntryThreshold := longEntryThreshold - thresholdRange
longExitThreshold := shortEntryThreshold
shortExitThreshold := longEntryThreshold
// 确定最长预热周期
warmupNum := 55 + smaPeriod + 10
return &strat.TradeStrat{
Name: "FATR",
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1,
EachMaxShort: 1,
RunTimeFrames: []string{"4h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算真实高低点和真实波动范围
trueLow := e.Low.Min(e.Close.Back(1))
trueHigh := e.High.Max(e.Close.Back(1))
trueRange := trueHigh.Sub(trueLow)
// 计算买入压力(BP)
bp := e.Close.Sub(trueLow)
// 计算不同周期的压力比率
calcRatio := func(len int) *ta.Series {
sumBP := ta.Sum(bp, len)
sumTR := ta.Sum(trueRange, len)
return sumBP.Div(sumTR).Mul(100)
}
// 计算Fibonacci数列周期的压力比率
ratio8 := calcRatio(8)
ratio13 := calcRatio(13)
ratio21 := calcRatio(21)
ratio34 := calcRatio(34)
ratio55 := calcRatio(55)
// 计算加权平均值
weightedAvg := ratio8.Mul(5).Add(
ratio13.Mul(4)).Add(
ratio21.Mul(3)).Add(
ratio34.Mul(2)).Add(
ratio55).Div(15)
// 应用SMA平滑
weightedAvgSMA := ta.SMA(weightedAvg, smaPeriod)
// 计算ATR值
atrValue := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
// 获取当前值
atrVal := atrValue.Get(0)
closePrice := e.Close.Get(0)
// 生成交易信号
longCondition := weightedAvgSMA.Cross(longEntryThreshold) == 1
shortCondition := weightedAvgSMA.Cross(shortEntryThreshold) == -1
longExit := weightedAvgSMA.Cross(longExitThreshold) == -1
shortExit := weightedAvgSMA.Cross(shortExitThreshold) == 1
// 执行交易逻辑
isLongEnabled := tradingDirection == 0 || tradingDirection == 2
isShortEnabled := tradingDirection == 1 || tradingDirection == 2
// 多头交易逻辑
if isLongEnabled {
if longCondition && len(s.LongOrders) == 0 {
// 开多单
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
})
// 设置止盈订单
if useTakeProfit > 0.5 {
tpPrice1 := closePrice + tp1ATR*atrVal
tpPrice2 := closePrice + tp2ATR*atrVal
tpPrice3 := closePrice + tp3ATR*atrVal
// 设置多个止盈订单
s.CloseOrders(&strat.ExitReq{
Tag: "TP1_Long",
Dirt: core.OdDirtLong,
ExitRate: tp1Percent / 100,
Limit: tpPrice1,
})
s.CloseOrders(&strat.ExitReq{
Tag: "TP2_Long",
Dirt: core.OdDirtLong,
ExitRate: tp2Percent / 100,
Limit: tpPrice2,
})
s.CloseOrders(&strat.ExitReq{
Tag: "TP3_Long",
Dirt: core.OdDirtLong,
ExitRate: tp3Percent / 100,
Limit: tpPrice3,
})
}
}
if longExit && len(s.LongOrders) > 0 {
// 平多单
s.CloseOrders(&strat.ExitReq{
Tag: "Long_Exit",
Dirt: core.OdDirtLong,
})
}
}
// 空头交易逻辑
if isShortEnabled {
if shortCondition && len(s.ShortOrders) == 0 {
// 开空单
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
})
// 设置止盈订单
if useTakeProfit > 0.5 {
tpPrice1 := closePrice - tp1ATR*atrVal
tpPrice2 := closePrice - tp2ATR*atrVal
tpPrice3 := closePrice - tp3ATR*atrVal
// 设置多个止盈订单
s.CloseOrders(&strat.ExitReq{
Tag: "TP1_Short",
Dirt: core.OdDirtShort,
ExitRate: tp1Percent / 100,
Limit: tpPrice1,
})
s.CloseOrders(&strat.ExitReq{
Tag: "TP2_Short",
Dirt: core.OdDirtShort,
ExitRate: tp2Percent / 100,
Limit: tpPrice2,
})
s.CloseOrders(&strat.ExitReq{
Tag: "TP3_Short",
Dirt: core.OdDirtShort,
ExitRate: tp3Percent / 100,
Limit: tpPrice3,
})
}
}
if shortExit && len(s.ShortOrders) > 0 {
// 平空单
s.CloseOrders(&strat.ExitReq{
Tag: "Short_Exit",
Dirt: core.OdDirtShort,
})
}
}
},
}
}

59
llmjob/1357.go Normal file
View File

@@ -0,0 +1,59 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func J1357(pol *config.RunPolicyConfig) *strat.TradeStrat {
fastLength := pol.DefInt("fastLength", 16, core.PNorm(5, 20))
slowLength := pol.DefInt("slowLength", 26, core.PNorm(15, 50))
signalLength := pol.DefInt("signalLength", 9, core.PNorm(5, 15))
return &strat.TradeStrat{
WarmupNum: slowLength + signalLength,
EachMaxLong: 3,
EachMaxShort: 3,
HedgeOff: true,
RunTimeFrames: []string{"8h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算MACD指标
macd, signal := ta.MACD(e.Close, fastLength, slowLength, signalLength)
// 检测MACD线与信号线的交叉
buySignal := macd.Cross(signal)
sellSignal := signal.Cross(macd)
// 买入信号:MACD线上穿信号线
if buySignal == 1 {
// 开多单
s.OpenOrder(&strat.EnterReq{
Tag: "Long Trigger",
})
// 平空单
s.CloseOrders(&strat.ExitReq{
Tag: "Close Short Trigger",
Dirt: core.OdDirtShort,
})
}
// 卖出信号:MACD线下穿信号线
if sellSignal == 1 {
// 开空单
s.OpenOrder(&strat.EnterReq{
Tag: "Short Trigger",
Short: true,
})
// 平多单
s.CloseOrders(&strat.ExitReq{
Tag: "Close Long Trigger",
Dirt: core.OdDirtLong,
})
}
},
}
}

525
llmjob/1525.go Normal file
View File

@@ -0,0 +1,525 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"fmt"
"github.com/banbox/banbot/btime"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
ta "github.com/banbox/banta"
"github.com/sasha-s/go-deadlock"
"go.uber.org/zap"
"math"
)
/*
根据AtrBase基于不同品种权重,限制范围后更好
从4h根据行情自动调整加仓阈值和止盈阈值后:
之前:夏普0.93,显示回撤35.8%,最大回撤95.4%,最大资金占用3.2W,总收益119.6%
之后:夏普1.38,显示回撤5.66%,最大回撤36.2%,最大资金占用1.64W,总收益79.4%
尝试使用公式0.383*pow(1+target_qty, x)加仓,符合测试的数据,但效果更差
*/
// State1525S 存储每个品种的策略状态
type State1525S struct {
avgPrice float64 // 平均持仓成本
positionQty float64 // 当前持仓数量
tpLimit float64
score float64 // 下跌行情力度分数,值域[0,6]
}
func J1525(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
smaPeriod := pol.DefInt("sma_period", 18, core.PNorm(5, 50))
targetPercRaw := pol.Def("target_perc", -0.14, core.PNorm(-0.3, -0.05))
takeProfitRaw := pol.Def("take_profit", 0.09, core.PNorm(0.05, 0.3))
targetQty := pol.Def("target_qty", 0.65, core.PNorm(0.2, 1.0))
// 加仓次数,超过20后变差;低于19后有段时间不稳定
maxAdd := pol.DefInt("max_add", 19, core.PNorm(1, 30))
maxOpenPair := pol.DefInt("max_open_pair", 25, core.PNorm(1, 30)) // 最大同时交易品种
openPairs := make(map[*strat.StratJob]int)
pairLock := deadlock.Mutex{}
lenAtr := 20
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
targetPerc := targetPercRaw
takeProfit := takeProfitRaw
printMS := int64(0)
maxPairs := 0
// 加仓倍率,基于targetQty: 1, 0.6, 1 1.7 3 4.7 7.7 12.8 21.1 35 57.5 95 157 256 385 464 767
return &strat.TradeStrat{
WarmupNum: max(smaPeriod, baseAtrLen),
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"5m"},
BatchInfo: true,
OrderOnRotation: "open",
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
s.More = &State1525S{}
},
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "4h", 20},
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*State1525S)
closePrice := e.Close.Get(0)
// 不利行情下调整参数
targetPercJob, takeProfitJob := targetPercRaw, takeProfitRaw
if state.score >= 5 {
targetPercJob = targetPercRaw * 2
takeProfitJob = takeProfitRaw * 0.6
} else if state.score >= 4 {
targetPercJob = targetPercRaw * 1.7
takeProfitJob = takeProfitRaw * 0.7
} else if state.score >= 3 {
targetPercJob = targetPercRaw * 1.3
takeProfitJob = takeProfitRaw * 0.8
}
targetPercCur := (targetPercJob + targetPerc) / 2
takeProfitCur := (takeProfitJob + takeProfit) / 2
// 计算SMA
ma := ta.SMA(e.Close, smaPeriod)
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
openRate := 0.006 * closePrice / atrBase
if openRate < 1 {
openRate = math.Sqrt(openRate)
}
openRate = max(0.7, min(1.5, openRate))
// 计算当前持仓盈亏比例
var pnl float64
if state.positionQty > 0 && state.avgPrice > 0 {
pnl = (closePrice - state.avgPrice) / state.avgPrice
}
// 计算止盈价格
if state.positionQty > 0 && state.avgPrice > 0 {
curTakeProfit := state.avgPrice * (1 + takeProfitCur)
if curTakeProfit != state.tpLimit {
state.tpLimit = curTakeProfit
// 设置限价单止盈
s.CloseOrders(&strat.ExitReq{
Tag: "Take Profit",
Limit: state.tpLimit,
})
}
}
// 首次入场条件:价格上穿SMA且无持仓
if e.Close.Cross(ma) == 1 && state.positionQty == 0 {
pairLock.Lock()
_, allow := openPairs[s]
if !allow && len(openPairs) < maxOpenPair {
openPairs[s] = 1
maxPairs = max(maxPairs, len(openPairs))
allow = true
}
pairLock.Unlock()
if allow {
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
CostRate: openRate,
})
}
}
// 加仓条件:亏损达到目标比例且有持仓
if state.positionQty > 0 && pnl <= targetPercCur && len(s.LongOrders) < maxAdd {
// 计算加仓数量,确保是整数
odNum := len(s.LongOrders)
addQty := math.Floor(state.positionQty * targetQty)
pairLock.Lock()
openPairs[s] = odNum
pairLock.Unlock()
s.OpenOrder(&strat.EnterReq{
Tag: fmt.Sprintf("%v", odNum),
Amount: addQty,
})
if odNum > 5 {
printMS = e.TimeStop
}
}
},
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
score := float64(0)
if ta.RSI(e.Close, 14).Get(0) < 30 {
score += 1
}
roc := ta.ROC(e.Close, 20).Get(0)
if roc < -20 {
score += 2
} else if roc < -10 {
score += 1
}
plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14)
adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0)
if minusDi.Get(0) > plusDi.Get(0) {
if adx > 40 {
score += 2
} else if adx > 25 {
score += 1
}
}
ma25 := ta.SMA(e.Close, 25).Get(0)
ma99 := ta.SMA(e.Close, 99).Get(0)
if ma99 > ma25 {
score += 1
}
state, _ := s.More.(*State1525S)
state.score = score
},
OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) {
jobNum := len(jobs)
if jobNum < 3 {
return
}
downNum, downHeavyNum := float64(0), float64(0)
timeStop := int64(0)
var last *strat.JobEnv
for _, j := range jobs {
state, _ := j.Job.More.(*State1525S)
if state.score >= 5 {
downHeavyNum += 1
}
if state.score >= 3 {
downNum += 1
}
timeStop = j.Env.TimeStop
last = j
}
downRate := downNum / float64(len(jobs))
downHeavyRate := downHeavyNum / float64(len(jobs))
if printMS > 0 {
printMS = 0
dateStr := btime.ToDateStr(timeStop, core.DefaultDateFmt)
fmt.Printf("%v downRate: %.3f downHeavyRate: %.3f\n", dateStr, downRate*100, downHeavyRate*100)
}
if downHeavyRate >= 0.8 {
// 极端下跌
targetPerc = targetPercRaw * 2
takeProfit = takeProfitRaw * 0.5
} else if downRate >= 0.85 && downHeavyRate >= 0.6 {
// 严重下跌
targetPerc = targetPercRaw * 1.8
takeProfit = takeProfitRaw * 0.5
} else if downRate >= 0.7 && downRate <= 0.85 {
// 市场中度下跌
targetPerc = targetPercRaw * 1.6
takeProfit = takeProfitRaw * 0.65
} else if downRate >= 0.5 && downRate <= 0.7 {
// 轻度下跌
targetPerc = targetPercRaw * 1.3
takeProfit = takeProfitRaw * 0.8
} else {
// 轻度或未下跌
targetPerc = targetPercRaw
takeProfit = takeProfitRaw
}
pairLock.Lock()
totNum := 0
for _, num := range openPairs {
totNum += num
}
pairLock.Unlock()
if totNum > 0 {
last.Job.Strat.WriteOutput(fmt.Sprintf("pairNum: %v", totNum), true)
}
},
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
state, _ := s.More.(*State1525S)
// 当订单入场时更新持仓状态
if chgType == strat.OdChgEnterFill && od.EnterTag != "" {
// 计算新的平均成本
if state.positionQty == 0 {
// 首次开仓
state.avgPrice = od.InitPrice
state.positionQty = od.HoldAmount()
} else {
// 加仓,更新平均成本
oldQty := state.positionQty
oldAvgPrice := state.avgPrice
newQty := od.HoldAmount()
newAvgPrice := od.InitPrice
// 计算新的平均成本
totalQty := oldQty + newQty
totalCost := oldQty*oldAvgPrice + newQty*newAvgPrice
state.avgPrice = totalCost / totalQty
state.positionQty = totalQty
}
}
// 当订单完全平仓时重置持仓状态
if chgType == strat.OdChgExitFill && od.ExitTag != "" && od.Status == ormo.InOutStatusFullExit {
state.avgPrice = 0
state.positionQty = 0
state.tpLimit = 0
pairLock.Lock()
delete(openPairs, s)
pairLock.Unlock()
}
},
OnShutDown: func(s *strat.StratJob) {
if maxPairs == 0 {
return
}
log.Info("max open pairs", zap.Int("num", maxPairs))
maxPairs = 0
},
}
}
// State1525Sim 存储每个品种的策略状态
type State1525Sim struct {
totAmount float64
totCost float64
tpLimit float64 // 止盈出场价格
odNum int
upFlag float64 // 1: ma20 > ma99 && ma20 > ma20[2]
score float64 // 下跌行情力度分数,值域[0,6]
}
func J1525Sim(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 最多30个品种,单笔30,初始3W
// 定义可调超参数
smaPeriod := pol.DefInt("sma_period", 18, core.PNorm(5, 50))
targetPercRaw := pol.Def("target_perc", -0.14, core.PNorm(-0.3, -0.05))
takeProfitRaw := pol.Def("take_profit", 0.09, core.PNorm(0.05, 0.3))
targetQty := pol.Def("target_qty", 0.6, core.PNorm(0.2, 1.0))
// 加仓次数,超过20后变差;低于19后有段时间不稳定
maxAdd := pol.DefInt("max_add", 19, core.PNorm(1, 30))
startLvlRaw := pol.DefInt("start_lvl", 3, core.PNorm(1, 15))
maxOpenPair := pol.DefInt("max_open_pair", 20, core.PNorm(1, 30)) // 最大同时交易品种
openPairs := make(map[*strat.StratJob]int)
pairLock := deadlock.Mutex{}
lenAtr := 20
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
targetPerc := targetPercRaw
takeProfit := takeProfitRaw
startLvl := startLvlRaw
// 加仓倍率,基于targetQty: 1, 0.6, 1 1.7 3 4.7 7.7 12.8 21.1 35 57.5 95 157 256 385 464 767
return &strat.TradeStrat{
WarmupNum: max(smaPeriod, baseAtrLen),
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"5m"},
BatchInfo: true,
StakeRate: 0.25,
OrderOnRotation: "open", // 品种切换时允许开单
OnStartUp: func(s *strat.StratJob) {
s.More = &State1525Sim{}
},
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "4h", 20},
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*State1525Sim)
closePrice := e.Close.Get(0)
// 不利行情下调整参数
targetPercJob, takeProfitJob := targetPercRaw, takeProfitRaw
if state.score >= 5 {
targetPercJob = targetPercRaw * 2
takeProfitJob = takeProfitRaw * 0.6
} else if state.score >= 4 {
targetPercJob = targetPercRaw * 1.7
takeProfitJob = takeProfitRaw * 0.7
} else if state.score >= 3 {
targetPercJob = targetPercRaw * 1.3
takeProfitJob = takeProfitRaw * 0.8
}
targetPercCur := (targetPercJob + targetPerc) / 2
takeProfitCur := (takeProfitJob + takeProfit) / 2
// 计算SMA
ma := ta.SMA(e.Close, smaPeriod)
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
openRate := 0.006 * closePrice / atrBase
if openRate < 1 {
openRate = math.Sqrt(openRate)
}
openRate = max(0.7, min(1.5, openRate))
// 检查止盈触发
if state.tpLimit > 0 && state.tpLimit <= e.High.Get(0) {
state.totAmount = 0
state.totCost = 0
state.tpLimit = 0
state.odNum = 0
pairLock.Lock()
delete(openPairs, s)
pairLock.Unlock()
}
// 计算当前持仓盈亏比例、止盈
var pnl, avgPrice float64
if state.totAmount > 0 {
avgPrice = state.totCost / state.totAmount
pnl = closePrice/avgPrice - 1
curTP := avgPrice * (1 + takeProfitCur)
if curTP != state.tpLimit {
state.tpLimit = curTP
// 设置限价单止盈
s.CloseOrders(&strat.ExitReq{
Tag: "Take Profit",
Limit: curTP,
})
}
}
// 首次入场条件:价格上穿SMA且无持仓
if e.Close.Cross(ma) == 1 && state.totAmount == 0 && openRate > 0 {
state.odNum = 1
state.totAmount = openRate
state.totCost = closePrice * openRate
}
// 加仓条件:亏损达到目标比例且有持仓
if avgPrice > 0 && pnl <= targetPercCur && state.odNum < maxAdd {
amt := state.totAmount * targetQty
state.totAmount += amt
state.totCost += amt * closePrice
curStart := startLvl
if state.upFlag < 0 {
// 下降趋势上升3个阶梯,和全局阈值取平均
curStart = (startLvl + startLvlRaw + 3) / 2
}
if state.odNum >= curStart {
pairLock.Lock()
_, allow := openPairs[s]
if !allow && len(openPairs) < maxOpenPair {
allow = true
}
if allow {
openPairs[s] = len(s.LongOrders)
}
pairLock.Unlock()
if allow {
s.OpenOrder(&strat.EnterReq{
Tag: fmt.Sprintf("%v", state.odNum),
CostRate: amt,
})
}
}
state.odNum += 1
}
},
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
score := float64(0)
if ta.RSI(e.Close, 14).Get(0) < 30 {
score += 1
}
roc := ta.ROC(e.Close, 20).Get(0)
if roc < -20 {
score += 2
} else if roc < -10 {
score += 1
}
plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14)
adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0)
if minusDi.Get(0) > plusDi.Get(0) {
if adx > 40 {
score += 2
} else if adx > 25 {
score += 1
}
}
ma7 := ta.SMA(e.Close, 7).Get(0)
ma20 := ta.SMA(e.Close, 20)
ma20V := ma20.Get(0)
ma99 := ta.SMA(e.Close, 99).Get(0)
if ma99 > ma20V {
score += 1
}
state, _ := s.More.(*State1525Sim)
state.score = score
state.upFlag = 0
if ma20V > ma99 && ma20V > ma20.Get(2) {
state.upFlag = 1
} else if ma7 < min(ma20V, ma99) {
state.upFlag = -1
}
},
OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) {
jobNum := len(jobs)
if jobNum < 3 {
return
}
downNum, downHeavyNum, sumUp := float64(0), float64(0), float64(0)
var last *strat.JobEnv
for _, j := range jobs {
state, _ := j.Job.More.(*State1525Sim)
if state.score >= 5 {
downHeavyNum += 1
}
if state.score >= 3 {
downNum += 1
}
sumUp += max(0, state.upFlag)
last = j
}
downRate := downNum / float64(len(jobs))
downHeavyRate := downHeavyNum / float64(len(jobs))
sumUp /= float64(len(jobs))
if downHeavyRate >= 0.8 {
// 极端下跌
targetPerc = targetPercRaw * 2
takeProfit = takeProfitRaw * 0.5
} else if downRate >= 0.85 && downHeavyRate >= 0.6 {
// 严重下跌
targetPerc = targetPercRaw * 1.8
takeProfit = takeProfitRaw * 0.5
} else if downRate >= 0.7 && downRate <= 0.85 {
// 市场中度下跌
targetPerc = targetPercRaw * 1.6
takeProfit = takeProfitRaw * 0.65
} else if downRate >= 0.5 && downRate <= 0.7 {
// 轻度下跌
targetPerc = targetPercRaw * 1.3
takeProfit = takeProfitRaw * 0.8
} else {
// 轻度或未下跌
targetPerc = targetPercRaw
takeProfit = takeProfitRaw
}
// 调整最小开仓级别
if sumUp > 0.7 {
startLvl = startLvlRaw
} else if sumUp > 0.5 && downRate <= 0.5 {
startLvl = startLvlRaw + 2
} else if sumUp > 0.3 && downRate <= 0.7 {
startLvl = startLvlRaw + 4
} else {
startLvl = startLvlRaw + 6
}
pairLock.Lock()
totNum := 0
for _, num := range openPairs {
totNum += num
}
pairLock.Unlock()
if totNum > 0 {
last.Job.Strat.WriteOutput(fmt.Sprintf("pairNum: %v", totNum), true)
}
},
}
}

105
llmjob/1569.go Normal file
View File

@@ -0,0 +1,105 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func J1569(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
fastPeriod := pol.DefInt("fastPeriod", 10, core.PNorm(5, 20))
smaRatio := pol.Def("smaRatio", 2.0, core.PNorm(1.5, 4.0))
supportResistancePeriod := pol.DefInt("supportResistancePeriod", 12, core.PNorm(5, 20))
confirmPeriod := pol.DefInt("confirmPeriod", 3, core.PNorm(2, 5))
riskRewardRatio := pol.Def("riskRewardRatio", 2.2, core.PNorm(1.5, 3.0))
// 计算慢速SMA周期
slowPeriod := int(float64(fastPeriod) * smaRatio)
// 确保预热K线数量足够计算所有指标
warmupNum := slowPeriod
if supportResistancePeriod > warmupNum {
warmupNum = supportResistancePeriod
}
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多1个多头订单
EachMaxShort: 1, // 最多1个空头订单
RunTimeFrames: []string{"1h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算移动平均线
smaFast := ta.SMA(e.Close, fastPeriod)
smaSlow := ta.SMA(e.Close, slowPeriod)
// 计算支撑阻力位(过去10根K线的最高价和最低价)
recentLow := ta.Lowest(e.Low, supportResistancePeriod)
recentHigh := ta.Highest(e.High, supportResistancePeriod)
// 计算确认条件所需的指标
highestSmaSlow := ta.Highest(smaSlow, confirmPeriod)
lowestSmaSlow := ta.Lowest(smaSlow, confirmPeriod)
lowestLow3 := ta.Lowest(e.Low, confirmPeriod)
highestHigh3 := ta.Highest(e.High, confirmPeriod)
// 获取当前值
closePrice := e.Close.Get(0)
smaSlowVal := smaSlow.Get(0)
recentLowVal := recentLow.Get(0)
recentHighVal := recentHigh.Get(0)
highestSmaSlowVal := highestSmaSlow.Get(0)
lowestSmaSlowVal := lowestSmaSlow.Get(0)
lowestLow3Val := lowestLow3.Get(0)
highestHigh3Val := highestHigh3.Get(0)
// 生成交易信号 - 只计算一次交叉信号
crossSignal := smaFast.Cross(smaSlow)
bullishCross := crossSignal == 1 // 10周期SMA上穿20周期SMA
bearishCross := crossSignal == -1 // 10周期SMA下穿20周期SMA
breakoutUp := closePrice > highestSmaSlowVal // 收盘价突破过去3根K线的20周期SMA最高点
breakoutDown := closePrice < lowestSmaSlowVal // 收盘价跌破过去3根K线的20周期SMA最低点
retestUp := lowestLow3Val > smaSlowVal // 过去3根K线的最低价保持在20周期SMA之上
retestDown := highestHigh3Val < smaSlowVal // 过去3根K线的最高价保持在20周期SMA之下
// 三重确认条件
buyCondition := bullishCross && breakoutUp && retestUp
sellCondition := bearishCross && breakoutDown && retestDown
// 计算止损止盈
longSL := recentLowVal // 买入止损设为过去10根K线的最低价
shortSL := recentHighVal // 卖出止损设为过去10根K线的最高价
riskSizeLong := closePrice - longSL // 买入交易的风险大小
riskSizeShort := shortSL - closePrice // 卖出交易的风险大小
longTP := closePrice + (riskSizeLong * riskRewardRatio) // 买入交易的止盈价格
shortTP := closePrice - (riskSizeShort * riskRewardRatio) // 卖出交易的止盈价格
// 执行交易
if buyCondition && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
OrderType: core.OrderTypeMarket,
StopLoss: longSL,
TakeProfit: longTP,
})
}
if sellCondition && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
OrderType: core.OrderTypeMarket,
StopLoss: shortSL,
TakeProfit: shortTP,
})
}
},
}
}

197
llmjob/1662.go Normal file
View File

@@ -0,0 +1,197 @@
// 修复后的完整策略代码
// 请勿修改package包名和任意函数名
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
/*
因止盈止损触发价未检查是否合理的bug导致虚假净值曲线
*/
// SuperTrendData 存储每个品种的Supertrend相关数据
type SuperTrendData struct {
trend int // 当前趋势方向,1表示上升趋势,-1表示下降趋势
longTrailStop float64 // 多头仓位的移动止损价格
shortTrailStop float64 // 空头仓位的移动止损价格
up float64 // 上轨值
dn float64 // 下轨值
}
func J1662(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
atrPeriod := pol.DefInt("atrPeriod", 8, core.PNorm(5, 30))
atrMultiplier := pol.Def("atrMultiplier", 2.5, core.PNorm(1.5, 5.0))
lenAtr := 20
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
return &strat.TradeStrat{
WarmupNum: baseAtrLen, // 预热K线数量,确保有足够数据计算指标
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的Supertrend数据
s.More = &SuperTrendData{
trend: 1, // 初始趋势设为上升
longTrailStop: math.NaN(),
shortTrailStop: math.NaN(),
up: math.NaN(),
dn: math.NaN(),
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
data, _ := s.More.(*SuperTrendData)
// 计算数据源 (hl2 = (high + low) / 2)
src := ta.HL2(e.High, e.Low)
// 计算ATR值
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
// 计算Supertrend指标
upVal := src.Get(0) - atr.Get(0)*atrMultiplier
dnVal := src.Get(0) + atr.Get(0)*atrMultiplier
atr2 := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr2, lenAtr), baseAtrLen).Get(0)
// 获取历史值
up1 := data.up
if math.IsNaN(up1) {
up1 = upVal
}
dn1 := data.dn
if math.IsNaN(dn1) {
dn1 = dnVal
}
// 更新上轨和下轨
close1 := e.Close.Get(1)
if !math.IsNaN(close1) {
if close1 > up1 {
upVal = math.Max(upVal, up1)
}
if close1 < dn1 {
dnVal = math.Min(dnVal, dn1)
}
}
// 保存当前上下轨值
data.up = upVal
data.dn = dnVal
// 判断趋势方向
prevTrend := data.trend
closePrice := e.Close.Get(0)
if !math.IsNaN(closePrice) && !math.IsNaN(dn1) && !math.IsNaN(up1) && !math.IsNaN(atrBase) {
if data.trend == -1 && closePrice > dn1 {
data.trend = 1 // 趋势由下降转为上升
} else if data.trend == 1 && closePrice < up1 {
data.trend = -1 // 趋势由上升转为下降
}
}
// 生成交易信号
buySignal := data.trend == 1 && prevTrend == -1
sellSignal := data.trend == -1 && prevTrend == 1
// 执行交易策略
if buySignal {
// 开仓多头,使用下轨作为触发价格
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Stop: dnVal,
Log: true,
})
s.CloseOrders(&strat.ExitReq{
Tag: "XShort",
Dirt: core.OdDirtShort,
Log: true,
})
}
if sellSignal {
// 开仓空头,使用上轨作为触发价格
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
Stop: upVal,
Log: true,
})
s.CloseOrders(&strat.ExitReq{
Tag: "XLong",
Dirt: core.OdDirtLong,
Log: true,
})
}
pipValue := atrBase * 0
trailingValue := atrBase * 0
// 移动止损逻辑
if s.OrderNum > 0 {
// 多头移动止损
if len(s.LongOrders) > 0 {
// 获取持仓均价
avgPrice := s.PositionAvgPrice(core.OdDirtLong, "")
if avgPrice > 0 {
if math.IsNaN(data.longTrailStop) || closePrice > avgPrice+trailingValue {
data.longTrailStop = e.High.Get(0) - trailingValue
}
// 计算初始止损
longStopLevel := upVal - pipValue
// 使用更严格的止损(更接近当前价格的止损)
finalStop := math.Max(data.longTrailStop, longStopLevel)
if finalStop >= closePrice {
s.CloseOrders(&strat.ExitReq{
Tag: "SLLong",
Dirt: core.OdDirtLong,
})
} else {
s.SetAllStopLoss(core.OdDirtLong, &ormo.ExitTrigger{
Price: finalStop,
Tag: "sl",
})
}
}
}
// 空头移动止损
if len(s.ShortOrders) > 0 {
// 获取持仓均价
avgPrice := s.PositionAvgPrice(core.OdDirtShort, "")
if avgPrice > 0 {
if math.IsNaN(data.shortTrailStop) || closePrice < avgPrice-trailingValue {
data.shortTrailStop = e.Low.Get(0) + trailingValue
}
// 计算初始止损
shortStopLevel := dnVal + pipValue
// 使用更严格的止损(更接近当前价格的止损)
finalStop := math.Min(data.shortTrailStop, shortStopLevel)
if finalStop < closePrice {
s.CloseOrders(&strat.ExitReq{
Tag: "SLShort",
Dirt: core.OdDirtShort,
})
} else {
s.SetAllStopLoss(core.OdDirtShort, &ormo.ExitTrigger{
Price: finalStop,
Tag: "sl",
})
}
}
}
}
},
}
}

278
llmjob/1665.go Normal file
View File

@@ -0,0 +1,278 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
// StrategyState 保存每个品种的交易状态
type StrategyState struct {
isLong bool // 当前是否持有多头仓位
isShort bool // 当前是否持有空头仓位
partialTpTriggered bool // 第一止盈是否已触发
stopPrice float64 // 止损价格
firstTpPrice float64 // 第一止盈价格
secondTpPrice float64 // 第二止盈价格
firstTpQty float64 // 第一止盈数量
positionSize float64 // 仓位大小
entryPrice float64 // 入场价格
}
func J1665(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
fastLength := pol.DefInt("fastLength", 5, core.PNorm(3, 15))
emaRate := pol.Def("emaRate", 3.0, core.PNorm(1.5, 6.0))
rsiLength := pol.DefInt("rsiLength", 7, core.PNorm(3, 21))
atrPeriod := pol.DefInt("atrPeriod", 10, core.PNorm(5, 20))
stopLossRate := pol.Def("stopLossRate", 5.0, core.PNorm(2.0, 10.0))
firstTakeProfitRate := pol.Def("firstTakeProfitRate", 8.0, core.PNorm(4.0, 16.0))
secondTakeProfitRate := pol.Def("secondTakeProfitRate", 12.0, core.PNorm(6.0, 24.0))
firstTakeProfitQtyRate := pol.Def("firstTakeProfitQtyRate", 50.0, core.PNorm(25.0, 75.0))
leverageRate := pol.Def("leverageRate", 3.0, core.PNorm(1.0, 5.0))
// 计算慢速EMA长度
slowLength := int(float64(fastLength) * emaRate)
return &strat.TradeStrat{
WarmupNum: slowLength + 26, // 确保有足够数据计算所有指标
HedgeOff: true, // 单向持仓
EachMaxLong: 1, // 最多一个多单
EachMaxShort: 1, // 最多一个空单
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
s.More = &StrategyState{
isLong: false,
isShort: false,
partialTpTriggered: false,
stopPrice: math.NaN(),
firstTpPrice: math.NaN(),
secondTpPrice: math.NaN(),
firstTpQty: math.NaN(),
positionSize: math.NaN(),
entryPrice: math.NaN(),
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*StrategyState)
// 计算技术指标
fastEMA := ta.EMA(e.Close, fastLength)
slowEMA := ta.EMA(e.Close, slowLength)
rsi := ta.RSI(e.Close, rsiLength)
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
macd, signal := ta.MACD(e.Close, 12, 26, 9)
// 获取当前值
closePrice := e.Close.Get(0)
prevClosePrice := e.Close.Get(1)
highPrice := e.High.Get(0)
lowPrice := e.Low.Get(0)
if math.IsNaN(closePrice) {
return
}
// 检测MACD交叉
macdCross := macd.Cross(signal)
macdGolden := macdCross == 1 // MACD金叉
macdDeath := macdCross == -1 // MACD死叉
// 生成交易信号
longCondition := fastEMA.Get(0) > slowEMA.Get(0) && macdGolden && rsi.Get(0) < 70
shortCondition := fastEMA.Get(0) < slowEMA.Get(0) && macdDeath && rsi.Get(0) > 30
// 平仓信号
exitLong := shortCondition || fastEMA.Get(0) < slowEMA.Get(0)
exitShort := longCondition || fastEMA.Get(0) > slowEMA.Get(0)
// 计算仓位大小
if !math.IsNaN(atr.Get(0)) && atr.Get(0) > 0 {
maxRiskPerTrade := 0.05
// 修复错误1:使用动态账户权益而非固定值1000
stakeAmount := s.Strat.GetStakeAmount(s)
basePosition := stakeAmount * maxRiskPerTrade
atrAmount := atr.Get(0) * closePrice
riskPosition := basePosition / atrAmount * leverageRate
maxPosition := stakeAmount * 0.4 / closePrice
state.positionSize = math.Min(riskPosition, maxPosition)
// 修复错误6:将firstTpQty的初始化移到仓位计算函数内部
state.firstTpQty = state.positionSize * (firstTakeProfitQtyRate / 100.0)
}
// 多头入场
if longCondition && !state.isLong && !state.isShort && !math.IsNaN(state.positionSize) {
state.entryPrice = closePrice
state.stopPrice = closePrice * (1 - stopLossRate/100)
state.firstTpPrice = closePrice * (1 + firstTakeProfitRate/100)
state.secondTpPrice = closePrice * (1 + secondTakeProfitRate/100)
s.OpenOrder(&strat.EnterReq{
Tag: "long",
Amount: state.positionSize,
StopLoss: state.stopPrice,
StopLossTag: "stop_loss",
})
state.isLong = true
state.partialTpTriggered = false
}
// 空头入场
if shortCondition && !state.isShort && !state.isLong && !math.IsNaN(state.positionSize) {
state.entryPrice = closePrice
state.stopPrice = closePrice * (1 + stopLossRate/100)
state.firstTpPrice = closePrice * (1 - firstTakeProfitRate/100)
state.secondTpPrice = closePrice * (1 - secondTakeProfitRate/100)
s.OpenOrder(&strat.EnterReq{
Tag: "short",
Short: true,
Amount: state.positionSize,
StopLoss: state.stopPrice,
StopLossTag: "stop_loss",
})
state.isShort = true
state.partialTpTriggered = false
}
// 修复错误2:在每个K线更新时重新计算止盈止损价格
if state.isLong {
state.stopPrice = state.entryPrice * (1 - stopLossRate/100)
state.firstTpPrice = state.entryPrice * (1 + firstTakeProfitRate/100)
if !state.partialTpTriggered {
state.secondTpPrice = state.entryPrice * (1 + secondTakeProfitRate/100)
}
// 修复错误3:在触发第一止盈时立即调整第二止盈价格
if !state.partialTpTriggered && (!math.IsNaN(prevClosePrice) && prevClosePrice >= state.firstTpPrice || highPrice >= state.firstTpPrice) {
state.partialTpTriggered = true
state.secondTpPrice = highPrice * (1 + 0.04) // 基于当前最高价再上涨4%
}
}
if state.isShort {
state.stopPrice = state.entryPrice * (1 + stopLossRate/100)
state.firstTpPrice = state.entryPrice * (1 - firstTakeProfitRate/100)
if !state.partialTpTriggered {
state.secondTpPrice = state.entryPrice * (1 - secondTakeProfitRate/100)
}
// 修复错误3:在触发第一止盈时立即调整第二止盈价格
if !state.partialTpTriggered && (!math.IsNaN(prevClosePrice) && prevClosePrice <= state.firstTpPrice || lowPrice <= state.firstTpPrice) {
state.partialTpTriggered = true
state.secondTpPrice = lowPrice * (1 - 0.04) // 基于当前最低价再下跌4%
}
}
// 多头平仓条件
if exitLong && state.isLong {
s.CloseOrders(&strat.ExitReq{
Tag: "exit_long",
Dirt: core.OdDirtLong,
})
state.isLong = false
state.partialTpTriggered = false
}
// 空头平仓条件
if exitShort && state.isShort {
s.CloseOrders(&strat.ExitReq{
Tag: "exit_short",
Dirt: core.OdDirtShort,
})
state.isShort = false
state.partialTpTriggered = false
}
},
OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq {
state, _ := s.More.(*StrategyState)
prevClosePrice := s.Env.Close.Get(1)
highPrice := s.Env.High.Get(0)
lowPrice := s.Env.Low.Get(0)
if od.Short {
// 空仓管理
if state.isShort {
// 修复错误7:使用前一根K线的收盘价和当前K线的最高价/最低价检查止损
if (!math.IsNaN(prevClosePrice) && prevClosePrice >= state.stopPrice) || highPrice >= state.stopPrice {
return &strat.ExitReq{
Tag: "short_stop_loss",
OrderID: od.ID,
}
}
// 修复错误7:使用前一根K线的收盘价和当前K线的最低价检查第一止盈
if !state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice <= state.firstTpPrice) || lowPrice <= state.firstTpPrice) {
return &strat.ExitReq{
Tag: "short_first_take_profit",
OrderID: od.ID,
Amount: state.firstTpQty,
}
}
// 修复错误7:使用前一根K线的收盘价和当前K线的最低价检查第二止盈
if state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice <= state.secondTpPrice) || lowPrice <= state.secondTpPrice) {
state.isShort = false
state.partialTpTriggered = false
return &strat.ExitReq{
Tag: "short_second_take_profit",
OrderID: od.ID,
}
}
}
} else {
// 多仓管理
if state.isLong {
// 修复错误7:使用前一根K线的收盘价和当前K线的最低价检查止损
if (!math.IsNaN(prevClosePrice) && prevClosePrice <= state.stopPrice) || lowPrice <= state.stopPrice {
return &strat.ExitReq{
Tag: "long_stop_loss",
OrderID: od.ID,
}
}
// 修复错误7:使用前一根K线的收盘价和当前K线的最高价检查第一止盈
if !state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice >= state.firstTpPrice) || highPrice >= state.firstTpPrice) {
return &strat.ExitReq{
Tag: "long_first_take_profit",
OrderID: od.ID,
Amount: state.firstTpQty,
}
}
// 修复错误7:使用前一根K线的收盘价和当前K线的最高价检查第二止盈
if state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice >= state.secondTpPrice) || highPrice >= state.secondTpPrice) {
state.isLong = false
state.partialTpTriggered = false
return &strat.ExitReq{
Tag: "long_second_take_profit",
OrderID: od.ID,
}
}
}
}
return nil
},
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
state, _ := s.More.(*StrategyState)
// 订单状态变化时更新策略状态
if chgType == strat.OdChgExitFill {
if od.Short {
// 空单完全平仓
state.isShort = false
state.partialTpTriggered = false
} else {
// 多单完全平仓
state.isLong = false
state.partialTpTriggered = false
}
}
},
}
}

116
llmjob/199.go Normal file
View File

@@ -0,0 +1,116 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J199(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
emaShortPeriod := pol.DefInt("emaShortPeriod", 12, core.PNorm(5, 20))
emaPeriodRatio := pol.Def("emaPeriodRatio", 2.5, core.PNorm(1.5, 4))
rsiOverbought := pol.DefInt("rsiOverbought", 70, core.PNorm(60, 85))
rsiOversold := pol.DefInt("rsiOversold", 30, core.PNorm(15, 40))
// 计算长期EMA周期
emaLongPeriod := int(float64(emaShortPeriod) * emaPeriodRatio)
// MACD参数
macdFastLength := 12
macdSlowLength := 26
macdSignalSmoothing := 9
// RSI参数
rsiLength := 14
// 成交量移动平均周期
volumeMAPeriod := 20
// 计算预热数量,取所有指标中最大的周期
warmupNum := emaLongPeriod
if macdSlowLength > warmupNum {
warmupNum = macdSlowLength
}
if rsiLength > warmupNum {
warmupNum = rsiLength
}
if volumeMAPeriod > warmupNum {
warmupNum = volumeMAPeriod
}
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"4h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
emaShort := ta.EMA(e.Close, emaShortPeriod)
emaLong := ta.EMA(e.Close, emaLongPeriod)
macdLine, signalLine := ta.MACD(e.Close, macdFastLength, macdSlowLength, macdSignalSmoothing)
rsi := ta.RSI(e.Close, rsiLength)
volumeMA := ta.SMA(e.Volume, volumeMAPeriod)
// 获取当前价格
closePrice := e.Close.Get(0)
// 检查数据有效性
if math.IsNaN(closePrice) {
return
}
// 生成交易信号 - 修复重复计算EMA交叉的问题
emaCross := emaShort.Cross(emaLong) // 只计算一次EMA交叉
emaCrossUp := emaCross == 1 // 短期EMA上穿长期EMA
emaCrossDown := emaCross == -1 // 短期EMA下穿长期EMA
macdBullish := macdLine.Get(0) > signalLine.Get(0) // MACD多头信号
macdBearish := macdLine.Get(0) < signalLine.Get(0) // MACD空头信号
rsiNotOverbought := rsi.Get(0) < float64(rsiOverbought) // RSI未超买
rsiNotOversold := rsi.Get(0) > float64(rsiOversold) // RSI未超卖
volumeHigh := e.Volume.Get(0) > volumeMA.Get(0) // 成交量高于平均
// 交易条件判断
buyCondition := emaCrossUp && macdBullish && rsiNotOverbought && volumeHigh
sellCondition := emaCrossDown && macdBearish && rsiNotOversold && volumeHigh
// 计算止损止盈价格 - 修复为使用固定比例,与原始策略一致
//longStopLoss := closePrice * 0.95
//longTakeProfit := closePrice * 1.05
shortStopLoss := closePrice * 1.05
shortTakeProfit := closePrice * 0.95
// 执行交易
_ = buyCondition
//if buyCondition && len(s.LongOrders) == 0 {
// s.OpenOrder(&strat.EnterReq{
// Tag: "Buy",
// OrderType: core.OrderTypeMarket,
// StopLoss: longStopLoss,
// TakeProfit: longTakeProfit,
// StopLossTag: "Stop Loss",
// TakeProfitTag: "Take Profit",
// })
//}
if sellCondition && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Sell",
Short: true,
OrderType: core.OrderTypeMarket,
StopLoss: shortStopLoss,
TakeProfit: shortTakeProfit,
StopLossTag: "Stop Loss",
TakeProfitTag: "Take Profit",
})
}
},
}
}

68
llmjob/2251.go Normal file
View File

@@ -0,0 +1,68 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func J2251(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
fastPeriod := pol.DefInt("fastPeriod", 21, core.PNorm(5, 50))
slowPeriod := pol.DefInt("slowPeriod", 55, core.PNorm(30, 100))
slMultiplier := pol.Def("slMultiplier", 1.5, core.PNorm(0.8, 3))
tpMultiplier := pol.Def("tpMultiplier", 1, core.PNorm(0.5, 2.5))
atrLength := pol.DefInt("atrLength", 14, core.PNorm(7, 30))
return &strat.TradeStrat{
WarmupNum: slowPeriod, // 预热数量取较长的EMA周期
HedgeOff: true, // 单向持仓模式,与PineScript一致
RunTimeFrames: []string{"4h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
fastMA := ta.EMA(e.Close, fastPeriod)
slowMA := ta.EMA(e.Close, slowPeriod)
atr := ta.ATR(e.High, e.Low, e.Close, atrLength)
// 获取最新值
atrVal := atr.Get(0)
closePrice := e.Close.Get(0)
// 检测EMA交叉信号
//goLong := fastMA.Cross(slowMA) == 1
goShort := fastMA.Cross(slowMA) == -1
// 金叉信号:做多
//if goLong && s.OrderNum == 0 {
// // 计算止损和止盈价格
// slPrice := closePrice - atrVal*slMultiplier
// tpPrice := closePrice + atrVal*tpMultiplier*2
// s.OpenOrder(&strat.EnterReq{
// Tag: "Long",
// Short: false,
// Limit: closePrice - atrVal*slMultiplier*0.7,
// StopBars: 7,
// StopLoss: slPrice, // 做多止损价格
// TakeProfit: tpPrice, // 做多止盈价格
// })
//}
// 死叉信号:做空
if goShort && s.OrderNum == 0 {
// 计算止损和止盈价格
slPrice := closePrice + atrVal*slMultiplier
tpPrice := closePrice - atrVal*tpMultiplier
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
StopLoss: slPrice, // 做空止损价格
TakeProfit: tpPrice, // 做空止盈价格
})
}
},
}
}

174
llmjob/2391.go Normal file
View File

@@ -0,0 +1,174 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
// Sta2391 用于保存每个品种的策略状态
type Sta2391 struct {
posReversal123 float64 // 123反转策略的信号值
posmStochCross float64 // 随机指标交叉策略的信号值
}
func J2391(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 123反转策略参数
length := pol.DefInt("length", 40, core.PNorm(5, 30))
kSmoothing := pol.DefInt("kSmoothing", 1, core.PNorm(1, 5))
dLength := pol.DefInt("dLength", 3, core.PNorm(1, 5))
level := pol.Def("level", 30, core.PNorm(30, 70))
// 随机指标交叉策略参数
lengthSC := pol.DefInt("lengthSC", 20, core.PNorm(3, 21))
dLengthSC := pol.DefInt("dLengthSC", 3, core.PNorm(1, 5))
oversold := pol.Def("oversold", 10, core.PNorm(10, 30))
overbought := pol.Def("overbought", 90, core.PNorm(70, 90))
reverse := pol.Def("reverse", 0, core.PUniform(0, 1)) // 0表示不反转,1表示反转
// 计算预热数量,取两个策略中需要的历史数据最大值
warmupNum := length
if lengthSC > warmupNum {
warmupNum = lengthSC
}
if kSmoothing+dLength > warmupNum {
warmupNum = kSmoothing + dLength
}
if dLengthSC > warmupNum {
warmupNum = dLengthSC
}
// 额外需要2个历史K线判断价格反转
warmupNum += 2
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
TimeFrames: "8h",
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
s.More = &Sta2391{
posReversal123: 0,
posmStochCross: 0,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*Sta2391)
// 获取价格数据
close0 := e.Close.Get(0) // 当前收盘价
close1 := e.Close.Get(1) // 前一个收盘价
close2 := e.Close.Get(2) // 前两个收盘价
// 1. 计算123反转策略信号
// 计算随机指标
kFast := ta.Stoch(e.High, e.Low, e.Close, length)
// 对K值进行平滑
vFast := ta.SMA(kFast, kSmoothing)
// 计算D值
vSlow := ta.SMA(vFast, dLength)
// 获取当前值
vFastVal := vFast.Get(0)
vSlowVal := vSlow.Get(0)
// 判断123反转策略信号
if close2 < close1 && close1 < close0 && vFastVal < vSlowVal && vFastVal > level {
// 买入信号:两天内收盘价从低点转为高点,且随机指标快速线在慢速线下方但高于阈值
state.posReversal123 = 1
} else if close2 > close1 && close1 > close0 && vFastVal > vSlowVal && vFastVal < level {
// 卖出信号:两天内收盘价从高点转为低点,且随机指标快速线在慢速线上方但低于阈值
state.posReversal123 = -1
}
// 否则保持之前的信号(不需要else,因为不改变state.posReversal123的值)
// 2. 计算随机指标交叉策略信号
// 计算随机指标
kFastSC := ta.Stoch(e.High, e.Low, e.Close, lengthSC)
// 计算D值
vSlowSC := ta.SMA(kFastSC, dLengthSC)
// 获取当前值
vFastSCVal := kFastSC.Get(0)
vSlowSCVal := vSlowSC.Get(0)
// 判断随机指标交叉策略信号
if vFastSCVal >= vSlowSCVal && vFastSCVal < oversold && vSlowSCVal < oversold {
// 买入信号:快速线在慢速线上方,且两者都处于超卖区域
state.posmStochCross = 1
} else if vFastSCVal < vSlowSCVal && vFastSCVal > overbought && vSlowSCVal > overbought {
// 卖出信号:快速线在慢速线下方,且两者都处于超买区域
state.posmStochCross = -1
}
// 否则保持之前的信号(不需要else,因为不改变state.posmStochCross的值)
// 3. 组合信号
var pos float64
if state.posReversal123 == 1 && state.posmStochCross == 1 {
// 两个策略都给出买入信号
pos = 1
} else if state.posReversal123 == -1 && state.posmStochCross == -1 {
// 两个策略都给出卖出信号
pos = -1
} else {
// 信号不一致,无交易信号
pos = 0
}
// 4. 处理信号反转
var possig float64
if reverse == 1 { // 明确检查reverse是否为1
if pos == 1 {
possig = -1 // 反转买入信号为卖出信号
} else if pos == -1 {
possig = 1 // 反转卖出信号为买入信号
} else {
possig = 0
}
} else {
possig = pos // 不反转信号
}
// 5. 执行交易
if possig == 1 {
// 买入信号
if len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "long",
})
}
// 如果有做空持仓,平仓
if len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "close_short",
Dirt: core.OdDirtShort,
})
}
} else if possig == -1 {
// 卖出信号
if len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "short",
Short: true,
})
}
// 如果有做多持仓,平仓
if len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "close_long",
Dirt: core.OdDirtLong,
})
}
} else if possig == 0 {
// 无信号,平仓所有持仓
if len(s.LongOrders) > 0 || len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "close_all",
Dirt: core.OdDirtBoth,
})
}
}
},
}
}

144
llmjob/2439.go Normal file
View File

@@ -0,0 +1,144 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
"math"
"time"
)
type WeekendData struct {
FridayClose float64
HasLong bool
TotCost float64
TotAmt float64
}
func J2439(pol *config.RunPolicyConfig) *strat.TradeStrat {
entryThreshold := pol.Def("entryThreshold", 0.02, core.PNorm(0.01, 0.1))
profitTakingPercentThreshold := pol.Def("profitTakingPercentThreshold", 0.06, core.PNorm(0.01, 0.08))
return &strat.TradeStrat{
WarmupNum: 7, // 需要最多7天的数据来获取周五收盘价
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"5m"},
OnStartUp: func(s *strat.StratJob) {
s.More = &WeekendData{FridayClose: math.NaN()}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
data, _ := s.More.(*WeekendData)
currentTime := time.Unix(e.TimeStop/1000, 0)
weekday := currentTime.Weekday()
// 计算距离周五的天数
var daysSinceFriday int
switch weekday {
case time.Saturday:
daysSinceFriday = 1
case time.Sunday:
daysSinceFriday = 0
case time.Monday:
daysSinceFriday = 6
case time.Tuesday:
daysSinceFriday = 5
case time.Wednesday:
daysSinceFriday = 4
case time.Thursday:
daysSinceFriday = 3
case time.Friday:
daysSinceFriday = 2
}
// 获取周五收盘价 - 使用日线数据
if e.Close.Len() > daysSinceFriday {
data.FridayClose = e.Close.Get(daysSinceFriday)
}
// 如果没有有效的周五收盘价,不执行交易逻辑
if math.IsNaN(data.FridayClose) {
return
}
// 检查止盈条件
if s.EnteredNum > 0 {
totalProfitRate := 0.0
for _, od := range s.LongOrders {
if od.Status >= ormo.InOutStatusPartEnter {
totalProfitRate += od.ProfitRate
}
}
for _, od := range s.ShortOrders {
if od.Status >= ormo.InOutStatusPartEnter {
totalProfitRate += od.ProfitRate
}
}
if totalProfitRate > profitTakingPercentThreshold {
s.CloseOrders(&strat.ExitReq{
Tag: "profit_take",
Dirt: core.OdDirtBoth,
})
return
}
}
// 周末交易逻辑(周日和周一)
if weekday == time.Sunday || weekday == time.Monday {
currentPrice := e.Close.Get(0)
if math.IsNaN(currentPrice) {
return
}
upperThreshold := data.FridayClose * (1 + entryThreshold)
lowerThreshold := data.FridayClose * (1 - entryThreshold)
// 没有持仓时的入场逻辑
if s.OrderNum == 0 {
if currentPrice > upperThreshold {
s.OpenOrder(&strat.EnterReq{
Tag: "short_entry",
Short: true,
})
} else if currentPrice < lowerThreshold {
s.OpenOrder(&strat.EnterReq{
Tag: "long_entry",
})
}
} else {
// 已有持仓时的加仓逻辑
for _, od := range s.ShortOrders {
if od.Status >= ormo.InOutStatusPartEnter && currentPrice > od.InitPrice*(1+entryThreshold) {
s.OpenOrder(&strat.EnterReq{
Tag: "add_short",
Short: true,
})
break
}
}
for _, od := range s.LongOrders {
if od.Status >= ormo.InOutStatusPartEnter {
s.OpenOrder(&strat.EnterReq{
Tag: "add_long",
})
break
}
}
}
}
// 周二平仓逻辑
if weekday == time.Tuesday {
if s.OrderNum > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "monday_close",
Dirt: core.OdDirtBoth,
})
}
}
},
}
}

103
llmjob/2523.go Normal file
View File

@@ -0,0 +1,103 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
/*
以大止损实现的高胜率,订单太少
尝试使用atrBase针对CostRate、TakeProfit、StopLoss进行自适应,效果都更差。
*/
func J2523(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
maLen := pol.DefInt("maLen", 240, core.PNorm(50, 300))
adxThres := pol.Def("adxThres", 20, core.PNorm(15, 35))
slRate := pol.Def("slRate", 2.5, core.PNorm(0.8, 5.0))
// 计算预热K线数,确保所有指标都能正确计算
warmupNum := maLen + 20
//rates := make([]float64, 0, 10)
return &strat.TradeStrat{
WarmupNum: warmupNum,
RunTimeFrames: []string{"3d"},
StakeRate: 20,
EachMaxLong: 1,
EachMaxShort: 1,
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
maBig := ta.SMA(e.Close, maLen)
rsi := ta.RSI(e.Close, 14)
// 使用DMI计算ADX,与原始策略保持一致
adx := ta.ADX(e.High, e.Low, e.Close, 14)
atr := ta.ATR(e.High, e.Low, e.Close, 14)
// 获取当前值和历史值,避免重复计算
curClose := e.Close.Get(0)
curMA := maBig.Get(0)
curRSI := rsi.Get(0)
curADX := adx.Get(0)
curATR := atr.Get(0)
// 检查数据有效性
if math.IsNaN(curClose) || math.IsNaN(curMA) || math.IsNaN(curRSI) ||
math.IsNaN(curADX) || math.IsNaN(curATR) {
return
}
// 检查2根K线前是否发生交叉
closeXBig := e.Close.Cross(maBig)
rsi2 := rsi.Get(2)
adxOk := adx.Get(2) > adxThres
buyConfirmation := closeXBig == 3 && (rsi2 > 40) && adxOk
sellConfirmation := closeXBig == -3 && (rsi2 < 60) && adxOk
// 执行交易逻辑
if buyConfirmation {
s.OpenOrder(&strat.EnterReq{
Tag: "Buy",
TakeProfitVal: curClose * 0.02, // 2%止盈
StopLossVal: slRate * curATR,
})
s.CloseOrders(&strat.ExitReq{
Tag: "XShort",
Dirt: core.OdDirtShort,
})
}
if sellConfirmation {
s.OpenOrder(&strat.EnterReq{
Tag: "short",
Short: true,
TakeProfitVal: curClose * 0.02, // 2%止盈
StopLossVal: slRate * curATR,
})
s.CloseOrders(&strat.ExitReq{
Tag: "XLong",
Dirt: core.OdDirtLong,
})
}
},
//OnShutDown: func(s *strat.StratJob) {
// if len(rates) == 0 {
// return
// }
// sort.Float64s(rates)
// size := len(rates) / 10
// for i := 0; i < len(rates); i += size {
// block := rates[i:min(len(rates), i+size)]
// avg := floats.Sum(block) / float64(len(block))
// fmt.Printf("rate %v: %.3f\n", i, avg)
// }
// rates = nil
//},
}
}

168
llmjob/2625.go Normal file
View File

@@ -0,0 +1,168 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
// AtrRsiStrategy 存储每个品种的持仓期间最高价和最低价
type AtrRsiStrategy struct {
IntraTradeHigh float64
IntraTradeLow float64
}
func J2625(pol *config.RunPolicyConfig) *strat.TradeStrat {
atrLength := pol.DefInt("atr_length", 18, core.PNorm(10, 50))
atrMaRate := pol.Def("atr_ma_rate", 1.8, core.PNorm(1.2, 3.0))
rsiLength := pol.DefInt("rsi_length", 9, core.PNorm(7, 30))
rsiEntry := pol.DefInt("rsi_entry", 7, core.PNorm(5, 20))
atrMaNormMin := pol.Def("atr_ma_norm_min", 0.3, core.PNorm(0.1, 0.5))
atrMaNormMax := pol.Def("atr_ma_norm_max", 0.7, core.PNorm(0.5, 1.0))
trailingPercent := pol.Def("trailing_percent", 1.3, core.PNorm(0.5, 3.0))
// 计算ATR移动平均线周期
atrMaLength := int(float64(atrLength) * atrMaRate)
// 计算RSI买入和卖出阈值
rsiBuy := 50 + float64(rsiEntry)
rsiSell := 50 - float64(rsiEntry)
// 确定预热数量,取最大参数值
warmupNum := atrMaLength
if rsiLength > warmupNum {
warmupNum = rsiLength
}
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"1h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的持仓期间最高价和最低价
s.More = &AtrRsiStrategy{
IntraTradeHigh: 0,
IntraTradeLow: 0,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
m, _ := s.More.(*AtrRsiStrategy)
// 计算技术指标
atrValue := ta.ATR(e.High, e.Low, e.Close, atrLength)
atrMa := ta.SMA(atrValue, atrMaLength)
rsiValue := ta.RSI(e.Close, rsiLength)
// 获取当前价格
closePrice := e.Close.Get(0)
highPrice := e.High.Get(0)
lowPrice := e.Low.Get(0)
// 检查数据有效性
if math.IsNaN(closePrice) || math.IsNaN(highPrice) || math.IsNaN(lowPrice) ||
math.IsNaN(atrValue.Get(0)) || math.IsNaN(atrMa.Get(0)) || math.IsNaN(rsiValue.Get(0)) {
return
}
// 计算ATR移动平均线的标准化值
atrMaNorm := atrMa.Get(0) / closePrice * 100
// 获取当前持仓状态
longOrders := s.LongOrders
shortOrders := s.ShortOrders
hasLongPosition := len(longOrders) > 0
hasShortPosition := len(shortOrders) > 0
// 无持仓状态
if !hasLongPosition && !hasShortPosition {
// 重置持仓期间最高价和最低价
m.IntraTradeHigh = highPrice
m.IntraTradeLow = lowPrice
// 检查波动率是否在适合交易范围内
if atrMaNorm >= atrMaNormMin && atrMaNorm <= atrMaNormMax {
// 检查当前波动率是否高于平均波动率
if atrValue.Get(0) > atrMa.Get(0) {
// 检查RSI信号
if rsiValue.Get(0) > rsiBuy {
// RSI超买,做多
s.OpenOrder(&strat.EnterReq{
Tag: "B1",
OrderType: core.OrderTypeLimit,
Limit: closePrice + 5,
})
} else if rsiValue.Get(0) < rsiSell {
// RSI超卖,做空
s.OpenOrder(&strat.EnterReq{
Tag: "S1",
Short: true,
OrderType: core.OrderTypeLimit,
Limit: closePrice - 5,
})
}
}
}
} else if hasLongPosition {
// 多头持仓状态
// 更新持仓期间最高价
if highPrice > m.IntraTradeHigh {
m.IntraTradeHigh = highPrice
}
m.IntraTradeLow = lowPrice
// 计算多头追踪止损价格
longTp := m.IntraTradeHigh * (1 - trailingPercent/100)
// 获取入场价格
enterPrice := longOrders[0].InitPrice
// 设置止损和止盈
for _, od := range longOrders {
od.SetStopLoss(&ormo.ExitTrigger{
Price: longTp,
Rate: 1.0, // 全部退出
Tag: "trailing_stop",
})
od.SetTakeProfit(&ormo.ExitTrigger{
Price: enterPrice * 1.03,
Rate: 1.0, // 全部退出
Tag: "take_profit",
})
}
} else if hasShortPosition {
// 空头持仓状态
m.IntraTradeHigh = highPrice
// 更新持仓期间最低价
if lowPrice < m.IntraTradeLow {
m.IntraTradeLow = lowPrice
}
// 计算空头追踪止损价格
shortTp := m.IntraTradeLow * (1 + trailingPercent/100)
// 获取入场价格
enterPrice := shortOrders[0].InitPrice
// 设置止损和止盈
for _, od := range shortOrders {
od.SetStopLoss(&ormo.ExitTrigger{
Price: shortTp,
Rate: 1.0, // 全部退出
Tag: "trailing_stop",
})
od.SetTakeProfit(&ormo.ExitTrigger{
Price: enterPrice * 0.94,
Rate: 1.0, // 全部退出
Tag: "take_profit",
})
}
}
},
}
}

104
llmjob/2775.go Normal file
View File

@@ -0,0 +1,104 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
type MacdRsiState struct {
above50 bool // 标记RSI是否上穿50
below50 bool // 标记RSI是否下穿50
longEntryTime int64 // 记录多头入场时间
shortEntryTime int64 // 记录空头入场时间
}
func J2775(pol *config.RunPolicyConfig) *strat.TradeStrat {
fastPeriod := pol.DefInt("fastPeriod", 15, core.PNorm(5, 20))
macdRate := pol.Def("macdRate", 2.5, core.PNorm(1.5, 4))
rsiPeriod := pol.DefInt("rsiPeriod", 16, core.PNorm(7, 21))
exitBars := pol.DefInt("exitBars", 5, core.PNorm(1, 10))
signalSmoothing := pol.DefInt("signalSmoothing", 9, core.PNorm(5, 15))
// 计算MACD慢线长度
slowPeriod := int(float64(fastPeriod) * macdRate)
return &strat.TradeStrat{
WarmupNum: slowPeriod + signalSmoothing + rsiPeriod,
HedgeOff: true, // 单向持仓
EachMaxLong: 1, // 最多一个多头订单
EachMaxShort: 1, // 最多一个空头订单
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
s.More = &MacdRsiState{
above50: false,
below50: false,
longEntryTime: 0,
shortEntryTime: 0,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*MacdRsiState)
// 计算MACD指标
macdLine, signalLine := ta.MACD(e.Close, fastPeriod, slowPeriod, signalSmoothing)
// 计算RSI指标
rsi := ta.RSI(e.Close, rsiPeriod)
// 检测MACD金叉和死叉
bullishCross := macdLine.Cross(signalLine) == 1
bearishCross := macdLine.Cross(signalLine) == -1
// 更新RSI状态
if rsi.Get(0) > 50 && rsi.Get(1) <= 50 {
state.above50 = true
}
if rsi.Get(0) < 50 && rsi.Get(1) >= 50 {
state.below50 = true
}
// 执行交易信号
if bullishCross && state.above50 && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Buy",
})
state.longEntryTime = e.TimeStop
}
if bearishCross && state.below50 && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Sell",
Short: true,
})
state.shortEntryTime = e.TimeStop
}
// 检查退出条件
if len(s.LongOrders) > 0 && state.longEntryTime > 0 {
holdBars := e.BarCount(state.longEntryTime)
if holdBars >= float64(exitBars) {
s.CloseOrders(&strat.ExitReq{
Tag: "Exit Long",
Dirt: core.OdDirtLong,
})
state.longEntryTime = 0
}
}
if len(s.ShortOrders) > 0 && state.shortEntryTime > 0 {
holdBars := e.BarCount(state.shortEntryTime)
if holdBars >= float64(exitBars) {
s.CloseOrders(&strat.ExitReq{
Tag: "Exit Short",
Dirt: core.OdDirtShort,
})
state.shortEntryTime = 0
}
}
},
}
}

94
llmjob/2874.go Normal file
View File

@@ -0,0 +1,94 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J2874(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
rsiPeriod := pol.DefInt("rsiPeriod", 70, core.PNorm(20, 100))
longExitThreshold := pol.Def("longExitThreshold", 0.65, core.PNorm(0.55, 0.8))
shortExitThreshold := pol.Def("shortExitThreshold", 0.35, core.PNorm(0.2, 0.45))
allowLong := pol.Def("allowLong", 1.0, core.PUniform(0, 1)) > 0.5
allowShort := pol.Def("allowShort", 1.0, core.PUniform(0, 1)) > 0.5
return &strat.TradeStrat{
WarmupNum: rsiPeriod, // 预热期至少为RSI周期
HedgeOff: true, // 单向持仓模式,与原PineScript策略一致
RunTimeFrames: []string{"4h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算各种价格组合
ohlc4 := ta.HLC3(e.High, e.Low, e.Close).Add(e.Open).Div(4)
hlc3 := ta.HLC3(e.High, e.Low, e.Close)
hl2 := ta.HL2(e.High, e.Low)
// 计算各种RSI值并归一化(除以100)
rsiOhlc4 := ta.RSI(ohlc4, rsiPeriod).Div(100)
rsiClose := ta.RSI(e.Close, rsiPeriod).Div(100)
rsiOpen := ta.RSI(e.Open, rsiPeriod).Div(100)
rsiHigh := ta.RSI(e.High, rsiPeriod).Div(100)
rsiHlc3 := ta.RSI(hlc3, rsiPeriod).Div(100)
rsiHl2 := ta.RSI(hl2, rsiPeriod).Div(100)
// 计算RSI均值
rsiAvg := rsiOhlc4.Add(rsiClose).Add(rsiOpen).Add(rsiHigh).Add(rsiHlc3).Add(rsiHl2).Div(6)
rsiAvgVal := rsiAvg.Get(0)
// 检查数据有效性
if math.IsNaN(rsiAvgVal) {
return
}
// 检测RSI均值与0.5的交叉
entryThreshold := 0.5
crossSignal := rsiAvg.Cross(entryThreshold)
// 生成交易信号
longSignal := crossSignal == 1 // RSI均值上穿0.5
shortSignal := crossSignal == -1 // RSI均值下穿0.5
// 生成平仓信号
longExit := rsiAvgVal >= longExitThreshold || shortSignal
shortExit := rsiAvgVal <= shortExitThreshold || longSignal
// 执行交易逻辑
if allowLong && longSignal && len(s.LongOrders) == 0 {
// 开多头仓位
s.OpenOrder(&strat.EnterReq{
Tag: "long",
})
}
if allowShort && shortSignal && len(s.ShortOrders) == 0 {
// 开空头仓位
s.OpenOrder(&strat.EnterReq{
Tag: "short",
Short: true,
})
}
if allowLong && len(s.LongOrders) > 0 && longExit {
// 平多头仓位
s.CloseOrders(&strat.ExitReq{
Tag: "long_exit",
Dirt: core.OdDirtLong,
})
}
if allowShort && len(s.ShortOrders) > 0 && shortExit {
// 平空头仓位
s.CloseOrders(&strat.ExitReq{
Tag: "short_exit",
Dirt: core.OdDirtShort,
})
}
},
}
}

110
llmjob/2876.go Normal file
View File

@@ -0,0 +1,110 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J2876(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
frequency := pol.DefInt("frequency", 28, core.PNorm(5, 30))
rsiFrequency := pol.DefInt("rsiFrequency", 220, core.PNorm(14, 100))
buyZoneDistance := pol.Def("buyZoneDistance", 7, core.PNorm(2, 15))
avgDownATRSum := pol.DefInt("avgDownATRSum", 3, core.PNorm(1, 10))
useAbsoluteRSIBarrier := pol.Def("useAbsoluteRSIBarrier", 1, core.PUniform(0, 1)) // 1表示true,0表示false
barrierLevel := pol.Def("barrierLevel", 45, core.PNorm(30, 70))
// 计算预热数量,取RSI周期和均线周期中的较大值
warmupNum := rsiFrequency
if frequency > rsiFrequency {
warmupNum = frequency
}
// 再加上ATR周期
warmupNum += 20
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"15m"},
EachMaxLong: 3, // 最多允许3个多头订单,对应原策略的pyramiding=3
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
momentumRSI := ta.RSI(e.Close, rsiFrequency)
momentumRSI_slow := ta.SMA(momentumRSI, frequency)
atr := ta.ATR(e.High, e.Low, e.Close, 20)
// 获取当前值
rsiValue := momentumRSI.Get(0)
rsiSlowValue := momentumRSI_slow.Get(0)
closePrice := e.Close.Get(0)
// 检查数据有效性
if math.IsNaN(rsiValue) || math.IsNaN(rsiSlowValue) || math.IsNaN(closePrice) {
return
}
// 计算买卖区域
buyZoneLower := rsiSlowValue * (1 - buyZoneDistance/100)
// 生成交易信号
useBarrier := useAbsoluteRSIBarrier > 0.5
momentumRSISoftClose := (rsiValue > rsiSlowValue) && (rsiValue > barrierLevel || !useBarrier)
isClose := momentumRSISoftClose
// 计算风险管理条件
riskCondition := true
if len(s.LongOrders) > 0 {
// 计算平均持仓价格
totalCost := 0.0
totalAmount := 0.0
for _, od := range s.LongOrders {
if od.Status >= ormo.InOutStatusPartEnter {
totalCost += od.EnterCost()
totalAmount += od.HoldAmount()
}
}
if totalAmount > 0 {
avgPrice := totalCost / totalAmount
// 计算过去avgDownATRSum个周期的ATR值之和
atrSum := 0.0
for i := 0; i < avgDownATRSum; i++ {
atrVal := atr.Get(i)
if !math.IsNaN(atrVal) {
atrSum += atrVal
}
}
// 计算风险条件:平均持仓价格 - ATR求和值 * 开仓数量 > 当前价格
riskCondition = avgPrice-atrSum*float64(len(s.LongOrders)) > closePrice
}
}
// 买入信号:RSI低于买入区域下限且满足风险条件
isBuy := rsiValue < buyZoneLower && (riskCondition || len(s.LongOrders) == 0)
// 执行交易操作
if isBuy && len(s.LongOrders) < 3 { // 限制最多3个多头订单
s.OpenOrder(&strat.EnterReq{
Tag: "Buy",
Short: false,
OrderType: core.OrderTypeMarket,
})
}
// 平仓信号
if isClose && len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "Close",
Dirt: core.OdDirtLong,
OrderType: core.OrderTypeLimit,
Limit: closePrice,
})
}
},
}
}

269
llmjob/2984.go Normal file
View File

@@ -0,0 +1,269 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
// ZeroLagEMA 计算零滞后指数移动平均线
func ZeroLagEMA(source *ta.Series, length int) *ta.Series {
res := source.To("_zlema", length)
if res.Cached() {
return res
}
if source.Len() < length {
return res.Append(math.NaN())
}
ema1 := ta.EMA(source, length)
ema2 := ta.EMA(ema1, length)
c1 := ema1.Get(0) - ema2.Get(0)
zlema := ema1.Get(0) + c1
return res.Append(zlema)
}
// ZlemaState 存储每个品种的策略状态
type ZlemaState struct {
goLong bool
sectionLongs int
sectionShorts int
sectionLongConditions int
sectionShortConditions int
totalLongs float64
totalShorts float64
averageLongs float64
averageShorts float64
lastHigh float64
lastLow float64
inLong bool
inShort bool
}
func J2984(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
fastPeriod := pol.DefInt("fastPeriod", 10, core.PNorm(3, 20))
maRate := pol.Def("maRate", 2.4, core.PNorm(1.5, 5))
takeProfitRate := pol.Def("takeProfitRate", 3.5, core.PNorm(1, 10))
trailingStopRate := pol.Def("trailingStopRate", 3.5, core.PNorm(1, 8))
trailingActivationRate := pol.Def("trailingActivationRate", 16, core.PNorm(5, 25))
stopLossPercent := pol.Def("stopLossPercent", 7.5, core.PNorm(1, 15))
// 金字塔加仓参数
pyramidingLess := pol.DefInt("pyramidingLess", 1, core.PNorm(0, 5))
pyramidingEqual := pol.DefInt("pyramidingEqual", 0, core.PNorm(0, 5))
pyramidingGreater := pol.DefInt("pyramidingGreater", 1000000, core.PNorm(100, 1000000))
// 交易方向控制参数
longsOnly := pol.Def("longsOnly", 0, core.PUniform(0, 1)) > 0.5
shortsOnly := pol.Def("shortsOnly", 0, core.PUniform(0, 1)) > 0.5
flipOpens := pol.Def("flipOpens", 0, core.PUniform(0, 1)) > 0.5
// 计算慢线周期
slowPeriod := int(float64(fastPeriod) * maRate)
return &strat.TradeStrat{
WarmupNum: slowPeriod * 2, // 确保有足够的数据计算指标
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多开1单多仓
EachMaxShort: 1, // 最多开1单空仓
DrawDownExit: false, // 不使用回撤止盈
RunTimeFrames: []string{"8h"},
StopLoss: 0.075,
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
s.More = &ZlemaState{
goLong: true,
sectionLongs: 0,
sectionShorts: 0,
sectionLongConditions: 0,
sectionShortConditions: 0,
totalLongs: 0,
totalShorts: 0,
averageLongs: 0,
averageShorts: 0,
lastHigh: 0,
lastLow: 0,
inLong: false,
inShort: false,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*ZlemaState)
// 计算Zero-Lag EMA
zlemaFast := ZeroLagEMA(e.Close, fastPeriod)
zlemaSlow := ZeroLagEMA(e.Close, slowPeriod)
// 获取当前值
fastVal := zlemaFast.Get(0)
slowVal := zlemaSlow.Get(0)
fastVal1 := zlemaFast.Get(1)
slowVal1 := zlemaSlow.Get(1)
closePrice := e.Close.Get(0)
highPrice := e.High.Get(0)
lowPrice := e.Low.Get(0)
// 检查数据有效性
if math.IsNaN(fastVal) || math.IsNaN(slowVal) || math.IsNaN(closePrice) {
return
}
// 生成基础交易信号
longSignal := fastVal > slowVal && fastVal1 <= slowVal1 // 快线上穿慢线
shortSignal := fastVal < slowVal && fastVal1 >= slowVal1 // 快线下穿慢线
// 应用交易方向翻转功能
if flipOpens {
longSignal, shortSignal = shortSignal, longSignal
}
// 应用仅做多/仅做空选项
if longsOnly {
shortSignal = false
}
if shortsOnly {
longSignal = false
}
// 更新连续信号计数
if longSignal {
state.sectionLongs++
state.sectionShorts = 0
} else if shortSignal {
state.sectionShorts++
state.sectionLongs = 0
}
// 应用金字塔式加仓条件
longCondition := longSignal && (state.sectionLongs <= pyramidingLess || state.sectionLongs >= pyramidingGreater || state.sectionLongs == pyramidingEqual)
shortCondition := shortSignal && (state.sectionShorts <= pyramidingLess || state.sectionShorts >= pyramidingGreater || state.sectionShorts == pyramidingEqual)
// 更新开仓价格和数量
if longCondition {
state.totalLongs += closePrice
state.sectionLongConditions++
state.averageLongs = state.totalLongs / float64(state.sectionLongConditions)
state.inLong = true
state.inShort = false
} else if shortCondition {
state.totalShorts += closePrice
state.sectionShortConditions++
state.averageShorts = state.totalShorts / float64(state.sectionShortConditions)
state.inShort = true
state.inLong = false
}
// 更新最高价/最低价追踪
if state.inLong {
if state.lastHigh == 0 || highPrice > state.lastHigh {
state.lastHigh = highPrice
}
} else if state.inShort {
if state.lastLow == 0 || lowPrice < state.lastLow {
state.lastLow = lowPrice
}
}
// 执行交易
if longCondition && len(s.LongOrders) == 0 {
// 设置止损止盈 - 基于平均开仓价计算
stopLossVal := state.averageLongs * (1 - stopLossPercent/100)
takeProfitVal := state.averageLongs * (1 + takeProfitRate/100)
s.OpenOrder(&strat.EnterReq{
Tag: "long",
Short: false,
OrderType: core.OrderTypeMarket,
StopLossVal: stopLossVal,
TakeProfitVal: takeProfitVal,
StopLossTag: "stop_loss",
TakeProfitTag: "take_profit",
})
} else if shortCondition && len(s.ShortOrders) == 0 {
// 设置止损止盈 - 基于平均开仓价计算
stopLossVal := state.averageShorts * (1 + stopLossPercent/100)
takeProfitVal := state.averageShorts * (1 - takeProfitRate/100)
s.OpenOrder(&strat.EnterReq{
Tag: "short",
Short: true,
OrderType: core.OrderTypeMarket,
StopLossVal: stopLossVal,
TakeProfitVal: takeProfitVal,
StopLossTag: "stop_loss",
TakeProfitTag: "take_profit",
})
}
// 检查平仓条件
if state.inLong && shortSignal {
s.CloseOrders(&strat.ExitReq{
Tag: "trend_reverse",
Dirt: core.OdDirtLong,
})
} else if state.inShort && longSignal {
s.CloseOrders(&strat.ExitReq{
Tag: "trend_reverse",
Dirt: core.OdDirtShort,
})
}
},
OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq {
e := s.Env
state, _ := s.More.(*ZlemaState)
closePrice := e.Close.Get(0)
if od.Short {
// 做空订单检查
if state.inShort {
// 检查追踪止损条件 - 包含激活条件判断
if state.lastLow > 0 &&
state.lastLow <= state.averageShorts*(1-trailingActivationRate/100) && // 激活条件
closePrice >= state.lastLow*(1+trailingStopRate/100) { // 追踪止损条件
return &strat.ExitReq{Tag: "trailing_stop"}
}
}
} else {
// 做多订单检查
if state.inLong {
// 检查追踪止损条件 - 包含激活条件判断
if state.lastHigh > 0 &&
state.lastHigh >= state.averageLongs*(1+trailingActivationRate/100) && // 激活条件
closePrice <= state.lastHigh*(1-trailingStopRate/100) { // 追踪止损条件
return &strat.ExitReq{Tag: "trailing_stop"}
}
}
}
return nil
},
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
state, _ := s.More.(*ZlemaState)
// 订单状态变化时更新状态
if chgType == strat.OdChgExitFill {
if od.Short {
// 做空订单已平仓
state.inShort = false
state.averageShorts = 0
state.totalShorts = 0
state.sectionShorts = 0
state.sectionShortConditions = 0
state.lastLow = 0
} else {
// 做多订单已平仓
state.inLong = false
state.averageLongs = 0
state.totalLongs = 0
state.sectionLongs = 0
state.sectionLongConditions = 0
state.lastHigh = 0
}
}
},
}
}

154
llmjob/3087.go Normal file
View File

@@ -0,0 +1,154 @@
package llmjob
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"gonum.org/v1/gonum/floats"
"math"
"sort"
)
/*
基于初始的3087进行了简化,只保留盈利较好的14-26
*/
// DCAStrategyState 存储每个品种的策略状态
type DCAStrategyState struct {
currentSO int // 当前安全订单计数器
firstPrice float64 // 初始订单价格
totalQtys float64 // 总交易数量
totalCost float64 // 累计花费
realTotQtys float64 // 总交易数量
realTotCost float64 // 累计花费
}
// calculateSafetyOrderThreshold 计算安全订单触发价格阈值
func calculateSafetyOrderThreshold(initPrice, priceDeviation, stepScale float64, currentSO int) float64 {
curSO := float64(currentSO)
if stepScale == 1.0 {
return initPrice - initPrice*priceDeviation*stepScale*curSO
}
rate := priceDeviation*math.Pow(stepScale, curSO) - priceDeviation
return initPrice - initPrice*(rate/(stepScale-1))
}
// DynamicDCA 动态DCA策略
func J3087(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
priceDeviation := pol.Def("priceDeviation", 0.82, core.PNorm(0.5, 3.0))
volumeScale := pol.Def("volumeScale", 1.16, core.PNorm(1.05, 1.5))
stepScale := pol.Def("stepScale", 1.06, core.PNorm(1.05, 1.3))
takeProfit := pol.Def("takeProfit", 1.15, core.PNorm(0.5, 2.5))
baseOrder := pol.Def("baseOrder", 10.0, core.PNorm(5, 20))
startLvl := pol.DefInt("startLvl", 14.0, core.PNorm(1, 20))
endLvl := pol.DefInt("endLvl", 26.0, core.PNorm(3, 30))
maxSafeOrders := pol.DefInt("maxSafeOrders", 30, core.PNorm(5, 50))
lenAtr := 20
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
var rates []float64
// 14/15/16/17/18/19/20/21/22/23/24/25/26
return &strat.TradeStrat{
WarmupNum: 1,
HedgeOff: true, // 单向持仓
RunTimeFrames: []string{"15m"},
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
s.More = &DCAStrategyState{}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*DCAStrategyState)
// 计算OHLC4加权均价
price := (e.High.Get(0) + e.Low.Get(0) + e.Close.Get(0) + e.Open.Get(0)) / 4
closeP := e.Close.Get(0)
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
openRate := 0.004 * closeP / atrBase
if openRate < 1 {
openRate = math.Sqrt(openRate)
}
openRate = max(0.7, min(1.5, openRate))
// 计算止盈水平
hasOrders := state.currentSO > 0
var avgPrice, realAvgPrice float64
if hasOrders && state.totalQtys > 0 {
avgPrice = state.totalCost / state.totalQtys
realAvgPrice = state.realTotCost / state.realTotQtys
}
takeProfitLevel := avgPrice * (1 + takeProfit/100)
// 初始建仓逻辑
if state.currentSO == 0 && price > 0 {
// 计算买入数量
qty := baseOrder / price
// 首次买入表现不佳,禁止实际买入
state.totalQtys += qty
state.totalCost += qty * closeP
state.firstPrice = price
state.currentSO = 1
}
// 计算安全订单触发价格阈值
priceRate := priceDeviation / 100
threshold := calculateSafetyOrderThreshold(state.firstPrice, priceRate, stepScale, state.currentSO)
// 动态补仓逻辑
if hasOrders && price <= threshold && state.currentSO <= maxSafeOrders {
// 计算补仓数量
orderQty := baseOrder * openRate * math.Pow(volumeScale, float64(state.currentSO-1)) / price
// endLvl用于实际下单。maxSafeOrders用于止盈价格
if state.currentSO >= startLvl && state.currentSO <= endLvl {
s.OpenOrder(&strat.EnterReq{
Tag: fmt.Sprintf("%v", state.currentSO),
Amount: orderQty,
Log: true,
})
state.realTotQtys += orderQty
state.realTotCost += orderQty * closeP
}
state.totalQtys += orderQty
state.totalCost += orderQty * closeP
state.currentSO += 1
}
// 止盈平仓逻辑
if price >= takeProfitLevel && hasOrders {
s.CloseOrders(&strat.ExitReq{
Tag: "TakeProfit",
})
if realAvgPrice > 0 {
rates = append(rates, takeProfitLevel/realAvgPrice)
}
// 重置状态变量
state.currentSO = 0
state.totalQtys = 0.0
state.totalCost = 0
state.realTotQtys = 0
state.realTotCost = 0
state.firstPrice = 0
}
},
OnShutDown: func(s *strat.StratJob) {
if len(rates) == 0 {
return
}
sort.Float64s(rates)
step := len(rates) / 7
for i := 0; i < len(rates); i += step {
block := rates[i:min(len(rates), i+step)]
avg := floats.Sum(block) / float64(len(block))
fmt.Printf("rate: %v: %v\n", i, avg)
}
rates = nil
},
}
}

88
llmjob/3121.go Normal file
View File

@@ -0,0 +1,88 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J3121(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
bbLength := pol.DefInt("bbLength", 20, core.PNorm(10, 50))
bbStdDev := pol.Def("bbStdDev", 1.8, core.PNorm(1.0, 3.0))
cmoLength := pol.DefInt("cmoLength", 10, core.PNorm(5, 30))
cmoOverbought := pol.Def("cmoOverbought", 45.0, core.PNorm(30.0, 70.0))
cmoOversold := pol.Def("cmoOversold", -50.0, core.PNorm(-70.0, -30.0))
return &strat.TradeStrat{
WarmupNum: bbLength, // 预热期设置为布林带周期长度
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多一个多头订单
EachMaxShort: 1, // 最多一个空头订单
RunTimeFrames: []string{"5m"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算布林带
bbUpper, bbBasis, bbLower := ta.BBANDS(e.Close, bbLength, bbStdDev, bbStdDev)
// 计算钱德动量振荡器
cmo := ta.CMO(e.Close, cmoLength)
// 获取当前值
closePrice := e.Close.Get(0)
cmoValue := cmo.Get(0)
bbUpperValue := bbUpper.Get(0)
bbLowerValue := bbLower.Get(0)
bbBasisValue := bbBasis.Get(0)
// 检查数据有效性
if math.IsNaN(closePrice) || math.IsNaN(cmoValue) ||
math.IsNaN(bbUpperValue) || math.IsNaN(bbLowerValue) || math.IsNaN(bbBasisValue) {
return
}
// 做多条件:价格下穿布林带下轨且CMO低于超卖阈值
longCondition := e.Close.Cross(bbLower) == -1 && cmoValue < cmoOversold
// 做空条件:价格上穿布林带上轨且CMO高于超买阈值
shortCondition := e.Close.Cross(bbUpper) == 1 && cmoValue > cmoOverbought
// 平多条件:价格上穿布林带中轨或CMO高于超买阈值
exitLong := e.Close.Cross(bbBasis) == 1 || cmoValue > cmoOverbought
// 平空条件:价格下穿布林带中轨或CMO低于超卖阈值
exitShort := e.Close.Cross(bbBasis) == -1 || cmoValue < cmoOversold
// 执行交易操作
if longCondition && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
})
}
if shortCondition && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
})
}
if exitLong && len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "exit_long",
Dirt: core.OdDirtLong,
})
}
if exitShort && len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "exit_short",
Dirt: core.OdDirtShort,
})
}
},
}
}

55
llmjob/3127.go Normal file
View File

@@ -0,0 +1,55 @@
// 修复后的完整策略代码
// 请勿修改package包名和任意函数名
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J3127(pol *config.RunPolicyConfig) *strat.TradeStrat {
buyThreshold := pol.Def("buyThreshold", 0.025, core.PNorm(0.005, 0.05))
sellThreshold := pol.Def("sellThreshold", 0.025, core.PNorm(0.005, 0.05))
lookbackPeriod := pol.DefInt("lookbackPeriod", 16, core.PNorm(5, 20))
return &strat.TradeStrat{
WarmupNum: lookbackPeriod,
HedgeOff: true,
EachMaxLong: 1,
RunTimeFrames: []string{"15m"},
OnBar: func(s *strat.StratJob) {
e := s.Env
closePrice := e.Close.Get(0)
if math.IsNaN(closePrice) {
return
}
// 修复:使用收盘价计算历史极值
highest := ta.Highest(e.Close, lookbackPeriod).Get(0)
lowest := ta.Lowest(e.Close, lookbackPeriod).Get(0)
buyTrigger := highest * (1 - buyThreshold)
sellTrigger := lowest * (1 + sellThreshold)
if closePrice < buyTrigger && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Achat",
Short: false,
OrderType: core.OrderTypeMarket,
})
}
if closePrice > sellTrigger && len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "Vente",
Dirt: core.OdDirtLong,
Force: true,
})
}
},
}
}

127
llmjob/3137.go Normal file
View File

@@ -0,0 +1,127 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"math"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
)
type DDLState struct {
lastbull float64 // 最后一个涨停价(连续上涨时的最高点)
lastbear float64 // 最后一个跌停价(连续下跌时的最低点)
lot float64 // 计算后的开仓数量
}
func J3137(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
signalshort := pol.Def("signalshort", 5.1, core.PNorm(1.0, 10.0))
signallong := pol.Def("signallong", -5.1, core.PNorm(-10.0, -1.0))
capital := pol.Def("capital", 145.0, core.PNorm(10.0, 200.0))
needlong := pol.Def("needlong", 1.0, core.PNorm(0.0, 1.0))
needshort := pol.Def("needshort", 1.0, core.PNorm(0.0, 1.0))
return &strat.TradeStrat{
WarmupNum: 2, // 至少需要2个K线数据才能计算bull和bear
HedgeOff: false, // 允许双向持仓
RunTimeFrames: []string{"15m"},
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的状态
s.More = &DDLState{
lastbull: math.NaN(),
lastbear: math.NaN(),
lot: 0.0,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*DDLState)
// 获取当前和前一个K线的价格
closePrice := e.Close.Get(0)
prevClose := e.Close.Get(1)
openPrice := e.Open.Get(0)
// 计算capital比例,避免重复计算
capitalRate := capital / 100
// 如果数据不足,返回
if math.IsNaN(closePrice) || math.IsNaN(prevClose) {
return
}
// 计算价格波动方向
bull := closePrice > prevClose
bear := closePrice < prevClose
// 更新最后一个涨停价和跌停价
if bull {
state.lastbull = closePrice
}
if bear {
state.lastbear = closePrice
}
// 如果还没有初始化lastbull和lastbear,返回
if math.IsNaN(state.lastbull) || math.IsNaN(state.lastbear) {
return
}
// 计算价格变化比例
ddl := ((closePrice / state.lastbull) - 1) * 100
dds := ((closePrice / state.lastbear) - 1) * 100
// 生成交易信号
up := ddl < signallong
dn := dds > signalshort
// 计算开仓数量(如果没有持仓)
if s.OrderNum == 0 {
// 修复:使用策略方法获取权益计算仓位
stakeAmount := s.Strat.GetStakeAmount(s)
state.lot = stakeAmount / closePrice * capitalRate
}
// truetime条件,与原始策略保持一致
truetime := true
// 执行交易
if up && needlong > 0.5 && truetime {
// 修复:使用计算好的lot值作为开仓数量
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Short: false,
Amount: state.lot,
})
}
if dn && needshort > 0.5 && truetime {
// 修复:使用计算好的lot值作为开仓数量
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
Amount: state.lot,
})
}
// 平仓条件:开多仓后价格上涨就平多仓,开空仓后价格下跌就平空仓
if len(s.LongOrders) > 0 && closePrice > openPrice {
// 修复:使用CloseOrders进行平仓
s.CloseOrders(&strat.ExitReq{
Tag: "CloseLong",
Dirt: core.OdDirtLong,
})
}
if len(s.ShortOrders) > 0 && closePrice < openPrice {
// 修复:使用CloseOrders进行平仓
s.CloseOrders(&strat.ExitReq{
Tag: "CloseShort",
Dirt: core.OdDirtShort,
})
}
},
}
}

64
llmjob/32.go Normal file
View File

@@ -0,0 +1,64 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J32(pol *config.RunPolicyConfig) *strat.TradeStrat {
smaLength := pol.DefInt("smaLength", 30, core.PNorm(5, 30))
macdFastLength := pol.DefInt("macdFastLength", 25, core.PNorm(5, 20))
macdSlowLength := pol.DefInt("macdSlowLength", 38, core.PNorm(15, 40))
macdSignalSmoothing := pol.DefInt("macdSignalSmoothing", 3, core.PNorm(3, 15))
warmupNum := smaLength
if macdSlowLength > warmupNum {
warmupNum = macdSlowLength
}
if macdFastLength+macdSignalSmoothing > warmupNum {
warmupNum = macdFastLength + macdSignalSmoothing
}
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true,
EachMaxLong: 1,
RunTimeFrames: []string{"8h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
sma10 := ta.SMA(e.Close, smaLength)
macdLine, signalLine := ta.MACD(e.Close, macdFastLength, macdSlowLength, macdSignalSmoothing)
closePrice := e.Close.Get(0)
sma10Value := sma10.Get(0)
macdValue := macdLine.Get(0)
signalValue := signalLine.Get(0)
if math.IsNaN(closePrice) || math.IsNaN(sma10Value) || math.IsNaN(macdValue) || math.IsNaN(signalValue) {
return
}
priceCrossSMA := e.Close.Cross(sma10)
macdCrossSignal := macdLine.Cross(signalLine)
longCondition := priceCrossSMA == 1 && macdCrossSignal == 1
shortCondition := priceCrossSMA == -1 && macdCrossSignal == -1
if longCondition && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
})
s.CloseOrders(&strat.ExitReq{
Tag: "CloseShort",
Dirt: core.OdDirtShort,
})
}
if shortCondition && len(s.LongOrders) > 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
})
s.CloseOrders(&strat.ExitReq{
Tag: "CloseLong",
Dirt: core.OdDirtLong,
})
}
},
}
}

173
llmjob/3464.go Normal file
View File

@@ -0,0 +1,173 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
type TrendlineData struct {
upper float64
lower float64
slopePh float64
slopePl float64
upos int
dnos int
prevUpos int
prevDnos int
}
func J3464(pol *config.RunPolicyConfig) *strat.TradeStrat {
length := pol.DefInt("length", 13, core.PNorm(5, 30))
mult := pol.Def("mult", 1.05, core.PNorm(0.5, 2.0))
calcMethod := pol.Def("calcMethod", 0, core.PUniform(0, 2)) // 0: Atr, 1: Stdev, 2: Linreg
_ = pol.Def("backpaint", 1.0, core.PUniform(0, 1)) == 1.0 // 使用下划线忽略未使用的变量
return &strat.TradeStrat{
WarmupNum: length * 2,
HedgeOff: true, // 单向持仓
RunTimeFrames: []string{"4h"},
OnStartUp: func(s *strat.StratJob) {
s.More = &TrendlineData{
upper: 0,
lower: 0,
slopePh: 0,
slopePl: 0,
upos: 0,
dnos: 0,
prevUpos: 0,
prevDnos: 0,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
data, _ := s.More.(*TrendlineData)
// 获取当前价格
closePrice := e.Close.Get(0)
if math.IsNaN(closePrice) {
return
}
// 检测支点 - 实现与PineScript的pivothigh和pivotlow相似的功能
// 需要检查当前K线是否是过去length周期内的最高点,并且左右两侧都有length个K线
ph := false
pl := false
// 确保有足够的历史数据
if e.Close.Len() > length*2 {
// 检查当前K线是否是支点高
currentHigh := e.High.Get(length)
isPivotHigh := true
for i := 0; i < length; i++ {
if e.High.Get(i) >= currentHigh || e.High.Get(length*2-i) >= currentHigh {
isPivotHigh = false
break
}
}
// 检查当前K线是否是支点低
currentLow := e.Low.Get(length)
isPivotLow := true
for i := 0; i < length; i++ {
if e.Low.Get(i) <= currentLow || e.Low.Get(length*2-i) <= currentLow {
isPivotLow = false
break
}
}
ph = isPivotHigh && !math.IsNaN(currentHigh)
pl = isPivotLow && !math.IsNaN(currentLow)
}
// 计算斜率
var slope float64
method := int(calcMethod)
switch method {
case 0: // Atr
atr := ta.ATR(e.High, e.Low, e.Close, length)
slope = atr.Get(0) / float64(length) * mult
case 1: // Stdev
stdDev := ta.StdDev(e.Close, length)
slope = stdDev.Get(0) / float64(length) * mult
case 2: // Linreg
// 简化版的线性回归斜率计算
n := float64(length)
sumX := n * (n - 1) / 2
sumY := ta.SMA(e.Close, length).Get(0) * n
sumXY := 0.0
sumX2 := 0.0
for i := 0; i < length; i++ {
x := float64(i)
y := e.Close.Get(i)
if !math.IsNaN(y) {
sumXY += x * y
sumX2 += x * x
}
}
// 计算斜率
denominator := n*sumX2 - sumX*sumX
if denominator != 0 {
slope = (n*sumXY - sumX*sumY) / denominator * mult
} else {
slope = 0
}
}
// 更新趋势线斜率和值
if ph {
data.slopePh = slope
data.upper = e.High.Get(length)
} else {
data.upper = data.upper - data.slopePh
}
if pl {
data.slopePl = slope
data.lower = e.Low.Get(length)
} else {
data.lower = data.lower + data.slopePl
}
// 保存前一个状态
data.prevUpos = data.upos
data.prevDnos = data.dnos
// 检测突破状态
if ph {
data.upos = 0
} else if closePrice > data.upper-data.slopePh*float64(length) {
data.upos = 1
}
if pl {
data.dnos = 0
} else if closePrice < data.lower+data.slopePl*float64(length) {
data.dnos = 1
}
// 检测突破并生成交易信号
upBreakout := data.upos > data.prevUpos
dnBreakout := data.dnos > data.prevDnos
if upBreakout && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Up Breakout",
})
}
if dnBreakout && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Down Breakout",
Short: true,
})
}
},
}
}

66
llmjob/3614.go Normal file
View File

@@ -0,0 +1,66 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J3614(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
length := pol.DefInt("length", 66, core.PNorm(20, 100))
hmaRate := pol.Def("hmaRate", 8.72, core.PNorm(3, 15))
channelFactor := pol.Def("channelFactor", 0.98, core.PNorm(0.95, 0.995))
// 计算HMA实际周期
hmaPeriod := int(float64(length) * hmaRate)
return &strat.TradeStrat{
WarmupNum: hmaPeriod, // 预热数量取最大的指标周期
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"1h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算线性回归线
linreg := ta.LinReg(e.Close, length)
linregValue := linreg.Get(0)
// 计算通道上限线
band2 := linregValue * channelFactor
// 计算Hull移动平均线
hma := ta.HMA(e.Close, hmaPeriod)
hmaValue := hma.Get(0)
// 获取当前收盘价
closePrice := e.Close.Get(0)
// 检查数据有效性
if math.IsNaN(closePrice) || math.IsNaN(linregValue) || math.IsNaN(band2) || math.IsNaN(hmaValue) {
return
}
// 生成交易信号
longCondition := closePrice < band2 && hmaValue < closePrice
shortCondition := closePrice > linregValue
// 执行交易操作
if longCondition && len(s.LongOrders) == 0 {
// 满足做多条件且没有多头持仓时开仓
s.OpenOrder(&strat.EnterReq{
Tag: "BUY",
})
} else if shortCondition && len(s.LongOrders) > 0 {
// 满足平仓条件且持有多头仓位时平仓
s.CloseOrders(&strat.ExitReq{
Tag: "SELL",
Dirt: core.OdDirtLong,
})
}
},
}
}

218
llmjob/3644.go Normal file
View File

@@ -0,0 +1,218 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
// SupertrendState 存储每个品种的策略状态
type SupertrendState struct {
trend int // 趋势状态,1表示上升趋势,-1表示下降趋势
prevTrend int // 前一期的趋势状态
up float64 // 上轨
dn float64 // 下轨
tp float64 // 止盈价格
}
func J3644(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
atrPeriod := pol.DefInt("atrPeriod", 5, core.PNorm(2, 20))
atrMultiplier := pol.Def("atrMultiplier", 4.7, core.PNorm(2.0, 8.0))
profitRatio := pol.Def("profitRatio", 0.382, core.PNorm(0.2, 1.0))
useSmaATR := pol.DefInt("useSmaATR", 1, core.PUniform(0, 1)) // 1=true, 0=false
srcType := pol.DefInt("srcType", 0, core.PUniform(0, 7)) // 0=hlc3, 1=high, 2=low, 3=open, 4=hl2, 5=close, 6=hlcc4, 7=ohlc4
return &strat.TradeStrat{
WarmupNum: atrPeriod * 3, // 预热期设置为ATR周期的3倍,确保有足够数据
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多开1个多单
EachMaxShort: 1, // 最多开1个空单
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
s.More = &SupertrendState{
trend: 1, // 初始趋势状态为上升趋势
prevTrend: 1, // 前一期趋势状态也为上升趋势
tp: 0, // 初始止盈价格
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*SupertrendState)
// 获取数据源
var src *ta.Series
switch srcType {
case 0:
src = ta.HLC3(e.High, e.Low, e.Close)
case 1:
src = e.High
case 2:
src = e.Low
case 3:
src = e.Open
case 4:
src = ta.HL2(e.High, e.Low)
case 5:
src = e.Close
case 6:
// HLCC4: (high + low + close + close) / 4
src = e.High.Add(e.Low).Add(e.Close).Add(e.Close).Div(4)
case 7:
// OHLC4: (open + high + low + close) / 4
src = e.Open.Add(e.High).Add(e.Low).Add(e.Close).Div(4)
default:
src = ta.HLC3(e.High, e.Low, e.Close)
}
// 计算ATR
var atr *ta.Series
if useSmaATR == 1 {
// 使用标准ATR计算
atr = ta.ATR(e.High, e.Low, e.Close, atrPeriod)
} else {
// 使用SMA(TR)计算ATR
trueRange := ta.TR(e.High, e.Low, e.Close)
atr = ta.SMA(trueRange, atrPeriod)
}
// 计算超级趋势通道
closePrice := e.Close.Get(0)
prevClose := e.Close.Get(1)
atrVal := atr.Get(0)
srcVal := src.Get(0)
// 计算上下轨
up := srcVal - (atrMultiplier * atrVal)
dn := srcVal + (atrMultiplier * atrVal)
// 获取前一期上下轨
prevUp := state.up
prevDn := state.dn
// 如果是第一期或前一期值无效,使用当前值
if math.IsNaN(prevUp) {
prevUp = up
}
if math.IsNaN(prevDn) {
prevDn = dn
}
// 趋势跟踪逻辑
if !math.IsNaN(prevClose) && prevClose > prevUp {
up = math.Max(up, prevUp)
}
if !math.IsNaN(prevClose) && prevClose < prevDn {
dn = math.Min(dn, prevDn)
}
// 更新趋势状态
state.prevTrend = state.trend
if state.trend == -1 && closePrice > prevDn {
state.trend = 1
} else if state.trend == 1 && closePrice < prevUp {
state.trend = -1
}
// 保存当前上下轨值
state.up = up
state.dn = dn
// 生成交易信号
buySignal := state.trend == 1 && state.prevTrend == -1
sellSignal := state.trend == -1 && state.prevTrend == 1
// 获取当前订单
longOrders := s.LongOrders
shortOrders := s.ShortOrders
// 更新多头订单的止损
if len(longOrders) > 0 {
for _, od := range longOrders {
if od.Status >= ormo.InOutStatusFullEnter {
// 保持之前的止盈价
tp := state.tp
if math.IsNaN(tp) || tp <= 0 {
tp = od.InitPrice + (od.InitPrice-up)*profitRatio
}
// 更新止损价为上轨
od.SetStopLoss(&ormo.ExitTrigger{
Price: up,
Tag: "super_trend_sl",
})
// 更新止盈价
od.SetTakeProfit(&ormo.ExitTrigger{
Price: tp,
Tag: "super_trend_tp",
})
state.tp = tp
}
}
}
// 更新空头订单的止损
if len(shortOrders) > 0 {
for _, od := range shortOrders {
if od.Status >= ormo.InOutStatusFullEnter {
// 保持之前的止盈价
tp := state.tp
if math.IsNaN(tp) || tp <= 0 {
tp = od.InitPrice - (dn-od.InitPrice)*profitRatio
}
// 更新止损价为下轨
od.SetStopLoss(&ormo.ExitTrigger{
Price: dn,
Tag: "super_trend_sl",
})
// 更新止盈价
od.SetTakeProfit(&ormo.ExitTrigger{
Price: tp,
Tag: "super_trend_tp",
})
state.tp = tp
}
}
}
// 买入信号
if buySignal && len(longOrders) == 0 {
// 计算止盈价
tp := closePrice + (closePrice-up)*profitRatio
state.tp = tp
// 开多单,使用限价单
s.OpenOrder(&strat.EnterReq{
Tag: "super_trend_long",
Short: false,
OrderType: core.OrderTypeLimit,
Limit: tp,
StopLoss: up,
StopLossTag: "super_trend_sl",
TakeProfit: tp,
TakeProfitTag: "super_trend_tp",
})
}
// 卖出信号
if sellSignal && len(shortOrders) == 0 {
// 计算止盈价
tp := closePrice - (dn-closePrice)*profitRatio
state.tp = tp
// 开空单,使用限价单
s.OpenOrder(&strat.EnterReq{
Tag: "super_trend_short",
Short: true,
OrderType: core.OrderTypeLimit,
Limit: tp,
StopLoss: dn,
StopLossTag: "super_trend_sl",
TakeProfit: tp,
TakeProfitTag: "super_trend_tp",
})
}
},
}
}

86
llmjob/3683.go Normal file
View File

@@ -0,0 +1,86 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
type MacdState struct {
lastCrossUp bool
lastCrossDown bool
}
func J3683(pol *config.RunPolicyConfig) *strat.TradeStrat {
fastPeriod := pol.DefInt("fastPeriod", 16, core.PNorm(5, 20))
emaRatio := pol.Def("emaRatio", 1.7, core.PNorm(1.5, 4))
signalPeriod := pol.DefInt("signalPeriod", 8, core.PNorm(5, 15))
profitRate := pol.Def("profitRate", 0.012, core.PNorm(0.005, 0.03))
lossRate := pol.Def("lossRate", 0.009, core.PNorm(0.005, 0.03))
// 计算慢速EMA周期
slowPeriod := int(float64(fastPeriod) * emaRatio)
// 确保预热K线数量足够计算MACD指标
warmupNum := fastPeriod + slowPeriod + signalPeriod
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的状态变量
s.More = &MacdState{
lastCrossUp: false,
lastCrossDown: false,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*MacdState)
// 计算MACD指标
macdLine, signalLine := ta.MACD(e.Close, fastPeriod, slowPeriod, signalPeriod)
// 检测金叉和死叉
crossUp := macdLine.Cross(signalLine)
crossDown := signalLine.Cross(macdLine)
// 更新交叉状态
if crossUp == 1 { // 金叉
state.lastCrossUp = true
state.lastCrossDown = false
} else if crossDown == 1 { // 死叉
state.lastCrossUp = false
state.lastCrossDown = true
}
// 获取当前价格
currentPrice := e.Close.Get(0)
// 连续金叉且当前无多头订单,开多仓
if state.lastCrossUp && crossUp == 1 && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "买入开多",
TakeProfitVal: currentPrice * (1 + profitRate),
StopLossVal: currentPrice * (1 - lossRate),
TakeProfitTag: "多单止盈",
StopLossTag: "多单止损",
})
}
// 连续死叉且当前无空头订单,开空仓
if state.lastCrossDown && crossDown == 1 && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "卖出开空",
Short: true,
TakeProfitVal: currentPrice * (1 - profitRate),
StopLossVal: currentPrice * (1 + lossRate),
TakeProfitTag: "空单止盈",
StopLossTag: "空单止损",
})
}
},
}
}

72
llmjob/3707.go Normal file
View File

@@ -0,0 +1,72 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J3707(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义策略参数
rangeLength := pol.DefInt("rangeLength", 5, core.PNorm(3, 20))
useHeikinAshi := pol.Def("useHeikinAshi", 0.0, core.PUniform(0, 1)) // 1.0表示true,0.0表示false
return &strat.TradeStrat{
WarmupNum: rangeLength + 1, // 确保有足够的历史数据计算指标
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"3d"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 获取K线数据
var open, close *ta.Series
if useHeikinAshi > 0.5 {
// 使用Heikin Ashi蜡烛图
haOpen, _, _, haClose := ta.HeikinAshi(e)
open = haOpen
close = haClose
} else {
// 使用标准K线
open = e.Open
close = e.Close
}
// 计算成交量范围
highest := ta.Highest(e.Volume, rangeLength)
_ = ta.Lowest(e.Volume.Mul(-1), rangeLength) // 最低负成交量
// 获取当前值和前一个值
highestCurr := highest.Get(0)
highestPrev := highest.Get(1)
closeCurr := close.Get(0)
openCurr := open.Get(0)
// 检查数据有效性
if math.IsNaN(highestCurr) || math.IsNaN(highestPrev) || math.IsNaN(closeCurr) || math.IsNaN(openCurr) {
return
}
// 生成交易信号
volumeBreakout := highestCurr > highestPrev
isBullish := closeCurr > openCurr
isBearish := closeCurr < openCurr
// 执行交易
if volumeBreakout && isBullish {
// 成交量突破前高且为阳线,做多
s.OpenOrder(&strat.EnterReq{
Tag: "Long Entry",
})
} else if volumeBreakout && isBearish {
// 成交量突破前高且为阴线,做空
s.OpenOrder(&strat.EnterReq{
Tag: "Short Entry",
Short: true,
})
}
},
}
}

163
llmjob/3746.go Normal file
View File

@@ -0,0 +1,163 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
type KijunData struct {
initialCapital float64
netProfit float64
}
func J3746(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
ksPeriod := pol.DefInt("ksPeriod", 30, core.PNorm(9, 50))
atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 30))
atrMultiplier := pol.Def("atrMultiplier", 1.5, core.PNorm(0.8, 4))
targetPoints := pol.Def("targetPoints", 100, core.PNorm(50, 200))
riskPercent := pol.Def("riskPercent", 4, core.PNorm(1, 10))
equityProtection := pol.Def("equityProtection", 30, core.PNorm(10, 50))
return &strat.TradeStrat{
WarmupNum: ksPeriod, // 预热K线数量,确保基均线计算有足够数据
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"3d"},
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的特定数据
s.More = &KijunData{
initialCapital: 50000.0, // 初始资金
netProfit: 0.0, // 初始净利润
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
m, _ := s.More.(*KijunData)
// 计算技术指标
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
atrValue := atr.Get(0)
// 计算基均线 (Kijun Sen)
highest := ta.Highest(e.High, ksPeriod)
lowest := ta.Lowest(e.Low, ksPeriod)
kijunSen := highest.Add(lowest).Div(2)
kijunSenValue := kijunSen.Get(0)
// 检查数据有效性
if math.IsNaN(atrValue) || math.IsNaN(kijunSenValue) {
return
}
// 生成交易信号
openPrice := e.Open.Get(0)
closePrice := e.Close.Get(0)
baseLong := openPrice < kijunSenValue && closePrice > kijunSenValue
baseShort := openPrice > kijunSenValue && closePrice < kijunSenValue
// 计算止损距离
stopLossVal := atrValue * atrMultiplier
// 更新净利润
// 计算总浮动盈亏
totalFloating := 0.0
for _, od := range s.LongOrders {
totalFloating += od.Profit
}
for _, od := range s.ShortOrders {
totalFloating += od.Profit
}
m.netProfit = totalFloating
// 权益保护检查
equityStopout := false
if s.OrderNum > 0 {
// 计算当前余额(初始资金 + 净利润)
totalBalance := m.initialCapital + m.netProfit
// 检查是否触发权益保护
if totalFloating < 0 && math.Abs(totalFloating/totalBalance) > equityProtection/100 {
equityStopout = true
}
}
// 权益保护触发时平仓所有头寸
if equityStopout {
s.CloseOrders(&strat.ExitReq{
Tag: "equity_protection",
Dirt: core.OdDirtBoth,
Force: true,
})
return
}
// 检查是否有持仓
hasLongPosition := len(s.LongOrders) > 0
hasShortPosition := len(s.ShortOrders) > 0
// 计算头寸大小
balance := m.initialCapital + m.netProfit
riskAmount := balance * (riskPercent / 100) // 风险金额
size := riskAmount / stopLossVal // 合约数量
if size < 0.001 { // 最小合约数量检查
size = 0.001
}
// 多头交易
if baseLong && !hasLongPosition && !hasShortPosition {
s.OpenOrder(&strat.EnterReq{
Tag: "l_en",
Amount: size,
StopLossVal: stopLossVal,
TakeProfitVal: targetPoints,
StopLossTag: "stop_loss",
TakeProfitTag: "take_profit",
})
}
// 空头交易
if baseShort && !hasShortPosition && !hasLongPosition {
s.OpenOrder(&strat.EnterReq{
Tag: "s_en",
Short: true,
Amount: size,
StopLossVal: stopLossVal,
TakeProfitVal: targetPoints,
StopLossTag: "stop_loss",
TakeProfitTag: "take_profit",
})
}
},
OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq {
// 计算基均线
highest := ta.Highest(s.Env.High, ksPeriod)
lowest := ta.Lowest(s.Env.Low, ksPeriod)
kijunSen := highest.Add(lowest).Div(2)
kijunSenValue := kijunSen.Get(0)
closePrice := s.Env.Close.Get(0)
// 多头平仓条件:收盘价低于基均线
if !od.Short && closePrice < kijunSenValue {
return &strat.ExitReq{
Tag: "long_exit",
}
}
// 空头平仓条件:收盘价高于基均线
if od.Short && closePrice > kijunSenValue {
return &strat.ExitReq{
Tag: "short_exit",
}
}
return nil
},
}
}

270
llmjob/3762.go Normal file
View File

@@ -0,0 +1,270 @@
package llmjob
import (
"fmt"
"math"
"sort"
"sync"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
"github.com/banbox/banexg/log"
"github.com/banbox/banexg/utils"
ta "github.com/banbox/banta"
"go.uber.org/zap"
)
/*
超跌反弹策略。30个品种为宜;已增加暴跌抄底机制。
开单越多,后续抄底价指数下移
做空部分没有利润,不稳定,小亏,禁用
*/
type State3762 struct {
NoLongUntil int64
BWidth float64
MidPrice float64
SlRange float64
volRate float64
}
func J3762(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义策略参数
p := pol.DefInt("p", 20, core.PNorm(5, 50))
d := pol.Def("d", 30.0, core.PNorm(5, 50))
mult := pol.Def("mult", 1.5, core.PNorm(1.0, 3.0))
slRate := pol.Def("slRate", 25, core.PNorm(0, 250)) //default: 25
// 某品种入场时,下移其他品种的倍率(相对BWidth)
// 0.3开单太多亏损,0.5开单不足亏损
downOtherRate := 0.33
dayMS := int64(utils.TFToSecs("1d") * 1000)
longFillNum := float64(0)
fillLock := sync.Mutex{}
var allJobs []*strat.StratJob
// 基于成交量计算的区间倍率,成交量太大时,适当调低做多和止损价格
var rangeVolRate = float64(1)
allowPairs := 1 // 允许挂单的订单数,默认为品种数,随开单减少,防止交易所故障导致下单过多
openPairs := 0 // 实际有订单的品种数(挂单+持仓)
lenAtr := 20
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
calcBuyPrice := func(m *State3762) float64 {
return m.MidPrice - m.BWidth*(1+math.Pow(longFillNum, 1.5)*downOtherRate)*rangeVolRate
}
return &strat.TradeStrat{
WarmupNum: baseAtrLen * 2, // 预热K线数量,确保有足够数据计算指标
RunTimeFrames: []string{"15m"},
BatchInOut: true,
OnStartUp: func(s *strat.StratJob) {
s.More = &State3762{}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
m, _ := s.More.(*State3762)
// 计算基础布林带指标
sma := ta.SMA(e.Close, p)
ma7 := ta.SMA(e.Close, 7).Get(0)
ma30 := ta.SMA(e.Close, 30).Get(0)
multCur := mult
if ma7 < ma30 {
// 熊市下移下限
multCur *= 1.35
}
stddev := ta.StdDev(e.Close, p).Get(0) * multCur
upperBB := sma.Get(0) + stddev
lowerBB := sma.Get(0) - stddev
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
maVol := ta.EMA(e.Volume, lenAtr)
volBase := ta.Lowest(ta.Highest(maVol, lenAtr), baseAtrLen).Get(0)
m.volRate = maVol.Get(0) / volBase
volH3Rate := ta.Highest(maVol, 3).Get(0) / volBase
// 计算百分比偏离轨道
atrRt := d
if ma7 < ma30 {
// 熊市下移下限
atrRt *= 1.35
}
upex2 := sma.Get(0) + atrBase*atrRt
dnex2 := sma.Get(0) - atrBase*atrRt
// 计算最终轨道
upexPrice := max(upperBB, (upex2+upperBB)/2)
dnexPrice := min(lowerBB, (dnex2+lowerBB)/2)
m.BWidth = (upexPrice - dnexPrice) / 2
m.MidPrice = (upexPrice + dnexPrice) / 2
//lowPrice, highPrice := dnexPrice, upexPrice
lowPrice := calcBuyPrice(m)
// 获取当前价格
closePrice := e.Close.Get(0)
// 平仓条件:阳线
if len(s.LongOrders) > 0 && s.LongOrders[0].Status >= ormo.InOutStatusPartEnter {
doExit := closePrice > e.Open.Get(0)
if volH3Rate > 5 {
// 放巨量入场的订单,等待阳线后一个K线出场
doExit = e.Close.Get(1) > e.Open.Get(1)
}
if doExit {
s.CloseOrders(&strat.ExitReq{
Tag: "ExitLong",
Dirt: core.OdDirtLong,
Log: true,
})
}
}
if math.IsNaN(lowPrice) || lowPrice <= 0 {
return
}
slRange := min(atrBase*slRate, closePrice*0.25) * min(3, rangeVolRate)
m.SlRange = slRange
// 执行交易逻辑
//if !s.IsWarmUp && e.TimeStop >= 1760130900000 {
// line := fmt.Sprintf("%s upp: %.6f, low: %.6f upex2: %.6f, dnex2: %.6f dnex: %.6f slRange: %.6f",
// e.Symbol, upperBB, lowerBB, upex2, dnex2, lowPrice, slRange)
// s.Strat.WriteOutput(line, true)
//}
if len(s.LongOrders) == 0 {
// 使用限价单在下轨价格做多,不依赖当前价格是否突破轨道
if e.TimeStop >= m.NoLongUntil && openPairs < allowPairs {
fillLock.Lock()
openPairs += 1
fillLock.Unlock()
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Limit: lowPrice, // 以下轨价格作为限价单
StopLossVal: slRange,
Log: true,
})
}
} else {
for _, od := range s.LongOrders {
if od.Status == ormo.InOutStatusInit {
err := od.SetEnterLimit(lowPrice)
if err != nil {
log.Error("SetEnterLimit fail", zap.Error(err))
} else {
err = od.SetStopLoss(&ormo.ExitTrigger{
Price: lowPrice - slRange,
})
if err != nil {
log.Error("SetStopLoss fail", zap.Error(err))
}
}
} else if od.Status == ormo.InOutStatusFullEnter {
barNum := e.BarCount(od.Enter.UpdateAt)
if barNum > 2 && od.ProfitRate < 0 {
// 持仓2个k线,出现亏损,平仓
s.CloseOrders(&strat.ExitReq{
Tag: "ExitBad",
Dirt: core.OdDirtLong,
Log: true,
})
m.NoLongUntil = e.TimeStop + dayMS
}
}
}
}
},
OnBatchJobs: func(jobs []*strat.StratJob) {
fillLock.Lock()
longFillNum = 0
openPairs = 0
volRates := make([]float64, 0, len(jobs))
for _, s := range jobs {
if len(s.LongOrders) > 0 {
openPairs += 1
for _, od := range s.LongOrders {
if od.Status >= ormo.InOutStatusPartEnter {
longFillNum += 1
}
}
}
m, _ := s.More.(*State3762)
volRates = append(volRates, m.volRate)
}
fillLock.Unlock()
allJobs = jobs
sort.Float64s(volRates)
rangeVolRate = max(1, math.Pow(volRates[len(volRates)/2]/2, 0.3))
allowPairs = int(math.Round(math.Pow(0.8, longFillNum) * float64(len(allJobs))))
//jobs[0].Strat.WriteOutput(fmt.Sprintf("%.3f", rangeVolRate), true)
},
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
if chgType == core.OdChgExitFill {
if od.Enter.Amount > 0 {
m, _ := s.More.(*State3762)
m.NoLongUntil = od.ExitAt + dayMS
fillLock.Lock()
longFillNum -= 1
openPairs -= 1
fillLock.Unlock()
}
} else if chgType == core.OdChgEnterFill {
fillLock.Lock()
longFillNum += 1
openPairs = 0 // 这里应当重新计算,避免并发和回调导致错误
for _, job := range allJobs {
if len(job.LongOrders) > 0 {
openPairs += 1
}
}
fillLock.Unlock()
// 降低允许开仓的品种数,避免交易所故障导致下单太多
allowPairs = int(math.Round(math.Pow(0.85, longFillNum) * float64(len(allJobs))))
needClose := openPairs - allowPairs // 需要关闭的订单数
if needClose > 0 {
// 排序确保多次回测一致
sort.Slice(allJobs, func(i, j int) bool {
return allJobs[i].Symbol.Symbol < allJobs[j].Symbol.Symbol
})
}
odMgr := biz.GetOdMgr(s.Account)
// 移动其他品种的入场价格,避免同时开仓太多
for _, job := range allJobs {
if job == s || len(job.LongOrders) == 0 {
continue
}
m, _ := job.More.(*State3762)
lowPrice := calcBuyPrice(m)
for _, odr := range job.LongOrders {
if odr.Status <= ormo.InOutStatusPartEnter {
if lowPrice <= 0 || needClose > 0 {
needClose -= 1
job.CloseOrders(&strat.ExitReq{
Tag: "cancel",
OrderID: odr.ID,
Force: true,
})
odMgr.ProcessOrders(job)
} else {
err := odr.SetEnterLimit(lowPrice)
if err == nil {
odr.EnterTag = fmt.Sprintf("long%d", int(longFillNum))
odr.SetStopLoss(&ormo.ExitTrigger{
Price: max(lowPrice-m.SlRange, lowPrice*0.1),
})
}
}
} else {
odr.SetStopLoss(&ormo.ExitTrigger{
Price: max(lowPrice-m.SlRange, odr.Enter.Price*0.1),
})
}
}
}
}
},
}
}

310
llmjob/3782.go Normal file
View File

@@ -0,0 +1,310 @@
package llmjob
import (
"fmt"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
"sort"
"sync"
)
/*
需要多品种综合市场信息,品种数不可少于30
此策略有综合市场信息并限制开单数量,回测和实盘开单预期不一致,但开单数量基本符合
openMaxs中的前两个减少会降低收益
待改进:
zone1信号在22年有几次略大回撤
*/
// ZoneTradeState 用于存储每个交易区间的状态信息
type ZoneTradeState struct {
LastTimes [3]int64 // 3个买入信号的最后交易时间
LastSellTime int64 // 最后卖出时间
atrBase float64
score float64
upFlag float64
volRate float64
}
func J3782(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
maPeriod := pol.DefInt("maPeriod", 100, core.PNorm(20, 200))
baseRetra := pol.Def("baseRetra", 0.08, core.PNorm(0.05, 0.2))
retraRate := pol.Def("retraRate", 1.8, core.PNorm(1.5, 2.5))
profitRate := pol.Def("profitRate", 0.03, core.PNorm(0.015, 0.06))
tradeDelay := pol.Def("tradeDelay", 40, core.PNorm(10, 100))
slRate := pol.Def("slRate", 63, core.PNorm(5, 35))
openMaxs := [3]int{2, 8, 13} // 同一个bar最大同时允许开首单的数量
holdMaxs := [3]int{25, 35, 50} // 最大同时持仓数
buyWeis := [3]float64{1, 1.5, 3} // 不同信号开仓倍率
maxJobTrades := [3]int{1, 3, 4} // 单个品种最大开仓数量
// 计算各回撤比例
retracement2 := baseRetra
retracement3 := baseRetra * retraRate
retracement4 := baseRetra * retraRate * 1.33
lenAtr := 20
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
rates := make([]float64, 0, 10)
allOpens := 0
openNums := [3]int{0, 0, 0} // 同一个bar开单数量
holdNums := [3]int{0, 0, 0} // 总计持仓数量
openUnits := 0.0
var pairLock sync.Mutex
addThres, firstRate := 1.0, 1.0
avgAtrBase := 2.5
maxAllowUnit := 3.0 // 这里默认为品种数
rangeVolRate := 1.0 // 全局成交量倍率
checkMaxPair := func(level int, openNum int) (int, float64) {
if openNum >= maxJobTrades[level] {
return -1, 0
}
costRate := buyWeis[level]
pairLock.Lock()
if openUnits < maxAllowUnit && openNums[level] < openMaxs[level] && holdNums[level] < holdMaxs[level] {
openNums[level] += 1
holdNums[level] += 1
allOpens += 1
openUnits += costRate
} else {
level = -1
}
pairLock.Unlock()
return level, costRate
}
return &strat.TradeStrat{
WarmupNum: maPeriod,
BatchInfo: true,
BatchInOut: true,
RunTimeFrames: []string{"15m"},
OnStartUp: func(s *strat.StratJob) {
// 初始化状态信息
s.More = &ZoneTradeState{
LastTimes: [3]int64{},
}
},
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "4h", baseAtrLen * 2},
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*ZoneTradeState)
// 计算MA100
ma100 := ta.SMA(e.Close, maPeriod)
ma100Val := ma100.Get(0)
// 计算交易价格水平
curRate := max(addThres, 1+state.score/10)
itemRate := max(0.7, min(math.Sqrt(state.atrBase/avgAtrBase), 1.4))
buyPrice2 := ma100Val * (1 - retracement2*curRate*itemRate*firstRate)
buyPrice3 := ma100Val * (1 - retracement3*curRate*itemRate)
buyPrice4 := ma100Val * (1 - retracement4*curRate*itemRate)
sellPrice := ma100Val * (1 + profitRate)
gapRange := (buyPrice2 - buyPrice4) / 2
maVol := ta.EMA(e.Volume, lenAtr)
volBase := ta.Lowest(ta.Highest(maVol, lenAtr), baseAtrLen).Get(0)
state.volRate = maVol.Get(0) / volBase
// 获取当前价格
lowPrice := e.Low.Get(0)
highPrice := e.High.Get(0)
// 检查交易间隔条件
currentBarTime := e.TimeStop
lastEnt := max(state.LastTimes[0], state.LastTimes[1], state.LastTimes[2])
canTrade := e.BarCount(lastEnt) >= 10
canTrade2 := canTrade && e.BarCount(state.LastTimes[0]) >= tradeDelay
canTrade3 := canTrade && e.BarCount(state.LastTimes[1]) >= tradeDelay
canTrade4 := canTrade && e.BarCount(state.LastTimes[2]) >= tradeDelay
canSell := e.BarCount(state.LastSellTime) >= tradeDelay
// 获取当前总开仓数量,与原始策略的strategy.opentrades保持一致
openTrades := len(s.LongOrders) + len(s.ShortOrders)
buyIdx, costRate := -1, 0.0
if lowPrice <= buyPrice4 && canTrade4 {
buyIdx, costRate = checkMaxPair(2, openTrades)
} else if lowPrice <= buyPrice3 && canTrade3 {
buyIdx, costRate = checkMaxPair(1, openTrades)
} else if lowPrice <= buyPrice2 && canTrade2 {
buyIdx, costRate = checkMaxPair(0, openTrades)
}
if buyIdx >= 0 && costRate > 0 {
callNum := allOpens
//if buyIdx == 0 {
// // 对信号1,累计开单较多时,下移入场价
// pairLock.Lock()
// callNum = max(callNum, int(math.Round(float64(holdNums[0])*0.5))-3)
// pairLock.Unlock()
//}
// 同一个bar开单多次,从第三次开始,使用ln(num)下移入场价
currRate := 1 + max(0, math.Log10(float64(callNum-2)))
// 用倒抛物线映射成交量倍率,作为入场价距离调整系数
volRate := 1 + max(0, 0.4-math.Pow(1.4*(rangeVolRate-1.5), 2))
rangeRate := math.Pow(volRate*currRate, 0.3) - 1
limitPrice := float64(0)
if (rangeVolRate > 1.2 || callNum > 2) && rangeRate > 0.01 {
limitPrice = e.Close.Get(0) - rangeRate*gapRange
}
tag := fmt.Sprintf("zone%d", buyIdx+1)
s.OpenOrder(&strat.EnterReq{
Tag: tag,
CostRate: costRate,
Limit: limitPrice,
Log: true,
StopBars: 3,
})
state.LastTimes[buyIdx] = currentBarTime
}
// 检查卖出条件
if s.OrderNum > 0 {
curPrice := e.Close.Get(0)
slRange := slRate * itemRate * curPrice / 100
avgPrice := s.PositionAvgPrice(core.OdDirtLong, "")
if curPrice+slRange <= avgPrice {
s.CloseOrders(&strat.ExitReq{
Tag: "StopLoss",
Log: true,
})
state.LastSellTime = currentBarTime
} else {
exitPrice := avgPrice*(1+profitRate)*0.4 + sellPrice*0.6
if openTrades > 0 && highPrice >= exitPrice && canSell {
s.CloseOrders(&strat.ExitReq{
Tag: "SellAll",
Log: true,
})
state.LastSellTime = currentBarTime
}
}
}
},
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
// 多指标综合计算大周期行情下跌分数
score := float64(0)
if ta.RSI(e.Close, 14).Get(0) < 30 {
score += 1
}
roc := ta.ROC(e.Close, 20).Get(0)
if roc < -20 {
score += 2
} else if roc < -10 {
score += 1
}
plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14)
adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0)
if minusDi.Get(0) > plusDi.Get(0) {
if adx > 40 {
score += 2
} else if adx > 25 {
score += 1
}
}
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
rates = append(rates, atrBase/(e.Close.Get(0)*0.01))
//openRate := 0.006 * closePrice / atrBase
//if openRate < 1 {
// openRate = math.Sqrt(openRate)
//}
//openRate = max(0.7, min(1.5, openRate))
ma7 := ta.SMA(e.Close, 7).Get(0)
ma20 := ta.SMA(e.Close, 20)
ma20V := ma20.Get(0)
ma99 := ta.SMA(e.Close, 99).Get(0)
if ma99 > ma20V {
score += 1
}
state, _ := s.More.(*ZoneTradeState)
state.score = score
state.upFlag = 0
state.atrBase = atrBase / (e.Close.Get(0) * 0.01)
if ma20V > ma99 && ma20V > ma20.Get(2) {
state.upFlag = 1
} else if ma7 < min(ma20V, ma99) {
state.upFlag = -1
}
},
OnBatchJobs: func(jobs []*strat.StratJob) {
pairLock.Lock()
openUnits = 0
allOpens = 0
openNums = [3]int{0, 0, 0}
holdNums = [3]int{0, 0, 0}
volRates := make([]float64, 0, len(jobs))
for _, j := range jobs {
for _, od := range j.LongOrders {
if od.EnterTag == "zone1" {
holdNums[0] += 1
} else if od.EnterTag == "zone2" {
holdNums[1] += 1
} else if od.EnterTag == "zone3" {
holdNums[2] += 1
}
}
m, _ := j.More.(*ZoneTradeState)
volRates = append(volRates, m.volRate)
}
pairLock.Unlock()
sort.Float64s(volRates)
rangeVolRate = max(1, math.Pow(volRates[len(volRates)/2]/2, 0.3))
},
OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) {
if len(jobs) < 3 {
return
}
// 多品种综合计算整体市场下行激烈程度
downNum, downHeavyNum := float64(0), float64(0)
atrRates := make([]float64, 0, len(jobs))
for _, j := range jobs {
state, _ := j.Job.More.(*ZoneTradeState)
if state.score >= 5 {
downHeavyNum += 1
}
if state.score >= 3 {
downNum += 1
}
atrRates = append(atrRates, state.atrBase)
}
jobNum := float64(len(jobs))
downRate := downNum / jobNum
downHeavyRate := downHeavyNum / jobNum
sort.Float64s(atrRates)
avgAtrBase = atrRates[len(jobs)/2]
if downHeavyRate >= 0.8 {
// 极端下跌
addThres, firstRate = 1.6, 1.2
maxAllowUnit = jobNum * 0.5
} else if downRate >= 0.85 && downHeavyRate >= 0.6 {
// 严重下跌
addThres, firstRate = 1.45, 1.15
maxAllowUnit = jobNum * 0.55
} else if downRate >= 0.7 && downRate <= 0.85 {
// 市场中度下跌
addThres, firstRate = 1.3, 1.1
maxAllowUnit = jobNum * 0.6
} else if downRate >= 0.5 && downRate <= 0.7 {
// 轻度下跌
addThres, firstRate = 1.15, 1.05
maxAllowUnit = jobNum * 0.7
} else {
// 轻度或未下跌
maxAllowUnit = jobNum
addThres, firstRate = 1, 1
}
},
}
}

144
llmjob/3827.go Normal file
View File

@@ -0,0 +1,144 @@
package llmjob
import (
"math"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
// FibRSIGoldenCross 用于存储每个品种的斐波那契回撤水平
type FibRSIGoldenCross struct {
FibHigh float64
FibLow float64
Fib38 float64
Fib50 float64
Fib61 float64
}
func J3827(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
short_ma_length := pol.DefInt("short_ma_length", 40, core.PNorm(20, 100))
ma_ratio := pol.Def("ma_ratio", 3.8, core.PNorm(2, 6))
rsi_length := pol.DefInt("rsi_length", 14, core.PNorm(7, 21))
rsi_overbought := pol.Def("rsi_overbought", 70, core.PNorm(60, 80))
rsi_oversold := pol.Def("rsi_oversold", 30, core.PNorm(20, 40))
fib_period := pol.DefInt("fib_period", 50, core.PNorm(20, 100))
// 计算长期均线周期
long_ma_length := int(float64(short_ma_length) * ma_ratio)
// 计算预热数量,取所有指标中最大的周期
warmup_num := int(math.Max(float64(long_ma_length), float64(fib_period)))
return &strat.TradeStrat{
WarmupNum: warmup_num,
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多一个多头订单
EachMaxShort: 1, // 最多一个空头订单
TimeFrames: "8h",
OnStartUp: func(s *strat.StratJob) {
// 初始化斐波那契回撤水平
s.More = &FibRSIGoldenCross{
FibHigh: math.NaN(),
FibLow: math.NaN(),
Fib38: math.NaN(),
Fib50: math.NaN(),
Fib61: math.NaN(),
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
fib, _ := s.More.(*FibRSIGoldenCross)
// 计算技术指标
rsi := ta.RSI(e.Close, rsi_length)
short_ma := ta.SMA(e.Close, short_ma_length)
long_ma := ta.SMA(e.Close, long_ma_length)
// 获取当前价格
close_price := e.Close.Get(0)
if math.IsNaN(close_price) {
return
}
// 更新斐波那契回撤水平
highest := ta.Highest(e.Close, fib_period).Get(0)
lowest := ta.Lowest(e.Close, fib_period).Get(0)
// 如果最高价或最低价发生变化,更新斐波那契回撤水平
if !math.IsNaN(highest) && (math.IsNaN(fib.FibHigh) || highest != fib.FibHigh) {
fib.FibHigh = highest
}
if !math.IsNaN(lowest) && (math.IsNaN(fib.FibLow) || lowest != fib.FibLow) {
fib.FibLow = lowest
}
// 计算斐波那契回撤水平
if !math.IsNaN(fib.FibHigh) && !math.IsNaN(fib.FibLow) {
fib_range := fib.FibHigh - fib.FibLow
fib.Fib38 = fib.FibHigh - fib_range*0.382
fib.Fib50 = fib.FibHigh - fib_range*0.5
fib.Fib61 = fib.FibHigh - fib_range*0.618
}
// 获取当前指标值
rsi_value := rsi.Get(0)
short_ma_value := short_ma.Get(0)
long_ma_value := long_ma.Get(0)
// 检查指标是否有效
if math.IsNaN(rsi_value) || math.IsNaN(short_ma_value) || math.IsNaN(long_ma_value) ||
math.IsNaN(fib.Fib50) {
return
}
// 检测交叉信号 - 只计算一次交叉信号
cross_signal := short_ma.Cross(long_ma)
golden_cross := cross_signal == 1 // 1表示上穿
death_cross := cross_signal == -1 // -1表示下穿
// 检查当前持仓
long_position := s.Position(1, "") > 0 // 1表示多头
short_position := s.Position(-1, "") > 0 // -1表示空头
// 多头入场条件:黄金交叉且价格在50%斐波那契水平之上且RSI未超买
long_condition := golden_cross && close_price > fib.Fib50 && rsi_value < rsi_overbought
// 空头入场条件:死亡交叉且价格在50%斐波那契水平之下且RSI未超卖
short_condition := death_cross && close_price < fib.Fib50 && rsi_value > rsi_oversold
// 执行交易
if long_condition && !long_position {
s.OpenOrder(&strat.EnterReq{
Tag: "Buy",
})
}
if short_condition && !short_position {
s.OpenOrder(&strat.EnterReq{
Tag: "Sell",
Short: true,
})
}
// 多头平仓条件:RSI超买
if long_position && rsi_value > rsi_overbought {
s.CloseOrders(&strat.ExitReq{
Tag: "Buy",
Dirt: core.OdDirtLong,
})
}
// 空头平仓条件:RSI超卖
if short_position && rsi_value < rsi_oversold {
s.CloseOrders(&strat.ExitReq{
Tag: "Sell",
Dirt: core.OdDirtShort,
})
}
},
}
}

160
llmjob/3939.go Normal file
View File

@@ -0,0 +1,160 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func J3939(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
lwstPeriod := pol.DefInt("lwst_period", 10, core.PNorm(5, 25))
lwstMultiplier := pol.Def("lwst_multiplier", 3.0, core.PNorm(1.5, 5.5))
lwstLength := pol.DefInt("lwst_length", 20, core.PNorm(10, 40))
trendLength := pol.DefInt("trend_length", 14, core.PNorm(7, 25))
trendThreshold := pol.Def("trend_threshold", 0.5, core.PNorm(0.1, 2.0))
wtChannelLength := pol.DefInt("wt_channel_length", 9, core.PNorm(5, 20))
wtAverageLength := pol.DefInt("wt_average_length", 12, core.PNorm(6, 24))
wtMaLength := pol.DefInt("wt_ma_length", 3, core.PNorm(2, 8))
wtOverbought := pol.Def("wt_overbought", 53, core.PNorm(40, 70))
wtOversold := pol.Def("wt_oversold", -53, core.PNorm(-70, -40)) // 修复:独立的超卖参数
slPercent := pol.Def("sl_percent", 2.0, core.PNorm(0.5, 5.0))
tpPercent := pol.Def("tp_percent", 4.0, core.PNorm(1.0, 10.0))
warmupNum := max(lwstPeriod*2, trendLength*2, wtChannelLength+wtAverageLength)
return &strat.TradeStrat{
Name: "Multi-Indicator-Consensus",
WarmupNum: warmupNum + 50,
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1,
EachMaxShort: 1,
OnBar: func(s *strat.StratJob) {
e := s.Env
currentPrice := e.Close.Get(0)
// =================== Indicator 1: Liquidity Weighted Supertrend ===================
// Volume-weighted component for dynamic sensitivity
volSma := ta.SMA(e.Volume, lwstLength)
volWeight := e.Volume.Div(volSma)
// ATR-based bands with volume weighting
atr := ta.ATR(e.High, e.Low, e.Close, lwstPeriod)
hl2 := ta.HL2(e.High, e.Low)
_ = hl2.Add(atr.Mul(lwstMultiplier).Mul(volWeight)) // upperBand
_ = hl2.Sub(atr.Mul(lwstMultiplier).Mul(volWeight)) // lowerBand
// Trend determination based on price action and previous trend
// 修复:使用Series保存趋势状态
lwstTrendSeries := e.Close.To("_lwst_trend", 0)
var lwstTrend float64
if e.Close.Len() > 1 {
prevTrend := lwstTrendSeries.Get(1)
if currentPrice > prevTrend {
lwstTrend = 1
} else if currentPrice < prevTrend {
lwstTrend = -1
} else {
lwstTrend = prevTrend
}
} else {
lwstTrend = 1.0
}
lwstTrendSeries.Set(lwstTrend)
// =================== Indicator 2: Trend Signals ===================
// Dual EMA system for trend detection
fastEma := ta.EMA(e.Close, trendLength)
slowEma := ta.EMA(e.Close, trendLength*2)
trendDiff := fastEma.Sub(slowEma).Div(slowEma).Mul(100)
trendDiffVal := trendDiff.Get(0)
var trendSignal float64
if trendDiffVal > trendThreshold {
trendSignal = 1
} else if trendDiffVal < -trendThreshold {
trendSignal = -1
} else {
trendSignal = 0
}
// =================== Indicator 3: Enhanced Wavetrend ===================
// Calculate Wavetrend components
ap := ta.HLC3(e.High, e.Low, e.Close)
esa := ta.EMA(ap, wtChannelLength)
d := ta.EMA(ap.Sub(esa).Abs(), wtChannelLength)
ci := ap.Sub(esa).Div(d.Mul(0.015))
tci := ta.EMA(ci, wtAverageLength)
// Generate main and signal lines
wt1 := tci
wt2 := ta.SMA(wt1, wtMaLength)
// Generate Wavetrend Signal based on overbought/oversold conditions
wt1Val := wt1.Get(0)
wt2Val := wt2.Get(0)
// 修复:使用Series保存wt_signal状态
wtSignalSeries := e.Close.To("_wt_signal", 0)
var wtSignal float64
if wt1Val > wtOverbought && wt2Val > wtOverbought {
wtSignal = -1 // 超买信号
} else if wt1Val < wtOversold && wt2Val < wtOversold { // 修复:使用独立的wtOversold参数
wtSignal = 1 // 超卖信号
} else {
wtSignal = wtSignalSeries.Get(1)
}
wtSignalSeries.Set(wtSignal)
// =================== Consensus Signal Generation ===================
// 修复:正确计算净信号数(范围-3到3)
consensusCount := 0
if lwstTrend == 1 {
consensusCount++
} else if lwstTrend == -1 {
consensusCount--
}
if trendSignal == 1 {
consensusCount++
} else if trendSignal == -1 {
consensusCount--
}
if wtSignal == 1 {
consensusCount++
} else if wtSignal == -1 {
consensusCount--
}
// Generate trading signals when majority (2+ indicators) agree
buySignal := consensusCount >= 2
sellSignal := consensusCount <= -2
// =================== Trade Execution ===================
if buySignal && len(s.LongOrders) == 0 && len(s.ShortOrders) == 0 {
// 计算止损止盈价格
slAmount := currentPrice * slPercent / 100
tpAmount := currentPrice * tpPercent / 100
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
StopLossVal: slAmount,
TakeProfitVal: tpAmount,
})
} else if sellSignal && len(s.ShortOrders) == 0 && len(s.LongOrders) == 0 {
// 计算做空止损止盈价格
slAmount := currentPrice * slPercent / 100
tpAmount := currentPrice * tpPercent / 100
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Short: false,
StopLossVal: slAmount,
TakeProfitVal: tpAmount,
})
}
},
}
}

109
llmjob/3955.go Normal file
View File

@@ -0,0 +1,109 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J3955(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
macdFastPeriod := pol.DefInt("macdFastPeriod", 14, core.PNorm(5, 20))
macdSlowRate := pol.Def("macdSlowRate", 2, core.PNorm(1.5, 4))
macdSignalPeriod := pol.DefInt("macdSignalPeriod", 9, core.PNorm(5, 15))
vwmaFastPeriod := pol.DefInt("vwmaFastPeriod", 20, core.PNorm(10, 40))
leverage := pol.DefInt("leverage", 1, core.PNorm(1, 20))
// 计算MACD慢线周期
macdSlowPeriod := int(float64(macdFastPeriod) * macdSlowRate)
return &strat.TradeStrat{
WarmupNum: macdSlowPeriod + macdSignalPeriod, // 确保有足够的数据计算指标
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多一个多头订单
EachMaxShort: 1, // 最多一个空头订单
RunTimeFrames: []string{"8h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算MACD指标
macdLine, signalLine := ta.MACD(e.Close, macdFastPeriod, macdSlowPeriod, macdSignalPeriod)
histogram := macdLine.Sub(signalLine) // 计算MACD柱状图
// 计算VWMA指标
vwma20 := ta.VWMA(e.Close, e.Volume, vwmaFastPeriod) // 使用VWMA替代WMA,考虑成交量因素
vwma50 := ta.VWMA(e.Close, e.Volume, 50) // 使用VWMA替代WMA,考虑成交量因素
// 获取当前指标值
macdLineVal := macdLine.Get(0)
signalLineVal := signalLine.Get(0)
histogramVal := histogram.Get(0)
vwma20Val := vwma20.Get(0)
vwma50Val := vwma50.Get(0)
// 检查数据有效性
if math.IsNaN(macdLineVal) || math.IsNaN(signalLineVal) || math.IsNaN(histogramVal) ||
math.IsNaN(vwma20Val) || math.IsNaN(vwma50Val) {
return
}
// 生成MACD信号
macdLongEntrySignal := histogramVal > 0
macdShortEntrySignal := histogramVal < 0
// 生成VWMA信号
vwmaLongEntrySignal := vwma20Val > vwma50Val
vwmaShortEntrySignal := vwma20Val < vwma50Val
// 生成组合交易信号
longEntry := macdLongEntrySignal && vwmaLongEntrySignal
shortEntry := macdShortEntrySignal && vwmaShortEntrySignal
// 检测MACD交叉信号作为出场条件(优化为单次调用)
cross := macdLine.Cross(signalLine)
longExit := cross == -1 // MACD线下穿信号线
shortExit := cross == 1 // MACD线上穿信号线
// 执行交易逻辑
if longEntry && len(s.LongOrders) == 0 {
// 计算杠杆化后的合约数量
costRate := float64(leverage)
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Short: false,
CostRate: costRate,
Leverage: float64(leverage),
OrderType: core.OrderTypeMarket,
})
}
if longExit && len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "LongExit",
Dirt: core.OdDirtLong,
})
}
if shortEntry && len(s.ShortOrders) == 0 {
// 计算杠杆化后的合约数量
costRate := float64(leverage)
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
CostRate: costRate,
Leverage: float64(leverage),
OrderType: core.OrderTypeMarket,
})
}
if shortExit && len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "ShortExit",
Dirt: core.OdDirtShort,
})
}
},
}
}

94
llmjob/3956.go Normal file
View File

@@ -0,0 +1,94 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J3956(pol *config.RunPolicyConfig) *strat.TradeStrat {
fastLength := pol.DefInt("fastLength", 9, core.PNorm(5, 20))
slowMaRate := pol.Def("slowMaRate", 2.17, core.PNorm(1.5, 4))
signalLength := pol.DefInt("signalLength", 20, core.PNorm(9, 30))
entryLevel := pol.Def("entryLevel", 0, core.PNorm(-5, 5))
longExitLevel := pol.Def("longExitLevel", 11, core.PNorm(5, 20))
shortExitLevel := pol.Def("shortExitLevel", -9, core.PNorm(-20, -5))
// 计算慢速EMA周期
slowLength := int(float64(fastLength) * slowMaRate)
// 确定预热数量,取所有参数中的最大值
warmupNum := fastLength
if slowLength > warmupNum {
warmupNum = slowLength
}
if signalLength > warmupNum {
warmupNum = signalLength
}
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"8h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算MACD指标
fastEma := ta.EMA(e.Close, fastLength)
slowEma := ta.EMA(e.Close, slowLength)
macd := fastEma.Sub(slowEma)
signal := ta.EMA(macd, signalLength)
hist := macd.Sub(signal)
// 获取当前值
histVal := hist.Get(0)
if math.IsNaN(histVal) {
return
}
// 检查交叉信号
entryCross := hist.Cross(entryLevel)
longEntry := entryCross == 1 // 上穿入场水平线
shortEntry := entryCross == -1 // 下穿入场水平线
longExit := hist.Cross(longExitLevel) == 1 // 上穿多头出场水平线
shortExit := hist.Cross(shortExitLevel) == -1 // 下穿空头出场水平线
// 获取当前持仓状态
longPos := s.Position(core.OdDirtLong, "")
shortPos := s.Position(core.OdDirtShort, "")
// 多头入场
if longEntry && longPos == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
})
}
// 空头入场
if shortEntry && shortPos == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
})
}
// 多头出场
if longPos > 0 && longExit {
s.CloseOrders(&strat.ExitReq{
Tag: "Long Exit",
Dirt: core.OdDirtLong,
})
}
// 空头出场
if shortPos < 0 && shortExit {
s.CloseOrders(&strat.ExitReq{
Tag: "Short Exit",
Dirt: core.OdDirtShort,
})
}
},
}
}

113
llmjob/3992.go Normal file
View File

@@ -0,0 +1,113 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J3992(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
fastLength := pol.DefInt("fastLength", 10, core.PUniform(5, 20))
macdSlowRate := pol.Def("macdSlowRate", 2.17, core.PNorm(1.5, 4))
rsiThreshold := pol.DefInt("rsiThreshold", 65, core.PUniform(60, 80))
bbMult := pol.Def("bbMult", 2.0, core.PNorm(1.5, 3.0))
rsiLength := pol.DefInt("rsiLength", 14, core.PUniform(7, 21))
// 计算其他参数
MACDLength := 9 // 固定值
slowLength := int(float64(fastLength) * macdSlowRate)
rsiOverbought := float64(rsiThreshold)
rsiOversold := 100 - rsiOverbought
stopLossPercent := 0.02 // 2% stop loss
takeProfitPercent := 0.05 // 5% take profit
// 计算预热数量,取最大值
warmupNum := slowLength
if MACDLength > warmupNum {
warmupNum = MACDLength
}
if rsiLength > warmupNum {
warmupNum = rsiLength
}
if bbLength := 20; bbLength > warmupNum {
warmupNum = bbLength
}
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"8h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 检查数据长度是否足够
if e.Close.Len() < warmupNum {
return
}
// MACD calculations
fastMA := ta.EMA(e.Close, fastLength)
slowMA := ta.EMA(e.Close, slowLength)
macdLine := fastMA.Sub(slowMA)
signalLine := ta.EMA(macdLine, MACDLength)
// RSI calculation
rsi := ta.RSI(e.Close, rsiLength)
// Bollinger Bands calculation
bbLength := 20
basis := ta.SMA(e.Close, bbLength)
stdDev := ta.StdDev(e.Close, bbLength)
dev := stdDev.Mul(bbMult)
upper := basis.Add(dev)
lower := basis.Sub(dev)
// 获取当前值
closePrice := e.Close.Get(0)
macdVal := macdLine.Get(0)
signalVal := signalLine.Get(0)
rsiVal := rsi.Get(0)
upperVal := upper.Get(0)
lowerVal := lower.Get(0)
// 检查NaN值
if math.IsNaN(closePrice) || math.IsNaN(macdVal) || math.IsNaN(signalVal) ||
math.IsNaN(rsiVal) || math.IsNaN(upperVal) || math.IsNaN(lowerVal) {
return
}
// Entry conditions
macdCrossOver := macdLine.Cross(signalLine) == 1
rsiCrossUnder := rsi.Cross(rsiOversold) == -1
longCondition := (macdCrossOver || rsiCrossUnder) && closePrice > lowerVal
macdCrossUnder := macdLine.Cross(signalLine) == -1
rsiCrossOver := rsi.Cross(rsiOverbought) == 1
shortCondition := (macdCrossUnder || rsiCrossOver) && closePrice < upperVal
// Long position logic
if longCondition && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
OrderType: core.OrderTypeMarket,
StopLossVal: closePrice * (1 - stopLossPercent),
TakeProfitVal: closePrice * (1 + takeProfitPercent),
})
}
// Short position logic
if shortCondition && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
OrderType: core.OrderTypeMarket,
StopLossVal: closePrice * (1 + stopLossPercent),
TakeProfitVal: closePrice * (1 - takeProfitPercent),
})
}
},
}
}

177
llmjob/4009.go Normal file
View File

@@ -0,0 +1,177 @@
package llmjob
import (
"math"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
// Sta4009 存储每个品种的策略状态
type Sta4009 struct {
atrTrailingStop float64 // ATR跟踪止损值
position int // 当前持仓方向(1=多头,-1=空头,0=无持仓)
entryPrice float64 // 入场价格
buyTarget float64 // 多头目标价格
sellTarget float64 // 空头目标价格
}
func J4009(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
keyValue := pol.Def("keyValue", 1, core.PNorm(0.1, 5))
atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(5, 30))
useHeikinAshi := pol.DefInt("useHeikinAshi", 0, core.PNorm(0, 1)) // 0=false, 1=true
exitPercentage := pol.Def("exitPercentage", 0.002, core.PNorm(0.001, 0.01))
rsiPeriod := pol.DefInt("rsiPeriod", 14, core.PNorm(7, 30))
donchianLength := pol.DefInt("donchianLength", 20, core.PNorm(10, 50))
return &strat.TradeStrat{
WarmupNum: donchianLength + 20, // 确保有足够数据计算所有指标
HedgeOff: true, // 单向持仓模式
TimeFrames: "1d",
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
s.More = &Sta4009{
atrTrailingStop: math.NaN(),
position: 0,
entryPrice: math.NaN(),
buyTarget: math.NaN(),
sellTarget: math.NaN(),
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*Sta4009)
// 计算基础指标
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
lossDistance := keyValue * atr.Get(0)
rsiValue := ta.RSI(e.Close, rsiPeriod).Get(0)
// 计算价格源(普通收盘价或惠金-阿希收盘价)
var priceSource *ta.Series
if useHeikinAshi > 0 {
haOpen, haHigh, haLow, haClose := ta.HeikinAshi(e)
haCloseSeries := haOpen.Add(haHigh).Add(haLow).Add(haClose).Div(4)
priceSource = haCloseSeries
} else {
priceSource = e.Close
}
currentPrice := priceSource.Get(0)
prevPrice := priceSource.Get(1)
// 计算ATR跟踪止损
if !math.IsNaN(lossDistance) && !math.IsNaN(currentPrice) && !math.IsNaN(prevPrice) {
prevStop := state.atrTrailingStop
if math.IsNaN(prevStop) {
prevStop = 0
}
if currentPrice > prevStop && prevPrice > prevStop {
// 价格在止损线上方且前一价格也在止损线上方,止损线上移
state.atrTrailingStop = math.Max(prevStop, currentPrice-lossDistance)
} else if currentPrice < prevStop && prevPrice < prevStop {
// 价格在止损线下方且前一价格也在止损线下方,止损线下移
state.atrTrailingStop = math.Min(prevStop, currentPrice+lossDistance)
} else if currentPrice > prevStop {
// 价格上穿止损线,设置新的止损线
state.atrTrailingStop = currentPrice - lossDistance
} else {
// 价格下穿止损线,设置新的止损线
state.atrTrailingStop = currentPrice + lossDistance
}
}
// 计算持仓方向
if !math.IsNaN(prevPrice) && !math.IsNaN(state.atrTrailingStop) {
if prevPrice < state.atrTrailingStop && currentPrice > state.atrTrailingStop {
state.position = 1 // 多头信号
} else if prevPrice > state.atrTrailingStop && currentPrice < state.atrTrailingStop {
state.position = -1 // 空头信号
}
// 否则保持原有方向
}
// 计算唐奇安通道
donchianUpper := ta.Highest(e.High, donchianLength)
donchianLower := ta.Lowest(e.Low, donchianLength)
donchianBasis := donchianUpper.Add(donchianLower).Div(2).Get(0)
// 计算交叉信号
above := currentPrice > state.atrTrailingStop && prevPrice <= state.atrTrailingStop
below := currentPrice < state.atrTrailingStop && prevPrice >= state.atrTrailingStop
// 生成交易信号
buySignal := currentPrice > state.atrTrailingStop && above && rsiValue > 50 && currentPrice > donchianBasis
sellSignal := currentPrice < state.atrTrailingStop && below && rsiValue < 50 && currentPrice < donchianBasis
// 执行交易
if buySignal && s.OrderNum == 0 {
state.entryPrice = currentPrice
state.buyTarget = state.entryPrice * (1 + exitPercentage)
state.sellTarget = state.entryPrice * (1 - exitPercentage)
s.OpenOrder(&strat.EnterReq{
Tag: "Buy",
OrderType: core.OrderTypeMarket,
})
} else if sellSignal && s.OrderNum == 0 {
state.entryPrice = currentPrice
state.buyTarget = state.entryPrice * (1 + exitPercentage)
state.sellTarget = state.entryPrice * (1 - exitPercentage)
s.OpenOrder(&strat.EnterReq{
Tag: "Sell",
Short: true,
OrderType: core.OrderTypeMarket,
})
}
// 退出条件检查
if len(s.LongOrders) > 0 {
od := s.LongOrders[0]
if od.Status >= ormo.InOutStatusFullEnter {
if currentPrice >= state.buyTarget {
// 达到止盈目标
s.CloseOrders(&strat.ExitReq{
Tag: "Take Profit",
Dirt: core.OdDirtLong,
Limit: state.buyTarget,
})
} else if currentPrice <= state.sellTarget {
// 触发止损
s.CloseOrders(&strat.ExitReq{
Tag: "Stop Loss",
Dirt: core.OdDirtLong,
Limit: state.sellTarget,
})
}
}
}
if len(s.ShortOrders) > 0 {
od := s.ShortOrders[0]
if od.Status >= ormo.InOutStatusFullEnter {
if currentPrice <= state.sellTarget {
// 达到止盈目标
s.CloseOrders(&strat.ExitReq{
Tag: "Take Profit",
Dirt: core.OdDirtShort,
Limit: state.sellTarget,
})
} else if currentPrice >= state.buyTarget {
// 触发止损
s.CloseOrders(&strat.ExitReq{
Tag: "Stop Loss",
Dirt: core.OdDirtShort,
Limit: state.buyTarget,
})
}
}
}
},
}
}

128
llmjob/4034.go Normal file
View File

@@ -0,0 +1,128 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
// 定义一个结构体来存储策略需要的变量
type StrategyData struct {
atrValue float64
}
func J4034(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
fastLength := pol.DefInt("fastLength", 12, core.PNorm(5, 20))
emaRate := pol.Def("emaRate", 2.33, core.PNorm(1.5, 5.0))
rsiLength := pol.DefInt("rsiLength", 14, core.PNorm(7, 30))
rsiOverbought := pol.DefInt("rsiOverbought", 70, core.PNorm(60, 85))
rsiOversold := pol.DefInt("rsiOversold", 30, core.PNorm(15, 40))
volLength := pol.DefInt("volLength", 20, core.PNorm(10, 40))
atrLength := pol.DefInt("atrLength", 14, core.PNorm(7, 30))
atrMultiplierSL := pol.Def("atrMultiplierSL", 1.8, core.PNorm(1.0, 3.0))
atrMultiplierTP := pol.Def("atrMultiplierTP", 2.5, core.PNorm(1.5, 5.0))
// 计算慢速EMA周期
slowLength := int(float64(fastLength) * emaRate)
// 确定预热数量,取所有指标中最大的周期
warmupNum := fastLength
if slowLength > warmupNum {
warmupNum = slowLength
}
if rsiLength > warmupNum {
warmupNum = rsiLength
}
if volLength > warmupNum {
warmupNum = volLength
}
if atrLength > warmupNum {
warmupNum = atrLength
}
// 额外增加1个K线用于突破确认
warmupNum += 1
return &strat.TradeStrat{
WarmupNum: warmupNum,
EachMaxLong: 1, // 最多同时持有一个多头订单
EachMaxShort: 1, // 最多同时持有一个空头订单
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化策略数据
s.More = &StrategyData{atrValue: 0}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
fastEMA := ta.EMA(e.Close, fastLength)
slowEMA := ta.EMA(e.Close, slowLength)
rsiValue := ta.RSI(e.Close, rsiLength)
volMA := ta.SMA(e.Volume, volLength)
atr := ta.ATR(e.High, e.Low, e.Close, atrLength)
// 获取当前值
closePrice := e.Close.Get(0)
highPrev := e.High.Get(1)
lowPrev := e.Low.Get(1)
volume := e.Volume.Get(0)
volMAValue := volMA.Get(0)
rsi := rsiValue.Get(0)
atrValue := atr.Get(0)
maCross := fastEMA.Cross(slowEMA)
// 检查数据有效性
if math.IsNaN(closePrice) || math.IsNaN(highPrev) || math.IsNaN(lowPrev) ||
math.IsNaN(volume) || math.IsNaN(volMAValue) || math.IsNaN(rsi) || math.IsNaN(atrValue) {
return
}
// 存储ATR值以便在跟踪止损中使用
data, _ := s.More.(*StrategyData)
data.atrValue = atrValue
// K线突破条件
longCandleBreak := closePrice > highPrev
shortCandleBreak := closePrice < lowPrev
// 入场条件
longCondition := maCross == 1 && rsi < float64(rsiOverbought) && volume > volMAValue && longCandleBreak
shortCondition := maCross == -1 && rsi > float64(rsiOversold) && volume > volMAValue && shortCandleBreak
// 执行交易
if longCondition && len(s.LongOrders) == 0 {
// 计算具体止损止盈价格
longStop := closePrice - atrMultiplierSL*atrValue
longTP := closePrice + atrMultiplierTP*atrValue
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Short: false,
OrderType: core.OrderTypeMarket,
StopLoss: longStop,
TakeProfit: longTP,
})
}
if shortCondition && len(s.ShortOrders) == 0 {
// 计算具体止损止盈价格
shortStop := closePrice + atrMultiplierSL*atrValue
shortTP := closePrice - atrMultiplierTP*atrValue
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
OrderType: core.OrderTypeMarket,
StopLoss: shortStop,
TakeProfit: shortTP,
})
}
},
}
}

477
llmjob/4067.go Normal file
View File

@@ -0,0 +1,477 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
"time"
)
// State4067 存储每个品种的策略状态
type State4067 struct {
step int
inTrade bool
tradeDirection int // 1=多头, -1=空头, 0=无
entryPrice float64
tp1Hit bool
buytp1Hit bool
buySLPrice float64
buyTP1Price float64
buyTP2Price float64
selltp1Hit bool
sellSLPrice float64
sellTP1Price float64
sellTP2Price float64
higherEMA float64 // 更高时间周期EMA值
}
// MTFCross 多时间周期EMA交叉策略
func J4067(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
fastPeriod := pol.DefInt("fastPeriod", 9, core.PNorm(5, 20))
slowRate := pol.Def("slowRate", 5.56, core.PNorm(2, 10))
higherEMARate := pol.Def("higherEMARate", 2, core.PNorm(1.5, 5))
slRate := pol.Def("slRate", 1.6, core.PNorm(0.5, 5))
tp1Rate := pol.Def("tp1Rate", 1.4, core.PNorm(0.5, 3))
beoffRate := pol.Def("beoffRate", 0.3, core.PNorm(0.1, 1))
atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 30))
// 交易时间控制参数
startHour := pol.DefInt("startHour", 1, core.PNorm(0, 23))
endHour := pol.DefInt("endHour", 25, core.PNorm(0, 47))
tradeOnMonday := pol.DefInt("tradeOnMonday", 1, core.PNorm(0, 1)) != 0
tradeOnTuesday := pol.DefInt("tradeOnTuesday", 1, core.PNorm(0, 1)) != 0
tradeOnWednesday := pol.DefInt("tradeOnWednesday", 1, core.PNorm(0, 1)) != 0
tradeOnThursday := pol.DefInt("tradeOnThursday", 1, core.PNorm(0, 1)) != 0
tradeOnFriday := pol.DefInt("tradeOnFriday", 1, core.PNorm(0, 1)) != 0
// 计算衍生参数
slowPeriod := int(float64(fastPeriod) * slowRate)
higherPeriod := int(float64(slowPeriod) * higherEMARate)
// 调整endHour为24小时格式
adjustedEndHour := endHour % 24
return &strat.TradeStrat{
WarmupNum: slowPeriod, // 预热K线数量
HedgeOff: true, // 单向持仓
EachMaxLong: 1, // 最多一个多头订单
EachMaxShort: 1, // 最多一个空头订单
RunTimeFrames: []string{"4h"},
// 初始化策略状态
OnStartUp: func(s *strat.StratJob) {
s.More = &State4067{
step: 0,
inTrade: false,
tradeDirection: 0,
entryPrice: math.NaN(),
tp1Hit: false,
buytp1Hit: false,
selltp1Hit: false,
buySLPrice: math.NaN(),
buyTP1Price: math.NaN(),
buyTP2Price: math.NaN(),
sellSLPrice: math.NaN(),
sellTP1Price: math.NaN(),
sellTP2Price: math.NaN(),
higherEMA: math.NaN(),
}
},
// 定义需要订阅的其他时间周期数据
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{Pair: "_cur_", TimeFrame: "15m", WarmupNum: higherPeriod},
}
},
// 处理更高时间周期数据
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
state, _ := s.More.(*State4067)
// 计算更高时间周期的EMA
higherEMA := ta.EMA(e.Close, higherPeriod)
if higherEMA.Len() > 0 {
state.higherEMA = higherEMA.Get(0)
}
},
// 主策略逻辑
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*State4067)
// 计算技术指标
fastEMA := ta.EMA(e.Close, fastPeriod)
slowEMA := ta.EMA(e.Close, slowPeriod)
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
// 获取当前值
closePrice := e.Close.Get(0)
fastEMAV := fastEMA.Get(0)
slowEMAV := slowEMA.Get(0)
atrV := atr.Get(0)
// 检查数据有效性
if math.IsNaN(closePrice) || math.IsNaN(fastEMAV) || math.IsNaN(slowEMAV) ||
math.IsNaN(atrV) || math.IsNaN(state.higherEMA) {
return
}
// 计算止损止盈距离
slDist := atrV * slRate
tp1Dist := atrV * tp1Rate
tp2Dist := atrV * tp1Rate * 2 // 第二止盈是第一止盈的2倍
beoffDist := atrV * beoffRate
// 检查交易时间
t := time.UnixMilli(e.TimeStart)
isTradingTime := checkTradingTime(t, startHour, adjustedEndHour,
tradeOnMonday, tradeOnTuesday, tradeOnWednesday, tradeOnThursday, tradeOnFriday)
// 更新信号状态步骤
updateStep(state, fastEMA, slowEMA)
// 生成交易信号(仅在交易时间内)
if isTradingTime {
if state.step == 1 && !state.inTrade && fastEMAV > slowEMAV && fastEMAV > state.higherEMA {
// 买入信号
state.inTrade = true
state.tradeDirection = 1
state.entryPrice = closePrice
state.buytp1Hit = false
state.tp1Hit = false
state.buySLPrice = state.entryPrice - slDist
state.buyTP1Price = state.entryPrice + tp1Dist
state.buyTP2Price = state.entryPrice + tp2Dist
state.step = 3
// 开多单
//s.OpenOrder(&strat.EnterReq{
// Tag: "Buy",
// Short: false,
// StopLoss: state.buySLPrice,
// TakeProfit: state.buyTP1Price,
// StopLossVal: slDist,
//})
} else if state.step == 2 && !state.inTrade && fastEMAV < slowEMAV && fastEMAV < state.higherEMA {
// 卖出信号
state.inTrade = true
state.tradeDirection = -1
state.entryPrice = closePrice
state.selltp1Hit = false
state.tp1Hit = false
state.sellSLPrice = state.entryPrice + slDist
state.sellTP1Price = state.entryPrice - tp1Dist
state.sellTP2Price = state.entryPrice - tp2Dist
state.step = 4
// 开空单
s.OpenOrder(&strat.EnterReq{
Tag: "Sell",
Short: true,
StopLoss: state.sellSLPrice,
TakeProfit: state.sellTP1Price,
StopLossVal: slDist,
})
}
}
// 管理多头仓位
if state.inTrade && state.tradeDirection == 1 && len(s.LongOrders) > 0 {
od := s.LongOrders[0]
highPrice := e.High.Get(0)
lowPrice := e.Low.Get(0)
// 检查是否达到第一止盈目标
if highPrice >= state.buyTP1Price && !state.tp1Hit {
state.tp1Hit = true
state.buytp1Hit = true
// 移动止损到保本位置
state.buySLPrice = state.entryPrice + beoffDist
// 平仓25%
s.CloseOrders(&strat.ExitReq{
Tag: "TP1 Hit",
Dirt: core.OdDirtLong,
ExitRate: 0.25,
OrderType: core.OrderTypeMarket,
})
// 更新剩余仓位的止损止盈
od.SetStopLoss(&ormo.ExitTrigger{
Price: state.buySLPrice,
Rate: 1.0,
Tag: "Breakeven",
})
od.SetTakeProfit(&ormo.ExitTrigger{
Price: state.buyTP2Price,
Rate: 1.0,
Tag: "TP2",
})
}
// 检查是否触发止损
if lowPrice <= state.buySLPrice && !state.tp1Hit {
// 止损出场
s.CloseOrders(&strat.ExitReq{
Tag: "SL Hit",
Dirt: core.OdDirtLong,
ExitRate: 1.0,
OrderType: core.OrderTypeMarket,
})
resetState(state)
}
// 检查是否达到第二止盈目标
if state.tp1Hit && highPrice >= state.buyTP2Price {
// 第二止盈出场
s.CloseOrders(&strat.ExitReq{
Tag: "TP2 Hit",
Dirt: core.OdDirtLong,
ExitRate: 1.0,
OrderType: core.OrderTypeMarket,
})
resetState(state)
}
// 检查保本止损是否触发
if state.tp1Hit && lowPrice <= state.buySLPrice {
// 保本止损出场
s.CloseOrders(&strat.ExitReq{
Tag: "Breakeven Hit",
Dirt: core.OdDirtLong,
ExitRate: 1.0,
OrderType: core.OrderTypeMarket,
})
resetState(state)
}
}
// 管理空头仓位
if state.inTrade && state.tradeDirection == -1 && len(s.ShortOrders) > 0 {
od := s.ShortOrders[0]
highPrice := e.High.Get(0)
lowPrice := e.Low.Get(0)
// 检查是否达到第一止盈目标
if lowPrice <= state.sellTP1Price && !state.tp1Hit {
state.tp1Hit = true
state.selltp1Hit = true
// 移动止损到保本位置
state.sellSLPrice = state.entryPrice - beoffDist
// 平仓25%
s.CloseOrders(&strat.ExitReq{
Tag: "TP1 Hit",
Dirt: core.OdDirtShort,
ExitRate: 0.25,
OrderType: core.OrderTypeMarket,
})
// 更新剩余仓位的止损止盈
od.SetStopLoss(&ormo.ExitTrigger{
Price: state.sellSLPrice,
Rate: 1.0,
Tag: "Breakeven",
})
od.SetTakeProfit(&ormo.ExitTrigger{
Price: state.sellTP2Price,
Rate: 1.0,
Tag: "TP2",
})
}
// 检查是否触发止损
if highPrice >= state.sellSLPrice && !state.tp1Hit {
// 止损出场
s.CloseOrders(&strat.ExitReq{
Tag: "SL Hit",
Dirt: core.OdDirtShort,
ExitRate: 1.0,
OrderType: core.OrderTypeMarket,
})
resetState(state)
}
// 检查是否达到第二止盈目标
if state.tp1Hit && lowPrice <= state.sellTP2Price {
// 第二止盈出场
s.CloseOrders(&strat.ExitReq{
Tag: "TP2 Hit",
Dirt: core.OdDirtShort,
ExitRate: 1.0,
OrderType: core.OrderTypeMarket,
})
resetState(state)
}
// 检查保本止损是否触发
if state.tp1Hit && highPrice >= state.sellSLPrice {
// 保本止损出场
s.CloseOrders(&strat.ExitReq{
Tag: "Breakeven Hit",
Dirt: core.OdDirtShort,
ExitRate: 1.0,
OrderType: core.OrderTypeMarket,
})
resetState(state)
}
}
// 更新交易状态
if state.inTrade && state.tradeDirection == 1 && state.step == 2 {
// 多头收到卖出信号
state.inTrade = false
state.tradeDirection = 0
state.step = 2
}
if state.inTrade && state.tradeDirection == -1 && state.step == 1 {
// 空头收到买入信号
state.inTrade = false
state.tradeDirection = 0
state.step = 1
}
},
// 自定义订单退出逻辑
OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq {
state, _ := s.More.(*State4067)
// 多头订单检查
if !od.Short && state.inTrade && state.tradeDirection == 1 {
lowPrice := s.Env.Low.Get(0)
// 检查是否触发止损
if lowPrice <= state.buySLPrice {
return &strat.ExitReq{
Tag: "SL Hit",
Dirt: core.OdDirtLong,
ExitRate: 1.0,
}
}
// 检查是否达到第二止盈目标
if state.tp1Hit {
highPrice := s.Env.High.Get(0)
if highPrice >= state.buyTP2Price {
return &strat.ExitReq{
Tag: "TP2 Hit",
Dirt: core.OdDirtLong,
ExitRate: 1.0,
}
}
}
}
// 空头订单检查
if od.Short && state.inTrade && state.tradeDirection == -1 {
highPrice := s.Env.High.Get(0)
// 检查是否触发止损
if highPrice >= state.sellSLPrice {
return &strat.ExitReq{
Tag: "SL Hit",
Dirt: core.OdDirtShort,
ExitRate: 1.0,
}
}
// 检查是否达到第二止盈目标
if state.tp1Hit {
lowPrice := s.Env.Low.Get(0)
if lowPrice <= state.sellTP2Price {
return &strat.ExitReq{
Tag: "TP2 Hit",
Dirt: core.OdDirtShort,
ExitRate: 1.0,
}
}
}
}
return nil
},
}
}
// checkTradingTime 检查当前时间是否在交易时间内
func checkTradingTime(t time.Time, startHour, adjustedEndHour int,
tradeOnMonday, tradeOnTuesday, tradeOnWednesday, tradeOnThursday, tradeOnFriday bool) bool {
// 获取当前小时和星期几
currentHour := t.Hour()
currentDay := t.Weekday()
// 检查是否在交易小时内
isTradingHour := false
if startHour < adjustedEndHour {
isTradingHour = currentHour >= startHour && currentHour < adjustedEndHour
} else {
isTradingHour = currentHour >= startHour || currentHour < adjustedEndHour
}
// 检查是否在交易日内
isTradingDay := false
switch currentDay {
case time.Monday:
isTradingDay = tradeOnMonday
case time.Tuesday:
isTradingDay = tradeOnTuesday
case time.Wednesday:
isTradingDay = tradeOnWednesday
case time.Thursday:
isTradingDay = tradeOnThursday
case time.Friday:
isTradingDay = tradeOnFriday
}
return isTradingHour && isTradingDay
}
// updateStep 更新信号状态步骤
func updateStep(state *State4067, fastEMA, slowEMA *ta.Series) {
// 检查EMA交叉
crossUp := fastEMA.Cross(slowEMA)
crossDown := slowEMA.Cross(fastEMA)
// 更新状态步骤
if (state.step == 0 || state.step == 4) && crossUp == 1 && fastEMA.Get(0) > state.higherEMA {
state.step = 1
}
if state.step == 3 && crossUp == 1 && fastEMA.Get(0) > state.higherEMA {
state.step = 3
}
if state.step == 2 && crossUp == 1 && fastEMA.Get(0) > state.higherEMA {
state.step = 1
}
if (state.step == 0 || state.step == 3) && crossDown == 1 && fastEMA.Get(0) < state.higherEMA {
state.step = 2
}
if state.step == 4 && crossDown == 1 && fastEMA.Get(0) < state.higherEMA {
state.step = 4
}
if state.step == 1 && crossDown == 1 && fastEMA.Get(0) < state.higherEMA {
state.step = 2
}
}
// resetState 重置策略状态
func resetState(state *State4067) {
state.inTrade = false
state.tradeDirection = 0
state.entryPrice = math.NaN()
state.tp1Hit = false
state.buytp1Hit = false
state.selltp1Hit = false
state.buySLPrice = math.NaN()
state.buyTP1Price = math.NaN()
state.buyTP2Price = math.NaN()
state.sellSLPrice = math.NaN()
state.sellTP1Price = math.NaN()
state.sellTP2Price = math.NaN()
state.step = 0
}

134
llmjob/419.go Normal file
View File

@@ -0,0 +1,134 @@
package llmjob
import (
"math"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
type FLIData struct {
trendLine float64
iTrend float64
}
func J419(pol *config.RunPolicyConfig) *strat.TradeStrat {
bbPeriod := pol.DefInt("bbPeriod", 38, core.PNorm(10, 50))
bbDeviations := pol.Def("bbDeviations", 0.65, core.PNorm(0.5, 3.0))
useATRFilter := pol.Def("useATRFilter", 0.0, core.PUniform(0, 1)) // 0=false, 1=true
atrPeriod := pol.DefInt("atrPeriod", 5, core.PNorm(3, 20))
return &strat.TradeStrat{
WarmupNum: bbPeriod + 5, // 确保有足够数据计算指标
HedgeOff: true, // 单向持仓模式
TimeFrames: "4h",
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的数据
s.More = &FLIData{
trendLine: 0.0,
iTrend: 0.0,
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
data, _ := s.More.(*FLIData)
// 计算布林带
bbMiddle := ta.SMA(e.Close, bbPeriod)
stdDev := ta.StdDev(e.Close, bbPeriod)
bbUpper := bbMiddle.Add(stdDev.Mul(bbDeviations))
bbLower := bbMiddle.Sub(stdDev.Mul(bbDeviations))
// 获取当前价格
closePrice := e.Close.Get(0)
highPrice := e.High.Get(0)
lowPrice := e.Low.Get(0)
// 检查数据有效性
if math.IsNaN(closePrice) || math.IsNaN(highPrice) || math.IsNaN(lowPrice) {
return
}
// 确定布林带信号
bbSignal := 0
if closePrice > bbUpper.Get(0) {
bbSignal = 1 // 看涨信号
} else if closePrice < bbLower.Get(0) {
bbSignal = -1 // 看跌信号
}
// 计算趋势线
prevTrendLine := data.trendLine
var newTrendLine float64
if useATRFilter > 0.5 { // 启用ATR过滤器
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
atrValue := atr.Get(0)
switch bbSignal {
case 1: // 看涨信号
newTrendLine = lowPrice - atrValue
if newTrendLine < prevTrendLine {
newTrendLine = prevTrendLine
}
case -1: // 看跌信号
newTrendLine = highPrice + atrValue
if newTrendLine > prevTrendLine {
newTrendLine = prevTrendLine
}
default: // 无信号
newTrendLine = prevTrendLine
}
} else { // 不启用ATR过滤器
switch bbSignal {
case 1: // 看涨信号
newTrendLine = lowPrice
if newTrendLine < prevTrendLine {
newTrendLine = prevTrendLine
}
case -1: // 看跌信号
newTrendLine = highPrice
if newTrendLine > prevTrendLine {
newTrendLine = prevTrendLine
}
default: // 无信号
newTrendLine = prevTrendLine
}
}
// 更新趋势线
data.trendLine = newTrendLine
// 确定趋势方向
prevITrend := data.iTrend
var newITrend float64
if newTrendLine > prevTrendLine {
newITrend = 1 // 上升趋势
} else if newTrendLine < prevTrendLine {
newITrend = -1 // 下降趋势
} else {
newITrend = prevITrend // 保持原趋势
}
// 生成交易信号
if prevITrend == -1 && newITrend == 1 {
// 趋势从下跌转为上涨,产生买入信号
s.OpenOrder(&strat.EnterReq{
Tag: "buy",
})
} else if prevITrend == 1 && newITrend == -1 {
// 趋势从上涨转为下跌,产生卖出信号
s.OpenOrder(&strat.EnterReq{
Tag: "sell",
Short: true,
})
}
// 更新趋势方向
data.iTrend = newITrend
},
}
}

110
llmjob/4217.go Normal file
View File

@@ -0,0 +1,110 @@
package llmjob
import (
"math"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func J4217(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
fastLength := pol.DefInt("fastLength", 9, core.PNorm(5, 20))
maRate := pol.Def("maRate", 2.33, core.PNorm(1.5, 5))
rsiLength := pol.DefInt("rsiLength", 14, core.PNorm(7, 21))
rsiThreshold := pol.DefInt("rsiThreshold", 70, core.PNorm(60, 80))
atrStopLoss := pol.Def("atrStopLoss", 2.0, core.PNorm(1.0, 4.0))
atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 21))
// 添加MACD参数配置
macdFast := pol.DefInt("macdFast", 12, core.PNorm(8, 20))
macdSlow := pol.DefInt("macdSlow", 26, core.PNorm(20, 40))
macdSignal := pol.DefInt("macdSignal", 9, core.PNorm(5, 15))
// 计算慢速EMA周期
slowLength := int(float64(fastLength) * maRate)
// 计算RSI超买超卖水平
rsiOverboughtLevel := float64(rsiThreshold)
rsiOversoldLevel := 100 - float64(rsiThreshold)
return &strat.TradeStrat{
WarmupNum: slowLength + atrPeriod, // 确保有足够的数据计算指标
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多开1个多单
EachMaxShort: 1, // 最多开1个空单
TimeFrames: "8h",
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
fastMA := ta.EMA(e.Close, fastLength)
slowMA := ta.EMA(e.Close, slowLength)
rsi := ta.RSI(e.Close, rsiLength)
// 使用配置的MACD参数
macdLine, signalLine := ta.MACD(e.Close, macdFast, macdSlow, macdSignal)
atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod)
// 获取最新值
fastMAVal := fastMA.Get(0)
slowMAVal := slowMA.Get(0)
rsiVal := rsi.Get(0)
atrVal := atr.Get(0)
// 检查数据有效性
if math.IsNaN(fastMAVal) || math.IsNaN(slowMAVal) || math.IsNaN(rsiVal) || math.IsNaN(atrVal) {
return
}
// 判断趋势
bullishTrend := fastMAVal > slowMAVal
bearishTrend := fastMAVal < slowMAVal
// 生成交易信号
longCondition := bullishTrend && rsiVal < rsiOverboughtLevel && macdLine.Cross(signalLine) == 1
shortCondition := bearishTrend && rsiVal > rsiOversoldLevel && macdLine.Cross(signalLine) == -1
// 计算止损距离
stopLossDistance := atrVal * atrStopLoss
// 执行交易操作
if longCondition && len(s.LongOrders) == 0 {
// 开多单
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Short: false,
StopLossVal: stopLossDistance,
StopLossTag: "ATR Stop Loss",
})
}
if shortCondition && len(s.ShortOrders) == 0 {
// 开空单
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
StopLossVal: stopLossDistance,
StopLossTag: "ATR Stop Loss",
})
}
// 平仓条件
if len(s.LongOrders) > 0 && (bearishTrend || rsiVal > rsiOverboughtLevel) {
// 平多单
s.CloseOrders(&strat.ExitReq{
Tag: "Close Long",
Dirt: core.OdDirtLong,
})
}
if len(s.ShortOrders) > 0 && (bullishTrend || rsiVal < rsiOversoldLevel) {
// 平空单
s.CloseOrders(&strat.ExitReq{
Tag: "Close Short",
Dirt: core.OdDirtShort,
})
}
},
}
}

113
llmjob/4225.go Normal file
View File

@@ -0,0 +1,113 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
// StrategyMore 存储每个品种的策略状态
type StrategyMore struct {
LastBarNum float64 // 上次交易的K线编号
}
// MultiEMACross 多重均线交叉与时间间隔整合策略
func J4225(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调参数
emaBasePeriod := pol.DefInt("emaBasePeriod", 4, core.PNorm(3, 15))
emaMidRatio := pol.Def("emaMidRatio", 2.1, core.PNorm(1.5, 4))
emaSlowRatio := pol.Def("emaSlowRatio", 10, core.PNorm(5, 20))
timeInterval := pol.DefInt("timeInterval", 35, core.PNorm(10, 100))
atrMultiplier := pol.Def("atrMultiplier", 1.9, core.PNorm(0.5, 5))
// 计算EMA周期
emaMidPeriod := int(float64(emaBasePeriod) * emaMidRatio)
emaSlowPeriod := int(float64(emaBasePeriod) * emaSlowRatio)
// 确定预热K线数量,取最慢EMA周期的2倍
warmupNum := emaSlowPeriod * 2
return &strat.TradeStrat{
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化每个品种的策略状态
s.More = &StrategyMore{
LastBarNum: -1, // 初始化为-1,表示尚未交易
}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
m, _ := s.More.(*StrategyMore)
// 计算EMA指标
emaFast := ta.EMA(e.Close, emaBasePeriod)
emaMid := ta.EMA(e.Close, emaMidPeriod)
emaSlow := ta.EMA(e.Close, emaSlowPeriod)
// 计算ATR用于动态止盈止损
atr := ta.ATR(e.High, e.Low, e.Close, 14)
atrVal := atr.Get(0)
// 检查EMA交叉条件 - 修复重复计算问题
fastCrossSlow := emaFast.Cross(emaSlow)
midCrossSlow := emaMid.Cross(emaSlow)
fastCrossSlowUp := fastCrossSlow == 1 // 快线上穿慢线
midCrossSlowUp := midCrossSlow == 1 // 中线上穿慢线
fastCrossSlowDown := fastCrossSlow == -1 // 快线下穿慢线
midCrossSlowDown := midCrossSlow == -1 // 中线下穿慢线
// 生成交易信号
buyCondition := fastCrossSlowUp && midCrossSlowUp
sellCondition := fastCrossSlowDown && midCrossSlowDown
// 检查时间间隔是否满足要求
currentBarNum := e.BarCount(0)
barsSinceLastTrade := currentBarNum - m.LastBarNum
timeElapsedOk := m.LastBarNum == -1 || barsSinceLastTrade >= float64(timeInterval)
// 如果有交易信号且时间间隔满足要求
if (buyCondition || sellCondition) && timeElapsedOk {
// 先平掉所有现有仓位
if len(s.LongOrders) > 0 || len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "close_all",
Dirt: core.OdDirtBoth,
Force: true,
})
}
// 计算止盈止损价格
stopLossDist := atrVal * atrMultiplier
takeProfitDist := stopLossDist * 1.67 // 保持5:3的盈亏比
// 根据信号方向开仓
if buyCondition {
s.OpenOrder(&strat.EnterReq{
Tag: "Buy",
Short: false,
OrderType: core.OrderTypeMarket,
StopLossVal: stopLossDist,
TakeProfitVal: takeProfitDist,
StopLossTag: "stop_loss",
TakeProfitTag: "take_profit",
})
m.LastBarNum = currentBarNum
} else if sellCondition {
s.OpenOrder(&strat.EnterReq{
Tag: "Sell",
Short: true,
OrderType: core.OrderTypeMarket,
StopLossVal: stopLossDist,
TakeProfitVal: takeProfitDist,
StopLossTag: "stop_loss",
TakeProfitTag: "take_profit",
})
m.LastBarNum = currentBarNum
}
}
},
}
}

156
llmjob/4267.go Normal file
View File

@@ -0,0 +1,156 @@
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J4267(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
shortMaLength := pol.DefInt("shortMaLength", 10, core.PNorm(5, 20))
maRate := pol.Def("maRate", 1.8, core.PNorm(1.5, 4))
rsiOverbought := pol.DefInt("rsiOverbought", 75, core.PNorm(60, 85))
rsiOversold := pol.DefInt("rsiOversold", 25, core.PNorm(15, 40))
bbMultiplier := pol.Def("bbMultiplier", 2.0, core.PNorm(1.5, 3.0))
// 计算长期均线周期
longMaLength := int(float64(shortMaLength) * maRate)
// 确定预热数量,取所有指标中最大的周期
warmupNum := longMaLength
if warmupNum < 20 {
warmupNum = 20
}
if warmupNum < 26 {
warmupNum = 26
}
return &strat.TradeStrat{
Name: "Multi-Technical-Indicator-Synergistic-Trading-System",
WarmupNum: warmupNum,
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多一个做多订单
EachMaxShort: 1, // 最多一个做空订单
RunTimeFrames: []string{"4h"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算移动平均线
shortMa := ta.SMA(e.Close, shortMaLength)
longMa := ta.SMA(e.Close, longMaLength)
// 计算RSI
rsi := ta.RSI(e.Close, 14)
// 计算MACD
macdLine, signalLine := ta.MACD(e.Close, 12, 26, 9)
macdHist := macdLine.Sub(signalLine)
// 计算布林带
bbUpper, _, bbLower := ta.BBANDS(e.Close, 20, bbMultiplier, bbMultiplier)
// 获取当前价格
closePrice := e.Close.Get(0)
if math.IsNaN(closePrice) {
return
}
// 检测主要交易信号:MA交叉
maCross := shortMa.Cross(longMa)
buySignal := maCross == 1 // 短期均线上穿长期均线
sellSignal := maCross == -1 // 短期均线下穿长期均线
// 检测辅助交易信号
rsiVal := rsi.Get(0)
macdHistVal := macdHist.Get(0)
bbLowerVal := bbLower.Get(0)
bbUpperVal := bbUpper.Get(0)
additionalBuySignal := !math.IsNaN(rsiVal) && !math.IsNaN(macdHistVal) && !math.IsNaN(bbLowerVal) &&
rsiVal < float64(rsiOversold) && macdHistVal > 0 && closePrice < bbLowerVal
additionalSellSignal := !math.IsNaN(rsiVal) && !math.IsNaN(macdHistVal) && !math.IsNaN(bbUpperVal) &&
rsiVal > float64(rsiOverbought) && macdHistVal < 0 && closePrice > bbUpperVal
// 执行交易策略
if buySignal {
// 平掉所有做空订单
if len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "MA_Sell_Close",
Dirt: core.OdDirtShort,
Force: true,
})
}
// 开做多订单
if len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "MA_Buy",
})
}
} else if sellSignal {
// 平掉所有做多订单
if len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "MA_Buy_Close",
Dirt: core.OdDirtLong,
Force: true,
})
}
// 平掉所有做空订单
if len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "MA_Sell_Close",
Dirt: core.OdDirtShort,
Force: true,
})
}
// 开做空订单
if len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "MA_Sell",
Short: true,
})
}
}
// 辅助信号处理(仅在主要信号未触发时执行)
if additionalBuySignal && !buySignal && len(s.LongOrders) == 0 {
// 平掉所有做空订单
if len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "Aux_Sell_Close",
Dirt: core.OdDirtShort,
Force: true,
})
}
// 开做多订单
s.OpenOrder(&strat.EnterReq{
Tag: "Aux_Buy",
})
} else if additionalSellSignal && !sellSignal && len(s.ShortOrders) == 0 {
// 平掉所有做多订单
if len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "Aux_Buy_Close",
Dirt: core.OdDirtLong,
Force: true,
})
}
// 开做空订单
s.OpenOrder(&strat.EnterReq{
Tag: "Aux_Sell",
Short: true,
})
}
},
}
}

115
llmjob/4316.go Normal file
View File

@@ -0,0 +1,115 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm/ormo"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
func J4316(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
ema1Length := pol.DefInt("ema1_length", 19, core.PNorm(5, 50))
ema2Length := pol.DefInt("ema2_length", 50, core.PNorm(20, 100))
rsiOverbought := pol.DefInt("rsi_overbought", 70, core.PNorm(60, 80))
rsiOversold := pol.DefInt("rsi_oversold", 30, core.PNorm(20, 40))
macdFast := pol.DefInt("macd_fast", 12, core.PNorm(8, 16))
macdSlow := pol.DefInt("macd_slow", 26, core.PNorm(20, 32))
macdSignal := pol.DefInt("macd_signal", 9, core.PNorm(6, 12))
atrLength := pol.DefInt("atr_length", 14, core.PNorm(7, 21))
atrMult := pol.Def("atr_mult", 1.85, core.PNorm(1.5, 5))
useVolume := pol.Def("use_volume", 1.0, core.PNorm(0.0, 1.0)) > 0.5 // 修复:使用Def代替DefBool
volumeMult := pol.Def("volume_mult", 2.0, core.PNorm(1.0, 4.0))
capitalRisk := pol.Def("capital_risk", 2.0, core.PNorm(0.5, 5.0)) / 100
return &strat.TradeStrat{
WarmupNum: ema2Length + 20, // 确保所有指标都有足够数据
HedgeOff: true, // 单向持仓模式
EachMaxLong: 1, // 最多一个多头订单
EachMaxShort: 1, // 最多一个空头订单
DrawDownExit: true, // 启用回撤止盈
RunTimeFrames: []string{"15m"},
OnBar: func(s *strat.StratJob) {
e := s.Env
// 计算技术指标
ema1 := ta.EMA(e.Close, ema1Length)
ema2 := ta.EMA(e.Close, ema2Length)
rsi := ta.RSI(e.Close, 14)
// 计算MACD
macdLine := ta.EMA(e.Close, macdFast).Sub(ta.EMA(e.Close, macdSlow))
macdSignalLine := ta.EMA(macdLine, macdSignal)
macdHist := macdLine.Sub(macdSignalLine)
// 计算ATR
atr := ta.ATR(e.High, e.Low, e.Close, atrLength)
// 计算成交量过滤条件
volumeMA := ta.SMA(e.Volume, 20)
volumeFilter := e.Volume.Get(0) > volumeMA.Get(0)*volumeMult
// 检查数据有效性
if math.IsNaN(ema1.Get(0)) || math.IsNaN(ema2.Get(0)) ||
math.IsNaN(rsi.Get(0)) || math.IsNaN(macdHist.Get(0)) ||
math.IsNaN(atr.Get(0)) {
return
}
// 信号过滤 - 优化重复计算
crossResult := ema1.Cross(ema2)
emaCrossUp := crossResult == 1 // 快速EMA上穿慢速EMA
emaCrossDown := crossResult == -1 // 快速EMA下穿慢速EMA
rsiFilterLong := rsi.Get(0) > float64(rsiOversold) // RSI高于超卖水平
rsiFilterShort := rsi.Get(0) < float64(rsiOverbought) // RSI低于超买水平
macdFilterLong := macdHist.Get(0) > 0 // MACD柱状图为正
macdFilterShort := macdHist.Get(0) < 0 // MACD柱状图为负
volumeOk := !useVolume || volumeFilter // 成交量条件满足
// 生成交易信号
longEntry := emaCrossUp && rsiFilterLong && macdFilterLong && volumeOk
shortEntry := emaCrossDown && rsiFilterShort && macdFilterShort && volumeOk
// 风险管理计算
atrValue := atr.Get(0) // 修复:删除未使用的closePrice变量
// 计算交易规模 - 修复基于ATR的交易规模计算
riskAmount := s.Strat.GetStakeAmount(s) * capitalRisk
tradeSize := riskAmount / math.Max(atrValue*atrMult, 1.0)
// 计算追踪止损价格
// 执行交易
if longEntry && len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Long",
Amount: tradeSize,
// 修复:移除不存在的TrailOffset字段
})
}
if shortEntry && len(s.ShortOrders) == 0 {
s.OpenOrder(&strat.EnterReq{
Tag: "Short",
Short: true,
Amount: tradeSize,
// 修复:移除不存在的TrailOffset字段
})
}
},
// 实现回撤止盈逻辑
GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 {
// 根据最大盈利回撤比例决定止盈
if maxChg > 0.1 { // 盈利超过10%
return 0.5 // 回撤50%止盈
} else if maxChg > 0.05 { // 盈利超过5%
return 0.7 // 回撤70%止盈
}
return 0 // 不设置回撤止盈
},
}
}

268
llmjob/4513.go Normal file
View File

@@ -0,0 +1,268 @@
// 修复后的banbot完整策略代码
package llmjob
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
"math"
)
// State4513 存储每个品种的策略状态
type State4513 struct {
dir int // 当前趋势方向,1=上升趋势,-1=下降趋势
longStop float64 // 多头止损价格
shortStop float64 // 空头止损价格
longStopPrev float64 // 前一个多头止损价格
shortStopPrev float64 // 前一个空头止损价格
trailingStop float64 // 当前跟踪止损价格
}
// Demo 创建并返回策略实例
func J4513(pol *config.RunPolicyConfig) *strat.TradeStrat {
// 定义可调超参数
bbLength := pol.DefInt("BBLength", 100, core.PNorm(20, 200))
stdDev := pol.Def("StdDev", 10, core.PNorm(1, 20))
useMovingAverage := pol.Def("useMovingAverage", 1, core.PUniform(0, 1)) // 1=true, 0=false
maType := pol.DefInt("maType", 3, core.PUniform(0, 5)) // 0=ema, 1=sma, 2=hma, 3=rma, 4=vwma, 5=wma
lookbackPeriod := pol.DefInt("lookbackPeriod", 22, core.PNorm(5, 50))
atrMaType := pol.DefInt("atrMaType", 2, core.PUniform(0, 5)) // 0=ema, 1=sma, 2=hma, 3=rma, 4=vwma, 5=wma
atrLength := pol.DefInt("atrLength", 10, core.PNorm(5, 30))
atrMult := pol.Def("atrMult", 4.2, core.PNorm(1, 10))
wicks := pol.Def("wicks", 0, core.PUniform(0, 1)) // 1=true, 0=false
considerYearlyHighLow := pol.Def("considerYearlyHighLow", 0, core.PUniform(0, 1)) // 1=true, 0=false
considerNewLongTermHighLows := pol.Def("considerNewLongTermHighLows", 0, core.PUniform(0, 1)) // 1=true, 0=false
tradeDirection := pol.DefInt("tradeDirection", 0, core.PUniform(0, 2)) // 0=all, 1=long, 2=short
// 固定参数
shortHighLowPeriod := 100
longHighLowPeriod := 200
return &strat.TradeStrat{
Name: "Bollinger Percentage Bands",
WarmupNum: int(math.Max(float64(bbLength), float64(longHighLowPeriod))) + 10,
HedgeOff: true, // 单向持仓
EachMaxLong: 1,
EachMaxShort: 1,
RunTimeFrames: []string{"8h"},
OnStartUp: func(s *strat.StratJob) {
// 初始化策略状态
state := &State4513{
dir: 1,
longStop: 0,
shortStop: 0,
}
s.More = state
},
OnBar: func(s *strat.StratJob) {
e := s.Env
state, _ := s.More.(*State4513)
// 1. 计算布林带指标
// 修复错误1:布林带计算参数错误
// 原始策略使用bb(close, BBLength, StdDev),其中StdDev是标准差倍数
// 修改为使用正确的参数格式,提供上下轨的标准差倍数
upperClose, _, lowerClose := ta.BBANDS(e.Close, bbLength, stdDev, stdDev)
upperHigh, _, lowerHigh := ta.BBANDS(e.High, bbLength, stdDev, stdDev)
upperLow, _, lowerLow := ta.BBANDS(e.Low, bbLength, stdDev, stdDev)
// 2. 计算布林带百分比
percentBClose := e.Close.Sub(lowerClose).Div(upperClose.Sub(lowerClose)).Mul(100)
percentBHigh := e.High.Sub(lowerHigh).Div(upperHigh.Sub(lowerHigh)).Mul(100)
percentBLow := e.Low.Sub(lowerLow).Div(upperLow.Sub(lowerLow)).Mul(100)
// 3. 应用移动平均线平滑
var newClose, newHigh, newLow *ta.Series
if useMovingAverage > 0.5 {
percentBMAClose := getMovingAverage(percentBClose, maType, lookbackPeriod)
percentBMAHigh := getMovingAverage(percentBHigh, maType, lookbackPeriod)
percentBMALow := getMovingAverage(percentBLow, maType, lookbackPeriod)
newClose = percentBMAClose
newHigh = percentBMAHigh
newLow = percentBMALow
} else {
newClose = percentBClose
newHigh = percentBHigh
newLow = percentBLow
}
// 4. 计算ATR和止损
closePrev := newClose.Back(1)
highPrev := newHigh.Back(1)
lowPrev := newLow.Back(1)
trueRange := newHigh.Max(closePrev).Sub(newLow.Min(closePrev))
averageTrueRange := getMovingAverage(trueRange, atrMaType, atrLength)
atr := averageTrueRange.Mul(atrMult)
longStopVal := newClose.Sub(atr)
shortStopVal := newClose.Add(atr)
// 修复错误2:止损更新逻辑不完整
// 原始策略使用nz(longStop[1], longStop)处理前一个止损值不存在的情况
// 修改为正确处理初始状态,并确保止损只能向有利方向移动
if state.longStopPrev == 0 {
state.longStopPrev = longStopVal.Get(0)
}
if state.shortStopPrev == 0 {
state.shortStopPrev = shortStopVal.Get(0)
}
// 修复错误3:使用Series运算创建常量而不是ta.Const
if wicks > 0.5 {
if lowPrev.Get(0) > state.longStopPrev {
// 使用Max确保止损只能向有利方向移动
longStopVal = longStopVal.Max(newClose.Sub(newClose).Add(state.longStopPrev))
}
} else {
if closePrev.Get(0) > state.longStopPrev {
longStopVal = longStopVal.Max(newClose.Sub(newClose).Add(state.longStopPrev))
}
}
if wicks > 0.5 {
if highPrev.Get(0) < state.shortStopPrev {
shortStopVal = shortStopVal.Min(newClose.Sub(newClose).Add(state.shortStopPrev))
}
} else {
if closePrev.Get(0) < state.shortStopPrev {
shortStopVal = shortStopVal.Min(newClose.Sub(newClose).Add(state.shortStopPrev))
}
}
// 5. 判断趋势方向
// 修复错误4:趋势方向判断可能存在边界条件处理错误
// 原始策略使用三元运算符,逻辑清晰且紧凑
// 修改为更接近原始策略的实现
if state.dir == -1 {
if wicks > 0.5 {
if newHigh.Get(0) > state.shortStopPrev {
state.dir = 1
}
} else {
if newClose.Get(0) > state.shortStopPrev {
state.dir = 1
}
}
} else if state.dir == 1 {
if wicks > 0.5 {
if newLow.Get(0) < state.longStopPrev {
state.dir = -1
}
} else {
if newClose.Get(0) < state.longStopPrev {
state.dir = -1
}
}
}
if state.dir == 1 {
state.trailingStop = longStopVal.Get(0)
} else {
state.trailingStop = shortStopVal.Get(0)
}
// 6. 判断辅助条件
yearlyHighCondition, yearlyLowCondition := getYearlyHighLowCondition(e, considerYearlyHighLow > 0.5)
newHighS, newLowS := calculateNewHighLows(e, shortHighLowPeriod, longHighLowPeriod, considerNewLongTermHighLows > 0.5)
// 7. 生成交易信号
buyCondition := state.dir == 1 && yearlyHighCondition && newHighS
exitBuyCondition := state.dir == -1
sellCondition := state.dir == -1 && yearlyLowCondition && newLowS
exitSellCondition := state.dir == 1
// 8. 执行交易
if buyCondition && (tradeDirection == 0 || tradeDirection == 1) {
s.OpenOrder(&strat.EnterReq{
Tag: "Buy",
Short: false,
})
}
if exitBuyCondition && len(s.LongOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "ExitBuy",
Dirt: core.OdDirtLong,
})
}
if sellCondition && (tradeDirection == 0 || tradeDirection == 2) {
s.OpenOrder(&strat.EnterReq{
Tag: "Sell",
Short: true,
})
}
if exitSellCondition && len(s.ShortOrders) > 0 {
s.CloseOrders(&strat.ExitReq{
Tag: "ExitSell",
Dirt: core.OdDirtShort,
})
}
// 更新状态
state.longStop = longStopVal.Get(0)
state.shortStop = shortStopVal.Get(0)
state.longStopPrev = state.longStop
state.shortStopPrev = state.shortStop
},
}
}
// getMovingAverage 根据类型获取移动平均线
func getMovingAverage(source *ta.Series, maType int, length int) *ta.Series {
switch maType {
case 0: // ema
return ta.EMA(source, length)
case 1: // sma
return ta.SMA(source, length)
case 2: // hma
return ta.HMA(source, length)
case 3: // rma
return ta.RMA(source, length)
case 4: // vwma - 在Banbot中可能没有直接对应,使用SMA代替
return ta.SMA(source, length)
case 5: // wma
return ta.WMA(source, length)
default:
return ta.SMA(source, length)
}
}
// calculateNewHighLows 计算新的高低点条件
func calculateNewHighLows(e *ta.BarEnv, shortPeriod, longPeriod int, consider bool) (bool, bool) {
shortHigh := ta.Highest(e.High, shortPeriod)
longHigh := ta.Highest(e.High, longPeriod)
shortLow := ta.Lowest(e.Low, shortPeriod)
longLow := ta.Lowest(e.Low, longPeriod)
newHigh := true
newLow := true
if consider {
newHigh = math.Abs(shortHigh.Get(0)-longHigh.Get(0)) < 1e-10
newLow = math.Abs(shortLow.Get(0)-longLow.Get(0)) < 1e-10
}
return newHigh, newLow
}
// getYearlyHighLowCondition 获取年度高低点条件
func getYearlyHighLowCondition(e *ta.BarEnv, consider bool) (bool, bool) {
if !consider {
return true, true
}
// 在Banbot中,我们无法直接获取年度数据,所以简化处理
// 使用较长期的高低点来近似年度高低点
yearlyHigh := ta.Highest(e.High, 365)
yearlyLow := ta.Lowest(e.Low, 365)
yearlyHighCondition := e.Close.Get(0) > yearlyHigh.Get(0)
yearlyLowCondition := e.Close.Get(0) < yearlyLow.Get(0)
return yearlyHighCondition, yearlyLowCondition
}

Some files were not shown because too many files have changed in this diff Show More