Skip to content

Commit

Permalink
multi: implement a market-maker bot
Browse files Browse the repository at this point in the history
core:
Add a market-maker bot to **core**. The bot uses some internal and
(optionally) external signals to calculate a ideal buy and sell
price and a "break-even spread", which is the spread at which
a buy-sell sequence's tx fees equal its profit. These values can
be used as inputs into one of five "gap strategies", which determine
the target spread.

The bot can be created, started, updated, paused, and retired. Each
of those actions generates a notification.

db:
New BotProgram type is not specific to market-maker bots. Everything
is structured with a mind towards future expansion with new automated
trading routines.

ui:
New /mm view enables creation, re-configuration, and monitoring of
existing market-maker bots.
  • Loading branch information
buck54321 authored Nov 2, 2022
1 parent 4dc0bc4 commit 7d3e6c0
Show file tree
Hide file tree
Showing 63 changed files with 5,691 additions and 539 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ client/cmd/dexc/dexc
client/cmd/dexcctl/dexcctl
client/cmd/assetseed/assetseed
client/cmd/simnet-trade-tests/simnet-trade-tests
client/cmd/mmbot/mmbot
docs/examples/rpcclient/rpcclient
dex/testing/loadbot/loadbot
bin/
Expand Down
93 changes: 84 additions & 9 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,20 @@ type swapOptions struct {
FeeBump *float64 `ini:"swapfeebump"`
}

func (s *swapOptions) feeBump() (float64, error) {
bump := 1.0
if s.FeeBump != nil {
bump = *s.FeeBump
if bump > 2.0 {
return 0, fmt.Errorf("fee bump %f is higher than the 2.0 limit", bump)
}
if bump < 1.0 {
return 0, fmt.Errorf("fee bump %f is lower than 1", bump)
}
}
return bump, nil
}

