@backtest-kit/signals 14.0.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -140,13 +140,6 @@ const columns$3 = [
140
140
  ? `${await backtestKit.formatPrice(symbol, Number(v))} USD`
141
141
  : "N/A",
142
142
  },
143
- {
144
- key: "atr14_raw",
145
- label: "ATR(14) Raw",
146
- format: async (v, symbol) => v !== null
147
- ? `${await backtestKit.formatPrice(symbol, Number(v))} USD`
148
- : "N/A",
149
- },
150
143
  {
151
144
  key: "atr20",
152
145
  label: "ATR(20)",
@@ -209,6 +202,11 @@ const columns$3 = [
209
202
  ? `${await backtestKit.formatPrice(symbol, Number(v))} USD`
210
203
  : "N/A",
211
204
  },
205
+ {
206
+ key: "volumeTrendRatio",
207
+ label: "Volume Trend Ratio",
208
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
209
+ },
212
210
  {
213
211
  key: "currentPrice",
214
212
  label: "Current Price",
@@ -343,8 +341,8 @@ function calculateFibonacciLevels$2(candles, endIndex) {
343
341
  "61.8%": high - range * 0.618,
344
342
  "78.6%": high - range * 0.786,
345
343
  "100.0%": low,
346
- "127.2%": high - range * 1.272,
347
- "161.8%": high - range * 1.618,
344
+ "127.2% (downside)": high - range * 1.272,
345
+ "161.8% (downside)": high - range * 1.618,
348
346
  };
349
347
  const currentPrice = Number(candles[endIndex].close);
350
348
  let nearestLevel = {
@@ -438,20 +436,19 @@ function generateAnalysis$3(symbol, candles) {
438
436
  if (i < WARMUP_PERIOD$3) {
439
437
  return;
440
438
  }
441
- // Volume trend calculation
442
- const volumeSma6 = new tradingSignals.FasterSMA(6);
443
- const volumeSma6Prev = new tradingSignals.FasterSMA(6);
439
+ // Volume trend calculation: average of last 6 candles vs previous 6
444
440
  const volumeStart = Math.max(0, i + 1 - 12);
445
441
  const prevVolumeData = volumes.slice(volumeStart, Math.min(volumeStart + 6, i + 1));
446
442
  const recentVolumeData = volumes.slice(Math.max(0, i + 1 - 6), i + 1);
447
- if (prevVolumeData.length > 0) {
448
- prevVolumeData.forEach((vol) => volumeSma6Prev.update(vol, false));
449
- }
450
- if (recentVolumeData.length > 0) {
451
- recentVolumeData.forEach((vol) => volumeSma6.update(vol, false));
452
- }
453
- const recentVolumeRaw = volumeSma6.getResult();
454
- const prevVolumeRaw = volumeSma6Prev.getResult();
443
+ const averageOf = (values) => {
444
+ const safe = values.filter((value) => !isUnsafe$4(value));
445
+ if (safe.length === 0) {
446
+ return null;
447
+ }
448
+ return safe.reduce((sum, value) => sum + value, 0) / safe.length;
449
+ };
450
+ const recentVolumeRaw = averageOf(recentVolumeData);
451
+ const prevVolumeRaw = averageOf(prevVolumeData);
455
452
  const recentVolume = !isUnsafe$4(recentVolumeRaw)
456
453
  ? recentVolumeRaw
457
454
  : volumes[i];
@@ -503,9 +500,6 @@ function generateAnalysis$3(symbol, candles) {
503
500
  atr14: atr14.getResult() != null && !isUnsafe$4(atr14.getResult())
504
501
  ? atr14.getResult()
505
502
  : null,
506
- atr14_raw: atr14.getResult() != null && !isUnsafe$4(atr14.getResult())
507
- ? atr14.getResult()
508
- : null,
509
503
  atr20: atr20.getResult() != null && !isUnsafe$4(atr20.getResult())
510
504
  ? atr20.getResult()
511
505
  : null,
@@ -556,11 +550,12 @@ function generateAnalysis$3(symbol, candles) {
556
550
  fibonacciDistance: fibonacciNearest.distance,
557
551
  bodySize,
558
552
  closePrice: close,
559
- date: new Date(),
553
+ date: new Date(_candle.timestamp),
560
554
  lookbackPeriod: "48 candles (48 hours) with SMA(50) from 100 hours",
561
555
  });
562
556
  });
563
- return results;
557
+ // Return only the last TABLE_ROWS_LIMIT rows
558
+ return results.slice(-TABLE_ROWS_LIMIT$3);
564
559
  }
565
560
  /**
566
561
  * Generates markdown table with long-term technical analysis history.
@@ -623,9 +618,7 @@ async function generateHistoryTable$3(indicators, symbol) {
623
618
  markdown +=
624
619
  "- **ATR(14)**: over previous 14 candles (14 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞)\n";
625
620
  markdown +=
626
- "- **ATR(14) Raw**: raw value over previous 14 candles before row timestamp (Min: 0 USD, Max: +∞)\n";
627
- markdown +=
628
- "- **ATR(20) Raw**: raw value over previous 20 candles (20 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞)\n";
621
+ "- **ATR(20)**: over previous 20 candles (20 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞)\n";
629
622
  markdown +=
630
623
  "- **CCI(20)**: over previous 20 candles (20 hours on 1h timeframe) before row timestamp (Min: -∞, Max: +∞)\n";
631
624
  markdown +=
@@ -650,12 +643,14 @@ async function generateHistoryTable$3(indicators, symbol) {
650
643
  "- **EMA(34)**: over previous 34 candles (34 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
651
644
  markdown +=
652
645
  "- **Momentum(10)**: over previous 10 candles (10 hours on 1h timeframe) before row timestamp (Min: -∞ USD, Max: +∞ USD)\n";
646
+ markdown +=
647
+ "- **Volume Trend Ratio**: average volume of last 6 candles relative to previous 6 candles before row timestamp (Min: 0x, Max: +∞x; above 1x = volume increasing)\n";
653
648
  markdown +=
654
649
  "- **Support**: over previous 4 candles (4 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
655
650
  markdown +=
656
651
  "- **Resistance**: over previous 4 candles (4 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
657
652
  markdown +=
658
- "- **Fibonacci Nearest Level**: nearest level name before row timestamp\n";
653
+ "- **Fibonacci Nearest Level**: nearest level name before row timestamp; levels marked (downside) lie below the range low\n";
659
654
  markdown +=
660
655
  "- **Fibonacci Nearest Price**: nearest price level before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
661
656
  markdown +=
@@ -770,9 +765,7 @@ class LongTermHistoryService {
770
765
  this.getReport = async (symbol) => {
771
766
  this.loggerService.log("longTermHistoryService getReport", { symbol });
772
767
  const fullCandles = await backtestKit.getCandles(symbol, "1h", 100);
773
- // Use all candles for indicator warm-up, then filter to last TABLE_ROWS_LIMIT rows
774
- const allRows = await this.getData(symbol, fullCandles);
775
- const rows = allRows.slice(-TABLE_ROWS_LIMIT$3);
768
+ const rows = await this.getData(symbol, fullCandles);
776
769
  return generateHistoryTable$3(rows, symbol);
777
770
  };
778
771
  /**
@@ -1068,6 +1061,11 @@ const columns$2 = [
1068
1061
  label: "Volume Ratio",
1069
1062
  format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
1070
1063
  },
1064
+ {
1065
+ key: "volumeTrendRatio",
1066
+ label: "Volume Trend Ratio",
1067
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
1068
+ },
1071
1069
  {
1072
1070
  key: "volatility5",
1073
1071
  label: "Volatility(5)",
@@ -1157,9 +1155,11 @@ function calculateVolumeMetrics(candles, endIndex) {
1157
1155
  const volumeSma5 = new tradingSignals.FasterSMA(5);
1158
1156
  volumes.forEach((vol) => volumeSma5.update(vol, false));
1159
1157
  const avgVolumeRaw = volumeSma5.getResult();
1160
- const avgVolume = !isUnsafe$3(avgVolumeRaw) ? avgVolumeRaw : 0;
1158
+ const avgVolume = !isUnsafe$3(avgVolumeRaw) ? avgVolumeRaw : null;
1161
1159
  const currentVolume = volumes[volumes.length - 1];
1162
- const volumeRatio = avgVolume > 0 && !isUnsafe$3(currentVolume) ? currentVolume / avgVolume : 1;
1160
+ const volumeRatio = avgVolume !== null && avgVolume > 0 && !isUnsafe$3(currentVolume)
1161
+ ? currentVolume / avgVolume
1162
+ : null;
1163
1163
  let volumeTrendRatio = null;
1164
1164
  if (volumes.length >= 6) {
1165
1165
  const recent3 = volumes.slice(-3);
@@ -1196,15 +1196,15 @@ function calculatePriceChanges(candles, endIndex) {
1196
1196
  return { priceChange1m: null, priceChange3m: null, priceChange5m: null };
1197
1197
  }
1198
1198
  const current = closes[closes.length - 1];
1199
- const priceChange1m = closes.length >= 2
1199
+ const priceChange1m = closes.length >= 2 && closes[closes.length - 2] > 0
1200
1200
  ? ((current - closes[closes.length - 2]) / closes[closes.length - 2]) *
1201
1201
  100
1202
1202
  : null;
1203
- const priceChange3m = closes.length >= 4
1203
+ const priceChange3m = closes.length >= 4 && closes[closes.length - 4] > 0
1204
1204
  ? ((current - closes[closes.length - 4]) / closes[closes.length - 4]) *
1205
1205
  100
1206
1206
  : null;
1207
- const priceChange5m = closes.length >= 6
1207
+ const priceChange5m = closes.length >= 6 && closes[closes.length - 6] > 0
1208
1208
  ? ((current - closes[closes.length - 6]) / closes[closes.length - 6]) *
1209
1209
  100
1210
1210
  : null;
@@ -1350,9 +1350,9 @@ function generateAnalysis$2(symbol, candles) {
1350
1350
  sma8.update(close, false);
1351
1351
  dema8.update(close, false);
1352
1352
  wma5.update(close, false);
1353
- // Determine minimum warm-up period needed (largest indicator period)
1354
- // EMA(21) is the largest period
1355
- // Skip rows until all indicators are warmed up
1353
+ // Skip rows until the primary indicators are warmed up.
1354
+ // WARMUP_PERIOD covers EMA(21); slower indicators (e.g. StochRSI(14)
1355
+ // needs ~28 candles) render as N/A in the first rows until ready.
1356
1356
  if (i < WARMUP_PERIOD$2) {
1357
1357
  return;
1358
1358
  }
@@ -1523,7 +1523,7 @@ function generateAnalysis$2(symbol, candles) {
1523
1523
  squeezeMomentum,
1524
1524
  pressureIndex,
1525
1525
  closePrice: close,
1526
- date: new Date(),
1526
+ date: new Date(_candle.timestamp),
1527
1527
  lookbackPeriod: "60 candles (60 minutes)",
1528
1528
  });
1529
1529
  });
@@ -1609,7 +1609,7 @@ async function generateHistoryTable$2(indicators, symbol) {
1609
1609
  markdown +=
1610
1610
  "- **Bollinger Width(8,2.0)**: width percentage before row timestamp (Min: 0%, Max: +∞)\n";
1611
1611
  markdown +=
1612
- "- **Bollinger Position**: price position within bands before row timestamp (Min: 0%, Max: 100%)\n";
1612
+ "- **Bollinger Position**: price position within bands before row timestamp (0% = lower band, 100% = upper band; can go below 0% or above 100% when price is outside the bands)\n";
1613
1613
  markdown +=
1614
1614
  "- **Stochastic K(3,3,3)**: over previous 3 candles (3 minutes on 1m timeframe) before row timestamp (Min: 0, Max: 100)\n";
1615
1615
  markdown +=
@@ -1646,6 +1646,8 @@ async function generateHistoryTable$2(indicators, symbol) {
1646
1646
  "- **Volume SMA(5)**: over previous 5 candles (5 minutes on 1m timeframe) before row timestamp (Min: 0, Max: +∞)\n";
1647
1647
  markdown +=
1648
1648
  "- **Volume Ratio**: volume relative to average at row timestamp (Min: 0x, Max: +∞x)\n";
1649
+ markdown +=
1650
+ "- **Volume Trend Ratio**: average volume of last 3 candles relative to previous 3 candles at row timestamp (Min: 0x, Max: +∞x; above 1x = volume increasing)\n";
1649
1651
  markdown +=
1650
1652
  "- **Support**: over previous 30 candles (30 minutes on 1m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
1651
1653
  markdown +=
@@ -1896,6 +1898,11 @@ const columns$1 = [
1896
1898
  label: "ROC(10) - Rate of Change %",
1897
1899
  format: (v) => (v !== null ? `${Number(v).toFixed(3)}%` : "N/A"),
1898
1900
  },
1901
+ {
1902
+ key: "volumeTrendRatio",
1903
+ label: "Volume Trend Ratio",
1904
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
1905
+ },
1899
1906
  {
1900
1907
  key: "sma50",
1901
1908
  label: "SMA(50)",
@@ -2070,8 +2077,8 @@ function calculateFibonacciLevels$1(candles, endIndex) {
2070
2077
  "61.8%": high - range * 0.618,
2071
2078
  "78.6%": high - range * 0.786,
2072
2079
  "100.0%": low,
2073
- "127.2%": high - range * 1.272,
2074
- "161.8%": high - range * 1.618,
2080
+ "127.2% (downside)": high - range * 1.272,
2081
+ "161.8% (downside)": high - range * 1.618,
2075
2082
  };
2076
2083
  const currentPrice = Number(candles[endIndex].close);
2077
2084
  let nearestLevel = {
@@ -2250,7 +2257,8 @@ function generateAnalysis$1(symbol, candles) {
2250
2257
  bollingerWidth10_2: bollingerResult &&
2251
2258
  !isUnsafe$2(bollingerResult.upper) &&
2252
2259
  !isUnsafe$2(bollingerResult.lower) &&
2253
- !isUnsafe$2(bollingerResult.middle)
2260
+ !isUnsafe$2(bollingerResult.middle) &&
2261
+ bollingerResult.middle !== 0
2254
2262
  ? ((bollingerResult.upper - bollingerResult.lower) /
2255
2263
  bollingerResult.middle) *
2256
2264
  100
@@ -2311,7 +2319,7 @@ function generateAnalysis$1(symbol, candles) {
2311
2319
  fibonacciDistance: fibonacciNearest.distance,
2312
2320
  bodySize,
2313
2321
  closePrice: close,
2314
- date: new Date(),
2322
+ date: new Date(_candle.timestamp),
2315
2323
  lookbackPeriod: "144 candles (36 hours)",
2316
2324
  });
2317
2325
  });
@@ -2398,6 +2406,8 @@ async function generateHistoryTable$1(indicators, symbol) {
2398
2406
  "- **ROC(5)**: over previous 5 candles (75 minutes on 15m timeframe) before row timestamp (Min: -∞%, Max: +∞%)\n";
2399
2407
  markdown +=
2400
2408
  "- **ROC(10)**: over previous 10 candles (150 minutes on 15m timeframe) before row timestamp (Min: -∞%, Max: +∞%)\n";
2409
+ markdown +=
2410
+ "- **Volume Trend Ratio**: average volume of last 8 candles relative to previous 8 candles before row timestamp (Min: 0x, Max: +∞x; above 1x = volume increasing)\n";
2401
2411
  markdown +=
2402
2412
  "- **SMA(50)**: over previous 50 candles (750 minutes on 15m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2403
2413
  markdown +=
@@ -2413,9 +2423,9 @@ async function generateHistoryTable$1(indicators, symbol) {
2413
2423
  markdown +=
2414
2424
  "- **Resistance**: over previous 48 candles (12 hours on 15m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2415
2425
  markdown +=
2416
- "- **Fibonacci Nearest Level**: nearest level name before row timestamp\n";
2426
+ "- **Fibonacci Nearest Level**: nearest level name before row timestamp; levels marked (downside) lie below the range low\n";
2417
2427
  markdown +=
2418
- "- **Fibonacci Nearest Price**: nearest price level over 288 candles (72h on 15m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2428
+ "- **Fibonacci Nearest Price**: nearest price level over up to 288 candles (limited by the 144-candle / 36h report history) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2419
2429
  markdown +=
2420
2430
  "- **Fibonacci Distance**: distance to nearest level before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2421
2431
  markdown +=
@@ -2570,6 +2580,11 @@ const TABLE_ROWS_LIMIT = 30;
2570
2580
  * Ensures all technical indicators (especially EMA(34)) have sufficient data.
2571
2581
  */
2572
2582
  const WARMUP_PERIOD = 34;
2583
+ /**
2584
+ * Rolling window (in candles) for the volatility calculation.
2585
+ * Keeps the metric comparable across rows regardless of total history length.
2586
+ */
2587
+ const VOLATILITY_WINDOW = 20;
2573
2588
  const columns = [
2574
2589
  {
2575
2590
  key: "rsi14",
@@ -3003,16 +3018,20 @@ function generateAnalysis(symbol, candles) {
3003
3018
  if (i < WARMUP_PERIOD) {
3004
3019
  return;
3005
3020
  }
3006
- const priceChanges = closes
3007
- .slice(0, i + 1)
3008
- .slice(1)
3009
- .map((price, idx) => {
3010
- const prevPrice = closes.slice(0, i + 1)[idx];
3011
- return ((price - prevPrice) / prevPrice) * 100;
3012
- });
3013
- const volatility = priceChanges.length > 0
3014
- ? Math.sqrt(priceChanges.reduce((sum, change) => sum + change ** 2, 0) /
3015
- priceChanges.length)
3021
+ let volatilitySumSq = 0;
3022
+ let volatilityCount = 0;
3023
+ for (let idx = Math.max(1, i + 1 - VOLATILITY_WINDOW); idx <= i; idx++) {
3024
+ const prevPrice = closes[idx - 1];
3025
+ const currPrice = closes[idx];
3026
+ if (isUnsafe$1(prevPrice) || isUnsafe$1(currPrice) || prevPrice <= 0) {
3027
+ continue;
3028
+ }
3029
+ const change = ((currPrice - prevPrice) / prevPrice) * 100;
3030
+ volatilitySumSq += change ** 2;
3031
+ volatilityCount += 1;
3032
+ }
3033
+ const volatility = volatilityCount > 0
3034
+ ? Math.sqrt(volatilitySumSq / volatilityCount)
3016
3035
  : null;
3017
3036
  const { support, resistance } = calculateSupportResistance(candles, i);
3018
3037
  const fibonacci = calculateFibonacciLevels(candles, i);
@@ -3108,7 +3127,7 @@ function generateAnalysis(symbol, candles) {
3108
3127
  fibonacciPositionPercent: fibonacci.fibonacciPositionPercent,
3109
3128
  bodySize,
3110
3129
  closePrice: close,
3111
- date: new Date(),
3130
+ date: new Date(_candle.timestamp),
3112
3131
  lookbackPeriod: "96 candles (48 hours)",
3113
3132
  });
3114
3133
  });
@@ -3222,7 +3241,7 @@ async function generateHistoryTable(indicators, symbol) {
3222
3241
  markdown +=
3223
3242
  "- **Volume**: trading volume at row timestamp (Min: 0, Max: +∞)\n";
3224
3243
  markdown +=
3225
- "- **Volatility**: volatility percentage at row timestamp (Min: 0%, Max: +∞)\n";
3244
+ "- **Volatility**: RMS of 1-candle percentage changes over previous 20 candles (10 hours on 30m timeframe) before row timestamp (Min: 0%, Max: +∞)\n";
3226
3245
  markdown += "\n## Column Descriptions\n\n";
3227
3246
  markdown += "**RSI(14) - Relative Strength Index**: Momentum oscillator measuring the speed and magnitude of price changes on 30-minute timeframe. Values above 70 indicate overbought conditions suitable for profit-taking, below 30 indicate oversold conditions suitable for entries.\n\n";
3228
3247
  markdown += "**StochRSI(14) - RSI Oscillator**: Applies Stochastic calculation to RSI values, providing more sensitive overbought/oversold signals for swing trading. Range 0-100, with 80+ being overbought and 20- being oversold.\n\n";
@@ -3627,11 +3646,11 @@ class HourCandleHistoryService {
3627
3646
  markdown += `### 1h Candle ${index + 1}\n`;
3628
3647
  markdown += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3629
3648
  markdown += `- **Time**: ${formattedTime}\n`;
3630
- markdown += `- **Open**: ${backtestKit.formatPrice(symbol, candle.open)} USD\n`;
3631
- markdown += `- **High**: ${backtestKit.formatPrice(symbol, candle.high)} USD\n`;
3632
- markdown += `- **Low**: ${backtestKit.formatPrice(symbol, candle.low)} USD\n`;
3633
- markdown += `- **Close**: ${backtestKit.formatPrice(symbol, candle.close)} USD\n`;
3634
- markdown += `- **Volume**: ${backtestKit.formatQuantity(symbol, candle.volume)}\n`;
3649
+ markdown += `- **Open**: ${await backtestKit.formatPrice(symbol, candle.open)} USD\n`;
3650
+ markdown += `- **High**: ${await backtestKit.formatPrice(symbol, candle.high)} USD\n`;
3651
+ markdown += `- **Low**: ${await backtestKit.formatPrice(symbol, candle.low)} USD\n`;
3652
+ markdown += `- **Close**: ${await backtestKit.formatPrice(symbol, candle.close)} USD\n`;
3653
+ markdown += `- **Volume**: ${await backtestKit.formatQuantity(symbol, candle.volume)}\n`;
3635
3654
  markdown += `- **1h Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3636
3655
  markdown += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3637
3656
  }
@@ -3781,7 +3800,7 @@ class OneMinuteCandleHistoryService {
3781
3800
  markdown += `- **High**: ${await backtestKit.formatPrice(symbol, candle.high)} USD\n`;
3782
3801
  markdown += `- **Low**: ${await backtestKit.formatPrice(symbol, candle.low)} USD\n`;
3783
3802
  markdown += `- **Close**: ${await backtestKit.formatPrice(symbol, candle.close)} USD\n`;
3784
- markdown += `- **Volume**: ${backtestKit.formatQuantity(symbol, candle.volume)}\n`;
3803
+ markdown += `- **Volume**: ${await backtestKit.formatQuantity(symbol, candle.volume)}\n`;
3785
3804
  markdown += `- **1m Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3786
3805
  markdown += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3787
3806
  }
@@ -3931,7 +3950,7 @@ class ThirtyMinuteCandleHistoryService {
3931
3950
  report += `- **High**: ${await backtestKit.formatPrice(symbol, candle.high)} USD\n`;
3932
3951
  report += `- **Low**: ${await backtestKit.formatPrice(symbol, candle.low)} USD\n`;
3933
3952
  report += `- **Close**: ${await backtestKit.formatPrice(symbol, candle.close)} USD\n`;
3934
- report += `- **Volume**: ${backtestKit.formatQuantity(symbol, candle.volume)}\n`;
3953
+ report += `- **Volume**: ${await backtestKit.formatQuantity(symbol, candle.volume)}\n`;
3935
3954
  report += `- **30m Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3936
3955
  report += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3937
3956
  }
@@ -4262,18 +4281,18 @@ class BookDataMathService {
4262
4281
  });
4263
4282
  const depth = await backtestKit.getOrderBook(symbol, MAX_DEPTH_LEVELS);
4264
4283
  // Just process raw data - no calculations
4265
- const bids = processOrderBookSide(depth.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))); // Сортировка по убыванию
4266
- const asks = processOrderBookSide(depth.asks.sort((a, b) => parseFloat(a.price) - parseFloat(b.price))); // Сортировка по возрастанию
4267
- const bestBid = bids.length > 0 ? bids[0].price : 0;
4268
- const bestAsk = asks.length > 0 ? asks[0].price : 0;
4269
- const midPrice = (bestBid + bestAsk) / 2;
4270
- const spread = bestAsk - bestBid;
4284
+ const bids = processOrderBookSide([...depth.bids].sort((a, b) => parseFloat(b.price) - parseFloat(a.price))); // Сортировка по убыванию
4285
+ const asks = processOrderBookSide([...depth.asks].sort((a, b) => parseFloat(a.price) - parseFloat(b.price))); // Сортировка по возрастанию
4286
+ const bestBid = bids.length > 0 ? bids[0].price : null;
4287
+ const bestAsk = asks.length > 0 ? asks[0].price : null;
4288
+ const midPrice = bestBid !== null && bestAsk !== null ? (bestBid + bestAsk) / 2 : null;
4289
+ const spread = bestBid !== null && bestAsk !== null ? bestAsk - bestBid : null;
4271
4290
  // Calculate depth imbalance
4272
4291
  const totalBidVolume = bids.reduce((sum, bid) => sum + bid.quantity, 0);
4273
4292
  const totalAskVolume = asks.reduce((sum, ask) => sum + ask.quantity, 0);
4274
4293
  const depthImbalance = totalBidVolume + totalAskVolume > 0
4275
4294
  ? (totalBidVolume - totalAskVolume) / (totalBidVolume + totalAskVolume)
4276
- : 0;
4295
+ : null;
4277
4296
  return {
4278
4297
  symbol,
4279
4298
  timestamp: new Date().toISOString(),
@@ -5026,7 +5045,7 @@ const commitHistorySetup = async (symbol, history) => {
5026
5045
  await commitShortTermMath(symbol, history);
5027
5046
  await commitSwingTermMath(symbol, history);
5028
5047
  await commitLongTermMath(symbol, history);
5029
- const displayName = await String(symbol).toUpperCase();
5048
+ const displayName = String(symbol).toUpperCase();
5030
5049
  const currentPrice = await backtestKit.getAveragePrice(symbol);
5031
5050
  const currentData = await backtestKit.getDate();
5032
5051
  await history.push({