fin-ratios 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1040 @@
1
+ // src/utils/safe-divide.ts
2
+ function safeDivide(numerator, denominator) {
3
+ if (numerator == null || denominator == null) return null;
4
+ if (denominator === 0) return null;
5
+ return numerator / denominator;
6
+ }
7
+
8
+ // src/ratios/valuation/index.ts
9
+ function pe(input) {
10
+ return safeDivide(input.marketCap, input.netIncome);
11
+ }
12
+ pe.formula = "Market Capitalization / Net Income";
13
+ pe.description = "Trailing price-to-earnings ratio. How much investors pay per dollar of earnings.";
14
+ function forwardPe(input) {
15
+ return safeDivide(input.price, input.forwardEps);
16
+ }
17
+ forwardPe.formula = "Stock Price / Forward EPS Estimate";
18
+ forwardPe.description = "P/E based on next twelve months analyst EPS estimate.";
19
+ function peg(input) {
20
+ return safeDivide(input.peRatio, input.epsGrowthRatePercent);
21
+ }
22
+ peg.formula = "P/E Ratio / EPS Annual Growth Rate (%)";
23
+ peg.description = "PEG ratio. < 1 may indicate undervaluation relative to growth.";
24
+ function pb(input) {
25
+ return safeDivide(input.marketCap, input.totalEquity);
26
+ }
27
+ pb.formula = "Market Capitalization / Total Shareholders Equity";
28
+ pb.description = "Price-to-book ratio. < 1 may indicate trading below net asset value.";
29
+ function ps(input) {
30
+ return safeDivide(input.marketCap, input.revenue);
31
+ }
32
+ ps.formula = "Market Capitalization / Revenue";
33
+ ps.description = "Price-to-sales ratio. Useful when earnings are negative.";
34
+ function pFcf(input) {
35
+ const fcf = input.operatingCashFlow - Math.abs(input.capex);
36
+ return safeDivide(input.marketCap, fcf);
37
+ }
38
+ pFcf.formula = "Market Capitalization / (Operating Cash Flow - Capex)";
39
+ pFcf.description = "Price-to-free-cash-flow. Often considered cleaner than P/E.";
40
+ function enterpriseValue(input) {
41
+ return input.marketCap + input.totalDebt - input.cash + (input.minorityInterest ?? 0) + (input.preferredStock ?? 0);
42
+ }
43
+ enterpriseValue.formula = "Market Cap + Total Debt - Cash + Minority Interest + Preferred Stock";
44
+ enterpriseValue.description = "Enterprise value \u2014 the theoretical takeover price of a company.";
45
+ function evEbitda(input) {
46
+ return safeDivide(input.enterpriseValue, input.ebitda);
47
+ }
48
+ evEbitda.formula = "Enterprise Value / EBITDA";
49
+ evEbitda.description = "Capital-structure-neutral valuation multiple. Popular in LBO analysis.";
50
+ function evEbit(input) {
51
+ return safeDivide(input.enterpriseValue, input.ebit);
52
+ }
53
+ evEbit.formula = "Enterprise Value / EBIT";
54
+ evEbit.description = "Like EV/EBITDA but accounts for depreciation-heavy businesses.";
55
+ function evRevenue(input) {
56
+ return safeDivide(input.enterpriseValue, input.revenue);
57
+ }
58
+ evRevenue.formula = "Enterprise Value / Revenue";
59
+ evRevenue.description = "Useful for high-growth companies without positive EBITDA.";
60
+ function evFcf(input) {
61
+ return safeDivide(input.enterpriseValue, input.freeCashFlow);
62
+ }
63
+ evFcf.formula = "Enterprise Value / Free Cash Flow";
64
+ evFcf.description = "EV-to-free-cash-flow. Preferred by value investors.";
65
+ function evInvestedCapital(input) {
66
+ return safeDivide(input.enterpriseValue, input.investedCapital);
67
+ }
68
+ evInvestedCapital.formula = "Enterprise Value / Invested Capital";
69
+ evInvestedCapital.description = "Indicates whether the market values the company above its invested capital base.";
70
+ function tobinsQ(input) {
71
+ const marketValue = input.marketCap + input.totalDebt;
72
+ return safeDivide(marketValue, input.totalAssets);
73
+ }
74
+ tobinsQ.formula = "(Market Cap + Total Debt) / Total Assets";
75
+ tobinsQ.description = "Q > 1 = market values firm above replacement cost. Q < 1 = below.";
76
+ function grahamNumber(input) {
77
+ if (input.eps <= 0 || input.bookValuePerShare <= 0) return null;
78
+ return Math.sqrt(22.5 * input.eps * input.bookValuePerShare);
79
+ }
80
+ grahamNumber.formula = "sqrt(22.5 \xD7 EPS \xD7 Book Value Per Share)";
81
+ grahamNumber.description = "Ben Graham's estimate of fair value. Buy below, sell above.";
82
+ function grahamIntrinsicValue(input) {
83
+ if (input.aaaBondYield <= 0) return null;
84
+ return safeDivide(
85
+ input.eps * (8.5 + 2 * input.growthRate) * 4.4,
86
+ input.aaaBondYield
87
+ );
88
+ }
89
+ grahamIntrinsicValue.formula = "EPS \xD7 (8.5 + 2 \xD7 Growth Rate) \xD7 4.4 / AAA Bond Yield";
90
+ grahamIntrinsicValue.description = "Graham's 1962 revised intrinsic value formula. Use growth rate as %, e.g. 15.";
91
+
92
+ // src/ratios/valuation/dcf.ts
93
+ function dcf2Stage(input) {
94
+ if (input.wacc <= input.terminalGrowthRate) return null;
95
+ if (input.wacc <= 0) return null;
96
+ let pvStage1 = 0;
97
+ let fcf = input.baseFcf;
98
+ for (let t = 1; t <= input.years; t++) {
99
+ fcf = fcf * (1 + input.growthRate);
100
+ pvStage1 += fcf / Math.pow(1 + input.wacc, t);
101
+ }
102
+ const terminalFcf = fcf * (1 + input.terminalGrowthRate);
103
+ const terminalValue = terminalFcf / (input.wacc - input.terminalGrowthRate);
104
+ const pvTerminalValue = terminalValue / Math.pow(1 + input.wacc, input.years);
105
+ const enterpriseValue2 = pvStage1 + pvTerminalValue;
106
+ const intrinsicValue = enterpriseValue2 - (input.netDebt ?? 0);
107
+ const intrinsicValuePerShare = input.sharesOutstanding ? safeDivide(intrinsicValue, input.sharesOutstanding) : null;
108
+ return { intrinsicValue, intrinsicValuePerShare, pvStage1, pvTerminalValue, terminalValue };
109
+ }
110
+ dcf2Stage.formula = "Sum(FCF_t / (1+WACC)^t) + [FCF_n*(1+g) / (WACC-g)] / (1+WACC)^n";
111
+ dcf2Stage.description = "2-stage DCF. Stage 1 explicit growth, Stage 2 terminal value via Gordon Growth Model.";
112
+ function gordonGrowthModel(input) {
113
+ if (input.requiredReturn <= input.dividendGrowthRate) return null;
114
+ return safeDivide(input.nextDividend, input.requiredReturn - input.dividendGrowthRate);
115
+ }
116
+ gordonGrowthModel.formula = "D1 / (r - g)";
117
+ gordonGrowthModel.description = "DDM for stable dividend-paying stocks. Only valid when r > g.";
118
+ function reverseDcf(input) {
119
+ const target = input.marketCap + (input.netDebt ?? 0);
120
+ const computeEV = (g) => {
121
+ const result = dcf2Stage({
122
+ baseFcf: input.baseFcf,
123
+ growthRate: g,
124
+ years: input.years,
125
+ terminalGrowthRate: input.terminalGrowthRate,
126
+ wacc: input.wacc
127
+ });
128
+ return result?.intrinsicValue ?? null;
129
+ };
130
+ let low = -0.5;
131
+ let high = 5;
132
+ for (let i = 0; i < 50; i++) {
133
+ const mid = (low + high) / 2;
134
+ const ev = computeEV(mid);
135
+ if (ev == null) return null;
136
+ if (Math.abs(ev - target) < 1) break;
137
+ if (ev < target) low = mid;
138
+ else high = mid;
139
+ }
140
+ const impliedGrowthRate = (low + high) / 2;
141
+ return {
142
+ impliedGrowthRate,
143
+ interpretation: `Market implies ${(impliedGrowthRate * 100).toFixed(1)}% annual FCF growth over ${input.years} years`
144
+ };
145
+ }
146
+ reverseDcf.formula = "Solve g such that DCF(g) = Market Cap";
147
+ reverseDcf.description = "Reverse-engineers the FCF growth rate implied by the current stock price.";
148
+
149
+ // src/ratios/profitability/index.ts
150
+ function grossMargin(input) {
151
+ return safeDivide(input.grossProfit, input.revenue);
152
+ }
153
+ grossMargin.formula = "Gross Profit / Revenue";
154
+ grossMargin.description = "Percentage of revenue remaining after cost of goods sold.";
155
+ function operatingMargin(input) {
156
+ return safeDivide(input.ebit, input.revenue);
157
+ }
158
+ operatingMargin.formula = "EBIT / Revenue";
159
+ operatingMargin.description = "Operating income as a percentage of revenue.";
160
+ function ebitdaMargin(input) {
161
+ return safeDivide(input.ebitda, input.revenue);
162
+ }
163
+ ebitdaMargin.formula = "EBITDA / Revenue";
164
+ ebitdaMargin.description = "Earnings before interest, taxes, D&A as % of revenue.";
165
+ function netProfitMargin(input) {
166
+ return safeDivide(input.netIncome, input.revenue);
167
+ }
168
+ netProfitMargin.formula = "Net Income / Revenue";
169
+ netProfitMargin.description = "Bottom-line profitability after all expenses.";
170
+ function nopatMargin(input) {
171
+ return safeDivide(input.nopat, input.revenue);
172
+ }
173
+ nopatMargin.formula = "NOPAT / Revenue";
174
+ nopatMargin.description = "Net operating profit after tax as % of revenue.";
175
+ function roe(input) {
176
+ return safeDivide(input.netIncome, input.avgTotalEquity);
177
+ }
178
+ roe.formula = "Net Income / Average Total Equity";
179
+ roe.description = "Return on equity \u2014 how efficiently a company uses shareholder capital.";
180
+ function roa(input) {
181
+ return safeDivide(input.netIncome, input.avgTotalAssets);
182
+ }
183
+ roa.formula = "Net Income / Average Total Assets";
184
+ roa.description = "Return on assets \u2014 how efficiently assets are used to generate profit.";
185
+ function nopat(input) {
186
+ return input.ebit * (1 - input.taxRate);
187
+ }
188
+ nopat.formula = "EBIT \xD7 (1 - Effective Tax Rate)";
189
+ nopat.description = "Net operating profit after tax \u2014 removes financing effects.";
190
+ function roic(input) {
191
+ return safeDivide(input.nopat, input.investedCapital);
192
+ }
193
+ roic.formula = "NOPAT / Invested Capital";
194
+ roic.description = "ROIC vs WACC determines whether a company creates or destroys value.";
195
+ function roce(input) {
196
+ const capitalEmployed = input.totalAssets - input.currentLiabilities;
197
+ return safeDivide(input.ebit, capitalEmployed);
198
+ }
199
+ roce.formula = "EBIT / (Total Assets - Current Liabilities)";
200
+ roce.description = "Return on capital employed. Includes both equity and long-term debt.";
201
+ function rote(input) {
202
+ const tangibleEquity = input.avgTotalEquity - input.avgGoodwill - input.avgIntangibleAssets;
203
+ return safeDivide(input.netIncome, tangibleEquity);
204
+ }
205
+ rote.formula = "Net Income / (Avg Equity - Avg Goodwill - Avg Intangibles)";
206
+ rote.description = "Return on tangible equity. Strips acquisition premiums from the denominator.";
207
+ function duPont3(input) {
208
+ const margin = safeDivide(input.netIncome, input.revenue);
209
+ const turnover = safeDivide(input.revenue, input.avgTotalAssets);
210
+ const multiplier = safeDivide(input.avgTotalAssets, input.avgTotalEquity);
211
+ const roeVal = margin != null && turnover != null && multiplier != null ? margin * turnover * multiplier : null;
212
+ return { netProfitMargin: margin, assetTurnover: turnover, equityMultiplier: multiplier, roe: roeVal };
213
+ }
214
+ duPont3.formula = "ROE = Net Profit Margin \xD7 Asset Turnover \xD7 Equity Multiplier";
215
+ duPont3.description = "3-factor DuPont decomposition of ROE.";
216
+ function revenuePerEmployee(input) {
217
+ return safeDivide(input.revenue, input.employeeCount);
218
+ }
219
+ revenuePerEmployee.formula = "Revenue / Full-Time Employees";
220
+ revenuePerEmployee.description = "Measures workforce productivity.";
221
+ function profitPerEmployee(input) {
222
+ return safeDivide(input.netIncome, input.employeeCount);
223
+ }
224
+ profitPerEmployee.formula = "Net Income / Full-Time Employees";
225
+ profitPerEmployee.description = "Net income generated per employee.";
226
+ function investedCapital(input) {
227
+ return input.totalEquity + input.totalDebt - input.cash;
228
+ }
229
+ investedCapital.formula = "Total Equity + Total Debt - Excess Cash";
230
+ investedCapital.description = "Capital deployed to generate operating returns.";
231
+
232
+ // src/ratios/liquidity/index.ts
233
+ function currentRatio(input) {
234
+ return safeDivide(input.currentAssets, input.currentLiabilities);
235
+ }
236
+ currentRatio.formula = "Current Assets / Current Liabilities";
237
+ currentRatio.description = "Ability to pay short-term obligations. > 1 generally healthy.";
238
+ function quickRatio(input) {
239
+ const liquid = input.cash + input.shortTermInvestments + input.accountsReceivable;
240
+ return safeDivide(liquid, input.currentLiabilities);
241
+ }
242
+ quickRatio.formula = "(Cash + ST Investments + Accounts Receivable) / Current Liabilities";
243
+ quickRatio.description = "Liquidity excluding inventory. More conservative than current ratio.";
244
+ function cashRatio(input) {
245
+ return safeDivide(input.cash + input.shortTermInvestments, input.currentLiabilities);
246
+ }
247
+ cashRatio.formula = "(Cash + Short-Term Investments) / Current Liabilities";
248
+ cashRatio.description = "Most conservative liquidity measure. Only counts actual cash.";
249
+ function operatingCashFlowRatio(input) {
250
+ return safeDivide(input.operatingCashFlow, input.currentLiabilities);
251
+ }
252
+ operatingCashFlowRatio.formula = "Operating Cash Flow / Current Liabilities";
253
+ operatingCashFlowRatio.description = "How well operating cash flow covers short-term obligations.";
254
+ function dso(input) {
255
+ return safeDivide(input.accountsReceivable * 365, input.revenue);
256
+ }
257
+ dso.formula = "(Accounts Receivable / Revenue) \xD7 365";
258
+ dso.description = "Days Sales Outstanding \u2014 average days to collect payment after a sale.";
259
+ function dio(input) {
260
+ return safeDivide(input.inventory * 365, input.cogs);
261
+ }
262
+ dio.formula = "(Inventory / COGS) \xD7 365";
263
+ dio.description = "Days Inventory Outstanding \u2014 average days inventory is held before sale.";
264
+ function dpo(input) {
265
+ return safeDivide(input.accountsPayable * 365, input.cogs);
266
+ }
267
+ dpo.formula = "(Accounts Payable / COGS) \xD7 365";
268
+ dpo.description = "Days Payable Outstanding \u2014 average days taken to pay suppliers.";
269
+ function cashConversionCycle(input) {
270
+ return input.dso + input.dio - input.dpo;
271
+ }
272
+ cashConversionCycle.formula = "DSO + DIO - DPO";
273
+ cashConversionCycle.description = "Days from cash outflow (inventory) to cash inflow (collections). Lower is better.";
274
+ function defensiveIntervalRatio(input) {
275
+ const liquid = input.cash + input.shortTermInvestments + input.accountsReceivable;
276
+ return safeDivide(liquid, input.dailyOperatingExpenses);
277
+ }
278
+ defensiveIntervalRatio.formula = "(Cash + ST Investments + AR) / Daily Operating Expenses";
279
+ defensiveIntervalRatio.description = "Days the company can operate using liquid assets alone, without new revenue.";
280
+
281
+ // src/ratios/solvency/index.ts
282
+ function debtToEquity(input) {
283
+ return safeDivide(input.totalDebt, input.totalEquity);
284
+ }
285
+ debtToEquity.formula = "Total Debt / Total Equity";
286
+ debtToEquity.description = "Financial leverage. Higher = more debt-financed.";
287
+ function netDebtToEquity(input) {
288
+ const netDebt = input.totalDebt - input.cash;
289
+ return safeDivide(netDebt, input.totalEquity);
290
+ }
291
+ netDebtToEquity.formula = "(Total Debt - Cash) / Total Equity";
292
+ netDebtToEquity.description = "Net leverage ratio. Negative = net cash position.";
293
+ function netDebtToEbitda(input) {
294
+ const netDebt = input.totalDebt - input.cash;
295
+ return safeDivide(netDebt, input.ebitda);
296
+ }
297
+ netDebtToEbitda.formula = "(Total Debt - Cash) / EBITDA";
298
+ netDebtToEbitda.description = "Years to repay net debt from EBITDA. Lenders often require < 3x.";
299
+ function debtToAssets(input) {
300
+ return safeDivide(input.totalDebt, input.totalAssets);
301
+ }
302
+ debtToAssets.formula = "Total Debt / Total Assets";
303
+ debtToAssets.description = "Proportion of assets financed by debt.";
304
+ function debtToCapital(input) {
305
+ return safeDivide(input.totalDebt, input.totalDebt + input.totalEquity);
306
+ }
307
+ debtToCapital.formula = "Total Debt / (Total Debt + Total Equity)";
308
+ debtToCapital.description = "Proportion of capital structure that is debt.";
309
+ function interestCoverageRatio(input) {
310
+ return safeDivide(input.ebit, input.interestExpense);
311
+ }
312
+ interestCoverageRatio.formula = "EBIT / Interest Expense";
313
+ interestCoverageRatio.description = "Times interest is covered by operating earnings. < 1.5 is risky.";
314
+ function ebitdaCoverageRatio(input) {
315
+ return safeDivide(input.ebitda, input.interestExpense);
316
+ }
317
+ ebitdaCoverageRatio.formula = "EBITDA / Interest Expense";
318
+ ebitdaCoverageRatio.description = "Softer coverage ratio including D&A add-back.";
319
+ function debtServiceCoverageRatio(input) {
320
+ return safeDivide(input.netOperatingIncome, input.annualDebtService);
321
+ }
322
+ debtServiceCoverageRatio.formula = "Net Operating Income / Annual Debt Service (Principal + Interest)";
323
+ debtServiceCoverageRatio.description = "Lenders require DSCR > 1.25. Critical for real estate and leveraged deals.";
324
+ function fixedChargeCoverageRatio(input) {
325
+ return safeDivide(input.ebit + input.fixedCharges, input.fixedCharges + input.interestExpense);
326
+ }
327
+ fixedChargeCoverageRatio.formula = "(EBIT + Fixed Charges) / (Fixed Charges + Interest Expense)";
328
+ fixedChargeCoverageRatio.description = "Covers lease payments and other fixed obligations beyond interest.";
329
+ function equityMultiplier(input) {
330
+ return safeDivide(input.totalAssets, input.totalEquity);
331
+ }
332
+ equityMultiplier.formula = "Total Assets / Total Equity";
333
+ equityMultiplier.description = "Leverage component of DuPont analysis.";
334
+
335
+ // src/ratios/efficiency/index.ts
336
+ function assetTurnover(input) {
337
+ return safeDivide(input.revenue, input.avgTotalAssets);
338
+ }
339
+ assetTurnover.formula = "Revenue / Average Total Assets";
340
+ assetTurnover.description = "Sales generated per dollar of total assets.";
341
+ function fixedAssetTurnover(input) {
342
+ return safeDivide(input.revenue, input.avgNetPPE);
343
+ }
344
+ fixedAssetTurnover.formula = "Revenue / Average Net PP&E";
345
+ fixedAssetTurnover.description = "Efficiency of physical asset use. Low in capital-intensive industries.";
346
+ function inventoryTurnover(input) {
347
+ return safeDivide(input.cogs, input.avgInventory);
348
+ }
349
+ inventoryTurnover.formula = "COGS / Average Inventory";
350
+ inventoryTurnover.description = "How many times inventory is sold per year. Higher = less cash tied up.";
351
+ function receivablesTurnover(input) {
352
+ return safeDivide(input.revenue, input.avgAccountsReceivable);
353
+ }
354
+ receivablesTurnover.formula = "Revenue / Average Accounts Receivable";
355
+ receivablesTurnover.description = "How quickly the company collects what it is owed.";
356
+ function payablesTurnover(input) {
357
+ return safeDivide(input.cogs, input.avgAccountsPayable);
358
+ }
359
+ payablesTurnover.formula = "COGS / Average Accounts Payable";
360
+ payablesTurnover.description = "How quickly the company pays its suppliers.";
361
+ function workingCapitalTurnover(input) {
362
+ return safeDivide(input.revenue, input.avgWorkingCapital);
363
+ }
364
+ workingCapitalTurnover.formula = "Revenue / Average Working Capital";
365
+ workingCapitalTurnover.description = "Revenue generated from each dollar of working capital.";
366
+ function capitalTurnover(input) {
367
+ return safeDivide(input.revenue, input.investedCapital);
368
+ }
369
+ capitalTurnover.formula = "Revenue / Invested Capital";
370
+ capitalTurnover.description = "Sales generated per dollar of invested capital.";
371
+ function operatingLeverage(input) {
372
+ const ebitChange = safeDivide(
373
+ input.ebitCurrent - input.ebitPrior,
374
+ Math.abs(input.ebitPrior)
375
+ );
376
+ const revenueChange = safeDivide(
377
+ input.revenueCurrent - input.revenuePrior,
378
+ Math.abs(input.revenuePrior)
379
+ );
380
+ return safeDivide(ebitChange, revenueChange);
381
+ }
382
+ operatingLeverage.formula = "% Change in EBIT / % Change in Revenue";
383
+ operatingLeverage.description = "Sensitivity of operating income to revenue changes. High = more fixed costs.";
384
+
385
+ // src/ratios/cashflow/index.ts
386
+ function freeCashFlow(input) {
387
+ return input.operatingCashFlow - Math.abs(input.capex);
388
+ }
389
+ freeCashFlow.formula = "Operating Cash Flow - Capital Expenditures";
390
+ freeCashFlow.description = "Cash available after maintaining/growing asset base. The most important cash flow metric.";
391
+ function leveredFcf(input) {
392
+ return input.freeCashFlow + input.debtIssuance - input.debtRepayments;
393
+ }
394
+ leveredFcf.formula = "FCF + Net Debt Change (Issuance - Repayments)";
395
+ leveredFcf.description = "FCF after accounting for debt financing activities.";
396
+ function unleveredFcf(input) {
397
+ return input.nopat + input.depreciationAndAmortization - Math.abs(input.capex) - input.changeInWorkingCapital;
398
+ }
399
+ unleveredFcf.formula = "NOPAT + D&A - Capex - Change in Working Capital";
400
+ unleveredFcf.description = "FCF before debt payments \u2014 used in DCF valuation as FCFF.";
401
+ function ownerEarnings(input) {
402
+ return input.netIncome + input.depreciationAndAmortization - input.maintenanceCapex - (input.changeInWorkingCapital ?? 0);
403
+ }
404
+ ownerEarnings.formula = "Net Income + D&A - Maintenance Capex - Change in WC";
405
+ ownerEarnings.description = "Buffett's owner earnings \u2014 true economic earnings available to shareholders.";
406
+ function fcfYield(input) {
407
+ return safeDivide(input.freeCashFlow, input.marketCap);
408
+ }
409
+ fcfYield.formula = "Free Cash Flow / Market Capitalization";
410
+ fcfYield.description = "FCF per dollar invested. Inverse of P/FCF. Higher = cheaper.";
411
+ function fcfMargin(input) {
412
+ return safeDivide(input.freeCashFlow, input.revenue);
413
+ }
414
+ fcfMargin.formula = "Free Cash Flow / Revenue";
415
+ fcfMargin.description = "FCF generated per dollar of revenue.";
416
+ function fcfConversion(input) {
417
+ return safeDivide(input.freeCashFlow, input.netIncome);
418
+ }
419
+ fcfConversion.formula = "Free Cash Flow / Net Income";
420
+ fcfConversion.description = "FCF conversion > 1 means earnings are backed by real cash. < 1 raises quality concerns.";
421
+ function ocfToSales(input) {
422
+ return safeDivide(input.operatingCashFlow, input.revenue);
423
+ }
424
+ ocfToSales.formula = "Operating Cash Flow / Revenue";
425
+ ocfToSales.description = "Cash generated from operations per dollar of sales.";
426
+ function capexToRevenue(input) {
427
+ return safeDivide(Math.abs(input.capex), input.revenue);
428
+ }
429
+ capexToRevenue.formula = "Capital Expenditures / Revenue";
430
+ capexToRevenue.description = "Investment intensity. High in capital-intensive industries like manufacturing.";
431
+ function capexToDepreciation(input) {
432
+ return safeDivide(Math.abs(input.capex), input.depreciation);
433
+ }
434
+ capexToDepreciation.formula = "Capital Expenditures / Depreciation";
435
+ capexToDepreciation.description = "> 1 = company is investing more than assets are aging (growth). < 1 = under-investing.";
436
+ function cashReturnOnAssets(input) {
437
+ return safeDivide(input.operatingCashFlow, input.totalAssets);
438
+ }
439
+ cashReturnOnAssets.formula = "Operating Cash Flow / Total Assets";
440
+ cashReturnOnAssets.description = "Cash-based ROA. Harder to manipulate than accrual-based ROA.";
441
+
442
+ // src/utils/math.ts
443
+ function cagr(start, end, years) {
444
+ if (start == null || end == null || years <= 0) return null;
445
+ if (start <= 0) return null;
446
+ return Math.pow(end / start, 1 / years) - 1;
447
+ }
448
+ function annualizeReturn(returnValue, periodsPerYear) {
449
+ return Math.pow(1 + returnValue, periodsPerYear) - 1;
450
+ }
451
+ function stdDev(values, ddof = 1) {
452
+ if (values.length < 2) return null;
453
+ const mean2 = values.reduce((a, b) => a + b, 0) / values.length;
454
+ const variance = values.reduce((sum, v) => sum + Math.pow(v - mean2, 2), 0) / (values.length - ddof);
455
+ return Math.sqrt(variance);
456
+ }
457
+ function mean(values) {
458
+ if (values.length === 0) return null;
459
+ return values.reduce((a, b) => a + b, 0) / values.length;
460
+ }
461
+ function percentile(values, p) {
462
+ if (values.length === 0) return null;
463
+ const sorted = [...values].sort((a, b) => a - b);
464
+ const idx = p * (sorted.length - 1);
465
+ const lower = Math.floor(idx);
466
+ const upper = Math.ceil(idx);
467
+ if (lower === upper) return sorted[lower] ?? null;
468
+ const lv = sorted[lower];
469
+ const uv = sorted[upper];
470
+ if (lv == null || uv == null) return null;
471
+ return lv + (uv - lv) * (idx - lower);
472
+ }
473
+ function maxDrawdown(prices) {
474
+ if (prices.length < 2) return null;
475
+ let peak = prices[0] ?? 0;
476
+ let maxDD = 0;
477
+ for (const price of prices) {
478
+ if (price > peak) peak = price;
479
+ const dd = peak > 0 ? (peak - price) / peak : 0;
480
+ if (dd > maxDD) maxDD = dd;
481
+ }
482
+ return maxDD;
483
+ }
484
+ function pricesToReturns(prices) {
485
+ const returns = [];
486
+ for (let i = 1; i < prices.length; i++) {
487
+ const prev = prices[i - 1];
488
+ const curr = prices[i];
489
+ if (prev != null && curr != null && prev !== 0) {
490
+ returns.push((curr - prev) / prev);
491
+ }
492
+ }
493
+ return returns;
494
+ }
495
+
496
+ // src/ratios/growth/index.ts
497
+ function yoyGrowth(current, prior) {
498
+ if (prior === 0) return null;
499
+ return safeDivide(current - prior, Math.abs(prior));
500
+ }
501
+ function revenueGrowth(input) {
502
+ return yoyGrowth(input.revenueCurrent, input.revenuePrior);
503
+ }
504
+ revenueGrowth.formula = "(Revenue_t - Revenue_t-1) / |Revenue_t-1|";
505
+ revenueGrowth.description = "Year-over-year revenue growth rate.";
506
+ function revenueCAGR(input) {
507
+ return cagr(input.revenueStart, input.revenueEnd, input.years);
508
+ }
509
+ revenueCAGR.formula = "(Revenue_end / Revenue_start)^(1/years) - 1";
510
+ revenueCAGR.description = "Compound annual growth rate of revenue over multiple years.";
511
+ function epsGrowth(input) {
512
+ return yoyGrowth(input.epsCurrent, input.epsPrior);
513
+ }
514
+ epsGrowth.formula = "(EPS_t - EPS_t-1) / |EPS_t-1|";
515
+ epsGrowth.description = "Year-over-year earnings per share growth.";
516
+ function ebitdaGrowth(input) {
517
+ return yoyGrowth(input.ebitdaCurrent, input.ebitdaPrior);
518
+ }
519
+ ebitdaGrowth.formula = "(EBITDA_t - EBITDA_t-1) / |EBITDA_t-1|";
520
+ ebitdaGrowth.description = "Year-over-year EBITDA growth.";
521
+ function fcfGrowth(input) {
522
+ return yoyGrowth(input.fcfCurrent, input.fcfPrior);
523
+ }
524
+ fcfGrowth.formula = "(FCF_t - FCF_t-1) / |FCF_t-1|";
525
+ fcfGrowth.description = "Year-over-year free cash flow growth.";
526
+ function bvpsGrowth(input) {
527
+ return yoyGrowth(input.bvpsCurrent, input.bvpsPrior);
528
+ }
529
+ bvpsGrowth.formula = "(BVPS_t - BVPS_t-1) / |BVPS_t-1|";
530
+ bvpsGrowth.description = "Book value per share growth \u2014 tracks compounding of equity.";
531
+ function dividendGrowthRate(input) {
532
+ return yoyGrowth(input.dpsCurrent, input.dpsPrior);
533
+ }
534
+ dividendGrowthRate.formula = "(DPS_t - DPS_t-1) / DPS_t-1";
535
+ dividendGrowthRate.description = "Year-over-year dividend per share growth.";
536
+ function earningsPowerValue(input) {
537
+ const nopatValue = input.ebit * (1 - input.taxRate);
538
+ return safeDivide(nopatValue, input.wacc);
539
+ }
540
+ earningsPowerValue.formula = "EBIT \xD7 (1 - Tax Rate) / WACC";
541
+ earningsPowerValue.description = "EPV \u2014 intrinsic value assuming zero growth. Conservative floor for valuation.";
542
+
543
+ // src/ratios/risk/index.ts
544
+ function beta(input) {
545
+ const { stockReturns, marketReturns } = input;
546
+ if (stockReturns.length !== marketReturns.length || stockReturns.length < 2) return null;
547
+ const n = stockReturns.length;
548
+ const meanStock = stockReturns.reduce((a, b) => a + b, 0) / n;
549
+ const meanMarket = marketReturns.reduce((a, b) => a + b, 0) / n;
550
+ let covariance = 0;
551
+ let varianceMarket = 0;
552
+ for (let i = 0; i < n; i++) {
553
+ const ds = (stockReturns[i] ?? 0) - meanStock;
554
+ const dm = (marketReturns[i] ?? 0) - meanMarket;
555
+ covariance += ds * dm;
556
+ varianceMarket += dm * dm;
557
+ }
558
+ return safeDivide(covariance, varianceMarket);
559
+ }
560
+ beta.formula = "Cov(r_stock, r_market) / Var(r_market)";
561
+ beta.description = "Sensitivity of stock returns to market returns. Beta = 1 means moves with market.";
562
+ function jensensAlpha(input) {
563
+ return input.portfolioReturn - (input.riskFreeRate + input.beta * (input.marketReturn - input.riskFreeRate));
564
+ }
565
+ jensensAlpha.formula = "Rp - [Rf + \u03B2 \xD7 (Rm - Rf)]";
566
+ jensensAlpha.description = "Excess return above what CAPM predicts. Positive = outperformance.";
567
+ function sharpeRatio(input) {
568
+ const { returns, riskFreeRate, periodsPerYear = 252 } = input;
569
+ const avgReturn = mean(returns);
570
+ const vol = stdDev(returns, 1);
571
+ if (avgReturn == null || vol == null || vol === 0) return null;
572
+ const annualizedReturn = annualizeReturn(avgReturn, periodsPerYear);
573
+ const annualizedVol = vol * Math.sqrt(periodsPerYear);
574
+ return safeDivide(annualizedReturn - riskFreeRate, annualizedVol);
575
+ }
576
+ sharpeRatio.formula = "(Annualized Return - Risk-Free Rate) / Annualized StdDev";
577
+ sharpeRatio.description = "Risk-adjusted return per unit of total volatility. > 1 is good, > 2 is excellent.";
578
+ function sortinoRatio(input) {
579
+ const { returns, riskFreeRate, mar = 0, periodsPerYear = 252 } = input;
580
+ const avgReturn = mean(returns);
581
+ if (avgReturn == null) return null;
582
+ const downsideReturns = returns.filter((r) => r < mar);
583
+ if (downsideReturns.length === 0) return null;
584
+ const downsideVariance = downsideReturns.reduce((sum, r) => sum + Math.pow(r - mar, 2), 0) / returns.length;
585
+ const downsideDeviation = Math.sqrt(downsideVariance) * Math.sqrt(periodsPerYear);
586
+ if (downsideDeviation === 0) return null;
587
+ const annualizedReturn = annualizeReturn(avgReturn, periodsPerYear);
588
+ return safeDivide(annualizedReturn - riskFreeRate, downsideDeviation);
589
+ }
590
+ sortinoRatio.formula = "(Annualized Return - Rf) / Downside Deviation";
591
+ sortinoRatio.description = "Like Sharpe but penalizes only downside volatility. Better for asymmetric returns.";
592
+ function treynorRatio(input) {
593
+ return safeDivide(input.portfolioReturn - input.riskFreeRate, input.beta);
594
+ }
595
+ treynorRatio.formula = "(Portfolio Return - Risk-Free Rate) / Beta";
596
+ treynorRatio.description = "Risk-adjusted return per unit of market (systematic) risk.";
597
+ function calmarRatio(input) {
598
+ const { returns, periodsPerYear = 252 } = input;
599
+ const avgReturn = mean(returns);
600
+ if (avgReturn == null) return null;
601
+ returns.reduce((acc, r) => acc * (1 + r), 1);
602
+ const prices = [1];
603
+ let cur = 1;
604
+ for (const r of returns) {
605
+ cur *= 1 + r;
606
+ prices.push(cur);
607
+ }
608
+ const mdd = maxDrawdown(prices);
609
+ if (mdd == null || mdd === 0) return null;
610
+ const annualizedReturn = annualizeReturn(avgReturn, periodsPerYear);
611
+ return safeDivide(annualizedReturn, mdd);
612
+ }
613
+ calmarRatio.formula = "Annualized Return / Maximum Drawdown";
614
+ calmarRatio.description = "Return relative to worst drawdown. Popular in hedge fund analysis.";
615
+ function informationRatio(input) {
616
+ const { portfolioReturns, benchmarkReturns } = input;
617
+ if (portfolioReturns.length !== benchmarkReturns.length) return null;
618
+ const activeReturns = portfolioReturns.map((r, i) => r - (benchmarkReturns[i] ?? 0));
619
+ const avgActive = mean(activeReturns);
620
+ const trackingErr = stdDev(activeReturns, 1);
621
+ return safeDivide(avgActive, trackingErr);
622
+ }
623
+ informationRatio.formula = "Mean Active Return / Tracking Error";
624
+ informationRatio.description = "Skill of an active manager. IR > 0.5 is considered good.";
625
+ function omegaRatio(input) {
626
+ const { returns, threshold = 0 } = input;
627
+ let gains = 0;
628
+ let losses = 0;
629
+ for (const r of returns) {
630
+ if (r > threshold) gains += r - threshold;
631
+ else losses += threshold - r;
632
+ }
633
+ return safeDivide(gains, losses);
634
+ }
635
+ omegaRatio.formula = "E[returns above threshold] / E[returns below threshold]";
636
+ omegaRatio.description = "Non-parametric risk-adjusted return. > 1 is desirable.";
637
+ function maximumDrawdown(input) {
638
+ return maxDrawdown(input.prices);
639
+ }
640
+ maximumDrawdown.formula = "(Peak - Trough) / Peak";
641
+ maximumDrawdown.description = "Largest peak-to-trough decline. Measures worst-case loss scenario.";
642
+ function trackingError(input) {
643
+ const { portfolioReturns, benchmarkReturns, periodsPerYear = 252 } = input;
644
+ if (portfolioReturns.length !== benchmarkReturns.length) return null;
645
+ const activeReturns = portfolioReturns.map((r, i) => r - (benchmarkReturns[i] ?? 0));
646
+ const te = stdDev(activeReturns, 1);
647
+ if (te == null) return null;
648
+ return te * Math.sqrt(periodsPerYear);
649
+ }
650
+ trackingError.formula = "Annualized StdDev(Portfolio Returns - Benchmark Returns)";
651
+ trackingError.description = "How closely a portfolio tracks its benchmark. Lower = more index-like.";
652
+ function historicalVaR(input) {
653
+ const { returns, confidence = 0.95 } = input;
654
+ const p = percentile(returns, 1 - confidence);
655
+ return p != null ? -p : null;
656
+ }
657
+ historicalVaR.formula = "-Percentile(returns, 1 - confidence)";
658
+ historicalVaR.description = "Historical VaR at given confidence level. Returns a positive loss value.";
659
+ function parametricVaR(input) {
660
+ const { returns, confidence = 0.95 } = input;
661
+ const mu = mean(returns);
662
+ const sigma = stdDev(returns, 1);
663
+ if (mu == null || sigma == null) return null;
664
+ const zScores = {
665
+ 0.9: 1.282,
666
+ 0.95: 1.645,
667
+ 0.99: 2.326,
668
+ 0.999: 3.09
669
+ };
670
+ const z = zScores[confidence] ?? 1.645;
671
+ return -(mu - z * sigma);
672
+ }
673
+ parametricVaR.formula = "-(\u03BC - z \xD7 \u03C3) where z is the normal quantile for confidence level";
674
+ parametricVaR.description = "Parametric VaR assuming normal distribution.";
675
+ function conditionalVaR(input) {
676
+ const { returns, confidence = 0.95 } = input;
677
+ const var_ = percentile(returns, 1 - confidence);
678
+ if (var_ == null) return null;
679
+ const tailReturns = returns.filter((r) => r <= var_);
680
+ const cvar = mean(tailReturns);
681
+ return cvar != null ? -cvar : null;
682
+ }
683
+ conditionalVaR.formula = "-Mean(returns that are \u2264 VaR threshold)";
684
+ conditionalVaR.description = "CVaR / Expected Shortfall \u2014 average loss in the worst (1-confidence)% scenarios.";
685
+ function ulcerIndex(input) {
686
+ const { prices } = input;
687
+ if (prices.length < 2) return null;
688
+ let peak = prices[0] ?? 0;
689
+ const drawdowns = [];
690
+ for (const p of prices) {
691
+ if (p > peak) peak = p;
692
+ drawdowns.push(peak > 0 ? (p - peak) / peak * 100 : 0);
693
+ }
694
+ const meanSqDD = drawdowns.reduce((sum, d) => sum + d * d, 0) / drawdowns.length;
695
+ return Math.sqrt(meanSqDD);
696
+ }
697
+ ulcerIndex.formula = "sqrt(mean(drawdown_pct^2))";
698
+ ulcerIndex.description = "Measures drawdown depth and duration. Lower is less stressful.";
699
+ function upsideCaptureRatio(input) {
700
+ const { portfolioReturns, benchmarkReturns } = input;
701
+ const upPeriods = benchmarkReturns.map((b, i) => ({ b, p: portfolioReturns[i] ?? 0 })).filter(({ b }) => b > 0);
702
+ if (upPeriods.length === 0) return null;
703
+ const portAvg = mean(upPeriods.map((x) => x.p));
704
+ const benchAvg = mean(upPeriods.map((x) => x.b));
705
+ return safeDivide(portAvg, benchAvg);
706
+ }
707
+ upsideCaptureRatio.formula = "Portfolio Return (up markets) / Benchmark Return (up markets)";
708
+ upsideCaptureRatio.description = "> 100% means outperformed benchmark in up markets.";
709
+ function downsideCaptureRatio(input) {
710
+ const { portfolioReturns, benchmarkReturns } = input;
711
+ const downPeriods = benchmarkReturns.map((b, i) => ({ b, p: portfolioReturns[i] ?? 0 })).filter(({ b }) => b < 0);
712
+ if (downPeriods.length === 0) return null;
713
+ const portAvg = mean(downPeriods.map((x) => x.p));
714
+ const benchAvg = mean(downPeriods.map((x) => x.b));
715
+ return safeDivide(portAvg, benchAvg);
716
+ }
717
+ downsideCaptureRatio.formula = "Portfolio Return (down markets) / Benchmark Return (down markets)";
718
+ downsideCaptureRatio.description = "< 100% means lost less than benchmark in down markets.";
719
+
720
+ // src/ratios/composite/index.ts
721
+ function piotroskiFScore(input) {
722
+ const { current, prior } = input;
723
+ const roaCurrent = current.netIncome / current.totalAssets;
724
+ const roaPrior = prior.netIncome / prior.totalAssets;
725
+ const leverageCurrent = current.totalAssets > 0 ? current.longTermDebt / current.totalAssets : 0;
726
+ const leveragePrior = prior.totalAssets > 0 ? prior.longTermDebt / prior.totalAssets : 0;
727
+ const crCurrent = current.currentLiabilities > 0 ? current.currentAssets / current.currentLiabilities : 0;
728
+ const crPrior = prior.currentLiabilities > 0 ? prior.currentAssets / prior.currentLiabilities : 0;
729
+ const gmCurrent = current.revenue > 0 ? current.grossProfit / current.revenue : 0;
730
+ const gmPrior = prior.revenue > 0 ? prior.grossProfit / prior.revenue : 0;
731
+ const atCurrent = current.totalAssets > 0 ? current.revenue / current.totalAssets : 0;
732
+ const atPrior = prior.totalAssets > 0 ? prior.revenue / prior.totalAssets : 0;
733
+ const signals = {
734
+ // F1: ROA positive
735
+ roa_positive: roaCurrent > 0,
736
+ // F2: Operating cash flow positive
737
+ ocf_positive: current.operatingCashFlow > 0,
738
+ // F3: ROA improving year over year
739
+ roa_improving: roaCurrent > roaPrior,
740
+ // F4: Accruals — OCF > NI (quality earnings, cash-backed)
741
+ quality_earnings: current.operatingCashFlow > current.netIncome,
742
+ // F5: Long-term debt / assets ratio declined
743
+ lower_leverage: leverageCurrent < leveragePrior,
744
+ // F6: Current ratio improved
745
+ higher_liquidity: crCurrent > crPrior,
746
+ // F7: No new shares issued (dilution)
747
+ no_dilution: current.sharesOutstanding <= prior.sharesOutstanding,
748
+ // F8: Gross margin improved
749
+ higher_gross_margin: gmCurrent > gmPrior,
750
+ // F9: Asset turnover improved
751
+ higher_asset_turnover: atCurrent > atPrior
752
+ };
753
+ const score = Object.values(signals).filter(Boolean).length;
754
+ let interpretation = "";
755
+ if (score >= 8) interpretation = "Strong (8-9): High financial strength, potential value opportunity";
756
+ else if (score >= 6) interpretation = "Good (6-7): Reasonably healthy fundamentals";
757
+ else if (score >= 4) interpretation = "Neutral (4-5): Mixed signals, further analysis needed";
758
+ else interpretation = "Weak (0-3): Multiple red flags, high risk";
759
+ return { score, signals, interpretation };
760
+ }
761
+ piotroskiFScore.formula = "9 binary signals across Profitability, Leverage/Liquidity, Operating Efficiency";
762
+ piotroskiFScore.description = "F-Score 0-9. >= 8 is strong buy signal. <= 2 is short signal.";
763
+ function altmanZScore(input) {
764
+ if (input.totalAssets === 0 || input.totalLiabilities === 0) return null;
765
+ const x1 = input.workingCapital / input.totalAssets;
766
+ const x2 = input.retainedEarnings / input.totalAssets;
767
+ const x3 = input.ebit / input.totalAssets;
768
+ const x4 = input.marketCap / input.totalLiabilities;
769
+ const x5 = input.revenue / input.totalAssets;
770
+ const z = 1.2 * x1 + 1.4 * x2 + 3.3 * x3 + 0.6 * x4 + 1 * x5;
771
+ let zone;
772
+ let interpretation;
773
+ if (z > 2.99) {
774
+ zone = "safe";
775
+ interpretation = "Safe zone (Z > 2.99): Low probability of bankruptcy";
776
+ } else if (z > 1.81) {
777
+ zone = "grey";
778
+ interpretation = "Grey zone (1.81 < Z < 2.99): Uncertain, monitor closely";
779
+ } else {
780
+ zone = "distress";
781
+ interpretation = "Distress zone (Z < 1.81): High probability of financial distress";
782
+ }
783
+ return { z, x1, x2, x3, x4, x5, zone, interpretation };
784
+ }
785
+ altmanZScore.formula = "1.2\xD7X1 + 1.4\xD7X2 + 3.3\xD7X3 + 0.6\xD7X4 + 1.0\xD7X5 (public manufacturing)";
786
+ altmanZScore.description = "Bankruptcy prediction model. Safe > 2.99, Distress < 1.81.";
787
+ function beneishMScore(input) {
788
+ const { current: c, prior: p } = input;
789
+ if (p.revenue === 0 || p.totalAssets === 0 || p.grossProfit === 0) return null;
790
+ const dsri = safeDivide(
791
+ safeDivide(c.accountsReceivable, c.revenue),
792
+ safeDivide(p.accountsReceivable, p.revenue)
793
+ );
794
+ const gmi = safeDivide(
795
+ safeDivide(p.grossProfit, p.revenue),
796
+ safeDivide(c.grossProfit, c.revenue)
797
+ );
798
+ const aqiCurrent = c.totalAssets > 0 ? 1 - (c.accountsReceivable + c.ppGross + c.cashFlowFromOps) / c.totalAssets : null;
799
+ const aqiPrior = p.totalAssets > 0 ? 1 - (p.accountsReceivable + p.ppGross) / p.totalAssets : null;
800
+ const aqi = safeDivide(aqiCurrent, aqiPrior);
801
+ const sgi = safeDivide(c.revenue, p.revenue);
802
+ const depiCurrent = c.ppGross > 0 ? c.depreciation / (c.depreciation + c.ppGross) : null;
803
+ const depiPrior = p.ppGross > 0 ? p.depreciation / (p.depreciation + p.ppGross) : null;
804
+ const depi = safeDivide(depiPrior, depiCurrent);
805
+ const sgai = safeDivide(
806
+ safeDivide(c.sgaExpense, c.revenue),
807
+ safeDivide(p.sgaExpense, p.revenue)
808
+ );
809
+ const lvgi = safeDivide(
810
+ safeDivide(c.totalDebt, c.totalAssets),
811
+ safeDivide(p.totalDebt, p.totalAssets)
812
+ );
813
+ const tata = c.totalAssets > 0 ? (c.netIncome - c.cashFlowFromOps) / c.totalAssets : null;
814
+ if (dsri == null || gmi == null || aqi == null || sgi == null || depi == null || sgai == null || lvgi == null || tata == null) return null;
815
+ const mScore = -4.84 + 0.92 * dsri + 0.528 * gmi + 0.404 * aqi + 0.892 * sgi + 0.115 * depi - 0.172 * sgai + 4.679 * tata - 0.327 * lvgi;
816
+ const manipulationLikely = mScore > -2.22;
817
+ return {
818
+ mScore,
819
+ variables: { dsri, gmi, aqi, sgi, depi, sgai, lvgi, tata },
820
+ manipulationLikely,
821
+ interpretation: manipulationLikely ? `M-Score ${mScore.toFixed(2)} > -2.22: Possible earnings manipulation` : `M-Score ${mScore.toFixed(2)} \u2264 -2.22: No strong sign of manipulation`
822
+ };
823
+ }
824
+ beneishMScore.formula = "-4.84 + 0.92\xD7DSRI + 0.528\xD7GMI + 0.404\xD7AQI + 0.892\xD7SGI + 0.115\xD7DEPI - 0.172\xD7SGAI + 4.679\xD7TATA - 0.327\xD7LVGI";
825
+ beneishMScore.description = "Earnings manipulation detector. M-Score > -2.22 indicates likely manipulation.";
826
+ function magicFormula(input) {
827
+ const tangibleCapital = input.netWorkingCapital + input.netFixedAssets;
828
+ return {
829
+ roic: safeDivide(input.ebit, tangibleCapital),
830
+ evEbit: safeDivide(input.enterpriseValue, input.ebit)
831
+ };
832
+ }
833
+ magicFormula.formula = "ROIC = EBIT / (Net Working Capital + Net Fixed Assets); Earnings Yield = EBIT / EV";
834
+ magicFormula.description = "Greenblatt's Magic Formula: rank by ROIC + EV/EBIT. Best combo = buy.";
835
+ function ohlsonOScore(input) {
836
+ if (input.totalAssets <= 0) return null;
837
+ const t1 = -1.32 - 0.407 * Math.log(input.totalAssets / input.gnp);
838
+ const t2 = 6.03 * (input.totalLiabilities / input.totalAssets);
839
+ const t3 = -1.43 * (input.workingCapital / input.totalAssets);
840
+ const t4 = 0.0757 * (input.currentLiabilities / input.currentAssets);
841
+ const t5 = input.totalLiabilities > input.totalAssets ? -1.72 * 1 : 0;
842
+ const t6 = -2.37 * (input.netIncome / input.totalAssets);
843
+ const t7 = -1.83 * (input.operatingCashFlow / input.totalAssets);
844
+ const t8 = 0.285 * (input.netIncome + input.priorNetIncome < 0 ? 1 : 0);
845
+ const t9 = -0.521 * safeDivide(
846
+ input.netIncome - input.priorNetIncome,
847
+ Math.abs(input.netIncome) + Math.abs(input.priorNetIncome)
848
+ );
849
+ const oScore = t1 + t2 + t3 + t4 + t5 + t6 + t7 + t8 + t9;
850
+ const bankruptcyProbability = 1 / (1 + Math.exp(-oScore));
851
+ return {
852
+ oScore,
853
+ bankruptcyProbability,
854
+ interpretation: bankruptcyProbability > 0.5 ? `High bankruptcy risk (${(bankruptcyProbability * 100).toFixed(1)}% probability)` : `Low bankruptcy risk (${(bankruptcyProbability * 100).toFixed(1)}% probability)`
855
+ };
856
+ }
857
+ ohlsonOScore.formula = "Logistic regression: -1.32 - 0.407*SIZE + 6.03*TLTA - 1.43*WCTA + 0.0757*CLCA ...";
858
+ ohlsonOScore.description = "Bankruptcy prediction via logistic regression. Outputs probability 0-1.";
859
+
860
+ // src/ratios/sector/saas/index.ts
861
+ function ruleOf40(input) {
862
+ return input.revenueGrowthRatePct + input.fcfMarginPct;
863
+ }
864
+ ruleOf40.formula = "Revenue Growth Rate (%) + FCF Margin (%)";
865
+ ruleOf40.description = "SaaS health metric. > 40 = healthy balance of growth and profitability.";
866
+ function magicNumber(input) {
867
+ const netNewArrAnnualized = (input.currentQuarterRevenue - input.priorQuarterRevenue) * 4;
868
+ return safeDivide(netNewArrAnnualized, input.priorQuarterSalesAndMarketingSpend);
869
+ }
870
+ magicNumber.formula = "(Current Quarter Revenue - Prior Quarter Revenue) \xD7 4 / Prior Quarter S&M";
871
+ magicNumber.description = "SaaS go-to-market efficiency. > 0.75 is good, > 1.0 is exceptional.";
872
+ function netRevenueRetention(input) {
873
+ return safeDivide(
874
+ input.beginningArr + input.expansion - input.churn - input.contraction,
875
+ input.beginningArr
876
+ );
877
+ }
878
+ netRevenueRetention.formula = "(Beginning ARR + Expansion - Churn - Contraction) / Beginning ARR";
879
+ netRevenueRetention.description = "NRR > 100% means existing customers generate more revenue over time (net expansion).";
880
+ function grossRevenueRetention(input) {
881
+ return safeDivide(
882
+ input.beginningArr - input.churn - input.contraction,
883
+ input.beginningArr
884
+ );
885
+ }
886
+ grossRevenueRetention.formula = "(Beginning ARR - Churn - Contraction) / Beginning ARR";
887
+ grossRevenueRetention.description = "GRR measures pure retention without expansion. Best case = 100%.";
888
+ function customerAcquisitionCost(input) {
889
+ return safeDivide(input.salesAndMarketingSpend, input.newCustomersAcquired);
890
+ }
891
+ customerAcquisitionCost.formula = "Sales & Marketing Spend / New Customers Acquired";
892
+ customerAcquisitionCost.description = "Cost to acquire one new customer.";
893
+ function customerLifetimeValue(input) {
894
+ if (input.monthlyChurnRate <= 0) return null;
895
+ return safeDivide(
896
+ input.avgMonthlyRevenuePerCustomer * input.grossMargin,
897
+ input.monthlyChurnRate
898
+ );
899
+ }
900
+ customerLifetimeValue.formula = "(Avg Monthly Revenue \xD7 Gross Margin) / Monthly Churn Rate";
901
+ customerLifetimeValue.description = "Expected revenue from a customer over their lifetime.";
902
+ function ltvCacRatio(input) {
903
+ return safeDivide(input.ltv, input.cac);
904
+ }
905
+ ltvCacRatio.formula = "LTV / CAC";
906
+ ltvCacRatio.description = "LTV:CAC > 3 is healthy. < 1 means you lose money on every customer.";
907
+ function cacPaybackPeriod(input) {
908
+ const monthlyMargin = input.avgMonthlyRevenuePerCustomer * input.grossMarginPct;
909
+ return safeDivide(input.cac, monthlyMargin);
910
+ }
911
+ cacPaybackPeriod.formula = "CAC / (Avg Monthly Revenue \xD7 Gross Margin %)";
912
+ cacPaybackPeriod.description = "Months to recoup customer acquisition cost. < 12 months is excellent.";
913
+ function burnMultiple(input) {
914
+ return safeDivide(input.netBurnRate, input.netNewArr);
915
+ }
916
+ burnMultiple.formula = "Net Burn Rate / Net New ARR";
917
+ burnMultiple.description = "Cash spent to acquire $1 of new ARR. < 1 is excellent, > 2 is concerning.";
918
+ function saasQuickRatio(input) {
919
+ const gained = input.newMrr + input.expansionMrr;
920
+ const lost = input.churnedMrr + input.contractionMrr;
921
+ return safeDivide(gained, lost);
922
+ }
923
+ saasQuickRatio.formula = "(New MRR + Expansion MRR) / (Churned MRR + Contraction MRR)";
924
+ saasQuickRatio.description = "Growth efficiency. > 4 is excellent, < 1 means shrinking.";
925
+ function arrPerFte(input) {
926
+ return safeDivide(input.arr, input.fullTimeEmployees);
927
+ }
928
+ arrPerFte.formula = "ARR / Full-Time Employees";
929
+ arrPerFte.description = "Productivity benchmark. > $200k/FTE is world-class.";
930
+
931
+ // src/ratios/sector/reit/index.ts
932
+ function ffo(input) {
933
+ return input.netIncome + input.depreciation - input.gainsOnSaleOfProperties;
934
+ }
935
+ ffo.formula = "Net Income + Depreciation & Amortization - Gains on Sale of Properties";
936
+ ffo.description = "Funds From Operations \u2014 the primary REIT earnings metric, strips out real estate depreciation.";
937
+ function affo(input) {
938
+ return input.ffo - input.recurringCapex + input.straightLineRentAdjustment;
939
+ }
940
+ affo.formula = "FFO - Recurring Capex + Straight-Line Rent Adjustment";
941
+ affo.description = "Adjusted FFO \u2014 closer to actual distributable cash flow.";
942
+ function pFfo(input) {
943
+ return safeDivide(input.marketCap, input.ffo);
944
+ }
945
+ pFfo.formula = "Market Cap / FFO";
946
+ pFfo.description = "Price-to-FFO \u2014 the REIT equivalent of P/E.";
947
+ function pAffo(input) {
948
+ return safeDivide(input.marketCap, input.affo);
949
+ }
950
+ pAffo.formula = "Market Cap / AFFO";
951
+ pAffo.description = "Price-to-AFFO. More conservative and widely used by REIT analysts.";
952
+ function netOperatingIncome(input) {
953
+ return input.revenue - input.operatingExpenses;
954
+ }
955
+ netOperatingIncome.formula = "Revenue - Operating Expenses (excluding D&A and interest)";
956
+ netOperatingIncome.description = "NOI \u2014 the core profitability metric for real estate properties.";
957
+ function capRate(input) {
958
+ return safeDivide(input.noi, input.propertyValue);
959
+ }
960
+ capRate.formula = "Net Operating Income / Property Value";
961
+ capRate.description = "Expected annual return on a property. Higher cap rate = higher yield, often higher risk.";
962
+ function occupancyRate(input) {
963
+ return safeDivide(input.occupiedUnits, input.totalUnits);
964
+ }
965
+ occupancyRate.formula = "Occupied Units / Total Units";
966
+ occupancyRate.description = "Key operational metric. > 95% is generally considered strong.";
967
+
968
+ // src/ratios/sector/banking/index.ts
969
+ function netInterestMargin(input) {
970
+ return safeDivide(input.interestIncome - input.interestExpense, input.avgEarningAssets);
971
+ }
972
+ netInterestMargin.formula = "(Interest Income - Interest Expense) / Average Earning Assets";
973
+ netInterestMargin.description = "NIM \u2014 core profitability of a bank's lending activity.";
974
+ function efficiencyRatio(input) {
975
+ return safeDivide(input.nonInterestExpense, input.netInterestIncome + input.nonInterestIncome);
976
+ }
977
+ efficiencyRatio.formula = "Non-Interest Expense / (Net Interest Income + Non-Interest Income)";
978
+ efficiencyRatio.description = "Cost to generate $1 of revenue. Lower is better. < 60% is well-run.";
979
+ function loanToDepositRatio(input) {
980
+ return safeDivide(input.totalLoans, input.totalDeposits);
981
+ }
982
+ loanToDepositRatio.formula = "Total Loans / Total Deposits";
983
+ loanToDepositRatio.description = "Liquidity indicator. > 100% means more loans than deposits (reliant on borrowing).";
984
+ function nplRatio(input) {
985
+ return safeDivide(input.nonPerformingLoans, input.totalLoans);
986
+ }
987
+ nplRatio.formula = "Non-Performing Loans / Total Loans";
988
+ nplRatio.description = "Asset quality metric. > 2-3% warrants scrutiny.";
989
+ function provisionCoverageRatio(input) {
990
+ return safeDivide(input.loanLossReserves, input.nonPerformingLoans);
991
+ }
992
+ provisionCoverageRatio.formula = "Loan Loss Reserves / Non-Performing Loans";
993
+ provisionCoverageRatio.description = "How much of bad loans are reserved for. > 100% is conservative.";
994
+ function tier1CapitalRatio(input) {
995
+ return safeDivide(input.tier1Capital, input.riskWeightedAssets);
996
+ }
997
+ tier1CapitalRatio.formula = "Tier 1 Capital / Risk-Weighted Assets";
998
+ tier1CapitalRatio.description = "Core capital adequacy. Regulatory minimum is 6%, well-capitalized is > 8%.";
999
+ function cet1Ratio(input) {
1000
+ return safeDivide(input.commonEquityTier1, input.riskWeightedAssets);
1001
+ }
1002
+ cet1Ratio.formula = "Common Equity Tier 1 / Risk-Weighted Assets";
1003
+ cet1Ratio.description = "Highest-quality capital ratio. Regulatory minimum is 4.5%.";
1004
+ function tangibleBookValuePerShare(input) {
1005
+ const tbv = input.totalEquity - input.goodwill - input.intangibleAssets;
1006
+ return safeDivide(tbv, input.sharesOutstanding);
1007
+ }
1008
+ tangibleBookValuePerShare.formula = "(Equity - Goodwill - Intangibles) / Shares Outstanding";
1009
+ tangibleBookValuePerShare.description = "TBVPS \u2014 the most conservative per-share book value for banks.";
1010
+
1011
+ // src/ratios/sector/insurance/index.ts
1012
+ function lossRatio(input) {
1013
+ return safeDivide(input.lossesIncurred, input.premiumsEarned);
1014
+ }
1015
+ lossRatio.formula = "Losses Incurred / Premiums Earned";
1016
+ lossRatio.description = "Portion of premiums paid out as claims. < 60% is generally good for P&C.";
1017
+ function expenseRatio(input) {
1018
+ return safeDivide(input.underwritingExpenses, input.premiumsWritten);
1019
+ }
1020
+ expenseRatio.formula = "Underwriting Expenses / Net Premiums Written";
1021
+ expenseRatio.description = "Cost of writing insurance. Lower is more efficient.";
1022
+ function combinedRatio(input) {
1023
+ return input.lossRatio + input.expenseRatio;
1024
+ }
1025
+ combinedRatio.formula = "Loss Ratio + Expense Ratio";
1026
+ combinedRatio.description = "The key insurance profitability metric. < 100% = underwriting profit. > 100% = loss.";
1027
+ function underwritingProfitMargin(input) {
1028
+ return 1 - input.combinedRatio;
1029
+ }
1030
+ underwritingProfitMargin.formula = "1 - Combined Ratio";
1031
+ underwritingProfitMargin.description = "Positive = underwriting profit. Most insurers also earn investment income.";
1032
+ function premiumsToSurplus(input) {
1033
+ return safeDivide(input.netPremiumsWritten, input.policyholderSurplus);
1034
+ }
1035
+ premiumsToSurplus.formula = "Net Premiums Written / Policyholder Surplus";
1036
+ premiumsToSurplus.description = "Leverage ratio for insurers. > 3x is considered risky.";
1037
+
1038
+ export { affo, altmanZScore, annualizeReturn, arrPerFte, assetTurnover, beneishMScore, beta, burnMultiple, bvpsGrowth, cacPaybackPeriod, cagr, calmarRatio, capRate, capexToDepreciation, capexToRevenue, capitalTurnover, cashConversionCycle, cashRatio, cashReturnOnAssets, cet1Ratio, combinedRatio, conditionalVaR, currentRatio, customerAcquisitionCost, customerLifetimeValue, dcf2Stage, debtServiceCoverageRatio, debtToAssets, debtToCapital, debtToEquity, defensiveIntervalRatio, dio, dividendGrowthRate, downsideCaptureRatio, dpo, dso, duPont3, earningsPowerValue, ebitdaCoverageRatio, ebitdaGrowth, ebitdaMargin, efficiencyRatio, enterpriseValue, epsGrowth, equityMultiplier, evEbit, evEbitda, evFcf, evInvestedCapital, evRevenue, expenseRatio, fcfConversion, fcfGrowth, fcfMargin, fcfYield, ffo, fixedAssetTurnover, fixedChargeCoverageRatio, forwardPe, freeCashFlow, gordonGrowthModel, grahamIntrinsicValue, grahamNumber, grossMargin, grossRevenueRetention, historicalVaR, informationRatio, interestCoverageRatio, inventoryTurnover, investedCapital, jensensAlpha, leveredFcf, loanToDepositRatio, lossRatio, ltvCacRatio, magicFormula, magicNumber, maxDrawdown, maximumDrawdown, mean, netDebtToEbitda, netDebtToEquity, netInterestMargin, netOperatingIncome, netProfitMargin, netRevenueRetention, nopat, nopatMargin, nplRatio, occupancyRate, ocfToSales, ohlsonOScore, omegaRatio, operatingCashFlowRatio, operatingLeverage, operatingMargin, ownerEarnings, pAffo, pFcf, pFfo, parametricVaR, payablesTurnover, pb, pe, peg, percentile, piotroskiFScore, premiumsToSurplus, pricesToReturns, profitPerEmployee, provisionCoverageRatio, ps, quickRatio, receivablesTurnover, revenueCAGR, revenueGrowth, revenuePerEmployee, reverseDcf, roa, roce, roe, roic, rote, ruleOf40, saasQuickRatio, safeDivide, sharpeRatio, sortinoRatio, stdDev, tangibleBookValuePerShare, tier1CapitalRatio, tobinsQ, trackingError, treynorRatio, ulcerIndex, underwritingProfitMargin, unleveredFcf, upsideCaptureRatio, workingCapitalTurnover };
1039
+ //# sourceMappingURL=index.js.map
1040
+ //# sourceMappingURL=index.js.map