backtest-kit 1.4.10 → 1.4.11

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
@@ -7262,6 +7262,118 @@ function formatMetric(value) {
7262
7262
  }
7263
7263
  return value.toFixed(2);
7264
7264
  }
7265
+ /**
7266
+ * Creates strategy comparison columns based on metric name.
7267
+ * Dynamically builds column configuration with metric-specific header.
7268
+ *
7269
+ * @param metric - Metric being optimized
7270
+ * @returns Array of column configurations for strategy comparison table
7271
+ */
7272
+ function createStrategyColumns(metric) {
7273
+ return [
7274
+ {
7275
+ key: "rank",
7276
+ label: "Rank",
7277
+ format: (data, index) => `${index + 1}`,
7278
+ },
7279
+ {
7280
+ key: "strategy",
7281
+ label: "Strategy",
7282
+ format: (data) => data.strategyName,
7283
+ },
7284
+ {
7285
+ key: "metric",
7286
+ label: metric,
7287
+ format: (data) => formatMetric(data.metricValue),
7288
+ },
7289
+ {
7290
+ key: "totalSignals",
7291
+ label: "Total Signals",
7292
+ format: (data) => `${data.stats.totalSignals}`,
7293
+ },
7294
+ {
7295
+ key: "winRate",
7296
+ label: "Win Rate",
7297
+ format: (data) => data.stats.winRate !== null
7298
+ ? `${data.stats.winRate.toFixed(2)}%`
7299
+ : "N/A",
7300
+ },
7301
+ {
7302
+ key: "avgPnl",
7303
+ label: "Avg PNL",
7304
+ format: (data) => data.stats.avgPnl !== null
7305
+ ? `${data.stats.avgPnl > 0 ? "+" : ""}${data.stats.avgPnl.toFixed(2)}%`
7306
+ : "N/A",
7307
+ },
7308
+ {
7309
+ key: "totalPnl",
7310
+ label: "Total PNL",
7311
+ format: (data) => data.stats.totalPnl !== null
7312
+ ? `${data.stats.totalPnl > 0 ? "+" : ""}${data.stats.totalPnl.toFixed(2)}%`
7313
+ : "N/A",
7314
+ },
7315
+ {
7316
+ key: "sharpeRatio",
7317
+ label: "Sharpe Ratio",
7318
+ format: (data) => data.stats.sharpeRatio !== null
7319
+ ? `${data.stats.sharpeRatio.toFixed(3)}`
7320
+ : "N/A",
7321
+ },
7322
+ {
7323
+ key: "stdDev",
7324
+ label: "Std Dev",
7325
+ format: (data) => data.stats.stdDev !== null
7326
+ ? `${data.stats.stdDev.toFixed(3)}%`
7327
+ : "N/A",
7328
+ },
7329
+ ];
7330
+ }
7331
+ /**
7332
+ * Column configuration for PNL table.
7333
+ * Defines all columns for displaying closed signals across strategies.
7334
+ */
7335
+ const pnlColumns = [
7336
+ {
7337
+ key: "strategy",
7338
+ label: "Strategy",
7339
+ format: (data) => data.strategyName,
7340
+ },
7341
+ {
7342
+ key: "signalId",
7343
+ label: "Signal ID",
7344
+ format: (data) => data.signalId,
7345
+ },
7346
+ {
7347
+ key: "symbol",
7348
+ label: "Symbol",
7349
+ format: (data) => data.symbol,
7350
+ },
7351
+ {
7352
+ key: "position",
7353
+ label: "Position",
7354
+ format: (data) => data.position.toUpperCase(),
7355
+ },
7356
+ {
7357
+ key: "pnl",
7358
+ label: "PNL (net)",
7359
+ format: (data) => `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`,
7360
+ },
7361
+ {
7362
+ key: "closeReason",
7363
+ label: "Close Reason",
7364
+ format: (data) => data.closeReason,
7365
+ },
7366
+ {
7367
+ key: "openTime",
7368
+ label: "Open Time",
7369
+ format: (data) => new Date(data.openTime).toISOString(),
7370
+ },
7371
+ {
7372
+ key: "closeTime",
7373
+ label: "Close Time",
7374
+ format: (data) => new Date(data.closeTime).toISOString(),
7375
+ },
7376
+ ];
7265
7377
  /**
7266
7378
  * Storage class for accumulating walker results.
7267
7379
  * Maintains a list of all strategy results and provides methods to generate reports.
@@ -7274,9 +7386,12 @@ let ReportStorage$1 = class ReportStorage {
7274
7386
  this._bestStats = null;
7275
7387
  this._bestMetric = null;
7276
7388
  this._bestStrategy = null;
7389
+ /** All strategy results for comparison table */
7390
+ this._strategyResults = [];
7277
7391
  }
