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.cjs CHANGED
@@ -1129,6 +1129,11 @@ const errorEmitter = new functoolsKit.Subject();
1129
1129
  * Emits when background tasks complete (Live.background, Backtest.background).
1130
1130
  */
1131
1131
  const doneEmitter = new functoolsKit.Subject();
1132
+ /**
1133
+ * Progress emitter for backtest execution progress.
1134
+ * Emits progress updates during backtest execution.
1135
+ */
1136
+ const progressEmitter = new functoolsKit.Subject();
1132
1137
 
1133
1138
  const INTERVAL_MINUTES$1 = {
1134
1139
  "1m": 1,
@@ -2402,6 +2407,7 @@ class BacktestLogicPrivateService {
2402
2407
  this.strategyGlobalService = inject(TYPES.strategyGlobalService);
2403
2408
  this.exchangeGlobalService = inject(TYPES.exchangeGlobalService);
2404
2409
  this.frameGlobalService = inject(TYPES.frameGlobalService);
2410
+ this.methodContextService = inject(TYPES.methodContextService);
2405
2411
  }
2406
2412
  /**
2407
2413
  * Runs backtest for a symbol, streaming closed signals as async generator.
@@ -2422,9 +2428,21 @@ class BacktestLogicPrivateService {
2422
2428
  symbol,
2423
2429
  });
2424
2430
  const timeframes = await this.frameGlobalService.getTimeframe(symbol);
2431
+ const totalFrames = timeframes.length;
2425
2432
  let i = 0;
2426
2433
  while (i < timeframes.length) {
2427
2434
  const when = timeframes[i];
2435
+ // Emit progress event if context is available
2436
+ {
2437
+ await progressEmitter.next({
2438
+ exchangeName: this.methodContextService.context.exchangeName,
2439
+ strategyName: this.methodContextService.context.strategyName,
2440
+ symbol,
2441
+ totalFrames,
2442
+ processedFrames: i,
2443
+ progress: totalFrames > 0 ? i / totalFrames : 0,
2444
+ });
2445
+ }
2428
2446
  const result = await this.strategyGlobalService.tick(symbol, when, true);
2429
2447
  // Если сигнал открыт, вызываем backtest
2430
2448
  if (result.action === "opened") {
@@ -2461,6 +2479,17 @@ class BacktestLogicPrivateService {
2461
2479
  }
2462
2480
  i++;
2463
2481
  }
2482
+ // Emit final progress event (100%)
2483
+ {
2484
+ await progressEmitter.next({
2485
+ exchangeName: this.methodContextService.context.exchangeName,
2486
+ strategyName: this.methodContextService.context.strategyName,
2487
+ symbol,
2488
+ totalFrames,
2489
+ processedFrames: totalFrames,
2490
+ progress: 1.0,
2491
+ });
2492
+ }
2464
2493
  }
2465
2494
  }
2466
2495
 
@@ -2713,6 +2742,24 @@ class BacktestGlobalService {
2713
2742
  }
2714
2743
  }
2715
2744
 
2745
+ /**
2746
+ * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
2747
+ *
2748
+ * @param value - Value to check
2749
+ * @returns true if value is unsafe, false otherwise
2750
+ */
2751
+ function isUnsafe$1(value) {
2752
+ if (typeof value !== "number") {
2753
+ return true;
2754
+ }
2755
+ if (isNaN(value)) {
2756
+ return true;
2757
+ }
2758
+ if (!isFinite(value)) {
2759
+ return true;
2760
+ }
2761
+ return false;
2762
+ }
2716
2763
  const columns$1 = [
2717
2764
  {
2718
2765
  key: "signalId",
@@ -2805,13 +2852,80 @@ let ReportStorage$1 = class ReportStorage {
2805
2852
  this._signalList.push(data);
2806
2853
  }
2807
2854
  /**
2808
- * Generates markdown report with all closed signals for a strategy.
2855
+ * Calculates statistical data from closed signals (Controller).
2856
+ * Returns null for any unsafe numeric values (NaN, Infinity, etc).
2857
+ *
2858
+ * @returns Statistical data (empty object if no signals)
2859
+ */
2860
+ async getData() {
2861
+ if (this._signalList.length === 0) {
2862
+ return {
2863
+ signalList: [],
2864
+ totalSignals: 0,
2865
+ winCount: 0,
2866
+ lossCount: 0,
2867
+ winRate: null,
2868
+ avgPnl: null,
2869
+ totalPnl: null,
2870
+ stdDev: null,
2871
+ sharpeRatio: null,
2872
+ annualizedSharpeRatio: null,
2873
+ certaintyRatio: null,
2874
+ expectedYearlyReturns: null,
2875
+ };
2876
+ }
2877
+ const totalSignals = this._signalList.length;
2878
+ const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2879
+ const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2880
+ // Calculate basic statistics
2881
+ const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2882
+ const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2883
+ const winRate = (winCount / totalSignals) * 100;
2884
+ // Calculate Sharpe Ratio (risk-free rate = 0)
2885
+ const returns = this._signalList.map((s) => s.pnl.pnlPercentage);
2886
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalSignals;
2887
+ const stdDev = Math.sqrt(variance);
2888
+ const sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
2889
+ const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
2890
+ // Calculate Certainty Ratio
2891
+ const wins = this._signalList.filter((s) => s.pnl.pnlPercentage > 0);
2892
+ const losses = this._signalList.filter((s) => s.pnl.pnlPercentage < 0);
2893
+ const avgWin = wins.length > 0
2894
+ ? wins.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / wins.length
2895
+ : 0;
2896
+ const avgLoss = losses.length > 0
2897
+ ? losses.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / losses.length
2898
+ : 0;
2899
+ const certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
2900
+ // Calculate Expected Yearly Returns
2901
+ const avgDurationMs = this._signalList.reduce((sum, s) => sum + (s.closeTimestamp - s.signal.timestamp), 0) / totalSignals;
2902
+ const avgDurationDays = avgDurationMs / (1000 * 60 * 60 * 24);
2903
+ const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
2904
+ const expectedYearlyReturns = avgPnl * tradesPerYear;
2905
+ return {
2906
+ signalList: this._signalList,
2907
+ totalSignals,
2908
+ winCount,
2909
+ lossCount,
2910
+ winRate: isUnsafe$1(winRate) ? null : winRate,
2911
+ avgPnl: isUnsafe$1(avgPnl) ? null : avgPnl,
2912
+ totalPnl: isUnsafe$1(totalPnl) ? null : totalPnl,
2913
+ stdDev: isUnsafe$1(stdDev) ? null : stdDev,
2914
+ sharpeRatio: isUnsafe$1(sharpeRatio) ? null : sharpeRatio,
2915
+ annualizedSharpeRatio: isUnsafe$1(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
2916
+ certaintyRatio: isUnsafe$1(certaintyRatio) ? null : certaintyRatio,
2917
+ expectedYearlyReturns: isUnsafe$1(expectedYearlyReturns) ? null : expectedYearlyReturns,
2918
+ };
2919
+ }
2920
+ /**
2921
+ * Generates markdown report with all closed signals for a strategy (View).
2809
2922
  *
2810
2923
  * @param strategyName - Strategy name
2811
2924
  * @returns Markdown formatted report with all signals
2812
2925
  */
2813
- getReport(strategyName) {
2814
- if (this._signalList.length === 0) {
2926
+ async getReport(strategyName) {
2927
+ const stats = await this.getData();
2928
+ if (stats.totalSignals === 0) {
2815
2929
  return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
2816
2930
  }
2817
2931
  const header = columns$1.map((col) => col.label);
@@ -2819,13 +2933,7 @@ let ReportStorage$1 = class ReportStorage {
2819
2933
  const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
2820
2934
  const tableData = [header, separator, ...rows];
2821
2935
  const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
2822
- // Calculate statistics
2823
- const totalSignals = this._signalList.length;
2824
- const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
2825
- const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
2826
- const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
2827
- const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
2828
- return functoolsKit.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)}%`);
2936
+ 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)`}`);
2829
2937
  }
2830
2938
  /**
2831
2939
  * Saves strategy report to disk.
@@ -2834,7 +2942,7 @@ let ReportStorage$1 = class ReportStorage {
2834
2942
  * @param path - Directory path to save report (default: "./logs/backtest")
2835
2943
  */
2836
2944
  async dump(strategyName, path$1 = "./logs/backtest") {
2837
- const markdown = this.getReport(strategyName);
2945
+ const markdown = await this.getReport(strategyName);
2838
2946
  try {
2839
2947
  const dir = path.join(process.cwd(), path$1);
2840
2948
  await fs.mkdir(dir, { recursive: true });
@@ -2913,6 +3021,27 @@ class BacktestMarkdownService {
2913
3021
  const storage = this.getStorage(data.strategyName);
2914
3022
  storage.addSignal(data);
2915
3023
  };
3024
+ /**
3025
+ * Gets statistical data from all closed signals for a strategy.
3026
+ * Delegates to ReportStorage.getData().
3027
+ *
3028
+ * @param strategyName - Strategy name to get data for
3029
+ * @returns Statistical data object with all metrics
3030
+ *
3031
+ * @example
3032
+ * ```typescript
3033
+ * const service = new BacktestMarkdownService();
3034
+ * const stats = await service.getData("my-strategy");
3035
+ * console.log(stats.sharpeRatio, stats.winRate);
3036
+ * ```
3037
+ */
3038
+ this.getData = async (strategyName) => {
3039
+ this.loggerService.log("backtestMarkdownService getData", {
3040
+ strategyName,
3041
+ });
3042
+ const storage = this.getStorage(strategyName);
3043
+ return storage.getData();
3044
+ };
2916
3045
  /**
2917
3046
  * Generates markdown report with all closed signals for a strategy.
2918
3047
  * Delegates to ReportStorage.generateReport().
@@ -2923,7 +3052,7 @@ class BacktestMarkdownService {
2923
3052
  * @example
2924
3053
  * ```typescript
2925
3054
  * const service = new BacktestMarkdownService();
2926
- * const markdown = service.generateReport("my-strategy");
3055
+ * const markdown = await service.getReport("my-strategy");
2927
3056
  * console.log(markdown);
2928
3057
  * ```
2929
3058
  */
@@ -3003,6 +3132,24 @@ class BacktestMarkdownService {
3003
3132
  }
3004
3133
  }
3005
3134
 
3135
+ /**
3136
+ * Checks if a value is unsafe for display (not a number, NaN, or Infinity).
3137
+ *
3138
+ * @param value - Value to check
3139
+ * @returns true if value is unsafe, false otherwise
3140
+ */
3141
+ function isUnsafe(value) {
3142
+ if (typeof value !== "number") {
3143
+ return true;
3144
+ }
3145
+ if (isNaN(value)) {
3146
+ return true;
3147
+ }
3148
+ if (!isFinite(value)) {
3149
+ return true;
3150
+ }
3151
+ return false;
3152
+ }
3006
3153
  const columns = [
3007
3154
  {
3008
3155
  key: "timestamp",
@@ -3210,36 +3357,103 @@ class ReportStorage {
3210
3357
  }
3211
3358
  }
3212
3359
  /**
3213
- * Generates markdown report with all tick events for a strategy.
3360
+ * Calculates statistical data from live trading events (Controller).
3361
+ * Returns null for any unsafe numeric values (NaN, Infinity, etc).
3214
3362
  *
3215
- * @param strategyName - Strategy name
3216
- * @returns Markdown formatted report with all events
3363
+ * @returns Statistical data (empty object if no events)
3217
3364
  */
3218
- getReport(strategyName) {
3365
+ async getData() {
3219
3366
  if (this._eventList.length === 0) {
3220
- return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3367
+ return {
3368
+ eventList: [],
3369
+ totalEvents: 0,
3370
+ totalClosed: 0,
3371
+ winCount: 0,
3372
+ lossCount: 0,
3373
+ winRate: null,
3374
+ avgPnl: null,
3375
+ totalPnl: null,
3376
+ stdDev: null,
3377
+ sharpeRatio: null,
3378
+ annualizedSharpeRatio: null,
3379
+ certaintyRatio: null,
3380
+ expectedYearlyReturns: null,
3381
+ };
3221
3382
  }
3222
- const header = columns.map((col) => col.label);
3223
- const separator = columns.map(() => "---");
3224
- const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3225
- const tableData = [header, separator, ...rows];
3226
- const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3227
- // Calculate statistics
3228
3383
  const closedEvents = this._eventList.filter((e) => e.action === "closed");
3229
3384
  const totalClosed = closedEvents.length;
3230
3385
  const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
3231
3386
  const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
3387
+ // Calculate basic statistics
3232
3388
  const avgPnl = totalClosed > 0
3233
3389
  ? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
3234
3390
  : 0;
3235
3391
  const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
3236
- return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", table, "", `**Total events:** ${this._eventList.length}`, `**Closed signals:** ${totalClosed}`, totalClosed > 0
3237
- ? `**Win rate:** ${((winCount / totalClosed) * 100).toFixed(2)}% (${winCount}W / ${lossCount}L)`
3238
- : "", totalClosed > 0
3239
- ? `**Average PNL:** ${avgPnl > 0 ? "+" : ""}${avgPnl.toFixed(2)}%`
3240
- : "", totalClosed > 0
3241
- ? `**Total PNL:** ${totalPnl > 0 ? "+" : ""}${totalPnl.toFixed(2)}%`
3242
- : "");
3392
+ const winRate = (winCount / totalClosed) * 100;
3393
+ // Calculate Sharpe Ratio (risk-free rate = 0)
3394
+ let sharpeRatio = 0;
3395
+ let stdDev = 0;
3396
+ if (totalClosed > 0) {
3397
+ const returns = closedEvents.map((e) => e.pnl || 0);
3398
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / totalClosed;
3399
+ stdDev = Math.sqrt(variance);
3400
+ sharpeRatio = stdDev > 0 ? avgPnl / stdDev : 0;
3401
+ }
3402
+ const annualizedSharpeRatio = sharpeRatio * Math.sqrt(365);
3403
+ // Calculate Certainty Ratio
3404
+ let certaintyRatio = 0;
3405
+ if (totalClosed > 0) {
3406
+ const wins = closedEvents.filter((e) => e.pnl && e.pnl > 0);
3407
+ const losses = closedEvents.filter((e) => e.pnl && e.pnl < 0);
3408
+ const avgWin = wins.length > 0
3409
+ ? wins.reduce((sum, e) => sum + (e.pnl || 0), 0) / wins.length
3410
+ : 0;
3411
+ const avgLoss = losses.length > 0
3412
+ ? losses.reduce((sum, e) => sum + (e.pnl || 0), 0) / losses.length
3413
+ : 0;
3414
+ certaintyRatio = avgLoss < 0 ? avgWin / Math.abs(avgLoss) : 0;
3415
+ }
3416
+ // Calculate Expected Yearly Returns
3417
+ let expectedYearlyReturns = 0;
3418
+ if (totalClosed > 0) {
3419
+ const avgDurationMin = closedEvents.reduce((sum, e) => sum + (e.duration || 0), 0) / totalClosed;
3420
+ const avgDurationDays = avgDurationMin / (60 * 24);
3421
+ const tradesPerYear = avgDurationDays > 0 ? 365 / avgDurationDays : 0;
3422
+ expectedYearlyReturns = avgPnl * tradesPerYear;
3423
+ }
3424
+ return {
3425
+ eventList: this._eventList,
3426
+ totalEvents: this._eventList.length,
3427
+ totalClosed,
3428
+ winCount,
3429
+ lossCount,
3430
+ winRate: isUnsafe(winRate) ? null : winRate,
3431
+ avgPnl: isUnsafe(avgPnl) ? null : avgPnl,
3432
+ totalPnl: isUnsafe(totalPnl) ? null : totalPnl,
3433
+ stdDev: isUnsafe(stdDev) ? null : stdDev,
3434
+ sharpeRatio: isUnsafe(sharpeRatio) ? null : sharpeRatio,
3435
+ annualizedSharpeRatio: isUnsafe(annualizedSharpeRatio) ? null : annualizedSharpeRatio,
3436
+ certaintyRatio: isUnsafe(certaintyRatio) ? null : certaintyRatio,
3437
+ expectedYearlyReturns: isUnsafe(expectedYearlyReturns) ? null : expectedYearlyReturns,
3438
+ };
3439
+ }
3440
+ /**
3441
+ * Generates markdown report with all tick events for a strategy (View).
3442
+ *
3443
+ * @param strategyName - Strategy name
3444
+ * @returns Markdown formatted report with all events
3445
+ */
3446
+ async getReport(strategyName) {
3447
+ const stats = await this.getData();
3448
+ if (stats.totalEvents === 0) {
3449
+ return functoolsKit.str.newline(`# Live Trading Report: ${strategyName}`, "", "No events recorded yet.");
3450
+ }
3451
+ const header = columns.map((col) => col.label);
3452
+ const separator = columns.map(() => "---");
3453
+ const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
3454
+ const tableData = [header, separator, ...rows];
3455
+ const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
3456
+ 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)`}`);
3243
3457
  }
3244
3458
  /**
3245
3459
  * Saves strategy report to disk.
@@ -3248,7 +3462,7 @@ class ReportStorage {
3248
3462
  * @param path - Directory path to save report (default: "./logs/live")
3249
3463
  */
3250
3464
  async dump(strategyName, path$1 = "./logs/live") {
3251
- const markdown = this.getReport(strategyName);
3465
+ const markdown = await this.getReport(strategyName);
3252
3466
  try {
3253
3467
  const dir = path.join(process.cwd(), path$1);
3254
3468
  await fs.mkdir(dir, { recursive: true });
@@ -3340,6 +3554,27 @@ class LiveMarkdownService {
3340
3554
  storage.addClosedEvent(data);
3341
3555
  }
3342
3556
  };
3557
+ /**
3558
+ * Gets statistical data from all live trading events for a strategy.
3559
+ * Delegates to ReportStorage.getData().
3560
+ *
3561
+ * @param strategyName - Strategy name to get data for
3562
+ * @returns Statistical data object with all metrics
3563
+ *
3564
+ * @example
3565
+ * ```typescript
3566
+ * const service = new LiveMarkdownService();
3567
+ * const stats = await service.getData("my-strategy");
3568
+ * console.log(stats.sharpeRatio, stats.winRate);
3569
+ * ```
3570
+ */
3571
+ this.getData = async (strategyName) => {
3572
+ this.loggerService.log("liveMarkdownService getData", {
3573
+ strategyName,
3574
+ });
3575
+ const storage = this.getStorage(strategyName);
3576
+ return storage.getData();
3577
+ };
3343
3578
  /**
3344
3579
  * Generates markdown report with all events for a strategy.
3345
3580
  * Delegates to ReportStorage.getReport().
@@ -3479,6 +3714,15 @@ class ExchangeValidationService {
3479
3714
  }
3480
3715
  return true;
3481
3716
  });
3717
+ /**
3718
+ * Returns a list of all registered exchange schemas
3719
+ * @public
3720
+ * @returns Array of exchange schemas with their configurations
3721
+ */
3722
+ this.list = async () => {
3723
+ this.loggerService.log("exchangeValidationService list");
3724
+ return Array.from(this._exchangeMap.values());
3725
+ };
3482
3726
  }
3483
3727
  }
3484
3728
 
@@ -3531,6 +3775,15 @@ class StrategyValidationService {
3531
3775
  }
3532
3776
  return true;
3533
3777
  });
3778
+ /**
3779
+ * Returns a list of all registered strategy schemas
3780
+ * @public
3781
+ * @returns Array of strategy schemas with their configurations
3782
+ */
3783
+ this.list = async () => {
3784
+ this.loggerService.log("strategyValidationService list");
3785
+ return Array.from(this._strategyMap.values());
3786
+ };
3534
3787
  }
3535
3788
  }
3536
3789
 
@@ -3583,6 +3836,15 @@ class FrameValidationService {
3583
3836
  }
3584
3837
  return true;
3585
3838
  });
3839
+ /**
3840
+ * Returns a list of all registered frame schemas
3841
+ * @public
3842
+ * @returns Array of frame schemas with their configurations
3843
+ */
3844
+ this.list = async () => {
3845
+ this.loggerService.log("frameValidationService list");
3846
+ return Array.from(this._frameMap.values());
3847
+ };
3586
3848
  }
3587
3849
  }
3588
3850
 
@@ -3828,6 +4090,102 @@ function addFrame(frameSchema) {
3828
4090
  backtest$1.frameSchemaService.register(frameSchema.frameName, frameSchema);
3829
4091
  }
