decred.org/dcrdex@v1.0.5/client/webserver/site/src/js/mmutil.ts (about)

     1  import {
     2    app,
     3    PageElement,
     4    BotConfig,
     5    MMBotStatus,
     6    CEXConfig,
     7    MarketMakingStatus,
     8    ExchangeBalance,
     9    RunStats,
    10    StartConfig,
    11    MarketWithHost,
    12    RunningBotInventory,
    13    Spot,
    14    OrderPlacement,
    15    Token,
    16    UnitInfo,
    17    MarketReport,
    18    BotBalanceAllocation,
    19    ProjectedAlloc,
    20    BalanceNote,
    21    BotBalance,
    22    Order,
    23    LotFeeRange,
    24    BookingFees,
    25    BotProblems,
    26    EpochReportNote,
    27    OrderReport,
    28    EpochReport,
    29    TradePlacement,
    30    SupportedAsset,
    31    CEXProblemsNote,
    32    CEXProblems
    33  } from './registry'
    34  import { getJSON, postJSON } from './http'
    35  import Doc, { clamp } from './doc'
    36  import * as OrderUtil from './orderutil'
    37  import { Chart, Region, Extents, Translator } from './charts'
    38  import * as intl from './locales'
    39  import { Forms } from './forms'
    40  
    41  export const GapStrategyMultiplier = 'multiplier'
    42  export const GapStrategyAbsolute = 'absolute'
    43  export const GapStrategyAbsolutePlus = 'absolute-plus'
    44  export const GapStrategyPercent = 'percent'
    45  export const GapStrategyPercentPlus = 'percent-plus'
    46  
    47  export const botTypeBasicMM = 'basicMM'
    48  export const botTypeArbMM = 'arbMM'
    49  export const botTypeBasicArb = 'basicArb'
    50  
    51  export interface CEXDisplayInfo {
    52    name: string
    53    logo: string
    54  }
    55  
    56  export const CEXDisplayInfos: Record<string, CEXDisplayInfo> = {
    57    'Binance': {
    58      name: 'Binance',
    59      logo: '/img/binance.com.png'
    60    },
    61    'BinanceUS': {
    62      name: 'Binance U.S.',
    63      logo: '/img/binance.us.png'
    64    }
    65  }
    66  
    67  /*
    68   * MarketMakerBot is the front end representation of the server's
    69   * mm.MarketMaker. MarketMakerBot is a singleton assigned to MM below.
    70   */
    71  class MarketMakerBot {
    72    cexBalanceCache: Record<string, Record<number, ExchangeBalance>> = {}
    73  
    74    /*
    75     * updateBotConfig appends or updates the specified BotConfig.
    76     */
    77    async updateBotConfig (cfg: BotConfig) {
    78      return postJSON('/api/updatebotconfig', cfg)
    79    }
    80  
    81    /*
    82     * updateCEXConfig appends or updates the specified CEXConfig.
    83     */
    84    async updateCEXConfig (cfg: CEXConfig) {
    85      return postJSON('/api/updatecexconfig', cfg)
    86    }
    87  
    88    async removeBotConfig (host: string, baseID: number, quoteID: number) {
    89      return postJSON('/api/removebotconfig', { host, baseID, quoteID })
    90    }
    91  
    92    async report (host: string, baseID: number, quoteID: number) {
    93      return postJSON('/api/marketreport', { host, baseID, quoteID })
    94    }
    95  
    96    async startBot (config: StartConfig) {
    97      return await postJSON('/api/startmarketmakingbot', { config })
    98    }
    99  
   100    async stopBot (market: MarketWithHost) : Promise<void> {
   101      await postJSON('/api/stopmarketmakingbot', { market })
   102    }
   103  
   104    async status () : Promise<MarketMakingStatus> {
   105      return (await getJSON('/api/marketmakingstatus')).status
   106    }
   107  
   108    // botStats returns the RunStats for a running bot with the specified parameters.
   109    botStats (baseID: number, quoteID: number, host: string, startTime: number): RunStats | undefined {
   110      for (const botStatus of Object.values(app().mmStatus.bots)) {
   111        if (!botStatus.runStats) continue
   112        const runStats = botStatus.runStats
   113        const cfg = botStatus.config
   114        if (cfg.baseID === baseID && cfg.quoteID === quoteID && cfg.host === host && runStats.startTime === startTime) {
   115          return runStats
   116        }
   117      }
   118    }
   119  
   120    cachedCexBalance (cexName: string, assetID: number): ExchangeBalance | undefined {
   121      return this.cexBalanceCache[cexName]?.[assetID]
   122    }
   123  
   124    async cexBalance (cexName: string, assetID: number): Promise<ExchangeBalance> {
   125      if (!this.cexBalanceCache[cexName]) this.cexBalanceCache[cexName] = {}
   126      const cexBalance = (await postJSON('/api/cexbalance', { cexName, assetID })).cexBalance
   127      this.cexBalanceCache[cexName][assetID] = cexBalance
   128      return cexBalance
   129    }
   130  }
   131  
   132  // MM is the front end representation of the server's mm.MarketMaker.
   133  export const MM = new MarketMakerBot()
   134  
   135  export function runningBotInventory (assetID: number): RunningBotInventory {
   136    return app().mmStatus.bots.reduce((v, { runStats, running }) => {
   137      if (!running || !runStats) return v
   138      const { dexBalances: d, cexBalances: c } = runStats
   139      v.cex.locked += c[assetID]?.locked ?? 0
   140      v.cex.locked += c[assetID]?.reserved ?? 0
   141      v.cex.avail += c[assetID]?.available ?? 0
   142      v.cex.total = v.cex.avail + v.cex.locked
   143      v.dex.locked += d[assetID]?.locked ?? 0
   144      v.dex.locked += d[assetID]?.reserved ?? 0
   145      v.dex.avail += d[assetID]?.available ?? 0
   146      v.dex.total = v.dex.avail + v.dex.locked
   147      v.avail += (d[assetID]?.available ?? 0) + (c[assetID]?.available ?? 0)
   148      v.locked += (d[assetID]?.locked ?? 0) + (c[assetID]?.locked ?? 0)
   149      return v
   150    }, { avail: 0, locked: 0, cex: { avail: 0, locked: 0, total: 0 }, dex: { avail: 0, locked: 0, total: 0 } })
   151  }
   152  
   153  export function setMarketElements (ancestor: PageElement, baseID: number, quoteID: number, host: string) {
   154    Doc.setText(ancestor, '[data-host]', host)
   155    const { unitInfo: bui, name: baseName, symbol: baseSymbol, token: baseToken } = app().assets[baseID]
   156    Doc.setText(ancestor, '[data-base-name]', baseName)
   157    Doc.setSrc(ancestor, '[data-base-logo]', Doc.logoPath(baseSymbol))
   158    Doc.setText(ancestor, '[data-base-ticker]', bui.conventional.unit)
   159    const { unitInfo: baseFeeUI, name: baseFeeName, symbol: baseFeeSymbol } = app().assets[baseToken ? baseToken.parentID : baseID]
   160    Doc.setText(ancestor, '[data-base-fee-name]', baseFeeName)
   161    Doc.setSrc(ancestor, '[data-base-fee-logo]', Doc.logoPath(baseFeeSymbol))
   162    Doc.setText(ancestor, '[data-base-fee-ticker]', baseFeeUI.conventional.unit)
   163    const { unitInfo: qui, name: quoteName, symbol: quoteSymbol, token: quoteToken } = app().assets[quoteID]
   164    Doc.setText(ancestor, '[data-quote-name]', quoteName)
   165    Doc.setSrc(ancestor, '[data-quote-logo]', Doc.logoPath(quoteSymbol))
   166    Doc.setText(ancestor, '[data-quote-ticker]', qui.conventional.unit)
   167    const { unitInfo: quoteFeeUI, name: quoteFeeName, symbol: quoteFeeSymbol } = app().assets[quoteToken ? quoteToken.parentID : quoteID]
   168    Doc.setText(ancestor, '[data-quote-fee-name]', quoteFeeName)
   169    Doc.setSrc(ancestor, '[data-quote-fee-logo]', Doc.logoPath(quoteFeeSymbol))
   170    Doc.setText(ancestor, '[data-quote-fee-ticker]', quoteFeeUI.conventional.unit)
   171  }
   172  
   173  export function setCexElements (ancestor: PageElement, cexName: string) {
   174    const dinfo = CEXDisplayInfos[cexName]
   175    Doc.setText(ancestor, '[data-cex-name]', dinfo.name)
   176    Doc.setSrc(ancestor, '[data-cex-logo]', dinfo.logo)
   177    for (const img of Doc.applySelector(ancestor, '[data-cex-logo]')) Doc.show(img)
   178  }
   179  
   180  export function calculateQuoteLot (lotSize: number, baseID: number, quoteID: number, spot?: Spot) {
   181    const baseRate = app().fiatRatesMap[baseID]
   182    const quoteRate = app().fiatRatesMap[quoteID]
   183    const { unitInfo: { conventional: { conversionFactor: bFactor } } } = app().assets[baseID]
   184    const { unitInfo: { conventional: { conversionFactor: qFactor } } } = app().assets[quoteID]
   185    if (baseRate && quoteRate) {
   186      return lotSize * baseRate / quoteRate * qFactor / bFactor
   187    } else if (spot) {
   188      return lotSize * spot.rate / OrderUtil.RateEncodingFactor
   189    }
   190    return qFactor
   191  }
   192  
   193  export interface PlacementChartConfig {
   194    cexName: string
   195    botType: string
   196    baseFiatRate: number
   197    dict: {
   198      profit: number
   199      buyPlacements: OrderPlacement[]
   200      sellPlacements: OrderPlacement[]
   201    }
   202  }
   203  
   204  export class PlacementsChart extends Chart {
   205    cfg: PlacementChartConfig
   206    loadedCEX: string
   207    cexLogo: HTMLImageElement
   208  
   209    constructor (parent: PageElement) {
   210      super(parent, {
   211        resize: () => this.resized(),
   212        click: (/* e: MouseEvent */) => { /* pass */ },
   213        zoom: (/* bigger: boolean */) => { /* pass */ }
   214      })
   215    }
   216  
   217    resized () {
   218      this.render()
   219    }
   220  
   221    draw () { /* pass */ }
   222  
   223    setMarket (cfg: PlacementChartConfig) {
   224      this.cfg = cfg
   225      const { loadedCEX, cfg: { cexName } } = this
   226      if (cexName && cexName !== loadedCEX) {
   227        this.loadedCEX = cexName
   228        this.cexLogo = new Image()
   229        Doc.bind(this.cexLogo, 'load', () => { this.render() })
   230        this.cexLogo.src = CEXDisplayInfos[cexName || ''].logo
   231      }
   232      this.render()
   233    }
   234  
   235    render () {
   236      const { ctx, canvas, theme, cfg } = this
   237      if (canvas.width === 0 || !cfg) return
   238      const { dict: { buyPlacements, sellPlacements, profit }, baseFiatRate, botType } = cfg
   239      if (botType === botTypeBasicArb) return
   240  
   241      this.clear()
   242  
   243      const drawDashedLine = (x0: number, y0: number, x1: number, y1: number, color: string) => {
   244        ctx.save()
   245        ctx.setLineDash([3, 5])
   246        ctx.lineWidth = 1.5
   247        ctx.strokeStyle = color
   248        this.line(x0, y0, x1, y1)
   249        ctx.restore()
   250      }
   251  
   252      const isBasicMM = botType === botTypeBasicMM
   253      const cx = canvas.width / 2
   254      const [cexGapL, cexGapR] = isBasicMM ? [cx, cx] : [0.48 * canvas.width, 0.52 * canvas.width]
   255  
   256      const buyLots = buyPlacements.reduce((v: number, p: OrderPlacement) => v + p.lots, 0)
   257      const sellLots = sellPlacements.reduce((v: number, p: OrderPlacement) => v + p.lots, 0)
   258      const maxLots = Math.max(buyLots, sellLots)
   259  
   260      let widest = 0
   261      let fauxSpacer = 0
   262      if (isBasicMM) {
   263        const leftmost = buyPlacements.reduce((v: number, p: OrderPlacement) => Math.max(v, p.gapFactor), 0)
   264        const rightmost = sellPlacements.reduce((v: number, p: OrderPlacement) => Math.max(v, p.gapFactor), 0)
   265        widest = Math.max(leftmost, rightmost)
   266      } else {
   267        // For arb-mm, we don't know how the orders will be spaced because it
   268        // depends on the vwap. But we're just trying to capture the general sense
   269        // of how the parameters will affect order placement, so we'll fake it.
   270        // Higher match buffer values will lead to orders with less favorable
   271        // rates, e.g. the spacing will be larger.
   272        const ps = [...buyPlacements, ...sellPlacements]
   273        const matchBuffer = ps.reduce((sum: number, p: OrderPlacement) => sum + p.gapFactor, 0) / ps.length
   274        fauxSpacer = 0.01 * (1 + matchBuffer)
   275        widest = Math.min(10, Math.max(buyPlacements.length, sellPlacements.length)) * fauxSpacer // arb-mm
   276      }
   277  
   278      // Make the range 15% on each side, which will include profit + placements,
   279      // unless they have orders with larger gap factors.
   280      const minRange = profit + widest
   281      const defaultRange = 0.155
   282      const range = Math.max(minRange * 1.05, defaultRange)
   283  
   284      // Increase data height logarithmically up to 1,000,000 USD.
   285      const maxCommitUSD = maxLots * baseFiatRate
   286      const regionHeight = 0.2 + 0.7 * Math.log(clamp(maxCommitUSD, 0, 1e6)) / Math.log(1e6)
   287  
   288      // Draw a region in the middle representing the cex gap.
   289      const plotRegion = new Region(ctx, new Extents(0, canvas.width, 0, canvas.height))
   290  
   291      if (isBasicMM) {
   292        drawDashedLine(cx, 0, cx, canvas.height, theme.gapLine)
   293      } else { // arb-mm
   294        plotRegion.plot(new Extents(0, 1, 0, 1), (ctx: CanvasRenderingContext2D, tools: Translator) => {
   295          const [y0, y1] = [tools.y(0), tools.y(1)]
   296          drawDashedLine(cexGapL, y0, cexGapL, y1, theme.gapLine)
   297          drawDashedLine(cexGapR, y0, cexGapR, y1, theme.gapLine)
   298          const y = tools.y(0.95)
   299          ctx.drawImage(this.cexLogo, cx - 8, y, 16, 16)
   300          this.applyLabelStyle(18)
   301          ctx.fillText('δ', cx, y + 29)
   302        })
   303      }
   304  
   305      const plotSide = (isBuy: boolean, placements: OrderPlacement[]) => {
   306        if (!placements?.length) return
   307        const [xMin, xMax] = isBuy ? [0, cexGapL] : [cexGapR, canvas.width]
   308        const reg = new Region(ctx, new Extents(xMin, xMax, canvas.height * (1 - regionHeight), canvas.height))
   309        const [l, r] = isBuy ? [-range, 0] : [0, range]
   310        reg.plot(new Extents(l, r, 0, maxLots), (ctx: CanvasRenderingContext2D, tools: Translator) => {
   311          ctx.lineWidth = 2.5
   312          ctx.strokeStyle = isBuy ? theme.buyLine : theme.sellLine
   313          ctx.fillStyle = isBuy ? theme.buyFill : theme.sellFill
   314          ctx.beginPath()
   315          const sideFactor = isBuy ? -1 : 1
   316          const firstPt = placements[0]
   317          const y0 = tools.y(0)
   318          const firstX = tools.x((isBasicMM ? firstPt.gapFactor : profit + fauxSpacer) * sideFactor)
   319          ctx.moveTo(firstX, y0)
   320          let cumulativeLots = 0
   321          for (let i = 0; i < placements.length; i++) {
   322            const p = placements[i]
   323            // For arb-mm, we don't know exactly
   324            const rawX = isBasicMM ? p.gapFactor : profit + (i + 1) * fauxSpacer
   325            const x = tools.x(rawX * sideFactor)
   326            ctx.lineTo(x, tools.y(cumulativeLots))
   327            cumulativeLots += p.lots
   328            ctx.lineTo(x, tools.y(cumulativeLots))
   329          }
   330          const xInfinity = isBuy ? canvas.width * -0.1 : canvas.width * 1.1
   331          ctx.lineTo(xInfinity, tools.y(cumulativeLots))
   332          ctx.stroke()
   333          ctx.lineTo(xInfinity, y0)
   334          ctx.lineTo(firstX, y0)
   335          ctx.closePath()
   336          ctx.globalAlpha = 0.25
   337          ctx.fill()
   338        }, true)
   339      }
   340  
   341      plotSide(false, sellPlacements)
   342      plotSide(true, buyPlacements)
   343    }
   344  }
   345  
   346  export function hostedMarketID (host: string, baseID: number, quoteID: number) {
   347    return `${host}-${baseID}-${quoteID}` // same as MarketWithHost.String()
   348  }
   349  
   350  export function liveBotConfig (host: string, baseID: number, quoteID: number): BotConfig | undefined {
   351    const s = liveBotStatus(host, baseID, quoteID)
   352    if (s) return s.config
   353  }
   354  
   355  export function liveBotStatus (host: string, baseID: number, quoteID: number): MMBotStatus | undefined {
   356    const statuses = (app().mmStatus.bots || []).filter((s: MMBotStatus) => {
   357      return s.config.baseID === baseID && s.config.quoteID === quoteID && s.config.host === host
   358    })
   359    if (statuses.length) return statuses[0]
   360  }
   361  
   362  interface Lotter {
   363    lots: number
   364  }
   365  
   366  function sumLots (lots: number, p: Lotter) {
   367    return lots + p.lots
   368  }
   369  
   370  interface AllocationProjection {
   371    bProj: ProjectedAlloc
   372    qProj: ProjectedAlloc
   373    alloc: Record<number, number>
   374  }
   375  
   376  function emptyProjection (): ProjectedAlloc {
   377    return { book: 0, bookingFees: 0, swapFeeReserves: 0, cex: 0, orderReserves: 0, slippageBuffer: 0 }
   378  }
   379  
   380  export class BotMarket {
   381    cfg: BotConfig
   382    host: string
   383    baseID: number
   384    baseSymbol: string
   385    baseTicker: string
   386    baseFeeID: number
   387    baseIsAccountLocker: boolean
   388    baseFeeSymbol: string
   389    baseFeeTicker: string
   390    baseToken?: Token
   391    quoteID: number
   392    quoteSymbol: string
   393    quoteTicker: string
   394    quoteFeeID: number
   395    quoteIsAccountLocker: boolean
   396    quoteFeeSymbol: string
   397    quoteFeeTicker: string
   398    quoteToken?: Token
   399    botType: string
   400    cexName: string
   401    dinfo: CEXDisplayInfo
   402    alloc: BotBalanceAllocation
   403    proj: AllocationProjection
   404    bui: UnitInfo
   405    baseFactor: number
   406    baseFeeUI: UnitInfo
   407    baseFeeFactor: number
   408    qui: UnitInfo
   409    quoteFactor: number
   410    quoteFeeUI: UnitInfo
   411    quoteFeeFactor: number
   412    id: string // includes host
   413    mktID: string
   414    lotSize: number
   415    lotSizeConv: number
   416    lotSizeUSD: number
   417    quoteLot: number
   418    quoteLotConv: number
   419    quoteLotUSD: number
   420    rateStep: number
   421    baseFeeFiatRate: number
   422    quoteFeeFiatRate: number
   423    baseLots: number
   424    quoteLots: number
   425    marketReport: MarketReport
   426    nBuyPlacements: number
   427    nSellPlacements: number
   428  
   429    constructor (cfg: BotConfig) {
   430      const host = this.host = cfg.host
   431      const baseID = this.baseID = cfg.baseID
   432      const quoteID = this.quoteID = cfg.quoteID
   433      this.cexName = cfg.cexName
   434      const status = app().mmStatus.bots.find(({ config: c }: MMBotStatus) => c.baseID === baseID && c.quoteID === quoteID && c.host === host)
   435      if (!status) throw Error('where\'s the bot status?')
   436      this.cfg = status.config
   437  
   438      const { token: baseToken, symbol: baseSymbol, unitInfo: bui } = app().assets[baseID]
   439      this.baseSymbol = baseSymbol
   440      this.baseTicker = bui.conventional.unit
   441      this.bui = bui
   442      this.baseFactor = bui.conventional.conversionFactor
   443      this.baseToken = baseToken
   444      const baseFeeID = this.baseFeeID = baseToken ? baseToken.parentID : baseID
   445      const { unitInfo: baseFeeUI, symbol: baseFeeSymbol, wallet: baseWallet } = app().assets[this.baseFeeID]
   446      const traitAccountLocker = 1 << 14
   447      this.baseIsAccountLocker = (baseWallet.traits & traitAccountLocker) > 0
   448      this.baseFeeUI = baseFeeUI
   449      this.baseFeeTicker = baseFeeUI.conventional.unit
   450      this.baseFeeSymbol = baseFeeSymbol
   451      this.baseFeeFactor = this.baseFeeUI.conventional.conversionFactor
   452  
   453      const { token: quoteToken, symbol: quoteSymbol, unitInfo: qui } = app().assets[quoteID]
   454      this.quoteSymbol = quoteSymbol
   455      this.quoteTicker = qui.conventional.unit
   456      this.qui = qui
   457      this.quoteFactor = qui.conventional.conversionFactor
   458      this.quoteToken = quoteToken
   459      const quoteFeeID = this.quoteFeeID = quoteToken ? quoteToken.parentID : quoteID
   460      const { unitInfo: quoteFeeUI, symbol: quoteFeeSymbol, wallet: quoteWallet } = app().assets[this.quoteFeeID]
   461      this.quoteIsAccountLocker = (quoteWallet.traits & traitAccountLocker) > 0
   462      this.quoteFeeUI = quoteFeeUI
   463      this.quoteFeeTicker = quoteFeeUI.conventional.unit
   464      this.quoteFeeSymbol = quoteFeeSymbol
   465      this.quoteFeeFactor = this.quoteFeeUI.conventional.conversionFactor
   466  
   467      this.id = hostedMarketID(host, baseID, quoteID)
   468      this.mktID = `${baseSymbol}_${quoteSymbol}`
   469  
   470      const { markets } = app().exchanges[host]
   471      const { lotsize: lotSize, ratestep: rateStep } = markets[this.mktID]
   472      this.lotSize = lotSize
   473      this.lotSizeConv = lotSize / bui.conventional.conversionFactor
   474      this.rateStep = rateStep
   475      this.quoteLot = calculateQuoteLot(lotSize, baseID, quoteID)
   476      this.quoteLotConv = this.quoteLot / qui.conventional.conversionFactor
   477  
   478      this.baseFeeFiatRate = app().fiatRatesMap[baseFeeID]
   479      this.quoteFeeFiatRate = app().fiatRatesMap[quoteFeeID]
   480  
   481      if (cfg.arbMarketMakingConfig) {
   482        this.botType = botTypeArbMM
   483        this.baseLots = cfg.arbMarketMakingConfig.sellPlacements.reduce(sumLots, 0)
   484        this.quoteLots = cfg.arbMarketMakingConfig.buyPlacements.reduce(sumLots, 0)
   485        this.nBuyPlacements = cfg.arbMarketMakingConfig.buyPlacements.length
   486        this.nSellPlacements = cfg.arbMarketMakingConfig.sellPlacements.length
   487      } else if (cfg.simpleArbConfig) {
   488        this.botType = botTypeBasicArb
   489        this.baseLots = cfg.uiConfig.simpleArbLots as number
   490        this.quoteLots = cfg.uiConfig.simpleArbLots as number
   491      } else if (cfg.basicMarketMakingConfig) { // basicmm
   492        this.botType = botTypeBasicMM
   493        this.baseLots = cfg.basicMarketMakingConfig.sellPlacements.reduce(sumLots, 0)
   494        this.quoteLots = cfg.basicMarketMakingConfig.buyPlacements.reduce(sumLots, 0)
   495        this.nBuyPlacements = cfg.basicMarketMakingConfig.buyPlacements.length
   496        this.nSellPlacements = cfg.basicMarketMakingConfig.sellPlacements.length
   497      }
   498    }
   499  
   500    async initialize () {
   501      const { host, baseID, quoteID, lotSizeConv, quoteLotConv } = this
   502      const res = await MM.report(host, baseID, quoteID)
   503      const r = this.marketReport = res.report as MarketReport
   504      this.lotSizeUSD = lotSizeConv * r.baseFiatRate
   505      this.quoteLotUSD = quoteLotConv * r.quoteFiatRate
   506      this.proj = this.projectedAllocations()
   507    }
   508  
   509    status () {
   510      const { baseID, quoteID } = this
   511      const botStatus = app().mmStatus.bots.find((s: MMBotStatus) => s.config.baseID === baseID && s.config.quoteID === quoteID)
   512      if (!botStatus) return { botCfg: {} as BotConfig, running: false, runStats: {} as RunStats }
   513      const { config: botCfg, running, runStats, latestEpoch, cexProblems } = botStatus
   514      return { botCfg, running, runStats, latestEpoch, cexProblems }
   515    }
   516  
   517    /*
   518    * adjustedBalances calculates the user's available balances and fee-asset
   519    * balances for a market, with consideration for currently running bots.
   520    */
   521    adjustedBalances () {
   522      const {
   523        baseID, quoteID, baseFeeID, quoteFeeID, cexName,
   524        baseFactor, quoteFactor, baseFeeFactor, quoteFeeFactor
   525      } = this
   526      const [baseWallet, quoteWallet] = [app().walletMap[baseID], app().walletMap[quoteID]]
   527      const [bInv, qInv] = [runningBotInventory(baseID), runningBotInventory(quoteID)]
   528  
   529      // In these available balance calcs, only subtract the available balance of
   530      // running bots, since the locked/reserved/immature is already subtracted
   531      // from the wallet's total available balance.
   532      let cexBaseAvail = 0
   533      let cexQuoteAvail = 0
   534      let cexBaseBalance: ExchangeBalance | undefined
   535      let cexQuoteBalance: ExchangeBalance | undefined
   536      if (cexName) {
   537        const cex = app().mmStatus.cexes[cexName]
   538        if (!cex) throw Error('where\'s the cex status?')
   539        cexBaseBalance = cex.balances[baseID]
   540        cexQuoteBalance = cex.balances[quoteID]
   541      }
   542      if (cexBaseBalance) cexBaseAvail = (cexBaseBalance.available || 0) - bInv.cex.avail
   543      if (cexQuoteBalance) cexQuoteAvail = (cexQuoteBalance.available || 0) - qInv.cex.avail
   544      const [dexBaseAvail, dexQuoteAvail] = [baseWallet.balance.available - bInv.dex.avail, quoteWallet.balance.available - qInv.dex.avail]
   545      const baseAvail = dexBaseAvail + cexBaseAvail
   546      const quoteAvail = dexQuoteAvail + cexQuoteAvail
   547      const baseFeeWallet = baseFeeID === baseID ? baseWallet : app().walletMap[baseFeeID]
   548      const quoteFeeWallet = quoteFeeID === quoteID ? quoteWallet : app().walletMap[quoteFeeID]
   549  
   550      let [baseFeeAvail, dexBaseFeeAvail, cexBaseFeeAvail] = [baseAvail, dexBaseAvail, cexBaseAvail]
   551      if (baseFeeID !== baseID) {
   552        const bFeeInv = runningBotInventory(baseID)
   553        dexBaseFeeAvail = baseFeeWallet.balance.available - bFeeInv.dex.total
   554        if (cexBaseBalance) cexBaseFeeAvail = (cexBaseBalance.available || 0) - bFeeInv.cex.total
   555        baseFeeAvail = dexBaseFeeAvail + cexBaseFeeAvail
   556      }
   557      let [quoteFeeAvail, dexQuoteFeeAvail, cexQuoteFeeAvail] = [quoteAvail, dexQuoteAvail, cexQuoteAvail]
   558      if (quoteFeeID !== quoteID) {
   559        const qFeeInv = runningBotInventory(quoteID)
   560        dexQuoteFeeAvail = quoteFeeWallet.balance.available - qFeeInv.dex.total
   561        if (cexQuoteBalance) cexQuoteFeeAvail = (cexQuoteBalance.available || 0) - qFeeInv.cex.total
   562        quoteFeeAvail = dexQuoteFeeAvail + cexQuoteFeeAvail
   563      }
   564      return { // convert to conventioanl.
   565        baseAvail: baseAvail / baseFactor,
   566        quoteAvail: quoteAvail / quoteFactor,
   567        dexBaseAvail: dexBaseAvail / baseFactor,
   568        dexQuoteAvail: dexQuoteAvail / quoteFactor,
   569        cexBaseAvail: cexBaseAvail / baseFactor,
   570        cexQuoteAvail: cexQuoteAvail / quoteFactor,
   571        baseFeeAvail: baseFeeAvail / baseFeeFactor,
   572        quoteFeeAvail: quoteFeeAvail / quoteFeeFactor,
   573        dexBaseFeeAvail: dexBaseFeeAvail / baseFeeFactor,
   574        dexQuoteFeeAvail: dexQuoteFeeAvail / quoteFeeFactor,
   575        cexBaseFeeAvail: cexBaseFeeAvail / baseFeeFactor,
   576        cexQuoteFeeAvail: cexQuoteFeeAvail / quoteFeeFactor
   577      }
   578    }
   579  
   580    /*
   581     * feesAndCommit generates a snapshot of current market fees, as well as a
   582     * "commit", which is the funding dedicated to being on order. The commit
   583     * values do not include booking fees, order reserves, etc. just the order
   584     * quantity.
   585     */
   586    feesAndCommit () {
   587      const {
   588        baseID, quoteID, marketReport: { baseFees, quoteFees }, lotSize,
   589        baseLots, quoteLots, baseFeeID, quoteFeeID, baseIsAccountLocker, quoteIsAccountLocker,
   590        cfg: { uiConfig: { baseConfig, quoteConfig } }
   591      } = this
   592  
   593      return feesAndCommit(
   594        baseID, quoteID, baseFees, quoteFees, lotSize, baseLots, quoteLots,
   595        baseFeeID, quoteFeeID, baseIsAccountLocker, quoteIsAccountLocker,
   596        baseConfig.orderReservesFactor, quoteConfig.orderReservesFactor
   597      )
   598    }
   599  
   600    /*
   601     * projectedAllocations calculates the required asset allocations from the
   602     * user's configuration settings and the current market state.
   603     */
   604    projectedAllocations () {
   605      const {
   606        cfg: { uiConfig: { quoteConfig, baseConfig } },
   607        baseFactor, quoteFactor, baseID, quoteID, lotSizeConv, quoteLotConv,
   608        baseFeeFactor, quoteFeeFactor, baseFeeID, quoteFeeID, baseToken,
   609        quoteToken, cexName
   610      } = this
   611      const { commit, fees } = this.feesAndCommit()
   612  
   613      const bProj = emptyProjection()
   614      const qProj = emptyProjection()
   615  
   616      bProj.book = commit.dex.base.lots * lotSizeConv
   617      qProj.book = commit.cex.base.lots * quoteLotConv
   618  
   619      bProj.orderReserves = Math.max(commit.cex.base.val, commit.dex.base.val) * baseConfig.orderReservesFactor / baseFactor
   620      qProj.orderReserves = Math.max(commit.cex.quote.val, commit.dex.quote.val) * quoteConfig.orderReservesFactor / quoteFactor
   621  
   622      if (cexName) {
   623        bProj.cex = commit.cex.base.lots * lotSizeConv
   624        qProj.cex = commit.cex.quote.lots * quoteLotConv
   625      }
   626  
   627      bProj.bookingFees = fees.base.bookingFees / baseFeeFactor
   628      qProj.bookingFees = fees.quote.bookingFees / quoteFeeFactor
   629  
   630      if (baseToken) bProj.swapFeeReserves = fees.base.tokenFeesPerSwap * baseConfig.swapFeeN / baseFeeFactor
   631      if (quoteToken) qProj.swapFeeReserves = fees.quote.tokenFeesPerSwap * quoteConfig.swapFeeN / quoteFeeFactor
   632      qProj.slippageBuffer = (qProj.book + qProj.cex + qProj.orderReserves) * quoteConfig.slippageBufferFactor
   633  
   634      const alloc: Record<number, number> = {}
   635      const addAlloc = (assetID: number, amt: number) => { alloc[assetID] = (alloc[assetID] ?? 0) + amt }
   636      addAlloc(baseID, Math.round((bProj.book + bProj.cex + bProj.orderReserves) * baseFactor))
   637      addAlloc(baseFeeID, Math.round((bProj.bookingFees + bProj.swapFeeReserves) * baseFeeFactor))
   638      addAlloc(quoteID, Math.round((qProj.book + qProj.cex + qProj.orderReserves + qProj.slippageBuffer) * quoteFactor))
   639      addAlloc(quoteFeeID, Math.round((qProj.bookingFees + qProj.swapFeeReserves) * quoteFeeFactor))
   640  
   641      return { qProj, bProj, alloc }
   642    }
   643  
   644    /*
   645     * fundingState examines the projected allocations and the user's wallet
   646     * balances to determine whether the user can fund the bot fully, unbalanced,
   647     * or starved, and what funding source options might be available.
   648     */
   649    fundingState () {
   650      const {
   651        proj: { bProj, qProj }, baseID, quoteID, baseFeeID, quoteFeeID,
   652        cfg: { uiConfig: { cexRebalance } }, cexName
   653      } = this
   654      const {
   655        baseAvail, quoteAvail, dexBaseAvail, dexQuoteAvail, cexBaseAvail, cexQuoteAvail,
   656        dexBaseFeeAvail, dexQuoteFeeAvail
   657      } = this.adjustedBalances()
   658  
   659      const canRebalance = Boolean(cexName && cexRebalance)
   660  
   661      // Three possible states.
   662      // 1. We have the funding in the projection, and its in the right places.
   663      //    Give them some options for which wallet to pull order reserves from,
   664      //    but they can start immediately..
   665      // 2. We have the funding, but it's in the wrong place or the wrong asset,
   666      //    but we have deposits and withdraws enabled. We can offer them the
   667      //    option to start in an unbalanced state.
   668      // 3. We don't have the funds. We offer them an option to start in a
   669      //    starved state.
   670      const cexMinBaseAlloc = bProj.cex
   671      let [dexMinBaseAlloc, transferableBaseAlloc, dexBaseFeeReq] = [bProj.book, 0, 0]
   672      // Only add booking fees if this is the fee asset.
   673      if (baseID === baseFeeID) dexMinBaseAlloc += bProj.bookingFees
   674      // Base asset is a token.
   675      else dexBaseFeeReq += bProj.bookingFees + bProj.swapFeeReserves
   676      // If we can rebalance, the order reserves could potentially be withdrawn.
   677      if (canRebalance) transferableBaseAlloc += bProj.orderReserves
   678      // If we can't rebalance, order reserves are required in dex balance.
   679      else dexMinBaseAlloc += bProj.orderReserves
   680      // Handle the special case where the base asset it the quote asset's fee
   681      // asset.
   682      if (baseID === quoteFeeID) {
   683        if (canRebalance) transferableBaseAlloc += qProj.bookingFees + qProj.swapFeeReserves
   684        else dexMinBaseAlloc += qProj.bookingFees + qProj.swapFeeReserves
   685      }
   686  
   687      let [dexMinQuoteAlloc, cexMinQuoteAlloc, transferableQuoteAlloc, dexQuoteFeeReq] = [qProj.book, qProj.cex, 0, 0]
   688      if (quoteID === quoteFeeID) dexMinQuoteAlloc += qProj.bookingFees
   689      else dexQuoteFeeReq += qProj.bookingFees + qProj.swapFeeReserves
   690      if (canRebalance) transferableQuoteAlloc += qProj.orderReserves + qProj.slippageBuffer
   691      else {
   692        // The slippage reserves reserves should be split between cex and dex.
   693        dexMinQuoteAlloc += qProj.orderReserves
   694        const basis = qProj.book + qProj.cex + qProj.orderReserves
   695        dexMinQuoteAlloc += (qProj.book + qProj.orderReserves) / basis * qProj.slippageBuffer
   696        cexMinQuoteAlloc += qProj.cex / basis * qProj.slippageBuffer
   697      }
   698      if (quoteID === baseFeeID) {
   699        if (canRebalance) transferableQuoteAlloc += bProj.bookingFees + bProj.swapFeeReserves
   700        else dexMinQuoteAlloc += bProj.bookingFees + bProj.swapFeeReserves
   701      }
   702  
   703      const dexBaseFunded = dexBaseAvail >= dexMinBaseAlloc
   704      const cexBaseFunded = cexBaseAvail >= cexMinBaseAlloc
   705      const dexQuoteFunded = dexQuoteAvail >= dexMinQuoteAlloc
   706      const cexQuoteFunded = cexQuoteAvail >= cexMinQuoteAlloc
   707      const totalBaseReq = dexMinBaseAlloc + cexMinBaseAlloc + transferableBaseAlloc
   708      const totalQuoteReq = dexMinQuoteAlloc + cexMinQuoteAlloc + transferableQuoteAlloc
   709      const baseFundedAndBalanced = dexBaseFunded && cexBaseFunded && baseAvail >= totalBaseReq
   710      const quoteFundedAndBalanced = dexQuoteFunded && cexQuoteFunded && quoteAvail >= totalQuoteReq
   711      const baseFeesFunded = dexBaseFeeAvail >= dexBaseFeeReq
   712      const quoteFeesFunded = dexQuoteFeeAvail >= dexQuoteFeeReq
   713  
   714      const fundedAndBalanced = baseFundedAndBalanced && quoteFundedAndBalanced && baseFeesFunded && quoteFeesFunded
   715  
   716      // Are we funded but not balanced, but able to rebalance with a cex?
   717      let fundedAndNotBalanced = !fundedAndBalanced
   718      if (!fundedAndBalanced) {
   719        const ordersFunded = baseAvail >= totalBaseReq && quoteAvail >= totalQuoteReq
   720        const feesFunded = baseFeesFunded && quoteFeesFunded
   721        fundedAndNotBalanced = ordersFunded && feesFunded && canRebalance
   722      }
   723  
   724      return {
   725        base: {
   726          dex: {
   727            avail: dexBaseAvail,
   728            req: dexMinBaseAlloc,
   729            funded: dexBaseFunded
   730          },
   731          cex: {
   732            avail: cexBaseAvail,
   733            req: cexMinBaseAlloc,
   734            funded: cexBaseFunded
   735          },
   736          transferable: transferableBaseAlloc,
   737          fees: {
   738            avail: dexBaseFeeAvail,
   739            req: dexBaseFeeReq,
   740            funded: baseFeesFunded
   741          },
   742          fundedAndBalanced: baseFundedAndBalanced,
   743          fundedAndNotBalanced: !baseFundedAndBalanced && baseAvail >= totalBaseReq && canRebalance
   744        },
   745        quote: {
   746          dex: {
   747            avail: dexQuoteAvail,
   748            req: dexMinQuoteAlloc,
   749            funded: dexQuoteFunded
   750          },
   751          cex: {
   752            avail: cexQuoteAvail,
   753            req: cexMinQuoteAlloc,
   754            funded: cexQuoteFunded
   755          },
   756          transferable: transferableQuoteAlloc,
   757          fees: {
   758            avail: dexQuoteFeeAvail,
   759            req: dexQuoteFeeReq,
   760            funded: quoteFeesFunded
   761          },
   762          fundedAndBalanced: quoteFundedAndBalanced,
   763          fundedAndNotBalanced: !quoteFundedAndBalanced && quoteAvail >= totalQuoteReq && canRebalance
   764        },
   765        fundedAndBalanced,
   766        fundedAndNotBalanced,
   767        starved: !fundedAndBalanced && !fundedAndNotBalanced
   768      }
   769    }
   770  }
   771  
   772  export type RunningMMDisplayElements = {
   773    orderReportForm: PageElement
   774    dexBalancesRowTmpl: PageElement
   775    placementRowTmpl: PageElement
   776    placementAmtRowTmpl: PageElement
   777  }
   778  
   779  export class RunningMarketMakerDisplay {
   780    div: PageElement
   781    page: Record<string, PageElement>
   782    mkt: BotMarket
   783    startTime: number
   784    ticker: any
   785    currentForm: PageElement
   786    forms: Forms
   787    latestEpoch?: EpochReport
   788    cexProblems?: CEXProblems
   789    orderReportFormEl: PageElement
   790    orderReportForm: Record<string, PageElement>
   791    displayedOrderReportFormSide: 'buys' | 'sells'
   792    dexBalancesRowTmpl: PageElement
   793    placementRowTmpl: PageElement
   794    placementAmtRowTmpl: PageElement
   795  
   796    constructor (div: PageElement, forms: Forms, elements: RunningMMDisplayElements, page: string) {
   797      this.div = div
   798      this.page = Doc.parseTemplate(div)
   799      this.orderReportFormEl = elements.orderReportForm
   800      this.orderReportForm = Doc.idDescendants(elements.orderReportForm)
   801      this.dexBalancesRowTmpl = elements.dexBalancesRowTmpl
   802      this.placementRowTmpl = elements.placementRowTmpl
   803      this.placementAmtRowTmpl = elements.placementAmtRowTmpl
   804      Doc.cleanTemplates(this.dexBalancesRowTmpl, this.placementRowTmpl, this.placementAmtRowTmpl)
   805      this.forms = forms
   806      Doc.bind(this.page.stopBttn, 'click', () => this.stop())
   807      Doc.bind(this.page.runLogsBttn, 'click', () => {
   808        const { mkt: { baseID, quoteID, host }, startTime } = this
   809        app().loadPage('mmlogs', { baseID, quoteID, host, startTime, returnPage: page })
   810      })
   811      Doc.bind(this.page.buyOrdersBttn, 'click', () => this.showOrderReport('buys'))
   812      Doc.bind(this.page.sellOrdersBttn, 'click', () => this.showOrderReport('sells'))
   813    }
   814  
   815    async stop () {
   816      const { page, mkt: { host, baseID, quoteID } } = this
   817      const loaded = app().loading(page.stopBttn)
   818      await MM.stopBot({ host, baseID: baseID, quoteID: quoteID })
   819      loaded()
   820    }
   821  
   822    async setMarket (host: string, baseID: number, quoteID: number) {
   823      const botStatus = app().mmStatus.bots.find(({ config: c }: MMBotStatus) => c.baseID === baseID && c.quoteID === quoteID && c.host === host)
   824      if (!botStatus) return
   825      const mkt = new BotMarket(botStatus.config)
   826      await mkt.initialize()
   827      this.setBotMarket(mkt)
   828    }
   829  
   830    async setBotMarket (mkt: BotMarket) {
   831      this.mkt = mkt
   832      const {
   833        page, div, mkt: {
   834          host, baseID, quoteID, baseFeeID, quoteFeeID, cexName, baseFeeSymbol,
   835          quoteFeeSymbol, baseFeeTicker, quoteFeeTicker, cfg, baseFactor, quoteFactor
   836        }
   837      } = this
   838      setMarketElements(div, baseID, quoteID, host)
   839      Doc.setVis(baseFeeID !== baseID, page.baseFeeReservesBox)
   840      Doc.setVis(quoteFeeID !== quoteID, page.quoteFeeReservesBox)
   841      Doc.setVis(Boolean(cexName), ...Doc.applySelector(div, '[data-cex-show]'))
   842      page.baseFeeLogo.src = Doc.logoPath(baseFeeSymbol)
   843      page.baseFeeTicker.textContent = baseFeeTicker
   844      page.quoteFeeLogo.src = Doc.logoPath(quoteFeeSymbol)
   845      page.quoteFeeTicker.textContent = quoteFeeTicker
   846  
   847      const basicCfg = cfg.basicMarketMakingConfig
   848      const gapStrategy = basicCfg?.gapStrategy ?? GapStrategyPercent
   849      let gapFactor = cfg.arbMarketMakingConfig?.profit ?? cfg.simpleArbConfig?.profitTrigger ?? 0
   850      if (basicCfg) {
   851        const buys = [...basicCfg.buyPlacements].sort((a: OrderPlacement, b: OrderPlacement) => a.gapFactor - b.gapFactor)
   852        const sells = [...basicCfg.sellPlacements].sort((a: OrderPlacement, b: OrderPlacement) => a.gapFactor - b.gapFactor)
   853        if (buys.length > 0) {
   854          if (sells.length > 0) {
   855            gapFactor = (buys[0].gapFactor + sells[0].gapFactor) / 2
   856          } else {
   857            gapFactor = buys[0].gapFactor
   858          }
   859        } else gapFactor = sells[0].gapFactor
   860      }
   861      Doc.hide(page.profitLabel, page.gapLabel, page.multiplierLabel, page.profitUnit, page.gapUnit, page.multiplierUnit)
   862      switch (gapStrategy) {
   863        case GapStrategyPercent:
   864        case GapStrategyPercentPlus:
   865          Doc.show(page.profitLabel, page.profitUnit)
   866          page.gapFactor.textContent = (gapFactor * 100).toFixed(2)
   867          break
   868        case GapStrategyMultiplier:
   869          Doc.show(page.multiplierLabel, page.multiplierUnit)
   870          page.gapFactor.textContent = (gapFactor * 100).toFixed(2)
   871          break
   872        default:
   873          page.gapFactor.textContent = Doc.formatFourSigFigs(gapFactor / OrderUtil.RateEncodingFactor * baseFactor / quoteFactor)
   874      }
   875  
   876      this.update()
   877      this.readBook()
   878    }
   879  
   880    handleBalanceNote (n: BalanceNote) {
   881      if (!this.mkt) return
   882      const { baseID, quoteID, baseFeeID, quoteFeeID } = this.mkt
   883      if (n.assetID !== baseID && n.assetID !== baseFeeID && n.assetID !== quoteID && n.assetID !== quoteFeeID) return
   884      this.update()
   885    }
   886  
   887    handleEpochReportNote (n: EpochReportNote) {
   888      if (!this.mkt) return
   889      const { baseID, quoteID, host } = this.mkt
   890      if (n.baseID !== baseID || n.quoteID !== quoteID || n.host !== host) return
   891      if (!n.report) return
   892      this.latestEpoch = n.report
   893      if (this.forms.currentForm === this.orderReportFormEl && this.forms.currentFormID === this.mkt.id) {
   894        const orderReport = this.displayedOrderReportFormSide === 'buys' ? n.report.buysReport : n.report.sellsReport
   895        if (orderReport) this.updateOrderReport(orderReport, this.displayedOrderReportFormSide, n.report.epochNum)
   896        else this.forms.close()
   897      }
   898      this.update()
   899    }
   900  
   901    handleCexProblemsNote (n: CEXProblemsNote) {
   902      if (!this.mkt) return
   903      const { baseID, quoteID, host } = this.mkt
   904      if (n.baseID !== baseID || n.quoteID !== quoteID || n.host !== host) return
   905      this.cexProblems = n.problems
   906      this.update()
   907    }
   908  
   909    setTicker () {
   910      this.page.runTime.textContent = Doc.hmsSince(this.startTime)
   911    }
   912  
   913    update () {
   914      const {
   915        div, page, mkt: {
   916          baseID, quoteID, baseFeeID, quoteFeeID, baseFactor, quoteFactor, baseFeeFactor,
   917          quoteFeeFactor, marketReport: { baseFiatRate, quoteFiatRate }
   918        }
   919      } = this
   920      // Get fresh stats
   921      const { botCfg: { cexName, basicMarketMakingConfig: bmmCfg }, runStats, latestEpoch, cexProblems } = this.mkt.status()
   922      this.latestEpoch = latestEpoch
   923      this.cexProblems = cexProblems
   924  
   925      Doc.hide(page.stats, page.cexRow, page.pendingDepositBox, page.pendingWithdrawalBox)
   926  
   927      if (!runStats) {
   928        if (this.ticker) {
   929          clearInterval(this.ticker)
   930          this.ticker = undefined
   931        }
   932        return
   933      } else if (!this.ticker) {
   934        this.startTime = runStats.startTime
   935        this.setTicker()
   936        this.ticker = setInterval(() => this.setTicker(), 1000)
   937      }
   938  
   939      Doc.show(page.stats)
   940      setSignedValue(runStats.profitLoss.profitRatio * 100, page.profit, page.profitSign, 2)
   941      setSignedValue(runStats.profitLoss.profit, page.profitLoss, page.plSign, 2)
   942      this.startTime = runStats.startTime
   943  
   944      const summedBalance = (b: BotBalance) => {
   945        if (!b) return 0
   946        return b.available + b.locked + b.pending + b.reserved
   947      }
   948  
   949      const dexBaseInv = summedBalance(runStats.dexBalances[baseID]) / baseFactor
   950      page.walletBaseInventory.textContent = Doc.formatFourSigFigs(dexBaseInv)
   951      page.walletBaseInvFiat.textContent = Doc.formatFourSigFigs(dexBaseInv * baseFiatRate, 2)
   952      const dexQuoteInv = summedBalance(runStats.dexBalances[quoteID]) / quoteFactor
   953      page.walletQuoteInventory.textContent = Doc.formatFourSigFigs(dexQuoteInv)
   954      page.walletQuoteInvFiat.textContent = Doc.formatFourSigFigs(dexQuoteInv * quoteFiatRate, 2)
   955  
   956      Doc.setVis(cexName, page.cexRow)
   957      if (cexName) {
   958        Doc.show(page.pendingDepositBox, page.pendingWithdrawalBox)
   959        setCexElements(div, cexName)
   960        const cexBaseInv = summedBalance(runStats.cexBalances[baseID]) / baseFactor
   961        page.cexBaseInventory.textContent = Doc.formatFourSigFigs(cexBaseInv)
   962        page.cexBaseInventoryFiat.textContent = Doc.formatFourSigFigs(cexBaseInv * baseFiatRate, 2)
   963        const cexQuoteInv = summedBalance(runStats.cexBalances[quoteID]) / quoteFactor
   964        page.cexQuoteInventory.textContent = Doc.formatFourSigFigs(cexQuoteInv)
   965        page.cexQuoteInventoryFiat.textContent = Doc.formatFourSigFigs(cexQuoteInv * quoteFiatRate, 2)
   966      }
   967  
   968      if (baseFeeID !== baseID) {
   969        const feeBalance = summedBalance(runStats.dexBalances[baseFeeID]) / baseFeeFactor
   970        page.baseFeeReserves.textContent = Doc.formatFourSigFigs(feeBalance)
   971      }
   972      if (quoteFeeID !== quoteID) {
   973        const feeBalance = summedBalance(runStats.dexBalances[quoteFeeID]) / quoteFeeFactor
   974        page.quoteFeeReserves.textContent = Doc.formatFourSigFigs(feeBalance)
   975      }
   976  
   977      page.pendingDeposits.textContent = String(Math.round(runStats.pendingDeposits))
   978      page.pendingWithdrawals.textContent = String(Math.round(runStats.pendingWithdrawals))
   979      page.completedMatches.textContent = String(Math.round(runStats.completedMatches))
   980      Doc.setVis(runStats.tradedUSD, page.tradedUSDBox)
   981      if (runStats.tradedUSD > 0) page.tradedUSD.textContent = Doc.formatFourSigFigs(runStats.tradedUSD)
   982      Doc.setVis(baseFiatRate, page.roundTripFeesBox)
   983      if (baseFiatRate) page.roundTripFeesUSD.textContent = Doc.formatFourSigFigs((runStats.feeGap?.roundTripFees / baseFactor * baseFiatRate) || 0)
   984      const basisPrice = app().conventionalRate(baseID, quoteID, runStats.feeGap?.basisPrice || 0)
   985      page.basisPrice.textContent = Doc.formatFourSigFigs(basisPrice)
   986  
   987      const displayFeeGap = !bmmCfg || bmmCfg.gapStrategy === GapStrategyAbsolutePlus || bmmCfg.gapStrategy === GapStrategyPercentPlus
   988      Doc.setVis(displayFeeGap, page.feeGapBox)
   989      if (displayFeeGap) {
   990        const feeGap = app().conventionalRate(baseID, quoteID, runStats.feeGap?.feeGap || 0)
   991        page.feeGap.textContent = Doc.formatFourSigFigs(feeGap)
   992        page.feeGapPct.textContent = (feeGap / basisPrice * 100 || 0).toFixed(2)
   993      }
   994      Doc.setVis(bmmCfg, page.gapStrategyBox)
   995      if (bmmCfg) page.gapStrategy.textContent = bmmCfg.gapStrategy
   996  
   997      const remoteGap = app().conventionalRate(baseID, quoteID, runStats.feeGap?.remoteGap || 0)
   998      Doc.setVis(remoteGap, page.remoteGapBox)
   999      if (remoteGap) {
  1000        page.remoteGap.textContent = Doc.formatFourSigFigs(remoteGap)
  1001        page.remoteGapPct.textContent = (remoteGap / basisPrice * 100 || 0).toFixed(2)
  1002      }
  1003  
  1004      Doc.setVis(latestEpoch?.buysReport, page.buyOrdersReportBox)
  1005      if (latestEpoch?.buysReport) {
  1006        const allPlaced = allOrdersPlaced(latestEpoch.buysReport)
  1007        Doc.setVis(allPlaced, page.buyOrdersSuccess)
  1008        Doc.setVis(!allPlaced, page.buyOrdersFailed)
  1009      }
  1010  
  1011      Doc.setVis(latestEpoch?.sellsReport, page.sellOrdersReportBox)
  1012      if (latestEpoch?.sellsReport) {
  1013        const allPlaced = allOrdersPlaced(latestEpoch.sellsReport)
  1014        Doc.setVis(allPlaced, page.sellOrdersSuccess)
  1015        Doc.setVis(!allPlaced, page.sellOrdersFailed)
  1016      }
  1017  
  1018      const preOrderProblemMessages = botProblemMessages(latestEpoch?.preOrderProblems, this.mkt.cexName, this.mkt.host)
  1019      const cexErrorMessages = cexProblemMessages(this.cexProblems)
  1020      const allMessages = [...preOrderProblemMessages, ...cexErrorMessages]
  1021      Doc.setVis(allMessages.length > 0, page.preOrderProblemsBox)
  1022      Doc.empty(page.preOrderProblemsBox)
  1023      for (const msg of allMessages) {
  1024        const spanEl = document.createElement('span') as PageElement
  1025        spanEl.textContent = `- ${msg}`
  1026        page.preOrderProblemsBox.appendChild(spanEl)
  1027      }
  1028    }
  1029  
  1030    updateOrderReport (report: OrderReport, side: 'buys' | 'sells', epochNum: number) {
  1031      const form = this.orderReportForm
  1032      const sideTxt = side === 'buys' ? intl.prep(intl.ID_BUY) : intl.prep(intl.ID_SELL)
  1033      form.orderReportTitle.textContent = intl.prep(intl.ID_ORDER_REPORT_TITLE, { side: sideTxt, epochNum: `${epochNum}` })
  1034  
  1035      Doc.setVis(report.error, form.orderReportError)
  1036      Doc.setVis(!report.error, form.orderReportDetails)
  1037      if (report.error) {
  1038        const problemMessages = botProblemMessages(report.error, this.mkt.cexName, this.mkt.host)
  1039        Doc.empty(form.orderReportError)
  1040        for (const msg of problemMessages) {
  1041          const spanEl = document.createElement('span') as PageElement
  1042          spanEl.textContent = `- ${msg}`
  1043          form.orderReportError.appendChild(spanEl)
  1044        }
  1045        return
  1046      }
  1047  
  1048      Doc.empty(form.dexBalancesBody, form.placementsBody)
  1049      const createRow = (assetID: number): [PageElement, number] => {
  1050        const row = this.dexBalancesRowTmpl.cloneNode(true) as HTMLElement
  1051        const rowTmpl = Doc.parseTemplate(row)
  1052        const asset = app().assets[assetID]
  1053        rowTmpl.asset.textContent = asset.symbol.toUpperCase()
  1054        rowTmpl.assetLogo.src = Doc.logoPath(asset.symbol)
  1055        const unitInfo = asset.unitInfo
  1056        const available = report.availableDexBals[assetID] ? report.availableDexBals[assetID].available : 0
  1057        const required = report.requiredDexBals[assetID] ? report.requiredDexBals[assetID] : 0
  1058        const remaining = report.remainingDexBals[assetID] ? report.remainingDexBals[assetID] : 0
  1059        const pending = report.availableDexBals[assetID] ? report.availableDexBals[assetID].pending : 0
  1060        const locked = report.availableDexBals[assetID] ? report.availableDexBals[assetID].locked : 0
  1061        const used = report.usedDexBals[assetID] ? report.usedDexBals[assetID] : 0
  1062        rowTmpl.available.textContent = Doc.formatCoinValue(available, unitInfo)
  1063        rowTmpl.locked.textContent = Doc.formatCoinValue(locked, unitInfo)
  1064        rowTmpl.required.textContent = Doc.formatCoinValue(required, unitInfo)
  1065        rowTmpl.remaining.textContent = Doc.formatCoinValue(remaining, unitInfo)
  1066        rowTmpl.pending.textContent = Doc.formatCoinValue(pending, unitInfo)
  1067        rowTmpl.used.textContent = Doc.formatCoinValue(used, unitInfo)
  1068        const deficiency = safeSub(required, available)
  1069        rowTmpl.deficiency.textContent = Doc.formatCoinValue(deficiency, unitInfo)
  1070        if (deficiency > 0) rowTmpl.deficiency.classList.add('text-warning')
  1071        const deficiencyWithPending = safeSub(deficiency, pending)
  1072        rowTmpl.deficiencyWithPending.textContent = Doc.formatCoinValue(deficiencyWithPending, unitInfo)
  1073        if (deficiencyWithPending > 0) rowTmpl.deficiencyWithPending.classList.add('text-warning')
  1074        return [row, deficiency]
  1075      }
  1076      const setDeficiencyVisibility = (deficiency: boolean, rows: HTMLElement[]) => {
  1077        Doc.setVis(deficiency, form.dexDeficiencyHeader, form.dexDeficiencyWithPendingHeader)
  1078        for (const row of rows) {
  1079          const rowTmpl = Doc.parseTemplate(row)
  1080          Doc.setVis(deficiency, rowTmpl.deficiency, rowTmpl.deficiencyWithPending)
  1081        }
  1082      }
  1083      const assetIDs = [this.mkt.baseID, this.mkt.quoteID]
  1084      if (!assetIDs.includes(this.mkt.baseFeeID)) assetIDs.push(this.mkt.baseFeeID)
  1085      if (!assetIDs.includes(this.mkt.quoteFeeID)) assetIDs.push(this.mkt.quoteFeeID)
  1086      let totalDeficiency = 0
  1087      const rows : PageElement[] = []
  1088      for (const assetID of assetIDs) {
  1089        const [row, deficiency] = createRow(assetID)
  1090        totalDeficiency += deficiency
  1091        form.dexBalancesBody.appendChild(row)
  1092        rows.push(row)
  1093      }
  1094      setDeficiencyVisibility(totalDeficiency > 0, rows)
  1095  
  1096      Doc.setVis(this.mkt.cexName, form.cexSection, form.counterTradeRateHeader, form.requiredCEXHeader, form.usedCEXHeader)
  1097      let cexAsset: SupportedAsset
  1098      if (this.mkt.cexName) {
  1099        const cexDisplayInfo = CEXDisplayInfos[this.mkt.cexName]
  1100        if (cexDisplayInfo) {
  1101          form.cexLogo.src = cexDisplayInfo.logo
  1102          form.cexBalancesTitle.textContent = intl.prep(intl.ID_CEX_BALANCES, { cexName: cexDisplayInfo.name })
  1103        } else {
  1104          console.error(`CEXDisplayInfo not found for ${this.mkt.cexName}`)
  1105        }
  1106        const cexAssetID = side === 'buys' ? this.mkt.baseID : this.mkt.quoteID
  1107        cexAsset = app().assets[cexAssetID]
  1108        form.cexAsset.textContent = cexAsset.symbol.toUpperCase()
  1109        form.cexAssetLogo.src = Doc.logoPath(cexAsset.symbol)
  1110        const availableCexBal = report.availableCexBal ? report.availableCexBal.available : 0
  1111        const requiredCexBal = report.requiredCexBal ? report.requiredCexBal : 0
  1112        const remainingCexBal = report.remainingCexBal ? report.remainingCexBal : 0
  1113        const pendingCexBal = report.availableCexBal ? report.availableCexBal.pending : 0
  1114        const reservedCexBal = report.availableCexBal ? report.availableCexBal.reserved : 0
  1115        const usedCexBal = report.usedCexBal ? report.usedCexBal : 0
  1116        const deficiencyCexBal = safeSub(requiredCexBal, availableCexBal)
  1117        const deficiencyWithPendingCexBal = safeSub(deficiencyCexBal, pendingCexBal)
  1118        form.cexAvailable.textContent = Doc.formatCoinValue(availableCexBal, cexAsset.unitInfo)
  1119        form.cexLocked.textContent = Doc.formatCoinValue(reservedCexBal, cexAsset.unitInfo)
  1120        form.cexRequired.textContent = Doc.formatCoinValue(requiredCexBal, cexAsset.unitInfo)
  1121        form.cexRemaining.textContent = Doc.formatCoinValue(remainingCexBal, cexAsset.unitInfo)
  1122        form.cexPending.textContent = Doc.formatCoinValue(pendingCexBal, cexAsset.unitInfo)
  1123        form.cexUsed.textContent = Doc.formatCoinValue(usedCexBal, cexAsset.unitInfo)
  1124        const deficient = deficiencyCexBal > 0
  1125        Doc.setVis(deficient, form.cexDeficiencyHeader, form.cexDeficiencyWithPendingHeader,
  1126          form.cexDeficiency, form.cexDeficiencyWithPending)
  1127        if (deficient) {
  1128          form.cexDeficiency.textContent = Doc.formatCoinValue(deficiencyCexBal, cexAsset.unitInfo)
  1129          form.cexDeficiencyWithPending.textContent = Doc.formatCoinValue(deficiencyWithPendingCexBal, cexAsset.unitInfo)
  1130          if (deficiencyWithPendingCexBal > 0) form.cexDeficiencyWithPending.classList.add('text-warning')
  1131          else form.cexDeficiencyWithPending.classList.remove('text-warning')
  1132        }
  1133      }
  1134  
  1135      let anyErrors = false
  1136      for (const placement of report.placements) if (placement.error) { anyErrors = true; break }
  1137      Doc.setVis(anyErrors, form.errorHeader)
  1138      const createPlacementRow = (placement: TradePlacement, priority: number): PageElement => {
  1139        const row = this.placementRowTmpl.cloneNode(true) as HTMLElement
  1140        const rowTmpl = Doc.parseTemplate(row)
  1141        const baseUI = app().assets[this.mkt.baseID].unitInfo
  1142        const quoteUI = app().assets[this.mkt.quoteID].unitInfo
  1143        rowTmpl.priority.textContent = String(priority)
  1144        rowTmpl.rate.textContent = Doc.formatRateFullPrecision(placement.rate, baseUI, quoteUI, this.mkt.rateStep)
  1145        rowTmpl.lots.textContent = String(placement.lots)
  1146        rowTmpl.standingLots.textContent = String(placement.standingLots)
  1147        rowTmpl.orderedLots.textContent = String(placement.orderedLots)
  1148        if (placement.standingLots + placement.orderedLots < placement.lots) {
  1149          rowTmpl.lots.classList.add('text-warning')
  1150          rowTmpl.standingLots.classList.add('text-warning')
  1151          rowTmpl.orderedLots.classList.add('text-warning')
  1152        }
  1153        Doc.setVis(placement.counterTradeRate > 0, rowTmpl.counterTradeRate)
  1154        rowTmpl.counterTradeRate.textContent = Doc.formatRateFullPrecision(placement.counterTradeRate, baseUI, quoteUI, this.mkt.rateStep)
  1155        for (const assetID of assetIDs) {
  1156          const asset = app().assets[assetID]
  1157          const unitInfo = asset.unitInfo
  1158          const requiredAmt = placement.requiredDex[assetID] ? placement.requiredDex[assetID] : 0
  1159          const usedAmt = placement.usedDex[assetID] ? placement.usedDex[assetID] : 0
  1160          const requiredRow = this.placementAmtRowTmpl.cloneNode(true) as HTMLElement
  1161          const requiredRowTmpl = Doc.parseTemplate(requiredRow)
  1162          const usedRow = this.placementAmtRowTmpl.cloneNode(true) as HTMLElement
  1163          const usedRowTmpl = Doc.parseTemplate(usedRow)
  1164          requiredRowTmpl.amt.textContent = Doc.formatCoinValue(requiredAmt, unitInfo)
  1165          requiredRowTmpl.assetLogo.src = Doc.logoPath(asset.symbol)
  1166          requiredRowTmpl.assetSymbol.textContent = asset.symbol.toUpperCase()
  1167          usedRowTmpl.amt.textContent = Doc.formatCoinValue(usedAmt, unitInfo)
  1168          usedRowTmpl.assetLogo.src = Doc.logoPath(asset.symbol)
  1169          usedRowTmpl.assetSymbol.textContent = asset.symbol.toUpperCase()
  1170          rowTmpl.requiredDEX.appendChild(requiredRow)
  1171          rowTmpl.usedDEX.appendChild(usedRow)
  1172        }
  1173        Doc.setVis(this.mkt.cexName, rowTmpl.requiredCEX, rowTmpl.usedCEX)
  1174        if (this.mkt.cexName) {
  1175          const requiredAmt = Doc.formatCoinValue(placement.requiredCex, cexAsset.unitInfo)
  1176          rowTmpl.requiredCEX.textContent = `${requiredAmt} ${cexAsset.symbol.toUpperCase()}`
  1177          const usedAmt = Doc.formatCoinValue(placement.usedCex, cexAsset.unitInfo)
  1178          rowTmpl.usedCEX.textContent = `${usedAmt} ${cexAsset.symbol.toUpperCase()}`
  1179        }
  1180        Doc.setVis(anyErrors, rowTmpl.error)
  1181        if (placement.error) {
  1182          const errMessages = botProblemMessages(placement.error, this.mkt.cexName, this.mkt.host)
  1183          rowTmpl.error.textContent = errMessages.join('\n')
  1184        }
  1185        return row
  1186      }
  1187      for (let i = 0; i < report.placements.length; i++) {
  1188        form.placementsBody.appendChild(createPlacementRow(report.placements[i], i + 1))
  1189      }
  1190    }
  1191  
  1192    showOrderReport (side: 'buys' | 'sells') {
  1193      if (!this.latestEpoch) return
  1194      const report = side === 'buys' ? this.latestEpoch.buysReport : this.latestEpoch.sellsReport
  1195      if (!report) return
  1196      this.updateOrderReport(report, side, this.latestEpoch.epochNum)
  1197      this.displayedOrderReportFormSide = side
  1198      this.forms.show(this.orderReportFormEl, this.mkt.id)
  1199    }
  1200  
  1201    readBook () {
  1202      if (!this.mkt) return
  1203      const { page, mkt: { host, mktID } } = this
  1204      const orders = app().exchanges[host].markets[mktID].orders || []
  1205      page.nBookedOrders.textContent = String(orders.filter((ord: Order) => ord.status === OrderUtil.StatusBooked).length)
  1206    }
  1207  }
  1208  
  1209  function allOrdersPlaced (report: OrderReport) {
  1210    if (report.error) return false
  1211    for (let i = 0; i < report.placements.length; i++) {
  1212      const placement = report.placements[i]
  1213      if (placement.orderedLots + placement.standingLots < placement.lots) return false
  1214      if (placement.error) return false
  1215    }
  1216    return true
  1217  }
  1218  
  1219  function setSignedValue (v: number, vEl: PageElement, signEl: PageElement, maxDecimals?: number) {
  1220    vEl.textContent = Doc.formatFourSigFigs(v, maxDecimals)
  1221    signEl.classList.toggle('ico-plus', v > 0)
  1222    signEl.classList.toggle('text-good', v > 0)
  1223    // signEl.classList.toggle('ico-minus', v < 0)
  1224  }
  1225  
  1226  export function feesAndCommit (
  1227    baseID: number, quoteID: number, baseFees: LotFeeRange, quoteFees: LotFeeRange,
  1228    lotSize: number, baseLots: number, quoteLots: number, baseFeeID: number, quoteFeeID: number,
  1229    baseIsAccountLocker: boolean, quoteIsAccountLocker: boolean, baseOrderReservesFactor: number,
  1230    quoteOrderReservesFactor: number
  1231  ) {
  1232    const quoteLot = calculateQuoteLot(lotSize, baseID, quoteID)
  1233    const [cexBaseLots, cexQuoteLots] = [quoteLots, baseLots]
  1234    const commit = {
  1235      dex: {
  1236        base: {
  1237          lots: baseLots,
  1238          val: baseLots * lotSize
  1239        },
  1240        quote: {
  1241          lots: quoteLots,
  1242          val: quoteLots * quoteLot
  1243        }
  1244      },
  1245      cex: {
  1246        base: {
  1247          lots: cexBaseLots,
  1248          val: cexBaseLots * lotSize
  1249        },
  1250        quote: {
  1251          lots: cexQuoteLots,
  1252          val: cexQuoteLots * quoteLot
  1253        }
  1254      }
  1255    }
  1256  
  1257    let baseTokenFeesPerSwap = 0
  1258    let baseRedeemReservesPerLot = 0
  1259    if (baseID !== baseFeeID) { // token
  1260      baseTokenFeesPerSwap += baseFees.estimated.swap
  1261      if (baseFeeID === quoteFeeID) baseTokenFeesPerSwap += quoteFees.estimated.redeem
  1262    }
  1263    let baseBookingFeesPerLot = baseFees.max.swap
  1264    if (baseID === quoteFeeID) baseBookingFeesPerLot += quoteFees.max.redeem
  1265    if (baseIsAccountLocker) {
  1266      baseBookingFeesPerLot += baseFees.max.refund
  1267      if (!quoteIsAccountLocker && baseFeeID !== quoteFeeID) baseRedeemReservesPerLot = baseFees.max.redeem
  1268    }
  1269  
  1270    let quoteTokenFeesPerSwap = 0
  1271    let quoteRedeemReservesPerLot = 0
  1272    if (quoteID !== quoteFeeID) {
  1273      quoteTokenFeesPerSwap += quoteFees.estimated.swap
  1274      if (quoteFeeID === baseFeeID) quoteTokenFeesPerSwap += baseFees.estimated.redeem
  1275    }
  1276    let quoteBookingFeesPerLot = quoteFees.max.swap
  1277    if (quoteID === baseFeeID) quoteBookingFeesPerLot += baseFees.max.redeem
  1278    if (quoteIsAccountLocker) {
  1279      quoteBookingFeesPerLot += quoteFees.max.refund
  1280      if (!baseIsAccountLocker && quoteFeeID !== baseFeeID) quoteRedeemReservesPerLot = quoteFees.max.redeem
  1281    }
  1282  
  1283    const baseReservesFactor = 1 + baseOrderReservesFactor
  1284    const quoteReservesFactor = 1 + quoteOrderReservesFactor
  1285  
  1286    const baseBookingFees = (baseBookingFeesPerLot * baseLots) * baseReservesFactor
  1287    const baseRedeemFees = (baseRedeemReservesPerLot * quoteLots) * quoteReservesFactor
  1288    const quoteBookingFees = (quoteBookingFeesPerLot * quoteLots) * quoteReservesFactor
  1289    const quoteRedeemFees = (quoteRedeemReservesPerLot * baseLots) * baseReservesFactor
  1290  
  1291    const fees: BookingFees = {
  1292      base: {
  1293        ...baseFees,
  1294        bookingFeesPerLot: baseBookingFeesPerLot,
  1295        bookingFeesPerCounterLot: baseRedeemReservesPerLot,
  1296        bookingFees: baseBookingFees + baseRedeemFees,
  1297        swapReservesFactor: baseReservesFactor,
  1298        redeemReservesFactor: quoteReservesFactor,
  1299        tokenFeesPerSwap: baseTokenFeesPerSwap
  1300      },
  1301      quote: {
  1302        ...quoteFees,
  1303        bookingFeesPerLot: quoteBookingFeesPerLot,
  1304        bookingFeesPerCounterLot: quoteRedeemReservesPerLot,
  1305        bookingFees: quoteBookingFees + quoteRedeemFees,
  1306        swapReservesFactor: quoteReservesFactor,
  1307        redeemReservesFactor: baseReservesFactor,
  1308        tokenFeesPerSwap: quoteTokenFeesPerSwap
  1309      }
  1310    }
  1311  
  1312    return { commit, fees }
  1313  }
  1314  
  1315  function botProblemMessages (problems: BotProblems | undefined, cexName: string, dexHost: string): string[] {
  1316    if (!problems) return []
  1317    const msgs: string[] = []
  1318  
  1319    if (problems.walletNotSynced) {
  1320      for (const [assetID, notSynced] of Object.entries(problems.walletNotSynced)) {
  1321        if (notSynced) {
  1322          msgs.push(intl.prep(intl.ID_WALLET_NOT_SYNCED, { assetSymbol: app().assets[Number(assetID)].symbol.toUpperCase() }))
  1323        }
  1324      }
  1325    }
  1326  
  1327    if (problems.noWalletPeers) {
  1328      for (const [assetID, noPeers] of Object.entries(problems.noWalletPeers)) {
  1329        if (noPeers) {
  1330          msgs.push(intl.prep(intl.ID_WALLET_NO_PEERS, { assetSymbol: app().assets[Number(assetID)].symbol.toUpperCase() }))
  1331        }
  1332      }
  1333    }
  1334  
  1335    if (problems.accountSuspended) {
  1336      msgs.push(intl.prep(intl.ID_ACCOUNT_SUSPENDED, { dexHost: dexHost }))
  1337    }
  1338  
  1339    if (problems.userLimitTooLow) {
  1340      msgs.push(intl.prep(intl.ID_USER_LIMIT_TOO_LOW, { dexHost: dexHost }))
  1341    }
  1342  
  1343    if (problems.noPriceSource) {
  1344      msgs.push(intl.prep(intl.ID_NO_PRICE_SOURCE))
  1345    }
  1346  
  1347    if (problems.cexOrderbookUnsynced) {
  1348      msgs.push(intl.prep(intl.ID_CEX_ORDERBOOK_UNSYNCED, { cexName: cexName }))
  1349    }
  1350  
  1351    if (problems.causesSelfMatch) {
  1352      msgs.push(intl.prep(intl.ID_CAUSES_SELF_MATCH))
  1353    }
  1354  
  1355    if (problems.unknownError) {
  1356      msgs.push(problems.unknownError)
  1357    }
  1358  
  1359    return msgs
  1360  }
  1361  
  1362  function cexProblemMessages (problems: CEXProblems | undefined): string[] {
  1363    if (!problems) return []
  1364    const msgs: string[] = []
  1365    if (problems.depositErr) {
  1366      for (const [assetID, depositErr] of Object.entries(problems.depositErr)) {
  1367        msgs.push(intl.prep(intl.ID_DEPOSIT_ERROR,
  1368          {
  1369            assetSymbol: app().assets[Number(assetID)].symbol.toUpperCase(),
  1370            time: new Date(depositErr.stamp * 1000).toLocaleString(),
  1371            error: depositErr.error
  1372          }))
  1373      }
  1374    }
  1375    if (problems.withdrawErr) {
  1376      for (const [assetID, withdrawErr] of Object.entries(problems.withdrawErr)) {
  1377        msgs.push(intl.prep(intl.ID_WITHDRAW_ERROR,
  1378          {
  1379            assetSymbol: app().assets[Number(assetID)].symbol.toUpperCase(),
  1380            time: new Date(withdrawErr.stamp * 1000).toLocaleString(),
  1381            error: withdrawErr.error
  1382          }))
  1383      }
  1384    }
  1385    if (problems.tradeErr) {
  1386      msgs.push(intl.prep(intl.ID_CEX_TRADE_ERROR,
  1387        {
  1388          time: new Date(problems.tradeErr.stamp * 1000).toLocaleString(),
  1389          error: problems.tradeErr.error
  1390        }))
  1391    }
  1392    return msgs
  1393  }
  1394  
  1395  function safeSub (a: number, b: number) {
  1396    return a - b > 0 ? a - b : 0
  1397  }
  1398  
  1399  window.mmstatus = function () : Promise<MarketMakingStatus> {
  1400    return MM.status()
  1401  }