Skip to content
11

PortfolioOptimisers.jlQuantitative portfolio construction

Democratising, demystifying, and derisking investing

PortfolioOptimisers

Welcome to PortfolioOptimisers.jl

DANGER

Investing conveys real risk, the entire point of portfolio optimisation is to minimise it to tolerable levels. The examples use outdated data and a variety of stocks (including what I consider to be meme stocks) for demonstration purposes only. None of the information in this documentation should be taken as financial advice. Any advice is limited to improving portfolio construction, most of which is common investment and statistical knowledge.

Portfolio optimisation is the science of either:

  • Minimising risk whilst keeping returns to acceptable levels.

  • Maximising returns whilst keeping risk to acceptable levels.

To some definition of acceptable, and with any number of additional constraints available to the optimisation type.

There exist myriad statistical, pre- and post-processing, optimisations, and constraints that allow one to explore an extensive landscape of "optimal" portfolios.

PortfolioOptimisers.jl is an attempt at providing as many of these as possible under a single banner. We make extensive use of Julia's type system, module extensions, and multiple dispatch to simplify development and maintenance.

Please visit the examples and API for details.

Caveat emptor

  • PortfolioOptimisers.jl is under active development and still in v0.*.*. Therefore, breaking changes should be expected with v0.X.0 releases. All other releases will fall under v0.X.Y.

  • The documentation is still under construction.

  • Testing coverage is still under 95 %. We're mainly missing assertion tests, but some lesser used features are partially or wholly untested.

  • Please feel free to submit issues, discussions and/or PRs regarding missing docs, examples, features, tests, and bugs.

Installation

PortfolioOptimisers.jl is a registered package, so installation is as simple as:

julia
julia> using Pkg

julia> Pkg.add(PackageSpec(; name = "PortfolioOptimisers"))

Quick-start

The library is quite powerful and extremely flexible. Here is what a very basic end-to-end workflow can look like. The examples contain more thorough explanations and demos. The API docs contain toy examples of the many, many features.

First we import the packages we will need for the example.

  • StatsPlots and GraphRecipes are needed to load the plotting extension.

  • Clarabel and HiGHS are the optimisers we will use.

  • YFinance and TimeSeries for downloading and preprocessing price data.

  • PrettyTables and DataFrames for displaying the results.

julia
# Import module and plotting extension.
using PortfolioOptimisers, StatsPlots, GraphRecipes
# Import optimisers.
using Clarabel, HiGHS
# Download data.
using YFinance, TimeSeries
# Pretty printing.
using PrettyTables, DataFrames

# Format for pretty tables.
fmt1 = (v, i, j) -> begin
    if j == 1
        return Date(v)
    else
        return v
    end
end;
fmt2 = (v, i, j) -> begin
    if j  (1, 2, 3)
        return v
    else
        return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v
    end
end;

For illustration purposes, we will use a set of popular meme stocks. We need to download and set the price data in a format PortfolioOptimisers.jl can consume.

julia
# Function to convert prices to time array.
function stock_price_to_time_array(x)
    # Only get the keys that are not ticker or datetime.
    coln = collect(keys(x))[3:end]
    # Convert the dictionary into a matrix.
    m = hcat([x[k] for k in coln]...)
    return TimeArray(x["timestamp"], m, Symbol.(coln), x["ticker"])
end

# Tickers to download. These are popular meme stocks, use something better.
assets = sort!(["SOUN", "RIVN", "GME", "AMC", "SOFI", "ENVX", "ANVS", "LUNR", "EOSE", "SMR",
                "NVAX", "UPST", "ACHR", "RKLB", "MARA", "LGVN", "LCID", "CHPT", "MAXN",
                "BB"])

# Prices date range.
Date_0 = "2024-01-01"
Date_1 = "2025-10-05"

# Download the price data using YFinance.
prices = get_prices.(assets; startdt = Date_0, enddt = Date_1)
prices = stock_price_to_time_array.(prices)
prices = hcat(prices...)
cidx = colnames(prices)[occursin.(r"adj", string.(colnames(prices)))]
prices = prices[cidx]
TimeSeries.rename!(prices, Symbol.(assets))
pretty_table(prices[(end - 5):end]; formatters = [fmt1])
┌────────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬──────
  timestamp     ACHR      AMC     ANVS       BB     CHPT     ENVX 
   DateTime  Float64  Float64  Float64  Float64  Float64  Float64  Flo
├────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼──────
│ 2025-09-26 │    9.28 │    2.89 │    1.97 │    4.96 │   10.84 │   10.09 │   1 ⋯
│ 2025-09-29 │    9.65 │     3.0 │    2.04 │     5.0 │   11.05 │    9.97 │   1 ⋯
│ 2025-09-30 │    9.58 │     2.9 │    2.07 │    4.88 │   10.92 │    9.97 │   1 ⋯
│ 2025-10-01 │    9.81 │    2.95 │    2.13 │    4.79 │   11.63 │   11.11 │   1 ⋯
│ 2025-10-02 │   10.18 │    3.15 │    2.23 │    4.75 │   11.32 │   11.65 │   1 ⋯
│ 2025-10-03 │   11.57 │    3.06 │    2.22 │     4.5 │   11.94 │   11.92 │     ⋯
└────────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴──────
                                                              14 columns omitted

Now we can compute our returns by calling prices_to_returns.

julia
# Compute the returns.
rd = prices_to_returns(prices)
ReturnsResult
    nx ┼ 20-element Vector{String}
     X ┼ 440×20 Matrix{Float64}
    nf ┼ nothing
     F ┼ nothing
    ts ┼ 440-element Vector{DateTime}
    iv ┼ nothing
  ivpa ┴ nothing