3830
4092
 
4093
+ const LIST_EXCHANGES_METHOD_NAME = "list.listExchanges";
4094
+ const LIST_STRATEGIES_METHOD_NAME = "list.listStrategies";
4095
+ const LIST_FRAMES_METHOD_NAME = "list.listFrames";
4096
+ /**
4097
+ * Returns a list of all registered exchange schemas.
4098
+ *
4099
+ * Retrieves all exchanges that have been registered via addExchange().
4100
+ * Useful for debugging, documentation, or building dynamic UIs.
4101
+ *
4102
+ * @returns Array of exchange schemas with their configurations
4103
+ *
4104
+ * @example
4105
+ * ```typescript
4106
+ * import { listExchanges, addExchange } from "backtest-kit";
4107
+ *
4108
+ * addExchange({
4109
+ * exchangeName: "binance",
4110
+ * note: "Binance cryptocurrency exchange",
4111
+ * getCandles: async (symbol, interval, since, limit) => [...],
4112
+ * formatPrice: async (symbol, price) => price.toFixed(2),
4113
+ * formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
4114
+ * });
4115
+ *
4116
+ * const exchanges = listExchanges();
4117
+ * console.log(exchanges);
4118
+ * // [{ exchangeName: "binance", note: "Binance cryptocurrency exchange", ... }]
4119
+ * ```
4120
+ */
4121
+ async function listExchanges() {
4122
+ backtest$1.loggerService.log(LIST_EXCHANGES_METHOD_NAME);
4123
+ return await backtest$1.exchangeValidationService.list();
4124
+ }
4125
+ /**
4126
+ * Returns a list of all registered strategy schemas.
4127
+ *
4128
+ * Retrieves all strategies that have been registered via addStrategy().
4129
+ * Useful for debugging, documentation, or building dynamic UIs.
4130
+ *
4131
+ * @returns Array of strategy schemas with their configurations
4132
+ *
4133
+ * @example
4134
+ * ```typescript
4135
+ * import { listStrategies, addStrategy } from "backtest-kit";
4136
+ *
4137
+ * addStrategy({
4138
+ * strategyName: "my-strategy",
4139
+ * note: "Simple moving average crossover strategy",
4140
+ * interval: "5m",
4141
+ * getSignal: async (symbol) => ({
4142
+ * position: "long",
4143
+ * priceOpen: 50000,
4144
+ * priceTakeProfit: 51000,
4145
+ * priceStopLoss: 49000,
4146
+ * minuteEstimatedTime: 60,
4147
+ * }),
4148
+ * });
4149
+ *
4150
+ * const strategies = listStrategies();
4151
+ * console.log(strategies);
4152
+ * // [{ strategyName: "my-strategy", note: "Simple moving average...", ... }]
4153
+ * ```
4154
+ */
4155
+ async function listStrategies() {
4156
+ backtest$1.loggerService.log(LIST_STRATEGIES_METHOD_NAME);
4157
+ return await backtest$1.strategyValidationService.list();
4158
+ }
4159
+ /**
4160
+ * Returns a list of all registered frame schemas.
4161
+ *
4162
+ * Retrieves all frames that have been registered via addFrame().
4163
+ * Useful for debugging, documentation, or building dynamic UIs.
4164
+ *
4165
+ * @returns Array of frame schemas with their configurations
4166
+ *
4167
+ * @example
4168
+ * ```typescript
4169
+ * import { listFrames, addFrame } from "backtest-kit";
4170
+ *
4171
+ * addFrame({
4172
+ * frameName: "1d-backtest",
4173
+ * note: "One day backtest period for testing",
4174
+ * interval: "1m",
4175
+ * startDate: new Date("2024-01-01T00:00:00Z"),
4176
+ * endDate: new Date("2024-01-02T00:00:00Z"),
4177
+ * });
4178
+ *
4179
+ * const frames = listFrames();
4180
+ * console.log(frames);
4181
+ * // [{ frameName: "1d-backtest", note: "One day backtest...", ... }]
4182
+ * ```
4183
+ */
4184
+ async function listFrames() {
4185
+ backtest$1.loggerService.log(LIST_FRAMES_METHOD_NAME);
4186
+ return await backtest$1.frameValidationService.list();
4187
+ }
4188
+
3831
4189
  const LISTEN_SIGNAL_METHOD_NAME = "event.listenSignal";
