@adaptic/utils 0.0.1031 → 0.0.1032

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.
Files changed (29) hide show
  1. package/dist/index.cjs +306 -133
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.mjs +303 -134
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/types/__tests__/indicator-parity/generate.d.ts +64 -0
  6. package/dist/types/__tests__/indicator-parity/generate.d.ts.map +1 -0
  7. package/dist/types/__tests__/indicator-parity/record.d.ts +144 -0
  8. package/dist/types/__tests__/indicator-parity/record.d.ts.map +1 -0
  9. package/dist/types/__tests__/indicator-parity/reference.d.ts +92 -0
  10. package/dist/types/__tests__/indicator-parity/reference.d.ts.map +1 -0
  11. package/dist/types/__tests__/indicator-parity/series.d.ts +64 -0
  12. package/dist/types/__tests__/indicator-parity/series.d.ts.map +1 -0
  13. package/dist/types/__tests__/indicator-parity/subjects.d.ts +55 -0
  14. package/dist/types/__tests__/indicator-parity/subjects.d.ts.map +1 -0
  15. package/dist/types/__tests__/support/statistic.d.ts +18 -0
  16. package/dist/types/__tests__/support/statistic.d.ts.map +1 -0
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/index.d.ts.map +1 -1
  19. package/dist/types/metrics-calcs.d.ts +25 -0
  20. package/dist/types/metrics-calcs.d.ts.map +1 -1
  21. package/dist/types/performance-metrics.d.ts +16 -4
  22. package/dist/types/performance-metrics.d.ts.map +1 -1
  23. package/dist/types/sample-statistic.d.ts +123 -0
  24. package/dist/types/sample-statistic.d.ts.map +1 -0
  25. package/dist/types/strategy-metrics.d.ts +38 -16
  26. package/dist/types/strategy-metrics.d.ts.map +1 -1
  27. package/dist/types/types/alpaca-types.d.ts +26 -2
  28. package/dist/types/types/alpaca-types.d.ts.map +1 -1
  29. package/package.json +3 -1
package/dist/index.cjs CHANGED
@@ -20243,6 +20243,93 @@ function getEquityValues(equityData, portfolioHistory, marketTimeUtil, period) {
20243
20243
  };
20244
20244
  }
20245
20245
 
20246
+ /**
20247
+ * A measured statistic and the cohort it was measured on, carried as one
20248
+ * inseparable value.
20249
+ *
20250
+ * A ratio is meaningless without the population it was taken over: the same
20251
+ * `0.42` is a strong result on 2,000 trades and noise on five, and a `0.0`
20252
+ * returned because nothing could be computed is indistinguishable from a `0.0`
20253
+ * that was genuinely measured. Both confusions are the same error — a number
20254
+ * read apart from its unit and its cohort — and both have produced wrong
20255
+ * conclusions from correct arithmetic.
20256
+ *
20257
+ * This type removes the option. Every statistic shaped by a population carries
20258
+ * `sampleCount` (how many observations actually entered the computation) and
20259
+ * `coverage` (what fraction of the observations the caller offered were usable),
20260
+ * on BOTH branches: an unavailable statistic still reports how much data it
20261
+ * saw, because "we had nothing" and "we had 900 rows and still could not
20262
+ * compute it" are different facts with different responses.
20263
+ *
20264
+ * Absence is a branch of the union rather than a sentinel value. There is no
20265
+ * number a caller can read without first proving the statistic exists, which is
20266
+ * what keeps an unknown from silently becoming a zero on its way to a decision.
20267
+ *
20268
+ * @module sample-statistic
20269
+ */
20270
+ /**
20271
+ * Build the cohort descriptor for a computation.
20272
+ *
20273
+ * `coverage` is derived here rather than supplied, so it cannot drift from the
20274
+ * counts it claims to summarise. A zero request yields zero coverage: no
20275
+ * observations were asked for, so none were covered, and the alternative (`1`)
20276
+ * would report a vacuous computation as fully covered.
20277
+ *
20278
+ * @param requestedCount - Observations offered, or the window width requested.
20279
+ * @param sampleCount - Observations that entered the computation.
20280
+ * @returns The cohort descriptor with `coverage` derived from the two counts.
20281
+ * @throws When either count is negative or non-finite, which is a programming
20282
+ * error rather than a data condition.
20283
+ */
20284
+ function sampleCohort(requestedCount, sampleCount) {
20285
+ if (!Number.isFinite(requestedCount) || requestedCount < 0) {
20286
+ throw new Error(`sampleCohort: requestedCount must be a non-negative finite number (got ${requestedCount})`);
20287
+ }
20288
+ if (!Number.isFinite(sampleCount) || sampleCount < 0) {
20289
+ throw new Error(`sampleCohort: sampleCount must be a non-negative finite number (got ${sampleCount})`);
20290
+ }
20291
+ const NOTHING_REQUESTED_COVERAGE = 0;
20292
+ const FULL_COVERAGE = 1;
20293
+ const coverage = requestedCount === 0
20294
+ ? NOTHING_REQUESTED_COVERAGE
20295
+ : Math.min(FULL_COVERAGE, sampleCount / requestedCount);
20296
+ return { sampleCount, requestedCount, coverage };
20297
+ }
20298
+ /**
20299
+ * Wrap a computed value with its cohort.
20300
+ *
20301
+ * @param value - The measured statistic.
20302
+ * @param cohort - The cohort it was measured on.
20303
+ * @returns The available branch of {@link SampleStatistic}.
20304
+ */
20305
+ function availableStatistic(value, cohort) {
20306
+ return { available: true, value, ...cohort };
20307
+ }
20308
+ /**
20309
+ * Record that a statistic could not be computed, and what was seen instead.
20310
+ *
20311
+ * @param reason - Which class of failure prevented the computation.
20312
+ * @param detail - Specifics for logs; never machine-parsed.
20313
+ * @param cohort - What data was available when the attempt was abandoned.
20314
+ * @returns The unavailable branch of {@link SampleStatistic}.
20315
+ */
20316
+ function unavailableStatistic(reason, detail, cohort) {
20317
+ return { available: false, reason, detail, ...cohort };
20318
+ }
20319
+ /**
20320
+ * Narrow a statistic to its available branch.
20321
+ *
20322
+ * Exists so consumers in other packages can discriminate without restating the
20323
+ * predicate, and so the discriminant stays a single named concept if the shape
20324
+ * ever grows a third branch.
20325
+ *
20326
+ * @param statistic - The statistic to test.
20327
+ * @returns Whether the statistic carries a value.
20328
+ */
20329
+ function isAvailable(statistic) {
20330
+ return statistic.available;
20331
+ }
20332
+
20246
20333
  // risk-free-rate.ts
