readthrough / dashboard /statement_view.py
Viney's picture
Claude Opus 5 (1M context)
feat: decompose the EPS headline and read the statements behind it
bb3ad6b
Raw History Blame Contribute Delete
7.03 kB
"""Three figures, then whatever is out of line — and nothing else.
One block, bounded height. The alternative shape, two blocks of ratios, was
rejected on purpose: this page already runs long, and a reader who has to
decide which of forty numbers is the finding has been handed the analyst's
job back.
So the strip is fixed at three cards, and the list under it is usually
empty. When it is not, each line names the figure, the base it is out of
line with, and nothing else. The base is always the company's own record --
its median, its own days, its own prior year -- which is what turns a ratio
into a finding.
"""
from __future__ import annotations
from html import escape
import streamlit as st
from dashboard.components import evidence_link, section_header
from dashboard.i18n import t
from dashboard.theme import (
BEAR,
BEAR_BG,
BORDER,
BULL,
FS_EYEBROW,
FS_META,
TEXT,
TEXT_FAINT,
TEXT_MUTED,
WARN,
)
# A flow-through above this is genuine operating leverage rather than a
# normal year; below zero the added revenue lost money. Only the colouring
# depends on these -- the figure itself is always shown.
_STRONG_INCREMENTAL_PCT = 40.0
# Cash conversion below this means the reported profit did not arrive in the
# period it was reported for.
_WEAK_CONVERSION_PCT = 80.0
def has_signals(ticker: str) -> bool:
"""True when either half has something to show. No Streamlit."""
from analytics.statement_signals import signals
return bool(signals(ticker))
def _pct(value: float | None, digits: int = 0, signed: bool = False) -> str:
if value is None:
return "—"
return f"{value:+.{digits}f}%" if signed else f"{value:.{digits}f}%"
def cards(view: dict) -> list[dict]:
"""The context strip, in display order. Pure, so the wording is testable.
A figure that could not be computed is dropped rather than shown as a
dash: three cards where one is empty reads as a broken panel, and the
other two say everything the third would have qualified.
"""
out: list[dict] = []
incremental = view.get("incremental_margin_pct")
if incremental is not None:
out.append({
"key": "incremental_margin",
"label": t("stmt_incremental_label"),
"value": _pct(incremental),
"note": t("stmt_incremental_note"),
"tone": (
"good" if incremental >= _STRONG_INCREMENTAL_PCT
else "bad" if incremental < 0 else "flat"
),
})
stack = view.get("two_year_stack")
if stack:
out.append({
"key": "two_year_stack",
"label": t("stmt_stack_label"),
"value": _pct(stack["stack_pct"], signed=True),
"note": t("stmt_stack_note").format(
latest=_pct(stack["latest_pct"], signed=True),
earlier=_pct(stack["earlier_pct"], signed=True),
period=stack["earlier_period"],
),
"tone": "flat",
})
conversion = view.get("cash_conversion_pct")
if conversion is not None:
prior = view.get("cash_conversion_prior_pct")
out.append({
"key": "cash_conversion",
"label": t("stmt_conversion_label"),
"value": _pct(conversion),
"note": (
t("stmt_conversion_note").format(prior=_pct(prior), period=view.get("prior_period") or "")
if prior is not None else t("stmt_conversion_note_alone")
),
"tone": "bad" if conversion < _WEAK_CONVERSION_PCT else "good",
})
return out
def flag_lines(view: dict) -> list[str]:
"""One sentence per flag, in the order the analytics ranked them."""
out: list[str] = []
for flag in view.get("flags") or []:
key = flag.get("key")
if key == "accruals":
out.append(t("stmt_flag_accruals").format(
value=f'{flag["value"]:.1f}', base=f'{flag["base"]:.1f}',
multiple=f'{flag["multiple"]:.1f}',
))
elif key in ("receivables", "inventory"):
out.append(t(f"stmt_flag_{key}").format(
days=f'{flag["value"]:.0f}', base=f'{flag["base"]:.0f}',
change=f'{flag["change_days"]:+.0f}',
))
elif key == "payout":
out.append(t("stmt_flag_payout").format(pct=f'{flag["value"]:.0f}'))
elif key == "sbc_offset":
out.append(t("stmt_flag_sbc").format(
pct=f'{flag["value"]:.0f}',
gross=f'{flag["gross"] / 1e9:.1f}',
net=f'{flag["net"] / 1e9:.1f}',
))
return out
_TONE_COLOR = {"good": BULL, "bad": BEAR, "flat": TEXT}
def _card_html(card: dict) -> str:
return (
f'<div style="flex:1 1 0;min-width:0;padding:12px 14px;border:1px solid {BORDER};'
f'border-radius:10px;">'
f'<div style="font-size:{FS_EYEBROW};font-weight:700;letter-spacing:0.08em;'
f'text-transform:uppercase;color:{TEXT_FAINT};">{escape(card["label"])}</div>'
f'<div style="font-size:1.35rem;font-weight:700;margin:4px 0 2px;'
f'color:{_TONE_COLOR.get(card["tone"], TEXT)};">{escape(card["value"])}</div>'
f'<div style="font-size:{FS_META};color:{TEXT_MUTED};line-height:1.45;">'
f'{escape(card["note"])}</div>'
f'</div>'
)
def _flags_html(lines: list[str]) -> str:
if not lines:
return ""
items = "".join(
f'<div style="font-size:0.85rem;line-height:1.55;color:{TEXT};'
f'padding:8px 12px;border-left:2px solid {WARN};background:{BEAR_BG};'
f'border-radius:0 6px 6px 0;margin-top:6px;">{escape(line)}</div>'
for line in lines
)
return (
f'<div style="margin-top:14px;">'
f'<div style="font-size:{FS_EYEBROW};font-weight:700;letter-spacing:0.1em;'
f'text-transform:uppercase;color:{TEXT_MUTED};">{t("stmt_flags_title")}</div>'
f'{items}</div>'
)
def render(ticker: str) -> None:
"""Render the statement read for *ticker*, or nothing."""
from analytics.statement_signals import signals
view = signals(ticker)
if not view:
return
strip = cards(view)
lines = flag_lines(view)
if not strip and not lines:
return
caption = t("stmt_caption").format(
period=view.get("period") or "", prior=view.get("prior_period") or "",
)
link = evidence_link({"source_url": view.get("source_url")}, t("stmt_source_link"))
st.markdown(
section_header(t("stmt_title"))
+ f'<div style="font-size:{FS_META};color:{TEXT_FAINT};margin:-4px 0 10px;">'
f'{escape(caption)}{" · " + link if link else ""}</div>'
+ f'<div style="display:flex;gap:10px;flex-wrap:wrap;">'
+ "".join(_card_html(card) for card in strip)
+ "</div>"
+ _flags_html(lines),
unsafe_allow_html=True,
)
st.markdown("<div style='height:12px'></div>", unsafe_allow_html=True)