backtest-kit 12.0.0 → 12.1.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/build/index.cjs CHANGED
@@ -23935,25 +23935,6 @@ MarkdownFileBase = functoolsKit.makeExtendable(MarkdownFileBase);
23935
23935
  ReportBase = functoolsKit.makeExtendable(ReportBase);
23936
23936
  const ReportWriter = new ReportWriterAdapter();
23937
23937
 
23938
- /**
23939
- * Price-profile metrics derived purely from a series of trade closes — no
23940
- * candles, no exchange queries. Designed for `BacktestMarkdownService`,
23941
- * `LiveMarkdownService` and per-symbol Heat: all three already have a
23942
- * chronological series of `(closeAt, close)` points and nothing else.
23943
- *
23944
- * Conventions
23945
- * -----------
23946
- * - Pressure: fraction of up-moves vs down-moves (frequency).
23947
- * - Strength: fraction of upward magnitude vs total movement.
23948
- * - A divergence between pressure and strength surfaces asymmetry — e.g.
23949
- * frequent shallow up-moves vs rare deep down-moves is "rising on weak
23950
- * buys, falling on strong sells".
23951
- * - Trend: linear regression of log(close) vs days. Slope in %/day,
23952
- * confidence in R². Classification is bivariate (slope × R²): neither
23953
- * axis alone fires, both must agree. Slope threshold is normalised by
23954
- * medianStepSize so the metric self-tunes to the instrument's typical
23955
- * move size.
23956
- */
23957
23938
  /** Minimum samples to surface any price-profile metric. Below this the
23958
23939
  * per-trade step-distribution and the regression are statistically noisy. */
23959
23940
  const MIN_SIGNALS = 10;
@@ -24301,16 +24282,18 @@ let ReportStorage$a = class ReportStorage {
24301
24282
  const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
24302
24283
  // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1) for unbiased estimate.
24303
24284
  // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
24304
- // are too noisy to publish (high chance of spurious ±Sharpe).
24285
+ // are too noisy to publish (high chance of spurious ±Sharpe). When the gate fails the
24286
+ // standard deviation itself is reported as null (NOT 0) so the report doesn't suggest
24287
+ // a flat distribution for a small but variable sample.
24305
24288
  const returns = validSignals.map((s) => s.pnl.pnlPercentage);
24306
24289
  const canComputeRatios = totalSignals >= MIN_SIGNALS_FOR_RATIOS$2;
24307
24290
  const stdDev = canComputeRatios
24308
24291
  ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (totalSignals - 1))
24309
- : 0;
24292
+ : null;
24310
24293
  // Use STDDEV_EPSILON gate (not stdDev > 0) — identical-returns series produce
24311
24294
  // float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously
24312
24295
  // inflates sharpe to astronomical magnitudes (avgPnl / epsilon).
24313
- const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$2
24296
+ const sharpeRatio = canComputeRatios && stdDev !== null && stdDev > STDDEV_EPSILON$2
24314
24297
  ? avgPnl / stdDev
24315
24298
  : null;
24316
24299
  // Annualize only when gate passes; otherwise null.
@@ -24657,24 +24640,38 @@ let ReportStorage$a = class ReportStorage {
24657
24640
  `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
24658
24641
  `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
24659
24642
  "",
24660
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
24661
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24662
- `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$2} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$2} days. Assumes returns are iid autocorrelated strategies are overstated.*`,
24663
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
24664
- `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
24665
- `*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.*`,
24666
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). Capped at ±${MAX_CALMAR_RATIO$2}.*`,
24667
- `*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 and the mark-to-market max drawdown as denominator.*`,
24668
- `*Max Drawdown: mark-to-market the compounded equity curve applies 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.*`,
24669
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
24670
- `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24671
- `*IMPORTANT: 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 portfolio return / drawdown will be roughly X/100 of the reported figures these metrics represent a theoretical upper bound under full allocation.*`,
24672
- `*Trend / Trend Strength / Trend Confidence: linear regression of log(close) vs days across closed-trade prices. "Trend Strength" is the slope in %/day; "Trend Confidence" is R². Classification gates on 0.30 (else "sideways") AND |slope| 0.25 × medianStepSize (else "neutral" a real but weak tilt). Self-tunes to the instrument's typical move size no magic constants on price magnitude.*`,
24673
- `*Buyer / Seller Pressure: fraction of up-moves (resp. down-moves) among decisive close-to-close changes. Frequency-based; flats excluded.*`,
24674
- `*Buyer / Seller Strength: share of upward (resp. downward) absolute movement in total movement. Magnitude-based. A divergence between pressure and strength surfaces asymmetry: "frequent shallow buys, rare deep sells" is a different regime than "rare deep buys, frequent shallow sells".*`,
24675
- `*Pressure Imbalance: buyerStrength sellerStrength [1, +1]. Single signed summary of magnitude bias.*`,
24676
- `*Median Step Size: median |close[i] close[i1]| / close[i−1] across closed trades, in %. Robust to outliers describes the typical close-to-close jump. Used as the scale that normalises the slope-vs-step trend gate. NOTE: this is NOT classical (candle-based) volatility there are no candles between trade closes in the report data; it measures step-distribution at the rate trades close.*`,
24677
- `*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.*`,
24643
+ `*Win Rate: percent of closed signals that ended with per-trade PNL > 0, computed as winning-trade count / (winning-trade count + losing-trade count) × 100 — break-even trades (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistical reliability: noisy below 30 signals (a single streak can shift it 1020 points); stable above 200 signals.*`,
24644
+ `*Average PNL: arithmetic mean of per-trade PNL across every closed signal, computed as Σ per-trade PNL / closed signal count. UNITS: percent per trade. Sign mirrors strategy edge — positive = profitable on average per trade. Sensitive to one whale trade; cross-check with Median PNL to detect skew.*`,
24645
+ `*Total PNL: arithmetic sum of per-trade PNL across every closed signal. UNITS: percent. This is the additive total, NOT the compounded equity return — for the geometrically-compounded variant see Recovery Factor's numerator and Expected Yearly Returns. Useful as a quick scoreboard but ignores volatility drag.*`,
24646
+ `*Standard Deviation Per Trade: sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL. UNITS: percent. Measures volatility of returns. Denominator for per-trade Sharpe Ratio. Below STDDEV_EPSILON = 1e-9 it is treated as zero (identical-returns guard). Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} (variance too noisy on small samples).*`,
24647
+ `*Sharpe Ratio: per-trade Sharpe = Average PNL / Standard Deviation Per Trade (risk-free rate = 0). UNITS: dimensionless ratio. Higher = better risk-adjusted return per trade. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
24648
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √(trades per year), where trades per year = closed signal count × 365 / calendar span in days. UNITS: dimensionless. Null when the closed signal count < ${MIN_SIGNALS_FOR_ANNUALIZATION$2}, OR calendar span < ${MIN_CALENDAR_SPAN_DAYS$2} days, OR raw frequency > ${MAX_TRADES_PER_YEAR$2} (clustered sample, annualisation unreliable). Assumes returns are iid — autocorrelated strategies are overstated.*`,
24649
+ `*Certainty Ratio: mean per-trade PNL over winning trades, divided by the absolute value of the mean per-trade PNL over losing trades. UNITS: dimensionless ratio. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, OR there are no losing trades, OR the absolute mean losing PNL < 1e-9 (float-artifact loss guard).*`,
24650
+ `*Expected Yearly Returns: geometric annualisation of the equity curve: (final equity ^ (trades per year / closed signal count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over all closed signals in chronological close order. UNITS: percent per year. Accounts for volatility drag (unlike a simple Σ × 365 / calendar-span projection). Null under the same closed-signal-count / calendar-span / frequency gates as Annualized Sharpe Ratio, AND null when |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS$2}% (capped numbers mislead). −100% when the equity curve hits ≤ 0 (account blown).*`,
24651
+ `*Avg Peak PNL: arithmetic mean of each closed signal's peak-PNL snapshot — the best mark-to-market PNL recorded while the position was open. Signals that never recorded such a snapshot are excluded from both numerator and denominator (no zero dilution). UNITS: percent. Describes the typical best-case unrealised excursion during the position's lifetime, not the realised close. NOT gated by MIN_SIGNALS computed whenever at least one signal carries the snapshot; null only if no signal carries it.*`,
24652
+ `*Avg Max Drawdown PNL: arithmetic mean of each closed signal's trough-PNL snapshot the worst mark-to-market PNL recorded while the position was open. Signals that never recorded such a snapshot are excluded (no zero dilution). UNITS: percent (negative for losing excursions). Describes the typical worst-case unrealised PNL during the position's lifetime; closer to 0 is better. NOT gated by MIN_SIGNALS — computed whenever at least one signal carries the snapshot.*`,
24653
+ `*Sortino Ratio: Average PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / closed signal count ) (canonical Sortino 1991: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, OR there are no losing trades (downside is undefined — flawless ≠ infinitely good), OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
24654
+ `*Calmar Ratio: Expected Yearly Returns divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$2}. The denominator is the max drawdown of the compounded equity curve: each trade's worst intra-trade excursion (the trough-PNL snapshot, 0) is applied before booking its realised close, so a position that dipped to −18% and recovered to +2% contributes a real drawdown rather than zero. UNITS: dimensionless. Rule of thumb: below 0.5 poor, 0.5–1.0 acceptable, above 1.0 strong. Null when Expected Yearly Returns is null OR the equity max drawdown 0.*`,
24655
+ `*Recovery Factor: (final equity 1) × 100 divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$2}. The numerator is the compounded total return — NOT the arithmetic Total PNL shown above; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = profit doesn't cover drawdown; above 3.0 = good. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, the equity curve blew up (account ≤ 0), or the equity max drawdown ≤ 0.*`,
24656
+ `*Expectancy: per-trade expected value = (winning-trade count / closed signal count) × mean winning PNL + (losing-trade count / closed signal count) × mean losing PNL. Break-even trades contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24657
+ `*Median PNL: median of per-trade PNL across all closed signals (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers — comparing it with Average PNL reveals distribution skew. A large gap (e.g. avg +1.5%, median +0.2%) means one or two trades carry the arithmetic mean. NOT gated by MIN_SIGNALS computed whenever the closed signal count ≥ 1.*`,
24658
+ `*Avg Duration: arithmetic mean of (close-timepending-time) / 60_000 over all closed signals. UNITS: minutes (synchronised with the strategy's estimated-minutes setting). Describes the typical position hold time. NOT gated by MIN_SIGNALS — computed whenever the closed signal count ≥ 1.*`,
24659
+ `*Avg Win Duration: arithmetic mean of (close-time pending-time) / 60_000 restricted to winning trades (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning trades (NOT gated by MIN_SIGNALS — computed at any winning-trade count ≥ 1). Pair with Avg Loss Duration to detect the classic asymmetry "let winners run, cut losers short" (Win Duration > Loss Duration is healthy) versus the inverse red flag "cut winners short, let losers run".*`,
24660
+ `*Avg Loss Duration: arithmetic mean of (close-time pending-time) / 60_000 restricted to losing trades (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing trades (NOT gated by MIN_SIGNALS).*`,
24661
+ `*Avg Consecutive Win PNL: a "win streak" is a run of consecutive trades with per-trade PNL > 0 bounded by either a losing or break-even trade; this metric sums per-trade PNL within each streak and then averages those sums. Trades are walked in chronological close order. UNITS: percent per streak. Higher = winning streaks are typically bigger. Null only when there is not a single complete win streak in the stored history (NOT gated by MIN_SIGNALS).*`,
24662
+ `*Avg Consecutive Loss PNL: a "loss streak" is a run of consecutive trades with per-trade PNL < 0 bounded by either a winning or break-even trade; this metric sums per-trade PNL within each streak and then averages those sums. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. Null only when there is not a single complete loss streak (NOT gated by MIN_SIGNALS).*`,
24663
+ `*Trend: classification computed FROM CLOSING PRICES OF CLOSED SIGNALS (one point per closed trade = the price at which it closed, with its close-timestamp — no candles, no order book, no tick stream). Bivariate gate: "sideways" when R² < 0.30 (regression too weak to call any direction). Otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size (slope detectable but smaller than the typical daily step). Otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate — never a single-axis if on a magic-constant slope magnitude. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS$2}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected.*`,
24664
+ `*Trend Strength: ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED SIGNALS (one point per closed trade in chronological order — no candles). UNITS: percent per day (small-slope approximation: log-return ≈ percent-return). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator at signal entry — a static fit over the entire stored history at report time. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} or the calendar span is degenerate.*`,
24665
+ `*Trend Confidence (R²): coefficient of determination of the same log-price regression that produces Trend Strength, computed on log(close) — NOT on absolute close. UNITS: dimensionless in [0, 1]. High R² = log-price moves linearly with time (clean trend); low R² = the series is noisy regardless of slope sign. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} or the calendar span is degenerate.*`,
24666
+ `*Buyer Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. (count of i where close[i] > close[i−1]) / (count of decisive moves), where "decisive" excludes flats (close[i] == close[i−1]). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — backtest data has no order book; "buyer" labels only the SIGN of the close-to-close return. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24667
+ `*Seller Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Seller Pressure = 1 − Buyer Pressure (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — labels only the SIGN of the close-to-close return. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24668
+ `*Buyer Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Σ |close[i] − close[i−1]| / close[i−1] over up-moves (close[i] > close[i−1]), divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pressure (count of up-moves) and Buyer Strength (sum of up-magnitudes) use the SAME close-to-close return series; only count vs magnitude differs. A divergence between them (e.g. Pressure 0.70 with Strength 0.45) means "many small up-moves, fewer but larger down-moves" — a regime asymmetry frequency alone hides. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24669
+ `*Seller Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Σ |close[i] − close[i−1]| / close[i−1] over down-moves (close[i] < close[i−1]), divided by the same sum over ALL decisive moves. By construction Seller Strength = 1 − Buyer Strength (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24670
+ `*Pressure Imbalance: DIFFERENCE, not ratio: Buyer Strength − Seller Strength, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Equivalent to (2 × Buyer Strength − 1). UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias (positive = bullish bias on magnitude); absolute value = how lopsided. Single signed scalar that compresses the strength pair into one number. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24671
+ `*Median Step Size: median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED SIGNALS — "step" = consecutive closes of CLOSED TRADES (NOT ticks, NOT bars of any timeframe — the report has no candle access). UNITS: percent, normalised by price → directly comparable across symbols at any price level. Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24672
+ `*General reliability note: per-trade ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS$2} closed signals because the underlying variance estimates are too noisy. Annualised metrics (Annualized Sharpe, Expected Yearly Returns, Calmar) additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS$2} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR$2} per year. 100+ signals are needed for statistical reliability of the ratios; annualised metrics assume the observed frequency and market regime persist year-round.*`,
24673
+ `*IMPORTANT: Total PNL and the equity-curve metrics (Expected Yearly Returns, Calmar, Recovery, and the equity max drawdown that feeds them) all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem (PositionSize / Kelly / ATR): per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging, 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. If your strategy risks X% of capital per trade, the realised portfolio return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
24674
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing strategy (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
24678
24675
  ].join("\n");
24679
24676
  }
24680
24677
  /**
@@ -25362,14 +25359,16 @@ let ReportStorage$9 = class ReportStorage {
25362
25359
  const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
25363
25360
  // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1).
25364
25361
  // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
25365
- // are too noisy to publish (high chance of spurious ±Sharpe).
25362
+ // are too noisy to publish (high chance of spurious ±Sharpe). When the gate fails the
25363
+ // standard deviation itself is reported as null (NOT 0) so the report doesn't suggest
25364
+ // a flat distribution for a small but variable sample.
25366
25365
  const canComputeRatios = returns.length >= MIN_SIGNALS_FOR_RATIOS$1;
25367
25366
  const stdDev = canComputeRatios
25368
25367
  ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (returns.length - 1))
25369
- : 0;
25368
+ : null;
25370
25369
  // STDDEV_EPSILON guard — protects against float-artifact stdDev from identical
25371
25370
  // returns producing spuriously astronomical sharpe.
25372
- const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$1
25371
+ const sharpeRatio = canComputeRatios && stdDev !== null && stdDev > STDDEV_EPSILON$1
25373
25372
  ? avgPnl / stdDev
25374
25373
  : null;
25375
25374
  // Annualize only when gate passes; otherwise null.
@@ -25720,24 +25719,38 @@ let ReportStorage$9 = class ReportStorage {
25720
25719
  `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
25721
25720
  `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
25722
25721
  "",
25723
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
25724
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
25725
- `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$1} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$1} days. Assumes returns are iid autocorrelated strategies are overstated.*`,
25726
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
25727
- `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
25728
- `*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.*`,
25729
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). Capped at ±${MAX_CALMAR_RATIO$1}.*`,
25730
- `*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 and the mark-to-market max drawdown as denominator.*`,
25731
- `*Max Drawdown: mark-to-market the compounded equity curve applies 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.*`,
25732
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
25733
- `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
25734
- `*IMPORTANT: 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 portfolio return / drawdown will be roughly X/100 of the reported figures these metrics represent a theoretical upper bound under full allocation.*`,
25735
- `*Trend / Trend Strength / Trend Confidence: linear regression of log(close) vs days across closed-trade prices. "Trend Strength" is the slope in %/day; "Trend Confidence" is R². Classification gates on 0.30 (else "sideways") AND |slope| 0.25 × medianStepSize (else "neutral" a real but weak tilt). Self-tunes to the instrument's typical move size no magic constants on price magnitude.*`,
25736
- `*Buyer / Seller Pressure: fraction of up-moves (resp. down-moves) among decisive close-to-close changes. Frequency-based; flats excluded.*`,
25737
- `*Buyer / Seller Strength: share of upward (resp. downward) absolute movement in total movement. Magnitude-based. A divergence between pressure and strength surfaces asymmetry: "frequent shallow buys, rare deep sells" is a different regime than "rare deep buys, frequent shallow sells".*`,
25738
- `*Pressure Imbalance: buyerStrength sellerStrength [1, +1]. Single signed summary of magnitude bias.*`,
25739
- `*Median Step Size: median |close[i] close[i−1]| / close[i1] across closed trades, in %. Robust to outliers describes the typical close-to-close jump. Used as the scale that normalises the slope-vs-step trend gate. NOTE: this is NOT classical (candle-based) volatility there are no candles between trade closes in the report data; it measures step-distribution at the rate trades close.*`,
25740
- `*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.*`,
25722
+ `*Win Rate: percent of closed events that ended with per-trade PNL > 0, computed as winning-event count / (winning-event count + losing-event count) × 100 — break-even closed events (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistical reliability: noisy below 30 closed events (a single streak can shift it 1020 points); stable above 200.*`,
25723
+ `*Average PNL: arithmetic mean of per-trade PNL across every closed event, computed as Σ per-trade PNL / closed event count. UNITS: percent per trade. Sign mirrors strategy edge — positive = profitable on average per trade. Sensitive to one whale trade; cross-check with Median PNL to detect skew.*`,
25724
+ `*Total PNL: arithmetic sum of per-trade PNL across every closed event. UNITS: percent. This is the additive total, NOT the compounded equity return — for the geometrically-compounded variant see Recovery Factor's numerator and Expected Yearly Returns. Useful as a quick scoreboard but ignores volatility drag.*`,
25725
+ `*Standard Deviation Per Trade: sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL across closed events. UNITS: percent. Measures volatility of realised returns. Denominator for per-trade Sharpe Ratio. Below STDDEV_EPSILON = 1e-9 it is treated as zero (identical-returns guard). Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25726
+ `*Sharpe Ratio: per-trade Sharpe = Average PNL / Standard Deviation Per Trade (risk-free rate = 0). UNITS: dimensionless ratio. Higher = better risk-adjusted return per trade. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
25727
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √(trades per year), where trades per year = closed event count × 365 / calendar span in days. UNITS: dimensionless. Null when the closed event count < ${MIN_SIGNALS_FOR_ANNUALIZATION$1}, OR calendar span < ${MIN_CALENDAR_SPAN_DAYS$1} days, OR raw frequency > ${MAX_TRADES_PER_YEAR$1}. Assumes returns are iid autocorrelated strategies are overstated.*`,
25728
+ `*Certainty Ratio: mean per-trade PNL over winning closed events, divided by the absolute value of the mean per-trade PNL over losing closed events. UNITS: dimensionless ratio. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, OR there are no losing events, OR the absolute mean losing PNL < 1e-9 (float-artifact loss guard).*`,
25729
+ `*Expected Yearly Returns: geometric annualisation of the equity curve: (final equity ^ (trades per year / closed event count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over all closed events in chronological close order. UNITS: percent per year. Accounts for volatility drag. Null under the same closed-event-count / calendar-span / frequency gates as Annualized Sharpe Ratio, AND null when |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS$1}%. −100% when the equity curve hits ≤ 0 (account blown).*`,
25730
+ `*Avg Peak PNL: arithmetic mean of each closed event's peak-PNL snapshot — the best mark-to-market PNL recorded while the position was open. Closed events that never recorded such a snapshot are excluded from both numerator and denominator (no zero dilution). UNITS: percent. Describes the typical best-case unrealised PNL during the position's lifetime, not the realised close. NOT gated by MIN_SIGNALS computed whenever at least one closed event carries the snapshot.*`,
25731
+ `*Avg Max Drawdown PNL: arithmetic mean of each closed event's trough-PNL snapshot the worst mark-to-market PNL recorded while the position was open. Closed events that never recorded such a snapshot are excluded (no zero dilution). UNITS: percent (negative for losing excursions). Describes the typical worst-case unrealised PNL during the position's lifetime; closer to 0 is better. NOT gated by MIN_SIGNALS — computed whenever at least one closed event carries the snapshot.*`,
25732
+ `*Sortino Ratio: Average PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / closed event count ) (canonical Sortino 1991: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, OR there are no losing events, OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
25733
+ `*Calmar Ratio: Expected Yearly Returns divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$1}. The denominator is the max drawdown of the compounded equity curve: each closed event's worst intra-trade excursion (the trough-PNL snapshot, 0) is applied before booking its realised close, so a position that dipped to −18% and recovered to +2% contributes a real drawdown rather than zero. UNITS: dimensionless. Rule of thumb: below 0.5 poor, 0.5–1.0 acceptable, above 1.0 strong. Null when Expected Yearly Returns is null OR the equity max drawdown 0.*`,
25734
+ `*Recovery Factor: (final equity 1) × 100 divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$1}. The numerator is the compounded total return — NOT the arithmetic Total PNL shown above; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = profit doesn't cover drawdown; above 3.0 = good. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, the equity curve blew up (account ≤ 0), or the equity max drawdown ≤ 0.*`,
25735
+ `*Expectancy: per-trade expected value = (winning-event count / closed event count) × mean winning PNL + (losing-event count / closed event count) × mean losing PNL. Break-even events contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25736
+ `*Median PNL: median of per-trade PNL across all closed events (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers — comparing it with Average PNL reveals distribution skew. A large gap (e.g. avg +1.5%, median +0.2%) means one or two events carry the arithmetic mean. NOT gated by MIN_SIGNALS computed whenever the closed event count ≥ 1.*`,
25737
+ `*Avg Duration: arithmetic mean of (close-timepending-time) / 60_000 over all closed events. UNITS: minutes. Describes the typical position hold time. Closed events that have no pending timestamp fall back to using their own close timestamp (yielding a zero-duration contribution). NOT gated by MIN_SIGNALS — computed whenever the closed event count ≥ 1.*`,
25738
+ `*Avg Win Duration: arithmetic mean of (close-timepending-time) / 60_000 restricted to winning closed events (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning events (NOT gated by MIN_SIGNALS). Pair with Avg Loss Duration to detect the classic asymmetry "let winners run, cut losers short" versus the inverse "cut winners short, let losers run".*`,
25739
+ `*Avg Loss Duration: arithmetic mean of (close-time pending-time) / 60_000 restricted to losing closed events (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing events (NOT gated by MIN_SIGNALS).*`,
25740
+ `*Avg Consecutive Win PNL: a "win streak" is a run of consecutive closed events with per-trade PNL > 0 bounded by either a losing or break-even event; this metric sums per-trade PNL within each streak and then averages those sums. Events are walked in chronological close order. UNITS: percent per streak. Higher = winning streaks are typically bigger. Null only when there is not a single complete win streak (NOT gated by MIN_SIGNALS).*`,
25741
+ `*Avg Consecutive Loss PNL: a "loss streak" is a run of consecutive closed events with per-trade PNL < 0 bounded by either a winning or break-even event; this metric sums per-trade PNL within each streak and then averages those sums. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. Null only when there is not a single complete loss streak (NOT gated by MIN_SIGNALS).*`,
25742
+ `*Trend: classification computed FROM CLOSING PRICES OF CLOSED EVENTS (one point per closed event = the price at which it closed, with its close-timestamp; the live mode ingests sub-minute events of other kinds but the price profile uses ONLY closed-event prices — no candles, no order book, no tick stream). Bivariate gate: "sideways" when R² < 0.30. Otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size. Otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate — never a single-axis if on a magic-constant slope magnitude. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS$1}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected.*`,
25743
+ `*Trend Strength: ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED EVENTS in chronological order. UNITS: percent per day (small-slope approximation). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator at signal entry — a static fit over the entire stored history at report time. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} or the calendar span is degenerate.*`,
25744
+ `*Trend Confidence (R²): coefficient of determination of the same log-price regression that produces Trend Strength, computed on log(close) — NOT on absolute close. UNITS: dimensionless in [0, 1]. High R² = log-price moves linearly with time (clean trend); low R² = the series is noisy regardless of slope sign. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} or the calendar span is degenerate.*`,
25745
+ `*Buyer Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. (count of i where close[i] > close[i−1]) / (count of decisive moves), where "decisive" excludes flats. UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — the live ingest does not separate buy/sell side; "buyer" labels only the SIGN of the close-to-close return. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25746
+ `*Seller Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Seller Pressure = 1 − Buyer Pressure (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25747
+ `*Buyer Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. Σ |close[i] − close[i−1]| / close[i−1] over up-moves, divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pressure (count of up-moves) and Buyer Strength (sum of up-magnitudes) use the SAME close-to-close return series; only count vs magnitude differs. A divergence between them (e.g. Pressure 0.70 with Strength 0.45) means "many small up-moves, fewer but larger down-moves". Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25748
+ `*Seller Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. Σ |close[i] − close[i−1]| / close[i−1] over down-moves, divided by the same sum over ALL decisive moves. By construction Seller Strength = 1 − Buyer Strength (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25749
+ `*Pressure Imbalance: DIFFERENCE, not ratio: Buyer Strength − Seller Strength, computed FROM CLOSING PRICES OF CLOSED EVENTS. Equivalent to (2 × Buyer Strength − 1). UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias (positive = bullish bias on magnitude); absolute value = how lopsided. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25750
+ `*Median Step Size: median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED EVENTS — "step" = consecutive closed-event prices (NOT ticks, NOT bars; the report has no candle access even though the live mode ingests sub-minute events — they are not used here). UNITS: percent, normalised by price → directly comparable across symbols at any price level. Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25751
+ `*General reliability note: per-trade ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS$1} closed events. Annualised metrics additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS$1} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR$1} per year. 100+ closed events are needed for statistical reliability of the ratios.*`,
25752
+ `*IMPORTANT: Total PNL and the equity-curve metrics (Expected Yearly Returns, Calmar, Recovery, and the equity max drawdown that feeds them) all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem: per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging the cost basis is the sum of all entries and the entry price is dollar-cost-weighted. If your strategy risks X% of capital per trade, the realised portfolio return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
25753
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing strategy (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
25741
25754
  ].join("\n");
