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,834 @@
|
|
|
1
|
+
"""Advanced portfolio optimization for stock recommendations"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from typing import Dict, List, Tuple, Optional, Union, Any
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import logging
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
|
|
13
|
+
# Optimization libraries
|
|
14
|
+
import cvxpy as cp
|
|
15
|
+
from scipy.optimize import minimize
|
|
16
|
+
from scipy.stats import norm
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
import seaborn as sns
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OptimizationObjective(Enum):
|
|
24
|
+
"""Portfolio optimization objectives"""
|
|
25
|
+
MEAN_VARIANCE = "mean_variance"
|
|
26
|
+
RISK_PARITY = "risk_parity"
|
|
27
|
+
MINIMUM_VARIANCE = "minimum_variance"
|
|
28
|
+
MAXIMUM_SHARPE = "maximum_sharpe"
|
|
29
|
+
BLACK_LITTERMAN = "black_litterman"
|
|
30
|
+
FACTOR_MODEL = "factor_model"
|
|
31
|
+
CVaR = "conditional_value_at_risk"
|
|
32
|
+
KELLY_CRITERION = "kelly_criterion"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class OptimizationConstraints:
|
|
37
|
+
"""Portfolio optimization constraints"""
|
|
38
|
+
# Weight constraints
|
|
39
|
+
min_weight: float = 0.0
|
|
40
|
+
max_weight: float = 1.0
|
|
41
|
+
sum_weights: float = 1.0
|
|
42
|
+
|
|
43
|
+
# Sector/factor constraints
|
|
44
|
+
max_sector_weight: Optional[Dict[str, float]] = None
|
|
45
|
+
max_factor_exposure: Optional[Dict[str, float]] = None
|
|
46
|
+
|
|
47
|
+
# Risk constraints
|
|
48
|
+
max_volatility: Optional[float] = None
|
|
49
|
+
max_var: Optional[float] = None
|
|
50
|
+
max_cvar: Optional[float] = None
|
|
51
|
+
|
|
52
|
+
# Transaction costs
|
|
53
|
+
transaction_costs: float = 0.001
|
|
54
|
+
max_turnover: Optional[float] = None
|
|
55
|
+
|
|
56
|
+
# Long/short constraints
|
|
57
|
+
allow_short: bool = False
|
|
58
|
+
gross_leverage: Optional[float] = None
|
|
59
|
+
|
|
60
|
+
# Cardinality constraints
|
|
61
|
+
min_assets: Optional[int] = None
|
|
62
|
+
max_assets: Optional[int] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class PortfolioAllocation:
|
|
67
|
+
"""Portfolio allocation result"""
|
|
68
|
+
weights: Dict[str, float]
|
|
69
|
+
expected_return: float
|
|
70
|
+
expected_volatility: float
|
|
71
|
+
sharpe_ratio: float
|
|
72
|
+
|
|
73
|
+
# Risk metrics
|
|
74
|
+
var_95: Optional[float] = None
|
|
75
|
+
cvar_95: Optional[float] = None
|
|
76
|
+
max_drawdown: Optional[float] = None
|
|
77
|
+
|
|
78
|
+
# Portfolio characteristics
|
|
79
|
+
concentration: float = 0.0
|
|
80
|
+
turnover: float = 0.0
|
|
81
|
+
transaction_cost: float = 0.0
|
|
82
|
+
|
|
83
|
+
# Factor exposures
|
|
84
|
+
factor_exposures: Dict[str, float] = field(default_factory=dict)
|
|
85
|
+
sector_allocations: Dict[str, float] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
# Metadata
|
|
88
|
+
optimization_method: str = ""
|
|
89
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BaseOptimizer(ABC):
|
|
93
|
+
"""Base class for portfolio optimizers"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, constraints: OptimizationConstraints):
|
|
96
|
+
self.constraints = constraints
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def optimize(self, expected_returns: pd.Series,
|
|
100
|
+
covariance_matrix: pd.DataFrame,
|
|
101
|
+
**kwargs) -> PortfolioAllocation:
|
|
102
|
+
"""Optimize portfolio allocation"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def _calculate_portfolio_metrics(self, weights: np.ndarray,
|
|
106
|
+
expected_returns: pd.Series,
|
|
107
|
+
covariance_matrix: pd.DataFrame,
|
|
108
|
+
risk_free_rate: float = 0.02) -> Tuple[float, float, float]:
|
|
109
|
+
"""Calculate portfolio return, volatility, and Sharpe ratio"""
|
|
110
|
+
portfolio_return = np.dot(weights, expected_returns)
|
|
111
|
+
portfolio_variance = np.dot(weights.T, np.dot(covariance_matrix, weights))
|
|
112
|
+
portfolio_volatility = np.sqrt(portfolio_variance)
|
|
113
|
+
|
|
114
|
+
sharpe_ratio = (portfolio_return - risk_free_rate) / portfolio_volatility if portfolio_volatility > 0 else 0
|
|
115
|
+
|
|
116
|
+
return portfolio_return, portfolio_volatility, sharpe_ratio
|
|
117
|
+
|
|
118
|
+
def _calculate_var_cvar(self, weights: np.ndarray,
|
|
119
|
+
expected_returns: pd.Series,
|
|
120
|
+
covariance_matrix: pd.DataFrame,
|
|
121
|
+
confidence_level: float = 0.95) -> Tuple[float, float]:
|
|
122
|
+
"""Calculate Value at Risk and Conditional Value at Risk"""
|
|
123
|
+
portfolio_return = np.dot(weights, expected_returns)
|
|
124
|
+
portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(covariance_matrix, weights)))
|
|
125
|
+
|
|
126
|
+
# Assuming normal distribution
|
|
127
|
+
z_score = norm.ppf(1 - confidence_level)
|
|
128
|
+
var = portfolio_return + z_score * portfolio_volatility
|
|
129
|
+
|
|
130
|
+
# CVaR calculation
|
|
131
|
+
cvar = portfolio_return - portfolio_volatility * norm.pdf(z_score) / (1 - confidence_level)
|
|
132
|
+
|
|
133
|
+
return var, cvar
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class MeanVarianceOptimizer(BaseOptimizer):
|
|
137
|
+
"""Modern Portfolio Theory mean-variance optimizer"""
|
|
138
|
+
|
|
139
|
+
def optimize(self, expected_returns: pd.Series,
|
|
140
|
+
covariance_matrix: pd.DataFrame,
|
|
141
|
+
risk_aversion: float = 1.0,
|
|
142
|
+
**kwargs) -> PortfolioAllocation:
|
|
143
|
+
"""Optimize using mean-variance framework"""
|
|
144
|
+
n_assets = len(expected_returns)
|
|
145
|
+
|
|
146
|
+
# Decision variable: portfolio weights
|
|
147
|
+
w = cp.Variable(n_assets)
|
|
148
|
+
|
|
149
|
+
# Objective: maximize utility (return - risk_aversion * variance)
|
|
150
|
+
portfolio_return = expected_returns.values.T @ w
|
|
151
|
+
portfolio_variance = cp.quad_form(w, covariance_matrix.values)
|
|
152
|
+
objective = cp.Maximize(portfolio_return - 0.5 * risk_aversion * portfolio_variance)
|
|
153
|
+
|
|
154
|
+
# Constraints
|
|
155
|
+
constraints = [
|
|
156
|
+
cp.sum(w) == self.constraints.sum_weights, # Weights sum to 1
|
|
157
|
+
w >= self.constraints.min_weight, # Min weight
|
|
158
|
+
w <= self.constraints.max_weight # Max weight
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# Additional constraints
|
|
162
|
+
if not self.constraints.allow_short:
|
|
163
|
+
constraints.append(w >= 0)
|
|
164
|
+
|
|
165
|
+
if self.constraints.max_volatility:
|
|
166
|
+
constraints.append(cp.sqrt(portfolio_variance) <= self.constraints.max_volatility)
|
|
167
|
+
|
|
168
|
+
# Solve optimization problem
|
|
169
|
+
problem = cp.Problem(objective, constraints)
|
|
170
|
+
problem.solve()
|
|
171
|
+
|
|
172
|
+
if problem.status not in ["infeasible", "unbounded"]:
|
|
173
|
+
optimal_weights = w.value
|
|
174
|
+
weights_dict = dict(zip(expected_returns.index, optimal_weights))
|
|
175
|
+
|
|
176
|
+
# Calculate metrics
|
|
177
|
+
port_return, port_vol, sharpe = self._calculate_portfolio_metrics(
|
|
178
|
+
optimal_weights, expected_returns, covariance_matrix
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
var_95, cvar_95 = self._calculate_var_cvar(
|
|
182
|
+
optimal_weights, expected_returns, covariance_matrix
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return PortfolioAllocation(
|
|
186
|
+
weights=weights_dict,
|
|
187
|
+
expected_return=port_return,
|
|
188
|
+
expected_volatility=port_vol,
|
|
189
|
+
sharpe_ratio=sharpe,
|
|
190
|
+
var_95=var_95,
|
|
191
|
+
cvar_95=cvar_95,
|
|
192
|
+
concentration=np.sum(np.square(optimal_weights)), # Herfindahl index
|
|
193
|
+
optimization_method="mean_variance"
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(f"Optimization failed with status: {problem.status}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class RiskParityOptimizer(BaseOptimizer):
|
|
200
|
+
"""Risk parity portfolio optimizer"""
|
|
201
|
+
|
|
202
|
+
def optimize(self, expected_returns: pd.Series,
|
|
203
|
+
covariance_matrix: pd.DataFrame,
|
|
204
|
+
**kwargs) -> PortfolioAllocation:
|
|
205
|
+
"""Optimize using risk parity approach"""
|
|
206
|
+
|
|
207
|
+
def risk_parity_objective(weights, cov_matrix):
|
|
208
|
+
"""Risk parity objective function"""
|
|
209
|
+
portfolio_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
|
|
210
|
+
marginal_contrib = np.dot(cov_matrix, weights) / portfolio_vol
|
|
211
|
+
contrib = weights * marginal_contrib
|
|
212
|
+
target_contrib = np.ones(len(weights)) / len(weights)
|
|
213
|
+
|
|
214
|
+
return np.sum((contrib / np.sum(contrib) - target_contrib) ** 2)
|
|
215
|
+
|
|
216
|
+
# Initial guess: equal weights
|
|
217
|
+
n_assets = len(expected_returns)
|
|
218
|
+
initial_weights = np.ones(n_assets) / n_assets
|
|
219
|
+
|
|
220
|
+
# Constraints
|
|
221
|
+
constraints = [
|
|
222
|
+
{'type': 'eq', 'fun': lambda x: np.sum(x) - 1.0} # Weights sum to 1
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
# Bounds
|
|
226
|
+
bounds = [(self.constraints.min_weight, self.constraints.max_weight)
|
|
227
|
+
for _ in range(n_assets)]
|
|
228
|
+
|
|
229
|
+
if not self.constraints.allow_short:
|
|
230
|
+
bounds = [(0, self.constraints.max_weight) for _ in range(n_assets)]
|
|
231
|
+
|
|
232
|
+
# Optimize
|
|
233
|
+
result = minimize(
|
|
234
|
+
risk_parity_objective,
|
|
235
|
+
initial_weights,
|
|
236
|
+
args=(covariance_matrix.values,),
|
|
237
|
+
method='SLSQP',
|
|
238
|
+
bounds=bounds,
|
|
239
|
+
constraints=constraints
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if result.success:
|
|
243
|
+
optimal_weights = result.x
|
|
244
|
+
weights_dict = dict(zip(expected_returns.index, optimal_weights))
|
|
245
|
+
|
|
246
|
+
# Calculate metrics
|
|
247
|
+
port_return, port_vol, sharpe = self._calculate_portfolio_metrics(
|
|
248
|
+
optimal_weights, expected_returns, covariance_matrix
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
var_95, cvar_95 = self._calculate_var_cvar(
|
|
252
|
+
optimal_weights, expected_returns, covariance_matrix
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return PortfolioAllocation(
|
|
256
|
+
weights=weights_dict,
|
|
257
|
+
expected_return=port_return,
|
|
258
|
+
expected_volatility=port_vol,
|
|
259
|
+
sharpe_ratio=sharpe,
|
|
260
|
+
var_95=var_95,
|
|
261
|
+
cvar_95=cvar_95,
|
|
262
|
+
concentration=np.sum(np.square(optimal_weights)),
|
|
263
|
+
optimization_method="risk_parity"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(f"Risk parity optimization failed: {result.message}")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class BlackLittermanOptimizer(BaseOptimizer):
|
|
270
|
+
"""Black-Litterman portfolio optimizer"""
|
|
271
|
+
|
|
272
|
+
def optimize(self, expected_returns: pd.Series,
|
|
273
|
+
covariance_matrix: pd.DataFrame,
|
|
274
|
+
market_caps: Optional[pd.Series] = None,
|
|
275
|
+
views: Optional[Dict[str, float]] = None,
|
|
276
|
+
view_uncertainties: Optional[Dict[str, float]] = None,
|
|
277
|
+
tau: float = 0.1,
|
|
278
|
+
risk_aversion: float = 3.0,
|
|
279
|
+
**kwargs) -> PortfolioAllocation:
|
|
280
|
+
"""Optimize using Black-Litterman model"""
|
|
281
|
+
|
|
282
|
+
# Market capitalization weights (if not provided, use equal weights)
|
|
283
|
+
if market_caps is None:
|
|
284
|
+
market_weights = np.ones(len(expected_returns)) / len(expected_returns)
|
|
285
|
+
else:
|
|
286
|
+
market_weights = (market_caps / market_caps.sum()).values
|
|
287
|
+
|
|
288
|
+
# Implied equilibrium returns
|
|
289
|
+
implied_returns = risk_aversion * np.dot(covariance_matrix.values, market_weights)
|
|
290
|
+
|
|
291
|
+
# If no views provided, use implied returns
|
|
292
|
+
if views is None or len(views) == 0:
|
|
293
|
+
bl_returns = pd.Series(implied_returns, index=expected_returns.index)
|
|
294
|
+
bl_cov = covariance_matrix
|
|
295
|
+
else:
|
|
296
|
+
# Black-Litterman adjustment with views
|
|
297
|
+
P = np.zeros((len(views), len(expected_returns))) # Picking matrix
|
|
298
|
+
Q = np.zeros(len(views)) # View returns
|
|
299
|
+
Omega = np.zeros((len(views), len(views))) # View uncertainty matrix
|
|
300
|
+
|
|
301
|
+
for i, (asset, view_return) in enumerate(views.items()):
|
|
302
|
+
asset_idx = expected_returns.index.get_loc(asset)
|
|
303
|
+
P[i, asset_idx] = 1.0
|
|
304
|
+
Q[i] = view_return
|
|
305
|
+
|
|
306
|
+
# View uncertainty (if not provided, use default)
|
|
307
|
+
if view_uncertainties and asset in view_uncertainties:
|
|
308
|
+
Omega[i, i] = view_uncertainties[asset]
|
|
309
|
+
else:
|
|
310
|
+
Omega[i, i] = tau * covariance_matrix.iloc[asset_idx, asset_idx]
|
|
311
|
+
|
|
312
|
+
# Black-Litterman formulas
|
|
313
|
+
tau_cov = tau * covariance_matrix.values
|
|
314
|
+
|
|
315
|
+
# New expected returns
|
|
316
|
+
M1 = np.linalg.inv(tau_cov)
|
|
317
|
+
M2 = np.dot(P.T, np.dot(np.linalg.inv(Omega), P))
|
|
318
|
+
M3 = np.dot(np.linalg.inv(tau_cov), implied_returns)
|
|
319
|
+
M4 = np.dot(P.T, np.dot(np.linalg.inv(Omega), Q))
|
|
320
|
+
|
|
321
|
+
bl_returns = np.dot(np.linalg.inv(M1 + M2), M3 + M4)
|
|
322
|
+
bl_returns = pd.Series(bl_returns, index=expected_returns.index)
|
|
323
|
+
|
|
324
|
+
# New covariance matrix
|
|
325
|
+
bl_cov = np.linalg.inv(M1 + M2)
|
|
326
|
+
bl_cov = pd.DataFrame(bl_cov, index=covariance_matrix.index, columns=covariance_matrix.columns)
|
|
327
|
+
|
|
328
|
+
# Now optimize using mean-variance with BL inputs
|
|
329
|
+
mv_optimizer = MeanVarianceOptimizer(self.constraints)
|
|
330
|
+
allocation = mv_optimizer.optimize(bl_returns, bl_cov, risk_aversion)
|
|
331
|
+
allocation.optimization_method = "black_litterman"
|
|
332
|
+
|
|
333
|
+
return allocation
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class CVaROptimizer(BaseOptimizer):
|
|
337
|
+
"""Conditional Value at Risk optimizer"""
|
|
338
|
+
|
|
339
|
+
def optimize(self, expected_returns: pd.Series,
|
|
340
|
+
covariance_matrix: pd.DataFrame,
|
|
341
|
+
scenarios: Optional[pd.DataFrame] = None,
|
|
342
|
+
confidence_level: float = 0.95,
|
|
343
|
+
**kwargs) -> PortfolioAllocation:
|
|
344
|
+
"""Optimize portfolio to minimize CVaR"""
|
|
345
|
+
|
|
346
|
+
if scenarios is None:
|
|
347
|
+
# Generate scenarios from normal distribution
|
|
348
|
+
n_scenarios = 10000
|
|
349
|
+
mean_returns = expected_returns.values
|
|
350
|
+
cov_matrix = covariance_matrix.values
|
|
351
|
+
|
|
352
|
+
scenarios = np.random.multivariate_normal(mean_returns, cov_matrix, n_scenarios)
|
|
353
|
+
scenarios = pd.DataFrame(scenarios, columns=expected_returns.index)
|
|
354
|
+
|
|
355
|
+
n_assets = len(expected_returns)
|
|
356
|
+
n_scenarios = len(scenarios)
|
|
357
|
+
|
|
358
|
+
# Decision variables
|
|
359
|
+
w = cp.Variable(n_assets) # Portfolio weights
|
|
360
|
+
alpha = cp.Variable() # VaR
|
|
361
|
+
u = cp.Variable(n_scenarios, nonneg=True) # Auxiliary variables for CVaR
|
|
362
|
+
|
|
363
|
+
# Portfolio returns for each scenario
|
|
364
|
+
portfolio_returns = scenarios.values @ w
|
|
365
|
+
|
|
366
|
+
# CVaR objective
|
|
367
|
+
cvar = alpha - cp.sum(u) / (n_scenarios * (1 - confidence_level))
|
|
368
|
+
objective = cp.Maximize(cvar)
|
|
369
|
+
|
|
370
|
+
# Constraints
|
|
371
|
+
constraints = [
|
|
372
|
+
cp.sum(w) == 1, # Weights sum to 1
|
|
373
|
+
w >= self.constraints.min_weight,
|
|
374
|
+
w <= self.constraints.max_weight,
|
|
375
|
+
u >= 0,
|
|
376
|
+
u >= alpha - portfolio_returns # CVaR constraints
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
if not self.constraints.allow_short:
|
|
380
|
+
constraints.append(w >= 0)
|
|
381
|
+
|
|
382
|
+
# Solve
|
|
383
|
+
problem = cp.Problem(objective, constraints)
|
|
384
|
+
problem.solve()
|
|
385
|
+
|
|
386
|
+
if problem.status not in ["infeasible", "unbounded"]:
|
|
387
|
+
optimal_weights = w.value
|
|
388
|
+
weights_dict = dict(zip(expected_returns.index, optimal_weights))
|
|
389
|
+
|
|
390
|
+
# Calculate metrics
|
|
391
|
+
port_return, port_vol, sharpe = self._calculate_portfolio_metrics(
|
|
392
|
+
optimal_weights, expected_returns, covariance_matrix
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
var_95, cvar_95 = self._calculate_var_cvar(
|
|
396
|
+
optimal_weights, expected_returns, covariance_matrix
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return PortfolioAllocation(
|
|
400
|
+
weights=weights_dict,
|
|
401
|
+
expected_return=port_return,
|
|
402
|
+
expected_volatility=port_vol,
|
|
403
|
+
sharpe_ratio=sharpe,
|
|
404
|
+
var_95=var_95,
|
|
405
|
+
cvar_95=cvar_95,
|
|
406
|
+
concentration=np.sum(np.square(optimal_weights)),
|
|
407
|
+
optimization_method="cvar"
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
raise ValueError(f"CVaR optimization failed with status: {problem.status}")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class KellyCriterionOptimizer(BaseOptimizer):
|
|
414
|
+
"""Kelly Criterion optimizer for growth-optimal portfolios"""
|
|
415
|
+
|
|
416
|
+
def optimize(self, expected_returns: pd.Series,
|
|
417
|
+
covariance_matrix: pd.DataFrame,
|
|
418
|
+
**kwargs) -> PortfolioAllocation:
|
|
419
|
+
"""Optimize using Kelly Criterion"""
|
|
420
|
+
|
|
421
|
+
# Kelly optimal weights: w* = Σ^(-1) * μ
|
|
422
|
+
# where μ is expected excess returns and Σ is covariance matrix
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
inv_cov = np.linalg.inv(covariance_matrix.values)
|
|
426
|
+
kelly_weights = np.dot(inv_cov, expected_returns.values)
|
|
427
|
+
|
|
428
|
+
# Apply constraints
|
|
429
|
+
if not self.constraints.allow_short:
|
|
430
|
+
kelly_weights = np.maximum(kelly_weights, 0)
|
|
431
|
+
|
|
432
|
+
kelly_weights = np.clip(kelly_weights,
|
|
433
|
+
self.constraints.min_weight,
|
|
434
|
+
self.constraints.max_weight)
|
|
435
|
+
|
|
436
|
+
# Normalize to sum to 1
|
|
437
|
+
if np.sum(kelly_weights) > 0:
|
|
438
|
+
kelly_weights = kelly_weights / np.sum(kelly_weights)
|
|
439
|
+
else:
|
|
440
|
+
# Fall back to equal weights
|
|
441
|
+
kelly_weights = np.ones(len(expected_returns)) / len(expected_returns)
|
|
442
|
+
|
|
443
|
+
weights_dict = dict(zip(expected_returns.index, kelly_weights))
|
|
444
|
+
|
|
445
|
+
# Calculate metrics
|
|
446
|
+
port_return, port_vol, sharpe = self._calculate_portfolio_metrics(
|
|
447
|
+
kelly_weights, expected_returns, covariance_matrix
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
var_95, cvar_95 = self._calculate_var_cvar(
|
|
451
|
+
kelly_weights, expected_returns, covariance_matrix
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return PortfolioAllocation(
|
|
455
|
+
weights=weights_dict,
|
|
456
|
+
expected_return=port_return,
|
|
457
|
+
expected_volatility=port_vol,
|
|
458
|
+
sharpe_ratio=sharpe,
|
|
459
|
+
var_95=var_95,
|
|
460
|
+
cvar_95=cvar_95,
|
|
461
|
+
concentration=np.sum(np.square(kelly_weights)),
|
|
462
|
+
optimization_method="kelly_criterion"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
except np.linalg.LinAlgError:
|
|
466
|
+
# Covariance matrix is singular, use regularization
|
|
467
|
+
regularization = 1e-8 * np.eye(len(expected_returns))
|
|
468
|
+
regularized_cov = covariance_matrix.values + regularization
|
|
469
|
+
|
|
470
|
+
inv_cov = np.linalg.inv(regularized_cov)
|
|
471
|
+
kelly_weights = np.dot(inv_cov, expected_returns.values)
|
|
472
|
+
kelly_weights = kelly_weights / np.sum(np.abs(kelly_weights))
|
|
473
|
+
|
|
474
|
+
weights_dict = dict(zip(expected_returns.index, kelly_weights))
|
|
475
|
+
|
|
476
|
+
port_return, port_vol, sharpe = self._calculate_portfolio_metrics(
|
|
477
|
+
kelly_weights, expected_returns, covariance_matrix
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return PortfolioAllocation(
|
|
481
|
+
weights=weights_dict,
|
|
482
|
+
expected_return=port_return,
|
|
483
|
+
expected_volatility=port_vol,
|
|
484
|
+
sharpe_ratio=sharpe,
|
|
485
|
+
optimization_method="kelly_criterion_regularized"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class AdvancedPortfolioOptimizer:
|
|
490
|
+
"""Advanced portfolio optimization system"""
|
|
491
|
+
|
|
492
|
+
def __init__(self, constraints: Optional[OptimizationConstraints] = None):
|
|
493
|
+
self.constraints = constraints or OptimizationConstraints()
|
|
494
|
+
|
|
495
|
+
# Initialize optimizers
|
|
496
|
+
self.optimizers = {
|
|
497
|
+
OptimizationObjective.MEAN_VARIANCE: MeanVarianceOptimizer(self.constraints),
|
|
498
|
+
OptimizationObjective.RISK_PARITY: RiskParityOptimizer(self.constraints),
|
|
499
|
+
OptimizationObjective.BLACK_LITTERMAN: BlackLittermanOptimizer(self.constraints),
|
|
500
|
+
OptimizationObjective.CVaR: CVaROptimizer(self.constraints),
|
|
501
|
+
OptimizationObjective.KELLY_CRITERION: KellyCriterionOptimizer(self.constraints)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
self.optimization_history = []
|
|
505
|
+
|
|
506
|
+
def optimize_portfolio(self,
|
|
507
|
+
expected_returns: pd.Series,
|
|
508
|
+
covariance_matrix: pd.DataFrame,
|
|
509
|
+
objective: OptimizationObjective = OptimizationObjective.MEAN_VARIANCE,
|
|
510
|
+
**optimizer_kwargs) -> PortfolioAllocation:
|
|
511
|
+
"""Optimize portfolio using specified objective"""
|
|
512
|
+
|
|
513
|
+
if objective not in self.optimizers:
|
|
514
|
+
raise ValueError(f"Unsupported optimization objective: {objective}")
|
|
515
|
+
|
|
516
|
+
optimizer = self.optimizers[objective]
|
|
517
|
+
allocation = optimizer.optimize(expected_returns, covariance_matrix, **optimizer_kwargs)
|
|
518
|
+
|
|
519
|
+
# Add additional metrics
|
|
520
|
+
allocation = self._enhance_allocation_metrics(allocation, expected_returns, covariance_matrix)
|
|
521
|
+
|
|
522
|
+
# Store in history
|
|
523
|
+
self.optimization_history.append(allocation)
|
|
524
|
+
|
|
525
|
+
return allocation
|
|
526
|
+
|
|
527
|
+
def multi_objective_optimization(self,
|
|
528
|
+
expected_returns: pd.Series,
|
|
529
|
+
covariance_matrix: pd.DataFrame,
|
|
530
|
+
objectives: List[OptimizationObjective],
|
|
531
|
+
weights: Optional[List[float]] = None) -> PortfolioAllocation:
|
|
532
|
+
"""Combine multiple optimization objectives"""
|
|
533
|
+
|
|
534
|
+
if weights is None:
|
|
535
|
+
weights = [1.0 / len(objectives)] * len(objectives)
|
|
536
|
+
|
|
537
|
+
if len(weights) != len(objectives):
|
|
538
|
+
raise ValueError("Number of weights must match number of objectives")
|
|
539
|
+
|
|
540
|
+
# Get individual allocations
|
|
541
|
+
allocations = []
|
|
542
|
+
for obj in objectives:
|
|
543
|
+
allocation = self.optimize_portfolio(expected_returns, covariance_matrix, obj)
|
|
544
|
+
allocations.append(allocation)
|
|
545
|
+
|
|
546
|
+
# Combine allocations using weights
|
|
547
|
+
combined_weights = {}
|
|
548
|
+
for asset in expected_returns.index:
|
|
549
|
+
combined_weights[asset] = sum(
|
|
550
|
+
w * alloc.weights.get(asset, 0) for w, alloc in zip(weights, allocations)
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Normalize
|
|
554
|
+
total_weight = sum(combined_weights.values())
|
|
555
|
+
if total_weight > 0:
|
|
556
|
+
combined_weights = {k: v / total_weight for k, v in combined_weights.items()}
|
|
557
|
+
|
|
558
|
+
# Calculate metrics for combined portfolio
|
|
559
|
+
weights_array = np.array([combined_weights[asset] for asset in expected_returns.index])
|
|
560
|
+
port_return, port_vol, sharpe = self.optimizers[objectives[0]]._calculate_portfolio_metrics(
|
|
561
|
+
weights_array, expected_returns, covariance_matrix
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
var_95, cvar_95 = self.optimizers[objectives[0]]._calculate_var_cvar(
|
|
565
|
+
weights_array, expected_returns, covariance_matrix
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return PortfolioAllocation(
|
|
569
|
+
weights=combined_weights,
|
|
570
|
+
expected_return=port_return,
|
|
571
|
+
expected_volatility=port_vol,
|
|
572
|
+
sharpe_ratio=sharpe,
|
|
573
|
+
var_95=var_95,
|
|
574
|
+
cvar_95=cvar_95,
|
|
575
|
+
concentration=np.sum(np.square(weights_array)),
|
|
576
|
+
optimization_method=f"multi_objective_{'+'.join([obj.value for obj in objectives])}"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def efficient_frontier(self,
|
|
580
|
+
expected_returns: pd.Series,
|
|
581
|
+
covariance_matrix: pd.DataFrame,
|
|
582
|
+
n_points: int = 20) -> pd.DataFrame:
|
|
583
|
+
"""Generate efficient frontier"""
|
|
584
|
+
|
|
585
|
+
min_vol_allocation = self.optimize_portfolio(
|
|
586
|
+
expected_returns, covariance_matrix,
|
|
587
|
+
OptimizationObjective.MEAN_VARIANCE,
|
|
588
|
+
risk_aversion=1000 # High risk aversion for min vol
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
max_return = expected_returns.max()
|
|
592
|
+
min_return = min_vol_allocation.expected_return
|
|
593
|
+
|
|
594
|
+
target_returns = np.linspace(min_return, max_return, n_points)
|
|
595
|
+
|
|
596
|
+
frontier_data = []
|
|
597
|
+
|
|
598
|
+
for target_return in target_returns:
|
|
599
|
+
try:
|
|
600
|
+
# Optimize for minimum variance given target return
|
|
601
|
+
n_assets = len(expected_returns)
|
|
602
|
+
w = cp.Variable(n_assets)
|
|
603
|
+
|
|
604
|
+
portfolio_return = expected_returns.values.T @ w
|
|
605
|
+
portfolio_variance = cp.quad_form(w, covariance_matrix.values)
|
|
606
|
+
|
|
607
|
+
objective = cp.Minimize(portfolio_variance)
|
|
608
|
+
constraints = [
|
|
609
|
+
cp.sum(w) == 1,
|
|
610
|
+
portfolio_return >= target_return,
|
|
611
|
+
w >= self.constraints.min_weight,
|
|
612
|
+
w <= self.constraints.max_weight
|
|
613
|
+
]
|
|
614
|
+
|
|
615
|
+
if not self.constraints.allow_short:
|
|
616
|
+
constraints.append(w >= 0)
|
|
617
|
+
|
|
618
|
+
problem = cp.Problem(objective, constraints)
|
|
619
|
+
problem.solve()
|
|
620
|
+
|
|
621
|
+
if problem.status not in ["infeasible", "unbounded"]:
|
|
622
|
+
optimal_weights = w.value
|
|
623
|
+
port_return = np.dot(optimal_weights, expected_returns)
|
|
624
|
+
port_vol = np.sqrt(np.dot(optimal_weights.T,
|
|
625
|
+
np.dot(covariance_matrix.values, optimal_weights)))
|
|
626
|
+
|
|
627
|
+
frontier_data.append({
|
|
628
|
+
'return': port_return,
|
|
629
|
+
'volatility': port_vol,
|
|
630
|
+
'sharpe': (port_return - 0.02) / port_vol if port_vol > 0 else 0
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
except Exception as e:
|
|
634
|
+
logger.warning(f"Failed to compute efficient frontier point for return {target_return}: {e}")
|
|
635
|
+
continue
|
|
636
|
+
|
|
637
|
+
return pd.DataFrame(frontier_data)
|
|
638
|
+
|
|
639
|
+
def rebalance_portfolio(self,
|
|
640
|
+
current_weights: Dict[str, float],
|
|
641
|
+
target_allocation: PortfolioAllocation,
|
|
642
|
+
rebalance_threshold: float = 0.05) -> Dict[str, Any]:
|
|
643
|
+
"""Calculate rebalancing trades"""
|
|
644
|
+
|
|
645
|
+
trades = {}
|
|
646
|
+
total_deviation = 0
|
|
647
|
+
|
|
648
|
+
for asset in target_allocation.weights:
|
|
649
|
+
current_weight = current_weights.get(asset, 0)
|
|
650
|
+
target_weight = target_allocation.weights[asset]
|
|
651
|
+
deviation = abs(target_weight - current_weight)
|
|
652
|
+
|
|
653
|
+
total_deviation += deviation
|
|
654
|
+
|
|
655
|
+
if deviation > rebalance_threshold:
|
|
656
|
+
trades[asset] = target_weight - current_weight
|
|
657
|
+
|
|
658
|
+
transaction_cost = sum(abs(trade) * self.constraints.transaction_costs
|
|
659
|
+
for trade in trades.values())
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
'trades': trades,
|
|
663
|
+
'total_deviation': total_deviation,
|
|
664
|
+
'transaction_cost': transaction_cost,
|
|
665
|
+
'rebalance_needed': total_deviation > rebalance_threshold,
|
|
666
|
+
'net_trades': sum(trades.values()) # Should be close to 0
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
def _enhance_allocation_metrics(self,
|
|
670
|
+
allocation: PortfolioAllocation,
|
|
671
|
+
expected_returns: pd.Series,
|
|
672
|
+
covariance_matrix: pd.DataFrame) -> PortfolioAllocation:
|
|
673
|
+
"""Add additional metrics to allocation"""
|
|
674
|
+
|
|
675
|
+
weights_array = np.array([allocation.weights.get(asset, 0) for asset in expected_returns.index])
|
|
676
|
+
|
|
677
|
+
# Calculate max drawdown (simplified)
|
|
678
|
+
returns_series = expected_returns.values
|
|
679
|
+
cumulative_returns = np.cumprod(1 + returns_series)
|
|
680
|
+
running_max = np.maximum.accumulate(cumulative_returns)
|
|
681
|
+
drawdowns = (cumulative_returns - running_max) / running_max
|
|
682
|
+
allocation.max_drawdown = np.min(drawdowns) if len(drawdowns) > 0 else 0
|
|
683
|
+
|
|
684
|
+
return allocation
|
|
685
|
+
|
|
686
|
+
def plot_allocation(self, allocation: PortfolioAllocation,
|
|
687
|
+
save_path: Optional[Path] = None) -> None:
|
|
688
|
+
"""Plot portfolio allocation"""
|
|
689
|
+
|
|
690
|
+
# Filter out zero weights
|
|
691
|
+
non_zero_weights = {k: v for k, v in allocation.weights.items() if abs(v) > 0.001}
|
|
692
|
+
|
|
693
|
+
if not non_zero_weights:
|
|
694
|
+
logger.warning("No significant weights to plot")
|
|
695
|
+
return
|
|
696
|
+
|
|
697
|
+
plt.figure(figsize=(12, 8))
|
|
698
|
+
|
|
699
|
+
# Pie chart of allocations
|
|
700
|
+
plt.subplot(2, 2, 1)
|
|
701
|
+
assets = list(non_zero_weights.keys())
|
|
702
|
+
weights = list(non_zero_weights.values())
|
|
703
|
+
|
|
704
|
+
plt.pie(weights, labels=assets, autopct='%1.1f%%', startangle=90)
|
|
705
|
+
plt.title('Portfolio Allocation')
|
|
706
|
+
|
|
707
|
+
# Bar chart of weights
|
|
708
|
+
plt.subplot(2, 2, 2)
|
|
709
|
+
plt.bar(range(len(assets)), weights)
|
|
710
|
+
plt.xticks(range(len(assets)), assets, rotation=45)
|
|
711
|
+
plt.ylabel('Weight')
|
|
712
|
+
plt.title('Asset Weights')
|
|
713
|
+
|
|
714
|
+
# Risk metrics
|
|
715
|
+
plt.subplot(2, 2, 3)
|
|
716
|
+
metrics = ['Expected Return', 'Volatility', 'Sharpe Ratio', 'VaR 95%', 'CVaR 95%']
|
|
717
|
+
values = [
|
|
718
|
+
allocation.expected_return * 100,
|
|
719
|
+
allocation.expected_volatility * 100,
|
|
720
|
+
allocation.sharpe_ratio,
|
|
721
|
+
allocation.var_95 * 100 if allocation.var_95 else 0,
|
|
722
|
+
allocation.cvar_95 * 100 if allocation.cvar_95 else 0
|
|
723
|
+
]
|
|
724
|
+
|
|
725
|
+
plt.bar(metrics, values)
|
|
726
|
+
plt.xticks(rotation=45)
|
|
727
|
+
plt.ylabel('Value (%)')
|
|
728
|
+
plt.title('Portfolio Metrics')
|
|
729
|
+
|
|
730
|
+
# Summary text
|
|
731
|
+
plt.subplot(2, 2, 4)
|
|
732
|
+
plt.axis('off')
|
|
733
|
+
summary_text = f"""
|
|
734
|
+
Optimization Method: {allocation.optimization_method}
|
|
735
|
+
Expected Return: {allocation.expected_return:.3f}
|
|
736
|
+
Volatility: {allocation.expected_volatility:.3f}
|
|
737
|
+
Sharpe Ratio: {allocation.sharpe_ratio:.3f}
|
|
738
|
+
Concentration: {allocation.concentration:.3f}
|
|
739
|
+
Number of Assets: {len(non_zero_weights)}
|
|
740
|
+
"""
|
|
741
|
+
plt.text(0.1, 0.5, summary_text, fontsize=10, verticalalignment='center')
|
|
742
|
+
|
|
743
|
+
plt.tight_layout()
|
|
744
|
+
|
|
745
|
+
if save_path:
|
|
746
|
+
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
|
747
|
+
|
|
748
|
+
plt.show()
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# Example usage
|
|
752
|
+
if __name__ == "__main__":
|
|
753
|
+
# Generate sample data
|
|
754
|
+
np.random.seed(42)
|
|
755
|
+
|
|
756
|
+
assets = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA']
|
|
757
|
+
n_assets = len(assets)
|
|
758
|
+
|
|
759
|
+
# Expected returns (annual)
|
|
760
|
+
expected_returns = pd.Series(
|
|
761
|
+
np.random.uniform(0.05, 0.15, n_assets),
|
|
762
|
+
index=assets
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
# Generate covariance matrix
|
|
766
|
+
correlation_matrix = np.random.uniform(0.1, 0.7, (n_assets, n_assets))
|
|
767
|
+
correlation_matrix = (correlation_matrix + correlation_matrix.T) / 2
|
|
768
|
+
np.fill_diagonal(correlation_matrix, 1.0)
|
|
769
|
+
|
|
770
|
+
volatilities = np.random.uniform(0.15, 0.35, n_assets)
|
|
771
|
+
covariance_matrix = np.outer(volatilities, volatilities) * correlation_matrix
|
|
772
|
+
covariance_matrix = pd.DataFrame(covariance_matrix, index=assets, columns=assets)
|
|
773
|
+
|
|
774
|
+
# Initialize optimizer
|
|
775
|
+
constraints = OptimizationConstraints(
|
|
776
|
+
max_weight=0.4,
|
|
777
|
+
transaction_costs=0.001,
|
|
778
|
+
allow_short=False
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
optimizer = AdvancedPortfolioOptimizer(constraints)
|
|
782
|
+
|
|
783
|
+
# Test different optimization methods
|
|
784
|
+
objectives = [
|
|
785
|
+
OptimizationObjective.MEAN_VARIANCE,
|
|
786
|
+
OptimizationObjective.RISK_PARITY,
|
|
787
|
+
OptimizationObjective.KELLY_CRITERION
|
|
788
|
+
]
|
|
789
|
+
|
|
790
|
+
results = {}
|
|
791
|
+
|
|
792
|
+
for obj in objectives:
|
|
793
|
+
try:
|
|
794
|
+
allocation = optimizer.optimize_portfolio(expected_returns, covariance_matrix, obj)
|
|
795
|
+
results[obj.value] = allocation
|
|
796
|
+
|
|
797
|
+
print(f"\n{obj.value.upper()} Optimization:")
|
|
798
|
+
print(f"Expected Return: {allocation.expected_return:.3f}")
|
|
799
|
+
print(f"Volatility: {allocation.expected_volatility:.3f}")
|
|
800
|
+
print(f"Sharpe Ratio: {allocation.sharpe_ratio:.3f}")
|
|
801
|
+
print("Top 3 Holdings:")
|
|
802
|
+
sorted_weights = sorted(allocation.weights.items(), key=lambda x: abs(x[1]), reverse=True)
|
|
803
|
+
for asset, weight in sorted_weights[:3]:
|
|
804
|
+
print(f" {asset}: {weight:.3f}")
|
|
805
|
+
|
|
806
|
+
except Exception as e:
|
|
807
|
+
logger.error(f"Failed to optimize with {obj.value}: {e}")
|
|
808
|
+
|
|
809
|
+
# Test multi-objective optimization
|
|
810
|
+
try:
|
|
811
|
+
multi_obj_allocation = optimizer.multi_objective_optimization(
|
|
812
|
+
expected_returns, covariance_matrix,
|
|
813
|
+
objectives=[OptimizationObjective.MEAN_VARIANCE, OptimizationObjective.RISK_PARITY],
|
|
814
|
+
weights=[0.7, 0.3]
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
print(f"\nMULTI-OBJECTIVE Optimization:")
|
|
818
|
+
print(f"Expected Return: {multi_obj_allocation.expected_return:.3f}")
|
|
819
|
+
print(f"Volatility: {multi_obj_allocation.expected_volatility:.3f}")
|
|
820
|
+
print(f"Sharpe Ratio: {multi_obj_allocation.sharpe_ratio:.3f}")
|
|
821
|
+
|
|
822
|
+
except Exception as e:
|
|
823
|
+
logger.error(f"Multi-objective optimization failed: {e}")
|
|
824
|
+
|
|
825
|
+
# Generate efficient frontier
|
|
826
|
+
try:
|
|
827
|
+
frontier_df = optimizer.efficient_frontier(expected_returns, covariance_matrix, n_points=10)
|
|
828
|
+
print(f"\nEfficient Frontier generated with {len(frontier_df)} points")
|
|
829
|
+
print(f"Max Sharpe Ratio: {frontier_df['sharpe'].max():.3f}")
|
|
830
|
+
|
|
831
|
+
except Exception as e:
|
|
832
|
+
logger.error(f"Efficient frontier generation failed: {e}")
|
|
833
|
+
|
|
834
|
+
logger.info("Advanced portfolio optimization demo completed")
|