backtest-kit 1.4.15 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -1700,6 +1700,53 @@ var emitters = /*#__PURE__*/Object.freeze({
1700
1700
  walkerStopSubject: walkerStopSubject
1701
1701
  });
1702
1702
 
1703
+ /**
1704
+ * Converts markdown content to plain text with minimal formatting
1705
+ * @param content - Markdown string to convert
1706
+ * @returns Plain text representation
1707
+ */
1708
+ const toPlainString = (content) => {
1709
+ if (!content) {
1710
+ return "";
1711
+ }
1712
+ let text = content;
1713
+ // Remove code blocks
1714
+ text = text.replace(/```[\s\S]*?```/g, "");
1715
+ text = text.replace(/`([^`]+)`/g, "$1");
1716
+ // Remove images
1717
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
1718
+ // Convert links to text only (keep link text, remove URL)
1719
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
1720
+ // Remove headers (convert to plain text)
1721
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
1722
+ // Remove bold and italic markers
1723
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
1724
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
1725
+ text = text.replace(/\*(.+?)\*/g, "$1");
1726
+ text = text.replace(/___(.+?)___/g, "$1");
1727
+ text = text.replace(/__(.+?)__/g, "$1");
1728
+ text = text.replace(/_(.+?)_/g, "$1");
1729
+ // Remove strikethrough
1730
+ text = text.replace(/~~(.+?)~~/g, "$1");
1731
+ // Convert lists to plain text with bullets
1732
+ text = text.replace(/^\s*[-*+]\s+/gm, "• ");
1733
+ text = text.replace(/^\s*\d+\.\s+/gm, "• ");
1734
+ // Remove blockquotes
1735
+ text = text.replace(/^\s*>\s+/gm, "");
1736
+ // Remove horizontal rules
1737
+ text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
1738
+ // Remove HTML tags
1739
+ text = text.replace(/<[^>]+>/g, "");
1740
+ // Remove excessive whitespace and normalize line breaks
1741
+ text = text.replace(/\n[\s\n]*\n/g, "\n");
1742
+ text = text.replace(/[ \t]+/g, " ");
1743
+ // Remove all newline characters
1744
+ text = text.replace(/\n/g, " ");
1745
+ // Remove excessive spaces after newline removal
1746
+ text = text.replace(/\s+/g, " ");
1747
+ return text.trim();
1748
+ };
1749
+
1703
1750
  const INTERVAL_MINUTES$1 = {
1704
1751
  "1m": 1,
1705
1752
  "3m": 3,
@@ -1912,7 +1959,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1912
1959
  id: functoolsKit.randomString(),
1913
1960
  priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
1914
1961
  position: signal.position,
1915
- note: signal.note,
1962
+ note: toPlainString(signal.note),
1916
1963
  priceTakeProfit: signal.priceTakeProfit,
1917
1964
  priceStopLoss: signal.priceStopLoss,
1918
1965
  minuteEstimatedTime: signal.minuteEstimatedTime,
@@ -1932,7 +1979,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1932
1979
  id: functoolsKit.randomString(),
1933
1980
  priceOpen: signal.priceOpen,
1934
1981
  position: signal.position,
1935
- note: signal.note,
1982
+ note: toPlainString(signal.note),
1936
1983
  priceTakeProfit: signal.priceTakeProfit,
1937
1984
  priceStopLoss: signal.priceStopLoss,
1938
1985
  minuteEstimatedTime: signal.minuteEstimatedTime,
@@ -1951,6 +1998,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1951
1998
  id: functoolsKit.randomString(),
1952
1999
  priceOpen: currentPrice,
1953
2000
  ...signal,
2001
+ note: toPlainString(signal.note),
1954
2002
  symbol: self.params.execution.context.symbol,
1955
2003
  exchangeName: self.params.method.context.exchangeName,
1956
2004
  strategyName: self.params.method.context.strategyName,
@@ -2279,7 +2327,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2279
2327
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2280
2328
  }
2281
2329
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2282
- await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice);
2330
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2283
2331
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2284
2332
  strategyName: self.params.method.context.strategyName,
2285
2333
  riskName: self.params.riskName,
@@ -2302,31 +2350,50 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2302
2350
  return result;
2303
2351
  };
2304
2352
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2305
- // Calculate revenue percentage for partial fill/loss callbacks
2353
+ // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2306
2354
  {
2307
- let revenuePercent = 0;
2308
2355
  if (signal.position === "long") {
2309
- // For long: positive if current > open, negative if current < open
2310
- revenuePercent = ((currentPrice - signal.priceOpen) / signal.priceOpen) * 100;
2356
+ // For long: calculate progress towards TP or SL
2357
+ const currentDistance = currentPrice - signal.priceOpen;
2358
+ if (currentDistance > 0) {
2359
+ // Moving towards TP
2360
+ const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2361
+ const progressPercent = (currentDistance / tpDistance) * 100;
2362
+ 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);
2363
+ if (self.params.callbacks?.onPartialProfit) {
2364
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2365
+ }
2366
+ }
2367
+ else if (currentDistance < 0) {
2368
+ // Moving towards SL
2369
+ const slDistance = signal.priceOpen - signal.priceStopLoss;
2370
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2371
+ 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);
2372
+ if (self.params.callbacks?.onPartialLoss) {
2373
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2374
+ }
2375
+ }
2311
2376
  }
2312
2377
  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);
2378
+ // For short: calculate progress towards TP or SL
2379
+ const currentDistance = signal.priceOpen - currentPrice;
2380
+ if (currentDistance > 0) {
2381
+ // Moving towards TP
2382
+ const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2383
+ const progressPercent = (currentDistance / tpDistance) * 100;
2384
+ 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);
2385
+ if (self.params.callbacks?.onPartialProfit) {
2386
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2387
+ }
2322
2388
  }
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);
2389
+ if (currentDistance < 0) {
2390
+ // Moving towards SL
2391
+ const slDistance = signal.priceStopLoss - signal.priceOpen;
2392
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2393
+ 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);
2394
+ if (self.params.callbacks?.onPartialLoss) {
2395
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2396
+ }
2330
2397
  }
2331
2398
  }
2332
2399
  }
@@ -2458,7 +2525,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2458
2525
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2459
2526
  }
2460
2527
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2461
- await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice);
2528
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2462
2529
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2463
2530
  strategyName: self.params.method.context.strategyName,
2464
2531
  riskName: self.params.riskName,
@@ -2601,29 +2668,50 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2601
2668
  return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
2602
2669
  }
2603
2670
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
2604
- // Calculate revenue percentage
2671
+ // Calculate percentage of path to TP/SL
2605
2672
  {
2606
- let revenuePercent = 0;
2607
2673
  if (signal.position === "long") {
2608
- revenuePercent = ((averagePrice - signal.priceOpen) / signal.priceOpen) * 100;
2674
+ // For long: calculate progress towards TP or SL
2675
+ const currentDistance = averagePrice - signal.priceOpen;
2676
+ if (currentDistance > 0) {
2677
+ // Moving towards TP
2678
+ const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2679
+ const progressPercent = (currentDistance / tpDistance) * 100;
2680
+ 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);
2681
+ if (self.params.callbacks?.onPartialProfit) {
2682
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2683
+ }
2684
+ }
2685
+ else if (currentDistance < 0) {
2686
+ // Moving towards SL
2687
+ const slDistance = signal.priceOpen - signal.priceStopLoss;
2688
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2689
+ 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);
2690
+ if (self.params.callbacks?.onPartialLoss) {
2691
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2692
+ }
2693
+ }
2609
2694
  }
2610
2695
  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);
2696
+ // For short: calculate progress towards TP or SL
2697
+ const currentDistance = signal.priceOpen - averagePrice;
2698
+ if (currentDistance > 0) {
2699
+ // Moving towards TP
2700
+ const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2701
+ const progressPercent = (currentDistance / tpDistance) * 100;
2702
+ 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);
2703
+ if (self.params.callbacks?.onPartialProfit) {
2704
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2705
+ }
2619
2706
  }
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);
2707
+ if (currentDistance < 0) {
2708
+ // Moving towards SL
2709
+ const slDistance = signal.priceStopLoss - signal.priceOpen;
2710
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2711
+ 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);
2712
+ if (self.params.callbacks?.onPartialLoss) {
2713
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2714
+ }
2627
2715
  }
2628
2716
  }
2629
2717
  }
@@ -5613,7 +5701,7 @@ const columns$4 = [
5613
5701
  {
5614
5702
  key: "note",
5615
5703
  label: "Note",
5616
- format: (data) => data.signal.note ?? "N/A",
5704
+ format: (data) => toPlainString(data.signal.note ?? "N/A"),
5617
5705
  },
5618
5706
  {
5619
5707
  key: "openPrice",
@@ -6025,7 +6113,7 @@ const columns$3 = [
6025
6113
  {
6026
6114
  key: "note",
6027
6115
  label: "Note",
6028
- format: (data) => data.note ?? "N/A",
6116
+ format: (data) => toPlainString(data.note ?? "N/A"),
6029
6117
  },
6030
6118
  {
6031
6119
  key: "currentPrice",
@@ -6552,7 +6640,7 @@ const columns$2 = [
6552
6640
  {
6553
6641
  key: "note",
6554
6642
  label: "Note",
6555
- format: (data) => data.note ?? "N/A",
6643
+ format: (data) => toPlainString(data.note ?? "N/A"),
6556
6644
  },
6557
6645
  {
6558
6646
  key: "currentPrice",
@@ -8764,6 +8852,8 @@ class OptimizerTemplateService {
8764
8852
  interval,
8765
8853
  prompt,
8766
8854
  });
8855
+ // Convert prompt to plain text first
8856
+ const plainPrompt = toPlainString(prompt);
8767
8857
  // Escape special characters to prevent code injection
8768
8858
  const escapedStrategyName = String(strategyName)
8769
8859
  .replace(/\\/g, '\\\\')
@@ -8771,7 +8861,7 @@ class OptimizerTemplateService {
8771
8861
  const escapedInterval = String(interval)
8772
8862
  .replace(/\\/g, '\\\\')
8773
8863
  .replace(/"/g, '\\"');
8774
- const escapedPrompt = String(prompt)
8864
+ const escapedPrompt = String(plainPrompt)
8775
8865
  .replace(/\\/g, '\\\\')
8776
8866
  .replace(/`/g, '\\`')
