avinashhm commited on
Commit
e521ee3
·
verified ·
1 Parent(s): fbc0d3d

Add trading_intelligence/risk_model.py

Browse files
Files changed (1) hide show
  1. trading_intelligence/risk_model.py +343 -0
trading_intelligence/risk_model.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Risk Model Module
3
+ ==================
4
+ Portfolio-aware risk modeling engine.
5
+
6
+ Takes user portfolio as input, learns trading behavior patterns,
7
+ and outputs risk scores, position sizing, stop-loss/take-profit levels.
8
+
9
+ Inspired by:
10
+ - Deep RL for Portfolio Optimization (2412.18563): Sharpe-ratio reward
11
+ - Distributional Forecasting (2508.18921): VaR estimation with DNNs
12
+ - Modern Portfolio Theory + DL (2508.14999): Covariance estimation
13
+ """
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+ import numpy as np
19
+ from typing import Dict, List, Optional, Tuple
20
+
21
+
22
+ class PortfolioEncoder(nn.Module):
23
+ """
24
+ Encode portfolio state into a fixed-dimensional representation.
25
+
26
+ Portfolio state includes:
27
+ - Current positions (asset, size, entry price, unrealized PnL)
28
+ - Historical trades (win/loss ratio, avg holding period)
29
+ - Account metrics (equity, margin, drawdown)
30
+ """
31
+
32
+ def __init__(self, position_dim: int = 8, max_positions: int = 20, d_model: int = 64):
33
+ super().__init__()
34
+ self.max_positions = max_positions
35
+
36
+ # Position embedding
37
+ self.position_encoder = nn.Sequential(
38
+ nn.Linear(position_dim, d_model),
39
+ nn.GELU(),
40
+ nn.Linear(d_model, d_model),
41
+ )
42
+
43
+ # Set-based aggregation (permutation invariant via attention)
44
+ self.position_attention = nn.MultiheadAttention(d_model, num_heads=4, batch_first=True)
45
+ self.norm = nn.LayerNorm(d_model)
46
+
47
+ # Account-level features
48
+ self.account_encoder = nn.Sequential(
49
+ nn.Linear(6, d_model), # equity, margin, drawdown, num_positions, total_exposure, cash_ratio
50
+ nn.GELU(),
51
+ )
52
+
53
+ # Combine
54
+ self.combine = nn.Sequential(
55
+ nn.Linear(d_model * 2, d_model),
56
+ nn.GELU(),
57
+ )
58
+
59
+ def forward(self, positions: torch.Tensor, account_features: torch.Tensor,
60
+ position_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
61
+ """
62
+ Args:
63
+ positions: (B, max_positions, position_dim) - padded position features
64
+ account_features: (B, 6) - account-level metrics
65
+ position_mask: (B, max_positions) - True for valid positions
66
+
67
+ Returns:
68
+ portfolio_repr: (B, d_model)
69
+ """
70
+ # Encode individual positions
71
+ pos_encoded = self.position_encoder(positions) # (B, P, d_model)
72
+
73
+ # Self-attention across positions (order-invariant aggregation)
74
+ key_padding_mask = ~position_mask if position_mask is not None else None
75
+ pos_attn, _ = self.position_attention(
76
+ pos_encoded, pos_encoded, pos_encoded,
77
+ key_padding_mask=key_padding_mask
78
+ )
79
+ pos_attn = self.norm(pos_attn + pos_encoded)
80
+
81
+ # Pool across positions
82
+ if position_mask is not None:
83
+ mask_expanded = position_mask.unsqueeze(-1).float()
84
+ pos_pooled = (pos_attn * mask_expanded).sum(dim=1) / (mask_expanded.sum(dim=1) + 1e-8)
85
+ else:
86
+ pos_pooled = pos_attn.mean(dim=1)
87
+
88
+ # Encode account features
89
+ account_encoded = self.account_encoder(account_features)
90
+
91
+ # Combine
92
+ combined = torch.cat([pos_pooled, account_encoded], dim=-1)
93
+ return self.combine(combined)
94
+
95
+
96
+ class TraderBehaviorAnalyzer(nn.Module):
97
+ """
98
+ Learn trader behavior patterns from historical trade sequences.
99
+
100
+ Patterns detected:
101
+ - Risk appetite (average position size relative to portfolio)
102
+ - Drawdown tolerance (max drawdown before behavior change)
103
+ - Win/loss ratio patterns
104
+ - Position sizing habits
105
+ - Overtrading tendency
106
+ - Revenge trading patterns (increased size after losses)
107
+ """
108
+
109
+ def __init__(self, trade_dim: int = 12, d_model: int = 64, n_layers: int = 2):
110
+ super().__init__()
111
+
112
+ # Trade sequence encoder (LSTM for sequential behavior patterns)
113
+ self.trade_encoder = nn.LSTM(
114
+ input_size=trade_dim,
115
+ hidden_size=d_model,
116
+ num_layers=n_layers,
117
+ batch_first=True,
118
+ dropout=0.1
119
+ )
120
+
121
+ # Behavior pattern heads
122
+ self.risk_appetite_head = nn.Sequential(
123
+ nn.Linear(d_model, 32),
124
+ nn.GELU(),
125
+ nn.Linear(32, 1),
126
+ nn.Sigmoid() # 0-1 scale
127
+ )
128
+
129
+ self.drawdown_tolerance_head = nn.Sequential(
130
+ nn.Linear(d_model, 32),
131
+ nn.GELU(),
132
+ nn.Linear(32, 1),
133
+ nn.Sigmoid()
134
+ )
135
+
136
+ self.overtrading_head = nn.Sequential(
137
+ nn.Linear(d_model, 32),
138
+ nn.GELU(),
139
+ nn.Linear(32, 1),
140
+ nn.Sigmoid()
141
+ )
142
+
143
+ self.revenge_trading_head = nn.Sequential(
144
+ nn.Linear(d_model, 32),
145
+ nn.GELU(),
146
+ nn.Linear(32, 1),
147
+ nn.Sigmoid()
148
+ )
149
+
150
+ # Trader type classifier (5 types)
151
+ self.trader_type_head = nn.Sequential(
152
+ nn.Linear(d_model, 32),
153
+ nn.GELU(),
154
+ nn.Linear(32, 5), # conservative, moderate, aggressive, scalper, swing
155
+ )
156
+
157
+ def forward(self, trade_history: torch.Tensor) -> Dict[str, torch.Tensor]:
158
+ """
159
+ Args:
160
+ trade_history: (B, num_trades, trade_dim)
161
+ trade_dim features: [entry_price, exit_price, size, pnl, holding_time,
162
+ is_winner, direction, max_drawdown, entry_hour,
163
+ day_of_week, time_since_last_trade, consecutive_losses]
164
+
165
+ Returns:
166
+ behavior_profile: Dict of behavioral metrics
167
+ """
168
+ _, (hidden, _) = self.trade_encoder(trade_history)
169
+ h = hidden[-1] # Last layer hidden state: (B, d_model)
170
+
171
+ return {
172
+ 'risk_appetite': self.risk_appetite_head(h).squeeze(-1),
173
+ 'drawdown_tolerance': self.drawdown_tolerance_head(h).squeeze(-1),
174
+ 'overtrading_prob': self.overtrading_head(h).squeeze(-1),
175
+ 'revenge_trading_prob': self.revenge_trading_head(h).squeeze(-1),
176
+ 'trader_type_logits': self.trader_type_head(h),
177
+ 'behavior_embedding': h, # For downstream use
178
+ }
179
+
180
+
181
+ class RiskModel(nn.Module):
182
+ """
183
+ Complete risk modeling engine.
184
+
185
+ Combines:
186
+ 1. Market state (from prediction model)
187
+ 2. Portfolio state (positions, account)
188
+ 3. Trader behavior profile
189
+
190
+ Outputs:
191
+ - Risk score (0-1)
192
+ - Recommended position size (fraction of portfolio)
193
+ - Stop-loss / take-profit levels
194
+ - Probability of portfolio drawdown exceeding threshold
195
+ """
196
+
197
+ def __init__(
198
+ self,
199
+ market_dim: int = 128, # Dimension of market state from prediction model
200
+ portfolio_dim: int = 64, # Portfolio encoder output dim
201
+ behavior_dim: int = 64, # Behavior analyzer output dim
202
+ d_model: int = 128,
203
+ num_horizons: int = 3,
204
+ ):
205
+ super().__init__()
206
+
207
+ self.portfolio_encoder = PortfolioEncoder(d_model=portfolio_dim)
208
+ self.behavior_analyzer = TraderBehaviorAnalyzer(d_model=behavior_dim)
209
+
210
+ # Fusion network
211
+ total_dim = market_dim + portfolio_dim + behavior_dim
212
+ self.fusion = nn.Sequential(
213
+ nn.Linear(total_dim, d_model),
214
+ nn.GELU(),
215
+ nn.Dropout(0.1),
216
+ nn.Linear(d_model, d_model),
217
+ nn.GELU(),
218
+ )
219
+
220
+ # Risk score head
221
+ self.risk_score_head = nn.Sequential(
222
+ nn.Linear(d_model, 64),
223
+ nn.GELU(),
224
+ nn.Linear(64, 1),
225
+ nn.Sigmoid() # 0-1
226
+ )
227
+
228
+ # Position size head (Kelly-criterion inspired)
229
+ self.position_size_head = nn.Sequential(
230
+ nn.Linear(d_model, 64),
231
+ nn.GELU(),
232
+ nn.Linear(64, 1),
233
+ nn.Sigmoid() # 0-1 (fraction of portfolio)
234
+ )
235
+
236
+ # Stop-loss / Take-profit head (outputs as ATR multiples)
237
+ self.sl_tp_head = nn.Sequential(
238
+ nn.Linear(d_model, 64),
239
+ nn.GELU(),
240
+ nn.Linear(64, 2), # [stop_loss_atr_mult, take_profit_atr_mult]
241
+ nn.Softplus() # Positive values
242
+ )
243
+
244
+ # Drawdown probability head (predicts P(drawdown > threshold) for multiple thresholds)
245
+ self.drawdown_head = nn.Sequential(
246
+ nn.Linear(d_model, 64),
247
+ nn.GELU(),
248
+ nn.Linear(64, 4), # P(dd > 5%), P(dd > 10%), P(dd > 15%), P(dd > 20%)
249
+ nn.Sigmoid()
250
+ )
251
+
252
+ # Value at Risk head
253
+ self.var_head = nn.Sequential(
254
+ nn.Linear(d_model, 64),
255
+ nn.GELU(),
256
+ nn.Linear(64, 3), # VaR at 95%, 99%, 99.5%
257
+ )
258
+
259
+ def forward(
260
+ self,
261
+ market_state: torch.Tensor,
262
+ positions: torch.Tensor,
263
+ account_features: torch.Tensor,
264
+ trade_history: torch.Tensor,
265
+ position_mask: Optional[torch.Tensor] = None,
266
+ ) -> Dict[str, torch.Tensor]:
267
+ """
268
+ Full risk assessment.
269
+
270
+ Args:
271
+ market_state: (B, market_dim) from prediction model
272
+ positions: (B, max_positions, position_dim)
273
+ account_features: (B, 6)
274
+ trade_history: (B, num_trades, trade_dim)
275
+ position_mask: (B, max_positions)
276
+
277
+ Returns:
278
+ Dict with all risk outputs
279
+ """
280
+ # Encode portfolio
281
+ portfolio_repr = self.portfolio_encoder(positions, account_features, position_mask)
282
+
283
+ # Analyze behavior
284
+ behavior = self.behavior_analyzer(trade_history)
285
+ behavior_repr = behavior['behavior_embedding']
286
+
287
+ # Fuse all signals
288
+ fused = self.fusion(torch.cat([market_state, portfolio_repr, behavior_repr], dim=-1))
289
+
290
+ # Compute outputs
291
+ risk_score = self.risk_score_head(fused).squeeze(-1)
292
+ position_size = self.position_size_head(fused).squeeze(-1)
293
+ sl_tp = self.sl_tp_head(fused)
294
+ drawdown_probs = self.drawdown_head(fused)
295
+ var_estimates = self.var_head(fused)
296
+
297
+ # Adjust position size based on risk score (lower risk tolerance → smaller positions)
298
+ adjusted_position_size = position_size * (1 - 0.5 * risk_score)
299
+
300
+ return {
301
+ 'risk_score': risk_score,
302
+ 'raw_position_size': position_size,
303
+ 'adjusted_position_size': adjusted_position_size,
304
+ 'stop_loss_atr_mult': sl_tp[:, 0],
305
+ 'take_profit_atr_mult': sl_tp[:, 1],
306
+ 'drawdown_probs': drawdown_probs,
307
+ 'var_estimates': var_estimates,
308
+ 'behavior_profile': behavior,
309
+ }
310
+
311
+
312
+ class RiskLoss(nn.Module):
313
+ """Loss function for risk model training."""
314
+
315
+ def __init__(self):
316
+ super().__init__()
317
+
318
+ def forward(self, predictions: Dict, targets: Dict) -> Dict[str, torch.Tensor]:
319
+ """
320
+ Targets should include:
321
+ - actual_risk: realized risk score from hindsight
322
+ - actual_drawdown: realized drawdown
323
+ - optimal_position_size: computed from Kelly criterion or similar
324
+ """
325
+ losses = {}
326
+
327
+ if 'actual_risk' in targets:
328
+ losses['risk_loss'] = F.mse_loss(predictions['risk_score'], targets['actual_risk'])
329
+
330
+ if 'optimal_position_size' in targets:
331
+ losses['position_loss'] = F.mse_loss(
332
+ predictions['adjusted_position_size'], targets['optimal_position_size']
333
+ )
334
+
335
+ if 'drawdown_occurred' in targets:
336
+ losses['drawdown_loss'] = F.binary_cross_entropy(
337
+ predictions['drawdown_probs'], targets['drawdown_occurred']
338
+ )
339
+
340
+ total = sum(losses.values())
341
+ losses['total_loss'] = total
342
+
343
+ return losses