fin-ratios 0.7.3 → 1.0.2
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/LICENSE +21 -0
- package/README.md +779 -0
- package/dist/fetchers/alphavantage/index.cjs +0 -2
- package/dist/fetchers/alphavantage/index.js +0 -2
- package/dist/fetchers/edgar/index.cjs +0 -2
- package/dist/fetchers/edgar/index.js +0 -2
- package/dist/fetchers/fmp/index.cjs +0 -2
- package/dist/fetchers/fmp/index.js +0 -2
- package/dist/fetchers/polygon/index.cjs +174 -0
- package/dist/fetchers/polygon/index.d.cts +84 -0
- package/dist/fetchers/polygon/index.d.ts +84 -0
- package/dist/fetchers/polygon/index.js +171 -0
- package/dist/fetchers/yahoo/index.cjs +0 -2
- package/dist/fetchers/yahoo/index.js +0 -2
- package/dist/hooks/index.cjs +0 -2
- package/dist/hooks/index.js +0 -2
- package/dist/index.cjs +557 -8
- package/dist/index.d.cts +289 -1
- package/dist/index.d.ts +289 -1
- package/dist/index.js +553 -9
- package/package.json +11 -3
- package/dist/fetchers/alphavantage/index.cjs.map +0 -1
- package/dist/fetchers/alphavantage/index.js.map +0 -1
- package/dist/fetchers/edgar/index.cjs.map +0 -1
- package/dist/fetchers/edgar/index.js.map +0 -1
- package/dist/fetchers/fmp/index.cjs.map +0 -1
- package/dist/fetchers/fmp/index.js.map +0 -1
- package/dist/fetchers/yahoo/index.cjs.map +0 -1
- package/dist/fetchers/yahoo/index.js.map +0 -1
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -117,10 +117,10 @@ gordonGrowthModel.formula = "D1 / (r - g)";
|
|
|
117
117
|
gordonGrowthModel.description = "DDM for stable dividend-paying stocks. Only valid when r > g.";
|
|
118
118
|
function reverseDcf(input) {
|
|
119
119
|
const target = input.marketCap + (input.netDebt ?? 0);
|
|
120
|
-
const computeEV = (
|
|
120
|
+
const computeEV = (g7) => {
|
|
121
121
|
const result = dcf2Stage({
|
|
122
122
|
baseFcf: input.baseFcf,
|
|
123
|
-
growthRate:
|
|
123
|
+
growthRate: g7,
|
|
124
124
|
years: input.years,
|
|
125
125
|
terminalGrowthRate: input.terminalGrowthRate,
|
|
126
126
|
wacc: input.wacc
|
|
@@ -450,8 +450,8 @@ function annualizeReturn(returnValue, periodsPerYear) {
|
|
|
450
450
|
}
|
|
451
451
|
function stdDev(values, ddof = 1) {
|
|
452
452
|
if (values.length < 2) return null;
|
|
453
|
-
const
|
|
454
|
-
const variance = values.reduce((sum, v) => sum + Math.pow(v -
|
|
453
|
+
const mean7 = values.reduce((a, b) => a + b, 0) / values.length;
|
|
454
|
+
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean7, 2), 0) / (values.length - ddof);
|
|
455
455
|
return Math.sqrt(variance);
|
|
456
456
|
}
|
|
457
457
|
function mean(values) {
|
|
@@ -1945,8 +1945,8 @@ function fairValueRange(options) {
|
|
|
1945
1945
|
if (fy && fy > 0) estimates[`FCF Yield (${(targetYield * 100).toFixed(0)}%)`] = fy;
|
|
1946
1946
|
}
|
|
1947
1947
|
if (eps && bvps) {
|
|
1948
|
-
const
|
|
1949
|
-
if (
|
|
1948
|
+
const g7 = grahamValue(eps, bvps);
|
|
1949
|
+
if (g7 && g7 > 0) estimates["Graham Number"] = g7;
|
|
1950
1950
|
}
|
|
1951
1951
|
if (ebitda && ebitda > 0 && shares && shares > 0) {
|
|
1952
1952
|
const v = evEbitdaValue(ebitda, totalDebt, cash, shares, evEbitdaMultiple);
|
|
@@ -2057,6 +2057,550 @@ function portfolioQuality(holdings, options = {}) {
|
|
|
2057
2057
|
};
|
|
2058
2058
|
}
|
|
2059
2059
|
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2060
|
+
// src/utils/valuation-score.ts
|
|
2061
|
+
function _clamp(x, lo, hi) {
|
|
2062
|
+
return Math.max(lo, Math.min(hi, x));
|
|
2063
|
+
}
|
|
2064
|
+
function _lerp(x, x0, x1, y0, y1) {
|
|
2065
|
+
if (x1 === x0) return y0;
|
|
2066
|
+
const t = (x - x0) / (x1 - x0);
|
|
2067
|
+
return y0 + _clamp(t, 0, 1) * (y1 - y0);
|
|
2068
|
+
}
|
|
2069
|
+
function scoreEarningsYield(params, rf) {
|
|
2070
|
+
let ey = null;
|
|
2071
|
+
if (params.earningsYieldPct !== void 0) {
|
|
2072
|
+
ey = params.earningsYieldPct / 100;
|
|
2073
|
+
} else if (params.peRatio !== void 0 && params.peRatio > 0) {
|
|
2074
|
+
ey = 1 / params.peRatio;
|
|
2075
|
+
}
|
|
2076
|
+
if (ey === null) {
|
|
2077
|
+
return [0.5, ["Earnings yield: no P/E or earnings yield data (neutral score)"]];
|
|
2078
|
+
}
|
|
2079
|
+
const excess = ey - rf;
|
|
2080
|
+
const score = _lerp(excess, -0.04, 0.04, 0, 1);
|
|
2081
|
+
const label = excess >= 0.02 ? "attractive" : excess >= 0 ? "slight premium" : "below risk-free";
|
|
2082
|
+
return [score, [
|
|
2083
|
+
`Earnings yield: ${(ey * 100).toFixed(2)}% vs risk-free ${(rf * 100).toFixed(2)}% (spread ${excess >= 0 ? "+" : ""}${(excess * 100).toFixed(2)}%)`,
|
|
2084
|
+
`Earnings yield spread: ${label}`
|
|
2085
|
+
]];
|
|
2086
|
+
}
|
|
2087
|
+
function scoreFcfYield(params) {
|
|
2088
|
+
let fy = null;
|
|
2089
|
+
if (params.fcfYieldPct !== void 0) {
|
|
2090
|
+
fy = params.fcfYieldPct / 100;
|
|
2091
|
+
} else if (params.pFcf !== void 0 && params.pFcf > 0) {
|
|
2092
|
+
fy = 1 / params.pFcf;
|
|
2093
|
+
}
|
|
2094
|
+
if (fy === null) {
|
|
2095
|
+
return [0.5, ["FCF yield: no P/FCF or FCF yield data (neutral score)"]];
|
|
2096
|
+
}
|
|
2097
|
+
let score;
|
|
2098
|
+
if (fy < 0.02) {
|
|
2099
|
+
score = _lerp(fy, -0.02, 0.02, 0, 0.3);
|
|
2100
|
+
} else if (fy < 0.05) {
|
|
2101
|
+
score = _lerp(fy, 0.02, 0.05, 0.3, 0.7);
|
|
2102
|
+
} else {
|
|
2103
|
+
score = _lerp(fy, 0.05, 0.08, 0.7, 1);
|
|
2104
|
+
}
|
|
2105
|
+
const label = fy >= 0.06 ? "excellent" : fy >= 0.04 ? "good" : fy >= 0.02 ? "modest" : "thin";
|
|
2106
|
+
return [score, [
|
|
2107
|
+
`FCF yield: ${(fy * 100).toFixed(2)}% (${label})`
|
|
2108
|
+
]];
|
|
2109
|
+
}
|
|
2110
|
+
function scoreEvEbitda(params) {
|
|
2111
|
+
if (params.evEbitda === void 0) {
|
|
2112
|
+
return [0.5, ["EV/EBITDA: not provided (neutral score)"]];
|
|
2113
|
+
}
|
|
2114
|
+
const ev = params.evEbitda;
|
|
2115
|
+
if (ev <= 0) {
|
|
2116
|
+
return [0.2, ["EV/EBITDA: negative or zero (uninvestable or distressed)"]];
|
|
2117
|
+
}
|
|
2118
|
+
let score;
|
|
2119
|
+
if (ev < 12) {
|
|
2120
|
+
score = _lerp(ev, 5, 12, 1, 0.6);
|
|
2121
|
+
} else if (ev < 20) {
|
|
2122
|
+
score = _lerp(ev, 12, 20, 0.6, 0.2);
|
|
2123
|
+
} else {
|
|
2124
|
+
score = _lerp(ev, 20, 30, 0.2, 0.05);
|
|
2125
|
+
}
|
|
2126
|
+
const label = ev < 10 ? "deep value" : ev < 14 ? "reasonable" : ev < 20 ? "full valued" : "expensive";
|
|
2127
|
+
return [score, [
|
|
2128
|
+
`EV/EBITDA: ${ev.toFixed(1)}\xD7 (${label})`
|
|
2129
|
+
]];
|
|
2130
|
+
}
|
|
2131
|
+
function scorePbRatio(params) {
|
|
2132
|
+
if (params.pbRatio === void 0) {
|
|
2133
|
+
return [0.5, ["P/B ratio: not provided (neutral score)"]];
|
|
2134
|
+
}
|
|
2135
|
+
const pb2 = params.pbRatio;
|
|
2136
|
+
if (pb2 <= 0) {
|
|
2137
|
+
return [0.15, ["P/B ratio: negative book value \u2014 balance sheet impaired"]];
|
|
2138
|
+
}
|
|
2139
|
+
let score;
|
|
2140
|
+
if (pb2 < 2) {
|
|
2141
|
+
score = _lerp(pb2, 0.5, 2, 1, 0.65);
|
|
2142
|
+
} else if (pb2 < 4) {
|
|
2143
|
+
score = _lerp(pb2, 2, 4, 0.65, 0.3);
|
|
2144
|
+
} else {
|
|
2145
|
+
score = _lerp(pb2, 4, 8, 0.3, 0.05);
|
|
2146
|
+
}
|
|
2147
|
+
const label = pb2 < 1.5 ? "near book value" : pb2 < 3 ? "moderate premium" : pb2 < 5 ? "high premium" : "very high premium";
|
|
2148
|
+
return [score, [
|
|
2149
|
+
`P/B ratio: ${pb2.toFixed(2)}\xD7 (${label})`
|
|
2150
|
+
]];
|
|
2151
|
+
}
|
|
2152
|
+
function scoreDcfUpside(params) {
|
|
2153
|
+
if (params.dcfUpsidePct === void 0) {
|
|
2154
|
+
return [0.5, ["DCF upside: not provided (neutral score)"]];
|
|
2155
|
+
}
|
|
2156
|
+
const u = params.dcfUpsidePct / 100;
|
|
2157
|
+
let score;
|
|
2158
|
+
if (u < 0) {
|
|
2159
|
+
score = _lerp(u, -0.5, 0, 0, 0.3);
|
|
2160
|
+
} else if (u < 0.3) {
|
|
2161
|
+
score = _lerp(u, 0, 0.3, 0.3, 0.75);
|
|
2162
|
+
} else {
|
|
2163
|
+
score = _lerp(u, 0.3, 0.6, 0.75, 1);
|
|
2164
|
+
}
|
|
2165
|
+
const label = u >= 0.3 ? "significant margin of safety" : u >= 0.1 ? "modest upside" : u >= 0 ? "fairly valued" : "trading above intrinsic value";
|
|
2166
|
+
return [score, [
|
|
2167
|
+
`DCF upside: ${(u * 100).toFixed(1)}% (${label})`
|
|
2168
|
+
]];
|
|
2169
|
+
}
|
|
2170
|
+
function valuationAttractivenessScore(params) {
|
|
2171
|
+
const rf = params.riskFreeRate ?? 0.045;
|
|
2172
|
+
const [eyScore, eyEv] = scoreEarningsYield(params, rf);
|
|
2173
|
+
const [fyScore, fyEv] = scoreFcfYield(params);
|
|
2174
|
+
const [evScore, evEv] = scoreEvEbitda(params);
|
|
2175
|
+
const [pbScore, pbEv] = scorePbRatio(params);
|
|
2176
|
+
const [dcfScore, dcfEv] = scoreDcfUpside(params);
|
|
2177
|
+
const raw = 0.25 * eyScore + 0.25 * fyScore + 0.2 * evScore + 0.15 * pbScore + 0.15 * dcfScore;
|
|
2178
|
+
const score = Math.round(_clamp(raw, 0, 1) * 100);
|
|
2179
|
+
const rating = score >= 65 ? "attractive" : score >= 40 ? "fair" : score >= 20 ? "expensive" : "overvalued";
|
|
2180
|
+
const desc = {
|
|
2181
|
+
attractive: "Multiple signals indicate price is below intrinsic value \u2014 margin of safety present",
|
|
2182
|
+
fair: "Price appears broadly in line with fundamental value",
|
|
2183
|
+
expensive: "Price embeds optimistic assumptions \u2014 limited margin of safety",
|
|
2184
|
+
overvalued: "Price materially exceeds indicated intrinsic value across multiple metrics"
|
|
2185
|
+
};
|
|
2186
|
+
return {
|
|
2187
|
+
score,
|
|
2188
|
+
rating,
|
|
2189
|
+
components: {
|
|
2190
|
+
earningsYield: Math.round(eyScore * 1e4) / 1e4,
|
|
2191
|
+
fcfYield: Math.round(fyScore * 1e4) / 1e4,
|
|
2192
|
+
evEbitda: Math.round(evScore * 1e4) / 1e4,
|
|
2193
|
+
pbRatio: Math.round(pbScore * 1e4) / 1e4,
|
|
2194
|
+
dcfUpside: Math.round(dcfScore * 1e4) / 1e4
|
|
2195
|
+
},
|
|
2196
|
+
riskFreeRate: rf,
|
|
2197
|
+
evidence: [...eyEv, ...fyEv, ...evEv, ...pbEv, ...dcfEv],
|
|
2198
|
+
interpretation: `Score ${score}/100: ${desc[rating]}`
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// src/utils/management-score.ts
|
|
2203
|
+
function mean5(xs) {
|
|
2204
|
+
return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
2205
|
+
}
|
|
2206
|
+
function std4(xs) {
|
|
2207
|
+
if (xs.length < 2) return 0;
|
|
2208
|
+
const m = mean5(xs);
|
|
2209
|
+
return Math.sqrt(xs.reduce((a, x) => a + (x - m) ** 2, 0) / (xs.length - 1));
|
|
2210
|
+
}
|
|
2211
|
+
function cv4(xs) {
|
|
2212
|
+
const m = mean5(xs);
|
|
2213
|
+
return Math.abs(m) > 1e-9 ? std4(xs) / Math.abs(m) : 1;
|
|
2214
|
+
}
|
|
2215
|
+
function olsSlope4(ys) {
|
|
2216
|
+
const n = ys.length;
|
|
2217
|
+
if (n < 2) return 0;
|
|
2218
|
+
const xm = (n - 1) / 2;
|
|
2219
|
+
const ym = mean5(ys);
|
|
2220
|
+
const ssXX = Array.from({ length: n }, (_, i) => (i - xm) ** 2).reduce((a, b) => a + b, 0);
|
|
2221
|
+
const ssXY = Array.from({ length: n }, (_, i) => (i - xm) * ((ys[i] ?? 0) - ym)).reduce((a, b) => a + b, 0);
|
|
2222
|
+
return ssXX ? ssXY / ssXX : 0;
|
|
2223
|
+
}
|
|
2224
|
+
function clamp3(x, lo, hi) {
|
|
2225
|
+
return Math.max(lo, Math.min(hi, x));
|
|
2226
|
+
}
|
|
2227
|
+
function lerp(x, x0, x1, y0, y1) {
|
|
2228
|
+
if (x1 === x0) return y0;
|
|
2229
|
+
return y0 + clamp3((x - x0) / (x1 - x0), 0, 1) * (y1 - y0);
|
|
2230
|
+
}
|
|
2231
|
+
function g5(d, k) {
|
|
2232
|
+
const v = d[k];
|
|
2233
|
+
return typeof v === "number" && isFinite(v) ? v : 0;
|
|
2234
|
+
}
|
|
2235
|
+
function yearRoic3(d) {
|
|
2236
|
+
const ebit = g5(d, "ebit");
|
|
2237
|
+
const ic = g5(d, "totalEquity") + g5(d, "totalDebt") - g5(d, "cash");
|
|
2238
|
+
if (ic <= 0) return null;
|
|
2239
|
+
const ebt = g5(d, "ebt") || ebit - g5(d, "interestExpense");
|
|
2240
|
+
const tax = g5(d, "incomeTaxExpense");
|
|
2241
|
+
const taxRate = ebt > 0 && tax > 0 ? clamp3(tax / ebt, 0, 0.5) : 0.21;
|
|
2242
|
+
return ebit * (1 - taxRate) / ic;
|
|
2243
|
+
}
|
|
2244
|
+
function estimateHurdleRate(series, provided) {
|
|
2245
|
+
if (provided !== void 0) return provided;
|
|
2246
|
+
const d = series[series.length - 1];
|
|
2247
|
+
const equity = g5(d, "totalEquity");
|
|
2248
|
+
const debt = g5(d, "totalDebt");
|
|
2249
|
+
const cash = g5(d, "cash");
|
|
2250
|
+
const totalCap = equity + debt - cash;
|
|
2251
|
+
if (totalCap <= 0) return 0.1;
|
|
2252
|
+
const costEquity = 0.045 + 0.055;
|
|
2253
|
+
const interest = g5(d, "interestExpense");
|
|
2254
|
+
let costDebt = 0.04;
|
|
2255
|
+
if (debt > 0 && interest > 0) {
|
|
2256
|
+
const preTax = clamp3(interest / debt, 0.02, 0.15);
|
|
2257
|
+
const ebt = g5(d, "ebt") || g5(d, "ebit") - interest;
|
|
2258
|
+
const tax = g5(d, "incomeTaxExpense");
|
|
2259
|
+
const taxRate = ebt > 0 && tax > 0 ? clamp3(tax / ebt, 0, 0.4) : 0.21;
|
|
2260
|
+
costDebt = preTax * (1 - taxRate);
|
|
2261
|
+
}
|
|
2262
|
+
const wE = clamp3(equity / totalCap, 0, 1);
|
|
2263
|
+
return clamp3(wE * costEquity + (1 - wE) * costDebt, 0.06, 0.2);
|
|
2264
|
+
}
|
|
2265
|
+
function scoreRoicExcellence(series, hurdle) {
|
|
2266
|
+
const roicVals = series.map(yearRoic3).filter((r) => r !== null);
|
|
2267
|
+
if (!roicVals.length) return [0.35, ["ROIC excellence: insufficient data (neutral score)"]];
|
|
2268
|
+
const meanRoic = mean5(roicVals);
|
|
2269
|
+
const slope = olsSlope4(roicVals);
|
|
2270
|
+
const consistency = clamp3(1 - cv4(roicVals) * 0.5, 0, 1);
|
|
2271
|
+
const level = lerp(meanRoic, 0.05, 0.3, 0, 1);
|
|
2272
|
+
const trendAdj = slope > 0.01 ? 0.08 : slope < -0.01 ? -0.08 : 0;
|
|
2273
|
+
const score = clamp3(0.55 * level + 0.35 * consistency + trendAdj, 0, 1);
|
|
2274
|
+
const aboveHurdle = roicVals.filter((r) => r > hurdle).length;
|
|
2275
|
+
const dir = slope > 0.01 ? "improving" : slope < -0.01 ? "declining" : "stable";
|
|
2276
|
+
return [score, [
|
|
2277
|
+
`ROIC excellence: mean ${(meanRoic * 100).toFixed(1)}% (hurdle ${(hurdle * 100).toFixed(1)}%, ${aboveHurdle}/${roicVals.length} years above)`,
|
|
2278
|
+
`ROIC trend: ${dir} (OLS ${(slope * 100).toFixed(2)}%/yr)`
|
|
2279
|
+
]];
|
|
2280
|
+
}
|
|
2281
|
+
function scoreMarginStability(series) {
|
|
2282
|
+
const margins = series.filter((d) => g5(d, "revenue") > 0 && g5(d, "ebit") !== 0).map((d) => g5(d, "ebit") / g5(d, "revenue"));
|
|
2283
|
+
if (margins.length < 2) return [0.4, ["Margin stability: insufficient revenue/EBIT data (neutral score)"]];
|
|
2284
|
+
const meanMargin = mean5(margins);
|
|
2285
|
+
const slope = olsSlope4(margins);
|
|
2286
|
+
const consistency = clamp3(1 - cv4(margins) * 1, 0, 1);
|
|
2287
|
+
const level = lerp(meanMargin, 0, 0.3, 0, 1);
|
|
2288
|
+
const trendAdj = slope > 5e-3 ? 0.08 : slope < -5e-3 ? -0.08 : 0;
|
|
2289
|
+
const score = clamp3(0.5 * level + 0.4 * consistency + trendAdj, 0, 1);
|
|
2290
|
+
const dir = slope > 5e-3 ? "expanding" : slope < -5e-3 ? "contracting" : "stable";
|
|
2291
|
+
const quality = meanMargin >= 0.2 ? "high" : meanMargin >= 0.1 ? "moderate" : "thin";
|
|
2292
|
+
return [score, [
|
|
2293
|
+
`Operating margin: mean ${(meanMargin * 100).toFixed(1)}% (${quality} quality, CV ${cv4(margins).toFixed(3)})`,
|
|
2294
|
+
`Margin trend: ${dir} (OLS ${(slope * 100).toFixed(3)}%/yr)`
|
|
2295
|
+
]];
|
|
2296
|
+
}
|
|
2297
|
+
function scoreShareholderOrientation(series) {
|
|
2298
|
+
const shares = series.map((d) => g5(d, "sharesOutstanding")).filter((s) => s > 0);
|
|
2299
|
+
if (shares.length < 2) return [0.5, ["Shareholder orientation: share count data unavailable (neutral score)"]];
|
|
2300
|
+
const meanShares = mean5(shares);
|
|
2301
|
+
const slope = olsSlope4(shares);
|
|
2302
|
+
const slopePct = meanShares > 0 ? slope / meanShares : 0;
|
|
2303
|
+
const score = clamp3(0.5 - slopePct * 5, 0, 1);
|
|
2304
|
+
const pctChange = (shares[shares.length - 1] - shares[0]) / shares[0];
|
|
2305
|
+
const dir = slopePct < -0.01 ? "declining (buybacks)" : slopePct > 0.01 ? "growing (dilution)" : "roughly flat";
|
|
2306
|
+
return [score, [
|
|
2307
|
+
`Share count: ${dir} (${pctChange >= 0 ? "+" : ""}${(pctChange * 100).toFixed(1)}% over period)`,
|
|
2308
|
+
slopePct < -5e-3 ? "Management is reducing share count \u2014 shareholder friendly" : slopePct > 0.02 ? "Material share dilution detected \u2014 value transfer risk" : "Share count broadly stable"
|
|
2309
|
+
]];
|
|
2310
|
+
}
|
|
2311
|
+
function scoreRevenueExecution(series) {
|
|
2312
|
+
const growthRates = [];
|
|
2313
|
+
for (let i = 1; i < series.length; i++) {
|
|
2314
|
+
const prevRev = g5(series[i - 1], "revenue");
|
|
2315
|
+
const currRev = g5(series[i], "revenue");
|
|
2316
|
+
if (prevRev > 0) growthRates.push((currRev - prevRev) / prevRev);
|
|
2317
|
+
}
|
|
2318
|
+
if (!growthRates.length) return [0.4, ["Revenue execution: insufficient revenue data (neutral score)"]];
|
|
2319
|
+
const meanGrowth = mean5(growthRates);
|
|
2320
|
+
const level = lerp(meanGrowth, -0.05, 0.2, 0, 1);
|
|
2321
|
+
const consistency = clamp3(1 - cv4(growthRates) * 0.4, 0, 1);
|
|
2322
|
+
const score = clamp3(0.65 * level + 0.35 * consistency, 0, 1);
|
|
2323
|
+
const posYears = growthRates.filter((r) => r > 0).length;
|
|
2324
|
+
const quality = meanGrowth >= 0.1 ? "strong" : meanGrowth >= 0.04 ? "adequate" : meanGrowth >= 0 ? "sluggish" : "declining";
|
|
2325
|
+
return [score, [
|
|
2326
|
+
`Revenue growth: mean ${(meanGrowth * 100).toFixed(1)}%/yr (${quality}, ${posYears}/${growthRates.length} years positive)`
|
|
2327
|
+
]];
|
|
2328
|
+
}
|
|
2329
|
+
function managementQualityScoreFromSeries(annualData, hurdleRate) {
|
|
2330
|
+
if (annualData.length < 3) {
|
|
2331
|
+
throw new Error("managementQualityScoreFromSeries requires at least 3 years of data.");
|
|
2332
|
+
}
|
|
2333
|
+
const hurdle = estimateHurdleRate(annualData, hurdleRate);
|
|
2334
|
+
const [roicScore, roicEv] = scoreRoicExcellence(annualData, hurdle);
|
|
2335
|
+
const [marginScore, marginEv] = scoreMarginStability(annualData);
|
|
2336
|
+
const [soScore, soEv] = scoreShareholderOrientation(annualData);
|
|
2337
|
+
const [revScore, revEv] = scoreRevenueExecution(annualData);
|
|
2338
|
+
const raw = 0.35 * roicScore + 0.25 * marginScore + 0.25 * soScore + 0.15 * revScore;
|
|
2339
|
+
const score = Math.round(clamp3(raw, 0, 1) * 100);
|
|
2340
|
+
const rating = score >= 75 ? "excellent" : score >= 50 ? "good" : score >= 25 ? "fair" : "poor";
|
|
2341
|
+
const desc = {
|
|
2342
|
+
excellent: "Management consistently earns high ROIC, protects margins, and treats shareholders well",
|
|
2343
|
+
good: "Solid operational track record with most quality signals positive",
|
|
2344
|
+
fair: "Mixed signals \u2014 some areas of strength but meaningful shortfalls elsewhere",
|
|
2345
|
+
poor: "Weak returns, margin deterioration, or material shareholder dilution detected"
|
|
2346
|
+
};
|
|
2347
|
+
return {
|
|
2348
|
+
score,
|
|
2349
|
+
rating,
|
|
2350
|
+
components: {
|
|
2351
|
+
roicExcellence: Math.round(roicScore * 1e4) / 1e4,
|
|
2352
|
+
marginStability: Math.round(marginScore * 1e4) / 1e4,
|
|
2353
|
+
shareholderOrientation: Math.round(soScore * 1e4) / 1e4,
|
|
2354
|
+
revenueExecution: Math.round(revScore * 1e4) / 1e4
|
|
2355
|
+
},
|
|
2356
|
+
hurdleRateUsed: hurdle,
|
|
2357
|
+
yearsAnalyzed: annualData.length,
|
|
2358
|
+
evidence: [...roicEv, ...marginEv, ...soEv, ...revEv],
|
|
2359
|
+
interpretation: `Score ${score}/100: ${desc[rating]}`
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// src/utils/dividend-score.ts
|
|
2364
|
+
function mean6(xs) {
|
|
2365
|
+
return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
2366
|
+
}
|
|
2367
|
+
function clamp4(x, lo, hi) {
|
|
2368
|
+
return Math.max(lo, Math.min(hi, x));
|
|
2369
|
+
}
|
|
2370
|
+
function g6(d, k) {
|
|
2371
|
+
const v = d[k];
|
|
2372
|
+
return typeof v === "number" && isFinite(v) ? v : 0;
|
|
2373
|
+
}
|
|
2374
|
+
function isDividendPayer(series) {
|
|
2375
|
+
return series.some((d) => {
|
|
2376
|
+
const div = d.dividendsPaid;
|
|
2377
|
+
return typeof div === "number" && isFinite(div) && div > 0;
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
function scoreFcfPayout(series) {
|
|
2381
|
+
const ratios = [];
|
|
2382
|
+
for (const d of series) {
|
|
2383
|
+
const div = g6(d, "dividendsPaid");
|
|
2384
|
+
if (div <= 0) continue;
|
|
2385
|
+
const cfo = g6(d, "operatingCashFlow");
|
|
2386
|
+
const capex = g6(d, "capex");
|
|
2387
|
+
const fcf = cfo - capex;
|
|
2388
|
+
if (fcf !== 0) ratios.push(div / fcf);
|
|
2389
|
+
}
|
|
2390
|
+
if (!ratios.length) return [0.5, ["FCF payout ratio: no dividend/FCF overlap (neutral score)"]];
|
|
2391
|
+
const avg = mean6(ratios);
|
|
2392
|
+
let score;
|
|
2393
|
+
if (avg < 0.4) score = 1;
|
|
2394
|
+
else if (avg < 0.6) score = 0.75;
|
|
2395
|
+
else if (avg < 0.8) score = 0.45;
|
|
2396
|
+
else if (avg <= 1) score = 0.15;
|
|
2397
|
+
else score = 0;
|
|
2398
|
+
const quality = avg < 0.4 ? "very well covered" : avg < 0.6 ? "adequately covered" : avg < 0.8 ? "tight" : avg <= 1 ? "stressed" : "uncovered by FCF";
|
|
2399
|
+
return [score, [
|
|
2400
|
+
`FCF payout ratio: mean ${(avg * 100).toFixed(0)}% (${quality})`
|
|
2401
|
+
]];
|
|
2402
|
+
}
|
|
2403
|
+
function scoreEarningsPayout(series) {
|
|
2404
|
+
const ratios = [];
|
|
2405
|
+
for (const d of series) {
|
|
2406
|
+
const div = g6(d, "dividendsPaid");
|
|
2407
|
+
const ni = g6(d, "netIncome");
|
|
2408
|
+
if (div > 0 && ni > 0) ratios.push(div / ni);
|
|
2409
|
+
}
|
|
2410
|
+
if (!ratios.length) return [0.5, ["Earnings payout ratio: no dividend/earnings overlap (neutral score)"]];
|
|
2411
|
+
const avg = mean6(ratios);
|
|
2412
|
+
let score;
|
|
2413
|
+
if (avg < 0.35) score = 1;
|
|
2414
|
+
else if (avg < 0.55) score = 0.75;
|
|
2415
|
+
else if (avg < 0.75) score = 0.45;
|
|
2416
|
+
else if (avg <= 1) score = 0.2;
|
|
2417
|
+
else score = 0;
|
|
2418
|
+
const quality = avg < 0.35 ? "conservative" : avg < 0.55 ? "moderate" : avg < 0.75 ? "elevated" : avg <= 1 ? "high" : "exceeds earnings";
|
|
2419
|
+
return [score, [
|
|
2420
|
+
`Earnings payout ratio: mean ${(avg * 100).toFixed(0)}% (${quality})`
|
|
2421
|
+
]];
|
|
2422
|
+
}
|
|
2423
|
+
function scoreBalanceSheet(series) {
|
|
2424
|
+
const d = series[series.length - 1];
|
|
2425
|
+
const totalDebt = g6(d, "totalDebt");
|
|
2426
|
+
const cash = g6(d, "cash");
|
|
2427
|
+
const netDebt = totalDebt - cash;
|
|
2428
|
+
const ebit = g6(d, "ebit");
|
|
2429
|
+
const dep = g6(d, "depreciation");
|
|
2430
|
+
const ebitda = ebit + dep;
|
|
2431
|
+
if (ebitda <= 0) {
|
|
2432
|
+
return [0.3, ["Balance sheet strength: EBITDA unavailable or negative (below-neutral score)"]];
|
|
2433
|
+
}
|
|
2434
|
+
const ratio = netDebt / ebitda;
|
|
2435
|
+
let score;
|
|
2436
|
+
if (ratio < 0) score = 1;
|
|
2437
|
+
else if (ratio < 1) score = 0.85;
|
|
2438
|
+
else if (ratio < 2) score = 0.65;
|
|
2439
|
+
else if (ratio < 3) score = 0.4;
|
|
2440
|
+
else if (ratio < 4) score = 0.2;
|
|
2441
|
+
else score = 0.05;
|
|
2442
|
+
const quality = ratio < 0 ? "net cash position" : ratio < 1 ? "very low leverage" : ratio < 2 ? "modest leverage" : ratio < 3 ? "moderate leverage" : ratio < 4 ? "elevated leverage" : "highly levered";
|
|
2443
|
+
return [score, [
|
|
2444
|
+
`Net debt / EBITDA: ${ratio.toFixed(2)}\xD7 (${quality})`
|
|
2445
|
+
]];
|
|
2446
|
+
}
|
|
2447
|
+
function scoreDividendGrowthTrack(series) {
|
|
2448
|
+
const divs = series.map((d) => g6(d, "dividendsPaid")).filter((d) => d > 0);
|
|
2449
|
+
if (divs.length < 2) {
|
|
2450
|
+
return [0.15, ["Dividend growth track: insufficient history (below-neutral score)"]];
|
|
2451
|
+
}
|
|
2452
|
+
let consecutive = 0;
|
|
2453
|
+
for (let i = 1; i < divs.length; i++) {
|
|
2454
|
+
if ((divs[i] ?? 0) >= (divs[i - 1] ?? 0)) {
|
|
2455
|
+
consecutive++;
|
|
2456
|
+
} else {
|
|
2457
|
+
consecutive = 0;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
let score;
|
|
2461
|
+
if (consecutive >= 10) score = 1;
|
|
2462
|
+
else if (consecutive >= 7) score = 0.75;
|
|
2463
|
+
else if (consecutive >= 4) score = 0.55;
|
|
2464
|
+
else if (consecutive >= 2) score = 0.35;
|
|
2465
|
+
else score = 0.15;
|
|
2466
|
+
const label = consecutive >= 10 ? "dividend aristocrat-level streak" : consecutive >= 7 ? "strong track record" : consecutive >= 4 ? "solid track record" : consecutive >= 2 ? "limited track record" : "recent cut or minimal history";
|
|
2467
|
+
return [score, [
|
|
2468
|
+
`Dividend growth track: ${consecutive} consecutive non-declining year(s) (${label})`
|
|
2469
|
+
]];
|
|
2470
|
+
}
|
|
2471
|
+
function dividendSafetyScoreFromSeries(annualData) {
|
|
2472
|
+
const payer = isDividendPayer(annualData);
|
|
2473
|
+
if (!payer) {
|
|
2474
|
+
return {
|
|
2475
|
+
score: 50,
|
|
2476
|
+
rating: "non-payer",
|
|
2477
|
+
isDividendPayer: false,
|
|
2478
|
+
yearsAnalyzed: annualData.length,
|
|
2479
|
+
components: {
|
|
2480
|
+
fcfPayoutRatio: 0.5,
|
|
2481
|
+
earningsPayoutRatio: 0.5,
|
|
2482
|
+
balanceSheetStrength: 0.5,
|
|
2483
|
+
dividendGrowthTrack: 0.5
|
|
2484
|
+
},
|
|
2485
|
+
evidence: ["No dividends detected across the provided data series"],
|
|
2486
|
+
interpretation: "Score 50/100: Company does not pay a dividend \u2014 score is not meaningful"
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
const [fcfScore, fcfEv] = scoreFcfPayout(annualData);
|
|
2490
|
+
const [epScore, epEv] = scoreEarningsPayout(annualData);
|
|
2491
|
+
const [bsScore, bsEv] = scoreBalanceSheet(annualData);
|
|
2492
|
+
const [dgtScore, dgtEv] = scoreDividendGrowthTrack(annualData);
|
|
2493
|
+
const raw = 0.35 * fcfScore + 0.25 * epScore + 0.25 * bsScore + 0.15 * dgtScore;
|
|
2494
|
+
const score = Math.round(clamp4(raw, 0, 1) * 100);
|
|
2495
|
+
const rating = score >= 70 ? "safe" : score >= 45 ? "adequate" : score >= 20 ? "risky" : "danger";
|
|
2496
|
+
const desc = {
|
|
2497
|
+
safe: "Dividend well-covered by cash flows with a strong balance sheet",
|
|
2498
|
+
adequate: "Dividend appears sustainable but headroom is limited in some areas",
|
|
2499
|
+
risky: "Coverage is strained \u2014 dividend could be at risk in a downturn",
|
|
2500
|
+
danger: "Dividend likely unsustainable; cut probability is high"
|
|
2501
|
+
};
|
|
2502
|
+
return {
|
|
2503
|
+
score,
|
|
2504
|
+
rating,
|
|
2505
|
+
components: {
|
|
2506
|
+
fcfPayoutRatio: Math.round(fcfScore * 1e4) / 1e4,
|
|
2507
|
+
earningsPayoutRatio: Math.round(epScore * 1e4) / 1e4,
|
|
2508
|
+
balanceSheetStrength: Math.round(bsScore * 1e4) / 1e4,
|
|
2509
|
+
dividendGrowthTrack: Math.round(dgtScore * 1e4) / 1e4
|
|
2510
|
+
},
|
|
2511
|
+
isDividendPayer: true,
|
|
2512
|
+
yearsAnalyzed: annualData.length,
|
|
2513
|
+
evidence: [...fcfEv, ...epEv, ...bsEv, ...dgtEv],
|
|
2514
|
+
interpretation: `Score ${score}/100: ${desc[rating]}`
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/utils/investment-score.ts
|
|
2519
|
+
function clamp5(x, lo, hi) {
|
|
2520
|
+
return Math.max(lo, Math.min(hi, x));
|
|
2521
|
+
}
|
|
2522
|
+
function gradeFromScore(score) {
|
|
2523
|
+
if (score >= 90) return "A+";
|
|
2524
|
+
if (score >= 80) return "A";
|
|
2525
|
+
if (score >= 70) return "B+";
|
|
2526
|
+
if (score >= 60) return "B";
|
|
2527
|
+
if (score >= 45) return "C";
|
|
2528
|
+
if (score >= 25) return "D";
|
|
2529
|
+
return "F";
|
|
2530
|
+
}
|
|
2531
|
+
function convictionFromScore(score) {
|
|
2532
|
+
if (score >= 80) return "strongBuy";
|
|
2533
|
+
if (score >= 65) return "buy";
|
|
2534
|
+
if (score >= 45) return "hold";
|
|
2535
|
+
if (score >= 30) return "sell";
|
|
2536
|
+
return "strongSell";
|
|
2537
|
+
}
|
|
2538
|
+
function computeWeightedScore(inputs) {
|
|
2539
|
+
const hasValuation = inputs.valuationScore !== void 0;
|
|
2540
|
+
if (hasValuation) {
|
|
2541
|
+
return 0.25 * inputs.moatScore + 0.2 * inputs.capitalAllocationScore + 0.2 * inputs.earningsQualityScore + 0.15 * inputs.managementScore + 0.2 * inputs.valuationScore;
|
|
2542
|
+
}
|
|
2543
|
+
return 0.3125 * inputs.moatScore + 0.25 * inputs.capitalAllocationScore + 0.25 * inputs.earningsQualityScore + 0.1875 * inputs.managementScore;
|
|
2544
|
+
}
|
|
2545
|
+
function investmentScoreFromScores(inputs) {
|
|
2546
|
+
const rawScore = computeWeightedScore(inputs);
|
|
2547
|
+
const score = Math.round(clamp5(rawScore, 0, 100));
|
|
2548
|
+
const grade = gradeFromScore(score);
|
|
2549
|
+
const conviction = convictionFromScore(score);
|
|
2550
|
+
const hasVal = inputs.valuationScore !== void 0;
|
|
2551
|
+
const evidence = [
|
|
2552
|
+
`Economic Moat: ${inputs.moatScore}/100 (weight ${hasVal ? "25" : "31"}%)`,
|
|
2553
|
+
`Capital Allocation: ${inputs.capitalAllocationScore}/100 (weight ${hasVal ? "20" : "25"}%)`,
|
|
2554
|
+
`Earnings Quality: ${inputs.earningsQualityScore}/100 (weight ${hasVal ? "20" : "25"}%)`,
|
|
2555
|
+
`Management Quality: ${inputs.managementScore}/100 (weight ${hasVal ? "15" : "19"}%)`
|
|
2556
|
+
];
|
|
2557
|
+
if (hasVal) {
|
|
2558
|
+
evidence.push(`Valuation: ${inputs.valuationScore}/100 (weight 20%)`);
|
|
2559
|
+
} else {
|
|
2560
|
+
evidence.push("Valuation: not provided (weight redistributed)");
|
|
2561
|
+
}
|
|
2562
|
+
const convictionLabel = {
|
|
2563
|
+
strongBuy: "Strong Buy \u2014 exceptional quality at an attractive price",
|
|
2564
|
+
buy: "Buy \u2014 above-average quality with reasonable valuation",
|
|
2565
|
+
hold: "Hold \u2014 average quality or price adequately reflects value",
|
|
2566
|
+
sell: "Sell \u2014 below-average quality or price significantly exceeds value",
|
|
2567
|
+
strongSell: "Strong Sell \u2014 poor quality and/or materially overpriced"
|
|
2568
|
+
};
|
|
2569
|
+
return {
|
|
2570
|
+
score,
|
|
2571
|
+
grade,
|
|
2572
|
+
conviction,
|
|
2573
|
+
components: {
|
|
2574
|
+
moat: inputs.moatScore,
|
|
2575
|
+
capitalAllocation: inputs.capitalAllocationScore,
|
|
2576
|
+
earningsQuality: inputs.earningsQualityScore,
|
|
2577
|
+
management: inputs.managementScore,
|
|
2578
|
+
valuation: inputs.valuationScore ?? null
|
|
2579
|
+
},
|
|
2580
|
+
evidence,
|
|
2581
|
+
interpretation: `Grade ${grade} | Score ${score}/100 | ${convictionLabel[conviction]}`
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function investmentScoreFromSeries(annualData, valuationParams, wacc) {
|
|
2585
|
+
if (annualData.length < 3) {
|
|
2586
|
+
throw new Error("investmentScoreFromSeries requires at least 3 years of data.");
|
|
2587
|
+
}
|
|
2588
|
+
const waccOpt = wacc !== void 0 ? { wacc } : {};
|
|
2589
|
+
const moatResult = moatScore(annualData, waccOpt);
|
|
2590
|
+
const caResult = capitalAllocationScore(annualData, waccOpt);
|
|
2591
|
+
const eqResult = earningsQualityScore(annualData);
|
|
2592
|
+
const mgmtResult = managementQualityScoreFromSeries(annualData, wacc);
|
|
2593
|
+
const inputs = {
|
|
2594
|
+
moatScore: moatResult.score,
|
|
2595
|
+
capitalAllocationScore: caResult.score,
|
|
2596
|
+
earningsQualityScore: eqResult.score,
|
|
2597
|
+
managementScore: mgmtResult.score
|
|
2598
|
+
};
|
|
2599
|
+
if (valuationParams !== void 0) {
|
|
2600
|
+
const valResult = valuationAttractivenessScore(valuationParams);
|
|
2601
|
+
inputs.valuationScore = valResult.score;
|
|
2602
|
+
}
|
|
2603
|
+
return investmentScoreFromScores(inputs);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
export { affo, altmanZScore, annualizeReturn, arrPerFte, assetTurnover, beneishMScore, beta, burnMultiple, bvpsGrowth, cacPaybackPeriod, cacheStats, cached, cagr, calmarRatio, capRate, capexToDepreciation, capexToRevenue, capitalAllocationScore, capitalTurnover, cashConversionCycle, cashRatio, cashReturnOnAssets, cet1Ratio, clearCache, combinedRatio, computeAll, conditionalVaR, currentRatio, customerAcquisitionCost, customerLifetimeValue, dcf2Stage, debtServiceCoverageRatio, debtToAssets, debtToCapital, debtToEquity, defensiveIntervalRatio, dio, dividendGrowthRate, dividendSafetyScoreFromSeries, downsideCaptureRatio, dpo, dso, duPont3, earningsPowerValue, earningsQualityScore, ebitdaCoverageRatio, ebitdaGrowth, ebitdaMargin, efficiencyRatio, enterpriseValue, epsGrowth, equityMultiplier, evEbit, evEbitda, evFcf, evInvestedCapital, evRevenue, expenseRatio, fairValueRange, fcfConversion, fcfGrowth, fcfMargin, fcfYield, ffo, fixedAssetTurnover, fixedChargeCoverageRatio, forwardPe, freeCashFlow, gordonGrowthModel, grahamIntrinsicValue, grahamNumber, grossMargin, grossRevenueRetention, historicalVaR, informationRatio, interestCoverageRatio, invalidate as invalidateCache, inventoryTurnover, investedCapital, investmentScoreFromScores, investmentScoreFromSeries, jensensAlpha, leveredFcf, loanToDepositRatio, lossRatio, ltvCacRatio, magicFormula, magicNumber, managementQualityScoreFromSeries, maxDrawdown, maximumDrawdown, mean, moatScore, montierCScore, 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, portfolioQuality, premiumsToSurplus, pricesToReturns, profitPerEmployee, provisionCoverageRatio, ps, qualityScore, quickRatio, receivablesTurnover, revenueCAGR, revenueGrowth, revenuePerEmployee, reverseDcf, roa, roce, roe, roic, rote, ruleOf40, saasQuickRatio, safeDivide, scenarioDcf, setCache, sharpeRatio, sortinoRatio, stdDev, tangibleBookValuePerShare, tier1CapitalRatio, tobinsQ, trackingError, treynorRatio, ulcerIndex, underwritingProfitMargin, unleveredFcf, upsideCaptureRatio, valuationAttractivenessScore, workingCapitalTurnover };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fin-ratios",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The most comprehensive financial ratios library. 136+ ratios
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "The most comprehensive financial ratios library. 136+ ratios · 10 institutional-grade scoring models · TypeScript + Python · Valuation, Management, Dividend Safety, Investment Scores · FastAPI, MCP server, React hooks, EDGAR fetcher, Pandas/Polars · Zero runtime core dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"import": "./dist/fetchers/alphavantage/index.js",
|
|
33
33
|
"require": "./dist/fetchers/alphavantage/index.cjs"
|
|
34
34
|
},
|
|
35
|
+
"./fetchers/polygon": {
|
|
36
|
+
"types": "./dist/fetchers/polygon/index.d.ts",
|
|
37
|
+
"import": "./dist/fetchers/polygon/index.js",
|
|
38
|
+
"require": "./dist/fetchers/polygon/index.cjs"
|
|
39
|
+
},
|
|
35
40
|
"./hooks": {
|
|
36
41
|
"types": "./dist/hooks/index.d.ts",
|
|
37
42
|
"import": "./dist/hooks/index.js",
|
|
@@ -57,6 +62,8 @@
|
|
|
57
62
|
"test": "vitest run",
|
|
58
63
|
"test:watch": "vitest",
|
|
59
64
|
"typecheck": "tsc --noEmit",
|
|
65
|
+
"prepack": "cp ../README.md . && cp ../LICENSE .",
|
|
66
|
+
"postpack": "rm -f README.md LICENSE",
|
|
60
67
|
"prepublishOnly": "npm run build && npm run typecheck"
|
|
61
68
|
},
|
|
62
69
|
"keywords": [
|
|
@@ -80,7 +87,8 @@
|
|
|
80
87
|
"url": "https://github.com/piyushgupta344"
|
|
81
88
|
},
|
|
82
89
|
"license": "MIT",
|
|
83
|
-
"
|
|
90
|
+
"sideEffects": false,
|
|
91
|
+
"homepage": "https://piyushgupta344.github.io/fin-ratios/",
|
|
84
92
|
"repository": {
|
|
85
93
|
"type": "git",
|
|
86
94
|
"url": "git+https://github.com/piyushgupta344/fin-ratios.git"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/fetchers/alphavantage/index.ts"],"names":[],"mappings":";;;AAcA,IAAM,IAAA,GAAO,mCAAA;AAmBb,eAAsB,iBAAA,CACpB,QACA,OAAA,EACmC;AACnC,EAAA,MAAM,EAAE,MAAA,EAAQ,OAAA,GAAU,CAAA,EAAG,SAAA,GAAY,OAAM,GAAI,OAAA;AAEnD,EAAA,MAAM,IAAA,GAAO,YAAY,kBAAA,GAAqB,eAAA;AAE9C,EAAA,MAAM,CAAC,SAAS,OAAA,EAAS,MAAA,EAAQ,SAAS,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,IAC9D,IAAA,CAAK,EAAE,QAAA,EAAU,kBAAA,EAAoB,QAAQ,MAAA,EAAQ,MAAA,EAAQ,QAAQ,CAAA;AAAA,IACrE,IAAA,CAAK,EAAE,QAAA,EAAU,eAAA,EAAoB,QAAQ,MAAA,EAAQ,MAAA,EAAQ,QAAQ,CAAA;AAAA,IACrE,IAAA,CAAK,EAAE,QAAA,EAAU,WAAA,EAAoB,QAAQ,MAAA,EAAQ,MAAA,EAAQ,QAAQ,CAAA;AAAA,IACrE,IAAA,CAAK,EAAE,QAAA,EAAU,cAAA,EAAoB,QAAQ,MAAA,EAAQ,MAAA,EAAQ,QAAQ;AAAA,GACtE,CAAA;AAED,EAAA,MAAM,UAAA,GAAA,CAAqB,QAAQ,IAAI,CAAA,IAAK,EAAC,EAAG,KAAA,CAAM,GAAG,OAAO,CAAA;AAChE,EAAA,MAAM,UAAA,GAAA,CAAqB,QAAQ,IAAI,CAAA,IAAK,EAAC,EAAG,KAAA,CAAM,GAAG,OAAO,CAAA;AAChE,EAAA,MAAM,SAAA,GAAA,CAAqB,OAAO,IAAI,CAAA,IAAM,EAAC,EAAG,KAAA,CAAM,GAAG,OAAO,CAAA;AAChE,EAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,cAAc,CAAA,IAAK,EAAC;AAE5C,EAAA,MAAM,KAAA,GAAQ,EAAA,CAAG,KAAA,CAAM,WAAW,CAAC,CAAA;AACnC,EAAA,MAAM,UAAoC,EAAC;AAE3C,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,UAAA,CAAW,QAAQ,CAAA,EAAA,EAAK;AAC1C,IAAA,MAAM,GAAA,GAAM,UAAA,CAAW,CAAC,CAAA,IAAK,EAAC;AAC9B,IAAA,MAAM,GAAA,GAAM,UAAA,CAAW,CAAC,CAAA,IAAK,EAAC;AAC9B,IAAA,MAAM,EAAA,GAAM,SAAA,CAAU,CAAC,CAAA,IAAM,EAAC;AAE9B,IAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,MACX,MAAA,EAAQ,IAAI,gBAAA,IAAoB,EAAA;AAAA,MAChC,MAAA,EAAQ;AAAA,QACN,OAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,YAAY,CAAA;AAAA,QAC/C,WAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,WAAW,CAAA;AAAA,QAC9C,IAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,aAAa,CAAA;AAAA,QAChD,IAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,IAAI,CAAA;AAAA,QACvC,MAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,MAAM,CAAA;AAAA,QACzC,SAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,SAAS,CAAA;AAAA,QAC5C,iBAA4B,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,GAAA,CAAI,eAAe,CAAC,CAAA;AAAA,QAC5D,gBAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,gBAAgB,CAAA;AAAA,QACnD,GAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,eAAe,CAAA;AAAA,QAClD,2BAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,2BAA2B,CAAA;AAAA,QAC9D,iBAAA,EAA4B,EAAA,CAAG,GAAA,CAAI,4BAA4B,CAAA;AAAA,QAC/D,GAAA,EAA4B;AAAA,OAC9B;AAAA,MACA,OAAA,EAAS;AAAA,QACP,WAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,WAAW,CAAA;AAAA,QACtC,aAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,kBAAkB,CAAA;AAAA,QAC7C,IAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,qCAAqC,CAAA;AAAA,QAChE,kBAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,qBAAqB,CAAA;AAAA,QAChD,SAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,SAAS,CAAA;AAAA,QACpC,MAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,yBAAyB,CAAA;AAAA,QACpD,QAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,QAAQ,CAAA;AAAA,QACnC,gBAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,gBAAgB,CAAA;AAAA,QAC3C,gBAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,gBAAgB,CAAA;AAAA,QAC3C,kBAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,uBAAuB,CAAA;AAAA,QAClD,eAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,sBAAsB,CAAA;AAAA,QACjD,YAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,sBAAsB,CAAA;AAAA,QACjD,SAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,sBAAsB,CAAA;AAAA,QACjD,WAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,sBAAsB,CAAA;AAAA,QACjD,iBAAA,EAAoB,EAAA,CAAG,GAAA,CAAI,4BAA4B;AAAA,OACzD;AAAA,MACA,QAAA,EAAU;AAAA,QACR,iBAAA,EAAmB,EAAA,CAAG,EAAA,CAAG,iBAAiB,CAAA;AAAA,QAC1C,OAAmB,IAAA,CAAK,GAAA,CAAI,EAAA,CAAG,EAAA,CAAG,mBAAmB,CAAC,CAAA;AAAA,QACtD,iBAAA,EAAmB,EAAA,CAAG,EAAA,CAAG,sBAAsB,CAAA;AAAA,QAC/C,iBAAA,EAAmB,EAAA,CAAG,EAAA,CAAG,qBAAqB,CAAA;AAAA,QAC9C,aAAA,EAAmB,EAAA,CAAG,EAAA,CAAG,cAAc;AAAA,OACzC;AAAA,MACA,UAAA,EAAY;AAAA,QACV,KAAA;AAAA,QACA,SAAA,EAAmB,CAAA;AAAA;AAAA,QACnB,iBAAA,EAAmB,EAAA,CAAG,GAAA,CAAI,4BAA4B,CAAA;AAAA,QACtD;AAAA;AACF,KACD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,OAAA;AACT;AAEA,eAAe,KAAK,MAAA,EAAkD;AACpE,EAAA,MAAM,KAAA,GAAQ,IAAI,eAAA,CAAgB,MAAM,CAAA;AACxC,EAAA,MAAM,OAAO,MAAM,KAAA,CAAM,GAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAE,CAAA;AAC3C,EAAA,IAAI,CAAC,KAAK,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,8BAAA,EAAiC,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA;AAC5E,EAAA,OAAO,KAAK,IAAA,EAAK;AACnB;AAEA,SAAS,GAAG,CAAA,EAAoB;AAC9B,EAAA,IAAI,KAAK,IAAA,IAAQ,CAAA,KAAM,MAAA,IAAU,CAAA,KAAM,KAAK,OAAO,CAAA;AACnD,EAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,EAAA,OAAO,KAAA,CAAM,CAAC,CAAA,GAAI,CAAA,GAAI,CAAA;AACxB","file":"index.cjs","sourcesContent":["/**\n * Alpha Vantage fetcher for fin-ratios (TypeScript).\n *\n * Free tier: 25 requests/day. Premium: higher limits.\n * Get a free API key at https://www.alphavantage.co/support/#api-key\n *\n * @example\n * import { fetchAlphaVantage } from 'fin-ratios/fetchers/alphavantage'\n * const data = await fetchAlphaVantage('IBM', { apiKey: 'your_key' })\n * console.log(data.income.revenue)\n */\n\nimport type { IncomeStatement, BalanceSheet, CashFlowStatement, MarketData } from '../../types/index.js'\n\nconst BASE = 'https://www.alphavantage.co/query'\n\nexport interface AlphaVantageOptions {\n apiKey: string\n periods?: number\n quarterly?: boolean\n}\n\nexport interface AlphaVantagePeriodData {\n period: string\n income: IncomeStatement\n balance: BalanceSheet\n cashFlow: CashFlowStatement\n marketData: MarketData\n}\n\n/**\n * Fetch financial statements from Alpha Vantage for a given ticker.\n */\nexport async function fetchAlphaVantage(\n ticker: string,\n options: AlphaVantageOptions\n): Promise<AlphaVantagePeriodData[]> {\n const { apiKey, periods = 4, quarterly = false } = options\n\n const freq = quarterly ? 'quarterlyReports' : 'annualReports'\n\n const [incData, balData, cfData, quoteData] = await Promise.all([\n _get({ function: 'INCOME_STATEMENT', symbol: ticker, apikey: apiKey }),\n _get({ function: 'BALANCE_SHEET', symbol: ticker, apikey: apiKey }),\n _get({ function: 'CASH_FLOW', symbol: ticker, apikey: apiKey }),\n _get({ function: 'GLOBAL_QUOTE', symbol: ticker, apikey: apiKey }),\n ]) as any[]\n\n const incReports: any[] = (incData[freq] ?? []).slice(0, periods)\n const balReports: any[] = (balData[freq] ?? []).slice(0, periods)\n const cfReports: any[] = (cfData[freq] ?? []).slice(0, periods)\n const quote = quoteData['Global Quote'] ?? {}\n\n const price = _n(quote['05. price'])\n const results: AlphaVantagePeriodData[] = []\n\n for (let i = 0; i < incReports.length; i++) {\n const inc = incReports[i] ?? {}\n const bal = balReports[i] ?? {}\n const cf = cfReports[i] ?? {}\n\n results.push({\n period: inc.fiscalDateEnding ?? '',\n income: {\n revenue: _n(inc.totalRevenue),\n grossProfit: _n(inc.grossProfit),\n cogs: _n(inc.costOfRevenue),\n ebit: _n(inc.ebit),\n ebitda: _n(inc.ebitda),\n netIncome: _n(inc.netIncome),\n interestExpense: Math.abs(_n(inc.interestExpense)),\n incomeTaxExpense: _n(inc.incomeTaxExpense),\n ebt: _n(inc.incomeBeforeTax),\n depreciationAndAmortization:_n(inc.depreciationAndAmortization),\n sharesOutstanding: _n(inc.commonStockSharesOutstanding),\n eps: 0,\n },\n balance: {\n totalAssets: _n(bal.totalAssets),\n currentAssets: _n(bal.totalCurrentAssets),\n cash: _n(bal.cashAndCashEquivalentsAtCarryingValue),\n accountsReceivable: _n(bal.currentNetReceivables),\n inventory: _n(bal.inventory),\n netPPE: _n(bal.propertyPlantEquipmentNet),\n goodwill: _n(bal.goodwill),\n retainedEarnings: _n(bal.retainedEarnings),\n totalLiabilities: _n(bal.totalLiabilities),\n currentLiabilities: _n(bal.totalCurrentLiabilities),\n accountsPayable: _n(bal.currentAccountsPayable),\n longTermDebt: _n(bal.longTermDebtNoncurrent),\n totalDebt: _n(bal.shortLongTermDebtTotal),\n totalEquity: _n(bal.totalShareholderEquity),\n sharesOutstanding: _n(bal.commonStockSharesOutstanding),\n },\n cashFlow: {\n operatingCashFlow: _n(cf.operatingCashflow),\n capex: Math.abs(_n(cf.capitalExpenditures)),\n investingCashFlow: _n(cf.cashflowFromInvestment),\n financingCashFlow: _n(cf.cashflowFromFinancing),\n dividendsPaid: _n(cf.dividendPayout),\n },\n marketData: {\n price,\n marketCap: 0, // not returned by Alpha Vantage income statement endpoints\n sharesOutstanding: _n(inc.commonStockSharesOutstanding),\n ticker,\n },\n })\n }\n\n return results\n}\n\nasync function _get(params: Record<string, string>): Promise<unknown> {\n const query = new URLSearchParams(params)\n const resp = await fetch(`${BASE}?${query}`)\n if (!resp.ok) throw new Error(`Alpha Vantage request failed: ${resp.status}`)\n return resp.json()\n}\n\nfunction _n(v: unknown): number {\n if (v == null || v === 'None' || v === '-') return 0\n const n = Number(v)\n return isNaN(n) ? 0 : n\n}\n"]}
|