Skip to content

Commit

Permalink
client: Put market making logic outside of core
Browse files Browse the repository at this point in the history
This moves the market making logic outside of core. There is a new
`MarketMaker` type which calls into core. When running the `MarketMaker`,
one strategy can be defined for each dex market. Since the Gap strategy
was originally designed to have multiple bots running on a market, the
strategy will have to be modified, but this diff only deals with
refactoring the logic outside of core.

The RPCServer is updated with startMarketMaking and stopMarketMaking
commands. Market making can be started by providing a path to a config
file.

New Core methods are also added to support the MarketMaker: `ExchangeMarket`
and `SingleLotFees`.
  • Loading branch information
martonp committed May 4, 2023
1 parent 6cdcea4 commit 6c77f80
Show file tree
Hide file tree
Showing 29 changed files with 1,702 additions and 159 deletions.
74 changes: 33 additions & 41 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1890,15 +1890,11 @@ func (btc *baseWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
}, nil
}

// SingleLotSwapFees is a fallback for PreSwap that uses estimation when funds
// aren't available. The returned fees are the RealisticWorstCase. The Lots
// field of the PreSwapForm is ignored and assumed to be a single lot.
// Similarly, the MaxFeeRate, Immediate, RedeemVersion, and RedeemAssetID
// fields are unused.
func (btc *baseWallet) SingleLotSwapFees(form *asset.PreSwapForm) (fees uint64, err 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(form.SelectedOptions, customCfg)
err = config.Unmapify(options, customCfg)
if err != nil {
return 0, fmt.Errorf("error parsing selected swap options: %w", err)
}
Expand All @@ -1914,34 +1910,34 @@ func (btc *baseWallet) SingleLotSwapFees(form *asset.PreSwapForm) (fees uint64,
return 0, err
}

bumpedNetRate := form.FeeSuggestion
bumpedNetRate := feeSuggestion
if feeBump > 1 {
bumpedNetRate = uint64(math.Round(float64(bumpedNetRate) * feeBump))
}

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

const numInputs = 10
var txSize int
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 int
if split {
// TODO: The following is not correct for all BTC clones. e.g. Zcash has
// a different MinimumTxOverhead (29).
if btc.segwit {
fees += (dexbtc.MinimumTxOverhead + dexbtc.RedeemP2WPKHInputSize + dexbtc.P2WPKHOutputSize) * bumpedNetRate
splitTxSize = dexbtc.MinimumTxOverhead + dexbtc.RedeemP2WPKHInputSize + dexbtc.P2WPKHOutputSize
} else {
fees += (dexbtc.MinimumTxOverhead + dexbtc.RedeemP2PKHInputSize + dexbtc.P2PKHOutputSize) * bumpedNetRate
splitTxSize = dexbtc.MinimumTxOverhead + dexbtc.RedeemP2PKHInputSize + dexbtc.P2PKHOutputSize
}
}

var inputSize uint64
if btc.segwit {
inputSize = dexbtc.RedeemP2WPKHInputSize
} else {
inputSize = dexbtc.RedeemP2PKHInputSize
}
totalTxSize := uint64(txSize + splitTxSize)

const maxSwaps = 1 // Assumed single lot order
swapFunds := calc.RequiredOrderFundsAlt(form.LotSize, inputSize, maxSwaps,
btc.initTxSizeBase, btc.initTxSize, bumpedNetRate)
fees += swapFunds - form.LotSize

return fees, nil
return totalTxSize * bumpedNetRate, nil
}

