-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BT combined strategies example for pmorissette/bt#415
- Loading branch information
1 parent
b0316f5
commit ee18846
Showing
5 changed files
with
993 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,36 @@ | ||
# bt-composite-strategies | ||
# BT composite strategies | ||
|
||
This repository shows the effect of the issue https://github.com/pmorissette/bt/issues/415. | ||
For more details see the `test_interpreter.py` unit test or use the Jupyter notebook `example.ipynb` | ||
|
||
|
||
# Simple buy and hold strategy: | ||
|
||
We create a simple buy and hold strategy with the QQQ. | ||
* Everything is fine. | ||
* We have transactions. | ||
* The backtest works fine. | ||
* The portfolio gets rebalanced every day. | ||
|
||
# First level composite strategy | ||
We create a combined strategy containing two buy and hold strategies of QQQ and SPY | ||
* The strategy does not have a return, but some other statistics. | ||
* It has sold everything on the 4th day and rebalancing is not propagated to the children. | ||
|
||
# Second level composite strategy | ||
We create a combined strategy containing to combined strategies and one asset. | ||
* The execution fails with | ||
```shell | ||
ZeroDivisionError: Could not update df876984-3be8-40c9-8847-e3b3c6af3cdc on 2023-05-17 00:00:00. Last value was 0.0 and net flows were 0. Currentvalue is 1000000.0. Therefore, we are dividing by zero to obtain the return for the period. | ||
``` | ||
|
||
# How to get this working? | ||
|
||
```shell | ||
git clone https://github.com/Pirat83/bt-composite-strategies.git | ||
cd bt-composite-strategies/ | ||
conda create --name bt-composite-strategies | ||
conda env update | ||
``` | ||
|
||
If you can contribute to the solution I would appreciate it very much. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
name: bt-composite-strategies | ||
channels: | ||
- conda-forge | ||
- defaults | ||
dependencies: | ||
- python>=3.10 | ||
- jupyter | ||
- pandas<2.0.0 | ||
- pip | ||
- pytest | ||
|
||
- pip: | ||
- git+https://github.com/pmorissette/bt.git | ||
- pytest-resource-path |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
from datetime import date | ||
from typing import Union | ||
|
||
import bt | ||
import pandas as pd | ||
from pandas import DataFrame, Series | ||
|
||
|
||
def flatten(i, result=None) -> list[bt.Backtest]: | ||
if result is None: | ||
result = [] | ||
if isinstance(i, bt.Backtest): | ||
result.append(i) | ||
elif isinstance(i, bt.backtest.Result): | ||
for b in i.backtest_list: | ||
flatten(b, result) | ||
elif isinstance(i, list): | ||
for b in i: | ||
flatten(b, result) | ||
elif isinstance(i, tuple): | ||
for b in i: | ||
flatten(b, result) | ||
else: | ||
raise NotImplementedError() | ||
|
||
return result | ||
|
||
|
||
class BTInterpreter: | ||
root: dict | ||
|
||
start: date | ||
end: date | ||
rebalance: bt.algos.RunPeriod | ||
|
||
def __init__(self, root: dict, start: date, end: date): | ||
self.root = root | ||
|
||
self.start = start | ||
self.end = end | ||
|
||
def traverse(self, node: dict = None) -> bt.backtest.Result: | ||
if node is None: | ||
node = self.root | ||
node_type: str = node.get('node-type') | ||
match node_type: | ||
case 'group': | ||
return self.parse_group(node) | ||
case 'group': | ||
return self.parse_group(node) | ||
case 'asset': | ||
return self.parse_asset(node) | ||
case _: | ||
raise NotImplementedError() | ||
|
||
def parse_group(self, node: dict) -> bt.backtest.Result: | ||
identifier: str = node.get('id') | ||
|
||
children: list = node.get('children') | ||
children: list[bt.backtest.Result] = [self.traverse(c) for c in children] | ||
|
||
prices: DataFrame = pd.DataFrame() | ||
for p in [c.prices for c in children]: | ||
prices = bt.merge(p) | ||
|
||
backtests: list[bt.Backtest] = flatten([c.backtest_list for c in children]) | ||
#for p in [b.data for b in backtests]: | ||
# prices = bt.merge(prices, p) | ||
|
||
strategies: list[bt.Strategy] = [b.strategy for b in backtests] | ||
#for p in [s.universe for s in strategies]: | ||
# prices = bt.merge(prices, p) | ||
|
||
strategy: bt.Strategy = self.build_strategy(identifier, strategies, debug=True) | ||
backtest: bt.Backtest = self.build_backtest(strategy, prices) | ||
result: bt.backtest.Result = bt.run(backtest) | ||
return result | ||
|
||
def parse_asset(self, node: dict) -> bt.backtest.Result: | ||
identifier: str = node.get('id') | ||
|
||
ticker: str = node.get('ticker') | ||
prices: DataFrame = bt.data.get(ticker, clean_tickers=False, start=self.start, end=self.end) | ||
strategy: bt.Strategy = self.build_strategy(identifier, [bt.Security(ticker)]) | ||
backtest: bt.Backtest = self.build_backtest(strategy, prices) | ||
result: bt.backtest.Result = bt.run(backtest) | ||
return result | ||
|
||
def build_strategy(self, name: str, | ||
children: Union[list[bt.algos.SecurityBase], list[bt.core.StrategyBase]], | ||
selection: bt.algos.Algo = bt.algos.SelectAll(), | ||
weight: bt.algos.Algo = bt.algos.WeighInvVol(), | ||
debug: bool = False | ||
) -> bt.Strategy: | ||
algos: [list[bt.algos.Algo]] = [ | ||
bt.algos.RunAfterDate(self.start), | ||
bt.algos.RunDaily(), | ||
selection, | ||
weight | ||
] | ||
if debug: | ||
algos.append(bt.algos.PrintInfo('\n{now}: {name} -> Value:{_value:0.0f}, Price:{_price:0.4f}')) | ||
algos.append(bt.algos.PrintTempData('Weights: \n{weights}')) | ||
|
||
algos.append(bt.algos.Rebalance()) | ||
result: bt.Strategy = bt.Strategy(name, algos, children) | ||
return result | ||
|
||
@staticmethod | ||
def build_backtest(strategy: bt.Strategy, prices: Union[Series, DataFrame]) -> bt.Backtest: | ||
return bt.Backtest(strategy, prices, integer_positions=False) | ||
|
||
|
||
def main(): | ||
print("Hello World!") | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import pprint | ||
from datetime import date, timedelta | ||
|
||
import bt.algos | ||
|
||
from interpreter import BTInterpreter | ||
|
||
|
||
def print_backtest_results(result: bt.backtest.Result): | ||
result.display() | ||
for k, v in result.items(): | ||
pprint.pp(result.get_transactions(k)) | ||
pprint.pp(result.get_weights(k)) | ||
pprint.pp(result.get_security_weights(k)) | ||
|
||
|
||
def test_traverse_asset(): | ||
node: dict = { | ||
'id': 'df876984-3be8-40c9-8847-e3b3c6af3cdc', | ||
'node-type': 'asset', | ||
'ticker': 'QQQ' | ||
} | ||
|
||
subject = BTInterpreter(node, date.today() - timedelta(weeks=4), date.today()) | ||
actual: bt.backtest.Result = subject.traverse() | ||
print_backtest_results(actual) | ||
|
||
|
||
def test_traverse_first_level_asset(): | ||
node: dict = { | ||
'id': '5fc986bf-d7c8-4582-bc27-f1ede76bdc29', | ||
'node-type': 'group', | ||
'children': [ | ||
{ | ||
'id': 'df876984-3be8-40c9-8847-e3b3c6af3cdc', | ||
'node-type': 'asset', | ||
'ticker': 'QQQ' | ||
}, | ||
{ | ||
'id': '742dc790-d0f7-472d-bd3e-405e411c0b2c', | ||
'node-type': 'asset', | ||
'ticker': 'SPY' | ||
} | ||
] | ||
} | ||
|
||
subject = BTInterpreter(node, date.today() - timedelta(weeks=4), date.today()) | ||
actual: bt.backtest.Result = subject.traverse() | ||
print_backtest_results(actual) | ||
|
||
|
||
def test_traverse_second_level_asset(): | ||
node: dict = { | ||
'id': 'c20d0968-2dfa-4ff7-8dfc-4c3d0df36dd4', | ||
'node-type': 'group', | ||
'children': [ | ||
{ | ||
'id': '5fc986bf-d7c8-4582-bc27-f1ede76bdc29 ', | ||
'node-type': 'group', | ||
'children': [ | ||
{ | ||
'id': 'df876984-3be8-40c9-8847-e3b3c6af3cdc', | ||
'node-type': 'asset', | ||
'ticker': 'QQQ' | ||
}, | ||
{ | ||
'id': '742dc790-d0f7-472d-bd3e-405e411c0b2c ', | ||
'node-type': 'asset', | ||
'ticker': 'SPY' | ||
} | ||
] | ||
}, | ||
{ | ||
'id': 'e5286ea7-9591-4b43-896e-cf34fb63a0e0', | ||
'node-type': 'group', | ||
'children': [ | ||
{ | ||
'id': '9e4f255b-343a-43ee-a433-f2366f8e9e62', | ||
'node-type': 'asset', | ||
'ticker': 'IYY' | ||
}, | ||
{ | ||
'id': '57033cdf-c185-4091-9d3e-3fc1e17913be ', | ||
'node-type': 'asset', | ||
'ticker': 'IWM' | ||
} | ||
] | ||
}, | ||
{ | ||
'id': '07306351-709d-41d8-b8dd-d8f6e6ae2900 ', | ||
'node-type': 'asset', | ||
'ticker': 'IVV' | ||
} | ||
] | ||
} | ||
|
||
subject = BTInterpreter(node, date.today() - timedelta(weeks=4), date.today()) | ||
actual: bt.backtest.Result = subject.traverse() | ||
print_backtest_results(actual) |