backtest-kit 1.4.15 → 1.5.1

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
@@ -38,6 +38,15 @@ const GLOBAL_CONFIG = {
38
38
  * Default: 1440 minutes (1 day)
39
39
  */
40
40
  CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
41
+ /**
42
+ * Maximum time allowed for signal generation (in seconds).
43
+ * Prevents long-running or stuck signal generation routines from blocking
44
+ * execution or consuming resources indefinitely. If generation exceeds this
45
+ * threshold the attempt should be aborted, logged and optionally retried.
46
+ *
47
+ * Default: 180 seconds (3 minutes)
48
+ */
49
+ CC_MAX_SIGNAL_GENERATION_SECONDS: 180,
41
50
  /**
42
51
  * Number of retries for getCandles function
43
52
  * Default: 3 retries
@@ -1700,6 +1709,53 @@ var emitters = /*#__PURE__*/Object.freeze({
1700
1709
  walkerStopSubject: walkerStopSubject
1701
1710
  });
1702
1711
 
1712
+ /**
1713
+ * Converts markdown content to plain text with minimal formatting
1714
+ * @param content - Markdown string to convert
1715
+ * @returns Plain text representation
1716
+ */
1717
+ const toPlainString = (content) => {
1718
+ if (!content) {
1719
+ return "";
1720
+ }
1721
+ let text = content;
1722
+ // Remove code blocks
1723
+ text = text.replace(/```[\s\S]*?```/g, "");
1724
+ text = text.replace(/`([^`]+)`/g, "$1");
1725
+ // Remove images
1726
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
1727
+ // Convert links to text only (keep link text, remove URL)
1728
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
1729
+ // Remove headers (convert to plain text)
1730
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
1731
+ // Remove bold and italic markers
1732
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
1733
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
1734
+ text = text.replace(/\*(.+?)\*/g, "$1");
1735
+ text = text.replace(/___(.+?)___/g, "$1");
1736
+ text = text.replace(/__(.+?)__/g, "$1");
1737
+ text = text.replace(/_(.+?)_/g, "$1");
1738
+ // Remove strikethrough
1739
+ text = text.replace(/~~(.+?)~~/g, "$1");
1740
+ // Convert lists to plain text with bullets
1741
+ text = text.replace(/^\s*[-*+]\s+/gm, "• ");
1742
+ text = text.replace(/^\s*\d+\.\s+/gm, "• ");
1743
+ // Remove blockquotes
1744
+ text = text.replace(/^\s*>\s+/gm, "");
1745
+ // Remove horizontal rules
1746
+ text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
1747
+ // Remove HTML tags
1748
+ text = text.replace(/<[^>]+>/g, "");
1749
+ // Remove excessive whitespace and normalize line breaks
1750
+ text = text.replace(/\n[\s\n]*\n/g, "\n");
1751
+ text = text.replace(/[ \t]+/g, " ");
1752
+ // Remove all newline characters
1753
+ text = text.replace(/\n/g, " ");
1754
+ // Remove excessive spaces after newline removal
1755
+ text = text.replace(/\s+/g, " ");
1756
+ return text.trim();
1757
+ };
1758
+
1703
1759
  const INTERVAL_MINUTES$1 = {
1704
1760
  "1m": 1,
1705
1761
  "3m": 3,
@@ -1708,6 +1764,7 @@ const INTERVAL_MINUTES$1 = {
1708
1764
  "30m": 30,
1709
1765
  "1h": 60,
1710
1766
  };
1767
+ const TIMEOUT_SYMBOL = Symbol('timeout');
1711
1768
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1712
1769
  const errors = [];
1713
1770
  // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
@@ -1891,7 +1948,14 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1891
1948
  }))) {
1892
1949
  return null;
1893
1950
  }
1894
- const signal = await self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when);
1951
+ const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
1952
+ const signal = await Promise.race([
1953
+ self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
1954
+ functoolsKit.sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
1955
+ ]);
1956
+ if (typeof signal === "symbol") {
1957
+ throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
1958
+ }
1895
1959
  if (!signal) {
1896
1960
  return null;
1897
1961
  }
@@ -1912,7 +1976,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1912
1976
  id: functoolsKit.randomString(),
1913
1977
  priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
1914
1978
  position: signal.position,
1915
- note: signal.note,
1979
+ note: toPlainString(signal.note),
1916
1980
  priceTakeProfit: signal.priceTakeProfit,
1917
1981
  priceStopLoss: signal.priceStopLoss,
1918
1982
  minuteEstimatedTime: signal.minuteEstimatedTime,
@@ -1932,7 +1996,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1932
1996
  id: functoolsKit.randomString(),
1933
1997
  priceOpen: signal.priceOpen,
1934
1998
  position: signal.position,
1935
- note: signal.note,
1999
+ note: toPlainString(signal.note),
1936
2000
  priceTakeProfit: signal.priceTakeProfit,
1937
2001
  priceStopLoss: signal.priceStopLoss,
1938
2002
  minuteEstimatedTime: signal.minuteEstimatedTime,
@@ -1951,6 +2015,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1951
2015
  id: functoolsKit.randomString(),
1952
2016
  priceOpen: currentPrice,
1953
2017
  ...signal,
2018
+ note: toPlainString(signal.note),
1954
2019
  symbol: self.params.execution.context.symbol,
1955
2020
  exchangeName: self.params.method.context.exchangeName,
1956
2021
  strategyName: self.params.method.context.strategyName,
@@ -2176,6 +2241,8 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
2176
2241
  strategyName: self.params.method.context.strategyName,
2177
2242
  exchangeName: self.params.method.context.exchangeName,
2178
2243
  symbol: self.params.execution.context.symbol,
2244
+ percentTp: 0,
2245
+ percentSl: 0,
2179
2246
  };
2180
2247
  if (self.params.callbacks?.onTick) {
2181
2248
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -2279,7 +2346,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2279
2346
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2280
2347
  }
2281
2348
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2282
- await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice);
2349
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2283
2350
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2284
2351
  strategyName: self.params.method.context.strategyName,
2285
2352
  riskName: self.params.riskName,
@@ -2302,31 +2369,56 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2302
2369
  return result;
2303
2370
  };
2304
2371
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2305
- // Calculate revenue percentage for partial fill/loss callbacks
2372
+ let percentTp = 0;
2373
+ let percentSl = 0;
2374
+ // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2306
2375
  {
2307
- let revenuePercent = 0;
2308
2376
  if (signal.position === "long") {
2309
- // For long: positive if current > open, negative if current < open
2310
- revenuePercent = ((currentPrice - signal.priceOpen) / signal.priceOpen) * 100;
2377
+ // For long: calculate progress towards TP or SL
2378
+ const currentDistance = currentPrice - signal.priceOpen;
2379
+ if (currentDistance > 0) {
2380
+ // Moving towards TP
2381
+ const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2382
+ const progressPercent = (currentDistance / tpDistance) * 100;
2383
+ percentTp = Math.min(progressPercent, 100);
2384
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2385
+ if (self.params.callbacks?.onPartialProfit) {
2386
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2387
+ }
2388
+ }
2389
+ else if (currentDistance < 0) {
2390
+ // Moving towards SL
2391
+ const slDistance = signal.priceOpen - signal.priceStopLoss;
2392
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2393
+ percentSl = Math.min(progressPercent, 100);
2394
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2395
+ if (self.params.callbacks?.onPartialLoss) {
2396
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2397
+ }
2398
+ }
2311
2399
  }
2312
2400
  else if (signal.position === "short") {
2313
- // For short: positive if current < open, negative if current > open
2314
- revenuePercent = ((signal.priceOpen - currentPrice) / signal.priceOpen) * 100;
2315
- }
2316
- // Call onPartialProfit if revenue is positive (but not reached TP yet)
2317
- if (revenuePercent > 0) {
2318
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2319
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2320
- if (self.params.callbacks?.onPartialProfit) {
2321
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2401
+ // For short: calculate progress towards TP or SL
2402
+ const currentDistance = signal.priceOpen - currentPrice;
2403
+ if (currentDistance > 0) {
2404
+ // Moving towards TP
2405
+ const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2406
+ const progressPercent = (currentDistance / tpDistance) * 100;
2407
+ percentTp = Math.min(progressPercent, 100);
2408
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2409
+ if (self.params.callbacks?.onPartialProfit) {
2410
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2411
+ }
2322
2412
  }
2323
- }
2324
- // Call onPartialLoss if revenue is negative (but not hit SL yet)
2325
- if (revenuePercent < 0) {
2326
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2327
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2328
- if (self.params.callbacks?.onPartialLoss) {
2329
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2413
+ if (currentDistance < 0) {
2414
+ // Moving towards SL
2415
+ const slDistance = signal.priceStopLoss - signal.priceOpen;
2416
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2417
+ percentSl = Math.min(progressPercent, 100);
2418
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2419
+ if (self.params.callbacks?.onPartialLoss) {
2420
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2421
+ }
2330
2422
  }
2331
2423
  }
2332
2424
  }
@@ -2337,6 +2429,8 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2337
2429
  strategyName: self.params.method.context.strategyName,
2338
2430
  exchangeName: self.params.method.context.exchangeName,
2339
2431
  symbol: self.params.execution.context.symbol,
2432
+ percentTp,
2433
+ percentSl,
2340
2434
  };
2341
2435
  if (self.params.callbacks?.onTick) {
2342
2436
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -2458,7 +2552,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2458
2552
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2459
2553
  }
2460
2554
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2461
- await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice);
2555
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2462
2556
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2463
2557
  strategyName: self.params.method.context.strategyName,
2464
2558
  riskName: self.params.riskName,
@@ -2601,29 +2695,50 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2601
2695
  return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
2602
2696
  }
2603
2697
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
2604
- // Calculate revenue percentage
2698
+ // Calculate percentage of path to TP/SL
2605
2699
  {
2606
- let revenuePercent = 0;
2607
2700
  if (signal.position === "long") {
2608
- revenuePercent = ((averagePrice - signal.priceOpen) / signal.priceOpen) * 100;
2701
+ // For long: calculate progress towards TP or SL
2702
+ const currentDistance = averagePrice - signal.priceOpen;
2703
+ if (currentDistance > 0) {
2704
+ // Moving towards TP
2705
+ const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2706
+ const progressPercent = (currentDistance / tpDistance) * 100;
2707
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2708
+ if (self.params.callbacks?.onPartialProfit) {
2709
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2710
+ }
2711
+ }
2712
+ else if (currentDistance < 0) {
2713
+ // Moving towards SL
2714
+ const slDistance = signal.priceOpen - signal.priceStopLoss;
2715
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2716
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2717
+ if (self.params.callbacks?.onPartialLoss) {
2718
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2719
+ }
2720
+ }
2609
2721
  }
2610
2722
  else if (signal.position === "short") {
2611
- revenuePercent = ((signal.priceOpen - averagePrice) / signal.priceOpen) * 100;
2612
- }
2613
- // Call onPartialProfit if revenue is positive (but not reached TP yet)
2614
- if (revenuePercent > 0) {
2615
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2616
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2617
- if (self.params.callbacks?.onPartialProfit) {
2618
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2723
+ // For short: calculate progress towards TP or SL
2724
+ const currentDistance = signal.priceOpen - averagePrice;
2725
+ if (currentDistance > 0) {
2726
+ // Moving towards TP
2727
+ const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2728
+ const progressPercent = (currentDistance / tpDistance) * 100;
2729
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2730
+ if (self.params.callbacks?.onPartialProfit) {
2731
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2732
+ }
2619
2733
  }
2620
- }
2621
- // Call onPartialLoss if revenue is negative (but not hit SL yet)
2622
- if (revenuePercent < 0) {
2623
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2624
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2625
- if (self.params.callbacks?.onPartialLoss) {
2626
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2734
+ if (currentDistance < 0) {
2735
+ // Moving towards SL
2736
+ const slDistance = signal.priceStopLoss - signal.priceOpen;
2737
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2738
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest, self.params.execution.context.when);
2739
+ if (self.params.callbacks?.onPartialLoss) {
2740
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2741
+ }
2627
2742
  }
2628
2743
  }
2629
2744
  }
@@ -2912,6 +3027,8 @@ class ClientStrategy {
2912
3027
  action: "active",
2913
3028
  signal: scheduled,
2914
3029
  currentPrice: lastPrice,
3030
+ percentSl: 0,
3031
+ percentTp: 0,
2915
3032
  strategyName: this.params.method.context.strategyName,
2916
3033
  exchangeName: this.params.method.context.exchangeName,
2917
3034
  symbol: this.params.execution.context.symbol,
@@ -5613,7 +5730,7 @@ const columns$4 = [
5613
5730
  {
5614
5731
  key: "note",
5615
5732
  label: "Note",
5616
- format: (data) => data.signal.note ?? "N/A",
5733
+ format: (data) => toPlainString(data.signal.note ?? "N/A"),
5617
5734
  },
5618
5735
  {
5619
5736
  key: "openPrice",
@@ -5760,14 +5877,33 @@ let ReportStorage$4 = class ReportStorage {
5760
5877
  async getReport(strategyName) {
5761
5878
  const stats = await this.getData();
5762
5879
  if (stats.totalSignals === 0) {
5763
- return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
5880
+ return [
5881
+ `# Backtest Report: ${strategyName}`,
5882
+ "",
5883
+ "No signals closed yet."
5884
+ ].join("\n");
5764
5885
  }
