jashdoshi77's picture
QuantHedge: Full deployment with Docker + nginx + uvicorn
9d29748
Raw
History Blame Contribute Delete
7.97 kB
"""
Risk Analysis Engine.
Comprehensive risk metrics computation:
- Annualized Volatility
- Sharpe & Sortino Ratios
- Maximum Drawdown
- Value at Risk (Parametric & Historical)
- Conditional VaR (CVaR / Expected Shortfall)
- Beta & Alpha
- Correlation Matrix
- Tracking Error
- Information Ratio
- Downside Deviation
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
import numpy as np
import pandas as pd
from scipy import stats as sp_stats
from app.services.data_ingestion.yahoo import yahoo_adapter
logger = logging.getLogger(__name__)
class RiskEngine:
"""Compute risk analytics for individual assets and portfolios."""
async def analyze(
self,
tickers: List[str],
period: str = "1y",
benchmark_ticker: str = "SPY",
confidence_level: float = 0.95,
risk_free_rate: float = 0.04,
) -> Dict[str, Any]:
"""
Compute comprehensive risk metrics for a list of tickers.
Returns:
Dict with per-asset metrics, correlation matrix, and portfolio risk.
"""
# Fetch returns
price_data: Dict[str, pd.Series] = {}
for ticker in tickers:
df = await yahoo_adapter.get_price_dataframe(ticker, period=period)
if not df.empty and "Close" in df.columns:
price_data[ticker] = df["Close"]
if not price_data:
return {"metrics": [], "correlation_matrix": None}
prices_df = pd.DataFrame(price_data).dropna()
returns_df = prices_df.pct_change().dropna()
# Fetch benchmark
bench_df = await yahoo_adapter.get_price_dataframe(benchmark_ticker, period=period)
bench_returns = None
if not bench_df.empty:
bench_returns = bench_df["Close"].pct_change().dropna()
# Per-asset risk metrics
metrics = []
for ticker in returns_df.columns:
ret = returns_df[ticker].values
m = self._compute_metrics(
ret,
ticker=ticker,
benchmark_returns=bench_returns.values if bench_returns is not None else None,
confidence_level=confidence_level,
risk_free_rate=risk_free_rate,
)
metrics.append(m)
# Correlation matrix
corr = returns_df.corr()
corr_matrix = {
"strategy_names": list(corr.columns),
"matrix": corr.values.tolist(),
}
# Equal-weighted portfolio risk
n = len(returns_df.columns)
if n > 0:
port_returns = returns_df.mean(axis=1).values
portfolio_risk = self._compute_metrics(
port_returns,
portfolio_name="Equal-Weight Portfolio",
benchmark_returns=bench_returns.values if bench_returns is not None else None,
confidence_level=confidence_level,
risk_free_rate=risk_free_rate,
)
else:
portfolio_risk = None
return {
"metrics": metrics,
"correlation_matrix": corr_matrix,
"portfolio_risk": portfolio_risk,
}
def _compute_metrics(
self,
returns: np.ndarray,
ticker: Optional[str] = None,
portfolio_name: Optional[str] = None,
benchmark_returns: Optional[np.ndarray] = None,
confidence_level: float = 0.95,
risk_free_rate: float = 0.04,
) -> Dict[str, Any]:
"""Compute all risk metrics for a single return series."""
if len(returns) < 5:
return {
"ticker": ticker,
"portfolio_name": portfolio_name,
"error": "Insufficient data",
}
# Annualized metrics
ann_return = float(np.mean(returns) * 252)
ann_vol = float(np.std(returns, ddof=1) * np.sqrt(252))
# Sharpe Ratio
excess_return = ann_return - risk_free_rate
sharpe = excess_return / ann_vol if ann_vol > 0 else 0.0
# Sortino Ratio
downside_returns = returns[returns < 0]
downside_dev = float(np.std(downside_returns, ddof=1) * np.sqrt(252)) if len(downside_returns) > 1 else ann_vol
sortino = excess_return / downside_dev if downside_dev > 0 else 0.0
# Maximum Drawdown
cum_returns = np.cumprod(1 + returns)
peak = np.maximum.accumulate(cum_returns)
drawdowns = (cum_returns - peak) / peak
max_drawdown = float(np.min(drawdowns))
# Calmar Ratio
calmar = ann_return / abs(max_drawdown) if max_drawdown != 0 else 0.0
# Value at Risk (Parametric - normal distribution)
z_score = sp_stats.norm.ppf(1 - confidence_level)
daily_var = float(np.mean(returns) + z_score * np.std(returns, ddof=1))
var_95 = float(daily_var * np.sqrt(252)) # Annualized
# Value at Risk (Historical)
hist_var = float(np.percentile(returns, (1 - confidence_level) * 100))
# Conditional VaR (Expected Shortfall)
var_threshold = np.percentile(returns, (1 - confidence_level) * 100)
tail_returns = returns[returns <= var_threshold]
cvar = float(np.mean(tail_returns)) if len(tail_returns) > 0 else hist_var
# VaR at 99%
var_99 = float(np.percentile(returns, 1))
result = {
"ticker": ticker,
"portfolio_name": portfolio_name,
"volatility": round(ann_vol, 4),
"sharpe_ratio": round(sharpe, 4),
"sortino_ratio": round(sortino, 4),
"max_drawdown": round(max_drawdown, 4),
"calmar_ratio": round(calmar, 4),
"var_95": round(var_95, 6),
"var_99": round(var_99, 6),
"cvar_95": round(cvar, 6),
"downside_deviation": round(downside_dev, 4),
"annualized_return": round(ann_return, 4),
}
# Beta, Alpha, Tracking Error, Information Ratio vs benchmark
if benchmark_returns is not None:
min_len = min(len(returns), len(benchmark_returns))
if min_len > 10:
r = returns[:min_len]
b = benchmark_returns[:min_len]
cov_rb = np.cov(r, b)[0, 1]
var_b = np.var(b, ddof=1)
beta = cov_rb / var_b if var_b > 0 else 1.0
bench_ann_return = float(np.mean(b) * 252)
alpha = ann_return - beta * bench_ann_return
tracking_diff = r - b
tracking_error = float(np.std(tracking_diff, ddof=1) * np.sqrt(252))
info_ratio = (
(ann_return - bench_ann_return) / tracking_error
if tracking_error > 0
else 0.0
)
result.update({
"beta": round(float(beta), 4),
"alpha": round(float(alpha), 4),
"tracking_error": round(tracking_error, 4),
"information_ratio": round(float(info_ratio), 4),
})
return result
async def correlation_matrix(
self, tickers: List[str], period: str = "1y"
) -> Dict[str, Any]:
"""Compute return correlation matrix for given tickers."""
price_data: Dict[str, pd.Series] = {}
for ticker in tickers:
df = await yahoo_adapter.get_price_dataframe(ticker, period=period)
if not df.empty:
price_data[ticker] = df["Close"]
if len(price_data) < 2:
return {"strategy_names": list(price_data.keys()), "matrix": []}
prices = pd.DataFrame(price_data).dropna()
returns = prices.pct_change().dropna()
corr = returns.corr()
return {
"strategy_names": list(corr.columns),
"matrix": [[round(v, 4) for v in row] for row in corr.values.tolist()],
}
risk_engine = RiskEngine()