Files
shawnkim1997 d337c63976 refactor: modular architecture v3.0 + SEC filing viewer fix + README
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>
2026-03-19 22:11:07 +00:00

173 lines
7.6 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
try:
import yfinance as yf
except ImportError:
yf = None
@st.cache_data(ttl=300)
def get_analyst_consensus(ticker: str) -> dict:
"""Fetch analyst consensus from yfinance."""
out = {"targetMeanPrice": None, "targetHighPrice": None, "targetLowPrice": None, "recommendationKey": "N/A", "revenueGrowth": "N/A", "earningsGrowth": "N/A", "numberOfAnalystOpinions": "N/A", "currentPrice": None}
if not yf or not ticker:
return out
try:
t = yf.Ticker(ticker.upper())
info = t.info or {}
for key in ("targetMeanPrice", "targetHighPrice", "targetLowPrice"):
v = info.get(key)
if v is not None:
try:
out[key] = float(v)
except (TypeError, ValueError):
pass
out["currentPrice"] = info.get("currentPrice") or info.get("regularMarketPrice") or info.get("previousClose")
out["numberOfAnalystOpinions"] = info.get("numberOfAnalystOpinions") or "N/A"
rec = info.get("recommendationKey") or info.get("recommendation")
if rec is not None:
out["recommendationKey"] = str(rec)
rg = info.get("revenueGrowth")
if rg is not None:
try:
out["revenueGrowth"] = f"{float(rg) * 100:.1f}%"
except (TypeError, ValueError):
out["revenueGrowth"] = str(rg)
eg = info.get("earningsGrowth")
if eg is not None:
try:
out["earningsGrowth"] = f"{float(eg) * 100:.1f}%"
except (TypeError, ValueError):
out["earningsGrowth"] = str(eg)
return out
except Exception:
return out
@st.cache_data(ttl=300)
def get_dcf_smart_defaults(ticker: str) -> dict:
"""Smart default assumptions: WACC from CAPM (Beta), Terminal Growth = 2.5%, FCF Growth from revenueGrowth/earningsGrowth or 8%."""
out = {"wacc_pct": 10.0, "term_growth_pct": 2.5, "fcf_growth_pct": 8.0}
if not yf or not ticker:
return out
try:
t = yf.Ticker(ticker.upper())
info = t.info or {}
beta = info.get("beta")
if beta is None:
beta = 1.0
else:
try:
beta = float(beta)
except (TypeError, ValueError):
beta = 1.0
risk_free = 4.0
market_risk_premium = 5.0
calculated_wacc = risk_free + (beta * market_risk_premium)
out["wacc_pct"] = round(min(20.0, max(4.0, calculated_wacc)), 1)
out["term_growth_pct"] = 2.5
rev_growth = info.get("revenueGrowth") or info.get("earningsGrowth")
if rev_growth is not None:
try:
g = float(rev_growth)
out["fcf_growth_pct"] = round(min(30.0, max(-10.0, g * 100)), 1)
except (TypeError, ValueError):
pass
return out
except Exception:
return out
@st.cache_data(ttl=300)
def get_fcff_fcfe_valuation(ticker: str) -> dict:
"""FCFF/FCFE two-stage valuation model. Returns dict with fcff, fcfe, and per-share values."""
if not yf:
return {}
try:
t = yf.Ticker(ticker.upper())
info = t.info or {}
fin = t.financials
bal = t.balance_sheet
cf = t.cashflow
if fin is None or fin.empty:
return {}
# Get latest year data
rev = _get_row_series(fin, "Total Revenue", "Revenue")
ebit = _get_row_series(fin, "EBIT", "Operating Income")
ni = _get_row_series(fin, "Net Income", "Net Income Common Stockholders")
ocf = _get_row_series(cf, "Operating Cash Flow", "Cash From Operating Activities") if cf is not None else None
capx = _get_row_series(cf, "Capital Expenditure", "Capital Expenditures") if cf is not None else None
dep = _get_row_series(cf, "Depreciation And Amortization", "Depreciation & Amortization") if cf is not None else None
r0 = _safe_float(rev.iloc[0]) if rev is not None and len(rev) > 0 else None
ebit0 = _safe_float(ebit.iloc[0]) if ebit is not None and len(ebit) > 0 else None
ni0 = _safe_float(ni.iloc[0]) if ni is not None and len(ni) > 0 else None
ocf0 = _safe_float(ocf.iloc[0]) if ocf is not None and len(ocf) > 0 else None
capx0 = abs(_safe_float(capx.iloc[0]) or 0) if capx is not None and len(capx) > 0 else 0
dep0 = _safe_float(dep.iloc[0]) if dep is not None and len(dep) > 0 else 0
# Tax rate estimation
tax_expense = _get_row_series(fin, "Tax Provision", "Income Tax Expense")
pretax = _get_row_series(fin, "Pretax Income", "Income Before Tax")
tax_rate = 0.21 # default US corporate
if tax_expense is not None and pretax is not None and len(tax_expense) > 0 and len(pretax) > 0:
te = _safe_float(tax_expense.iloc[0])
pt = _safe_float(pretax.iloc[0])
if pt and pt > 0 and te is not None:
tax_rate = min(max(te / pt, 0.05), 0.40)
# Balance sheet items
total_debt_series = _get_row_series(bal, "Total Debt") if bal is not None else None
cash_series = _get_row_series(bal, "Cash And Cash Equivalents", "Cash Cash Equivalents And Short Term Investments") if bal is not None else None
equity_series = _get_row_series(bal, "Total Stockholder Equity", "Stockholders Equity", "Total Equity Gross Minority Interest") if bal is not None else None
total_debt = _safe_float(total_debt_series.iloc[0]) if total_debt_series is not None and len(total_debt_series) > 0 else 0
cash_val = _safe_float(cash_series.iloc[0]) if cash_series is not None and len(cash_series) > 0 else 0
equity_val = _safe_float(equity_series.iloc[0]) if equity_series is not None and len(equity_series) > 0 else 0
shares = info.get("sharesOutstanding") or info.get("impliedSharesOutstanding") or 1
beta = info.get("beta") or 1.0
# FCFF = EBIT(1-t) + D&A - CapEx - delta WC (approximate)
fcff = None
if ebit0 is not None:
fcff = ebit0 * (1 - tax_rate) + (dep0 or 0) - capx0
# FCFE = Net Income + D&A - CapEx - delta WC + Net Borrowing (approximate as NI + D&A - CapEx)
fcfe = None
if ni0 is not None:
fcfe = ni0 + (dep0 or 0) - capx0
# WACC components
rf = 0.045 # risk-free rate
erp = 0.055 # equity risk premium
cost_of_equity = rf + beta * erp
cost_of_debt = 0.05 # approximate
if total_debt and equity_val and (total_debt + equity_val) > 0:
debt_weight = total_debt / (total_debt + equity_val)
equity_weight = equity_val / (total_debt + equity_val)
else:
debt_weight, equity_weight = 0.2, 0.8
wacc = equity_weight * cost_of_equity + debt_weight * cost_of_debt * (1 - tax_rate)
# Margins
fcff_margin = (fcff / r0 * 100) if fcff and r0 and r0 > 0 else None
fcfe_margin = (fcfe / r0 * 100) if fcfe and r0 and r0 > 0 else None
return {
"fcff": fcff, "fcfe": fcfe, "fcff_margin": fcff_margin, "fcfe_margin": fcfe_margin,
"ebit": ebit0, "net_income": ni0, "revenue": r0,
"tax_rate": tax_rate * 100, "depreciation": dep0, "capex": capx0,
"total_debt": total_debt, "cash": cash_val, "equity": equity_val,
"shares": shares, "beta": beta,
"wacc": wacc * 100, "cost_of_equity": cost_of_equity * 100, "cost_of_debt": cost_of_debt * 100,
"debt_weight": debt_weight * 100, "equity_weight": equity_weight * 100,
}
except Exception:
return {}