PortfolioOptimisers.jl uses JuMP for handling the optimisation problems, which means it is solver agnostic and therefore does not ship with any pre-installed solver. Solver lets us define the optimiser factory, its solver-specific settings, and JuMP's solution acceptance criteria.

julia
# Define the continuous solver.
slv = Solver(; name = :clarabel1, solver = Clarabel.Optimizer,
             settings = Dict("verbose" => false, "max_step_fraction" => 0.9),
             check_sol = (; allow_local = true, allow_almost = true))
Solver
         name ┼ Symbol: :clarabel1
       solver ┼ UnionAll: Clarabel.MOIwrapper.Optimizer
     settings ┼ Dict{String, Real}: Dict{String, Real}("verbose" => false, "max_step_fraction" => 0.9)
    check_sol ┼ @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
  add_bridges ┴ Bool: true

PortfolioOptimisers.jl implements a number of optimisation types as estimators. All the ones which use mathematical optimisation require a [JuMPOptimiser]-(@ref) structure which defines general solver constraints. This structure in turn requires an instance (or vector) of Solver.

julia
opt = JuMPOptimiser(; slv = slv);

Here we will use the traditional Mean-Risk [MeanRisk]-(@ref) optimisation estimator, which defaults to the Markowitz optimisation (minimum risk mean-variance optimisation).

julia
# Vanilla (Markowitz) mean risk optimisation.
mr = MeanRisk(; opt = opt)
MeanRisk
  opt ┼ JuMPOptimiser
      │        pe ┼ EmpiricalPrior
      │           │        ce ┼ PortfolioOptimisersCovariance
      │           │           │   ce ┼ Covariance
      │           │           │      │    me ┼ SimpleExpectedReturns
      │           │           │      │       │     w ┼ nothing
      │           │           │      │       │   idx ┴ nothing
      │           │           │      │    ce ┼ GeneralCovariance
      │           │           │      │       │    ce ┼ SimpleCovariance: SimpleCovariance(true)
      │           │           │      │       │     w ┼ nothing
      │           │           │      │       │   idx ┴ nothing
      │           │           │      │   alg ┴ Full()
      │           │           │   mp ┼ DenoiseDetoneAlgMatrixProcessing
      │           │           │      │     pdm ┼ Posdef
      │           │           │      │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
      │           │           │      │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
      │           │           │      │      dn ┼ nothing
      │           │           │      │      dt ┼ nothing
      │           │           │      │     alg ┼ nothing
      │           │           │      │   order ┴ DenoiseDetoneAlg()
      │           │        me ┼ SimpleExpectedReturns
      │           │           │     w ┼ nothing
      │           │           │   idx ┴ nothing
      │           │   horizon ┴ nothing
      │       slv ┼ Solver
      │           │          name ┼ Symbol: :clarabel1
      │           │        solver ┼ UnionAll: Clarabel.MOIwrapper.Optimizer
      │           │      settings ┼ Dict{String, Real}: Dict{String, Real}("verbose" => false, "max_step_fraction" => 0.9)
      │           │     check_sol ┼ @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
      │           │   add_bridges ┴ Bool: true
      │        wb ┼ WeightBounds
      │           │   lb ┼ Float64: 0.0
      │           │   ub ┴ Float64: 1.0
      │       bgt ┼ Float64: 1.0
      │      sbgt ┼ nothing
      │        lt ┼ nothing
      │        st ┼ nothing
      │      lcse ┼ nothing
      │       cte ┼ nothing
      │    gcarde ┼ nothing
      │   sgcarde ┼ nothing
      │      smtx ┼ nothing
      │     sgmtx ┼ nothing
      │       slt ┼ nothing
      │       sst ┼ nothing
      │      sglt ┼ nothing
      │      sgst ┼ nothing
      │        tn ┼ nothing
      │      fees ┼ nothing
      │      sets ┼ nothing
      │        tr ┼ nothing
      │       ple ┼ nothing
      │       ret ┼ ArithmeticReturn
      │           │   ucs ┼ nothing
      │           │    lb ┼ nothing
      │           │    mu ┴ nothing
      │       sca ┼ SumScalariser()
      │      ccnt ┼ nothing
      │      cobj ┼ nothing
      │        sc ┼ Int64: 1
      │        so ┼ Int64: 1
      │        ss ┼ nothing
      │      card ┼ nothing
      │     scard ┼ nothing
      │       nea ┼ nothing
      │        l1 ┼ nothing
      │        l2 ┼ nothing
      │      linf ┼ nothing
      │        lp ┼ nothing
      │    strict ┴ Bool: false
    r ┼ Variance
      │   settings ┼ RiskMeasureSettings
      │            │   scale ┼ Float64: 1.0
      │            │      ub ┼ nothing
      │            │     rke ┴ Bool: true
      │      sigma ┼ nothing
      │       chol ┼ nothing
      │         rc ┼ nothing
      │        alg ┴ SquaredSOCRiskExpr()
  obj ┼ MinimumRisk()
   wi ┼ nothing
   fb ┴ nothing

As you can see, there are a lot of fields in this structure, which correspond to a wide variety of optimisation constraints. We will explore these in the examples. For now, we will perform the optimisation via [optimise]-(@ref).

