@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.mjs CHANGED
@@ -138,13 +138,6 @@ const columns$3 = [
138
138
  ? `${await formatPrice(symbol, Number(v))} USD`
139
139
  : "N/A",
140
140
  },
141
- {
142
- key: "atr14_raw",
143
- label: "ATR(14) Raw",
144
- format: async (v, symbol) => v !== null
145
- ? `${await formatPrice(symbol, Number(v))} USD`
146
- : "N/A",
147
- },
148
141
  {
149
142
  key: "atr20",
150
143
  label: "ATR(20)",
@@ -207,6 +200,11 @@ const columns$3 = [
207
200
  ? `${await formatPrice(symbol, Number(v))} USD`
208
201
  : "N/A",
209
202
  },
203
+ {
204
+ key: "volumeTrendRatio",
205
+ label: "Volume Trend Ratio",
206
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
207
+ },
210
208
  {
211
209
  key: "currentPrice",
212
210
  label: "Current Price",
@@ -341,8 +339,8 @@ function calculateFibonacciLevels$2(candles, endIndex) {
341
339
  "61.8%": high - range * 0.618,
342
340
  "78.6%": high - range * 0.786,
343
341
  "100.0%": low,
344
- "127.2%": high - range * 1.272,
345
- "161.8%": high - range * 1.618,
342
+ "127.2% (downside)": high - range * 1.272,
343
+ "161.8% (downside)": high - range * 1.618,
346
344
  };
347
345
  const currentPrice = Number(candles[endIndex].close);
348
346
  let nearestLevel = {
@@ -436,20 +434,19 @@ function generateAnalysis$3(symbol, candles) {
436
434
  if (i < WARMUP_PERIOD$3) {
437
435
  return;
438
436
  }
439
- // Volume trend calculation
440
- const volumeSma6 = new FasterSMA(6);
441
- const volumeSma6Prev = new FasterSMA(6);
437
+ // Volume trend calculation: average of last 6 candles vs previous 6
442
438
  const volumeStart = Math.max(0, i + 1 - 12);
443
439
  const prevVolumeData = volumes.slice(volumeStart, Math.min(volumeStart + 6, i + 1));
444
440
  const recentVolumeData = volumes.slice(Math.max(0, i + 1 - 6), i + 1);
445
- if (prevVolumeData.length > 0) {
446
- prevVolumeData.forEach((vol) => volumeSma6Prev.update(vol, false));
447
- }
448
- if (recentVolumeData.length > 0) {
449
- recentVolumeData.forEach((vol) => volumeSma6.update(vol, false));
450
- }
451
- const recentVolumeRaw = volumeSma6.getResult();
452
- const prevVolumeRaw = volumeSma6Prev.getResult();
441
+ const averageOf = (values) => {
442
+ const safe = values.filter((value) => !isUnsafe$4(value));
443
+ if (safe.length === 0) {
444
+ return null;
445
+ }
446
+ return safe.reduce((sum, value) => sum + value, 0) / safe.length;
447
+ };
448
+ const recentVolumeRaw = averageOf(recentVolumeData);
449
+ const prevVolumeRaw = averageOf(prevVolumeData);
453
450
  const recentVolume = !isUnsafe$4(recentVolumeRaw)
454
451
  ? recentVolumeRaw
455
452
  : volumes[i];
@@ -501,9 +498,6 @@ function generateAnalysis$3(symbol, candles) {
501
498
  atr14: atr14.getResult() != null && !isUnsafe$4(atr14.getResult())
502
499
  ? atr14.getResult()
503
500
  : null,
504
- atr14_raw: atr14.getResult() != null && !isUnsafe$4(atr14.getResult())
505
- ? atr14.getResult()
506
- : null,
507
501
  atr20: atr20.getResult() != null && !isUnsafe$4(atr20.getResult())
508
502
  ? atr20.getResult()
509
503
  : null,
@@ -554,11 +548,12 @@ function generateAnalysis$3(symbol, candles) {
554
548
  fibonacciDistance: fibonacciNearest.distance,
555
549
  bodySize,
556
550
  closePrice: close,
557
- date: new Date(),
551
+ date: new Date(_candle.timestamp),
558
552
  lookbackPeriod: "48 candles (48 hours) with SMA(50) from 100 hours",
559
553
  });
560
554
  });
561
- return results;
555
+ // Return only the last TABLE_ROWS_LIMIT rows
556
+ return results.slice(-TABLE_ROWS_LIMIT$3);
562
557
  }
563
558
  /**
564
559
  * Generates markdown table with long-term technical analysis history.
@@ -621,9 +616,7 @@ async function generateHistoryTable$3(indicators, symbol) {
621
616
  markdown +=
622
617
  "- **ATR(14)**: over previous 14 candles (14 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞)\n";
623
618
  markdown +=
624
- "- **ATR(14) Raw**: raw value over previous 14 candles before row timestamp (Min: 0 USD, Max: +∞)\n";
625
- markdown +=
626
- "- **ATR(20) Raw**: raw value over previous 20 candles (20 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞)\n";
619
+ "- **ATR(20)**: over previous 20 candles (20 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞)\n";
627
620
  markdown +=
628
621
  "- **CCI(20)**: over previous 20 candles (20 hours on 1h timeframe) before row timestamp (Min: -∞, Max: +∞)\n";
629
622
  markdown +=
@@ -648,12 +641,14 @@ async function generateHistoryTable$3(indicators, symbol) {
648
641
  "- **EMA(34)**: over previous 34 candles (34 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
649
642
  markdown +=
650
643
  "- **Momentum(10)**: over previous 10 candles (10 hours on 1h timeframe) before row timestamp (Min: -∞ USD, Max: +∞ USD)\n";
644
+ markdown +=
645
+ "- **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";
651
646
  markdown +=
652
647
  "- **Support**: over previous 4 candles (4 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
653
648
  markdown +=
654
649
  "- **Resistance**: over previous 4 candles (4 hours on 1h timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
655
650
  markdown +=
656
- "- **Fibonacci Nearest Level**: nearest level name before row timestamp\n";
651
+ "- **Fibonacci Nearest Level**: nearest level name before row timestamp; levels marked (downside) lie below the range low\n";
657
652
  markdown +=
658
653
  "- **Fibonacci Nearest Price**: nearest price level before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
659
654
  markdown +=
@@ -768,9 +763,7 @@ class LongTermHistoryService {
768
763
  this.getReport = async (symbol) => {
769
764
  this.loggerService.log("longTermHistoryService getReport", { symbol });
770
765
  const fullCandles = await getCandles(symbol, "1h", 100);
771
- // Use all candles for indicator warm-up, then filter to last TABLE_ROWS_LIMIT rows
772
- const allRows = await this.getData(symbol, fullCandles);
773
- const rows = allRows.slice(-TABLE_ROWS_LIMIT$3);
766
+ const rows = await this.getData(symbol, fullCandles);
774
767
  return generateHistoryTable$3(rows, symbol);
775
768
  };
776
769
  /**
@@ -1066,6 +1059,11 @@ const columns$2 = [
1066
1059
  label: "Volume Ratio",
1067
1060
  format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
1068
1061
  },
1062
+ {
1063
+ key: "volumeTrendRatio",
1064
+ label: "Volume Trend Ratio",
1065
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
1066
+ },
1069
1067
  {
1070
1068
  key: "volatility5",
1071
1069
  label: "Volatility(5)",
@@ -1155,9 +1153,11 @@ function calculateVolumeMetrics(candles, endIndex) {
1155
1153
  const volumeSma5 = new FasterSMA(5);
1156
1154
  volumes.forEach((vol) => volumeSma5.update(vol, false));
1157
1155
  const avgVolumeRaw = volumeSma5.getResult();
1158
- const avgVolume = !isUnsafe$3(avgVolumeRaw) ? avgVolumeRaw : 0;
1156
+ const avgVolume = !isUnsafe$3(avgVolumeRaw) ? avgVolumeRaw : null;
1159
1157
  const currentVolume = volumes[volumes.length - 1];
1160
- const volumeRatio = avgVolume > 0 && !isUnsafe$3(currentVolume) ? currentVolume / avgVolume : 1;
1158
+ const volumeRatio = avgVolume !== null && avgVolume > 0 && !isUnsafe$3(currentVolume)
1159
+ ? currentVolume / avgVolume
1160
+ : null;
1161
1161
  let volumeTrendRatio = null;
1162
1162
  if (volumes.length >= 6) {
1163
1163
  const recent3 = volumes.slice(-3);
@@ -1194,15 +1194,15 @@ function calculatePriceChanges(candles, endIndex) {
1194
1194
  return { priceChange1m: null, priceChange3m: null, priceChange5m: null };
1195
1195
  }
1196
1196
  const current = closes[closes.length - 1];
1197
- const priceChange1m = closes.length >= 2
1197
+ const priceChange1m = closes.length >= 2 && closes[closes.length - 2] > 0
1198
1198
  ? ((current - closes[closes.length - 2]) / closes[closes.length - 2]) *
1199
1199
  100
1200
1200
  : null;
1201
- const priceChange3m = closes.length >= 4
1201
+ const priceChange3m = closes.length >= 4 && closes[closes.length - 4] > 0
1202
1202
  ? ((current - closes[closes.length - 4]) / closes[closes.length - 4]) *
1203
1203
  100
1204
1204
  : null;
1205
- const priceChange5m = closes.length >= 6
1205
+ const priceChange5m = closes.length >= 6 && closes[closes.length - 6] > 0
1206
1206
  ? ((current - closes[closes.length - 6]) / closes[closes.length - 6]) *
1207
1207
  100
1208
1208
  : null;
@@ -1348,9 +1348,9 @@ function generateAnalysis$2(symbol, candles) {
1348
1348
  sma8.update(close, false);
1349
1349
  dema8.update(close, false);
1350
1350
  wma5.update(close, false);
1351
- // Determine minimum warm-up period needed (largest indicator period)
1352
- // EMA(21) is the largest period
1353
- // Skip rows until all indicators are warmed up
1351
+ // Skip rows until the primary indicators are warmed up.
1352
+ // WARMUP_PERIOD covers EMA(21); slower indicators (e.g. StochRSI(14)
1353
+ // needs ~28 candles) render as N/A in the first rows until ready.
1354
1354
  if (i < WARMUP_PERIOD$2) {
1355
1355
  return;
1356
1356
  }
@@ -1521,7 +1521,7 @@ function generateAnalysis$2(symbol, candles) {
1521
1521
  squeezeMomentum,
1522
1522
  pressureIndex,
1523
1523
  closePrice: close,
1524
- date: new Date(),
1524
+ date: new Date(_candle.timestamp),
1525
1525
  lookbackPeriod: "60 candles (60 minutes)",
1526
1526
  });
1527
1527
  });
@@ -1607,7 +1607,7 @@ async function generateHistoryTable$2(indicators, symbol) {
1607
1607
  markdown +=
1608
1608
  "- **Bollinger Width(8,2.0)**: width percentage before row timestamp (Min: 0%, Max: +∞)\n";
1609
1609
  markdown +=
1610
- "- **Bollinger Position**: price position within bands before row timestamp (Min: 0%, Max: 100%)\n";
1610
+ "- **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";
1611
1611
  markdown +=
1612
1612
  "- **Stochastic K(3,3,3)**: over previous 3 candles (3 minutes on 1m timeframe) before row timestamp (Min: 0, Max: 100)\n";
1613
1613
  markdown +=
@@ -1644,6 +1644,8 @@ async function generateHistoryTable$2(indicators, symbol) {
1644
1644
  "- **Volume SMA(5)**: over previous 5 candles (5 minutes on 1m timeframe) before row timestamp (Min: 0, Max: +∞)\n";
1645
1645
  markdown +=
1646
1646
  "- **Volume Ratio**: volume relative to average at row timestamp (Min: 0x, Max: +∞x)\n";
1647
+ markdown +=
1648
+ "- **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";
1647
1649
  markdown +=
1648
1650
  "- **Support**: over previous 30 candles (30 minutes on 1m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
1649
1651
  markdown +=
@@ -1894,6 +1896,11 @@ const columns$1 = [
1894
1896
  label: "ROC(10) - Rate of Change %",
1895
1897
  format: (v) => (v !== null ? `${Number(v).toFixed(3)}%` : "N/A"),
1896
1898
  },
1899
+ {
1900
+ key: "volumeTrendRatio",
1901
+ label: "Volume Trend Ratio",
1902
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}x` : "N/A"),
1903
+ },
1897
1904
  {
1898
1905
  key: "sma50",
1899
1906
  label: "SMA(50)",
@@ -2068,8 +2075,8 @@ function calculateFibonacciLevels$1(candles, endIndex) {
2068
2075
  "61.8%": high - range * 0.618,
2069
2076
  "78.6%": high - range * 0.786,
2070
2077
  "100.0%": low,
2071
- "127.2%": high - range * 1.272,
2072
- "161.8%": high - range * 1.618,
2078
+ "127.2% (downside)": high - range * 1.272,
2079
+ "161.8% (downside)": high - range * 1.618,
2073
2080
  };
2074
2081
  const currentPrice = Number(candles[endIndex].close);
2075
2082
  let nearestLevel = {
@@ -2248,7 +2255,8 @@ function generateAnalysis$1(symbol, candles) {
2248
2255
  bollingerWidth10_2: bollingerResult &&
2249
2256
  !isUnsafe$2(bollingerResult.upper) &&
2250
2257
  !isUnsafe$2(bollingerResult.lower) &&
2251
- !isUnsafe$2(bollingerResult.middle)
2258
+ !isUnsafe$2(bollingerResult.middle) &&
2259
+ bollingerResult.middle !== 0
2252
2260
  ? ((bollingerResult.upper - bollingerResult.lower) /
2253
2261
  bollingerResult.middle) *
2254
2262
  100
@@ -2309,7 +2317,7 @@ function generateAnalysis$1(symbol, candles) {
2309
2317
  fibonacciDistance: fibonacciNearest.distance,
2310
2318
  bodySize,
2311
2319
  closePrice: close,
2312
- date: new Date(),
2320
+ date: new Date(_candle.timestamp),
2313
2321
  lookbackPeriod: "144 candles (36 hours)",
2314
2322
  });
2315
2323
  });
@@ -2396,6 +2404,8 @@ async function generateHistoryTable$1(indicators, symbol) {
2396
2404
  "- **ROC(5)**: over previous 5 candles (75 minutes on 15m timeframe) before row timestamp (Min: -∞%, Max: +∞%)\n";
2397
2405
  markdown +=
2398
2406
  "- **ROC(10)**: over previous 10 candles (150 minutes on 15m timeframe) before row timestamp (Min: -∞%, Max: +∞%)\n";
2407
+ markdown +=
2408
+ "- **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";
2399
2409
  markdown +=
2400
2410
  "- **SMA(50)**: over previous 50 candles (750 minutes on 15m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2401
2411
  markdown +=
@@ -2411,9 +2421,9 @@ async function generateHistoryTable$1(indicators, symbol) {
2411
2421
  markdown +=
2412
2422
  "- **Resistance**: over previous 48 candles (12 hours on 15m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2413
2423
  markdown +=
2414
- "- **Fibonacci Nearest Level**: nearest level name before row timestamp\n";
2424
+ "- **Fibonacci Nearest Level**: nearest level name before row timestamp; levels marked (downside) lie below the range low\n";
2415
2425
  markdown +=
2416
- "- **Fibonacci Nearest Price**: nearest price level over 288 candles (72h on 15m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2426
+ "- **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";
2417
2427
  markdown +=
2418
2428
  "- **Fibonacci Distance**: distance to nearest level before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
2419
2429
  markdown +=
@@ -2568,6 +2578,11 @@ const TABLE_ROWS_LIMIT = 30;
2568
2578
  * Ensures all technical indicators (especially EMA(34)) have sufficient data.
2569
2579
  */
2570
2580
  const WARMUP_PERIOD = 34;
2581
+ /**
2582
+ * Rolling window (in candles) for the volatility calculation.
2583
+ * Keeps the metric comparable across rows regardless of total history length.
2584
+ */
2585
+ const VOLATILITY_WINDOW = 20;
2571
2586
  const columns = [
2572
2587
  {
2573
2588
  key: "rsi14",
@@ -3001,16 +3016,20 @@ function generateAnalysis(symbol, candles) {
3001
3016
  if (i < WARMUP_PERIOD) {
3002
3017
  return;
3003
3018
  }
3004
- const priceChanges = closes
3005
- .slice(0, i + 1)
3006
- .slice(1)
3007
- .map((price, idx) => {
3008
- const prevPrice = closes.slice(0, i + 1)[idx];
3009
- return ((price - prevPrice) / prevPrice) * 100;
3010
- });
3011
- const volatility = priceChanges.length > 0
3012
- ? Math.sqrt(priceChanges.reduce((sum, change) => sum + change ** 2, 0) /
3013
- priceChanges.length)
3019
+ let volatilitySumSq = 0;
3020
+ let volatilityCount = 0;
3021
+ for (let idx = Math.max(1, i + 1 - VOLATILITY_WINDOW); idx <= i; idx++) {
3022
+ const prevPrice = closes[idx - 1];
3023
+ const currPrice = closes[idx];
3024
+ if (isUnsafe$1(prevPrice) || isUnsafe$1(currPrice) || prevPrice <= 0) {
3025
+ continue;
3026
+ }
3027
+ const change = ((currPrice - prevPrice) / prevPrice) * 100;
3028
+ volatilitySumSq += change ** 2;
3029
+ volatilityCount += 1;
3030
+ }
3031
+ const volatility = volatilityCount > 0
3032
+ ? Math.sqrt(volatilitySumSq / volatilityCount)
3014
3033
  : null;
3015
3034
  const { support, resistance } = calculateSupportResistance(candles, i);
3016
3035
  const fibonacci = calculateFibonacciLevels(candles, i);
@@ -3106,7 +3125,7 @@ function generateAnalysis(symbol, candles) {
3106
3125
  fibonacciPositionPercent: fibonacci.fibonacciPositionPercent,
3107
3126
  bodySize,
3108
3127
  closePrice: close,
3109
- date: new Date(),
3128
+ date: new Date(_candle.timestamp),
3110
3129
  lookbackPeriod: "96 candles (48 hours)",
3111
3130
  });
3112
3131
  });
@@ -3220,7 +3239,7 @@ async function generateHistoryTable(indicators, symbol) {
3220
3239
  markdown +=
3221
3240
  "- **Volume**: trading volume at row timestamp (Min: 0, Max: +∞)\n";
3222
3241
  markdown +=
3223
- "- **Volatility**: volatility percentage at row timestamp (Min: 0%, Max: +∞)\n";
3242
+ "- **Volatility**: RMS of 1-candle percentage changes over previous 20 candles (10 hours on 30m timeframe) before row timestamp (Min: 0%, Max: +∞)\n";
3224
3243
  markdown += "\n## Column Descriptions\n\n";
3225
3244
  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";
3226
3245
  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";
@@ -3625,11 +3644,11 @@ class HourCandleHistoryService {
3625
3644
  markdown += `### 1h Candle ${index + 1}\n`;
3626
3645
  markdown += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3627
3646
  markdown += `- **Time**: ${formattedTime}\n`;
3628
- markdown += `- **Open**: ${formatPrice(symbol, candle.open)} USD\n`;
3629
- markdown += `- **High**: ${formatPrice(symbol, candle.high)} USD\n`;
3630
- markdown += `- **Low**: ${formatPrice(symbol, candle.low)} USD\n`;
3631
- markdown += `- **Close**: ${formatPrice(symbol, candle.close)} USD\n`;
3632
- markdown += `- **Volume**: ${formatQuantity(symbol, candle.volume)}\n`;
3647
+ markdown += `- **Open**: ${await formatPrice(symbol, candle.open)} USD\n`;
3648
+ markdown += `- **High**: ${await formatPrice(symbol, candle.high)} USD\n`;
3649
+ markdown += `- **Low**: ${await formatPrice(symbol, candle.low)} USD\n`;
3650
+ markdown += `- **Close**: ${await formatPrice(symbol, candle.close)} USD\n`;
3651
+ markdown += `- **Volume**: ${await formatQuantity(symbol, candle.volume)}\n`;
3633
3652
  markdown += `- **1h Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3634
3653
  markdown += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3635
3654
  }
@@ -3779,7 +3798,7 @@ class OneMinuteCandleHistoryService {
3779
3798
  markdown += `- **High**: ${await formatPrice(symbol, candle.high)} USD\n`;
3780
3799
  markdown += `- **Low**: ${await formatPrice(symbol, candle.low)} USD\n`;
3781
3800
  markdown += `- **Close**: ${await formatPrice(symbol, candle.close)} USD\n`;
3782
- markdown += `- **Volume**: ${formatQuantity(symbol, candle.volume)}\n`;
3801
+ markdown += `- **Volume**: ${await formatQuantity(symbol, candle.volume)}\n`;
3783
3802
  markdown += `- **1m Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3784
3803
  markdown += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3785
3804
  }
@@ -3929,7 +3948,7 @@ class ThirtyMinuteCandleHistoryService {
3929
3948
  report += `- **High**: ${await formatPrice(symbol, candle.high)} USD\n`;
3930
3949
  report += `- **Low**: ${await formatPrice(symbol, candle.low)} USD\n`;
3931
3950
  report += `- **Close**: ${await formatPrice(symbol, candle.close)} USD\n`;
3932
- report += `- **Volume**: ${formatQuantity(symbol, candle.volume)}\n`;
3951
+ report += `- **Volume**: ${await formatQuantity(symbol, candle.volume)}\n`;
3933
3952
  report += `- **30m Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3934
3953
  report += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3935
3954
  }
@@ -4260,18 +4279,18 @@ class BookDataMathService {
4260
4279
  });
4261
4280
  const depth = await getOrderBook(symbol, MAX_DEPTH_LEVELS);
4262
4281
  // Just process raw data - no calculations
4263
- const bids = processOrderBookSide(depth.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))); // Сортировка по убыванию
4264
- const asks = processOrderBookSide(depth.asks.sort((a, b) => parseFloat(a.price) - parseFloat(b.price))); // Сортировка по возрастанию
4265
- const bestBid = bids.length > 0 ? bids[0].price : 0;
4266
- const bestAsk = asks.length > 0 ? asks[0].price : 0;
4267
- const midPrice = (bestBid + bestAsk) / 2;
4268
- const spread = bestAsk - bestBid;
4282
+ const bids = processOrderBookSide([...depth.bids].sort((a, b) => parseFloat(b.price) - parseFloat(a.price))); // Сортировка по убыванию
4283
+ const asks = processOrderBookSide([...depth.asks].sort((a, b) => parseFloat(a.price) - parseFloat(b.price))); // Сортировка по возрастанию
4284
+ const bestBid = bids.length > 0 ? bids[0].price : null;
4285
+ const bestAsk = asks.length > 0 ? asks[0].price : null;
4286
+ const midPrice = bestBid !== null && bestAsk !== null ? (bestBid + bestAsk) / 2 : null;
4287
+ const spread = bestBid !== null && bestAsk !== null ? bestAsk - bestBid : null;
4269
4288
  // Calculate depth imbalance
4270
4289
  const totalBidVolume = bids.reduce((sum, bid) => sum + bid.quantity, 0);
4271
4290
  const totalAskVolume = asks.reduce((sum, ask) => sum + ask.quantity, 0);
4272
4291
  const depthImbalance = totalBidVolume + totalAskVolume > 0
4273
4292
  ? (totalBidVolume - totalAskVolume) / (totalBidVolume + totalAskVolume)
4274
- : 0;
4293
+ : null;
4275
4294
  return {
4276
4295
  symbol,
4277
4296
  timestamp: new Date().toISOString(),
@@ -5024,7 +5043,7 @@ const commitHistorySetup = async (symbol, history) => {
5024
5043
  await commitShortTermMath(symbol, history);
5025
5044
  await commitSwingTermMath(symbol, history);
5026
5045
  await commitLongTermMath(symbol, history);
5027
- const displayName = await String(symbol).toUpperCase();
5046
+ const displayName = String(symbol).toUpperCase();
5028
5047
  const currentPrice = await getAveragePrice(symbol);
5029
5048
  const currentData = await getDate();
5030
5049
  await history.push({
package/package.json CHANGED
@@ -1,87 +1,87 @@
1
- {
2
- "name": "@backtest-kit/signals",
3
- "version": "14.0.0",
4
- "description": "Technical analysis and trading signal generation library for AI-powered trading systems. Computes 50+ indicators across 4 timeframes and generates markdown reports for LLM consumption.",
5
- "author": {
6
- "name": "Petr Tripolsky",
7
- "email": "tripolskypetr@gmail.com",
8
- "url": "https://github.com/tripolskypetr"
9
- },
10
- "funding": {
11
- "type": "individual",
12
- "url": "http://paypal.me/tripolskypetr"
13
- },
14
- "license": "MIT",
15
- "homepage": "https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html",
16
- "keywords": [
17
- "framework",
18
- "technical-analysis",
19
- "trading-bot",
20
- "algorithmic-trading",
21
- "quantitative-trading",
22
- "stock-market",
23
- "cryptocurrency",
24
- "forex",
25
- "strategy-testing",
26
- "portfolio-management",
27
- "market-data",
28
- "ohlcv",
29
- "candlestick",
30
- "time-series"
31
- ],
32
- "files": [
33
- "build",
34
- "types.d.ts",
35
- "README.md"
36
- ],
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/tripolskypetr/backtest-kit",
40
- "documentation": "https://github.com/tripolskypetr/backtest-kit/tree/master/docs"
41
- },
42
- "bugs": {
43
- "url": "https://github.com/tripolskypetr/backtest-kit/issues"
44
- },
45
- "scripts": {
46
- "build": "rollup -c"
47
- },
48
- "main": "build/index.cjs",
49
- "module": "build/index.mjs",
50
- "source": "src/index.ts",
51
- "types": "./types.d.ts",
52
- "exports": {
53
- "require": "./build/index.cjs",
54
- "types": "./types.d.ts",
55
- "import": "./build/index.mjs",
56
- "default": "./build/index.cjs"
57
- },
58
- "devDependencies": {
59
- "@rollup/plugin-typescript": "11.1.6",
60
- "@types/node": "22.9.0",
61
- "glob": "11.0.1",
62
- "rimraf": "6.0.1",
63
- "rollup": "3.29.5",
64
- "rollup-plugin-dts": "6.1.1",
65
- "rollup-plugin-peer-deps-external": "2.2.4",
66
- "ts-morph": "27.0.2",
67
- "tslib": "2.7.0",
68
- "typedoc": "0.27.9",
69
- "backtest-kit": "14.0.0",
70
- "worker-testbed": "2.1.0"
71
- },
72
- "peerDependencies": {
73
- "backtest-kit": "^14.0.0",
74
- "typescript": "^5.0.0"
75
- },
76
- "dependencies": {
77
- "di-kit": "^1.1.1",
78
- "di-scoped": "^1.0.21",
79
- "functools-kit": "^3.0.0",
80
- "trading-signals": "^6.9.1",
81
- "get-moment-stamp": "^2.0.0",
82
- "agent-swarm-kit": "^3.2.0"
83
- },
84
- "publishConfig": {
85
- "access": "public"
86
- }
87
- }
1
+ {
2
+ "name": "@backtest-kit/signals",
3
+ "version": "15.0.0",
4
+ "description": "Technical analysis and trading signal generation library for AI-powered trading systems. Computes 50+ indicators across 4 timeframes and generates markdown reports for LLM consumption.",
5
+ "author": {
6
+ "name": "Petr Tripolsky",
7
+ "email": "tripolskypetr@gmail.com",
8
+ "url": "https://github.com/tripolskypetr"
9
+ },
10
+ "funding": {
11
+ "type": "individual",
12
+ "url": "http://paypal.me/tripolskypetr"
13
+ },
14
+ "license": "MIT",
15
+ "homepage": "https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html",
16
+ "keywords": [
17
+ "framework",
18
+ "technical-analysis",
19
+ "trading-bot",
20
+ "algorithmic-trading",
21
+ "quantitative-trading",
22
+ "stock-market",
23
+ "cryptocurrency",
24
+ "forex",
25
+ "strategy-testing",
26
+ "portfolio-management",
27
+ "market-data",
28
+ "ohlcv",
29
+ "candlestick",
30
+ "time-series"
31
+ ],
32
+ "files": [
33
+ "build",
34
+ "types.d.ts",
35
+ "README.md"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/tripolskypetr/backtest-kit",
40
+ "documentation": "https://github.com/tripolskypetr/backtest-kit/tree/master/docs"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/tripolskypetr/backtest-kit/issues"
44
+ },
45
+ "scripts": {
46
+ "build": "rollup -c"
47
+ },
48
+ "main": "build/index.cjs",
49
+ "module": "build/index.mjs",
50
+ "source": "src/index.ts",
51
+ "types": "./types.d.ts",
52
+ "exports": {
53
+ "require": "./build/index.cjs",
54
+ "types": "./types.d.ts",
55
+ "import": "./build/index.mjs",
56
+ "default": "./build/index.cjs"
57
+ },
58
+ "devDependencies": {
59
+ "@rollup/plugin-typescript": "11.1.6",
60
+ "@types/node": "22.9.0",
61
+ "glob": "11.0.1",
62
+ "rimraf": "6.0.1",
63
+ "rollup": "3.29.5",
64
+ "rollup-plugin-dts": "6.1.1",
65
+ "rollup-plugin-peer-deps-external": "2.2.4",
66
+ "ts-morph": "27.0.2",
67
+ "tslib": "2.7.0",
68
+ "typedoc": "0.27.9",
69
+ "backtest-kit": "15.0.0",
70
+ "worker-testbed": "3.0.0"
71
+ },
72
+ "peerDependencies": {
73
+ "backtest-kit": "^15.0.0",
74
+ "typescript": "^5.0.0"
75
+ },
76
+ "dependencies": {
77
+ "di-kit": "^1.1.1",
78
+ "di-scoped": "^1.0.21",
79
+ "functools-kit": "^4.0.0",
80
+ "trading-signals": "^6.9.1",
81
+ "get-moment-stamp": "^2.0.0",
82
+ "agent-swarm-kit": "^4.0.0"
83
+ },
84
+ "publishConfig": {
85
+ "access": "public"
86
+ }
87
+ }
package/types.d.ts CHANGED
@@ -632,7 +632,6 @@ interface ILongTermRow {
632
632
  pdi14: number | null;
633
633
  ndi14: number | null;
634
634
  atr14: number | null;
635
- atr14_raw: number | null;
636
635
  atr20: number | null;
637
636
  cci20: number | null;
638
637
  bollinger20_2_upper: number | null;
@@ -1543,16 +1542,16 @@ interface IBookDataAnalysis {
1543
1542
  bids: IOrderBookEntry[];
1544
1543
  /** Ask (sell) levels with percentages */
1545
1544
  asks: IOrderBookEntry[];
1546
- /** Highest bid price */
1547
- bestBid: number;
1548
- /** Lowest ask price */
1549
- bestAsk: number;
1550
- /** Mid price: (bestBid + bestAsk) / 2 */
1551
- midPrice: number;
1552
- /** Spread: bestAsk - bestBid */
1553
- spread: number;
1554
- /** Depth imbalance: (bidVol - askVol) / (bidVol + askVol) */
1555
- depthImbalance: number;
1545
+ /** Highest bid price (null when the book side is empty) */
1546
+ bestBid: number | null;
1547
+ /** Lowest ask price (null when the book side is empty) */
1548
+ bestAsk: number | null;
1549
+ /** Mid price: (bestBid + bestAsk) / 2 (null when either side is empty) */
1550
+ midPrice: number | null;
1551
+ /** Spread: bestAsk - bestBid (null when either side is empty) */
1552
+ spread: number | null;
1553
+ /** Depth imbalance: (bidVol - askVol) / (bidVol + askVol) (null when the book is empty) */
1554
+ depthImbalance: number | null;
1556
1555
  }
1557
1556
  /**
1558
1557
  * Service for order book analysis and markdown report generation.