3832
4190
  const LISTEN_SIGNAL_ONCE_METHOD_NAME = "event.listenSignalOnce";
3833
4191
  const LISTEN_SIGNAL_LIVE_METHOD_NAME = "event.listenSignalLive";
@@ -3837,6 +4195,7 @@ const LISTEN_SIGNAL_BACKTEST_ONCE_METHOD_NAME = "event.listenSignalBacktestOnce"
3837
4195
  const LISTEN_ERROR_METHOD_NAME = "event.listenError";
3838
4196
  const LISTEN_DONE_METHOD_NAME = "event.listenDone";
3839
4197
  const LISTEN_DONE_ONCE_METHOD_NAME = "event.listenDoneOnce";
4198
+ const LISTEN_PROGRESS_METHOD_NAME = "event.listenProgress";
3840
4199
  /**
3841
4200
  * Subscribes to all signal events with queued async processing.
3842
4201
  *
@@ -4092,6 +4451,40 @@ function listenDoneOnce(filterFn, fn) {
4092
4451
  backtest$1.loggerService.log(LISTEN_DONE_ONCE_METHOD_NAME);
4093
4452
  return doneEmitter.filter(filterFn).once(fn);
4094
4453
  }
4454
+ /**
4455
+ * Subscribes to backtest progress events with queued async processing.
4456
+ *
4457
+ * Emits during Backtest.background() execution to track progress.
4458
+ * Events are processed sequentially in order received, even if callback is async.
4459
+ * Uses queued wrapper to prevent concurrent execution of the callback.
4460
+ *
4461
+ * @param fn - Callback function to handle progress events
4462
+ * @returns Unsubscribe function to stop listening to events
4463
+ *
4464
+ * @example
4465
+ * ```typescript
4466
+ * import { listenProgress, Backtest } from "backtest-kit";
4467
+ *
4468
+ * const unsubscribe = listenProgress((event) => {
4469
+ * console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
4470
+ * console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
4471
+ * console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
4472
+ * });
4473
+ *
4474
+ * Backtest.background("BTCUSDT", {
4475
+ * strategyName: "my-strategy",
4476
+ * exchangeName: "binance",
4477
+ * frameName: "1d-backtest"
4478
+ * });
4479
+ *
4480
+ * // Later: stop listening
4481
+ * unsubscribe();
4482
+ * ```
4483
+ */
4484
+ function listenProgress(fn) {
4485
+ backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
4486
+ return progressEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
4487
+ }
4095
4488
 
