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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -1277
app.py CHANGED
@@ -1,1330 +1,208 @@
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}")
 
1
+ # ============================================================
2
+ # PORTFOLIO INTELLIGENCE ENGINE - FULL APP.PY
3
+ # Live + Historical Data Enabled
4
+ # ============================================================
 
 
 
 
 
 
 
 
 
 
5
 
6
+ import yfinance as yf
7
  import numpy as np
8
  import pandas as pd
9
  from datetime import datetime, timedelta
 
 
10
  from scipy.optimize import minimize
11
  from scipy import stats
12
+ from dataclasses import dataclass
13
+ from typing import Dict, List
14
  import warnings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ warnings.filterwarnings("ignore")
17
+
18
+ # ============================================================
19
+ # DATA ENGINE
20
+ # ============================================================
21
+
22
+ class MarketDataLoader:
23
+ def __init__(self, lookback_days=365, interval="1d"):
24
+ self.lookback_days = lookback_days
25
+ self.interval = interval
26
+
27
+ def fetch(self, symbols: List[str]) -> Dict[str, pd.DataFrame]:
28
+ end = datetime.now()
29
+ start = end - timedelta(days=self.lookback_days)
30
+
31
+ data = {}
32
+ for s in symbols:
33
+ df = yf.download(
34
+ s,
35
+ start=start.strftime("%Y-%m-%d"),
36
+ end=end.strftime("%Y-%m-%d"),
37
+ interval=self.interval,
38
+ auto_adjust=True,
39
+ progress=False
40
+ )
41
+ if not df.empty:
42
+ data[s] = df[["Open", "High", "Low", "Close", "Volume"]].dropna()
43
+ return data
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ # ============================================================
47
+ # AI FORECAST ENGINE (PROXY)
48
+ # ============================================================
49
 
50
+ class SimpleAIForecaster:
51
+ def forecast(self, prices: pd.Series, horizon=30) -> Dict:
52
+ returns = np.log(prices / prices.shift(1)).dropna()
53
+ mu = returns.mean() * horizon
54
+ sigma = returns.std() * np.sqrt(horizon)
55
 
