├── .gitignore ├── AUTHOR ├── COPYRIGHT ├── LICENSE ├── README.rst ├── btalib ├── __init__.py ├── config.py ├── errors.py ├── indicator.py ├── indicators │ ├── __init__.py │ ├── ad.py │ ├── aroon.py │ ├── atr.py │ ├── bbands.py │ ├── beta.py │ ├── bop.py │ ├── cci.py │ ├── cmo.py │ ├── correl.py │ ├── crossover.py │ ├── dema.py │ ├── directionalmove.py │ ├── ema.py │ ├── ewma.py │ ├── ht_dcperiod.py │ ├── ht_dcphase.py │ ├── ht_phasor.py │ ├── ht_sine.py │ ├── ht_trendline.py │ ├── ht_trendmode.py │ ├── kama.py │ ├── linreg.py │ ├── macd.py │ ├── madev.py │ ├── mama.py │ ├── math.py │ ├── mathop.py │ ├── mavp.py │ ├── mfi.py │ ├── midpoint.py │ ├── mom.py │ ├── obv.py │ ├── ppo.py │ ├── price.py │ ├── roc.py │ ├── rsi.py │ ├── sar.py │ ├── sarext.py │ ├── sma.py │ ├── smma.py │ ├── stddev.py │ ├── stochastic.py │ ├── stochrsi.py │ ├── t3.py │ ├── tema.py │ ├── trima.py │ ├── trix.py │ ├── ultimateoscillator.py │ ├── var.py │ ├── williamsr.py │ └── wma.py ├── meta │ ├── __init__.py │ ├── aliases.py │ ├── docs.py │ ├── groups.py │ ├── inputs.py │ ├── lines.py │ ├── linesholder.py │ ├── linesops.py │ ├── metadata.py │ ├── outputs.py │ └── params.py ├── utils.py └── version.py ├── changelog.md ├── data └── 2006-day-001.txt ├── pypi.sh ├── requirements-test.txt ├── requirements.txt ├── setup.py └── tests ├── test_indicators.py ├── test_linesholder.py ├── test_outputs.py ├── test_series_fetcher.py └── testcommon.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # Backups 57 | *.bak 58 | *~ 59 | .#* 60 | *# 61 | *.swp 62 | *.swo 63 | 64 | .ipynb* 65 | samples2/ 66 | -------------------------------------------------------------------------------- /AUTHOR: -------------------------------------------------------------------------------- 1 | Project Founder and Lead Developer: Daniel Rodriguez (https://github.com/mementum) 2 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Daniel Rodriguez 2 | This projct is governed by the MIT License 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniel Rodriguez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``bta-lib`` - A ``pandas`` based *Technical Analysis Library* 2 | ============================================================= 3 | 4 | .. image:: https://img.shields.io/pypi/v/bta-lib.svg 5 | :alt: PyPi Version 6 | :scale: 100% 7 | :target: https://pypi.python.org/pypi/bta-lib/ 8 | 9 | .. image:: https://img.shields.io/github/release/mementum/bta-lib.svg 10 | :alt: Release 11 | :scale: 100% 12 | :target: https://github.com/mementum/bta-lib/releases/ 13 | 14 | ``bta-lib`` is ``pandas`` based technical analysis library and part of the 15 | ``backtrader`` family. 16 | 17 | Links 18 | ----- 19 | 20 | - Main Page: https://btalib.backtrader.com/ 21 | - Documentation: https://btalib.backtrader.com/introduction/ 22 | - Community: https://community.backtrader.com/category/10/bta-lib 23 | - GitHub Repo: https://github.com/mementum/bta-lib 24 | 25 | Installation 26 | ------------ 27 | 28 | Via ``pip``:: 29 | 30 | pip install bta-lib 31 | 32 | **Remember**: ``pandas`` is a requirement. Read the docs!!! 33 | -------------------------------------------------------------------------------- /btalib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from .version import * # noqa: F401 F403 8 | 9 | from .utils import * # noqa: F401 F403 10 | 11 | from .meta import * # noqa: F401 F403 12 | 13 | from .indicator import * # noqa: F401 F403 14 | 15 | from .indicators import * # noqa: F401 F403 16 | from . import indicators as ind # noqa: F401 17 | -------------------------------------------------------------------------------- /btalib/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | 8 | __all__ = [] 9 | 10 | 11 | # Standard ordering of OHLCV_OI fields 12 | OHLC_INDICES = { 13 | 'open': 0, 14 | 'high': 1, 15 | 'low': 2, 16 | 'close': 3, 17 | 'volume': 4, 18 | 'openinterest': 5, 19 | } 20 | 21 | OHLC_FIRST = False 22 | 23 | 24 | def set_use_ohlc_indices_first(onoff=True): 25 | global OHLC_FIRST 26 | OHLC_FIRST = onoff 27 | 28 | 29 | def set_input_indices(**kwargs): 30 | OHLC_INDICES.update(kwargs) 31 | 32 | 33 | def get_input_indices(): 34 | return OHLC_INDICES.copy() 35 | 36 | 37 | RETVAL = '' # can be 'dataframe', 'df' 38 | 39 | 40 | def set_return(val): 41 | global RETVAL 42 | RETVAL = val 43 | 44 | 45 | def set_return_dataframe(): 46 | global RETVAL 47 | RETVAL = 'df' 48 | 49 | 50 | def get_return(): 51 | return RETVAL 52 | 53 | 54 | def get_return_dataframe(): 55 | return RETVAL in ['df', 'dataframe'] 56 | 57 | 58 | TALIB_COMPAT = False # global flag for ta-lib compatibility 59 | 60 | 61 | def set_talib_compat(onoff=True): 62 | global TALIB_COMPAT 63 | TALIB_COMPAT = onoff 64 | 65 | 66 | def get_talib_compat(): 67 | return TALIB_COMPAT 68 | -------------------------------------------------------------------------------- /btalib/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | __all__ = ['TaPyError', 'InputsError', ] 8 | 9 | 10 | class TaPyError(Exception): 11 | pass 12 | 13 | 14 | class InputsError(TaPyError): 15 | pass 16 | 17 | 18 | def OneInputNeededZeroProvided(): 19 | errmsg = 'One (1) input is at least needed and 0 were provided' 20 | raise InputsError(errmsg) 21 | 22 | 23 | def MultiDimSmall(): 24 | errmsg = ( 25 | 'The multidimensional input size is smaller than the number of needed ' 26 | 'inputs' 27 | ) 28 | raise InputsError(errmsg) 29 | 30 | 31 | def MultiDimType(): 32 | errmsg = ( 33 | 'Only DataFrames or library/user indicators are accepted as ' 34 | 'multi-dimensional inputs' 35 | ) 36 | raise InputsError(errmsg) 37 | 38 | 39 | def PandasNotTopStack(): 40 | errmsg = ( 41 | 'Pandas DataFrames only valid as input from outside of the library' 42 | ) 43 | raise InputsError(errmsg) 44 | 45 | 46 | def ColdIndexStrNotFound(name, rename): 47 | errmsg = ( 48 | 'OHLC Column Name "{}" remapped to "{}", but this cannot be found' 49 | ) 50 | raise InputsError(errmsg.format(name, rename)) 51 | -------------------------------------------------------------------------------- /btalib/indicator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import collections 8 | 9 | from . import config 10 | from . import meta 11 | from .meta import metadata 12 | 13 | __all__ = [ 14 | 'get_indicators', 'get_ind_names', 'get_ind_by_name', 15 | 'get_ind_by_group', 'get_ind_names_by_group', 'get_groups', 16 | ] 17 | 18 | 19 | metadata.callstack = [] # keep indicators which are currently being executed 20 | 21 | 22 | _NAMES_IND = {} 23 | _IND_NAMES = {} 24 | _GRP_IND = collections.defaultdict(list) 25 | 26 | 27 | def get_indicators(): 28 | return [_NAMES_IND[name] for name in sorted(_NAMES_IND)] 29 | 30 | 31 | def get_ind_names(): 32 | return sorted(_NAMES_IND) 33 | 34 | 35 | def get_ind_by_name(): 36 | return dict(sorted(_NAMES_IND.items())) 37 | 38 | 39 | def get_ind_by_group(): 40 | return dict(_GRP_IND) 41 | 42 | 43 | def get_ind_names_by_group(): 44 | return {c: sorted(_IND_NAMES[i] for i in il) for c, il in _GRP_IND.items()} 45 | 46 | 47 | def get_groups(): 48 | return list(_GRP_IND) 49 | 50 | 51 | class MetaIndicator(meta.linesholder.LinesHolder.__class__): 52 | # The metaclass takes care of parsing the appropriate defintions during 53 | # class creation (alias, lines, ...) and properly definiing them if needed 54 | # in the final class (considering the bases) 55 | 56 | def __new__(metacls, name, bases, dct, **kwargs): 57 | # Creates class, gathers all lines definitions, installs aliases and 58 | # auto-exports itself and the aliases 59 | cls = super().__new__(metacls, name, bases, dct) # create 60 | 61 | meta.aliases._generate(cls, bases, dct, **kwargs) 62 | meta.inputs._generate(cls, bases, dct, **kwargs) 63 | meta.outputs._generate(cls, bases, dct, **kwargs) 64 | meta.params._generate(cls, bases, dct, **kwargs) 65 | meta.docs._generate(cls, bases, dct, **kwargs) 66 | meta.groups._generate(cls, bases, dct, **kwargs) 67 | 68 | modsplit = cls.__module__.split('.') 69 | to_register = modsplit[0] == __package__ and len(modsplit) > 2 70 | if to_register and not name.startswith('_'): 71 | _NAMES_IND[name] = cls 72 | _IND_NAMES[cls] = name 73 | 74 | for grp in cls.group: 75 | _GRP_IND[grp].append(cls) 76 | 77 | return cls # return newly created and patched class 78 | 79 | def __call__(cls, *args, **kwargs): 80 | # In charge of object creation and initialization. 81 | # Parses and assigns declared parameters 82 | # Adds auto-magical 83 | # member attributes before __init__ is given a change to do 84 | # something. Any subclass with something to do in __init__ will already 85 | # be able to access the auto-magical attributes 86 | self = cls.__new__(cls, *args, *kwargs) # create instance as usual 87 | 88 | # Determine base classes for auto-calling 89 | # Non-overridden functions will be filtered with list(dict.fromkeys), 90 | # which removes duplicates and retains order 91 | bases, bcls = [], cls 92 | while(bcls != Indicator): 93 | bcls = bases.append(bcls) or bcls.__bases__[0] # append rets None 94 | 95 | bases.append(bcls) # append Indicator which defines neutral methods 96 | 97 | # check if ta-lib compatibility is requestd 98 | talibflag = kwargs.pop('_talib', False) or config.get_talib_compat() 99 | 100 | # Check if ta-lib compatibility is requested. If so and the indicator 101 | # defines a _talib function, give it the **ACTUAL** kwargs and use the 102 | # modified version. Don't let a '_talib' parameter make it to the 103 | # indicator (hence pop) 104 | if talibflag: 105 | talibclass = list(dict.fromkeys(b._talib_class for b in bases)) 106 | for b_ta in reversed(talibclass): 107 | b_ta(kwargs) 108 | 109 | # Create and install the lines holding instance 110 | self.outputs = self.o = meta.outputs._from_class(cls) 111 | self.lines = self.l = self.outputs # noqa: E741 112 | 113 | # Get inputs and remaining args 114 | self.inputs, args = meta.inputs._from_args(cls, *args) 115 | self.i = self.inputs # shorthand 116 | 117 | # Add array of data feeds ... the s in "datas" to indicate multiple 118 | self.datas = self.d = list(self.inputs) 119 | self.data = self.datas[0] # add main alias to 1st data 120 | 121 | # add direct aliases with numeric index 122 | for i, _in in enumerate(self.inputs): 123 | for inalias in ('i{}', 'input{}', 'd{}', 'data{}'): 124 | setattr(self, inalias.format(i), _in) 125 | 126 | # add direct aliases with naming index 127 | for i, _in in enumerate(self.inputs.__slots__): 128 | for inalias in ('i_{}', 'input_{}', 'd_{}', 'data_{}'): 129 | setattr(self, inalias.format(i), _in) 130 | 131 | # Gather minimum periods and get the dominant mininum period 132 | self._minperiods = [_in._minperiod for _in in self.inputs] 133 | self._minperiod = max(self._minperiods) 134 | 135 | # Check if ta-lib compatibility is requested. If so and the indicator 136 | # defines a _talib function, give it the **ACTUAL** kwargs and use the 137 | # modified version. Don't let a '_talib' parameter make it to the 138 | # indicator (hence pop) 139 | if talibflag: 140 | for b_ta in reversed(list(dict.fromkeys(b._talib for b in bases))): 141 | b_ta(self, kwargs) 142 | 143 | # Get params instance and remaining kwargs 144 | self.params, kwargs = meta.params._from_kwargs(cls, **kwargs) 145 | self.p = self.params # shorthand 146 | 147 | # All boilerplate is done, to into execution mode 148 | metadata.callstack.append(self) # let ind know hwere in the stack 149 | 150 | # Auto-call base classes 151 | for b_init in reversed(list(dict.fromkeys(b.__init__ for b in bases))): 152 | b_init(self, *args, **kwargs) 153 | 154 | # delete old aliases only meant for operational purposes 155 | for oalias in ('l', 'lines', 'data', 'd', 'datas'): 156 | delattr(self, oalias) 157 | 158 | # remove direct aliases with numeric index 159 | for i, _in in enumerate(self.inputs): 160 | for inalias in ('d{}', 'data{}'): 161 | delattr(self, inalias.format(i)) 162 | 163 | # remove direct aliases with naming index 164 | for i, _in in enumerate(self.inputs.__slots__): 165 | for inalias in ('d_{}', 'data_{}'): 166 | delattr(self, inalias.format(i)) 167 | 168 | # update min periods of lines after calculations and replicate 169 | self.outputs._update_minperiod() 170 | self._minperiods = self.outputs._minperiods 171 | self._minperiod = self.outputs._minperiod 172 | 173 | metadata.callstack.pop() # let ind know hwere in the stack 174 | 175 | # set def return value, but consider stack depth and user pref 176 | ret = self 177 | if not metadata.callstack: # top-of the stack, ret following prefs 178 | if config.get_return_dataframe(): 179 | ret = self.df 180 | 181 | return ret # Return itself for now 182 | 183 | def _regenerate_inputs(cls, inputs): 184 | meta.inputs._generate(cls, cls.__bases__, {'inputs': inputs}) 185 | 186 | 187 | class Indicator(meta.linesholder.LinesHolder, metaclass=MetaIndicator): 188 | # Base class for any indicator. The heavy lifting to ensure consistency is 189 | # done by the metaclass. 190 | 191 | _minperiod = 1 192 | _minperiods = [1] 193 | 194 | inputs = ('close',) # default input to look for 195 | 196 | def __init__(self, *args, **kwargs): 197 | # The goal of having an empty __init__ is to avoid passing any 198 | # arg/kwargs to object.__init__ which would generate an error 199 | pass 200 | 201 | _talib_ = False 202 | 203 | def _talib(self, kwdict): 204 | self._talib_ = True # for subclasses to use if needed 205 | 206 | @classmethod 207 | def _talib_class(cls, kwdict): 208 | pass 209 | -------------------------------------------------------------------------------- /btalib/indicators/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from numpy import nan as NaN # noqa: F401 8 | 9 | # Internal objects to work in INdicator development 10 | from .. import Indicator # noqa: F401 11 | from ..utils import * # noqa: F401 F403 12 | 13 | # Price Transform 14 | from .price import * # noqa: F401 F403 15 | 16 | # Math Operators 17 | from .mathop import * # noqa: F401 F403 18 | 19 | # Math Transform 20 | from .math import * # noqa: F401 F403 21 | 22 | # Utils 23 | from .crossover import * # noqa: F401 F403 24 | 25 | # Overlap 26 | from .ewma import * # noqa: F401 F403 27 | 28 | from .sma import * # noqa: F401 F403 29 | from .ema import * # noqa: F401 F403 30 | from .smma import * # noqa: F401 F403 31 | from .wma import * # noqa: F401 F403 32 | 33 | from .dema import * # noqa: F401 F403 34 | from .kama import * # noqa: F401 F403 35 | from .tema import * # noqa: F401 F403 36 | from .trima import * # noqa: F401 F403 37 | from .trix import * # noqa: F401 F403 38 | from .t3 import * # noqa: F401 F403 39 | 40 | from .mavp import * # noqa: F401 F403 41 | 42 | from .mama import * # noqa: F401 F403 43 | from .ht_trendline import * # noqa: F401 F403 44 | 45 | # ## overlap non-ma 46 | from .midpoint import * # noqa: F401 F403 47 | 48 | # Cycle 49 | from .ht_dcperiod import * # noqa: F401 F403 50 | from .ht_dcphase import * # noqa: F401 F403 51 | from .ht_phasor import * # noqa: F401 F403 52 | from .ht_sine import * # noqa: F401 F403 53 | from .ht_trendmode import * # noqa: F401 F403 54 | 55 | # Statistics 56 | from .beta import * # noqa: F401 F403 57 | from .correl import * # noqa: F401 F403 58 | from .linreg import * # noqa: F401 F403 59 | from .madev import * # noqa: F401 F403 60 | from .stddev import * # noqa: F401 F403 61 | from .var import * # noqa: F401 F403 62 | 63 | # ## Overlap - depends on stddev 64 | from .bbands import * # noqa: F401 F403 65 | 66 | # Volatility 67 | from .atr import * # noqa: F401 F403 68 | 69 | # Momentum 70 | from .aroon import * # noqa: F401 F403 71 | from .bop import * # noqa: F401 F403 72 | from .cci import * # noqa: F401 F403 73 | from .cmo import * # noqa: F401 F403 74 | from .directionalmove import * # noqa: F401 F403 75 | from .macd import * # noqa: F401 F403 76 | from .mfi import * # noqa: F401 F403 77 | from .mom import * # noqa: F401 F403 78 | from .ppo import * # noqa: F401 F403 79 | from .roc import * # noqa: F401 F403 80 | from .rsi import * # noqa: F401 F403 81 | from .sar import * # noqa: F401 F403 82 | from .sarext import * # noqa: F401 F403 83 | from .stochastic import * # noqa: F401 F403 84 | from .stochrsi import * # noqa: F401 F403 85 | from .williamsr import * # noqa: F401 F403 86 | from .ultimateoscillator import * # noqa: F401 F403 87 | 88 | # Volume 89 | from .ad import * # noqa: F401 F403 90 | from .obv import * # noqa: F401 F403 91 | -------------------------------------------------------------------------------- /btalib/indicators/ad.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, ema, ewma 8 | 9 | 10 | class ad(Indicator): 11 | ''' 12 | Originally the "Cumulative Money Flow Line" by Mark Chaikin, which attempts 13 | to measure the incoming and outgoing flow of money by using the volume in 14 | addition the standard price components. 15 | 16 | Formula: 17 | - mfm = ((close - high) + (close - low)) / (high - low) 18 | - ergo: mfm = (2*close - high - low) / (high - low) 19 | 20 | - mfv = mf * volume 21 | - ad = cumulative_sum(mvf) 22 | 23 | See also: 24 | - https://en.wikipedia.org/wiki/Accumulation/distribution_index 25 | - https://school.stockcharts.com/doku.php?id=technical_indicators:accumulation_distribution_line 26 | ''' 27 | group = 'volume' 28 | alias = 'AD', 'adl', 'ADL', 'ChaikinAD', 'ChaikinADL', 'chaikinad' 29 | inputs = 'high', 'low', 'close', 'volume' 30 | outputs = 'ad' 31 | 32 | def __init__(self): 33 | hilo = self.i.high - self.i.low 34 | 35 | mfm = (2.0 * self.i.close - self.i.high - self.i.low) / hilo 36 | mfv = mfm * self.i.volume # money flow volume 37 | self.o.ad = mfv.cumsum() # ad line 38 | 39 | 40 | class adosc(ad): 41 | ''' 42 | The Chaikin Oscillator applies a `MACD` formula to the 43 | Accumulation/Distribution Line (`ad`) to calculate the momentum of the 44 | money flow. 45 | 46 | See also: 47 | - https://en.wikipedia.org/wiki/Chaikin_Analytics#Chaikin_Oscillator 48 | - https://school.stockcharts.com/doku.php?id=technical_indicators:chaikin_oscillator 49 | - https://www.metastock.com/customer/resources/taaz/?p=41 50 | ''' 51 | alias = 'ADOSC', 'ChaikinADOSC', 'ChaikinOsc', 'ChaikinOscillator' 52 | outputs = 'adosc' # automapping output adosc to ad from base class 53 | 54 | params = ( 55 | ('pfast', 3, 'Fast ema period'), 56 | ('pslow', 10, 'Slow ema period'), 57 | ('_ma', ema, 'Moving average to use'), 58 | ) 59 | 60 | def __init__(self): 61 | ma3 = self.p._ma(self.o.ad, period=self.p.pfast) 62 | ma10 = self.p._ma(self.o.ad, period=self.p.pslow) 63 | self.o.adosc = ma3 - ma10 64 | 65 | def _talib(self, kwdict): 66 | '''Switch to `ewma` (i.e.: *exponential weighted moving mean* with no seed 67 | instead of using a standard `ema` exponential moving average 68 | ''' 69 | kwdict.setdefault('_ma', ewma) 70 | -------------------------------------------------------------------------------- /btalib/indicators/aroon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import numpy as np 10 | 11 | 12 | def _first_idx_highest(x): 13 | return np.argmax(x[::-1]) 14 | 15 | 16 | def _first_idx_lowest(x): 17 | return np.argmin(x[::-1]) 18 | 19 | 20 | class _aroon(Indicator): 21 | ''' 22 | Base class for `aroon` and `aroonosc`. The up and down components are 23 | calculated and the subclasses can presente them individually or after an 24 | operation on them. 25 | ''' 26 | group = 'momentum' 27 | inputs = 'high', 'low' 28 | params = ( 29 | ('period', 14, 'Period to consider'), 30 | ) 31 | 32 | def __init__(self): 33 | p = self.p.period 34 | 35 | hhidx = self.i.high.rolling(window=p + 1).apply(_first_idx_highest) 36 | self._aup = 100.0 - 100.0 * hhidx / p 37 | 38 | llidx = self.i.low.rolling(window=p + 1).apply(_first_idx_lowest) 39 | self._adn = 100.0 - 100.0 * llidx / p 40 | 41 | 42 | class aroon(_aroon): 43 | ''' 44 | Developed by Tushar Chande in 1995. 45 | 46 | It tries to determine if a trend exists or not by calculating how far away 47 | within a given period the last highs/lows are (AroonUp/AroonDown) 48 | 49 | Formula: 50 | - up = 100 * (period - distance to highest high) / period 51 | - down = 100 * (period - distance to lowest low) / period 52 | 53 | Note: 54 | The lines oscillate between 0 and 100. That means that the "distance" to 55 | the last highest or lowest must go from 0 to period so that the formula 56 | can yield 0 and 100. 57 | 58 | Hence the lookback period is period + 1, because the current bar is also 59 | taken into account. And therefore this indicator needs an effective 60 | lookback period of period + 1. 61 | 62 | See: 63 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon 64 | ''' 65 | alias = 'AROON', 'Aroon' 66 | outputs = 'aroondn', 'aroonup' 67 | 68 | def __init__(self): 69 | self.o.aroondn = self._adn 70 | self.o.aroonup = self._aup 71 | 72 | 73 | class aroonosc(_aroon): 74 | ''' 75 | It is a variation of the AroonUpDown indicator which shows the current 76 | difference between the AroonUp and AroonDown value, trying to present a 77 | visualization which indicates which is stronger (greater than 0 -> AroonUp 78 | and less than 0 -> AroonDown) 79 | 80 | Formula: 81 | - aroonosc = aroonup - aroondn 82 | 83 | See: 84 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon 85 | ''' 86 | alias = 'AROONOSC', 'AroonOscillator' 87 | outputs = 'aroonosc' 88 | 89 | def __init__(self): 90 | self.o.aroonosc = self._aup - self._adn 91 | -------------------------------------------------------------------------------- /btalib/indicators/atr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, smma 8 | 9 | 10 | class truehigh(Indicator): 11 | ''' 12 | Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in 13 | Technical Trading Systems"* for the ATR 14 | 15 | Records the "true high" which is the maximum of today's high and 16 | yesterday's close 17 | 18 | Formula: 19 | - truehigh = max(high, close_prev) 20 | 21 | See: 22 | - http://en.wikipedia.org/wiki/Average_true_range 23 | ''' 24 | group = 'volatility' 25 | alias = 'TR', 'TrueRange', 'trange', 'TRANGE' 26 | inputs = 'low', 'close' 27 | outputs = 'truehi' 28 | params = ( 29 | ('_period', 1, 'Period to consider'), 30 | ) 31 | 32 | def __init__(self): 33 | self.o.truehi = self.i.close(-self.p._period).clip(lower=self.i.high) 34 | 35 | 36 | class truelow(Indicator): 37 | ''' 38 | Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in 39 | Technical Trading Systems"* for the ATR 40 | 41 | Records the "true low" which is the minimum of today's low and 42 | yesterday's close 43 | 44 | Formula: 45 | - truelow = min(low, close_prev) 46 | 47 | See: 48 | - http://en.wikipedia.org/wiki/Average_true_range 49 | ''' 50 | group = 'volatility' 51 | alias = 'TR', 'TrueRange', 'trange', 'TRANGE' 52 | inputs = 'low', 'close' 53 | outputs = 'truelo' 54 | params = ( 55 | ('_period', 1, 'Period to consider'), 56 | ) 57 | 58 | def __init__(self): 59 | self.o.truelo = self.i.close(-self.p._period).clip(upper=self.i.low) 60 | 61 | 62 | class truerange(Indicator): 63 | ''' 64 | Defined by J. Welles Wilder, Jr. in 1978 in his book New Concepts in 65 | Technical Trading Systems. 66 | 67 | Formula: 68 | - max(high - low, abs(high - prev_close), abs(prev_close - low) 69 | 70 | which can be simplified to 71 | 72 | - truerange = max(high, prev_close) - min(low, prev_close) 73 | 74 | See: 75 | - http://en.wikipedia.org/wiki/Average_true_range 76 | 77 | The idea is to take the previous close into account to calculate the range 78 | if it yields a larger range than the daily range (High - Low) 79 | ''' 80 | group = 'volatility' 81 | alias = 'TR', 'TrueRange', 'trange', 'TRANGE' 82 | inputs = 'high', 'low', 'close' 83 | outputs = 'tr' 84 | params = ( 85 | ('_period', 1, 'Period for high/low vs close for truerange calc'), 86 | ) 87 | 88 | def __init__(self): 89 | close1 = self.i.close(-self.p._period) 90 | truehi = close1.clip(lower=self.i.high) # max of close(-1) and hi 91 | truelo = close1.clip(upper=self.i.low) # min of close(-1) and low 92 | self.o.tr = truehi - truelo 93 | 94 | 95 | class atr(truerange): 96 | ''' 97 | Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in 98 | Technical Trading Systems"*. 99 | 100 | The idea is to take the close into account to calculate the range if it 101 | yields a larger range than the daily range (High - Low) 102 | 103 | Formula: 104 | - truerange = max(high, close(-1)) - min(low, close(-1)) 105 | - atr = SmoothedMovingAverage(truerange, period) 106 | 107 | See: 108 | - http://en.wikipedia.org/wiki/Average_true_range 109 | ''' 110 | group = 'volatility' 111 | alias = 'ATR', 'AverageTrueRange' 112 | outputs = 'atr' # outputs_override in class def, autoalias tr => atr added 113 | params = ( 114 | ('period', 14, 'Period to consider'), 115 | ('_ma', smma, 'Moving average to use'), 116 | ) 117 | 118 | def __init__(self): 119 | self.o.atr = self.p._ma(self.o.tr, period=self.p.period) 120 | 121 | 122 | class natr(atr): 123 | ''' 124 | Offers a normalized (against the `close`) version of the `atr`, which can 125 | provide better values for comparison against different price ranges. 126 | 127 | Formula: 128 | - natr = 100.0 * atr / close 129 | 130 | See: 131 | - http://en.wikipedia.org/wiki/Average_true_range 132 | ''' 133 | group = 'volatility' 134 | alias = 'NATR', 'NormalizedAverageTrueRange' 135 | outputs = 'natr' # outputs_override above, autoalias atr => natr added 136 | 137 | def __init__(self): 138 | self.o.natr = 100.0 * self.o.atr / self.i.close 139 | -------------------------------------------------------------------------------- /btalib/indicators/bbands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, sma, stddev 8 | 9 | 10 | class bbands(Indicator): 11 | ''' 12 | Defined by John Bollinger in the 80s. It measures volatility by defining 13 | upper and lower bands at distance x standard deviations 14 | 15 | Formula: 16 | - midband = SimpleMovingAverage(close, period) 17 | - topband = midband + devfactor * StandardDeviation(data, period) 18 | - botband = midband - devfactor * StandardDeviation(data, period) 19 | 20 | See: 21 | - http://en.wikipedia.org/wiki/Bollinger_Bands 22 | ''' 23 | group = 'overlap' 24 | alias = 'BBANDS', 'BollingerBands', 'BOLLINGERBANDS' 25 | outputs = 'mid', 'top', 'bot' 26 | params = ( 27 | ('period', 20, 'Period to consider'), 28 | ('devs', 2.0, 'Standard Deviations of Top/Bottom Bands'), 29 | ('_ma', sma, 'Moving average to use'), 30 | ('_stdev', stddev, 'Standard Deviation Calculation to use'), 31 | ) 32 | 33 | def __init__(self): 34 | self.o.mid = mid = self.p._ma(self.i0, period=self.p.period) 35 | 36 | devdist = self.p.devs * stddev(self.i0, period=self.p.period) 37 | self.o.top = mid + devdist 38 | self.o.bot = mid - devdist 39 | 40 | def _talib(self, kwdict): 41 | '''Change period to 5''' 42 | kwdict.setdefault('period', 5) 43 | -------------------------------------------------------------------------------- /btalib/indicators/beta.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class beta(Indicator): 11 | ''' 12 | The description of the algorithm has been adapted from `ta-lib` 13 | 14 | The Beta 'algorithm' is a measure of a stocks volatility vs from index. The 15 | stock prices are given as the 1st input and the index prices are given in 16 | the 2nd input. The size of the inputs should be equal. The algorithm is to 17 | calculate the change between prices in both inputs and then 'plot' these 18 | changes are points in the Euclidean plane. The x value of the point is 19 | market return and the y value is the security return. The beta value is the 20 | slope of a linear regression through these points. A beta of 1.0 is simple 21 | the line y=x, so the stock varies precisely with the market. A beta of less 22 | than 1.0 means the stock varies less thandd the market and a beta of more 23 | than 1.0 means the stock varies more than market. 24 | 25 | See: 26 | - http://www.moneychimp.com/articles/risk/regression.htm 27 | ''' 28 | group = 'statistic' 29 | alias = 'BETA', 'Beta' 30 | inputs = 'asset', 'market' 31 | outputs = 'beta' 32 | params = ( 33 | ('period', 5, 'Period to consider'), 34 | ('_prets', 1, 'Lookback period to calculate the returns'), 35 | ('_rets', True, 'Calculate beta on returns'), 36 | ) 37 | 38 | def __init__(self): 39 | p, prets = self.p.period, self.p._prets 40 | 41 | if self.p._rets: 42 | x = self.i.asset.pct_change(periods=prets) # stock returns 43 | y = self.i.market.pct_change(periods=prets) # market returns 44 | else: 45 | x, y = self.i.asset, self.i.market 46 | 47 | s_x = x.rolling(window=p).sum() 48 | s_y = y.rolling(window=p).sum() 49 | 50 | s_xx = x.pow(2).rolling(window=p).sum() 51 | s_xy = (x * y).rolling(window=p).sum() 52 | 53 | self.o.beta = (p * s_xy - s_x * s_y) / (p * s_xx - s_x.pow(2)) 54 | -------------------------------------------------------------------------------- /btalib/indicators/bop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class bop(Indicator): 11 | ''' 12 | Bop tries to determine the power of buyers vs sellers, by calculating the 13 | power of being able to take the price to the extremes. 14 | 15 | Igor Livshin introduced in August 2001 in the Stocks and Commodities 16 | Magazine 17 | 18 | Formula: 19 | - bop = (close - open) / (high - low) 20 | 21 | See also: 22 | - https://www.interactivebrokers.com/en/software/tws/usersguidebook/technicalanalytics/balancePower.htm 23 | - https://www.marketvolume.com/technicalanalysis/balanceofpower.asp 24 | ''' 25 | group = 'momentum' 26 | alias = 'BOP', 'BalanceOfPower' 27 | inputs = 'open', 'high', 'low', 'close' 28 | outputs = 'bop' 29 | params = () 30 | 31 | def __init__(self): 32 | self.o.bop = (self.i.close - self.i.open) / (self.i.high - self.i.low) 33 | -------------------------------------------------------------------------------- /btalib/indicators/cci.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, mad, sma 8 | 9 | 10 | class cci(Indicator): 11 | ''' 12 | Introduced by Donald Lambert in 1980 to measure variations of the 13 | "typical price" (see below) from its mean to identify extremes and 14 | reversals 15 | 16 | Formula: 17 | - tp = typical_price = (high + low + close) / 3 18 | - tpmean = MovingAverage(tp, period) 19 | - deviation = tp - tpmean 20 | - meandev = MeanDeviation(tp) 21 | - cci = deviation / (meandeviation * factor) 22 | 23 | See: 24 | - https://en.wikipedia.org/wiki/Commodity_channel_index 25 | ''' 26 | group = 'momentum' 27 | alias = 'CCI', 'CommodityChannelIndex' 28 | inputs = 'high', 'low', 'close' 29 | outputs = 'cci' 30 | params = ( 31 | ('period', 20, 'Period to consider'), 32 | ('factor', 0.015, 'Channel width factor'), 33 | ('_ma', sma, 'Moving Average to sue'), 34 | ('_dev', mad, 'Deviation to use (Def: Mean Abs Dev)'), 35 | ) 36 | 37 | def __init__(self): 38 | tp = (self.i.high + self.i.low + self.i.close) / 3.0 # typical price 39 | tpmean = self.p._ma(tp, period=self.p.period) # mean of tp 40 | madev = mad(tp, period=self.p.period) # mean abs deviation of tp 41 | self.o.cci = (tp - tpmean) / (madev * self.p.factor) # cci formula 42 | 43 | def _talib(self, kwdict): 44 | '''Change period to 14''' 45 | kwdict.setdefault('period', 14) 46 | -------------------------------------------------------------------------------- /btalib/indicators/cmo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, SumN, smma 8 | 9 | 10 | class cmo(Indicator): 11 | ''' 12 | 13 | Formula: 14 | - sum_updays = Sum(max(close - close(-1), 0.0), period) 15 | - sum_downdays = Sum(abs(min(close - close(-1)), 0.0), period) 16 | 17 | - cmo = 100 * (sum_updays - sum_downdays) / (sum_updays + sum_downdays) 18 | 19 | See also: 20 | - https://www.investopedia.com/terms/c/chandemomentumoscillator.asp 21 | - https://www.tradingtechnologies.com/xtrader-help/x-study/technical-indicator-definitions/chande-momentum-oscillator-cmo/ 22 | ''' 23 | groups = 'momentum' 24 | alias = 'CMO', 'ChandeMomentumOscillator' 25 | outputs = 'cmo' 26 | 27 | params = ( 28 | ('period', 14, 'Period to consider',), 29 | ('_sum', SumN, 'Summation used to accumulate updays/downdays'), 30 | ) 31 | 32 | def __init__(self): 33 | cdiff = self.i0.diff(periods=1) 34 | updays = self.p._sum(cdiff.clip(lower=0.0), period=self.p.period) 35 | dodays = self.p._sum(cdiff.clip(upper=0.0).abs(), period=self.p.period) 36 | self.o.cmo = 100.0 * (updays - dodays) / (updays + dodays) 37 | 38 | def _talib(self, kwdict): 39 | '''Against what the book by Chande states, ta-lib smooths the values before 40 | performing the RSI like calculation using the `smma`, whereas Chande 41 | clearly states that the advantage of his oscillator is the fact that 42 | unsmoothed values are being used 43 | ''' 44 | kwdict.setdefault('_sum', smma) 45 | -------------------------------------------------------------------------------- /btalib/indicators/correl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class correl(Indicator): 11 | ''' 12 | Rolling Pearson correlation of the two inputs `asset1` and `asset2` 13 | 14 | Note: the default inputs are named `high` and `low` to ensure an easy match 15 | with a multiple input (dataframe). But they can be anything 16 | 17 | See: 18 | - https://en.wikipedia.org/wiki/Pearson_correlation_coefficient 19 | ''' 20 | group = 'statistic' 21 | alias = 'CORREL', 'Correl' 22 | inputs = 'high', 'low' 23 | outputs = 'correl' 24 | params = ( 25 | ('period', 30, 'Period to consider'), 26 | ('_prets', 1, 'Lookback period to calculate the returns'), 27 | ('_rets', True, 'Calculate beta on returns'), 28 | ) 29 | 30 | def __init__(self): 31 | self.o.correl = self.i0.rolling(window=self.p.period).corr(self.i1) 32 | -------------------------------------------------------------------------------- /btalib/indicators/crossover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class _crossbase(Indicator, inputs_override=True): 11 | '''Base crossover class doing the calculations depending on the attribute 12 | _updown, set by subclasses 13 | ''' 14 | _updown = 0 # -1 crossdown, 0 both, 1 crossup 15 | 16 | inputs = 'crosser', 'crossed' # overrides (see above) and add an input 17 | params = ( 18 | ('_strict', False, 19 | 'If `True`, Consider cross only with two consecutive bars. Else the ' 20 | 'default method will take into account that a cross up/down can ' 21 | 'happen along several bars with the extremes being above/below and ' 22 | 'below/above respectively, and the middle bars having a zero ' 23 | 'difference between the crosser and crossed inputs'), 24 | ) 25 | 26 | def __init__(self): 27 | i0, i1 = self.i0, self.i1 28 | 29 | idiff = (i0 - i1).shift(periods=1) # inputs (io crosser, i1 crossed) 30 | if not self.p._strict: # fill 0.0 intervening bars with previous vals 31 | # replace consecutive zeros with the value before the first zero 32 | # this accounts for crossovers which happen over several bars, with 33 | # the middle bars having both inputs equal (0.0 diff between them) 34 | idiff = idiff.replace(to_replace=0.0, method='ffill') 35 | 36 | if self._updown >= 0: # cross upwards 37 | self._cup = (idiff < 0.0) & (i0 > i1) # i0(-1) < i1(-1) & ... 38 | 39 | if self._updown <= 0: # cross downwards 40 | self._cdown = (idiff > 0.0) & (i0 < i1) # i0(-1) > i1(-1) & ... 41 | 42 | 43 | class crossup(_crossbase): 44 | ''' 45 | This indicator gives a signal if the 1st provided data crosses over the 2nd 46 | indicator upwards 47 | 48 | It does need to look into the current time index (0) and the previous time 49 | index (-1) of both the 1st and 2nd data 50 | 51 | Formula: 52 | - crossup = data0(-1) < data1(-1) and data0 > data1 53 | ''' 54 | group = 'utils' 55 | alias = 'CrossUp' 56 | outputs = 'crossup' 57 | 58 | _updown = 1 59 | 60 | def __init__(self): 61 | self.o.crossup = 1.0 * self._cup 62 | 63 | 64 | class crossdown(_crossbase): 65 | ''' 66 | This indicator gives a signal if the 1st provided data crosses over the 2nd 67 | indicator upwards 68 | 69 | It does need to look into the current time index (0) and the previous time 70 | index (-1) of both the 1st and 2nd data 71 | 72 | Formula: 73 | - crossdown = data0(-1) > data1(-1) and data0 < data1 74 | ''' 75 | group = 'utils' 76 | alias = 'CrossDown' 77 | outputs = 'crossdown' 78 | 79 | _updown = -1 80 | 81 | def __init__(self): 82 | self.o.crossdown = -1.0 * self._cdown 83 | 84 | 85 | class crossover(_crossbase): 86 | ''' 87 | This indicator gives a signal if the provided datas (2) cross up or down. 88 | 89 | - `1.0` if the 1st data crosses the 2nd data upwards 90 | - `-1.0` if the 1st data crosses the 2nd data downwards 91 | 92 | It does need to look into the current time index (0) and the previous time 93 | index (-1) of both the 1st and 2nd data 94 | 95 | Formula: 96 | - crossup = data0(-1) < data1(-1) and data0 > data1 97 | - crossdown = data0(-1) > data1(-1) and data0 < data1 98 | - crossover = crossup - crossdown 99 | ''' 100 | group = 'utils' 101 | alias = 'CrossOver' 102 | outputs = 'crossover' 103 | 104 | _updown = 0 105 | 106 | def __init__(self): 107 | self.o.crossover = self._cup - self._cdown 108 | -------------------------------------------------------------------------------- /btalib/indicators/dema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, ema 8 | 9 | 10 | class dema(Indicator): 11 | ''' 12 | DEMA was first time introduced in 1994, in the article "Smoothing Data with 13 | Faster Moving Averages" by Patrick G. Mulloy in "Technical Analysis of 14 | Stocks & Commodities" magazine. 15 | 16 | It attempts to reduce the inherent lag associated to Moving Averages 17 | 18 | Formula: 19 | - dema = (2.0 * ema(data, period)) - ema(ema(data, period), period) 20 | 21 | See: 22 | - https://en.wikipedia.org/wiki/Double_exponential_moving_average 23 | ''' 24 | group = 'overlap' 25 | alias = 'DEMA', 'DoubleExponentialMovingAverage' 26 | outputs = 'dema' 27 | params = ( 28 | ('period', 30, 'Period to consider'), 29 | ('_ma', ema, 'Moving Average to use'), 30 | ) 31 | 32 | def __init__(self): 33 | ema1 = self.p._ma(self.i0, period=self.p.period) 34 | ema2 = self.p._ma(ema1, period=self.p.period) 35 | self.o.dema = 2.0 * ema1 - ema2 36 | -------------------------------------------------------------------------------- /btalib/indicators/ema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, SEED_AVG 8 | 9 | # named argument poffset in __init__ below is for compatibility with ta-lib 10 | # broken MACD. When poffset > period, the delivery of the 1st valid value 11 | # happens at poffset instead of at period 12 | # The start of the calculation is accordingly delayed: poffset - period 13 | 14 | 15 | class ema(Indicator): 16 | ''' 17 | A Moving Average that smoothes data exponentially over time. 18 | 19 | - Exponential Smotthing factor: alpha = 2 / (1 + period) 20 | 21 | Formula 22 | - prev = mean(data, period) 23 | - movav = prev * (1.0 - alpha) + newdata * alpha 24 | - (or alternatively # movav = prev + alpha(new - prev)) 25 | 26 | See also: 27 | - http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 28 | ''' 29 | group = 'overlap' 30 | alias = 'EMA', 'ExponentialMovingAverage' 31 | outputs = 'ema' 32 | params = ( 33 | ('period', 30, 'Period for the moving average calculation'), 34 | ('_seed', SEED_AVG, 'Default to use average of periods as seed'), 35 | ) 36 | 37 | def __init__(self, poffset=0): # see above for poffset 38 | span, seed, poff = self.p.period, self.p._seed, poffset 39 | self.o.ema = self.i0._ewm(span=span, _seed=seed, _poffset=poff).mean() 40 | -------------------------------------------------------------------------------- /btalib/indicators/ewma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | from . import SEED_AVG 9 | 10 | 11 | class ewma(Indicator): 12 | ''' 13 | This is **NOT** the `ema` or `ExponentialMovingAverage`. This is a wrap 14 | around pandas.Series.ewm where `ewma` stands for `ExponentialWeigthedMean` 15 | ... to which later a function like `mean` is applied 16 | 17 | Applying `mean` doesn't make it the `ExponentialMovingAverage` (aka `EMA` 18 | or `ema`) because `ewma` in `pandas.Series` or `pandas.DataFrames` does not 19 | support using a seed of the first n periods of an `ewm` of `span=n` 20 | 21 | The purpose of this, is to be able to use this in place of the real `ema` 22 | with parameters like `period` and `_seed` for compatibility. 23 | ''' 24 | group = 'overlap' 25 | alias = 'EWMA' 26 | outputs = 'ewma' 27 | params = ( 28 | ('period', 30, 'Default Period for the ewm calculation'), 29 | ('adjust', False, 'Default calc individual terms like in `ema`'), 30 | ('_seed', SEED_AVG, '(nop) for compatibility with `ema`'), 31 | ) 32 | 33 | def __init__(self, **kwargs): 34 | kwargs.setdefault('span', self.p.period) # translate period to span 35 | self.o.ewma = self.i0.ewm(adjust=self.p.adjust, **kwargs).mean() 36 | -------------------------------------------------------------------------------- /btalib/indicators/ht_dcperiod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import collections 10 | from math import atan 11 | 12 | import numpy as np 13 | 14 | RAD2DEG = 180.0 / (4.0 * atan(1)) 15 | 16 | 17 | class ht_dcperiod(Indicator): 18 | ''' 19 | Ehlers': Hilber Transform Dominant Cycle Period 20 | 21 | Formula: 22 | - From *"Rocket Science for Traders: Digital Signal Processing Applications"* 23 | 24 | See: 25 | - https://www.amazon.com/Rocket-Science-Traders-Processing-Applications/dp/0471405671 26 | ''' 27 | group = 'cycle' 28 | alias = 'HT_DCPERIOD', 'HilberTransform_DominantCyclePeriod' 29 | inputs = 'high', 'low' 30 | allowinputs = 1 # allow only 1 input if only 1 passed for ta-lib compat 31 | outputs = 'dcperiod' 32 | 33 | LOOKBACK_TOTAL = 33 34 | LOOKBACK_SMOOTH = 4 35 | LOOKBACK_HT = 7 36 | LOOKBACK_HT_SKIP = 0 # skip before applying ht 37 | LOOKBACK_SMOOTH_EXTRA = LOOKBACK_HT - LOOKBACK_SMOOTH 38 | LOOKBACK_REST = LOOKBACK_TOTAL - LOOKBACK_HT 39 | 40 | def __init__(self): 41 | # Choose p0, depending on passed number o inputs 42 | p0 = (self.i.high + self.i.low) / 2.0 if len(self.i) > 1 else self.i0 43 | 44 | # smooth p0 45 | p0smooth = (4.0*p0 + 3.0*p0(-1) + 2.0*p0(-2) + p0(-3)) / 10.0 46 | # Add the needed lookback for HT, not yet offered by the smoothing 47 | p0smooth._period(self.LOOKBACK_SMOOTH_EXTRA) 48 | 49 | dcbuffer = p0smooth(val=0.0) # copy p0smooth index, fill with 0.0 50 | dcperiod = p0smooth._apply(self._periodize, dcbuffer, raw=True) # calc 51 | 52 | # _periodize - no auto period. Add non-count period, filled with nan 53 | self.o.dcperiod = dcperiod._period(self.LOOKBACK_REST, val=np.nan) 54 | 55 | def _ht(self, x, adjperiod, i): 56 | ht0 = 0.0962*x[i] + 0.5769*x[i - 2] - 0.5769*x[i - 4] - 0.0962*x[i - 6] 57 | return ht0 * adjperiod 58 | 59 | def _periodize(self, price, dcperiod): 60 | # period 7 needed in _periodize for hilbert transform 61 | # p0smooth has: 4 and needs additional 3 before applying _periodize 62 | # actual "return" values to be used in __init__ for phase calculations 63 | LOOKBACK = self.LOOKBACK_HT 64 | LOOKIDX = LOOKBACK - 1 65 | LOOKSTART = LOOKIDX + self.LOOKBACK_HT_SKIP 66 | 67 | # circular buffers for ht calcs 68 | detrender = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 69 | i1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 70 | q1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 71 | 72 | # the first LOOKBACK elements of the input are ignored in the ta-lib 73 | # calculations for the detrender. Nullify them. 74 | price[0:LOOKSTART] = 0.0 75 | 76 | # Variables for the running calculations 77 | i2, q2, re, im, period, smoothperiod = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 78 | 79 | for i in range(LOOKSTART, len(price)): 80 | adjperiod = 0.075*period + 0.54 # adj period_1 for ht transformx 81 | 82 | detrender.append(self._ht(price, adjperiod, i)) 83 | 84 | # New detrender val pushed, append to the right -1 is actual value 85 | i10 = detrender[-4] # 3 periods ago: -2, -3, -4 86 | q10 = self._ht(detrender, adjperiod, LOOKIDX) 87 | 88 | i1.append(i10), q1.append(q10) 89 | 90 | ji = self._ht(i1, adjperiod, LOOKIDX) # looback up to -6 91 | jq = self._ht(q1, adjperiod, LOOKIDX) 92 | 93 | i21, q21 = i2, q2 # need them for re/im before recalc 94 | 95 | i2 = i10 - jq 96 | q2 = q10 + ji 97 | 98 | i2 = 0.2*i2 + 0.8*i21 # smooth 99 | q2 = 0.2*q2 + 0.8*q21 # smooth 100 | 101 | re0 = i2*i21 + q2*q21 102 | im0 = i2*q21 - q2*i21 103 | 104 | re = 0.2*re0 + 0.8*re # smooth 105 | im = 0.2*im0 + 0.8*im # smooth 106 | 107 | period1 = period 108 | period = 360 / (RAD2DEG*atan(im / re)) if re and im else period1 109 | period = min(period, period1 * 1.5) 110 | period = max(period, period1 * 0.67) 111 | period = max(period, 6) 112 | period = min(period, 50) 113 | period = 0.2*period + 0.8*period1 # smooth 114 | 115 | smoothperiod = 0.33*period + 0.67*smoothperiod 116 | dcperiod[i] = smoothperiod 117 | 118 | return dcperiod 119 | -------------------------------------------------------------------------------- /btalib/indicators/ht_dcphase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import collections 10 | from math import atan, cos, sin 11 | 12 | import numpy as np 13 | 14 | RAD2DEG = 180.0 / (4.0 * atan(1)) 15 | DEG2RADBY360 = 360.0 / RAD2DEG 16 | 17 | 18 | class ht_dcphase(Indicator): 19 | ''' 20 | Ehlers': Hilber Transform Dominant Cycle Phase 21 | 22 | Formula: 23 | - From *"Rocket Science for Traders: Digital Signal Processing Applications"* 24 | 25 | See: 26 | - https://www.amazon.com/Rocket-Science-Traders-Processing-Applications/dp/0471405671 27 | ''' 28 | group = 'cycle' 29 | alias = 'HT_DCPHASE', 'HilberTransform_DominantCyclePhase' 30 | inputs = 'high', 'low' 31 | allowinputs = 1 # allow only 1 input if only 1 passed for ta-lib compat 32 | outputs = 'dcphase' 33 | 34 | LOOKBACK_TOTAL = 64 35 | LOOKBACK_SMOOTH = 4 36 | LOOKBACK_HT = 7 37 | LOOKBACK_HT_SKIP = 25 # skip before applying ht 38 | LOOKBACK_SMOOTH_EXTRA = LOOKBACK_HT - LOOKBACK_SMOOTH 39 | LOOKBACK_REST = LOOKBACK_TOTAL - LOOKBACK_HT 40 | 41 | def __init__(self): 42 | # Choose p0, depending on passed number o inputs 43 | p0 = (self.i.high + self.i.low) / 2.0 if len(self.i) > 1 else self.i0 44 | 45 | # smooth p0 46 | p0smooth = (4.0*p0 + 3.0*p0(-1) + 2.0*p0(-2) + p0(-3)) / 10.0 47 | # Add the needed lookback for HT, not yet offered by the smoothing 48 | p0smooth._period(self.LOOKBACK_SMOOTH_EXTRA) 49 | 50 | dcbuffer = p0smooth(val=0.0) # copy p0smooth index, fill with 0.0 51 | dcphase = p0smooth._apply(self._periodize, dcbuffer, raw=True) # calc 52 | 53 | # _periodize - no auto period. Add non-count period, filled with nan 54 | self.o.dcphase = dcphase._period(self.LOOKBACK_REST, val=np.nan) 55 | 56 | def _ht(self, x, adjperiod, i): 57 | ht0 = 0.0962*x[i] + 0.5769*x[i - 2] - 0.5769*x[i - 4] - 0.0962*x[i - 6] 58 | return ht0 * adjperiod 59 | 60 | def _periodize(self, price, dcphasebuf): 61 | # period 7 needed in _periodize for hilbert transform 62 | # p0smooth has: 4 and needs additional 3 before applying _periodize 63 | # actual "return" values to be used in __init__ for phase calculations 64 | LOOKBACK = self.LOOKBACK_HT 65 | LOOKIDX = LOOKBACK - 1 66 | LOOKSTART = LOOKIDX + self.LOOKBACK_HT_SKIP 67 | 68 | # circular buffers for ht calcs 69 | detrender = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 70 | i1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 71 | q1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 72 | 73 | # the first LOOKBACK elements of the input are ignored in the ta-lib 74 | # calculations for the detrender. Nullify them. 75 | price[0:LOOKSTART] = 0.0 76 | 77 | # Variables for the running calculations 78 | i2, q2, re, im, period, smoothperiod = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 79 | 80 | dcphase = 0.0 81 | 82 | for i in range(LOOKSTART, len(price)): 83 | adjperiod = 0.075*period + 0.54 # adj period_1 for ht transformx 84 | 85 | detrender.append(self._ht(price, adjperiod, i)) 86 | 87 | # New detrender val pushed, append to the right -1 is actual value 88 | i10 = detrender[-4] # 3 periods ago: -2, -3, -4 89 | q10 = self._ht(detrender, adjperiod, LOOKIDX) 90 | 91 | i1.append(i10), q1.append(q10) 92 | 93 | ji = self._ht(i1, adjperiod, LOOKIDX) # looback up to -6 94 | jq = self._ht(q1, adjperiod, LOOKIDX) 95 | 96 | i21, q21 = i2, q2 # need them for re/im before recalc 97 | 98 | i2 = i10 - jq 99 | q2 = q10 + ji 100 | 101 | i2 = 0.2*i2 + 0.8*i21 # smooth 102 | q2 = 0.2*q2 + 0.8*q21 # smooth 103 | 104 | re0 = i2*i21 + q2*q21 105 | im0 = i2*q21 - q2*i21 106 | 107 | re = 0.2*re0 + 0.8*re # smooth 108 | im = 0.2*im0 + 0.8*im # smooth 109 | 110 | period1 = period 111 | period = 360 / (RAD2DEG*atan(im / re)) if re and im else period1 112 | period = min(period, period1 * 1.5) 113 | period = max(period, period1 * 0.67) 114 | period = max(period, 6) 115 | period = min(period, 50) 116 | period = 0.2*period + 0.8*period1 # smooth 117 | 118 | smoothperiod = 0.33*period + 0.67*smoothperiod 119 | 120 | dcperiod = int(smoothperiod + 0.5) 121 | 122 | realpart, imagpart = 0.0, 0.0 123 | for dci in range(dcperiod): 124 | x = dci * DEG2RADBY360 / dcperiod 125 | y = price[i - dci] # backwards from last 126 | realpart += sin(x) * y 127 | imagpart += cos(x) * y 128 | 129 | abs_imagpart = abs(imagpart) 130 | if abs_imagpart > 0.0: 131 | dcphase = atan(realpart/imagpart) * RAD2DEG 132 | elif abs_imagpart <= 0.01: 133 | if realpart < 0.0: 134 | dcphase -= 90.0 135 | elif realpart > 0.0: 136 | dcphase += 90.0 137 | 138 | dcphase += 90.0 139 | dcphase += 360.0 / smoothperiod 140 | 141 | if imagpart < 0.0: 142 | dcphase += 180.0 143 | 144 | if dcphase > 315.0: 145 | dcphase -= 360.0 146 | 147 | dcphasebuf[i] = dcphase 148 | 149 | return dcphasebuf 150 | -------------------------------------------------------------------------------- /btalib/indicators/ht_phasor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import collections 10 | from math import atan 11 | 12 | import numpy as np 13 | 14 | RAD2DEG = 180.0 / (4.0 * atan(1)) 15 | 16 | 17 | class ht_phasor(Indicator): 18 | ''' 19 | Ehlers': Hilbert Transform Phasor 20 | 21 | Formula: 22 | - From *"Rocket Science for Traders: Digital Signal Processing Applications"* 23 | 24 | See: 25 | - https://www.amazon.com/Rocket-Science-Traders-Processing-Applications/dp/0471405671 26 | ''' 27 | group = 'cycle' 28 | alias = 'HT_PHASOR', 'HilberTransform_Phasor' 29 | inputs = 'high', 'low' 30 | allowinputs = 1 31 | outputs = 'inphase', 'quadrature' 32 | 33 | LOOKBACK_TOTAL = 33 34 | LOOKBACK_SMOOTH = 4 35 | LOOKBACK_HT = 7 36 | LOOKBACK_HT_SKIP = 0 # skip before applying ht 37 | LOOKBACK_SMOOTH_EXTRA = LOOKBACK_HT - LOOKBACK_SMOOTH 38 | LOOKBACK_REST = LOOKBACK_TOTAL - LOOKBACK_HT 39 | 40 | def __init__(self): 41 | # Choose p0, depending on passed number o inputs 42 | p0 = (self.i.high + self.i.low) / 2.0 if len(self.i) > 1 else self.i0 43 | 44 | # smooth p0 45 | p0smooth = (4.0*p0 + 3.0*p0(-1) + 2.0*p0(-2) + 1.0*p0(-3)) / 10.0 46 | # Add the needed lookback for HT, not yet offered by the smoothing 47 | p0smooth._period(self.LOOKBACK_SMOOTH_EXTRA) 48 | 49 | # zero-fill to ensure ht lookbacks deliver results (nan would break) 50 | i1, q1 = p0smooth(val=0.0), p0smooth(val=0.0) 51 | 52 | # use applymulti to get more than one result back 53 | i1, q1 = p0smooth._applymulti(self._periodize, i1, q1, raw=True) 54 | 55 | # _periodize - no auto period. Add non-count period, filled with nan 56 | self.o.inphase = i1._period(self.LOOKBACK_REST, val=np.nan) 57 | self.o.quadrature = q1._period(self.LOOKBACK_REST, val=np.nan) 58 | 59 | def _ht(self, x, adjperiod, i): 60 | ht0 = 0.0962*x[i] + 0.5769*x[i - 2] - 0.5769*x[i - 4] - 0.0962*x[i - 6] 61 | return ht0 * adjperiod 62 | 63 | def _periodize(self, price, i1, q1): 64 | # period 7 needed in _periodize for hilbert transform 65 | # p0smooth has: 4 and needs additional 3 before applying _periodize 66 | # actual "return" values to be used in __init__ for phase calculations 67 | LOOKBACK = self.LOOKBACK_HT 68 | LOOKIDX = LOOKBACK - 1 69 | LOOKSTART = LOOKIDX + self.LOOKBACK_HT_SKIP 70 | 71 | # circular buffers for ht calcs 72 | detrender = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 73 | 74 | # the first LOOKBACK elements of the input are ignored in the ta-lib 75 | # calculations for the detrender. Nullify them. 76 | price[0:LOOKSTART] = 0.0 77 | 78 | i2, q2, re, im, period = 0.0, 0.0, 0.0, 0.0, 0.0 79 | 80 | for i in range(LOOKSTART, len(price)): 81 | adjperiod = 0.075*period + 0.54 # adj period_1 for ht transformx 82 | 83 | detrender.append(self._ht(price, adjperiod, i)) 84 | 85 | # New detrender val pushed, append to the right -1 is actual value 86 | i1[i] = i10 = detrender[-4] # 3 periods ago: -2, -3, -4 87 | q1[i] = q10 = self._ht(detrender, adjperiod, LOOKIDX) 88 | 89 | ji = self._ht(i1, adjperiod, i) 90 | jq = self._ht(q1, adjperiod, i) 91 | 92 | i21, q21 = i2, q2 # keep for next round 93 | 94 | i2 = i10 - jq 95 | q2 = q10 + ji 96 | 97 | i2 = 0.2*i2 + 0.8*i21 # smooth 98 | q2 = 0.2*q2 + 0.8*q21 # smooth 99 | 100 | re0 = i2*i21 + q2*q21 101 | im0 = i2*q21 - q2*i21 102 | 103 | re = 0.2*re0 + 0.8*re # smooth 104 | im = 0.2*im0 + 0.8*im # smooth 105 | 106 | period1 = period 107 | period = 360 / (RAD2DEG*atan(im / re)) if re and im else period1 108 | period = min(period, period1 * 1.5) 109 | period = max(period, period1 * 0.67) 110 | period = max(period, 6) 111 | period = min(period, 50) 112 | period = 0.2*period + 0.8*period1 # smooth 113 | 114 | # return the results 115 | return i1, q1 116 | -------------------------------------------------------------------------------- /btalib/indicators/ht_sine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import collections 10 | from math import atan, cos, sin 11 | 12 | import numpy as np 13 | 14 | RAD2DEG = 180.0 / (4.0 * atan(1)) 15 | DEG2RAD = 1.0 / RAD2DEG 16 | DEG2RADBY360 = 360.0 / RAD2DEG 17 | 18 | 19 | class ht_sine(Indicator): 20 | ''' 21 | Ehlers': Hilber Transform Sine 22 | 23 | Formula: 24 | - From *"Rocket Science for Traders: Digital Signal Processing Applications"* 25 | 26 | See: 27 | - https://www.amazon.com/Rocket-Science-Traders-Processing-Applications/dp/0471405671 28 | ''' 29 | group = 'cycle' 30 | alias = 'HT_SINE', 'HilberTransform_Sine' 31 | inputs = 'high', 'low' 32 | allowinputs = 1 # allow only 1 input if only 1 passed for ta-lib compat 33 | outputs = 'sine', 'leadsine' 34 | 35 | LOOKBACK_TOTAL = 64 36 | LOOKBACK_SMOOTH = 4 37 | LOOKBACK_HT = 7 38 | LOOKBACK_HT_SKIP = 25 # skip before applying ht 39 | LOOKBACK_SMOOTH_EXTRA = LOOKBACK_HT - LOOKBACK_SMOOTH 40 | LOOKBACK_REST = LOOKBACK_TOTAL - LOOKBACK_HT 41 | 42 | def __init__(self): 43 | # Choose p0, depending on passed number o inputs 44 | p0 = (self.i.high + self.i.low) / 2.0 if len(self.i) > 1 else self.i0 45 | 46 | # smooth p0 47 | p0smooth = (4.0*p0 + 3.0*p0(-1) + 2.0*p0(-2) + p0(-3)) / 10.0 48 | # Add the needed lookback for HT, not yet offered by the smoothing 49 | p0smooth._period(self.LOOKBACK_SMOOTH_EXTRA) 50 | 51 | sbuf = p0smooth(val=0.0) # copy p0smooth index, fill with 0.0 52 | lsbuf = p0smooth(val=0.0) # copy p0smooth index, fill with 0.0 53 | s, ls = p0smooth._applymulti(self._periodize, sbuf, lsbuf, raw=True) 54 | 55 | # _periodize - no auto period. Add non-count period, filled with nan 56 | self.o.sine = s._period(self.LOOKBACK_REST, val=np.nan) 57 | self.o.leadsine = ls._period(self.LOOKBACK_REST, val=np.nan) 58 | 59 | def _ht(self, x, adjperiod, i): 60 | ht0 = 0.0962*x[i] + 0.5769*x[i - 2] - 0.5769*x[i - 4] - 0.0962*x[i - 6] 61 | return ht0 * adjperiod 62 | 63 | def _periodize(self, price, sinebuf, leadsinebuf): 64 | # period 7 needed in _periodize for hilbert transform 65 | # p0smooth has: 4 and needs additional 3 before applying _periodize 66 | # actual "return" values to be used in __init__ for phase calculations 67 | LOOKBACK = self.LOOKBACK_HT 68 | LOOKIDX = LOOKBACK - 1 69 | LOOKSTART = LOOKIDX + self.LOOKBACK_HT_SKIP 70 | 71 | # circular buffers for ht calcs 72 | detrender = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 73 | i1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 74 | q1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 75 | 76 | # the first LOOKBACK elements of the input are ignored in the ta-lib 77 | # calculations for the detrender. Nullify them. 78 | price[0:LOOKSTART] = 0.0 79 | 80 | # Variables for the running calculations 81 | i2, q2, re, im, period, smoothperiod = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 82 | 83 | dcphase = 0.0 84 | 85 | for i in range(LOOKSTART, len(price)): 86 | adjperiod = 0.075*period + 0.54 # adj period_1 for ht transformx 87 | 88 | detrender.append(self._ht(price, adjperiod, i)) 89 | 90 | # New detrender val pushed, append to the right -1 is actual value 91 | i10 = detrender[-4] # 3 periods ago: -2, -3, -4 92 | q10 = self._ht(detrender, adjperiod, LOOKIDX) 93 | 94 | i1.append(i10), q1.append(q10) 95 | 96 | ji = self._ht(i1, adjperiod, LOOKIDX) # looback up to -6 97 | jq = self._ht(q1, adjperiod, LOOKIDX) 98 | 99 | i21, q21 = i2, q2 # need them for re/im before recalc 100 | 101 | i2 = i10 - jq 102 | q2 = q10 + ji 103 | 104 | i2 = 0.2*i2 + 0.8*i21 # smooth 105 | q2 = 0.2*q2 + 0.8*q21 # smooth 106 | 107 | re0 = i2*i21 + q2*q21 108 | im0 = i2*q21 - q2*i21 109 | 110 | re = 0.2*re0 + 0.8*re # smooth 111 | im = 0.2*im0 + 0.8*im # smooth 112 | 113 | period1 = period 114 | period = 360 / (RAD2DEG*atan(im / re)) if re and im else period1 115 | period = min(period, period1 * 1.5) 116 | period = max(period, period1 * 0.67) 117 | period = max(period, 6) 118 | period = min(period, 50) 119 | period = 0.2*period + 0.8*period1 # smooth 120 | 121 | smoothperiod = 0.33*period + 0.67*smoothperiod 122 | 123 | dcperiod = int(smoothperiod + 0.5) 124 | 125 | realpart, imagpart = 0.0, 0.0 126 | for dci in range(dcperiod): 127 | x = dci * DEG2RADBY360 / dcperiod 128 | y = price[i - dci] # backwards from last 129 | realpart += sin(x) * y 130 | imagpart += cos(x) * y 131 | 132 | abs_imagpart = abs(imagpart) 133 | if abs_imagpart > 0.0: 134 | dcphase = atan(realpart/imagpart) * RAD2DEG 135 | elif abs_imagpart <= 0.01: 136 | if realpart < 0.0: 137 | dcphase -= 90.0 138 | elif realpart > 0.0: 139 | dcphase += 90.0 140 | 141 | dcphase += 90.0 142 | dcphase += 360.0 / smoothperiod 143 | 144 | if imagpart < 0.0: 145 | dcphase += 180.0 146 | 147 | if dcphase > 315.0: 148 | dcphase -= 360.0 149 | 150 | sinebuf[i] = sin(dcphase * DEG2RAD) 151 | leadsinebuf[i] = sin((dcphase + 45.0) * DEG2RAD) 152 | 153 | return sinebuf, leadsinebuf 154 | -------------------------------------------------------------------------------- /btalib/indicators/ht_trendline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import collections 10 | from math import atan, fsum 11 | 12 | import numpy as np 13 | 14 | RAD2DEG = 180.0 / (4.0 * atan(1)) 15 | 16 | 17 | class ht_trendline(Indicator): 18 | ''' 19 | Ehlers': Hilber Transform Dominant Cycle Period 20 | 21 | Formula: 22 | - From *"Rocket Science for Traders: Digital Signal Processing Applications"* 23 | 24 | See: 25 | - https://www.amazon.com/Rocket-Science-Traders-Processing-Applications/dp/0471405671 26 | ''' 27 | group = 'overlap' 28 | alias = 'HT_TRENDLINE', 'HilberTransform_Trendline' 29 | inputs = 'high', 'low' 30 | allowinputs = 1 31 | outputs = 'trendline' 32 | 33 | LOOKBACK_TOTAL = 64 34 | LOOKBACK_SMOOTH = 4 35 | LOOKBACK_HT = 7 36 | LOOKBACK_HT_SKIP = 25 # skip before applying ht 37 | LOOKBACK_SMOOTH_EXTRA = LOOKBACK_HT - LOOKBACK_SMOOTH 38 | LOOKBACK_REST = LOOKBACK_TOTAL - LOOKBACK_HT 39 | 40 | def __init__(self): 41 | # Choose p0, depending on passed number o inputs 42 | p0 = (self.i.high + self.i.low) / 2.0 if len(self.i) > 1 else self.i0 43 | 44 | # smooth p0 45 | p0smooth = (4.0*p0 + 3.0*p0(-1) + 2.0*p0(-2) + p0(-3)) / 10.0 46 | # Add the needed lookback for HT, not yet offered by the smoothing 47 | p0smooth._period(self.LOOKBACK_SMOOTH_EXTRA) 48 | 49 | # ta-lib starts 1st at bar 37 50 | # p0smooth._period(33 - 6) # to give a minimum lookup to ht transforms 51 | # p0smooth._period(3) # to give a minimum lookup to ht transforms 52 | 53 | trendbuffer = p0smooth(val=0.0) # copy p0smooth index, fill with 0.0 54 | result = p0smooth._apply(self._periodize, p0, trendbuffer, raw=True) 55 | 56 | # _periodize - no auto period. Add non-count period, filled with nan 57 | self.o.trendline = result._period(self.LOOKBACK_REST, val=np.nan) 58 | 59 | def _ht(self, x, adjperiod, i): 60 | ht0 = 0.0962*x[i] + 0.5769*x[i - 2] - 0.5769*x[i - 4] - 0.0962*x[i - 6] 61 | return ht0 * adjperiod 62 | 63 | def _periodize(self, price, price0, trendline): 64 | # period 7 needed in _periodize for hilbert transform 65 | # p0smooth has: 4 and needs additional 3 before applying _periodize 66 | # actual "return" values to be used in __init__ for phase calculations 67 | LOOKBACK = self.LOOKBACK_HT 68 | LOOKIDX = LOOKBACK - 1 69 | LOOKSTART = LOOKIDX + self.LOOKBACK_HT_SKIP 70 | 71 | # circular buffers for ht calcs 72 | detrender = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 73 | i1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 74 | q1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 75 | 76 | # the first LOOKBACK elements of the input are ignored in the ta-lib 77 | # calculations for the detrender. Nullify them. 78 | price[0:LOOKSTART] = 0.0 79 | 80 | # Variables for the running calculations 81 | i2, q2, re, im, period, smoothperiod = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 82 | 83 | # trendline running calculations 84 | it0, it1, it2, it3 = 0.0, 0.0, 0.0, 0.0 85 | 86 | for i in range(LOOKSTART, len(price)): 87 | # Start round calculations 88 | adjperiod = 0.075*period + 0.54 # adj period1 for ht transformx 89 | 90 | detrender.append(self._ht(price, adjperiod, i)) 91 | 92 | # New detrender val pushed, append to the right -1 is actual value 93 | i10 = detrender[-4] # 3 periods ago: -2, -3, -4 94 | q10 = self._ht(detrender, adjperiod, LOOKIDX) 95 | 96 | i1.append(i10), q1.append(q10) 97 | 98 | ji = self._ht(i1, adjperiod, LOOKIDX) # looback up to -6 99 | jq = self._ht(q1, adjperiod, LOOKIDX) 100 | 101 | i21, q21 = i2, q2 # need them for re/im before recalc 102 | 103 | i2 = i10 - jq 104 | q2 = q10 + ji 105 | 106 | i2 = 0.2*i2 + 0.8*i21 # smooth 107 | q2 = 0.2*q2 + 0.8*q21 # smooth 108 | 109 | re0 = i2*i21 + q2*q21 110 | im0 = i2*q21 - q2*i21 111 | 112 | re = 0.2*re0 + 0.8*re # smooth 113 | im = 0.2*im0 + 0.8*im # smooth 114 | 115 | period1 = period 116 | period = 360 / (RAD2DEG*atan(im / re)) if re and im else period1 117 | period = min(period, period1*1.5) 118 | period = max(period, period1*0.67) 119 | period = max(period, 6) 120 | period = min(period, 50) 121 | period = 0.2*period + 0.8*period1 # smooth 122 | 123 | smoothperiod = 0.33*period + 0.67*smoothperiod 124 | 125 | dcperiod = int(smoothperiod + 0.5) 126 | it0 = fsum(price0[i - (dcperiod - 1):i + 1]) 127 | if dcperiod > 0: 128 | it0 /= dcperiod 129 | 130 | trendline[i] = (4*it0 + 3*it1 + 2*it2 + it3) / 10.0 131 | 132 | it1, it2, it3 = it0, it1, it2 # update values 133 | 134 | return trendline 135 | -------------------------------------------------------------------------------- /btalib/indicators/ht_trendmode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import collections 10 | from math import atan, fsum, cos, sin 11 | 12 | import numpy as np 13 | 14 | RAD2DEG = 180.0 / (4.0 * atan(1)) 15 | DEG2RAD = 1.0 / RAD2DEG 16 | DEG2RADBY360 = 360.0 / RAD2DEG 17 | 18 | 19 | class ht_trendmode(Indicator): 20 | ''' 21 | Ehlers': Hilber Transform Trend Mode 22 | 23 | Formula: 24 | - From *"Rocket Science for Traders: Digital Signal Processing Applications"* 25 | 26 | See: 27 | - https://www.amazon.com/Rocket-Science-Traders-Processing-Applications/dp/0471405671 28 | ''' 29 | group = 'cycle' 30 | alias = 'HT_TRENDMODE', 'HilberTransform_Trendmode' 31 | inputs = 'high', 'low' 32 | allowinputs = 1 33 | outputs = 'trendline' 34 | 35 | LOOKBACK_TOTAL = 64 36 | LOOKBACK_SMOOTH = 4 37 | LOOKBACK_HT = 7 38 | LOOKBACK_HT_SKIP = 25 # skip before applying ht 39 | LOOKBACK_SMOOTH_EXTRA = LOOKBACK_HT - LOOKBACK_SMOOTH 40 | LOOKBACK_REST = LOOKBACK_TOTAL - LOOKBACK_HT 41 | 42 | def __init__(self): 43 | # Choose p0, depending on passed number o inputs 44 | p0 = (self.i.high + self.i.low) / 2.0 if len(self.i) > 1 else self.i0 45 | 46 | # smooth p0 47 | p0smooth = (4.0*p0 + 3.0*p0(-1) + 2.0*p0(-2) + p0(-3)) / 10.0 48 | # Add the needed lookback for HT, not yet offered by the smoothing 49 | p0smooth._period(self.LOOKBACK_SMOOTH_EXTRA) 50 | 51 | # ta-lib starts 1st at bar 37 52 | # p0smooth._period(33 - 6) # to give a minimum lookup to ht transforms 53 | # p0smooth._period(3) # to give a minimum lookup to ht transforms 54 | 55 | trendbuffer = p0smooth(val=0.0) # copy p0smooth index, fill with 0.0 56 | result = p0smooth._apply(self._periodize, p0, trendbuffer) # cal 57 | 58 | # _periodize - no auto period. Add non-count period, filled with nan 59 | self.o.trendline = result._period(self.LOOKBACK_REST, val=np.nan) 60 | 61 | def _ht(self, x, adjperiod, i): 62 | ht0 = 0.0962*x[i] + 0.5769*x[i - 2] - 0.5769*x[i - 4] - 0.0962*x[i - 6] 63 | return ht0 * adjperiod 64 | 65 | def _periodize(self, price, price0, trendbuf): 66 | # period 7 needed in _periodize for hilbert transform 67 | # p0smooth has: 4 and needs additional 3 before applying _periodize 68 | # actual "return" values to be used in __init__ for phase calculations 69 | LOOKBACK = self.LOOKBACK_HT 70 | LOOKIDX = LOOKBACK - 1 71 | LOOKSTART = LOOKIDX + self.LOOKBACK_HT_SKIP 72 | 73 | # circular buffers for ht calcs 74 | detrender = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 75 | i1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 76 | q1 = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 77 | 78 | # the first LOOKBACK elements of the input are ignored in the ta-lib 79 | # calculations for the detrender. Nullify them. 80 | price[0:LOOKSTART] = 0.0 81 | 82 | # Variables for the running calculations 83 | i2, q2, re, im, period, smoothperiod = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 84 | 85 | # trendline running calculations 86 | it0, it1, it2, it3 = 0.0, 0.0, 0.0, 0.0 87 | 88 | daysintrend, dcphase, sine, leadsine = 0, 0.0, 0.0, 0.0 89 | 90 | for i in range(LOOKSTART, len(price)): 91 | # Start round calculations 92 | adjperiod = 0.075*period + 0.54 # adj period1 for ht transformx 93 | 94 | detrender.append(self._ht(price, adjperiod, i)) 95 | 96 | # New detrender val pushed, append to the right -1 is actual value 97 | i10 = detrender[-4] # 3 periods ago: -2, -3, -4 98 | q10 = self._ht(detrender, adjperiod, LOOKIDX) 99 | 100 | i1.append(i10), q1.append(q10) 101 | 102 | ji = self._ht(i1, adjperiod, LOOKIDX) # looback up to -6 103 | jq = self._ht(q1, adjperiod, LOOKIDX) 104 | 105 | i21, q21 = i2, q2 # need them for re/im before recalc 106 | 107 | i2 = i10 - jq 108 | q2 = q10 + ji 109 | 110 | i2 = 0.2*i2 + 0.8*i21 # smooth 111 | q2 = 0.2*q2 + 0.8*q21 # smooth 112 | 113 | re0 = i2*i21 + q2*q21 114 | im0 = i2*q21 - q2*i21 115 | 116 | re = 0.2*re0 + 0.8*re # smooth 117 | im = 0.2*im0 + 0.8*im # smooth 118 | 119 | period1 = period 120 | period = 360 / (RAD2DEG*atan(im / re)) if re and im else period1 121 | period = min(period, period1 * 1.5) 122 | period = max(period, period1 * 0.67) 123 | period = max(period, 6) 124 | period = min(period, 50) 125 | period = 0.2*period + 0.8*period1 # smooth 126 | 127 | smoothperiod = 0.33*period + 0.67*smoothperiod 128 | 129 | dcperiod = int(smoothperiod + 0.5) 130 | 131 | # Calculate dcphase component 132 | dcphase1 = dcphase # save prev value 133 | 134 | realpart, imagpart = 0.0, 0.0 135 | for dci in range(dcperiod): 136 | x = dci * DEG2RADBY360 / dcperiod 137 | y = price[i - dci] # backwards from last 138 | realpart += sin(x) * y 139 | imagpart += cos(x) * y 140 | 141 | abs_imagpart = abs(imagpart) 142 | if abs_imagpart > 0.0: 143 | dcphase = atan(realpart/imagpart) * RAD2DEG 144 | elif abs_imagpart <= 0.01: 145 | if realpart < 0.0: 146 | dcphase -= 90.0 147 | elif realpart > 0.0: 148 | dcphase += 90.0 149 | 150 | dcphase += 90.0 151 | dcphase += 360.0 / smoothperiod 152 | 153 | if imagpart < 0.0: 154 | dcphase += 180.0 155 | 156 | if dcphase > 315.0: 157 | dcphase -= 360.0 158 | 159 | # Calculate sine/leadsine components 160 | sine1, leadsine1 = sine, leadsine 161 | sine = sin(dcphase * DEG2RAD) 162 | leadsine = sin((dcphase + 45.0) * DEG2RAD) 163 | 164 | # Calculate trendline 165 | it0 = fsum(price0[i - (dcperiod - 1):i + 1]) 166 | if dcperiod > 0: 167 | it0 /= dcperiod 168 | 169 | trendline = (4*it0 + 3*it1 + 2*it2 + it3) / 10.0 170 | 171 | it1, it2, it3 = it0, it1, it2 # update values 172 | 173 | # Calculate the trend 174 | trend = 1 175 | 176 | if ((sine > leadsine and sine1 <= leadsine1) or 177 | (sine < leadsine and sine1 >= leadsine1)): # noqa: E129 178 | daysintrend = trend = 0 179 | 180 | daysintrend += 1 181 | 182 | if daysintrend < 0.5*smoothperiod: 183 | trend = 0 184 | 185 | if smoothperiod: 186 | phdiff = dcphase - dcphase1 187 | sm360 = 360 / smoothperiod 188 | if 0.67*sm360 < phdiff < 1.5*sm360: 189 | trend = 0 190 | 191 | if trendline: 192 | if abs(price[i]/trendline - 1.0) >= 0.015: 193 | trend = 1 194 | 195 | trendbuf[i] = trend 196 | 197 | return trendbuf 198 | -------------------------------------------------------------------------------- /btalib/indicators/kama.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, SumN 8 | from . import SEED_AVG, SEED_LAST 9 | 10 | 11 | class kama(Indicator): 12 | ''' 13 | Defined by Perry Kaufman in his book `"Smarter Trading"`. 14 | 15 | It is A Moving Average with a continuously scaled smoothing factor by 16 | taking into account market direction and volatility. The smoothing factor 17 | is calculated from 2 ExponetialMovingAverage smoothing factors, a fast one 18 | and slow one. 19 | 20 | If the market trends the value will tend to the fast ema smoothing 21 | period. If the market doesn't trend it will move towards the slow EMA 22 | smoothing period. 23 | 24 | It is a subclass of SmoothingMovingAverage, overriding once to account for 25 | the live nature of the smoothing factor 26 | 27 | Formula: 28 | - direction = close - close_period 29 | - volatility = sumN(abs(close - close_n), period) 30 | - effiency_ratio = abs(direction / volatility) 31 | - smoothing constant fast = 2 / (fast_period + 1) 32 | - smoothing constant slow = 2 / (slow_period + 1) 33 | 34 | - smoothing factor = pow(efficienty_ratio * (fast - slow) + slow), 2) 35 | - kama = ewm(data, alpha=smoothing factor, period=period) 36 | 37 | Notice that "smoothing factor" isn't a single value, hence the 38 | exponentially weighted moving average uses a value which changes for each 39 | step of the calculation. 40 | 41 | The standard seed is the simple moving average, use _seed=btalib.SEED_LAST 42 | to apply the "last" known value of the input as the seed (for compatibility 43 | this can be simply `True` or `1`) 44 | 45 | Because the dynamic smoothing constant has a larger period (+1) than the 46 | actual moving average, this average has alrady a seed value when the 47 | calculation can start. Hence the seed value (eithre the simple moving 48 | average or the last known value, is not seen because it can be calculated 49 | before the actual period comes in effect) 50 | 51 | See also: 52 | - https://school.stockcharts.com/doku.php?id=technical_indicators:kaufman_s_adaptive_moving_average 53 | - http://fxcodebase.com/wiki/index.php/Kaufman's_Adaptive_Moving_Average_(KAMA) 54 | - http://www.metatrader5.com/en/terminal/help/analytics/indicators/trend_indicators/ama 55 | - http://help.cqg.com/cqgic/default.htm#!Documents/adaptivemovingaverag2.htm 56 | ''' 57 | group = 'overlap' 58 | alias = 'KAMA', 'KaufmanAdaptiveMovingAverage' 59 | outputs = 'kama' 60 | params = ( 61 | ('period', 30, 'Period to consider'), 62 | ('fast', 2, 'Fast exponential smoothing factor'), 63 | ('slow', 30, 'Slow exponential smoothing factor'), 64 | ('_seed', SEED_AVG, 'Default to use average of n periods as seed'), 65 | ('_pvol', 1, 'Lookback period for volatility calculation'), 66 | ) 67 | 68 | def __init__(self): 69 | # Calculate components of effratio and the ratio itself 70 | direction = self.i0.diff(periods=self.p.period) 71 | volseries = self.i0.diff(periods=self.p._pvol) 72 | volatility = SumN(volseries.abs(), period=self.p.period) 73 | 74 | effratio = (direction / volatility).abs() # efficiency ratio 75 | 76 | # smoothing constast alpha values for fast and slow ema behaviors 77 | scfast = 2.0 / (self.p.fast + 1.0) 78 | scslow = 2.0 / (self.p.slow + 1.0) 79 | 80 | # Calculate the "smoothing constant": alpha input for exp smoothing 81 | sc = (effratio * (scfast - scslow) + scslow).pow(2) 82 | 83 | # Get the _ewm window function and calculate the dynamic mean on it 84 | self.o.kama = self.i0._ewm( 85 | span=self.p.period, alpha=sc, _seed=self.p._seed)._mean() 86 | 87 | def _talib(self, kwdict): 88 | '''Apply las value as seed, instead of average of period values''' 89 | kwdict.setdefault('_seed', SEED_LAST) 90 | -------------------------------------------------------------------------------- /btalib/indicators/macd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, ema 8 | 9 | 10 | class macd(Indicator): 11 | ''' 12 | Moving Average Convergence Divergence. Defined by Gerald Appel in the 70s. 13 | 14 | It measures the distance of a fast and a slow moving average to try to 15 | identify the trend. 16 | 17 | A second lagging moving average over the convergence-divergence should 18 | provide a "signal" upon being crossed by the macd 19 | 20 | Formula: 21 | - macd = ma(data, pfast) - ma(data, pslow) 22 | - signal = ma(macd, psignal) 23 | - histogram = macd - signal 24 | 25 | See: 26 | - http://en.wikipedia.org/wiki/MACD 27 | ''' 28 | group = 'momentum' 29 | alias = 'MACD', 'MovingAverageConvergenceDivergence', 'MACDEXT', 'MACDFIX' 30 | outputs = 'macd', 'signal', 'histogram' 31 | params = ( 32 | ('pfast', 12, 'Fast moving average period'), 33 | ('pslow', 26, 'Slow moving average period'), 34 | ('psignal', 9, 'Signal smoothing period'), 35 | ('_ma', ema, 'Moving average to use'), 36 | ('_masig', None, 'Signal moving average (if `None`, same as others)'), 37 | ) 38 | 39 | def __init__(self): 40 | ma1 = self.p._ma(self.i0, period=self.p.pfast, **self._talibkw) 41 | ma2 = self.p._ma(self.i0, period=self.p.pslow) 42 | self.o.macd = ma1 - ma2 43 | 44 | masignal = self.p._masig or self.p._ma # determine movav for signal 45 | self.o.signal = masignal(self.o.macd, period=self.p.psignal) 46 | 47 | self.o.histogram = self.o.macd - self.o.signal # simple diff of others 48 | 49 | _talibkw = {} # hold kwargs for ta-lib compatibility if requested 50 | 51 | def _talib(self, kwdict): 52 | '''Start fast ema calc delivery at the offset of the slow ema''' 53 | # Determine pslow param (from user or default) and pass it to ema as 54 | # the offset for the 1st data delivery of the calculations 55 | pslow = kwdict.get('pslow', self.__class__.params.get('pslow')) 56 | 57 | # _talibkw is a class attribute (empty dict) 58 | # This create a *specifi* instance attribute obscuring the class 59 | # attibute and filled with a value. 60 | self._talibkw = {'poffset': pslow} 61 | -------------------------------------------------------------------------------- /btalib/indicators/madev.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, sma 8 | 9 | import numpy as np 10 | 11 | 12 | class mad(Indicator): 13 | ''' 14 | Calculates the Mean Absolute Deviation ('mad') of the input over a given 15 | period 16 | See: 17 | - https://en.wikipedia.org/wiki/Average_absolute_deviation 18 | ''' 19 | group = 'statistic' 20 | alias = 'MAD', 'meandev', 'MeanDev', 'MeanDeviation', 'MeanAbsDeviation' 21 | outputs = 'meandev' 22 | params = ( 23 | ('period', 20, 'Period to consider'), 24 | ('_ma', sma, 'Moving Average to use'), 25 | ) 26 | 27 | _mad = lambda self, x: np.fabs(x - x.mean()).mean() # noqa: E731 28 | 29 | def __init__(self, mean=None): 30 | r = self.i0.rolling(window=self.p.period) # get rolling win of period 31 | self.o.meandev = r.apply(self._mad, raw=False) # apply mean abs dev 32 | -------------------------------------------------------------------------------- /btalib/indicators/mama.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, arctan, SEED_ZERO 8 | 9 | import collections 10 | from math import atan 11 | 12 | import numpy as np 13 | 14 | RAD2DEG = 180.0 / (4.0 * atan(1)) 15 | 16 | 17 | class mama(Indicator): 18 | ''' 19 | Quoting Ehlers: "The MESA Adaptive Moving Average (MAMA) adapts to price 20 | movement in an entirely new and unique way. The adapation is based on the 21 | rate change of phase as measured by the Hilbert Transform Discriminator" 22 | 23 | Formula: 24 | - The formula is overly complex. See the linked PDF from Ehlers himself 25 | 26 | See: 27 | - https://www.mesasoftware.com/papers/MAMA.pdf 28 | ''' 29 | group = 'overlap' 30 | alias = 'MAMA', 'MesaAdaptiveMovingAverage' 31 | inputs = 'high', 'low' 32 | allowinputs = 1 33 | outputs = 'mama', 'fama' 34 | params = ( 35 | ('fastlimit', 0.5, 'Fast Limit'), 36 | ('slowlimit', 0.05, 'Fast Limit'), 37 | ) 38 | 39 | LOOKBACK_TOTAL = 33 40 | LOOKBACK_SMOOTH = 4 41 | LOOKBACK_HT = 7 42 | LOOKBACK_HT_SKIP = 0 # skip before applying ht 43 | LOOKBACK_SMOOTH_EXTRA = LOOKBACK_HT - LOOKBACK_SMOOTH 44 | LOOKBACK_REST = LOOKBACK_TOTAL - LOOKBACK_HT 45 | 46 | def __init__(self): 47 | # Choose p0, depending on passed number o inputs 48 | p0 = (self.i.high + self.i.low) / 2.0 if len(self.i) > 1 else self.i0 49 | 50 | # smooth p0 51 | p0smooth = (4.0*p0 + 3.0*p0(-1) + 2.0*p0(-2) + 1.0*p0(-3)) / 10.0 52 | # Add the needed lookback for HT, not yet offered by the smoothing 53 | p0smooth._period(self.LOOKBACK_SMOOTH_EXTRA) 54 | 55 | # zero-fill to ensure ht lookbacks deliver results (nan would break) 56 | i1, q1 = p0smooth(val=0.0), p0smooth(val=0.0) 57 | 58 | # use applymulti to get more than one result back 59 | i1, q1 = p0smooth._applymulti(self._periodize, i1, q1, raw=True) 60 | 61 | # i1 carries a -3 from detrender and q1 a -6 (LOOKBACK_HT - 1). Add the 62 | # largest dominant to q1, because both work together now 63 | q1._period(self.LOOKBACK_HT - 1) 64 | 65 | # where i1 == 0 fore arctan to return also 0, with Ehlers formula 66 | atanq1num = q1.mask(i1 == 0.0, 0.0) 67 | phase = RAD2DEG * arctan(atanq1num / i1).fillna(0.0) 68 | 69 | # ta-lib cals deltaphase starting as soon as phase has 1 value in spite 70 | # of using phase(-1) which would be void, but it is considered as 71 | # 0.0. Reduce the phase period by 1 and set the initial value to 0. 72 | # This behavior matches all other calculations in _speriodize 73 | phase._period(-1, val=0.0) # at minper rel idx 0 => 0.0 74 | 75 | deltaphase = (phase(-1) - phase).clip(lower=1.0) 76 | alpha = (self.p.fastlimit / deltaphase).clip(lower=self.p.slowlimit) 77 | 78 | # span set to use p0, but let alpha dominate if period is greater 79 | _mama = p0._ewm(alpha=alpha, span=1, _seed=SEED_ZERO)._mean() 80 | # Add no span, to let the fama calculation use the entire _mama range 81 | _fama = _mama._ewm(alpha=alpha*0.5, _seed=SEED_ZERO)._mean() 82 | 83 | # _periodize - no auto period. Add non-count period, filled with nan 84 | # removing what was already added to q1 85 | _mama._period(self.LOOKBACK_REST - self.LOOKBACK_HT + 1, val=np.nan) 86 | _fama._period(self.LOOKBACK_REST - self.LOOKBACK_HT + 1, val=np.nan) 87 | 88 | self.o.mama = _mama 89 | self.o.fama = _fama 90 | 91 | def _ht(self, x, adjperiod, i): 92 | ht0 = 0.0962*x[i] + 0.5769*x[i - 2] - 0.5769*x[i - 4] - 0.0962*x[i - 6] 93 | return ht0 * adjperiod 94 | 95 | def _periodize(self, price, i1, q1): 96 | # period 7 needed in _periodize for hilbert transform 97 | # p0smooth has: 4 and needs additional 3 before applying _periodize 98 | # actual "return" values to be used in __init__ for phase calculations 99 | LOOKBACK = self.LOOKBACK_HT 100 | LOOKIDX = LOOKBACK - 1 101 | LOOKSTART = LOOKIDX + self.LOOKBACK_HT_SKIP 102 | 103 | # circular buffers for ht calcs 104 | detrender = collections.deque([0.0] * LOOKBACK, maxlen=LOOKBACK) 105 | 106 | # the first LOOKBACK elements of the input are ignored in the ta-lib 107 | # calculations for the detrender. Nullify them. 108 | price[0:LOOKSTART] = 0.0 109 | 110 | i2, q2, re, im, period = 0.0, 0.0, 0.0, 0.0, 0.0 111 | 112 | for i in range(LOOKSTART, len(price)): 113 | adjperiod = 0.075*period + 0.54 # adj period_1 for ht transformx 114 | 115 | detrender.append(self._ht(price, adjperiod, i)) 116 | 117 | # New detrender val pushed, append to the right -1 is actual value 118 | i1[i] = i10 = detrender[-4] # 3 periods ago: -2, -3, -4 119 | q1[i] = q10 = self._ht(detrender, adjperiod, LOOKIDX) 120 | 121 | ji = self._ht(i1, adjperiod, i) 122 | jq = self._ht(q1, adjperiod, i) 123 | 124 | i21, q21 = i2, q2 # keep for next round 125 | 126 | i2 = i10 - jq 127 | q2 = q10 + ji 128 | 129 | i2 = 0.2*i2 + 0.8*i21 # smooth 130 | q2 = 0.2*q2 + 0.8*q21 # smooth 131 | 132 | re0 = i2*i21 + q2*q21 133 | im0 = i2*q21 - q2*i21 134 | 135 | re = 0.2*re0 + 0.8*re # smooth 136 | im = 0.2*im0 + 0.8*im # smooth 137 | 138 | period1 = period 139 | period = 360 / (RAD2DEG*atan(im / re)) if re and im else period1 140 | period = min(period, period1 * 1.5) 141 | period = max(period, period1 * 0.67) 142 | period = max(period, 6) 143 | period = min(period, 50) 144 | period = 0.2*period + 0.8*period1 # smooth 145 | 146 | # return the results 147 | return i1, q1 148 | -------------------------------------------------------------------------------- /btalib/indicators/math.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import numpy as np 10 | 11 | 12 | def _to_radians(line, convert): 13 | return line.apply(np.radians) if convert else line 14 | 15 | 16 | class _mathrad(Indicator): 17 | group = 'math' 18 | params = ( 19 | ('degrees', False, 'Input is in degrees, not radians ... convert it!'), 20 | ) 21 | 22 | def __init__(self): 23 | self.o[0] = _to_radians(self.i0, self.p.degrees).apply(self._func) 24 | 25 | 26 | # Trigonometric math transforms 27 | 28 | class sin(_mathrad): 29 | ''' 30 | Calculates the `sine` function of the input 31 | 32 | Formula: 33 | - sin = sine(data) 34 | 35 | See also: 36 | - https://en.wikipedia.org/wiki/Trigonometric_functions 37 | ''' 38 | alias = 'SIN', 'sine' 39 | outputs = 'sin' 40 | 41 | _func = np.sin 42 | 43 | 44 | class cos(_mathrad): 45 | ''' 46 | Calculates the `cosine` function of the input 47 | 48 | Formula: 49 | - cos = cosine(data) 50 | 51 | See also: 52 | - https://en.wikipedia.org/wiki/Trigonometric_functions 53 | ''' 54 | alias = 'COS', 'cosine' 55 | outputs = 'cos' 56 | 57 | _func = np.cos 58 | 59 | 60 | class tan(_mathrad): 61 | ''' 62 | Calculates the `tangent` function of the input 63 | 64 | Formula: 65 | - tan = tangent(data) 66 | 67 | See also: 68 | - https://en.wikipedia.org/wiki/Trigonometric_functions 69 | ''' 70 | alias = 'TAN', 'tangent' 71 | outputs = 'tan' 72 | 73 | _func = np.tan 74 | 75 | 76 | # Co-Trigonometric math transforms 77 | 78 | class sinh(_mathrad): 79 | ''' 80 | Calculates the `cosecant` function of the input (1/sine) 81 | 82 | Formula: 83 | - sinh = cosecant(data) 84 | 85 | See also: 86 | - https://en.wikipedia.org/wiki/Trigonometric_functions 87 | ''' 88 | alias = 'SINH', 'cosecant' 89 | outputs = 'sinh' 90 | 91 | _func = np.sinh 92 | 93 | 94 | class cosh(_mathrad): 95 | ''' 96 | Calculates the `secant` function of the input (1/cosine) 97 | 98 | Formula: 99 | - cosh = secant(data) 100 | 101 | See also: 102 | - https://en.wikipedia.org/wiki/Trigonometric_functions 103 | ''' 104 | alias = 'COSH', 'secant' 105 | outputs = 'cosh' 106 | 107 | _func = np.cosh 108 | 109 | 110 | class tanh(_mathrad): 111 | ''' 112 | Calculates the `cotangent` function of the input (1/tangent) 113 | 114 | Formula: 115 | - tanh = cotangent(data) 116 | 117 | See also: 118 | - https://en.wikipedia.org/wiki/Trigonometric_functions 119 | ''' 120 | alias = 'TANH', 'cotangent' 121 | outputs = 'tanh' 122 | 123 | _func = np.tanh 124 | 125 | 126 | # Inverse trigonometric math transforms 127 | 128 | class asin(_mathrad): 129 | ''' 130 | Calculates the `arcsin` function of the input, i.e.: the inverse 131 | trigonometric function of `sin` 132 | 133 | Formula: 134 | - asin = arcsin(data) 135 | 136 | See also: 137 | - https://en.wikipedia.org/wiki/Inverse_trigonometric_functions 138 | ''' 139 | alias = 'ASIN', 'arcsin', 'arcsine' 140 | outputs = 'asin' 141 | 142 | _func = np.arcsin 143 | 144 | 145 | class acos(_mathrad): 146 | ''' 147 | Calculates the `arccos` function of the input, i.e.: the inverse 148 | trigonometric function of `cos` 149 | 150 | Formula: 151 | - acos = arccos(data) 152 | 153 | See also: 154 | - https://en.wikipedia.org/wiki/Inverse_trigonometric_functions 155 | ''' 156 | alias = 'ACOS', 'arccos', 'arccosine' 157 | outputs = 'acos' 158 | 159 | _func = np.arccos 160 | 161 | 162 | class atan(_mathrad): 163 | ''' 164 | Calculates the `arctan` function of the input, i.e.: the inverse 165 | trigonometric function of `tan` 166 | 167 | Formula: 168 | - atan = arctan(data) 169 | 170 | See also: 171 | - https://en.wikipedia.org/wiki/Inverse_trigonometric_functions 172 | ''' 173 | alias = 'ATAN', 'arctan', 'arctangent' 174 | outputs = 'atan' 175 | 176 | _func = np.arctan 177 | 178 | 179 | # Non-trigonometric math transforms 180 | 181 | class ceil(Indicator): 182 | ''' 183 | Calculates the `ceil` function which maps the input to the least integer 184 | greater than or equal to the input 185 | 186 | Formula: 187 | - ceil = ceil(data) 188 | 189 | See also: 190 | - https://en.wikipedia.org/wiki/Floor_and_ceiling_functions 191 | ''' 192 | group = 'math' 193 | alias = 'CEIL' 194 | outputs = 'ceil' 195 | 196 | def __init__(self): 197 | self.o.ceil = self.i0.apply(np.ceil) 198 | 199 | 200 | class exp(Indicator): 201 | ''' 202 | Calculates the natural logarithm of the input, i.e.: the logarithm with 203 | base `e` 204 | 205 | Formula: 206 | - exp = exp(data) 207 | 208 | See also: 209 | - https://en.wikipedia.org/wiki/Exponential_function 210 | ''' 211 | group = 'math' 212 | alias = 'EXP' 213 | outputs = 'exp' 214 | 215 | def __init__(self): 216 | self.o.exp = self.i0.apply(np.exp) 217 | 218 | 219 | class floor(Indicator): 220 | ''' 221 | Calculates the `floor` function which maps the input to the greatest 222 | integer least than or equal to the input 223 | 224 | Formula: 225 | - floor = floor(data) 226 | 227 | See also: 228 | - https://en.wikipedia.org/wiki/Floor_and_ceiling_functions 229 | ''' 230 | group = 'math' 231 | alias = 'FLOOR' 232 | outputs = 'floor' 233 | 234 | def __init__(self): 235 | self.o.floor = self.i0.apply(np.floor) 236 | 237 | 238 | class ln(Indicator): 239 | ''' 240 | Calculates the natural logarithm of the input, i.e.: the logarithm with 241 | base `e` 242 | 243 | Formula: 244 | - ln = ln(data) 245 | 246 | See also: 247 | - https://en.wikipedia.org/wiki/Natural_logarithm 248 | ''' 249 | group = 'math' 250 | alias = 'LN', 'log', 'LOG' 251 | outputs = 'ln' 252 | 253 | def __init__(self): 254 | self.o.ln = self.i0.apply(np.log) 255 | 256 | 257 | class log10(Indicator): 258 | ''' 259 | Calculates the common logarithm, i.e.: the logarithm with base 10, of the 260 | input 261 | 262 | Formula: 263 | - log10 = log10(data) 264 | 265 | See also: 266 | - https://en.wikipedia.org/wiki/Common_logarithm 267 | ''' 268 | group = 'math' 269 | alias = 'LOG10' 270 | outputs = 'log10' 271 | 272 | def __init__(self): 273 | self.o.log10 = self.i0.apply(np.log10) 274 | 275 | 276 | class sqrt(Indicator): 277 | ''' 278 | Calculates the square root of the input 279 | 280 | Formula: 281 | - sqrt = sqrt(data) 282 | 283 | See also: 284 | - https://en.wikipedia.org/wiki/Square_root 285 | ''' 286 | group = 'math' 287 | alias = 'SQRT' 288 | outputs = 'sqrt' 289 | 290 | def __init__(self): 291 | self.o.sqrt = self.i0.pow(0.5) 292 | -------------------------------------------------------------------------------- /btalib/indicators/mathop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import itertools 10 | 11 | import numpy as np 12 | 13 | 14 | # ## over the entire series 15 | 16 | class add(Indicator): 17 | ''' 18 | Calculates the summation of the two inputs 19 | 20 | Formula: 21 | - add = data0 + data1 22 | ''' 23 | group = 'mathop' 24 | alias = 'ADD' 25 | inputs = 'input1', 'input2' 26 | outputs = 'add' 27 | 28 | def __init__(self): 29 | self.o.add = self.i.input1 + self.i.input2 30 | 31 | 32 | class div(Indicator): 33 | ''' 34 | Calculates the division of the two inputs 35 | 36 | Formula: 37 | - div = data0 / data1 38 | ''' 39 | group = 'mathop' 40 | alias = 'DIV' 41 | inputs = 'input1', 'input2' 42 | outputs = 'div' 43 | 44 | def __init__(self): 45 | self.o.div = self.i.input1 / self.i.input2 46 | 47 | 48 | class mult(Indicator): 49 | ''' 50 | Calculates the multiplication of the two inputs 51 | 52 | Formula: 53 | - mult = data0 * data1 54 | ''' 55 | group = 'mathop' 56 | alias = 'MULT' 57 | inputs = 'input1', 'input2' 58 | outputs = 'mult' 59 | 60 | def __init__(self): 61 | self.o.mult = self.i.input1 * self.i.input2 62 | 63 | 64 | class sub(Indicator): 65 | ''' 66 | Calculates the subtraction of the two inputs 67 | 68 | Formula: 69 | - sub = data0 - data1 70 | ''' 71 | group = 'mathop' 72 | alias = 'SUB' 73 | inputs = 'input1', 'input2' 74 | outputs = 'sub' 75 | 76 | def __init__(self): 77 | self.o.sub = self.i.input1 - self.i.input2 78 | 79 | 80 | # ## over a period 81 | 82 | class max(Indicator): 83 | ''' 84 | Rolling maximum over `period` of the input 85 | 86 | Formula: 87 | - max = max(data, period) 88 | ''' 89 | group = 'mathop' 90 | alias = 'highest', 'Highest', 'maxn', 'MaxN', 'MAX' 91 | outputs = 'max' 92 | params = ( 93 | ('period', 30, 'Period to consider'), 94 | ) 95 | 96 | def __init__(self): 97 | self.o.max = self.i0.rolling(window=self.p.period).max() 98 | 99 | 100 | class min(Indicator): 101 | ''' 102 | Rolling minimum over `period` of the input 103 | 104 | Formula: 105 | - min = min(data, period) 106 | ''' 107 | group = 'mathop' 108 | alias = 'lowest', 'Lowest', 'minn', 'MinN', 'MIN' 109 | outputs = 'min' 110 | params = ( 111 | ('period', 30, 'Period to consider'), 112 | ) 113 | 114 | def __init__(self): 115 | self.o.min = self.i0.rolling(window=self.p.period).min() 116 | 117 | 118 | class minmax(Indicator): 119 | ''' 120 | Rolling the minimum and maximo over `period` of the input 121 | 122 | Formula: 123 | - min = min(data, period) 124 | - min = max(data, period) 125 | ''' 126 | group = 'mathop' 127 | alias = 'MINMAX' 128 | outputs = 'min', 'max' 129 | params = ( 130 | ('period', 30, 'Period to consider'), 131 | ) 132 | 133 | def __init__(self): 134 | self.o.min = min(self.i0, period=self.p.period) 135 | self.o.max = max(self.i0, period=self.p.period) 136 | 137 | 138 | class maxindex(Indicator): 139 | ''' 140 | Rolling index of the max value over a period 141 | 142 | Formula: 143 | - maxindex = data.index(max(data, period)) 144 | ''' 145 | group = 'mathop' 146 | alias = 'MAXINDEX' 147 | outputs = 'maxindex' 148 | params = ( 149 | ('period', 30, 'Period to consider'), 150 | ('_absidx', False, 'Return maxindex over the entire period'), 151 | ) 152 | 153 | def _argmax(self, x): 154 | return np.argmax(x) + (next(self._count) * self.p._absidx) 155 | 156 | def __init__(self): 157 | i0rolling = self.i0.rolling(window=self.p.period) # prep rolling win 158 | 159 | if not self.p._absidx: # maxindex relative to window period 160 | self.o.maxindex = i0rolling.apply(np.argmax) 161 | else: 162 | # maxindex is absolute with respect to all previous vals in array 163 | self._count = itertools.count() # help win rel-index => absolute 164 | self.o.maxindex = i0rolling.apply(self._argmax)._series.fillna(0) 165 | # using the raw _series resets period to 1, fillna fills as ta-lib 166 | 167 | def _talib(self, kwdict): 168 | '''ta-lib returns 0 as index during the warm-up period and then returns the 169 | absolute index over the entire series and not over the window period 170 | ''' 171 | kwdict.setdefault('_absidx', True) 172 | 173 | 174 | class minindex(Indicator): 175 | ''' 176 | Rolling index of the max value over a period 177 | 178 | Formula: 179 | - maxindex = data.index(max(data, period)) 180 | ''' 181 | group = 'mathop' 182 | alias = 'MININDEX' 183 | outputs = 'minindex' 184 | params = ( 185 | ('period', 30, 'Period to consider'), 186 | ('_absidx', False, 'Return maxindex over the entire period'), 187 | ) 188 | 189 | def _argmin(self, x): 190 | return np.argmin(x) + (next(self._count) * self.p._absidx) 191 | 192 | def __init__(self): 193 | i0rolling = self.i0.rolling(window=self.p.period) # prep rolling win 194 | 195 | if not self.p._absidx: # maxindex relative to window period 196 | self.o.minindex = i0rolling.apply(np.argmin) 197 | else: 198 | # maxindex is absolute with respect to all previous vals in array 199 | self._count = itertools.count() # help win rel-index => absolute 200 | self.o.minindex = i0rolling.apply(self._argmin)._series.fillna(0) 201 | # using the raw _series resets period to 1, fillna fills as ta-lib 202 | 203 | def _talib(self, kwdict): 204 | '''ta-lib returns 0 as index during the warm-up period and then returns the 205 | absolute index over the entire series and not over the window period 206 | ''' 207 | kwdict.setdefault('_absidx', True) 208 | 209 | 210 | class minmaxindex(Indicator): 211 | ''' 212 | Rolling index of the max value over a period 213 | 214 | Formula: 215 | - maxindex = data.index(max(data, period)) 216 | ''' 217 | group = 'mathop' 218 | alias = 'MINMAXINDEX' 219 | outputs = 'minindex', 'maxindex' 220 | params = ( 221 | ('period', 30, 'Period to consider'), 222 | ('_absidx', False, 'Return maxindex over the entire period'), 223 | ) 224 | 225 | def __init__(self, **kwargs): 226 | kwargs.update(**self.params) 227 | self.o.minindex = minindex(self.i0, **kwargs) 228 | self.o.maxindex = maxindex(self.i0, **kwargs) 229 | 230 | def _talib(self, kwdict): 231 | '''ta-lib returns 0 as index during the warm-up period and then returns 232 | the absolute index over the entire series and not over the window 233 | period 234 | ''' 235 | kwdict.setdefault('_absidx', True) 236 | kwdict.setdefault('_talib', True) # re-set value for sub-indicators 237 | 238 | 239 | class sum(Indicator): 240 | ''' 241 | Rolling sum over `period` of the input 242 | 243 | Formula: 244 | - sum = sum(data, period) 245 | ''' 246 | group = 'mathop' 247 | alias = 'sumn', 'Sum', 'SumN', 'SUM' 248 | outputs = 'sum' 249 | params = ( 250 | ('period', 30, 'Period to consider'), 251 | ) 252 | 253 | def __init__(self): 254 | self.o.sum = self.i0.rolling(window=self.p.period).sum() 255 | -------------------------------------------------------------------------------- /btalib/indicators/mavp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, sma 8 | 9 | 10 | def _mavp(closes, periods, **ma): 11 | # kwargs cannot have ints as keys, re-convert them 12 | ma = {int(k): v for k, v in ma.items()} 13 | for i, c in enumerate(closes): 14 | closes[i] = ma[periods[i]][i] 15 | 16 | return closes 17 | 18 | 19 | class mavp(Indicator): 20 | ''' 21 | Moving Average Variable Period. 22 | 23 | It takes two inputs of equal length 24 | 25 | - data (usually the "close" 26 | - periods 27 | 28 | It delivers for each timepoint "i", the value of the moving average 29 | dictated by period[i], at point it (movingaverage[i]) 30 | 31 | Formula: 32 | - mavp[i] = MovingAverage(data, period[i])[i] 33 | 34 | See also: 35 | - (None) 36 | ''' 37 | group = 'overlap' 38 | alias = 'MAVP', 'MovingAverageVariablePeriod' 39 | inputs = 'close', 'periods' 40 | outputs = 'mavp' 41 | params = ( 42 | ('minperiod', 2, 'Minimum allowed period for the moving averages'), 43 | ('maxperiod', 30, 'Maximum allowed period for the moving averages'), 44 | ('_ma', sma, 'Moving Average to use'), 45 | ) 46 | 47 | def __init__(self): 48 | periods = self.i.periods 49 | # restrict to min/max period 50 | periods = periods.clip(lower=self.p.minperiod, upper=self.p.maxperiod) 51 | 52 | # Calculate only the needed mas by getting the unique periods 53 | uperiods = periods.unique() # capped and unique now 54 | # use str(p) to be able to pass it as kwargs in _apply 55 | ma = {str(p): self.p._ma(self.i.close, period=p) for p in uperiods} 56 | 57 | # ta-lib delivers at param "maxperiod" and not where already possible 58 | maxp = max(uperiods) if not self._talib_ else self.p.maxperiod 59 | 60 | pclose = self.i.close._period(maxp, rolling=True) 61 | self.o.mavp = pclose._apply(_mavp, periods, raw=True, **ma) 62 | -------------------------------------------------------------------------------- /btalib/indicators/mfi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, sumn 8 | 9 | 10 | class mfi(Indicator): 11 | ''' 12 | Created by Gene Quong and Avrum Soudack to identify buying/selling 13 | pressure by combining price and volume in the calculation. 14 | 15 | Pressure is positive if the (typical) price increases and negative if it 16 | decreases. The volume is the weight factor for how much pressure is being 17 | exercised on the asset. 18 | 19 | Formula: 20 | - tp = typical_price = (high + low + close) / 3 21 | - money_flow_raw = tp * volume 22 | - flow_positive = Sum(money_flow_raw * (tp > tp(-1)), period) 23 | - flow_netagive = Sum(money_flow_raw * (tp < tp(-1)), period) 24 | - money_flow_ratio = flow_positive / flow_negative 25 | - mfi 100 - 100 / (1 + money_flow_ratio) 26 | 27 | See: 28 | - https://school.stockcharts.com/doku.php?id=technical_indicators:money_flow_index_mfi 29 | - https://www.investopedia.com/terms/m/mfi.asp 30 | ''' 31 | group = 'momentum' 32 | alias = 'MFI', 'MoneyFlowIndicator' 33 | inputs = 'high', 'low', 'close', 'volume' 34 | outputs = 'mfi' 35 | params = ( 36 | ('period', 14, 'Period to consider'), 37 | ) 38 | 39 | def __init__(self): 40 | tprice = (self.i.high + self.i.low + self.i.close) / 3.0 41 | mfraw = tprice * self.i.volume 42 | flowpos = sumn(mfraw * (tprice > tprice(-1)), period=self.p.period) 43 | flowneg = sumn(mfraw * (tprice < tprice(-1)), period=self.p.period) 44 | self.o.mfi = 100.0 - 100.0 / (1.0 + (flowpos / flowneg)) 45 | -------------------------------------------------------------------------------- /btalib/indicators/midpoint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, highest, lowest 8 | 9 | 10 | class midprice(Indicator, inputs_override=True): 11 | ''' 12 | Calculates the middle price of the highest high/lowest low across a period 13 | 14 | Formula: 15 | - midprice = (Highest(high, period) + Lowest(low, period)) / 2.0 16 | 17 | See: 18 | - https://www.tradingtechnologies.com/xtrader-help/x-study/technical-indicator-definitions/midprice-midpri/ 19 | ''' 20 | group = 'overlap' 21 | alias = 'MIDPRICE', 'MidPrice' 22 | inputs = 'high', 'low' # notice inputs_override in class def 23 | outputs = 'midprice' 24 | params = ( 25 | ('period', 14, 'Period to consider'), 26 | ) 27 | 28 | def __init__(self): 29 | hh = highest(self.i.high, period=self.p.period) 30 | ll = lowest(self.i.low, period=self.p.period) 31 | self.o.midprice = (hh + ll) / 2.0 32 | 33 | 34 | class midpoint(Indicator): 35 | ''' 36 | Calculates the middle price of the highest/lowest price across a period 37 | 38 | Formula: 39 | - midpoint = (Highest(close, period) + Lowest(close, period)) / 2.0 40 | 41 | See: 42 | - https://www.tradingtechnologies.com/xtrader-help/x-study/technical-indicator-definitions/midpoint-midpnt/ 43 | ''' 44 | group = 'overlap' 45 | alias = 'MIDPOINT', 'MidPoint' 46 | outputs = 'midpoint' 47 | params = ( 48 | ('period', 14, 'Period to consider'), 49 | ) 50 | 51 | def __init__(self): 52 | self.o.midpoint = midprice(self.i0, self.i0, period=self.p.period) 53 | -------------------------------------------------------------------------------- /btalib/indicators/mom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class mom(Indicator): 11 | ''' 12 | Measures the change in price by calculating the difference between the 13 | current price and the price from a given period ago 14 | 15 | 16 | Formula: 17 | - momentum = data - data(-period) 18 | 19 | See: 20 | - http://en.wikipedia.org/wiki/Momentum_(technical_analysis) 21 | ''' 22 | groups = 'momentum' 23 | alias = 'momentum', 'MOM', 'Momentum' 24 | outputs = 'mom' 25 | 26 | params = ( 27 | ('period', 10, 'Period to consider',), 28 | ) 29 | 30 | def __init__(self): 31 | self.l.mom = self.i0.diff(periods=self.p.period) 32 | -------------------------------------------------------------------------------- /btalib/indicators/obv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import numpy as np 10 | 11 | 12 | class obv(Indicator): 13 | ''' 14 | Originally called "continuous volume" by Woods and Vignola, it was later 15 | named "on-balance volume" by Joseph Granville in his 1963 book Granville's 16 | New Key to Stock Market Profits. 17 | 18 | On Balance Volume (OBV) measures buying and selling pressure as a 19 | cumulative indicator, adding volume on up days and subtracting it on down 20 | days. 21 | 22 | Formula: 23 | - close > close(-1) => vol_pressure = +volume 24 | - close < close(-1) => vol_pressure = -volume 25 | - close == close(-1) => vol_pressure = 0 26 | 27 | - obv = obv(-1) + vol_pressure 28 | 29 | See also: 30 | - https://en.wikipedia.org/wiki/On-balance_volume 31 | - https://www.investopedia.com/terms/o/onbalancevolume.asp 32 | - https://school.stockcharts.com/doku.php?id=technical_indicators:on_balance_volume_obv 33 | ''' 34 | group = 'volume' 35 | alias = 'OBV', 'OnBalanceVolume' 36 | inputs = 'close', 'volume' # could have done inputs_extend = 'volume' 37 | outputs = 'obv' 38 | params = ( 39 | ('_period', 1, 'Period for `close` comparison'), 40 | ) 41 | 42 | def __init__(self): 43 | close1 = self.i.close.diff(periods=self.p._period) 44 | 45 | if self._talib_: # ## black voodoo to overcome ta-lib errors 46 | # Force use of first valid value as positive volume (ta-lib rules) 47 | close1._period(-1, val=1.0) # reduce minperiod, fill region with 1.0 48 | 49 | self.o.obv = (self.i.volume * close1.apply(np.sign)).cumsum() 50 | 51 | # non-numpy alternative also one-line vectorized formulation 52 | # self.o.obv = (self.i.volume * (close1 / close1.abs())).cumsum() 53 | 54 | def _talib(self, kwdict): 55 | '''Use first day volume as *positive* seed value''' 56 | -------------------------------------------------------------------------------- /btalib/indicators/ppo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, ema, sma 8 | 9 | 10 | class _posc(Indicator): 11 | '''Base clas or po (points) and ppo (percentage)''' 12 | 13 | _fast = False 14 | 15 | params = ( 16 | ('pfast', 12, 'Fast moving average period'), 17 | ('pslow', 26, 'Slow moving average period'), 18 | ('_ma', ema, 'Moving average to use'), 19 | ) 20 | 21 | def __init__(self): 22 | self.ma1 = self.p._ma(self.i0, period=self.p.pfast) 23 | self.ma2 = self.p._ma(self.i0, period=self.p.pslow) 24 | if self._fast: # swap the moving averages for ppofast 25 | self.ma1, self.ma2 = self.ma2, self.ma1 26 | 27 | def _talib(self, kwdict): 28 | '''Set moving average to sma instead of ema''' 29 | kwdict.setdefault('_ma', sma) 30 | 31 | 32 | class apo(_posc): 33 | ''' 34 | Shows the difference between a short and long exponential moving 35 | averages expressed in points, unlike the `ppo` which does in percent values 36 | 37 | Formula: 38 | - apo = ema(data, pfast) - ema(data, pslow) 39 | 40 | See: 41 | - http://www.metastock.com/Customer/Resources/TAAZ/?c=3&p=94 42 | - https://www.tradingtechnologies.com/xtrader-help/x-study/technical-indicator-definitions/absolute-price-oscillator-apo/ 43 | ''' 44 | group = 'momentum' 45 | alias = 'APO', 'AbsolutePriceOscillator', 'PriceOscillator' 46 | outputs = 'apo' 47 | 48 | def __init__(self): 49 | self.o.apo = self.ma1 - self.ma2 50 | 51 | 52 | class ppo(_posc): 53 | ''' 54 | Shows the difference between a fast and slow exponential moving 55 | averages expressed in percentage. The MACD does the same but expressed in 56 | absolute points. 57 | 58 | Expressing the difference in percentage allows to compare the indicator at 59 | different points in time when the underlying value has significatnly 60 | different values. 61 | 62 | Formula: 63 | - ppo = 100.0 * (ema(data, pfast)/ema(data, pslow) - 1.0) 64 | 65 | See: 66 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:price_oscillators_ppo 67 | ''' 68 | group = 'momentum' 69 | alias = 'PPO', 'PercentagePriceOscillator' 70 | outputs = 'ppo', 'signal', 'histogram' 71 | params = ( 72 | ('psignal', 9, 'Signal smoothing period'), 73 | ('_masig', None, 'Signal moving average (if `None`, same as others)'), 74 | ) 75 | 76 | def __init__(self): 77 | self.o.ppo = 100.0 * (self.ma1 / self.ma2 - 1.0) # base class stored 78 | masignal = self.p._masig or self.p._ma # determine movav for signal 79 | self.o.signal = masignal(self.o.ppo, period=self.p.psignal) 80 | self.o.histogram = self.o.ppo - self.o.signal 81 | 82 | 83 | class ppofast(_posc): 84 | ''' 85 | Shows the difference between a fast and slow exponential moving 86 | averages expressed in percentage. The MACD does the same but expressed in 87 | absolute points. 88 | 89 | Expressing the difference in percentage allows to compare the indicator at 90 | different points in time when the underlying value has significatnly 91 | different values. 92 | 93 | Most on-line literature shows the percentage calculation having the long 94 | exponential moving average as the denominator. Some sources like MetaStock 95 | use the fast one. 96 | 97 | Formula: 98 | - ppo = 100.0 * (1.0 - ema(data, pslow) / ema(data, pshort)) 99 | - Alternative = ppo = 100.0 - 100.0 * (ema_slow / ema_fast) 100 | 101 | See: 102 | - http://www.metastock.com/Customer/Resources/TAAZ/?c=3&p=94 103 | ''' 104 | group = 'momentum' 105 | alias = 'PPOFast', 'PercentagePriceOscillatorFast' 106 | outputs = 'ppo', 'signal', 'histogram' 107 | params = ( 108 | ('psignal', 9, 'Signal smoothing period'), 109 | ('_masig', None, 'Signal moving average (if `None`, same as others)'), 110 | ) 111 | 112 | def __init__(self): 113 | self.o.ppo = 100.0 * (self.ma1 / self.ma2 - 1.0) # base class stored 114 | masignal = self.p._masig or self.p._ma # determine movav for signal 115 | self.o.signal = masignal(self.o.ppo, period=self.p.psignal) 116 | self.o.histogram = self.o.ppo - self.o.signal 117 | 118 | _fast = True # base class will swap fast/slow calculations 119 | -------------------------------------------------------------------------------- /btalib/indicators/price.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class avgprice(Indicator): 11 | ''' 12 | Returns the average of the 4 OHLC price components 13 | 14 | Formula: 15 | - avg = (open + high + low + close) / 3 16 | 17 | See: 18 | - (None) 19 | ''' 20 | group = 'price' 21 | alias = 'AvgPrice', 'AveragePrice', 'AVGPrice', 'AVGPRICE' 22 | inputs = 'open', 'high', 'low', 'close' 23 | outputs = 'avg' 24 | params = ( 25 | ('divisor', 4.0, 'Factor to use in the division'), 26 | ) 27 | 28 | def __init__(self): 29 | prices_sum = self.i.open + self.i.high + self.i.low + self.i.close 30 | self.o.avg = prices_sum / self.p.divisor 31 | 32 | 33 | class typprice(Indicator): 34 | ''' 35 | Delivers the typical price 36 | 37 | Formula: 38 | - tp = (high + low + close) / 3 39 | 40 | See: 41 | - https://en.wikipedia.org/wiki/Typical_price 42 | ''' 43 | group = 'price' 44 | alias = 'typicalprice', 'TypicalPrice', 'TypPrice', 'TYPPRICE' 45 | inputs = 'high', 'low', 'close' 46 | outputs = 'tp' 47 | params = ( 48 | ('divisor', 3.0, 'Factor to use in the division'), 49 | ) 50 | 51 | def __init__(self): 52 | self.o.tp = (self.i.high + self.i.low + self.i.close) / self.p.divisor 53 | 54 | 55 | class wclprice(Indicator): 56 | ''' 57 | Delivers the Weighted Close Price 58 | 59 | Formula: 60 | - wcl = (high + low + close * 2) / 4 61 | 62 | See: 63 | - https://www.metastock.com/customer/resources/taaz/?p=124 64 | - http://www.ta-guru.com/Book/TechnicalAnalysis/TechnicalIndicators/WeightedClose.php5 65 | ''' 66 | group = 'price' 67 | alias = 'WCLPrice', 'WeightedClosePrice', 'WCLPRICE', 'weightedcloseprice' 68 | inputs = 'high', 'low', 'close' 69 | outputs = 'wcl' 70 | params = ( 71 | ('weight', 2.0, 'Weight Factor for close'), 72 | ('divisor', 4.0, 'Factor to use in the division'), 73 | ) 74 | 75 | def __init__(self): 76 | wclose = self.i.close * self.p.weight 77 | self.o.wcl = (self.i.high + self.i.low + wclose) / self.p.divisor 78 | 79 | 80 | class medprice(Indicator, inputs_override=True): 81 | ''' 82 | Delivers the Median Price 83 | 84 | Formula: 85 | - med = (high + low) / 2.0 86 | 87 | See: 88 | - https://www.metastock.com/customer/resources/taaz/?p=70 89 | ''' 90 | group = 'price' 91 | alias = 'WCLPrice', 'WeightedClosePrice', 'WCLPRICE', 'weightedcloseprice' 92 | # inputs = {'high': 'close'}, 'low' # map "high" in place of default close 93 | inputs = 'high', 'low' # inputs have been overriden in class def 94 | outputs = 'med' 95 | params = ( 96 | ('divisor', 2.0, 'Factor to use in the division'), 97 | ) 98 | 99 | def __init__(self): 100 | self.o.med = (self.i.high + self.i.low) / self.p.divisor 101 | -------------------------------------------------------------------------------- /btalib/indicators/roc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class _rocbase(Indicator): 11 | params = ( 12 | ('period', 12, 'Period to consider',), 13 | ) 14 | 15 | group = 'momentum' 16 | 17 | def __init__(self): 18 | self.o[0] = self.i0 / self.i0(-self.p.period) 19 | 20 | def _talib(self, kwdict): 21 | '''Change period to 10''' 22 | kwdict.setdefault('period', 10) 23 | 24 | 25 | class roc(_rocbase): 26 | ''' 27 | The ROC calculation compares the current price with the price n periods 28 | ago, to determine momentum the as the percent change in price. This version 29 | scales the percentage to 100. 30 | 31 | - roc = 100.0 * (data / data(-period) - 1.0) 32 | 33 | See: 34 | - https://school.stockcharts.com/doku.php?id=technical_indicators:rate_of_change_roc_and_momentum 35 | 36 | ''' 37 | alias = 'rateofchange', 'ROC', 'RateOfChange' 38 | outputs = 'roc' 39 | 40 | def __init__(self): 41 | self.o.roc = 100.0 * (self.o.roc - 1.0) 42 | 43 | 44 | class rocp(_rocbase): 45 | ''' 46 | The ROC calculation compares the current price with the price n periods 47 | ago, to determine momentum the as the percent change in price. 48 | 49 | Formula: 50 | 51 | - rocp = data / data(-period) - 1.0 52 | 53 | See: 54 | - https://school.stockcharts.com/doku.php?id=technical_indicators:rate_of_change_roc_and_momentum 55 | ''' 56 | alias = 'ROCP', 'RateOfChangePercentage' 57 | outputs = 'rocp' 58 | 59 | def __init__(self): 60 | self.o.rocp = self.o.rocp - 1.0 61 | 62 | 63 | class rocr(_rocbase): 64 | ''' 65 | The ROCR calculation compares the current price with the price n periods 66 | ago as a ratio to determine momentum. 67 | 68 | Formula: 69 | 70 | - rocr = data / data(-period) 71 | 72 | See: 73 | - https://school.stockcharts.com/doku.php?id=technical_indicators:rate_of_change_roc_and_momentum 74 | ''' 75 | alias = 'ROCR', 'RateOfChangeRatio' 76 | outputs = 'rocr' 77 | 78 | 79 | class rocr100(_rocbase): 80 | ''' 81 | The ROCR calculation compares the current price with the price n periods 82 | ago as a ratio to determine momentum, scaled to 100. 83 | 84 | Formula: 85 | 86 | - rocr100 = 100.0 * data / data(-period) 87 | 88 | See: 89 | - https://school.stockcharts.com/doku.php?id=technical_indicators:rate_of_change_roc_and_momentum 90 | ''' 91 | alias = 'ROCR100', 'RateOfChangeRatio100' 92 | outputs = 'rocr100' 93 | 94 | def __init__(self): 95 | self.o.rocr100 = 100.0 * self.o.rocr100 96 | -------------------------------------------------------------------------------- /btalib/indicators/rsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, smma 8 | 9 | 10 | class rsi(Indicator): 11 | ''' 12 | Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in 13 | Technical Trading Systems"*. 14 | 15 | It measures momentum by calculating the ration of higher closes and 16 | lower closes after having been smoothed by an average, normalizing 17 | the result between 0 and 100 18 | 19 | Formula: 20 | - up = upday(data) # max(close - close(-1), 0.0) 21 | - down = downday(data) # abs( min(close - close(-1), 0.0) ) 22 | - maup = movingaverage(up, period) 23 | - madown = movingaverage(down, period) 24 | - rs = maup / madown 25 | - rsi = 100 - 100 / (1 + rs) 26 | 27 | The moving average used is the one originally defined by Wilder, 28 | the SmoothedMovingAverage 29 | 30 | See: 31 | - http://en.wikipedia.org/wiki/Relative_strength_index 32 | ''' 33 | group = 'momentum' 34 | alias = 'RSI', 'RelativeStrengthIndex' 35 | outputs = 'rsi' 36 | params = ( 37 | ('period', 14, 'Period to consider'), 38 | ('lookback', 1, 'Lookback for up/down days'), 39 | ('_ma', smma, 'Smoothing moving average'), 40 | ) 41 | 42 | def __init__(self): 43 | upday = self.i0.diff(periods=self.p.lookback).clip(lower=0.0) 44 | downday = self.i0.diff(periods=self.p.lookback).clip(upper=0.0).abs() 45 | maup = self.p._ma(upday, period=self.p.period) 46 | madown = self.p._ma(downday, period=self.p.period) 47 | rs = maup / madown 48 | self.o.rsi = 100.0 - 100.0 / (1.0 + rs) 49 | -------------------------------------------------------------------------------- /btalib/indicators/sar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import numpy as np 10 | 11 | 12 | class sar(Indicator): 13 | ''' 14 | Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in 15 | Technical Trading Systems"* for the RSI 16 | 17 | SAR stands for *Stop and Reverse* and the indicator was meant as a signal 18 | for entry (and reverse) 19 | 20 | How to select the 1st signal is left unspecified in the book. Because the 21 | inputs are "high" and "low", a 1-bar MinusDM is calculated, which accounts 22 | for both downmove and upmove of high/low. This is also done by ta-lib 23 | 24 | See: 25 | - https://en.wikipedia.org/wiki/Parabolic_SAR 26 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:parabolic_sar 27 | ''' 28 | group = 'momentum' 29 | alias = 'SAR', 'psar', 'ParabolicStopAndReverse' 30 | inputs = 'high', 'low' 31 | outputs = 'sar' 32 | params = ( 33 | ('af', 0.02, 'Acceleration Factor'), 34 | ('afmax', 0.20, 'Maximum Acceleration Factor'), 35 | ) 36 | 37 | def __init__(self): 38 | sarbuf = self.i.high(val=np.nan) # result buffer 39 | sar = self.i.high._apply(self._sarize, self.i.low, sarbuf, raw=True) 40 | self.o.sar = sar._period(1) # the 1st bar is ignored in _sarize 41 | 42 | def _sarize(self, high, low, sarbuf): 43 | # kick start values 44 | hi1, lo1 = high[0], low[0] 45 | hi, lo = high[1], low[1] 46 | 47 | # Calculate a minusdm of the 1st two values to set the trend 48 | upmove, downmove = hi - hi1, lo1 - lo 49 | minusdm = max(downmove, 0.0) * (downmove > upmove) 50 | trend = not (minusdm > 0) # initial trend, long if not downmove 51 | 52 | # use the trend to set the first ep, sar values 53 | ep, sar = (hi, lo1) if trend else (lo, hi1) 54 | 55 | af, AF, AFMAX = self.p.af, self.p.af, self.p.afmax # acceleration 56 | 57 | for i in range(1, len(high)): # loop over 58 | hi1, lo1 = hi, lo 59 | hi, lo = high[i], low[i] 60 | 61 | if trend: 62 | if lo <= sar: # trend reversal 63 | trend = 0 64 | sarbuf[i] = sar = ep # update sar and annotate 65 | ep, af = lo, AF # kickstart ep and af 66 | sar = max(sar + af * (ep - sar), hi, hi1) # new sar 67 | else: 68 | sarbuf[i] = sar # no change, annotate current sar 69 | if hi > ep: # if extreme breached 70 | ep, af = hi, min(af + AF, AFMAX) # annotate, update af 71 | sar = min(sar + af * (ep - sar), lo, lo1) # recalc sar 72 | else: # trend is 0 73 | if hi >= sar: # trend reversal 74 | trend = 1 75 | sarbuf[i] = sar = ep # update sar and annotate 76 | ep, af = hi, AF # kickstart ep and af 77 | sar = min(sar + af * (ep - sar), lo, lo1) 78 | else: 79 | sarbuf[i] = sar 80 | if lo < ep: # if extreme breached 81 | ep, af = lo, min(af + AF, AFMAX) # annotate, update af 82 | sar = max(sar + af * (ep - sar), hi, hi1) 83 | 84 | return sarbuf 85 | -------------------------------------------------------------------------------- /btalib/indicators/sarext.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import numpy as np 10 | 11 | 12 | class sarext(Indicator): 13 | ''' 14 | Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in 15 | Technical Trading Systems"* for the RSI 16 | 17 | SAR stands for *Stop and Reverse* and the indicator was meant as a signal 18 | for entry (and reverse) 19 | 20 | How to select the 1st signal is left unspecified in the book. Because the 21 | inputs are "high" and "low", a 1-bar MinusDM is calculated, which accounts 22 | for both downmove and upmove of high/low. This is also done by ta-lib 23 | 24 | Compared to the standard `sar`, one can define the initial trend, the value 25 | of the initial `sar`, different acceleration factors for long and short, 26 | and an offset to be applied to the sar when the trend is reversed 27 | 28 | See: 29 | - https://en.wikipedia.org/wiki/Parabolic_SAR 30 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:parabolic_sar 31 | ''' 32 | group = 'momentum' 33 | alias = 'SAR', 'psar', 'ParabolicStopAndReverse' 34 | inputs = 'high', 'low' 35 | outputs = 'sar' 36 | params = ( 37 | ('startval', 0, 'Start trend (0 auto, 1 long, -1 short)'), 38 | ('aflong', 0.02, 'Acceleration Factor (Long)'), 39 | ('afmaxlong', 0.20, 'Maximum Acceleration Factor (Long)'), 40 | ('afshort', 0.02, 'Acceleration Factor (Long)'), 41 | ('afmaxshort', 0.20, 'Maximum Acceleration Factor (Long)'), 42 | ('offsetonreverse', 0.0, 'Offset to apply when reversing position'), 43 | ) 44 | 45 | def __init__(self): 46 | sarbuf = self.i.high(val=np.nan) # result buffer 47 | sar = self.i.high._apply(self._sarize, self.i.low, sarbuf, raw=True) 48 | self.o.sar = sar._period(1) # the 1st bar is ignored in _sarize 49 | 50 | def _sarize(self, high, low, sarbuf): 51 | # kick start values 52 | hi1, lo1 = high[0], low[0] 53 | hi, lo = high[1], low[1] 54 | 55 | # Calculate a minusdm of the 1st two values to set the trend 56 | if not self.p.startval: 57 | upmove, downmove = hi - hi1, lo1 - lo 58 | minusdm = max(downmove, 0.0) * (downmove > upmove) 59 | trend = not (minusdm > 0) # initial trend, long if not downmove 60 | 61 | # use the trend to set the first ep, sar values 62 | ep, sar = (hi, lo1) if trend else (lo, hi1) 63 | else: 64 | if self.p.startval > 0: 65 | trend = 1 66 | ep, sar = hi, self.p.startval 67 | else: 68 | trend = 0 69 | ep, sar = lo, abs(self.p.startval) 70 | 71 | AFLONG, AFMAXLONG = self.p.aflong, self.p.afmaxlong 72 | AFSHORT, AFMAXSHORT = self.p.afshort, self.p.afmaxshort 73 | OFFSETONREVERSE = self.p.offsetonreverse 74 | 75 | aflong, afshort = AFLONG, AFSHORT 76 | 77 | for i in range(1, len(high)): # loop over 78 | hi1, lo1 = hi, lo 79 | hi, lo = high[i], low[i] 80 | 81 | if trend: 82 | if lo <= sar: # trend reversal 83 | trend = 0 84 | sar = ep + sar * OFFSETONREVERSE 85 | sarbuf[i] = -sar 86 | ep, afshort = lo, AFSHORT # kickstart ep and af 87 | sar = max(sar + afshort * (ep - sar), hi, hi1) # new sar 88 | else: 89 | sarbuf[i] = sar # no change, annotate current sar 90 | if hi > ep: # if extreme breached 91 | ep, aflong = hi, min(aflong + AFLONG, AFMAXLONG) 92 | sar = min(sar + aflong * (ep - sar), lo, lo1) # recalc sar 93 | else: # trend is 0 94 | if hi >= sar: # trend reversal 95 | trend = 1 96 | sarbuf[i] = sar = ep - sar * OFFSETONREVERSE 97 | 98 | ep, aflong = hi, AFLONG # kickstart ep and af 99 | sar = min(sar + aflong * (ep - sar), lo, lo1) 100 | else: 101 | sarbuf[i] = -sar 102 | if lo < ep: # if extreme breached 103 | ep, afshort = lo, min(afshort + AFSHORT, AFMAXSHORT) 104 | sar = max(sar + afshort * (ep - sar), hi, hi1) 105 | 106 | return sarbuf 107 | -------------------------------------------------------------------------------- /btalib/indicators/sma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class sma(Indicator): 11 | ''' 12 | Non-weighted average of the last n periods 13 | 14 | Formula: 15 | - movav = Sum(data, period) / period 16 | 17 | See also: 18 | - http://en.wikipedia.org/wiki/Moving_average#Simple_moving_average 19 | ''' 20 | group = 'overlap' 21 | alias = 'SMA', 'SimpleMovingAverage' 22 | outputs = 'sma' 23 | params = ( 24 | ('period', 30, 'Period for the moving average calculation'), 25 | ) 26 | 27 | def __init__(self): 28 | self.o.sma = self.i0.rolling(window=self.p.period).mean() 29 | -------------------------------------------------------------------------------- /btalib/indicators/smma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, SEED_AVG 8 | 9 | 10 | class smma(Indicator): 11 | ''' 12 | Smoothed Moving Average used by Wilder in his 1978 book `New Concepts in 13 | Technical Trading` 14 | 15 | Defined in his book originally as: 16 | 17 | - new_value = (old_value * (period - 1) + new_data) / period 18 | 19 | Which is a moving average that smoothes data exponentially over time. 20 | 21 | - Exponential Smotthing factor: alpha = 1 / period 22 | 23 | Formula 24 | - prev = mean(data, period) 25 | - movav = prev * (1.0 - alpha) + newdata * alpha 26 | - (or alternatively # movav = prev + alpha(new - prev)) 27 | 28 | See also: 29 | - http://en.wikipedia.org/wiki/Moving_average#Modified_moving_average 30 | ''' 31 | group = 'overlap' 32 | alias = 'SMMA', 'SmoothedMovingAverage', 'MMA', 'ModifiedMovingAverage' 33 | outputs = 'smma' 34 | params = ( 35 | ('period', 30, 'Period for the moving average calculation'), 36 | ('_seed', SEED_AVG, 'Default to use average of periods as seed'), 37 | ) 38 | 39 | def __init__(self): # use data prepared by base class 40 | period, seed = self.p.period, self.p._seed 41 | self.o.smma = self.i0._ewm(com=period - 1, _seed=seed).mean() 42 | -------------------------------------------------------------------------------- /btalib/indicators/stddev.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class stddev(Indicator): 11 | ''' 12 | Calculates the standard deviation of the passed data for a given period, 13 | considering the data to be the entire population 14 | 15 | See: 16 | - http://en.wikipedia.org/wiki/Standard_deviation 17 | ''' 18 | group = 'statistic' 19 | alias = 'StdDev', 'STDDEV', 'StandardDeviation', 'stddev_p', 'STDDEV_P' 20 | outputs = 'std' 21 | params = ( 22 | ('period', 20, 'Period to consider'), 23 | ('ddof', 0, 'Degree of Freedom: 0 = population, > 0 sample'), 24 | ) 25 | 26 | def __init__(self): 27 | self.o.std = ( 28 | self.i0.rolling(window=self.p.period).std(ddof=self.p.ddof)) 29 | 30 | def _talib(self, kwdict): 31 | '''Change period to 5''' 32 | kwdict.setdefault('period', 5) 33 | 34 | 35 | class stddev_s(stddev): 36 | ''' 37 | Calculates the standard deviation of the passed data for a given period, 38 | considering the data to be a sample 39 | 40 | See: 41 | - http://en.wikipedia.org/wiki/Standard_deviation 42 | ''' 43 | ''' 44 | Calculates the standard deviation of the passed data for a given period, 45 | considering the data to be the entire population and not a sample 46 | 47 | See: 48 | - http://en.wikipedia.org/wiki/Standard_deviation 49 | ''' 50 | group = 'statistic' 51 | alias = 'stddev_sample', 'STDDEV_S', 'StandardDeviationSample' 52 | params = ( 53 | ('ddof', 1, 'Degree of Freedom: 0 = population, > 0 sample'), 54 | ) 55 | -------------------------------------------------------------------------------- /btalib/indicators/stochastic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, sma, highest, lowest 8 | 9 | 10 | class stochf(Indicator): 11 | ''' 12 | By Dr. George Lane in the 50s. It compares a closing price to the price 13 | range and tries to show convergence if the closing prices are close to the 14 | extremes 15 | 16 | - It will go up if closing prices are close to the highs 17 | - It will go down if closing prices are close to the lows 18 | 19 | It shows divergence if the extremes keep on growing/decreasing but closing 20 | prices do not in the same manner (distance to the extremes increases) 21 | 22 | Formula: 23 | - hh = highest(high, period) 24 | - ll = lowest(low, period) 25 | - kfast = 100 * (close - ll) / (hh - ll) 26 | - dfast = MovingAverage(kfast, pfast) 27 | 28 | See: 29 | - http://en.wikipedia.org/wiki/Stochastic_oscillator 30 | ''' 31 | group = 'momentum' 32 | alias = 'stochfast', 'StochasticFast', 'STOCHF' 33 | inputs = 'high', 'low', 'close' 34 | outputs = 'k', 'd' 35 | params = ( 36 | ('period', 14, 'Period to consider'), 37 | ('pfast', 3, 'Fast smoothing period'), 38 | ('_ma', sma, 'Moving average to use'), 39 | ) 40 | 41 | def __init__(self): 42 | hh = highest(self.i.high, period=self.p.period) 43 | ll = lowest(self.i.low, period=self.p.period) 44 | self.o.k = 100.0 * (self.i.close - ll) / (hh - ll) 45 | self.o.d = self.p._ma(self.o.k, period=self.p.pfast) 46 | 47 | def _talib(self, kwdict): 48 | '''Change period to 5''' 49 | kwdict.setdefault('period', 5) 50 | 51 | 52 | class stochastic(stochf): 53 | ''' 54 | By Dr. George Lane in the 50s. It compares a closing price to the price 55 | range and tries to show convergence if the closing prices are close to the 56 | extremes 57 | 58 | - It will go up if closing prices are close to the highs 59 | - It will go down if closing prices are close to the lows 60 | 61 | It shows divergence if the extremes keep on growing/decreasing but closing 62 | prices do not in the same manner (distance to the extremes increases) 63 | 64 | Formula: 65 | - hh = highest(high, period) 66 | - ll = lowest(low, period) 67 | - kfast = 100 * (close - ll) / (hh - ll) 68 | - k = MovingAverage(kfast, pfast) 69 | - d = MovingAverage(k, pslow) 70 | 71 | See: 72 | - http://en.wikipedia.org/wiki/Stochastic_oscillator 73 | ''' 74 | alias = 'stoch', 'Stochastic', 'STOCHASTIC', 'STOCH' 75 | params = ( 76 | ('pslow', 3, 'Slow moving average period'), 77 | ('_maslow', None, 'Slow moving average (if `None`, use same as fast)'), 78 | ) 79 | 80 | def __init__(self): 81 | # The non-fast version uses slow d as k - smooths/slows d with maslow 82 | self.o.k = k = self.o.d # promote slow to fast and slow down d further 83 | self.o.d = (self.p._maslow or self.p._ma)(k, period=self.p.pslow) 84 | -------------------------------------------------------------------------------- /btalib/indicators/stochrsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, rsi, sma, highest, lowest 8 | 9 | 10 | class stochrsi(Indicator): 11 | ''' 12 | Presented by Chande and Kroll the 1990 book: "The New Technical Trader". 13 | The RSI is fed into a atochastic-like calculation to increase its 14 | sensitivity. 15 | 16 | The recommendation is to keep the period for looking for highest highes and 17 | lowest lows the same as the for the RSI, but it can be played with for 18 | experimentation. 19 | 20 | Scaling to 100 is also suggested as a possiblity (the range is 0.0 => 1.0) 21 | 22 | Formula: 23 | 24 | - rsi = RSI(data, period) 25 | - maxrsi = Highest(rsi, period) 26 | - minrsi = Lowest(rsi, period) 27 | 28 | - stochrsi = (rsi - minrsi) / (maxrsi - minrsi) 29 | 30 | See 31 | - https://school.stockcharts.com/doku.php?id=technical_indicators:stochrsi 32 | ''' 33 | group = 'momentum' 34 | alias = 'StochRsi', 'STOCHRSI' 35 | outputs = 'stochrsi' 36 | params = ( 37 | ('period', 14, 'Period to consider'), 38 | ('_philo', None, 'Period for highest/lowest (None => period)'), 39 | ('_scale', 1.0, 'Scale the result by this factor'), 40 | ) 41 | 42 | def __init__(self, _pfast=None, _ma=None): 43 | philo = self.p._philo or self.p.period # set highest/lowest period 44 | 45 | r = rsi(self.i0, period=self.p.period) # rsi 46 | maxrsi = highest(r, period=philo) # max in period 47 | minrsi = lowest(r, period=philo) # min in period 48 | self.o.stochrsi = (r - minrsi) / (maxrsi - minrsi) * self.p._scale 49 | 50 | if _pfast: # set by _talib. A 2nd output d will have been defined 51 | self.o.d = _ma(self.o.stochrsi, period=_pfast) 52 | 53 | def _talib(self, kwdict): 54 | ''' 55 | ta-lib uses internally the fast stochastic to calculate the stochrsi, 56 | with these side-effects 57 | 58 | - The scale changes from 0.0-1.0 to 0.0-100.0 59 | 60 | - A 2nd output is returned (stochrsi as defined by its authors has 61 | only 1 output) 62 | 63 | - The highest/lowest period is no longer symmetric with the rsi 64 | period 65 | 66 | Compatibility does this 67 | - Change the scale to 0.0-100.0 68 | - Change the highest/lowest period 5 69 | - Add a 2nd output named 'd' (as the 2nd output of the stochastic) 70 | - Run a simple moving average on it of period 3 71 | ''' 72 | # Change regular play-with parameters to match ta-lib expectations 73 | kwdict.setdefault('_philo', 5) 74 | kwdict.setdefault('_scale', 100.0) 75 | 76 | # 2nd output - ta-lib extra parameters defined as kwargs to __init__ 77 | self.o.__slots__.append('d') # add real-time second output 78 | kwdict.setdefault('_pfast', 3) # defined in kwargs 79 | kwdict.setdefault('_ma', sma) # defined in kwargs 80 | -------------------------------------------------------------------------------- /btalib/indicators/t3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, ema 8 | 9 | 10 | class gdema(Indicator): 11 | ''' 12 | Described first by Tim Tillson in Stocks & Commodities V16:1, January 1998 13 | for its use in the T3 indicator. 14 | 15 | It generalizes the DEMA by using a "volume" factor to put weight on 16 | the 1st or 2nd factor of the DEMA to smooth the output. 17 | 18 | When "vfactor" is 1, the DEMA is the output. When "vfactor" is 0, the 19 | output is s simple EMA. 20 | ''' 21 | group = 'overlap' 22 | alias = 'gd', 'GD', 'GDEMA', 'GeneralizedDEMA' 23 | outputs = 'gd' 24 | params = ( 25 | ('period', 5, 'Period to consider'), 26 | ('vfactor', 0.7, 'volume factor'), 27 | ('_ma', ema, 'Moving Average to use'), 28 | ) 29 | 30 | def __init__(self, _gd=False): # _gd may be set by t3. Ingore it. 31 | ema1 = self.p._ma(self.i0, period=self.p.period) 32 | ema2 = self.p._ma(ema1, period=self.p.period) 33 | 34 | self.o.gd = (1.0 + self.p.vfactor)*ema1 - self.p.vfactor*ema2 35 | 36 | 37 | class t3(Indicator): 38 | ''' 39 | Described first by Tim Tillson in Stocks & Commodities V16:1, January 1998. 40 | 41 | It first generalizes the DEMA by using a "volume" factor to put weight on 42 | the 1st or 2nd factor of the DEMA to smooth the output. 43 | 44 | And it then passes the input three times (hence T3) through the generalized 45 | dema to reduce phase lag. 46 | 47 | The default behavir implements the quadractic equation MetaStock version 48 | presented in the article, which is also ta-lib compatible. 49 | 50 | Using the paramter `_gd` one can enable the GeneralizedDema Triple Filter 51 | formulation shown in the article (which is expanded to the quadratic 52 | version) by the auther. The initial results are similar but not the 53 | same. The results converge aftera number of bars (period dependent) to at 54 | lest 6 decimals of precision (period 5 => after 71 bars) 55 | 56 | See also: 57 | 58 | - https://www.fmlabs.com/reference/default.htm?url=T3.htm 59 | - http://www.tradingpedia.com/forex-trading-indicators/t3-moving-average-indicator/ 60 | - https://www.tradingtechnologies.com/xtrader-help/x-study/technical-indicator-definitions/t3-t3/ 61 | ''' 62 | group = 'overlap' 63 | alias = 'T3', 'TilsonsT3' 64 | outputs = 't3' 65 | params = ( 66 | ('period', 5, 'Period to consider'), 67 | ('vfactor', 0.7, 'Volume factor'), 68 | ('_ma', ema, 'Moving Average to use'), 69 | ('_gd', False, '`True` use articl triple gd filter math formulation'), 70 | ) 71 | 72 | def __init__(self): 73 | if self.p._gd: # use gd triple filter math formulation 74 | self.o.t3 = gd(gd(gd(self.i0, **self.p), **self.p), **self.p) # noqa: F821, E501 75 | return 76 | 77 | # Else use the expanded quadratic code shown in the article 78 | a, a2, a3 = self.p.vfactor, self.p.vfactor**2, self.p.vfactor**3 79 | 80 | c1 = -a3 81 | c2, c3, c4 = (3*a2 + 3*a3), (-6*a2 - 3*a - 3*a3), (1 + 3*a + a3 + 3*a2) 82 | 83 | ema1 = ema(self.i0, period=self.p.period) 84 | ema2 = ema(ema1, period=self.p.period) 85 | ema3 = ema(ema2, period=self.p.period) 86 | ema4 = ema(ema3, period=self.p.period) 87 | ema5 = ema(ema4, period=self.p.period) 88 | ema6 = ema(ema5, period=self.p.period) 89 | 90 | self.o.t3 = c1*ema6 + c2*ema5 + c3*ema4 + c4*ema3 91 | 92 | # ALTERANTIVE FORMULATIONS OF THE GDEMA Alternative 93 | if False: 94 | # gd has exactly the same parameters signature as t3_gd 95 | # self.o.t3 = gd(gd(gd(self.i0, **self.p), **self.p), **self.p) 96 | 97 | # Alternatives with the definition of a partial "_gd" 98 | # _gd = lambda x: gd(x, **self.params) # noqa: E731 99 | 100 | # p, v, _ma = self.p.period, self.p.vfactor, self.p._ma 101 | # _gd = lambda x: gd(x, period=p, vfactor=v, _ma=_ma) # noqa: E731 102 | 103 | # def _gd(x, p=self.p.period, v=self.p.vfactor, _ma=self.p._ma): 104 | # return gd(x, period=p, vfactor=v, _ma=_ma) # noqa: E116 105 | 106 | # def _gd(x): return gd(x, **self.params) 107 | # self.o.t3 = _gd(_gd(_gd(self.i0))) 108 | pass 109 | -------------------------------------------------------------------------------- /btalib/indicators/tema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, ema 8 | 9 | 10 | class tema(Indicator): 11 | ''' 12 | TEMA was first time introduced in 1994, in the article "Smoothing Data with 13 | Faster Moving Averages" by Patrick G. Mulloy in "Technical Analysis of 14 | Stocks & Commodities" magazine. 15 | 16 | It attempts to reduce the inherent lag associated to Moving Averages 17 | 18 | Formula: 19 | - ema1 = ema(data, period) 20 | - ema2 = ema(ema1, period) 21 | - ema3 = ema(ema2, period) 22 | - tema = 3 * ema1 - 3 * ema2 + ema3 23 | 24 | See: 25 | - https://en.wikipedia.org/wiki/Triple_exponential_moving_average 26 | ''' 27 | group = 'overlap' 28 | alias = 'TEMA', 'TripleExponentialMovingAverage' 29 | outputs = 'tema' 30 | params = ( 31 | ('period', 30, 'Period to consider'), 32 | ('_ma', ema, 'Moving Average to use'), 33 | ) 34 | 35 | def __init__(self): 36 | ema1 = self.p._ma(self.i0, period=self.p.period) 37 | ema2 = self.p._ma(ema1, period=self.p.period) 38 | ema3 = self.p._ma(ema2, period=self.p.period) 39 | self.o.tema = 3.0 * ema1 - 3.0 * ema2 + ema3 40 | -------------------------------------------------------------------------------- /btalib/indicators/trima.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, sma 8 | 9 | 10 | class trima(Indicator): 11 | ''' 12 | The Triangular Moving Average (TRIMA) is an average of an average, similar 13 | to the `sma` but placing more weight on the middle values due to the second 14 | smoothing. 15 | 16 | Formula: 17 | - if period is odd: p1 = p2 = (period + 1) // 2 18 | - if period is even: p1, p2 = (p // 2) + 1, p //2 19 | - trima = sma(sma(data, p2), p1) 20 | 21 | See also: 22 | - https://www.tradingtechnologies.com/xtrader-help/x-study/technical-indicator-definitions/triangular-moving-average-trima/ 23 | ''' 24 | group = 'overlap' 25 | alias = 'TRIMA', 'TriangularMovingAverage' 26 | outputs = 'trima' 27 | params = ( 28 | ('period', 30, 'Period for the moving average calculation'), 29 | ('_ma', sma, 'Moving average to use'), 30 | ) 31 | 32 | def __init__(self): 33 | p = self.p.period 34 | if p % 2: # odd 35 | p1 = p2 = (p + 1) // 2 36 | else: 37 | p1, p2 = (p // 2) + 1, p // 2 38 | 39 | self.o.trima = sma(sma(self.i0, period=p2), period=p1) 40 | -------------------------------------------------------------------------------- /btalib/indicators/trix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, ema 8 | 9 | 10 | class trix(Indicator): 11 | ''' 12 | Defined by Jack Hutson in the 80s and shows the Rate of Change (%) or slope 13 | of a triple exponentially smoothed moving average 14 | 15 | Formula: 16 | - ema1 = EMA(data, period) 17 | - ema2 = EMA(ema1, period) 18 | - ema3 = EMA(ema2, period) 19 | - trix = 100 * (ema3 / ema3(1) - 1) 20 | Where -1 is the lookback period to consider the rate of change 21 | 22 | The final formula can be simplified to: 100 * (ema3 / ema3(-1) - 1) 23 | 24 | The moving average used is the one originally defined by Wilder, 25 | the SmoothedMovingAverage 26 | 27 | See: 28 | - https://en.wikipedia.org/wiki/Trix_(technical_analysis) 29 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:trix 30 | ''' 31 | group = 'momentum' 32 | alias = 'Trix', 'TRIX' 33 | outputs = 'trix' 34 | params = ( 35 | ('period', 30, 'Period to consider'), 36 | ('_ma', ema, 'Moving average to use'), 37 | ) 38 | 39 | def __init__(self): 40 | ema1 = self.p._ma(self.i0, period=self.p.period) 41 | ema2 = self.p._ma(ema1, period=self.p.period) 42 | ema3 = self.p._ma(ema2, period=self.p.period) 43 | self.o.trix = 100.0 * (ema3 / ema3(-1) - 1.0) 44 | -------------------------------------------------------------------------------- /btalib/indicators/ultimateoscillator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, SumN, truelow, truerange 8 | 9 | 10 | class ultimateoscillator(Indicator): 11 | ''' 12 | Developed by Larry Williams. It is based on notion of buying or selling 13 | pressure by considering where where a the closing price is within the true 14 | range. 15 | 16 | Formula (default values): 17 | - bp = close - truelow(low, close) # buying pressure 18 | - tr = truerange(high, low, close) 19 | - av7 = Sum(bp, 7) / Sum(tr, 7) 20 | - av14 = Sum(bp, 14) / Sum(tr, 14) 21 | - av28= Sum(bp, 28) / Sum(tr, 28) 22 | - uo = 100 * (4*av7 + 2*av14 + av18) / (4 + 2 + 1) 23 | 24 | See: 25 | - https://en.wikipedia.org/wiki/Ultimate_oscillator 26 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:ultimate_oscillator 27 | ''' 28 | group = 'momentum' 29 | alias = 'ultosc', 'UltimateOscillator', 'ULTOSC' 30 | inputs = 'high', 'low', 'close' 31 | outputs = 'uo' 32 | params = ( 33 | ('period1', 7, 'Faster oscillating period'), 34 | ('period2', 14, 'Medium oscillating period'), 35 | ('period3', 28, 'Slower oscillating period'), 36 | ('factor1', 4.0, 'Factor weight for the faster oscillation'), 37 | ('factor2', 2.0, 'Factor weight for the medium oscillation'), 38 | ('factor3', 1.0, 'Factor weight for the slower oscillation'), 39 | ) 40 | 41 | def __init__(self): 42 | bp = self.i.close - truelow(self.i.low, self.i.close) 43 | tr = truerange(self.i.high, self.i.low, self.i.close) 44 | 45 | av1 = SumN(bp, period=self.p.period1) / SumN(tr, period=self.p.period1) 46 | av2 = SumN(bp, period=self.p.period2) / SumN(tr, period=self.p.period2) 47 | av3 = SumN(bp, period=self.p.period3) / SumN(tr, period=self.p.period3) 48 | 49 | factor = 100.0 / (self.p.factor1 + self.p.factor2 + self.p.factor3) 50 | uo1 = (self.p.factor1 * factor) * av1 51 | uo2 = (self.p.factor2 * factor) * av2 52 | uo3 = (self.p.factor3 * factor) * av3 53 | 54 | self.o.uo = uo1 + uo2 + uo3 55 | -------------------------------------------------------------------------------- /btalib/indicators/var.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | 10 | class var(Indicator): 11 | ''' 12 | Variance is the expectation of the squared deviation of a random variable 13 | from its mean 14 | 15 | See: 16 | - https://en.wikipedia.org/wiki/Variance 17 | ''' 18 | group = 'statistic' 19 | alias = 'VAR', 'Var' 20 | outputs = 'var' 21 | params = ( 22 | ('period', 5, 'Period to consider'), 23 | ('ddof', 0, 'Degree of Freedom: 0 = population, > 0 sample'), 24 | ) 25 | 26 | def __init__(self): 27 | self.o.var = ( 28 | self.i0.rolling(window=self.p.period).var(ddof=self.p.ddof)) 29 | 30 | 31 | class var_s(var): 32 | ''' 33 | Variance is the expectation of the squared deviation of a random variable 34 | from its mean 35 | 36 | This version considers the population is a sample (ddof=1) 37 | 38 | See: 39 | - https://en.wikipedia.org/wiki/Variance 40 | ''' 41 | group = 'statistic' 42 | alias = 'varsample', 'VARS', 'VarSample' 43 | params = ( 44 | ('ddof', 1, 'Degree of Freedom: 0 = population, > 0 sample'), 45 | ) 46 | -------------------------------------------------------------------------------- /btalib/indicators/williamsr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator, highest, lowest 8 | 9 | 10 | class williamsr(Indicator): 11 | ''' 12 | Developed by Larry Williams to show the relation of closing prices to 13 | the highest-lowest range of a given period. 14 | 15 | Known as Williams %R (but % is not allowed in Python identifiers) 16 | 17 | Formula: 18 | - num = highest(high, period) - close 19 | - den = highest(high, period) - lowest(low, period) 20 | - %R = -100.0 * (num / den) 21 | 22 | See: 23 | - http://en.wikipedia.org/wiki/Williams_%25R 24 | - https://school.stockcharts.com/doku.php?id=technical_indicators:williams_r 25 | ''' 26 | group = 'momentum' 27 | alias = 'willr', 'WilliamsR', 'WILLIAMSR', 'WILLR' 28 | inputs = 'high', 'low', 'close' 29 | outputs = 'r' 30 | params = ( 31 | ('period', 14, 'Period to consider'), 32 | ) 33 | 34 | def __init__(self): 35 | hh = highest(self.i.high, period=self.p.period) 36 | ll = lowest(self.i.low, period=self.p.period) 37 | self.o.r = -100.0 * (hh - self.i.close) / (hh - ll) 38 | -------------------------------------------------------------------------------- /btalib/indicators/wma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import Indicator 8 | 9 | import numpy as np 10 | 11 | 12 | class wma(Indicator): 13 | ''' 14 | A Moving Average which gives an arithmetic weighting to values with the 15 | newest having the more weight 16 | 17 | Formula: 18 | - weights = range(1, period + 1) 19 | - coef = 2 / (period * (period + 1)) 20 | - movav = coef * Sum(weight[i] * data[period - i] for i in range(period)) 21 | 22 | See also: 23 | - http://en.wikipedia.org/wiki/Moving_average#Weighted_moving_average 24 | ''' 25 | group = 'overlap' 26 | alias = 'WMA', 'WeightedMovingAverage' 27 | outputs = 'wma' 28 | params = ( 29 | ('period', 30, 'Period for the moving average calculation'), 30 | ('coef', 1.0), 31 | ) 32 | 33 | def __init__(self): 34 | # pre-calculate coe 35 | # weights = np.array([x for x in range(1, self.p.period + 1)]) # weights 36 | weights = np.array(list(range(1, self.p.period + 1))) # weights 37 | _wma = lambda x: np.dot(x, weights) # closure # noqa: E731 38 | 39 | coef = 2.0 / (self.p.period * (self.p.period + 1)) # calc coef & 40 | self.o.wma = coef * self.i0.rolling(window=self.p.period).apply(_wma) 41 | -------------------------------------------------------------------------------- /btalib/meta/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from .. import errors # noqa: F401 8 | from .. import config # noqa: F401 9 | from ..utils import * # noqa: F401, F403 10 | 11 | from .metadata import metadata # noqa: F401 12 | 13 | from . import linesholder # noqa: F401 14 | from . import aliases # noqa: F401 15 | from . import groups # noqa: F401 16 | from . import lines # noqa: F401 17 | from . import inputs # noqa: F401 18 | from . import outputs # noqa: F401 19 | from . import params # noqa: F401 20 | from . import docs # noqa: F401 21 | -------------------------------------------------------------------------------- /btalib/meta/aliases.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import sys 8 | 9 | __all__ = [] 10 | 11 | 12 | def _generate(cls, bases, dct, name='alias', autoexport=True, **kwargs): 13 | # Ensure exporting itself and the alias is done 14 | clsmod = sys.modules[cls.__module__] # keep handy ref to mod 15 | 16 | if autoexport: 17 | if not hasattr(clsmod, '__all__'): 18 | setattr(clsmod, '__all__', []) # add exports section if not there 19 | 20 | clsmod.__all__.append(cls.__name__) # add itelf to exports 21 | 22 | # Add aliases as needed and autoexport aliases and main class 23 | aliases = dct.get(name, ()) 24 | if isinstance(aliases, str): 25 | aliases = (aliases,) 26 | 27 | if aliases: 28 | cls.alias = aliases # re-set as tuple even if string was passed 29 | 30 | for alias in aliases: # add and export aliases 31 | setattr(clsmod, alias, cls) 32 | if autoexport: 33 | clsmod.__all__.append(alias) 34 | -------------------------------------------------------------------------------- /btalib/meta/docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import textwrap 8 | 9 | from . import linesholder 10 | from . import params as metaparams 11 | 12 | __all__ = [] 13 | 14 | 15 | def wrap_indent(txt, ilevel=0, ichar=' ', width=78, cr=1): 16 | ifill = ilevel * ichar 17 | wrapped_lines = [] 18 | for line in txt.splitlines(): 19 | wrapped = textwrap.wrap( 20 | line, 21 | initial_indent=ifill, 22 | subsequent_indent=ifill, 23 | # drop_whitespace=False, 24 | replace_whitespace=False, 25 | width=width, 26 | ) 27 | wrapped_lines.extend(wrapped) 28 | 29 | return '\n'*cr + '\n'.join(wrapped_lines) 30 | 31 | 32 | def process_docstring(d): 33 | slines = d.split('\n') 34 | slines2 = textwrap.dedent('\n'.join(slines[1:])).split('\n') 35 | return '\n'.join([slines[0]] + slines2) 36 | 37 | 38 | def _generate(cls, bases, dct, **kwargs): 39 | # Get actual docstring 40 | 41 | cls.__doc_orig__ = clsdoc = (cls.__doc__ or '') 42 | 43 | gendoc = process_docstring(clsdoc) 44 | 45 | if getattr(cls, 'alias', None): 46 | txt = '\n' 47 | txt += 'Aliases: ' + ', '.join(cls.alias) 48 | gendoc += wrap_indent(txt, cr=2) 49 | 50 | if getattr(cls, 'inputs', None): 51 | txt = '\n' 52 | txt += 'Inputs: ' + ', '.join(cls.inputs) 53 | gendoc += wrap_indent(txt, cr=2) 54 | 55 | if getattr(cls, 'outputs', None): 56 | txt = '\n' 57 | txt += 'Outputs: ' + ', '.join(cls.outputs) 58 | gendoc += wrap_indent(txt, cr=2) 59 | 60 | if getattr(cls, 'params', None): 61 | txt = '\n' 62 | txt += 'Params:' 63 | gendoc += wrap_indent(txt, cr=2) 64 | 65 | pinfo = metaparams.get_pinfo(cls) 66 | for name, val in cls.params.items(): 67 | if isinstance(val, str): 68 | val = "'{}'".format(val) 69 | else: 70 | try: 71 | if issubclass(val, linesholder.LinesHolder): 72 | val = val.__name__ 73 | except TypeError: # val is not a class 74 | pass # skip ... let format handle it 75 | 76 | txt = '' 77 | txt += '\n - {} (default: {})'.format(name, val) 78 | 79 | pdoc = pinfo.get(name, {}).get('doc', '') 80 | if pdoc: 81 | txt += '\n\n {}'.format(pdoc) 82 | 83 | gendoc += wrap_indent(txt, cr=1) 84 | 85 | _talib = getattr(cls, '_talib', None) 86 | if _talib: # must be mehot 87 | tadoc = getattr(_talib, '__doc__', '') or '' 88 | if tadoc: 89 | txt = 'TA-LIB (with compatibility flag "_talib=True"):' 90 | gendoc += wrap_indent(txt, cr=2) 91 | gendoc += '\n\n' + process_docstring(tadoc) 92 | 93 | cls.__doc__ = gendoc 94 | -------------------------------------------------------------------------------- /btalib/meta/groups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | __all__ = [] 8 | 9 | 10 | def _generate(cls, bases, dct, **kwargs): 11 | # Try to find a group definition in the directory of the class and it not 12 | # possible, get the attribute which will have been inherited from the class 13 | # Add the final attribute in tuple form, to support many 14 | grps = dct.get('group', ()) or getattr(cls, 'group', ()) 15 | if isinstance(grps, str): 16 | grps = (grps,) # if only str, simulate iterable 17 | 18 | cls.group = grps # set it in the instance, let others process 19 | -------------------------------------------------------------------------------- /btalib/meta/inputs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import pandas as pd 8 | 9 | from . import config 10 | from . import errors 11 | from . import lines 12 | from . import linesholder 13 | from . import metadata 14 | 15 | 16 | __all__ = ['Inputs', 'Input'] 17 | 18 | 19 | _CLSINPUTS = {} # holds auto-generated inputs clases 20 | _CLSINAME = {} # holds naming used for the class 21 | 22 | 23 | class Input(lines.Line): 24 | pass 25 | 26 | 27 | class Inputs(lines.Lines): 28 | pass 29 | 30 | 31 | def _generate(cls, bases, dct, name='inputs', klass=Inputs, **kwargs): 32 | inputscls = lines._generate( 33 | cls, bases, dct, name=name, klass=klass, **kwargs, 34 | ) 35 | _CLSINPUTS[cls] = inputscls 36 | _CLSINAME[cls] = name 37 | 38 | 39 | def _from_args(cls, *args): 40 | if not args: # this must break ... no inputs ... no fun 41 | errors.OneInputNeededZeroProvided() 42 | 43 | clsinputs = getattr(cls, _CLSINAME[cls]) # Get input definitions 44 | linputs, largs = len(clsinputs), len(args) # different logic with lengths 45 | 46 | allowinputs = 0 # control at the end if inputs length has to be capped 47 | 48 | if largs >= linputs: 49 | argsinputs, args = args[:linputs], args[linputs:] # split inputs/args 50 | 51 | # map actual inputs to expected input. Let constructor do conversions 52 | inputargs = {clsinput: Input(arginput, clsinput) 53 | for arginput, clsinput in zip(argsinputs, clsinputs)} 54 | 55 | else: # largs < linputs 56 | # less arguments, the 1st argument must then be multidimensional 57 | arginput, args = args[0], args[1:] # take only 1-elem, rest is args 58 | 59 | if isinstance(arginput, pd.DataFrame): # check input validity 60 | inputargs = _from_arg_dataframe(arginput, clsinputs) 61 | elif isinstance(arginput, linesholder.LinesHolder): 62 | inputargs = _from_arg_linesholder(arginput, clsinputs) 63 | else: 64 | allowinputs = linputs = getattr(cls, 'allowinputs', 0) 65 | if not linputs: 66 | errors.MultiDimType() # raise error, non-acceptable type 67 | 68 | # reconstruct args 69 | args = (arginput,) + args 70 | 71 | # repeat as above for single inputs but with less inputs 72 | # less inputs are allowed ... let's go fot it - split inputs/args 73 | argsinputs, args = args[:linputs], args[linputs:] 74 | 75 | # map inputs to expected input. Let constructor do conversions 76 | inputargs = {clsinput: Input(arginput, clsinput) 77 | for arginput, clsinput in zip(argsinputs, clsinputs)} 78 | 79 | # Create instance of inputs and adjust if needed 80 | inpret = _CLSINPUTS[cls](**inputargs) # return instance / rem. args 81 | if allowinputs: 82 | inpret.__slots__[:] = inpret.__slots__[:linputs] # cap the inputs length 83 | 84 | return inpret, args # return the instance and remaining args 85 | 86 | 87 | def _from_arg_dataframe(arginput, clsinputs): 88 | if metadata.callstack: # top-of the stack, pandas cannot be used 89 | errors.PandasNotTopStack() 90 | 91 | cols = [x.lower() for x in arginput.columns] 92 | 93 | if len(cols) < len(clsinputs): # check input validity 94 | errors.MultiDimSmall() # raise error 95 | 96 | # create colindices reversed to have 0 atthe end (use list.pop def -1) 97 | colindices = list(range(len(cols) - 1, -1, -1)) 98 | 99 | # try to find the input 100 | # 0. Use OHLC indices first if dictated by config 101 | # 1. By columnn name (case insensitive) (if OHLC_index and not helpful) 102 | # 2. By using the col index from the configuration settings 103 | # 3. Else, get the next free column 104 | inputargs = {} 105 | for i, clsinput in enumerate(clsinputs): 106 | inputidx = -1 107 | if config.OHLC_FIRST: 108 | inputidx = inputidxstr = config.OHLC_INDICES.get(clsinput, -1) 109 | 110 | if isinstance(inputidx, str): # index set specifically to colname 111 | try: 112 | inputidx = cols.index(inputidxstr) 113 | except ValueError: 114 | inputidx = -1 115 | 116 | if inputidx == -1: 117 | errors.ColdIndexStrNotFound(clsinput, inputidxstr) # raise 118 | 119 | try: 120 | if inputidx not in colindices: # not found yet, try names 121 | inputidx = cols.index(clsinput) # try first by name 122 | except ValueError: # else pre-def index ... or default to 0 123 | inputidx = inputidxstr = config.OHLC_INDICES.get(clsinput, -1) 124 | if isinstance(inputidx, str): 125 | try: 126 | inputidx = cols.index(inputidxstr) 127 | except ValueError: 128 | inputidx = -1 129 | 130 | if inputidx == -1: 131 | errors.ColdIndexStrNotFound(clsinput, inputidxstr) # raise 132 | 133 | try: 134 | colindices.remove(inputidx) # see if index is valid 135 | except ValueError: 136 | inputidx = colindices.pop() # wasn't there, get next free 137 | 138 | inputargs[clsinput] = arginput.iloc[:, inputidx] # store input 139 | 140 | return inputargs 141 | 142 | 143 | def _from_arg_linesholder(arginput, clsinputs): 144 | if arginput.size < len(clsinputs): # check input validity 145 | errors.MultiDimSmall() # raise error 146 | 147 | # Simple first come first served, no name checking for internal objects 148 | return {clsinp: out for clsinp, out in zip(clsinputs, arginput.outputs)} 149 | -------------------------------------------------------------------------------- /btalib/meta/linesholder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import pandas as pd 8 | 9 | from . import linesops 10 | 11 | __all__ = ['LinesHolder'] 12 | 13 | 14 | class SeriesFetcher: 15 | def __init__(self, outputs): 16 | self._o = outputs 17 | 18 | def __getattr__(self, attr): 19 | if not hasattr(self._o, attr): 20 | return super().__getattr__(attr) 21 | 22 | return getattr(self._o, attr).series 23 | 24 | def __contains__(self, item): 25 | return self._o.__contains__(item) 26 | 27 | def __len__(self): 28 | return len(self._o) 29 | 30 | def __iter__(self): 31 | yield from (x.series for x in self._o) 32 | 33 | def __getitem__(self, item): 34 | return self._o[item].series 35 | 36 | def keys(self): 37 | return self._o.keys() 38 | 39 | 40 | def binary_op(name): 41 | def real_binary_op(self, other, *args, **kwargs): 42 | return getattr(self.outputs[0], name)(other, *args, **kwargs) 43 | 44 | linesops.install_cls(name=name, attr=real_binary_op) 45 | 46 | 47 | class LinesHolder: 48 | # base class for any object holding lines 49 | _df = None 50 | _sf = None 51 | 52 | # Install the different proxy operations 53 | for name in linesops._BINOPS: 54 | binary_op(name) 55 | 56 | def __call__(self, *args, **kwargs): 57 | return self.outputs[0](*args, **kwargs) 58 | 59 | def __getattr__(self, attr): 60 | if hasattr(self.outputs, attr): 61 | return getattr(self.outputs, attr) 62 | 63 | try: 64 | return getattr(self.outputs[0], attr) 65 | except AttributeError: 66 | pass 67 | 68 | # retrieval was impossible, signal real culprit 69 | raise AttributeError(attr) 70 | 71 | def __contains__(self, item): 72 | return hasattr(self.outputs, item) 73 | 74 | def __len__(self): 75 | return len(self.outputs) 76 | 77 | def __iter__(self): 78 | return iter(self.outputs) 79 | 80 | def __getitem__(self, item): 81 | return self.outputs[item] 82 | 83 | def keys(self): 84 | return self.outputs.keys() 85 | 86 | @property 87 | def size(self): 88 | return self.outputs.size 89 | 90 | @property 91 | def df(self): 92 | if self._df is None: 93 | self._df = pd.DataFrame(dict(self.series)) 94 | 95 | return self._df 96 | 97 | @property 98 | def series(self): 99 | if self._sf is None: 100 | self._sf = SeriesFetcher(self.outputs) 101 | 102 | return self._sf 103 | 104 | def _period(self, p): 105 | self._minperiod += 1 106 | return self 107 | -------------------------------------------------------------------------------- /btalib/meta/linesops.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import sys 8 | 9 | 10 | __all__ = [] 11 | 12 | 13 | def install_cls(startlevel=2, name=None, attr=None, propertize=False): 14 | # Go up the frames until __module__ is found (present in class declaration) 15 | while True: 16 | try: 17 | frame = sys._getframe(startlevel) 18 | except ValueError: 19 | return # went to far up the chain of frames 20 | 21 | if '__module__' in frame.f_locals: # found class in definition 22 | break # found ... time to 23 | 24 | startlevel += 1 # one up ... 25 | 26 | if propertize: 27 | attr = property(attr) 28 | 29 | frame.f_locals[name] = attr # install in locals => add to class def 30 | 31 | 32 | _BINOPS = ( 33 | # BINary OPerationS: operate on self and other, returning a length-wise 34 | # equal series with the result of the self-other element-wise operation 35 | 36 | # comparison 37 | '__eq__', 'eq', 38 | '__le__', 'le', 39 | '__lt__', 'lt', 40 | '__ge__', 'ge', 41 | '__gt__', 'gt', 42 | '__ne__', 'ne', 43 | 44 | # arithmetic 45 | '__add__', '__radd__', 'add', 'radd', 46 | '__div__', '__rdiv__', 'div', 'divide', 'rdiv', 47 | '__floordiv__', '__rfloordiv__', 'floordiv', 'rfloordiv', 48 | '__mod__', '__rmod__', 'mod', 'rmod', 49 | '__mul__', '__rmul__', 'mul', 'multiply', 'rmul', 50 | '__pow__', '__rpow__', 'pow', 'rpow', 51 | '__sub__', '__rsub__', 'sub', 'subtract', 'rsub', 52 | '__truediv__', '__rtruediv__', 'truediv', 'rtruediv', 53 | 54 | # logic 55 | '__and__', '__or__', '__xor__', 56 | ) 57 | 58 | _STDOPS = { 59 | # STandarD OPerationS: do something with the series 60 | # the period may be changed and a copy may or ma not be returned 61 | 62 | # sargs: True => *args may contain other series (Line => Series) 63 | # skwargs: True => **kwargs may contain other series (Line => Series) 64 | 65 | # change period 66 | 'diff': dict(parg='periods'), 67 | 'shift': dict(parg='periods'), 68 | 'pct_change': dict(parg='periods'), 69 | 70 | # operate on existing values 71 | 'abs': {}, '__abs__': {}, 72 | 'append': {'sargs': True}, 73 | 'apply': {'skwargs': True}, 74 | 'astype': {}, 75 | 'between': {'sargs': True}, 76 | 'bfill': {}, 77 | 'clip': {'skwargs': True}, 78 | 'combine': {'sargs': True}, # other => potential binop 79 | 'combine_first': {'sargs': True}, # other => potential binop 80 | 'concat': {'sargs': True}, 81 | 'copy': {}, 82 | 'cummax': {}, 83 | 'cummin': {}, 84 | 'cumprod': {}, 85 | 'cumsum': {}, 86 | 'drop': {}, 87 | 'drop_duplicates': {}, 88 | 'dropna': {}, 89 | 'duplicated': {}, 90 | 'ffill': {}, 91 | 'fillna': {}, 92 | 'filter': {}, 93 | 'first': {}, 94 | 'head': {}, 95 | 'last': {}, 96 | 'interpolate': {}, 97 | 'isin': {'sargs': True}, 98 | 'isna': {}, 99 | 'isnull': {}, 100 | 'nlargest': {}, 101 | 'mask': {'sargs': True, 'skwargs': True}, 102 | 'notna': {}, 103 | 'notnull': {}, 104 | 'nsmallest': {}, 105 | 'replace': {}, 106 | 'rank': {}, 107 | 'round': {}, 108 | # 'update': {'sargs': True}, 109 | 'tail': {}, 110 | 'where': {'sargs': True, 'skwargs': True}, 111 | } 112 | 113 | _REDOPS = { 114 | # REDuction OPerationS: convert the series to a single value 115 | 'all': {}, 116 | 'any': {}, 117 | 'autocorr': {}, 118 | 'corr': {'sargs': True}, 119 | 'count': {}, 120 | 'cov': {'sargs': True}, 121 | 'dot': {'sargs': True}, # other => potential binop 122 | 'equals': {'sargs': True}, # other => potential binop 123 | 'factorize': {}, # returns a tuple 124 | 'first_valid_index': {}, 125 | 'idxmax': {}, 126 | 'idxmin': {}, 127 | 'item': {}, 128 | 'items': {}, 129 | 'iteritems': {}, 130 | 'keys': {}, 131 | 'kurt': {}, 132 | 'kurtosis': {}, 133 | 'last_valid_index': {}, 134 | 'mad': {}, 135 | 'max': {}, 136 | 'mean': {}, 137 | 'median': {}, 138 | 'memory_usage': {}, 139 | 'min': {}, 140 | 'mode': {}, 141 | 'nunique': {}, 142 | 'prod': {}, 143 | 'product': {}, 144 | 'quantile': {}, 145 | 'sem': {}, 146 | 'skew': {}, 147 | 'std': {}, 148 | 'sum': {}, 149 | 'unique': {}, 150 | } 151 | 152 | 153 | _MULTIFUNCOPS = dict( 154 | # MULTIFUNCtion OPerations 155 | # Provide an object which can later provide any (of many) operations like 156 | # in a series: rolling(window=10).mean() 157 | 158 | # provide a set of oprations 159 | expanding=dict(parg='min_periods'), 160 | ewm=dict(), 161 | _ewm=dict(), 162 | rolling=dict(parg='window'), 163 | 164 | # accessors 165 | iloc=dict(propertize=True), 166 | loc=dict(propertize=True), 167 | ) 168 | -------------------------------------------------------------------------------- /btalib/meta/metadata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import threading 8 | 9 | __all__ = ['metadata'] 10 | 11 | metadata = threading.local() 12 | -------------------------------------------------------------------------------- /btalib/meta/outputs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | from . import lines 8 | 9 | __all__ = ['Output', 'Outputs'] 10 | 11 | 12 | _CLSOUTPUTS = {} # holds auto-generated Lines clases 13 | 14 | 15 | class Output(lines.Line): 16 | pass 17 | 18 | 19 | class Outputs(lines.Lines): 20 | pass 21 | 22 | 23 | def _generate(cls, bases, dct, name='outputs', klass=Outputs, **kwargs): 24 | _CLSOUTPUTS[cls] = lines._generate( 25 | cls, bases, dct, name=name, klass=klass, **kwargs, 26 | ) 27 | 28 | 29 | def _from_class(cls): 30 | return _CLSOUTPUTS[cls]() # defvals params in dict format 31 | -------------------------------------------------------------------------------- /btalib/meta/params.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | __all__ = ['Params'] 8 | 9 | 10 | _CLSPARAMS = {} # holds auto-generated Params classes 11 | _CLSPINFO = {} # holds auto-generated Params information 12 | 13 | 14 | def get_pinfo(cls): 15 | return _CLSPINFO.get(cls, {}) 16 | 17 | 18 | class Params: 19 | # Base slotted class to hold output lines (results) of indicators, which 20 | # cannot have any member attribute which has not been declared by 21 | # indicators using the "lines = ('line1', 'line2', ...) syntax 22 | 23 | # It does implemente a Mapping-like interface using __slots__ contents as 24 | # keys to be able to treat it like a dict for handy usage like "**" 25 | # expansion and iteration (general and of keys/values) 26 | 27 | __slots__ = [] 28 | 29 | def __init__(self, **kwargs): 30 | for k, v in kwargs.items(): 31 | setattr(self, k, v) 32 | 33 | def __contains__(self, item): 34 | return hasattr(self, item) 35 | 36 | def __len__(self): 37 | return len(self.__slots__) 38 | 39 | def __iter__(self): 40 | yield from self.__slots__ 41 | 42 | def __getitem__(self, item): 43 | try: 44 | return getattr(self, item) 45 | except AttributeError: 46 | pass # doe not raise inside another exception 47 | 48 | # Raise proper KeyError instead of AttributeError 49 | raise KeyError('Key not found: {}'.format(item)) 50 | 51 | def keys(self): 52 | yield from self.__slots__ 53 | 54 | def _values(self): 55 | yield from (getattr(self, x) for x in self.__slots__) 56 | 57 | def _items(self): 58 | yield from ((x, getattr(self, x)) for x in self.__slots__) 59 | 60 | def _get(self, item, default=None): 61 | try: 62 | return getattr(self, item) 63 | except AttributeError: 64 | pass 65 | 66 | return default 67 | 68 | def _update(self, **kwargs): 69 | for k, v in kwargs.items(): 70 | setattr(self, k, v) 71 | 72 | def __str__(self): 73 | return str(dict(self)) 74 | 75 | 76 | def _generate(cls, bases, dct, **kwargs): 77 | # Get the params, join them and update to final definition 78 | params = {} # to keep declared default values 79 | _CLSPINFO[cls] = pinfo = {} # to keep rest of info (doc, req'ed, ...) 80 | 81 | # Gather all params from bases and the newly updated/defined 82 | pbases = [getattr(base, 'params', {}) for base in bases] 83 | pbases.append(dct.get('params', {})) 84 | 85 | # Gather previous doc definition from bases 86 | pibases = [_CLSPINFO.get(base, {}) for base in bases] 87 | pbdocs = {k: v for pibase in pibases for k, v in pibase.items()} 88 | 89 | # Scan and set as dict, support dict/list/tuple formats 90 | # Examples (component name, default value, docstring, required) 91 | # docstring and required can be ommited and actually swapped 92 | # required must always be True/False if present 93 | # params = (('p1', 30, 'Doc1'), ('p2', None, 'Doc2', True), ('p3', 5),) 94 | # params = dict(p1=(30, 'My 1'), p2=(None, 'My2', True), p3=20) 95 | for pbase in pbases: 96 | if isinstance(pbase, (list, tuple)): 97 | pbase = dict((pname, tuple(pval)) for pname, *pval in pbase) 98 | # else: # ... it was already a dict 99 | 100 | for pname, pval in pbase.items(): 101 | if not isinstance(pval, tuple): # dict with only def val 102 | pval = (pval,) # tuple 103 | 104 | pdefault, *pothers = pval 105 | pdoc = pothers[0] if pothers else '' 106 | prequired = pothers[1] if len(pothers) > 1 else False 107 | if pdoc is True or pdoc is False: # required set before doc 108 | # prequired then set to real docstring or default 'False' 109 | pdoc, prequired = (prequired or ''), pdoc # swap 110 | 111 | if pname in params: # param set by previous base class 112 | pdoc = pdoc or pbdocs.get('doc', '') # update doc or keep 113 | 114 | params[pname] = pdefault # update rest 115 | pinfo[pname] = {'doc': pdoc, 'required': prequired} 116 | 117 | cls.params = params 118 | 119 | # Create a specific slotted Params class and install it 120 | clsdct = dict(__module__=cls.__module__, __slots__=list(params)) 121 | clsname = 'params'.capitalize() + cls.__name__ 122 | _CLSPARAMS[cls] = type(clsname, (Params,), clsdct) 123 | 124 | 125 | def _from_kwargs(cls, **kwargs): 126 | # separate instance params from other kwargs 127 | params = {k: kwargs.pop(k) for k in list(kwargs) if k in cls.params} 128 | 129 | # Params 130 | self = _CLSPARAMS[cls](**cls.params) # defvals params in dict format 131 | self._update(**params) # update with instance params 132 | return self, kwargs # rest of kwargs and params 133 | -------------------------------------------------------------------------------- /btalib/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | __all__ = [ 8 | 'SEED_AVG', 'SEED_LAST', 'SEED_SUM', 'SEED_NONE', 'SEED_ZERO', 9 | 'SEED_ZFILL', 10 | '_INCPERIOD', '_DECPERIOD', '_MINIDX', 11 | '_SERIES', '_MPSERIES', 12 | '_SETVAL', '_MPSETVAL', 13 | ] 14 | 15 | 16 | SEED_AVG = 0 17 | SEED_LAST = 1 18 | SEED_SUM = 2 19 | SEED_NONE = 4 20 | SEED_ZERO = 5 21 | SEED_ZFILL = 6 22 | 23 | 24 | def _INCPERIOD(x, p=1): 25 | ''' 26 | Forces an increase `p` in the minperiod of object `x`. 27 | 28 | Example: `ta-lib` calculates `+DM` a period of 1 too early, but calculates 29 | the depending `+DI` from the right starting point. Increasing the period, 30 | without changing the underlying already calculated `+DM` values, allows the 31 | `+DI` values to be right 32 | ''' 33 | x._minperiod += p 34 | 35 | 36 | def _DECPERIOD(x, p=1): 37 | ''' 38 | Forces an increase `p` in the minperiod of object `x`. 39 | 40 | Example: `ta-lib` calculates `obv` already when the period is `1`, 41 | discarding the needed "close" to "previous close" comparison. The only way 42 | to take this into account is to decrease the delivery period of the 43 | comparison by 1 to start the calculation before (and using a fixed 44 | criterion as to what to do in the absence of a valid close to close 45 | comparison) 46 | ''' 47 | x._minperiod -= p 48 | 49 | 50 | def _MINIDX(x, p=0): 51 | ''' 52 | Delivers the index to an array which corresponds to `_minperiod` offset by 53 | `p`. This allow direct manipulation of single values in arrays like in the 54 | `obv` scenario in which a seed value is needed for the 1st delivered value 55 | (in `ta-lib` mode) because no `close` to `previous close` comparison is 56 | possible. 57 | ''' 58 | return x._minperiod - 1 + p 59 | 60 | 61 | def _SERIES(x): 62 | '''Macro like function which makes clear that one is retrieving the actual 63 | underlying series and not something a wrapped version''' 64 | return x._series 65 | 66 | 67 | def _MPSERIES(x): 68 | '''Macro like function which makes clear that one is retrieving the actual 69 | underlying series, sliced starting at the MINPERIOD of the series''' 70 | return x._series[x._minperiod - 1:] 71 | 72 | 73 | def _SETVAL(x, idx, val): 74 | '''Macro like function which makes clear that one is setting a value in the 75 | underlying series''' 76 | x._series[idx] = val 77 | 78 | 79 | def _MPSETVAL(x, idx, val): 80 | '''Macro like function which makes clear that one is setting a value in the 81 | underlying series''' 82 | x._series[x._minperiod - 1 + idx] = val 83 | -------------------------------------------------------------------------------- /btalib/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import sys 8 | 9 | __all__ = ['__version__', '__version_info__'] 10 | 11 | __version__ = '1.0.0' 12 | 13 | __version_info__ = tuple(int(x) for x in __version__.split('.')) 14 | 15 | 16 | _min_py_error = 'Python version >=3.6 is needed. The interpreter rerports {}' 17 | 18 | assert sys.version_info >= (3, 6), _min_py_error.format(sys.version) 19 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | - Indicators: 3 | - `sar`, `sarext` 4 | - Added missing check conditions in `ht_trendmode` 5 | - Improved auto-docstring generation 6 | 7 | ## 0.9.8 8 | 9 | - Indicators: 10 | - `ht_dcperiod`, `ht_dcphase`, `ht_phasor`, `ht_sine`, `ht_trendline`, 11 | `ht_trendmode` 12 | - Allow indicators to declare numerically than less inputs than declared can 13 | be accepted with `allowinputs=x` 14 | - Refactor `mama` in terms of `ht_phasor` 15 | - Add `_applymulti` primitive to allow an apply with multiple return results 16 | 17 | ## 0.9.7 18 | 19 | - Indicators: 20 | - `mama` 21 | - Refactoring internal param names, fixing reduction op and other cosmetics 22 | - Refactor obv as a function of _period and _setval 23 | - Re-locate cumXXX and map as standard ops and not as reduction ops 24 | - Add `_talib_class` classmethod fo ta-lib compatibility actions needed 25 | before the instance is created 26 | - API call to regenerate inputs 27 | - New SEED_ZERO, SEED_ZFILL for _ewm 28 | - New behavior for `_ewm` in which if alpha is passed but no `span` is 29 | present, the actual minimum period of the calling lines is applied 30 | - Added calls `mask` and `where` 31 | - Correct from 0.9.6 that the dtype of the series was lost, which caused 32 | problems with wrapped calls which actually check the dtype for example for 33 | "boolean" like mask and where 34 | - Internal semantics improved for the `__call__` notation and line `clone` 35 | - Fix reduction operation minimum period 36 | - Remove unneeded overlap parameter for standard operations and rolling 37 | functions 38 | - Added `_setval` method to set a value/range from the minimum period 39 | - Extended `_period` to be able to set a value in the period which is 40 | added/removed 41 | - Refactor `obv` and others to use `_period` 42 | 43 | ## 0.9.6 44 | 45 | - Indicators: 46 | - `mavp` 47 | - Make scipy.signal an optional requirement with a run-time import and 48 | default to use _mean_exp if not available 49 | - Refactor _mean_exp to use prev local variable and let it take an empty beta 50 | (1-alpha) 51 | - Report right attribute which was not found in Linesholder 52 | - Add _minperiodize method which scans and adapts hidden series in args, 53 | kwargs to minperiod 54 | - Add line method which allow a dot notation period increase 55 | - Add generic _apply method to series which mimics hidden _apply in "ewm" but 56 | with args and kwargs, supporting raw for ndarrays 57 | - Refactor standard_op to use minperiodize and some renaming 58 | - Refactor reduction operations to use minperiodize and consider only the 59 | minperiod 60 | - Use SEED_AVG (0) enum as default seed instead of non-obvious False 61 | - Add SEED_NONE logic to multifunc window operations 62 | - Add reduction operation "unique" and adjust minperiod of series hidden in 63 | kwargs in apply 64 | 65 | ## 0.9.5 66 | 67 | - Indicators: 68 | - `correl` 69 | - Remove name setting because it may overwrite external inputs and change 70 | macro-like extraction to fix name, adapting test framework 71 | - Refactor crossover/up/down family, removing _type, simplifiyng formula and 72 | adding a _strict parameter to switch between 2 bar crossover (strict) and a 73 | crossover over multiple bars (non-strict) 74 | - Macro-like additions: _MPSERIES, _SETVAL and _MPSETVAL to get series 75 | relative to minperiod and set values (raw and relative to minperiod) 76 | - Make "series" and "index" properties return only the raw underlying series 77 | and index, to conform to public API to retrieve series 78 | - Re-adapt test framework to series macro-like API re-change 79 | - Remove old pandas macro module and replace with utils 80 | - Refactor obv to use new _MPSETVAL macro-like function 81 | 82 | ## 0.9.4 83 | 84 | - Indicators: 85 | - `linearreg`, `linearreg_angle`, `linearreg_intercept`, `linearreg_slope`, 86 | `tsf` 87 | - Changed inputs_override/outputs_override to be the default behavior when 88 | defining inputs in an indicator 89 | - Added inputs_extend/outputs_extend to support a partial input definition 90 | inheriting from the base 91 | - Refactor all indicators using overrides to new ruling 92 | - Add superclass _talib_ compat flag and refactor current _talib_ users to 93 | consume it 94 | - Add _SERIES macro to work with the underlying series 95 | - Convert maxindex/minindex to work with _SERIES 96 | - Convert all math indicators to "apply" instead of using series 97 | - Make "series" and "index" properties return only the [_minperiod:] slice 98 | and adapt test framework to change 99 | - Refactor for logic of indicator auto-registration 100 | - Use class name for parameterif the parameter is in the indicator hierarchy 101 | 102 | ## 0.9.3 103 | 104 | - Indicators: 105 | - `ad`, `add`, `adosc`, `adx`, `adxr`, `apo`, `aroon`, `aroonosc`, `beta`, 106 | `ceil`, `cmo`, `di`, `div`, `dm`, `dx`, `ewma`, `exp`, `floor`, `ln`, 107 | `log10`, `maxindex`, `minindex`, `minmaxindex`, `minus_di`, `minus_dm`, 108 | `mult`, `natr`, `plus_di`, `plus_dm`, `ppo`, `obv`, `ppofast`, `roc`, 109 | `rocp`, `rocr`, `rocr100`, `smacc`, `sqrt`, `sub`, `trima`, `var`, 110 | `var_s` 111 | 112 | - Improved autosuper to call all leftmost bases up to Indicator and _talib 113 | compatibility methods 114 | - Support of new testcase definition features and refactoring of testcase 115 | definitions, detection of empty tests sets and improved logging for string 116 | defined tests, also refactoring their pseudo-execution position 117 | - Refactoring of _last:_seed, _seed:_seeded and added early (period reducing) 118 | seed calculations for ta-lib compatibility 119 | - Add enum SEED_xxx for seed determination 120 | - Refactor ema, smma, kama for _last:_seed changes 121 | - Refactor obv to use minperiod "macros" 122 | - Util Macro-Style Functions to get/set periods/indices for ta-lib 123 | compatibility 124 | 125 | ## 0.9.2 126 | 127 | - Indicators: 128 | - `avgprice`, `cci`, `kama`, `medprice`, `mfi`, `stochf`, `stochrsi`, 129 | `truelow`, `truehigh`, `typprice`, `ultimateoscillator`, `wclprice`, 130 | `williamsr` 131 | 132 | - Changes and improvements to the testing framework 133 | - Internal API changes to streamline indicator development syntax 134 | - Added custom `_ewm` to use and seed directly instead in the indicators 135 | - Added support for dynamic alphas in `_ewm` 136 | - Rewrite of `ewm` based indicators to `_ewm` for simplification 137 | 138 | ## 0.9.1 139 | 140 | - First initial public release 141 | - Indicators: 142 | - `bbands`, `dema`, `ema`, `gdema`, `midpoint`, `midprice`, `sma`, `smma`, 143 | `t3`, `tema`, `macd`, `stochastic`, `rsi`, `trix`, `atr`, `truerange`, 144 | `stddev`, `stddev_s`, `acos`, `asin`, `atan`, `cos`, `cosh`, `sin`, `sinh`, 145 | `tan`, `tanh`, `max`, `min`, `sum`, `crossdown`, `crossover`, `crossup` 146 | - Utility and Informational methods 147 | - `ta-lib` compatibility flag/global setting 148 | - Testing framework in place 149 | - Documentation, except for indicator development 150 | -------------------------------------------------------------------------------- /pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Generate pypi wheels universal package and upload 4 | # 5 | python setup.py bdist_wheel --universal upload -r pypi 6 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | -r reqirements.txt 2 | nosetests 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright (C) 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import codecs # To use a consistent encoding 8 | import os.path 9 | import setuptools 10 | 11 | # Some settings 12 | AUTHOR = 'Daniel Rodriguez' 13 | 14 | PACKAGENAME = 'btalib' 15 | PYPINAME = 'bta-lib' 16 | 17 | GITHUB_BASE = 'https://github.com' 18 | GITHUB_USER = 'mementum' 19 | GITHUB_NAME = 'bta-lib' 20 | 21 | LICENSE = 'MIT' 22 | 23 | KEYWORDS = ['trading', 'development', 'backtesting', 'algotrading', 24 | 'technical analysis'] 25 | 26 | REQUIREMENTS = ['pandas'] 27 | PYTHON_VERSION = '>=3.6' 28 | 29 | DESCRIPTION = 'bta-lib technical analysis library' 30 | 31 | README = 'README.rst' 32 | VERSION_PY = 'version.py' 33 | 34 | 35 | # Get the long description from the relevant file 36 | cwd = os.path.abspath(os.path.dirname(__file__)) 37 | with codecs.open(os.path.join(cwd, README), encoding='utf-8') as f: 38 | LONG_DESCRIPTION = f.read() 39 | 40 | # GET the version ... execfile is only on Py2 ... use exec + compile + open 41 | with open(os.path.join(PACKAGENAME, VERSION_PY)) as f: 42 | exec(compile(f.read(), VERSION_PY, 'exec')) 43 | 44 | # Generate links 45 | GITHUB_URL = '/'.join((GITHUB_BASE, GITHUB_USER, GITHUB_NAME)) 46 | GITHUB_DOWN_URL = '/'.join((GITHUB_URL, 'tarball', __version__)) # noqa: F821 47 | 48 | # SETUP Proceedings 49 | setuptools.setup( 50 | name=PYPINAME, 51 | 52 | # Versions should comply with PEP440. For a discussion on single-sourcing 53 | # the version across setup.py and the project code, see 54 | # https://packaging.python.org/en/latest/single_source_version.html 55 | version=__version__, # noqa: F821 56 | 57 | description=DESCRIPTION, 58 | long_description=LONG_DESCRIPTION, 59 | 60 | # The project's main homepage. 61 | url=GITHUB_URL, 62 | download_url=GITHUB_DOWN_URL, 63 | 64 | # Author details 65 | author=AUTHOR, 66 | 67 | # Choose your license 68 | license=LICENSE, 69 | 70 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 71 | classifiers=[ 72 | # How mature is this project? Common values are 73 | # 3 - Alpha 74 | # 4 - Beta 75 | # 5 - Production/Stable 76 | 'Development Status :: 5 - Production/Stable', 77 | 78 | # Indicate who your project is intended for 79 | 'Intended Audience :: Developers', 80 | 'Intended Audience :: Financial and Insurance Industry', 81 | 82 | # Indicate which Topics are covered by the package 83 | 'Topic :: Software Development', 84 | 'Topic :: Office/Business :: Financial', 85 | 86 | # Pick your license as you wish (should match "license" above) 87 | 'License :: OSI Approved :: MIT License', 88 | 89 | # Specify the Python versions you support here. In particular, ensure 90 | # that you indicate whether you support Python 2, Python 3 or both. 91 | 'Programming Language :: Python :: 3.6', 92 | 'Programming Language :: Python :: 3.7', 93 | 'Programming Language :: Python :: 3.8', 94 | 95 | # Operating Systems on which it runs 96 | 'Operating System :: OS Independent', 97 | ], 98 | 99 | # specify minimum vresion 100 | python_requires=PYTHON_VERSION, 101 | 102 | # What does your project relate to? 103 | keywords=KEYWORDS, 104 | 105 | # You can just specify the packages manually here if your project is 106 | # simple. Or you can use find_packages(). 107 | # packages=[PACKAGENAME], 108 | packages=setuptools.find_packages(), 109 | 110 | # List run-time dependencies here. 111 | # These will be installed by pip when your 112 | # project is installed. For an analysis of "install_requires" vs pip's 113 | # requirements files see: 114 | # https://packaging.python.org/en/latest/requirements.html 115 | # install_requires=['six'], 116 | install_requires=REQUIREMENTS, 117 | 118 | # List additional groups of dependencies here 119 | # (e.g. development dependencies). 120 | # You can install these using the following syntax, for example: 121 | # $ pip install -e .[dev,test] 122 | # extras_require={}, 123 | 124 | # If there are data files included in your packages that need to be 125 | # installed, specify them here. If using Python 2.6 or less, then these 126 | # have to be included in MANIFEST.in as well. 127 | # package_data={'sample': ['package_data.dat'],}, 128 | 129 | # Although 'package_data' is the preferred approach, in some case you may 130 | # need to place data files outside of your packages. See: 131 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files 132 | # In this case, 'data_file' will be installed into '/my_data' 133 | # data_files=[('my_data', ['data/data_file'])], 134 | 135 | # To provide executable scripts, use entry points in preference to the 136 | # "scripts" keyword. Entry points provide cross-platform support and allow 137 | # pip to create the appropriate form of executable for the target platform. 138 | # entry_points={'console_scripts': ['sample=sample:main',],}, 139 | 140 | # scripts=[], 141 | ) 142 | -------------------------------------------------------------------------------- /tests/test_indicators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import testcommon 8 | 9 | import test_linesholder 10 | import test_outputs 11 | import test_series_fetcher 12 | 13 | 14 | def test_run(main=False): 15 | testcommon.run_indicators(metatests, main=main) 16 | 17 | 18 | metatests = dict( 19 | # Price Transform 20 | avgprice=dict(minperiod=1, decimals=12), 21 | medprice=dict(minperiod=1, decimals=12), 22 | typprice=dict(minperiod=1, decimals=12), 23 | wclprice=dict(minperiod=1, decimals=12), 24 | 25 | # Math Operators 26 | # ## over the entire series 27 | add=dict(inputs=['high', 'low']), 28 | div=dict(inputs=['high', 'low']), 29 | mult=dict(inputs=['high', 'low']), 30 | sub=dict(inputs=['high', 'low']), 31 | 32 | # ## over a period 33 | max=dict(minperiod=30), 34 | min=dict(minperiod=30), 35 | minmax=dict(minperiod=30), 36 | maxindex=dict(minperiod=1, talib=True), 37 | minindex=dict(minperiod=1, talib=True), 38 | minmaxindex=dict(minperiod=1, talib=True), 39 | sum=dict(minperiod=30), 40 | 41 | # Math Transform 42 | acos=dict(minperiod=1, inputop=lambda *a: [x / 4000.0 for x in a]), 43 | asin=dict(minperiod=1, inputop=lambda *a: [x / 4000.0 for x in a]), 44 | atan=dict(minperiod=1), 45 | cos=dict(minperiod=1), 46 | cosh=dict(minperiod=1, inputop=lambda *a: [x / 4000.0 for x in a]), 47 | sin=dict(minperiod=1), 48 | sinh=dict(minperiod=1, inputop=lambda *a: [x / 4000.0 for x in a]), 49 | tan=dict(minperiod=1), 50 | tanh=dict(minperiod=1), 51 | 52 | ceil=dict(minperiod=1), 53 | exp=dict(minperiod=1, inputop=lambda *a: [x / 1000.0 for x in a]), 54 | floor=dict(minperiod=1), 55 | ln=dict(minperiod=1), 56 | log10=dict(minperiod=1), 57 | sqrt=dict(minperiod=1), 58 | 59 | # Statistic Functions 60 | mad='cci', # mean asbsolute deviation is tested by cci, not in ta-lib 61 | stddev=dict(minperiod=5, decimals=6, talib=True), 62 | var=dict(minperiod=5, decimals=7), 63 | 64 | beta=dict(minperiod=6, decimals=10, inputs=['high', 'low']), 65 | correl=dict(minperiod=30, decimals=9), 66 | linearreg=dict(minperiod=14, decimals=10, talib=True), 67 | linearreg_angle=dict(minperiod=14, decimals=9), 68 | linearreg_intercept=dict(minperiod=14, decimals=10, talib=True), 69 | linearreg_slope=dict(minperiod=14, decimals=10), 70 | tsf=dict(minperiod=14, decimals=9, talib=True), 71 | 72 | # Overlap 73 | sma=dict(minperiod=30), 74 | smma='rsi', # tested by RSI, no direct comparison with ta-lib possible 75 | wma=dict(minperiod=30, decimals=9), 76 | ema=dict(minperiod=30, decimals=9), 77 | ewma='adosc', 78 | dema=dict(minperiod=59, decimals=9), 79 | gdema='t3', # tested with T3 80 | kama=dict(minperiod=31, talib=True), 81 | t3=dict(minperiod=25), 82 | tema=dict(minperiod=88, decimals=10), 83 | trima=dict(minperiod=30, decimals=10), 84 | trix=dict(minperiod=89, decimals=12), 85 | 86 | mama=dict(minperiod=33, decimals=9, talib=True, inputs=['close']), 87 | ht_trendline=dict(minperiod=64, decimals=9, inputs=['close']), 88 | 89 | # ta-lib order top/mid/bot, swap outputs 0:1 to match 90 | bbands=dict(minperiod=5, decimals=6, talib=True, swapouts={0: 1}), 91 | midpoint=dict(minperiod=14), 92 | midprice=dict(minperiod=14), 93 | 94 | 95 | mavp=dict( 96 | minperiod=30, 97 | decimals=10, 98 | talib=True, 99 | inputs=['close', 'high'], 100 | inputop=lambda i0, i1: [i0, (i1 % 25).astype(int)], 101 | ), 102 | 103 | # ## acumulation (also inside the overlap group) 104 | smacc='plus_dm', # tested with the directoinal movement indicators 105 | 106 | # Cycle 107 | ht_dcperiod=dict(minperiod=33, decimals=9, inputs=['close']), 108 | ht_dcphase=dict(minperiod=64, decimals=9, inputs=['close']), 109 | ht_phasor=dict(minperiod=33, decimals=9, inputs=['close']), 110 | ht_sine=dict(minperiod=64, decimals=9, inputs=['close']), 111 | ht_trendmode=dict(minperiod=64, decimals=9, inputs=['close']), 112 | 113 | # Momentum 114 | apo=dict(minperiod=26, decimals=9, talib=True), 115 | aroon=dict(minperiod=15, decimals=13), 116 | aroonosc=dict(minperiod=15, decimals=12), 117 | bop=dict(minperiod=1), 118 | cci=dict(minperiod=14, decimals=8, talib=True), 119 | cmo=dict(minperiod=15, decimals=11, talib=True), 120 | macd=dict(minperiods=[26, 34, 34], decimals=9, talib=True), 121 | mfi=dict(minperiod=15, decimals=11), 122 | mom=dict(minperiod=11), 123 | ppo=dict(minperiods=[26, 34, 34], decimals=10, talib=True), 124 | ppofast='ppo', 125 | roc=dict(minperiod=11, talib=True), 126 | rocp=dict(minperiod=11, decimals=14, talib=True), 127 | rocr=dict(minperiod=11, talib=True), 128 | rocr100=dict(minperiod=11, talib=True), 129 | rsi=dict(minperiod=15, decimals=11), 130 | sar=dict(minperiod=2), 131 | sarext=dict(minperiod=2), 132 | stoch=dict(minperiods=[7, 9], decimals=11, talib=True), 133 | stochf=dict(minperiods=[5, 7], decimals=11, talib=True), 134 | stochrsi=dict(minperiods=[19, 21], decimals=9, talib=True), 135 | williamsr=dict(minperiod=14, decimals=12), 136 | ultimateoscillator=dict(minperiod=29, decimals=11), 137 | 138 | # ## also momentum: Directional Movement/Indicators/Index from Wilders 139 | # ## In the order in which they are defined due to the dependencies 140 | plus_dm=dict(minperiods=14, decimals=10, talib=True), 141 | minus_dm=dict(minperiods=14, decimals=11, talib=True), 142 | dm='plus_dm', 143 | plus_di=dict(minperiods=15, decimals=11, talib=True), 144 | minus_di=dict(minperiods=15, decimals=11, talib=True), 145 | di='plus_di', 146 | dx=dict(minperiods=15, decimals=11, talib=True), 147 | adx=dict(minperiods=28, decimals=11, talib=True), 148 | adxr=dict(minperiods=41, decimals=11, talib=True), 149 | 150 | # Volatility 151 | atr=dict(minperiod=15, decimals=11), 152 | natr=dict(minperiod=15, decimals=13), 153 | truerange=dict(minperiod=2), 154 | truehigh='truerange', 155 | truelow='truerange', 156 | 157 | # Volume 158 | ad=dict(minperiod=1), 159 | adosc=dict(minperiod=10, talib=True), 160 | obv=dict(minperiod=1, talib=True), 161 | 162 | # OTHER TESTS - Internal functionality 163 | series_fetcher=test_series_fetcher.run, 164 | linesholder=test_linesholder.run, 165 | outputs=test_outputs.run, 166 | ) 167 | 168 | 169 | if __name__ == '__main__': 170 | test_run(main=True) 171 | -------------------------------------------------------------------------------- /tests/test_linesholder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import testcommon 8 | 9 | import btalib 10 | 11 | 12 | def run(main=False): 13 | IND = btalib.stochastic 14 | INDOUTS = IND.outputs 15 | TYPE = btalib.meta.lines.Line 16 | 17 | df = testcommon.df 18 | indicator = IND(df) 19 | 20 | assert all(hasattr(indicator, x) for x in INDOUTS) 21 | assert all(type(getattr(indicator, x)) == TYPE for x in INDOUTS) 22 | 23 | assert len(indicator) == len(INDOUTS) 24 | 25 | slist = list(indicator) 26 | assert len(slist) == len(INDOUTS) 27 | assert all(type(x) == TYPE for x in slist) 28 | 29 | slist = list(iter(indicator)) 30 | assert len(slist) == len(INDOUTS) 31 | assert all(type(x) == TYPE for x in slist) 32 | 33 | sdict = dict(indicator) 34 | assert len(sdict) == len(INDOUTS) 35 | assert all(type(x) == TYPE for x in sdict.values()) 36 | assert all(x == y for x, y in zip(sdict, INDOUTS)) 37 | 38 | sdict = dict(**indicator) 39 | assert len(sdict) == len(INDOUTS) 40 | assert all(type(x) == TYPE for x in sdict.values()) 41 | assert all(x == y for x, y in zip(sdict, INDOUTS)) 42 | 43 | assert all(x in sdict for x in INDOUTS) 44 | 45 | def xargs(*args): 46 | assert len(args) == len(INDOUTS) 47 | assert all(type(x) == TYPE for x in args) 48 | 49 | xargs(*indicator) 50 | 51 | return True 52 | 53 | 54 | if __name__ == '__main__': 55 | run(main=True) 56 | -------------------------------------------------------------------------------- /tests/test_outputs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import testcommon 8 | 9 | import btalib 10 | 11 | 12 | def run(main=False): 13 | IND = btalib.stochastic 14 | INDOUTS = IND.outputs 15 | TYPE = btalib.meta.lines.Line 16 | 17 | df = testcommon.df 18 | outputs = IND(df).outputs 19 | 20 | assert all(hasattr(outputs, x) for x in INDOUTS) 21 | assert all(type(getattr(outputs, x)) == TYPE for x in INDOUTS) 22 | 23 | assert len(outputs) == len(INDOUTS) 24 | 25 | slist = list(outputs) 26 | assert len(slist) == len(INDOUTS) 27 | assert all(type(x) == TYPE for x in slist) 28 | 29 | slist = list(iter(outputs)) 30 | assert len(slist) == len(INDOUTS) 31 | assert all(type(x) == TYPE for x in slist) 32 | 33 | sdict = dict(outputs) 34 | assert len(sdict) == len(INDOUTS) 35 | assert all(type(x) == TYPE for x in sdict.values()) 36 | assert all(x == y for x, y in zip(sdict, INDOUTS)) 37 | 38 | sdict = dict(**outputs) 39 | assert len(sdict) == len(INDOUTS) 40 | assert all(type(x) == TYPE for x in sdict.values()) 41 | assert all(x == y for x, y in zip(sdict, INDOUTS)) 42 | 43 | assert all(x in sdict for x in INDOUTS) 44 | 45 | def xargs(*args): 46 | assert len(args) == len(INDOUTS) 47 | assert all(type(x) == TYPE for x in args) 48 | 49 | xargs(*outputs) 50 | 51 | return True 52 | 53 | 54 | if __name__ == '__main__': 55 | run(main=True) 56 | -------------------------------------------------------------------------------- /tests/test_series_fetcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # Copyright 2020 Daniel Rodriguez 5 | # Use of this source code is governed by the MIT License 6 | ############################################################################### 7 | import testcommon 8 | 9 | import btalib 10 | import pandas as pd 11 | 12 | 13 | def run(main=False): 14 | IND = btalib.stochastic 15 | INDOUTS = IND.outputs 16 | TYPE = pd.Series 17 | 18 | df = testcommon.df 19 | series = IND(df).series 20 | 21 | assert all(hasattr(series, x) for x in INDOUTS) 22 | assert all(type(getattr(series, x)) == TYPE for x in INDOUTS) 23 | 24 | assert len(series) == len(INDOUTS) 25 | 26 | slist = list(series) 27 | assert len(slist) == len(INDOUTS) 28 | assert all(type(x) == TYPE for x in slist) 29 | 30 | slist = list(iter(series)) 31 | assert len(slist) == len(INDOUTS) 32 | assert all(type(x) == TYPE for x in slist) 33 | 34 | sdict = dict(series) 35 | assert len(sdict) == len(INDOUTS) 36 | assert all(type(x) == TYPE for x in sdict.values()) 37 | assert all(x == y for x, y in zip(sdict, INDOUTS)) 38 | 39 | sdict = dict(**series) 40 | assert len(sdict) == len(INDOUTS) 41 | assert all(type(x) == TYPE for x in sdict.values()) 42 | assert all(x == y for x, y in zip(sdict, INDOUTS)) 43 | 44 | assert all(x in sdict for x in INDOUTS) 45 | 46 | def xargs(*args): 47 | assert len(args) == len(INDOUTS) 48 | assert all(type(x) == TYPE for x in args) 49 | 50 | xargs(*series) 51 | 52 | return True 53 | 54 | 55 | if __name__ == '__main__': 56 | run(main=True) 57 | --------------------------------------------------------------------------------