Skip to content

Commit

Permalink
client/mm: Update Basic Market Maker
Browse files Browse the repository at this point in the history
This updates the basic market maker strategy to place orders at multiple
distances from the basis price instead of just one. It uses the new
MultiTrade function to do this.

The basis price calculation is also updated to use fiat rates as a
fallback if there are no trades on the dex or an oracle rate.
  • Loading branch information
martonp committed Jul 15, 2023
1 parent 80eedea commit e33988f
Show file tree
Hide file tree
Showing 14 changed files with 1,726 additions and 334 deletions.
47 changes: 9 additions & 38 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1997,54 +1997,25 @@ func (btc *baseWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
}

// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
func (btc *baseWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, options map[string]string) (fees uint64, err error) {
// Load the user's selected order-time options.
customCfg := new(swapOptions)
err = config.Unmapify(options, customCfg)
if err != nil {
return 0, fmt.Errorf("error parsing selected swap options: %w", err)
}

// Parse the configured split transaction.
split := btc.useSplitTx()
if customCfg.Split != nil {
split = *customCfg.Split
}

feeBump, err := customCfg.feeBump()
if err != nil {
return 0, err
}

bumpedNetRate := feeSuggestion
if feeBump > 1 {
bumpedNetRate = uint64(math.Round(float64(bumpedNetRate) * feeBump))
func (btc *baseWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (fees uint64, err error) {
var numInputs uint64
if useSafeTxSize {
numInputs = 12
} else {
numInputs = 2
}

// TODO: The following is not correct for all BTC clones. e.g. Zcash has
// a different MinimumTxOverhead (29).

const numInputs = 12 // plan for lots of inputs to get a safe estimate

var txSize uint64
if btc.segwit {
txSize = dexbtc.MinimumTxOverhead + (numInputs * dexbtc.RedeemP2WPKHInputSize) + dexbtc.P2WSHOutputSize + dexbtc.P2WPKHOutputSize
} else {
txSize = dexbtc.MinimumTxOverhead + (numInputs * dexbtc.RedeemP2PKHInputSize) + dexbtc.P2SHOutputSize + dexbtc.P2PKHOutputSize
}

var splitTxSize uint64
if split {
if btc.segwit {
splitTxSize = dexbtc.MinimumTxOverhead + dexbtc.RedeemP2WPKHInputSize + dexbtc.P2WPKHOutputSize
} else {
splitTxSize = dexbtc.MinimumTxOverhead + dexbtc.RedeemP2PKHInputSize + dexbtc.P2PKHOutputSize
}
}

totalTxSize := txSize + splitTxSize

return totalTxSize * bumpedNetRate, nil
return txSize * feeSuggestion, nil
}

// splitOption constructs an *asset.OrderOption with customized text based on the
Expand Down Expand Up @@ -2266,8 +2237,8 @@ func (btc *baseWallet) PreRedeem(form *asset.PreRedeemForm) (*asset.PreRedeem, e
}

// SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
func (btc *baseWallet) SingleLotRedeemFees(_ uint32, feeSuggestion uint64, options map[string]string) (uint64, error) {
preRedeem, err := btc.preRedeem(1, feeSuggestion, options)
func (btc *baseWallet) SingleLotRedeemFees(_ uint32, feeSuggestion uint64) (uint64, error) {
preRedeem, err := btc.preRedeem(1, feeSuggestion, nil)
if err != nil {
return 0, err
}
Expand Down
46 changes: 12 additions & 34 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1654,43 +1654,19 @@ func (dcr *ExchangeWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, erro
}

// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
func (dcr *ExchangeWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, options map[string]string) (fees uint64, err error) {
// Load the user's selected order-time options.
customCfg := new(swapOptions)
err = config.Unmapify(options, customCfg)
if err != nil {
return 0, fmt.Errorf("error parsing selected swap options: %w", err)
}

// Parse the configured split transaction.
split := dcr.config().useSplitTx
if customCfg.Split != nil {
split = *customCfg.Split
}

feeBump, err := customCfg.feeBump()
if err != nil {
return 0, err
}

bumpedNetRate := feeSuggestion
if feeBump > 1 {
bumpedNetRate = uint64(math.Round(float64(bumpedNetRate) * feeBump))
func (dcr *ExchangeWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (fees uint64, err error) {
var numInputs uint64
if useSafeTxSize {
numInputs = 12
} else {
numInputs = 2
}

const numInputs = 12 // plan for lots of inputs to get a safe estimate
var txSize uint64 = dexdcr.InitTxSizeBase + (numInputs * dexdcr.P2PKHInputSize)

var splitTxSize uint64
if split {
// If there is a split, the split tx could have more inputs, and the
// swap would just have one, but the math works out the same this way
// anyways.
splitTxSize = dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + (2 * dexdcr.P2PKHOutputSize)
}
dcr.log.Infof("SingleLotSwapFees: txSize = %d, feeSuggestion = %d", txSize, feeSuggestion)

totalTxSize := txSize + splitTxSize
return totalTxSize * bumpedNetRate, nil
return txSize * feeSuggestion, nil
}

// MaxFundingFees returns the maximum funding fees for an order/multi-order.
Expand Down Expand Up @@ -1833,12 +1809,14 @@ func (dcr *ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem
}

// SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
func (dcr *ExchangeWallet) SingleLotRedeemFees(_ uint32, feeSuggestion uint64, options map[string]string) (uint64, error) {
preRedeem, err := dcr.preRedeem(1, feeSuggestion, options)
func (dcr *ExchangeWallet) SingleLotRedeemFees(_ uint32, feeSuggestion uint64) (uint64, error) {
preRedeem, err := dcr.preRedeem(1, feeSuggestion, nil)
if err != nil {
return 0, err
}

dcr.log.Infof("SingleLotRedeemFees: worst case = %d, feeSuggestion = %d", preRedeem.Estimate.RealisticWorstCase, feeSuggestion)

return preRedeem.Estimate.RealisticWorstCase, nil
}

Expand Down
8 changes: 6 additions & 2 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,11 +1287,12 @@ func (w *baseWallet) MaxFundingFees(_ uint32, _ map[string]string) uint64 {
}

// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
func (w *assetWallet) SingleLotSwapFees(version uint32, feeSuggestion uint64, _ map[string]string) (fees uint64, err error) {
func (w *assetWallet) SingleLotSwapFees(version uint32, feeSuggestion uint64, _ bool) (fees uint64, err error) {
g := w.gases(version)
if g == nil {
return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
}

return g.Swap * feeSuggestion, nil
}

Expand Down Expand Up @@ -1356,11 +1357,12 @@ func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, err
}

// SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
func (w *assetWallet) SingleLotRedeemFees(version uint32, feeSuggestion uint64, options map[string]string) (fees uint64, err error) {
func (w *assetWallet) SingleLotRedeemFees(version uint32, feeSuggestion uint64) (fees uint64, err error) {
g := w.gases(version)
if g == nil {
return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
}

return g.Redeem * feeSuggestion, nil
}

Expand Down Expand Up @@ -3276,6 +3278,8 @@ func (eth *baseWallet) FeeRate() uint64 {
return 0
}

eth.log.Infof("Got ETH fee rate: %v\n", feeRate)

feeRateGwei, err := dexeth.WeiToGweiUint64(feeRate)
if err != nil {
eth.log.Errorf("Failed to convert wei to gwei: %v", err)
Expand Down
4 changes: 2 additions & 2 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,9 +507,9 @@ type Wallet interface {
// used to call the function.
ConfirmRedemption(coinID dex.Bytes, redemption *Redemption, feeSuggestion uint64) (*ConfirmRedemptionStatus, error)
// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
SingleLotSwapFees(version uint32, feeRate uint64, options map[string]string) (uint64, error)
SingleLotSwapFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, error)
// SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
SingleLotRedeemFees(version uint32, feeRate uint64, options map[string]string) (uint64, error)
SingleLotRedeemFees(version uint32, feeRate uint64) (uint64, error)
// MaxFundingFees returns the max fees that could be paid for funding a swap.
MaxFundingFees(numTrades uint32, options map[string]string) uint64
}
Expand Down
4 changes: 2 additions & 2 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -5528,12 +5528,12 @@ func (c *Core) SingleLotFees(form *SingleLotFeesForm) (uint64, uint64, error) {
}
}

swapFees, err := wallets.fromWallet.SingleLotSwapFees(assetConfigs.fromAsset.Version, swapFeeRate, form.Options)
swapFees, err := wallets.fromWallet.SingleLotSwapFees(assetConfigs.fromAsset.Version, swapFeeRate, form.UseSafeTxSize)
if err != nil {
return 0, 0, fmt.Errorf("error calculating swap fees: %w", err)
}

redeemFees, err := wallets.toWallet.SingleLotRedeemFees(assetConfigs.toAsset.Version, redeemFeeRate, form.Options)
redeemFees, err := wallets.toWallet.SingleLotRedeemFees(assetConfigs.toAsset.Version, redeemFeeRate)
if err != nil {
return 0, 0, fmt.Errorf("error calculating redeem fees: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions client/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1030,11 +1030,11 @@ func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, chan
return w.preAccelerateSwapRate, &w.preAccelerateSuggestedRange, nil, nil
}

func (w *TXCWallet) SingleLotSwapFees(version uint32, feeRate uint64, options map[string]string) (uint64, error) {
func (w *TXCWallet) SingleLotSwapFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, error) {
return 0, nil
}

func (w *TXCWallet) SingleLotRedeemFees(version uint32, feeRate uint64, options map[string]string) (uint64, error) {
func (w *TXCWallet) SingleLotRedeemFees(version uint32, feeRate uint64) (uint64, error) {
return 0, nil
}

Expand Down
12 changes: 6 additions & 6 deletions client/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1012,12 +1012,12 @@ type MultiTradeForm struct {

// SingleLotFeesForm is used to determine the fees for a single lot trade.
type SingleLotFeesForm struct {
Host string `json:"host"`
Base uint32 `json:"base"`
Quote uint32 `json:"quote"`
Sell bool `json:"sell"`
Options map[string]string `json:"options"`
UseMaxFeeRate bool `json:"useMaxFeeRate"`
Host string `json:"host"`
Base uint32 `json:"base"`
Quote uint32 `json:"quote"`
Sell bool `json:"sell"`
UseMaxFeeRate bool `json:"useMaxFeeRate"`
UseSafeTxSize bool `json:"useSafeTxSize"`
}

// marketName is a string ID constructed from the asset IDs.
Expand Down
13 changes: 9 additions & 4 deletions client/mm/balance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,8 @@ func (b *balanceHandler) handleOrderUpdate(o *core.Order) {
if maxSwapFees > o.FeesPaid.Swap {
b.log.Tracef("oid: %s, return excess swap fees, maxSwapFees %v, swap fees %v", o.ID, maxSwapFees, o.FeesPaid.Swap)
b.increaseBotBalance(orderInfo.bot, fromAsset, maxSwapFees-o.FeesPaid.Swap, o.ID)
} else {
b.log.Errorf("oid: %v - maxSwapFees %d > swap fees %d", hex.EncodeToString(o.ID), maxSwapFees, o.FeesPaid.Swap)
} else if maxSwapFees < o.FeesPaid.Swap {
b.log.Errorf("oid: %v - maxSwapFees %d < swap fees %d", hex.EncodeToString(o.ID), maxSwapFees, o.FeesPaid.Swap)
}

// Return excess redeem fees
Expand All @@ -388,8 +388,8 @@ func (b *balanceHandler) handleOrderUpdate(o *core.Order) {
if maxRedeemFees > o.FeesPaid.Redemption {
b.log.Tracef("oid: %s, return excess redeem fees, maxRedeemFees %v, redemption fees %v", o.ID, maxRedeemFees, o.FeesPaid.Redemption)
b.increaseBotBalance(orderInfo.bot, toAsset, maxRedeemFees-o.FeesPaid.Redemption, o.ID)
} else {
b.log.Errorf("oid: %v - maxRedeemFees %d > redemption fees %d",
} else if maxRedeemFees < o.FeesPaid.Redemption {
b.log.Errorf("oid: %v - maxRedeemFees %d < redemption fees %d",
hex.EncodeToString(o.ID), maxRedeemFees, o.FeesPaid.Redemption)
}
}
Expand Down Expand Up @@ -545,6 +545,7 @@ func (c *coreWithSegregatedBalance) Trade(pw []byte, form *core.TradeForm) (*cor
Quote: form.Quote,
Sell: form.Sell,
UseMaxFeeRate: true,
UseSafeTxSize: true,
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -609,6 +610,7 @@ func (c *coreWithSegregatedBalance) MultiTrade(pw []byte, form *core.MultiTradeF
Base: form.Base,
Quote: form.Quote,
Sell: form.Sell,
UseSafeTxSize: true,
UseMaxFeeRate: true,
})
if err != nil {
Expand Down Expand Up @@ -687,6 +689,7 @@ func (c *coreWithSegregatedBalance) maxBuyQty(host string, base, quote uint32, r
Base: base,
Quote: quote,
UseMaxFeeRate: true,
UseSafeTxSize: true,
})
if err != nil {
return 0, err
Expand Down Expand Up @@ -762,6 +765,7 @@ func (c *coreWithSegregatedBalance) maxSellQty(host string, base, quote uint32,
Quote: quote,
Sell: true,
UseMaxFeeRate: true,
UseSafeTxSize: true,
})
if err != nil {
return 0, err
Expand Down Expand Up @@ -866,6 +870,7 @@ func (c *coreWithSegregatedBalance) sufficientBalanceForTrades(host string, base
Base: base,
Quote: quote,
UseMaxFeeRate: true,
UseSafeTxSize: true,
})
if err != nil {
return false, err
Expand Down
37 changes: 35 additions & 2 deletions client/mm/mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,19 @@ type clientCore interface {
WalletState(assetID uint32) *core.WalletState
MultiTrade(pw []byte, form *core.MultiTradeForm) ([]*core.Order, error)
MaxFundingFees(fromAsset uint32, numTrades uint32, options map[string]string) (uint64, error)
User() *core.User
}

var _ clientCore = (*core.Core)(nil)

// dexOrderBook is satisfied by orderbook.OrderBook.
// Avoids having to mock the entire orderbook in tests.
type dexOrderBook interface {
MidGap() (uint64, error)
}

var _ dexOrderBook = (*orderbook.OrderBook)(nil)

// MarketMaker handles the market making process. It supports running different
// strategies on different markets.
type MarketMaker struct {
Expand Down Expand Up @@ -117,7 +126,24 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
return fmt.Errorf("failed to login: %w", err)
}

// TODO: unlock all wallets
unlocked := make(map[uint32]interface{})
for _, cfg := range cfgs {
if _, done := unlocked[cfg.BaseAsset]; !done {
err := m.core.OpenWallet(cfg.BaseAsset, pw)
if err != nil {
return fmt.Errorf("failed to unlock wallet for asset %d: %w", cfg.BaseAsset, err)
}
unlocked[cfg.BaseAsset] = true
}

if _, done := unlocked[cfg.QuoteAsset]; !done {
err := m.core.OpenWallet(cfg.QuoteAsset, pw)
if err != nil {
return fmt.Errorf("failed to unlock wallet for asset %d: %w", cfg.QuoteAsset, err)
}
unlocked[cfg.QuoteAsset] = true
}
}

oracle, err := priceOracleFromConfigs(m.ctx, cfgs, m.log.SubLogger("PriceOracle"))
if err != nil {
Expand All @@ -129,6 +155,8 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
return err
}

user := m.core.User()

startedMarketMaking = true

wg := new(sync.WaitGroup)
Expand All @@ -145,7 +173,12 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
go func(cfg *BotConfig) {
logger := m.log.SubLogger(fmt.Sprintf("MarketMaker-%s-%d-%d", cfg.Host, cfg.BaseAsset, cfg.QuoteAsset))
mktID := dexMarketID(cfg.Host, cfg.BaseAsset, cfg.QuoteAsset)
RunBasicMarketMaker(m.ctx, cfg, m.bh.wrappedCoreForBot(mktID), oracle, pw, logger)
var baseFiatRate, quoteFiatRate float64
if user != nil {
baseFiatRate = user.FiatRates[cfg.BaseAsset]
quoteFiatRate = user.FiatRates[cfg.QuoteAsset]
}
RunBasicMarketMaker(m.ctx, cfg, m.bh.wrappedCoreForBot(mktID), oracle, baseFiatRate, quoteFiatRate, logger)
wg.Done()
}(cfg)
default:
Expand Down
Loading

0 comments on commit e33988f

Please sign in to comment.