{% extends "base.html" %} {% block title %}{{ symbol.ticker }}{% endblock %} {% block description %}{{ symbol.name }} ({{ symbol.ticker }}): price chart, key stats, and history.{% endblock %} {% block extra_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) %}
{% if table.periods %}
{% for p in table.periods %}{% endfor %} {% for row in table.rows %} {% for c in row.cells %}{% endfor %} {% endfor %}
{{ p }}
{{ row.label }}{{ c.display }}{% if c.dir %}{% endif %}
{% else %}

No {{ period }} figures have been reported.

{% endif %}
{% endmacro %} {% block main %}
{# 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. #}
{% if stale_data %}
No recent trading data. 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.
{% endif %}

{{ symbol.ticker }}

{{ symbol.kind }} {% if symbol.exchange %}{{ symbol.exchange }}{% endif %} {% if symbol.sector %}{{ symbol.sector }}{% endif %} {% if symbol.industry %}{{ symbol.industry }}{% endif %}
{{ symbol.name }}
{# 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 %}
{{ price|money }} {% if chg_pct is not none %} {{ chg_abs|signed }} ({{ chg_pct|pct }}) {% endif %} {% if quote %}{{ quote.state_label }}{% if symbol.last_quote_at %} · quoted {{ symbol.last_quote_at|ago }}{% endif %}{% else %}Last close · {{ stats.date|shortdate }}{% endif %}
{% 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 %}
{{ verdict.verdict }} {{ verdict.percent }}% {% if health and health.trajectory != 'unknown' %} {{ health.trajectory_label }} {% endif %}
{% 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 %}
{% 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 %}
{% endif %}
{# 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 %}
{# 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.) #}
{% for r in ["YTD", "1M", "3M", "6M", "1Y", "3Y", "5Y", "MAX"] %} {% endfor %}
{# Filled by chart.js after each load: the % / absolute move over the visible range, so the headline change agrees with the chart. #}
{# Indicator toggles. `is-active` is the initial visibility; chart.js reads it and paints each swatch from its own ink palette. #}
{% 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)} ] %} {% endfor %}
{# 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 %}

What the indicators say

{{ indicators.verdict }} {{ indicators.verdict_note }}
{# RSI momentum gauge: a 0–100 track with oversold / overbought zones and a marker at the current value, coloured by its zone. #}
Momentum · RSI 14 {{ indicators.rsi_label }}
{{ indicators.rsi|round(0)|int }}
0oversold 30overbought 70100

{{ indicators.rsi_note }}

{# Trend signal tiles, colour-coded bullish / bearish. #}
{% for sig in indicators.signals %}
{{ sig.label }} {% if sig.value %}{{ sig.value }}{% endif %}
{{ sig.status }}
{{ sig.note }}
{% endfor %}

A mechanical reading of the chart’s indicators — not investment advice.

{% endif %}

Key statsas of {{ stats.date|shortdate }}

{# --- the trading day: open & close placed in the day's range --- #}
The day open and close within the day's range
Low {{ stats.low|money }} High {{ stats.high|money }}
Open
{{ stats.open|money }}
{% if stats.open_change_pct is not none %}
{{ stats.open_change_pct|pct }}
{% else %}
{% endif %}
Close
{{ stats.close|money }}
{% if stats.change_pct is not none %}
{{ stats.change_pct|pct }}
{% else %}
{% endif %}
{# --- 52-week range: current price & prev close along the year --- #}
52-week range where the price sits across its year
{% if stats.yr_prev_pos is not none %} {% endif %}
52w low {{ stats.low_52w|money }} 52w high {{ stats.high_52w|money }}
Current
{{ stats.close|money }}
{% if stats.prev_close is not none %}
Prev close
{{ stats.prev_close|money }}
{% endif %}
{# --- volume vs its own 3-month average --- #}
Volume {% if stats.vol_ratio is not none %}{{ stats.vol_ratio|round(2) }}× the 3-month average{% else %}today's share volume{% endif %}
0 avg 2× avg
Today
{{ stats.volume|compact }}
3-mo avg
{{ stats.avg_volume|compact }}
{% else %}
{% if symbol.kind == 'future' %}

{{ symbol.ticker }} 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.

{% else %}

No daily price history is available for {{ symbol.ticker }}.

{% endif %}
{% 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 %}

Earnings{% if earnings.earnings_synced_at %}calendar synced from Yahoo {{ earnings.earnings_synced_at|ago }}{% endif %}

Most recent
{% if earnings.most_recent %}
{{ earnings.most_recent.date|shortdate }}{{ earnings.most_recent.days_ago }} day{% if earnings.most_recent.days_ago != 1 %}s{% endif %} ago
{% else %}
no past earnings filed
{% endif %}
Next expected
{% if earnings.next_date %}
{{ earnings.next_date|shortdate }} {%- 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 %}
{% else %}
no upcoming date on file
{% endif %}
{% if earnings.past %}

Recent earnings dates

    {% for e in earnings.past %}
  • {{ e.date|shortdate }} {{ e.days_ago }} day{% if e.days_ago != 1 %}s{% endif %} ago
  • {% endfor %}
{% endif %}

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.

{% endif %}

Fundamentals{% if fundamentals and fundamentals.ratios and symbol.fundamentals_synced_at %}synced from SEC {{ symbol.fundamentals_synced_at|ago }}{% endif %}

{% if fundamentals and fundamentals.ratios %} {# The rolled-up standing badge sits above the per-ratio cards (Phase 20). #} {% if standing %}
{{ verdict_badge(standing) }}

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 {{ standing.verdict|lower }} overall.

{% elif fundamentals.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.

{% endif %} {% if fundamentals.earnings_stale %}

Latest SEC earnings are from {{ fundamentals.earnings_period }}; the price-based ratios may lag a more recent quarter.

{% endif %}
{% for r in fundamentals.ratios %}
{{ r.label }} {{ r.verdict }}
{{ r.display }}

{{ r.reading }}

{{ r.explain }}

{% endfor %}
{% else %}
{% 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 Refresh above to fetch them now. {% endif %}
{% endif %} {% if fundamentals and (fundamentals.has_annual or fundamentals.has_quarterly) %}

Financials{% if symbol.fundamentals_synced_at %}synced from SEC {{ symbol.fundamentals_synced_at|ago }}{% endif %}

{{ fin_panel(fundamentals.annual, "annual", true) }} {{ fin_panel(fundamentals.quarterly, "quarterly", false) }}
{% endif %} {# --- leadership: officers, board & recent changes (Phase 14) --- #}

Leadership{% if symbol.leadership_synced_at %}synced from SEC {{ symbol.leadership_synced_at|ago }}{% endif %}

{% if leadership and leadership.roster %}

Officers and board are drawn from {{ symbol.ticker }}’s recent SEC ownership filings (Forms 3, 4 and 5).

{% if leadership.changes %}

Recent leadership changes

{% endif %}
{% elif leadership and leadership.synced %}
No leadership data is available for {{ symbol.ticker }}.
{% else %}
Leadership data for {{ symbol.ticker }} has not synced yet. It is pulled on demand — hit Refresh above to fetch it now.
{% endif %} {% endif %} {# --- per-ticker anomaly feed (Phase 16): hidden when there are no qualifying events in the past year --- #} {% if anomalies %}

Notable recent eventspast year

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.

{% 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). #}

About this fund{% if fund_meta and symbol.fund_metadata_synced_at %}synced from Yahoo {{ symbol.fund_metadata_synced_at|ago }}{% endif %}

{% if fund_meta %}
Expense ratio
{{ fund_meta.expense_ratio }}
Distribution yield
{{ fund_meta.yield_pct }}
{% if fund_meta.nav_price is not none %}
NAV
{{ fund_meta.nav_price|money }}{% if fund_meta.premium %}{{ fund_meta.premium.text }} to price{% endif %}
{% endif %} {% if fund_meta.inception_date %}
Inception
{{ fund_meta.inception_date|shortdate }}
{% endif %} {% if fund_meta.category %}
Category
{{ fund_meta.category }}
{% endif %} {% if fund_meta.fund_family %}
Family
{{ fund_meta.fund_family }}
{% endif %}
{% if fund_meta.strategy_summary %}

{{ fund_meta.strategy_summary }}

{% endif %}
{% else %}
{% 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 Refresh above to fetch them now. {% endif %}
{% endif %} {# Trailing returns (Phase 28). Eight periods, annualised past 1 year. #} {% if returns %}

Trailing returns{% if symbol.history_synced_at %}from price history{% endif %}

{% for r in returns %} {% endfor %}
WindowReturnAnnualised
{{ r.label }} {{ r.pct }} {{ r.annualised }}

Cumulative price returns from {{ symbol.ticker }}’s stored daily closes. Distributions are not folded in; see Distributions below.

{# Growth of $10,000 (Phase 28). Drawn client-side from the longest #} {# available range, alongside the curated benchmark when one is set. #}

Growth of $10,000{% if symbol.benchmark %}vs {{ symbol.benchmark }}{% endif %}

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 %}

{% endif %}

Fund profile{% if fund and symbol.fund_synced_at %}synced from SEC {{ symbol.fund_synced_at|ago }}{% endif %}

{% if fund %}
Net assets
{{ fund.net_assets if fund.net_assets else '—' }}
{% if not fund.is_commodity %}
Holdings
{{ fund.holdings_count if fund.holdings_count is not none else '—' }}
{% endif %} {% if fund.report_date %}
As of
{{ fund.report_date }}
{% endif %}
{% if fund.is_commodity %}

{{ symbol.ticker }} is a grantor trust: it holds the physical commodity directly rather than a portfolio of securities, so it reports no holdings.

{% elif fund.asset_mix %}

Asset mix

{% for s in fund.asset_mix %} {% endfor %}
{% for s in fund.asset_mix %}
{{ s.label }}
{{ s.pct }}
{% endfor %}
{% 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 %}

Issuer sectors

{% for s in fund.sector_mix %} {% endfor %}
{% for s in fund.sector_mix %}
{{ s.label }}
{{ s.pct }}
{% endfor %}
{% endif %} {% if fund.geography_mix and fund.geography_mix|length > 1 %}

Geography

{% for s in fund.geography_mix %} {% endfor %}
{% for s in fund.geography_mix %}
{{ s.label }}
{{ s.pct }}
{% endfor %}
{% endif %}
{% if fund.holdings %}

Top holdings{% if fund.report_date %}holdings as of {{ fund.report_date|shortdate }}{% endif %}

    {% for h in fund.holdings %}
  1. {{ h.rank }} {{ h.name }} {{ h.weight }} {{ h.value }}
  2. {% endfor %}
{% endif %} {% else %}
{% 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 Refresh above to fetch it now. {% endif %}
{% endif %} {% endif %} {# --- dividends / distributions: stocks AND ETFs (Phase 26 + 28) --- #} {% if dividends %} {% set div_label = 'Distributions' if symbol.kind == 'etf' else 'Dividends' %}

{{ div_label }}{% if symbol.dividends_synced_at %}synced from Yahoo {{ symbol.dividends_synced_at|ago }}{% endif %}

{% if dividends.synced and dividends.history %}
{# headline: inferred cadence + the on-track verdict pill #}
{{ dividends.pace.cadence_caption }} {% if dividends.projection_display %} {{ dividends.pace.verdict }} {% endif %}
{# pace: prior calendar year vs YTD-projected current year #}
{{ dividends.current_year - 1 }} total
{% if dividends.pace.prior_year_total > 0 %}{{ dividends.prior_year_display }}{% else %}—{% endif %}
{{ dividends.current_year }} so far
{{ dividends.ytd_display }}{{ dividends.pace.ytd_count }} payment{% if dividends.pace.ytd_count != 1 %}s{% endif %}
{% if dividends.projection_display %}
{{ dividends.current_year }} projected
{{ dividends.projection_display }}{{ dividends.pct_change_display }} vs {{ dividends.current_year - 1 }}
{% endif %}

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.

{# per-event history, newest first #}

Payment history

{% elif dividends.synced %}
{{ symbol.ticker }} has not {% if symbol.kind == 'etf' %}made a distribution{% else %}paid a dividend{% endif %} in the past five years.
{% else %}
{{ div_label }} history for {{ symbol.ticker }} has not synced yet. It is pulled on demand — hit Refresh above to fetch it now.
{% 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 %}

Recent SEC filings{% if filings_synced %}synced from SEC {{ filings_synced|ago }}{% endif %}

{% endif %}
{% endblock %} {% block extra_js %}{% endblock %}