43.3 KB
raw
{% extends "base.html" %}
{% block title %}{{ symbol.ticker }}{% endblock %}
{% block description %}{{ symbol.name }} ({{ symbol.ticker }}): price chart, key stats, and history.{% endblock %}
{% block extra_css %}<link rel="stylesheet" href="{{ vite_asset('static_src/symbol/index.js', 'css') }}">{% endblock %}
{% from "includes/macros.html" import verdict_badge %}
{# One financials table (annual or quarterly). Both are rendered; the toggle
script shows one. `active` decides which is visible on load. #}
{% macro fin_panel(table, period, active) %}
<div class="fin__panel" data-period="{{ period }}"{% if not active %} hidden{% endif %}>
{% if table.periods %}
<div class="fin__scroll">
<table class="fin__table">
<thead>
<tr><th scope="col"></th>{% for p in table.periods %}<th scope="col" class="num">{{ p }}</th>{% endfor %}</tr>
</thead>
<tbody>
{% for row in table.rows %}
<tr>
<th scope="row">{{ row.label }}</th>
{% for c in row.cells %}<td class="num{% if c.sense %} fin-cell--{{ c.sense }}{% endif %}">{{ c.display }}{% if c.dir %}<span class="fin-cell__dir fin-cell__dir--{{ c.dir }}" aria-hidden="true"></span>{% endif %}</td>{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="fin__empty">No {{ period }} figures have been reported.</p>
{% endif %}
</div>
{% endmacro %}
{% block main %}
<div class="wrap">
{# On-demand refresh control (Phase B). On load the page pulls the latest
data for this symbol (live price always; slow SEC / metadata only when
stale), showing the progress bar; the button re-pulls everything. The data
for each section carries its own "synced … ago" age in its heading. #}
<div class="refresh" data-refresh-root data-ticker="{{ symbol.ticker }}">
<button type="button" class="refresh__btn" data-refresh aria-label="Refresh all data">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6"/></svg>
<span>Refresh</span>
</button>
<span class="refresh__status" data-refresh-status aria-live="polite"></span>
<div class="refresh__bar" data-refresh-bar hidden><div class="refresh__fill" data-refresh-fill></div></div>
</div>
{% if stale_data %}
<div class="stale-banner" role="note">
<strong>No recent trading data.</strong> The last price we hold for {{ symbol.ticker }}
is from {{ stale_data.last_date|shortdate }} ({{ stale_data.days }} days ago). This symbol
may be delisted, renamed, or halted — the figures below reflect its last active
session, not the current market.
</div>
{% endif %}
<header class="sym-head{% if health or etf_quality %} sym-head--has-health{% endif %}">
<div class="sym-head__id">
<h1 class="sym-head__ticker">{{ symbol.ticker }}</h1>
<span class="sym-head__tag">{{ symbol.kind }}</span>
{% if symbol.exchange %}<span class="sym-head__tag">{{ symbol.exchange }}</span>{% endif %}
{% if symbol.sector %}<span class="sym-head__tag">{{ symbol.sector }}</span>{% endif %}
{% if symbol.industry %}<span class="sym-head__tag">{{ symbol.industry }}</span>{% endif %}
</div>
<div class="sym-head__name">{{ symbol.name }}</div>
{# Prefer the live quote; fall back to the most recent daily close. The
stream client patches the data-field nodes in place as quotes arrive. #}
{% if quote or stats %}
{% set price = quote.price if quote else stats.close %}
{% set chg_abs = quote.change_abs if quote else stats.change_abs %}
{% set chg_pct = quote.change_pct if quote else stats.change_pct %}
<div class="sym-head__quote" data-ticker="{{ symbol.ticker }}">
<span class="sym-head__price num" data-field="price">{{ price|money }}</span>
{% if chg_pct is not none %}
<span class="sym-head__chg num {{ 'is-up' if chg_pct >= 0 else 'is-down' }}" data-field="change">
{{ chg_abs|signed }} ({{ chg_pct|pct }})
</span>
{% endif %}
<span class="sym-head__asof">{% if quote %}{{ quote.state_label }}{% if symbol.last_quote_at %} · <span data-field="quoted">quoted {{ symbol.last_quote_at|ago }}</span>{% endif %}{% else %}Last close · {{ stats.date|shortdate }}{% endif %}</span>
</div>
{% endif %}
{# Mobile health verdict line (Phase 6): the same read the donut carries on
desktop, flattened into a one-line "verdict · % · trajectory" band that
sits directly under the price where a phone reads it first. Hidden on
desktop, where the donut (below) is the treatment. Works for a stock
(`health`, with a trajectory arrow) or an ETF (`etf_quality`, no
trajectory); a symbol only ever has one. #}
{% if health or etf_quality %}
{% set verdict = health if health else etf_quality %}
<div class="sym-verdict sym-verdict--{{ verdict.overall }}">
<span class="sym-verdict__dot" aria-hidden="true"></span>
<span class="sym-verdict__text">{{ verdict.verdict }}</span>
<span class="sym-verdict__pct num">{{ verdict.percent }}%</span>
{% if health and health.trajectory != 'unknown' %}
<span class="sym-verdict__traj sym-verdict__traj--{{ health.trajectory }}">
<span class="sym-verdict__arrow" aria-hidden="true">{% if health.trajectory == 'good' %}↑{% elif health.trajectory == 'bad' %}↓{% else %}→{% endif %}</span>{{ health.trajectory_label }}
</span>
{% endif %}
</div>
{% endif %}
{# Stock health badge (Phase 17, redesigned): a circular % donut anchors
the top-right of the header. Hovering or focusing reveals the three
sub-readings (fundamentals / trajectory / leadership) — the previous
full Stock-health section is gone in favour of this glanceable badge. #}
{% if health %}
<div class="sym-head__health">
<button type="button" class="health-badge health-badge--{{ health.overall }}"
aria-label="Stock health: {{ health.verdict }}, {{ health.percent }}%"
aria-describedby="health-pop">
{# r = 50/π ≈ 15.9155 makes the circle's circumference exactly 100,
so stroke-dasharray "N, 100" fills N% of the ring with no maths
in the template. #}
<svg class="health-badge__ring" viewBox="0 0 36 36" aria-hidden="true">
<circle class="health-badge__track" cx="18" cy="18" r="15.9155" fill="none"/>
<circle class="health-badge__fill" cx="18" cy="18" r="15.9155" fill="none"
stroke-dasharray="{{ health.percent }}, 100"/>
</svg>
<span class="health-badge__num num">
<span class="health-badge__pct">{{ health.percent }}</span><span class="health-badge__unit">%</span>
</span>
<span class="health-badge__caption">{{ health.verdict }}</span>
</button>
<div class="health-pop" id="health-pop" role="tooltip">
<p class="health-pop__head">Overall <strong class="health-pop__verdict health-pop__verdict--{{ health.overall }}">{{ health.verdict }}</strong> · <span class="num">{{ health.percent }}%</span></p>
<ul class="health-pop__rows">
<li class="health-pop__row health-pop__row--{{ health.strength }}">
<span class="health-pop__label">Fundamentals</span>
<span class="health-pop__value">{{ health.strength_label }}</span>
</li>
<li class="health-pop__row health-pop__row--{{ health.trajectory }}">
<span class="health-pop__label">Trajectory</span>
<span class="health-pop__value">{{ health.trajectory_label }}</span>
</li>
<li class="health-pop__row health-pop__row--{{ health.stability }}">
<span class="health-pop__label">Leadership</span>
<span class="health-pop__value">
{%- if health.stability == 'unknown' -%}
—
{%- else -%}
{{ health.stability_label }}
{%- endif -%}
</span>
</li>
</ul>
<p class="health-pop__src">A read across fundamentals, recent price & growth trajectory, and leadership stability. For fun and reading at a glance, not investment advice.</p>
</div>
</div>
{% endif %}
{# ETF quality badge (Phase 4): the fund-side sibling of the stock health
donut. Same glanceable % ring, but the four sub-readings are the wrapper's
quality — cost, tracking (price vs NAV), diversification, size. Reuses the
`health-badge` / `health-pop` styling; a symbol is either a stock or an
ETF, so only one of the two badges ever renders. #}
{% if etf_quality %}
<div class="sym-head__health sym-head__health--etf">
<button type="button" class="health-badge health-badge--{{ etf_quality.overall }}"
aria-label="ETF quality: {{ etf_quality.verdict }}, {{ etf_quality.percent }}%"
aria-describedby="etf-quality-pop">
<svg class="health-badge__ring" viewBox="0 0 36 36" aria-hidden="true">
<circle class="health-badge__track" cx="18" cy="18" r="15.9155" fill="none"/>
<circle class="health-badge__fill" cx="18" cy="18" r="15.9155" fill="none"
stroke-dasharray="{{ etf_quality.percent }}, 100"/>
</svg>
<span class="health-badge__num num">
<span class="health-badge__pct">{{ etf_quality.percent }}</span><span class="health-badge__unit">%</span>
</span>
<span class="health-badge__caption">{{ etf_quality.verdict }}</span>
</button>
<div class="health-pop" id="etf-quality-pop" role="tooltip">
<p class="health-pop__head">ETF quality <strong class="health-pop__verdict health-pop__verdict--{{ etf_quality.overall }}">{{ etf_quality.verdict }}</strong> · <span class="num">{{ etf_quality.percent }}%</span></p>
<ul class="health-pop__rows">
<li class="health-pop__row health-pop__row--{{ etf_quality.cost }}">
<span class="health-pop__label">Cost</span>
<span class="health-pop__value">{{ etf_quality.cost_label }}</span>
</li>
<li class="health-pop__row health-pop__row--{{ etf_quality.tracking }}">
<span class="health-pop__label">Tracking</span>
<span class="health-pop__value">{{ etf_quality.tracking_label }}</span>
</li>
<li class="health-pop__row health-pop__row--{{ etf_quality.diversification }}">
<span class="health-pop__label">Diversification</span>
<span class="health-pop__value">{{ etf_quality.diversification_label }}</span>
</li>
<li class="health-pop__row health-pop__row--{{ etf_quality.size }}">
<span class="health-pop__label">Size</span>
<span class="health-pop__value">{{ etf_quality.size_label }}</span>
</li>
</ul>
<p class="health-pop__src">A read on the fund wrapper: cost, how tightly it tracks NAV, diversification, and size. For reading at a glance, not investment advice.</p>
</div>
</div>
{% endif %}
</header>
{# Chart and key stats need daily history. A future has none by design
(live quotes only), and so do the historyless indexes; those fall to the
explanatory empty state below instead of an empty chart. #}
{% if stats %}
<section class="panel chart-panel">
<div class="chart-bar">
{# Daily-candle ranges. Default 1Y. (The old 1D / 1W intraday ranges were
dropped — the demand-only model can't keep enough 15m bars to draw them
legibly; short-range detail comes from the denser-candle ranges.) #}
<div class="range-bar">
{% for r in ["YTD", "1M", "3M", "6M", "1Y", "3Y", "5Y", "MAX"] %}
<button type="button" class="range-btn{% if r == '1Y' %} is-active{% endif %}" data-range="{{ r }}">{{ r }}</button>
{% endfor %}
</div>
{# Filled by chart.js after each load: the % / absolute move over the
visible range, so the headline change agrees with the chart. #}
<p class="range-summary" id="range-summary" hidden></p>
</div>
{# Indicator toggles. `is-active` is the initial visibility; chart.js reads
it and paints each swatch from its own ink palette. #}
<div class="ind-bar" role="group" aria-label="Chart indicators">
{% for ind in [
{"key": "sma50", "label": "SMA 50", "on": true, "dot": true, "hide": false},
{"key": "sma200", "label": "SMA 200", "on": true, "dot": true, "hide": false},
{"key": "ema21", "label": "EMA 21", "on": false, "dot": true, "hide": false},
{"key": "supertrend", "label": "Supertrend", "on": false, "dot": true, "hide": false},
{"key": "volume", "label": "Volume", "on": true, "dot": false, "hide": false},
{"key": "rsi", "label": "RSI", "on": true, "dot": true, "hide": false},
{"key": "benchmark", "label": (symbol.benchmark or "Benchmark"), "on": false, "dot": true, "hide": (not symbol.benchmark)}
] %}
<button type="button" class="ind-btn{% if ind.on %} is-active{% endif %}"
data-ind="{{ ind.key }}" aria-pressed="{{ 'true' if ind.on else 'false' }}"
{% if ind.hide %}hidden{% endif %}>
{% if ind.dot %}<span class="ind-btn__dot" aria-hidden="true"></span>{% endif %}{{ ind.label }}
</button>
{% endfor %}
</div>
<div id="chart" data-ticker="{{ symbol.ticker }}"></div>
</section>
{# Colour-coded read of the chart's indicators: an overall trend verdict, an
RSI momentum gauge, and one tile per moving-average signal. Mechanical, not
advice. #}
{% if indicators %}
<section class="panel ind-read">
<div class="ind-read__head">
<h2 class="ind-read__title">What the indicators say</h2>
<span class="ind-verdict ind-verdict--{{ indicators.verdict_tone }}">
<span class="ind-verdict__dot" aria-hidden="true"></span>{{ indicators.verdict }}
<span class="ind-verdict__note">{{ indicators.verdict_note }}</span>
</span>
</div>
{# RSI momentum gauge: a 0–100 track with oversold / overbought zones and a
marker at the current value, coloured by its zone. #}
<div class="ind-rsi ind-tone--{{ indicators.rsi_tone }}">
<div class="ind-rsi__top">
<span class="ind-rsi__label">Momentum · RSI 14</span>
<span class="ind-rsi__verdict">{{ indicators.rsi_label }}</span>
</div>
<div class="ind-rsi__track">
<span class="ind-rsi__zone ind-rsi__zone--over"></span>
<span class="ind-rsi__zone ind-rsi__zone--under"></span>
<span class="ind-rsi__marker" style="left: {{ indicators.rsi_pos }}%">
<span class="ind-rsi__bubble num">{{ indicators.rsi|round(0)|int }}</span>
</span>
</div>
<div class="ind-rsi__scale">
<span>0</span><span>oversold 30</span><span>overbought 70</span><span>100</span>
</div>
<p class="ind-rsi__note">{{ indicators.rsi_note }}</p>
</div>
{# Trend signal tiles, colour-coded bullish / bearish. #}
<div class="ind-signals">
{% for sig in indicators.signals %}
<div class="ind-sig ind-tone--{{ sig.tone }}">
<div class="ind-sig__top">
<span class="ind-sig__label">{{ sig.label }}</span>
{% if sig.value %}<span class="ind-sig__value num">{{ sig.value }}</span>{% endif %}
</div>
<div class="ind-sig__status">{{ sig.status }}</div>
<div class="ind-sig__note">{{ sig.note }}</div>
</div>
{% endfor %}
</div>
<p class="ind-read__foot">A mechanical reading of the chart’s indicators — not investment advice.</p>
</section>
{% endif %}
<h2 class="section-title">Key stats<span class="section-title__asof">as of {{ stats.date|shortdate }}</span></h2>
<section class="keystats">
{# --- the trading day: open & close placed in the day's range --- #}
<div class="gauge">
<div class="gauge__row">
<span class="gauge__label">The day</span>
<span class="gauge__meta">open and close within the day's range</span>
</div>
<div class="track gauge__track">
<span class="track__pip track__pip--ghost" style="left:{{ stats.day_open_pos }}%"></span>
<span class="track__pip track__pip--{{ 'down' if stats.change_pct is not none and stats.change_pct < 0 else 'up' }}" style="left:{{ stats.day_close_pos }}%"></span>
</div>
<div class="gauge__ends">
<span><span class="gauge__cap">Low</span> <span class="num">{{ stats.low|money }}</span></span>
<span class="gauge__ends-r"><span class="gauge__cap">High</span> <span class="num">{{ stats.high|money }}</span></span>
</div>
<dl class="legend">
<div class="legend__item">
<dt><i class="legend__dot legend__dot--ghost"></i> Open</dt>
<dd class="num">{{ stats.open|money }}</dd>
{% if stats.open_change_pct is not none %}
<dd class="legend__delta num {{ 'is-up' if stats.open_change_pct >= 0 else 'is-down' }}">{{ stats.open_change_pct|pct }}</dd>
{% else %}<dd></dd>{% endif %}
</div>
<div class="legend__item">
<dt><i class="legend__dot legend__dot--{{ 'down' if stats.change_pct is not none and stats.change_pct < 0 else 'up' }}"></i> Close</dt>
<dd class="num">{{ stats.close|money }}</dd>
{% if stats.change_pct is not none %}
<dd class="legend__delta num {{ 'is-up' if stats.change_pct >= 0 else 'is-down' }}">{{ stats.change_pct|pct }}</dd>
{% else %}<dd></dd>{% endif %}
</div>
</dl>
</div>
{# --- 52-week range: current price & prev close along the year --- #}
<div class="gauge">
<div class="gauge__row">
<span class="gauge__label">52-week range</span>
<span class="gauge__meta">where the price sits across its year</span>
</div>
<div class="track gauge__track">
{% if stats.yr_prev_pos is not none %}
<span class="track__pip track__pip--ghost" style="left:{{ stats.yr_prev_pos }}%"></span>
{% endif %}
<span class="track__pip track__pip--{{ 'down' if stats.change_pct is not none and stats.change_pct < 0 else 'up' }}" style="left:{{ stats.yr_close_pos }}%"></span>
</div>
<div class="gauge__ends">
<span><span class="gauge__cap">52w low</span> <span class="num">{{ stats.low_52w|money }}</span></span>
<span class="gauge__ends-r"><span class="gauge__cap">52w high</span> <span class="num">{{ stats.high_52w|money }}</span></span>
</div>
<dl class="legend">
<div class="legend__item">
<dt><i class="legend__dot legend__dot--{{ 'down' if stats.change_pct is not none and stats.change_pct < 0 else 'up' }}"></i> Current</dt>
<dd class="num">{{ stats.close|money }}</dd><dd></dd>
</div>
{% if stats.prev_close is not none %}
<div class="legend__item">
<dt><i class="legend__dot legend__dot--ghost"></i> Prev close</dt>
<dd class="num">{{ stats.prev_close|money }}</dd><dd></dd>
</div>
{% endif %}
</dl>
</div>
{# --- volume vs its own 3-month average --- #}
<div class="gauge">
<div class="gauge__row">
<span class="gauge__label">Volume</span>
<span class="gauge__meta">
{% if stats.vol_ratio is not none %}{{ stats.vol_ratio|round(2) }}× the 3-month average{% else %}today's share volume{% endif %}
</span>
</div>
<div class="track gauge__track gauge__track--bar">
<span class="track__fill" style="width:{{ stats.vol_fill_pct }}%"></span>
<span class="track__pip track__pip--ghost" style="left:50%"></span>
</div>
<div class="gauge__ends">
<span><span class="gauge__cap">0</span></span>
<span class="gauge__ends-c"><span class="gauge__cap">avg</span></span>
<span class="gauge__ends-r"><span class="gauge__cap">2× avg</span></span>
</div>
<dl class="legend">
<div class="legend__item">
<dt><i class="legend__dot"></i> Today</dt>
<dd class="num">{{ stats.volume|compact }}</dd><dd></dd>
</div>
<div class="legend__item">
<dt><i class="legend__dot legend__dot--ghost"></i> 3-mo avg</dt>
<dd class="num">{{ stats.avg_volume|compact }}</dd><dd></dd>
</div>
</dl>
</div>
</section>
{% else %}
<section class="empty">
{% if symbol.kind == 'future' %}
<p><strong>{{ symbol.ticker }}</strong> is a futures contract. Finance follows
futures with live quotes only, so there is no historical daily chart; the
price above updates live while the market is trading.</p>
{% else %}
<p>No daily price history is available for {{ symbol.ticker }}.</p>
{% endif %}
</section>
{% endif %}
{# --- fundamentals + financials: stocks only --- #}
{% if symbol.kind == 'stock' %}
{# --- earnings dates (Phase 25): next-expected date with provenance, days
since the last print, and a short list of recent past dates. Past
dates come from 8-K item-2.02 filings (already stored by Phase 14);
the next date is Yahoo's `calendarEvents` when present, otherwise a
cadence estimate from those past dates. --- #}
{% if earnings %}
<h2 class="section-title">Earnings{% if earnings.earnings_synced_at %}<span class="section-title__asof">calendar synced from Yahoo {{ earnings.earnings_synced_at|ago }}</span>{% endif %}</h2>
<section class="panel earn">
<dl class="earn__pair">
<div class="earn__cell">
<dt class="earn__cap">Most recent</dt>
{% if earnings.most_recent %}
<dd class="earn__val num">{{ earnings.most_recent.date|shortdate }}<span class="earn__sub">{{ earnings.most_recent.days_ago }} day{% if earnings.most_recent.days_ago != 1 %}s{% endif %} ago</span></dd>
{% else %}
<dd class="earn__val">—<span class="earn__sub">no past earnings filed</span></dd>
{% endif %}
</div>
<div class="earn__cell">
<dt class="earn__cap">Next expected</dt>
{% if earnings.next_date %}
<dd class="earn__val num">{{ earnings.next_date|shortdate }}<span class="earn__sub">
{%- if earnings.next_days is not none -%}
{%- if earnings.next_days >= 0 -%}
in {{ earnings.next_days }} day{% if earnings.next_days != 1 %}s{% endif %}
{%- else -%}
{{ earnings.next_days|abs }} day{% if earnings.next_days|abs != 1 %}s{% endif %} ago
{%- endif -%}
{%- endif -%}
{% if earnings.next_source == 'estimate' %} · estimated from cadence{% endif %}
</span></dd>
{% else %}
<dd class="earn__val">—<span class="earn__sub">no upcoming date on file</span></dd>
{% endif %}
</div>
</dl>
{% if earnings.past %}
<div class="earn__past">
<h3 class="earn__past-title">Recent earnings dates</h3>
<ul class="earn__list">
{% for e in earnings.past %}
<li class="earn-row">
<span class="earn-row__date num">{{ e.date|shortdate }}</span>
<span class="earn-row__ago">{{ e.days_ago }} day{% if e.days_ago != 1 %}s{% endif %} ago</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<p class="earn__src">Past earnings dates come from {{ symbol.ticker }}’s 8-K item 2.02 filings; the next date is Yahoo’s when available, otherwise estimated from the recent reporting cadence.</p>
</section>
{% endif %}
<h2 class="section-title">Fundamentals{% if fundamentals and fundamentals.ratios and symbol.fundamentals_synced_at %}<span class="section-title__asof">synced from SEC {{ symbol.fundamentals_synced_at|ago }}</span>{% endif %}</h2>
{% if fundamentals and fundamentals.ratios %}
{# The rolled-up standing badge sits above the per-ratio cards (Phase 20). #}
{% if standing %}
<div class="fund-standing">
{{ verdict_badge(standing) }}
<p class="fund-standing__text">Across the nine graded ratios{% if fundamentals.basis %}
({{ fundamentals.basis }} balance-sheet figures{% if fundamentals.pe_basis %}, P/E on {{ fundamentals.pe_basis }} earnings{% endif %}, against the latest price){% endif %},
{{ symbol.ticker }}’s fundamentals read as <strong>{{ standing.verdict|lower }}</strong> overall.</p>
</div>
{% elif fundamentals.basis %}
<p class="fund-basis">Balance-sheet ratios read {{ fundamentals.basis }} figures{% if fundamentals.pe_basis %}; the P/E uses {{ fundamentals.pe_basis }} EPS{% endif %}, against the latest price.</p>
{% endif %}
{% if fundamentals.earnings_stale %}<p class="fund-stale" role="note">Latest SEC earnings are from {{ fundamentals.earnings_period }}; the price-based ratios may lag a more recent quarter.</p>{% endif %}
<section class="ratios">
{% for r in fundamentals.ratios %}
<div class="ratio ratio--{{ r.grade }}">
<div class="ratio__head">
<span class="ratio__label">{{ r.label }}</span>
<span class="ratio__badge ratio__badge--{{ r.grade }}">{{ r.verdict }}</span>
</div>
<div class="ratio__value num">{{ r.display }}</div>
<p class="ratio__reading">{{ r.reading }}</p>
<p class="ratio__explain">{{ r.explain }}</p>
</div>
{% endfor %}
</section>
{% else %}
<div class="fund-pending">
{% if symbol.fundamentals_synced_at %}
No SEC fundamentals are available for {{ symbol.ticker }}. It may not file
financial reports with the SEC (a foreign issuer or non-filer), or the
symbol may be delisted. (Checked {{ symbol.fundamentals_synced_at|ago }}.)
{% else %}
SEC fundamentals for {{ symbol.ticker }} have not synced yet. They are pulled
on demand — hit <strong>Refresh</strong> above to fetch them now.
{% endif %}
</div>
{% endif %}
{% if fundamentals and (fundamentals.has_annual or fundamentals.has_quarterly) %}
<h2 class="section-title">Financials{% if symbol.fundamentals_synced_at %}<span class="section-title__asof">synced from SEC {{ symbol.fundamentals_synced_at|ago }}</span>{% endif %}</h2>
<section class="panel fin">
<div class="fin__toggle" role="tablist" aria-label="Reporting period">
<button type="button" class="fin__tab is-active" data-period="annual" aria-selected="true">Annual</button>
<button type="button" class="fin__tab" data-period="quarterly" aria-selected="false">Quarterly</button>
</div>
{{ fin_panel(fundamentals.annual, "annual", true) }}
{{ fin_panel(fundamentals.quarterly, "quarterly", false) }}
</section>
{% endif %}
{# --- leadership: officers, board & recent changes (Phase 14) --- #}
<h2 class="section-title">Leadership{% if symbol.leadership_synced_at %}<span class="section-title__asof">synced from SEC {{ symbol.leadership_synced_at|ago }}</span>{% endif %}</h2>
{% if leadership and leadership.roster %}
<section class="panel leadership">
<ul class="roster">
{% for p in leadership.roster %}
<li class="roster__row">
<span class="roster__name">{{ p.name }}</span>
<span class="roster__role">{{ p.role }}</span>
</li>
{% endfor %}
</ul>
<p class="roster__src">Officers and board are drawn from {{ symbol.ticker }}’s recent SEC ownership filings (Forms 3, 4 and 5).</p>
{% if leadership.changes %}
<div class="lead-changes">
<h3 class="lead-changes__title">Recent leadership changes</h3>
<ul class="lead-changes__list">
{% for c in leadership.changes %}
<li class="lead-change">
<a class="lead-change__link" href="{{ c.url }}" target="_blank" rel="noopener noreferrer">
<span class="lead-change__body">
<span class="lead-change__desc">Officer or director change</span>
<span class="lead-change__meta">Reported in an 8-K filed {{ c.filed_at|shortdate }}</span>
</span>
<svg class="filing__ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 5h5v5M19 5l-9 9M11 5H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5"/>
</svg>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</section>
{% elif leadership and leadership.synced %}
<div class="fund-pending">No leadership data is available for {{ symbol.ticker }}.</div>
{% else %}
<div class="fund-pending">
Leadership data for {{ symbol.ticker }} has not synced yet. It is pulled on
demand — hit <strong>Refresh</strong> above to fetch it now.
</div>
{% endif %}
{% endif %}
{# --- per-ticker anomaly feed (Phase 16): hidden when there are no
qualifying events in the past year --- #}
{% if anomalies %}
<h2 class="section-title">Notable recent events<span class="section-title__asof">past year</span></h2>
<section class="panel anomalies">
<ul class="anomaly-list">
{% for e in anomalies.events %}
<li class="anomaly anomaly--{{ e.glyph }} anomaly--{{ e.polarity }}">
{% if e.url %}<a class="anomaly__link" href="{{ e.url }}" target="_blank" rel="noopener noreferrer">{% else %}<span class="anomaly__link">{% endif %}
<span class="anomaly__date num">{{ e.date|shortdate }}</span>
<span class="anomaly__glyph anomaly__glyph--{{ e.glyph }}" aria-hidden="true">{% if e.glyph == 'up' %}↑{% elif e.glyph == 'down' %}↓{% elif e.glyph == 'drawdown' %}↡{% elif e.glyph == 'fund-up' %}+{% elif e.glyph == 'fund-down' %}−{% else %}❖{% endif %}</span>
<span class="anomaly__body">{{ e.headline }}</span>
<span class="anomaly__ext" aria-hidden="true">
{% if e.url %}
<svg class="filing__ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 5h5v5M19 5l-9 9M11 5H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5"/>
</svg>
{% endif %}
</span>
{% if e.url %}</a>{% else %}</span>{% endif %}
</li>
{% endfor %}
</ul>
<p class="anomaly-list__src">Large daily moves, new 6-month lows, year-over-year fundamentals swings, and reported leadership changes — pulled from the data this app already holds, never investment advice.</p>
</section>
{% endif %}
{# --- ETFs only: Phase 18 (fund profile) + Phase 28 (about / sector / --- #}
{# --- geography / trailing returns / growth of $10k / benchmark) --- #}
{% if symbol.kind == 'etf' %}
{# About this fund: expense ratio, yield, NAV + premium/discount, family, #}
{# category, inception, and the issuer's strategy paragraph (Phase 28). #}
<h2 class="section-title">About this fund{% if fund_meta and symbol.fund_metadata_synced_at %}<span class="section-title__asof">synced from Yahoo {{ symbol.fund_metadata_synced_at|ago }}</span>{% endif %}</h2>
{% if fund_meta %}
<section class="panel fund-about">
<dl class="fund-about__stats">
<div class="fund-about__stat">
<dt class="fund-about__cap">Expense ratio</dt>
<dd class="fund-about__val num">{{ fund_meta.expense_ratio }}</dd>
</div>
<div class="fund-about__stat">
<dt class="fund-about__cap">Distribution yield</dt>
<dd class="fund-about__val num">{{ fund_meta.yield_pct }}</dd>
</div>
{% if fund_meta.nav_price is not none %}
<div class="fund-about__stat">
<dt class="fund-about__cap">NAV</dt>
<dd class="fund-about__val num">{{ fund_meta.nav_price|money }}{% if fund_meta.premium %}<span class="fund-about__sub fund-about__sub--{{ fund_meta.premium.grade }}">{{ fund_meta.premium.text }} to price</span>{% endif %}</dd>
</div>
{% endif %}
{% if fund_meta.inception_date %}
<div class="fund-about__stat">
<dt class="fund-about__cap">Inception</dt>
<dd class="fund-about__val num">{{ fund_meta.inception_date|shortdate }}</dd>
</div>
{% endif %}
{% if fund_meta.category %}
<div class="fund-about__stat">
<dt class="fund-about__cap">Category</dt>
<dd class="fund-about__val">{{ fund_meta.category }}</dd>
</div>
{% endif %}
{% if fund_meta.fund_family %}
<div class="fund-about__stat">
<dt class="fund-about__cap">Family</dt>
<dd class="fund-about__val">{{ fund_meta.fund_family }}</dd>
</div>
{% endif %}
</dl>
{% if fund_meta.strategy_summary %}
<p class="fund-about__summary">{{ fund_meta.strategy_summary }}</p>
{% endif %}
</section>
{% else %}
<div class="fund-pending">
{% if symbol.fund_metadata_synced_at %}
No fund details are available for {{ symbol.ticker }} from Yahoo. The fund may
be delisted or renamed, or Yahoo may not carry details for it.
(Checked {{ symbol.fund_metadata_synced_at|ago }}.)
{% else %}
Fund details for {{ symbol.ticker }} have not synced yet. They are pulled on
demand — hit <strong>Refresh</strong> above to fetch them now.
{% endif %}
</div>
{% endif %}
{# Trailing returns (Phase 28). Eight periods, annualised past 1 year. #}
{% if returns %}
<h2 class="section-title">Trailing returns{% if symbol.history_synced_at %}<span class="section-title__asof">from price history</span>{% endif %}</h2>
<section class="panel ret-panel">
<table class="ret-table">
<thead>
<tr><th scope="col">Window</th><th scope="col" class="num">Return</th><th scope="col" class="num">Annualised</th></tr>
</thead>
<tbody>
{% for r in returns %}
<tr>
<th scope="row">{{ r.label }}</th>
<td class="num{% if r.dir > 0 %} ret-cell--up{% elif r.dir < 0 %} ret-cell--down{% endif %}">{{ r.pct }}</td>
<td class="num ret-cell--ann">{{ r.annualised }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="ret-panel__src">Cumulative price returns from {{ symbol.ticker }}’s stored daily closes. Distributions are not folded in; see Distributions below.</p>
</section>
{# Growth of $10,000 (Phase 28). Drawn client-side from the longest #}
{# available range, alongside the curated benchmark when one is set. #}
<h2 class="section-title">Growth of $10,000{% if symbol.benchmark %}<span class="section-title__asof">vs {{ symbol.benchmark }}</span>{% endif %}</h2>
<section class="panel growth-panel">
<div id="growth-chart" data-ticker="{{ symbol.ticker }}"{% if symbol.benchmark %} data-benchmark="{{ symbol.benchmark }}"{% endif %}></div>
<p class="growth-panel__src">A $10,000 investment scaled by the fund’s daily closes since inception.{% if symbol.benchmark %} The dashed line tracks the same amount in {{ symbol.benchmark }} from the fund’s first trading day forward.{% endif %}</p>
</section>
{% endif %}
<h2 class="section-title">Fund profile{% if fund and symbol.fund_synced_at %}<span class="section-title__asof">synced from SEC {{ symbol.fund_synced_at|ago }}</span>{% endif %}</h2>
{% if fund %}
<section class="panel fund">
<dl class="fund__stats">
<div class="fund__stat">
<dt class="fund__stat-cap">Net assets</dt>
<dd class="fund__stat-val num">{{ fund.net_assets if fund.net_assets else '—' }}</dd>
</div>
{% if not fund.is_commodity %}
<div class="fund__stat">
<dt class="fund__stat-cap">Holdings</dt>
<dd class="fund__stat-val num">{{ fund.holdings_count if fund.holdings_count is not none else '—' }}</dd>
</div>
{% endif %}
{% if fund.report_date %}
<div class="fund__stat">
<dt class="fund__stat-cap">As of</dt>
<dd class="fund__stat-val num">{{ fund.report_date }}</dd>
</div>
{% endif %}
</dl>
{% if fund.is_commodity %}
<p class="fund__note">{{ symbol.ticker }} is a grantor trust: it holds the physical commodity directly rather than a portfolio of securities, so it reports no holdings.</p>
{% elif fund.asset_mix %}
<div class="fund__mix">
<h3 class="fund__mix-label">Asset mix</h3>
<div class="mixbar">
{% for s in fund.asset_mix %}
<span class="mixbar__seg" style="width:{{ s.width }}%" title="{{ s.label }} {{ s.pct }}"></span>
{% endfor %}
</div>
<dl class="mixlegend">
{% for s in fund.asset_mix %}
<div class="mixlegend__item">
<dt><i class="mixlegend__dot"></i>{{ s.label }}</dt>
<dd class="num">{{ s.pct }}</dd>
</div>
{% endfor %}
</dl>
</div>
{% endif %}
{# Phase 28 mixes hide when degenerate: an equity ETF whose issuer #}
{# category rolls up entirely to "Corporate" gets a single-bucket #}
{# sector_mix, which carries no information — skip it then. #}
{% if fund.sector_mix and fund.sector_mix|length > 1 %}
<div class="fund__mix">
<h3 class="fund__mix-label">Issuer sectors</h3>
<div class="mixbar">
{% for s in fund.sector_mix %}
<span class="mixbar__seg" style="width:{{ s.width }}%" title="{{ s.label }} {{ s.pct }}"></span>
{% endfor %}
</div>
<dl class="mixlegend">
{% for s in fund.sector_mix %}
<div class="mixlegend__item">
<dt><i class="mixlegend__dot"></i>{{ s.label }}</dt>
<dd class="num">{{ s.pct }}</dd>
</div>
{% endfor %}
</dl>
</div>
{% endif %}
{% if fund.geography_mix and fund.geography_mix|length > 1 %}
<div class="fund__mix">
<h3 class="fund__mix-label">Geography</h3>
<div class="mixbar">
{% for s in fund.geography_mix %}
<span class="mixbar__seg" style="width:{{ s.width }}%" title="{{ s.label }} {{ s.pct }}"></span>
{% endfor %}
</div>
<dl class="mixlegend">
{% for s in fund.geography_mix %}
<div class="mixlegend__item">
<dt><i class="mixlegend__dot"></i>{{ s.label }}</dt>
<dd class="num">{{ s.pct }}</dd>
</div>
{% endfor %}
</dl>
</div>
{% endif %}
</section>
{% if fund.holdings %}
<h2 class="section-title">Top holdings{% if fund.report_date %}<span class="section-title__asof">holdings as of {{ fund.report_date|shortdate }}</span>{% endif %}</h2>
<section class="panel holdings">
<ol class="hold-list">
{% for h in fund.holdings %}
<li class="hold">
<span class="hold__fill" style="width:{{ h.bar_pct }}%"></span>
<span class="hold__rank num">{{ h.rank }}</span>
<span class="hold__name">{{ h.name }}</span>
<span class="hold__wt num">{{ h.weight }}</span>
<span class="hold__val num">{{ h.value }}</span>
</li>
{% endfor %}
</ol>
</section>
{% endif %}
{% else %}
<div class="fund-pending">
{% if symbol.fund_synced_at %}
No fund profile is available for {{ symbol.ticker }} from the SEC. The fund may
be delisted or renamed, or it does not file N-PORT portfolio reports.
(Checked {{ symbol.fund_synced_at|ago }}.)
{% else %}
The fund profile for {{ symbol.ticker }} has not synced yet. It is pulled on
demand — hit <strong>Refresh</strong> above to fetch it now.
{% endif %}
</div>
{% endif %}
{% endif %}
{# --- dividends / distributions: stocks AND ETFs (Phase 26 + 28) --- #}
{% if dividends %}
{% set div_label = 'Distributions' if symbol.kind == 'etf' else 'Dividends' %}
<h2 class="section-title">{{ div_label }}{% if symbol.dividends_synced_at %}<span class="section-title__asof">synced from Yahoo {{ symbol.dividends_synced_at|ago }}</span>{% endif %}</h2>
{% if dividends.synced and dividends.history %}
<section class="panel div-panel">
{# headline: inferred cadence + the on-track verdict pill #}
<div class="div-head">
<span class="div-cadence">{{ dividends.pace.cadence_caption }}</span>
{% if dividends.projection_display %}
<span class="vbadge vbadge--{{ dividends.pace.grade }}">{{ dividends.pace.verdict }}</span>
{% endif %}
</div>
{# pace: prior calendar year vs YTD-projected current year #}
<dl class="div-pace">
<div class="div-pace__item">
<dt class="div-pace__cap">{{ dividends.current_year - 1 }} total</dt>
<dd class="div-pace__val num">{% if dividends.pace.prior_year_total > 0 %}{{ dividends.prior_year_display }}{% else %}—{% endif %}</dd>
</div>
<div class="div-pace__item">
<dt class="div-pace__cap">{{ dividends.current_year }} so far</dt>
<dd class="div-pace__val num">{{ dividends.ytd_display }}<span class="div-pace__sub">{{ dividends.pace.ytd_count }} payment{% if dividends.pace.ytd_count != 1 %}s{% endif %}</span></dd>
</div>
{% if dividends.projection_display %}
<div class="div-pace__item">
<dt class="div-pace__cap">{{ dividends.current_year }} projected</dt>
<dd class="div-pace__val num">{{ dividends.projection_display }}<span class="div-pace__sub div-pace__sub--{{ dividends.pace.grade }}">{{ dividends.pct_change_display }} vs {{ dividends.current_year - 1 }}</span></dd>
</div>
{% endif %}
</dl>
<p class="div-pace__src">Projection scales {{ symbol.ticker }}’s declared payments so far this year up to the inferred cadence, then compares to last year’s total. Between payouts it is an estimate, not a guarantee.</p>
{# per-event history, newest first #}
<h3 class="div-hist__title">Payment history</h3>
<ul class="div-hist">
{% for d in dividends.history %}
<li class="div-row">
<span class="div-row__date">{{ d.ex_date|shortdate }}</span>
<span class="div-row__amt num">{{ d.amount }}</span>
</li>
{% endfor %}
</ul>
</section>
{% elif dividends.synced %}
<div class="fund-pending">{{ symbol.ticker }} has not {% if symbol.kind == 'etf' %}made a distribution{% else %}paid a dividend{% endif %} in the past five years.</div>
{% else %}
<div class="fund-pending">
{{ div_label }} history for {{ symbol.ticker }} has not synced yet. It is
pulled on demand — hit <strong>Refresh</strong> above to fetch it now.
</div>
{% endif %}
{% endif %}
{# --- SEC filings: stocks and ETFs --- #}
{% if filings %}
{# A stock's filings sync stamps `filings_synced_at`; an ETF's ride along
with the Phase 18 fund-profile sweep, which stamps `fund_synced_at`. #}
{% set filings_synced = symbol.filings_synced_at if symbol.filings_synced_at else symbol.fund_synced_at %}
<h2 class="section-title">Recent SEC filings{% if filings_synced %}<span class="section-title__asof">synced from SEC {{ filings_synced|ago }}</span>{% endif %}</h2>
<section class="panel filings">
<ul class="filing-list">
{% for f in filings %}
<li class="filing">
<a class="filing__link" href="{{ f.url }}" target="_blank" rel="noopener noreferrer">
<span class="filing__form num">{{ f.form }}</span>
<span class="filing__body">
<span class="filing__desc">{{ f.title }}</span>
<span class="filing__meta">Filed {{ f.filed_at|shortdate }}{% if f.period_of_report %} · period {{ f.period_of_report|shortdate }}{% endif %}</span>
</span>
<svg class="filing__ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 5h5v5M19 5l-9 9M11 5H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5"/>
</svg>
</a>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}<script type="module" src="{{ vite_asset('static_src/symbol/index.js') }}"></script>{% endblock %}