Files
All-in-one-Financial-Analysis/views/tab1_quant.py
T

210 lines
12 KiB
Python
Raw Normal View History

"""Tab 1 — Financial Health (Tables & Charts): Sankey, Radar, F-Score,
Altman Z, red flags, sector metrics, YoY changes, quarterly momentum/ratios."""
import pandas as pd
import streamlit as st
from data.sec_downloader import get_10k_sections
from data.ratios import get_dupont_altman_redflags_yoy, get_quarterly_momentum, get_quarterly_ratio_changes
from data.scores import (
get_income_statement_sankey_data, get_radar_metrics_normalized, _build_radar_figure,
get_piotroski_fscore, get_sector_specific_metrics,
)
from data.scores_ai import sankey_data_from_ai, piotroski_from_ai, radar_metrics_from_ai
from ai.gemini_sec import get_sec_financials_llm
from utils.charts import (
_build_sankey_figure, _build_radar_figure_from_metrics,
_build_radar_from_manual, _apply_dark_theme,
)
try:
from yahooquery import Ticker as YQTicker
except ImportError:
YQTicker = None
def _style_change_column(df: pd.DataFrame):
"""Green for improvement (+), red for decline (-) in Change column."""
change_col = "Change (%)" if "Change (%)" in df.columns else "Change"
if change_col not in df.columns or df.empty:
return df.style
def _cell_style(v):
if v is None or (isinstance(v, float) and pd.isna(v)):
return ""
s = str(v).strip()
if s == "—":
return ""
if s.startswith("+") or "↑" in s:
return "background-color: #d4edda; color: #155724"
if s.startswith("-") or "↓" in s:
return "background-color: #f8d7da; color: #721c24"
return ""
return df.style.apply(lambda col: [_cell_style(v) for v in col], subset=[change_col])
def render_tab1_quantitative(ticker, quant_ticker, market, sector, industry, google_api_key, sec_email):
"""Render the Financial Health section of Tab 1."""
ai_data = {}
if market and "US" in market and google_api_key and sec_email:
with st.spinner("SEC 10-K 원본에서 재무제표 데이터를 해독하여 그래프를 생성 중입니다... (약 30~60초 소요)"):
sections, _ = get_10k_sections(ticker, sec_email)
item8 = (sections or {}).get("item8") or ""
if item8.strip():
ai_data = get_sec_financials_llm(google_api_key, item8, ticker)
q = get_dupont_altman_redflags_yoy(quant_ticker)
dupont_df = (q or {}).get("dupont") if q else None
if q or ai_data:
st.markdown("---")
st.markdown("#### 📊 Financial Health (Tables & Charts)")
c1, c2 = st.columns(2)
with c1:
if ai_data and ai_data.get("current_yr"):
sankey_data = sankey_data_from_ai(ai_data)
else:
sankey_data = get_income_statement_sankey_data(quant_ticker)
if sankey_data.get("revenue", 0) > 0:
fig_sankey = _build_sankey_figure(sankey_data)
if fig_sankey is not None:
_apply_dark_theme(fig_sankey)
st.plotly_chart(fig_sankey, use_container_width=True)
else:
st.caption("Income Statement flow: data not available.")
with c2:
if ai_data and ai_data.get("current_yr"):
radar_metrics = radar_metrics_from_ai(ai_data)
fig_radar = _build_radar_figure_from_metrics(radar_metrics) if radar_metrics else None
else:
fig_radar = _build_radar_figure(quant_ticker)
if fig_radar is not None:
_apply_dark_theme(fig_radar)
st.plotly_chart(fig_radar, use_container_width=True)
else:
st.caption("Financial radar: need 2+ years of data.")
with st.expander("Manual Data Entry (Radar Chart Fallback)", expanded=False):
st.caption("Enter 5 key ratios to plot a custom radar. ROE %, Current Ratio, Asset Turnover, Equity Mult., Revenue YoY %.")
roe_man = st.number_input("ROE %", value=15.0, min_value=-50.0, max_value=100.0, step=1.0, key="radar_roe")
cr_man = st.number_input("Current Ratio", value=1.5, min_value=0.0, max_value=10.0, step=0.1, key="radar_cr")
at_man = st.number_input("Asset Turnover", value=0.8, min_value=0.0, max_value=5.0, step=0.1, key="radar_at")
em_man = st.number_input("Equity Mult.", value=2.0, min_value=0.5, max_value=10.0, step=0.1, key="radar_em")
yoy_man = st.number_input("Revenue YoY %", value=10.0, min_value=-50.0, max_value=200.0, step=1.0, key="radar_yoy")
if st.button("Plot Radar", key="radar_plot_btn"):
fig_man = _build_radar_from_manual(roe_man, cr_man, at_man, em_man, yoy_man)
if fig_man is not None:
st.session_state["radar_manual_fig"] = fig_man
if st.session_state.get("radar_manual_fig") is not None:
_apply_dark_theme(st.session_state["radar_manual_fig"])
st.plotly_chart(st.session_state["radar_manual_fig"], use_container_width=True)
with st.expander("Debug: Raw YahooQuery Data", expanded=False):
if YQTicker and quant_ticker:
try:
yq_ticker = YQTicker(quant_ticker.upper())
inc_raw = yq_ticker.income_statement(trailing=False)
bal_raw = yq_ticker.balance_sheet(trailing=False)
if inc_raw is not None and not inc_raw.empty:
st.caption("Income statement (last 2 periods) — check column names for mapping.")
st.dataframe(inc_raw.tail(2), use_container_width=True, hide_index=True)
else:
st.caption("Income statement: no data.")
if bal_raw is not None and not bal_raw.empty:
st.caption("Balance sheet (last 2 periods) — check column names for mapping.")
st.dataframe(bal_raw.tail(2), use_container_width=True, hide_index=True)
else:
st.caption("Balance sheet: no data.")
except Exception as e:
st.error(f"YahooQuery debug failed: {e}")
else:
st.caption("YahooQuery not available or no ticker selected.")
if ai_data and ai_data.get("current_yr"):
piot = piotroski_from_ai(ai_data)
else:
piot = get_piotroski_fscore(quant_ticker)
st.markdown("**Piotroski F-Score (9-point checklist)**")
score = piot.get("score", 0)
legend = "**Score 89: Excellent** · 47: Average · 03: High Risk"
st.metric("F-Score", f"{score} / 9", legend)
if ai_data and ai_data.get("current_yr"):
st.caption("*(from SEC 10-K Item 8)*")
elif piot.get("used_ttm"):
st.caption("*(Estimated via TTM Data)*")
st.caption("✅ = Good (passes criterion). ❌ = Fails criterion.")
criteria = piot.get("criteria", [])
if criteria:
cols = st.columns(3)
for i, (label, passed) in enumerate(criteria):
with cols[i % 3]:
st.caption(("✅ " if passed else "❌ ") + label)
az = (q or {}).get("altman_z")
if az is not None:
st.caption(f"**Altman Z-Score:** {az} (Safe > 2.99 · Grey 1.812.99 · Distress < 1.81)")
red_flags = (q or {}).get("red_flags") or []
if red_flags:
for rf in red_flags:
val = rf.get("value")
val_str = "N/A" if (val is None or (isinstance(val, float) and (pd.isna(val) or val != val))) else val
st.warning(f"**{rf.get('metric')}:** {val_str} (threshold: {rf.get('threshold')})")
elif dupont_df is not None and not dupont_df.empty:
st.success("No red flags (Current Ratio ≥ 1.0, Interest Coverage ≥ 1.5).")
sector_metrics = get_sector_specific_metrics(quant_ticker, sector) if quant_ticker else {}
if sector_metrics:
st.markdown("**Sector-specific metrics**")
cols = st.columns(min(len(sector_metrics), 4))
for i, (k, v) in enumerate(sector_metrics.items()):
with cols[i % len(cols)]:
disp = f"{v}" if v is not None else "N/A"
st.metric(k, disp, None)
yoy_list = (q or {}).get("yoy") or []
if yoy_list:
st.markdown("**YoY ratio changes**")
rows_yoy = []
for item in yoy_list:
cur = item.get("Latest")
if cur is not None and isinstance(cur, (int, float)):
cur = round(cur, 2)
chg_pp = item.get("YoY (pp)")
chg_pct = item.get("YoY %")
if chg_pp is not None:
chg_str = f"{chg_pp:+.1f}%"
elif chg_pct is not None:
chg_str = f"{chg_pct:+.1f}%"
else:
chg_str = "—"
status = "↑" if (chg_pp is not None and chg_pp > 0) or (chg_pct is not None and chg_pct > 0) else ("↓" if (chg_pp is not None and chg_pp < 0) or (chg_pct is not None and chg_pct < 0) else "—")
cur_disp = f"{cur:.2f}" if isinstance(cur, (int, float)) else ("—" if cur is None else str(cur))
rows_yoy.append({"Metric": item.get("Ratio"), "Current Value": cur_disp, "Change (%)": chg_str, "Status": status})
if rows_yoy:
df_yoy = pd.DataFrame(rows_yoy)
st.dataframe(_style_change_column(df_yoy), use_container_width=True, hide_index=True)
st.markdown("**Quarter ratio changes**")
qmom = get_quarterly_momentum(quant_ticker)
qoq_rows = get_quarterly_ratio_changes(quant_ticker)
qoq_r, qoq_n = qmom.get("qoq_revenue_pct"), qmom.get("qoq_ni_pct")
build = []
if qoq_r is not None:
build.append({"Metric": "Revenue", "Current Value": "—", "Change (%)": f"{qoq_r:+.1f}%", "Status": "↑" if qoq_r > 0 else "↓"})
if qoq_n is not None:
build.append({"Metric": "Net Income", "Current Value": "—", "Change (%)": f"{qoq_n:+.1f}%", "Status": "↑" if qoq_n > 0 else "↓"})
for r in qoq_rows:
r_copy = dict(r)
if "Current Value" in r_copy:
v = r_copy["Current Value"]
if isinstance(v, (int, float)):
r_copy["Current Value"] = f"{round(v, 2):.2f}"
elif v is None:
r_copy["Current Value"] = "—"
else:
r_copy["Current Value"] = str(v)
if "Change" in r_copy and "Change (%)" not in r_copy:
r_copy["Change (%)"] = r_copy.pop("Change", "—")
if "Trend" in r_copy:
r_copy["Status"] = r_copy.pop("Trend", "—")
build.append(r_copy)
if build:
df_q = pd.DataFrame(build)
if "Change" in df_q.columns and "Change (%)" not in df_q.columns:
df_q = df_q.rename(columns={"Change": "Change (%)"})
if "Trend" in df_q.columns:
df_q = df_q.rename(columns={"Trend": "Status"})
st.dataframe(_style_change_column(df_q), use_container_width=True, hide_index=True)
elif not qmom.get("df") or qmom["df"].empty:
st.caption("Quarterly data not available for this ticker.")
else:
st.info("Quantitative data not available for this ticker.")