Mean Variance Portfolio Example

Consider the Markowitz portfolio selection problem, which allocates weights $x \in \mathbb{R}^n$ to $n$ assets so as to maximize returns subject to a variance limit $v_{\max}$:

\[\max_{x} \quad \mu^\top x \quad\text{s.t.}\quad x^\top \Sigma x \;\le\; v_{\max}, \quad \mathbf{1}^\top x = 1,\quad x \succeq 0,\]

where $\mu$ is the vector of expected returns, $\Sigma$ is the covariance matrix, and $x$ must sum to 1 (fully invest the budget). An efficient conic version of this problem casts the variance limit as a second order cone constraint:

\[\| \Sigma^{1/2} x \|_{2} \;\le\; \sigma_{\max}\]

where $\Sigma^{1/2}$ is the Cholesky factorization of the covariance matrix and $\sigma_{\max}$ is the standard deviation limit.

Practitioners often care about an \emph{out-of-sample performance metric} $L(x)$ evaluated on test data or scenarios that differ from those used to form $\mu$ and $\Sigma$. To assess the impact of the risk profile in the performance evaluation, one can compute:

\[\frac{dL}{d\,\sigma_{\max}} \;=\; \underbrace{\frac{\partial L}{\partial x}}_{\text{(1) decision impact}}\; \cdot\; \underbrace{\frac{\partial x^*}{\partial \sigma_{\max}}}_{\text{(2) from DiffOpt.jl}},\]

where $x^*(\sigma_{\max})$ is the portfolio that solves the conic Markowitz problem under a given risk limit.

Define and solve the Mean-Variance Portfolio Problem for a range of risk limits

First, import the libraries.

using Test
using JuMP
import DiffOpt
using LinearAlgebra
import SCS
using Plots
using Plots.Measures

Fixed data

Training data (in-sample)

Σ = [
    0.002 0.0005 0.001
    0.0005 0.003 0.0002
    0.001 0.0002 0.0025
]
μ_train = [0.05, 0.08, 0.12]
3-element Vector{Float64}:
 0.05
 0.08
 0.12

Test data (out-of-sample)

μ_test = [0.02, -0.3, 0.1]             # simple forecast error example
3-element Vector{Float64}:
  0.02
 -0.3
  0.1

Sweep over σ_max

σ_grid = 0.002:0.002:0.06
N = length(σ_grid)

predicted_ret = zeros(N)                 # μ_train' * x*
realised_ret = zeros(N)                 # μ_test'  * x*
loss = zeros(N)                 # L(x*)
dL_dσ = zeros(N)                 # ∂L/∂σ_max  from DiffOpt

for (k, σ_val) in enumerate(σ_grid)

    # 1) differentiable conic model
    model = DiffOpt.conic_diff_model(SCS.Optimizer)
    set_silent(model)

    # 2) parameter σ_max
    @variable(model, σ_max in Parameter(σ_val))

    # 3) portfolio weights
    @variable(model, x[1:3] >= 0)
    @constraint(model, sum(x) <= 1)

    # 4) objective: maximise expected return (training data)
    @objective(model, Max, dot(μ_train, x))

    # 5) conic variance constraint  ||L*x|| <= σ_max
    L_chol = cholesky(Symmetric(Σ)).L
    @variable(model, t >= 0)
    @constraint(model, [t; L_chol * x] in SecondOrderCone())
    @constraint(model, t <= σ_max)

    optimize!(model)

    x_opt = value.(x)
    println("Optimal portfolio weights: ", x_opt)

    # store performance numbers
    predicted_ret[k] = dot(μ_train, x_opt)
    realised_ret[k] = dot(μ_test, x_opt)

    # -------- reverse differentiation wrt σ_max --------
    DiffOpt.empty_input_sensitivities!(model)
    # ∂L/∂x   (adjoint)  =  -μ_test
    DiffOpt.set_reverse_variable.(model, x, μ_test)
    DiffOpt.reverse_differentiate!(model)
    dL_dσ[k] = DiffOpt.get_reverse_parameter(model, σ_max)
