package xfixedmaker import ( "context" "fmt" "sync" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" ) const ID = "xfixedmaker" var log = logrus.WithField("strategy", ID) func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } // Fixed spread market making strategy type Strategy struct { *common.Strategy Environment *bbgo.Environment TradingExchange string `json:"tradingExchange"` Symbol string `json:"symbol"` Interval types.Interval `json:"interval"` Quantity fixedpoint.Value `json:"quantity"` HalfSpread fixedpoint.Value `json:"halfSpread"` OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` ReferenceExchange string `json:"referenceExchange"` ReferencePriceEMA types.IntervalWindow `json:"referencePriceEMA"` OrderPriceLossThreshold fixedpoint.Value `json:"orderPriceLossThreshold"` InventorySkew common.InventorySkew `json:"inventorySkew"` market types.Market activeOrderBook *bbgo.ActiveOrderBook orderPriceRiskControl *riskcontrol.OrderPriceRiskControl } func (s *Strategy) Defaults() error { if s.OrderType == "" { log.Infof("order type is not set, using limit maker order type") s.OrderType = types.OrderTypeLimitMaker } return nil } func (s *Strategy) Initialize() error { if s.Strategy == nil { s.Strategy = &common.Strategy{} } return nil } func (s *Strategy) ID() string { return ID } func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } func (s *Strategy) Validate() error { if s.Quantity.Float64() <= 0 { return fmt.Errorf("quantity should be positive") } if s.HalfSpread.Float64() <= 0 { return fmt.Errorf("halfSpread should be positive") } if err := s.InventorySkew.Validate(); err != nil { return err } return nil } func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { tradingSession, ok := sessions[s.TradingExchange] if !ok { log.Errorf("trading session %s is not defined", s.TradingExchange) return } tradingSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) if !s.CircuitBreakLossThreshold.IsZero() { tradingSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.CircuitBreakEMA.Interval}) } referenceSession, ok := sessions[s.ReferenceExchange] if !ok { log.Errorf("reference session %s is not defined", s.ReferenceExchange) } referenceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ReferencePriceEMA.Interval}) } func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { tradingSession, ok := sessions[s.TradingExchange] if !ok { return fmt.Errorf("trading session %s is not defined", s.TradingExchange) } referenceSession, ok := sessions[s.ReferenceExchange] if !ok { return fmt.Errorf("reference session %s is not defined", s.ReferenceExchange) } market, ok := tradingSession.Market(s.Symbol) if !ok { return fmt.Errorf("market %s not found", s.Symbol) } s.market = market s.Strategy.Initialize(ctx, s.Environment, tradingSession, s.market, ID, s.InstanceID()) s.orderPriceRiskControl = riskcontrol.NewOrderPriceRiskControl( referenceSession.Indicators(s.Symbol).EMA(s.ReferencePriceEMA), s.OrderPriceLossThreshold, ) s.activeOrderBook = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrderBook.BindStream(tradingSession.UserDataStream) s.activeOrderBook.OnFilled(func(order types.Order) { if s.IsHalted(order.UpdateTime.Time()) { log.Infof("circuit break halted") return } if s.activeOrderBook.NumOfOrders() == 0 { log.Infof("no active orders, placing orders...") s.placeOrders(ctx) } }) tradingSession.MarketDataStream.OnKLineClosed(func(kline types.KLine) { log.Infof("kline: %s", kline.String()) if s.IsHalted(kline.EndTime.Time()) { log.Infof("circuit break halted") return } if kline.Interval == s.Interval { s.cancelOrders(ctx) s.placeOrders(ctx) } }) // the shutdown handler, you can cancel all orders bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() _ = s.OrderExecutor.GracefulCancel(ctx) bbgo.Sync(ctx, s) }) return nil } func (s *Strategy) cancelOrders(ctx context.Context) { if err := s.activeOrderBook.GracefulCancel(ctx, s.Session.Exchange); err != nil { log.WithError(err).Errorf("failed to cancel orders") } } func (s *Strategy) placeOrders(ctx context.Context) { submitOrders, err := s.generateOrders(ctx) if err != nil { log.WithError(err).Error("failed to generate orders") return } log.Infof("submit orders: %+v", submitOrders) if s.DryRun { log.Infof("dry run, not submitting orders") return } createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, submitOrders...) if err != nil { log.WithError(err).Error("failed to submit orders") return } log.Infof("created orders: %+v", createdOrders) s.activeOrderBook.Add(createdOrders...) } func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, error) { orders := []types.SubmitOrder{} baseBalance, ok := s.Session.GetAccount().Balance(s.market.BaseCurrency) if !ok { return nil, fmt.Errorf("base currency %s balance not found", s.market.BaseCurrency) } log.Infof("base balance: %s", baseBalance.String()) quoteBalance, ok := s.Session.GetAccount().Balance(s.market.QuoteCurrency) if !ok { return nil, fmt.Errorf("quote currency %s balance not found", s.market.QuoteCurrency) } log.Infof("quote balance: %s", quoteBalance.String()) ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { return nil, err } midPrice := ticker.Buy.Add(ticker.Sell).Div(fixedpoint.NewFromFloat(2.0)) log.Infof("mid price: %s", midPrice.String()) // calculate bid and ask price // sell price = mid price * (1 + r)) // buy price = mid price * (1 - r)) sellPrice := midPrice.Mul(fixedpoint.One.Add(s.HalfSpread)).Round(s.market.PricePrecision, fixedpoint.Up) buyPrice := midPrice.Mul(fixedpoint.One.Sub(s.HalfSpread)).Round(s.market.PricePrecision, fixedpoint.Down) log.Infof("sell price: %s, buy price: %s", sellPrice.String(), buyPrice.String()) buyQuantity := s.Quantity sellQuantity := s.Quantity if !s.InventorySkew.InventoryRangeMultiplier.IsZero() { ratios := s.InventorySkew.CalculateBidAskRatios( s.Quantity, midPrice, baseBalance.Total(), quoteBalance.Total(), ) log.Infof("bid ratio: %s, ask ratio: %s", ratios.BidRatio.String(), ratios.AskRatio.String()) buyQuantity = s.Quantity.Mul(ratios.BidRatio) sellQuantity = s.Quantity.Mul(ratios.AskRatio) log.Infof("buy quantity: %s, sell quantity: %s", buyQuantity.String(), sellQuantity.String()) } // check balance and generate orders amount := s.Quantity.Mul(buyPrice) if quoteBalance.Available.Compare(amount) > 0 { if s.orderPriceRiskControl.IsSafe(types.SideTypeBuy, buyPrice, s.Quantity) { orders = append(orders, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: s.OrderType, Price: buyPrice, Quantity: buyQuantity, }) } else { log.Infof("ref price risk control triggered, not placing buy order") } } else { log.Infof("not enough quote balance to buy, available: %s, amount: %s", quoteBalance.Available, amount) } if baseBalance.Available.Compare(s.Quantity) > 0 { if s.orderPriceRiskControl.IsSafe(types.SideTypeSell, sellPrice, s.Quantity) { orders = append(orders, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: s.OrderType, Price: sellPrice, Quantity: sellQuantity, }) } else { log.Infof("ref price risk control triggered, not placing sell order") } } else { log.Infof("not enough base balance to sell, available: %s, quantity: %s", baseBalance.Available, s.Quantity) } return orders, nil }