import { defineConnector, raw, type NormalizedBatch, type ProposedInstrument } from "@market-atlas/connector-sdk"; const URL = "https://www.sec.gov/files/company_tickers_exchange.json"; const EXCHANGE_MAP: Record = { Nasdaq: { exchangeId: "xnas", mic: "XNAS" }, NYSE: { exchangeId: "xnys", mic: "XNYS" }, CBOE: { exchangeId: "bats", mic: "BATS" }, OTC: null, }; /** SEC company ↔ ticker ↔ exchange directory (bulk JSON, refreshed daily). Enriches instruments with CIK + company; creates listed US equities. */ export const secCompanyTickers = defineConnector({ metadata: { id: "sec-company-tickers", name: "SEC — company tickers & exchanges directory", version: "1.0.0", sourceId: "sec-edgar", organization: "U.S. Securities and Exchange Commission", sourceType: "BULK_FILE", jurisdiction: "US", rightsStatus: "OFFICIAL_OPEN_DATA", realtimeStatus: "END_OF_DAY", expectedLatencyMs: null, supportsStreaming: false, supportsHistorical: false, assetClasses: ["EQUITY"], exchanges: ["xnas", "xnys", "bats"], homepage: "https://www.sec.gov/files/company_tickers_exchange.json", description: "Bulk directory of ~10,000 SEC registrants with ticker, listing exchange and CIK. Used to resolve filings to instruments and to attach companies (CIK) to US equities.", rightsNotes: "U.S. government data (public domain).", termsUrl: "https://www.sec.gov/os/accessing-edgar-data", sourceFamily: "sec", enabled: true, }, rateLimits: { "www.sec.gov": 2 }, schedule: { intervalMs: 24 * 60 * 60_000 }, async poll(ctx) { const res = await ctx.http.getText(URL, { timeoutMs: 60_000, headers: { accept: "application/json" } }); if (res.notModified) return []; return [raw("sec-company-tickers", "sec-edgar", "directory", JSON.parse(res.text))]; }, normalize(r): NormalizedBatch { const p = r.payload as { fields?: string[]; data?: Array<[number, string, string, string | null]> }; if (r.kind !== "directory" || !Array.isArray(p.data) || !Array.isArray(p.fields)) return { observations: [] }; const idx = (n: string) => p.fields!.indexOf(n); const iCik = idx("cik"), iName = idx("name"), iTicker = idx("ticker"), iEx = idx("exchange"); if ([iCik, iName, iTicker, iEx].some((i) => i < 0)) return { observations: [] }; const instruments: ProposedInstrument[] = []; for (const row of p.data) { const ticker = String(row[iTicker] ?? "").trim(); const name = String(row[iName] ?? "").trim(); const cik = String(row[iCik] ?? "").trim(); const ex = row[iEx] ? EXCHANGE_MAP[String(row[iEx])] : null; if (!ticker || !name || !cik) continue; const listed = ex != null; instruments.push({ symbol: ticker.replace("-", "."), aliases: ticker.includes("-") ? [ticker] : [], // Listed names may be created (id derives from the exchange); OTC/unknown venues only enrich existing rows. createIfMissing: listed, hint: { assetClass: "EQUITY", name: titleCase(name), exchangeId: ex?.exchangeId ?? null, mic: ex?.mic ?? null, currency: "USD", country: "US", securityType: "COMMON_STOCK", companyName: titleCase(name), cik }, }); } return { observations: [], instruments, stats: { rows: p.data.length } }; }, fixturesDir: "fixtures", }); /** "MICROSOFT CORP" → "Microsoft Corp" while preserving already mixed-case names like "Apple Inc.". */ export function titleCase(s: string): string { if (s !== s.toUpperCase()) return s; return s .toLowerCase() .replace(/(^|[\s\-/(&])([a-z])/g, (m, pre: string, c: string) => pre + c.toUpperCase()) .replace(/\b(Llc|Inc|Corp|Ltd|Plc|Co|Sa|Nv|Ag|Lp)\b/g, (w) => w) .replace(/\b(Etf|Reit|Spac|Usa|Us|Adr|Ii|Iii|Iv)\b/g, (w) => w.toUpperCase()); }