// redeemOptions are order options that apply to redemptions.
type redeemOptions struct {
FeeBump *float64 `ini:"redeemfeebump"`
Expand Down Expand Up @@ -1606,15 +1620,9 @@ func (btc *baseWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
}

// Parse the configured fee bump.
bump := 1.0
if customCfg.FeeBump != nil {
bump = *customCfg.FeeBump
if bump > 2.0 {
return nil, fmt.Errorf("fee bump %f is higher than the 2.0 limit", bump)
}
if bump < 1.0 {
return nil, fmt.Errorf("fee bump %f is lower than 1", bump)
}
bump, err := customCfg.feeBump()
if err != nil {
return nil, err
}

// Get the estimate using the current configuration.
Expand Down Expand Up @@ -1693,6 +1701,56 @@ 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.
func (btc *baseWallet) SingleLotSwapFees(form *asset.PreSwapForm) (fees uint64, err error) {
// Load the user's selected order-time options.
customCfg := new(swapOptions)
err = config.Unmapify(form.SelectedOptions, 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 := form.FeeSuggestion
if feeBump > 1 {
bumpedNetRate = uint64(math.Round(float64(bumpedNetRate) * feeBump))
}

if split {
if btc.segwit {
fees += (dexbtc.MinimumTxOverhead + dexbtc.RedeemP2WPKHInputSize + dexbtc.P2WPKHOutputSize) * bumpedNetRate
} else {
fees += (dexbtc.MinimumTxOverhead + dexbtc.RedeemP2PKHInputSize + dexbtc.P2PKHOutputSize) * bumpedNetRate
}
}

var inputSize uint64
if btc.segwit {
inputSize = dexbtc.RedeemP2WPKHInputSize
} else {
inputSize = dexbtc.RedeemP2PKHInputSize
}

nfo := form.AssetConfig
const maxSwaps = 1 // Assumed single lot order
swapFunds := calc.RequiredOrderFundsAlt(form.LotSize, inputSize, maxSwaps, nfo.SwapSizeBase, nfo.SwapSize, bumpedNetRate)
fees += swapFunds - form.LotSize

return fees, nil
}

// splitOption constructs an *asset.OrderOption with customized text based on the
// difference in fees between the configured and test split condition.
func (btc *baseWallet) splitOption(req *asset.PreSwapForm, utxos []*compositeUTXO, bump float64) *asset.OrderOption {
Expand Down Expand Up @@ -1885,6 +1943,23 @@ 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)
if err != nil {
return 0, err
}
return preRedeem.Estimate.RealisticWorstCase, nil
}

// FundOrder selects coins for use in an order. The coins will be locked, and
// will not be returned in subsequent calls to FundOrder or calculated in calls
// to Available, unless they are unlocked with ReturnCoins.
Expand Down
81 changes: 72 additions & 9 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,20 @@ type swapOptions struct {
FeeBump *float64 `ini:"swapfeebump"`
}

func (s *swapOptions) feeBump() (float64, error) {
bump := 1.0
if s.FeeBump != nil {
bump = *s.FeeBump
if bump > 2.0 {
return 0, fmt.Errorf("fee bump %f is higher than the 2.0 limit", bump)
}
if bump < 1.0 {
return 0, fmt.Errorf("fee bump %f is lower than 1", bump)
}
}
return bump, nil
}

// redeemOptions are order options that apply to redemptions.
type redeemOptions struct {
FeeBump *float64 `ini:"redeemfeebump"`
Expand Down Expand Up @@ -1240,15 +1254,9 @@ func (dcr *ExchangeWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, erro
}

// Parse the configured fee bump.
var bump float64 = 1.0
if customCfg.FeeBump != nil {
bump = *customCfg.FeeBump
if bump > 2.0 {
return nil, fmt.Errorf("fee bump %f is higher than the 2.0 limit", bump)
}
if bump < 1.0 {
return nil, fmt.Errorf("fee bump %f is lower than 1", bump)
}
bump, err := customCfg.feeBump()
if err != nil {
return nil, err
}

// Get the estimate for the requested number of lots.
Expand Down Expand Up @@ -1327,6 +1335,45 @@ 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) {
// Load the user's selected order-time options.
customCfg := new(swapOptions)
err = config.Unmapify(form.SelectedOptions, 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 := form.FeeSuggestion
if feeBump > 1 {
bumpedNetRate = uint64(math.Round(float64(bumpedNetRate) * feeBump))
}

if split {
fees += (dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + dexdcr.P2PKHOutputSize) * bumpedNetRate
}

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

return fees, nil
}

// splitOption constructs an *asset.OrderOption with customized text based on the
// difference in fees between the configured and test split condition.
func (dcr *ExchangeWallet) splitOption(req *asset.PreSwapForm, utxos []*compositeUTXO, bump float64) *asset.OrderOption {
Expand Down Expand Up @@ -1442,6 +1489,22 @@ 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)
if err != nil {
return 0, err
}
return preRedeem.Estimate.RealisticWorstCase, nil
}

// orderEnough generates a function that can be used as the enough argument to
// the fund method.
func orderEnough(val, lots, feeRate uint64, nfo *dex.Asset) func(sum uint64, size uint32, unspent *compositeUTXO) bool {
Expand Down
19 changes: 19 additions & 0 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,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) {
return form.AssetConfig.SwapSize * form.FeeSuggestion, nil
}

// estimateSwap prepares an *asset.SwapEstimate. The estimate does not include
// funds that might be locked for refunds.
func (w *assetWallet) estimateSwap(lots, lotSize, feeSuggestion uint64, dexSwapCfg, dexRedeemCfg *dex.Asset) (*asset.SwapEstimate, error) {
Expand Down Expand Up @@ -1204,6 +1211,18 @@ 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.PreSwapForm) (fees uint64, err error) {
redeemSize := form.AssetConfig.RedeemSize
if redeemSize == 0 {
g := w.gases(form.AssetConfig.Version)
redeemSize = g.Redeem
}
return redeemSize * form.FeeSuggestion, nil
}

// coin implements the asset.Coin interface for ETH
type coin struct {
id common.Hash
Expand Down
2 changes: 1 addition & 1 deletion client/asset/eth/nodeclient_harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ func runSimnet(m *testing.M) (int, error) {
return 1, fmt.Errorf("error unlocking initiator client: %w", err)
}

// Fund the wallets. Can use the simharness package once #1738 is merged.
// Fund the wallets.
homeDir, _ := os.UserHomeDir()
harnessCtlDir := filepath.Join(homeDir, "dextest", "eth", "harness-ctl")
send := func(exe, addr, amt string) error {
Expand Down
13 changes: 13 additions & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,19 @@ 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)
}

// Balance is categorized information about a wallet's balance.
type Balance struct {
// Available is the balance that is available for trading immediately.
Expand Down
1 change: 1 addition & 0 deletions client/cmd/dexc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type Config struct {
Onion string `long:"onion" description:"Proxy for .onion addresses, if torproxy not set (eg. 127.0.0.1:9050)."`
Net dex.Network
CertHosts []string
Experimental bool `long:"experimental" description:"Enable experimental features"`
}

var defaultConfig = Config{
Expand Down
1 change: 1 addition & 0 deletions client/cmd/dexc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func runCore() error {
NoEmbed: cfg.NoEmbedSite,
HttpProf: cfg.HTTPProfile,
Language: cfg.Language,
Experimental: cfg.Experimental,
})
if err != nil {
return fmt.Errorf("failed creating web server: %w", err)
Expand Down
Loading

0 comments on commit 7d3e6c0

Please sign in to comment.