# \[ANN\] PortfolioOptimisers.jl: Ape together strong

**URL:** <https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099>\
**Category:** Package Announcements\
**Tags:** plotting, jump, optimization, clustering, portfolioanalytics\
**Created:** [October 13, 2025, 12:22am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099 "2025-10-13T00:22:45Z")\
**Posts on this page:** 18\
**Page:** 1

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [October 13, 2025, 12:22am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/1 "2025-10-13T00:22:45Z")

</div>

# PortfolioOptimisers.jl

_Editorial note: If you read this post and then go to the docs’ homepage or the package’s readme and they seem familiar, it’s because I started writing this before I had a proper readme._

Portfolio optimisation is the science of reducing investment risk by being clever about how you distribute your money. Ironically, some of the most robust ways to ensure risk is minimised is to distribute your money equally among a portfolio of proven assets. There exist however, a rather large number of methods, risk measures, constraints, prior statistics estimators, etc. Which give a huge number of combinations.

PortfolioOptimisers.jl is an attempt at providing as many as possible, and to make it possible to add more by leveraging Julia’s type system.

The feature list is _quite large_ and under _active development_. New features will be added over time. Check out the [examples](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/1_Getting_Started) and [API](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/api/00_Introduction) documentation for details.

Please feel free to file issues and/or start discussions if you have any issues using the library, or if I haven’t got to writing docs/examples for something you need. That way I know what to prioritise.

## Caveats

### Documentation

- Mathematical formalism: I’ve got API documentation for a lot of features, but the mathematical formalisms aren’t yet thoroughly explained. It’s more of a high level view.
- Citation needed: I haven’t gone over all the citations for the docs because stabilising the API, adding new features, and writing the API docs has taken priority.
- Docstring examples: some features require set up steps, and I haven’t had the patience to do that. Mostly the examples are still mostly for doctesting my implementation of `Base.show` for my types, and showcasing low-hanging fruit of functionality.

### API

- Unstable: there will likely be breaking changes as I figure out better, more general ways to do things, or better naming conventions.

### Internals

- Dependencies: some deps are only used for certain small things, I may end up removing them in favour of having just the small bit of functionality the package needs. I’m very open to replacement suggestions.

## Features

The feature list is rather large, so I will attempt to summarise it ~~via interpretative dance~~ as best I can. There are also some experimental features (some tracking risk measures) that I’m not sure how well they’d perform, but they’re interesting nonetheless, especially when used in clustering optimisations. Luckily, those haven’t been documented yet, so I haven’t had to reckon with the consequences of my actions just yet.

Without further ado, here is a summary of the features in this package.

### Price data

Every optimisation but the finite allocation work off of returns data. Some optimisations may use price data in the future.

- Preprocessing to drop highly correlated and/or incomplete data. These are not well integrated yet, but the functions exist.
- Computing them, validating and cleaning up data.

### Co-moment matrix processing

Price data is often noisy and follows general macroeconomic trends. Every optimisation model is at risk of overfitting the data. In particular, those which rely on summary statistics (moments) can be overly sensitive to the input data, for example a covariance matrix. It is therefore important to have methods that increase the robustness of their estimation.

- Positive definite projection.
- Matrix denoising.
  - Spectral, shrunk, fixed.

- Matrix detoning.

### Moment estimation

Many of these can be used in conjunction. For example, some covariance estimators use expected returns, or variance estimators in their calculation, and some expected returns use the covariance in turn. Also, some accept weight vectors.

- Expected returns.
  - Arithmetic expected returns.
  - Shrunk expected returns.
    - James-Stein, Bayes-Stein, Bodnar-Okhrin-Parolya. All of them with Grand Mean, Volatility Weighted, Mean Squared Error targets.

  - Equilibrium expected returns.
  - Excess expected returns.

- Variance.
- Covariance/Correlation matrix.
  - Custom: estimator + processing pipeline.
  - Pearson: weighted, unweighted, any `StatsBase.CovarianceEstimator`.
    - Full.
    - Semi.

  - Gerber.
    - Gerber 0, 1, 2. Standardised and unstandardised.

  - Smyth-Broby.
    - Smyth-Broby 0, 1, 2. Standardised and unstandardised.
    - Smyth-Broby-Gerber 0, 1, 2. Standardised and unstandardised.

  - Distance covariance.
  - Lower tail dependence.
  - Kendall.
  - Spearman.
  - Mutual information.
    - Predefined, Hacine-Gharbi-Ravier, Knuth, Scott, Freedman-Draconis bin widths.

  - Coskewness.
    - Full.
    - Semi.

  - Cokurtosis.
    - Full.
    - Semi.

  - Implied volatility.

### Regression

Factor models and implied volatility use regression in their estimation.

- Stepwise.
  - Forward and Backward.
    - P-value, Corrected and “vanilla” Akaike info, Bayesian info, R-squared, and Adjusted R-squared criteria.

- Dimensional reduction.
  - Principal Component.
  - Probabilistic Principal Component.

### Ordered weights array and Linear moments

- Ordered weights arrays.
  - Gini Mean Difference.
  - Conditional Value at Risk.
  - Weighted Conditional Value at Risk.
  - Tail Gini.
  - Worst Realisation.
  - Range.
  - Conditional Value at Risk Range.
  - Weighted Conditional Value at Risk Range.
  - Tail Gini Range.

- Linear Moments Convex Risk Measure: linear moments can be combined using different minimisation targets.
  - Normalised Constant Relative Risk Aversion.
  - Minimum Squared Distance.
  - Minimum Sum Squares.

### Distance

Distance matrices are used for clustering. They are related to correlation distances, but all positive and with zero diagonal.

- Distance: these compare pairwise relationships.
- Distance of distances: these are computed by applying a distance metric to every pair of columns/rows of the distance matrix. They compare the entire space and often give more stable clusters.

Individual entries can be raised to an integer power and scaled according to whether that power is even or odd. The following methods can be used to compute distance matrices.

- Simple.
- Absolute.
- Logarithmic.
- Correlation.
- Variation of Information.
  - Predefined, Hacine-Gharbi-Ravier, Knuth, Scott, Freedman-Draconis bin widths.

- Canonical: depends on the covariance/correlation estimator used.

### Phylogeny

These define asset relationships. They can be used to set constraints on and/or compute the relatedness of assets in a portfolio.

- Clustering.
  - Optimal number of clusters:
    - Predefined, Second order difference, Standardised silhouette scores.

  - Hierarchical clustering.
  - Direct Bubble Hierarchy Trees.
    - Local Global sparsification of the inverse covariance/correlation matrix.

- Phylogeny matrices.
  - Network (Minimum Spanning Tree) adjacency.
  - Clustering adjacency.

- Centrality vectors and average centrality.
  - Betweenness, Closeness, Degree, Eigenvector, Katz, Pagerank, Radiality, Stress centrality measures.

- Asset phylogeny score.

### Constraint generation

These let users easily manually or programatically define optimisation constraints.

- Equation parsing.
- Linear weights.
- Risk budget.
- Asset set matrices.
- Phylogeny.
  - Phylogeny matrix.
    - Semi definite.
    - Mixed integer programming.

  - Centrality.

- Weight bounds.
- Buy-in threshold.

### Prior statistics

As previously mentioned, every optimisation but the finite allocation work off of returns data. These returns can be adjusted and summarised using these estimators. Like the moment estimators, these can be mixed in various ways.

- Empirical.
- Factor model.
- High order moments (coskewness and cokurtosis).
- Black-Litterman.
  - Vanilla.
  - Bayesian.
  - Factor model.
  - Augmented.

- Entropy pooling.
- Opinion pooling.

### Uncertainty sets

These sets can be used to make some optimisations more robust. Namely, there exist uncertainty sets on expected returns and covariance. They can be used on any optimisation which uses any one of these quantities.

- Box.
  - Delta.
  - Normally distributed returns.
  - Autoregressive Conditional Heteroskedasticity.
    - Circular, moving, stationary bootstrap.

- Ellipse uncertainty sets.
  - Normally distributed returns.
  - Autoregressive Conditional Heteroskedasticity.
    - Circular, moving, stationary bootstrap.

### Turnover (Rebalancing)

These penalise moving away from a benchmark vector of weights.

- Risk measure (experimental).
- Constraints.
- Fees.

### Fees

These encode various types of fees, which can be used in portfolio optimisation and analysis.

- Relative long.
- Relative short
- Fixed long.
- Fixed short.
- Turnover (rebalance).

### Tracking

These can be used to track the performance of an index, indicator, or portfolio.

- Risk measure (experimental).
- Constraints.

There are four things that can be tracked.

- Returns via L1 or L2 norm.
  - Asset weights.
  - Returns vector.

- Risk tracking via asset weights.
  - Dependent variables (experimental).
  - Independent variables.

### Risk measures

Different optimisations support different risk measures, most measures can also be used to quantify a portfolio’s risk-return characteristics.

- Variance.
- Risk Contribution Variance.
  - Asset risk contribution.
  - Factor risk contribution.

- Uncertainty set variance.
- Standard deviation.
- First lower moment.
- Second lower moment.
  - Semi variance.
  - Semi deviation.

- Second central moment (historical returns, no covariance matrix).
  - Variance.
  - Standard deviation.

- Mean absolute deviation.
- Third lower moment (historical returns, no coskewness matrix).
  - Standardised (semi skewness).
  - Unstandardised.

- Fourth lower moment (historical returns, no cokurtosis matrix).
  - Standardised (semi kurtosis).
  - Unstandardised.

- Third central moment (historical returns, no coskewness matrix).
  - Standardised (skewness).
  - Unstandardised.

- Fourth central moment (historical returns, no cokurtosis matrix).
  - Standardised (kurtosis).
  - Unstandardised.

- Square root kurtosis.
  - Full.
  - Semi.

- Negative skewness.
  - Full.
  - Semi (experimental).

- Negative quadratic skewness.
  - Full.
  - Semi (experimental).

- Value at Risk.
- Conditional Value at Risk.
- Distributionally Robust Conditional Value at Risk.
- Entropic Value at Risk.
- Relativistic Value at Risk.
- Value at Risk Range.
- Conditional Value at Risk Range.
- Distributionally Robust Conditional Value at Risk Range.
- Entropic Value at Risk Range.
- Relativistic Value at Risk Range.
- Drawdown at Risk.
  - Absolute (simple returns).
  - Relative (compounded returns).

- Conditional Drawdown at Risk.
  - Absolute (simple returns).
  - Relative (compounded returns).

- Entropic Drawdown at Risk.
  - Absolute (simple returns).
  - Relative (compounded returns).

- Relativistic Drawdown at Risk.
  - Absolute (simple returns).
  - Relative (compounded returns).

- Ordered Weights Array risk measure.
- Ordered Weights Array range risk measure.
- Average Drawdown.
- Ulcer Index.
- Maximum Drawdown.
- Brownian Distance Variance.
- Worst Realisation.
- Range.
- Equal risk.
- Turnover risk.
- Tracking risk.
- Mean return risk.
- Ratio of measures.

### Portfolio statistics

These are used to summarise a portfolio’s risk and return characteristics.

- Expected returns.
  - Arithmetic.
  - Kelly (Logarithmic).

- Risk-adjusted return ratio.
  - Vanilla.
  - Sharpe ratio information criterion.

- Risk contribution.
  - Asset risk contribution.
  - Factor risk contribution.

### Optimisation

There are many different optimisation methods, each with different characteristics and configurable options, including exclusive constraint types and risk measures. Though all of them have an optional fallback method in case the optimisation fails.

- Clustering.
  - Hierarchical Risk Parity.
  - Hierarchical Equal Risk Contribution.
  - Nested Clustered Optimisation.
  - Schur Complement Hierarchical Risk Parity.

- `JuMP`-based.
  - Mean Risk.
  - Factor Risk Contribution.
  - Near Optimal Centering.
  - Risk Budgeting.
    - Asset risk budgeting.
    - Factor risk budgeting.

  - Relaxed Risk Budgeting.

- Stacking.
- Naive.
  - Inverse volatility.
  - Equal weighted.
  - Random weighted.

- Finite Allocation.
  - Discrete.
  - Greedy.

### Optimisation constraints

Many of these use the various constraint generation mechanisms mentioned above. These constrain the optimisation so the results meet the user’s requirements. Some have specific requirements like a Mixed Integer Programming capable solver, others cannot be used in conjunction with each other, and there exist combinations that make problems infeasible.

- `JuMP`-based.
  - Risk constraints.
    - Maximum risk for all supported measures (can be simultaneously provided).

  - Return constraints.
    - Minimum return.
    - Expected return uncertainty set.

  - Pareto front/surface/hypersurface (efficient frontier 2D, 3D, ND).
    - Via risk constraints.
    - Via return constraints.

  - Objective functions.
    - Minimum risk.
    - Maximum utility.
    - Maximum risk-adjusted return ratio.
    - Maximum return.

  - Budget constraints.
    - Long and/or short budget.
      - Exact.
      - Upper and lower bounds.

    - Cost budget.
    - Market impact budget.

  - Weight bounds.
  - Linear weights.
  - Cardinality.
    - Asset.
    - Set.

  - Group cardinality.
    - Asset.
    - Set.

  - Long and short buy-in threshold.
  - Turnover.
  - Fees.
  - Tracking.
  - Phylogeny.
  - Centrality.
  - Regularisation.
    - L1.
    - L2.

  - Custom: via subtyping and multiple dispatch.
    - Constraint.
    - Objective.
    - Objective penalty.

- Non-`JuMP`-based.
  - Weight bounds.
    - Upper.
    - Lower.

  - Weight finaliser.

- Optimisers without a fixed risk measure.
  - Scalarisers for multiple simultaneous risk measures.
    - Weighted sum.
    - Max risk.
    - LogSumExp.

### Plotting

- Simple or compound cumulative returns.
  - Portfolio.
  - Assets.

- Portfolio composition.
  - Single portfolio.
  - Multi portfolio.
    - Stacked bar.
    - Stacked area.

- Risk contribution.
  - Asset risk contribution.
  - Factor risk contribution.

- Asset dendrogram.
- Asset clusters + optional dendrogram.
- Simple or compound drawdowns.
- Portfolio returns histogram + density.
- 2/3D risk measure scatter plots.

---

<div class="post-metadata">

