Skip to content

Commit

Permalink
client/mm: Fix fee handling
Browse files Browse the repository at this point in the history
  • Loading branch information
martonp committed Jan 3, 2024
1 parent 50e17de commit 156ea4d
Show file tree
Hide file tree
Showing 7 changed files with 734 additions and 355 deletions.
2 changes: 1 addition & 1 deletion client/mm/exchange_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type orderFees struct {
// a *core.WalletBalance.
type botCoreAdaptor interface {
SyncBook(host string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error)
SingleLotFees(form *core.SingleLotFeesForm) (uint64, uint64, uint64, error)
SingleLotFees(form *core.SingleLotFeesForm) (swapFees, redeemFees, refundFees uint64, err error)
Cancel(oidB dex.Bytes) error
MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error)
MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error)
Expand Down
159 changes: 129 additions & 30 deletions client/mm/mm_arb_market_maker.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ type arbMarketMaker struct {

matchesMtx sync.RWMutex
matchesSeen map[order.MatchID]bool
pendingOrders map[order.OrderID]bool
pendingOrders map[order.OrderID]uint64 // orderID -> rate for counter trade on cex

cexTradesMtx sync.RWMutex
cexTrades map[string]uint64
Expand All @@ -171,21 +171,12 @@ func (a *arbMarketMaker) handleCEXTradeUpdate(update *libxc.Trade) {
}
}

