Files
banstagy/flux/grid/common.go
2026-08-24 20:53:21 +08:00

303 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package 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, ", ") + "] "
}