**Author:** ![colintbowers](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/colintbowers/32/8033_2.png) [@colintbowers](https://discourse.julialang.org/u/colintbowers)\
**Post date:** [October 13, 2025, 11:50pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/2 "2025-10-13T23:50:28Z")

</div>

I’ve done a fair bit of work in this area and can testify that:

> attempt at providing as many as possible

you weren’t exaggerating! That is a very comprehensive list of techniques. This is a great resource, I look forward to digging into it. Thank you!

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [October 14, 2025, 9:39am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/3 "2025-10-14T09:39:03Z")

</div>

Glad you like it.

I’m very open to suggestions and contributions.

I’d also like to add stuff like synthetic returns, but i gotta wait for Copulas.jl to add vines. Adding cross validation would also be nice.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [January 29, 2026, 1:53pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/4 "2026-01-29T13:53:02Z")

</div>

# PortfolioOptimisers.jl v0.14.2 Release

## Breaking changes

- Lots of quality of life breaking releases since the inaugural post.
  - Better names for abstract and concrete types.
  - Shorter and unambiguous fieldnames.

## Bug fixes

- Lots of minor and quality of life bug fixes. Most don’t affect any results aside from disallowing certain types/values that would error further down the chain rather than at instantiation.
- One pretty big bug one relating to high order prior statistics using low order factor prior statistics.

## New features

- New risk measures and formulations.
- Enable existing measures on more optimisation types.

## Docs

- Lots of new API docs, including instructions on defining interfaces for new estimators and algorithms.
- A few new examples.
- Better readmes.

## Project maturity

- Stabler API, still expect breaking changes with `v0.X` releases.

## Roadmap

I don’t have a timeline or a serious plan, but there are a few features I’d like the package to have. In order of desirability:

1. Cross validation prediction.
2. Pipelines, i.e. data validation → optimisation → prediction → post-processing
3. Report generation.  
3a. Better plots.  
3b. Perhaps excel output.
4. A non-shitpost logo (though tbh I kinda like that it’s a shitpost).

## Feedback and contributions welcome

The package is quite functional, but it is missing a lot of docs and sample use. If anyone out there is using/wants to use the package I’d love to hear about how it can be improved and would love some contributions/help with implementation and design.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [March 9, 2026, 12:30am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/5 "2026-03-09T00:30:18Z")

</div>

# PortfolioOptimisers.jl v0.17.0

## Breaking changes

- Renamed a few internal fields to make it clear they are incompatible with more advanced features like cross validation and the outer optimisation on nested clusters and stacking optimisers. This is because they need to generate returns data dynamically according to the cross validation folds/inner optimisation results.

## New features

[Cross validation](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/10_CrossValidation) and [hyper parameter tuning](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/11_HyperparameterTuning) come with a simple example each.

- Cross validation.
  - KFold: basic splitting into training and testing sets.
  - Walk Forward: enables dynamic optimisation with periodic rebalancing.
    - Index based.
    - Date based: It’s now possible to rebalance on weird dates like the 2nd tuesday of every 3 weeks or something equally arbitrary.

  - Combinatorial: combinatorially generated paths for much more robustness.
  - Multiple Randomised: temporal and cross sectional (sampling the asset universe) walk forward path randomisation for much more robustness but less generality.

- Ability to define which constraints which use benchmark weights can carry forward previous optimisation weights in walk forward and multiple randomised cross validation.
- Time-dependent constraints to be used in walk forward and multiple randomised cross validation. None have been implemented as I didn’t have the creativity, but the interface exists and is implemented.
- NestedClusters and Stacking optimisers can now use cross validation to predict the returns to be used in their outer estimators.
- Hyper parameter tuning: it’s now possible to evaluate out-of-sample values for all the different non-finite allocation optimisers which eliminates much of the guesswork.
  - Grid search cross validation.
  - Randomised search cross validation.

- Prediction machinery. These came with the cross validation and are the predicted performance of an optimisation, they are the out-of-sample performance of an optimisation.

## Plans

I’ve been burning the candle at both ends so I’ll ease off from my anxiety-induced freneticism to make the package “feature complete”. I classify the remaining features as nice-to-haves. For now the top priority would be the API docs and improved examples.

### Features

- Pipelines, most of the hard work is done. And this isn’t a game-changing feature, more of a nice to have. I’ll implement them eventually.
- Synthetic returns prior. I need a package to implement vine copulas.
- Expand the hyperparameter tuning to do adaptive refinement of parameters.
- Adding more quality of life features like summaries, reports, post-processing bells and whistles (intersects heavily with plots).

### Documentation

- API: I just discovered DocStringExtensions.jl, but I’m having trouble implementing my own templates (if anyone has an example please help a brother out and point me to it). I’d have to redo a bunch of the docs I already have, but I think it’s worth it. Especially for a package like this one.
- Examples: more examples with more plots and fewer tables would be nice. This means I have to implement the plots though.
- Formalisms and citations. I really need to keep adding these, there are just so many features that it’s overwhelming at times, but I’ll be passively adding these as patch releases.

### Plotting

- More plots in general with nicer APIs, especially now that cross validation and prediction features have been added to the package, it would be nice to have plots that show how the predictions behave as a function of time.

## Future breaking changes

- I never expect more breaking changes, but there always are. However, they should slow down now. I’m liking how the APIs for the important parts have been turning out. However, there’s always a better name for a property or a type and I never hesitate to make the change if I think it’ll make interfacing with the package more predictable and logical.

## Feedback and contributions always welcome

As always, ideas, contributions and criticisms are always welcome.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [April 14, 2026, 4:25pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/6 "2026-04-14T16:25:05Z")

</div>

# PortfolioOptimisers.jl v0.19.0

## Highlights

- [Quick start guide](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/user_guide/01_Basic_Optimisation#1.-Computing-returns).
- The entire [API](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/api/00_API#Features) has been documented by clankers.

## New features

### Breaking

- Added benchmark prices. They can be specified per asset (as a matrix), or as an index (as a vector). This enables optimising on excess returns which is a form of tracking. It’s compatible with every optimiser, cross validation, and prediction. Breaking because I had to add a flag to all optimisation estimation structs even though it didn’t break existing code or APIs.
- Improved the consistency of various moment estimators which internally compute the variance. They also used to contain expected returns estimators, but we now use the ones inside the variance estimator because the expected returns estimator was computing the mean used to compute the variance.
- `expend_train -> expand_train` keyword in `WalkForwardEstimator` subtypes to make it consistent with `reduce_test`.

### Non-breaking

- `SubsetResampling` optimiser. This takes random, unique, samples of the asset universe and optimises each one in turn. The final weights are the (optionally weighted) average weights of each asset in each optimisation. If an asset wasn’t sampled for a given optimisation, its weight is taken to be zero. It interpolates between the equal weighted portfolio and the pure internal optimiser. It’s quite cool, i like this optimiser a lot because it adds noise that reduces overfitting for mid assets, but buries shit ones and lets great ones shine through. It’s like a targeted yet stochastic combination of the `L_n` penalty families.
- `LogRiskBudgeting` what used to be the normal risk budgeting algorithm can now accept an orthant vector, which means risk budgeting portfolios can now take on negative weights. But they have to be specified _a priori_. Limited to asset risk budgeting, factor risk budgeting already allows for negative asset weights. It might be possible to generalise but i haven’t thought it through.
- `MixedIntegerRiskBudgeting` explores all orthants to get a low risk portfolion that meets the risk budget threshold. It’s very expensive because there are `2^N` sign combinations, where `N` is the number of assets. You don’t have to specify the orthant vector, it is obtained on the fly. It also doesn’t guarantee the lowest risk portfolio simply because the MIP optimiser might give up depending on your settings. Limited to asset risk budgeting, factor risk budgeting already allows for negative asset weights. It might be possible to generalise but i haven’t thought it through.
- Windowed variants of all moment estimators. They simply wrap one of the already provided estimators. You can provide a `window` which can be an integer to take the last `window` number of entries, or a vector to index those specific indices. The moments will be computed based on that window rather than the entire history. They also support weighted estimators by currying the outer weights inward with the right indexing.

## Docs

- Added a quick start guide.
- I’ve added more docstring examples and defined some of the necessary interfaces one would need to implement to add custom behaviour.
- I caved and unleashed the clankers on the API docs. I only asked for API documentation of function signatures, variable, type and function explanations. Almost 18k lines later here we are. It seems to have done a fairly good job, but one does not simply read 18k lines and find all issues at once. But at least, the APIs are now documented so people can actually use the entire library without looking at the code. I will methodically go over the new docs to improve their quality.

## Bug fixes

- Fixed a bug in risk budgeting optimisers where constraints were being correctly built from the wrong variable, which meant unnecessary coupling between systems that also needlessly made certain constraint combinations impossible to define.

## Future plans

- Improve the documentation.
  - The clanker-made docs are competent, but not to the standard I’d like.
  - They’re missing interface definitions, sources, language standardisation, nicer mathematical notation, not all methods are documented (mainly internal ones).
  - Split the API docs into public and private.
  - More examples and entries to the user guide.

- Plots, this is a sore point for me.
  - I have had trouble with Makie.jl due to its less than stellar support for dendrograms.
  - Plots has the problem that I have to load GraphRecipes.jl and StatsPlots.jl to load the extension, which is less than ideal.
  - I’d like to have interactive plots with mouse-over labels but that would lock me into Plotly
  - The API of my plotting functions sucks and I haven’t figured out a way to make it better. Recipes seem fragile but I’ll maybe ask a clanker to figure it out for me.
  - Need plots for prediction results, not just optimisation results.

- Cross sectional factor prior.
- Copula based synthetic returns. Needs vine copulas, I’ve not found any Julia implementation.
- Remove python dependencies. Needs bootstrapping and bin width computation functions, but I’ve found no suitable Julia library that does either of these.
- Pipelines.
- Online statistical methods maybe? Though I’m not sure how to make most of the current ones can be updated. I’m inclined to let it ride as it is, it may not be worth it. Unless someone else wants to give it a stab and open a PR.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [April 15, 2026, 12:38am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/7 "2026-04-15T00:38:35Z")

</div>

You can now do ridiculous things like this. I don’t see why anyone would, but you can. Seriously, don’t try this, it’ll probably run for hours. This is barely scratching the surface of all the features in the library.

```julia
using PortfolioOptimisers, StatsPlots, GraphRecipes, Clarabel, HiGHS, Pajarito, JuMP,
      YFinance, TimeSeries, DataFrames

# Use MIP solvers because we will use buy in threshold constraints.
Highs_resetGlobalScheduler(1)
slv = [Solver(; name = :mip1,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false)),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :mip2,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false,
                                                                                             "max_step_fraction" => 0.95)),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :mip3,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false,
                                                                                             "max_step_fraction" => 0.90)),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :mip4,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false,
                                                                                             "max_step_fraction" => 0.85)),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :mip5,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false,
                                                                                             "max_step_fraction" => 0.80)),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :mip6,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false,
                                                                                             "max_step_fraction" => 0.75)),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :mip7,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false,
                                                                                             "max_step_fraction" => 0.7)),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :mip8,
              solver = optimizer_with_attributes(Pajarito.Optimizer,
                                                 JuMP.MOI.Silent() => false,
                                                 "oa_solver" => optimizer_with_attributes(HiGHS.Optimizer,
                                                                                          JuMP.MOI.Silent() => false,
                                                                                          "threads" => 6),
                                                 "conic_solver" => optimizer_with_attributes(Clarabel.Optimizer,
                                                                                             JuMP.MOI.Silent() => false,
                                                                                             "max_step_fraction" => 0.6,
                                                                                             "max_iter" => 1500,
                                                                                             "tol_gap_abs" => 1e-4,
                                                                                             "tol_gap_rel" => 1e-4,
                                                                                             "tol_ktratio" => 1e-3,
                                                                                             "tol_feas" => 1e-4,
                                                                                             "tol_infeas_abs" => 1e-4,
                                                                                             "tol_infeas_rel" => 1e-4,
                                                                                             "reduced_tol_gap_abs" => 1e-4,
                                                                                             "reduced_tol_gap_rel" => 1e-4,
                                                                                             "reduced_tol_ktratio" => 1e-3,
                                                                                             "reduced_tol_feas" => 1e-4,
                                                                                             "reduced_tol_infeas_abs" => 1e-4,
                                                                                             "reduced_tol_infeas_rel" => 1e-4)),
              check_sol = (; allow_local = true, allow_almost = true))];

# Convert YFinance data to TimeArray.
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

# We'll use PCA regression for factor modelling.
assets = sort!(["JCI", "TGT", "CMCSA", "CPB", "MO", "APA", "MRSH", "JPM", "ZION", "PSA",
                "BAX", "BMY", "LUV", "PCAR", "TXT", "TMO", "DE", "MSFT", "HPQ", "SEE", "VZ",
                "CNP", "NI", "T", "BA"]);
factors = sort!(["MTUM", "QUAL", "VLUE", "SIZE", "USMV"]);

Date_0 = "2024-01-01";
Date_1 = "2026-10-05";

# Download the price data using YFinance.
prices = get_prices.([assets; factors]; 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; factors]));

# Compute returns.
rd = prices_to_returns(prices[Symbol.(assets)], prices[Symbol.(factors)]);

# Asset sets
sets = AssetSets(; dict = Dict("nx" => rd.nx));

# Covariance estimator.
ce = PortfolioOptimisersCovariance(; ce = SmythBrobyCovariance());

# Expected returns estimator.
me = ShrunkExpectedReturns(; ce = ce);

# Clusters estimator.
cle = ClustersEstimator(; ce = ce);

# Cross validation.
n_folds, n_test_folds = optimal_number_folds(length(rd.ts), 60, 10);
cv = OptimisationCrossValidation(;
                                 cv = CombinatorialCrossValidation(; n_folds = n_folds,
                                                                   n_test_folds = n_test_folds,
                                                                   purged_size = 3,
                                                                   embargo_size = 7));

# When used in optimisations, vectors of risk measures 
# get scalarised via one of three methods (default is weighted sum).
# Expensive JuMP compatible risk measures. 
jr0 = [NegativeSkewness(; settings = RiskMeasureSettings(; scale = 20)),
       RelativisticDrawdownatRisk()];
jr1 = [Kurtosis(; settings = RiskMeasureSettings(; scale = 30)), RelativisticValueatRisk()];

# Clustering risk measures.
hr0 = [MedianAbsoluteDeviation(; settings = HierarchicalRiskMeasureSettings(; scale = 15)),
       RelativeEntropicDrawdownatRisk()];

# Prior statistics.
pe = HighOrderFactorPriorEstimator(;
                                   pe = FactorPrior(;
                                                    pe = EmpiricalPrior(; me = me, ce = ce),
                                                    re = DimensionReductionRegression()));
# Weight bounds
wb = WeightBounds(; lb = -1, ub = 1);

# Buy in threshold (MIP constraint)
st = Threshold(; val = 0.1);
lt = ThresholdEstimator(; val = UniformValues());

# Base optimisers.
jopt = JuMPOptimiser(; pe = pe, slv = slv, wb = wb, sbgt = 1, lt = lt, st = st, sets = sets);
hopt = HierarchicalOptimiser(; pe = pe, cle = cle, cle_pr = false, slv = slv);

# JuMP optimisers.
jopt0 = NearOptimalCentering(; r = jr0, obj = MaximumRatio(; rf = 4.2 / 252 / 100),
                             opt = jopt);
jopt1 = MeanRisk(; r = jr1, obj = MaximumRatio(; rf = 4.2 / 252 / 100), opt = jopt);

# Hierarchical optimisers.
hopt0 = HierarchicalEqualRiskContribution(; ri = hr0, ro = jr0, opt = hopt);

# Clustering optimiser.
nopt = NestedClustered(; pe = pe, wb = wb, cle = cle, cle_pr = false, opti = jopt0,
                       opto = jopt0, cv = cv);

# Stacking optimiser.
sopt = Stacking(; pe = pe, wb = wb, opto = jopt0, opti = [jopt0, jopt1, hopt0, nopt],
                cv = cv);

# Subset resampling optimiser.
rsopt = SubsetResampling(; pe = pe, wb = wb, opt = sopt, subset_size = 0.9, n_subsets = 10);

# Optimise
res = optimise(rsopt, rd)

```

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [July 6, 2026, 11:50pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/8 "2026-07-06T23:50:13Z")

</div>

# PortfolioOptimisers.jl v0.24.0

`PortfolioOptimisers.jl` builds portfolios through a `data → moments → prior → optimisation → post-processing` pipeline. This release is a maturity pass on that pipeline: the Python dependency is gone, failure diagnostics from nested optimisers are now structured instead of stringly-typed, the equation parser that reads untrusted constraint/view strings got a real security review, and a handful of long-standing naming inconsistencies were cleaned up (breaking, but with a one-line fix each).

## Highlights

- **Examples and user guide** : There is now a comprehensive set of deep [examples](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/00_Examples) and a shallow [user guide](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/user_guide/00_Introduction) that showcase the library’s features.

- **Better compatibility documentation** : There is now a table of solver requirements and suggestions for the various risk measures in the [base risk measures API page](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/api/19_RiskMeasures/01_Base_RiskMeasures). There are plans to do the same with optimisation constraints.

- **Aliases** : There is now a large set of convenience aliases which are a suprising quality of life improvement. There is a dedicated [page](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/api/25_Aliases) for them.

- **No more Python** : Histogram bin selection and block-bootstrap uncertainty sets used to shell out to `astropy` and `arch` via `PythonCall`/`CondaPkg`. Both are now native Julia, one less interpreter, two fewer PyPI/conda packages, and no more `CondaPkg.toml` in the repo.

- **Structured failure diagnostics** : Nested optimisers (`NearOptimalCentering`, `Stacking`/meta-optimisation, `SubsetResampling`) used to collapse every sub-solve failure into one opaque string. They now propagate the individual `OptimisationFailure` objects from each failed sub-problem, so you can actually see which stage failed and why.

- **Risk-measure/optimiser compatibility is now a trait, not prose** : `supports_risk_measure(::Type{<:Opt}, ::Type{<:RM})` and `supported_risk_measures(::Type{<:Opt})` are real, dispatchable functions, and the user-guide compatibility table is now generated from them, so the docs and the dispatch logic can’t drift apart.

- **Type hierarchy tree** : an automatically generated type hierarchy [tree](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/api/25_TypeHierarchy) with links to the API docs.

- **The equation parser got a real security pass** : Constraint equations, Black-Litterman views, and entropy-pooling views are untrusted strings by design (they come from config files, spreadsheets, UIs). The parser’s callable-function resolution was rewritten from “gate a name, then resolve it against all of `Base`” to an explicit enumerated allowlist, and string length / AST depth are now capped to close a stack-exhaustion denial-of-service path.

- **Global configuration via Preferences.jl** : Pretty-printing collapse, fuzzy “did you mean?” suggestion distance, and equation-parser resource caps are now `ScopedConfig` holders: thread-safe, atomically-swapped global defaults with task-scoped `with\_\*` overrides, seedable per-project via `Preferences.jl`.

- **`LxNorm` generalizes beyond tracking error** : The `L1/L2/SquaredL2/Lp/LInf` norm family moved out of the tracking-error file into `01_Base.jl` as a shared primitive, which is what let entropy pooling reconcile multiple CVaR views using the same norms instead of a hard-coded, uncustomisable `LinearAlgebra.norm`.

- **A dozen Gerber/Smyth-Broby “marker” types collapsed into one kernel** : `SmythBrobyKernel` replaces `Gerber0/1/2`, `SmythBroby0/1/2`, `SmythBrobyGerber0/1/2`, `SmythBrobyCount0/1/2`, same two orthogonal axes (normalisation variant × scoring family), one accumulation loop instead of ~980 lines of near-duplicated code.

- Matrix post processing pipeline simplified and made much more extensible.

- **Gerber Information Quality** : new `GerberIQ` statistic for correlation and covariance estimation. It uses a template based system to compress co-movement information with a low signal-to-noise ratio.

## Breaking Changes & Migration Guide

### Matrix Processing simplified

- `DenoiseDetoneAlgMatrixProcessing` → `MatrixProcessing` and the associated permutation types removed in favour of a tuple which defines what and in which order the processing steps are applied.

### `FiniteAllocationInput` replaces positional allocator arguments

`optimise(allocator, w, prices, cash, T, fees)` is now `optimise(allocator, FiniteAllocationInput(; w, prices, cash))`. The shared problem data for both finite-allocation optimisers (`DiscreteAllocation`, `GreedyAllocation`) is bundled into one struct instead of loose positionals.

```julia
# before
greedy = optimise(GreedyAllocation(), res.w, prices, cash)

# after
greedy = optimise(GreedyAllocation(), FiniteAllocationInput(res.w, prices, cash))

# alias
greedy = optimise(GreedyAllocation(), FAI(res.w, prices, cash))

```

### `LxTracking` family renamed to `LxNorm`

`L1Tracking`, `L2Tracking`, `SquaredL2Tracking`, `LpTracking`, `LInfTracking` → `L1Norm`, `L2Norm`, `SquaredL2Norm`, `LpNorm`, `LInfNorm`. Straight rename, same semantics, just find/replace.

```julia
# before
alg = L1Tracking()

# after
alg = L1Norm()

```

### Disambiguation renames

- `Full` → `FullMoment`, `Semi` → `SemiMoment` (the lower/higher partial moment algorithm selector).

- `Forward` → `ForwardSelection`, `Backward` → `BackwardElimination` (stepwise regression algorithms).

- `RiskRatioRiskMeasure` → `RiskRatio`.

- `NonOptimisationRiskRatioRiskMeasure` → `NonOptimisationRiskRatio`.

```julia
# before
r = LowOrderMoment(; alg = SecondMoment(; alg1 = Semi(), alg2 = SOCRiskExpr()))

# after
r = LowOrderMoment(; alg = SecondMoment(; alg1 = SemiMoment(), alg2 = SOCRiskExpr()))

```

The rule going forward (see ADR 0015): a type is named bare unless another, non-sibling construct already owns that word, `RiskMeasure`/`Estimator`/`Result` suffixes exist only to disambiguate, never as a blanket category marker.

### `CombinatorialCrossValidation`

- `warn_comb` → `max_comb`, and it now throws

Previously, exceeding `warn_comb` (default 100,000 combinations) logged a warning and proceeded anyway. It’s now `max_comb`, and exceeding it raises an `ArgumentError` instead.

### Removed defaults that caused friction

A few default keyword values were removed in favor of requiring an explicit choice at the call site, if you were relying on an implicit default in `10_JuMPModelOptimisation.jl` or the moment/covariance estimators touched by the ergonomics pass, you’ll need to pass the value explicitly now.

### Python dependency removed

If you were constructing `AstroPyBins` directly, or depended on the `arch`-package-backed block bootstrap in `04_BootstrapUncertaintySets.jl`, those code paths are now native-Julia reimplementations under the same public API, no `PythonCall`/`CondaPkg` install required, but check numerical output if you had pinned exact Python-library behavior.

## Bug Fixes

- **HERC’s `LogSumExpScalariser` path was wrong** : The per-optimiser copy of the log-sum-exp reduction in `HierarchicalEqualRiskContribution` applied the reduction over the wrong risk slice, producing incorrect scalarised weights. Fixed as part of consolidating all scalariser logic into one `scalarise` seam; the reference test fixture changed accordingly.

- **`SubsetResampling` risk-measure support fixed** : correctly reports/enforces which risk measures it supports, and raises `ArgumentError` (rather than a deeper `MethodError`) for unsupported ones.

- **Uncertainty-set variance handling made consistent** : its risk computation matches its optimisation formulation, making it consistent with hierarchical optimisers and `NearOptimalCentering`.

- **Recursion stack overflow fixed** : a max-recursion and max-string-length cap added to `parse_length` to prevent accidental stack overflows.

- **Nondeterministic logging failures fixed** : log output no longer varies run-to-run in ways that broke tests or made warnings unreliable.

- **One `scalarise` seam** : `HierarchicalRiskParity`, `HierarchicalEqualRiskContribution`, and `NearOptimalCentering` each carried their own copy of vector-of-risk-measures-to-scalar reduction logic (~20 near-identical methods across scalariser type × shape). Consolidating them also surfaced and fixed a real bug.

## Usability Improvements

- **Better, bounded failure messages** : `JuMPResult` now builds its failure `@warn` via a shared `failed_solve_msg` builder: one bounded line per failed solver stage (name, stage, first line of the error) instead of dumping the entire trials dict, solver settings, and full exception payloads into the log. The full diagnostics are still available on `.trials` if you need them.

- **Fuzzy “did you mean?” suggestions** for unrecognised variable names in constraint/view strings, tunable via `set_string_distance!`/`with_string_distance` (distance metric and minimum similarity score).

- **Scoped configuration overrides** : `with_compact_show`, `with_string_distance`, and `with_equation_limits` let you override a global setting for the dynamic extent of a single call, e.g. silencing “did you mean?” suggestions inside a meta-optimiser’s inner loop (where an asset legitimately absent from a subset isn’t a typo) without touching the global default or affecting concurrent tasks.

- **Per-project defaults via `Preferences.jl`** : The above configuration knobs can now be seeded once per project instead of called at every session start.

- **Improved error messages** : in the phylogeny, prior, and uncertainty-set base modules.

- **A generated, always-current risk-measure/optimiser compatibility table** in the user guide (see Highlights).

- **Lenient constraint parsing kept, with better diagnostics** : rather than making a misspelled asset name in a constraint string fail closed by default (which would break the legitimate meta-optimiser pattern of writing one constraint over the full universe and letting it apply partially to subsets), the lenient default stayed, and the warning it produces was made clearer instead.

## New Features

- **`SmythBrobyKernel`** : unifies the Gerber/Smyth-Broby co-movement covariance family behind one data-carrying algorithm struct, replacing ~12 separate marker types.

- **`GerberIQ`** : provides a new co-movement based covariance and correlation estimator.

- **`AbstractMIPSpace`** : unifies asset-space and sub-group (SMIP) mixed-integer constraint construction over a single dispatchable abstraction.

- **`NormError` / `LxNorm`** : promoted to a shared `01_Base.jl` primitive, and entropy pooling can now reconcile **multiple** CVaR entropy pooling views using any of the `LxNorm` family (previously hard-coded to a single, uncustomisable norm).

- Covariance entropy pooling views.

- **`ProcessedAttributes` / `ProcessedRiskBudgetingAttributes`** abstract supertypes added to give the processed-attribute struct hierarchy (JuMP optimiser and risk-budgeting variants) an actual shared type instead of everything sitting directly under `AbstractResult`.

- **New/expanded examples** : including `2_moments_priors/10_Windowed`, `7_putting/04_Factor_Views`, and general rebuilds across the example set to track the renames above.

- **27 ADRs** now document the architecture decisions behind this release, from the pipeline’s core Estimator/Algorithm/Result/Factory/View vocabulary to the security-hardening rationale, for anyone extending the library.

---

<div class="post-metadata">

**Author:** ![loisel](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/loisel/32/50626_2.png) [@loisel](https://discourse.julialang.org/u/loisel)\
**Post date:** [July 7, 2026, 3:39am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/9 "2026-07-07T03:39:42Z")

</div>

The documentation badges in your README.md on the front page of your github, lead to 404 errors. I’m getting the links below:

[https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/)  
[https://dcelisgarza.github.io/PortfolioOptimisers.jl/dev/](https://dcelisgarza.github.io/PortfolioOptimisers.jl/dev/)

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [July 7, 2026, 6:57am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/10 "2026-07-07T06:57:39Z")

</div>

Landing pages work again. Thanks for the report @loisel .

Turns out I had renamed the root pages and documenter didn’t like that. It’s fixed now. The docs had deployed and other links worked, as well as the navigation to the landing page. But the web address of the landing page was now dofferent. I’ve fixed it and added some code to make sure that doesn’t happen again.

---

<div class="post-metadata">

**Author:** ![sob](https://avatars.discourse-cdn.com/v4/letter/s/65b543/32.png) [@sob](https://discourse.julialang.org/u/sob)\
**Post date:** [July 9, 2026, 6:10pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/11 "2026-07-09T18:10:12Z")

</div>

I’m an investment professional, so your package picked my curiosity. If I may ask, do you use it personally, and it what context? Who is the (future) intended audience?

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [July 9, 2026, 6:47pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/12 "2026-07-09T18:47:20Z")

</div>

I have personally used a previous version. It’s archived and was never registered because it had some ugly spaghetti code that made it increasingly difficult to develop. So I rewrote it from scratch to make it more maintainable, testable, and robust.

I had 3 long-only portfolios (myself, my mum and my partner) over 2-3 years starting in 2022. They all beat the market by quite a margin, especially in 2022 where they made money when the S&P lost about 20%. We had to take out the money out earlier this year for a house deposit.

My goal is to make it useful for anyone regardless of expertise level. I’ve been mostly focusing on adding features, and only now that it’s very feature rich am i adding convenience functionality (mainly aliases).

I’m about to release a new version which adds pipelines and the ability to perform cross validations and hyper-parameter tuning on them. That’s to say everything from asset selection to priors, to clustering, to optimisations.

I’ll then move on to working on enabling time-dependent constraints. So it’s possible to provide either a vector or a function which will vary the huge number of constraints depending on the walk-forward fold being computed.

I also want to have vine-copula based synthetic returns generation for a synthetic data prior. And cross-sectional factor priors.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [July 11, 2026, 9:21am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/13 "2026-07-11T09:21:25Z")

</div>

# PortfolioOptimisers.jl v0.25.0

Thanks to the clanker currently known as Fable, I finally had the energy and rote work capacity to rather quickly implement, test, and document three long-standing, major pieces of functionality I’ve wanted to have for a long time.

## Asset pre-selection

I have to be honest, this was in the backlog because it’s not complicated or exciting. I had some scaffolding in place (bare functions) but with a bit of prompting there is now a nice, general set of asset pre-selectors.

They make use of existing machinery to filter out assets without going through costly optimisations. More will come in time, but the necessary plumbing and structural code is there for quickly implementing new ones. Here’s a short [example](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/1_foundations/03_Asset_Preselection).

## Pipelines

This is a long time coming, it lets you do every process from a simple `fit` call. It also enables cross validation and hyperparameter tuning on the entire process explicitly rather than through optimisations. It also comes with an accompanying [example](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/5_validation_tuning/03_Pipelines).

## Time dependent constraints

It’s now possible to provide time-dependent constraints. These let users apply different constraints to different cross-validation folds. They come in two different flavours:

- Static: These are predefined _a-priori_ and applied per fold.
- Dynamic: These are computed on the fly using all the information available at a given fold. They can be provided as functions or as callable structs.

Both support providing vectors of constraints, so at each fold you can apply multiples of the same type of constraints. Here’s its [example](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/5_validation_tuning/04_Time_Dependent_Constraints).

This machinery unlocks other potential capabilities such as varying the objective function and even risk measure per fold, different inner optimisers per fold, etc. But I have to think about it, probably with the help of the clanker who will be formerly known as Fable in the not too distant future.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [July 15, 2026, 3:51pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/14 "2026-07-15T15:51:02Z")

</div>

# PortfolioOptimisers.jl v0.26.0

_Many people are saying it, loooots of people saying it, this is beeg, yuge, even. Very good looking, real capable piece of software, no bugs, zero bugs, purrrfect. If it weren’t my library I’d smooch it, grab it by the private API and not even quaahleefy the accesses. Big beautiful PR, some would say the biggest, wouldn’t you say, that’s right, I like you you know that. No I didn’t vibe code this, this all in my brain, beeg very smooth, really smooth brain, you’d kiss it if you saw it, go ahead, yeaaahh it’s alright._

Anyway with that nice reflection written by the orange goblin living rent-free in my head, I bring the very fruitful culmination of what I’m starting to discover is a very effective clanker-human collaboration. It’s like having an incredible copy editor who remembers to dot and cross all the T’s and I’s you forgot and _really_ don’t want to meticulously go round and do. And it then takes your sorry ass tests and expands them so they cover all the bases, and documents your APIs because it’s now got enough in its memory to know what things are and what the standards are.

This brings four new features, but one is pretty big. Lets start with the small one.

## Small features

- I added some new weight norm constraints that bound the number of effective assets according to some given norm.

- Expanded weight bounds to be able to be automatically generated by providing an `AbstractEstimatorValueAlgorithm`. I’ll see if the same mechanism can be applied to other constraints.

- `train_test_split` is finally a thing, comes with its own [example](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/5_validation_tuning/05_Train_Test_Split) (might still be building).

### Breaking

- `nea` → `w2n`: just a rename to bring it in line with the rest of its family of constraints.
- `l2` which adds/subtracts an L2 norm penalty to the objective used to take a number but now takes an `L2Regularisation` or a vector of them because I decided to offer different formulations and the code already existed.

### Non-breaking

- `wnp`: bounds the number of effective assets but instead of using an L2-norm uses an Lp-norm or vector of them.
- `wninf`: does the same but uses the infinity norm, so the effective number of assets is bound by the reciprocal of the largest absolute weight.

## Major pipeline improvements

### Optimisers and all their fields can be time-dependent

I don’t think there’s another library out there that can do this. There’s [skfolio](https://github.com/skfolio/skfolio) with some extra prior statistics computations. But that’s mainly Julia’s lack of regression support like scikit learn, or the up until recently lack of vine copulas in the ecosystem. There’s also Riskfolio which is the goat for how many risk measures that man puts out, but it’s much more static than either [skfolio](https://github.com/skfolio/skfolio) or PortfolioOptimisers.jl. But I think this might be the only one capable of doing something so powerful.

It’s now possible to tag any field in any optimiser, or indeed full optimisers as `TimeDependent` such that they can be swapped in a cross-validation fold. This unlocks some real potential, especially when combined with the callable struct formulation or a function formulation (essentially the same but a callable struct can keep state in a non-global). This essentially enables call-backs for every single constraint in any cross validation fold, including parameter sweeps.

There’s an [example](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/examples/5_validation_tuning/06_Time_Dependent_Optimisers) (might still be building) which shows how you can detect a regime switch and change to a more robust optimiser on the fly for that fold.

But that’s not all, it’s possible to do this with full optimisers as well. It unlocks being able to tune hyperparameters or train an entire model under different regimes.

The result of a pipeline is a prediction, which is a returns vector (the portfolio returns) we’ll call `R`. Predictions don’t have a vector of portfolio weights because they are made of various different portfolios at different times (folds), and those returns are concatenated into `R`. But if we take the asset returns series for the training period `X`, we can compute the would-be portfolio weights by inverting the usual relationship:

`R = X * w -> w = pinv(X) * R`

I think I’ll add this to the relevant structs, or perhaps have it as a separate function. I need to think about how to best do it because it may differ between prediction structures. But it will definitely be added once i have a better understanding, i’ll ask my clanker mate to help me prototype and work through it together. This would make it so predictions from pipelines and validators can actually serve as a kind of meta-meta optimiser.

### Pipelines starting from prices

I went back and forth on whether I wanted this, but in the end my the vibes coming from my _ **fuck it, we ball** _ gremlin beat those from my _ **it’s so over** _ one with “facts and logic” (just do it bro). So it’s now also possible to declare a pipeline that will take prices as a starting point instead of returns. They may not behave as well in cross-validation, as each fold would recompute its returns at the start of the fold, but that may also useful in some contexts. As I said, I was in two minds about it, but I’ll allow it. It offers more flexibility.

## Final remarks

I think the library’s now very usable. The examples and user guide are quite well-fleshed out. Anything more is going to be extra credit. Honestly Claude and Kaimon.jl have sped things up substantially, it’s much more fun when you don’t have to write the entire test suite yourself and hunt down missing methods/hidden typos or plugging in all the piping only to realise you missed one bit, or the type parameters you defined are too narrow/broad. It has greatly helped catch all of those small mistakes and it’s been much easier and painless to prototype and tweak things. 10/10 would recommend.

---

<div class="post-metadata">

**Author:** ![Snowy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/snowy/32/36765_2.png) [@Snowy](https://discourse.julialang.org/u/Snowy)\
**Post date:** [July 16, 2026, 4:22pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/15 "2026-07-16T16:22:24Z")

</div>

Haven’t used the package yet, but just wanted to comment and remark how nicely documented everything is!

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [July 16, 2026, 5:00pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/16 "2026-07-16T17:00:46Z")

</div>

I’m gonna take credit for that because I was writing the API docs by hand and I was crashing out over how many there were and how slowly that was going. I think i’d written about 200, and there were at least another ~400 to go (symbols that is, so that’s an undercount). So I wrote some standards and got clankers to write the API docs.

They still have a long way to go, they’re missing references, some get the meaning of certain variables wrong, some of the maths could be better typeset. But damnit it, it helped me so much and freed me up to do other things.

Also any and all feedback is welcome @Snowy , I’m adding convenience aliases and it would be good to know what I should prioritise. Also any suggestions on making the docs easier to use is greatly appreciated.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [September 15, 2026, 9:46am UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/17 "2026-09-15T09:46:04Z")

</div>

# PortfolioOptimisers.jl v0.31.0

Alright, it’s been some time since v0.26.0. These updates have brought gigantic changes. What had started as a trepidatious wade into AI assisted development, has turned into a fully-fledged clanker assisted/enabled workflow.

Now before you think it’s all vibecoded, it’s not. I’ve made a post about my careful approach to using clankers [here](https://discourse.julialang.org/t/idiomatic-julia-code-in-ai-generated-code/139183/4). I must say, it’s been a huge net positive for the library and my free time. They have enabled the addition of features that aren’t complete game-changers but nice to haves that are annoying to try to implement. They’ve also straight up supercharged the development of other features I’ve been wanting to add for a while. Here’s a little list of non-feature things where the clankers have been straight up buffs to the library’s general maintenance.

1. Standardise and greatly improve documentation—including missing references, algorithm explanations, missing mathematical descriptions. There are some rough edges, for example, the clankers added some very specific numeric examples that they worked through while checking the code’s correctness and behaviour.
2. I’ve set up a process which sweeps through the code to ensure compliance with standards, code correctness, improve type stability, reduce complexity, and keep file size in check whilst keeping a logical framework.
3. It’s allowed me to flesh out examples. At the time of writing (while the latest PR is under review) there are 60 examples and 9 user guide entries.
4. It’s found and fixed a variety of bugs. To my great delight, not that many correctness ones. Though there was a pretty egregious one in denoising, which didn’t affect most cases and was thus missed by the tests. Most of the fixes have had to do with how the library composes itself.
5. Many new features.
6. Hugely increased coverage, not of code (though that’s improved too), but of scenarios (see point 3).

## New feature summary

- Quantile portfolios. Uses especially calibrated L1-norms to apply an uncertainty set on the expected returns (or any characteristic vector) to produce equal weighted (assuming a plain return maximisation with no other constraints) portfolios for a specific number or percentage of securities based on how performant each one has been. It’s got two versions:
  - An unsigned version, i.e. the best performing `x %` (relative), or the best performing `n` (absolute).
  - A signed version, i.e. the best and worst `x %` (relative), or the best and worst performing `n` (absolute).

- New budget constraints, and the ability to turn the old ones from slack constraints into exact ones.
- Ability to provide more than one expected return. This enables using linear combinations of characteristics vectors which work with all the existing expected returns machinery. A fun use case is to use ESG scores and use the quantile portfolio formulation to pick the best ones based on that. Another interesting usecase could be a centrality score which could be used to generate portfolios whose assets sit on the periphery or the nucleus of the universe. It could also be any other vector or combination of them.
- Phylogeny constraints gained a continuous analogue based on the distances in a weighted adjacency graph rather than by degrees of separation.
- Lots of fields gained the ability to be calibrated on the fly. Inspired by the quintile portfolio, i thought it would be a good idea to provide a way to automatically calibrate various parameters based on the the returns distribution. The point wasn’t to find the best values to get the best fits, but rather finding the parameters which return the most robust portfolios without losing all signal. This has been extended to all L-type penalties and constraints, but also significance, deformation, tail weight, and ambiguity sets. It’s pretty cool.
- Online walk-forward (and by extension multiple randomised) cross validation. Most priors and moment estimators (including higher order ones) gained the ability to be updated online, thus saving on recompute time at every step.
- Cross sectional factor prior. This lets you find the relationship between external, non-returns data to the behaviour of securities. For example, if you want to correlate the orange man’s degen hour twitter feed to the price of geriatric nappies and thus the value of walmart, you can do so now.
- Weight drift, previous weight source, and performance evaluation. In sequential cross validation, it’s now possible to choose whether to compute the drifted weights. As the portfolio evolves in time, its asset returns cause weights returned by the optimisation to change as the relative value of each security changes as the securities gain or lose value as dictated by the returns. Or keep them at the target weights, which are the weights returned by the optimiser for the previous period. And whether the constraints, risk measures and fees which use the previous weights (like turnover and tracking) take the drifted or target weights.
- Everything is now `NaN` aware. Which means the library now deals with late listings, delistings, holidays, cloudflare outages, etc. This is in the context of batch and online cross-validation. Both batch and online will handle late listing, delisting, and halts.
- Forced proportional and fixed liquidation fees, for when a security stops being investible.
- Vanilla moment estimators (including high order ones) now handle late listing, delisting, and halts based on a coverage policy, whose supertype can be subtyped by users to create their own.
- Regime adjusted moments. These natively handle late listing, delisting, and halts.

## Bug fixes

- Bug fix to matrix denoising (dang).
- Bug fix with how fixed fees were applied (oof).
- Bug fix in one of the GerberIQ templates (yikes).
- Bug fix to one of the Black-Litterman priors with double counting the risk free rate (F in chat)
- A failed optimisation in walk forward cross validation would forward its vector of `NaN` weights to the next unlucky schmuck and frick shit up for the rest of em. It now forwards the last successful weights.
- Lots of bugs with esoteric compositions not returning auxiliary data. Really only used for bookkeeping and reporting (these could be manually generated by running through the steps explicitly so not that big a deal, just a nuisance).

## Misc

- Tons of ergonomics changes, including the cleanup of overloadable function APIs. For example defining custom constraints and objective function penalties.
- More security changes, mainly to do with preventing accidental OOM. Can be set via Preferences.jl or by calling a function to change their values.
- Pretty printing no longer defaults to printing `nothing`. It’s now opt-in, so pretty printing will now be concise and not take 3 pages of the terminal for the more esoteric compositions.

## Breaking changes and migration guide

Since there’s been quite a few breaking changes instead of pasting them here I’ll refer you to the [release notes](https://github.com/dcelisgarza/PortfolioOptimisers.jl/releases) and new [migration guide](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/migration/) in the docs. The migration guides were clanker written because frankly i trust their pedantry more than i trust my own.

## Plans

- Thanks to the kind folk behind the development of Copulas.jl and VineCopulas.jl, i have all i need to implement the synthetic returns prior. So that’s the next feature on the list.
- Online portfolio optimisation/selection. What currently exists in the library concerns the prior and moments, as the optimisation models are rebuilt every time. However, there’s a few truly online methods and those aren’t in the library yet.
- Keep sweeping the library to improve the docs, find and fix bugs, improve type stability, coverage, performance, and ergonomics.
- I’m thinking about the 1.0 roadmap, but that may be some time away. There’s probably going to be a systematic revision of the API before it because I enjoy being able to break things without remorse.

---

<div class="post-metadata">

**Author:** ![dcelisgarza](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dcelisgarza/32/215951_2.png) [@dcelisgarza](https://discourse.julialang.org/u/dcelisgarza)\
**Post date:** [September 27, 2026, 3:21pm UTC](https://discourse.julialang.org/t/ann-portfoliooptimisers-jl-ape-together-strong/133099/19 "2026-09-27T15:21:47Z")

</div>

# PortfolioOptimisers.jl v0.32.0

_It’s 3 am in yo mamma’s basement. A gentle glow emanates from the darkness. Whence a faint, rhythmic buzzing can be heard. All is still, but for the black monolith in the corner…_

> _The honest answer the seam ships pays forward in the bit by bit representation of the previous solution in favour of the new methodology of the current embedded block — in contrast to the previous one which deviated by 1.31416 ulp or 2e-16 in CI and 3.4e-16 local — the new method deviates by 1.1169 ulp or 1.67e-16 in CI and 3.1e-16 local whilst being 40% faster and using 20% less memory. 259 tests cover the 6 changed lines._

> _The gap recovers commit `42069666ayylmao`’s previous state by threading the configuration via keyword arguments through the 42 function barriers. Measured, not inferred. 27 defects found and fixed — none change the answer as all tests use `Int64` but would overflow on 2% of tested cases if they were done on `Int8`. You should know `rabbit_foot_resize` was renamed `resize_rabbit_foot` to match the current style._

-– _What the fuck did you just say to me you little shit…_

* * *

As I’ve been growing more confident in using clankers to help me write docs and code. I’ve had to come up with ways to stop them from slopifying my artisanal, hand-crafted codebase. I’ve gone to painstaking lengths to prevent this from happening with the code—and, to a lesser extent, the docstrings (on-going process). And have been fairly successful at preventing it code-wise. I dare say they’re better than me at shipping bug-free code, catching bugs before they ship, thoroughly testing, and documenting my damn work. I find they’re still shit at architecting it—aside from mechanical, boiler-plate-y work that uses established patterns—but that’s ok, the mechanical aspect is not something i enjoy anyway.

However they produce unspeakable literary atrocities (looking at you Opus 5). I’ve been unapologetic at having the API docs get sloppified, as that meant the API was getting documented and updated accurately rather than being left to bitrot. Which brings me to admitting that I’ve been embarrassingly _laissez-faire_ with the examples and user guide. Like the API docs, I’ve gone for coverage rather than readability—especially given how vast the library has become.

Far be it from me to name names, but I have a strong suspicion there are a [couple](https://beta.thestorygraph.com/book_reviews/3dd98d9f-d288-4b86-9d45-e42eae73db76?sort=most_liked) choice [authors](https://onehundredpages.wordpress.com/2013/06/12/dont-make-fun-of-renowned-dan-brown/) whose illustrious “_ouvre_”_,_ has formed the bedrock of the clanker’s literary training. Reading the examples is comparable only to the feelings of disgust, shame, rage, and profound bewilderment at forcing myself to read the da vinci code at 15 because of all the normie love it was getting (it’s likely the shittiest book i’ve ever read lol).

So I’ve finally gone to the effort of somewhat remedying my transgressions. By systematically unslopifying docs, docstrings, examples and user guide. And setting up a process by which future and current additions shall be born unslopped.

With this out of the way, I bring shiny things.

* * *

v0.32 is a breaking release. Read the [migration guide](https://dcelisgarza.github.io/PortfolioOptimisers.jl/stable/migration/#From-v0.31-to-v0.32) before you upgrade.

It’s almost an entire library’s worth of features. Maybe in the future we’ll have `PortfolioOptimisers` and `OnlinePortfolioOptimisers`, or maybe more separate packages for all the stuff I’ve had to implement. Perhaps some of these can live on in lower-level packages. But that is a question for future me and claude.

## Changed walkforward schemes

They’re compatible with all optimisers, including the new online selectors.

- `OnlineIndexWalkForward`: steps through the data by a count of rows.
- `OnlineDateWalkForward`: steps through the data by calendar periods.
- `OnlineHindsightSplit`: fold `t` trains on rows `1:t` and tests on row `t`. Use it to build hindsight comparators for regret, as an online run.
- The training window always expands. An online update cannot “forget” a row. A rolling window is set on the estimator instead.
- These constructors replace `IndexWalkForward(…; ff = OnlineStep())`.

## Online portfolio selection

`OnlinePortfolioSelection` is a new optimiser family. It does not solve one large problem over all of the history. It updates the portfolio one period at a time, from the newest price change, with a fast rule that needs no solver. The rules have a guarantee: over time, they lose little log-wealth against the best fixed-mix portfolio, which you can know only in hindsight.

- What you give it:
  - `alg`: the rule that updates the weights. All of the rules below go here.
  - `w0`: the start allocation. The default is equal weights.
  - `fees`: a turnover fee. The fee is charged at each rebalance.
  - `set`: the allocation set, which gives the constraints on the weights. The default is long-only and fully invested.

- Use it for frequent rebalancing, for streaming data, or for strategies that must adapt quickly. A backtest one row at a time and a single `optimise` call give the same path.
- Do _not_ use it if you need a full risk model at each step. Many of these rules trade often, so test them with fees.

### Benchmarks

- `BuyAndHold`: buys once and never trades. From equal weights, it is the “market index” of the online literature. Use it as the “do nothing” baseline.
- `ConstantRebalancedPortfolio`: rebalances to the same weights `w` after each price move. With equal weights (UCRP), it is a strong baseline that many clever rules fail to beat.
- `BestConstantRebalancedPortfolio`: finds the fixed weights with the most log-wealth over a window. It uses Cover’s fixed point and needs no solver. It stops when a duality-gap certificate proves that it is optimal.
  - Use it as the comparator for regret.
  - Do not trade it. It reads the rows that it is scored on, so it looks into the future.

### Follow the winner

These rules move weight towards the assets that did well recently. They are smooth and have strong theory. They do badly in markets that mean-revert.

- `ExponentiatedGradient` (EG): multiplies each weight by a factor that grows with its recent gain, then normalises. It is the classic rule. It is stable and long-only, and a weight of zero stays zero.
- `GradientProjection` (OGD): adds a step to the weights, then projects them back onto the allocation set. The projection can set small weights to zero, so the portfolio can be sparse.
- `EGE`, `EGR`, `EGA`: EG with momentum, EG with per-asset scaling, and EG with both. Use them when the gradients are noisy. They have more parameters to tune.
- `AEG`, `MAEG`: EG that selects its own learning rate from a grid. It uses the rate that made the most wealth over all of the history (AEG) or over a recent window (MAEG). Use them when you do not know which rate to use.
- `NewtonStep` (ONS): a second-order rule with the best regret bound of the family, which is logarithmic. Each step solves an N×N system and a small programme, so it is slow for a large universe.
- `AdaptiveSubgradient` (AdaGrad): gives each asset its own step size, from the size of that asset’s past gradients. It suits a universe where the volatility of the assets is very different.
- `ExpectationMaximisation` (EM, also called Soft-Bayes): moves part of the way to the portfolio that drifted with the market. At `eta = 1` it is buy-and-hold. Smaller values make it slower and more conservative.
- `UniversalPortfolio`: Cover’s universal portfolio. It averages many sampled constant-rebalanced portfolios and weights them by their wealth. It has a strong guarantee. Its cost grows with the number of samples, which is 2000 by default.
- `AggregatingExponentialGradient` (WAEG): a mixture of EG experts at many learning rates, under the weak aggregating algorithm. Use it as an alternative to a search over the rate.
- `OptimisticStep`: wraps a first-order rule and uses a guess of the next gradient. If the guess is good, the regret is smaller. If the guess is bad, the cost is small. The source of the guess is one of these:
  - `LastGradient` (the default): the last gradient. This is good when the market changes slowly.
  - `MeanGradient`: the average of all of the past gradients. This is good when the gradients stay near a stable mean.
  - `ForecastGradient`: the gradient that a forecast from any expected-returns estimator implies. This is how your own forecast enters an online rule.

### Geometries, learning rates, gradient transforms and losses

Most users can keep the defaults. These settings are for users who research or tune a rule.

- _Geometry_: how the rule measures the distance between two portfolios. This controls the shape of the step and of the projection.
  - `EuclideanProjection`: the default of most rules. It sets small weights to zero, which is why the reversion rules are sparse. It is the only geometry that accepts a negative lower bound.
  - `EntropicProjection`: the geometry of EG. It keeps positive weights positive, and a weight of zero stays zero. It is long-only.
  - `TsallisProjection(; alpha)`: a family between the entropic and the Euclidean geometries, from the bandit literature. Use `alpha` in `(0, 1)` to tune it.
  - `LogBarrierProjection`: the Burg-entropy geometry. Its regret bound does not need a floor on the price relatives. This is attractive for markets with large crashes.
  - `GramProjection`: the geometry of `NewtonStep`. It needs a solver at each step.

- _Learning-rate schedule_: how the step size changes over time.
  - `InverseSquareRootRate`: `c / sqrt(t)`. It is the standard choice and does not need to know the horizon.
  - `DoublingTrickRate`: runs the rule in stages of increasing length, and restarts at the start allocation after each stage. It is useful for theory, but each restart can cause a large trade.
  - `SelfConfidentRate`: sets the rate from how far the portfolio is behind the best single asset. It adapts without tuning.
  - `HintResidualRate`: for `OptimisticStep` only. It sets the rate from the quality of the past guesses.
  - `WindowedBestRate`: selects the rate from a grid by recent wealth. It is the schedule of AEG and MAEG.

- _Gradient transform_: how the raw gradient changes before the step.
  - `PlainGradient` (the default): no change.
  - `GradientMomentum`: a moving average of the gradients. It removes noise, but it reacts more slowly.
  - `RootMeanSquareGradient`: divides each asset’s gradient by its typical size.
  - `AdaptiveMomentGradient`: both of the above.

- _Loss_: what the rule tries to optimise.
  - `LogWealth` (the default): maximise growth.
  - `RiskLoss`: minimise any risk measure of the library over the last `window` rows. The rule refits the prior at each step. Use it for an online minimum-risk strategy. It does not target growth. `risk_gradient` gives the gradient of the loss.

### Follow the loser

These rules bet that recent losers will recover (mean reversion), or that prices will go back to a recent level. They often show strong backtests. Fees and survivorship bias can remove most of that result, so test them with realistic costs.

- `PassiveAggressiveMeanReversion` (PAMR): if the last period’s return was above a threshold `eps`, it sells what went up. If not, it does nothing. The step size is one of these:
  - `NoSlack`: the full step (PAMR).
  - `LinearSlack`: the step with a cap `C` (PAMR-1).
  - `QuadraticSlack`: a softer step (PAMR-2).
  - Use a slack version in noisy markets, where a full step overreacts.

- `ForecastReversion`: moves the portfolio by the smallest amount that gives a forecast return of at least `eps`. The forecast can come from any expected-returns estimator. Ready-made versions:
  - `MovingAverageReversion` (OLMAR): the forecast is the average of the last 5 prices.
  - `ExponentialMovingAverageReversion` (OLMAR-2): the forecast is an exponential average. You choose a decay, not a window.
  - `RobustMedianReversion` (RMR): the forecast is a spatial median. One extreme price moves it very little.
  - `ReweightedPriceRelativeTracking` (RPRT): the forecast is an average with weights per asset that come from the data.
  - `GaussianWeightingReversion` (GWR): the forecast is two estimates that Gaussian weights give.
  - `LocalAdaptiveLearning` (LOAD): tests each asset for a trend. An asset with a trend uses its recent peak, and the other assets use an exponential average. It mixes trend and reversion.

- `ForecastTracking`: moves a fixed distance towards the forecast. It is the trend-following counterpart. Ready-made versions:
  - `PeakPriceTracking` (PPT): bets that each price goes back to its recent high.
  - `AdaptiveInputCompositeTrend` (AICTR): mixes a moving average, an exponential average and a peak, and gives most weight to the one that did best recently.
  - `TrendPromotePriceTracking` (TPPT): uses a different forecast for rising, flat and falling assets.

- `KernelTrendPatternTracking` (KTPT): predicts the next price in three states and scales the step with a kernel. `ElasticNetPath` does the regression inside it.
- `TransactionCostOptimisation` (TCO): trades only when the expected gain is larger than the cost, and sets small trades to zero. Use it when fees are important.
- `ShortTermSparsePortfolio` (SSPO): a sparse portfolio from an L1 penalty. You choose how it solves each step:
  - `HuberOptimum` (the default): the exact answer in closed form.
  - `AlternatingDirectionMethod`: the loop as defined in the paper the rule was published in, with its early stop. Use it to reproduce paper’s results.
  - `L1Optimum`: puts all of the budget on the asset with the best forecast. It is the most concentrated choice.

- `ConfidenceWeightedMeanReversion` (CWMR): keeps a belief about each weight, as a mean and an uncertainty. Weights with high confidence move less.
  - `VarianceUpdate` and `StandardDeviationUpdate` are the two versions defined in the paper the rule was published in (CWMR-Var and CWMR-Stdev).

- `AntiCorrelation` (Anticor): moves wealth from an asset that grew to an asset whose returns followed it with a lag in the past.

### Pattern matching

`FollowTheLeader` finds past periods that look at the recent market. It solves for the best portfolio over those periods, which is “the leader”, and holds it. With `gamma > 0`, it moves only part of the way to the leader.

- _Selectors_: which past periods to use.
  - `Prefix`: all of the history. It is stable, but slow to adapt.
  - `LastRows(W)`: the last `W` periods. It adapts quickly, but it is noisy.
  - `HistogramMatch` (BH): periods whose recent window falls in the same histogram cells as now.
  - `KernelMatch` (BK): periods whose recent window is within a distance `radius` of now.
  - `NearestNeighbourMatch` (BNN): the `k` periods whose recent windows are nearest to now.
  - `CorrelationMatch` (CORN): periods whose recent window has a correlation of at least `rho` with now. It is one of the strongest rules in the literature.
  - `ClusterMatch`: periods in the same cluster as now. It takes any clustering estimator of the library.

- _Leader_: `BestConstantRebalancedPortfolio`, which needs no solver, or any JuMP optimiser. With a JuMP optimiser, the leader can use a risk measure and the full set of constraints.
- `ShortTermLossControlPortfolio` (SPOLC): maximises the worst return of a short window, less a variance penalty.
  - `RankOneCovariance` estimates the covariance from a short window with many assets.

- `LowDimensionEnsemblePortfolio` (OLDEM): forecasts with many small regressions over random subsets of assets. It includes a turnover fee.
  - `LowDimensionEnsemblePrior` gives this forecast as a prior, so you can use it elsewhere.
  - It is designed for large universes.

- _Cost_: these rules solve a problem at each step, so they are slower than the closed-form rules.

### Meta-learning

`ExpertMixture` runs many rules at the same time and combines their portfolios. Use it when you do not know which rule suits the market.

- _Weighting_: which experts to trust.
  - `BuyAndHold` (the default): weights each expert by the wealth that it made.
  - `AggregatingAlgorithm(; eta)`: a tempered wealth weighting. A value of `eta` below one is more cautious.
  - `TopK`: holds only the `k` richest experts. At `k = 1` it follows the best one.
  - `WeakAggregatingAlgorithm`: a wealth weighting whose rate decreases over time.
  - `SwitchingWeighting`: at each period, moves some weight to the other experts. This is good when the best expert changes over time.

- _More options_:
  - A bounded set over the experts, for example a cap on each expert’s weight.
  - A start allocation over the experts.
  - _Gradient point_:
    - `OwnPoint` (the default): each expert runs as if it were alone.
    - `BlendPoint`: each expert reads its gradient at the combined portfolio. The dynamic-regret guarantees need this.

- _Ready-made mixtures_:
  - `SwitchingPortfolio`: switches between single assets.
  - `Ader` and `Sword`: a grid of gradient-projection experts at different learning rates. They have guarantees against a moving target. `Sword` adapts better when the losses are small.
  - `FollowTheLeadingHistory`: copies of one rule, each started at a different period. It adapts to changes of regime. `prune` keeps the number of copies small.

### Allocation sets

- `BoundedAllocationSet`: fully invested, with weight bounds. It is exact and fast, with no solver. The entropic and barrier geometries need non-negative bounds.
- `ProgrammeAllocationSet`: all of the constraints of `JuMPOptimiser`, with the same field names. These include linear constraints, turnover, cardinality, tracking error and a risk ceiling under any risk measure (or vectors of them). It needs a solver at each step, so it is slower. Cash is an asset with a price relative of one.
- `TimeDependent`: a different set for each fold, for example bounds that change over time. The set itself doesn’t accept time-dependent constraints. The whole thing has to be swapped out. This might change in the future when the clankers and I figure out a way to do support individual time-dependent constraints.

### Price-level forecasts

`PriceLevelExpectedReturns` forecasts the next return from a statistic of recent price levels. You can use it in any expected-returns slot, not only in online rules.

- _Statistics_:
  - `MovingAverage`: the average of the last `window` prices.
  - `ExponentialMovingAverage`: recent prices get more weight.
  - `TruncatedExponentialMovingAverage`: an exponential average of the last `window` prices only.
  - `SpatialMedian`: a robust centre. It resists outliers.
  - `WindowPeak`: the highest recent price.
  - `LaggedPrice`: the price `lag` periods ago. It is a bet on reversal.
  - `TrendSwitch`: selects a statistic per asset by its trend. It has one statistic for rising, one for flat and one for falling assets.
  - `CompositeTrend`: mixes several trend forecasts and gives most weight to the best recent one.
  - `ReweightedPriceRelative`, `GaussianWeightedDoubleEstimate`, `KernelTrendPattern`: the forecasts of RPRT, GWR and KTPT.

- _Trend tests for `TrendSwitch`_:
  - `RegressionSlope`: the slope of a fitted line, compared with a threshold.
  - `PairwiseSlopeSum`: the sign of the sum of the slopes between each pair of recent prices.

- A statistic with an exact recursive update reads one row at a time, so it is fast online.
- `PriorExpectedReturns` uses the mean of any prior as a forecast. For example, a Black–Litterman posterior can drive a reversion rule, so your views enter an online strategy.

### Regret and performance

- `log_wealth_regret`: how much log-wealth a strategy lost against a comparator. It returns:
  - the total regret and the regret per period,
  - the running regret over time,
  - a Newey–West test of whether the difference is significant,
  - the last wealth of both,
  - the path length of the comparator, which is how much the comparator traded.

- _Static regret_: the comparator is `BestConstantRebalancedPortfolio`, which is one fixed portfolio.
- _Dynamic regret_: the comparator can change each period.
  - `HindsightSplit(; prefix = true)`: fold `t` trains on rows `1:t`. This gives “be-the-leader”.
  - `HindsightSplit(; prefix = false)`: fold `t` trains on row `t` alone. This gives the best portfolio of each period, which is the hardest comparator.
  - `BudgetedHindsightPath`: the best sequence of portfolios whose total change is at most `L`. It is between the two extremes. `L = 0` is static regret, and a large `L` is fully dynamic regret.

- `performance_summary` now takes a `benchmark`. It reports excess return, tracking error, information ratio and turnover. `plot_performance_summary` shows them as four more bars.
- `plot_stacked_area_composition` shows how the weights change over the folds of a walk-forward.

### Missing data and delistings

- An asset can have no price for a period. Its leg is held as cash for that period.
- Each statistic over a window uses only the assets that the window observes.
- An asset that leaves the market and comes back starts “cold”. Its forecast is `NaN` until it has a price again.
- No rule ever trades on a return that did not happen. This lets you backtest on a realistic universe, without survivorship bias.

## Other additions

- `GeodesicShrinkageCovariance` shrinks a noisy covariance towards a target along a curved path, not entry by entry. The result is positive definite at every intensity when both ends are positive definite. It costs more to compute than linear shrinkage.
  - `IdentityTarget`: the identity matrix. It ignores scale, so use it on standardised data.
  - `ScaledIdentityTarget`: the identity times the average variance. It is the most conservative target.
  - `CommonCovarianceTarget`: all of the assets have the average variance and the average covariance.
  - `ConstantCorrelationTarget`: keeps each variance and gives each pair the average correlation.
  - `DiagonalTarget`: keeps the variances and removes all of the correlations.
  - A fixed matrix: for example, a covariance from a factor model.

- _Exact weight finalisers_ move weights that break their bounds back inside them, in one exact step and without a solver.
  - `EuclideanWeightFinaliser`: makes the smallest total change. It works for long-short portfolios and for any budget.
  - `EntropicWeightFinaliser`: keeps the ratios between the free weights. It works for long-only portfolios. Outside that case, it falls back to the Euclidean projection.
  - Example: with a cap of `0.5`, the weights `[0.9, 0.09, 0.01]` become `[0.5, 0.29, 0.21]` under the Euclidean finaliser, and `[0.5, 0.45, 0.05]` under the entropic finaliser.
  - `IterativeWeightFinaliser` stays the default.

- _Collateral in finite allocation_: when you convert weights to whole shares, you choose how the short side pays for the long side.
  - `ProceedsCollateral` (the default): the long side can spend the cash from the short sales.
  - `CashCollateral(; amount)`: the short sales give no cash to spend. The long money, the short money and the fees together cannot exceed `amount`. Use it when short proceeds cannot pay for purchases.
  - You can write your own rule as a subtype of `AbstractCollateralAlgorithm`.

- _Hedged factor risk budgeting_: `FactorRiskBudgeting(; flag = true, hedge = true)` sets the weights that the factors do not explain to a minimum-variance hedge. Then all of the risk comes from the factors that you budget. Use it for a pure factor risk-parity portfolio.
- _Extension interfaces_: 75 more names are now `public`.
  - 58 are new. They are the hooks for your own online rules, learning rates, projections, sample selectors and shrinkage targets, for example `online_update!`, `project`, `learning_rate`, `select_rows` and `shrinkage_target`.
  - 17 existed before and are now public. They include the abstract preprocessing and data-result types, so you can write your own preprocessing steps.

- _Documentation_:
  - A new user-guide chapter compares every group of online rules on two synthetic markets, by static and dynamic regret.
  - A new example runs the online rules on real prices, with a turnover fee, a learning-rate schedule, a mixture, a risk loss and a risk ceiling.

## Fixes

This release also fixes many defects that the clanker-based reviews have found. The migration guide lists each one.

- _Results that move._ 52 results from v0.31 were wrong, and 7 were approximate. If you compare new results with v0.31 results, recompute them first.
- _Clearer errors._ 27 calls that ran in v0.31, or failed with an unclear error, now actually explain what went wrong instead of failing on some arcane method error.
- Improved type stability and genericity.
