9.2 KB
raw
{% extends "base.html" %}
{% block title %}Markets{% endblock %}
{% block description %}A live market overview — the major indexes, gold, crude, and bitcoin, each its own day chart — plus your personal watchlist.{% endblock %}
{% block extra_css %}<link rel="stylesheet" href="{{ vite_asset('static_src/home/index.js', 'css') }}">{% endblock %}
{% block main %}
<div class="wrap dash">
{# Market-hours banner: makes the current session unmistakable. The dot colour
keys off data-session; hero.js keeps it live as the session flips. #}
<div class="dash-banner" data-role="session-banner" data-session="{{ session }}">
<span class="dash-banner__dot"></span>
<span class="dash-banner__label" data-role="session-label">{{ session_label }}</span>
<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 market overview: one chart per instrument (the major indexes + gold,
crude and bitcoin), each on its own axis showing the day's actual values
(points or dollars) with its live value + % change from the open. Cash
indexes during the regular session, the E-mini futures off-hours. hero.js
builds the cards into the grid from /api/dashboard; this is its mount point. #}
<section class="hero-graph">
<div class="hero-graph__head">
<h1 class="hero-graph__title">Market overview</h1>
<span class="hero-graph__note">value & % change vs prev close</span>
</div>
{# Until /api/dashboard answers, the grid shows one shimmer-skeleton card
per overview slot — same shape as the real sparkline cards, so the page
doesn't jump when they land. hero.js hides this wrapper (display: contents,
so the skeletons are real grid items) on the first draw. #}
<div class="overview-grid" data-role="overview-grid">
<div class="ov-skels" data-role="hero-empty" aria-hidden="true">
{% for t in overview_tickers %}{% if loop.index <= 6 %}
<div class="ov-card ov-card--skel">
<span class="ov-skel ov-skel--name"></span>
<span class="ov-skel ov-skel--num"></span>
<div class="ov-card__chart ov-skel ov-skel--chart"></div>
</div>
{% endif %}{% endfor %}
</div>
</div>
</section>
{# 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>
{# Sector heatmap: the 11 SPDR sector ETFs, coloured by today's move, so the
dashboard answers "what part of the market is driving it". hero.js fills the
grid from /api/dashboard; the hidden interest nodes keep the ETFs polled. #}
<section class="sectors">
<div class="sectors__head">
<h2 class="section-title">What’s moving</h2>
<span class="sectors__note">sectors · latest-session % move</span>
</div>
<div class="sectors__grid" data-role="sectors-grid" aria-busy="true"></div>
</section>
<div class="sectors-interest" hidden aria-hidden="true">
{% for t in sector_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>
<span class="reads-head__refresh" data-role="refresh-state" hidden><span class="reads-head__spinner" aria-hidden="true"></span>Refreshing…</span>
</div>
<section class="reads">
<div class="read read--lead">
<div class="read__label">S&P from high</div>
<div class="read__value num read__tone--{{ reads.drawdown_tone or 'steady' }}" data-role="drawdown-pct">{{ reads.drawdown_pct|pct if reads.drawdown_pct is not none else "—" }}</div>
<div class="read__sub" data-role="drawdown-label">{{ reads.drawdown_label or "Drawdown from record close" }}</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>
<div class="read__sub read__tone read__tone--{{ reads.vix_tone or 'steady' }}" data-role="vix-tone">{{ reads.vix_tone|capitalize if reads.vix_tone else "—" }}</div>
</div>
<div class="read" data-ticker="{{ credit_proxy }}">
<div class="read__label">Credit · high yield</div>
<div class="read__value num read__tone--{{ reads.credit_tone or 'steady' }}" data-role="credit-pct">{{ reads.credit_pct|pct if reads.credit_pct is not none else "—" }}</div>
<div class="read__sub" data-role="credit-label">{{ reads.credit_label or "HYG vs prev close" }}</div>
</div>
<div class="read" data-ticker="{{ volume_proxy }}">
<div class="read__label">Market volume</div>
<div class="read__value num" data-role="volume">{{ reads.volume|compact if reads.volume is not none else "—" }}</div>
<div class="read__sub" data-role="volume-label">{% if reads.volume_label %}{{ reads.volume_label }} vs avg{% else %}—{% endif %}</div>
</div>
<div class="read">
<div class="read__label">S&P trend</div>
<div class="read__value read__value--text read__tone--{{ reads.sma_tone or 'steady' }}" data-role="sma-read">{{ reads.sma_read or "—" }}</div>
<div class="read__sub">50 & 200-day average</div>
</div>
</section>
{# The editable, session-scoped watchlist. #}
<section class="watch">
<div class="watch__head">
<h2 class="section-title">Watchlist</h2>
<form class="watch-add" data-role="watch-add" autocomplete="off">
<input class="watch-add__input" name="ticker" type="text" inputmode="text"
placeholder="Add a symbol — e.g. AAPL" aria-label="Add a symbol to your watchlist" maxlength="15">
<button class="watch-add__btn" type="submit">Add</button>
</form>
</div>
<p class="watch-msg" data-role="watch-msg" role="status" hidden></p>
{% if cards %}
{# Same chart-card shell as the overview. data-ticker registers the symbol
with the live stream (so its intraday bars stay polled); hero.js draws the
Schwab-day chart + refreshes the value/% from /api/dashboard. The remove
button sits outside the link so the whole card stays navigable. #}
<div class="overview-grid watch-grid">
{% for c in cards %}
<div class="ov-card ov-card--watch" data-ticker="{{ c.ticker }}" data-unit="{{ c.unit }}">
<a class="ov-card__link" href="/s/{{ c.ticker|urlencode }}">
<div class="ov-card__name ov-card__name--watch">{{ c.ticker }}<span class="ov-card__sub">{{ c.name }}</span></div>
<div class="ov-card__nums">
<span class="ov-card__value num">{{ c.price|money }}</span>
<span class="ov-card__chg num {% if c.change_pct is none %}is-flat{% elif c.change_pct >= 0 %}is-up{% else %}is-down{% endif %}">{{ c.change_pct|pct }}</span>
</div>
<div class="ov-card__chart"></div>
</a>
<button class="ov-card__remove" type="button" data-remove data-ticker="{{ c.ticker }}" aria-label="Remove {{ c.ticker }} from watchlist" title="Remove">×</button>
</div>
{% endfor %}
</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.</p>
{% endif %}
</section>
{# Market movers: top gainers / losers / most active, the "what specific names
moved and why" detail. hero.js fills the three lists from /api/movers (an
8-minute server-side cache), after first paint so it never blocks the page. #}
<section class="movers">
<div class="movers__head">
<h2 class="section-title">Market movers <span class="movers__scope">S&P 500</span></h2>
<span class="movers__asof" data-role="movers-asof"></span>
</div>
<div class="movers__cols" data-role="movers" aria-busy="true">
<div class="movers__col" data-mv="gainers">
<h3 class="movers__title movers__title--up">Gainers</h3>
<div class="movers__list"></div>
</div>
<div class="movers__col" data-mv="losers">
<h3 class="movers__title movers__title--down">Losers</h3>
<div class="movers__list"></div>
</div>
<div class="movers__col" data-mv="actives">
<h3 class="movers__title">Most active</h3>
<div class="movers__list"></div>
</div>
</div>
</section>
</div>
{% endblock %}
{% block extra_js %}<script type="module" src="{{ vite_asset('static_src/home/index.js') }}"></script>{% endblock %}