decred.org/dcrdex@v1.0.3/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 }