backtest-kit 1.1.4 → 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
@@ -1127,6 +1127,11 @@ const errorEmitter = new Subject();
1127
1127
  * Emits when background tasks complete (Live.background, Backtest.background).
1128
1128
  */
1129
1129
  const doneEmitter = new Subject();
1130
+ /**
1131
+ * Progress emitter for backtest execution progress.
1132
+ * Emits progress updates during backtest execution.
1133
+ */
1134
+ const progressEmitter = new Subject();
1130
1135
 
1131
1136
  const INTERVAL_MINUTES$1 = {
1132
1137
  "1m": 1,
@@ -2400,6 +2405,7 @@ class BacktestLogicPrivateService {
2400
2405
  this.strategyGlobalService = inject(TYPES.strategyGlobalService);
2401
2406
  this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
2402
2407
  this.frameGlobalService = inject(TYPES.frameGlobalService);
2408
+ this.methodContextService = inject(TYPES.methodContextService);
2403
2409
  }
2404
2410
  /**
2405
2411
  * Runs backtest for a symbol, streaming closed signals as async generator.
@@ -2420,9 +2426,21 @@ class BacktestLogicPrivateService {
2420
2426
  symbol,
2421
2427
  });
2422
2428
  const timeframes = await this.frameGlobalService.getTimeframe(symbol);
2429
+ const totalFrames = timeframes.length;
2423
2430
  let i = 0;
2424
2431
  while (i < timeframes.length) {
2425
2432
  const when = timeframes[i];
2433
+ // Emit progress event if context is available
2434
+ {
2435
+ await progressEmitter.next({
2436
+ exchangeName: this.methodContextService.context.exchangeName,
2437
+ strategyName: this.methodContextService.context.strategyName,
2438
+ symbol,
2439
+ totalFrames,
2440
+ processedFrames: i,
2441
+ progress: totalFrames > 0 ? i / totalFrames : 0,
2442
+ });
2443
+ }
2426
2444
  const result = await this.strategyGlobalService.tick(symbol, when, true);
2427
2445
  // Если сигнал открыт, вызываем backtest
2428
2446
  if (result.action === "opened") {
@@ -2459,6 +2477,17 @@ class BacktestLogicPrivateService {
2459
2477
  }
2460
2478
  i++;
2461
2479
  }
2480
+ // Emit final progress event (100%)
2481
+ {
2482
+ await progressEmitter.next({
2483
+ exchangeName: this.methodContextService.context.exchangeName,
2484
+ strategyName: this.methodContextService.context.strategyName,
2485
+ symbol,
2486
+ totalFrames,
2487
+ processedFrames: totalFrames,
2488
+ progress: 1.0,
2489
+ });
2490
+ }
2462
2491
  }
2463
2492
  }
2464
2493
 
@@ -2711,6 +2740,24 @@ class BacktestGlobalService {
2711
2740
  }
2712
2741
  }
2713
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
+ }
2714
2761
  const columns$1 = [
2715
2762
  {
2716
2763
  key: "signalId",
@@ -2803,13 +2850,80 @@ let ReportStorage$1 = class ReportStorage {
2803
2850
  this._signalList.push(data);
2804
2851
  }
2805
2852
  /**
2806
- * 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).
2807
2920
  *
2808
2921
  * @param strategyName - Strategy name
2809
2922
  * @returns Markdown formatted report with all signals
2810
2923
  */
2811
- getReport(strategyName) {
2812
- if (this._signalList.length === 0) {
2924
+ async getReport(strategyName) {
2925
+ const stats = await this.getData();
2926
+ if (stats.totalSignals === 0) {
2813
2927
  return str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
2814
2928
  }
2815
2929
  const header = columns$1.map((col) => col.label);
@@ -2817,13 +2931,7 @@ let ReportStorage$1 = class ReportStorage {
2817
2931
  const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
2818
2932
  const tableData = [header, separator, ...rows];
2819
2933
  const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
2820
- // Calculate statistics
2821
- const totalSignals = this._signalList.length;
2822
- const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2823
- const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2824
- const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2825
- const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2826
- 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)`}`);
2827
2935
  }
2828
2936
  /**
2829
2937
  * Saves strategy report to disk.
@@ -2832,7 +2940,7 @@ let ReportStorage$1 = class ReportStorage {
2832
2940
  * @param path - Directory path to save report (default: "./logs/backtest")
2833
2941
  */
2834
2942
  async dump(strategyName, path = "./logs/backtest") {
2835
- const markdown = this.getReport(strategyName);
2943
+ const markdown = await this.getReport(strategyName);
2836
2944
  try {
2837
2945
  const dir = join(process.cwd(), path);
2838
2946
  await mkdir(dir, { recursive: true });
@@ -2911,6 +3019,27 @@ class BacktestMarkdownService {
2911
3019
  const storage = this.getStorage(data.strategyName);
2912
3020
  storage.addSignal(data);
2913
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
+ };
2914
3043
  /**
2915
3044
  * Generates markdown report with all closed signals for a strategy.
2916
3045
  * Delegates to ReportStorage.generateReport().
@@ -2921,7 +3050,7 @@ class BacktestMarkdownService {
2921
3050
  * @example
2922
3051
  * ```typescript
2923
3052
  * const service = new BacktestMarkdownService();
2924
- * const markdown = service.generateReport("my-strategy");
3053
+ * const markdown = await service.getReport("my-strategy");
2925
3054
  * console.log(markdown);
2926
3055
  * ```
2927
3056
  */
@@ -3001,6 +3130,24 @@ class BacktestMarkdownService {
3001
3130
  }
3002
3131
  }
3003
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
+ }
3004
3151
  const columns = [
3005
3152
  {
3006
3153
  key: "timestamp",
@@ -3208,36 +3355,103 @@ class ReportStorage {
3208
3355
  }
3209
3356
  }
3210
3357
  /**
3211
- * 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).
3212
3360
  *
3213
- * @param strategyName - Strategy name
3214
- * @returns Markdown formatted report with all events
3361
+ * @returns Statistical data (empty object if no events)
3215
3362
  */
3216
- getReport(strategyName) {
3363
+ async getData() {
3217
3364
  if (this._eventList.length === 0) {
3218
- 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
+ };
3219
3380
  }
3220
- const header = columns.map((col) => col.label);
3221
- const separator = columns.map(() => "---");
3222
- const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3223
- const tableData = [header, separator, ...rows];
3224
- const table = str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3225
- // Calculate statistics
3226
3381
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
3227
3382
  const totalClosed = closedEvents.length;
3228
3383
  const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
3229
3384
  const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
3385
+ // Calculate basic statistics
3230
3386
  const avgPnl = totalClosed > 0
3231
3387
  ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
3232
3388
  : 0;
3233
3389
  const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
3234
- return str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${this._eventList.length}`, `**Closed signals:** ${totalClosed}`, totalClosed > 0
3235
- ? `**Win rate:** ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3236
- : "", totalClosed > 0
3237
- ? `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3238
- : "", totalClosed > 0
3239
- ? `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`
3240
- : "");
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)`}`);
3241
3455
  }
3242
3456
  /**
3243
3457
  * Saves strategy report to disk.
@@ -3246,7 +3460,7 @@ class ReportStorage {
3246
3460
  * @param path - Directory path to save report (default: "./logs/live")
3247
3461
  */
3248
3462
  async dump(strategyName, path = "./logs/live") {
3249
- const markdown = this.getReport(strategyName);
3463
+ const markdown = await this.getReport(strategyName);
3250
3464
  try {
3251
3465
  const dir = join(process.cwd(), path);
3252
3466
  await mkdir(dir, { recursive: true });
@@ -3338,6 +3552,27 @@ class LiveMarkdownService {
3338
3552
  storage.addClosedEvent(data);
3339
3553
  }
3340
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
+ };
3341
3576
  /**
3342
3577
  * Generates markdown report with all events for a strategy.
3343
3578
  * Delegates to ReportStorage.getReport().
@@ -3477,6 +3712,15 @@ class ExchangeValidationService {
3477
3712
  }
3478
3713
  return true;
3479
3714
  });
3715
+ /**
3716
+ * Returns a list of all registered exchange schemas
3717
+ * @public
3718
+ * @returns Array of exchange schemas with their configurations
3719
+ */
3720
+ this.list = async () => {
3721
+ this.loggerService.log("exchangeValidationService list");
3722
+ return Array.from(this._exchangeMap.values());
3723
+ };
3480
3724
  }
3481
3725
  }
3482
3726
 
@@ -3529,6 +3773,15 @@ class StrategyValidationService {
3529
3773
  }
3530
3774
  return true;
3531
3775
  });
3776
+ /**
3777
+ * Returns a list of all registered strategy schemas
3778
+ * @public
3779
+ * @returns Array of strategy schemas with their configurations
3780
+ */
3781
+ this.list = async () => {
3782
+ this.loggerService.log("strategyValidationService list");
3783
+ return Array.from(this._strategyMap.values());
3784
+ };
3532
3785
  }
3533
3786
  }
3534
3787
 
@@ -3581,6 +3834,15 @@ class FrameValidationService {
3581
3834
  }
3582
3835
  return true;
3583
3836
  });
3837
+ /**
3838
+ * Returns a list of all registered frame schemas
3839
+ * @public
3840
+ * @returns Array of frame schemas with their configurations
3841
+ */
3842
+ this.list = async () => {
3843
+ this.loggerService.log("frameValidationService list");
3844
+ return Array.from(this._frameMap.values());
3845
+ };
3584
3846
  }
3585
3847
  }
3586
3848
 
@@ -3826,6 +4088,102 @@ function addFrame(frameSchema) {
3826
4088
  backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
3827
4089
  }
3828
4090
 
4091
+ const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
4092
+ const LIST_STRATEGIES_METHOD_NAME = "list.listStrategies";
4093
+ const LIST_FRAMES_METHOD_NAME = "list.listFrames";
4094
+ /**
4095
+ * Returns a list of all registered exchange schemas.
4096
+ *
4097
+ * Retrieves all exchanges that have been registered via addExchange().
4098
+ * Useful for debugging, documentation, or building dynamic UIs.
4099
+ *
4100
+ * @returns Array of exchange schemas with their configurations
4101
+ *
4102
+ * @example
4103
+ * ```typescript
4104
+ * import { listExchanges, addExchange } from "backtest-kit";
4105
+ *
4106
+ * addExchange({
4107
+ * exchangeName: "binance",
4108
+ * note: "Binance cryptocurrency exchange",
4109
+ * getCandles: async (symbol, interval, since, limit) => [...],
4110
+ * formatPrice: async (symbol, price) => price.toFixed(2),
4111
+ * formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
4112
+ * });
4113
+ *
4114
+ * const exchanges = listExchanges();
4115
+ * console.log(exchanges);
4116
+ * // [{ exchangeName: "binance", note: "Binance cryptocurrency exchange", ... }]
4117
+ * ```
4118
+ */
4119
+ async function listExchanges() {
4120
+ backtest$1.loggerService.log(LIST_EXCHANGES_METHOD_NAME);
4121
+ return await backtest$1.exchangeValidationService.list();
4122
+ }
4123
+ /**
4124
+ * Returns a list of all registered strategy schemas.
4125
+ *
4126
+ * Retrieves all strategies that have been registered via addStrategy().
4127
+ * Useful for debugging, documentation, or building dynamic UIs.
4128
+ *
4129
+ * @returns Array of strategy schemas with their configurations
4130
+ *
4131
+ * @example
4132
+ * ```typescript
4133
+ * import { listStrategies, addStrategy } from "backtest-kit";
4134
+ *
4135
+ * addStrategy({
4136
+ * strategyName: "my-strategy",
4137
+ * note: "Simple moving average crossover strategy",
4138
+ * interval: "5m",
4139
+ * getSignal: async (symbol) => ({
4140
+ * position: "long",
4141
+ * priceOpen: 50000,
4142
+ * priceTakeProfit: 51000,
4143
+ * priceStopLoss: 49000,
4144
+ * minuteEstimatedTime: 60,
4145
+ * }),
4146
+ * });
4147
+ *
4148
+ * const strategies = listStrategies();
4149
+ * console.log(strategies);
4150
+ * // [{ strategyName: "my-strategy", note: "Simple moving average...", ... }]
4151
+ * ```
4152
+ */
4153
+ async function listStrategies() {
4154
+ backtest$1.loggerService.log(LIST_STRATEGIES_METHOD_NAME);
4155
+ return await backtest$1.strategyValidationService.list();
4156
+ }
4157
+ /**
4158
+ * Returns a list of all registered frame schemas.
4159
+ *
4160
+ * Retrieves all frames that have been registered via addFrame().
4161
+ * Useful for debugging, documentation, or building dynamic UIs.
4162
+ *
4163
+ * @returns Array of frame schemas with their configurations
4164
+ *
4165
+ * @example
4166
+ * ```typescript
4167
+ * import { listFrames, addFrame } from "backtest-kit";
4168
+ *
4169
+ * addFrame({
4170
+ * frameName: "1d-backtest",
4171
+ * note: "One day backtest period for testing",
4172
+ * interval: "1m",
4173
+ * startDate: new Date("2024-01-01T00:00:00Z"),
4174
+ * endDate: new Date("2024-01-02T00:00:00Z"),
4175
+ * });
4176
+ *
4177
+ * const frames = listFrames();
4178
+ * console.log(frames);
4179
+ * // [{ frameName: "1d-backtest", note: "One day backtest...", ... }]
4180
+ * ```
4181
+ */
4182
+ async function listFrames() {
4183
+ backtest$1.loggerService.log(LIST_FRAMES_METHOD_NAME);
4184
+ return await backtest$1.frameValidationService.list();
4185
+ }
4186
+
3829
4187
  const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
3830
4188
  const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
3831
4189
  const LISTEN_SIGNAL_LIVE_METHOD_NAME = "event.listenSignalLive";
@@ -3835,6 +4193,7 @@ const LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME = "event.listenSignalBacktestOnce"
3835
4193
  const LISTEN_ERROR_METHOD_NAME = "event.listenError";
3836
4194
  const LISTEN_DONE_METHOD_NAME = "event.listenDone";
3837
4195
  const LISTEN_DONE_ONCE_METHOD_NAME = "event.listenDoneOnce";
4196
+ const LISTEN_PROGRESS_METHOD_NAME = "event.listenProgress";
3838
4197
  /**
3839
4198
  * Subscribes to all signal events with queued async processing.
3840
4199
  *
@@ -4090,6 +4449,40 @@ function listenDoneOnce(filterFn, fn) {
4090
4449
  backtest$1.loggerService.log(LISTEN_DONE_ONCE_METHOD_NAME);
4091
4450
  return doneEmitter.filter(filterFn).once(fn);
4092
4451
  }
4452
+ /**
4453
+ * Subscribes to backtest progress events with queued async processing.
4454
+ *
4455
+ * Emits during Backtest.background() execution to track progress.
4456
+ * Events are processed sequentially in order received, even if callback is async.
4457
+ * Uses queued wrapper to prevent concurrent execution of the callback.
4458
+ *
4459
+ * @param fn - Callback function to handle progress events
4460
+ * @returns Unsubscribe function to stop listening to events
4461
+ *
4462
+ * @example
4463
+ * ```typescript
4464
+ * import { listenProgress, Backtest } from "backtest-kit";
4465
+ *
4466
+ * const unsubscribe = listenProgress((event) => {
4467
+ * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
4468
+ * console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
4469
+ * console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
4470
+ * });
4471
+ *
4472
+ * Backtest.background("BTCUSDT", {
4473
+ * strategyName: "my-strategy",
4474
+ * exchangeName: "binance",
4475
+ * frameName: "1d-backtest"
4476
+ * });
4477
+ *
4478
+ * // Later: stop listening
4479
+ * unsubscribe();
4480
+ * ```
4481
+ */
4482
+ function listenProgress(fn) {
4483
+ backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
4484
+ return progressEmitter.subscribe(queued(async (event) => fn(event)));
4485
+ }
4093
4486
 
4094
4487
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
4095
4488
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -4317,6 +4710,24 @@ class BacktestUtils {
4317
4710
  isStopped = true;
4318
4711
  };
4319
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
+ };
4320
4731
  /**
4321
4732
  * Generates markdown report with all closed signals for a strategy.
4322
4733
  *
@@ -4479,6 +4890,24 @@ class LiveUtils {
4479
4890
  isStopped = true;
4480
4891
  };
4481
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
+ };
4482
4911
  /**
4483
4912
  * Generates markdown report with all events for a strategy.
4484
4913
  *
@@ -4538,4 +4967,4 @@ class LiveUtils {
4538
4967
  */
4539
4968
  const Live = new LiveUtils();
4540
4969
 
4541
- export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listenDone, listenDoneOnce, listenError, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, setLogger };
4970
+ export { Backtest, ExecutionContextService, Live, MethodContextService, PersistBase, PersistSignalAdaper, addExchange, addFrame, addStrategy, formatPrice, formatQuantity, getAveragePrice, getCandles, getDate, getMode, backtest as lib, listExchanges, listFrames, listStrategies, listenDone, listenDoneOnce, listenError, listenProgress, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, setLogger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",