Premchan369 commited on
Commit
958d6b7
·
verified ·
1 Parent(s): 27d524a

Upload alpha_model.py

Browse files
Files changed (1) hide show
  1. alpha_model.py +253 -0
alpha_model.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Multi-Asset Alpha Model - Predicts expected returns using LSTM, Transformer, and XGBoost ensemble."""
2
+ import numpy as np
3
+ import pandas as pd
4
+ import torch
5
+ import torch.nn as nn
6
+ from torch.utils.data import Dataset, DataLoader
7
+ from sklearn.ensemble import GradientBoostingRegressor
8
+ from typing import Dict, Tuple, Optional
9
+ import warnings
10
+ warnings.filterwarnings('ignore')
11
+
12
+
13
+ class AlphaDataset(Dataset):
14
+ """PyTorch dataset for alpha model training"""
15
+ def __init__(self, X: np.ndarray, y: np.ndarray):
16
+ self.X = torch.FloatTensor(X)
17
+ self.y = torch.FloatTensor(y).unsqueeze(1)
18
+
19
+ def __len__(self):
20
+ return len(self.X)
21
+
22
+ def __getitem__(self, idx):
23
+ return self.X[idx], self.y[idx]
24
+
25
+
26
+ class LSTMAlpha(nn.Module):
27
+ """LSTM-based alpha model"""
28
+ def __init__(self, input_size: int, hidden_size: int = 128,
29
+ num_layers: int = 2, dropout: float = 0.2):
30
+ super().__init__()
31
+ self.lstm = nn.LSTM(
32
+ input_size, hidden_size, num_layers,
33
+ batch_first=True, dropout=dropout if num_layers > 1 else 0
34
+ )
35
+ self.dropout = nn.Dropout(dropout)
36
+ self.fc1 = nn.Linear(hidden_size, 64)
37
+ self.fc2 = nn.Linear(64, 1)
38
+ self.relu = nn.ReLU()
39
+
40
+ def forward(self, x):
41
+ out, _ = self.lstm(x)
42
+ out = self.dropout(out[:, -1, :])
43
+ out = self.relu(self.fc1(out))
44
+ return self.fc2(out)
45
+
46
+
47
+ class TransformerAlpha(nn.Module):
48
+ """Transformer-based alpha model"""
49
+ def __init__(self, input_size: int, d_model: int = 128,
50
+ nhead: int = 4, num_layers: int = 2, dropout: float = 0.2):
51
+ super().__init__()
52
+ self.input_proj = nn.Linear(input_size, d_model)
53
+ encoder_layer = nn.TransformerEncoderLayer(
54
+ d_model=d_model, nhead=nhead,
55
+ dim_feedforward=d_model*4, dropout=dropout,
56
+ batch_first=True
57
+ )
58
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
59
+ self.dropout = nn.Dropout(dropout)
60
+ self.fc1 = nn.Linear(d_model, 64)
61
+ self.fc2 = nn.Linear(64, 1)
62
+ self.relu = nn.ReLU()
63
+
64
+ def forward(self, x):
65
+ x = self.input_proj(x)
66
+ out = self.transformer(x)
67
+ out = self.dropout(out.mean(dim=1))
68
+ out = self.relu(self.fc1(out))
69
+ return self.fc2(out)
70
+
71
+
72
+ class XGBoostAlpha:
73
+ """XGBoost-based alpha model (using sklearn GradientBoosting)"""
74
+ def __init__(self, max_depth: int = 6, learning_rate: float = 0.05,
75
+ n_estimators: int = 200):
76
+ self.model = GradientBoostingRegressor(
77
+ max_depth=max_depth,
78
+ learning_rate=learning_rate,
79
+ n_estimators=n_estimators,
80
+ subsample=0.8,
81
+ random_state=42
82
+ )
83
+
84
+ def fit(self, X: np.ndarray, y: np.ndarray):
85
+ """X should be flattened: (n_samples, lookback * features)"""
86
+ n_samples = X.shape[0]
87
+ X_flat = X.reshape(n_samples, -1)
88
+ self.model.fit(X_flat, y)
89
+ return self
90
+
91
+ def predict(self, X: np.ndarray) -> np.ndarray:
92
+ n_samples = X.shape[0]
93
+ X_flat = X.reshape(n_samples, -1)
94
+ return self.model.predict(X_flat)
95
+
96
+ def feature_importances(self) -> np.ndarray:
97
+ return self.model.feature_importances_
98
+
99
+
100
+ class AlphaEnsemble:
101
+ """Ensemble of LSTM, Transformer, and XGBoost alpha models"""
102
+
103
+ def __init__(self, input_size: int, seq_len: int,
104
+ lstm_hidden: int = 128, lstm_layers: int = 2,
105
+ trans_d_model: int = 128, trans_nhead: int = 4, trans_layers: int = 2,
106
+ xgb_depth: int = 6, xgb_lr: float = 0.05, xgb_estimators: int = 200,
107
+ weights: Optional[Dict[str, float]] = None,
108
+ device: str = 'cpu'):
109
+ self.device = torch.device(device)
110
+ self.seq_len = seq_len
111
+ self.input_size = input_size
112
+
113
+ # Models
114
+ self.lstm = LSTMAlpha(input_size, lstm_hidden, lstm_layers).to(self.device)
115
+ self.transformer = TransformerAlpha(input_size, trans_d_model,
116
+ trans_nhead, trans_layers).to(self.device)
117
+ self.xgboost = XGBoostAlpha(xgb_depth, xgb_lr, xgb_estimators)
118
+
119
+ # Weights
120
+ self.weights = weights or {'lstm': 0.3, 'transformer': 0.3, 'xgboost': 0.4}
121
+
122
+ self.is_fitted = False
123
+ self.ic_history = []
124
+ self.feature_drift_history = []
125
+
126
+ def fit(self, X_train: np.ndarray, y_train: np.ndarray,
127
+ X_val: Optional[np.ndarray] = None, y_val: Optional[np.ndarray] = None,
128
+ epochs: int = 50, batch_size: int = 64, lr: float = 1e-4) -> Dict:
129
+ """Train all models"""
130
+
131
+ # Train LSTM
132
+ print("Training LSTM alpha model...")
133
+ lstm_metrics = self._train_nn(self.lstm, X_train, y_train,
134
+ X_val, y_val, epochs, batch_size, lr)
135
+
136
+ # Train Transformer
137
+ print("Training Transformer alpha model...")
138
+ trans_metrics = self._train_nn(self.transformer, X_train, y_train,
139
+ X_val, y_val, epochs, batch_size, lr)
140
+
141
+ # Train XGBoost
142
+ print("Training XGBoost alpha model...")
143
+ self.xgboost.fit(X_train, y_train)
144
+ xgb_pred = self.xgboost.predict(X_val) if X_val is not None else None
145
+ xgb_ic = self._compute_ic(xgb_pred, y_val) if xgb_pred is not None else None
146
+
147
+ self.is_fitted = True
148
+
149
+ return {
150
+ 'lstm': lstm_metrics,
151
+ 'transformer': trans_metrics,
152
+ 'xgboost': {'ic': xgb_ic}
153
+ }
154
+
155
+ def _train_nn(self, model: nn.Module, X_train: np.ndarray, y_train: np.ndarray,
156
+ X_val: Optional[np.ndarray], y_val: Optional[np.ndarray],
157
+ epochs: int, batch_size: int, lr: float) -> Dict:
158
+ """Train a neural network model"""
159
+ train_dataset = AlphaDataset(X_train, y_train)
160
+ train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
161
+
162
+ optimizer = torch.optim.Adam(model.parameters(), lr=lr)
163
+ criterion = nn.MSELoss()
164
+
165
+ metrics = {'train_loss': [], 'val_loss': [], 'val_ic': []}
166
+
167
+ for epoch in range(epochs):
168
+ model.train()
169
+ epoch_loss = 0
170
+ for X_batch, y_batch in train_loader:
171
+ X_batch, y_batch = X_batch.to(self.device), y_batch.to(self.device)
172
+ optimizer.zero_grad()
173
+ pred = model(X_batch)
174
+ loss = criterion(pred, y_batch)
175
+ loss.backward()
176
+ optimizer.step()
177
+ epoch_loss += loss.item()
178
+
179
+ metrics['train_loss'].append(epoch_loss / len(train_loader))
180
+
181
+ # Validation
182
+ if X_val is not None and y_val is not None:
183
+ model.eval()
184
+ with torch.no_grad():
185
+ X_val_t = torch.FloatTensor(X_val).to(self.device)
186
+ val_pred = model(X_val_t).cpu().numpy().flatten()
187
+ val_loss = np.mean((val_pred - y_val) ** 2)
188
+ val_ic = self._compute_ic(val_pred, y_val)
189
+ metrics['val_loss'].append(val_loss)
190
+ metrics['val_ic'].append(val_ic)
191
+
192
+ if epoch % 10 == 0:
193
+ print(f" Epoch {epoch}: train_loss={metrics['train_loss'][-1]:.6f}, "
194
+ f"val_loss={val_loss:.6f}, val_ic={val_ic:.4f}")
195
+
196
+ return metrics
197
+
198
+ def predict(self, X: np.ndarray) -> np.ndarray:
199
+ """Generate ensemble predictions"""
200
+ if not self.is_fitted:
201
+ raise ValueError("Models must be fitted before prediction")
202
+
203
+ # LSTM prediction
204
+ self.lstm.eval()
205
+ with torch.no_grad():
206
+ X_t = torch.FloatTensor(X).to(self.device)
207
+ lstm_pred = self.lstm(X_t).cpu().numpy().flatten()
208
+
209
+ # Transformer prediction
210
+ self.transformer.eval()
211
+ with torch.no_grad():
212
+ trans_pred = self.transformer(X_t).cpu().numpy().flatten()
213
+
214
+ # XGBoost prediction
215
+ xgb_pred = self.xgboost.predict(X)
216
+
217
+ # Weighted ensemble
218
+ ensemble = (self.weights['lstm'] * lstm_pred +
219
+ self.weights['transformer'] * trans_pred +
220
+ self.weights['xgboost'] * xgb_pred)
221
+
222
+ return ensemble
223
+
224
+ def _compute_ic(self, pred: np.ndarray, actual: np.ndarray) -> float:
225
+ """Compute Information Coefficient (rank correlation)"""
226
+ if pred is None or len(pred) < 10:
227
+ return 0.0
228
+ mask = ~(np.isnan(pred) | np.isnan(actual))
229
+ if mask.sum() < 10:
230
+ return 0.0
231
+ from scipy.stats import spearmanr
232
+ ic, _ = spearmanr(pred[mask], actual[mask])
233
+ return ic if not np.isnan(ic) else 0.0
234
+
235
+ def track_ic(self, pred: np.ndarray, actual: np.ndarray):
236
+ """Track IC over time"""
237
+ ic = self._compute_ic(pred, actual)
238
+ self.ic_history.append(ic)
239
+ return ic
240
+
241
+ def track_feature_drift(self, X_current: np.ndarray, X_reference: np.ndarray):
242
+ """Track feature importance drift using XGBoost"""
243
+ current_imp = self.xgboost.feature_importances()
244
+
245
+ # Fit reference model
246
+ ref_model = XGBoostAlpha()
247
+ ref_model.fit(X_reference, np.zeros(len(X_reference)))
248
+ ref_imp = ref_model.feature_importances()
249
+
250
+ # JS divergence between importance distributions
251
+ drift = np.sum(np.abs(current_imp - ref_imp))
252
+ self.feature_drift_history.append(drift)
253
+ return drift