spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import { defineConnector, raw, type NormalizedBatch, type ProposedInstrument } from "@market-atlas/connector-sdk";23const URL = "https://www.sec.gov/files/company_tickers_exchange.json";4const EXCHANGE_MAP: Record<string, { exchangeId: string; mic: string } | null> = {5 Nasdaq: { exchangeId: "xnas", mic: "XNAS" },6 NYSE: { exchangeId: "xnys", mic: "XNYS" },7 CBOE: { exchangeId: "bats", mic: "BATS" },8 OTC: null,9};1011/** SEC company ↔ ticker ↔ exchange directory (bulk JSON, refreshed daily). Enriches instruments with CIK + company; creates listed US equities. */12export const secCompanyTickers = defineConnector({13 metadata: {14 id: "sec-company-tickers",15 name: "SEC — company tickers & exchanges directory",16 version: "1.0.0",17 sourceId: "sec-edgar",18 organization: "U.S. Securities and Exchange Commission",19 sourceType: "BULK_FILE",20 jurisdiction: "US",21 rightsStatus: "OFFICIAL_OPEN_DATA",22 realtimeStatus: "END_OF_DAY",23 expectedLatencyMs: null,24 supportsStreaming: false,25 supportsHistorical: false,26 assetClasses: ["EQUITY"],27 exchanges: ["xnas", "xnys", "bats"],28 homepage: "https://www.sec.gov/files/company_tickers_exchange.json",29 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.",30 rightsNotes: "U.S. government data (public domain).",31 termsUrl: "https://www.sec.gov/os/accessing-edgar-data",32 sourceFamily: "sec",33 enabled: true,34 },35 rateLimits: { "www.sec.gov": 2 },36 schedule: { intervalMs: 24 * 60 * 60_000 },37 async poll(ctx) {38 const res = await ctx.http.getText(URL, { timeoutMs: 60_000, headers: { accept: "application/json" } });39 if (res.notModified) return [];40 return [raw("sec-company-tickers", "sec-edgar", "directory", JSON.parse(res.text))];41 },42 normalize(r): NormalizedBatch {43 const p = r.payload as { fields?: string[]; data?: Array<[number, string, string, string | null]> };44 if (r.kind !== "directory" || !Array.isArray(p.data) || !Array.isArray(p.fields)) return { observations: [] };45 const idx = (n: string) => p.fields!.indexOf(n);46 const iCik = idx("cik"), iName = idx("name"), iTicker = idx("ticker"), iEx = idx("exchange");47 if ([iCik, iName, iTicker, iEx].some((i) => i < 0)) return { observations: [] };48 const instruments: ProposedInstrument[] = [];49 for (const row of p.data) {50 const ticker = String(row[iTicker] ?? "").trim();51 const name = String(row[iName] ?? "").trim();52 const cik = String(row[iCik] ?? "").trim();53 const ex = row[iEx] ? EXCHANGE_MAP[String(row[iEx])] : null;54 if (!ticker || !name || !cik) continue;55 const listed = ex != null;56 instruments.push({57 symbol: ticker.replace("-", "."),58 aliases: ticker.includes("-") ? [ticker] : [],59 // Listed names may be created (id derives from the exchange); OTC/unknown venues only enrich existing rows.60 createIfMissing: listed,61 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 },62 });63 }64 return { observations: [], instruments, stats: { rows: p.data.length } };65 },66 fixturesDir: "fixtures",67});6869/** "MICROSOFT CORP" → "Microsoft Corp" while preserving already mixed-case names like "Apple Inc.". */70export function titleCase(s: string): string {71 if (s !== s.toUpperCase()) return s;72 return s73 .toLowerCase()74 .replace(/(^|[\s\-/(&])([a-z])/g, (m, pre: string, c: string) => pre + c.toUpperCase())75 .replace(/\b(Llc|Inc|Corp|Ltd|Plc|Co|Sa|Nv|Ag|Lp)\b/g, (w) => w)76 .replace(/\b(Etf|Reit|Spac|Usa|Us|Adr|Ii|Iii|Iv)\b/g, (w) => w.toUpperCase());77}78