julia
# Perform the optimisation, res.w contains the optimal weights.
res = optimise(mr, rd)
MeanRiskResult
       oe ┼ DataType: DataType
       pa ┼ ProcessedJuMPOptimiserAttributes
          │        pr ┼ LowOrderPrior
          │           │         X ┼ 440×20 Matrix{Float64}
          │           │        mu ┼ 20-element Vector{Float64}
          │           │     sigma ┼ 20×20 Matrix{Float64}
          │           │      chol ┼ nothing
          │           │         w ┼ nothing
          │           │       ens ┼ nothing
          │           │       kld ┼ nothing
          │           │        ow ┼ nothing
          │           │        rr ┼ nothing
          │           │      f_mu ┼ nothing
          │           │   f_sigma ┼ nothing
          │           │       f_w ┴ nothing
          │        wb ┼ WeightBounds
          │           │   lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
          │           │   ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
          │        lt ┼ nothing
          │        st ┼ nothing
          │      lcsr ┼ nothing
          │       ctr ┼ nothing
          │    gcardr ┼ nothing
          │   sgcardr ┼ nothing
          │      smtx ┼ nothing
          │     sgmtx ┼ nothing
          │       slt ┼ nothing
          │       sst ┼ nothing
          │      sglt ┼ nothing
          │      sgst ┼ nothing
          │        tn ┼ nothing
          │      fees ┼ nothing
          │       plr ┼ nothing
          │       ret ┼ ArithmeticReturn
          │           │   ucs ┼ nothing
          │           │    lb ┼ nothing
          │           │    mu ┴ 20-element Vector{Float64}
  retcode ┼ OptimisationSuccess
          │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
      sol ┼ JuMPOptimisationSolution
          │   w ┴ 20-element Vector{Float64}
    model ┼ A JuMP Model
          │ ├ solver: Clarabel
          │ ├ objective_sense: MIN_SENSE
          │ │ └ objective_function_type: JuMP.QuadExpr
          │ ├ num_variables: 21
          │ ├ num_constraints: 4
          │ │ ├ JuMP.AffExpr in MOI.EqualTo{Float64}: 1
          │ │ ├ Vector{JuMP.AffExpr} in MOI.Nonnegatives: 1
          │ │ ├ Vector{JuMP.AffExpr} in MOI.Nonpositives: 1
          │ │ └ Vector{JuMP.AffExpr} in MOI.SecondOrderCone: 1
          │ └ Names registered in the model
          │   └ :G, :bgt, :dev_1, :dev_1_soc, :k, :lw, :obj_expr, :ret, :risk, :risk_vec, :sc, :so, :variance_flag, :variance_risk_1, :w, :w_lb, :w_ub
       fb ┴ nothing

The solution lives in the sol field, but the weights can be accessed via the w property.

PortfolioOptimisers.jl also has the capability to perform finite allocations, which is useful for those of us without infinite money. There are two ways to do so, a greedy algorithm [GreedyAllocation]-(@ref) that does not guarantee optimality but is fast and always converges, and a discrete allocation [DiscreteAllocation]-(@ref) which uses mixed-integer programming (MIP) and requires a capable solver.

Here we will use the latter.

julia
# Define the MIP solver for finite discrete allocation.
mip_slv = Solver(; name = :highs1, solver = HiGHS.Optimizer,
                 settings = Dict("log_to_console" => false),
                 check_sol = (; allow_local = true, allow_almost = true))

# Discrete finite allocation.
da = DiscreteAllocation(; slv = mip_slv)
DiscreteAllocation
  slv ┼ Solver
      │          name ┼ Symbol: :highs1
      │        solver ┼ DataType: DataType
      │      settings ┼ Dict{String, Bool}: Dict{String, Bool}("log_to_console" => 0)
      │     check_sol ┼ @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
      │   add_bridges ┴ Bool: true
   sc ┼ Int64: 1
   so ┼ Int64: 1
   wf ┼ AbsoluteErrorWeightFinaliser()
   fb ┼ GreedyAllocation
      │     unit ┼ Int64: 1
      │     args ┼ Tuple{}: ()
      │   kwargs ┼ @NamedTuple{}: NamedTuple()
      │       fb ┴ nothing

The discrete allocation minimises the absolute or relative L1- or L2-norm (configurable) between the ideal allocation to the one you can afford plus the leftover cash. As such, it needs to know a few extra things, namely the optimal weights res.w, a vector of the latest prices vec(values(prices[end])), and available cash which we define to be 4206.90.

julia
# Perform the finite discrete allocation, uses the final asset
# prices, and an available cash amount. This is for us mortals
# without infinite wealth.
mip_res = optimise(da, res.w, vec(values(prices[end])), 4206.90)
DiscreteAllocationResult
         oe ┼ DataType: DataType
    retcode ┼ OptimisationSuccess
            │   res ┴ nothing
  s_retcode ┼ nothing
  l_retcode ┼ OptimisationSuccess
            │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
     shares ┼ 20-element SubArray{Float64, 1, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}
       cost ┼ 20-element SubArray{Float64, 1, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}
          w ┼ 20-element SubArray{Float64, 1, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}
       cash ┼ Float64: 0.20003798008110607
    s_model ┼ nothing
    l_model ┼ A JuMP Model
            │ ├ solver: HiGHS
            │ ├ objective_sense: MIN_SENSE
            │ │ └ objective_function_type: JuMP.AffExpr
            │ ├ num_variables: 21
            │ ├ num_constraints: 42
            │ │ ├ JuMP.AffExpr in MOI.GreaterThan{Float64}: 1
            │ │ ├ Vector{JuMP.AffExpr} in MOI.NormOneCone: 1
            │ │ ├ JuMP.VariableRef in MOI.GreaterThan{Float64}: 20
            │ │ └ JuMP.VariableRef in MOI.Integer: 20
            │ └ Names registered in the model
            │   └ :cabs_err, :cr, :r, :sc, :so, :u, :x
         fb ┴ nothing

We can display the results in a table.

