Docstring dictionaries

PortfolioOptimisersModule

PortfolioOptimisers.jl

PortfolioOptimisers.jl is a portfolio optimisation (portfolio optimization) library for Julia. Every component is an immutable estimator you compose, so a prior, a risk measure or a constraint swaps out without touching the optimiser.

CategoryBadge
DocsStable Documentation Development documentation
CITest workflow status Docs workflow Status Aqua
CoverageCoverage
ContributeContributor Covenant
MiscBestieTemplate

<!– Build Status DOI –>

<!– All Contributors –>

[!CAUTION] 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.

What it does

  • Optimisers — mean-risk, risk budgeting and relaxed risk budgeting, near-optimal centering, hierarchical risk parity, hierarchical equal risk contribution, Schur complement, nested clustered optimisation, stacking, subset resampling and naive portfolios, with discrete and greedy finite allocation. See the optimisers guide.
  • Risk measures — over 50 risk measures: variance, semi-moments, mean absolute deviation, VaR, CVaR, EVaR and RLVaR, ordered weights arrays, average, maximum and ulcer drawdowns, worst realisation, range, tracking and turnover measures, skewness and kurtosis. See the risk measures guide.
  • Priors and views — empirical, factor and high-order priors; four Black-Litterman variants, entropy pooling in Meucci's form and the general form, and opinion pooling. See the data and priors guide.
  • Moment estimation — Gerber, Gerber-IQ and Smyth-Broby covariances, distance and mutual-information covariance, denoising, detoning, regime-adjusted exponentially-weighted covariance, coskewness and cokurtosis. See the covariance estimation example.
  • Constraints and costs — budget, group, factor exposure, cardinality, turnover, tracking, phylogeny and centrality constraints; fees and market impact; your own JuMP expressions. See the constraints and costs guide.
  • Validation and tuning — walk-forward and combinatorial cross-validation, grid and randomised hyperparameter search, and pipelines. See the validation and tuning guide.

The capability catalogue is the full inventory, generated from the live package.

Installation

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

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

Roadmap

  • The Issues page is used as a tracker for bugs, feature requests, plans, and works in progress.

  • The dev branch is used as a staging ground before merging into main.

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 is needed to load the plotting extension.
  • Clarabel and HiGHS are the optimisers we will use.
  • CSV, TimeSeries and DataFrames for loading and preprocessing price data.
  • PrettyTables for displaying the results.

We use the S&P 500 sample dataset shipped in examples/SP500.csv.gz: daily adjusted close prices for 20 large-cap stocks. To keep things quick, we only use the most recent year.

# Import module and plotting extension.using PortfolioOptimisers, StatsPlots, GraphRecipes# Import optimisers.using Clarabel, HiGHS# Load and preprocess data.using CSV, TimeSeries, DataFrames# Pretty printing.using PrettyTables# Format for pretty tables.fmt1 = (v, i, j) -> begin    if j == 1        return Date(v)    else        return v    endend;fmt2 = (v, i, j) -> begin    if j  (1, 2, 3)        return v    else        return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v    endend# Load the shipped S&P 500 price data as a TimeArray (run from the repo root).prices = TimeArray(CSV.File(joinpath("examples", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]#=Any price history with a `Date` column and one column per asset works. To pull livedata instead, download it with YFinance and assemble a TimeArray:    using YFinance, TimeSeries    function stock_price_to_time_array(x)        coln = collect(keys(x))[3:end]        m = hcat([x[k] for k in coln]...)        return TimeArray(x["timestamp"], m, Symbol.(coln), x["ticker"])    end    assets = sort!(["AAPL", "AMD", "BAC", "BBY", "CVX", "GE", "HD", "JNJ", "JPM", "KO",                    "LLY", "MRK", "MSFT", "PEP", "PFE", "PG", "RRC", "UNH", "WMT", "XOM"])    prices = get_prices.(assets; startdt = "2024-01-01", enddt = "2025-01-01")    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])# Compute the returns.rd = prices_to_returns(prices)# 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))# `PortfolioOptimisers.jl` implements a number of optimisation types as estimators. All the ones which use mathematical optimisation require a `JuMPOptimiser` structure which defines general solver constraints. This structure in turn requires an instance (or vector) of `Solver`.opt = JuMPOptimiser(; slv = slv);# Vanilla (Markowitz) mean risk optimisation, i.e. minimum variance portfoliomr = MeanRisk(; opt = opt)# Perform the optimisation, res.w contains the optimal weights.res = optimise(mr, rd)# 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)# 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, FiniteAllocationInput(; w = res.w, prices = vec(values(prices[end])), cash = 4206.90))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])# Plot the portfolio cumulative returns of the finite allocation portfolio.plot_portfolio_cumulative_returns(mip_res.w, rd.X; ts = rd.ts, compound = true)

Fig. 1

# 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.plot_risk_contribution(factory(Variance(), res.pr), mip_res.w, rd.X; nx = rd.nx, erc = false)# 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](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/00_Examples).

Fig. 2

# We can also plot the returns' histogram and probability density.plot_histogram(mip_res.w, rd.X; slv = slv)

Fig. 3

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

Fig. 4

source