μ = [0.1, 0.15, 0.12] # expected returns
Σ = [0.1 0.05 0.03;
0.05 0.12 0.04;
0.03 0.04 0.08] # covariances
n_assets = length(μ) # number of assets3
“Portfolio optimization is not about finding the perfect mix, but about managing risk and return in a way that aligns with your goals and circumstances.” — Unknown
This chapter introduces optimization in a portfolio context, illustrating how mathematical and computational techniques can be applied to the classic problem of allocating capital among different assets. We begin with the foundations of mean–variance optimization, describing how investors balance expected return and risk, and how these trade-offs can be expressed as a constrained optimization problem. The chapter then expands to cover a variety of real-world extensions and constraints, including risk-based capital, prior-posterior views and Sharpe ratios.
We will use a toy three-asset portfolio throughout the chapter. The vector μ contains annualized expected returns and Σ stores the covariance matrix implied by historical or modeled volatilities. Keeping the dataset intentionally small lets us focus on the optimization mechanics before scaling up to tens or hundreds of assets in practice.
μ = [0.1, 0.15, 0.12] # expected returns
Σ = [0.1 0.05 0.03;
0.05 0.12 0.04;
0.03 0.04 0.08] # covariances
n_assets = length(μ) # number of assets3
Harry Markowitz introduced the modern portfolio theory in 1952. The main idea is that investors are pursuing to maximize their expected return of a portfolio given a certain amount of risk. Under standard assumptions, achieving higher expected returns typically requires accepting higher risk; portfolios that simultaneously deliver higher return and lower risk are dominated and lie above the feasible set. The efficient frontier traces the best achievable trade-off.
Mean-variance optimization is a mathematical framework that seeks to maximize expected returns while minimizing portfolio variance (or standard deviation). It involves calculating the expected return and risk of individual assets and finding the optimal combination of assets to achieve the desired risk-return tradeoff.
\[ \begin{aligned} \text{minimize} \quad &{w}^{T}\Sigma{w}\\ \text{subject to} \quad &{r}^{T}{w}\geq{\mu}_{\text{target}}\\ &{1}^{T}{w}={1}\\ &{w}\geq{0} \end{aligned} \]
using JuMP, Ipopt, LinearAlgebra
# Create an optimization model
model = Model(Ipopt.Optimizer)
set_silent(model)
# Set up weights as variables to optimize
@variable(model, w[1:n_assets] >= 0.0)
# Objective: minimize portfolio variance
@objective(model, Min, dot(w, Σ * w))
# Constraints: Sum of portfolio weights should equal to 1, and all weights should be zero or positive
@constraint(model, sum(w) == 1.0)
# May also add additional constraints
# target_return = 0.1
# @constraint(model, dot(μ, w) >= target_return)
# Solve the optimization problem
optimize!(model)
w_meanvar = value.(w)
μ_meanvar = dot(μ, w_meanvar)
σ_meanvar = sqrt(dot(w_meanvar, Σ * w_meanvar))
# Print results
println("Optimal minimum mean-variance portfolio weights:")
for (i, wᵢ) in enumerate(w_meanvar)
println("Asset ", i, ": ", round(wᵢ, digits=4))
end
println("Portfolio mean optimized under mean-variance = ", round(μ_meanvar, digits = 4))
println("Portfolio standard deviation optimized under mean-variance = ", round(σ_meanvar, digits = 4))
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit https://github.com/coin-or/Ipopt
******************************************************************************
Optimal minimum mean-variance portfolio weights:
Asset 1: 0.3333
Asset 2: 0.1667
Asset 3: 0.5
Portfolio mean optimized under mean-variance = 0.1183
Portfolio standard deviation optimized under mean-variance = 0.238
In real-world settings you may impose lower/upper bounds, forbid short positions, or layer liquidity constraints, but the structure is identical. Once the baseline model is producing sensible weights, iterate on additional business rules.
In mean-variance portfolio optimization, incorporating a cost of risk-based capital on assets is a practical consideration that reflects the additional capital required to support riskier assets in a portfolio. This approach ensures that the optimization process not only maximizes returns relative to risk but also considers the regulatory or internal cost implications associated with holding riskier assets.
\[ \begin{aligned} \text{maximize} \quad &{w}^{T}R_{adj}\\ \text{subject to} \quad &{w}^{T}\Sigma{w}\le\sigma^2_{max}\\ &{1}^{T}{w}={1}\\ &{w}\geq{0} \end{aligned} \]
where \(R_{\text{adj}} = [(\mu_1 - \lambda_1), (\mu_2 - \lambda_2), \ldots, (\mu_N - \lambda_N)]\) are the risk-adjusted expected returns.
using JuMP, Ipopt, LinearAlgebra
# Create an optimization model
model = Model(Ipopt.Optimizer)
set_silent(model)
λ = [0.01, 0.02, 0.05] # RBC cost per asset
r = μ .- λ # risk adjusted returns
σ²_max = 0.1 # maximum portfolio variance
# Set up weights as variables to optimize
@variable(model, w[1:n_assets] >= 0.0)
# Objective: minimize portfolio variance
@objective(model, Max, dot(r, w))
# Constraints: Sum of portfolio weights should equal to 1, and all weights should be zero or positive
@constraint(model, sum(w) == 1.0)
# Constraints: Sum of allowable portfolio variance is limited
@constraint(model, dot(w, Σ * w) <= σ²_max)
# May also add additional constraints
# target_return = 0.1
# @constraint(model, dot(μ, w) >= target_return)
# Solve the optimization problem
optimize!(model)
w_rbc = value.(w)
μ_rbc = dot(μ, w_rbc)
σ_rbc = sqrt(dot(w_rbc, Σ * w_rbc))
# Print results
println("Portfolio weights with RBC-adjusted returns:")
for (i, wᵢ) in enumerate(w_rbc)
println("Asset ", i, ": ", round(wᵢ, digits=4))
end
println("Portfolio mean optimized under RBC = ", round(μ_rbc, digits = 4))
println("Portfolio standard deviation optimized under RBC = ", round(σ_rbc, digits = 4))Portfolio weights with RBC-adjusted returns:
Asset 1: 0.1667
Asset 2: 0.8333
Asset 3: 0.0
Portfolio mean optimized under RBC = 0.1417
Portfolio standard deviation optimized under RBC = 0.3162
Capital charges effectively haircut the expected return of risky assets. In this simple illustration the optimizer shifts weight toward the lower-cost asset once the variance constraint is binding. In a production setting you would source the adjustments from your solvency framework (for example, regulatory RBC factors or internal economic capital) and consider separate limits for issuer, sector, or region exposures.
The efficient frontier represents the set of portfolios that offer the highest expected return for a given level of risk or the lowest risk for a given level of return. Efficient frontier analysis involves plotting risk-return combinations for different portfolios and identifying the optimal portfolio on the frontier.
using JuMP, Ipopt, CairoMakie, LinearAlgebra
import MathOptInterface as MOI
function efficient_frontier(μ, Σ; points::Int = 100)
targets = range(minimum(μ), maximum(μ), length = points)
frontier = Tuple{Float64, Float64}[]
for target in targets
model = Model(Ipopt.Optimizer)
set_silent(model)
n = length(μ)
@variable(model, w[1:n] >= 0.0)
@objective(model, Min, dot(w, Σ * w))
@constraint(model, sum(w) == 1.0)
@constraint(model, dot(μ, w) == target)
optimize!(model)
status = termination_status(model)
if status in (MOI.LOCALLY_SOLVED, MOI.OPTIMAL)
push!(frontier, (sqrt(objective_value(model)), target))
end
end
frontier
end
ef = efficient_frontier(μ, Σ)
# Plot Efficient Frontier
fig = Figure()
Axis(fig[1, 1], xlabel = "Portfolio Volatility (σ)", ylabel = "Expected Return (μ)")
lines!(first.(ef), last.(ef), color = :dodgerblue, linewidth = 2)
figTracing the efficient frontier is useful for communicating trade-offs to stakeholders. Each point corresponds to a distinct target return; the efficient_frontier helper simply sweeps a grid of targets and resolves the mean-variance programme. In client reporting this visual is usually overlaid with realised portfolios, policy benchmarks, and stress scenarios.
The Black-Litterman model combines the views of investors with market equilibrium assumptions to generate optimal portfolios. It starts with a market equilibrium portfolio and adjusts it based on investor views and confidence levels. The model incorporates subjective opinions while maintaining diversification and risk management principles.
\[ \begin{aligned} \text{maximize} \quad & \mu_{BL}^{T} w \;-\; \frac{\lambda}{2}\, w^{T} \Sigma \, w \\ \text{subject to} \quad & \sum_{i=1}^{N} w_i = 1 \\ & w_i \geq 0, \quad \forall i \\ \text{where} \mu_{BL} = \\ \Big( (\tau \Sigma)^{-1} + P^{T} \Omega^{-1} P \Big)^{-1} \Big( (\tau \Sigma)^{-1} \mu + P^{T} \Omega^{-1} Q \Big) \end{aligned} \]
\(\mu\) = prior (implied) returns from the market equilibrium. \(P\) = matrix encoding which assets your views are on. \(Q\) = your views (expected returns for specific portfolios). \(\Omega\) = uncertainty (covariance) of your views. \(\tau\) = scaling factor for the prior covariance (usually small, like 0.025).
using JuMP, Ipopt, LinearAlgebra
# Market equilibrium parameters (prior)
λ = 2.5 # risk aversion
μ_market = fill(0.08, n_assets) # Market equilibrium return
Σ_prior = Σ # Market equilibrium covariance matrix
# Investor views
Q = μ # Expected returns on assets according to investor views
P = Matrix(I, n_assets, n_assets) # Pick matrix specifying which assets views are on
Ω = Diagonal([1e-6, 4e-6, 9e-6]) # Views uncertainty (covariance matrix)
τ = 0.05 # Scaling factor
# Black-Litterman expected return adjustment
Σ_inv = inv(Σ_prior)
# Calculate the posterior expected returns
A = Σ_inv / τ + P' * (Ω \ P)
B = Σ_inv * μ_market / τ + P' * (Ω \ Q)
μ_BL = A \ B
# Create an optimization model
model = Model(Ipopt.Optimizer)
set_silent(model)
# Set up weights as variables to optimize
@variable(model, w[1:n_assets] >= 0.0)
# Objective: maximize sharpe ratio
@objective(model, Max, dot(μ_BL, w) - (λ / 2) * dot(w, Σ_prior * w))
# Constraints: Sum of portfolio weights should equal to 1, and all weights should be zero or positive
@constraint(model, sum(w) == 1.0)
# Solve the optimization problem
optimize!(model)
w_BL = value.(w)
μ_BLpost = dot(μ_BL, w_BL)
σ_BLpost = sqrt(dot(w_BL, Σ_prior * w_BL))
# Print results
println("Portfolio standard deviation optimized under Black-Litterman = ", round(σ_BLpost, digits = 4))
for (i, wᵢ) in enumerate(w_BL)
println("Asset ", i, ": ", round(wᵢ, digits=4))
end
println("Portfolio Weights optimized under Black-Litterman:")
println("Portfolio mean optimized under Black-Litterman = ", round(μ_BLpost, digits = 4))Portfolio standard deviation optimized under Black-Litterman = 0.245
Asset 1: 0.178
Asset 2: 0.3444
Asset 3: 0.4776
Portfolio Weights optimized under Black-Litterman:
Portfolio mean optimized under Black-Litterman = 0.1267
Black-Litterman provides a disciplined way to blend equilibrium returns (for instance, those implied by a global CAP-weighted index) with discretionary views. The prior acts as an anchor; the pick matrix \(P\), the view vector \(Q\), and the confidence matrix \(\Omega\) tilt the portfolio toward convictions. In practice you would calibrate \(\tau\) and the \(\Omega\) diagonal entries so that strong views meaningfully shift allocations while weak views barely move the dial.
Risk parity is an asset allocation strategy that allocates capital based on risk rather than traditional measures such as market capitalization or asset prices. It aims to balance risk contributions across different assets or asset classes to achieve a more stable portfolio. Risk parity portfolios often include assets with different risk profiles, such as stocks, bonds, and commodities.
\[ \begin{aligned} \min_{w \in \mathbb{R}^N} \quad & \sum_{i=1}^{N} \left( RC_i - t \right)^2 \\ \text{subject to} \quad & \sum_{i=1}^{N} w_i = 1, \\ & w_i \ge 0, \quad i = 1, \dots, N \\ \text{where} \quad RC_i = \frac{w_i (\Sigma w)_i}{w^{T} \Sigma w + \epsilon} \end{aligned} \]
using JuMP, Ipopt, LinearAlgebra
# Create an optimization model
model = Model(Ipopt.Optimizer)
set_silent(model)
# Set up weights as variables to optimize
@variable(model, w[1:n_assets] >= 0.0)
# Objective: minimize portfolio variance
@expression(model, Σw[i=1:n_assets], sum(Σ[i, j] * w[j] for j in 1:n_assets))
@expression(model, var_p, sum(w[i] * Σw[i] for i in 1:n_assets))
@expression(model, RC[i=1:n_assets], (w[i] * Σw[i]) / (var_p + eps()))
@expression(model, target_contribution, var_p / n_assets)
@objective(model, Min, sum((RC[i] - target_contribution)^2 for i in 1:n_assets))
# Constraints: Sum of portfolio weights should equal to 1, and all weights should be zero or positive
@constraint(model, sum(w) == 1.0)
# Solve the optimization problem
optimize!(model)
w_rp = value.(w)
Σw_val = Σ * w_rp
rc = w_rp .* Σw_val
rc_share = rc ./ sum(rc)
# Print results
println("Optimal portfolio weights under risk parity:")
for (i, wᵢ) in enumerate(w_rp)
println("Asset ", i, ": ", round(wᵢ, digits=4))
println("RC share ", i, ": ", round(rc_share[i], digits=4))
endOptimal portfolio weights under risk parity:
Asset 1: 0.3309
RC share 1: 0.3334
Asset 2: 0.2951
RC share 2: 0.3369
Asset 3: 0.3741
RC share 3: 0.3297
Each asset contributes one third of total portfolio variance at the optimum, a hallmark of risk parity construction. Compared with the minimum-variance allocation, the solution spreads weight more evenly because it penalizes large disparities in marginal risk contributions.
The helper eps() in the formulation avoids dividing by zero when the portfolio variance is extremely small. In production code you would replace this with an explicit numerical tolerance.
Robust optimization techniques aim to create portfolios that are resilient to uncertainties and fluctuations in market conditions. These techniques consider a range of possible scenarios and optimize portfolios to perform well across different market environments. A robust parameter in robust portfolio optimization is typically chosen to ensure the portfolio’s performance remains stable and satisfactory under different market conditions or variations in input data. Robust optimization may involve incorporating stress tests, scenario analysis, or robust risk measures into the portfolio construction process.
\[ \begin{aligned} \text{minimize} \quad & w^T \Sigma w + \gamma \|w - w_0\|_2^2 \\ \text{subject to} \quad & \sum_{i=1}^{N} w_i = 1 \\ & w_i \geq 0, \quad \forall i \\ & \|(\Sigma^{1/2} (w - w_0))\|_2 \leq \epsilon \end{aligned} \]
using JuMP, Ipopt, LinearAlgebra
# Create an optimization model
model = Model(Ipopt.Optimizer)
set_silent(model)
# Set up weights as variables to optimize
@variable(model, w[1:n_assets] >= 0.0)
# Objective: minimize portfolio variance
ε = 0.05 # Uncertainty level
γ = 0.1 # Robustness parameter
w₀ = [0.3, 0.4, 0.3] # expected weights
@objective(model, Min, dot(w, Σ * w) + γ * sum((w[i] - w₀[i])^2 for i in 1:n_assets))
# Constraints: Sum of portfolio weights should equal to 1, and all weights should be zero or positive
@constraint(model, sum(w) == 1)
@constraint(model, sum(((w[i] - w₀[i]) * Σ[i, j] * (w[j] - w₀[j])) for i in 1:n_assets, j in 1:n_assets) <= ε)
# Solve the optimization problem
optimize!(model)
w_robust = value.(w)
μ_robust = dot(μ, w_robust)
σ_robust = sqrt(dot(w_robust, Σ * w_robust))
# Print results
println("Portfolio Weights under robust optimization:")
for (i, wᵢ) in enumerate(w_robust)
println("Asset ", i, ": ", round(wᵢ, digits=4))
end
println("Portfolio mean under robust optimization = ", round(μ_robust, digits = 4))
println("Portfolio standard deviation under robust optimization = ", round(σ_robust, digits = 4))Portfolio Weights under robust optimization:
Asset 1: 0.3125
Asset 2: 0.3125
Asset 3: 0.375
Portfolio mean under robust optimization = 0.1231
Portfolio standard deviation under robust optimization = 0.2427
Robust optimization frameworks vary from simple shrinkage toward a reference portfolio (as shown here) to full-fledged worst-case analysis across an uncertainty set. They are especially helpful when market regimes shift quickly or when return estimates are notoriously noisy, as is common with alternative assets and private markets.
| Methodology | Asset weights |
|---|---|
| Standard mean variance | [0.33, 0.17, 0.50] |
| (with RBC costs) | [0.17, 0.83, 0.00] |
| Black-Litterman | [0.18, 0.34, 0.48] |
| Risk parity | [0.33, 0.30, 0.37] |
| Sharpe ratio | [0.02, 0.53, 0.45] |
| Robust | [0.31, 0.31, 0.38] |
Comparing the allocations highlights how each methodology encodes a different business question. The RBC-adjusted run rotates capital toward the asset with the lowest capital charge even though it is not the highest-return instrument. The Sharpe maximizer hugs the asset with the best risk-adjusted profile, while risk parity and robust optimization deliberately keep weights closer to equal to avoid concentration risk.
In traditional portfolio optimization, fractional purchases of assets refer to the ability to allocate fractions or percentages of capital to individual assets. However, in certain contexts or practical implementations, fractional purchases may not be allowed or considered.
Practical constraints. Some investment vehicles or platforms may restrict investors from purchasing fractions of shares or assets. For instance, certain mutual funds, exchange-traded funds (ETFs), or other investment products may require whole units of shares to be purchased.
Simplicity and cost-effectiveness. Handling fractional shares can add complexity and operational costs to portfolio management, especially in terms of transaction fees, administrative overhead, and reconciliation processes.
Market liquidity. Some assets may have limited liquidity or trading volumes, making it impractical or difficult to execute fractional purchases without significantly impacting market prices or transaction costs.
Regulatory considerations. Regulations in certain jurisdictions may impose restrictions on fractional share trading or ownership, potentially limiting the ability to include fractional purchases in portfolio optimization strategies.
In portfolio optimization, a penalty factor for a large volume of assets typically refers to a mechanism or adjustment applied to the optimization process to mitigate the potential biases or challenges that arise when dealing with a large number of assets. This concept is particularly relevant in the context of mean-variance optimization and other optimization frameworks where computational efficiency and practical portfolio management considerations come into play. Too many assets may have the following issues.
Dimensionality. As the number of assets (or dimensions) increases in a portfolio, traditional optimization methods may become computationally intensive or prone to overfitting. This is because the complexity of the optimization problem grows exponentially with the number of assets.
Sparsity and concentration. In practice, not all assets may contribute equally to portfolio performance. Some assets may have negligible impact on the overall portfolio characteristics (such as risk or return) due to low weights or correlations with other assets.
Penalizing excessive complexity. A penalty factor can be introduced to penalize portfolios that overly diversify or allocate small weights to a large number of assets. This encourages the optimization process to focus on more significant assets or reduce the complexity of the portfolio structure.
There are various ways to implement a penalty factor for a large volume of assets: