Skip to main content 홈 크리에이터 xjtulyc awesome-rosetta-skills portfolio-optimization
portfolio-optimization Use this Skill to build optimal portfolios: mean-variance frontier, minimum variance, maximum Sharpe, Black-Litterman views, and risk parity with PyPortfolioOpt.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill portfolio-optimization명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... name portfolio-optimization description Use this Skill to build optimal portfolios: mean-variance frontier, minimum variance, maximum Sharpe, Black-Litterman views, and risk parity with PyPortfolioOpt.
tags ["finance","portfolio-optimization","mean-variance","Black-Litterman","risk-parity"] version 1.0.0 authors [{"name":"awesome-rosetta-skills contributors","github":"@xjtulyc"}] license MIT platforms ["claude-code","codex","gemini-cli","cursor"] dependencies {"python":["PyPortfolioOpt>=1.5","cvxpy>=1.4","pandas>=1.5","numpy>=1.23","matplotlib>=3.6","scipy>=1.9"]} last_updated 2026-03-18 status stable
Portfolio Optimization
TL;DR — Construct the mean-variance efficient frontier, find minimum-variance
and maximum-Sharpe portfolios, incorporate investor views via Black-Litterman,
and build risk parity portfolios using PyPortfolioOpt and CVXPY.
When to Use
Situation Recommended Method Allocate across risky assets, minimize variance at target return Mean-variance optimization Minimize variance without return target Global Minimum Variance portfolio Maximize risk-adjusted return (Sharpe ratio) Tangency / Max-Sharpe portfolio Incorporate subjective market views Black-Litterman model Equal risk contribution across assets Risk Parity Realistic portfolio with turnover constraints Transaction cost-aware MVO
Background
Mean-Variance Optimization (Markowitz 1952)
Given expected returns μ (k×1) and covariance matrix Σ (k×k), the efficient frontier
is traced by solving for each target return μ*:
min w' Σ w
s.t. w' μ = μ*
w' 1 = 1
w ≥ 0 (long-only)
Global Minimum Variance (GMV) : Drop the return constraint; minimize variance only.
Maximum Sharpe (Tangency) : max (w' μ - r_f) / √(w' Σ w), equivalent to
a convex problem after transformation.
Shrinkage Estimators
Sample covariance matrix is ill-conditioned with many assets. Ledoit-Wolf (2004)
shrinkage estimator:
Σ̂_LW = (1 - α) Σ_sample + α μ_LW I
reduces estimation error. PyPortfolioOpt wraps sklearn's LedoitWolf.
Black-Litterman Model (1990/1992)
Combines the market equilibrium prior (from CAPM reverse optimization) with
investor views:
Prior : Π = δ Σ w_mkt (implied equilibrium returns)
Views : P Q with uncertainty Ω (diagonal)
Posterior : μ_BL = [(τΣ)^{-1} + P' Ω^{-1} P]^{-1} [(τΣ)^{-1} Π + P' Ω^{-1} Q]
where τ is a scalar (uncertainty in the prior, typically 0.05–0.25).
Risk Parity / Equal Risk Contribution
The risk contribution of asset i is:
RC_i = w_i × (∂σ_p / ∂w_i) = w_i × (Σ w)_i / σ_p
Equal Risk Contribution (ERC): solve w* such that RC_i = σ_p / k for all i.
This is a nonlinear optimization, solvable via CVXPY or scipy.
Environment Setup conda create -n portopt python=3.11 -y
conda activate portopt
pip install PyPortfolioOpt>=1.5 cvxpy>=1.4 pandas>=1.5 numpy>=1.23 matplotlib>=3.6 scipy>=1.9
python -c "from pypfopt import EfficientFrontier; print('PyPortfolioOpt OK')"
python -c "import cvxpy as cp; print('CVXPY', cp.__version__)"
Core Workflow
Step 1 — Efficient Frontier and Optimal Portfolios import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pypfopt import EfficientFrontier, risk_models, expected_returns
from pypfopt.plotting import plot_efficient_frontier
import warnings
np.random.seed(42 )
def simulate_returns (
n_assets: int = 10 ,
n_periods: int = 252 * 5 ,
annualize: bool = False ,
) -> pd.DataFrame:
"""
Simulate daily asset returns with a factor structure.
Args:
n_assets: Number of risky assets.
n_periods: Number of daily return observations.
annualize: If True, return annualized returns (252-day).
Returns:
DataFrame of daily returns (n_periods × n_assets).
"""
beta = np.random.uniform(0.5 , 1.5 , n_assets)
alpha = np.random.uniform(-0.0002 , 0.0005 , n_assets)
market = np.random.normal(0.0004 , 0.01 , n_periods)
idio = np.random.normal(0 , 0.008 , (n_periods, n_assets))
returns_raw = alpha + np.outer(market, beta) + idio
cols = [f"Asset{i+1 } " for i in range (n_assets)]
df = pd.DataFrame(returns_raw, columns=cols)
df.index = pd.date_range("2020-01-01" , periods=n_periods, freq="B" )
return df
def plot_efficient_frontier_custom (
returns_df: pd.DataFrame,
risk_free_rate: float = 0.02 ,
n_points: int = 100 ,
output_path: str = None ,
) -> dict :
"""
Compute and plot the efficient frontier with key portfolio markers.
Args:
returns_df: Daily returns DataFrame (T × k).
risk_free_rate: Annual risk-free rate for Sharpe ratio.
n_points: Number of frontier points.
output_path: If provided, save the plot.
Returns:
Dictionary with keys: mu, S, min_vol_weights, max_sharpe_weights,
min_vol_perf, max_sharpe_perf.
"""
mu = expected_returns.mean_historical_return(returns_df)
S = risk_models.ledoit_wolf(returns_df)
ef_frontier_points = []
min_ret = float (mu.min ()) * 0.5
max_ret = float (mu.max ()) * 0.95
for target_ret in np.linspace(min_ret, max_ret, n_points):
try :
ef = EfficientFrontier(mu, S)
ef.efficient_return(target_ret, market_neutral=False )
perf = ef.portfolio_performance(risk_free_rate=risk_free_rate / 252 )
ef_frontier_points.append({"sigma" : perf[1 ], "mu" : perf[0 ]})
except Exception:
pass
ef_df = pd.DataFrame(ef_frontier_points)
ef_gmv = EfficientFrontier(mu, S)
ef_gmv.min_volatility()
gmv_weights = ef_gmv.clean_weights()
gmv_perf = ef_gmv.portfolio_performance(verbose=False , risk_free_rate=risk_free_rate / 252 )
ef_ms = EfficientFrontier(mu, S)
ef_ms.max_sharpe(risk_free_rate=risk_free_rate / 252 )
ms_weights = ef_ms.clean_weights()
ms_perf = ef_ms.portfolio_performance(verbose=False , risk_free_rate=risk_free_rate / 252 )
print (f"GMV Portfolio: return={gmv_perf[0 ]*252 :.4 f} vol={gmv_perf[1 ]*np.sqrt(252 ):.4 f} Sharpe={gmv_perf[2 ]:.4 f} " )
print (f"Max Sharpe: return={ms_perf[0 ]*252 :.4 f} vol={ms_perf[1 ]*np.sqrt(252 ):.4 f} Sharpe={ms_perf[2 ]:.4 f} " )
fig, ax = plt.subplots(figsize=(10 , 6 ))
ax.plot(ef_df["sigma" ] * np.sqrt(252 ), ef_df["mu" ] * 252 ,
color="#2980B9" , linewidth=2 , label="Efficient Frontier" )
ax.scatter(gmv_perf[1 ] * np.sqrt(252 ), gmv_perf[0 ] * 252 ,
marker="*" , s=200 , color="#2ECC71" , zorder=5 , label="Min Variance" )
ax.scatter(ms_perf[1 ] * np.sqrt(252 ), ms_perf[0 ] * 252 ,
marker="*" , s=200 , color="#E74C3C" , zorder=5 , label="Max Sharpe" )
for asset in returns_df.columns:
asset_vol = float (returns_df[asset].std() * np.sqrt(252 ))
asset_ret = float (mu[asset] * 252 )
ax.scatter(asset_vol, asset_ret, color="gray" , alpha=0.6 , s=40 )
ax.annotate(asset, (asset_vol, asset_ret), textcoords="offset points" ,
xytext=(3 , 3 ), fontsize=7 )
ax.set_xlabel("Annualized Volatility" )
ax.set_ylabel("Annualized Return" )
ax.set_title("Mean-Variance Efficient Frontier" )
ax.legend()
fig.tight_layout()
if output_path:
fig.savefig(output_path, dpi=150 )
print (f"Saved frontier plot to {output_path} " )
plt.show()
return {"mu" : mu, "S" : S,
"min_vol_weights" : gmv_weights, "max_sharpe_weights" : ms_weights,
"min_vol_perf" : gmv_perf, "max_sharpe_perf" : ms_perf}
Step 2 — Black-Litterman with Investor Views from pypfopt.black_litterman import BlackLittermanModel, market_implied_risk_aversion
from pypfopt.expected_returns import ema_historical_return
def black_litterman_optimization (
returns_df: pd.DataFrame,
market_caps: pd.Series = None ,
views_dict: dict = None ,
view_confidences: list = None ,
tau: float = 0.05 ,
risk_free_rate: float = 0.02 ,
output_path: str = None ,
) -> dict :
"""
Black-Litterman model: combine equilibrium with investor views.
Args:
returns_df: Daily returns DataFrame.
market_caps: Market capitalizations for each asset (for prior weights).
If None, equal weights are used.
views_dict: Dictionary of views: {asset_name: expected_return}.
Example: {"Asset1": 0.10, "Asset3": 0.05}
view_confidences: Confidence level (0–1) for each view. Default: 0.5 each.
tau: Uncertainty in the prior (typically 0.01–0.25).
risk_free_rate: Annual risk-free rate.
output_path: If provided, save weight comparison bar chart.
Returns:
Dictionary with bl_returns, bl_weights, bl_performance.
"""
n = len (returns_df.columns)
S = risk_models.ledoit_wolf(returns_df)
if market_caps is None :
market_caps = pd.Series(np.ones(n), index=returns_df.columns)
market_weights = market_caps / market_caps.sum ()
delta = market_implied_risk_aversion(returns_df.mean() * 252 , market_weights, S)
pi = BlackLittermanModel.market_implied_prior_returns(market_weights, delta, S)
if views_dict is None :
assets = returns_df.columns.tolist()
views_dict = {assets[0 ]: float (pi[assets[0 ]]) + 0.03 ,
assets[1 ]: float (pi[assets[1 ]]) - 0.02 }
if view_confidences is None :
view_confidences = [0.5 ] * len (views_dict)
view_assets = list (views_dict.keys())
P = np.zeros((len (view_assets), n))
Q = np.zeros(len (view_assets))
assets_list = returns_df.columns.tolist()
for i, asset in enumerate (view_assets):
P[i, assets_list.index(asset)] = 1.0
Q[i] = views_dict[asset]
variances = np.diag(P @ S.values @ P.T)
omega = np.diag(variances * (1 / np.array(view_confidences) - 1 ))
bl = BlackLittermanModel(S, pi=pi, P=P, Q=Q, omega=omega, tau=tau)
bl_returns = bl.bl_returns()
ef_bl = EfficientFrontier(bl_returns, S)
ef_bl.max_sharpe(risk_free_rate=risk_free_rate / 252 )
bl_weights = ef_bl.clean_weights()
bl_perf = ef_bl.portfolio_performance(verbose=False , risk_free_rate=risk_free_rate / 252 )
print (f"\nBlack-Litterman Max-Sharpe Portfolio:" )
for asset, w in bl_weights.items():
if w > 0.01 :
print (f" {asset} : {w:.4 f} " )
print (f"Return: {bl_perf[0 ]*252 :.4 f} Vol: {bl_perf[1 ]*np.sqrt(252 ):.4 f} Sharpe: {bl_perf[2 ]:.4 f} " )
if output_path:
fig, ax = plt.subplots(figsize=(10 , 5 ))
x = np.arange(n)
width = 0.35
ax.bar(x - width/2 , pi.values * 252 , width, label="Prior (Equilibrium)" , color="#AED6F1" )
ax.bar(x + width/2 , bl_returns.values * 252 , width, label="BL Posterior" , color="#2980B9" )
ax.set_xticks(x)
ax.set_xticklabels(returns_df.columns, rotation=45 )
ax.set_ylabel("Expected Return (annual)" )
ax.set_title("Black-Litterman: Prior vs Posterior Expected Returns" )
ax.legend()
fig.tight_layout()
fig.savefig(output_path, dpi=150 )
print (f"Saved BL return comparison to {output_path} " )
return {"bl_returns" : bl_returns, "bl_weights" : bl_weights, "bl_performance" : bl_perf}
Step 3 — Risk Parity via CVXPY import cvxpy as cp
from scipy.optimize import minimize
def risk_parity_portfolio (
S: pd.DataFrame,
output_path: str = None ,
) -> dict :
"""
Equal Risk Contribution (risk parity) portfolio via scipy optimization.
Each asset contributes equally to total portfolio volatility.
Args:
S: Covariance matrix (pd.DataFrame or np.ndarray).
output_path: If provided, save weight comparison plot.
Returns:
Dictionary with keys: weights, risk_contributions, portfolio_vol.
"""
Sigma = S.values if isinstance (S, pd.DataFrame) else S
n = Sigma.shape[0 ]
asset_names = list (S.index) if isinstance (S, pd.DataFrame) else [f"A{i+1 } " for i in range (n)]
def risk_parity_objective (w ):
w = np.abs (w) / np.sum (np.abs (w))
sigma_p = np.sqrt(w @ Sigma @ w)
marginal_rc = Sigma @ w
rc = w * marginal_rc / sigma_p
target_rc = np.ones(n) / n
return float (np.sum ((rc / sigma_p - target_rc) ** 2 ))
w0 = np.ones(n) / n
constraints = [{"type" : "eq" , "fun" : lambda w: np.sum (w) - 1 }]
bounds = [(0.0 , 1.0 )] * n
result = minimize(risk_parity_objective, w0, method="SLSQP" ,
bounds=bounds, constraints=constraints,
options={"ftol" : 1e-12 , "maxiter" : 5000 })
w_rp = np.abs (result.x) / np.sum (np.abs (result.x))
sigma_p = float (np.sqrt(w_rp @ Sigma @ w_rp))
marginal_rc = Sigma @ w_rp
rc = w_rp * marginal_rc / sigma_p
print ("\nRisk Parity Portfolio:" )
for name, wt, rci in zip (asset_names, w_rp, rc):
print (f" {name} : weight={wt:.4 f} RC={rci:.4 f} ({rci/sigma_p:.1 %} of vol)" )
print (f"Portfolio volatility: {sigma_p:.4 f} " )
w_ew = np.ones(n) / n
sigma_ew = float (np.sqrt(w_ew @ Sigma @ w_ew))
if output_path:
fig, ax = plt.subplots(figsize=(10 , 5 ))
x = np.arange(n)
width = 0.35
ax.bar(x - width/2 , w_ew, width, label="Equal Weight" , color="#AED6F1" )
ax.bar(x + width/2 , w_rp, width, label="Risk Parity" , color="#2ECC71" )
ax.set_xticks(x)
ax.set_xticklabels(asset_names, rotation=45 )
ax.set_ylabel("Portfolio Weight" )
ax.set_title("Equal Weight vs Risk Parity Allocation" )
ax.legend()
fig.tight_layout()
fig.savefig(output_path, dpi=150 )
print (f"Saved risk parity comparison to {output_path} " )
return {"weights" : dict (zip (asset_names, w_rp)),
"risk_contributions" : dict (zip (asset_names, rc)),
"portfolio_vol" : sigma_p,
"equal_weight_vol" : sigma_ew}
Advanced Usage
Rolling Rebalancing Backtest def rolling_rebalance_backtest (
returns_df: pd.DataFrame,
method: str = "max_sharpe" ,
lookback: int = 252 ,
rebalance_freq: int = 63 ,
risk_free_rate: float = 0.02 ,
transaction_cost: float = 0.001 ,
) -> pd.DataFrame:
"""
Walk-forward portfolio backtest with periodic rebalancing.
Args:
returns_df: Daily returns (T × k).
method: 'max_sharpe', 'min_vol', or 'equal_weight'.
lookback: Estimation window in trading days.
rebalance_freq: Rebalancing frequency in trading days.
risk_free_rate: Annual risk-free rate.
transaction_cost: One-way cost per unit of weight change.
Returns:
DataFrame with columns: date, portfolio_return, cumulative_return, weights.
"""
T, k = returns_df.shape
assets = returns_df.columns.tolist()
portfolio_returns = []
current_weights = np.ones(k) / k
rebalance_dates = list (range (lookback, T, rebalance_freq))
for t in range (lookback, T):
if t in rebalance_dates:
hist = returns_df.iloc[t - lookback:t]
try :
mu = expected_returns.mean_historical_return(hist)
S = risk_models.ledoit_wolf(hist)
ef = EfficientFrontier(mu, S)
if method == "max_sharpe" :
ef.max_sharpe(risk_free_rate=risk_free_rate / 252 )
elif method == "min_vol" :
ef.min_volatility()
else :
new_weights = np.ones(k) / k
portfolio_returns.append({
"date" : returns_df.index[t],
"portfolio_return" : float (returns_df.iloc[t].values @ current_weights),
})
current_weights = new_weights
continue
w_dict = ef.clean_weights()
new_weights = np.array([w_dict.get(a, 0 ) for a in assets])
except Exception:
new_weights = current_weights.copy()
turnover = np.sum (np.abs (new_weights - current_weights))
tc = turnover * transaction_cost
current_weights = new_weights
else :
tc = 0.0
daily_ret = float (returns_df.iloc[t].values @ current_weights) - tc
portfolio_returns.append({"date" : returns_df.index[t], "portfolio_return" : daily_ret})
port_df = pd.DataFrame(portfolio_returns)
port_df["cumulative_return" ] = (1 + port_df["portfolio_return" ]).cumprod()
return port_df
Troubleshooting Error / Issue Cause Resolution OptimizationError: Optimization failedExpected return out of frontier Reduce target return or use efficient_risk() GMV weights all in 1 asset Near-singular covariance matrix Use Ledoit-Wolf shrinkage instead of sample covariance Black-Litterman posterior = prior Views matrix P rank-deficient Check P dimensions match (n_views × n_assets) Risk parity optimizer diverges Very correlated assets Add small ridge regularization: Σ += 1e-4 × I Negative weights after clean_weights() Rounding issue Set clean_weights(cutoff=1e-4) Covariance matrix not PSD Short history or collinear assets Use risk_models.fix_nonpositive_semidefinite(S)
External Resources
Markowitz, H. (1952). "Portfolio Selection." Journal of Finance , 7(1), 77–91.
Black, F., Litterman, R. (1992). "Global Portfolio Optimization." Financial Analysts Journal , 48(5), 28–43.
Ledoit, O., Wolf, M. (2004). "A Well-Conditioned Estimator for Large-Dimensional Covariance Matrices."
Journal of Multivariate Analysis , 88(2), 365–411.
Maillard, S., Roncalli, T., Teïletche, J. (2010). "On the Properties of Equally Weighted Risk
Contribution Portfolios." Journal of Portfolio Management , 36(4).
PyPortfolioOpt docs: https://pyportfolioopt.readthedocs.io/
CVXPY docs: https://www.cvxpy.org/
Examples
Example 1 — Efficient Frontier with Optimal Portfolios returns_df = simulate_returns(n_assets=10 , n_periods=1260 )
frontier_results = plot_efficient_frontier_custom(
returns_df, risk_free_rate=0.02 , output_path="efficient_frontier.png"
)
print ("\nMax Sharpe weights (top 5):" )
ms_w = frontier_results["max_sharpe_weights" ]
for asset, w in sorted (ms_w.items(), key=lambda x: -x[1 ])[:5 ]:
print (f" {asset} : {w:.4 f} " )
Example 2 — Black-Litterman and Risk Parity Comparison returns_df = simulate_returns(n_assets=8 , n_periods=1260 )
assets = returns_df.columns.tolist()
bl_result = black_litterman_optimization(
returns_df,
views_dict={assets[0 ]: 0.12 , assets[1 ]: 0.05 , assets[4 ]: 0.08 },
view_confidences=[0.8 , 0.5 , 0.6 ],
output_path="bl_returns.png" ,
)
S = risk_models.ledoit_wolf(returns_df)
rp_result = risk_parity_portfolio(S, output_path="risk_parity.png" )
bt_ms = rolling_rebalance_backtest(returns_df, method="max_sharpe" )
bt_ew = rolling_rebalance_backtest(returns_df, method="equal_weight" )
fig, ax = plt.subplots(figsize=(12 , 5 ))
ax.plot(bt_ms["date" ], bt_ms["cumulative_return" ], label="Max Sharpe" )
ax.plot(bt_ew["date" ], bt_ew["cumulative_return" ], label="Equal Weight" , linestyle="--" )
ax.set_title("Portfolio Backtest: Max Sharpe vs Equal Weight" )
ax.set_ylabel("Cumulative Return" )
ax.legend()
plt.tight_layout()
plt.savefig("backtest.png" , dpi=150 )
plt.show()
Changelog Version Date Change 1.0.0 2026-03-18 Initial release — MVO, GMV, Max Sharpe, BL, Risk Parity, backtest