diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/financials.py | 278 |
1 files changed, 222 insertions, 56 deletions
diff --git a/components/financials.py b/components/financials.py index bc2a482..83e9a16 100644 --- a/components/financials.py +++ b/components/financials.py @@ -1,8 +1,7 @@ -"""Financial statements — Income Statement, Balance Sheet, Cash Flow.""" -import numpy as np +"""Financial statements — organized and raw views for Income Statement, Balance Sheet, Cash Flow.""" import pandas as pd import streamlit as st -from services.data_service import get_income_statement, get_balance_sheet, get_cash_flow +from services.data_service import get_income_statement, get_balance_sheet, get_cash_flow, get_balance_sheet_bridge_items from utils.formatters import fmt_large # Rows where an increase is bad (decline = green, increase = red). @@ -18,10 +17,11 @@ _INVERSE_ROWS = { "interest expense", "interest expense non operating", "tax provision", - "reconciled depreciation", # non-cash expense; higher = lower reported income + "reconciled depreciation", # ── Balance sheet ───────────────────────────────────────────────────────── "net debt", + "prism net debt", "total debt", "long term debt", "long term debt and capital lease obligation", @@ -46,26 +46,139 @@ _INVERSE_ROWS = { "tradeand other payables non current", # ── Cash flow ───────────────────────────────────────────────────────────── - # Debt issuance: positive value = new borrowing = bad "issuance of debt", "long term debt issuance", "net long term debt issuance", "net short term debt issuance", "net issuance payments of debt", - # Equity dilution: positive value = new shares issued = bad for holders "issuance of capital stock", "common stock issuance", "net common stock issuance", - # Taxes & interest paid: higher outflow = bad "income tax paid supplemental data", "interest paid supplemental data", - # Buybacks shown as negative outflow — more negative = more buybacks = good, - # so INVERSE: decline (more negative) → green "repurchase of capital stock", "common stock payments", } +INCOME_GROUPS = { + "Revenue": [ + "Total Revenue", + "Cost Of Revenue", + "Reconciled Cost Of Revenue", + "Gross Profit", + ], + "Operating Expenses": [ + "Research And Development", + "Selling General And Administration", + "Operating Expense", + "Total Expenses", + ], + "Profitability": [ + "Operating Income", + "EBIT", + "EBITDA", + "Normalized EBITDA", + "Pretax Income", + "Tax Provision", + "Net Income", + ], + "Other": [ + "Interest Expense", + "Interest Expense Non Operating", + "Diluted EPS", + "Basic EPS", + ], +} + +BALANCE_GROUPS = { + "Prism Bridge Metrics": [ + "Prism Net Debt", + "Net Debt", + "Total Debt", + "Current Debt", + "Long Term Debt", + "Cash And Cash Equivalents", + "Cash Cash Equivalents And Short Term Investments", + "Cash", + "Preferred Stock", + "Minority Interest", + "Minority Interests", + ], + "Assets": [ + "Cash And Cash Equivalents", + "Cash Cash Equivalents And Short Term Investments", + "Cash", + "Short Term Investments", + "Receivables", + "Inventory", + "Current Assets", + "Net PPE", + "Gross PPE", + "Goodwill", + "Other Intangible Assets", + "Total Assets", + ], + "Liabilities": [ + "Accounts Payable", + "Payables And Accrued Expenses", + "Payables", + "Current Liabilities", + "Current Debt", + "Current Debt And Capital Lease Obligation", + "Long Term Debt", + "Long Term Debt And Capital Lease Obligation", + "Total Debt", + "Total Liabilities Net Minority Interest", + "Total Non Current Liabilities Net Minority Interest", + ], + "Equity": [ + "Common Stock Equity", + "Stockholders Equity", + "Retained Earnings", + "Total Equity Gross Minority Interest", + "Preferred Stock", + "Minority Interest", + "Minority Interests", + ], +} + +CASHFLOW_GROUPS = { + "Operating Cash Flow": [ + "Operating Cash Flow", + "Net Income From Continuing Operations", + "Depreciation And Amortization", + "Deferred Tax", + "Stock Based Compensation", + "Change In Working Capital", + ], + "Investing Cash Flow": [ + "Investing Cash Flow", + "Capital Expenditure", + "Net Business Purchase And Sale", + "Purchase Of Investment", + "Sale Of Investment", + ], + "Financing Cash Flow": [ + "Financing Cash Flow", + "Issuance Of Debt", + "Repayment Of Debt", + "Net Issuance Payments Of Debt", + "Issuance Of Capital Stock", + "Repurchase Of Capital Stock", + "Cash Dividends Paid", + "Common Stock Dividend Paid", + ], + "Free Cash Flow & Returns": [ + "Free Cash Flow", + "Operating Cash Flow", + "Capital Expenditure", + "Repurchase Of Capital Stock", + "Cash Dividends Paid", + ], +} + + def _is_inverse(label: str) -> bool: return label.strip().lower() in _INVERSE_ROWS @@ -79,7 +192,6 @@ def _fmt_cell(value) -> str: def _yoy_raw(current, previous): - """Return raw float YoY % change, or None.""" try: c, p = float(current), float(previous) if p == 0: @@ -97,11 +209,6 @@ def _yoy_str(pct) -> str: def _build_statement(df: pd.DataFrame): - """ - Returns (display_df, color_df). - display_df: formatted string DataFrame for rendering. - color_df: same shape, cells are 'green', 'red', or '' for styling. - """ df = df.copy() df.columns = [str(c)[:10] for c in df.columns] cols = list(df.columns) @@ -116,7 +223,6 @@ def _build_statement(df: pd.DataFrame): if i + 1 < len(cols): prev_col = cols[i + 1] yoy_label = f"YoY {col[:4]}" - raw_yoy = df.apply(lambda row: _yoy_raw(row[col], row[prev_col]), axis=1) display[yoy_label] = raw_yoy.apply(_yoy_str) @@ -138,7 +244,7 @@ def _build_statement(df: pd.DataFrame): def _style(display: pd.DataFrame, colors: pd.DataFrame): GREEN_BG = "background-color: rgba(46, 204, 113, 0.18); color: #7ce3a1;" - RED_BG = "background-color: rgba(231, 76, 60, 0.18); color: #ff8a8a;" + RED_BG = "background-color: rgba(231, 76, 60, 0.18); color: #ff8a8a;" def apply_colors(row): return [ @@ -151,57 +257,117 @@ def _style(display: pd.DataFrame, colors: pd.DataFrame): return display.style.apply(apply_colors, axis=1) +def _augment_balance_sheet(df: pd.DataFrame, ticker: str, quarterly: bool) -> pd.DataFrame: + if quarterly or df is None or df.empty: + return df + + bridge = get_balance_sheet_bridge_items(ticker) + prism_net_debt = bridge.get("net_debt") + if prism_net_debt is None: + return df + + out = df.copy() + if "Prism Net Debt" not in out.index: + out.loc["Prism Net Debt"] = [float("nan")] * len(out.columns) + out.loc["Prism Net Debt", out.columns[0]] = prism_net_debt + return out + + +def _pick_existing_rows(df: pd.DataFrame, candidates: list[str]) -> list[str]: + return [row for row in candidates if row in df.index] + + +def _render_grouped_statement(df: pd.DataFrame, groups: dict[str, list[str]], empty_msg: str): + grouped_rows = [] + for section, candidates in groups.items(): + rows = _pick_existing_rows(df, candidates) + if not rows: + continue + grouped_rows.extend(rows) + with st.expander(section, expanded=(section == list(groups.keys())[0])): + section_df = df.loc[rows] + display, colors = _build_statement(section_df) + st.dataframe(_style(display, colors), use_container_width=True) + + remaining = [row for row in df.index if row not in grouped_rows] + if remaining: + with st.expander("Other Reported Line Items", expanded=False): + display, colors = _build_statement(df.loc[remaining]) + st.dataframe(_style(display, colors), use_container_width=True) + elif not grouped_rows: + st.info(empty_msg) + + +def _render_statement_block(title: str, df: pd.DataFrame, groups: dict[str, list[str]] | None, download_name: str, raw_key: str, empty_msg: str): + if df.empty: + st.info(empty_msg) + return + + view = st.radio( + f"{title} view", + ["Organized", "Raw"], + horizontal=True, + key=f"view_{raw_key}", + label_visibility="collapsed", + ) + + if view == "Organized" and groups: + _render_grouped_statement(df, groups, empty_msg) + else: + display, colors = _build_statement(df) + st.dataframe(_style(display, colors), use_container_width=True) + + st.download_button( + "Download CSV", + df.to_csv().encode(), + file_name=download_name, + mime="text/csv", + key=f"dl_{raw_key}", + ) + + def render_financials(ticker: str): col1, col2 = st.columns([1, 3]) with col1: freq = st.radio("Frequency", ["Annual", "Quarterly"], horizontal=False) quarterly = freq == "Quarterly" - tab_income, tab_balance, tab_cashflow = st.tabs( - ["Income Statement", "Balance Sheet", "Cash Flow"] - ) + tab_income, tab_balance, tab_cashflow = st.tabs([ + "Income Statement", "Balance Sheet", "Cash Flow" + ]) with tab_income: df = get_income_statement(ticker, quarterly=quarterly) - if df.empty: - st.info("Income statement data unavailable.") - else: - display, colors = _build_statement(df) - st.dataframe(_style(display, colors), use_container_width=True) - st.download_button( - "Download CSV", - df.to_csv().encode(), - file_name=f"{ticker.upper()}_income_{'quarterly' if quarterly else 'annual'}.csv", - mime="text/csv", - key=f"dl_income_{ticker}_{quarterly}", - ) + _render_statement_block( + "Income Statement", + df, + INCOME_GROUPS, + f"{ticker.upper()}_income_{'quarterly' if quarterly else 'annual'}.csv", + f"income_{ticker}_{quarterly}", + "Income statement data unavailable.", + ) with tab_balance: df = get_balance_sheet(ticker, quarterly=quarterly) - if df.empty: - st.info("Balance sheet data unavailable.") - else: - display, colors = _build_statement(df) - st.dataframe(_style(display, colors), use_container_width=True) - st.download_button( - "Download CSV", - df.to_csv().encode(), - file_name=f"{ticker.upper()}_balance_{'quarterly' if quarterly else 'annual'}.csv", - mime="text/csv", - key=f"dl_balance_{ticker}_{quarterly}", - ) + df = _augment_balance_sheet(df, ticker, quarterly) + if not quarterly and not df.empty: + st.caption("Prism Net Debt is calculated as Total Debt - Cash & Equivalents using the same bridge logic as Valuation.") + _render_statement_block( + "Balance Sheet", + df, + BALANCE_GROUPS, + f"{ticker.upper()}_balance_{'quarterly' if quarterly else 'annual'}.csv", + f"balance_{ticker}_{quarterly}", + "Balance sheet data unavailable.", + ) with tab_cashflow: df = get_cash_flow(ticker, quarterly=quarterly) - if df.empty: - st.info("Cash flow data unavailable.") - else: - display, colors = _build_statement(df) - st.dataframe(_style(display, colors), use_container_width=True) - st.download_button( - "Download CSV", - df.to_csv().encode(), - file_name=f"{ticker.upper()}_cashflow_{'quarterly' if quarterly else 'annual'}.csv", - mime="text/csv", - key=f"dl_cashflow_{ticker}_{quarterly}", - ) + _render_statement_block( + "Cash Flow", + df, + CASHFLOW_GROUPS, + f"{ticker.upper()}_cashflow_{'quarterly' if quarterly else 'annual'}.csv", + f"cashflow_{ticker}_{quarterly}", + "Cash flow data unavailable.", + ) |
