Merge pull request #1737 from c9s/c9s/xmaker/ioc-arb
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run

FEATURE: [xmaker] implement tryArbitrage
This commit is contained in:
c9s 2024-09-09 22:57:20 +08:00 committed by GitHub
commit d7ddc9c462
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 129 additions and 27 deletions

2
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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 {