julia
# View the results.
df = DataFrame(:assets => rd.nx, :shares => mip_res.shares, :cost => mip_res.cost,
               :opt_weights => res.w, :mip_weights => mip_res.w)
pretty_table(df; formatters = [fmt2])
┌────────┬─────────┬─────────┬─────────────┬─────────────┐
 assets   shares     cost  opt_weights  mip_weights 
 String  Float64  Float64      Float64      Float64 
├────────┼─────────┼─────────┼─────────────┼─────────────┤
│   ACHR │     0.0 │     0.0 │       0.0 % │       0.0 % │
│    AMC │    73.0 │  223.38 │     5.324 % │      5.31 % │
│   ANVS │    22.0 │   48.84 │     1.249 % │     1.161 % │
│     BB │   273.0 │  1228.5 │    29.184 % │    29.203 % │
│   CHPT │    11.0 │  131.34 │     3.002 % │     3.122 % │
│   ENVX │     0.0 │     0.0 │       0.0 % │       0.0 % │
│   EOSE │     8.0 │   100.8 │     2.435 % │     2.396 % │
│    GME │     0.0 │     0.0 │       0.0 % │       0.0 % │
│   LCID │     1.0 │   24.77 │     0.638 % │     0.589 % │
│   LGVN │   325.0 │   256.1 │     6.089 % │     6.088 % │
│   LUNR │     0.0 │     0.0 │       0.0 % │       0.0 % │
│   MARA │     1.0 │   18.82 │     0.613 % │     0.447 % │
│   MAXN │     0.0 │     0.0 │       0.0 % │       0.0 % │
│   NVAX │    28.0 │  264.88 │      6.21 % │     6.297 % │
│   RIVN │    55.0 │  750.75 │    17.897 % │    17.847 % │
│      ⋮ │       ⋮ │       ⋮ │           ⋮ │           ⋮ │
└────────┴─────────┴─────────┴─────────────┴─────────────┘
                                            5 rows omitted

We can also visualise the portfolio using various plotting functions. For example, we can plot the portfolio's cumulative returns, in this case compound returns.

julia
# Plot the portfolio cumulative returns of the finite allocation portfolio.
plot_ptf_cumulative_returns(mip_res.w, rd.X; ts = rd.ts, compound = true)

We can plot the histogram of portfolio returns.

julia
# Plot histogram of returns.
plot_histogram(mip_res.w, rd.X, slv)

We can plot the portfolio drawdowns, in this case compound drawdowns.

julia
# Plot compounded drawdowns.
plot_drawdowns(mip_res.w, rd.X, slv; ts = rd.ts, compound = true)

Furthermore, we can also plot the risk contribution per asset. For this, we must provide an instance of the risk measure we want to use with the appropriate statistics/parameters. We can do this by using the factory function (recommended when doing so programmatically), or manually set the quantities ourselves.

julia
# Plot the risk contribution per asset.
plot_risk_contribution(factory(Variance(), res.pr), mip_res.w, rd.X; nx = rd.nx,
                       percentage = true)

This awkwardness is due to the fact that PortfolioOptimisers.jl tries to decouple the risk measures from optimisation estimators and results. However, the advantage of this approach is that it lets us use multiple different risk measures as part of the risk expression, or as risk limits in optimisations. We explore this further in the examples.

We can also plot the returns' histogram and probability density.

julia
plot_histogram(mip_res.w, rd.X, slv)

We can also plot the compounded or uncompounded drawdowns, here we plot the former.

julia
plot_drawdowns(mip_res.w, rd.X, slv; ts = rd.ts, compound = true)

There are other kinds of plots which we explore in the examples.

Roadmap

  • For a roadmap of planned and desired features in no particular order please refer to Issue #37.

  • Some docstrings are incomplete and/or outdated, please refer to Issue #58 for details on what docstrings have been completed in the dev branch.

Features

This section is under active development so any [<name>]-(@ref) lacks docstrings.

Preprocessing

Matrix processing

Regression models

Factor prior models and implied volatility use regression in their estimation, which return a Regression object.

Regression targets

Regression types

Moment estimation

Expected returns

Overloads Statistics.mean.

Variance and standard deviation

Overloads Statistics.var and Statistics.std.

  • Optionally weighted variance with custom expected returns estimator SimpleVariance

Covariance and correlation

Overloads Statistics.cov and Statistics.cor.

Coskewness

Implements coskewness.

  • Coskewness and spectral decomposition of the negative coskewness with custom expected returns estimator and matrix processing pipeline Coskewness

Cokurtosis

Implements cokurtosis.

  • Cokurtosis with custom expected returns estimator and matrix processing pipeline Cokurtosis

Distance matrices

Implements distance and cor_and_dist.

  • First order distance estimator with custom distance algorithm, and optional exponent Distance

  • Second order distance estimator with custom pairwise distance algorithm from Distances.jl, custom distance algorithm, and optional exponent DistanceDistance

The distance estimators are used together with various distance matrix algorithms.

Phylogeny

PortfolioOptimisers.jl can make use of asset relationships to perform optimisations, define constraints, and compute relatedness characteristics of portfolios.

Clustering

Phylogeny constraints and clustering optimisations make use of clustering algorithms via ClustersEstimator, Clusters, and clusterise. Most clustering algorithms come from Clustering.jl.

Hierarchical
  • Hierarchical clustering HClustAlgorithm

  • Direct Bubble Hierarchical Trees DBHT and Local Global sparsification of the covariance matrix LoGo, logo!, and [logo]-(@ref)

Non-hierarchical

Non-hierarchical clustering algorithms are incompatible with hierarchical clustering optimisations, but they can be used for phylogeny constraints and [NestedClustered]-(@ref) optimisations.

  • K-means clustering [KMeansAlgorithm]-(@ref)

