-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4448f0f
commit 7273ec4
Showing
3 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |