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.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- 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)
|