import { defineConnector, parseDelimited, raw, type NormalizedBatch, type ProposedInstrument } from "@market-atlas/connector-sdk"; const NASDAQ_LISTED = "https://www.nasdaqtrader.com/dynamic/SymDir/nasdaqlisted.txt"; const OTHER_LISTED = "https://www.nasdaqtrader.com/dynamic/SymDir/otherlisted.txt"; /** otherlisted.txt "Exchange" column codes. */ const OTHER_EXCHANGES: Record = { N: { exchangeId: "xnys", mic: "XNYS" }, A: { exchangeId: "xase", mic: "XASE" }, P: { exchangeId: "arcx", mic: "ARCX" }, Z: { exchangeId: "bats", mic: "BATS" }, V: { exchangeId: "iexg", mic: "IEXG" }, }; /** * Nasdaq Trader symbol directories (nasdaqlisted.txt + otherlisted.txt): the complete list of * US-listed securities with ETF flag and listing venue. Populates the instrument master daily. */ export const nasdaqSymbolDirectory = defineConnector({ metadata: { id: "nasdaq-symbol-directory", name: "Nasdaq Trader — US symbol directory", version: "1.0.0", sourceId: "nasdaq-trader", organization: "Nasdaq, Inc.", sourceType: "BULK_FILE", jurisdiction: "US", rightsStatus: "PUBLIC_ATTRIBUTED", realtimeStatus: "END_OF_DAY", expectedLatencyMs: null, supportsStreaming: false, supportsHistorical: false, assetClasses: ["EQUITY", "ETF"], exchanges: ["xnas", "xnys", "xase", "arcx", "bats"], homepage: "https://www.nasdaqtrader.com/trader.aspx?id=symboldirdefs", description: "Daily pipe-delimited directories of every Nasdaq-listed and other US exchange-listed security (~11,000 rows): symbol, security name, listing venue, ETF flag, test-issue flag. Seeds the US instrument master.", rightsNotes: "Public reference files published by Nasdaq Trader; displayed with attribution.", termsUrl: "https://www.nasdaqtrader.com/Trader.aspx?id=Terms", sourceFamily: "nasdaq", enabled: true, }, rateLimits: { "www.nasdaqtrader.com": 1 }, schedule: { intervalMs: 24 * 60 * 60_000 }, async poll(ctx) { const out = []; for (const [kind, url] of [["nasdaqlisted", NASDAQ_LISTED], ["otherlisted", OTHER_LISTED]] as const) { const res = await ctx.http.getText(url, { timeoutMs: 60_000 }); if (res.notModified) continue; out.push(raw("nasdaq-symbol-directory", "nasdaq-trader", kind, res.text)); } return out; }, normalize(r): NormalizedBatch { if (typeof r.payload !== "string") return { observations: [] }; const rows = parseDelimited(r.payload, "|").filter((row) => !String(Object.values(row)[0] ?? "").startsWith("File Creation Time")); const instruments: ProposedInstrument[] = []; if (r.kind === "nasdaqlisted") { for (const row of rows) { if (row["Test Issue"] === "Y" || !row.Symbol) continue; const etf = row.ETF === "Y"; instruments.push(proposal(row.Symbol!, row["Security Name"] ?? row.Symbol!, "xnas", "XNAS", etf, { market_category: row["Market Category"], financial_status: row["Financial Status"] })); } } else if (r.kind === "otherlisted") { for (const row of rows) { if (row["Test Issue"] === "Y" || !row["ACT Symbol"]) continue; const ex = OTHER_EXCHANGES[row.Exchange ?? ""]; if (!ex) continue; const etf = row.ETF === "Y"; instruments.push(proposal(row["ACT Symbol"]!, row["Security Name"] ?? row["ACT Symbol"]!, ex.exchangeId, ex.mic, etf, { cqs_symbol: row["CQS Symbol"], nasdaq_symbol: row["NASDAQ Symbol"] }, [row["CQS Symbol"], row["NASDAQ Symbol"]])); } } return { observations: [], instruments, stats: { rows: rows.length } }; }, fixturesDir: "fixtures", }); function proposal(symbol: string, rawName: string, exchangeId: string, mic: string, etf: boolean, metadata: Record, extraAliases: Array = []): ProposedInstrument { const name = cleanName(rawName); const sym = symbol.trim(); const aliases = [...new Set([sym.replace(".", "-"), sym.replace(".", "/"), ...extraAliases.filter((a): a is string => !!a && a !== sym)])].filter((a) => a !== sym); const securityType = classify(rawName); return { symbol: sym, aliases, hint: { assetClass: etf ? "ETF" : securityType === "COMMON_STOCK" || securityType === "ADR" ? "EQUITY" : "OTHER", name, exchangeId, mic, currency: "USD", country: "US", securityType: etf ? "ETF" : securityType, companyName: !etf && securityType === "COMMON_STOCK" ? name : null, metadata }, }; } export function cleanName(n: string): string { return n .trim() .replace(/\s+-\s+(Common Stock|Class [A-Z] Common Stock|Ordinary Shares|Common Shares|American Depositary Shares.*|Class [A-Z] Ordinary Shares?.*|Units?.*|Warrants?.*|Depositary Shares.*)$/i, "") .replace(/\s+Common Stock$/i, "") .replace(/\s{2,}/g, " ") .trim(); } export function classify(n: string): string { const s = n.toLowerCase(); if (/warrant/.test(s)) return "WARRANT"; if (/\bunits?\b/.test(s)) return "UNIT"; if (/\bright(s)?\b/.test(s)) return "RIGHT"; if (/american depositary|\badr\b|\bads\b/.test(s)) return "ADR"; if (/preferred|preference|depositary share/.test(s)) return "PREFERRED"; if (/\bnotes?\b|debenture|bond/.test(s)) return "DEBT"; return "COMMON_STOCK"; }