25742
25755
  }
25743
25756
  /**
@@ -26292,7 +26305,14 @@ let ReportStorage$8 = class ReportStorage {
26292
26305
  `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
26293
26306
  `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`,
26294
26307
  "",
26295
- `*Activation / Cancellation rates are computed over scheduled signals whose outcome (opened or cancelled) is also in the buffer matched by signalId. "Scheduled signals (raw)" above is the unmatched count and may include records whose outcome has not yet arrived or was evicted from the buffer.*`
26308
+ `*Total events: integer count of every scheduled / opened / cancelled event in the ring buffer (capped at CC_MAX_SCHEDULE_MARKDOWN_ROWS, newest-first; oldest entries are dropped past the cap). UNITS: count.*`,
26309
+ `*Scheduled signals (raw): count of events whose action is "scheduled" in the buffer. UNITS: count. NOTE: this is the unmatched raw count — it may include records whose outcome (opened/cancelled) has not yet arrived or was evicted from the buffer. Activation / Cancellation rates do NOT use this denominator (see below).*`,
26310
+ `*Opened signals: count of events whose action is "opened" in the buffer. UNITS: count.*`,
26311
+ `*Cancelled signals: count of events whose action is "cancelled" in the buffer. UNITS: count.*`,
26312
+ `*Activation rate: matched-opened count / matched-resolved count × 100, where matched-opened = number of "opened" events whose signal id also appears among the buffer's "scheduled" events, matched-cancelled = same for "cancelled" events, and matched-resolved = matched-opened + matched-cancelled. The signal-id match guarantees the numerator and denominator come from the SAME population — a sliding window of CC_MAX_SCHEDULE_MARKDOWN_ROWS can otherwise drop a "scheduled" record before its outcome arrives, inflating rates above 100% or firing one rate without the other. UNITS: percent in [0, 100]. Null when matched-resolved = 0 (no matched outcomes in the buffer).*`,
26313
+ `*Cancellation rate: matched-cancelled count / matched-resolved count × 100 — same signal-id-matched denominator as Activation rate. UNITS: percent in [0, 100]. Null when matched-resolved = 0. By construction Activation rate + Cancellation rate = 100% (or both null together).*`,
26314
+ `*Average activation time: arithmetic mean of duration over events whose action is "opened" AND whose duration is a number. Events without a numeric duration are excluded from both numerator and denominator (no zero dilution). UNITS: minutes. Null when no "opened" event carries a numeric duration. The duration on an opened event represents wait time from the moment the signal was scheduled until the moment it activated (became pending).*`,
26315
+ `*Average wait time (cancelled): arithmetic mean of duration over events whose action is "cancelled" AND whose duration is a number. Same exclusion rule as Average activation time. UNITS: minutes. Null when no "cancelled" event carries a numeric duration. The duration on a cancelled event represents wait time from the moment the signal was scheduled until cancellation.*`,
26296
26316
  ].join("\n");
26297
26317
  }
26298
26318
  /**
@@ -26775,8 +26795,6 @@ class PerformanceStorage {
26775
26795
  "",
26776
26796
  summaryTable,
26777
26797
  "",
26778
- "**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type.",
26779
- "",
26780
26798
  `**Total events:** ${stats.totalEvents}`,
26781
26799
  `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
26782
26800
  `**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
@@ -26785,6 +26803,17 @@ class PerformanceStorage {
26785
26803
  "",
26786
26804
  percentages.join("\n"),
26787
26805
  "",
26806
+ `*Total events: integer count of every performance event in the ring buffer (capped at CC_MAX_PERFORMANCE_MARKDOWN_ROWS, newest-first; oldest entries are dropped past the cap). UNITS: count.*`,
26807
+ `*Total execution time: arithmetic sum of every event's duration in the buffer. UNITS: milliseconds.*`,
26808
+ `*Number of metric types: count of distinct metric-type labels present in the buffer. UNITS: count.*`,
26809
+ `*Time Distribution (per metric): the metric's Total Duration divided by overall Total Execution Time × 100; rendered as a percent next to that metric's total duration in milliseconds. Guard: when the overall execution time is 0 (all-instant operations) the percent is forced to 0 instead of NaN. UNITS: percent of total execution time.*`,
26810
+ `*Count (table column): integer number of events of this metric type in the buffer.*`,
26811
+ `*Total Duration (table column): sum of every event's duration for that metric type. UNITS: milliseconds.*`,
26812
+ `*Avg Duration (table column): Total Duration divided by Count for that metric type. UNITS: milliseconds.*`,
26813
+ `*Min / Max Duration (table columns): smallest / largest single-event duration in the metric-type bucket. UNITS: milliseconds.*`,
26814
+ `*Std Dev (table column): sample standard deviation (Bessel-corrected, N−1 denominator) of single-event durations for that metric type. UNITS: milliseconds. Zero when the bucket has only one event (no variance defined).*`,
26815
+ `*Median / P95 / P99 (table columns): percentiles of the sorted single-event durations using linear interpolation between adjacent ranks — equivalent to numpy.percentile with the default linear method. P95 / P99 represent the 95th / 99th percentile response times (tail latency). UNITS: milliseconds. Buckets of size 0 fall back to 0; size 1 returns the single value for every percentile.*`,
26816
+ `*Avg / Min / Max Wait Time (table columns): wait time = (event's timestamp − the previous event's timestamp) for events where a previous-timestamp value is recorded; this captures the interval between consecutive events of the same metric type. Mean / smallest / largest of those gaps. UNITS: milliseconds. All zero when no event in the bucket carries a previous-timestamp value.*`,
26788
26817
  ].join("\n");
26789
26818
  }
26790
26819
  /**
@@ -27244,7 +27273,17 @@ let ReportStorage$7 = class ReportStorage {
27244
27273
  "",
27245
27274
  await this.getPnlTable(pnlColumns),
27246
27275
  "",
27247
- "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
27276
+ "",
27277
+ `*Optimization Metric: the name of the per-strategy statistic that this walker run ranked strategies by. The "Best ${results.metric}" value below is the largest value of that statistic among all tested strategies (or, if the statistic is one for which "lower is better", the smallest); the strategy that produced it is reported as Best Strategy. Strategies whose ${results.metric} is N/A are skipped from "best" selection — a missing value never wins.*`,
27278
+ `*Strategies Tested: integer count of distinct strategies that were run during this walker comparison. UNITS: count.*`,
27279
+ `*Total Signals (best strategy): integer count of closed signals produced by the best strategy during this walker run.*`,
27280
+ `*Sortino Ratio (best strategy summary): the best strategy's Average PNL divided by downside deviation (risk-free rate = 0). Downside deviation = √( Σ min(0, per-trade PNL)² / closed signal count ) — squared sums computed over the strategy's closed signals and divided by the total signal count (canonical Sortino 1991, MAR = 0). UNITS: dimensionless. N/A when the best strategy has fewer than 10 closed signals, when it has no losing trades, or when downside deviation is below a float-artifact threshold (~1e-9). Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong.*`,
27281
+ `*Calmar Ratio (best strategy summary): the best strategy's Expected Yearly Returns divided by its equity max drawdown, clamped to ±1000. Expected Yearly Returns is the geometric annualisation of the strategy's compounded equity curve; the equity max drawdown is the mark-to-market max drawdown of that same curve (each trade's worst intra-trade excursion is applied as a trough before booking the realised close, so deep round-trip dips count rather than only close-to-close drops). UNITS: dimensionless. N/A when Expected Yearly Returns is N/A (e.g. too few signals, calendar span too short, or trade frequency too clustered to extrapolate) or when the equity max drawdown ≤ 0. Rule of thumb: below 0.5 poor, above 1.0 strong.*`,
27282
+ `*Recovery Factor (best strategy summary): the best strategy's (final equity − 1) × 100 divided by its equity max drawdown, clamped to ±1000. The numerator is the strategy's compounded total return (the equity curve's end value minus its start, in percent) — NOT the arithmetic sum of per-trade PNLs; mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown described under Calmar Ratio. UNITS: dimensionless. N/A when the best strategy has fewer than 10 closed signals, the strategy blew its account (equity ≤ 0), or the equity max drawdown ≤ 0. Rule of thumb: below 1.0 means profit doesn't cover drawdown, above 3.0 is good.*`,
27283
+ `*Top Strategies Comparison (table): up to N strategies (newest-best first) ranked by the optimization metric, where N is the configured top-N limit for this report. Each row shows that strategy's full statistical summary through the configured comparison columns.*`,
27284
+ `*All Signals (PNL Table): every closed signal produced by every tested strategy during the run, in storage order (newest first). Each row shows one closed trade — entry / exit prices, realised PNL percentage and cost, peak / trough excursions, close reason, durations — through the configured PNL columns.*`,
27285
+ "",
27286
+ "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better). The per-strategy ratios shown above (Sortino, Calmar, Recovery) are forwarded verbatim from each strategy's own backtest statistics — Walker only ranks and surfaces those values, it does not recompute them. Their formulas, null-conditions and statistical gates are spelled out in the individual legend entries above."
27248
27287
  ].join("\n");
27249
27288
  }
27250
27289
  /**
@@ -28511,30 +28550,69 @@ class HeatmapStorage {
28511
28550
  "",
28512
28551
  table,
28513
28552
  "",
28514
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
28515
- `*Pooled Sharpe: Sharpe computed over all trades across symbols treated as one sample. NOT a Markowitz portfolio Sharpe ignores cross-symbol correlations and capital allocation. N/A unless ≥${MIN_SIGNALS_FOR_RATIOS} pooled trades.*`,
28516
- `*Annualized Sharpe: per-trade Sharpe × √tradesPerYear. N/A unless the underlying Sharpe and tradesPerYear are both available (≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals, span ≥${MIN_CALENDAR_SPAN_DAYS} days, raw frequency ≤${MAX_TRADES_PER_YEAR}). Assumes returns are iid autocorrelated strategies are overstated.*`,
28517
- `*Certainty Ratio: avgWin / |avgLoss|. Below 1.0 means average loss exceeds average win. Above 1.5 is considered good. N/A when no losing trades or |avgLoss| is sub-epsilon.*`,
28518
- `*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS}% values above the cap return N/A.*`,
28519
- `*Trades Per Year: observed trade frequency extrapolated to one year (signals × 365 / calendarSpanDays). N/A when too few signals or too short a calendar span; also null when the raw frequency exceeds ${MAX_TRADES_PER_YEAR} (too clustered for reliable annualization).*`,
28520
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals per symbol.*`,
28521
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
28522
- `*Profit Factor: below 1.0 means strategy is losing overall. Above 1.5 is considered good.*`,
28523
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals per symbol and span ≥${MIN_CALENDAR_SPAN_DAYS} days. Capped at ±${MAX_CALMAR_RATIO}.*`,
28524
- `*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 and the mark-to-market max drawdown as denominator.*`,
28525
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
28526
- `*Median PNL: middle value of the pnl distribution. Robust to outliers; compare to Average PNL a large gap signals a skewed distribution (e.g. one whale trade dragging the mean).*`,
28527
- `*Avg Peak PNL / Avg Max Drawdown PNL: mean of per-trade _peak.pnlPercentage / _fall.pnlPercentage. Higher avg-peak with deeper avg-drawdown means strategy needs to tolerate bigger swings to capture the upside.*`,
28528
- `*Peak Profit PNL / Max Drawdown PNL: extremes the best best-case and worst worst-case observed across all trades. Tail behaviour the averages hide.*`,
28529
- `*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").*`,
28530
- `*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).*`,
28531
- `*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.*`,
28532
- `*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
28533
- `*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.*`,
28534
- `*Trend / Trend %/d / Trend (per-symbol only): linear regression of log(close) vs days across that symbol's closed-trade prices. Classification gates on 0.30 AND |slope| 0.25 × medianStepSize self-tunes to the instrument. Not aggregated portfolio-wide (price series across symbols are not comparable).*`,
28535
- `*Buyer / Seller Pres (per-symbol only): fraction of up-moves (resp. down-moves) among decisive close-to-close changes for that symbol. Buyer / Seller Str: magnitude share. Pres Imb: buyerStrength sellerStrength [−1, +1].*`,
28536
- `*Median Step (per-symbol only): typical |close[i] close[i−1]| / close[i−1] across closed trades, in %. Robust to outliers. NOT classical volatility there are no candles between closes in the report data.*`,
28537
- `*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.*`,
28553
+ `*Total Symbols: count of distinct symbol buckets currently tracked in this storage. Each bucket holds at most CC_MAX_HEATMAP_MARKDOWN_ROWS closed signals (newest-first ring buffer); when capacity is reached the oldest signal is dropped. UNITS: integer count of symbols.*`,
28554
+ `*Portfolio PNL: arithmetic sum of per-trade PNL across every closed signal of every tracked symbol (Σ per-symbol Total PNL over symbols whose Total PNL is non-null). UNITS: percent. Additive total — NOT the compounded equity return. Useful as a quick scoreboard; ignores volatility drag and cross-symbol diversification.*`,
28555
+ `*Pooled Sharpe: pooled mean per-trade PNL divided by the pooled sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL, computed over the POOLED set of every closed signal of every symbol treated as one sample. UNITS: dimensionless ratio. NOT a Markowitz portfolio Sharpe — ignores cross-symbol correlations and capital allocation. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS} OR pooled standard deviation ≤ 1e-9. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong.*`,
28556
+ `*Annualized Sharpe (portfolio): Pooled Sharpe × √(portfolio Trades Per Year). UNITS: dimensionless. Null when Pooled Sharpe is null OR portfolio Trades Per Year is null OR ≤ 0. Assumes returns are iid autocorrelated strategies are overstated.*`,
28557
+ `*Certainty Ratio (portfolio): pooledAvgWin / |pooledAvgLoss| where pooledAvgWin = mean over pooled winning closed signals and pooledAvgLoss = mean over pooled losing closed signals. UNITS: dimensionless. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when pooled N < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades in the pool, OR |pooledAvgLoss| < 1e-9.*`,
28558
+ `*Expected Yearly Returns (portfolio): geometric annualisation of the pooled equity curve: (pooled final equity ^ (portfolio Trades Per Year / pooled trade count) 1) × 100, where the pooled equity curve walks every closed signal of every symbol chronologically by close-time and compounds (1 + per-trade PNL / 100). UNITS: percent per year. Null when the pooled calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, raw portfolio Trades Per Year > ${MAX_TRADES_PER_YEAR}, or |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS}%. −100% if the pooled equity curve hits ≤ 0.*`,
28559
+ `*Trades Per Year (portfolio): pooled trade count × 365 / pooled span in days, where pooled span = (latest close-time − earliest pending-time across the whole pool) / day. UNITS: trades / year. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR pooled span < ${MIN_CALENDAR_SPAN_DAYS} days, OR raw value > ${MAX_TRADES_PER_YEAR} (sample too clustered to extrapolate reliably). NOTE: portfolio Trades Per Year inherits the ratios-block sample-size gate (MIN_SIGNALS_FOR_RATIOS) on the pooled count, whereas the per-symbol Trades Per Year column uses the annualisation-block gate (MIN_SIGNALS_FOR_ANNUALIZATION) on the per-symbol count. The two constants are equal (${MIN_SIGNALS_FOR_RATIOS}) in this build, so this never produces a discrepancy in practice; should they ever diverge, portfolio Trades Per Year would surface earlier than per-symbol Trades Per Year, which is consistent with the rest of the portfolio block already being scoped under the ratios gate.*`,
28560
+ `*Total Trades (portfolio = portfolioTotalTrades): sum of per-symbol totalTrades over all tracked symbols. UNITS: integer count.*`,
28561
+ `*Avg Peak PNL (portfolio): trade-count-weighted mean of per-symbol Avg Peak PNL over symbols whose value is non-null. Weights are per-symbol Total Trades; symbols without any peak-snapshot signals don't contribute and don't dilute. UNITS: percent. Describes the portfolio's typical best-case unrealised excursion. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Avg Peak PNL is null.*`,
28562
+ `*Avg Max Drawdown PNL (portfolio): trade-count-weighted mean of per-symbol Avg Max Drawdown PNL over symbols whose value is non-null. UNITS: percent (negative for losing excursions). Closer to 0 is better describes the portfolio's typical worst-case unrealised excursion. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Avg Max Drawdown PNL is null.*`,
28563
+ `*Peak Profit PNL (portfolio): MAX of per-symbol Peak Profit PNL over all symbols whose value is non-null. UNITS: percent. The single best best-case excursion observed anywhere across the portfolio (tail behaviour the average hides). NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Peak Profit PNL is null.*`,
28564
+ `*Max Drawdown PNL (portfolio): MIN of per-symbol Max Drawdown PNL over all symbols whose value is non-null. UNITS: percent (negative). The single deepest unrealised dip observed anywhere across the portfolio. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Max Drawdown PNL is null.*`,
28565
+ `*Median PNL (portfolio): median of per-trade PNL over the POOLED set of every closed signal of every symbol (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers; compare with the pooled mean per-trade PNL that underlies Pooled Sharpe to detect distribution skew (a large gap means one or two trades drag the arithmetic mean). NOT gated by MIN_SIGNALS — computed whenever pooled trade count 1.*`,
28566
+ `*Avg Duration (portfolio): arithmetic mean of (close-time − pending-time) / 60_000 over the pooled set of every closed signal of every symbol that has both timestamps. UNITS: minutes. NOT gated by MIN_SIGNALS computed whenever at least one closed signal across the pool carries valid timestamps.*`,
28567
+ `*Avg Win Duration (portfolio): mean hold time in minutes restricted to pooled winning closed signals (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning trades across the pool (NOT gated by MIN_SIGNALS).*`,
28568
+ `*Avg Loss Duration (portfolio): mean hold time in minutes restricted to pooled losing closed signals (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing trades across the pool (NOT gated by MIN_SIGNALS). Pair with Avg Win Duration to detect the asymmetry "winner-shorter-than-loser" (a red flag: cut winners short, let losers run).*`,
28569
+ `*Avg Consecutive Win PNL (portfolio): trade-count-weighted mean of per-symbol Avg Consecutive Win PNL over symbols whose value is non-null. A per-symbol "win streak" is a run of consecutive same-symbol closed signals with per-trade PNL > 0 bounded by a losing or break-even trade; the per-symbol metric averages the SUM of per-trade PNL within each streak. The portfolio aggregate weights by per-symbol Total Trades — concatenating streaks ACROSS symbols would be meaningless (different markets, different timeframes). UNITS: percent per streak. NOT gated by MIN_SIGNALS at portfolio level — null only when every per-symbol Avg Consecutive Win PNL is null (i.e. no symbol has a single complete win streak).*`,
28570
+ `*Avg Consecutive Loss PNL (portfolio): trade-count-weighted mean of per-symbol Avg Consecutive Loss PNL over symbols whose value is non-null. Same weighting rationale as the Win-streak counterpart above. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. NOT gated by MIN_SIGNALS at portfolio level null only when every per-symbol Avg Consecutive Loss PNL is null.*`,
28571
+ `*Standard Deviation Per Trade (portfolio): sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL over the pooled set of every closed signal of every symbol. UNITS: percent. Denominator for Pooled Sharpe. Below 1e-9 it is treated as zero (identical-returns guard). Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}.*`,
28572
+ `*Sortino Ratio (portfolio): pooled-mean per-trade PNL divided by pooled downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / pooled trade count ) over the pool (canonical Sortino: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades in the pool, OR pooled downside deviation 1e-9 (float-artifact guard).*`,
28573
+ `*Calmar Ratio (portfolio): portfolio Expected Yearly Returns divided by the pooled mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO}. The denominator is the max drawdown of the POOLED equity curve walked chronologically by close-timestamp across every symbol — each closed signal's worst intra-trade excursion (its trough-PNL snapshot, 0) is applied before booking its realised close, so deep round-trip dips count. Cross-symbol simultaneous drawdowns within the same minute are still serialised (one signal applied at a time), so genuine same-instant tail correlation is not modelled. UNITS: dimensionless. Null when the portfolio Expected Yearly Returns is null OR the pooled equity max drawdown ≤ 0.*`,
28574
+ `*Recovery Factor (portfolio): (pooled final equity − 1) × 100 divided by the pooled equity max drawdown, clamped to ±${MAX_CALMAR_RATIO}. The numerator is the compounded total return of the pooled equity curve — NOT the arithmetic Portfolio PNL shown at the top of this report; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same pooled mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = compounded profit doesn't cover max drawdown; above 3.0 = good. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, the pooled equity curve blew up (reached ≤ 0), or the pooled equity max drawdown ≤ 0.*`,
28575
+ `*Expectancy (portfolio): (pooled winning-trade count / pooled trade count) × pooled mean of winning PNLs + (pooled losing-trade count / pooled trade count) × pooled mean of losing PNLs. Break-even closed signals contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade across the portfolio. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR when the entire pool is break-even (no wins AND no losses).*`,
28576
+ ``,
28577
+ `*--- Per-symbol table columns ---*`,
28578
+ `*Total PNL (column): arithmetic sum of per-trade PNL across that symbol's stored closed signals. UNITS: percent. Additive total — NOT a compounded equity return. Null only when that symbol has zero stored signals.*`,
28579
+ `*Total Trades (column): integer count of that symbol's stored closed signals. UNITS: count.*`,
28580
+ `*Win Rate (column): per-symbol winning-trade count / (winning-trade count + losing-trade count) × 100; break-even trades (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistically noisy below 30 per-symbol signals; stable above 200. NOT gated by MIN_SIGNALS — null only when there are no decisive trades for that symbol.*`,
28581
+ `*Avg PNL (column): arithmetic mean of per-trade PNL across that symbol's signals — Σ per-trade PNL / Total Trades. UNITS: percent per trade. Null only when Total Trades = 0.*`,
28582
+ `*Standard Deviation (column): per-symbol sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL. UNITS: percent. Denominator for per-symbol Sharpe Ratio. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}.*`,
28583
+ `*Avg Win (column): arithmetic mean of per-trade PNL restricted to that symbol's winning trades (per-trade PNL > 0). UNITS: percent. NOT gated by MIN_SIGNALS — null only when that symbol has no winning trades.*`,
28584
+ `*Avg Loss (column): arithmetic mean of per-trade PNL restricted to that symbol's losing trades (per-trade PNL < 0). UNITS: percent (negative). NOT gated by MIN_SIGNALS — null only when that symbol has no losing trades.*`,
28585
+ `*Max Win Streak (column): longest run of consecutive same-symbol closed signals with per-trade PNL > 0, bounded by an opposite-sign or break-even trade. UNITS: integer count of consecutive wins. The streak structure is sign-invariant under reversal, so the count is identical whether signals are walked in chronological or reverse-chronological order. NOT gated by MIN_SIGNALS — 0 when that symbol has no winning trades.*`,
28586
+ `*Max Loss Streak (column): longest run of consecutive same-symbol closed signals with per-trade PNL < 0. UNITS: integer count. Same iteration rules as Max Win Streak. NOT gated by MIN_SIGNALS — 0 when that symbol has no losing trades.*`,
28587
+ `*Median PNL (column): per-symbol median of per-trade PNL (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers; compare with Avg PNL to detect distribution skew. NOT gated by MIN_SIGNALS — null only when Total Trades = 0.*`,
28588
+ `*Sharpe Ratio (column): per-symbol Avg PNL / Standard Deviation (risk-free rate = 0). UNITS: dimensionless. Below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
28589
+ `*Annualized Sharpe (column): per-symbol Sharpe Ratio × √(per-symbol Trades Per Year), where Trades Per Year for that symbol = its trade count × 365 / its calendar span in days. UNITS: dimensionless. Null when per-symbol Sharpe Ratio is null, OR that symbol's Trades Per Year is null (requires trade count ≥ ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days, raw frequency ≤ ${MAX_TRADES_PER_YEAR}). Assumes iid returns.*`,
28590
+ `*Certainty Ratio (column): per-symbol Avg Win / |Avg Loss|. UNITS: dimensionless. Below 1.0 = typical loss exceeds typical win; above 1.5 generally good. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR Avg Win / Avg Loss missing, OR Avg Loss ≥ 0, OR |Avg Loss| < 1e-9 (float-artifact loss guard).*`,
28591
+ `*Expected Yearly Returns (column): per-symbol geometric annualisation of that symbol's equity curve — (final equity ^ (Trades Per Year / trade count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over that symbol's signals in chronological close order. UNITS: percent per year. Null when trade count < ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, raw frequency > ${MAX_TRADES_PER_YEAR}, or |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS}%. −100% when the symbol's equity curve hits ≤ 0.*`,
28592
+ `*Trades Per Year (column): per-symbol trade count × 365 / calendar span in days, where calendar span = (latest close-time − earliest pending-time for that symbol) / day. UNITS: trades / year. Null when trade count < ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, or raw value > ${MAX_TRADES_PER_YEAR}.*`,
28593
+ `*Profit Factor (column): per-symbol Σ winning per-trade PNL / |Σ losing per-trade PNL|. UNITS: dimensionless. Below 1.0 means the symbol is net losing; above 1.5 is generally good. Null when there are no winning or no losing trades for that symbol, or when Σ|losing PNL| < 1e-9 (float-artifact guard).*`,
28594
+ `*Sortino Ratio (column): per-symbol Avg PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / total trade count ) (canonical Sortino: MAR = 0, divide by N_total). UNITS: dimensionless. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades, OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
28595
+ `*Calmar Ratio (column): per-symbol Expected Yearly Returns / Max Drawdown, clamped to ±${MAX_CALMAR_RATIO}. Denominator is the mark-to-market max drawdown of that symbol's compounded equity curve. Null when Expected Yearly Returns is null (requires ≥ ${MIN_SIGNALS_FOR_ANNUALIZATION} signals and a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days for that symbol) OR Max Drawdown ≤ 0.*`,
28596
+ `*Recovery Factor (column): per-symbol (final equity − 1) × 100 / Max Drawdown, clamped to ±${MAX_CALMAR_RATIO}. The numerator is the compounded total return of that symbol's equity curve. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, the equity curve blew up (reached ≤ 0), or Max Drawdown ≤ 0.*`,
28597
+ `*Expectancy (column): per-symbol expected value per trade. Three cases depending on what kinds of trades exist for that symbol: (a) BOTH winning and losing trades present → (winning-trade count / Total Trades) × Avg Win + (losing-trade count / Total Trades) × Avg Loss; (b) only winning trades present → (winning-trade count / Total Trades) × Avg Win (zero contribution from non-existent losses); (c) only losing trades present → (losing-trade count / Total Trades) × Avg Loss. Break-even trades contribute 0 (excluded from both probabilities). UNITS: percent per trade. NOT gated by MIN_SIGNALS — computed whenever Total Trades ≥ 1 and at least one decisive trade exists. (Note: the portfolio-level Expectancy further up in this report uses a single combined formula and IS gated by MIN_SIGNALS_FOR_RATIOS over the pooled count; per-symbol Expectancy is intentionally looser to populate the row early.)*`,
28598
+ `*Max Drawdown (column): per-symbol mark-to-market max drawdown — the symbol's compounded equity curve applies each closed signal's worst intra-trade excursion (its trough-PNL snapshot, ≤ 0) before booking the realised close, so deep round-trip dips count rather than only realised close-to-close drops. UNITS: percent. NOT realised-only.*`,
28599
+ `*Avg Peak PNL / Avg Max Drawdown PNL (columns): per-symbol arithmetic means of each closed signal's peak-PNL / trough-PNL snapshot — the best / worst mark-to-market PNL recorded while the position was open. Signals that never recorded the snapshot are excluded — no zero dilution. UNITS: percent. NOT gated by MIN_SIGNALS — each is null only if no signal for that symbol carries the corresponding snapshot.*`,
28600
+ `*Peak Profit PNL / Max Drawdown PNL (columns): per-symbol MAX of the peak-PNL snapshot / MIN of the trough-PNL snapshot across the symbol's stored closed signals. UNITS: percent. The single best best-case and worst worst-case excursions for that symbol — tail behaviour the averages hide. NOT gated by MIN_SIGNALS — each is null only if no signal for that symbol carries the corresponding snapshot.*`,
28601
+ `*Avg Duration / Avg Win Duration / Avg Loss Duration (columns): per-symbol mean hold time in minutes — (close-time − pending-time) / 60_000 — over all signals / winning signals / losing signals respectively. UNITS: minutes. NOT gated by MIN_SIGNALS — each is null only if the corresponding bucket (all / wins / losses) is empty for that symbol. Pair Avg Win vs Avg Loss durations to detect "let winners run, cut losers short" (Avg Win Duration > Avg Loss Duration is healthy).*`,
28602
+ `*Avg Consecutive Win PNL / Avg Consecutive Loss PNL (columns): a per-symbol "streak" is a run of consecutive same-symbol closed signals with same-sign per-trade PNL bounded by an opposite-sign or break-even trade; each metric sums per-trade PNL within each streak and averages those sums. The streak structure is sign-invariant under iteration order, so the resulting average is the same whether signals are walked chronologically or in reverse. UNITS: percent per streak. NOT gated by MIN_SIGNALS — each is null only if not a single complete streak of the corresponding sign exists in that symbol's history. Pair with Max Win Streak / Max Loss Streak to compare typical vs worst-case streak magnitude.*`,
28603
+ `*Trend (column, per-symbol only): bivariate classification computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol (one point per closed trade = the price at which it closed, with its close-timestamp — no candles, no order book, no tick stream). "sideways" when R² < 0.30; otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size; otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate, never a single magic constant on slope magnitude. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected. Not aggregated portfolio-wide because price series across symbols are not comparable.*`,
28604
+ `*Trend %/d (column, per-symbol only): ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED SIGNALS of that symbol. UNITS: percent per day (small-slope approximation). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator — static fit at report time. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} or the calendar span is degenerate.*`,
28605
+ `*Trend R² (column, per-symbol only): coefficient of determination of the same log-price regression that produces Trend %/d, computed on log(close) (NOT on absolute close). UNITS: dimensionless in [0, 1]. High R² = clean trend; low R² = noisy regardless of slope sign. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} or the calendar span is degenerate.*`,
28606
+ `*Buyer Pres (column, per-symbol only): frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. (count of i where close[i] > close[i−1]) / (count of decisive moves); flats (close[i] == close[i−1]) excluded from numerator and denominator. UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — the report has no orderbook access; "buyer" labels only the SIGN of the close-to-close return.*`,
28607
+ `*Seller Pres (column, per-symbol only): frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Buyer Pres + Seller Pres = 1. UNITS: dimensionless fraction in [0, 1].*`,
28608
+ `*Buyer Str (column, per-symbol only): magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. Σ |close[i] − close[i−1]| / close[i−1] over up-moves, divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pres (count) and Buyer Str (Σ|Δ|) use the SAME return series; divergence between them (e.g. Pres 0.70 with Str 0.45) reveals regime asymmetry: many small up-moves vs fewer but larger down-moves.*`,
28609
+ `*Seller Str (column, per-symbol only): magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. Σ |close[i] − close[i−1]| / close[i−1] over down-moves, divided by the same sum over ALL decisive moves. By construction Buyer Str + Seller Str = 1.*`,
28610
+ `*Pres Imb (column, per-symbol only): DIFFERENCE, not ratio: Buyer Str − Seller Str = (2 × Buyer Str − 1), computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias.*`,
28611
+ `*Median Step (column, per-symbol only): median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol — "step" = consecutive trade closes (NOT ticks, NOT bars of any timeframe; the report has no candle access). UNITS: percent (normalised by price → directly comparable between symbols at any price level). Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE.*`,
28612
+ ``,
28613
+ `*General reliability note: per-symbol ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS} per-symbol signals. Annualised metrics (Annualized Sharpe, Expected Yearly Returns, Calmar) additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR} per year. The same gates apply to portfolio-level pooled metrics, with the pooled trade count replacing per-symbol trade count. 100+ per-symbol signals are needed for statistical reliability of the ratios; annualised metrics assume the observed frequency and market regime persist year-round.*`,
28614
+ `*IMPORTANT: per-symbol AND pooled equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem (PositionSize / Kelly / ATR): per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging, the cost basis is the sum of all entries and the entry price is dollar-cost-weighted. If your strategy risks X% of capital per trade, the realised return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
28615
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol or portfolio (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
28538
28616
  ].join("\n");
28539
28617
  }
28540
28618
  /**
package/build/index.mjs CHANGED
@@ -23915,25 +23915,6 @@ MarkdownFileBase = makeExtendable(MarkdownFileBase);
23915
23915
  ReportBase = makeExtendable(ReportBase);
23916
23916
  const ReportWriter = new ReportWriterAdapter();
23917
23917
 
23918
- /**
23919
- * Price-profile metrics derived purely from a series of trade closes — no
23920
- * candles, no exchange queries. Designed for `BacktestMarkdownService`,
23921
- * `LiveMarkdownService` and per-symbol Heat: all three already have a
23922
- * chronological series of `(closeAt, close)` points and nothing else.
23923
- *
23924
- * Conventions
23925
- * -----------
23926
- * - Pressure: fraction of up-moves vs down-moves (frequency).
23927
- * - Strength: fraction of upward magnitude vs total movement.
23928
- * - A divergence between pressure and strength surfaces asymmetry — e.g.
23929
- * frequent shallow up-moves vs rare deep down-moves is "rising on weak
23930
- * buys, falling on strong sells".
23931
- * - Trend: linear regression of log(close) vs days. Slope in %/day,
23932
- * confidence in R². Classification is bivariate (slope × R²): neither
23933
- * axis alone fires, both must agree. Slope threshold is normalised by
23934
- * medianStepSize so the metric self-tunes to the instrument's typical
23935
- * move size.
23936
- */
23937
23918
  /** Minimum samples to surface any price-profile metric. Below this the
23938
23919
  * per-trade step-distribution and the regression are statistically noisy. */
