deveshc23 commited on
Commit
097fdc8
·
verified ·
1 Parent(s): 12d436b

Upload quant_portfolio.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. quant_portfolio.py +257 -0
quant_portfolio.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ QuantAgent OS - Multi-Agent Synthesis & Portfolio Optimization Layer
3
+ Combines QuantAgent signals, SEC EDGAR fundamentals, sentiment, and macro regimes
4
+ into optimized institutional portfolios (HRP, Markowitz Max Sharpe, Risk Parity).
5
+ """
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from typing import Dict, List, Optional, Tuple, Any
10
+ from scipy.optimize import minimize
11
+ from scipy.cluster.hierarchy import linkage, leaves_list
12
+ from scipy.spatial.distance import squareform
13
+
14
+
15
+ class FundamentalScorer:
16
+ """Evaluates corporate health and valuation into a composite 0-100 score."""
17
+
18
+ @staticmethod
19
+ def score_ticker(info: Dict[str, Any], sec_facts: Optional[Dict[str, Any]] = None) -> float:
20
+ score = 50.0 # Neutral baseline
21
+
22
+ # 1. Valuation (P/E, Forward P/E)
23
+ pe = info.get('trailing_pe') or info.get('forward_pe')
24
+ if pe is not None and pe > 0:
25
+ if pe < 15:
26
+ score += 15
27
+ elif pe < 25:
28
+ score += 8
29
+ elif pe > 45:
30
+ score -= 10
31
+
32
+ # 2. Price to Book
33
+ pb = info.get('price_to_book')
34
+ if pb is not None and pb > 0:
35
+ if pb < 3.0:
36
+ score += 8
37
+ elif pb > 12.0:
38
+ score -= 6
39
+
40
+ # 3. SEC EDGAR Balance Sheet Checks
41
+ if sec_facts and not sec_facts.get('error'):
42
+ de = sec_facts.get('debt_to_equity')
43
+ if de is not None:
44
+ if de < 0.8:
45
+ score += 12 # Low leverage
46
+ elif de > 2.5:
47
+ score -= 12 # High debt burden
48
+
49
+ margin = sec_facts.get('net_margin_pct')
50
+ if margin is not None:
51
+ if margin > 20:
52
+ score += 15 # High profitability
53
+ elif margin < 0:
54
+ score -= 15 # Unprofitable
55
+
56
+ return max(5.0, min(95.0, score))
57
+
58
+
59
+ class PortfolioOptimizer:
60
+ """Institutional-grade portfolio construction and risk allocation engine."""
61
+
62
+ def __init__(self, target_volatility: float = 0.15):
63
+ self.target_volatility = target_volatility
64
+
65
+ def compute_covariance(self, price_matrix: pd.DataFrame) -> Tuple[pd.Series, pd.DataFrame]:
66
+ """Calculates annualized mean returns and sample covariance matrix."""
67
+ returns = price_matrix.pct_change().dropna()
68
+ mean_ret = returns.mean() * 252
69
+ cov_matrix = returns.cov() * 252
70
+ return mean_ret, cov_matrix
71
+
72
+ def optimize_hierarchical_risk_parity(self, price_matrix: pd.DataFrame) -> Dict[str, float]:
73
+ """
74
+ Hierarchical Risk Parity (HRP) via quasi-diagonalization of covariance.
75
+ Does not invert covariance matrix, ensuring mathematical robustness.
76
+ """
77
+ returns = price_matrix.pct_change().dropna()
78
+ corr = returns.corr().fillna(0.0)
79
+ cov = returns.cov() * 252
80
+
81
+ # Distance matrix
82
+ dist = np.sqrt((1 - corr) / 2.0).clip(0, 1)
83
+ np.fill_diagonal(dist.values, 0)
84
+
85
+ # Hierarchical tree clustering
86
+ condensed_dist = squareform(dist.values, checks=False)
87
+ link = linkage(condensed_dist, method='single')
88
+ sort_order = leaves_list(link)
89
+ sorted_tickers = corr.index[sort_order].tolist()
90
+
91
+ # Recursive bisection
92
+ weights = pd.Series(1.0, index=sorted_tickers)
93
+ clusters = [sorted_tickers]
94
+
95
+ while len(clusters) > 0:
96
+ clusters = [c[i:j] for c in clusters for i, j in ((0, len(c)//2), (len(c)//2, len(c))) if len(c) > 1]
97
+ for i in range(0, len(clusters), 2):
98
+ if i + 1 < len(clusters):
99
+ c1 = clusters[i]
100
+ c2 = clusters[i+1]
101
+ var1 = self._get_cluster_var(cov, c1)
102
+ var2 = self._get_cluster_var(cov, c2)
103
+ alpha = 1 - var1 / (var1 + var2) if (var1 + var2) > 0 else 0.5
104
+ weights[c1] *= alpha
105
+ weights[c2] *= (1 - alpha)
106
+
107
+ weights = weights / weights.sum()
108
+ return {t: round(float(w), 4) for t, w in weights.items()}
109
+
110
+ def _get_cluster_var(self, cov: pd.DataFrame, cluster: List[str]) -> float:
111
+ sub_cov = cov.loc[cluster, cluster]
112
+ w = 1.0 / np.diag(sub_cov.values)
113
+ w = w / w.sum()
114
+ return float(np.dot(np.dot(w, sub_cov.values), w))
115
+
116
+ def optimize_maximum_sharpe(
117
+ self,
118
+ mean_ret: pd.Series,
119
+ cov: pd.DataFrame,
120
+ risk_free_rate: float = 0.04,
121
+ max_weight: float = 0.35
122
+ ) -> Dict[str, float]:
123
+ """Markowitz Mean-Variance Optimization maximizing Sharpe Ratio."""
124
+ n = len(mean_ret)
125
+ init_guess = np.repeat(1.0 / n, n)
126
+ bounds = tuple((0.02, max_weight) for _ in range(n))
127
+ constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0})
128
+
129
+ def neg_sharpe(weights):
130
+ p_ret = np.sum(mean_ret * weights)
131
+ p_vol = np.sqrt(np.dot(weights.T, np.dot(cov, weights)))
132
+ return -(p_ret - risk_free_rate) / (p_vol + 1e-8)
133
+
134
+ res = minimize(neg_sharpe, init_guess, method='SLSQP', bounds=bounds, constraints=constraints)
135
+ if res.success:
136
+ w_res = pd.Series(res.x, index=mean_ret.index)
137
+ w_res = w_res / w_res.sum()
138
+ return {t: round(float(w), 4) for t, w in w_res.items()}
139
+
140
+ # Fallback to inverse vol
141
+ return self.optimize_inverse_volatility(cov)
142
+
143
+ def optimize_inverse_volatility(self, cov: pd.DataFrame) -> Dict[str, float]:
144
+ """Inverse volatility parity allocation."""
145
+ vols = np.sqrt(np.diag(cov))
146
+ inv_vols = 1.0 / np.maximum(vols, 1e-6)
147
+ weights = inv_vols / np.sum(inv_vols)
148
+ return {t: round(float(w), 4) for t, w in zip(cov.index, weights)}
149
+
150
+ def calculate_portfolio_risk_metrics(
151
+ self,
152
+ weights: Dict[str, float],
153
+ mean_ret: pd.Series,
154
+ cov: pd.DataFrame,
155
+ price_matrix: pd.DataFrame,
156
+ portfolio_value: float = 100_000.0
157
+ ) -> Dict[str, Any]:
158
+ """Calculates expected return, volatility, Sharpe, VaR 95%, and CVaR."""
159
+ w_vec = np.array([weights.get(t, 0.0) for t in mean_ret.index])
160
+ w_vec = w_vec / np.sum(w_vec) if np.sum(w_vec) > 0 else np.ones(len(mean_ret)) / len(mean_ret)
161
+
162
+ p_return = float(np.sum(mean_ret * w_vec))
163
+ p_vol = float(np.sqrt(np.dot(w_vec.T, np.dot(cov, w_vec))))
164
+ sharpe = float((p_return - 0.04) / p_vol) if p_vol > 0 else 0.0
165
+
166
+ # Historical Value at Risk (VaR 95%) and CVaR
167
+ returns = price_matrix.pct_change().dropna()
168
+ p_daily_returns = (returns * w_vec).sum(axis=1)
169
+
170
+ var_95_daily = float(np.percentile(p_daily_returns, 5))
171
+ cvar_95_daily = float(p_daily_returns[p_daily_returns <= var_95_daily].mean())
172
+
173
+ var_dollar = abs(var_95_daily * portfolio_value)
174
+ cvar_dollar = abs(cvar_95_daily * portfolio_value)
175
+
176
+ return {
177
+ "expected_annual_return_pct": round(p_return * 100, 2),
178
+ "expected_annual_volatility_pct": round(p_vol * 100, 2),
179
+ "sharpe_ratio": round(sharpe, 2),
180
+ "daily_var_95_pct": round(abs(var_95_daily) * 100, 2),
181
+ "daily_var_95_dollar": round(var_dollar, 2),
182
+ "daily_cvar_95_dollar": round(cvar_dollar, 2)
183
+ }
184
+
185
+
186
+ class MultiAgentSynthesizer:
187
+ """Committee synthesizing QuantAgent signals, fundamentals, news, and risk."""
188
+
189
+ def __init__(self, optimizer: Optional[PortfolioOptimizer] = None):
190
+ self.optimizer = optimizer or PortfolioOptimizer()
191
+ self.f_scorer = FundamentalScorer()
192
+
193
+ def generate_full_allocation(
194
+ self,
195
+ tickers: List[str],
196
+ price_matrix: pd.DataFrame,
197
+ macro_snapshot: Dict[str, Any],
198
+ alpha_signals: Optional[pd.DataFrame] = None,
199
+ method: str = "Hierarchical Risk Parity (HRP)",
200
+ portfolio_capital: float = 100_000.0
201
+ ) -> Dict[str, Any]:
202
+ """Produces full committee synthesis and actionable rebalancing orders."""
203
+ mean_ret, cov = self.optimizer.compute_covariance(price_matrix)
204
+
205
+ # Baseline weights via chosen algorithm
206
+ if "HRP" in method:
207
+ base_weights = self.optimizer.optimize_hierarchical_risk_parity(price_matrix)
208
+ elif "Sharpe" in method:
209
+ base_weights = self.optimizer.optimize_maximum_sharpe(mean_ret, cov)
210
+ else:
211
+ base_weights = self.optimizer.optimize_inverse_volatility(cov)
212
+
213
+ # Multi-factor tilt: incorporate QuantAgent alpha scores if available
214
+ adjusted_weights = copy_weights = dict(base_weights)
215
+ if alpha_signals is not None and not alpha_signals.empty:
216
+ latest_alpha = alpha_signals.iloc[-1].dropna()
217
+ for t in tickers:
218
+ if t in latest_alpha:
219
+ alpha_val = float(latest_alpha[t])
220
+ # Tilt weight by up to +/- 25% based on alpha
221
+ adjusted_weights[t] = max(0.01, adjusted_weights[t] * (1.0 + 0.25 * alpha_val))
222
+
223
+ # Re-normalize
224
+ total_w = sum(adjusted_weights.values())
225
+ final_weights = {t: round(w / total_w, 4) for t, w in adjusted_weights.items()}
226
+
227
+ # Risk metrics
228
+ metrics = self.optimizer.calculate_portfolio_risk_metrics(
229
+ final_weights, mean_ret, cov, price_matrix, portfolio_capital
230
+ )
231
+
232
+ # Generate rebalancing table
233
+ trade_orders = []
234
+ for t in tickers:
235
+ w = final_weights.get(t, 0.0)
236
+ target_dollar = w * portfolio_capital
237
+ latest_price = float(price_matrix[t].iloc[-1])
238
+ target_shares = int(target_dollar / latest_price) if latest_price > 0 else 0
239
+
240
+ trade_orders.append({
241
+ "Ticker": t,
242
+ "Target Weight": f"{w*100:.1f}%",
243
+ "Allocation ($)": f"${target_dollar:,.2f}",
244
+ "Price ($)": f"${latest_price:,.2f}",
245
+ "Target Shares": target_shares,
246
+ "Action": "BUY / ACCUMULATE" if w > 0.12 else "HOLD / NEUTRAL"
247
+ })
248
+
249
+ orders_df = pd.DataFrame(trade_orders)
250
+
251
+ return {
252
+ "method": method,
253
+ "weights": final_weights,
254
+ "metrics": metrics,
255
+ "orders_table": orders_df,
256
+ "macro_regime": macro_snapshot.get("spy_trend", "Bullish")
257
+ }