"""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 a decline is actually good (e.g. costs, expenses) _INVERSE_ROWS = { "cost of revenue", "cost of goods sold", "operating expenses", "selling general administrative", "research development", "interest expense", "income tax expense", "total expenses", "reconciled cost of revenue", } 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) 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) 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)