From e14f09a914649fb62e0fa6debd7a5d2d3fcf78b0 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 6 Sep 2024 21:47:43 +0800 Subject: [PATCH 1/6] xmaker: add sourceDepthLevel option --- pkg/strategy/xmaker/strategy.go | 38 +++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index de97f691d..e90d16e89 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -91,11 +91,12 @@ type Strategy struct { SignalConfigList []SignalConfig `json:"signals"` SignalMarginScale *bbgo.SlideRule `json:"signalMarginScale,omitempty"` - Margin fixedpoint.Value `json:"margin"` - BidMargin fixedpoint.Value `json:"bidMargin"` - AskMargin fixedpoint.Value `json:"askMargin"` - UseDepthPrice bool `json:"useDepthPrice"` - DepthQuantity fixedpoint.Value `json:"depthQuantity"` + Margin fixedpoint.Value `json:"margin"` + BidMargin fixedpoint.Value `json:"bidMargin"` + AskMargin fixedpoint.Value `json:"askMargin"` + UseDepthPrice bool `json:"useDepthPrice"` + DepthQuantity fixedpoint.Value `json:"depthQuantity"` + SourceDepthLevel types.Depth `json:"sourceDepthLevel"` EnableBollBandMargin bool `json:"enableBollBandMargin"` BollBandInterval types.Interval `json:"bollBandInterval"` @@ -159,7 +160,7 @@ type Strategy struct { ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"` - book *types.StreamOrderBook + sourceBook *types.StreamOrderBook activeMakerOrders *bbgo.ActiveOrderBook hedgeErrorLimiter *rate.Limiter @@ -199,7 +200,10 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { panic(fmt.Errorf("source session %s is not defined", s.SourceExchange)) } - sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) + sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{ + Depth: s.SourceDepthLevel, + }) + sourceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) makerSession, ok := sessions[s.MakerExchange] @@ -212,6 +216,8 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { for _, sig := range s.SignalConfigList { if sig.TradeVolumeWindowSignal != nil { sourceSession.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } else if sig.BollingerBandTrendSignal != nil { + sourceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: sig.BollingerBandTrendSignal.Interval}) } } } @@ -435,7 +441,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { } } - bestBid, bestAsk, hasPrice := s.book.BestBidAndAsk() + bestBid, bestAsk, hasPrice := s.sourceBook.BestBidAndAsk() if !hasPrice { s.logger.Warnf("no valid price, skip quoting") return @@ -446,7 +452,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { s.priceSolver.Update(s.Symbol, s.lastPrice) - bookLastUpdateTime := s.book.LastUpdateTime() + bookLastUpdateTime := s.sourceBook.LastUpdateTime() if _, err := s.bidPriceHeartBeat.Update(bestBid); err != nil { s.logger.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago", @@ -462,7 +468,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { return } - sourceBook := s.book.CopyDepth(10) + sourceBook := s.sourceBook.CopyDepth(10) if valid, err := sourceBook.IsValid(); !valid { s.logger.WithError(err).Errorf("%s invalid copied order book, skip quoting: %v", s.Symbol, err) return @@ -906,7 +912,7 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { } lastPrice := s.lastPrice - sourceBook := s.book.CopyDepth(1) + sourceBook := s.sourceBook.CopyDepth(1) switch side { case types.SideTypeBuy: @@ -1029,6 +1035,10 @@ func (s *Strategy) Defaults() error { s.BollBandInterval = types.Interval1m } + if s.SourceDepthLevel == "" { + s.SourceDepthLevel = types.DepthLevelMedium + } + if s.BollBandMarginFactor.IsZero() { s.BollBandMarginFactor = fixedpoint.One } @@ -1350,8 +1360,8 @@ func (s *Strategy) CrossRun( s.ProfitStats.ProfitStats = profitStats } - s.book = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName) - s.book.BindStream(s.sourceSession.MarketDataStream) + s.sourceBook = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName) + s.sourceBook.BindStream(s.sourceSession.MarketDataStream) if s.EnableSignalMargin { scale, err := s.SignalMarginScale.Scale() @@ -1365,7 +1375,7 @@ func (s *Strategy) CrossRun( for _, signalConfig := range s.SignalConfigList { if signalConfig.OrderBookBestPriceSignal != nil { - signalConfig.OrderBookBestPriceSignal.book = s.book + signalConfig.OrderBookBestPriceSignal.book = s.sourceBook if err := signalConfig.OrderBookBestPriceSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil { return err } From 6ad16b74887c046bc8f9b5c09f44a3e6d5d64c1f Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 7 Sep 2024 13:47:21 +0800 Subject: [PATCH 2/6] xmaker: add EnableArbitrage option and makerBook --- pkg/strategy/xmaker/strategy.go | 70 +++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index e90d16e89..7101fea68 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -127,6 +127,8 @@ type Strategy struct { NotifyTrade bool `json:"notifyTrade"` + EnableArbitrage bool `json:"arbitrage"` + // RecoverTrade tries to find the missing trades via the REStful API RecoverTrade bool `json:"recoverTrade"` @@ -160,8 +162,8 @@ type Strategy struct { ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"` - sourceBook *types.StreamOrderBook - activeMakerOrders *bbgo.ActiveOrderBook + sourceBook, makerBook *types.StreamOrderBook + activeMakerOrders *bbgo.ActiveOrderBook hedgeErrorLimiter *rate.Limiter hedgeErrorRateReservation *rate.Reservation @@ -213,6 +215,12 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { makerSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) + if s.EnableArbitrage { + makerSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{ + Depth: types.DepthLevelMedium, + }) + } + for _, sig := range s.SignalConfigList { if sig.TradeVolumeWindowSignal != nil { sourceSession.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) @@ -277,7 +285,7 @@ func (s *Strategy) getBollingerTrend(quote *Quote) int { } func (s *Strategy) applySignalMargin(ctx context.Context, quote *Quote) error { - signal, err := s.calculateSignal(ctx) + signal, err := s.aggregateSignal(ctx) if err != nil { return err } @@ -373,7 +381,7 @@ func (s *Strategy) applyBollingerMargin( return nil } -func (s *Strategy) calculateSignal(ctx context.Context) (float64, error) { +func (s *Strategy) aggregateSignal(ctx context.Context) (float64, error) { sum := 0.0 voters := 0.0 for _, signal := range s.SignalConfigList { @@ -417,10 +425,11 @@ func (s *Strategy) updateQuote(ctx context.Context) { } if s.activeMakerOrders.NumOfOrders() > 0 { + s.logger.Warnf("unable to cancel all %s orders, skipping placing maker orders", s.Symbol) return } - signal, err := s.calculateSignal(ctx) + signal, err := s.aggregateSignal(ctx) if err != nil { return } @@ -447,6 +456,34 @@ func (s *Strategy) updateQuote(ctx context.Context) { return } + bestBidPrice := bestBid.Price + bestAskPrice := bestAsk.Price + s.logger.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice) + + if bestBidPrice.Compare(bestAskPrice) > 0 { + log.Errorf("best bid price %f is higher than best ask price %f, skip quoting", + bestBidPrice.Float64(), + bestAskPrice.Float64(), + ) + return + } + + if s.EnableArbitrage { + if makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk(); ok { + if makerAsk.Price.Compare(bestBid.Price) <= 0 { + askPvs := s.makerBook.SideBook(types.SideTypeSell) + for _, pv := range askPvs { + if pv.Price.Compare(bestBid.Price) <= 0 { + + } + } + // send ioc order for arbitrage + } else if makerBid.Price.Compare(bestAsk.Price) >= 0 { + // send ioc order for arbitrage + } + } + } + // use mid-price for the last price s.lastPrice = bestBid.Price.Add(bestAsk.Price).Div(two) @@ -645,18 +682,6 @@ func (s *Strategy) updateQuote(ctx context.Context) { return } - bestBidPrice := bestBid.Price - bestAskPrice := bestAsk.Price - s.logger.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice) - - if bestBidPrice.Compare(bestAskPrice) > 0 { - log.Errorf("best bid price %f is higher than best ask price %f, skip quoting", - bestBidPrice.Float64(), - bestAskPrice.Float64(), - ) - return - } - var submitOrders []types.SubmitOrder var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value var bidQuantity = s.Quantity @@ -687,14 +712,6 @@ func (s *Strategy) updateQuote(ctx context.Context) { bidPrice := quote.BestBidPrice askPrice := quote.BestAskPrice - if bidPrice.Compare(askPrice) > 0 { - log.Errorf("maker bid price %f is higher than maker ask price %f, skip quoting", - bidPrice.Float64(), - askPrice.Float64(), - ) - return - } - bidMarginMetrics.With(s.metricsLabels).Set(quote.BidMargin.Float64()) askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64()) @@ -1360,6 +1377,9 @@ func (s *Strategy) CrossRun( s.ProfitStats.ProfitStats = profitStats } + s.makerBook = types.NewStreamBook(s.Symbol, s.makerSession.ExchangeName) + s.makerBook.BindStream(s.makerSession.MarketDataStream) + s.sourceBook = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName) s.sourceBook.BindStream(s.sourceSession.MarketDataStream) From f24a96c8c3d3552bcb0d77412dad90f768f8bf13 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 7 Sep 2024 14:19:07 +0800 Subject: [PATCH 3/6] xmaker: refactor getInitialLayerQuantity for quantity multiplier --- pkg/strategy/xmaker/strategy.go | 83 ++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 7101fea68..009c37edf 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -417,21 +417,48 @@ func (s *Strategy) aggregateSignal(ctx context.Context) (float64, error) { return sum / voters, nil } -func (s *Strategy) updateQuote(ctx context.Context) { +// getInitialLayerQuantity returns the initial quantity for the layer +// i is the layer index, starting from 0 +func (s *Strategy) getInitialLayerQuantity(i int) (fixedpoint.Value, error) { + if s.QuantityScale != nil { + qf, err := s.QuantityScale.Scale(i + 1) + if err != nil { + return fixedpoint.Zero, fmt.Errorf("quantityScale error: %w", err) + } + + log.Infof("%s scaling bid #%d quantity to %f", s.Symbol, i+1, qf) + + // override the default quantity + return fixedpoint.NewFromFloat(qf), nil + } + + q := s.Quantity + + if s.QuantityMultiplier.Sign() > 0 && i > 0 { + q = fixedpoint.NewFromFloat( + q.Float64() * math.Pow( + s.QuantityMultiplier.Float64(), float64(i+1))) + } + + // fallback to the fixed quantity + return q, nil +} + +func (s *Strategy) updateQuote(ctx context.Context) error { if err := s.activeMakerOrders.GracefulCancel(ctx, s.makerSession.Exchange); err != nil { s.logger.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol) s.activeMakerOrders.Print() - return + return nil } if s.activeMakerOrders.NumOfOrders() > 0 { s.logger.Warnf("unable to cancel all %s orders, skipping placing maker orders", s.Symbol) - return + return nil } signal, err := s.aggregateSignal(ctx) if err != nil { - return + return err } s.logger.Infof("aggregated signal: %f", signal) @@ -446,14 +473,14 @@ func (s *Strategy) updateQuote(ctx context.Context) { bbgo.Notify("Strategy %s is halted, reason: %s", ID, reason) } - return + return nil } } bestBid, bestAsk, hasPrice := s.sourceBook.BestBidAndAsk() if !hasPrice { s.logger.Warnf("no valid price, skip quoting") - return + return fmt.Errorf("no valid book price") } bestBidPrice := bestBid.Price @@ -461,11 +488,10 @@ func (s *Strategy) updateQuote(ctx context.Context) { s.logger.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice) if bestBidPrice.Compare(bestAskPrice) > 0 { - log.Errorf("best bid price %f is higher than best ask price %f, skip quoting", + return fmt.Errorf("best bid price %f is higher than best ask price %f, skip quoting", bestBidPrice.Float64(), bestAskPrice.Float64(), ) - return } if s.EnableArbitrage { @@ -495,20 +521,20 @@ func (s *Strategy) updateQuote(ctx context.Context) { s.logger.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago", s.Symbol, time.Since(bookLastUpdateTime)) - return + return err } if _, err := s.askPriceHeartBeat.Update(bestAsk); err != nil { s.logger.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago", s.Symbol, time.Since(bookLastUpdateTime)) - return + return err } sourceBook := s.sourceBook.CopyDepth(10) if valid, err := sourceBook.IsValid(); !valid { s.logger.WithError(err).Errorf("%s invalid copied order book, skip quoting: %v", s.Symbol, err) - return + return err } var disableMakerBid = false @@ -679,12 +705,11 @@ func (s *Strategy) updateQuote(ctx context.Context) { if disableMakerAsk && disableMakerBid { log.Warnf("%s bid/ask maker is disabled due to insufficient balances", s.Symbol) - return + return nil } var submitOrders []types.SubmitOrder var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value - var bidQuantity = s.Quantity var askQuantity = s.Quantity var quote = &Quote{ @@ -715,22 +740,14 @@ func (s *Strategy) updateQuote(ctx context.Context) { bidMarginMetrics.With(s.metricsLabels).Set(quote.BidMargin.Float64()) askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64()) - for i := 0; i < s.NumLayers; i++ { - // for maker bid orders - if !disableMakerBid { - if s.QuantityScale != nil { - qf, err := s.QuantityScale.Scale(i + 1) - if err != nil { - log.WithError(err).Errorf("quantityScale error") - return - } - - log.Infof("%s scaling bid #%d quantity to %f", s.Symbol, i+1, qf) - - // override the default bid quantity - bidQuantity = fixedpoint.NewFromFloat(qf) + if !disableMakerBid { + for i := 0; i < s.NumLayers; i++ { + bidQuantity, err := s.getInitialLayerQuantity(i) + if err != nil { + return err } + // for maker bid orders accumulativeBidQuantity = accumulativeBidQuantity.Add(bidQuantity) if s.UseDepthPrice { @@ -785,14 +802,15 @@ func (s *Strategy) updateQuote(ctx context.Context) { bidQuantity = bidQuantity.Mul(s.QuantityMultiplier) } } + } + for i := 0; i < s.NumLayers; i++ { // for maker ask orders if !disableMakerAsk { if s.QuantityScale != nil { qf, err := s.QuantityScale.Scale(i + 1) if err != nil { - log.WithError(err).Errorf("quantityScale error") - return + return fmt.Errorf("quantityScale error: %w", err) } log.Infof("%s scaling ask #%d quantity to %f", s.Symbol, i+1, qf) @@ -859,12 +877,12 @@ func (s *Strategy) updateQuote(ctx context.Context) { if len(submitOrders) == 0 { log.Warnf("no orders generated") - return + return nil } formattedOrders, err := s.makerSession.FormatOrders(submitOrders) if err != nil { - return + return err } orderCreateCallback := func(createdOrder types.Order) { @@ -877,7 +895,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, orderCreateCallback, formattedOrders...) if err != nil { log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders) - return + return err } openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64()) @@ -885,6 +903,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { _ = errIdx _ = createdOrders + return nil } func (s *Strategy) adjustHedgeQuantityWithAvailableBalance( From 960ea89d8cb7a3b18d5d87f9a31cbde6de045afa Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 14:41:27 +0800 Subject: [PATCH 4/6] testhelper: add more test helpers --- pkg/testing/testhelper/market.go | 43 +++++++++++++++++++ pkg/testing/testhelper/pricevolumeslice.go | 49 ++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 pkg/testing/testhelper/market.go create mode 100644 pkg/testing/testhelper/pricevolumeslice.go diff --git a/pkg/testing/testhelper/market.go b/pkg/testing/testhelper/market.go new file mode 100644 index 000000000..48cd30e5d --- /dev/null +++ b/pkg/testing/testhelper/market.go @@ -0,0 +1,43 @@ +package testhelper + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +var markets = map[string]types.Market{ + "BTCUSDT": { + Symbol: "BTCUSDT", + PricePrecision: 2, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + MinNotional: fixedpoint.MustNewFromString("0.001"), + MinAmount: fixedpoint.MustNewFromString("10.0"), + MinQuantity: fixedpoint.MustNewFromString("0.001"), + TickSize: fixedpoint.MustNewFromString("0.01"), + }, + + "ETHUSDT": { + Symbol: "ETH", + PricePrecision: 2, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "ETH", + MinNotional: fixedpoint.MustNewFromString("0.005"), + MinAmount: fixedpoint.MustNewFromString("10.0"), + MinQuantity: fixedpoint.MustNewFromString("0.001"), + TickSize: fixedpoint.MustNewFromString("0.01"), + }, +} + +func Market(symbol string) types.Market { + market, ok := markets[symbol] + if !ok { + panic(fmt.Errorf("%s market not found, valid markets: %+v", symbol, markets)) + } + + return market +} diff --git a/pkg/testing/testhelper/pricevolumeslice.go b/pkg/testing/testhelper/pricevolumeslice.go new file mode 100644 index 000000000..ea7838063 --- /dev/null +++ b/pkg/testing/testhelper/pricevolumeslice.go @@ -0,0 +1,49 @@ +package testhelper + +import ( + "fmt" + "strings" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func PriceVolumeSliceFromText(str string) (slice types.PriceVolumeSlice) { + lines := strings.Split(str, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if len(line) == 0 { + continue + } + + cols := strings.SplitN(line, ",", 2) + if len(cols) < 2 { + panic(fmt.Errorf("column length should be 2, got %d", len(cols))) + } + + price := fixedpoint.MustNewFromString(strings.TrimSpace(cols[0])) + volume := fixedpoint.MustNewFromString(strings.TrimSpace(cols[1])) + slice = append(slice, types.PriceVolume{ + Price: price, + Volume: volume, + }) + } + + return slice +} + +func PriceVolumeSlice(values ...fixedpoint.Value) (slice types.PriceVolumeSlice) { + if len(values)%2 != 0 { + panic("values should be paired") + } + + for i := 0; i < len(values); i += 2 { + slice = append(slice, types.PriceVolume{ + Price: values[i], + Volume: values[i+1], + }) + + } + + return slice +} From 77dfe213e5a3abc3126ce994d3ec6a66a0316483 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 14:41:41 +0800 Subject: [PATCH 5/6] xmaker: pull out getLayerPrice and add test against the method --- pkg/strategy/xmaker/strategy.go | 71 ++++++++++++++++------ pkg/strategy/xmaker/strategy_test.go | 91 +++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 32 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 009c37edf..f8a8a173f 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -444,6 +444,53 @@ func (s *Strategy) getInitialLayerQuantity(i int) (fixedpoint.Value, error) { return q, nil } +func (s *Strategy) getLayerPrice( + i int, + side types.SideType, + sourceBook *types.StreamOrderBook, + quote *Quote, + requiredDepth fixedpoint.Value, +) (price fixedpoint.Value) { + var margin, delta, pips fixedpoint.Value + + switch side { + case types.SideTypeSell: + margin = quote.AskMargin + delta = margin + + if quote.AskLayerPips.Sign() > 0 { + pips = quote.AskLayerPips + } else { + pips = fixedpoint.One + } + + case types.SideTypeBuy: + margin = quote.BidMargin + delta = margin.Neg() + + if quote.BidLayerPips.Sign() > 0 { + pips = quote.BidLayerPips.Neg() + } else { + pips = fixedpoint.One.Neg() + } + } + + if s.UseDepthPrice { + price = aggregatePrice(sourceBook.SideBook(side), requiredDepth) + price = price.Mul(fixedpoint.One.Add(delta)) + if i > 0 { + price = price.Add(pips.Mul(s.makerMarket.TickSize)) + } + } else { + price = price.Mul(fixedpoint.One.Add(delta)) + if i > 0 { + price = price.Add(pips.Mul(s.makerMarket.TickSize)) + } + } + + return price +} + func (s *Strategy) updateQuote(ctx context.Context) error { if err := s.activeMakerOrders.GracefulCancel(ctx, s.makerSession.Exchange); err != nil { s.logger.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol) @@ -710,7 +757,6 @@ func (s *Strategy) updateQuote(ctx context.Context) error { var submitOrders []types.SubmitOrder var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value - var askQuantity = s.Quantity var quote = &Quote{ BestBidPrice: bestBidPrice, @@ -798,26 +844,17 @@ func (s *Strategy) updateQuote(ctx context.Context) error { hedgeQuota.Rollback() } - if s.QuantityMultiplier.Sign() > 0 { - bidQuantity = bidQuantity.Mul(s.QuantityMultiplier) - } } } - for i := 0; i < s.NumLayers; i++ { - // for maker ask orders - if !disableMakerAsk { - if s.QuantityScale != nil { - qf, err := s.QuantityScale.Scale(i + 1) - if err != nil { - return fmt.Errorf("quantityScale error: %w", err) - } - - log.Infof("%s scaling ask #%d quantity to %f", s.Symbol, i+1, qf) - - // override the default bid quantity - askQuantity = fixedpoint.NewFromFloat(qf) + // for maker ask orders + if !disableMakerAsk { + for i := 0; i < s.NumLayers; i++ { + askQuantity, err := s.getInitialLayerQuantity(i) + if err != nil { + return err } + accumulativeAskQuantity = accumulativeAskQuantity.Add(askQuantity) if s.UseDepthPrice { diff --git a/pkg/strategy/xmaker/strategy_test.go b/pkg/strategy/xmaker/strategy_test.go index d3f2b7fb9..82183ccf0 100644 --- a/pkg/strategy/xmaker/strategy_test.go +++ b/pkg/strategy/xmaker/strategy_test.go @@ -2,28 +2,89 @@ package xmaker import ( "testing" + "time" + + "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/stretchr/testify/assert" + + . "github.com/c9s/bbgo/pkg/testing/testhelper" ) -func Test_aggregatePrice(t *testing.T) { - bids := types.PriceVolumeSlice{ - { - Price: fixedpoint.NewFromFloat(1000.0), - Volume: fixedpoint.NewFromFloat(1.0), - }, - { - Price: fixedpoint.NewFromFloat(1200.0), - Volume: fixedpoint.NewFromFloat(1.0), - }, - { - Price: fixedpoint.NewFromFloat(1400.0), - Volume: fixedpoint.NewFromFloat(1.0), - }, +func TestStrategy_getLayerPrice(t *testing.T) { + symbol := "BTCUSDT" + market := Market(symbol) + + s := &Strategy{ + UseDepthPrice: true, + DepthQuantity: Number(3.0), + makerMarket: market, } + sourceBook := types.NewStreamBook(symbol, types.ExchangeBinance) + sourceBook.Load(types.SliceOrderBook{ + Symbol: symbol, + Bids: PriceVolumeSlice( + Number(1300.0), Number(1.0), + Number(1200.0), Number(2.0), + Number(1100.0), Number(3.0), + ), + Asks: PriceVolumeSlice( + Number(1301.0), Number(1.0), + Number(1400.0), Number(2.0), + Number(1500.0), Number(3.0), + ), + Time: time.Time{}, + LastUpdateId: 1, + }) + + quote := &Quote{ + BestBidPrice: Number(1300.0), + BestAskPrice: Number(1301.0), + BidMargin: Number(0.001), + AskMargin: Number(0.001), + BidLayerPips: Number(100.0), + AskLayerPips: Number(100.0), + } + + t.Run("depthPrice bid price at 0", func(t *testing.T) { + price := s.getLayerPrice(0, types.SideTypeBuy, sourceBook, quote, s.DepthQuantity) + + // (1300 + 1200*2)/3 * (1 - 0.001) + assert.InDelta(t, 1232.10, price.Float64(), 0.01) + }) + + t.Run("depthPrice bid price at 1", func(t *testing.T) { + price := s.getLayerPrice(1, types.SideTypeBuy, sourceBook, quote, s.DepthQuantity) + + // (1300 + 1200*2)/3 * (1 - 0.001) - 100 * 0.01 + assert.InDelta(t, 1231.10, price.Float64(), 0.01) + }) + + t.Run("depthPrice ask price at 0", func(t *testing.T) { + price := s.getLayerPrice(0, types.SideTypeSell, sourceBook, quote, s.DepthQuantity) + + // (1301 + 1400*2)/3 * (1 + 0.001) + assert.InDelta(t, 1368.367, price.Float64(), 0.01) + }) + + t.Run("depthPrice ask price at 1", func(t *testing.T) { + price := s.getLayerPrice(1, types.SideTypeSell, sourceBook, quote, s.DepthQuantity) + + // (1301 + 1400*2)/3 * (1 + 0.001) + 100 * 0.01 + assert.InDelta(t, 1369.367, price.Float64(), 0.01) + }) + +} + +func Test_aggregatePrice(t *testing.T) { + bids := PriceVolumeSliceFromText(` + 1000.0, 1.0 + 1200.0, 1.0 + 1400.0, 1.0 +`) + aggregatedPrice1 := aggregatePrice(bids, fixedpoint.NewFromFloat(0.5)) assert.Equal(t, fixedpoint.NewFromFloat(1000.0), aggregatedPrice1) From 90749f4873263c8c6fb3e92293050ef64d61e072 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 15:04:56 +0800 Subject: [PATCH 6/6] xmaker: pull out s.UseDepthPrice dependency --- pkg/strategy/xmaker/strategy.go | 54 +++++++++++---------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index f8a8a173f..5c8210784 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -203,7 +203,8 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { } sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{ - Depth: s.SourceDepthLevel, + // TODO: fix depth20 stream for binance + // Depth: s.SourceDepthLevel, }) sourceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) @@ -444,6 +445,10 @@ func (s *Strategy) getInitialLayerQuantity(i int) (fixedpoint.Value, error) { return q, nil } +// getLayerPrice returns the price for the layer +// i is the layer index, starting from 0 +// side is the side of the order +// sourceBook is the source order book func (s *Strategy) getLayerPrice( i int, side types.SideType, @@ -475,7 +480,7 @@ func (s *Strategy) getLayerPrice( } } - if s.UseDepthPrice { + if requiredDepth.Sign() > 0 { price = aggregatePrice(sourceBook.SideBook(side), requiredDepth) price = price.Mul(fixedpoint.One.Add(delta)) if i > 0 { @@ -796,29 +801,17 @@ func (s *Strategy) updateQuote(ctx context.Context) error { // for maker bid orders accumulativeBidQuantity = accumulativeBidQuantity.Add(bidQuantity) + requiredDepth := fixedpoint.Zero if s.UseDepthPrice { - sideBook := sourceBook.SideBook(types.SideTypeBuy) if s.DepthQuantity.Sign() > 0 { - if i == 0 { - bidPrice = aggregatePrice(sideBook, s.DepthQuantity) - bidPrice = bidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) - } else if i > 0 && quote.BidLayerPips.Sign() > 0 { - pips := quote.BidLayerPips.Mul(s.makerMarket.TickSize) - bidPrice = bidPrice.Sub(pips) - } + requiredDepth = s.DepthQuantity } else { - bidPrice = aggregatePrice(sideBook, accumulativeBidQuantity) - bidPrice = bidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) - } - } else { - if i == 0 { - bidPrice = bidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) - } else if i > 0 && quote.BidLayerPips.Sign() > 0 { - pips := quote.BidLayerPips.Mul(s.makerMarket.TickSize) - bidPrice = bidPrice.Sub(pips) + requiredDepth = accumulativeBidQuantity } } + bidPrice = s.getLayerPrice(i, types.SideTypeBuy, s.sourceBook, quote, requiredDepth) + if i == 0 { s.logger.Infof("maker best bid price %f", bidPrice.Float64()) makerBestBidPriceMetrics.With(s.metricsLabels).Set(bidPrice.Float64()) @@ -857,28 +850,17 @@ func (s *Strategy) updateQuote(ctx context.Context) error { accumulativeAskQuantity = accumulativeAskQuantity.Add(askQuantity) + requiredDepth := fixedpoint.Zero if s.UseDepthPrice { if s.DepthQuantity.Sign() > 0 { - if i == 0 { - askPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeSell), s.DepthQuantity) - askPrice = askPrice.Mul(fixedpoint.One.Add(quote.AskMargin)) - } else if i > 0 && quote.AskLayerPips.Sign() > 0 { - pips := quote.AskLayerPips.Mul(s.makerMarket.TickSize) - askPrice = askPrice.Add(pips) - } + requiredDepth = s.DepthQuantity } else { - askPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeSell), accumulativeAskQuantity) - askPrice = askPrice.Mul(fixedpoint.One.Add(quote.AskMargin)) - } - } else { - if i == 0 { - askPrice = askPrice.Mul(fixedpoint.One.Add(quote.AskMargin)) - } else if i > 0 && quote.AskLayerPips.Sign() > 0 { - pips := quote.AskLayerPips.Mul(s.makerMarket.TickSize) - askPrice = askPrice.Add(pips) + requiredDepth = accumulativeAskQuantity } } + askPrice = s.getLayerPrice(i, types.SideTypeSell, s.sourceBook, quote, requiredDepth) + if i == 0 { s.logger.Infof("maker best ask price %f", askPrice.Float64()) makerBestAskPriceMetrics.With(s.metricsLabels).Set(askPrice.Float64()) @@ -1168,7 +1150,7 @@ func (s *Strategy) Defaults() error { } func (s *Strategy) Validate() error { - if s.Quantity.IsZero() || s.QuantityScale == nil { + if s.Quantity.IsZero() && s.QuantityScale == nil { return errors.New("quantity or quantityScale can not be empty") }