repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

9.2 KB · 170 lines · HTML Raw History
  1{% extends "base.html" %}
  2{% block title %}Markets{% endblock %}
  3{% block description %}A live market overview — the major indexes, gold, crude, and bitcoin, each its own day chart — plus your personal watchlist.{% endblock %}
  4{% block extra_css %}<link rel="stylesheet" href="{{ vite_asset('static_src/home/index.js', 'css') }}">{% endblock %}
  5
  6{% block main %}
  7<div class="wrap dash">
  8
  9  {# Market-hours banner: makes the current session unmistakable. The dot colour
 10     keys off data-session; hero.js keeps it live as the session flips. #}
 11  <div class="dash-banner" data-role="session-banner" data-session="{{ session }}">
 12    <span class="dash-banner__dot"></span>
 13    <span class="dash-banner__label" data-role="session-label">{{ session_label }}</span>
 14    <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>
 15  </div>
 16
 17  {# The market overview: one chart per instrument (the major indexes + gold,
 18     crude and bitcoin), each on its own axis showing the day's actual values
 19     (points or dollars) with its live value + % change from the open. Cash
 20     indexes during the regular session, the E-mini futures off-hours. hero.js
 21     builds the cards into the grid from /api/dashboard; this is its mount point. #}
 22  <section class="hero-graph">
 23    <div class="hero-graph__head">
 24      <h1 class="hero-graph__title">Market overview</h1>
 25      <span class="hero-graph__note">value &amp; % change vs prev close</span>
 26    </div>
 27    {# Until /api/dashboard answers, the grid shows one shimmer-skeleton card
 28       per overview slot — same shape as the real sparkline cards, so the page
 29       doesn't jump when they land. hero.js hides this wrapper (display: contents,
 30       so the skeletons are real grid items) on the first draw. #}
 31    <div class="overview-grid" data-role="overview-grid">
 32      <div class="ov-skels" data-role="hero-empty" aria-hidden="true">
 33        {% for t in overview_tickers %}{% if loop.index <= 6 %}
 34        <div class="ov-card ov-card--skel">
 35          <span class="ov-skel ov-skel--name"></span>
 36          <span class="ov-skel ov-skel--num"></span>
 37          <div class="ov-card__chart ov-skel ov-skel--chart"></div>
 38        </div>
 39        {% endif %}{% endfor %}
 40      </div>
 41    </div>
 42  </section>
 43
 44  {# Overview symbols as hidden data-ticker nodes: the live stream registers them
 45     with the interest registry so the demand-driven intraday poll keeps their
 46     bars fresh while the dashboard is open. The graph itself is drawn from
 47     /api/dashboard; these carry no fields, so quote events no-op on them. #}
 48  <div class="overview-interest" hidden aria-hidden="true">
 49    {% for t in overview_tickers %}<span data-ticker="{{ t }}"></span>{% endfor %}
 50  </div>
 51
 52  {# Sector heatmap: the 11 SPDR sector ETFs, coloured by today's move, so the
 53     dashboard answers "what part of the market is driving it". hero.js fills the
 54     grid from /api/dashboard; the hidden interest nodes keep the ETFs polled. #}
 55  <section class="sectors">
 56    <div class="sectors__head">
 57      <h2 class="section-title">What&rsquo;s moving</h2>
 58      <span class="sectors__note">sectors &middot; latest-session % move</span>
 59    </div>
 60    <div class="sectors__grid" data-role="sectors-grid" aria-busy="true"></div>
 61  </section>
 62  <div class="sectors-interest" hidden aria-hidden="true">
 63    {% for t in sector_tickers %}<span data-ticker="{{ t }}"></span>{% endfor %}
 64  </div>
 65
 66  {# Headline market reads (context the graph doesn't carry). VIX and SPY carry
 67     data-ticker only (to keep them polled) and are patched by hero.js, which also
 68     fills volume + the S&P trend. The S&P price itself lives on the graph now. #}
 69  <div class="reads-head">
 70    <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>
 71    <span class="reads-head__refresh" data-role="refresh-state" hidden><span class="reads-head__spinner" aria-hidden="true"></span>Refreshing&hellip;</span>
 72  </div>
 73  <section class="reads">
 74    <div class="read read--lead">
 75      <div class="read__label">S&amp;P from high</div>
 76      <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>
 77      <div class="read__sub" data-role="drawdown-label">{{ reads.drawdown_label or "Drawdown from record close" }}</div>
 78    </div>
 79
 80    <div class="read" data-ticker="{{ vix }}">
 81      <div class="read__label">Volatility &middot; VIX</div>
 82      <div class="read__value num" data-role="vix-level">{{ reads.vix_level|round(2) if reads.vix_level is not none else "—" }}</div>
 83      <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>
 84    </div>
 85
 86    <div class="read" data-ticker="{{ credit_proxy }}">
 87      <div class="read__label">Credit &middot; high yield</div>
 88      <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>
 89      <div class="read__sub" data-role="credit-label">{{ reads.credit_label or "HYG vs prev close" }}</div>
 90    </div>
 91
 92    <div class="read" data-ticker="{{ volume_proxy }}">
 93      <div class="read__label">Market volume</div>
 94      <div class="read__value num" data-role="volume">{{ reads.volume|compact if reads.volume is not none else "—" }}</div>
 95      <div class="read__sub" data-role="volume-label">{% if reads.volume_label %}{{ reads.volume_label }} vs avg{% else %}&mdash;{% endif %}</div>
 96    </div>
 97
 98    <div class="read">
 99      <div class="read__label">S&amp;P trend</div>
100      <div class="read__value read__value--text read__tone--{{ reads.sma_tone or 'steady' }}" data-role="sma-read">{{ reads.sma_read or "—" }}</div>
101      <div class="read__sub">50 &amp; 200-day average</div>
102    </div>
103  </section>
104
105  {# The editable, session-scoped watchlist. #}
106  <section class="watch">
107    <div class="watch__head">
108      <h2 class="section-title">Watchlist</h2>
109      <form class="watch-add" data-role="watch-add" autocomplete="off">
110        <input class="watch-add__input" name="ticker" type="text" inputmode="text"
111               placeholder="Add a symbol — e.g. AAPL" aria-label="Add a symbol to your watchlist" maxlength="15">
112        <button class="watch-add__btn" type="submit">Add</button>
113      </form>
114    </div>
115    <p class="watch-msg" data-role="watch-msg" role="status" hidden></p>
116
117    {% if cards %}
118    {# Same chart-card shell as the overview. data-ticker registers the symbol
119       with the live stream (so its intraday bars stay polled); hero.js draws the
120       Schwab-day chart + refreshes the value/% from /api/dashboard. The remove
121       button sits outside the link so the whole card stays navigable. #}
122    <div class="overview-grid watch-grid">
123      {% for c in cards %}
124      <div class="ov-card ov-card--watch" data-ticker="{{ c.ticker }}" data-unit="{{ c.unit }}">
125        <a class="ov-card__link" href="/s/{{ c.ticker|urlencode }}">
126          <div class="ov-card__name ov-card__name--watch">{{ c.ticker }}<span class="ov-card__sub">{{ c.name }}</span></div>
127          <div class="ov-card__nums">
128            <span class="ov-card__value num">{{ c.price|money }}</span>
129            <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>
130          </div>
131          <div class="ov-card__chart"></div>
132        </a>
133        <button class="ov-card__remove" type="button" data-remove data-ticker="{{ c.ticker }}" aria-label="Remove {{ c.ticker }} from watchlist" title="Remove">&times;</button>
134      </div>
135      {% endfor %}
136    </div>
137    {% else %}
138    <p class="watch-empty">Your watchlist is empty. Add a stock or ETF above, or
139      <a href="/search">browse the universe</a> to find one.</p>
140    {% endif %}
141  </section>
142
143  {# Market movers: top gainers / losers / most active, the "what specific names
144     moved and why" detail. hero.js fills the three lists from /api/movers (an
145     8-minute server-side cache), after first paint so it never blocks the page. #}
146  <section class="movers">
147    <div class="movers__head">
148      <h2 class="section-title">Market movers <span class="movers__scope">S&amp;P 500</span></h2>
149      <span class="movers__asof" data-role="movers-asof"></span>
150    </div>
151    <div class="movers__cols" data-role="movers" aria-busy="true">
152      <div class="movers__col" data-mv="gainers">
153        <h3 class="movers__title movers__title--up">Gainers</h3>
154        <div class="movers__list"></div>
155      </div>
156      <div class="movers__col" data-mv="losers">
157        <h3 class="movers__title movers__title--down">Losers</h3>
158        <div class="movers__list"></div>
159      </div>
160      <div class="movers__col" data-mv="actives">
161        <h3 class="movers__title">Most active</h3>
162        <div class="movers__list"></div>
163      </div>
164    </div>
165  </section>
166</div>
167{% endblock %}
168
169{% block extra_js %}<script type="module" src="{{ vite_asset('static_src/home/index.js') }}"></script>{% endblock %}