mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-04 06:17:44 +00:00
d337c63976
Architecture (3,909-line monolith → 28 focused modules, all < 300 lines):
- config/: constants.py (company lists, row maps, Damodaran baselines), theme.py (CSS/HTML)
- utils/: prefs, formatting, ticker, dcf, charts, ui_helpers
- data/: sec_parser, sec_fetcher, sec_downloader, financials, fundamentals,
valuation, ratios, scores, scores_ai, market
- ai/: gemini_core, gemini_sec, gemini_insights
- views/: sidebar, tab1_quant, tab1_ai, tab1_filings, tab2_dcf,
tab3_comps, tab4_news, tab5_markets, tab6_crypto, tab7_technical
- app.py: thin orchestrator (~118 lines)
- Strict unidirectional dependency graph (no circular imports)
- All @st.cache_data TTLs and st.session_state keys preserved identically
SEC filing viewer fix:
- Rebuilt EDGAR fetch chain: company_tickers.json → CIK → submissions API
→ filings.recent.primaryDocument[] (replaces deprecated directory.item)
- Filing type selectbox (10-K, 10-Q, 8-K, 20-F, 6-K) connected to backend
- Native HTML rendered via streamlit.components.v1.html() with CSS reset
- Errors surfaced explicitly with st.error()
- DART direct links restored for Korean-listed companies
.gitignore: data/ → data/*.json + data/*.html (preserve Python modules)
README: full rewrite for master's portfolio — 7-tab layout, architecture
diagram, modular structure tree, technical challenges, design rationale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
262 lines
14 KiB
Python
262 lines
14 KiB
Python
from typing import Optional
|
|
import pandas as pd
|
|
import streamlit as st
|
|
from utils.formatting import _safe_float
|
|
from data.financials import _get_row_series, _get_annual_financials_balance_cashflow
|
|
from data.ratios import get_dupont_altman_redflags_yoy
|
|
|
|
try:
|
|
import yfinance as yf
|
|
except ImportError:
|
|
yf = None
|
|
|
|
|
|
@st.cache_data(ttl=300)
|
|
def get_income_statement_sankey_data(ticker: str) -> dict:
|
|
"""Latest year (or TTM): Revenue, COGS, Gross Profit, OpEx, Operating Income, Tax/Interest/Other, Net Income. Uses yahooquery then yfinance."""
|
|
out = {"revenue": 0, "cogs": 0, "gross_profit": 0, "opex": 0, "operating_income": 0, "tax_interest_other": 0, "net_income": 0}
|
|
fin, _, _ = _get_annual_financials_balance_cashflow(ticker)
|
|
if fin is None or fin.empty:
|
|
return out
|
|
try:
|
|
rev = _get_row_series(fin, "Total Revenue", "Revenue", "Net Revenue")
|
|
cogs = _get_row_series(fin, "Cost Of Revenue", "Cost Of Goods Sold")
|
|
gross = _get_row_series(fin, "Gross Profit")
|
|
op_inc = _get_row_series(fin, "Operating Income", "EBIT")
|
|
ni = _get_row_series(fin, "Net Income", "Net Income Common Stockholders")
|
|
if rev is None or len(rev) == 0:
|
|
return out
|
|
d = rev.index[0]
|
|
revenue = abs(_safe_float(rev.get(d)) or 0)
|
|
cogs_val = abs(_safe_float(cogs.get(d)) if cogs is not None and d in cogs.index else 0) or 0
|
|
gross_val = _safe_float(gross.get(d)) if gross is not None and d in gross.index else None
|
|
if gross_val is None and revenue and cogs_val is not None:
|
|
gross_val = revenue - cogs_val
|
|
elif gross_val is None:
|
|
gross_val = revenue
|
|
gross_val = abs(gross_val) if gross_val is not None else 0
|
|
op_inc_val = _safe_float(op_inc.get(d)) if op_inc is not None and d in op_inc.index else None
|
|
op_inc_val = op_inc_val if op_inc_val is not None else 0
|
|
ni_val = _safe_float(ni.get(d)) if ni is not None and d in ni.index else None
|
|
ni_val = ni_val if ni_val is not None else 0
|
|
opex_val = max(0, gross_val - op_inc_val) if (gross_val >= op_inc_val) else 0
|
|
tax_interest_other = max(0, op_inc_val - ni_val) if (op_inc_val - ni_val) > 0 else abs(min(0, op_inc_val - ni_val))
|
|
out["revenue"] = max(revenue, 1)
|
|
out["cogs"] = min(cogs_val, revenue - 1e-6)
|
|
out["gross_profit"] = gross_val
|
|
out["opex"] = opex_val
|
|
out["operating_income"] = op_inc_val
|
|
out["tax_interest_other"] = tax_interest_other
|
|
out["net_income"] = ni_val
|
|
return out
|
|
except Exception:
|
|
return out
|
|
|
|
|
|
# sankey_data_from_ai, piotroski_from_ai, radar_metrics_from_ai → data/scores_ai.py
|
|
|
|
|
|
@st.cache_data(ttl=300)
|
|
def get_radar_metrics_normalized(ticker: str) -> dict:
|
|
"""ROE, Current Ratio, Asset Turnover, Equity Mult, Revenue YoY. Normalized to 0-100 for radar. Returns {theta: [...], r: [...], labels: [...]} or empty."""
|
|
if not ticker:
|
|
return {}
|
|
q = get_dupont_altman_redflags_yoy(ticker)
|
|
if not q:
|
|
return {}
|
|
dupont_df = q.get("dupont")
|
|
if dupont_df is None or dupont_df.empty or len(dupont_df) < 2:
|
|
return {}
|
|
row0 = dupont_df.iloc[0]
|
|
row1 = dupont_df.iloc[1]
|
|
roe = row0.get("ROE %") or 0
|
|
cr = row0.get("Current Ratio") or 0
|
|
at = row0.get("Asset Turnover") or 0
|
|
em = row0.get("Equity Mult.") or 0
|
|
rev0 = dupont_df["Revenue"].iloc[0] if "Revenue" in dupont_df.columns else None
|
|
rev1 = dupont_df["Revenue"].iloc[1] if "Revenue" in dupont_df.columns else None
|
|
rev_yoy = ((rev0 - rev1) / rev1 * 100) if (rev0 and rev1 and rev1 != 0) else 0
|
|
def norm_roe(x):
|
|
if x is None: return 50
|
|
return min(100, max(0, (x + 10) / 40 * 100))
|
|
def norm_cr(x):
|
|
if x is None: return 50
|
|
return min(100, max(0, x / 3 * 100))
|
|
def norm_at(x):
|
|
if x is None: return 50
|
|
return min(100, max(0, x * 50))
|
|
def norm_em(x):
|
|
if x is None: return 50
|
|
return min(100, max(0, (x - 0.5) / 2.5 * 100))
|
|
def norm_yoy(x):
|
|
if x is None: return 50
|
|
return min(100, max(0, (x + 20) / 50 * 100))
|
|
return {
|
|
"theta": ["Profitability (ROE)", "Liquidity (Curr.Ratio)", "Efficiency (Asset Turn.)", "Solvency (Equity Mult.)", "Growth (Rev YoY)"],
|
|
"r": [norm_roe(roe), norm_cr(cr), norm_at(at), norm_em(em), norm_yoy(rev_yoy)],
|
|
"labels": ["Profitability (ROE)", "Liquidity (Curr.Ratio)", "Efficiency (Asset Turn.)", "Solvency (Equity Mult.)", "Growth (Rev YoY)"],
|
|
}
|
|
|
|
|
|
def _build_radar_figure(ticker: str) -> "go.Figure":
|
|
"""Plotly radar chart from ticker data."""
|
|
from utils.charts import _build_radar_common
|
|
data = get_radar_metrics_normalized(ticker)
|
|
if not data or not data.get("r"):
|
|
return None
|
|
return _build_radar_common(data["theta"], data["r"])
|
|
|
|
|
|
@st.cache_data(ttl=300)
|
|
def get_piotroski_fscore(ticker: str) -> dict:
|
|
"""Piotroski F-Score (0-9) from last 2 periods. Uses yahooquery then yfinance with TTM fallback. Returns score + criteria + used_ttm."""
|
|
out = {"score": 0, "criteria": [], "used_ttm": False}
|
|
fin, bal, cf = _get_annual_financials_balance_cashflow(ticker)
|
|
if fin is None or fin.empty or bal is None or bal.empty:
|
|
return out
|
|
if cf is None or cf.empty:
|
|
cf = pd.DataFrame()
|
|
try:
|
|
ncol = min(2, len(fin.columns))
|
|
rev = _get_row_series(fin, "Total Revenue", "Revenue")
|
|
ni = _get_row_series(fin, "Net Income", "Net Income Common Stockholders")
|
|
gross = _get_row_series(fin, "Gross Profit")
|
|
ta = _get_row_series(bal, "Total Assets")
|
|
lt_debt = _get_row_series(bal, "Long Term Debt")
|
|
ca = _get_row_series(bal, "Current Assets")
|
|
cl = _get_row_series(bal, "Current Liabilities")
|
|
ocf = _get_row_series(cf, "Operating Cash Flow", "Cash From Operating Activities") if not cf.empty else None
|
|
shares = _get_row_series(bal, "Share Issued") or _get_row_series(bal, "Ordinary Shares Number")
|
|
if shares is None and yf:
|
|
t = yf.Ticker(ticker.upper())
|
|
info = getattr(t, "info", None) or {}
|
|
sh_info = info.get("sharesOutstanding") or info.get("Shares Outstanding")
|
|
if sh_info is not None:
|
|
try:
|
|
sh_float = float(sh_info)
|
|
shares = pd.Series([sh_float] * ncol, index=fin.columns[:ncol])
|
|
except (TypeError, ValueError):
|
|
pass
|
|
def v0(s):
|
|
if s is None or len(s) == 0:
|
|
return None
|
|
x = _safe_float(s.iloc[0])
|
|
return x if (x is not None and x == x and not (isinstance(x, float) and pd.isna(x))) else None
|
|
def v1(s):
|
|
if s is None or len(s) < 2:
|
|
return None
|
|
x = _safe_float(s.iloc[1])
|
|
return x if (x is not None and x == x and not (isinstance(x, float) and pd.isna(x))) else None
|
|
ni0, ni1 = v0(ni), v1(ni)
|
|
ocf0 = v0(ocf) if ocf is not None else None
|
|
ta0, ta1 = v0(ta), v1(ta)
|
|
roa0 = (ni0 / ta0 * 100) if (ni0 is not None and ta0 is not None and ta0 != 0) else None
|
|
roa1 = (ni1 / ta1 * 100) if (ni1 is not None and ta1 is not None and ta1 != 0) else None
|
|
c1 = (ni0 is not None and ni0 > 0)
|
|
c2 = (ocf0 is not None and ocf0 > 0)
|
|
c3 = (roa0 is not None and roa1 is not None and roa0 > roa1)
|
|
c4 = (ocf0 is not None and ni0 is not None and ocf0 > ni0)
|
|
lt0 = v0(lt_debt) or 0
|
|
lt1 = v1(lt_debt) or 0
|
|
c5 = (ta0 is not None and ta0 != 0 and ta1 is not None and ta1 != 0 and (lt0 / ta0) < (lt1 / ta1))
|
|
cl0, cl1 = v0(cl), v1(cl)
|
|
ca0, ca1 = v0(ca), v1(ca)
|
|
cr0 = (ca0 / cl0) if (ca0 is not None and cl0 is not None and cl0 != 0) else None
|
|
cr1 = (ca1 / cl1) if (ca1 is not None and cl1 is not None and cl1 != 0) else None
|
|
c6 = (cr0 is not None and cr1 is not None and cr0 > cr1)
|
|
sh0, sh1 = v0(shares), v1(shares)
|
|
c7 = (sh0 is not None and sh1 is not None and sh0 <= sh1) if (sh0 is not None and sh1 is not None) else True
|
|
rev0, rev1 = v0(rev), v1(rev)
|
|
gm0 = (v0(gross) / rev0 * 100) if (gross is not None and rev0 is not None and rev0 != 0) else None
|
|
gm1 = (v1(gross) / rev1 * 100) if (gross is not None and rev1 is not None and rev1 != 0) else None
|
|
c8 = (gm0 is not None and gm1 is not None and gm0 > gm1)
|
|
at0 = (rev0 / ta0) if (rev0 is not None and ta0 is not None and ta0 != 0) else None
|
|
at1 = (rev1 / ta1) if (rev1 is not None and ta1 is not None and ta1 != 0) else None
|
|
c9 = (at0 is not None and at1 is not None and at0 > at1)
|
|
criteria = [
|
|
("Net Income > 0 (profitability)", c1),
|
|
("Operating Cash Flow > 0 (cash generative)", c2),
|
|
("ROA increased vs prior period (improving returns)", c3),
|
|
("OCF > Net Income (earnings quality, less accruals)", c4),
|
|
("Leverage decreased: LT Debt/Assets lower (less debt)", c5),
|
|
("Current Ratio improved (better liquidity)", c6),
|
|
("No dilution: shares unchanged or lower (no equity raise)", c7),
|
|
("Gross Margin improved (pricing power)", c8),
|
|
("Asset Turnover improved (efficiency)", c9),
|
|
]
|
|
score = sum(1 for _, p in criteria if p)
|
|
out["score"] = score
|
|
out["criteria"] = criteria
|
|
out["used_ttm"] = bool(fin is not None and hasattr(fin, "columns") and len(fin.columns) > 0 and any(str(c).startswith("TTM") for c in fin.columns))
|
|
return out
|
|
except Exception:
|
|
out["used_ttm"] = False
|
|
return out
|
|
|
|
|
|
@st.cache_data(ttl=300)
|
|
def get_sector_specific_metrics(ticker: str, sector: str) -> dict:
|
|
"""Technology: Rule of 40, R&D % revenue. Retail/Consumer: Inventory Turnover, Operating Margin. Financials: ROE, ROA."""
|
|
if not yf:
|
|
return {}
|
|
try:
|
|
t = yf.Ticker(ticker.upper())
|
|
info = t.info or {}
|
|
fin = t.financials
|
|
bal = t.balance_sheet
|
|
if fin is None or fin.empty:
|
|
fin = getattr(t, "quarterly_financials", None)
|
|
if fin is not None and not fin.empty:
|
|
fin = fin.iloc[:, :4].sum(axis=1).to_frame()
|
|
if bal is None or bal.empty:
|
|
bal = getattr(t, "quarterly_balance_sheet", None)
|
|
out = {}
|
|
sector_lower = (sector or "").lower()
|
|
if "technology" in sector_lower or "software" in sector_lower or "tech" in sector_lower:
|
|
rev = _get_row_series(fin, "Total Revenue", "Revenue", "Net Revenue")
|
|
ocf = _get_row_series(t.cashflow or getattr(t, "quarterly_cashflow", None), "Operating Cash Flow", "Cash From Operating Activities")
|
|
capx = _get_row_series(t.cashflow or getattr(t, "quarterly_cashflow", None), "Capital Expenditure", "Capital Expenditures")
|
|
rd = _get_row_series(fin, "Research And Development", "Research And Development Expense")
|
|
if rev is not None and len(rev) > 0:
|
|
r0 = _safe_float(rev.iloc[0])
|
|
if ocf is not None and len(ocf) > 0 and capx is not None and len(capx) > 0:
|
|
fcf = _safe_float(ocf.iloc[0]) - _safe_float(capx.iloc[0])
|
|
out["FCF Margin %"] = round(fcf / r0 * 100, 2) if r0 and fcf is not None else None
|
|
if rd is not None and len(rd) > 0:
|
|
out["R&D % of Revenue"] = round(_safe_float(rd.iloc[0]) / r0 * 100, 2) if r0 else None
|
|
rev_growth = None
|
|
if rev is not None and len(rev) >= 2:
|
|
cur, prev = _safe_float(rev.iloc[0]), _safe_float(rev.iloc[1])
|
|
if prev and prev != 0:
|
|
rev_growth = (cur - prev) / prev * 100
|
|
if rev_growth is not None and "FCF Margin %" in out and out["FCF Margin %"] is not None:
|
|
out["Rule of 40 (Rev Growth + FCF Margin)"] = round(rev_growth + out["FCF Margin %"], 1)
|
|
if "consumer" in sector_lower or "retail" in sector_lower or "cyclical" in sector_lower:
|
|
inv = _get_row_series(bal, "Inventory", "Total Inventory")
|
|
cogs = _get_row_series(fin, "Cost Of Revenue", "Cost Of Goods Sold", "Cost of Goods Sold")
|
|
rev = _get_row_series(fin, "Total Revenue", "Revenue", "Net Revenue")
|
|
op_inc = _get_row_series(fin, "Operating Income", "EBIT")
|
|
if inv is not None and len(inv) > 0 and cogs is not None and len(cogs) > 0:
|
|
inv0 = _safe_float(inv.iloc[0])
|
|
cogs0 = _safe_float(cogs.iloc[0])
|
|
out["Inventory Turnover"] = round(cogs0 / inv0, 2) if inv0 else None
|
|
if rev is not None and len(rev) > 0 and op_inc is not None and len(op_inc) > 0:
|
|
r0 = _safe_float(rev.iloc[0])
|
|
op0 = _safe_float(op_inc.iloc[0])
|
|
out["Operating Margin %"] = round(op0 / r0 * 100, 2) if r0 else None
|
|
if "financial" in sector_lower or "bank" in sector_lower or "insurance" in sector_lower:
|
|
ni = _get_row_series(fin, "Net Income", "Net Income Common Stockholders")
|
|
te = _get_row_series(bal, "Total Stockholder Equity", "Stockholders Equity", "Total Equity Gross Minority Interest")
|
|
ta = _get_row_series(bal, "Total Assets")
|
|
if ni is not None and te is not None and len(ni) > 0 and len(te) > 0:
|
|
te0 = _safe_float(te.iloc[0])
|
|
ni0 = _safe_float(ni.iloc[0])
|
|
out["ROE %"] = round(ni0 / te0 * 100, 2) if te0 else None
|
|
if ni is not None and ta is not None and len(ni) > 0 and len(ta) > 0:
|
|
ta0 = _safe_float(ta.iloc[0])
|
|
ni0 = _safe_float(ni.iloc[0])
|
|
out["ROA %"] = round(ni0 / ta0 * 100, 2) if ta0 else None
|
|
return out
|
|
except Exception:
|
|
return {}
|