The exact projection weight finalisers

The two weight finalisers below move weights into their bounds by an exact projection, and keep the sum of the weights. Each projection is a clip of the weights at one scalar. A binary search over the kinks of that scalar finds it exactly, so no solver is necessary. The Euclidean projection moves every free weight by the same amount, and it works with any budget and with bounds on long and short weights. The entropic projection scales every free weight by the same factor, so it keeps their ratios, and it is defined for long-only weights. The default IterativeWeightFinaliser falls back to the Euclidean projection when its loop cannot reach the bounds.

PortfolioOptimisers.EuclideanWeightFinaliser — Type
struct EuclideanWeightFinaliser <: WeightFinaliser

Moves weights into their bounds by the exact Euclidean projection, which needs no solver.

It solves the programme of a JuMPWeightFinaliser with a SquaredAbsoluteErrorWeightFinaliser in closed form. It keeps any budget that the input weights carry, a zero or a negative budget too, and it accepts long-short bounds. When the budget lies outside the sums of the bounds, no projection exists, and finalise_weight_bounds then reports an OptimisationFailure.

The projection moves every free weight by the same amount, so it does not keep the ratios of the free weights. EntropicWeightFinaliser and IterativeWeightFinaliser scale them instead. Under ub = 0.5, the weights [0.9, 0.09, 0.01] become [0.5, 0.29, 0.21] here and [0.5, 0.45, 0.05] there.

Mathematical definition

\[\begin{align} \underset{\boldsymbol{w}}{\min} &\quad \left\lVert \boldsymbol{w} - \boldsymbol{w}_{0} \right\rVert_{2}\,, \\ \textrm{s.t.} &\quad \boldsymbol{1}^\intercal \boldsymbol{w} = \boldsymbol{1}^\intercal \boldsymbol{w}_{0}\,, \\ &\quad \boldsymbol{l} \leq \boldsymbol{w} \leq \boldsymbol{u}\,. \end{align}\]

The minimiser clips the input at one shift:

\[w_{i} = \mathrm{clip}\left(w_{0,i} - \theta,\, l_{i},\, u_{i}\right)\,, \qquad \sum_{i} \mathrm{clip}\left(w_{0,i} - \theta,\, l_{i},\, u_{i}\right) = \boldsymbol{1}^\intercal \boldsymbol{w}_{0}\,.\]

Where:

  • $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
  • $\boldsymbol{w}_{0}$: Portfolio weights vector $N \times 1$ that the optimisation produced, which the finaliser repairs.
  • $\boldsymbol{l}$, $\boldsymbol{u}$: Lower and upper weight bounds. An absent bound is dropped from the programme rather than set to an infinity.
  • $\theta$: The shift that restores the budget.
  • $\mathrm{clip}(x, l, u) = \min(\max(x, l), u)$: The clip of $x$ to the interval $[l, u]$.

The sum of the clipped weights is continuous and does not increase in $\theta$. It falls from $\boldsymbol{1}^\intercal \boldsymbol{u}$ to $\boldsymbol{1}^\intercal \boldsymbol{l}$, so a shift exists if and only if $\boldsymbol{1}^\intercal \boldsymbol{l} \leq \boldsymbol{1}^\intercal \boldsymbol{w}_{0} \leq \boldsymbol{1}^\intercal \boldsymbol{u}$.

Algorithm

The method of opt_weight_bounds for this tag runs these steps.

  1. When weights_break_bounds finds no weight of w outside the bounds, return w itself.
  2. Otherwise, return the projection of w from euclidean_weight_projection.

Constructors

EuclideanWeightFinaliser() -> EuclideanWeightFinaliser

Examples

julia> EuclideanWeightFinaliser()EuclideanWeightFinaliser()julia> PortfolioOptimisers.opt_weight_bounds(EuclideanWeightFinaliser(),                                             WeightBounds(; lb = 0.0, ub = 0.5), [0.9, 0.09, 0.01])3-element Vector{Float64}: 0.5 0.29000000000000004 0.21000000000000008

Related

source
PortfolioOptimisers.EntropicWeightFinaliser — Type
struct EntropicWeightFinaliser <: WeightFinaliser

Moves long-only weights into their bounds by the exact entropic projection, which needs no solver and keeps the ratios of the free weights.

The projection scales every free weight by the same factor, so their ratios hold. For positive weights under zero lower bounds, the loop of IterativeWeightFinaliser converges to the same point. Under two-sided bounds the loop can stop at a different point. Under lb = 0.3 and ub = 0.45, the weights [0.5, 0.4, 0.1] become [0.4, 0.3, 0.3] there and [0.389, 0.311, 0.3] here. The second point is nearer to the input in relative entropy.

The projection is defined only when the weights and the lower bounds are non-negative, the budget is positive, and a scaled vector can meet the bounds. Outside this domain, the finaliser returns the Euclidean projection of EuclideanWeightFinaliser instead.

Mathematical definition

\[\begin{align} \underset{\boldsymbol{w}}{\min} &\quad \sum_{i} w_{i} \log \frac{w_{i}}{w_{0,i}}\,, \\ \textrm{s.t.} &\quad \boldsymbol{1}^\intercal \boldsymbol{w} = \boldsymbol{1}^\intercal \boldsymbol{w}_{0}\,, \\ &\quad \boldsymbol{l} \leq \boldsymbol{w} \leq \boldsymbol{u}\,. \end{align}\]

The minimiser clips the scaled input:

\[w_{i} = \mathrm{clip}\left(\frac{w_{0,i}}{Z},\, l_{i},\, u_{i}\right)\,, \qquad \sum_{i} \mathrm{clip}\left(\frac{w_{0,i}}{Z},\, l_{i},\, u_{i}\right) = \boldsymbol{1}^\intercal \boldsymbol{w}_{0}\,.\]

Where:

  • $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
  • $\boldsymbol{w}_{0}$: Portfolio weights vector $N \times 1$ that the optimisation produced, which the finaliser repairs.
  • $\boldsymbol{l}$, $\boldsymbol{u}$: Lower and upper weight bounds. An absent bound is dropped from the programme rather than set to an infinity.
  • $Z$: The positive normaliser that restores the budget.
  • $\mathrm{clip}(x, l, u) = \min(\max(x, l), u)$: The clip of $x$ to the interval $[l, u]$.

A zero $w_{0,i}$ with a positive $l_{i}$ gives the programme no finite objective. The clip sets such a weight to $l_{i}$ for every $Z$, so a zero weight cannot rise above its lower bound. The sum of the clipped weights rises with $1 / Z$ from $\boldsymbol{1}^\intercal \boldsymbol{l}$ to the sum of $u_{i}$ over the positive weights and $l_{i}$ over the zero weights. A normaliser therefore exists when the budget is above the first sum and at most the second. At the first sum, the only feasible vector is $\boldsymbol{l}$.

Algorithm

The method of opt_weight_bounds for this tag runs these steps.

  1. When weights_break_bounds finds no weight of w outside the bounds, return w itself.
  2. Otherwise, return the projection of w from entropic_weight_projection.

Constructors

EntropicWeightFinaliser() -> EntropicWeightFinaliser

Examples

julia> EntropicWeightFinaliser()EntropicWeightFinaliser()julia> PortfolioOptimisers.opt_weight_bounds(EntropicWeightFinaliser(),                                             WeightBounds(; lb = 0.0, ub = 0.5), [0.9, 0.09, 0.01])3-element Vector{Float64}: 0.5 0.44999999999999996 0.05

Related

source