# universal-portfolios: twenty years of portfolio theory in one Python package

> universal-portfolios collects online portfolio selection algorithms in a single Python package with a common base class, a shared result and metrics layer, and two notebooks that teach the field from the ground up. The author is candid that the implementations follow his reading of the literature, and the toolchain around it is unusually disciplined.

**Marigold/universal-portfolios** — Collection of algorithms for online portfolio selection

- Repository: https://github.com/Marigold/universal-portfolios
- Stars: 869 · Forks: 222
- Language: Jupyter Notebook
- License: NOASSERTION
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/marigold-universal-portfolios

## The problem is choosing weights before you see the future

Online portfolio selection, shortened to OLPS in the README, is about choosing portfolio weights in every period to maximise final wealth. The point of the field is the setting: you decide the weights each period without knowing what the next period will do, and you are judged on the wealth you end up with rather than on how close your weights were to some ideal allocation.

The README names two examples of such portfolios, the Markowitz portfolio and the Universal portfolio, and it frames the field's maturity accurately. There is active research in online portfolios, the results are mostly theoretical, and algorithms for practical use are starting to appear. That last clause is the important one, because it is the reason a package like this is worth having: the literature moves faster than the reference implementations, and each algorithm in the papers comes with its own notation, its own data conventions and its own way of reporting a result.

The package's stated purpose is to bring those algorithms together and provide unified tools for their analysis. The author is also explicit about the epistemics: several state of the art algorithms are implemented based on his understanding of the available literature, and contributions or corrections are more than welcome. That is a more useful statement than it might look, because it tells you the implementations are a reading of the papers rather than ports of the authors' own code, and it tells you where to direct a bug report about a discrepancy.

The project is on PyPI as universal-portfolios, the primary language of the repository is Jupyter Notebook, and the last push was on 2026-10-19.

## A base class, a result class, and a table of algorithm families

The package layout is the architecture, and it is small enough to hold in your head.

The universal.algo module provides a general class that all the individual algorithm subclasses inherit, and its job is to compute the weights at every timestep. The universal.result module computes the portfolio wealth from those weights and produces the various metrics on the strategy's performance. The universal.data module holds datasets to help you on your way, and universal.algos holds the implementations themselves.

That separation is the value of the package. Each algorithm is a subclass that produces a weight vector, and everything downstream, the wealth curve and the metrics, is shared. Adding the twentieth-first algorithm means writing one class, not writing a new back test harness.

The README groups the algorithms into families in a table, which is the fastest way to understand the taxonomy of the field. Under benchmarks sit BAH, CRP, BCRP and DCRP. Follow the winner contains the Universal Portfolios algorithm itself, the Exponential Gradient method and AdaHedge. Follow the loser holds Anticorr, PAMR and RMR. Pattern matching has BNN and CORN, the nearest neighbour and correlation based approaches. The other column gathers Kelly, Soft-Bayes, Best so far, MPT, Markowitz, ONS, CWMR, WMAMR and RPRT.

The names alone give you the shape of the literature. The follow the winner and follow the loser families are the two classic responses to a benchmark that performs badly: either move your money towards whatever is currently winning, or move it away from whatever is losing. Pattern matching is the local version of the same idea, using similarity to a target rather than a global ranking. Kelly is the classical fractional betting approach adapted to weights, and Markowitz and MPT are the mean variance baselines that the online methods are usually measured against.

## Two notebooks carry the teaching material

The examples directory holds two notebooks and they do different jobs.

The first, On-line portfolios, explains the basic use of the library. It covers the script sequence, the various options, the method arguments, and a strategy template intended to get you started. That is a practical orientation: what the entry points are, what a strategy looks like, and what you have to fill in.

The second, modern-portfolio-theory, goes deeper into the OLPS principle and into the tools the library develops to approach it. It is the one that explains why the weight computation is shaped the way it is, which matters if you are going to implement an algorithm yourself or debug one that behaves oddly.

Both are runnable without installing anything, because the README links Binder versions of each notebook. That is a small but real convenience: you can open the notebook in a browser and follow along with a live Python kernel rather than setting up an environment first.

There is also a third notebook in the examples directory of the repository, with a title about beating the market with a simple possible predictive metric, and it is worth noticing that the package is not only about the classic online algorithms. The presence of a metric-driven example suggests the library is meant to be used for the analysis side as much as the strategy side, which is consistent with the stated goal of unified tools for analysis.

For a package in this field, the notebooks are the documentation. The README is a map, and the notebooks are the territory, and the Binder links remove the excuse of a broken local environment.

## Install from PyPI, then run one function

The install is the standard one.

```bash
pip install universal-portfolios
```

The quick start in the README shows how little code it takes to run a strategy. The tools module is imported, an algorithm is imported from universal.algos, and a single call does the work:

```python
from universal import tools
from universal.algos import CRP

if __name__ == '__main__':
  # Run CRP on a computer-generated portfolio of 3 stocks and plot the results
  tools.quickrun(CRP())
```

That quickrun helper is doing a lot for the reader. It builds a computer-generated portfolio of three stocks, runs the constant rebalanced portfolio strategy over it and plots the results, which means you get a complete, runnable example without writing a data loader, a weight loop or a wealth accumulator yourself. It is also the fastest sanity check that an installation works.

The dependency list in the project file explains where the other features come from. scipy and numpy do the numerics, cvxopt is the convex optimisation library that the Markowitz style methods need, pandas and pandas-datareader bring market data in, yfinance is the other market data source, statsmodels and scikit-learn cover the statistical and machine learning parts, and matplotlib, seaborn and plotly are the three plotting libraries, with Pillow and tqdm for images and progress. The package requires Python 3.11 up to but not including 3.15.

