backtest-kit 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -2740,6 +2740,24 @@ class BacktestGlobalService {
2740
2740
  }
2741
2741
  }
2742
2742
 
2743
+ /**
2744
+ * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
2745
+ *
2746
+ * @param value - Value to check
2747
+ * @returns true if value is unsafe, false otherwise
2748
+ */
2749
+ function isUnsafe$1(value) {
2750
+ if (typeof value !== "number") {
2751
+ return true;
2752
+ }
2753
+ if (isNaN(value)) {
2754
+ return true;
2755
+ }
2756
+ if (!isFinite(value)) {
2757
+ return true;
2758
+ }
2759
+ return false;
2760
+ }
2743
2761
  const columns$1 = [
2744
2762
  {
2745
2763
  key: "signalId",
@@ -2832,13 +2850,80 @@ let ReportStorage$1 = class ReportStorage {
2832
2850
  this._signalList.push(data);
2833
2851
  }
2834
2852
  /**
2835
- * Generates markdown report with all closed signals for a strategy.
2853
+ * Calculates statistical data from closed signals (Controller).
2854
+ * Returns null for any unsafe numeric values (NaN, Infinity, etc).
2855
+ *
2856
+ * @returns Statistical data (empty object if no signals)
2857
+ */
2858
+ async getData() {
2859
+ if (this._signalList.length === 0) {
2860
+ return {
2861
+ signalList: [],
2862
+ totalSignals: 0,
2863
+ winCount: 0,
2864
+ lossCount: 0,
2865
+ winRate: null,
2866
+ avgPnl: null,
2867
+ totalPnl: null,
2868
+ stdDev: null,
2869
+ sharpeRatio: null,
2870
+ annualizedSharpeRatio: null,
2871
+ certaintyRatio: null,
2872
+ expectedYearlyReturns: null,
2873
+ };
2874
+ }
2875
+ const totalSignals = this._signalList.length;
2876
+ const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2877
+ const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2878
+ // Calculate basic statistics
2879
+ const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2880
+ const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2881
+ const winRate = (winCount / totalSignals) * 100;
2882
+ // Calculate Sharpe Ratio (risk-free rate = 0)
2883
+ const returns = this._signalList.map((s) => s.pnl.pnlPercentage);
2884
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalSignals;
2885
+ const stdDev = Math.sqrt(variance);
2886
+ const sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
2887
+ const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
2888
+ // Calculate Certainty Ratio
2889
+ const wins = this._signalList.filter((s) => s.pnl.pnlPercentage > 0);
2890
+ const losses = this._signalList.filter((s) => s.pnl.pnlPercentage < 0);
2891
+ const avgWin = wins.length > 0
2892
+ ? wins.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / wins.length
2893
+ : 0;
2894
+ const avgLoss = losses.length > 0
2895
+ ? losses.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / losses.length
2896
+ : 0;
2897
+ const certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
2898
+ // Calculate Expected Yearly Returns
2899
+ const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.timestamp), 0) / totalSignals;
2900
+ const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
2901
+ const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
2902
+ const expectedYearlyReturns = avgPnl * tradesPerYear;
2903
+ return {
2904
+ signalList: this._signalList,
2905
+ totalSignals,
2906
+ winCount,
2907
+ lossCount,
2908
+ winRate: isUnsafe$1(winRate) ? null : winRate,
2909
+ avgPnl: isUnsafe$1(avgPnl) ? null : avgPnl,
2910
+ totalPnl: isUnsafe$1(totalPnl) ? null : totalPnl,
2911
+ stdDev: isUnsafe$1(stdDev) ? null : stdDev,
2912
+ sharpeRatio: isUnsafe$1(sharpeRatio) ? null : sharpeRatio,
2913
+ annualizedSharpeRatio: isUnsafe$1(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
2914
+ certaintyRatio: isUnsafe$1(certaintyRatio) ? null : certaintyRatio,
2915
+ expectedYearlyReturns: isUnsafe$1(expectedYearlyReturns) ? null : expectedYearlyReturns,
2916
+ };
2917
+ }
2918
+ /**
2919
+ * Generates markdown report with all closed signals for a strategy (View).
2836
2920
  *
2837
2921
  * @param strategyName - Strategy name
2838
2922
  * @returns Markdown formatted report with all signals
2839
2923
  */
2840
- getReport(strategyName) {
2841
- if (this._signalList.length === 0) {
2924
+ async getReport(strategyName) {
2925
+ const stats = await this.getData();
2926
+ if (stats.totalSignals === 0) {
2842
2927
  return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
2843
2928
  }
2844
2929
  const header = columns$1.map((col) => col.label);
@@ -2846,13 +2931,7 @@ let ReportStorage$1 = class ReportStorage {
2846
2931
  const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
2847
2932
  const tableData = [header, separator, ...rows];
2848
2933
  const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
2849
- // Calculate statistics
2850
- const totalSignals = this._signalList.length;
2851
- const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2852
- const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2853
- const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2854
- const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2855
- return str.newline(`# Backtest Report: ${strategyName}`, "", table, "", `**Total signals:** ${totalSignals}`, `**Closed signals:** ${totalSignals}`, `**Win rate:** ${((winCount / totalSignals) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`, `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`, `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`);
2934
+ 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)`}`);
2856
2935
  }
2857
2936
  /**
2858
2937
  * Saves strategy report to disk.
@@ -2861,7 +2940,7 @@ let ReportStorage$1 = class ReportStorage {
2861
2940
  * @param path - Directory path to save report (default: "./logs/backtest")
2862
2941
  */
2863
2942
  async dump(strategyName, path = "./logs/backtest") {
2864
- const markdown = this.getReport(strategyName);
2943
+ const markdown = await this.getReport(strategyName);
2865
2944
  try {
2866
2945
  const dir = join(process.cwd(), path);
2867
2946
  await mkdir(dir, { recursive: true });
@@ -2940,6 +3019,27 @@ class BacktestMarkdownService {
2940
3019
  const storage = this.getStorage(data.strategyName);
2941
3020
  storage.addSignal(data);
2942
3021
  };
3022
+ /**
3023
+ * Gets statistical data from all closed signals for a strategy.
3024
+ * Delegates to ReportStorage.getData().
3025
+ *
3026
+ * @param strategyName - Strategy name to get data for
3027
+ * @returns Statistical data object with all metrics
3028
+ *
3029
+ * @example
3030
+ * ```typescript
3031
+ * const service = new BacktestMarkdownService();
3032
+ * const stats = await service.getData("my-strategy");
3033
+ * console.log(stats.sharpeRatio, stats.winRate);
3034
+ * ```
3035
+ */
3036
+ this.getData = async (strategyName) => {
3037
+ this.loggerService.log("backtestMarkdownService getData", {
3038
+ strategyName,
3039
+ });
3040
+ const storage = this.getStorage(strategyName);
3041
+ return storage.getData();
3042
+ };
2943
3043
  /**
2944
3044
  * Generates markdown report with all closed signals for a strategy.
2945
3045
  * Delegates to ReportStorage.generateReport().
@@ -2950,7 +3050,7 @@ class BacktestMarkdownService {
2950
3050
  * @example
2951
3051
  * ```typescript
2952
3052
  * const service = new BacktestMarkdownService();
2953
- * const markdown = service.generateReport("my-strategy");
3053
+ * const markdown = await service.getReport("my-strategy");
2954
3054
  * console.log(markdown);
2955
3055
  * ```
2956
3056
  */
@@ -3030,6 +3130,24 @@ class BacktestMarkdownService {
3030
3130
  }
3031
3131
  }
3032
3132
 
3133
+ /**
3134
+ * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
3135
+ *
3136
+ * @param value - Value to check
3137
+ * @returns true if value is unsafe, false otherwise
3138
+ */
3139
+ function isUnsafe(value) {
3140
+ if (typeof value !== "number") {
3141
+ return true;
3142
+ }
3143
+ if (isNaN(value)) {
3144
+ return true;
3145
+ }
3146
+ if (!isFinite(value)) {
3147
+ return true;
3148
+ }
3149
+ return false;
3150
+ }
3033
3151
  const columns = [
3034
3152
  {
3035
3153
  key: "timestamp",
@@ -3237,36 +3355,103 @@ class ReportStorage {
3237
3355
  }
3238
3356
  }
3239
3357
  /**
3240
- * Generates markdown report with all tick events for a strategy.
3358
+ * Calculates statistical data from live trading events (Controller).
3359
+ * Returns null for any unsafe numeric values (NaN, Infinity, etc).
3241
3360
  *
3242
- * @param strategyName - Strategy name
3243
- * @returns Markdown formatted report with all events
3361
+ * @returns Statistical data (empty object if no events)
3244
3362
  */
3245
- getReport(strategyName) {
3363
+ async getData() {
3246
3364
  if (this._eventList.length === 0) {
3247
- return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3365
+ return {
3366
+ eventList: [],
3367
+ totalEvents: 0,
3368
+ totalClosed: 0,
3369
+ winCount: 0,
3370
+ lossCount: 0,
3371
+ winRate: null,
3372
+ avgPnl: null,
3373
+ totalPnl: null,
3374
+ stdDev: null,
3375
+ sharpeRatio: null,
3376
+ annualizedSharpeRatio: null,
3377
+ certaintyRatio: null,
3378
+ expectedYearlyReturns: null,
3379
+ };
3248
3380
  }
3249
- const header = columns.map((col) => col.label);
3250
- const separator = columns.map(() => "---");
3251
- const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3252
- const tableData = [header, separator, ...rows];
3253
- const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3254
- // Calculate statistics
3255
3381
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
3256
3382
  const totalClosed = closedEvents.length;
3257
3383
  const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
3258
3384
  const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
3385
+ // Calculate basic statistics
3259
3386
  const avgPnl = totalClosed > 0
3260
3387
  ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
3261
3388
  : 0;
3262
3389
  const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
3263
- return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${this._eventList.length}`, `**Closed signals:** ${totalClosed}`, totalClosed > 0
3264
- ? `**Win rate:** ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3265
- : "", totalClosed > 0
3266
- ? `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3267
- : "", totalClosed > 0
3268
- ? `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`
3269
- : "");
3390
+ const winRate = (winCount / totalClosed) * 100;
3391
+ // Calculate Sharpe Ratio (risk-free rate = 0)
3392
+ let sharpeRatio = 0;
3393
+ let stdDev = 0;
3394
+ if (totalClosed > 0) {
3395
+ const returns = closedEvents.map((e) => e.pnl || 0);
3396
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
3397
+ stdDev = Math.sqrt(variance);
3398
+ sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
3399
+ }
3400
+ const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
3401
+ // Calculate Certainty Ratio
3402
+ let certaintyRatio = 0;
3403
+ if (totalClosed > 0) {
3404
+ const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
3405
+ const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
3406
+ const avgWin = wins.length > 0
3407
+ ? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
3408
+ : 0;
3409
+ const avgLoss = losses.length > 0
3410
+ ? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
3411
+ : 0;
3412
+ certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
3413
+ }
3414
+ // Calculate Expected Yearly Returns
3415
+ let expectedYearlyReturns = 0;
3416
+ if (totalClosed > 0) {
3417
+ const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
3418
+ const avgDurationDays = avgDurationMin / (60 * 24);
3419
+ const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
3420
+ expectedYearlyReturns = avgPnl * tradesPerYear;
3421
+ }
3422
+ return {
3423
+ eventList: this._eventList,
3424
+ totalEvents: this._eventList.length,
3425
+ totalClosed,
3426
+ winCount,
3427
+ lossCount,
3428
+ winRate: isUnsafe(winRate) ? null : winRate,
3429
+ avgPnl: isUnsafe(avgPnl) ? null : avgPnl,
3430
+ totalPnl: isUnsafe(totalPnl) ? null : totalPnl,
3431
+ stdDev: isUnsafe(stdDev) ? null : stdDev,
3432
+ sharpeRatio: isUnsafe(sharpeRatio) ? null : sharpeRatio,
3433
+ annualizedSharpeRatio: isUnsafe(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
3434
+ certaintyRatio: isUnsafe(certaintyRatio) ? null : certaintyRatio,
3435
+ expectedYearlyReturns: isUnsafe(expectedYearlyReturns) ? null : expectedYearlyReturns,
3436
+ };
3437
+ }
3438
+ /**
3439
+ * Generates markdown report with all tick events for a strategy (View).
3440
+ *
3441
+ * @param strategyName - Strategy name
3442
+ * @returns Markdown formatted report with all events
3443
+ */
3444
+ async getReport(strategyName) {
3445
+ const stats = await this.getData();
3446
+ if (stats.totalEvents === 0) {
3447
+ return str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3448
+ }
3449
+ const header = columns.map((col) => col.label);
3450
+ const separator = columns.map(() => "---");
3451
+ const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3452
+ const tableData = [header, separator, ...rows];
3453
+ const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3454
+ 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)`}`);
3270
3455
  }
3271
3456
  /**
3272
3457
  * Saves strategy report to disk.
@@ -3275,7 +3460,7 @@ class ReportStorage {
3275
3460
  * @param path - Directory path to save report (default: "./logs/live")
3276
3461
  */
3277
3462
  async dump(strategyName, path = "./logs/live") {
3278
- const markdown = this.getReport(strategyName);
3463
+ const markdown = await this.getReport(strategyName);
3279
3464
  try {
3280
3465
  const dir = join(process.cwd(), path);
3281
3466
  await mkdir(dir, { recursive: true });
@@ -3367,6 +3552,27 @@ class LiveMarkdownService {
3367
3552
  storage.addClosedEvent(data);
3368
3553
  }
3369
3554
  };
3555
+ /**
3556
+ * Gets statistical data from all live trading events for a strategy.
3557
+ * Delegates to ReportStorage.getData().
3558
+ *
3559
+ * @param strategyName - Strategy name to get data for
3560
+ * @returns Statistical data object with all metrics
3561
+ *
3562
+ * @example
3563
+ * ```typescript
3564
+ * const service = new LiveMarkdownService();
3565
+ * const stats = await service.getData("my-strategy");
3566
+ * console.log(stats.sharpeRatio, stats.winRate);
3567
+ * ```
3568
+ */
3569
+ this.getData = async (strategyName) => {
3570
+ this.loggerService.log("liveMarkdownService getData", {
3571
+ strategyName,
3572
+ });
3573
+ const storage = this.getStorage(strategyName);
3574
+ return storage.getData();
3575
+ };
3370
3576
  /**
3371
3577
  * Generates markdown report with all events for a strategy.
3372
3578
  * Delegates to ReportStorage.getReport().
@@ -4504,6 +4710,24 @@ class BacktestUtils {
4504
4710
  isStopped = true;
4505
4711
  };
4506
4712
  };
4713
+ /**
4714
+ * Gets statistical data from all closed signals for a strategy.
4715
+ *
4716
+ * @param strategyName - Strategy name to get data for
4717
+ * @returns Promise resolving to statistical data object
4718
+ *
4719
+ * @example
4720
+ * ```typescript
4721
+ * const stats = await Backtest.getData("my-strategy");
4722
+ * console.log(stats.sharpeRatio, stats.winRate);
4723
+ * ```
4724
+ */
4725
+ this.getData = async (strategyName) => {
4726
+ backtest$1.loggerService.info("BacktestUtils.getData", {
4727
+ strategyName,
4728
+ });
4729
+ return await backtest$1.backtestMarkdownService.getData(strategyName);
4730
+ };
4507
4731
  /**
4508
4732
  * Generates markdown report with all closed signals for a strategy.
4509
4733
  *
@@ -4666,6 +4890,24 @@ class LiveUtils {
4666
4890
  isStopped = true;
4667
4891
  };
4668
4892
  };
4893
+ /**
4894
+ * Gets statistical data from all live trading events for a strategy.
4895
+ *
4896
+ * @param strategyName - Strategy name to get data for
4897
+ * @returns Promise resolving to statistical data object
4898
+ *
4899
+ * @example
4900
+ * ```typescript
4901
+ * const stats = await Live.getData("my-strategy");
4902
+ * console.log(stats.sharpeRatio, stats.winRate);
4903
+ * ```
4904
+ */
4905
+ this.getData = async (strategyName) => {
4906
+ backtest$1.loggerService.info("LiveUtils.getData", {
4907
+ strategyName,
4908
+ });
4909
+ return await backtest$1.liveMarkdownService.getData(strategyName);
4910
+ };
4669
4911
  /**
4670
4912
  * Generates markdown report with all events for a strategy.
4671
4913
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",