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/README.md +299 -30
- package/build/index.cjs +524 -10
- package/build/index.mjs +523 -11
- package/package.json +2 -2
- package/types.d.ts +346 -9
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
|
-
|
|
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,
|
|
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: (
|
|
4001
|
-
* onClose: (
|
|
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;
|