Skip to content

Commit 90ddbbf

Browse files
committed
Add run_batch method.
Add sharpe ration as internal calc. Allow weiner path sims to have different sigmas. Swap ql for pyfeng in notebooks. Add some features to README.
1 parent e23c181 commit 90ddbbf

File tree

9 files changed

+777
-679
lines changed

9 files changed

+777
-679
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
Python module for backtesting trading strategies.
44

5-
Support event driven backtesting, ie `on_open`, `on_close`, etc. Also supports multiple assets.
5+
Features
66

7-
Very basic statistics like book cash, mtm and total value. Currently, everything else needs to be deferred to a 3rd party module like `empyrical`.
7+
* Event driven, ie `on_open`, `on_close`, etc.
8+
* Multiple assets.
9+
* OHLC Asset. Extendable (e.g support additional fields, e.g. Volatility, or entirely different fields, e.g. Barrels per day).
10+
* Multiple books.
11+
* Positional and Basket orders. Extendible (e.g. can support stop loss).
12+
* Batch runs (for optimization).
13+
* Captures book history including transactions & daily cash, MtM and total values.
814

9-
There are some basic tests but use at your own peril. It's not production level code.
15+
The module provides basic statistics like book cash, mtm and total value. Currently, everything else needs to be deferred to a 3rd party module like `empyrical`.
1016

1117
## Core dependencies
1218

@@ -20,7 +26,7 @@ pip install yatbe
2026

2127
## Usage
2228

