From c737843b1d8e6a3f0d13862bab64b25a151f246a Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:15:58 -0500 Subject: [PATCH] Uplift packaging for 2023, move to rust, rework ci sciprs Disallow 0.3.8 Align versions to 0.3.0, force newer ffn Force sdist for now, will move to cibuildwheel Bump to 1.0.0 for full semver Align bt and ffn versions --- .coveragerc | 15 ----- .github/workflows/build.yml | 12 ++-- .github/workflows/deploy.yml | 19 ++---- LICENSE.txt => LICENSE | 0 MANIFEST.in | 23 ++++--- Makefile | 13 ++-- README.rst => README.md | 93 ++++++++++---------------- bt/__init__.py | 2 +- bt/algos.py | 90 ++++++------------------- bt/backtest.py | 36 +++------- bt/core.py | 125 ++++++++--------------------------- docs/source/conf.py | 2 +- pyproject.toml | 7 +- setup.cfg | 11 --- setup.py | 34 +++++----- 15 files changed, 143 insertions(+), 339 deletions(-) delete mode 100644 .coveragerc rename LICENSE.txt => LICENSE (100%) rename README.rst => README.md (54%) delete mode 100644 setup.cfg 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",