|
| 1 | +package pivotshort |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + |
| 6 | + "github.com/c9s/bbgo/pkg/bbgo" |
| 7 | + "github.com/c9s/bbgo/pkg/fixedpoint" |
| 8 | + "github.com/c9s/bbgo/pkg/indicator" |
| 9 | + "github.com/c9s/bbgo/pkg/types" |
| 10 | +) |
| 11 | + |
| 12 | +// BreakLow -- when price breaks the previous pivot low, we set a trade entry |
| 13 | +type BreakLow struct { |
| 14 | + Symbol string |
| 15 | + Market types.Market |
| 16 | + types.IntervalWindow |
| 17 | + |
| 18 | + // Ratio is a number less than 1.0, price * ratio will be the price triggers the short order. |
| 19 | + Ratio fixedpoint.Value `json:"ratio"` |
| 20 | + |
| 21 | + // MarketOrder is the option to enable market order short. |
| 22 | + MarketOrder bool `json:"marketOrder"` |
| 23 | + |
| 24 | + // BounceRatio is a ratio used for placing the limit order sell price |
| 25 | + // limit sell price = breakLowPrice * (1 + BounceRatio) |
| 26 | + BounceRatio fixedpoint.Value `json:"bounceRatio"` |
| 27 | + |
| 28 | + Quantity fixedpoint.Value `json:"quantity"` |
| 29 | + StopEMARange fixedpoint.Value `json:"stopEMARange"` |
| 30 | + StopEMA *types.IntervalWindow `json:"stopEMA"` |
| 31 | + |
| 32 | + lastLow fixedpoint.Value |
| 33 | + pivot *indicator.Pivot |
| 34 | + stopEWMA *indicator.EWMA |
| 35 | + pivotLowPrices []fixedpoint.Value |
| 36 | + |
| 37 | + orderExecutor *bbgo.GeneralOrderExecutor |
| 38 | + session *bbgo.ExchangeSession |
| 39 | +} |
| 40 | + |
| 41 | +func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { |
| 42 | + s.session = session |
| 43 | + s.orderExecutor = orderExecutor |
| 44 | + |
| 45 | + position := orderExecutor.Position() |
| 46 | + symbol := position.Symbol |
| 47 | + store, _ := session.MarketDataStore(symbol) |
| 48 | + standardIndicator, _ := session.StandardIndicatorSet(symbol) |
| 49 | + |
| 50 | + s.lastLow = fixedpoint.Zero |
| 51 | + |
| 52 | + s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} |
| 53 | + s.pivot.Bind(store) |
| 54 | + preloadPivot(s.pivot, store) |
| 55 | + |
| 56 | + if s.StopEMA != nil { |
| 57 | + s.stopEWMA = standardIndicator.EWMA(*s.StopEMA) |
| 58 | + } |
| 59 | + |
| 60 | + // update pivot low data |
| 61 | + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { |
| 62 | + lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow()) |
| 63 | + if lastLow.IsZero() { |
| 64 | + return |
| 65 | + } |
| 66 | + |
| 67 | + if lastLow.Compare(s.lastLow) != 0 { |
| 68 | + log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time()) |
| 69 | + } |
| 70 | + |
| 71 | + s.lastLow = lastLow |
| 72 | + s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) |
| 73 | + })) |
| 74 | + |
| 75 | + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, types.Interval1m, func(kline types.KLine) { |
| 76 | + if position.IsOpened(kline.Close) { |
| 77 | + return |
| 78 | + } |
| 79 | + |
| 80 | + if len(s.pivotLowPrices) == 0 { |
| 81 | + log.Infof("currently there is no pivot low prices, skip placing orders...") |
| 82 | + return |
| 83 | + } |
| 84 | + |
| 85 | + previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1] |
| 86 | + |
| 87 | + // truncate the pivot low prices |
| 88 | + if len(s.pivotLowPrices) > 10 { |
| 89 | + s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:] |
| 90 | + } |
| 91 | + |
| 92 | + ratio := fixedpoint.One.Add(s.Ratio) |
| 93 | + breakPrice := previousLow.Mul(ratio) |
| 94 | + |
| 95 | + openPrice := kline.Open |
| 96 | + closePrice := kline.Close |
| 97 | + |
| 98 | + // if previous low is not break, skip |
| 99 | + if closePrice.Compare(breakPrice) >= 0 { |
| 100 | + return |
| 101 | + } |
| 102 | + |
| 103 | + // we need the price cross the break line or we do nothing |
| 104 | + if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { |
| 105 | + log.Infof("%s kline is not between the break low price %f", kline.Symbol, breakPrice.Float64()) |
| 106 | + return |
| 107 | + } |
| 108 | + |
| 109 | + // force direction to be down |
| 110 | + if closePrice.Compare(openPrice) >= 0 { |
| 111 | + log.Infof("%s price %f is closed higher than the open price %f, skip this break", kline.Symbol, closePrice.Float64(), openPrice.Float64()) |
| 112 | + // skip UP klines |
| 113 | + return |
| 114 | + } |
| 115 | + |
| 116 | + log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64()) |
| 117 | + |
| 118 | + // stop EMA protection |
| 119 | + if s.stopEWMA != nil { |
| 120 | + ema := fixedpoint.NewFromFloat(s.stopEWMA.Last()) |
| 121 | + if ema.IsZero() { |
| 122 | + return |
| 123 | + } |
| 124 | + |
| 125 | + emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange)) |
| 126 | + if closePrice.Compare(emaStopShortPrice) < 0 { |
| 127 | + log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.StopEMA, ema.Float64()) |
| 128 | + return |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + ctx := context.Background() |
| 133 | + |
| 134 | + // graceful cancel all active orders |
| 135 | + _ = orderExecutor.GracefulCancel(ctx) |
| 136 | + |
| 137 | + quantity := s.useQuantityOrBaseBalance(s.Quantity) |
| 138 | + if s.MarketOrder { |
| 139 | + bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64()) |
| 140 | + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ |
| 141 | + Symbol: s.Symbol, |
| 142 | + Side: types.SideTypeSell, |
| 143 | + Type: types.OrderTypeMarket, |
| 144 | + Quantity: quantity, |
| 145 | + MarginSideEffect: types.SideEffectTypeMarginBuy, |
| 146 | + Tag: "breakLowMarket", |
| 147 | + }) |
| 148 | + |
| 149 | + } else { |
| 150 | + sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BounceRatio)) |
| 151 | + |
| 152 | + bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64(), sellPrice.Float64()) |
| 153 | + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ |
| 154 | + Symbol: kline.Symbol, |
| 155 | + Side: types.SideTypeSell, |
| 156 | + Type: types.OrderTypeLimit, |
| 157 | + Price: sellPrice, |
| 158 | + Quantity: quantity, |
| 159 | + MarginSideEffect: types.SideEffectTypeMarginBuy, |
| 160 | + Tag: "breakLowLimit", |
| 161 | + }) |
| 162 | + } |
| 163 | + })) |
| 164 | + |
| 165 | + if !bbgo.IsBackTesting { |
| 166 | + // use market trade to submit short order |
| 167 | + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { |
| 168 | + |
| 169 | + }) |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +func (s *BreakLow) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { |
| 174 | + if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { |
| 175 | + return quantity |
| 176 | + } |
| 177 | + |
| 178 | + balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) |
| 179 | + if hasBalance { |
| 180 | + if quantity.IsZero() { |
| 181 | + bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) |
| 182 | + quantity = balance.Available |
| 183 | + } else { |
| 184 | + quantity = fixedpoint.Min(quantity, balance.Available) |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + if quantity.IsZero() { |
| 189 | + log.Errorf("quantity is zero, can not submit sell order, please check settings") |
| 190 | + } |
| 191 | + |
| 192 | + return quantity |
| 193 | +} |
0 commit comments