GoshawkVortexAI commited on
Commit
31e9e6b
·
verified ·
1 Parent(s): 21c2a7a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1330 -0
app.py ADDED
@@ -0,0 +1,1330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PORTFOLIO INTELLIGENCE ENGINE
3
+ Institutional-grade multi-asset portfolio construction system
4
+
5
+ Extends single-asset AI forecasting to portfolio-level optimization with:
6
+ - Risk-aware optimization
7
+ - Regime-adaptive allocation
8
+ - Stress testing
9
+ - Explainable narratives
10
+
11
+ Author: Portfolio Intelligence Team
12
+ Version: 2.0
13
+ Date: 2026-02-07
14
+ """
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ from datetime import datetime, timedelta
19
+ from typing import List, Dict, Tuple, Optional, Union
20
+ from dataclasses import dataclass, field
21
+ from scipy.optimize import minimize
22
+ from scipy import stats
23
+ import warnings
24
+ warnings.filterwarnings('ignore')
25
+
26
+ # ============================================================================
27
+ # LAYER 1: INPUT & CONSTRAINTS
28
+ # ============================================================================
29
+
30
+ @dataclass
31
+ class PortfolioConstraints:
32
+ """Portfolio construction constraints and parameters"""
33
+
34
+ # Asset Universe
35
+ assets: List[str] = field(default_factory=list)
36
+ asset_types: Dict[str, str] = field(default_factory=dict)
37
+
38
+ # Weight Constraints
39
+ min_weight: Dict[str, float] = field(default_factory=dict)
40
+ max_weight: Dict[str, float] = field(default_factory=dict)
41
+ sector_caps: Dict[str, float] = field(default_factory=dict)
42
+
43
+ # Cash & Liquidity
44
+ cash_allowance: Tuple[float, float] = (0.0, 0.30)
45
+ min_liquidity_threshold: float = 1e6
46
+
47
+ # Rebalancing Rules
48
+ rebalance_frequency: str = 'weekly'
49
+ rebalance_threshold: float = 0.05
50
+ transaction_cost: float = 0.001
51
+
52
+ # Risk Profile
53
+ risk_profile: str = 'balanced'
54
+ max_portfolio_volatility: Optional[float] = None
55
+ target_sharpe: Optional[float] = None
56
+
57
+ # Advanced Constraints
58
+ leverage_allowed: bool = False
59
+ short_selling_allowed: bool = False
60
+ concentration_limit: float = 0.35
61
+
62
+ def validate(self) -> 'ValidationResult':
63
+ """Validate constraint feasibility"""
64
+ errors = []
65
+ warnings_list = []
66
+
67
+ # Check sum of min weights
68
+ if self.min_weight:
69
+ min_sum = sum(self.min_weight.values())
70
+ if min_sum > 1.0:
71
+ errors.append(f"Sum of min_weights ({min_sum:.2%}) exceeds 100%")
72
+
73
+ # Check min < max for each asset
74
+ for asset in self.assets:
75
+ min_w = self.min_weight.get(asset, 0.0)
76
+ max_w = self.max_weight.get(asset, 1.0)
77
+ if min_w >= max_w:
78
+ errors.append(f"{asset}: min_weight ({min_w}) >= max_weight ({max_w})")
79
+
80
+ # Check risk profile validity
81
+ if self.risk_profile not in ['conservative', 'balanced', 'aggressive']:
82
+ errors.append(f"Invalid risk_profile: {self.risk_profile}")
83
+
84
+ # Check concentration limit
85
+ if self.concentration_limit < 0 or self.concentration_limit > 1:
86
+ errors.append(f"concentration_limit must be in [0, 1], got {self.concentration_limit}")
87
+
88
+ # Warnings for common issues
89
+ if not self.sector_caps:
90
+ warnings_list.append("No sector caps specified - portfolio may be over-concentrated")
91
+
92
+ if self.transaction_cost == 0:
93
+ warnings_list.append("transaction_cost = 0 may lead to excessive rebalancing")
94
+
95
+ return ValidationResult(
96
+ is_valid=len(errors) == 0,
97
+ errors=errors,
98
+ warnings=warnings_list
99
+ )
100
+
101
+ @dataclass
102
+ class ValidationResult:
103
+ """Result of constraint validation"""
104
+ is_valid: bool
105
+ errors: List[str]
106
+ warnings: List[str]
107
+
108
+ # Risk profile templates
109
+ RISK_PROFILES = {
110
+ 'conservative': {
111
+ 'max_volatility': 0.10,
112
+ 'max_drawdown': 0.15,
113
+ 'min_sharpe': 0.8,
114
+ 'cash_range': (0.10, 0.40),
115
+ 'max_beta': 0.7,
116
+ 'regime_sensitivity': 'high'
117
+ },
118
+ 'balanced': {
119
+ 'max_volatility': 0.15,
120
+ 'max_drawdown': 0.25,
121
+ 'min_sharpe': 0.6,
122
+ 'cash_range': (0.05, 0.20),
123
+ 'max_beta': 1.0,
124
+ 'regime_sensitivity': 'medium'
125
+ },
126
+ 'aggressive': {
127
+ 'max_volatility': 0.25,
128
+ 'max_drawdown': 0.40,
129
+ 'min_sharpe': 0.4,
130
+ 'cash_range': (0.00, 0.10),
131
+ 'max_beta': 1.5,
132
+ 'regime_sensitivity': 'low'
133
+ }
134
+ }
135
+
136
+
137
+ # ============================================================================
138
+ # LAYER 2: DATA & FEATURE
139
+ # ============================================================================
140
+
141
+ @dataclass
142
+ class AssetFeatures:
143
+ """Comprehensive features for a single asset"""
144
+
145
+ symbol: str
146
+
147
+ # Historical Data
148
+ historical_returns: pd.Series = None
149
+ historical_prices: pd.Series = None
150
+ historical_volume: pd.Series = None
151
+
152
+ # Technical Indicators
153
+ sma_20: float = 0.0
154
+ sma_50: float = 0.0
155
+ sma_200: float = 0.0
156
+ rsi: float = 50.0
157
+ macd: float = 0.0
158
+ macd_signal: float = 0.0
159
+
160
+ # Forward-Looking AI Forecast
161
+ expected_return: float = 0.0
162
+ expected_return_confidence: float = 0.5
163
+ forecast_horizon: int = 30
164
+ return_distribution: np.ndarray = None
165
+
166
+ # Volatility Metrics
167
+ realized_volatility: float = 0.15
168
+ predicted_volatility: float = 0.15
169
+ volatility_regime: str = 'medium'
170
+
171
+ # Market Regime
172
+ current_regime: str = 'sideways'
173
+ regime_probability: float = 0.5
174
+ regime_transition_risk: float = 0.3
175
+
176
+ # Risk Metrics
177
+ beta: float = 1.0
178
+ correlation_to_market: float = 0.5
179
+ downside_deviation: float = 0.10
180
+ max_drawdown: float = -0.20
181
+
182
+ # Liquidity & Fundamental
183
+ avg_daily_volume: float = 1e6
184
+ market_cap: Optional[float] = None
185
+ sector: Optional[str] = None
186
+
187
+ # Metadata
188
+ last_update: datetime = field(default_factory=datetime.now)
189
+ data_quality_score: float = 1.0
190
+
191
+
192
+ class DataFeatureLayer:
193
+ """Layer 2: Extract and compute asset features"""
194
+
195
+ def compute_asset_features(
196
+ self,
197
+ symbol: str,
198
+ historical_data: pd.DataFrame,
199
+ forecast_result: Dict,
200
+ regime_result: Dict
201
+ ) -> AssetFeatures:
202
+ """Transform raw data into structured features"""
203
+
204
+ features = AssetFeatures(symbol=symbol)
205
+
206
+ # Historical statistics
207
+ if 'Close' in historical_data.columns:
208
+ features.historical_prices = historical_data['Close']
209
+ features.historical_returns = np.log(
210
+ historical_data['Close'] / historical_data['Close'].shift(1)
211
+ ).dropna()
212
+
213
+ # Calculate realized volatility
214
+ features.realized_volatility = features.historical_returns.std() * np.sqrt(252)
215
+
216
+ if 'Volume' in historical_data.columns:
217
+ features.historical_volume = historical_data['Volume']
218
+ features.avg_daily_volume = historical_data['Volume'].tail(20).mean()
219
+
220
+ # Extract AI forecast
221
+ if forecast_result:
222
+ exp_ret, confidence = self._extract_expected_return(forecast_result)
223
+ features.expected_return = exp_ret
224
+ features.expected_return_confidence = confidence
225
+
226
+ if 'probabilistic_samples' in forecast_result:
227
+ features.return_distribution = forecast_result['probabilistic_samples']
228
+
229
+ # Regime information
230
+ if regime_result:
231
+ features.current_regime = regime_result.get('regime', 'sideways')
232
+ features.regime_probability = regime_result.get('confidence', 0.5)
233
+
234
+ # Compute forward volatility
235
+ features.predicted_volatility = self._compute_forward_volatility(
236
+ features.realized_volatility,
237
+ None, # GARCH forecast (placeholder)
238
+ features.current_regime
239
+ )
240
+
241
+ # Compute beta (vs market)
242
+ if len(features.historical_returns) > 60:
243
+ # Simple market proxy (would normally fetch SPY)
244
+ features.beta = 1.0 # Placeholder
245
+
246
+ features.last_update = datetime.now()
247
+
248
+ return features
249
+
250
+ def _extract_expected_return(self, forecast_result: Dict) -> Tuple[float, float]:
251
+ """Convert probabilistic forecast to expected return + confidence"""
252
+
253
+ if 'probabilistic_samples' in forecast_result:
254
+ samples = forecast_result['probabilistic_samples']
255
+ median_return = np.median(samples)
256
+ p10 = np.percentile(samples, 10)
257
+ p90 = np.percentile(samples, 90)
258
+
259
+ # Confidence based on uncertainty
260
+ uncertainty = (p90 - p10) / abs(median_return) if median_return != 0 else 10.0
261
+ confidence = 1.0 / (1.0 + uncertainty)
262
+ confidence = np.clip(confidence, 0.0, 1.0)
263
+
264
+ # Annualize (assuming 30-day forecast)
265
+ annualized_return = median_return * (252 / 30)
266
+
267
+ return annualized_return, confidence
268
+
269
+ return 0.0, 0.5
270
+
271
+ def _compute_forward_volatility(
272
+ self,
273
+ historical_vol: float,
274
+ garch_forecast: Optional[float],
275
+ regime: str
276
+ ) -> float:
277
+ """Blend historical, GARCH, and regime-adjusted volatility"""
278
+
279
+ # Regime multipliers
280
+ regime_multipliers = {
281
+ 'bull': 0.85,
282
+ 'sideways': 1.0,
283
+ 'bear': 1.3
284
+ }
285
+
286
+ multiplier = regime_multipliers.get(regime, 1.0)
287
+
288
+ if garch_forecast:
289
+ # Weighted blend
290
+ vol = 0.4 * historical_vol + 0.4 * garch_forecast + 0.2 * historical_vol * multiplier
291
+ else:
292
+ # Simple regime adjustment
293
+ vol = historical_vol * multiplier
294
+
295
+ return vol
296
+
297
+
298
+ # ============================================================================
299
+ # LAYER 3: CORRELATION & DEPENDENCY
300
+ # ============================================================================
301
+
302
+ class CovarianceEstimator:
303
+ """Layer 3: Model asset interdependencies"""
304
+
305
+ def rolling_covariance(
306
+ self,
307
+ returns: pd.DataFrame,
308
+ window: int = 60
309
+ ) -> np.ndarray:
310
+ """Exponentially weighted rolling covariance"""
311
+
312
+ # Use exponential weighting
313
+ cov_matrix = returns.ewm(span=window).cov()
314
+
315
+ # Extract the most recent covariance matrix
316
+ n_assets = len(returns.columns)
317
+ latest_cov = cov_matrix.iloc[-n_assets:, :].values
318
+
319
+ return self._ensure_positive_definite(latest_cov)
320
+
321
+ def regime_conditional_covariance(
322
+ self,
323
+ returns: pd.DataFrame,
324
+ regimes: pd.Series
325
+ ) -> Dict[str, np.ndarray]:
326
+ """Compute separate covariance matrices per regime"""
327
+
328
+ regime_covs = {}
329
+
330
+ for regime in regimes.unique():
331
+ regime_mask = (regimes == regime)
332
+ regime_returns = returns[regime_mask]
333
+
334
+ if len(regime_returns) > 20: # Minimum sample size
335
+ cov = regime_returns.cov().values
336
+ regime_covs[regime] = self._ensure_positive_definite(cov)
337
+
338
+ return regime_covs
339
+
340
+ def stress_covariance(
341
+ self,
342
+ base_cov: np.ndarray,
343
+ stress_scenario: str
344
+ ) -> np.ndarray:
345
+ """Adjust covariance for stress scenarios"""
346
+
347
+ n = base_cov.shape[0]
348
+
349
+ if stress_scenario == 'market_crash':
350
+ # All correlations → 0.8
351
+ corr_matrix = np.full((n, n), 0.8)
352
+ np.fill_diagonal(corr_matrix, 1.0)
353
+
354
+ # Extract standard deviations
355
+ std_devs = np.sqrt(np.diag(base_cov))
356
+
357
+ # Reconstruct covariance
358
+ stressed_cov = np.outer(std_devs, std_devs) * corr_matrix
359
+
360
+ elif stress_scenario == 'volatility_spike':
361
+ # Scale all variances by 2x
362
+ stressed_cov = base_cov * 2.0
363
+
364
+ else:
365
+ stressed_cov = base_cov
366
+
367
+ return self._ensure_positive_definite(stressed_cov)
368
+
369
+ def _ensure_positive_definite(self, cov_matrix: np.ndarray) -> np.ndarray:
370
+ """Ensure covariance matrix is positive definite"""
371
+
372
+ # Eigenvalue decomposition
373
+ eigvals, eigvecs = np.linalg.eigh(cov_matrix)
374
+
375
+ # Replace negative eigenvalues
376
+ eigvals[eigvals < 1e-8] = 1e-8
377
+
378
+ # Reconstruct matrix
379
+ return eigvecs @ np.diag(eigvals) @ eigvecs.T
380
+
381
+
382
+ def calculate_diversification_ratio(
383
+ weights: np.ndarray,
384
+ volatilities: np.ndarray,
385
+ cov_matrix: np.ndarray
386
+ ) -> float:
387
+ """
388
+ Diversification Ratio = (Weighted Avg Vol) / (Portfolio Vol)
389
+ Higher ratio = better diversification
390
+ """
391
+ weighted_vol = np.sum(weights * volatilities)
392
+ portfolio_vol = np.sqrt(weights @ cov_matrix @ weights)
393
+
394
+ if portfolio_vol > 0:
395
+ return weighted_vol / portfolio_vol
396
+ return 1.0
397
+
398
+
399
+ # ============================================================================
400
+ # LAYER 4: FORECAST AGGREGATION
401
+ # ============================================================================
402
+
403
+ @dataclass
404
+ class OptimizationInputs:
405
+ """Aggregated inputs for portfolio optimization"""
406
+ expected_returns: np.ndarray
407
+ cov_matrix: np.ndarray
408
+ confidence_weights: np.ndarray
409
+ symbols: List[str]
410
+
411
+
412
+ class ForecastAggregator:
413
+ """Layer 4: Transform individual forecasts into portfolio inputs"""
414
+
415
+ def aggregate_to_optimization_inputs(
416
+ self,
417
+ asset_features: List[AssetFeatures],
418
+ current_regime: str
419
+ ) -> OptimizationInputs:
420
+ """Convert asset-level forecasts into portfolio vectors"""
421
+
422
+ n_assets = len(asset_features)
423
+
424
+ # Extract expected returns
425
+ raw_returns = np.array([f.expected_return for f in asset_features])
426
+ confidences = np.array([f.expected_return_confidence for f in asset_features])
427
+ volatilities = np.array([f.predicted_volatility for f in asset_features])
428
+
429
+ # Confidence-adjusted returns
430
+ adjusted_returns = self._confidence_adjusted_returns(
431
+ raw_returns, confidences, risk_aversion=2.5
432
+ )
433
+
434
+ # Apply regime tilts
435
+ regime_adjusted_returns = self._apply_regime_tilts(
436
+ adjusted_returns, volatilities, current_regime
437
+ )
438
+
439
+ # Build covariance matrix
440
+ returns_df = pd.DataFrame({
441
+ f.symbol: f.historical_returns
442
+ for f in asset_features if f.historical_returns is not None
443
+ })
444
+
445
+ cov_estimator = CovarianceEstimator()
446
+ cov_matrix = cov_estimator.rolling_covariance(returns_df)
447
+
448
+ # Symbols
449
+ symbols = [f.symbol for f in asset_features]
450
+
451
+ return OptimizationInputs(
452
+ expected_returns=regime_adjusted_returns,
453
+ cov_matrix=cov_matrix,
454
+ confidence_weights=confidences,
455
+ symbols=symbols
456
+ )
457
+
458
+ def _confidence_adjusted_returns(
459
+ self,
460
+ raw_returns: np.ndarray,
461
+ confidences: np.ndarray,
462
+ risk_aversion: float = 2.5
463
+ ) -> np.ndarray:
464
+ """Adjust returns based on forecast confidence"""
465
+
466
+ rf = 0.04 # Risk-free rate
467
+
468
+ # Shrink toward risk-free rate based on confidence
469
+ adjusted = confidences * raw_returns + (1 - confidences) * rf
470
+
471
+ return adjusted
472
+
473
+ def _apply_regime_tilts(
474
+ self,
475
+ returns: np.ndarray,
476
+ volatilities: np.ndarray,
477
+ regime: str
478
+ ) -> np.ndarray:
479
+ """Apply regime-based adjustments"""
480
+
481
+ regime_tilts = {
482
+ 'bull': {'multiplier': 1.2, 'vol_penalty': 0.8},
483
+ 'bear': {'multiplier': 0.7, 'vol_penalty': 1.5},
484
+ 'sideways': {'multiplier': 1.0, 'vol_penalty': 1.0}
485
+ }
486
+
487
+ tilt = regime_tilts.get(regime, {'multiplier': 1.0, 'vol_penalty': 1.0})
488
+
489
+ # Adjust returns and penalize volatility
490
+ adjusted_returns = returns * tilt['multiplier']
491
+ adjusted_returns -= volatilities * tilt['vol_penalty'] * 0.1
492
+
493
+ return adjusted_returns
494
+
495
+
496
+ # ============================================================================
497
+ # LAYER 5: OPTIMIZATION ENGINE
498
+ # ============================================================================
499
+
500
+ class OptimizationEngine:
501
+ """Layer 5: Solve for optimal portfolio weights"""
502
+
503
+ def minimize_variance(
504
+ self,
505
+ cov_matrix: np.ndarray,
506
+ constraints: PortfolioConstraints
507
+ ) -> np.ndarray:
508
+ """Minimum variance portfolio"""
509
+
510
+ n = cov_matrix.shape[0]
511
+
512
+ def objective(w):
513
+ return w @ cov_matrix @ w
514
+
515
+ # Bounds
516
+ bounds = []
517
+ for i, symbol in enumerate(constraints.assets[:n]):
518
+ min_w = constraints.min_weight.get(symbol, 0.0)
519
+ max_w = constraints.max_weight.get(symbol, 1.0)
520
+ bounds.append((min_w, max_w))
521
+
522
+ # Constraints
523
+ constraints_list = [
524
+ {'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}
525
+ ]
526
+
527
+ # Initial guess: equal weights
528
+ x0 = np.ones(n) / n
529
+
530
+ result = minimize(
531
+ objective,
532
+ x0=x0,
533
+ method='SLSQP',
534
+ bounds=bounds,
535
+ constraints=constraints_list
536
+ )
537
+
538
+ return result.x if result.success else x0
539
+
540
+ def maximize_sharpe(
541
+ self,
542
+ returns: np.ndarray,
543
+ cov_matrix: np.ndarray,
544
+ risk_free_rate: float,
545
+ constraints: PortfolioConstraints
546
+ ) -> np.ndarray:
547
+ """Maximum Sharpe ratio portfolio"""
548
+
549
+ n = len(returns)
550
+
551
+ def negative_sharpe(w):
552
+ portfolio_return = returns @ w
553
+ portfolio_vol = np.sqrt(w @ cov_matrix @ w)
554
+
555
+ if portfolio_vol == 0:
556
+ return 1e10
557
+
558
+ return -(portfolio_return - risk_free_rate) / portfolio_vol
559
+
560
+ # Bounds
561
+ bounds = []
562
+ for i, symbol in enumerate(constraints.assets[:n]):
563
+ min_w = constraints.min_weight.get(symbol, 0.0)
564
+ max_w = constraints.max_weight.get(symbol, 1.0)
565
+ bounds.append((min_w, max_w))
566
+
567
+ # Constraints
568
+ constraints_list = [
569
+ {'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}
570
+ ]
571
+
572
+ x0 = np.ones(n) / n
573
+
574
+ result = minimize(
575
+ negative_sharpe,
576
+ x0=x0,
577
+ method='SLSQP',
578
+ bounds=bounds,
579
+ constraints=constraints_list
580
+ )
581
+
582
+ return result.x if result.success else x0
583
+
584
+ def risk_parity_allocation(
585
+ self,
586
+ cov_matrix: np.ndarray,
587
+ constraints: PortfolioConstraints
588
+ ) -> np.ndarray:
589
+ """Risk parity: equal risk contribution from each asset"""
590
+
591
+ n = cov_matrix.shape[0]
592
+
593
+ def risk_contribution_objective(w):
594
+ portfolio_var = w @ cov_matrix @ w
595
+
596
+ if portfolio_var == 0:
597
+ return 1e10
598
+
599
+ marginal_contrib = cov_matrix @ w
600
+ risk_contrib = w * marginal_contrib / portfolio_var
601
+ target_rc = 1.0 / n
602
+
603
+ return np.sum((risk_contrib - target_rc) ** 2)
604
+
605
+ # Bounds
606
+ bounds = [(0.01, 1.0) for _ in range(n)]
607
+
608
+ # Constraints
609
+ constraints_list = [
610
+ {'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}
611
+ ]
612
+
613
+ x0 = np.ones(n) / n
614
+
615
+ result = minimize(
616
+ risk_contribution_objective,
617
+ x0=x0,
618
+ method='SLSQP',
619
+ bounds=bounds,
620
+ constraints=constraints_list
621
+ )
622
+
623
+ return result.x if result.success else x0
624
+
625
+ def ai_weighted_allocation(
626
+ self,
627
+ returns: np.ndarray,
628
+ cov_matrix: np.ndarray,
629
+ confidences: np.ndarray,
630
+ regime_scores: np.ndarray,
631
+ constraints: PortfolioConstraints
632
+ ) -> np.ndarray:
633
+ """
634
+ AI-weighted: Blend forecast confidence, regime suitability, Sharpe
635
+ This is the proprietary "secret sauce" mode
636
+ """
637
+
638
+ n = len(returns)
639
+
640
+ # Calculate Sharpe per asset
641
+ volatilities = np.sqrt(np.diag(cov_matrix))
642
+ sharpe_per_asset = returns / (volatilities + 1e-8)
643
+
644
+ # Utility score
645
+ utility = confidences * np.array(regime_scores) * sharpe_per_asset
646
+
647
+ def objective(w):
648
+ # Maximize utility while controlling risk
649
+ portfolio_utility = np.dot(utility, w)
650
+ portfolio_risk = np.sqrt(w @ cov_matrix @ w)
651
+
652
+ # Risk-adjusted utility
653
+ return -(portfolio_utility - 2.0 * portfolio_risk)
654
+
655
+ # Bounds
656
+ bounds = []
657
+ for i, symbol in enumerate(constraints.assets[:n]):
658
+ min_w = constraints.min_weight.get(symbol, 0.0)
659
+ max_w = constraints.max_weight.get(symbol, 0.5) # Cap at 50%
660
+ bounds.append((min_w, max_w))
661
+
662
+ # Constraints
663
+ constraints_list = [
664
+ {'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}
665
+ ]
666
+
667
+ x0 = np.ones(n) / n
668
+
669
+ result = minimize(
670
+ objective,
671
+ x0=x0,
672
+ method='SLSQP',
673
+ bounds=bounds,
674
+ constraints=constraints_list
675
+ )
676
+
677
+ return result.x if result.success else x0
678
+
679
+
680
+ # ============================================================================
681
+ # LAYER 6: RISK ANALYTICS
682
+ # ============================================================================
683
+
684
+ @dataclass
685
+ class RiskReport:
686
+ """Comprehensive risk analysis of portfolio"""
687
+
688
+ expected_return: float
689
+ portfolio_volatility: float
690
+ sharpe_ratio: float
691
+ sortino_ratio: float = 0.0
692
+ max_drawdown: float = 0.0
693
+ max_drawdown_recovery_days: int = 0
694
+ var_95: float = 0.0
695
+ cvar_95: float = 0.0
696
+ risk_contribution: Dict[str, float] = field(default_factory=dict)
697
+ diversification_ratio: float = 1.0
698
+ concentration_index: float = 0.0
699
+
700
+ def to_markdown(self) -> str:
701
+ """Generate investor-ready risk summary"""
702
+
703
+ md = f"""
704
+ ## Risk Metrics
705
+
706
+ **Return & Volatility:**
707
+ - Expected Return: {self.expected_return:.2%} annually
708
+ - Portfolio Volatility: {self.portfolio_volatility:.2%} annually
709
+ - Sharpe Ratio: {self.sharpe_ratio:.2f}
710
+ - Sortino Ratio: {self.sortino_ratio:.2f}
711
+
712
+ **Downside Risk:**
713
+ - Maximum Drawdown: {self.max_drawdown:.2%}
714
+ - Recovery Time: {self.max_drawdown_recovery_days} days
715
+ - 95% VaR (1-day): {self.var_95:.2%}
716
+ - 95% CVaR: {self.cvar_95:.2%}
717
+
718
+ **Diversification:**
719
+ - Diversification Ratio: {self.diversification_ratio:.2f}
720
+ - Concentration Index (HHI): {self.concentration_index:.3f}
721
+
722
+ **Risk Contributors:**
723
+ """
724
+
725
+ for asset, contrib in sorted(self.risk_contribution.items(),
726
+ key=lambda x: x[1], reverse=True):
727
+ md += f"- {asset}: {contrib:.2%}\n"
728
+
729
+ return md
730
+
731
+
732
+ class PortfolioRiskAnalytics:
733
+ """Layer 6: Compute institutional-grade risk metrics"""
734
+
735
+ def compute_all_metrics(
736
+ self,
737
+ weights: np.ndarray,
738
+ returns: np.ndarray,
739
+ cov_matrix: np.ndarray,
740
+ asset_features: List[AssetFeatures]
741
+ ) -> RiskReport:
742
+ """Comprehensive risk analysis"""
743
+
744
+ # Basic metrics
745
+ expected_return = self.expected_return(weights, returns)
746
+ portfolio_vol = self.portfolio_volatility(weights, cov_matrix)
747
+ sharpe = self.sharpe_ratio(expected_return, portfolio_vol)
748
+
749
+ # VaR / CVaR
750
+ var_95 = self.value_at_risk(weights, returns, cov_matrix)
751
+ cvar_95 = self.conditional_var(weights, returns, cov_matrix)
752
+
753
+ # Risk contribution
754
+ risk_contrib = self.risk_contribution_per_asset(weights, cov_matrix)
755
+ risk_contrib_dict = {
756
+ f.symbol: risk_contrib[i]
757
+ for i, f in enumerate(asset_features)
758
+ }
759
+
760
+ # Diversification
761
+ volatilities = np.sqrt(np.diag(cov_matrix))
762
+ div_ratio = calculate_diversification_ratio(weights, volatilities, cov_matrix)
763
+
764
+ # Concentration (HHI)
765
+ hhi = np.sum(weights ** 2)
766
+
767
+ return RiskReport(
768
+ expected_return=expected_return,
769
+ portfolio_volatility=portfolio_vol,
770
+ sharpe_ratio=sharpe,
771
+ var_95=var_95,
772
+ cvar_95=cvar_95,
773
+ risk_contribution=risk_contrib_dict,
774
+ diversification_ratio=div_ratio,
775
+ concentration_index=hhi
776
+ )
777
+
778
+ def expected_return(self, weights: np.ndarray, returns: np.ndarray) -> float:
779
+ """E[R_p] = w^T μ"""
780
+ return np.dot(weights, returns)
781
+
782
+ def portfolio_volatility(self, weights: np.ndarray, cov_matrix: np.ndarray) -> float:
783
+ """σ_p = sqrt(w^T Σ w)"""
784
+ return np.sqrt(weights @ cov_matrix @ weights)
785
+
786
+ def sharpe_ratio(
787
+ self,
788
+ portfolio_return: float,
789
+ portfolio_vol: float,
790
+ risk_free_rate: float = 0.04
791
+ ) -> float:
792
+ """Sharpe = (R_p - R_f) / σ_p"""
793
+ if portfolio_vol == 0:
794
+ return 0.0
795
+ return (portfolio_return - risk_free_rate) / portfolio_vol
796
+
797
+ def value_at_risk(
798
+ self,
799
+ weights: np.ndarray,
800
+ returns: np.ndarray,
801
+ cov_matrix: np.ndarray,
802
+ confidence_level: float = 0.95
803
+ ) -> float:
804
+ """95% VaR: Maximum loss at confidence level"""
805
+
806
+ portfolio_return = returns @ weights
807
+ portfolio_vol = np.sqrt(weights @ cov_matrix @ weights)
808
+
809
+ # Daily VaR (parametric)
810
+ z_score = stats.norm.ppf(1 - confidence_level)
811
+ var_daily = -(portfolio_return / 252 + z_score * portfolio_vol / np.sqrt(252))
812
+
813
+ return var_daily
814
+
815
+ def conditional_var(
816
+ self,
817
+ weights: np.ndarray,
818
+ returns: np.ndarray,
819
+ cov_matrix: np.ndarray,
820
+ confidence_level: float = 0.95
821
+ ) -> float:
822
+ """CVaR: Expected loss beyond VaR"""
823
+
824
+ portfolio_return = returns @ weights
825
+ portfolio_vol = np.sqrt(weights @ cov_matrix @ weights)
826
+
827
+ z_score = stats.norm.ppf(1 - confidence_level)
828
+ pdf_at_z = stats.norm.pdf(z_score)
829
+
830
+ cvar = -(portfolio_return / 252 + portfolio_vol / np.sqrt(252) * pdf_at_z / (1 - confidence_level))
831
+
832
+ return cvar
833
+
834
+ def risk_contribution_per_asset(
835
+ self,
836
+ weights: np.ndarray,
837
+ cov_matrix: np.ndarray
838
+ ) -> np.ndarray:
839
+ """Marginal risk contribution of each asset"""
840
+
841
+ portfolio_vol = self.portfolio_volatility(weights, cov_matrix)
842
+
843
+ if portfolio_vol == 0:
844
+ return np.zeros_like(weights)
845
+
846
+ marginal_contrib = cov_matrix @ weights
847
+ risk_contrib = weights * marginal_contrib / portfolio_vol
848
+
849
+ # Normalize to sum to 1
850
+ return risk_contrib / risk_contrib.sum()
851
+
852
+
853
+ # ============================================================================
854
+ # LAYER 7: STRESS TEST & SCENARIO
855
+ # ============================================================================
856
+
857
+ @dataclass
858
+ class StressTestResult:
859
+ """Result of a stress test scenario"""
860
+
861
+ scenario_name: str
862
+ portfolio_loss_median: float
863
+ portfolio_loss_95th: float
864
+ max_drawdown: float
865
+ recovery_time_days: int
866
+ asset_level_losses: Dict[str, float]
867
+ vs_benchmark: float = 0.0
868
+
869
+ def to_narrative(self) -> str:
870
+ """Generate human-readable stress summary"""
871
+
872
+ return f"""
873
+ **{self.scenario_name}:**
874
+ - Median loss: {self.portfolio_loss_median:.2%}
875
+ - Worst case (95th percentile): {self.portfolio_loss_95th:.2%}
876
+ - Maximum drawdown: {self.max_drawdown:.2%}
877
+ - Recovery time: {self.recovery_time_days} days
878
+ - vs Benchmark: {self.vs_benchmark:+.2%}
879
+ """
880
+
881
+
882
+ class StressTestEngine:
883
+ """Layer 7: Simulate portfolio under stress"""
884
+
885
+ SCENARIOS = {
886
+ 'market_crash': {
887
+ 'description': '2008-style market crash',
888
+ 'equity_shock': -0.35,
889
+ 'vol_multiplier': 2.5,
890
+ 'correlation_surge': 0.85,
891
+ 'duration_days': 120
892
+ },
893
+ 'volatility_spike': {
894
+ 'description': 'VIX spikes to 60',
895
+ 'equity_shock': -0.20,
896
+ 'vol_multiplier': 3.0,
897
+ 'correlation_surge': 0.75,
898
+ 'duration_days': 30
899
+ },
900
+ 'sector_selloff': {
901
+ 'description': 'Tech sector crash',
902
+ 'sector_shocks': {'Technology': -0.40, 'Consumer': -0.15},
903
+ 'vol_multiplier': 2.0,
904
+ 'duration_days': 90
905
+ }
906
+ }
907
+
908
+ def run_stress_test(
909
+ self,
910
+ weights: np.ndarray,
911
+ asset_features: List[AssetFeatures],
912
+ scenario_name: str
913
+ ) -> StressTestResult:
914
+ """Simulate portfolio under stress scenario"""
915
+
916
+ scenario = self.SCENARIOS.get(scenario_name, {})
917
+
918
+ # Apply shocks to expected returns
919
+ shocked_returns = np.array([f.expected_return for f in asset_features])
920
+ equity_shock = scenario.get('equity_shock', 0.0)
921
+ shocked_returns += equity_shock
922
+
923
+ # Portfolio loss
924
+ portfolio_loss_median = np.dot(weights, shocked_returns)
925
+ portfolio_loss_95th = portfolio_loss_median * 1.5 # Simplified
926
+
927
+ # Asset-level losses
928
+ asset_losses = {
929
+ f.symbol: shocked_returns[i]
930
+ for i, f in enumerate(asset_features)
931
+ }
932
+
933
+ # Recovery time (simplified)
934
+ recovery_days = scenario.get('duration_days', 90)
935
+
936
+ return StressTestResult(
937
+ scenario_name=scenario_name,
938
+ portfolio_loss_median=portfolio_loss_median,
939
+ portfolio_loss_95th=portfolio_loss_95th,
940
+ max_drawdown=portfolio_loss_95th,
941
+ recovery_time_days=recovery_days,
942
+ asset_level_losses=asset_losses
943
+ )
944
+
945
+
946
+ # ============================================================================
947
+ # LAYER 8: REGIME-ADAPTIVE LOGIC
948
+ # ============================================================================
949
+
950
+ @dataclass
951
+ class RegimeState:
952
+ """Current market regime state"""
953
+ regime: str
954
+ confidence: float
955
+ transition_probability: float = 0.0
956
+
957
+
958
+ class RegimeAdaptiveAllocator:
959
+ """Layer 8: Dynamically adjust allocations by regime"""
960
+
961
+ REGIME_RULES = {
962
+ 'bull': {
963
+ 'strategy': 'maximize_sharpe',
964
+ 'equity_target': (0.70, 0.90),
965
+ 'cash_target': (0.00, 0.10),
966
+ 'rebalance_frequency': 'weekly'
967
+ },
968
+ 'bear': {
969
+ 'strategy': 'minimum_variance',
970
+ 'equity_target': (0.30, 0.50),
971
+ 'cash_target': (0.20, 0.40),
972
+ 'rebalance_frequency': 'daily'
973
+ },
974
+ 'sideways': {
975
+ 'strategy': 'risk_parity',
976
+ 'equity_target': (0.50, 0.70),
977
+ 'cash_target': (0.10, 0.20),
978
+ 'rebalance_frequency': 'monthly'
979
+ }
980
+ }
981
+
982
+ def detect_current_regime(
983
+ self,
984
+ market_data: pd.DataFrame,
985
+ asset_features: List[AssetFeatures]
986
+ ) -> RegimeState:
987
+ """Detect current market regime"""
988
+
989
+ # Aggregate regime signals from individual assets
990
+ regime_votes = {}
991
+ for feature in asset_features:
992
+ regime = feature.current_regime
993
+ regime_votes[regime] = regime_votes.get(regime, 0) + feature.regime_probability
994
+
995
+ # Most probable regime
996
+ if regime_votes:
997
+ current_regime = max(regime_votes, key=regime_votes.get)
998
+ confidence = regime_votes[current_regime] / len(asset_features)
999
+ else:
1000
+ current_regime = 'sideways'
1001
+ confidence = 0.5
1002
+
1003
+ return RegimeState(regime=current_regime, confidence=confidence)
1004
+
1005
+ def apply_regime_rules(
1006
+ self,
1007
+ weights: np.ndarray,
1008
+ regime: RegimeState,
1009
+ asset_features: List[AssetFeatures]
1010
+ ) -> np.ndarray:
1011
+ """Apply regime-specific adjustments to weights"""
1012
+
1013
+ rules = self.REGIME_RULES.get(regime.regime, self.REGIME_RULES['sideways'])
1014
+
1015
+ # For bear markets, increase defensive assets
1016
+ if regime.regime == 'bear' and regime.confidence > 0.7:
1017
+ # Identify defensive assets (low beta, commodities)
1018
+ for i, feature in enumerate(asset_features):
1019
+ if feature.sector in ['Commodities', 'Utilities'] or feature.beta < 0.8:
1020
+ weights[i] *= 1.3 # Increase defensive weight
1021
+ elif feature.beta > 1.2:
1022
+ weights[i] *= 0.7 # Reduce high-beta weight
1023
+
1024
+ # Renormalize
1025
+ weights = weights / weights.sum()
1026
+
1027
+ return weights
1028
+
1029
+
1030
+ # ============================================================================
1031
+ # LAYER 9: EXPLAINABILITY & NARRATIVE
1032
+ # ============================================================================
1033
+
1034
+ class PortfolioNarrativeGenerator:
1035
+ """Layer 9: Generate human-readable investment rationale"""
1036
+
1037
+ def generate_full_report(
1038
+ self,
1039
+ weights: np.ndarray,
1040
+ asset_features: List[AssetFeatures],
1041
+ risk_metrics: RiskReport,
1042
+ regime: str,
1043
+ optimization_mode: str
1044
+ ) -> str:
1045
+ """Create investor-grade portfolio summary"""
1046
+
1047
+ report = f"""
1048
+ # 📊 PORTFOLIO INTELLIGENCE REPORT
1049
+
1050
+ **Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
1051
+ **Optimization Mode:** {optimization_mode.replace('_', ' ').title()}
1052
+ **Market Regime:** {regime.title()}
1053
+
1054
+ ---
1055
+
1056
+ ## 🎯 PORTFOLIO SNAPSHOT
1057
+
1058
+ {risk_metrics.to_markdown()}
1059
+
1060
+ ---
1061
+
1062
+ ## 📈 ALLOCATION BREAKDOWN
1063
+
1064
+ """
1065
+
1066
+ # Asset allocations
1067
+ for i, feature in enumerate(asset_features):
1068
+ weight = weights[i]
1069
+ report += f"""
1070
+ ### {feature.symbol} - {weight:.1%}
1071
+
1072
+ **Key Metrics:**
1073
+ - Expected Return: {feature.expected_return:.2%} (AI Confidence: {feature.expected_return_confidence:.0%})
1074
+ - Volatility: {feature.predicted_volatility:.2%}
1075
+ - Sector: {feature.sector or 'Unknown'}
1076
+ - Beta: {feature.beta:.2f}
1077
+ - Risk Contribution: {risk_metrics.risk_contribution.get(feature.symbol, 0):.2%}
1078
+
1079
+ **Allocation Rationale:**
1080
+ {self._explain_single_asset_weight(feature, weight, regime)}
1081
+
1082
+ ---
1083
+ """
1084
+
1085
+ return report
1086
+
1087
+ def _explain_single_asset_weight(
1088
+ self,
1089
+ feature: AssetFeatures,
1090
+ weight: float,
1091
+ regime: str
1092
+ ) -> str:
1093
+ """Explain why a specific asset has its allocation"""
1094
+
1095
+ reasons = []
1096
+
1097
+ if feature.expected_return_confidence > 0.75:
1098
+ reasons.append(f"High AI forecast confidence ({feature.expected_return_confidence:.0%})")
1099
+
1100
+ if feature.current_regime == regime:
1101
+ reasons.append(f"Favorable in {regime} regime")
1102
+
1103
+ if feature.beta < 0.8:
1104
+ reasons.append("Defensive characteristics (low beta)")
1105
+ elif feature.beta > 1.2:
1106
+ reasons.append("Growth characteristics (high beta)")
1107
+
1108
+ if weight > 0.15:
1109
+ reasons.append("High conviction position")
1110
+ elif weight < 0.05:
1111
+ reasons.append("Diversification play")
1112
+
1113
+ return " | ".join(reasons) if reasons else "Balanced allocation"
1114
+
1115
+
1116
+ # ============================================================================
1117
+ # MAIN PORTFOLIO MODE CLASS
1118
+ # ============================================================================
1119
+
1120
+ @dataclass
1121
+ class PortfolioResult:
1122
+ """Complete portfolio construction result"""
1123
+
1124
+ weights: np.ndarray
1125
+ symbols: List[str]
1126
+ risk_metrics: RiskReport
1127
+ stress_tests: List[StressTestResult]
1128
+ narrative: str
1129
+ regime: RegimeState
1130
+ timestamp: datetime = field(default_factory=datetime.now)
1131
+
1132
+ def to_dict(self) -> Dict:
1133
+ """Export as dictionary"""
1134
+ return {
1135
+ 'weights': {self.symbols[i]: float(self.weights[i]) for i in range(len(self.symbols))},
1136
+ 'expected_return': float(self.risk_metrics.expected_return),
1137
+ 'volatility': float(self.risk_metrics.portfolio_volatility),
1138
+ 'sharpe_ratio': float(self.risk_metrics.sharpe_ratio),
1139
+ 'regime': self.regime.regime,
1140
+ 'timestamp': self.timestamp.isoformat()
1141
+ }
1142
+
1143
+
1144
+ class PortfolioMode:
1145
+ """
1146
+ Main orchestrator for institutional-grade portfolio construction.
1147
+
1148
+ Integrates all 10 layers into a single workflow.
1149
+ """
1150
+
1151
+ def __init__(
1152
+ self,
1153
+ constraints: PortfolioConstraints,
1154
+ chronos_pipeline = None, # Existing AI forecaster
1155
+ regime_detector = None # Existing regime detector
1156
+ ):
1157
+ self.constraints = constraints
1158
+ self.chronos_pipeline = chronos_pipeline
1159
+ self.regime_detector = regime_detector
1160
+
1161
+ # Initialize layer components
1162
+ self.data_layer = DataFeatureLayer()
1163
+ self.correlation_layer = CovarianceEstimator()
1164
+ self.forecast_aggregator = ForecastAggregator()
1165
+ self.optimizer = OptimizationEngine()
1166
+ self.risk_analytics = PortfolioRiskAnalytics()
1167
+ self.stress_tester = StressTestEngine()
1168
+ self.regime_allocator = RegimeAdaptiveAllocator()
1169
+ self.narrative_generator = PortfolioNarrativeGenerator()
1170
+
1171
+ def construct_portfolio(
1172
+ self,
1173
+ symbols: List[str],
1174
+ historical_data: Dict[str, pd.DataFrame],
1175
+ forecast_results: Dict[str, Dict],
1176
+ regime_results: Dict[str, Dict],
1177
+ optimization_mode: str = 'ai_weighted'
1178
+ ) -> PortfolioResult:
1179
+ """
1180
+ End-to-end portfolio construction pipeline.
1181
+
1182
+ Args:
1183
+ symbols: List of asset tickers
1184
+ historical_data: Dict of symbol -> DataFrame with OHLCV data
1185
+ forecast_results: Dict of symbol -> AI forecast result
1186
+ regime_results: Dict of symbol -> regime detection result
1187
+ optimization_mode: 'min_variance' | 'max_sharpe' | 'risk_parity' | 'ai_weighted'
1188
+
1189
+ Returns:
1190
+ PortfolioResult with allocations, metrics, narratives
1191
+ """
1192
+
1193
+ # Layer 1: Validate
1194
+ validation = self.constraints.validate()
1195
+ if not validation.is_valid:
1196
+ raise ValueError(f"Invalid constraints: {validation.errors}")
1197
+
1198
+ # Layer 2: Compute features for each asset
1199
+ asset_features = []
1200
+ for symbol in symbols:
1201
+ hist_data = historical_data.get(symbol)
1202
+ forecast = forecast_results.get(symbol, {})
1203
+ regime = regime_results.get(symbol, {})
1204
+
1205
+ if hist_data is not None:
1206
+ features = self.data_layer.compute_asset_features(
1207
+ symbol, hist_data, forecast, regime
1208
+ )
1209
+ asset_features.append(features)
1210
+
1211
+ if not asset_features:
1212
+ raise ValueError("No valid asset features computed")
1213
+
1214
+ # Layer 3: Build correlation/covariance
1215
+ returns_df = pd.DataFrame({
1216
+ f.symbol: f.historical_returns
1217
+ for f in asset_features if f.historical_returns is not None
1218
+ })
1219
+
1220
+ # Layer 4: Aggregate forecasts
1221
+ current_regime = self.regime_allocator.detect_current_regime(
1222
+ returns_df, asset_features
1223
+ )
1224
+
1225
+ opt_inputs = self.forecast_aggregator.aggregate_to_optimization_inputs(
1226
+ asset_features, current_regime.regime
1227
+ )
1228
+
1229
+ # Layer 5: Optimize
1230
+ if optimization_mode == 'min_variance':
1231
+ weights = self.optimizer.minimize_variance(
1232
+ opt_inputs.cov_matrix, self.constraints
1233
+ )
1234
+ elif optimization_mode == 'max_sharpe':
1235
+ weights = self.optimizer.maximize_sharpe(
1236
+ opt_inputs.expected_returns,
1237
+ opt_inputs.cov_matrix,
1238
+ risk_free_rate=0.04,
1239
+ constraints=self.constraints
1240
+ )
1241
+ elif optimization_mode == 'risk_parity':
1242
+ weights = self.optimizer.risk_parity_allocation(
1243
+ opt_inputs.cov_matrix, self.constraints
1244
+ )
1245
+ elif optimization_mode == 'ai_weighted':
1246
+ weights = self.optimizer.ai_weighted_allocation(
1247
+ opt_inputs.expected_returns,
1248
+ opt_inputs.cov_matrix,
1249
+ opt_inputs.confidence_weights,
1250
+ regime_scores=[f.regime_probability for f in asset_features],
1251
+ constraints=self.constraints
1252
+ )
1253
+ else:
1254
+ raise ValueError(f"Unknown optimization mode: {optimization_mode}")
1255
+
1256
+ # Layer 6: Risk analytics
1257
+ risk_report = self.risk_analytics.compute_all_metrics(
1258
+ weights, opt_inputs.expected_returns, opt_inputs.cov_matrix, asset_features
1259
+ )
1260
+
1261
+ # Layer 7: Stress testing
1262
+ stress_results = []
1263
+ for scenario_name in ['market_crash', 'volatility_spike', 'sector_selloff']:
1264
+ stress_result = self.stress_tester.run_stress_test(
1265
+ weights, asset_features, scenario_name
1266
+ )
1267
+ stress_results.append(stress_result)
1268
+
1269
+ # Layer 8: Regime adaptation
1270
+ regime_adjusted_weights = self.regime_allocator.apply_regime_rules(
1271
+ weights, current_regime, asset_features
1272
+ )
1273
+
1274
+ # Layer 9: Generate narrative
1275
+ narrative = self.narrative_generator.generate_full_report(
1276
+ regime_adjusted_weights,
1277
+ asset_features,
1278
+ risk_report,
1279
+ current_regime.regime,
1280
+ optimization_mode
1281
+ )
1282
+
1283
+ return PortfolioResult(
1284
+ weights=regime_adjusted_weights,
1285
+ symbols=[f.symbol for f in asset_features],
1286
+ risk_metrics=risk_report,
1287
+ stress_tests=stress_results,
1288
+ narrative=narrative,
1289
+ regime=current_regime,
1290
+ timestamp=datetime.now()
1291
+ )
1292
+
1293
+
1294
+ # ============================================================================
1295
+ # EXAMPLE USAGE
1296
+ # ============================================================================
1297
+
1298
+ if __name__ == "__main__":
1299
+
1300
+ # Example: Construct a balanced portfolio
1301
+
1302
+ # Define constraints
1303
+ constraints = PortfolioConstraints(
1304
+ assets=['AAPL', 'MSFT', 'GLD', 'TLT'],
1305
+ asset_types={
1306
+ 'AAPL': 'equity',
1307
+ 'MSFT': 'equity',
1308
+ 'GLD': 'commodity',
1309
+ 'TLT': 'bond'
1310
+ },
1311
+ min_weight={'AAPL': 0.05, 'MSFT': 0.05, 'GLD': 0.0, 'TLT': 0.0},
1312
+ max_weight={'AAPL': 0.30, 'MSFT': 0.30, 'GLD': 0.25, 'TLT': 0.25},
1313
+ sector_caps={'Technology': 0.60},
1314
+ risk_profile='balanced',
1315
+ rebalance_frequency='weekly'
1316
+ )
1317
+
1318
+ # Validate
1319
+ validation = constraints.validate()
1320
+ print(f"Constraints valid: {validation.is_valid}")
1321
+ if validation.warnings:
1322
+ print(f"Warnings: {validation.warnings}")
1323
+
1324
+ # Initialize portfolio mode (without actual AI models for demo)
1325
+ portfolio_mode = PortfolioMode(constraints=constraints)
1326
+
1327
+ print("\n✅ Portfolio Intelligence Engine initialized successfully!")
1328
+ print(f" - {len(constraints.assets)} assets in universe")
1329
+ print(f" - Risk profile: {constraints.risk_profile}")
1330
+ print(f" - Rebalance frequency: {constraints.rebalance_frequency}")