The project is built with hatchling, and the version in the manifest is 0.4.17, which tells you it is a mature line rather than an experiment. There are no GitHub releases, so PyPI is where the versions live.

## The toolchain is stricter than the code

The development configuration is the most interesting part of the repository for anyone thinking about maintaining a numerical package.

The uv configuration sets a rolling exclusion window of three days for dependency resolution, and the comment explains why the exception exists for the linter and the type checker: ruff and ty are pinned exactly, so the rolling cutoff only has to allow whatever their currently pinned release is. The two tools are pinned at ruff 0.16.0 and ty 0.0.65, and the reasoning is spelled out in the file. A new ruff changes the default rule set and the formatter, and a new ty always finds new errors, so either one turns continuous integration red with no code change, and therefore upgrades happen deliberately rather than automatically.

The dependency groups are split for a reason that is worth copying. The ci group holds pytest plus those two pinned tools, and the dev group adds ipdb, line-profiler, ipykernel, jupyter and nbdime. The comment states the outcome plainly: the CI installs only the ci group, which is 57 packages instead of 147. The notebook stack never reaches the test runner, so a broken Jupyter dependency cannot turn a build red.

The Makefile keeps the same separation. It includes a default.mk that holds the test and check targets, sets the source paths to the universal package and the tests, and provides a requirements target that runs uv export to regenerate the requirements file. The exported requirements.txt in the repository even carries a header saying it was autogenerated by that command, and the pinned versions inside it are the resolved output, with numpy pinned differently depending on the Python version.

The result is a package where the reproducible environment is a first class concern, which for numerical research code is the difference between a result you can rerun and one you cannot.

## Data sources, licences and the gap to practice

Three things deserve a note before you build on this.

The first is where the data comes from. The universal.data module ships datasets, and the dependency list shows that market data is also pulled from pandas-datareader and yfinance at runtime. That means some of the examples depend on the network and on upstream providers, and that a back test which looked reproducible yesterday may not be today if the data source changed. For research comparisons it is better to pin a dataset locally than to re-fetch it.

The second is the licence. The repository listing records the licence as other, described as a custom licence that GitHub cannot classify, with a pointer to the LICENSE file. For a package that is installed from PyPI and used in coursework, that is worth reading rather than assuming.

The third is the field's own gap, which the README is upfront about. The results are mostly theoretical and practical algorithms are only starting to appear. This package is a research tool, and the honest use of it is to compare strategies and to understand the structure of the online learning problem, not to allocate money.

That is where the extra resources the README lists become valuable rather than decorative. There is Ernest Chan's blog post introducing online portfolios for a general audience, CASTrader for a further reading, and a survey by Bin Li and Steven Hoi on arXiv for the formal treatment. There is an independent comparison of all the algorithms by Paul Perry run over more recent exchange traded fund datasets, which is a direct check on the implementations here. The original authors of some of the algorithms published their own toolbox in MATLAB, which is the reference to compare against when a Python result looks wrong. And for readers who prefer R, the logopt package and blog by Marc Delvaux cover the Universal portfolio approach in that ecosystem. Using several of those together is how you tell an implementation bug from a real finding.

## Conclusion

Adopt universal-portfolios if you are researching or teaching online portfolio selection and want every algorithm behind one interface with one set of metrics, rather than a dozen repositories each with its own data format and result object. Do not adopt it as an investment library, because the README says the results are mostly theoretical and that practical algorithms are only starting to appear, and because the package pulls market data from the network at runtime. Before you rely on a specific implementation, compare it against the alternatives the README points to, namely the MATLAB toolbox from the original authors and the independent comparison notebooks, since the author asks for corrections and contributions precisely because these are reimplementations from the literature.

## FAQ

### What is online portfolio selection and what does the package do?

Online portfolio selection is about choosing portfolio weights in every period to maximise final wealth. The universal-portfolios package brings different OLPS algorithms together and provides unified tools for their analysis, with a common base class that computes the weights and a result module that computes wealth and the performance metrics.

### Which algorithms does universal-portfolios implement?

The README groups them into families: benchmarks such as BAH, CRP, BCRP and DCRP, follow the winner methods including Universal Portfolios, Exponential Gradient and AdaHedge, follow the loser methods Anticorr, PAMR and RMR, pattern matching with BNN and CORN, and others including Kelly, Soft-Bayes, Best so far, MPT, Markowitz, ONS, CWMR, WMAMR and RPRT.

### How do I run a strategy with universal-portfolios?

The README's quick start imports tools from universal and CRP from universal.algos, then calls tools.quickrun(CRP()), which runs the strategy on a computer-generated portfolio of three stocks and plots the results. The package itself is installed with pip install universal-portfolios and requires Python 3.11 or newer.

### Where can I read about the algorithms without installing anything?

The repository holds two notebooks, one on the basic use of the library and one on modern portfolio theory, and the README links Binder versions of both so they can be run in a browser. It also points to an independent comparison of all the algorithms over exchange traded fund datasets and to the MATLAB toolbox published by the original authors.

### How does the project run its tests?

The Makefile delegates to an included default.mk, and the README says to set up a virtual environment, install the dependencies and run make test. The continuous integration group installs pytest plus pinned versions of ruff and ty, which is 57 packages instead of the 147 that the full development group with the Jupyter stack would need.

## Sources

- [Issues](https://github.com/Marigold/universal-portfolios/issues)
- [Marigold/universal-portfolios on GitHub](https://github.com/Marigold/universal-portfolios)
- [README](https://github.com/Marigold/universal-portfolios/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/marigold-universal-portfolios
