decred.org/dcrdex@v1.0.3/client/mm/mm.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package mm 5 6 import ( 7 "context" 8 "encoding/hex" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "os" 13 "sync" 14 "time" 15 16 "decred.org/dcrdex/client/asset" 17 "decred.org/dcrdex/client/core" 18 "decred.org/dcrdex/client/mm/libxc" 19 "decred.org/dcrdex/client/orderbook" 20 "decred.org/dcrdex/dex" 21 "decred.org/dcrdex/dex/order" 22 ) 23 24 // clientCore is satisfied by core.Core. 25 type clientCore interface { 26 NotificationFeed() *core.NoteFeed 27 ExchangeMarket(host string, baseID, quoteID uint32) (*core.Market, error) 28 SyncBook(host string, baseID, quoteID uint32) (*orderbook.OrderBook, core.BookFeed, error) 29 SupportedAssets() map[uint32]*core.SupportedAsset 30 SingleLotFees(form *core.SingleLotFeesForm) (uint64, uint64, uint64, error) 31 Cancel(oidB dex.Bytes) error 32 AssetBalance(assetID uint32) (*core.WalletBalance, error) 33 WalletTraits(assetID uint32) (asset.WalletTrait, error) 34 MultiTrade(pw []byte, form *core.MultiTradeForm) []*core.MultiTradeResult 35 MaxFundingFees(fromAsset uint32, host string, numTrades uint32, fromSettings map[string]string) (uint64, error) 36 Login(pw []byte) error 37 OpenWallet(assetID uint32, appPW []byte) error 38 Broadcast(core.Notification) 39 FiatConversionRates() map[uint32]float64 40 Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) 41 NewDepositAddress(assetID uint32) (string, error) 42 Network() dex.Network 43 Order(oidB dex.Bytes) (*core.Order, error) 44 WalletTransaction(uint32, string) (*asset.WalletTransaction, error) 45 TradingLimits(host string) (userParcels, parcelLimit uint32, err error) 46 WalletState(assetID uint32) *core.WalletState 47 Exchange(host string) (*core.Exchange, error) 48 } 49 50 var _ clientCore = (*core.Core)(nil) 51 52 // dexOrderBook is satisfied by orderbook.OrderBook. 53 // Avoids having to mock the entire orderbook in tests. 54 type dexOrderBook interface { 55 MidGap() (uint64, error) 56 VWAP(lots, lotSize uint64, sell bool) (avg, extrema uint64, filled bool, err error) 57 } 58 59 var _ dexOrderBook = (*orderbook.OrderBook)(nil) 60 61 // MarketWithHost represents a market on a specific dex server. 62 type MarketWithHost struct { 63 Host string `json:"host"` 64 BaseID uint32 `json:"baseID"` 65 QuoteID uint32 `json:"quoteID"` 66 } 67 68 func (m MarketWithHost) String() string { 69 return fmt.Sprintf("%s-%d-%d", m.Host, m.BaseID, m.QuoteID) 70 } 71 72 func (m MarketWithHost) ID() string { 73 n, _ := dex.MarketName(m.BaseID, m.QuoteID) 74 return n 75 } 76 77 // centralizedExchange is used to manage an exchange API connection. 78 type centralizedExchange struct { 79 libxc.CEX 80 *CEXConfig 81 82 mtx sync.RWMutex 83 cm *dex.ConnectionMaster 84 mkts map[string]*libxc.Market 85 balances map[uint32]*libxc.ExchangeBalance 86 connectErr string 87 } 88 89 // mtx must be locked 90 func (c *centralizedExchange) balancesCopy() map[uint32]*libxc.ExchangeBalance { 91 bs := make(map[uint32]*libxc.ExchangeBalance, len(c.balances)) 92 for assetID, bal := range c.balances { 93 bs[assetID] = bal 94 } 95 return bs 96 } 97 98 // bot is an interface used by the MarketMaker to access functions in order to 99 // check balances and update the bot configuration. An interface is created to 100 // simplify testing. 101 type bot interface { 102 dex.Connector 103 refreshAllPendingEvents(context.Context) 104 DEXBalance(assetID uint32) *BotBalance 105 CEXBalance(assetID uint32) *BotBalance 106 stats() *RunStats 107 latestEpoch() *EpochReport 108 latestCEXProblems() *CEXProblems 109 updateConfig(cfg *BotConfig) error 110 updateInventory(balanceDiffs *BotInventoryDiffs) 111 withPause(func() error) error 112 timeStart() int64 113 botCfg() *BotConfig 114 Book() (buys, sells []*core.MiniOrder, _ error) 115 } 116 117 type runningBot struct { 118 bot 119 cm *dex.ConnectionMaster 120 cexCfg *CEXConfig 121 } 122 123 func (rb *runningBot) assets() map[uint32]interface{} { 124 assets := make(map[uint32]interface{}) 125 cfg := rb.botCfg() 126 assets[cfg.BaseID] = struct{}{} 127 assets[cfg.QuoteID] = struct{}{} 128 assets[feeAssetID(cfg.BaseID)] = struct{}{} 129 assets[feeAssetID(cfg.QuoteID)] = struct{}{} 130 131 return assets 132 } 133 134 func (rb *runningBot) cexName() string { 135 return rb.botCfg().CEXName 136 } 137 138 // MarketMaker handles the market making process. It supports running different 139 // strategies on different markets. 140 type MarketMaker struct { 141 ctx context.Context 142 log dex.Logger 143 core clientCore 144 defaultCfgPath string 145 eventLogDBPath string 146 eventLogDB eventLogDB 147 oracle *priceOracle 148 149 defaultCfgMtx sync.RWMutex 150 // defaultCfg is the configuration specified by the file at the path passed 151 // to NewMarketMaker as an argument. An alternateCfgPath can be passed to 152 // some functions to use a different config file (this is how the 153 // MarketMaker is used from the CLI). 154 defaultCfg *MarketMakingConfig 155 156 runningBotsMtx sync.RWMutex 157 runningBots map[MarketWithHost]*runningBot 158 159 // startUpdateMtx is used to prevent starting or updating bots concurrently. 160 startUpdateMtx sync.Mutex 161 162 cexMtx sync.RWMutex 163 cexes map[string]*centralizedExchange 164 } 165 166 // NewMarketMaker creates a new MarketMaker. 167 func NewMarketMaker(c clientCore, eventLogDBPath, cfgPath string, log dex.Logger) (*MarketMaker, error) { 168 var cfg MarketMakingConfig 169 if b, err := os.ReadFile(cfgPath); err != nil && !os.IsNotExist(err) { 170 return nil, fmt.Errorf("error reading config file from %q: %w", cfgPath, err) 171 } else if len(b) > 0 { 172 if err := json.Unmarshal(b, &cfg); err != nil { 173 return nil, fmt.Errorf("error unmarshaling config file: %v", err) 174 } 175 } 176 177 return &MarketMaker{ 178 core: c, 179 log: log, 180 defaultCfgPath: cfgPath, 181 defaultCfg: &cfg, 182 eventLogDBPath: eventLogDBPath, 183 runningBots: make(map[MarketWithHost]*runningBot), 184 cexes: make(map[string]*centralizedExchange), 185 }, nil 186 } 187 188 // runningBotsLookup returns a lookup map for running bots. 189 func (m *MarketMaker) runningBotsLookup() map[MarketWithHost]*runningBot { 190 m.runningBotsMtx.RLock() 191 defer m.runningBotsMtx.RUnlock() 192 193 mkts := make(map[MarketWithHost]*runningBot, len(m.runningBots)) 194 for mkt, rb := range m.runningBots { 195 mkts[mkt] = rb 196 } 197 198 return mkts 199 } 200 201 // Status is state information about the MarketMaker. 202 type Status struct { 203 Bots []*BotStatus `json:"bots"` 204 CEXes map[string]*CEXStatus `json:"cexes"` 205 } 206 207 // CEXStatus is state information about a cex. 208 type CEXStatus struct { 209 Config *CEXConfig `json:"config"` 210 Connected bool `json:"connected"` 211 ConnectionError string `json:"connectErr"` 212 Markets map[string]*libxc.Market `json:"markets"` 213 Balances map[uint32]*libxc.ExchangeBalance `json:"balances"` 214 } 215 216 // StampedError is an error with a timestamp. 217 type StampedError struct { 218 Stamp int64 `json:"stamp"` 219 Error string `json:"error"` 220 } 221 222 func (se *StampedError) isEqual(se2 *StampedError) bool { 223 if se == nil != (se2 == nil) { 224 return false 225 } 226 if se == nil { 227 return true 228 } 229 230 return se.Stamp == se2.Stamp && se.Error == se2.Error 231 } 232 233 func newStampedError(err error) *StampedError { 234 if err == nil { 235 return nil 236 } 237 return &StampedError{ 238 Stamp: time.Now().Unix(), 239 Error: err.Error(), 240 } 241 } 242 243 // BotProblems contains problems that prevent orders from being placed. 244 type BotProblems struct { 245 // WalletNotSynced is true if orders were unable to be placed due to a 246 // wallet not being synced. 247 WalletNotSynced map[uint32]bool `json:"walletNotSynced"` 248 // NoWalletPeers is true if orders were unable to be placed due to a wallet 249 // not having any peers. 250 NoWalletPeers map[uint32]bool `json:"noWalletPeers"` 251 // AccountSuspended is true if orders were unable to be placed due to the 252 // account being suspended. 253 AccountSuspended bool `json:"accountSuspended"` 254 // UserLimitTooLow is true if the user does not have the bonding amount 255 // necessary to place all of their orders. 256 UserLimitTooLow bool `json:"userLimitTooLow"` 257 // NoPriceSource is true if there is no oracle or fiat rate available. 258 NoPriceSource bool `json:"noPriceSource"` 259 // OracleFiatMismatch is true if the mid-gap is outside the oracle's 260 // safe range as defined by the config. 261 OracleFiatMismatch bool `json:"oracleFiatMismatch"` 262 // CEXOrderbookUnsynced is true if the CEX orderbook is unsynced. 263 CEXOrderbookUnsynced bool `json:"cexOrderbookUnsynced"` 264 // CausesSelfMatch is true if the order would cause a self match. 265 CausesSelfMatch bool `json:"causesSelfMatch"` 266 // UnknownError is set if an error occurred that was not one of the above. 267 UnknownError string `json:"unknownError"` 268 } 269 270 // EpochReport contains a report of a bot's activity during an epoch. 271 type EpochReport struct { 272 // PreOrderProblems is set if there were problems with the bot's 273 // configuration or state that prevents orders from being placed. 274 PreOrderProblems *BotProblems `json:"preOrderProblems"` 275 // BuysReport is the report for the buys. 276 BuysReport *OrderReport `json:"buysReport"` 277 // SellsReport is the report for the sells. 278 SellsReport *OrderReport `json:"sellsReport"` 279 // EpochNum is the number of the epoch. 280 EpochNum uint64 `json:"epochNum"` 281 } 282 283 func (er *EpochReport) setPreOrderProblems(err error) { 284 if err == nil { 285 er.PreOrderProblems = nil 286 return 287 } 288 289 er.PreOrderProblems = &BotProblems{} 290 updateBotProblemsBasedOnError(er.PreOrderProblems, err) 291 } 292 293 // CEXProblems contains a record of the last attempted CEX operations by 294 // a bot. 295 type CEXProblems struct { 296 // DepositErr is set if the last attempted deposit for an asset failed. 297 DepositErr map[uint32]*StampedError `json:"depositErr"` 298 // WithdrawErr is set if the last attempted withdrawal for an asset failed. 299 WithdrawErr map[uint32]*StampedError `json:"withdrawErr"` 300 // TradeErr is set if the last attempted CEX trade failed. 301 TradeErr *StampedError `json:"tradeErr"` 302 } 303 304 func (c *CEXProblems) copy() *CEXProblems { 305 cp := &CEXProblems{ 306 DepositErr: make(map[uint32]*StampedError, len(c.DepositErr)), 307 WithdrawErr: make(map[uint32]*StampedError, len(c.WithdrawErr)), 308 } 309 for assetID, err := range c.DepositErr { 310 if err == nil { 311 continue 312 } 313 cp.DepositErr[assetID] = &StampedError{ 314 Stamp: err.Stamp, 315 Error: err.Error, 316 } 317 } 318 for assetID, err := range c.WithdrawErr { 319 if err == nil { 320 continue 321 } 322 cp.WithdrawErr[assetID] = &StampedError{ 323 Stamp: err.Stamp, 324 Error: err.Error, 325 } 326 } 327 if c.TradeErr != nil { 328 cp.TradeErr = &StampedError{ 329 Stamp: c.TradeErr.Stamp, 330 Error: c.TradeErr.Error, 331 } 332 } 333 return cp 334 } 335 336 func newCEXProblems() *CEXProblems { 337 return &CEXProblems{ 338 DepositErr: make(map[uint32]*StampedError), 339 WithdrawErr: make(map[uint32]*StampedError), 340 } 341 } 342 343 // BotStatus is state information about a configured bot. 344 type BotStatus struct { 345 Config *BotConfig `json:"config"` 346 Running bool `json:"running"` 347 // RunStats being non-nil means the bot is running. 348 RunStats *RunStats `json:"runStats"` 349 LatestEpoch *EpochReport `json:"latestEpoch"` 350 CEXProblems *CEXProblems `json:"cexProblems"` 351 } 352 353 // Status generates a Status for the MarketMaker. This returns the status of 354 // all bots specified in the default config file. 355 func (m *MarketMaker) Status() *Status { 356 cfg := m.defaultConfig() 357 status := &Status{ 358 CEXes: make(map[string]*CEXStatus, len(cfg.CexConfigs)), 359 Bots: make([]*BotStatus, 0, len(cfg.BotConfigs)), 360 } 361 runningBots := m.runningBotsLookup() 362 for _, botCfg := range cfg.BotConfigs { 363 mkt := MarketWithHost{botCfg.Host, botCfg.BaseID, botCfg.QuoteID} 364 rb := runningBots[mkt] 365 var stats *RunStats 366 var epochReport *EpochReport 367 var cexProblems *CEXProblems 368 if rb != nil { 369 stats = rb.stats() 370 epochReport = rb.latestEpoch() 371 cexProblems = rb.latestCEXProblems() 372 } 373 status.Bots = append(status.Bots, &BotStatus{ 374 Config: botCfg, 375 Running: rb != nil, 376 RunStats: stats, 377 LatestEpoch: epochReport, 378 CEXProblems: cexProblems, 379 }) 380 } 381 for _, cex := range m.cexList() { 382 s := &CEXStatus{Config: cex.CEXConfig} 383 if cex != nil { 384 cex.mtx.RLock() 385 s.Connected = cex.cm != nil && cex.cm.On() 386 s.Markets = cex.mkts 387 s.ConnectionError = cex.connectErr 388 s.Balances = cex.balancesCopy() 389 cex.mtx.RUnlock() 390 } 391 status.CEXes[cex.Name] = s 392 } 393 return status 394 } 395 396 // RunningBotsStatus returns the status of all currently running bots. This 397 // should be used by the CLI which may have passed in an alternate config 398 // file when starting bots. 399 func (m *MarketMaker) RunningBotsStatus() *Status { 400 status := &Status{ 401 CEXes: make(map[string]*CEXStatus, 0), 402 Bots: make([]*BotStatus, 0), 403 } 404 runningBots := m.runningBotsLookup() 405 for _, rb := range runningBots { 406 status.Bots = append(status.Bots, &BotStatus{ 407 Config: rb.botCfg(), 408 Running: true, 409 RunStats: rb.stats(), 410 LatestEpoch: rb.latestEpoch(), 411 CEXProblems: rb.latestCEXProblems(), 412 }) 413 } 414 return status 415 } 416 417 func (m *MarketMaker) CEXBalance(cexName string, assetID uint32) (*libxc.ExchangeBalance, error) { 418 cfg := m.defaultConfig() 419 420 var cexCfg *CEXConfig 421 for _, cfg := range cfg.CexConfigs { 422 if cfg.Name == cexName { 423 cexCfg = cfg 424 break 425 } 426 } 427 if cexCfg == nil { 428 return nil, fmt.Errorf("no CEX config found for %s", cexName) 429 } 430 431 cex, err := m.loadAndConnectCEX(m.ctx, cexCfg) 432 if err != nil { 433 return nil, fmt.Errorf("error getting connected CEX: %w", err) 434 } 435 436 return cex.Balance(assetID) 437 } 438 439 // MarketReport returns information about the oracle rates on a market 440 // pair and the fiat rates of the base and quote assets. 441 func (m *MarketMaker) MarketReport(host string, baseID, quoteID uint32) (*MarketReport, error) { 442 fiatRates := m.core.FiatConversionRates() 443 baseFiatRate := fiatRates[baseID] 444 quoteFiatRate := fiatRates[quoteID] 445 446 price, oracles, err := m.oracle.getOracleInfo(baseID, quoteID) 447 if err != nil { 448 return nil, err 449 } 450 if price == 0 && baseFiatRate > 0 && quoteFiatRate > 0 { 451 price = baseFiatRate / quoteFiatRate 452 } 453 454 baseFeesEst, quoteFeesEst, err := marketFees(m.core, host, baseID, quoteID, false) 455 if err != nil { 456 return nil, err 457 } 458 459 baseFeesMax, quoteFeesMax, err := marketFees(m.core, host, baseID, quoteID, true) 460 if err != nil { 461 return nil, err 462 } 463 464 return &MarketReport{ 465 Price: price, 466 Oracles: oracles, 467 BaseFiatRate: baseFiatRate, 468 QuoteFiatRate: quoteFiatRate, 469 BaseFees: &LotFeeRange{ 470 Max: baseFeesMax, 471 Estimated: baseFeesEst, 472 }, 473 QuoteFees: &LotFeeRange{ 474 Max: quoteFeesMax, 475 Estimated: quoteFeesEst, 476 }, 477 }, nil 478 } 479 480 func (m *MarketMaker) loginAndUnlockWallets(pw []byte, cfg *BotConfig) error { 481 err := m.core.Login(pw) 482 if err != nil { 483 return fmt.Errorf("failed to login: %w", err) 484 } 485 486 err = m.core.OpenWallet(cfg.BaseID, pw) 487 if err != nil { 488 return fmt.Errorf("failed to unlock wallet for asset %d: %w", cfg.BaseID, err) 489 } 490 491 err = m.core.OpenWallet(cfg.QuoteID, pw) 492 if err != nil { 493 return fmt.Errorf("failed to unlock wallet for asset %d: %w", cfg.QuoteID, err) 494 } 495 496 return nil 497 } 498 499 func (m *MarketMaker) connectCEX(ctx context.Context, c *centralizedExchange) error { 500 var cm *dex.ConnectionMaster 501 c.mtx.Lock() 502 defer c.mtx.Unlock() 503 if c.cm == nil || !c.cm.On() { 504 cm = dex.NewConnectionMaster(c) 505 c.cm = cm 506 } else { 507 cm = c.cm 508 } 509 510 if !cm.On() { 511 c.connectErr = "" 512 if err := cm.ConnectOnce(ctx); err != nil { 513 c.connectErr = core.UnwrapErr(err).Error() 514 return fmt.Errorf("failed to connect to CEX: %w", err) 515 } 516 mkts, err := c.Markets(ctx) 517 if err != nil { 518 // Probably can't get here if we didn't error on connect, but 519 // checking anyway. 520 c.connectErr = core.UnwrapErr(err).Error() 521 return fmt.Errorf("error refreshing markets: %w", err) 522 } 523 c.mkts = mkts 524 bals, err := c.Balances(ctx) 525 if err != nil { 526 c.connectErr = core.UnwrapErr(err).Error() 527 return fmt.Errorf("error getting balances: %w", err) 528 } 529 c.balances = bals 530 } 531 532 return nil 533 } 534 535 // loadAndConnectCEX initializes the centralizedExchange if required, and 536 // connects if not already connected. 537 func (m *MarketMaker) loadAndConnectCEX(ctx context.Context, cfg *CEXConfig) (*centralizedExchange, error) { 538 c, err := m.loadCEX(ctx, cfg) 539 if err != nil { 540 return nil, fmt.Errorf("error loading CEX: %w", err) 541 } 542 543 if err := m.connectCEX(ctx, c); err != nil { 544 return nil, fmt.Errorf("error connecting to CEX: %w", err) 545 } 546 547 return c, nil 548 } 549 550 // loadCEX initializes the cex if required and returns the centralizedExchange. 551 func (m *MarketMaker) loadCEX(ctx context.Context, cfg *CEXConfig) (*centralizedExchange, error) { 552 m.cexMtx.Lock() 553 defer m.cexMtx.Unlock() 554 var success bool 555 if cex := m.cexes[cfg.Name]; cex != nil { 556 if cex.APIKey == cfg.APIKey && cex.APISecret == cfg.APISecret { 557 return cex, nil 558 } 559 if m.cexInUse(cfg.Name) { 560 return nil, fmt.Errorf("CEX %s already in use with different API key", cfg.Name) 561 } 562 // New credentials. Delete the old cex. 563 defer func() { 564 if success { 565 cex.mtx.Lock() 566 cex.cm.Disconnect() 567 cex.cm = nil 568 cex.mtx.Unlock() 569 } 570 }() 571 } 572 logger := m.log.SubLogger(fmt.Sprintf("CEX-%s", cfg.Name)) 573 cex, err := libxc.NewCEX(cfg.Name, &libxc.CEXConfig{ 574 APIKey: cfg.APIKey, 575 SecretKey: cfg.APISecret, 576 Logger: logger, 577 Net: m.core.Network(), 578 Notify: func(n interface{}) { 579 m.handleCEXUpdate(cfg.Name, n) 580 }, 581 }) 582 if err != nil { 583 return nil, fmt.Errorf("failed to create CEX: %v", err) 584 } 585 c := ¢ralizedExchange{ 586 CEX: cex, 587 CEXConfig: cfg, 588 } 589 c.mkts, err = cex.Markets(ctx) 590 if err != nil { 591 m.log.Errorf("Failed to get markets for %s: %v", cfg.Name, err) 592 c.mkts = make(map[string]*libxc.Market) 593 c.connectErr = core.UnwrapErr(err).Error() 594 } 595 if c.balances, err = c.Balances(ctx); err != nil { 596 m.log.Errorf("Failed to get balances for %s: %v", cfg.Name, err) 597 c.balances = make(map[uint32]*libxc.ExchangeBalance) 598 c.connectErr = core.UnwrapErr(err).Error() 599 } 600 m.cexes[cfg.Name] = c 601 success = true 602 return c, nil 603 } 604 605 func (m *MarketMaker) handleCEXUpdate(cexName string, ni interface{}) { 606 switch n := ni.(type) { 607 case *libxc.BalanceUpdate: 608 m.cexMtx.RLock() 609 cex := m.cexes[cexName] 610 m.cexMtx.RUnlock() 611 if cex == nil { 612 m.log.Errorf("CEX update received from unknown cex %q?", cexName) 613 return 614 } 615 cex.mtx.Lock() 616 cex.balances[n.AssetID] = n.Balance 617 cex.mtx.Unlock() 618 m.core.Broadcast(newCexUpdateNote(cexName, TopicBalanceUpdate, ni)) 619 } 620 } 621 622 // cexList generates a slice of configured centralizedExchange. 623 func (m *MarketMaker) cexList() []*centralizedExchange { 624 m.cexMtx.RLock() 625 defer m.cexMtx.RUnlock() 626 627 cexes := make([]*centralizedExchange, 0, len(m.cexes)) 628 for _, cex := range m.cexes { 629 cexes = append(cexes, cex) 630 } 631 632 return cexes 633 } 634 635 func (m *MarketMaker) defaultConfig() *MarketMakingConfig { 636 m.defaultCfgMtx.RLock() 637 defer m.defaultCfgMtx.RUnlock() 638 return m.defaultCfg.Copy() 639 } 640 641 func (m *MarketMaker) Connect(ctx context.Context) (*sync.WaitGroup, error) { 642 m.ctx = ctx 643 cfg := m.defaultConfig() 644 for _, cexCfg := range cfg.CexConfigs { 645 if c, err := m.loadCEX(ctx, cexCfg); err != nil { 646 m.log.Errorf("Error adding %s: %v", cexCfg.Name, err) 647 } else { 648 // Try to connect so we can update our balances and set the 649 // connected flag, but ignore errors. 650 if err := m.connectCEX(ctx, c); err != nil { 651 m.log.Infof("Could not connect to %q: %v", cexCfg.Name, err) 652 } 653 } 654 } 655 656 eventLogDB, err := newBoltEventLogDB(ctx, m.eventLogDBPath, m.log.SubLogger("eventlogdb")) 657 if err != nil { 658 return nil, fmt.Errorf("error creating event log DB: %v", err) 659 } 660 m.eventLogDB = eventLogDB 661 662 m.oracle = newPriceOracle(m.ctx, m.log.SubLogger("oracle")) 663 664 var wg sync.WaitGroup 665 666 wg.Add(1) 667 go func() { 668 defer wg.Done() 669 <-ctx.Done() 670 671 m.cexMtx.Lock() 672 defer m.cexMtx.Unlock() 673 674 for _, cex := range m.cexes { 675 cex.mtx.RLock() 676 cm := cex.cm 677 cex.mtx.RUnlock() 678 if cm != nil { 679 cm.Disconnect() 680 } 681 682 delete(m.cexes, cex.Name) 683 } 684 }() 685 686 return &wg, nil 687 } 688 689 func (m *MarketMaker) balancesSufficient(balances *BotBalanceAllocation, mkt *MarketWithHost, cexCfg *CEXConfig) error { 690 availableDEXBalances, availableCEXBalances, err := m.availableBalances(mkt, cexCfg) 691 if err != nil { 692 return fmt.Errorf("error getting available balances: %v", err) 693 } 694 695 for assetID, amount := range balances.DEX { 696 availableBalance := availableDEXBalances[assetID] 697 if amount > availableBalance { 698 return fmt.Errorf("insufficient DEX balance for %s: %d < %d", dex.BipIDSymbol(assetID), availableBalance, amount) 699 } 700 } 701 702 for assetID, amount := range balances.CEX { 703 availableBalance := availableCEXBalances[assetID] 704 if amount > availableBalance { 705 return fmt.Errorf("insufficient CEX balance for %s: %d < %d", dex.BipIDSymbol(assetID), availableBalance, amount) 706 } 707 } 708 709 return nil 710 } 711 712 // botCfgForMarket returns the configuration for a bot on a specific market. 713 // If alternateConfigPath is not nil, the configuration will be loaded from the 714 // file at that path. 715 func (m *MarketMaker) configsForMarket(mkt *MarketWithHost, alternateConfigPath *string) (botConfig *BotConfig, cexConfig *CEXConfig, err error) { 716 fullCfg := m.defaultConfig() 717 if alternateConfigPath != nil { 718 fullCfg, err = getMarketMakingConfig(*alternateConfigPath) 719 if err != nil { 720 return nil, nil, fmt.Errorf("error loading custom market making config: %v", err) 721 } 722 } 723 724 for _, c := range fullCfg.BotConfigs { 725 if c.Host == mkt.Host && c.BaseID == mkt.BaseID && c.QuoteID == mkt.QuoteID { 726 botConfig = c 727 } 728 } 729 if botConfig == nil { 730 return nil, nil, fmt.Errorf("no bot config found for %s", mkt) 731 } 732 733 if botConfig.CEXName != "" { 734 for _, c := range fullCfg.CexConfigs { 735 if c.Name == botConfig.CEXName { 736 cexConfig = c 737 } 738 } 739 if cexConfig == nil { 740 return nil, nil, fmt.Errorf("no CEX config found for %s", botConfig.CEXName) 741 } 742 } 743 744 return 745 } 746 747 func (m *MarketMaker) botSubLogger(cfg *BotConfig) dex.Logger { 748 mktID := dexMarketID(cfg.Host, cfg.BaseID, cfg.QuoteID) 749 switch { 750 case cfg.BasicMMConfig != nil: 751 return m.log.SubLogger(fmt.Sprintf("MM-%s", mktID)) 752 case cfg.SimpleArbConfig != nil: 753 return m.log.SubLogger(fmt.Sprintf("ARB-%s", mktID)) 754 case cfg.ArbMarketMakerConfig != nil: 755 return m.log.SubLogger(fmt.Sprintf("AMM-%s", mktID)) 756 } 757 // This will error in the caller. 758 return m.log.SubLogger(fmt.Sprintf("Bot-%s", mktID)) 759 } 760 761 func (m *MarketMaker) cexInUse(cexName string) bool { 762 runningBots := m.runningBotsLookup() 763 for _, bot := range runningBots { 764 if bot.cexName() == cexName { 765 return true 766 } 767 } 768 return false 769 } 770 771 func (m *MarketMaker) newBot(cfg *BotConfig, adaptorCfg *exchangeAdaptorCfg) (bot, error) { 772 mktID := dexMarketID(cfg.Host, cfg.BaseID, cfg.QuoteID) 773 switch { 774 case cfg.ArbMarketMakerConfig != nil: 775 return newArbMarketMaker(cfg, adaptorCfg, m.log.SubLogger(fmt.Sprintf("AMM-%s", mktID))) 776 case cfg.BasicMMConfig != nil: 777 return newBasicMarketMaker(cfg, adaptorCfg, m.oracle, m.log.SubLogger(fmt.Sprintf("MM-%s", mktID))) 778 case cfg.SimpleArbConfig != nil: 779 return newSimpleArbMarketMaker(cfg, adaptorCfg, m.log.SubLogger(fmt.Sprintf("ARB-%s", mktID))) 780 default: 781 return nil, fmt.Errorf("not bot config found") 782 } 783 } 784 785 // StartConfig contains the data that must be submitted with a call to StartBot. 786 type StartConfig struct { 787 MarketWithHost 788 AutoRebalance *AutoRebalanceConfig `json:"autoRebalance"` 789 Alloc *BotBalanceAllocation `json:"alloc"` 790 } 791 792 // StartBot starts a market making bot. 793 func (m *MarketMaker) StartBot(startCfg *StartConfig, alternateConfigPath *string, appPW []byte) (err error) { 794 mkt := startCfg.MarketWithHost 795 796 m.startUpdateMtx.Lock() 797 defer m.startUpdateMtx.Unlock() 798 799 m.runningBotsMtx.RLock() 800 _, found := m.runningBots[startCfg.MarketWithHost] 801 m.runningBotsMtx.RUnlock() 802 if found { 803 return fmt.Errorf("bot for %s already running", mkt) 804 } 805 806 coreMkt, err := m.core.ExchangeMarket(startCfg.Host, startCfg.BaseID, startCfg.QuoteID) 807 if err != nil { 808 return fmt.Errorf("error getting market: %v", err) 809 } 810 811 for _, ord := range coreMkt.Orders { 812 if ord.Status <= order.OrderStatusBooked { 813 err = m.core.Cancel(ord.ID) 814 if err != nil { 815 return fmt.Errorf("error canceling order %s: %v", ord.ID, err) 816 } 817 } 818 } 819 820 botCfg, cexCfg, err := m.configsForMarket(&startCfg.MarketWithHost, alternateConfigPath) 821 if err != nil { 822 return err 823 } 824 825 if botCfg.RPCConfig != nil { 826 startCfg.Alloc = botCfg.RPCConfig.Alloc 827 startCfg.AutoRebalance = botCfg.RPCConfig.AutoRebalance 828 } 829 830 return m.startBot(startCfg, botCfg, cexCfg, appPW) 831 } 832 833 func (m *MarketMaker) startBot(startCfg *StartConfig, botCfg *BotConfig, cexCfg *CEXConfig, appPW []byte) (err error) { 834 mwh := &startCfg.MarketWithHost 835 if err := m.balancesSufficient(startCfg.Alloc, mwh, cexCfg); err != nil { 836 return err 837 } 838 839 if err := m.loginAndUnlockWallets(appPW, botCfg); err != nil { 840 return err 841 } 842 843 var cex *centralizedExchange 844 if cexCfg != nil { 845 cex, err = m.loadAndConnectCEX(m.ctx, cexCfg) 846 if err != nil { 847 return fmt.Errorf("error loading %s: %w", cexCfg.Name, err) 848 } 849 } 850 851 var startedBot bool 852 853 requiresOracle := botCfg.requiresPriceOracle() 854 if requiresOracle { 855 err := m.oracle.startAutoSyncingMarket(botCfg.BaseID, botCfg.QuoteID) 856 if err != nil { 857 return err 858 } 859 defer func() { 860 if !startedBot { 861 m.oracle.stopAutoSyncingMarket(botCfg.BaseID, botCfg.QuoteID) 862 } 863 }() 864 } 865 866 adaptorCfg := &exchangeAdaptorCfg{ 867 botID: dexMarketID(botCfg.Host, botCfg.BaseID, botCfg.QuoteID), 868 mwh: mwh, 869 baseDexBalances: startCfg.Alloc.DEX, 870 baseCexBalances: startCfg.Alloc.CEX, 871 autoRebalanceConfig: startCfg.AutoRebalance, 872 core: m.core, 873 cex: cex, 874 log: m.botSubLogger(botCfg), 875 botCfg: botCfg, 876 eventLogDB: m.eventLogDB, 877 } 878 879 bot, err := m.newBot(botCfg, adaptorCfg) 880 if err != nil { 881 return err 882 } 883 884 cm := dex.NewConnectionMaster(bot) 885 if err := cm.ConnectOnce(m.ctx); err != nil { 886 return fmt.Errorf("error connecting bot: %w", err) 887 } 888 889 go func() { 890 cm.Wait() 891 m.runningBotsMtx.Lock() 892 if bot, found := m.runningBots[*mwh]; found { 893 if bot.botCfg().requiresPriceOracle() { 894 m.oracle.stopAutoSyncingMarket(mwh.BaseID, mwh.QuoteID) 895 } 896 delete(m.runningBots, *mwh) 897 } 898 m.runningBotsMtx.Unlock() 899 m.core.Broadcast(newRunStatsNote(mwh.Host, mwh.BaseID, mwh.QuoteID, nil)) 900 }() 901 902 startedBot = true 903 904 rb := &runningBot{ 905 bot: bot, 906 cm: cm, 907 cexCfg: cexCfg, 908 } 909 910 m.runningBotsMtx.Lock() 911 m.runningBots[*mwh] = rb 912 m.runningBotsMtx.Unlock() 913 914 return nil 915 } 916 917 // StopBot stops a running bot. 918 func (m *MarketMaker) StopBot(mkt *MarketWithHost) error { 919 runningBots := m.runningBotsLookup() 920 bot, found := runningBots[*mkt] 921 if !found { 922 return fmt.Errorf("no bot running on market: %s", mkt) 923 } 924 bot.cm.Disconnect() 925 m.core.Broadcast(newRunStatsNote(mkt.Host, mkt.BaseID, mkt.QuoteID, nil)) 926 return nil 927 } 928 929 func getMarketMakingConfig(path string) (*MarketMakingConfig, error) { 930 if path == "" { 931 return nil, fmt.Errorf("no config file provided") 932 } 933 934 data, err := os.ReadFile(path) 935 if err != nil { 936 return nil, err 937 } 938 939 cfg := &MarketMakingConfig{} 940 err = json.Unmarshal(data, cfg) 941 if err != nil { 942 return nil, err 943 } 944 945 return cfg, nil 946 } 947 948 func (m *MarketMaker) writeConfigFile(cfg *MarketMakingConfig) error { 949 data, err := json.MarshalIndent(cfg, "", " ") 950 if err != nil { 951 return fmt.Errorf("error marshalling market making config: %v", err) 952 } 953 954 err = os.WriteFile(m.defaultCfgPath, data, 0644) 955 if err != nil { 956 return fmt.Errorf("error writing market making config: %v", err) 957 } 958 m.defaultCfgMtx.Lock() 959 m.defaultCfg = cfg 960 m.defaultCfgMtx.Unlock() 961 return nil 962 } 963 964 func (m *MarketMaker) updateDefaultBotConfig(updatedCfg *BotConfig) { 965 cfg := m.defaultConfig() 966 967 var updated bool 968 for i, c := range cfg.BotConfigs { 969 if c.Host == updatedCfg.Host && c.QuoteID == updatedCfg.QuoteID && c.BaseID == updatedCfg.BaseID { 970 cfg.BotConfigs[i] = updatedCfg 971 updated = true 972 break 973 } 974 } 975 if !updated { 976 cfg.BotConfigs = append(cfg.BotConfigs, updatedCfg) 977 } 978 979 if err := m.writeConfigFile(cfg); err != nil { 980 m.log.Errorf("Error saving configuration file: %v", err) 981 } 982 } 983 984 // UpdateBotConfig updates the configuration for one of the bots. 985 func (m *MarketMaker) UpdateBotConfig(updatedCfg *BotConfig) error { 986 m.runningBotsMtx.RLock() 987 _, running := m.runningBots[MarketWithHost{updatedCfg.Host, updatedCfg.BaseID, updatedCfg.QuoteID}] 988 m.runningBotsMtx.RUnlock() 989 if running { 990 return fmt.Errorf("call UpdateRunningBotCfg to update the config of a running bot") 991 } 992 993 m.updateDefaultBotConfig(updatedCfg) 994 return nil 995 } 996 997 func (m *MarketMaker) UpdateCEXConfig(updatedCfg *CEXConfig) error { 998 _, err := m.loadAndConnectCEX(m.ctx, updatedCfg) 999 if err != nil { 1000 return fmt.Errorf("error loading %s with updated config: %w", updatedCfg.Name, err) 1001 } 1002 1003 var updated bool 1004 m.defaultCfgMtx.Lock() 1005 for i, c := range m.defaultCfg.CexConfigs { 1006 if c.Name == updatedCfg.Name { 1007 m.defaultCfg.CexConfigs[i] = updatedCfg 1008 updated = true 1009 break 1010 } 1011 } 1012 if !updated { 1013 m.defaultCfg.CexConfigs = append(m.defaultCfg.CexConfigs, updatedCfg) 1014 } 1015 m.defaultCfgMtx.Unlock() 1016 1017 if err := m.writeConfigFile(m.defaultConfig()); err != nil { 1018 m.log.Errorf("Error saving new bot configuration: %w", err) 1019 } 1020 1021 return nil 1022 } 1023 1024 // RemoveConfig removes a bot config from the market making config. 1025 func (m *MarketMaker) RemoveBotConfig(host string, baseID, quoteID uint32) error { 1026 cfg := m.defaultConfig() 1027 1028 var updated bool 1029 for i, c := range cfg.BotConfigs { 1030 if c.Host == host && c.QuoteID == quoteID && c.BaseID == baseID { 1031 cfg.BotConfigs = append(cfg.BotConfigs[:i], cfg.BotConfigs[i+1:]...) 1032 updated = true 1033 break 1034 } 1035 } 1036 if !updated { 1037 return fmt.Errorf("config not found") 1038 } 1039 1040 if err := m.writeConfigFile(cfg); err != nil { 1041 m.log.Errorf("Error saving updated config file: %v", err) 1042 } 1043 1044 return nil 1045 } 1046 1047 func validRunningBotCfgUpdate(oldCfg, newCfg *BotConfig) error { 1048 if oldCfg.CEXName != "" && newCfg.CEXName == "" { 1049 return fmt.Errorf("cannot remove CEX config from running bot") 1050 } 1051 1052 if oldCfg.CEXName != "" && (oldCfg.CEXName != newCfg.CEXName) { 1053 return fmt.Errorf("cannot change CEX config for running bot") 1054 } 1055 1056 if oldCfg.BasicMMConfig == nil != (newCfg.BasicMMConfig == nil) { 1057 return fmt.Errorf("cannot change bot type for running bot") 1058 } 1059 1060 if oldCfg.SimpleArbConfig == nil != (newCfg.SimpleArbConfig == nil) { 1061 return fmt.Errorf("cannot change bot type for running bot") 1062 } 1063 1064 if oldCfg.ArbMarketMakerConfig == nil != (newCfg.ArbMarketMakerConfig == nil) { 1065 return fmt.Errorf("cannot change bot type for running bot") 1066 } 1067 1068 return nil 1069 } 1070 1071 // UpdateRunningBotInventory updates the inventory of a running bot. 1072 func (m *MarketMaker) UpdateRunningBotInventory(mkt *MarketWithHost, balanceDiffs *BotInventoryDiffs) error { 1073 m.startUpdateMtx.Lock() 1074 defer m.startUpdateMtx.Unlock() 1075 1076 m.runningBotsMtx.RLock() 1077 rb := m.runningBots[*mkt] 1078 m.runningBotsMtx.RUnlock() 1079 if rb == nil { 1080 return fmt.Errorf("no bot running on market: %s", mkt) 1081 } 1082 1083 if err := m.balancesSufficient(balanceDiffsToAllocation(balanceDiffs), mkt, rb.cexCfg); err != nil { 1084 return err 1085 } 1086 1087 if err := rb.withPause(func() error { 1088 rb.bot.updateInventory(balanceDiffs) 1089 return nil 1090 }); err != nil { 1091 rb.cm.Disconnect() 1092 return fmt.Errorf("configuration update error. bot stopped: %w", err) 1093 } 1094 return nil 1095 } 1096 1097 // UpdateRunningBotCfg updates the configuration and balance allocation for a 1098 // running bot. If saveUpdate is true, the update configuration will be saved 1099 // to the default config file. 1100 func (m *MarketMaker) UpdateRunningBotCfg(cfg *BotConfig, balanceDiffs *BotInventoryDiffs, saveUpdate bool) error { 1101 m.startUpdateMtx.Lock() 1102 defer m.startUpdateMtx.Unlock() 1103 1104 if cfg == nil { 1105 return fmt.Errorf("nil config") 1106 } 1107 1108 mkt := MarketWithHost{cfg.Host, cfg.BaseID, cfg.QuoteID} 1109 m.runningBotsMtx.RLock() 1110 rb := m.runningBots[mkt] 1111 m.runningBotsMtx.RUnlock() 1112 if rb == nil { 1113 return fmt.Errorf("no bot running on market: %s", mkt) 1114 } 1115 1116 oldCfg := rb.botCfg() 1117 if err := validRunningBotCfgUpdate(oldCfg, cfg); err != nil { 1118 return err 1119 } 1120 1121 if balanceDiffs != nil { 1122 if err := m.balancesSufficient(balanceDiffsToAllocation(balanceDiffs), &mkt, rb.cexCfg); err != nil { 1123 return err 1124 } 1125 } 1126 1127 var stoppedOracle, startedOracle, updateSuccess bool 1128 defer func() { 1129 if updateSuccess { 1130 return 1131 } 1132 if startedOracle { 1133 m.oracle.stopAutoSyncingMarket(cfg.BaseID, cfg.QuoteID) 1134 } else if stoppedOracle { 1135 err := m.oracle.startAutoSyncingMarket(oldCfg.BaseID, oldCfg.QuoteID) 1136 if err != nil { 1137 m.log.Errorf("Error restarting oracle for %s: %v", mkt, err) 1138 } 1139 } 1140 }() 1141 1142 if !oldCfg.requiresPriceOracle() && cfg.requiresPriceOracle() { 1143 err := m.oracle.startAutoSyncingMarket(cfg.BaseID, cfg.QuoteID) 1144 if err != nil { 1145 return err 1146 } 1147 startedOracle = true 1148 } else if oldCfg.requiresPriceOracle() && !cfg.requiresPriceOracle() { 1149 m.oracle.stopAutoSyncingMarket(cfg.BaseID, cfg.QuoteID) 1150 stoppedOracle = true 1151 } 1152 1153 if err := rb.withPause(func() error { 1154 if err := rb.updateConfig(cfg); err != nil { 1155 return err 1156 } 1157 if balanceDiffs != nil { 1158 rb.updateInventory(balanceDiffs) 1159 } 1160 return nil 1161 }); err != nil { 1162 rb.cm.Disconnect() 1163 return fmt.Errorf("running bot reconfiguration unsuccessful. bot stopped: %w", err) 1164 } 1165 1166 updateSuccess = true 1167 1168 return nil 1169 } 1170 1171 // ArchivedRuns returns all archived market making runs. 1172 func (m *MarketMaker) ArchivedRuns() ([]*MarketMakingRun, error) { 1173 allRuns, err := m.eventLogDB.runs(0, nil, nil) 1174 if err != nil { 1175 return nil, err 1176 } 1177 1178 runningBots := m.runningBotsLookup() 1179 archivedRuns := make([]*MarketMakingRun, 0, len(allRuns)) 1180 for _, run := range allRuns { 1181 runningBot := runningBots[*run.Market] 1182 if runningBot == nil || runningBot.bot.timeStart() != run.StartTime { 1183 archivedRuns = append(archivedRuns, run) 1184 } 1185 } 1186 1187 return archivedRuns, nil 1188 } 1189 1190 // RunOverview returns the overview of a market making run. 1191 func (m *MarketMaker) RunOverview(startTime int64, mkt *MarketWithHost) (*MarketMakingRunOverview, error) { 1192 return m.eventLogDB.runOverview(startTime, mkt) 1193 } 1194 1195 func (m *MarketMaker) updateDEXOrderEvent(mkt *MarketWithHost, event *MarketMakingEvent) (*MarketMakingEvent, error) { 1196 orderEvent := event.DEXOrderEvent 1197 1198 findEventTx := func(txid string) *asset.WalletTransaction { 1199 for _, tx := range orderEvent.Transactions { 1200 if tx.ID == txid { 1201 return tx 1202 } 1203 } 1204 return nil 1205 } 1206 1207 oidB, err := hex.DecodeString(orderEvent.ID) 1208 if err != nil { 1209 return nil, fmt.Errorf("error decoding order ID: %v", err) 1210 } 1211 o, err := m.core.Order(oidB) 1212 if err != nil { 1213 return nil, fmt.Errorf("error fetching order: %v", err) 1214 } 1215 1216 swapIDs, redeemIDs, refundIDs := orderCoinIDs(o) 1217 fromAsset, _, toAsset, _ := orderAssets(mkt.BaseID, mkt.QuoteID, o.Sell) 1218 swaps := make(map[string]*asset.WalletTransaction, len(swapIDs)) 1219 redeems := make(map[string]*asset.WalletTransaction, len(redeemIDs)) 1220 refunds := make(map[string]*asset.WalletTransaction, len(refundIDs)) 1221 allTxs := make([]*asset.WalletTransaction, 0, len(orderEvent.Transactions)) 1222 pendingTx := false 1223 1224 processTxs := func(assetID uint32, coinIDs map[string]bool, txs map[string]*asset.WalletTransaction) { 1225 for coinID := range coinIDs { 1226 tx := findEventTx(coinID) 1227 1228 if tx == nil || !tx.Confirmed { 1229 var err error 1230 tx, err = m.core.WalletTransaction(assetID, coinID) 1231 if err != nil { 1232 m.log.Errorf("Error fetching transaction %s for %s: %v", coinID, mkt, err) 1233 pendingTx = true 1234 continue 1235 } 1236 } 1237 1238 txs[tx.ID] = tx 1239 allTxs = append(allTxs, tx) 1240 pendingTx = pendingTx || !tx.Confirmed 1241 } 1242 } 1243 1244 processTxs(fromAsset, swapIDs, swaps) 1245 processTxs(toAsset, redeemIDs, redeems) 1246 processTxs(fromAsset, refundIDs, refunds) 1247 1248 var activeMatches bool 1249 for _, match := range o.Matches { 1250 if match.Active { 1251 activeMatches = true 1252 break 1253 } 1254 } 1255 1256 baseTraits, err := m.core.WalletTraits(mkt.BaseID) 1257 if err != nil { 1258 return nil, fmt.Errorf("error getting base asset traits: %v", err) 1259 } 1260 1261 quoteTraits, err := m.core.WalletTraits(mkt.QuoteID) 1262 if err != nil { 1263 return nil, fmt.Errorf("error getting quote asset traits: %v", err) 1264 } 1265 1266 return &MarketMakingEvent{ 1267 ID: event.ID, 1268 TimeStamp: event.TimeStamp, 1269 Pending: pendingTx || o.Status <= order.OrderStatusBooked || activeMatches, 1270 BalanceEffects: combineBalanceEffects(dexOrderEffects(o, swaps, redeems, refunds, 0, baseTraits, quoteTraits)), 1271 DEXOrderEvent: &DEXOrderEvent{ 1272 ID: orderEvent.ID, 1273 Sell: o.Sell, 1274 Rate: o.Rate, 1275 Qty: o.Qty, 1276 Transactions: allTxs, 1277 }, 1278 }, nil 1279 } 1280 1281 func (m *MarketMaker) updateCEXOrderEvent(mkt *MarketWithHost, event *MarketMakingEvent, cexName string) (*MarketMakingEvent, error) { 1282 cex, err := m.connectedCEX(cexName) 1283 if err != nil { 1284 return nil, fmt.Errorf("error connecting to CEX: %v", err) 1285 } 1286 1287 orderEvent := event.CEXOrderEvent 1288 1289 trade, err := cex.TradeStatus(m.ctx, orderEvent.ID, mkt.BaseID, mkt.QuoteID) 1290 if err != nil { 1291 return nil, fmt.Errorf("error fetching trade status: %v", err) 1292 } 1293 1294 return cexOrderEvent(trade, event.ID, event.TimeStamp), nil 1295 } 1296 1297 func (m *MarketMaker) updateDepositEvent(event *MarketMakingEvent, cexName string) (*MarketMakingEvent, error) { 1298 wt := event.DepositEvent.Transaction 1299 if wt == nil { 1300 return nil, fmt.Errorf("nil transaction") 1301 } 1302 1303 if !wt.Confirmed { 1304 tx, err := m.core.WalletTransaction(event.DepositEvent.AssetID, wt.ID) 1305 if err != nil { 1306 return nil, fmt.Errorf("error fetching transaction: %v", err) 1307 } 1308 wt = tx 1309 } 1310 1311 cex, err := m.connectedCEX(cexName) 1312 if err != nil { 1313 return nil, fmt.Errorf("error connecting to CEX: %v", err) 1314 } 1315 1316 unitInfo, err := asset.UnitInfo(event.DepositEvent.AssetID) 1317 if err != nil { 1318 return nil, fmt.Errorf("error getting unit info: %v", err) 1319 } 1320 1321 convAmount := float64(wt.Amount) / float64(unitInfo.Conventional.ConversionFactor) 1322 confirmed, cexCredit := cex.ConfirmDeposit(m.ctx, &libxc.DepositData{ 1323 AssetID: event.DepositEvent.AssetID, 1324 AmountConventional: convAmount, 1325 TxID: wt.ID, 1326 }) 1327 1328 return &MarketMakingEvent{ 1329 ID: event.ID, 1330 TimeStamp: event.TimeStamp, 1331 Pending: !confirmed, 1332 BalanceEffects: combineBalanceEffects(depositBalanceEffects(event.DepositEvent.AssetID, wt, confirmed)), 1333 DepositEvent: &DepositEvent{ 1334 Transaction: wt, 1335 AssetID: event.DepositEvent.AssetID, 1336 CEXCredit: cexCredit, 1337 }, 1338 }, nil 1339 } 1340 1341 func (m *MarketMaker) updateWithdrawalEvent(mkt *MarketWithHost, event *MarketMakingEvent, cexName string) (*MarketMakingEvent, error) { 1342 tx := event.WithdrawalEvent.Transaction 1343 withdrawalID := event.WithdrawalEvent.ID 1344 assetID := event.WithdrawalEvent.AssetID 1345 var cexDebit uint64 1346 if tx == nil { 1347 cex, err := m.connectedCEX(cexName) 1348 if err != nil { 1349 return nil, fmt.Errorf("error connecting to CEX: %v", err) 1350 } 1351 1352 var txID string 1353 cexDebit, txID, err = cex.ConfirmWithdrawal(m.ctx, withdrawalID, assetID) 1354 if errors.Is(err, libxc.ErrWithdrawalPending) { 1355 return event, nil 1356 } 1357 if err != nil { 1358 return nil, fmt.Errorf("error confirming withdrawal: %v", err) 1359 } 1360 1361 tx, err = m.core.WalletTransaction(assetID, txID) 1362 if err != nil { 1363 return nil, fmt.Errorf("error fetching transaction: %v", err) 1364 } 1365 } else { 1366 cexDebit = event.WithdrawalEvent.CEXDebit 1367 } 1368 1369 return &MarketMakingEvent{ 1370 ID: event.ID, 1371 TimeStamp: event.TimeStamp, 1372 BalanceEffects: combineBalanceEffects(withdrawalBalanceEffects(tx, cexDebit, event.WithdrawalEvent.AssetID)), 1373 Pending: tx == nil || !tx.Confirmed, 1374 WithdrawalEvent: &WithdrawalEvent{ 1375 AssetID: assetID, 1376 ID: withdrawalID, 1377 Transaction: tx, 1378 CEXDebit: cexDebit, 1379 }, 1380 }, nil 1381 } 1382 1383 func (m *MarketMaker) connectedCEX(cexName string) (*centralizedExchange, error) { 1384 m.cexMtx.RLock() 1385 cex := m.cexes[cexName] 1386 m.cexMtx.RUnlock() 1387 if cex == nil { 1388 return nil, fmt.Errorf("CEX %s not found", cexName) 1389 } 1390 1391 err := m.connectCEX(m.ctx, cex) 1392 if err != nil { 1393 return nil, fmt.Errorf("error connecting to CEX: %w", err) 1394 } 1395 1396 return cex, nil 1397 } 1398 1399 // updatePendingEvent looks up the latest state related to a pending 1400 // MarketMakingEvent returns an updated MarketMakingEvent. 1401 func (m *MarketMaker) updatePendingEvent(mkt *MarketWithHost, event *MarketMakingEvent, overview *MarketMakingRunOverview) (*MarketMakingEvent, error) { 1402 if len(overview.Cfgs) == 0 { 1403 return nil, fmt.Errorf("no bot config found for %s", mkt) 1404 } 1405 cexName := overview.Cfgs[0].Cfg.CEXName // may be empty string, but that's OK 1406 1407 switch { 1408 case event.DEXOrderEvent != nil: 1409 return m.updateDEXOrderEvent(mkt, event) 1410 case event.CEXOrderEvent != nil: 1411 return m.updateCEXOrderEvent(mkt, event, cexName) 1412 case event.DepositEvent != nil: 1413 return m.updateDepositEvent(event, cexName) 1414 case event.WithdrawalEvent != nil: 1415 return m.updateWithdrawalEvent(mkt, event, cexName) 1416 default: 1417 return event, nil 1418 } 1419 } 1420 1421 type RunLogFilters struct { 1422 DexBuys bool `json:"dexBuys"` 1423 DexSells bool `json:"dexSells"` 1424 CexBuys bool `json:"cexBuys"` 1425 CexSells bool `json:"cexSells"` 1426 Deposits bool `json:"deposits"` 1427 Withdrawals bool `json:"withdrawals"` 1428 } 1429 1430 func (f *RunLogFilters) filter(event *MarketMakingEvent) bool { 1431 switch { 1432 case event.DEXOrderEvent != nil: 1433 if event.DEXOrderEvent.Sell { 1434 return f.DexSells 1435 } 1436 return f.DexBuys 1437 case event.CEXOrderEvent != nil: 1438 if event.CEXOrderEvent.Sell { 1439 return f.CexSells 1440 } 1441 return f.CexBuys 1442 case event.DepositEvent != nil: 1443 return f.Deposits 1444 case event.WithdrawalEvent != nil: 1445 return f.Withdrawals 1446 default: 1447 return false 1448 } 1449 } 1450 1451 var noFilters = &RunLogFilters{ 1452 DexBuys: true, 1453 DexSells: true, 1454 CexBuys: true, 1455 CexSells: true, 1456 Deposits: true, 1457 Withdrawals: true, 1458 } 1459 1460 // RunLogs returns the event logs of a market making run. At most n events are 1461 // returned, if n == 0 then all events are returned. If refID is not nil, then 1462 // the events including and after refID are returned. 1463 // Updated events are events that were updated from pending to confirmed during 1464 // this call. For completed runs, on each call to RunLogs, all pending events are 1465 // checked for updates, and anything that was updated is returned. 1466 func (m *MarketMaker) RunLogs(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, filters *RunLogFilters) (events, updatedEvents []*MarketMakingEvent, overview *MarketMakingRunOverview, err error) { 1467 var running bool 1468 runningBotsLookup := m.runningBotsLookup() 1469 if bot, found := runningBotsLookup[*mkt]; found { 1470 running = bot.timeStart() == startTime 1471 } 1472 1473 if filters == nil { 1474 filters = noFilters 1475 } 1476 1477 if !running { 1478 pendingEvents, err := m.eventLogDB.runEvents(startTime, mkt, 0, nil, true, noFilters) 1479 if err != nil { 1480 return nil, nil, nil, err 1481 } 1482 if len(pendingEvents) > 0 { 1483 updatedEvents = make([]*MarketMakingEvent, 0, len(pendingEvents)) 1484 overview, err := m.eventLogDB.runOverview(startTime, mkt) 1485 if err != nil { 1486 return nil, nil, nil, err 1487 } 1488 for _, event := range pendingEvents { 1489 if event.Pending { 1490 updatedEvent, err := m.updatePendingEvent(mkt, event, overview) 1491 if err != nil { 1492 m.log.Errorf("Error updating pending event: %v", err) 1493 continue 1494 } 1495 updatedEvents = append(updatedEvents, updatedEvent) 1496 m.eventLogDB.storeEvent(startTime, mkt, updatedEvent, nil) 1497 } 1498 } 1499 } 1500 } 1501 1502 events, err = m.eventLogDB.runEvents(startTime, mkt, n, refID, false, filters) 1503 if err != nil { 1504 return nil, nil, nil, err 1505 } 1506 1507 overview, err = m.eventLogDB.runOverview(startTime, mkt) 1508 if err != nil { 1509 return nil, nil, nil, err 1510 } 1511 1512 return events, updatedEvents, overview, nil 1513 } 1514 1515 // CEXBook generates a snapshot of the specified CEX order book. 1516 func (m *MarketMaker) CEXBook(host string, baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) { 1517 mwh := MarketWithHost{Host: host, BaseID: baseID, QuoteID: quoteID} 1518 m.runningBotsMtx.RLock() 1519 bot, found := m.runningBots[mwh] 1520 m.runningBotsMtx.RUnlock() 1521 if !found { 1522 return nil, nil, fmt.Errorf("no running bot found for market %s", mwh) 1523 } 1524 return bot.Book() 1525 } 1526 1527 // LotFees are the fees for trading one lot. 1528 type LotFees struct { 1529 Swap uint64 `json:"swap"` 1530 Redeem uint64 `json:"redeem"` 1531 Refund uint64 `json:"refund"` 1532 } 1533 1534 // LotFeeRange combine the estimated and maximum LotFees. 1535 type LotFeeRange struct { 1536 Max *LotFees `json:"max"` 1537 Estimated *LotFees `json:"estimated"` 1538 } 1539 1540 // marketFees calculates the LotFees for the base and quote assets. 1541 func marketFees(c clientCore, host string, baseID, quoteID uint32, useMaxFeeRate bool) (baseFees, quoteFees *LotFees, _ error) { 1542 buySwapFees, buyRedeemFees, buyRefundFees, err := c.SingleLotFees(&core.SingleLotFeesForm{ 1543 Host: host, 1544 Base: baseID, 1545 Quote: quoteID, 1546 UseMaxFeeRate: useMaxFeeRate, 1547 UseSafeTxSize: useMaxFeeRate, 1548 }) 1549 if err != nil { 1550 return nil, nil, fmt.Errorf("failed to get buy single lot fees: %v", err) 1551 } 1552 1553 sellSwapFees, sellRedeemFees, sellRefundFees, err := c.SingleLotFees(&core.SingleLotFeesForm{ 1554 Host: host, 1555 Base: baseID, 1556 Quote: quoteID, 1557 UseMaxFeeRate: useMaxFeeRate, 1558 UseSafeTxSize: useMaxFeeRate, 1559 Sell: true, 1560 }) 1561 if err != nil { 1562 return nil, nil, fmt.Errorf("failed to get sell single lot fees: %v", err) 1563 } 1564 1565 return &LotFees{ 1566 Swap: sellSwapFees, 1567 Redeem: buyRedeemFees, 1568 Refund: sellRefundFees, 1569 }, &LotFees{ 1570 Swap: buySwapFees, 1571 Redeem: sellRedeemFees, 1572 Refund: buyRefundFees, 1573 }, nil 1574 } 1575 1576 func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig) (dexBalances, cexBalances map[uint32]uint64, _ error) { 1577 dexAssets := make(map[uint32]interface{}) 1578 cexAssets := make(map[uint32]interface{}) 1579 1580 dexAssets[mkt.BaseID] = struct{}{} 1581 dexAssets[mkt.QuoteID] = struct{}{} 1582 dexAssets[feeAssetID(mkt.BaseID)] = struct{}{} 1583 dexAssets[feeAssetID(mkt.QuoteID)] = struct{}{} 1584 1585 if cexCfg != nil { 1586 cexAssets[mkt.BaseID] = struct{}{} 1587 cexAssets[mkt.QuoteID] = struct{}{} 1588 } 1589 1590 checkTotalBalances := func() (dexBals, cexBals map[uint32]uint64, err error) { 1591 dexBals = make(map[uint32]uint64, len(dexAssets)) 1592 cexBals = make(map[uint32]uint64, len(cexAssets)) 1593 1594 for assetID := range dexAssets { 1595 bal, err := m.core.AssetBalance(assetID) 1596 if err != nil { 1597 return nil, nil, err 1598 } 1599 dexBals[assetID] = bal.Available 1600 } 1601 1602 if cexCfg != nil { 1603 cex, err := m.loadAndConnectCEX(m.ctx, cexCfg) 1604 if err != nil { 1605 return nil, nil, err 1606 } 1607 1608 for assetID := range cexAssets { 1609 balance, err := cex.Balance(assetID) 1610 if err != nil { 1611 return nil, nil, err 1612 } 1613 1614 cexBals[assetID] = balance.Available 1615 } 1616 } 1617 1618 return dexBals, cexBals, nil 1619 } 1620 1621 checkBot := func(bot *runningBot) bool { 1622 botAssets := bot.assets() 1623 for assetID := range dexAssets { 1624 if _, found := botAssets[assetID]; found { 1625 return true 1626 } 1627 } 1628 return false 1629 } 1630 1631 balancesEqual := func(bal1, bal2 map[uint32]uint64) bool { 1632 if len(bal1) != len(bal2) { 1633 return false 1634 } 1635 for assetID, bal := range bal1 { 1636 if bal2[assetID] != bal { 1637 return false 1638 } 1639 } 1640 return true 1641 } 1642 1643 // We first check the available balances in the DEX wallets and on 1644 // the CEX, then check the amounts reserved by the running bots, 1645 // and then recheck the amounts available on the DEX and CEX. If 1646 // the available balances in the first and last checks are equal, 1647 // then we know that nothing has changed. If not, we try again. 1648 totalDEXBalances, totalCEXBalances, err := checkTotalBalances() 1649 if err != nil { 1650 return nil, nil, err 1651 } 1652 1653 const maxTries = 5 1654 for i := 0; i < maxTries; i++ { 1655 reservedDEXBalances := make(map[uint32]uint64, len(dexAssets)) 1656 reservedCEXBalances := make(map[uint32]uint64, len(cexAssets)) 1657 1658 runningBots := m.runningBotsLookup() 1659 for _, rb := range runningBots { 1660 if !checkBot(rb) { 1661 continue 1662 } 1663 1664 rb.refreshAllPendingEvents(m.ctx) 1665 1666 for assetID := range dexAssets { 1667 botBalance := rb.DEXBalance(assetID) 1668 reservedDEXBalances[assetID] += botBalance.Available 1669 } 1670 1671 if cexCfg != nil && rb.cexName() == cexCfg.Name { 1672 for assetID := range cexAssets { 1673 botBalance := rb.CEXBalance(assetID) 1674 reservedCEXBalances[assetID] += botBalance.Available + botBalance.Reserved 1675 } 1676 } 1677 } 1678 1679 updatedDEXBalances, updatedCEXBalances, err := checkTotalBalances() 1680 if err != nil { 1681 return nil, nil, err 1682 } 1683 1684 if balancesEqual(updatedDEXBalances, totalDEXBalances) && balancesEqual(updatedCEXBalances, totalCEXBalances) { 1685 for assetID, bal := range reservedDEXBalances { 1686 if bal > totalDEXBalances[assetID] { 1687 m.log.Warnf("reserved DEX balance for %s exceeds available balance: %d > %d", dex.BipIDSymbol(assetID), bal, totalDEXBalances[assetID]) 1688 totalDEXBalances[assetID] = 0 1689 } else { 1690 totalDEXBalances[assetID] -= bal 1691 } 1692 } 1693 for assetID, bal := range reservedCEXBalances { 1694 if bal > totalCEXBalances[assetID] { 1695 m.log.Warnf("reserved CEX balance for %s exceeds available balance: %d > %d", dex.BipIDSymbol(assetID), bal, totalCEXBalances[assetID]) 1696 totalCEXBalances[assetID] = 0 1697 } else { 1698 totalCEXBalances[assetID] -= bal 1699 } 1700 } 1701 return totalDEXBalances, totalCEXBalances, nil 1702 } 1703 1704 totalDEXBalances = updatedDEXBalances 1705 totalCEXBalances = updatedCEXBalances 1706 } 1707 1708 return nil, nil, fmt.Errorf("failed to get available balances after %d tries", maxTries) 1709 } 1710 1711 // AvailableBalances returns the available balances of assets relevant to 1712 // market making on the specified market on the DEX (including fee assets), 1713 // and optionally a CEX depending on the configured strategy. 1714 func (m *MarketMaker) AvailableBalances(mkt *MarketWithHost, alternateConfigPath *string) (dexBalances, cexBalances map[uint32]uint64, _ error) { 1715 _, cexCfg, err := m.configsForMarket(mkt, alternateConfigPath) 1716 if err != nil { 1717 return nil, nil, err 1718 } 1719 1720 return m.availableBalances(mkt, cexCfg) 1721 } 1722 1723 func sellStr(sell bool) string { 1724 if sell { 1725 return "sell" 1726 } 1727 return "buy" 1728 }