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

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},
}
}
}