Networks

Adjacency matrices

Adjacency matrices encode asset relationships either with clustering or graph theory via phylogeny_matrix and PhylogenyResult.

Centrality and phylogeny measures

Optimisation constraints

Non clustering optimisers support a wide range of constraints, while naive and clustering optimisers only support weight bounds. Furthermore, entropy pooling prior supports a variety of views constraints. It is therefore important to provide users with the ability to generate constraints manually and/or programmatically. We therefore provide a wide, robust, and extensible range of types such as AbstractEstimatorValueAlgorithm and UniformValues, and functions that make this easy, fast, and safe.

Constraints can be defined via their estimators or directly by their result types. Some using estimators need to map key-value pairs to the asset universe, this is done by defining the assets and asset groups in AssetSets. Internally, PortfolioOptimisers.jl uses all the information and calls group_to_val!, and replace_group_by_assets to produce the appropriate arrays.

Prior statistics

Many optimisations and constraints use prior statistics computed via prior.

Uncertainty sets

In order to make optimisations more robust to noise and measurement error, it is possible to define uncertainty sets on the expected returns and covariance. These can be used in optimisations which use either of these two quantities. These are implemented via ucs, mu_ucs, and sigma_ucs.

PortfolioOptimisers.jl implements two types of uncertainty sets.

It also implements various estimators for the uncertainty sets, the following two can generate box and ellipsoidal sets.

The following estimator can only generate box sets.

Turnover

The turnover is defined as the element-wise absolute difference between the vector of current weights and a vector of benchmark weights. It can be used as a constraint, method for fee calculation, and risk measure. These are all implemented using turnover_constraints, TurnoverEstimator, and Turnover.

Fees

Fees are a non-negligible aspect of active investing. As such PortfolioOptimiser.jl has the ability to account for them in all optimisations but the naive ones. They can also be used to adjust expected returns calculations via calc_fees and calc_asset_fees.

  • Fees FeesEstimator and Fees
    • Proportional long

    • Proportional short

    • Fixed long

    • Fixed short

    • Turnover

Portfolio returns and drawdowns

Various risk measures and analyses require the computation of simple and cumulative portfolio returns and drawdowns both in aggregate and per-asset. These are computed by calc_net_returns, calc_net_asset_returns, cumulative_returns, drawdowns.

Tracking

It is often useful to create portfolios that track the performance of an index, indicator, or another portfolio.

The error can be computed using different algorithms using norm_tracking.

It is also possible to track the error in with risk measures [RiskTrackingError]-(@ref) using WeightsTracking, which allows for two approaches.

Risk measures

PortfolioOptimisers.jl provides a wide range of risk measures. These are broadly categorised into two types based on the type of optimisations that support them.

Risk measures for traditional optimisation

These are all subtypes of RiskMeasure, and are supported by all optimisation estimators.

  • Variance [Variance]
    • Traditional optimisations also support:
  • Standard deviation StandardDeviation

  • Uncertainty set variance UncertaintySetVariance (same as variance when used in non-traditional optimisation)

  • Low order moments LowOrderMoment
  • Kurtosis Kurtosis
  • Negative skewness [NegativeSkewness]-(@ref)
    • Squared negative skewness
      • Full and semi-skewness are supported in traditional optimisers via the sk and V fields. Risk calculation uses
      • Traditional optimisation formulations
      • Square root negative skewness SOCRiskExpr

  • Value at Risk [ValueatRisk]-(@ref)
    • Traditional optimisation formulations
      • Exact MIP formulation [MIPValueatRisk]-(@ref)

      • Approximate distribution based [DistributionValueatRisk]-(@ref)

  • Value at Risk Range [ValueatRiskRange]-(@ref)
    • Traditional optimisation formulations
      • Exact MIP formulation [MIPValueatRisk]-(@ref)

      • Approximate distribution based [DistributionValueatRisk]-(@ref)

  • Drawdown at Risk [DrawdownatRisk]-(@ref)

  • Conditional Value at Risk [ConditionalValueatRisk]-(@ref)

  • Distributionally Robust Conditional Value at Risk [DistributionallyRobustConditionalValueatRisk]-(@ref) (same as conditional value at risk when used in non-traditional optimisation)

  • Conditional Value at Risk Range [ConditionalValueatRiskRange]-(@ref)

  • Distributionally Robust Conditional Value at Risk Range [DistributionallyRobustConditionalValueatRiskRange]-(@ref) (same as conditional value at risk range when used in non-traditional optimisation)

  • Conditional Drawdown at Risk [ConditionalDrawdownatRisk]-(@ref)

  • Distributionally Robust Conditional Drawdown at Risk [DistributionallyRobustConditionalDrawdownatRisk]-(@ref)(same as conditional drawdown at risk when used in non-traditional optimisation)

  • Entropic Value at Risk [EntropicValueatRisk]-(@ref)

  • Entropic Value at Risk Range [EntropicValueatRiskRange]-(@ref)

  • Entropic Drawdown at Risk [EntropicDrawdownatRisk]-(@ref)

  • Relativistic Value at Risk [RelativisticValueatRisk]-(@ref)

  • Relativistic Value at Risk Range [RelativisticValueatRiskRange]-(@ref)

  • Relativistic Drawdown at Risk [RelativisticDrawdownatRisk]-(@ref)

  • Ordered Weights Array
    • Risk measures
      • Ordered Weights Array risk measure [OrderedWeightsArray]-(@ref)

      • Ordered Weights Array range risk measure [OrderedWeightsArrayRange]-(@ref)

    • Traditional optimisation formulations
      • Exact [ExactOrderedWeightsArray]-(@ref)

      • Approximate [ApproxOrderedWeightsArray]-(@ref)

    • Array functions
      • Gini Mean Difference owa_gmd

      • Worst Realisation owa_wr

      • Range owa_rg

      • Conditional Value at Risk owa_cvar

      • Weighted Conditional Value at Risk owa_wcvar

      • Conditional Value at Risk Range owa_cvarrg

      • Weighted Conditional Value at Risk Range owa_wcvarrg

      • Tail Gini owa_tg

      • Tail Gini Range owa_tgrg

      • Linear moments (L-moments)
        • Linear Moment owa_l_moment

        • Linear Moment Convex Risk Measure owa_l_moment_crm
          • L-moment combination formulations
            • Maximum Entropy [MaximumEntropy]-(@ref)
              • Exponential Cone Entropy [ExponentialConeEntropy]-(@ref)

              • Relative Entropy [RelativeEntropy]-(@ref)

            • Minimum Squared Distance [MinimumSquaredDistance]-(@ref)

            • Minimum Sum Squares [MinimumSumSquares]-(@ref)

  • Average Drawdown [AverageDrawdown]-(@ref)

  • Ulcer Index [UlcerIndex]-(@ref)

  • Maximum Drawdown [MaximumDrawdown]-(@ref)

  • Brownian Distance Variance [BrownianDistanceVariance]-(@ref)
    • Traditional optimisation formulations
      • Distance matrix constraint formulations
        • Norm one cone Brownian distance variance [NormOneConeBrownianDistanceVariance]-(@ref)

        • Inequality Brownian distance variance [IneqBrownianDistanceVariance]-(@ref)

      • Risk formulation
  • Worst Realisation [WorstRealisation]-(@ref)

  • Range [Range]-(@ref)

  • Turnover Risk Measure [TurnoverRiskMeasure]-(@ref)

  • Tracking Risk Measure [TrackingRiskMeasure]-(@ref)
  • Risk Tracking Risk Measure
  • Power Norm Value at Risk [PowerNormValueatRisk]-(@ref)

  • Power Norm Value at Risk Range [PowerNormValueatRiskRange]-(@ref)

  • Power Norm Drawdown at Risk [PowerNormDrawdownatRisk]-(@ref)

