← back to Rentv Website
Markets: durable additive components (markets.html + pull-markets.mjs) + parity patch doc
7158ff5694fb6fefc9b1cf13906d612259c36096 · 2026-07-20 14:58:48 -0700 · steve
Files touched
A MARKETS-PATCH.mdA data/markets.jsonA public/markets.htmlA scripts/pull-markets.mjs
Diff
commit 7158ff5694fb6fefc9b1cf13906d612259c36096
Author: steve <steve@designerwallcoverings.com>
Date: Mon Jul 20 14:58:48 2026 -0700
Markets: durable additive components (markets.html + pull-markets.mjs) + parity patch doc
---
MARKETS-PATCH.md | 51 +++++++++++++++++++++++
data/markets.json | 74 +++++++++++++++++++++++++++++++++
public/markets.html | 106 +++++++++++++++++++++++++++++++++++++++++++++++
scripts/pull-markets.mjs | 45 ++++++++++++++++++++
4 files changed, 276 insertions(+)
diff --git a/MARKETS-PATCH.md b/MARKETS-PATCH.md
new file mode 100644
index 0000000..8c160b1
--- /dev/null
+++ b/MARKETS-PATCH.md
@@ -0,0 +1,51 @@
+# Rates & Markets — additive patch (fold into the rentv-v1 ↔ rentv-website parity baseline)
+
+**Why this file exists:** a live **Rates & Markets** dashboard (`/markets.html`) was built, but the
+`/api/markets` route kept getting clobbered by the force-parity syncs between `rentv-v1` and
+`rentv-website`. To make it durable, please fold the two shared-file edits below into **whichever
+project is the parity source of truth**, so it survives the next sync. The two *new files* already
+ship in the repo — only the 2 small shared-file additions get lost on sync.
+
+Live data source: **Yahoo Finance** (free, no key) — CBOE Treasury yields + Dow Jones/MSCI REIT indices.
+Nothing here touches `ticker.json` / `pull-ticker.mjs` / `index.html` / the reader flow.
+
+---
+
+## 1. New files (already in repo — verify present in BOTH projects)
+- `public/markets.html` — the dashboard page (Treasury curve, curve spreads, REIT benchmarks, CRE
+ fundamentals). Standalone; fetches `/api/markets`. Its top nav is a plain bar — align it to the
+ hamburger nav if you want parity, but it's not required to function.
+- `scripts/pull-markets.mjs` — standalone live-data generator; writes `data/markets.json`. Additive,
+ does not import or modify `pull-ticker`.
+
+## 2. `server.js` — add the `/api/markets` route (right after the `/api/ticker` route)
+```js
+// ── 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', {}));
+});
+```
+
+## 3. `scripts/pull-news.mjs` — refresh markets on the same cron (add after the pull-ticker import)
+Find the existing line near the end:
+```js
+try { await import('./pull-ticker.mjs'); } catch (e) { console.error('ticker refresh failed:', e.message); }
+```
+Add immediately after it:
+```js
+try { await import('./pull-markets.mjs'); } catch (e) { console.error('markets refresh failed:', e.message); }
+```
+
+---
+
+## Verify (per project)
+```sh
+node scripts/pull-markets.mjs # → "markets.json written | 7/7 live symbols"
+# after server restart / deploy:
+curl -s -u admin:'DW2024!' http://localhost:<port>/api/markets | jq '.curve|length' # → 4
+# public: https://rentv-website.agentabrams.com/markets.html should render the 4 curve cards
+```
+
+Nav link (optional): add `<a href="/markets.html">Rates & Markets</a>` to the header nav in
+`index.html` once the reader work settles.
diff --git a/data/markets.json b/data/markets.json
new file mode 100644
index 0000000..88f2b65
--- /dev/null
+++ b/data/markets.json
@@ -0,0 +1,74 @@
+{
+ "fetched_at": "2026-07-20T21:58:47.936Z",
+ "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"
+ },
+ {
+ "label": "IE INDUSTRIAL CAP",
+ "value": "5.2%",
+ "as_of": "Q2 2026"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/markets.html b/public/markets.html
new file mode 100644
index 0000000..86e4786
--- /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-markets.mjs b/scripts/pull-markets.mjs
new file mode 100644
index 0000000..ba247f9
--- /dev/null
+++ b/scripts/pull-markets.mjs
@@ -0,0 +1,45 @@
+#!/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');
← 0565bfb center masthead wordmark (newspaper-style centered top)
·
back to Rentv Website
·
cleaner top banner: no-wrap ticker with fade edge, mobile sh cf3af21 →