4096
4489
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
4097
4490
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -4319,6 +4712,24 @@ class BacktestUtils {
4319
4712
  isStopped = true;
4320
4713
  };
4321
4714
  };
4715
+ /**
4716
+ * Gets statistical data from all closed signals for a strategy.
4717
+ *
4718
+ * @param strategyName - Strategy name to get data for
4719
+ * @returns Promise resolving to statistical data object
4720
+ *
4721
+ * @example
4722
+ * ```typescript
4723
+ * const stats = await Backtest.getData("my-strategy");
4724
+ * console.log(stats.sharpeRatio, stats.winRate);
4725
+ * ```
4726
+ */
4727
+ this.getData = async (strategyName) => {
4728
+ backtest$1.loggerService.info("BacktestUtils.getData", {
4729
+ strategyName,
4730
+ });
4731
+ return await backtest$1.backtestMarkdownService.getData(strategyName);
4732
+ };
4322
4733
  /**
4323
4734
  * Generates markdown report with all closed signals for a strategy.
4324
4735
  *
@@ -4481,6 +4892,24 @@ class LiveUtils {
4481
4892
  isStopped = true;
4482
4893
  };
4483
4894
  };
4895
+ /**
4896
+ * Gets statistical data from all live trading events for a strategy.
4897
+ *
4898
+ * @param strategyName - Strategy name to get data for
4899
+ * @returns Promise resolving to statistical data object
4900
+ *
4901
+ * @example
4902
+ * ```typescript
4903
+ * const stats = await Live.getData("my-strategy");
4904
+ * console.log(stats.sharpeRatio, stats.winRate);
4905
+ * ```
4906
+ */
4907
+ this.getData = async (strategyName) => {
4908
+ backtest$1.loggerService.info("LiveUtils.getData", {
4909
+ strategyName,
4910
+ });
4911
+ return await backtest$1.liveMarkdownService.getData(strategyName);
4912
+ };
4484
4913
  /**
4485
4914
  * Generates markdown report with all events for a strategy.
4486
4915
  *
@@ -4556,9 +4985,13 @@ exports.getCandles = getCandles;
4556
4985
  exports.getDate = getDate;
4557
4986
  exports.getMode = getMode;
4558
4987
  exports.lib = backtest;
4988
+ exports.listExchanges = listExchanges;
4989
+ exports.listFrames = listFrames;
4990
+ exports.listStrategies = listStrategies;
4559
4991
  exports.listenDone = listenDone;
4560
4992
  exports.listenDoneOnce = listenDoneOnce;
4561
4993
  exports.listenError = listenError;
4994
+ exports.listenProgress = listenProgress;
4562
4995
  exports.listenSignal = listenSignal;
4563
4996
  exports.listenSignalBacktest = listenSignalBacktest;
4564
4997
  exports.listenSignalBacktestOnce = listenSignalBacktestOnce;