Adding CVaR Optimization using GARCH-LSTM-EVT
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 48.7k
- Forks
- 7.7k
- PR merge metrics
- No merged PRs in 30d
Description
GARCH-EVT-LSTM Hybrid Simulator with CVaR Optimization
📌 Description
While Qlib offers a robust Model Zoo of deep learning architectures (LSTMs, Transformers, LightGBM), it currently lacks hybrid models that explicitly combine deep learning with rigorous stochastic volatility and extreme value statistical frameworks.
This feature proposes the integration of a GARCH-EVT-LSTM Hybrid Simulator coupled with a Conditional Value at Risk (CVaR) Portfolio Optimizer.
Standard deep learning models often fail to capture the fat tails and volatility clustering inherent in financial markets. By integrating Extreme Value Theory (EVT) and GARCH, this model stabilizes the LSTM's learning process and provides robust quantile forecasts, making it resilient to black-swan events (e.g., COVID-19 crash).
🚀 Proposed Core Components
1. EVT (Extreme Value Theory) Smoothing Module
- Uses Peak-Over-Threshold (POT)
- Fits Generalized Pareto Distribution (GPD) on extreme residuals
- Prevents overfitting to anomalies while preserving tail risk
2. LSTM-GARCH Joint Simulator
- GARCH(1,1) → models conditional heteroskedasticity (volatility clustering)
- LSTM → predicts conditional mean of returns
- Cholesky / Copula → models cross-asset dependencies
3. CVaR Portfolio Optimizer
- Replaces Mean-Variance optimization
- Minimizes Expected Shortfall (CVaR @ 95%)
- Uses simulated multi-path trajectories
💻 Proof of Concept (PoC)
1. EVT Residual Smoothing
import numpy as np
from scipy import stats
def apply_evt_smoothing(residuals, threshold_q=0.95):
"""
Stabilizes residuals by replacing extreme outliers with GPD samples.
"""
threshold = np.quantile(np.abs(residuals), threshold_q)
extreme_mask = np.abs(residuals) > threshold
if np.any(extreme_mask):
exceedances = np.abs(residuals[extreme_mask]) - threshold
shape, loc, scale = stats.genpareto.fit(exceedances)
gpd_samples = stats.genpareto.rvs(shape, loc=loc, scale=scale, size=np.sum(extreme_mask))
smoothed = residuals.copy()
smoothed[extreme_mask] = np.sign(residuals[extreme_mask]) * (threshold + gpd_samples)
return smoothed
return residuals
2. GARCH-LSTM Joint Forecasting
import torch
import torch.nn as nn
from arch import arch_model
from sklearn.preprocessing import StandardScaler
class LSTMMean(nn.Module):
def __init__(self, input_size=1, hidden_size=50):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, 1)
def forward(self, x):
out, _ = self.lstm(x)
return self.fc(out[:, -1, :])
def simulate_garch_lstm_joint(returns, corr_matrix, lookback, horizon, n_paths, device="cpu"):
"""
Vectorized simulation using GARCH volatility, EVT residuals, and LSTM mean.
"""
n_assets = returns.shape[1]
garch_results = []
lstm_models = []
scalers = []
all_stable_residuals = []
# Phase 1: Training + EVT
for i in range(n_assets):
series = returns.iloc[:, i]
res = arch_model(series * 100, vol="Garch", p=1, q=1).fit(disp="off")
garch_results.append(res)
smoothed_z = apply_evt_smoothing(res.std_resid)
all_stable_residuals.append(np.sort(smoothed_z))
scaler = StandardScaler().fit(series.values.reshape(-1, 1))
scalers.append(scaler)
lstm_models.append(LSTMMean().to(device))
# Phase 2: Copula Sampling
L = np.linalg.cholesky(corr_matrix)
shocks = (np.random.normal(size=(horizon, n_paths, n_assets)) @ L.T)
u_shocks = stats.norm.cdf(shocks)
sim_returns = np.zeros((n_paths, horizon, n_assets))
# Phase 3: Step-forward simulation (omitted for brevity)
return sim_returns
3. CVaR Optimization
import cvxpy as cp
def optimize_cvar(simulated_returns, alpha=0.95, max_weight=0.3):
"""
Minimizes Conditional Value at Risk (CVaR).
"""
n_paths, n_assets = simulated_returns.shape
weights = cp.Variable(n_assets)
z = cp.Variable(n_paths)
gamma = cp.Variable()
loss = -simulated_returns @ weights
objective = cp.Minimize(gamma + (1.0 / (n_paths * (1 - alpha))) * cp.sum(z))
constraints = [
z >= 0,
z >= loss - gamma,
cp.sum(weights) == 1,
weights >= 0,
weights <= max_weight
]
prob = cp.Problem(objective, constraints)
prob.solve(solver=cp.ECOS)
return weights.value
I have done some sort of work on it which is curently working like a fine wine .If you want i can build a Qlib style wrapper that will have multiple try and except to reduce the error and also add different types of stochastic differential equation models in the wrapper that the user will just import and tune on the data
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the attached GARCH_LSTM_SVJ.py and the issue's PoC functions: apply_evt_smoothing, simulate_garch_lstm_joint, and optimize_cvar. Compare them with Qlib's existing Model Zoo entry points, then clarify the required wrapper API, supported dependencies, integration location, and validation criteria before implementation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- numpy, python, pytorch, scikit-learn
- Domain
- fintech-quant, machine-learning
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100