23939
23920
  const MIN_SIGNALS = 10;
@@ -24281,16 +24262,18 @@ let ReportStorage$a = class ReportStorage {
24281
24262
  const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
24282
24263
  // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1) for unbiased estimate.
24283
24264
  // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
24284
- // are too noisy to publish (high chance of spurious ±Sharpe).
24265
+ // are too noisy to publish (high chance of spurious ±Sharpe). When the gate fails the
24266
+ // standard deviation itself is reported as null (NOT 0) so the report doesn't suggest
24267
+ // a flat distribution for a small but variable sample.
24285
24268
  const returns = validSignals.map((s) => s.pnl.pnlPercentage);
24286
24269
  const canComputeRatios = totalSignals >= MIN_SIGNALS_FOR_RATIOS$2;
24287
24270
  const stdDev = canComputeRatios
24288
24271
  ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (totalSignals - 1))
24289
- : 0;
24272
+ : null;
24290
24273
  // Use STDDEV_EPSILON gate (not stdDev > 0) — identical-returns series produce
24291
24274
  // float-artifact stdDev (~1e-17) that's mathematically > 0 but spuriously
24292
24275
  // inflates sharpe to astronomical magnitudes (avgPnl / epsilon).
24293
- const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$2
24276
+ const sharpeRatio = canComputeRatios && stdDev !== null && stdDev > STDDEV_EPSILON$2
24294
24277
  ? avgPnl / stdDev
