aboutsummaryrefslogtreecommitdiff
path: root/components/financials.py
blob: 828f256e72e9068fdbafb5d23b5add71be909427 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""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)