diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index cf95d92c..00000000 --- a/.coveragerc +++ /dev/null @@ -1,15 +0,0 @@ -[run] -branch = True - -[report] -exclude_lines = - if __name__ == .__main__.: - if self.debug: - if self.progress_bar: - pragma: no cover - raise NotImplementedError - -ignore_errors = True -omit = - bt/tests/* - bt/docs/* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b4abfe5..2426aad8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,8 +18,8 @@ jobs: strategy: matrix: - python-version: [3.9] os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [3.8, 3.9, '3.10', 3.11] steps: - uses: actions/checkout@v4 @@ -36,17 +36,13 @@ jobs: python -m pip install -U wheel twine setuptools - name: Lint - run: | - make lint + run: make lint - name: Test - run: | - make test + run: make test - name: Coverage uses: codecov/codecov-action@v3 - name: Package and check - run: | - python setup.py sdist bdist_wheel - python -m twine check dist/* + run: make dist diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1de171df..5ec99659 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,6 +1,6 @@ name: Deploy -on: +on: push: tags: - 'v*' @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8, 3.9, '3.10', 3.11] steps: - uses: actions/checkout@v4 @@ -28,24 +28,17 @@ jobs: python -m pip install -U wheel twine setuptools - name: Lint - run: | - make lint + run: make lint - name: Test - run: | - make test + run: make test - name: Package and check - run: | - python setup.py sdist bdist_wheel - python -m twine check dist/* + run: make dist - name: Publish distribution 📦 to PyPI if: ${{ startsWith(github.ref, 'refs/tags') && matrix.os != 'ubuntu-latest' }} env: TWINE_USERNAME: ${{ secrets.PYPI_UN }} TWINE_PASSWORD: ${{ secrets.PYPI_PW }} - run: | - python -m twine upload dist/* --skip-existing || echo "Failed to deploy" - - + run: make upload diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/MANIFEST.in b/MANIFEST.in index 494e4197..22581b84 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,15 +1,16 @@ -graft bt +graft ffn +include LICENSE +include README.md +include pyproject.toml -include LICENSE.txt -include README.rst -include setup.py +prune docs +prune tests -graft docs -prune docs/build - -global-exclude *.so -global-exclude *.pyd +# Patterns to exclude from any directory +global-exclude *~ global-exclude *.pyc -global-exclude .git* -global-exclude .DS_Store +global-exclude *.pyo +global-exclude .git +global-exclude .ipynb_checkpoints global-exclude .mypy_cache +global-exclude .DS_Store diff --git a/Makefile b/Makefile index 0156b9c9..fd519f9f 100644 --- a/Makefile +++ b/Makefile @@ -11,22 +11,23 @@ test: python -m pytest -vvv tests --cov=bt --junitxml=python_junit.xml --cov-report=xml --cov-branch --cov-report term lint: - python -m flake8 bt setup.py docs/source/conf.py + python -m ruff bt setup.py docs/source/conf.py fix: - python -m black bt setup.py docs/source/conf.py + python -m ruff format bt setup.py docs/source/conf.py dist: - python setup.py sdist + python setup.py sdist bdist_wheel + python -m twine check dist/* upload: clean dist - twine upload dist/* + python -m twine upload dist/* --skip-existing -docs: +docs: $(MAKE) -C docs/ clean $(MAKE) -C docs/ html -pages: +pages: rm -rf $(TMPREPO) git clone -b gh-pages git@github.com:pmorissette/bt.git $(TMPREPO) rm -rf $(TMPREPO)/* diff --git a/README.rst b/README.md similarity index 54% rename from README.rst rename to README.md index 0e81e739..e97bbebe 100644 --- a/README.rst +++ b/README.md @@ -1,75 +1,59 @@ -.. image:: http://pmorissette.github.io/bt/_static/logo.png +![](http://pmorissette.github.io/bt/_static/logo.png) -.. image:: https://github.com/pmorissette/bt/workflows/Build%20Status/badge.svg - :target: https://github.com/pmorissette/bt/actions/ +[![Build Status](https://github.com/pmorissette/bt/workflows/Build%20Status/badge.svg)](https://github.com/pmorissette/bt/actions/) +[![Codecov](https://codecov.io/gh/pmorissette/bt/branch/master/graph/badge.svg)](https://codecov.io/pmorissette/bt) +[![PyPI Version](https://img.shields.io/pypi/v/bt)](https://pypi.org/project/bt/) +[![PyPI License](https://img.shields.io/pypi/l/bt)](https://pypi.org/project/bt/) -.. image:: https://codecov.io/gh/pmorissette/bt/branch/master/graph/badge.svg - :target: https://codecov.io/pmorissette/bt - -.. image:: https://img.shields.io/pypi/v/bt - :alt: PyPI - :target: https://pypi.org/project/bt/ - -.. image:: https://img.shields.io/pypi/l/bt - :alt: PyPI - License - :target: https://pypi.org/project/bt/ - -bt - Flexible Backtesting for Python -==================================== +# bt - Flexible Backtesting for Python bt is currently in alpha stage - if you find a bug, please submit an issue. Read the docs here: http://pmorissette.github.io/bt. -What is bt? ------------ +## What is bt? **bt** is a flexible backtesting framework for Python used to test quantitative -trading strategies. **Backtesting** is the process of testing a strategy over a given -data set. This framework allows you to easily create strategies that mix and match -different `Algos `_. It aims to foster the creation of easily testable, re-usable and -flexible blocks of strategy logic to facilitate the rapid development of complex -trading strategies. +trading strategies. **Backtesting** is the process of testing a strategy over a given +data set. This framework allows you to easily create strategies that mix and match +different [Algos](http://pmorissette.github.io/bt/bt.html#bt.core.Algo). It aims to foster the creation of easily testable, re-usable and +flexible blocks of strategy logic to facilitate the rapid development of complex +trading strategies. -The goal: to save **quants** from re-inventing the wheel and let them focus on the +The goal: to save **quants** from re-inventing the wheel and let them focus on the important part of the job - strategy development. -**bt** is coded in **Python** and joins a vibrant and rich ecosystem for data analysis. +**bt** is coded in **Python** and joins a vibrant and rich ecosystem for data analysis. Numerous libraries exist for machine learning, signal processing and statistics and can be leveraged to avoid re-inventing the wheel - something that happens all too often when using other languages that don't have the same wealth of high-quality, open-source projects. -bt is built atop `ffn `_ - a financial function library for Python. Check it out! +bt is built atop [ffn](https://github.com/pmorissette/ffn) - a financial function library for Python. Check it out! -Features ---------- +## Features * **Tree Structure** - `The tree structure `_ facilitates the construction and composition of complex algorithmic trading - strategies that are modular and re-usable. Furthermore, each tree `Node - `_ - has its own price index that can be - used by Algos to determine a Node's allocation. + [The tree structure](http://pmorissette.github.io/bt/tree.html) facilitates the construction and composition of complex algorithmic trading + strategies that are modular and re-usable. Furthermore, each tree [Node](http://pmorissette.github.io/bt/bt.html#bt.core.Node) has its own + price index that can be used by Algos to determine a Node's allocation. * **Algorithm Stacks** - `Algos `_ and `AlgoStacks `_ are + [Algos](http://pmorissette.github.io/bt/bt.html#bt.core.Algo) and [AlgoStacks](http://pmorissette.github.io/bt/bt.html#bt.core.AlgoStack) are another core feature that facilitate the creation of modular and re-usable strategy logic. Due to their modularity, these logic blocks are also easier to test - an important step in building robust financial solutions. * **Charting and Reporting** bt also provides many useful charting functions that help visualize backtest - results. We also plan to add more charts, tables and report formats in the future, + results. We also plan to add more charts, tables and report formats in the future, such as automatically generated PDF reports. * **Detailed Statistics** Furthermore, bt calculates a bunch of stats relating to a backtest and offers a quick way to compare - these various statistics across many different backtests via `Results' - `_ display methods. + these various statistics across many different backtests via [Results](http://pmorissette.github.io/bt/bt.html#bt.backtest.Result) display methods. -Roadmap --------- +## Roadmap Future development efforts will focus on: @@ -87,30 +71,25 @@ Future development efforts will focus on: as reporting is an important aspect of the job. Charting and reporting also facilitate finding bugs in strategy logic. -Installing bt -------------- - -The easiest way to install ``bt`` is from the `Python Package Index `_ -using ``pip`` or ``easy_install``: +## Installing bt -.. code-block:: bash +The easiest way to install `bt` is from the [Python Package Index](https://pypi.python.org/pypi/bt/) +using `pip`: - $ pip install bt +```bash +pip install bt +``` -Since bt has many dependencies, we strongly recommend installing the `Anaconda Scientific Python -Distribution `_, especially on Windows. This distribution -comes with many of the required packages pre-installed, including pip. Once Anaconda is installed, the above -command should complete the installation. -bt should be compatible with Python 2.7 and Python 3 thanks to the contributions -made by fellow users. +Since bt has many dependencies, we strongly recommend installing the [Anaconda Scientific Python +Distribution](https://store.continuum.io/cshop/anaconda/), especially on Windows. This distribution +comes with many of the required packages pre-installed, including pip. Once Anaconda is installed, the above +command should complete the installation. -Recommended Setup ------------------ +## Recommended Setup -We believe the best environment to develop with bt is the `IPython Notebook -`__. From their homepage, the IPython Notebook -is: +We believe the best environment to develop with bt is the [IPython Notebook](http://ipython.org/notebook.html). +From their homepage, the IPython Notebook is: "[...] a web-based interactive computational environment where you can combine code execution, text, mathematics, plots and rich diff --git a/bt/__init__.py b/bt/__init__.py index c30ec3ce..3d3e5933 100644 --- a/bt/__init__.py +++ b/bt/__init__.py @@ -10,4 +10,4 @@ import ffn from ffn import utils, data, get, merge -__version__ = (0, 2, 10) +__version__ = "1.0.0" diff --git a/bt/algos.py b/bt/algos.py index 51b04d8d..9ec92c94 100644 --- a/bt/algos.py +++ b/bt/algos.py @@ -137,9 +137,7 @@ def __call__(self, target): class RunPeriod(Algo): - def __init__( - self, run_on_first_date=True, run_on_end_of_period=False, run_on_last_date=False - ): + def __init__(self, run_on_first_date=True, run_on_end_of_period=False, run_on_last_date=False): super(RunPeriod, self).__init__() self._run_on_first_date = run_on_first_date self._run_on_end_of_period = run_on_end_of_period @@ -427,9 +425,7 @@ def __call__(self, target): return True if "cash" in target.temp: - cash_deviation = abs( - (target.capital - targets.value) / targets.value - target.temp["cash"] - ) + cash_deviation = abs((target.capital - targets.value) / targets.value - target.temp["cash"]) if cash_deviation > self.tolerance: return True @@ -641,9 +637,7 @@ class SelectN(Algo): """ - def __init__( - self, n, sort_descending=True, all_or_none=False, filter_selected=False - ): + def __init__(self, n, sort_descending=True, all_or_none=False, filter_selected=False): super(SelectN, self).__init__() if n < 0: raise ValueError("n cannot be negative") @@ -906,9 +900,7 @@ def __call__(self, target): resolved = list(universe.index) else: resolved = list(universe[universe > 0].index) - target.temp["selected"] = resolved + [ - s for s in selected if s not in on_the_run.columns - ] + target.temp["selected"] = resolved + [s for s in selected if s not in on_the_run.columns] return True @@ -1057,9 +1049,7 @@ def __init__(self, scale): self.scale = scale def __call__(self, target): - target.temp["weights"] = { - k: self.scale * w for k, w in target.temp["weights"].items() - } + target.temp["weights"] = {k: self.scale * w for k, w in target.temp["weights"].items()} return True @@ -1525,25 +1515,16 @@ def __call__(self, target): else: raise NotImplementedError("covar_method not implemented") - weights = pd.Series( - [current_weights[x] for x in covar.columns], index=covar.columns - ) + weights = pd.Series([current_weights[x] for x in covar.columns], index=covar.columns) - vol = np.sqrt( - np.matmul(weights.values.T, np.matmul(covar.values, weights.values)) - * self.annualization_factor - ) + vol = np.sqrt(np.matmul(weights.values.T, np.matmul(covar.values, weights.values)) * self.annualization_factor) if isinstance(self.target_volatility, (float, int)): - self.target_volatility = { - k: self.target_volatility for k in target.temp["weights"].keys() - } + self.target_volatility = {k: self.target_volatility for k in target.temp["weights"].keys()} for k in target.temp["weights"].keys(): if k in self.target_volatility.keys(): - target.temp["weights"][k] = ( - target.temp["weights"][k] * self.target_volatility[k] / vol - ) + target.temp["weights"][k] = target.temp["weights"][k] * self.target_volatility[k] / vol return True @@ -1622,10 +1603,7 @@ def __call__(self, target): else: raise NotImplementedError("covar_method not implemented") - PTE_vol = np.sqrt( - np.matmul(weights.values.T, np.matmul(covar.values, weights.values)) - * self.annualization_factor - ) + PTE_vol = np.sqrt(np.matmul(weights.values.T, np.matmul(covar.values, weights.values)) * self.annualization_factor) if pd.isnull(PTE_vol): return False @@ -1863,9 +1841,7 @@ def __call__(self, target): # scale delta relative to # of periods left and set that as the new # target for cname in self._weights.keys(): - curr = ( - target.children[cname].weight if cname in target.children else 0.0 - ) + curr = target.children[cname].weight if cname in target.children else 0.0 dlt = (self._weights[cname] - curr) / self._days_left tgt[cname] = curr + dlt @@ -1998,12 +1974,7 @@ def __init__(self, include_types=(bt.core.Node,), exclude_types=()): self.exclude_types = exclude_types or (type(None),) def __call__(self, target): - selected = [ - sec_name - for sec_name, sec in target.children.items() - if isinstance(sec, self.include_types) - and not isinstance(sec, self.exclude_types) - ] + selected = [sec_name for sec_name, sec in target.children.items() if isinstance(sec, self.include_types) and not isinstance(sec, self.exclude_types)] if "selected" in target.temp: selected = [s for s in selected if s in target.temp["selected"]] target.temp["selected"] = selected @@ -2046,11 +2017,7 @@ def __call__(self, target): close_dates = target.get_data(self.close_dates)["date"] # Find securities that are candidate for closing sec_names = [ - sec_name - for sec_name, sec in target.children.items() - if isinstance(sec, SecurityBase) - and sec_name in close_dates.index - and sec_name not in target.perm["closed"] + sec_name for sec_name, sec in target.children.items() if isinstance(sec, SecurityBase) and sec_name in close_dates.index and sec_name not in target.perm["closed"] ] # Check whether closed @@ -2100,11 +2067,7 @@ def __call__(self, target): transactions = {} # Find securities that are candidate for roll sec_names = [ - sec_name - for sec_name, sec in target.children.items() - if isinstance(sec, SecurityBase) - and sec_name in roll_data.index - and sec_name not in target.perm["rolled"] + sec_name for sec_name, sec in target.children.items() if isinstance(sec, SecurityBase) and sec_name in roll_data.index and sec_name not in target.perm["rolled"] ] # Calculate new transaction and close old position @@ -2195,9 +2158,7 @@ def __call__(self, target): transactions = all_transactions[(timestamps > start) & (timestamps <= end)] for (_, security), transaction in transactions.iterrows(): c = target[security] - c.transact( - transaction["quantity"], price=transaction["price"], update=False - ) + c.transact(transaction["quantity"], price=transaction["price"], update=False) # Now update target.root.update(target.now) @@ -2245,9 +2206,7 @@ def __call__(self, target): for (_, security), transaction in transactions.iterrows(): c = target[security] - c.transact( - transaction["quantity"], price=transaction["price"], update=False - ) + c.transact(transaction["quantity"], price=transaction["price"], update=False) # Now update target.root.update(target.now) @@ -2407,15 +2366,11 @@ def __call__(self, target): securities = target.temp["selected"] # Get target risk - target_risk = np.array( - [self._get_target_risk(target, m) for m in self.measures] - ) + target_risk = np.array([self._get_target_risk(target, m) for m in self.measures]) if self.strategy is not None: # Add the target risk of the strategy to the risk of the target # (which contains existing hedges) - target_risk += np.array( - [self._get_target_risk(self.strategy, m) for m in self.measures] - ) + target_risk += np.array([self._get_target_risk(self.strategy, m) for m in self.measures]) # Turn target_risk into a column array target_risk = target_risk.reshape(len(self.measures), 1) @@ -2424,16 +2379,11 @@ def __call__(self, target): for m in self.measures: d = target.get_data("unit_risk").get(m) if d is None: - raise ValueError( - "unit_risk for %s not present in temp on %s" - % (self.measure, target.name) - ) + raise ValueError("unit_risk for %s not present in temp on %s" % (self.measure, target.name)) i = d.index.get_loc(target.now) data.append((i, d)) - hedge_risk = np.array( - [[_get_unit_risk(s, d, i) for (i, d) in data] for s in securities] - ) + hedge_risk = np.array([[_get_unit_risk(s, d, i) for (i, d) in data] for s in securities]) # Get hedge ratios if self.pseudo: diff --git a/bt/backtest.py b/bt/backtest.py index e9e919cd..2a768416 100644 --- a/bt/backtest.py +++ b/bt/backtest.py @@ -150,10 +150,7 @@ def __init__( ): if data.columns.duplicated().any(): cols = data.columns[data.columns.duplicated().tolist()].tolist() - raise Exception( - "data provided has some duplicate column names: \n%s \n" - "Please remove duplicates!" % cols - ) + raise Exception("data provided has some duplicate column names: \n%s \n" "Please remove duplicates!" % cols) # we want to reuse strategy logic - copy it! # basically strategy is a template @@ -212,9 +209,7 @@ def _process_data(self, data, additional_data): new = pd.concat([empty_row, old]) self.additional_data[k] = new elif isinstance(old, pd.Series) and old.index.equals(data.index): - empty_row = pd.Series( - np.nan, index=[old.index[0] - pd.DateOffset(days=1)] - ) + empty_row = pd.Series(np.nan, index=[old.index[0] - pd.DateOffset(days=1)]) new = pd.concat([empty_row, old]) self.additional_data[k] = new @@ -272,14 +267,10 @@ def weights(self): return self._weights else: if self.strategy.fixed_income: - vals = pd.DataFrame( - {x.full_name: x.notional_values for x in self.strategy.members} - ) + vals = pd.DataFrame({x.full_name: x.notional_values for x in self.strategy.members}) vals = vals.div(self.strategy.notional_values, axis=0) else: - vals = pd.DataFrame( - {x.full_name: x.values for x in self.strategy.members} - ) + vals = pd.DataFrame({x.full_name: x.values for x in self.strategy.members}) vals = vals.div(self.strategy.values, axis=0) self._weights = vals return vals @@ -538,9 +529,7 @@ def __init__(self, *backtests): self.r_stats = self.stats.drop(self.base_name, axis=1) self.b_stats = self.stats[self.base_name] - def plot_histogram( - self, statistic="monthly_sharpe", figsize=(15, 5), title=None, bins=20, **kwargs - ): + def plot_histogram(self, statistic="monthly_sharpe", figsize=(15, 5), title=None, bins=20, **kwargs): """ Plots the distribution of a given statistic. The histogram represents the distribution of the random strategies' statistic @@ -560,9 +549,7 @@ def plot_histogram( """ if statistic not in self.r_stats.index: - raise ValueError( - "Invalid statistic. Valid statistics" "are the statistics in self.stats" - ) + raise ValueError("Invalid statistic. Valid statistics" "are the statistics in self.stats") if title is None: title = "%s histogram" % statistic @@ -601,17 +588,10 @@ class RenormalizedFixedIncomeResult(Result): def __init__(self, normalizing_value, *backtests): for backtest in backtests: if not backtest.strategy.fixed_income: - raise ValueError( - "Cannot apply RenormalizedFixedIncomeResult " - "because backtest %s is not on a fixed income " - "strategy" % backtest.name - ) + raise ValueError("Cannot apply RenormalizedFixedIncomeResult " "because backtest %s is not on a fixed income " "strategy" % backtest.name) if not isinstance(normalizing_value, dict): normalizing_value = {x.name: normalizing_value for x in backtests} - tmp = [ - pd.DataFrame({x.name: self._price(x.strategy, normalizing_value[x.name])}) - for x in backtests - ] + tmp = [pd.DataFrame({x.name: self._price(x.strategy, normalizing_value[x.name])}) for x in backtests] super(Result, self).__init__(*tmp) self.backtest_list = backtests self.backtests = {x.name: x for x in backtests} diff --git a/bt/core.py b/bt/core.py index e750053e..a995f73e 100644 --- a/bt/core.py +++ b/bt/core.py @@ -84,9 +84,7 @@ def __init__(self, name, parent=None, children=None): self._lazy_children = {} self._universe_tickers = [] self._childrenv = [] # Shortcut to self.children.values() - self._original_children_are_present = (children is not None) and ( - len(children) >= 1 - ) + self._original_children_are_present = (children is not None) and (len(children) >= 1) # strategy children helpers self._has_strat_children = False @@ -314,9 +312,7 @@ def to_dot(self, root=True): Represent the node structure in DOT format. """ name = lambda x: x.name or repr(self) # noqa: E731 - edges = "\n".join( - '\t"%s" -> "%s"' % (name(self), name(c)) for c in self.children.values() - ) + edges = "\n".join('\t"%s" -> "%s"' % (name(self), name(c)) for c in self.children.values()) below = "\n".join(c.to_dot(False) for c in self.children.values()) body = "\n".join([edges, below]).rstrip() if root: @@ -483,10 +479,7 @@ def bidoffer_paid(self): self.root.update(self.now, None) return self._bidoffer_paid else: - raise Exception( - "Bid/offer accounting not turned on: " - '"bidoffer" argument not provided during setup' - ) + raise Exception("Bid/offer accounting not turned on: " '"bidoffer" argument not provided during setup') @property def bidoffers_paid(self): @@ -498,10 +491,7 @@ def bidoffers_paid(self): self.root.update(self.now, None) return self._bidoffers_paid.loc[: self.now] else: - raise Exception( - "Bid/offer accounting not turned on: " - '"bidoffer" argument not provided during setup' - ) + raise Exception("Bid/offer accounting not turned on: " '"bidoffer" argument not provided during setup') @property def universe(self): @@ -574,11 +564,7 @@ def setup(self, universe, **kwargs): # strategies as the "price" is just a reference # value and should not be used for capital allocation if self.fixed_income and not self.parent.fixed_income: - raise ValueError( - "Cannot have fixed income " - "strategy child (%s) of non-" - "fixed income strategy (%s)" % (self.name, self.parent.name) - ) + raise ValueError("Cannot have fixed income " "strategy child (%s) of non-" "fixed income strategy (%s)" % (self.name, self.parent.name)) # determine if needs paper trading # and setup if so @@ -604,9 +590,7 @@ def setup(self, universe, **kwargs): if self._original_children_are_present: # if we have universe_tickers defined, limit universe to # those tickers - valid_filter = list( - set(universe.columns).intersection(self._universe_tickers) - ) + valid_filter = list(set(universe.columns).intersection(self._universe_tickers)) funiverse = universe[valid_filter].copy() @@ -741,12 +725,7 @@ def update(self, date, data=None, inow=None): val += coupons if self.root == self: - if ( - (val < 0) - and not self.bankrupt - and not self.fixed_income - and not is_zero(val) - ): + if (val < 0) and not self.bankrupt and not self.fixed_income and not is_zero(val): # Declare a bankruptcy self.bankrupt = True self.flatten() @@ -754,11 +733,7 @@ def update(self, date, data=None, inow=None): # update data if this value is different or # if now has changed - avoid all this if not since it # won't change - if ( - newpt - or not is_zero(self._value - val) - or not is_zero(self._notl_value - notl_val) - ): + if newpt or not is_zero(self._value - val) or not is_zero(self._notl_value - notl_val): self._value = val self._values.values[inow] = val @@ -785,8 +760,7 @@ def update(self, date, data=None, inow=None): "Could not update %s on %s. Last notional value " "was %s and pnl was %s. Therefore, " "we are dividing by zero to obtain the pnl " - "per unit notional for the period." - % (self.name, self.now, self._last_notl_value, pnl) + "per unit notional for the period." % (self.name, self.now, self._last_notl_value, pnl) ) self._price = self._last_price + ret @@ -1064,17 +1038,9 @@ def flatten(self): """ # go right to base alloc if self.fixed_income: - [ - c.transact(-c.position, update=False) - for c in self._childrenv - if c.position != 0 - ] + [c.transact(-c.position, update=False) for c in self._childrenv if c.position != 0] else: - [ - c.allocate(-c.value, update=False) - for c in self._childrenv - if c.value != 0 - ] + [c.allocate(-c.value, update=False) for c in self._childrenv if c.value != 0] self.root.stale = True @@ -1129,14 +1095,10 @@ def get_transactions(self): # Adjust prices for bid/offer paid if needed if self._bidoffer_set: - bidoffer = pd.DataFrame( - {x.name: x.bidoffers_paid for x in self.securities} - ).unstack() + bidoffer = pd.DataFrame({x.name: x.bidoffers_paid for x in self.securities}).unstack() prc += bidoffer / trades - res = pd.DataFrame({"price": prc, "quantity": trades}).dropna( - subset=["quantity"] - ) + res = pd.DataFrame({"price": prc, "quantity": trades}).dropna(subset=["quantity"]) # set names res.index.names = ["Security", "Date"] @@ -1329,10 +1291,7 @@ def bidoffers(self): self.update(self.root.now) return self._bidoffers.loc[: self.now] else: - raise Exception( - "Bid/offer accounting not turned on: " - '"bidoffer" argument not provided during setup' - ) + raise Exception("Bid/offer accounting not turned on: " '"bidoffer" argument not provided during setup') @property def bidoffer_paid(self): @@ -1357,10 +1316,7 @@ def bidoffers_paid(self): self.root.update(self.root.now, None) return self._bidoffers_paid.loc[: self.now] else: - raise Exception( - "Bid/offer accounting not turned on: " - '"bidoffer" argument not provided during setup' - ) + raise Exception("Bid/offer accounting not turned on: " '"bidoffer" argument not provided during setup') def setup(self, universe, **kwargs): """ @@ -1473,11 +1429,7 @@ def update(self, date, data=None, inow=None): if is_zero(self._position): self._value = 0 else: - raise Exception( - "Position is open (non-zero: %s) and latest price is NaN " - "for security %s on %s. Cannot update node value." - % (self._position, self.name, date) - ) + raise Exception("Position is open (non-zero: %s) and latest price is NaN " "for security %s on %s. Cannot update node value." % (self._position, self.name, date)) else: self._value = self._position * self._price * self.multiplier @@ -1498,9 +1450,7 @@ def update(self, date, data=None, inow=None): if self._bidoffer_set: self._bidoffers_paid.values[inow] = self._bidoffer_paid - @cy.locals( - amount=cy.double, update=cy.bint, q=cy.double, outlay=cy.double, i=cy.int - ) + @cy.locals(amount=cy.double, update=cy.bint, q=cy.double, outlay=cy.double, i=cy.int) def allocate(self, amount, update=True): """ This allocates capital to the Security. This is the method used to @@ -1537,11 +1487,7 @@ def allocate(self, amount, update=True): raise Exception("Cannot allocate capital to a parentless security") if is_zero(self._price) or np.isnan(self._price): - raise Exception( - "Cannot allocate capital to " - "%s because price is %s as of %s" - % (self.name, self._price, self.parent.now) - ) + raise Exception("Cannot allocate capital to " "%s because price is %s as of %s" % (self.name, self._price, self.parent.now)) # buy/sell # determine quantity - must also factor in commission @@ -1595,9 +1541,7 @@ def allocate(self, amount, update=True): last_q = q last_amount_short = full_outlay - amount while not np.isclose(full_outlay, amount, rtol=0.0) and q != 0: - dq_wout_considering_tx_costs = (full_outlay - amount) / ( - self._price * self.multiplier - ) + dq_wout_considering_tx_costs = (full_outlay - amount) / (self._price * self.multiplier) q = q - dq_wout_considering_tx_costs if self.integer_positions: @@ -1684,10 +1628,7 @@ def transact(self, q, update=True, update_self=True, price=None): return if price is not None and not self._bidoffer_set: - raise ValueError( - 'Cannot transact at custom prices when "bidoffer" has ' - "not been passed during setup to enable bid-offer tracking." - ) + raise ValueError('Cannot transact at custom prices when "bidoffer" has ' "not been passed during setup to enable bid-offer tracking.") # this security will need an update, even if pos is 0 (for example if # we close the positions, value and pos is 0, but still need to do that @@ -1854,9 +1795,7 @@ def setup(self, universe, **kwargs): # Handle coupons if "coupons" not in kwargs: - raise Exception( - '"coupons" must be passed to setup for a CouponPayingSecurity' - ) + raise Exception('"coupons" must be passed to setup for a CouponPayingSecurity') try: self._coupons = kwargs["coupons"][self.name] @@ -1909,11 +1848,7 @@ def update(self, date, data=None, inow=None): if is_zero(self._position): self._coupon = 0.0 else: - raise Exception( - "Position is open (non-zero) and latest coupon is NaN " - "for security %s on %s. Cannot update node value." - % (self.name, date) - ) + raise Exception("Position is open (non-zero) and latest coupon is NaN " "for security %s on %s. Cannot update node value." % (self.name, date)) else: self._coupon = self._position * coupon @@ -1935,9 +1870,7 @@ def coupon(self): """ Current coupon payment (scaled by position) """ - if ( - self.root.stale - ): # Stale check needed because coupon paid depends on position + if self.root.stale: # Stale check needed because coupon paid depends on position self.root.update(self.root.now, None) return self._coupon @@ -1946,9 +1879,7 @@ def coupons(self): """ TimeSeries of coupons paid (scaled by position) """ - if ( - self.root.stale - ): # Stale check needed because coupon paid depends on position + if self.root.stale: # Stale check needed because coupon paid depends on position self.root.update(self.root.now, None) return self._coupon_income.loc[: self.now] @@ -1957,9 +1888,7 @@ def holding_cost(self): """ Current holding cost (scaled by position) """ - if ( - self.root.stale - ): # Stale check needed because coupon paid depends on position + if self.root.stale: # Stale check needed because coupon paid depends on position self.root.update(self.root.now, None) return self._holding_cost @@ -1968,9 +1897,7 @@ def holding_costs(self): """ TimeSeries of coupons paid (scaled by position) """ - if ( - self.root.stale - ): # Stale check needed because coupon paid depends on position + if self.root.stale: # Stale check needed because coupon paid depends on position self.root.update(self.root.now, None) return self._holding_costs.loc[: self.now] diff --git a/docs/source/conf.py b/docs/source/conf.py index 21478df8..f9f1cd1d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -64,7 +64,7 @@ # built documents. # # The short X.Y version. -version = ".".join([str(x) for x in bt.__version__]) +version = bt.__version__ # The full version, including alpha/beta/rc tags. release = version diff --git a/pyproject.toml b/pyproject.toml index b9a741c8..f309d811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,8 @@ [build-system] -# Minimum requirements for the build system to execute. requires = ["setuptools", "wheel", "cython>0.25"] + +[tool.ruff] +line-length = 180 + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401", "F403"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 13ccd2dc..00000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[bdist_wheel] -universal=1 - -[metadata] -description_file = README.rst - -[flake8] -ignore=E203, W503 -max-line-length=180 -per-file-ignores= - bt/__init__.py:F401,F403 \ No newline at end of file diff --git a/setup.py b/setup.py index f991fb62..d5d5177a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ import codecs import os -import re from setuptools import setup from setuptools.extension import Extension @@ -10,12 +9,6 @@ def local_file(filename): return codecs.open(os.path.join(os.path.dirname(__file__), filename), "r", "utf-8") -version = re.search( - "^__version__ = \\((\\d+), (\\d+), (\\d+)\\)", - local_file(os.path.join("bt", "__init__.py")).read(), - re.MULTILINE, -).groups() - try: from Cython.Build import cythonize except ImportError: @@ -32,36 +25,41 @@ def local_file(filename): setup( name="bt", - version=".".join(version), + version="1.0.0", author="Philippe Morissette", author_email="morissette.philippe@gmail.com", description="A flexible backtesting framework for Python", - keywords="python finance quant backtesting strategies", + keywords="python finance quant backtesting strategies algotrading algorithmic trading", url="https://github.com/pmorissette/bt", license="MIT", - install_requires=["ffn>=0.3.7", "pyprind>=2.11"], + install_requires=["ffn>=1.0.0", "pyprind>=2.11"], extras_require={ "dev": [ - "black>=20.8b1", - "codecov", "cython>=0.25", - "ffn>=0.3.5", - "flake8", - "flake8-black", + "ffn>=1.0.0", "matplotlib>=2", "numpy>=1", "pandas>=0.19", "pyprind>=2.11", "pytest", "pytest-cov", + "ruff", ], }, packages=["bt"], - long_description=local_file("README.rst").read().replace("\r\n", "\n"), + long_description=local_file("README.md").read().replace("\r\n", "\n"), + long_description_content_type="text/markdown", classifiers=[ - "Development Status :: 3 - Alpha", - "Topic :: Software Development :: Libraries", + "Development Status :: 4 - Beta", "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", + "License :: OSI Approved :: MIT License", ], ext_modules=ext_modules, python_requires=">=3.7",