// splitOption constructs an *asset.OrderOption with customized text based on the
Expand Down Expand Up @@ -2086,15 +2082,15 @@ func (btc *baseWallet) estimateSwap(lots, lotSize, feeSuggestion, maxFeeRate uin

// PreRedeem generates an estimate of the range of redemption fees that could
// be assessed.
func (btc *baseWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) {
feeRate := req.FeeSuggestion
func (btc *baseWallet) preRedeem(numLots, feeSuggestion uint64, options map[string]string) (*asset.PreRedeem, error) {
feeRate := feeSuggestion
if feeRate == 0 {
feeRate = btc.targetFeeRateWithFallback(btc.redeemConfTarget(), 0)
}
// Best is one transaction with req.Lots inputs and 1 output.
var best uint64 = dexbtc.MinimumTxOverhead
// Worst is req.Lots transactions, each with one input and one output.
var worst uint64 = dexbtc.MinimumTxOverhead * req.Lots
var worst uint64 = dexbtc.MinimumTxOverhead * numLots
var inputSize, outputSize uint64
if btc.segwit {
// Add the marker and flag weight here.
Expand All @@ -2105,12 +2101,12 @@ func (btc *baseWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, er
inputSize = dexbtc.TxInOverhead + dexbtc.RedeemSwapSigScriptSize
outputSize = dexbtc.P2PKHOutputSize
}
best += inputSize*req.Lots + outputSize
worst += (inputSize + outputSize) * req.Lots
best += inputSize*numLots + outputSize
worst += (inputSize + outputSize) * numLots

// Read the order options.
customCfg := new(redeemOptions)
err := config.Unmapify(req.SelectedOptions, customCfg)
err := config.Unmapify(options, customCfg)
if err != nil {
return nil, fmt.Errorf("error parsing selected options: %w", err)
}
Expand Down Expand Up @@ -2157,17 +2153,13 @@ func (btc *baseWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, er
}, nil
}

// SingleLotRedeemFees is a fallback for PreRedeem that uses estimation when
// funds aren't available. The returned fees are the RealisticWorstCase. The
// Lots field of the PreSwapForm is ignored and assumed to be a single lot.
func (btc *baseWallet) SingleLotRedeemFees(req *asset.PreRedeemForm) (uint64, error) {
// For BTC, there are no funds required to redeem, so we'll never actually
// end up here unless there are some bad order options, since this method
// is a backup for PreRedeem. We'll almost certainly generate the same error
// again.
form := *req
form.Lots = 1
preRedeem, err := btc.PreRedeem(&form)
func (btc *baseWallet) PreRedeem(form *asset.PreRedeemForm) (*asset.PreRedeem, error) {
return btc.preRedeem(form.Lots, form.FeeSuggestion, form.SelectedOptions)
}

// 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)
if err != nil {
return 0, err
}
Expand Down
63 changes: 31 additions & 32 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1646,13 +1646,11 @@ func (dcr *ExchangeWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, erro
}, nil
}

// SingleLotSwapFees is a fallback for PreSwap that uses estimation when funds
// aren't available. The returned fees are the RealisticWorstCase. The Lots
// field of the PreSwapForm is ignored and assumed to be a single lot.
func (dcr *ExchangeWallet) SingleLotSwapFees(form *asset.PreSwapForm) (fees uint64, err error) {
// 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(form.SelectedOptions, customCfg)
err = config.Unmapify(options, customCfg)
if err != nil {
return 0, fmt.Errorf("error parsing selected swap options: %w", err)
}
Expand All @@ -1668,21 +1666,24 @@ func (dcr *ExchangeWallet) SingleLotSwapFees(form *asset.PreSwapForm) (fees uint
return 0, err
}

bumpedNetRate := form.FeeSuggestion
bumpedNetRate := feeSuggestion
if feeBump > 1 {
bumpedNetRate = uint64(math.Round(float64(bumpedNetRate) * feeBump))
}

const numInputs = 10
txSize := dexdcr.InitTxSizeBase + (numInputs * dexdcr.P2PKHInputSize)

var splitTxSize int
if split {
fees += (dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + dexdcr.P2PKHOutputSize) * bumpedNetRate
// 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)
}

const maxSwaps = 1 // Assumed single lot order
swapFunds := calc.RequiredOrderFundsAlt(form.LotSize, dexdcr.P2PKHInputSize,
maxSwaps, dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedNetRate)
fees += swapFunds - form.LotSize

return fees, nil
totalTxSize := uint64(txSize + splitTxSize)
return totalTxSize * bumpedNetRate, nil
}

// splitOption constructs an *asset.OrderOption with customized text based on the
Expand Down Expand Up @@ -1736,27 +1737,25 @@ func (dcr *ExchangeWallet) splitOption(req *asset.PreSwapForm, utxos []*composit
return opt
}

// PreRedeem generates an estimate of the range of redemption fees that could
// be assessed.
func (dcr *ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) {
func (dcr *ExchangeWallet) preRedeem(numLots, feeSuggestion uint64, options map[string]string) (*asset.PreRedeem, error) {
cfg := dcr.config()

feeRate := req.FeeSuggestion
feeRate := feeSuggestion
if feeRate == 0 { // or just document that the caller must set it?
feeRate = dcr.targetFeeRateWithFallback(cfg.redeemConfTarget, req.FeeSuggestion)
feeRate = dcr.targetFeeRateWithFallback(cfg.redeemConfTarget, feeSuggestion)
}
// Best is one transaction with req.Lots inputs and 1 output.
var best uint64 = dexdcr.MsgTxOverhead
// Worst is req.Lots transactions, each with one input and one output.
var worst uint64 = dexdcr.MsgTxOverhead * req.Lots
var worst uint64 = dexdcr.MsgTxOverhead * numLots
var inputSize uint64 = dexdcr.TxInOverhead + dexdcr.RedeemSwapSigScriptSize
var outputSize uint64 = dexdcr.P2PKHOutputSize
best += inputSize*req.Lots + outputSize
worst += (inputSize + outputSize) * req.Lots
best += inputSize*numLots + outputSize
worst += (inputSize + outputSize) * numLots

// Read the order options.
customCfg := new(redeemOptions)
err := config.Unmapify(req.SelectedOptions, customCfg)
err := config.Unmapify(options, customCfg)
if err != nil {
return nil, fmt.Errorf("error parsing selected options: %w", err)
}
Expand Down Expand Up @@ -1803,19 +1802,19 @@ func (dcr *ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem
}, nil
}

// SingleLotRedeemFees is a fallback for PreRedeem that uses estimation when
// funds aren't available. The returned fees are the RealisticWorstCase.
func (dcr *ExchangeWallet) SingleLotRedeemFees(req *asset.PreRedeemForm) (uint64, error) {
// For DCR, there are no funds required to redeem, so we'll never actually
// end up here unless there are some bad order options, since this method
// is a backup for PreRedeem. We'll almost certainly generate the same error
// again.
form := *req
form.Lots = 1
preRedeem, err := dcr.PreRedeem(&form)
// PreRedeem generates an estimate of the range of redemption fees that could
// be assessed.
func (dcr *ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) {
return dcr.preRedeem(req.Lots, req.FeeSuggestion, req.SelectedOptions)
}

// 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)
if err != nil {
return 0, err
}

return preRedeem.Estimate.RealisticWorstCase, nil
}

Expand Down
25 changes: 10 additions & 15 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,6 @@ var _ asset.TxFeeEstimator = (*ETHWallet)(nil)
var _ asset.TxFeeEstimator = (*TokenWallet)(nil)
var _ asset.DynamicSwapper = (*ETHWallet)(nil)
var _ asset.DynamicSwapper = (*TokenWallet)(nil)
var _ asset.BotWallet = (*assetWallet)(nil)
var _ asset.Authenticator = (*ETHWallet)(nil)

type baseWallet struct {
Expand Down Expand Up @@ -1274,15 +1273,13 @@ func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (*
}, nil
}

// SingleLotSwapFees is a fallback for PreSwap that uses estimation when funds
// aren't available. The returned fees are the RealisticWorstCase. The Lots
// field of the PreSwapForm is ignored and assumed to be a single lot.
func (w *assetWallet) SingleLotSwapFees(form *asset.PreSwapForm) (fees uint64, err error) {
g := w.gases(form.Version)
// 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) {
g := w.gases(version)
if g == nil {
return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, form.Version)
return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
}
return g.Swap * form.FeeSuggestion, nil
return g.Swap * feeSuggestion, nil
}

// estimateSwap prepares an *asset.SwapEstimate. The estimate does not include
Expand Down Expand Up @@ -1386,15 +1383,13 @@ func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, err
}, nil
}

// SingleLotRedeemFees is a fallback for PreRedeem that uses estimation when
// funds aren't available. The returned fees are the RealisticWorstCase. The
// Lots field of the PreSwapForm is ignored and assumed to be a single lot.
func (w *assetWallet) SingleLotRedeemFees(form *asset.PreRedeemForm) (fees uint64, err error) {
g := w.gases(form.Version)
// 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) {
g := w.gases(version)
if g == nil {
return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, form.Version)
return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
}
return g.Redeem * form.FeeSuggestion, nil
return g.Redeem * feeSuggestion, nil
}

