spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import { defineConnector, parseFeed, raw, type NormalizedBatch, type ProposedEvent } from "@market-atlas/connector-sdk";2import { zonedTimeToUtc } from "@market-atlas/market-model";34const FEED = "https://www.nasdaqtrader.com/rss.aspx?feed=tradehalts";5const ET = "America/New_York";6/** Nasdaq halt reason codes (subset; unknown codes are passed through verbatim). */7export const REASON_CODES: Record<string, string> = {8 T1: "News pending",9 T2: "News released",10 T5: "Single stock trading pause in effect",11 T6: "Extraordinary market activity",12 T8: "ETF halt",13 T12: "Additional information requested by Nasdaq",14 H4: "Non-compliance with listing requirements",15 H9: "Not current in required filings",16 H10: "SEC trading suspension",17 H11: "Regulatory concern",18 O1: "Operations halt",19 LUDP: "Volatility trading pause (LULD)",20 LUDS: "Volatility trading pause (LULD, straddle)",21 MWC1: "Market-wide circuit breaker level 1",22 MWC2: "Market-wide circuit breaker level 2",23 MWC3: "Market-wide circuit breaker level 3",24 IPO1: "IPO not yet trading",25 M1: "Corporate action",26 M2: "Quotation not available",27 D: "Security deletion",28};2930/** Nasdaq Trader trade-halts RSS (TTL 1 min): halts and resumptions across US listed markets → TRADING_HALT / TRADING_RESUME events. */31export const nasdaqTradeHalts = defineConnector({32 metadata: {33 id: "nasdaq-trade-halts",34 name: "Nasdaq Trader — trading halts feed",35 version: "1.0.0",36 sourceId: "nasdaq-trader",37 organization: "Nasdaq, Inc.",38 sourceType: "RSS",39 jurisdiction: "US",40 rightsStatus: "PUBLIC_ATTRIBUTED",41 realtimeStatus: "REALTIME",42 expectedLatencyMs: 60_000,43 supportsStreaming: false,44 supportsHistorical: false,45 assetClasses: ["EQUITY", "ETF"],46 exchanges: ["xnas", "xnys", "xase", "arcx", "bats"],47 homepage: "https://www.nasdaqtrader.com/trader.aspx?id=TradeHalts",48 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.",49 rightsNotes: "Public feed; displayed with attribution to Nasdaq Trader.",50 termsUrl: "https://www.nasdaqtrader.com/Trader.aspx?id=Terms",51 sourceFamily: "nasdaq",52 enabled: true,53 },54 rateLimits: { "www.nasdaqtrader.com": 1 },55 schedule: { openMs: 60_000, closedMs: 10 * 60_000, weekendMs: 60 * 60_000, exchangeId: "xnas" },56 async poll(ctx) {57 const res = await ctx.http.getText(FEED, { timeoutMs: 20_000, headers: { accept: "application/rss+xml, application/xml" } });58 if (res.notModified) return [];59 return [raw("nasdaq-trade-halts", "nasdaq-trader", "rss", res.text)];60 },61 normalize(r): NormalizedBatch {62 if (r.kind !== "rss" || typeof r.payload !== "string") return { observations: [] };63 const feed = parseFeed(r.payload);64 const events: ProposedEvent[] = [];65 for (const item of feed.items) {66 const x = item.extra as Record<string, unknown>;67 const t = (k: string) => {68 const v = x[`ndaq:${k}`];69 return typeof v === "string" ? v.trim() : typeof v === "number" ? String(v) : "";70 };71 const symbol = t("IssueSymbol") || item.title.trim();72 const haltDate = t("HaltDate"); // MM/DD/YYYY73 const haltTime = t("HaltTime"); // HH:MM:SS.mmm74 if (!symbol || !haltDate || !haltTime) continue;75 const ts = toUtc(haltDate, haltTime);76 if (!ts) continue;77 const reason = t("ReasonCode");78 const market = t("Market");79 const name = t("IssueName");80 const key = `${symbol}|${haltDate}|${haltTime}`;81 events.push({82 type: "TRADING_HALT",83 symbols: [symbol],84 timestamp: ts,85 severity: reason.startsWith("MWC") ? "CRITICAL" : reason.startsWith("LUD") || reason === "T5" ? "NOTICE" : "WARNING",86 confidence: 1,87 title: `${symbol}: trading halt (${REASON_CODES[reason] ?? (reason || "reason not stated")})`,88 summary: name ? `${name} · ${market}` : market || null,89 dedupeKey: `halt:${key}`,90 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 },91 });92 const resDate = t("ResumptionDate");93 const resTrade = t("ResumptionTradeTime");94 if (resDate && resTrade) {95 const rts = toUtc(resDate, resTrade);96 if (rts)97 events.push({98 type: "TRADING_RESUME",99 symbols: [symbol],100 timestamp: rts,101 severity: "INFO",102 confidence: 1,103 title: `${symbol}: trading resumed`,104 summary: name ? `${name} · ${market}` : market || null,105 dedupeKey: `resume:${key}|${resDate}|${resTrade}`,106 data: { symbol, market, reason_code: reason, resumption_quote_time: t("ResumptionQuoteTime") || null, resumption_trade_time: resTrade, halt_time: `${haltDate} ${haltTime}` },107 });108 }109 }110 return { observations: [], events, stats: { items: feed.items.length } };111 },112 fixturesDir: "fixtures",113});114115function toUtc(mdy: string, hms: string): number | null {116 const m = mdy.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);117 const h = hms.match(/^(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/);118 if (!m || !h) return null;119 return zonedTimeToUtc(`${m[3]}-${m[1]}-${m[2]}T${h[1]}:${h[2]}:${h[3]}${h[4] ? `.${h[4]}` : ""}`, ET);120}121