SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
11 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
5.4 KB · 121 lines typescript
Raw Blame History
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