backtest-kit 1.5.0 → 1.5.2

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
@@ -23,7 +23,13 @@ const GLOBAL_CONFIG = {
23
23
  * Must be greater than trading fees to ensure profitable trades
24
24
  * Default: 0.3% (covers 2×0.1% fees + minimum profit margin)
25
25
  */
26
- CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.1,
26
+ CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.3,
27
+ /**
28
+ * Minimum StopLoss distance from priceOpen (percentage)
29
+ * Prevents signals from being immediately stopped out due to price volatility
30
+ * Default: 0.5% (buffer to avoid instant stop loss on normal market fluctuations)
31
+ */
32
+ CC_MIN_STOPLOSS_DISTANCE_PERCENT: 0.5,
27
33
  /**
28
34
  * Maximum StopLoss distance from priceOpen (percentage)
29
35
  * Prevents catastrophic losses from extreme StopLoss values
@@ -36,6 +42,15 @@ const GLOBAL_CONFIG = {
36
42
  * Default: 1440 minutes (1 day)
37
43
  */
38
44
  CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
45
+ /**
46
+ * Maximum time allowed for signal generation (in seconds).
47
+ * Prevents long-running or stuck signal generation routines from blocking
48
+ * execution or consuming resources indefinitely. If generation exceeds this
49
+ * threshold the attempt should be aborted, logged and optionally retried.
50
+ *
51
+ * Default: 180 seconds (3 minutes)
52
+ */
53
+ CC_MAX_SIGNAL_GENERATION_SECONDS: 180,
39
54
  /**
40
55
  * Number of retries for getCandles function
41
56
  * Default: 3 retries
@@ -1753,6 +1768,7 @@ const INTERVAL_MINUTES$1 = {
1753
1768
  "30m": 30,
1754
1769
  "1h": 60,
1755
1770
  };
1771
+ const TIMEOUT_SYMBOL = Symbol('timeout');
1756
1772
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1757
1773
  const errors = [];
1758
1774
  // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
@@ -1809,18 +1825,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1809
1825
  if (signal.priceStopLoss >= signal.priceOpen) {
1810
1826
  errors.push(`Long: priceStopLoss (${signal.priceStopLoss}) must be < priceOpen (${signal.priceOpen})`);
1811
1827
  }
1812
- // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1813
- // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1814
- if (!isScheduled) {
1815
- // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1816
- if (isFinite(currentPrice) && currentPrice < signal.priceStopLoss) {
1817
- errors.push(`Long: currentPrice (${currentPrice}) < priceStopLoss (${signal.priceStopLoss}). ` +
1818
- `Signal would be immediately cancelled. This signal is invalid.`);
1828
+ // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
1829
+ if (!isScheduled && isFinite(currentPrice)) {
1830
+ // LONG: currentPrice должна быть МЕЖДУ SL и TP (не пробита ни одна граница)
1831
+ // SL < currentPrice < TP
1832
+ if (currentPrice <= signal.priceStopLoss) {
1833
+ errors.push(`Long immediate: currentPrice (${currentPrice}) <= priceStopLoss (${signal.priceStopLoss}). ` +
1834
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
1819
1835
  }
1820
- // Текущая цена уже достигла TakeProfit - профит упущен
1821
- if (isFinite(currentPrice) && currentPrice > signal.priceTakeProfit) {
1822
- errors.push(`Long: currentPrice (${currentPrice}) > priceTakeProfit (${signal.priceTakeProfit}). ` +
1823
- `Signal is invalid - the profit opportunity has already passed.`);
1836
+ if (currentPrice >= signal.priceTakeProfit) {
1837
+ errors.push(`Long immediate: currentPrice (${currentPrice}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
1838
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
1839
+ }
1840
+ }
1841
+ // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
1842
+ if (isScheduled && isFinite(signal.priceOpen)) {
1843
+ // LONG scheduled: priceOpen должен быть МЕЖДУ SL и TP
1844
+ // SL < priceOpen < TP
1845
+ if (signal.priceOpen <= signal.priceStopLoss) {
1846
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
1847
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
1848
+ }
1849
+ if (signal.priceOpen >= signal.priceTakeProfit) {
1850
+ errors.push(`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
1851
+ `Signal would close immediately on activation. This is logically impossible for LONG position.`);
1824
1852
  }
1825
1853
  }
1826
1854
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
@@ -1832,8 +1860,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1832
1860
  `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
1833
1861
  }
1834
1862
  }
1863
+ // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
1864
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
1865
+ const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
1866
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
1867
+ errors.push(`Long: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
1868
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
1869
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
1870
+ }
1871
+ }
1835
1872
  // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
1836
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1873
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1837
1874
  const slDistancePercent = ((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen) * 100;
1838
1875
  if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1839
1876
  errors.push(`Long: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
@@ -1850,18 +1887,30 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1850
1887
  if (signal.priceStopLoss <= signal.priceOpen) {
1851
1888
  errors.push(`Short: priceStopLoss (${signal.priceStopLoss}) must be > priceOpen (${signal.priceOpen})`);
1852
1889
  }
1853
- // ЗАЩИТА ОТ EDGE CASE: для immediate сигналов проверяем что текущая цена не пробила SL/TP
1854
- // Для scheduled сигналов эта проверка избыточна т.к. priceOpen уже проверен выше
1855
- if (!isScheduled) {
1856
- // Текущая цена уже пробила StopLoss - позиция откроется и сразу закроется по SL
1857
- if (isFinite(currentPrice) && currentPrice > signal.priceStopLoss) {
1858
- errors.push(`Short: currentPrice (${currentPrice}) > priceStopLoss (${signal.priceStopLoss}). ` +
1859
- `Signal would be immediately cancelled. This signal is invalid.`);
1890
+ // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ: проверяем что позиция не закроется сразу после открытия
1891
+ if (!isScheduled && isFinite(currentPrice)) {
1892
+ // SHORT: currentPrice должна быть МЕЖДУ TP и SL (не пробита ни одна граница)
1893
+ // TP < currentPrice < SL
1894
+ if (currentPrice >= signal.priceStopLoss) {
1895
+ errors.push(`Short immediate: currentPrice (${currentPrice}) >= priceStopLoss (${signal.priceStopLoss}). ` +
1896
+ `Signal would be immediately closed by stop loss. Cannot open position that is already stopped out.`);
1897
+ }
1898
+ if (currentPrice <= signal.priceTakeProfit) {
1899
+ errors.push(`Short immediate: currentPrice (${currentPrice}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
1900
+ `Signal would be immediately closed by take profit. The profit opportunity has already passed.`);
1901
+ }
1902
+ }
1903
+ // ЗАЩИТА ОТ МОМЕНТАЛЬНОГО ЗАКРЫТИЯ scheduled сигналов
1904
+ if (isScheduled && isFinite(signal.priceOpen)) {
1905
+ // SHORT scheduled: priceOpen должен быть МЕЖДУ TP и SL
1906
+ // TP < priceOpen < SL
1907
+ if (signal.priceOpen >= signal.priceStopLoss) {
1908
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
1909
+ `Signal would be immediately cancelled on activation. Cannot activate position that is already stopped out.`);
1860
1910
  }
1861
- // Текущая цена уже достигла TakeProfit - профит упущен
1862
- if (isFinite(currentPrice) && currentPrice < signal.priceTakeProfit) {
1863
- errors.push(`Short: currentPrice (${currentPrice}) < priceTakeProfit (${signal.priceTakeProfit}). ` +
1864
- `Signal is invalid - the profit opportunity has already passed.`);
1911
+ if (signal.priceOpen <= signal.priceTakeProfit) {
1912
+ errors.push(`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
1913
+ `Signal would close immediately on activation. This is logically impossible for SHORT position.`);
1865
1914
  }
1866
1915
  }
1867
1916
  // ЗАЩИТА ОТ МИКРО-ПРОФИТА: TakeProfit должен быть достаточно далеко, чтобы покрыть комиссии
@@ -1873,8 +1922,17 @@ const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1873
1922
  `Current: TP=${signal.priceTakeProfit}, Open=${signal.priceOpen}`);
1874
1923
  }
1875
1924
  }
1925
+ // ЗАЩИТА ОТ СЛИШКОМ УЗКОГО STOPLOSS: минимальный буфер для избежания моментального закрытия
1926
+ if (GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
1927
+ const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
1928
+ if (slDistancePercent < GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT) {
1929
+ errors.push(`Short: StopLoss too close to priceOpen (${slDistancePercent.toFixed(3)}%). ` +
1930
+ `Minimum distance: ${GLOBAL_CONFIG.CC_MIN_STOPLOSS_DISTANCE_PERCENT}% to avoid instant stop out on market volatility. ` +
1931
+ `Current: SL=${signal.priceStopLoss}, Open=${signal.priceOpen}`);
1932
+ }
1933
+ }
1876
1934
  // ЗАЩИТА ОТ ЭКСТРЕМАЛЬНОГО STOPLOSS: ограничиваем максимальный убыток
1877
- if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT && GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1935
+ if (GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1878
1936
  const slDistancePercent = ((signal.priceStopLoss - signal.priceOpen) / signal.priceOpen) * 100;
1879
1937
  if (slDistancePercent > GLOBAL_CONFIG.CC_MAX_STOPLOSS_DISTANCE_PERCENT) {
1880
1938
  errors.push(`Short: StopLoss too far from priceOpen (${slDistancePercent.toFixed(3)}%). ` +
@@ -1936,7 +1994,14 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1936
1994
  }))) {
1937
1995
  return null;
1938
1996
  }
1939
- const signal = await self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when);
1997
+ const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
1998
+ const signal = await Promise.race([
1999
+ self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
2000
+ sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
2001
+ ]);
2002
+ if (typeof signal === "symbol") {
2003
+ throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
2004
+ }
1940
2005
  if (!signal) {
1941
2006
  return null;
1942
2007
  }
@@ -2222,6 +2287,8 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
2222
2287
  strategyName: self.params.method.context.strategyName,
2223
2288
  exchangeName: self.params.method.context.exchangeName,
2224
2289
  symbol: self.params.execution.context.symbol,
2290
+ percentTp: 0,
2291
+ percentSl: 0,
2225
2292
  };
2226
2293
  if (self.params.callbacks?.onTick) {
2227
2294
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -2348,6 +2415,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2348
2415
  return result;
2349
2416
  };
2350
2417
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2418
+ let percentTp = 0;
2419
+ let percentSl = 0;
2351
2420
  // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2352
2421
  {
2353
2422
  if (signal.position === "long") {
@@ -2357,18 +2426,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2357
2426
  // Moving towards TP
2358
2427
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2359
2428
  const progressPercent = (currentDistance / tpDistance) * 100;
2360
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2429
+ percentTp = Math.min(progressPercent, 100);
2430
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2361
2431
  if (self.params.callbacks?.onPartialProfit) {
2362
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2432
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2363
2433
  }
2364
2434
  }
2365
2435
  else if (currentDistance < 0) {
2366
2436
  // Moving towards SL
2367
2437
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2368
2438
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2369
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2439
+ percentSl = Math.min(progressPercent, 100);
2440
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2370
2441
  if (self.params.callbacks?.onPartialLoss) {
2371
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2442
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2372
2443
  }
2373
2444
  }
2374
2445
  }
@@ -2379,18 +2450,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2379
2450
  // Moving towards TP
2380
2451
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2381
2452
  const progressPercent = (currentDistance / tpDistance) * 100;
2382
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2453
+ percentTp = Math.min(progressPercent, 100);
2454
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2383
2455
  if (self.params.callbacks?.onPartialProfit) {
2384
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2456
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2385
2457
  }
2386
2458
  }
2387
2459
  if (currentDistance < 0) {
2388
2460
  // Moving towards SL
2389
2461
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2390
2462
  const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2391
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2463
+ percentSl = Math.min(progressPercent, 100);
2464
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2392
2465
  if (self.params.callbacks?.onPartialLoss) {
2393
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2466
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2394
2467
  }
2395
2468
  }
2396
2469
  }
@@ -2402,6 +2475,8 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2402
2475
  strategyName: self.params.method.context.strategyName,
2403
2476
  exchangeName: self.params.method.context.exchangeName,
2404
2477
  symbol: self.params.execution.context.symbol,
2478
+ percentTp,
2479
+ percentSl,
2405
2480
  };
2406
2481
  if (self.params.callbacks?.onTick) {
2407
2482
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -2548,8 +2623,14 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2548
2623
  const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) => {
2549
2624
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
2550
2625
  const maxTimeToWait = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
2626
+ const bufferCandlesCount = candlesCount - 1;
2551
2627
  for (let i = 0; i < candles.length; i++) {
2552
2628
  const candle = candles[i];
2629
+ // КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
2630
+ // BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
2631
+ if (i < bufferCandlesCount) {
2632
+ continue;
2633
+ }
2553
2634
  const recentCandles = candles.slice(Math.max(0, i - (candlesCount - 1)), i + 1);
2554
2635
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
2555
2636
  // КРИТИЧНО: Проверяем timeout ПЕРЕД проверкой цены
@@ -2615,11 +2696,21 @@ const PROCESS_SCHEDULED_SIGNAL_CANDLES_FN = async (self, scheduled, candles) =>
2615
2696
  };
2616
2697
  const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2617
2698
  const candlesCount = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
2618
- for (let i = candlesCount - 1; i < candles.length; i++) {
2619
- const recentCandles = candles.slice(i - (candlesCount - 1), i + 1);
2699
+ const bufferCandlesCount = candlesCount - 1;
2700
+ // КРИТИЧНО: проверяем TP/SL на КАЖДОЙ свече начиная после буфера
2701
+ // Первые bufferCandlesCount свечей - это буфер для VWAP
2702
+ for (let i = 0; i < candles.length; i++) {
2703
+ const currentCandle = candles[i];
2704
+ const currentCandleTimestamp = currentCandle.timestamp;
2705
+ // КРИТИЧНО: Пропускаем первые bufferCandlesCount свечей (буфер для VWAP)
2706
+ // BacktestLogicPrivateService запросил свечи начиная с (when - bufferMinutes)
2707
+ if (i < bufferCandlesCount) {
2708
+ continue;
2709
+ }
2710
+ // Берем последние candlesCount свечей для VWAP (включая буфер)
2711
+ const startIndex = Math.max(0, i - (candlesCount - 1));
2712
+ const recentCandles = candles.slice(startIndex, i + 1);
2620
2713
  const averagePrice = GET_AVG_PRICE_FN(recentCandles);
2621
- const currentCandleTimestamp = recentCandles[recentCandles.length - 1].timestamp;
2622
- const currentCandle = recentCandles[recentCandles.length - 1];
2623
2714
  let shouldClose = false;
2624
2715
  let closeReason;
2625
2716
  // Check time expiration FIRST (КРИТИЧНО!)
@@ -2919,9 +3010,10 @@ class ClientStrategy {
2919
3010
  * 4. If cancelled: returns closed result with closeReason "cancelled"
2920
3011
  *
2921
3012
  * For pending signals:
2922
- * 1. Iterates through candles checking VWAP against TP/SL on each timeframe
2923
- * 2. Starts from index 4 (needs 5 candles for VWAP calculation)
2924
- * 3. Returns closed result (either TP/SL or time_expired)
3013
+ * 1. Iterates through ALL candles starting from the first one
3014
+ * 2. Checks TP/SL using candle.high/low (immediate detection)
3015
+ * 3. VWAP calculated with dynamic window (1 to CC_AVG_PRICE_CANDLES_COUNT candles)
3016
+ * 4. Returns closed result (either TP/SL or time_expired)
2925
3017
  *
2926
3018
  * @param candles - Array of candles to process
2927
3019
  * @returns Promise resolving to closed signal result with PNL
@@ -2998,6 +3090,8 @@ class ClientStrategy {
2998
3090
  action: "active",
2999
3091
  signal: scheduled,
3000
3092
  currentPrice: lastPrice,
3093
+ percentSl: 0,
3094
+ percentTp: 0,
3001
3095
  strategyName: this.params.method.context.strategyName,
3002
3096
  exchangeName: this.params.method.context.exchangeName,
3003
3097
  symbol: this.params.execution.context.symbol,
@@ -4875,13 +4969,17 @@ class BacktestLogicPrivateService {
4875
4969
  minuteEstimatedTime: signal.minuteEstimatedTime,
4876
4970
  });
4877
4971
  // Запрашиваем минутные свечи для мониторинга активации/отмены
4878
- // КРИТИЧНО: запрашиваем CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
4879
- // + minuteEstimatedTime для работы сигнала ПОСЛЕ активации
4880
- // +1 потому что when включается как первая свеча (timestamp начинается с when, а не after when)
4881
- const candlesNeeded = GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
4972
+ // КРИТИЧНО: запрашиваем:
4973
+ // - CC_AVG_PRICE_CANDLES_COUNT-1 для буфера VWAP (ДО when)
4974
+ // - CC_SCHEDULE_AWAIT_MINUTES для ожидания активации
4975
+ // - minuteEstimatedTime для работы сигнала ПОСЛЕ активации
4976
+ // - +1 потому что when включается как первая свеча
4977
+ const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
4978
+ const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
4979
+ const candlesNeeded = bufferMinutes + GLOBAL_CONFIG.CC_SCHEDULE_AWAIT_MINUTES + signal.minuteEstimatedTime + 1;
4882
4980
  let candles;
4883
4981
  try {
4884
- candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, when, true);
4982
+ candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", candlesNeeded, bufferStartTime, true);
4885
4983
  }
4886
4984
  catch (error) {
4887
4985
  console.warn(`backtestLogicPrivateService getNextCandles failed for scheduled signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
@@ -4889,6 +4987,7 @@ class BacktestLogicPrivateService {
4889
4987
  symbol,
4890
4988
  signalId: signal.id,
4891
4989
  candlesNeeded,
4990
+ bufferMinutes,
4892
4991
  error: errorData(error), message: getErrorMessage(error),
4893
4992
  });
4894
4993
  await errorEmitter.next(error);
@@ -4961,16 +5060,23 @@ class BacktestLogicPrivateService {
4961
5060
  signalId: signal.id,
4962
5061
  minuteEstimatedTime: signal.minuteEstimatedTime,
4963
5062
  });
4964
- // Получаем свечи для бектеста
5063
+ // КРИТИЧНО: Получаем свечи включая буфер для VWAP
5064
+ // Сдвигаем начало назад на CC_AVG_PRICE_CANDLES_COUNT-1 минут для буфера VWAP
5065
+ // Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
5066
+ const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
5067
+ const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
5068
+ const totalCandles = signal.minuteEstimatedTime + bufferMinutes;
4965
5069
  let candles;
4966
5070
  try {
4967
- candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", signal.minuteEstimatedTime, when, true);
5071
+ candles = await this.exchangeGlobalService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
4968
5072
  }
4969
5073
  catch (error) {
4970
5074
  console.warn(`backtestLogicPrivateService getNextCandles failed for opened signal when=${when.toISOString()} symbol=${symbol} strategyName=${this.methodContextService.context.strategyName} exchangeName=${this.methodContextService.context.exchangeName}`);
4971
5075
  this.loggerService.warn("backtestLogicPrivateService getNextCandles failed for opened signal", {
4972
5076
  symbol,
4973
5077
  signalId: signal.id,
5078
+ totalCandles,
5079
+ bufferMinutes,
4974
5080
  error: errorData(error), message: getErrorMessage(error),
4975
5081
  });
4976
5082
  await errorEmitter.next(error);
@@ -5846,14 +5952,33 @@ let ReportStorage$4 = class ReportStorage {
5846
5952
  async getReport(strategyName) {
5847
5953
  const stats = await this.getData();
5848
5954
  if (stats.totalSignals === 0) {
5849
- return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
5955
+ return [
5956
+ `# Backtest Report: ${strategyName}`,
5957
+ "",
5958
+ "No signals closed yet."
5959
+ ].join("\n");
5850
5960
  }
5851
5961
  const header = columns$4.map((col) => col.label);
5852
5962
  const separator = columns$4.map(() => "---");
5853
5963
  const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
5854
5964
  const tableData = [header, separator, ...rows];
5855
- const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
5856
- return str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${stats.totalSignals}`, `**Closed signals:** ${stats.totalSignals}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
5965
+ const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
5966
+ return [
5967
+ `# Backtest Report: ${strategyName}`,
5968
+ "",
5969
+ table,
5970
+ "",
5971
+ `**Total signals:** ${stats.totalSignals}`,
5972
+ `**Closed signals:** ${stats.totalSignals}`,
5973
+ `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
5974
+ `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
5975
+ `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
5976
+ `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
5977
+ `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
5978
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
5979
+ `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
5980
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
5981
+ ].join("\n");
5857
5982
  }
5858
5983
  /**
5859
5984
  * Saves strategy report to disk.
@@ -6135,6 +6260,16 @@ const columns$3 = [
6135
6260
  label: "Stop Loss",
6136
6261
  format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
6137
6262
  },
6263
+ {
6264
+ key: "percentTp",
6265
+ label: "% to TP",
6266
+ format: (data) => data.percentTp !== undefined ? `${data.percentTp.toFixed(2)}%` : "N/A",
6267
+ },
6268
+ {
6269
+ key: "percentSl",
6270
+ label: "% to SL",
6271
+ format: (data) => data.percentSl !== undefined ? `${data.percentSl.toFixed(2)}%` : "N/A",
6272
+ },
6138
6273
  {
6139
6274
  key: "pnl",
6140
6275
  label: "PNL (net)",
@@ -6237,6 +6372,8 @@ let ReportStorage$3 = class ReportStorage {
6237
6372
  openPrice: data.signal.priceOpen,
6238
6373
  takeProfit: data.signal.priceTakeProfit,
6239
6374
  stopLoss: data.signal.priceStopLoss,
6375
+ percentTp: data.percentTp,
6376
+ percentSl: data.percentSl,
6240
6377
  };
6241
6378
  // Replace existing event or add new one
6242
6379
  if (existingIndex !== -1) {
@@ -6378,14 +6515,33 @@ let ReportStorage$3 = class ReportStorage {
6378
6515
  async getReport(strategyName) {
6379
6516
  const stats = await this.getData();
6380
6517
  if (stats.totalEvents === 0) {
6381
- return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
6518
+ return [
6519
+ `# Live Trading Report: ${strategyName}`,
6520
+ "",
6521
+ "No events recorded yet."
6522
+ ].join("\n");
6382
6523
  }
6383
6524
  const header = columns$3.map((col) => col.label);
6384
6525
  const separator = columns$3.map(() => "---");
6385
6526
  const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
6386
6527
  const tableData = [header, separator, ...rows];
6387
- const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
6388
- return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Closed signals:** ${stats.totalClosed}`, `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`, `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`, `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`, `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`, `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`, `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`, `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`, `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`);
6528
+ const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6529
+ return [
6530
+ `# Live Trading Report: ${strategyName}`,
6531
+ "",
6532
+ table,
6533
+ "",
6534
+ `**Total events:** ${stats.totalEvents}`,
6535
+ `**Closed signals:** ${stats.totalClosed}`,
6536
+ `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
6537
+ `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
6538
+ `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
6539
+ `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
6540
+ `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
6541
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
6542
+ `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
6543
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
6544
+ ].join("\n");
6389
6545
  }
6390
6546
  /**
6391
6547
  * Saves strategy report to disk.
@@ -6782,14 +6938,28 @@ let ReportStorage$2 = class ReportStorage {
6782
6938
  async getReport(strategyName) {
6783
6939
  const stats = await this.getData();
6784
6940
  if (stats.totalEvents === 0) {
6785
- return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
6941
+ return [
6942
+ `# Scheduled Signals Report: ${strategyName}`,
6943
+ "",
6944
+ "No scheduled signals recorded yet."
6945
+ ].join("\n");
6786
6946
  }
6787
6947
  const header = columns$2.map((col) => col.label);
6788
6948
  const separator = columns$2.map(() => "---");
6789
6949
  const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6790
6950
  const tableData = [header, separator, ...rows];
6791
- const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
6792
- return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Scheduled signals:** ${stats.totalScheduled}`, `**Cancelled signals:** ${stats.totalCancelled}`, `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`, `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`);
6951
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
6952
+ return [
6953
+ `# Scheduled Signals Report: ${strategyName}`,
6954
+ "",
6955
+ table,
6956
+ "",
6957
+ `**Total events:** ${stats.totalEvents}`,
6958
+ `**Scheduled signals:** ${stats.totalScheduled}`,
6959
+ `**Cancelled signals:** ${stats.totalCancelled}`,
6960
+ `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
6961
+ `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
6962
+ ].join("\n");
6793
6963
  }
6794
6964
  /**
6795
6965
  * Saves strategy report to disk.
@@ -7107,7 +7277,11 @@ class PerformanceStorage {
7107
7277
  async getReport(strategyName) {
7108
7278
  const stats = await this.getData(strategyName);
7109
7279
  if (stats.totalEvents === 0) {
7110
- return str.newline(`# Performance Report: ${strategyName}`, "", "No performance metrics recorded yet.");
7280
+ return [
7281
+ `# Performance Report: ${strategyName}`,
7282
+ "",
7283
+ "No performance metrics recorded yet."
7284
+ ].join("\n");
7111
7285
  }
7112
7286
  // Sort metrics by total duration (descending) to show bottlenecks first
7113
7287
  const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
@@ -7144,13 +7318,29 @@ class PerformanceStorage {
7144
7318
  metric.maxWaitTime.toFixed(2),
7145
7319
  ]);
7146
7320
  const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
7147
- const summaryTable = str.newline(summaryTableData.map((row) => `| ${row.join(" | ")} |`));
7321
+ const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7148
7322
  // Calculate percentage of total time for each metric
7149
7323
  const percentages = sortedMetrics.map((metric) => {
7150
7324
  const pct = (metric.totalDuration / stats.totalDuration) * 100;
7151
7325
  return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
7152
7326
  });
7153
- return str.newline(`# Performance Report: ${strategyName}`, "", `**Total events:** ${stats.totalEvents}`, `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`, `**Number of metric types:** ${Object.keys(stats.metricStats).length}`, "", "## Time Distribution", "", str.newline(percentages), "", "## Detailed Metrics", "", summaryTable, "", "**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type.");
7327
+ return [
7328
+ `# Performance Report: ${strategyName}`,
7329
+ "",
7330
+ `**Total events:** ${stats.totalEvents}`,
7331
+ `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
7332
+ `**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
7333
+ "",
7334
+ "## Time Distribution",
7335
+ "",
7336
+ percentages.join("\n"),
7337
+ "",
7338
+ "## Detailed Metrics",
7339
+ "",
7340
+ summaryTable,
7341
+ "",
7342
+ "**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type."
7343
+ ].join("\n");
7154
7344
  }
7155
7345
  /**
7156
7346
  * Saves performance report to disk.
@@ -7554,7 +7744,7 @@ let ReportStorage$1 = class ReportStorage {
7554
7744
  // Build table rows
7555
7745
  const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7556
7746
  const tableData = [header, separator, ...rows];
7557
- return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7747
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7558
7748
  }
7559
7749
  /**
7560
7750
  * Generates PNL table showing all closed signals across all strategies (View).
@@ -7591,7 +7781,7 @@ let ReportStorage$1 = class ReportStorage {
7591
7781
  // Build table rows
7592
7782
  const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7593
7783
  const tableData = [header, separator, ...rows];
7594
- return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7784
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7595
7785
  }
7596
7786
  /**
7597
7787
  * Generates markdown report with all strategy results (View).
@@ -7606,7 +7796,30 @@ let ReportStorage$1 = class ReportStorage {
7606
7796
  const results = await this.getData(symbol, metric, context);
7607
7797
  // Get total signals for best strategy
7608
7798
  const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
7609
- return str.newline(`# Walker Comparison Report: ${results.walkerName}`, "", `**Symbol:** ${results.symbol}`, `**Exchange:** ${results.exchangeName}`, `**Frame:** ${results.frameName}`, `**Optimization Metric:** ${results.metric}`, `**Strategies Tested:** ${results.totalStrategies}`, "", `## Best Strategy: ${results.bestStrategy}`, "", `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`, `**Total Signals:** ${bestStrategySignals}`, "", "## Top Strategies Comparison", "", this.getComparisonTable(metric, 10), "", "## All Signals (PNL Table)", "", this.getPnlTable(), "", "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
7799
+ return [
7800
+ `# Walker Comparison Report: ${results.walkerName}`,
7801
+ "",
7802
+ `**Symbol:** ${results.symbol}`,
7803
+ `**Exchange:** ${results.exchangeName}`,
7804
+ `**Frame:** ${results.frameName}`,
7805
+ `**Optimization Metric:** ${results.metric}`,
7806
+ `**Strategies Tested:** ${results.totalStrategies}`,
7807
+ "",
7808
+ `## Best Strategy: ${results.bestStrategy}`,
7809
+ "",
7810
+ `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
7811
+ `**Total Signals:** ${bestStrategySignals}`,
7812
+ "",
7813
+ "## Top Strategies Comparison",
7814
+ "",
7815
+ this.getComparisonTable(metric, 10),
7816
+ "",
7817
+ "## All Signals (PNL Table)",
7818
+ "",
7819
+ this.getPnlTable(),
7820
+ "",
7821
+ "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
7822
+ ].join("\n");
7610
7823
  }
7611
7824
  /**
7612
7825
  * Saves walker report to disk.
@@ -8120,14 +8333,24 @@ class HeatmapStorage {
8120
8333
  async getReport(strategyName) {
8121
8334
  const data = await this.getData();
8122
8335
  if (data.symbols.length === 0) {
8123
- return str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
8336
+ return [
8337
+ `# Portfolio Heatmap: ${strategyName}`,
8338
+ "",
8339
+ "*No data available*"
8340
+ ].join("\n");
8124
8341
  }
8125
8342
  const header = columns$1.map((col) => col.label);
8126
8343
  const separator = columns$1.map(() => "---");
8127
8344
  const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
8128
8345
  const tableData = [header, separator, ...rows];
8129
- const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
8130
- return str.newline(`# Portfolio Heatmap: ${strategyName}`, "", `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`, "", table);
8346
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8347
+ return [
8348
+ `# Portfolio Heatmap: ${strategyName}`,
8349
+ "",
8350
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
8351
+ "",
8352
+ table
8353
+ ].join("\n");
8131
8354
  }
8132
8355
  /**
8133
8356
  * Saves heatmap report to disk.
@@ -10685,14 +10908,26 @@ class ReportStorage {
10685
10908
  async getReport(symbol, strategyName) {
10686
10909
  const stats = await this.getData();
10687
10910
  if (stats.totalEvents === 0) {
10688
- return str.newline(`# Partial Profit/Loss Report: ${symbol}:${strategyName}`, "", "No partial profit/loss events recorded yet.");
10911
+ return [
10912
+ `# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
10913
+ "",
10914
+ "No partial profit/loss events recorded yet."
10915
+ ].join("\n");
10689
10916
  }
10690
10917
  const header = columns.map((col) => col.label);
10691
10918
  const separator = columns.map(() => "---");
10692
10919
  const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
10693
10920
  const tableData = [header, separator, ...rows];
10694
- const table = str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
10695
- return str.newline(`# Partial Profit/Loss Report: ${symbol}:${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Profit events:** ${stats.totalProfit}`, `**Loss events:** ${stats.totalLoss}`);
10921
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
10922
+ return [
10923
+ `# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
10924
+ "",
10925
+ table,
10926
+ "",
10927
+ `**Total events:** ${stats.totalEvents}`,
10928
+ `**Profit events:** ${stats.totalProfit}`,
10929
+ `**Loss events:** ${stats.totalLoss}`
10930
+ ].join("\n");
10696
10931
  }
10697
10932
  /**
10698
10933
  * Saves symbol-strategy report to disk.