mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-22 07:08:04 +00:00
feat: add Atlas Terminal — Next.js 14 + FastAPI full-stack migration
Complete migration from Streamlit to Next.js 14 App Router + FastAPI backend. Frontend (Next.js 14): - 10 pages: Overview, Research, Valuation, Technical, Markets, Earnings, News, Portfolio, Filings, Settings - Terminal Noir dark theme with custom Tailwind config - TradingView Lightweight Charts for candlestick/volume - Valuation: DCF, Sensitivity Matrix, Monte Carlo, Tornado, Reverse DCF - Financial Statements table with YoY growth badges and margin rows - SEC EDGAR inline filing viewer with section tabs - News split-view with iframe article embedding - Technical Analysis with RSI, MACD, Bollinger, Fibonacci, Moving Averages - Earnings beat/miss visualization - AI Copilot chat panel with Gemini integration Backend (FastAPI): - 13 routers: market_data, financials, valuation, technical, earnings, insider, edgar, news, portfolio, analysis, chat, estimates, fx - Services: DCF engine, Monte Carlo simulation, sensitivity analysis, risk metrics, SEC parser, technical indicators - yfinance + yahooquery data sources with fallback pattern - SQLite caching layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
56a9561f71
commit
b2acda81ee
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Tab 10 — Company Valuation
|
||||
PER, PBR, PSR, P/OCF cards with 5Y history, industry comparison, historical chart.
|
||||
"""
|
||||
import streamlit as st
|
||||
from config.constants import MARKET_OPTIONS
|
||||
from utils.ticker import get_global_ticker
|
||||
from data.valuation_metrics import (
|
||||
get_valuation_multiples, get_historical_multiples,
|
||||
get_industry_avg_multiples, get_pe_history_chart_data,
|
||||
)
|
||||
from data.fundamentals import get_sector_industry
|
||||
|
||||
try:
|
||||
import plotly.graph_objects as go
|
||||
except ImportError:
|
||||
go = None
|
||||
|
||||
|
||||
def _metric_card(label, value, sub_items: dict, col):
|
||||
"""Render a single valuation metric card."""
|
||||
with col:
|
||||
val_str = f"{value:.2f}" if isinstance(value, (int, float)) and value == value else "N/A"
|
||||
st.markdown(
|
||||
f'<div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);'
|
||||
f'border-radius:10px;padding:16px 20px;min-height:160px;">'
|
||||
f'<div style="color:#9CA3AF;font-size:0.8rem;font-weight:600;">{label}</div>'
|
||||
f'<div style="color:#F3F4F6;font-size:2rem;font-weight:700;margin:4px 0;">{val_str}</div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
for k, v in sub_items.items():
|
||||
v_str = f"{v:.2f}" if isinstance(v, (int, float)) and v == v else "N/A"
|
||||
color = "#9CA3AF"
|
||||
st.markdown(
|
||||
f'<div style="color:{color};font-size:0.75rem;padding:1px 0;">'
|
||||
f'{k}: <b>{v_str}</b></div>', unsafe_allow_html=True,
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
|
||||
def _render_pe_chart(ticker: str, quant_ticker: str):
|
||||
"""Render 5Y price chart with PE overlay."""
|
||||
if not go:
|
||||
return
|
||||
df = get_pe_history_chart_data(quant_ticker)
|
||||
if df.empty:
|
||||
return
|
||||
st.markdown("#### Historical Price (5Y)")
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["Date"], y=df["Close"], mode="lines",
|
||||
name="Price", line=dict(color="#34D399", width=2),
|
||||
))
|
||||
fig.update_layout(
|
||||
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#E5E7EB", height=300,
|
||||
margin=dict(t=20, b=30, l=50, r=20),
|
||||
xaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
|
||||
yaxis=dict(gridcolor="rgba(255,255,255,0.05)", title="Price ($)"),
|
||||
showlegend=False,
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
|
||||
def _render_comparison_bar(label, current, avg_5y, industry_avg):
|
||||
"""Render a horizontal comparison bar for a metric."""
|
||||
vals = {"Current": current, "5Y Avg": avg_5y, "Industry": industry_avg}
|
||||
items = []
|
||||
for k, v in vals.items():
|
||||
if v and isinstance(v, (int, float)) and v == v:
|
||||
items.append(f'<span style="color:#9CA3AF;font-size:0.8rem;">{k}: </span>'
|
||||
f'<span style="color:#F3F4F6;font-weight:600;">{v:.2f}</span>')
|
||||
if items:
|
||||
st.markdown(
|
||||
f'<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.06);">'
|
||||
f'<span style="color:#60A5FA;font-weight:600;min-width:80px;display:inline-block;">{label}</span>'
|
||||
f'{" · ".join(items)}</div>', unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
|
||||
def render_tab10(ticker):
|
||||
"""Render the Company Valuation tab."""
|
||||
market = st.session_state.get("market") or MARKET_OPTIONS[0]
|
||||
quant_ticker = get_global_ticker(ticker, market) if ticker else ""
|
||||
st.subheader("Company Valuation")
|
||||
|
||||
if not ticker:
|
||||
st.info("Select a company from the sidebar to view valuation metrics.")
|
||||
return
|
||||
|
||||
si = get_sector_industry(quant_ticker)
|
||||
sector = si.get("sector", "N/A")
|
||||
industry = si.get("industry", "N/A")
|
||||
st.caption(f"**{ticker}** · {sector} · {industry}")
|
||||
|
||||
with st.spinner("Fetching valuation data..."):
|
||||
multiples = get_valuation_multiples(quant_ticker)
|
||||
hist = get_historical_multiples(quant_ticker)
|
||||
ind_avg = get_industry_avg_multiples(quant_ticker)
|
||||
|
||||
if not multiples:
|
||||
st.warning("Could not fetch valuation data for this ticker.")
|
||||
return
|
||||
|
||||
# ── Metric Cards: PER, PBR, PSR, P/OCF ──
|
||||
c1, c2, c3, c4 = st.columns(4)
|
||||
_metric_card("PER (Trailing)", multiples.get("PER"), {
|
||||
"Forward": multiples.get("Forward PER"),
|
||||
"5Y Avg": hist.get("5Y Avg PER"),
|
||||
"Industry": ind_avg.get("Industry Avg PER"),
|
||||
}, c1)
|
||||
_metric_card("PBR", multiples.get("PBR"), {
|
||||
"5Y Avg": hist.get("5Y Avg PBR"),
|
||||
"Industry": ind_avg.get("Industry Avg PBR"),
|
||||
}, c2)
|
||||
_metric_card("PSR", multiples.get("PSR"), {
|
||||
"Industry": ind_avg.get("Industry Avg PSR"),
|
||||
"EV/Revenue": multiples.get("EV/Revenue"),
|
||||
}, c3)
|
||||
_metric_card("P/OCF", multiples.get("P/OCF"), {
|
||||
"EV/EBITDA": multiples.get("EV/EBITDA"),
|
||||
"PEG Ratio": multiples.get("PEG"),
|
||||
}, c4)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ── Comparison Table ──
|
||||
left, right = st.columns([3, 2])
|
||||
with left:
|
||||
st.markdown("#### Valuation Comparison")
|
||||
_render_comparison_bar("PER", multiples.get("PER"),
|
||||
hist.get("5Y Avg PER"), ind_avg.get("Industry Avg PER"))
|
||||
_render_comparison_bar("PBR", multiples.get("PBR"),
|
||||
hist.get("5Y Avg PBR"), ind_avg.get("Industry Avg PBR"))
|
||||
_render_comparison_bar("PSR", multiples.get("PSR"), None,
|
||||
ind_avg.get("Industry Avg PSR"))
|
||||
_render_comparison_bar("EV/EBITDA", multiples.get("EV/EBITDA"), None, None)
|
||||
|
||||
# Premium/Discount indicator
|
||||
pe_cur = multiples.get("PER")
|
||||
pe_5y = hist.get("5Y Avg PER")
|
||||
if pe_cur and pe_5y and pe_5y > 0:
|
||||
prem = (pe_cur - pe_5y) / pe_5y * 100
|
||||
color = "#F87171" if prem > 0 else "#34D399"
|
||||
word = "premium" if prem > 0 else "discount"
|
||||
st.markdown(
|
||||
f'<div style="margin-top:12px;padding:10px;background:rgba(255,255,255,0.03);'
|
||||
f'border-radius:8px;"><span style="color:{color};font-weight:700;">'
|
||||
f'{abs(prem):.1f}% {word}</span>'
|
||||
f' <span style="color:#9CA3AF;">vs 5Y average PER</span></div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
with right:
|
||||
_render_pe_chart(ticker, quant_ticker)
|
||||
|
||||
# ── Additional Metrics Table ──
|
||||
st.markdown("---")
|
||||
st.markdown("#### Additional Metrics")
|
||||
m1, m2, m3, m4 = st.columns(4)
|
||||
with m1:
|
||||
beta = multiples.get("Beta")
|
||||
st.metric("Beta", f"{beta:.2f}" if beta else "N/A")
|
||||
with m2:
|
||||
dy = multiples.get("Dividend Yield")
|
||||
st.metric("Div Yield", f"{dy*100:.2f}%" if dy else "N/A")
|
||||
with m3:
|
||||
h52 = multiples.get("52W High")
|
||||
st.metric("52W High", f"${h52:,.2f}" if h52 else "N/A")
|
||||
with m4:
|
||||
l52 = multiples.get("52W Low")
|
||||
st.metric("52W Low", f"${l52:,.2f}" if l52 else "N/A")
|
||||
|
||||
# ── KPI Analysis ──
|
||||
from views.tab10_kpi import render_kpi_section
|
||||
render_kpi_section(ticker, sector, industry)
|
||||
Reference in New Issue
Block a user