"""Financial statements — Income Statement, Balance Sheet, Cash Flow.""" import numpy as np import pandas as pd import streamlit as st from services.data_service import get_income_statement, get_balance_sheet, get_cash_flow from utils.formatters import fmt_large # Rows where an increase is bad (decline = green, increase = red). # Labels must match yfinance row names (case-insensitive after .strip().lower()). _INVERSE_ROWS = { # ── Income statement ────────────────────────────────────────────────────── "cost of revenue", "reconciled cost of revenue", "operating expense", "research and development", "selling general and administration", "total expenses", "interest expense", "interest expense non operating", "tax provision", "reconciled depreciation", # non-cash expense; higher = lower reported income # ── Balance sheet ───────────────────────────────────────────────────────── "net debt", "total debt", "long term debt", "long term debt and capital lease obligation", "long term capital lease obligation", "current debt", "current debt and capital lease obligation", "current capital lease obligation", "capital lease obligations", "other current borrowings", "commercial paper", "total liabilities net minority interest", "total non current liabilities net minority interest", "current liabilities", "accounts payable", "payables and accrued expenses", "payables", "current accrued expenses", "total tax payable", "income tax payable", "current deferred liabilities", "current deferred revenue", "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", } def _is_inverse(label: str) -> bool: return label.strip().lower() in _INVERSE_ROWS def _fmt_cell(value) -> str: try: v = float(value) except (TypeError, ValueError): return "—" return fmt_large(v) def _yoy_raw(current, previous): """Return raw float YoY % change, or None.""" try: c, p = float(current), float(previous) if p == 0: return None return (c - p) / abs(p) * 100 except (TypeError, ValueError): return None def _yoy_str(pct) -> str: if pct is None: return "—" arrow = "▲" if pct >= 0 else "▼" return f"{arrow} {abs(pct):.1f}%" 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) display = pd.DataFrame(index=df.index) colors = pd.DataFrame(index=df.index) for i, col in enumerate(cols): display[col] = df[col].apply(_fmt_cell) colors[col] = "" 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) def cell_color(row_label, pct): if pct is None: return "" inverse = _is_inverse(row_label) positive_change = pct >= 0 good = positive_change if not inverse else not positive_change return "green" if good else "red" colors[yoy_label] = pd.Series( [cell_color(idx, pct) for idx, pct in zip(df.index, raw_yoy)], index=df.index, ) return display, colors 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;" def apply_colors(row): return [ GREEN_BG if colors.loc[row.name, col] == "green" else RED_BG if colors.loc[row.name, col] == "red" else "" for col in display.columns ] return display.style.apply(apply_colors, axis=1) 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"] ) 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}", ) 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}", ) 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}", )