mcli-framework 7.1.0__py3-none-any.whl → 7.1.2__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/completion_cmd.py +59 -49
- mcli/app/completion_helpers.py +60 -138
- mcli/app/logs_cmd.py +46 -13
- mcli/app/main.py +17 -14
- mcli/app/model_cmd.py +19 -4
- mcli/chat/chat.py +3 -2
- mcli/lib/search/cached_vectorizer.py +1 -0
- mcli/lib/services/data_pipeline.py +12 -5
- mcli/lib/services/lsh_client.py +69 -58
- mcli/ml/api/app.py +28 -36
- mcli/ml/api/middleware.py +8 -16
- mcli/ml/api/routers/admin_router.py +3 -1
- mcli/ml/api/routers/auth_router.py +32 -56
- mcli/ml/api/routers/backtest_router.py +3 -1
- mcli/ml/api/routers/data_router.py +3 -1
- mcli/ml/api/routers/model_router.py +35 -74
- mcli/ml/api/routers/monitoring_router.py +3 -1
- mcli/ml/api/routers/portfolio_router.py +3 -1
- mcli/ml/api/routers/prediction_router.py +60 -65
- mcli/ml/api/routers/trade_router.py +6 -2
- mcli/ml/api/routers/websocket_router.py +12 -9
- mcli/ml/api/schemas.py +10 -2
- mcli/ml/auth/auth_manager.py +49 -114
- mcli/ml/auth/models.py +30 -15
- mcli/ml/auth/permissions.py +12 -19
- mcli/ml/backtesting/backtest_engine.py +134 -108
- mcli/ml/backtesting/performance_metrics.py +142 -108
- mcli/ml/cache.py +12 -18
- mcli/ml/cli/main.py +37 -23
- mcli/ml/config/settings.py +29 -12
- mcli/ml/dashboard/app.py +122 -130
- mcli/ml/dashboard/app_integrated.py +283 -152
- mcli/ml/dashboard/app_supabase.py +176 -108
- mcli/ml/dashboard/app_training.py +212 -206
- mcli/ml/dashboard/cli.py +14 -5
- mcli/ml/data_ingestion/api_connectors.py +51 -81
- mcli/ml/data_ingestion/data_pipeline.py +127 -125
- mcli/ml/data_ingestion/stream_processor.py +72 -80
- mcli/ml/database/migrations/env.py +3 -2
- mcli/ml/database/models.py +112 -79
- mcli/ml/database/session.py +6 -5
- mcli/ml/experimentation/ab_testing.py +149 -99
- mcli/ml/features/ensemble_features.py +9 -8
- mcli/ml/features/political_features.py +6 -5
- mcli/ml/features/recommendation_engine.py +15 -14
- mcli/ml/features/stock_features.py +7 -6
- mcli/ml/features/test_feature_engineering.py +8 -7
- mcli/ml/logging.py +10 -15
- mcli/ml/mlops/data_versioning.py +57 -64
- mcli/ml/mlops/experiment_tracker.py +49 -41
- mcli/ml/mlops/model_serving.py +59 -62
- mcli/ml/mlops/pipeline_orchestrator.py +203 -149
- mcli/ml/models/base_models.py +8 -7
- mcli/ml/models/ensemble_models.py +6 -5
- mcli/ml/models/recommendation_models.py +7 -6
- mcli/ml/models/test_models.py +18 -14
- mcli/ml/monitoring/drift_detection.py +95 -74
- mcli/ml/monitoring/metrics.py +10 -22
- mcli/ml/optimization/portfolio_optimizer.py +172 -132
- mcli/ml/predictions/prediction_engine.py +235 -0
- mcli/ml/preprocessing/data_cleaners.py +6 -5
- mcli/ml/preprocessing/feature_extractors.py +7 -6
- mcli/ml/preprocessing/ml_pipeline.py +3 -2
- mcli/ml/preprocessing/politician_trading_preprocessor.py +11 -10
- mcli/ml/preprocessing/test_preprocessing.py +4 -4
- mcli/ml/scripts/populate_sample_data.py +36 -16
- mcli/ml/tasks.py +82 -83
- mcli/ml/tests/test_integration.py +86 -76
- mcli/ml/tests/test_training_dashboard.py +169 -142
- mcli/mygroup/test_cmd.py +2 -1
- mcli/self/self_cmd.py +38 -18
- mcli/self/test_cmd.py +2 -1
- mcli/workflow/dashboard/dashboard_cmd.py +13 -6
- mcli/workflow/lsh_integration.py +46 -58
- mcli/workflow/politician_trading/commands.py +576 -427
- mcli/workflow/politician_trading/config.py +7 -7
- mcli/workflow/politician_trading/connectivity.py +35 -33
- mcli/workflow/politician_trading/data_sources.py +72 -71
- mcli/workflow/politician_trading/database.py +18 -16
- mcli/workflow/politician_trading/demo.py +4 -3
- mcli/workflow/politician_trading/models.py +5 -5
- mcli/workflow/politician_trading/monitoring.py +13 -13
- mcli/workflow/politician_trading/scrapers.py +332 -224
- mcli/workflow/politician_trading/scrapers_california.py +116 -94
- mcli/workflow/politician_trading/scrapers_eu.py +70 -71
- mcli/workflow/politician_trading/scrapers_uk.py +118 -90
- mcli/workflow/politician_trading/scrapers_us_states.py +125 -92
- mcli/workflow/politician_trading/workflow.py +98 -71
- {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/METADATA +2 -2
- {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/RECORD +94 -93
- {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
"""Advanced portfolio optimization for stock recommendations"""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
from typing import Dict, List, Tuple, Optional, Union, Any
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
6
5
|
from dataclasses import dataclass, field
|
|
7
6
|
from datetime import datetime, timedelta
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
import logging
|
|
10
7
|
from enum import Enum
|
|
11
|
-
from
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
12
10
|
|
|
13
11
|
# Optimization libraries
|
|
14
12
|
import cvxpy as cp
|
|
15
|
-
from scipy.optimize import minimize
|
|
16
|
-
from scipy.stats import norm
|
|
17
13
|
import matplotlib.pyplot as plt
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
18
16
|
import seaborn as sns
|
|
17
|
+
from scipy.optimize import minimize
|
|
18
|
+
from scipy.stats import norm
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class OptimizationObjective(Enum):
|
|
24
24
|
"""Portfolio optimization objectives"""
|
|
25
|
+
|
|
25
26
|
MEAN_VARIANCE = "mean_variance"
|
|
26
27
|
RISK_PARITY = "risk_parity"
|
|
27
28
|
MINIMUM_VARIANCE = "minimum_variance"
|
|
@@ -35,6 +36,7 @@ class OptimizationObjective(Enum):
|
|
|
35
36
|
@dataclass
|
|
36
37
|
class OptimizationConstraints:
|
|
37
38
|
"""Portfolio optimization constraints"""
|
|
39
|
+
|
|
38
40
|
# Weight constraints
|
|
39
41
|
min_weight: float = 0.0
|
|
40
42
|
max_weight: float = 1.0
|
|
@@ -65,6 +67,7 @@ class OptimizationConstraints:
|
|
|
65
67
|
@dataclass
|
|
66
68
|
class PortfolioAllocation:
|
|
67
69
|
"""Portfolio allocation result"""
|
|
70
|
+
|
|
68
71
|
weights: Dict[str, float]
|
|
69
72
|
expected_return: float
|
|
70
73
|
expected_volatility: float
|
|
@@ -96,29 +99,39 @@ class BaseOptimizer(ABC):
|
|
|
96
99
|
self.constraints = constraints
|
|
97
100
|
|
|
98
101
|
@abstractmethod
|
|
99
|
-
def optimize(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
def optimize(
|
|
103
|
+
self, expected_returns: pd.Series, covariance_matrix: pd.DataFrame, **kwargs
|
|
104
|
+
) -> PortfolioAllocation:
|
|
102
105
|
"""Optimize portfolio allocation"""
|
|
103
106
|
pass
|
|
104
107
|
|
|
105
|
-
def _calculate_portfolio_metrics(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
def _calculate_portfolio_metrics(
|
|
109
|
+
self,
|
|
110
|
+
weights: np.ndarray,
|
|
111
|
+
expected_returns: pd.Series,
|
|
112
|
+
covariance_matrix: pd.DataFrame,
|
|
113
|
+
risk_free_rate: float = 0.02,
|
|
114
|
+
) -> Tuple[float, float, float]:
|
|
109
115
|
"""Calculate portfolio return, volatility, and Sharpe ratio"""
|
|
110
116
|
portfolio_return = np.dot(weights, expected_returns)
|
|
111
117
|
portfolio_variance = np.dot(weights.T, np.dot(covariance_matrix, weights))
|
|
112
118
|
portfolio_volatility = np.sqrt(portfolio_variance)
|
|
113
119
|
|
|
114
|
-
sharpe_ratio = (
|
|
120
|
+
sharpe_ratio = (
|
|
121
|
+
(portfolio_return - risk_free_rate) / portfolio_volatility
|
|
122
|
+
if portfolio_volatility > 0
|
|
123
|
+
else 0
|
|
124
|
+
)
|
|
115
125
|
|
|
116
126
|
return portfolio_return, portfolio_volatility, sharpe_ratio
|
|
117
127
|
|
|
118
|
-
def _calculate_var_cvar(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
def _calculate_var_cvar(
|
|
129
|
+
self,
|
|
130
|
+
weights: np.ndarray,
|
|
131
|
+
expected_returns: pd.Series,
|
|
132
|
+
covariance_matrix: pd.DataFrame,
|
|
133
|
+
confidence_level: float = 0.95,
|
|
134
|
+
) -> Tuple[float, float]:
|
|
122
135
|
"""Calculate Value at Risk and Conditional Value at Risk"""
|
|
123
136
|
portfolio_return = np.dot(weights, expected_returns)
|
|
124
137
|
portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(covariance_matrix, weights)))
|
|
@@ -136,10 +149,13 @@ class BaseOptimizer(ABC):
|
|
|
136
149
|
class MeanVarianceOptimizer(BaseOptimizer):
|
|
137
150
|
"""Modern Portfolio Theory mean-variance optimizer"""
|
|
138
151
|
|
|
139
|
-
def optimize(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
def optimize(
|
|
153
|
+
self,
|
|
154
|
+
expected_returns: pd.Series,
|
|
155
|
+
covariance_matrix: pd.DataFrame,
|
|
156
|
+
risk_aversion: float = 1.0,
|
|
157
|
+
**kwargs,
|
|
158
|
+
) -> PortfolioAllocation:
|
|
143
159
|
"""Optimize using mean-variance framework"""
|
|
144
160
|
n_assets = len(expected_returns)
|
|
145
161
|
|
|
@@ -155,7 +171,7 @@ class MeanVarianceOptimizer(BaseOptimizer):
|
|
|
155
171
|
constraints = [
|
|
156
172
|
cp.sum(w) == self.constraints.sum_weights, # Weights sum to 1
|
|
157
173
|
w >= self.constraints.min_weight, # Min weight
|
|
158
|
-
w <= self.constraints.max_weight
|
|
174
|
+
w <= self.constraints.max_weight, # Max weight
|
|
159
175
|
]
|
|
160
176
|
|
|
161
177
|
# Additional constraints
|
|
@@ -190,7 +206,7 @@ class MeanVarianceOptimizer(BaseOptimizer):
|
|
|
190
206
|
var_95=var_95,
|
|
191
207
|
cvar_95=cvar_95,
|
|
192
208
|
concentration=np.sum(np.square(optimal_weights)), # Herfindahl index
|
|
193
|
-
optimization_method="mean_variance"
|
|
209
|
+
optimization_method="mean_variance",
|
|
194
210
|
)
|
|
195
211
|
else:
|
|
196
212
|
raise ValueError(f"Optimization failed with status: {problem.status}")
|
|
@@ -199,9 +215,9 @@ class MeanVarianceOptimizer(BaseOptimizer):
|
|
|
199
215
|
class RiskParityOptimizer(BaseOptimizer):
|
|
200
216
|
"""Risk parity portfolio optimizer"""
|
|
201
217
|
|
|
202
|
-
def optimize(
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
def optimize(
|
|
219
|
+
self, expected_returns: pd.Series, covariance_matrix: pd.DataFrame, **kwargs
|
|
220
|
+
) -> PortfolioAllocation:
|
|
205
221
|
"""Optimize using risk parity approach"""
|
|
206
222
|
|
|
207
223
|
def risk_parity_objective(weights, cov_matrix):
|
|
@@ -218,13 +234,12 @@ class RiskParityOptimizer(BaseOptimizer):
|
|
|
218
234
|
initial_weights = np.ones(n_assets) / n_assets
|
|
219
235
|
|
|
220
236
|
# Constraints
|
|
221
|
-
constraints = [
|
|
222
|
-
{'type': 'eq', 'fun': lambda x: np.sum(x) - 1.0} # Weights sum to 1
|
|
223
|
-
]
|
|
237
|
+
constraints = [{"type": "eq", "fun": lambda x: np.sum(x) - 1.0}] # Weights sum to 1
|
|
224
238
|
|
|
225
239
|
# Bounds
|
|
226
|
-
bounds = [
|
|
227
|
-
|
|
240
|
+
bounds = [
|
|
241
|
+
(self.constraints.min_weight, self.constraints.max_weight) for _ in range(n_assets)
|
|
242
|
+
]
|
|
228
243
|
|
|
229
244
|
if not self.constraints.allow_short:
|
|
230
245
|
bounds = [(0, self.constraints.max_weight) for _ in range(n_assets)]
|
|
@@ -234,9 +249,9 @@ class RiskParityOptimizer(BaseOptimizer):
|
|
|
234
249
|
risk_parity_objective,
|
|
235
250
|
initial_weights,
|
|
236
251
|
args=(covariance_matrix.values,),
|
|
237
|
-
method=
|
|
252
|
+
method="SLSQP",
|
|
238
253
|
bounds=bounds,
|
|
239
|
-
constraints=constraints
|
|
254
|
+
constraints=constraints,
|
|
240
255
|
)
|
|
241
256
|
|
|
242
257
|
if result.success:
|
|
@@ -260,7 +275,7 @@ class RiskParityOptimizer(BaseOptimizer):
|
|
|
260
275
|
var_95=var_95,
|
|
261
276
|
cvar_95=cvar_95,
|
|
262
277
|
concentration=np.sum(np.square(optimal_weights)),
|
|
263
|
-
optimization_method="risk_parity"
|
|
278
|
+
optimization_method="risk_parity",
|
|
264
279
|
)
|
|
265
280
|
else:
|
|
266
281
|
raise ValueError(f"Risk parity optimization failed: {result.message}")
|
|
@@ -269,14 +284,17 @@ class RiskParityOptimizer(BaseOptimizer):
|
|
|
269
284
|
class BlackLittermanOptimizer(BaseOptimizer):
|
|
270
285
|
"""Black-Litterman portfolio optimizer"""
|
|
271
286
|
|
|
272
|
-
def optimize(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
287
|
+
def optimize(
|
|
288
|
+
self,
|
|
289
|
+
expected_returns: pd.Series,
|
|
290
|
+
covariance_matrix: pd.DataFrame,
|
|
291
|
+
market_caps: Optional[pd.Series] = None,
|
|
292
|
+
views: Optional[Dict[str, float]] = None,
|
|
293
|
+
view_uncertainties: Optional[Dict[str, float]] = None,
|
|
294
|
+
tau: float = 0.1,
|
|
295
|
+
risk_aversion: float = 3.0,
|
|
296
|
+
**kwargs,
|
|
297
|
+
) -> PortfolioAllocation:
|
|
280
298
|
"""Optimize using Black-Litterman model"""
|
|
281
299
|
|
|
282
300
|
# Market capitalization weights (if not provided, use equal weights)
|
|
@@ -323,7 +341,9 @@ class BlackLittermanOptimizer(BaseOptimizer):
|
|
|
323
341
|
|
|
324
342
|
# New covariance matrix
|
|
325
343
|
bl_cov = np.linalg.inv(M1 + M2)
|
|
326
|
-
bl_cov = pd.DataFrame(
|
|
344
|
+
bl_cov = pd.DataFrame(
|
|
345
|
+
bl_cov, index=covariance_matrix.index, columns=covariance_matrix.columns
|
|
346
|
+
)
|
|
327
347
|
|
|
328
348
|
# Now optimize using mean-variance with BL inputs
|
|
329
349
|
mv_optimizer = MeanVarianceOptimizer(self.constraints)
|
|
@@ -336,11 +356,14 @@ class BlackLittermanOptimizer(BaseOptimizer):
|
|
|
336
356
|
class CVaROptimizer(BaseOptimizer):
|
|
337
357
|
"""Conditional Value at Risk optimizer"""
|
|
338
358
|
|
|
339
|
-
def optimize(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
359
|
+
def optimize(
|
|
360
|
+
self,
|
|
361
|
+
expected_returns: pd.Series,
|
|
362
|
+
covariance_matrix: pd.DataFrame,
|
|
363
|
+
scenarios: Optional[pd.DataFrame] = None,
|
|
364
|
+
confidence_level: float = 0.95,
|
|
365
|
+
**kwargs,
|
|
366
|
+
) -> PortfolioAllocation:
|
|
344
367
|
"""Optimize portfolio to minimize CVaR"""
|
|
345
368
|
|
|
346
369
|
if scenarios is None:
|
|
@@ -373,7 +396,7 @@ class CVaROptimizer(BaseOptimizer):
|
|
|
373
396
|
w >= self.constraints.min_weight,
|
|
374
397
|
w <= self.constraints.max_weight,
|
|
375
398
|
u >= 0,
|
|
376
|
-
u >= alpha - portfolio_returns # CVaR constraints
|
|
399
|
+
u >= alpha - portfolio_returns, # CVaR constraints
|
|
377
400
|
]
|
|
378
401
|
|
|
379
402
|
if not self.constraints.allow_short:
|
|
@@ -404,7 +427,7 @@ class CVaROptimizer(BaseOptimizer):
|
|
|
404
427
|
var_95=var_95,
|
|
405
428
|
cvar_95=cvar_95,
|
|
406
429
|
concentration=np.sum(np.square(optimal_weights)),
|
|
407
|
-
optimization_method="cvar"
|
|
430
|
+
optimization_method="cvar",
|
|
408
431
|
)
|
|
409
432
|
else:
|
|
410
433
|
raise ValueError(f"CVaR optimization failed with status: {problem.status}")
|
|
@@ -413,9 +436,9 @@ class CVaROptimizer(BaseOptimizer):
|
|
|
413
436
|
class KellyCriterionOptimizer(BaseOptimizer):
|
|
414
437
|
"""Kelly Criterion optimizer for growth-optimal portfolios"""
|
|
415
438
|
|
|
416
|
-
def optimize(
|
|
417
|
-
|
|
418
|
-
|
|
439
|
+
def optimize(
|
|
440
|
+
self, expected_returns: pd.Series, covariance_matrix: pd.DataFrame, **kwargs
|
|
441
|
+
) -> PortfolioAllocation:
|
|
419
442
|
"""Optimize using Kelly Criterion"""
|
|
420
443
|
|
|
421
444
|
# Kelly optimal weights: w* = Σ^(-1) * μ
|
|
@@ -429,9 +452,9 @@ class KellyCriterionOptimizer(BaseOptimizer):
|
|
|
429
452
|
if not self.constraints.allow_short:
|
|
430
453
|
kelly_weights = np.maximum(kelly_weights, 0)
|
|
431
454
|
|
|
432
|
-
kelly_weights = np.clip(
|
|
433
|
-
|
|
434
|
-
|
|
455
|
+
kelly_weights = np.clip(
|
|
456
|
+
kelly_weights, self.constraints.min_weight, self.constraints.max_weight
|
|
457
|
+
)
|
|
435
458
|
|
|
436
459
|
# Normalize to sum to 1
|
|
437
460
|
if np.sum(kelly_weights) > 0:
|
|
@@ -459,7 +482,7 @@ class KellyCriterionOptimizer(BaseOptimizer):
|
|
|
459
482
|
var_95=var_95,
|
|
460
483
|
cvar_95=cvar_95,
|
|
461
484
|
concentration=np.sum(np.square(kelly_weights)),
|
|
462
|
-
optimization_method="kelly_criterion"
|
|
485
|
+
optimization_method="kelly_criterion",
|
|
463
486
|
)
|
|
464
487
|
|
|
465
488
|
except np.linalg.LinAlgError:
|
|
@@ -482,7 +505,7 @@ class KellyCriterionOptimizer(BaseOptimizer):
|
|
|
482
505
|
expected_return=port_return,
|
|
483
506
|
expected_volatility=port_vol,
|
|
484
507
|
sharpe_ratio=sharpe,
|
|
485
|
-
optimization_method="kelly_criterion_regularized"
|
|
508
|
+
optimization_method="kelly_criterion_regularized",
|
|
486
509
|
)
|
|
487
510
|
|
|
488
511
|
|
|
@@ -498,16 +521,18 @@ class AdvancedPortfolioOptimizer:
|
|
|
498
521
|
OptimizationObjective.RISK_PARITY: RiskParityOptimizer(self.constraints),
|
|
499
522
|
OptimizationObjective.BLACK_LITTERMAN: BlackLittermanOptimizer(self.constraints),
|
|
500
523
|
OptimizationObjective.CVaR: CVaROptimizer(self.constraints),
|
|
501
|
-
OptimizationObjective.KELLY_CRITERION: KellyCriterionOptimizer(self.constraints)
|
|
524
|
+
OptimizationObjective.KELLY_CRITERION: KellyCriterionOptimizer(self.constraints),
|
|
502
525
|
}
|
|
503
526
|
|
|
504
527
|
self.optimization_history = []
|
|
505
528
|
|
|
506
|
-
def optimize_portfolio(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
529
|
+
def optimize_portfolio(
|
|
530
|
+
self,
|
|
531
|
+
expected_returns: pd.Series,
|
|
532
|
+
covariance_matrix: pd.DataFrame,
|
|
533
|
+
objective: OptimizationObjective = OptimizationObjective.MEAN_VARIANCE,
|
|
534
|
+
**optimizer_kwargs,
|
|
535
|
+
) -> PortfolioAllocation:
|
|
511
536
|
"""Optimize portfolio using specified objective"""
|
|
512
537
|
|
|
513
538
|
if objective not in self.optimizers:
|
|
@@ -517,18 +542,22 @@ class AdvancedPortfolioOptimizer:
|
|
|
517
542
|
allocation = optimizer.optimize(expected_returns, covariance_matrix, **optimizer_kwargs)
|
|
518
543
|
|
|
519
544
|
# Add additional metrics
|
|
520
|
-
allocation = self._enhance_allocation_metrics(
|
|
545
|
+
allocation = self._enhance_allocation_metrics(
|
|
546
|
+
allocation, expected_returns, covariance_matrix
|
|
547
|
+
)
|
|
521
548
|
|
|
522
549
|
# Store in history
|
|
523
550
|
self.optimization_history.append(allocation)
|
|
524
551
|
|
|
525
552
|
return allocation
|
|
526
553
|
|
|
527
|
-
def multi_objective_optimization(
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
554
|
+
def multi_objective_optimization(
|
|
555
|
+
self,
|
|
556
|
+
expected_returns: pd.Series,
|
|
557
|
+
covariance_matrix: pd.DataFrame,
|
|
558
|
+
objectives: List[OptimizationObjective],
|
|
559
|
+
weights: Optional[List[float]] = None,
|
|
560
|
+
) -> PortfolioAllocation:
|
|
532
561
|
"""Combine multiple optimization objectives"""
|
|
533
562
|
|
|
534
563
|
if weights is None:
|
|
@@ -573,19 +602,19 @@ class AdvancedPortfolioOptimizer:
|
|
|
573
602
|
var_95=var_95,
|
|
574
603
|
cvar_95=cvar_95,
|
|
575
604
|
concentration=np.sum(np.square(weights_array)),
|
|
576
|
-
optimization_method=f"multi_objective_{'+'.join([obj.value for obj in objectives])}"
|
|
605
|
+
optimization_method=f"multi_objective_{'+'.join([obj.value for obj in objectives])}",
|
|
577
606
|
)
|
|
578
607
|
|
|
579
|
-
def efficient_frontier(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
n_points: int = 20) -> pd.DataFrame:
|
|
608
|
+
def efficient_frontier(
|
|
609
|
+
self, expected_returns: pd.Series, covariance_matrix: pd.DataFrame, n_points: int = 20
|
|
610
|
+
) -> pd.DataFrame:
|
|
583
611
|
"""Generate efficient frontier"""
|
|
584
612
|
|
|
585
613
|
min_vol_allocation = self.optimize_portfolio(
|
|
586
|
-
expected_returns,
|
|
614
|
+
expected_returns,
|
|
615
|
+
covariance_matrix,
|
|
587
616
|
OptimizationObjective.MEAN_VARIANCE,
|
|
588
|
-
risk_aversion=1000 # High risk aversion for min vol
|
|
617
|
+
risk_aversion=1000, # High risk aversion for min vol
|
|
589
618
|
)
|
|
590
619
|
|
|
591
620
|
max_return = expected_returns.max()
|
|
@@ -609,7 +638,7 @@ class AdvancedPortfolioOptimizer:
|
|
|
609
638
|
cp.sum(w) == 1,
|
|
610
639
|
portfolio_return >= target_return,
|
|
611
640
|
w >= self.constraints.min_weight,
|
|
612
|
-
w <= self.constraints.max_weight
|
|
641
|
+
w <= self.constraints.max_weight,
|
|
613
642
|
]
|
|
614
643
|
|
|
615
644
|
if not self.constraints.allow_short:
|
|
@@ -621,25 +650,32 @@ class AdvancedPortfolioOptimizer:
|
|
|
621
650
|
if problem.status not in ["infeasible", "unbounded"]:
|
|
622
651
|
optimal_weights = w.value
|
|
623
652
|
port_return = np.dot(optimal_weights, expected_returns)
|
|
624
|
-
port_vol = np.sqrt(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
653
|
+
port_vol = np.sqrt(
|
|
654
|
+
np.dot(optimal_weights.T, np.dot(covariance_matrix.values, optimal_weights))
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
frontier_data.append(
|
|
658
|
+
{
|
|
659
|
+
"return": port_return,
|
|
660
|
+
"volatility": port_vol,
|
|
661
|
+
"sharpe": (port_return - 0.02) / port_vol if port_vol > 0 else 0,
|
|
662
|
+
}
|
|
663
|
+
)
|
|
632
664
|
|
|
633
665
|
except Exception as e:
|
|
634
|
-
logger.warning(
|
|
666
|
+
logger.warning(
|
|
667
|
+
f"Failed to compute efficient frontier point for return {target_return}: {e}"
|
|
668
|
+
)
|
|
635
669
|
continue
|
|
636
670
|
|
|
637
671
|
return pd.DataFrame(frontier_data)
|
|
638
672
|
|
|
639
|
-
def rebalance_portfolio(
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
673
|
+
def rebalance_portfolio(
|
|
674
|
+
self,
|
|
675
|
+
current_weights: Dict[str, float],
|
|
676
|
+
target_allocation: PortfolioAllocation,
|
|
677
|
+
rebalance_threshold: float = 0.05,
|
|
678
|
+
) -> Dict[str, Any]:
|
|
643
679
|
"""Calculate rebalancing trades"""
|
|
644
680
|
|
|
645
681
|
trades = {}
|
|
@@ -655,24 +691,29 @@ class AdvancedPortfolioOptimizer:
|
|
|
655
691
|
if deviation > rebalance_threshold:
|
|
656
692
|
trades[asset] = target_weight - current_weight
|
|
657
693
|
|
|
658
|
-
transaction_cost = sum(
|
|
659
|
-
|
|
694
|
+
transaction_cost = sum(
|
|
695
|
+
abs(trade) * self.constraints.transaction_costs for trade in trades.values()
|
|
696
|
+
)
|
|
660
697
|
|
|
661
698
|
return {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
699
|
+
"trades": trades,
|
|
700
|
+
"total_deviation": total_deviation,
|
|
701
|
+
"transaction_cost": transaction_cost,
|
|
702
|
+
"rebalance_needed": total_deviation > rebalance_threshold,
|
|
703
|
+
"net_trades": sum(trades.values()), # Should be close to 0
|
|
667
704
|
}
|
|
668
705
|
|
|
669
|
-
def _enhance_allocation_metrics(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
706
|
+
def _enhance_allocation_metrics(
|
|
707
|
+
self,
|
|
708
|
+
allocation: PortfolioAllocation,
|
|
709
|
+
expected_returns: pd.Series,
|
|
710
|
+
covariance_matrix: pd.DataFrame,
|
|
711
|
+
) -> PortfolioAllocation:
|
|
673
712
|
"""Add additional metrics to allocation"""
|
|
674
713
|
|
|
675
|
-
weights_array = np.array(
|
|
714
|
+
weights_array = np.array(
|
|
715
|
+
[allocation.weights.get(asset, 0) for asset in expected_returns.index]
|
|
716
|
+
)
|
|
676
717
|
|
|
677
718
|
# Calculate max drawdown (simplified)
|
|
678
719
|
returns_series = expected_returns.values
|
|
@@ -683,8 +724,9 @@ class AdvancedPortfolioOptimizer:
|
|
|
683
724
|
|
|
684
725
|
return allocation
|
|
685
726
|
|
|
686
|
-
def plot_allocation(
|
|
687
|
-
|
|
727
|
+
def plot_allocation(
|
|
728
|
+
self, allocation: PortfolioAllocation, save_path: Optional[Path] = None
|
|
729
|
+
) -> None:
|
|
688
730
|
"""Plot portfolio allocation"""
|
|
689
731
|
|
|
690
732
|
# Filter out zero weights
|
|
@@ -701,35 +743,35 @@ class AdvancedPortfolioOptimizer:
|
|
|
701
743
|
assets = list(non_zero_weights.keys())
|
|
702
744
|
weights = list(non_zero_weights.values())
|
|
703
745
|
|
|
704
|
-
plt.pie(weights, labels=assets, autopct=
|
|
705
|
-
plt.title(
|
|
746
|
+
plt.pie(weights, labels=assets, autopct="%1.1f%%", startangle=90)
|
|
747
|
+
plt.title("Portfolio Allocation")
|
|
706
748
|
|
|
707
749
|
# Bar chart of weights
|
|
708
750
|
plt.subplot(2, 2, 2)
|
|
709
751
|
plt.bar(range(len(assets)), weights)
|
|
710
752
|
plt.xticks(range(len(assets)), assets, rotation=45)
|
|
711
|
-
plt.ylabel(
|
|
712
|
-
plt.title(
|
|
753
|
+
plt.ylabel("Weight")
|
|
754
|
+
plt.title("Asset Weights")
|
|
713
755
|
|
|
714
756
|
# Risk metrics
|
|
715
757
|
plt.subplot(2, 2, 3)
|
|
716
|
-
metrics = [
|
|
758
|
+
metrics = ["Expected Return", "Volatility", "Sharpe Ratio", "VaR 95%", "CVaR 95%"]
|
|
717
759
|
values = [
|
|
718
760
|
allocation.expected_return * 100,
|
|
719
761
|
allocation.expected_volatility * 100,
|
|
720
762
|
allocation.sharpe_ratio,
|
|
721
763
|
allocation.var_95 * 100 if allocation.var_95 else 0,
|
|
722
|
-
allocation.cvar_95 * 100 if allocation.cvar_95 else 0
|
|
764
|
+
allocation.cvar_95 * 100 if allocation.cvar_95 else 0,
|
|
723
765
|
]
|
|
724
766
|
|
|
725
767
|
plt.bar(metrics, values)
|
|
726
768
|
plt.xticks(rotation=45)
|
|
727
|
-
plt.ylabel(
|
|
728
|
-
plt.title(
|
|
769
|
+
plt.ylabel("Value (%)")
|
|
770
|
+
plt.title("Portfolio Metrics")
|
|
729
771
|
|
|
730
772
|
# Summary text
|
|
731
773
|
plt.subplot(2, 2, 4)
|
|
732
|
-
plt.axis(
|
|
774
|
+
plt.axis("off")
|
|
733
775
|
summary_text = f"""
|
|
734
776
|
Optimization Method: {allocation.optimization_method}
|
|
735
777
|
Expected Return: {allocation.expected_return:.3f}
|
|
@@ -738,12 +780,12 @@ class AdvancedPortfolioOptimizer:
|
|
|
738
780
|
Concentration: {allocation.concentration:.3f}
|
|
739
781
|
Number of Assets: {len(non_zero_weights)}
|
|
740
782
|
"""
|
|
741
|
-
plt.text(0.1, 0.5, summary_text, fontsize=10, verticalalignment=
|
|
783
|
+
plt.text(0.1, 0.5, summary_text, fontsize=10, verticalalignment="center")
|
|
742
784
|
|
|
743
785
|
plt.tight_layout()
|
|
744
786
|
|
|
745
787
|
if save_path:
|
|
746
|
-
plt.savefig(save_path, dpi=300, bbox_inches=
|
|
788
|
+
plt.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
747
789
|
|
|
748
790
|
plt.show()
|
|
749
791
|
|
|
@@ -753,14 +795,11 @@ if __name__ == "__main__":
|
|
|
753
795
|
# Generate sample data
|
|
754
796
|
np.random.seed(42)
|
|
755
797
|
|
|
756
|
-
assets = [
|
|
798
|
+
assets = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"]
|
|
757
799
|
n_assets = len(assets)
|
|
758
800
|
|
|
759
801
|
# Expected returns (annual)
|
|
760
|
-
expected_returns = pd.Series(
|
|
761
|
-
np.random.uniform(0.05, 0.15, n_assets),
|
|
762
|
-
index=assets
|
|
763
|
-
)
|
|
802
|
+
expected_returns = pd.Series(np.random.uniform(0.05, 0.15, n_assets), index=assets)
|
|
764
803
|
|
|
765
804
|
# Generate covariance matrix
|
|
766
805
|
correlation_matrix = np.random.uniform(0.1, 0.7, (n_assets, n_assets))
|
|
@@ -773,9 +812,7 @@ if __name__ == "__main__":
|
|
|
773
812
|
|
|
774
813
|
# Initialize optimizer
|
|
775
814
|
constraints = OptimizationConstraints(
|
|
776
|
-
max_weight=0.4,
|
|
777
|
-
transaction_costs=0.001,
|
|
778
|
-
allow_short=False
|
|
815
|
+
max_weight=0.4, transaction_costs=0.001, allow_short=False
|
|
779
816
|
)
|
|
780
817
|
|
|
781
818
|
optimizer = AdvancedPortfolioOptimizer(constraints)
|
|
@@ -784,7 +821,7 @@ if __name__ == "__main__":
|
|
|
784
821
|
objectives = [
|
|
785
822
|
OptimizationObjective.MEAN_VARIANCE,
|
|
786
823
|
OptimizationObjective.RISK_PARITY,
|
|
787
|
-
OptimizationObjective.KELLY_CRITERION
|
|
824
|
+
OptimizationObjective.KELLY_CRITERION,
|
|
788
825
|
]
|
|
789
826
|
|
|
790
827
|
results = {}
|
|
@@ -799,7 +836,9 @@ if __name__ == "__main__":
|
|
|
799
836
|
print(f"Volatility: {allocation.expected_volatility:.3f}")
|
|
800
837
|
print(f"Sharpe Ratio: {allocation.sharpe_ratio:.3f}")
|
|
801
838
|
print("Top 3 Holdings:")
|
|
802
|
-
sorted_weights = sorted(
|
|
839
|
+
sorted_weights = sorted(
|
|
840
|
+
allocation.weights.items(), key=lambda x: abs(x[1]), reverse=True
|
|
841
|
+
)
|
|
803
842
|
for asset, weight in sorted_weights[:3]:
|
|
804
843
|
print(f" {asset}: {weight:.3f}")
|
|
805
844
|
|
|
@@ -809,9 +848,10 @@ if __name__ == "__main__":
|
|
|
809
848
|
# Test multi-objective optimization
|
|
810
849
|
try:
|
|
811
850
|
multi_obj_allocation = optimizer.multi_objective_optimization(
|
|
812
|
-
expected_returns,
|
|
851
|
+
expected_returns,
|
|
852
|
+
covariance_matrix,
|
|
813
853
|
objectives=[OptimizationObjective.MEAN_VARIANCE, OptimizationObjective.RISK_PARITY],
|
|
814
|
-
weights=[0.7, 0.3]
|
|
854
|
+
weights=[0.7, 0.3],
|
|
815
855
|
)
|
|
816
856
|
|
|
817
857
|
print(f"\nMULTI-OBJECTIVE Optimization:")
|
|
@@ -831,4 +871,4 @@ if __name__ == "__main__":
|
|
|
831
871
|
except Exception as e:
|
|
832
872
|
logger.error(f"Efficient frontier generation failed: {e}")
|
|
833
873
|
|
|
834
|
-
logger.info("Advanced portfolio optimization demo completed")
|
|
874
|
+
logger.info("Advanced portfolio optimization demo completed")
|