backtest-kit 1.5.0 → 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
@@ -1755,6 +1764,7 @@ const INTERVAL_MINUTES$1 = {
1755
1764
  "30m": 30,
1756
1765
  "1h": 60,
1757
1766
  };
1767
+ const TIMEOUT_SYMBOL = Symbol('timeout');
1758
1768
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1759
1769
  const errors = [];
1760
1770
  // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
@@ -1938,7 +1948,14 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1938
1948
  }))) {
1939
1949
  return null;
1940
1950
  }
1941
- 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
+ }
1942
1959
  if (!signal) {
1943
1960
  return null;
1944
1961
  }
@@ -2224,6 +2241,8 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
2224
2241
  strategyName: self.params.method.context.strategyName,
2225
2242
  exchangeName: self.params.method.context.exchangeName,
2226
2243
  symbol: self.params.execution.context.symbol,
2244
+ percentTp: 0,
2245
+ percentSl: 0,
2227
2246
  };
2228
2247
  if (self.params.callbacks?.onTick) {
2229
2248
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -2350,6 +2369,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2350
2369
  return result;
2351
2370
  };
2352
2371
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2372
+ let percentTp = 0;
2373
+ let percentSl = 0;
2353
2374
  // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2354
2375
  {
2355
2376
  if (signal.position === "long") {
@@ -2359,18 +2380,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2359
2380
  // Moving towards TP
2360
2381
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2361
2382
  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);
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);
2363
2385
  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);
2386
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2365
2387
  }
2366
2388
  }
2367
2389
  else if (currentDistance < 0) {
2368
2390
  // Moving towards SL
2369
2391
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2370
2392
  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);
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);
2372
2395
  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);
2396
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2374
2397
  }
2375
2398
  }
2376
2399
  }
@@ -2381,18 +2404,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2381
2404
  // Moving towards TP
2382
2405
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2383
2406
  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);
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);
2385
2409
  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);
2410
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2387
2411
  }
2388
2412
  }
2389
2413
  if (currentDistance < 0) {
2390
2414
  // Moving towards SL
2391
2415
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2392
2416
  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);
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);
2394
2419
  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);
2420
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2396
2421
  }
2397
2422
  }
2398
2423
  }
@@ -2404,6 +2429,8 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2404
2429
  strategyName: self.params.method.context.strategyName,
2405
2430
  exchangeName: self.params.method.context.exchangeName,
2406
2431
  symbol: self.params.execution.context.symbol,
2432
+ percentTp,
2433
+ percentSl,
2407
2434
  };
