mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-20 14:18:05 +00:00
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>
99 lines
2.7 KiB
Python
99 lines
2.7 KiB
Python
"""Portfolio risk metrics -- VaR, Sharpe, Sortino, MDD, Beta, Correlation."""
|
|
|
|
import numpy as np
|
|
|
|
|
|
def compute_portfolio_risk(positions: list, benchmark: str = "SPY") -> dict:
|
|
"""Compute VaR, Sharpe, Sortino, MDD, Beta, Correlation for portfolio."""
|
|
import yfinance as yf
|
|
|
|
tickers = [p["ticker"] for p in positions]
|
|
if not tickers:
|
|
return {}
|
|
|
|
values = [
|
|
p.get("value", p.get("quantity", 0) * p.get("avg_price", 0))
|
|
for p in positions
|
|
]
|
|
total = sum(values) or 1
|
|
weights = np.array([v / total for v in values])
|
|
|
|
data = yf.download(tickers + [benchmark], period="1y", progress=False)["Close"]
|
|
if data.empty:
|
|
return {}
|
|
|
|
returns = data.pct_change().dropna()
|
|
|
|
if len(tickers) == 1:
|
|
port_returns = (
|
|
returns[tickers[0]]
|
|
if tickers[0] in returns.columns
|
|
else returns.iloc[:, 0]
|
|
)
|
|
else:
|
|
ticker_returns = (
|
|
returns[tickers]
|
|
if all(t in returns.columns for t in tickers)
|
|
else returns.iloc[:, : len(tickers)]
|
|
)
|
|
port_returns = (ticker_returns * weights).sum(axis=1)
|
|
|
|
bench_returns = (
|
|
returns[benchmark] if benchmark in returns.columns else returns.iloc[:, -1]
|
|
)
|
|
|
|
# VaR
|
|
var_95 = float(np.percentile(port_returns, 5))
|
|
var_99 = float(np.percentile(port_returns, 1))
|
|
|
|
# Sharpe (annualized, rf=0.04)
|
|
rf_daily = 0.04 / 252
|
|
excess = port_returns - rf_daily
|
|
sharpe = (
|
|
float(np.sqrt(252) * excess.mean() / excess.std())
|
|
if excess.std() > 0
|
|
else 0
|
|
)
|
|
|
|
# Sortino
|
|
downside = excess[excess < 0]
|
|
sortino = (
|
|
float(np.sqrt(252) * excess.mean() / downside.std())
|
|
if len(downside) > 0 and downside.std() > 0
|
|
else 0
|
|
)
|
|
|
|
# Max Drawdown
|
|
cumulative = (1 + port_returns).cumprod()
|
|
peak = cumulative.expanding().max()
|
|
drawdown = (cumulative - peak) / peak
|
|
max_dd = float(drawdown.min())
|
|
|
|
# Beta
|
|
cov = np.cov(port_returns, bench_returns)
|
|
beta = float(cov[0, 1] / cov[1, 1]) if cov[1, 1] > 0 else 1.0
|
|
|
|
# Correlation matrix
|
|
corr = {}
|
|
if len(tickers) > 1:
|
|
corr_df = (
|
|
returns[tickers].corr()
|
|
if all(t in returns.columns for t in tickers)
|
|
else {}
|
|
)
|
|
if hasattr(corr_df, "to_dict"):
|
|
corr = {
|
|
str(k): {str(k2): round(v2, 3) for k2, v2 in v.items()}
|
|
for k, v in corr_df.to_dict().items()
|
|
}
|
|
|
|
return {
|
|
"var_95": round(var_95 * 100, 2),
|
|
"var_99": round(var_99 * 100, 2),
|
|
"sharpe": round(sharpe, 2),
|
|
"sortino": round(sortino, 2),
|
|
"max_drawdown": round(max_dd * 100, 2),
|
|
"beta": round(beta, 2),
|
|
"correlation_matrix": corr,
|
|
}
|