backtest-kit 11.6.0 โ 11.7.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 +45 -21
- package/build/index.mjs +45 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ Install the core library and peer dependencies manually. Use this approach when
|
|
|
80
80
|
- ๐ **Pluggable**: Custom data sources (CCXT), persistence (file/Redis), and sizing calculators.
|
|
81
81
|
- ๐๏ธ **Transactional Live Orders**: Broker adapter intercepts every trade mutation before internal state changes โ exchange rejection rolls back the operation atomically.
|
|
82
82
|
- โฐ **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.
|
|
83
|
-
- ๐งช **Tested**:
|
|
83
|
+
- ๐งช **Tested**: 775+ unit/integration tests for validation, recovery, and events.
|
|
84
84
|
- ๐ **Self hosted**: Zero dependency on third-party node_modules or platforms; run entirely in your own environment.
|
|
85
85
|
|
|
86
86
|
## ๐ Supported Order Types
|
|
@@ -1984,7 +1984,7 @@ Python-based (WASI) strategy that uses EMA(9) and EMA(21) crossover signals exec
|
|
|
1984
1984
|
|
|
1985
1985
|
## โ
Tested & Reliable
|
|
1986
1986
|
|
|
1987
|
-
|
|
1987
|
+
775+ tests cover validation, recovery, reports, and events.
|
|
1988
1988
|
|
|
1989
1989
|
## ๐ค Contribute
|
|
1990
1990
|
|
package/build/index.cjs
CHANGED
|
@@ -23978,13 +23978,19 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23978
23978
|
// mark-to-market low); equity then moves to the realized close.
|
|
23979
23979
|
// If equity (at trough or close) goes โค 0 (e.g. leveraged loss < -100%) โ account
|
|
23980
23980
|
// blown, fix DD at 100% and stop walking the curve.
|
|
23981
|
+
// Walk the equity curve in chronological close order. Storage is
|
|
23982
|
+
// newest-first (unshift on addSignal); reverse-storage iteration normally
|
|
23983
|
+
// gives chronological order, but explicitly sorting by closeTimestamp
|
|
23984
|
+
// removes the dependency on insertion-order matching close-order (which
|
|
23985
|
+
// can break under crash recovery, signal backfill, or disk replays).
|
|
23986
|
+
const orderedSignals = [...validSignals].sort((a, b) => a.closeTimestamp - b.closeTimestamp);
|
|
23981
23987
|
let equity = 1;
|
|
23982
23988
|
let peak = 1;
|
|
23983
23989
|
let equityMaxDrawdown = 0;
|
|
23984
23990
|
let blown = false;
|
|
23985
|
-
for (
|
|
23991
|
+
for (const s of orderedSignals) {
|
|
23986
23992
|
// Intra-trade trough โ mark-to-market low while the position was open.
|
|
23987
|
-
const fallPct =
|
|
23993
|
+
const fallPct = s.signal.maxDrawdown?.pnlPercentage;
|
|
23988
23994
|
if (typeof fallPct === "number" && fallPct < 0) {
|
|
23989
23995
|
const trough = equity * (1 + fallPct / 100);
|
|
23990
23996
|
if (trough <= 0) {
|
|
@@ -23997,7 +24003,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23997
24003
|
equityMaxDrawdown = troughDd;
|
|
23998
24004
|
}
|
|
23999
24005
|
// Realized close โ book the final per-trade result.
|
|
24000
|
-
equity *= 1 +
|
|
24006
|
+
equity *= 1 + s.pnl.pnlPercentage / 100;
|
|
24001
24007
|
if (equity <= 0) {
|
|
24002
24008
|
equityMaxDrawdown = 100;
|
|
24003
24009
|
blown = true;
|
|
@@ -25142,14 +25148,20 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
25142
25148
|
// snapshot, โค 0) is applied as a trough BEFORE booking the realized close. Without it
|
|
25143
25149
|
// the curve only steps at close, so a trade that dipped to -18% and recovered to +2%
|
|
25144
25150
|
// would register zero drawdown โ understating DD and inflating Calmar/Recovery.
|
|
25145
|
-
|
|
25146
|
-
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
|
|
25150
|
-
|
|
25151
|
-
|
|
25152
|
-
|
|
25151
|
+
// Walk the equity curve in chronological close order. Reverse-storage
|
|
25152
|
+
// iteration (newest-first storage โ reverse) normally yields chronological
|
|
25153
|
+
// order for live ingest, but explicitly sorting by event.timestamp removes
|
|
25154
|
+
// the dependency on insertion-order matching close-order. This matters
|
|
25155
|
+
// under crash recovery (events reloaded from disk in arbitrary order) and
|
|
25156
|
+
// when ingest latency reorders closed events relative to wall-clock time.
|
|
25157
|
+
const chronological = validClosed
|
|
25158
|
+
.map((e) => ({
|
|
25159
|
+
r: e.pnl,
|
|
25160
|
+
fall: typeof e.fallPnl === "number" ? e.fallPnl : null,
|
|
25161
|
+
ts: e.timestamp,
|
|
25162
|
+
}))
|
|
25163
|
+
.sort((a, b) => a.ts - b.ts)
|
|
25164
|
+
.map(({ r, fall }) => ({ r, fall }));
|
|
25153
25165
|
let equity = 1;
|
|
25154
25166
|
let peak = 1;
|
|
25155
25167
|
let equityMaxDrawdown = 0;
|
|
@@ -27223,11 +27235,18 @@ class HeatmapStorage {
|
|
|
27223
27235
|
let equityFinal = 1;
|
|
27224
27236
|
let blown = false;
|
|
27225
27237
|
if (signals.length > 0) {
|
|
27238
|
+
// Walk the per-symbol equity curve in chronological close order.
|
|
27239
|
+
// Storage is newest-first (unshift on addSignal), but if signals were
|
|
27240
|
+
// ingested out-of-order (e.g. Live + crash recovery loading from disk in
|
|
27241
|
+
// arbitrary order, or a backfill replay), reverse-storage iteration
|
|
27242
|
+
// would misplace peak/trough and silently distort maxDrawdown. Sorting
|
|
27243
|
+
// by closeTimestamp explicitly removes that dependency.
|
|
27244
|
+
const ordered = [...signals].sort((a, b) => a.closeTimestamp - b.closeTimestamp);
|
|
27226
27245
|
let equity = 1;
|
|
27227
27246
|
let peak = 1;
|
|
27228
27247
|
let maxDD = 0;
|
|
27229
|
-
for (
|
|
27230
|
-
const fallPct =
|
|
27248
|
+
for (const s of ordered) {
|
|
27249
|
+
const fallPct = s.signal.maxDrawdown?.pnlPercentage;
|
|
27231
27250
|
if (typeof fallPct === "number" && fallPct < 0) {
|
|
27232
27251
|
const trough = equity * (1 + fallPct / 100);
|
|
27233
27252
|
if (trough <= 0) {
|
|
@@ -27239,7 +27258,7 @@ class HeatmapStorage {
|
|
|
27239
27258
|
if (troughDd > maxDD)
|
|
27240
27259
|
maxDD = troughDd;
|
|
27241
27260
|
}
|
|
27242
|
-
equity *= 1 +
|
|
27261
|
+
equity *= 1 + s.pnl.pnlPercentage / 100;
|
|
27243
27262
|
if (equity <= 0) {
|
|
27244
27263
|
maxDD = 100;
|
|
27245
27264
|
blown = true;
|
|
@@ -27676,23 +27695,28 @@ class HeatmapStorage {
|
|
|
27676
27695
|
let portfolioCertaintyRatio = null;
|
|
27677
27696
|
let portfolioExpectedYearlyReturns = null;
|
|
27678
27697
|
let portfolioTradesPerYear = null;
|
|
27679
|
-
const
|
|
27680
|
-
// Parallel array of intra-trade troughs (โค 0), aligned 1:1 with allReturns,
|
|
27681
|
-
// used for mark-to-market DD in the pooled equity curve below.
|
|
27682
|
-
const allFalls = [];
|
|
27698
|
+
const pooledTrades = [];
|
|
27683
27699
|
let poolFirstPendingAt = Infinity;
|
|
27684
27700
|
let poolLastCloseAt = -Infinity;
|
|
27685
27701
|
for (const signals of this.symbolData.values()) {
|
|
27686
27702
|
for (const s of signals) {
|
|
27687
|
-
allReturns.push(s.pnl.pnlPercentage);
|
|
27688
27703
|
const fall = s.signal.maxDrawdown?.pnlPercentage;
|
|
27689
|
-
|
|
27704
|
+
pooledTrades.push({
|
|
27705
|
+
r: s.pnl.pnlPercentage,
|
|
27706
|
+
fall: typeof fall === "number" ? fall : null,
|
|
27707
|
+
closeAt: s.closeTimestamp,
|
|
27708
|
+
});
|
|
27690
27709
|
if (s.signal.pendingAt < poolFirstPendingAt)
|
|
27691
27710
|
poolFirstPendingAt = s.signal.pendingAt;
|
|
27692
27711
|
if (s.closeTimestamp > poolLastCloseAt)
|
|
27693
27712
|
poolLastCloseAt = s.closeTimestamp;
|
|
27694
27713
|
}
|
|
27695
27714
|
}
|
|
27715
|
+
pooledTrades.sort((a, b) => a.closeAt - b.closeAt);
|
|
27716
|
+
const allReturns = pooledTrades.map((t) => t.r);
|
|
27717
|
+
// Parallel array of intra-trade troughs (โค 0), aligned 1:1 with allReturns,
|
|
27718
|
+
// used for mark-to-market DD in the pooled equity curve below.
|
|
27719
|
+
const allFalls = pooledTrades.map((t) => t.fall);
|
|
27696
27720
|
if (allReturns.length >= MIN_SIGNALS_FOR_RATIOS) {
|
|
27697
27721
|
const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
|
|
27698
27722
|
const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
|
|
@@ -28066,7 +28090,7 @@ class HeatmapStorage {
|
|
|
28066
28090
|
`*Peak Profit PNL / Max Drawdown PNL: extremes โ the best best-case and worst worst-case observed across all trades. Tail behaviour the averages hide.*`,
|
|
28067
28091
|
`*Avg Duration / Avg Win Duration / Avg Loss Duration: mean hold time in minutes (closeTimestamp - pendingAt). Winner-shorter-than-loser is a red flag ("cut winners short, let losers run").*`,
|
|
28068
28092
|
`*Avg Consecutive Win/Loss PNL: average sum of pnlPercentage across consecutive streaks. Pairs with max streak length to show the typical (not worst-case) streak magnitude. Portfolio uses trade-count-weighted mean of per-symbol streak averages โ concatenating streaks across symbols would be meaningless (different markets, different timeframes).*`,
|
|
28069
|
-
`*Max Drawdown: mark-to-market โ both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery.
|
|
28093
|
+
`*Max Drawdown: mark-to-market โ both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery. The pooled curve walks trades chronologically by closeTimestamp; simultaneous cross-symbol drawdowns within the same minute are still serialised (one trade applied at a time), so genuine same-instant tail correlation is not modelled.*`,
|
|
28070
28094
|
`*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
|
|
28071
28095
|
`*IMPORTANT: Per-symbol equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized return / drawdown will be roughly X/100 of the reported figures โ these metrics represent a theoretical upper bound under full allocation.*`,
|
|
28072
28096
|
`*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies โ closer to zero is less bad, positive is profitable.*`,
|
package/build/index.mjs
CHANGED
|
@@ -23958,13 +23958,19 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23958
23958
|
// mark-to-market low); equity then moves to the realized close.
|
|
23959
23959
|
// If equity (at trough or close) goes โค 0 (e.g. leveraged loss < -100%) โ account
|
|
23960
23960
|
// blown, fix DD at 100% and stop walking the curve.
|
|
23961
|
+
// Walk the equity curve in chronological close order. Storage is
|
|
23962
|
+
// newest-first (unshift on addSignal); reverse-storage iteration normally
|
|
23963
|
+
// gives chronological order, but explicitly sorting by closeTimestamp
|
|
23964
|
+
// removes the dependency on insertion-order matching close-order (which
|
|
23965
|
+
// can break under crash recovery, signal backfill, or disk replays).
|
|
23966
|
+
const orderedSignals = [...validSignals].sort((a, b) => a.closeTimestamp - b.closeTimestamp);
|
|
23961
23967
|
let equity = 1;
|
|
23962
23968
|
let peak = 1;
|
|
23963
23969
|
let equityMaxDrawdown = 0;
|
|
23964
23970
|
let blown = false;
|
|
23965
|
-
for (
|
|
23971
|
+
for (const s of orderedSignals) {
|
|
23966
23972
|
// Intra-trade trough โ mark-to-market low while the position was open.
|
|
23967
|
-
const fallPct =
|
|
23973
|
+
const fallPct = s.signal.maxDrawdown?.pnlPercentage;
|
|
23968
23974
|
if (typeof fallPct === "number" && fallPct < 0) {
|
|
23969
23975
|
const trough = equity * (1 + fallPct / 100);
|
|
23970
23976
|
if (trough <= 0) {
|
|
@@ -23977,7 +23983,7 @@ let ReportStorage$a = class ReportStorage {
|
|
|
23977
23983
|
equityMaxDrawdown = troughDd;
|
|
23978
23984
|
}
|
|
23979
23985
|
// Realized close โ book the final per-trade result.
|
|
23980
|
-
equity *= 1 +
|
|
23986
|
+
equity *= 1 + s.pnl.pnlPercentage / 100;
|
|
23981
23987
|
if (equity <= 0) {
|
|
23982
23988
|
equityMaxDrawdown = 100;
|
|
23983
23989
|
blown = true;
|
|
@@ -25122,14 +25128,20 @@ let ReportStorage$9 = class ReportStorage {
|
|
|
25122
25128
|
// snapshot, โค 0) is applied as a trough BEFORE booking the realized close. Without it
|
|
25123
25129
|
// the curve only steps at close, so a trade that dipped to -18% and recovered to +2%
|
|
25124
25130
|
// would register zero drawdown โ understating DD and inflating Calmar/Recovery.
|
|
25125
|
-
|
|
25126
|
-
|
|
25127
|
-
|
|
25128
|
-
|
|
25129
|
-
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25131
|
+
// Walk the equity curve in chronological close order. Reverse-storage
|
|
25132
|
+
// iteration (newest-first storage โ reverse) normally yields chronological
|
|
25133
|
+
// order for live ingest, but explicitly sorting by event.timestamp removes
|
|
25134
|
+
// the dependency on insertion-order matching close-order. This matters
|
|
25135
|
+
// under crash recovery (events reloaded from disk in arbitrary order) and
|
|
25136
|
+
// when ingest latency reorders closed events relative to wall-clock time.
|
|
25137
|
+
const chronological = validClosed
|
|
25138
|
+
.map((e) => ({
|
|
25139
|
+
r: e.pnl,
|
|
25140
|
+
fall: typeof e.fallPnl === "number" ? e.fallPnl : null,
|
|
25141
|
+
ts: e.timestamp,
|
|
25142
|
+
}))
|
|
25143
|
+
.sort((a, b) => a.ts - b.ts)
|
|
25144
|
+
.map(({ r, fall }) => ({ r, fall }));
|
|
25133
25145
|
let equity = 1;
|
|
25134
25146
|
let peak = 1;
|
|
25135
25147
|
let equityMaxDrawdown = 0;
|
|
@@ -27203,11 +27215,18 @@ class HeatmapStorage {
|
|
|
27203
27215
|
let equityFinal = 1;
|
|
27204
27216
|
let blown = false;
|
|
27205
27217
|
if (signals.length > 0) {
|
|
27218
|
+
// Walk the per-symbol equity curve in chronological close order.
|
|
27219
|
+
// Storage is newest-first (unshift on addSignal), but if signals were
|
|
27220
|
+
// ingested out-of-order (e.g. Live + crash recovery loading from disk in
|
|
27221
|
+
// arbitrary order, or a backfill replay), reverse-storage iteration
|
|
27222
|
+
// would misplace peak/trough and silently distort maxDrawdown. Sorting
|
|
27223
|
+
// by closeTimestamp explicitly removes that dependency.
|
|
27224
|
+
const ordered = [...signals].sort((a, b) => a.closeTimestamp - b.closeTimestamp);
|
|
27206
27225
|
let equity = 1;
|
|
27207
27226
|
let peak = 1;
|
|
27208
27227
|
let maxDD = 0;
|
|
27209
|
-
for (
|
|
27210
|
-
const fallPct =
|
|
27228
|
+
for (const s of ordered) {
|
|
27229
|
+
const fallPct = s.signal.maxDrawdown?.pnlPercentage;
|
|
27211
27230
|
if (typeof fallPct === "number" && fallPct < 0) {
|
|
27212
27231
|
const trough = equity * (1 + fallPct / 100);
|
|
27213
27232
|
if (trough <= 0) {
|
|
@@ -27219,7 +27238,7 @@ class HeatmapStorage {
|
|
|
27219
27238
|
if (troughDd > maxDD)
|
|
27220
27239
|
maxDD = troughDd;
|
|
27221
27240
|
}
|
|
27222
|
-
equity *= 1 +
|
|
27241
|
+
equity *= 1 + s.pnl.pnlPercentage / 100;
|
|
27223
27242
|
if (equity <= 0) {
|
|
27224
27243
|
maxDD = 100;
|
|
27225
27244
|
blown = true;
|
|
@@ -27656,23 +27675,28 @@ class HeatmapStorage {
|
|
|
27656
27675
|
let portfolioCertaintyRatio = null;
|
|
27657
27676
|
let portfolioExpectedYearlyReturns = null;
|
|
27658
27677
|
let portfolioTradesPerYear = null;
|
|
27659
|
-
const
|
|
27660
|
-
// Parallel array of intra-trade troughs (โค 0), aligned 1:1 with allReturns,
|
|
27661
|
-
// used for mark-to-market DD in the pooled equity curve below.
|
|
27662
|
-
const allFalls = [];
|
|
27678
|
+
const pooledTrades = [];
|
|
27663
27679
|
let poolFirstPendingAt = Infinity;
|
|
27664
27680
|
let poolLastCloseAt = -Infinity;
|
|
27665
27681
|
for (const signals of this.symbolData.values()) {
|
|
27666
27682
|
for (const s of signals) {
|
|
27667
|
-
allReturns.push(s.pnl.pnlPercentage);
|
|
27668
27683
|
const fall = s.signal.maxDrawdown?.pnlPercentage;
|
|
27669
|
-
|
|
27684
|
+
pooledTrades.push({
|
|
27685
|
+
r: s.pnl.pnlPercentage,
|
|
27686
|
+
fall: typeof fall === "number" ? fall : null,
|
|
27687
|
+
closeAt: s.closeTimestamp,
|
|
27688
|
+
});
|
|
27670
27689
|
if (s.signal.pendingAt < poolFirstPendingAt)
|
|
27671
27690
|
poolFirstPendingAt = s.signal.pendingAt;
|
|
27672
27691
|
if (s.closeTimestamp > poolLastCloseAt)
|
|
27673
27692
|
poolLastCloseAt = s.closeTimestamp;
|
|
27674
27693
|
}
|
|
27675
27694
|
}
|
|
27695
|
+
pooledTrades.sort((a, b) => a.closeAt - b.closeAt);
|
|
27696
|
+
const allReturns = pooledTrades.map((t) => t.r);
|
|
27697
|
+
// Parallel array of intra-trade troughs (โค 0), aligned 1:1 with allReturns,
|
|
27698
|
+
// used for mark-to-market DD in the pooled equity curve below.
|
|
27699
|
+
const allFalls = pooledTrades.map((t) => t.fall);
|
|
27676
27700
|
if (allReturns.length >= MIN_SIGNALS_FOR_RATIOS) {
|
|
27677
27701
|
const portfolioAvg = allReturns.reduce((acc, r) => acc + r, 0) / allReturns.length;
|
|
27678
27702
|
const portfolioVariance = allReturns.reduce((acc, r) => acc + Math.pow(r - portfolioAvg, 2), 0) /
|
|
@@ -28046,7 +28070,7 @@ class HeatmapStorage {
|
|
|
28046
28070
|
`*Peak Profit PNL / Max Drawdown PNL: extremes โ the best best-case and worst worst-case observed across all trades. Tail behaviour the averages hide.*`,
|
|
28047
28071
|
`*Avg Duration / Avg Win Duration / Avg Loss Duration: mean hold time in minutes (closeTimestamp - pendingAt). Winner-shorter-than-loser is a red flag ("cut winners short, let losers run").*`,
|
|
28048
28072
|
`*Avg Consecutive Win/Loss PNL: average sum of pnlPercentage across consecutive streaks. Pairs with max streak length to show the typical (not worst-case) streak magnitude. Portfolio uses trade-count-weighted mean of per-symbol streak averages โ concatenating streaks across symbols would be meaningless (different markets, different timeframes).*`,
|
|
28049
|
-
`*Max Drawdown: mark-to-market โ both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery.
|
|
28073
|
+
`*Max Drawdown: mark-to-market โ both the per-symbol and pooled equity curves apply each trade's worst intra-trade excursion (the lowest unrealized point while the position was open) before booking its realized close, so deep round-trip dips count. It is NOT realized-only (close-to-close); a realized-only curve would understate drawdown and inflate Calmar/Recovery. The pooled curve walks trades chronologically by closeTimestamp; simultaneous cross-symbol drawdowns within the same minute are still serialised (one trade applied at a time), so genuine same-instant tail correlation is not modelled.*`,
|
|
28050
28074
|
`*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
|
|
28051
28075
|
`*IMPORTANT: Per-symbol equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). These metrics ignore the position-sizing subsystem (PositionSize / Kelly / ATR): pnlPercentage is a return on the position's own invested capital, never scaled by account balance. With DCA (commitAverageBuy) the cost basis is the sum of all entries and the entry price is dollar-cost-weighted, so per-trade % is measured against the averaged position, not a fixed stake. If your strategy risks X% of capital per trade, the realized return / drawdown will be roughly X/100 of the reported figures โ these metrics represent a theoretical upper bound under full allocation.*`,
|
|
28052
28076
|
`*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol (avgPnl < 0 or totalPnl < 0). "Higher is better" still applies โ closer to zero is less bad, positive is profitable.*`,
|