aboutsummaryrefslogtreecommitdiff
path: root/components/financials.py
diff options
context:
space:
mode:
authorOpenclaw <openclaw@mail.tylerhoang.xyz>2026-03-30 19:46:33 -0700
committerOpenclaw <openclaw@mail.tylerhoang.xyz>2026-03-30 19:46:33 -0700
commit3ac70d7231bac5c43ecb343e940cec4f9517eff2 (patch)
tree86cab4e849dd9fc533af8702c675ca7ae5f85f79 /components/financials.py
parent2de6ae37b902e3632ea62b904164552538501ec3 (diff)
Organize financial statements into grouped views
- add Organized/Raw toggle for income statement, balance sheet, and cash flow - group line items into expandable sections for easier navigation - add Prism Net Debt row to annual balance sheet using valuation bridge logic - keep full raw statement tables and CSV download support
Diffstat (limited to 'components/financials.py')
-rw-r--r--components/financials.py278
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.",
+ )