import { defineConnector, parseFeed, raw, type NormalizedBatch, type ProposedEvent } from "@market-atlas/connector-sdk"; import { zonedTimeToUtc } from "@market-atlas/market-model"; const FEED = "https://www.nasdaqtrader.com/rss.aspx?feed=tradehalts"; const ET = "America/New_York"; /** Nasdaq halt reason codes (subset; unknown codes are passed through verbatim). */ export const REASON_CODES: Record = { T1: "News pending", T2: "News released", T5: "Single stock trading pause in effect", T6: "Extraordinary market activity", T8: "ETF halt", T12: "Additional information requested by Nasdaq", H4: "Non-compliance with listing requirements", H9: "Not current in required filings", H10: "SEC trading suspension", H11: "Regulatory concern", O1: "Operations halt", LUDP: "Volatility trading pause (LULD)", LUDS: "Volatility trading pause (LULD, straddle)", MWC1: "Market-wide circuit breaker level 1", MWC2: "Market-wide circuit breaker level 2", MWC3: "Market-wide circuit breaker level 3", IPO1: "IPO not yet trading", M1: "Corporate action", M2: "Quotation not available", D: "Security deletion", }; /** Nasdaq Trader trade-halts RSS (TTL 1 min): halts and resumptions across US listed markets → TRADING_HALT / TRADING_RESUME events. */ export const nasdaqTradeHalts = defineConnector({ metadata: { id: "nasdaq-trade-halts", name: "Nasdaq Trader — trading halts feed", version: "1.0.0", sourceId: "nasdaq-trader", organization: "Nasdaq, Inc.", sourceType: "RSS", jurisdiction: "US", rightsStatus: "PUBLIC_ATTRIBUTED", realtimeStatus: "REALTIME", expectedLatencyMs: 60_000, supportsStreaming: false, supportsHistorical: false, assetClasses: ["EQUITY", "ETF"], exchanges: ["xnas", "xnys", "xase", "arcx", "bats"], homepage: "https://www.nasdaqtrader.com/trader.aspx?id=TradeHalts", description: "Current-day trading halts published by Nasdaq (all US listing markets): symbol, halt time, reason code, LULD threshold price and resumption times. Polled every minute during trading hours.", rightsNotes: "Public feed; displayed with attribution to Nasdaq Trader.", termsUrl: "https://www.nasdaqtrader.com/Trader.aspx?id=Terms", sourceFamily: "nasdaq", enabled: true, }, rateLimits: { "www.nasdaqtrader.com": 1 }, schedule: { openMs: 60_000, closedMs: 10 * 60_000, weekendMs: 60 * 60_000, exchangeId: "xnas" }, async poll(ctx) { const res = await ctx.http.getText(FEED, { timeoutMs: 20_000, headers: { accept: "application/rss+xml, application/xml" } }); if (res.notModified) return []; return [raw("nasdaq-trade-halts", "nasdaq-trader", "rss", res.text)]; }, normalize(r): NormalizedBatch { if (r.kind !== "rss" || typeof r.payload !== "string") return { observations: [] }; const feed = parseFeed(r.payload); const events: ProposedEvent[] = []; for (const item of feed.items) { const x = item.extra as Record; const t = (k: string) => { const v = x[`ndaq:${k}`]; return typeof v === "string" ? v.trim() : typeof v === "number" ? String(v) : ""; }; const symbol = t("IssueSymbol") || item.title.trim(); const haltDate = t("HaltDate"); // MM/DD/YYYY const haltTime = t("HaltTime"); // HH:MM:SS.mmm if (!symbol || !haltDate || !haltTime) continue; const ts = toUtc(haltDate, haltTime); if (!ts) continue; const reason = t("ReasonCode"); const market = t("Market"); const name = t("IssueName"); const key = `${symbol}|${haltDate}|${haltTime}`; events.push({ type: "TRADING_HALT", symbols: [symbol], timestamp: ts, severity: reason.startsWith("MWC") ? "CRITICAL" : reason.startsWith("LUD") || reason === "T5" ? "NOTICE" : "WARNING", confidence: 1, title: `${symbol}: trading halt (${REASON_CODES[reason] ?? (reason || "reason not stated")})`, summary: name ? `${name} · ${market}` : market || null, dedupeKey: `halt:${key}`, data: { symbol, market, reason_code: reason, reason: REASON_CODES[reason] ?? null, issue_name: name, pause_threshold_price: t("PauseThresholdPrice") || null, halt_date: haltDate, halt_time: haltTime }, }); const resDate = t("ResumptionDate"); const resTrade = t("ResumptionTradeTime"); if (resDate && resTrade) { const rts = toUtc(resDate, resTrade); if (rts) events.push({ type: "TRADING_RESUME", symbols: [symbol], timestamp: rts, severity: "INFO", confidence: 1, title: `${symbol}: trading resumed`, summary: name ? `${name} · ${market}` : market || null, dedupeKey: `resume:${key}|${resDate}|${resTrade}`, data: { symbol, market, reason_code: reason, resumption_quote_time: t("ResumptionQuoteTime") || null, resumption_trade_time: resTrade, halt_time: `${haltDate} ${haltTime}` }, }); } } return { observations: [], events, stats: { items: feed.items.length } }; }, fixturesDir: "fixtures", }); function toUtc(mdy: string, hms: string): number | null { const m = mdy.match(/^(\d{2})\/(\d{2})\/(\d{4})$/); const h = hms.match(/^(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/); if (!m || !h) return null; return zonedTimeToUtc(`${m[3]}-${m[1]}-${m[2]}T${h[1]}:${h[2]}:${h[3]}${h[4] ? `.${h[4]}` : ""}`, ET); }