8777
8867
  .replace(/\$/g, '\\$');
@@ -9952,7 +10042,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
9952
10042
  }
9953
10043
  }
9954
10044
  if (shouldPersist) {
9955
- await self._persistState(symbol);
10045
+ await self._persistState(symbol, backtest);
9956
10046
  }
9957
10047
  };
9958
10048
  /**
@@ -9999,7 +10089,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
9999
10089
  }
10000
10090
  }
10001
10091
  if (shouldPersist) {
10002
- await self._persistState(symbol);
10092
+ await self._persistState(symbol, backtest);
10003
10093
  }
10004
10094
  };
10005
10095
  /**
@@ -10135,7 +10225,10 @@ class ClientPartial {
10135
10225
  * @param symbol - Trading pair symbol
10136
10226
  * @returns Promise that resolves when persistence is complete
10137
10227
  */
10138
- async _persistState(symbol) {
10228
+ async _persistState(symbol, backtest) {
10229
+ if (backtest) {
10230
+ return;
10231
+ }
10139
10232
  this.params.logger.debug("ClientPartial persistState", { symbol });
10140
10233
  if (this._states === NEED_FETCH) {
10141
10234
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
@@ -10258,7 +10351,7 @@ class ClientPartial {
10258
10351
  * // Cleanup: PartialConnectionService.getPartial.clear(signal.id)
10259
10352
  * ```
10260
10353
  */
10261
- async clear(symbol, data, priceClose) {
10354
+ async clear(symbol, data, priceClose, backtest) {
10262
10355
  this.params.logger.log("ClientPartial clear", {
10263
10356
  symbol,
10264
10357
  data,
@@ -10268,7 +10361,7 @@ class ClientPartial {
10268
10361
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
10269
10362
  }
10270
10363
  this._states.delete(data.id);
10271
- await this._persistState(symbol);
10364
+ await this._persistState(symbol, backtest);
10272
10365
  }
10273
10366
  }
10274
10367
 
@@ -10439,7 +10532,7 @@ class PartialConnectionService {
10439
10532
  * @param priceClose - Final closing price
10440
10533
  * @returns Promise that resolves when clear is complete
10441
10534
  */
10442
- this.clear = async (symbol, data, priceClose) => {
10535
+ this.clear = async (symbol, data, priceClose, backtest) => {
10443
10536
  this.loggerService.log("partialConnectionService profit", {
10444
10537
  symbol,
10445
10538
  data,
@@ -10447,7 +10540,7 @@ class PartialConnectionService {
10447
10540
  });
10448
10541
  const partial = this.getPartial(data.id);
10449
10542
  await partial.waitForInit(symbol);
10450
- await partial.clear(symbol, data, priceClose);
10543
+ await partial.clear(symbol, data, priceClose, backtest);
10451
10544
  this.getPartial.clear(data.id);
10452
10545
  };
10453
10546
  }
@@ -10916,13 +11009,14 @@ class PartialGlobalService {
10916
11009
  * @param priceClose - Final closing price
10917
11010
  * @returns Promise that resolves when clear is complete
10918
11011
  */
10919
- this.clear = async (symbol, data, priceClose) => {
11012
+ this.clear = async (symbol, data, priceClose, backtest) => {
10920
11013
  this.loggerService.log("partialGlobalService profit", {
10921
11014
  symbol,
10922
11015
  data,
10923
11016
  priceClose,
11017
+ backtest,
10924
11018
  });
10925
- return await this.partialConnectionService.clear(symbol, data, priceClose);
11019
+ return await this.partialConnectionService.clear(symbol, data, priceClose, backtest);
10926
11020
  };
10927
11021
  }
10928
11022
  }
package/build/index.mjs CHANGED
@@ -1698,6 +1698,53 @@ var emitters = /*#__PURE__*/Object.freeze({
1698
1698
  walkerStopSubject: walkerStopSubject
1699
1699
  });
1700
1700
 
1701
+ /**
1702
+ * Converts markdown content to plain text with minimal formatting
1703
+ * @param content - Markdown string to convert
1704
+ * @returns Plain text representation
1705
+ */
1706
+ const toPlainString = (content) => {
1707
+ if (!content) {
1708
+ return "";
1709
+ }
1710
+ let text = content;
1711
+ // Remove code blocks
1712
+ text = text.replace(/```[\s\S]*?```/g, "");
1713
+ text = text.replace(/`([^`]+)`/g, "$1");
1714
+ // Remove images
1715
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
1716
+ // Convert links to text only (keep link text, remove URL)
1717
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
1718
+ // Remove headers (convert to plain text)
1719
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
1720
+ // Remove bold and italic markers
1721
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
1722
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
1723
+ text = text.replace(/\*(.+?)\*/g, "$1");
1724
+ text = text.replace(/___(.+?)___/g, "$1");
1725
+ text = text.replace(/__(.+?)__/g, "$1");
1726
+ text = text.replace(/_(.+?)_/g, "$1");
1727
+ // Remove strikethrough
1728
+ text = text.replace(/~~(.+?)~~/g, "$1");
1729
+ // Convert lists to plain text with bullets
1730
+ text = text.replace(/^\s*[-*+]\s+/gm, "• ");
1731
+ text = text.replace(/^\s*\d+\.\s+/gm, "• ");
1732
+ // Remove blockquotes
1733
+ text = text.replace(/^\s*>\s+/gm, "");
1734
+ // Remove horizontal rules
1735
+ text = text.replace(/^(\*{3,}|-{3,}|_{3,})$/gm, "");
1736
+ // Remove HTML tags
1737
+ text = text.replace(/<[^>]+>/g, "");
1738
+ // Remove excessive whitespace and normalize line breaks
1739
+ text = text.replace(/\n[\s\n]*\n/g, "\n");
1740
+ text = text.replace(/[ \t]+/g, " ");
1741
+ // Remove all newline characters
1742
+ text = text.replace(/\n/g, " ");
1743
+ // Remove excessive spaces after newline removal
1744
+ text = text.replace(/\s+/g, " ");
1745
+ return text.trim();
1746
+ };
1747
+
1701
1748
  const INTERVAL_MINUTES$1 = {
1702
1749
  "1m": 1,
1703
1750
  "3m": 3,
@@ -1910,7 +1957,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1910
1957
  id: randomString(),
1911
1958
  priceOpen: signal.priceOpen, // Используем priceOpen из сигнала
1912
1959
  position: signal.position,
1913
- note: signal.note,
1960
+ note: toPlainString(signal.note),
1914
1961
  priceTakeProfit: signal.priceTakeProfit,
1915
1962
  priceStopLoss: signal.priceStopLoss,
1916
1963
  minuteEstimatedTime: signal.minuteEstimatedTime,
@@ -1930,7 +1977,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1930
1977
  id: randomString(),
1931
1978
  priceOpen: signal.priceOpen,
1932
1979
  position: signal.position,
1933
- note: signal.note,
1980
+ note: toPlainString(signal.note),
1934
1981
  priceTakeProfit: signal.priceTakeProfit,
1935
1982
  priceStopLoss: signal.priceStopLoss,
1936
1983
  minuteEstimatedTime: signal.minuteEstimatedTime,
@@ -1949,6 +1996,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1949
1996
  id: randomString(),
1950
1997
  priceOpen: currentPrice,
1951
1998
  ...signal,
1999
+ note: toPlainString(signal.note),
1952
2000
  symbol: self.params.execution.context.symbol,
1953
2001
  exchangeName: self.params.method.context.exchangeName,
1954
2002
  strategyName: self.params.method.context.strategyName,
@@ -2277,7 +2325,7 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2277
2325
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2278
2326
  }
2279
2327
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2280
- await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice);
2328
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, currentPrice, self.params.execution.context.backtest);
2281
2329
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2282
2330
  strategyName: self.params.method.context.strategyName,
2283
2331
  riskName: self.params.riskName,
@@ -2300,31 +2348,50 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2300
2348
  return result;
2301
2349
  };
2302
2350
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2303
- // Calculate revenue percentage for partial fill/loss callbacks
2351
+ // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2304
2352
  {
2305
- let revenuePercent = 0;
2306
2353
  if (signal.position === "long") {
2307
- // For long: positive if current > open, negative if current < open
2308
- revenuePercent = ((currentPrice - signal.priceOpen) / signal.priceOpen) * 100;
2354
+ // For long: calculate progress towards TP or SL
2355
+ const currentDistance = currentPrice - signal.priceOpen;
2356
+ if (currentDistance > 0) {
2357
+ // Moving towards TP
2358
+ const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2359
+ 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);
2361
+ 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);
2363
+ }
2364
+ }
2365
+ else if (currentDistance < 0) {
2366
+ // Moving towards SL
2367
+ const slDistance = signal.priceOpen - signal.priceStopLoss;
2368
+ 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);
2370
+ 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);
2372
+ }
2373
+ }
2309
2374
  }
