Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Misc 20240506 #5

Merged
merged 5 commits into from
May 11, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
run: poetry install --with dev
- if: ${{ matrix.use-mypyc }}
name: Build mypyc modules
run: poetry run python build.py --inplace
run: poetry run python build_mypyc.py --inplace
- name: Run tests with coverage
run: poetry run coverage run -m unittest
- name: Show coverage report
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

Python module for backtesting trading strategies.

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

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

There are some basic tests but use at your own peril. It's not production level code.
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`.

## Core dependencies

Expand All @@ -20,7 +26,7 @@ pip install yatbe

## Usage

Below is an example usage (the performance of the example strategy won't be good).
Below is an example usage (the economic performance of the example strategy won't be good).

```python
import pandas as pd
Expand Down
File renamed without changes.
1,432 changes: 736 additions & 696 deletions notebooks/Delta_Hedging.ipynb

Large diffs are not rendered by default.

1,653 changes: 835 additions & 818 deletions poetry.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ readme = "README.md"
repository = "https://github.com/bsdz/yabte"

[tool.poetry.build]
script = "build.py"
script = "build_mypyc.py"
generate-setup-file = true

[tool.poetry.dependencies]
python = "^3.10,<3.13"
pandas = ">1.5,<3"
pandas = "^2.2.1"
scipy = "^1.10.0"
pandas-stubs = "^2.1.4.231227"
mypy = "^1.8.0"
Expand Down Expand Up @@ -46,11 +46,12 @@ optional = true
matplotlib = "^3.6.2"
plotly = "^5.10.0"
ipykernel = "^6.20.2"
pyfeng = "^0.2.5"
nbconvert = "^7.2.9"
quantlib = "^1.34"

[tool.isort]
profile = "black"
skip_glob = [".venv*/*"]

[[tool.mypy.overrides]]
module = "plotly.*,scipy.*,matplotlib.*"
Expand Down
5 changes: 4 additions & 1 deletion yabte/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
_author__ = "Blair Azzopardi"
from importlib.metadata import version

__author__ = "Blair Azzopardi"
__version__ = version(__package__)
15 changes: 14 additions & 1 deletion yabte/backtest/strategyrunner.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import concurrent.futures
import logging
from concurrent.futures import ProcessPoolExecutor
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from typing import Any, Dict, Iterable, List, Optional

import pandas as pd

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

return srr

def run_batch(
self,
params_iterable: Iterable[Dict[str, Any]],
executor: ProcessPoolExecutor | None = None,
) -> List[StrategyRunnerResult]:
"""Run a set of parameter combinations."""

executor = executor or concurrent.futures.ThreadPoolExecutor()
with executor:
return list(executor.map(self.run, params_iterable))
28 changes: 28 additions & 0 deletions yabte/tests/test_strategy_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import numpy as np
import pandas as pd

import yabte.utilities.pandas_extension
from yabte.backtest import (
BasketOrder,
Book,
Expand Down Expand Up @@ -571,6 +572,33 @@ def on_close(self):
[(o.status, o.key, o.size) for o in srr.orders_unprocessed],
)

def test_run_batch(self):
book = Book(name="Main", cash=Decimal("100000"))

sr = StrategyRunner(
data=self.df_combined,
assets=self.assets,
strategies=[TestSMAXOStrat()],
books=[book],
)

param_iter = [
{"days_long": n, "days_short": m}
for n, m in zip([20, 30, 40, 50], [5, 10, 15, 20])
if n > m
]

srrs = sr.run_batch(param_iter)

self.assertEqual(len(srrs), len(param_iter))

# check we have distinct sharpe ratios for each param set
sharpes = {
srr.book_history.loc[:, ("Main", "total")].prc.sharpe_ratio()
for srr in srrs
}
self.assertEqual(len(sharpes), len(param_iter))


if __name__ == "__main__":
unittest.main()
14 changes: 11 additions & 3 deletions yabte/utilities/pandas_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,23 @@ def standard(self):


@pd.api.extensions.register_dataframe_accessor("prc")
@pd.api.extensions.register_series_accessor("prc")
class PriceAccessor:
# TODO: add ledoit cov (via sklearn)
# http://www.ledoit.net/honey.pdf
# TODO: add Sharpe ratio

def __init__(self, pandas_obj):
self._validate(pandas_obj)
self._obj = pandas_obj

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

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

@property
Expand All @@ -42,6 +43,8 @@ def frequency(self):
return 252
elif days == 7:
return 52
elif 28 <= days <= 31:
return 12

def capm_returns(self, risk_free_rate=0):
returns = self.returns
Expand All @@ -56,6 +59,11 @@ def capm_returns(self, risk_free_rate=0):
+ betas * (returns_mkt.mean() * self.frequency - risk_free_rate)
).rename("CAPM")

def sharpe_ratio(self, risk_free_rate=0, use_log_returns=True):
ann_factor = np.sqrt(self.frequency)
returns = self.log_returns if use_log_returns else self.returns
return ann_factor * (returns.mean() - risk_free_rate) / returns.std()

def null_blips(self, sd=5, sdd=7):
df = self._obj
z = df.scl.standard
Expand Down
6 changes: 4 additions & 2 deletions yabte/utilities/simulation/weiner.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@
def weiner_simulate_paths(
n_steps: int,
n_sims: int = 1,
stdev: float = 1,
stdev: float | np.ndarray = 1,
R: np.ndarray = np.array([[1]]),
rng=None,
):
"""Generate simulated Weiner paths.

`stdev` is the increment size, `R` a correlation matrix, `n_steps`
is how many time steps, `n_sims` the number of simulations and `rng`
a numpy random number generator (optional).
a numpy random number generator (optional). If `stdev` is a scalar
it will be broadcasted to the size of `n_sims`.
"""

R = np.atleast_2d(R)
stdev = np.resize(stdev, n_sims).reshape(n_sims, 1)

if rng is None:
rng = np.random.default_rng()
Expand Down
Loading