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,481 @@
|
|
|
1
|
+
"""REST API for model serving"""
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, HTTPException, BackgroundTasks, File, UploadFile
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from typing import Dict, Any, Optional, List, Union
|
|
7
|
+
import torch
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
import logging
|
|
12
|
+
import asyncio
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import json
|
|
15
|
+
import uvicorn
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
import os
|
|
20
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
|
|
21
|
+
|
|
22
|
+
from ml.models.recommendation_models import StockRecommendationModel, PortfolioRecommendation
|
|
23
|
+
from ml.features.stock_features import StockRecommendationFeatures
|
|
24
|
+
from ml.features.political_features import PoliticalInfluenceFeatures
|
|
25
|
+
from ml.features.ensemble_features import EnsembleFeatureBuilder
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Pydantic models for API
|
|
31
|
+
class PredictionRequest(BaseModel):
|
|
32
|
+
"""Request model for predictions"""
|
|
33
|
+
trading_data: Dict[str, Any] = Field(..., description="Politician trading data")
|
|
34
|
+
stock_data: Optional[Dict[str, Any]] = Field(None, description="Stock price data")
|
|
35
|
+
tickers: List[str] = Field(..., description="Stock tickers to analyze")
|
|
36
|
+
features: Optional[Dict[str, float]] = Field(None, description="Pre-computed features")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PredictionResponse(BaseModel):
|
|
40
|
+
"""Response model for predictions"""
|
|
41
|
+
recommendations: List[Dict[str, Any]]
|
|
42
|
+
timestamp: str
|
|
43
|
+
model_version: str
|
|
44
|
+
processing_time_ms: float
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HealthResponse(BaseModel):
|
|
48
|
+
"""Health check response"""
|
|
49
|
+
status: str
|
|
50
|
+
model_loaded: bool
|
|
51
|
+
model_version: Optional[str]
|
|
52
|
+
uptime_seconds: float
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ModelMetricsResponse(BaseModel):
|
|
56
|
+
"""Model metrics response"""
|
|
57
|
+
accuracy: float
|
|
58
|
+
precision: float
|
|
59
|
+
recall: float
|
|
60
|
+
f1_score: float
|
|
61
|
+
last_updated: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BatchPredictionRequest(BaseModel):
|
|
65
|
+
"""Batch prediction request"""
|
|
66
|
+
batch_id: str
|
|
67
|
+
data: List[PredictionRequest]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BatchPredictionResponse(BaseModel):
|
|
71
|
+
"""Batch prediction response"""
|
|
72
|
+
batch_id: str
|
|
73
|
+
status: str
|
|
74
|
+
progress: float
|
|
75
|
+
results: Optional[List[PredictionResponse]]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ModelEndpoint:
|
|
79
|
+
"""Model endpoint manager"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, model_path: Optional[str] = None):
|
|
82
|
+
self.model = None
|
|
83
|
+
self.model_path = model_path
|
|
84
|
+
self.model_version = "1.0.0"
|
|
85
|
+
self.feature_extractors = {}
|
|
86
|
+
self.start_time = datetime.now()
|
|
87
|
+
self.prediction_cache = {}
|
|
88
|
+
self.metrics = {
|
|
89
|
+
"total_predictions": 0,
|
|
90
|
+
"avg_latency_ms": 0,
|
|
91
|
+
"cache_hits": 0,
|
|
92
|
+
"errors": 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if model_path:
|
|
96
|
+
self.load_model(model_path)
|
|
97
|
+
|
|
98
|
+
self._setup_feature_extractors()
|
|
99
|
+
|
|
100
|
+
def _setup_feature_extractors(self):
|
|
101
|
+
"""Initialize feature extractors"""
|
|
102
|
+
self.feature_extractors = {
|
|
103
|
+
"stock": StockRecommendationFeatures(),
|
|
104
|
+
"political": PoliticalInfluenceFeatures(),
|
|
105
|
+
"ensemble": EnsembleFeatureBuilder()
|
|
106
|
+
}
|
|
107
|
+
logger.info("Feature extractors initialized")
|
|
108
|
+
|
|
109
|
+
def load_model(self, model_path: str):
|
|
110
|
+
"""Load model from file"""
|
|
111
|
+
try:
|
|
112
|
+
checkpoint = torch.load(model_path, map_location='cpu')
|
|
113
|
+
|
|
114
|
+
# Reconstruct model (simplified - would need proper config)
|
|
115
|
+
from ml.models.ensemble_models import EnsembleConfig, ModelConfig
|
|
116
|
+
from ml.models.recommendation_models import RecommendationConfig
|
|
117
|
+
|
|
118
|
+
# Create dummy config for loading
|
|
119
|
+
model_configs = [
|
|
120
|
+
ModelConfig(
|
|
121
|
+
model_type="mlp",
|
|
122
|
+
hidden_dims=[256, 128],
|
|
123
|
+
dropout_rate=0.3,
|
|
124
|
+
learning_rate=0.001,
|
|
125
|
+
weight_decay=1e-4,
|
|
126
|
+
batch_size=32,
|
|
127
|
+
epochs=10
|
|
128
|
+
)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
ensemble_config = EnsembleConfig(base_models=model_configs)
|
|
132
|
+
recommendation_config = RecommendationConfig(ensemble_config=ensemble_config)
|
|
133
|
+
|
|
134
|
+
# Initialize model (need to know input dimension)
|
|
135
|
+
input_dim = 100 # Default, would be stored in checkpoint
|
|
136
|
+
self.model = StockRecommendationModel(input_dim, recommendation_config)
|
|
137
|
+
|
|
138
|
+
# Load state
|
|
139
|
+
self.model.load_state_dict(checkpoint['model_state_dict'])
|
|
140
|
+
self.model.eval()
|
|
141
|
+
|
|
142
|
+
self.model_version = checkpoint.get('version', '1.0.0')
|
|
143
|
+
logger.info(f"Model loaded from {model_path} (version: {self.model_version})")
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"Failed to load model: {e}")
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
def extract_features(self, trading_data: pd.DataFrame,
|
|
150
|
+
stock_data: Optional[pd.DataFrame] = None) -> np.ndarray:
|
|
151
|
+
"""Extract features from raw data"""
|
|
152
|
+
features = pd.DataFrame()
|
|
153
|
+
|
|
154
|
+
# Extract political features
|
|
155
|
+
if not trading_data.empty:
|
|
156
|
+
political_features = self.feature_extractors["political"].extract_influence_features(trading_data)
|
|
157
|
+
features = pd.concat([features, political_features], axis=1)
|
|
158
|
+
|
|
159
|
+
# Extract stock features if available
|
|
160
|
+
if stock_data is not None and not stock_data.empty:
|
|
161
|
+
try:
|
|
162
|
+
stock_features = self.feature_extractors["stock"].extract_features(stock_data)
|
|
163
|
+
features = pd.concat([features, stock_features], axis=1)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"Could not extract stock features: {e}")
|
|
166
|
+
|
|
167
|
+
# Build ensemble features
|
|
168
|
+
if not features.empty:
|
|
169
|
+
features = self.feature_extractors["ensemble"].build_ensemble_features(features)
|
|
170
|
+
|
|
171
|
+
return features.values if not features.empty else np.array([[]])
|
|
172
|
+
|
|
173
|
+
async def predict(self, request: PredictionRequest) -> PredictionResponse:
|
|
174
|
+
"""Generate predictions"""
|
|
175
|
+
start_time = datetime.now()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Check cache
|
|
179
|
+
cache_key = json.dumps(request.dict(), sort_keys=True)
|
|
180
|
+
if cache_key in self.prediction_cache:
|
|
181
|
+
self.metrics["cache_hits"] += 1
|
|
182
|
+
cached_response = self.prediction_cache[cache_key]
|
|
183
|
+
cached_response.processing_time_ms = 0.1 # Cache hit is fast
|
|
184
|
+
return cached_response
|
|
185
|
+
|
|
186
|
+
# Convert request data to DataFrames
|
|
187
|
+
trading_df = pd.DataFrame([request.trading_data])
|
|
188
|
+
stock_df = pd.DataFrame([request.stock_data]) if request.stock_data else None
|
|
189
|
+
|
|
190
|
+
# Extract features or use provided features
|
|
191
|
+
if request.features:
|
|
192
|
+
features = np.array([list(request.features.values())])
|
|
193
|
+
else:
|
|
194
|
+
features = self.extract_features(trading_df, stock_df)
|
|
195
|
+
|
|
196
|
+
# Generate recommendations
|
|
197
|
+
if self.model:
|
|
198
|
+
recommendations = self.model.generate_recommendations(
|
|
199
|
+
features,
|
|
200
|
+
request.tickers,
|
|
201
|
+
market_data=stock_df
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
# Mock recommendations if no model loaded
|
|
205
|
+
recommendations = self._generate_mock_recommendations(request.tickers)
|
|
206
|
+
|
|
207
|
+
# Convert recommendations to dict
|
|
208
|
+
recommendations_dict = [
|
|
209
|
+
{
|
|
210
|
+
"ticker": rec.ticker,
|
|
211
|
+
"score": rec.recommendation_score,
|
|
212
|
+
"confidence": rec.confidence,
|
|
213
|
+
"risk_level": rec.risk_level,
|
|
214
|
+
"expected_return": rec.expected_return,
|
|
215
|
+
"position_size": rec.position_size,
|
|
216
|
+
"entry_price": rec.entry_price,
|
|
217
|
+
"target_price": rec.target_price,
|
|
218
|
+
"stop_loss": rec.stop_loss,
|
|
219
|
+
"reason": rec.recommendation_reason
|
|
220
|
+
}
|
|
221
|
+
for rec in recommendations
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
# Calculate processing time
|
|
225
|
+
processing_time = (datetime.now() - start_time).total_seconds() * 1000
|
|
226
|
+
|
|
227
|
+
# Update metrics
|
|
228
|
+
self.metrics["total_predictions"] += 1
|
|
229
|
+
self.metrics["avg_latency_ms"] = (
|
|
230
|
+
(self.metrics["avg_latency_ms"] * (self.metrics["total_predictions"] - 1) + processing_time)
|
|
231
|
+
/ self.metrics["total_predictions"]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
response = PredictionResponse(
|
|
235
|
+
recommendations=recommendations_dict,
|
|
236
|
+
timestamp=datetime.now().isoformat(),
|
|
237
|
+
model_version=self.model_version,
|
|
238
|
+
processing_time_ms=processing_time
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Cache response
|
|
242
|
+
self.prediction_cache[cache_key] = response
|
|
243
|
+
|
|
244
|
+
# Limit cache size
|
|
245
|
+
if len(self.prediction_cache) > 1000:
|
|
246
|
+
# Remove oldest entries
|
|
247
|
+
keys_to_remove = list(self.prediction_cache.keys())[:100]
|
|
248
|
+
for key in keys_to_remove:
|
|
249
|
+
del self.prediction_cache[key]
|
|
250
|
+
|
|
251
|
+
return response
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.metrics["errors"] += 1
|
|
255
|
+
logger.error(f"Prediction error: {e}")
|
|
256
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
257
|
+
|
|
258
|
+
def _generate_mock_recommendations(self, tickers: List[str]) -> List[PortfolioRecommendation]:
|
|
259
|
+
"""Generate mock recommendations for testing"""
|
|
260
|
+
recommendations = []
|
|
261
|
+
for ticker in tickers:
|
|
262
|
+
rec = PortfolioRecommendation(
|
|
263
|
+
ticker=ticker,
|
|
264
|
+
recommendation_score=np.random.random(),
|
|
265
|
+
confidence=np.random.random(),
|
|
266
|
+
risk_level=np.random.choice(["low", "medium", "high"]),
|
|
267
|
+
expected_return=np.random.normal(0.05, 0.15),
|
|
268
|
+
risk_adjusted_score=np.random.random(),
|
|
269
|
+
position_size=np.random.uniform(0.01, 0.1),
|
|
270
|
+
recommendation_reason="Mock recommendation for testing"
|
|
271
|
+
)
|
|
272
|
+
recommendations.append(rec)
|
|
273
|
+
return recommendations
|
|
274
|
+
|
|
275
|
+
def get_health(self) -> HealthResponse:
|
|
276
|
+
"""Get endpoint health status"""
|
|
277
|
+
uptime = (datetime.now() - self.start_time).total_seconds()
|
|
278
|
+
|
|
279
|
+
return HealthResponse(
|
|
280
|
+
status="healthy",
|
|
281
|
+
model_loaded=self.model is not None,
|
|
282
|
+
model_version=self.model_version,
|
|
283
|
+
uptime_seconds=uptime
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
287
|
+
"""Get endpoint metrics"""
|
|
288
|
+
return {
|
|
289
|
+
**self.metrics,
|
|
290
|
+
"model_version": self.model_version,
|
|
291
|
+
"uptime_seconds": (datetime.now() - self.start_time).total_seconds(),
|
|
292
|
+
"cache_size": len(self.prediction_cache)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class PredictionService:
|
|
297
|
+
"""Async prediction service for batch processing"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, model_endpoint: ModelEndpoint):
|
|
300
|
+
self.model_endpoint = model_endpoint
|
|
301
|
+
self.batch_jobs = {}
|
|
302
|
+
self.executor = None
|
|
303
|
+
|
|
304
|
+
async def process_batch(self, batch_request: BatchPredictionRequest) -> BatchPredictionResponse:
|
|
305
|
+
"""Process batch predictions asynchronously"""
|
|
306
|
+
batch_id = batch_request.batch_id
|
|
307
|
+
|
|
308
|
+
# Initialize batch job
|
|
309
|
+
self.batch_jobs[batch_id] = {
|
|
310
|
+
"status": "processing",
|
|
311
|
+
"progress": 0.0,
|
|
312
|
+
"results": [],
|
|
313
|
+
"start_time": datetime.now()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Process each request
|
|
317
|
+
results = []
|
|
318
|
+
total = len(batch_request.data)
|
|
319
|
+
|
|
320
|
+
for i, request in enumerate(batch_request.data):
|
|
321
|
+
try:
|
|
322
|
+
result = await self.model_endpoint.predict(request)
|
|
323
|
+
results.append(result)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error(f"Batch prediction error for item {i}: {e}")
|
|
326
|
+
results.append(None)
|
|
327
|
+
|
|
328
|
+
# Update progress
|
|
329
|
+
self.batch_jobs[batch_id]["progress"] = (i + 1) / total
|
|
330
|
+
|
|
331
|
+
# Update job status
|
|
332
|
+
self.batch_jobs[batch_id]["status"] = "completed"
|
|
333
|
+
self.batch_jobs[batch_id]["results"] = results
|
|
334
|
+
self.batch_jobs[batch_id]["end_time"] = datetime.now()
|
|
335
|
+
|
|
336
|
+
return BatchPredictionResponse(
|
|
337
|
+
batch_id=batch_id,
|
|
338
|
+
status="completed",
|
|
339
|
+
progress=1.0,
|
|
340
|
+
results=results
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def get_batch_status(self, batch_id: str) -> BatchPredictionResponse:
|
|
344
|
+
"""Get batch job status"""
|
|
345
|
+
if batch_id not in self.batch_jobs:
|
|
346
|
+
raise HTTPException(status_code=404, detail="Batch job not found")
|
|
347
|
+
|
|
348
|
+
job = self.batch_jobs[batch_id]
|
|
349
|
+
|
|
350
|
+
return BatchPredictionResponse(
|
|
351
|
+
batch_id=batch_id,
|
|
352
|
+
status=job["status"],
|
|
353
|
+
progress=job["progress"],
|
|
354
|
+
results=job.get("results")
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class ModelServer:
|
|
359
|
+
"""FastAPI model server"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, model_path: Optional[str] = None):
|
|
362
|
+
self.model_endpoint = ModelEndpoint(model_path)
|
|
363
|
+
self.prediction_service = PredictionService(self.model_endpoint)
|
|
364
|
+
self.app = self._create_app()
|
|
365
|
+
|
|
366
|
+
@asynccontextmanager
|
|
367
|
+
async def lifespan(self, app: FastAPI):
|
|
368
|
+
"""Manage application lifecycle"""
|
|
369
|
+
# Startup
|
|
370
|
+
logger.info("Starting model server...")
|
|
371
|
+
yield
|
|
372
|
+
# Shutdown
|
|
373
|
+
logger.info("Shutting down model server...")
|
|
374
|
+
|
|
375
|
+
def _create_app(self) -> FastAPI:
|
|
376
|
+
"""Create FastAPI application"""
|
|
377
|
+
app = FastAPI(
|
|
378
|
+
title="Stock Recommendation API",
|
|
379
|
+
description="ML-powered stock recommendation system based on politician trading data",
|
|
380
|
+
version="1.0.0",
|
|
381
|
+
lifespan=self.lifespan
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Health check
|
|
385
|
+
@app.get("/health", response_model=HealthResponse)
|
|
386
|
+
async def health():
|
|
387
|
+
"""Health check endpoint"""
|
|
388
|
+
return self.model_endpoint.get_health()
|
|
389
|
+
|
|
390
|
+
# Metrics
|
|
391
|
+
@app.get("/metrics")
|
|
392
|
+
async def metrics():
|
|
393
|
+
"""Get service metrics"""
|
|
394
|
+
return self.model_endpoint.get_metrics()
|
|
395
|
+
|
|
396
|
+
# Single prediction
|
|
397
|
+
@app.post("/predict", response_model=PredictionResponse)
|
|
398
|
+
async def predict(request: PredictionRequest):
|
|
399
|
+
"""Generate stock recommendations"""
|
|
400
|
+
return await self.model_endpoint.predict(request)
|
|
401
|
+
|
|
402
|
+
# Batch prediction
|
|
403
|
+
@app.post("/batch/predict", response_model=BatchPredictionResponse)
|
|
404
|
+
async def batch_predict(batch_request: BatchPredictionRequest,
|
|
405
|
+
background_tasks: BackgroundTasks):
|
|
406
|
+
"""Submit batch prediction job"""
|
|
407
|
+
background_tasks.add_task(
|
|
408
|
+
self.prediction_service.process_batch,
|
|
409
|
+
batch_request
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return BatchPredictionResponse(
|
|
413
|
+
batch_id=batch_request.batch_id,
|
|
414
|
+
status="submitted",
|
|
415
|
+
progress=0.0,
|
|
416
|
+
results=None
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Batch status
|
|
420
|
+
@app.get("/batch/{batch_id}", response_model=BatchPredictionResponse)
|
|
421
|
+
async def batch_status(batch_id: str):
|
|
422
|
+
"""Get batch job status"""
|
|
423
|
+
return self.prediction_service.get_batch_status(batch_id)
|
|
424
|
+
|
|
425
|
+
# Model reload
|
|
426
|
+
@app.post("/model/reload")
|
|
427
|
+
async def reload_model(model_path: str):
|
|
428
|
+
"""Reload model from new path"""
|
|
429
|
+
try:
|
|
430
|
+
self.model_endpoint.load_model(model_path)
|
|
431
|
+
return {"status": "success", "model_version": self.model_endpoint.model_version}
|
|
432
|
+
except Exception as e:
|
|
433
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
434
|
+
|
|
435
|
+
# Upload and predict
|
|
436
|
+
@app.post("/upload/predict")
|
|
437
|
+
async def upload_predict(file: UploadFile = File(...)):
|
|
438
|
+
"""Upload CSV and get predictions"""
|
|
439
|
+
try:
|
|
440
|
+
# Read uploaded file
|
|
441
|
+
content = await file.read()
|
|
442
|
+
df = pd.read_csv(pd.io.common.BytesIO(content))
|
|
443
|
+
|
|
444
|
+
# Extract tickers
|
|
445
|
+
tickers = df['ticker_cleaned'].unique().tolist() if 'ticker_cleaned' in df.columns else []
|
|
446
|
+
|
|
447
|
+
# Create request
|
|
448
|
+
request = PredictionRequest(
|
|
449
|
+
trading_data=df.to_dict(orient='records')[0] if len(df) > 0 else {},
|
|
450
|
+
tickers=tickers[:5] # Limit to 5 tickers
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return await self.model_endpoint.predict(request)
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
457
|
+
|
|
458
|
+
return app
|
|
459
|
+
|
|
460
|
+
def run(self, host: str = "0.0.0.0", port: int = 8000):
|
|
461
|
+
"""Run the server"""
|
|
462
|
+
uvicorn.run(self.app, host=host, port=port)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def main():
|
|
466
|
+
"""Run the model server"""
|
|
467
|
+
import argparse
|
|
468
|
+
|
|
469
|
+
parser = argparse.ArgumentParser(description="Stock Recommendation Model Server")
|
|
470
|
+
parser.add_argument("--model-path", type=str, help="Path to model file")
|
|
471
|
+
parser.add_argument("--host", type=str, default="0.0.0.0", help="Server host")
|
|
472
|
+
parser.add_argument("--port", type=int, default=8000, help="Server port")
|
|
473
|
+
|
|
474
|
+
args = parser.parse_args()
|
|
475
|
+
|
|
476
|
+
server = ModelServer(args.model_path)
|
|
477
|
+
server.run(args.host, args.port)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
if __name__ == "__main__":
|
|
481
|
+
main()
|