24295
24278
  : null;
24296
24279
  // Annualize only when gate passes; otherwise null.
@@ -24637,24 +24620,38 @@ let ReportStorage$a = class ReportStorage {
24637
24620
  `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
24638
24621
  `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
24639
24622
  "",
24640
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
24641
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
24642
- `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$2} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$2} days. Assumes returns are iid autocorrelated strategies are overstated.*`,
24643
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
24644
- `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
24645
- `*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.*`,
24646
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). Capped at ±${MAX_CALMAR_RATIO$2}.*`,
24647
- `*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 and the mark-to-market max drawdown as denominator.*`,
24648
- `*Max Drawdown: mark-to-market the compounded equity curve applies 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.*`,
24649
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
24650
- `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
24651
- `*IMPORTANT: 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 portfolio return / drawdown will be roughly X/100 of the reported figures these metrics represent a theoretical upper bound under full allocation.*`,
24652
- `*Trend / Trend Strength / Trend Confidence: linear regression of log(close) vs days across closed-trade prices. "Trend Strength" is the slope in %/day; "Trend Confidence" is R². Classification gates on 0.30 (else "sideways") AND |slope| 0.25 × medianStepSize (else "neutral" a real but weak tilt). Self-tunes to the instrument's typical move size no magic constants on price magnitude.*`,
24653
- `*Buyer / Seller Pressure: fraction of up-moves (resp. down-moves) among decisive close-to-close changes. Frequency-based; flats excluded.*`,
24654
- `*Buyer / Seller Strength: share of upward (resp. downward) absolute movement in total movement. Magnitude-based. A divergence between pressure and strength surfaces asymmetry: "frequent shallow buys, rare deep sells" is a different regime than "rare deep buys, frequent shallow sells".*`,
24655
- `*Pressure Imbalance: buyerStrength sellerStrength [1, +1]. Single signed summary of magnitude bias.*`,
24656
- `*Median Step Size: median |close[i] close[i1]| / close[i−1] across closed trades, in %. Robust to outliers describes the typical close-to-close jump. Used as the scale that normalises the slope-vs-step trend gate. NOTE: this is NOT classical (candle-based) volatility there are no candles between trade closes in the report data; it measures step-distribution at the rate trades close.*`,
24657
- `*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.*`,
24623
+ `*Win Rate: percent of closed signals that ended with per-trade PNL > 0, computed as winning-trade count / (winning-trade count + losing-trade count) × 100 — break-even trades (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistical reliability: noisy below 30 signals (a single streak can shift it 1020 points); stable above 200 signals.*`,
24624
+ `*Average PNL: arithmetic mean of per-trade PNL across every closed signal, computed as Σ per-trade PNL / closed signal count. UNITS: percent per trade. Sign mirrors strategy edge — positive = profitable on average per trade. Sensitive to one whale trade; cross-check with Median PNL to detect skew.*`,
24625
+ `*Total PNL: arithmetic sum of per-trade PNL across every closed signal. UNITS: percent. This is the additive total, NOT the compounded equity return — for the geometrically-compounded variant see Recovery Factor's numerator and Expected Yearly Returns. Useful as a quick scoreboard but ignores volatility drag.*`,
24626
+ `*Standard Deviation Per Trade: sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL. UNITS: percent. Measures volatility of returns. Denominator for per-trade Sharpe Ratio. Below STDDEV_EPSILON = 1e-9 it is treated as zero (identical-returns guard). Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} (variance too noisy on small samples).*`,
24627
+ `*Sharpe Ratio: per-trade Sharpe = Average PNL / Standard Deviation Per Trade (risk-free rate = 0). UNITS: dimensionless ratio. Higher = better risk-adjusted return per trade. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
24628
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √(trades per year), where trades per year = closed signal count × 365 / calendar span in days. UNITS: dimensionless. Null when the closed signal count < ${MIN_SIGNALS_FOR_ANNUALIZATION$2}, OR calendar span < ${MIN_CALENDAR_SPAN_DAYS$2} days, OR raw frequency > ${MAX_TRADES_PER_YEAR$2} (clustered sample, annualisation unreliable). Assumes returns are iid — autocorrelated strategies are overstated.*`,
24629
+ `*Certainty Ratio: mean per-trade PNL over winning trades, divided by the absolute value of the mean per-trade PNL over losing trades. UNITS: dimensionless ratio. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, OR there are no losing trades, OR the absolute mean losing PNL < 1e-9 (float-artifact loss guard).*`,
24630
+ `*Expected Yearly Returns: geometric annualisation of the equity curve: (final equity ^ (trades per year / closed signal count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over all closed signals in chronological close order. UNITS: percent per year. Accounts for volatility drag (unlike a simple Σ × 365 / calendar-span projection). Null under the same closed-signal-count / calendar-span / frequency gates as Annualized Sharpe Ratio, AND null when |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS$2}% (capped numbers mislead). −100% when the equity curve hits ≤ 0 (account blown).*`,
24631
+ `*Avg Peak PNL: arithmetic mean of each closed signal's peak-PNL snapshot — the best mark-to-market PNL recorded while the position was open. Signals that never recorded such a snapshot are excluded from both numerator and denominator (no zero dilution). UNITS: percent. Describes the typical best-case unrealised excursion during the position's lifetime, not the realised close. NOT gated by MIN_SIGNALS computed whenever at least one signal carries the snapshot; null only if no signal carries it.*`,
24632
+ `*Avg Max Drawdown PNL: arithmetic mean of each closed signal's trough-PNL snapshot the worst mark-to-market PNL recorded while the position was open. Signals that never recorded such a snapshot are excluded (no zero dilution). UNITS: percent (negative for losing excursions). Describes the typical worst-case unrealised PNL during the position's lifetime; closer to 0 is better. NOT gated by MIN_SIGNALS — computed whenever at least one signal carries the snapshot.*`,
24633
+ `*Sortino Ratio: Average PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / closed signal count ) (canonical Sortino 1991: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, OR there are no losing trades (downside is undefined — flawless ≠ infinitely good), OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
24634
+ `*Calmar Ratio: Expected Yearly Returns divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$2}. The denominator is the max drawdown of the compounded equity curve: each trade's worst intra-trade excursion (the trough-PNL snapshot, 0) is applied before booking its realised close, so a position that dipped to −18% and recovered to +2% contributes a real drawdown rather than zero. UNITS: dimensionless. Rule of thumb: below 0.5 poor, 0.5–1.0 acceptable, above 1.0 strong. Null when Expected Yearly Returns is null OR the equity max drawdown 0.*`,
24635
+ `*Recovery Factor: (final equity 1) × 100 divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$2}. The numerator is the compounded total return — NOT the arithmetic Total PNL shown above; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = profit doesn't cover drawdown; above 3.0 = good. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, the equity curve blew up (account ≤ 0), or the equity max drawdown ≤ 0.*`,
24636
+ `*Expectancy: per-trade expected value = (winning-trade count / closed signal count) × mean winning PNL + (losing-trade count / closed signal count) × mean losing PNL. Break-even trades contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24637
+ `*Median PNL: median of per-trade PNL across all closed signals (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers — comparing it with Average PNL reveals distribution skew. A large gap (e.g. avg +1.5%, median +0.2%) means one or two trades carry the arithmetic mean. NOT gated by MIN_SIGNALS computed whenever the closed signal count ≥ 1.*`,
24638
+ `*Avg Duration: arithmetic mean of (close-timepending-time) / 60_000 over all closed signals. UNITS: minutes (synchronised with the strategy's estimated-minutes setting). Describes the typical position hold time. NOT gated by MIN_SIGNALS — computed whenever the closed signal count ≥ 1.*`,
24639
+ `*Avg Win Duration: arithmetic mean of (close-time pending-time) / 60_000 restricted to winning trades (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning trades (NOT gated by MIN_SIGNALS — computed at any winning-trade count ≥ 1). Pair with Avg Loss Duration to detect the classic asymmetry "let winners run, cut losers short" (Win Duration > Loss Duration is healthy) versus the inverse red flag "cut winners short, let losers run".*`,
24640
+ `*Avg Loss Duration: arithmetic mean of (close-time pending-time) / 60_000 restricted to losing trades (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing trades (NOT gated by MIN_SIGNALS).*`,
24641
+ `*Avg Consecutive Win PNL: a "win streak" is a run of consecutive trades with per-trade PNL > 0 bounded by either a losing or break-even trade; this metric sums per-trade PNL within each streak and then averages those sums. Trades are walked in chronological close order. UNITS: percent per streak. Higher = winning streaks are typically bigger. Null only when there is not a single complete win streak in the stored history (NOT gated by MIN_SIGNALS).*`,
24642
+ `*Avg Consecutive Loss PNL: a "loss streak" is a run of consecutive trades with per-trade PNL < 0 bounded by either a winning or break-even trade; this metric sums per-trade PNL within each streak and then averages those sums. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. Null only when there is not a single complete loss streak (NOT gated by MIN_SIGNALS).*`,
24643
+ `*Trend: classification computed FROM CLOSING PRICES OF CLOSED SIGNALS (one point per closed trade = the price at which it closed, with its close-timestamp — no candles, no order book, no tick stream). Bivariate gate: "sideways" when R² < 0.30 (regression too weak to call any direction). Otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size (slope detectable but smaller than the typical daily step). Otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate — never a single-axis if on a magic-constant slope magnitude. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS$2}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected.*`,
24644
+ `*Trend Strength: ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED SIGNALS (one point per closed trade in chronological order — no candles). UNITS: percent per day (small-slope approximation: log-return ≈ percent-return). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator at signal entry — a static fit over the entire stored history at report time. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} or the calendar span is degenerate.*`,
24645
+ `*Trend Confidence (R²): coefficient of determination of the same log-price regression that produces Trend Strength, computed on log(close) — NOT on absolute close. UNITS: dimensionless in [0, 1]. High R² = log-price moves linearly with time (clean trend); low R² = the series is noisy regardless of slope sign. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2} or the calendar span is degenerate.*`,
24646
+ `*Buyer Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. (count of i where close[i] > close[i−1]) / (count of decisive moves), where "decisive" excludes flats (close[i] == close[i−1]). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — backtest data has no order book; "buyer" labels only the SIGN of the close-to-close return. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24647
+ `*Seller Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Seller Pressure = 1 − Buyer Pressure (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — labels only the SIGN of the close-to-close return. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24648
+ `*Buyer Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Σ |close[i] − close[i−1]| / close[i−1] over up-moves (close[i] > close[i−1]), divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pressure (count of up-moves) and Buyer Strength (sum of up-magnitudes) use the SAME close-to-close return series; only count vs magnitude differs. A divergence between them (e.g. Pressure 0.70 with Strength 0.45) means "many small up-moves, fewer but larger down-moves" — a regime asymmetry frequency alone hides. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24649
+ `*Seller Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Σ |close[i] − close[i−1]| / close[i−1] over down-moves (close[i] < close[i−1]), divided by the same sum over ALL decisive moves. By construction Seller Strength = 1 − Buyer Strength (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24650
+ `*Pressure Imbalance: DIFFERENCE, not ratio: Buyer Strength − Seller Strength, computed FROM CLOSING PRICES OF CLOSED SIGNALS. Equivalent to (2 × Buyer Strength − 1). UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias (positive = bullish bias on magnitude); absolute value = how lopsided. Single signed scalar that compresses the strength pair into one number. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24651
+ `*Median Step Size: median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED SIGNALS — "step" = consecutive closes of CLOSED TRADES (NOT ticks, NOT bars of any timeframe — the report has no candle access). UNITS: percent, normalised by price → directly comparable across symbols at any price level. Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE. Null when the closed signal count < ${MIN_SIGNALS_FOR_RATIOS$2}.*`,
24652
+ `*General reliability note: per-trade ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS$2} closed signals because the underlying variance estimates are too noisy. Annualised metrics (Annualized Sharpe, Expected Yearly Returns, Calmar) additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS$2} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR$2} per year. 100+ signals are needed for statistical reliability of the ratios; annualised metrics assume the observed frequency and market regime persist year-round.*`,
24653
+ `*IMPORTANT: Total PNL and the equity-curve metrics (Expected Yearly Returns, Calmar, Recovery, and the equity max drawdown that feeds them) all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem (PositionSize / Kelly / ATR): per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging, 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. If your strategy risks X% of capital per trade, the realised portfolio return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
24654
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing strategy (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
24658
24655
  ].join("\n");
24659
24656
  }
