fin-ratios 0.3.0 → 0.4.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/dist/index.cjs CHANGED
@@ -119,10 +119,10 @@ gordonGrowthModel.formula = "D1 / (r - g)";
119
119
  gordonGrowthModel.description = "DDM for stable dividend-paying stocks. Only valid when r > g.";
120
120
  function reverseDcf(input) {
121
121
  const target = input.marketCap + (input.netDebt ?? 0);
122
- const computeEV = (g3) => {
122
+ const computeEV = (g4) => {
123
123
  const result = dcf2Stage({
124
124
  baseFcf: input.baseFcf,
125
- growthRate: g3,
125
+ growthRate: g4,
126
126
  years: input.years,
127
127
  terminalGrowthRate: input.terminalGrowthRate,
128
128
  wacc: input.wacc
@@ -452,8 +452,8 @@ function annualizeReturn(returnValue, periodsPerYear) {
452
452
  }
453
453
  function stdDev(values, ddof = 1) {
454
454
  if (values.length < 2) return null;
455
- const mean3 = values.reduce((a, b) => a + b, 0) / values.length;
456
- const variance = values.reduce((sum, v) => sum + Math.pow(v - mean3, 2), 0) / (values.length - ddof);
455
+ const mean4 = values.reduce((a, b) => a + b, 0) / values.length;
456
+ const variance = values.reduce((sum, v) => sum + Math.pow(v - mean4, 2), 0) / (values.length - ddof);
457
457
  return Math.sqrt(variance);
458
458
  }
459
459
  function mean(values) {
@@ -1557,6 +1557,174 @@ function moatScore(annualData, options = {}) {
1557
1557
  };
1558
1558
  }
1559
1559
 
1560
+ // src/utils/capital-allocation.ts
1561
+ function mean3(xs) {
1562
+ return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
1563
+ }
1564
+ function std2(xs) {
1565
+ if (xs.length < 2) return 0;
1566
+ const m = mean3(xs);
1567
+ return Math.sqrt(xs.reduce((a, x) => a + (x - m) ** 2, 0) / (xs.length - 1));
1568
+ }
1569
+ function cv2(xs) {
1570
+ const m = mean3(xs);
1571
+ return Math.abs(m) > 1e-9 ? std2(xs) / Math.abs(m) : 1;
1572
+ }
1573
+ function olsSlope2(ys) {
1574
+ const n = ys.length;
1575
+ if (n < 2) return 0;
1576
+ const xm = (n - 1) / 2;
1577
+ const ym = mean3(ys);
1578
+ const ssXX = Array.from({ length: n }, (_, i) => (i - xm) ** 2).reduce((a, b) => a + b, 0);
1579
+ const ssXY = Array.from({ length: n }, (_, i) => (i - xm) * ((ys[i] ?? 0) - ym)).reduce((a, b) => a + b, 0);
1580
+ return ssXX ? ssXY / ssXX : 0;
1581
+ }
1582
+ function clamp(x, lo, hi) {
1583
+ return Math.max(lo, Math.min(hi, x));
1584
+ }
1585
+ function g3(d, k) {
1586
+ const v = d[k];
1587
+ return typeof v === "number" && isFinite(v) ? v : 0;
1588
+ }
1589
+ function estimateWacc2(series, provided) {
1590
+ if (provided !== void 0) return provided;
1591
+ const d = series[series.length - 1];
1592
+ const equity = g3(d, "totalEquity");
1593
+ const debt = g3(d, "totalDebt");
1594
+ const cash = g3(d, "cash");
1595
+ const totalCap = equity + debt - cash;
1596
+ if (totalCap <= 0) return 0.1;
1597
+ const costEquity = 0.045 + 0.055;
1598
+ const interest = g3(d, "interestExpense");
1599
+ let costDebt = 0.04;
1600
+ if (debt > 0 && interest > 0) {
1601
+ const preTax = clamp(interest / debt, 0.02, 0.15);
1602
+ const ebt = g3(d, "ebt") || g3(d, "ebit") - interest;
1603
+ const tax = g3(d, "incomeTaxExpense");
1604
+ const taxRate = ebt > 0 && tax > 0 ? clamp(tax / ebt, 0, 0.4) : 0.21;
1605
+ costDebt = preTax * (1 - taxRate);
1606
+ }
1607
+ const wE = clamp(equity / totalCap, 0, 1);
1608
+ return clamp(wE * costEquity + (1 - wE) * costDebt, 0.06, 0.2);
1609
+ }
1610
+ function yearRoic2(d) {
1611
+ const ebit = g3(d, "ebit");
1612
+ const ic = g3(d, "totalEquity") + g3(d, "totalDebt") - g3(d, "cash");
1613
+ if (ic <= 0) return null;
1614
+ const ebt = g3(d, "ebt") || ebit - g3(d, "interestExpense");
1615
+ const tax = g3(d, "incomeTaxExpense");
1616
+ const taxRate = ebt > 0 && tax > 0 ? clamp(tax / ebt, 0, 0.5) : 0.21;
1617
+ return ebit * (1 - taxRate) / ic;
1618
+ }
1619
+ function scoreValueCreation(series, wacc) {
1620
+ const roicVals = series.map(yearRoic2).filter((r) => r !== null);
1621
+ if (!roicVals.length) return [0.3, ["Value creation: insufficient ROIC data (neutral score)"]];
1622
+ const spreads = roicVals.map((r) => r - wacc);
1623
+ const ms = mean3(spreads);
1624
+ const slope = olsSlope2(spreads);
1625
+ const level = clamp((ms + 0.05) / 0.25, 0, 1);
1626
+ const trendAdj = slope > 0.01 ? 0.1 : slope < -0.01 ? -0.1 : 0;
1627
+ const consistency = Math.max(0, 1 - cv2(spreads) * 0.5);
1628
+ const score = clamp(0.6 * level + 0.4 * consistency + trendAdj, 0, 1);
1629
+ const pos = spreads.filter((s) => s > 0).length;
1630
+ const dir = slope > 0.01 ? "improving" : slope < -0.01 ? "declining" : "stable";
1631
+ return [score, [
1632
+ `Value creation: mean ROIC-WACC spread ${(ms * 100).toFixed(1)}% (${pos}/${spreads.length} years positive)`,
1633
+ `Spread trend: ${dir} (OLS ${(slope * 100).toFixed(2)}%/yr)`
1634
+ ]];
1635
+ }
1636
+ function scoreFcfQuality(series) {
1637
+ const conversions = [];
1638
+ for (const d of series) {
1639
+ const ebit = g3(d, "ebit");
1640
+ if (ebit <= 0) continue;
1641
+ const ebt = g3(d, "ebt") || ebit - g3(d, "interestExpense");
1642
+ const tax = g3(d, "incomeTaxExpense");
1643
+ const taxRate = ebt > 0 && tax > 0 ? clamp(tax / ebt, 0, 0.5) : 0.21;
1644
+ const nopat2 = ebit * (1 - taxRate);
1645
+ if (nopat2 <= 0) continue;
1646
+ const fcf = nopat2 + g3(d, "depreciation") - g3(d, "capex");
1647
+ conversions.push(fcf / nopat2);
1648
+ }
1649
+ if (!conversions.length) return [0.4, ["FCF quality: insufficient data (neutral score)"]];
1650
+ const mc = mean3(conversions);
1651
+ const level = clamp(mc / 1.2, 0, 1);
1652
+ const stability = Math.max(0, 1 - cv2(conversions) * 1.5);
1653
+ const score = clamp(0.65 * level + 0.35 * stability, 0, 1);
1654
+ const quality = mc >= 1 ? "capital-light" : mc < 0.5 ? "capital-heavy" : "moderate";
1655
+ return [score, [`FCF quality: mean NOPAT-to-FCF conversion ${(mc * 100).toFixed(0)}% (${quality})`]];
1656
+ }
1657
+ function scoreReinvestmentYield(series) {
1658
+ const yields = [];
1659
+ for (let i = 1; i < series.length; i++) {
1660
+ const prev = series[i - 1];
1661
+ const curr = series[i];
1662
+ const da = g3(curr, "totalAssets") - g3(prev, "totalAssets");
1663
+ const dr = g3(curr, "revenue") - g3(prev, "revenue");
1664
+ if (da > 1e6) yields.push(dr / da);
1665
+ }
1666
+ if (!yields.length) return [0.4, ["Reinvestment yield: insufficient data (neutral score)"]];
1667
+ const my = mean3(yields);
1668
+ const level = clamp((my + 0.2) / 1.7, 0, 1);
1669
+ const consistency = Math.max(0, 1 - cv2(yields) * 0.5);
1670
+ const score = clamp(0.65 * level + 0.35 * consistency, 0, 1);
1671
+ const pos = yields.filter((y) => y > 0).length;
1672
+ return [score, [
1673
+ `Reinvestment yield: mean incremental revenue/asset ${my.toFixed(2)}x (${pos}/${yields.length} periods productive)`
1674
+ ]];
1675
+ }
1676
+ function scorePayoutDiscipline(series, wacc) {
1677
+ const coverages = [];
1678
+ for (const d of series) {
1679
+ const ebit = g3(d, "ebit");
1680
+ if (ebit <= 0) continue;
1681
+ const ebt = g3(d, "ebt") || ebit - g3(d, "interestExpense");
1682
+ const tax = g3(d, "incomeTaxExpense");
1683
+ const taxRate = ebt > 0 && tax > 0 ? clamp(tax / ebt, 0, 0.5) : 0.21;
1684
+ const nopat2 = ebit * (1 - taxRate);
1685
+ const fcf = nopat2 + g3(d, "depreciation") - g3(d, "capex");
1686
+ const div = g3(d, "dividendsPaid");
1687
+ if (div > 0) coverages.push(fcf > 0 ? fcf / div : 0);
1688
+ }
1689
+ if (coverages.length) {
1690
+ const mc = mean3(coverages);
1691
+ const score = clamp((mc - 0.5) / 2, 0, 1);
1692
+ const health = mc >= 2 ? "healthy" : mc >= 1 ? "tight" : "stressed";
1693
+ return [score, [`Payout discipline: mean FCF/dividend coverage ${mc.toFixed(1)}x (${health})`]];
1694
+ }
1695
+ const roicVals = series.map(yearRoic2).filter((r) => r !== null);
1696
+ const mr = mean3(roicVals);
1697
+ if (mr > wacc * 1.2) return [0.7, ["Payout discipline: retaining FCF in above-WACC business (good reinvestment)"]];
1698
+ if (mr <= wacc) return [0.35, ["Payout discipline: retaining FCF despite below-WACC returns (concerning)"]];
1699
+ return [0.5, ["Payout discipline: no dividend data available (neutral score)"]];
1700
+ }
1701
+ function capitalAllocationScore(annualData, options = {}) {
1702
+ if (annualData.length < 2) {
1703
+ throw new Error("capitalAllocationScore requires at least 2 years of data.");
1704
+ }
1705
+ const wacc = estimateWacc2(annualData, options.wacc);
1706
+ const [vcScore, vcEv] = scoreValueCreation(annualData, wacc);
1707
+ const [fcfScore, fcfEv] = scoreFcfQuality(annualData);
1708
+ const [riScore, riEv] = scoreReinvestmentYield(annualData);
1709
+ const [pdScore, pdEv] = scorePayoutDiscipline(annualData, wacc);
1710
+ const raw = 0.35 * vcScore + 0.25 * fcfScore + 0.25 * riScore + 0.15 * pdScore;
1711
+ const score = Math.round(clamp(raw, 0, 1) * 100);
1712
+ const rating = score >= 75 ? "excellent" : score >= 50 ? "good" : score >= 25 ? "fair" : "poor";
1713
+ return {
1714
+ score,
1715
+ rating,
1716
+ components: {
1717
+ valueCreation: Math.round(vcScore * 1e4) / 1e4,
1718
+ fcfQuality: Math.round(fcfScore * 1e4) / 1e4,
1719
+ reinvestmentYield: Math.round(riScore * 1e4) / 1e4,
1720
+ payoutDiscipline: Math.round(pdScore * 1e4) / 1e4
1721
+ },
1722
+ waccUsed: wacc,
1723
+ yearsAnalyzed: annualData.length,
1724
+ evidence: [...vcEv, ...fcfEv, ...riEv, ...pdEv]
1725
+ };
1726
+ }
1727
+
1560
1728
  exports.affo = affo;
1561
1729
  exports.altmanZScore = altmanZScore;
1562
1730
  exports.annualizeReturn = annualizeReturn;
@@ -1574,6 +1742,7 @@ exports.calmarRatio = calmarRatio;
1574
1742
  exports.capRate = capRate;
1575
1743
  exports.capexToDepreciation = capexToDepreciation;
1576
1744
  exports.capexToRevenue = capexToRevenue;
1745
+ exports.capitalAllocationScore = capitalAllocationScore;
1577
1746
  exports.capitalTurnover = capitalTurnover;
1578
1747
  exports.cashConversionCycle = cashConversionCycle;
1579
1748
  exports.cashRatio = cashRatio;