Risk measures for hierarchical optimisation

These are all subtypes of HierarchicalRiskMeasure, and are only supported by hierarchical optimisation estimators.

  • High order moment HighOrderMoment
  • Relative Drawdown at Risk [RelativeDrawdownatRisk]-(@ref)

  • Relative Conditional Drawdown at Risk [RelativeConditionalDrawdownatRisk]-(@ref)

  • Relative Entropic Drawdown at Risk [RelativeEntropicDrawdownatRisk]-(@ref)

  • Relative Relativistic Drawdown at Risk [RelativeRelativisticDrawdownatRisk]-(@ref)

  • Relative Average Drawdown [RelativeAverageDrawdown]-(@ref)

  • Relative Ulcer Index [RelativeUlcerIndex]-(@ref)

  • Relative Maximum Drawdown [RelativeMaximumDrawdown]-(@ref)

  • Relative Power Norm Drawdown at Risk [RelativePowerNormDrawdownatRisk]-(@ref)

  • Risk Ratio Risk Measure [RiskRatioRiskMeasure]-(@ref)

  • Equal Risk Measure [EqualRiskMeasure]-(@ref)

  • Median Absolute Deviation [MedianAbsoluteDeviation]-(@ref)

Non-optimisation risk measures

These risk measures are unsuitable for optimisation because they can return negative values. However, they can be used for performance metrics.

  • Mean Return [MeanReturn]-(@ref)

  • Third Central Moment [ThirdCentralMoment]-@(ref)

  • Skewness [Skewness]-(@ref)

  • Return Risk Measure ExpectedReturn

  • Return Risk Ratio Risk Measure ExpectedReturnRiskRatio

Performance metrics

  • Expected risk [expected_risk]-(@ref)

  • Number of effective assets [number_effective_assets]-(@ref)

  • Risk contribution
    • Asset risk contribution [risk_contribution]-(@ref)

    • Factor risk contribution [factor_risk_contribution]-(@ref)

  • Expected return expected_return
    • Arithmetic [ArithmeticReturn]-(@ref)

    • Logarithmic [LogarithmicReturn]-(@ref)

  • Expected risk-adjusted return ratio expected_ratio and expected_risk_ret_ratio

  • Expected risk-adjusted ratio information criterion expected_sric and expected_risk_ret_sric

  • Brinson performance attribution brinson_attribution

Portfolio optimisation

Optimisations are implemented via [optimise]-(@ref). Optimisations consume an estimator and return a result.

Naive

These return a [NaiveOptimisationResult]-(@ref).

  • Inverse Volatility [InverseVolatility]-(@ref)

  • Equal Weighted [EqualWeighted]-(@ref)

  • Random (Dirichlet) [RandomWeighted]-(@ref)

Naive optimisation features
  • Weight bounds WeightBoundsEstimator, UniformValues, and WeightBounds

  • Weight finalisers
    • Iterative Weight Finaliser [IterativeWeightFinaliser]-(@ref)

    • JuMP Weight Finaliser [JuMPWeightFinaliser]-(@ref)
      • Relative Error Weight Finaliser [RelativeErrorWeightFinaliser]-(@ref)

      • Squared Relative Error Weight Finaliser [SquaredRelativeErrorWeightFinaliser]-(@ref)

      • Absolute Error Weight Finaliser [AbsoluteErrorWeightFinaliser]-(@ref)

      • Squared Absolute Error Weight Finaliser [SquaredAbsoluteErrorWeightFinaliser]-(@ref)

