From 7786d085d002b77e3bab50e51000aaa8216047e7 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sun, 19 Nov 2023 19:17:56 -0500 Subject: [PATCH] Update package for 2023, move to ruff for formatting, remove seaborn import styling side effect fixes #205 --- LICENSE.txt => LICENSE | 0 MANIFEST.in | 5 +- Makefile | 13 ++--- README.md | 53 +++++++++++++++++++++ README.rst | 67 -------------------------- ffn/core.py | 106 ++++++++++------------------------------- ffn/data.py | 13 +---- pyproject.toml | 69 +++++++++++++++++++++++++++ setup.cfg | 11 ----- setup.py | 55 +-------------------- 10 files changed, 159 insertions(+), 233 deletions(-) rename LICENSE.txt => LICENSE (100%) create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 setup.cfg 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 bc7a7ad2..e72fe736 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ graft ffn include LICENSE.txt include README.rst - -include setup.cfg include pyproject.toml +exclude setup.cfg +prune tests + # Patterns to exclude from any directory global-exclude *~ global-exclude *.pyc diff --git a/Makefile b/Makefile index 93ffb9e7..56ca80b7 100644 --- a/Makefile +++ b/Makefile @@ -9,26 +9,27 @@ test: python -m pytest -vvv tests --cov=ffn --junitxml=python_junit.xml --cov-report=xml --cov-branch --cov-report term lint: - python -m flake8 ffn setup.py docs/source/conf.py + python -m ruff ffn setup.py docs/source/conf.py fix: - python -m black ffn setup.py docs/source/conf.py + python -m ruff format ffn setup.py docs/source/conf.py clean: - rm -rf dist - rm -rf ffn.egg-info dist: - python setup.py sdist + python -m build + twine check dist/* upload: clean dist twine upload dist/* -docs: +docs: $(MAKE) -C docs/ clean $(MAKE) -C docs/ html -pages: +pages: rm -rf $(TMPREPO) git clone -b gh-pages git@github.com:pmorissette/ffn.git $(TMPREPO) rm -rf $(TMPREPO)/* @@ -38,7 +39,7 @@ pages: git commit -a -m 'auto-updating docs';\ git push;\ -serve: +serve: cd docs/build/html; \ python -m http.server 9087 diff --git a/README.md b/README.md new file mode 100644 index 00000000..4252ad26 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +![](http://pmorissette.github.io/ffn/_static/logo.png) + +[![Build Status](https://github.com/pmorissette/ffn/workflows/Build%20Status/badge.svg)](https://github.com/pmorissette/ffn/actions/) +[![Codecov](https://codecov.io/gh/pmorissette/ffn/branch/master/graph/badge.svg)](https://codecov.io/pmorissette/ffn) +[![PyPI Version](https://img.shields.io/pypi/v/ffn)](https://pypi.org/project/ffn/) +[![PyPI License](https://img.shields.io/pypi/l/ffn)](https://pypi.org/project/ffn/) + +# ffn - Financial Functions for Python + +Alpha release - please let me know if you find any bugs! + +If you are looking for a full backtesting framework, please check out [bt](https://github.com/pmorissette/bt). +bt is built atop ffn and makes it easy and fast to backtest quantitative strategies. + +## Overview + +ffn is a library that contains many useful functions for those who work in **quantitative +finance**. It stands on the shoulders of giants (Pandas, Numpy, Scipy, etc.) and provides +a vast array of utilities, from performance measurement and evaluation to +graphing and common data transformations. + +```python +import ffn +returns = ffn.get('aapl,msft,c,gs,ge', start='2010-01-01').to_returns().dropna() +returns.calc_mean_var_weights().as_format('.2%') + aapl 62.54% + c -0.00% + ge 36.19% + gs -0.00% + msft 1.26% + dtype: object +``` + + +## Installation + +The easiest way to install `ffn` is from the [Python Package Index](https://pypi.python.org/pypi/ffn/) +using `pip`. + +```bash +pip install ffn +``` + +Since ffn has many dependencies, we strongly recommend installing the [Anaconda Scientific Python Distribution](https://store.continuum.io/cshop/anaconda/). This distribution comes with many of the required packages pre-installed, including pip. Once Anaconda is installed, the above command should complete the installation. + +ffn should be compatible with Python 2.7 and Python 3. + +## Documentation + +Read the docs at http://pmorissette.github.io/ffn + +- [Quickstart](http://pmorissette.github.io/ffn/quick.html) +- [Full API](http://pmorissette.github.io/ffn/ffn.html) diff --git a/README.rst b/README.rst deleted file mode 100644 index faf5a5f2..00000000 --- a/README.rst +++ /dev/null @@ -1,67 +0,0 @@ -.. image:: http://pmorissette.github.io/ffn/_static/logo.png - -.. image:: https://github.com/pmorissette/ffn/workflows/Build%20Status/badge.svg - :target: https://github.com/pmorissette/ffn/actions/ - -.. image:: https://codecov.io/gh/pmorissette/ffn/branch/master/graph/badge.svg - :target: https://codecov.io/pmorissette/ffn - -.. image:: https://img.shields.io/pypi/v/ffn - :alt: PyPI - :target: https://pypi.org/project/ffn/ - -.. image:: https://img.shields.io/pypi/l/ffn - :alt: PyPI - License - :target: https://pypi.org/project/ffn/ - -ffn - Financial Functions for Python -==================================== - -Alpha release - please let me know if you find any bugs! - -If you are looking for a full backtesting framework, please check out `bt -`_. bt is built atop ffn and makes it easy -and fast to backtest quantitative strategies. - -Overview --------- - -ffn is a library that contains many useful functions for those who work in **quantitative -finance**. It stands on the shoulders of giants (Pandas, Numpy, Scipy, etc.) and provides -a vast array of utilities, from performance measurement and evaluation to -graphing and common data transformations. - -.. code:: python - - >> import ffn - >> returns = ffn.get('aapl,msft,c,gs,ge', start='2010-01-01').to_returns().dropna() - >> returns.calc_mean_var_weights().as_format('.2%') - aapl 62.54% - c -0.00% - ge 36.19% - gs -0.00% - msft 1.26% - dtype: object - - -Installation ------------- - -The easiest way to install ``ffn`` is from the `Python Package Index `_ -using ``pip`` or ``easy_install``: - -.. code-block:: bash - - $ pip install ffn - -Since ffn has many dependencies, we strongly recommend installing the `Anaconda Scientific Python Distribution `_. This distribution comes with many of the required packages pre-installed, including pip. Once Anaconda is installed, the above command should complete the installation. - -ffn should be compatible with Python 2.7 and Python 3. - -Documentation -------------- - -Read the docs at http://pmorissette.github.io/ffn - -- `Quickstart `__ -- `Full API `__ diff --git a/ffn/core.py b/ffn/core.py index 8353835c..4a444651 100644 --- a/ffn/core.py +++ b/ffn/core.py @@ -20,15 +20,9 @@ from . import utils from .utils import fmtn, fmtp, fmtpn, get_freq_name -try: - import seaborn as sns - - sns.set(style="ticks", palette="Set2") -except ImportError: - pass - _PANDAS_TWO = Version(pd.__version__) >= Version("2") + # module level variable, can be different for non traditional markets (eg. crypto - 360) TRADING_DAYS_PER_YEAR = 252 @@ -224,21 +218,13 @@ def _calculate(self, obj): self.daily_vol = np.std(r, ddof=1) * np.sqrt(self.annualization_factor) if isinstance(self.rf, float): - self.daily_sharpe = r.calc_sharpe( - rf=self.rf, nperiods=self.annualization_factor - ) - self.daily_sortino = calc_sortino_ratio( - r, rf=self.rf, nperiods=self.annualization_factor - ) + self.daily_sharpe = r.calc_sharpe(rf=self.rf, nperiods=self.annualization_factor) + self.daily_sortino = calc_sortino_ratio(r, rf=self.rf, nperiods=self.annualization_factor) # rf is a price series else: _rf_daily_price_returns = self.rf.to_returns() - self.daily_sharpe = r.calc_sharpe( - rf=_rf_daily_price_returns, nperiods=self.annualization_factor - ) - self.daily_sortino = calc_sortino_ratio( - r, rf=_rf_daily_price_returns, nperiods=self.annualization_factor - ) + self.daily_sharpe = r.calc_sharpe(rf=_rf_daily_price_returns, nperiods=self.annualization_factor) + self.daily_sortino = calc_sortino_ratio(r, rf=_rf_daily_price_returns, nperiods=self.annualization_factor) self.best_day = r.max() self.worst_day = r.min() @@ -281,18 +267,14 @@ def _calculate(self, obj): self.monthly_mean = mr.mean() * 12 self.monthly_vol = np.std(mr, ddof=1) * np.sqrt(12) - if type(self.rf) is float: + if isinstance(self.rf, float): self.monthly_sharpe = mr.calc_sharpe(rf=self.rf, nperiods=12) self.monthly_sortino = calc_sortino_ratio(mr, rf=self.rf, nperiods=12) # rf is a price series else: _rf_monthly_price_returns = self.rf.resample("M").last().to_returns() - self.monthly_sharpe = mr.calc_sharpe( - rf=_rf_monthly_price_returns, nperiods=12 - ) - self.monthly_sortino = calc_sortino_ratio( - mr, rf=_rf_monthly_price_returns, nperiods=12 - ) + self.monthly_sharpe = mr.calc_sharpe(rf=_rf_monthly_price_returns, nperiods=12) + self.monthly_sortino = calc_sortino_ratio(mr, rf=_rf_monthly_price_returns, nperiods=12) self.best_month = mr.max() self.worst_month = mr.min() @@ -384,12 +366,8 @@ def _calculate(self, obj): else: _rf_yearly_price_returns = self.rf.resample("A").last().to_returns() if self.yearly_vol > 0: - self.yearly_sharpe = yr.calc_sharpe( - rf=_rf_yearly_price_returns, nperiods=1 - ) - self.yearly_sortino = calc_sortino_ratio( - yr, rf=_rf_yearly_price_returns, nperiods=1 - ) + self.yearly_sharpe = yr.calc_sharpe(rf=_rf_yearly_price_returns, nperiods=1) + self.yearly_sortino = calc_sortino_ratio(yr, rf=_rf_yearly_price_returns, nperiods=1) self.best_year = yr.max() self.worst_year = yr.min() @@ -515,7 +493,7 @@ def display(self): provided. """ print("Stats for %s from %s - %s" % (self.name, self.start, self.end)) - if type(self.rf) is float: + if isinstance(self.rf, float): print("Annual risk-free rate considered: %s" % (fmtp(self.rf))) print("Summary:") data = [ @@ -526,9 +504,7 @@ def display(self): fmtp(self.max_drawdown), ] ] - print( - tabulate(data, headers=["Total Return", "Sharpe", "CAGR", "Max Drawdown"]) - ) + print(tabulate(data, headers=["Total Return", "Sharpe", "CAGR", "Max Drawdown"])) print("\nAnnualized Returns:") data = [ @@ -916,9 +892,7 @@ def _stats(self): def _update_stats(self): # lookback returns dataframe - self.lookback_returns = pd.DataFrame( - {x.lookback_returns.name: x.lookback_returns for x in self.values()} - ) + self.lookback_returns = pd.DataFrame({x.lookback_returns.name: x.lookback_returns for x in self.values()}) self.stats = pd.DataFrame({x.name: x.stats for x in self.values()}) @@ -1349,9 +1323,7 @@ def drawdown_details(drawdown, index_type=pd.DatetimeIndex): if start[-1] > end[-1]: end.append(drawdown.index[-1]) - result = pd.DataFrame( - columns=("Start", "End", "Length", "drawdown"), index=range(0, len(start)) - ) + result = pd.DataFrame(columns=("Start", "End", "Length", "drawdown"), index=range(0, len(start))) for i in range(0, len(start)): dd = drawdown[start[i] : end[i]].min() @@ -1538,9 +1510,7 @@ def asfreq_actual(series, freq, method="ffill", how="end", normalize=False): orig = pd.DataFrame({name: series}) # add date column - t = pd.concat( - [orig, pd.DataFrame({"dt": orig.index.values}, index=orig.index.values)], axis=1 - ) + t = pd.concat([orig, pd.DataFrame({"dt": orig.index.values}, index=orig.index.values)], axis=1) # fetch dates dts = t.asfreq(freq=freq, method=method, how=how, normalize=normalize)["dt"] @@ -1573,9 +1543,7 @@ def calc_inv_vol_weights(returns): return np.divide(vol, volsum) -def calc_mean_var_weights( - returns, weight_bounds=(0.0, 1.0), rf=0.0, covar_method="ledoit-wolf", options=None -): +def calc_mean_var_weights(returns, weight_bounds=(0.0, 1.0), rf=0.0, covar_method="ledoit-wolf", options=None): """ Calculates the mean-variance weights given a DataFrame of returns. @@ -1740,9 +1708,7 @@ def _erc_weights_ccd(x0, cov, b, maximum_iterations, tolerance): ctr = ctr - cov[i] * x_i + cov[i] * x_tilde sigma_x = sigma_x * sigma_x - 2 * x_i * cov[i].dot(x) + x_i * x_i * var[i] x[i] = x_tilde - sigma_x = np.sqrt( - sigma_x + 2 * x_tilde * cov[i].dot(x) - x_tilde * x_tilde * var[i] - ) + sigma_x = np.sqrt(sigma_x + 2 * x_tilde * cov[i].dot(x) - x_tilde * x_tilde * var[i]) # check convergence if np.power((x - x0) / x.sum(), 2).sum() < tolerance: @@ -1751,9 +1717,7 @@ def _erc_weights_ccd(x0, cov, b, maximum_iterations, tolerance): x0 = x.copy() # no solution found - raise ValueError( - "No solution found after {0} iterations.".format(maximum_iterations) - ) + raise ValueError("No solution found after {0} iterations.".format(maximum_iterations)) def calc_erc_weights( @@ -1810,14 +1774,10 @@ def calc_erc_weights( # calc risk parity weights matrix if risk_parity_method == "ccd": # cyclical coordinate descent implementation - erc_weights = _erc_weights_ccd( - initial_weights, covar, risk_weights, maximum_iterations, tolerance - ) + erc_weights = _erc_weights_ccd(initial_weights, covar, risk_weights, maximum_iterations, tolerance) elif risk_parity_method == "slsqp": # scipys slsqp optimizer - erc_weights = _erc_weights_slsqp( - initial_weights, covar, risk_weights, maximum_iterations, tolerance - ) + erc_weights = _erc_weights_slsqp(initial_weights, covar, risk_weights, maximum_iterations, tolerance) else: raise NotImplementedError("risk_parity_method not implemented") @@ -1826,9 +1786,7 @@ def calc_erc_weights( return pd.Series(erc_weights, index=returns.columns, name="erc") -def get_num_days_required( - offset, period="d", perc_required=0.90, annualization_factor=252 -): +def get_num_days_required(offset, period="d", perc_required=0.90, annualization_factor=252): """ Estimates the number of days required to assume that data is OK. @@ -2051,9 +2009,7 @@ def limit_weights(weights, limit=0.1): weights = pd.Series(weights) if np.round(weights.sum(), 1) != 1.0: - raise ValueError( - "Expecting weights (that sum to 1) - sum is %s" % weights.sum() - ) + raise ValueError("Expecting weights (that sum to 1) - sum is %s" % weights.sum()) res = np.round(weights.copy(), 4) to_rebalance = (res[res > limit] - limit).sum() @@ -2115,19 +2071,7 @@ def random_weights(n, bounds=(0.0, 1.0), total=1.0): return w -def plot_heatmap( - data, - title="Heatmap", - show_legend=True, - show_labels=True, - label_fmt=".2f", - vmin=None, - vmax=None, - figsize=None, - label_color="w", - cmap="RdBu", - **kwargs -): +def plot_heatmap(data, title="Heatmap", show_legend=True, show_labels=True, label_fmt=".2f", vmin=None, vmax=None, figsize=None, label_color="w", cmap="RdBu", **kwargs): """ Plot a heatmap using matplotlib's pcolor. @@ -2332,9 +2276,7 @@ def infer_nperiods(data, annualization_factor=TRADING_DAYS_PER_YEAR): whole_periods_str = freq[-1] num_str = freq[:-1] num = int(num_str) - return num * _whole_periods_str_to_nperiods( - whole_periods_str, annualization_factor - ) + return num * _whole_periods_str_to_nperiods(whole_periods_str, annualization_factor) except KeyboardInterrupt: raise except BaseException: diff --git a/ffn/data.py b/ffn/data.py index a841f127..915f9553 100644 --- a/ffn/data.py +++ b/ffn/data.py @@ -14,18 +14,7 @@ @utils.memoize -def get( - tickers, - provider=None, - common_dates=True, - forward_fill=False, - clean_tickers=True, - column_names=None, - ticker_field_sep=":", - mrefresh=False, - existing=None, - **kwargs -): +def get(tickers, provider=None, common_dates=True, forward_fill=False, clean_tickers=True, column_names=None, ticker_field_sep=":", mrefresh=False, existing=None, **kwargs): """ Helper function for retrieving data as a DataFrame. diff --git a/pyproject.toml b/pyproject.toml index 41146969..a9bc1e0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,72 @@ [build-system] # Minimum requirements for the build system to execute. requires = ["setuptools", "wheel"] + +[project] +name = "ffn" +description = "Financial functions for Python" +version = "0.3.7" +readme = "README.md" +license = { file = "LICENSE.txt" } +requires-python = ">=3.8" +authors = [ + { name = "Philippe Morissette", email = "morissette.philippe@gmail.com" }, +] +keywords = [ + "python", + "finance", + "quant", + "quant finance", + "algotrading", + "algorithmic trading", +] +classifiers = [ + "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", +] +dependencies = [ + "decorator>=4", + "matplotlib>=1", + "numpy>=1.5", + "pandas>=0.19", + "pandas-datareader>=0.2", + "scikit-learn>=0.15", + "scipy>=0.15", + "tabulate>=0.7.5", + "yfinance>=0.2", +] + +[project.optional-dependencies] +dev = [ + "build", + "ruff", + "pytest", + "pytest-cov", + "wheel", +] +test = [ + "pytest", + "pytest-cov", +] + +[project.urls] +Repository = "https://github.com/pmorissette/ffn" +Homepage = "http://pmorissette.github.io/ffn/" + + +[tool.ruff] +line-length = 180 + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401", "F403"] + +[tool.setuptools] +py-modules = ["ffn"] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e23408b5..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= - ffn/__init__.py:F401,F403 \ No newline at end of file diff --git a/setup.py b/setup.py index 8cda28ea..b6c66813 100644 --- a/setup.py +++ b/setup.py @@ -1,53 +1,2 @@ -import os -import re - -import setuptools - -with open(os.path.join(os.path.dirname(__file__), "ffn", "__init__.py"), "r") as fp: - version = re.search( - "^__version__ = \\((\\d+), (\\d+), (\\d+)\\)$", fp.read(), re.MULTILINE - ).groups() - - -with open(os.path.join(os.path.dirname(__file__), "README.rst"), "r") as fp: - description = fp.read().replace("\r\n", "\n") - -setuptools.setup( - name="ffn", - version=".".join(version), - author="Philippe Morissette", - author_email="morissette.philippe@gmail.com", - description="Financial functions for Python", - keywords="python finance quant functions", - url="https://github.com/pmorissette/ffn", - license="MIT", - install_requires=[ - "decorator>=4", - "matplotlib>=1", - "numpy>=1.5", - "pandas>=0.19", - "pandas-datareader>=0.2", - "scikit-learn>=0.15", - "scipy>=0.15", - "tabulate>=0.7.5", - "yfinance>=0.2", - ], - extras_require={ - "dev": [ - "black>=22", - "codecov", - "flake8", - "flake8-black", - "pytest", - "pytest-cov", - ], - }, - packages=["ffn"], - long_description=description, - classifiers=[ - "Development Status :: 3 - Alpha", - "Topic :: Software Development :: Libraries", - "Programming Language :: Python", - ], - python_requires=">=3.7", -) +# setup.py shim for use with applications that require it. +__import__("setuptools").setup()