Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Portfolio theory, optimization algorithms, and asset allocation methods
.claude/skills/brycewang-stanford-portfolio-optimization-guide/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 163% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 93% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 115% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 94% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 134% | 0% |
A skill for implementing and researching portfolio optimization methods, from classical mean-variance optimization to modern robust and factor-based approaches. Covers Markowitz theory, Black-Litterman, risk parity, and machine learning-enhanced portfolio construction.
pythonimport numpy as np from scipy.optimize import minimize def mean_variance_optimize(expected_returns: np.ndarray, cov_matrix: np.ndarray, target_return: float = None, risk_free_rate: float = 0.02) -> dict: """ Markowitz mean-variance optimization. expected_returns: array of expected returns for each asset cov_matrix: covariance matrix of asset returns target_return: target portfolio return (None for max Sharpe) """ n_assets = len(expected_returns) def portfolio_volatility(weights): return np.sqrt(weights @ cov_matrix @ weights) def neg_sharpe(weights): ret = weights @ expected_returns vol = portfolio_volatility(weights) return -(ret - risk_free_rate) / vol # Constraints constraints = [ {"type": "eq", "fun": lambda w: np.sum(w) - 1}, # weights sum to 1 ] if target_return is not None: constraints.append( {"type": "eq", "fun": lambda w: w @ expected_returns - target_return} ) # Bounds: no short selling (0 to 1 per asset) bounds = [(0, 1) for _ in range(n_assets)] # Initial guess: equal weight w0 = np.ones(n_assets) / n_assets if target_return is not None: # Minimize volatility for given return result = minimize(portfolio_volatility, w0, bounds=bounds, constraints=constraints) else: # Maximize Sharpe ratio result = minimize(neg_sharpe, w0, bounds=bounds, constraints=constraints) weights = result.x ret = weights @ expected_returns vol = portfolio_volatility(weights) return { "weights": {f"asset_{i}": round(w, 4) for i, w in enumerate(weights)}, "expected_return": round(ret, 4), "volatility": round(vol, 4), "sharpe_ratio": round((ret - risk_free_rate) / vol, 4), }
pythondef compute_efficient_frontier(expected_returns: np.ndarray, cov_matrix: np.ndarray, n_points: int = 50) -> list[dict]: """ Compute the efficient frontier by solving for minimum variance portfolios at each target return level. """ min_ret = expected_returns.min() * 0.8 max_ret = expected_returns.max() * 1.1 target_returns = np.linspace(min_ret, max_ret, n_points) frontier = [] for target in target_returns: try: result = mean_variance_optimize( expected_returns, cov_matrix, target_return=target ) frontier.append({ "return": result["expected_return"], "volatility": result["volatility"], "sharpe": result["sharpe_ratio"], }) except Exception: continue return frontier
pythondef black_litterman(market_cap_weights: np.ndarray, cov_matrix: np.ndarray, P: np.ndarray, Q: np.ndarray, omega: np.ndarray = None, risk_aversion: float = 2.5, tau: float = 0.05) -> dict: """ Black-Litterman model for combining market equilibrium with investor views. market_cap_weights: market-cap weighted portfolio cov_matrix: covariance matrix P: pick matrix (k views x n assets), identifies assets in each view Q: view returns (k x 1), expected returns for each view omega: view uncertainty (k x k), diagonal matrix """ # Step 1: Implied equilibrium returns (reverse optimization) pi = risk_aversion * cov_matrix @ market_cap_weights # Step 2: View uncertainty (if not provided, use He-Litterman) if omega is None: omega = np.diag(np.diag(tau * P @ cov_matrix @ P.T)) # Step 3: Posterior expected returns tau_sigma = tau * cov_matrix inv_tau_sigma = np.linalg.inv(tau_sigma) inv_omega = np.linalg.inv(omega) posterior_precision = inv_tau_sigma + P.T @ inv_omega @ P posterior_cov = np.linalg.inv(posterior_precision) posterior_mean = posterior_cov @ (inv_tau_sigma @ pi + P.T @ inv_omega @ Q) return { "equilibrium_returns": pi.round(4).tolist(), "posterior_returns": posterior_mean.round(4).tolist(), "posterior_covariance": posterior_cov.round(6).tolist(), }
pythondef risk_parity(cov_matrix: np.ndarray, budget: np.ndarray = None) -> dict: """ Risk parity: each asset contributes equally to total portfolio risk. budget: risk budget (default: equal, 1/n each) """ n = cov_matrix.shape[0] if budget is None: budget = np.ones(n) / n def objective(weights): portfolio_vol = np.sqrt(weights @ cov_matrix @ weights) marginal_risk = cov_matrix @ weights risk_contribution = weights * marginal_risk / portfolio_vol target_risk = budget * portfolio_vol return np.sum((risk_contribution - target_risk) ** 2) constraints = [{"type": "eq", "fun": lambda w: np.sum(w) - 1}] bounds = [(0.01, 1) for _ in range(n)] w0 = np.ones(n) / n result = minimize(objective, w0, bounds=bounds, constraints=constraints) weights = result.x # Verify risk contributions portfolio_vol = np.sqrt(weights @ cov_matrix @ weights) marginal_risk = cov_matrix @ weights risk_contrib = weights * marginal_risk / portfolio_vol risk_pct = risk_contrib / risk_contrib.sum() return { "weights": weights.round(4).tolist(), "portfolio_volatility": round(portfolio_vol, 4), "risk_contributions": risk_pct.round(4).tolist(), "max_risk_deviation": round(np.max(np.abs(risk_pct - budget)), 4), }
pythonimport statsmodels.api as sm def estimate_factor_exposures(asset_returns: pd.DataFrame, factor_returns: pd.DataFrame) -> pd.DataFrame: """ Estimate asset exposures to Fama-French factors using regression. factor_returns columns: Mkt-RF, SMB, HML, RMW, CMA (5-factor model) """ results = [] for asset in asset_returns.columns: y = asset_returns[asset] - factor_returns.get("RF", 0) X = sm.add_constant(factor_returns[["Mkt-RF", "SMB", "HML", "RMW", "CMA"]]) model = sm.OLS(y, X).fit() results.append({ "asset": asset, "alpha": round(model.params["const"], 6), "beta_market": round(model.params["Mkt-RF"], 4), "beta_size": round(model.params["SMB"], 4), "beta_value": round(model.params["HML"], 4), "beta_profit": round(model.params["RMW"], 4), "beta_invest": round(model.params["CMA"], 4), "r_squared": round(model.rsquared, 4), }) return pd.DataFrame(results)
pythondef rebalance_with_costs(current_weights: np.ndarray, target_weights: np.ndarray, portfolio_value: float, cost_per_trade: float = 0.001, threshold: float = 0.02) -> dict: """ Determine rebalancing trades considering transaction costs. threshold: minimum deviation to trigger rebalancing (2% default) cost_per_trade: proportional transaction cost (10 bps) """ deviations = np.abs(current_weights - target_weights) needs_rebalance = np.any(deviations > threshold) if not needs_rebalance: return {"action": "hold", "reason": "within threshold"} trades = target_weights - current_weights trade_value = np.abs(trades) * portfolio_value total_cost = trade_value.sum() * cost_per_trade return { "action": "rebalance", "trades": trades.round(4).tolist(), "turnover": np.abs(trades).sum() / 2, "transaction_cost": round(total_cost, 2), "cost_as_pct": round(total_cost / portfolio_value * 100, 4), }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 17,591 | 27,406 | +56% | 1 | 1 | 0% | 3,301 | 8,100 | +145% | 0 | 0 | — |
case-02 | fail→pass | 34,430 | 16,305 | -53% | 1 | 1 | 0% | 2,237 | 5,876 | +163% | 0 | 0 | — |
case-03 | pass→pass | 13,382 | 16,445 | +23% | 1 | 1 | 0% | 2,884 | 6,195 | +115% | 0 | 0 | — |
case-04 | pass→pass | 20,094 | 26,382 | +31% | 1 | 1 | 0% | 3,876 | 7,511 | +94% | 0 | 0 | — |
case-05 | pass→pass | 22,378 | 28,270 | +26% | 1 | 1 | 0% | 3,327 | 7,778 | +134% | 0 | 0 | — |
case-06 | pass→pass | 21,331 | 18,529 | -13% | 1 | 1 | 0% | 3,202 | 5,814 | +82% | 0 | 0 | — |
case-07 | pass→pass | 17,059 | 14,264 | -16% | 1 | 1 | 0% | 3,394 | 5,374 | +58% | 0 | 0 | — |
case-08 | fail→fail | 29,573 | 17,843 | -40% | 1 | 1 | 0% | 2,698 | 5,506 | +104% | 0 | 0 | — |
case-09 | pass→pass | 14,147 | 13,312 | -6% | 1 | 1 | 0% | 2,462 | 5,022 | +104% | 0 | 0 | — |
case-10 | pass→pass | 10,405 | 6,852 | -34% | 1 | 1 | 0% | 1,550 | 3,830 | +147% | 0 | 0 | — |
case-11 | fail→fail | 19,817 | 20,680 | +4% | 1 | 1 | 0% | 3,882 | 6,525 | +68% | 0 | 0 | — |
case-12 | fail→fail | 13,256 | 18,345 | +38% | 1 | 1 | 0% | 2,316 | 5,262 | +127% | 0 | 0 | — |
case-13 | pass→pass | 10,871 | 10,432 | -4% | 1 | 1 | 0% | 1,982 | 4,498 | +127% | 0 | 0 | — |
case-14 | pass→pass | 14,888 | 12,744 | -14% | 1 | 1 | 0% | 2,277 | 4,830 | +112% | 0 | 0 | — |
case-15 | pass→pass | 8,157 | 8,694 | +7% | 1 | 1 | 0% | 1,781 | 4,474 | +151% | 0 | 0 | — |
case-16 | pass→pass | 9,632 | 10,015 | +4% | 1 | 1 | 0% | 1,720 | 4,523 | +163% | 0 | 0 | — |
case-17 | pass→pass | 14,147 | 10,653 | -25% | 1 | 1 | 0% | 2,194 | 4,518 | +106% | 0 | 0 | — |
case-18 | pass→pass | 10,301 | 9,928 | -4% | 1 | 1 | 0% | 1,584 | 4,246 | +168% | 0 | 0 | — |
case-19 | pass→pass | 12,460 | 10,457 | -16% | 1 | 1 | 0% | 1,851 | 4,414 | +138% | 0 | 0 | — |
case-20 | pass→fail | 20,142 | 19,224 | -5% | 1 | 1 | 0% | 3,101 | 5,981 | +93% | 0 | 0 | — |
case-21 | pass→pass | 9,098 | 6,085 | -33% | 1 | 1 | 0% | 1,534 | 3,654 | +138% | 0 | 0 | — |
case-22 | pass→pass | 6,596 | 3,990 | -40% | 1 | 1 | 0% | 1,041 | 3,234 | +211% | 0 | 0 | — |
case-23 | pass→pass | 10,406 | 4,573 | -56% | 1 | 1 | 0% | 1,752 | 3,402 | +94% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 22 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of -50 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Other measured skills in the registry, with their headline benchmark lift.