fin-ratios 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,614 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/hooks/index.ts
6
+
7
+ // src/utils/safe-divide.ts
8
+ function safeDivide(numerator, denominator) {
9
+ if (numerator == null || denominator == null) return null;
10
+ if (denominator === 0) return null;
11
+ return numerator / denominator;
12
+ }
13
+
14
+ // src/ratios/valuation/index.ts
15
+ function pe(input) {
16
+ return safeDivide(input.marketCap, input.netIncome);
17
+ }
18
+ pe.formula = "Market Capitalization / Net Income";
19
+ pe.description = "Trailing price-to-earnings ratio. How much investors pay per dollar of earnings.";
20
+ function peg(input) {
21
+ return safeDivide(input.peRatio, input.epsGrowthRatePercent);
22
+ }
23
+ peg.formula = "P/E Ratio / EPS Annual Growth Rate (%)";
24
+ peg.description = "PEG ratio. < 1 may indicate undervaluation relative to growth.";
25
+ function pb(input) {
26
+ return safeDivide(input.marketCap, input.totalEquity);
27
+ }
28
+ pb.formula = "Market Capitalization / Total Shareholders Equity";
29
+ pb.description = "Price-to-book ratio. < 1 may indicate trading below net asset value.";
30
+ function ps(input) {
31
+ return safeDivide(input.marketCap, input.revenue);
32
+ }
33
+ ps.formula = "Market Capitalization / Revenue";
34
+ ps.description = "Price-to-sales ratio. Useful when earnings are negative.";
35
+ function pFcf(input) {
36
+ const fcf = input.operatingCashFlow - Math.abs(input.capex);
37
+ return safeDivide(input.marketCap, fcf);
38
+ }
39
+ pFcf.formula = "Market Capitalization / (Operating Cash Flow - Capex)";
40
+ pFcf.description = "Price-to-free-cash-flow. Often considered cleaner than P/E.";
41
+ function evEbitda(input) {
42
+ return safeDivide(input.enterpriseValue, input.ebitda);
43
+ }
44
+ evEbitda.formula = "Enterprise Value / EBITDA";
45
+ evEbitda.description = "Capital-structure-neutral valuation multiple. Popular in LBO analysis.";
46
+ function evEbit(input) {
47
+ return safeDivide(input.enterpriseValue, input.ebit);
48
+ }
49
+ evEbit.formula = "Enterprise Value / EBIT";
50
+ evEbit.description = "Like EV/EBITDA but accounts for depreciation-heavy businesses.";
51
+ function evRevenue(input) {
52
+ return safeDivide(input.enterpriseValue, input.revenue);
53
+ }
54
+ evRevenue.formula = "Enterprise Value / Revenue";
55
+ evRevenue.description = "Useful for high-growth companies without positive EBITDA.";
56
+ function evFcf(input) {
57
+ return safeDivide(input.enterpriseValue, input.freeCashFlow);
58
+ }
59
+ evFcf.formula = "Enterprise Value / Free Cash Flow";
60
+ evFcf.description = "EV-to-free-cash-flow. Preferred by value investors.";
61
+ function tobinsQ(input) {
62
+ const marketValue = input.marketCap + input.totalDebt;
63
+ return safeDivide(marketValue, input.totalAssets);
64
+ }
65
+ tobinsQ.formula = "(Market Cap + Total Debt) / Total Assets";
66
+ tobinsQ.description = "Q > 1 = market values firm above replacement cost. Q < 1 = below.";
67
+ function grahamNumber(input) {
68
+ if (input.eps <= 0 || input.bookValuePerShare <= 0) return null;
69
+ return Math.sqrt(22.5 * input.eps * input.bookValuePerShare);
70
+ }
71
+ grahamNumber.formula = "sqrt(22.5 \xD7 EPS \xD7 Book Value Per Share)";
72
+ grahamNumber.description = "Ben Graham's estimate of fair value. Buy below, sell above.";
73
+
74
+ // src/ratios/profitability/index.ts
75
+ function grossMargin(input) {
76
+ return safeDivide(input.grossProfit, input.revenue);
77
+ }
78
+ grossMargin.formula = "Gross Profit / Revenue";
79
+ grossMargin.description = "Percentage of revenue remaining after cost of goods sold.";
80
+ function operatingMargin(input) {
81
+ return safeDivide(input.ebit, input.revenue);
82
+ }
83
+ operatingMargin.formula = "EBIT / Revenue";
84
+ operatingMargin.description = "Operating income as a percentage of revenue.";
85
+ function ebitdaMargin(input) {
86
+ return safeDivide(input.ebitda, input.revenue);
87
+ }
88
+ ebitdaMargin.formula = "EBITDA / Revenue";
89
+ ebitdaMargin.description = "Earnings before interest, taxes, D&A as % of revenue.";
90
+ function netProfitMargin(input) {
91
+ return safeDivide(input.netIncome, input.revenue);
92
+ }
93
+ netProfitMargin.formula = "Net Income / Revenue";
94
+ netProfitMargin.description = "Bottom-line profitability after all expenses.";
95
+ function roe(input) {
96
+ return safeDivide(input.netIncome, input.avgTotalEquity);
97
+ }
98
+ roe.formula = "Net Income / Average Total Equity";
99
+ roe.description = "Return on equity \u2014 how efficiently a company uses shareholder capital.";
100
+ function roa(input) {
101
+ return safeDivide(input.netIncome, input.avgTotalAssets);
102
+ }
103
+ roa.formula = "Net Income / Average Total Assets";
104
+ roa.description = "Return on assets \u2014 how efficiently assets are used to generate profit.";
105
+ function nopat(input) {
106
+ return input.ebit * (1 - input.taxRate);
107
+ }
108
+ nopat.formula = "EBIT \xD7 (1 - Effective Tax Rate)";
109
+ nopat.description = "Net operating profit after tax \u2014 removes financing effects.";
110
+ function roic(input) {
111
+ return safeDivide(input.nopat, input.investedCapital);
112
+ }
113
+ roic.formula = "NOPAT / Invested Capital";
114
+ roic.description = "ROIC vs WACC determines whether a company creates or destroys value.";
115
+ function roce(input) {
116
+ const capitalEmployed = input.totalAssets - input.currentLiabilities;
117
+ return safeDivide(input.ebit, capitalEmployed);
118
+ }
119
+ roce.formula = "EBIT / (Total Assets - Current Liabilities)";
120
+ roce.description = "Return on capital employed. Includes both equity and long-term debt.";
121
+ function investedCapital(input) {
122
+ return input.totalEquity + input.totalDebt - input.cash;
123
+ }
124
+ investedCapital.formula = "Total Equity + Total Debt - Excess Cash";
125
+ investedCapital.description = "Capital deployed to generate operating returns.";
126
+
127
+ // src/ratios/liquidity/index.ts
128
+ function currentRatio(input) {
129
+ return safeDivide(input.currentAssets, input.currentLiabilities);
130
+ }
131
+ currentRatio.formula = "Current Assets / Current Liabilities";
132
+ currentRatio.description = "Ability to pay short-term obligations. > 1 generally healthy.";
133
+ function quickRatio(input) {
134
+ const liquid = input.cash + input.shortTermInvestments + input.accountsReceivable;
135
+ return safeDivide(liquid, input.currentLiabilities);
136
+ }
137
+ quickRatio.formula = "(Cash + ST Investments + Accounts Receivable) / Current Liabilities";
138
+ quickRatio.description = "Liquidity excluding inventory. More conservative than current ratio.";
139
+ function dso(input) {
140
+ return safeDivide(input.accountsReceivable * 365, input.revenue);
141
+ }
142
+ dso.formula = "(Accounts Receivable / Revenue) \xD7 365";
143
+ dso.description = "Days Sales Outstanding \u2014 average days to collect payment after a sale.";
144
+ function dio(input) {
145
+ return safeDivide(input.inventory * 365, input.cogs);
146
+ }
147
+ dio.formula = "(Inventory / COGS) \xD7 365";
148
+ dio.description = "Days Inventory Outstanding \u2014 average days inventory is held before sale.";
149
+ function dpo(input) {
150
+ return safeDivide(input.accountsPayable * 365, input.cogs);
151
+ }
152
+ dpo.formula = "(Accounts Payable / COGS) \xD7 365";
153
+ dpo.description = "Days Payable Outstanding \u2014 average days taken to pay suppliers.";
154
+ function cashConversionCycle(input) {
155
+ return input.dso + input.dio - input.dpo;
156
+ }
157
+ cashConversionCycle.formula = "DSO + DIO - DPO";
158
+ cashConversionCycle.description = "Days from cash outflow (inventory) to cash inflow (collections). Lower is better.";
159
+
160
+ // src/ratios/solvency/index.ts
161
+ function debtToEquity(input) {
162
+ return safeDivide(input.totalDebt, input.totalEquity);
163
+ }
164
+ debtToEquity.formula = "Total Debt / Total Equity";
165
+ debtToEquity.description = "Financial leverage. Higher = more debt-financed.";
166
+ function netDebtToEquity(input) {
167
+ const netDebt = input.totalDebt - input.cash;
168
+ return safeDivide(netDebt, input.totalEquity);
169
+ }
170
+ netDebtToEquity.formula = "(Total Debt - Cash) / Total Equity";
171
+ netDebtToEquity.description = "Net leverage ratio. Negative = net cash position.";
172
+ function netDebtToEbitda(input) {
173
+ const netDebt = input.totalDebt - input.cash;
174
+ return safeDivide(netDebt, input.ebitda);
175
+ }
176
+ netDebtToEbitda.formula = "(Total Debt - Cash) / EBITDA";
177
+ netDebtToEbitda.description = "Years to repay net debt from EBITDA. Lenders often require < 3x.";
178
+ function debtToAssets(input) {
179
+ return safeDivide(input.totalDebt, input.totalAssets);
180
+ }
181
+ debtToAssets.formula = "Total Debt / Total Assets";
182
+ debtToAssets.description = "Proportion of assets financed by debt.";
183
+ function interestCoverageRatio(input) {
184
+ return safeDivide(input.ebit, input.interestExpense);
185
+ }
186
+ interestCoverageRatio.formula = "EBIT / Interest Expense";
187
+ interestCoverageRatio.description = "Times interest is covered by operating earnings. < 1.5 is risky.";
188
+ function equityMultiplier(input) {
189
+ return safeDivide(input.totalAssets, input.totalEquity);
190
+ }
191
+ equityMultiplier.formula = "Total Assets / Total Equity";
192
+ equityMultiplier.description = "Leverage component of DuPont analysis.";
193
+
194
+ // src/ratios/efficiency/index.ts
195
+ function assetTurnover(input) {
196
+ return safeDivide(input.revenue, input.avgTotalAssets);
197
+ }
198
+ assetTurnover.formula = "Revenue / Average Total Assets";
199
+ assetTurnover.description = "Sales generated per dollar of total assets.";
200
+ function inventoryTurnover(input) {
201
+ return safeDivide(input.cogs, input.avgInventory);
202
+ }
203
+ inventoryTurnover.formula = "COGS / Average Inventory";
204
+ inventoryTurnover.description = "How many times inventory is sold per year. Higher = less cash tied up.";
205
+ function receivablesTurnover(input) {
206
+ return safeDivide(input.revenue, input.avgAccountsReceivable);
207
+ }
208
+ receivablesTurnover.formula = "Revenue / Average Accounts Receivable";
209
+ receivablesTurnover.description = "How quickly the company collects what it is owed.";
210
+ function payablesTurnover(input) {
211
+ return safeDivide(input.cogs, input.avgAccountsPayable);
212
+ }
213
+ payablesTurnover.formula = "COGS / Average Accounts Payable";
214
+ payablesTurnover.description = "How quickly the company pays its suppliers.";
215
+
216
+ // src/ratios/cashflow/index.ts
217
+ function freeCashFlow(input) {
218
+ return input.operatingCashFlow - Math.abs(input.capex);
219
+ }
220
+ freeCashFlow.formula = "Operating Cash Flow - Capital Expenditures";
221
+ freeCashFlow.description = "Cash available after maintaining/growing asset base. The most important cash flow metric.";
222
+ function fcfYield(input) {
223
+ return safeDivide(input.freeCashFlow, input.marketCap);
224
+ }
225
+ fcfYield.formula = "Free Cash Flow / Market Capitalization";
226
+ fcfYield.description = "FCF per dollar invested. Inverse of P/FCF. Higher = cheaper.";
227
+ function fcfMargin(input) {
228
+ return safeDivide(input.freeCashFlow, input.revenue);
229
+ }
230
+ fcfMargin.formula = "Free Cash Flow / Revenue";
231
+ fcfMargin.description = "FCF generated per dollar of revenue.";
232
+ function fcfConversion(input) {
233
+ return safeDivide(input.freeCashFlow, input.netIncome);
234
+ }
235
+ fcfConversion.formula = "Free Cash Flow / Net Income";
236
+ fcfConversion.description = "FCF conversion > 1 means earnings are backed by real cash. < 1 raises quality concerns.";
237
+ function ocfToSales(input) {
238
+ return safeDivide(input.operatingCashFlow, input.revenue);
239
+ }
240
+ ocfToSales.formula = "Operating Cash Flow / Revenue";
241
+ ocfToSales.description = "Cash generated from operations per dollar of sales.";
242
+ function capexToRevenue(input) {
243
+ return safeDivide(Math.abs(input.capex), input.revenue);
244
+ }
245
+ capexToRevenue.formula = "Capital Expenditures / Revenue";
246
+ capexToRevenue.description = "Investment intensity. High in capital-intensive industries like manufacturing.";
247
+
248
+ // src/ratios/composite/index.ts
249
+ function piotroskiFScore(input) {
250
+ const { current, prior } = input;
251
+ const roaCurrent = current.netIncome / current.totalAssets;
252
+ const roaPrior = prior.netIncome / prior.totalAssets;
253
+ const leverageCurrent = current.totalAssets > 0 ? current.longTermDebt / current.totalAssets : 0;
254
+ const leveragePrior = prior.totalAssets > 0 ? prior.longTermDebt / prior.totalAssets : 0;
255
+ const crCurrent = current.currentLiabilities > 0 ? current.currentAssets / current.currentLiabilities : 0;
256
+ const crPrior = prior.currentLiabilities > 0 ? prior.currentAssets / prior.currentLiabilities : 0;
257
+ const gmCurrent = current.revenue > 0 ? current.grossProfit / current.revenue : 0;
258
+ const gmPrior = prior.revenue > 0 ? prior.grossProfit / prior.revenue : 0;
259
+ const atCurrent = current.totalAssets > 0 ? current.revenue / current.totalAssets : 0;
260
+ const atPrior = prior.totalAssets > 0 ? prior.revenue / prior.totalAssets : 0;
261
+ const signals = {
262
+ // F1: ROA positive
263
+ roa_positive: roaCurrent > 0,
264
+ // F2: Operating cash flow positive
265
+ ocf_positive: current.operatingCashFlow > 0,
266
+ // F3: ROA improving year over year
267
+ roa_improving: roaCurrent > roaPrior,
268
+ // F4: Accruals — OCF > NI (quality earnings, cash-backed)
269
+ quality_earnings: current.operatingCashFlow > current.netIncome,
270
+ // F5: Long-term debt / assets ratio declined
271
+ lower_leverage: leverageCurrent < leveragePrior,
272
+ // F6: Current ratio improved
273
+ higher_liquidity: crCurrent > crPrior,
274
+ // F7: No new shares issued (dilution)
275
+ no_dilution: current.sharesOutstanding <= prior.sharesOutstanding,
276
+ // F8: Gross margin improved
277
+ higher_gross_margin: gmCurrent > gmPrior,
278
+ // F9: Asset turnover improved
279
+ higher_asset_turnover: atCurrent > atPrior
280
+ };
281
+ const score = Object.values(signals).filter(Boolean).length;
282
+ let interpretation = "";
283
+ if (score >= 8) interpretation = "Strong (8-9): High financial strength, potential value opportunity";
284
+ else if (score >= 6) interpretation = "Good (6-7): Reasonably healthy fundamentals";
285
+ else if (score >= 4) interpretation = "Neutral (4-5): Mixed signals, further analysis needed";
286
+ else interpretation = "Weak (0-3): Multiple red flags, high risk";
287
+ return { score, signals, interpretation };
288
+ }
289
+ piotroskiFScore.formula = "9 binary signals across Profitability, Leverage/Liquidity, Operating Efficiency";
290
+ piotroskiFScore.description = "F-Score 0-9. >= 8 is strong buy signal. <= 2 is short signal.";
291
+ function altmanZScore(input) {
292
+ if (input.totalAssets === 0 || input.totalLiabilities === 0) return null;
293
+ const x1 = input.workingCapital / input.totalAssets;
294
+ const x2 = input.retainedEarnings / input.totalAssets;
295
+ const x3 = input.ebit / input.totalAssets;
296
+ const x4 = input.marketCap / input.totalLiabilities;
297
+ const x5 = input.revenue / input.totalAssets;
298
+ const z = 1.2 * x1 + 1.4 * x2 + 3.3 * x3 + 0.6 * x4 + 1 * x5;
299
+ let zone;
300
+ let interpretation;
301
+ if (z > 2.99) {
302
+ zone = "safe";
303
+ interpretation = "Safe zone (Z > 2.99): Low probability of bankruptcy";
304
+ } else if (z > 1.81) {
305
+ zone = "grey";
306
+ interpretation = "Grey zone (1.81 < Z < 2.99): Uncertain, monitor closely";
307
+ } else {
308
+ zone = "distress";
309
+ interpretation = "Distress zone (Z < 1.81): High probability of financial distress";
310
+ }
311
+ return { z, x1, x2, x3, x4, x5, zone, interpretation };
312
+ }
313
+ altmanZScore.formula = "1.2\xD7X1 + 1.4\xD7X2 + 3.3\xD7X3 + 0.6\xD7X4 + 1.0\xD7X5 (public manufacturing)";
314
+ altmanZScore.description = "Bankruptcy prediction model. Safe > 2.99, Distress < 1.81.";
315
+
316
+ // src/utils/compute-all.ts
317
+ function g(data, key) {
318
+ const v = data[key];
319
+ if (v === void 0 || v === null || typeof v === "object") return 0;
320
+ return v || 0;
321
+ }
322
+ function computeAll(data) {
323
+ const result = {};
324
+ const revenue = g(data, "revenue");
325
+ const grossProfit = g(data, "grossProfit");
326
+ const ebit = g(data, "ebit");
327
+ const ebitda = g(data, "ebitda");
328
+ const netIncome = g(data, "netIncome");
329
+ const totalAssets = g(data, "totalAssets");
330
+ const totalEquity = g(data, "totalEquity");
331
+ const totalDebt = g(data, "totalDebt");
332
+ const currentAssets = g(data, "currentAssets");
333
+ const currentLiabilities = g(data, "currentLiabilities");
334
+ const cash = g(data, "cash");
335
+ const accountsReceivable = g(data, "accountsReceivable");
336
+ const inventory = g(data, "inventory");
337
+ const marketCap = g(data, "marketCap");
338
+ const operatingCashFlow = g(data, "operatingCashFlow");
339
+ const capexRaw = g(data, "capex");
340
+ const interestExpense = g(data, "interestExpense");
341
+ const incomeTaxExpense = g(data, "incomeTaxExpense");
342
+ const ebt = g(data, "ebt");
343
+ const cogs = g(data, "cogs");
344
+ const accountsPayable = g(data, "accountsPayable");
345
+ const retainedEarnings = g(data, "retainedEarnings");
346
+ const totalLiabilities = g(data, "totalLiabilities");
347
+ const sharesOutstanding = g(data, "sharesOutstanding");
348
+ const fcf = freeCashFlow({ operatingCashFlow, capex: capexRaw });
349
+ const ev = data.enterpriseValue || marketCap + totalDebt - cash || void 0;
350
+ const taxRate = ebt ? Math.min(Math.max(safeDivide(incomeTaxExpense, ebt) ?? 0.21, 0), 0.5) : 0.21;
351
+ const nopatVal = nopat({ ebit, taxRate });
352
+ const icVal = investedCapital({ totalEquity, totalDebt, cash });
353
+ result.pe = pe({ marketCap, netIncome });
354
+ result.pb = pb({ marketCap, totalEquity });
355
+ result.ps = ps({ marketCap, revenue });
356
+ if (result.pe && data.epsGrowthPct) {
357
+ result.peg = peg({ peRatio: result.pe, epsGrowthRatePercent: data.epsGrowthPct });
358
+ }
359
+ if (operatingCashFlow) result.pFcf = pFcf({ marketCap, operatingCashFlow, capex: capexRaw });
360
+ result.enterpriseValue = ev ?? null;
361
+ if (ev && ebitda) result.evEbitda = evEbitda({ enterpriseValue: ev, ebitda });
362
+ if (ev && ebit) result.evEbit = evEbit({ enterpriseValue: ev, ebit });
363
+ if (ev && revenue) result.evRevenue = evRevenue({ enterpriseValue: ev, revenue });
364
+ if (ev && fcf) result.evFcf = evFcf({ enterpriseValue: ev, freeCashFlow: fcf });
365
+ result.tobinsQ = tobinsQ({ marketCap, totalDebt, totalAssets });
366
+ if (sharesOutstanding) {
367
+ const eps = safeDivide(netIncome, sharesOutstanding);
368
+ const bvps = safeDivide(totalEquity, sharesOutstanding);
369
+ if (eps && bvps) result.grahamNumber = grahamNumber({ eps, bookValuePerShare: bvps });
370
+ }
371
+ result.grossMargin = grossMargin({ grossProfit, revenue });
372
+ result.operatingMargin = operatingMargin({ ebit, revenue });
373
+ result.netMargin = netProfitMargin({ netIncome, revenue });
374
+ if (ebitda) result.ebitdaMargin = ebitdaMargin({ ebitda, revenue });
375
+ result.roe = roe({ netIncome, avgTotalEquity: totalEquity });
376
+ result.roa = roa({ netIncome, avgTotalAssets: totalAssets });
377
+ result.nopat = nopatVal;
378
+ result.investedCapital = icVal;
379
+ if (nopatVal && icVal) result.roic = roic({ nopat: nopatVal, investedCapital: icVal });
380
+ result.roce = roce({ ebit, totalAssets, currentLiabilities });
381
+ result.freeCashFlow = fcf ?? null;
382
+ if (fcf) {
383
+ result.fcfMargin = fcfMargin({ freeCashFlow: fcf, revenue });
384
+ result.fcfConversion = fcfConversion({ freeCashFlow: fcf, netIncome });
385
+ if (marketCap) result.fcfYield = fcfYield({ freeCashFlow: fcf, marketCap });
386
+ }
387
+ result.ocfToSales = ocfToSales({ operatingCashFlow, revenue });
388
+ result.capexToRevenue = capexToRevenue({ capex: capexRaw, revenue });
389
+ result.currentRatio = currentRatio({ currentAssets, currentLiabilities });
390
+ result.quickRatio = quickRatio({ cash, shortTermInvestments: 0, accountsReceivable, currentLiabilities });
391
+ result.dso = dso({ accountsReceivable, revenue });
392
+ if (cogs) {
393
+ result.dio = dio({ inventory, cogs });
394
+ result.dpo = dpo({ accountsPayable, cogs });
395
+ }
396
+ if (result.dso != null) {
397
+ result.cashConversionCycle = cashConversionCycle({
398
+ dso: result.dso,
399
+ dio: result.dio ?? 0,
400
+ dpo: result.dpo ?? 0
401
+ });
402
+ }
403
+ result.debtToEquity = debtToEquity({ totalDebt, totalEquity });
404
+ result.netDebtToEquity = netDebtToEquity({ totalDebt, cash, totalEquity });
405
+ if (ebitda) result.netDebtToEbitda = netDebtToEbitda({ totalDebt, cash, ebitda });
406
+ result.debtToAssets = debtToAssets({ totalDebt, totalAssets });
407
+ result.interestCoverage = interestCoverageRatio({ ebit, interestExpense });
408
+ result.equityMultiplier = equityMultiplier({ totalAssets, totalEquity });
409
+ result.assetTurnover = assetTurnover({ revenue, avgTotalAssets: totalAssets });
410
+ result.receivablesTurnover = receivablesTurnover({ revenue, avgAccountsReceivable: accountsReceivable });
411
+ if (cogs) {
412
+ result.inventoryTurnover = inventoryTurnover({ cogs, avgInventory: inventory });
413
+ result.payablesTurnover = payablesTurnover({ cogs, avgAccountsPayable: accountsPayable });
414
+ }
415
+ if (totalAssets && totalLiabilities) {
416
+ result.altmanZ = altmanZScore({
417
+ workingCapital: currentAssets - currentLiabilities,
418
+ retainedEarnings,
419
+ ebit,
420
+ marketCap,
421
+ totalLiabilities,
422
+ totalAssets,
423
+ revenue
424
+ });
425
+ }
426
+ if (data.prior) {
427
+ const p = data.prior;
428
+ const pGet = (key) => {
429
+ const v = p[key];
430
+ return (typeof v === "number" ? v : 0) || 0;
431
+ };
432
+ try {
433
+ result.piotroski = piotroskiFScore({
434
+ current: {
435
+ netIncome,
436
+ totalAssets,
437
+ operatingCashFlow,
438
+ longTermDebt: g(data, "longTermDebt"),
439
+ currentAssets,
440
+ currentLiabilities,
441
+ sharesOutstanding,
442
+ grossProfit,
443
+ revenue
444
+ },
445
+ prior: {
446
+ netIncome: pGet("netIncome"),
447
+ totalAssets: pGet("totalAssets"),
448
+ longTermDebt: pGet("longTermDebt"),
449
+ currentAssets: pGet("currentAssets"),
450
+ currentLiabilities: pGet("currentLiabilities"),
451
+ sharesOutstanding: pGet("sharesOutstanding"),
452
+ grossProfit: pGet("grossProfit"),
453
+ revenue: pGet("revenue")
454
+ }
455
+ });
456
+ } catch {
457
+ result.piotroski = null;
458
+ }
459
+ }
460
+ return result;
461
+ }
462
+
463
+ // src/utils/scenario-dcf.ts
464
+ var DEFAULT_SCENARIOS = {
465
+ bear: { growthRate: 0.03, wacc: 0.12, terminalGrowth: 0.02, years: 10 },
466
+ base: { growthRate: 0.07, wacc: 0.09, terminalGrowth: 0.02, years: 10 },
467
+ bull: { growthRate: 0.12, wacc: 0.08, terminalGrowth: 0.03, years: 10 }
468
+ };
469
+ function scenarioDcf(input) {
470
+ const {
471
+ baseFcf,
472
+ sharesOutstanding,
473
+ currentPrice,
474
+ scenarios = DEFAULT_SCENARIOS
475
+ } = input;
476
+ const result = {};
477
+ for (const [name, params] of Object.entries(scenarios)) {
478
+ const growthRate = params.growthRate ?? 0.07;
479
+ const wacc = params.wacc ?? 0.09;
480
+ const terminalGrowth = params.terminalGrowth ?? 0.02;
481
+ const years = params.years ?? 10;
482
+ let fcf = baseFcf;
483
+ let pvSum = 0;
484
+ for (let t = 1; t <= years; t++) {
485
+ fcf = fcf * (1 + growthRate);
486
+ pvSum += fcf / Math.pow(1 + wacc, t);
487
+ }
488
+ let terminalValue = 0;
489
+ if (wacc > terminalGrowth) {
490
+ const terminalFcf = fcf * (1 + terminalGrowth);
491
+ terminalValue = terminalFcf / (wacc - terminalGrowth);
492
+ }
493
+ const pvTerminal = terminalValue / Math.pow(1 + wacc, years);
494
+ const intrinsicValue = pvSum + pvTerminal;
495
+ const intrinsicValuePerShare = safeDivide(intrinsicValue, sharesOutstanding) ?? null;
496
+ let upsidePct = null;
497
+ if (currentPrice && currentPrice > 0 && intrinsicValuePerShare !== null) {
498
+ upsidePct = (intrinsicValuePerShare - currentPrice) / currentPrice;
499
+ }
500
+ result[name] = {
501
+ intrinsicValue,
502
+ intrinsicValuePerShare,
503
+ pvFcfs: pvSum,
504
+ pvTerminal,
505
+ terminalValue,
506
+ upsidePct,
507
+ params: { growthRate, wacc, terminalGrowth, years }
508
+ };
509
+ }
510
+ return result;
511
+ }
512
+
513
+ // src/hooks/index.ts
514
+ function useRatios(ticker, fetcher) {
515
+ const [state, setState] = react.useState({
516
+ data: null,
517
+ loading: false,
518
+ error: null,
519
+ refetch: () => {
520
+ }
521
+ });
522
+ const fetcherRef = react.useRef(fetcher);
523
+ fetcherRef.current = fetcher;
524
+ const fetch = react.useCallback(() => {
525
+ if (!ticker) return;
526
+ setState((s) => ({ ...s, loading: true, error: null }));
527
+ fetcherRef.current(ticker).then((raw) => {
528
+ const ratios = computeAll(raw);
529
+ setState({ data: ratios, loading: false, error: null, refetch: fetch });
530
+ }).catch((err) => {
531
+ const message = err instanceof Error ? err.message : String(err);
532
+ setState((s) => ({ ...s, loading: false, error: message }));
533
+ });
534
+ }, [ticker]);
535
+ react.useEffect(() => {
536
+ fetch();
537
+ }, [fetch]);
538
+ return { ...state, refetch: fetch };
539
+ }
540
+ function useRatio(ticker, ratio, fetcher) {
541
+ const ratiosState = useRatios(ticker, fetcher);
542
+ return {
543
+ data: ratiosState.data ? ratiosState.data[ratio] ?? null : null,
544
+ loading: ratiosState.loading,
545
+ error: ratiosState.error,
546
+ refetch: ratiosState.refetch
547
+ };
548
+ }
549
+ function useHealthScore(ticker, fetcher, scoreFn) {
550
+ const ratiosState = useRatios(ticker, fetcher);
551
+ const score = ratiosState.data ? scoreFn(ratiosState.data) : null;
552
+ return {
553
+ data: score,
554
+ loading: ratiosState.loading,
555
+ error: ratiosState.error,
556
+ refetch: ratiosState.refetch
557
+ };
558
+ }
559
+ function useScenarioDcf(input) {
560
+ const [data, setData] = react.useState(null);
561
+ react.useEffect(() => {
562
+ if (!input) {
563
+ setData(null);
564
+ return;
565
+ }
566
+ setData(scenarioDcf(input));
567
+ }, [
568
+ input?.baseFcf,
569
+ input?.sharesOutstanding,
570
+ input?.currentPrice
571
+ ]);
572
+ return { data };
573
+ }
574
+ function useCompareRatios(tickers, fetcher) {
575
+ const [state, setState] = react.useState({
576
+ data: null,
577
+ loading: false,
578
+ error: null,
579
+ refetch: () => {
580
+ }
581
+ });
582
+ const fetcherRef = react.useRef(fetcher);
583
+ fetcherRef.current = fetcher;
584
+ const tickersKey = tickers.join(",");
585
+ const fetch = react.useCallback(() => {
586
+ if (!tickers.length) return;
587
+ setState((s) => ({ ...s, loading: true, error: null }));
588
+ Promise.allSettled(tickers.map((t) => fetcherRef.current(t).then((raw) => [t, computeAll(raw)]))).then((results) => {
589
+ const data = {};
590
+ for (const result of results) {
591
+ if (result.status === "fulfilled") {
592
+ const [ticker, ratios] = result.value;
593
+ data[ticker] = ratios;
594
+ }
595
+ }
596
+ setState({ data, loading: false, error: null, refetch: fetch });
597
+ }).catch((err) => {
598
+ const message = err instanceof Error ? err.message : String(err);
599
+ setState((s) => ({ ...s, loading: false, error: message }));
600
+ });
601
+ }, [tickersKey]);
602
+ react.useEffect(() => {
603
+ fetch();
604
+ }, [fetch]);
605
+ return { ...state, refetch: fetch };
606
+ }
607
+
608
+ exports.useCompareRatios = useCompareRatios;
609
+ exports.useHealthScore = useHealthScore;
610
+ exports.useRatio = useRatio;
611
+ exports.useRatios = useRatios;
612
+ exports.useScenarioDcf = useScenarioDcf;
613
+ //# sourceMappingURL=index.cjs.map
614
+ //# sourceMappingURL=index.cjs.map