2310
2375
  else if (signal.position === "short") {
2311
- // For short: positive if current < open, negative if current > open
2312
- revenuePercent = ((signal.priceOpen - currentPrice) / signal.priceOpen) * 100;
2313
- }
2314
- // Call onPartialProfit if revenue is positive (but not reached TP yet)
2315
- if (revenuePercent > 0) {
2316
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2317
- await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2318
- if (self.params.callbacks?.onPartialProfit) {
2319
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2376
+ // For short: calculate progress towards TP or SL
2377
+ const currentDistance = signal.priceOpen - currentPrice;
2378
+ if (currentDistance > 0) {
2379
+ // Moving towards TP
2380
+ const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2381
+ 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);
2383
+ 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);
2385
+ }
2320
2386
  }
2321
- }
2322
- // Call onPartialLoss if revenue is negative (but not hit SL yet)
2323
- if (revenuePercent < 0) {
2324
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2325
- await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2326
- if (self.params.callbacks?.onPartialLoss) {
2327
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, revenuePercent, self.params.execution.context.backtest);
2387
+ if (currentDistance < 0) {
2388
+ // Moving towards SL
2389
+ const slDistance = signal.priceStopLoss - signal.priceOpen;
2390
+ 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);
2392
+ 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);
2394
+ }
2328
2395
  }
