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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
"""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}",
)
|