From 870f8e6c8b88d61d0f7183b938b9a496c193b141 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 15 May 2026 23:47:50 -0700 Subject: Redesign filings tab as client-side surface --- components/filings.py | 438 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 356 insertions(+), 82 deletions(-) (limited to 'components') diff --git a/components/filings.py b/components/filings.py index 9e3b156..4343a07 100644 --- a/components/filings.py +++ b/components/filings.py @@ -1,8 +1,14 @@ -"""SEC filings — recent 10-K, 10-Q, 8-K and other forms with direct links.""" +"""SEC filings tab rendered as a client-side HTML surface.""" +from datetime import date as _date +from datetime import datetime as _dt +from html import escape as _esc + import streamlit as st +import streamlit.components.v1 as components -from services.data_service import get_sec_filings +from services.data_service import get_company_info, get_latest_price, get_sec_filings +_XMAP = {"NYQ": "NYSE", "NMS": "NASDAQ", "NGM": "NASDAQ", "NCM": "NASDAQ", "ASE": "AMEX"} _FORM_DESCRIPTIONS = { "10-K": "Annual report", @@ -16,93 +22,361 @@ _FORM_DESCRIPTIONS = { "SC 13D": "Beneficial ownership (active)", } -_FORM_COLORS = { - "10-K": "rgba(74,120,181,0.15)", - "10-Q": "rgba(79,140,94,0.15)", - "8-K": "rgba(196,149,69,0.15)", -} + +def _normalize_date(raw) -> str | None: + if raw is None: + return None + if isinstance(raw, _dt): + return raw.strftime("%Y-%m-%d") + if isinstance(raw, _date): + return raw.strftime("%Y-%m-%d") + + text = str(raw).strip() + if not text: + return None + + if " " in text: + text = text.split(" ", 1)[0] + if "T" in text: + text = text.split("T", 1)[0] + + for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%Y/%m/%d"): + try: + return _dt.strptime(text, fmt).strftime("%Y-%m-%d") + except Exception: + pass + + if len(text) >= 10: + cand = text[:10] + try: + return _dt.strptime(cand, "%Y-%m-%d").strftime("%Y-%m-%d") + except Exception: + pass + + return None def render_filings(ticker: str): - with st.spinner("Loading SEC filings…"): - filings = get_sec_filings(ticker) + import json as _json + + info = get_company_info(ticker) or {} + price = get_latest_price(ticker) + filings = get_sec_filings(ticker) if not filings: st.info("No SEC filing data available for this ticker.") return - # yfinance returns: date (datetime.date), type, title, edgarUrl, exhibits (dict) - form_types = sorted({str(f.get("type", "")).strip() for f in filings if f.get("type")}) - priority = [t for t in ["10-K", "10-Q", "8-K"] if t in form_types] - other = [t for t in form_types if t not in ("10-K", "10-Q", "8-K")] - filter_options = ["All"] + priority + other - - filter_col, _ = st.columns([1, 3]) - with filter_col: - selected_type = st.selectbox("Form type", options=filter_options, index=0, key="filings_filter") - - # Summary counts - counts = {} - for f in filings: - ft = str(f.get("type", "Other")).strip() - counts[ft] = counts.get(ft, 0) + 1 - - if priority: - cols = st.columns(len(priority)) - for col, ft in zip(cols, priority): - col.metric(ft, counts.get(ft, 0)) - st.write("") - - filtered = filings if selected_type == "All" else [ - f for f in filings if str(f.get("type", "")).strip() == selected_type - ] - - if not filtered: - st.info("No filings match the current filter.") + rows = [] + for filing in filings: + filing = filing or {} + form_raw = filing.get("type") + form = str(form_raw).strip() if form_raw is not None else "" + if not form: + form = "Other" + + date_str = _normalize_date(filing.get("date")) + month_str = date_str[:7] if date_str else None + + title_raw = filing.get("title") + title = str(title_raw).strip() if title_raw is not None else "" + if not title: + title = _FORM_DESCRIPTIONS.get(form) + + exhibits = filing.get("exhibits") + url = None + if isinstance(exhibits, dict): + direct = exhibits.get(form) + if direct: + url = str(direct).strip() or None + + if url is None: + edgar = filing.get("edgarUrl") + if edgar: + url = str(edgar).strip() or None + + rows.append( + { + "date": date_str, + "month": month_str, + "form": form, + "title": title or None, + "url": url, + "has_exhibits": bool(isinstance(exhibits, dict) and len(exhibits) > 0), + } + ) + + rows.sort(key=lambda r: (r.get("date") is not None, r.get("date") or ""), reverse=True) + + if not rows: + st.info("No SEC filing data available for this ticker.") return - for item in filtered[:40]: - form = str(item.get("type", "—")).strip() - date = str(item.get("date", ""))[:10] - title = item.get("title") or _FORM_DESCRIPTIONS.get(form, "") - edgar_url = item.get("edgarUrl", "") - exhibits = item.get("exhibits") or {} - - color = _FORM_COLORS.get(form, "rgba(255,255,255,0.05)") - - with st.container(): - left, right = st.columns([5, 1]) - with left: - st.markdown( - f"
" - f"{form}" - f"{title}" - f"{date}" - f"
", - unsafe_allow_html=True, - ) - with right: - doc_url = exhibits.get(form) or edgar_url - if doc_url: - st.markdown( - f"
" - f"View ↗
", - unsafe_allow_html=True, - ) + n_rows = max(len(rows), 20) + height = 1280 + n_rows * 30 + + def _safe_float(value): + try: + return float(value) + except (TypeError, ValueError): + return None + + current_price = info.get("currentPrice") or info.get("regularMarketPrice") or price + prev_close = info.get("previousClose") + cur_num = _safe_float(current_price) + prev_num = _safe_float(prev_close) + + if cur_num is not None and prev_num is not None and prev_num > 0: + chg_pct = (cur_num - prev_num) / prev_num * 100.0 + chg_sign = "+" if chg_pct >= 0 else "" + chg_arrow = "▲" if chg_pct >= 0 else "▼" + chg_str = chg_arrow + " " + chg_sign + "{:.2f}%".format(chg_pct) + chg_cls = "chg-pos" if chg_pct >= 0 else "chg-neg" + else: + chg_str = "—" + chg_cls = "" + + exchange_raw = str(info.get("exchange") or "") + exchange = _XMAP.get(exchange_raw, exchange_raw) or "—" + co_name = _esc(info.get("longName") or info.get("shortName") or ticker.upper()) + price_str = "${:,.2f}".format(cur_num) if cur_num is not None else "—" + + rows_js = "const FILINGS=" + _json.dumps(rows) + ";" + meta_js = "const FILINGS_META=" + _json.dumps({"ticker": ticker.upper(), "rows": len(rows)}) + ";" + + plotly_cdn = "" + _ROOT = ( + "" + ) + + fonts_link = ( + "" + "" + ) + + _FI_CSS = """""" + + ctx_html = ( + '
' + + '' + + _esc(ticker.upper()) + + "" + + '' + + co_name + + "" + + 'Filings' + + '
' + + "" + + _esc(exchange) + + "" + + '' + + price_str + + "" + + '' + + chg_str + + "" + + "
" + ) + + lede_html = ( + '
' + + '
' + + 'Regulatory' + + '
How the company is reporting
' + + '

SEC filing flow for ' + + _esc(ticker.upper()) + + ' across annual, quarterly, and event-driven forms. Data is sourced from Yahoo Finance via yfinance and links route to EDGAR documents when available.

' + + "
" + + '
' + + '
Sourceyfinance
' + + '
FeedSEC filings
' + + '
Filings' + + str(len(rows)) + + "
" + + "
" + ) + + controls_html = ( + '
' + + '
' + + 'Range' + + '' + + '' + + '' + + "
" + + '
' + + 'Form' + + "
" + + "
" + ) + + foot_html = ( + '
' + + "SEC filing data provided by Yahoo Finance via yfinance · Filing links route to EDGAR documents when available · Form labels and counts reflect the current feed returned for this ticker" + + "
" + ) + + js = ( + "" + ) + + doc = ( + "" + + plotly_cdn + + fonts_link + + _ROOT + + _FI_CSS + + "
" + + ctx_html + + '
' + + lede_html + + controls_html + + '
' + + '
' + + '
Monthly filing cadence
' + + '
Form mix
' + + "
" + + '
' + + '
' + + foot_html + + "
" + + js + + "" + ) + + components.html(doc, height=height, scrolling=False) -- cgit v1.3-2-g0d8e