From ee9482a0db0b22680c46a80673647e03c9e0867d Mon Sep 17 00:00:00 2001 From: Impulssi <169295838+Impulssi@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:34:43 +0300 Subject: [PATCH] fix(analysis): survive numeric botFunds values in analyze-orders bots.json documents botFunds as accepting either percentage strings ("90%") or absolute numbers (35 meaning 35 units of the side's asset), and the bot editor writes numbers when a bare value is entered. The "Funds:" display path in analyze-orders called string methods (padEnd/stripColorCodes) directly on the raw config values, so a numeric setting aborted the whole per-bot analysis with "str.replace is not a function" and the bot was skipped from the report. Normalize both botFunds sides to strings when the analysis object is built, so every display consumer (width alignment, Funds line) receives a string regardless of the configured type. Percentage strings pass through unchanged. ## Testing Notes - tests/test_analyze_orders_dynamic_weight.ts: new testAnalyzeOrderFormatsNumericBotFunds covering numeric normalization ("35" -> "35"), percentage passthrough ("35%" unchanged), and that formatAnalysis renders the Funds line without throwing for both forms. Verified the new test fails with the pre-fix analyzer (reproduces the original crash) and passes with the fix. - npm run typecheck clean. --- scripts/analyze-orders.ts | 26 ++++++++++++- tests/test_analyze_orders_dynamic_weight.ts | 43 +++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/scripts/analyze-orders.ts b/scripts/analyze-orders.ts index bd7fbe8a..cb8edffd 100755 --- a/scripts/analyze-orders.ts +++ b/scripts/analyze-orders.ts @@ -340,6 +340,25 @@ function stripColorCodes(str: string): string { // Load bot configurations const botsConfig = readJSON(BOTS_CONFIG).bots; +/** + * formatBotFunds: Normalize a botFunds side value for display. + * + * botFunds accepts both percentage strings ("90%") and absolute numbers (35 + * meaning 35 units of the side's asset) — see the README bot options table. + * The "Funds:" display path calls string methods (padEnd/replace) on these + * values, so a numeric setting crashed the whole analysis with + * "str.replace is not a function". Always hand the display a string. + * + * @param {string|number|null} value - raw botFunds side value from bots.json + * @returns {string} display representation, '-' when unset + */ +function formatBotFunds(value: any): string { + if (value == null) return '-'; + if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '-'; + const str = String(value); + return str.length > 0 ? str : '-'; +} + function getConfiguredBotConfig(botKey: string, botData: any): any { const meta = botData?.meta || {}; return botsConfig.find((bot: any, index: any) => { @@ -569,8 +588,11 @@ function analyzeOrder(botData: any, config: any, botKey: string): any { }, // Target active orders from config activeOrdersTarget: config ? config.activeOrders : null, - // Bot fund allocation settings from config - botFunds: config ? config.botFunds : null, + // Bot fund allocation settings from config (normalized to strings for + // the display paths; bots.json allows percentage strings or numbers) + botFunds: config && config.botFunds + ? { buy: formatBotFunds(config.botFunds.buy), sell: formatBotFunds(config.botFunds.sell) } + : null, // Weight distribution from config weightDistribution: config ? config.weightDistribution : null, // Resolved grid price label, value, and staleness flag diff --git a/tests/test_analyze_orders_dynamic_weight.ts b/tests/test_analyze_orders_dynamic_weight.ts index cf8c13fc..517cd210 100644 --- a/tests/test_analyze_orders_dynamic_weight.ts +++ b/tests/test_analyze_orders_dynamic_weight.ts @@ -642,6 +642,48 @@ function testAnalyzeOrderOmitsDynamicWeightForNonAma() { assert.strictEqual(analysis.dynamicWeight, null, 'non-AMA bot should have null dynamicWeight'); } +function testAnalyzeOrderFormatsNumericBotFunds() { + const { analyzeOrder, formatAnalysis } = loadAnalyzer(); + const botData = { + meta: { assetA: 'XRP', assetB: 'BTS', updatedAt: new Date().toISOString() }, + boundaryIdx: 0, + grid: [ + { type: 'buy', state: 'active', orderId: 'a', price: 100, size: 1 }, + { type: 'sell', state: 'active', orderId: 'b', price: 110, size: 1 }, + ], + }; + // bots.json documents botFunds as either percentage strings ("90%") or + // absolute numbers. A numeric value used to crash formatAnalysis with + // "str.replace is not a function" because the Funds display path calls + // string methods on the raw config values. + const config = { + gridPrice: 'fixed', + targetSpreadPercent: 1.5, + incrementPercent: 0.5, + activeOrders: { buy: 1, sell: 1 }, + botFunds: { buy: 35, sell: 90 }, + weightDistribution: { buy: 0.5, sell: 0.5 }, + }; + const analysis = analyzeOrder(botData, config, 'numeric-funds-bot'); + assert.strictEqual(analysis.botFunds.buy, '35', 'numeric botFunds.buy should be normalized to a string'); + assert.strictEqual(analysis.botFunds.sell, '90', 'numeric botFunds.sell should be normalized to a string'); + + // The original crash: formatAnalysis renders the Funds line with + // padEnd/stripColorCodes on these values. + let output; + assert.doesNotThrow(() => { output = formatAnalysis(analysis); }, 'formatAnalysis must survive numeric botFunds configs'); + const fundsLine = stripColorCodes(String(output)).split('\n').find((l) => l.includes('Funds:')); + assert.ok(fundsLine, 'Funds line should be rendered'); + assert.ok(fundsLine.includes('35'), 'Funds line should show the numeric buy value'); + assert.ok(fundsLine.includes('90'), 'Funds line should show the numeric sell value'); + + // Percentage strings must keep their exact form. + const analysisPct = analyzeOrder(botData, { ...config, botFunds: { buy: '35%', sell: '90%' } }, 'pct-funds-bot'); + assert.strictEqual(analysisPct.botFunds.buy, '35%', 'percentage botFunds.buy should pass through unchanged'); + assert.strictEqual(analysisPct.botFunds.sell, '90%', 'percentage botFunds.sell should pass through unchanged'); + assert.doesNotThrow(() => { formatAnalysis(analysisPct); }, 'formatAnalysis must survive percentage botFunds configs'); +} + function testResolveAmaKey() { const { resolveAmaKey } = loadAnalyzer(); assert.strictEqual(resolveAmaKey({ gridPrice: 'ama' }), 'AMA3', 'ama resolves to AMA3 (default)'); @@ -782,6 +824,7 @@ async function main() { testFormatWeightLineNullWeights(); testAnalyzeOrderIncludesDynamicWeightForAma(); testAnalyzeOrderOmitsDynamicWeightForNonAma(); + testAnalyzeOrderFormatsNumericBotFunds(); console.log('analyze-orders dynamic weight tests passed'); }