Files
All-in-one-Financial-Analysis/atlas-terminal/server/services/risk_metrics.py
T
shawnkim1997andClaude Opus 4.6 b2acda81ee 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>
2026-03-21 02:10:10 +00:00

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,
}