Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ name: CI

on:
push:
branches: [ main ]
branches: [ main, dev ]
pull_request:

branches: [ main, dev ]

jobs:
test:
runs-on: ${{ matrix.os }}
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ The information provided by OPES is for educational, research and informational
| Constant | L1 Regularization |
| Gamma | L2 Regularization |
| Lognormal | L-infinity Regularization |
| Inverse Gaussian | Entropy |
| Inverse Gaussian | Negative Entropy |
| Compound Poisson-Lognormal | Weight Variance |
| | Mean Pairwise Absolute Deviation |
| | KL-Divergence from Uniform (Experimental) |
| | JS-Divergence from Uniform (Experimental) |
| | Maximum Pairwise Deviation |
| | JS-Divergence from Uniform Weights |
| | Wasserstein-1 from Uniform Weights |

---

Expand Down Expand Up @@ -143,7 +144,7 @@ You can also verify by using python.
import yfinance as yf

# Importing our Kelly class
from opes.objectives.utility_theory import Kelly
from opes.objectives import Kelly

# Obtaining ticker data
# Basic yfinance stuff
Expand Down
16 changes: 7 additions & 9 deletions docs/docs/backtesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ It also stores transaction cost parameters for portfolio simulations.

**Args:**

- `train_data` (*pd.DataFrame*): Historical training data. Defaults to None.
- `test_data` (*pd.DataFrame*): Historical testing data. Defaults to None.
- `train_data` (*pd.DataFrame*): Historical training data. Defaults to `None`.
- `test_data` (*pd.DataFrame*): Historical testing data. Defaults to `None`.
- `cost` (*dict, optional*): Transaction cost parameters. Defaults to `{'const': 10.0}`. Various cost models are given below:
- `{'const': constant_bps_value}`: Constant cost value throughout time. Deterministic.
- `{'gamma': (shape, scale)}`: Gamma distributed cost. Stochastic.
- `{'lognormal': (mu, sigma)}`: lognormally distributed cost. Stochastic.
- `{'lognormal': (mu, sigma)}`: Lognormally distributed cost. Stochastic.
- `{'inversegaussian': (mean, shape)}`: Inverse gaussian distributed cost. Stochastic.
- `{'jump': (arrival_rate, mu, sigma)}`: Poisson-compound lognormally distributed cost. Stochastic.

Expand Down Expand Up @@ -64,8 +64,6 @@ It also stores transaction cost parameters for portfolio simulations.

$$R_t = \frac{P^{(t)}}{P^{(t-1)}} - 1$$

---

### Methods

#### `backtest`
Expand Down Expand Up @@ -254,9 +252,9 @@ def plot_wealth(
OPES ships with a basic plotting utility for visualizing portfolio wealth over time.

This method exists for quick inspection and debugging, not for deep performance analysis.
It visualizes cumulative wealth for one or multiple strategies
using their periodic returns. It also provides a breakeven reference line
and optional saving of the plot to a file.
It visualizes cumulative wealth for one or multiple strategies using their periodic
returns. It also provides a breakeven reference line and optional saving of the plot to
a file.

!!! tip "Recommendation:"
For serious research, reporting, or strategy comparison, we strongly recommend writing your own custom plotting pipeline.
Expand Down Expand Up @@ -309,7 +307,7 @@ and optional saving of the plot to a file.
"Maximum Mean (L2, 1e-3)": scenario_1['returns'],
"Mean Variance (RA=1.5)": scenario_2,
},
timeline=scenario_1['dates']
timeline=scenario_1['timeline']
)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ OPES is a research-oriented and experimentation-focused Python module for portfo
# Demonstration of portfolio optimization using the Kelly Criterion
# 'data' represents OHLCV market data grouped by ticker symbols

from opes.objectives.utility_theory import Kelly
from opes.objectives import Kelly

