← back to Rentv Website
scripts/pull-markets.mjs
46 lines
#!/usr/bin/env node
// Standalone Rates & Markets engine for rentv-website — writes data/markets.json
// (the /markets.html dashboard). Additive: does NOT touch ticker.json / pull-ticker.
// LIVE via Yahoo Finance (free, no key): Treasury curve + REIT indices.
import { writeFileSync, 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';
const FUNDAMENTALS = [
{ label: 'LA OFFICE VAC', value: '23.1%', as_of: 'Q2 2026' },
{ label: 'IE INDUSTRIAL CAP',value: '5.2%', as_of: 'Q2 2026' },
];
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; } };
const [irx, fvx, tnx, tyx, djusre, rmz, vnq] = await Promise.all(
['^IRX', '^FVX', '^TNX', '^TYX', '^DJUSRE', '^RMZ', 'VNQ'].map(q));
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,
};
writeFileSync(join(OUT, 'markets.json'), JSON.stringify(markets, null, 2));
console.log('markets.json written |', [irx, fvx, tnx, tyx, djusre, rmz, vnq].filter(Boolean).length, '/7 live symbols');