← back to Rentv 2026
Live Rates & Markets page (/markets.html): Treasury curve + REIT benchmarks + spreads via Yahoo; /api/markets
e8f63f034322109a281aae91e5181ab7a050c5db · 2026-07-20 14:29:24 -0700 · steve
Files touched
A data/markets.jsonA public/markets.htmlM scripts/pull-ticker.mjsM server.js
Diff
commit e8f63f034322109a281aae91e5181ab7a050c5db
Author: steve <steve@designerwallcoverings.com>
Date: Mon Jul 20 14:29:24 2026 -0700
Live Rates & Markets page (/markets.html): Treasury curve + REIT benchmarks + spreads via Yahoo; /api/markets
---
data/markets.json | 76 ++++++++++++++++++++++++++++++++++
public/markets.html | 106 ++++++++++++++++++++++++++++++++++++++++++++++++
scripts/pull-ticker.mjs | 71 +++++++++++++++++---------------
server.js | 6 +++
4 files changed, 227 insertions(+), 32 deletions(-)
diff --git a/data/markets.json b/data/markets.json
new file mode 100644
index 00000000..58fc6db9
--- /dev/null
+++ b/data/markets.json
@@ -0,0 +1,76 @@
+{
+ "fetched_at": "2026-07-20T21:27:36.267Z",
+ "source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
+ "curve": [
+ {
+ "label": "3-Month",
+ "value": "3.71%",
+ "change_bps": 0,
+ "dir": "flat"
+ },
+ {
+ "label": "5-Year",
+ "value": "4.33%",
+ "change_bps": 6,
+ "dir": "up"
+ },
+ {
+ "label": "10-Year",
+ "value": "4.60%",
+ "change_bps": 6,
+ "dir": "up"
+ },
+ {
+ "label": "30-Year",
+ "value": "5.12%",
+ "change_bps": 5,
+ "dir": "up"
+ }
+ ],
+ "spreads": [
+ {
+ "label": "30Y – 10Y (curve)",
+ "value": "+52 bps",
+ "dir": "up"
+ },
+ {
+ "label": "10Y – 3M (curve)",
+ "value": "+89 bps",
+ "dir": "up"
+ }
+ ],
+ "reits": [
+ {
+ "label": "DJ U.S. Real Estate",
+ "value": "397.62",
+ "change_pct": -0.58,
+ "dir": "down"
+ },
+ {
+ "label": "MSCI U.S. REIT",
+ "value": "1,289.55",
+ "change_pct": 0,
+ "dir": "flat"
+ },
+ {
+ "label": "Vanguard REIT ETF (VNQ)",
+ "value": "$99.48",
+ "change_pct": -0.59,
+ "dir": "down"
+ }
+ ],
+ "fundamentals": [
+ {
+ "label": "LA OFFICE VAC",
+ "value": "23.1%",
+ "as_of": "Q2 2026",
+ "source": "CBRE/JLL market reports"
+ },
+ {
+ "label": "IE INDUSTRIAL CAP",
+ "value": "5.2%",
+ "as_of": "Q2 2026",
+ "source": "Colliers IE industrial"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/markets.html b/public/markets.html
new file mode 100644
index 00000000..86e47860
--- /dev/null
+++ b/public/markets.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Rates & Markets — RENTV.com</title>
+<meta name="description" content="Live Treasury yield curve, REIT indices and Western-U.S. CRE fundamentals — RENTV.com Rates & Markets.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
+<style>
+ :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8;--up:#0f8a4d;--dn:#c8102e;--sans:'Inter',system-ui,sans-serif;--serif:'Playfair Display',Georgia,serif}
+ *{box-sizing:border-box;margin:0;padding:0}
+ body{font-family:var(--sans);color:var(--ink);background:var(--wash);line-height:1.45}
+ a{color:inherit;text-decoration:none}
+ .wrap{max-width:1200px;margin:0 auto;padding:0 20px}
+ .util{background:var(--ink);color:#cfd4d9;font-size:12px}
+ .util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}
+ .util .live{display:inline-flex;align-items:center;gap:7px;letter-spacing:.05em;text-transform:uppercase;font-size:11px}
+ .util .dot{width:7px;height:7px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
+ @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.6)}70%{box-shadow:0 0 0 7px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
+ .util .acts a{margin-left:14px;color:#cfd4d9}.util .acts a.sub{color:#fff;background:var(--red);padding:4px 12px;border-radius:3px;font-weight:700}
+ .mast{background:#fff;border-bottom:2px solid var(--ink);padding:16px 0 12px}
+ .mast .wrap{display:flex;align-items:center;justify-content:space-between}
+ .logo{font-family:var(--serif);font-weight:700;font-size:32px;letter-spacing:-1px}.logo span{color:var(--red)}
+ .logo small{display:block;font-size:11px;font-weight:600;letter-spacing:3px;color:var(--sub);text-transform:uppercase;margin-top:2px;font-family:var(--sans)}
+ nav{background:#fff;border-bottom:1px solid var(--line)}
+ nav .wrap{display:flex;gap:2px;align-items:center;height:46px;overflow-x:auto}
+ nav a{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:14px 13px;border-bottom:3px solid transparent;white-space:nowrap}
+ nav a:hover,nav a.on{color:var(--red);border-color:var(--red)}
+ .head{padding:30px 0 6px}
+ .head h1{font-family:var(--serif);font-size:34px;letter-spacing:-.5px}
+ .head p{color:var(--sub);font-size:14px;margin-top:4px}
+ .head .fresh{font-size:12px;color:var(--sub);margin-top:8px}
+ section{margin:26px 0}
+ h2.sec{font-size:14px;text-transform:uppercase;letter-spacing:.08em;border-top:3px solid var(--ink);padding-top:10px;margin-bottom:14px}
+ .cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
+ .cards.sp{grid-template-columns:repeat(2,1fr)}
+ .cards.re{grid-template-columns:repeat(3,1fr)}
+ .card{background:#fff;border:1px solid var(--line);border-radius:8px;padding:16px 18px}
+ .card .l{font-size:12px;color:var(--sub);text-transform:uppercase;letter-spacing:.05em}
+ .card .v{font-family:var(--serif);font-size:30px;font-weight:700;margin-top:4px;line-height:1}
+ .card .c{font-size:13px;font-weight:700;margin-top:6px}
+ .up{color:var(--up)}.dn{color:var(--dn)}.flat{color:var(--sub)}
+ .note{font-size:12px;color:var(--sub);margin-top:8px}
+ footer{background:var(--ink);color:#8b939b;margin-top:40px;padding:26px 0;font-size:12px}
+ footer .wrap{display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
+ @media(max-width:860px){.cards{grid-template-columns:1fr 1fr}.cards.re{grid-template-columns:1fr}}
+</style>
+</head>
+<body>
+ <div class="util"><div class="wrap">
+ <span class="live"><span class="dot"></span> Live market data</span>
+ <div class="acts"><a href="/">Home</a><a href="/review">The REview</a><a class="sub" href="/#news">Subscribe</a></div>
+ </div></div>
+ <header class="mast"><div class="wrap">
+ <a class="logo" href="/">REN<span>TV</span>.com<small>Commercial Real Estate News</small></a>
+ <div style="font-size:12px;color:var(--sub);text-align:right">Western U.S. Edition<br><span id="today"></span></div>
+ </div></header>
+ <nav><div class="wrap">
+ <a href="/">Home</a><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a>
+ <a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a>
+ <a href="/retail">Retail</a><a class="on" href="/markets.html">Rates & Markets</a>
+ </div></nav>
+
+ <main class="wrap">
+ <div class="head">
+ <h1>Rates & Markets</h1>
+ <p>The rate backdrop that moves commercial real estate — Treasury curve, REIT benchmarks and Western-U.S. fundamentals.</p>
+ <div class="fresh" id="fresh">Loading live market data…</div>
+ </div>
+
+ <section><h2 class="sec">U.S. Treasury Yield Curve</h2><div class="cards" id="curve"></div>
+ <div class="note">Higher long rates raise borrowing costs and pressure cap rates. Daily change shown in basis points.</div></section>
+ <section><h2 class="sec">Curve Spreads</h2><div class="cards sp" id="spreads"></div>
+ <div class="note">A steeper curve (positive spread) generally signals easier financing ahead; inversion has historically preceded slowdowns.</div></section>
+ <section><h2 class="sec">REIT Benchmarks</h2><div class="cards re" id="reits"></div>
+ <div class="note">Public real-estate equities lead private CRE pricing. Daily change shown as a percentage.</div></section>
+ <section><h2 class="sec">Western-U.S. Fundamentals</h2><div class="cards sp" id="fund"></div>
+ <div class="note">Market-report figures, updated quarterly (CBRE / JLL / Colliers).</div></section>
+ </main>
+
+ <footer><div class="wrap"><span>© RENTV.com — Commercial Real Estate News, Western U.S.</span><span>Market data via Yahoo Finance · delayed · informational only</span></div></footer>
+
+<script>
+document.getElementById('today').textContent=new Date().toLocaleDateString(undefined,{weekday:'long',year:'numeric',month:'long',day:'numeric'});
+const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+function chg(m){
+ if(m.change_bps!=null){const a=Math.abs(m.change_bps);return m.dir==='up'?'<span class="up">▲ '+a+' bps</span>':m.dir==='down'?'<span class="dn">▼ '+a+' bps</span>':'<span class="flat">— unch</span>';}
+ if(m.change_pct!=null){const a=Math.abs(m.change_pct);return m.dir==='up'?'<span class="up">▲ '+a+'%</span>':m.dir==='down'?'<span class="dn">▼ '+a+'%</span>':'<span class="flat">— unch</span>';}
+ return '';
+}
+const card=m=>'<div class="card"><div class="l">'+esc(m.label)+'</div><div class="v">'+esc(m.value)+'</div><div class="c">'+chg(m)+(m.as_of?'<span class="flat" style="font-weight:400"> · '+esc(m.as_of)+'</span>':'')+'</div></div>';
+async function load(){
+ try{
+ const d=await (await fetch('/api/markets')).json();
+ document.getElementById('curve').innerHTML=(d.curve||[]).map(card).join('');
+ document.getElementById('spreads').innerHTML=(d.spreads||[]).map(card).join('');
+ document.getElementById('reits').innerHTML=(d.reits||[]).map(card).join('');
+ document.getElementById('fund').innerHTML=(d.fundamentals||[]).map(card).join('');
+ const mins=Math.max(0,Math.round((Date.now()-new Date(d.fetched_at))/60000));
+ document.getElementById('fresh').textContent=(d.source||'Live market data')+' · '+(mins<2?'updated just now':'updated '+mins+' min ago');
+ }catch(e){document.getElementById('fresh').textContent='Market data temporarily unavailable — retrying…';}
+}
+load(); setInterval(load,120000);
+</script>
+</body>
+</html>
diff --git a/scripts/pull-ticker.mjs b/scripts/pull-ticker.mjs
index e2dfd3ca..4925b92c 100644
--- a/scripts/pull-ticker.mjs
+++ b/scripts/pull-ticker.mjs
@@ -1,55 +1,62 @@
#!/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).
+// 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 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;
+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 j = await r.json();
- const m = j?.chart?.result?.[0]?.meta;
- if (!m || m.regularMarketPrice == null) throw new Error('no price for ' + sym);
+ 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 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';
+// Fetch everything once
+const [irx, fvx, tnx, tyx, djusre, rmz, vnq] = await Promise.all(
+ ['^IRX', '^FVX', '^TNX', '^TYX', '^DJUSRE', '^RMZ', 'VNQ'].map(q));
-const out = { fetched_at: new Date().toISOString(), ...FUNDAMENTALS };
+// ── 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));
-// 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; }
+// ── 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' };
-// 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; }
+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));
-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)' : '');
+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`);
diff --git a/server.js b/server.js
index 4b2d8515..897d92d7 100644
--- a/server.js
+++ b/server.js
@@ -34,6 +34,12 @@ app.get('/api/ticker', (_q, r) => {
r.json(readJSON('ticker.json', {}));
});
+// ── Rates & Markets dashboard data (Treasury curve + REIT indices, live via Yahoo) ──
+app.get('/api/markets', (_q, r) => {
+ r.set('Cache-Control', 'public, max-age=120');
+ r.json(readJSON('markets.json', {}));
+});
+
// ── The REview: live CRE video catalog (synced from rentvreview.com by cron) ──
app.get('/api/videos', (_q, r) => {
r.set('Cache-Control', 'public, max-age=300');
← d9ec1da5 add in-shell article.html + section.html reader templates; w
·
back to Rentv 2026
·
auto-save: 2026-07-20T14:32:39 (2 files) — data/news.json da 71395b46 →