@livefolio/sdk 0.3.6 → 0.3.7

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/dist/index.d.ts CHANGED
@@ -372,6 +372,85 @@ declare class PortfolioHandle {
372
372
  trades(target: AllocationHandle, prices: [TickerHandle, number][], date: string): Trade[];
373
373
  }
374
374
 
375
+ interface MetricsOptions {
376
+ riskFreeRate?: number;
377
+ topDrawdowns?: number;
378
+ varConfidence?: number;
379
+ }
380
+ interface DrawdownEntry {
381
+ peakDate: string;
382
+ troughDate: string;
383
+ recoveryDate: string | null;
384
+ depth: number;
385
+ durationDays: number;
386
+ underwaterDays: number;
387
+ }
388
+ interface MonthlyReturnsTable {
389
+ rows: Array<{
390
+ year: number;
391
+ months: (number | null)[];
392
+ ytd: number | null;
393
+ }>;
394
+ }
395
+ interface MetricsResult {
396
+ range: {
397
+ from: string;
398
+ to: string;
399
+ years: number;
400
+ };
401
+ returns: {
402
+ totalReturn: number;
403
+ cagr: number;
404
+ bestYear: {
405
+ year: number;
406
+ return: number;
407
+ } | null;
408
+ worstYear: {
409
+ year: number;
410
+ return: number;
411
+ } | null;
412
+ bestMonth: {
413
+ date: string;
414
+ return: number;
415
+ } | null;
416
+ worstMonth: {
417
+ date: string;
418
+ return: number;
419
+ } | null;
420
+ pctPositiveMonths: number;
421
+ };
422
+ risk: {
423
+ volatility: number;
424
+ downsideDeviation: number;
425
+ maxDrawdown: DrawdownEntry;
426
+ currentDrawdown: number;
427
+ ulcerIndex: number;
428
+ skew: number;
429
+ kurtosis: number;
430
+ var95: number;
431
+ cvar95: number;
432
+ };
433
+ riskAdjusted: {
434
+ sharpe: number;
435
+ sortino: number;
436
+ calmar: number;
437
+ };
438
+ activity: {
439
+ rebalances: number;
440
+ trades: number;
441
+ turnover: number;
442
+ winRate: number;
443
+ };
444
+ tables: {
445
+ drawdowns: DrawdownEntry[];
446
+ monthly: MonthlyReturnsTable;
447
+ yearly: Array<{
448
+ year: number;
449
+ return: number;
450
+ }>;
451
+ };
452
+ }
453
+
375
454
  interface SimulateOptions {
376
455
  from: string;
377
456
  to: string;
@@ -466,6 +545,7 @@ declare class SimulationHandle {
466
545
  * the current UTC ISO date; callers with non-UTC semantics or after-hours
467
546
  * rollover should supply their own.
468
547
  */
548
+ metrics(options?: MetricsOptions): MetricsResult;
469
549
  pushAndPreview(quotes: Record<string, number>, options?: {
470
550
  date?: string;
471
551
  }): Promise<LivePreviewState>;
@@ -628,4 +708,25 @@ declare function allocationsEqual(a: AllocationHandle | null, b: AllocationHandl
628
708
 
629
709
  declare function computeRebalanceDates(tradingDays: string[], freq: TradingFreq, offset: number): Set<string>;
630
710
 
631
- export { AllocationHandle, type Comparison, type DailyBar, type DateRange, IndicatorHandle, type IndicatorIdentity, type IndicatorType, type LiveEvaluator, type LivePreviewState, type LiveRuleState, type LiveSignalState, type LivefolioClient, type LivefolioClientOptions, type MarketProvider, PortfolioHandle, type PortfolioSnapshot, type PriceStream, SignalHandle, type SignalIdentity, type SimulateOptions, SimulationHandle, type StorageProvider, type StrategyBar, type StrategyDefinition, StrategyHandle, type StrategyLiveState, type StrategyOptions, type StrategyReferenceData, type StrategyRule, type StrategyRuleDefinition, type StrategySeriesEntry, type StreamStatus, TickerHandle, type Trade, type TradingFreq, type Unit, allocationsEqual, computeRebalanceDates, createClient };
711
+ declare function computeMetrics(series: DailyBar[], trades: Trade[], options?: MetricsOptions): MetricsResult;
712
+
713
+ declare function sharpe(returns: number[], rfAnnual: number): number;
714
+ declare function sortino(returns: number[], rfAnnual: number): number;
715
+
716
+ declare function computeDrawdownTable(series: DailyBar[], topN: number): DrawdownEntry[];
717
+
718
+ interface MonthlyReturn {
719
+ year: number;
720
+ month: number;
721
+ return: number;
722
+ partial: boolean;
723
+ }
724
+ interface YearlyReturn {
725
+ year: number;
726
+ return: number;
727
+ partial: boolean;
728
+ }
729
+ declare function monthlyReturns(series: DailyBar[]): MonthlyReturn[];
730
+ declare function yearlyReturns(series: DailyBar[]): YearlyReturn[];
731
+
732
+ export { AllocationHandle, type Comparison, type DailyBar, type DateRange, type DrawdownEntry, IndicatorHandle, type IndicatorIdentity, type IndicatorType, type LiveEvaluator, type LivePreviewState, type LiveRuleState, type LiveSignalState, type LivefolioClient, type LivefolioClientOptions, type MarketProvider, type MetricsOptions, type MetricsResult, type MonthlyReturn, type MonthlyReturnsTable, PortfolioHandle, type PortfolioSnapshot, type PriceStream, SignalHandle, type SignalIdentity, type SimulateOptions, SimulationHandle, type StorageProvider, type StrategyBar, type StrategyDefinition, StrategyHandle, type StrategyLiveState, type StrategyOptions, type StrategyReferenceData, type StrategyRule, type StrategyRuleDefinition, type StrategySeriesEntry, type StreamStatus, TickerHandle, type Trade, type TradingFreq, type Unit, type YearlyReturn, allocationsEqual, computeDrawdownTable, computeMetrics, monthlyReturns as computeMonthlyReturns, computeRebalanceDates, sharpe as computeSharpe, sortino as computeSortino, yearlyReturns as computeYearlyReturns, createClient };
package/dist/index.js CHANGED
@@ -222,20 +222,20 @@ function returnNext(prev, newRaw, lookback, mode = "pct") {
222
222
  // src/computations/volatility.ts
223
223
  function computeVolatility(bars, lookback) {
224
224
  if (bars.length < lookback + 1) return [];
225
- const dailyReturns = [];
225
+ const dailyReturns2 = [];
226
226
  for (let i = 1; i < bars.length; i++) {
227
- dailyReturns.push({
227
+ dailyReturns2.push({
228
228
  date: bars[i].date,
229
229
  value: bars[i].value / bars[i - 1].value - 1
230
230
  });
231
231
  }
232
- if (dailyReturns.length < lookback) return [];
232
+ if (dailyReturns2.length < lookback) return [];
233
233
  const result = [];
234
- for (let i = lookback - 1; i < dailyReturns.length; i++) {
235
- const window = dailyReturns.slice(i - lookback + 1, i + 1);
236
- const mean = window.reduce((s, r) => s + r.value, 0) / lookback;
237
- const variance = window.reduce((s, r) => s + (r.value - mean) ** 2, 0) / lookback;
238
- result.push({ date: dailyReturns[i].date, value: Math.sqrt(variance) });
234
+ for (let i = lookback - 1; i < dailyReturns2.length; i++) {
235
+ const window = dailyReturns2.slice(i - lookback + 1, i + 1);
236
+ const mean2 = window.reduce((s, r) => s + r.value, 0) / lookback;
237
+ const variance = window.reduce((s, r) => s + (r.value - mean2) ** 2, 0) / lookback;
238
+ result.push({ date: dailyReturns2[i].date, value: Math.sqrt(variance) });
239
239
  }
240
240
  return result;
241
241
  }
@@ -247,8 +247,8 @@ function volatilityNext(prev, newRaw, lookback) {
247
247
  const tail = [...prev.tail.slice(1), newRaw];
248
248
  const returns = [];
249
249
  for (let i = 1; i < tail.length; i++) returns.push(tail[i] / tail[i - 1] - 1);
250
- const mean = returns.reduce((s, r) => s + r, 0) / lookback;
251
- const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / lookback;
250
+ const mean2 = returns.reduce((s, r) => s + r, 0) / lookback;
251
+ const variance = returns.reduce((s, r) => s + (r - mean2) ** 2, 0) / lookback;
252
252
  return { value: Math.sqrt(variance), state: { tail } };
253
253
  }
254
254
 
@@ -1527,6 +1527,506 @@ function runSimulation(bars, prices, rebalanceDates, portfolio) {
1527
1527
  return { series, trades, finalPortfolio };
1528
1528
  }
1529
1529
 
1530
+ // src/metrics/returns.ts
1531
+ function dailyReturns(series) {
1532
+ const out = [];
1533
+ for (let i = 1; i < series.length; i++) {
1534
+ const prev = series[i - 1].value;
1535
+ const curr = series[i].value;
1536
+ out.push(curr / prev - 1);
1537
+ }
1538
+ return out;
1539
+ }
1540
+ function ymd(date) {
1541
+ return {
1542
+ y: Number(date.slice(0, 4)),
1543
+ m: Number(date.slice(5, 7)) - 1,
1544
+ d: Number(date.slice(8, 10))
1545
+ };
1546
+ }
1547
+ function lastDayOfMonth(year, month) {
1548
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
1549
+ }
1550
+ function monthlyReturns(series) {
1551
+ if (series.length < 2) return [];
1552
+ const buckets = [];
1553
+ for (const bar of series) {
1554
+ const { y, m } = ymd(bar.date);
1555
+ const last = buckets[buckets.length - 1];
1556
+ if (!last || last.year !== y || last.month !== m) {
1557
+ buckets.push({
1558
+ year: y,
1559
+ month: m,
1560
+ firstDate: bar.date,
1561
+ firstValue: bar.value,
1562
+ lastDate: bar.date,
1563
+ lastValue: bar.value
1564
+ });
1565
+ } else {
1566
+ last.lastDate = bar.date;
1567
+ last.lastValue = bar.value;
1568
+ }
1569
+ }
1570
+ const out = [];
1571
+ for (let i = 0; i < buckets.length; i++) {
1572
+ const b = buckets[i];
1573
+ const prevLast = i === 0 ? b.firstValue : buckets[i - 1].lastValue;
1574
+ const ret = b.lastValue / prevLast - 1;
1575
+ const startsAtMonthStart = ymd(b.firstDate).d === 1;
1576
+ const endsAtMonthEnd = ymd(b.lastDate).d === lastDayOfMonth(b.year, b.month);
1577
+ const isFirst = i === 0;
1578
+ const isLast = i === buckets.length - 1;
1579
+ const partial = isFirst && !startsAtMonthStart || isLast && !endsAtMonthEnd;
1580
+ out.push({ year: b.year, month: b.month, return: ret, partial });
1581
+ }
1582
+ return out;
1583
+ }
1584
+ function yearlyReturns(series) {
1585
+ if (series.length < 2) return [];
1586
+ const buckets = [];
1587
+ for (const bar of series) {
1588
+ const { y } = ymd(bar.date);
1589
+ const last = buckets[buckets.length - 1];
1590
+ if (!last || last.year !== y) {
1591
+ buckets.push({
1592
+ year: y,
1593
+ firstDate: bar.date,
1594
+ firstValue: bar.value,
1595
+ lastDate: bar.date,
1596
+ lastValue: bar.value
1597
+ });
1598
+ } else {
1599
+ last.lastDate = bar.date;
1600
+ last.lastValue = bar.value;
1601
+ }
1602
+ }
1603
+ const out = [];
1604
+ for (let i = 0; i < buckets.length; i++) {
1605
+ const b = buckets[i];
1606
+ const prevLast = i === 0 ? b.firstValue : buckets[i - 1].lastValue;
1607
+ const ret = b.lastValue / prevLast - 1;
1608
+ const isFirst = i === 0;
1609
+ const isLast = i === buckets.length - 1;
1610
+ const startsAtYearStart = b.firstDate.endsWith("-01-01");
1611
+ const endsAtYearEnd = b.lastDate.endsWith("-12-31");
1612
+ const partial = isFirst && !startsAtYearStart || isLast && !endsAtYearEnd;
1613
+ out.push({ year: b.year, return: ret, partial });
1614
+ }
1615
+ return out;
1616
+ }
1617
+
1618
+ // src/metrics/summary.ts
1619
+ var DAY_MS = 24 * 60 * 60 * 1e3;
1620
+ function dateUTC(iso) {
1621
+ return Date.UTC(Number(iso.slice(0, 4)), Number(iso.slice(5, 7)) - 1, Number(iso.slice(8, 10)));
1622
+ }
1623
+ function totalReturn(series) {
1624
+ return series[series.length - 1].value / series[0].value - 1;
1625
+ }
1626
+ function years(series) {
1627
+ const first = dateUTC(series[0].date);
1628
+ const last = dateUTC(series[series.length - 1].date);
1629
+ return (last - first) / DAY_MS / 365.25;
1630
+ }
1631
+ function cagr(series) {
1632
+ const y = years(series);
1633
+ if (y <= 0) return 0;
1634
+ const ratio = series[series.length - 1].value / series[0].value;
1635
+ return Math.pow(ratio, 1 / y) - 1;
1636
+ }
1637
+ function bestYear(yr) {
1638
+ let best = null;
1639
+ for (const y of yr) {
1640
+ if (y.partial) continue;
1641
+ if (!best || y.return > best.return) best = y;
1642
+ }
1643
+ return best ? { year: best.year, return: best.return } : null;
1644
+ }
1645
+ function worstYear(yr) {
1646
+ let worst = null;
1647
+ for (const y of yr) {
1648
+ if (y.partial) continue;
1649
+ if (!worst || y.return < worst.return) worst = y;
1650
+ }
1651
+ return worst ? { year: worst.year, return: worst.return } : null;
1652
+ }
1653
+ function monthKey(m) {
1654
+ return `${m.year}-${String(m.month + 1).padStart(2, "0")}`;
1655
+ }
1656
+ function bestMonth(mr) {
1657
+ let best = null;
1658
+ for (const m of mr) {
1659
+ if (m.partial) continue;
1660
+ if (!best || m.return > best.return) best = m;
1661
+ }
1662
+ return best ? { date: monthKey(best), return: best.return } : null;
1663
+ }
1664
+ function worstMonth(mr) {
1665
+ let worst = null;
1666
+ for (const m of mr) {
1667
+ if (m.partial) continue;
1668
+ if (!worst || m.return < worst.return) worst = m;
1669
+ }
1670
+ return worst ? { date: monthKey(worst), return: worst.return } : null;
1671
+ }
1672
+ function pctPositiveMonths(mr) {
1673
+ let total = 0;
1674
+ let pos = 0;
1675
+ for (const m of mr) {
1676
+ if (m.partial) continue;
1677
+ total++;
1678
+ if (m.return > 0) pos++;
1679
+ }
1680
+ return total === 0 ? 0 : pos / total;
1681
+ }
1682
+
1683
+ // src/metrics/risk.ts
1684
+ var TRADING_DAYS = 252;
1685
+ function mean(xs) {
1686
+ if (xs.length === 0) return 0;
1687
+ let s = 0;
1688
+ for (const x of xs) s += x;
1689
+ return s / xs.length;
1690
+ }
1691
+ function stdev(xs) {
1692
+ if (xs.length < 2) return 0;
1693
+ const m = mean(xs);
1694
+ let s = 0;
1695
+ for (const x of xs) {
1696
+ const d = x - m;
1697
+ s += d * d;
1698
+ }
1699
+ return Math.sqrt(s / (xs.length - 1));
1700
+ }
1701
+ function volatility(returns) {
1702
+ return stdev(returns) * Math.sqrt(TRADING_DAYS);
1703
+ }
1704
+ function downsideDeviation(returns, marDaily) {
1705
+ if (returns.length === 0) return 0;
1706
+ let s = 0;
1707
+ for (const r of returns) {
1708
+ const d = Math.min(0, r - marDaily);
1709
+ s += d * d;
1710
+ }
1711
+ return Math.sqrt(s / returns.length) * Math.sqrt(TRADING_DAYS);
1712
+ }
1713
+ function skewness(xs) {
1714
+ const n = xs.length;
1715
+ if (n < 3) return 0;
1716
+ const m = mean(xs);
1717
+ const s = stdev(xs);
1718
+ if (s === 0) return 0;
1719
+ let sum = 0;
1720
+ for (const x of xs) {
1721
+ const z = (x - m) / s;
1722
+ sum += z * z * z;
1723
+ }
1724
+ return n / ((n - 1) * (n - 2)) * sum;
1725
+ }
1726
+ function excessKurtosis(xs) {
1727
+ const n = xs.length;
1728
+ if (n < 4) return 0;
1729
+ const m = mean(xs);
1730
+ const s = stdev(xs);
1731
+ if (s === 0) return 0;
1732
+ let sum = 0;
1733
+ for (const x of xs) {
1734
+ const z = (x - m) / s;
1735
+ sum += z * z * z * z;
1736
+ }
1737
+ const term1 = n * (n + 1) / ((n - 1) * (n - 2) * (n - 3));
1738
+ const term2 = 3 * (n - 1) * (n - 1) / ((n - 2) * (n - 3));
1739
+ return term1 * sum - term2;
1740
+ }
1741
+ function quantile(sortedAsc, p) {
1742
+ if (sortedAsc.length === 0) return NaN;
1743
+ if (sortedAsc.length === 1) return sortedAsc[0];
1744
+ const idx = p * (sortedAsc.length - 1);
1745
+ const lo = Math.floor(idx);
1746
+ const hi = Math.ceil(idx);
1747
+ if (lo === hi) return sortedAsc[lo];
1748
+ const frac = idx - lo;
1749
+ return sortedAsc[lo] * (1 - frac) + sortedAsc[hi] * frac;
1750
+ }
1751
+ function historicalVar(returns, confidence) {
1752
+ if (returns.length === 0) return 0;
1753
+ const sorted = [...returns].sort((a, b) => a - b);
1754
+ const q = quantile(sorted, 1 - confidence);
1755
+ return Math.max(0, -q);
1756
+ }
1757
+ function historicalCvar(returns, confidence) {
1758
+ if (returns.length === 0) return 0;
1759
+ const sorted = [...returns].sort((a, b) => a - b);
1760
+ const q = quantile(sorted, 1 - confidence);
1761
+ const tail = sorted.filter((r) => r <= q);
1762
+ if (tail.length === 0) return 0;
1763
+ return Math.max(0, -mean(tail));
1764
+ }
1765
+ function ulcerIndex(series) {
1766
+ if (series.length === 0) return 0;
1767
+ let runningMax = -Infinity;
1768
+ let sumSq = 0;
1769
+ for (const bar of series) {
1770
+ if (bar.value > runningMax) runningMax = bar.value;
1771
+ const ddPct = (bar.value - runningMax) / runningMax * 100;
1772
+ sumSq += ddPct * ddPct;
1773
+ }
1774
+ return Math.sqrt(sumSq / series.length);
1775
+ }
1776
+
1777
+ // src/metrics/drawdown.ts
1778
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
1779
+ var NOISE = 1e-4;
1780
+ function dateUTC2(iso) {
1781
+ return Date.UTC(Number(iso.slice(0, 4)), Number(iso.slice(5, 7)) - 1, Number(iso.slice(8, 10)));
1782
+ }
1783
+ function daysBetween2(a, b) {
1784
+ return Math.round((dateUTC2(b) - dateUTC2(a)) / DAY_MS2);
1785
+ }
1786
+ function computeDrawdownTable(series, topN) {
1787
+ if (series.length === 0) return [];
1788
+ const segments = [];
1789
+ let peakDate = series[0].date;
1790
+ let peakValue = series[0].value;
1791
+ let open = null;
1792
+ for (let i = 0; i < series.length; i++) {
1793
+ const bar = series[i];
1794
+ if (bar.value >= peakValue) {
1795
+ if (open) {
1796
+ const recoveryDate = bar.date;
1797
+ const depth = open.troughValue / open.peakValue - 1;
1798
+ if (Math.abs(depth) >= NOISE) {
1799
+ segments.push({
1800
+ peakDate: open.peakDate,
1801
+ troughDate: open.troughDate,
1802
+ recoveryDate,
1803
+ depth,
1804
+ durationDays: daysBetween2(open.peakDate, recoveryDate),
1805
+ underwaterDays: daysBetween2(open.peakDate, open.troughDate)
1806
+ });
1807
+ }
1808
+ open = null;
1809
+ }
1810
+ peakDate = bar.date;
1811
+ peakValue = bar.value;
1812
+ } else {
1813
+ if (!open) {
1814
+ open = { peakDate, peakValue, troughDate: bar.date, troughValue: bar.value };
1815
+ } else if (bar.value < open.troughValue) {
1816
+ open.troughDate = bar.date;
1817
+ open.troughValue = bar.value;
1818
+ }
1819
+ }
1820
+ }
1821
+ if (open) {
1822
+ const lastDate = series[series.length - 1].date;
1823
+ const depth = open.troughValue / open.peakValue - 1;
1824
+ if (Math.abs(depth) >= NOISE) {
1825
+ segments.push({
1826
+ peakDate: open.peakDate,
1827
+ troughDate: open.troughDate,
1828
+ recoveryDate: null,
1829
+ depth,
1830
+ durationDays: daysBetween2(open.peakDate, lastDate),
1831
+ underwaterDays: daysBetween2(open.peakDate, open.troughDate)
1832
+ });
1833
+ }
1834
+ }
1835
+ segments.sort((a, b) => Math.abs(b.depth) - Math.abs(a.depth));
1836
+ return segments.slice(0, topN);
1837
+ }
1838
+ function currentDrawdown(series) {
1839
+ if (series.length === 0) return 0;
1840
+ let runningMax = -Infinity;
1841
+ for (const bar of series) {
1842
+ if (bar.value > runningMax) runningMax = bar.value;
1843
+ }
1844
+ const last = series[series.length - 1].value;
1845
+ return last / runningMax - 1;
1846
+ }
1847
+
1848
+ // src/metrics/riskAdjusted.ts
1849
+ var TRADING_DAYS2 = 252;
1850
+ function dailyRiskFree(annual) {
1851
+ return Math.pow(1 + annual, 1 / TRADING_DAYS2) - 1;
1852
+ }
1853
+ function sharpe(returns, rfAnnual) {
1854
+ const rfDaily = dailyRiskFree(rfAnnual);
1855
+ const s = stdev(returns);
1856
+ if (s === 0) return NaN;
1857
+ return (mean(returns) - rfDaily) / s * Math.sqrt(TRADING_DAYS2);
1858
+ }
1859
+ function sortino(returns, rfAnnual) {
1860
+ const rfDaily = dailyRiskFree(rfAnnual);
1861
+ const dd = downsideDeviation(returns, rfDaily);
1862
+ if (dd === 0) return NaN;
1863
+ return (mean(returns) - rfDaily) * TRADING_DAYS2 / dd;
1864
+ }
1865
+ function calmar(cagrValue, maxDdDepth) {
1866
+ if (maxDdDepth === 0) return Infinity;
1867
+ return cagrValue / Math.abs(maxDdDepth);
1868
+ }
1869
+
1870
+ // src/metrics/activity.ts
1871
+ function rebalanceCount(trades) {
1872
+ const dates = /* @__PURE__ */ new Set();
1873
+ for (const t of trades) dates.add(t.date);
1874
+ return dates.size;
1875
+ }
1876
+ function tradeCount(trades) {
1877
+ return trades.length;
1878
+ }
1879
+ function turnover(trades, series, years2) {
1880
+ if (years2 <= 0 || series.length === 0) return 0;
1881
+ let gross = 0;
1882
+ for (const t of trades) {
1883
+ if (t.symbol === "CASHX") continue;
1884
+ gross += Math.abs(t.quantity * t.price);
1885
+ }
1886
+ let navSum = 0;
1887
+ for (const bar of series) navSum += bar.value;
1888
+ const avgNav = navSum / series.length;
1889
+ if (avgNav === 0) return 0;
1890
+ return gross / avgNav / years2;
1891
+ }
1892
+ function navAtOrBefore(series, date) {
1893
+ let result = null;
1894
+ for (const bar of series) {
1895
+ if (bar.date <= date) result = bar.value;
1896
+ else break;
1897
+ }
1898
+ return result;
1899
+ }
1900
+ function winRatePerRebalance(series, trades) {
1901
+ if (series.length < 2) return 0;
1902
+ const firstDate = series[0].date;
1903
+ const lastDate = series[series.length - 1].date;
1904
+ const distinctTradeDates = Array.from(new Set(trades.map((t) => t.date))).sort();
1905
+ const inRange = distinctTradeDates.filter((d) => d > firstDate && d < lastDate);
1906
+ if (inRange.length === 0) {
1907
+ const total2 = series[series.length - 1].value / series[0].value - 1;
1908
+ return total2 > 0 ? 1 : 0;
1909
+ }
1910
+ const boundaries = [firstDate, ...inRange, lastDate];
1911
+ let wins = 0;
1912
+ let total = 0;
1913
+ for (let i = 0; i < boundaries.length - 1; i++) {
1914
+ const a = navAtOrBefore(series, boundaries[i]);
1915
+ const b = navAtOrBefore(series, boundaries[i + 1]);
1916
+ if (a == null || b == null || a === 0) continue;
1917
+ total++;
1918
+ if (b / a - 1 > 0) wins++;
1919
+ }
1920
+ return total === 0 ? 0 : wins / total;
1921
+ }
1922
+
1923
+ // src/metrics/tables.ts
1924
+ function buildMonthlyTable(monthly) {
1925
+ if (monthly.length === 0) return { rows: [] };
1926
+ const byYear = /* @__PURE__ */ new Map();
1927
+ for (const m of monthly) {
1928
+ let row = byYear.get(m.year);
1929
+ if (!row) {
1930
+ row = new Array(12).fill(null);
1931
+ byYear.set(m.year, row);
1932
+ }
1933
+ row[m.month] = m.return;
1934
+ }
1935
+ const years2 = Array.from(byYear.keys()).sort((a, b) => a - b);
1936
+ const rows = years2.map((year) => {
1937
+ const months = byYear.get(year);
1938
+ let ytd = null;
1939
+ for (const v of months) {
1940
+ if (v == null) continue;
1941
+ ytd = (ytd == null ? 1 : 1 + ytd) * (1 + v) - 1;
1942
+ }
1943
+ return { year, months, ytd };
1944
+ });
1945
+ return { rows };
1946
+ }
1947
+ function buildYearlyList(monthly) {
1948
+ const byYear = /* @__PURE__ */ new Map();
1949
+ for (const m of monthly) {
1950
+ if (m.partial) continue;
1951
+ let arr = byYear.get(m.year);
1952
+ if (!arr) {
1953
+ arr = [];
1954
+ byYear.set(m.year, arr);
1955
+ }
1956
+ arr.push(m.return);
1957
+ }
1958
+ const years2 = Array.from(byYear.keys()).sort((a, b) => a - b);
1959
+ return years2.map((year) => {
1960
+ const months = byYear.get(year);
1961
+ let compounded = 1;
1962
+ for (const v of months) compounded *= 1 + v;
1963
+ return { year, return: compounded - 1 };
1964
+ });
1965
+ }
1966
+
1967
+ // src/metrics/compute.ts
1968
+ function computeMetrics(series, trades, options = {}) {
1969
+ if (series.length < 2) {
1970
+ throw new Error("metrics requires at least 2 daily bars");
1971
+ }
1972
+ const rfAnnual = options.riskFreeRate ?? 0;
1973
+ const topN = options.topDrawdowns ?? 5;
1974
+ const conf = options.varConfidence ?? 0.95;
1975
+ const ret = dailyReturns(series);
1976
+ const monthly = monthlyReturns(series);
1977
+ const yearly = yearlyReturns(series);
1978
+ const yrs = years(series);
1979
+ const dds = computeDrawdownTable(series, Math.max(topN, 1));
1980
+ const maxDd = dds[0] ?? {
1981
+ peakDate: series[0].date,
1982
+ troughDate: series[0].date,
1983
+ recoveryDate: series[series.length - 1].date,
1984
+ depth: 0,
1985
+ durationDays: 0,
1986
+ underwaterDays: 0
1987
+ };
1988
+ const cagrVal = cagr(series);
1989
+ return {
1990
+ range: { from: series[0].date, to: series[series.length - 1].date, years: yrs },
1991
+ returns: {
1992
+ totalReturn: totalReturn(series),
1993
+ cagr: cagrVal,
1994
+ bestYear: bestYear(yearly),
1995
+ worstYear: worstYear(yearly),
1996
+ bestMonth: bestMonth(monthly),
1997
+ worstMonth: worstMonth(monthly),
1998
+ pctPositiveMonths: pctPositiveMonths(monthly)
1999
+ },
2000
+ risk: {
2001
+ volatility: volatility(ret),
2002
+ downsideDeviation: downsideDeviation(ret, dailyRiskFree(rfAnnual)),
2003
+ maxDrawdown: maxDd,
2004
+ currentDrawdown: currentDrawdown(series),
2005
+ ulcerIndex: ulcerIndex(series),
2006
+ skew: skewness(ret),
2007
+ kurtosis: excessKurtosis(ret),
2008
+ var95: historicalVar(ret, conf),
2009
+ cvar95: historicalCvar(ret, conf)
2010
+ },
2011
+ riskAdjusted: {
2012
+ sharpe: sharpe(ret, rfAnnual),
2013
+ sortino: sortino(ret, rfAnnual),
2014
+ calmar: calmar(cagrVal, maxDd.depth)
2015
+ },
2016
+ activity: {
2017
+ rebalances: rebalanceCount(trades),
2018
+ trades: tradeCount(trades),
2019
+ turnover: turnover(trades, series, yrs),
2020
+ winRate: winRatePerRebalance(series, trades)
2021
+ },
2022
+ tables: {
2023
+ drawdowns: dds.slice(0, topN),
2024
+ monthly: buildMonthlyTable(monthly),
2025
+ yearly: buildYearlyList(monthly)
2026
+ }
2027
+ };
2028
+ }
2029
+
1530
2030
  // src/backtest/types.ts
1531
2031
  var SimulationHandle = class {
1532
2032
  series;
@@ -1612,6 +2112,9 @@ var SimulationHandle = class {
1612
2112
  * the current UTC ISO date; callers with non-UTC semantics or after-hours
1613
2113
  * rollover should supply their own.
1614
2114
  */
2115
+ metrics(options = {}) {
2116
+ return computeMetrics(this.series, this.trades, options);
2117
+ }
1615
2118
  async pushAndPreview(quotes, options = {}) {
1616
2119
  const priceArgs = [];
1617
2120
  if (this._portfolio) {
@@ -2309,7 +2812,13 @@ export {
2309
2812
  StrategyHandle,
2310
2813
  TickerHandle,
2311
2814
  allocationsEqual,
2815
+ computeDrawdownTable,
2816
+ computeMetrics,
2817
+ monthlyReturns as computeMonthlyReturns,
2312
2818
  computeRebalanceDates,
2819
+ sharpe as computeSharpe,
2820
+ sortino as computeSortino,
2821
+ yearlyReturns as computeYearlyReturns,
2313
2822
  createClient
2314
2823
  };
2315
2824
  //# sourceMappingURL=index.js.map