// 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" "sync" log "github.com/sirupsen/logrus" "git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint" "git.qtrade.icu/lychiyu/qbtrade/pkg/indicator" "git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade" "git.qtrade.icu/lychiyu/qbtrade/pkg/types" ) const ID = "flashcrash" func init() { qbtrade.RegisterStrategy(ID, &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 fixedpoint.Value `json:"percentage"` // BaseQuantity is the quantity you want to submit for each order. BaseQuantity fixedpoint.Value `json:"baseQuantity"` // activeOrders is the locally maintained active order book of the maker orders. activeOrders *qbtrade.ActiveOrderBook // 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. *qbtrade.StandardIndicatorSet // ewma is the exponential weighted moving average indicator ewma *indicator.EWMA } func (s *Strategy) ID() string { return ID } func (s *Strategy) updateOrders(orderExecutor qbtrade.OrderExecutor, session *qbtrade.ExchangeSession) { if err := s.activeOrders.GracefulCancel(context.Background(), session.Exchange); err != nil { log.WithError(err).Errorf("cancel order error") } s.updateBidOrders(orderExecutor, session) } func (s *Strategy) updateBidOrders(orderExecutor qbtrade.OrderExecutor, session *qbtrade.ExchangeSession) { quoteCurrency := s.Market.QuoteCurrency balances := session.GetAccount().Balances() balance, ok := balances[quoteCurrency] if !ok || balance.Available.Sign() <= 0 { log.Infof("insufficient balance of %s: %v", quoteCurrency, balance.Available) return } var startPrice = fixedpoint.NewFromFloat(s.ewma.Last(0)).Mul(s.Percentage) var submitOrders []types.SubmitOrder for i := 0; i < s.GridNum; i++ { submitOrders = append(submitOrders, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, Market: s.Market, Quantity: s.BaseQuantity, Price: startPrice, TimeInForce: types.TimeInForceGTC, }) startPrice = startPrice.Mul(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) Subscribe(session *qbtrade.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) Run(ctx context.Context, orderExecutor qbtrade.OrderExecutor, session *qbtrade.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 = qbtrade.NewActiveOrderBook(s.Symbol) s.activeOrders.BindStream(session.UserDataStream) qbtrade.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") if err := orderExecutor.CancelOrders(ctx, s.activeOrders.Orders()...); err != nil { log.WithError(err).Errorf("cancel order error") } }) s.ewma = s.StandardIndicatorSet.EWMA(types.IntervalWindow{ Interval: s.Interval, Window: 25, }) session.UserDataStream.OnStart(func() { s.updateOrders(orderExecutor, session) }) session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { s.updateOrders(orderExecutor, session) }) return nil }