← back to Rentv Website

scripts/pull-ticker.mjs

56 lines

#!/usr/bin/env node
// Live ticker engine for rentv-v1 — writes data/ticker.json the header serves.
// LIVE (free, no key): 10-YR Treasury yield (^TNX) + CRE index (Dow Jones U.S. Real
// Estate ^DJUSRE) via Yahoo Finance. Market fundamentals (LA office vac, IE industrial
// cap) are quarterly figures kept as maintainable config here (not real-time-fetchable free).
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 FILE = join(OUT, 'ticker.json');
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/537.36 RENTV-v1';

// Quarterly CRE fundamentals — update from CBRE/JLL/Colliers market reports.
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' },
};
// Rebase factor so the Dow Jones U.S. Real Estate index reads as a ~4,000s "RENTV CRE Index"
// (matches the site's index scale; % change is identical to the underlying real index).
const CRE_SCALE = 10.51;

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 j = await r.json();
  const m = j?.chart?.result?.[0]?.meta;
  if (!m || m.regularMarketPrice == null) throw new Error('no price for ' + sym);
  return { price: m.regularMarketPrice, prev: m.chartPreviousClose ?? m.previousClose ?? m.regularMarketPrice };
}

const prior = (() => { try { return JSON.parse(readFileSync(FILE, 'utf8')); } catch { return {}; } })();
const pct = (p, prev) => prev ? +(((p - prev) / prev) * 100).toFixed(2) : 0;
const dir = (d) => d > 0.001 ? 'up' : d < -0.001 ? 'down' : 'flat';

const out = { fetched_at: new Date().toISOString(), ...FUNDAMENTALS };

// CRE INDEX (Dow Jones U.S. Real Estate, rebased)
try {
  const d = await yahoo('^DJUSRE');
  const cp = pct(d.price, d.prev);
  out.cre_index = { label: 'CRE INDEX', value: Math.round(d.price * CRE_SCALE).toLocaleString('en-US'),
    change_pct: cp, dir: dir(cp), source: 'Dow Jones U.S. Real Estate' };
} catch (e) { out.cre_index = prior.cre_index || { label: 'CRE INDEX', value: '—', change_pct: 0, dir: 'flat' }; out.cre_index.stale = true; }

// 10-YR Treasury yield (^TNX quotes the yield directly)
try {
  const d = await yahoo('^TNX');
  const bps = Math.round((d.price - d.prev) * 100);
  out.ten_yr = { label: '10-YR', value: d.price.toFixed(2) + '%', change_bps: bps, dir: dir(bps), source: 'CBOE 10-Yr (^TNX)' };
} catch (e) { out.ten_yr = prior.ten_yr || { label: '10-YR', value: '—', change_bps: 0, dir: 'flat' }; out.ten_yr.stale = true; }

writeFileSync(FILE, JSON.stringify(out, null, 2));
console.log('ticker.json:', out.cre_index.value, '| 10-YR', out.ten_yr.value,
  '| CRE', out.cre_index.change_pct + '%', out.cre_index.stale ? '(stale)' : '');