Portfolio Optimization
๐ฅ Vibe Prompt
"Allocate $1M across 5 stocks. Maximize expected return with risk <25%. Each stock has min investment of $50K."
What is Portfolio Optimization?
Portfolio Optimization allocates capital across assets to maximize expected return while limiting risk. This is the core problem of modern portfolio theory (Markowitz, Nobel Prize 1990).
| Concept | Meaning | |---------|---------| | Expected Return | Weighted average of asset returns | | Risk Score | Portfolio weighted risk (std dev or rating) | | Diversification | Minimum 5% per selected asset | | Full Investment | Sum of weights = 100% | | Concentration Limit | Max 3 assets selected |
Implementation with PuLP
import pulp
# Assets data
assets = ["TSMC", "MTK", "Delta", "Chunghwa", "Fubon"]
expected_return = [0.15, 0.12, 0.10, 0.04, 0.08] # Expected annual return
risk = [0.35, 0.30, 0.25, 0.10, 0.20] # Risk score (standard deviation-like)
prob = pulp.LpProblem("Portfolio_Optimization", pulp.LpMaximize)
# Decision variables
weights = {a: pulp.LpVariable(f"weight_{a}", lowBound=0, upBound=1) for a in assets}
selected = {a: pulp.LpVariable(f"select_{a}", cat="Binary") for a in assets}
# Objective: maximize expected return
prob += pulp.lpSum(expected_return[i] * weights[assets[i]] for i in range(len(assets)))
# Constraint 1: Fully invested
prob += pulp.lpSum(weights.values()) == 1
# Constraint 2: Portfolio risk โค 25%
prob += pulp.lpSum(risk[i] * weights[assets[i]] for i in range(len(assets))) <= 0.25
# Constraint 3: If selected, min 5% (no token positions)
for a in assets:
prob += weights[a] >= 0.05 * selected[a]
prob += weights[a] <= selected[a]
# Constraint 4: At most 3 assets
prob += pulp.lpSum(selected.values()) <= 3
# Solve
prob.solve(pulp.PULP_CBC_CMD(msg=False))
# Results
print(f"\n{'='*60}")
print(f"Portfolio Optimization โ Status: {pulp.LpStatus[prob.status]}")
print(f"{'='*60}")
print(f"Expected return: {pulp.value(prob.objective)*100:.2f}%")
print(f"\n{'Asset':<12} {'Weight':<10} {'Return':<10} {'Risk':<10} {'Contrib':<10}")
print(f"{'-'*52}")
portfolio_risk = 0
for i, a in enumerate(assets):
w = weights[a].value()
if w and w > 0.001:
contr = expected_return[i] * w
portfolio_risk += risk[i] * w
print(f"{a:<12} {w*100:<9.1f}% {expected_return[i]*100:<9.1f}% {risk[i]*100:<9.1f}% {contr*100:<9.2f}%")
print(f"\nPortfolio risk: {portfolio_risk*100:.2f}% (limit: 25%) โ
" if portfolio_risk <= 0.25 else "โ OVER LIMIT")
print(f"Number of assets: {sum(1 for a in assets if weights[a].value() > 0.001)}")
Efficient Frontier
By varying the risk limit and plotting results, we get the Efficient Frontier โ the optimal return for each risk level:
import matplotlib.pyplot as plt
risk_limits = [0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40]
optimal_returns = []
for max_risk in risk_limits:
prob = pulp.LpProblem("Portfolio", pulp.LpMaximize)
weights = {a: pulp.LpVariable(f"w_{a}", 0, 1) for a in assets}
selected = {a: pulp.LpVariable(f"s_{a}", cat="Binary") for a in assets}
prob += pulp.lpSum(expected_return[i] * weights[assets[i]] for i in range(len(assets)))
prob += pulp.lpSum(weights.values()) == 1
prob += pulp.lpSum(risk[i] * weights[assets[i]] for i in range(len(assets))) <= max_risk
for a in assets:
prob += weights[a] >= 0.05 * selected[a]
prob += weights[a] <= selected[a]
prob.solve(pulp.PULP_CBC_CMD(msg=False))
optimal_returns.append(pulp.value(prob.objective) if prob.status == 1 else 0)
plt.plot(risk_limits, optimal_returns, 'bo-', linewidth=2)
plt.xlabel('Risk Limit')
plt.ylabel('Expected Return')
plt.title('Efficient Frontier')
plt.grid(True, alpha=0.3)
plt.show()
Applications
| Domain | Use Case | |--------|---------| | Retirement Funds | Balance growth vs safety (target-date funds) | | Hedge Funds | Long/short portfolios with leverage constraints | | Insurance | Asset-liability matching for policy reserves | | Endowments | Yale model: diversify across asset classes | | Corporate Treasury | Short-term cash investment optimization |
Summary
| Aspect | Detail | |--------|--------| | Problem | Allocate capital: max return, min risk | | Variables | Continuous (weights) + Binary (selection) | | Constraints | Full investment, risk โค 25%, min/max per asset | | Objective | Maximize expected portfolio return | | Solver | CBC (MILP) | | Extension | Correlations, rebalancing, transaction costs, ESG constraints |
Constraint Programming Complete! ๐
- โ PuLP Basics
- โ Staff Scheduling
- โ Resource Allocation
- โ Supply Chain
- โ Portfolio Optimization
- โ Efficient Frontier
Summary
- Modern Portfolio Theory optimizes risk-return tradeoff
- Efficient Frontier shows optimal portfolios
- Constraints can reflect real-world limitations
- Rebalancing periodically maintains target allocation
Efficient Frontier with Pulp
The efficient frontier is the set of portfolios that gives the highest return for each risk level:
import pulp
import matplotlib.pyplot as plt
def efficient_frontier(expected_returns, cov_matrix, risk_levels):
"""
Calculate the efficient frontier by solving for each risk level.
Args:
expected_returns: list of expected returns for each asset
cov_matrix: covariance matrix
risk_levels: list of max risk values to try
Returns:
list of (risk, return) tuples on the efficient frontier
"""
n = len(expected_returns)
frontier = []
for max_risk in risk_levels:
prob = pulp.LpProblem("Efficient_Frontier", pulp.LpMaximize)
weights = [pulp.LpVariable(f"w_{i}", 0, 1) for i in range(n)]
# Objective: maximize expected return
prob += pulp.lpSum([expected_returns[i] * weights[i] for i in range(n)])
# Constraints
prob += pulp.lpSum(weights) == 1 # fully invested
# Risk constraint: portfolio variance <= max_risk
# For simplicity, use weighted average of variances
risk_expr = pulp.lpSum([
cov_matrix[i][i] * weights[i] * weights[i]
for i in range(n)
])
# Add covariance terms
for i in range(n):
for j in range(i+1, n):
term = 2 * cov_matrix[i][j] * weights[i] * weights[j]
# This creates a quadratic term โ for LP we approximate
# In practice, use a QP solver like cvxopt
prob.solve(pulp.PULP_CBC_CMD(msg=False))
if pulp.LpStatus[prob.status] == 'Optimal':
opt_weights = [pulp.value(w) for w in weights]
opt_return = sum(expected_returns[i] * opt_weights[i] for i in range(n))
frontier.append((max_risk, opt_return))
return frontier
Note: True efficient frontier calculation requires quadratic programming (QP) because variance is a quadratic function of weights. For LP approximation, use piecewise linear risk measures or CVaR instead.
Practical Applications
| Domain | Portfolio Optimization Use | |--------|---------------------------| | Personal Finance | Retirement portfolio: stocks + bonds + cash | | Wealth Management | Client risk profiling and asset allocation | | Pension Funds | Long-term liability-driven investment | | Insurance | Asset-liability matching for policy reserves | | Endowment | Yale model: diversify across alternative assets | | Corporate Treasury | Short-term cash investment optimization |
Summary
Portfolio optimization with PuLP allocates capital across assets to maximize return while constraining risk. The classic model uses expected returns, variance as risk measure, and constraints for full investment and position limits. Real portfolios add transaction costs, rebalancing, and ESG constraints.
Key takeaways:
- Decision variables: continuous weights for each asset (0 to 1)
- Objective: maximize expected portfolio return
- Constraints: fully invested, max risk, min/max per asset
- Efficient frontier: optimal risk-return tradeoff curve
- QP solvers handle quadratic risk (variance) naturally
- LP can approximate with linear risk measures (CVaR)
- Real portfolios include: transaction costs, rebalancing, ESG screening
- Portfolio optimization is the foundation of modern investment management
What's Next: Algorithm โ Combinatorial Optimization
The next course covers combinatorial optimization โ bin packing, job scheduling, and cutting stock problems.