decred.org/dcrdex@v1.0.3/client/webserver/site/src/js/mmsettings.ts (about) 1 import { 2 PageElement, 3 BotConfig, 4 OrderPlacement, 5 app, 6 Spot, 7 MarketReport, 8 OrderOption, 9 CEXConfig, 10 BasicMarketMakingConfig, 11 ArbMarketMakingConfig, 12 SimpleArbConfig, 13 ArbMarketMakingPlacement, 14 ExchangeBalance, 15 MarketMakingStatus, 16 MMCEXStatus, 17 BalanceNote, 18 BotAssetConfig, 19 ApprovalStatus, 20 SupportedAsset, 21 WalletState, 22 UnitInfo, 23 ProjectedAlloc, 24 AssetBookingFees 25 } from './registry' 26 import Doc, { 27 NumberInput, 28 MiniSlider, 29 IncrementalInput, 30 toFourSigFigs, 31 toPrecision, 32 parseFloatDefault 33 } from './doc' 34 import State from './state' 35 import BasePage from './basepage' 36 import { setOptionTemplates } from './opts' 37 import { 38 MM, 39 CEXDisplayInfos, 40 botTypeBasicArb, 41 botTypeArbMM, 42 botTypeBasicMM, 43 runningBotInventory, 44 setMarketElements, 45 setCexElements, 46 calculateQuoteLot, 47 PlacementsChart, 48 liveBotConfig, 49 GapStrategyMultiplier, 50 GapStrategyAbsolute, 51 GapStrategyAbsolutePlus, 52 GapStrategyPercent, 53 GapStrategyPercentPlus, 54 feesAndCommit 55 } from './mmutil' 56 import { Forms, bind as bindForm, NewWalletForm, TokenApprovalForm, DepositAddress, CEXConfigurationForm } from './forms' 57 import * as intl from './locales' 58 import * as OrderUtil from './orderutil' 59 60 const specLK = 'lastMMSpecs' 61 const lastBotsLK = 'lastBots' 62 const lastArbExchangeLK = 'lastArbExchange' 63 const arbMMRowCacheKey = 'arbmm' 64 65 const defaultSwapReserves = { 66 n: 50, 67 prec: 0, 68 inc: 10, 69 minR: 0, 70 maxR: 1000, 71 range: 1000 72 } 73 const defaultOrderReserves = { 74 factor: 1.0, 75 minR: 0, 76 maxR: 3, 77 range: 3, 78 prec: 3 79 } 80 const defaultTransfer = { 81 factor: 0.1, 82 minR: 0, 83 maxR: 1, 84 range: 1 85 } 86 const defaultSlippage = { 87 factor: 0.05, 88 minR: 0, 89 maxR: 0.3, 90 range: 0.3, 91 prec: 3 92 } 93 const defaultDriftTolerance = { 94 value: 0.002, 95 minV: 0, 96 maxV: 0.02, 97 range: 0.02, 98 prec: 5 99 } 100 const defaultOrderPersistence = { 101 value: 20, 102 minV: 0, 103 maxV: 40, // 10 minutes @ 15 second epochs 104 range: 40, 105 prec: 0 106 } 107 const defaultProfit = { 108 prec: 3, 109 value: 0.01, 110 minV: 0.001, 111 maxV: 0.1, 112 range: 0.1 - 0.001 113 } 114 const defaultLevelSpacing = { 115 prec: 3, 116 value: 0.005, 117 minV: 0.001, 118 maxV: 0.02, 119 range: 0.02 - 0.0001 120 } 121 const defaultMatchBuffer = { 122 value: 0, 123 prec: 3, 124 minV: 0, 125 maxV: 1, 126 range: 1 127 } 128 const defaultLevelsPerSide = { 129 prec: 0, 130 inc: 1, 131 value: 1, 132 minV: 1 133 } 134 const defaultLotsPerLevel = { 135 prec: 0, 136 value: 1, 137 minV: 1, 138 usdIncrement: 100 139 } 140 const defaultUSDPerSide = { 141 prec: 2 142 } 143 144 const defaultMarketMakingConfig: ConfigState = { 145 gapStrategy: GapStrategyPercentPlus, 146 sellPlacements: [], 147 buyPlacements: [], 148 driftTolerance: defaultDriftTolerance.value, 149 profit: 0.02, 150 orderPersistence: defaultOrderPersistence.value, 151 cexRebalance: true, 152 simpleArbLots: 1 153 } as any as ConfigState 154 155 const defaultBotAssetConfig: BotAssetConfig = { 156 swapFeeN: defaultSwapReserves.n, 157 orderReservesFactor: defaultOrderReserves.factor, 158 slippageBufferFactor: defaultSlippage.factor, 159 transferFactor: defaultTransfer.factor 160 } 161 162 // cexButton stores parts of a CEX selection button. 163 interface cexButton { 164 name: string 165 div: PageElement 166 tmpl: Record<string, PageElement> 167 } 168 169 /* 170 * ConfigState is an amalgamation of BotConfig, ArbMarketMakingCfg, and 171 * BasicMarketMakingCfg. ConfigState tracks the global state of the options 172 * presented on the page, with a single field for each option / control element. 173 * ConfigState is necessary because there are duplicate fields in the various 174 * config structs, and the placement types are not identical. 175 */ 176 interface ConfigState { 177 gapStrategy: string 178 profit: number 179 driftTolerance: number 180 orderPersistence: number // epochs 181 cexRebalance: boolean 182 disabled: boolean 183 buyPlacements: OrderPlacement[] 184 sellPlacements: OrderPlacement[] 185 baseOptions: Record<string, string> 186 quoteOptions: Record<string, string> 187 baseConfig: BotAssetConfig 188 quoteConfig: BotAssetConfig 189 simpleArbLots: number 190 } 191 192 interface BotSpecs { 193 host: string 194 baseID: number 195 quoteID: number 196 botType: string 197 cexName?: string 198 } 199 200 interface MarketRow { 201 tr: PageElement 202 tmpl: Record<string, PageElement> 203 host: string 204 name: string 205 baseID: number 206 quoteID: number 207 arbs: string[] 208 spot: Spot 209 } 210 211 interface UIOpts { 212 usingUSDPerSide?: boolean 213 } 214 215 export default class MarketMakerSettingsPage extends BasePage { 216 page: Record<string, PageElement> 217 forms: Forms 218 opts: UIOpts 219 newWalletForm: NewWalletForm 220 approveTokenForm: TokenApprovalForm 221 walletAddrForm: DepositAddress 222 cexConfigForm: CEXConfigurationForm 223 currentMarket: string 224 originalConfig: ConfigState 225 updatedConfig: ConfigState 226 creatingNewBot: boolean 227 marketReport: MarketReport 228 qcProfit: NumberInput 229 qcProfitSlider: MiniSlider 230 qcLevelSpacing: NumberInput 231 qcLevelSpacingSlider: MiniSlider 232 qcMatchBuffer: NumberInput 233 qcMatchBufferSlider: MiniSlider 234 qcLevelsPerSide: IncrementalInput 235 qcLotsPerLevel: IncrementalInput 236 qcUSDPerSide: IncrementalInput 237 cexBaseBalance: ExchangeBalance 238 cexQuoteBalance: ExchangeBalance 239 specs: BotSpecs 240 mktID: string 241 formSpecs: BotSpecs 242 formCexes: Record<string, cexButton> 243 placementsCache: Record<string, [OrderPlacement[], OrderPlacement[]]> 244 botTypeSelectors: PageElement[] 245 marketRows: MarketRow[] 246 lotsPerLevelIncrement: number 247 placementsChart: PlacementsChart 248 basePane: AssetPane 249 quotePane: AssetPane 250 driftTolerance: NumberInput 251 driftToleranceSlider: MiniSlider 252 orderPersistence: NumberInput 253 orderPersistenceSlider: MiniSlider 254 255 constructor (main: HTMLElement, specs: BotSpecs) { 256 super() 257 258 this.placementsCache = {} 259 this.opts = {} 260 261 const page = this.page = Doc.idDescendants(main) 262 263 this.forms = new Forms(page.forms, { 264 closed: () => { 265 if (!this.specs?.host || !this.specs?.botType) app().loadPage('mm') 266 } 267 }) 268 269 this.placementsChart = new PlacementsChart(page.placementsChart) 270 this.approveTokenForm = new TokenApprovalForm(page.approveTokenForm, () => { this.submitBotType() }) 271 this.walletAddrForm = new DepositAddress(page.walletAddrForm) 272 this.cexConfigForm = new CEXConfigurationForm(page.cexConfigForm, (cexName: string) => this.cexConfigured(cexName)) 273 page.quotePane = page.basePane.cloneNode(true) as PageElement 274 page.assetPaneBox.appendChild(page.quotePane) 275 this.basePane = new AssetPane(this, page.basePane) 276 this.quotePane = new AssetPane(this, page.quotePane) 277 278 app().headerSpace.appendChild(page.mmTitle) 279 280 setOptionTemplates(page) 281 Doc.cleanTemplates( 282 page.orderOptTmpl, page.booleanOptTmpl, page.rangeOptTmpl, page.placementRowTmpl, 283 page.oracleTmpl, page.cexOptTmpl, page.arbBttnTmpl, page.marketRowTmpl, page.needRegTmpl 284 ) 285 page.basePane.removeAttribute('id') // don't remove from layout 286 287 Doc.bind(page.resetButton, 'click', () => { this.setOriginalValues() }) 288 Doc.bind(page.updateButton, 'click', () => { this.saveSettings() }) 289 Doc.bind(page.createButton, 'click', async () => { this.saveSettings() }) 290 Doc.bind(page.deleteBttn, 'click', () => { this.delete() }) 291 bindForm(page.botTypeForm, page.botTypeSubmit, () => { this.submitBotType() }) 292 Doc.bind(page.noMarketBttn, 'click', () => { this.showMarketSelectForm() }) 293 Doc.bind(page.botTypeHeader, 'click', () => { this.reshowBotTypeForm() }) 294 Doc.bind(page.botTypeChangeMarket, 'click', () => { this.showMarketSelectForm() }) 295 Doc.bind(page.marketHeader, 'click', () => { this.showMarketSelectForm() }) 296 Doc.bind(page.marketFilterInput, 'input', () => { this.sortMarketRows() }) 297 Doc.bind(page.cexRebalanceCheckbox, 'change', () => { this.autoRebalanceChanged() }) 298 Doc.bind(page.switchToAdvanced, 'click', () => { this.showAdvancedConfig() }) 299 Doc.bind(page.switchToQuickConfig, 'click', () => { this.switchToQuickConfig() }) 300 Doc.bind(page.qcMatchBuffer, 'change', () => { this.matchBufferChanged() }) 301 Doc.bind(page.switchToUSDPerSide, 'click', () => { this.changeSideCommitmentDialog() }) 302 Doc.bind(page.switchToLotsPerLevel, 'click', () => { this.changeSideCommitmentDialog() }) 303 // Gap Strategy 304 Doc.bind(page.gapStrategySelect, 'change', () => { 305 if (!page.gapStrategySelect.value) return 306 const gapStrategy = page.gapStrategySelect.value 307 this.clearPlacements(this.updatedConfig.gapStrategy) 308 this.loadCachedPlacements(gapStrategy) 309 this.updatedConfig.gapStrategy = gapStrategy 310 this.setGapFactorLabels(gapStrategy) 311 this.updateModifiedMarkers() 312 }) 313 314 // Buy/Sell placements 315 Doc.bind(page.addBuyPlacementBtn, 'click', () => { 316 this.addPlacement(true, null) 317 page.addBuyPlacementLots.value = '' 318 page.addBuyPlacementGapFactor.value = '' 319 this.updateModifiedMarkers() 320 this.placementsChart.render() 321 this.updateAllocations() 322 }) 323 Doc.bind(page.addSellPlacementBtn, 'click', () => { 324 this.addPlacement(false, null) 325 page.addSellPlacementLots.value = '' 326 page.addSellPlacementGapFactor.value = '' 327 this.updateModifiedMarkers() 328 this.placementsChart.render() 329 this.updateAllocations() 330 }) 331 332 this.driftTolerance = new NumberInput(page.driftToleranceInput, { 333 prec: defaultDriftTolerance.prec - 2, // converting to percent for display 334 sigFigs: true, 335 min: 0, 336 changed: (rawV: number) => { 337 const { minV, range, prec } = defaultDriftTolerance 338 const [v] = toFourSigFigs(rawV / 100, prec) 339 this.driftToleranceSlider.setValue((v - minV) / range) 340 this.updatedConfig.driftTolerance = v 341 } 342 }) 343 344 this.driftToleranceSlider = new MiniSlider(page.driftToleranceSlider, (r: number) => { 345 const { minV, range, prec } = defaultDriftTolerance 346 const [v] = toFourSigFigs(minV + r * range, prec) 347 this.updatedConfig.driftTolerance = v 348 this.driftTolerance.setValue(v * 100) 349 }) 350 351 this.orderPersistence = new NumberInput(page.orderPersistence, { 352 changed: (v: number) => { 353 const { minV, range } = defaultOrderPersistence 354 this.updatedConfig.orderPersistence = v 355 this.orderPersistenceSlider.setValue((v - minV) / range) 356 } 357 }) 358 359 this.orderPersistenceSlider = new MiniSlider(page.orderPersistenceSlider, (r: number) => { 360 const { minV, range, prec } = defaultOrderPersistence 361 const rawV = minV + r * range 362 const [v] = toPrecision(rawV, prec) 363 this.updatedConfig.orderPersistence = v 364 this.orderPersistence.setValue(v) 365 }) 366 367 this.qcProfit = new NumberInput(page.qcProfit, { 368 prec: defaultProfit.prec - 2, // converting to percent 369 sigFigs: true, 370 min: defaultProfit.minV * 100, 371 changed: (vPct: number) => { 372 const { minV, range } = defaultProfit 373 const v = vPct / 100 374 this.updatedConfig.profit = v 375 page.profitInput.value = this.qcProfit.input.value 376 this.qcProfitSlider.setValue((v - minV) / range) 377 this.quickConfigUpdated() 378 } 379 }) 380 381 this.qcProfitSlider = new MiniSlider(page.qcProfitSlider, (r: number) => { 382 const { minV, range, prec } = defaultProfit 383 const [v] = toFourSigFigs((minV + r * range) * 100, prec) 384 this.updatedConfig.profit = v / 100 385 this.qcProfit.setValue(v) 386 page.profitInput.value = this.qcProfit.input.value 387 this.quickConfigUpdated() 388 }) 389 390 this.qcLevelSpacing = new NumberInput(page.qcLevelSpacing, { 391 prec: defaultLevelSpacing.prec - 2, // converting to percent 392 sigFigs: true, 393 min: defaultLevelSpacing.minV * 100, 394 changed: (vPct: number) => { 395 const { minV, range } = defaultLevelSpacing 396 this.qcLevelSpacingSlider.setValue((vPct / 100 - minV) / range) 397 this.quickConfigUpdated() 398 } 399 }) 400 401 this.qcLevelSpacingSlider = new MiniSlider(page.qcLevelSpacingSlider, (r: number) => { 402 const { minV, range } = defaultLevelSpacing 403 this.qcLevelSpacing.setValue(minV + r * range * 100) 404 this.quickConfigUpdated() 405 }) 406 407 this.qcMatchBuffer = new NumberInput(page.qcMatchBuffer, { 408 prec: defaultMatchBuffer.prec - 2, // converting to percent 409 sigFigs: true, 410 min: defaultMatchBuffer.minV * 100, 411 changed: (vPct: number) => { 412 const { minV, range } = defaultMatchBuffer 413 this.qcMatchBufferSlider.setValue((vPct / 100 - minV) / range) 414 this.quickConfigUpdated() 415 } 416 }) 417 418 this.qcMatchBufferSlider = new MiniSlider(page.qcMatchBufferSlider, (r: number) => { 419 const { minV, range } = defaultMatchBuffer 420 this.qcMatchBuffer.setValue(minV + r * range * 100) 421 this.quickConfigUpdated() 422 }) 423 424 this.qcLevelsPerSide = new IncrementalInput(page.qcLevelsPerSide, { 425 prec: defaultLevelsPerSide.prec, 426 min: defaultLevelsPerSide.minV, 427 inc: defaultLevelsPerSide.inc, 428 changed: (v: number) => { 429 this.qcUSDPerSide.setValue(this.lotSizeUSD() * v * this.qcLotsPerLevel.value()) 430 this.quickConfigUpdated() 431 } 432 }) 433 434 this.qcLotsPerLevel = new IncrementalInput(page.qcLotsPerLevel, { 435 prec: defaultLotsPerLevel.prec, 436 min: defaultLotsPerLevel.minV, 437 inc: 1, // set showQuickConfig 438 changed: (v: number) => { 439 this.qcUSDPerSide.setValue(this.lotSizeUSD() * v * this.qcLevelsPerSide.value()) 440 page.qcUSDPerSideEcho.textContent = this.qcUSDPerSide.input.value as string 441 this.quickConfigUpdated() 442 }, 443 set: (v: number) => { 444 const [, s] = toFourSigFigs(v * this.qcLevelsPerSide.value() * this.lotSizeUSD(), 2) 445 page.qcUSDPerSideEcho.textContent = s 446 page.qcLotsPerLevelEcho.textContent = s 447 } 448 }) 449 450 this.qcUSDPerSide = new IncrementalInput(page.qcUSDPerSide, { 451 prec: defaultUSDPerSide.prec, 452 min: 1, // changed by showQuickConfig 453 inc: 1, // changed by showQuickConfig 454 changed: (v: number) => { 455 this.qcLotsPerLevel.setValue(v / this.qcLevelsPerSide.value() / this.lotSizeUSD()) 456 page.qcLotsPerLevelEcho.textContent = this.qcLotsPerLevel.input.value as string 457 this.quickConfigUpdated() 458 }, 459 set: (v: number, s: string) => { 460 page.qcUSDPerSideEcho.textContent = s 461 page.qcLotsPerLevelEcho.textContent = String(Math.round(v / this.lotSizeUSD())) 462 } 463 }) 464 465 const maybeSubmitBuyRow = (e: KeyboardEvent) => { 466 if (e.key !== 'Enter') return 467 if ( 468 !isNaN(parseFloat(page.addBuyPlacementGapFactor.value || '')) && 469 !isNaN(parseFloat(page.addBuyPlacementLots.value || '')) 470 ) { 471 page.addBuyPlacementBtn.click() 472 } 473 } 474 Doc.bind(page.addBuyPlacementGapFactor, 'keyup', (e: KeyboardEvent) => { maybeSubmitBuyRow(e) }) 475 Doc.bind(page.addBuyPlacementLots, 'keyup', (e: KeyboardEvent) => { maybeSubmitBuyRow(e) }) 476 477 const maybeSubmitSellRow = (e: KeyboardEvent) => { 478 if (e.key !== 'Enter') return 479 if ( 480 !isNaN(parseFloat(page.addSellPlacementGapFactor.value || '')) && 481 !isNaN(parseFloat(page.addSellPlacementLots.value || '')) 482 ) { 483 page.addSellPlacementBtn.click() 484 } 485 } 486 Doc.bind(page.addSellPlacementGapFactor, 'keyup', (e: KeyboardEvent) => { maybeSubmitSellRow(e) }) 487 Doc.bind(page.addSellPlacementLots, 'keyup', (e: KeyboardEvent) => { maybeSubmitSellRow(e) }) 488 489 Doc.bind(page.profitInput, 'change', () => { 490 Doc.hide(page.profitInputErr) 491 const showError = (errID: string) => { 492 Doc.show(page.profitInputErr) 493 page.profitInputErr.textContent = intl.prep(errID) 494 } 495 const profit = parseFloat(page.profitInput.value || '') / 100 496 if (isNaN(profit)) return showError(intl.ID_INVALID_VALUE) 497 if (profit === 0) return showError(intl.ID_NO_ZERO) 498 this.updatedConfig.profit = profit 499 this.updateModifiedMarkers() 500 }) 501 502 this.botTypeSelectors = Doc.applySelector(page.botTypeForm, '[data-bot-type]') 503 for (const div of this.botTypeSelectors) { 504 Doc.bind(div, 'click', () => { 505 if (div.classList.contains('disabled')) return 506 Doc.hide(page.botTypeErr) 507 page.cexSelection.classList.toggle('disabled', div.dataset.botType === botTypeBasicMM) 508 this.setBotTypeSelected(div.dataset.botType as string) 509 }) 510 } 511 512 this.newWalletForm = new NewWalletForm( 513 page.newWalletForm, 514 async () => { 515 await app().fetchUser() 516 this.submitBotType() 517 } 518 ) 519 520 app().registerNoteFeeder({ 521 balance: (note: BalanceNote) => { this.handleBalanceNote(note) } 522 }) 523 524 this.initialize(specs) 525 } 526 527 unload () { 528 this.forms.exit() 529 } 530 531 async initialize (specs?: BotSpecs) { 532 this.setupCEXes() 533 this.initializeMarketRows() 534 535 const isRefresh = specs && Object.keys(specs).length === 0 536 if (isRefresh) specs = State.fetchLocal(specLK) 537 if (!specs || !app().walletMap[specs.baseID] || !app().walletMap[specs.quoteID]) { 538 this.showMarketSelectForm() 539 return 540 } 541 542 // If we have specs specifying only a market, make sure the cex name and 543 // bot type are set. 544 if (specs && !specs.botType) { 545 const botCfg = liveBotConfig(specs.host, specs.baseID, specs.quoteID) 546 specs.cexName = botCfg?.cexName ?? '' 547 specs.botType = botTypeBasicMM 548 if (botCfg?.arbMarketMakingConfig) specs.botType = botTypeArbMM 549 else if (botCfg?.simpleArbConfig) specs.botType = botTypeBasicArb 550 } 551 552 // Must be a reconfig. 553 this.specs = specs 554 await this.fetchCEXBalances(specs) 555 this.configureUI() 556 } 557 558 async configureUI () { 559 const { page, specs } = this 560 const { host, baseID, quoteID, cexName, botType } = specs 561 562 const [{ symbol: baseSymbol, token: baseToken }, { symbol: quoteSymbol, token: quoteToken }] = [app().assets[baseID], app().assets[quoteID]] 563 this.mktID = `${baseSymbol}_${quoteSymbol}` 564 Doc.hide( 565 page.botSettingsContainer, page.marketBox, page.updateButton, page.resetButton, 566 page.createButton, page.noMarket, page.missingFiatRates 567 ) 568 569 if ([baseID, quoteID, baseToken?.parentID ?? baseID, quoteToken?.parentID ?? quoteID].some((assetID: number) => !app().fiatRatesMap[assetID])) { 570 Doc.show(page.missingFiatRates) 571 return 572 } 573 574 Doc.show(page.marketLoading) 575 State.storeLocal(specLK, specs) 576 577 const mmStatus = app().mmStatus 578 const viewOnly = isViewOnly(specs, mmStatus) 579 let botCfg = liveBotConfig(host, baseID, quoteID) 580 if (botCfg) { 581 const oldBotType = botCfg.arbMarketMakingConfig ? botTypeArbMM : botCfg.basicMarketMakingConfig ? botTypeBasicMM : botTypeBasicArb 582 if (oldBotType !== botType) botCfg = undefined 583 } 584 Doc.setVis(botCfg, page.deleteBttnBox) 585 586 const oldCfg = this.originalConfig = Object.assign({}, defaultMarketMakingConfig, { 587 disabled: viewOnly, 588 baseOptions: this.defaultWalletOptions(baseID), 589 quoteOptions: this.defaultWalletOptions(quoteID), 590 buyPlacements: [], 591 sellPlacements: [], 592 baseConfig: Object.assign({}, defaultBotAssetConfig), 593 quoteConfig: Object.assign({}, defaultBotAssetConfig) 594 }) as ConfigState 595 596 if (botCfg) { 597 const { basicMarketMakingConfig: mmCfg, arbMarketMakingConfig: arbMMCfg, simpleArbConfig: arbCfg, uiConfig: { cexRebalance } } = botCfg 598 this.creatingNewBot = false 599 // This is kinda sloppy, but we'll copy any relevant issues from the 600 // old config into the originalConfig. 601 const idx = oldCfg as { [k: string]: any } // typescript 602 for (const [k, v] of Object.entries(botCfg)) if (idx[k] !== undefined) idx[k] = v 603 604 oldCfg.baseConfig = Object.assign({}, defaultBotAssetConfig, botCfg.uiConfig.baseConfig) 605 oldCfg.quoteConfig = Object.assign({}, defaultBotAssetConfig, botCfg.uiConfig.quoteConfig) 606 oldCfg.baseOptions = botCfg.baseWalletOptions || {} 607 oldCfg.quoteOptions = botCfg.quoteWalletOptions || {} 608 oldCfg.cexRebalance = cexRebalance 609 610 if (mmCfg) { 611 oldCfg.buyPlacements = mmCfg.buyPlacements 612 oldCfg.sellPlacements = mmCfg.sellPlacements 613 oldCfg.driftTolerance = mmCfg.driftTolerance 614 oldCfg.gapStrategy = mmCfg.gapStrategy 615 } else if (arbMMCfg) { 616 const { buyPlacements, sellPlacements } = arbMMCfg 617 oldCfg.buyPlacements = Array.from(buyPlacements, (p: ArbMarketMakingPlacement) => { return { lots: p.lots, gapFactor: p.multiplier } }) 618 oldCfg.sellPlacements = Array.from(sellPlacements, (p: ArbMarketMakingPlacement) => { return { lots: p.lots, gapFactor: p.multiplier } }) 619 oldCfg.profit = arbMMCfg.profit 620 oldCfg.driftTolerance = arbMMCfg.driftTolerance 621 oldCfg.orderPersistence = arbMMCfg.orderPersistence 622 } else if (arbCfg) { 623 // TODO: expose maxActiveArbs 624 oldCfg.profit = arbCfg.profitTrigger 625 oldCfg.orderPersistence = arbCfg.numEpochsLeaveOpen 626 oldCfg.simpleArbLots = botCfg.uiConfig.simpleArbLots ?? 1 627 } 628 Doc.setVis(!viewOnly, page.updateButton, page.resetButton) 629 } else { 630 this.creatingNewBot = true 631 Doc.setVis(!viewOnly, page.createButton) 632 } 633 634 // Now that we've updated the originalConfig, we'll copy it. 635 this.updatedConfig = JSON.parse(JSON.stringify(oldCfg)) 636 637 switch (botType) { 638 case botTypeBasicMM: 639 page.botTypeDisplay.textContent = intl.prep(intl.ID_BOTTYPE_BASIC_MM) 640 break 641 case botTypeArbMM: 642 page.botTypeDisplay.textContent = intl.prep(intl.ID_BOTTYPE_ARB_MM) 643 break 644 case botTypeBasicArb: 645 page.botTypeDisplay.textContent = intl.prep(intl.ID_BOTTYPE_SIMPLE_ARB) 646 } 647 648 setMarketElements(document.body, baseID, quoteID, host) 649 Doc.setVis(botType !== botTypeBasicArb, page.driftToleranceBox, page.switchToAdvanced) 650 Doc.setVis(Boolean(cexName), ...Doc.applySelector(document.body, '[data-cex-show]')) 651 652 Doc.setVis(viewOnly, page.viewOnlyRunning) 653 Doc.setVis(cexName, page.cexRebalanceSettings) 654 if (cexName) setCexElements(document.body, cexName) 655 656 await this.fetchMarketReport() 657 658 const lotSizeUSD = this.lotSizeUSD() 659 this.lotsPerLevelIncrement = Math.round(Math.max(1, defaultLotsPerLevel.usdIncrement / lotSizeUSD)) 660 this.qcLotsPerLevel.inc = this.lotsPerLevelIncrement 661 this.qcUSDPerSide.inc = this.lotsPerLevelIncrement * lotSizeUSD 662 this.qcUSDPerSide.min = lotSizeUSD 663 664 this.basePane.setAsset(baseID, false) 665 this.quotePane.setAsset(quoteID, true) 666 const { marketReport: { baseFiatRate } } = this 667 this.placementsChart.setMarket({ cexName: cexName as string, botType, baseFiatRate, dict: this.updatedConfig }) 668 669 // If this is a new bot, show the quick config form. 670 const isQuickPlacements = !botCfg || this.isQuickPlacements(this.updatedConfig.buyPlacements, this.updatedConfig.sellPlacements) 671 const gapStrategy = botCfg?.basicMarketMakingConfig?.gapStrategy ?? GapStrategyPercentPlus 672 page.gapStrategySelect.value = gapStrategy 673 if (botType === botTypeBasicArb || (isQuickPlacements && gapStrategy === GapStrategyPercentPlus)) this.showQuickConfig() 674 else this.showAdvancedConfig() 675 676 this.setOriginalValues() 677 678 Doc.hide(page.marketLoading) 679 Doc.show(page.botSettingsContainer, page.marketBox) 680 } 681 682 initializeMarketRows () { 683 this.marketRows = [] 684 Doc.empty(this.page.marketSelect) 685 for (const { host, markets, assets, auth: { effectiveTier, pendingStrength } } of Object.values(app().exchanges)) { 686 if (effectiveTier + pendingStrength === 0) { 687 const { needRegTmpl, needRegBox } = this.page 688 const bttn = needRegTmpl.cloneNode(true) as PageElement 689 const tmpl = Doc.parseTemplate(bttn) 690 Doc.bind(bttn, 'click', () => { app().loadPage('register', { host, backTo: 'mmsettings' }) }) 691 tmpl.host.textContent = host 692 needRegBox.appendChild(bttn) 693 Doc.show(needRegBox) 694 continue 695 } 696 for (const { name, baseid: baseID, quoteid: quoteID, spot, basesymbol: baseSymbol, quotesymbol: quoteSymbol } of Object.values(markets)) { 697 if (!app().assets[baseID] || !app().assets[quoteID]) continue 698 const tr = this.page.marketRowTmpl.cloneNode(true) as PageElement 699 const tmpl = Doc.parseTemplate(tr) 700 const mr = { tr, tmpl, host: host, name, baseID, quoteID, spot: spot, arbs: [] } as MarketRow 701 this.marketRows.push(mr) 702 this.page.marketSelect.appendChild(tr) 703 tmpl.baseIcon.src = Doc.logoPath(baseSymbol) 704 tmpl.quoteIcon.src = Doc.logoPath(quoteSymbol) 705 tmpl.baseSymbol.appendChild(Doc.symbolize(assets[baseID], true)) 706 tmpl.quoteSymbol.appendChild(Doc.symbolize(assets[quoteID], true)) 707 tmpl.host.textContent = host 708 const cexHasMarket = this.cexMarketSupportFilter(baseID, quoteID) 709 for (const [cexName, dinfo] of Object.entries(CEXDisplayInfos)) { 710 if (cexHasMarket(cexName)) { 711 const img = this.page.arbBttnTmpl.cloneNode(true) as PageElement 712 img.src = dinfo.logo 713 tmpl.arbs.appendChild(img) 714 mr.arbs.push(cexName) 715 } 716 } 717 Doc.bind(tr, 'click', () => { this.showBotTypeForm(host, baseID, quoteID) }) 718 } 719 } 720 if (this.marketRows.length === 0) { 721 const { marketSelectionTable, marketFilterBox, noMarkets } = this.page 722 Doc.hide(marketSelectionTable, marketFilterBox) 723 Doc.show(noMarkets) 724 } else Doc.hide(this.page.noMarkets) 725 const fiatRates = app().fiatRatesMap 726 this.marketRows.sort((a: MarketRow, b: MarketRow) => { 727 let [volA, volB] = [a.spot?.vol24 ?? 0, b.spot?.vol24 ?? 0] 728 if (fiatRates[a.baseID] && fiatRates[b.baseID]) { 729 volA *= fiatRates[a.baseID] 730 volB *= fiatRates[b.baseID] 731 } 732 return volB - volA 733 }) 734 } 735 736 runningBotInventory (assetID: number) { 737 return runningBotInventory(assetID) 738 } 739 740 adjustedBalances (baseWallet: WalletState, quoteWallet: WalletState) { 741 const { cexBaseBalance, cexQuoteBalance } = this 742 const [bInv, qInv] = [this.runningBotInventory(baseWallet.assetID), this.runningBotInventory(quoteWallet.assetID)] 743 const [cexBaseAvail, cexQuoteAvail] = [(cexBaseBalance?.available || 0) - bInv.cex.total, (cexQuoteBalance?.available || 0) - qInv.cex.total] 744 const [dexBaseAvail, dexQuoteAvail] = [baseWallet.balance.available - bInv.dex.total, quoteWallet.balance.available - qInv.dex.total] 745 const baseAvail = dexBaseAvail + cexBaseAvail 746 const quoteAvail = dexQuoteAvail + cexQuoteAvail 747 return { baseAvail, quoteAvail, dexBaseAvail, dexQuoteAvail, cexBaseAvail, cexQuoteAvail } 748 } 749 750 lotSizeUSD () { 751 const { specs: { host, baseID }, mktID, marketReport: { baseFiatRate } } = this 752 const xc = app().exchanges[host] 753 const market = xc.markets[mktID] 754 const { lotsize: lotSize } = market 755 const { unitInfo: ui } = app().assets[baseID] 756 return lotSize / ui.conventional.conversionFactor * baseFiatRate 757 } 758 759 /* 760 * marketStuff is just a bunch of useful properties for the current specs 761 * gathered in one place and with preferable names. 762 */ 763 marketStuff () { 764 const { 765 page, specs: { host, baseID, quoteID, cexName, botType }, basePane, quotePane, 766 marketReport: { baseFiatRate, quoteFiatRate, baseFees, quoteFees }, 767 lotsPerLevelIncrement, updatedConfig: cfg, originalConfig: oldCfg, mktID 768 } = this 769 const { symbol: baseSymbol, unitInfo: bui } = app().assets[baseID] 770 const { symbol: quoteSymbol, unitInfo: qui } = app().assets[quoteID] 771 const xc = app().exchanges[host] 772 const market = xc.markets[mktID] 773 const { lotsize: lotSize, spot } = market 774 const lotSizeUSD = lotSize / bui.conventional.conversionFactor * baseFiatRate 775 const atomicRate = 1 / bui.conventional.conversionFactor * baseFiatRate / quoteFiatRate * qui.conventional.conversionFactor 776 const xcRate = { 777 conv: quoteFiatRate / baseFiatRate, 778 atomic: atomicRate, 779 msg: Math.round(atomicRate * OrderUtil.RateEncodingFactor), // unadjusted 780 spot 781 } 782 783 let [dexBaseLots, dexQuoteLots] = [cfg.simpleArbLots, cfg.simpleArbLots] 784 if (botType !== botTypeBasicArb) { 785 dexBaseLots = this.updatedConfig.sellPlacements.reduce((lots: number, p: OrderPlacement) => lots + p.lots, 0) 786 dexQuoteLots = this.updatedConfig.buyPlacements.reduce((lots: number, p: OrderPlacement) => lots + p.lots, 0) 787 } 788 const quoteLot = calculateQuoteLot(lotSize, baseID, quoteID, spot) 789 const walletStuff = this.walletStuff() 790 const { baseFeeAssetID, quoteFeeAssetID, baseIsAccountLocker, quoteIsAccountLocker } = walletStuff 791 792 const { commit, fees } = feesAndCommit( 793 baseID, quoteID, baseFees, quoteFees, lotSize, dexBaseLots, dexQuoteLots, 794 baseFeeAssetID, quoteFeeAssetID, baseIsAccountLocker, quoteIsAccountLocker, 795 cfg.baseConfig.orderReservesFactor, cfg.quoteConfig.orderReservesFactor 796 ) 797 798 return { 799 page, cfg, oldCfg, host, xc, baseID, quoteID, botType, cexName, baseFiatRate, quoteFiatRate, 800 xcRate, baseSymbol, quoteSymbol, mktID, lotSize, lotSizeUSD, lotsPerLevelIncrement, 801 quoteLot, commit, basePane, quotePane, fees, ...walletStuff 802 } 803 } 804 805 walletStuff () { 806 const { specs: { baseID, quoteID } } = this 807 const [baseWallet, quoteWallet] = [app().walletMap[baseID], app().walletMap[quoteID]] 808 const [{ token: baseToken, unitInfo: bui }, { token: quoteToken, unitInfo: qui }] = [app().assets[baseID], app().assets[quoteID]] 809 const baseFeeAssetID = baseToken ? baseToken.parentID : baseID 810 const quoteFeeAssetID = quoteToken ? quoteToken.parentID : quoteID 811 const [baseFeeUI, quoteFeeUI] = [app().assets[baseFeeAssetID].unitInfo, app().assets[quoteFeeAssetID].unitInfo] 812 const traitAccountLocker = 1 << 14 813 const baseIsAccountLocker = (baseWallet.traits & traitAccountLocker) > 0 814 const quoteIsAccountLocker = (quoteWallet.traits & traitAccountLocker) > 0 815 return { 816 baseWallet, quoteWallet, baseFeeUI, quoteFeeUI, baseToken, quoteToken, 817 bui, qui, baseFeeAssetID, quoteFeeAssetID, baseIsAccountLocker, quoteIsAccountLocker, 818 ...this.adjustedBalances(baseWallet, quoteWallet) 819 } 820 } 821 822 showAdvancedConfig () { 823 const { page } = this 824 Doc.show(page.advancedConfig) 825 Doc.hide(page.quickConfig) 826 this.placementsChart.render() 827 } 828 829 isQuickPlacements (buyPlacements: OrderPlacement[], sellPlacements: OrderPlacement[]) { 830 if (buyPlacements.length === 0 || buyPlacements.length !== sellPlacements.length) return false 831 for (let i = 0; i < buyPlacements.length; i++) { 832 if (buyPlacements[i].gapFactor !== sellPlacements[i].gapFactor) return false 833 if (buyPlacements[i].lots !== sellPlacements[i].lots) return false 834 } 835 return true 836 } 837 838 switchToQuickConfig () { 839 const { cfg, botType, lotSizeUSD } = this.marketStuff() 840 const { buyPlacements: buys, sellPlacements: sells } = cfg 841 // If we have both buys and sells, get the best approximation quick config 842 // approximation. 843 if (buys.length > 0 && sells.length > 0) { 844 const bestBuy = buys.reduce((prev: OrderPlacement, curr: OrderPlacement) => curr.gapFactor < prev.gapFactor ? curr : prev) 845 const bestSell = sells.reduce((prev: OrderPlacement, curr: OrderPlacement) => curr.gapFactor < prev.gapFactor ? curr : prev) 846 const placementCount = buys.length + sells.length 847 const levelsPerSide = Math.max(1, Math.floor((placementCount) / 2)) 848 if (botType === botTypeBasicMM) { 849 cfg.profit = (bestBuy.gapFactor + bestSell.gapFactor) / 2 850 const worstBuy = buys.reduce((prev: OrderPlacement, curr: OrderPlacement) => curr.gapFactor > prev.gapFactor ? curr : prev) 851 const worstSell = sells.reduce((prev: OrderPlacement, curr: OrderPlacement) => curr.gapFactor > prev.gapFactor ? curr : prev) 852 const range = ((worstBuy.gapFactor - bestBuy.gapFactor) + (worstSell.gapFactor - bestSell.gapFactor)) / 2 853 const inc = range / (levelsPerSide - 1) 854 this.qcProfit.setValue(cfg.profit * 100) 855 this.qcProfitSlider.setValue((cfg.profit - defaultProfit.minV) / defaultProfit.range) 856 this.qcLevelSpacing.setValue(inc * 100) 857 this.qcLevelSpacingSlider.setValue((inc - defaultLevelSpacing.minV) / defaultLevelSpacing.range) 858 } else if (botType === botTypeArbMM) { 859 const multSum = buys.reduce((v: number, p: OrderPlacement) => v + p.gapFactor, 0) + sells.reduce((v: number, p: OrderPlacement) => v + p.gapFactor, 0) 860 const buffer = ((multSum / placementCount) - 1) || defaultMatchBuffer.value 861 this.qcMatchBuffer.setValue(buffer * 100) 862 this.qcMatchBufferSlider.setValue((buffer - defaultMatchBuffer.minV) / defaultMatchBuffer.range) 863 } 864 const lots = buys.reduce((v: number, p: OrderPlacement) => v + p.lots, 0) + sells.reduce((v: number, p: OrderPlacement) => v + p.lots, 0) 865 const lotsPerLevel = Math.max(1, Math.round(lots / 2 / levelsPerSide)) 866 this.qcLotsPerLevel.setValue(lotsPerLevel) 867 this.qcUSDPerSide.setValue(lotsPerLevel * levelsPerSide * lotSizeUSD) 868 this.qcLevelsPerSide.setValue(levelsPerSide) 869 } else if (botType === botTypeBasicArb) { 870 this.qcLotsPerLevel.setValue(cfg.simpleArbLots) 871 } 872 this.showQuickConfig() 873 this.quickConfigUpdated() 874 } 875 876 showQuickConfig () { 877 const { page, lotSizeUSD, botType, lotsPerLevelIncrement } = this.marketStuff() 878 879 if (!this.qcLevelsPerSide.input.value) { 880 this.qcLevelsPerSide.setValue(defaultLevelsPerSide.value) 881 this.qcUSDPerSide.setValue(defaultLevelsPerSide.value * (this.qcLotsPerLevel.value() || lotsPerLevelIncrement) * lotSizeUSD) 882 } 883 if (!this.qcLotsPerLevel.input.value) { 884 this.qcLotsPerLevel.setValue(lotsPerLevelIncrement) 885 this.qcUSDPerSide.setValue(lotSizeUSD * lotsPerLevelIncrement * this.qcLevelsPerSide.value()) 886 } 887 if (!page.qcLevelSpacing.value) { 888 this.qcLevelSpacing.setValue(defaultLevelSpacing.value * 100) 889 this.qcLevelSpacingSlider.setValue((defaultLevelSpacing.value - defaultLevelSpacing.minV) / defaultLevelSpacing.range) 890 } 891 if (!page.qcMatchBuffer.value) page.qcMatchBuffer.value = String(defaultMatchBuffer.value * 100) 892 893 Doc.hide(page.advancedConfig) 894 Doc.show(page.quickConfig) 895 896 this.showInputsForBot(botType) 897 } 898 899 showInputsForBot (botType: string) { 900 const { page, opts: { usingUSDPerSide } } = this 901 Doc.hide( 902 page.matchMultiplierBox, page.placementsChartBox, page.placementChartLegend, 903 page.lotsPerLevelLabel, page.levelSpacingBox, page.arbLotsLabel, page.qcLevelPerSideBox 904 ) 905 Doc.setVis(usingUSDPerSide, page.qcUSDPerSideBox) 906 Doc.setVis(!usingUSDPerSide, page.qcLotsBox) 907 switch (botType) { 908 case botTypeArbMM: 909 Doc.show( 910 page.qcLevelPerSideBox, page.matchMultiplierBox, page.placementsChartBox, 911 page.placementChartLegend, page.lotsPerLevelLabel 912 ) 913 break 914 case botTypeBasicMM: 915 Doc.show( 916 page.qcLevelPerSideBox, page.levelSpacingBox, page.placementsChartBox, 917 page.lotsPerLevelLabel 918 ) 919 break 920 case botTypeBasicArb: 921 Doc.show(page.arbLotsLabel) 922 } 923 } 924 925 quickConfigUpdated () { 926 const { page, cfg, botType, cexName } = this.marketStuff() 927 928 Doc.hide(page.qcError) 929 const setError = (msg: string) => { 930 page.qcError.textContent = msg 931 Doc.show(page.qcError) 932 } 933 934 const levelsPerSide = botType === botTypeBasicArb ? 1 : this.qcLevelsPerSide.value() 935 if (isNaN(levelsPerSide)) { 936 setError('invalid value for levels per side') 937 } 938 939 const lotsPerLevel = this.qcLotsPerLevel.value() 940 if (isNaN(lotsPerLevel)) { 941 setError('invalid value for lots per level') 942 } 943 944 const profit = parseFloat(page.qcProfit.value ?? '') / 100 945 if (isNaN(profit)) { 946 setError('invalid value for profit') 947 } 948 949 const levelSpacing = botType === botTypeBasicMM ? parseFloat(page.qcLevelSpacing.value ?? '') / 100 : 0 950 if (isNaN(levelSpacing)) { 951 setError('invalid value for level spacing') 952 } 953 954 const matchBuffer = botType === botTypeArbMM ? parseFloat(page.qcMatchBuffer.value ?? '') / 100 : 0 955 if (isNaN(matchBuffer)) { 956 setError('invalid value for match buffer') 957 } 958 const multiplier = matchBuffer + 1 959 960 const levelSpacingDisabled = levelsPerSide === 1 961 page.levelSpacingBox.classList.toggle('disabled', levelSpacingDisabled) 962 page.qcLevelSpacing.disabled = levelSpacingDisabled 963 cfg.simpleArbLots = lotsPerLevel 964 965 if (botType !== botTypeBasicArb) { 966 this.clearPlacements(cexName ? arbMMRowCacheKey : cfg.gapStrategy) 967 for (let levelN = 0; levelN < levelsPerSide; levelN++) { 968 const placement = { lots: lotsPerLevel } as OrderPlacement 969 placement.gapFactor = botType === botTypeBasicMM ? profit + levelSpacing * levelN : multiplier 970 cfg.buyPlacements.push(placement) 971 cfg.sellPlacements.push(placement) 972 // Add rows in the advanced config table. 973 this.addPlacement(true, placement) 974 this.addPlacement(false, placement) 975 } 976 977 this.placementsChart.render() 978 } 979 980 this.updateAllocations() 981 } 982 983 updateAllocations () { 984 this.updateBaseAllocations() 985 this.updateQuoteAllocations() 986 } 987 988 updateBaseAllocations () { 989 const { commit, lotSize, basePane, fees } = this.marketStuff() 990 991 basePane.updateInventory(commit.dex.base.lots, commit.dex.quote.lots, lotSize, commit.dex.base.val, commit.cex.base.val, fees.base) 992 basePane.updateCommitTotal() 993 } 994 995 updateQuoteAllocations () { 996 const { commit, quoteLot: lotSize, quotePane, fees } = this.marketStuff() 997 998 quotePane.updateInventory(commit.dex.quote.lots, commit.dex.base.lots, lotSize, commit.dex.quote.val, commit.cex.quote.val, fees.quote) 999 quotePane.updateCommitTotal() 1000 } 1001 1002 matchBufferChanged () { 1003 const { page } = this 1004 page.qcMatchBuffer.value = Math.max(0, parseFloat(page.qcMatchBuffer.value ?? '') || defaultMatchBuffer.value * 100).toFixed(2) 1005 this.quickConfigUpdated() 1006 } 1007 1008 showAddress (assetID: number) { 1009 this.walletAddrForm.setAsset(assetID) 1010 this.forms.show(this.page.walletAddrForm) 1011 } 1012 1013 changeSideCommitmentDialog () { 1014 const { page, opts } = this 1015 opts.usingUSDPerSide = !opts.usingUSDPerSide 1016 Doc.setVis(opts.usingUSDPerSide, page.qcUSDPerSideBox) 1017 Doc.setVis(!opts.usingUSDPerSide, page.qcLotsBox) 1018 } 1019 1020 async showBotTypeForm (host: string, baseID: number, quoteID: number, botType?: string, configuredCEX?: string) { 1021 const { page } = this 1022 this.formSpecs = { host, baseID, quoteID, botType: '' } 1023 const viewOnly = isViewOnly(this.formSpecs, app().mmStatus) 1024 if (viewOnly) { 1025 const botCfg = liveBotConfig(host, baseID, quoteID) 1026 const specs = this.specs = this.formSpecs 1027 switch (true) { 1028 case Boolean(botCfg?.simpleArbConfig): 1029 specs.botType = botTypeBasicArb 1030 break 1031 case Boolean(botCfg?.arbMarketMakingConfig): 1032 specs.botType = botTypeArbMM 1033 break 1034 default: 1035 specs.botType = botTypeBasicMM 1036 } 1037 specs.cexName = botCfg?.cexName 1038 await this.fetchCEXBalances(this.formSpecs) 1039 await this.configureUI() 1040 this.forms.close() 1041 return 1042 } 1043 setMarketElements(page.botTypeForm, baseID, quoteID, host) 1044 Doc.empty(page.botTypeBaseSymbol, page.botTypeQuoteSymbol) 1045 const [b, q] = [app().assets[baseID], app().assets[quoteID]] 1046 page.botTypeBaseSymbol.appendChild(Doc.symbolize(b, true)) 1047 page.botTypeQuoteSymbol.appendChild(Doc.symbolize(q, true)) 1048 for (const div of this.botTypeSelectors) div.classList.remove('selected') 1049 for (const { div } of Object.values(this.formCexes)) div.classList.remove('selected') 1050 this.setCEXAvailability(baseID, quoteID) 1051 Doc.hide(page.noCexesConfigured, page.noCexMarket, page.noCexMarketConfigureMore, page.botTypeErr) 1052 const cexHasMarket = this.cexMarketSupportFilter(baseID, quoteID) 1053 const supportingCexes: Record<string, CEXConfig> = {} 1054 for (const cex of Object.values(app().mmStatus.cexes)) { 1055 if (cexHasMarket(cex.config.name)) supportingCexes[cex.config.name] = cex.config 1056 } 1057 const nCexes = Object.keys(supportingCexes).length 1058 const arbEnabled = nCexes > 0 1059 for (const div of this.botTypeSelectors) div.classList.toggle('disabled', div.dataset.botType !== botTypeBasicMM && !arbEnabled) 1060 if (Object.keys(app().mmStatus.cexes).length === 0) { 1061 Doc.show(page.noCexesConfigured) 1062 this.setBotTypeSelected(botTypeBasicMM) 1063 } else { 1064 const lastBots = (State.fetchLocal(lastBotsLK) || {}) as Record<string, BotSpecs> 1065 const lastBot = lastBots[`${baseID}_${quoteID}_${host}`] 1066 let cex: CEXConfig | undefined 1067 botType = botType ?? (lastBot ? lastBot.botType : botTypeArbMM) 1068 if (botType !== botTypeBasicMM) { 1069 // Four ways to auto-select a cex. 1070 // 1. Coming back from the cex configuration form. 1071 if (configuredCEX) cex = supportingCexes[configuredCEX] 1072 // 2. We have a saved configuration. 1073 if (!cex && lastBot) cex = supportingCexes[lastBot.cexName ?? ''] 1074 // 3. The last exchange that the user selected. 1075 if (!cex) { 1076 const lastCEX = State.fetchLocal(lastArbExchangeLK) 1077 if (lastCEX) cex = supportingCexes[lastCEX] 1078 } 1079 // 4. Any supporting cex. 1080 if (!cex && nCexes > 0) cex = Object.values(supportingCexes)[0] 1081 } 1082 if (cex) { 1083 page.cexSelection.classList.remove('disabled') 1084 this.setBotTypeSelected(botType ?? (lastBot ? lastBot.botType : botTypeArbMM)) 1085 this.selectFormCEX(cex.name) 1086 } else { 1087 page.cexSelection.classList.add('disabled') 1088 Doc.show(page.noCexMarket) 1089 this.setBotTypeSelected(botTypeBasicMM) 1090 // If there are unconfigured cexes, show configureMore message. 1091 const unconfigured = Object.keys(CEXDisplayInfos).filter((cexName: string) => !app().mmStatus.cexes[cexName]) 1092 const allConfigured = unconfigured.length === 0 || (unconfigured.length === 1 && (unconfigured[0] === 'Binance' || unconfigured[0] === 'BinanceUS')) 1093 if (!allConfigured) Doc.show(page.noCexMarketConfigureMore) 1094 } 1095 } 1096 1097 Doc.show(page.cexSelection) 1098 // Check if we have any cexes configured. 1099 this.forms.show(page.botTypeForm) 1100 } 1101 1102 reshowBotTypeForm () { 1103 if (isViewOnly(this.specs, app().mmStatus)) this.showMarketSelectForm() 1104 const { baseID, quoteID, host, cexName, botType } = this.specs 1105 this.showBotTypeForm(host, baseID, quoteID, botType, cexName) 1106 } 1107 1108 setBotTypeSelected (selectedType: string) { 1109 const { formSpecs: { baseID, quoteID, host }, botTypeSelectors, formCexes } = this 1110 for (const { classList, dataset: { botType } } of botTypeSelectors) classList.toggle('selected', botType === selectedType) 1111 // If we don't have a cex selected, attempt to select one 1112 if (selectedType === botTypeBasicMM) return 1113 const mmStatus = app().mmStatus 1114 if (Object.keys(mmStatus.cexes).length === 0) return 1115 const cexHasMarket = this.cexMarketSupportFilter(baseID, quoteID) 1116 // If there is one currently selected and it supports this market, leave it. 1117 const selecteds = Object.values(formCexes).filter((cex: cexButton) => cex.div.classList.contains('selected')) 1118 if (selecteds.length && cexHasMarket(selecteds[0].name)) return 1119 // See if we have a saved configuration. 1120 const lastBots = (State.fetchLocal(lastBotsLK) || {}) as Record<string, BotSpecs> 1121 const lastBot = lastBots[`${baseID}_${quoteID}_${host}`] 1122 if (lastBot) { 1123 const cex = mmStatus.cexes[lastBot.cexName ?? ''] 1124 if (cex && cexHasMarket(cex.config.name)) { 1125 this.selectFormCEX(cex.config.name) 1126 return 1127 } 1128 } 1129 // 2. The last exchange that the user selected. 1130 const lastCEX = State.fetchLocal(lastArbExchangeLK) 1131 if (lastCEX) { 1132 const cex = mmStatus.cexes[lastCEX] 1133 if (cex && cexHasMarket(cex.config.name)) { 1134 this.selectFormCEX(cex.config.name) 1135 return 1136 } 1137 } 1138 // 3. Any supporting cex. 1139 const cexes = Object.values(mmStatus.cexes).filter((cex: MMCEXStatus) => cexHasMarket(cex.config.name)) 1140 if (cexes.length) this.selectFormCEX(cexes[0].config.name) 1141 } 1142 1143 showMarketSelectForm () { 1144 this.page.marketFilterInput.value = '' 1145 this.sortMarketRows() 1146 this.forms.show(this.page.marketSelectForm) 1147 } 1148 1149 sortMarketRows () { 1150 const page = this.page 1151 const filter = page.marketFilterInput.value?.toLowerCase() 1152 Doc.empty(page.marketSelect) 1153 for (const mr of this.marketRows) { 1154 mr.tr.classList.remove('selected') 1155 if (filter && !mr.name.includes(filter)) continue 1156 page.marketSelect.appendChild(mr.tr) 1157 } 1158 } 1159 1160 handleBalanceNote (n: BalanceNote) { 1161 this.approveTokenForm.handleBalanceNote(n) 1162 if (!this.marketReport) return 1163 const { baseID, quoteID, quoteToken, baseToken } = this.marketStuff() 1164 if (n.assetID === baseID || n.assetID === baseToken?.parentID) { 1165 this.basePane.updateBalances() 1166 } else if (n.assetID === quoteID || n.assetID === quoteToken?.parentID) { 1167 this.quotePane.updateBalances() 1168 } 1169 } 1170 1171 autoRebalanceChanged () { 1172 const { page, updatedConfig: cfg } = this 1173 cfg.cexRebalance = page.cexRebalanceCheckbox?.checked ?? false 1174 this.updateAllocations() 1175 } 1176 1177 async submitBotType () { 1178 const loaded = app().loading(this.page.botTypeForm) 1179 try { 1180 await this.submitBotWithValidation() 1181 } finally { 1182 loaded() 1183 } 1184 } 1185 1186 async submitBotWithValidation () { 1187 // check for wallets 1188 const { page, forms, formSpecs: { baseID, quoteID, host } } = this 1189 1190 if (!app().walletMap[baseID]) { 1191 this.newWalletForm.setAsset(baseID) 1192 forms.show(this.page.newWalletForm) 1193 return 1194 } 1195 if (!app().walletMap[quoteID]) { 1196 this.newWalletForm.setAsset(quoteID) 1197 forms.show(this.page.newWalletForm) 1198 return 1199 } 1200 // Are tokens approved? 1201 const [bApproval, qApproval] = tokenAssetApprovalStatuses(host, app().assets[baseID], app().assets[quoteID]) 1202 if (bApproval === ApprovalStatus.NotApproved) { 1203 this.approveTokenForm.setAsset(baseID, host) 1204 forms.show(page.approveTokenForm) 1205 return 1206 } 1207 if (qApproval === ApprovalStatus.NotApproved) { 1208 this.approveTokenForm.setAsset(quoteID, host) 1209 forms.show(page.approveTokenForm) 1210 return 1211 } 1212 1213 const { botTypeSelectors } = this 1214 const selecteds = botTypeSelectors.filter((div: PageElement) => div.classList.contains('selected')) 1215 if (selecteds.length < 1) { 1216 page.botTypeErr.textContent = intl.prep(intl.ID_NO_BOTTYPE) 1217 Doc.show(page.botTypeErr) 1218 return 1219 } 1220 const botType = this.formSpecs.botType = selecteds[0].dataset.botType ?? '' 1221 if (botType !== botTypeBasicMM) { 1222 const selecteds = Object.values(this.formCexes).filter((cex: cexButton) => cex.div.classList.contains('selected')) 1223 if (selecteds.length < 1) { 1224 page.botTypeErr.textContent = intl.prep(intl.ID_NO_CEX) 1225 Doc.show(page.botTypeErr) 1226 return 1227 } 1228 const cexName = selecteds[0].name 1229 this.formSpecs.cexName = cexName 1230 await this.fetchCEXBalances(this.formSpecs) 1231 } 1232 1233 this.specs = this.formSpecs 1234 1235 this.configureUI() 1236 this.forms.close() 1237 } 1238 1239 async fetchCEXBalances (specs: BotSpecs) { 1240 const { page } = this 1241 const { baseID, quoteID, cexName, botType } = specs 1242 if (botType === botTypeBasicMM || !cexName) return 1243 1244 try { 1245 // This won't work if we implement live reconfiguration, because locked 1246 // funds would need to be considered. 1247 this.cexBaseBalance = await MM.cexBalance(cexName, baseID) 1248 } catch (e) { 1249 page.botTypeErr.textContent = intl.prep(intl.ID_CEXBALANCE_ERR, { cexName, assetID: String(baseID), err: String(e) }) 1250 Doc.show(page.botTypeErr) 1251 throw e 1252 } 1253 1254 try { 1255 this.cexQuoteBalance = await MM.cexBalance(cexName, quoteID) 1256 } catch (e) { 1257 page.botTypeErr.textContent = intl.prep(intl.ID_CEXBALANCE_ERR, { cexName, assetID: String(quoteID), err: String(e) }) 1258 Doc.show(page.botTypeErr) 1259 throw e 1260 } 1261 } 1262 1263 defaultWalletOptions (assetID: number): Record<string, string> { 1264 const walletDef = app().currentWalletDefinition(assetID) 1265 if (!walletDef.multifundingopts) { 1266 return {} 1267 } 1268 const options: Record<string, string> = {} 1269 for (const opt of walletDef.multifundingopts) { 1270 if (opt.quoteAssetOnly && assetID !== this.specs.quoteID) { 1271 continue 1272 } 1273 options[opt.key] = `${opt.default}` 1274 } 1275 return options 1276 } 1277 1278 /* 1279 * updateModifiedMarkers checks each of the input elements on the page and 1280 * if the current value does not match the original value (since the last 1281 * save), then the input will have a colored border. 1282 */ 1283 updateModifiedMarkers () { 1284 if (this.creatingNewBot) return 1285 const { page, originalConfig: oldCfg, updatedConfig: newCfg } = this 1286 1287 // Gap strategy input 1288 const gapStrategyModified = oldCfg.gapStrategy !== newCfg.gapStrategy 1289 page.gapStrategySelect.classList.toggle('modified', gapStrategyModified) 1290 1291 const profitModified = oldCfg.profit !== newCfg.profit 1292 page.profitInput.classList.toggle('modified', profitModified) 1293 1294 // Buy placements Input 1295 let buyPlacementsModified = false 1296 if (oldCfg.buyPlacements.length !== newCfg.buyPlacements.length) { 1297 buyPlacementsModified = true 1298 } else { 1299 for (let i = 0; i < oldCfg.buyPlacements.length; i++) { 1300 if (oldCfg.buyPlacements[i].lots !== newCfg.buyPlacements[i].lots || 1301 oldCfg.buyPlacements[i].gapFactor !== newCfg.buyPlacements[i].gapFactor) { 1302 buyPlacementsModified = true 1303 break 1304 } 1305 } 1306 } 1307 page.buyPlacementsTableWrapper.classList.toggle('modified', buyPlacementsModified) 1308 1309 // Sell placements input 1310 let sellPlacementsModified = false 1311 if (oldCfg.sellPlacements.length !== newCfg.sellPlacements.length) { 1312 sellPlacementsModified = true 1313 } else { 1314 for (let i = 0; i < oldCfg.sellPlacements.length; i++) { 1315 if (oldCfg.sellPlacements[i].lots !== newCfg.sellPlacements[i].lots || 1316 oldCfg.sellPlacements[i].gapFactor !== newCfg.sellPlacements[i].gapFactor) { 1317 sellPlacementsModified = true 1318 break 1319 } 1320 } 1321 } 1322 page.sellPlacementsTableWrapper.classList.toggle('modified', sellPlacementsModified) 1323 } 1324 1325 /* 1326 * gapFactorHeaderUnit returns the header on the placements table and the 1327 * units in the gap factor rows needed for each gap strategy. 1328 */ 1329 gapFactorHeaderUnit (gapStrategy: string): [string, string] { 1330 switch (gapStrategy) { 1331 case GapStrategyMultiplier: 1332 return ['Multiplier', 'x'] 1333 case GapStrategyAbsolute: 1334 case GapStrategyAbsolutePlus: { 1335 const rateUnit = `${app().assets[this.specs.quoteID].symbol}/${app().assets[this.specs.baseID].symbol}` 1336 return ['Rate', rateUnit] 1337 } 1338 case GapStrategyPercent: 1339 case GapStrategyPercentPlus: 1340 return ['Percent', '%'] 1341 default: 1342 throw new Error(`Unknown gap strategy ${gapStrategy}`) 1343 } 1344 } 1345 1346 /* 1347 * checkGapFactorRange returns an error string if the value input for a 1348 * gap factor is valid for the currently selected gap strategy. 1349 */ 1350 checkGapFactorRange (gapFactor: string, value: number): (string | null) { 1351 switch (gapFactor) { 1352 case GapStrategyMultiplier: 1353 if (value < 1 || value > 100) { 1354 return 'Multiplier must be between 1 and 100' 1355 } 1356 return null 1357 case GapStrategyAbsolute: 1358 case GapStrategyAbsolutePlus: 1359 if (value <= 0) { 1360 return 'Rate must be greater than 0' 1361 } 1362 return null 1363 case GapStrategyPercent: 1364 case GapStrategyPercentPlus: 1365 if (value <= 0 || value > 10) { 1366 return 'Percent must be between 0 and 10' 1367 } 1368 return null 1369 default: { 1370 throw new Error(`Unknown gap factor ${gapFactor}`) 1371 } 1372 } 1373 } 1374 1375 /* 1376 * convertGapFactor converts between the displayed gap factor in the 1377 * placement tables and the number that is passed to the market maker. 1378 * For gap strategies that involve a percentage it converts between the 1379 * decimal value required by the backend and a percentage displayed to 1380 * the user. 1381 */ 1382 convertGapFactor (gapFactor: number, gapStrategy: string, toDisplay: boolean): number { 1383 switch (gapStrategy) { 1384 case GapStrategyMultiplier: 1385 case GapStrategyAbsolute: 1386 case GapStrategyAbsolutePlus: 1387 return gapFactor 1388 case GapStrategyPercent: 1389 case GapStrategyPercentPlus: 1390 if (toDisplay) { 1391 return gapFactor * 100 1392 } 1393 return gapFactor / 100 1394 default: 1395 throw new Error(`Unknown gap factor ${gapStrategy}`) 1396 } 1397 } 1398 1399 /* 1400 * addPlacement adds a row to a placement table. This is called both when 1401 * the page is initially loaded, and when the "add" button is pressed on 1402 * the placement table. initialLoadPlacement is non-nil if this is being 1403 * called on the initial load. 1404 */ 1405 addPlacement (isBuy: boolean, initialLoadPlacement: OrderPlacement | null, gapStrategy?: string) { 1406 const { page, updatedConfig: cfg } = this 1407 1408 let tableBody: PageElement = page.sellPlacementsTableBody 1409 let addPlacementRow: PageElement = page.addSellPlacementRow 1410 let lotsElement: PageElement = page.addSellPlacementLots 1411 let gapFactorElement: PageElement = page.addSellPlacementGapFactor 1412 let errElement: PageElement = page.sellPlacementsErr 1413 if (isBuy) { 1414 tableBody = page.buyPlacementsTableBody 1415 addPlacementRow = page.addBuyPlacementRow 1416 lotsElement = page.addBuyPlacementLots 1417 gapFactorElement = page.addBuyPlacementGapFactor 1418 errElement = page.buyPlacementsErr 1419 } 1420 1421 Doc.hide(errElement) 1422 1423 // updateArrowVis updates the visibility of the move up/down arrows in 1424 // each row of the placement table. The up arrow is not shown on the 1425 // top row, and the down arrow is not shown on the bottom row. They 1426 // are all hidden if market making is running. 1427 const updateArrowVis = () => { 1428 for (let i = 0; i < tableBody.children.length - 1; i++) { 1429 const row = Doc.parseTemplate(tableBody.children[i] as HTMLElement) 1430 Doc.setVis(i !== 0, row.upBtn) 1431 Doc.setVis(i !== tableBody.children.length - 2, row.downBtn) 1432 } 1433 } 1434 1435 Doc.hide(errElement) 1436 const setErr = (err: string) => { 1437 errElement.textContent = err 1438 Doc.show(errElement) 1439 } 1440 1441 let lots: number 1442 let actualGapFactor: number 1443 let displayedGapFactor: number 1444 if (!gapStrategy) gapStrategy = this.specs.cexName ? GapStrategyMultiplier : cfg.gapStrategy 1445 const placements = isBuy ? cfg.buyPlacements : cfg.sellPlacements 1446 const unit = this.gapFactorHeaderUnit(gapStrategy)[1] 1447 if (initialLoadPlacement) { 1448 lots = initialLoadPlacement.lots 1449 actualGapFactor = initialLoadPlacement.gapFactor 1450 displayedGapFactor = this.convertGapFactor(actualGapFactor, gapStrategy, true) 1451 } else { 1452 lots = parseInt(lotsElement.value || '0') 1453 displayedGapFactor = parseFloat(gapFactorElement.value || '0') 1454 actualGapFactor = this.convertGapFactor(displayedGapFactor, gapStrategy, false) 1455 if (lots === 0) { 1456 setErr('Lots must be greater than 0') 1457 return 1458 } 1459 1460 const gapFactorErr = this.checkGapFactorRange(gapStrategy, displayedGapFactor) 1461 if (gapFactorErr) { 1462 setErr(gapFactorErr) 1463 return 1464 } 1465 1466 if (placements.find((placement) => placement.gapFactor === actualGapFactor) 1467 ) { 1468 setErr('Duplicate placement') 1469 return 1470 } 1471 1472 placements.push({ lots, gapFactor: actualGapFactor }) 1473 } 1474 1475 const newRow = page.placementRowTmpl.cloneNode(true) as PageElement 1476 const newRowTmpl = Doc.parseTemplate(newRow) 1477 newRowTmpl.priority.textContent = `${tableBody.children.length}` 1478 newRowTmpl.lots.textContent = `${lots}` 1479 newRowTmpl.gapFactor.textContent = `${displayedGapFactor} ${unit}` 1480 Doc.bind(newRowTmpl.removeBtn, 'click', () => { 1481 const index = placements.findIndex((placement) => { 1482 return placement.lots === lots && placement.gapFactor === actualGapFactor 1483 }) 1484 if (index === -1) return 1485 placements.splice(index, 1) 1486 newRow.remove() 1487 updateArrowVis() 1488 this.updateModifiedMarkers() 1489 this.placementsChart.render() 1490 this.updateAllocations() 1491 }) 1492 1493 Doc.bind(newRowTmpl.upBtn, 'click', () => { 1494 const index = placements.findIndex((p: OrderPlacement) => p.lots === lots && p.gapFactor === actualGapFactor) 1495 if (index === 0) return 1496 const prevPlacement = placements[index - 1] 1497 placements[index - 1] = placements[index] 1498 placements[index] = prevPlacement 1499 newRowTmpl.priority.textContent = `${index}` 1500 newRow.remove() 1501 tableBody.insertBefore(newRow, tableBody.children[index - 1]) 1502 const movedDownTmpl = Doc.parseTemplate( 1503 tableBody.children[index] as HTMLElement 1504 ) 1505 movedDownTmpl.priority.textContent = `${index + 1}` 1506 updateArrowVis() 1507 this.updateModifiedMarkers() 1508 }) 1509 1510 Doc.bind(newRowTmpl.downBtn, 'click', () => { 1511 const index = placements.findIndex((p) => p.lots === lots && p.gapFactor === actualGapFactor) 1512 if (index === placements.length - 1) return 1513 const nextPlacement = placements[index + 1] 1514 placements[index + 1] = placements[index] 1515 placements[index] = nextPlacement 1516 newRowTmpl.priority.textContent = `${index + 2}` 1517 newRow.remove() 1518 tableBody.insertBefore(newRow, tableBody.children[index + 1]) 1519 const movedUpTmpl = Doc.parseTemplate( 1520 tableBody.children[index] as HTMLElement 1521 ) 1522 movedUpTmpl.priority.textContent = `${index + 1}` 1523 updateArrowVis() 1524 this.updateModifiedMarkers() 1525 }) 1526 1527 tableBody.insertBefore(newRow, addPlacementRow) 1528 updateArrowVis() 1529 } 1530 1531 setArbMMLabels () { 1532 this.page.buyGapFactorHdr.textContent = intl.prep(intl.ID_MATCH_BUFFER) 1533 this.page.sellGapFactorHdr.textContent = intl.prep(intl.ID_MATCH_BUFFER) 1534 } 1535 1536 /* 1537 * setGapFactorLabels sets the headers on the gap factor column of each 1538 * placement table. 1539 */ 1540 setGapFactorLabels (gapStrategy: string) { 1541 const page = this.page 1542 const header = this.gapFactorHeaderUnit(gapStrategy)[0] 1543 page.buyGapFactorHdr.textContent = header 1544 page.sellGapFactorHdr.textContent = header 1545 Doc.hide(page.percentPlusInfo, page.percentInfo, page.absolutePlusInfo, page.absoluteInfo, page.multiplierInfo) 1546 switch (gapStrategy) { 1547 case 'percent-plus': 1548 return Doc.show(page.percentPlusInfo) 1549 case 'percent': 1550 return Doc.show(page.percentInfo) 1551 case 'absolute-plus': 1552 return Doc.show(page.absolutePlusInfo) 1553 case 'absolute': 1554 return Doc.show(page.absoluteInfo) 1555 case 'multiplier': 1556 return Doc.show(page.multiplierInfo) 1557 } 1558 } 1559 1560 clearPlacements (cacheKey: string) { 1561 const { page, updatedConfig: cfg } = this 1562 while (page.buyPlacementsTableBody.children.length > 1) { 1563 page.buyPlacementsTableBody.children[0].remove() 1564 } 1565 while (page.sellPlacementsTableBody.children.length > 1) { 1566 page.sellPlacementsTableBody.children[0].remove() 1567 } 1568 this.placementsCache[cacheKey] = [cfg.buyPlacements, cfg.sellPlacements] 1569 cfg.buyPlacements.splice(0, cfg.buyPlacements.length) 1570 cfg.sellPlacements.splice(0, cfg.sellPlacements.length) 1571 } 1572 1573 loadCachedPlacements (cacheKey: string) { 1574 const c = this.placementsCache[cacheKey] 1575 if (!c) return 1576 const { updatedConfig: cfg } = this 1577 cfg.buyPlacements.splice(0, cfg.buyPlacements.length) 1578 cfg.sellPlacements.splice(0, cfg.sellPlacements.length) 1579 cfg.buyPlacements.push(...c[0]) 1580 cfg.sellPlacements.push(...c[1]) 1581 const gapStrategy = cacheKey === arbMMRowCacheKey ? GapStrategyMultiplier : cacheKey 1582 for (const p of cfg.buyPlacements) this.addPlacement(true, p, gapStrategy) 1583 for (const p of cfg.sellPlacements) this.addPlacement(false, p, gapStrategy) 1584 } 1585 1586 /* 1587 * setOriginalValues sets the updatedConfig field to be equal to the 1588 * and sets the values displayed buy each field input to be equal 1589 * to the values since the last save. 1590 */ 1591 setOriginalValues () { 1592 const { 1593 page, originalConfig: oldCfg, updatedConfig: cfg, specs: { cexName, botType } 1594 } = this 1595 1596 this.clearPlacements(cexName ? arbMMRowCacheKey : cfg.gapStrategy) 1597 1598 const assign = (to: any, from: any) => { // not recursive 1599 for (const [k, v] of Object.entries(from)) { 1600 if (Array.isArray(v)) { 1601 to[k].splice(0, to[k].length) 1602 for (const i of v) to[k].push(i) 1603 } else if (typeof v === 'object') Object.assign(to[k], v) 1604 else to[k] = from[k] 1605 } 1606 } 1607 assign(cfg, JSON.parse(JSON.stringify(oldCfg))) 1608 1609 const tol = cfg.driftTolerance ?? defaultDriftTolerance.value 1610 this.driftTolerance.setValue(tol * 100) 1611 this.driftToleranceSlider.setValue(tol / defaultDriftTolerance.maxV) 1612 1613 const persist = cfg.orderPersistence ?? defaultOrderPersistence.value 1614 this.orderPersistence.setValue(persist) 1615 this.orderPersistenceSlider.setValue(persist / defaultOrderPersistence.maxV) 1616 1617 const profit = cfg.profit ?? defaultProfit.value 1618 page.profitInput.value = String(profit * 100) 1619 this.qcProfit.setValue(profit * 100) 1620 this.qcProfitSlider.setValue((profit - defaultProfit.minV) / defaultProfit.range) 1621 1622 if (cexName) { 1623 page.cexRebalanceCheckbox.checked = cfg.cexRebalance 1624 this.autoRebalanceChanged() 1625 } 1626 1627 // Gap strategy 1628 if (!page.gapStrategySelect.options) return 1629 Array.from(page.gapStrategySelect.options).forEach((opt: HTMLOptionElement) => { opt.selected = opt.value === cfg.gapStrategy }) 1630 this.setGapFactorLabels(cfg.gapStrategy) 1631 1632 if (botType === botTypeBasicMM) { 1633 Doc.show(page.gapStrategyBox) 1634 Doc.hide(page.profitSelectorBox, page.orderPersistenceBox) 1635 this.setGapFactorLabels(page.gapStrategySelect.value || '') 1636 } else if (cexName && app().mmStatus.cexes[cexName]) { 1637 Doc.hide(page.gapStrategyBox) 1638 Doc.show(page.profitSelectorBox, page.orderPersistenceBox) 1639 this.setArbMMLabels() 1640 } 1641 1642 // Buy/Sell placements 1643 oldCfg.buyPlacements.forEach((p) => { this.addPlacement(true, p) }) 1644 oldCfg.sellPlacements.forEach((p) => { this.addPlacement(false, p) }) 1645 1646 this.basePane.setupWalletSettings() 1647 this.quotePane.setupWalletSettings() 1648 1649 this.updateModifiedMarkers() 1650 if (Doc.isDisplayed(page.quickConfig)) this.switchToQuickConfig() 1651 } 1652 1653 /* 1654 * validateFields validates configuration values and optionally shows error 1655 * messages. 1656 */ 1657 validateFields (showErrors: boolean): boolean { 1658 let ok = true 1659 const { 1660 page, specs: { botType }, 1661 updatedConfig: { sellPlacements, buyPlacements, profit } 1662 } = this 1663 const setError = (errEl: PageElement, errID: string) => { 1664 ok = false 1665 if (!showErrors) return 1666 errEl.textContent = intl.prep(errID) 1667 Doc.show(errEl) 1668 } 1669 if (showErrors) { 1670 Doc.hide( 1671 page.buyPlacementsErr, page.sellPlacementsErr, page.profitInputErr 1672 ) 1673 } 1674 if (botType !== botTypeBasicArb && buyPlacements.length + sellPlacements.length === 0) { 1675 setError(page.buyPlacementsErr, intl.ID_NO_PLACEMENTS) 1676 setError(page.sellPlacementsErr, intl.ID_NO_PLACEMENTS) 1677 } 1678 if (botType !== botTypeBasicMM) { 1679 if (isNaN(profit)) setError(page.profitInputErr, intl.ID_INVALID_VALUE) 1680 else if (profit === 0) setError(page.profitInputErr, intl.ID_NO_ZERO) 1681 } 1682 return ok 1683 } 1684 1685 /* 1686 * saveSettings updates the settings in the backend, and sets the originalConfig 1687 * to be equal to the updatedConfig. 1688 */ 1689 async saveSettings () { 1690 // Make a copy and delete either the basic mm config or the arb-mm config, 1691 // depending on whether a cex is selected. 1692 if (!this.validateFields(true)) return 1693 const { cfg, baseID, quoteID, host, botType, cexName } = this.marketStuff() 1694 1695 const botCfg: BotConfig = { 1696 host: host, 1697 baseID: baseID, 1698 quoteID: quoteID, 1699 cexName: cexName ?? '', 1700 uiConfig: { 1701 simpleArbLots: cfg.simpleArbLots, 1702 baseConfig: cfg.baseConfig, 1703 quoteConfig: cfg.quoteConfig, 1704 cexRebalance: cfg.cexRebalance 1705 }, 1706 baseWalletOptions: cfg.baseOptions, 1707 quoteWalletOptions: cfg.quoteOptions 1708 } 1709 switch (botType) { 1710 case botTypeBasicMM: 1711 botCfg.basicMarketMakingConfig = this.basicMMConfig() 1712 break 1713 case botTypeArbMM: 1714 botCfg.arbMarketMakingConfig = this.arbMMConfig() 1715 break 1716 case botTypeBasicArb: 1717 botCfg.simpleArbConfig = this.basicArbConfig() 1718 } 1719 1720 app().log('mm', 'saving bot config', botCfg) 1721 await MM.updateBotConfig(botCfg) 1722 await app().fetchMMStatus() 1723 this.originalConfig = JSON.parse(JSON.stringify(cfg)) 1724 this.updateModifiedMarkers() 1725 const lastBots = State.fetchLocal(lastBotsLK) || {} 1726 lastBots[`${baseID}_${quoteID}_${host}`] = this.specs 1727 State.storeLocal(lastBotsLK, lastBots) 1728 if (cexName) State.storeLocal(lastArbExchangeLK, cexName) 1729 app().loadPage('mm') 1730 } 1731 1732 async delete () { 1733 const { page, specs: { host, baseID, quoteID } } = this 1734 Doc.hide(page.deleteErr) 1735 const loaded = app().loading(page.botSettingsContainer) 1736 const resp = await MM.removeBotConfig(host, baseID, quoteID) 1737 loaded() 1738 if (!app().checkResponse(resp)) { 1739 page.deleteErr.textContent = intl.prep(intl.ID_API_ERROR, { msg: resp.msg }) 1740 Doc.show(page.deleteErr) 1741 return 1742 } 1743 await app().fetchMMStatus() 1744 app().loadPage('mm') 1745 } 1746 1747 /* 1748 * arbMMConfig parses the configuration for the arb-mm bot. Only one of 1749 * arbMMConfig or basicMMConfig should be used when updating the bot 1750 * configuration. Which is used depends on if the user has configured and 1751 * selected a CEX or not. 1752 */ 1753 arbMMConfig (): ArbMarketMakingConfig { 1754 const { updatedConfig: cfg } = this 1755 const arbCfg: ArbMarketMakingConfig = { 1756 buyPlacements: [], 1757 sellPlacements: [], 1758 profit: cfg.profit, 1759 driftTolerance: cfg.driftTolerance, 1760 orderPersistence: cfg.orderPersistence 1761 } 1762 for (const p of cfg.buyPlacements) arbCfg.buyPlacements.push({ lots: p.lots, multiplier: p.gapFactor }) 1763 for (const p of cfg.sellPlacements) arbCfg.sellPlacements.push({ lots: p.lots, multiplier: p.gapFactor }) 1764 return arbCfg 1765 } 1766 1767 basicArbConfig (): SimpleArbConfig { 1768 const { updatedConfig: cfg } = this 1769 const arbCfg: SimpleArbConfig = { 1770 profitTrigger: cfg.profit, 1771 maxActiveArbs: 100, // TODO 1772 numEpochsLeaveOpen: cfg.orderPersistence 1773 } 1774 return arbCfg 1775 } 1776 1777 /* 1778 * basicMMConfig parses the configuration for the basic marketmaker. Only of 1779 * of basidMMConfig or arbMMConfig should be used when updating the bot 1780 * configuration. 1781 */ 1782 basicMMConfig (): BasicMarketMakingConfig { 1783 const { updatedConfig: cfg } = this 1784 const mmCfg: BasicMarketMakingConfig = { 1785 gapStrategy: cfg.gapStrategy, 1786 sellPlacements: cfg.sellPlacements, 1787 buyPlacements: cfg.buyPlacements, 1788 driftTolerance: cfg.driftTolerance 1789 } 1790 return mmCfg 1791 } 1792 1793 /* 1794 * fetchOracles fetches the current oracle rates and fiat rates, and displays 1795 * them on the screen. 1796 */ 1797 async fetchMarketReport (): Promise<void> { 1798 const { page, specs: { host, baseID, quoteID } } = this 1799 1800 const res = await MM.report(host, baseID, quoteID) 1801 Doc.hide(page.oraclesLoading, page.oraclesTable, page.noOracles) 1802 1803 if (!app().checkResponse(res)) { 1804 page.oraclesErrMsg.textContent = res.msg 1805 Doc.show(page.oraclesErr) 1806 return 1807 } 1808 1809 const r = this.marketReport = res.report as MarketReport 1810 if (!r.oracles || r.oracles.length === 0) { 1811 Doc.show(page.noOracles) 1812 } else { 1813 Doc.hide(page.noOracles) 1814 Doc.empty(page.oracles) 1815 for (const o of r.oracles ?? []) { 1816 const tr = page.oracleTmpl.cloneNode(true) as PageElement 1817 page.oracles.appendChild(tr) 1818 const tmpl = Doc.parseTemplate(tr) 1819 tmpl.logo.src = 'img/' + o.host + '.png' 1820 tmpl.host.textContent = ExchangeNames[o.host] 1821 tmpl.volume.textContent = Doc.formatFourSigFigs(o.usdVol) 1822 tmpl.price.textContent = Doc.formatFourSigFigs((o.bestBuy + o.bestSell) / 2) 1823 } 1824 page.avgPrice.textContent = r.price ? Doc.formatFourSigFigs(r.price) : '0' 1825 Doc.show(page.oraclesTable) 1826 } 1827 1828 if (r.baseFiatRate > 0) { 1829 page.baseFiatRate.textContent = Doc.formatFourSigFigs(r.baseFiatRate) 1830 } else { 1831 page.baseFiatRate.textContent = 'N/A' 1832 } 1833 1834 if (r.quoteFiatRate > 0) { 1835 page.quoteFiatRate.textContent = Doc.formatFourSigFigs(r.quoteFiatRate) 1836 } else { 1837 page.quoteFiatRate.textContent = 'N/A' 1838 } 1839 Doc.show(page.fiatRates) 1840 } 1841 1842 /* 1843 * handleCEXSubmit handles clicks on the CEX configuration submission button. 1844 */ 1845 async cexConfigured (cexName: string) { 1846 const { page, formSpecs: { host, baseID, quoteID } } = this 1847 const dinfo = CEXDisplayInfos[cexName] 1848 for (const { baseID, quoteID, tmpl, arbs } of this.marketRows) { 1849 if (arbs.indexOf(cexName) !== -1) continue 1850 const cexHasMarket = this.cexMarketSupportFilter(baseID, quoteID) 1851 if (cexHasMarket(cexName)) { 1852 const img = page.arbBttnTmpl.cloneNode(true) as PageElement 1853 img.src = dinfo.logo 1854 tmpl.arbs.appendChild(img) 1855 arbs.push(cexName) 1856 } 1857 } 1858 this.setCEXAvailability(baseID, quoteID, cexName) 1859 this.showBotTypeForm(host, baseID, quoteID, botTypeArbMM, cexName) 1860 } 1861 1862 /* 1863 * setupCEXes should be called during initialization. 1864 */ 1865 setupCEXes () { 1866 this.formCexes = {} 1867 for (const name of Object.keys(CEXDisplayInfos)) this.addCEX(name) 1868 } 1869 1870 /* 1871 * setCEXAvailability sets the coloring and messaging of the CEX selection 1872 * buttons. 1873 */ 1874 setCEXAvailability (baseID: number, quoteID: number, selectedCEX?: string) { 1875 const cexHasMarket = this.cexMarketSupportFilter(baseID, quoteID) 1876 for (const { name, div, tmpl } of Object.values(this.formCexes)) { 1877 const has = cexHasMarket(name) 1878 const cexStatus = app().mmStatus.cexes[name] 1879 Doc.hide(tmpl.unavailable, tmpl.needsconfig, tmpl.disconnected) 1880 Doc.setVis(Boolean(cexStatus), tmpl.reconfig) 1881 tmpl.logo.classList.remove('greyscal') 1882 div.classList.toggle('configured', Boolean(cexStatus) && !cexStatus.connectErr) 1883 if (!cexStatus) { 1884 Doc.show(tmpl.needsconfig) 1885 } else if (cexStatus.connectErr) { 1886 Doc.show(tmpl.disconnected) 1887 } else if (!has) { 1888 Doc.show(tmpl.unavailable) 1889 tmpl.logo.classList.add('greyscal') 1890 } else if (name === selectedCEX) this.selectFormCEX(name) 1891 } 1892 } 1893 1894 selectFormCEX (cexName: string) { 1895 for (const { name, div } of Object.values(this.formCexes)) { 1896 div.classList.toggle('selected', name === cexName) 1897 } 1898 } 1899 1900 addCEX (cexName: string) { 1901 const dinfo = CEXDisplayInfos[cexName] 1902 const div = this.page.cexOptTmpl.cloneNode(true) as PageElement 1903 const tmpl = Doc.parseTemplate(div) 1904 tmpl.name.textContent = dinfo.name 1905 tmpl.logo.src = dinfo.logo 1906 this.page.cexSelection.appendChild(div) 1907 this.formCexes[cexName] = { name: cexName, div, tmpl } 1908 Doc.bind(div, 'click', () => { 1909 const cexStatus = app().mmStatus.cexes[cexName] 1910 if (!cexStatus || cexStatus.connectErr) { 1911 this.showCEXConfigForm(cexName) 1912 return 1913 } 1914 const cex = this.formCexes[cexName] 1915 if (cex.div.classList.contains('selected')) { // unselect 1916 for (const cex of Object.values(this.formCexes)) cex.div.classList.remove('selected') 1917 const { baseID, quoteID } = this.formSpecs 1918 this.setCEXAvailability(baseID, quoteID) 1919 return 1920 } 1921 for (const cex of Object.values(this.formCexes)) cex.div.classList.toggle('selected', cex.name === cexName) 1922 }) 1923 Doc.bind(tmpl.reconfig, 'click', (e: MouseEvent) => { 1924 e.stopPropagation() 1925 this.showCEXConfigForm(cexName) 1926 }) 1927 } 1928 1929 showCEXConfigForm (cexName: string) { 1930 const page = this.page 1931 this.cexConfigForm.setCEX(cexName) 1932 this.forms.show(page.cexConfigForm) 1933 } 1934 1935 /* 1936 * cexMarketSupportFilter returns a lookup CEXes that have a matching market 1937 * for the currently selected base and quote assets. 1938 */ 1939 cexMarketSupportFilter (baseID: number, quoteID: number) { 1940 const cexes: Record<string, boolean> = {} 1941 for (const [cexName, cexStatus] of Object.entries(app().mmStatus.cexes)) { 1942 for (const { baseID: b, quoteID: q } of Object.values(cexStatus.markets ?? [])) { 1943 if (b === baseID && q === quoteID) { 1944 cexes[cexName] = true 1945 break 1946 } 1947 } 1948 } 1949 return (cexName: string) => Boolean(cexes[cexName]) 1950 } 1951 } 1952 1953 function isViewOnly (specs: BotSpecs, mmStatus: MarketMakingStatus): boolean { 1954 const botStatus = mmStatus.bots.find(({ config: cfg }) => cfg.host === specs.host && cfg.baseID === specs.baseID && cfg.quoteID === specs.quoteID) 1955 return Boolean(botStatus?.running) 1956 } 1957 1958 const ExchangeNames: Record<string, string> = { 1959 'binance.com': 'Binance', 1960 'coinbase.com': 'Coinbase', 1961 'bittrex.com': 'Bittrex', 1962 'hitbtc.com': 'HitBTC', 1963 'exmo.com': 'EXMO' 1964 } 1965 1966 function tokenAssetApprovalStatuses (host: string, b: SupportedAsset, q: SupportedAsset) { 1967 let baseAssetApprovalStatus = ApprovalStatus.Approved 1968 let quoteAssetApprovalStatus = ApprovalStatus.Approved 1969 1970 if (b?.token) { 1971 const baseAsset = app().assets[b.id] 1972 const baseVersion = app().exchanges[host].assets[b.id].version 1973 if (baseAsset?.wallet?.approved && baseAsset.wallet.approved[baseVersion] !== undefined) { 1974 baseAssetApprovalStatus = baseAsset.wallet.approved[baseVersion] 1975 } 1976 } 1977 if (q?.token) { 1978 const quoteAsset = app().assets[q.id] 1979 const quoteVersion = app().exchanges[host].assets[q.id].version 1980 if (quoteAsset?.wallet?.approved && quoteAsset.wallet.approved[quoteVersion] !== undefined) { 1981 quoteAssetApprovalStatus = quoteAsset.wallet.approved[quoteVersion] 1982 } 1983 } 1984 1985 return [ 1986 baseAssetApprovalStatus, 1987 quoteAssetApprovalStatus 1988 ] 1989 } 1990 1991 class AssetPane { 1992 pg: MarketMakerSettingsPage 1993 div: PageElement 1994 page: Record<string, PageElement> 1995 assetID: number 1996 ui: UnitInfo 1997 walletConfig: Record<string, string> 1998 feeAssetID: number 1999 feeUI: UnitInfo 2000 isQuote: boolean 2001 isToken: boolean 2002 lotSize: number // might be quote converted 2003 lotSizeConv: number 2004 cfg: BotAssetConfig 2005 inv: ProjectedAlloc 2006 nSwapFees: IncrementalInput 2007 nSwapFeesSlider: MiniSlider 2008 orderReserves: NumberInput 2009 orderReservesSlider: MiniSlider 2010 slippageBuffer: NumberInput 2011 slippageBufferSlider: MiniSlider 2012 minTransfer: NumberInput 2013 minTransferSlider: MiniSlider 2014 2015 constructor (pg: MarketMakerSettingsPage, div: PageElement) { 2016 this.pg = pg 2017 this.div = div 2018 const page = this.page = Doc.parseTemplate(div) 2019 2020 this.nSwapFees = new IncrementalInput(page.nSwapFees, { 2021 prec: defaultSwapReserves.prec, 2022 inc: defaultSwapReserves.inc, 2023 changed: (v: number) => { 2024 const { minR, range } = defaultSwapReserves 2025 this.cfg.swapFeeN = v 2026 this.nSwapFeesSlider.setValue((v - minR) / range) 2027 this.pg.updateAllocations() 2028 } 2029 }) 2030 2031 this.nSwapFeesSlider = new MiniSlider(page.nSwapFeesSlider, (r: number) => { 2032 const { minR, range, prec } = defaultSwapReserves 2033 const [v] = toPrecision(minR + r * range, prec) 2034 this.cfg.swapFeeN = v 2035 this.nSwapFees.setValue(v) 2036 this.pg.updateAllocations() 2037 }) 2038 this.orderReserves = new NumberInput(page.orderReservesFactor, { 2039 prec: defaultOrderReserves.prec, 2040 min: 0, 2041 changed: (v: number) => { 2042 const { minR, range } = defaultOrderReserves 2043 this.cfg.orderReservesFactor = v 2044 this.orderReservesSlider.setValue((v - minR) / range) 2045 this.pg.updateAllocations() 2046 } 2047 }) 2048 this.orderReservesSlider = new MiniSlider(page.orderReservesSlider, (r: number) => { 2049 const { minR, range, prec } = defaultOrderReserves 2050 const [v] = toPrecision(minR + r * range, prec) 2051 this.orderReserves.setValue(v) 2052 this.cfg.orderReservesFactor = v 2053 this.pg.updateAllocations() 2054 }) 2055 this.slippageBuffer = new NumberInput(page.slippageBufferFactor, { 2056 prec: defaultSlippage.prec, 2057 min: 0, 2058 changed: (v: number) => { 2059 const { minR, range } = defaultSlippage 2060 this.cfg.slippageBufferFactor = v 2061 this.slippageBufferSlider.setValue((v - minR) / range) 2062 this.pg.updateAllocations() 2063 } 2064 }) 2065 this.slippageBufferSlider = new MiniSlider(page.slippageBufferSlider, (r: number) => { 2066 const { minR, range, prec } = defaultSlippage 2067 const [v] = toPrecision(minR + r * range, prec) 2068 this.slippageBuffer.setValue(minR + r * range) 2069 this.cfg.slippageBufferFactor = v 2070 this.pg.updateAllocations() 2071 }) 2072 this.minTransfer = new NumberInput(page.minTransfer, { 2073 sigFigs: true, 2074 min: 0, 2075 changed: (v: number) => { 2076 const { cfg } = this 2077 const totalInventory = this.commit() 2078 const [minV, maxV] = [this.minTransfer.min, Math.max(this.minTransfer.min * 2, totalInventory)] 2079 cfg.transferFactor = (v - minV) / (maxV - minV) 2080 this.minTransferSlider.setValue(cfg.transferFactor) 2081 } 2082 }) 2083 this.minTransferSlider = new MiniSlider(page.minTransferSlider, (r: number) => { 2084 const { cfg } = this 2085 const totalInventory = this.commit() 2086 const [minV, maxV] = [this.minTransfer.min, Math.max(this.minTransfer.min, totalInventory)] 2087 cfg.transferFactor = r 2088 this.minTransfer.setValue(minV + r * (maxV - minV)) 2089 }) 2090 2091 Doc.bind(page.showBalance, 'click', () => { pg.showAddress(this.assetID) }) 2092 } 2093 2094 // lot size can change if this is the quote asset, keep it updated. 2095 setLotSize (lotSize: number) { 2096 const { ui } = this 2097 this.lotSize = lotSize 2098 this.lotSizeConv = lotSize / ui.conventional.conversionFactor 2099 } 2100 2101 setAsset (assetID: number, isQuote: boolean) { 2102 this.assetID = assetID 2103 this.isQuote = isQuote 2104 const cfg = this.cfg = isQuote ? this.pg.updatedConfig.quoteConfig : this.pg.updatedConfig.baseConfig 2105 const { page, div, pg: { specs: { botType, baseID, cexName }, mktID, updatedConfig: { baseOptions, quoteOptions } } } = this 2106 const { symbol, name, token, unitInfo: ui } = app().assets[assetID] 2107 this.ui = ui 2108 this.walletConfig = assetID === baseID ? baseOptions : quoteOptions 2109 const { conventional: { unit: ticker } } = ui 2110 this.feeAssetID = token ? token.parentID : assetID 2111 const { unitInfo: feeUI, name: feeName, symbol: feeSymbol } = app().assets[this.feeAssetID] 2112 this.feeUI = feeUI 2113 this.inv = { book: 0, bookingFees: 0, swapFeeReserves: 0, cex: 0, orderReserves: 0, slippageBuffer: 0 } 2114 this.isToken = Boolean(token) 2115 Doc.setVis(this.isToken, page.feeTotalBox, page.feeReservesBox, page.feeBalances) 2116 Doc.setVis(isQuote, page.slippageBufferBox) 2117 Doc.setSrc(div, '[data-logo]', Doc.logoPath(symbol)) 2118 Doc.setText(div, '[data-name]', name) 2119 Doc.setText(div, '[data-ticker]', ticker) 2120 const { conventional: { unit: feeTicker } } = feeUI 2121 Doc.setText(div, '[data-fee-ticker]', feeTicker) 2122 Doc.setText(div, '[data-fee-name]', feeName) 2123 Doc.setSrc(div, '[data-fee-logo]', Doc.logoPath(feeSymbol)) 2124 Doc.setVis(botType !== botTypeBasicMM, page.cexMinInvBox) 2125 Doc.setVis(botType !== botTypeBasicArb, page.orderReservesBox) 2126 this.nSwapFees.setValue(cfg.swapFeeN ?? defaultSwapReserves.n) 2127 this.nSwapFeesSlider.setValue(cfg.swapFeeN / defaultSwapReserves.maxR) 2128 if (botType !== botTypeBasicArb) { 2129 const [v] = toPrecision(cfg.orderReservesFactor ?? defaultOrderReserves.factor, defaultOrderReserves.prec) 2130 this.orderReserves.setValue(v) 2131 this.orderReservesSlider.setValue((v - defaultOrderReserves.minR) / defaultOrderReserves.range) 2132 } 2133 if (botType !== botTypeBasicMM) { 2134 this.minTransfer.prec = Math.log10(ui.conventional.conversionFactor) 2135 const mkt = app().mmStatus.cexes[cexName as string].markets[mktID] 2136 this.minTransfer.min = ((isQuote ? mkt.quoteMinWithdraw : mkt.baseMinWithdraw) / ui.conventional.conversionFactor) 2137 } 2138 this.slippageBuffer.setValue(cfg.slippageBufferFactor) 2139 const { minR, range } = defaultSlippage 2140 this.slippageBufferSlider.setValue((cfg.slippageBufferFactor - minR) / range) 2141 this.setupWalletSettings() 2142 this.updateBalances() 2143 } 2144 2145 commit () { 2146 const { inv, isToken } = this 2147 let commit = inv.book + inv.cex + inv.orderReserves + inv.slippageBuffer 2148 if (!isToken) commit += inv.bookingFees + inv.swapFeeReserves 2149 return commit 2150 } 2151 2152 updateInventory (lots: number, counterLots: number, lotSize: number, dexCommit: number, cexCommit: number, fees: AssetBookingFees) { 2153 this.setLotSize(lotSize) 2154 const { page, cfg, lotSizeConv, inv, ui, feeUI, isToken, isQuote, pg: { specs: { cexName, botType } } } = this 2155 page.bookLots.textContent = String(lots) 2156 page.bookLotSize.textContent = Doc.formatFourSigFigs(lotSizeConv) 2157 inv.book = lots * lotSizeConv 2158 page.bookCommitment.textContent = Doc.formatFourSigFigs(inv.book) 2159 const feesPerLotConv = fees.bookingFeesPerLot / feeUI.conventional.conversionFactor 2160 page.bookingFeesPerLot.textContent = Doc.formatFourSigFigs(feesPerLotConv) 2161 page.swapReservesFactor.textContent = fees.swapReservesFactor.toFixed(2) 2162 page.bookingFeesLots.textContent = String(lots) 2163 inv.bookingFees = fees.bookingFees / feeUI.conventional.conversionFactor 2164 page.bookingFees.textContent = Doc.formatFourSigFigs(inv.bookingFees) 2165 if (cexName) { 2166 inv.cex = cexCommit / ui.conventional.conversionFactor 2167 page.cexMinInv.textContent = Doc.formatFourSigFigs(inv.cex) 2168 } 2169 if (botType !== botTypeBasicArb) { 2170 const totalInventory = Math.max(cexCommit, dexCommit) / ui.conventional.conversionFactor 2171 page.orderReservesBasis.textContent = Doc.formatFourSigFigs(totalInventory) 2172 const orderReserves = totalInventory * cfg.orderReservesFactor 2173 inv.orderReserves = orderReserves 2174 page.orderReserves.textContent = Doc.formatFourSigFigs(orderReserves) 2175 } 2176 if (isToken) { 2177 const feesPerSwapConv = fees.tokenFeesPerSwap / feeUI.conventional.conversionFactor 2178 page.feeReservesPerSwap.textContent = Doc.formatFourSigFigs(feesPerSwapConv) 2179 inv.swapFeeReserves = feesPerSwapConv * cfg.swapFeeN 2180 page.feeReserves.textContent = Doc.formatFourSigFigs(inv.swapFeeReserves) 2181 } 2182 if (isQuote) { 2183 const basis = inv.book + inv.cex + inv.orderReserves 2184 page.slippageBufferBasis.textContent = Doc.formatCoinValue(basis * ui.conventional.conversionFactor, ui) 2185 inv.slippageBuffer = basis * cfg.slippageBufferFactor 2186 page.slippageBuffer.textContent = Doc.formatCoinValue(inv.slippageBuffer * ui.conventional.conversionFactor, ui) 2187 } 2188 Doc.setVis(fees.bookingFeesPerCounterLot > 0, page.redemptionFeesBox) 2189 if (fees.bookingFeesPerCounterLot > 0) { 2190 const feesPerLotConv = fees.bookingFeesPerCounterLot / feeUI.conventional.conversionFactor 2191 page.redemptionFeesPerLot.textContent = Doc.formatFourSigFigs(feesPerLotConv) 2192 page.redemptionFeesLots.textContent = String(counterLots) 2193 page.redeemReservesFactor.textContent = fees.redeemReservesFactor.toFixed(2) 2194 } 2195 this.updateCommitTotal() 2196 this.updateTokenFees() 2197 this.updateRebalance() 2198 } 2199 2200 updateCommitTotal () { 2201 const { page, assetID, ui } = this 2202 const commit = this.commit() 2203 page.commitTotal.textContent = Doc.formatCoinValue(Math.round(commit * ui.conventional.conversionFactor), ui) 2204 page.commitTotalFiat.textContent = Doc.formatFourSigFigs(commit * app().fiatRatesMap[assetID]) 2205 } 2206 2207 updateTokenFees () { 2208 const { page, inv, feeAssetID, feeUI, isToken } = this 2209 if (!isToken) return 2210 const feeReserves = inv.bookingFees + inv.swapFeeReserves 2211 page.feeTotal.textContent = Doc.formatCoinValue(feeReserves * feeUI.conventional.conversionFactor, feeUI) 2212 page.feeTotalFiat.textContent = Doc.formatFourSigFigs(feeReserves * app().fiatRatesMap[feeAssetID]) 2213 } 2214 2215 updateRebalance () { 2216 const { page, cfg, pg: { updatedConfig: { cexRebalance }, specs: { cexName } } } = this 2217 const showRebalance = cexName && cexRebalance 2218 Doc.setVis(showRebalance, page.rebalanceOpts) 2219 if (!showRebalance) return 2220 const totalInventory = this.commit() 2221 const [minV, maxV] = [this.minTransfer.min, Math.max(this.minTransfer.min * 2, totalInventory)] 2222 const rangeV = maxV - minV 2223 this.minTransfer.setValue(minV + cfg.transferFactor * rangeV) 2224 this.minTransferSlider.setValue((cfg.transferFactor - defaultTransfer.minR) / defaultTransfer.range) 2225 } 2226 2227 setupWalletSettings () { 2228 const { page, assetID, walletConfig } = this 2229 const walletSettings = app().currentWalletDefinition(assetID) 2230 Doc.empty(page.walletSettings) 2231 Doc.setVis(!walletSettings.multifundingopts, page.walletSettingsNone) 2232 if (!walletSettings.multifundingopts) return 2233 const optToDiv: Record<string, PageElement> = {} 2234 const dependentOpts: Record<string, string[]> = {} 2235 const addDependentOpt = (optKey: string, optSetting: PageElement, dependentOn: string) => { 2236 if (!dependentOpts[dependentOn]) dependentOpts[dependentOn] = [] 2237 dependentOpts[dependentOn].push(optKey) 2238 optToDiv[optKey] = optSetting 2239 } 2240 const setDependentOptsVis = (parentOptKey: string, vis: boolean) => { 2241 const optKeys = dependentOpts[parentOptKey] 2242 if (!optKeys) return 2243 for (const optKey of optKeys) Doc.setVis(vis, optToDiv[optKey]) 2244 } 2245 const addOpt = (opt: OrderOption) => { 2246 if (opt.quoteAssetOnly && !this.isQuote) return 2247 const currVal = walletConfig[opt.key] 2248 let div: PageElement | undefined 2249 if (opt.isboolean) { 2250 div = page.boolSettingTmpl.cloneNode(true) as PageElement 2251 const tmpl = Doc.parseTemplate(div) 2252 tmpl.name.textContent = opt.displayname 2253 tmpl.input.checked = currVal === 'true' 2254 Doc.bind(tmpl.input, 'change', () => { 2255 walletConfig[opt.key] = tmpl.input.checked ? 'true' : 'false' 2256 setDependentOptsVis(opt.key, Boolean(tmpl.input.checked)) 2257 }) 2258 if (opt.description) tmpl.tooltip.dataset.tooltip = opt.description 2259 } else if (opt.xyRange) { 2260 const { start, end, xUnit } = opt.xyRange 2261 const range = end.x - start.x 2262 div = page.rangeSettingTmpl.cloneNode(true) as PageElement 2263 const tmpl = Doc.parseTemplate(div) 2264 tmpl.name.textContent = opt.displayname 2265 if (opt.description) tmpl.tooltip.dataset.tooltip = opt.description 2266 if (xUnit) tmpl.unit.textContent = xUnit 2267 else Doc.hide(tmpl.unit) 2268 2269 const input = new NumberInput(tmpl.value, { 2270 prec: 1, 2271 changed: (rawV: number) => { 2272 const [v, s] = toFourSigFigs(rawV, 1) 2273 walletConfig[opt.key] = s 2274 slider.setValue((v - start.x) / range) 2275 } 2276 }) 2277 const slider = new MiniSlider(tmpl.slider, (r: number) => { 2278 const rawV = start.x + r * range 2279 const [v, s] = toFourSigFigs(rawV, 1) 2280 walletConfig[opt.key] = s 2281 input.setValue(v) 2282 }) 2283 // TODO: default value should be smaller or none for base asset. 2284 const [v, s] = toFourSigFigs(parseFloatDefault(currVal, start.x), 3) 2285 walletConfig[opt.key] = s 2286 slider.setValue((v - start.x) / range) 2287 input.setValue(v) 2288 tmpl.value.textContent = s 2289 } 2290 if (!div) return console.error("don't know how to handle opt", opt) 2291 page.walletSettings.appendChild(div) 2292 if (opt.dependsOn) { 2293 addDependentOpt(opt.key, div, opt.dependsOn) 2294 const parentOptVal = walletConfig[opt.dependsOn] 2295 Doc.setVis(parentOptVal === 'true', div) 2296 } 2297 } 2298 2299 if (walletSettings.multifundingopts && walletSettings.multifundingopts.length > 0) { 2300 for (const opt of walletSettings.multifundingopts) addOpt(opt) 2301 } 2302 app().bindTooltips(page.walletSettings) 2303 } 2304 2305 updateBalances () { 2306 const { page, assetID, ui, feeAssetID, feeUI, pg: { specs: { cexName, baseID }, cexBaseBalance, cexQuoteBalance } } = this 2307 const { balance: { available } } = app().walletMap[assetID] 2308 const botInv = this.pg.runningBotInventory(assetID) 2309 const dexAvail = available - botInv.dex.total 2310 let cexAvail = 0 2311 Doc.setVis(cexName, page.balanceBreakdown) 2312 if (cexName) { 2313 page.dexAvail.textContent = Doc.formatFourSigFigs(dexAvail / ui.conventional.conversionFactor) 2314 const { available: cexRawAvail } = assetID === baseID ? cexBaseBalance : cexQuoteBalance 2315 cexAvail = cexRawAvail - botInv.cex.total 2316 page.cexAvail.textContent = Doc.formatFourSigFigs(cexAvail / ui.conventional.conversionFactor) 2317 } 2318 page.avail.textContent = Doc.formatFourSigFigs((dexAvail + cexAvail) / ui.conventional.conversionFactor) 2319 if (assetID === feeAssetID) return 2320 const { balance: { available: feeAvail } } = app().walletMap[feeAssetID] 2321 page.feeAvail.textContent = Doc.formatFourSigFigs(feeAvail / feeUI.conventional.conversionFactor) 2322 } 2323 }