Premchan369 commited on
Commit
e7e2207
·
verified ·
1 Parent(s): de3f3f3

Upload risk_engine.py

Browse files
Files changed (1) hide show
  1. risk_engine.py +227 -0
risk_engine.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Risk Engine - VaR, CVaR, tail risk, and drawdown control."""
2
+ import numpy as np
3
+ import pandas as pd
4
+ from scipy import stats
5
+ from typing import Dict, List, Optional, Tuple
6
+ import warnings
7
+ warnings.filterwarnings('ignore')
8
+
9
+
10
+ class RiskEngine:
11
+ """Comprehensive risk analytics engine."""
12
+
13
+ def __init__(self, confidence_levels: List[float] = None):
14
+ self.confidence_levels = confidence_levels or [0.95, 0.99]
15
+ self.var_history = []
16
+ self.cvar_history = []
17
+ self.tail_risk_history = []
18
+
19
+ def compute_var(self, returns: np.ndarray,
20
+ method: str = 'historical',
21
+ confidence: float = 0.95) -> float:
22
+ """
23
+ Compute Value at Risk.
24
+
25
+ Methods:
26
+ - historical: empirical quantile
27
+ - parametric: assume normal distribution
28
+ - cornish_fisher: adjust for skewness and kurtosis
29
+ """
30
+ if method == 'historical':
31
+ return -np.percentile(returns, (1 - confidence) * 100)
32
+
33
+ elif method == 'parametric':
34
+ z = stats.norm.ppf(1 - confidence)
35
+ return -(returns.mean() + z * returns.std())
36
+
37
+ elif method == 'cornish_fisher':
38
+ z = stats.norm.ppf(1 - confidence)
39
+ s = stats.skew(returns)
40
+ k = stats.kurtosis(returns)
41
+
42
+ # Cornish-Fisher expansion
43
+ z_cf = (z +
44
+ (z**2 - 1) * s / 6 +
45
+ (z**3 - 3*z) * (k - 3) / 24 -
46
+ (2*z**3 - 5*z) * s**2 / 36)
47
+
48
+ return -(returns.mean() + z_cf * returns.std())
49
+
50
+ else:
51
+ raise ValueError(f"Unknown VaR method: {method}")
52
+
53
+ def compute_cvar(self, returns: np.ndarray,
54
+ confidence: float = 0.95) -> float:
55
+ """
56
+ Compute Conditional Value at Risk (Expected Shortfall).
57
+ Average loss beyond VaR threshold.
58
+ """
59
+ var = self.compute_var(returns, 'historical', confidence)
60
+ tail_losses = returns[returns <= -var]
61
+
62
+ if len(tail_losses) == 0:
63
+ return var
64
+
65
+ return -tail_losses.mean()
66
+
67
+ def compute_tail_risk(self, returns: np.ndarray) -> Dict:
68
+ """
69
+ Compute comprehensive tail risk metrics.
70
+ """
71
+ # Skewness and kurtosis
72
+ skewness = stats.skew(returns)
73
+ kurt = stats.kurtosis(returns)
74
+
75
+ # Tail ratio: ratio of 95th percentile to 5th percentile
76
+ tail_ratio = np.percentile(returns, 95) / abs(np.percentile(returns, 5))
77
+
78
+ # Maximum consecutive losses
79
+ losses = returns < 0
80
+ consecutive = []
81
+ current = 0
82
+ for is_loss in losses:
83
+ if is_loss:
84
+ current += 1
85
+ else:
86
+ if current > 0:
87
+ consecutive.append(current)
88
+ current = 0
89
+ if current > 0:
90
+ consecutive.append(current)
91
+
92
+ max_consecutive_losses = max(consecutive) if consecutive else 0
93
+
94
+ # Pain ratio: annualized return / max drawdown
95
+ cumulative = np.cumprod(1 + returns)
96
+ running_max = np.maximum.accumulate(cumulative)
97
+ drawdown = (cumulative - running_max) / running_max
98
+ max_dd = np.min(drawdown)
99
+
100
+ annual_return = np.mean(returns) * 252
101
+ pain_ratio = annual_return / abs(max_dd) if max_dd != 0 else 0
102
+
103
+ # Ulcer index
104
+ ulcer = np.sqrt(np.mean(drawdown**2))
105
+
106
+ return {
107
+ 'skewness': skewness,
108
+ 'kurtosis': kurt,
109
+ 'tail_ratio': tail_ratio,
110
+ 'max_consecutive_losses': max_consecutive_losses,
111
+ 'max_drawdown': max_dd,
112
+ 'pain_ratio': pain_ratio,
113
+ 'ulcer_index': ulcer,
114
+ 'downside_deviation': np.std(returns[returns < 0]) * np.sqrt(252)
115
+ }
116
+
117
+ def compute_all_var(self, returns: np.ndarray) -> Dict:
118
+ """Compute VaR and CVaR at all confidence levels."""
119
+ results = {}
120
+ for conf in self.confidence_levels:
121
+ var_hist = self.compute_var(returns, 'historical', conf)
122
+ var_param = self.compute_var(returns, 'parametric', conf)
123
+ var_cf = self.compute_var(returns, 'cornish_fisher', conf)
124
+ cvar = self.compute_cvar(returns, conf)
125
+
126
+ results[f'var_{int(conf*100)}_historical'] = var_hist
127
+ results[f'var_{int(conf*100)}_parametric'] = var_param
128
+ results[f'var_{int(conf*100)}_cornish_fisher'] = var_cf
129
+ results[f'cvar_{int(conf*100)}'] = cvar
130
+
131
+ return results
132
+
133
+ def rolling_risk(self, returns: pd.Series,
134
+ window: int = 63) -> pd.DataFrame:
135
+ """Compute rolling risk metrics."""
136
+ rolling_var = returns.rolling(window).quantile(0.05)
137
+ rolling_cvar = returns.rolling(window).apply(
138
+ lambda x: -x[x <= x.quantile(0.05)].mean() if len(x[x <= x.quantile(0.05)]) > 0 else -x.quantile(0.05)
139
+ )
140
+ rolling_vol = returns.rolling(window).std() * np.sqrt(252)
141
+
142
+ return pd.DataFrame({
143
+ 'rolling_var_95': -rolling_var,
144
+ 'rolling_cvar_95': rolling_cvar,
145
+ 'rolling_volatility': rolling_vol
146
+ })
147
+
148
+ def portfolio_var(self, weights: np.ndarray,
149
+ returns_df: pd.DataFrame,
150
+ method: str = 'parametric',
151
+ confidence: float = 0.95) -> float:
152
+ """
153
+ Compute portfolio-level VaR using covariance matrix.
154
+ """
155
+ cov_matrix = returns_df.cov() * 252
156
+ port_vol = np.sqrt(np.dot(weights, np.dot(cov_matrix, weights)))
157
+
158
+ if method == 'parametric':
159
+ z = stats.norm.ppf(1 - confidence)
160
+ port_mean = np.dot(weights, returns_df.mean() * 252)
161
+ return -(port_mean + z * port_vol)
162
+
163
+ elif method == 'historical':
164
+ port_returns = returns_df.dot(weights)
165
+ return -np.percentile(port_returns, (1 - confidence) * 100)
166
+
167
+ else:
168
+ raise ValueError(f"Unknown method: {method}")
169
+
170
+
171
+ class DrawdownControl:
172
+ """Dynamic position sizing based on drawdown state."""
173
+
174
+ def __init__(self,
175
+ max_drawdown_threshold: float = -0.10,
176
+ risk_scaling: bool = True,
177
+ scaling_factor: float = 2.0):
178
+ self.max_drawdown_threshold = max_drawdown_threshold
179
+ self.risk_scaling = risk_scaling
180
+ self.scaling_factor = scaling_factor
181
+ self.drawdown_history = []
182
+ self.scale_history = []
183
+
184
+ def compute_scale_factor(self,
185
+ current_equity: float,
186
+ peak_equity: float) -> float:
187
+ """
188
+ Compute position scaling factor based on drawdown.
189
+
190
+ As drawdown increases, reduce exposure exponentially.
191
+ """
192
+ drawdown = (current_equity - peak_equity) / peak_equity
193
+ self.drawdown_history.append(drawdown)
194
+
195
+ if drawdown >= 0:
196
+ # No drawdown, full exposure
197
+ scale = 1.0
198
+ elif drawdown > self.max_drawdown_threshold:
199
+ # Mild drawdown, linear scaling
200
+ scale = 1.0 + (drawdown / abs(self.max_drawdown_threshold)) * 0.5
201
+ else:
202
+ # Severe drawdown, exponential scaling
203
+ excess_dd = abs(drawdown) - abs(self.max_drawdown_threshold)
204
+ scale = 0.5 * np.exp(-self.scaling_factor * excess_dd)
205
+
206
+ scale = max(scale, 0.1) # Minimum 10% exposure
207
+ self.scale_history.append(scale)
208
+
209
+ return scale
210
+
211
+ def apply_to_weights(self, weights: np.ndarray, scale: float) -> np.ndarray:
212
+ """Apply scaling factor to portfolio weights."""
213
+ return weights * scale
214
+
215
+ def get_drawdown_stats(self) -> Dict:
216
+ """Get drawdown statistics."""
217
+ if not self.drawdown_history:
218
+ return {}
219
+
220
+ dd = np.array(self.drawdown_history)
221
+ return {
222
+ 'max_drawdown': np.min(dd),
223
+ 'avg_drawdown': np.mean(dd[dd < 0]),
224
+ 'current_drawdown': dd[-1],
225
+ 'avg_scale': np.mean(self.scale_history),
226
+ 'current_scale': self.scale_history[-1] if self.scale_history else 1.0
227
+ }