Traditional

These optimisations are implemented as JuMP problems and make use of [JuMPOptimiser]-(@ref), which encodes all supported constraints.

Objective function optimisations

These optimisations support a variety of objective functions.

  • Objective functions
    • Minimum risk [MinimumRisk]-(@ref)

    • Maximum utility [MaximumUtility]-(@ref)

    • Maximum return over risk ratio [MaximumRatio]-(@ref)

    • Maximum return [MaximumReturn]-(@ref)

  • Exclusive to [MeanRisk]-(@ref) and [NearOptimalCentering]-(@ref)
    • N-dimensional Pareto fronts Frontier
      • Return based

      • Risk based

  • Optimisation estimators
    • Mean-Risk [MeanRisk]-(@ref) returns a [MeanRiskResult]-(@ref)

    • Near Optimal Centering [NearOptimalCentering]-(@ref) returns a [NearOptimalCenteringResult]-(@ref)

    • Factor Risk Contribution [FactorRiskContribution]-(@ref) returns a [FactorRiskContributionResult]-(@ref)

Risk budgeting optimisations

These optimisations attempt to achieve weight values according to a risk budget vector. This vector can be provided on a per asset or per factor basis.

  • Budget targets
    • Asset risk budgeting [AssetRiskBudgeting]-(@ref)

    • Factor risk budgeting [FactorRiskBudgeting]-(@ref)

  • Optimisation estimators
    • Risk Budgeting [RiskBudgeting]-(@ref) returns a [RiskBudgetingResult]-(@ref)

    • Relaxed Risk Budgeting [RelaxedRiskBudgeting]-(@ref) returns a [RiskBudgetingResult]-(@ref)
      • Basic [BasicRelaxedRiskBudgeting]-(@ref)

      • Regularised [RegularisedRelaxedRiskBudgeting]-(@ref)

      • Regularised and penalised [RegularisedPenalisedRelaxedRiskBudgeting]-(@ref)

Traditional optimisation features

Clustering optimisation

Clustering optimisations make use of asset relationships to either minimise the risk exposure by breaking the asset universe into subsets which are hierarchically or individually optimised.

Hierarchical clustering optimisation

These optimisations minimise risk by hierarchically splitting the asset universe into subsets, computing the risk of each subset, and combining them according to their hierarchy.

  • Hierarchical Risk Parity [HierarchicalRiskParity]-(@ref) returns a [HierarchicalResult]-(@ref)

  • Hierarchical Equal Risk Contribution [HierarchicalEqualRiskContribution]-(@ref) returns a [HierarchicalResult]-(@ref)

Hierarchical clustering optimisation features
  • Weight bounds WeightBoundsEstimator, UniformValues, and WeightBounds

  • Fees FeesEstimator and Fees

  • Risk vector scalarisation
  • Weight finalisers
    • Iterative Weight Finaliser [IterativeWeightFinaliser]-(@ref)

    • JuMP Weight Finaliser [JuMPWeightFinaliser]-(@ref)
      • Relative Error Weight Finaliser [RelativeErrorWeightFinaliser]-(@ref)

      • Squared Relative Error Weight Finaliser [SquaredRelativeErrorWeightFinaliser]-(@ref)

      • Absolute Error Weight Finaliser [AbsoluteErrorWeightFinaliser]-(@ref)

      • Squared Absolute Error Weight Finaliser [SquaredAbsoluteErrorWeightFinaliser]-(@ref)

Schur complementary optimisation

Schur complementary hierarchical risk parity provides a bridge between mean variance optimisation and hierarchical risk parity by using an interpolation parameter. It converges to hierarchical risk parity, and approximates mean variance by adjusting this parameter. It uses the Schur complement to adjust the weights of a portfolio according to how much more useful information is gained by assigning more weight to a group of assets.

  • Schur Complementary Hierarchical Risk Parity [SchurComplementHierarchicalRiskParity]-(@ref) returns a [SchurComplementHierarchicalRiskParityResult]-(@ref)
Schur complementary optimisation features
  • Weight bounds WeightBoundsEstimator, UniformValues, and WeightBounds

  • Fees FeesEstimator and Fees

  • Weight finalisers
    • Iterative Weight Finaliser [IterativeWeightFinaliser]-(@ref)

    • JuMP Weight Finaliser [JuMPWeightFinaliser]-(@ref)
      • Relative Error Weight Finaliser [RelativeErrorWeightFinaliser]-(@ref)

      • Squared Relative Error Weight Finaliser [SquaredRelativeErrorWeightFinaliser]-(@ref)

      • Absolute Error Weight Finaliser [AbsoluteErrorWeightFinaliser]-(@ref)

      • Squared Absolute Error Weight Finaliser [SquaredAbsoluteErrorWeightFinaliser]-(@ref)

Nested clusters optimisation

Nested clustered optimisation breaks the asset universe of size N into C smaller subsets and treats every subset as an individual portfolio. The weights assigned to each asset are placed in an N×C matrix. In each column, non-zero values correspond to assets assigned to that subset, this means that assets only contribute to the column (and therefore synthetic asset) corresponding to their assigned subset. In other words, each row of the matrix contains a single non-zero value and each row contains as many non-zero values as there are assets in that subset.

From here there are two options:

  1. Compute the returns matrix of the synthetic assets directly by multiplying the original T×N matrix by the N×C matrix of asset weights to produce a T×C matrix of predicted returns, where T is the number of observations.

  2. For each subset perform a cross validation prediction, yielding a vector of returns for that subset. These vectors are then horizontally concatenated into a Y×C matrix of cross-validation predicted returns, where Y ≤ T because the cross validation may not use the full history.

