diff --git a/config/flashcrash.yaml b/config/flashcrash.yaml new file mode 100644 index 000000000..367ebb52a --- /dev/null +++ b/config/flashcrash.yaml @@ -0,0 +1,14 @@ +--- +sessions: + max: + exchange: max + envVarPrefix: max + +exchangeStrategies: +- on: max + flashcrash: + symbol: BTCUSDT + interval: 1m + baseQuantity: 0.01 + percentage: 0.6 # 60% of the current price from the moving average + gridNumber: 3 diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index e5d534365..7c7cdc3fd 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -3,6 +3,7 @@ package cmd // import built-in strategies import ( _ "github.com/c9s/bbgo/pkg/strategy/buyandhold" + _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" _ "github.com/c9s/bbgo/pkg/strategy/grid" _ "github.com/c9s/bbgo/pkg/strategy/pricealert" _ "github.com/c9s/bbgo/pkg/strategy/swing" diff --git a/pkg/strategy/flashcrash/strategy.go b/pkg/strategy/flashcrash/strategy.go new file mode 100644 index 000000000..dbfe35390 --- /dev/null +++ b/pkg/strategy/flashcrash/strategy.go @@ -0,0 +1,145 @@ +// flashcrash strategy tries to place the orders at 30%~50% of the current price, +// so that you can catch the orders while flashcrash happens +package flashcrash + +import ( + "context" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +func init() { + bbgo.RegisterStrategy("flashcrash", &Strategy{}) +} + +type Strategy struct { + // These fields will be filled from the config file (it translates YAML to JSON) + // Symbol is the symbol of market you want to run this strategy + Symbol string `json:"symbol"` + + // Interval is the interval used to trigger order updates + Interval types.Interval `json:"interval"` + + // GridNum is the grid number, how many orders you want to places + GridNum int `json:"gridNumber"` + + Percentage float64 `json:"percentage"` + + // BaseQuantity is the quantity you want to submit for each order. + BaseQuantity float64 `json:"baseQuantity"` + + // activeOrders is the locally maintained active order book of the maker orders. + activeOrders *bbgo.LocalActiveOrderBook + + // Injection fields start + // -------------------------- + // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc + // This field will be injected automatically since we defined the Symbol field. + types.Market + + // StandardIndicatorSet contains the standard indicators of a market (symbol) + // This field will be injected automatically since we defined the Symbol field. + *bbgo.StandardIndicatorSet + // -------------------------- + + // ewma is the exponential weighted moving average indicator + ewma *indicator.EWMA +} + + +func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { + if err := session.Exchange.CancelOrders(context.Background(), s.activeOrders.Bids.Orders()...); err != nil { + log.WithError(err).Errorf("cancel order error") + } + + s.updateBidOrders(orderExecutor, session) +} + +func (s *Strategy) updateBidOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { + quoteCurrency := s.Market.QuoteCurrency + balances := session.Account.Balances() + + balance, ok := balances[quoteCurrency] + if !ok || balance.Available <= 0.0 { + return + } + + var numOrders = s.GridNum - s.activeOrders.NumOfBids() + if numOrders <= 0 { + return + } + + var startPrice = s.ewma.Last() * s.Percentage + + var submitOrders []types.SubmitOrder + for i := 0; i < numOrders; i++ { + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Market: s.Market, + Quantity: s.BaseQuantity, + Price: startPrice, + TimeInForce: "GTC", + }) + + startPrice *= s.Percentage + } + + orders, err := orderExecutor.SubmitOrders(context.Background(), submitOrders...) + if err != nil { + log.WithError(err).Error("submit bid order error") + return + } + + s.activeOrders.Add(orders...) +} + +func (s *Strategy) orderUpdateHandler(order types.Order) { + if order.Symbol != s.Symbol { + return + } + + log.Infof("received order update: %+v", order) + + switch order.Status { + case types.OrderStatusFilled: + s.activeOrders.Delete(order) + + case types.OrderStatusCanceled, types.OrderStatusRejected: + log.Infof("order status %s, removing %d from the active order pool...", order.Status, order.OrderID) + s.activeOrders.Delete(order) + + case types.OrderStatusPartiallyFilled: + s.activeOrders.Add(order) + + default: + s.activeOrders.Add(order) + } +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: string(s.Interval)}) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. + s.activeOrders = bbgo.NewLocalActiveOrderBook() + s.ewma = s.StandardIndicatorSet.GetEWMA(types.IntervalWindow{ + Interval: s.Interval, + Window: 25, + }) + + session.Stream.OnOrderUpdate(s.orderUpdateHandler) + session.Stream.OnKLineClosed(func(kline types.KLine) { + s.updateOrders(orderExecutor, session) + }) + + // TODO: move this to the stream onConnect handler + s.updateOrders(orderExecutor, session) + return nil +} diff --git a/pkg/types/standardstream_callbacks.go b/pkg/types/standardstream_callbacks.go index 360e34651..e773f440e 100644 --- a/pkg/types/standardstream_callbacks.go +++ b/pkg/types/standardstream_callbacks.go @@ -2,6 +2,8 @@ package types +import () + func (stream *StandardStream) OnTradeUpdate(cb func(trade Trade)) { stream.tradeUpdateCallbacks = append(stream.tradeUpdateCallbacks, cb) }