From 78246d200e5bd2010ab0ed7ebcd753e3530fe2fc Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Thu, 15 Jul 2021 14:43:29 +0200 Subject: [PATCH 01/11] Initial version --- pandas_ta/custom.py | 132 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 pandas_ta/custom.py diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py new file mode 100644 index 0000000..d3f0a09 --- /dev/null +++ b/pandas_ta/custom.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +import os +import sys +from os.path import dirname, abspath, join +from glob import glob +import importlib +import pandas_ta + +def import_dir(dir_path, create_categories=True, verbose=True): + + # ensure that the passed directory exists / is readable + if not os.path.exists(dir_path): + print(f"[X] Unable to read the directory '{dir_path}'.") + return + + # list the contents of the directory + dirs = glob(abspath(join(dir_path, '*'))) + + # optionally add any missing category subdirectories + if create_categories: + for sd in [*pandas_ta.Category]: + d = abspath(join(dir_path, sd) + if not os.path.exists(d): + os.makedirs(directory + + # traverse the directory, importing all modules found there + for d in dirs: + dirname = os.path.basename(d) + + if dirname not in [*pandas_ta.Category]: + print(f"[i] Skipping the sub-directory '{dirname}' since it's not a pandas_ta category.") + continue + + for module in glob(abspath(join(dir_path, dirname, '*.py'))): + module = os.path.splitext(os.path.basename(module))[0] + + if module in pandas_ta.Category[dirname]: + print(f"[X] Skipping the custom module '{module}' since a function with that name already exists in pandas_ta.") + continue + + # import the module and add it to the correct category + pandas_ta.Category[dirname].append(module) + if d not in sys.path: + sys.path.append(d) + importlib.import_module(module, d) + + if verbose: + print(f"[i] Successfully imported the module '{module}' into category '{dirname}'.") + +import_dir.__doc__ = \ +""" +This method allows you to experiment and develop your own technical analysis +indicators independantly in a separate local directory of your choice but +still use them seamlessly together with the existing pandas_ta functions just +like if they were part of pandas_ta. + +If you at some late point would like to push them into the pandas_ta library +you can do so very easily by following the step by step instruction here +https://github.com/twopirllc/pandas-ta/issues/264. + +---------------------------------- + +By default, the 'ta' extension uses lower case column names: open, high, +low, close, and volume. You can override the defaults by providing the it's +replacement name when calling the indicator. For example, to call the +indicator hl2(). + +With 'default' columns: open, high, low, close, and volume. +>>> df.ta.hl2() +>>> df.ta(kind="hl2") + +With DataFrame columns: Open, High, Low, Close, and Volume. +>>> df.ta.hl2(high="High", low="Low") +>>> df.ta(kind="hl2", high="High", low="Low") + +If you do not want to use a DataFrame Extension, just call it normally. +>>> sma10 = ta.sma(df["Close"]) # Default length=10 +>>> sma50 = ta.sma(df["Close"], length=50) +>>> ichimoku, span = ta.ichimoku(df["High"], df["Low"], df["Close"]) + +Args: + kind (str, optional): Default: None. Kind is the 'name' of the indicator. + It converts kind to lowercase before calling. + timed (bool, optional): Default: False. Curious about the execution + speed? + kwargs: Extension specific modifiers. + append (bool, optional): Default: False. When True, it appends the + resultant column(s) to the DataFrame. + +Returns: + Most Indicators will return a Pandas Series. Others like MACD, BBANDS, + KC, et al will return a Pandas DataFrame. Ichimoku on the other hand + will return two DataFrames, the Ichimoku DataFrame for the known period + and a Span DataFrame for the future of the Span values. + +Let's get started! + +1. Loading the 'ta' module: +>>> import pandas as pd +>>> import ta as ta + +2. Load some data: +>>> df = pd.read_csv("AAPL.csv", index_col="date", parse_dates=True) + +3. Help! +3a. General Help: +>>> help(df.ta) +>>> df.ta() +3b. Indicator Help: +>>> help(ta.apo) +3c. Indicator Extension Help: +>>> help(df.ta.apo) + +4. Ways of calling an indicator. +4a. Standard: Calling just the APO indicator without "ta" DataFrame extension. +>>> ta.apo(df["close"]) +4b. DataFrame Extension: Calling just the APO indicator with "ta" DataFrame extension. +>>> df.ta.apo() +4c. DataFrame Extension (kind): Calling APO using 'kind' +>>> df.ta(kind="apo") +4d. Strategy: +>>> df.ta.strategy("All") # Default +>>> df.ta.strategy(ta.Strategy("My Strat", ta=[{"kind": "apo"}])) # Custom + +5. Working with kwargs +5a. Append the result to the working df. +>>> df.ta.apo(append=True) +5b. Timing an indicator. +>>> apo = df.ta(kind="apo", timed=True) +>>> print(apo.timed) +""" From b316e1064c5738a0e4dd4a3e2e268881f72de914 Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Fri, 16 Jul 2021 13:43:43 +0200 Subject: [PATCH 02/11] Initial tests ok --- pandas_ta/custom.py | 186 ++++++++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 83 deletions(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index d3f0a09..c5ede24 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -2,131 +2,151 @@ import os import sys -from os.path import dirname, abspath, join +from os.path import abspath, join, exists, basename, splitext from glob import glob import importlib import pandas_ta +import pandas as pd -def import_dir(dir_path, create_categories=True, verbose=True): +def create_dir(dir_path, create_categories=True, verbose=True): + """ + Helper function to setup a suitable folder structure for working with + custom indicators. + + Args: + dir_path (str): Full path to where you want your indicator tree + create_categories (bool): If True create category sub-folders + verbose (bool): If True verbose output of results + """ # ensure that the passed directory exists / is readable - if not os.path.exists(dir_path): - print(f"[X] Unable to read the directory '{dir_path}'.") - return - + if not exists(dir_path): + os.makedirs(dir_path) + if verbose: + print(f"[i] Created main directory '{dir_path}'.") + # list the contents of the directory dirs = glob(abspath(join(dir_path, '*'))) # optionally add any missing category subdirectories if create_categories: for sd in [*pandas_ta.Category]: - d = abspath(join(dir_path, sd) - if not os.path.exists(d): - os.makedirs(directory + d = abspath(join(dir_path, sd)) + if not exists(d): + os.makedirs(d) + if verbose: + dirname = basename(d) + print(f"[i] Created an empty sub-directory '{dirname}'.") - # traverse the directory, importing all modules found there +def import_dir(dir_path, verbose=True): + + # ensure that the passed directory exists / is readable + if not exists(dir_path): + print(f"[X] Unable to read the directory '{dir_path}'.") + return + + # obtain a list of all reserved pandas_ta indicator names + df = pd.DataFrame() + names_already_in_use = df.ta.indicators(as_list=True) + + # list the contents of the directory + dirs = glob(abspath(join(dir_path, '*'))) + + # traverse full directory, importing all modules found there for d in dirs: - dirname = os.path.basename(d) + dirname = basename(d) + # only look in directories which are valid pandas_ta categories if dirname not in [*pandas_ta.Category]: - print(f"[i] Skipping the sub-directory '{dirname}' since it's not a pandas_ta category.") + if verbose: + print(f"[i] Skipping the sub-directory '{dirname}' since it's not a valid pandas_ta category.") continue + # for each module found in that category (directory)... for module in glob(abspath(join(dir_path, dirname, '*.py'))): - module = os.path.splitext(os.path.basename(module))[0] + module = splitext(basename(module))[0] - if module in pandas_ta.Category[dirname]: - print(f"[X] Skipping the custom module '{module}' since a function with that name already exists in pandas_ta.") + # check that we only load modules not already loaded in pandas_ta + if module in names_already_in_use: + print(f"[i] Warning: the custom module '{module}' will replace a module in pandas_ta.") continue - # import the module and add it to the correct category - pandas_ta.Category[dirname].append(module) + # ensure that the supplied path is included in our python path if d not in sys.path: sys.path.append(d) + + # import the module and add it to the correct category importlib.import_module(module, d) + pandas_ta.Category[dirname].append(module) if verbose: - print(f"[i] Successfully imported the module '{module}' into category '{dirname}'.") + print(f"[i] Successfully imported the indicator '{module}' into category '{dirname}'.") import_dir.__doc__ = \ """ +Import a directory of custom indicators into pandas_ta + +Args: + dir_path (str): Full path to your indicator tree + verbose (bool): If True verbose output of results + This method allows you to experiment and develop your own technical analysis -indicators independantly in a separate local directory of your choice but -still use them seamlessly together with the existing pandas_ta functions just -like if they were part of pandas_ta. +indicators in a separate local directory of your choice but use them seamlessly +together with the existing pandas_ta functions just like if they were part of +pandas_ta. If you at some late point would like to push them into the pandas_ta library you can do so very easily by following the step by step instruction here https://github.com/twopirllc/pandas-ta/issues/264. ----------------------------------- - -By default, the 'ta' extension uses lower case column names: open, high, -low, close, and volume. You can override the defaults by providing the it's -replacement name when calling the indicator. For example, to call the -indicator hl2(). - -With 'default' columns: open, high, low, close, and volume. ->>> df.ta.hl2() ->>> df.ta(kind="hl2") - -With DataFrame columns: Open, High, Low, Close, and Volume. ->>> df.ta.hl2(high="High", low="Low") ->>> df.ta(kind="hl2", high="High", low="Low") - -If you do not want to use a DataFrame Extension, just call it normally. ->>> sma10 = ta.sma(df["Close"]) # Default length=10 ->>> sma50 = ta.sma(df["Close"], length=50) ->>> ichimoku, span = ta.ichimoku(df["High"], df["Low"], df["Close"]) - -Args: - kind (str, optional): Default: None. Kind is the 'name' of the indicator. - It converts kind to lowercase before calling. - timed (bool, optional): Default: False. Curious about the execution - speed? - kwargs: Extension specific modifiers. - append (bool, optional): Default: False. When True, it appends the - resultant column(s) to the DataFrame. - -Returns: - Most Indicators will return a Pandas Series. Others like MACD, BBANDS, - KC, et al will return a Pandas DataFrame. Ichimoku on the other hand - will return two DataFrames, the Ichimoku DataFrame for the known period - and a Span DataFrame for the future of the Span values. - Let's get started! 1. Loading the 'ta' module: >>> import pandas as pd >>> import ta as ta -2. Load some data: +2. Create an empty directory on your machine where you want to work with your +indicators. Invoke pandas_ta.custom.import_dir once to pre-populate it with +sub-folders for all available indicator categories, e.g.: + +>>> ta.custom.create_dir('~/my_indicators') + +3. You can now create your own custom indicator e.g. by copying existing +ones from pandas_ta core module and modifying them. Each custom indicator +should have a unique name and have both a function and a method defined. +For an example of the correct structure, look at the example ni.py in the +examples folder. + +The ni.py indicator is a trend indicator so we drop it into the sub-folder +named trend. Thus we have a folder structure like this: + +~/my_indicators/ +│ +├── candles/ +. +. +└── trend/ +. └── ni.py +. +└── volume/ + +4. We can now dynamically load all our custom indicators located in our +designated indicators directory like this: + +>>> ta.custom.import_dir('~/my_indicators') + +If your custom indicator loaded succesfully then it should behave exactly +like all other native indicators in pandas_ta. E.g. + +>>> help(ta.ni) +>>> help(df.ta.ni) >>> df = pd.read_csv("AAPL.csv", index_col="date", parse_dates=True) - -3. Help! -3a. General Help: ->>> help(df.ta) ->>> df.ta() -3b. Indicator Help: ->>> help(ta.apo) -3c. Indicator Extension Help: ->>> help(df.ta.apo) - -4. Ways of calling an indicator. -4a. Standard: Calling just the APO indicator without "ta" DataFrame extension. ->>> ta.apo(df["close"]) -4b. DataFrame Extension: Calling just the APO indicator with "ta" DataFrame extension. ->>> df.ta.apo() -4c. DataFrame Extension (kind): Calling APO using 'kind' ->>> df.ta(kind="apo") -4d. Strategy: +>>> ta.ni(df["close"]) +>>> df.ta.ni() +>>> df.ta(kind="ni") >>> df.ta.strategy("All") # Default ->>> df.ta.strategy(ta.Strategy("My Strat", ta=[{"kind": "apo"}])) # Custom - -5. Working with kwargs -5a. Append the result to the working df. ->>> df.ta.apo(append=True) -5b. Timing an indicator. ->>> apo = df.ta(kind="apo", timed=True) ->>> print(apo.timed) +>>> df.ta.strategy(ta.Strategy("My Strat", ta=[{"kind": "ni"}])) # Custom +>>> df.ta.ni(append=True) +>>> ni = df.ta(kind="ni", timed=True) +>>> print(ni.timed) """ From c1eaebbf9549007a234f1c57644180e800da13b1 Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Fri, 16 Jul 2021 15:10:32 +0200 Subject: [PATCH 03/11] Fixed names_alredy_in_use --- pandas_ta/custom.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index c5ede24..91b21ac 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -67,9 +67,8 @@ def import_dir(dir_path, verbose=True): module = splitext(basename(module))[0] # check that we only load modules not already loaded in pandas_ta - if module in names_already_in_use: - print(f"[i] Warning: the custom module '{module}' will replace a module in pandas_ta.") - continue + if verbose and module in names_already_in_use: + print(f"[i] Warning: the custom module '{module}' will replace a module currently loaded in pandas_ta.") # ensure that the supplied path is included in our python path if d not in sys.path: From e8b68f92496b33009d1e3af05bf391bd6788a3fe Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Fri, 16 Jul 2021 16:49:41 +0200 Subject: [PATCH 04/11] Added utility fcn bind --- pandas_ta/custom.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index 91b21ac..007e276 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -7,6 +7,7 @@ from glob import glob import importlib import pandas_ta import pandas as pd +from pandas_ta import AnalysisIndicators def create_dir(dir_path, create_categories=True, verbose=True): """ @@ -45,10 +46,6 @@ def import_dir(dir_path, verbose=True): print(f"[X] Unable to read the directory '{dir_path}'.") return - # obtain a list of all reserved pandas_ta indicator names - df = pd.DataFrame() - names_already_in_use = df.ta.indicators(as_list=True) - # list the contents of the directory dirs = glob(abspath(join(dir_path, '*'))) @@ -66,10 +63,6 @@ def import_dir(dir_path, verbose=True): for module in glob(abspath(join(dir_path, dirname, '*.py'))): module = splitext(basename(module))[0] - # check that we only load modules not already loaded in pandas_ta - if verbose and module in names_already_in_use: - print(f"[i] Warning: the custom module '{module}' will replace a module currently loaded in pandas_ta.") - # ensure that the supplied path is included in our python path if d not in sys.path: sys.path.append(d) @@ -149,3 +142,17 @@ like all other native indicators in pandas_ta. E.g. >>> ni = df.ta(kind="ni", timed=True) >>> print(ni.timed) """ + +def bind(function_name, function, method): + """ + Helper function to bind the function and class method defined in a custom + indicator module to the active pandas_ta instance. It is supposed to be + invoked last in all custom indicator modules. + + Args: + function_name (str): The name of the indicator within pandas_ta + function (fcn): The indicator function + method (fcn): The class method corresponding to the passed function + """ + setattr(pandas_ta, function_name, function) + setattr(AnalysisIndicators, function_name, method) \ No newline at end of file From b2baedf0d1f1c825448a1fc35a19fb772ac80fba Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Fri, 16 Jul 2021 17:45:00 +0200 Subject: [PATCH 05/11] Removed re-load test again --- pandas_ta/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index 007e276..d9dadb8 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -72,7 +72,7 @@ def import_dir(dir_path, verbose=True): pandas_ta.Category[dirname].append(module) if verbose: - print(f"[i] Successfully imported the indicator '{module}' into category '{dirname}'.") + print(f"[i] Successfully imported the custom indicator '{module}' into category '{dirname}'.") import_dir.__doc__ = \ """ From 54bfa24c0e7d314aac926cc76692137890c206fd Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Fri, 16 Jul 2021 18:54:05 +0200 Subject: [PATCH 06/11] Fixes in comments --- pandas_ta/custom.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index d9dadb8..3d4e192 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -95,22 +95,33 @@ Let's get started! 1. Loading the 'ta' module: >>> import pandas as pd ->>> import ta as ta +>>> import pandas_ta as ta 2. Create an empty directory on your machine where you want to work with your indicators. Invoke pandas_ta.custom.import_dir once to pre-populate it with sub-folders for all available indicator categories, e.g.: ->>> ta.custom.create_dir('~/my_indicators') +>>> import os +>>> from os.path import abspath, join, expanduser +>>> from pandas_ta.custom import create_dir, import_dir +>>> my_dir = abspath(join(expanduser("~"), "my_indicators")) +>>> create_dir(my_dir) 3. You can now create your own custom indicator e.g. by copying existing ones from pandas_ta core module and modifying them. Each custom indicator -should have a unique name and have both a function and a method defined. +should have a unique name and have both a function and a method defined +within the module. In essence these modules should look exactly like the +standard indicators available in categories under the pandas_ta-folder. + +The only difference will be an addition of a matching class method that +will be imported dynamically to the AnalysisIndicators class and a +call to the utility function that binds the indicator name to pandas_ta. + For an example of the correct structure, look at the example ni.py in the examples folder. -The ni.py indicator is a trend indicator so we drop it into the sub-folder -named trend. Thus we have a folder structure like this: +The ni.py indicator is a trend indicator so therefoe we drop it into the +sub-folder named trend. Thus we have a folder structure like this: ~/my_indicators/ │ @@ -125,22 +136,10 @@ named trend. Thus we have a folder structure like this: 4. We can now dynamically load all our custom indicators located in our designated indicators directory like this: ->>> ta.custom.import_dir('~/my_indicators') +>>> import_dir(my_dir) If your custom indicator loaded succesfully then it should behave exactly -like all other native indicators in pandas_ta. E.g. - ->>> help(ta.ni) ->>> help(df.ta.ni) ->>> df = pd.read_csv("AAPL.csv", index_col="date", parse_dates=True) ->>> ta.ni(df["close"]) ->>> df.ta.ni() ->>> df.ta(kind="ni") ->>> df.ta.strategy("All") # Default ->>> df.ta.strategy(ta.Strategy("My Strat", ta=[{"kind": "ni"}])) # Custom ->>> df.ta.ni(append=True) ->>> ni = df.ta(kind="ni", timed=True) ->>> print(ni.timed) +like all other native indicators in pandas_ta, including help functions. """ def bind(function_name, function, method): From 32c91686a5fedef745d2da52ed2d9aa11c507a1c Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Fri, 16 Jul 2021 18:54:56 +0200 Subject: [PATCH 07/11] An example trend indicator --- examples/ni.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 examples/ni.py diff --git a/examples/ni.py b/examples/ni.py new file mode 100644 index 0000000..0431946 --- /dev/null +++ b/examples/ni.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +from pandas_ta.overlap import sma +from pandas_ta.utils import get_offset, verify_series + +# - Standard definition of your custom indicator function (including docs)- + +def ni(close, length=None, centered=False, offset=None, **kwargs): + """ + Example indicator ni + """ + # Validate Arguments + length = int(length) if length and length > 0 else 20 + close = verify_series(close, length) + offset = get_offset(offset) + + if close is None: return + + # Calculate Result + t = int(0.5 * length) + 1 + ma = sma(close, length) + + ni = close - ma.shift(t) + if centered: + ni = (close.shift(t) - ma).shift(-t) + + # Offset + if offset != 0: + ni = ni.shift(offset) + + # Handle fills + if "fillna" in kwargs: + ni.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + ni.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + ni.name = f"ni_{length}" + ni.category = "trend" + + return ni + +ni.__doc__ = \ +"""Example indicator (NI) + +Is an indicator provided solely as an example + +Sources: + https://github.com/twopirllc/pandas-ta/issues/264 + +Calculation: + Default Inputs: + length=20, centered=False + SMA = Simple Moving Average + t = int(0.5 * length) + 1 + + ni = close.shift(t) - SMA(close, length) + if centered: + ni = ni.shift(-t) + +Args: + close (pd.Series): Series of 'close's + length (int): It's period. Default: 20 + centered (bool): Shift the ni back by int(0.5 * length) + 1. 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. +""" + +# - Define a matching class method -------------------------------------------- + +# NOTE: we need to temporarily use another name for the method than for the +# function to avoid a name conflict in this module. But by using the bind +# function below we can name it right so that doesn't really matter. +# Just remember to rename the method if you at a later stage decide to move it +# into the core.py module inside pandas_ta +def ni_method(self, length=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = ni(close=close, length=length, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + +# - Bind the function to pandas_ta and the method to AnalysisIndicators ------- + +from pandas_ta.custom import bind +bind('ni', ni, ni_method) \ No newline at end of file From 35b896a98a14c836cb47d7a4b537b7dd4f3a4f1a Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Mon, 19 Jul 2021 15:48:00 +0200 Subject: [PATCH 08/11] Module re-load implemented --- pandas_ta/custom.py | 97 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 14 deletions(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index 3d4e192..3699c1b 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -4,6 +4,7 @@ import os import sys from os.path import abspath, join, exists, basename, splitext from glob import glob +import types import importlib import pandas_ta import pandas as pd @@ -61,16 +62,32 @@ def import_dir(dir_path, verbose=True): # for each module found in that category (directory)... for module in glob(abspath(join(dir_path, dirname, '*.py'))): - module = splitext(basename(module))[0] + module_name = splitext(basename(module))[0] # ensure that the supplied path is included in our python path if d not in sys.path: - sys.path.append(d) + sys.path.append(d) - # import the module and add it to the correct category - importlib.import_module(module, d) - pandas_ta.Category[dirname].append(module) + # (re)load the indicator module + module_functions = load_indicator_module(module_name) + # figure out which of the modules functions to bind to pandas_ta + fcn_callable = module_functions.get(module_name, None) + fcn_method_callable = module_functions.get(module_name + "_method", None) + + if fcn_callable == None: + print(f"[X] Unable to find a function named '{module_name}' in the module '{module_name}.py'.") + continue + if fcn_method_callable == None: + missing_method = module_name + "_method" + print(f"[X] Unable to find a method function named '{missing_method}' in the module '{module_name}.py'.") + continue + + # add it to the correct category if it's not there yet + if module_name not in pandas_ta.Category[dirname]: + pandas_ta.Category[dirname].append(module_name) + + bind(module_name, fcn_callable, fcn_method_callable) if verbose: print(f"[i] Successfully imported the custom indicator '{module}' into category '{dirname}'.") @@ -108,19 +125,21 @@ sub-folders for all available indicator categories, e.g.: >>> create_dir(my_dir) 3. You can now create your own custom indicator e.g. by copying existing -ones from pandas_ta core module and modifying them. Each custom indicator -should have a unique name and have both a function and a method defined -within the module. In essence these modules should look exactly like the -standard indicators available in categories under the pandas_ta-folder. +ones from pandas_ta core module and modifying them. -The only difference will be an addition of a matching class method that -will be imported dynamically to the AnalysisIndicators class and a -call to the utility function that binds the indicator name to pandas_ta. +IMPORTANT: Each custom indicator should have a unique name and have both +a) a function named exactly as the module, e.g. 'ni' if the module is ni.py +b) a matching method used by AnalysisIndicators named as the module but + ending with '_method'. E.g. 'ni_method' + +In essence these modules should look exactly like the standard indicators +available in categories under the pandas_ta-folder. The only difference will +be an addition of a matching class method. For an example of the correct structure, look at the example ni.py in the examples folder. -The ni.py indicator is a trend indicator so therefoe we drop it into the +The ni.py indicator is a trend indicator so therefore we drop it into the sub-folder named trend. Thus we have a folder structure like this: ~/my_indicators/ @@ -154,4 +173,54 @@ def bind(function_name, function, method): method (fcn): The class method corresponding to the passed function """ setattr(pandas_ta, function_name, function) - setattr(AnalysisIndicators, function_name, method) \ No newline at end of file + setattr(AnalysisIndicators, function_name, method) + +def load_indicator_module(module_name): + """ + Helper function to (re)load an indicator module. + + Returns: + dict: module functions mapping + { + "func1_name": func, + "func2_name": func2 + . + . + . + } + + """ + # load module + try: + module = importlib.import_module(module_name) + except Exception as ex: + print(f"[X] An error occurred when attempting to load module {module_name}: {ex}") + sys.exit(1) + + # reload to refresh previously loaded module + module = importlib.reload(module) + return get_module_functions(module) + +def get_module_functions(module): + """ + Helper function to get the functions of an imported module as a dictionary. + + Args: + module: python module + + Returns: + dict: functions mapping for specified python module + + { + "func1_name": func1, + "func2_name": func2 + } + + """ + module_functions = {} + + for name, item in vars(module).items(): + if isinstance(item, types.FunctionType): + module_functions[name] = item + + return module_functions \ No newline at end of file From 25560d0da202fafbef96ff997d5b86c144f4c6d6 Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Mon, 19 Jul 2021 15:52:12 +0200 Subject: [PATCH 09/11] Removed the need to call bind explicitly --- examples/ni.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/examples/ni.py b/examples/ni.py index 0431946..41aedc0 100644 --- a/examples/ni.py +++ b/examples/ni.py @@ -73,17 +73,7 @@ Returns: # - Define a matching class method -------------------------------------------- -# NOTE: we need to temporarily use another name for the method than for the -# function to avoid a name conflict in this module. But by using the bind -# function below we can name it right so that doesn't really matter. -# Just remember to rename the method if you at a later stage decide to move it -# into the core.py module inside pandas_ta def ni_method(self, length=None, offset=None, **kwargs): close = self._get_column(kwargs.pop("close", "close")) result = ni(close=close, length=length, offset=offset, **kwargs) - return self._post_process(result, **kwargs) - -# - Bind the function to pandas_ta and the method to AnalysisIndicators ------- - -from pandas_ta.custom import bind -bind('ni', ni, ni_method) \ No newline at end of file + return self._post_process(result, **kwargs) \ No newline at end of file From 1d237ad6844406633ff325323e31e352c904eae6 Mon Sep 17 00:00:00 2001 From: Urban Ottosson Date: Sat, 24 Jul 2021 18:57:16 +0200 Subject: [PATCH 10/11] Updated comments --- pandas_ta/custom.py | 88 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index 3699c1b..51a23a6 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -10,36 +10,6 @@ import pandas_ta import pandas as pd from pandas_ta import AnalysisIndicators -def create_dir(dir_path, create_categories=True, verbose=True): - """ - Helper function to setup a suitable folder structure for working with - custom indicators. - - Args: - dir_path (str): Full path to where you want your indicator tree - create_categories (bool): If True create category sub-folders - verbose (bool): If True verbose output of results - """ - - # ensure that the passed directory exists / is readable - if not exists(dir_path): - os.makedirs(dir_path) - if verbose: - print(f"[i] Created main directory '{dir_path}'.") - - # list the contents of the directory - dirs = glob(abspath(join(dir_path, '*'))) - - # optionally add any missing category subdirectories - if create_categories: - for sd in [*pandas_ta.Category]: - d = abspath(join(dir_path, sd)) - if not exists(d): - os.makedirs(d) - if verbose: - dirname = basename(d) - print(f"[i] Created an empty sub-directory '{dirname}'.") - def import_dir(dir_path, verbose=True): # ensure that the passed directory exists / is readable @@ -108,7 +78,7 @@ If you at some late point would like to push them into the pandas_ta library you can do so very easily by following the step by step instruction here https://github.com/twopirllc/pandas-ta/issues/264. -Let's get started! +A brief example of usage: 1. Loading the 'ta' module: >>> import pandas as pd @@ -157,15 +127,14 @@ designated indicators directory like this: >>> import_dir(my_dir) -If your custom indicator loaded succesfully then it should behave exactly +If your custom indicator(s) loaded succesfully then it should behave exactly like all other native indicators in pandas_ta, including help functions. """ def bind(function_name, function, method): """ Helper function to bind the function and class method defined in a custom - indicator module to the active pandas_ta instance. It is supposed to be - invoked last in all custom indicator modules. + indicator module to the active pandas_ta instance. Args: function_name (str): The name of the indicator within pandas_ta @@ -182,11 +151,8 @@ def load_indicator_module(module_name): Returns: dict: module functions mapping { - "func1_name": func, - "func2_name": func2 - . - . - . + "func1_name": func1, + "func2_name": func2,... } """ @@ -209,12 +175,11 @@ def get_module_functions(module): module: python module Returns: - dict: functions mapping for specified python module - - { - "func1_name": func1, - "func2_name": func2 - } + dict: module functions mapping + { + "func1_name": func1, + "func2_name": func2,... + } """ module_functions = {} @@ -223,4 +188,35 @@ def get_module_functions(module): if isinstance(item, types.FunctionType): module_functions[name] = item - return module_functions \ No newline at end of file + return module_functions + +def create_dir(dir_path, create_categories=True, verbose=True): + """ + Helper function to setup a suitable folder structure for working with + custom indicators. You only need to call this once whenever you want to + setup a new custom indicators folder. + + Args: + dir_path (str): Full path to where you want your indicator tree + create_categories (bool): If True create category sub-folders + verbose (bool): If True print verbose output of results + """ + + # ensure that the passed directory exists / is readable + if not exists(dir_path): + os.makedirs(dir_path) + if verbose: + print(f"[i] Created main directory '{dir_path}'.") + + # list the contents of the directory + dirs = glob(abspath(join(dir_path, '*'))) + + # optionally add any missing category subdirectories + if create_categories: + for sd in [*pandas_ta.Category]: + d = abspath(join(dir_path, sd)) + if not exists(d): + os.makedirs(d) + if verbose: + dirname = basename(d) + print(f"[i] Created an empty sub-directory '{dirname}'.") \ No newline at end of file From e68f84017a0be71cd6248752f87cfa528716d0df Mon Sep 17 00:00:00 2001 From: Kevin Johnson Date: Mon, 26 Jul 2021 14:27:53 -0700 Subject: [PATCH 11/11] MAINT minor refactoring --- pandas_ta/custom.py | 175 ++++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 86 deletions(-) diff --git a/pandas_ta/custom.py b/pandas_ta/custom.py index 51a23a6..1854249 100644 --- a/pandas_ta/custom.py +++ b/pandas_ta/custom.py @@ -1,24 +1,92 @@ # -*- coding: utf-8 -*- - +import importlib import os import sys from os.path import abspath, join, exists, basename, splitext from glob import glob import types -import importlib import pandas_ta import pandas as pd from pandas_ta import AnalysisIndicators -def import_dir(dir_path, verbose=True): + +def bind(function_name, function, method): + """ + Helper function to bind the function and class method defined in a custom + indicator module to the active pandas_ta instance. + + Args: + function_name (str): The name of the indicator within pandas_ta + function (fcn): The indicator function + method (fcn): The class method corresponding to the passed function + """ + setattr(pandas_ta, function_name, function) + setattr(AnalysisIndicators, function_name, method) + + +def create_dir(path, create_categories=True, verbose=True): + """ + Helper function to setup a suitable folder structure for working with + custom indicators. You only need to call this once whenever you want to + setup a new custom indicators folder. + + Args: + path (str): Full path to where you want your indicator tree + create_categories (bool): If True create category sub-folders + verbose (bool): If True print verbose output of results + """ # ensure that the passed directory exists / is readable - if not exists(dir_path): - print(f"[X] Unable to read the directory '{dir_path}'.") + if not exists(path): + os.makedirs(path) + if verbose: + print(f"[i] Created main directory '{path}'.") + + # list the contents of the directory + # dirs = glob(abspath(join(path, '*'))) + + # optionally add any missing category subdirectories + if create_categories: + for sd in [*pandas_ta.Category]: + d = abspath(join(path, sd)) + if not exists(d): + os.makedirs(d) + if verbose: + dirname = basename(d) + print(f"[i] Created an empty sub-directory '{dirname}'.") + + +def get_module_functions(module): + """ + Helper function to get the functions of an imported module as a dictionary. + + Args: + module: python module + + Returns: + dict: module functions mapping + { + "func1_name": func1, + "func2_name": func2,... + } + """ + module_functions = {} + + for name, item in vars(module).items(): + if isinstance(item, types.FunctionType): + module_functions[name] = item + + return module_functions + + +def import_dir(path, verbose=True): + # ensure that the passed directory exists / is readable + if not exists(path): + print(f"[X] Unable to read the directory '{path}'.") return # list the contents of the directory - dirs = glob(abspath(join(dir_path, '*'))) + dirs = glob(abspath(join(path, '*'))) # traverse full directory, importing all modules found there for d in dirs: @@ -31,20 +99,20 @@ def import_dir(dir_path, verbose=True): continue # for each module found in that category (directory)... - for module in glob(abspath(join(dir_path, dirname, '*.py'))): + for module in glob(abspath(join(path, dirname, '*.py'))): module_name = splitext(basename(module))[0] # ensure that the supplied path is included in our python path if d not in sys.path: sys.path.append(d) - # (re)load the indicator module + # (re)load the indicator module module_functions = load_indicator_module(module_name) # figure out which of the modules functions to bind to pandas_ta fcn_callable = module_functions.get(module_name, None) fcn_method_callable = module_functions.get(module_name + "_method", None) - + if fcn_callable == None: print(f"[X] Unable to find a function named '{module_name}' in the module '{module_name}.py'.") continue @@ -61,17 +129,18 @@ def import_dir(dir_path, verbose=True): if verbose: print(f"[i] Successfully imported the custom indicator '{module}' into category '{dirname}'.") + import_dir.__doc__ = \ """ Import a directory of custom indicators into pandas_ta Args: - dir_path (str): Full path to your indicator tree + path (str): Full path to your indicator tree verbose (bool): If True verbose output of results This method allows you to experiment and develop your own technical analysis -indicators in a separate local directory of your choice but use them seamlessly -together with the existing pandas_ta functions just like if they were part of +indicators in a separate local directory of your choice but use them seamlessly +together with the existing pandas_ta functions just like if they were part of pandas_ta. If you at some late point would like to push them into the pandas_ta library @@ -85,7 +154,7 @@ A brief example of usage: >>> import pandas_ta as ta 2. Create an empty directory on your machine where you want to work with your -indicators. Invoke pandas_ta.custom.import_dir once to pre-populate it with +indicators. Invoke pandas_ta.custom.import_dir once to pre-populate it with sub-folders for all available indicator categories, e.g.: >>> import os @@ -94,22 +163,22 @@ sub-folders for all available indicator categories, e.g.: >>> my_dir = abspath(join(expanduser("~"), "my_indicators")) >>> create_dir(my_dir) -3. You can now create your own custom indicator e.g. by copying existing -ones from pandas_ta core module and modifying them. +3. You can now create your own custom indicator e.g. by copying existing +ones from pandas_ta core module and modifying them. -IMPORTANT: Each custom indicator should have a unique name and have both +IMPORTANT: Each custom indicator should have a unique name and have both a) a function named exactly as the module, e.g. 'ni' if the module is ni.py b) a matching method used by AnalysisIndicators named as the module but ending with '_method'. E.g. 'ni_method' -In essence these modules should look exactly like the standard indicators +In essence these modules should look exactly like the standard indicators available in categories under the pandas_ta-folder. The only difference will be an addition of a matching class method. -For an example of the correct structure, look at the example ni.py in the +For an example of the correct structure, look at the example ni.py in the examples folder. -The ni.py indicator is a trend indicator so therefore we drop it into the +The ni.py indicator is a trend indicator so therefore we drop it into the sub-folder named trend. Thus we have a folder structure like this: ~/my_indicators/ @@ -131,18 +200,6 @@ If your custom indicator(s) loaded succesfully then it should behave exactly like all other native indicators in pandas_ta, including help functions. """ -def bind(function_name, function, method): - """ - Helper function to bind the function and class method defined in a custom - indicator module to the active pandas_ta instance. - - Args: - function_name (str): The name of the indicator within pandas_ta - function (fcn): The indicator function - method (fcn): The class method corresponding to the passed function - """ - setattr(pandas_ta, function_name, function) - setattr(AnalysisIndicators, function_name, method) def load_indicator_module(module_name): """ @@ -165,58 +222,4 @@ def load_indicator_module(module_name): # reload to refresh previously loaded module module = importlib.reload(module) - return get_module_functions(module) - -def get_module_functions(module): - """ - Helper function to get the functions of an imported module as a dictionary. - - Args: - module: python module - - Returns: - dict: module functions mapping - { - "func1_name": func1, - "func2_name": func2,... - } - - """ - module_functions = {} - - for name, item in vars(module).items(): - if isinstance(item, types.FunctionType): - module_functions[name] = item - - return module_functions - -def create_dir(dir_path, create_categories=True, verbose=True): - """ - Helper function to setup a suitable folder structure for working with - custom indicators. You only need to call this once whenever you want to - setup a new custom indicators folder. - - Args: - dir_path (str): Full path to where you want your indicator tree - create_categories (bool): If True create category sub-folders - verbose (bool): If True print verbose output of results - """ - - # ensure that the passed directory exists / is readable - if not exists(dir_path): - os.makedirs(dir_path) - if verbose: - print(f"[i] Created main directory '{dir_path}'.") - - # list the contents of the directory - dirs = glob(abspath(join(dir_path, '*'))) - - # optionally add any missing category subdirectories - if create_categories: - for sd in [*pandas_ta.Category]: - d = abspath(join(dir_path, sd)) - if not exists(d): - os.makedirs(d) - if verbose: - dirname = basename(d) - print(f"[i] Created an empty sub-directory '{dirname}'.") \ No newline at end of file + return get_module_functions(module) \ No newline at end of file