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 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= diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 5c8210784..936f07735 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, makerBalances, hedgeBalances) + 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,120 @@ 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, makerBalances, hedgeBalances types.BalanceMap) (bool, error) { + marginBidPrice := quote.BestBidPrice.Mul(fixedpoint.One.Sub(quote.BidMargin)) + marginAskPrice := quote.BestAskPrice.Mul(fixedpoint.One.Add(quote.AskMargin)) + + makerBid, makerAsk, ok := s.makerBook.BestBidAndAsk() + if !ok { + return false, nil + } + + var iocOrders []types.SubmitOrder + if makerAsk.Price.Compare(marginBidPrice) <= 0 { + quoteBalance, hasQuote := makerBalances[s.makerMarket.QuoteCurrency] + if !hasQuote { + return false, nil + } + + 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 + iocOrders = append(iocOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeSell, + Price: sumPv.Price, + Quantity: qty, + 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 len(createdOrders) > 0, err + } + + s.logger.Infof("sent arbitrage IOC order: %+v", createdOrders) + return true, nil +} + func (s *Strategy) adjustHedgeQuantityWithAvailableBalance( account *types.Account, side types.SideType, quantity, lastPrice fixedpoint.Value, ) fixedpoint.Value {