2329
2396
  }
2330
2397
  }
@@ -2456,7 +2523,7 @@ const CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN = async (self, signal, averagePrice, c
2456
2523
  self.params.callbacks.onClose(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2457
2524
  }
2458
2525
  // КРИТИЧНО: Очищаем состояние ClientPartial при закрытии позиции
2459
- await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice);
2526
+ await self.params.partial.clear(self.params.execution.context.symbol, signal, averagePrice, self.params.execution.context.backtest);
2460
2527
  await self.params.risk.removeSignal(self.params.execution.context.symbol, {
2461
2528
  strategyName: self.params.method.context.strategyName,
2462
2529
  riskName: self.params.riskName,
@@ -2599,29 +2666,50 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
2599
2666
  return await CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN(self, signal, closePrice, closeReason, currentCandleTimestamp);
2600
2667
  }
2601
2668
  // Call onPartialProfit/onPartialLoss callbacks during backtest candle processing
2602
- // Calculate revenue percentage
2669
+ // Calculate percentage of path to TP/SL
2603
2670
  {
2604
- let revenuePercent = 0;
2605
2671
  if (signal.position === "long") {
2606
- revenuePercent = ((averagePrice - signal.priceOpen) / signal.priceOpen) * 100;
2672
+ // For long: calculate progress towards TP or SL
2673
+ const currentDistance = averagePrice - signal.priceOpen;
2674
+ if (currentDistance > 0) {
2675
+ // Moving towards TP
2676
+ const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2677
+ const progressPercent = (currentDistance / tpDistance) * 100;
2678
+ 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);
2679
+ if (self.params.callbacks?.onPartialProfit) {
2680
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2681
+ }
2682
+ }
2683
+ else if (currentDistance < 0) {
2684
+ // Moving towards SL
2685
+ const slDistance = signal.priceOpen - signal.priceStopLoss;
2686
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2687
+ 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);
2688
+ if (self.params.callbacks?.onPartialLoss) {
2689
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2690
+ }
2691
+ }
2607
2692
  }