5765
5886
  const header = columns$4.map((col) => col.label);
5766
5887
  const separator = columns$4.map(() => "---");
5767
5888
  const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
5768
5889
  const tableData = [header, separator, ...rows];
5769
- const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
5770
- return functoolsKit.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)`}`);
5890
+ const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
5891
+ return [
5892
+ `# Backtest Report: ${strategyName}`,
5893
+ "",
5894
+ table,
5895
+ "",
5896
+ `**Total signals:** ${stats.totalSignals}`,
5897
+ `**Closed signals:** ${stats.totalSignals}`,
5898
+ `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
5899
+ `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
5900
+ `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
5901
+ `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
5902
+ `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
5903
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
5904
+ `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
5905
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
5906
+ ].join("\n");
5771
5907
  }
5772
5908
  /**
5773
5909
  * Saves strategy report to disk.
@@ -6025,7 +6161,7 @@ const columns$3 = [
6025
6161
  {
6026
6162
  key: "note",
6027
6163
  label: "Note",
6028
- format: (data) => data.note ?? "N/A",
6164
+ format: (data) => toPlainString(data.note ?? "N/A"),
6029
6165
  },
6030
6166
  {
6031
6167
  key: "currentPrice",
@@ -6049,6 +6185,16 @@ const columns$3 = [
6049
6185
  label: "Stop Loss",
6050
6186
  format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
6051
6187
  },
6188
+ {
6189
+ key: "percentTp",
6190
+ label: "% to TP",
6191
+ format: (data) => data.percentTp !== undefined ? `${data.percentTp.toFixed(2)}%` : "N/A",
6192
+ },
6193
+ {
6194
+ key: "percentSl",
6195
+ label: "% to SL",
6196
+ format: (data) => data.percentSl !== undefined ? `${data.percentSl.toFixed(2)}%` : "N/A",
6197
+ },
6052
6198
  {
6053
6199
  key: "pnl",
6054
6200
  label: "PNL (net)",
@@ -6151,6 +6297,8 @@ let ReportStorage$3 = class ReportStorage {
6151
6297
  openPrice: data.signal.priceOpen,
6152
6298
  takeProfit: data.signal.priceTakeProfit,
6153
6299
  stopLoss: data.signal.priceStopLoss,
6300
+ percentTp: data.percentTp,
6301
+ percentSl: data.percentSl,
6154
6302
  };
6155
6303
  // Replace existing event or add new one
6156
6304
  if (existingIndex !== -1) {
@@ -6292,14 +6440,33 @@ let ReportStorage$3 = class ReportStorage {
6292
6440
  async getReport(strategyName) {
6293
6441
  const stats = await this.getData();
6294
6442
  if (stats.totalEvents === 0) {
6295
- return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
6443
+ return [
6444
+ `# Live Trading Report: ${strategyName}`,
6445
+ "",
6446
+ "No events recorded yet."
6447
+ ].join("\n");
6296
6448
  }
6297
6449
  const header = columns$3.map((col) => col.label);
6298
6450
  const separator = columns$3.map(() => "---");
6299
6451
  const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
6300
6452
  const tableData = [header, separator, ...rows];
6301
- const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
6302
- return functoolsKit.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)`}`);
6453
+ const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6454
+ return [
6455
+ `# Live Trading Report: ${strategyName}`,
6456
+ "",
6457
+ table,
6458
+ "",
6459
+ `**Total events:** ${stats.totalEvents}`,
6460
+ `**Closed signals:** ${stats.totalClosed}`,
6461
+ `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
6462
+ `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
6463
+ `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
6464
+ `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
6465
+ `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
6466
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
6467
+ `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
6468
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
6469
+ ].join("\n");
6303
6470
  }
6304
6471
  /**
6305
6472
  * Saves strategy report to disk.
@@ -6552,7 +6719,7 @@ const columns$2 = [
6552
6719
  {
6553
6720
  key: "note",
6554
6721
  label: "Note",
6555
- format: (data) => data.note ?? "N/A",
6722
+ format: (data) => toPlainString(data.note ?? "N/A"),
6556
6723
  },
6557
6724
  {
6558
6725
  key: "currentPrice",
@@ -6696,14 +6863,28 @@ let ReportStorage$2 = class ReportStorage {
6696
6863
  async getReport(strategyName) {
6697
6864
  const stats = await this.getData();
6698
6865
  if (stats.totalEvents === 0) {
6699
- return functoolsKit.str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
6866
+ return [
6867
+ `# Scheduled Signals Report: ${strategyName}`,
6868
+ "",
6869
+ "No scheduled signals recorded yet."
6870
+ ].join("\n");
6700
6871
  }
6701
6872
  const header = columns$2.map((col) => col.label);
6702
6873
  const separator = columns$2.map(() => "---");
6703
6874
  const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6704
6875
  const tableData = [header, separator, ...rows];
6705
- const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
6706
- return functoolsKit.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`}`);
6876
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
6877
+ return [
6878
+ `# Scheduled Signals Report: ${strategyName}`,
6879
+ "",
6880
+ table,
6881
+ "",
6882
+ `**Total events:** ${stats.totalEvents}`,
6883
+ `**Scheduled signals:** ${stats.totalScheduled}`,
6884
+ `**Cancelled signals:** ${stats.totalCancelled}`,
6885
+ `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
6886
+ `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
6887
+ ].join("\n");
6707
6888
  }
6708
6889
  /**
6709
6890
  * Saves strategy report to disk.
@@ -7021,7 +7202,11 @@ class PerformanceStorage {
7021
7202
  async getReport(strategyName) {
7022
7203
  const stats = await this.getData(strategyName);
7023
7204
  if (stats.totalEvents === 0) {
7024
- return functoolsKit.str.newline(`# Performance Report: ${strategyName}`, "", "No performance metrics recorded yet.");
7205
+ return [
7206
+ `# Performance Report: ${strategyName}`,
7207
+ "",
7208
+ "No performance metrics recorded yet."
7209
+ ].join("\n");
7025
7210
  }
7026
7211
  // Sort metrics by total duration (descending) to show bottlenecks first
7027
7212
  const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
@@ -7058,13 +7243,29 @@ class PerformanceStorage {
7058
7243
  metric.maxWaitTime.toFixed(2),
7059
7244
  ]);
7060
7245
  const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
7061
- const summaryTable = functoolsKit.str.newline(summaryTableData.map((row) => `| ${row.join(" | ")} |`));
7246
+ const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7062
7247
  // Calculate percentage of total time for each metric
7063
7248
  const percentages = sortedMetrics.map((metric) => {
7064
7249
  const pct = (metric.totalDuration / stats.totalDuration) * 100;
7065
7250
  return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
7066
7251
  });
7067
- return functoolsKit.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", "", functoolsKit.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.");
7252
+ return [
7253
+ `# Performance Report: ${strategyName}`,
7254
+ "",
7255
+ `**Total events:** ${stats.totalEvents}`,
7256
+ `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
7257
+ `**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
7258
+ "",
7259
+ "## Time Distribution",
7260
+ "",
7261
+ percentages.join("\n"),
7262
+ "",
7263
+ "## Detailed Metrics",
7264
+ "",
7265
+ summaryTable,
7266
+ "",
7267
+ "**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."
7268
+ ].join("\n");
7068
7269
  }
7069
7270
  /**
7070
7271
  * Saves performance report to disk.
@@ -7468,7 +7669,7 @@ let ReportStorage$1 = class ReportStorage {
7468
7669
  // Build table rows
7469
7670
  const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7470
7671
  const tableData = [header, separator, ...rows];
7471
- return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7672
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7472
7673
  }
7473
7674
  /**
7474
7675
  * Generates PNL table showing all closed signals across all strategies (View).
@@ -7505,7 +7706,7 @@ let ReportStorage$1 = class ReportStorage {
7505
7706
  // Build table rows
7506
7707
  const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7507
7708
  const tableData = [header, separator, ...rows];
7508
- return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7709
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7509
7710
  }
7510
7711
  /**
7511
7712
  * Generates markdown report with all strategy results (View).
@@ -7520,7 +7721,30 @@ let ReportStorage$1 = class ReportStorage {
7520
7721
  const results = await this.getData(symbol, metric, context);
7521
7722
  // Get total signals for best strategy
7522
7723
  const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
7523
- return functoolsKit.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).");
7724
+ return [
7725
+ `# Walker Comparison Report: ${results.walkerName}`,
7726
+ "",
7727
+ `**Symbol:** ${results.symbol}`,
7728
+ `**Exchange:** ${results.exchangeName}`,
7729
+ `**Frame:** ${results.frameName}`,
7730
+ `**Optimization Metric:** ${results.metric}`,
7731
+ `**Strategies Tested:** ${results.totalStrategies}`,
7732
+ "",
7733
+ `## Best Strategy: ${results.bestStrategy}`,
7734
+ "",
7735
+ `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
7736
+ `**Total Signals:** ${bestStrategySignals}`,
7737
+ "",
7738
+ "## Top Strategies Comparison",
7739
+ "",
7740
+ this.getComparisonTable(metric, 10),
7741
+ "",
7742
+ "## All Signals (PNL Table)",
7743
+ "",
7744
+ this.getPnlTable(),
7745
+ "",
7746
+ "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
7747
+ ].join("\n");
7524
7748
  }
7525
7749
  /**
7526
7750
  * Saves walker report to disk.
@@ -8034,14 +8258,24 @@ class HeatmapStorage {
8034
8258
  async getReport(strategyName) {
8035
8259
  const data = await this.getData();
8036
8260
  if (data.symbols.length === 0) {
8037
- return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
8261
+ return [
8262
+ `# Portfolio Heatmap: ${strategyName}`,
8263
+ "",
8264
+ "*No data available*"
8265
+ ].join("\n");
8038
8266
  }
8039
8267
  const header = columns$1.map((col) => col.label);
8040
8268
  const separator = columns$1.map(() => "---");
8041
8269
  const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
8042
8270
  const tableData = [header, separator, ...rows];
8043
- const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
8044
- return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`, "", table);
8271
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8272
+ return [
8273
+ `# Portfolio Heatmap: ${strategyName}`,
8274
+ "",
8275
+ `**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%+.2f%%") : "N/A"} | **Portfolio Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio, "%.2f") : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades}`,
8276
+ "",
8277
+ table
8278
+ ].join("\n");
8045
8279
  }
8046
8280
  /**
8047
8281
  * Saves heatmap report to disk.
@@ -8764,6 +8998,8 @@ class OptimizerTemplateService {
8764
8998
  interval,
8765
8999
  prompt,
8766
9000
  });
9001
+ // Convert prompt to plain text first
9002
+ const plainPrompt = toPlainString(prompt);
8767
9003
  // Escape special characters to prevent code injection
8768
9004
  const escapedStrategyName = String(strategyName)
8769
9005
  .replace(/\\/g, '\\\\')
@@ -8771,7 +9007,7 @@ class OptimizerTemplateService {
8771
9007
  const escapedInterval = String(interval)
8772
9008
  .replace(/\\/g, '\\\\')
8773
9009
  .replace(/"/g, '\\"');
8774
- const escapedPrompt = String(prompt)
9010
+ const escapedPrompt = String(plainPrompt)
8775
9011
  .replace(/\\/g, '\\\\')
8776
9012
  .replace(/`/g, '\\`')
8777
9013
  .replace(/\$/g, '\\$');
@@ -9952,7 +10188,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
9952
10188
  }
9953
10189
  }
9954
10190
  if (shouldPersist) {
9955
- await self._persistState(symbol);
10191
+ await self._persistState(symbol, backtest);
9956
10192
  }
9957
10193
  };
9958
10194
  /**
@@ -9999,7 +10235,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
9999
10235
  }
10000
10236
  }
10001
10237
  if (shouldPersist) {
10002
- await self._persistState(symbol);
10238
+ await self._persistState(symbol, backtest);
10003
10239
  }
10004
10240
  };
10005
10241
  /**
@@ -10135,7 +10371,10 @@ class ClientPartial {
10135
10371
  * @param symbol - Trading pair symbol
10136
10372
  * @returns Promise that resolves when persistence is complete
10137
10373
  */
