mcli-framework 7.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,675 @@
1
+ """Ensemble models for stock prediction"""
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ import numpy as np
7
+ import pandas as pd
8
+ from typing import Dict, List, Optional, Tuple, Any, Union
9
+ from dataclasses import dataclass
10
+ import logging
11
+ from base_models import BaseStockModel, ModelMetrics, ValidationResult
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class ModelConfig:
18
+ """Configuration for individual models"""
19
+
20
+ model_type: str
21
+ hidden_dims: List[int]
22
+ dropout_rate: float
23
+ learning_rate: float
24
+ weight_decay: float
25
+ batch_size: int
26
+ epochs: int
27
+
28
+
29
+ @dataclass
30
+ class EnsembleConfig:
31
+ """Configuration for ensemble model"""
32
+
33
+ base_models: List[ModelConfig]
34
+ ensemble_method: str = "weighted_average" # weighted_average, stacking, voting
35
+ meta_learner_config: Optional[ModelConfig] = None
36
+ feature_subsampling: bool = True
37
+ bootstrap_samples: bool = True
38
+ n_bootstrap: int = 5
39
+
40
+
41
+ class AttentionStockPredictor(BaseStockModel):
42
+ """Attention-based stock predictor"""
43
+
44
+ def __init__(
45
+ self,
46
+ input_dim: int,
47
+ hidden_dim: int = 256,
48
+ num_heads: int = 8,
49
+ num_layers: int = 3,
50
+ output_dim: int = 2,
51
+ dropout_rate: float = 0.1,
52
+ config: Optional[Dict[str, Any]] = None,
53
+ ):
54
+ super().__init__(input_dim, config)
55
+
56
+ self.hidden_dim = hidden_dim
57
+ self.num_heads = num_heads
58
+ self.num_layers = num_layers
59
+ self.output_dim = output_dim
60
+
61
+ # Input projection
62
+ self.input_proj = nn.Linear(input_dim, hidden_dim)
63
+
64
+ # Multi-head attention layers
65
+ self.attention_layers = nn.ModuleList(
66
+ [
67
+ nn.MultiheadAttention(
68
+ embed_dim=hidden_dim,
69
+ num_heads=num_heads,
70
+ dropout=dropout_rate,
71
+ batch_first=True,
72
+ )
73
+ for _ in range(num_layers)
74
+ ]
75
+ )
76
+
77
+ # Feed-forward networks
78
+ self.ffn_layers = nn.ModuleList(
79
+ [
80
+ nn.Sequential(
81
+ nn.Linear(hidden_dim, hidden_dim * 2),
82
+ nn.ReLU(),
83
+ nn.Dropout(dropout_rate),
84
+ nn.Linear(hidden_dim * 2, hidden_dim),
85
+ nn.Dropout(dropout_rate),
86
+ )
87
+ for _ in range(num_layers)
88
+ ]
89
+ )
90
+
91
+ # Layer normalization
92
+ self.layer_norms = nn.ModuleList([nn.LayerNorm(hidden_dim) for _ in range(num_layers * 2)])
93
+
94
+ # Output layers
95
+ self.output_layer = nn.Sequential(
96
+ nn.Linear(hidden_dim, hidden_dim // 2),
97
+ nn.ReLU(),
98
+ nn.Dropout(dropout_rate),
99
+ nn.Linear(hidden_dim // 2, output_dim),
100
+ )
101
+
102
+ self.softmax = nn.Softmax(dim=1)
103
+
104
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
105
+ """Forward pass with attention mechanism"""
106
+ # Project input
107
+ x = self.input_proj(x)
108
+
109
+ # Add sequence dimension for attention (treat features as sequence)
110
+ if x.dim() == 2:
111
+ x = x.unsqueeze(1) # [batch, 1, features]
112
+
113
+ # Apply attention layers
114
+ for i in range(self.num_layers):
115
+ # Self-attention
116
+ residual = x
117
+ x = self.layer_norms[i * 2](x)
118
+ attn_output, _ = self.attention_layers[i](x, x, x)
119
+ x = residual + attn_output
120
+
121
+ # Feed-forward
122
+ residual = x
123
+ x = self.layer_norms[i * 2 + 1](x)
124
+ ffn_output = self.ffn_layers[i](x)
125
+ x = residual + ffn_output
126
+
127
+ # Global pooling and output
128
+ x = x.mean(dim=1) # Average pooling across sequence
129
+ return self.output_layer(x)
130
+
131
+ def predict_proba(self, X: Union[torch.Tensor, np.ndarray, pd.DataFrame]) -> np.ndarray:
132
+ """Predict class probabilities"""
133
+ self.eval()
134
+ with torch.no_grad():
135
+ X_tensor = self.preprocess_input(X)
136
+ logits = self.forward(X_tensor)
137
+ probas = self.softmax(logits)
138
+ return probas.cpu().numpy()
139
+
140
+
141
+ class TransformerStockModel(BaseStockModel):
142
+ """Transformer model for stock prediction"""
143
+
144
+ def __init__(
145
+ self,
146
+ input_dim: int,
147
+ d_model: int = 256,
148
+ nhead: int = 8,
149
+ num_layers: int = 4,
150
+ dim_feedforward: int = 1024,
151
+ output_dim: int = 2,
152
+ dropout_rate: float = 0.1,
153
+ config: Optional[Dict[str, Any]] = None,
154
+ ):
155
+ super().__init__(input_dim, config)
156
+
157
+ self.d_model = d_model
158
+ self.nhead = nhead
159
+ self.num_layers = num_layers
160
+
161
+ # Input embedding
162
+ self.input_embedding = nn.Linear(input_dim, d_model)
163
+ self.pos_encoding = PositionalEncoding(d_model, dropout_rate)
164
+
165
+ # Transformer encoder
166
+ encoder_layer = nn.TransformerEncoderLayer(
167
+ d_model=d_model,
168
+ nhead=nhead,
169
+ dim_feedforward=dim_feedforward,
170
+ dropout=dropout_rate,
171
+ batch_first=True,
172
+ )
173
+ self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers)
174
+
175
+ # Output layers
176
+ self.classifier = nn.Sequential(
177
+ nn.Linear(d_model, d_model // 2),
178
+ nn.ReLU(),
179
+ nn.Dropout(dropout_rate),
180
+ nn.Linear(d_model // 2, output_dim),
181
+ )
182
+
183
+ self.softmax = nn.Softmax(dim=1)
184
+
185
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
186
+ """Forward pass through transformer"""
187
+ # Embed input
188
+ x = self.input_embedding(x)
189
+
190
+ # Add sequence dimension if needed
191
+ if x.dim() == 2:
192
+ x = x.unsqueeze(1)
193
+
194
+ # Add positional encoding
195
+ x = self.pos_encoding(x)
196
+
197
+ # Apply transformer
198
+ x = self.transformer_encoder(x)
199
+
200
+ # Global average pooling
201
+ x = x.mean(dim=1)
202
+
203
+ return self.classifier(x)
204
+
205
+ def predict_proba(self, X: Union[torch.Tensor, np.ndarray, pd.DataFrame]) -> np.ndarray:
206
+ """Predict class probabilities"""
207
+ self.eval()
208
+ with torch.no_grad():
209
+ X_tensor = self.preprocess_input(X)
210
+ logits = self.forward(X_tensor)
211
+ probas = self.softmax(logits)
212
+ return probas.cpu().numpy()
213
+
214
+
215
+ class PositionalEncoding(nn.Module):
216
+ """Positional encoding for transformer"""
217
+
218
+ def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
219
+ super().__init__()
220
+ self.dropout = nn.Dropout(p=dropout)
221
+
222
+ pe = torch.zeros(max_len, d_model)
223
+ position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
224
+ div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model))
225
+ pe[:, 0::2] = torch.sin(position * div_term)
226
+ pe[:, 1::2] = torch.cos(position * div_term)
227
+ pe = pe.unsqueeze(0)
228
+ self.register_buffer("pe", pe)
229
+
230
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
231
+ x = x + self.pe[:, : x.size(1), :]
232
+ return self.dropout(x)
233
+
234
+
235
+ class LSTMStockPredictor(BaseStockModel):
236
+ """LSTM-based stock predictor"""
237
+
238
+ def __init__(
239
+ self,
240
+ input_dim: int,
241
+ hidden_dim: int = 256,
242
+ num_layers: int = 2,
243
+ output_dim: int = 2,
244
+ dropout_rate: float = 0.2,
245
+ config: Optional[Dict[str, Any]] = None,
246
+ ):
247
+ super().__init__(input_dim, config)
248
+
249
+ self.hidden_dim = hidden_dim
250
+ self.num_layers = num_layers
251
+
252
+ # LSTM layers
253
+ self.lstm = nn.LSTM(
254
+ input_size=input_dim,
255
+ hidden_size=hidden_dim,
256
+ num_layers=num_layers,
257
+ dropout=dropout_rate if num_layers > 1 else 0,
258
+ batch_first=True,
259
+ bidirectional=True,
260
+ )
261
+
262
+ # Output layers
263
+ self.classifier = nn.Sequential(
264
+ nn.Linear(hidden_dim * 2, hidden_dim), # *2 for bidirectional
265
+ nn.ReLU(),
266
+ nn.Dropout(dropout_rate),
267
+ nn.Linear(hidden_dim, output_dim),
268
+ )
269
+
270
+ self.softmax = nn.Softmax(dim=1)
271
+
272
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
273
+ """Forward pass through LSTM"""
274
+ # Add sequence dimension if needed
275
+ if x.dim() == 2:
276
+ x = x.unsqueeze(1)
277
+
278
+ # LSTM forward pass
279
+ lstm_out, (hidden, cell) = self.lstm(x)
280
+
281
+ # Use the last output
282
+ x = lstm_out[:, -1, :]
283
+
284
+ return self.classifier(x)
285
+
286
+ def predict_proba(self, X: Union[torch.Tensor, np.ndarray, pd.DataFrame]) -> np.ndarray:
287
+ """Predict class probabilities"""
288
+ self.eval()
289
+ with torch.no_grad():
290
+ X_tensor = self.preprocess_input(X)
291
+ logits = self.forward(X_tensor)
292
+ probas = self.softmax(logits)
293
+ return probas.cpu().numpy()
294
+
295
+
296
+ class CNNFeatureExtractor(BaseStockModel):
297
+ """CNN-based feature extractor for tabular data"""
298
+
299
+ def __init__(
300
+ self,
301
+ input_dim: int,
302
+ num_filters: int = 64,
303
+ filter_sizes: List[int] = [3, 5, 7],
304
+ output_dim: int = 2,
305
+ dropout_rate: float = 0.3,
306
+ config: Optional[Dict[str, Any]] = None,
307
+ ):
308
+ super().__init__(input_dim, config)
309
+
310
+ self.num_filters = num_filters
311
+ self.filter_sizes = filter_sizes
312
+
313
+ # Reshape layer to create "image-like" structure
314
+ self.feature_reshape_dim = int(np.sqrt(input_dim)) + 1
315
+ if self.feature_reshape_dim**2 < input_dim:
316
+ self.feature_reshape_dim += 1
317
+
318
+ # Padding layer to reach perfect square
319
+ self.padding_size = self.feature_reshape_dim**2 - input_dim
320
+ if self.padding_size > 0:
321
+ self.input_padding = nn.ConstantPad1d((0, self.padding_size), 0)
322
+ else:
323
+ self.input_padding = None
324
+
325
+ # 1D Convolutions
326
+ self.conv_layers = nn.ModuleList(
327
+ [
328
+ nn.Conv1d(1, num_filters, kernel_size=filter_size, padding=filter_size // 2)
329
+ for filter_size in filter_sizes
330
+ ]
331
+ )
332
+
333
+ # Batch normalization
334
+ self.batch_norms = nn.ModuleList([nn.BatchNorm1d(num_filters) for _ in filter_sizes])
335
+
336
+ # Global pooling
337
+ self.global_pool = nn.AdaptiveMaxPool1d(1)
338
+
339
+ # Final classifier
340
+ total_filters = num_filters * len(filter_sizes)
341
+ self.classifier = nn.Sequential(
342
+ nn.Linear(total_filters, total_filters // 2),
343
+ nn.ReLU(),
344
+ nn.Dropout(dropout_rate),
345
+ nn.Linear(total_filters // 2, output_dim),
346
+ )
347
+
348
+ self.softmax = nn.Softmax(dim=1)
349
+
350
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
351
+ """Forward pass through CNN"""
352
+ batch_size = x.size(0)
353
+
354
+ # Apply padding if needed
355
+ if self.input_padding is not None:
356
+ x = self.input_padding(x)
357
+
358
+ # Add channel dimension
359
+ x = x.unsqueeze(1) # [batch, 1, features]
360
+
361
+ # Apply convolutions
362
+ conv_outputs = []
363
+ for conv_layer, batch_norm in zip(self.conv_layers, self.batch_norms):
364
+ conv_out = F.relu(batch_norm(conv_layer(x)))
365
+ pooled = self.global_pool(conv_out).squeeze(-1)
366
+ conv_outputs.append(pooled)
367
+
368
+ # Concatenate all conv outputs
369
+ x = torch.cat(conv_outputs, dim=1)
370
+
371
+ return self.classifier(x)
372
+
373
+ def predict_proba(self, X: Union[torch.Tensor, np.ndarray, pd.DataFrame]) -> np.ndarray:
374
+ """Predict class probabilities"""
375
+ self.eval()
376
+ with torch.no_grad():
377
+ X_tensor = self.preprocess_input(X)
378
+ logits = self.forward(X_tensor)
379
+ probas = self.softmax(logits)
380
+ return probas.cpu().numpy()
381
+
382
+
383
+ class DeepEnsembleModel(BaseStockModel):
384
+ """Deep ensemble combining multiple models"""
385
+
386
+ def __init__(self, input_dim: int, config: EnsembleConfig):
387
+ super().__init__(input_dim, config.__dict__)
388
+ self.ensemble_config = config
389
+ self.models = nn.ModuleList()
390
+ self.model_weights = None
391
+
392
+ # Create base models
393
+ for model_config in config.base_models:
394
+ model = self._create_model(model_config, input_dim)
395
+ self.models.append(model)
396
+
397
+ # Meta-learner for stacking
398
+ if config.ensemble_method == "stacking" and config.meta_learner_config:
399
+ meta_input_dim = len(config.base_models) * 2 # Each model outputs 2 classes
400
+ self.meta_learner = self._create_model(config.meta_learner_config, meta_input_dim)
401
+ else:
402
+ self.meta_learner = None
403
+
404
+ self.softmax = nn.Softmax(dim=1)
405
+
406
+ def _create_model(self, model_config: ModelConfig, input_dim: int) -> BaseStockModel:
407
+ """Create individual model based on configuration"""
408
+ if model_config.model_type == "attention":
409
+ return AttentionStockPredictor(
410
+ input_dim=input_dim,
411
+ hidden_dim=model_config.hidden_dims[0],
412
+ dropout_rate=model_config.dropout_rate,
413
+ )
414
+ elif model_config.model_type == "transformer":
415
+ return TransformerStockModel(
416
+ input_dim=input_dim,
417
+ d_model=model_config.hidden_dims[0],
418
+ dropout_rate=model_config.dropout_rate,
419
+ )
420
+ elif model_config.model_type == "lstm":
421
+ return LSTMStockPredictor(
422
+ input_dim=input_dim,
423
+ hidden_dim=model_config.hidden_dims[0],
424
+ dropout_rate=model_config.dropout_rate,
425
+ )
426
+ elif model_config.model_type == "cnn":
427
+ return CNNFeatureExtractor(
428
+ input_dim=input_dim,
429
+ num_filters=model_config.hidden_dims[0],
430
+ dropout_rate=model_config.dropout_rate,
431
+ )
432
+ else:
433
+ # Default to MLP
434
+ from base_models import MLPBaseModel
435
+
436
+ return MLPBaseModel(
437
+ input_dim=input_dim,
438
+ hidden_dims=model_config.hidden_dims,
439
+ dropout_rate=model_config.dropout_rate,
440
+ )
441
+
442
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
443
+ """Forward pass through ensemble"""
444
+ model_outputs = []
445
+
446
+ for model in self.models:
447
+ output = model(x)
448
+ model_outputs.append(output)
449
+
450
+ # Stack outputs
451
+ stacked_outputs = torch.stack(model_outputs, dim=1) # [batch, n_models, n_classes]
452
+
453
+ if self.ensemble_config.ensemble_method == "weighted_average":
454
+ # Weighted average of predictions
455
+ if self.model_weights is None:
456
+ weights = torch.ones(len(self.models), device=x.device) / len(self.models)
457
+ else:
458
+ weights = self.model_weights.to(x.device)
459
+
460
+ weights = weights.view(1, -1, 1) # [1, n_models, 1]
461
+ ensemble_output = (stacked_outputs * weights).sum(dim=1)
462
+
463
+ elif self.ensemble_config.ensemble_method == "voting":
464
+ # Majority voting (using argmax)
465
+ predictions = torch.argmax(stacked_outputs, dim=2) # [batch, n_models]
466
+ ensemble_pred = torch.mode(predictions, dim=1)[0] # [batch]
467
+ ensemble_output = F.one_hot(ensemble_pred, num_classes=stacked_outputs.size(2)).float()
468
+
469
+ elif self.ensemble_config.ensemble_method == "stacking" and self.meta_learner is not None:
470
+ # Use meta-learner
471
+ meta_input = stacked_outputs.view(stacked_outputs.size(0), -1) # Flatten
472
+ ensemble_output = self.meta_learner(meta_input)
473
+
474
+ else:
475
+ # Default to simple average
476
+ ensemble_output = stacked_outputs.mean(dim=1)
477
+
478
+ return ensemble_output
479
+
480
+ def predict_proba(self, X: Union[torch.Tensor, np.ndarray, pd.DataFrame]) -> np.ndarray:
481
+ """Predict class probabilities"""
482
+ self.eval()
483
+ with torch.no_grad():
484
+ X_tensor = self.preprocess_input(X)
485
+ logits = self.forward(X_tensor)
486
+ probas = self.softmax(logits)
487
+ return probas.cpu().numpy()
488
+
489
+ def set_model_weights(self, weights: List[float]):
490
+ """Set weights for weighted ensemble"""
491
+ self.model_weights = torch.FloatTensor(weights)
492
+
493
+ def get_individual_predictions(
494
+ self, X: Union[torch.Tensor, np.ndarray, pd.DataFrame]
495
+ ) -> List[np.ndarray]:
496
+ """Get predictions from individual models"""
497
+ predictions = []
498
+ self.eval()
499
+
500
+ for model in self.models:
501
+ with torch.no_grad():
502
+ pred = model.predict_proba(X)
503
+ predictions.append(pred)
504
+
505
+ return predictions
506
+
507
+
508
+ class EnsembleTrainer:
509
+ """Trainer for ensemble models"""
510
+
511
+ def __init__(self, ensemble_model: DeepEnsembleModel, config: EnsembleConfig):
512
+ self.ensemble_model = ensemble_model
513
+ self.config = config
514
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
515
+ self.ensemble_model.to(self.device)
516
+
517
+ def train(
518
+ self,
519
+ X_train: np.ndarray,
520
+ y_train: np.ndarray,
521
+ X_val: Optional[np.ndarray] = None,
522
+ y_val: Optional[np.ndarray] = None,
523
+ ) -> ValidationResult:
524
+ """Train the ensemble model"""
525
+ logger.info("Training ensemble model...")
526
+
527
+ # Train individual models first
528
+ individual_results = []
529
+ for i, (model, model_config) in enumerate(
530
+ zip(self.ensemble_model.models, self.config.base_models)
531
+ ):
532
+ logger.info(
533
+ f"Training model {i+1}/{len(self.ensemble_model.models)} ({model_config.model_type})"
534
+ )
535
+
536
+ # Create data subsets if enabled
537
+ X_subset, y_subset = self._create_subset(X_train, y_train, model_config)
538
+
539
+ # Train individual model
540
+ result = self._train_individual_model(
541
+ model, model_config, X_subset, y_subset, X_val, y_val
542
+ )
543
+ individual_results.append(result)
544
+
545
+ # Calculate ensemble weights based on validation performance
546
+ if X_val is not None and y_val is not None:
547
+ self._calculate_ensemble_weights(X_val, y_val)
548
+
549
+ # Train meta-learner if using stacking
550
+ if (
551
+ self.config.ensemble_method == "stacking"
552
+ and self.ensemble_model.meta_learner is not None
553
+ ):
554
+ self._train_meta_learner(X_train, y_train, X_val, y_val)
555
+
556
+ # Evaluate ensemble
557
+ train_metrics = self._evaluate(X_train, y_train)
558
+ val_metrics = self._evaluate(X_val, y_val) if X_val is not None else None
559
+
560
+ return ValidationResult(
561
+ train_metrics=train_metrics,
562
+ val_metrics=val_metrics,
563
+ training_history={"individual_results": individual_results},
564
+ )
565
+
566
+ def _create_subset(
567
+ self, X: np.ndarray, y: np.ndarray, model_config: ModelConfig
568
+ ) -> Tuple[np.ndarray, np.ndarray]:
569
+ """Create data subset for individual model training"""
570
+ if self.config.bootstrap_samples:
571
+ # Bootstrap sampling
572
+ n_samples = len(X)
573
+ indices = np.random.choice(n_samples, n_samples, replace=True)
574
+ return X[indices], y[indices]
575
+ else:
576
+ return X, y
577
+
578
+ def _train_individual_model(
579
+ self,
580
+ model: BaseStockModel,
581
+ model_config: ModelConfig,
582
+ X_train: np.ndarray,
583
+ y_train: np.ndarray,
584
+ X_val: Optional[np.ndarray],
585
+ y_val: Optional[np.ndarray],
586
+ ) -> Dict[str, Any]:
587
+ """Train individual model"""
588
+ from torch.utils.data import DataLoader, TensorDataset
589
+
590
+ # Convert to tensors
591
+ X_tensor = torch.FloatTensor(X_train).to(self.device)
592
+ y_tensor = torch.LongTensor(y_train).to(self.device)
593
+
594
+ # Create data loader
595
+ dataset = TensorDataset(X_tensor, y_tensor)
596
+ loader = DataLoader(dataset, batch_size=model_config.batch_size, shuffle=True)
597
+
598
+ # Setup optimizer and loss
599
+ optimizer = torch.optim.Adam(
600
+ model.parameters(),
601
+ lr=model_config.learning_rate,
602
+ weight_decay=model_config.weight_decay,
603
+ )
604
+ criterion = nn.CrossEntropyLoss()
605
+
606
+ # Training loop
607
+ model.train()
608
+ train_losses = []
609
+
610
+ for epoch in range(model_config.epochs):
611
+ epoch_loss = 0.0
612
+ for batch_X, batch_y in loader:
613
+ optimizer.zero_grad()
614
+ outputs = model(batch_X)
615
+ loss = criterion(outputs, batch_y)
616
+ loss.backward()
617
+ optimizer.step()
618
+ epoch_loss += loss.item()
619
+
620
+ avg_loss = epoch_loss / len(loader)
621
+ train_losses.append(avg_loss)
622
+
623
+ if epoch % 10 == 0:
624
+ logger.info(f"Epoch {epoch}/{model_config.epochs}, Loss: {avg_loss:.4f}")
625
+
626
+ model.is_trained = True
627
+ return {"train_losses": train_losses}
628
+
629
+ def _calculate_ensemble_weights(self, X_val: np.ndarray, y_val: np.ndarray):
630
+ """Calculate optimal ensemble weights based on validation performance"""
631
+ individual_predictions = self.ensemble_model.get_individual_predictions(X_val)
632
+
633
+ # Calculate individual model accuracies
634
+ accuracies = []
635
+ for pred in individual_predictions:
636
+ pred_labels = np.argmax(pred, axis=1)
637
+ accuracy = np.mean(pred_labels == y_val)
638
+ accuracies.append(accuracy)
639
+
640
+ # Convert to weights (higher accuracy = higher weight)
641
+ weights = np.array(accuracies)
642
+ weights = weights / weights.sum() # Normalize
643
+
644
+ self.ensemble_model.set_model_weights(weights.tolist())
645
+ logger.info(f"Ensemble weights: {weights}")
646
+
647
+ def _train_meta_learner(
648
+ self,
649
+ X_train: np.ndarray,
650
+ y_train: np.ndarray,
651
+ X_val: Optional[np.ndarray],
652
+ y_val: Optional[np.ndarray],
653
+ ):
654
+ """Train meta-learner for stacking ensemble"""
655
+ # Get predictions from base models
656
+ train_predictions = self.ensemble_model.get_individual_predictions(X_train)
657
+ meta_X_train = np.concatenate(train_predictions, axis=1)
658
+
659
+ # Train meta-learner
660
+ meta_config = self.config.meta_learner_config
661
+ result = self._train_individual_model(
662
+ self.ensemble_model.meta_learner, meta_config, meta_X_train, y_train, None, None
663
+ )
664
+
665
+ logger.info("Meta-learner training completed")
666
+
667
+ def _evaluate(self, X: np.ndarray, y: np.ndarray) -> ModelMetrics:
668
+ """Evaluate ensemble model"""
669
+ if X is None or y is None:
670
+ return ModelMetrics(0, 0, 0, 0, 0)
671
+
672
+ predictions = self.ensemble_model.predict(X)
673
+ probabilities = self.ensemble_model.predict_proba(X)
674
+
675
+ return self.ensemble_model.calculate_metrics(y, predictions, probabilities)