Solve a minimum-variance problem for each target return, then plot the resulting expected returns against portfolio volatility. With long-only weights that sum to one, the efficient targets run from the global minimum-variance portfolio's return to the highest expected asset return. A constrained optimiser handles the change in weights when an asset reaches zero allocation.
Find the part of the frontier that is efficient
Let be the vector of expected returns and the covariance matrix for the same horizon. Use as the objective, impose and , and add at each target . Start by solving without the target equality to get the global minimum-variance point. For a long-only portfolio with only a budget constraint, the upper return endpoint is because a weighted average cannot exceed its largest component.
Plot horizontally and vertically. Store the weights with each point so you can inspect whether the higher return relies on concentrated holdings or a small change in several allocations.
Three sleeves and two constraints to check
Use synthetic annual inputs for an equity sleeve, a bond sleeve and short bills. Their expected returns are 8%, 4% and 2.5%, and their volatilities are 18%, 8% and 2.5%. Assume a 0.20 correlation between equities and bonds and low positive correlations with bills. A 6% target requires at least 50% in equities because the other sleeves return at most 4%. Pairing 50% equities with 50% bonds achieves exactly 6% at about 10.55% volatility. The optimiser assigns bills zero weight at this target. The extra equities needed to fund bills would increase variance here. At the minimum-variance end, bills receive most of the weight, so the solution changes as the return constraint tightens.
When sweeping target returns, keep solver status with every point. Recompute and from the returned weights and check that they respect the requested target and constraints within tolerance. Connecting the dots from unsuccessful solves can make an attractive curve with infeasible points. A minimum-variance solution already dominates any target with less expected return and more risk. That is why the frontier for the long-only sweep starts at the global minimum-variance return.
Constrained optimisation in Python
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import LinearConstraint, minimize
mu = np.array([0.08, 0.04, 0.025])
vol = np.array([0.18, 0.08, 0.025])
corr = np.array([[1.0, 0.20, 0.05],
[0.20, 1.0, 0.10],
[0.05, 0.10, 1.0]])
cov = corr * np.outer(vol, vol)
ones = np.ones(mu.size)
def variance(weights):
return weights @ cov @ weights
def minimise(constraints, starting_weights):
result = minimize(
variance, starting_weights, jac=lambda w: 2 * cov @ w,
method="SLSQP", bounds=[(0, 1)] * mu.size,
constraints=constraints, options={"ftol": 1e-12},
)
if not result.success:
raise RuntimeError(result.message)
return result.x
minimum = minimise(LinearConstraint(ones, 1, 1), ones / mu.size)
minimum_return = mu @ minimum
highest = np.eye(mu.size)[mu.argmax()]
points = []
for fraction in np.linspace(0, 1, 41):
start = (1 - fraction) * minimum + fraction * highest
target = (1 - fraction) * minimum_return + fraction * mu.max()
limits = LinearConstraint(np.vstack([ones, mu]), [1, target], [1, target])
weights = minimise(limits, start)
points.append((np.sqrt(variance(weights)), mu @ weights))
risk, expected_return = 100 * np.asarray(points).T
plt.plot(risk, expected_return)
plt.scatter(100 * vol, 100 * mu, label="Individual sleeves")
plt.xlabel("Annual volatility (%)")
plt.ylabel("Expected annual return (%)")
plt.legend()
plt.show()