diff --git a/config/fixedmaker.yaml b/config/fixedmaker.yaml index 63d3ee058..d66a426f7 100644 --- a/config/fixedmaker.yaml +++ b/config/fixedmaker.yaml @@ -16,12 +16,19 @@ exchangeStrategies: - on: max fixedmaker: symbol: USDCUSDT - interval: 5m + interval: 1m halfSpread: 0.05% quantity: 15 orderType: LIMIT_MAKER - dryRun: false + dryRun: true positionHardLimit: 1500 maxPositionQuantity: 1500 circuitBreakLossThreshold: -0.15 + circuitBreakEMA: + interval: 1m + window: 14 + + inventorySkew: + inventoryRangeMultiplier: 1.0 + targetBaseRatio: 0.5 diff --git a/config/xfixedmaker.yaml b/config/xfixedmaker.yaml index bfee2e771..b1ebaf01f 100644 --- a/config/xfixedmaker.yaml +++ b/config/xfixedmaker.yaml @@ -6,6 +6,7 @@ sessions: binance: exchange: binance envVarPrefix: binance + publicOnly: true backtest: startTime: "2023-01-01" @@ -25,11 +26,11 @@ crossExchangeStrategies: - xfixedmaker: tradingExchange: max symbol: BTCUSDT - interval: 5m + interval: 1m halfSpread: 0.01% quantity: 0.005 orderType: LIMIT_MAKER - dryRun: false + dryRun: true referenceExchange: binance referencePriceEMA: @@ -44,3 +45,7 @@ crossExchangeStrategies: circuitBreakEMA: interval: 1m window: 14 + + inventorySkew: + inventoryRangeMultiplier: 1.0 + targetBaseRatio: 0.5 diff --git a/pkg/strategy/fixedmaker/inventory_skew.go b/pkg/strategy/fixedmaker/inventory_skew.go new file mode 100644 index 000000000..dae68e7c4 --- /dev/null +++ b/pkg/strategy/fixedmaker/inventory_skew.go @@ -0,0 +1,56 @@ +package fixedmaker + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +var ( + zero = fixedpoint.Zero + two = fixedpoint.NewFromFloat(2.0) +) + +type InventorySkewBidAskRatios struct { + BidRatio fixedpoint.Value + AskRatio fixedpoint.Value +} + +// https://hummingbot.org/strategy-configs/inventory-skew/ +// https://github.com/hummingbot/hummingbot/blob/31fc61d5e71b2c15732142d30983f3ea2be4d466/hummingbot/strategy/pure_market_making/inventory_skew_calculator.pyx +type InventorySkew struct { + InventoryRangeMultiplier fixedpoint.Value `json:"inventoryRangeMultiplier"` + TargetBaseRatio fixedpoint.Value `json:"targetBaseRatio"` +} + +func (s *InventorySkew) Validate() error { + if s.InventoryRangeMultiplier.Float64() < 0 { + return fmt.Errorf("inventoryRangeMultiplier should be positive") + } + + if s.TargetBaseRatio.Float64() < 0 { + return fmt.Errorf("targetBaseRatio should be positive") + } + return nil +} + +func (s *InventorySkew) CalculateBidAskRatios(quantity fixedpoint.Value, price fixedpoint.Value, baseBalance fixedpoint.Value, quoteBalance fixedpoint.Value) *InventorySkewBidAskRatios { + baseValue := baseBalance.Mul(price) + totalValue := baseValue.Add(quoteBalance) + + inventoryRange := s.InventoryRangeMultiplier.Mul(quantity.Mul(two)).Mul(price) + leftLimit := s.TargetBaseRatio.Mul(totalValue).Sub(inventoryRange) + rightLimit := s.TargetBaseRatio.Mul(totalValue).Add(inventoryRange) + + bidAdjustment := interp(baseValue, leftLimit, rightLimit, two, zero).Clamp(zero, two) + askAdjustment := interp(baseValue, leftLimit, rightLimit, zero, two).Clamp(zero, two) + + return &InventorySkewBidAskRatios{ + BidRatio: bidAdjustment, + AskRatio: askAdjustment, + } +} + +func interp(x, x0, x1, y0, y1 fixedpoint.Value) fixedpoint.Value { + return y0.Add(x.Sub(x0).Mul(y1.Sub(y0)).Div(x1.Sub(x0))) +} diff --git a/pkg/strategy/fixedmaker/inventory_skew_test.go b/pkg/strategy/fixedmaker/inventory_skew_test.go new file mode 100644 index 000000000..cd40931a1 --- /dev/null +++ b/pkg/strategy/fixedmaker/inventory_skew_test.go @@ -0,0 +1,69 @@ +package fixedmaker + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/stretchr/testify/assert" +) + +func Test_InventorySkew_CalculateBidAskRatios(t *testing.T) { + cases := []struct { + quantity fixedpoint.Value + price fixedpoint.Value + baseBalance fixedpoint.Value + quoteBalance fixedpoint.Value + want *InventorySkewBidAskRatios + }{ + { + quantity: fixedpoint.NewFromFloat(1.0), + price: fixedpoint.NewFromFloat(1000), + baseBalance: fixedpoint.NewFromFloat(1.0), + quoteBalance: fixedpoint.NewFromFloat(1000), + want: &InventorySkewBidAskRatios{ + BidRatio: fixedpoint.NewFromFloat(1.0), + AskRatio: fixedpoint.NewFromFloat(1.0), + }, + }, + { + quantity: fixedpoint.NewFromFloat(1.0), + price: fixedpoint.NewFromFloat(1000), + baseBalance: fixedpoint.NewFromFloat(1.0), + quoteBalance: fixedpoint.NewFromFloat(1200), + want: &InventorySkewBidAskRatios{ + BidRatio: fixedpoint.NewFromFloat(1.5), + AskRatio: fixedpoint.NewFromFloat(0.5), + }, + }, + { + quantity: fixedpoint.NewFromFloat(1.0), + price: fixedpoint.NewFromFloat(1000), + baseBalance: fixedpoint.NewFromFloat(0.0), + quoteBalance: fixedpoint.NewFromFloat(10000), + want: &InventorySkewBidAskRatios{ + BidRatio: fixedpoint.NewFromFloat(2.0), + AskRatio: fixedpoint.NewFromFloat(0.0), + }, + }, + { + quantity: fixedpoint.NewFromFloat(1.0), + price: fixedpoint.NewFromFloat(1000), + baseBalance: fixedpoint.NewFromFloat(2.0), + quoteBalance: fixedpoint.NewFromFloat(0.0), + want: &InventorySkewBidAskRatios{ + BidRatio: fixedpoint.NewFromFloat(0.0), + AskRatio: fixedpoint.NewFromFloat(2.0), + }, + }, + } + + for _, c := range cases { + s := &InventorySkew{ + InventoryRangeMultiplier: fixedpoint.NewFromFloat(0.1), + TargetBaseRatio: fixedpoint.NewFromFloat(0.5), + } + got := s.CalculateBidAskRatios(c.quantity, c.price, c.baseBalance, c.quoteBalance) + assert.Equal(t, c.want.BidRatio.Float64(), got.BidRatio.Float64()) + assert.Equal(t, c.want.AskRatio.Float64(), got.AskRatio.Float64()) + } +} diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index af561de09..8046636a8 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -35,6 +35,8 @@ type Strategy struct { OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` + InventorySkew InventorySkew `json:"inventorySkew"` + activeOrderBook *bbgo.ActiveOrderBook } @@ -70,6 +72,10 @@ func (s *Strategy) Validate() error { if s.HalfSpread.Float64() <= 0 { return fmt.Errorf("halfSpread should be positive") } + + if err := s.InventorySkew.Validate(); err != nil { + return err + } return nil } @@ -123,7 +129,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } func (s *Strategy) cancelOrders(ctx context.Context) { - if err := s.Session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil { + if err := s.activeOrderBook.GracefulCancel(ctx, s.Session.Exchange); err != nil { log.WithError(err).Errorf("failed to cancel orders") } } @@ -180,6 +186,21 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err 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 { @@ -188,7 +209,7 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err Side: types.SideTypeBuy, Type: s.OrderType, Price: buyPrice, - Quantity: s.Quantity, + Quantity: buyQuantity, }) } else { log.Infof("not enough quote balance to buy, available: %s, amount: %s", quoteBalance.Available, amount) @@ -200,7 +221,7 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err Side: types.SideTypeSell, Type: s.OrderType, Price: sellPrice, - Quantity: s.Quantity, + Quantity: sellQuantity, }) } else { log.Infof("not enough base balance to sell, available: %s, quantity: %s", baseBalance.Available, s.Quantity) diff --git a/pkg/strategy/xfixedmaker/strategy.go b/pkg/strategy/xfixedmaker/strategy.go index 2e018f571..45d1bd78a 100644 --- a/pkg/strategy/xfixedmaker/strategy.go +++ b/pkg/strategy/xfixedmaker/strategy.go @@ -10,6 +10,7 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/strategy/common" + "github.com/c9s/bbgo/pkg/strategy/fixedmaker" "github.com/c9s/bbgo/pkg/types" ) @@ -35,9 +36,10 @@ type Strategy struct { OrderType types.OrderType `json:"orderType"` DryRun bool `json:"dryRun"` - ReferenceExchange string `json:"referenceExchange"` - ReferencePriceEMA types.IntervalWindow `json:"referencePriceEMA"` - OrderPriceLossThreshold fixedpoint.Value `json:"orderPriceLossThreshold"` + ReferenceExchange string `json:"referenceExchange"` + ReferencePriceEMA types.IntervalWindow `json:"referencePriceEMA"` + OrderPriceLossThreshold fixedpoint.Value `json:"orderPriceLossThreshold"` + InventorySkew fixedmaker.InventorySkew `json:"inventorySkew"` market types.Market activeOrderBook *bbgo.ActiveOrderBook @@ -73,6 +75,10 @@ func (s *Strategy) Validate() error { if s.HalfSpread.Float64() <= 0 { return fmt.Errorf("halfSpread should be positive") } + + if err := s.InventorySkew.Validate(); err != nil { + return err + } return nil } @@ -155,7 +161,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se } func (s *Strategy) cancelOrders(ctx context.Context) { - if err := s.Session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil { + if err := s.activeOrderBook.GracefulCancel(ctx, s.Session.Exchange); err != nil { log.WithError(err).Errorf("failed to cancel orders") } } @@ -212,6 +218,21 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err 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 { @@ -221,7 +242,7 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err Side: types.SideTypeBuy, Type: s.OrderType, Price: buyPrice, - Quantity: s.Quantity, + Quantity: buyQuantity, }) } else { @@ -238,7 +259,7 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err Side: types.SideTypeSell, Type: s.OrderType, Price: sellPrice, - Quantity: s.Quantity, + Quantity: sellQuantity, }) } else { log.Infof("ref price risk control triggered, not placing sell order")