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
37 changes: 33 additions & 4 deletions stock_indicators/indicators/fractal.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ def get_fractal(


def get_fractal(
quotes, left_span=None, right_span=EndType.HIGH_LOW, end_type=EndType.HIGH_LOW
quotes,
left_span=None,
right_span=None,
end_type=None,
*,
window_span=None,
):
"""Get Williams Fractal calculated.

Expand Down Expand Up @@ -57,11 +62,35 @@ def get_fractal(
- [Williams Fractal Reference](https://python.stockindicators.dev/indicators/Fractal/#content)
- [Helper Methods](https://python.stockindicators.dev/utilities/#content)
"""
# `window_span` is the documented name for the symmetric form's span.
# It is an alternative to the left/right span pair, not a third span,
# so it cannot be combined with either span argument.
if window_span is not None:
if left_span is not None or right_span is not None:
raise TypeError(
"window_span cannot be combined with left_span or right_span"
)
left_span = window_span

# In the symmetric form the second positional argument is `end_type`,
# not `right_span`: get_fractal(quotes, 2, EndType.CLOSE)
if isinstance(right_span, EndType):
if left_span is None:
left_span = 2
if end_type is not None and end_type != right_span:
raise ValueError(
"end_type was given twice with different values: "
f"{right_span} and {end_type}"
)
end_type = right_span
right_span = None

if left_span is None:
left_span = 2
if end_type is None:
end_type = EndType.HIGH_LOW

if right_span is None:
fractal_results = CsIndicator.GetFractal[Quote](
CsList(Quote, quotes), left_span, right_span.cs_value
CsList(Quote, quotes), left_span, end_type.cs_value
)
else:
fractal_results = CsIndicator.GetFractal[Quote](
Expand Down
69 changes: 69 additions & 0 deletions tests/test_fractal.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,75 @@ def test_standard_span_4(self, quotes):
assert r.fractal_bear is None
assert r.fractal_bull is None

def test_window_span_keyword(self, quotes):
# Documented form: get_fractal(quotes, window_span=4, end_type=...).
# A non-default span, so an ignored keyword would change the result.
results = indicators.get_fractal(quotes, window_span=4)

assert 502 == len(results)
assert 35 == len(list(filter(lambda x: x.fractal_bear is not None, results)))
assert 34 == len(list(filter(lambda x: x.fractal_bull is not None, results)))

r = results[7]
assert r.fractal_bear is None
assert 212.53 == round(float(r.fractal_bull), 2)

def test_window_span_with_end_type_keyword(self, quotes):
results = indicators.get_fractal(quotes, window_span=2, end_type=EndType.CLOSE)
positional = indicators.get_fractal(quotes, 2, EndType.CLOSE)

assert 64 == len(list(filter(lambda x: x.fractal_bear is not None, results)))
assert 69 == len(list(filter(lambda x: x.fractal_bull is not None, results)))

for keyword_result, positional_result in zip(results, positional):
assert keyword_result.fractal_bear == positional_result.fractal_bear
assert keyword_result.fractal_bull == positional_result.fractal_bull

def test_end_type_keyword_window_form(self, quotes):
# end_type given as a keyword in the window-span form must not be
# silently ignored: it must match the positional EndType form.
results = indicators.get_fractal(quotes, 2, end_type=EndType.CLOSE)
positional = indicators.get_fractal(quotes, 2, EndType.CLOSE)

assert 64 == len(list(filter(lambda x: x.fractal_bear is not None, results)))
assert 69 == len(list(filter(lambda x: x.fractal_bull is not None, results)))

for keyword_result, positional_result in zip(results, positional):
assert keyword_result.fractal_bear == positional_result.fractal_bear
assert keyword_result.fractal_bull == positional_result.fractal_bull

def test_end_type_keyword_only(self, quotes):
results = indicators.get_fractal(quotes, end_type=EndType.CLOSE)

assert 64 == len(list(filter(lambda x: x.fractal_bear is not None, results)))
assert 69 == len(list(filter(lambda x: x.fractal_bull is not None, results)))
Comment thread
DaveSkender marked this conversation as resolved.
Resolved

def test_end_type_keyword_asymmetric_form(self, quotes):
results = indicators.get_fractal(quotes, 4, 4, end_type=EndType.HIGH_LOW)
positional = indicators.get_fractal(quotes, 4, 4, EndType.HIGH_LOW)

for keyword_result, positional_result in zip(results, positional):
assert keyword_result.fractal_bear == positional_result.fractal_bear
assert keyword_result.fractal_bull == positional_result.fractal_bull

def test_window_span_conflicts_raise(self, quotes):
with pytest.raises(TypeError, match="window_span cannot be combined"):
indicators.get_fractal(quotes, 2, 3, window_span=4)
with pytest.raises(TypeError, match="window_span cannot be combined"):
indicators.get_fractal(quotes, window_span=2, right_span=2)
with pytest.raises(TypeError, match="window_span cannot be combined"):
indicators.get_fractal(quotes, left_span=2, window_span=2)

def test_end_type_conflict_raises(self, quotes):
with pytest.raises(ValueError, match="end_type was given twice"):
indicators.get_fractal(quotes, 2, EndType.CLOSE, end_type=EndType.HIGH_LOW)

# The same value given twice is not a conflict.
results = indicators.get_fractal(
quotes, 2, EndType.CLOSE, end_type=EndType.CLOSE
)
assert 64 == len(list(filter(lambda x: x.fractal_bear is not None, results)))

def test_no_data(self, quotes):
r = indicators.get_fractal([])
assert 0 == len(r)
Expand Down