← 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); });