56
+ samples = np.random.normal(mu, sigma, 1000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ return {
59
+ "samples": samples,
60
+ "expected_return": mu,
61
+ "confidence": float(np.clip(1 / (1 + sigma * 10), 0.3, 0.9))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ # ============================================================
66
+ # REGIME DETECTOR
67
+ # ============================================================
68
 
69
+ class RegimeDetector:
70
+ def detect(self, returns: pd.Series) -> Dict:
71
+ vol = returns.std() * np.sqrt(252)
72
+ momentum = returns.rolling(20).mean().iloc[-1]
73
 
74
+ if momentum > 0 and vol < 0.25:
75
+ return {"regime": "bull", "confidence": 0.7}
76
+ elif momentum < 0:
77
+ return {"regime": "bear", "confidence": 0.7}
78
+ else:
79
+ return {"regime": "sideways", "confidence": 0.6}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
 
82
+ # ============================================================
83
+ # FEATURE ENGINE
84
+ # ============================================================
85
 
86
  @dataclass
87
+ class AssetFeature:
88
+ symbol: str
 
89
  expected_return: float
90
+ confidence: float
91
+ volatility: float
92
+ returns: pd.Series
93
+ regime: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
 
 
 
 
 
 
 
 
95
 
96
+ class FeatureBuilder:
97
+ def build(self, symbol, df, forecast, regime) -> AssetFeature:
98
+ returns = np.log(df["Close"] / df["Close"].shift(1)).dropna()
99
+ vol = returns.std() * np.sqrt(252)
100
 
101
+ return AssetFeature(
102
+ symbol=symbol,
103
+ expected_return=forecast["expected_return"] * forecast["confidence"],
104
+ confidence=forecast["confidence"],
105
+ volatility=vol,
106
+ returns=returns,
107
+ regime=regime["regime"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
 
111
+ # ============================================================
112
+ # OPTIMIZATION ENGINE
113
+ # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ class Optimizer:
116
+ def optimize(self, features: List[AssetFeature]) -> np.ndarray:
117
+ returns = np.array([f.expected_return for f in features])
118
+ returns = np.clip(returns, -0.5, 0.5)
119
 
120
+ returns_df = pd.concat(
121
+ [f.returns for f in features],
122
+ axis=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  )
124
+ returns_df.columns = [f.symbol for f in features]
125
 
126
+ cov = returns_df.cov().values
127
+ n = len(features)
128
 
129
+ def objective(w):
130
+ port_ret = w @ returns
131
+ port_vol = np.sqrt(w @ cov @ w)
132
+ return -(port_ret / (port_vol + 1e-8))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ bounds = [(0.05, 0.5) for _ in range(n)]
135
+ cons = [{"type": "eq", "fun": lambda w: np.sum(w) - 1}]
136
+ w0 = np.ones(n) / n
137
 
138
+ res = minimize(objective, w0, bounds=bounds, constraints=cons)
139
+ return res.x if res.success else w0
140
 
 
141
 
142
+ # ============================================================
143
+ # RISK ANALYTICS
144
+ # ============================================================
145
 
146
+ class RiskEngine:
147
+ def analyze(self, weights, features):
148
+ returns = np.array([f.expected_return for f in features])
149
+ returns_df = pd.concat([f.returns for f in features], axis=1)
150
+ cov = returns_df.cov().values
151
 
152
+ port_ret = weights @ returns
153
+ port_vol = np.sqrt(weights @ cov @ weights)
154
+ sharpe = (port_ret - 0.04) / port_vol if port_vol > 0 else 0
155
 
156
+ return {
157
+ "expected_return": port_ret,
158
+ "volatility": port_vol,
159
+ "sharpe": sharpe
160
+ }
 
 
161
 
 
 
 
 
 
 
162
 
163
+ # ============================================================
164
+ # MAIN ORCHESTRATOR
165
+ # ============================================================
166
 
167
+ class PortfolioApp:
168
+ def __init__(self, symbols):
169
+ self.symbols = symbols
170
+ self.data_loader = MarketDataLoader()
171
+ self.forecaster = SimpleAIForecaster()
172
+ self.regime_detector = RegimeDetector()
173
+ self.feature_builder = FeatureBuilder()
174
+ self.optimizer = Optimizer()
175
+ self.risk_engine = RiskEngine()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ def run(self):
178
+ market_data = self.data_loader.fetch(self.symbols)
179
+ features = []
180
 
181
+ for s, df in market_data.items():
182
+ forecast = self.forecaster.forecast(df["Close"])
183
+ returns = np.log(df["Close"] / df["Close"].shift(1)).dropna()
184
+ regime = self.regime_detector.detect(returns)
185
+ feature = self.feature_builder.build(s, df, forecast, regime)
186
+ features.append(feature)
187
 
188
+ weights = self.optimizer.optimize(features)
189
+ risk = self.risk_engine.analyze(weights, features)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ print("\n📊 PORTFÖY SONUCU\n")
192
+ for i, f in enumerate(features):
193
+ print(f"{f.symbol:5s} | Weight: {weights[i]:.2%} | Regime: {f.regime}")
194
 
195
+ print("\n--- RISK METRICS ---")
196
+ print(f"Expected Return : {risk['expected_return']:.2%}")
197
+ print(f"Volatility : {risk['volatility']:.2%}")
198
+ print(f"Sharpe Ratio : {risk['sharpe']:.2f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
 
201
+ # ============================================================
202
+ # RUN
203
+ # ============================================================
204
 
205
  if __name__ == "__main__":
206
+ symbols = ["XOM", "GOOGL", "WMT", "JNJ"]
207
+ app = PortfolioApp(symbols)
208
+ app.run()