7278
7392
  /**
7279
7393
  * Adds a strategy result to the storage.
7394
+ * Updates best strategy tracking and accumulates result for comparison table.
7280
7395
  *
7281
7396
  * @param data - Walker contract with strategy result
7282
7397
  */
@@ -7290,6 +7405,12 @@ let ReportStorage$1 = class ReportStorage {
7290
7405
  if (data.strategyName === data.bestStrategy) {
7291
7406
  this._bestStats = data.stats;
7292
7407
  }
7408
+ // Add strategy result to comparison list
7409
+ this._strategyResults.push({
7410
+ strategyName: data.strategyName,
7411
+ stats: data.stats,
7412
+ metricValue: data.metricValue,
7413
+ });
7293
7414
  }
7294
7415
  /**
7295
7416
  * Calculates walker results from strategy results.
@@ -7314,10 +7435,79 @@ let ReportStorage$1 = class ReportStorage {
7314
7435
  bestStrategy: this._bestStrategy,
7315
7436
  bestMetric: this._bestMetric,
7316
7437
  bestStats: this._bestStats,
7438
+ strategyResults: this._strategyResults,
7317
7439
  };
7318
7440
  }
7441
+ /**
7442
+ * Generates comparison table for top N strategies (View).
7443
+ * Sorts strategies by metric value and formats as markdown table.
7444
+ *
7445
+ * @param metric - Metric being optimized
7446
+ * @param topN - Number of top strategies to include (default: 10)
7447
+ * @returns Markdown formatted comparison table
7448
+ */
7449
+ getComparisonTable(metric, topN = 10) {
7450
+ if (this._strategyResults.length === 0) {
7451
+ return "No strategy results available.";
7452
+ }
7453
+ // Sort strategies by metric value (descending)
7454
+ const sortedResults = [...this._strategyResults].sort((a, b) => {
7455
+ const aValue = a.metricValue ?? -Infinity;
7456
+ const bValue = b.metricValue ?? -Infinity;
7457
+ return bValue - aValue;
7458
+ });
7459
+ // Take top N strategies
7460
+ const topStrategies = sortedResults.slice(0, topN);
7461
+ // Get columns configuration
7462
+ const columns = createStrategyColumns(metric);
7463
+ // Build table header
7464
+ const header = columns.map((col) => col.label);
7465
+ const separator = columns.map(() => "---");
7466
+ // Build table rows
7467
+ const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7468
+ const tableData = [header, separator, ...rows];
7469
+ return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7470
+ }
7471
+ /**
7472
+ * Generates PNL table showing all closed signals across all strategies (View).
7473
+ * Collects all signals from all strategies and formats as markdown table.
7474
+ *
7475
+ * @returns Markdown formatted PNL table
7476
+ */
7477
+ getPnlTable() {
7478
+ if (this._strategyResults.length === 0) {
7479
+ return "No strategy results available.";
7480
+ }
7481
+ // Collect all closed signals from all strategies
7482
+ const allSignals = [];
7483
+ for (const result of this._strategyResults) {
7484
+ for (const signal of result.stats.signalList) {
7485
+ allSignals.push({
7486
+ strategyName: result.strategyName,
7487
+ signalId: signal.signal.id,
7488
+ symbol: signal.signal.symbol,
7489
+ position: signal.signal.position,
7490
+ pnl: signal.pnl.pnlPercentage,
7491
+ closeReason: signal.closeReason,
7492
+ openTime: signal.signal.pendingAt,
7493
+ closeTime: signal.closeTimestamp,
7494
+ });
7495
+ }
7496
+ }
7497
+ if (allSignals.length === 0) {
7498
+ return "No closed signals available.";
7499
+ }
7500
+ // Build table header
7501
+ const header = pnlColumns.map((col) => col.label);
7502
+ const separator = pnlColumns.map(() => "---");
7503
+ // Build table rows
7504
+ const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7505
+ const tableData = [header, separator, ...rows];
7506
+ return functoolsKit.str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7507
+ }
7319
7508
  /**
7320
7509
  * Generates markdown report with all strategy results (View).
7510
+ * Includes best strategy summary, comparison table, and PNL table.
7321
7511
  *
7322
7512
  * @param symbol - Trading symbol
7323
7513
  * @param metric - Metric being optimized
@@ -7326,7 +7516,9 @@ let ReportStorage$1 = class ReportStorage {
7326
7516
  */
