Skip to content

Commit

Permalink
feat: Add ATR Trailing Stop (#369)
Browse files Browse the repository at this point in the history
  • Loading branch information
LeeDongGeon1996 authored Apr 7, 2024
1 parent 4448f0f commit 7273ec4
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 0 deletions.
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)

0 comments on commit 7273ec4

Please sign in to comment.