end
Optimal portfolio weights: [5.658840610739365e-6, 0.018882734867448424, 0.03907243442891081]
Optimal portfolio weights: [2.9615619577749345e-6, 0.03710076184568147, 0.07838697285139078]
Optimal portfolio weights: [3.9910013314173664e-6, 0.05565731138250393, 0.11757663205503585]
Optimal portfolio weights: [2.87610536394916e-5, 0.07467364261772505, 0.15655147380391396]
Optimal portfolio weights: [5.533624864514992e-6, 0.09275568036221664, 0.19597607214347895]
Optimal portfolio weights: [6.045266090474022e-6, 0.11131252558479116, 0.23515581300517177]
Optimal portfolio weights: [6.794648067915429e-6, 0.1299586604405889, 0.274297015070987]
Optimal portfolio weights: [3.2231106725818725e-6, 0.14841388889229973, 0.3135455555828517]
Optimal portfolio weights: [5.69155012085509e-6, 0.1669701262218718, 0.35273221097379875]
Optimal portfolio weights: [7.0239769786900575e-6, 0.1858022763711049, 0.3920032362468273]
Optimal portfolio weights: [4.625005834765434e-6, 0.2040057967872567, 0.43115527968641965]
Optimal portfolio weights: [5.727794365400229e-6, 0.22262094919774178, 0.4703006360363648]
Optimal portfolio weights: [2.7606178387955075e-6, 0.2412201228264976, 0.5094959120067006]
Optimal portfolio weights: [-4.011210804809348e-6, 0.25971799991642425, 0.5486899189727742]
Optimal portfolio weights: [2.4831339383202443e-7, 0.27822041997195535, 0.5879285717690728]
Optimal portfolio weights: [1.265623825856975e-7, 0.29683064703787815, 0.6270859990244294]
Optimal portfolio weights: [2.8403640209926924e-8, 0.31538277336701637, 0.6662793615647071]
Optimal portfolio weights: [-2.0006496362888272e-6, 0.24354374835842568, 0.756458201459553]
Optimal portfolio weights: [1.3793346965544707e-6, 0.17088013855027953, 0.8291136723356591]
Optimal portfolio weights: [1.1272822651327975e-5, 0.11344879862100907, 0.8865392792741486]
Optimal portfolio weights: [-6.398848978782594e-9, 0.06232492177698473, 0.9376750967704772]
Optimal portfolio weights: [2.950594238037073e-7, 0.014238262956888114, 0.98575899128837]
Optimal portfolio weights: [-2.161503798854554e-5, -1.8791684432607876e-5, 1.000070778549155]
Optimal portfolio weights: [-1.3066285169084386e-5, -1.3542094994945041e-5, 1.000048037096506]
Optimal portfolio weights: [-1.3028821103253019e-5, -1.3498883909816854e-5, 1.0000479074416562]
Optimal portfolio weights: [-1.3334204599601495e-5, -1.3812256923059167e-5, 1.00004903592973]
Optimal portfolio weights: [-1.3095781404024467e-5, -1.3567686347469338e-5, 1.0000481547145874]
Optimal portfolio weights: [-1.3398025908087403e-5, -1.3878512617563315e-5, 1.0000492703600532]
Optimal portfolio weights: [-1.3439487712455741e-5, -1.3921342567582751e-5, 1.000049423052067]
Optimal portfolio weights: [-1.3379282995708692e-5, -1.3859468989832669e-5, 1.0000492007495634]

Results with Plot graphs

default(;
    size = (1150, 350),
    legendfontsize = 8,
    guidefontsize = 9,
    tickfontsize = 7,
)

(a) predicted vs realised return

plt_ret = plot(
    σ_grid,
    realised_ret;
    lw = 2,
    label = "Realised (test)",
    xlabel = "σ_max (risk limit)",
    ylabel = "Return",
    title = "Return vs risk limit",
    legend = :bottomright,
);
plot!(
    plt_ret,
    σ_grid,
    predicted_ret;
    lw = 2,
    ls = :dash,
    label = "Predicted (train)",
);

(b) out-of-sample loss and its gradient

plt_loss = plot(
    σ_grid,
    dL_dσ;
    xlabel = "σ_max (risk limit)",
    ylabel = "∂L/∂σ_max",
    title = "Return Gradient",
    legend = false,
);

plot_all = plot(
    plt_ret,
    plt_loss;
    layout = (1, 2),
    left_margin = 5Plots.Measures.mm,
    bottom_margin = 5Plots.Measures.mm,
)
Example block output

Impact of the risk limit $\sigma_{\max}$ on Markowitz portfolios. Left: predicted in-sample return versus realized out-of-sample return. Right: the out-of-sample loss $L(x)$ together with the absolute gradient $|\partial L/\partial\sigma_{\max}|$ obtained from DiffOpt.jl. The gradient tells the practitioner which way—and how aggressively—to adjust $\sigma_{\max}$ to reduce forecast error; its value is computed in one reverse-mode call without re-solving the optimization for perturbed risk limits.


This page was generated using Literate.jl.