decred.org/dcrdex@v1.0.5/client/mm/config.go (about)

     1  package mm
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strconv"
     7  
     8  	"decred.org/dcrdex/dex/utils"
     9  )
    10  
    11  // MarketMakingConfig is the overall configuration of the market maker.
    12  type MarketMakingConfig struct {
    13  	BotConfigs []*BotConfig `json:"botConfigs"`
    14  	CexConfigs []*CEXConfig `json:"cexConfigs"`
    15  }
    16  
    17  func (cfg *MarketMakingConfig) Copy() *MarketMakingConfig {
    18  	c := &MarketMakingConfig{
    19  		BotConfigs: make([]*BotConfig, len(cfg.BotConfigs)),
    20  		CexConfigs: make([]*CEXConfig, len(cfg.CexConfigs)),
    21  	}
    22  	copy(c.BotConfigs, cfg.BotConfigs)
    23  	copy(c.CexConfigs, cfg.CexConfigs)
    24  	return c
    25  }
    26  
    27  // CEXConfig is a configuration for connecting to a CEX API.
    28  type CEXConfig struct {
    29  	// Name is the name of the cex.
    30  	Name string `json:"name"`
    31  	// APIKey is the API key for the CEX.
    32  	APIKey string `json:"apiKey"`
    33  	// APISecret is the API secret for the CEX.
    34  	APISecret string `json:"apiSecret"`
    35  }
    36  
    37  // AutoRebalanceConfig configures deposits and withdrawals by setting minimum
    38  // transfer sizes. Minimum transfer sizes should be set to prevent excessive
    39  // fees on high-fee blockchains. To calculate a minimum transfer size for an
    40  // asset, choose a fee-loss tolerance <= your profit target. If you only wanted to
    41  // lose a maximum of 1% to transfers, and the fees associated with a transfer
    42  // are 350 Sats, then your minimum transfer size might be set to
    43  // 350 * (1 / 0.01) = 35000 Sats.
    44  // For low-fee assets, a transfer size of zero would be perfectly fine in most
    45  // cases, but using a higher value prevents churn.
    46  // For obvious reasons, minimum transfer sizes should never be more than the
    47  // total amount allocated for trading.
    48  // The way these are configured will probably be changed to better capture the
    49  // reasoning above.
    50  type AutoRebalanceConfig struct {
    51  	MinBaseTransfer  uint64 `json:"minBaseTransfer"`
    52  	MinQuoteTransfer uint64 `json:"minQuoteTransfer"`
    53  }
    54  
    55  func (a *AutoRebalanceConfig) copy() *AutoRebalanceConfig {
    56  	return &AutoRebalanceConfig{
    57  		MinBaseTransfer:  a.MinBaseTransfer,
    58  		MinQuoteTransfer: a.MinQuoteTransfer,
    59  	}
    60  }
    61  
    62  // BotBalanceAllocation is the initial allocation of funds for a bot.
    63  type BotBalanceAllocation struct {
    64  	DEX map[uint32]uint64 `json:"dex"`
    65  	CEX map[uint32]uint64 `json:"cex"`
    66  }
    67  
    68  func (b *BotBalanceAllocation) copy() *BotBalanceAllocation {
    69  	return &BotBalanceAllocation{
    70  		DEX: utils.CopyMap(b.DEX),
    71  		CEX: utils.CopyMap(b.CEX),
    72  	}
    73  }
    74  
    75  // BotInventoryDiffs is the amount of funds to add or remove from a bot's
    76  // allocation.
    77  type BotInventoryDiffs struct {
    78  	DEX map[uint32]int64 `json:"dex"`
    79  	CEX map[uint32]int64 `json:"cex"`
    80  }
    81  
    82  func (d *BotInventoryDiffs) copy() *BotInventoryDiffs {
    83  	return &BotInventoryDiffs{
    84  		DEX: utils.CopyMap(d.DEX),
    85  		CEX: utils.CopyMap(d.CEX),
    86  	}
    87  }
    88  
    89  // balanceDiffsToAllocations converts a BotInventoryDiffs to a
    90  // BotBalanceAllocation by removing all negative diffs.
    91  func balanceDiffsToAllocation(diffs *BotInventoryDiffs) *BotBalanceAllocation {
    92  	allocations := &BotBalanceAllocation{
    93  		DEX: make(map[uint32]uint64, len(diffs.DEX)),
    94  		CEX: make(map[uint32]uint64, len(diffs.CEX)),
    95  	}
    96  
    97  	for assetID, diff := range diffs.DEX {
    98  		if diff > 0 {
    99  			allocations.DEX[assetID] += uint64(diff)
   100  		}
   101  	}
   102  	for assetID, diff := range diffs.CEX {
   103  		if diff > 0 {
   104  			allocations.CEX[assetID] += uint64(diff)
   105  		}
   106  	}
   107  
   108  	return allocations
   109  }
   110  
   111  // #### IMPORTANT ###
   112  // If non-backwards compatible changes are made to the BotConfig, a new version
   113  // should be created and the event log db should be updated to support both
   114  // versions.
   115  
   116  type rpcConfig struct {
   117  	Alloc         *BotBalanceAllocation `json:"alloc"`
   118  	AutoRebalance *AutoRebalanceConfig  `json:"autoRebalance"`
   119  }
   120  
   121  func (r *rpcConfig) copy() *rpcConfig {
   122  	return &rpcConfig{
   123  		Alloc:         r.Alloc.copy(),
   124  		AutoRebalance: r.AutoRebalance.copy(),
   125  	}
   126  }
   127  
   128  // BotConfig is the configuration for a market making bot.
   129  // The balance fields are the initial amounts that will be reserved to use for
   130  // this bot. As the bot trades, the amounts reserved for it will be updated.
   131  type BotConfig struct {
   132  	Host    string `json:"host"`
   133  	BaseID  uint32 `json:"baseID"`
   134  	QuoteID uint32 `json:"quoteID"`
   135  
   136  	BaseWalletOptions  map[string]string `json:"baseWalletOptions"`
   137  	QuoteWalletOptions map[string]string `json:"quoteWalletOptions"`
   138  
   139  	CEXName string `json:"cexName"`
   140  
   141  	// UIConfig is settings defined and used by the front end to determine
   142  	// allocations.
   143  	UIConfig json.RawMessage `json:"uiConfig,omitempty"`
   144  
   145  	// RPCConfig can be used for file-based initial allocations and
   146  	// auto-rebalance settings.
   147  	RPCConfig *rpcConfig `json:"rpcConfig"`
   148  
   149  	// LotSize is the lot size of the market at the time this configuration
   150  	// was created. It is used to notify the user if the lot size changes
   151  	// when they are starting the bot.
   152  	LotSize uint64 `json:"lotSize"`
   153  
   154  	// Only one of the following configs should be set
   155  	BasicMMConfig        *BasicMarketMakingConfig `json:"basicMarketMakingConfig,omitempty"`
   156  	SimpleArbConfig      *SimpleArbConfig         `json:"simpleArbConfig,omitempty"`
   157  	ArbMarketMakerConfig *ArbMarketMakerConfig    `json:"arbMarketMakingConfig,omitempty"`
   158  }
   159  
   160  func (c *BotConfig) copy() *BotConfig {
   161  	b := *c
   162  
   163  	b.BaseWalletOptions = utils.CopyMap(c.BaseWalletOptions)
   164  	b.QuoteWalletOptions = utils.CopyMap(c.QuoteWalletOptions)
   165  
   166  	if c.UIConfig != nil {
   167  		b.UIConfig = make(json.RawMessage, len(c.UIConfig))
   168  		copy(b.UIConfig, c.UIConfig)
   169  	}
   170  	if c.RPCConfig != nil {
   171  		b.RPCConfig = c.RPCConfig.copy()
   172  	}
   173  	if c.BasicMMConfig != nil {
   174  		b.BasicMMConfig = c.BasicMMConfig.copy()
   175  	}
   176  	if c.SimpleArbConfig != nil {
   177  		b.SimpleArbConfig = c.SimpleArbConfig.copy()
   178  	}
   179  	if c.ArbMarketMakerConfig != nil {
   180  		b.ArbMarketMakerConfig = c.ArbMarketMakerConfig.copy()
   181  	}
   182  
   183  	return &b
   184  }
   185  
   186  // updateLotSize modifies the bot's configuration based on an update to the
   187  // market's lot size.
   188  func (c *BotConfig) updateLotSize(oldLotSize, newLotSize uint64) {
   189  	if c.BasicMMConfig != nil {
   190  		c.BasicMMConfig.updateLotSize(oldLotSize, newLotSize)
   191  	} else if c.ArbMarketMakerConfig != nil {
   192  		c.ArbMarketMakerConfig.updateLotSize(oldLotSize, newLotSize)
   193  	}
   194  }
   195  
   196  func (c *BotConfig) validate() error {
   197  	if c.BasicMMConfig != nil {
   198  		return c.BasicMMConfig.validate()
   199  	} else if c.SimpleArbConfig != nil {
   200  		return c.SimpleArbConfig.validate()
   201  	} else if c.ArbMarketMakerConfig != nil {
   202  		return c.ArbMarketMakerConfig.validate()
   203  	}
   204  
   205  	return fmt.Errorf("no bot config set")
   206  }
   207  
   208  func validateConfigUpdate(old, new *BotConfig) error {
   209  	if (old.BasicMMConfig == nil) != (new.BasicMMConfig == nil) ||
   210  		(old.SimpleArbConfig == nil) != (new.SimpleArbConfig == nil) ||
   211  		(old.ArbMarketMakerConfig == nil) != (new.ArbMarketMakerConfig == nil) {
   212  		return fmt.Errorf("cannot change bot type")
   213  	}
   214  
   215  	return new.validate()
   216  }
   217  
   218  func (c *BotConfig) requiresPriceOracle() bool {
   219  	return c.BasicMMConfig != nil
   220  }
   221  
   222  func (c *BotConfig) requiresCEX() bool {
   223  	return c.SimpleArbConfig != nil || c.ArbMarketMakerConfig != nil
   224  }
   225  
   226  // multiSplitBuffer returns the additional buffer to add to the order size
   227  // when doing a multi-split. This only applies to the quote asset.
   228  func (c *BotConfig) multiSplitBuffer() float64 {
   229  	if c.QuoteWalletOptions == nil {
   230  		return 0
   231  	}
   232  	multiSplitBuffer, ok := c.QuoteWalletOptions["multisplitbuffer"]
   233  	if !ok {
   234  		return 0
   235  	}
   236  	multiSplitBufferFloat, err := strconv.ParseFloat(multiSplitBuffer, 64)
   237  	if err != nil {
   238  		return 0
   239  	}
   240  	return multiSplitBufferFloat
   241  }
   242  
   243  // maxPlacements returns the max amount of placements this bot will place on
   244  // either side of the market in an epoch.
   245  func (c *BotConfig) maxPlacements() (buy, sell uint32) {
   246  	switch {
   247  	case c.SimpleArbConfig != nil:
   248  		return 1, 1
   249  	case c.ArbMarketMakerConfig != nil:
   250  		return uint32(len(c.ArbMarketMakerConfig.BuyPlacements)), uint32(len(c.ArbMarketMakerConfig.SellPlacements))
   251  	case c.BasicMMConfig != nil:
   252  		return uint32(len(c.BasicMMConfig.BuyPlacements)), uint32(len(c.BasicMMConfig.SellPlacements))
   253  	default:
   254  		return 1, 1
   255  	}
   256  }
   257  
   258  func dexMarketID(host string, base, quote uint32) string {
   259  	return fmt.Sprintf("%s-%d-%d", host, base, quote)
   260  }