23-
Below is an example usage (the performance of the example strategy won't be good).
29+
Below is an example usage (the economic performance of the example strategy won't be good).
2430

2531
```python
2632
import pandas as pd

build.py renamed to build_mypyc.py

File renamed without changes.

poetry.lock

Lines changed: 702 additions & 666 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ readme = "README.md"
88
repository = "https://github.com/bsdz/yabte"
99

1010
[tool.poetry.build]
11-
script = "build.py"
11+
script = "build_mypyc.py"
1212
generate-setup-file = true
1313

1414
[tool.poetry.dependencies]
1515
python = "^3.10,<3.13"
16-
pandas = ">1.5,<3"
16+
pandas = "^2.2.1"
1717
scipy = "^1.10.0"
1818
pandas-stubs = "^2.1.4.231227"
1919
mypy = "^1.8.0"
@@ -48,6 +48,7 @@ plotly = "^5.10.0"
4848
ipykernel = "^6.20.2"
4949
pyfeng = "^0.2.5"
5050
nbconvert = "^7.2.9"
51+
quantlib = "^1.34"
5152

5253
[tool.isort]
5354
profile = "black"

yabte/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
_author__ = "Blair Azzopardi"
1+
from importlib.metadata import version
2+
3+
__author__ = "Blair Azzopardi"
4+
__version__ = version(__package__)

yabte/backtest/strategyrunner.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import concurrent.futures
12
import logging
3+
from concurrent.futures import ProcessPoolExecutor
24
from copy import deepcopy
35
from dataclasses import dataclass, field
4-
from typing import Any, Dict, List, Optional
6+
from typing import Any, Dict, Iterable, List, Optional
57

68
import pandas as pd
79

@@ -219,3 +221,14 @@ def run(self, params: Dict[str, Any] = None) -> StrategyRunnerResult:
219221
book.eod_tasks(ts, day_data, asset_map)
220222

221223
return srr
224+
225+
def run_batch(
226+
self,
227+
params_iterable: Iterable[Dict[str, Any]],
228+
executor: ProcessPoolExecutor | None = None,
229+
) -> List[StrategyRunnerResult]:
230+
"""Run a set of parameter combinations."""
231+
232+
executor = executor or concurrent.futures.ThreadPoolExecutor()
233+
with executor:
234+
return list(executor.map(self.run, params_iterable))

yabte/tests/test_strategy_runner.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66
import pandas as pd
77

8+
import yabte.utilities.pandas_extension
89
from yabte.backtest import (
910
BasketOrder,
1011
Book,
@@ -571,6 +572,34 @@ def on_close(self):
571572
[(o.status, o.key, o.size) for o in srr.orders_unprocessed],
572573
)
573574

575+
def test_run_batch(self):
576+
577+
book = Book(name="Main", cash=Decimal("100000"))
578+
579+
sr = StrategyRunner(
580+
data=self.df_combined,
581+
assets=self.assets,
582+
strategies=[TestSMAXOStrat()],
583+
books=[book],
584+
)
585+
586+
param_iter = [
587+
{"days_long": n, "days_short": m}
588+
for n, m in zip([20, 30, 40, 50], [5, 10, 15, 20])
589+
if n > m
590+
]
591+
592+
srrs = sr.run_batch(param_iter)
593+
594+
self.assertEqual(len(srrs), len(param_iter))
595+
596+
# check we have distinct sharpe ratios for each param set
597+
sharpes = {
598+
srr.book_history.loc[:, ("Main", "total")].prc.sharpe_ratio()
599+
for srr in srrs
600+
}
601+
self.assertEqual(len(sharpes), len(param_iter))
602+
574603

575604
if __name__ == "__main__":
576605
unittest.main()

yabte/utilities/pandas_extension.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,23 @@ def standard(self):
1313

1414

1515
@pd.api.extensions.register_dataframe_accessor("prc")
16+
@pd.api.extensions.register_series_accessor("prc")
1617
class PriceAccessor:
1718
# TODO: add ledoit cov (via sklearn)
1819
# http://www.ledoit.net/honey.pdf
19-
# TODO: add Sharpe ratio
2020

2121
def __init__(self, pandas_obj):
2222
self._validate(pandas_obj)
2323
self._obj = pandas_obj
2424

2525
@staticmethod
2626
def _validate(obj):
27-
if (obj < 0).any(axis=None):
28-
raise AttributeError("Prices must be non-negative")
27+
pass
2928

3029
@property
3130
def log_returns(self):
31+
if (self._obj < 0).any(axis=None):
32+
raise AttributeError("Prices must be non-negative for log returns")
3233
return np.log((self._obj / self._obj.shift())[1:])
3334

3435
@property
@@ -42,6 +43,8 @@ def frequency(self):
4243
return 252
4344
elif days == 7:
4445
return 52
46+
elif 28 <= days <= 31:
47+
return 12
4548

4649
def capm_returns(self, risk_free_rate=0):
4750
returns = self.returns
@@ -56,6 +59,11 @@ def capm_returns(self, risk_free_rate=0):
5659
+ betas * (returns_mkt.mean() * self.frequency - risk_free_rate)
5760
).rename("CAPM")
5861

62+
def sharpe_ratio(self, risk_free_rate=0, use_log_returns=True):
63+
ann_factor = np.sqrt(self.frequency)
64+
returns = self.log_returns if use_log_returns else self.returns
65+
return ann_factor * (returns.mean() - risk_free_rate) / returns.std()
66+
5967
def null_blips(self, sd=5, sdd=7):
6068
df = self._obj
6169
z = df.scl.standard

yabte/utilities/simulation/weiner.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@
1414
def weiner_simulate_paths(
1515
n_steps: int,
1616
n_sims: int = 1,
17-
stdev: float = 1,
17+
stdev: float | np.ndarray = 1,
1818
R: np.ndarray = np.array([[1]]),
1919
rng=None,
2020
):
2121
"""Generate simulated Weiner paths.
2222
2323
`stdev` is the increment size, `R` a correlation matrix, `n_steps`
2424
is how many time steps, `n_sims` the number of simulations and `rng`
25-
a numpy random number generator (optional).
25+
a numpy random number generator (optional). If `stdev` is a scalar
26+
it will be broadcasted to the size of `n_sims`.
2627
"""
2728

2829
R = np.atleast_2d(R)
30+
stdev = np.resize(stdev, n_sims).reshape(n_sims, 1)
2931

3032
if rng is None:
3133
rng = np.random.default_rng()

0 commit comments

Comments
 (0)