@@ -32,9 +32,58 @@ commit + auto-deploy (`git push server master`) and a clean breakpoint.## Status_Last updated: 2026-06-04 (Phases A-D all built + verified on dev; roadmapcomplete; everything uncommitted, holding per the user's call to batch thecommit/deploy).__Last updated: 2026-06-04 (Phases A-E all done + deployed. Phases A-D shipped in`818cf58`; **Phase E — market-overview graph — committed + deployed** this roundper new user direction.)_Phase E outcome (market-overview graph, split from the watchlist):- **Top graph is now a fixed, session-aware market overview**, not the watchlist. `src/routes/home.rs` carries an `OVERVIEW` slot table + `overview_for(session)`; `/api/dashboard` feeds the normalized %-from-open overlay from it (S&P the ink baseline). Verified at the close (session=closed): the graph drew the seven futures-mode lines — `ES=F` ("S&P 500"), `YM=F` (Dow), `NQ=F` (Nasdaq 100), `RTY=F` (Russell 2000), `GC=F` (Gold), `CL=F` (Crude Oil), `BTC-USD` (Bitcoin) — and the note read "% change from the open · index futures (off-hours)". During the regular session the four index slots swap to the cash indexes (`^SPX` etc.).- **Watchlist fully separate.** Its cards render below as before and are no longer on the graph; copy dropped the "S&P stays on the graph as baseline" line.- **VIX read-only** (off the overlay, where it would squash the scale); the reads strip was trimmed to VIX / Market volume / S&P 50-200 trend (dropped the now-redundant S&P price tile), desktop grid → 3 cols.- **Polling.** Overview tickers render as hidden `data-ticker` nodes so the stream registers them; `run_intraday`'s off-hours filter widened from `kind = 'future'` to `kind IN ('future','crypto')` so `BTC-USD` keeps polling overnight. `/api/dashboard/refresh` (on-open) now pulls the session's overview + `^VIX` + `SPY` + the watchlist (verified `refreshed: 14`).- **`BTC-USD` added** to `universe/starter.csv` as `kind = 'crypto'` (gets a real daily chart, unlike `=F` futures); `crypto` handled like index/future in search (filter pill + ordering), the refresh plan (price-only steps), and backfill (no SEC). `ES=F` already existed.- **Verified on dev:** `cargo build` + `vite build` clean, zero warnings; boot synced 563 symbols (BTC-USD added); `/api/dashboard` returns the futures-mode series with friendly names + S&P baseline and the trimmed reads; Playwright at 1280 and 390 rendered the overview graph, the off-hours note, the 3-tile reads strip, and the separate watchlist with **zero console errors**.- **Known rough edge (polish backlog):** the futures' overnight session boundary shows a small x-axis gap/step in the overlay (the ~23h window spans a settlement break, and a just-seeded symbol like BTC starts with few bars); lines align better once polled together during live hours. Same class of gap noted in Phase C; a candidate for the eventual polish pass, not a correctness issue.**Phase E direction (decided 2026-06-04).** After living with the Phase Cdashboard the user wants the top graph to stop being the watchlist and become afixed **market overview** ("at a glance, how is the whole market doing"), withthe **watchlist kept entirely separate** (its cards stay where they are, but areno longer drawn on the top graph). The overview is a fixed, non-editable set ofthe things the user usually tracks, and it is **session-aware**: cash indexesduring the regular session, the E-mini futures during pre / after-hours / closed(so the overview keeps moving overnight and shows where the market is heading).Answered four design questions (see the Decisions log): VIX stays a **read only**(it swings ~10x the indexes and would squash a normalized overlay); Nasdaq tracksthe **Nasdaq 100 (^NDX → NQ=F)** so the cash/futures swap is the same index; thereads strip is **kept but trimmed** (drop the now-redundant S&P price tile, keepVIX / Market volume / S&P 50-200 trend); the overview set is **fixed** (thewatchlist stays the only editable list). See Phase E in the Roadmap for the build.**Major refocus in progress (the "demand-only" rewrite).** The previous roadmap(the "distill + ETF-first" rewrite, Phases 1-7, all deployed at `645b351`) shipped
@@ -435,6 +484,52 @@ Rebuild home around the watchlist and the day graph. each session; watchlist refreshes on the ~5-minute cadence with live data-age; nothing polls once the dashboard tab is closed.### Phase E — Market-overview graph (split from the watchlist) ✅ DONE — committed + deployedMake the top graph a fixed, session-aware **market overview** and keep the**watchlist** entirely separate (cards only, off the graph).- **Overview set (fixed, non-editable), session-aware.** Seven slots, each shown as its cash instrument during the **regular** session and its E-mini future during **pre / after-hours / closed**; instruments that already trade ~24h use one ticker in both states: | Slot | Regular | Off-hours | |---|---|---| | S&P 500 | `^SPX` | `ES=F` | | Dow | `^DJI` | `YM=F` | | Nasdaq 100 | `^NDX` | `NQ=F` | | Russell 2000 | `^RUT` | `RTY=F` | | Gold | `GC=F` | `GC=F` | | Crude Oil | `CL=F` | `CL=F` | | Bitcoin | `BTC-USD` | `BTC-USD` | The S&P slot is the chart's ink baseline line; the rest take the palette. VIX is **not** on the graph (read only).- **Graph = the existing normalized %-from-open overlay**, just fed the overview set (via `overview_for(session)`) instead of the watchlist. Header copy becomes "Market overview", with an "index futures (off-hours)" note when the session is not regular (kept live by `hero.js`).- **Watchlist** is unchanged structurally (session cookie, add/remove, spark cards) but no longer appears on the top graph; its copy drops the "S&P stays on the graph as the baseline" line.- **Reads strip trimmed:** drop the S&P price tile (it's on the graph); keep VIX, Market volume, and the S&P 50/200-day trend. Desktop grid → 3 columns.- **Polling.** The page renders the overview tickers as hidden `data-ticker` nodes so the live stream registers them with the interest registry; the demand-driven `run_intraday` then keeps their intraday bars fresh while the dashboard is open. Off-hours `run_intraday` already restricts to symbols that trade ~24h — widened from `kind = 'future'` to `kind IN ('future','crypto')` so `BTC-USD` keeps polling overnight. `/api/dashboard/refresh` (on-open) pulls the session's overview set + `^VIX` + `SPY` + the watchlist.- **New universe rows:** `BTC-USD` (`kind = 'crypto'`; gets a real daily chart, unlike `=F` futures). `ES=F` already existed. `crypto` is handled like `index`/`future` everywhere it matters (search filter + ordering, refresh plan price-only steps, no SEC).- **Verify:** during the regular session the graph shows the four cash indexes + gold/crude/BTC; off-hours it swaps the four to ES/YM/NQ/RTY and the note reads "index futures (off-hours)"; VIX shows only in the reads strip; the watchlist cards render below and are absent from the graph; off-hours the overview keeps ticking (futures + BTC) while VIX/SPY/watchlist hold; zero console errors at mobile + desktop; `cargo build` + `vite build` clean.### Phase D — Cohesion + polish pass ✅ DONE on dev (commit + deploy pending)Removed the dead Summary machinery, trimmed `/health` to the demand-only job set +copy, added a "Prices as of" age to the dashboard reads, and fixed a verdict
@@ -454,6 +549,25 @@ block above for the full outcome. **This completes the demand-only roadmap.**## Decisions log**2026-06-04 — Phase E: split the dashboard into a market overview + a separatewatchlist.** The user wanted the top graph to read "how is the whole marketdoing" at a glance, not show the watchlist; the watchlist stays as its ownsection (cards) and comes off the graph. The overview is a fixed set the usertracks — S&P, Dow, Nasdaq, Russell 2000, gold, BTC, crude — plus VIX, and is**session-aware**: live cash indexes during regular hours, the E-mini futures(S&P mini, etc.) off-hours so it keeps moving overnight. Four questions answered:1. **VIX = read only, not on the graph.** On a normalized %-from-open overlay VIX swings ~10x the indexes and squashes the scale; it stays a headline read.2. **Nasdaq = Nasdaq 100 (`^NDX` → `NQ=F`).** Pairs cleanly with the E-mini so the cash/futures swap is the same index (not the Composite, which has no exact future).3. **Reads strip kept but trimmed.** Drop the now-redundant S&P price tile (it's on the graph); keep VIX, Market volume, and the S&P 50/200-day trend.4. **Overview set is fixed/non-editable.** The watchlist stays the only editable list; the overview is the user's curated market read.Budgeted into Phase E above. `BTC-USD` added to the universe as `kind = 'crypto'`(handled like index/future); `ES=F` already existed.**2026-06-03 — The "demand-only refocus" kickoff.** The user steered a focus shiftaway from the broad always-on dashboard: the app could not source enough live datato show what they wanted, and the timed sweeps were spending API budget on symbols
modified
frontend/static_src/home/scripts/hero.js
@@ -1,14 +1,16 @@// The dashboard's hero day graph + market reads (Phase C).// The dashboard's market-overview graph + market reads (Phase C / E).//// Draws every watchlist symbol plus the S&P 500 on one chart, each as % change// from today's open (the TradingView/Google "compare" shape), and fills the// headline reads. Both come from /api/dashboard, re-fetched ~every minute (and// on tab focus) so the chart and reads stay live without a reload — the// watchlist cards below already live-tick via the base stream client.// Draws the market overview — the major indexes plus gold, crude and bitcoin// (cash indexes during the regular session, the E-mini futures off-hours) — on// one chart, each as % change from today's open (the TradingView/Google// "compare" shape), and fills the headline reads. Both come from /api/dashboard,// re-fetched ~every minute (and on tab focus) so the chart and reads stay live// without a reload. The personal watchlist is a separate section below and is// not on this graph; its cards live-tick via the base stream client.import { createChart, LineSeries, ColorType } from "lightweight-charts";// Non-semantic line palette for the watchlist (green/amber/red stay reserved// Non-semantic line palette for the overview (green/amber/red stay reserved// for good/ok/bad reads elsewhere). Chosen to spread across the wheel so// adjacent lines stay tellable apart; the S&P baseline is drawn in ink.const PALETTE = [
@@ -24,8 +26,9 @@ const PALETTE = [const INK = "#211f1a";const DASH = "·";// Display name for a ticker on the axis label and legend.const displayName = (ticker) => (ticker === "^SPX" ? "S&P 500" : ticker);// The server sends a friendly `name` per series (e.g. "S&P 500", "Gold"); fall// back to the raw ticker if one is ever missing.const displayName = (s) => s.name || s.ticker;const SESSION_LABELS = { regular: "Regular session",
@@ -132,7 +135,7 @@ export function initHero() { lineWidth: s.baseline ? 2 : 1.75, // The title labels the line at its last value on the price axis, so // each line is identifiable without decoding the colour. title: displayName(s.ticker), title: displayName(s), priceLineVisible: false, lastValueVisible: true, crosshairMarkerRadius: 3,
@@ -140,11 +143,11 @@ export function initHero() { entry = { series }; seriesByTicker.set(s.ticker, entry); } else { entry.series.applyOptions({ color, title: displayName(s.ticker) }); entry.series.applyOptions({ color, title: displayName(s) }); } entry.series.setData(s.points.map((p) => ({ time: p.t, value: p.v }))); const last = s.points.length ? s.points[s.points.length - 1].v : null; legend.push({ ticker: s.ticker, color, baseline: s.baseline, last }); legend.push({ name: displayName(s), color, baseline: s.baseline, last }); } // Drop series whose ticker is no longer in the payload.
@@ -170,7 +173,7 @@ export function initHero() { sw.className = "legend-item__swatch"; sw.style.background = it.color; const name = document.createElement("span"); name.textContent = it.ticker === "^SPX" ? "S&P 500" : it.ticker; name.textContent = it.name; const pct = document.createElement("span"); pct.className = "legend-item__pct"; pct.textContent = it.last == null ? "" : pctFmt(it.last);
@@ -206,6 +209,12 @@ export function initHero() { ? "Prices update during market hours " + DASH + " ET" : "US equities " + DASH + " all times ET", ); // Mirror the overview's cash-vs-futures state on the graph note: outside the // regular session the indexes are drawn from their E-mini futures. setText( "overview-mode", session === "regular" ? "" : " " + DASH + " index futures (off-hours)", ); } async function refresh() {
modified
frontend/static_src/home/styles/home.scss
@@ -158,7 +158,9 @@}@media (min-width: $bp-sm) { .reads { grid-template-columns: repeat(4, minmax(0, 1fr)); // Three reads since Phase E (VIX, Market volume, S&P trend): the S&P price // tile moved onto the overview graph. grid-template-columns: repeat(3, minmax(0, 1fr)); }}
modified
src/routes/home.rs
@@ -31,7 +31,7 @@ pub fn router() -> Router<AppState> { .route("/api/dashboard/refresh", get(dashboard_refresh))}/// The S&P 500 cash index — the day graph's baseline and the headline read./// The S&P 500 cash index — the SMA-trend read and the day graph's baseline.const BASELINE: &str = "^SPX";/// The volatility gauge behind the VIX read.const VIX: &str = "^VIX";
@@ -40,6 +40,44 @@ const VIX: &str = "^VIX";/// while the dashboard is open (it carries a `data-ticker`) so it stays fresh.const VOLUME_PROXY: &str = "SPY";/// One slot in the fixed market-overview graph (Phase E). During the **regular**/// session the `cash` ticker is drawn (the live cash index); outside it (pre //// after-hours / closed) the `off` ticker is drawn instead — the E-mini future,/// which trades nearly 24h, so the overview keeps moving overnight and shows/// where the market is heading. Instruments that already trade ~24h (gold, crude,/// BTC) use the same ticker in both states.struct OverviewSlot { cash: &'static str, off: &'static str, name: &'static str, /// The S&P slot is drawn as the chart's ink baseline line. baseline: bool,}/// The market overview: a fixed, non-editable read of "how is the whole market/// doing", separate from the personal watchlist (which is cards only and no/// longer on this graph). VIX is deliberately absent — it swings ~10x the indexes/// and would squash a normalized %-from-open overlay; it stays a read instead.const OVERVIEW: &[OverviewSlot] = &[ OverviewSlot { cash: "^SPX", off: "ES=F", name: "S&P 500", baseline: true }, OverviewSlot { cash: "^DJI", off: "YM=F", name: "Dow", baseline: false }, OverviewSlot { cash: "^NDX", off: "NQ=F", name: "Nasdaq 100", baseline: false }, OverviewSlot { cash: "^RUT", off: "RTY=F", name: "Russell 2000", baseline: false }, OverviewSlot { cash: "GC=F", off: "GC=F", name: "Gold", baseline: false }, OverviewSlot { cash: "CL=F", off: "CL=F", name: "Crude Oil", baseline: false }, OverviewSlot { cash: "BTC-USD", off: "BTC-USD", name: "Bitcoin", baseline: false },];/// The overview tickers + display names for `session`: cash indexes during the/// regular session, the E-mini futures (and the ~24h instruments) otherwise.fn overview_for(session: market::Session) -> Vec<(&'static str, &'static str, bool)> { let regular = session == market::Session::Regular; OVERVIEW .iter() .map(|s| (if regular { s.cash } else { s.off }, s.name, s.baseline)) .collect()}/// A symbol's latest session = the intraday bars within this window of its most/// recent bar (regular+extended spans ~16h; the prior session sits ~24h back).const SESSION_WINDOW_MS: i64 = 23 * 3600 * 1000;
@@ -68,9 +106,6 @@ struct SparkCard {/// as a dash and is simply skipped by the live patcher.#[derive(Serialize, Default)]struct MarketReads { /// S&P 500 level + day move. spx_price: Option<f64>, spx_pct: Option<f64>, /// VIX level, its tone bucket (calm/steady/elevated/stressed), and a label. vix_level: Option<f64>, vix_tone: Option<String>,
@@ -127,14 +162,22 @@ async fn home(State(state): State<AppState>, headers: HeaderMap) -> Response { let reads = market_reads(&state).await; let market_session = market::session_at(chrono::Utc::now()); // The overview tickers for this session, rendered as hidden `data-ticker` // nodes so the live stream registers them with the interest registry and the // demand-driven intraday poll keeps their bars fresh while the page is open. let overview_tickers: Vec<&str> = overview_for(market_session) .into_iter() .map(|(t, _, _)| t) .collect(); let extra = minijinja::context! { title => "Markets", cards => cards, empty => tickers.is_empty(), reads => reads, baseline => BASELINE, vix => VIX, volume_proxy => VOLUME_PROXY, overview_tickers => overview_tickers, session => market_session.as_str(), session_label => session_label(market_session), };
@@ -157,34 +200,28 @@ async fn home(State(state): State<AppState>, headers: HeaderMap) -> Response {/// page (~every minute) so the chart and reads stay live without a reload. The/// series are normalized %-from-open so the watchlist and the S&P baseline share/// one axis (the TradingView/Google "compare" shape).async fn dashboard_api(State(state): State<AppState>, headers: HeaderMap) -> Response { let session = watchlist::resolve(&state.pool, &headers).await; let tickers = watchlist::list(&state.pool, &session.sid).await;async fn dashboard_api(State(state): State<AppState>) -> Response { // No session needed: the overview is fixed, not per-browser. (The watchlist // cards live-tick over the base stream; they are not on this graph.) let market_session = market::session_at(chrono::Utc::now()); // The baseline leads, then each watchlist symbol. let mut series = Vec::with_capacity(tickers.len() + 1); if let Some(s) = pct_series(&state, BASELINE, "S&P 500", true).await { series.push(s); } for t in &tickers { if let Some(s) = pct_series(&state, t, t, false).await { // The market-overview set for this session: cash indexes during the regular // session, the E-mini futures otherwise. The baseline (S&P) leads. let overview = overview_for(market_session); let mut series = Vec::with_capacity(overview.len()); for (ticker, name, baseline) in overview { if let Some(s) = pct_series(&state, ticker, name, baseline).await { series.push(s); } } let data = DashboardData { session: market::session_at(chrono::Utc::now()).as_str().to_string(), session: market_session.as_str().to_string(), reads: market_reads(&state).await, series, }; let mut resp = Json(data).into_response(); if let Some(c) = session.set_cookie { if let Ok(v) = header::HeaderValue::from_str(&c) { resp.headers_mut().insert(header::SET_COOKIE, v); } } resp Json(data).into_response()}/// `GET /api/dashboard/refresh` — the dashboard's on-open refresh. Pulls fresh
@@ -194,8 +231,13 @@ async fn dashboard_api(State(state): State<AppState>, headers: HeaderMap) -> Res/// reload doesn't re-hit Yahoo. Published quotes live-tick the open cards.async fn dashboard_refresh(State(state): State<AppState>, headers: HeaderMap) -> Response { let session = watchlist::resolve(&state.pool, &headers).await; // The watchlist cards, the session's overview symbols, and the VIX / volume // reads — everything the open dashboard shows gets a fresh quote. let mut tickers = watchlist::list(&state.pool, &session.sid).await; for b in [BASELINE, VIX, VOLUME_PROXY] { for (t, _, _) in overview_for(market::session_at(chrono::Utc::now())) { tickers.push(t.to_string()); } for b in [VIX, VOLUME_PROXY] { tickers.push(b.to_string()); } let refreshed =
@@ -266,14 +308,6 @@ async fn pct_series(state: &AppState, ticker: &str, name: &str, baseline: bool)async fn market_reads(state: &AppState) -> MarketReads { let mut r = MarketReads::default(); // S&P 500 level + day move. if let Some((last, prev)) = last_and_prev(state, BASELINE).await { r.spx_price = last; if let (Some(l), Some(p)) = (last, prev) { r.spx_pct = Some(compute::change(l, p).pct); } } // VIX level + tone. if let Some((Some(level), _)) = last_and_prev(state, VIX).await { r.vix_level = Some(level);
modified
src/routes/search.rs
@@ -53,7 +53,7 @@ async fn search_page(Query(sq): Query<SearchQuery>, State(state): State<AppState let query = raw.trim().to_uppercase(); // Normalise the kind filter to one of the three known kinds, else "all". let kind = match sq.kind.as_deref().map(str::trim).unwrap_or("") { k @ ("index" | "future" | "etf" | "stock") => k, k @ ("index" | "future" | "crypto" | "etf" | "stock") => k, _ => "", };
@@ -78,7 +78,7 @@ async fn search_page(Query(sq): Query<SearchQuery>, State(state): State<AppState AND (? = '' OR s.kind = ?) \ ORDER BY (s.ticker = ?) DESC, (s.ticker LIKE ? ESCAPE '\\') DESC, \ CASE s.kind WHEN 'index' THEN 0 WHEN 'future' THEN 1 \ WHEN 'etf' THEN 2 ELSE 3 END, s.ticker \ WHEN 'crypto' THEN 2 WHEN 'etf' THEN 3 ELSE 4 END, s.ticker \ LIMIT 240", ) .bind(&query)
modified
src/scheduler.rs
@@ -211,10 +211,11 @@ async fn run_boot_seed(pool: &SqlitePool, config: &Config) -> anyhow::Result<()>////// Which viewed symbols are polled depends on the session. Inside any trading/// session (pre, regular, post) every viewed symbol is fair game. Outside it,/// only viewed futures are polled: index futures and commodities trade nearly/// around the clock, while indexes, stocks and ETFs sit frozen until the next/// session, so polling them off-hours would only re-fetch a flat quote. This/// is what keeps the dashboard's commodity cards live overnight./// only viewed symbols that trade ~around the clock are polled — index futures,/// commodities, and crypto (BTC) — while indexes, stocks and ETFs sit frozen/// until the next session, so polling them off-hours would only re-fetch a flat/// quote. This is what keeps the dashboard's overview futures/commodity/BTC/// lines live overnight.////// A clean run is recorded only in `data_status` (plus each `quotes.fetched_at`/// row); a `fetch_log` row is written only for a notable run, an error or a
@@ -233,13 +234,19 @@ async fn run_intraday( let mut targets: Vec<String> = if session.is_open() { viewed } else { let futures: HashSet<String> = sqlx::query_scalar("SELECT ticker FROM symbols WHERE kind = 'future'") // Off-hours, only poll symbols that trade ~24h: index futures (and // commodities) plus crypto (BTC). Cash indexes / stocks / ETFs are // frozen, so polling them outside the session just burns budget. let around_clock: HashSet<String> = sqlx::query_scalar("SELECT ticker FROM symbols WHERE kind IN ('future', 'crypto')") .fetch_all(pool) .await? .into_iter() .collect(); viewed.into_iter().filter(|t| futures.contains(t)).collect() viewed .into_iter() .filter(|t| around_clock.contains(t)) .collect() }; if targets.is_empty() { return Ok(());
modified
templates/pages/home.html
@@ -1,6 +1,6 @@{% extends "base.html" %}{% block title %}Markets{% endblock %}{% block description %}A live, normalized view of your watchlist against the S&P 500, with market hours, volume, and volatility.{% endblock %}{% block description %}A live market overview — the major indexes, gold, crude, and bitcoin normalized for the day (index futures off-hours) — plus your personal watchlist.{% endblock %}{% block extra_css %}<link rel="stylesheet" href="{{ vite_asset('static_src/home/index.js', 'css') }}">{% endblock %}{% block main %}
@@ -14,13 +14,14 @@ <span class="dash-banner__sub" data-role="session-note">{% if session == 'closed' %}Prices update during market hours · ET{% else %}US equities · all times ET{% endif %}</span> </div> {# The hero day graph: every watchlist symbol plus the S&P 500, each as % change from today's open, on one axis. Drawn by hero.js from /api/dashboard; {# The market-overview graph: the major indexes + gold, crude and bitcoin, each as % change from today's open, on one axis. Cash indexes during the regular session, the E-mini futures off-hours. Drawn by hero.js from /api/dashboard; this is its mount point and a no-JS fallback line. #} <section class="hero-graph"> <div class="hero-graph__head"> <h1 class="hero-graph__title">Today, vs the S&P 500</h1> <span class="hero-graph__note">% change from the open · <span class="hero-graph__base">S&P 500 baseline</span></span> <h1 class="hero-graph__title">Market overview</h1> <span class="hero-graph__note">% change from the open<span class="hero-graph__mode" data-role="overview-mode">{% if session != 'regular' %} · index futures (off-hours){% endif %}</span></span> </div> <div class="hero-graph__chart" data-role="hero-chart"> <p class="hero-graph__empty" data-role="hero-empty">Loading today’s session…</p>
@@ -28,19 +29,21 @@ <div class="hero-graph__legend" data-role="hero-legend"></div> </section> {# Headline market reads. S&P carries data-ticker/data-field so the base stream client live-patches its price; VIX and SPY carry data-ticker only (to keep them polled) and are patched by hero.js, which also fills volume + trend. #} {# Overview symbols as hidden data-ticker nodes: the live stream registers them with the interest registry so the demand-driven intraday poll keeps their bars fresh while the dashboard is open. The graph itself is drawn from /api/dashboard; these carry no fields, so quote events no-op on them. #} <div class="overview-interest" hidden aria-hidden="true"> {% for t in overview_tickers %}<span data-ticker="{{ t }}"></span>{% endfor %} </div> {# Headline market reads (context the graph doesn't carry). VIX and SPY carry data-ticker only (to keep them polled) and are patched by hero.js, which also fills volume + the S&P trend. The S&P price itself lives on the graph now. #} <div class="reads-head"> <span class="reads-head__asof" data-role="reads-asof">{% if reads.asof %}Prices as of {{ reads.asof|asof }}{% else %}Awaiting first quotes…{% endif %}</span> </div> <section class="reads"> <div class="read" data-ticker="{{ baseline }}"> <div class="read__label">S&P 500</div> <div class="read__value num" data-field="price">{{ reads.spx_price|money }}</div> <div class="read__sub num{% if reads.spx_pct is none %} is-flat{% elif reads.spx_pct >= 0 %} is-up{% else %} is-down{% endif %}" data-field="change_pct">{{ reads.spx_pct|pct }}</div> </div> <div class="read" data-ticker="{{ vix }}"> <div class="read__label">Volatility · VIX</div> <div class="read__value num" data-role="vix-level">{{ reads.vix_level|round(2) if reads.vix_level is not none else "—" }}</div>
@@ -107,8 +110,7 @@ </div> {% else %} <p class="watch-empty">Your watchlist is empty. Add a stock or ETF above, or <a href="/search">browse the universe</a> to find one. The S&P 500 stays on the graph as the baseline.</p> <a href="/search">browse the universe</a> to find one.</p> {% endif %} </section></div>
modified
templates/pages/search.html
@@ -27,6 +27,7 @@ {"value": "", "label": "All"}, {"value": "index", "label": "Indexes"}, {"value": "future", "label": "Futures"}, {"value": "crypto", "label": "Crypto"}, {"value": "etf", "label": "ETFs"}, {"value": "stock", "label": "Stocks"} ] %}
modified
universe/starter.csv
@@ -561,3 +561,4 @@ GC=F,Gold Futures,future,COMEX,SI=F,Silver Futures,future,COMEX,HG=F,Copper Futures,future,COMEX,NG=F,Natural Gas Futures,future,NYMEX,BTC-USD,Bitcoin,crypto,,