mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-17 04:18:08 +00:00
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()
|