repos
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 &middot; ET{% else %}US equities &middot; 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 &amp; % 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&rsquo;s moving</h2>
      <span class="sectors__note">sectors &middot; 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&hellip;{% endif %}</span>
    <span class="reads-head__refresh" data-role="refresh-state" hidden><span class="reads-head__spinner" aria-hidden="true"></span>Refreshing&hellip;</span>
  </div>
  <section class="reads">
    <div class="read read--lead">
      <div class="read__label">S&amp;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 &middot; 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 &middot; 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 %}&mdash;{% endif %}</div>
    </div>

    <div class="read">
      <div class="read__label">S&amp;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 &amp; 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">&times;</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&amp;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 %}