From ceda1e06b91336824456e95703be9d9a622bddc2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 17:49:53 +0800 Subject: [PATCH 1/6] xmaker: implement tryArbitrage --- pkg/strategy/xmaker/strategy.go | 120 +++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 5c8210784..918147d57 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -546,22 +546,6 @@ func (s *Strategy) updateQuote(ctx context.Context) error { ) } - 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) @@ -785,12 +769,19 @@ func (s *Strategy) updateQuote(ctx context.Context) error { bidExposureInUsd := fixedpoint.Zero askExposureInUsd := fixedpoint.Zero - bidPrice := quote.BestBidPrice - askPrice := quote.BestAskPrice bidMarginMetrics.With(s.metricsLabels).Set(quote.BidMargin.Float64()) askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64()) + if s.EnableArbitrage { + done, err := s.tryArbitrage(ctx, quote) + if err != nil { + s.logger.WithError(err).Errorf("unable to arbitrage") + } else if done { + return nil + } + } + if !disableMakerBid { for i := 0; i < s.NumLayers; i++ { bidQuantity, err := s.getInitialLayerQuantity(i) @@ -810,7 +801,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { } } - bidPrice = s.getLayerPrice(i, types.SideTypeBuy, s.sourceBook, quote, requiredDepth) + bidPrice := s.getLayerPrice(i, types.SideTypeBuy, s.sourceBook, quote, requiredDepth) if i == 0 { s.logger.Infof("maker best bid price %f", bidPrice.Float64()) @@ -859,7 +850,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { } } - askPrice = s.getLayerPrice(i, types.SideTypeSell, s.sourceBook, quote, requiredDepth) + askPrice := s.getLayerPrice(i, types.SideTypeSell, s.sourceBook, quote, requiredDepth) if i == 0 { s.logger.Infof("maker best ask price %f", askPrice.Float64()) @@ -904,14 +895,9 @@ func (s *Strategy) updateQuote(ctx context.Context) error { return err } - orderCreateCallback := func(createdOrder types.Order) { - s.orderStore.Add(createdOrder) - s.activeMakerOrders.Add(createdOrder) - } - defer s.tradeCollector.Process() - createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, orderCreateCallback, formattedOrders...) + createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, s.makerOrderCreateCallback, formattedOrders...) if err != nil { log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders) return err @@ -925,6 +911,88 @@ func (s *Strategy) updateQuote(ctx context.Context) error { return nil } +func (s *Strategy) makerOrderCreateCallback(createdOrder types.Order) { + s.orderStore.Add(createdOrder) + s.activeMakerOrders.Add(createdOrder) +} + +func aggregatePriceVolumeSliceWithPriceFilter(pvs types.PriceVolumeSlice, filterPrice fixedpoint.Value) types.PriceVolume { + var totalVolume = fixedpoint.Zero + var lastPrice = fixedpoint.Zero + for _, pv := range pvs { + if pv.Price.Compare(filterPrice) > 0 { + break + } + + lastPrice = pv.Price + totalVolume = totalVolume.Add(pv.Volume) + } + + return types.PriceVolume{ + Price: lastPrice, + Volume: totalVolume, + } +} + +// tryArbitrage tries to arbitrage between the source and maker exchange +func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote) (bool, error) { + marginBidPrice := quote.BestBidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) + marginAskPrice := quote.BestAskPrice.Mul(fixedpoint.One.Add(quote.BidMargin)) + + var iocOrders []types.SubmitOrder + if makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk(); ok { + if makerAsk.Price.Compare(marginBidPrice) <= 0 { + askPvs := s.makerBook.SideBook(types.SideTypeSell) + sumPv := aggregatePriceVolumeSliceWithPriceFilter(askPvs, marginBidPrice) + + iocOrders = append(iocOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeBuy, + Price: sumPv.Price, + Quantity: sumPv.Volume, + TimeInForce: types.TimeInForceIOC, + }) + + } else if makerBid.Price.Compare(marginAskPrice) >= 0 { + askPvs := s.makerBook.SideBook(types.SideTypeSell) + sumPv := aggregatePriceVolumeSliceWithPriceFilter(askPvs, marginBidPrice) + + // send ioc order for arbitrage + iocOrders = append(iocOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeSell, + Price: sumPv.Price, + Quantity: sumPv.Volume, + TimeInForce: types.TimeInForceIOC, + }) + } + + if len(iocOrders) == 0 { + return false, nil + } + + // send ioc order for arbitrage + formattedOrders, err := s.makerSession.FormatOrders(iocOrders) + if err != nil { + return false, err + } + + defer s.tradeCollector.Process() + + createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, s.makerOrderCreateCallback, formattedOrders...) + if err != nil { + return false, err + } + + s.logger.Infof("sent arbitrage orders: %+v", createdOrders) + return true, nil + } + + return false, nil +} + func (s *Strategy) adjustHedgeQuantityWithAvailableBalance( account *types.Account, side types.SideType, quantity, lastPrice fixedpoint.Value, ) fixedpoint.Value { From b4f2748892b52b199badb01e3780051ec984e784 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 18:03:03 +0800 Subject: [PATCH 2/6] xmaker: fix sides --- pkg/strategy/xmaker/strategy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 918147d57..abbc146c0 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -937,7 +937,7 @@ func aggregatePriceVolumeSliceWithPriceFilter(pvs types.PriceVolumeSlice, filter // tryArbitrage tries to arbitrage between the source and maker exchange func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote) (bool, error) { marginBidPrice := quote.BestBidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) - marginAskPrice := quote.BestAskPrice.Mul(fixedpoint.One.Add(quote.BidMargin)) + marginAskPrice := quote.BestAskPrice.Mul(fixedpoint.One.Add(quote.AskMargin)) var iocOrders []types.SubmitOrder if makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk(); ok { @@ -955,8 +955,8 @@ func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote) (bool, error) }) } else if makerBid.Price.Compare(marginAskPrice) >= 0 { - askPvs := s.makerBook.SideBook(types.SideTypeSell) - sumPv := aggregatePriceVolumeSliceWithPriceFilter(askPvs, marginBidPrice) + bidPvs := s.makerBook.SideBook(types.SideTypeBuy) + sumPv := aggregatePriceVolumeSliceWithPriceFilter(bidPvs, marginBidPrice) // send ioc order for arbitrage iocOrders = append(iocOrders, types.SubmitOrder{ From 52925c5643ddaf5757276163b5fd16295d96f131 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 18:12:46 +0800 Subject: [PATCH 3/6] xmaker: calculate balance for arbitrage --- pkg/strategy/xmaker/strategy.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index abbc146c0..c7a55ce97 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -774,7 +774,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64()) if s.EnableArbitrage { - done, err := s.tryArbitrage(ctx, quote) + done, err := s.tryArbitrage(ctx, quote, makerBalances) if err != nil { s.logger.WithError(err).Errorf("unable to arbitrage") } else if done { @@ -935,28 +935,32 @@ func aggregatePriceVolumeSliceWithPriceFilter(pvs types.PriceVolumeSlice, filter } // tryArbitrage tries to arbitrage between the source and maker exchange -func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote) (bool, error) { +func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote, balances types.BalanceMap) (bool, error) { marginBidPrice := quote.BestBidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) marginAskPrice := quote.BestAskPrice.Mul(fixedpoint.One.Add(quote.AskMargin)) + quoteBalance, hasQuote := balances[s.makerMarket.QuoteCurrency] + baseBalance, hasBase := balances[s.makerMarket.BaseCurrency] + var iocOrders []types.SubmitOrder if makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk(); ok { - if makerAsk.Price.Compare(marginBidPrice) <= 0 { + if hasQuote && makerAsk.Price.Compare(marginBidPrice) <= 0 { askPvs := s.makerBook.SideBook(types.SideTypeSell) sumPv := aggregatePriceVolumeSliceWithPriceFilter(askPvs, marginBidPrice) - + qty := fixedpoint.Min(quoteBalance.Available.Div(sumPv.Price), sumPv.Volume) iocOrders = append(iocOrders, types.SubmitOrder{ Symbol: s.Symbol, Type: types.OrderTypeLimit, Side: types.SideTypeBuy, Price: sumPv.Price, - Quantity: sumPv.Volume, + Quantity: qty, TimeInForce: types.TimeInForceIOC, }) - } else if makerBid.Price.Compare(marginAskPrice) >= 0 { + } else if hasBase && makerBid.Price.Compare(marginAskPrice) >= 0 { bidPvs := s.makerBook.SideBook(types.SideTypeBuy) sumPv := aggregatePriceVolumeSliceWithPriceFilter(bidPvs, marginBidPrice) + qty := fixedpoint.Min(baseBalance.Available, sumPv.Volume) // send ioc order for arbitrage iocOrders = append(iocOrders, types.SubmitOrder{ @@ -964,7 +968,7 @@ func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote) (bool, error) Type: types.OrderTypeLimit, Side: types.SideTypeSell, Price: sumPv.Price, - Quantity: sumPv.Volume, + Quantity: qty, TimeInForce: types.TimeInForceIOC, }) } From 34ef50d88911cae80050e7a08c0ce4bdb0a44e11 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 22:03:06 +0800 Subject: [PATCH 4/6] xmaker: refactor and clean up tryArbitrage --- pkg/strategy/xmaker/strategy.go | 124 +++++++++++++++++++------------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index c7a55ce97..936f07735 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -774,7 +774,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error { askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64()) if s.EnableArbitrage { - done, err := s.tryArbitrage(ctx, quote, makerBalances) + done, err := s.tryArbitrage(ctx, quote, makerBalances, hedgeBalances) if err != nil { s.logger.WithError(err).Errorf("unable to arbitrage") } else if done { @@ -935,66 +935,94 @@ func aggregatePriceVolumeSliceWithPriceFilter(pvs types.PriceVolumeSlice, filter } // tryArbitrage tries to arbitrage between the source and maker exchange -func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote, balances types.BalanceMap) (bool, error) { +func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote, makerBalances, hedgeBalances types.BalanceMap) (bool, error) { marginBidPrice := quote.BestBidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) marginAskPrice := quote.BestAskPrice.Mul(fixedpoint.One.Add(quote.AskMargin)) - quoteBalance, hasQuote := balances[s.makerMarket.QuoteCurrency] - baseBalance, hasBase := balances[s.makerMarket.BaseCurrency] + makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk() + if !ok { + return false, nil + } var iocOrders []types.SubmitOrder - if makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk(); ok { - if hasQuote && makerAsk.Price.Compare(marginBidPrice) <= 0 { - askPvs := s.makerBook.SideBook(types.SideTypeSell) - sumPv := aggregatePriceVolumeSliceWithPriceFilter(askPvs, marginBidPrice) - qty := fixedpoint.Min(quoteBalance.Available.Div(sumPv.Price), sumPv.Volume) - iocOrders = append(iocOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimit, - Side: types.SideTypeBuy, - Price: sumPv.Price, - Quantity: qty, - TimeInForce: types.TimeInForceIOC, - }) - - } else if hasBase && makerBid.Price.Compare(marginAskPrice) >= 0 { - bidPvs := s.makerBook.SideBook(types.SideTypeBuy) - sumPv := aggregatePriceVolumeSliceWithPriceFilter(bidPvs, marginBidPrice) - qty := fixedpoint.Min(baseBalance.Available, sumPv.Volume) - - // send ioc order for arbitrage - iocOrders = append(iocOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimit, - Side: types.SideTypeSell, - Price: sumPv.Price, - Quantity: qty, - TimeInForce: types.TimeInForceIOC, - }) + if makerAsk.Price.Compare(marginBidPrice) <= 0 { + quoteBalance, hasQuote := makerBalances[s.makerMarket.QuoteCurrency] + if !hasQuote { + return false, nil } - if len(iocOrders) == 0 { + askPvs := s.makerBook.SideBook(types.SideTypeSell) + sumPv := aggregatePriceVolumeSliceWithPriceFilter(askPvs, marginBidPrice) + qty := fixedpoint.Min(quoteBalance.Available.Div(sumPv.Price), sumPv.Volume) + + if sourceBase, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok { + qty = fixedpoint.Min(qty, sourceBase.Available) + } else { + // insufficient hedge base balance for arbitrage + return false, nil + } + + iocOrders = append(iocOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeBuy, + Price: sumPv.Price, + Quantity: qty, + TimeInForce: types.TimeInForceIOC, + }) + + } else if makerBid.Price.Compare(marginAskPrice) >= 0 { + baseBalance, hasBase := makerBalances[s.makerMarket.BaseCurrency] + if !hasBase { + return false, nil + } + + bidPvs := s.makerBook.SideBook(types.SideTypeBuy) + sumPv := aggregatePriceVolumeSliceWithPriceFilter(bidPvs, marginAskPrice) + qty := fixedpoint.Min(baseBalance.Available, sumPv.Volume) + + if sourceQuote, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok { + qty = fixedpoint.Min(qty, quote.BestAskPrice.Div(sourceQuote.Available)) + } else { + // insufficient hedge quote balance for arbitrage return false, nil } // send ioc order for arbitrage - formattedOrders, err := s.makerSession.FormatOrders(iocOrders) - if err != nil { - return false, err - } - - defer s.tradeCollector.Process() - - createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, s.makerSession.Exchange, s.makerOrderCreateCallback, formattedOrders...) - if err != nil { - return false, err - } - - s.logger.Infof("sent arbitrage orders: %+v", createdOrders) - return true, nil + iocOrders = append(iocOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeSell, + Price: sumPv.Price, + Quantity: qty, + TimeInForce: types.TimeInForceIOC, + }) } - return false, nil + if len(iocOrders) == 0 { + return false, nil + } + + // send ioc order for arbitrage + formattedOrders, err := s.makerSession.FormatOrders(iocOrders) + if err != nil { + return false, err + } + + defer s.tradeCollector.Process() + + createdOrders, _, err := bbgo.BatchPlaceOrder( + ctx, + s.makerSession.Exchange, + s.makerOrderCreateCallback, + formattedOrders...) + + if err != nil { + return len(createdOrders) > 0, err + } + + s.logger.Infof("sent arbitrage IOC order: %+v", createdOrders) + return true, nil } func (s *Strategy) adjustHedgeQuantityWithAvailableBalance( From 83ed9b0811bd4cd70844d575bf29ccd7a5ea00ff Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 22:23:02 +0800 Subject: [PATCH 5/6] go: upgrade go sqlite --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 1bc3d35d0..e365c91c2 100644 --- a/go.mod +++ b/go.mod @@ -119,7 +119,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mattn/go-sqlite3 v1.14.23 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect From bd19b63c7b5ed83d1736e7023b440ed1f334ccc5 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 9 Sep 2024 22:23:25 +0800 Subject: [PATCH 6/6] go: update sum file --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 89be1351f..cdcbdfa3b 100644 --- a/go.sum +++ b/go.sum @@ -474,6 +474,8 @@ github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGw github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=