@rdcahalane/edgar-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @rdcahalane/edgar-client
2
+
3
+ Unified client for public SEC EDGAR data.
4
+
5
+ ## Features
6
+
7
+ - ticker-to-CIK lookup
8
+ - full-text filing search
9
+ - XBRL financial series access
10
+ - Form 4 insider transaction parsing
11
+ - 8-K event parsing
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @rdcahalane/edgar-client
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import { lookupCIK, searchFilings, fetchFinancials } from "@rdcahalane/edgar-client";
23
+
24
+ const cik = await lookupCIK("AAPL");
25
+ const filings = await searchFilings({ ticker: "AAPL", form: "8-K" });
26
+ const revenue = await fetchFinancials("AAPL", "RevenueFromContractWithCustomerExcludingAssessedTax");
27
+ ```
28
+
29
+ ## Notes
30
+
31
+ - No API key required
32
+ - Includes courtesy delays for SEC-friendly usage
33
+ - You should set a real contact address in the User-Agent before production use
34
+
35
+ Good for finance tools, public-company research, and filing ingestion pipelines.
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@rdcahalane/edgar-client",
3
+ "version": "0.1.0",
4
+ "description": "Unified SEC EDGAR client: CIK lookup, EFTS search, XBRL financials, Form 4, 8-K parsing. Zero API key needed.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "typecheck": "tsc --noEmit --pretty false"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "typescript": "^5"
22
+ }
23
+ }
package/src/cik.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * CIK (Central Index Key) resolution.
3
+ *
4
+ * Maps ticker symbols to SEC CIK numbers and vice versa.
5
+ * Uses company_tickers.json — loaded once and cached.
6
+ */
7
+
8
+ import { edgarFetch, RATE_LIMIT_MS } from "./index.js";
9
+
10
+ export interface CIKEntry {
11
+ cik: number;
12
+ ticker: string;
13
+ name: string;
14
+ }
15
+
16
+ let _tickerMap: Map<string, CIKEntry> | null = null;
17
+ let _cikMap: Map<number, CIKEntry> | null = null;
18
+
19
+ /**
20
+ * Load the full SEC ticker→CIK mapping. Cached after first call.
21
+ */
22
+ export async function loadTickerMap(): Promise<Map<string, CIKEntry>> {
23
+ if (_tickerMap) return _tickerMap;
24
+
25
+ const r = await edgarFetch("https://www.sec.gov/files/company_tickers.json");
26
+ if (!r.ok) throw new Error(`Failed to load company_tickers.json: ${r.status}`);
27
+ const data = await r.json() as Record<string, { cik_str: number; ticker: string; title: string }>;
28
+
29
+ _tickerMap = new Map();
30
+ _cikMap = new Map();
31
+
32
+ for (const entry of Object.values(data)) {
33
+ const e: CIKEntry = {
34
+ cik: entry.cik_str,
35
+ ticker: entry.ticker.toUpperCase(),
36
+ name: entry.title,
37
+ };
38
+ _tickerMap.set(e.ticker, e);
39
+ _cikMap.set(e.cik, e);
40
+ }
41
+
42
+ return _tickerMap;
43
+ }
44
+
45
+ /**
46
+ * Resolve ticker → CIK number. Returns null if not found.
47
+ */
48
+ export async function lookupCIK(ticker: string): Promise<number | null> {
49
+ const map = await loadTickerMap();
50
+ return map.get(ticker.toUpperCase())?.cik ?? null;
51
+ }
52
+
53
+ /**
54
+ * Resolve CIK → ticker symbol. Returns null if not found.
55
+ */
56
+ export async function lookupTicker(cik: number): Promise<string | null> {
57
+ if (!_cikMap) await loadTickerMap();
58
+ return _cikMap!.get(cik)?.ticker ?? null;
59
+ }
package/src/efts.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * EDGAR Full-Text Search (EFTS)
3
+ *
4
+ * Search SEC filings by form type, date range, and content keywords.
5
+ * Used by 8-K, Form 4, 10-K, DEF 14A scrapers.
6
+ */
7
+
8
+ import { edgarFetch, RATE_LIMIT_MS } from "./index.js";
9
+
10
+ export interface FilingSearchResult {
11
+ accessionNo: string;
12
+ cik: number;
13
+ ticker?: string;
14
+ companyName: string;
15
+ formType: string;
16
+ filedAt: string;
17
+ documentUrl: string;
18
+ description?: string;
19
+ }
20
+
21
+ const EFTS_BASE = "https://efts.sec.gov/LATEST/search-index";
22
+
23
+ interface EFTSParams {
24
+ /** Free-text query (e.g. '"Item 5.02"') */
25
+ query?: string;
26
+ /** Form types to search (e.g. "8-K", "10-K", "DEF 14A") */
27
+ forms?: string;
28
+ /** Start date (YYYY-MM-DD) */
29
+ startDate?: string;
30
+ /** End date (YYYY-MM-DD) */
31
+ endDate?: string;
32
+ /** Max results per page */
33
+ count?: number;
34
+ /** Start index for pagination */
35
+ from?: number;
36
+ }
37
+
38
+ /**
39
+ * Search EDGAR filings via the full-text search API.
40
+ *
41
+ * @example
42
+ * // Find 8-K filings mentioning executive transitions in the last 90 days
43
+ * const results = await searchFilings({
44
+ * query: '"Item 5.02"',
45
+ * forms: '8-K',
46
+ * startDate: '2025-01-01',
47
+ * });
48
+ */
49
+ export async function searchFilings(params: EFTSParams): Promise<FilingSearchResult[]> {
50
+ const searchParams = new URLSearchParams();
51
+
52
+ if (params.query) searchParams.set("q", params.query);
53
+ if (params.forms) searchParams.set("forms", params.forms);
54
+ if (params.startDate || params.endDate) {
55
+ searchParams.set("dateRange", "custom");
56
+ if (params.startDate) searchParams.set("startdt", params.startDate);
57
+ if (params.endDate) searchParams.set("enddt", params.endDate);
58
+ }
59
+ searchParams.set("from", String(params.from ?? 0));
60
+ searchParams.set("count", String(params.count ?? 40));
61
+
62
+ const url = `${EFTS_BASE}?${searchParams}`;
63
+ const r = await edgarFetch(url);
64
+ if (!r.ok) return [];
65
+
66
+ const data = await r.json() as {
67
+ hits: {
68
+ hits: Array<{
69
+ _source: Record<string, unknown> & {
70
+ display_names?: string[];
71
+ };
72
+ }>;
73
+ };
74
+ };
75
+
76
+ const hits = data?.hits?.hits ?? [];
77
+ const results: FilingSearchResult[] = [];
78
+
79
+ for (const hit of hits) {
80
+ const src = hit._source;
81
+ const accNo = String(src.file_num ?? src.accession_no ?? "").replace(/-/g, "");
82
+ const cik = Number(src.entity_id ?? src.cik ?? 0);
83
+
84
+ results.push({
85
+ accessionNo: accNo,
86
+ cik,
87
+ companyName: String(src.entity_name ?? src.display_names?.[0] ?? ""),
88
+ formType: String(src.form_type ?? src.file_type ?? ""),
89
+ filedAt: String(src.file_date ?? src.filed_at ?? ""),
90
+ documentUrl: src.file_url
91
+ ? String(src.file_url)
92
+ : `https://www.sec.gov/Archives/edgar/data/${cik}/${accNo}/`,
93
+ description: String(src.display_description ?? ""),
94
+ });
95
+ }
96
+
97
+ return results;
98
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * 8-K Filing Parser
3
+ *
4
+ * Extracts structured events from SEC 8-K filings:
5
+ * - Item 5.02: Executive transitions (appointments/departures)
6
+ * - Item 2.04: Debt triggers
7
+ * - Item 1.01: M&A/material agreements
8
+ * - Item 1.02: Contract terminations
9
+ * - Item 2.05: Workforce reductions
10
+ * - Item 4.02: Financial restatements
11
+ * - Item 1.05: Cybersecurity incidents
12
+ */
13
+
14
+ import { searchFilings } from "./efts.js";
15
+ import { edgarFetch, RATE_LIMIT_MS } from "./index.js";
16
+
17
+ export interface Event8K {
18
+ ticker?: string;
19
+ companyName: string;
20
+ cik: number;
21
+ item: string;
22
+ itemDescription: string;
23
+ eventDate?: string;
24
+ filingDate: string;
25
+ accessionNo: string;
26
+ documentUrl: string;
27
+ /** Extracted people (for Item 5.02) */
28
+ people?: Array<{
29
+ name: string;
30
+ role_raw?: string;
31
+ direction: "appointment" | "departure" | "unknown";
32
+ }>;
33
+ /** Raw text snippet from the filing */
34
+ excerpt?: string;
35
+ }
36
+
37
+ const ITEM_DESCRIPTIONS: Record<string, string> = {
38
+ "1.01": "Entry into Material Agreement",
39
+ "1.02": "Termination of Material Agreement",
40
+ "1.05": "Material Cybersecurity Incident",
41
+ "2.04": "Triggering Events (Debt Acceleration)",
42
+ "2.05": "Costs of Workforce Reduction",
43
+ "4.02": "Non-Reliance on Previously Issued Financial Statements",
44
+ "5.02": "Departure/Election of Directors or Officers",
45
+ };
46
+
47
+ /**
48
+ * Fetch 8-K events for a specific item type.
49
+ *
50
+ * @example
51
+ * // Get executive transitions from the last 90 days
52
+ * const events = await fetch8KEvents("5.02", { daysBack: 90 });
53
+ */
54
+ export async function fetch8KEvents(
55
+ item: string,
56
+ opts: { daysBack?: number; limit?: number } = {}
57
+ ): Promise<Event8K[]> {
58
+ const daysBack = opts.daysBack ?? 90;
59
+ const limit = opts.limit ?? 100;
60
+
61
+ const endDate = new Date();
62
+ const startDate = new Date(endDate.getTime() - daysBack * 86400000);
63
+
64
+ const filings = await searchFilings({
65
+ query: `"Item ${item}"`,
66
+ forms: "8-K",
67
+ startDate: startDate.toISOString().slice(0, 10),
68
+ endDate: endDate.toISOString().slice(0, 10),
69
+ count: Math.min(limit, 40),
70
+ });
71
+
72
+ const events: Event8K[] = [];
73
+
74
+ for (const filing of filings.slice(0, limit)) {
75
+ const event: Event8K = {
76
+ companyName: filing.companyName,
77
+ cik: filing.cik,
78
+ item,
79
+ itemDescription: ITEM_DESCRIPTIONS[item] ?? `Item ${item}`,
80
+ filingDate: filing.filedAt,
81
+ accessionNo: filing.accessionNo,
82
+ documentUrl: filing.documentUrl,
83
+ };
84
+
85
+ // Extract ticker from company name if available (pattern: "COMPANY NAME (TICKER)")
86
+ const tickerMatch = filing.companyName.match(/\(([A-Z]{1,6})\)/);
87
+ if (tickerMatch) event.ticker = tickerMatch[1];
88
+
89
+ // For Item 5.02, try to extract people from the filing text
90
+ if (item === "5.02") {
91
+ try {
92
+ await new Promise(r => setTimeout(r, RATE_LIMIT_MS));
93
+ const docR = await edgarFetch(filing.documentUrl);
94
+ if (docR.ok) {
95
+ const text = await docR.text();
96
+ event.people = extractPeopleFrom502(text);
97
+ // Extract a short excerpt
98
+ const idx = text.toLowerCase().indexOf("item 5.02");
99
+ if (idx >= 0) {
100
+ event.excerpt = text.slice(idx, idx + 500).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
101
+ }
102
+ }
103
+ } catch { /* skip extraction */ }
104
+ }
105
+
106
+ events.push(event);
107
+ }
108
+
109
+ return events;
110
+ }
111
+
112
+ /**
113
+ * Extract people from Item 5.02 filing text.
114
+ */
115
+ function extractPeopleFrom502(html: string): Event8K["people"] {
116
+ const text = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
117
+ const people: NonNullable<Event8K["people"]> = [];
118
+ const seen = new Set<string>();
119
+
120
+ // Pattern: "appointed X as Y" or "resignation of X" etc.
121
+ const patterns = [
122
+ { re: /(?:appoint|elect|nam)\w*\s+([A-Z][a-z]+ (?:[A-Z]\. )?[A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\s+(?:as|to)\s+([^.,]{3,50})/gi, dir: "appointment" as const },
123
+ { re: /([A-Z][a-z]+ (?:[A-Z]\. )?[A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\s+(?:has been|was)\s+(?:appoint|elect|nam)\w+\s+(?:as|to)\s+([^.,]{3,50})/gi, dir: "appointment" as const },
124
+ { re: /(?:resign|depart|terminat|retir)\w*\s+(?:of\s+)?([A-Z][a-z]+ (?:[A-Z]\. )?[A-Z][a-z]+(?:\s[A-Z][a-z]+)?)/gi, dir: "departure" as const },
125
+ { re: /([A-Z][a-z]+ (?:[A-Z]\. )?[A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\s+(?:has\s+)?(?:resign|depart|retir)\w+/gi, dir: "departure" as const },
126
+ ];
127
+
128
+ for (const { re, dir } of patterns) {
129
+ for (const match of text.matchAll(re)) {
130
+ const name = match[1]?.trim();
131
+ const role = match[2]?.trim();
132
+ if (name && name.length > 4 && name.includes(" ") && !seen.has(name.toLowerCase())) {
133
+ seen.add(name.toLowerCase());
134
+ people.push({ name, role_raw: role, direction: dir });
135
+ }
136
+ }
137
+ }
138
+
139
+ return people;
140
+ }
package/src/form4.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Form 4 Insider Transaction Parser
3
+ *
4
+ * Extracts officer/director names and trade details from SEC Form 4 filings.
5
+ * Useful for ownership tracking, officer lookup, and insider-trading analysis workflows.
6
+ */
7
+
8
+ import { edgarFetch, RATE_LIMIT_MS } from "./index.js";
9
+ import { lookupCIK } from "./cik.js";
10
+
11
+ export interface Form4Person {
12
+ name: string;
13
+ title: string;
14
+ isOfficer: boolean;
15
+ isDirector: boolean;
16
+ is10PctOwner: boolean;
17
+ ticker: string;
18
+ cik: number;
19
+ /** Transactions (buys/sells) if available */
20
+ transactions?: Array<{
21
+ date: string;
22
+ code: string; // P=purchase, S=sale, A=award, etc.
23
+ shares: number;
24
+ pricePerShare: number;
25
+ sharesOwned: number;
26
+ }>;
27
+ accessionNo: string;
28
+ filingDate: string;
29
+ }
30
+
31
+ /**
32
+ * Fetch Form 4 filers for a ticker.
33
+ *
34
+ * Returns officer/director names with their insider transactions.
35
+ */
36
+ export async function fetchForm4People(
37
+ ticker: string,
38
+ opts: { limit?: number } = {}
39
+ ): Promise<Form4Person[]> {
40
+ const cik = await lookupCIK(ticker);
41
+ if (!cik) return [];
42
+
43
+ const cikPadded = String(cik).padStart(10, "0");
44
+ const url = `https://data.sec.gov/submissions/CIK${cikPadded}.json`;
45
+
46
+ const r = await edgarFetch(url);
47
+ if (!r.ok) return [];
48
+
49
+ const data = await r.json() as {
50
+ filings: {
51
+ recent: {
52
+ accessionNumber: string[];
53
+ form: string[];
54
+ filingDate: string[];
55
+ primaryDocument: string[];
56
+ };
57
+ };
58
+ };
59
+
60
+ const recent = data?.filings?.recent;
61
+ if (!recent) return [];
62
+
63
+ // Find Form 4 filings
64
+ const form4Indices: number[] = [];
65
+ for (let i = 0; i < recent.form.length; i++) {
66
+ if (recent.form[i] === "4" || recent.form[i] === "4/A") {
67
+ form4Indices.push(i);
68
+ }
69
+ }
70
+
71
+ const limit = opts.limit ?? 20;
72
+ const people: Form4Person[] = [];
73
+ const seen = new Set<string>();
74
+
75
+ for (const idx of form4Indices.slice(0, limit * 2)) {
76
+ const accNo = recent.accessionNumber[idx];
77
+ const filingDate = recent.filingDate[idx];
78
+ const docName = recent.primaryDocument[idx];
79
+
80
+ if (!accNo || !docName) continue;
81
+
82
+ // Fetch the Form 4 XML
83
+ const xmlUrl = `https://www.sec.gov/Archives/edgar/data/${cik}/${accNo.replace(/-/g, "")}/${docName}`;
84
+
85
+ try {
86
+ await new Promise(r => setTimeout(r, RATE_LIMIT_MS));
87
+ const xmlR = await edgarFetch(xmlUrl, {
88
+ headers: { Accept: "application/xml,text/xml,text/html" } as Record<string, string>,
89
+ });
90
+ if (!xmlR.ok) continue;
91
+
92
+ const xmlText = await xmlR.text();
93
+
94
+ // Extract reporter name
95
+ const nameMatch = xmlText.match(/<rptOwnerName>([^<]+)/);
96
+ const titleMatch = xmlText.match(/<officerTitle>([^<]+)/);
97
+ const isOfficer = /<isOfficer>true/i.test(xmlText) || /<isOfficer>1/.test(xmlText);
98
+ const isDirector = /<isDirector>true/i.test(xmlText) || /<isDirector>1/.test(xmlText);
99
+ const is10Pct = /<isTenPercentOwner>true/i.test(xmlText) || /<isTenPercentOwner>1/.test(xmlText);
100
+
101
+ if (!nameMatch) continue;
102
+
103
+ const rawName = nameMatch[1].trim();
104
+ const key = rawName.toLowerCase();
105
+ if (seen.has(key)) continue;
106
+ seen.add(key);
107
+
108
+ // Extract transactions
109
+ const transactions: Form4Person["transactions"] = [];
110
+ const txMatches = xmlText.matchAll(
111
+ /<transactionDate>.*?<value>([^<]+).*?<transactionCoding>.*?<transactionCode>([^<]+).*?<transactionAmounts>.*?<transactionShares>.*?<value>([^<]+).*?<transactionPricePerShare>.*?<value>([^<]*)/gs
112
+ );
113
+ for (const tx of txMatches) {
114
+ transactions.push({
115
+ date: tx[1]?.trim() || filingDate,
116
+ code: tx[2]?.trim() || "?",
117
+ shares: parseFloat(tx[3]?.trim() || "0"),
118
+ pricePerShare: parseFloat(tx[4]?.trim() || "0"),
119
+ sharesOwned: 0,
120
+ });
121
+ }
122
+
123
+ people.push({
124
+ name: rawName,
125
+ title: titleMatch?.[1]?.trim() || (isDirector ? "Director" : "Officer"),
126
+ isOfficer,
127
+ isDirector,
128
+ is10PctOwner: is10Pct,
129
+ ticker: ticker.toUpperCase(),
130
+ cik,
131
+ transactions: transactions.length ? transactions : undefined,
132
+ accessionNo: accNo,
133
+ filingDate,
134
+ });
135
+
136
+ if (people.length >= limit) break;
137
+ } catch {
138
+ continue;
139
+ }
140
+ }
141
+
142
+ return people;
143
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * edgar-client
3
+ *
4
+ * Unified SEC EDGAR client. Zero API key needed — all public data.
5
+ *
6
+ * Modules:
7
+ * - cik: Ticker → CIK resolution (cached)
8
+ * - efts: EDGAR Full-Text Search (8-K, 10-K, DEF 14A, etc.)
9
+ * - xbrl: XBRL financial time series (revenue, income, capex, etc.)
10
+ * - form4: Insider transaction parsing (officer/director names + trades)
11
+ * - filing8k: 8-K item extraction (5.02 exec transitions, 2.04 debt, 1.01 M&A)
12
+ *
13
+ * Rate limit: SEC allows 10 req/sec with proper User-Agent.
14
+ * All functions enforce a courtesy delay.
15
+ */
16
+
17
+ export { lookupCIK, lookupTicker, loadTickerMap, type CIKEntry } from "./cik.js";
18
+ export { searchFilings, type FilingSearchResult } from "./efts.js";
19
+ export { fetchFinancials, type FinancialSeries } from "./xbrl.js";
20
+ export { fetchForm4People, type Form4Person } from "./form4.js";
21
+ export { fetch8KEvents, type Event8K } from "./filing8k.js";
22
+
23
+ /** Shared EDGAR headers — SEC requires contact info in User-Agent */
24
+ export const EDGAR_HEADERS = {
25
+ "User-Agent": "edgar-client/0.1.0 (opensource@example.com)",
26
+ "Accept": "application/json",
27
+ "Accept-Encoding": "gzip, deflate",
28
+ };
29
+
30
+ /** Courtesy delay between requests (ms) — SEC rate limit is 10/sec */
31
+ export const RATE_LIMIT_MS = 120;
32
+
33
+ export async function edgarFetch(url: string, init?: RequestInit): Promise<Response> {
34
+ const headers = { ...EDGAR_HEADERS, ...(init?.headers as Record<string, string> ?? {}) };
35
+ return fetch(url, { ...init, headers, signal: init?.signal ?? AbortSignal.timeout(30000) });
36
+ }
package/src/xbrl.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * XBRL Financial Time Series
3
+ *
4
+ * Fetches quarterly financial data from SEC EDGAR's XBRL companyfacts API.
5
+ * No API key needed. Returns structured time series for fingerprinting.
6
+ */
7
+
8
+ import { edgarFetch, RATE_LIMIT_MS } from "./index.js";
9
+ import { lookupCIK } from "./cik.js";
10
+
11
+ export interface FinancialSeries {
12
+ ticker: string;
13
+ concept: string;
14
+ unit: string;
15
+ data: Array<{ date: string; value: number; form: string }>;
16
+ }
17
+
18
+ /** Core financial concepts to fetch */
19
+ const CORE_CONCEPTS = [
20
+ "us-gaap/Revenues",
21
+ "us-gaap/RevenueFromContractWithCustomerExcludingAssessedTax",
22
+ "us-gaap/NetIncomeLoss",
23
+ "us-gaap/OperatingIncomeLoss",
24
+ "us-gaap/GrossProfit",
25
+ "us-gaap/CostOfGoodsAndServicesSold",
26
+ "us-gaap/InventoriesNet",
27
+ "us-gaap/Assets",
28
+ "us-gaap/CapitalExpendituresIncurredButNotYetPaid",
29
+ "us-gaap/PaymentsToAcquirePropertyPlantAndEquipment",
30
+ "us-gaap/OperatingCashFlow",
31
+ "us-gaap/NetCashProvidedByOperatingActivities",
32
+ "us-gaap/EarningsPerShareDiluted",
33
+ ];
34
+
35
+ /**
36
+ * Fetch XBRL financial data for a ticker.
37
+ *
38
+ * Returns quarterly time series for core financial concepts.
39
+ * Falls back to alternate concept names (e.g. different revenue labels).
40
+ */
41
+ export async function fetchFinancials(ticker: string): Promise<FinancialSeries[]> {
42
+ const cik = await lookupCIK(ticker);
43
+ if (!cik) return [];
44
+
45
+ const cikPadded = String(cik).padStart(10, "0");
46
+ const url = `https://data.sec.gov/api/xbrl/companyfacts/CIK${cikPadded}.json`;
47
+
48
+ const r = await edgarFetch(url);
49
+ if (!r.ok) return [];
50
+
51
+ const data = await r.json() as {
52
+ facts: Record<string, Record<string, {
53
+ units: Record<string, Array<{
54
+ end: string; val: number; form: string; fp: string; fy: number;
55
+ }>>;
56
+ }>>;
57
+ };
58
+
59
+ const results: FinancialSeries[] = [];
60
+
61
+ for (const conceptPath of CORE_CONCEPTS) {
62
+ const [taxonomy, concept] = conceptPath.split("/");
63
+ const conceptData = data?.facts?.[taxonomy]?.[concept];
64
+ if (!conceptData) continue;
65
+
66
+ // Prefer USD units
67
+ const units = conceptData.units;
68
+ const unitKey = units["USD"] ? "USD" : units["USD/shares"] ? "USD/shares" : Object.keys(units)[0];
69
+ if (!unitKey || !units[unitKey]) continue;
70
+
71
+ // Filter to quarterly (10-Q) and annual (10-K) filings
72
+ const points = units[unitKey]
73
+ .filter(p => p.form === "10-Q" || p.form === "10-K")
74
+ .filter(p => p.end && !isNaN(p.val))
75
+ .map(p => ({ date: p.end, value: p.val, form: p.form }))
76
+ .sort((a, b) => a.date.localeCompare(b.date));
77
+
78
+ // Deduplicate by date (keep latest filing)
79
+ const seen = new Set<string>();
80
+ const deduped = [];
81
+ for (let i = points.length - 1; i >= 0; i--) {
82
+ if (!seen.has(points[i].date)) {
83
+ seen.add(points[i].date);
84
+ deduped.unshift(points[i]);
85
+ }
86
+ }
87
+
88
+ if (deduped.length >= 4) {
89
+ results.push({
90
+ ticker: ticker.toUpperCase(),
91
+ concept,
92
+ unit: unitKey,
93
+ data: deduped,
94
+ });
95
+ }
96
+ }
97
+
98
+ return results;
99
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src"]
15
+ }