backtest-kit 12.1.0 → 12.3.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
@@ -12383,6 +12383,10 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => functoolsKit.trycatch(async (sy
12383
12383
  backtest,
12384
12384
  timestamp,
12385
12385
  };
12386
+ {
12387
+ await self.priceMetaService.next(symbol, currentPrice, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12388
+ await self.timeMetaService.next(symbol, timestamp, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12389
+ }
12386
12390
  await schedulePingSubject.next(event);
12387
12391
  await self.actionCoreService.pingScheduled(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
12388
12392
  }, {
@@ -12417,6 +12421,10 @@ const CREATE_COMMIT_IDLE_PING_FN = (self) => functoolsKit.trycatch(async (symbol
12417
12421
  backtest,
12418
12422
  timestamp,
12419
12423
  };
12424
+ {
12425
+ await self.priceMetaService.next(symbol, currentPrice, { strategyName, exchangeName, frameName: frameName }, backtest);
12426
+ await self.timeMetaService.next(symbol, timestamp, { strategyName, exchangeName, frameName: frameName }, backtest);
12427
+ }
12420
12428
  await idlePingSubject.next(event);
12421
12429
  await self.actionCoreService.pingIdle(backtest, event, { strategyName, exchangeName, frameName });
12422
12430
  }, {
@@ -12451,6 +12459,10 @@ const CREATE_COMMIT_ACTIVE_PING_FN = (self) => functoolsKit.trycatch(async (symb
12451
12459
  backtest,
12452
12460
  timestamp,
12453
12461
  };
12462
+ {
12463
+ await self.priceMetaService.next(symbol, currentPrice, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12464
+ await self.timeMetaService.next(symbol, timestamp, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12465
+ }
12454
12466
  await activePingSubject.next(event);
12455
12467
  await self.actionCoreService.pingActive(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
12456
12468
  }, {
@@ -27679,9 +27691,12 @@ class HeatmapStorage {
27679
27691
  }
27680
27692
  // Per-trade Sharpe Ratio
27681
27693
  let sharpeRatio = null;
27694
+ // canComputeRatios gate is explicit here (matching the standalone Backtest/Live
27695
+ // paths) even though stdDev is already null below MIN_SIGNALS_FOR_RATIOS — relying
27696
+ // on the stdDev===null implication would couple the gate to an unrelated branch.
27682
27697
  // STDDEV_EPSILON guard — protects against float-artifact stdDev producing
27683
27698
  // spuriously astronomical sharpe on identical-returns symbols.
27684
- if (avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
27699
+ if (canComputeRatios && avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
27685
27700
  sharpeRatio = avgPnl / stdDev;
27686
27701
  }
27687
27702
  // Equity-curve max drawdown via compounded equity ("as-if 100% allocation per trade").
@@ -27874,17 +27889,21 @@ class HeatmapStorage {
27874
27889
  : sorted[mid];
27875
27890
  }
27876
27891
  // Expectancy — probabilities from observed win/loss counts (break-evens contribute 0).
27892
+ // Gated by MIN_SIGNALS_FOR_RATIOS (via canComputeRatios), same as the standalone
27893
+ // Backtest/Live paths and the portfolio-level pooled Expectancy below — on a tiny
27894
+ // sample the per-trade EV is too noisy to publish, and an ungated per-symbol value
27895
+ // would disagree with the symbol's own standalone report (which IS gated).
27877
27896
  let expectancy = null;
27878
- if (totalTrades > 0 && avgWin !== null && avgLoss !== null) {
27897
+ if (canComputeRatios && totalTrades > 0 && avgWin !== null && avgLoss !== null) {
27879
27898
  const winProb = winCount / totalTrades;
27880
27899
  const lossProb = lossCount / totalTrades;
27881
27900
  expectancy = winProb * avgWin + lossProb * avgLoss;
27882
27901
  }
27883
- else if (totalTrades > 0 && avgWin !== null && avgLoss === null) {
27902
+ else if (canComputeRatios && totalTrades > 0 && avgWin !== null && avgLoss === null) {
27884
27903
  // No losing trades — expectancy is just average win frequency × avgWin
27885
27904
  expectancy = (winCount / totalTrades) * avgWin;
27886
27905
  }
27887
- else if (totalTrades > 0 && avgWin === null && avgLoss !== null) {
27906
+ else if (canComputeRatios && totalTrades > 0 && avgWin === null && avgLoss !== null) {
27888
27907
  expectancy = (lossCount / totalTrades) * avgLoss;
27889
27908
  }
27890
27909
  // Average only over signals that have the value — do not dilute the mean with zeros.
@@ -28594,7 +28613,7 @@ class HeatmapStorage {
28594
28613
  `*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
28614
  `*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
28615
  `*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.)*`,
28616
+ `*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. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} the same sample-size gate as the standalone Backtest/Live report and the portfolio-level pooled Expectancy further up, so the per-symbol value never disagrees with the symbol's own standalone report.*`,
28598
28617
  `*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
28618
  `*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
28619
  `*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.*`,
package/build/index.mjs CHANGED
@@ -12363,6 +12363,10 @@ const CREATE_COMMIT_SCHEDULE_PING_FN = (self) => trycatch(async (symbol, strateg
12363
12363
  backtest,
12364
12364
  timestamp,
12365
12365
  };
12366
+ {
12367
+ await self.priceMetaService.next(symbol, currentPrice, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12368
+ await self.timeMetaService.next(symbol, timestamp, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12369
+ }
12366
12370
  await schedulePingSubject.next(event);
12367
12371
  await self.actionCoreService.pingScheduled(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
12368
12372
  }, {
@@ -12397,6 +12401,10 @@ const CREATE_COMMIT_IDLE_PING_FN = (self) => trycatch(async (symbol, strategyNam
12397
12401
  backtest,
12398
12402
  timestamp,
12399
12403
  };
12404
+ {
12405
+ await self.priceMetaService.next(symbol, currentPrice, { strategyName, exchangeName, frameName: frameName }, backtest);
12406
+ await self.timeMetaService.next(symbol, timestamp, { strategyName, exchangeName, frameName: frameName }, backtest);
12407
+ }
12400
12408
  await idlePingSubject.next(event);
12401
12409
  await self.actionCoreService.pingIdle(backtest, event, { strategyName, exchangeName, frameName });
12402
12410
  }, {
@@ -12431,6 +12439,10 @@ const CREATE_COMMIT_ACTIVE_PING_FN = (self) => trycatch(async (symbol, strategyN
12431
12439
  backtest,
12432
12440
  timestamp,
12433
12441
  };
12442
+ {
12443
+ await self.priceMetaService.next(symbol, currentPrice, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12444
+ await self.timeMetaService.next(symbol, timestamp, { strategyName, exchangeName, frameName: data.frameName }, backtest);
12445
+ }
12434
12446
  await activePingSubject.next(event);
12435
12447
  await self.actionCoreService.pingActive(backtest, event, { strategyName, exchangeName, frameName: data.frameName });
12436
12448
  }, {
@@ -27659,9 +27671,12 @@ class HeatmapStorage {
27659
27671
  }
27660
27672
  // Per-trade Sharpe Ratio
27661
27673
  let sharpeRatio = null;
27674
+ // canComputeRatios gate is explicit here (matching the standalone Backtest/Live
27675
+ // paths) even though stdDev is already null below MIN_SIGNALS_FOR_RATIOS — relying
27676
+ // on the stdDev===null implication would couple the gate to an unrelated branch.
27662
27677
  // STDDEV_EPSILON guard — protects against float-artifact stdDev producing
27663
27678
  // spuriously astronomical sharpe on identical-returns symbols.
27664
- if (avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
27679
+ if (canComputeRatios && avgPnl !== null && stdDev !== null && stdDev > STDDEV_EPSILON) {
27665
27680
  sharpeRatio = avgPnl / stdDev;
27666
27681
  }
27667
27682
  // Equity-curve max drawdown via compounded equity ("as-if 100% allocation per trade").
@@ -27854,17 +27869,21 @@ class HeatmapStorage {
27854
27869
  : sorted[mid];
27855
27870
  }
27856
27871
  // Expectancy — probabilities from observed win/loss counts (break-evens contribute 0).
27872
+ // Gated by MIN_SIGNALS_FOR_RATIOS (via canComputeRatios), same as the standalone
27873
+ // Backtest/Live paths and the portfolio-level pooled Expectancy below — on a tiny
27874
+ // sample the per-trade EV is too noisy to publish, and an ungated per-symbol value
27875
+ // would disagree with the symbol's own standalone report (which IS gated).
27857
27876
  let expectancy = null;
27858
- if (totalTrades > 0 && avgWin !== null && avgLoss !== null) {
27877
+ if (canComputeRatios && totalTrades > 0 && avgWin !== null && avgLoss !== null) {
27859
27878
  const winProb = winCount / totalTrades;
27860
27879
  const lossProb = lossCount / totalTrades;
27861
27880
  expectancy = winProb * avgWin + lossProb * avgLoss;
27862
27881
  }
27863
- else if (totalTrades > 0 && avgWin !== null && avgLoss === null) {
27882
+ else if (canComputeRatios && totalTrades > 0 && avgWin !== null && avgLoss === null) {
27864
27883
  // No losing trades — expectancy is just average win frequency × avgWin
27865
27884
  expectancy = (winCount / totalTrades) * avgWin;
27866
27885
  }
27867
- else if (totalTrades > 0 && avgWin === null && avgLoss !== null) {
27886
+ else if (canComputeRatios && totalTrades > 0 && avgWin === null && avgLoss !== null) {
27868
27887
  expectancy = (lossCount / totalTrades) * avgLoss;
27869
27888
  }
27870
27889
  // Average only over signals that have the value — do not dilute the mean with zeros.
@@ -28574,7 +28593,7 @@ class HeatmapStorage {
28574
28593
  `*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
28594
  `*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
28595
  `*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.)*`,
28596
+ `*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. Null when that symbol's trade count < ${MIN_SIGNALS_FOR_RATIOS} the same sample-size gate as the standalone Backtest/Live report and the portfolio-level pooled Expectancy further up, so the per-symbol value never disagrees with the symbol's own standalone report.*`,
28578
28597
  `*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
28598
  `*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
28599
  `*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.*`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "12.1.0",
3
+ "version": "12.3.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",