2408
2435
  if (self.params.callbacks?.onTick) {
2409
2436
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -3000,6 +3027,8 @@ class ClientStrategy {
3000
3027
  action: "active",
3001
3028
  signal: scheduled,
3002
3029
  currentPrice: lastPrice,
3030
+ percentSl: 0,
3031
+ percentTp: 0,
3003
3032
  strategyName: this.params.method.context.strategyName,
3004
3033
  exchangeName: this.params.method.context.exchangeName,
3005
3034
  symbol: this.params.execution.context.symbol,
@@ -5848,14 +5877,33 @@ let ReportStorage$4 = class ReportStorage {
5848
5877
  async getReport(strategyName) {
5849
5878
  const stats = await this.getData();
5850
5879
  if (stats.totalSignals === 0) {
5851
- 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");
5852
5885
  }
5853
5886
  const header = columns$4.map((col) => col.label);
5854
5887
  const separator = columns$4.map(() => "---");
5855
5888
  const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
5856
5889
  const tableData = [header, separator, ...rows];
5857
- const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
5858
- 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");
5859
5907
  }
5860
5908
  /**
5861
5909
  * Saves strategy report to disk.
@@ -6137,6 +6185,16 @@ const columns$3 = [
6137
6185
  label: "Stop Loss",
6138
6186
  format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
6139
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
+ },
6140
6198
  {
6141
6199
  key: "pnl",
6142
6200
  label: "PNL (net)",
@@ -6239,6 +6297,8 @@ let ReportStorage$3 = class ReportStorage {
6239
6297
  openPrice: data.signal.priceOpen,
6240
6298
  takeProfit: data.signal.priceTakeProfit,
6241
6299
  stopLoss: data.signal.priceStopLoss,
6300
+ percentTp: data.percentTp,
6301
+ percentSl: data.percentSl,
6242
6302
  };
6243
6303
  // Replace existing event or add new one
6244
6304
  if (existingIndex !== -1) {
@@ -6380,14 +6440,33 @@ let ReportStorage$3 = class ReportStorage {
6380
6440
  async getReport(strategyName) {
6381
6441
  const stats = await this.getData();
6382
6442
  if (stats.totalEvents === 0) {
6383
- 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");
6384
6448
  }
6385
6449
  const header = columns$3.map((col) => col.label);
6386
6450
  const separator = columns$3.map(() => "---");
6387
6451
  const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
6388
6452
  const tableData = [header, separator, ...rows];
6389
- const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
6390
- 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");
6391
6470
  }
6392
6471
  /**
6393
6472
  * Saves strategy report to disk.
@@ -6784,14 +6863,28 @@ let ReportStorage$2 = class ReportStorage {
6784
6863
  async getReport(strategyName) {
6785
6864
  const stats = await this.getData();
6786
6865
  if (stats.totalEvents === 0) {
6787
- 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");
6788
6871
  }
6789
6872
  const header = columns$2.map((col) => col.label);
6790
6873
  const separator = columns$2.map(() => "---");
6791
6874
  const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6792
6875
  const tableData = [header, separator, ...rows];
6793
- const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
6794
- 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");
6795
6888
  }
6796
6889
  /**
6797
6890
  * Saves strategy report to disk.
@@ -7109,7 +7202,11 @@ class PerformanceStorage {
7109
7202
  async getReport(strategyName) {
7110
7203
  const stats = await this.getData(strategyName);
7111
7204
  if (stats.totalEvents === 0) {
7112
- 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");
7113
7210
  }
7114
7211
  // Sort metrics by total duration (descending) to show bottlenecks first
7115
7212
  const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
@@ -7146,13 +7243,29 @@ class PerformanceStorage {
7146
7243
  metric.maxWaitTime.toFixed(2),
7147
7244
  ]);
7148
7245
  const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
7149
- const summaryTable = functoolsKit.str.newline(summaryTableData.map((row) => `| ${row.join(" | ")} |`));
7246
+ const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7150
7247
  // Calculate percentage of total time for each metric
7151
7248
  const percentages = sortedMetrics.map((metric) => {
7152
7249
  const pct = (metric.totalDuration / stats.totalDuration) * 100;
7153
7250
  return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
7154
7251
  });
7155
- 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");
7156
7269
  }
7157
7270
  /**
7158
7271
  * Saves performance report to disk.
@@ -7556,7 +7669,7 @@ let ReportStorage$1 = class ReportStorage {
7556
7669
  // Build table rows
7557
7670
  const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7558
7671
  const tableData = [header, separator, ...rows];
7559
- return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7672
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7560
7673
  }
7561
7674
  /**
7562
7675
  * Generates PNL table showing all closed signals across all strategies (View).
@@ -7593,7 +7706,7 @@ let ReportStorage$1 = class ReportStorage {
7593
7706
  // Build table rows
7594
7707
  const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7595
7708
  const tableData = [header, separator, ...rows];
7596
- return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7709
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7597
7710
  }
7598
7711
  /**
7599
7712
  * Generates markdown report with all strategy results (View).
@@ -7608,7 +7721,30 @@ let ReportStorage$1 = class ReportStorage {
7608
7721
  const results = await this.getData(symbol, metric, context);
7609
7722
  // Get total signals for best strategy
7610
7723
  const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
7611
- 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");
7612
7748
  }
7613
7749
  /**
7614
7750
  * Saves walker report to disk.
@@ -8122,14 +8258,24 @@ class HeatmapStorage {
8122
8258
  async getReport(strategyName) {
8123
8259
  const data = await this.getData();
8124
8260
  if (data.symbols.length === 0) {
8125
- return functoolsKit.str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
8261
+ return [
8262
+ `# Portfolio Heatmap: ${strategyName}`,
8263
+ "",
8264
+ "*No data available*"
8265
+ ].join("\n");
8126
8266
  }
8127
8267
  const header = columns$1.map((col) => col.label);
8128
8268
  const separator = columns$1.map(() => "---");
8129
8269
  const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
8130
8270
  const tableData = [header, separator, ...rows];
8131
- const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
8132
- 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");
8133
8279
  }
8134
8280
  /**
8135
8281
  * Saves heatmap report to disk.
@@ -10687,14 +10833,26 @@ class ReportStorage {
10687
10833
  async getReport(symbol, strategyName) {
10688
10834
  const stats = await this.getData();
10689
10835
  if (stats.totalEvents === 0) {
10690
- 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");
10691
10841
  }
10692
10842
  const header = columns.map((col) => col.label);
10693
10843
  const separator = columns.map(() => "---");
10694
10844
  const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
10695
10845
  const tableData = [header, separator, ...rows];
10696
- const table = functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
10697
- 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");
10698
10856
  }
10699
10857
  /**
10700
10858
  * Saves symbol-strategy report to disk.
package/build/index.mjs CHANGED
@@ -36,6 +36,15 @@ const GLOBAL_CONFIG = {
36
36
  * Default: 1440 minutes (1 day)
37
37
  */
38
38
  CC_MAX_SIGNAL_LIFETIME_MINUTES: 1440,
39
+ /**
40
+ * Maximum time allowed for signal generation (in seconds).
41
+ * Prevents long-running or stuck signal generation routines from blocking
42
+ * execution or consuming resources indefinitely. If generation exceeds this
43
+ * threshold the attempt should be aborted, logged and optionally retried.
44
+ *
45
+ * Default: 180 seconds (3 minutes)
46
+ */
47
+ CC_MAX_SIGNAL_GENERATION_SECONDS: 180,
39
48
  /**
40
49
  * Number of retries for getCandles function
41
50
  * Default: 3 retries
@@ -1753,6 +1762,7 @@ const INTERVAL_MINUTES$1 = {
1753
1762
  "30m": 30,
1754
1763
  "1h": 60,
1755
1764
  };
1765
+ const TIMEOUT_SYMBOL = Symbol('timeout');
1756
1766
  const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
1757
1767
  const errors = [];
1758
1768
  // ПРОВЕРКА ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ISignalRow
@@ -1936,7 +1946,14 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
1936
1946
  }))) {
1937
1947
  return null;
1938
1948
  }
1939
- const signal = await self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when);
1949
+ const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
1950
+ const signal = await Promise.race([
1951
+ self.params.getSignal(self.params.execution.context.symbol, self.params.execution.context.when),
1952
+ sleep(timeoutMs).then(() => TIMEOUT_SYMBOL),
1953
+ ]);
1954
+ if (typeof signal === "symbol") {
1955
+ throw new Error(`Timeout for ${self.params.method.context.strategyName} symbol=${self.params.execution.context.symbol}`);
1956
+ }
1940
1957
  if (!signal) {
1941
1958
  return null;
1942
1959
  }
@@ -2222,6 +2239,8 @@ const RETURN_SCHEDULED_SIGNAL_ACTIVE_FN = async (self, scheduled, currentPrice)
2222
2239
  strategyName: self.params.method.context.strategyName,
2223
2240
  exchangeName: self.params.method.context.exchangeName,
2224
2241
  symbol: self.params.execution.context.symbol,
2242
+ percentTp: 0,
2243
+ percentSl: 0,
2225
2244
  };
2226
2245
  if (self.params.callbacks?.onTick) {
2227
2246
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -2348,6 +2367,8 @@ const CLOSE_PENDING_SIGNAL_FN = async (self, signal, currentPrice, closeReason)
2348
2367
  return result;
2349
2368
  };
2350
2369
  const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2370
+ let percentTp = 0;
2371
+ let percentSl = 0;
2351
2372
  // Calculate percentage of path to TP/SL for partial fill/loss callbacks
2352
2373
  {
2353
2374
  if (signal.position === "long") {
@@ -2357,18 +2378,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2357
2378
  // Moving towards TP
2358
2379
  const tpDistance = signal.priceTakeProfit - signal.priceOpen;
2359
2380
  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);
2381
+ percentTp = Math.min(progressPercent, 100);
2382
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2361
2383
  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);
2384
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2363
2385
  }
2364
2386
  }
2365
2387
  else if (currentDistance < 0) {
2366
2388
  // Moving towards SL
2367
2389
  const slDistance = signal.priceOpen - signal.priceStopLoss;
2368
2390
  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);
2391
+ percentSl = Math.min(progressPercent, 100);
2392
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2370
2393
  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);
2394
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2372
2395
  }
2373
2396
  }
2374
2397
  }
@@ -2379,18 +2402,20 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2379
2402
  // Moving towards TP
2380
2403
  const tpDistance = signal.priceOpen - signal.priceTakeProfit;
2381
2404
  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);
2405
+ percentTp = Math.min(progressPercent, 100);
2406
+ await self.params.partial.profit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest, self.params.execution.context.when);
2383
2407
  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);
2408
+ self.params.callbacks.onPartialProfit(self.params.execution.context.symbol, signal, currentPrice, percentTp, self.params.execution.context.backtest);
2385
2409
  }
2386
2410
  }
2387
2411
  if (currentDistance < 0) {
2388
2412
  // Moving towards SL
2389
2413
  const slDistance = signal.priceStopLoss - signal.priceOpen;
2390
2414
  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);
2415
+ percentSl = Math.min(progressPercent, 100);
2416
+ await self.params.partial.loss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest, self.params.execution.context.when);
2392
2417
  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);
2418
+ self.params.callbacks.onPartialLoss(self.params.execution.context.symbol, signal, currentPrice, percentSl, self.params.execution.context.backtest);
2394
2419
  }
2395
2420
  }
2396
2421
  }
@@ -2402,6 +2427,8 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
2402
2427
  strategyName: self.params.method.context.strategyName,
2403
2428
  exchangeName: self.params.method.context.exchangeName,
2404
2429
  symbol: self.params.execution.context.symbol,
2430
+ percentTp,
2431
+ percentSl,
2405
2432
  };
2406
2433
  if (self.params.callbacks?.onTick) {
2407
2434
  self.params.callbacks.onTick(self.params.execution.context.symbol, result, self.params.execution.context.backtest);
@@ -2998,6 +3025,8 @@ class ClientStrategy {
2998
3025
  action: "active",
2999
3026
  signal: scheduled,
3000
3027
  currentPrice: lastPrice,
3028
+ percentSl: 0,
3029
+ percentTp: 0,
3001
3030
  strategyName: this.params.method.context.strategyName,
3002
3031
  exchangeName: this.params.method.context.exchangeName,
3003
3032
  symbol: this.params.execution.context.symbol,
@@ -5846,14 +5875,33 @@ let ReportStorage$4 = class ReportStorage {
5846
5875
  async getReport(strategyName) {
5847
5876
  const stats = await this.getData();
5848
5877
  if (stats.totalSignals === 0) {
5849
- return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
5878
+ return [
5879
+ `# Backtest Report: ${strategyName}`,
5880
+ "",
5881
+ "No signals closed yet."
5882
+ ].join("\n");
5850
5883
  }
5851
5884
  const header = columns$4.map((col) => col.label);
5852
5885
  const separator = columns$4.map(() => "---");
5853
5886
  const rows = this._signalList.map((closedSignal) => columns$4.map((col) => col.format(closedSignal)));
5854
5887
  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)`}`);
5888
+ const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
5889
+ return [
5890
+ `# Backtest Report: ${strategyName}`,
5891
+ "",
5892
+ table,
5893
+ "",
5894
+ `**Total signals:** ${stats.totalSignals}`,
5895
+ `**Closed signals:** ${stats.totalSignals}`,
5896
+ `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
5897
+ `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
5898
+ `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
5899
+ `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
5900
+ `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
5901
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
5902
+ `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
5903
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
5904
+ ].join("\n");
5857
5905
  }
5858
5906
  /**
5859
5907
  * Saves strategy report to disk.
@@ -6135,6 +6183,16 @@ const columns$3 = [
6135
6183
  label: "Stop Loss",
6136
6184
  format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
6137
6185
  },
6186
+ {
6187
+ key: "percentTp",
6188
+ label: "% to TP",
6189
+ format: (data) => data.percentTp !== undefined ? `${data.percentTp.toFixed(2)}%` : "N/A",
6190
+ },
6191
+ {
6192
+ key: "percentSl",
6193
+ label: "% to SL",
6194
+ format: (data) => data.percentSl !== undefined ? `${data.percentSl.toFixed(2)}%` : "N/A",
6195
+ },
6138
6196
  {
6139
6197
  key: "pnl",
6140
6198
  label: "PNL (net)",
@@ -6237,6 +6295,8 @@ let ReportStorage$3 = class ReportStorage {
6237
6295
  openPrice: data.signal.priceOpen,
6238
6296
  takeProfit: data.signal.priceTakeProfit,
6239
6297
  stopLoss: data.signal.priceStopLoss,
6298
+ percentTp: data.percentTp,
6299
+ percentSl: data.percentSl,
6240
6300
  };
6241
6301
  // Replace existing event or add new one
6242
6302
  if (existingIndex !== -1) {
@@ -6378,14 +6438,33 @@ let ReportStorage$3 = class ReportStorage {
6378
6438
  async getReport(strategyName) {
6379
6439
  const stats = await this.getData();
6380
6440
  if (stats.totalEvents === 0) {
6381
- return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
6441
+ return [
6442
+ `# Live Trading Report: ${strategyName}`,
6443
+ "",
6444
+ "No events recorded yet."
6445
+ ].join("\n");
6382
6446
  }
6383
6447
  const header = columns$3.map((col) => col.label);
6384
6448
  const separator = columns$3.map(() => "---");
6385
6449
  const rows = this._eventList.map((event) => columns$3.map((col) => col.format(event)));
6386
6450
  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)`}`);
6451
+ const table = tableData.map(row => `| ${row.join(" | ")} |`).join("\n");
6452
+ return [
6453
+ `# Live Trading Report: ${strategyName}`,
6454
+ "",
6455
+ table,
6456
+ "",
6457
+ `**Total events:** ${stats.totalEvents}`,
6458
+ `**Closed signals:** ${stats.totalClosed}`,
6459
+ `**Win rate:** ${stats.winRate === null ? "N/A" : `${stats.winRate.toFixed(2)}% (${stats.winCount}W / ${stats.lossCount}L) (higher is better)`}`,
6460
+ `**Average PNL:** ${stats.avgPnl === null ? "N/A" : `${stats.avgPnl > 0 ? "+" : ""}${stats.avgPnl.toFixed(2)}% (higher is better)`}`,
6461
+ `**Total PNL:** ${stats.totalPnl === null ? "N/A" : `${stats.totalPnl > 0 ? "+" : ""}${stats.totalPnl.toFixed(2)}% (higher is better)`}`,
6462
+ `**Standard Deviation:** ${stats.stdDev === null ? "N/A" : `${stats.stdDev.toFixed(3)}% (lower is better)`}`,
6463
+ `**Sharpe Ratio:** ${stats.sharpeRatio === null ? "N/A" : `${stats.sharpeRatio.toFixed(3)} (higher is better)`}`,
6464
+ `**Annualized Sharpe Ratio:** ${stats.annualizedSharpeRatio === null ? "N/A" : `${stats.annualizedSharpeRatio.toFixed(3)} (higher is better)`}`,
6465
+ `**Certainty Ratio:** ${stats.certaintyRatio === null ? "N/A" : `${stats.certaintyRatio.toFixed(3)} (higher is better)`}`,
6466
+ `**Expected Yearly Returns:** ${stats.expectedYearlyReturns === null ? "N/A" : `${stats.expectedYearlyReturns > 0 ? "+" : ""}${stats.expectedYearlyReturns.toFixed(2)}% (higher is better)`}`,
6467
+ ].join("\n");
6389
6468
  }
6390
6469
  /**
6391
6470
  * Saves strategy report to disk.
@@ -6782,14 +6861,28 @@ let ReportStorage$2 = class ReportStorage {
6782
6861
  async getReport(strategyName) {
6783
6862
  const stats = await this.getData();
6784
6863
  if (stats.totalEvents === 0) {
6785
- return str.newline(`# Scheduled Signals Report: ${strategyName}`, "", "No scheduled signals recorded yet.");
6864
+ return [
6865
+ `# Scheduled Signals Report: ${strategyName}`,
6866
+ "",
6867
+ "No scheduled signals recorded yet."
6868
+ ].join("\n");
6786
6869
  }
6787
6870
  const header = columns$2.map((col) => col.label);
6788
6871
  const separator = columns$2.map(() => "---");
6789
6872
  const rows = this._eventList.map((event) => columns$2.map((col) => col.format(event)));
6790
6873
  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`}`);
6874
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
6875
+ return [
6876
+ `# Scheduled Signals Report: ${strategyName}`,
6877
+ "",
6878
+ table,
6879
+ "",
6880
+ `**Total events:** ${stats.totalEvents}`,
6881
+ `**Scheduled signals:** ${stats.totalScheduled}`,
6882
+ `**Cancelled signals:** ${stats.totalCancelled}`,
6883
+ `**Cancellation rate:** ${stats.cancellationRate === null ? "N/A" : `${stats.cancellationRate.toFixed(2)}% (lower is better)`}`,
6884
+ `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`
6885
+ ].join("\n");
6793
6886
  }
6794
6887
  /**
6795
6888
  * Saves strategy report to disk.
@@ -7107,7 +7200,11 @@ class PerformanceStorage {
7107
7200
  async getReport(strategyName) {
7108
7201
  const stats = await this.getData(strategyName);
7109
7202
  if (stats.totalEvents === 0) {
7110
- return str.newline(`# Performance Report: ${strategyName}`, "", "No performance metrics recorded yet.");
7203
+ return [
7204
+ `# Performance Report: ${strategyName}`,
7205
+ "",
7206
+ "No performance metrics recorded yet."
7207
+ ].join("\n");
7111
7208
  }
7112
7209
  // Sort metrics by total duration (descending) to show bottlenecks first
7113
7210
  const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
@@ -7144,13 +7241,29 @@ class PerformanceStorage {
7144
7241
  metric.maxWaitTime.toFixed(2),
7145
7242
  ]);
7146
7243
  const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
7147
- const summaryTable = str.newline(summaryTableData.map((row) => `| ${row.join(" | ")} |`));
7244
+ const summaryTable = summaryTableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7148
7245
  // Calculate percentage of total time for each metric
7149
7246
  const percentages = sortedMetrics.map((metric) => {
7150
7247
  const pct = (metric.totalDuration / stats.totalDuration) * 100;
7151
7248
  return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
7152
7249
  });
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.");
7250
+ return [
7251
+ `# Performance Report: ${strategyName}`,
7252
+ "",
7253
+ `**Total events:** ${stats.totalEvents}`,
7254
+ `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
7255
+ `**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
7256
+ "",
7257
+ "## Time Distribution",
7258
+ "",
7259
+ percentages.join("\n"),
7260
+ "",
7261
+ "## Detailed Metrics",
7262
+ "",
7263
+ summaryTable,
7264
+ "",
7265
+ "**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."
7266
+ ].join("\n");
7154
7267
  }
7155
7268
  /**
7156
7269
  * Saves performance report to disk.
@@ -7554,7 +7667,7 @@ let ReportStorage$1 = class ReportStorage {
7554
7667
  // Build table rows
7555
7668
  const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7556
7669
  const tableData = [header, separator, ...rows];
7557
- return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7670
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7558
7671
  }
7559
7672
  /**
7560
7673
  * Generates PNL table showing all closed signals across all strategies (View).
@@ -7591,7 +7704,7 @@ let ReportStorage$1 = class ReportStorage {
7591
7704
  // Build table rows
7592
7705
  const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7593
7706
  const tableData = [header, separator, ...rows];
7594
- return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7707
+ return tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
7595
7708
  }
7596
7709
  /**
7597
7710
  * Generates markdown report with all strategy results (View).
@@ -7606,7 +7719,30 @@ let ReportStorage$1 = class ReportStorage {
7606
7719
  const results = await this.getData(symbol, metric, context);
7607
7720
  // Get total signals for best strategy
7608
7721
  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).");
7722
+ return [
7723
+ `# Walker Comparison Report: ${results.walkerName}`,
7724
+ "",
7725
+ `**Symbol:** ${results.symbol}`,
7726
+ `**Exchange:** ${results.exchangeName}`,
7727
+ `**Frame:** ${results.frameName}`,
7728
+ `**Optimization Metric:** ${results.metric}`,
7729
+ `**Strategies Tested:** ${results.totalStrategies}`,
7730
+ "",
7731
+ `## Best Strategy: ${results.bestStrategy}`,
7732
+ "",
7733
+ `**Best ${results.metric}:** ${formatMetric(results.bestMetric)}`,
7734
+ `**Total Signals:** ${bestStrategySignals}`,
7735
+ "",
7736
+ "## Top Strategies Comparison",
7737
+ "",
7738
+ this.getComparisonTable(metric, 10),
7739
+ "",
7740
+ "## All Signals (PNL Table)",
7741
+ "",
7742
+ this.getPnlTable(),
7743
+ "",
7744
+ "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
7745
+ ].join("\n");
7610
7746
  }
7611
7747
  /**
7612
7748
  * Saves walker report to disk.
@@ -8120,14 +8256,24 @@ class HeatmapStorage {
8120
8256
  async getReport(strategyName) {
8121
8257
  const data = await this.getData();
8122
8258
  if (data.symbols.length === 0) {
8123
- return str.newline(`# Portfolio Heatmap: ${strategyName}`, "", "*No data available*");
8259
+ return [
8260
+ `# Portfolio Heatmap: ${strategyName}`,
8261
+ "",
8262
+ "*No data available*"
8263
+ ].join("\n");
8124
8264
  }
8125
8265
  const header = columns$1.map((col) => col.label);
8126
8266
  const separator = columns$1.map(() => "---");
8127
8267
  const rows = data.symbols.map((row) => columns$1.map((col) => col.format(row)));
8128
8268
  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);
8269
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
8270
+ return [
8271
+ `# Portfolio Heatmap: ${strategyName}`,
8272
+ "",
8273
+ `**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}`,
8274
+ "",
8275
+ table
8276
+ ].join("\n");
8131
8277
  }
8132
8278
  /**
8133
8279
  * Saves heatmap report to disk.
@@ -10685,14 +10831,26 @@ class ReportStorage {
10685
10831
  async getReport(symbol, strategyName) {
10686
10832
  const stats = await this.getData();
10687
10833
  if (stats.totalEvents === 0) {
10688
- return str.newline(`# Partial Profit/Loss Report: ${symbol}:${strategyName}`, "", "No partial profit/loss events recorded yet.");
10834
+ return [
10835
+ `# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
10836
+ "",
10837
+ "No partial profit/loss events recorded yet."
10838
+ ].join("\n");
10689
10839
  }
10690
10840
  const header = columns.map((col) => col.label);
10691
10841
  const separator = columns.map(() => "---");
10692
10842
  const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
10693
10843
  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}`);
10844
+ const table = tableData.map((row) => `| ${row.join(" | ")} |`).join("\n");
10845
+ return [
10846
+ `# Partial Profit/Loss Report: ${symbol}:${strategyName}`,
10847
+ "",
10848
+ table,
10849
+ "",
10850
+ `**Total events:** ${stats.totalEvents}`,
10851
+ `**Profit events:** ${stats.totalProfit}`,
10852
+ `**Loss events:** ${stats.totalLoss}`
10853
+ ].join("\n");
10696
10854
  }
10697
10855
  /**
10698
10856
  * Saves symbol-strategy report to disk.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -31,6 +31,15 @@ declare const GLOBAL_CONFIG: {
31
31
  * Default: 1440 minutes (1 day)
32
32
  */
33
33
  CC_MAX_SIGNAL_LIFETIME_MINUTES: number;
34
+ /**
35
+ * Maximum time allowed for signal generation (in seconds).
36
+ * Prevents long-running or stuck signal generation routines from blocking
37
+ * execution or consuming resources indefinitely. If generation exceeds this
38
+ * threshold the attempt should be aborted, logged and optionally retried.
39
+ *
40
+ * Default: 180 seconds (3 minutes)
41
+ */
42
+ CC_MAX_SIGNAL_GENERATION_SECONDS: number;
34
43
  /**
35
44
  * Number of retries for getCandles function
36
45
  * Default: 3 retries
@@ -918,6 +927,10 @@ interface IStrategyTickResultActive {
918
927
  exchangeName: ExchangeName;
919
928
  /** Trading pair symbol (e.g., "BTCUSDT") */
920
929
  symbol: string;
930
+ /** Percentage progress towards take profit (0-100%, 0 if moving towards SL) */
931
+ percentTp: number;
932
+ /** Percentage progress towards stop loss (0-100%, 0 if moving towards TP) */
933
+ percentSl: number;
921
934
  }
922
935
  /**
923
936
  * Tick result: signal closed with PNL.
@@ -3764,6 +3777,10 @@ interface TickEvent {
3764
3777
  takeProfit?: number;
3765
3778
  /** Stop loss price (only for opened/active/closed) */
3766
3779
  stopLoss?: number;
3780
+ /** Percentage progress towards take profit (only for active) */
3781
+ percentTp?: number;
3782
+ /** Percentage progress towards stop loss (only for active) */
3783
+ percentSl?: number;
3767
3784
  /** PNL percentage (only for closed) */
3768
3785
  pnl?: number;
3769
3786
  /** Close reason (only for closed) */