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

141 lines
6.4 KiB
Python

"""
Plotly chart builders: Sankey, Radar, dark theme.
"""
try:
import plotly.graph_objects as go
except ImportError:
go = None
def _build_sankey_figure(data: dict) -> "go.Figure":
"""Sankey: Revenue -> COGS + Gross Profit; Gross Profit -> OpEx + OpInc; OpInc -> Tax/Interest/Other + Net Income."""
if go is None:
return None
rev, cogs, gp, opex, opinc, tax_other, ni = (
data["revenue"], data["cogs"], data["gross_profit"], data["opex"],
data["operating_income"], data["tax_interest_other"], data["net_income"],
)
if rev <= 0:
return None
# Format labels with dollar values
def _fmt(label, val):
if abs(val) >= 1e9:
return f"{label}<br>${val/1e9:.1f}B"
if abs(val) >= 1e6:
return f"{label}<br>${val/1e6:.0f}M"
return label
nodes = [
_fmt("Revenue", rev), _fmt("Cost of Revenue", cogs), _fmt("Gross Profit", gp),
_fmt("Operating Exp.", opex), _fmt("Operating Inc.", opinc),
_fmt("Tax/Int./Other", tax_other), _fmt("Net Income", ni),
]
node_colors = [
"#3B82F6", # Revenue — blue
"#F87171", # COGS — red
"#34D399", # Gross Profit — green
"#FB923C", # OpEx — orange
"#60A5FA", # Operating Income — light blue
"#9CA3AF", # Tax/Interest — grey
"#10B981", # Net Income — bright green
]
link_colors = [
"rgba(248,113,113,0.3)", # Rev -> COGS (red flow)
"rgba(52,211,153,0.3)", # Rev -> GP (green flow)
"rgba(251,146,60,0.3)", # GP -> OpEx (orange flow)
"rgba(96,165,250,0.3)", # GP -> OpInc (blue flow)
"rgba(156,163,175,0.25)", # OpInc -> Tax (grey flow)
"rgba(16,185,129,0.35)", # OpInc -> NI (green flow)
]
source = [0, 0, 2, 2, 4, 4]
target = [1, 2, 3, 4, 5, 6]
value = [max(0, float(v)) for v in [cogs, gp, opex, opinc, tax_other, ni]]
fig = go.Figure(data=[go.Sankey(
node=dict(label=nodes, color=node_colors, pad=20, thickness=24,
line=dict(color="rgba(255,255,255,0.1)", width=1)),
link=dict(source=source, target=target, value=value, color=link_colors),
)])
fig.update_layout(
title=dict(text="Income Statement Flow", font=dict(size=14, color="#F3F4F6", family="Inter")),
height=420, margin=dict(t=45, b=15, l=10, r=10),
font=dict(size=12, color="#D1D5DB", family="Inter"),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
)
return fig
def _build_radar_common(theta_list, r_list, title_text="Financial Health Radar") -> "go.Figure":
"""Shared radar chart builder with Soft Navy theme."""
if go is None:
return None
theta = theta_list + [theta_list[0]]
r = r_list + [r_list[0]]
fig = go.Figure()
# Add a "benchmark 50" ring for reference
fig.add_trace(go.Scatterpolar(
r=[50] * (len(theta_list) + 1), theta=theta,
fill="toself", fillcolor="rgba(255,255,255,0.02)",
line=dict(color="rgba(255,255,255,0.1)", width=1, dash="dot"),
name="Avg (50)", hoverinfo="skip",
))
fig.add_trace(go.Scatterpolar(
r=r, theta=theta, fill="toself",
fillcolor="rgba(59, 130, 246, 0.2)",
line=dict(color="#60A5FA", width=2.5),
marker=dict(size=6, color="#60A5FA", symbol="circle"),
name="Score",
))
fig.update_layout(
polar=dict(
bgcolor="rgba(0,0,0,0)",
radialaxis=dict(visible=True, range=[0, 100], tickvals=[20, 40, 60, 80],
tickfont=dict(size=9, color="#4B5563", family="JetBrains Mono"),
gridcolor="rgba(255,255,255,0.06)", linecolor="rgba(255,255,255,0.06)"),
angularaxis=dict(tickfont=dict(size=11, color="#D1D5DB", family="Inter"),
gridcolor="rgba(255,255,255,0.06)", linecolor="rgba(255,255,255,0.08)"),
),
title=dict(text=title_text, font=dict(size=14, color="#F3F4F6", family="Inter")),
height=420, showlegend=False,
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
margin=dict(t=45, b=25, l=60, r=60),
)
return fig
def _build_radar_figure_from_metrics(metrics: dict) -> "go.Figure":
"""Build radar chart from precomputed metrics dict."""
if not metrics or not metrics.get("r"):
return None
return _build_radar_common(metrics["theta"], metrics["r"], "Financial Health Radar (10-K Item 8)")
def _radar_norm(roe_pct, current_ratio, asset_turnover, equity_mult, rev_yoy_pct):
"""Normalize 5 raw metrics to 0-100 for radar (same logic as get_radar_metrics_normalized)."""
def n_roe(x): return min(100, max(0, (x + 10) / 40 * 100)) if x is not None else 50
def n_cr(x): return min(100, max(0, x / 3 * 100)) if x is not None else 50
def n_at(x): return min(100, max(0, x * 50)) if x is not None else 50
def n_em(x): return min(100, max(0, (x - 0.5) / 2.5 * 100)) if x is not None else 50
def n_yoy(x): return min(100, max(0, (x + 20) / 50 * 100)) if x is not None else 50
return [n_roe(roe_pct), n_cr(current_ratio), n_at(asset_turnover), n_em(equity_mult), n_yoy(rev_yoy_pct)]
def _build_radar_from_manual(roe_pct, current_ratio, asset_turnover, equity_mult, rev_yoy_pct) -> "go.Figure":
"""Build radar chart from 5 manually entered ratios (fallback)."""
theta = ["Profitability (ROE)", "Liquidity (Curr.Ratio)", "Efficiency (Asset Turn.)", "Solvency (Equity Mult.)", "Growth (Rev YoY)"]
r = _radar_norm(roe_pct, current_ratio, asset_turnover, equity_mult, rev_yoy_pct)
return _build_radar_common(theta, r, "Financial Health Radar (Manual)")
def _apply_dark_theme(fig):
"""Apply Soft Navy theme to Plotly figures."""
fig.update_layout(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(255,255,255,0.02)',
font=dict(color='#D1D5DB', family='Inter, JetBrains Mono, sans-serif', size=12),
xaxis=dict(gridcolor='rgba(255,255,255,0.05)', zerolinecolor='rgba(255,255,255,0.08)', tickfont=dict(family='JetBrains Mono', size=11)),
yaxis=dict(gridcolor='rgba(255,255,255,0.05)', zerolinecolor='rgba(255,255,255,0.08)', tickfont=dict(family='JetBrains Mono', size=11)),
legend=dict(bgcolor='rgba(0,0,0,0)', bordercolor='rgba(255,255,255,0.06)', font=dict(size=11)),
title_font=dict(color='#F3F4F6', size=14),
margin=dict(l=40, r=20, t=40, b=40),
)
return fig