decred.org/dcrdex@v1.0.3/client/mm/mm_simple_arb.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package mm
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"math"
    11  	"sort"
    12  	"sync"
    13  	"sync/atomic"
    14  
    15  	"decred.org/dcrdex/client/core"
    16  	"decred.org/dcrdex/client/mm/libxc"
    17  	"decred.org/dcrdex/dex"
    18  	"decred.org/dcrdex/dex/calc"
    19  	"decred.org/dcrdex/dex/order"
    20  )
    21  
    22  // SimpleArbConfig is the configuration for an arbitrage bot that only places
    23  // orders when there is a profitable arbitrage opportunity.
    24  type SimpleArbConfig struct {
    25  	// ProfitTrigger is the minimum profit before a cross-exchange trade
    26  	// sequence is initiated. Range: 0 < ProfitTrigger << 1. For example, if
    27  	// the ProfitTrigger is 0.01 and a trade sequence would produce a 1% profit
    28  	// or better, a trade sequence will be initiated.
    29  	ProfitTrigger float64 `json:"profitTrigger"`
    30  	// MaxActiveArbs sets a limit on the number of active arbitrage sequences
    31  	// that can be open simultaneously.
    32  	MaxActiveArbs uint32 `json:"maxActiveArbs"`
    33  	// NumEpochsLeaveOpen is the number of epochs an arbitrage sequence will
    34  	// stay open if one or both of the orders were not filled.
    35  	NumEpochsLeaveOpen uint32 `json:"numEpochsLeaveOpen"`
    36  }
    37  
    38  func (c *SimpleArbConfig) Validate() error {
    39  	if c.ProfitTrigger <= 0 || c.ProfitTrigger > 1 {
    40  		return fmt.Errorf("profit trigger must be 0 < t <= 1, but got %v", c.ProfitTrigger)
    41  	}
    42  
    43  	if c.MaxActiveArbs == 0 {
    44  		return fmt.Errorf("must allow at least 1 active arb")
    45  	}
    46  
    47  	if c.NumEpochsLeaveOpen < 2 {
    48  		return fmt.Errorf("arbs must be left open for at least 2 epochs")
    49  	}
    50  
    51  	return nil
    52  }
    53  
    54  // arbSequence represents an attempted arbitrage sequence.
    55  type arbSequence struct {
    56  	dexOrder       *core.Order
    57  	cexOrderID     string
    58  	dexRate        uint64
    59  	cexRate        uint64
    60  	cexOrderFilled bool
    61  	dexOrderFilled bool
    62  	sellOnDEX      bool
    63  	startEpoch     uint64
    64  }
    65  
    66  type simpleArbMarketMaker struct {
    67  	*unifiedExchangeAdaptor
    68  	cex              botCexAdaptor
    69  	core             botCoreAdaptor
    70  	cfgV             atomic.Value // *SimpleArbConfig
    71  	book             dexOrderBook
    72  	rebalanceRunning atomic.Bool
    73  
    74  	activeArbsMtx sync.RWMutex
    75  	activeArbs    []*arbSequence
    76  }
    77  
    78  var _ bot = (*simpleArbMarketMaker)(nil)
    79  
    80  func (a *simpleArbMarketMaker) cfg() *SimpleArbConfig {
    81  	return a.cfgV.Load().(*SimpleArbConfig)
    82  }
    83  
    84  // arbExists checks if an arbitrage opportunity exists.
    85  func (a *simpleArbMarketMaker) arbExists() (exists, sellOnDex bool, lotsToArb, dexRate, cexRate uint64, err error) {
    86  	sellOnDex = false
    87  	exists, lotsToArb, dexRate, cexRate, err = a.arbExistsOnSide(sellOnDex)
    88  	if err != nil || exists {
    89  		return
    90  	}
    91  
    92  	sellOnDex = true
    93  	exists, lotsToArb, dexRate, cexRate, err = a.arbExistsOnSide(sellOnDex)
    94  	if err != nil || exists {
    95  		return
    96  	}
    97  
    98  	return
    99  }
   100  
   101  // arbExistsOnSide checks if an arbitrage opportunity exists either when
   102  // buying or selling on the dex.
   103  func (a *simpleArbMarketMaker) arbExistsOnSide(sellOnDEX bool) (exists bool, lotsToArb, dexRate, cexRate uint64, err error) {
   104  	lotSize := a.lotSize
   105  	var prevProfit uint64
   106  
   107  	for numLots := uint64(1); ; numLots++ {
   108  		dexAvg, dexExtrema, dexFilled, err := a.book.VWAP(numLots, a.lotSize, !sellOnDEX)
   109  		if err != nil {
   110  			return false, 0, 0, 0, fmt.Errorf("error calculating dex VWAP: %w", err)
   111  		}
   112  		cexAvg, cexExtrema, cexFilled, err := a.CEX.VWAP(a.baseID, a.quoteID, sellOnDEX, numLots*lotSize)
   113  		if err != nil {
   114  			return false, 0, 0, 0, fmt.Errorf("error calculating cex VWAP: %w", err)
   115  		}
   116  
   117  		if !dexFilled || !cexFilled {
   118  			break
   119  		}
   120  
   121  		var buyRate, sellRate, buyAvg, sellAvg uint64
   122  		if sellOnDEX {
   123  			buyRate = cexExtrema
   124  			sellRate = dexExtrema
   125  			buyAvg = cexAvg
   126  			sellAvg = dexAvg
   127  		} else {
   128  			buyRate = dexExtrema
   129  			sellRate = cexExtrema
   130  			buyAvg = dexAvg
   131  			sellAvg = cexAvg
   132  		}
   133  
   134  		// For 1 lots, check balances in order to add insufficient balances to BotProblems
   135  		if buyRate >= sellRate && numLots > 1 {
   136  			break
   137  		}
   138  
   139  		dexSufficient, err := a.core.SufficientBalanceForDEXTrade(dexExtrema, numLots*lotSize, sellOnDEX)
   140  		if err != nil {
   141  			return false, 0, 0, 0, fmt.Errorf("error checking dex balance: %w", err)
   142  		}
   143  
   144  		cexSufficient := a.cex.SufficientBalanceForCEXTrade(a.baseID, a.quoteID, !sellOnDEX, cexExtrema, numLots*lotSize)
   145  		if !dexSufficient || !cexSufficient {
   146  			if numLots == 1 {
   147  				return false, 0, 0, 0, nil
   148  			} else {
   149  				break
   150  			}
   151  		}
   152  
   153  		if buyRate >= sellRate /* && numLots == 1 */ {
   154  			break
   155  		}
   156  
   157  		feesInQuoteUnits, err := a.core.OrderFeesInUnits(sellOnDEX, false, dexAvg)
   158  		if err != nil {
   159  			return false, 0, 0, 0, fmt.Errorf("error getting fees: %w", err)
   160  		}
   161  
   162  		qty := numLots * lotSize
   163  		quoteForBuy := calc.BaseToQuote(buyAvg, qty)
   164  		quoteFromSell := calc.BaseToQuote(sellAvg, qty)
   165  		if quoteFromSell-quoteForBuy <= feesInQuoteUnits {
   166  			break
   167  		}
   168  		profitInQuote := quoteFromSell - quoteForBuy - feesInQuoteUnits
   169  		profitInBase := calc.QuoteToBase((buyRate+sellRate)/2, profitInQuote)
   170  		if profitInBase < prevProfit || float64(profitInBase)/float64(qty) < a.cfg().ProfitTrigger {
   171  			break
   172  		}
   173  
   174  		prevProfit = profitInBase
   175  		lotsToArb = numLots
   176  		dexRate = dexExtrema
   177  		cexRate = cexExtrema
   178  	}
   179  
   180  	if lotsToArb > 0 {
   181  		a.log.Infof("arb opportunity - sellOnDex: %t, lotsToArb: %d, dexRate: %s, cexRate: %s: profit: %s",
   182  			sellOnDEX, lotsToArb, a.fmtRate(dexRate), a.fmtRate(cexRate), a.fmtBase(prevProfit))
   183  		return true, lotsToArb, dexRate, cexRate, nil
   184  	}
   185  
   186  	return false, 0, 0, 0, nil
   187  }
   188  
   189  // executeArb will execute an arbitrage sequence by placing orders on the dex
   190  // and cex. An entry will be added to the a.activeArbs slice if both orders
   191  // are successfully placed.
   192  func (a *simpleArbMarketMaker) executeArb(sellOnDex bool, lotsToArb, dexRate, cexRate, epoch uint64) {
   193  	a.log.Debugf("executing arb opportunity - sellOnDex: %v, lotsToArb: %v, dexRate: %v, cexRate: %v",
   194  		sellOnDex, lotsToArb, a.fmtRate(dexRate), a.fmtRate(cexRate))
   195  
   196  	a.activeArbsMtx.RLock()
   197  	numArbs := len(a.activeArbs)
   198  	a.activeArbsMtx.RUnlock()
   199  	if numArbs >= int(a.cfg().MaxActiveArbs) {
   200  		a.log.Info("cannot execute arb because already at max arbs")
   201  		return
   202  	}
   203  
   204  	if a.selfMatch(sellOnDex, dexRate) {
   205  		a.log.Info("cannot execute arb opportunity due to self-match")
   206  		return
   207  	}
   208  	// also check self-match on CEX?
   209  
   210  	// Hold the lock for this entire process because updates to the cex trade
   211  	// may come even before the Trade function has returned, and in order to
   212  	// be able to process them, the new arbSequence struct must already be in
   213  	// the activeArbs slice.
   214  	a.activeArbsMtx.Lock()
   215  	defer a.activeArbsMtx.Unlock()
   216  
   217  	// Place cex order first. If placing dex order fails then can freely cancel cex order.
   218  	cexTrade, err := a.cex.CEXTrade(a.ctx, a.baseID, a.quoteID, !sellOnDex, cexRate, lotsToArb*a.lotSize)
   219  	if err != nil {
   220  		a.log.Errorf("error placing cex order: %v", err)
   221  		return
   222  	}
   223  
   224  	dexOrder, err := a.core.DEXTrade(dexRate, lotsToArb*a.lotSize, sellOnDex)
   225  	if err != nil {
   226  		if err != nil {
   227  			a.log.Errorf("error placing dex order: %v", err)
   228  		}
   229  
   230  		err := a.cex.CancelTrade(a.ctx, a.baseID, a.quoteID, cexTrade.ID)
   231  		if err != nil {
   232  			a.log.Errorf("error canceling cex order: %v", err)
   233  			// TODO: keep retrying failed cancel
   234  		}
   235  		return
   236  	}
   237  
   238  	a.activeArbs = append(a.activeArbs, &arbSequence{
   239  		dexOrder:   dexOrder,
   240  		dexRate:    dexRate,
   241  		cexOrderID: cexTrade.ID,
   242  		cexRate:    cexRate,
   243  		sellOnDEX:  sellOnDex,
   244  		startEpoch: epoch,
   245  	})
   246  }
   247  
   248  func (a *simpleArbMarketMaker) sortedOrders() (buys, sells []*core.Order) {
   249  	buys, sells = make([]*core.Order, 0), make([]*core.Order, 0)
   250  
   251  	a.activeArbsMtx.RLock()
   252  	for _, arb := range a.activeArbs {
   253  		if arb.sellOnDEX {
   254  			sells = append(sells, arb.dexOrder)
   255  		} else {
   256  			buys = append(buys, arb.dexOrder)
   257  		}
   258  	}
   259  	a.activeArbsMtx.RUnlock()
   260  
   261  	sort.Slice(buys, func(i, j int) bool { return buys[i].Rate > buys[j].Rate })
   262  	sort.Slice(sells, func(i, j int) bool { return sells[i].Rate < sells[j].Rate })
   263  
   264  	return buys, sells
   265  }
   266  
   267  // selfMatch checks if a order could match with any other orders
   268  // already placed on the dex.
   269  func (a *simpleArbMarketMaker) selfMatch(sell bool, rate uint64) bool {
   270  	buys, sells := a.sortedOrders()
   271  
   272  	if sell && len(buys) > 0 && buys[0].Rate >= rate {
   273  		return true
   274  	}
   275  
   276  	if !sell && len(sells) > 0 && sells[0].Rate <= rate {
   277  		return true
   278  	}
   279  
   280  	return false
   281  }
   282  
   283  // cancelArbSequence will cancel both the dex and cex orders in an arb sequence
   284  // if they have not yet been filled.
   285  func (a *simpleArbMarketMaker) cancelArbSequence(arb *arbSequence) {
   286  	if !arb.cexOrderFilled {
   287  		err := a.cex.CancelTrade(a.ctx, a.baseID, a.quoteID, arb.cexOrderID)
   288  		if err != nil {
   289  			a.log.Errorf("failed to cancel cex trade ID %s: %v", arb.cexOrderID, err)
   290  		}
   291  	}
   292  
   293  	if !arb.dexOrderFilled {
   294  		err := a.core.Cancel(arb.dexOrder.ID)
   295  		if err != nil {
   296  			a.log.Errorf("failed to cancel dex order ID %s: %v", arb.dexOrder.ID, err)
   297  		}
   298  	}
   299  
   300  	// keep retrying if failed to cancel?
   301  }
   302  
   303  // removeActiveArb removes the active arb at index i.
   304  //
   305  // activeArbsMtx MUST be held when calling this function.
   306  func (a *simpleArbMarketMaker) removeActiveArb(i int) {
   307  	a.activeArbs[i] = a.activeArbs[len(a.activeArbs)-1]
   308  	a.activeArbs = a.activeArbs[:len(a.activeArbs)-1]
   309  }
   310  
   311  // handleCEXTradeUpdate is called when the CEX sends a notification that the
   312  // status of a trade has changed.
   313  func (a *simpleArbMarketMaker) handleCEXTradeUpdate(update *libxc.Trade) {
   314  	if !update.Complete {
   315  		return
   316  	}
   317  
   318  	a.activeArbsMtx.Lock()
   319  	defer a.activeArbsMtx.Unlock()
   320  
   321  	for i, arb := range a.activeArbs {
   322  		if arb.cexOrderID == update.ID {
   323  			arb.cexOrderFilled = true
   324  			if arb.dexOrderFilled {
   325  				a.removeActiveArb(i)
   326  			}
   327  			return
   328  		}
   329  	}
   330  }
   331  
   332  // handleDEXOrderUpdate is called when the DEX sends a notification that the
   333  // status of an order has changed.
   334  func (a *simpleArbMarketMaker) handleDEXOrderUpdate(o *core.Order) {
   335  	if o.Status <= order.OrderStatusBooked {
   336  		return
   337  	}
   338  
   339  	a.activeArbsMtx.Lock()
   340  	defer a.activeArbsMtx.Unlock()
   341  
   342  	for i, arb := range a.activeArbs {
   343  		if bytes.Equal(arb.dexOrder.ID, o.ID) {
   344  			arb.dexOrderFilled = true
   345  			if arb.cexOrderFilled {
   346  				a.removeActiveArb(i)
   347  			}
   348  			return
   349  		}
   350  	}
   351  }
   352  
   353  func (a *simpleArbMarketMaker) tryArb(newEpoch uint64) (exists, sellOnDEX bool, err error) {
   354  	if !(a.checkBotHealth(newEpoch) && a.tradingLimitNotReached(newEpoch)) {
   355  		return false, false, nil
   356  	}
   357  
   358  	exists, sellOnDex, lotsToArb, dexRate, cexRate, err := a.arbExists()
   359  	if err != nil {
   360  		return false, false, err
   361  	}
   362  	if a.log.Level() == dex.LevelTrace {
   363  		a.log.Tracef("%s rebalance. exists = %t, %s on dex, lots = %d, dex rate = %s, cex rate = %s",
   364  			a.name, exists, sellStr(sellOnDex), lotsToArb, a.fmtRate(dexRate), a.fmtRate(cexRate))
   365  	}
   366  	if exists {
   367  		// Execution will not happen if it would cause a self-match.
   368  		a.executeArb(sellOnDex, lotsToArb, dexRate, cexRate, newEpoch)
   369  	}
   370  
   371  	return exists, sellOnDex, nil
   372  }
   373  
   374  // rebalance checks if there is an arbitrage opportunity between the dex and cex,
   375  // and if so, executes trades to capitalize on it.
   376  func (a *simpleArbMarketMaker) rebalance(newEpoch uint64) {
   377  	if !a.rebalanceRunning.CompareAndSwap(false, true) {
   378  		return
   379  	}
   380  	defer a.rebalanceRunning.Store(false)
   381  	a.log.Tracef("rebalance: epoch %d", newEpoch)
   382  
   383  	actionTaken, err := a.tryTransfers(newEpoch)
   384  	if err != nil {
   385  		a.log.Errorf("Error performing transfers: %v", err)
   386  	} else if actionTaken {
   387  		return
   388  	}
   389  
   390  	epochReport := &EpochReport{EpochNum: newEpoch}
   391  
   392  	exists, sellOnDex, err := a.tryArb(newEpoch)
   393  	if err != nil {
   394  		epochReport.setPreOrderProblems(err)
   395  		a.unifiedExchangeAdaptor.updateEpochReport(epochReport)
   396  		return
   397  	}
   398  
   399  	a.unifiedExchangeAdaptor.updateEpochReport(epochReport)
   400  
   401  	a.activeArbsMtx.Lock()
   402  	remainingArbs := make([]*arbSequence, 0, len(a.activeArbs))
   403  	for _, arb := range a.activeArbs {
   404  		expired := newEpoch-arb.startEpoch > uint64(a.cfg().NumEpochsLeaveOpen)
   405  		oppositeDirectionArbFound := exists && sellOnDex != arb.sellOnDEX
   406  
   407  		if expired || oppositeDirectionArbFound {
   408  			a.cancelArbSequence(arb)
   409  		} else {
   410  			remainingArbs = append(remainingArbs, arb)
   411  		}
   412  	}
   413  	a.activeArbs = remainingArbs
   414  	a.activeArbsMtx.Unlock()
   415  
   416  	a.registerFeeGap()
   417  }
   418  
   419  func (a *simpleArbMarketMaker) distribution() (dist *distribution, err error) {
   420  	sellVWAP, buyVWAP, err := a.cexCounterRates(1, 1)
   421  	if err != nil {
   422  		return nil, fmt.Errorf("error getting cex counter-rates: %w", err)
   423  	}
   424  	// TODO: Adjust these rates to account for profit and fees.
   425  	sellFeesInBase, err := a.OrderFeesInUnits(true, true, sellVWAP)
   426  	if err != nil {
   427  		return nil, fmt.Errorf("error getting converted fees: %w", err)
   428  	}
   429  	adj := float64(sellFeesInBase)/float64(a.lotSize) + a.cfg().ProfitTrigger
   430  	sellRate := steppedRate(uint64(math.Round(float64(sellVWAP)*(1+adj))), a.rateStep)
   431  	buyFeesInBase, err := a.OrderFeesInUnits(false, true, buyVWAP)
   432  	if err != nil {
   433  		return nil, fmt.Errorf("error getting converted fees: %w", err)
   434  	}
   435  	adj = float64(buyFeesInBase)/float64(a.lotSize) + a.cfg().ProfitTrigger
   436  	buyRate := steppedRate(uint64(math.Round(float64(buyVWAP)/(1+adj))), a.rateStep)
   437  	perLot, err := a.lotCosts(sellRate, buyRate)
   438  	if perLot == nil {
   439  		return nil, fmt.Errorf("error getting lot costs: %w", err)
   440  	}
   441  	dist = a.newDistribution(perLot)
   442  	avgBaseLot, avgQuoteLot := float64(perLot.dexBase+perLot.cexBase)/2, float64(perLot.dexQuote+perLot.cexQuote)/2
   443  	baseLots := uint64(math.Round(float64(dist.baseInv.total) / avgBaseLot / 2))
   444  	quoteLots := uint64(math.Round(float64(dist.quoteInv.total) / avgQuoteLot / 2))
   445  	a.optimizeTransfers(dist, baseLots, quoteLots, baseLots*2, quoteLots*2)
   446  	return dist, nil
   447  }
   448  
   449  func (a *simpleArbMarketMaker) tryTransfers(currEpoch uint64) (actionTaken bool, err error) {
   450  	dist, err := a.distribution()
   451  	if err != nil {
   452  		a.log.Errorf("distribution calculation error: %v", err)
   453  		return
   454  	}
   455  	return a.transfer(dist, currEpoch)
   456  }
   457  
   458  func (a *simpleArbMarketMaker) botLoop(ctx context.Context) (*sync.WaitGroup, error) {
   459  	book, bookFeed, err := a.core.SyncBook(a.host, a.baseID, a.quoteID)
   460  	if err != nil {
   461  		return nil, fmt.Errorf("failed to sync book: %v", err)
   462  	}
   463  	a.book = book
   464  
   465  	err = a.cex.SubscribeMarket(ctx, a.baseID, a.quoteID)
   466  	if err != nil {
   467  		bookFeed.Close()
   468  		return nil, fmt.Errorf("failed to subscribe to cex market: %v", err)
   469  	}
   470  
   471  	tradeUpdates := a.cex.SubscribeTradeUpdates()
   472  
   473  	var wg sync.WaitGroup
   474  	wg.Add(1)
   475  	go func() {
   476  		defer wg.Done()
   477  		defer bookFeed.Close()
   478  		for {
   479  			select {
   480  			case ni := <-bookFeed.Next():
   481  				switch epoch := ni.Payload.(type) {
   482  				case *core.ResolvedEpoch:
   483  					a.rebalance(epoch.Current)
   484  				}
   485  			case <-ctx.Done():
   486  				return
   487  			}
   488  		}
   489  	}()
   490  
   491  	wg.Add(1)
   492  	go func() {
   493  		defer wg.Done()
   494  		for {
   495  			select {
   496  			case update := <-tradeUpdates:
   497  				a.handleCEXTradeUpdate(update)
   498  			case <-ctx.Done():
   499  				return
   500  			}
   501  		}
   502  	}()
   503  
   504  	wg.Add(1)
   505  	go func() {
   506  		defer wg.Done()
   507  		orderUpdates := a.core.SubscribeOrderUpdates()
   508  		for {
   509  			select {
   510  			case n := <-orderUpdates:
   511  				a.handleDEXOrderUpdate(n)
   512  			case <-ctx.Done():
   513  				return
   514  			}
   515  		}
   516  	}()
   517  
   518  	a.registerFeeGap()
   519  
   520  	return &wg, nil
   521  }
   522  
   523  func (a *simpleArbMarketMaker) registerFeeGap() {
   524  	feeGap, err := feeGap(a.core, a.CEX, a.baseID, a.quoteID, a.lotSize)
   525  	if err != nil {
   526  		a.log.Warnf("error getting fee-gap stats: %v", err)
   527  		return
   528  	}
   529  	a.unifiedExchangeAdaptor.registerFeeGap(feeGap)
   530  }
   531  
   532  func (a *simpleArbMarketMaker) updateConfig(cfg *BotConfig) error {
   533  	if cfg.SimpleArbConfig == nil {
   534  		// implies bug in caller
   535  		return fmt.Errorf("no arb config provided")
   536  	}
   537  	a.cfgV.Store(cfg.SimpleArbConfig)
   538  	return nil
   539  }
   540  
   541  func newSimpleArbMarketMaker(cfg *BotConfig, adaptorCfg *exchangeAdaptorCfg, log dex.Logger) (*simpleArbMarketMaker, error) {
   542  	if cfg.SimpleArbConfig == nil {
   543  		// implies bug in caller
   544  		return nil, fmt.Errorf("no arb config provided")
   545  	}
   546  
   547  	adaptor, err := newUnifiedExchangeAdaptor(adaptorCfg)
   548  	if err != nil {
   549  		return nil, fmt.Errorf("error constructing exchange adaptor: %w", err)
   550  	}
   551  
   552  	simpleArb := &simpleArbMarketMaker{
   553  		unifiedExchangeAdaptor: adaptor,
   554  		cex:                    adaptor,
   555  		core:                   adaptor,
   556  		activeArbs:             make([]*arbSequence, 0),
   557  	}
   558  	simpleArb.cfgV.Store(cfg.SimpleArbConfig)
   559  	adaptor.setBotLoop(simpleArb.botLoop)
   560  	return simpleArb, nil
   561  }