20247
20334
  /**
20248
20335
  * Conservative fallback annual risk-free rate used when no live rate has been
@@ -20601,57 +20688,60 @@ function alignReturns(tradeBars, benchmarkBars) {
20601
20688
  });
20602
20689
  return { alignedTradeReturns, alignedBenchmarkReturns, alignedDates };
20603
20690
  }
20604
- /*
20605
- * Calculate Beta from Returns
20606
- * @param portfolioReturns - Array of portfolio returns
20607
- * @param benchmarkReturns - Array of benchmark returns
20608
- * @returns Object containing beta, covariance, variance, and average returns
20691
+ /**
20692
+ * Beta of a portfolio against a benchmark, from paired period returns.
20693
+ *
20694
+ * Non-finite rows are dropped pairwise — a return that is `NaN` on either leg
20695
+ * cannot contribute to a covariance — and the count that survives is reported
20696
+ * as the cohort rather than discarded. That reporting is the point: silently
20697
+ * computing a beta on the 12 rows that happened to be clean, and returning it
20698
+ * with the same shape as a beta over all 900, is how a statistic measured on
20699
+ * one population gets applied to another.
20700
+ *
20701
+ * When beta cannot be computed the result is the unavailable branch, never a
20702
+ * numeric stand-in. A beta of `0` asserts that the portfolio does not move with
20703
+ * the market, which is a strong and consequential claim; emitting it to mean
20704
+ * "we could not tell" makes every alpha derived from it wrong by the whole
20705
+ * benchmark term.
20706
+ *
20707
+ * @param portfolioReturns - Portfolio period returns.
20708
+ * @param benchmarkReturns - Benchmark period returns, index-aligned to the portfolio.
20709
+ * @returns The beta components with their cohort, or a typed unavailable result.
20609
20710
  * @example
20610
- * const portfolioReturns = [0.05, -0.02, 0.03];
20611
- * const benchmarkReturns = [0.03, -0.01, 0.02];
20612
- * const beta = calculateBetaFromReturns(portfolioReturns, benchmarkReturns);
20613
- * // beta = { beta: 1.5, covariance: 0.0005, variance: 0.0003, averagePortfolioReturn: 0.02, averageBenchmarkReturn: 0.02 }
20614
- * @throws Will log warnings if input data is invalid or insufficient
20615
- * @throws Will log warnings if benchmark variance is effectively zero
20616
- * @throws Will log warnings if beta calculation results in a non-finite value
20617
- * @throws Will log warnings if there are not enough valid data points for calculation
20618
- * @throws Will log warnings if benchmark variance is zero or non-finite
20711
+ * const result = calculateBetaFromReturns([0.05, -0.02, 0.03], [0.03, -0.01, 0.02]);
20712
+ * if (result.available) {
20713
+ * // result.value.beta, alongside result.sampleCount and result.coverage
20714
+ * }
20619
20715
  */
