backtest-kit 11.2.0 β 11.4.0
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 +2 -2
- package/build/index.cjs +98 -7
- package/build/index.mjs +98 -7
- package/package.json +1 -1
- package/types.d.ts +14 -0
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ Install the core library and peer dependencies manually. Use this approach when
|
|
|
79
79
|
- π **Pluggable**: Custom data sources (CCXT), persistence (file/Redis), and sizing calculators.
|
|
80
80
|
- ποΈ **Transactional Live Orders**: Broker adapter intercepts every trade mutation before internal state changes β exchange rejection rolls back the operation atomically.
|
|
81
81
|
- β° **Built-in Crontab**: Register periodic or fire-once jobs that fire on virtual-time boundaries with singleshot coordination across parallel backtests β one handler invocation per boundary, no double-fires.
|
|
82
|
-
- π§ͺ **Tested**:
|
|
82
|
+
- π§ͺ **Tested**: 770+ unit/integration tests for validation, recovery, and events.
|
|
83
83
|
- π **Self hosted**: Zero dependency on third-party node_modules or platforms; run entirely in your own environment.
|
|
84
84
|
|
|
85
85
|
## π Supported Order Types
|
|
@@ -1983,7 +1983,7 @@ Python-based (WASI) strategy that uses EMA(9) and EMA(21) crossover signals exec
|
|
|
1983
1983
|
|
|
1984
1984
|
## β
Tested & Reliable
|
|
1985
1985
|
|
|
1986
|
-
|
|
1986
|
+
770+ tests cover validation, recovery, reports, and events.
|
|
1987
1987
|
|
|
1988
1988
|
## π€ Contribute
|
|
1989
1989
|
|
package/build/index.cjs
CHANGED
|
@@ -23816,6 +23816,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23816
23816
|
sortinoRatio: null,
|
|
23817
23817
|
calmarRatio: null,
|
|
23818
23818
|
recoveryFactor: null,
|
|
23819
|
+
expectancy: null,
|
|
23819
23820
|
};
|
|
23820
23821
|
}
|
|
23821
23822
|
// Valid signal set β those with usable pendingAt AND closeTimestamp. Single source
|
|
@@ -23938,6 +23939,12 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23938
23939
|
const certaintyRatio = canComputeRatios && Math.abs(avgLoss) > STDDEV_EPSILON$2 && avgLoss < 0
|
|
23939
23940
|
? avgWin / Math.abs(avgLoss)
|
|
23940
23941
|
: null;
|
|
23942
|
+
// Per-trade Expectancy: winProb*avgWin + lossProb*avgLoss. Break-even trades
|
|
23943
|
+
// contribute 0 (they're excluded from both probabilities). N-gated like the
|
|
23944
|
+
// other ratios β on a tiny sample the per-trade EV is too noisy to publish.
|
|
23945
|
+
const expectancy = canComputeRatios && totalSignals > 0
|
|
23946
|
+
? (wins.length / totalSignals) * avgWin + (losses.length / totalSignals) * avgLoss
|
|
23947
|
+
: null;
|
|
23941
23948
|
// Average peak/fall PNL β over validSignals; only signals that actually have the
|
|
23942
23949
|
// value contribute (no zero dilution from missing peakProfit/maxDrawdown).
|
|
23943
23950
|
const peakValues = validSignals
|
|
@@ -24002,6 +24009,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
24002
24009
|
sortinoRatio: isUnsafe$4(sortinoRatio) ? null : sortinoRatio,
|
|
24003
24010
|
calmarRatio: isUnsafe$4(calmarRatio) ? null : calmarRatio,
|
|
24004
24011
|
recoveryFactor: isUnsafe$4(recoveryFactor) ? null : recoveryFactor,
|
|
24012
|
+
expectancy: isUnsafe$4(expectancy) ? null : expectancy,
|
|
24005
24013
|
};
|
|
24006
24014
|
}
|
|
24007
24015
|
/**
|
|
@@ -24051,6 +24059,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
24051
24059
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
24052
24060
|
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
24053
24061
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
24062
|
+
`**Expectancy:** ${stats.expectancy === null ? "N/A" : `${stats.expectancy > 0 ? "+" : ""}${stats.expectancy.toFixed(3)}% (higher is better)`}`,
|
|
24054
24063
|
"",
|
|
24055
24064
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
24056
24065
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
@@ -24060,6 +24069,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
24060
24069
|
`*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at Β±${MAX_EXPECTED_YEARLY_RETURNS$2}% β values above the cap return N/A.*`,
|
|
24061
24070
|
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at Β±${MAX_CALMAR_RATIO$2}.*`,
|
|
24062
24071
|
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
|
|
24072
|
+
`*Expectancy: per-trade expected value (winProb Γ avgWin + lossProb Γ avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
|
|
24063
24073
|
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24064
24074
|
`*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
|
|
24065
24075
|
`*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies β closer to zero is less bad, positive is profitable.*`,
|
|
@@ -24685,6 +24695,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24685
24695
|
sortinoRatio: null,
|
|
24686
24696
|
calmarRatio: null,
|
|
24687
24697
|
recoveryFactor: null,
|
|
24698
|
+
expectancy: null,
|
|
24688
24699
|
};
|
|
24689
24700
|
}
|
|
24690
24701
|
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
@@ -24755,6 +24766,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24755
24766
|
// so the report doesn't surface certainty on a handful of trades while
|
|
24756
24767
|
// withholding the rest.
|
|
24757
24768
|
let certaintyRatio = null;
|
|
24769
|
+
let expectancy = null;
|
|
24758
24770
|
if (canComputeRatios && totalClosed > 0) {
|
|
24759
24771
|
const wins = validClosed.filter((e) => e.pnl > 0);
|
|
24760
24772
|
const losses = validClosed.filter((e) => e.pnl < 0);
|
|
@@ -24769,6 +24781,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24769
24781
|
certaintyRatio = Math.abs(avgLoss) > STDDEV_EPSILON$1 && avgLoss < 0
|
|
24770
24782
|
? avgWin / Math.abs(avgLoss)
|
|
24771
24783
|
: null;
|
|
24784
|
+
// Per-trade Expectancy: winProb*avgWin + lossProb*avgLoss. Break-even
|
|
24785
|
+
// trades contribute 0 (excluded from both probabilities).
|
|
24786
|
+
expectancy = (wins.length / totalClosed) * avgWin + (losses.length / totalClosed) * avgLoss;
|
|
24772
24787
|
}
|
|
24773
24788
|
// Average only over signals that have the value β do not dilute the mean with zeros.
|
|
24774
24789
|
// Use validClosed to keep all metric denominators consistent.
|
|
@@ -24872,6 +24887,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24872
24887
|
sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
|
|
24873
24888
|
calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
|
|
24874
24889
|
recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
|
|
24890
|
+
expectancy: isUnsafe$3(expectancy) ? null : expectancy,
|
|
24875
24891
|
};
|
|
24876
24892
|
}
|
|
24877
24893
|
/**
|
|
@@ -24921,6 +24937,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24921
24937
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
24922
24938
|
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
24923
24939
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
24940
|
+
`**Expectancy:** ${stats.expectancy === null ? "N/A" : `${stats.expectancy > 0 ? "+" : ""}${stats.expectancy.toFixed(3)}% (higher is better)`}`,
|
|
24924
24941
|
"",
|
|
24925
24942
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
24926
24943
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
@@ -24930,6 +24947,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24930
24947
|
`*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at Β±${MAX_EXPECTED_YEARLY_RETURNS$1}% β values above the cap return N/A.*`,
|
|
24931
24948
|
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at Β±${MAX_CALMAR_RATIO$1}.*`,
|
|
24932
24949
|
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
|
|
24950
|
+
`*Expectancy: per-trade expected value (winProb Γ avgWin + lossProb Γ avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
|
|
24933
24951
|
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24934
24952
|
`*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
|
|
24935
24953
|
`*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies β closer to zero is less bad, positive is profitable.*`,
|
|
@@ -27127,11 +27145,15 @@ class HeatmapStorage {
|
|
|
27127
27145
|
: null;
|
|
27128
27146
|
portfolioTotalTrades = symbols.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
27129
27147
|
}
|
|
27130
|
-
// Pooled
|
|
27131
|
-
//
|
|
27132
|
-
//
|
|
27133
|
-
// produce a noisy Β±Sharpe.
|
|
27148
|
+
// Pooled metrics over all returns across symbols. NOT a Markowitz portfolio β
|
|
27149
|
+
// ignores cross-symbol correlations, treats trades as a single pooled sample.
|
|
27150
|
+
// Gated by MIN_SIGNALS_FOR_RATIOS so a tiny pool can't produce noisy ratios.
|
|
27134
27151
|
let portfolioSharpeRatio = null;
|
|
27152
|
+
let portfolioStdDev = null;
|
|
27153
|
+
let portfolioSortinoRatio = null;
|
|
27154
|
+
let portfolioExpectancy = null;
|
|
27155
|
+
let portfolioCalmarRatio = null;
|
|
27156
|
+
let portfolioRecoveryFactor = null;
|
|
27135
27157
|
const allReturns = [];
|
|
27136
27158
|
for (const signals of this.symbolData.values()) {
|
|
27137
27159
|
for (const s of signals) {
|
|
@@ -27142,10 +27164,63 @@ class HeatmapStorage {
|
|
|
27142
27164
|
const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
|
|
27143
27165
|
const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
|
|
27144
27166
|
(allReturns.length - 1);
|
|
27145
|
-
const
|
|
27167
|
+
const stdDev = Math.sqrt(portfolioVariance);
|
|
27146
27168
|
// STDDEV_EPSILON guard β same protection as per-symbol Sharpe.
|
|
27147
|
-
|
|
27148
|
-
|
|
27169
|
+
portfolioStdDev = stdDev;
|
|
27170
|
+
if (stdDev > STDDEV_EPSILON) {
|
|
27171
|
+
portfolioSharpeRatio = portfolioAvg / stdDev;
|
|
27172
|
+
}
|
|
27173
|
+
// Canonical Sortino: downside dev = β( Ξ£ min(0, r)Β² / N_total ), MAR=0.
|
|
27174
|
+
const negativeReturns = allReturns.filter((r) => r < 0);
|
|
27175
|
+
if (negativeReturns.length > 0) {
|
|
27176
|
+
const downsideVariance = negativeReturns.reduce((acc, r) => acc + r * r, 0) / allReturns.length;
|
|
27177
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
27178
|
+
if (downsideDeviation > STDDEV_EPSILON) {
|
|
27179
|
+
portfolioSortinoRatio = portfolioAvg / downsideDeviation;
|
|
27180
|
+
}
|
|
27181
|
+
}
|
|
27182
|
+
// Pooled Expectancy: per-trade EV = winProb*avgWin + lossProb*avgLoss.
|
|
27183
|
+
// Break-even trades contribute 0 (excluded from both probs).
|
|
27184
|
+
const wins = allReturns.filter((r) => r > 0);
|
|
27185
|
+
const losses = allReturns.filter((r) => r < 0);
|
|
27186
|
+
const total = allReturns.length;
|
|
27187
|
+
const avgWin = wins.length > 0 ? wins.reduce((a, b) => a + b, 0) / wins.length : 0;
|
|
27188
|
+
const avgLoss = losses.length > 0 ? losses.reduce((a, b) => a + b, 0) / losses.length : 0;
|
|
27189
|
+
if (wins.length > 0 || losses.length > 0) {
|
|
27190
|
+
portfolioExpectancy = (wins.length / total) * avgWin + (losses.length / total) * avgLoss;
|
|
27191
|
+
}
|
|
27192
|
+
// Pooled equity-curve max drawdown (compounded).
|
|
27193
|
+
let equity = 1;
|
|
27194
|
+
let peak = 1;
|
|
27195
|
+
let maxDD = 0;
|
|
27196
|
+
let blown = false;
|
|
27197
|
+
for (const r of allReturns) {
|
|
27198
|
+
equity *= 1 + r / 100;
|
|
27199
|
+
if (equity <= 0) {
|
|
27200
|
+
maxDD = 100;
|
|
27201
|
+
blown = true;
|
|
27202
|
+
break;
|
|
27203
|
+
}
|
|
27204
|
+
if (equity > peak)
|
|
27205
|
+
peak = equity;
|
|
27206
|
+
const dd = ((peak - equity) / peak) * 100;
|
|
27207
|
+
if (dd > maxDD)
|
|
27208
|
+
maxDD = dd;
|
|
27209
|
+
}
|
|
27210
|
+
const equityFinal = blown ? 0 : equity;
|
|
27211
|
+
// Pooled Calmar / Recovery, both clamped at Β±MAX_CALMAR_RATIO and using
|
|
27212
|
+
// compounded total return / DD. Same shape as per-symbol formula.
|
|
27213
|
+
if (maxDD > 0) {
|
|
27214
|
+
if (!blown) {
|
|
27215
|
+
const rawCalmar = ((equityFinal - 1) * 100) / maxDD;
|
|
27216
|
+
portfolioCalmarRatio = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawCalmar));
|
|
27217
|
+
const rawRec = ((equityFinal - 1) * 100) / maxDD;
|
|
27218
|
+
portfolioRecoveryFactor = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawRec));
|
|
27219
|
+
}
|
|
27220
|
+
else {
|
|
27221
|
+
// Blown β full loss is the only meaningful value; recovery undefined.
|
|
27222
|
+
portfolioCalmarRatio = -1; // -100 / 100
|
|
27223
|
+
}
|
|
27149
27224
|
}
|
|
27150
27225
|
}
|
|
27151
27226
|
// Portfolio-wide weighted average peak/fall PNL. Denominator must include only
|
|
@@ -27172,6 +27247,16 @@ class HeatmapStorage {
|
|
|
27172
27247
|
portfolioAvgPeakPnl = null;
|
|
27173
27248
|
if (isUnsafe(portfolioAvgFallPnl))
|
|
27174
27249
|
portfolioAvgFallPnl = null;
|
|
27250
|
+
if (isUnsafe(portfolioStdDev))
|
|
27251
|
+
portfolioStdDev = null;
|
|
27252
|
+
if (isUnsafe(portfolioSortinoRatio))
|
|
27253
|
+
portfolioSortinoRatio = null;
|
|
27254
|
+
if (isUnsafe(portfolioCalmarRatio))
|
|
27255
|
+
portfolioCalmarRatio = null;
|
|
27256
|
+
if (isUnsafe(portfolioRecoveryFactor))
|
|
27257
|
+
portfolioRecoveryFactor = null;
|
|
27258
|
+
if (isUnsafe(portfolioExpectancy))
|
|
27259
|
+
portfolioExpectancy = null;
|
|
27175
27260
|
return {
|
|
27176
27261
|
symbols,
|
|
27177
27262
|
totalSymbols,
|
|
@@ -27180,6 +27265,11 @@ class HeatmapStorage {
|
|
|
27180
27265
|
portfolioTotalTrades,
|
|
27181
27266
|
portfolioAvgPeakPnl,
|
|
27182
27267
|
portfolioAvgFallPnl,
|
|
27268
|
+
portfolioStdDev,
|
|
27269
|
+
portfolioSortinoRatio,
|
|
27270
|
+
portfolioCalmarRatio,
|
|
27271
|
+
portfolioRecoveryFactor,
|
|
27272
|
+
portfolioExpectancy,
|
|
27183
27273
|
};
|
|
27184
27274
|
}
|
|
27185
27275
|
/**
|
|
@@ -27229,6 +27319,7 @@ class HeatmapStorage {
|
|
|
27229
27319
|
`# Portfolio Heatmap: ${strategyName}`,
|
|
27230
27320
|
"",
|
|
27231
27321
|
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? functoolsKit.str(data.portfolioTotalPnl, "%") : "N/A"} | **Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? functoolsKit.str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? functoolsKit.str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? functoolsKit.str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
|
|
27322
|
+
`**Standard Deviation:** ${data.portfolioStdDev !== null ? functoolsKit.str(data.portfolioStdDev, "%") : "N/A"} | **Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? functoolsKit.str(data.portfolioSortinoRatio) : "N/A"} | **Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? functoolsKit.str(data.portfolioCalmarRatio) : "N/A"} | **Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? functoolsKit.str(data.portfolioRecoveryFactor) : "N/A"} | **Expectancy:** ${data.portfolioExpectancy !== null ? functoolsKit.str(data.portfolioExpectancy, "%") : "N/A"}`,
|
|
27232
27323
|
"",
|
|
27233
27324
|
table,
|
|
27234
27325
|
"",
|
package/build/index.mjs
CHANGED
|
@@ -23796,6 +23796,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23796
23796
|
sortinoRatio: null,
|
|
23797
23797
|
calmarRatio: null,
|
|
23798
23798
|
recoveryFactor: null,
|
|
23799
|
+
expectancy: null,
|
|
23799
23800
|
};
|
|
23800
23801
|
}
|
|
23801
23802
|
// Valid signal set β those with usable pendingAt AND closeTimestamp. Single source
|
|
@@ -23918,6 +23919,12 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23918
23919
|
const certaintyRatio = canComputeRatios && Math.abs(avgLoss) > STDDEV_EPSILON$2 && avgLoss < 0
|
|
23919
23920
|
? avgWin / Math.abs(avgLoss)
|
|
23920
23921
|
: null;
|
|
23922
|
+
// Per-trade Expectancy: winProb*avgWin + lossProb*avgLoss. Break-even trades
|
|
23923
|
+
// contribute 0 (they're excluded from both probabilities). N-gated like the
|
|
23924
|
+
// other ratios β on a tiny sample the per-trade EV is too noisy to publish.
|
|
23925
|
+
const expectancy = canComputeRatios && totalSignals > 0
|
|
23926
|
+
? (wins.length / totalSignals) * avgWin + (losses.length / totalSignals) * avgLoss
|
|
23927
|
+
: null;
|
|
23921
23928
|
// Average peak/fall PNL β over validSignals; only signals that actually have the
|
|
23922
23929
|
// value contribute (no zero dilution from missing peakProfit/maxDrawdown).
|
|
23923
23930
|
const peakValues = validSignals
|
|
@@ -23982,6 +23989,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23982
23989
|
sortinoRatio: isUnsafe$4(sortinoRatio) ? null : sortinoRatio,
|
|
23983
23990
|
calmarRatio: isUnsafe$4(calmarRatio) ? null : calmarRatio,
|
|
23984
23991
|
recoveryFactor: isUnsafe$4(recoveryFactor) ? null : recoveryFactor,
|
|
23992
|
+
expectancy: isUnsafe$4(expectancy) ? null : expectancy,
|
|
23985
23993
|
};
|
|
23986
23994
|
}
|
|
23987
23995
|
/**
|
|
@@ -24031,6 +24039,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
24031
24039
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
24032
24040
|
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
24033
24041
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
24042
|
+
`**Expectancy:** ${stats.expectancy === null ? "N/A" : `${stats.expectancy > 0 ? "+" : ""}${stats.expectancy.toFixed(3)}% (higher is better)`}`,
|
|
24034
24043
|
"",
|
|
24035
24044
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
24036
24045
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
@@ -24040,6 +24049,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
24040
24049
|
`*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at Β±${MAX_EXPECTED_YEARLY_RETURNS$2}% β values above the cap return N/A.*`,
|
|
24041
24050
|
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at Β±${MAX_CALMAR_RATIO$2}.*`,
|
|
24042
24051
|
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
|
|
24052
|
+
`*Expectancy: per-trade expected value (winProb Γ avgWin + lossProb Γ avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
|
|
24043
24053
|
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24044
24054
|
`*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
|
|
24045
24055
|
`*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies β closer to zero is less bad, positive is profitable.*`,
|
|
@@ -24665,6 +24675,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24665
24675
|
sortinoRatio: null,
|
|
24666
24676
|
calmarRatio: null,
|
|
24667
24677
|
recoveryFactor: null,
|
|
24678
|
+
expectancy: null,
|
|
24668
24679
|
};
|
|
24669
24680
|
}
|
|
24670
24681
|
const closedEvents = this._eventList.filter((e) => e.action === "closed");
|
|
@@ -24735,6 +24746,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24735
24746
|
// so the report doesn't surface certainty on a handful of trades while
|
|
24736
24747
|
// withholding the rest.
|
|
24737
24748
|
let certaintyRatio = null;
|
|
24749
|
+
let expectancy = null;
|
|
24738
24750
|
if (canComputeRatios && totalClosed > 0) {
|
|
24739
24751
|
const wins = validClosed.filter((e) => e.pnl > 0);
|
|
24740
24752
|
const losses = validClosed.filter((e) => e.pnl < 0);
|
|
@@ -24749,6 +24761,9 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24749
24761
|
certaintyRatio = Math.abs(avgLoss) > STDDEV_EPSILON$1 && avgLoss < 0
|
|
24750
24762
|
? avgWin / Math.abs(avgLoss)
|
|
24751
24763
|
: null;
|
|
24764
|
+
// Per-trade Expectancy: winProb*avgWin + lossProb*avgLoss. Break-even
|
|
24765
|
+
// trades contribute 0 (excluded from both probabilities).
|
|
24766
|
+
expectancy = (wins.length / totalClosed) * avgWin + (losses.length / totalClosed) * avgLoss;
|
|
24752
24767
|
}
|
|
24753
24768
|
// Average only over signals that have the value β do not dilute the mean with zeros.
|
|
24754
24769
|
// Use validClosed to keep all metric denominators consistent.
|
|
@@ -24852,6 +24867,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24852
24867
|
sortinoRatio: isUnsafe$3(sortinoRatio) ? null : sortinoRatio,
|
|
24853
24868
|
calmarRatio: isUnsafe$3(calmarRatio) ? null : calmarRatio,
|
|
24854
24869
|
recoveryFactor: isUnsafe$3(recoveryFactor) ? null : recoveryFactor,
|
|
24870
|
+
expectancy: isUnsafe$3(expectancy) ? null : expectancy,
|
|
24855
24871
|
};
|
|
24856
24872
|
}
|
|
24857
24873
|
/**
|
|
@@ -24901,6 +24917,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24901
24917
|
`**Sortino Ratio:** ${stats.sortinoRatio === null ? "N/A" : `${stats.sortinoRatio.toFixed(3)} (higher is better)`}`,
|
|
24902
24918
|
`**Calmar Ratio:** ${stats.calmarRatio === null ? "N/A" : `${stats.calmarRatio.toFixed(3)} (higher is better)`}`,
|
|
24903
24919
|
`**Recovery Factor:** ${stats.recoveryFactor === null ? "N/A" : `${stats.recoveryFactor.toFixed(3)} (higher is better)`}`,
|
|
24920
|
+
`**Expectancy:** ${stats.expectancy === null ? "N/A" : `${stats.expectancy > 0 ? "+" : ""}${stats.expectancy.toFixed(3)}% (higher is better)`}`,
|
|
24904
24921
|
"",
|
|
24905
24922
|
`*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
|
|
24906
24923
|
`*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
|
|
@@ -24910,6 +24927,7 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
24910
24927
|
`*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at Β±${MAX_EXPECTED_YEARLY_RETURNS$1}% β values above the cap return N/A.*`,
|
|
24911
24928
|
`*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is compounded equity-curve max drawdown. Capped at Β±${MAX_CALMAR_RATIO$1}.*`,
|
|
24912
24929
|
`*Recovery Factor: below 1.0 means total profit does not cover max drawdown. Above 3.0 is considered good. Uses compounded total return as numerator.*`,
|
|
24930
|
+
`*Expectancy: per-trade expected value (winProb Γ avgWin + lossProb Γ avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
|
|
24913
24931
|
`*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
|
|
24914
24932
|
`*IMPORTANT: Equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per trade** (no sizing, no portfolio fraction). Per-trade pnlPercentage is treated as a return on full equity. If your strategy risks X% of capital per trade, the realized portfolio return / drawdown will be roughly X/100 of the reported figures. The framework does not track portfolio-level sizing, so these metrics represent a theoretical upper bound under full allocation.*`,
|
|
24915
24933
|
`*Negative values for Sharpe / Sortino / Calmar / Recovery / Expected Yearly Returns indicate a losing strategy (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies β closer to zero is less bad, positive is profitable.*`,
|
|
@@ -27107,11 +27125,15 @@ class HeatmapStorage {
|
|
|
27107
27125
|
: null;
|
|
27108
27126
|
portfolioTotalTrades = symbols.reduce((acc, s) => acc + s.totalTrades, 0);
|
|
27109
27127
|
}
|
|
27110
|
-
// Pooled
|
|
27111
|
-
//
|
|
27112
|
-
//
|
|
27113
|
-
// produce a noisy Β±Sharpe.
|
|
27128
|
+
// Pooled metrics over all returns across symbols. NOT a Markowitz portfolio β
|
|
27129
|
+
// ignores cross-symbol correlations, treats trades as a single pooled sample.
|
|
27130
|
+
// Gated by MIN_SIGNALS_FOR_RATIOS so a tiny pool can't produce noisy ratios.
|
|
27114
27131
|
let portfolioSharpeRatio = null;
|
|
27132
|
+
let portfolioStdDev = null;
|
|
27133
|
+
let portfolioSortinoRatio = null;
|
|
27134
|
+
let portfolioExpectancy = null;
|
|
27135
|
+
let portfolioCalmarRatio = null;
|
|
27136
|
+
let portfolioRecoveryFactor = null;
|
|
27115
27137
|
const allReturns = [];
|
|
27116
27138
|
for (const signals of this.symbolData.values()) {
|
|
27117
27139
|
for (const s of signals) {
|
|
@@ -27122,10 +27144,63 @@ class HeatmapStorage {
|
|
|
27122
27144
|
const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
|
|
27123
27145
|
const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
|
|
27124
27146
|
(allReturns.length - 1);
|
|
27125
|
-
const
|
|
27147
|
+
const stdDev = Math.sqrt(portfolioVariance);
|
|
27126
27148
|
// STDDEV_EPSILON guard β same protection as per-symbol Sharpe.
|
|
27127
|
-
|
|
27128
|
-
|
|
27149
|
+
portfolioStdDev = stdDev;
|
|
27150
|
+
if (stdDev > STDDEV_EPSILON) {
|
|
27151
|
+
portfolioSharpeRatio = portfolioAvg / stdDev;
|
|
27152
|
+
}
|
|
27153
|
+
// Canonical Sortino: downside dev = β( Ξ£ min(0, r)Β² / N_total ), MAR=0.
|
|
27154
|
+
const negativeReturns = allReturns.filter((r) => r < 0);
|
|
27155
|
+
if (negativeReturns.length > 0) {
|
|
27156
|
+
const downsideVariance = negativeReturns.reduce((acc, r) => acc + r * r, 0) / allReturns.length;
|
|
27157
|
+
const downsideDeviation = Math.sqrt(downsideVariance);
|
|
27158
|
+
if (downsideDeviation > STDDEV_EPSILON) {
|
|
27159
|
+
portfolioSortinoRatio = portfolioAvg / downsideDeviation;
|
|
27160
|
+
}
|
|
27161
|
+
}
|
|
27162
|
+
// Pooled Expectancy: per-trade EV = winProb*avgWin + lossProb*avgLoss.
|
|
27163
|
+
// Break-even trades contribute 0 (excluded from both probs).
|
|
27164
|
+
const wins = allReturns.filter((r) => r > 0);
|
|
27165
|
+
const losses = allReturns.filter((r) => r < 0);
|
|
27166
|
+
const total = allReturns.length;
|
|
27167
|
+
const avgWin = wins.length > 0 ? wins.reduce((a, b) => a + b, 0) / wins.length : 0;
|
|
27168
|
+
const avgLoss = losses.length > 0 ? losses.reduce((a, b) => a + b, 0) / losses.length : 0;
|
|
27169
|
+
if (wins.length > 0 || losses.length > 0) {
|
|
27170
|
+
portfolioExpectancy = (wins.length / total) * avgWin + (losses.length / total) * avgLoss;
|
|
27171
|
+
}
|
|
27172
|
+
// Pooled equity-curve max drawdown (compounded).
|
|
27173
|
+
let equity = 1;
|
|
27174
|
+
let peak = 1;
|
|
27175
|
+
let maxDD = 0;
|
|
27176
|
+
let blown = false;
|
|
27177
|
+
for (const r of allReturns) {
|
|
27178
|
+
equity *= 1 + r / 100;
|
|
27179
|
+
if (equity <= 0) {
|
|
27180
|
+
maxDD = 100;
|
|
27181
|
+
blown = true;
|
|
27182
|
+
break;
|
|
27183
|
+
}
|
|
27184
|
+
if (equity > peak)
|
|
27185
|
+
peak = equity;
|
|
27186
|
+
const dd = ((peak - equity) / peak) * 100;
|
|
27187
|
+
if (dd > maxDD)
|
|
27188
|
+
maxDD = dd;
|
|
27189
|
+
}
|
|
27190
|
+
const equityFinal = blown ? 0 : equity;
|
|
27191
|
+
// Pooled Calmar / Recovery, both clamped at Β±MAX_CALMAR_RATIO and using
|
|
27192
|
+
// compounded total return / DD. Same shape as per-symbol formula.
|
|
27193
|
+
if (maxDD > 0) {
|
|
27194
|
+
if (!blown) {
|
|
27195
|
+
const rawCalmar = ((equityFinal - 1) * 100) / maxDD;
|
|
27196
|
+
portfolioCalmarRatio = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawCalmar));
|
|
27197
|
+
const rawRec = ((equityFinal - 1) * 100) / maxDD;
|
|
27198
|
+
portfolioRecoveryFactor = Math.max(-MAX_CALMAR_RATIO, Math.min(MAX_CALMAR_RATIO, rawRec));
|
|
27199
|
+
}
|
|
27200
|
+
else {
|
|
27201
|
+
// Blown β full loss is the only meaningful value; recovery undefined.
|
|
27202
|
+
portfolioCalmarRatio = -1; // -100 / 100
|
|
27203
|
+
}
|
|
27129
27204
|
}
|
|
27130
27205
|
}
|
|
27131
27206
|
// Portfolio-wide weighted average peak/fall PNL. Denominator must include only
|
|
@@ -27152,6 +27227,16 @@ class HeatmapStorage {
|
|
|
27152
27227
|
portfolioAvgPeakPnl = null;
|
|
27153
27228
|
if (isUnsafe(portfolioAvgFallPnl))
|
|
27154
27229
|
portfolioAvgFallPnl = null;
|
|
27230
|
+
if (isUnsafe(portfolioStdDev))
|
|
27231
|
+
portfolioStdDev = null;
|
|
27232
|
+
if (isUnsafe(portfolioSortinoRatio))
|
|
27233
|
+
portfolioSortinoRatio = null;
|
|
27234
|
+
if (isUnsafe(portfolioCalmarRatio))
|
|
27235
|
+
portfolioCalmarRatio = null;
|
|
27236
|
+
if (isUnsafe(portfolioRecoveryFactor))
|
|
27237
|
+
portfolioRecoveryFactor = null;
|
|
27238
|
+
if (isUnsafe(portfolioExpectancy))
|
|
27239
|
+
portfolioExpectancy = null;
|
|
27155
27240
|
return {
|
|
27156
27241
|
symbols,
|
|
27157
27242
|
totalSymbols,
|
|
@@ -27160,6 +27245,11 @@ class HeatmapStorage {
|
|
|
27160
27245
|
portfolioTotalTrades,
|
|
27161
27246
|
portfolioAvgPeakPnl,
|
|
27162
27247
|
portfolioAvgFallPnl,
|
|
27248
|
+
portfolioStdDev,
|
|
27249
|
+
portfolioSortinoRatio,
|
|
27250
|
+
portfolioCalmarRatio,
|
|
27251
|
+
portfolioRecoveryFactor,
|
|
27252
|
+
portfolioExpectancy,
|
|
27163
27253
|
};
|
|
27164
27254
|
}
|
|
27165
27255
|
/**
|
|
@@ -27209,6 +27299,7 @@ class HeatmapStorage {
|
|
|
27209
27299
|
`# Portfolio Heatmap: ${strategyName}`,
|
|
27210
27300
|
"",
|
|
27211
27301
|
`**Total Symbols:** ${data.totalSymbols} | **Portfolio PNL:** ${data.portfolioTotalPnl !== null ? str(data.portfolioTotalPnl, "%") : "N/A"} | **Pooled Sharpe:** ${data.portfolioSharpeRatio !== null ? str(data.portfolioSharpeRatio) : "N/A"} | **Total Trades:** ${data.portfolioTotalTrades} | **Avg Peak PNL:** ${data.portfolioAvgPeakPnl !== null ? str(data.portfolioAvgPeakPnl, "%") : "N/A"} | **Avg Max Drawdown PNL:** ${data.portfolioAvgFallPnl !== null ? str(data.portfolioAvgFallPnl, "%") : "N/A"}`,
|
|
27302
|
+
`**Standard Deviation:** ${data.portfolioStdDev !== null ? str(data.portfolioStdDev, "%") : "N/A"} | **Sortino Ratio:** ${data.portfolioSortinoRatio !== null ? str(data.portfolioSortinoRatio) : "N/A"} | **Calmar Ratio:** ${data.portfolioCalmarRatio !== null ? str(data.portfolioCalmarRatio) : "N/A"} | **Recovery Factor:** ${data.portfolioRecoveryFactor !== null ? str(data.portfolioRecoveryFactor) : "N/A"} | **Expectancy:** ${data.portfolioExpectancy !== null ? str(data.portfolioExpectancy, "%") : "N/A"}`,
|
|
27212
27303
|
"",
|
|
27213
27304
|
table,
|
|
27214
27305
|
"",
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -4634,6 +4634,8 @@ interface BacktestStatisticsModel {
|
|
|
4634
4634
|
calmarRatio: number | null;
|
|
4635
4635
|
/** Recovery Factor (totalPnl / max drawdown), null if unsafe. Higher is better. */
|
|
4636
4636
|
recoveryFactor: number | null;
|
|
4637
|
+
/** Per-trade Expectancy (winProb*avgWin + lossProb*avgLoss), null if unsafe. Higher is better. */
|
|
4638
|
+
expectancy: number | null;
|
|
4637
4639
|
}
|
|
4638
4640
|
|
|
4639
4641
|
/**
|
|
@@ -12609,6 +12611,8 @@ interface LiveStatisticsModel {
|
|
|
12609
12611
|
calmarRatio: number | null;
|
|
12610
12612
|
/** Recovery Factor (totalPnl / max drawdown), null if unsafe. Higher is better. */
|
|
12611
12613
|
recoveryFactor: number | null;
|
|
12614
|
+
/** Per-trade Expectancy (winProb*avgWin + lossProb*avgLoss), null if unsafe. Higher is better. */
|
|
12615
|
+
expectancy: number | null;
|
|
12612
12616
|
}
|
|
12613
12617
|
|
|
12614
12618
|
/**
|
|
@@ -12630,6 +12634,16 @@ interface HeatmapStatisticsModel {
|
|
|
12630
12634
|
portfolioAvgPeakPnl: number | null;
|
|
12631
12635
|
/** Trade-count-weighted average fall PNL across all symbols. Closer to 0 is better. */
|
|
12632
12636
|
portfolioAvgFallPnl: number | null;
|
|
12637
|
+
/** Pooled sample standard deviation of returns across all symbols. */
|
|
12638
|
+
portfolioStdDev: number | null;
|
|
12639
|
+
/** Pooled Sortino Ratio over all trades. Same canonical formula as per-symbol. */
|
|
12640
|
+
portfolioSortinoRatio: number | null;
|
|
12641
|
+
/** Pooled Calmar Ratio: pooled compound annual / equity drawdown. Capped at Β±MAX_CALMAR_RATIO. */
|
|
12642
|
+
portfolioCalmarRatio: number | null;
|
|
12643
|
+
/** Pooled Recovery Factor: (equityFinal-1)*100 / equityMaxDrawdown. Capped at Β±MAX_CALMAR_RATIO. */
|
|
12644
|
+
portfolioRecoveryFactor: number | null;
|
|
12645
|
+
/** Pooled Expectancy: winProb*avgWin + lossProb*avgLoss (per-trade expected %). */
|
|
12646
|
+
portfolioExpectancy: number | null;
|
|
12633
12647
|
}
|
|
12634
12648
|
|
|
12635
12649
|
/**
|