Tracking
PortfolioOptimisers.AbstractTrackingAlgorithm — Type
abstract type AbstractTrackingAlgorithm <: AbstractAlgorithmAbstract supertype for all tracking algorithm types.
All concrete and/or abstract types representing tracking algorithms (such as weights or returns tracking) should be subtypes of AbstractTrackingAlgorithm.
Interfaces
In order to implement a new tracking algorithm that works seamlessly with the library, subtype AbstractTrackingAlgorithm and implement the following methods:
tracking_benchmark(tr::AbstractTrackingAlgorithm, X::MatNum) -> VecNum: Compute benchmark returns from the asset return matrixX.factory(tr::AbstractTrackingAlgorithm, w::VecNum) -> AbstractTrackingAlgorithm: Construct a new instance with updated portfolio weightsw.port_opt_view(tr::AbstractTrackingAlgorithm, i) -> AbstractTrackingAlgorithm: Create a view of the tracking algorithm for the subset of assets at indicesi.
Arguments
tr: The concrete tracking algorithm instance.X: Covariance-like or correlation-like matrixassets × assets.w: New portfolio weights.i: Index or indices for asset subset.
Returns
b::VecNum: Benchmark returns (fortracking_benchmark).tr::AbstractTrackingAlgorithm: Updated or viewed tracking algorithm (forfactory,port_opt_view).
Examples
julia> struct MyTracking <: PortfolioOptimisers.AbstractTrackingAlgorithm w::Vector{Float64} endjulia> function PortfolioOptimisers.tracking_benchmark(tr::MyTracking, X::PortfolioOptimisers.MatNum) return X * tr.w endjulia> PortfolioOptimisers.factory(tr::MyTracking, w) = MyTracking(w)julia> PortfolioOptimisers.port_opt_view(tr::MyTracking, i) = MyTracking(tr.w[i])julia> tr = MyTracking([0.5, 0.5]);julia> X = [0.01 0.02; 0.03 0.04];julia> PortfolioOptimisers.tracking_benchmark(tr, X)2-element Vector{Float64}: 0.015 0.035Related
PortfolioOptimisers.IndependentVariableTracking — Type
struct IndependentVariableTracking <: VariableTrackingApplies the risk measure to the difference between the portfolio weights and the benchmark weights.
The weights are the independent variable of a risk measure, so this formulation compares the two portfolios before the measure is evaluated: it reports $R(\boldsymbol{w} - \boldsymbol{w}_{b})$.
Constructors
IndependentVariableTracking() -> IndependentVariableTrackingExamples
julia> IndependentVariableTracking()IndependentVariableTracking()Related
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.2.
PortfolioOptimisers.DependentVariableTracking — Type
struct DependentVariableTracking <: VariableTrackingApplies the risk measure to each portfolio, then takes the absolute difference of the two risks.
The risk is the dependent variable of a risk measure, so this formulation compares the two portfolios after the measure is evaluated: it reports $\left\lvert R(\boldsymbol{w}) - R(\boldsymbol{w}_{b}) \right\rvert$.
Constructors
DependentVariableTracking() -> DependentVariableTrackingExamples
julia> DependentVariableTracking()DependentVariableTracking()Related
PortfolioOptimisers.WeightsTracking — Type
struct WeightsTracking{__T_fees, __T_w, __T_fixed} <: AbstractTrackingAlgorithmBuilds the benchmark return series by holding a fixed weight vector, net of its own fees.
The benchmark is stated as a portfolio rather than as a return series, so tracking_benchmark computes $\mathbf{X}\boldsymbol{w}_{b}$ on whichever return matrix the caller supplies. Use it when the benchmark is a known allocation. Use ReturnsTracking when only its realised returns are known.
Mathematical definition
\[\begin{align} \boldsymbol{b} &= \mathbf{X}\boldsymbol{w}_{b} - \boldsymbol{F}(\boldsymbol{w}_{b})\,. \end{align}\]
Where:
- $\boldsymbol{b}$:
T × 1benchmark return vector. - $\mathbf{X}$:
T × Nasset return matrix. - $\boldsymbol{w}_{b}$:
N × 1benchmark weight vector, thewfield. - $\boldsymbol{F}(\boldsymbol{w}_{b})$: Per-period fee charged on the benchmark, from the
feesfield. It is zero whenfeesisnothing. Seecalc_net_returns.
The fees field reads no fold, and it needs none. charge_fees charges this fee over the benchmark series it builds, so an AmortisedFees fa spreads the two fixed charges over that series, and a nothing fa charges them on its first observation. The count is never stored on the fee, so this site needs no fold to settle one.
Fields
fees: Fees estimator or result.
w: Reference portfolio weight vector. Deviations are measured against it, and it is never the candidate weight vector an optimiser solves for.
fixed: Whether the estimator is fixed and does not update with new weights.
Constructors
WeightsTracking(; fees::Option{<:Fees} = nothing, w::VecNum, fixed::Bool = false) -> WeightsTrackingKeywords correspond to the struct's fields.
Validation
w, throughassert_nonempty_finite_val:!isempty(w)andall(isfinite, w). Every entry must be finite, so[0.5, NaN]is refused.
View parameters
When port_opt_view is called on this type, the following @vprop-tagged fields are automatically subset to the selected indices:
fees: Recursively viewed viaport_opt_view.w: Sliced to the selected indices viaport_opt_view.
Examples
julia> WeightsTracking(; w = [0.5, 0.5])WeightsTracking fees ┼ nothing w ┼ Vector{Float64}: [0.5, 0.5] fixed ┴ Bool: falseRelated
ReturnsTrackingTrackingErrorAbstractTrackingAlgorithmFeesOptiontracking_benchmarkcalc_net_returnsport_opt_view
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.2, Equations 9.16 and 9.18.
PortfolioOptimisers.ReturnsTracking — Type
struct ReturnsTracking{__T_w} <: AbstractTrackingAlgorithmCarries the benchmark return series itself, for a benchmark whose weights are unknown.
tracking_benchmark returns the w field unchanged, so no return matrix is read and no fee is applied. This is the case the book states first: an index whose published series is all the caller has. Use WeightsTracking when the benchmark allocation is known.
The w field holds T returns, one per observation, not N weights. Its length must match the number of rows of the return matrix the model is built on.
Fields
w: Benchmark portfolio returns vector. It holdsTreturns, one per observation, and notNweights, so its length must match the number of rows of the return matrix the model is built on.
Constructors
ReturnsTracking(; w::VecNum) -> ReturnsTrackingKeywords correspond to the struct's fields.
Validation
w, throughassert_nonempty_finite_val:!isempty(w)andall(isfinite, w). Every entry must be finite, so[0.01, NaN]is refused.
Examples
julia> ReturnsTracking(; w = [0.01, 0.02, 0.03])ReturnsTracking w ┴ Vector{Float64}: [0.01, 0.02, 0.03]Related
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.2, Equations 9.16 and 9.17.
PortfolioOptimisers.TrackingError — Type
struct TrackingError{__T_tr, __T_err, __T_alg} <: AbstractTrackingBounds how far the portfolio return series may drift from a benchmark return series.
err is an upper bound, not a computed value: set_tracking_error_constraints! writes one cone per alg and holds the scaled deviation below err. tr supplies the benchmark and alg names the norm that measures the deviation.
Mathematical definition
\[\begin{align} \mathrm{TE}(\boldsymbol{w}) &= \lVert \boldsymbol{r}(\boldsymbol{w}) - \boldsymbol{b} \rVert \cdot c^{-1} \leq \mathrm{err}\,. \end{align}\]
Where:
- $\mathrm{TE}(\boldsymbol{w})$: Tracking error.
- $\boldsymbol{r}(\boldsymbol{w})$:
T × 1net portfolio return series. - $\boldsymbol{b}$:
T × 1benchmark return series, fromtracking_benchmarkon thetrfield. - $\lVert \cdot \rVert$, $c$: The norm and the scaling factor that
algnames.norm_errorcomputes the pair andnorm_factorgives $c$. - $\mathrm{err}$: The
errfield.
err is stated in the units of alg, and SquaredL2Norm squares. The same number therefore means two different bounds: TrackingError(; alg = SquaredL2Norm(), err = 5e-6) admits an L2Norm error up to sqrt(5e-6), where TrackingError(; alg = L2Norm(), err = 5e-6) admits 5e-6. Convert with the square, not by reusing the tolerance. The model, norm_error and set_risk_constraints! all read err the same way.
The conversion is the square root, it carries no dependence on T, and the two norms then write the same cone bound. tracking_error_soc_factor is where they meet: (SquaredL2Norm(), err^2) and (L2Norm(), err) give one factor, one weight vector, and realised deviations that satisfy the square. The ddof field of alg moves that bound.
The keys the model registers are picked by alg, and each carries the constraint index appended. Every branch registers :t_te_ for the cone variable, :te_ for the deviation expression $\mathbf{X}\boldsymbol{w} - \boldsymbol{b}k$, and :cte_ for the row that holds the cone variable below the scaled tolerance. The cone, its row, and the rows a branch adds beyond those three, are:
alg | Cone | Cone row | Rows the branch adds |
|---|---|---|---|
L1Norm | JuMP.MOI.NormOneCone | :cte_noc_ | none |
L2Norm, SquaredL2Norm | JuMP.SecondOrderCone | :cte_soc_ | none |
LpNorm | JuMP.MOI.PowerCone | :cte_pnorm_ | :r_te_, :cste_ |
LInfNorm | JuMP.MOI.NormInfinityCone | :cte_infnorm_ | none |
:cte_soc_ is therefore the key of the default alg = L2Norm() and of SquaredL2Norm alone. The model registers no :tracking_risk_ and no :sq_tracking_risk_: those two keys belong to TrackingRiskMeasure, which measures a risk difference rather than a return-series deviation.
Fields
tr: Tracking error constraint estimator.
err: Tracking error tolerance.
alg: Tracking formulation algorithm.
Constructors
TrackingError(; tr::AbstractTrackingAlgorithm, err::Number = 0.0, alg::NormError = L2Norm()) -> TrackingErrorKeywords correspond to the struct's fields.
Validation
err, throughassert_nonempty_nonneg_finite_val:isfinite(err)anderr >= 0, each raising aDomainErrornamingerr.err = 0.0is admitted, and it pins the portfolio to the benchmark.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
tr: Recursively updated viafactory.
View parameters
When port_opt_view is called on this type, the following @vprop-tagged fields are automatically subset to the selected indices:
tr: Recursively viewed viaport_opt_view.
Examples
julia> tr = WeightsTracking(; w = [0.5, 0.5]);julia> TrackingError(; tr = tr, err = 0.01)TrackingError tr ┼ WeightsTracking │ fees ┼ nothing │ w ┼ Vector{Float64}: [0.5, 0.5] │ fixed ┴ Bool: false err ┼ Float64: 0.01 alg ┼ L2Norm │ ddof ┴ Int64: 1Related
set_tracking_error_constraints!AbstractTrackingWeightsTrackingReturnsTrackingNormErrorL2NormSquaredL2NormL1Normnorm_errornorm_factortracking_error_soc_factor: Turnserrinto the cone bound, and is where the twoL2norms meet.TrackingRiskMeasure: The risk-difference measure that owns:tracking_risk_and:sq_tracking_risk_.tracking_benchmarkfactoryport_opt_view
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 9.2, Equations 9.19 to 9.21.
PortfolioOptimisers.factory — Method
factory(tr::WeightsTracking, w::VecNum)Construct a new WeightsTracking object with updated portfolio weights.
The fixed flag decides whether the benchmark moves, and it obeys it the way factory(tn::Turnover, w::VecNum) obeys its own. A fixed benchmark is a fixed allocation, so the object is returned unchanged and === holds. A benchmark that is not fixed takes w as its new reference, and its fees advance one step: the nested Turnover takes the old tr.w as its reference, because that is the allocation the portfolio is turning over from.
Algorithm
- Read
tr.fixed. - When
tr.fixedistrue, returntritself. The argumentwis not read. - When
tr.fixedisfalse, advance the fees withfactory(tr.fees, tr.w), which hands the old weights to the nested turnover as its reference. - Build a new
WeightsTrackingfrom the advanced fees, the neww, and the unchangedtr.fixed.
Arguments
tr: AWeightsTrackingobject to copy fees from.w: Portfolio weights.
Returns
tr::WeightsTracking: New tracking algorithm object with updated weights, ortritself whentr.fixedistrue.
Examples
julia> tr = WeightsTracking(; fees = Fees(; l = 0.002), w = [0.5, 0.5])WeightsTracking fees ┼ Fees │ tn ┼ nothing │ l ┼ Float64: 0.002 │ s ┼ nothing │ fl ┼ nothing │ fs ┼ nothing │ lq ┼ nothing │ flq ┼ nothing │ fa ┼ nothing │ kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,) w ┼ Vector{Float64}: [0.5, 0.5] fixed ┴ Bool: falsejulia> PortfolioOptimisers.factory(tr, [0.6, 0.4])WeightsTracking fees ┼ Fees │ tn ┼ nothing │ l ┼ Float64: 0.002 │ s ┼ nothing │ fl ┼ nothing │ fs ┼ nothing │ lq ┼ nothing │ flq ┼ nothing │ fa ┼ nothing │ kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,) w ┼ Vector{Float64}: [0.6, 0.4] fixed ┴ Bool: falsejulia> tr = WeightsTracking(; fees = Fees(; l = 0.002), w = [0.5, 0.5], fixed = true)WeightsTracking fees ┼ Fees │ tn ┼ nothing │ l ┼ Float64: 0.002 │ s ┼ nothing │ fl ┼ nothing │ fs ┼ nothing │ lq ┼ nothing │ flq ┼ nothing │ fa ┼ nothing │ kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,) w ┼ Vector{Float64}: [0.5, 0.5] fixed ┴ Bool: truejulia> PortfolioOptimisers.factory(tr, [0.1, 0.1])WeightsTracking fees ┼ Fees │ tn ┼ nothing │ l ┼ Float64: 0.002 │ s ┼ nothing │ fl ┼ nothing │ fs ┼ nothing │ lq ┼ nothing │ flq ┼ nothing │ fa ┼ nothing │ kwargs ┴ @NamedTuple{atol::Float64}: (atol = 1.0e-8,) w ┼ Vector{Float64}: [0.5, 0.5] fixed ┴ Bool: trueRelated
WeightsTrackingVecNumfactoryfactory(tn::Turnover, w::VecNum): The verb that reads the samefixedflag, one level down.needs_previous_weights: Reads the same flag to decide whether the optimiser must supply a previous weight vector.
PortfolioOptimisers.tracking_benchmark — Function
tracking_benchmark(tr::WeightsTracking, X::MatNum)Compute the benchmark portfolio returns for a weights-based tracking algorithm.
tracking_benchmark computes the net portfolio returns for the benchmark weights stored in a WeightsTracking object, optionally adjusting for transaction fees if specified. The asset return matrix X is multiplied by the benchmark weights, and fees are deducted if present.
This method restates no definition of its own. It is calc_net_returns(tr.w, X, tr.fees), so the fee falls on the clock that function reads from tr.fees.fa. On the three-period matrix [0.01 0.02 -0.01 0.03; 0.03 0.04 0.02 -0.02; -0.01 0.005 0.01 0.04] and $\boldsymbol{w}_{b} = [0.3,\, 0.2,\, 0.4,\, 0.1]$, the benchmark measured [0.006, 0.023, 0.006] with no fee, and [0.005, 0.022, 0.005] under Fees(; l = 0.001), whose fee is 0.001.
Algorithm
- Forward
tr.w,Xandtr.feestocalc_net_returns. - A
nothingtr.feesreaches theargs...method ofcalc_net_returns, which returnsX * tr.w. It does not reach theFeesmethod and charge a zero fee. - A
Feestr.feesreaches theFeesmethod, which handsX * tr.wtocharge_fees.
Arguments
tr:WeightsTrackingtracking algorithm containing benchmark weights and optional fees.X: Asset return matrix (observations × assets).
Returns
b::VecNum: Net benchmark portfolio returns, one entry per row ofX.
Examples
julia> tr = WeightsTracking(; w = [0.5, 0.5]);julia> X = [0.01 0.02; 0.03 0.04];julia> PortfolioOptimisers.tracking_benchmark(tr, X)2-element Vector{Float64}: 0.015 0.035Related
WeightsTrackingMatNumcalc_net_returns: The single definition this method forwards to.Feestracking_benchmark(tr::ReturnsTracking, args...): The sibling, which reads no return matrix at all.
tracking_benchmark(tr::ReturnsTracking, args...)Return the benchmark portfolio returns for a returns-based tracking algorithm.
tracking_benchmark extracts the benchmark portfolio returns stored in a ReturnsTracking object. This is used for tracking error measurement and constraint generation where the comparison is made directly between portfolio returns and benchmark returns.
No return matrix is read, and no fee is applied. Every trailing argument is ignored, so a matrix whose number of rows does not match length(tr.w) still returns tr.w unchanged: a three-entry w against a seven-row matrix succeeds here. That is deliberate. A length mismatch is a property of the model the series is put into, so it is raised there rather than by this function.
Algorithm
- Return
tr.w. No argument aftertris read.
Arguments
tr:ReturnsTrackingtracking algorithm containing benchmark returns.args...: For interface compatibility (ignored).
Returns
b::VecNum: Benchmark portfolio returns. It istr.witself, not a copy, sotracking_benchmark(tr) === tr.w.
Examples
julia> tr = ReturnsTracking(; w = [0.01, 0.02, 0.03]);julia> PortfolioOptimisers.tracking_benchmark(tr)3-element Vector{Float64}: 0.01 0.02 0.03Related
ReturnsTrackingWeightsTrackingtracking_benchmark(tr::WeightsTracking, X::MatNum): The sibling, which does read the return matrix and does charge a fee.TrackingError
PortfolioOptimisers.needs_previous_weights — Method
needs_previous_weights(tr::AbstractTrackingAlgorithm) -> Bool
needs_previous_weights(tr::WeightsTracking) -> Bool
needs_previous_weights(tr::TrackingError) -> Bool
needs_previous_weights(tr::VecTr) -> BoolCheck whether a tracking algorithm or tracking result needs the previous portfolio weights.
Only a WeightsTracking that is not fixed needs them, because only it moves its reference allocation when factory runs. A ReturnsTracking carries a return series and reaches the fallback, which answers false. The vector method answers any and not all, so one entry that needs the previous weights makes the whole vector need them: a vector holding one fixed and one free WeightsTracking answers true where all would answer false.
Algorithm
- On any
AbstractTrackingAlgorithmthat no method below claims, returnfalse. - On a
WeightsTracking, return!tr.fixed. - On a
TrackingError, forward to thetr.trfield and return its answer. - On a
VecTr, apply steps 1 to 3 to every entry and reduce withany.
Arguments
tr: One tracking algorithm, one tracking result, or a vector of tracking results.
Returns
Bool:trueif previous weights are needed,falseotherwise.
Related
AbstractTrackingAlgorithmWeightsTrackingReturnsTrackingTrackingErrorVecTrfactory(tr::WeightsTracking, w::VecNum): The verb that reads the samefixedflag.Turnover: Carries the samefixedflag and the sameanyrule one family across.
References
- [5]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).