← back to Abrams
scripts/market-data.js
100 lines
#!/usr/bin/env node
// market-data.js — Bloomberg-style index/bond/commodity/crypto snapshot for the Abrams Terminal.
// Uses Yahoo Finance's public spark endpoint (no auth, free).
// Falls back to Stooq if Yahoo errors. Writes data/market.json.
const https = require('https');
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const OUT = path.join(ROOT, 'data', 'market.json');
const LOG = path.join(ROOT, 'data', 'build-log.jsonl');
const log = (type, payload) => fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), type, ...payload }) + '\n');
const TICKERS = [
{ sym: 'SPY', name: 'S&P 500 ETF', cat: 'Stocks' },
{ sym: 'QQQ', name: 'Nasdaq-100 ETF', cat: 'Stocks' },
{ sym: 'DIA', name: 'Dow Industrials ETF', cat: 'Stocks' },
{ sym: 'IWM', name: 'Russell 2000 ETF', cat: 'Stocks' },
{ sym: '^VIX', name: 'CBOE Volatility', cat: 'Stocks' },
{ sym: '^TNX', name: '10-Year Treasury', cat: 'Bonds' },
{ sym: '^TYX', name: '30-Year Treasury', cat: 'Bonds' },
{ sym: '^IRX', name: '13-Week T-Bill', cat: 'Bonds' },
{ sym: 'GC=F', name: 'Gold Futures', cat: 'Commodities' },
{ sym: 'CL=F', name: 'WTI Crude Oil', cat: 'Commodities' },
{ sym: 'BTC-USD', name: 'Bitcoin', cat: 'Crypto' },
];
function fetchJson(url, timeoutMs = 4000) {
return new Promise(resolve => {
const req = https.get(url, {
timeout: timeoutMs,
headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_0) AbramsTerminal/1.0' }
}, r => {
let data = '';
r.on('data', c => data += c);
r.on('end', () => { try { resolve(JSON.parse(data)); } catch { resolve(null); } });
});
req.on('error', () => resolve(null));
req.on('timeout', () => { req.destroy(); resolve(null); });
});
}
async function fetchSpark(symbol) {
// Yahoo "spark" endpoint — free, no auth. Gives us last ~30 days of closes.
const url = `https://query1.finance.yahoo.com/v7/finance/spark?symbols=${encodeURIComponent(symbol)}&range=1mo&interval=1d&indicators=close&includeTimestamps=false&includePrePost=false`;
const j = await fetchJson(url);
const result = j?.spark?.result?.[0]?.response?.[0];
if (!result) return null;
const closes = (result.indicators?.quote?.[0]?.close || []).filter(x => x != null);
const meta = result.meta || {};
return {
symbol,
price: meta.regularMarketPrice ?? closes[closes.length - 1] ?? null,
previousClose: meta.previousClose ?? meta.chartPreviousClose ?? closes[closes.length - 2] ?? null,
dayHigh: meta.regularMarketDayHigh ?? null,
dayLow: meta.regularMarketDayLow ?? null,
fiftyTwoWeekHigh: meta.fiftyTwoWeekHigh ?? null,
fiftyTwoWeekLow: meta.fiftyTwoWeekLow ?? null,
sparkline: closes,
currency: meta.currency ?? 'USD',
exchangeName: meta.exchangeName ?? '',
};
}
async function main() {
const out = { generated_at: new Date().toISOString(), tickers: [] };
let okCount = 0, errCount = 0;
for (const t of TICKERS) {
try {
const d = await fetchSpark(t.sym);
if (d && d.price != null) {
const change = d.previousClose ? d.price - d.previousClose : 0;
const changePct = d.previousClose ? (change / d.previousClose) * 100 : 0;
out.tickers.push({
...t,
...d,
change: Number(change.toFixed(4)),
changePct: Number(changePct.toFixed(4)),
updated_at: new Date().toISOString(),
});
okCount++;
} else {
out.tickers.push({ ...t, error: 'fetch failed', sparkline: [] });
errCount++;
}
} catch (e) {
out.tickers.push({ ...t, error: String(e.message || e), sparkline: [] });
errCount++;
}
// courtesy delay so Yahoo doesn't throttle
await new Promise(r => setTimeout(r, 120));
}
fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
log('market-data', { ok: okCount, err: errCount, total: TICKERS.length });
console.log(`market: ${okCount}/${TICKERS.length} tickers → ${OUT}`);
}
main().catch(e => { console.error(e); process.exit(1); });