mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-19 05:18:08 +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,141 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user