fin-ratios 0.4.0 → 0.7.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/fetchers/edgar/index.cjs +28 -0
- package/dist/fetchers/edgar/index.cjs.map +1 -1
- package/dist/fetchers/edgar/index.d.cts +31 -1
- package/dist/fetchers/edgar/index.d.ts +31 -1
- package/dist/fetchers/edgar/index.js +27 -1
- package/dist/fetchers/edgar/index.js.map +1 -1
- package/dist/index.cjs +342 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +213 -1
- package/dist/index.d.ts +213 -1
- package/dist/index.js +339 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -89,6 +89,32 @@ async function fetchEdgar(ticker, options = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
return results;
|
|
91
91
|
}
|
|
92
|
+
function flattenEdgarData(filings) {
|
|
93
|
+
return [...filings].reverse().map((f) => ({
|
|
94
|
+
year: f.fiscalYear,
|
|
95
|
+
revenue: f.income.revenue,
|
|
96
|
+
grossProfit: f.income.grossProfit,
|
|
97
|
+
ebit: f.income.ebit,
|
|
98
|
+
ebt: f.income.ebt,
|
|
99
|
+
netIncome: f.income.netIncome,
|
|
100
|
+
incomeTaxExpense: f.income.incomeTaxExpense,
|
|
101
|
+
interestExpense: f.income.interestExpense,
|
|
102
|
+
totalAssets: f.balance.totalAssets,
|
|
103
|
+
totalEquity: f.balance.totalEquity,
|
|
104
|
+
totalDebt: f.balance.totalDebt || f.balance.longTermDebt,
|
|
105
|
+
cash: f.balance.cash,
|
|
106
|
+
currentAssets: f.balance.currentAssets,
|
|
107
|
+
currentLiabilities: f.balance.currentLiabilities,
|
|
108
|
+
capex: f.cashFlow.capex,
|
|
109
|
+
depreciation: f.income.depreciationAndAmortization,
|
|
110
|
+
operatingCashFlow: f.cashFlow.operatingCashFlow,
|
|
111
|
+
accountsReceivable: f.balance.accountsReceivable
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
async function fetchEdgarFlat(ticker, options = {}) {
|
|
115
|
+
const filings = await fetchEdgar(ticker, options);
|
|
116
|
+
return flattenEdgarData(filings);
|
|
117
|
+
}
|
|
92
118
|
async function _get(url) {
|
|
93
119
|
const resp = await fetch(url, { headers: HEADERS });
|
|
94
120
|
if (!resp.ok) throw new Error(`EDGAR request failed: ${resp.status} ${url}`);
|
|
@@ -96,5 +122,7 @@ async function _get(url) {
|
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
exports.fetchEdgar = fetchEdgar;
|
|
125
|
+
exports.fetchEdgarFlat = fetchEdgarFlat;
|
|
126
|
+
exports.flattenEdgarData = flattenEdgarData;
|
|
99
127
|
//# sourceMappingURL=index.cjs.map
|
|
100
128
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/fetchers/edgar/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * SEC EDGAR fetcher for fin-ratios (TypeScript).\n *\n * Fetches financial data from the free SEC EDGAR XBRL API.\n * No API key required. US companies only.\n *\n * Rate limit: ~10 requests/second (SEC guidelines). Add delays for bulk use.\n *\n * @example\n * import { fetchEdgar } from 'fin-ratios/fetchers/edgar'\n * const filings = await fetchEdgar('AAPL', { numYears: 3 })\n * for (const f of filings) {\n * console.log(`${f.fiscalYear}: Revenue $${(f.income.revenue/1e9).toFixed(1)}B`)\n * }\n */\n\nimport type { IncomeStatement, BalanceSheet, CashFlowStatement } from '../../types/index.js'\n\nconst COMPANY_TICKERS_URL = 'https://data.sec.gov/files/company_tickers.json'\nconst EDGAR_BASE = 'https://data.sec.gov/api/xbrl/companyfacts'\n\nconst HEADERS = {\n 'User-Agent': 'fin-ratios/0.1.0 contact@fin-ratios.dev',\n 'Accept-Encoding': 'gzip, deflate',\n}\n\nexport interface EdgarOptions {\n numYears?: number\n}\n\nexport interface EdgarFilingData {\n fiscalYear: string\n income: IncomeStatement\n balance: BalanceSheet\n cashFlow: CashFlowStatement\n}\n\n/**\n * Fetch multi-year annual filings from SEC EDGAR for a US stock ticker.\n */\nexport async function fetchEdgar(\n ticker: string,\n options: EdgarOptions = {}\n): Promise<EdgarFilingData[]> {\n const { numYears = 3 } = options\n\n // Step 1: Resolve ticker → CIK\n const tickerMap = (await _get(COMPANY_TICKERS_URL)) as Record<string, { cik_str: number; ticker: string; title: string }>\n const entry = Object.values(tickerMap).find(v => v.ticker.toUpperCase() === ticker.toUpperCase())\n if (!entry) throw new Error(`EDGAR: ticker ${ticker} not found`)\n\n const cik = String(entry.cik_str).padStart(10, '0')\n\n // Step 2: Fetch company facts (XBRL)\n const facts = (await _get(`${EDGAR_BASE}/CIK${cik}.json`)) as {\n facts: {\n 'us-gaap'?: Record<string, { units: { USD?: { val: number; end: string; form: string; fp: string }[] } }>\n }\n }\n\n const gaap = facts.facts['us-gaap'] ?? {}\n\n function _annual(concept: string): { end: string; val: number }[] {\n const entries = gaap[concept]?.units?.USD ?? []\n return entries\n .filter(e => e.form === '10-K')\n .sort((a, b) => b.end.localeCompare(a.end))\n }\n\n function _latestForYear(concept: string, year: string): number {\n const annual = _annual(concept)\n const match = annual.find(e => e.end.startsWith(year))\n return match?.val ?? 0\n }\n\n // Collect available fiscal years from revenue\n const revenueEntries = _annual('Revenues').concat(_annual('RevenueFromContractWithCustomerExcludingAssessedTax'))\n const years = [...new Set(revenueEntries.map(e => e.end.slice(0, 4)))].slice(0, numYears)\n\n const results: EdgarFilingData[] = []\n\n for (const year of years) {\n const rev = _latestForYear('Revenues', year) || _latestForYear('RevenueFromContractWithCustomerExcludingAssessedTax', year)\n const ni = _latestForYear('NetIncomeLoss', year)\n const ebit = _latestForYear('OperatingIncomeLoss', year)\n const ta = _latestForYear('Assets', year)\n const ca = _latestForYear('AssetsCurrent', year)\n const cash = _latestForYear('CashAndCashEquivalentsAtCarryingValue', year)\n const ar = _latestForYear('AccountsReceivableNetCurrent', year)\n const inv = _latestForYear('InventoryNet', year)\n const cl = _latestForYear('LiabilitiesCurrent', year)\n const tl = _latestForYear('Liabilities', year)\n const ltd = _latestForYear('LongTermDebt', year)\n const re = _latestForYear('RetainedEarningsAccumulatedDeficit', year)\n const te = _latestForYear('StockholdersEquity', year)\n const ocf = _latestForYear('NetCashProvidedByUsedInOperatingActivities', year)\n const capex = _latestForYear('PaymentsToAcquirePropertyPlantAndEquipment', year)\n const gp = _latestForYear('GrossProfit', year)\n const int = _latestForYear('InterestExpense', year)\n const tax = _latestForYear('IncomeTaxExpense', year) || _latestForYear('IncomeTaxesPaidNet', year)\n\n results.push({\n fiscalYear: year,\n income: {\n revenue: rev, grossProfit: gp, cogs: rev - gp,\n ebit, ebitda: 0, netIncome: ni,\n interestExpense: Math.abs(int), incomeTaxExpense: tax, ebt: ni + tax,\n depreciationAndAmortization: 0, sharesOutstanding: 0, eps: 0,\n },\n balance: {\n totalAssets: ta, currentAssets: ca, cash, accountsReceivable: ar,\n inventory: inv, netPPE: 0, goodwill: 0,\n retainedEarnings: re, totalLiabilities: tl, currentLiabilities: cl,\n accountsPayable: 0, longTermDebt: ltd, totalDebt: ltd,\n totalEquity: te, sharesOutstanding: 0,\n },\n cashFlow: {\n operatingCashFlow: ocf, capex: Math.abs(capex),\n investingCashFlow: 0, financingCashFlow: 0,\n },\n })\n }\n\n return results\n}\n\nasync function _get(url: string): Promise<unknown> {\n const resp = await fetch(url, { headers: HEADERS })\n if (!resp.ok) throw new Error(`EDGAR request failed: ${resp.status} ${url}`)\n return resp.json()\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/fetchers/edgar/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * SEC EDGAR fetcher for fin-ratios (TypeScript).\n *\n * Fetches financial data from the free SEC EDGAR XBRL API.\n * No API key required. US companies only.\n *\n * Rate limit: ~10 requests/second (SEC guidelines). Add delays for bulk use.\n *\n * @example\n * import { fetchEdgar } from 'fin-ratios/fetchers/edgar'\n * const filings = await fetchEdgar('AAPL', { numYears: 3 })\n * for (const f of filings) {\n * console.log(`${f.fiscalYear}: Revenue $${(f.income.revenue/1e9).toFixed(1)}B`)\n * }\n */\n\nimport type { IncomeStatement, BalanceSheet, CashFlowStatement } from '../../types/index.js'\n\nconst COMPANY_TICKERS_URL = 'https://data.sec.gov/files/company_tickers.json'\nconst EDGAR_BASE = 'https://data.sec.gov/api/xbrl/companyfacts'\n\nconst HEADERS = {\n 'User-Agent': 'fin-ratios/0.1.0 contact@fin-ratios.dev',\n 'Accept-Encoding': 'gzip, deflate',\n}\n\nexport interface EdgarOptions {\n numYears?: number\n}\n\nexport interface EdgarFilingData {\n fiscalYear: string\n income: IncomeStatement\n balance: BalanceSheet\n cashFlow: CashFlowStatement\n}\n\n/**\n * Fetch multi-year annual filings from SEC EDGAR for a US stock ticker.\n */\nexport async function fetchEdgar(\n ticker: string,\n options: EdgarOptions = {}\n): Promise<EdgarFilingData[]> {\n const { numYears = 3 } = options\n\n // Step 1: Resolve ticker → CIK\n const tickerMap = (await _get(COMPANY_TICKERS_URL)) as Record<string, { cik_str: number; ticker: string; title: string }>\n const entry = Object.values(tickerMap).find(v => v.ticker.toUpperCase() === ticker.toUpperCase())\n if (!entry) throw new Error(`EDGAR: ticker ${ticker} not found`)\n\n const cik = String(entry.cik_str).padStart(10, '0')\n\n // Step 2: Fetch company facts (XBRL)\n const facts = (await _get(`${EDGAR_BASE}/CIK${cik}.json`)) as {\n facts: {\n 'us-gaap'?: Record<string, { units: { USD?: { val: number; end: string; form: string; fp: string }[] } }>\n }\n }\n\n const gaap = facts.facts['us-gaap'] ?? {}\n\n function _annual(concept: string): { end: string; val: number }[] {\n const entries = gaap[concept]?.units?.USD ?? []\n return entries\n .filter(e => e.form === '10-K')\n .sort((a, b) => b.end.localeCompare(a.end))\n }\n\n function _latestForYear(concept: string, year: string): number {\n const annual = _annual(concept)\n const match = annual.find(e => e.end.startsWith(year))\n return match?.val ?? 0\n }\n\n // Collect available fiscal years from revenue\n const revenueEntries = _annual('Revenues').concat(_annual('RevenueFromContractWithCustomerExcludingAssessedTax'))\n const years = [...new Set(revenueEntries.map(e => e.end.slice(0, 4)))].slice(0, numYears)\n\n const results: EdgarFilingData[] = []\n\n for (const year of years) {\n const rev = _latestForYear('Revenues', year) || _latestForYear('RevenueFromContractWithCustomerExcludingAssessedTax', year)\n const ni = _latestForYear('NetIncomeLoss', year)\n const ebit = _latestForYear('OperatingIncomeLoss', year)\n const ta = _latestForYear('Assets', year)\n const ca = _latestForYear('AssetsCurrent', year)\n const cash = _latestForYear('CashAndCashEquivalentsAtCarryingValue', year)\n const ar = _latestForYear('AccountsReceivableNetCurrent', year)\n const inv = _latestForYear('InventoryNet', year)\n const cl = _latestForYear('LiabilitiesCurrent', year)\n const tl = _latestForYear('Liabilities', year)\n const ltd = _latestForYear('LongTermDebt', year)\n const re = _latestForYear('RetainedEarningsAccumulatedDeficit', year)\n const te = _latestForYear('StockholdersEquity', year)\n const ocf = _latestForYear('NetCashProvidedByUsedInOperatingActivities', year)\n const capex = _latestForYear('PaymentsToAcquirePropertyPlantAndEquipment', year)\n const gp = _latestForYear('GrossProfit', year)\n const int = _latestForYear('InterestExpense', year)\n const tax = _latestForYear('IncomeTaxExpense', year) || _latestForYear('IncomeTaxesPaidNet', year)\n\n results.push({\n fiscalYear: year,\n income: {\n revenue: rev, grossProfit: gp, cogs: rev - gp,\n ebit, ebitda: 0, netIncome: ni,\n interestExpense: Math.abs(int), incomeTaxExpense: tax, ebt: ni + tax,\n depreciationAndAmortization: 0, sharesOutstanding: 0, eps: 0,\n },\n balance: {\n totalAssets: ta, currentAssets: ca, cash, accountsReceivable: ar,\n inventory: inv, netPPE: 0, goodwill: 0,\n retainedEarnings: re, totalLiabilities: tl, currentLiabilities: cl,\n accountsPayable: 0, longTermDebt: ltd, totalDebt: ltd,\n totalEquity: te, sharesOutstanding: 0,\n },\n cashFlow: {\n operatingCashFlow: ocf, capex: Math.abs(capex),\n investingCashFlow: 0, financingCashFlow: 0,\n },\n })\n }\n\n return results\n}\n\n/**\n * Flatten EDGAR filing data into the format expected by scoring utilities\n * (moatScore, capitalAllocationScore, earningsQualityScore, qualityScore).\n * Returns records in chronological order (oldest first).\n */\nexport function flattenEdgarData(filings: EdgarFilingData[]): Array<{\n year: string\n revenue: number\n grossProfit: number\n ebit: number\n ebt: number\n netIncome: number\n incomeTaxExpense: number\n interestExpense: number\n totalAssets: number\n totalEquity: number\n totalDebt: number\n cash: number\n currentAssets: number\n currentLiabilities: number\n capex: number\n depreciation: number\n operatingCashFlow: number\n accountsReceivable: number\n}> {\n return [...filings]\n .reverse() // EDGAR returns newest-first; we want oldest-first\n .map(f => ({\n year: f.fiscalYear,\n revenue: f.income.revenue,\n grossProfit: f.income.grossProfit,\n ebit: f.income.ebit,\n ebt: f.income.ebt,\n netIncome: f.income.netIncome,\n incomeTaxExpense: f.income.incomeTaxExpense,\n interestExpense: f.income.interestExpense,\n totalAssets: f.balance.totalAssets,\n totalEquity: f.balance.totalEquity,\n totalDebt: f.balance.totalDebt || f.balance.longTermDebt,\n cash: f.balance.cash,\n currentAssets: f.balance.currentAssets,\n currentLiabilities: f.balance.currentLiabilities,\n capex: f.cashFlow.capex,\n depreciation: f.income.depreciationAndAmortization,\n operatingCashFlow: f.cashFlow.operatingCashFlow,\n accountsReceivable: f.balance.accountsReceivable,\n }))\n}\n\n/**\n * Fetch EDGAR data for a ticker and return flat records ready for scoring utilities.\n * Records are returned oldest-first.\n */\nexport async function fetchEdgarFlat(\n ticker: string,\n options: EdgarOptions = {},\n): Promise<ReturnType<typeof flattenEdgarData>> {\n const filings = await fetchEdgar(ticker, options)\n return flattenEdgarData(filings)\n}\n\nasync function _get(url: string): Promise<unknown> {\n const resp = await fetch(url, { headers: HEADERS })\n if (!resp.ok) throw new Error(`EDGAR request failed: ${resp.status} ${url}`)\n return resp.json()\n}\n"]}
|
|
@@ -29,5 +29,35 @@ interface EdgarFilingData {
|
|
|
29
29
|
* Fetch multi-year annual filings from SEC EDGAR for a US stock ticker.
|
|
30
30
|
*/
|
|
31
31
|
declare function fetchEdgar(ticker: string, options?: EdgarOptions): Promise<EdgarFilingData[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Flatten EDGAR filing data into the format expected by scoring utilities
|
|
34
|
+
* (moatScore, capitalAllocationScore, earningsQualityScore, qualityScore).
|
|
35
|
+
* Returns records in chronological order (oldest first).
|
|
36
|
+
*/
|
|
37
|
+
declare function flattenEdgarData(filings: EdgarFilingData[]): Array<{
|
|
38
|
+
year: string;
|
|
39
|
+
revenue: number;
|
|
40
|
+
grossProfit: number;
|
|
41
|
+
ebit: number;
|
|
42
|
+
ebt: number;
|
|
43
|
+
netIncome: number;
|
|
44
|
+
incomeTaxExpense: number;
|
|
45
|
+
interestExpense: number;
|
|
46
|
+
totalAssets: number;
|
|
47
|
+
totalEquity: number;
|
|
48
|
+
totalDebt: number;
|
|
49
|
+
cash: number;
|
|
50
|
+
currentAssets: number;
|
|
51
|
+
currentLiabilities: number;
|
|
52
|
+
capex: number;
|
|
53
|
+
depreciation: number;
|
|
54
|
+
operatingCashFlow: number;
|
|
55
|
+
accountsReceivable: number;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch EDGAR data for a ticker and return flat records ready for scoring utilities.
|
|
59
|
+
* Records are returned oldest-first.
|
|
60
|
+
*/
|
|
61
|
+
declare function fetchEdgarFlat(ticker: string, options?: EdgarOptions): Promise<ReturnType<typeof flattenEdgarData>>;
|
|
32
62
|
|
|
33
|
-
export { type EdgarFilingData, type EdgarOptions, fetchEdgar };
|
|
63
|
+
export { type EdgarFilingData, type EdgarOptions, fetchEdgar, fetchEdgarFlat, flattenEdgarData };
|
|
@@ -29,5 +29,35 @@ interface EdgarFilingData {
|
|
|
29
29
|
* Fetch multi-year annual filings from SEC EDGAR for a US stock ticker.
|
|
30
30
|
*/
|
|
31
31
|
declare function fetchEdgar(ticker: string, options?: EdgarOptions): Promise<EdgarFilingData[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Flatten EDGAR filing data into the format expected by scoring utilities
|
|
34
|
+
* (moatScore, capitalAllocationScore, earningsQualityScore, qualityScore).
|
|
35
|
+
* Returns records in chronological order (oldest first).
|
|
36
|
+
*/
|
|
37
|
+
declare function flattenEdgarData(filings: EdgarFilingData[]): Array<{
|
|
38
|
+
year: string;
|
|
39
|
+
revenue: number;
|
|
40
|
+
grossProfit: number;
|
|
41
|
+
ebit: number;
|
|
42
|
+
ebt: number;
|
|
43
|
+
netIncome: number;
|
|
44
|
+
incomeTaxExpense: number;
|
|
45
|
+
interestExpense: number;
|
|
46
|
+
totalAssets: number;
|
|
47
|
+
totalEquity: number;
|
|
48
|
+
totalDebt: number;
|
|
49
|
+
cash: number;
|
|
50
|
+
currentAssets: number;
|
|
51
|
+
currentLiabilities: number;
|
|
52
|
+
capex: number;
|
|
53
|
+
depreciation: number;
|
|
54
|
+
operatingCashFlow: number;
|
|
55
|
+
accountsReceivable: number;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch EDGAR data for a ticker and return flat records ready for scoring utilities.
|
|
59
|
+
* Records are returned oldest-first.
|
|
60
|
+
*/
|
|
61
|
+
declare function fetchEdgarFlat(ticker: string, options?: EdgarOptions): Promise<ReturnType<typeof flattenEdgarData>>;
|
|
32
62
|
|
|
33
|
-
export { type EdgarFilingData, type EdgarOptions, fetchEdgar };
|
|
63
|
+
export { type EdgarFilingData, type EdgarOptions, fetchEdgar, fetchEdgarFlat, flattenEdgarData };
|
|
@@ -87,12 +87,38 @@ async function fetchEdgar(ticker, options = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
return results;
|
|
89
89
|
}
|
|
90
|
+
function flattenEdgarData(filings) {
|
|
91
|
+
return [...filings].reverse().map((f) => ({
|
|
92
|
+
year: f.fiscalYear,
|
|
93
|
+
revenue: f.income.revenue,
|
|
94
|
+
grossProfit: f.income.grossProfit,
|
|
95
|
+
ebit: f.income.ebit,
|
|
96
|
+
ebt: f.income.ebt,
|
|
97
|
+
netIncome: f.income.netIncome,
|
|
98
|
+
incomeTaxExpense: f.income.incomeTaxExpense,
|
|
99
|
+
interestExpense: f.income.interestExpense,
|
|
100
|
+
totalAssets: f.balance.totalAssets,
|
|
101
|
+
totalEquity: f.balance.totalEquity,
|
|
102
|
+
totalDebt: f.balance.totalDebt || f.balance.longTermDebt,
|
|
103
|
+
cash: f.balance.cash,
|
|
104
|
+
currentAssets: f.balance.currentAssets,
|
|
105
|
+
currentLiabilities: f.balance.currentLiabilities,
|
|
106
|
+
capex: f.cashFlow.capex,
|
|
107
|
+
depreciation: f.income.depreciationAndAmortization,
|
|
108
|
+
operatingCashFlow: f.cashFlow.operatingCashFlow,
|
|
109
|
+
accountsReceivable: f.balance.accountsReceivable
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
async function fetchEdgarFlat(ticker, options = {}) {
|
|
113
|
+
const filings = await fetchEdgar(ticker, options);
|
|
114
|
+
return flattenEdgarData(filings);
|
|
115
|
+
}
|
|
90
116
|
async function _get(url) {
|
|
91
117
|
const resp = await fetch(url, { headers: HEADERS });
|
|
92
118
|
if (!resp.ok) throw new Error(`EDGAR request failed: ${resp.status} ${url}`);
|
|
93
119
|
return resp.json();
|
|
94
120
|
}
|
|
95
121
|
|
|
96
|
-
export { fetchEdgar };
|
|
122
|
+
export { fetchEdgar, fetchEdgarFlat, flattenEdgarData };
|
|
97
123
|
//# sourceMappingURL=index.js.map
|
|
98
124
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/fetchers/edgar/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SEC EDGAR fetcher for fin-ratios (TypeScript).\n *\n * Fetches financial data from the free SEC EDGAR XBRL API.\n * No API key required. US companies only.\n *\n * Rate limit: ~10 requests/second (SEC guidelines). Add delays for bulk use.\n *\n * @example\n * import { fetchEdgar } from 'fin-ratios/fetchers/edgar'\n * const filings = await fetchEdgar('AAPL', { numYears: 3 })\n * for (const f of filings) {\n * console.log(`${f.fiscalYear}: Revenue $${(f.income.revenue/1e9).toFixed(1)}B`)\n * }\n */\n\nimport type { IncomeStatement, BalanceSheet, CashFlowStatement } from '../../types/index.js'\n\nconst COMPANY_TICKERS_URL = 'https://data.sec.gov/files/company_tickers.json'\nconst EDGAR_BASE = 'https://data.sec.gov/api/xbrl/companyfacts'\n\nconst HEADERS = {\n 'User-Agent': 'fin-ratios/0.1.0 contact@fin-ratios.dev',\n 'Accept-Encoding': 'gzip, deflate',\n}\n\nexport interface EdgarOptions {\n numYears?: number\n}\n\nexport interface EdgarFilingData {\n fiscalYear: string\n income: IncomeStatement\n balance: BalanceSheet\n cashFlow: CashFlowStatement\n}\n\n/**\n * Fetch multi-year annual filings from SEC EDGAR for a US stock ticker.\n */\nexport async function fetchEdgar(\n ticker: string,\n options: EdgarOptions = {}\n): Promise<EdgarFilingData[]> {\n const { numYears = 3 } = options\n\n // Step 1: Resolve ticker → CIK\n const tickerMap = (await _get(COMPANY_TICKERS_URL)) as Record<string, { cik_str: number; ticker: string; title: string }>\n const entry = Object.values(tickerMap).find(v => v.ticker.toUpperCase() === ticker.toUpperCase())\n if (!entry) throw new Error(`EDGAR: ticker ${ticker} not found`)\n\n const cik = String(entry.cik_str).padStart(10, '0')\n\n // Step 2: Fetch company facts (XBRL)\n const facts = (await _get(`${EDGAR_BASE}/CIK${cik}.json`)) as {\n facts: {\n 'us-gaap'?: Record<string, { units: { USD?: { val: number; end: string; form: string; fp: string }[] } }>\n }\n }\n\n const gaap = facts.facts['us-gaap'] ?? {}\n\n function _annual(concept: string): { end: string; val: number }[] {\n const entries = gaap[concept]?.units?.USD ?? []\n return entries\n .filter(e => e.form === '10-K')\n .sort((a, b) => b.end.localeCompare(a.end))\n }\n\n function _latestForYear(concept: string, year: string): number {\n const annual = _annual(concept)\n const match = annual.find(e => e.end.startsWith(year))\n return match?.val ?? 0\n }\n\n // Collect available fiscal years from revenue\n const revenueEntries = _annual('Revenues').concat(_annual('RevenueFromContractWithCustomerExcludingAssessedTax'))\n const years = [...new Set(revenueEntries.map(e => e.end.slice(0, 4)))].slice(0, numYears)\n\n const results: EdgarFilingData[] = []\n\n for (const year of years) {\n const rev = _latestForYear('Revenues', year) || _latestForYear('RevenueFromContractWithCustomerExcludingAssessedTax', year)\n const ni = _latestForYear('NetIncomeLoss', year)\n const ebit = _latestForYear('OperatingIncomeLoss', year)\n const ta = _latestForYear('Assets', year)\n const ca = _latestForYear('AssetsCurrent', year)\n const cash = _latestForYear('CashAndCashEquivalentsAtCarryingValue', year)\n const ar = _latestForYear('AccountsReceivableNetCurrent', year)\n const inv = _latestForYear('InventoryNet', year)\n const cl = _latestForYear('LiabilitiesCurrent', year)\n const tl = _latestForYear('Liabilities', year)\n const ltd = _latestForYear('LongTermDebt', year)\n const re = _latestForYear('RetainedEarningsAccumulatedDeficit', year)\n const te = _latestForYear('StockholdersEquity', year)\n const ocf = _latestForYear('NetCashProvidedByUsedInOperatingActivities', year)\n const capex = _latestForYear('PaymentsToAcquirePropertyPlantAndEquipment', year)\n const gp = _latestForYear('GrossProfit', year)\n const int = _latestForYear('InterestExpense', year)\n const tax = _latestForYear('IncomeTaxExpense', year) || _latestForYear('IncomeTaxesPaidNet', year)\n\n results.push({\n fiscalYear: year,\n income: {\n revenue: rev, grossProfit: gp, cogs: rev - gp,\n ebit, ebitda: 0, netIncome: ni,\n interestExpense: Math.abs(int), incomeTaxExpense: tax, ebt: ni + tax,\n depreciationAndAmortization: 0, sharesOutstanding: 0, eps: 0,\n },\n balance: {\n totalAssets: ta, currentAssets: ca, cash, accountsReceivable: ar,\n inventory: inv, netPPE: 0, goodwill: 0,\n retainedEarnings: re, totalLiabilities: tl, currentLiabilities: cl,\n accountsPayable: 0, longTermDebt: ltd, totalDebt: ltd,\n totalEquity: te, sharesOutstanding: 0,\n },\n cashFlow: {\n operatingCashFlow: ocf, capex: Math.abs(capex),\n investingCashFlow: 0, financingCashFlow: 0,\n },\n })\n }\n\n return results\n}\n\nasync function _get(url: string): Promise<unknown> {\n const resp = await fetch(url, { headers: HEADERS })\n if (!resp.ok) throw new Error(`EDGAR request failed: ${resp.status} ${url}`)\n return resp.json()\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/fetchers/edgar/index.ts"],"names":[],"mappings":";AAkBA,IAAM,mBAAA,GAAsB,iDAAA;AAC5B,IAAM,UAAA,GAAa,4CAAA;AAEnB,IAAM,OAAA,GAAU;AAAA,EACd,YAAA,EAAc,yCAAA;AAAA,EACd,iBAAA,EAAmB;AACrB,CAAA;AAgBA,eAAsB,UAAA,CACpB,MAAA,EACA,OAAA,GAAwB,EAAC,EACG;AAC5B,EAAA,MAAM,EAAE,QAAA,GAAW,CAAA,EAAE,GAAI,OAAA;AAGzB,EAAA,MAAM,SAAA,GAAa,MAAM,IAAA,CAAK,mBAAmB,CAAA;AACjD,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA,CAAE,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,MAAA,CAAO,WAAA,EAAY,KAAM,MAAA,CAAO,aAAa,CAAA;AAChG,EAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,KAAA,CAAM,CAAA,cAAA,EAAiB,MAAM,CAAA,UAAA,CAAY,CAAA;AAE/D,EAAA,MAAM,MAAM,MAAA,CAAO,KAAA,CAAM,OAAO,CAAA,CAAE,QAAA,CAAS,IAAI,GAAG,CAAA;AAGlD,EAAA,MAAM,QAAS,MAAM,IAAA,CAAK,GAAG,UAAU,CAAA,IAAA,EAAO,GAAG,CAAA,KAAA,CAAO,CAAA;AAMxD,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,SAAS,KAAK,EAAC;AAExC,EAAA,SAAS,QAAQ,OAAA,EAAiD;AAChE,IAAA,MAAM,UAAU,IAAA,CAAK,OAAO,CAAA,EAAG,KAAA,EAAO,OAAO,EAAC;AAC9C,IAAA,OAAO,QACJ,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,KAAS,MAAM,CAAA,CAC7B,IAAA,CAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,GAAA,CAAI,aAAA,CAAc,CAAA,CAAE,GAAG,CAAC,CAAA;AAAA,EAC9C;AAEA,EAAA,SAAS,cAAA,CAAe,SAAiB,IAAA,EAAsB;AAC7D,IAAA,MAAM,MAAA,GAAS,QAAQ,OAAO,CAAA;AAC9B,IAAA,MAAM,KAAA,GAAQ,OAAO,IAAA,CAAK,CAAA,CAAA,KAAK,EAAE,GAAA,CAAI,UAAA,CAAW,IAAI,CAAC,CAAA;AACrD,IAAA,OAAO,OAAO,GAAA,IAAO,CAAA;AAAA,EACvB;AAGA,EAAA,MAAM,iBAAiB,OAAA,CAAQ,UAAU,EAAE,MAAA,CAAO,OAAA,CAAQ,qDAAqD,CAAC,CAAA;AAChH,EAAA,MAAM,QAAQ,CAAC,GAAG,IAAI,GAAA,CAAI,cAAA,CAAe,IAAI,CAAA,CAAA,KAAK,CAAA,CAAE,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAC,CAAC,CAAA,CAAE,KAAA,CAAM,GAAG,QAAQ,CAAA;AAExF,EAAA,MAAM,UAA6B,EAAC;AAEpC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,MAAQ,cAAA,CAAe,UAAA,EAAY,IAAI,CAAA,IAAK,cAAA,CAAe,uDAAuD,IAAI,CAAA;AAC5H,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,eAAA,EAAiB,IAAI,CAAA;AAClD,IAAA,MAAM,IAAA,GAAQ,cAAA,CAAe,qBAAA,EAAuB,IAAI,CAAA;AACxD,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,QAAA,EAAU,IAAI,CAAA;AAC3C,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,eAAA,EAAiB,IAAI,CAAA;AAClD,IAAA,MAAM,IAAA,GAAQ,cAAA,CAAe,uCAAA,EAAyC,IAAI,CAAA;AAC1E,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,8BAAA,EAAgC,IAAI,CAAA;AACjE,IAAA,MAAM,GAAA,GAAQ,cAAA,CAAe,cAAA,EAAgB,IAAI,CAAA;AACjD,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,oBAAA,EAAsB,IAAI,CAAA;AACvD,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,aAAA,EAAe,IAAI,CAAA;AAChD,IAAA,MAAM,GAAA,GAAQ,cAAA,CAAe,cAAA,EAAgB,IAAI,CAAA;AACjD,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,oCAAA,EAAsC,IAAI,CAAA;AACvE,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,oBAAA,EAAsB,IAAI,CAAA;AACvD,IAAA,MAAM,GAAA,GAAQ,cAAA,CAAe,4CAAA,EAA8C,IAAI,CAAA;AAC/E,IAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,4CAAA,EAA8C,IAAI,CAAA;AAC/E,IAAA,MAAM,EAAA,GAAQ,cAAA,CAAe,aAAA,EAAe,IAAI,CAAA;AAChD,IAAA,MAAM,GAAA,GAAQ,cAAA,CAAe,iBAAA,EAAmB,IAAI,CAAA;AACpD,IAAA,MAAM,MAAQ,cAAA,CAAe,kBAAA,EAAoB,IAAI,CAAA,IAAK,cAAA,CAAe,sBAAsB,IAAI,CAAA;AAEnG,IAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,MACX,UAAA,EAAY,IAAA;AAAA,MACZ,MAAA,EAAQ;AAAA,QACN,OAAA,EAAS,GAAA;AAAA,QAAK,WAAA,EAAa,EAAA;AAAA,QAAI,MAAM,GAAA,GAAM,EAAA;AAAA,QAC3C,IAAA;AAAA,QAAM,MAAA,EAAQ,CAAA;AAAA,QAAG,SAAA,EAAW,EAAA;AAAA,QAC5B,eAAA,EAAiB,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AAAA,QAAG,gBAAA,EAAkB,GAAA;AAAA,QAAK,KAAK,EAAA,GAAK,GAAA;AAAA,QACjE,2BAAA,EAA6B,CAAA;AAAA,QAAG,iBAAA,EAAmB,CAAA;AAAA,QAAG,GAAA,EAAK;AAAA,OAC7D;AAAA,MACA,OAAA,EAAS;AAAA,QACP,WAAA,EAAa,EAAA;AAAA,QAAI,aAAA,EAAe,EAAA;AAAA,QAAI,IAAA;AAAA,QAAM,kBAAA,EAAoB,EAAA;AAAA,QAC9D,SAAA,EAAW,GAAA;AAAA,QAAK,MAAA,EAAQ,CAAA;AAAA,QAAG,QAAA,EAAU,CAAA;AAAA,QACrC,gBAAA,EAAkB,EAAA;AAAA,QAAI,gBAAA,EAAkB,EAAA;AAAA,QAAI,kBAAA,EAAoB,EAAA;AAAA,QAChE,eAAA,EAAiB,CAAA;AAAA,QAAG,YAAA,EAAc,GAAA;AAAA,QAAK,SAAA,EAAW,GAAA;AAAA,QAClD,WAAA,EAAa,EAAA;AAAA,QAAI,iBAAA,EAAmB;AAAA,OACtC;AAAA,MACA,QAAA,EAAU;AAAA,QACR,iBAAA,EAAmB,GAAA;AAAA,QAAK,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AAAA,QAC7C,iBAAA,EAAmB,CAAA;AAAA,QAAG,iBAAA,EAAmB;AAAA;AAC3C,KACD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,OAAA;AACT;AAOO,SAAS,iBAAiB,OAAA,EAmB9B;AACD,EAAA,OAAO,CAAC,GAAG,OAAO,EACf,OAAA,EAAQ,CACR,IAAI,CAAA,CAAA,MAAM;AAAA,IACT,MAAoB,CAAA,CAAE,UAAA;AAAA,IACtB,OAAA,EAAoB,EAAE,MAAA,CAAO,OAAA;AAAA,IAC7B,WAAA,EAAoB,EAAE,MAAA,CAAO,WAAA;AAAA,IAC7B,IAAA,EAAoB,EAAE,MAAA,CAAO,IAAA;AAAA,IAC7B,GAAA,EAAoB,EAAE,MAAA,CAAO,GAAA;AAAA,IAC7B,SAAA,EAAoB,EAAE,MAAA,CAAO,SAAA;AAAA,IAC7B,gBAAA,EAAoB,EAAE,MAAA,CAAO,gBAAA;AAAA,IAC7B,eAAA,EAAoB,EAAE,MAAA,CAAO,eAAA;AAAA,IAC7B,WAAA,EAAoB,EAAE,OAAA,CAAQ,WAAA;AAAA,IAC9B,WAAA,EAAoB,EAAE,OAAA,CAAQ,WAAA;AAAA,IAC9B,SAAA,EAAoB,CAAA,CAAE,OAAA,CAAQ,SAAA,IAAa,EAAE,OAAA,CAAQ,YAAA;AAAA,IACrD,IAAA,EAAoB,EAAE,OAAA,CAAQ,IAAA;AAAA,IAC9B,aAAA,EAAoB,EAAE,OAAA,CAAQ,aAAA;AAAA,IAC9B,kBAAA,EAAoB,EAAE,OAAA,CAAQ,kBAAA;AAAA,IAC9B,KAAA,EAAoB,EAAE,QAAA,CAAS,KAAA;AAAA,IAC/B,YAAA,EAAoB,EAAE,MAAA,CAAO,2BAAA;AAAA,IAC7B,iBAAA,EAAoB,EAAE,QAAA,CAAS,iBAAA;AAAA,IAC/B,kBAAA,EAAoB,EAAE,OAAA,CAAQ;AAAA,GAChC,CAAE,CAAA;AACN;AAMA,eAAsB,cAAA,CACpB,MAAA,EACA,OAAA,GAAwB,EAAC,EACqB;AAC9C,EAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,MAAA,EAAQ,OAAO,CAAA;AAChD,EAAA,OAAO,iBAAiB,OAAO,CAAA;AACjC;AAEA,eAAe,KAAK,GAAA,EAA+B;AACjD,EAAA,MAAM,OAAO,MAAM,KAAA,CAAM,KAAK,EAAE,OAAA,EAAS,SAAS,CAAA;AAClD,EAAA,IAAI,CAAC,IAAA,CAAK,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAA;AAC3E,EAAA,OAAO,KAAK,IAAA,EAAK;AACnB","file":"index.js","sourcesContent":["/**\n * SEC EDGAR fetcher for fin-ratios (TypeScript).\n *\n * Fetches financial data from the free SEC EDGAR XBRL API.\n * No API key required. US companies only.\n *\n * Rate limit: ~10 requests/second (SEC guidelines). Add delays for bulk use.\n *\n * @example\n * import { fetchEdgar } from 'fin-ratios/fetchers/edgar'\n * const filings = await fetchEdgar('AAPL', { numYears: 3 })\n * for (const f of filings) {\n * console.log(`${f.fiscalYear}: Revenue $${(f.income.revenue/1e9).toFixed(1)}B`)\n * }\n */\n\nimport type { IncomeStatement, BalanceSheet, CashFlowStatement } from '../../types/index.js'\n\nconst COMPANY_TICKERS_URL = 'https://data.sec.gov/files/company_tickers.json'\nconst EDGAR_BASE = 'https://data.sec.gov/api/xbrl/companyfacts'\n\nconst HEADERS = {\n 'User-Agent': 'fin-ratios/0.1.0 contact@fin-ratios.dev',\n 'Accept-Encoding': 'gzip, deflate',\n}\n\nexport interface EdgarOptions {\n numYears?: number\n}\n\nexport interface EdgarFilingData {\n fiscalYear: string\n income: IncomeStatement\n balance: BalanceSheet\n cashFlow: CashFlowStatement\n}\n\n/**\n * Fetch multi-year annual filings from SEC EDGAR for a US stock ticker.\n */\nexport async function fetchEdgar(\n ticker: string,\n options: EdgarOptions = {}\n): Promise<EdgarFilingData[]> {\n const { numYears = 3 } = options\n\n // Step 1: Resolve ticker → CIK\n const tickerMap = (await _get(COMPANY_TICKERS_URL)) as Record<string, { cik_str: number; ticker: string; title: string }>\n const entry = Object.values(tickerMap).find(v => v.ticker.toUpperCase() === ticker.toUpperCase())\n if (!entry) throw new Error(`EDGAR: ticker ${ticker} not found`)\n\n const cik = String(entry.cik_str).padStart(10, '0')\n\n // Step 2: Fetch company facts (XBRL)\n const facts = (await _get(`${EDGAR_BASE}/CIK${cik}.json`)) as {\n facts: {\n 'us-gaap'?: Record<string, { units: { USD?: { val: number; end: string; form: string; fp: string }[] } }>\n }\n }\n\n const gaap = facts.facts['us-gaap'] ?? {}\n\n function _annual(concept: string): { end: string; val: number }[] {\n const entries = gaap[concept]?.units?.USD ?? []\n return entries\n .filter(e => e.form === '10-K')\n .sort((a, b) => b.end.localeCompare(a.end))\n }\n\n function _latestForYear(concept: string, year: string): number {\n const annual = _annual(concept)\n const match = annual.find(e => e.end.startsWith(year))\n return match?.val ?? 0\n }\n\n // Collect available fiscal years from revenue\n const revenueEntries = _annual('Revenues').concat(_annual('RevenueFromContractWithCustomerExcludingAssessedTax'))\n const years = [...new Set(revenueEntries.map(e => e.end.slice(0, 4)))].slice(0, numYears)\n\n const results: EdgarFilingData[] = []\n\n for (const year of years) {\n const rev = _latestForYear('Revenues', year) || _latestForYear('RevenueFromContractWithCustomerExcludingAssessedTax', year)\n const ni = _latestForYear('NetIncomeLoss', year)\n const ebit = _latestForYear('OperatingIncomeLoss', year)\n const ta = _latestForYear('Assets', year)\n const ca = _latestForYear('AssetsCurrent', year)\n const cash = _latestForYear('CashAndCashEquivalentsAtCarryingValue', year)\n const ar = _latestForYear('AccountsReceivableNetCurrent', year)\n const inv = _latestForYear('InventoryNet', year)\n const cl = _latestForYear('LiabilitiesCurrent', year)\n const tl = _latestForYear('Liabilities', year)\n const ltd = _latestForYear('LongTermDebt', year)\n const re = _latestForYear('RetainedEarningsAccumulatedDeficit', year)\n const te = _latestForYear('StockholdersEquity', year)\n const ocf = _latestForYear('NetCashProvidedByUsedInOperatingActivities', year)\n const capex = _latestForYear('PaymentsToAcquirePropertyPlantAndEquipment', year)\n const gp = _latestForYear('GrossProfit', year)\n const int = _latestForYear('InterestExpense', year)\n const tax = _latestForYear('IncomeTaxExpense', year) || _latestForYear('IncomeTaxesPaidNet', year)\n\n results.push({\n fiscalYear: year,\n income: {\n revenue: rev, grossProfit: gp, cogs: rev - gp,\n ebit, ebitda: 0, netIncome: ni,\n interestExpense: Math.abs(int), incomeTaxExpense: tax, ebt: ni + tax,\n depreciationAndAmortization: 0, sharesOutstanding: 0, eps: 0,\n },\n balance: {\n totalAssets: ta, currentAssets: ca, cash, accountsReceivable: ar,\n inventory: inv, netPPE: 0, goodwill: 0,\n retainedEarnings: re, totalLiabilities: tl, currentLiabilities: cl,\n accountsPayable: 0, longTermDebt: ltd, totalDebt: ltd,\n totalEquity: te, sharesOutstanding: 0,\n },\n cashFlow: {\n operatingCashFlow: ocf, capex: Math.abs(capex),\n investingCashFlow: 0, financingCashFlow: 0,\n },\n })\n }\n\n return results\n}\n\n/**\n * Flatten EDGAR filing data into the format expected by scoring utilities\n * (moatScore, capitalAllocationScore, earningsQualityScore, qualityScore).\n * Returns records in chronological order (oldest first).\n */\nexport function flattenEdgarData(filings: EdgarFilingData[]): Array<{\n year: string\n revenue: number\n grossProfit: number\n ebit: number\n ebt: number\n netIncome: number\n incomeTaxExpense: number\n interestExpense: number\n totalAssets: number\n totalEquity: number\n totalDebt: number\n cash: number\n currentAssets: number\n currentLiabilities: number\n capex: number\n depreciation: number\n operatingCashFlow: number\n accountsReceivable: number\n}> {\n return [...filings]\n .reverse() // EDGAR returns newest-first; we want oldest-first\n .map(f => ({\n year: f.fiscalYear,\n revenue: f.income.revenue,\n grossProfit: f.income.grossProfit,\n ebit: f.income.ebit,\n ebt: f.income.ebt,\n netIncome: f.income.netIncome,\n incomeTaxExpense: f.income.incomeTaxExpense,\n interestExpense: f.income.interestExpense,\n totalAssets: f.balance.totalAssets,\n totalEquity: f.balance.totalEquity,\n totalDebt: f.balance.totalDebt || f.balance.longTermDebt,\n cash: f.balance.cash,\n currentAssets: f.balance.currentAssets,\n currentLiabilities: f.balance.currentLiabilities,\n capex: f.cashFlow.capex,\n depreciation: f.income.depreciationAndAmortization,\n operatingCashFlow: f.cashFlow.operatingCashFlow,\n accountsReceivable: f.balance.accountsReceivable,\n }))\n}\n\n/**\n * Fetch EDGAR data for a ticker and return flat records ready for scoring utilities.\n * Records are returned oldest-first.\n */\nexport async function fetchEdgarFlat(\n ticker: string,\n options: EdgarOptions = {},\n): Promise<ReturnType<typeof flattenEdgarData>> {\n const filings = await fetchEdgar(ticker, options)\n return flattenEdgarData(filings)\n}\n\nasync function _get(url: string): Promise<unknown> {\n const resp = await fetch(url, { headers: HEADERS })\n if (!resp.ok) throw new Error(`EDGAR request failed: ${resp.status} ${url}`)\n return resp.json()\n}\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -119,10 +119,10 @@ gordonGrowthModel.formula = "D1 / (r - g)";
|
|
|
119
119
|
gordonGrowthModel.description = "DDM for stable dividend-paying stocks. Only valid when r > g.";
|
|
120
120
|
function reverseDcf(input) {
|
|
121
121
|
const target = input.marketCap + (input.netDebt ?? 0);
|
|
122
|
-
const computeEV = (
|
|
122
|
+
const computeEV = (g5) => {
|
|
123
123
|
const result = dcf2Stage({
|
|
124
124
|
baseFcf: input.baseFcf,
|
|
125
|
-
growthRate:
|
|
125
|
+
growthRate: g5,
|
|
126
126
|
years: input.years,
|
|
127
127
|
terminalGrowthRate: input.terminalGrowthRate,
|
|
128
128
|
wacc: input.wacc
|
|
@@ -452,8 +452,8 @@ function annualizeReturn(returnValue, periodsPerYear) {
|
|
|
452
452
|
}
|
|
453
453
|
function stdDev(values, ddof = 1) {
|
|
454
454
|
if (values.length < 2) return null;
|
|
455
|
-
const
|
|
456
|
-
const variance = values.reduce((sum, v) => sum + Math.pow(v -
|
|
455
|
+
const mean5 = values.reduce((a, b) => a + b, 0) / values.length;
|
|
456
|
+
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean5, 2), 0) / (values.length - ddof);
|
|
457
457
|
return Math.sqrt(variance);
|
|
458
458
|
}
|
|
459
459
|
function mean(values) {
|
|
@@ -1725,6 +1725,340 @@ function capitalAllocationScore(annualData, options = {}) {
|
|
|
1725
1725
|
};
|
|
1726
1726
|
}
|
|
1727
1727
|
|
|
1728
|
+
// src/utils/earnings-quality.ts
|
|
1729
|
+
function mean4(xs) {
|
|
1730
|
+
return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
1731
|
+
}
|
|
1732
|
+
function std3(xs) {
|
|
1733
|
+
if (xs.length < 2) return 0;
|
|
1734
|
+
const m = mean4(xs);
|
|
1735
|
+
return Math.sqrt(xs.reduce((a, x) => a + (x - m) ** 2, 0) / (xs.length - 1));
|
|
1736
|
+
}
|
|
1737
|
+
function cv3(xs) {
|
|
1738
|
+
const m = mean4(xs);
|
|
1739
|
+
return Math.abs(m) > 1e-9 ? std3(xs) / Math.abs(m) : 1;
|
|
1740
|
+
}
|
|
1741
|
+
function olsSlope3(ys) {
|
|
1742
|
+
const n = ys.length;
|
|
1743
|
+
if (n < 2) return 0;
|
|
1744
|
+
const xm = (n - 1) / 2;
|
|
1745
|
+
const ym = mean4(ys);
|
|
1746
|
+
const ssXX = Array.from({ length: n }, (_, i) => (i - xm) ** 2).reduce((a, b) => a + b, 0);
|
|
1747
|
+
const ssXY = Array.from({ length: n }, (_, i) => (i - xm) * ((ys[i] ?? 0) - ym)).reduce((a, b) => a + b, 0);
|
|
1748
|
+
return ssXX ? ssXY / ssXX : 0;
|
|
1749
|
+
}
|
|
1750
|
+
function clamp2(x, lo, hi) {
|
|
1751
|
+
return Math.max(lo, Math.min(hi, x));
|
|
1752
|
+
}
|
|
1753
|
+
function g4(d, k) {
|
|
1754
|
+
const v = d[k];
|
|
1755
|
+
return typeof v === "number" && isFinite(v) ? v : 0;
|
|
1756
|
+
}
|
|
1757
|
+
function scoreAccruals(series) {
|
|
1758
|
+
const ratios = [];
|
|
1759
|
+
for (let i = 0; i < series.length; i++) {
|
|
1760
|
+
const d = series[i];
|
|
1761
|
+
const ni = g4(d, "netIncome");
|
|
1762
|
+
const cfo = g4(d, "operatingCashFlow");
|
|
1763
|
+
if (ni === 0 && cfo === 0) continue;
|
|
1764
|
+
const prevAssets = i > 0 ? g4(series[i - 1], "totalAssets") : g4(d, "totalAssets");
|
|
1765
|
+
const avgAssets = (g4(d, "totalAssets") + prevAssets) / 2;
|
|
1766
|
+
if (avgAssets <= 0) continue;
|
|
1767
|
+
ratios.push((ni - cfo) / avgAssets);
|
|
1768
|
+
}
|
|
1769
|
+
if (!ratios.length) return [0.4, ["Accruals ratio: operating cash flow not available (neutral score)"]];
|
|
1770
|
+
const ma = mean4(ratios);
|
|
1771
|
+
const level = clamp2(1 - (ma + 0.05) / 0.15, 0, 1);
|
|
1772
|
+
const stability = Math.max(0, 1 - cv3(ratios));
|
|
1773
|
+
const score = clamp2(0.7 * level + 0.3 * stability, 0, 1);
|
|
1774
|
+
const quality = ma < 0 ? "cash-backed" : ma < 0.05 ? "modest accruals" : "high accruals";
|
|
1775
|
+
return [score, [
|
|
1776
|
+
`Accruals ratio: mean ${(ma * 100).toFixed(1)}% of assets (${quality})`,
|
|
1777
|
+
ma < 0.02 ? "Earnings predominantly backed by cash flows" : "Elevated accruals \u2014 verify cash conversion"
|
|
1778
|
+
]];
|
|
1779
|
+
}
|
|
1780
|
+
function scoreCashEarnings(series) {
|
|
1781
|
+
const ratios = [];
|
|
1782
|
+
for (const d of series) {
|
|
1783
|
+
const ni = g4(d, "netIncome");
|
|
1784
|
+
const cfo = g4(d, "operatingCashFlow");
|
|
1785
|
+
if (ni > 0) ratios.push(cfo / ni);
|
|
1786
|
+
}
|
|
1787
|
+
if (!ratios.length) return [0.4, ["Cash earnings quality: insufficient CFO/NI data (neutral score)"]];
|
|
1788
|
+
const mr = mean4(ratios);
|
|
1789
|
+
const level = clamp2((mr - 0.5) / 1, 0, 1);
|
|
1790
|
+
const stability = Math.max(0, 1 - cv3(ratios) * 0.8);
|
|
1791
|
+
const score = clamp2(0.65 * level + 0.35 * stability, 0, 1);
|
|
1792
|
+
const above = ratios.filter((r) => r >= 1).length;
|
|
1793
|
+
const quality = mr >= 1.2 ? "excellent" : mr >= 0.9 ? "good" : "weak";
|
|
1794
|
+
return [score, [
|
|
1795
|
+
`CFO/NI conversion: mean ${mr.toFixed(2)}\xD7 (${above}/${ratios.length} years \u2265 1.0)`,
|
|
1796
|
+
`Cash earnings quality: ${quality}`
|
|
1797
|
+
]];
|
|
1798
|
+
}
|
|
1799
|
+
function scoreRevenueRecognition(series) {
|
|
1800
|
+
const spreads = [];
|
|
1801
|
+
for (let i = 1; i < series.length; i++) {
|
|
1802
|
+
const prev = series[i - 1];
|
|
1803
|
+
const curr = series[i];
|
|
1804
|
+
if (g4(prev, "revenue") <= 0) continue;
|
|
1805
|
+
const revGrowth = (g4(curr, "revenue") - g4(prev, "revenue")) / g4(prev, "revenue");
|
|
1806
|
+
const arPrev = g4(prev, "accountsReceivable");
|
|
1807
|
+
const arCurr = g4(curr, "accountsReceivable");
|
|
1808
|
+
if (arPrev > 0 && arCurr > 0) {
|
|
1809
|
+
spreads.push(revGrowth - (arCurr - arPrev) / arPrev);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (!spreads.length) return [0.45, ["Revenue recognition: accounts receivable data unavailable (neutral score)"]];
|
|
1813
|
+
const ms = mean4(spreads);
|
|
1814
|
+
const level = clamp2((ms + 0.15) / 0.25, 0, 1);
|
|
1815
|
+
const consistency = Math.max(0, 1 - cv3(spreads) * 0.5);
|
|
1816
|
+
const score = clamp2(0.65 * level + 0.35 * consistency, 0, 1);
|
|
1817
|
+
const quality = ms > 0.05 ? "conservative" : ms > -0.05 ? "neutral" : "aggressive";
|
|
1818
|
+
const pos = spreads.filter((s) => s >= 0).length;
|
|
1819
|
+
return [score, [
|
|
1820
|
+
`Revenue recognition: revenue-AR spread ${(ms * 100).toFixed(1)}%/yr (${quality})`,
|
|
1821
|
+
`Revenue outpaced receivables in ${pos}/${spreads.length} periods`
|
|
1822
|
+
]];
|
|
1823
|
+
}
|
|
1824
|
+
function scoreGrossMarginStability(series) {
|
|
1825
|
+
const gms = series.filter((d) => g4(d, "revenue") > 0 && g4(d, "grossProfit") > 0).map((d) => g4(d, "grossProfit") / g4(d, "revenue"));
|
|
1826
|
+
if (gms.length < 2) return [0.45, ["Gross margin stability: insufficient data (neutral score)"]];
|
|
1827
|
+
const mgm = mean4(gms);
|
|
1828
|
+
const cvGm = cv3(gms);
|
|
1829
|
+
const slope = olsSlope3(gms);
|
|
1830
|
+
const stability = clamp2(1 - cvGm * 3, 0, 1);
|
|
1831
|
+
const trendAdj = slope > 5e-3 ? 0.08 : slope < -5e-3 ? -0.08 : 0;
|
|
1832
|
+
const score = clamp2(stability + trendAdj, 0, 1);
|
|
1833
|
+
const trend = slope > 5e-3 ? "improving" : slope < -5e-3 ? "eroding" : "stable";
|
|
1834
|
+
const quality = cvGm < 0.05 ? "high" : cvGm < 0.15 ? "moderate" : "low";
|
|
1835
|
+
return [score, [
|
|
1836
|
+
`Gross margin: mean ${(mgm * 100).toFixed(1)}% stability ${quality} (CV ${cvGm.toFixed(3)})`,
|
|
1837
|
+
`Gross margin trend: ${trend}`
|
|
1838
|
+
]];
|
|
1839
|
+
}
|
|
1840
|
+
function scoreAssetEfficiency(series) {
|
|
1841
|
+
const tos = series.filter((d) => g4(d, "totalAssets") > 0 && g4(d, "revenue") > 0).map((d) => g4(d, "revenue") / g4(d, "totalAssets"));
|
|
1842
|
+
if (tos.length < 2) return [0.45, ["Asset efficiency: insufficient data (neutral score)"]];
|
|
1843
|
+
const slope = olsSlope3(tos);
|
|
1844
|
+
const mto = mean4(tos);
|
|
1845
|
+
const trendScore = clamp2(0.5 + slope * 5, 0, 1);
|
|
1846
|
+
const level = clamp2((mto - 0.2) / 1.5, 0, 1);
|
|
1847
|
+
const score = clamp2(0.5 * trendScore + 0.5 * level, 0, 1);
|
|
1848
|
+
const dir = slope > 0.02 ? "improving" : slope < -0.02 ? "declining" : "stable";
|
|
1849
|
+
return [score, [`Asset turnover: mean ${mto.toFixed(2)}\xD7 trend ${dir} (slope ${slope.toFixed(3)}/yr)`]];
|
|
1850
|
+
}
|
|
1851
|
+
function earningsQualityScore(annualData, _options = {}) {
|
|
1852
|
+
if (annualData.length < 2) {
|
|
1853
|
+
throw new Error("earningsQualityScore requires at least 2 years of data.");
|
|
1854
|
+
}
|
|
1855
|
+
const [accScore, accEv] = scoreAccruals(annualData);
|
|
1856
|
+
const [cfeScore, cfeEv] = scoreCashEarnings(annualData);
|
|
1857
|
+
const [revScore, revEv] = scoreRevenueRecognition(annualData);
|
|
1858
|
+
const [gmsScore, gmsEv] = scoreGrossMarginStability(annualData);
|
|
1859
|
+
const [effScore, effEv] = scoreAssetEfficiency(annualData);
|
|
1860
|
+
const raw = 0.3 * accScore + 0.25 * cfeScore + 0.2 * revScore + 0.15 * gmsScore + 0.1 * effScore;
|
|
1861
|
+
const score = Math.round(clamp2(raw, 0, 1) * 100);
|
|
1862
|
+
const rating = score >= 75 ? "high" : score >= 50 ? "medium" : score >= 25 ? "low" : "poor";
|
|
1863
|
+
return {
|
|
1864
|
+
score,
|
|
1865
|
+
rating,
|
|
1866
|
+
components: {
|
|
1867
|
+
accrualsRatio: Math.round(accScore * 1e4) / 1e4,
|
|
1868
|
+
cashEarnings: Math.round(cfeScore * 1e4) / 1e4,
|
|
1869
|
+
revenueRecognition: Math.round(revScore * 1e4) / 1e4,
|
|
1870
|
+
grossMarginStability: Math.round(gmsScore * 1e4) / 1e4,
|
|
1871
|
+
assetEfficiency: Math.round(effScore * 1e4) / 1e4
|
|
1872
|
+
},
|
|
1873
|
+
yearsAnalyzed: annualData.length,
|
|
1874
|
+
evidence: [...accEv, ...cfeEv, ...revEv, ...gmsEv, ...effEv]
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/utils/fair-value.ts
|
|
1879
|
+
function percentile2(xs, p) {
|
|
1880
|
+
if (!xs.length) return 0;
|
|
1881
|
+
const sorted = [...xs].sort((a, b) => a - b);
|
|
1882
|
+
const idx = p / 100 * (sorted.length - 1);
|
|
1883
|
+
const lo = Math.floor(idx);
|
|
1884
|
+
const hi = Math.min(lo + 1, sorted.length - 1);
|
|
1885
|
+
return (sorted[lo] ?? 0) + ((sorted[hi] ?? 0) - (sorted[lo] ?? 0)) * (idx - lo);
|
|
1886
|
+
}
|
|
1887
|
+
function trimmedMean(xs) {
|
|
1888
|
+
if (xs.length <= 3) return xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
1889
|
+
const sorted = [...xs].sort((a, b) => a - b).slice(1, -1);
|
|
1890
|
+
return sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
1891
|
+
}
|
|
1892
|
+
function round2(x) {
|
|
1893
|
+
return Math.round(x * 100) / 100;
|
|
1894
|
+
}
|
|
1895
|
+
function dcfValue(fcf, growthRate, terminalGrowth, wacc, shares, years) {
|
|
1896
|
+
if (wacc <= terminalGrowth || fcf <= 0 || shares <= 0) return null;
|
|
1897
|
+
let pv = 0;
|
|
1898
|
+
let cf = fcf;
|
|
1899
|
+
for (let i = 1; i <= years; i++) {
|
|
1900
|
+
cf *= 1 + growthRate;
|
|
1901
|
+
pv += cf / (1 + wacc) ** i;
|
|
1902
|
+
}
|
|
1903
|
+
const terminal = cf * (1 + terminalGrowth) / (wacc - terminalGrowth);
|
|
1904
|
+
pv += terminal / (1 + wacc) ** years;
|
|
1905
|
+
return pv / shares;
|
|
1906
|
+
}
|
|
1907
|
+
function grahamValue(eps, bvps) {
|
|
1908
|
+
return eps > 0 && bvps > 0 ? Math.sqrt(22.5 * eps * bvps) : null;
|
|
1909
|
+
}
|
|
1910
|
+
function fcfYieldValue(fcf, shares, targetYield) {
|
|
1911
|
+
return fcf > 0 && shares > 0 && targetYield > 0 ? fcf / targetYield / shares : null;
|
|
1912
|
+
}
|
|
1913
|
+
function evEbitdaValue(ebitda, totalDebt, cash, shares, multiple) {
|
|
1914
|
+
if (ebitda <= 0 || shares <= 0) return null;
|
|
1915
|
+
const equityValue = ebitda * multiple - totalDebt + cash;
|
|
1916
|
+
return equityValue > 0 ? equityValue / shares : null;
|
|
1917
|
+
}
|
|
1918
|
+
function epvValue(ebit, taxRate, wacc, shares, cash, totalDebt) {
|
|
1919
|
+
if (ebit <= 0 || wacc <= 0 || shares <= 0) return null;
|
|
1920
|
+
const equityValue = ebit * (1 - taxRate) / wacc + cash - totalDebt;
|
|
1921
|
+
return equityValue > 0 ? equityValue / shares : null;
|
|
1922
|
+
}
|
|
1923
|
+
function fairValueRange(options) {
|
|
1924
|
+
const {
|
|
1925
|
+
fcf,
|
|
1926
|
+
shares,
|
|
1927
|
+
growthRate = 0.08,
|
|
1928
|
+
terminalGrowth = 0.03,
|
|
1929
|
+
wacc = 0.09,
|
|
1930
|
+
dcfYears = 10,
|
|
1931
|
+
eps,
|
|
1932
|
+
bvps,
|
|
1933
|
+
targetYield = 0.04,
|
|
1934
|
+
ebitda,
|
|
1935
|
+
totalDebt = 0,
|
|
1936
|
+
cash = 0,
|
|
1937
|
+
evEbitdaMultiple = 12,
|
|
1938
|
+
ebit,
|
|
1939
|
+
taxRate = 0.21,
|
|
1940
|
+
currentPrice
|
|
1941
|
+
} = options;
|
|
1942
|
+
const estimates = {};
|
|
1943
|
+
if (fcf && fcf > 0 && shares && shares > 0) {
|
|
1944
|
+
const v = dcfValue(fcf, growthRate, terminalGrowth, wacc, shares, dcfYears);
|
|
1945
|
+
if (v && v > 0) estimates["DCF (2-stage)"] = v;
|
|
1946
|
+
const fy = fcfYieldValue(fcf, shares, targetYield);
|
|
1947
|
+
if (fy && fy > 0) estimates[`FCF Yield (${(targetYield * 100).toFixed(0)}%)`] = fy;
|
|
1948
|
+
}
|
|
1949
|
+
if (eps && bvps) {
|
|
1950
|
+
const g5 = grahamValue(eps, bvps);
|
|
1951
|
+
if (g5 && g5 > 0) estimates["Graham Number"] = g5;
|
|
1952
|
+
}
|
|
1953
|
+
if (ebitda && ebitda > 0 && shares && shares > 0) {
|
|
1954
|
+
const v = evEbitdaValue(ebitda, totalDebt, cash, shares, evEbitdaMultiple);
|
|
1955
|
+
if (v && v > 0) estimates[`EV/EBITDA (${evEbitdaMultiple.toFixed(0)}\xD7)`] = v;
|
|
1956
|
+
}
|
|
1957
|
+
if (ebit && ebit > 0 && shares && shares > 0) {
|
|
1958
|
+
const v = epvValue(ebit, taxRate, wacc, shares, cash, totalDebt);
|
|
1959
|
+
if (v && v > 0) estimates["Earnings Power Value"] = v;
|
|
1960
|
+
}
|
|
1961
|
+
if (!Object.keys(estimates).length) {
|
|
1962
|
+
throw new Error(
|
|
1963
|
+
"No valuation methods could be computed. Provide at least one of: (fcf + shares), (eps + bvps), (ebitda + shares), (ebit + shares)."
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
const vals = Object.values(estimates);
|
|
1967
|
+
const baseValue = round2(trimmedMean(vals));
|
|
1968
|
+
const bearValue = round2(percentile2(vals, 25));
|
|
1969
|
+
const bullValue = round2(percentile2(vals, 75));
|
|
1970
|
+
const result = {
|
|
1971
|
+
estimates: Object.fromEntries(Object.entries(estimates).map(([k, v]) => [k, round2(v)])),
|
|
1972
|
+
baseValue,
|
|
1973
|
+
bearValue,
|
|
1974
|
+
bullValue,
|
|
1975
|
+
methodsUsed: vals.length
|
|
1976
|
+
};
|
|
1977
|
+
if (currentPrice !== void 0 && currentPrice > 0) {
|
|
1978
|
+
result.currentPrice = currentPrice;
|
|
1979
|
+
result.upsidePct = Math.round((baseValue / currentPrice - 1) * 1e3) / 10;
|
|
1980
|
+
result.marginOfSafety = Math.round((bearValue / currentPrice - 1) * 1e3) / 10;
|
|
1981
|
+
}
|
|
1982
|
+
return result;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// src/utils/quality-score.ts
|
|
1986
|
+
function qualityScore(annualData, options = {}) {
|
|
1987
|
+
if (annualData.length < 2) {
|
|
1988
|
+
throw new Error("qualityScore requires at least 2 years of data.");
|
|
1989
|
+
}
|
|
1990
|
+
const eq = earningsQualityScore(annualData);
|
|
1991
|
+
const ms = moatScore(annualData, options);
|
|
1992
|
+
const ca = capitalAllocationScore(annualData, options);
|
|
1993
|
+
const eqNorm = eq.score / 100;
|
|
1994
|
+
const msNorm = ms.score / 100;
|
|
1995
|
+
const caNorm = ca.score / 100;
|
|
1996
|
+
const raw = 0.35 * eqNorm + 0.35 * msNorm + 0.3 * caNorm;
|
|
1997
|
+
const score = Math.round(raw * 100);
|
|
1998
|
+
const grade = score >= 80 ? "exceptional" : score >= 60 ? "strong" : score >= 40 ? "moderate" : score >= 20 ? "weak" : "poor";
|
|
1999
|
+
return {
|
|
2000
|
+
score,
|
|
2001
|
+
grade,
|
|
2002
|
+
components: {
|
|
2003
|
+
earningsQuality: Math.round(eqNorm * 1e4) / 1e4,
|
|
2004
|
+
moat: Math.round(msNorm * 1e4) / 1e4,
|
|
2005
|
+
capitalAllocation: Math.round(caNorm * 1e4) / 1e4
|
|
2006
|
+
},
|
|
2007
|
+
subScores: { earningsQuality: eq, moat: ms, capitalAllocation: ca },
|
|
2008
|
+
yearsAnalyzed: annualData.length,
|
|
2009
|
+
evidence: [
|
|
2010
|
+
`Earnings Quality: ${eq.score}/100 [${eq.rating.toUpperCase()}]`,
|
|
2011
|
+
`Economic Moat: ${ms.score}/100 [${ms.width.toUpperCase()}]`,
|
|
2012
|
+
`Capital Allocation: ${ca.score}/100 [${ca.rating.toUpperCase()}]`
|
|
2013
|
+
]
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/utils/portfolio.ts
|
|
2018
|
+
function portfolioQuality(holdings, options = {}) {
|
|
2019
|
+
if (!holdings.length) throw new Error("holdings must be a non-empty array.");
|
|
2020
|
+
const totalWeight = holdings.reduce((s, h) => s + h.weight, 0);
|
|
2021
|
+
if (totalWeight <= 0) throw new Error("Sum of holding weights must be positive.");
|
|
2022
|
+
const results = holdings.map((h) => {
|
|
2023
|
+
const normWeight = h.weight / totalWeight;
|
|
2024
|
+
try {
|
|
2025
|
+
const q = qualityScore(h.annualData, options);
|
|
2026
|
+
return { ticker: h.ticker, weight: normWeight, quality: q };
|
|
2027
|
+
} catch (err) {
|
|
2028
|
+
return {
|
|
2029
|
+
ticker: h.ticker,
|
|
2030
|
+
weight: normWeight,
|
|
2031
|
+
quality: null,
|
|
2032
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
const successful = results.filter(
|
|
2037
|
+
(r) => r.quality !== null
|
|
2038
|
+
);
|
|
2039
|
+
if (!successful.length) {
|
|
2040
|
+
throw new Error("All holdings failed to compute. Check data and minimum year requirements.");
|
|
2041
|
+
}
|
|
2042
|
+
const effWeight = successful.reduce((s, h) => s + h.weight, 0);
|
|
2043
|
+
const renorm = effWeight > 0 ? 1 / effWeight : 1;
|
|
2044
|
+
const wavg = (fn) => Math.round(successful.reduce((s, h) => s + h.weight * fn(h.quality) * renorm, 0) * 10) / 10;
|
|
2045
|
+
const wq = wavg((q) => q.score);
|
|
2046
|
+
const wm = wavg((q) => q.subScores.moat.score);
|
|
2047
|
+
const weq = wavg((q) => q.subScores.earningsQuality.score);
|
|
2048
|
+
const wca = wavg((q) => q.subScores.capitalAllocation.score);
|
|
2049
|
+
const grade = wq >= 80 ? "exceptional" : wq >= 60 ? "strong" : wq >= 40 ? "moderate" : wq >= 20 ? "weak" : "poor";
|
|
2050
|
+
return {
|
|
2051
|
+
holdings: results,
|
|
2052
|
+
weightedQualityScore: wq,
|
|
2053
|
+
weightedMoatScore: wm,
|
|
2054
|
+
weightedEarningsQuality: weq,
|
|
2055
|
+
weightedCapitalAllocation: wca,
|
|
2056
|
+
effectiveWeight: Math.round(effWeight * 1e4) / 1e4,
|
|
2057
|
+
grade,
|
|
2058
|
+
errors: results.filter((r) => r.error).map((r) => `${r.ticker}: ${r.error}`)
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1728
2062
|
exports.affo = affo;
|
|
1729
2063
|
exports.altmanZScore = altmanZScore;
|
|
1730
2064
|
exports.annualizeReturn = annualizeReturn;
|
|
@@ -1768,6 +2102,7 @@ exports.dpo = dpo;
|
|
|
1768
2102
|
exports.dso = dso;
|
|
1769
2103
|
exports.duPont3 = duPont3;
|
|
1770
2104
|
exports.earningsPowerValue = earningsPowerValue;
|
|
2105
|
+
exports.earningsQualityScore = earningsQualityScore;
|
|
1771
2106
|
exports.ebitdaCoverageRatio = ebitdaCoverageRatio;
|
|
1772
2107
|
exports.ebitdaGrowth = ebitdaGrowth;
|
|
1773
2108
|
exports.ebitdaMargin = ebitdaMargin;
|
|
@@ -1781,6 +2116,7 @@ exports.evFcf = evFcf;
|
|
|
1781
2116
|
exports.evInvestedCapital = evInvestedCapital;
|
|
1782
2117
|
exports.evRevenue = evRevenue;
|
|
1783
2118
|
exports.expenseRatio = expenseRatio;
|
|
2119
|
+
exports.fairValueRange = fairValueRange;
|
|
1784
2120
|
exports.fcfConversion = fcfConversion;
|
|
1785
2121
|
exports.fcfGrowth = fcfGrowth;
|
|
1786
2122
|
exports.fcfMargin = fcfMargin;
|
|
@@ -1840,11 +2176,13 @@ exports.pe = pe;
|
|
|
1840
2176
|
exports.peg = peg;
|
|
1841
2177
|
exports.percentile = percentile;
|
|
1842
2178
|
exports.piotroskiFScore = piotroskiFScore;
|
|
2179
|
+
exports.portfolioQuality = portfolioQuality;
|
|
1843
2180
|
exports.premiumsToSurplus = premiumsToSurplus;
|
|
1844
2181
|
exports.pricesToReturns = pricesToReturns;
|
|
1845
2182
|
exports.profitPerEmployee = profitPerEmployee;
|
|
1846
2183
|
exports.provisionCoverageRatio = provisionCoverageRatio;
|
|
1847
2184
|
exports.ps = ps;
|
|
2185
|
+
exports.qualityScore = qualityScore;
|
|
1848
2186
|
exports.quickRatio = quickRatio;
|
|
1849
2187
|
exports.receivablesTurnover = receivablesTurnover;
|
|
1850
2188
|
exports.revenueCAGR = revenueCAGR;
|