Trading Dev AcademyFree quant education

Free lesson · Portfolio construction

Optimise under realistic constraints

Open interactive lessonPractice calculationsExplore labs

Start with the idea

An optimiser formalises trade-offs; it does not improve the quality of its inputs automatically. Costs discourage unnecessary trades, constraints express the mandate, and shrinkage reduces sensitivity to noisy estimates.

Symbols, units & horizon
  • w: vector of signed portfolio weights
  • μ: expected excess-return vector
  • Σ: return covariance matrix
  • λ: positive risk aversion
  • κ: nonnegative turnover penalty
  • w_pre: pretrade weights after price drift
  • || ||₁: sum of absolute vector entries
  • 1: vector of ones
  • n₀: required net exposure
  • G: gross-exposure limit
  • lᵢ,uᵢ: lower and upper asset bounds
  • S: sample covariance matrix
  • F: shrinkage target matrix
  • δ: fraction assigned to target, between 0 and 1
  • Σ̂: blended estimate

When and why to use this

Use constrained optimisation to convert multiple forecasts into a risk-budgeted book and compare the result against simple allocations.

maxw⁡μ𝖳w−λ2w𝖳Σw−κ‖w−wpre‖1
Differentiation + subgradient reasoning

Differentiate the smooth portfolio objective

  1. Ignoring turnover temporarily, gradient of μTw−λwTΣw2 is μ−λΣw for symmetric Σ. An unconstrained optimum solves λΣw=μ.
  2. The absolute-value turnover penalty is nonsmooth. Away from zero trade its derivative contributes κsign⁡(w−wpre); at zero use a subgradient between −κ and κ. This creates a no-trade region.
Work it by hand

For one asset with μ=.01, λ=2 and variance=.04, the no-cost solution=.125. Nonzero transaction costs can make retaining the existing position preferable.

𝟏𝖳w=n0,‖w‖1≤G,li≤wi≤ui
Constraint arithmetic

Check an allocation against its constraints

  1. Add signed weights to check 𝟏Tw=n0. Add absolute weights to check ‖w‖1≤G. Compare each coordinate with its lower and upper bounds.
  2. For two assets with fixed net n₀, substitute w2=n0−w1 into the objective. This reduces a small hand problem to one variable, but the gross constraint remains piecewise.
Work it by hand

Weights .6,−.2 have net .4 and gross .8. They satisfy net=.4 and gross≤1, but fail a .5 individual long limit.

μ is the forecast vector for the optimisation horizon; Σ is covariance for that same horizon. Risk aversion λ penalises variance, and κ penalises turnover. Net exposure n0, gross limit G, and position bounds keep the mathematical solution within a mandate. Include cash explicitly when interpreting a fully invested constraint.

Unconstrained weights often magnify small differences in noisy expected returns or nearly collinear assets. Do not treat an optimiser’s precision as forecast accuracy. Shrink expected returns toward zero or a prior; shrink covariance toward a stable target.

Σ^=(1−δ)S+δF,0≤δ≤1
Algebra and arithmetic

Blend sample covariance with a target

  1. For every matrix entry compute (1−δ)Sij+δFij. Coefficients sum to 1, so this is a convex combination.
  2. For any vector x, xTΣ^x=(1−δ)xTSx+δxTFx≥0 when both input matrices are positive semidefinite.
Work it by hand

S diagonal .04,.09 and covariance .03; diagonal target F with the same variances; δ=.5 leaves variances unchanged and reduces covariance to .015.

S is sample covariance and F is a structured target such as a diagonal matrix. The shrinkage intensity trades variance for bias. Choose it without consulting the final holdout, then compare weights and out-of-sample behaviour with simple equal-weight and inverse-volatility baselines.

Further reading: CFA Institute: Portfolio Risk and Return ↗

Python implementation

Self-contained teaching example. Python 3.10+; dependencies and input conventions are shown in the code and notation. Run in your own Python environment.

import numpy as np  # dependency: numpy

def smooth_optimum(expected_returns, covariance, risk_aversion):
    """Unconstrained, zero-turnover-penalty solution only; invertible covariance."""
    return np.linalg.solve(np.asarray(covariance), expected_returns)/risk_aversion

def objective(w, mu, cov, risk_aversion, turnover_penalty, pretrade):
    w, mu, cov, pre = map(np.asarray, (w,mu,cov,pretrade))
    return float(mu@w-.5*risk_aversion*(w@cov@w)-turnover_penalty*np.abs(w-pre).sum())

def feasible(w, required_net, gross_limit, lower, upper, tolerance=1e-9):
    w, lower, upper = map(np.asarray, (w,lower,upper))
    return bool(abs(w.sum()-required_net)<=tolerance
                and np.abs(w).sum()<=gross_limit+tolerance
                and np.all(w>=lower-tolerance) and np.all(w<=upper+tolerance))

def shrink_covariance(sample_cov, target_cov, fraction):
    if not 0 <= fraction <= 1:
        raise ValueError("Shrinkage fraction must be in [0,1]")
    return (1-fraction)*np.asarray(sample_cov)+fraction*np.asarray(target_cov)

print(feasible([.6,.4], 1, 1, [0,0], [1,1]))

Continue learning

Portfolio Construction & Factors — all lessons
  1. Separate alpha from compensated exposures
  2. Optimise under realistic constraints
  3. Allocate risk, not just capital
  4. Explain results and marginal diversification

Quantitative finance and development glossary · Python resources and libraries · Research sources and limitations