Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions stock_indicators/indicators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .alligator import (get_alligator)
from .alma import (get_alma)
from .aroon import (get_aroon)
from .atr_stop import (get_atr_stop)
from .atr import (get_atr)
from .awesome import (get_awesome)
from .basic_quotes import (get_basic_quote)
Expand Down
83 changes: 83 additions & 0 deletions stock_indicators/indicators/atr_stop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from decimal import Decimal
from typing import Iterable, Optional, TypeVar

from stock_indicators._cslib import CsIndicator
from stock_indicators._cstypes import List as CsList
from stock_indicators._cstypes import Decimal as CsDecimal
from stock_indicators._cstypes import to_pydecimal
from stock_indicators.indicators.common.enums import EndType
from stock_indicators.indicators.common.helpers import RemoveWarmupMixin
from stock_indicators.indicators.common.results import IndicatorResults, ResultBase
from stock_indicators.indicators.common.quote import Quote


def get_atr_stop(quotes: Iterable[Quote], lookback_periods: int = 21,
multiplier: float = 3, end_type: EndType = EndType.CLOSE):
"""Get ATR Trailing Stop calculated.

ATR Trailing Stop attempts to determine the primary trend of prices by using
Average True Range (ATR) band thresholds. It can indicate a buy/sell signal or a
trailing stop when the trend changes.

Parameters:
`quotes` : Iterable[Quote]
Historical price quotes.

`lookback_periods` : int, defaults 21
Number of periods for ATR.

`multiplier` : float, defaults 3
Multiplier sets the ATR band width.

`end_type` : EndType, defaults EndType.CLOSE
Sets basis for stop offsets (Close or High/Low).

Returns:
`AtrStopResults[AtrStopResult]`
AtrStopResults is list of AtrStopResult with providing useful helper methods.

See more:
- [ATR Trailing Stop Reference](https://python.stockindicators.dev/indicators/AtrStop/#content)
- [Helper Methods](https://python.stockindicators.dev/utilities/#content)
"""
results = CsIndicator.GetAtrStop[Quote](CsList(Quote, quotes), lookback_periods, multiplier, end_type.cs_value)
return AtrStopResults(results, AtrStopResult)


class AtrStopResult(ResultBase):
"""
A wrapper class for a single unit of ATR Trailing Stop results.
"""

@property
def atr_stop(self) -> Optional[Decimal]:
return to_pydecimal(self._csdata.AtrStop)

@atr_stop.setter
def atr_stop(self, value):
self._csdata.AtrStop = CsDecimal(value)

@property
def buy_stop(self) -> Optional[Decimal]:
return to_pydecimal(self._csdata.BuyStop)

@buy_stop.setter
def buy_stop(self, value):
self._csdata.BuyStop = CsDecimal(value)

@property
def sell_stop(self) -> Optional[Decimal]:
return to_pydecimal(self._csdata.SellStop)

@sell_stop.setter
def sell_stop(self, value):
self._csdata.SellStop = CsDecimal(value)


_T = TypeVar("_T", bound=AtrStopResult)
class AtrStopResults(RemoveWarmupMixin, IndicatorResults[_T]):
"""
A wrapper class for the list of ATR Trailing Stop results.
It is exactly same with built-in `list` except for that it provides
some useful helper methods written in CSharp implementation.
"""
107 changes: 107 additions & 0 deletions tests/test_atr_stop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import pytest
from stock_indicators import indicators
from stock_indicators.indicators.common.enums import EndType

class TestAtrStop:
def test_standard(self, quotes):
results = indicators.get_atr_stop(quotes, 21, 3, EndType.CLOSE)

assert 502 == len(results)
assert 481 == len(list(filter(lambda x: x.atr_stop is not None, results)))

r = results[20]
assert r.atr_stop is None
assert r.buy_stop is None
assert r.sell_stop is None

r = results[21]
assert 211.13 == round(float(r.atr_stop), 4)
assert r.buy_stop is None
assert r.atr_stop == r.sell_stop

r = results[151]
assert 232.7861 == round(float(r.atr_stop), 4)
assert r.buy_stop is None
assert r.atr_stop == r.sell_stop

r = results[152]
assert 236.3913 == round(float(r.atr_stop), 4)
assert r.atr_stop == r.buy_stop
assert r.sell_stop is None

r = results[249]
assert 253.8863 == round(float(r.atr_stop), 4)
assert r.buy_stop is None
assert r.atr_stop == r.sell_stop

r = results[501]
assert 246.3232 == round(float(r.atr_stop), 4)
assert r.atr_stop == r.buy_stop
assert r.sell_stop is None

def test_high_low(self, quotes):
results = indicators.get_atr_stop(quotes, 21, 3, EndType.HIGH_LOW)

assert 502 == len(results)
assert 481 == len(list(filter(lambda x: x.atr_stop is not None, results)))

r = results[20]
assert r.atr_stop is None
assert r.buy_stop is None
assert r.sell_stop is None

r = results[21]
assert 210.23 == round(float(r.atr_stop), 4)
assert r.buy_stop is None
assert r.atr_stop == r.sell_stop

r = results[69]
assert 221.0594 == round(float(r.atr_stop), 4)
assert r.buy_stop is None
assert r.atr_stop == r.sell_stop

r = results[70]
assert 226.4624 == round(float(r.atr_stop), 4)
assert r.atr_stop == r.buy_stop
assert r.sell_stop is None

r = results[249]
assert 253.4863 == round(float(r.atr_stop), 4)
assert r.buy_stop is None
assert r.atr_stop == r.sell_stop

r = results[501]
assert 252.6932 == round(float(r.atr_stop), 4)
assert r.atr_stop == r.buy_stop
assert r.sell_stop is None


def test_bad_data(self, bad_quotes):
r = indicators.get_atr_stop(bad_quotes, 7)

assert 502 == len(r)

def test_no_quotes(self, quotes):
r = indicators.get_atr_stop([])
assert 0 == len(r)

r = indicators.get_atr_stop(quotes[:1])
assert 1 == len(r)

def test_removed(self, quotes):
results = indicators.get_atr_stop(quotes, 21, 3).remove_warmup_periods()

assert 481 == len(results)

last = results.pop()
assert 246.3232 == round(float(last.atr_stop), 4)
assert last.atr_stop == last.buy_stop
assert last.sell_stop is None

def test_exceptions(self, quotes):
from System import ArgumentOutOfRangeException
with pytest.raises(ArgumentOutOfRangeException):
indicators.get_atr_stop(quotes, 1)

with pytest.raises(ArgumentOutOfRangeException):
indicators.get_atr_stop(quotes, 7, 0)