mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1402 from c9s/narumi/fixedmaker/inventory-skew
FEATURE: inventory skew
This commit is contained in:
commit
6367bd79d3
|
@ -16,12 +16,19 @@ exchangeStrategies:
|
||||||
- on: max
|
- on: max
|
||||||
fixedmaker:
|
fixedmaker:
|
||||||
symbol: USDCUSDT
|
symbol: USDCUSDT
|
||||||
interval: 5m
|
interval: 1m
|
||||||
halfSpread: 0.05%
|
halfSpread: 0.05%
|
||||||
quantity: 15
|
quantity: 15
|
||||||
orderType: LIMIT_MAKER
|
orderType: LIMIT_MAKER
|
||||||
dryRun: false
|
dryRun: true
|
||||||
|
|
||||||
positionHardLimit: 1500
|
positionHardLimit: 1500
|
||||||
maxPositionQuantity: 1500
|
maxPositionQuantity: 1500
|
||||||
circuitBreakLossThreshold: -0.15
|
circuitBreakLossThreshold: -0.15
|
||||||
|
circuitBreakEMA:
|
||||||
|
interval: 1m
|
||||||
|
window: 14
|
||||||
|
|
||||||
|
inventorySkew:
|
||||||
|
inventoryRangeMultiplier: 1.0
|
||||||
|
targetBaseRatio: 0.5
|
||||||
|
|
|
@ -6,6 +6,7 @@ sessions:
|
||||||
binance:
|
binance:
|
||||||
exchange: binance
|
exchange: binance
|
||||||
envVarPrefix: binance
|
envVarPrefix: binance
|
||||||
|
publicOnly: true
|
||||||
|
|
||||||
backtest:
|
backtest:
|
||||||
startTime: "2023-01-01"
|
startTime: "2023-01-01"
|
||||||
|
@ -25,11 +26,11 @@ crossExchangeStrategies:
|
||||||
- xfixedmaker:
|
- xfixedmaker:
|
||||||
tradingExchange: max
|
tradingExchange: max
|
||||||
symbol: BTCUSDT
|
symbol: BTCUSDT
|
||||||
interval: 5m
|
interval: 1m
|
||||||
halfSpread: 0.01%
|
halfSpread: 0.01%
|
||||||
quantity: 0.005
|
quantity: 0.005
|
||||||
orderType: LIMIT_MAKER
|
orderType: LIMIT_MAKER
|
||||||
dryRun: false
|
dryRun: true
|
||||||
|
|
||||||
referenceExchange: binance
|
referenceExchange: binance
|
||||||
referencePriceEMA:
|
referencePriceEMA:
|
||||||
|
@ -44,3 +45,7 @@ crossExchangeStrategies:
|
||||||
circuitBreakEMA:
|
circuitBreakEMA:
|
||||||
interval: 1m
|
interval: 1m
|
||||||
window: 14
|
window: 14
|
||||||
|
|
||||||
|
inventorySkew:
|
||||||
|
inventoryRangeMultiplier: 1.0
|
||||||
|
targetBaseRatio: 0.5
|
||||||
|
|
56
pkg/strategy/fixedmaker/inventory_skew.go
Normal file
56
pkg/strategy/fixedmaker/inventory_skew.go
Normal file
|
@ -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)))
|
||||||
|
}
|
69
pkg/strategy/fixedmaker/inventory_skew_test.go
Normal file
69
pkg/strategy/fixedmaker/inventory_skew_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,8 @@ type Strategy struct {
|
||||||
OrderType types.OrderType `json:"orderType"`
|
OrderType types.OrderType `json:"orderType"`
|
||||||
DryRun bool `json:"dryRun"`
|
DryRun bool `json:"dryRun"`
|
||||||
|
|
||||||
|
InventorySkew InventorySkew `json:"inventorySkew"`
|
||||||
|
|
||||||
activeOrderBook *bbgo.ActiveOrderBook
|
activeOrderBook *bbgo.ActiveOrderBook
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +72,10 @@ func (s *Strategy) Validate() error {
|
||||||
if s.HalfSpread.Float64() <= 0 {
|
if s.HalfSpread.Float64() <= 0 {
|
||||||
return fmt.Errorf("halfSpread should be positive")
|
return fmt.Errorf("halfSpread should be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.InventorySkew.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +129,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) cancelOrders(ctx context.Context) {
|
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")
|
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)
|
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())
|
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
|
// check balance and generate orders
|
||||||
amount := s.Quantity.Mul(buyPrice)
|
amount := s.Quantity.Mul(buyPrice)
|
||||||
if quoteBalance.Available.Compare(amount) > 0 {
|
if quoteBalance.Available.Compare(amount) > 0 {
|
||||||
|
@ -188,7 +209,7 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
Type: s.OrderType,
|
Type: s.OrderType,
|
||||||
Price: buyPrice,
|
Price: buyPrice,
|
||||||
Quantity: s.Quantity,
|
Quantity: buyQuantity,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
log.Infof("not enough quote balance to buy, available: %s, amount: %s", quoteBalance.Available, amount)
|
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,
|
Side: types.SideTypeSell,
|
||||||
Type: s.OrderType,
|
Type: s.OrderType,
|
||||||
Price: sellPrice,
|
Price: sellPrice,
|
||||||
Quantity: s.Quantity,
|
Quantity: sellQuantity,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
log.Infof("not enough base balance to sell, available: %s, quantity: %s", baseBalance.Available, s.Quantity)
|
log.Infof("not enough base balance to sell, available: %s, quantity: %s", baseBalance.Available, s.Quantity)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/strategy/common"
|
"github.com/c9s/bbgo/pkg/strategy/common"
|
||||||
|
"github.com/c9s/bbgo/pkg/strategy/fixedmaker"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ type Strategy struct {
|
||||||
ReferenceExchange string `json:"referenceExchange"`
|
ReferenceExchange string `json:"referenceExchange"`
|
||||||
ReferencePriceEMA types.IntervalWindow `json:"referencePriceEMA"`
|
ReferencePriceEMA types.IntervalWindow `json:"referencePriceEMA"`
|
||||||
OrderPriceLossThreshold fixedpoint.Value `json:"orderPriceLossThreshold"`
|
OrderPriceLossThreshold fixedpoint.Value `json:"orderPriceLossThreshold"`
|
||||||
|
InventorySkew fixedmaker.InventorySkew `json:"inventorySkew"`
|
||||||
|
|
||||||
market types.Market
|
market types.Market
|
||||||
activeOrderBook *bbgo.ActiveOrderBook
|
activeOrderBook *bbgo.ActiveOrderBook
|
||||||
|
@ -73,6 +75,10 @@ func (s *Strategy) Validate() error {
|
||||||
if s.HalfSpread.Float64() <= 0 {
|
if s.HalfSpread.Float64() <= 0 {
|
||||||
return fmt.Errorf("halfSpread should be positive")
|
return fmt.Errorf("halfSpread should be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.InventorySkew.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +161,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) cancelOrders(ctx context.Context) {
|
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")
|
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)
|
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())
|
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
|
// check balance and generate orders
|
||||||
amount := s.Quantity.Mul(buyPrice)
|
amount := s.Quantity.Mul(buyPrice)
|
||||||
if quoteBalance.Available.Compare(amount) > 0 {
|
if quoteBalance.Available.Compare(amount) > 0 {
|
||||||
|
@ -221,7 +242,7 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
Type: s.OrderType,
|
Type: s.OrderType,
|
||||||
Price: buyPrice,
|
Price: buyPrice,
|
||||||
Quantity: s.Quantity,
|
Quantity: buyQuantity,
|
||||||
})
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -238,7 +259,7 @@ func (s *Strategy) generateOrders(ctx context.Context) ([]types.SubmitOrder, err
|
||||||
Side: types.SideTypeSell,
|
Side: types.SideTypeSell,
|
||||||
Type: s.OrderType,
|
Type: s.OrderType,
|
||||||
Price: sellPrice,
|
Price: sellPrice,
|
||||||
Quantity: s.Quantity,
|
Quantity: sellQuantity,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
log.Infof("ref price risk control triggered, not placing sell order")
|
log.Infof("ref price risk control triggered, not placing sell order")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user