decred.org/dcrdex@v1.0.5/client/mm/exchange_adaptor.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/json" 9 "errors" 10 "fmt" 11 "math" 12 "sort" 13 "strconv" 14 "strings" 15 "sync" 16 "sync/atomic" 17 "time" 18 19 "decred.org/dcrdex/client/asset" 20 "decred.org/dcrdex/client/core" 21 "decred.org/dcrdex/client/mm/libxc" 22 "decred.org/dcrdex/client/orderbook" 23 "decred.org/dcrdex/dex" 24 "decred.org/dcrdex/dex/calc" 25 "decred.org/dcrdex/dex/order" 26 "decred.org/dcrdex/dex/utils" 27 ) 28 29 // BotBalance keeps track of the amount of funds available for a 30 // bot's use, locked to fund orders, and pending. 31 type BotBalance struct { 32 Available uint64 `json:"available"` 33 Locked uint64 `json:"locked"` 34 Pending uint64 `json:"pending"` 35 Reserved uint64 `json:"reserved"` 36 } 37 38 func (b *BotBalance) copy() *BotBalance { 39 return &BotBalance{ 40 Available: b.Available, 41 Locked: b.Locked, 42 Pending: b.Pending, 43 Reserved: b.Reserved, 44 } 45 } 46 47 // OrderFees represents the fees that will be required for a single lot of a 48 // dex order. 49 type OrderFees struct { 50 *LotFeeRange 51 Funding uint64 `json:"funding"` 52 // bookingFeesPerLot is the amount of fee asset that needs to be reserved 53 // for fees, per ordered lot. For all assets, this will include 54 // LotFeeRange.Max.Swap. For non-token EVM assets (eth, matic) Max.Refund 55 // will be added. If the asset is the parent chain of a token counter-asset, 56 // Max.Redeem is added. This is a commonly needed sum in various validation 57 // and optimization functions. 58 BookingFeesPerLot uint64 `json:"bookingFeesPerLot"` 59 } 60 61 // botCoreAdaptor is an interface used by bots to access DEX related 62 // functions. Common functionality used by multiple market making 63 // strategies is implemented here. The functions in this interface 64 // do not need to take assetID parameters, as the bot will only be 65 // trading on a single DEX market. 66 type botCoreAdaptor interface { 67 SyncBook(host string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error) 68 Cancel(oidB dex.Bytes) error 69 DEXTrade(rate, qty uint64, sell bool) (*core.Order, error) 70 ExchangeMarket(host string, baseID, quoteID uint32) (*core.Market, error) 71 ExchangeRateFromFiatSources() uint64 72 OrderFeesInUnits(sell, base bool, rate uint64) (uint64, error) // estimated fees, not max 73 SubscribeOrderUpdates() (updates <-chan *core.Order) 74 SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error) 75 } 76 77 // botCexAdaptor is an interface used by bots to access CEX related 78 // functions. Common functionality used by multiple market making 79 // strategies is implemented here. The functions in this interface 80 // take assetID parameters, unlike botCoreAdaptor, to support a 81 // multi-hop strategy. 82 type botCexAdaptor interface { 83 CancelTrade(ctx context.Context, baseID, quoteID uint32, tradeID string) error 84 SubscribeMarket(ctx context.Context, baseID, quoteID uint32) error 85 SubscribeTradeUpdates() <-chan *libxc.Trade 86 CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error) 87 SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool 88 MidGap(baseID, quoteID uint32) uint64 89 Book() (buys, sells []*core.MiniOrder, _ error) 90 } 91 92 // BalanceEffects represents the effects that a market making event has on 93 // the bot's balances. 94 type BalanceEffects struct { 95 Settled map[uint32]int64 `json:"settled"` 96 Locked map[uint32]uint64 `json:"locked"` 97 Pending map[uint32]uint64 `json:"pending"` 98 Reserved map[uint32]uint64 `json:"reserved"` 99 } 100 101 func newBalanceEffects() *BalanceEffects { 102 return &BalanceEffects{ 103 Settled: make(map[uint32]int64), 104 Locked: make(map[uint32]uint64), 105 Pending: make(map[uint32]uint64), 106 Reserved: make(map[uint32]uint64), 107 } 108 } 109 110 type balanceEffectsDiff struct { 111 settled map[uint32]int64 112 locked map[uint32]int64 113 pending map[uint32]int64 114 reserved map[uint32]int64 115 } 116 117 func newBalanceEffectsDiff() *balanceEffectsDiff { 118 return &balanceEffectsDiff{ 119 settled: make(map[uint32]int64), 120 locked: make(map[uint32]int64), 121 pending: make(map[uint32]int64), 122 reserved: make(map[uint32]int64), 123 } 124 } 125 126 func (b *BalanceEffects) sub(other *BalanceEffects) *balanceEffectsDiff { 127 res := newBalanceEffectsDiff() 128 129 for assetID, v := range b.Settled { 130 res.settled[assetID] = v 131 } 132 for assetID, v := range b.Locked { 133 res.locked[assetID] = int64(v) 134 } 135 for assetID, v := range b.Pending { 136 res.pending[assetID] = int64(v) 137 } 138 for assetID, v := range b.Reserved { 139 res.reserved[assetID] = int64(v) 140 } 141 142 for assetID, v := range other.Settled { 143 res.settled[assetID] -= v 144 } 145 for assetID, v := range other.Locked { 146 res.locked[assetID] -= int64(v) 147 } 148 for assetID, v := range other.Pending { 149 res.pending[assetID] -= int64(v) 150 } 151 for assetID, v := range other.Reserved { 152 res.reserved[assetID] -= int64(v) 153 } 154 155 return res 156 } 157 158 // pendingWithdrawal represents a withdrawal from a CEX that has been 159 // initiated, but the DEX has not yet received. 160 type pendingWithdrawal struct { 161 eventLogID uint64 162 timestamp int64 163 withdrawalID string 164 assetID uint32 165 // amtWithdrawn is the amount the CEX balance is decreased by. 166 // It will not be the same as the amount received in the dex wallet. 167 amtWithdrawn uint64 168 169 txMtx sync.RWMutex 170 txID string 171 tx *asset.WalletTransaction 172 } 173 174 func withdrawalBalanceEffects(tx *asset.WalletTransaction, cexDebit uint64, assetID uint32) (dex, cex *BalanceEffects) { 175 dex = newBalanceEffects() 176 cex = newBalanceEffects() 177 178 cex.Settled[assetID] = -int64(cexDebit) 179 180 if tx != nil { 181 if tx.Confirmed { 182 dex.Settled[assetID] += int64(tx.Amount) 183 } else { 184 dex.Pending[assetID] += tx.Amount 185 } 186 } else { 187 dex.Pending[assetID] += cexDebit 188 } 189 190 return 191 } 192 193 func (w *pendingWithdrawal) balanceEffects() (dex, cex *BalanceEffects) { 194 w.txMtx.RLock() 195 defer w.txMtx.RUnlock() 196 197 return withdrawalBalanceEffects(w.tx, w.amtWithdrawn, w.assetID) 198 } 199 200 // pendingDeposit represents a deposit to a CEX that has not yet been 201 // confirmed. 202 type pendingDeposit struct { 203 eventLogID uint64 204 timestamp int64 205 assetID uint32 206 amtConventional float64 207 208 mtx sync.RWMutex 209 tx *asset.WalletTransaction 210 feeConfirmed bool 211 cexConfirmed bool 212 amtCredited uint64 213 } 214 215 func depositBalanceEffects(assetID uint32, tx *asset.WalletTransaction, cexConfirmed bool) (dex, cex *BalanceEffects) { 216 feeAsset := assetID 217 token := asset.TokenInfo(assetID) 218 if token != nil { 219 feeAsset = token.ParentID 220 } 221 222 dex, cex = newBalanceEffects(), newBalanceEffects() 223 224 dex.Settled[assetID] -= int64(tx.Amount) 225 dex.Settled[feeAsset] -= int64(tx.Fees) 226 227 if cexConfirmed { 228 cex.Settled[assetID] += int64(tx.Amount) 229 } else { 230 cex.Pending[assetID] += tx.Amount 231 } 232 233 return dex, cex 234 } 235 236 func (d *pendingDeposit) balanceEffects() (dex, cex *BalanceEffects) { 237 d.mtx.RLock() 238 defer d.mtx.RUnlock() 239 240 return depositBalanceEffects(d.assetID, d.tx, d.cexConfirmed) 241 } 242 243 type dexOrderState struct { 244 dexBalanceEffects *BalanceEffects 245 cexBalanceEffects *BalanceEffects 246 order *core.Order 247 counterTradeRate uint64 248 } 249 250 // pendingDEXOrder keeps track of the balance effects of a pending DEX order. 251 // The actual order is not stored here, only its effects on the balance. 252 type pendingDEXOrder struct { 253 eventLogID uint64 254 timestamp int64 255 256 // swaps, redeems, and refunds are caches of transactions. This avoids 257 // having to query the wallet for transactions that are already confirmed. 258 txsMtx sync.RWMutex 259 swaps map[string]*asset.WalletTransaction 260 swapCoinIDToTxID map[string]string 261 redeems map[string]*asset.WalletTransaction 262 redeemCoinIDToTxID map[string]string 263 refunds map[string]*asset.WalletTransaction 264 refundCoinIDToTxID map[string]string 265 // txsMtx is required to be locked for writes to state 266 state atomic.Value // *dexOrderState 267 268 // placementIndex/counterTradeRate are used by MultiTrade to know 269 // which orders to place/cancel. 270 placementIndex uint64 271 counterTradeRate uint64 272 } 273 274 func (p *pendingDEXOrder) cexBalanceEffects() *BalanceEffects { 275 return p.currentState().cexBalanceEffects 276 } 277 278 // currentState can be called without locking, but to get a consistent view of 279 // the transactions and the state, txsMtx should be read locked. 280 func (p *pendingDEXOrder) currentState() *dexOrderState { 281 return p.state.Load().(*dexOrderState) 282 } 283 284 // counterTradeAsset is the asset that the bot will need to trade on the CEX 285 // to arbitrage matches on the DEX. 286 func (p *pendingDEXOrder) counterTradeAsset() uint32 { 287 o := p.currentState().order 288 if o.Sell { 289 return o.QuoteID 290 } 291 return o.BaseID 292 } 293 294 type pendingCEXOrder struct { 295 eventLogID uint64 296 timestamp int64 297 298 tradeMtx sync.RWMutex 299 trade *libxc.Trade 300 } 301 302 // market is the market-related data for the unifiedExchangeAdaptor and the 303 // calculators. market provides a number of methods for conversions and 304 // formatting. 305 type market struct { 306 host string 307 name string 308 rateStep atomic.Uint64 309 lotSize atomic.Uint64 310 baseID uint32 311 baseTicker string 312 bui dex.UnitInfo 313 baseFeeID uint32 314 baseFeeUI dex.UnitInfo 315 quoteID uint32 316 quoteTicker string 317 qui dex.UnitInfo 318 quoteFeeID uint32 319 quoteFeeUI dex.UnitInfo 320 } 321 322 func parseMarket(host string, mkt *core.Market) (*market, error) { 323 bui, err := asset.UnitInfo(mkt.BaseID) 324 if err != nil { 325 return nil, err 326 } 327 baseFeeID := mkt.BaseID 328 baseFeeUI := bui 329 if tkn := asset.TokenInfo(mkt.BaseID); tkn != nil { 330 baseFeeID = tkn.ParentID 331 baseFeeUI, err = asset.UnitInfo(tkn.ParentID) 332 if err != nil { 333 return nil, err 334 } 335 } 336 qui, err := asset.UnitInfo(mkt.QuoteID) 337 if err != nil { 338 return nil, err 339 } 340 quoteFeeID := mkt.QuoteID 341 quoteFeeUI := qui 342 if tkn := asset.TokenInfo(mkt.QuoteID); tkn != nil { 343 quoteFeeID = tkn.ParentID 344 quoteFeeUI, err = asset.UnitInfo(tkn.ParentID) 345 if err != nil { 346 return nil, err 347 } 348 } 349 350 m := &market{ 351 host: host, 352 name: mkt.Name, 353 baseID: mkt.BaseID, 354 baseTicker: bui.Conventional.Unit, 355 bui: bui, 356 baseFeeID: baseFeeID, 357 baseFeeUI: baseFeeUI, 358 quoteID: mkt.QuoteID, 359 quoteTicker: qui.Conventional.Unit, 360 qui: qui, 361 quoteFeeID: quoteFeeID, 362 quoteFeeUI: quoteFeeUI, 363 } 364 m.lotSize.Store(mkt.LotSize) 365 m.rateStep.Store(mkt.RateStep) 366 return m, nil 367 } 368 369 func (m *market) fmtRate(msgRate uint64) string { 370 r := calc.ConventionalRate(msgRate, m.bui, m.qui) 371 s := strconv.FormatFloat(r, 'f', 8, 64) 372 if strings.Contains(s, ".") { 373 s = strings.TrimRight(strings.TrimRight(s, "0"), ".") 374 } 375 return s 376 } 377 func (m *market) fmtBase(atoms uint64) string { 378 return m.bui.FormatAtoms(atoms) 379 } 380 func (m *market) fmtQuote(atoms uint64) string { 381 return m.qui.FormatAtoms(atoms) 382 } 383 func (m *market) fmtQty(assetID uint32, atoms uint64) string { 384 if assetID == m.baseID { 385 return m.fmtBase(atoms) 386 } 387 return m.fmtQuote(atoms) 388 } 389 390 func (m *market) fmtBaseFees(atoms uint64) string { 391 return m.baseFeeUI.FormatAtoms(atoms) 392 } 393 394 func (m *market) fmtQuoteFees(atoms uint64) string { 395 return m.quoteFeeUI.FormatAtoms(atoms) 396 } 397 func (m *market) fmtFees(assetID uint32, atoms uint64) string { 398 if assetID == m.baseID { 399 return m.fmtBaseFees(atoms) 400 } 401 return m.fmtQuoteFees(atoms) 402 } 403 404 func (m *market) msgRate(convRate float64) uint64 { 405 return calc.MessageRate(convRate, m.bui, m.qui) 406 } 407 408 // unifiedExchangeAdaptor implements both botCoreAdaptor and botCexAdaptor. 409 type unifiedExchangeAdaptor struct { 410 *market 411 clientCore 412 libxc.CEX 413 414 ctx context.Context 415 kill context.CancelFunc 416 wg sync.WaitGroup 417 botID string 418 log dex.Logger 419 fiatRates atomic.Value // map[uint32]float64 420 orderUpdates atomic.Value // chan *core.Order 421 mwh *MarketWithHost 422 eventLogDB eventLogDB 423 botCfgV atomic.Value // *BotConfig 424 initialBalances map[uint32]uint64 425 baseTraits asset.WalletTrait 426 quoteTraits asset.WalletTrait 427 428 botLooper dex.Connector 429 botLoop *dex.ConnectionMaster 430 paused atomic.Bool 431 432 autoRebalanceCfg *AutoRebalanceConfig 433 434 subscriptionIDMtx sync.RWMutex 435 subscriptionID *int 436 437 feesMtx sync.RWMutex 438 buyFees *OrderFees 439 sellFees *OrderFees 440 441 startTime atomic.Int64 442 eventLogID atomic.Uint64 443 444 balancesMtx sync.RWMutex 445 // baseDEXBalance/baseCEXBalance are the balances the bots have before 446 // taking into account any pending actions. These are updated whenever 447 // a pending action is completed. They may become negative if a balance 448 // is decreased during an update while there are pending actions that 449 // positively affect the available balance. 450 baseDexBalances map[uint32]int64 451 baseCexBalances map[uint32]int64 452 pendingDEXOrders map[order.OrderID]*pendingDEXOrder 453 pendingCEXOrders map[string]*pendingCEXOrder 454 pendingWithdrawals map[string]*pendingWithdrawal 455 pendingDeposits map[string]*pendingDeposit 456 inventoryMods map[uint32]int64 457 458 // If pendingBaseRebalance/pendingQuoteRebalance are true, it means 459 // there is a pending deposit/withdrawal of the base/quote asset, 460 // and no other deposits/withdrawals of that asset should happen 461 // until it is complete. 462 pendingBaseRebalance atomic.Bool 463 pendingQuoteRebalance atomic.Bool 464 465 // The following are updated whenever a pending action is complete. 466 // For accurate run stats, the pending actions must be taken into 467 // account. 468 runStats struct { 469 completedMatches atomic.Uint32 470 tradedUSD struct { 471 sync.Mutex 472 v float64 473 } 474 feeGapStats atomic.Value 475 } 476 477 epochReport atomic.Value // *EpochReport 478 479 cexProblemsMtx sync.RWMutex 480 cexProblems *CEXProblems 481 } 482 483 var _ botCoreAdaptor = (*unifiedExchangeAdaptor)(nil) 484 var _ botCexAdaptor = (*unifiedExchangeAdaptor)(nil) 485 486 func (u *unifiedExchangeAdaptor) botCfg() *BotConfig { 487 return u.botCfgV.Load().(*BotConfig) 488 } 489 490 // botLooper is just a dex.Connector for a function. 491 type botLooper func(context.Context) (*sync.WaitGroup, error) 492 493 func (f botLooper) Connect(ctx context.Context) (*sync.WaitGroup, error) { 494 return f(ctx) 495 } 496 497 // setBotLoop sets the loop that must be shut down for configuration updates. 498 // Every bot should call setBotLoop during construction. 499 func (u *unifiedExchangeAdaptor) setBotLoop(f botLooper) { 500 u.botLooper = f 501 } 502 503 func (u *unifiedExchangeAdaptor) runBotLoop(ctx context.Context) error { 504 if u.botLooper == nil { 505 return errors.New("no bot looper set") 506 } 507 u.botLoop = dex.NewConnectionMaster(u.botLooper) 508 return u.botLoop.ConnectOnce(ctx) 509 } 510 511 // withPause runs a function with the bot loop paused. 512 func (u *unifiedExchangeAdaptor) withPause(f func() error) error { 513 if !u.paused.CompareAndSwap(false, true) { 514 return errors.New("already paused") 515 } 516 defer u.paused.Store(false) 517 518 u.botLoop.Disconnect() 519 520 if err := f(); err != nil { 521 return err 522 } 523 if u.ctx.Err() != nil { // Make sure we weren't shut down during pause. 524 return u.ctx.Err() 525 } 526 527 return u.botLoop.ConnectOnce(u.ctx) 528 } 529 530 // logBalanceAdjustments logs a trace log of balance adjustments and updated 531 // settled balances. 532 // 533 // balancesMtx must be read locked when calling this function. 534 func (u *unifiedExchangeAdaptor) logBalanceAdjustments(dexDiffs, cexDiffs map[uint32]int64, reason string) { 535 if u.log.Level() > dex.LevelTrace { 536 return 537 } 538 539 var msg strings.Builder 540 writeLine := func(s string, a ...interface{}) { 541 msg.WriteString("\n" + fmt.Sprintf(s, a...)) 542 } 543 writeLine("") 544 writeLine("Balance adjustments(%s):", reason) 545 546 format := func(assetID uint32, v int64, plusSign bool) string { 547 ui, err := asset.UnitInfo(assetID) 548 if err != nil { 549 return "<what the asset?>" 550 } 551 return ui.FormatSignedAtoms(v, plusSign) 552 } 553 554 if len(dexDiffs) > 0 { 555 writeLine(" DEX:") 556 for assetID, dexDiff := range dexDiffs { 557 writeLine(" %s", format(assetID, dexDiff, true)) 558 } 559 } 560 561 if len(cexDiffs) > 0 { 562 writeLine(" CEX:") 563 for assetID, cexDiff := range cexDiffs { 564 writeLine(" %s", format(assetID, cexDiff, true)) 565 } 566 } 567 568 writeLine("Updated settled balances:") 569 writeLine(" DEX:") 570 571 for assetID, bal := range u.baseDexBalances { 572 writeLine(" %s", format(assetID, bal, false)) 573 } 574 if len(u.baseCexBalances) > 0 { 575 writeLine(" CEX:") 576 for assetID, bal := range u.baseCexBalances { 577 writeLine(" %s", format(assetID, bal, false)) 578 } 579 } 580 581 dexPending := make(map[uint32]uint64) 582 addDexPending := func(assetID uint32) { 583 if v := u.dexBalance(assetID).Pending; v > 0 { 584 dexPending[assetID] = v 585 } 586 } 587 cexPending := make(map[uint32]uint64) 588 addCexPending := func(assetID uint32) { 589 if v := u.cexBalance(assetID).Pending; v > 0 { 590 cexPending[assetID] = v 591 } 592 } 593 addDexPending(u.baseID) 594 addCexPending(u.baseID) 595 addDexPending(u.quoteID) 596 addCexPending(u.quoteID) 597 if u.baseFeeID != u.baseID { 598 addCexPending(u.baseFeeID) 599 addCexPending(u.baseFeeID) 600 } 601 if u.quoteFeeID != u.quoteID && u.quoteFeeID != u.baseID { 602 addCexPending(u.quoteFeeID) 603 addCexPending(u.quoteFeeID) 604 } 605 if len(dexPending) > 0 { 606 writeLine(" DEX pending:") 607 for assetID, v := range dexPending { 608 writeLine(" %s", format(assetID, int64(v), true)) 609 } 610 } 611 if len(cexPending) > 0 { 612 writeLine(" CEX pending:") 613 for assetID, v := range cexPending { 614 writeLine(" %s", format(assetID, int64(v), true)) 615 } 616 } 617 618 writeLine("") 619 u.log.Tracef(msg.String()) 620 } 621 622 // SufficientBalanceForDEXTrade returns whether the bot has sufficient balance 623 // to place a DEX trade. 624 func (u *unifiedExchangeAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error) { 625 fromAsset, fromFeeAsset, toAsset, toFeeAsset := orderAssets(u.baseID, u.quoteID, sell) 626 balances := map[uint32]uint64{} 627 for _, assetID := range []uint32{fromAsset, fromFeeAsset, toAsset, toFeeAsset} { 628 if _, found := balances[assetID]; !found { 629 bal := u.DEXBalance(assetID) 630 balances[assetID] = bal.Available 631 } 632 } 633 634 buyFees, sellFees, err := u.orderFees() 635 if err != nil { 636 return false, err 637 } 638 fees, fundingFees := buyFees.Max, buyFees.Funding 639 if sell { 640 fees, fundingFees = sellFees.Max, sellFees.Funding 641 } 642 643 if balances[fromFeeAsset] < fundingFees { 644 return false, nil 645 } 646 balances[fromFeeAsset] -= fundingFees 647 648 fromQty := qty 649 if !sell { 650 fromQty = calc.BaseToQuote(rate, qty) 651 } 652 if balances[fromAsset] < fromQty { 653 return false, nil 654 } 655 balances[fromAsset] -= fromQty 656 657 numLots := qty / u.lotSize.Load() 658 if balances[fromFeeAsset] < numLots*fees.Swap { 659 return false, nil 660 } 661 balances[fromFeeAsset] -= numLots * fees.Swap 662 663 if u.isAccountLocker(fromAsset) { 664 if balances[fromFeeAsset] < numLots*fees.Refund { 665 return false, nil 666 } 667 balances[fromFeeAsset] -= numLots * fees.Refund 668 } 669 670 if u.isAccountLocker(toAsset) { 671 if balances[toFeeAsset] < numLots*fees.Redeem { 672 return false, nil 673 } 674 balances[toFeeAsset] -= numLots * fees.Redeem 675 } 676 677 return true, nil 678 } 679 680 // SufficientBalanceOnCEXTrade returns whether the bot has sufficient balance 681 // to place a CEX trade. 682 func (u *unifiedExchangeAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool { 683 var fromAssetID uint32 684 var fromAssetQty uint64 685 if sell { 686 fromAssetID = u.baseID 687 fromAssetQty = qty 688 } else { 689 fromAssetID = u.quoteID 690 fromAssetQty = calc.BaseToQuote(rate, qty) 691 } 692 693 fromAssetBal := u.CEXBalance(fromAssetID) 694 return fromAssetBal.Available >= fromAssetQty 695 } 696 697 // dexOrderInfo is used by MultiTrade to keep track of the placement index 698 // and counter trade rate of an order. 699 type dexOrderInfo struct { 700 placementIndex uint64 701 counterTradeRate uint64 702 placement *core.QtyRate 703 } 704 705 // updateDEXOrderEvent updates the event log with the current state of a 706 // pending DEX order and sends an event notification. 707 func (u *unifiedExchangeAdaptor) updateDEXOrderEvent(o *pendingDEXOrder, complete bool) { 708 o.txsMtx.RLock() 709 transactions := make([]*asset.WalletTransaction, 0, len(o.swaps)+len(o.redeems)+len(o.refunds)) 710 txIDSeen := make(map[string]bool) 711 addTxs := func(txs map[string]*asset.WalletTransaction) { 712 for _, tx := range txs { 713 if txIDSeen[tx.ID] { 714 continue 715 } 716 txIDSeen[tx.ID] = true 717 transactions = append(transactions, tx) 718 } 719 } 720 addTxs(o.swaps) 721 addTxs(o.redeems) 722 addTxs(o.refunds) 723 state := o.currentState() 724 o.txsMtx.RUnlock() 725 726 e := &MarketMakingEvent{ 727 ID: o.eventLogID, 728 TimeStamp: o.timestamp, 729 Pending: !complete, 730 BalanceEffects: combineBalanceEffects(state.dexBalanceEffects, state.cexBalanceEffects), 731 DEXOrderEvent: &DEXOrderEvent{ 732 ID: state.order.ID.String(), 733 Rate: state.order.Rate, 734 Qty: state.order.Qty, 735 Sell: state.order.Sell, 736 Transactions: transactions, 737 }, 738 } 739 740 u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState()) 741 u.notifyEvent(e) 742 } 743 744 func cexOrderEvent(trade *libxc.Trade, eventID uint64, timestamp int64) *MarketMakingEvent { 745 return &MarketMakingEvent{ 746 ID: eventID, 747 TimeStamp: timestamp, 748 Pending: !trade.Complete, 749 BalanceEffects: cexTradeBalanceEffects(trade), 750 CEXOrderEvent: &CEXOrderEvent{ 751 ID: trade.ID, 752 Rate: trade.Rate, 753 Qty: trade.Qty, 754 Sell: trade.Sell, 755 BaseFilled: trade.BaseFilled, 756 QuoteFilled: trade.QuoteFilled, 757 }, 758 } 759 } 760 761 // updateCEXOrderEvent updates the event log with the current state of a 762 // pending CEX order and sends an event notification. 763 func (u *unifiedExchangeAdaptor) updateCEXOrderEvent(trade *libxc.Trade, eventID uint64, timestamp int64) { 764 event := cexOrderEvent(trade, eventID, timestamp) 765 u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, event, u.balanceState()) 766 u.notifyEvent(event) 767 } 768 769 // updateDepositEvent updates the event log with the current state of a 770 // pending deposit and sends an event notification. 771 func (u *unifiedExchangeAdaptor) updateDepositEvent(deposit *pendingDeposit) { 772 deposit.mtx.RLock() 773 e := &MarketMakingEvent{ 774 ID: deposit.eventLogID, 775 TimeStamp: deposit.timestamp, 776 BalanceEffects: combineBalanceEffects(depositBalanceEffects(deposit.assetID, deposit.tx, deposit.cexConfirmed)), 777 Pending: !deposit.cexConfirmed || !deposit.feeConfirmed, 778 DepositEvent: &DepositEvent{ 779 AssetID: deposit.assetID, 780 Transaction: deposit.tx, 781 CEXCredit: deposit.amtCredited, 782 }, 783 } 784 deposit.mtx.RUnlock() 785 786 u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState()) 787 u.notifyEvent(e) 788 } 789 790 func (u *unifiedExchangeAdaptor) updateConfigEvent(updatedCfg *BotConfig) { 791 e := &MarketMakingEvent{ 792 ID: u.eventLogID.Add(1), 793 TimeStamp: time.Now().Unix(), 794 UpdateConfig: updatedCfg, 795 } 796 u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState()) 797 } 798 799 func (u *unifiedExchangeAdaptor) updateInventoryEvent(inventoryMods map[uint32]int64) { 800 e := &MarketMakingEvent{ 801 ID: u.eventLogID.Add(1), 802 TimeStamp: time.Now().Unix(), 803 UpdateInventory: &inventoryMods, 804 } 805 u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState()) 806 } 807 808 func combineBalanceEffects(dex, cex *BalanceEffects) *BalanceEffects { 809 effects := newBalanceEffects() 810 for assetID, v := range dex.Settled { 811 effects.Settled[assetID] += v 812 } 813 for assetID, v := range dex.Locked { 814 effects.Locked[assetID] += v 815 } 816 for assetID, v := range dex.Pending { 817 effects.Pending[assetID] += v 818 } 819 for assetID, v := range dex.Reserved { 820 effects.Reserved[assetID] += v 821 } 822 823 for assetID, v := range cex.Settled { 824 effects.Settled[assetID] += v 825 } 826 for assetID, v := range cex.Locked { 827 effects.Locked[assetID] += v 828 } 829 for assetID, v := range cex.Pending { 830 effects.Pending[assetID] += v 831 } 832 for assetID, v := range cex.Reserved { 833 effects.Reserved[assetID] += v 834 } 835 836 return effects 837 838 } 839 840 // updateWithdrawalEvent updates the event log with the current state of a 841 // pending withdrawal and sends an event notification. 842 func (u *unifiedExchangeAdaptor) updateWithdrawalEvent(withdrawal *pendingWithdrawal, tx *asset.WalletTransaction) { 843 complete := tx != nil && tx.Confirmed 844 e := &MarketMakingEvent{ 845 ID: withdrawal.eventLogID, 846 TimeStamp: withdrawal.timestamp, 847 BalanceEffects: combineBalanceEffects(withdrawal.balanceEffects()), 848 Pending: !complete, 849 WithdrawalEvent: &WithdrawalEvent{ 850 AssetID: withdrawal.assetID, 851 ID: withdrawal.withdrawalID, 852 Transaction: tx, 853 CEXDebit: withdrawal.amtWithdrawn, 854 }, 855 } 856 857 u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState()) 858 u.notifyEvent(e) 859 } 860 861 // groupedBookedOrders returns pending dex orders grouped by the placement 862 // index used to create them when they were placed with MultiTrade. 863 func (u *unifiedExchangeAdaptor) groupedBookedOrders(sells bool) (orders map[uint64][]*pendingDEXOrder) { 864 orders = make(map[uint64][]*pendingDEXOrder) 865 866 groupPendingOrder := func(pendingOrder *pendingDEXOrder) { 867 o := pendingOrder.currentState().order 868 if o.Status > order.OrderStatusBooked { 869 return 870 } 871 872 pi := pendingOrder.placementIndex 873 if sells == o.Sell { 874 if orders[pi] == nil { 875 orders[pi] = []*pendingDEXOrder{} 876 } 877 orders[pi] = append(orders[pi], pendingOrder) 878 } 879 } 880 881 u.balancesMtx.RLock() 882 defer u.balancesMtx.RUnlock() 883 884 for _, pendingOrder := range u.pendingDEXOrders { 885 groupPendingOrder(pendingOrder) 886 } 887 888 return 889 } 890 891 // rateCausesSelfMatchFunc returns a function that can be called to determine 892 // whether a rate would cause a self match. The sell parameter indicates whether 893 // the returned function will support sell or buy orders. 894 func (u *unifiedExchangeAdaptor) rateCausesSelfMatchFunc(sell bool) func(rate uint64) bool { 895 var highestExistingBuy, lowestExistingSell uint64 = 0, math.MaxUint64 896 897 for _, groupedOrders := range u.groupedBookedOrders(!sell) { 898 for _, o := range groupedOrders { 899 order := o.currentState().order 900 if sell && order.Rate > highestExistingBuy { 901 highestExistingBuy = order.Rate 902 } 903 if !sell && order.Rate < lowestExistingSell { 904 lowestExistingSell = order.Rate 905 } 906 } 907 } 908 909 return func(rate uint64) bool { 910 if sell { 911 return rate <= highestExistingBuy 912 } 913 return rate >= lowestExistingSell 914 } 915 } 916 917 // reservedForCounterTrade returns the amount that is required to be reserved 918 // on the CEX in order for this order to be counter traded when matched. 919 func reservedForCounterTrade(sellOnDEX bool, counterTradeRate, remainingQty uint64) uint64 { 920 if counterTradeRate == 0 { 921 return 0 922 } 923 924 if sellOnDEX { 925 return calc.BaseToQuote(counterTradeRate, remainingQty) 926 } 927 928 return remainingQty 929 } 930 931 func withinTolerance(rate, target uint64, driftTolerance float64) bool { 932 tolerance := uint64(float64(target) * driftTolerance) 933 lowerBound := target - tolerance 934 upperBound := target + tolerance 935 return rate >= lowerBound && rate <= upperBound 936 } 937 938 func (u *unifiedExchangeAdaptor) placeMultiTrade(placements []*dexOrderInfo, sell bool) []*core.MultiTradeResult { 939 corePlacements := make([]*core.QtyRate, 0, len(placements)) 940 for _, p := range placements { 941 corePlacements = append(corePlacements, p.placement) 942 } 943 944 botCfg := u.botCfg() 945 var walletOptions map[string]string 946 if sell { 947 walletOptions = botCfg.BaseWalletOptions 948 } else { 949 walletOptions = botCfg.QuoteWalletOptions 950 } 951 952 fromAsset, fromFeeAsset, toAsset, toFeeAsset := orderAssets(u.baseID, u.quoteID, sell) 953 multiTradeForm := &core.MultiTradeForm{ 954 Host: u.host, 955 Base: u.baseID, 956 Quote: u.quoteID, 957 Sell: sell, 958 Placements: corePlacements, 959 Options: walletOptions, 960 MaxLock: u.DEXBalance(fromAsset).Available, 961 } 962 963 newPendingDEXOrders := make([]*pendingDEXOrder, 0, len(placements)) 964 defer func() { 965 // defer until u.balancesMtx is unlocked 966 for _, o := range newPendingDEXOrders { 967 u.updateDEXOrderEvent(o, false) 968 } 969 u.sendStatsUpdate() 970 }() 971 972 u.balancesMtx.Lock() 973 defer u.balancesMtx.Unlock() 974 975 results := u.clientCore.MultiTrade([]byte{}, multiTradeForm) 976 977 if len(placements) != len(results) { 978 u.log.Errorf("unexpected number of results. expected %d, got %d", len(placements), len(results)) 979 return results 980 } 981 982 for i, res := range results { 983 if res.Error != nil { 984 continue 985 } 986 987 o := res.Order 988 var orderID order.OrderID 989 copy(orderID[:], o.ID) 990 991 dexEffects, cexEffects := newBalanceEffects(), newBalanceEffects() 992 993 dexEffects.Settled[fromAsset] -= int64(o.LockedAmt) 994 dexEffects.Settled[fromFeeAsset] -= int64(o.ParentAssetLockedAmt + o.RefundLockedAmt) 995 dexEffects.Settled[toFeeAsset] -= int64(o.RedeemLockedAmt) 996 997 dexEffects.Locked[fromAsset] += o.LockedAmt 998 dexEffects.Locked[fromFeeAsset] += o.ParentAssetLockedAmt + o.RefundLockedAmt 999 dexEffects.Locked[toFeeAsset] += o.RedeemLockedAmt 1000 1001 if o.FeesPaid != nil && o.FeesPaid.Funding > 0 { 1002 dexEffects.Settled[fromFeeAsset] -= int64(o.FeesPaid.Funding) 1003 } 1004 1005 reserved := reservedForCounterTrade(o.Sell, placements[i].counterTradeRate, o.Qty) 1006 cexEffects.Settled[toAsset] -= int64(reserved) 1007 cexEffects.Reserved[toAsset] = reserved 1008 1009 pendingOrder := &pendingDEXOrder{ 1010 eventLogID: u.eventLogID.Add(1), 1011 timestamp: time.Now().Unix(), 1012 swaps: make(map[string]*asset.WalletTransaction), 1013 redeems: make(map[string]*asset.WalletTransaction), 1014 refunds: make(map[string]*asset.WalletTransaction), 1015 swapCoinIDToTxID: make(map[string]string), 1016 redeemCoinIDToTxID: make(map[string]string), 1017 refundCoinIDToTxID: make(map[string]string), 1018 placementIndex: placements[i].placementIndex, 1019 counterTradeRate: placements[i].counterTradeRate, 1020 } 1021 1022 pendingOrder.state.Store( 1023 &dexOrderState{ 1024 order: o, 1025 dexBalanceEffects: dexEffects, 1026 cexBalanceEffects: cexEffects, 1027 counterTradeRate: pendingOrder.counterTradeRate, 1028 }) 1029 u.pendingDEXOrders[orderID] = pendingOrder 1030 newPendingDEXOrders = append(newPendingDEXOrders, u.pendingDEXOrders[orderID]) 1031 } 1032 1033 return results 1034 } 1035 1036 // TradePlacement represents a placement to be made on a DEX order book 1037 // using the MultiTrade function. A non-zero counterTradeRate indicates that 1038 // the bot intends to make a counter-trade on a CEX when matches are made on 1039 // the DEX, and this must be taken into consideration in combination with the 1040 // bot's balance on the CEX when deciding how many lots to place. This 1041 // information is also used when considering deposits and withdrawals. 1042 type TradePlacement struct { 1043 Rate uint64 `json:"rate"` 1044 Lots uint64 `json:"lots"` 1045 StandingLots uint64 `json:"standingLots"` 1046 OrderedLots uint64 `json:"orderedLots"` 1047 CounterTradeRate uint64 `json:"counterTradeRate"` 1048 RequiredDEX map[uint32]uint64 `json:"requiredDex"` 1049 RequiredCEX uint64 `json:"requiredCex"` 1050 UsedDEX map[uint32]uint64 `json:"usedDex"` 1051 UsedCEX uint64 `json:"usedCex"` 1052 Error *BotProblems `json:"error"` 1053 } 1054 1055 // setError sets the error field of the TradePlacement and updates the fields 1056 // that indicate that the trade was placed to 0. 1057 func (tp *TradePlacement) setError(err error) { 1058 if err == nil { 1059 tp.Error = nil 1060 return 1061 } 1062 tp.OrderedLots = 0 1063 tp.UsedDEX = make(map[uint32]uint64) 1064 tp.UsedCEX = 0 1065 problems := &BotProblems{} 1066 updateBotProblemsBasedOnError(problems, err) 1067 tp.Error = problems 1068 } 1069 1070 func (tp *TradePlacement) requiredLots() uint64 { 1071 if tp.Lots > tp.StandingLots { 1072 return tp.Lots - tp.StandingLots 1073 } 1074 return 0 1075 } 1076 1077 // OrderReport summarizes the results of a MultiTrade operation. 1078 type OrderReport struct { 1079 Placements []*TradePlacement `json:"placements"` 1080 Fees *OrderFees `json:"buyFees"` 1081 AvailableDEXBals map[uint32]*BotBalance `json:"availableDexBals"` 1082 RequiredDEXBals map[uint32]uint64 `json:"requiredDexBals"` 1083 UsedDEXBals map[uint32]uint64 `json:"usedDexBals"` 1084 RemainingDEXBals map[uint32]uint64 `json:"remainingDexBals"` 1085 AvailableCEXBal *BotBalance `json:"availableCexBal"` 1086 RequiredCEXBal uint64 `json:"requiredCexBal"` 1087 UsedCEXBal uint64 `json:"usedCexBal"` 1088 RemainingCEXBal uint64 `json:"remainingCexBal"` 1089 Error *BotProblems `json:"error"` 1090 } 1091 1092 func (or *OrderReport) setError(err error) { 1093 if err == nil { 1094 or.Error = nil 1095 return 1096 } 1097 if or.Error == nil { 1098 or.Error = &BotProblems{} 1099 } 1100 updateBotProblemsBasedOnError(or.Error, err) 1101 } 1102 1103 func newOrderReport(placements []*TradePlacement) *OrderReport { 1104 cpPlacements := make([]*TradePlacement, len(placements)) 1105 for i, p := range placements { 1106 cpPlacements[i] = &TradePlacement{ 1107 Rate: p.Rate, 1108 Lots: p.Lots, 1109 CounterTradeRate: p.CounterTradeRate, 1110 RequiredDEX: make(map[uint32]uint64), 1111 UsedDEX: make(map[uint32]uint64), 1112 } 1113 } 1114 1115 return &OrderReport{ 1116 AvailableDEXBals: make(map[uint32]*BotBalance), 1117 RequiredDEXBals: make(map[uint32]uint64), 1118 RemainingDEXBals: make(map[uint32]uint64), 1119 UsedDEXBals: make(map[uint32]uint64), 1120 Placements: cpPlacements, 1121 } 1122 } 1123 1124 // MultiTrade places multiple orders on the DEX order book. The placements 1125 // arguments does not represent the trades that should be placed at this time, 1126 // but rather the amount of lots that the caller expects consistently have on 1127 // the orderbook at various rates. It is expected that the caller will 1128 // periodically (each epoch) call this function with the same number of 1129 // placements in the same order, with the rates updated to reflect the current 1130 // market conditions. 1131 // 1132 // When an order is placed, the index of the placement that initiated the order 1133 // is tracked. On subsequent calls, as the rates change, the placements will be 1134 // compared with prior trades with the same placement index. If the trades on 1135 // the books differ from the rates in the placements by greater than 1136 // driftTolerance, the orders will be cancelled. As orders get filled, and there 1137 // are less than the number of lots specified in the placement on the books, 1138 // new trades will be made. 1139 // 1140 // The caller can pass a rate of 0 for any placement to indicate that all orders 1141 // that were made during previous calls to MultiTrade with the same placement index 1142 // should be cancelled. 1143 // 1144 // dexReserves and cexReserves are the amount of funds that should not be used to 1145 // place orders. These are used in case the bot is about to make a deposit or 1146 // withdrawal, and does not want those funds to get locked up in a trade. 1147 // 1148 // The placements must be passed in decreasing priority order. If there is not 1149 // enough balance to place all of the orders, the lower priority trades that 1150 // were made in previous calls will be cancelled. 1151 func (u *unifiedExchangeAdaptor) multiTrade( 1152 placements []*TradePlacement, 1153 sell bool, 1154 driftTolerance float64, 1155 currEpoch uint64, 1156 ) (_ map[order.OrderID]*dexOrderInfo, or *OrderReport) { 1157 or = newOrderReport(placements) 1158 if len(placements) == 0 { 1159 return nil, or 1160 } 1161 1162 buyFees, sellFees, err := u.orderFees() 1163 if err != nil { 1164 or.setError(err) 1165 return nil, or 1166 } 1167 or.Fees = buyFees 1168 if sell { 1169 or.Fees = sellFees 1170 } 1171 lotSize := u.lotSize.Load() 1172 1173 fromID, fromFeeID, toID, toFeeID := orderAssets(u.baseID, u.quoteID, sell) 1174 fees, fundingFees := or.Fees.Max, or.Fees.Funding 1175 1176 // First, determine the amount of balances the bot has available to place 1177 // DEX trades taking into account dexReserves. 1178 for _, assetID := range []uint32{fromID, fromFeeID, toID, toFeeID} { 1179 if _, found := or.RemainingDEXBals[assetID]; !found { 1180 or.AvailableDEXBals[assetID] = u.DEXBalance(assetID).copy() 1181 or.RemainingDEXBals[assetID] = or.AvailableDEXBals[assetID].Available 1182 } 1183 } 1184 1185 // If the placements include a counterTradeRate, the CEX balance must also 1186 // be taken into account to determine how many trades can be placed. 1187 accountForCEXBal := placements[0].CounterTradeRate > 0 1188 if accountForCEXBal { 1189 or.AvailableCEXBal = u.CEXBalance(toID).copy() 1190 or.RemainingCEXBal = or.AvailableCEXBal.Available 1191 } 1192 1193 cancels := make([]dex.Bytes, 0, len(placements)) 1194 1195 addCancel := func(o *core.Order) { 1196 if currEpoch-o.Epoch < 2 { // TODO: check epoch 1197 u.log.Debugf("multiTrade: skipping cancel not past free cancel threshold") 1198 return 1199 } 1200 cancels = append(cancels, o.ID) 1201 } 1202 1203 // keptOrders is a list of pending orders that are not being cancelled, in 1204 // decreasing order of placementIndex. If the bot doesn't have enough 1205 // balance to place an order with a higher priority (lower placementIndex) 1206 // then the lower priority orders in this list will be cancelled. 1207 keptOrders := make([]*pendingDEXOrder, 0, len(placements)) 1208 1209 for _, groupedOrders := range u.groupedBookedOrders(sell) { 1210 for _, o := range groupedOrders { 1211 order := o.currentState().order 1212 if o.placementIndex >= uint64(len(or.Placements)) { 1213 // This will happen if there is a reconfig in which there are 1214 // now less placements than before. 1215 addCancel(order) 1216 continue 1217 } 1218 1219 mustCancel := !withinTolerance(order.Rate, placements[o.placementIndex].Rate, driftTolerance) 1220 or.Placements[o.placementIndex].StandingLots += (order.Qty - order.Filled) / lotSize 1221 if or.Placements[o.placementIndex].StandingLots > or.Placements[o.placementIndex].Lots { 1222 mustCancel = true 1223 } 1224 1225 if mustCancel { 1226 u.log.Tracef("%s cancel with order rate = %s, placement rate = %s, drift tolerance = %.4f%%", 1227 u.mwh, u.fmtRate(order.Rate), u.fmtRate(placements[o.placementIndex].Rate), driftTolerance*100, 1228 ) 1229 addCancel(order) 1230 } else { 1231 keptOrders = append([]*pendingDEXOrder{o}, keptOrders...) 1232 } 1233 } 1234 } 1235 1236 rateCausesSelfMatch := u.rateCausesSelfMatchFunc(sell) 1237 1238 multiSplitBuffer := u.botCfg().multiSplitBuffer() 1239 1240 fundingReq := func(rate, lots, counterTradeRate uint64) (dexReq map[uint32]uint64, cexReq uint64) { 1241 qty := lotSize * lots 1242 swapFees := fees.Swap * lots 1243 if !sell { 1244 qty = calc.BaseToQuote(rate, qty) 1245 qty = uint64(math.Round(float64(qty) * (100 + multiSplitBuffer) / 100)) 1246 swapFees = uint64(math.Round(float64(swapFees) * (100 + multiSplitBuffer) / 100)) 1247 } 1248 dexReq = make(map[uint32]uint64) 1249 dexReq[fromID] += qty 1250 dexReq[fromFeeID] += swapFees 1251 if u.isAccountLocker(fromID) { 1252 dexReq[fromFeeID] += fees.Refund * lots 1253 } 1254 if u.isAccountLocker(toID) { 1255 dexReq[toFeeID] += fees.Redeem * lots 1256 } 1257 if accountForCEXBal { 1258 if sell { 1259 cexReq = calc.BaseToQuote(counterTradeRate, lotSize*lots) 1260 } else { 1261 cexReq = lotSize * lots 1262 } 1263 } 1264 return 1265 } 1266 1267 canAffordLots := func(rate, lots, counterTradeRate uint64) bool { 1268 dexReq, cexReq := fundingReq(rate, lots, counterTradeRate) 1269 for assetID, v := range dexReq { 1270 if or.RemainingDEXBals[assetID] < v { 1271 return false 1272 } 1273 } 1274 return or.RemainingCEXBal >= cexReq 1275 } 1276 1277 orderInfos := make([]*dexOrderInfo, 0, len(or.Placements)) 1278 1279 // Calculate required balances for each placement and the total required. 1280 placementRequired := false 1281 for _, placement := range or.Placements { 1282 if placement.requiredLots() == 0 { 1283 continue 1284 } 1285 placementRequired = true 1286 dexReq, cexReq := fundingReq(placement.Rate, placement.requiredLots(), placement.CounterTradeRate) 1287 for assetID, v := range dexReq { 1288 placement.RequiredDEX[assetID] = v 1289 or.RequiredDEXBals[assetID] += v 1290 } 1291 placement.RequiredCEX = cexReq 1292 or.RequiredCEXBal += cexReq 1293 } 1294 if placementRequired { 1295 or.RequiredDEXBals[fromFeeID] += fundingFees 1296 } 1297 1298 or.RemainingDEXBals[fromFeeID] = utils.SafeSub(or.RemainingDEXBals[fromFeeID], fundingFees) 1299 for i, placement := range or.Placements { 1300 if placement.requiredLots() == 0 { 1301 continue 1302 } 1303 1304 if rateCausesSelfMatch(placement.Rate) { 1305 u.log.Warnf("multiTrade: rate %d causes self match. Placements should be farther from mid-gap.", placement.Rate) 1306 placement.Error = &BotProblems{CausesSelfMatch: true} 1307 continue 1308 } 1309 1310 searchN := int(placement.requiredLots() + 1) 1311 lotsPlus1 := sort.Search(searchN, func(lotsi int) bool { 1312 return !canAffordLots(placement.Rate, uint64(lotsi), placement.CounterTradeRate) 1313 }) 1314 1315 var lotsToPlace uint64 1316 if lotsPlus1 > 1 { 1317 lotsToPlace = uint64(lotsPlus1) - 1 1318 placement.UsedDEX, placement.UsedCEX = fundingReq(placement.Rate, lotsToPlace, placement.CounterTradeRate) 1319 placement.OrderedLots = lotsToPlace 1320 for assetID, v := range placement.UsedDEX { 1321 or.RemainingDEXBals[assetID] -= v 1322 or.UsedDEXBals[assetID] += v 1323 } 1324 or.RemainingCEXBal -= placement.UsedCEX 1325 or.UsedCEXBal += placement.UsedCEX 1326 1327 orderInfos = append(orderInfos, &dexOrderInfo{ 1328 placementIndex: uint64(i), 1329 counterTradeRate: placement.CounterTradeRate, 1330 placement: &core.QtyRate{ 1331 Qty: lotsToPlace * lotSize, 1332 Rate: placement.Rate, 1333 }, 1334 }) 1335 } 1336 1337 // If there is insufficient balance to place a higher priority order, 1338 // cancel the lower priority orders. 1339 if lotsToPlace < placement.requiredLots() { 1340 u.log.Tracef("multiTrade(%s,%d) out of funds for more placements. %d of %d lots for rate %s", 1341 sellStr(sell), i, lotsToPlace, placement.requiredLots(), u.fmtRate(placement.Rate)) 1342 for _, o := range keptOrders { 1343 if o.placementIndex > uint64(i) { 1344 order := o.currentState().order 1345 addCancel(order) 1346 } 1347 } 1348 1349 break 1350 } 1351 } 1352 1353 if len(orderInfos) > 0 { 1354 or.UsedDEXBals[fromFeeID] += fundingFees 1355 } 1356 1357 for _, cancel := range cancels { 1358 if err := u.Cancel(cancel); err != nil { 1359 u.log.Errorf("multiTrade: error canceling order %s: %v", cancel, err) 1360 } 1361 } 1362 1363 if len(orderInfos) > 0 { 1364 results := u.placeMultiTrade(orderInfos, sell) 1365 ordered := make(map[order.OrderID]*dexOrderInfo, len(placements)) 1366 for i, res := range results { 1367 if res.Error != nil { 1368 or.Placements[orderInfos[i].placementIndex].setError(res.Error) 1369 continue 1370 } 1371 var orderID order.OrderID 1372 copy(orderID[:], res.Order.ID) 1373 ordered[orderID] = orderInfos[i] 1374 } 1375 1376 return ordered, or 1377 } 1378 1379 return nil, or 1380 } 1381 1382 // DEXTrade places a single order on the DEX order book. 1383 func (u *unifiedExchangeAdaptor) DEXTrade(rate, qty uint64, sell bool) (*core.Order, error) { 1384 enough, err := u.SufficientBalanceForDEXTrade(rate, qty, sell) 1385 if err != nil { 1386 return nil, err 1387 } 1388 if !enough { 1389 return nil, fmt.Errorf("insufficient balance") 1390 } 1391 1392 placements := []*dexOrderInfo{{ 1393 placement: &core.QtyRate{ 1394 Qty: qty, 1395 Rate: rate, 1396 }, 1397 }} 1398 1399 // multiTrade is used instead of Trade because Trade does not support 1400 // maxLock. 1401 results := u.placeMultiTrade(placements, sell) 1402 if len(results) == 0 { 1403 return nil, fmt.Errorf("no orders placed") 1404 } 1405 if results[0].Error != nil { 1406 return nil, results[0].Error 1407 } 1408 1409 return results[0].Order, nil 1410 } 1411 1412 type BotBalances struct { 1413 DEX *BotBalance `json:"dex"` 1414 CEX *BotBalance `json:"cex"` 1415 } 1416 1417 // dexBalance must be called with the balancesMtx read locked. 1418 func (u *unifiedExchangeAdaptor) dexBalance(assetID uint32) *BotBalance { 1419 bal, found := u.baseDexBalances[assetID] 1420 if !found { 1421 return &BotBalance{} 1422 } 1423 1424 totalEffects := newBalanceEffects() 1425 addEffects := func(effects *BalanceEffects) { 1426 for assetID, v := range effects.Settled { 1427 totalEffects.Settled[assetID] += v 1428 } 1429 for assetID, v := range effects.Locked { 1430 totalEffects.Locked[assetID] += v 1431 } 1432 for assetID, v := range effects.Pending { 1433 totalEffects.Pending[assetID] += v 1434 } 1435 for assetID, v := range effects.Reserved { 1436 totalEffects.Reserved[assetID] += v 1437 } 1438 } 1439 1440 for _, pendingOrder := range u.pendingDEXOrders { 1441 addEffects(pendingOrder.currentState().dexBalanceEffects) 1442 } 1443 1444 for _, pendingDeposit := range u.pendingDeposits { 1445 dexEffects, _ := pendingDeposit.balanceEffects() 1446 addEffects(dexEffects) 1447 } 1448 1449 for _, pendingWithdrawal := range u.pendingWithdrawals { 1450 dexEffects, _ := pendingWithdrawal.balanceEffects() 1451 addEffects(dexEffects) 1452 } 1453 1454 availableBalance := bal + totalEffects.Settled[assetID] 1455 if availableBalance < 0 { 1456 u.log.Errorf("negative dex balance for %s: %d", dex.BipIDSymbol(assetID), availableBalance) 1457 availableBalance = 0 1458 } 1459 1460 return &BotBalance{ 1461 Available: uint64(availableBalance), 1462 Locked: totalEffects.Locked[assetID], 1463 Pending: totalEffects.Pending[assetID], 1464 } 1465 } 1466 1467 // DEXBalance returns the balance of the bot on the DEX. 1468 func (u *unifiedExchangeAdaptor) DEXBalance(assetID uint32) *BotBalance { 1469 u.balancesMtx.RLock() 1470 defer u.balancesMtx.RUnlock() 1471 1472 return u.dexBalance(assetID) 1473 } 1474 1475 func (u *unifiedExchangeAdaptor) timeStart() int64 { 1476 return u.startTime.Load() 1477 } 1478 1479 // refreshAllPendingEvents updates the state of all pending events so that 1480 // balances will be up to date. 1481 func (u *unifiedExchangeAdaptor) refreshAllPendingEvents(ctx context.Context) { 1482 // Make copies of all maps to avoid locking balancesMtx for the entire process 1483 u.balancesMtx.Lock() 1484 pendingDEXOrders := make(map[order.OrderID]*pendingDEXOrder, len(u.pendingDEXOrders)) 1485 for oid, pendingOrder := range u.pendingDEXOrders { 1486 pendingDEXOrders[oid] = pendingOrder 1487 } 1488 pendingCEXOrders := make(map[string]*pendingCEXOrder, len(u.pendingCEXOrders)) 1489 for tradeID, pendingOrder := range u.pendingCEXOrders { 1490 pendingCEXOrders[tradeID] = pendingOrder 1491 } 1492 pendingWithdrawals := make(map[string]*pendingWithdrawal, len(u.pendingWithdrawals)) 1493 for withdrawalID, pendingWithdrawal := range u.pendingWithdrawals { 1494 pendingWithdrawals[withdrawalID] = pendingWithdrawal 1495 } 1496 pendingDeposits := make(map[string]*pendingDeposit, len(u.pendingDeposits)) 1497 for txID, pendingDeposit := range u.pendingDeposits { 1498 pendingDeposits[txID] = pendingDeposit 1499 } 1500 u.balancesMtx.Unlock() 1501 1502 for _, pendingOrder := range pendingDEXOrders { 1503 pendingOrder.txsMtx.Lock() 1504 state := pendingOrder.currentState() 1505 pendingOrder.updateState(state.order, u.clientCore.WalletTransaction, u.baseTraits, u.quoteTraits) 1506 pendingOrder.txsMtx.Unlock() 1507 } 1508 1509 for _, pendingDeposit := range pendingDeposits { 1510 pendingDeposit.mtx.RLock() 1511 id := pendingDeposit.tx.ID 1512 pendingDeposit.mtx.RUnlock() 1513 u.confirmDeposit(ctx, id) 1514 } 1515 1516 for _, pendingWithdrawal := range pendingWithdrawals { 1517 u.confirmWithdrawal(ctx, pendingWithdrawal.withdrawalID) 1518 } 1519 1520 for _, pendingOrder := range pendingCEXOrders { 1521 pendingOrder.tradeMtx.RLock() 1522 id, baseID, quoteID := pendingOrder.trade.ID, pendingOrder.trade.BaseID, pendingOrder.trade.QuoteID 1523 pendingOrder.tradeMtx.RUnlock() 1524 1525 trade, err := u.CEX.TradeStatus(ctx, id, baseID, quoteID) 1526 if err != nil { 1527 u.log.Errorf("error getting CEX trade status: %v", err) 1528 continue 1529 } 1530 1531 u.handleCEXTradeUpdate(trade) 1532 } 1533 } 1534 1535 // incompleteCexTradeBalanceEffects returns the balance effects of an 1536 // incomplete CEX trade. As soon as a CEX trade is complete, it is removed 1537 // from the pending map, so completed trades do not need to be calculated. 1538 func cexTradeBalanceEffects(trade *libxc.Trade) (effects *BalanceEffects) { 1539 effects = newBalanceEffects() 1540 1541 if trade.Complete { 1542 if trade.Sell { 1543 effects.Settled[trade.BaseID] = -int64(trade.BaseFilled) 1544 effects.Settled[trade.QuoteID] = int64(trade.QuoteFilled) 1545 } else { 1546 effects.Settled[trade.QuoteID] = -int64(trade.QuoteFilled) 1547 effects.Settled[trade.BaseID] = int64(trade.BaseFilled) 1548 } 1549 return 1550 } 1551 1552 if trade.Sell { 1553 effects.Settled[trade.BaseID] = -int64(trade.Qty) 1554 effects.Locked[trade.BaseID] = trade.Qty - trade.BaseFilled 1555 effects.Settled[trade.QuoteID] = int64(trade.QuoteFilled) 1556 } else { 1557 effects.Settled[trade.QuoteID] = -int64(calc.BaseToQuote(trade.Rate, trade.Qty)) 1558 effects.Locked[trade.QuoteID] = calc.BaseToQuote(trade.Rate, trade.Qty) - trade.QuoteFilled 1559 effects.Settled[trade.BaseID] = int64(trade.BaseFilled) 1560 } 1561 1562 return 1563 } 1564 1565 // cexBalance must be called with the balancesMtx read locked. 1566 func (u *unifiedExchangeAdaptor) cexBalance(assetID uint32) *BotBalance { 1567 totalEffects := newBalanceEffects() 1568 addEffects := func(effects *BalanceEffects) { 1569 for assetID, v := range effects.Settled { 1570 totalEffects.Settled[assetID] += v 1571 } 1572 for assetID, v := range effects.Locked { 1573 totalEffects.Locked[assetID] += v 1574 } 1575 for assetID, v := range effects.Pending { 1576 totalEffects.Pending[assetID] += v 1577 } 1578 for assetID, v := range effects.Reserved { 1579 totalEffects.Reserved[assetID] += v 1580 } 1581 } 1582 1583 for _, pendingOrder := range u.pendingCEXOrders { 1584 pendingOrder.tradeMtx.RLock() 1585 trade := pendingOrder.trade 1586 pendingOrder.tradeMtx.RUnlock() 1587 1588 addEffects(cexTradeBalanceEffects(trade)) 1589 } 1590 1591 for _, withdrawal := range u.pendingWithdrawals { 1592 _, cexEffects := withdrawal.balanceEffects() 1593 addEffects(cexEffects) 1594 } 1595 1596 // Credited deposits generally should already be part of the base balance, 1597 // but just in case the amount was credited before the wallet confirmed the 1598 // fee. 1599 for _, deposit := range u.pendingDeposits { 1600 _, cexEffects := deposit.balanceEffects() 1601 addEffects(cexEffects) 1602 } 1603 1604 for _, pendingDEXOrder := range u.pendingDEXOrders { 1605 addEffects(pendingDEXOrder.currentState().cexBalanceEffects) 1606 } 1607 1608 available := u.baseCexBalances[assetID] + totalEffects.Settled[assetID] 1609 if available < 0 { 1610 u.log.Errorf("negative CEX balance for %s: %d", dex.BipIDSymbol(assetID), available) 1611 available = 0 1612 } 1613 1614 return &BotBalance{ 1615 Available: uint64(available), 1616 Locked: totalEffects.Locked[assetID], 1617 Pending: totalEffects.Pending[assetID], 1618 Reserved: totalEffects.Reserved[assetID], 1619 } 1620 } 1621 1622 // CEXBalance returns the balance of the bot on the CEX. 1623 func (u *unifiedExchangeAdaptor) CEXBalance(assetID uint32) *BotBalance { 1624 u.balancesMtx.RLock() 1625 defer u.balancesMtx.RUnlock() 1626 1627 return u.cexBalance(assetID) 1628 } 1629 1630 func (u *unifiedExchangeAdaptor) balanceState() *BalanceState { 1631 u.balancesMtx.RLock() 1632 defer u.balancesMtx.RUnlock() 1633 1634 fromAsset, toAsset, fromFeeAsset, toFeeAsset := orderAssets(u.baseID, u.quoteID, true) 1635 1636 balances := make(map[uint32]*BotBalance, 4) 1637 assets := []uint32{fromAsset, toAsset} 1638 if fromFeeAsset != fromAsset { 1639 assets = append(assets, fromFeeAsset) 1640 } 1641 if toFeeAsset != toAsset { 1642 assets = append(assets, toFeeAsset) 1643 } 1644 1645 for _, assetID := range assets { 1646 dexBal := u.dexBalance(assetID) 1647 cexBal := u.cexBalance(assetID) 1648 balances[assetID] = &BotBalance{ 1649 Available: dexBal.Available + cexBal.Available, 1650 Locked: dexBal.Locked + cexBal.Locked, 1651 Pending: dexBal.Pending + cexBal.Pending, 1652 Reserved: dexBal.Reserved + cexBal.Reserved, 1653 } 1654 } 1655 1656 mods := make(map[uint32]int64, len(u.inventoryMods)) 1657 for assetID, mod := range u.inventoryMods { 1658 mods[assetID] = mod 1659 } 1660 1661 return &BalanceState{ 1662 FiatRates: u.fiatRates.Load().(map[uint32]float64), 1663 Balances: balances, 1664 InventoryMods: mods, 1665 } 1666 } 1667 1668 func (u *unifiedExchangeAdaptor) pendingDepositComplete(deposit *pendingDeposit) { 1669 deposit.mtx.RLock() 1670 tx := deposit.tx 1671 assetID := deposit.assetID 1672 amtCredited := deposit.amtCredited 1673 deposit.mtx.RUnlock() 1674 1675 u.balancesMtx.Lock() 1676 if _, found := u.pendingDeposits[tx.ID]; !found { 1677 u.balancesMtx.Unlock() 1678 return 1679 } 1680 1681 delete(u.pendingDeposits, tx.ID) 1682 u.baseDexBalances[assetID] -= int64(tx.Amount) 1683 var feeAssetID uint32 1684 token := asset.TokenInfo(assetID) 1685 if token != nil { 1686 feeAssetID = token.ParentID 1687 } else { 1688 feeAssetID = assetID 1689 } 1690 u.baseDexBalances[feeAssetID] -= int64(tx.Fees) 1691 u.baseCexBalances[assetID] += int64(amtCredited) 1692 u.balancesMtx.Unlock() 1693 1694 if assetID == u.baseID { 1695 u.pendingBaseRebalance.Store(false) 1696 } else { 1697 u.pendingQuoteRebalance.Store(false) 1698 } 1699 1700 dexDiffs := map[uint32]int64{} 1701 cexDiffs := map[uint32]int64{} 1702 dexDiffs[assetID] -= int64(tx.Amount) 1703 dexDiffs[feeAssetID] -= int64(tx.Fees) 1704 cexDiffs[assetID] += int64(amtCredited) 1705 1706 var msg string 1707 if amtCredited > 0 { 1708 msg = fmt.Sprintf("Deposit %s complete.", tx.ID) 1709 } else { 1710 msg = fmt.Sprintf("Deposit %s complete, but not successful.", tx.ID) 1711 } 1712 1713 u.sendStatsUpdate() 1714 1715 u.balancesMtx.RLock() 1716 u.logBalanceAdjustments(dexDiffs, cexDiffs, msg) 1717 u.balancesMtx.RUnlock() 1718 } 1719 1720 func (u *unifiedExchangeAdaptor) confirmDeposit(ctx context.Context, txID string) bool { 1721 u.balancesMtx.RLock() 1722 deposit, found := u.pendingDeposits[txID] 1723 u.balancesMtx.RUnlock() 1724 if !found { 1725 return true 1726 } 1727 1728 var updated bool 1729 defer func() { 1730 if updated { 1731 u.updateDepositEvent(deposit) 1732 } 1733 }() 1734 1735 deposit.mtx.RLock() 1736 cexConfirmed, feeConfirmed := deposit.cexConfirmed, deposit.feeConfirmed 1737 deposit.mtx.RUnlock() 1738 1739 if !cexConfirmed { 1740 complete, amtCredited := u.CEX.ConfirmDeposit(ctx, &libxc.DepositData{ 1741 AssetID: deposit.assetID, 1742 TxID: txID, 1743 AmountConventional: deposit.amtConventional, 1744 }) 1745 1746 deposit.mtx.Lock() 1747 deposit.amtCredited = amtCredited 1748 if complete { 1749 updated = true 1750 cexConfirmed = true 1751 deposit.cexConfirmed = true 1752 } 1753 deposit.mtx.Unlock() 1754 } 1755 1756 if !feeConfirmed { 1757 tx, err := u.clientCore.WalletTransaction(deposit.assetID, txID) 1758 if err != nil { 1759 u.log.Errorf("Error confirming deposit: %v", err) 1760 return false 1761 } 1762 1763 deposit.mtx.Lock() 1764 deposit.tx = tx 1765 if tx.Confirmed { 1766 feeConfirmed = true 1767 deposit.feeConfirmed = true 1768 updated = true 1769 } 1770 deposit.mtx.Unlock() 1771 } 1772 1773 if feeConfirmed && cexConfirmed { 1774 u.pendingDepositComplete(deposit) 1775 return true 1776 } 1777 1778 return false 1779 } 1780 1781 // deposit deposits funds to the CEX. The deposited funds will be removed from 1782 // the bot's wallet balance and allocated to the bot's CEX balance. After both 1783 // the fees of the deposit transaction are confirmed by the wallet and the 1784 // CEX confirms the amount they received, the onConfirm callback is called. 1785 func (u *unifiedExchangeAdaptor) deposit(ctx context.Context, assetID uint32, amount uint64) error { 1786 balance := u.DEXBalance(assetID) 1787 // TODO: estimate fee and make sure we have enough to cover it. 1788 if balance.Available < amount { 1789 return fmt.Errorf("bot has insufficient balance to deposit %d. required: %v, have: %v", assetID, amount, balance.Available) 1790 } 1791 1792 addr, err := u.CEX.GetDepositAddress(ctx, assetID) 1793 if err != nil { 1794 return err 1795 } 1796 coin, err := u.clientCore.Send([]byte{}, assetID, amount, addr, u.isWithdrawer(assetID)) 1797 if err != nil { 1798 return err 1799 } 1800 1801 if assetID == u.baseID { 1802 u.pendingBaseRebalance.Store(true) 1803 } else { 1804 u.pendingQuoteRebalance.Store(true) 1805 } 1806 1807 tx, err := u.clientCore.WalletTransaction(assetID, coin.TxID()) 1808 if err != nil { 1809 // If the wallet does not know about the transaction it just sent, 1810 // this is a serious bug in the wallet. Should Send be updated to 1811 // return asset.WalletTransaction? 1812 return err 1813 } 1814 1815 u.log.Infof("Deposited %s. TxID = %s", u.fmtQty(assetID, amount), tx.ID) 1816 1817 eventID := u.eventLogID.Add(1) 1818 ui, _ := asset.UnitInfo(assetID) 1819 deposit := &pendingDeposit{ 1820 eventLogID: eventID, 1821 timestamp: time.Now().Unix(), 1822 tx: tx, 1823 assetID: assetID, 1824 feeConfirmed: !u.isDynamicSwapper(assetID), 1825 amtConventional: float64(amount) / float64(ui.Conventional.ConversionFactor), 1826 } 1827 u.updateDepositEvent(deposit) 1828 1829 u.balancesMtx.Lock() 1830 u.pendingDeposits[tx.ID] = deposit 1831 u.balancesMtx.Unlock() 1832 1833 u.wg.Add(1) 1834 go func() { 1835 defer u.wg.Done() 1836 timer := time.NewTimer(0) 1837 defer timer.Stop() 1838 for { 1839 select { 1840 case <-timer.C: 1841 if u.confirmDeposit(ctx, tx.ID) { 1842 return 1843 } 1844 timer = time.NewTimer(time.Minute) 1845 case <-ctx.Done(): 1846 return 1847 } 1848 } 1849 }() 1850 1851 return nil 1852 } 1853 1854 // pendingWithdrawalComplete is called after a withdrawal has been confirmed. 1855 // The amount received is applied to the base balance, and the withdrawal is 1856 // removed from the pending map. 1857 func (u *unifiedExchangeAdaptor) pendingWithdrawalComplete(id string, tx *asset.WalletTransaction) { 1858 u.balancesMtx.Lock() 1859 withdrawal, found := u.pendingWithdrawals[id] 1860 if !found { 1861 u.balancesMtx.Unlock() 1862 return 1863 } 1864 delete(u.pendingWithdrawals, id) 1865 1866 if withdrawal.assetID == u.baseID { 1867 u.pendingBaseRebalance.Store(false) 1868 } else { 1869 u.pendingQuoteRebalance.Store(false) 1870 } 1871 1872 dexEffects, cexEffects := withdrawal.balanceEffects() 1873 u.baseDexBalances[withdrawal.assetID] += dexEffects.Settled[withdrawal.assetID] 1874 u.baseCexBalances[withdrawal.assetID] += cexEffects.Settled[withdrawal.assetID] 1875 u.balancesMtx.Unlock() 1876 1877 u.updateWithdrawalEvent(withdrawal, tx) 1878 u.sendStatsUpdate() 1879 1880 dexDiffs := map[uint32]int64{withdrawal.assetID: dexEffects.Settled[withdrawal.assetID]} 1881 cexDiffs := map[uint32]int64{withdrawal.assetID: cexEffects.Settled[withdrawal.assetID]} 1882 1883 u.balancesMtx.RLock() 1884 u.logBalanceAdjustments(dexDiffs, cexDiffs, fmt.Sprintf("Withdrawal %s complete.", id)) 1885 u.balancesMtx.RUnlock() 1886 } 1887 1888 func (u *unifiedExchangeAdaptor) confirmWithdrawal(ctx context.Context, id string) bool { 1889 u.balancesMtx.RLock() 1890 withdrawal, found := u.pendingWithdrawals[id] 1891 u.balancesMtx.RUnlock() 1892 if !found { 1893 u.log.Errorf("Withdrawal %s not found among pending withdrawals", id) 1894 return false 1895 } 1896 1897 withdrawal.txMtx.RLock() 1898 txID := withdrawal.txID 1899 withdrawal.txMtx.RUnlock() 1900 1901 if txID == "" { 1902 var err error 1903 _, txID, err = u.CEX.ConfirmWithdrawal(ctx, id, withdrawal.assetID) 1904 if errors.Is(err, libxc.ErrWithdrawalPending) { 1905 return false 1906 } 1907 if err != nil { 1908 u.log.Errorf("Error confirming withdrawal: %v", err) 1909 return false 1910 } 1911 1912 withdrawal.txMtx.Lock() 1913 withdrawal.txID = txID 1914 withdrawal.txMtx.Unlock() 1915 } 1916 1917 tx, err := u.clientCore.WalletTransaction(withdrawal.assetID, txID) 1918 if errors.Is(err, asset.CoinNotFoundError) { 1919 u.log.Warnf("%s wallet does not know about withdrawal tx: %s", dex.BipIDSymbol(withdrawal.assetID), id) 1920 return false 1921 } 1922 if err != nil { 1923 u.log.Errorf("Error getting wallet transaction: %v", err) 1924 return false 1925 } 1926 1927 withdrawal.txMtx.Lock() 1928 withdrawal.tx = tx 1929 withdrawal.txMtx.Unlock() 1930 1931 if tx.Confirmed { 1932 u.pendingWithdrawalComplete(id, tx) 1933 return true 1934 } 1935 1936 return false 1937 } 1938 1939 // withdraw withdraws funds from the CEX. After withdrawing, the CEX is queried 1940 // for the transaction ID. After the transaction ID is available, the wallet is 1941 // queried for the amount received. 1942 func (u *unifiedExchangeAdaptor) withdraw(ctx context.Context, assetID uint32, amount uint64) error { 1943 symbol := dex.BipIDSymbol(assetID) 1944 1945 balance := u.CEXBalance(assetID) 1946 if balance.Available < amount { 1947 return fmt.Errorf("bot has insufficient balance to withdraw %s. required: %v, have: %v", symbol, amount, balance.Available) 1948 } 1949 1950 addr, err := u.clientCore.NewDepositAddress(assetID) 1951 if err != nil { 1952 return err 1953 } 1954 1955 // Pull transparent address out of unified address. There may be a different 1956 // field "exchangeAddress" once we add support for the new special encoding 1957 // required on binance global for zec and firo. 1958 if strings.HasPrefix(addr, "unified:") { 1959 var addrs struct { 1960 Transparent string `json:"transparent"` 1961 } 1962 if err := json.Unmarshal([]byte(addr[len("unified:"):]), &addrs); err != nil { 1963 return fmt.Errorf("error decoding unified address %q: %v", addr, err) 1964 } 1965 addr = addrs.Transparent 1966 } 1967 1968 u.balancesMtx.Lock() 1969 withdrawalID, err := u.CEX.Withdraw(ctx, assetID, amount, addr) 1970 if err != nil { 1971 u.balancesMtx.Unlock() 1972 return err 1973 } 1974 1975 u.log.Infof("Withdrew %s", u.fmtQty(assetID, amount)) 1976 if assetID == u.baseID { 1977 u.pendingBaseRebalance.Store(true) 1978 } else { 1979 u.pendingQuoteRebalance.Store(true) 1980 } 1981 withdrawal := &pendingWithdrawal{ 1982 eventLogID: u.eventLogID.Add(1), 1983 timestamp: time.Now().Unix(), 1984 assetID: assetID, 1985 amtWithdrawn: amount, 1986 withdrawalID: withdrawalID, 1987 } 1988 u.pendingWithdrawals[withdrawalID] = withdrawal 1989 u.balancesMtx.Unlock() 1990 1991 u.updateWithdrawalEvent(withdrawal, nil) 1992 u.sendStatsUpdate() 1993 1994 u.wg.Add(1) 1995 go func() { 1996 defer u.wg.Done() 1997 timer := time.NewTimer(0) 1998 defer timer.Stop() 1999 for { 2000 select { 2001 case <-timer.C: 2002 if u.confirmWithdrawal(ctx, withdrawalID) { 2003 // TODO: Trigger a rebalance here somehow. Same with 2004 // confirmed deposit. Maybe confirmWithdrawal should be 2005 // checked as part of the rebalance sequence instead of in 2006 // a goroutine. 2007 return 2008 } 2009 timer = time.NewTimer(time.Minute) 2010 case <-ctx.Done(): 2011 return 2012 } 2013 } 2014 }() 2015 2016 return nil 2017 } 2018 2019 func (u *unifiedExchangeAdaptor) reversePriorityOrders(sell bool) []*dexOrderState { 2020 orderMap := u.groupedBookedOrders(sell) 2021 orderGroups := utils.MapItems(orderMap) 2022 2023 sort.Slice(orderGroups, func(i, j int) bool { 2024 return orderGroups[i][0].placementIndex > orderGroups[j][0].placementIndex 2025 }) 2026 orders := make([]*dexOrderState, 0, len(orderGroups)) 2027 for _, g := range orderGroups { 2028 // Order the group by smallest order first. 2029 states := make([]*dexOrderState, len(g)) 2030 for i, o := range g { 2031 states[i] = o.currentState() 2032 } 2033 sort.Slice(states, func(i, j int) bool { 2034 return (states[i].order.Qty - states[i].order.Filled) < (states[j].order.Qty - states[j].order.Filled) 2035 }) 2036 orders = append(orders, states...) 2037 } 2038 return orders 2039 } 2040 2041 // freeUpFunds identifies cancelable orders to free up funds for a proposed 2042 // transfer. Identified orders are sorted in reverse order of priority. For 2043 // orders with the same placement index, smaller orders are first. minToFree 2044 // specifies a minimum amount of funds to liberate. pruneMatchableTo is the 2045 // counter-asset quantity for some amount of cex balance, and we'll continue to 2046 // add cancel orders until we're not over-matching. freeUpFunds does not 2047 // actually cancel any orders. It just identifies orders that can be canceled 2048 // immediately to satisfy the conditions specified. 2049 func (u *unifiedExchangeAdaptor) freeUpFunds( 2050 assetID uint32, 2051 minToFree uint64, 2052 pruneMatchableTo uint64, 2053 currEpoch uint64, 2054 ) ([]*dexOrderState, bool) { 2055 2056 orders := u.reversePriorityOrders(assetID == u.baseID) 2057 var matchableCounterQty, freeable, persistentMatchable uint64 2058 for _, o := range orders { 2059 var matchable uint64 2060 if assetID == o.order.BaseID { 2061 matchable += calc.BaseToQuote(o.counterTradeRate, o.order.Qty) 2062 } else { 2063 matchable += o.order.Qty 2064 } 2065 matchableCounterQty += matchable 2066 if currEpoch-o.order.Epoch >= 2 { 2067 freeable += o.dexBalanceEffects.Locked[assetID] 2068 } else { 2069 persistentMatchable += matchable 2070 } 2071 } 2072 2073 if freeable < minToFree { 2074 return nil, false 2075 } 2076 if persistentMatchable > pruneMatchableTo { 2077 return nil, false 2078 } 2079 2080 if minToFree == 0 && matchableCounterQty <= pruneMatchableTo { 2081 return nil, true 2082 } 2083 2084 amtFreedByCancellingOrder := func(o *dexOrderState) (locked, counterQty uint64) { 2085 if assetID == o.order.BaseID { 2086 return o.dexBalanceEffects.Locked[assetID], calc.BaseToQuote(o.counterTradeRate, o.order.Qty) 2087 } 2088 return o.dexBalanceEffects.Locked[assetID], o.order.Qty 2089 } 2090 2091 unfreed := minToFree 2092 cancels := make([]*dexOrderState, 0, len(orders)) 2093 for _, o := range orders { 2094 if currEpoch-o.order.Epoch < 2 { 2095 continue 2096 } 2097 cancels = append(cancels, o) 2098 freed, counterQty := amtFreedByCancellingOrder(o) 2099 if freed >= unfreed { 2100 unfreed = 0 2101 } else { 2102 unfreed -= freed 2103 } 2104 matchableCounterQty -= counterQty 2105 if matchableCounterQty <= pruneMatchableTo && unfreed == 0 { 2106 break 2107 } 2108 } 2109 return cancels, true 2110 } 2111 2112 // handleCEXTradeUpdate handles a trade update from the CEX. If the trade is in 2113 // the pending map, it will be updated. If the trade is complete, the base balances 2114 // will be updated. 2115 func (u *unifiedExchangeAdaptor) handleCEXTradeUpdate(trade *libxc.Trade) { 2116 var currCEXOrder *pendingCEXOrder 2117 defer func() { 2118 if currCEXOrder != nil { 2119 u.updateCEXOrderEvent(trade, currCEXOrder.eventLogID, currCEXOrder.timestamp) 2120 u.sendStatsUpdate() 2121 } 2122 }() 2123 2124 u.balancesMtx.Lock() 2125 currCEXOrder, found := u.pendingCEXOrders[trade.ID] 2126 u.balancesMtx.Unlock() 2127 if !found { 2128 return 2129 } 2130 2131 if !trade.Complete { 2132 currCEXOrder.tradeMtx.Lock() 2133 currCEXOrder.trade = trade 2134 currCEXOrder.tradeMtx.Unlock() 2135 return 2136 } 2137 2138 u.balancesMtx.Lock() 2139 defer u.balancesMtx.Unlock() 2140 2141 delete(u.pendingCEXOrders, trade.ID) 2142 2143 if trade.BaseFilled == 0 && trade.QuoteFilled == 0 { 2144 u.log.Infof("CEX trade %s completed with zero filled amount", trade.ID) 2145 return 2146 } 2147 2148 diffs := make(map[uint32]int64) 2149 2150 balanceEffects := cexTradeBalanceEffects(trade) 2151 for assetID, v := range balanceEffects.Settled { 2152 u.baseCexBalances[assetID] += v 2153 diffs[assetID] = v 2154 } 2155 2156 u.logBalanceAdjustments(nil, diffs, fmt.Sprintf("CEX trade %s completed.", trade.ID)) 2157 } 2158 2159 // SubscribeTradeUpdates subscribes to trade updates for the bot's trades on 2160 // the CEX. This should be called before making any trades, and only once. 2161 func (w *unifiedExchangeAdaptor) SubscribeTradeUpdates() <-chan *libxc.Trade { 2162 w.subscriptionIDMtx.Lock() 2163 defer w.subscriptionIDMtx.Unlock() 2164 if w.subscriptionID != nil { 2165 w.log.Errorf("SubscribeTradeUpdates called more than once by bot %s", w.botID) 2166 return nil 2167 } 2168 2169 updates, unsubscribe, subscriptionID := w.CEX.SubscribeTradeUpdates() 2170 w.subscriptionID = &subscriptionID 2171 2172 forwardUpdates := make(chan *libxc.Trade, 256) 2173 go func() { 2174 for { 2175 select { 2176 case <-w.ctx.Done(): 2177 unsubscribe() 2178 return 2179 case note := <-updates: 2180 w.handleCEXTradeUpdate(note) 2181 select { 2182 case forwardUpdates <- note: 2183 default: 2184 w.log.Errorf("CEX trade update channel full") 2185 } 2186 } 2187 } 2188 }() 2189 2190 return forwardUpdates 2191 } 2192 2193 // Trade executes a trade on the CEX. The trade will be executed using the 2194 // bot's CEX balance. 2195 func (u *unifiedExchangeAdaptor) CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error) { 2196 if !u.SufficientBalanceForCEXTrade(baseID, quoteID, sell, rate, qty) { 2197 return nil, fmt.Errorf("insufficient balance") 2198 } 2199 2200 u.subscriptionIDMtx.RLock() 2201 subscriptionID := u.subscriptionID 2202 u.subscriptionIDMtx.RUnlock() 2203 if subscriptionID == nil { 2204 return nil, fmt.Errorf("trade called before SubscribeTradeUpdates") 2205 } 2206 2207 var trade *libxc.Trade 2208 now := time.Now().Unix() 2209 eventID := u.eventLogID.Add(1) 2210 defer func() { 2211 if trade != nil { 2212 u.updateCEXOrderEvent(trade, eventID, now) 2213 u.sendStatsUpdate() 2214 } 2215 }() 2216 2217 u.balancesMtx.Lock() 2218 defer u.balancesMtx.Unlock() 2219 2220 trade, err := u.CEX.Trade(ctx, baseID, quoteID, sell, rate, qty, *subscriptionID) 2221 u.updateCEXProblems(cexTradeProblem, u.baseID, err) 2222 if err != nil { 2223 return nil, err 2224 } 2225 2226 if trade.Complete { 2227 diffs := make(map[uint32]int64) 2228 if trade.Sell { 2229 u.baseCexBalances[trade.BaseID] -= int64(trade.BaseFilled) 2230 u.baseCexBalances[trade.QuoteID] += int64(trade.QuoteFilled) 2231 diffs[trade.BaseID] = -int64(trade.BaseFilled) 2232 diffs[trade.QuoteID] = int64(trade.QuoteFilled) 2233 } else { 2234 u.baseCexBalances[trade.BaseID] += int64(trade.BaseFilled) 2235 u.baseCexBalances[trade.QuoteID] -= int64(trade.QuoteFilled) 2236 diffs[trade.BaseID] = int64(trade.BaseFilled) 2237 diffs[trade.QuoteID] = -int64(trade.QuoteFilled) 2238 } 2239 u.logBalanceAdjustments(nil, diffs, fmt.Sprintf("CEX trade %s completed.", trade.ID)) 2240 } else { 2241 u.pendingCEXOrders[trade.ID] = &pendingCEXOrder{ 2242 trade: trade, 2243 eventLogID: eventID, 2244 timestamp: now, 2245 } 2246 } 2247 2248 return trade, nil 2249 } 2250 2251 func (u *unifiedExchangeAdaptor) fiatRate(assetID uint32) float64 { 2252 rates := u.fiatRates.Load() 2253 if rates == nil { 2254 return 0 2255 } 2256 2257 return rates.(map[uint32]float64)[assetID] 2258 } 2259 2260 // ExchangeRateFromFiatSources returns market's exchange rate using fiat sources. 2261 func (u *unifiedExchangeAdaptor) ExchangeRateFromFiatSources() uint64 { 2262 atomicCFactor, err := u.atomicConversionRateFromFiat(u.baseID, u.quoteID) 2263 if err != nil { 2264 u.log.Errorf("Error genrating atomic conversion rate: %v", err) 2265 return 0 2266 } 2267 return uint64(math.Round(atomicCFactor * calc.RateEncodingFactor)) 2268 } 2269 2270 // atomicConversionRateFromFiat generates a conversion rate suitable for 2271 // converting from atomic units of one asset to atomic units of another. 2272 // This is the same as a message-rate, but without the RateEncodingFactor, 2273 // hence a float. 2274 func (u *unifiedExchangeAdaptor) atomicConversionRateFromFiat(fromID, toID uint32) (float64, error) { 2275 fromRate := u.fiatRate(fromID) 2276 toRate := u.fiatRate(toID) 2277 if fromRate == 0 || toRate == 0 { 2278 return 0, fmt.Errorf("missing fiat rate. rate for %d = %f, rate for %d = %f", fromID, fromRate, toID, toRate) 2279 } 2280 2281 fromUI, err := asset.UnitInfo(fromID) 2282 if err != nil { 2283 return 0, fmt.Errorf("exchangeRates from asset %d not found", fromID) 2284 } 2285 toUI, err := asset.UnitInfo(toID) 2286 if err != nil { 2287 return 0, fmt.Errorf("exchangeRates to asset %d not found", toID) 2288 } 2289 2290 // v_to_atomic = v_from_atomic / from_conv_factor * convConversionRate / to_conv_factor 2291 return 1 / float64(fromUI.Conventional.ConversionFactor) * fromRate / toRate * float64(toUI.Conventional.ConversionFactor), nil 2292 } 2293 2294 // OrderFees returns the fees for a buy and sell order. The order fees are for 2295 // placing orders on the market specified by the exchangeAdaptorCfg used to 2296 // create the unifiedExchangeAdaptor. 2297 func (u *unifiedExchangeAdaptor) orderFees() (buyFees, sellFees *OrderFees, err error) { 2298 u.feesMtx.RLock() 2299 buyFees, sellFees = u.buyFees, u.sellFees 2300 u.feesMtx.RUnlock() 2301 2302 if u.buyFees == nil || u.sellFees == nil { 2303 return u.updateFeeRates() 2304 } 2305 2306 return buyFees, sellFees, nil 2307 } 2308 2309 // OrderFeesInUnits returns the estimated swap and redemption fees for either a 2310 // buy or sell order in units of either the base or quote asset. If either the 2311 // base or quote asset is a token, the fees are converted using fiat rates. 2312 // Otherwise, the rate parameter is used for the conversion. 2313 func (u *unifiedExchangeAdaptor) OrderFeesInUnits(sell, base bool, rate uint64) (uint64, error) { 2314 buyFeeRange, sellFeeRange, err := u.orderFees() 2315 if err != nil { 2316 return 0, fmt.Errorf("error getting order fees: %v", err) 2317 } 2318 2319 buyFees, sellFees := buyFeeRange.Estimated, sellFeeRange.Estimated 2320 baseFees, quoteFees := buyFees.Redeem, buyFees.Swap 2321 if sell { 2322 baseFees, quoteFees = sellFees.Swap, sellFees.Redeem 2323 } 2324 2325 convertViaFiat := func(fees uint64, fromID, toID uint32) (uint64, error) { 2326 atomicCFactor, err := u.atomicConversionRateFromFiat(fromID, toID) 2327 if err != nil { 2328 return 0, err 2329 } 2330 return uint64(math.Round(float64(fees) * atomicCFactor)), nil 2331 } 2332 2333 var baseFeesInUnits, quoteFeesInUnits uint64 2334 if tkn := asset.TokenInfo(u.baseID); tkn != nil { 2335 baseFees, err = convertViaFiat(baseFees, tkn.ParentID, u.baseID) 2336 if err != nil { 2337 return 0, err 2338 } 2339 } 2340 if tkn := asset.TokenInfo(u.quoteID); tkn != nil { 2341 quoteFees, err = convertViaFiat(quoteFees, tkn.ParentID, u.quoteID) 2342 if err != nil { 2343 return 0, err 2344 } 2345 } 2346 2347 if base { 2348 baseFeesInUnits = baseFees 2349 } else { 2350 baseFeesInUnits = calc.BaseToQuote(rate, baseFees) 2351 } 2352 2353 if base { 2354 quoteFeesInUnits = calc.QuoteToBase(rate, quoteFees) 2355 } else { 2356 quoteFeesInUnits = quoteFees 2357 } 2358 2359 return baseFeesInUnits + quoteFeesInUnits, nil 2360 } 2361 2362 // tryCancelOrders cancels all booked DEX orders that are past the free cancel 2363 // threshold. If cancelCEXOrders is true, it will also cancel CEX orders. True 2364 // is returned if all orders have been cancelled. If cancelCEXOrders is false, 2365 // false will always be returned. 2366 func (u *unifiedExchangeAdaptor) tryCancelOrders(ctx context.Context, epoch *uint64, cancelCEXOrders bool) bool { 2367 u.balancesMtx.RLock() 2368 defer u.balancesMtx.RUnlock() 2369 2370 done := true 2371 2372 freeCancel := func(orderEpoch uint64) bool { 2373 if epoch == nil { 2374 return true 2375 } 2376 return *epoch-orderEpoch >= 2 2377 } 2378 2379 cancels := make([]dex.Bytes, 0, len(u.pendingDEXOrders)) 2380 2381 for _, pendingOrder := range u.pendingDEXOrders { 2382 o := pendingOrder.currentState().order 2383 2384 orderLatestState, err := u.clientCore.Order(o.ID) 2385 if err != nil { 2386 u.log.Errorf("Error fetching order %s: %v", o.ID, err) 2387 continue 2388 } 2389 if orderLatestState.Status > order.OrderStatusBooked { 2390 continue 2391 } 2392 2393 done = false 2394 if freeCancel(o.Epoch) { 2395 err := u.clientCore.Cancel(o.ID) 2396 if err != nil { 2397 u.log.Errorf("Error canceling order %s: %v", o.ID, err) 2398 } else { 2399 cancels = append(cancels, o.ID) 2400 } 2401 } 2402 } 2403 2404 if !cancelCEXOrders { 2405 return false 2406 } 2407 2408 for _, pendingOrder := range u.pendingCEXOrders { 2409 ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 2410 defer cancel() 2411 2412 pendingOrder.tradeMtx.RLock() 2413 tradeID, baseID, quoteID := pendingOrder.trade.ID, pendingOrder.trade.BaseID, pendingOrder.trade.QuoteID 2414 pendingOrder.tradeMtx.RUnlock() 2415 2416 tradeStatus, err := u.CEX.TradeStatus(ctx, tradeID, baseID, quoteID) 2417 if err != nil { 2418 u.log.Errorf("Error getting CEX trade status: %v", err) 2419 continue 2420 } 2421 if tradeStatus.Complete { 2422 continue 2423 } 2424 2425 done = false 2426 err = u.CEX.CancelTrade(ctx, baseID, quoteID, tradeID) 2427 if err != nil { 2428 u.log.Errorf("Error canceling CEX trade %s: %v", tradeID, err) 2429 } 2430 } 2431 2432 return done 2433 } 2434 2435 func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) { 2436 book, bookFeed, err := u.clientCore.SyncBook(u.host, u.baseID, u.quoteID) 2437 if err != nil { 2438 u.log.Errorf("Error syncing book for cancellations: %v", err) 2439 u.tryCancelOrders(ctx, nil, true) 2440 return 2441 } 2442 defer bookFeed.Close() 2443 2444 mktCfg, err := u.clientCore.ExchangeMarket(u.host, u.baseID, u.quoteID) 2445 if err != nil { 2446 u.log.Errorf("Error getting market configuration: %v", err) 2447 u.tryCancelOrders(ctx, nil, true) 2448 return 2449 } 2450 2451 currentEpoch := book.CurrentEpoch() 2452 if u.tryCancelOrders(ctx, ¤tEpoch, true) { 2453 return 2454 } 2455 2456 timeout := time.Millisecond * time.Duration(3*mktCfg.EpochLen) 2457 timer := time.NewTimer(timeout) 2458 defer timer.Stop() 2459 2460 i := 0 2461 for { 2462 select { 2463 case ni, ok := <-bookFeed.Next(): 2464 if !ok { 2465 u.log.Error("Stopping bot due to nil book feed.") 2466 u.kill() 2467 return 2468 } 2469 switch epoch := ni.Payload.(type) { 2470 case *core.ResolvedEpoch: 2471 if u.tryCancelOrders(ctx, &epoch.Current, true) { 2472 return 2473 } 2474 timer.Reset(timeout) 2475 i++ 2476 } 2477 case <-timer.C: 2478 u.tryCancelOrders(ctx, nil, true) 2479 return 2480 } 2481 2482 if i >= 3 { 2483 return 2484 } 2485 } 2486 } 2487 2488 // SubscribeOrderUpdates returns a channel that sends updates for orders placed 2489 // on the DEX. This function should be called only once. 2490 func (u *unifiedExchangeAdaptor) SubscribeOrderUpdates() <-chan *core.Order { 2491 orderUpdates := make(chan *core.Order, 128) 2492 u.orderUpdates.Store(orderUpdates) 2493 return orderUpdates 2494 } 2495 2496 // isAccountLocker returns if the asset's wallet is an asset.AccountLocker. 2497 func (u *unifiedExchangeAdaptor) isAccountLocker(assetID uint32) bool { 2498 if assetID == u.baseID { 2499 return u.baseTraits.IsAccountLocker() 2500 } 2501 return u.quoteTraits.IsAccountLocker() 2502 } 2503 2504 // isDynamicSwapper returns if the asset's wallet is an asset.DynamicSwapper. 2505 func (u *unifiedExchangeAdaptor) isDynamicSwapper(assetID uint32) bool { 2506 if assetID == u.baseID { 2507 return u.baseTraits.IsDynamicSwapper() 2508 } 2509 return u.quoteTraits.IsDynamicSwapper() 2510 } 2511 2512 // isWithdrawer returns if the asset's wallet is an asset.Withdrawer. 2513 func (u *unifiedExchangeAdaptor) isWithdrawer(assetID uint32) bool { 2514 if assetID == u.baseID { 2515 return u.baseTraits.IsWithdrawer() 2516 } 2517 return u.quoteTraits.IsWithdrawer() 2518 } 2519 2520 func orderAssets(baseID, quoteID uint32, sell bool) (fromAsset, fromFeeAsset, toAsset, toFeeAsset uint32) { 2521 if sell { 2522 fromAsset = baseID 2523 toAsset = quoteID 2524 } else { 2525 fromAsset = quoteID 2526 toAsset = baseID 2527 } 2528 if token := asset.TokenInfo(fromAsset); token != nil { 2529 fromFeeAsset = token.ParentID 2530 } else { 2531 fromFeeAsset = fromAsset 2532 } 2533 if token := asset.TokenInfo(toAsset); token != nil { 2534 toFeeAsset = token.ParentID 2535 } else { 2536 toFeeAsset = toAsset 2537 } 2538 return 2539 } 2540 2541 func feeAssetID(assetID uint32) uint32 { 2542 if token := asset.TokenInfo(assetID); token != nil { 2543 return token.ParentID 2544 } 2545 return assetID 2546 } 2547 2548 func dexOrderComplete(o *core.Order) bool { 2549 if o.Status.IsActive() { 2550 return false 2551 } 2552 2553 for _, match := range o.Matches { 2554 if match.Active { 2555 return false 2556 } 2557 } 2558 2559 return o.AllFeesConfirmed 2560 } 2561 2562 // orderCoinIDs returns all of the swap, redeem, and refund transactions 2563 // involving a dex order. There may be multiple coin IDs representing the 2564 // same transaction. 2565 func orderCoinIDs(o *core.Order) (swaps map[string]bool, redeems map[string]bool, refunds map[string]bool) { 2566 swaps = make(map[string]bool) 2567 redeems = make(map[string]bool) 2568 refunds = make(map[string]bool) 2569 2570 for _, match := range o.Matches { 2571 if match.Swap != nil { 2572 swaps[match.Swap.ID.String()] = true 2573 } 2574 if match.Redeem != nil { 2575 redeems[match.Redeem.ID.String()] = true 2576 } 2577 if match.Refund != nil { 2578 refunds[match.Refund.ID.String()] = true 2579 } 2580 } 2581 2582 return 2583 } 2584 2585 func dexOrderEffects(o *core.Order, swaps, redeems, refunds map[string]*asset.WalletTransaction, counterTradeRate uint64, baseTraits, quoteTraits asset.WalletTrait) (dex, cex *BalanceEffects) { 2586 dex, cex = newBalanceEffects(), newBalanceEffects() 2587 2588 fromAsset, fromFeeAsset, toAsset, toFeeAsset := orderAssets(o.BaseID, o.QuoteID, o.Sell) 2589 2590 // Account for pending funds locked in swaps awaiting confirmation. 2591 for _, match := range o.Matches { 2592 if match.Swap == nil || match.Redeem != nil || match.Refund != nil { 2593 continue 2594 } 2595 2596 if match.Revoked { 2597 swapTx, found := swaps[match.Swap.ID.String()] 2598 if found { 2599 dex.Pending[fromAsset] += swapTx.Amount 2600 } 2601 continue 2602 } 2603 2604 var redeemAmt uint64 2605 if o.Sell { 2606 redeemAmt = calc.BaseToQuote(match.Rate, match.Qty) 2607 } else { 2608 redeemAmt = match.Qty 2609 } 2610 dex.Pending[toAsset] += redeemAmt 2611 } 2612 2613 dex.Settled[fromAsset] -= int64(o.LockedAmt) 2614 dex.Settled[fromFeeAsset] -= int64(o.ParentAssetLockedAmt + o.RefundLockedAmt) 2615 dex.Settled[toFeeAsset] -= int64(o.RedeemLockedAmt) 2616 2617 dex.Locked[fromAsset] += o.LockedAmt 2618 dex.Locked[fromFeeAsset] += o.ParentAssetLockedAmt + o.RefundLockedAmt 2619 dex.Locked[toFeeAsset] += o.RedeemLockedAmt 2620 2621 if o.FeesPaid != nil { 2622 dex.Settled[fromFeeAsset] -= int64(o.FeesPaid.Funding) 2623 } 2624 2625 for _, tx := range swaps { 2626 dex.Settled[fromAsset] -= int64(tx.Amount) 2627 dex.Settled[fromFeeAsset] -= int64(tx.Fees) 2628 } 2629 2630 var reedeemIsDynamicSwapper, refundIsDynamicSwapper bool 2631 if o.Sell { 2632 reedeemIsDynamicSwapper = quoteTraits.IsDynamicSwapper() 2633 refundIsDynamicSwapper = baseTraits.IsDynamicSwapper() 2634 } else { 2635 reedeemIsDynamicSwapper = baseTraits.IsDynamicSwapper() 2636 refundIsDynamicSwapper = quoteTraits.IsDynamicSwapper() 2637 } 2638 2639 for _, tx := range redeems { 2640 if tx.Confirmed { 2641 dex.Settled[toAsset] += int64(tx.Amount) 2642 dex.Settled[toFeeAsset] -= int64(tx.Fees) 2643 continue 2644 } 2645 2646 dex.Pending[toAsset] += tx.Amount 2647 if reedeemIsDynamicSwapper { 2648 dex.Settled[toFeeAsset] -= int64(tx.Fees) 2649 } else if dex.Pending[toFeeAsset] >= tx.Fees { 2650 dex.Pending[toFeeAsset] -= tx.Fees 2651 } 2652 } 2653 2654 for _, tx := range refunds { 2655 if tx.Confirmed { 2656 dex.Settled[fromAsset] += int64(tx.Amount) 2657 dex.Settled[fromFeeAsset] -= int64(tx.Fees) 2658 continue 2659 } 2660 2661 dex.Pending[fromAsset] += tx.Amount 2662 if refundIsDynamicSwapper { 2663 dex.Settled[fromFeeAsset] -= int64(tx.Fees) 2664 } else if dex.Pending[fromFeeAsset] >= tx.Fees { 2665 dex.Pending[fromFeeAsset] -= tx.Fees 2666 } 2667 } 2668 2669 if counterTradeRate > 0 { 2670 reserved := reservedForCounterTrade(o.Sell, counterTradeRate, o.Qty-o.Filled) 2671 cex.Settled[toAsset] -= int64(reserved) 2672 cex.Reserved[toAsset] += reserved 2673 } 2674 2675 return 2676 } 2677 2678 // updateState should only be called with txsMtx write locked. The dex order 2679 // state is stored as an atomic.Value in order to allow reads without locking. 2680 // The mutex only needs to be locked for reading if the caller wants a consistent 2681 // view of the transactions and the state. 2682 func (p *pendingDEXOrder) updateState(o *core.Order, getTx func(uint32, string) (*asset.WalletTransaction, error), baseTraits, quoteTraits asset.WalletTrait) { 2683 swaps, redeems, refunds := orderCoinIDs(o) 2684 2685 // Add new txs to tx cache 2686 fromAsset, _, toAsset, _ := orderAssets(o.BaseID, o.QuoteID, o.Sell) 2687 processTxs := func(assetID uint32, txs map[string]*asset.WalletTransaction, coinIDs map[string]bool, coinIDToTxID map[string]string) { 2688 // Query the wallet regarding all unconfirmed transactions 2689 for txID, oldTx := range txs { 2690 if oldTx.Confirmed { 2691 continue 2692 } 2693 tx, err := getTx(assetID, txID) 2694 if err != nil { 2695 continue 2696 } 2697 txs[tx.ID] = tx 2698 } 2699 2700 // Add new txs to tx cache 2701 for coinID := range coinIDs { 2702 txID, found := coinIDToTxID[coinID] 2703 if found { 2704 continue 2705 } 2706 if _, found := txs[txID]; found { 2707 continue 2708 } 2709 tx, err := getTx(assetID, coinID) 2710 if err != nil { 2711 continue 2712 } 2713 coinIDToTxID[coinID] = tx.ID 2714 txs[tx.ID] = tx 2715 } 2716 } 2717 2718 processTxs(fromAsset, p.swaps, swaps, p.swapCoinIDToTxID) 2719 processTxs(toAsset, p.redeems, redeems, p.redeemCoinIDToTxID) 2720 processTxs(fromAsset, p.refunds, refunds, p.refundCoinIDToTxID) 2721 2722 dexEffects, cexEffects := dexOrderEffects(o, p.swaps, p.redeems, p.refunds, p.counterTradeRate, baseTraits, quoteTraits) 2723 p.state.Store(&dexOrderState{ 2724 order: o, 2725 dexBalanceEffects: dexEffects, 2726 cexBalanceEffects: cexEffects, 2727 counterTradeRate: p.counterTradeRate, 2728 }) 2729 } 2730 2731 // updatePendingDEXOrder updates a pending DEX order based its current state. 2732 // If the order is complete, its effects are applied to the base balance, 2733 // and it is removed from the pending list. 2734 func (u *unifiedExchangeAdaptor) handleDEXOrderUpdate(o *core.Order) { 2735 var orderID order.OrderID 2736 copy(orderID[:], o.ID) 2737 2738 u.balancesMtx.RLock() 2739 pendingOrder, found := u.pendingDEXOrders[orderID] 2740 u.balancesMtx.RUnlock() 2741 if !found { 2742 return 2743 } 2744 2745 pendingOrder.txsMtx.Lock() 2746 pendingOrder.updateState(o, u.clientCore.WalletTransaction, u.baseTraits, u.quoteTraits) 2747 dexEffects := pendingOrder.currentState().dexBalanceEffects 2748 var havePending bool 2749 for _, v := range dexEffects.Pending { 2750 if v > 0 { 2751 havePending = true 2752 break 2753 } 2754 } 2755 pendingOrder.txsMtx.Unlock() 2756 2757 orderUpdates := u.orderUpdates.Load() 2758 if orderUpdates != nil { 2759 orderUpdates.(chan *core.Order) <- o 2760 } 2761 2762 complete := !havePending && dexOrderComplete(o) 2763 // If complete, remove the order from the pending list, and update the 2764 // bot's balance. 2765 2766 if complete { // TODO: complete when all fees are confirmed 2767 u.balancesMtx.Lock() 2768 delete(u.pendingDEXOrders, orderID) 2769 2770 adjustedBals := false 2771 for assetID, diff := range dexEffects.Settled { 2772 adjustedBals = adjustedBals || diff != 0 2773 u.baseDexBalances[assetID] += diff 2774 } 2775 2776 if adjustedBals { 2777 u.logBalanceAdjustments(dexEffects.Settled, nil, fmt.Sprintf("DEX order %s complete.", orderID)) 2778 } 2779 u.balancesMtx.Unlock() 2780 } 2781 2782 u.updateDEXOrderEvent(pendingOrder, complete) 2783 } 2784 2785 func (u *unifiedExchangeAdaptor) handleServerConfigUpdate() { 2786 coreMkt, err := u.clientCore.ExchangeMarket(u.host, u.baseID, u.quoteID) 2787 if err != nil { 2788 u.log.Errorf("Stopping bot due to error getting market params: %v", err) 2789 u.kill() 2790 return 2791 } 2792 2793 if coreMkt.LotSize == u.lotSize.Load() && coreMkt.RateStep == u.rateStep.Load() { 2794 return 2795 } 2796 2797 err = u.withPause(func() error { 2798 if coreMkt.LotSize != u.lotSize.Load() { 2799 cfg := u.botCfg() 2800 copy := cfg.copy() 2801 copy.updateLotSize(u.lotSize.Load(), coreMkt.LotSize) 2802 err := u.updateConfig(copy) 2803 if err != nil { 2804 return err 2805 } 2806 u.lotSize.Store(coreMkt.LotSize) 2807 } 2808 u.rateStep.Store(coreMkt.RateStep) 2809 return nil 2810 }) 2811 if err != nil { 2812 u.log.Errorf("Error updating config due to server config update. stopping bot: %v", err) 2813 u.kill() 2814 } 2815 } 2816 2817 func (u *unifiedExchangeAdaptor) handleDEXNotification(n core.Notification) { 2818 switch note := n.(type) { 2819 case *core.OrderNote: 2820 u.handleDEXOrderUpdate(note.Order) 2821 case *core.MatchNote: 2822 o, err := u.clientCore.Order(note.OrderID) 2823 if err != nil { 2824 u.log.Errorf("handleDEXNotification: failed to get order %s: %v", note.OrderID, err) 2825 return 2826 } 2827 u.handleDEXOrderUpdate(o) 2828 cfg := u.botCfg() 2829 if cfg.Host != note.Host || u.mwh.ID() != note.MarketID { 2830 return 2831 } 2832 if note.Topic() == core.TopicRedemptionConfirmed { 2833 u.runStats.completedMatches.Add(1) 2834 fiatRates := u.fiatRates.Load().(map[uint32]float64) 2835 if r := fiatRates[cfg.BaseID]; r > 0 && note.Match != nil { 2836 ui, _ := asset.UnitInfo(cfg.BaseID) 2837 u.runStats.tradedUSD.Lock() 2838 u.runStats.tradedUSD.v += float64(note.Match.Qty) / float64(ui.Conventional.ConversionFactor) * r 2839 u.runStats.tradedUSD.Unlock() 2840 } 2841 } 2842 case *core.FiatRatesNote: 2843 u.fiatRates.Store(note.FiatRates) 2844 case *core.ServerConfigUpdateNote: 2845 if note.Host != u.host { 2846 return 2847 } 2848 u.handleServerConfigUpdate() 2849 } 2850 } 2851 2852 // Lot costs are the reserves and fees associated with current market rates. The 2853 // per-lot reservations estimates include booking fees, and redemption fees if 2854 // the asset is the counter-asset's parent asset. The quote estimates are based 2855 // on vwap estimates using cexCounterRates, 2856 type lotCosts struct { 2857 dexBase, dexQuote, 2858 cexBase, cexQuote uint64 2859 baseRedeem, quoteRedeem uint64 2860 baseFunding, quoteFunding uint64 // per multi-order 2861 } 2862 2863 func (u *unifiedExchangeAdaptor) lotCosts(sellVWAP, buyVWAP uint64) (*lotCosts, error) { 2864 perLot := new(lotCosts) 2865 buyFees, sellFees, err := u.orderFees() 2866 if err != nil { 2867 return nil, fmt.Errorf("error getting order fees: %w", err) 2868 } 2869 lotSize := u.lotSize.Load() 2870 perLot.dexBase = lotSize 2871 if u.baseID == u.baseFeeID { 2872 perLot.dexBase += sellFees.BookingFeesPerLot 2873 } 2874 perLot.cexBase = lotSize 2875 perLot.baseRedeem = buyFees.Max.Redeem 2876 perLot.baseFunding = sellFees.Funding 2877 2878 dexQuoteLot := calc.BaseToQuote(sellVWAP, lotSize) 2879 cexQuoteLot := calc.BaseToQuote(buyVWAP, lotSize) 2880 perLot.dexQuote = dexQuoteLot 2881 if u.quoteID == u.quoteFeeID { 2882 perLot.dexQuote += buyFees.BookingFeesPerLot 2883 } 2884 perLot.cexQuote = cexQuoteLot 2885 perLot.quoteRedeem = sellFees.Max.Redeem 2886 perLot.quoteFunding = buyFees.Funding 2887 return perLot, nil 2888 } 2889 2890 // distribution is a collection of asset distributions and per-lot estimates. 2891 type distribution struct { 2892 baseInv *assetInventory 2893 quoteInv *assetInventory 2894 perLot *lotCosts 2895 } 2896 2897 func (u *unifiedExchangeAdaptor) newDistribution(perLot *lotCosts) *distribution { 2898 return &distribution{ 2899 baseInv: u.inventory(u.baseID, perLot.dexBase, perLot.cexBase), 2900 quoteInv: u.inventory(u.quoteID, perLot.dexQuote, perLot.cexQuote), 2901 perLot: perLot, 2902 } 2903 } 2904 2905 // optimizeTransfers populates the toDeposit and toWithdraw fields of the base 2906 // and quote assetDistribution. To find the best asset distribution, a series 2907 // of possible target configurations are tested and the distribution that 2908 // results in the highest matchability is chosen. 2909 func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLots, dexBuyLots, maxSellLots, maxBuyLots uint64) { 2910 baseInv, quoteInv := dist.baseInv, dist.quoteInv 2911 perLot := dist.perLot 2912 2913 if u.autoRebalanceCfg == nil { 2914 return 2915 } 2916 minBaseTransfer, minQuoteTransfer := u.autoRebalanceCfg.MinBaseTransfer, u.autoRebalanceCfg.MinQuoteTransfer 2917 2918 additionalBaseFees, additionalQuoteFees := perLot.baseFunding, perLot.quoteFunding 2919 if u.baseID == u.quoteFeeID { 2920 additionalBaseFees += perLot.baseRedeem * dexBuyLots 2921 } 2922 if u.quoteID == u.baseFeeID { 2923 additionalQuoteFees += perLot.quoteRedeem * dexSellLots 2924 } 2925 var baseAvail, quoteAvail uint64 2926 if baseInv.total > additionalBaseFees { 2927 baseAvail = baseInv.total - additionalBaseFees 2928 } 2929 if quoteInv.total > additionalQuoteFees { 2930 quoteAvail = quoteInv.total - additionalQuoteFees 2931 } 2932 2933 // matchability is the number of lots that can be matched with a specified 2934 // asset distribution. 2935 matchability := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots uint64) uint64 { 2936 sells := utils.Min(dexBaseLots, cexQuoteLots, maxSellLots) 2937 buys := utils.Min(dexQuoteLots, cexBaseLots, maxBuyLots) 2938 return buys + sells 2939 } 2940 2941 currentScore := matchability(baseInv.dexLots, quoteInv.dexLots, baseInv.cexLots, quoteInv.cexLots) 2942 2943 // targetedSplit finds a distribution that targets a specified ratio of 2944 // dex-to-cex. 2945 targetedSplit := func(avail, dexTarget, cexTarget, dexLot, cexLot uint64) (dexLots, cexLots, extra uint64) { 2946 if dexTarget+cexTarget == 0 { 2947 return 2948 } 2949 cexR := float64(cexTarget*cexLot) / float64(dexTarget*dexLot+cexTarget*cexLot) 2950 cexLots = uint64(math.Round(cexR*float64(avail))) / cexLot 2951 cexBal := cexLots * cexLot 2952 dexLots = (avail - cexBal) / dexLot 2953 dexBal := dexLots * dexLot 2954 if cexLot < dexLot { 2955 cexLots = (avail - dexBal) / cexLot 2956 cexBal = cexLots * cexLot 2957 } 2958 extra = avail - cexBal - dexBal 2959 return 2960 } 2961 2962 baseSplit := func(dexTarget, cexTarget uint64) (dexLots, cexLots, extra uint64) { 2963 return targetedSplit(baseAvail, utils.Min(dexTarget, maxSellLots), utils.Min(cexTarget, maxBuyLots), perLot.dexBase, perLot.cexBase) 2964 } 2965 quoteSplit := func(dexTarget, cexTarget uint64) (dexLots, cexLots, extra uint64) { 2966 return targetedSplit(quoteAvail, utils.Min(dexTarget, maxBuyLots), utils.Min(cexTarget, maxSellLots), perLot.dexQuote, perLot.cexQuote) 2967 } 2968 2969 // We'll keep track of any distributions that have a matchability score 2970 // better than the score for the current distribution. 2971 type scoredSplit struct { 2972 score uint64 // matchability 2973 // spread is just the minimum of baseDeposit, baseWithdraw, quoteDeposit 2974 // and quoteWithdraw. This is tertiary criteria for prioritizing splits, 2975 // with a higher spread being preferable. 2976 spread uint64 2977 fees uint64 2978 baseDeposit, baseWithdraw uint64 2979 quoteDeposit, quoteWithdraw uint64 2980 } 2981 baseSplits := [][2]uint64{ 2982 {baseInv.dex, baseInv.cex}, // current 2983 {dexSellLots, dexBuyLots}, // ideal 2984 {quoteInv.cex, quoteInv.dex}, // match the counter asset 2985 } 2986 quoteSplits := [][2]uint64{ 2987 {quoteInv.dex, quoteInv.cex}, 2988 {dexBuyLots, dexSellLots}, 2989 {baseInv.cex, baseInv.dex}, 2990 } 2991 2992 splits := make([]*scoredSplit, 0) 2993 // scoreSplit gets a score for the proposed asset distribution and, if the 2994 // score is higher than currentScore, saves the result to the splits slice. 2995 scoreSplit := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote uint64) { 2996 score := matchability(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots) 2997 if score <= currentScore { 2998 return 2999 } 3000 3001 var fees uint64 3002 var baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64 3003 if dexBaseLots != baseInv.dexLots || cexBaseLots != baseInv.cexLots { 3004 fees++ 3005 dexTarget := dexBaseLots*perLot.dexBase + additionalBaseFees + extraBase 3006 cexTarget := cexBaseLots * perLot.cexBase 3007 3008 if dexTarget > baseInv.dex { 3009 if withdraw := dexTarget - baseInv.dex; withdraw >= minBaseTransfer { 3010 baseWithdraw = withdraw 3011 } else { 3012 return 3013 } 3014 } else if cexTarget > baseInv.cex { 3015 if deposit := cexTarget - baseInv.cex; deposit >= minBaseTransfer { 3016 baseDeposit = deposit 3017 } else { 3018 return 3019 } 3020 } 3021 // TODO: Use actual fee estimates. 3022 if u.baseID == 0 || u.baseID == 42 { 3023 fees++ 3024 } 3025 } 3026 if dexQuoteLots != quoteInv.dexLots || cexQuoteLots != quoteInv.cexLots { 3027 fees++ 3028 dexTarget := dexQuoteLots*perLot.dexQuote + additionalQuoteFees + (extraQuote / 2) 3029 cexTarget := cexQuoteLots*perLot.cexQuote + (extraQuote / 2) 3030 if dexTarget > quoteInv.dex { 3031 if withdraw := dexTarget - quoteInv.dex; withdraw >= minQuoteTransfer { 3032 quoteWithdraw = withdraw 3033 } else { 3034 return 3035 } 3036 3037 } else if cexTarget > quoteInv.cex { 3038 if deposit := cexTarget - quoteInv.cex; deposit >= minQuoteTransfer { 3039 quoteDeposit = deposit 3040 } else { 3041 return 3042 } 3043 } 3044 if u.quoteID == 0 || u.quoteID == 60 { 3045 fees++ 3046 } 3047 } 3048 3049 splits = append(splits, &scoredSplit{ 3050 score: score, 3051 fees: fees, 3052 baseDeposit: baseDeposit, 3053 baseWithdraw: baseWithdraw, 3054 quoteDeposit: quoteDeposit, 3055 quoteWithdraw: quoteWithdraw, 3056 spread: utils.Min(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots), 3057 }) 3058 } 3059 3060 // Try to hit all possible combinations. 3061 for _, b := range baseSplits { 3062 dexBaseLots, cexBaseLots, extraBase := baseSplit(b[0], b[1]) 3063 dexQuoteLots, cexQuoteLots, extraQuote := quoteSplit(cexBaseLots, dexBaseLots) 3064 scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote) 3065 for _, q := range quoteSplits { 3066 dexQuoteLots, cexQuoteLots, extraQuote = quoteSplit(q[0], q[1]) 3067 scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote) 3068 } 3069 } 3070 // Try in both directions. 3071 for _, q := range quoteSplits { 3072 dexQuoteLots, cexQuoteLots, extraQuote := quoteSplit(q[0], q[1]) 3073 dexBaseLots, cexBaseLots, extraBase := baseSplit(cexQuoteLots, dexQuoteLots) 3074 scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote) 3075 for _, b := range baseSplits { 3076 dexBaseLots, cexBaseLots, extraBase := baseSplit(b[0], b[1]) 3077 scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote) 3078 } 3079 } 3080 3081 if len(splits) == 0 { 3082 return 3083 } 3084 3085 // Sort by score, then fees, then spread. 3086 sort.Slice(splits, func(ii, ji int) bool { 3087 i, j := splits[ii], splits[ji] 3088 return i.score > j.score || (i.score == j.score && (i.fees < j.fees || i.spread > j.spread)) 3089 }) 3090 split := splits[0] 3091 baseInv.toDeposit = split.baseDeposit 3092 baseInv.toWithdraw = split.baseWithdraw 3093 quoteInv.toDeposit = split.quoteDeposit 3094 quoteInv.toWithdraw = split.quoteWithdraw 3095 } 3096 3097 // transfer attempts to perform the transers specified in the distribution. 3098 func (u *unifiedExchangeAdaptor) transfer(dist *distribution, currEpoch uint64) (actionTaken bool, err error) { 3099 baseInv, quoteInv := dist.baseInv, dist.quoteInv 3100 if baseInv.toDeposit+baseInv.toWithdraw+quoteInv.toDeposit+quoteInv.toWithdraw == 0 { 3101 return false, nil 3102 } 3103 3104 var cancels []*dexOrderState 3105 if baseInv.toDeposit > 0 || quoteInv.toWithdraw > 0 { 3106 var toFree uint64 3107 if baseInv.dexAvail < baseInv.toDeposit { 3108 if baseInv.dexAvail+baseInv.dexPending >= baseInv.toDeposit { 3109 u.log.Tracef("Waiting on pending balance for base deposit") 3110 return false, nil 3111 } 3112 toFree = baseInv.toDeposit - baseInv.dexAvail 3113 } 3114 counterQty := quoteInv.cex - quoteInv.toWithdraw + quoteInv.toDeposit 3115 cs, ok := u.freeUpFunds(u.baseID, toFree, counterQty, currEpoch) 3116 if !ok { 3117 if u.log.Level() == dex.LevelTrace { 3118 u.log.Tracef( 3119 "Unable to free up funds for deposit = %s, withdraw = %s, "+ 3120 "counter-quantity = %s, to free = %s, dex pending = %s", 3121 u.fmtBase(baseInv.toDeposit), u.fmtQuote(quoteInv.toWithdraw), u.fmtQuote(counterQty), 3122 u.fmtBase(toFree), u.fmtBase(baseInv.dexPending), 3123 ) 3124 } 3125 return false, nil 3126 } 3127 cancels = cs 3128 } 3129 if quoteInv.toDeposit > 0 || baseInv.toWithdraw > 0 { 3130 var toFree uint64 3131 if quoteInv.dexAvail < quoteInv.toDeposit { 3132 if quoteInv.dexAvail+quoteInv.dexPending >= quoteInv.toDeposit { 3133 // waiting on pending 3134 u.log.Tracef("Waiting on pending balance for quote deposit") 3135 return false, nil 3136 } 3137 toFree = quoteInv.toDeposit - quoteInv.dexAvail 3138 } 3139 counterQty := baseInv.cex - baseInv.toWithdraw + baseInv.toDeposit 3140 cs, ok := u.freeUpFunds(u.quoteID, toFree, counterQty, currEpoch) 3141 if !ok { 3142 if u.log.Level() == dex.LevelTrace { 3143 u.log.Tracef( 3144 "Unable to free up funds for deposit = %s, withdraw = %s, "+ 3145 "counter-quantity = %s, to free = %s, dex pending = %s", 3146 u.fmtQuote(quoteInv.toDeposit), u.fmtBase(baseInv.toWithdraw), u.fmtBase(counterQty), 3147 u.fmtQuote(toFree), u.fmtQuote(quoteInv.dexPending), 3148 ) 3149 } 3150 return false, nil 3151 } 3152 cancels = append(cancels, cs...) 3153 } 3154 3155 if len(cancels) > 0 { 3156 for _, o := range cancels { 3157 if err := u.Cancel(o.order.ID); err != nil { 3158 return false, fmt.Errorf("error canceling order: %w", err) 3159 } 3160 } 3161 return true, nil 3162 } 3163 3164 if baseInv.toDeposit > 0 { 3165 err := u.deposit(u.ctx, u.baseID, baseInv.toDeposit) 3166 u.updateCEXProblems(cexDepositProblem, u.baseID, err) 3167 if err != nil { 3168 return false, fmt.Errorf("error depositing base: %w", err) 3169 } 3170 } else if baseInv.toWithdraw > 0 { 3171 err := u.withdraw(u.ctx, u.baseID, baseInv.toWithdraw) 3172 u.updateCEXProblems(cexWithdrawProblem, u.baseID, err) 3173 if err != nil { 3174 return false, fmt.Errorf("error withdrawing base: %w", err) 3175 } 3176 } 3177 3178 if quoteInv.toDeposit > 0 { 3179 err := u.deposit(u.ctx, u.quoteID, quoteInv.toDeposit) 3180 u.updateCEXProblems(cexDepositProblem, u.quoteID, err) 3181 if err != nil { 3182 return false, fmt.Errorf("error depositing quote: %w", err) 3183 } 3184 } else if quoteInv.toWithdraw > 0 { 3185 err := u.withdraw(u.ctx, u.quoteID, quoteInv.toWithdraw) 3186 u.updateCEXProblems(cexWithdrawProblem, u.quoteID, err) 3187 if err != nil { 3188 return false, fmt.Errorf("error withdrawing quote: %w", err) 3189 } 3190 } 3191 return true, nil 3192 } 3193 3194 // assetInventory is an accounting of the distribution of base- or quote-asset 3195 // funding. 3196 type assetInventory struct { 3197 dex uint64 3198 dexAvail uint64 3199 dexPending uint64 3200 dexLocked uint64 3201 dexLots uint64 3202 3203 cex uint64 3204 cexAvail uint64 3205 cexPending uint64 3206 cexReserved uint64 3207 cexLocked uint64 3208 cexLots uint64 3209 3210 total uint64 3211 3212 toDeposit uint64 3213 toWithdraw uint64 3214 } 3215 3216 // inventory generates a current view of the the bot's asset distribution. 3217 // Use optimizeTransfers to set toDeposit and toWithdraw. 3218 func (u *unifiedExchangeAdaptor) inventory(assetID uint32, dexLot, cexLot uint64) (b *assetInventory) { 3219 b = new(assetInventory) 3220 u.balancesMtx.RLock() 3221 defer u.balancesMtx.RUnlock() 3222 3223 dexBalance := u.dexBalance(assetID) 3224 b.dexAvail = dexBalance.Available 3225 b.dexPending = dexBalance.Pending 3226 b.dexLocked = dexBalance.Locked 3227 b.dex = dexBalance.Available + dexBalance.Locked + dexBalance.Pending 3228 b.dexLots = b.dex / dexLot 3229 cexBalance := u.cexBalance(assetID) 3230 b.cexAvail = cexBalance.Available 3231 b.cexPending = cexBalance.Pending 3232 b.cexReserved = cexBalance.Reserved 3233 b.cexLocked = cexBalance.Locked 3234 b.cex = cexBalance.Available + cexBalance.Reserved + cexBalance.Pending 3235 b.cexLots = b.cex / cexLot 3236 b.total = b.dex + b.cex 3237 return 3238 } 3239 3240 // cexCounterRates attempts to get vwap estimates for the cex book for a 3241 // specified number of lots. If the book is too empty for the specified number 3242 // of lots, a 1-lot estimate will be attempted too. 3243 func (u *unifiedExchangeAdaptor) cexCounterRates(cexBuyLots, cexSellLots uint64) (dexBuyRate, dexSellRate uint64, err error) { 3244 lotSize := u.lotSize.Load() 3245 tryLots := func(b, s uint64) (uint64, uint64, bool, error) { 3246 if b == 0 { 3247 b = 1 3248 } 3249 if s == 0 { 3250 s = 1 3251 } 3252 buyRate, _, filled, err := u.CEX.VWAP(u.baseID, u.quoteID, true, lotSize*s) 3253 if err != nil { 3254 return 0, 0, false, fmt.Errorf("error calculating dex buy price for quote conversion: %w", err) 3255 } 3256 if !filled { 3257 return 0, 0, false, nil 3258 } 3259 sellRate, _, filled, err := u.CEX.VWAP(u.baseID, u.quoteID, false, lotSize*b) 3260 if err != nil { 3261 return 0, 0, false, fmt.Errorf("error calculating dex sell price for quote conversion: %w", err) 3262 } 3263 if !filled { 3264 return 0, 0, false, nil 3265 } 3266 return buyRate, sellRate, true, nil 3267 } 3268 var filled bool 3269 if dexBuyRate, dexSellRate, filled, err = tryLots(cexBuyLots, cexSellLots); err != nil || filled { 3270 return 3271 } 3272 u.log.Tracef("Failed to get cex counter-rate for requested lots. Trying 1 lot estimate") 3273 dexBuyRate, dexSellRate, filled, err = tryLots(1, 1) 3274 if err != nil { 3275 return 3276 } 3277 if !filled { 3278 err = errors.New("cex book too empty to get a counter-rate estimate") 3279 } 3280 return 3281 } 3282 3283 // bookingFees are the per-lot fees that have to be available before placing an 3284 // order. 3285 func (u *unifiedExchangeAdaptor) bookingFees(buyFees, sellFees *LotFees) (buyBookingFeesPerLot, sellBookingFeesPerLot uint64) { 3286 buyBookingFeesPerLot = buyFees.Swap 3287 // If we're redeeming on the same chain, add redemption fees. 3288 if u.quoteFeeID == u.baseFeeID { 3289 buyBookingFeesPerLot += buyFees.Redeem 3290 } 3291 // EVM assets need to reserve refund gas. 3292 if u.quoteTraits.IsAccountLocker() { 3293 buyBookingFeesPerLot += buyFees.Refund 3294 } 3295 sellBookingFeesPerLot = sellFees.Swap 3296 if u.baseFeeID == u.quoteFeeID { 3297 sellBookingFeesPerLot += sellFees.Redeem 3298 } 3299 if u.baseTraits.IsAccountLocker() { 3300 sellBookingFeesPerLot += sellFees.Refund 3301 } 3302 return 3303 } 3304 3305 // updateFeeRates updates the cached fee rates for placing orders on the market 3306 // specified by the exchangeAdaptorCfg used to create the unifiedExchangeAdaptor. 3307 func (u *unifiedExchangeAdaptor) updateFeeRates() (buyFees, sellFees *OrderFees, err error) { 3308 defer func() { 3309 if err == nil { 3310 return 3311 } 3312 3313 // In case of an error, clear the cached fees to avoid using stale data. 3314 u.feesMtx.Lock() 3315 defer u.feesMtx.Unlock() 3316 u.buyFees = nil 3317 u.sellFees = nil 3318 }() 3319 3320 maxBaseFees, maxQuoteFees, err := marketFees(u.clientCore, u.host, u.baseID, u.quoteID, true) 3321 if err != nil { 3322 return nil, nil, err 3323 } 3324 3325 estBaseFees, estQuoteFees, err := marketFees(u.clientCore, u.host, u.baseID, u.quoteID, false) 3326 if err != nil { 3327 return nil, nil, err 3328 } 3329 3330 botCfg := u.botCfg() 3331 maxBuyPlacements, maxSellPlacements := botCfg.maxPlacements() 3332 3333 buyFundingFees, err := u.clientCore.MaxFundingFees(u.quoteID, u.host, maxBuyPlacements, botCfg.QuoteWalletOptions) 3334 if err != nil { 3335 return nil, nil, fmt.Errorf("failed to get buy funding fees: %v", err) 3336 } 3337 3338 sellFundingFees, err := u.clientCore.MaxFundingFees(u.baseID, u.host, maxSellPlacements, botCfg.BaseWalletOptions) 3339 if err != nil { 3340 return nil, nil, fmt.Errorf("failed to get sell funding fees: %v", err) 3341 } 3342 3343 maxBuyFees := &LotFees{ 3344 Swap: maxQuoteFees.Swap, 3345 Redeem: maxBaseFees.Redeem, 3346 Refund: maxQuoteFees.Refund, 3347 } 3348 maxSellFees := &LotFees{ 3349 Swap: maxBaseFees.Swap, 3350 Redeem: maxQuoteFees.Redeem, 3351 Refund: maxBaseFees.Refund, 3352 } 3353 3354 buyBookingFeesPerLot, sellBookingFeesPerLot := u.bookingFees(maxBuyFees, maxSellFees) 3355 3356 u.feesMtx.Lock() 3357 defer u.feesMtx.Unlock() 3358 3359 u.buyFees = &OrderFees{ 3360 LotFeeRange: &LotFeeRange{ 3361 Max: maxBuyFees, 3362 Estimated: &LotFees{ 3363 Swap: estQuoteFees.Swap, 3364 Redeem: estBaseFees.Redeem, 3365 Refund: estQuoteFees.Refund, 3366 }, 3367 }, 3368 Funding: buyFundingFees, 3369 BookingFeesPerLot: buyBookingFeesPerLot, 3370 } 3371 3372 u.sellFees = &OrderFees{ 3373 LotFeeRange: &LotFeeRange{ 3374 Max: maxSellFees, 3375 Estimated: &LotFees{ 3376 Swap: estBaseFees.Swap, 3377 Redeem: estQuoteFees.Redeem, 3378 Refund: estBaseFees.Refund, 3379 }, 3380 }, 3381 Funding: sellFundingFees, 3382 BookingFeesPerLot: sellBookingFeesPerLot, 3383 } 3384 3385 return u.buyFees, u.sellFees, nil 3386 } 3387 3388 func (u *unifiedExchangeAdaptor) Connect(ctx context.Context) (*sync.WaitGroup, error) { 3389 ctx, u.kill = context.WithCancel(ctx) 3390 u.ctx = ctx 3391 3392 fiatRates := u.clientCore.FiatConversionRates() 3393 u.fiatRates.Store(fiatRates) 3394 3395 _, _, err := u.updateFeeRates() 3396 if err != nil { 3397 return nil, fmt.Errorf("failed to getting fee rates: %v", err) 3398 } 3399 3400 startTime := time.Now().Unix() 3401 u.startTime.Store(startTime) 3402 3403 err = u.eventLogDB.storeNewRun(startTime, u.mwh, u.botCfg(), u.balanceState()) 3404 if err != nil { 3405 return nil, fmt.Errorf("failed to store new run in event log db: %v", err) 3406 } 3407 3408 u.wg.Add(1) 3409 go func() { 3410 defer u.wg.Done() 3411 <-ctx.Done() 3412 u.eventLogDB.endRun(startTime, u.mwh) 3413 }() 3414 3415 u.wg.Add(1) 3416 go func() { 3417 defer u.wg.Done() 3418 <-ctx.Done() 3419 u.cancelAllOrders(ctx) 3420 }() 3421 3422 // Listen for core notifications 3423 u.wg.Add(1) 3424 go func() { 3425 defer u.wg.Done() 3426 feed := u.clientCore.NotificationFeed() 3427 defer feed.ReturnFeed() 3428 3429 for { 3430 select { 3431 case <-ctx.Done(): 3432 return 3433 case n := <-feed.C: 3434 u.handleDEXNotification(n) 3435 } 3436 } 3437 }() 3438 3439 u.wg.Add(1) 3440 go func() { 3441 defer u.wg.Done() 3442 refreshTime := time.Minute * 10 3443 for { 3444 select { 3445 case <-time.NewTimer(refreshTime).C: 3446 _, _, err := u.updateFeeRates() 3447 if err != nil { 3448 u.log.Error(err) 3449 refreshTime = time.Minute 3450 } else { 3451 refreshTime = time.Minute * 10 3452 } 3453 case <-ctx.Done(): 3454 return 3455 } 3456 } 3457 }() 3458 3459 if err := u.runBotLoop(ctx); err != nil { 3460 return nil, fmt.Errorf("error starting bot loop: %w", err) 3461 } 3462 3463 u.sendStatsUpdate() 3464 3465 return &u.wg, nil 3466 } 3467 3468 // RunStats is a snapshot of the bot's balances and performance at a point in 3469 // time. 3470 type RunStats struct { 3471 InitialBalances map[uint32]uint64 `json:"initialBalances"` 3472 DEXBalances map[uint32]*BotBalance `json:"dexBalances"` 3473 CEXBalances map[uint32]*BotBalance `json:"cexBalances"` 3474 ProfitLoss *ProfitLoss `json:"profitLoss"` 3475 StartTime int64 `json:"startTime"` 3476 PendingDeposits int `json:"pendingDeposits"` 3477 PendingWithdrawals int `json:"pendingWithdrawals"` 3478 CompletedMatches uint32 `json:"completedMatches"` 3479 TradedUSD float64 `json:"tradedUSD"` 3480 FeeGap *FeeGapStats `json:"feeGap"` 3481 } 3482 3483 // Amount contains the conversions and formatted strings associated with an 3484 // amount of asset and a fiat exchange rate. 3485 type Amount struct { 3486 Atoms int64 `json:"atoms"` 3487 Conventional float64 `json:"conventional"` 3488 Fmt string `json:"fmt"` 3489 USD float64 `json:"usd"` 3490 FmtUSD string `json:"fmtUSD"` 3491 FiatRate float64 `json:"fiatRate"` 3492 } 3493 3494 // NewAmount generates an Amount for a known asset. 3495 func NewAmount(assetID uint32, atoms int64, fiatRate float64) *Amount { 3496 ui, err := asset.UnitInfo(assetID) 3497 if err != nil { 3498 return &Amount{} 3499 } 3500 conv := float64(atoms) / float64(ui.Conventional.ConversionFactor) 3501 usd := conv * fiatRate 3502 return &Amount{ 3503 Atoms: atoms, 3504 Conventional: conv, 3505 USD: usd, 3506 Fmt: ui.FormatSignedAtoms(atoms), 3507 FmtUSD: strconv.FormatFloat(usd, 'f', 2, 64) + " USD", 3508 FiatRate: fiatRate, 3509 } 3510 } 3511 3512 // ProfitLoss is a breakdown of the profit calculations. 3513 type ProfitLoss struct { 3514 Initial map[uint32]*Amount `json:"initial"` 3515 InitialUSD float64 `json:"initialUSD"` 3516 Mods map[uint32]*Amount `json:"mods"` 3517 ModsUSD float64 `json:"modsUSD"` 3518 Final map[uint32]*Amount `json:"final"` 3519 FinalUSD float64 `json:"finalUSD"` 3520 Diffs map[uint32]*Amount `json:"diffs"` 3521 Profit float64 `json:"profit"` 3522 ProfitRatio float64 `json:"profitRatio"` 3523 } 3524 3525 func newProfitLoss( 3526 initialBalances, 3527 finalBalances map[uint32]uint64, 3528 mods map[uint32]int64, 3529 fiatRates map[uint32]float64, 3530 ) *ProfitLoss { 3531 pl := &ProfitLoss{ 3532 Initial: make(map[uint32]*Amount, len(initialBalances)), 3533 Mods: make(map[uint32]*Amount, len(mods)), 3534 Diffs: make(map[uint32]*Amount, len(initialBalances)), 3535 Final: make(map[uint32]*Amount, len(finalBalances)), 3536 } 3537 for assetID, v := range initialBalances { 3538 if v == 0 { 3539 continue 3540 } 3541 fiatRate := fiatRates[assetID] 3542 init := NewAmount(assetID, int64(v), fiatRate) 3543 pl.Initial[assetID] = init 3544 mod := NewAmount(assetID, mods[assetID], fiatRate) 3545 pl.InitialUSD += init.USD 3546 pl.ModsUSD += mod.USD 3547 diff := int64(finalBalances[assetID]) - int64(initialBalances[assetID]) - mods[assetID] 3548 pl.Diffs[assetID] = NewAmount(assetID, diff, fiatRate) 3549 } 3550 for assetID, v := range finalBalances { 3551 if v == 0 { 3552 continue 3553 } 3554 fin := NewAmount(assetID, int64(v), fiatRates[assetID]) 3555 pl.Final[assetID] = fin 3556 pl.FinalUSD += fin.USD 3557 } 3558 3559 basis := pl.InitialUSD + pl.ModsUSD 3560 pl.Profit = pl.FinalUSD - basis 3561 pl.ProfitRatio = pl.Profit / basis 3562 return pl 3563 } 3564 3565 func (u *unifiedExchangeAdaptor) stats() *RunStats { 3566 u.balancesMtx.RLock() 3567 defer u.balancesMtx.RUnlock() 3568 3569 dexBalances := make(map[uint32]*BotBalance) 3570 cexBalances := make(map[uint32]*BotBalance) 3571 totalBalances := make(map[uint32]uint64) 3572 3573 for assetID := range u.baseDexBalances { 3574 bal := u.dexBalance(assetID) 3575 dexBalances[assetID] = bal 3576 totalBalances[assetID] = bal.Available + bal.Locked + bal.Pending + bal.Reserved 3577 } 3578 3579 for assetID := range u.baseCexBalances { 3580 bal := u.cexBalance(assetID) 3581 cexBalances[assetID] = bal 3582 totalBalances[assetID] += bal.Available + bal.Locked + bal.Pending + bal.Reserved 3583 } 3584 3585 fiatRates := u.fiatRates.Load().(map[uint32]float64) 3586 3587 var feeGap *FeeGapStats 3588 if feeGapI := u.runStats.feeGapStats.Load(); feeGapI != nil { 3589 feeGap = feeGapI.(*FeeGapStats) 3590 } 3591 3592 u.runStats.tradedUSD.Lock() 3593 tradedUSD := u.runStats.tradedUSD.v 3594 u.runStats.tradedUSD.Unlock() 3595 3596 // Effects of pendingWithdrawals are applied when the withdrawal is 3597 // complete. 3598 return &RunStats{ 3599 InitialBalances: u.initialBalances, 3600 DEXBalances: dexBalances, 3601 CEXBalances: cexBalances, 3602 ProfitLoss: newProfitLoss(u.initialBalances, totalBalances, u.inventoryMods, fiatRates), 3603 StartTime: u.startTime.Load(), 3604 PendingDeposits: len(u.pendingDeposits), 3605 PendingWithdrawals: len(u.pendingWithdrawals), 3606 CompletedMatches: u.runStats.completedMatches.Load(), 3607 TradedUSD: tradedUSD, 3608 FeeGap: feeGap, 3609 } 3610 } 3611 3612 func (u *unifiedExchangeAdaptor) sendStatsUpdate() { 3613 u.clientCore.Broadcast(newRunStatsNote(u.host, u.baseID, u.quoteID, u.stats())) 3614 } 3615 3616 func (u *unifiedExchangeAdaptor) notifyEvent(e *MarketMakingEvent) { 3617 u.clientCore.Broadcast(newRunEventNote(u.host, u.baseID, u.quoteID, u.startTime.Load(), e)) 3618 } 3619 3620 func (u *unifiedExchangeAdaptor) registerFeeGap(feeGap *FeeGapStats) { 3621 u.runStats.feeGapStats.Store(feeGap) 3622 } 3623 3624 func (u *unifiedExchangeAdaptor) applyInventoryDiffs(balanceDiffs *BotInventoryDiffs) map[uint32]int64 { 3625 u.balancesMtx.Lock() 3626 defer u.balancesMtx.Unlock() 3627 3628 mods := map[uint32]int64{} 3629 3630 for assetID, diff := range balanceDiffs.DEX { 3631 if diff < 0 { 3632 balance := u.dexBalance(assetID) 3633 if balance.Available < uint64(-diff) { 3634 u.log.Errorf("attempting to decrease %s balance by more than available balance. Setting balance to 0.", dex.BipIDSymbol(assetID)) 3635 diff = -int64(balance.Available) 3636 } 3637 } 3638 u.baseDexBalances[assetID] += diff 3639 mods[assetID] = diff 3640 } 3641 3642 for assetID, diff := range balanceDiffs.CEX { 3643 if diff < 0 { 3644 balance := u.cexBalance(assetID) 3645 if balance.Available < uint64(-diff) { 3646 u.log.Errorf("attempting to decrease %s balance by more than available balance. Setting balance to 0.", dex.BipIDSymbol(assetID)) 3647 diff = -int64(balance.Available) 3648 } 3649 } 3650 u.baseCexBalances[assetID] += diff 3651 mods[assetID] += diff 3652 } 3653 3654 for assetID, diff := range mods { 3655 u.inventoryMods[assetID] += diff 3656 } 3657 3658 u.logBalanceAdjustments(balanceDiffs.DEX, balanceDiffs.CEX, "Inventory updated") 3659 u.log.Debugf("Aggregate inventory mods: %+v", u.inventoryMods) 3660 3661 return mods 3662 } 3663 3664 func (u *unifiedExchangeAdaptor) updateConfig(cfg *BotConfig) error { 3665 if err := validateConfigUpdate(u.botCfg(), cfg); err != nil { 3666 return err 3667 } 3668 3669 u.botCfgV.Store(cfg) 3670 u.updateConfigEvent(cfg) 3671 return nil 3672 } 3673 3674 func (u *unifiedExchangeAdaptor) updateInventory(balanceDiffs *BotInventoryDiffs) { 3675 u.updateInventoryEvent(u.applyInventoryDiffs(balanceDiffs)) 3676 } 3677 3678 func (u *unifiedExchangeAdaptor) Book() (buys, sells []*core.MiniOrder, _ error) { 3679 if u.CEX == nil { 3680 return nil, nil, errors.New("not a cex-connected bot") 3681 } 3682 return u.CEX.Book(u.baseID, u.quoteID) 3683 } 3684 3685 func (u *unifiedExchangeAdaptor) latestCEXProblems() *CEXProblems { 3686 u.cexProblemsMtx.RLock() 3687 defer u.cexProblemsMtx.RUnlock() 3688 if u.cexProblems == nil { 3689 return nil 3690 } 3691 return u.cexProblems.copy() 3692 } 3693 3694 func (u *unifiedExchangeAdaptor) latestEpoch() *EpochReport { 3695 reportI := u.epochReport.Load() 3696 if reportI == nil { 3697 return nil 3698 } 3699 return reportI.(*EpochReport) 3700 } 3701 3702 func (u *unifiedExchangeAdaptor) updateEpochReport(report *EpochReport) { 3703 u.epochReport.Store(report) 3704 u.clientCore.Broadcast(newEpochReportNote(u.host, u.baseID, u.quoteID, report)) 3705 } 3706 3707 // tradingLimitNotReached returns true if the user has not reached their trading 3708 // limit. If it has, it updates the epoch report with the problems. 3709 func (u *unifiedExchangeAdaptor) tradingLimitNotReached(epochNum uint64) bool { 3710 var tradingLimitReached bool 3711 var err error 3712 defer func() { 3713 if err == nil && !tradingLimitReached { 3714 return 3715 } 3716 var unknownErr string 3717 if err != nil { 3718 unknownErr = err.Error() 3719 } 3720 u.updateEpochReport(&EpochReport{ 3721 PreOrderProblems: &BotProblems{ 3722 UserLimitTooLow: tradingLimitReached, 3723 UnknownError: unknownErr, 3724 }, 3725 EpochNum: epochNum, 3726 }) 3727 }() 3728 3729 userParcels, parcelLimit, err := u.clientCore.TradingLimits(u.host) 3730 if err != nil { 3731 return false 3732 } 3733 3734 tradingLimitReached = userParcels >= parcelLimit 3735 return !tradingLimitReached 3736 } 3737 3738 type cexProblemType uint16 3739 3740 const ( 3741 cexTradeProblem cexProblemType = iota 3742 cexDepositProblem 3743 cexWithdrawProblem 3744 ) 3745 3746 // updateCEXProblemState updates the state of a cex problem. It returns true 3747 // if the problem state was updated. It is always updated if the error is 3748 // non-nil. 3749 func (u *unifiedExchangeAdaptor) updateCEXProblemState(typ cexProblemType, assetID uint32, err error) bool { 3750 if err != nil { 3751 switch typ { 3752 case cexTradeProblem: 3753 u.cexProblems.TradeErr = newStampedError(err) 3754 case cexDepositProblem: 3755 u.cexProblems.DepositErr[assetID] = newStampedError(err) 3756 case cexWithdrawProblem: 3757 u.cexProblems.WithdrawErr[assetID] = newStampedError(err) 3758 } 3759 return true 3760 } 3761 3762 var updated bool 3763 switch typ { 3764 case cexTradeProblem: 3765 updated = u.cexProblems.TradeErr != nil 3766 u.cexProblems.TradeErr = nil 3767 case cexDepositProblem: 3768 updated = u.cexProblems.DepositErr[assetID] != nil 3769 delete(u.cexProblems.DepositErr, assetID) 3770 case cexWithdrawProblem: 3771 updated = u.cexProblems.WithdrawErr[assetID] != nil 3772 delete(u.cexProblems.WithdrawErr, assetID) 3773 } 3774 return updated 3775 } 3776 3777 func (u *unifiedExchangeAdaptor) updateCEXProblems(typ cexProblemType, assetID uint32, err error) { 3778 u.cexProblemsMtx.Lock() 3779 defer u.cexProblemsMtx.Unlock() 3780 3781 if u.updateCEXProblemState(typ, assetID, err) { 3782 u.clientCore.Broadcast(newCexProblemsNote(u.host, u.baseID, u.quoteID, u.cexProblems)) 3783 } 3784 } 3785 3786 // checkBotHealth returns true if the bot is healthy and can continue trading. 3787 // If it is not healthy, it updates the epoch report with the problems. 3788 func (u *unifiedExchangeAdaptor) checkBotHealth(epochNum uint64) (healthy bool) { 3789 var err error 3790 var baseAssetNotSynced, baseAssetNoPeers, quoteAssetNotSynced, quoteAssetNoPeers, accountSuspended bool 3791 3792 defer func() { 3793 if healthy { 3794 return 3795 } 3796 var unknownErr string 3797 if err != nil { 3798 unknownErr = err.Error() 3799 } 3800 problems := &BotProblems{ 3801 NoWalletPeers: map[uint32]bool{ 3802 u.baseID: baseAssetNoPeers, 3803 u.quoteID: quoteAssetNoPeers, 3804 }, 3805 WalletNotSynced: map[uint32]bool{ 3806 u.baseID: baseAssetNotSynced, 3807 u.quoteID: quoteAssetNotSynced, 3808 }, 3809 AccountSuspended: accountSuspended, 3810 UnknownError: unknownErr, 3811 } 3812 u.updateEpochReport(&EpochReport{ 3813 PreOrderProblems: problems, 3814 EpochNum: epochNum, 3815 }) 3816 }() 3817 3818 baseWallet := u.clientCore.WalletState(u.baseID) 3819 if baseWallet == nil { 3820 err = fmt.Errorf("base asset %d wallet not found", u.baseID) 3821 return false 3822 } 3823 3824 baseAssetNotSynced = !baseWallet.Synced 3825 baseAssetNoPeers = baseWallet.PeerCount == 0 3826 3827 quoteWallet := u.clientCore.WalletState(u.quoteID) 3828 if quoteWallet == nil { 3829 err = fmt.Errorf("quote asset %d wallet not found", u.quoteID) 3830 return false 3831 } 3832 3833 quoteAssetNotSynced = !quoteWallet.Synced 3834 quoteAssetNoPeers = quoteWallet.PeerCount == 0 3835 3836 exchange, err := u.clientCore.Exchange(u.host) 3837 if err != nil { 3838 err = fmt.Errorf("error getting exchange: %w", err) 3839 return false 3840 } 3841 accountSuspended = exchange.Auth.EffectiveTier <= 0 3842 3843 return !(baseAssetNotSynced || baseAssetNoPeers || quoteAssetNotSynced || quoteAssetNoPeers || accountSuspended) 3844 } 3845 3846 type exchangeAdaptorCfg struct { 3847 botID string 3848 mwh *MarketWithHost 3849 baseDexBalances map[uint32]uint64 3850 baseCexBalances map[uint32]uint64 3851 autoRebalanceConfig *AutoRebalanceConfig 3852 core clientCore 3853 cex libxc.CEX 3854 log dex.Logger 3855 eventLogDB eventLogDB 3856 botCfg *BotConfig 3857 } 3858 3859 // newUnifiedExchangeAdaptor is the constructor for a unifiedExchangeAdaptor. 3860 func newUnifiedExchangeAdaptor(cfg *exchangeAdaptorCfg) (*unifiedExchangeAdaptor, error) { 3861 initialBalances := make(map[uint32]uint64, len(cfg.baseDexBalances)) 3862 for assetID, balance := range cfg.baseDexBalances { 3863 initialBalances[assetID] = balance 3864 } 3865 for assetID, balance := range cfg.baseCexBalances { 3866 initialBalances[assetID] += balance 3867 } 3868 3869 baseDEXBalances := make(map[uint32]int64, len(cfg.baseDexBalances)) 3870 for assetID, balance := range cfg.baseDexBalances { 3871 baseDEXBalances[assetID] = int64(balance) 3872 } 3873 baseCEXBalances := make(map[uint32]int64, len(cfg.baseCexBalances)) 3874 for assetID, balance := range cfg.baseCexBalances { 3875 baseCEXBalances[assetID] = int64(balance) 3876 } 3877 3878 coreMkt, err := cfg.core.ExchangeMarket(cfg.mwh.Host, cfg.mwh.BaseID, cfg.mwh.QuoteID) 3879 if err != nil { 3880 return nil, err 3881 } 3882 3883 mkt, err := parseMarket(cfg.mwh.Host, coreMkt) 3884 if err != nil { 3885 return nil, err 3886 } 3887 3888 baseTraits, err := cfg.core.WalletTraits(mkt.baseID) 3889 if err != nil { 3890 return nil, fmt.Errorf("wallet trait error for base asset %d", mkt.baseID) 3891 } 3892 quoteTraits, err := cfg.core.WalletTraits(mkt.quoteID) 3893 if err != nil { 3894 return nil, fmt.Errorf("wallet trait error for quote asset %d", mkt.quoteID) 3895 } 3896 3897 adaptor := &unifiedExchangeAdaptor{ 3898 market: mkt, 3899 clientCore: cfg.core, 3900 CEX: cfg.cex, 3901 botID: cfg.botID, 3902 log: cfg.log, 3903 eventLogDB: cfg.eventLogDB, 3904 initialBalances: initialBalances, 3905 baseTraits: baseTraits, 3906 quoteTraits: quoteTraits, 3907 autoRebalanceCfg: cfg.autoRebalanceConfig, 3908 3909 baseDexBalances: baseDEXBalances, 3910 baseCexBalances: baseCEXBalances, 3911 pendingDEXOrders: make(map[order.OrderID]*pendingDEXOrder), 3912 pendingCEXOrders: make(map[string]*pendingCEXOrder), 3913 pendingDeposits: make(map[string]*pendingDeposit), 3914 pendingWithdrawals: make(map[string]*pendingWithdrawal), 3915 mwh: cfg.mwh, 3916 inventoryMods: make(map[uint32]int64), 3917 cexProblems: newCEXProblems(), 3918 } 3919 3920 adaptor.fiatRates.Store(map[uint32]float64{}) 3921 adaptor.botCfgV.Store(cfg.botCfg) 3922 3923 return adaptor, nil 3924 }