2608
2693
  else if (signal.position === "short") {
2609
- revenuePercent = ((signal.priceOpen - averagePrice) / signal.priceOpen) * 100;
2610
- }
2611
- // Call onPartialProfit if revenue is positive (but not reached TP yet)
2612
- if (revenuePercent > 0) {
2613
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2614
- await self.params.partial.profit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2615
- if (self.params.callbacks?.onPartialProfit) {
2616
- self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2694
+ // For short: calculate progress towards TP or SL
2695
+ const currentDistance = signal.priceOpen - averagePrice;
2696
+ if (currentDistance > 0) {
2697
+ // Moving towards TP
2698
+ const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2699
+ const progressPercent = (currentDistance / tpDistance) * 100;
2700
+ 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);
2701
+ if (self.params.callbacks?.onPartialProfit) {
2702
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2703
+ }
2617
2704
  }
2618
- }
2619
- // Call onPartialLoss if revenue is negative (but not hit SL yet)
2620
- if (revenuePercent < 0) {
2621
- // КРИТИЧНО: Вызываем ClientPartial для отслеживания уровней
2622
- await self.params.partial.loss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest, self.params.execution.context.when);
2623
- if (self.params.callbacks?.onPartialLoss) {
2624
- self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, revenuePercent, self.params.execution.context.backtest);
2705
+ if (currentDistance < 0) {
2706
+ // Moving towards SL
2707
+ const slDistance = signal.priceStopLoss - signal.priceOpen;
2708
+ const progressPercent = (Math.abs(currentDistance) / slDistance) * 100;
2709
+ 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);
2710
+ if (self.params.callbacks?.onPartialLoss) {
2711
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), self.params.execution.context.backtest);
2712
+ }
2625
2713
  }
