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