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