2626
2714
  }
2627
2715
  }
@@ -5611,7 +5699,7 @@ const columns$4 = [
5611
5699
  {
5612
5700
  key: "note",
5613
5701
  label: "Note",
5614
- format: (data) => data.signal.note ?? "N/A",
5702
+ format: (data) => toPlainString(data.signal.note ?? "N/A"),
5615
5703
  },
5616
5704
  {
5617
5705
  key: "openPrice",
@@ -6023,7 +6111,7 @@ const columns$3 = [
6023
6111
  {
6024
6112
  key: "note",
6025
6113
  label: "Note",
6026
- format: (data) => data.note ?? "N/A",
6114
+ format: (data) => toPlainString(data.note ?? "N/A"),
6027
6115
  },
6028
6116
  {
6029
6117
  key: "currentPrice",
@@ -6550,7 +6638,7 @@ const columns$2 = [
6550
6638
  {
6551
6639
  key: "note",
6552
6640
  label: "Note",
6553
- format: (data) => data.note ?? "N/A",
6641
+ format: (data) => toPlainString(data.note ?? "N/A"),
6554
6642
  },
6555
6643
  {
6556
6644
  key: "currentPrice",
@@ -8762,6 +8850,8 @@ class OptimizerTemplateService {
8762
8850
  interval,
8763
8851
  prompt,
8764
8852
  });
8853
+ // Convert prompt to plain text first
8854
+ const plainPrompt = toPlainString(prompt);
8765
8855
  // Escape special characters to prevent code injection
8766
8856
  const escapedStrategyName = String(strategyName)
8767
8857
  .replace(/\\/g, '\\\\')
@@ -8769,7 +8859,7 @@ class OptimizerTemplateService {
8769
8859
  const escapedInterval = String(interval)
8770
8860
  .replace(/\\/g, '\\\\')
8771
8861
  .replace(/"/g, '\\"');
8772
- const escapedPrompt = String(prompt)
8862
+ const escapedPrompt = String(plainPrompt)
8773
8863
  .replace(/\\/g, '\\\\')
8774
8864
  .replace(/`/g, '\\`')
8775
8865
  .replace(/\$/g, '\\$');
@@ -9950,7 +10040,7 @@ const HANDLE_PROFIT_FN = async (symbol, data, currentPrice, revenuePercent, back
9950
10040
  }
9951
10041
  }
9952
10042
  if (shouldPersist) {
9953
- await self._persistState(symbol);
10043
+ await self._persistState(symbol, backtest);
9954
10044
  }
9955
10045
  };
9956
10046
  /**
@@ -9997,7 +10087,7 @@ const HANDLE_LOSS_FN = async (symbol, data, currentPrice, lossPercent, backtest,
9997
10087
  }
9998
10088
  }
9999
10089
  if (shouldPersist) {
10000
- await self._persistState(symbol);
10090
+ await self._persistState(symbol, backtest);
10001
10091
  }
10002
10092
  };
10003
10093
  /**
@@ -10133,7 +10223,10 @@ class ClientPartial {
10133
10223
  * @param symbol - Trading pair symbol
10134
10224
  * @returns Promise that resolves when persistence is complete
10135
10225
  */
10136
- async _persistState(symbol) {
10226
+ async _persistState(symbol, backtest) {
10227
+ if (backtest) {
10228
+ return;
10229
+ }
10137
10230
  this.params.logger.debug("ClientPartial persistState", { symbol });
10138
10231
  if (this._states === NEED_FETCH) {
10139
10232
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
@@ -10256,7 +10349,7 @@ class ClientPartial {
10256
10349
  * // Cleanup: PartialConnectionService.getPartial.clear(signal.id)
10257
10350
  * ```
10258
10351
  */
10259
- async clear(symbol, data, priceClose) {
10352
+ async clear(symbol, data, priceClose, backtest) {
10260
10353
  this.params.logger.log("ClientPartial clear", {
10261
10354
  symbol,
10262
10355
  data,
@@ -10266,7 +10359,7 @@ class ClientPartial {
10266
10359
  throw new Error("ClientPartial not initialized. Call waitForInit() before using.");
10267
10360
  }
10268
10361
  this._states.delete(data.id);
10269
- await this._persistState(symbol);
10362
+ await this._persistState(symbol, backtest);
10270
10363
  }
10271
10364
  }
10272
10365
 
@@ -10437,7 +10530,7 @@ class PartialConnectionService {
10437
10530
  * @param priceClose - Final closing price
10438
10531
  * @returns Promise that resolves when clear is complete
10439
10532
  */
10440
- this.clear = async (symbol, data, priceClose) => {
10533
+ this.clear = async (symbol, data, priceClose, backtest) => {
10441
10534
  this.loggerService.log("partialConnectionService profit", {
10442
10535
  symbol,
10443
10536
  data,
@@ -10445,7 +10538,7 @@ class PartialConnectionService {
10445
10538
  });
10446
10539
  const partial = this.getPartial(data.id);
10447
10540
  await partial.waitForInit(symbol);
10448
- await partial.clear(symbol, data, priceClose);
10541
+ await partial.clear(symbol, data, priceClose, backtest);
10449
10542
  this.getPartial.clear(data.id);
10450
10543
  };
10451
10544
  }
@@ -10914,13 +11007,14 @@ class PartialGlobalService {
10914
11007
  * @param priceClose - Final closing price
10915
11008
  * @returns Promise that resolves when clear is complete
10916
11009
  */
10917
- this.clear = async (symbol, data, priceClose) => {
11010
+ this.clear = async (symbol, data, priceClose, backtest) => {
10918
11011
  this.loggerService.log("partialGlobalService profit", {
10919
11012
  symbol,
10920
11013
  data,
10921
11014
  priceClose,
11015
+ backtest,
10922
11016
  });
10923
- return await this.partialConnectionService.clear(symbol, data, priceClose);
11017
+ return await this.partialConnectionService.clear(symbol, data, priceClose, backtest);
10924
11018
  };
10925
11019
  }
10926
11020
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.4.15",
3
+ "version": "1.5.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -723,7 +723,7 @@ interface IPartial {
723
723
  * // Memoized instance cleared from getPartial cache
724
724
  * ```
725
725
  */
726
- clear(symbol: string, data: ISignalRow, priceClose: number): Promise<void>;
726
+ clear(symbol: string, data: ISignalRow, priceClose: number, backtest: boolean): Promise<void>;
727
727
  }
728
728
 
729
729
  /**
@@ -8661,7 +8661,7 @@ declare class PartialConnectionService implements IPartial {
8661
8661
  * @param priceClose - Final closing price
8662
8662
  * @returns Promise that resolves when clear is complete
8663
8663
  */
8664
- clear: (symbol: string, data: ISignalRow, priceClose: number) => Promise<void>;
8664
+ clear: (symbol: string, data: ISignalRow, priceClose: number, backtest: boolean) => Promise<void>;
8665
8665
  }
8666
8666
 
8667
8667
  /**
@@ -8742,7 +8742,7 @@ declare class PartialGlobalService {
8742
8742
  * @param priceClose - Final closing price
8743
8743
  * @returns Promise that resolves when clear is complete
8744
8744
  */
8745
- clear: (symbol: string, data: ISignalRow, priceClose: number) => Promise<void>;
8745
+ clear: (symbol: string, data: ISignalRow, priceClose: number, backtest: boolean) => Promise<void>;
8746
8746
  }
8747
8747
 
8748
8748
  /**