diff --git a/stock_indicators/indicators/__init__.py b/stock_indicators/indicators/__init__.py index 5acf08fa..3aa0c6da 100644 --- a/stock_indicators/indicators/__init__.py +++ b/stock_indicators/indicators/__init__.py @@ -24,6 +24,7 @@ from .donchian import (get_donchian) from .dema import (get_dema) from .dpo import (get_dpo) +from .dynamic import (get_dynamic) from .elder_ray import (get_elder_ray) from .ema import (get_ema) from .epma import (get_epma) diff --git a/stock_indicators/indicators/dynamic.py b/stock_indicators/indicators/dynamic.py new file mode 100644 index 00000000..12608cef --- /dev/null +++ b/stock_indicators/indicators/dynamic.py @@ -0,0 +1,56 @@ +from typing import Iterable, Optional, TypeVar + +from stock_indicators._cslib import CsIndicator +from stock_indicators._cstypes import List as CsList +from stock_indicators.indicators.common.results import IndicatorResults, ResultBase +from stock_indicators.indicators.common.quote import Quote + + +def get_dynamic(quotes: Iterable[Quote], lookback_periods: int, k_factor: float = 0.6): + """Get McGinley Dynamic calculated. + + McGinley Dynamic is a more responsive variant of exponential moving average. + + Parameters: + `quotes` : Iterable[Quote] + Historical price quotes. + + `lookback_periods` : int + Number of periods in the lookback window. + + `k_factor` : float, defaults 0.6 + Range adjustment factor. + + Returns: + `DynamicResults[DynamicResult]` + DynamicResults is list of DynamicResult with providing useful helper methods. + + See more: + - [McGinley Dynamic Reference](https://python.stockindicators.dev/indicators/Dynamic/#content) + - [Helper Methods](https://python.stockindicators.dev/utilities/#content) + """ + results = CsIndicator.GetDynamic[Quote](CsList(Quote, quotes), lookback_periods, k_factor) + return DynamicResults(results, DynamicResult) + + +class DynamicResult(ResultBase): + """ + A wrapper class for a single unit of McGinley Dynamic results. + """ + + @property + def dynamic(self) -> Optional[float]: + return self._csdata.Dynamic + + @dynamic.setter + def dynamic(self, value): + self._csdata.Dynamic = value + + +_T = TypeVar("_T", bound=DynamicResult) +class DynamicResults(IndicatorResults[_T]): + """ + A wrapper class for the list of McGinley Dynamic results. + It is exactly same with built-in `list` except for that it provides + some useful helper methods written in CSharp implementation. + """ diff --git a/tests/test_dynamic.py b/tests/test_dynamic.py new file mode 100644 index 00000000..44c9fab2 --- /dev/null +++ b/tests/test_dynamic.py @@ -0,0 +1,41 @@ +import pytest +from stock_indicators import indicators + +class TestDynamic: + def test_standard(self, quotes): + results = indicators.get_dynamic(quotes, 14) + + assert 502 == len(results) + assert 501 == len(list(filter(lambda x: x.dynamic is not None, results))) + + r = results[1] + assert 212.9465 == round(float(r.dynamic), 4) + + r = results[25] + assert 215.4801 == round(float(r.dynamic), 4) + + r = results[250] + assert 256.0554== round(float(r.dynamic), 4) + + r = results[501] + assert 245.7356 == round(float(r.dynamic), 4) + + def test_bad_data(self, bad_quotes): + r = indicators.get_dynamic(bad_quotes, 15) + + assert 502 == len(r) + + def test_no_quotes(self, quotes): + r = indicators.get_dynamic([], 14) + assert 0 == len(r) + + r = indicators.get_dynamic(quotes[:1], 14) + assert 1 == len(r) + + def test_exceptions(self, quotes): + from System import ArgumentOutOfRangeException + with pytest.raises(ArgumentOutOfRangeException): + indicators.get_dynamic(quotes, 0) + + with pytest.raises(ArgumentOutOfRangeException): + indicators.get_dynamic(quotes, 14, 0)