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