7327
7517
  async getReport(symbol, metric, context) {
7328
7518
  const results = await this.getData(symbol, metric, context);
7329
- 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)}`, "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
7519
+ // Get total signals for best strategy
7520
+ const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
7521
+ 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).");
7330
7522
  }
7331
7523
  /**
7332
7524
  * Saves walker report to disk.
package/build/index.mjs CHANGED
@@ -7260,6 +7260,118 @@ function formatMetric(value) {
7260
7260
  }
7261
7261
  return value.toFixed(2);
7262
7262
  }
7263
+ /**
7264
+ * Creates strategy comparison columns based on metric name.
7265
+ * Dynamically builds column configuration with metric-specific header.
7266
+ *
7267
+ * @param metric - Metric being optimized
7268
+ * @returns Array of column configurations for strategy comparison table
7269
+ */
7270
+ function createStrategyColumns(metric) {
7271
+ return [
7272
+ {
7273
+ key: "rank",
7274
+ label: "Rank",
7275
+ format: (data, index) => `${index + 1}`,
7276
+ },
7277
+ {
7278
+ key: "strategy",
7279
+ label: "Strategy",
7280
+ format: (data) => data.strategyName,
7281
+ },
7282
+ {
7283
+ key: "metric",
7284
+ label: metric,
7285
+ format: (data) => formatMetric(data.metricValue),
7286
+ },
7287
+ {
7288
+ key: "totalSignals",
7289
+ label: "Total Signals",
7290
+ format: (data) => `${data.stats.totalSignals}`,
7291
+ },
7292
+ {
7293
+ key: "winRate",
7294
+ label: "Win Rate",
7295
+ format: (data) => data.stats.winRate !== null
7296
+ ? `${data.stats.winRate.toFixed(2)}%`
7297
+ : "N/A",
7298
+ },
7299
+ {
7300
+ key: "avgPnl",
7301
+ label: "Avg PNL",
7302
+ format: (data) => data.stats.avgPnl !== null
7303
+ ? `${data.stats.avgPnl > 0 ? "+" : ""}${data.stats.avgPnl.toFixed(2)}%`
7304
+ : "N/A",
7305
+ },
7306
+ {
7307
+ key: "totalPnl",
7308
+ label: "Total PNL",
7309
+ format: (data) => data.stats.totalPnl !== null
7310
+ ? `${data.stats.totalPnl > 0 ? "+" : ""}${data.stats.totalPnl.toFixed(2)}%`
7311
+ : "N/A",
7312
+ },
7313
+ {
7314
+ key: "sharpeRatio",
7315
+ label: "Sharpe Ratio",
7316
+ format: (data) => data.stats.sharpeRatio !== null
7317
+ ? `${data.stats.sharpeRatio.toFixed(3)}`
7318
+ : "N/A",
7319
+ },
7320
+ {
7321
+ key: "stdDev",
7322
+ label: "Std Dev",
7323
+ format: (data) => data.stats.stdDev !== null
7324
+ ? `${data.stats.stdDev.toFixed(3)}%`
7325
+ : "N/A",
7326
+ },
7327
+ ];
7328
+ }
7329
+ /**
7330
+ * Column configuration for PNL table.
7331
+ * Defines all columns for displaying closed signals across strategies.
7332
+ */
7333
+ const pnlColumns = [
7334
+ {
7335
+ key: "strategy",
7336
+ label: "Strategy",
7337
+ format: (data) => data.strategyName,
7338
+ },
7339
+ {
7340
+ key: "signalId",
7341
+ label: "Signal ID",
7342
+ format: (data) => data.signalId,
7343
+ },
7344
+ {
7345
+ key: "symbol",
7346
+ label: "Symbol",
7347
+ format: (data) => data.symbol,
7348
+ },
7349
+ {
7350
+ key: "position",
7351
+ label: "Position",
7352
+ format: (data) => data.position.toUpperCase(),
7353
+ },
7354
+ {
7355
+ key: "pnl",
7356
+ label: "PNL (net)",
7357
+ format: (data) => `${data.pnl > 0 ? "+" : ""}${data.pnl.toFixed(2)}%`,
7358
+ },
7359
+ {
7360
+ key: "closeReason",
7361
+ label: "Close Reason",
7362
+ format: (data) => data.closeReason,
7363
+ },
7364
+ {
7365
+ key: "openTime",
7366
+ label: "Open Time",
7367
+ format: (data) => new Date(data.openTime).toISOString(),
7368
+ },
7369
+ {
7370
+ key: "closeTime",
7371
+ label: "Close Time",
7372
+ format: (data) => new Date(data.closeTime).toISOString(),
7373
+ },
7374
+ ];
7263
7375
  /**
7264
7376
  * Storage class for accumulating walker results.
7265
7377
  * Maintains a list of all strategy results and provides methods to generate reports.
@@ -7272,9 +7384,12 @@ let ReportStorage$1 = class ReportStorage {
7272
7384
  this._bestStats = null;
7273
7385
  this._bestMetric = null;
7274
7386
  this._bestStrategy = null;
7387
+ /** All strategy results for comparison table */
7388
+ this._strategyResults = [];
7275
7389
  }
7276
7390
  /**
7277
7391
  * Adds a strategy result to the storage.
7392
+ * Updates best strategy tracking and accumulates result for comparison table.
7278
7393
  *
7279
7394
  * @param data - Walker contract with strategy result
7280
7395
  */
@@ -7288,6 +7403,12 @@ let ReportStorage$1 = class ReportStorage {
7288
7403
  if (data.strategyName === data.bestStrategy) {
7289
7404
  this._bestStats = data.stats;
7290
7405
  }
7406
+ // Add strategy result to comparison list
7407
+ this._strategyResults.push({
7408
+ strategyName: data.strategyName,
7409
+ stats: data.stats,
7410
+ metricValue: data.metricValue,
7411
+ });
7291
7412
  }
7292
7413
  /**
7293
7414
  * Calculates walker results from strategy results.
@@ -7312,10 +7433,79 @@ let ReportStorage$1 = class ReportStorage {
7312
7433
  bestStrategy: this._bestStrategy,
7313
7434
  bestMetric: this._bestMetric,
7314
7435
  bestStats: this._bestStats,
7436
+ strategyResults: this._strategyResults,
7315
7437
  };
7316
7438
  }
7439
+ /**
7440
+ * Generates comparison table for top N strategies (View).
7441
+ * Sorts strategies by metric value and formats as markdown table.
7442
+ *
7443
+ * @param metric - Metric being optimized
7444
+ * @param topN - Number of top strategies to include (default: 10)
7445
+ * @returns Markdown formatted comparison table
7446
+ */
7447
+ getComparisonTable(metric, topN = 10) {
7448
+ if (this._strategyResults.length === 0) {
7449
+ return "No strategy results available.";
7450
+ }
7451
+ // Sort strategies by metric value (descending)
7452
+ const sortedResults = [...this._strategyResults].sort((a, b) => {
7453
+ const aValue = a.metricValue ?? -Infinity;
7454
+ const bValue = b.metricValue ?? -Infinity;
7455
+ return bValue - aValue;
7456
+ });
7457
+ // Take top N strategies
7458
+ const topStrategies = sortedResults.slice(0, topN);
7459
+ // Get columns configuration
7460
+ const columns = createStrategyColumns(metric);
7461
+ // Build table header
7462
+ const header = columns.map((col) => col.label);
7463
+ const separator = columns.map(() => "---");
7464
+ // Build table rows
7465
+ const rows = topStrategies.map((result, index) => columns.map((col) => col.format(result, index)));
7466
+ const tableData = [header, separator, ...rows];
7467
+ return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7468
+ }
7469
+ /**
7470
+ * Generates PNL table showing all closed signals across all strategies (View).
7471
+ * Collects all signals from all strategies and formats as markdown table.
7472
+ *
7473
+ * @returns Markdown formatted PNL table
7474
+ */
7475
+ getPnlTable() {
7476
+ if (this._strategyResults.length === 0) {
7477
+ return "No strategy results available.";
7478
+ }
7479
+ // Collect all closed signals from all strategies
7480
+ const allSignals = [];
7481
+ for (const result of this._strategyResults) {
7482
+ for (const signal of result.stats.signalList) {
7483
+ allSignals.push({
7484
+ strategyName: result.strategyName,
7485
+ signalId: signal.signal.id,
7486
+ symbol: signal.signal.symbol,
7487
+ position: signal.signal.position,
7488
+ pnl: signal.pnl.pnlPercentage,
7489
+ closeReason: signal.closeReason,
7490
+ openTime: signal.signal.pendingAt,
7491
+ closeTime: signal.closeTimestamp,
7492
+ });
7493
+ }
7494
+ }
7495
+ if (allSignals.length === 0) {
7496
+ return "No closed signals available.";
7497
+ }
7498
+ // Build table header
7499
+ const header = pnlColumns.map((col) => col.label);
7500
+ const separator = pnlColumns.map(() => "---");
7501
+ // Build table rows
7502
+ const rows = allSignals.map((signal) => pnlColumns.map((col) => col.format(signal)));
7503
+ const tableData = [header, separator, ...rows];
7504
+ return str.newline(tableData.map((row) => `| ${row.join(" | ")} |`));
7505
+ }
7317
7506
  /**
7318
7507
  * Generates markdown report with all strategy results (View).
7508
+ * Includes best strategy summary, comparison table, and PNL table.
7319
7509
  *
7320
7510
  * @param symbol - Trading symbol
7321
7511
  * @param metric - Metric being optimized
@@ -7324,7 +7514,9 @@ let ReportStorage$1 = class ReportStorage {
7324
7514
  */
7325
7515
  async getReport(symbol, metric, context) {
7326
7516
  const results = await this.getData(symbol, metric, context);
7327
- 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)}`, "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better).");
7517
+ // Get total signals for best strategy
7518
+ const bestStrategySignals = results.bestStats?.totalSignals ?? 0;
7519
+ 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).");
7328
7520
  }