# Initialize a Kelly portfolio with fractional exposure and L2 regularization
kelly_portfolio = Kelly(fraction=0.8, reg="l2", strength=0.01)
Expand Down
57 changes: 29 additions & 28 deletions docs/docs/regularization.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@ where $R(\mathbf{w})$ encodes structural preferences over the weights $\mathbf{w

## Regularization Schemes

| Name | Formulation | Use-case |
|------------|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
| `l1` | $\sum_i \lvert \mathbf{w}_i\rvert$ | Encourages sparse portfolios by driving many weights to zero. Mostly relevant for long-short or unconstrained gross exposure settings. |
| `l2` | $\sum_i \mathbf{w}_i^2$ | Produces smooth, stable portfolios and reduces sensitivity to noise. |
| `l-inf` | $\max_i \lvert \mathbf{w}_i\rvert$ | Penalizes the largest absolute position, enforcing a soft cap on single-asset dominance. |
| `entropy` | $-\sum_i \mathbf{w}_i \log \mathbf{w}_i$ | Encourages diversification by penalizing concentration. |
| `variance` | $\ \text{Var}(\mathbf{w})$ | Pushes allocations toward uniformity without strictly enforcing equal weights. |
| `mpad` | $\frac{1}{n^2} \sum_{i}^n \sum_{j}^n \lvert \mathbf{w}_i - \mathbf{w}_j\rvert$ | Measures and penalizes inequality across weights. |
| `kld` | $\ \text{D}_{\text{KL}}(\mathbf{w} \| \mathbf{u})$ | Measures Kullback-Leibler divergence from uniform weights. |
| `jsd` | $\ \text{D}_{\text{JSD}}(\mathbf{w} \| \mathbf{u})$ | Measures Jensen-Shannon divergence from uniform weights. |

!!! note "Note"
For long-short portfolios, mathematically grounded regularizers such as `entropy`, `kld`, and `jsd` first normalize the weights
and constrain them to the simplex before applying the regularization, ensuring mathematical coherence is not violated.

!!! note "Temporary Note"
Kullback-Leibler regularization and entropy are the exact same, since KL-divergence's prior distribution is uniform weights. However
it is included so that it *may* be later updated with custom prior distribution (weights).
| Regularization Scheme | Identifier | Formulation |
| ----------------------------------------------- | ---------- | ------------------------------------------------------------------------------------- |
| Taxicab Norm | `l1` | $\sum_i \lvert \mathbf{w}_i\rvert$ |
| Euclidean Norm | `l2` | $\sum_i \mathbf{w}_i^2$ |
| Chebyshev Norm | `l-inf` | $\max_i \lvert \mathbf{w}_i\rvert$ |
| Negative Entropy of Weights | `entropy` | $\sum_i \mathbf{w}_i \log \mathbf{w}_i$ |
| Jensen-Shannon Divergence from Uniform Weights | `jsd` | $\text{D}_{\text{JSD}}(\mathbf{w} \| \mathbf{u})$ |
| Variance of Weights | `variance` | $\text{Var}(\mathbf{w})$ |
| Mean Pairwise Absolute Deviation | `mpad` | $\frac{1}{n^2} \sum_{i}^n \sum_{j}^n \lvert \mathbf{w}_i - \mathbf{w}_j\rvert$ |
| Maximum Pairwise Deviation | `mpd` | $\max_{i,j} \lvert \mathbf{w}_i - \mathbf{w}_j \rvert$ |
| Wasserstein-1 Distance from Uniform Weights | `wass-1` | $\text{W}_{1}(\mathbf{w}, \mathbf{u})$ |

!!! note "Notes"
- `l1` regularization is mainly used for long-short portfolios to encourage less extreme
allocations to meet the net exposure of 1. Using it on long-only portfolios is redundant.
- For long-short portfolios, mathematically grounded regularizers such as `entropy`, `jsd`
and `wass-1` first normalize the weights and constrain them to the simplex before applying
the regularization, ensuring mathematical coherence is not violated.

---

Expand All @@ -60,18 +60,19 @@ The following objectives do not support regularization:

```python

# Importing a valid optimizer from opes
from opes.objectives.markowitz import MaxMean
# Importing an optimizer which supports regularization
from opes.objectives import MaxMean

# Initializing different portfolios with various regularization schemes
maxmean_taxi = MaxMean(reg='l1', strength=0.01)
maxmean_eucl = MaxMean(reg='l2', strength=0.01)
maxmean_cheby = MaxMean(reg='l-inf', strength=0.01)
maxmean_entropy = MaxMean(reg='entropy', strength=0.01)
maxmean_var = MaxMean(reg='variance', strength=0.01)
maxmean_mpad = MaxMean(reg='mpad', strength=0.01)
maxmean_mpad = MaxMean(reg='kld', strength=0.01)
maxmean_mpad = MaxMean(reg='jsd', strength=0.01)
maxmean_taxi = MaxMean(reg='l1', strength=0.01) # Taxicab norm
maxmean_eucl = MaxMean(reg='l2', strength=0.01) # Euclidean norm
maxmean_cheby = MaxMean(reg='l-inf', strength=0.01) # Chebyshev norm
maxmean_entropy = MaxMean(reg='entropy', strength=0.01) # Negative entropy of weights
maxmean_jsd = MaxMean(reg='jsd', strength=0.01) # Jensen-Shannon divergence
maxmean_var = MaxMean(reg='variance', strength=0.01) # Variance of weights
maxmean_mpad = MaxMean(reg='mpad', strength=0.01) # Mean pairwise absolute deviation
maxmean_mpd = MaxMean(reg='mpd', strength=0.01) # Maximum pairwise deviation
maxmean_wass = MaxMean(reg='wass-1', strength=0.01) # Wasserstein-1
```


Expand Down
6 changes: 5 additions & 1 deletion opes/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
__version__ = "0.8.2"
# Version Log
__version__ = "0.9.1"

# Backtester easy import
from .backtester import Backtester
16 changes: 7 additions & 9 deletions opes/backtester.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ def __init__(self, train_data=None, test_data=None, cost={"const": 10.0}):
"""
**Args:**

- `train_data` (*pd.DataFrame*): Historical training data. Defaults to None.
- `test_data` (*pd.DataFrame*): Historical testing data. Defaults to None.
- `train_data` (*pd.DataFrame*): Historical training data. Defaults to `None`.
- `test_data` (*pd.DataFrame*): Historical testing data. Defaults to `None`.
- `cost` (*dict, optional*): Transaction cost parameters. Defaults to `{'const': 10.0}`. Various cost models are given below:
- `{'const': constant_bps_value}`: Constant cost value throughout time. Deterministic.
- `{'gamma': (shape, scale)}`: Gamma distributed cost. Stochastic.
- `{'lognormal': (mu, sigma)}`: lognormally distributed cost. Stochastic.
- `{'lognormal': (mu, sigma)}`: Lognormally distributed cost. Stochastic.
- `{'inversegaussian': (mean, shape)}`: Inverse gaussian distributed cost. Stochastic.
- `{'jump': (arrival_rate, mu, sigma)}`: Poisson-compound lognormally distributed cost. Stochastic.

Expand Down Expand Up @@ -71,8 +71,6 @@ def __init__(self, train_data=None, test_data=None, cost={"const": 10.0}):
- After cleaning/truncation, close prices are extracted per asset. Returns are computed as:

$$R_t = \\frac{P^{(t)}}{P^{(t-1)}} - 1$$

---
"""
# Assigning by dropping nans to ensure proper indexing
# Dropping nan rows results makes backtest loops robust and predictable
Expand Down Expand Up @@ -489,9 +487,9 @@ def plot_wealth(
OPES ships with a basic plotting utility for visualizing portfolio wealth over time.

This method exists for quick inspection and debugging, not for deep performance analysis.
It visualizes cumulative wealth for one or multiple strategies
using their periodic returns. It also provides a breakeven reference line
and optional saving of the plot to a file.
It visualizes cumulative wealth for one or multiple strategies using their periodic
returns. It also provides a breakeven reference line and optional saving of the plot to
a file.

!!! tip "Recommendation:"
For serious research, reporting, or strategy comparison, we strongly recommend writing your own custom plotting pipeline.
Expand Down Expand Up @@ -542,7 +540,7 @@ def plot_wealth(
"Maximum Mean (L2, 1e-3)": scenario_1['returns'],
"Mean Variance (RA=1.5)": scenario_2,
},
timeline=scenario_1['dates']
timeline=scenario_1['timeline']
)
```
"""
Expand Down
38 changes: 38 additions & 0 deletions opes/objectives/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Utility Theory
from .utility_theory import Kelly, QuadraticUtility, CARA, CRRA, HARA

# Markowitz Portfolio Theory
from .markowitz import MaxMean, MinVariance, MeanVariance, MaxSharpe

# Risk Measures
from .risk_measures import (
VaR,
CVaR,
MeanCVaR,
EVaR,
MeanEVaR,
EntropicRisk,
WorstCaseLoss,
)

# Principled Heuristics
from .heuristics import (
Uniform,
InverseVolatility,
SoftmaxMean,
MaxDiversification,
RiskParity,
REPO,
)

# Online Portfolios
from .online import UniversalPortfolios, BCRP, ExponentialGradient

# Distributionally Robust Optimization
from .distributionally_robust import (
KLRobustKelly,
KLRobustMaxMean,
WassRobustMaxMean,
WassRobustMinVariance,
WassRobustMeanVariance,
)
6 changes: 3 additions & 3 deletions opes/objectives/distributionally_robust.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,8 @@ def _find_dual(self):
def _prepare_optimization_inputs(self, data, weight_bounds, w, custom_mean=None):
# Extracting trimmed return data from OHLCV and obtaining tickers and Checking for initial weights
# Checking for mean and weights and assigning optimization data accordingly
self.tickers, data = extract_trim(data)
self.mean = np.mean(data, axis=0) if custom_mean is None else custom_mean
self.tickers = extract_trim(data)[0]
self.weights = np.array(
np.ones(len(self.tickers)) / len(self.tickers) if w is None else w,
dtype=float,
Expand Down Expand Up @@ -490,6 +490,7 @@ def _find_dual(self):
def _prepare_optimization_inputs(self, data, weight_bounds, w, custom_cov=None):
# Extracting trimmed return data from OHLCV and obtaining tickers and Checking for initial weights
# Checking for mean and weights and assigning optimization data accordingly
self.tickers, data = extract_trim(data)
if custom_cov is None:
# Handling invertibility using the small epsilon * identity matrix
# small epsilon scales with the trace of the covariance
Expand All @@ -500,7 +501,6 @@ def _prepare_optimization_inputs(self, data, weight_bounds, w, custom_cov=None):
)
else:
self.covariance = custom_cov
self.tickers = extract_trim(data)[0]
self.weights = np.array(
np.ones(len(self.tickers)) / len(self.tickers) if w is None else w,
dtype=float,
Expand Down Expand Up @@ -667,6 +667,7 @@ def _prepare_optimization_inputs(
):
# Extracting trimmed return data from OHLCV and obtaining tickers and Checking for initial weights
# Checking for mean and weights and assigning optimization data accordingly
self.tickers, data = extract_trim(data)
self.mean = np.mean(data, axis=0) if custom_mean is None else custom_mean
if custom_cov is None:
# Handling invertibility using the small epsilon * identity matrix
Expand All @@ -678,7 +679,6 @@ def _prepare_optimization_inputs(
)
else:
self.covariance = custom_cov
self.tickers = extract_trim(data)[0]
self.weights = np.array(
np.ones(len(self.tickers)) / len(self.tickers) if w is None else w,
dtype=float,
Expand Down
Loading