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