Skip to content

Commit

Permalink
Start
Browse files Browse the repository at this point in the history
  • Loading branch information
martonp committed Jul 12, 2023
1 parent 86c2993 commit fc4caff
Show file tree
Hide file tree
Showing 26 changed files with 2,678 additions and 1,276 deletions.
17 changes: 11 additions & 6 deletions client/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ type Config struct {
// Web creates a configuration for the webserver. This is a Config method
// instead of a WebConfig method because Language is an app-level setting used
// by both core and rpcserver.
func (cfg *Config) Web(c *core.Core, log dex.Logger, utc bool) *webserver.Config {
func (cfg *Config) Web(c *core.Core, mm *mm.MarketMaker, log dex.Logger, utc bool) *webserver.Config {
addr := cfg.WebAddr
host, _, err := net.SplitHostPort(addr)
if err == nil && host != "" {
Expand All @@ -163,8 +163,12 @@ func (cfg *Config) Web(c *core.Core, log dex.Logger, utc bool) *webserver.Config
keyFile = filepath.Join(cfg.AppData, "web.key")
}

_, _, mmCfgPath := setNet(cfg.AppData, cfg.Net.String())

return &webserver.Config{
Core: c,
MarketMaker: mm,
MMCfgPath: mmCfgPath,
Addr: cfg.WebAddr,
CustomSiteDir: cfg.SiteDir,
Logger: log,
Expand Down Expand Up @@ -283,13 +287,13 @@ func ResolveConfig(appData string, cfg *Config) error {
switch {
case cfg.Testnet:
cfg.Net = dex.Testnet
defaultDBPath, defaultLogPath = setNet(appData, "testnet")
defaultDBPath, defaultLogPath, _ = setNet(appData, "testnet")
case cfg.Simnet:
cfg.Net = dex.Simnet
defaultDBPath, defaultLogPath = setNet(appData, "simnet")
defaultDBPath, defaultLogPath, _ = setNet(appData, "simnet")
default:
cfg.Net = dex.Mainnet
defaultDBPath, defaultLogPath = setNet(appData, "mainnet")
defaultDBPath, defaultLogPath, _ = setNet(appData, "mainnet")
}
defaultHost := DefaultHostByNetwork(cfg.Net)

Expand Down Expand Up @@ -330,10 +334,11 @@ func ResolveConfig(appData string, cfg *Config) error {
// files. It returns a suggested path for the database file and a log file. If
// using a file rotator, the directory of the log filepath as parsed by
// filepath.Dir is suitable for use.
func setNet(applicationDirectory, net string) (dbPath, logPath string) {
func setNet(applicationDirectory, net string) (dbPath, logPath, mmCfgPath string) {
netDirectory := filepath.Join(applicationDirectory, net)
logDirectory := filepath.Join(netDirectory, "logs")
logFilename := filepath.Join(logDirectory, "dexc.log")
mmCfgFilename := filepath.Join(netDirectory, "mm_cfg.json")
err := os.MkdirAll(netDirectory, 0700)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create net directory: %v\n", err)
Expand All @@ -344,7 +349,7 @@ func setNet(applicationDirectory, net string) (dbPath, logPath string) {
fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
os.Exit(1)
}
return filepath.Join(netDirectory, "dexc.db"), logFilename
return filepath.Join(netDirectory, "dexc.db"), logFilename, mmCfgFilename
}

// DefaultHostByNetwork accepts configured network and returns the network
Expand Down
2 changes: 1 addition & 1 deletion client/cmd/dexc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func runCore(cfg *app.Config) error {
}

if !cfg.NoWeb {
webSrv, err := webserver.New(cfg.Web(clientCore, logMaker.Logger("WEB"), utc))
webSrv, err := webserver.New(cfg.Web(clientCore, marketMaker, logMaker.Logger("WEB"), utc))
if err != nil {
return fmt.Errorf("failed creating web server: %w", err)
}
Expand Down
2 changes: 2 additions & 0 deletions client/mm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type BotConfig struct {
MMCfg *MarketMakingConfig `json:"marketMakingConfig,omitempty"`
MMWithCEXCfg *MarketMakingWithCEXConfig `json:"marketMakingWithCEXConfig,omitempty"`
ArbCfg *ArbitrageConfig `json:"arbitrageConfig,omitempty"`

Disabled bool `json:"disabled"`
}

func (c *BotConfig) requiresPriceOracle() bool {
Expand Down
157 changes: 143 additions & 14 deletions client/mm/mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"sync"
"sync/atomic"

Expand Down Expand Up @@ -49,17 +51,75 @@ var _ dexOrderBook = (*orderbook.OrderBook)(nil)
type MarketMaker struct {
ctx context.Context
die context.CancelFunc
running atomic.Bool
log dex.Logger
core *core.Core
bh *balanceHandler
running atomic.Bool
// syncedOracle is only available while the MarketMaker is running.
syncedOracle *priceOracle
unsyncedOracle *priceOracle

runningBotsMtx sync.RWMutex
runningBots map[string]interface{}

noteMtx sync.RWMutex
noteChans map[uint64]chan core.Notification
}

type MarketWithHost struct {
Host string `json:"host"`
Base uint32 `json:"base"`
Quote uint32 `json:"quote"`
}

func (m *MarketWithHost) String() string {
return fmt.Sprintf("%s-%d-%d", m.Host, m.Base, m.Quote)
}

func parseMarketWithHost(mkt string) (*MarketWithHost, error) {
parts := strings.Split(mkt, "-")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid market %s", mkt)
}
host := parts[0]
base64, err := strconv.ParseUint(parts[1], 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid market %s", mkt)
}
quote64, err := strconv.ParseUint(parts[2], 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid market %s", mkt)
}
return &MarketWithHost{
Host: host,
Base: uint32(base64),
Quote: uint32(quote64),
}, nil
}

// Running returns true if the MarketMaker is running.
// Running returns if the market maker is running.
func (m *MarketMaker) Running() bool {
return m.running.Load()
}

// RunningBots returns the markets on which a bot is running.
func (m *MarketMaker) RunningBots() []*MarketWithHost {
m.runningBotsMtx.RLock()
defer m.runningBotsMtx.RUnlock()

mkts := make([]*MarketWithHost, 0, len(m.runningBots))
for mkt := range m.runningBots {
mktWithHost, err := parseMarketWithHost(mkt)
if err != nil {
m.log.Errorf("failed to parse market %s: %v", mkt, err)
continue
}
mkts = append(mkts, mktWithHost)
}

return mkts
}

func marketsRequiringPriceOracle(cfgs []*BotConfig) []*mkt {
mkts := make([]*mkt, 0, len(cfgs))

Expand Down Expand Up @@ -93,7 +153,7 @@ func priceOracleFromConfigs(ctx context.Context, cfgs []*BotConfig, log dex.Logg
var err error
marketsRequiringOracle := marketsRequiringPriceOracle(cfgs)
if len(marketsRequiringOracle) > 0 {
oracle, err = newPriceOracle(ctx, marketsRequiringOracle, log)
oracle, err = newAutoSyncPriceOracle(ctx, marketsRequiringOracle, log)
if err != nil {
return nil, fmt.Errorf("failed to create PriceOracle: %v", err)
}
Expand All @@ -102,6 +162,50 @@ func priceOracleFromConfigs(ctx context.Context, cfgs []*BotConfig, log dex.Logg
return oracle, nil
}

func (m *MarketMaker) markBotAsRunning(id string, running bool) {
m.runningBotsMtx.Lock()
if running {
m.runningBots[id] = struct{}{}
} else {
delete(m.runningBots, id)
}

if len(m.runningBots) == 0 {
m.die()
}
m.runningBotsMtx.Unlock()
}

func (m *MarketMaker) MarketReport(base, quote uint32) (*MarketReport, error) {
user := m.core.User()
baseFiatRate := user.FiatRates[base]
quoteFiatRate := user.FiatRates[quote]

if m.running.Load() {
price, oracles, err := m.syncedOracle.GetOracleInfo(base, quote)
if err == nil {
return &MarketReport{
Price: price,
Oracles: oracles,
BaseFiatRate: baseFiatRate,
QuoteFiatRate: quoteFiatRate,
}, nil
}
}

price, oracles, err := m.unsyncedOracle.GetOracleInfo(base, quote)
if err != nil {
return nil, err
}

return &MarketReport{
Price: price,
Oracles: oracles,
BaseFiatRate: baseFiatRate,
QuoteFiatRate: quoteFiatRate,
}, nil
}

// Run starts the MarketMaker. There can only be one BotConfig per dex market.
func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) error {
if !m.running.CompareAndSwap(false, true) {
Expand All @@ -115,9 +219,19 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
}
}()

enabledCfgs := make([]*BotConfig, 0, len(cfgs))
for _, cfg := range cfgs {
if !cfg.Disabled {
enabledCfgs = append(enabledCfgs, cfg)
}
}
if len(enabledCfgs) == 0 {
return errors.New("no enabled bots")
}

m.ctx, m.die = context.WithCancel(ctx)

if err := duplicateBotConfig(cfgs); err != nil {
if err := duplicateBotConfig(enabledCfgs); err != nil {
return err
}

Expand All @@ -127,7 +241,7 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
}

unlocked := make(map[uint32]interface{})
for _, cfg := range cfgs {
for _, cfg := range enabledCfgs {
if _, done := unlocked[cfg.BaseAsset]; !done {
err := m.core.OpenWallet(cfg.BaseAsset, pw)
if err != nil {
Expand All @@ -145,19 +259,21 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
}
}

oracle, err := priceOracleFromConfigs(m.ctx, cfgs, m.log.SubLogger("PriceOracle"))
oracle, err := priceOracleFromConfigs(m.ctx, enabledCfgs, m.log.SubLogger("PriceOracle"))
if err != nil {
return err
}
m.syncedOracle = oracle

m.bh, err = newBalanceHandler(cfgs, m.core, m.log.SubLogger("BalanceHandler"))
m.bh, err = newBalanceHandler(enabledCfgs, m.core, m.log.SubLogger("BalanceHandler"))
if err != nil {
return err
}

user := m.core.User()

startedMarketMaking = true
m.notify(newMMStartStopNote(true))

wg := new(sync.WaitGroup)
wg.Add(1)
Expand All @@ -166,19 +282,29 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
m.bh.run(m.ctx)
}()

for _, cfg := range cfgs {
for _, cfg := range enabledCfgs {
switch {
case cfg.MMCfg != nil:
wg.Add(1)
go func(cfg *BotConfig) {
mkt := &MarketWithHost{cfg.Host, cfg.BaseAsset, cfg.QuoteAsset}
m.markBotAsRunning(mkt.String(), true)
defer func() {
m.markBotAsRunning(mkt.String(), false)
}()

m.notify(newBotStartStopNote(cfg.Host, cfg.BaseAsset, cfg.QuoteAsset, true))
defer func() {
m.notify(newBotStartStopNote(cfg.Host, cfg.BaseAsset, cfg.QuoteAsset, false))
}()
logger := m.log.SubLogger(fmt.Sprintf("MarketMaker-%s-%d-%d", cfg.Host, cfg.BaseAsset, cfg.QuoteAsset))
mktID := dexMarketID(cfg.Host, cfg.BaseAsset, cfg.QuoteAsset)
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)
RunBasicMarketMaker(m.ctx, cfg, m.bh.wrappedCoreForBot(mktID), oracle, baseFiatRate, quoteFiatRate, logger, m.notify)
wg.Done()
}(cfg)
default:
Expand All @@ -190,6 +316,7 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
wg.Wait()
m.log.Infof("All bots have stopped running.")
m.running.Store(false)
m.notify(newMMStartStopNote(false))
}()

return nil
Expand All @@ -212,11 +339,13 @@ func (m *MarketMaker) Stop() {
}
}

// NewMarketMaker creates a new MarketMaker.
func NewMarketMaker(core *core.Core, log dex.Logger) (*MarketMaker, error) {
func NewMarketMaker(c *core.Core, log dex.Logger) (*MarketMaker, error) {
return &MarketMaker{
core: core,
log: log,
running: atomic.Bool{},
core: c,
log: log,
running: atomic.Bool{},
runningBots: make(map[string]interface{}),
noteChans: make(map[uint64]chan core.Notification),
unsyncedOracle: newUnsyncedPriceOracle(log),
}, nil
}
10 changes: 6 additions & 4 deletions client/mm/mm_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ type MarketMakingConfig struct {

// EmptyMarketRate can be set if there is no market data available, and is
// ignored if there is market data available.
EmptyMarketRate float64 `json:"manualRate"`
EmptyMarketRate float64 `json:"emptyMarketRate"`

// SplitTxAllowed indicates whether the wallet (if it supports multi-splits)
// is allowed to do multi split transactions when funding orders. For UTXO
Expand Down Expand Up @@ -289,7 +289,7 @@ func basisPrice(book dexOrderBook, oracle oracle, cfg *MarketMakingConfig, mkt *
var oracleWeighting, oraclePrice float64
if cfg.OracleWeighting != nil && *cfg.OracleWeighting > 0 {
oracleWeighting = *cfg.OracleWeighting
oraclePrice = oracle.getMarketPrice(mkt.BaseID, mkt.QuoteID)
oraclePrice = oracle.GetMarketPrice(mkt.BaseID, mkt.QuoteID)
if oraclePrice == 0 {
log.Warnf("no oracle price available for %s bot", mkt.Name)
}
Expand Down Expand Up @@ -533,6 +533,7 @@ func orderPrice(basisPrice, breakEven uint64, strategy GapStrategy, factor float
if sell {
return basisPrice + halfSpread
}

return basisPrice - halfSpread
}

Expand Down Expand Up @@ -834,7 +835,8 @@ func (m *basicMarketMaker) run() {
}

// RunBasicMarketMaker starts a basic market maker bot.
func RunBasicMarketMaker(ctx context.Context, cfg *BotConfig, c clientCore, oracle oracle, baseFiatRate, quoteFiatRate float64, log dex.Logger) {
func RunBasicMarketMaker(ctx context.Context, cfg *BotConfig, c clientCore, oracle oracle, baseFiatRate, quoteFiatRate float64, log dex.Logger,
notify func(core.Notification)) {
if cfg.MMCfg == nil {
// implies bug in caller
log.Errorf("No market making config provided. Exiting.")
Expand All @@ -843,7 +845,7 @@ func RunBasicMarketMaker(ctx context.Context, cfg *BotConfig, c clientCore, orac

err := cfg.MMCfg.Validate()
if err != nil {
log.Errorf("Invalid market making config: %v. Exiting.", err)
notify(newValidationErrorNote(cfg.Host, cfg.BaseAsset, cfg.QuoteAsset, fmt.Sprintf("invalid market making config: %v", err)))
return
}

Expand Down
Loading

0 comments on commit fc4caff

Please sign in to comment.