// coin implements the asset.Coin interface for ETH
Expand Down
17 changes: 4 additions & 13 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ type Wallet interface {
// different CoinID in the returned asset.ConfirmRedemptionStatus as was
// 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)
// SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
SingleLotRedeemFees(version uint32, feeRate uint64, options map[string]string) (uint64, error)
}

// Authenticator is a wallet implementation that require authentication.
Expand Down Expand Up @@ -816,19 +820,6 @@ type Bond struct {
RedeemTx []byte
}

// BotWallet implements some methods that can help bots function.
type BotWallet interface {
// SingleLotSwapFees is a fallback for PreSwap that uses estimation
// when funds aren't available. The returned fees are the
// RealisticWorstCase. The Lots field of the PreSwapForm is ignored and
// assumed to be a single lot.
SingleLotSwapFees(*PreSwapForm) (uint64, error)
// SingleLotRedeemFees is a fallback for PreRedeem that uses estimation when
// funds aren't available. The returned fees are the RealisticWorstCase. The
// Lots field of the PreSwapForm is ignored and assumed to be a single lot.
SingleLotRedeemFees(*PreRedeemForm) (uint64, error)
}

// ShieldedStatus is the balance and address associated with the shielded
// account.
type ShieldedStatus struct {
Expand Down
12 changes: 12 additions & 0 deletions client/cmd/dexc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
_ "decred.org/dcrdex/client/asset/doge" // register doge asset
_ "decred.org/dcrdex/client/asset/ltc" // register ltc asset
_ "decred.org/dcrdex/client/asset/zec" // register zec asset
"decred.org/dcrdex/client/mm"

"decred.org/dcrdex/client/core"
"decred.org/dcrdex/client/rpcserver"
Expand Down Expand Up @@ -106,6 +107,16 @@ func runCore() error {
return fmt.Errorf("error creating client core: %w", err)
}

var marketMaker *mm.MarketMaker
if cfg.Experimental {
// TODO: on shutdown, stop market making and wait for trades to be
// canceled.
marketMaker, err = mm.NewMarketMaker(clientCore, logMaker.Logger("MM"))
if err != nil {
return fmt.Errorf("error creating market maker: %w", err)
}
}

// Catch interrupt signal (e.g. ctrl+c), prompting to shutdown if the user
// is logged in, and there are active orders or matches.
killChan := make(chan os.Signal, 1)
Expand Down Expand Up @@ -161,6 +172,7 @@ func runCore() error {
rpcserver.SetLogger(logMaker.Logger("RPC"))
rpcCfg := &rpcserver.Config{
Core: clientCore,
MarketMaker: marketMaker,
Addr: cfg.RPCAddr,
User: cfg.RPCUser,
Pass: cfg.RPCPass,
Expand Down
25 changes: 13 additions & 12 deletions client/cmd/dexcctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,19 @@ func main() {
// promptPasswords is a map of routes to password prompts. Passwords are
// prompted in the order given.
var promptPasswords = map[string][]string{
"cancel": {"App password:"},
"discoveracct": {"App password:"},
"init": {"Set new app password:"},
"login": {"App password:"},
"newwallet": {"App password:", "Wallet password:"},
"openwallet": {"App password:"},
"register": {"App password:"},
"postbond": {"App password:"},
"trade": {"App password:"},
"withdraw": {"App password:"},
"send": {"App password:"},
"appseed": {"App password:"},
"cancel": {"App password:"},
"discoveracct": {"App password:"},
"init": {"Set new app password:"},
"login": {"App password:"},
"newwallet": {"App password:", "Wallet password:"},
"openwallet": {"App password:"},
"register": {"App password:"},
"postbond": {"App password:"},
"trade": {"App password:"},
"withdraw": {"App password:"},
"send": {"App password:"},
"appseed": {"App password:"},
"startmarketmaking": {"App password:"},
}

// optionalTextFiles is a map of routes to arg index for routes that should read
Expand Down
Loading

0 comments on commit 6c77f80

Please sign in to comment.