From 8ecba4378c70909a45356cad5903735b1728797e Mon Sep 17 00:00:00 2001 From: narumi Date: Wed, 8 Nov 2023 17:59:37 +0800 Subject: [PATCH 1/3] inventory skew --- pkg/strategy/fixedmaker/inventory_skew.go | 43 ++++++++++++ .../fixedmaker/inventory_skew_test.go | 69 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 pkg/strategy/fixedmaker/inventory_skew.go create mode 100644 pkg/strategy/fixedmaker/inventory_skew_test.go diff --git a/pkg/strategy/fixedmaker/inventory_skew.go b/pkg/strategy/fixedmaker/inventory_skew.go new file mode 100644 index 000000000..fbbc03c60 --- /dev/null +++ b/pkg/strategy/fixedmaker/inventory_skew.go @@ -0,0 +1,43 @@ +package fixedmaker + +import ( + "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) 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..8c7313997 --- /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()) + } +} From f160ea856fc2c0a03b3245936424e894137a7983 Mon Sep 17 00:00:00 2001 From: narumi <4680567+narumiruna@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:29:46 +0800 Subject: [PATCH 2/3] apply inventory-skew to fixedmaker --- config/fixedmaker.yaml | 11 +++++++-- pkg/strategy/fixedmaker/inventory_skew.go | 13 +++++++++++ pkg/strategy/fixedmaker/strategy.go | 27 ++++++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) 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/pkg/strategy/fixedmaker/inventory_skew.go b/pkg/strategy/fixedmaker/inventory_skew.go index fbbc03c60..0dafe474a 100644 --- a/pkg/strategy/fixedmaker/inventory_skew.go +++ b/pkg/strategy/fixedmaker/inventory_skew.go @@ -1,6 +1,8 @@ package fixedmaker import ( + "fmt" + "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -21,6 +23,17 @@ type InventorySkew struct { 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) diff --git a/pkg/strategy/fixedmaker/strategy.go b/pkg/strategy/fixedmaker/strategy.go index af561de09..f08f6c4b4 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) From 7f0a4a9953c655b2b5fc1d8c26f77cf29aee0168 Mon Sep 17 00:00:00 2001 From: narumi <4680567+narumiruna@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:34:05 +0800 Subject: [PATCH 3/3] apply inventory-skew to xfixedmaker --- config/xfixedmaker.yaml | 9 +++-- pkg/strategy/fixedmaker/inventory_skew.go | 8 ++--- .../fixedmaker/inventory_skew_test.go | 20 +++++------ pkg/strategy/fixedmaker/strategy.go | 6 ++-- pkg/strategy/xfixedmaker/strategy.go | 33 +++++++++++++++---- 5 files changed, 51 insertions(+), 25 deletions(-) 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 index 0dafe474a..dae68e7c4 100644 --- a/pkg/strategy/fixedmaker/inventory_skew.go +++ b/pkg/strategy/fixedmaker/inventory_skew.go @@ -12,8 +12,8 @@ var ( ) type InventorySkewBidAskRatios struct { - bidRatio fixedpoint.Value - askRatio fixedpoint.Value + BidRatio fixedpoint.Value + AskRatio fixedpoint.Value } // https://hummingbot.org/strategy-configs/inventory-skew/ @@ -46,8 +46,8 @@ func (s *InventorySkew) CalculateBidAskRatios(quantity fixedpoint.Value, price f askAdjustment := interp(baseValue, leftLimit, rightLimit, zero, two).Clamp(zero, two) return &InventorySkewBidAskRatios{ - bidRatio: bidAdjustment, - askRatio: askAdjustment, + BidRatio: bidAdjustment, + AskRatio: askAdjustment, } } diff --git a/pkg/strategy/fixedmaker/inventory_skew_test.go b/pkg/strategy/fixedmaker/inventory_skew_test.go index 8c7313997..cd40931a1 100644 --- a/pkg/strategy/fixedmaker/inventory_skew_test.go +++ b/pkg/strategy/fixedmaker/inventory_skew_test.go @@ -21,8 +21,8 @@ func Test_InventorySkew_CalculateBidAskRatios(t *testing.T) { baseBalance: fixedpoint.NewFromFloat(1.0), quoteBalance: fixedpoint.NewFromFloat(1000), want: &InventorySkewBidAskRatios{ - bidRatio: fixedpoint.NewFromFloat(1.0), - askRatio: fixedpoint.NewFromFloat(1.0), + BidRatio: fixedpoint.NewFromFloat(1.0), + AskRatio: fixedpoint.NewFromFloat(1.0), }, }, { @@ -31,8 +31,8 @@ func Test_InventorySkew_CalculateBidAskRatios(t *testing.T) { baseBalance: fixedpoint.NewFromFloat(1.0), quoteBalance: fixedpoint.NewFromFloat(1200), want: &InventorySkewBidAskRatios{ - bidRatio: fixedpoint.NewFromFloat(1.5), - askRatio: fixedpoint.NewFromFloat(0.5), + BidRatio: fixedpoint.NewFromFloat(1.5), + AskRatio: fixedpoint.NewFromFloat(0.5), }, }, { @@ -41,8 +41,8 @@ func Test_InventorySkew_CalculateBidAskRatios(t *testing.T) { baseBalance: fixedpoint.NewFromFloat(0.0), quoteBalance: fixedpoint.NewFromFloat(10000), want: &InventorySkewBidAskRatios{ - bidRatio: fixedpoint.NewFromFloat(2.0), - askRatio: fixedpoint.NewFromFloat(0.0), + BidRatio: fixedpoint.NewFromFloat(2.0), + AskRatio: fixedpoint.NewFromFloat(0.0), }, }, { @@ -51,8 +51,8 @@ func Test_InventorySkew_CalculateBidAskRatios(t *testing.T) { baseBalance: fixedpoint.NewFromFloat(2.0), quoteBalance: fixedpoint.NewFromFloat(0.0), want: &InventorySkewBidAskRatios{ - bidRatio: fixedpoint.NewFromFloat(0.0), - askRatio: fixedpoint.NewFromFloat(2.0), + BidRatio: fixedpoint.NewFromFloat(0.0), + AskRatio: fixedpoint.NewFromFloat(2.0), }, }, } @@ -63,7 +63,7 @@ func Test_InventorySkew_CalculateBidAskRatios(t *testing.T) { 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()) + 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 f08f6c4b4..8046636a8 100644 --- a/pkg/strategy/fixedmaker/strategy.go +++ b/pkg/strategy/fixedmaker/strategy.go @@ -195,9 +195,9 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err 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("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()) } 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")