const https = require("https"); // fetches daily OHLC from yahoo finance v8 chart api // no api key needed but rate limited so we cache aggressively async function fetchYahooHistory(ticker, range = "6mo") { const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?range=${range}&interval=1d`; const body = await httpGet(url, { "User-Agent": "Mozilla/5.0 (compatible; duriin-intelligence/1.0)", }); let parsed; try { parsed = JSON.parse(body); } catch (_) { throw new Error(`yahoo response not JSON: ${body.slice(0, 200)}`); } const result = parsed?.chart?.result?.[0]; if (!result) return null; const ts = result.timestamp || []; const closes = result.indicators?.quote?.[0]?.close || []; const out = []; for (let i = 0; i < ts.length; i++) { if (closes[i] == null) continue; out.push({ date: new Date(ts[i] * 1000).toISOString().slice(0, 10), close: closes[i], }); } return out; } function nearestPriceOnOrBefore(history, dateStr) { // history is sorted ascending by date let last = null; for (const row of history) { if (row.date <= dateStr) last = row; else break; } return last ? last.close : null; } function computeStdev(values) { if (values.length < 2) return 0; const mean = values.reduce((a, b) => a + b, 0) / values.length; const sq = values.reduce((acc, v) => acc + (v - mean) ** 2, 0); return Math.sqrt(sq / (values.length - 1)); } function computeReturns(history) { const rets = []; for (let i = 1; i < history.length; i++) { const prev = history[i - 1].close; const cur = history[i].close; if (prev > 0) rets.push((cur - prev) / prev); } return rets; } // returns { price, price_30d_ago, price_90d_ago, vol_30d } as_of a given date async function getPriceContext(intelligenceDb, ticker, asOfDate) { if (!ticker || !asOfDate) return null; // skip private/synthetic tickers — yahoo wont know them if (/^(OPENAI|ANTHROPIC|XAI|HUAWEI|BYTEDANCE|DEEPSEEK|MISTRAL|COHERE|GROQ|SCALEAI|MCKINSEY|DELOITTE|STABILITY|INFLECTION|SPACEX|BLUEORIGIN)$/i.test(ticker)) { return null; } const cacheRow = intelligenceDb.prepare( "SELECT price, price_30d_ago, price_90d_ago, vol_30d FROM price_snapshots WHERE ticker = ? AND as_of = ?" ).get(ticker, asOfDate); if (cacheRow) return cacheRow; let history; try { history = await fetchYahooHistory(ticker, "6mo"); } catch (err) { // dont blow up the worker on a single bad ticker return null; } if (!history || history.length === 0) return null; const price = nearestPriceOnOrBefore(history, asOfDate); if (price == null) return null; const date30 = new Date(asOfDate); date30.setDate(date30.getDate() - 30); const price30 = nearestPriceOnOrBefore(history, date30.toISOString().slice(0, 10)); const date90 = new Date(asOfDate); date90.setDate(date90.getDate() - 90); const price90 = nearestPriceOnOrBefore(history, date90.toISOString().slice(0, 10)); // 30-day annualized vol from daily returns const recent = history.filter(h => h.date <= asOfDate).slice(-30); const vol30 = computeStdev(computeReturns(recent)) * Math.sqrt(252); const snapshot = { price, price_30d_ago: price30, price_90d_ago: price90, vol_30d: vol30, }; try { intelligenceDb.prepare( "INSERT OR REPLACE INTO price_snapshots (ticker, as_of, price, price_30d_ago, price_90d_ago, vol_30d) VALUES (?, ?, ?, ?, ?, ?)" ).run(ticker, asOfDate, snapshot.price, snapshot.price_30d_ago, snapshot.price_90d_ago, snapshot.vol_30d); } catch (_) {} return snapshot; } // formats the snapshot for inclusion in the LLM prompt function formatPriceContext(snapshot, ticker) { if (!snapshot || snapshot.price == null) return null; const lines = [`${ticker} price as of event: $${snapshot.price.toFixed(2)}`]; if (snapshot.price_30d_ago) { const ret30 = (snapshot.price - snapshot.price_30d_ago) / snapshot.price_30d_ago * 100; lines.push(`30-day return: ${ret30 >= 0 ? "+" : ""}${ret30.toFixed(1)}%`); } if (snapshot.price_90d_ago) { const ret90 = (snapshot.price - snapshot.price_90d_ago) / snapshot.price_90d_ago * 100; lines.push(`90-day return: ${ret90 >= 0 ? "+" : ""}${ret90.toFixed(1)}%`); } if (snapshot.vol_30d) { lines.push(`30-day annualized volatility: ${(snapshot.vol_30d * 100).toFixed(1)}%`); } return lines.join("\n"); } function httpGet(url, headers) { return new Promise((resolve, reject) => { const u = new URL(url); const req = https.request({ hostname: u.hostname, path: u.pathname + u.search, method: "GET", headers, }, (res) => { let data = ""; res.on("data", chunk => data += chunk); res.on("end", () => { if (res.statusCode >= 200 && res.statusCode < 300) resolve(data); else reject(new Error(`yahoo ${res.statusCode}: ${data.slice(0, 200)}`)); }); }); req.on("error", reject); req.end(); }); } module.exports = { getPriceContext, formatPriceContext };