This matrix of predicted returns is then used by the outer optimisation estimator to generate an optimisation of the synthetic assets. This produces a C×1 vector, essentially optimising a portfolio of asset clusters. The final weights are the product of the original N×C matrix of asset weights per cluster by the C×1 vector of optimal synthetic asset weights to produce the final N×1 vector of asset weights.

  • Nested Clustered [NestedClustered]-(@ref) returns a [NestedClusteredResult]-(@ref)
Clustering optimisation features
  • Any features supported by the inner and outer estimators.

  • Weight bounds WeightBoundsEstimator, UniformValues, and WeightBounds

  • Weight finalisers
    • Iterative Weight Finaliser [IterativeWeightFinaliser]-(@ref)

    • JuMP Weight Finaliser [JuMPWeightFinaliser]-(@ref)
      • Relative Error Weight Finaliser [RelativeErrorWeightFinaliser]-(@ref)

      • Squared Relative Error Weight Finaliser [SquaredRelativeErrorWeightFinaliser]-(@ref)

      • Absolute Error Weight Finaliser [AbsoluteErrorWeightFinaliser]-(@ref)

      • Squared Absolute Error Weight Finaliser [SquaredAbsoluteErrorWeightFinaliser]-(@ref)

  • Cross validation predictor for the outer estimator

Ensemble optimisation

These work similar to the Nested Clustered estimator, only instead of breaking the asset universe into subsets, a list of inner estimators is provided. The procedure is then exactly the same as the nested clusters optimisation, only instead of an N×C matrix of asset weights where each column corresponds to a subset of assets, each column corresponds to a completely independent and isolated inner estimator, which also means there is no enforced sparsity pattern on this matrix.

  • Stacking [Stacking]-(@ref) returns a [StackingResult]-(@ref)
Ensemble optimisation features
  • Any features supported by the inner and outer estimators.

  • Weight bounds WeightBoundsEstimator, UniformValues, and WeightBounds

  • Weight finalisers
    • Iterative Weight Finaliser [IterativeWeightFinaliser]-(@ref)

    • JuMP Weight Finaliser [JuMPWeightFinaliser]-(@ref)
      • Relative Error Weight Finaliser [RelativeErrorWeightFinaliser]-(@ref)

      • Squared Relative Error Weight Finaliser [SquaredRelativeErrorWeightFinaliser]-(@ref)

      • Absolute Error Weight Finaliser [AbsoluteErrorWeightFinaliser]-(@ref)

      • Squared Absolute Error Weight Finaliser [SquaredAbsoluteErrorWeightFinaliser]-(@ref)

  • Cross validation predictor for the outer estimator

Finite allocation optimisation

Unlike all other estimators, finite allocation does not yield an "optimal" value, but rather the optimal attainable solution based on a finite amount of capital. They use the result of other estimations, the latest prices, and a cash amount.

  • Discrete (MIP) [DiscreteAllocation]-(@ref)
    • Weight finalisers
      • Iterative Weight Finaliser [IterativeWeightFinaliser]-(@ref)

      • JuMP Weight Finaliser [JuMPWeightFinaliser]-(@ref)
        • Relative Error Weight Finaliser [RelativeErrorWeightFinaliser]-(@ref)

        • Squared Relative Error Weight Finaliser [SquaredRelativeErrorWeightFinaliser]-(@ref)

        • Absolute Error Weight Finaliser [AbsoluteErrorWeightFinaliser]-(@ref)

        • Squared Absolute Error Weight Finaliser [SquaredAbsoluteErrorWeightFinaliser]-(@ref)

  • Greedy [GreedyAllocation]

Cross validation

  • Prediction on unseen data [PredictionReturnsResult]-(@ref), [PredictionResult]-(@ref), [MultiPeriodPredictionResult]-(@ref), [PopulationPredictionResult]-(@ref) via [predict]-(@ref), [fit_and_predict]-(@ref)

  • Prediction scoring via [PredictionCrossValScorer]-(@ref), [NearestQuantilePrediction]-(@ref), and [quantile_by_measure]-(@ref)

  • Cross validation estimators used via [split]-(@ref) and [cross_val_predict]-(@ref)
    • K-Fold KFold returns a [KFoldResult]-(@ref)

    • Combinatorial CombinatorialCrossValidation returns a [CombinatorialCrossValidationResult]-(@ref)

    • Walk forward [WalkForward]-(@ref) return a [WalkForwardResult]-(@ref)
    • Multiple randomised [MultipleRandomised]-(@ref) returns a [MultipleRandomisedResult]-(@ref)

Plotting

Visualising the results is quite a useful way of summarising the portfolio characteristics or evolution. To this extent we provide a few plotting functions with more to come.

  • Simple or compound cumulative returns.
    • Portfolio [plot_ptf_cumulative_returns]-(@ref).

    • Assets [plot_asset_cumulative_returns]-(@ref).

  • Portfolio composition.
    • Single portfolio [plot_composition]-(@ref).

    • Multi portfolio.
      • Stacked bar [plot_stacked_bar_composition]-(@ref).

      • Stacked area [plot_stacked_area_composition]-(@ref).

  • Risk contribution.
    • Asset risk contribution [plot_risk_contribution]-(@ref).

    • Factor risk contribution [plot_factor_risk_contribution]-(@ref).

  • Asset dendrogram [plot_dendrogram]-(@ref).

  • Asset clusters + optional dendrogram [plot_clusters]-(@ref).

  • Simple or compound drawdowns [plot_drawdowns]-(@ref).

  • Portfolio returns histogram + density [plot_histogram]-(@ref).

  • 2/3D risk measure scatter plots [plot_measures]-(@ref).