mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-06 23:37:44 +00:00
b2acda81ee
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>
142 lines
4.7 KiB
Python
142 lines
4.7 KiB
Python
"""
|
|
Valuation metrics — PER, PBR, PSR, P/OCF, EV/EBITDA with historical & industry comparison.
|
|
"""
|
|
import streamlit as st
|
|
import pandas as pd
|
|
|
|
try:
|
|
import yfinance as yf
|
|
except ImportError:
|
|
yf = None
|
|
|
|
try:
|
|
from yahooquery import Ticker as YQTicker
|
|
except ImportError:
|
|
YQTicker = None
|
|
|
|
|
|
@st.cache_data(ttl=300)
|
|
def get_valuation_multiples(ticker: str) -> dict:
|
|
"""Fetch current valuation multiples for a ticker."""
|
|
if not yf or not ticker:
|
|
return {}
|
|
try:
|
|
t = yf.Ticker(ticker)
|
|
info = t.info or {}
|
|
price = info.get("regularMarketPrice") or info.get("currentPrice") or 0
|
|
mcap = info.get("marketCap") or 0
|
|
# P/OCF calculation
|
|
ocf = info.get("operatingCashflow")
|
|
shares = info.get("sharesOutstanding") or 1
|
|
p_ocf = (price / (ocf / shares)) if ocf and shares and ocf > 0 else None
|
|
|
|
return {
|
|
"PER": info.get("trailingPE"),
|
|
"Forward PER": info.get("forwardPE"),
|
|
"PBR": info.get("priceToBook"),
|
|
"PSR": info.get("priceToSalesTrailing12Months"),
|
|
"P/OCF": round(p_ocf, 2) if p_ocf else None,
|
|
"EV/EBITDA": info.get("enterpriseToEbitda"),
|
|
"EV/Revenue": info.get("enterpriseToRevenue"),
|
|
"PEG": info.get("pegRatio"),
|
|
"Dividend Yield": info.get("dividendYield"),
|
|
"Price": price,
|
|
"Market Cap": mcap,
|
|
"52W High": info.get("fiftyTwoWeekHigh"),
|
|
"52W Low": info.get("fiftyTwoWeekLow"),
|
|
"Beta": info.get("beta"),
|
|
}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
@st.cache_data(ttl=600)
|
|
def get_historical_multiples(ticker: str) -> dict:
|
|
"""Calculate 5Y average PE, PB, PS from historical data."""
|
|
if not yf or not ticker:
|
|
return {}
|
|
try:
|
|
t = yf.Ticker(ticker)
|
|
info = t.info or {}
|
|
hist = t.history(period="5y", interval="1mo")
|
|
if hist is None or hist.empty:
|
|
return {}
|
|
eps = info.get("trailingEps")
|
|
bvps = info.get("bookValue")
|
|
|
|
result = {}
|
|
if eps and eps > 0:
|
|
pe_series = hist["Close"] / eps
|
|
result["5Y Avg PER"] = round(pe_series.mean(), 2)
|
|
result["5Y High PER"] = round(pe_series.max(), 2)
|
|
result["5Y Low PER"] = round(pe_series.min(), 2)
|
|
if bvps and bvps > 0:
|
|
pb_series = hist["Close"] / bvps
|
|
result["5Y Avg PBR"] = round(pb_series.mean(), 2)
|
|
return result
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
@st.cache_data(ttl=600)
|
|
def get_industry_avg_multiples(ticker: str) -> dict:
|
|
"""Get industry peer average multiples for comparison."""
|
|
if not yf or not ticker:
|
|
return {}
|
|
try:
|
|
t = yf.Ticker(ticker)
|
|
info = t.info or {}
|
|
industry = info.get("industry", "")
|
|
sector = info.get("sector", "")
|
|
if not industry:
|
|
return {"industry": "N/A"}
|
|
|
|
# Use sector-based peer mapping
|
|
from config.constants import SECTORS
|
|
peers = []
|
|
sector_lower = sector.lower() if sector else ""
|
|
for sec_name, tickers_list in SECTORS.items():
|
|
if any(k in sector_lower for k in sec_name.lower().split()):
|
|
peers = [p for p in tickers_list if p != ticker.upper()][:4]
|
|
break
|
|
if not peers:
|
|
return {"industry": industry}
|
|
|
|
pe_vals, pb_vals, ps_vals = [], [], []
|
|
for p in peers:
|
|
try:
|
|
pi = yf.Ticker(p).info or {}
|
|
if pi.get("trailingPE") and pi["trailingPE"] > 0:
|
|
pe_vals.append(pi["trailingPE"])
|
|
if pi.get("priceToBook") and pi["priceToBook"] > 0:
|
|
pb_vals.append(pi["priceToBook"])
|
|
if pi.get("priceToSalesTrailing12Months"):
|
|
ps_vals.append(pi["priceToSalesTrailing12Months"])
|
|
except Exception:
|
|
continue
|
|
|
|
result = {"industry": industry, "peers": peers}
|
|
if pe_vals:
|
|
result["Industry Avg PER"] = round(sum(pe_vals) / len(pe_vals), 2)
|
|
if pb_vals:
|
|
result["Industry Avg PBR"] = round(sum(pb_vals) / len(pb_vals), 2)
|
|
if ps_vals:
|
|
result["Industry Avg PSR"] = round(sum(ps_vals) / len(ps_vals), 2)
|
|
return result
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
@st.cache_data(ttl=300)
|
|
def get_pe_history_chart_data(ticker: str) -> pd.DataFrame:
|
|
"""Return monthly close prices for 5Y PE chart overlay."""
|
|
if not yf or not ticker:
|
|
return pd.DataFrame()
|
|
try:
|
|
hist = yf.Ticker(ticker).history(period="5y", interval="1mo")
|
|
if hist is not None and not hist.empty:
|
|
return hist[["Close"]].reset_index()
|
|
except Exception:
|
|
pass
|
|
return pd.DataFrame()
|