20620
20716
  function calculateBetaFromReturns$1(portfolioReturns, benchmarkReturns) {
20621
- // Input validation
20622
- if (!Array.isArray(portfolioReturns) ||
20623
- !Array.isArray(benchmarkReturns) ||
20624
- portfolioReturns.length !== benchmarkReturns.length ||
20625
- portfolioReturns.length < 2) {
20626
- getLogger().warn("Invalid or insufficient return data for beta calculation");
20627
- return {
20628
- beta: 0,
20629
- covariance: 0,
20630
- variance: 0,
20631
- averagePortfolioReturn: 0,
20632
- averageBenchmarkReturn: 0,
20633
- };
20634
- }
20635
- // Filter out any non-finite values before calculations
20636
- const validIndices = [...Array(portfolioReturns.length).keys()].filter((i) => isFinite(portfolioReturns[i]) && isFinite(benchmarkReturns[i]));
20637
- if (validIndices.length < 2) {
20638
- getLogger().warn("Not enough valid data points for beta calculation");
20639
- return {
20640
- beta: 0,
20641
- covariance: 0,
20642
- variance: 0,
20643
- averagePortfolioReturn: 0,
20644
- averageBenchmarkReturn: 0,
20645
- };
20717
+ // A covariance is defined over PAIRS, so the offered cohort is the number of
20718
+ // index positions both series can supply. Ragged input is a caller defect
20719
+ // rather than a data condition, and it is reported as such instead of being
20720
+ // silently truncated to the shorter series.
20721
+ if (!Array.isArray(portfolioReturns) || !Array.isArray(benchmarkReturns)) {
20722
+ return unavailableStatistic("invalid_input", "portfolioReturns and benchmarkReturns must both be arrays", sampleCohort(0, 0));
20723
+ }
20724
+ const requestedCount = portfolioReturns.length;
20725
+ if (portfolioReturns.length !== benchmarkReturns.length) {
20726
+ return unavailableStatistic("invalid_input", `series lengths differ: portfolio ${portfolioReturns.length}, benchmark ${benchmarkReturns.length}`, sampleCohort(requestedCount, 0));
20727
+ }
20728
+ // Pairwise finiteness filter. Both legs must be usable for the pair to
20729
+ // contribute; keeping a pair on the strength of one leg would mix a real
20730
+ // observation with a fabricated one.
20731
+ const validIndices = [...Array(requestedCount).keys()].filter((i) => isFinite(portfolioReturns[i]) && isFinite(benchmarkReturns[i]));
20732
+ const cohort = sampleCohort(requestedCount, validIndices.length);
20733
+ // Bessel-corrected estimators need at least one degree of freedom, so two
20734
+ // usable pairs is the floor below which no sample variance exists.
20735
+ const MIN_PAIRS_FOR_SAMPLE_VARIANCE = 2;
20736
+ if (validIndices.length < MIN_PAIRS_FOR_SAMPLE_VARIANCE) {
20737
+ getLogger().warn(`Beta unavailable: ${validIndices.length} usable pairs of ${requestedCount} offered.`);
20738
+ return unavailableStatistic(validIndices.length === 0 ? "no_usable_samples" : "insufficient_samples", `beta needs at least ${MIN_PAIRS_FOR_SAMPLE_VARIANCE} finite pairs; ${validIndices.length} of ${requestedCount} were usable`, cohort);
20646
20739
  }
20647
- // Use validated indices only
20648
20740
  const validPortfolioReturns = validIndices.map((i) => portfolioReturns[i]);
20649
20741
  const validBenchmarkReturns = validIndices.map((i) => benchmarkReturns[i]);
20650
- // Calculate means
20651
20742
  const n = validIndices.length;
20652
20743
  const averagePortfolioReturn = validPortfolioReturns.reduce((sum, ret) => sum + ret, 0) / n;
20653
20744
  const averageBenchmarkReturn = validBenchmarkReturns.reduce((sum, ret) => sum + ret, 0) / n;
20654
- // Calculate covariance and variance with Welford's online algorithm for numerical stability
20655
20745
  let covariance = 0;
20656
20746
  let variance = 0;
20657
20747
  for (let i = 0; i < n; i++) {
@@ -20660,30 +20750,26 @@ function calculateBetaFromReturns$1(portfolioReturns, benchmarkReturns) {
20660
20750
  covariance += portfolioDiff * benchmarkDiff;
20661
20751
  variance += benchmarkDiff * benchmarkDiff;
20662
20752
  }
20663
- // Finalize calculations using sample (Bessel-corrected) estimators —
20664
- // divide by (n - 1), not n. The guard above (validIndices.length < 2)
20665
- // already ensures n >= 2, so (n - 1) is always safe.
20753
+ // Sample (Bessel-corrected) estimators — divide by (n - 1), not n. The guard
20754
+ // above ensures n >= 2, so (n - 1) is always safe.
20666
20755
  covariance /= n - 1;
20667
20756
  variance /= n - 1;
20668
- // Handle zero variance case
20669
- if (Math.abs(variance) < 1e-10) {
20670
- getLogger().warn("Benchmark variance is effectively zero. Setting beta to 0.");
20671
- return {
20672
- beta: 0,
20673
- covariance,
20674
- variance,
20675
- averagePortfolioReturn,
20676
- averageBenchmarkReturn,
20677
- };
20678
- }
20679
- const beta = covariance / variance;
20680
- return {
20681
- beta,
20757
+ // A benchmark that never moved has no variance to regress against, so beta is
20758
+ // undefined rather than zero. VARIANCE_NOISE_FLOOR absorbs the case where a
20759
+ // constant series still produces a tiny positive variance because the computed
20760
+ // mean differs from the constant by a rounding unit.
20761
+ const VARIANCE_NOISE_FLOOR = 1e-10;
20762
+ if (Math.abs(variance) < VARIANCE_NOISE_FLOOR) {
20763
+ getLogger().warn("Beta unavailable: benchmark variance is effectively zero.");
20764
+ return unavailableStatistic("degenerate_population", `benchmark variance ${variance} is below the noise floor ${VARIANCE_NOISE_FLOOR}; beta is undefined`, cohort);
20765
+ }
20766
+ return availableStatistic({
20767
+ beta: covariance / variance,
20682
20768
  covariance,
20683
20769
  variance,
20684
20770
  averagePortfolioReturn,
20685
20771
  averageBenchmarkReturn,
20686
- };
20772
+ }, cohort);
20687
20773
  }
20688
20774
  /**
20689
20775
  * Calculates the total return for a position, respecting position direction
@@ -20767,7 +20853,18 @@ async function calculateAlphaAndBeta$1(tradeBars, benchmarkBars, isShort) {
20767
20853
  : rawTradeReturns;
20768
20854
  // Calculate beta with position-adjusted returns
20769
20855
  const beta = calculateBetaFromReturns$1(alignedTradeReturns, alignedBenchmarkReturns);
20770
- if (!isFinite(beta.beta)) {
20856
+ // Alpha is the return left over after the benchmark term, so an unknown beta
20857
+ // makes alpha unknown too. Substituting any number here — zero most of all —
20858
+ // would credit the whole benchmark move to the strategy.
20859
+ if (!beta.available) {
20860
+ getLogger().warn(`Alpha unavailable: beta could not be computed (${beta.reason}: ${beta.detail}).`);
20861
+ return {
20862
+ alpha: "N/A",
20863
+ alphaAnnualized: "N/A",
20864
+ beta: "N/A",
20865
+ };
20866
+ }
20867
+ if (!isFinite(beta.value.beta)) {
20771
20868
  getLogger().warn("Beta calculation resulted in a non-finite value.");
20772
20869
  return {
20773
20870
  alpha: "N/A",
@@ -20778,7 +20875,7 @@ async function calculateAlphaAndBeta$1(tradeBars, benchmarkBars, isShort) {
20778
20875
  // For short positions, the interpretation of beta changes
20779
20876
  // A positive beta on a short means the position moves with the market,
20780
20877
  // which is bad for a short. We invert it for consistency.
20781
- const positionAwareBeta = isShort ? -beta.beta : beta.beta;
20878
+ const positionAwareBeta = isShort ? -beta.value.beta : beta.value.beta;
20782
20879
  const avgTradeReturn = alignedTradeReturns.reduce((sum, ret) => sum + ret, 0) /
20783
20880
  alignedTradeReturns.length;
20784
20881
  const avgBenchmarkReturn = alignedBenchmarkReturns.reduce((sum, ret) => sum + ret, 0) /
@@ -21397,7 +21494,18 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
21397
21494
  const benchmarkAvgReturn = alignedBenchmarkReturns.reduce((sum, ret) => sum + ret, 0) / n;
21398
21495
  // **Calculate beta**
21399
21496
  const beta = calculateBetaFromReturns(alignedPortfolioReturns, alignedBenchmarkReturns);
21400
- if (!isFinite(beta.beta)) {
21497
+ // Alpha is what remains after subtracting the benchmark term, so an unknown
21498
+ // beta leaves alpha unknown. Any numeric stand-in — zero above all — would
21499
+ // attribute the entire benchmark move to the strategy.
21500
+ if (!beta.available) {
21501
+ getLogger().warn(`Alpha unavailable: beta could not be computed (${beta.reason}: ${beta.detail}).`);
21502
+ return {
21503
+ alpha: "N/A",
21504
+ alphaAnnualized: "N/A",
21505
+ beta: "N/A",
21506
+ };
21507
+ }
21508
+ if (!isFinite(beta.value.beta)) {
21401
21509
  getLogger().warn("Beta calculation resulted in a non-finite value.");
21402
21510
  return {
21403
21511
  alpha: "N/A",
@@ -21412,7 +21520,7 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
21412
21520
  const tradingDaysPerYear = 252;
21413
21521
  const riskFreeRateDaily = riskFreeRateAnnual / tradingDaysPerYear;
21414
21522
  const alpha = portfolioAvgReturn -
21415
- (riskFreeRateDaily + beta.beta * (benchmarkAvgReturn - riskFreeRateDaily));
21523
+ (riskFreeRateDaily + beta.value.beta * (benchmarkAvgReturn - riskFreeRateDaily));
21416
21524
  const alphaAnnualized = alpha * tradingDaysPerYear;
21417
21525
  if (!isFinite(alphaAnnualized)) {
21418
21526
  getLogger().warn("Alpha calculation resulted in a non-finite value.");
@@ -21425,7 +21533,7 @@ async function calculateAlphaAndBeta(portfolioHistory, benchmarkBars) {
21425
21533
  return {
21426
21534
  alpha: `${(alpha * 100).toFixed(2)}`,
21427
21535
  alphaAnnualized: `${(alphaAnnualized * 100).toFixed(2)}`,
21428
- beta: `${(beta.beta * 100).toFixed(2)}`,
21536
+ beta: `${(beta.value.beta * 100).toFixed(2)}`,
21429
21537
  };
21430
21538
  }
21431
21539
  // **Helper function to calculate daily returns with Unix millisecond timestamps**
@@ -21648,27 +21756,40 @@ function alignReturnsByDate(portfolioHistory, benchmarkBars) {
21648
21756
  return { alignedPortfolioReturns, alignedBenchmarkReturns };
21649
21757
  }
21650
21758
  /**
21651
- * Calculates the beta of the portfolio compared to a benchmark.
21652
- * @param portfolioReturns - Array of portfolio returns.
21653
- * @param benchmarkReturns - Array of benchmark returns.
21654
- * @returns An object containing beta and intermediate calculations.
21759
+ * Beta of a portfolio against a benchmark, from paired period returns.
21760
+ *
21761
+ * The two series are index-aligned pairs by contract: every mean, covariance
21762
+ * and variance below is taken over the SAME row set. A length mismatch is
21763
+ * therefore reported as invalid input rather than absorbed, because dividing
21764
+ * one series' sum by the other series' length produces a mean of a population
21765
+ * that does not exist — a number with no cohort, which is the failure this
21766
+ * return type exists to make impossible.
21767
+ *
21768
+ * An uncomputable beta is returned as the unavailable branch, never as `0`.
21769
+ * Zero beta is a claim of no market exposure, and downstream alpha attributes
21770
+ * the entire benchmark move to the strategy when it believes that claim.
21771
+ *
21772
+ * @param portfolioReturns - Portfolio period returns.
21773
+ * @param benchmarkReturns - Benchmark period returns, index-aligned to the portfolio.
21774
+ * @returns The beta components with their cohort, or a typed unavailable result.
21655
21775
  */
21656
21776
  function calculateBetaFromReturns(portfolioReturns, benchmarkReturns) {
21657
- const n = portfolioReturns.length;
21658
- if (n === 0) {
21659
- getLogger().warn("No returns to calculate beta.");
21660
- return {
21661
- beta: 0,
21662
- covariance: 0,
21663
- variance: 0,
21664
- averagePortfolioReturn: 0,
21665
- averageBenchmarkReturn: 0,
21666
- };
21667
- }
21668
- // Calculate average returns
21777
+ const requestedCount = portfolioReturns.length;
21778
+ if (portfolioReturns.length !== benchmarkReturns.length) {
21779
+ getLogger().warn(`Beta unavailable: series lengths differ (portfolio ${portfolioReturns.length}, benchmark ${benchmarkReturns.length}).`);
21780
+ return unavailableStatistic("invalid_input", `series lengths differ: portfolio ${portfolioReturns.length}, benchmark ${benchmarkReturns.length}`, sampleCohort(requestedCount, 0));
21781
+ }
21782
+ // Bessel-corrected estimators need one degree of freedom, so two paired
21783
+ // observations is the floor below which no sample variance exists.
21784
+ const MIN_PAIRS_FOR_SAMPLE_VARIANCE = 2;
21785
+ const n = requestedCount;
21786
+ if (n < MIN_PAIRS_FOR_SAMPLE_VARIANCE) {
21787
+ getLogger().warn(`Beta unavailable: ${n} paired returns offered.`);
21788
+ return unavailableStatistic(n === 0 ? "no_usable_samples" : "insufficient_samples", `beta needs at least ${MIN_PAIRS_FOR_SAMPLE_VARIANCE} paired returns; ${n} were offered`, sampleCohort(requestedCount, n));
21789
+ }
21790
+ const cohort = sampleCohort(requestedCount, n);
21669
21791
  const averagePortfolioReturn = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / n;
21670
21792
  const averageBenchmarkReturn = benchmarkReturns.reduce((sum, ret) => sum + ret, 0) / n;
21671
- // Calculate covariance and variance
21672
21793
  let covariance = 0;
21673
21794
  let variance = 0;
21674
21795
  for (let i = 0; i < n; i++) {
@@ -21678,37 +21799,26 @@ function calculateBetaFromReturns(portfolioReturns, benchmarkReturns) {
21678
21799
  variance += benchmarkDiff ** 2;
21679
21800
  }
21680
21801
  // Use sample (Bessel-corrected) estimators — divide by (n - 1), not n.
21681
- // For n === 1 there is no degrees-of-freedom left; treat as zero variance
21682
- // so beta falls through to the zero-variance guard below.
21683
- const denom = n > 1 ? n - 1 : 1;
21684
- covariance /= denom;
21685
- variance /= denom;
21686
- // Handle zero (or numerically-degenerate) variance. A constant benchmark
21687
- // series can still produce a tiny nonzero variance because the computed
21688
- // mean differs from the constant by an ulp; dividing covariance by that
21689
- // rounding noise yields a meaningless beta. Treat any variance at or
21690
- // below the summation noise floor — (n * eps * |mean|)^2, the square of
21691
- // the worst-case naive-summation error — as zero. When the mean is
21692
- // exactly 0 this reduces to the exact zero check.
21802
+ covariance /= n - 1;
21803
+ variance /= n - 1;
21804
+ // A constant benchmark series can still produce a tiny nonzero variance
21805
+ // because the computed mean differs from the constant by an ulp; dividing
21806
+ // covariance by that rounding noise yields a meaningless beta. Treat any
21807
+ // variance at or below the summation noise floor — (n * eps * |mean|)^2, the
21808
+ // square of the worst-case naive-summation error — as no variance at all.
21809
+ // When the mean is exactly 0 this reduces to the exact zero check.
21693
21810
  const varianceNoiseFloor = (n * Number.EPSILON * Math.abs(averageBenchmarkReturn)) ** 2;
21694
21811
  if (variance <= varianceNoiseFloor) {
21695
- getLogger().warn("Benchmark variance is zero or below the floating-point noise floor. Setting beta to 0.");
21696
- return {
21697
- beta: 0,
21698
- covariance,
21699
- variance,
21700
- averagePortfolioReturn,
21701
- averageBenchmarkReturn,
21702
- };
21812
+ getLogger().warn("Beta unavailable: benchmark variance is zero or below the floating-point noise floor.");
21813
+ return unavailableStatistic("degenerate_population", `benchmark variance ${variance} is at or below the summation noise floor ${varianceNoiseFloor}; beta is undefined`, cohort);
21703
21814
  }
21704
- const beta = covariance / variance;
21705
- return {
21706
- beta,
21815
+ return availableStatistic({
21816
+ beta: covariance / variance,
21707
21817
  covariance,
21708
21818
  variance,
21709
21819
  averagePortfolioReturn,
21710
21820
  averageBenchmarkReturn,
21711
- };
21821
+ }, cohort);
21712
21822
  }
21713
21823
  /**
21714
21824
  * Calculates the information ratio of the portfolio compared to a benchmark.
@@ -22187,7 +22297,14 @@ var riskNs = /*#__PURE__*/Object.freeze({
22187
22297
  * Conventions:
22188
22298
  * - tradePnls / tradeReturns is an array of per-trade realised P&L or return
22189
22299
  * (positive = win, negative = loss, zero = breakeven).
22190
- * - All "rolling*" functions return null when fewer than `windowSize` trades exist.
22300
+ * - Every statistic here is a ratio or a mean over a WINDOW, so every one is
22301
+ * returned as a {@link SampleStatistic}: the value cannot be read without the
22302
+ * `sampleCount` it was taken over and the `coverage` of the window that was
22303
+ * asked for. A hit-rate is a different claim on 5 trades than on 500, and a
22304
+ * window that could only be half-filled is a different cohort from a full
22305
+ * one — a caller holding a bare number can tell neither apart.
22306
+ * - A window that cannot support the statistic returns the unavailable branch
22307
+ * with a reason, never a numeric stand-in. Zero is a measurement.
22191
22308
  * - All public functions reject non-finite inputs (NaN, Infinity) by throwing.
22192
22309
  * Callers must pre-validate or filter their inputs.
22193
22310
  */
@@ -22203,21 +22320,38 @@ function assertFiniteArray(name, arr) {
22203
22320
  }
22204
22321
  }
22205
22322
  }
22323
+ /**
22324
+ * Report a window that holds fewer trades than it asked for.
22325
+ *
22326
+ * Shared so every rolling function describes a short window the same way — the
22327
+ * cohort is `(requested = windowSize, sampled = what exists)`, which is the
22328
+ * pair a caller needs to distinguish a warm-up from a data gap.
22329
+ *
22330
+ * @param name - The calling function, for the detail string.
22331
+ * @param available - Trades actually present.
22332
+ * @param windowSize - Trades the window asked for.
22333
+ * @returns The unavailable branch describing the short window.
22334
+ */
22335
+ function insufficientWindow(name, available, windowSize) {
22336
+ return unavailableStatistic("insufficient_samples", `${name}: window of ${windowSize} requested, only ${available} trades available`, sampleCohort(windowSize, available));
22337
+ }
22206
22338
  /**
22207
22339
  * Rolling expectancy: mean P&L over the most-recent `windowSize` trades.
22208
22340
  *
22209
22341
  * @param tradePnls - Array of per-trade realised P&L values.
22210
22342
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22211
- * @returns Mean P&L of the last `windowSize` trades, or null when fewer than `windowSize` exist.
22343
+ * @returns Mean P&L of the last `windowSize` trades with its cohort, or a typed
22344
+ * unavailable result when fewer than `windowSize` trades exist.
22212
22345
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22213
22346
  */
22214
22347
  function calculateRollingExpectancy(tradePnls, windowSize) {
22215
22348
  assertWindowSize("calculateRollingExpectancy", windowSize);
22216
- if (tradePnls.length < windowSize)
22217
- return null;
22349
+ if (tradePnls.length < windowSize) {
22350
+ return insufficientWindow("calculateRollingExpectancy", tradePnls.length, windowSize);
22351
+ }
22218
22352
  assertFiniteArray("calculateRollingExpectancy", tradePnls);
22219
22353
  const slice = tradePnls.slice(-windowSize);
22220
- return slice.reduce((a, b) => a + b, 0) / windowSize;
22354
+ return availableStatistic(slice.reduce((a, b) => a + b, 0) / windowSize, sampleCohort(windowSize, windowSize));
22221
22355
  }
22222
22356
  /**
22223
22357
  * Rolling hit-rate: fraction of strictly-positive P&L trades in the most-recent
@@ -22225,42 +22359,53 @@ function calculateRollingExpectancy(tradePnls, windowSize) {
22225
22359
  *
22226
22360
  * @param tradePnls - Array of per-trade realised P&L values.
22227
22361
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22228
- * @returns Fraction of winning trades in the window, or null when fewer than `windowSize` exist.
22362
+ * @returns Fraction of winning trades in the window with its cohort, or a typed
22363
+ * unavailable result when fewer than `windowSize` trades exist.
22229
22364
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22230
22365
  */
22231
22366
  function calculateRollingHitRate(tradePnls, windowSize) {
22232
22367
  assertWindowSize("calculateRollingHitRate", windowSize);
22233
- if (tradePnls.length < windowSize)
22234
- return null;
22368
+ if (tradePnls.length < windowSize) {
22369
+ return insufficientWindow("calculateRollingHitRate", tradePnls.length, windowSize);
22370
+ }
22235
22371
  assertFiniteArray("calculateRollingHitRate", tradePnls);
22236
22372
  const slice = tradePnls.slice(-windowSize);
22237
22373
  const wins = slice.filter((p) => p > 0).length;
22238
- return wins / windowSize;
22374
+ return availableStatistic(wins / windowSize, sampleCohort(windowSize, windowSize));
22239
22375
  }
22240
22376
  /**
22241
22377
  * Rolling profit factor: sum(wins) / |sum(losses)| over the most-recent `windowSize` trades.
22242
22378
  *
22243
22379
  * Edge cases:
22244
- * - no losses and at least one win → +Infinity
22245
- * - no wins and no losses (all zeros) → 0
22246
- * - fewer than windowSize trades → null
22380
+ * - no losses and at least one win → +Infinity (an unbounded but real ratio)
22381
+ * - no wins and no losses (all zeros) → unavailable: `0 / 0` is undefined, and a
22382
+ * window of breakeven trades has no profit factor rather than a profit factor
22383
+ * of zero
22384
+ * - fewer than windowSize trades → unavailable
22247
22385
  *
22248
22386
  * @param tradePnls - Array of per-trade realised P&L values.
22249
22387
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22250
- * @returns Profit factor for the rolling window, or null when fewer than `windowSize` exist.
22388
+ * @returns Profit factor for the rolling window with its cohort, or a typed
22389
+ * unavailable result.
22251
22390
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22252
22391
  */
22253
22392
  function calculateRollingProfitFactor(tradePnls, windowSize) {
22254
22393
  assertWindowSize("calculateRollingProfitFactor", windowSize);
22255
- if (tradePnls.length < windowSize)
22256
- return null;
22394
+ if (tradePnls.length < windowSize) {
22395
+ return insufficientWindow("calculateRollingProfitFactor", tradePnls.length, windowSize);
22396
+ }
22257
22397
  assertFiniteArray("calculateRollingProfitFactor", tradePnls);
22398
+ const cohort = sampleCohort(windowSize, windowSize);
22258
22399
  const slice = tradePnls.slice(-windowSize);
22259
22400
  const wins = slice.filter((p) => p > 0).reduce((a, b) => a + b, 0);
22260
22401
  const losses = slice.filter((p) => p < 0).reduce((a, b) => a + Math.abs(b), 0);
22261
- if (losses === 0)
22262
- return wins > 0 ? Number.POSITIVE_INFINITY : 0;
22263
- return wins / losses;
22402
+ if (losses === 0) {
22403
+ if (wins > 0) {
22404
+ return availableStatistic(Number.POSITIVE_INFINITY, cohort);
22405
+ }
22406
+ return unavailableStatistic("degenerate_population", `calculateRollingProfitFactor: window of ${windowSize} contains neither wins nor losses; the ratio is undefined`, cohort);
22407
+ }
22408
+ return availableStatistic(wins / losses, cohort);
22264
22409
  }
22265
22410
  /**
22266
22411
  * Rolling Sortino: delegate to `calculateSortino` over the most-recent `windowSize` returns.
@@ -22268,34 +22413,58 @@ function calculateRollingProfitFactor(tradePnls, windowSize) {
22268
22413
  * @param tradeReturns - Array of per-trade return values.
22269
22414
  * @param windowSize - Number of most-recent trades to include. Must be a positive integer.
22270
22415
  * @param riskFreeRate - Risk-free rate to subtract from returns (default 0).
22271
- * @returns Sortino ratio for the rolling window, or null when fewer than `windowSize` exist.
22416
+ * @returns Sortino ratio for the rolling window with its cohort, or a typed
22417
+ * unavailable result.
22272
22418
  * @throws When `windowSize` is not a positive integer or any input is non-finite.
22273
22419
  */
22274
22420
  function calculateRollingSortino(tradeReturns, windowSize, riskFreeRate = 0) {
22275
22421
  assertWindowSize("calculateRollingSortino", windowSize);
22276
- if (tradeReturns.length < windowSize)
22277
- return null;
22422
+ if (tradeReturns.length < windowSize) {
22423
+ return insufficientWindow("calculateRollingSortino", tradeReturns.length, windowSize);
22424
+ }
22278
22425
  assertFiniteArray("calculateRollingSortino", tradeReturns);
22279
- return calculateSortino(tradeReturns.slice(-windowSize), riskFreeRate);
22426
+ const cohort = sampleCohort(windowSize, windowSize);
22427
+ const sortino = calculateSortino(tradeReturns.slice(-windowSize), riskFreeRate);
22428
+ if (sortino === null) {
22429
+ // `calculateSortino` returns null only for a window it cannot form a
22430
+ // dispersion over — fewer than two samples. That is a property of the
22431
+ // window, so it is reported as one rather than as a ratio of zero.
22432
+ return unavailableStatistic("insufficient_samples", `calculateRollingSortino: window of ${windowSize} cannot support a dispersion estimate`, cohort);
22433
+ }
22434
+ return availableStatistic(sortino, cohort);
22280
22435
  }
22281
22436
  /**
22282
22437
  * Z-score of live-expectancy vs backtest-expectancy, scaled by the backtest stddev.
22283
22438
  * Positive Z = live outperforming; negative Z = live underperforming.
22284
22439
  *
22285
- * @param liveExpectancy - Mean P&L per trade in the live window.
22440
+ * The live expectancy is taken as a {@link SampleStatistic} rather than a bare
22441
+ * number so the z-score inherits the cohort it was actually derived from. A
22442
+ * z-score is a statement about how surprising a sample mean is, and how
22443
+ * surprising it is depends entirely on how many trades produced it — quoting
22444
+ * the z alone is the exact substitution this type exists to block. An
22445
+ * unavailable live expectancy yields an unavailable z, because there is no
22446
+ * mean to compare.
22447
+ *
22448
+ * @param liveExpectancy - Mean P&L per trade in the live window, with its cohort.
22286
22449
  * @param backtestExpectancy - Mean P&L per trade from the calibration backtest.
22287
22450
  * @param backtestStddev - Stddev of per-trade P&L in the backtest. Must be > 0.
22288
- * @returns Z-score measuring divergence between live and backtest performance.
22289
- * @throws When any input is non-finite or `backtestStddev` is not positive.
22451
+ * @returns Z-score measuring live-vs-backtest divergence, carrying the live cohort.
22452
+ * @throws When the backtest inputs are non-finite or `backtestStddev` is not positive.
22290
22453
  */
22291
22454
  function calculateBacktestDivergenceZ(liveExpectancy, backtestExpectancy, backtestStddev) {
22292
- if (!Number.isFinite(liveExpectancy) || !Number.isFinite(backtestExpectancy) || !Number.isFinite(backtestStddev)) {
22455
+ if (!Number.isFinite(backtestExpectancy) || !Number.isFinite(backtestStddev)) {
22293
22456
  throw new Error("calculateBacktestDivergenceZ: inputs must be finite numbers");
22294
22457
  }
22295
22458
  if (backtestStddev <= 0) {
22296
22459
  throw new Error("calculateBacktestDivergenceZ: stddev must be > 0");
22297
22460
  }
22298
- return (liveExpectancy - backtestExpectancy) / backtestStddev;
22461
+ if (!liveExpectancy.available) {
22462
+ return unavailableStatistic(liveExpectancy.reason, `calculateBacktestDivergenceZ: live expectancy unavailable (${liveExpectancy.detail})`, sampleCohort(liveExpectancy.requestedCount, liveExpectancy.sampleCount));
22463
+ }
22464
+ if (!Number.isFinite(liveExpectancy.value)) {
22465
+ throw new Error("calculateBacktestDivergenceZ: inputs must be finite numbers");
22466
+ }
22467
+ return availableStatistic((liveExpectancy.value - backtestExpectancy) / backtestStddev, sampleCohort(liveExpectancy.requestedCount, liveExpectancy.sampleCount));
22299
22468
  }
22300
22469
 
22301
22470
  var strategyNs = /*#__PURE__*/Object.freeze({
@@ -80210,6 +80379,7 @@ exports.alpaca = alpaca;
80210
80379
  exports.analyzeBars = analyzeBars;
80211
80380
  exports.approximateImpliedVolatility = approximateImpliedVolatility;
80212
80381
  exports.atr = atrNs;
80382
+ exports.availableStatistic = availableStatistic;
80213
80383
  exports.bracketOrders = bracketOrders;
80214
80384
  exports.buildOCCSymbol = buildOCCSymbol;
80215
80385
  exports.buildOptionSymbol = buildOptionSymbol;
@@ -80383,6 +80553,7 @@ exports.hasSufficientVolume = hasSufficientVolume;
80383
80553
  exports.httpAgent = httpAgent;
80384
80554
  exports.httpsAgent = httpsAgent;
80385
80555
  exports.isAlpacaBrokerCredentials = isAlpacaBrokerCredentials;
80556
+ exports.isAvailable = isAvailable;
80386
80557
  exports.isContractTradable = isContractTradable;
80387
80558
  exports.isCryptoPair = isCryptoPair;
80388
80559
  exports.isExpiringWithin = isExpiringWithin;
@@ -80429,6 +80600,7 @@ exports.routeKeyFor = routeKeyFor;
80429
80600
  exports.routeSupports = routeSupports;
80430
80601
  exports.routeTable = routeTable;
80431
80602
  exports.safeValidateResponse = safeValidateResponse;
80603
+ exports.sampleCohort = sampleCohort;
80432
80604
  exports.searchNews = searchNews;
80433
80605
  exports.sellAllCrypto = sellAllCrypto;
80434
80606
  exports.sellCryptoNotional = sellCryptoNotional;
@@ -80442,6 +80614,7 @@ exports.strategy = strategyNs;
80442
80614
  exports.sumUsage = sumUsage;
80443
80615
  exports.tradingPolicy = index;
80444
80616
  exports.trailingStops = trailingStops;
80617
+ exports.unavailableStatistic = unavailableStatistic;
80445
80618
  exports.updateAccountConfiguration = updateAccountConfiguration;
80446
80619
  exports.updateTrailingStop = updateTrailingStop;
80447
80620
  exports.validateAlpacaCredentials = validateAlpacaCredentials;