@backtest-kit/signals 0.0.4 → 0.0.6

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
@@ -458,13 +458,9 @@ function generateAnalysis$3(symbol, candles) {
458
458
  const prevVolume = !isUnsafe$4(prevVolumeRaw)
459
459
  ? prevVolumeRaw
460
460
  : volumes[Math.max(0, i - 6)];
461
- const volumeTrend = !isUnsafe$4(recentVolume) && !isUnsafe$4(prevVolume)
462
- ? recentVolume > prevVolume * 1.1
463
- ? "increasing"
464
- : recentVolume < prevVolume * 0.9
465
- ? "decreasing"
466
- : "stable"
467
- : "stable";
461
+ const volumeTrendRatio = !isUnsafe$4(recentVolume) && !isUnsafe$4(prevVolume) && prevVolume > 0
462
+ ? recentVolume / prevVolume
463
+ : null;
468
464
  // Support/Resistance calculation
469
465
  const pivotPeriod = Math.min(4, i + 1);
470
466
  const startIdx = i + 1 - pivotPeriod;
@@ -543,7 +539,7 @@ function generateAnalysis$3(symbol, candles) {
543
539
  cci20: cci.getResult() != null && !isUnsafe$4(cci.getResult())
544
540
  ? cci.getResult()
545
541
  : null,
546
- volumeTrend,
542
+ volumeTrendRatio,
547
543
  support: support != null && !isUnsafe$4(support)
548
544
  ? support
549
545
  : !isUnsafe$4(currentPrice)
@@ -1120,26 +1116,26 @@ function isUnsafe$3(value) {
1120
1116
  /**
1121
1117
  * Calculates volume metrics including SMA, ratio, and trend.
1122
1118
  *
1123
- * Computes volume SMA(5), current volume to average ratio, and volume trend
1124
- * by comparing recent 3 candles to previous 3 candles. Returns "increasing"
1125
- * if recent average > 120% of previous, "decreasing" if < 80%, else "stable".
1119
+ * Computes volume SMA(5), current volume to average ratio, and volume trend ratio
1120
+ * by comparing recent 3 candles to previous 3 candles. Returns numeric ratio
1121
+ * of recent volume average to previous volume average (e.g., 1.2 = 20% increase).
1126
1122
  *
1127
1123
  * @param candles - Array of candle data
1128
1124
  * @param endIndex - Index of current candle in array
1129
- * @returns Object with volumeSma5, volumeRatio, and volumeTrend
1125
+ * @returns Object with volumeSma5, volumeRatio, and volumeTrendRatio
1130
1126
  *
1131
1127
  * @example
1132
1128
  * ```typescript
1133
1129
  * const candles = await getCandles('BTCUSDT', '1m', 60);
1134
1130
  * const metrics = calculateVolumeMetrics(candles, 59);
1135
1131
  * console.log(metrics);
1136
- * // { volumeSma5: 1500000, volumeRatio: 1.25, volumeTrend: "increasing" }
1132
+ * // { volumeSma5: 1500000, volumeRatio: 1.25, volumeTrendRatio: 1.15 }
1137
1133
  * ```
1138
1134
  */
1139
1135
  function calculateVolumeMetrics(candles, endIndex) {
1140
1136
  const volumes = candles.slice(0, endIndex + 1).map((c) => Number(c.volume));
1141
1137
  if (volumes.length < 5) {
1142
- return { volumeSma5: null, volumeRatio: null, volumeTrend: "stable" };
1138
+ return { volumeSma5: null, volumeRatio: null, volumeTrendRatio: null };
1143
1139
  }
1144
1140
  const volumeSma5 = new tradingSignals.FasterSMA(5);
1145
1141
  volumes.forEach((vol) => volumeSma5.update(vol, false));
@@ -1147,20 +1143,17 @@ function calculateVolumeMetrics(candles, endIndex) {
1147
1143
  const avgVolume = !isUnsafe$3(avgVolumeRaw) ? avgVolumeRaw : 0;
1148
1144
  const currentVolume = volumes[volumes.length - 1];
1149
1145
  const volumeRatio = avgVolume > 0 && !isUnsafe$3(currentVolume) ? currentVolume / avgVolume : 1;
1150
- let volumeTrend = "stable";
1146
+ let volumeTrendRatio = null;
1151
1147
  if (volumes.length >= 6) {
1152
1148
  const recent3 = volumes.slice(-3);
1153
1149
  const prev3 = volumes.slice(-6, -3);
1154
1150
  if (prev3.length >= 3) {
1155
1151
  const recentAvg = recent3.reduce((a, b) => a + b, 0) / 3;
1156
1152
  const prevAvg = prev3.reduce((a, b) => a + b, 0) / 3;
1157
- if (recentAvg > prevAvg * 1.2)
1158
- volumeTrend = "increasing";
1159
- else if (recentAvg < prevAvg * 0.8)
1160
- volumeTrend = "decreasing";
1153
+ volumeTrendRatio = prevAvg > 0 ? recentAvg / prevAvg : null;
1161
1154
  }
1162
1155
  }
1163
- return { volumeSma5: avgVolume, volumeRatio, volumeTrend };
1156
+ return { volumeSma5: avgVolume, volumeRatio, volumeTrendRatio };
1164
1157
  }
1165
1158
  /**
1166
1159
  * Calculates price change percentages over 1, 3, and 5 minute periods.
@@ -1493,7 +1486,7 @@ function generateAnalysis$2(symbol, candles) {
1493
1486
  : null,
1494
1487
  volumeSma5: volumeMetrics.volumeSma5,
1495
1488
  volumeRatio: volumeMetrics.volumeRatio,
1496
- volumeTrend: volumeMetrics.volumeTrend,
1489
+ volumeTrendRatio: volumeMetrics.volumeTrendRatio,
1497
1490
  currentPrice: currentPrice != null && !isUnsafe$3(currentPrice) ? currentPrice : null,
1498
1491
  priceChange1m: priceChanges.priceChange1m,
1499
1492
  priceChange3m: priceChanges.priceChange3m,
@@ -2056,38 +2049,33 @@ function calculateFibonacciLevels$1(candles, endIndex) {
2056
2049
  return nearestLevel;
2057
2050
  }
2058
2051
  /**
2059
- * Analyzes volume trend by comparing recent vs older volume averages.
2052
+ * Calculates volume trend ratio by comparing recent vs older volume averages.
2060
2053
  *
2061
2054
  * Compares average volume of last 8 candles against previous 8 candles.
2062
- * Returns "increasing" if recent volume > 120% of older volume,
2063
- * "decreasing" if recent < 80% of older volume, otherwise "stable".
2055
+ * Returns ratio of recent volume to older volume (e.g., 1.2 means 20% increase).
2064
2056
  *
2065
2057
  * @param candles - Array of candle data
2066
2058
  * @param endIndex - Index of current candle in array
2067
- * @returns Volume trend: "increasing", "decreasing", or "stable"
2059
+ * @returns Volume trend ratio (recent/older), or null if insufficient data
2068
2060
  *
2069
2061
  * @example
2070
2062
  * ```typescript
2071
2063
  * const candles = await getCandles('ETHUSDT', '15m', 100);
2072
- * const trend = calculateVolumeTrend(candles, 99);
2073
- * console.log(trend); // "increasing" | "decreasing" | "stable"
2064
+ * const ratio = calculateVolumeTrendRatio(candles, 99);
2065
+ * console.log(ratio); // 1.25 (25% increase)
2074
2066
  * ```
2075
2067
  */
2076
- function calculateVolumeTrend(candles, endIndex) {
2068
+ function calculateVolumeTrendRatio(candles, endIndex) {
2077
2069
  const volumes = candles.slice(0, endIndex + 1).map((c) => Number(c.volume));
2078
2070
  if (volumes.length < 16)
2079
- return "stable";
2071
+ return null;
2080
2072
  const recentVolumes = volumes.slice(-8);
2081
2073
  const olderVolumes = volumes.slice(-16, -8);
2082
2074
  if (recentVolumes.length < 4 || olderVolumes.length < 4)
2083
- return "stable";
2075
+ return null;
2084
2076
  const recentAvg = recentVolumes.reduce((sum, vol) => sum + vol, 0) / recentVolumes.length;
2085
2077
  const olderAvg = olderVolumes.reduce((sum, vol) => sum + vol, 0) / olderVolumes.length;
2086
- if (recentAvg > olderAvg * 1.2)
2087
- return "increasing";
2088
- if (recentAvg < olderAvg * 0.8)
2089
- return "decreasing";
2090
- return "stable";
2078
+ return olderAvg > 0 ? recentAvg / olderAvg : null;
2091
2079
  }
2092
2080
  /**
2093
2081
  * Generates comprehensive technical analysis for 15-minute candles.
@@ -2168,7 +2156,7 @@ function generateAnalysis$1(symbol, candles) {
2168
2156
  if (i < WARMUP_PERIOD$1) {
2169
2157
  return;
2170
2158
  }
2171
- const volumeTrend = calculateVolumeTrend(candles, i);
2159
+ const volumeTrendRatio = calculateVolumeTrendRatio(candles, i);
2172
2160
  const pivotPeriod = Math.min(48, i + 1);
2173
2161
  const startIdx = i + 1 - pivotPeriod;
2174
2162
  const recentHighs = highs
@@ -2267,7 +2255,7 @@ function generateAnalysis$1(symbol, candles) {
2267
2255
  roc10: roc10.getResult() != null && !isUnsafe$2(roc10.getResult())
2268
2256
  ? roc10.getResult()
2269
2257
  : null,
2270
- volumeTrend,
2258
+ volumeTrendRatio,
2271
2259
  support: support != null && !isUnsafe$2(support)
2272
2260
  ? support
2273
2261
  : !isUnsafe$2(currentPrice)
@@ -2702,9 +2690,9 @@ const columns = [
2702
2690
  : "N/A",
2703
2691
  },
2704
2692
  {
2705
- key: "fibonacciCurrentLevel",
2706
- label: "Fibonacci Current Level",
2707
- format: (v) => String(v),
2693
+ key: "fibonacciPositionPercent",
2694
+ label: "Fibonacci Position %",
2695
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}%` : "N/A"),
2708
2696
  },
2709
2697
  {
2710
2698
  key: "bodySize",
@@ -2781,7 +2769,7 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2781
2769
  return {
2782
2770
  nearestSupport: null,
2783
2771
  nearestResistance: null,
2784
- currentLevel: "insufficient data",
2772
+ fibonacciPositionPercent: null,
2785
2773
  };
2786
2774
  }
2787
2775
  const startIdx = endIndex + 1 - period;
@@ -2796,13 +2784,15 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2796
2784
  return {
2797
2785
  nearestSupport: null,
2798
2786
  nearestResistance: null,
2799
- currentLevel: "insufficient data",
2787
+ fibonacciPositionPercent: null,
2800
2788
  };
2801
2789
  }
2802
2790
  const high = Math.max(...highs);
2803
2791
  const low = Math.min(...lows);
2804
2792
  const range = high - low;
2805
2793
  const currentPrice = Number(candles[endIndex].close);
2794
+ // Calculate position as percentage from low to high (0% = low, 100% = high)
2795
+ const fibonacciPositionPercent = range > 0 ? ((currentPrice - low) / range) * 100 : null;
2806
2796
  const retracement = {
2807
2797
  level0: high,
2808
2798
  level236: high - range * 0.236,
@@ -2817,35 +2807,6 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2817
2807
  level1618: high + range * 0.618,
2818
2808
  level2618: high + range * 1.618,
2819
2809
  };
2820
- const tolerance = range * 0.015;
2821
- let currentLevel = "between levels";
2822
- if (Math.abs(currentPrice - retracement.level0) < tolerance) {
2823
- currentLevel = "0.0% (High)";
2824
- }
2825
- else if (Math.abs(currentPrice - retracement.level236) < tolerance) {
2826
- currentLevel = "23.6% Retracement";
2827
- }
2828
- else if (Math.abs(currentPrice - retracement.level382) < tolerance) {
2829
- currentLevel = "38.2% Retracement";
2830
- }
2831
- else if (Math.abs(currentPrice - retracement.level500) < tolerance) {
2832
- currentLevel = "50.0% Retracement";
2833
- }
2834
- else if (Math.abs(currentPrice - retracement.level618) < tolerance) {
2835
- currentLevel = "61.8% Retracement";
2836
- }
2837
- else if (Math.abs(currentPrice - retracement.level786) < tolerance) {
2838
- currentLevel = "78.6% Retracement";
2839
- }
2840
- else if (Math.abs(currentPrice - retracement.level1000) < tolerance) {
2841
- currentLevel = "100% Retracement (Low)";
2842
- }
2843
- else if (currentPrice > retracement.level0) {
2844
- currentLevel = "Above high";
2845
- }
2846
- else if (currentPrice < retracement.level1000) {
2847
- currentLevel = "Below low";
2848
- }
2849
2810
  const allRetracementLevels = Object.values(retracement).filter((level) => level !== null);
2850
2811
  const allExtensionLevels = Object.values(extension).filter((level) => level !== null && level > 0);
2851
2812
  const resistanceLevels = [...allRetracementLevels, ...allExtensionLevels]
@@ -2859,7 +2820,7 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2859
2820
  return {
2860
2821
  nearestSupport,
2861
2822
  nearestResistance,
2862
- currentLevel,
2823
+ fibonacciPositionPercent,
2863
2824
  };
2864
2825
  }
2865
2826
  /**
@@ -2918,7 +2879,7 @@ function calculateSupportResistance(candles, endIndex, window = 20) {
2918
2879
  * const candles = await getCandles('BTCUSDT', '30m', 96);
2919
2880
  * const analysis = generateAnalysis('BTCUSDT', candles);
2920
2881
  * console.log(analysis[0].rsi14); // 52.45
2921
- * console.log(analysis[0].fibonacciCurrentLevel); // "50.0% Retracement"
2882
+ * console.log(analysis[0].fibonacciPositionPercent); // 50.25
2922
2883
  * ```
2923
2884
  */
2924
2885
  function generateAnalysis(symbol, candles) {
@@ -3087,7 +3048,7 @@ function generateAnalysis(symbol, candles) {
3087
3048
  : null,
3088
3049
  fibonacciNearestSupport: fibonacci.nearestSupport,
3089
3050
  fibonacciNearestResistance: fibonacci.nearestResistance,
3090
- fibonacciCurrentLevel: fibonacci.currentLevel,
3051
+ fibonacciPositionPercent: fibonacci.fibonacciPositionPercent,
3091
3052
  bodySize,
3092
3053
  closePrice: close,
3093
3054
  date: new Date(),
@@ -3194,7 +3155,7 @@ async function generateHistoryTable(indicators, symbol) {
3194
3155
  markdown +=
3195
3156
  "- **Fibonacci Nearest Resistance**: nearest resistance level over 48 candles (24h on 30m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
3196
3157
  markdown +=
3197
- "- **Fibonacci Current Level**: current level description before row timestamp\n";
3158
+ "- **Fibonacci Position %**: price position in high-low range before row timestamp (Min: 0%, Max: 100%+)\n";
3198
3159
  markdown +=
3199
3160
  "- **Current Price**: close price at row timestamp (Min: 0 USD, Max: +∞ USD)\n";
3200
3161
  markdown +=
@@ -3239,7 +3200,7 @@ async function generateHistoryTable(indicators, symbol) {
3239
3200
  * const candles = await getCandles('ETHUSDT', '30m', 96);
3240
3201
  * const rows = await service.getData('ETHUSDT', candles);
3241
3202
  * console.log(rows[0].rsi14); // 52.45
3242
- * console.log(rows[0].fibonacciCurrentLevel); // "50.0% Retracement"
3203
+ * console.log(rows[0].fibonacciPositionPercent); // 50.25
3243
3204
  * ```
3244
3205
  */
3245
3206
  class SwingTermHistoryService {
@@ -3423,7 +3384,6 @@ class FifteenMinuteCandleHistoryService {
3423
3384
  */
3424
3385
  this.generateReport = async (symbol, candles) => {
3425
3386
  this.loggerService.log("fifteenMinuteCandleHistoryService generateReport", { symbol });
3426
- const averageVolatility = candles.reduce((sum, candle) => sum + ((candle.high - candle.low) / candle.close) * 100, 0) / candles.length;
3427
3387
  let report = "";
3428
3388
  const currentData = await backtestKit.getDate();
3429
3389
  report += `## 15-Minute Candles History (Last ${RECENT_CANDLES$3})\n`;
@@ -3431,25 +3391,21 @@ class FifteenMinuteCandleHistoryService {
3431
3391
  for (let index = 0; index < candles.length; index++) {
3432
3392
  const candle = candles[index];
3433
3393
  const volatilityPercent = ((candle.high - candle.low) / candle.close) * 100;
3434
- const isHighVolatility = volatilityPercent > averageVolatility * 1.5;
3435
3394
  const bodySize = Math.abs(candle.close - candle.open);
3436
3395
  const candleRange = candle.high - candle.low;
3437
3396
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3438
- const candleType = candle.close > candle.open
3439
- ? "Green"
3440
- : candle.close < candle.open
3441
- ? "Red"
3442
- : "Doji";
3397
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3443
3398
  const formattedTime = new Date(candle.timestamp).toISOString();
3444
- report += `### 15m Candle ${index + 1} (${candleType}) ${isHighVolatility ? "HIGH-VOLATILITY" : ""}\n`;
3399
+ report += `### 15m Candle ${index + 1}\n`;
3400
+ report += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3445
3401
  report += `- **Time**: ${formattedTime}\n`;
3446
3402
  report += `- **Open**: ${await backtestKit.formatPrice(symbol, candle.open)} USD\n`;
3447
3403
  report += `- **High**: ${await backtestKit.formatPrice(symbol, candle.high)} USD\n`;
3448
3404
  report += `- **Low**: ${await backtestKit.formatPrice(symbol, candle.low)} USD\n`;
3449
3405
  report += `- **Close**: ${await backtestKit.formatPrice(symbol, candle.close)} USD\n`;
3450
3406
  report += `- **Volume**: ${await backtestKit.formatQuantity(symbol, candle.volume)}\n`;
3451
- report += `- **15m Volatility**: ${volatilityPercent.toFixed(2)}\n`;
3452
- report += `- **Body Size**: ${bodyPercent.toFixed(1)}\n\n`;
3407
+ report += `- **15m Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3408
+ report += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3453
3409
  }
3454
3410
  return report;
3455
3411
  };
@@ -3588,13 +3544,10 @@ class HourCandleHistoryService {
3588
3544
  const bodySize = Math.abs(candle.close - candle.open);
3589
3545
  const candleRange = candle.high - candle.low;
3590
3546
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3591
- const candleType = candle.close > candle.open
3592
- ? "Green"
3593
- : candle.close < candle.open
3594
- ? "Red"
3595
- : "Doji";
3547
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3596
3548
  const formattedTime = new Date(candle.timestamp).toISOString();
3597
- markdown += `### 1h Candle ${index + 1} (${candleType})\n`;
3549
+ markdown += `### 1h Candle ${index + 1}\n`;
3550
+ markdown += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3598
3551
  markdown += `- **Time**: ${formattedTime}\n`;
3599
3552
  markdown += `- **Open**: ${backtestKit.formatPrice(symbol, candle.open)} USD\n`;
3600
3553
  markdown += `- **High**: ${backtestKit.formatPrice(symbol, candle.high)} USD\n`;
@@ -3741,9 +3694,10 @@ class OneMinuteCandleHistoryService {
3741
3694
  const bodySize = Math.abs(candle.close - candle.open);
3742
3695
  const candleRange = candle.high - candle.low;
3743
3696
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3744
- const candleType = candle.close > candle.open ? "Green" : candle.close < candle.open ? "Red" : "Doji";
3697
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3745
3698
  const formattedTime = new Date(candle.timestamp).toISOString();
3746
- markdown += `### 1m Candle ${index + 1} (${candleType})\n`;
3699
+ markdown += `### 1m Candle ${index + 1}\n`;
3700
+ markdown += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3747
3701
  markdown += `- **Time**: ${formattedTime}\n`;
3748
3702
  markdown += `- **Open**: ${backtestKit.formatPrice(symbol, candle.open)} USD\n`;
3749
3703
  markdown += `- **High**: ${backtestKit.formatPrice(symbol, candle.high)} USD\n`;
@@ -3890,13 +3844,10 @@ class ThirtyMinuteCandleHistoryService {
3890
3844
  const bodySize = Math.abs(candle.close - candle.open);
3891
3845
  const candleRange = candle.high - candle.low;
3892
3846
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3893
- const candleType = candle.close > candle.open
3894
- ? "Green"
3895
- : candle.close < candle.open
3896
- ? "Red"
3897
- : "Doji";
3847
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3898
3848
  const formattedTime = new Date(candle.timestamp).toISOString();
3899
- report += `### 30m Candle ${index + 1} (${candleType})\n`;
3849
+ report += `### 30m Candle ${index + 1}\n`;
3850
+ report += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3900
3851
  report += `- **Time**: ${formattedTime}\n`;
3901
3852
  report += `- **Open**: ${backtestKit.formatPrice(symbol, candle.open)} USD\n`;
3902
3853
  report += `- **High**: ${backtestKit.formatPrice(symbol, candle.high)} USD\n`;
package/build/index.mjs CHANGED
@@ -456,13 +456,9 @@ function generateAnalysis$3(symbol, candles) {
456
456
  const prevVolume = !isUnsafe$4(prevVolumeRaw)
457
457
  ? prevVolumeRaw
458
458
  : volumes[Math.max(0, i - 6)];
459
- const volumeTrend = !isUnsafe$4(recentVolume) && !isUnsafe$4(prevVolume)
460
- ? recentVolume > prevVolume * 1.1
461
- ? "increasing"
462
- : recentVolume < prevVolume * 0.9
463
- ? "decreasing"
464
- : "stable"
465
- : "stable";
459
+ const volumeTrendRatio = !isUnsafe$4(recentVolume) && !isUnsafe$4(prevVolume) && prevVolume > 0
460
+ ? recentVolume / prevVolume
461
+ : null;
466
462
  // Support/Resistance calculation
467
463
  const pivotPeriod = Math.min(4, i + 1);
468
464
  const startIdx = i + 1 - pivotPeriod;
@@ -541,7 +537,7 @@ function generateAnalysis$3(symbol, candles) {
541
537
  cci20: cci.getResult() != null && !isUnsafe$4(cci.getResult())
542
538
  ? cci.getResult()
543
539
  : null,
544
- volumeTrend,
540
+ volumeTrendRatio,
545
541
  support: support != null && !isUnsafe$4(support)
546
542
  ? support
547
543
  : !isUnsafe$4(currentPrice)
@@ -1118,26 +1114,26 @@ function isUnsafe$3(value) {
1118
1114
  /**
1119
1115
  * Calculates volume metrics including SMA, ratio, and trend.
1120
1116
  *
1121
- * Computes volume SMA(5), current volume to average ratio, and volume trend
1122
- * by comparing recent 3 candles to previous 3 candles. Returns "increasing"
1123
- * if recent average > 120% of previous, "decreasing" if < 80%, else "stable".
1117
+ * Computes volume SMA(5), current volume to average ratio, and volume trend ratio
1118
+ * by comparing recent 3 candles to previous 3 candles. Returns numeric ratio
1119
+ * of recent volume average to previous volume average (e.g., 1.2 = 20% increase).
1124
1120
  *
1125
1121
  * @param candles - Array of candle data
1126
1122
  * @param endIndex - Index of current candle in array
1127
- * @returns Object with volumeSma5, volumeRatio, and volumeTrend
1123
+ * @returns Object with volumeSma5, volumeRatio, and volumeTrendRatio
1128
1124
  *
1129
1125
  * @example
1130
1126
  * ```typescript
1131
1127
  * const candles = await getCandles('BTCUSDT', '1m', 60);
1132
1128
  * const metrics = calculateVolumeMetrics(candles, 59);
1133
1129
  * console.log(metrics);
1134
- * // { volumeSma5: 1500000, volumeRatio: 1.25, volumeTrend: "increasing" }
1130
+ * // { volumeSma5: 1500000, volumeRatio: 1.25, volumeTrendRatio: 1.15 }
1135
1131
  * ```
1136
1132
  */
1137
1133
  function calculateVolumeMetrics(candles, endIndex) {
1138
1134
  const volumes = candles.slice(0, endIndex + 1).map((c) => Number(c.volume));
1139
1135
  if (volumes.length < 5) {
1140
- return { volumeSma5: null, volumeRatio: null, volumeTrend: "stable" };
1136
+ return { volumeSma5: null, volumeRatio: null, volumeTrendRatio: null };
1141
1137
  }
1142
1138
  const volumeSma5 = new FasterSMA(5);
1143
1139
  volumes.forEach((vol) => volumeSma5.update(vol, false));
@@ -1145,20 +1141,17 @@ function calculateVolumeMetrics(candles, endIndex) {
1145
1141
  const avgVolume = !isUnsafe$3(avgVolumeRaw) ? avgVolumeRaw : 0;
1146
1142
  const currentVolume = volumes[volumes.length - 1];
1147
1143
  const volumeRatio = avgVolume > 0 && !isUnsafe$3(currentVolume) ? currentVolume / avgVolume : 1;
1148
- let volumeTrend = "stable";
1144
+ let volumeTrendRatio = null;
1149
1145
  if (volumes.length >= 6) {
1150
1146
  const recent3 = volumes.slice(-3);
1151
1147
  const prev3 = volumes.slice(-6, -3);
1152
1148
  if (prev3.length >= 3) {
1153
1149
  const recentAvg = recent3.reduce((a, b) => a + b, 0) / 3;
1154
1150
  const prevAvg = prev3.reduce((a, b) => a + b, 0) / 3;
1155
- if (recentAvg > prevAvg * 1.2)
1156
- volumeTrend = "increasing";
1157
- else if (recentAvg < prevAvg * 0.8)
1158
- volumeTrend = "decreasing";
1151
+ volumeTrendRatio = prevAvg > 0 ? recentAvg / prevAvg : null;
1159
1152
  }
1160
1153
  }
1161
- return { volumeSma5: avgVolume, volumeRatio, volumeTrend };
1154
+ return { volumeSma5: avgVolume, volumeRatio, volumeTrendRatio };
1162
1155
  }
1163
1156
  /**
1164
1157
  * Calculates price change percentages over 1, 3, and 5 minute periods.
@@ -1491,7 +1484,7 @@ function generateAnalysis$2(symbol, candles) {
1491
1484
  : null,
1492
1485
  volumeSma5: volumeMetrics.volumeSma5,
1493
1486
  volumeRatio: volumeMetrics.volumeRatio,
1494
- volumeTrend: volumeMetrics.volumeTrend,
1487
+ volumeTrendRatio: volumeMetrics.volumeTrendRatio,
1495
1488
  currentPrice: currentPrice != null && !isUnsafe$3(currentPrice) ? currentPrice : null,
1496
1489
  priceChange1m: priceChanges.priceChange1m,
1497
1490
  priceChange3m: priceChanges.priceChange3m,
@@ -2054,38 +2047,33 @@ function calculateFibonacciLevels$1(candles, endIndex) {
2054
2047
  return nearestLevel;
2055
2048
  }
2056
2049
  /**
2057
- * Analyzes volume trend by comparing recent vs older volume averages.
2050
+ * Calculates volume trend ratio by comparing recent vs older volume averages.
2058
2051
  *
2059
2052
  * Compares average volume of last 8 candles against previous 8 candles.
2060
- * Returns "increasing" if recent volume > 120% of older volume,
2061
- * "decreasing" if recent < 80% of older volume, otherwise "stable".
2053
+ * Returns ratio of recent volume to older volume (e.g., 1.2 means 20% increase).
2062
2054
  *
2063
2055
  * @param candles - Array of candle data
2064
2056
  * @param endIndex - Index of current candle in array
2065
- * @returns Volume trend: "increasing", "decreasing", or "stable"
2057
+ * @returns Volume trend ratio (recent/older), or null if insufficient data
2066
2058
  *
2067
2059
  * @example
2068
2060
  * ```typescript
2069
2061
  * const candles = await getCandles('ETHUSDT', '15m', 100);
2070
- * const trend = calculateVolumeTrend(candles, 99);
2071
- * console.log(trend); // "increasing" | "decreasing" | "stable"
2062
+ * const ratio = calculateVolumeTrendRatio(candles, 99);
2063
+ * console.log(ratio); // 1.25 (25% increase)
2072
2064
  * ```
2073
2065
  */
2074
- function calculateVolumeTrend(candles, endIndex) {
2066
+ function calculateVolumeTrendRatio(candles, endIndex) {
2075
2067
  const volumes = candles.slice(0, endIndex + 1).map((c) => Number(c.volume));
2076
2068
  if (volumes.length < 16)
2077
- return "stable";
2069
+ return null;
2078
2070
  const recentVolumes = volumes.slice(-8);
2079
2071
  const olderVolumes = volumes.slice(-16, -8);
2080
2072
  if (recentVolumes.length < 4 || olderVolumes.length < 4)
2081
- return "stable";
2073
+ return null;
2082
2074
  const recentAvg = recentVolumes.reduce((sum, vol) => sum + vol, 0) / recentVolumes.length;
2083
2075
  const olderAvg = olderVolumes.reduce((sum, vol) => sum + vol, 0) / olderVolumes.length;
2084
- if (recentAvg > olderAvg * 1.2)
2085
- return "increasing";
2086
- if (recentAvg < olderAvg * 0.8)
2087
- return "decreasing";
2088
- return "stable";
2076
+ return olderAvg > 0 ? recentAvg / olderAvg : null;
2089
2077
  }
2090
2078
  /**
2091
2079
  * Generates comprehensive technical analysis for 15-minute candles.
@@ -2166,7 +2154,7 @@ function generateAnalysis$1(symbol, candles) {
2166
2154
  if (i < WARMUP_PERIOD$1) {
2167
2155
  return;
2168
2156
  }
2169
- const volumeTrend = calculateVolumeTrend(candles, i);
2157
+ const volumeTrendRatio = calculateVolumeTrendRatio(candles, i);
2170
2158
  const pivotPeriod = Math.min(48, i + 1);
2171
2159
  const startIdx = i + 1 - pivotPeriod;
2172
2160
  const recentHighs = highs
@@ -2265,7 +2253,7 @@ function generateAnalysis$1(symbol, candles) {
2265
2253
  roc10: roc10.getResult() != null && !isUnsafe$2(roc10.getResult())
2266
2254
  ? roc10.getResult()
2267
2255
  : null,
2268
- volumeTrend,
2256
+ volumeTrendRatio,
2269
2257
  support: support != null && !isUnsafe$2(support)
2270
2258
  ? support
2271
2259
  : !isUnsafe$2(currentPrice)
@@ -2700,9 +2688,9 @@ const columns = [
2700
2688
  : "N/A",
2701
2689
  },
2702
2690
  {
2703
- key: "fibonacciCurrentLevel",
2704
- label: "Fibonacci Current Level",
2705
- format: (v) => String(v),
2691
+ key: "fibonacciPositionPercent",
2692
+ label: "Fibonacci Position %",
2693
+ format: (v) => (v !== null ? `${Number(v).toFixed(2)}%` : "N/A"),
2706
2694
  },
2707
2695
  {
2708
2696
  key: "bodySize",
@@ -2779,7 +2767,7 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2779
2767
  return {
2780
2768
  nearestSupport: null,
2781
2769
  nearestResistance: null,
2782
- currentLevel: "insufficient data",
2770
+ fibonacciPositionPercent: null,
2783
2771
  };
2784
2772
  }
2785
2773
  const startIdx = endIndex + 1 - period;
@@ -2794,13 +2782,15 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2794
2782
  return {
2795
2783
  nearestSupport: null,
2796
2784
  nearestResistance: null,
2797
- currentLevel: "insufficient data",
2785
+ fibonacciPositionPercent: null,
2798
2786
  };
2799
2787
  }
2800
2788
  const high = Math.max(...highs);
2801
2789
  const low = Math.min(...lows);
2802
2790
  const range = high - low;
2803
2791
  const currentPrice = Number(candles[endIndex].close);
2792
+ // Calculate position as percentage from low to high (0% = low, 100% = high)
2793
+ const fibonacciPositionPercent = range > 0 ? ((currentPrice - low) / range) * 100 : null;
2804
2794
  const retracement = {
2805
2795
  level0: high,
2806
2796
  level236: high - range * 0.236,
@@ -2815,35 +2805,6 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2815
2805
  level1618: high + range * 0.618,
2816
2806
  level2618: high + range * 1.618,
2817
2807
  };
2818
- const tolerance = range * 0.015;
2819
- let currentLevel = "between levels";
2820
- if (Math.abs(currentPrice - retracement.level0) < tolerance) {
2821
- currentLevel = "0.0% (High)";
2822
- }
2823
- else if (Math.abs(currentPrice - retracement.level236) < tolerance) {
2824
- currentLevel = "23.6% Retracement";
2825
- }
2826
- else if (Math.abs(currentPrice - retracement.level382) < tolerance) {
2827
- currentLevel = "38.2% Retracement";
2828
- }
2829
- else if (Math.abs(currentPrice - retracement.level500) < tolerance) {
2830
- currentLevel = "50.0% Retracement";
2831
- }
2832
- else if (Math.abs(currentPrice - retracement.level618) < tolerance) {
2833
- currentLevel = "61.8% Retracement";
2834
- }
2835
- else if (Math.abs(currentPrice - retracement.level786) < tolerance) {
2836
- currentLevel = "78.6% Retracement";
2837
- }
2838
- else if (Math.abs(currentPrice - retracement.level1000) < tolerance) {
2839
- currentLevel = "100% Retracement (Low)";
2840
- }
2841
- else if (currentPrice > retracement.level0) {
2842
- currentLevel = "Above high";
2843
- }
2844
- else if (currentPrice < retracement.level1000) {
2845
- currentLevel = "Below low";
2846
- }
2847
2808
  const allRetracementLevels = Object.values(retracement).filter((level) => level !== null);
2848
2809
  const allExtensionLevels = Object.values(extension).filter((level) => level !== null && level > 0);
2849
2810
  const resistanceLevels = [...allRetracementLevels, ...allExtensionLevels]
@@ -2857,7 +2818,7 @@ function calculateFibonacciLevels(candles, endIndex, period = 48) {
2857
2818
  return {
2858
2819
  nearestSupport,
2859
2820
  nearestResistance,
2860
- currentLevel,
2821
+ fibonacciPositionPercent,
2861
2822
  };
2862
2823
  }
2863
2824
  /**
@@ -2916,7 +2877,7 @@ function calculateSupportResistance(candles, endIndex, window = 20) {
2916
2877
  * const candles = await getCandles('BTCUSDT', '30m', 96);
2917
2878
  * const analysis = generateAnalysis('BTCUSDT', candles);
2918
2879
  * console.log(analysis[0].rsi14); // 52.45
2919
- * console.log(analysis[0].fibonacciCurrentLevel); // "50.0% Retracement"
2880
+ * console.log(analysis[0].fibonacciPositionPercent); // 50.25
2920
2881
  * ```
2921
2882
  */
2922
2883
  function generateAnalysis(symbol, candles) {
@@ -3085,7 +3046,7 @@ function generateAnalysis(symbol, candles) {
3085
3046
  : null,
3086
3047
  fibonacciNearestSupport: fibonacci.nearestSupport,
3087
3048
  fibonacciNearestResistance: fibonacci.nearestResistance,
3088
- fibonacciCurrentLevel: fibonacci.currentLevel,
3049
+ fibonacciPositionPercent: fibonacci.fibonacciPositionPercent,
3089
3050
  bodySize,
3090
3051
  closePrice: close,
3091
3052
  date: new Date(),
@@ -3192,7 +3153,7 @@ async function generateHistoryTable(indicators, symbol) {
3192
3153
  markdown +=
3193
3154
  "- **Fibonacci Nearest Resistance**: nearest resistance level over 48 candles (24h on 30m timeframe) before row timestamp (Min: 0 USD, Max: +∞ USD)\n";
3194
3155
  markdown +=
3195
- "- **Fibonacci Current Level**: current level description before row timestamp\n";
3156
+ "- **Fibonacci Position %**: price position in high-low range before row timestamp (Min: 0%, Max: 100%+)\n";
3196
3157
  markdown +=
3197
3158
  "- **Current Price**: close price at row timestamp (Min: 0 USD, Max: +∞ USD)\n";
3198
3159
  markdown +=
@@ -3237,7 +3198,7 @@ async function generateHistoryTable(indicators, symbol) {
3237
3198
  * const candles = await getCandles('ETHUSDT', '30m', 96);
3238
3199
  * const rows = await service.getData('ETHUSDT', candles);
3239
3200
  * console.log(rows[0].rsi14); // 52.45
3240
- * console.log(rows[0].fibonacciCurrentLevel); // "50.0% Retracement"
3201
+ * console.log(rows[0].fibonacciPositionPercent); // 50.25
3241
3202
  * ```
3242
3203
  */
3243
3204
  class SwingTermHistoryService {
@@ -3421,7 +3382,6 @@ class FifteenMinuteCandleHistoryService {
3421
3382
  */
3422
3383
  this.generateReport = async (symbol, candles) => {
3423
3384
  this.loggerService.log("fifteenMinuteCandleHistoryService generateReport", { symbol });
3424
- const averageVolatility = candles.reduce((sum, candle) => sum + ((candle.high - candle.low) / candle.close) * 100, 0) / candles.length;
3425
3385
  let report = "";
3426
3386
  const currentData = await getDate();
3427
3387
  report += `## 15-Minute Candles History (Last ${RECENT_CANDLES$3})\n`;
@@ -3429,25 +3389,21 @@ class FifteenMinuteCandleHistoryService {
3429
3389
  for (let index = 0; index < candles.length; index++) {
3430
3390
  const candle = candles[index];
3431
3391
  const volatilityPercent = ((candle.high - candle.low) / candle.close) * 100;
3432
- const isHighVolatility = volatilityPercent > averageVolatility * 1.5;
3433
3392
  const bodySize = Math.abs(candle.close - candle.open);
3434
3393
  const candleRange = candle.high - candle.low;
3435
3394
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3436
- const candleType = candle.close > candle.open
3437
- ? "Green"
3438
- : candle.close < candle.open
3439
- ? "Red"
3440
- : "Doji";
3395
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3441
3396
  const formattedTime = new Date(candle.timestamp).toISOString();
3442
- report += `### 15m Candle ${index + 1} (${candleType}) ${isHighVolatility ? "HIGH-VOLATILITY" : ""}\n`;
3397
+ report += `### 15m Candle ${index + 1}\n`;
3398
+ report += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3443
3399
  report += `- **Time**: ${formattedTime}\n`;
3444
3400
  report += `- **Open**: ${await formatPrice(symbol, candle.open)} USD\n`;
3445
3401
  report += `- **High**: ${await formatPrice(symbol, candle.high)} USD\n`;
3446
3402
  report += `- **Low**: ${await formatPrice(symbol, candle.low)} USD\n`;
3447
3403
  report += `- **Close**: ${await formatPrice(symbol, candle.close)} USD\n`;
3448
3404
  report += `- **Volume**: ${await formatQuantity(symbol, candle.volume)}\n`;
3449
- report += `- **15m Volatility**: ${volatilityPercent.toFixed(2)}\n`;
3450
- report += `- **Body Size**: ${bodyPercent.toFixed(1)}\n\n`;
3405
+ report += `- **15m Volatility**: ${volatilityPercent.toFixed(2)}%\n`;
3406
+ report += `- **Body Size**: ${bodyPercent.toFixed(1)}%\n\n`;
3451
3407
  }
3452
3408
  return report;
3453
3409
  };
@@ -3586,13 +3542,10 @@ class HourCandleHistoryService {
3586
3542
  const bodySize = Math.abs(candle.close - candle.open);
3587
3543
  const candleRange = candle.high - candle.low;
3588
3544
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3589
- const candleType = candle.close > candle.open
3590
- ? "Green"
3591
- : candle.close < candle.open
3592
- ? "Red"
3593
- : "Doji";
3545
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3594
3546
  const formattedTime = new Date(candle.timestamp).toISOString();
3595
- markdown += `### 1h Candle ${index + 1} (${candleType})\n`;
3547
+ markdown += `### 1h Candle ${index + 1}\n`;
3548
+ markdown += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3596
3549
  markdown += `- **Time**: ${formattedTime}\n`;
3597
3550
  markdown += `- **Open**: ${formatPrice(symbol, candle.open)} USD\n`;
3598
3551
  markdown += `- **High**: ${formatPrice(symbol, candle.high)} USD\n`;
@@ -3739,9 +3692,10 @@ class OneMinuteCandleHistoryService {
3739
3692
  const bodySize = Math.abs(candle.close - candle.open);
3740
3693
  const candleRange = candle.high - candle.low;
3741
3694
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3742
- const candleType = candle.close > candle.open ? "Green" : candle.close < candle.open ? "Red" : "Doji";
3695
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3743
3696
  const formattedTime = new Date(candle.timestamp).toISOString();
3744
- markdown += `### 1m Candle ${index + 1} (${candleType})\n`;
3697
+ markdown += `### 1m Candle ${index + 1}\n`;
3698
+ markdown += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3745
3699
  markdown += `- **Time**: ${formattedTime}\n`;
3746
3700
  markdown += `- **Open**: ${formatPrice(symbol, candle.open)} USD\n`;
3747
3701
  markdown += `- **High**: ${formatPrice(symbol, candle.high)} USD\n`;
@@ -3888,13 +3842,10 @@ class ThirtyMinuteCandleHistoryService {
3888
3842
  const bodySize = Math.abs(candle.close - candle.open);
3889
3843
  const candleRange = candle.high - candle.low;
3890
3844
  const bodyPercent = candleRange > 0 ? (bodySize / candleRange) * 100 : 0;
3891
- const candleType = candle.close > candle.open
3892
- ? "Green"
3893
- : candle.close < candle.open
3894
- ? "Red"
3895
- : "Doji";
3845
+ const priceChangePercent = candle.open > 0 ? ((candle.close - candle.open) / candle.open) * 100 : 0;
3896
3846
  const formattedTime = new Date(candle.timestamp).toISOString();
3897
- report += `### 30m Candle ${index + 1} (${candleType})\n`;
3847
+ report += `### 30m Candle ${index + 1}\n`;
3848
+ report += `- **Price Change**: ${priceChangePercent.toFixed(3)}%\n`;
3898
3849
  report += `- **Time**: ${formattedTime}\n`;
3899
3850
  report += `- **Open**: ${formatPrice(symbol, candle.open)} USD\n`;
3900
3851
  report += `- **High**: ${formatPrice(symbol, candle.high)} USD\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/signals",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
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
5
  "author": {
6
6
  "name": "Petr Tripolsky",
@@ -69,7 +69,6 @@
69
69
  "worker-testbed": "1.0.12"
70
70
  },
71
71
  "peerDependencies": {
72
- "agent-swarm-kit": "^1.1.180",
73
72
  "backtest-kit": "^1.12.1",
74
73
  "typescript": "^5.0.0"
75
74
  },
@@ -78,7 +77,8 @@
78
77
  "di-scoped": "^1.0.20",
79
78
  "functools-kit": "^1.0.95",
80
79
  "trading-signals": "^6.9.1",
81
- "get-moment-stamp": "^1.1.1"
80
+ "get-moment-stamp": "^1.1.1",
81
+ "agent-swarm-kit": "^1.1.180"
82
82
  },
83
83
  "publishConfig": {
84
84
  "access": "public"
package/types.d.ts CHANGED
@@ -504,7 +504,7 @@ interface ISwingTermRow {
504
504
  priceMomentum6: number | null;
505
505
  fibonacciNearestSupport: number | null;
506
506
  fibonacciNearestResistance: number | null;
507
- fibonacciCurrentLevel: string;
507
+ fibonacciPositionPercent: number | null;
508
508
  bodySize: number;
509
509
  closePrice: number;
510
510
  date: Date;
@@ -542,7 +542,7 @@ interface ISwingTermRow {
542
542
  * const candles = await getCandles('ETHUSDT', '30m', 96);
543
543
  * const rows = await service.getData('ETHUSDT', candles);
544
544
  * console.log(rows[0].rsi14); // 52.45
545
- * console.log(rows[0].fibonacciCurrentLevel); // "50.0% Retracement"
545
+ * console.log(rows[0].fibonacciPositionPercent); // 50.25
546
546
  * ```
547
547
  */
548
548
  declare class SwingTermHistoryService {
@@ -649,7 +649,7 @@ interface ILongTermRow {
649
649
  currentPrice: number;
650
650
  support: number;
651
651
  resistance: number;
652
- volumeTrend: string;
652
+ volumeTrendRatio: number | null;
653
653
  fibonacciNearestLevel: string;
654
654
  fibonacciNearestPrice: number;
655
655
  fibonacciDistance: number;
@@ -780,7 +780,7 @@ interface IShortTermRow {
780
780
  momentum8: number | null;
781
781
  roc5: number | null;
782
782
  roc10: number | null;
783
- volumeTrend: string;
783
+ volumeTrendRatio: number | null;
784
784
  support: number;
785
785
  resistance: number;
786
786
  currentPrice: number;
@@ -940,7 +940,7 @@ interface IMicroTermRow {
940
940
  wma5: number | null;
941
941
  volumeSma5: number | null;
942
942
  volumeRatio: number | null;
943
- volumeTrend: string;
943
+ volumeTrendRatio: number | null;
944
944
  currentPrice: number;
945
945
  priceChange1m: number | null;
946
946
  priceChange3m: number | null;