// makeCounterTrade sends a trade to the CEX that is the counter trade to the
// specified match. sell indicates that the bot is selling on the DEX.
func (a *arbMarketMaker) makeCounterTrade(match *core.Match, sell bool) {
var cexRate uint64
if sell {
cexRate = uint64(float64(match.Rate) / (1 + a.cfg.Profit))
} else {
cexRate = uint64(float64(match.Rate) * (1 + a.cfg.Profit))
}
cexRate = steppedRate(cexRate, a.mkt.RateStep)

// tradeOnCEX executes a trade on the CEX.
func (a *arbMarketMaker) tradeOnCEX(rate, qty uint64, sell bool) {
a.cexTradesMtx.Lock()
defer a.cexTradesMtx.Unlock()

cexTrade, err := a.cex.Trade(a.ctx, a.baseID, a.quoteID, !sell, cexRate, match.Qty)
cexTrade, err := a.cex.Trade(a.ctx, a.baseID, a.quoteID, sell, rate, qty)
if err != nil {
a.log.Errorf("Error sending trade to CEX: %v", err)
return
Expand All @@ -204,7 +195,8 @@ func (a *arbMarketMaker) processDEXOrderUpdate(o *core.Order) {
a.matchesMtx.Lock()
defer a.matchesMtx.Unlock()

if _, found := a.pendingOrders[orderID]; !found {
cexRate, found := a.pendingOrders[orderID]
if !found {
return
}

Expand All @@ -214,7 +206,7 @@ func (a *arbMarketMaker) processDEXOrderUpdate(o *core.Order) {

if !a.matchesSeen[matchID] {
a.matchesSeen[matchID] = true
a.makeCounterTrade(match, o.Sell)
a.tradeOnCEX(cexRate, match.Qty, !o.Sell)
}
}

Expand All @@ -228,12 +220,12 @@ func (a *arbMarketMaker) processDEXOrderUpdate(o *core.Order) {
}
}

func (a *arbMarketMaker) placeMultiTrade(placements []*rateLots, sell bool) {
func (a *arbMarketMaker) placeMultiTrade(placements []*arbMMPlacement, sell bool) {
multiTradePlacements := make([]*multiTradePlacement, 0, len(placements))
for _, p := range placements {
multiTradePlacements = append(multiTradePlacements, &multiTradePlacement{
qty: p.lots * a.mkt.LotSize,
rate: p.rate,
rate: p.dexRate,
grouping: uint64(p.placementIndex),
})
}
Expand Down Expand Up @@ -261,10 +253,10 @@ func (a *arbMarketMaker) placeMultiTrade(placements []*rateLots, sell bool) {
a.matchesMtx.Lock()
defer a.matchesMtx.Unlock()

for _, o := range orders {
for i, o := range orders {
var orderID order.OrderID
copy(orderID[:], o.ID)
a.pendingOrders[orderID] = true
a.pendingOrders[orderID] = placements[i].cexRate
}
}

Expand All @@ -288,9 +280,116 @@ func (a *arbMarketMaker) cancelExpiredCEXTrades() {
}
}

func dexPlacementRate(cexRate uint64, sell bool, profitRate float64, mkt *core.Market, buyFees, sellFees *orderFees, fiatRate func(uint32) float64) (uint64, error) {
var profitableRate uint64
if sell {
profitableRate = uint64(float64(cexRate) * (1 + profitRate))
} else {
profitableRate = uint64(float64(cexRate) / (1 + profitRate))
}

baseUnitInfo, err := asset.UnitInfo(mkt.BaseID)
if err != nil {
return 0, err
}
quoteUnitInfo, err := asset.UnitInfo(mkt.QuoteID)
if err != nil {
return 0, err
}

var baseFee, quoteFee uint64
if sell {
baseFee += sellFees.swap
quoteFee += sellFees.redemption
} else {
quoteFee += buyFees.swap
baseFee += buyFees.redemption
}

baseToken := asset.TokenInfo(mkt.BaseID)
quoteToken := asset.TokenInfo(mkt.QuoteID)
var baseFeeInQuoteUnits, quoteFeeInQuoteUnits uint64

if quoteToken != nil {
quoteFiatRate := fiatRate(mkt.QuoteID)
if quoteFiatRate == 0 {
return 0, fmt.Errorf("no fiat rate for quote asset %d", mkt.QuoteID)
}
quoteFeeFiatRate := fiatRate(quoteToken.ParentID)
if quoteFeeFiatRate == 0 {
return 0, fmt.Errorf("no fiat rate for quote parent asset %d", quoteToken.ParentID)
}
quoteParentUnitInfo, err := asset.UnitInfo(quoteToken.ParentID)
if err != nil {
return 0, err
}

quoteFeeConv := float64(quoteFee) / float64(quoteParentUnitInfo.Conventional.ConversionFactor)
quoteConv := quoteFeeConv * quoteFeeFiatRate / quoteFiatRate
quoteFeeInQuoteUnits = uint64(quoteConv * float64(quoteUnitInfo.Conventional.ConversionFactor))
} else {
quoteFeeInQuoteUnits = quoteFee
}

if baseToken != nil {
quoteFiatRate := fiatRate(mkt.QuoteID)
if quoteFiatRate == 0 {
return 0, fmt.Errorf("no fiat rate for quote asset %d", mkt.QuoteID)
}
baseFeeFiatRate := fiatRate(baseToken.ParentID)
if baseFeeFiatRate == 0 {
return 0, fmt.Errorf("no fiat rate for base parent asset %d", baseToken.ParentID)
}
baseParentUnitInfo, err := asset.UnitInfo(baseToken.ParentID)
if err != nil {
return 0, err
}
baseFeeConv := float64(quoteFee) / float64(baseParentUnitInfo.Conventional.ConversionFactor)
quoteConv := baseFeeConv * baseFeeFiatRate / quoteFiatRate
baseFeeInQuoteUnits = uint64(quoteConv * float64(quoteUnitInfo.Conventional.ConversionFactor))
} else {
baseFeeInQuoteUnits = calc.BaseToQuote(profitableRate, baseFee)
}

totalFeesInQuoteUnits := baseFeeInQuoteUnits + quoteFeeInQuoteUnits
totalFeesInConvQuoteUnits := float64(totalFeesInQuoteUnits) / float64(quoteUnitInfo.Conventional.ConversionFactor)
convLotSize := float64(mkt.LotSize) / float64(baseUnitInfo.Conventional.ConversionFactor)

rateAdjustment := totalFeesInConvQuoteUnits / convLotSize
originalConvRate := calc.ConventionalRate(profitableRate, baseUnitInfo, quoteUnitInfo)

if sell {
adjustedRate := calc.MessageRate(originalConvRate+rateAdjustment, baseUnitInfo, quoteUnitInfo)
return steppedRate(adjustedRate, mkt.RateStep), nil
}

if rateAdjustment > originalConvRate {
return 0, fmt.Errorf("rate adjustment required for fees %v > rate %v", rateAdjustment, originalConvRate)
}

adjustedRate := calc.MessageRate(originalConvRate-rateAdjustment, baseUnitInfo, quoteUnitInfo)
return steppedRate(adjustedRate, mkt.RateStep), nil
}

func (a *arbMarketMaker) dexPlacementRate(cexRate uint64, sell bool) (uint64, error) {
buyFees, sellFees, err := a.core.OrderFees()
if err != nil {
return 0, err
}

return dexPlacementRate(cexRate, sell, a.cfg.Profit, a.mkt, buyFees, sellFees, a.core.FiatRate)
}

type arbMMPlacement struct {
dexRate uint64
cexRate uint64
lots uint64
placementIndex int
}

// ordersToPlace is called once per epoch, and determines what buy, sell. and
// cancel orders need to be placed.
func (a *arbMarketMaker) ordersToPlace() (cancels []dex.Bytes, buyOrders, sellOrders []*rateLots) {
func (a *arbMarketMaker) ordersToPlace() (cancels []dex.Bytes, buyOrders, sellOrders []*arbMMPlacement) {
newEpoch := a.currEpoch.Load()

existingBuys, existingSells := a.core.GroupedBookedOrders()
Expand Down Expand Up @@ -341,7 +440,7 @@ func (a *arbMarketMaker) ordersToPlace() (cancels []dex.Bytes, buyOrders, sellOr
return
}

processSide := func(sell bool) []*rateLots {
processSide := func(sell bool) []*arbMMPlacement {
var cfgPlacements []*ArbMarketMakingPlacement
var existingOrders map[uint64][]*core.Order
var remainingDEXBalance, remainingCEXBalance, fundingFees uint64
Expand Down Expand Up @@ -412,7 +511,7 @@ func (a *arbMarketMaker) ordersToPlace() (cancels []dex.Bytes, buyOrders, sellOr
// on the books are outside of the drift tolerance, they will be
// cancelled, and if there are less than the required lots on the DEX
// books, new orders will be added.
placements := make([]*rateLots, 0, len(cfgPlacements))
placements := make([]*arbMMPlacement, 0, len(cfgPlacements))
var cumulativeCEXDepth uint64
for i, cfgPlacement := range cfgPlacements {
cumulativeCEXDepth += uint64(float64(cfgPlacement.Lots*a.mkt.LotSize) * cfgPlacement.Multiplier)
Expand All @@ -426,11 +525,10 @@ func (a *arbMarketMaker) ordersToPlace() (cancels []dex.Bytes, buyOrders, sellOr
break
}

var placementRate uint64
if sell {
placementRate = steppedRate(uint64(float64(extrema)*(1+a.cfg.Profit)), a.mkt.RateStep)
} else {
placementRate = steppedRate(uint64(float64(extrema)/(1+a.cfg.Profit)), a.mkt.RateStep)
placementRate, err := a.dexPlacementRate(extrema, sell)
if err != nil {
a.log.Errorf("Error calculating dex placement rate: %v", err)
break
}

ordersForPlacement := existingOrders[uint64(i)]
Expand Down Expand Up @@ -469,8 +567,9 @@ func (a *arbMarketMaker) ordersToPlace() (cancels []dex.Bytes, buyOrders, sellOr
remainingDEXBalance -= requiredOnDEX
remainingCEXBalance -= requiredOnCEX

placements = append(placements, &rateLots{
rate: placementRate,
placements = append(placements, &arbMMPlacement{
dexRate: placementRate,
cexRate: extrema,
lots: lotsToPlace,
placementIndex: i,
})
Expand Down Expand Up @@ -847,7 +946,7 @@ func RunArbMarketMaker(ctx context.Context, cfg *BotConfig, c botCoreAdaptor, ce
cfg: cfg.ArbMarketMakerConfig,
mkt: mkt,
matchesSeen: make(map[order.MatchID]bool),
pendingOrders: make(map[order.OrderID]bool),
pendingOrders: make(map[order.OrderID]uint64),
cexTrades: make(map[string]uint64),
}).run()
}
Loading

0 comments on commit 156ea4d

Please sign in to comment.