From 46b3a81b07ff3af2475f0c2c5740ff059b39500b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Nov 2023 10:32:03 +0800 Subject: [PATCH] xdepthmaker: add tests to the generateMakerOrders --- config/xdepthmaker.yaml | 67 +++++++++++ pkg/strategy/xdepthmaker/strategy.go | 127 ++++++++++++++++++++- pkg/strategy/xdepthmaker/strategy_test.go | 72 ++++++++++++ pkg/testing/testhelper/assert_priceside.go | 2 +- pkg/types/price_volume_slice.go | 21 +++- 5 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 config/xdepthmaker.yaml create mode 100644 pkg/strategy/xdepthmaker/strategy_test.go diff --git a/config/xdepthmaker.yaml b/config/xdepthmaker.yaml new file mode 100644 index 000000000..c278bbde9 --- /dev/null +++ b/config/xdepthmaker.yaml @@ -0,0 +1,67 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + switches: + trade: true + orderUpdate: false + submitOrder: false + +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +logging: + trade: true + order: true + fields: + env: staging + +sessions: + max: + exchange: max + envVarPrefix: max + + binance: + exchange: binance + envVarPrefix: binance + +crossExchangeStrategies: + +- xdepthmaker: + symbol: "BTCUSDT" + makerExchange: max + hedgeExchange: binance + + # disableHedge disables the hedge orders on the source exchange + # disableHedge: true + + hedgeInterval: 10s + notifyTrade: true + + margin: 0.004 + askMargin: 0.4% + bidMargin: 0.4% + + depthScale: + byLayer: + linear: + domain: [1, 30] + range: [50, 20_000] + + # numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders + numLayers: 30 + + # pips is the fraction numbers between each order. for BTC, 1 pip is 0.1, + # 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and + # 18002.00 + pips: 10 + persistence: + type: redis + diff --git a/pkg/strategy/xdepthmaker/strategy.go b/pkg/strategy/xdepthmaker/strategy.go index 7352adeb9..fea67795c 100644 --- a/pkg/strategy/xdepthmaker/strategy.go +++ b/pkg/strategy/xdepthmaker/strategy.go @@ -202,6 +202,9 @@ type Strategy struct { // QuantityScale helps user to define the quantity by layer scale QuantityScale *bbgo.LayerScale `json:"quantityScale,omitempty"` + // DepthScale helps user to define the depth by layer scale + DepthScale *bbgo.LayerScale `json:"depthScale,omitempty"` + // MaxExposurePosition defines the unhedged quantity of stop MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` @@ -266,8 +269,8 @@ func (s *Strategy) Validate() error { return errors.New("maker exchange is not configured") } - if s.Quantity.IsZero() || s.QuantityScale == nil { - return errors.New("quantity or quantityScale can not be empty") + if s.DepthScale == nil { + return errors.New("depthScale can not be empty") } if len(s.Symbol) == 0 { @@ -576,7 +579,106 @@ func (s *Strategy) runTradeRecover(ctx context.Context) { } } -func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter) { +func (s *Strategy) generateMakerOrders(pricingBook *types.StreamOrderBook) ([]types.SubmitOrder, error) { + bestBid, bestAsk, hasPrice := pricingBook.BestBidAndAsk() + if !hasPrice { + return nil, nil + } + + bestBidPrice := bestBid.Price + bestAskPrice := bestAsk.Price + log.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice) + + lastMidPrice := bestBidPrice.Add(bestAskPrice).Div(Two) + _ = lastMidPrice + + var submitOrders []types.SubmitOrder + var accumulatedBidQuantity = fixedpoint.Zero + var accumulatedAskQuantity = fixedpoint.Zero + var accumulatedBidQuoteQuantity = fixedpoint.Zero + + dupPricingBook := pricingBook.CopyDepth(0) + + for _, side := range []types.SideType{types.SideTypeBuy, types.SideTypeSell} { + for i := 1; i <= s.NumLayers; i++ { + requiredDepthFloat, err := s.DepthScale.Scale(i) + if err != nil { + return nil, errors.Wrapf(err, "depthScale scale error") + } + + // requiredDepth is the required depth in quote currency + requiredDepth := fixedpoint.NewFromFloat(requiredDepthFloat) + + sideBook := dupPricingBook.SideBook(side) + index := sideBook.IndexByQuoteVolumeDepth(requiredDepth) + + pvs := types.PriceVolumeSlice{} + if index == -1 { + pvs = sideBook[:] + } else { + pvs = sideBook[0 : index+1] + } + + log.Infof("required depth: %f, pvs: %+v", requiredDepth.Float64(), pvs) + + depthPrice, err := averageDepthPrice(pvs) + if err != nil { + log.WithError(err).Errorf("error aggregating depth price") + continue + } + + switch side { + case types.SideTypeBuy: + if s.BidMargin.Sign() > 0 { + depthPrice = depthPrice.Mul(fixedpoint.One.Sub(s.BidMargin)) + } + + depthPrice = depthPrice.Round(s.makerMarket.PricePrecision+1, fixedpoint.Down) + + case types.SideTypeSell: + if s.AskMargin.Sign() > 0 { + depthPrice = depthPrice.Mul(fixedpoint.One.Add(s.AskMargin)) + } + + depthPrice = depthPrice.Round(s.makerMarket.PricePrecision+1, fixedpoint.Up) + } + + depthPrice = s.makerMarket.TruncatePrice(depthPrice) + + quantity := requiredDepth.Div(depthPrice) + quantity = s.makerMarket.TruncateQuantity(quantity) + log.Infof("side: %s required depth: %f price: %f quantity: %f", side, requiredDepth.Float64(), depthPrice.Float64(), quantity.Float64()) + + switch side { + case types.SideTypeBuy: + quantity = quantity.Sub(accumulatedBidQuantity) + + accumulatedBidQuantity = accumulatedBidQuantity.Add(quantity) + quoteQuantity := fixedpoint.Mul(quantity, depthPrice) + quoteQuantity = quoteQuantity.Round(s.makerMarket.PricePrecision, fixedpoint.Up) + accumulatedBidQuoteQuantity = accumulatedBidQuoteQuantity.Add(quoteQuantity) + + case types.SideTypeSell: + quantity = quantity.Sub(accumulatedAskQuantity) + accumulatedAskQuantity = accumulatedAskQuantity.Add(quantity) + + } + + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Market: s.makerMarket, + Side: side, + Price: depthPrice, + Quantity: quantity, + }) + } + } + + return submitOrders, nil +} + +func (s *Strategy) updateQuote(ctx context.Context) { if err := s.MakerOrderExecutor.GracefulCancel(ctx); err != nil { log.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol) s.MakerOrderExecutor.ActiveMakerOrders().Print() @@ -844,3 +946,22 @@ func selectSessions2( s2 = sessions[n2] return s1, s2, nil } + +func averageDepthPrice(pvs types.PriceVolumeSlice) (price fixedpoint.Value, err error) { + if len(pvs) == 0 { + return fixedpoint.Zero, fmt.Errorf("empty pv slice") + } + + totalQuoteAmount := fixedpoint.Zero + totalQuantity := fixedpoint.Zero + + for i := 0; i < len(pvs); i++ { + pv := pvs[i] + quoteAmount := fixedpoint.Mul(pv.Volume, pv.Price) + totalQuoteAmount = totalQuoteAmount.Add(quoteAmount) + totalQuantity = totalQuantity.Add(pv.Volume) + } + + price = totalQuoteAmount.Div(totalQuantity) + return price, nil +} diff --git a/pkg/strategy/xdepthmaker/strategy_test.go b/pkg/strategy/xdepthmaker/strategy_test.go new file mode 100644 index 000000000..10ccced40 --- /dev/null +++ b/pkg/strategy/xdepthmaker/strategy_test.go @@ -0,0 +1,72 @@ +package xdepthmaker + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/bbgo" + . "github.com/c9s/bbgo/pkg/testing/testhelper" + "github.com/c9s/bbgo/pkg/types" +) + +func newTestBTCUSDTMarket() types.Market { + return types.Market{ + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + TickSize: Number(0.01), + StepSize: Number(0.000001), + PricePrecision: 2, + VolumePrecision: 8, + MinNotional: Number(8.0), + MinQuantity: Number(0.0003), + } +} + +func TestStrategy_generateMakerOrders(t *testing.T) { + s := &Strategy{ + Symbol: "BTCUSDT", + NumLayers: 3, + DepthScale: &bbgo.LayerScale{ + LayerRule: &bbgo.SlideRule{ + LinearScale: &bbgo.LinearScale{ + Domain: [2]float64{1.0, 3.0}, + Range: [2]float64{1000.0, 15000.0}, + }, + }, + }, + CrossExchangeMarketMakingStrategy: &CrossExchangeMarketMakingStrategy{ + makerMarket: newTestBTCUSDTMarket(), + }, + } + + pricingBook := types.NewStreamBook("BTCUSDT") + pricingBook.OrderBook.Load(types.SliceOrderBook{ + Symbol: "BTCUSDT", + Bids: types.PriceVolumeSlice{ + {Price: Number("25000.00"), Volume: Number("0.1")}, + {Price: Number("24900.00"), Volume: Number("0.2")}, + {Price: Number("24800.00"), Volume: Number("0.3")}, + {Price: Number("24700.00"), Volume: Number("0.4")}, + }, + Asks: types.PriceVolumeSlice{ + {Price: Number("25100.00"), Volume: Number("0.1")}, + {Price: Number("25200.00"), Volume: Number("0.2")}, + {Price: Number("25300.00"), Volume: Number("0.3")}, + {Price: Number("25400.00"), Volume: Number("0.4")}, + }, + Time: time.Now(), + }) + + orders, err := s.generateMakerOrders(pricingBook) + assert.NoError(t, err) + AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ + {Side: types.SideTypeBuy, Price: Number("25000"), Quantity: Number("0.04")}, // =~ $1000.00 + {Side: types.SideTypeBuy, Price: Number("24866.66"), Quantity: Number("0.281715")}, // =~ $7005.3111219, accumulated amount =~ $1000.00 + $7005.3111219 = $8005.3111219 + {Side: types.SideTypeBuy, Price: Number("24800"), Quantity: Number("0.283123")}, // =~ $7021.4504, accumulated amount =~ $1000.00 + $7005.3111219 + $7021.4504 = $8005.3111219 + $7021.4504 =~ $15026.7615219 + {Side: types.SideTypeSell, Price: Number("25100"), Quantity: Number("0.03984")}, + {Side: types.SideTypeSell, Price: Number("25233.33"), Quantity: Number("0.2772")}, + {Side: types.SideTypeSell, Price: Number("25233.33"), Quantity: Number("0.277411")}, + }, orders) +} diff --git a/pkg/testing/testhelper/assert_priceside.go b/pkg/testing/testhelper/assert_priceside.go index 7c45cdd9d..0d7f74df1 100644 --- a/pkg/testing/testhelper/assert_priceside.go +++ b/pkg/testing/testhelper/assert_priceside.go @@ -32,7 +32,7 @@ type PriceSideQuantityAssert struct { func AssertOrdersPriceSideQuantity( t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder, ) { - assert.Equalf(t, len(orders), len(asserts), "expecting %d orders", len(asserts)) + assert.Equalf(t, len(asserts), len(orders), "expecting %d orders", len(asserts)) var assertPrices, orderPrices fixedpoint.Slice var assertPricesFloat, orderPricesFloat []float64 diff --git a/pkg/types/price_volume_slice.go b/pkg/types/price_volume_slice.go index fdaaaf771..1aa1756fc 100644 --- a/pkg/types/price_volume_slice.go +++ b/pkg/types/price_volume_slice.go @@ -38,7 +38,7 @@ func (slice PriceVolumeSlice) Trim() (pvs PriceVolumeSlice) { } func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice { - if depth > len(slice) { + if depth == 0 || depth > len(slice) { return slice.Copy() } @@ -67,8 +67,23 @@ func (slice PriceVolumeSlice) First() (PriceVolume, bool) { return PriceVolume{}, false } +func (slice PriceVolumeSlice) IndexByQuoteVolumeDepth(requiredQuoteVolume fixedpoint.Value) int { + var totalQuoteVolume = fixedpoint.Zero + for x, pv := range slice { + // this should use float64 multiply + quoteVolume := fixedpoint.Mul(pv.Volume, pv.Price) + totalQuoteVolume = totalQuoteVolume.Add(quoteVolume) + if totalQuoteVolume.Compare(requiredQuoteVolume) >= 0 { + return x + } + } + + // depth not enough + return -1 +} + func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int { - var tv fixedpoint.Value = fixedpoint.Zero + var tv = fixedpoint.Zero for x, el := range slice { tv = tv.Add(el.Volume) if tv.Compare(requiredVolume) >= 0 { @@ -76,7 +91,7 @@ func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value } } - // not deep enough + // depth not enough return -1 }