7329
7521
  /**
7330
7522
  * Saves walker report to disk.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.4.10",
3
+ "version": "1.4.11",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -4258,8 +4258,24 @@ declare class PerformanceMarkdownService {
4258
4258
  * Alias for walker statistics result interface.
4259
4259
  * Used for clarity in markdown service context.
4260
4260
  *
4261
+ * Extends IWalkerResults with additional strategy comparison data.
4261
4262
  */
4262
- type WalkerStatistics = IWalkerResults;
4263
+ interface WalkerStatistics extends IWalkerResults {
4264
+ /** Array of all strategy results for comparison and analysis */
4265
+ strategyResults: IStrategyResult[];
4266
+ }
4267
+ /**
4268
+ * Strategy result entry for comparison table.
4269
+ * Contains strategy name, full statistics, and metric value for ranking.
4270
+ */
4271
+ interface IStrategyResult {
4272
+ /** Strategy name */
4273
+ strategyName: StrategyName;
4274
+ /** Complete backtest statistics for this strategy */
4275
+ stats: BacktestStatistics;
4276
+ /** Value of the optimization metric (null if invalid) */
4277
+ metricValue: number | null;
4278
+ }
4263
4279
  /**
4264
4280
  * Service for generating and saving walker markdown reports.
4265
4281
  *