Skip to content

Commit

Permalink
BT combined strategies example for pmorissette/bt#415
Browse files Browse the repository at this point in the history
  • Loading branch information
pirate-sys committed Jun 16, 2023
1 parent b0316f5 commit ee18846
Show file tree
Hide file tree
Showing 5 changed files with 993 additions and 1 deletion.
37 changes: 36 additions & 1 deletion README.md
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.
14 changes: 14 additions & 0 deletions environment.yml
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
725 changes: 725 additions & 0 deletions example.ipynb

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions src/interpreter.py
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()
99 changes: 99 additions & 0 deletions tests/test_interpreter.py
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)

0 comments on commit ee18846

Please sign in to comment.