backtest-kit 1.1.5 โ 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 +3 -3
- package/build/index.cjs +273 -31
- package/build/index.mjs +273 -31
- package/package.json +1 -1
- package/types.d.ts +441 -256
package/README.md
CHANGED
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
- ๐ **Async Generators** - Memory-efficient streaming for backtest and live execution
|
|
15
15
|
- ๐ **VWAP Pricing** - Volume-weighted average price from last 5 1m candles
|
|
16
16
|
- ๐ฏ **Signal Lifecycle** - Type-safe state machine (idle โ opened โ active โ closed)
|
|
17
|
-
-
|
|
17
|
+
- ๐ **Accurate PNL** - Calculation with fees (0.1%) and slippage (0.1%)
|
|
18
18
|
- ๐ง **Interval Throttling** - Prevents signal spam at strategy level
|
|
19
19
|
- โก **Memory Optimized** - Prototype methods + memoization + streaming
|
|
20
20
|
- ๐ **Flexible Architecture** - Plug your own exchanges and strategies
|
|
21
|
-
- ๐ **Markdown Reports** - Auto-generated trading reports with statistics (win rate, avg PNL)
|
|
21
|
+
- ๐ **Markdown Reports** - Auto-generated trading reports with statistics (win rate, avg PNL, Sharpe Ratio, Standard Deviation, Certainty Ratio, Expected Yearly Returns, Risk-Adjusted Returns)
|
|
22
22
|
- ๐ **Graceful Shutdown** - Live.background() waits for open positions to close before stopping
|
|
23
23
|
- ๐ **Strategy Dependency Injection** - addStrategy() enables DI pattern for trading strategies
|
|
24
24
|
- ๐ **Schema Reflection API** - listExchanges(), listStrategies(), listFrames() for runtime introspection
|
|
25
|
-
- ๐งช **Comprehensive Test Coverage** -
|
|
25
|
+
- ๐งช **Comprehensive Test Coverage** - 45+ unit tests covering validation, PNL, callbacks, reports, and event system
|
|
26
26
|
|
|
27
27
|
## Installation
|
|
28
28
|
|
package/build/index.cjs
CHANGED
|
@@ -2742,6 +2742,24 @@ class BacktestGlobalService {
|
|
|
2742
2742
|
}
|
|
2743
2743
|
}
|
|
2744
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
|
+
}
|
|
2745
2763
|
const columns$1 = [
|
|
2746
2764
|
{
|
|
2747
2765
|
key: "signalId",
|
|
@@ -2834,13 +2852,80 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
2834
2852
|
this._signalList.push(data);
|
|
2835
2853
|
}
|
|
2836
2854
|
/**
|
|
2837
|
-
*
|
|
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).
|
|
2838
2922
|
*
|
|
2839
2923
|
* @param strategyName - Strategy name
|
|
2840
2924
|
* @returns Markdown formatted report with all signals
|
|
2841
2925
|
*/
|
|
2842
|
-
getReport(strategyName) {
|
|
2843
|
-
|
|
2926
|
+
async getReport(strategyName) {
|
|
2927
|
+
const stats = await this.getData();
|
|
2928
|
+
if (stats.totalSignals === 0) {
|
|
2844
2929
|
return functoolsKit.str.newline(`# Backtest Report: ${strategyName}`, "", "No signals closed yet.");
|
|
2845
2930
|
}
|
|
2846
2931
|
const header = columns$1.map((col) => col.label);
|
|
@@ -2848,13 +2933,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
2848
2933
|
const rows = this._signalList.map((closedSignal) => columns$1.map((col) => col.format(closedSignal)));
|
|
2849
2934
|
const tableData = [header, separator, ...rows];
|
|
2850
2935
|
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
2851
|
-
|
|
2852
|
-
const totalSignals = this._signalList.length;
|
|
2853
|
-
const winCount = this._signalList.filter((s) => s.pnl.pnlPercentage > 0).length;
|
|
2854
|
-
const lossCount = this._signalList.filter((s) => s.pnl.pnlPercentage < 0).length;
|
|
2855
|
-
const avgPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0) / totalSignals;
|
|
2856
|
-
const totalPnl = this._signalList.reduce((sum, s) => sum + s.pnl.pnlPercentage, 0);
|
|
2857
|
-
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)`}`);
|
|
2858
2937
|
}
|
|
2859
2938
|
/**
|
|
2860
2939
|
* Saves strategy report to disk.
|
|
@@ -2863,7 +2942,7 @@ let ReportStorage$1 = class ReportStorage {
|
|
|
2863
2942
|
* @param path - Directory path to save report (default: "./logs/backtest")
|
|
2864
2943
|
*/
|
|
2865
2944
|
async dump(strategyName, path$1 = "./logs/backtest") {
|
|
2866
|
-
const markdown = this.getReport(strategyName);
|
|
2945
|
+
const markdown = await this.getReport(strategyName);
|
|
2867
2946
|
try {
|
|
2868
2947
|
const dir = path.join(process.cwd(), path$1);
|
|
2869
2948
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -2942,6 +3021,27 @@ class BacktestMarkdownService {
|
|
|
2942
3021
|
const storage = this.getStorage(data.strategyName);
|
|
2943
3022
|
storage.addSignal(data);
|
|
2944
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
|
+
};
|
|
2945
3045
|
/**
|
|
2946
3046
|
* Generates markdown report with all closed signals for a strategy.
|
|
2947
3047
|
* Delegates to ReportStorage.generateReport().
|
|
@@ -2952,7 +3052,7 @@ class BacktestMarkdownService {
|
|
|
2952
3052
|
* @example
|
|
2953
3053
|
* ```typescript
|
|
2954
3054
|
* const service = new BacktestMarkdownService();
|
|
2955
|
-
* const markdown = service.
|
|
3055
|
+
* const markdown = await service.getReport("my-strategy");
|
|
2956
3056
|
* console.log(markdown);
|
|
2957
3057
|
* ```
|
|
2958
3058
|
*/
|
|
@@ -3032,6 +3132,24 @@ class BacktestMarkdownService {
|
|
|
3032
3132
|
}
|
|
3033
3133
|
}
|
|
3034
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
|
+
}
|
|
3035
3153
|
const columns = [
|
|
3036
3154
|
{
|
|
3037
3155
|
key: "timestamp",
|
|
@@ -3239,36 +3357,103 @@ class ReportStorage {
|
|
|
3239
3357
|
}
|
|
3240
3358
|
}
|
|
3241
3359
|
/**
|
|
3242
|
-
*
|
|
3360
|
+
* Calculates statistical data from live trading events (Controller).
|
|
3361
|
+
* Returns null for any unsafe numeric values (NaN, Infinity, etc).
|
|
3243
3362
|
*
|
|
3244
|
-
* @
|
|
3245
|
-
* @returns Markdown formatted report with all events
|
|
3363
|
+
* @returns Statistical data (empty object if no events)
|
|
3246
3364
|
*/
|
|
3247
|
-
|
|
3365
|
+
async getData() {
|
|
3248
3366
|
if (this._eventList.length === 0) {
|
|
3249
|
-
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
|
+
};
|
|
3250
3382
|
}
|
|
3251
|
-
const header = columns.map((col) => col.label);
|
|
3252
|
-
const separator = columns.map(() => "---");
|
|
3253
|
-
const rows = this._eventList.map((event) => columns.map((col) => col.format(event)));
|
|
3254
|
-
const tableData = [header, separator, ...rows];
|
|
3255
|
-
const table = functoolsKit.str.newline(tableData.map(row => `| ${row.join(" | ")} |`));
|
|
3256
|
-
// Calculate statistics
|
|
3257
3383
|
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
3258
3384
|
const totalClosed = closedEvents.length;
|
|
3259
3385
|
const winCount = closedEvents.filter((e) => e.pnl && e.pnl > 0).length;
|
|
3260
3386
|
const lossCount = closedEvents.filter((e) => e.pnl && e.pnl < 0).length;
|
|
3387
|
+
// Calculate basic statistics
|
|
3261
3388
|
const avgPnl = totalClosed > 0
|
|
3262
3389
|
? closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0) / totalClosed
|
|
3263
3390
|
: 0;
|
|
3264
3391
|
const totalPnl = closedEvents.reduce((sum, e) => sum + (e.pnl || 0), 0);
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
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)`}`);
|
|
3272
3457
|
}
|
|
3273
3458
|
/**
|
|
3274
3459
|
* Saves strategy report to disk.
|
|
@@ -3277,7 +3462,7 @@ class ReportStorage {
|
|
|
3277
3462
|
* @param path - Directory path to save report (default: "./logs/live")
|
|
3278
3463
|
*/
|
|
3279
3464
|
async dump(strategyName, path$1 = "./logs/live") {
|
|
3280
|
-
const markdown = this.getReport(strategyName);
|
|
3465
|
+
const markdown = await this.getReport(strategyName);
|
|
3281
3466
|
try {
|
|
3282
3467
|
const dir = path.join(process.cwd(), path$1);
|
|
3283
3468
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -3369,6 +3554,27 @@ class LiveMarkdownService {
|
|
|
3369
3554
|
storage.addClosedEvent(data);
|
|
3370
3555
|
}
|
|
3371
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
|
+
};
|
|
3372
3578
|
/**
|
|
3373
3579
|
* Generates markdown report with all events for a strategy.
|
|
3374
3580
|
* Delegates to ReportStorage.getReport().
|
|
@@ -4506,6 +4712,24 @@ class BacktestUtils {
|
|
|
4506
4712
|
isStopped = true;
|
|
4507
4713
|
};
|
|
4508
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
|
+
};
|
|
4509
4733
|
/**
|
|
4510
4734
|
* Generates markdown report with all closed signals for a strategy.
|
|
4511
4735
|
*
|
|
@@ -4668,6 +4892,24 @@ class LiveUtils {
|
|
|
4668
4892
|
isStopped = true;
|
|
4669
4893
|
};
|
|
4670
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
|
+
};
|
|
4671
4913
|
/**
|
|
4672
4914
|
* Generates markdown report with all events for a strategy.
|
|
4673
4915
|
*
|