10138
- async _persistState(symbol) {
10374
+ async _persistState(symbol, backtest) {
10375
+ if (backtest) {
10376
+ return;
10377
+ }
10139
10378
  this.params.logger.debug("ClientPartial persistState", { symbol });
10140
10379
  if (this._states === NEED_FETCH) {
10141
10380
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
@@ -10258,7 +10497,7 @@ class ClientPartial {
10258
10497
  * // Cleanup: PartialConnectionService.getPartial.clear(signal.id)
10259
10498
  * ```
10260
10499
  */
10261
- async clear(symbol, data, priceClose) {
10500
+ async clear(symbol, data, priceClose, backtest) {
10262
10501
  this.params.logger.log("ClientPartial clear", {
10263
10502
  symbol,
10264
10503
  data,
@@ -10268,7 +10507,7 @@ class ClientPartial {
10268
10507
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
10269
10508
  }
10270
10509
  this._states.delete(data.id);
10271
- await this._persistState(symbol);
10510
+ await this._persistState(symbol, backtest);
10272
10511
  }
10273
10512
  }
10274
10513
 
@@ -10439,7 +10678,7 @@ class PartialConnectionService {
10439
10678
  * @param priceClose - Final closing price
10440
10679
  * @returns Promise that resolves when clear is complete
10441
10680
  */
10442
- this.clear = async (symbol, data, priceClose) => {
10681
+ this.clear = async (symbol, data, priceClose, backtest) => {
10443
10682
  this.loggerService.log("partialConnectionService profit", {
10444
10683
  symbol,
10445
10684
  data,
@@ -10447,7 +10686,7 @@ class PartialConnectionService {
10447
10686
  });
10448
10687
  const partial = this.getPartial(data.id);
10449
10688
  await partial.waitForInit(symbol);
10450
- await partial.clear(symbol, data, priceClose);
10689
+ await partial.clear(symbol, data, priceClose, backtest);
10451
10690
  this.getPartial.clear(data.id);
10452
10691
  };
10453
10692
  }
@@ -10594,14 +10833,26 @@ class ReportStorage {
10594
10833
  async getReport(symbol, strategyName) {
10595
10834
  const stats = await this.getData();
10596
10835
  if (stats.totalEvents === 0) {
10597
- return functoolsKit.str.newline(`# Partial Profit/Loss Report: ${symbol}:${strategyName}`, "", "No partial profit/loss events recorded yet.");
10836
+ return [
10837
+ `# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
10838
+ "",
10839
+ "No partial profit/loss events recorded yet."
10840
+ ].join("\n");
10598
10841
  }
10599
10842
  const header = columns.map((col) => col.label);
10600
10843
  const separator = columns.map(() => "---");
10601
10844
  const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
10602
10845
  const tableData = [header, separator, ...rows];
10603
- const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
10604
- return functoolsKit.str.newline(`# Partial Profit/Loss Report: ${symbol}:${strategyName}`, "", table, "", `**Total events:** ${stats.totalEvents}`, `**Profit events:** ${stats.totalProfit}`, `**Loss events:** ${stats.totalLoss}`);
10846
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
10847
+ return [
10848
+ `# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
10849
+ "",
10850
+ table,
10851
+ "",
10852
+ `**Total events:** ${stats.totalEvents}`,
10853
+ `**Profit events:** ${stats.totalProfit}`,
10854
+ `**Loss events:** ${stats.totalLoss}`
10855
+ ].join("\n");
10605
10856
  }
10606
10857
  /**
10607
10858
  * Saves symbol-strategy report to disk.
@@ -10916,13 +11167,14 @@ class PartialGlobalService {
10916
11167
  * @param priceClose - Final closing price
10917
11168
  * @returns Promise that resolves when clear is complete
10918
11169
  */
10919
- this.clear = async (symbol, data, priceClose) => {
11170
+ this.clear = async (symbol, data, priceClose, backtest) => {
10920
11171
  this.loggerService.log("partialGlobalService profit", {
10921
11172
  symbol,
10922
11173
  data,
10923
11174
  priceClose,
11175
+ backtest,
10924
11176
  });
10925
- return await this.partialConnectionService.clear(symbol, data, priceClose);
11177
+ return await this.partialConnectionService.clear(symbol, data, priceClose, backtest);
10926
11178
  };
10927
11179
  }
10928
11180
  }