Skip to content

Commit

Permalink
More review fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
martonp committed Sep 12, 2023
1 parent 42a3766 commit 78db6dd
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 139 deletions.
18 changes: 15 additions & 3 deletions client/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ type LogConfig struct {
LocalLogs bool `long:"loglocal" description:"Use local time zone time stamps in log entries."`
}

// MMConfig encapsulates the settings specific to market making.
type MMConfig struct {
BotConfigPath string `long:"botConfigPath"`
}

// Config is the common application configuration definition. This composite
// struct captures the configuration needed for core and both web and rpc
// servers, as well as some application-level directives.
Expand All @@ -129,6 +134,7 @@ type Config struct {
RPCConfig
WebConfig
LogConfig
MMConfig
// AppData and ConfigPath should be parsed from the command-line,
// as it makes no sense to set these in the config file itself. If no values
// are assigned, defaults will be used.
Expand Down Expand Up @@ -166,12 +172,9 @@ func (cfg *Config) Web(c *core.Core, mm *mm.MarketMaker, log dex.Logger, utc boo
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 @@ -205,6 +208,15 @@ func (cfg *Config) Core(log dex.Logger) *core.Config {
}
}

// MarketMakerConfigPath returns the path to the market maker config file.
func (cfg *Config) MarketMakerConfigPath() string {
if cfg.MMConfig.BotConfigPath != "" {
return cfg.MMConfig.BotConfigPath
}
_, _, mmCfgPath := setNet(cfg.AppData, cfg.Net.String())
return mmCfgPath
}

var DefaultConfig = Config{
AppData: defaultApplicationDirectory,
ConfigPath: defaultConfigPath,
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 @@ -100,7 +100,7 @@ func runCore(cfg *app.Config) error {
if cfg.Experimental {
// TODO: on shutdown, stop market making and wait for trades to be
// canceled.
marketMaker, err = mm.NewMarketMaker(clientCore, logMaker.Logger("MM"))
marketMaker, err = mm.NewMarketMaker(clientCore, cfg.MarketMakerConfigPath(), logMaker.Logger("MM"))
if err != nil {
return fmt.Errorf("error creating market maker: %w", err)
}
Expand Down
6 changes: 6 additions & 0 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -10281,6 +10281,12 @@ func (c *Core) FiatRateSources() map[string]bool {
return rateSources
}

// FiatConversionRates are the currently cached fiat conversion rates. Must have
// 1 or more fiat rate sources enabled.
func (c *Core) FiatConversionRates() map[uint32]float64 {
return c.fiatConversions()
}

// fiatConversions returns fiat rate for all supported assets that have a
// wallet.
func (c *Core) fiatConversions() map[uint32]float64 {
Expand Down
120 changes: 109 additions & 11 deletions client/mm/mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package mm
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"sync/atomic"

Expand Down Expand Up @@ -38,6 +40,7 @@ type clientCore interface {
Login(pw []byte) error
OpenWallet(assetID uint32, appPW []byte) error
Broadcast(core.Notification)
FiatConversionRates() map[uint32]float64
}

var _ clientCore = (*core.Core)(nil)
Expand Down Expand Up @@ -155,34 +158,38 @@ type MarketMaker struct {
die context.CancelFunc
running atomic.Bool
log dex.Logger
dir string
core clientCore
doNotKillWhenBotsStop bool // used for testing
botBalances map[string]*botBalances
// syncedOracle is only available while the MarketMaker is running.
cfgPath string
// syncedOracle is only available while the MarketMaker is running. It
// periodically refreshes the prices for the markets that have bots
// running on them.
syncedOracleMtx sync.RWMutex
syncedOracle *priceOracle

// unsyncedOracle is always available and can be used to query prices on
// all markets. It does not periodically refresh the prices, and queries
// them on demand.
unsyncedOracle *priceOracle

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

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

ordersMtx sync.RWMutex
orders map[order.OrderID]*orderInfo
}

// NewMarketMaker creates a new MarketMaker.
func NewMarketMaker(c clientCore, log dex.Logger) (*MarketMaker, error) {
func NewMarketMaker(c clientCore, cfgPath string, log dex.Logger) (*MarketMaker, error) {
return &MarketMaker{
core: c,
log: log,
cfgPath: cfgPath,
running: atomic.Bool{},
orders: make(map[order.OrderID]*orderInfo),
runningBots: make(map[MarketWithHost]interface{}),
noteChans: make(map[uint64]chan core.Notification),
unsyncedOracle: newUnsyncedPriceOracle(log),
}, nil
}
Expand Down Expand Up @@ -275,18 +282,18 @@ func (m *MarketMaker) markBotAsRunning(mkt MarketWithHost, running bool) {
// MarketReport returns information about the oracle rates on a market
// pair and the fiat rates of the base and quote assets.
func (m *MarketMaker) MarketReport(base, quote uint32) (*MarketReport, error) {
user := m.core.User()
baseFiatRate := user.FiatRates[base]
quoteFiatRate := user.FiatRates[quote]
fiatRates := m.core.FiatConversionRates()
baseFiatRate := fiatRates[base]
quoteFiatRate := fiatRates[quote]

m.syncedOracleMtx.RLock()
if m.syncedOracle != nil {
price, oracles, err := m.syncedOracle.getOracleInfo(base, quote)
m.syncedOracleMtx.RUnlock()
if err != nil && !errors.Is(err, errUnsyncedMarket) {
m.log.Errorf("failed to get oracle info for market %d-%d: %v", base, quote, err)
}
if err == nil {
m.syncedOracleMtx.RUnlock()
return &MarketReport{
Price: price,
Oracles: oracles,
Expand Down Expand Up @@ -820,10 +827,18 @@ func (m *MarketMaker) handleNotification(n core.Notification) {
}

// 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 {
func (m *MarketMaker) Run(ctx context.Context, pw []byte, alternateConfigPath *string) error {
if !m.running.CompareAndSwap(false, true) {
return errors.New("market making is already running")
}
path := m.cfgPath
if alternateConfigPath != nil {
path = *alternateConfigPath
}
cfgs, err := getMarketMakingConfig(path)
if err != nil {
return fmt.Errorf("error getting market making config: %v", err)
}

var startedMarketMaking bool
defer func() {
Expand Down Expand Up @@ -926,6 +941,89 @@ func (m *MarketMaker) Run(ctx context.Context, cfgs []*BotConfig, pw []byte) err
return nil
}

func getMarketMakingConfig(path string) ([]*BotConfig, error) {
cfg := []*BotConfig{}
if path == "" {
return cfg, nil
}
data, err := os.ReadFile(path)
if err == nil {
return cfg, json.Unmarshal(data, &cfg)
}
if os.IsNotExist(err) {
return cfg, nil
}
return nil, fmt.Errorf("error reading file: %w", err)
}

// GetMarketMakingConfig returns the market making config.
func (m *MarketMaker) GetMarketMakingConfig() ([]*BotConfig, error) {
return getMarketMakingConfig(m.cfgPath)
}

// UpdateMarketMakingConfig updates the market making config for one of the
// bots.
func (m *MarketMaker) UpdateMarketMakingConfig(updatedCfg *BotConfig) ([]*BotConfig, error) {
cfg, err := m.GetMarketMakingConfig()
if err != nil {
return nil, fmt.Errorf("error getting market making config: %v", err)
}

var updated bool
for i, c := range cfg {
if c.Host == updatedCfg.Host && c.QuoteAsset == updatedCfg.QuoteAsset && c.BaseAsset == updatedCfg.BaseAsset {
cfg[i] = updatedCfg
updated = true
break
}
}
if !updated {
cfg = append(cfg, updatedCfg)
}

data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return nil, fmt.Errorf("error marshalling market making config: %v", err)
}

err = os.WriteFile(m.cfgPath, data, 0644)
if err != nil {
return nil, fmt.Errorf("error writing market making config: %v", err)
}
return cfg, nil
}

// RemoveConfig removes a bot config from the market making config.
func (m *MarketMaker) RemoveConfig(host string, baseID, quoteID uint32) ([]*BotConfig, error) {
cfg, err := m.GetMarketMakingConfig()
if err != nil {
return nil, fmt.Errorf("error getting market making config: %v", err)
}

var updated bool
for i, c := range cfg {
if c.Host == host && c.QuoteAsset == quoteID && c.BaseAsset == baseID {
cfg = append(cfg[:i], cfg[i+1:]...)
updated = true
break
}
}
if !updated {
return nil, fmt.Errorf("config not found")
}

data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return nil, fmt.Errorf("error marshalling market making config: %v", err)
}

err = os.WriteFile(m.cfgPath, data, 0644)
if err != nil {
return nil, fmt.Errorf("error writing market making config: %v", err)
}
return cfg, nil
}

// Stop stops the MarketMaker.
func (m *MarketMaker) Stop() {
if m.die != nil {
Expand Down
53 changes: 37 additions & 16 deletions client/mm/mm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"math/rand"
"os"
"path/filepath"
"reflect"
"testing"
"time"
Expand Down Expand Up @@ -238,6 +240,10 @@ func (c *tCore) User() *core.User {

func (c *tCore) Broadcast(core.Notification) {}

func (c *tCore) FiatConversionRates() map[uint32]float64 {
return nil
}

var _ clientCore = (*tCore)(nil)

func tMaxOrderEstimate(lots uint64, swapFees, redeemFees uint64) *core.MaxOrderEstimate {
Expand Down Expand Up @@ -303,6 +309,19 @@ func (o *tOracle) getMarketPrice(base, quote uint32) float64 {
return o.marketPrice
}

func tNewMarketMaker(t *testing.T, c clientCore) (*MarketMaker, func()) {
t.Helper()
dir, _ := os.MkdirTemp("", "")
cfgPath := filepath.Join(dir, "mm.conf")
mm, err := NewMarketMaker(c, cfgPath, tLogger)
if err != nil {
if err != nil {
t.Fatalf("constructor error: %v", err)
}
}
return mm, func() { os.RemoveAll(dir) }
}

var tLogger = dex.StdOutLogger("mm_TEST", dex.LevelTrace)

func TestSetupBalances(t *testing.T) {
Expand All @@ -311,14 +330,15 @@ func TestSetupBalances(t *testing.T) {
dcrBtcID := fmt.Sprintf("%s-%d-%d", "host1", 42, 0)
dcrEthID := fmt.Sprintf("%s-%d-%d", "host1", 42, 60)

tests := []struct {
type ttest struct {
name string
cfgs []*BotConfig
assetBalances map[uint32]uint64

wantReserves map[string]map[uint32]uint64
wantErr bool
}{
}
tests := []*ttest{
{
name: "percentages only, ok",
cfgs: []*BotConfig{
Expand Down Expand Up @@ -465,20 +485,18 @@ func TestSetupBalances(t *testing.T) {
},
}

for _, test := range tests {
runTest := func(test *ttest) {
tCore.setAssetBalances(test.assetBalances)

mm, err := NewMarketMaker(tCore, tLogger)
if err != nil {
t.Fatalf("%s: unexpected error: %v", test.name, err)
}
mm, done := tNewMarketMaker(t, tCore)
defer done()

err = mm.setupBalances(test.cfgs)
err := mm.setupBalances(test.cfgs)
if test.wantErr {
if err == nil {
t.Fatalf("%s: expected error, got nil", test.name)
}
continue
return
}
if err != nil {
t.Fatalf("%s: unexpected error: %v", test.name, err)
Expand All @@ -495,6 +513,10 @@ func TestSetupBalances(t *testing.T) {
}
}
}

for _, test := range tests {
runTest(test)
}
}

func TestSegregatedCoreMaxSell(t *testing.T) {
Expand Down Expand Up @@ -750,7 +772,7 @@ func TestSegregatedCoreMaxSell(t *testing.T) {
tCore.sellRedeemFees = test.redeemFees
tCore.sellRefundFees = test.refundFees

mm, err := NewMarketMaker(tCore, tLogger)
mm, err := NewMarketMaker(tCore, "", tLogger)
if err != nil {
t.Fatalf("%s: unexpected error: %v", test.name, err)
}
Expand Down Expand Up @@ -1056,7 +1078,7 @@ func TestSegregatedCoreMaxBuy(t *testing.T) {
tCore.buySwapFees = test.swapFees
tCore.buyRedeemFees = test.redeemFees

mm, err := NewMarketMaker(tCore, tLogger)
mm, err := NewMarketMaker(tCore, "", tLogger)
if err != nil {
t.Fatalf("%s: unexpected error: %v", test.name, err)
}
Expand Down Expand Up @@ -4087,14 +4109,13 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) {
}
tCore.noteFeed = make(chan core.Notification)

mm, err := NewMarketMaker(tCore, tLogger)
if err != nil {
t.Fatalf("%s: unexpected error: %v", test.name, err)
}
mm, done := tNewMarketMaker(t, tCore)
defer done()
mm.doNotKillWhenBotsStop = true
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = mm.Run(ctx, []*BotConfig{test.cfg}, []byte{})
mm.UpdateMarketMakingConfig(test.cfg)
err := mm.Run(ctx, []byte{}, nil)
if err != nil {
t.Fatalf("%s: unexpected error: %v", test.name, err)
}
Expand Down
Loading

0 comments on commit 78db6dd

Please sign in to comment.