From 740d81568545cb648d040b1c83ae26c80b9b29e7 Mon Sep 17 00:00:00 2001 From: Kevin Johnson Date: Thu, 21 Feb 2019 10:31:55 -0800 Subject: [PATCH] added performance indicators with basic tests --- .gitignore | 5 +- README.md | 72 +++++++++++++++ pandas_ta/core.py | 33 +++++-- pandas_ta/performance.py | 114 ++++++++++++++++++++++++ setup.py | 15 +++- tests/test_indicator_performance.py | 43 +++++++++ tests/test_indicator_performance_ext.py | 49 ++++++++++ 7 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 pandas_ta/performance.py create mode 100644 tests/test_indicator_performance.py create mode 100644 tests/test_indicator_performance_ext.py diff --git a/.gitignore b/.gitignore index 2eea71b..46e2cf4 100644 --- a/.gitignore +++ b/.gitignore @@ -123,4 +123,7 @@ ta_extension.ipynb Charts.ipynb pandas_pips reqs.txt -requirements.txt \ No newline at end of file +requirements.txt + +test_indicator_statistics.py +test_indicator_statistics_ext.py \ No newline at end of file diff --git a/README.md b/README.md index 7375dca..9b90e93 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,78 @@ Technical Analysis (TA) is an easy to use library that is built upon Python's Pa This version contains both the orignal code branch as well as a newly refactored branch with the option to use [Pandas DataFrame Extension](https://pandas.pydata.org/pandas-docs/stable/extending.html) mode. All the indicators return a named Series or a DataFrame in uppercase underscore parameter format. For example, MACD(fast=12, slow=26, signal=9) will return a DataFrame with columns: ['MACD_12_26_9', 'MACDH_12_26_9', 'MACDS_12_26_9']. +## New Changes + +* At 70+ indicators. +* Abbreviated Indicator names as listed below. +* *Extended Pandas DataFrame* as 'ta'. See examples below. +* Parameter names are more consistent. +* Former indicators still exist and are renamed with '_depreciated' append to it's name. For example, 'average_true_range' is now 'average_true_range_depreciated'. +* Refactoring indicators into categories similar to [TA-lib](https://github.com/mrjbq7/ta-lib/tree/master/docs/func_groups). + +### What is a Pandas DataFrame Extension? + +A [Pandas DataFrame Extension](https://pandas.pydata.org/pandas-docs/stable/extending.html), extends a DataFrame allowing one to add more functionality and features to Pandas to suit your needs. As such, it is now easier to run Technical Analysis on existing Financial Time Series without leaving the current DataFrame. This extension by default returns the Indicator result or, inclusively, it can append the result to the existing DataFrame by including the parameter +'append=True' in the method call. See examples below. + + +# Getting Started and Examples + +## **Quick Start** using the DataFrame Extension + +```python +import pandas as pd +import pandas_ta as ta + +# Load data +df = pd.read_csv('symbol.csv', sep=',') + +# Calculate Returns and append to the df DataFrame +df.ta.log_return(cumulative=True, append=True) +df.ta.percent_return(cumulative=True, append=True) + +# New Columns with results +df.columns + +# Take a peek +df.tail() + +# vv Continue Post Processing vv +``` + +## Module and Indicator Help + +```python +import pandas as pd +import pandas_ta as ta + +# Help about this, 'ta', extension +help(pd.DataFrame().ta) + +# List of all indicators +pd.DataFrame().ta.indicators() + +# Help about the log_return indicator +help(ta.log_return) + +# Help about the log_return indicator as a DataFrame Extension +help(pd.DataFrame().ta.log_return) +``` + + + +# Technical Analysis Indicators (by Category) + +## _Performance_ (2) + +Use parameter: cumulative=**True** for cumulative results. + +* _Log Return_: **log_return** +* _Percent Return_: **percent_return** + +| _Percent Return_ (Cumulative) with _Simple Moving Average_ (SMA) | +|:--------:| +| ![Example Cumulative Percent Return](/images/SPY_CumulativePercentReturn.png) | # Inspiration diff --git a/pandas_ta/core.py b/pandas_ta/core.py index d4d98c7..6c95491 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import time import pandas as pd - -from .utils import * from pandas.core.base import PandasObject +from .utils import * +from .performance import * class BasePandasObject(PandasObject): @@ -122,12 +122,10 @@ class AnalysisIndicators(BasePandasObject): return indicator else: - # self.help() - pass + self.help() except: - # self.help() - pass + self.help() def _append(self, result=None, **kwargs): @@ -209,6 +207,7 @@ class AnalysisIndicators(BasePandasObject): header = f"pandas.ta - Technical Analysis Indicators" helper_methods = ['indicators', 'constants'] # Public non-indicator methods exclude_methods = kwargs.pop('exclude', None) + as_list = kwargs.pop('as_list', False) ta_indicators = list((x for x in dir(pd.DataFrame().ta) if not x.startswith('_') and not x.endswith('_'))) for x in helper_methods: @@ -218,6 +217,9 @@ class AnalysisIndicators(BasePandasObject): for x in exclude_methods: ta_indicators.remove(x) + if as_list: + return ta_indicators + total_indicators = len(ta_indicators) s = f"{header}\nTotal Indicators: {total_indicators}\n" if total_indicators > 0: @@ -225,4 +227,21 @@ class AnalysisIndicators(BasePandasObject): # print(f"{s}Abbreviations:\n {abbr_list}") print(f"{s}Abbreviations:\n {abbr_list}") else: - print(s) \ No newline at end of file + print(s) + + + + + def log_return(self, close=None, length=None, cumulative=False, percent=False, offset=None, **kwargs): + close = self._get_column(close, 'close') + result = log_return(close=close, length=length, cumulative=cumulative, percent=percent, offset=offset, **kwargs) + self._append(result, **kwargs) + # print(f"result:\n{result}") + return result + + + def percent_return(self, close=None, length=None, cumulative=False, percent=False, offset=None, **kwargs): + close = self._get_column(close, 'close') + result = percent_return(close=close, length=length, cumulative=cumulative, percent=percent, offset=offset, **kwargs) + self._append(result, **kwargs) + return result \ No newline at end of file diff --git a/pandas_ta/performance.py b/pandas_ta/performance.py new file mode 100644 index 0000000..7c26dcf --- /dev/null +++ b/pandas_ta/performance.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +import numpy as np +import pandas as pd + +from .utils import get_offset, verify_series + + +def log_return(close, length=None, cumulative=False, offset=None, **kwargs): + """Indicator: Log Return""" + # Validate Arguments + close = verify_series(close) + length = int(length) if length and length > 0 else 1 + offset = get_offset(offset) + + # Calculate Result + log_return = np.log(close).diff(periods=length) + + if cumulative: + log_return = log_return.cumsum() + + # Offset + if offset != 0: + log_return = log_return.shift(offset) + + # Name & Category + log_return.name = f"{'CUM' if cumulative else ''}LOGRET_{length}" + log_return.category = 'performance' + + return log_return + + +def percent_return(close, length=None, cumulative=False, offset=None, **kwargs): + """Indicator: Percent Return""" + # Validate Arguments + close = verify_series(close) + length = int(length) if length and length > 0 else 1 + offset = get_offset(offset) + + # Calculate Result + pct_return = close.pct_change(length) + + if cumulative: + pct_return = pct_return.cumsum() + + # Offset + if offset != 0: + pct_return = pct_return.shift(offset) + + # Name & Category + pct_return.name = f"{'CUM' if cumulative else ''}PCTRET_{length}" + pct_return.category = 'performance' + + return pct_return + + + +log_return.__doc__ = \ +"""Log Return + +Calculates the logarithmic return of a Series. +See also: help(df.ta.log_return) for additional **kwargs a valid 'df'. + +Sources: + https://stackoverflow.com/questions/31287552/logarithmic-returns-in-pandas-dataframe + +Calculation: + Default Inputs: + length=1, cummulative=False + LOGRET = log( close.diff(periods=length) ) + CUMLOGRET = LOGRET.cumsum() if cummulative + +Args: + close (pd.Series): Series of 'close's + length (int): It's period. Default: 20 + cummulative (bool): If True, returns the cummulative returns. Default: False + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +percent_return.__doc__ = \ +"""Percent Return + +Calculates the percent return of a Series. +See also: help(df.ta.percent_return) for additional **kwargs a valid 'df'. + +Sources: + https://stackoverflow.com/questions/31287552/logarithmic-returns-in-pandas-dataframe + +Calculation: + Default Inputs: + length=1, cummulative=False + PCTRET = close.pct_change(length) + CUMPCTRET = PCTRET.cumsum() if cummulative + +Args: + close (pd.Series): Series of 'close's + length (int): It's period. Default: 20 + cummulative (bool): If True, returns the cummulative returns. Default: False + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" \ No newline at end of file diff --git a/setup.py b/setup.py index 6063c1e..0c63c86 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ long_description = "A Python 3 Pandas Extension of Technical Analysis Indicators setup( name = "pandas_ta", packages = ["pandas_ta"], - version = "0.0.3a", + version = "0.0.4a", description=long_description, long_description=long_description, author = "Kevin Johnson", @@ -28,5 +28,16 @@ setup( 'Intended Audience :: Financial and Insurance Industry', 'Topic :: Office/Business :: Financial :: Investment', ], - # zip_safe=False, + package_data={ + 'data': ['data/*.csv'], + }, + install_requires=['pandas'], + + # List additional groups of dependencies here (e.g. development dependencies). + # You can install these using the following syntax, for example: + # $ pip install -e .[dev,test] + extras_require = { + 'dev': ['ta-lib', 'jupyterlab'], + 'test': ['ta-lib'], + }, ) diff --git a/tests/test_indicator_performance.py b/tests/test_indicator_performance.py new file mode 100644 index 0000000..8ad1c03 --- /dev/null +++ b/tests/test_indicator_performance.py @@ -0,0 +1,43 @@ +from unittest import TestCase +import numpy.testing as npt +import pandas.util.testing as pdt +from pandas import DataFrame, read_csv, Series + +from pandas_ta import performance + + +class TestPerformace(TestCase): + @classmethod + def setUpClass(cls): + cls.data = read_csv('data/sample.csv', index_col=0, parse_dates=True, infer_datetime_format=False, keep_date_col=True) + cls.close = cls.data['close'] + + @classmethod + def tearDownClass(cls): + del cls.data + del cls.close + + + def setUp(self): + self.performance = performance + + def tearDown(self): + del self.performance + + + def test_log_return(self): + log_return = self.performance.log_return(self.close) + self.assertIsInstance(log_return, Series) + self.assertEqual(log_return.name, 'LOGRET_1') + + cumlog_return = self.performance.log_return(self.close, cumulative=True) + self.assertEqual(cumlog_return.name, 'CUMLOGRET_1') + + def test_percent_return(self): + percent_return = self.performance.percent_return(self.close) + self.assertIsInstance(percent_return, Series) + self.assertEqual(percent_return.name, 'PCTRET_1') + + cumpercent_return = self.performance.percent_return(self.close, cumulative=True) + self.assertEqual(cumpercent_return.name, 'CUMPCTRET_1') + diff --git a/tests/test_indicator_performance_ext.py b/tests/test_indicator_performance_ext.py new file mode 100644 index 0000000..7c9eeae --- /dev/null +++ b/tests/test_indicator_performance_ext.py @@ -0,0 +1,49 @@ +from unittest import TestCase +import numpy.testing as npt +import pandas.util.testing as pdt +from pandas import DataFrame, read_csv, Series + +import pandas_ta as ta + + +class TestPerformaceExtension(TestCase): + @classmethod + def setUpClass(cls): + cls.data = read_csv('data/sample.csv', index_col=0, parse_dates=True, infer_datetime_format=False, keep_date_col=True) + cls.close = cls.data['close'] + + @classmethod + def tearDownClass(cls): + del cls.data + del cls.close + + + def setUp(self): + pass + + def tearDown(self): + pass + + + def test_log_return_ext(self): + self.data.ta.log_return(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], 'LOGRET_1') + + self.data.ta.log_return(append=True, cumulative=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], 'CUMLOGRET_1') + + def test_percent_return_ext(self): + self.data.ta.percent_return(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], 'PCTRET_1') + + self.data.ta.percent_return(append=True, cumulative=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], 'CUMPCTRET_1') + + # Example + # pta_sma = self.data[self.data.columns[-1]] + # tal_sma = tal.SMA(self.close) + # pdt.assert_series_equal(pta_sma, tal_sma) \ No newline at end of file