backtest-kit 1.1.6 → 1.1.8

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
@@ -71,6 +71,7 @@ const logicPublicServices$1 = {
71
71
  const markdownServices$1 = {
72
72
  backtestMarkdownService: Symbol('backtestMarkdownService'),
73
73
  liveMarkdownService: Symbol('liveMarkdownService'),
74
+ performanceMarkdownService: Symbol('performanceMarkdownService'),
74
75
  };
75
76
  const validationServices$1 = {
76
77
  exchangeValidationService: Symbol('exchangeValidationService'),
@@ -1043,8 +1044,7 @@ class PersistSignalUtils {
1043
1044
  const stateStorage = this.getSignalStorage(strategyName);
1044
1045
  await stateStorage.waitForInit(isInitial);
1045
1046
  if (await stateStorage.hasValue(symbol)) {
1046
- const { signalRow } = await stateStorage.readValue(symbol);
1047
- return signalRow;
1047
+ return await stateStorage.readValue(symbol);
1048
1048
  }
1049
1049
  return null;
1050
1050
  };
@@ -1064,7 +1064,7 @@ class PersistSignalUtils {
1064
1064
  const isInitial = !this.getSignalStorage.has(strategyName);
1065
1065
  const stateStorage = this.getSignalStorage(strategyName);
1066
1066
  await stateStorage.waitForInit(isInitial);
1067
- await stateStorage.writeValue(symbol, { signalRow });
1067
+ await stateStorage.writeValue(symbol, signalRow);
1068
1068
  };
1069
1069
  }
1070
1070
  /**
@@ -1134,6 +1134,11 @@ const doneEmitter = new functoolsKit.Subject();
1134
1134
  * Emits progress updates during backtest execution.
1135
1135
  */
1136
1136
  const progressEmitter = new functoolsKit.Subject();
1137
+ /**
1138
+ * Performance emitter for execution metrics.
1139
+ * Emits performance metrics for profiling and bottleneck detection.
1140
+ */
1141
+ const performanceEmitter = new functoolsKit.Subject();
1137
1142
 
1138
1143
  const INTERVAL_MINUTES$1 = {
1139
1144
  "1m": 1,
@@ -1206,6 +1211,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
1206
1211
  }
1207
1212
  const signalRow = {
1208
1213
  id: functoolsKit.randomString(),
1214
+ priceOpen: await self.params.exchange.getAveragePrice(self.params.execution.context.symbol),
1209
1215
  ...signal,
1210
1216
  symbol: self.params.execution.context.symbol,
1211
1217
  exchangeName: self.params.method.context.exchangeName,
@@ -2427,10 +2433,12 @@ class BacktestLogicPrivateService {
2427
2433
  this.loggerService.log("backtestLogicPrivateService run", {
2428
2434
  symbol,
2429
2435
  });
2436
+ const backtestStartTime = performance.now();
2430
2437
  const timeframes = await this.frameGlobalService.getTimeframe(symbol);
2431
2438
  const totalFrames = timeframes.length;
2432
2439
  let i = 0;
2433
2440
  while (i < timeframes.length) {
2441
+ const timeframeStartTime = performance.now();
2434
2442
  const when = timeframes[i];
2435
2443
  // Emit progress event if context is available
2436
2444
  {
@@ -2446,6 +2454,7 @@ class BacktestLogicPrivateService {
2446
2454
  const result = await this.strategyGlobalService.tick(symbol, when, true);
2447
2455
  // Если сигнал открыт, вызываем backtest
2448
2456
  if (result.action === "opened") {
2457
+ const signalStartTime = performance.now();
2449
2458
  const signal = result.signal;
2450
2459
  this.loggerService.info("backtestLogicPrivateService signal opened", {
2451
2460
  symbol,
@@ -2470,6 +2479,17 @@ class BacktestLogicPrivateService {
2470
2479
  closeTimestamp: backtestResult.closeTimestamp,
2471
2480
  closeReason: backtestResult.closeReason,
2472
2481
  });
2482
+ // Track signal processing duration
2483
+ const signalEndTime = performance.now();
2484
+ await performanceEmitter.next({
2485
+ timestamp: Date.now(),
2486
+ metricType: "backtest_signal",
2487
+ duration: signalEndTime - signalStartTime,
2488
+ strategyName: this.methodContextService.context.strategyName,
2489
+ exchangeName: this.methodContextService.context.exchangeName,
2490
+ symbol,
2491
+ backtest: true,
2492
+ });
2473
2493
  // Пропускаем timeframes до closeTimestamp
2474
2494
  while (i < timeframes.length &&
2475
2495
  timeframes[i].getTime() < backtestResult.closeTimestamp) {
@@ -2477,6 +2497,17 @@ class BacktestLogicPrivateService {
2477
2497
  }
2478
2498
  yield backtestResult;
2479
2499
  }
2500
+ // Track timeframe processing duration
2501
+ const timeframeEndTime = performance.now();
2502
+ await performanceEmitter.next({
2503
+ timestamp: Date.now(),
2504
+ metricType: "backtest_timeframe",
2505
+ duration: timeframeEndTime - timeframeStartTime,
2506
+ strategyName: this.methodContextService.context.strategyName,
2507
+ exchangeName: this.methodContextService.context.exchangeName,
2508
+ symbol,
2509
+ backtest: true,
2510
+ });
2480
2511
  i++;
2481
2512
  }
2482
2513
  // Emit final progress event (100%)
@@ -2490,6 +2521,17 @@ class BacktestLogicPrivateService {
2490
2521
  progress: 1.0,
2491
2522
  });
2492
2523
  }
2524
+ // Track total backtest duration
2525
+ const backtestEndTime = performance.now();
2526
+ await performanceEmitter.next({
2527
+ timestamp: Date.now(),
2528
+ metricType: "backtest_total",
2529
+ duration: backtestEndTime - backtestStartTime,
2530
+ strategyName: this.methodContextService.context.strategyName,
2531
+ exchangeName: this.methodContextService.context.exchangeName,
2532
+ symbol,
2533
+ backtest: true,
2534
+ });
2493
2535
  }
2494
2536
  }
2495
2537
 
@@ -2514,6 +2556,7 @@ class LiveLogicPrivateService {
2514
2556
  constructor() {
2515
2557
  this.loggerService = inject(TYPES.loggerService);
2516
2558
  this.strategyGlobalService = inject(TYPES.strategyGlobalService);
2559
+ this.methodContextService = inject(TYPES.methodContextService);
2517
2560
  }
2518
2561
  /**
2519
2562
  * Runs live trading for a symbol, streaming results as async generator.
@@ -2542,12 +2585,24 @@ class LiveLogicPrivateService {
2542
2585
  symbol,
2543
2586
  });
2544
2587
  while (true) {
2588
+ const tickStartTime = performance.now();
2545
2589
  const when = new Date();
2546
2590
  const result = await this.strategyGlobalService.tick(symbol, when, false);
2547
2591
  this.loggerService.info("liveLogicPrivateService tick result", {
2548
2592
  symbol,
2549
2593
  action: result.action,
2550
2594
  });
2595
+ // Track tick duration
2596
+ const tickEndTime = performance.now();
2597
+ await performanceEmitter.next({
2598
+ timestamp: Date.now(),
2599
+ metricType: "live_tick",
2600
+ duration: tickEndTime - tickStartTime,
2601
+ strategyName: this.methodContextService.context.strategyName,
2602
+ exchangeName: this.methodContextService.context.exchangeName,
2603
+ symbol,
2604
+ backtest: false,
2605
+ });
2551
2606
  if (result.action === "active") {
2552
2607
  await functoolsKit.sleep(TICK_TTL);
2553
2608
  continue;
@@ -3224,7 +3279,7 @@ const columns = [
3224
3279
  },
3225
3280
  ];
3226
3281
  /** Maximum number of events to store in live trading reports */
3227
- const MAX_EVENTS = 250;
3282
+ const MAX_EVENTS$1 = 250;
3228
3283
  /**
3229
3284
  * Storage class for accumulating all tick events per strategy.
3230
3285
  * Maintains a chronological list of all events (idle, opened, active, closed).
@@ -3257,7 +3312,7 @@ class ReportStorage {
3257
3312
  }
3258
3313
  {
3259
3314
  this._eventList.push(newEvent);
3260
- if (this._eventList.length > MAX_EVENTS) {
3315
+ if (this._eventList.length > MAX_EVENTS$1) {
3261
3316
  this._eventList.shift();
3262
3317
  }
3263
3318
  }
@@ -3281,7 +3336,7 @@ class ReportStorage {
3281
3336
  stopLoss: data.signal.priceStopLoss,
3282
3337
  });
3283
3338
  // Trim queue if exceeded MAX_EVENTS
3284
- if (this._eventList.length > MAX_EVENTS) {
3339
+ if (this._eventList.length > MAX_EVENTS$1) {
3285
3340
  this._eventList.shift();
3286
3341
  }
3287
3342
  }
@@ -3313,7 +3368,7 @@ class ReportStorage {
3313
3368
  else {
3314
3369
  this._eventList.push(newEvent);
3315
3370
  // Trim queue if exceeded MAX_EVENTS
3316
- if (this._eventList.length > MAX_EVENTS) {
3371
+ if (this._eventList.length > MAX_EVENTS$1) {
3317
3372
  this._eventList.shift();
3318
3373
  }
3319
3374
  }
@@ -3351,7 +3406,7 @@ class ReportStorage {
3351
3406
  else {
3352
3407
  this._eventList.push(newEvent);
3353
3408
  // Trim queue if exceeded MAX_EVENTS
3354
- if (this._eventList.length > MAX_EVENTS) {
3409
+ if (this._eventList.length > MAX_EVENTS$1) {
3355
3410
  this._eventList.shift();
3356
3411
  }
3357
3412
  }
@@ -3665,6 +3720,297 @@ class LiveMarkdownService {
3665
3720
  }
3666
3721
  }
3667
3722
 
3723
+ /**
3724
+ * Calculates percentile value from sorted array.
3725
+ */
3726
+ function percentile(sortedArray, p) {
3727
+ if (sortedArray.length === 0)
3728
+ return 0;
3729
+ const index = Math.ceil((sortedArray.length * p) / 100) - 1;
3730
+ return sortedArray[Math.max(0, index)];
3731
+ }
3732
+ /** Maximum number of performance events to store per strategy */
3733
+ const MAX_EVENTS = 10000;
3734
+ /**
3735
+ * Storage class for accumulating performance metrics per strategy.
3736
+ * Maintains a list of all performance events and provides aggregated statistics.
3737
+ */
3738
+ class PerformanceStorage {
3739
+ constructor() {
3740
+ /** Internal list of all performance events for this strategy */
3741
+ this._events = [];
3742
+ }
3743
+ /**
3744
+ * Adds a performance event to the storage.
3745
+ *
3746
+ * @param event - Performance event with timing data
3747
+ */
3748
+ addEvent(event) {
3749
+ this._events.push(event);
3750
+ // Trim queue if exceeded MAX_EVENTS (keep most recent)
3751
+ if (this._events.length > MAX_EVENTS) {
3752
+ this._events.shift();
3753
+ }
3754
+ }
3755
+ /**
3756
+ * Calculates aggregated statistics from all performance events.
3757
+ *
3758
+ * @returns Performance statistics with metrics grouped by type
3759
+ */
3760
+ async getData(strategyName) {
3761
+ if (this._events.length === 0) {
3762
+ return {
3763
+ strategyName,
3764
+ totalEvents: 0,
3765
+ totalDuration: 0,
3766
+ metricStats: {},
3767
+ events: [],
3768
+ };
3769
+ }
3770
+ // Group events by metric type
3771
+ const eventsByType = new Map();
3772
+ for (const event of this._events) {
3773
+ if (!eventsByType.has(event.metricType)) {
3774
+ eventsByType.set(event.metricType, []);
3775
+ }
3776
+ eventsByType.get(event.metricType).push(event);
3777
+ }
3778
+ // Calculate statistics for each metric type
3779
+ const metricStats = {};
3780
+ for (const [metricType, events] of eventsByType.entries()) {
3781
+ const durations = events.map((e) => e.duration).sort((a, b) => a - b);
3782
+ const totalDuration = durations.reduce((sum, d) => sum + d, 0);
3783
+ const avgDuration = totalDuration / durations.length;
3784
+ // Calculate standard deviation
3785
+ const variance = durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) /
3786
+ durations.length;
3787
+ const stdDev = Math.sqrt(variance);
3788
+ metricStats[metricType] = {
3789
+ metricType,
3790
+ count: events.length,
3791
+ totalDuration,
3792
+ avgDuration,
3793
+ minDuration: durations[0],
3794
+ maxDuration: durations[durations.length - 1],
3795
+ stdDev,
3796
+ median: percentile(durations, 50),
3797
+ p95: percentile(durations, 95),
3798
+ p99: percentile(durations, 99),
3799
+ };
3800
+ }
3801
+ const totalDuration = this._events.reduce((sum, e) => sum + e.duration, 0);
3802
+ return {
3803
+ strategyName,
3804
+ totalEvents: this._events.length,
3805
+ totalDuration,
3806
+ metricStats,
3807
+ events: this._events,
3808
+ };
3809
+ }
3810
+ /**
3811
+ * Generates markdown report with performance statistics.
3812
+ *
3813
+ * @param strategyName - Strategy name
3814
+ * @returns Markdown formatted report
3815
+ */
3816
+ async getReport(strategyName) {
3817
+ const stats = await this.getData(strategyName);
3818
+ if (stats.totalEvents === 0) {
3819
+ return functoolsKit.str.newline(`# Performance Report: ${strategyName}`, "", "No performance metrics recorded yet.");
3820
+ }
3821
+ // Sort metrics by total duration (descending) to show bottlenecks first
3822
+ const sortedMetrics = Object.values(stats.metricStats).sort((a, b) => b.totalDuration - a.totalDuration);
3823
+ // Generate summary table
3824
+ const summaryHeader = [
3825
+ "Metric Type",
3826
+ "Count",
3827
+ "Total (ms)",
3828
+ "Avg (ms)",
3829
+ "Min (ms)",
3830
+ "Max (ms)",
3831
+ "Std Dev (ms)",
3832
+ "Median (ms)",
3833
+ "P95 (ms)",
3834
+ "P99 (ms)",
3835
+ ];
3836
+ const summarySeparator = summaryHeader.map(() => "---");
3837
+ const summaryRows = sortedMetrics.map((metric) => [
3838
+ metric.metricType,
3839
+ metric.count.toString(),
3840
+ metric.totalDuration.toFixed(2),
3841
+ metric.avgDuration.toFixed(2),
3842
+ metric.minDuration.toFixed(2),
3843
+ metric.maxDuration.toFixed(2),
3844
+ metric.stdDev.toFixed(2),
3845
+ metric.median.toFixed(2),
3846
+ metric.p95.toFixed(2),
3847
+ metric.p99.toFixed(2),
3848
+ ]);
3849
+ const summaryTableData = [summaryHeader, summarySeparator, ...summaryRows];
3850
+ const summaryTable = functoolsKit.str.newline(summaryTableData.map((row) => `| ${row.join(" | ")} |`));
3851
+ // Calculate percentage of total time for each metric
3852
+ const percentages = sortedMetrics.map((metric) => {
3853
+ const pct = (metric.totalDuration / stats.totalDuration) * 100;
3854
+ return `- **${metric.metricType}**: ${pct.toFixed(1)}% (${metric.totalDuration.toFixed(2)}ms total)`;
3855
+ });
3856
+ return functoolsKit.str.newline(`# Performance Report: ${strategyName}`, "", `**Total events:** ${stats.totalEvents}`, `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`, `**Number of metric types:** ${Object.keys(stats.metricStats).length}`, "", "## Time Distribution", "", functoolsKit.str.newline(percentages), "", "## Detailed Metrics", "", summaryTable, "", "**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times.");
3857
+ }
3858
+ /**
3859
+ * Saves performance report to disk.
3860
+ *
3861
+ * @param strategyName - Strategy name
3862
+ * @param path - Directory path to save report
3863
+ */
3864
+ async dump(strategyName, path$1 = "./logs/performance") {
3865
+ const markdown = await this.getReport(strategyName);
3866
+ try {
3867
+ const dir = path.join(process.cwd(), path$1);
3868
+ await fs.mkdir(dir, { recursive: true });
3869
+ const filename = `${strategyName}.md`;
3870
+ const filepath = path.join(dir, filename);
3871
+ await fs.writeFile(filepath, markdown, "utf-8");
3872
+ console.log(`Performance report saved: ${filepath}`);
3873
+ }
3874
+ catch (error) {
3875
+ console.error(`Failed to save performance report:`, error);
3876
+ }
3877
+ }
3878
+ }
3879
+ /**
3880
+ * Service for collecting and analyzing performance metrics.
3881
+ *
3882
+ * Features:
3883
+ * - Listens to performance events via performanceEmitter
3884
+ * - Accumulates metrics per strategy
3885
+ * - Calculates aggregated statistics (avg, min, max, percentiles)
3886
+ * - Generates markdown reports with bottleneck analysis
3887
+ * - Saves reports to disk in logs/performance/{strategyName}.md
3888
+ *
3889
+ * @example
3890
+ * ```typescript
3891
+ * import { listenPerformance } from "backtest-kit";
3892
+ *
3893
+ * // Subscribe to performance events
3894
+ * listenPerformance((event) => {
3895
+ * console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
3896
+ * });
3897
+ *
3898
+ * // After execution, generate report
3899
+ * const stats = await Performance.getData("my-strategy");
3900
+ * console.log("Bottlenecks:", stats.metricStats);
3901
+ *
3902
+ * // Save report to disk
3903
+ * await Performance.dump("my-strategy");
3904
+ * ```
3905
+ */
3906
+ class PerformanceMarkdownService {
3907
+ constructor() {
3908
+ /** Logger service for debug output */
3909
+ this.loggerService = inject(TYPES.loggerService);
3910
+ /**
3911
+ * Memoized function to get or create PerformanceStorage for a strategy.
3912
+ * Each strategy gets its own isolated storage instance.
3913
+ */
3914
+ this.getStorage = functoolsKit.memoize(([strategyName]) => `${strategyName}`, () => new PerformanceStorage());
3915
+ /**
3916
+ * Processes performance events and accumulates metrics.
3917
+ * Should be called from performance tracking code.
3918
+ *
3919
+ * @param event - Performance event with timing data
3920
+ */
3921
+ this.track = async (event) => {
3922
+ this.loggerService.log("performanceMarkdownService track", {
3923
+ event,
3924
+ });
3925
+ const strategyName = event.strategyName || "global";
3926
+ const storage = this.getStorage(strategyName);
3927
+ storage.addEvent(event);
3928
+ };
3929
+ /**
3930
+ * Gets aggregated performance statistics for a strategy.
3931
+ *
3932
+ * @param strategyName - Strategy name to get data for
3933
+ * @returns Performance statistics with aggregated metrics
3934
+ *
3935
+ * @example
3936
+ * ```typescript
3937
+ * const stats = await performanceService.getData("my-strategy");
3938
+ * console.log("Total time:", stats.totalDuration);
3939
+ * console.log("Slowest operation:", Object.values(stats.metricStats)
3940
+ * .sort((a, b) => b.avgDuration - a.avgDuration)[0]);
3941
+ * ```
3942
+ */
3943
+ this.getData = async (strategyName) => {
3944
+ this.loggerService.log("performanceMarkdownService getData", {
3945
+ strategyName,
3946
+ });
3947
+ const storage = this.getStorage(strategyName);
3948
+ return storage.getData(strategyName);
3949
+ };
3950
+ /**
3951
+ * Generates markdown report with performance analysis.
3952
+ *
3953
+ * @param strategyName - Strategy name to generate report for
3954
+ * @returns Markdown formatted report string
3955
+ *
3956
+ * @example
3957
+ * ```typescript
3958
+ * const markdown = await performanceService.getReport("my-strategy");
3959
+ * console.log(markdown);
3960
+ * ```
3961
+ */
3962
+ this.getReport = async (strategyName) => {
3963
+ this.loggerService.log("performanceMarkdownService getReport", {
3964
+ strategyName,
3965
+ });
3966
+ const storage = this.getStorage(strategyName);
3967
+ return storage.getReport(strategyName);
3968
+ };
3969
+ /**
3970
+ * Saves performance report to disk.
3971
+ *
3972
+ * @param strategyName - Strategy name to save report for
3973
+ * @param path - Directory path to save report
3974
+ *
3975
+ * @example
3976
+ * ```typescript
3977
+ * // Save to default path: ./logs/performance/my-strategy.md
3978
+ * await performanceService.dump("my-strategy");
3979
+ *
3980
+ * // Save to custom path
3981
+ * await performanceService.dump("my-strategy", "./custom/path");
3982
+ * ```
3983
+ */
3984
+ this.dump = async (strategyName, path = "./logs/performance") => {
3985
+ this.loggerService.log("performanceMarkdownService dump", {
3986
+ strategyName,
3987
+ path,
3988
+ });
3989
+ const storage = this.getStorage(strategyName);
3990
+ await storage.dump(strategyName, path);
3991
+ };
3992
+ /**
3993
+ * Clears accumulated performance data from storage.
3994
+ *
3995
+ * @param strategyName - Optional strategy name to clear specific strategy data
3996
+ */
3997
+ this.clear = async (strategyName) => {
3998
+ this.loggerService.log("performanceMarkdownService clear", {
3999
+ strategyName,
4000
+ });
4001
+ this.getStorage.clear(strategyName);
4002
+ };
4003
+ /**
4004
+ * Initializes the service by subscribing to performance events.
4005
+ * Uses singleshot to ensure initialization happens only once.
4006
+ */
4007
+ this.init = functoolsKit.singleshot(async () => {
4008
+ this.loggerService.log("performanceMarkdownService init");
4009
+ performanceEmitter.subscribe(this.track);
4010
+ });
4011
+ }
4012
+ }
4013
+
3668
4014
  /**
3669
4015
  * @class ExchangeValidationService
3670
4016
  * Service for managing and validating exchange configurations
@@ -3883,6 +4229,7 @@ class FrameValidationService {
3883
4229
  {
3884
4230
  provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
3885
4231
  provide(TYPES.liveMarkdownService, () => new LiveMarkdownService());
4232
+ provide(TYPES.performanceMarkdownService, () => new PerformanceMarkdownService());
3886
4233
  }
3887
4234
  {
3888
4235
  provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
@@ -3925,6 +4272,7 @@ const logicPublicServices = {
3925
4272
  const markdownServices = {
3926
4273
  backtestMarkdownService: inject(TYPES.backtestMarkdownService),
3927
4274
  liveMarkdownService: inject(TYPES.liveMarkdownService),
4275
+ performanceMarkdownService: inject(TYPES.performanceMarkdownService),
3928
4276
  };
3929
4277
  const validationServices = {
3930
4278
  exchangeValidationService: inject(TYPES.exchangeValidationService),
@@ -3997,8 +4345,8 @@ const ADD_FRAME_METHOD_NAME = "add.addFrame";
3997
4345
  * timestamp: Date.now(),
3998
4346
  * }),
3999
4347
  * callbacks: {
4000
- * onOpen: (backtest, symbol, signal) => console.log("Signal opened"),
4001
- * onClose: (backtest, symbol, priceClose, signal) => console.log("Signal closed"),
4348
+ * onOpen: (symbol, signal, currentPrice, backtest) => console.log("Signal opened"),
4349
+ * onClose: (symbol, signal, priceClose, backtest) => console.log("Signal closed"),
4002
4350
  * },
4003
4351
  * });
4004
4352
  * ```
@@ -4196,6 +4544,7 @@ const LISTEN_ERROR_METHOD_NAME = "event.listenError";
4196
4544
  const LISTEN_DONE_METHOD_NAME = "event.listenDone";
4197
4545
  const LISTEN_DONE_ONCE_METHOD_NAME = "event.listenDoneOnce";
4198
4546
  const LISTEN_PROGRESS_METHOD_NAME = "event.listenProgress";
4547
+ const LISTEN_PERFORMANCE_METHOD_NAME = "event.listenPerformance";
4199
4548
  /**
4200
4549
  * Subscribes to all signal events with queued async processing.
4201
4550
  *
@@ -4485,6 +4834,42 @@ function listenProgress(fn) {
4485
4834
  backtest$1.loggerService.log(LISTEN_PROGRESS_METHOD_NAME);
4486
4835
  return progressEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
4487
4836
  }
4837
+ /**
4838
+ * Subscribes to performance metric events with queued async processing.
4839
+ *
4840
+ * Emits during strategy execution to track timing metrics for operations.
4841
+ * Useful for profiling and identifying performance bottlenecks.
4842
+ * Events are processed sequentially in order received, even if callback is async.
4843
+ * Uses queued wrapper to prevent concurrent execution of the callback.
4844
+ *
4845
+ * @param fn - Callback function to handle performance events
4846
+ * @returns Unsubscribe function to stop listening to events
4847
+ *
4848
+ * @example
4849
+ * ```typescript
4850
+ * import { listenPerformance, Backtest } from "backtest-kit";
4851
+ *
4852
+ * const unsubscribe = listenPerformance((event) => {
4853
+ * console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
4854
+ * if (event.duration > 100) {
4855
+ * console.warn("Slow operation detected:", event.metricType);
4856
+ * }
4857
+ * });
4858
+ *
4859
+ * Backtest.background("BTCUSDT", {
4860
+ * strategyName: "my-strategy",
4861
+ * exchangeName: "binance",
4862
+ * frameName: "1d-backtest"
4863
+ * });
4864
+ *
4865
+ * // Later: stop listening
4866
+ * unsubscribe();
4867
+ * ```
4868
+ */
4869
+ function listenPerformance(fn) {
4870
+ backtest$1.loggerService.log(LISTEN_PERFORMANCE_METHOD_NAME);
4871
+ return performanceEmitter.subscribe(functoolsKit.queued(async (event) => fn(event)));
4872
+ }
4488
4873
 
4489
4874
  const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
4490
4875
  const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
@@ -4969,10 +5354,138 @@ class LiveUtils {
4969
5354
  */
4970
5355
  const Live = new LiveUtils();
4971
5356
 
5357
+ /**
5358
+ * Performance class provides static methods for performance metrics analysis.
5359
+ *
5360
+ * Features:
5361
+ * - Get aggregated performance statistics by strategy
5362
+ * - Generate markdown reports with bottleneck analysis
5363
+ * - Save reports to disk
5364
+ * - Clear accumulated metrics
5365
+ *
5366
+ * @example
5367
+ * ```typescript
5368
+ * import { Performance, listenPerformance } from "backtest-kit";
5369
+ *
5370
+ * // Subscribe to performance events
5371
+ * listenPerformance((event) => {
5372
+ * console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
5373
+ * });
5374
+ *
5375
+ * // Run backtest...
5376
+ *
5377
+ * // Get aggregated statistics
5378
+ * const stats = await Performance.getData("my-strategy");
5379
+ * console.log("Total time:", stats.totalDuration);
5380
+ * console.log("Slowest operations:", Object.values(stats.metricStats)
5381
+ * .sort((a, b) => b.avgDuration - a.avgDuration)
5382
+ * .slice(0, 5));
5383
+ *
5384
+ * // Generate and save report
5385
+ * await Performance.dump("my-strategy");
5386
+ * ```
5387
+ */
5388
+ class Performance {
5389
+ /**
5390
+ * Gets aggregated performance statistics for a strategy.
5391
+ *
5392
+ * Returns detailed metrics grouped by operation type:
5393
+ * - Count, total duration, average, min, max
5394
+ * - Standard deviation for volatility
5395
+ * - Percentiles (median, P95, P99) for outlier detection
5396
+ *
5397
+ * @param strategyName - Strategy name to analyze
5398
+ * @returns Performance statistics with aggregated metrics
5399
+ *
5400
+ * @example
5401
+ * ```typescript
5402
+ * const stats = await Performance.getData("my-strategy");
5403
+ *
5404
+ * // Find slowest operation type
5405
+ * const slowest = Object.values(stats.metricStats)
5406
+ * .sort((a, b) => b.avgDuration - a.avgDuration)[0];
5407
+ * console.log(`Slowest: ${slowest.metricType} (${slowest.avgDuration.toFixed(2)}ms avg)`);
5408
+ *
5409
+ * // Check for outliers
5410
+ * for (const metric of Object.values(stats.metricStats)) {
5411
+ * if (metric.p99 > metric.avgDuration * 5) {
5412
+ * console.warn(`High variance in ${metric.metricType}: P99=${metric.p99}ms, Avg=${metric.avgDuration}ms`);
5413
+ * }
5414
+ * }
5415
+ * ```
5416
+ */
5417
+ static async getData(strategyName) {
5418
+ return backtest$1.performanceMarkdownService.getData(strategyName);
5419
+ }
5420
+ /**
5421
+ * Generates markdown report with performance analysis.
5422
+ *
5423
+ * Report includes:
5424
+ * - Time distribution across operation types
5425
+ * - Detailed metrics table with statistics
5426
+ * - Percentile analysis for bottleneck detection
5427
+ *
5428
+ * @param strategyName - Strategy name to generate report for
5429
+ * @returns Markdown formatted report string
5430
+ *
5431
+ * @example
5432
+ * ```typescript
5433
+ * const markdown = await Performance.getReport("my-strategy");
5434
+ * console.log(markdown);
5435
+ *
5436
+ * // Or save to file
5437
+ * import fs from "fs/promises";
5438
+ * await fs.writeFile("performance-report.md", markdown);
5439
+ * ```
5440
+ */
5441
+ static async getReport(strategyName) {
5442
+ return backtest$1.performanceMarkdownService.getReport(strategyName);
5443
+ }
5444
+ /**
5445
+ * Saves performance report to disk.
5446
+ *
5447
+ * Creates directory if it doesn't exist.
5448
+ * Default path: ./logs/performance/{strategyName}.md
5449
+ *
5450
+ * @param strategyName - Strategy name to save report for
5451
+ * @param path - Optional custom directory path
5452
+ *
5453
+ * @example
5454
+ * ```typescript
5455
+ * // Save to default path: ./logs/performance/my-strategy.md
5456
+ * await Performance.dump("my-strategy");
5457
+ *
5458
+ * // Save to custom path: ./reports/perf/my-strategy.md
5459
+ * await Performance.dump("my-strategy", "./reports/perf");
5460
+ * ```
5461
+ */
5462
+ static async dump(strategyName, path = "./logs/performance") {
5463
+ return backtest$1.performanceMarkdownService.dump(strategyName, path);
5464
+ }
5465
+ /**
5466
+ * Clears accumulated performance metrics from memory.
5467
+ *
5468
+ * @param strategyName - Optional strategy name to clear specific strategy's metrics
5469
+ *
5470
+ * @example
5471
+ * ```typescript
5472
+ * // Clear specific strategy metrics
5473
+ * await Performance.clear("my-strategy");
5474
+ *
5475
+ * // Clear all metrics for all strategies
5476
+ * await Performance.clear();
5477
+ * ```
5478
+ */
5479
+ static async clear(strategyName) {
5480
+ return backtest$1.performanceMarkdownService.clear(strategyName);
5481
+ }
5482
+ }
5483
+
4972
5484
  exports.Backtest = Backtest;
4973
5485
  exports.ExecutionContextService = ExecutionContextService;
4974
5486
  exports.Live = Live;
4975
5487
  exports.MethodContextService = MethodContextService;
5488
+ exports.Performance = Performance;
4976
5489
  exports.PersistBase = PersistBase;
4977
5490
  exports.PersistSignalAdaper = PersistSignalAdaper;
4978
5491
  exports.addExchange = addExchange;
@@ -4991,6 +5504,7 @@ exports.listStrategies = listStrategies;
4991
5504
  exports.listenDone = listenDone;
4992
5505
  exports.listenDoneOnce = listenDoneOnce;
4993
5506
  exports.listenError = listenError;
5507
+ exports.listenPerformance = listenPerformance;
4994
5508
  exports.listenProgress = listenProgress;
4995
5509
  exports.listenSignal = listenSignal;
4996
5510
  exports.listenSignalBacktest = listenSignalBacktest;