← back to Rentv V1
scripts/pull-ticker.mjs
63 lines
#!/usr/bin/env node
// Live market engine for rentv-v1 — writes data/ticker.json (header strip) and
// data/markets.json (the /markets Rates & Markets dashboard). LIVE via Yahoo Finance
// (free, no key): Treasury curve (3mo/5yr/10yr/30yr) + REIT indices (DJUSRE/RMZ/VNQ).
// LA office vacancy + IE industrial cap are quarterly figures kept as config below.
import { writeFileSync, readFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const OUT = join(HERE, '..', 'data'); mkdirSync(OUT, { recursive: true });
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/537.36 RENTV-v1';
const FUNDAMENTALS = {
la_office_vac: { label: 'LA OFFICE VAC', value: '23.1%', as_of: 'Q2 2026', source: 'CBRE/JLL market reports' },
ie_industrial_cap:{ label: 'IE INDUSTRIAL CAP',value: '5.2%', as_of: 'Q2 2026', source: 'Colliers IE industrial' },
};
const CRE_SCALE = 10.51; // rebase DJ U.S. Real Estate to the site's ~4,000s index scale
async function yahoo(sym) {
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(sym)}?interval=1d&range=2d`;
const r = await fetch(url, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(15000) });
const m = (await r.json())?.chart?.result?.[0]?.meta;
if (!m || m.regularMarketPrice == null) throw new Error('no price ' + sym);
return { price: m.regularMarketPrice, prev: m.chartPreviousClose ?? m.previousClose ?? m.regularMarketPrice };
}
const dir = d => d > 0.001 ? 'up' : d < -0.001 ? 'down' : 'flat';
const pct = (p, pv) => pv ? +(((p - pv) / pv) * 100).toFixed(2) : 0;
const bps = (p, pv) => Math.round((p - pv) * 100);
const q = async (sym) => { try { return await yahoo(sym); } catch { return null; } };
// Fetch everything once
const [irx, fvx, tnx, tyx, djusre, rmz, vnq] = await Promise.all(
['^IRX', '^FVX', '^TNX', '^TYX', '^DJUSRE', '^RMZ', 'VNQ'].map(q));
// ── ticker.json (header strip) ──
const prior = (() => { try { return JSON.parse(readFileSync(join(OUT, 'ticker.json'), 'utf8')); } catch { return {}; } })();
const ticker = { fetched_at: new Date().toISOString(), ...FUNDAMENTALS };
ticker.cre_index = djusre
? { label: 'CRE INDEX', value: Math.round(djusre.price * CRE_SCALE).toLocaleString('en-US'), change_pct: pct(djusre.price, djusre.prev), dir: dir(pct(djusre.price, djusre.prev)), source: 'Dow Jones U.S. Real Estate' }
: { ...(prior.cre_index || { label: 'CRE INDEX', value: '—', dir: 'flat' }), stale: true };
ticker.ten_yr = tnx
? { label: '10-YR', value: tnx.price.toFixed(2) + '%', change_bps: bps(tnx.price, tnx.prev), dir: dir(bps(tnx.price, tnx.prev)), source: 'CBOE 10-Yr (^TNX)' }
: { ...(prior.ten_yr || { label: '10-YR', value: '—', dir: 'flat' }), stale: true };
writeFileSync(join(OUT, 'ticker.json'), JSON.stringify(ticker, null, 2));
// ── markets.json (Rates & Markets dashboard) ──
const rate = (label, d) => d ? { label, value: d.price.toFixed(2) + '%', change_bps: bps(d.price, d.prev), dir: dir(bps(d.price, d.prev)) } : { label, value: '—', dir: 'flat', stale: true };
const idx = (label, d, prefix = '') => d ? { label, value: prefix + d.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }), change_pct: pct(d.price, d.prev), dir: dir(pct(d.price, d.prev)) } : { label, value: '—', dir: 'flat', stale: true };
const spread = (label, a, b) => (a && b) ? (() => { const s = Math.round((a.price - b.price) * 100); return { label, value: (s >= 0 ? '+' : '') + s + ' bps', dir: dir(s) }; })() : { label, value: '—', dir: 'flat' };
const markets = {
fetched_at: new Date().toISOString(),
source: 'Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)',
curve: [ rate('3-Month', irx), rate('5-Year', fvx), rate('10-Year', tnx), rate('30-Year', tyx) ],
spreads: [ spread('30Y – 10Y (curve)', tyx, tnx), spread('10Y – 3M (curve)', tnx, irx) ],
reits: [ idx('DJ U.S. Real Estate', djusre), idx('MSCI U.S. REIT', rmz), idx('Vanguard REIT ETF (VNQ)', vnq, '$') ],
fundamentals: [ FUNDAMENTALS.la_office_vac, FUNDAMENTALS.ie_industrial_cap ],
};
writeFileSync(join(OUT, 'markets.json'), JSON.stringify(markets, null, 2));
const live = [irx, fvx, tnx, tyx, djusre, rmz, vnq].filter(Boolean).length;
console.log(`ticker+markets written | CRE ${ticker.cre_index.value} | 10-YR ${ticker.ten_yr.value} | ${live}/7 live symbols`);