Files
worldmonitor/scripts/_ticker-validation.mjs
Elie Habib b83bf415dc feat(market-implications): validate LLM tickers against live Redis symbol set (#2433)
* feat(market-implications): validate LLM tickers against live Redis symbol set

Add _ticker-validation.mjs with loadTickerSet (reads market:stocks-bootstrap:v1)
and filterInvalidTickers. After static whitelist validation, equity cards are also
gated against the live symbol set from Redis — additive (ALL_ALLOWED_TICKERS union
live symbols), so curated ETF/defense tickers are never incorrectly dropped.
Drops are logged per-card for observability.

* fix(market-implications): move dynamic ticker gate before validation

The previous approach was a logical no-op: validateMarketImplications()
already filtered to ALL_ALLOWED_TICKERS, then the gate re-filtered
against ALL_ALLOWED_TICKERS ∪ liveSet — so every surviving card was
guaranteed to pass and filterInvalidTickers() could never drop anything.

Fix: load the live Redis ticker set first and pass it as the allowed set
into validateMarketImplications() (new allowedTickers parameter). When
Redis has data, effectiveTickers = NON_EQUITY_TICKERS ∪ liveTickerSet,
so hallucinated equity symbols absent from the live symbol set (WMTX,
GOOG, etc.) are dropped at the single validation pass. Falls back to
ALL_ALLOWED_TICKERS when Redis is unavailable. Remove now-dead
filterInvalidTickers export.

* fix(market-implications): use ALL_ALLOWED_TICKERS union liveTickerSet

NON_EQUITY_TICKERS + liveTickerSet incorrectly dropped curated ETF and
defense tickers (SPY, QQQ, XLE, RTX, LMT, NOC) that are in the static
allowlist but not in market:stocks-bootstrap:v1.

Correct model: ALL_ALLOWED_TICKERS is always preserved as the curated
baseline; liveTickerSet extends it with live-priced stocks (NFLX, WMT,
etc.) not in the static list. Hallucinations absent from both sets are
rejected. Remove now-unused NON_EQUITY_TICKERS constant.

* fix(market-implications): strip non-tradeable symbols before live ticker union

market:stocks-bootstrap:v1 contains index symbols (^GSPC, ^DJI, ^IXIC,
^NSEI) and foreign-exchange-suffixed symbols (RELIANCE.NS, TCS.NS) for
display/charting purposes. Unioning the full live set into effectiveTickers
would allow these non-tradeable instruments to pass card validation.

Filter liveTickerSet to /^[A-Z]{1,6}(-[A-Z])?$/ before the union so only
clean US-exchange symbols (NFLX, WMT, BRK-B) extend the static allowlist.
2026-03-28 19:51:29 +04:00

30 lines
1.1 KiB
JavaScript

// @ts-check
/** @typedef {{ symbol: string, name?: string, display?: string }} StockSymbol */
const STOCKS_BOOTSTRAP_KEY = 'market:stocks-bootstrap:v1';
/**
* Load the set of valid ticker symbols from Redis (market:stocks-bootstrap:v1).
* Returns an empty Set if the key is missing or malformed — callers must handle gracefully.
* @param {string} redisUrl
* @param {string} redisToken
* @returns {Promise<Set<string>>}
*/
export async function loadTickerSet(redisUrl, redisToken) {
try {
const resp = await fetch(`${redisUrl}/get/${encodeURIComponent(STOCKS_BOOTSTRAP_KEY)}`, {
headers: { Authorization: `Bearer ${redisToken}` },
signal: AbortSignal.timeout(8_000),
});
if (!resp.ok) return new Set();
const data = await resp.json();
if (!data?.result) return new Set();
/** @type {{ quotes?: StockSymbol[] } | null} */
const parsed = (() => { try { return JSON.parse(data.result); } catch { return null; } })();
if (!Array.isArray(parsed?.quotes)) return new Set();
return new Set(parsed.quotes.map(s => s.symbol?.toUpperCase()).filter(Boolean));
} catch {
return new Set();
}
}