24660
24657
  /**
@@ -25342,14 +25339,16 @@ let ReportStorage$9 = class ReportStorage {
25342
25339
  const tradesPerYear = canAnnualize ? rawTradesPerYear : 0;
25343
25340
  // Per-trade Sharpe Ratio (risk-free rate = 0). Sample stddev (N-1).
25344
25341
  // Per-trade ratios are gated by MIN_SIGNALS_FOR_RATIOS — below that, variance estimates
25345
- // are too noisy to publish (high chance of spurious ±Sharpe).
25342
+ // are too noisy to publish (high chance of spurious ±Sharpe). When the gate fails the
25343
+ // standard deviation itself is reported as null (NOT 0) so the report doesn't suggest
25344
+ // a flat distribution for a small but variable sample.
25346
25345
  const canComputeRatios = returns.length >= MIN_SIGNALS_FOR_RATIOS$1;
25347
25346
  const stdDev = canComputeRatios
25348
25347
  ? Math.sqrt(returns.reduce((sum, r) => sum + Math.pow(r - avgPnl, 2), 0) / (returns.length - 1))
25349
- : 0;
25348
+ : null;
25350
25349
  // STDDEV_EPSILON guard — protects against float-artifact stdDev from identical
25351
25350
  // returns producing spuriously astronomical sharpe.
25352
- const sharpeRatio = canComputeRatios && stdDev > STDDEV_EPSILON$1
25351
+ const sharpeRatio = canComputeRatios && stdDev !== null && stdDev > STDDEV_EPSILON$1
25353
25352
  ? avgPnl / stdDev
25354
25353
  : null;
25355
25354
  // Annualize only when gate passes; otherwise null.
@@ -25700,24 +25699,38 @@ let ReportStorage$9 = class ReportStorage {
25700
25699
  `**Pressure Imbalance:** ${stats.pressureImbalance === null ? "N/A" : `${stats.pressureImbalance > 0 ? "+" : ""}${stats.pressureImbalance.toFixed(3)}`}`,
25701
25700
  `**Median Step Size:** ${stats.medianStepSize === null ? "N/A" : `${stats.medianStepSize.toFixed(3)}%`}`,
25702
25701
  "",
25703
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
25704
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals.*`,
25705
- `*Annualized Sharpe Ratio: per-trade Sharpe × √tradesPerYear; tradesPerYear = signals × 365 / calendarSpanDays. N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION$1} signals and span ≥${MIN_CALENDAR_SPAN_DAYS$1} days. Assumes returns are iid autocorrelated strategies are overstated.*`,
25706
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
25707
- `*Certainty Ratio: below 1.0 means average loss exceeds average win. Above 1.5 is considered good.*`,
25708
- `*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.*`,
25709
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). Capped at ±${MAX_CALMAR_RATIO$1}.*`,
25710
- `*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 and the mark-to-market max drawdown as denominator.*`,
25711
- `*Max Drawdown: mark-to-market the compounded equity curve applies 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.*`,
25712
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
25713
- `*All metrics require 100+ signals to be statistically reliable. Annualized metrics assume the observed trading frequency and market conditions persist year-round.*`,
25714
- `*IMPORTANT: 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 portfolio return / drawdown will be roughly X/100 of the reported figures these metrics represent a theoretical upper bound under full allocation.*`,
25715
- `*Trend / Trend Strength / Trend Confidence: linear regression of log(close) vs days across closed-trade prices. "Trend Strength" is the slope in %/day; "Trend Confidence" is R². Classification gates on 0.30 (else "sideways") AND |slope| 0.25 × medianStepSize (else "neutral" a real but weak tilt). Self-tunes to the instrument's typical move size no magic constants on price magnitude.*`,
25716
- `*Buyer / Seller Pressure: fraction of up-moves (resp. down-moves) among decisive close-to-close changes. Frequency-based; flats excluded.*`,
25717
- `*Buyer / Seller Strength: share of upward (resp. downward) absolute movement in total movement. Magnitude-based. A divergence between pressure and strength surfaces asymmetry: "frequent shallow buys, rare deep sells" is a different regime than "rare deep buys, frequent shallow sells".*`,
25718
- `*Pressure Imbalance: buyerStrength sellerStrength [1, +1]. Single signed summary of magnitude bias.*`,
25719
- `*Median Step Size: median |close[i] close[i−1]| / close[i1] across closed trades, in %. Robust to outliers describes the typical close-to-close jump. Used as the scale that normalises the slope-vs-step trend gate. NOTE: this is NOT classical (candle-based) volatility there are no candles between trade closes in the report data; it measures step-distribution at the rate trades close.*`,
25720
- `*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.*`,
25702
+ `*Win Rate: percent of closed events that ended with per-trade PNL > 0, computed as winning-event count / (winning-event count + losing-event count) × 100 — break-even closed events (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistical reliability: noisy below 30 closed events (a single streak can shift it 1020 points); stable above 200.*`,
25703
+ `*Average PNL: arithmetic mean of per-trade PNL across every closed event, computed as Σ per-trade PNL / closed event count. UNITS: percent per trade. Sign mirrors strategy edge — positive = profitable on average per trade. Sensitive to one whale trade; cross-check with Median PNL to detect skew.*`,
25704
+ `*Total PNL: arithmetic sum of per-trade PNL across every closed event. UNITS: percent. This is the additive total, NOT the compounded equity return — for the geometrically-compounded variant see Recovery Factor's numerator and Expected Yearly Returns. Useful as a quick scoreboard but ignores volatility drag.*`,
25705
+ `*Standard Deviation Per Trade: sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL across closed events. UNITS: percent. Measures volatility of realised returns. Denominator for per-trade Sharpe Ratio. Below STDDEV_EPSILON = 1e-9 it is treated as zero (identical-returns guard). Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25706
+ `*Sharpe Ratio: per-trade Sharpe = Average PNL / Standard Deviation Per Trade (risk-free rate = 0). UNITS: dimensionless ratio. Higher = better risk-adjusted return per trade. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
25707
+ `*Annualized Sharpe Ratio: per-trade Sharpe × √(trades per year), where trades per year = closed event count × 365 / calendar span in days. UNITS: dimensionless. Null when the closed event count < ${MIN_SIGNALS_FOR_ANNUALIZATION$1}, OR calendar span < ${MIN_CALENDAR_SPAN_DAYS$1} days, OR raw frequency > ${MAX_TRADES_PER_YEAR$1}. Assumes returns are iid autocorrelated strategies are overstated.*`,
25708
+ `*Certainty Ratio: mean per-trade PNL over winning closed events, divided by the absolute value of the mean per-trade PNL over losing closed events. UNITS: dimensionless ratio. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, OR there are no losing events, OR the absolute mean losing PNL < 1e-9 (float-artifact loss guard).*`,
25709
+ `*Expected Yearly Returns: geometric annualisation of the equity curve: (final equity ^ (trades per year / closed event count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over all closed events in chronological close order. UNITS: percent per year. Accounts for volatility drag. Null under the same closed-event-count / calendar-span / frequency gates as Annualized Sharpe Ratio, AND null when |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS$1}%. −100% when the equity curve hits ≤ 0 (account blown).*`,
25710
+ `*Avg Peak PNL: arithmetic mean of each closed event's peak-PNL snapshot — the best mark-to-market PNL recorded while the position was open. Closed events that never recorded such a snapshot are excluded from both numerator and denominator (no zero dilution). UNITS: percent. Describes the typical best-case unrealised PNL during the position's lifetime, not the realised close. NOT gated by MIN_SIGNALS computed whenever at least one closed event carries the snapshot.*`,
25711
+ `*Avg Max Drawdown PNL: arithmetic mean of each closed event's trough-PNL snapshot the worst mark-to-market PNL recorded while the position was open. Closed events that never recorded such a snapshot are excluded (no zero dilution). UNITS: percent (negative for losing excursions). Describes the typical worst-case unrealised PNL during the position's lifetime; closer to 0 is better. NOT gated by MIN_SIGNALS — computed whenever at least one closed event carries the snapshot.*`,
25712
+ `*Sortino Ratio: Average PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / closed event count ) (canonical Sortino 1991: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, OR there are no losing events, OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
25713
+ `*Calmar Ratio: Expected Yearly Returns divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$1}. The denominator is the max drawdown of the compounded equity curve: each closed event's worst intra-trade excursion (the trough-PNL snapshot, 0) is applied before booking its realised close, so a position that dipped to −18% and recovered to +2% contributes a real drawdown rather than zero. UNITS: dimensionless. Rule of thumb: below 0.5 poor, 0.5–1.0 acceptable, above 1.0 strong. Null when Expected Yearly Returns is null OR the equity max drawdown 0.*`,
25714
+ `*Recovery Factor: (final equity 1) × 100 divided by the equity-curve mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO$1}. The numerator is the compounded total return — NOT the arithmetic Total PNL shown above; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = profit doesn't cover drawdown; above 3.0 = good. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, the equity curve blew up (account ≤ 0), or the equity max drawdown ≤ 0.*`,
25715
+ `*Expectancy: per-trade expected value = (winning-event count / closed event count) × mean winning PNL + (losing-event count / closed event count) × mean losing PNL. Break-even events contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25716
+ `*Median PNL: median of per-trade PNL across all closed events (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers — comparing it with Average PNL reveals distribution skew. A large gap (e.g. avg +1.5%, median +0.2%) means one or two events carry the arithmetic mean. NOT gated by MIN_SIGNALS computed whenever the closed event count ≥ 1.*`,
25717
+ `*Avg Duration: arithmetic mean of (close-timepending-time) / 60_000 over all closed events. UNITS: minutes. Describes the typical position hold time. Closed events that have no pending timestamp fall back to using their own close timestamp (yielding a zero-duration contribution). NOT gated by MIN_SIGNALS — computed whenever the closed event count ≥ 1.*`,
25718
+ `*Avg Win Duration: arithmetic mean of (close-timepending-time) / 60_000 restricted to winning closed events (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning events (NOT gated by MIN_SIGNALS). Pair with Avg Loss Duration to detect the classic asymmetry "let winners run, cut losers short" versus the inverse "cut winners short, let losers run".*`,
25719
+ `*Avg Loss Duration: arithmetic mean of (close-time pending-time) / 60_000 restricted to losing closed events (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing events (NOT gated by MIN_SIGNALS).*`,
25720
+ `*Avg Consecutive Win PNL: a "win streak" is a run of consecutive closed events with per-trade PNL > 0 bounded by either a losing or break-even event; this metric sums per-trade PNL within each streak and then averages those sums. Events are walked in chronological close order. UNITS: percent per streak. Higher = winning streaks are typically bigger. Null only when there is not a single complete win streak (NOT gated by MIN_SIGNALS).*`,
25721
+ `*Avg Consecutive Loss PNL: a "loss streak" is a run of consecutive closed events with per-trade PNL < 0 bounded by either a winning or break-even event; this metric sums per-trade PNL within each streak and then averages those sums. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. Null only when there is not a single complete loss streak (NOT gated by MIN_SIGNALS).*`,
25722
+ `*Trend: classification computed FROM CLOSING PRICES OF CLOSED EVENTS (one point per closed event = the price at which it closed, with its close-timestamp; the live mode ingests sub-minute events of other kinds but the price profile uses ONLY closed-event prices — no candles, no order book, no tick stream). Bivariate gate: "sideways" when R² < 0.30. Otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size. Otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate — never a single-axis if on a magic-constant slope magnitude. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS$1}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected.*`,
25723
+ `*Trend Strength: ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED EVENTS in chronological order. UNITS: percent per day (small-slope approximation). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator at signal entry — a static fit over the entire stored history at report time. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} or the calendar span is degenerate.*`,
25724
+ `*Trend Confidence (R²): coefficient of determination of the same log-price regression that produces Trend Strength, computed on log(close) — NOT on absolute close. UNITS: dimensionless in [0, 1]. High R² = log-price moves linearly with time (clean trend); low R² = the series is noisy regardless of slope sign. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1} or the calendar span is degenerate.*`,
25725
+ `*Buyer Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. (count of i where close[i] > close[i−1]) / (count of decisive moves), where "decisive" excludes flats. UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — the live ingest does not separate buy/sell side; "buyer" labels only the SIGN of the close-to-close return. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25726
+ `*Seller Pressure: frequency-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Seller Pressure = 1 − Buyer Pressure (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25727
+ `*Buyer Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. Σ |close[i] − close[i−1]| / close[i−1] over up-moves, divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pressure (count of up-moves) and Buyer Strength (sum of up-magnitudes) use the SAME close-to-close return series; only count vs magnitude differs. A divergence between them (e.g. Pressure 0.70 with Strength 0.45) means "many small up-moves, fewer but larger down-moves". Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25728
+ `*Seller Strength: magnitude-based, computed FROM CLOSING PRICES OF CLOSED EVENTS. Σ |close[i] − close[i−1]| / close[i−1] over down-moves, divided by the same sum over ALL decisive moves. By construction Seller Strength = 1 − Buyer Strength (the two sum to 1). UNITS: dimensionless fraction in [0, 1]. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25729
+ `*Pressure Imbalance: DIFFERENCE, not ratio: Buyer Strength − Seller Strength, computed FROM CLOSING PRICES OF CLOSED EVENTS. Equivalent to (2 × Buyer Strength − 1). UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias (positive = bullish bias on magnitude); absolute value = how lopsided. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25730
+ `*Median Step Size: median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED EVENTS — "step" = consecutive closed-event prices (NOT ticks, NOT bars; the report has no candle access even though the live mode ingests sub-minute events — they are not used here). UNITS: percent, normalised by price → directly comparable across symbols at any price level. Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE. Null when the closed event count < ${MIN_SIGNALS_FOR_RATIOS$1}.*`,
25731
+ `*General reliability note: per-trade ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS$1} closed events. Annualised metrics additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS$1} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR$1} per year. 100+ closed events are needed for statistical reliability of the ratios.*`,
25732
+ `*IMPORTANT: Total PNL and the equity-curve metrics (Expected Yearly Returns, Calmar, Recovery, and the equity max drawdown that feeds them) all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem: per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging the cost basis is the sum of all entries and the entry price is dollar-cost-weighted. If your strategy risks X% of capital per trade, the realised portfolio return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
25733
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing strategy (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
25721
25734
  ].join("\n");
25722
25735
  }
25723
25736
  /**
@@ -26272,7 +26285,14 @@ let ReportStorage$8 = class ReportStorage {
26272
26285
  `**Average activation time:** ${stats.avgActivationTime === null ? "N/A" : `${stats.avgActivationTime.toFixed(2)} minutes`}`,
26273
26286
  `**Average wait time (cancelled):** ${stats.avgWaitTime === null ? "N/A" : `${stats.avgWaitTime.toFixed(2)} minutes`}`,
26274
26287
  "",
26275
- `*Activation / Cancellation rates are computed over scheduled signals whose outcome (opened or cancelled) is also in the buffer matched by signalId. "Scheduled signals (raw)" above is the unmatched count and may include records whose outcome has not yet arrived or was evicted from the buffer.*`
26288
+ `*Total events: integer count of every scheduled / opened / cancelled event in the ring buffer (capped at CC_MAX_SCHEDULE_MARKDOWN_ROWS, newest-first; oldest entries are dropped past the cap). UNITS: count.*`,
26289
+ `*Scheduled signals (raw): count of events whose action is "scheduled" in the buffer. UNITS: count. NOTE: this is the unmatched raw count — it may include records whose outcome (opened/cancelled) has not yet arrived or was evicted from the buffer. Activation / Cancellation rates do NOT use this denominator (see below).*`,
26290
+ `*Opened signals: count of events whose action is "opened" in the buffer. UNITS: count.*`,
26291
+ `*Cancelled signals: count of events whose action is "cancelled" in the buffer. UNITS: count.*`,
26292
+ `*Activation rate: matched-opened count / matched-resolved count × 100, where matched-opened = number of "opened" events whose signal id also appears among the buffer's "scheduled" events, matched-cancelled = same for "cancelled" events, and matched-resolved = matched-opened + matched-cancelled. The signal-id match guarantees the numerator and denominator come from the SAME population — a sliding window of CC_MAX_SCHEDULE_MARKDOWN_ROWS can otherwise drop a "scheduled" record before its outcome arrives, inflating rates above 100% or firing one rate without the other. UNITS: percent in [0, 100]. Null when matched-resolved = 0 (no matched outcomes in the buffer).*`,
26293
+ `*Cancellation rate: matched-cancelled count / matched-resolved count × 100 — same signal-id-matched denominator as Activation rate. UNITS: percent in [0, 100]. Null when matched-resolved = 0. By construction Activation rate + Cancellation rate = 100% (or both null together).*`,
26294
+ `*Average activation time: arithmetic mean of duration over events whose action is "opened" AND whose duration is a number. Events without a numeric duration are excluded from both numerator and denominator (no zero dilution). UNITS: minutes. Null when no "opened" event carries a numeric duration. The duration on an opened event represents wait time from the moment the signal was scheduled until the moment it activated (became pending).*`,
26295
+ `*Average wait time (cancelled): arithmetic mean of duration over events whose action is "cancelled" AND whose duration is a number. Same exclusion rule as Average activation time. UNITS: minutes. Null when no "cancelled" event carries a numeric duration. The duration on a cancelled event represents wait time from the moment the signal was scheduled until cancellation.*`,
26276
26296
  ].join("\n");
26277
26297
  }
26278
26298
  /**
@@ -26755,8 +26775,6 @@ class PerformanceStorage {
26755
26775
  "",
26756
26776
  summaryTable,
26757
26777
  "",
26758
- "**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times. Wait times show the interval between consecutive events of the same type.",
26759
- "",
26760
26778
  `**Total events:** ${stats.totalEvents}`,
26761
26779
  `**Total execution time:** ${stats.totalDuration.toFixed(2)}ms`,
26762
26780
  `**Number of metric types:** ${Object.keys(stats.metricStats).length}`,
@@ -26765,6 +26783,17 @@ class PerformanceStorage {
26765
26783
  "",
26766
26784
  percentages.join("\n"),
26767
26785
  "",
26786
+ `*Total events: integer count of every performance event in the ring buffer (capped at CC_MAX_PERFORMANCE_MARKDOWN_ROWS, newest-first; oldest entries are dropped past the cap). UNITS: count.*`,
26787
+ `*Total execution time: arithmetic sum of every event's duration in the buffer. UNITS: milliseconds.*`,
26788
+ `*Number of metric types: count of distinct metric-type labels present in the buffer. UNITS: count.*`,
26789
+ `*Time Distribution (per metric): the metric's Total Duration divided by overall Total Execution Time × 100; rendered as a percent next to that metric's total duration in milliseconds. Guard: when the overall execution time is 0 (all-instant operations) the percent is forced to 0 instead of NaN. UNITS: percent of total execution time.*`,
26790
+ `*Count (table column): integer number of events of this metric type in the buffer.*`,
26791
+ `*Total Duration (table column): sum of every event's duration for that metric type. UNITS: milliseconds.*`,
26792
+ `*Avg Duration (table column): Total Duration divided by Count for that metric type. UNITS: milliseconds.*`,
26793
+ `*Min / Max Duration (table columns): smallest / largest single-event duration in the metric-type bucket. UNITS: milliseconds.*`,
26794
+ `*Std Dev (table column): sample standard deviation (Bessel-corrected, N−1 denominator) of single-event durations for that metric type. UNITS: milliseconds. Zero when the bucket has only one event (no variance defined).*`,
26795
+ `*Median / P95 / P99 (table columns): percentiles of the sorted single-event durations using linear interpolation between adjacent ranks — equivalent to numpy.percentile with the default linear method. P95 / P99 represent the 95th / 99th percentile response times (tail latency). UNITS: milliseconds. Buckets of size 0 fall back to 0; size 1 returns the single value for every percentile.*`,
26796
+ `*Avg / Min / Max Wait Time (table columns): wait time = (event's timestamp − the previous event's timestamp) for events where a previous-timestamp value is recorded; this captures the interval between consecutive events of the same metric type. Mean / smallest / largest of those gaps. UNITS: milliseconds. All zero when no event in the bucket carries a previous-timestamp value.*`,
26768
26797
  ].join("\n");
26769
26798
  }
26770
26799
  /**
@@ -27224,7 +27253,17 @@ let ReportStorage$7 = class ReportStorage {
27224
27253
  "",
27225
27254
  await this.getPnlTable(pnlColumns),
27226
27255
  "",
27227
- "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better)."
27256
+ "",
27257
+ `*Optimization Metric: the name of the per-strategy statistic that this walker run ranked strategies by. The "Best ${results.metric}" value below is the largest value of that statistic among all tested strategies (or, if the statistic is one for which "lower is better", the smallest); the strategy that produced it is reported as Best Strategy. Strategies whose ${results.metric} is N/A are skipped from "best" selection — a missing value never wins.*`,
27258
+ `*Strategies Tested: integer count of distinct strategies that were run during this walker comparison. UNITS: count.*`,
27259
+ `*Total Signals (best strategy): integer count of closed signals produced by the best strategy during this walker run.*`,
27260
+ `*Sortino Ratio (best strategy summary): the best strategy's Average PNL divided by downside deviation (risk-free rate = 0). Downside deviation = √( Σ min(0, per-trade PNL)² / closed signal count ) — squared sums computed over the strategy's closed signals and divided by the total signal count (canonical Sortino 1991, MAR = 0). UNITS: dimensionless. N/A when the best strategy has fewer than 10 closed signals, when it has no losing trades, or when downside deviation is below a float-artifact threshold (~1e-9). Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong.*`,
27261
+ `*Calmar Ratio (best strategy summary): the best strategy's Expected Yearly Returns divided by its equity max drawdown, clamped to ±1000. Expected Yearly Returns is the geometric annualisation of the strategy's compounded equity curve; the equity max drawdown is the mark-to-market max drawdown of that same curve (each trade's worst intra-trade excursion is applied as a trough before booking the realised close, so deep round-trip dips count rather than only close-to-close drops). UNITS: dimensionless. N/A when Expected Yearly Returns is N/A (e.g. too few signals, calendar span too short, or trade frequency too clustered to extrapolate) or when the equity max drawdown ≤ 0. Rule of thumb: below 0.5 poor, above 1.0 strong.*`,
27262
+ `*Recovery Factor (best strategy summary): the best strategy's (final equity − 1) × 100 divided by its equity max drawdown, clamped to ±1000. The numerator is the strategy's compounded total return (the equity curve's end value minus its start, in percent) — NOT the arithmetic sum of per-trade PNLs; mixing those units would inflate the ratio on long winning streaks. The denominator is the same mark-to-market max drawdown described under Calmar Ratio. UNITS: dimensionless. N/A when the best strategy has fewer than 10 closed signals, the strategy blew its account (equity ≤ 0), or the equity max drawdown ≤ 0. Rule of thumb: below 1.0 means profit doesn't cover drawdown, above 3.0 is good.*`,
27263
+ `*Top Strategies Comparison (table): up to N strategies (newest-best first) ranked by the optimization metric, where N is the configured top-N limit for this report. Each row shows that strategy's full statistical summary through the configured comparison columns.*`,
27264
+ `*All Signals (PNL Table): every closed signal produced by every tested strategy during the run, in storage order (newest first). Each row shows one closed trade — entry / exit prices, realised PNL percentage and cost, peak / trough excursions, close reason, durations — through the configured PNL columns.*`,
27265
+ "",
27266
+ "**Note:** Higher values are better for all metrics except Standard Deviation (lower is better). The per-strategy ratios shown above (Sortino, Calmar, Recovery) are forwarded verbatim from each strategy's own backtest statistics — Walker only ranks and surfaces those values, it does not recompute them. Their formulas, null-conditions and statistical gates are spelled out in the individual legend entries above."
27228
27267
  ].join("\n");
27229
27268
  }
27230
27269
  /**
@@ -28491,30 +28530,69 @@ class HeatmapStorage {
28491
28530
  "",
28492
28531
  table,
28493
28532
  "",
28494
- `*Win Rate: reliable above 200+ signals; below 30 signals a single streak can shift it by 10-20%.*`,
28495
- `*Pooled Sharpe: Sharpe computed over all trades across symbols treated as one sample. NOT a Markowitz portfolio Sharpe ignores cross-symbol correlations and capital allocation. N/A unless ≥${MIN_SIGNALS_FOR_RATIOS} pooled trades.*`,
28496
- `*Annualized Sharpe: per-trade Sharpe × √tradesPerYear. N/A unless the underlying Sharpe and tradesPerYear are both available (≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals, span ≥${MIN_CALENDAR_SPAN_DAYS} days, raw frequency ≤${MAX_TRADES_PER_YEAR}). Assumes returns are iid autocorrelated strategies are overstated.*`,
28497
- `*Certainty Ratio: avgWin / |avgLoss|. Below 1.0 means average loss exceeds average win. Above 1.5 is considered good. N/A when no losing trades or |avgLoss| is sub-epsilon.*`,
28498
- `*Expected Yearly Returns: compounded geometric return from the equity curve, annualized by tradesPerYear. Same gating as Annualized Sharpe. Capped at ±${MAX_EXPECTED_YEARLY_RETURNS}% values above the cap return N/A.*`,
28499
- `*Trades Per Year: observed trade frequency extrapolated to one year (signals × 365 / calendarSpanDays). N/A when too few signals or too short a calendar span; also null when the raw frequency exceeds ${MAX_TRADES_PER_YEAR} (too clustered for reliable annualization).*`,
28500
- `*Sharpe Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals per symbol.*`,
28501
- `*Sortino Ratio: below 1.0 is poor, 1.0-2.0 is acceptable, above 2.0 is strong. Requires 30+ signals. N/A when no losing trades — Sortino is mathematically undefined (infinite) and we cannot distinguish "truly flawless" from "lucky streak so far".*`,
28502
- `*Profit Factor: below 1.0 means strategy is losing overall. Above 1.5 is considered good.*`,
28503
- `*Calmar Ratio: below 0.5 is poor, 0.5-1.0 is acceptable, above 1.0 is strong. Denominator is the mark-to-market max drawdown (see below). N/A unless ≥${MIN_SIGNALS_FOR_ANNUALIZATION} signals per symbol and span ≥${MIN_CALENDAR_SPAN_DAYS} days. Capped at ±${MAX_CALMAR_RATIO}.*`,
28504
- `*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 and the mark-to-market max drawdown as denominator.*`,
28505
- `*Expectancy: per-trade expected value (winProb × avgWin + lossProb × avgLoss). Positive = profitable on average per trade. Break-even trades contribute 0.*`,
28506
- `*Median PNL: middle value of the pnl distribution. Robust to outliers; compare to Average PNL a large gap signals a skewed distribution (e.g. one whale trade dragging the mean).*`,
28507
- `*Avg Peak PNL / Avg Max Drawdown PNL: mean of per-trade _peak.pnlPercentage / _fall.pnlPercentage. Higher avg-peak with deeper avg-drawdown means strategy needs to tolerate bigger swings to capture the upside.*`,
28508
- `*Peak Profit PNL / Max Drawdown PNL: extremes the best best-case and worst worst-case observed across all trades. Tail behaviour the averages hide.*`,
28509
- `*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").*`,
28510
- `*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).*`,
28511
- `*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.*`,
28512
- `*All metrics require 100+ signals per symbol to be statistically reliable. Annualized metrics assume the observed trading frequency persists year-round.*`,
28513
- `*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.*`,
28514
- `*Trend / Trend %/d / Trend (per-symbol only): linear regression of log(close) vs days across that symbol's closed-trade prices. Classification gates on 0.30 AND |slope| 0.25 × medianStepSize self-tunes to the instrument. Not aggregated portfolio-wide (price series across symbols are not comparable).*`,
28515
- `*Buyer / Seller Pres (per-symbol only): fraction of up-moves (resp. down-moves) among decisive close-to-close changes for that symbol. Buyer / Seller Str: magnitude share. Pres Imb: buyerStrength sellerStrength [−1, +1].*`,
28516
- `*Median Step (per-symbol only): typical |close[i] close[i−1]| / close[i−1] across closed trades, in %. Robust to outliers. NOT classical volatility there are no candles between closes in the report data.*`,
28517
- `*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.*`,
28533
+ `*Total Symbols: count of distinct symbol buckets currently tracked in this storage. Each bucket holds at most CC_MAX_HEATMAP_MARKDOWN_ROWS closed signals (newest-first ring buffer); when capacity is reached the oldest signal is dropped. UNITS: integer count of symbols.*`,
28534
+ `*Portfolio PNL: arithmetic sum of per-trade PNL across every closed signal of every tracked symbol (Σ per-symbol Total PNL over symbols whose Total PNL is non-null). UNITS: percent. Additive total — NOT the compounded equity return. Useful as a quick scoreboard; ignores volatility drag and cross-symbol diversification.*`,
28535
+ `*Pooled Sharpe: pooled mean per-trade PNL divided by the pooled sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL, computed over the POOLED set of every closed signal of every symbol treated as one sample. UNITS: dimensionless ratio. NOT a Markowitz portfolio Sharpe — ignores cross-symbol correlations and capital allocation. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS} OR pooled standard deviation ≤ 1e-9. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong.*`,
28536
+ `*Annualized Sharpe (portfolio): Pooled Sharpe × √(portfolio Trades Per Year). UNITS: dimensionless. Null when Pooled Sharpe is null OR portfolio Trades Per Year is null OR ≤ 0. Assumes returns are iid autocorrelated strategies are overstated.*`,
28537
+ `*Certainty Ratio (portfolio): pooledAvgWin / |pooledAvgLoss| where pooledAvgWin = mean over pooled winning closed signals and pooledAvgLoss = mean over pooled losing closed signals. UNITS: dimensionless. Below 1.0 means the typical loss exceeds the typical win; above 1.5 is generally good. Null when pooled N < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades in the pool, OR |pooledAvgLoss| < 1e-9.*`,
28538
+ `*Expected Yearly Returns (portfolio): geometric annualisation of the pooled equity curve: (pooled final equity ^ (portfolio Trades Per Year / pooled trade count) 1) × 100, where the pooled equity curve walks every closed signal of every symbol chronologically by close-time and compounds (1 + per-trade PNL / 100). UNITS: percent per year. Null when the pooled calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, raw portfolio Trades Per Year > ${MAX_TRADES_PER_YEAR}, or |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS}%. −100% if the pooled equity curve hits ≤ 0.*`,
28539
+ `*Trades Per Year (portfolio): pooled trade count × 365 / pooled span in days, where pooled span = (latest close-time − earliest pending-time across the whole pool) / day. UNITS: trades / year. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR pooled span < ${MIN_CALENDAR_SPAN_DAYS} days, OR raw value > ${MAX_TRADES_PER_YEAR} (sample too clustered to extrapolate reliably). NOTE: portfolio Trades Per Year inherits the ratios-block sample-size gate (MIN_SIGNALS_FOR_RATIOS) on the pooled count, whereas the per-symbol Trades Per Year column uses the annualisation-block gate (MIN_SIGNALS_FOR_ANNUALIZATION) on the per-symbol count. The two constants are equal (${MIN_SIGNALS_FOR_RATIOS}) in this build, so this never produces a discrepancy in practice; should they ever diverge, portfolio Trades Per Year would surface earlier than per-symbol Trades Per Year, which is consistent with the rest of the portfolio block already being scoped under the ratios gate.*`,
28540
+ `*Total Trades (portfolio = portfolioTotalTrades): sum of per-symbol totalTrades over all tracked symbols. UNITS: integer count.*`,
28541
+ `*Avg Peak PNL (portfolio): trade-count-weighted mean of per-symbol Avg Peak PNL over symbols whose value is non-null. Weights are per-symbol Total Trades; symbols without any peak-snapshot signals don't contribute and don't dilute. UNITS: percent. Describes the portfolio's typical best-case unrealised excursion. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Avg Peak PNL is null.*`,
28542
+ `*Avg Max Drawdown PNL (portfolio): trade-count-weighted mean of per-symbol Avg Max Drawdown PNL over symbols whose value is non-null. UNITS: percent (negative for losing excursions). Closer to 0 is better describes the portfolio's typical worst-case unrealised excursion. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Avg Max Drawdown PNL is null.*`,
28543
+ `*Peak Profit PNL (portfolio): MAX of per-symbol Peak Profit PNL over all symbols whose value is non-null. UNITS: percent. The single best best-case excursion observed anywhere across the portfolio (tail behaviour the average hides). NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Peak Profit PNL is null.*`,
28544
+ `*Max Drawdown PNL (portfolio): MIN of per-symbol Max Drawdown PNL over all symbols whose value is non-null. UNITS: percent (negative). The single deepest unrealised dip observed anywhere across the portfolio. NOT gated by MIN_SIGNALS at portfolio level — null only if every symbol's Max Drawdown PNL is null.*`,
28545
+ `*Median PNL (portfolio): median of per-trade PNL over the POOLED set of every closed signal of every symbol (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers; compare with the pooled mean per-trade PNL that underlies Pooled Sharpe to detect distribution skew (a large gap means one or two trades drag the arithmetic mean). NOT gated by MIN_SIGNALS — computed whenever pooled trade count 1.*`,
28546
+ `*Avg Duration (portfolio): arithmetic mean of (close-time − pending-time) / 60_000 over the pooled set of every closed signal of every symbol that has both timestamps. UNITS: minutes. NOT gated by MIN_SIGNALS computed whenever at least one closed signal across the pool carries valid timestamps.*`,
28547
+ `*Avg Win Duration (portfolio): mean hold time in minutes restricted to pooled winning closed signals (per-trade PNL > 0). UNITS: minutes. Null only when there are no winning trades across the pool (NOT gated by MIN_SIGNALS).*`,
28548
+ `*Avg Loss Duration (portfolio): mean hold time in minutes restricted to pooled losing closed signals (per-trade PNL < 0). UNITS: minutes. Null only when there are no losing trades across the pool (NOT gated by MIN_SIGNALS). Pair with Avg Win Duration to detect the asymmetry "winner-shorter-than-loser" (a red flag: cut winners short, let losers run).*`,
28549
+ `*Avg Consecutive Win PNL (portfolio): trade-count-weighted mean of per-symbol Avg Consecutive Win PNL over symbols whose value is non-null. A per-symbol "win streak" is a run of consecutive same-symbol closed signals with per-trade PNL > 0 bounded by a losing or break-even trade; the per-symbol metric averages the SUM of per-trade PNL within each streak. The portfolio aggregate weights by per-symbol Total Trades — concatenating streaks ACROSS symbols would be meaningless (different markets, different timeframes). UNITS: percent per streak. NOT gated by MIN_SIGNALS at portfolio level — null only when every per-symbol Avg Consecutive Win PNL is null (i.e. no symbol has a single complete win streak).*`,
28550
+ `*Avg Consecutive Loss PNL (portfolio): trade-count-weighted mean of per-symbol Avg Consecutive Loss PNL over symbols whose value is non-null. Same weighting rationale as the Win-streak counterpart above. UNITS: percent per streak (negative). Closer to 0 = losing streaks are typically smaller. NOT gated by MIN_SIGNALS at portfolio level null only when every per-symbol Avg Consecutive Loss PNL is null.*`,
28551
+ `*Standard Deviation Per Trade (portfolio): sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL over the pooled set of every closed signal of every symbol. UNITS: percent. Denominator for Pooled Sharpe. Below 1e-9 it is treated as zero (identical-returns guard). Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}.*`,
28552
+ `*Sortino Ratio (portfolio): pooled-mean per-trade PNL divided by pooled downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / pooled trade count ) over the pool (canonical Sortino: MAR = 0, divide by N_total). UNITS: dimensionless. Penalises only downside volatility. Rule of thumb: below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades in the pool, OR pooled downside deviation 1e-9 (float-artifact guard).*`,
28553
+ `*Calmar Ratio (portfolio): portfolio Expected Yearly Returns divided by the pooled mark-to-market max drawdown, clamped to ±${MAX_CALMAR_RATIO}. The denominator is the max drawdown of the POOLED equity curve walked chronologically by close-timestamp across every symbol — each closed signal's worst intra-trade excursion (its trough-PNL snapshot, 0) is applied before booking its realised close, so deep round-trip dips count. Cross-symbol simultaneous drawdowns within the same minute are still serialised (one signal applied at a time), so genuine same-instant tail correlation is not modelled. UNITS: dimensionless. Null when the portfolio Expected Yearly Returns is null OR the pooled equity max drawdown ≤ 0.*`,
28554
+ `*Recovery Factor (portfolio): (pooled final equity − 1) × 100 divided by the pooled equity max drawdown, clamped to ±${MAX_CALMAR_RATIO}. The numerator is the compounded total return of the pooled equity curve — NOT the arithmetic Portfolio PNL shown at the top of this report; the denominator is computed on the compounded curve, so mixing those units would inflate the ratio on long winning streaks. The denominator is the same pooled mark-to-market max drawdown used by Calmar. UNITS: dimensionless. Below 1.0 = compounded profit doesn't cover max drawdown; above 3.0 = good. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, the pooled equity curve blew up (reached ≤ 0), or the pooled equity max drawdown ≤ 0.*`,
28555
+ `*Expectancy (portfolio): (pooled winning-trade count / pooled trade count) × pooled mean of winning PNLs + (pooled losing-trade count / pooled trade count) × pooled mean of losing PNLs. Break-even closed signals contribute 0 (excluded from both probabilities). UNITS: percent per trade. Positive = profitable on average per trade across the portfolio. Null when pooled trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR when the entire pool is break-even (no wins AND no losses).*`,
28556
+ ``,
28557
+ `*--- Per-symbol table columns ---*`,
28558
+ `*Total PNL (column): arithmetic sum of per-trade PNL across that symbol's stored closed signals. UNITS: percent. Additive total — NOT a compounded equity return. Null only when that symbol has zero stored signals.*`,
28559
+ `*Total Trades (column): integer count of that symbol's stored closed signals. UNITS: count.*`,
28560
+ `*Win Rate (column): per-symbol winning-trade count / (winning-trade count + losing-trade count) × 100; break-even trades (per-trade PNL == 0) are excluded from both numerator and denominator. UNITS: percent in [0, 100]. Statistically noisy below 30 per-symbol signals; stable above 200. NOT gated by MIN_SIGNALS — null only when there are no decisive trades for that symbol.*`,
28561
+ `*Avg PNL (column): arithmetic mean of per-trade PNL across that symbol's signals — Σ per-trade PNL / Total Trades. UNITS: percent per trade. Null only when Total Trades = 0.*`,
28562
+ `*Standard Deviation (column): per-symbol sample standard deviation (Bessel-corrected, N−1 denominator) of per-trade PNL. UNITS: percent. Denominator for per-symbol Sharpe Ratio. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}.*`,
28563
+ `*Avg Win (column): arithmetic mean of per-trade PNL restricted to that symbol's winning trades (per-trade PNL > 0). UNITS: percent. NOT gated by MIN_SIGNALS — null only when that symbol has no winning trades.*`,
28564
+ `*Avg Loss (column): arithmetic mean of per-trade PNL restricted to that symbol's losing trades (per-trade PNL < 0). UNITS: percent (negative). NOT gated by MIN_SIGNALS — null only when that symbol has no losing trades.*`,
28565
+ `*Max Win Streak (column): longest run of consecutive same-symbol closed signals with per-trade PNL > 0, bounded by an opposite-sign or break-even trade. UNITS: integer count of consecutive wins. The streak structure is sign-invariant under reversal, so the count is identical whether signals are walked in chronological or reverse-chronological order. NOT gated by MIN_SIGNALS — 0 when that symbol has no winning trades.*`,
28566
+ `*Max Loss Streak (column): longest run of consecutive same-symbol closed signals with per-trade PNL < 0. UNITS: integer count. Same iteration rules as Max Win Streak. NOT gated by MIN_SIGNALS — 0 when that symbol has no losing trades.*`,
28567
+ `*Median PNL (column): per-symbol median of per-trade PNL (middle value after sorting; for even N the mean of the two middles). UNITS: percent. Robust to outliers; compare with Avg PNL to detect distribution skew. NOT gated by MIN_SIGNALS — null only when Total Trades = 0.*`,
28568
+ `*Sharpe Ratio (column): per-symbol Avg PNL / Standard Deviation (risk-free rate = 0). UNITS: dimensionless. Below 1.0 poor, 1.0–2.0 acceptable, above 2.0 strong. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} OR Standard Deviation ≤ 1e-9 (identical-returns / float-artifact guard).*`,
28569
+ `*Annualized Sharpe (column): per-symbol Sharpe Ratio × √(per-symbol Trades Per Year), where Trades Per Year for that symbol = its trade count × 365 / its calendar span in days. UNITS: dimensionless. Null when per-symbol Sharpe Ratio is null, OR that symbol's Trades Per Year is null (requires trade count ≥ ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days, raw frequency ≤ ${MAX_TRADES_PER_YEAR}). Assumes iid returns.*`,
28570
+ `*Certainty Ratio (column): per-symbol Avg Win / |Avg Loss|. UNITS: dimensionless. Below 1.0 = typical loss exceeds typical win; above 1.5 generally good. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR Avg Win / Avg Loss missing, OR Avg Loss ≥ 0, OR |Avg Loss| < 1e-9 (float-artifact loss guard).*`,
28571
+ `*Expected Yearly Returns (column): per-symbol geometric annualisation of that symbol's equity curve — (final equity ^ (Trades Per Year / trade count) − 1) × 100, where final equity is the compounded product of (1 + per-trade PNL / 100) walked over that symbol's signals in chronological close order. UNITS: percent per year. Null when trade count < ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, raw frequency > ${MAX_TRADES_PER_YEAR}, or |raw value| > ${MAX_EXPECTED_YEARLY_RETURNS}%. −100% when the symbol's equity curve hits ≤ 0.*`,
28572
+ `*Trades Per Year (column): per-symbol trade count × 365 / calendar span in days, where calendar span = (latest close-time − earliest pending-time for that symbol) / day. UNITS: trades / year. Null when trade count < ${MIN_SIGNALS_FOR_ANNUALIZATION}, calendar span < ${MIN_CALENDAR_SPAN_DAYS} days, or raw value > ${MAX_TRADES_PER_YEAR}.*`,
28573
+ `*Profit Factor (column): per-symbol Σ winning per-trade PNL / |Σ losing per-trade PNL|. UNITS: dimensionless. Below 1.0 means the symbol is net losing; above 1.5 is generally good. Null when there are no winning or no losing trades for that symbol, or when Σ|losing PNL| < 1e-9 (float-artifact guard).*`,
28574
+ `*Sortino Ratio (column): per-symbol Avg PNL / downside deviation, where downside deviation = √( Σ min(0, per-trade PNL)² / total trade count ) (canonical Sortino: MAR = 0, divide by N_total). UNITS: dimensionless. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, OR no losing trades, OR downside deviation ≤ 1e-9 (float-artifact guard).*`,
28575
+ `*Calmar Ratio (column): per-symbol Expected Yearly Returns / Max Drawdown, clamped to ±${MAX_CALMAR_RATIO}. Denominator is the mark-to-market max drawdown of that symbol's compounded equity curve. Null when Expected Yearly Returns is null (requires ≥ ${MIN_SIGNALS_FOR_ANNUALIZATION} signals and a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days for that symbol) OR Max Drawdown ≤ 0.*`,
28576
+ `*Recovery Factor (column): per-symbol (final equity − 1) × 100 / Max Drawdown, clamped to ±${MAX_CALMAR_RATIO}. The numerator is the compounded total return of that symbol's equity curve. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, the equity curve blew up (reached ≤ 0), or Max Drawdown ≤ 0.*`,
28577
+ `*Expectancy (column): per-symbol expected value per trade. Three cases depending on what kinds of trades exist for that symbol: (a) BOTH winning and losing trades present → (winning-trade count / Total Trades) × Avg Win + (losing-trade count / Total Trades) × Avg Loss; (b) only winning trades present → (winning-trade count / Total Trades) × Avg Win (zero contribution from non-existent losses); (c) only losing trades present → (losing-trade count / Total Trades) × Avg Loss. Break-even trades contribute 0 (excluded from both probabilities). UNITS: percent per trade. NOT gated by MIN_SIGNALS — computed whenever Total Trades ≥ 1 and at least one decisive trade exists. (Note: the portfolio-level Expectancy further up in this report uses a single combined formula and IS gated by MIN_SIGNALS_FOR_RATIOS over the pooled count; per-symbol Expectancy is intentionally looser to populate the row early.)*`,
28578
+ `*Max Drawdown (column): per-symbol mark-to-market max drawdown — the symbol's compounded equity curve applies each closed signal's worst intra-trade excursion (its trough-PNL snapshot, ≤ 0) before booking the realised close, so deep round-trip dips count rather than only realised close-to-close drops. UNITS: percent. NOT realised-only.*`,
28579
+ `*Avg Peak PNL / Avg Max Drawdown PNL (columns): per-symbol arithmetic means of each closed signal's peak-PNL / trough-PNL snapshot — the best / worst mark-to-market PNL recorded while the position was open. Signals that never recorded the snapshot are excluded — no zero dilution. UNITS: percent. NOT gated by MIN_SIGNALS — each is null only if no signal for that symbol carries the corresponding snapshot.*`,
28580
+ `*Peak Profit PNL / Max Drawdown PNL (columns): per-symbol MAX of the peak-PNL snapshot / MIN of the trough-PNL snapshot across the symbol's stored closed signals. UNITS: percent. The single best best-case and worst worst-case excursions for that symbol — tail behaviour the averages hide. NOT gated by MIN_SIGNALS — each is null only if no signal for that symbol carries the corresponding snapshot.*`,
28581
+ `*Avg Duration / Avg Win Duration / Avg Loss Duration (columns): per-symbol mean hold time in minutes — (close-time − pending-time) / 60_000 — over all signals / winning signals / losing signals respectively. UNITS: minutes. NOT gated by MIN_SIGNALS — each is null only if the corresponding bucket (all / wins / losses) is empty for that symbol. Pair Avg Win vs Avg Loss durations to detect "let winners run, cut losers short" (Avg Win Duration > Avg Loss Duration is healthy).*`,
28582
+ `*Avg Consecutive Win PNL / Avg Consecutive Loss PNL (columns): a per-symbol "streak" is a run of consecutive same-symbol closed signals with same-sign per-trade PNL bounded by an opposite-sign or break-even trade; each metric sums per-trade PNL within each streak and averages those sums. The streak structure is sign-invariant under iteration order, so the resulting average is the same whether signals are walked chronologically or in reverse. UNITS: percent per streak. NOT gated by MIN_SIGNALS — each is null only if not a single complete streak of the corresponding sign exists in that symbol's history. Pair with Max Win Streak / Max Loss Streak to compare typical vs worst-case streak magnitude.*`,
28583
+ `*Trend (column, per-symbol only): bivariate classification computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol (one point per closed trade = the price at which it closed, with its close-timestamp — no candles, no order book, no tick stream). "sideways" when R² < 0.30; otherwise "neutral" when |Trend Strength| < 0.25 × Median Step Size; otherwise "bullish" if Trend Strength > 0, "bearish" if Trend Strength < 0. Two-axis gate, never a single magic constant on slope magnitude. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS}, which means Median Step Size, Trend Strength and Trend Confidence all gate as one bundle: if any is null the others are null too, so the classification never sees a partially-computed input. Boundary case: when every closed price is identical (a flat synthetic series at N ≥ ${MIN_SIGNALS_FOR_RATIOS}), Median Step Size is 0 and the slope-vs-step gate collapses — the regression then returns slope = R² = 0, R² < 0.30 fires first, and the classification is "sideways" as expected. Not aggregated portfolio-wide because price series across symbols are not comparable.*`,
28584
+ `*Trend %/d (column, per-symbol only): ordinary-least-squares slope of \`log(close) ~ days\` regressed across CLOSING PRICES OF CLOSED SIGNALS of that symbol. UNITS: percent per day (small-slope approximation). NOT ATR-normalised, NOT ADX, NOT a rolling-window indicator — static fit at report time. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} or the calendar span is degenerate.*`,
28585
+ `*Trend R² (column, per-symbol only): coefficient of determination of the same log-price regression that produces Trend %/d, computed on log(close) (NOT on absolute close). UNITS: dimensionless in [0, 1]. High R² = clean trend; low R² = noisy regardless of slope sign. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} or the calendar span is degenerate.*`,
28586
+ `*Buyer Pres (column, per-symbol only): frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. (count of i where close[i] > close[i−1]) / (count of decisive moves); flats (close[i] == close[i−1]) excluded from numerator and denominator. UNITS: dimensionless fraction in [0, 1]. NOT order-flow aggressor volume — the report has no orderbook access; "buyer" labels only the SIGN of the close-to-close return.*`,
28587
+ `*Seller Pres (column, per-symbol only): frequency-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. (count of i where close[i] < close[i−1]) / (count of decisive moves). By construction Buyer Pres + Seller Pres = 1. UNITS: dimensionless fraction in [0, 1].*`,
28588
+ `*Buyer Str (column, per-symbol only): magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. Σ |close[i] − close[i−1]| / close[i−1] over up-moves, divided by the same sum over ALL decisive moves. UNITS: dimensionless fraction in [0, 1]. Buyer Pres (count) and Buyer Str (Σ|Δ|) use the SAME return series; divergence between them (e.g. Pres 0.70 with Str 0.45) reveals regime asymmetry: many small up-moves vs fewer but larger down-moves.*`,
28589
+ `*Seller Str (column, per-symbol only): magnitude-based, computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. Σ |close[i] − close[i−1]| / close[i−1] over down-moves, divided by the same sum over ALL decisive moves. By construction Buyer Str + Seller Str = 1.*`,
28590
+ `*Pres Imb (column, per-symbol only): DIFFERENCE, not ratio: Buyer Str − Seller Str = (2 × Buyer Str − 1), computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol. UNITS: dimensionless in [−1, +1]. Sign = direction of magnitude bias.*`,
28591
+ `*Median Step (column, per-symbol only): median over i ∈ [1..N−1] of |close[i] − close[i−1]| / close[i−1], computed FROM CLOSING PRICES OF CLOSED SIGNALS of that symbol — "step" = consecutive trade closes (NOT ticks, NOT bars of any timeframe; the report has no candle access). UNITS: percent (normalised by price → directly comparable between symbols at any price level). Median (not mean) → robust to one whale trade. NOT classical candle-based volatility (ATR / σ of returns); it measures the step distribution AT THE RATE TRADES CLOSE.*`,
28592
+ ``,
28593
+ `*General reliability note: per-symbol ratios (Sharpe, Sortino, Certainty, Recovery, Expectancy) are gated to N/A below ${MIN_SIGNALS_FOR_RATIOS} per-symbol signals. Annualised metrics (Annualized Sharpe, Expected Yearly Returns, Calmar) additionally require a calendar span ≥ ${MIN_CALENDAR_SPAN_DAYS} days and a raw trade frequency ≤ ${MAX_TRADES_PER_YEAR} per year. The same gates apply to portfolio-level pooled metrics, with the pooled trade count replacing per-symbol trade count. 100+ per-symbol signals are needed for statistical reliability of the ratios; annualised metrics assume the observed frequency and market regime persist year-round.*`,
28594
+ `*IMPORTANT: per-symbol AND pooled equity curve, Expected Yearly Returns, Calmar, Recovery and Max Drawdown all assume **100% capital allocation per position** (no portfolio fraction). They ignore the position-sizing subsystem (PositionSize / Kelly / ATR): per-trade PNL is a return on the position's own invested capital, never scaled by the account balance. With DCA averaging, the cost basis is the sum of all entries and the entry price is dollar-cost-weighted. If your strategy risks X% of capital per trade, the realised return / drawdown is roughly X/100 of the reported figures — these are theoretical upper bounds under full allocation.*`,
28595
+ `*Negative values for Sharpe / Annualized Sharpe / Sortino / Calmar / Recovery / Expectancy / Expected Yearly Returns indicate a losing symbol or portfolio (Average PNL < 0 or Total PNL < 0). "Higher is better" still applies — closer to zero is less bad, positive is profitable.*`,
28518
28596
  ].join("\n");
28519
28597
  }
28520
28598
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "12.0.0",
3
+ "version": "12.1.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",