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