208 lines
7.9 KiB
Python
208 lines
7.9 KiB
Python
"""Unit tests for ferro_ta.indicators.pattern (CDL* functions)"""
|
|
import numpy as np
|
|
import pytest
|
|
from ferro_ta.indicators.pattern import (
|
|
CDL2CROWS, CDL3BLACKCROWS, CDL3INSIDE, CDL3LINESTRIKE, CDL3OUTSIDE,
|
|
CDL3STARSINSOUTH, CDL3WHITESOLDIERS, CDLABANDONEDBABY, CDLADVANCEBLOCK,
|
|
CDLBELTHOLD, CDLBREAKAWAY, CDLCLOSINGMARUBOZU, CDLCONCEALBABYSWALL,
|
|
CDLCOUNTERATTACK, CDLDARKCLOUDCOVER, CDLDOJI, CDLDOJISTAR, CDLDRAGONFLYDOJI,
|
|
CDLENGULFING, CDLEVENINGDOJISTAR, CDLEVENINGSTAR, CDLGAPSIDESIDEWHITE,
|
|
CDLGRAVESTONEDOJI, CDLHAMMER, CDLHANGINGMAN, CDLHARAMI, CDLHARAMICROSS,
|
|
CDLHIGHWAVE, CDLHIKKAKE, CDLHIKKAKEMOD, CDLHOMINGPIGEON, CDLIDENTICAL3CROWS,
|
|
CDLINNECK, CDLINVERTEDHAMMER, CDLKICKING, CDLKICKINGBYLENGTH, CDLLADDERBOTTOM,
|
|
CDLLONGLEGGEDDOJI, CDLLONGLINE, CDLMARUBOZU, CDLMATCHINGLOW, CDLMATHOLD,
|
|
CDLMORNINGDOJISTAR, CDLMORNINGSTAR, CDLONNECK, CDLPIERCING, CDLRICKSHAWMAN,
|
|
CDLRISEFALL3METHODS, CDLSEPARATINGLINES, CDLSHOOTINGSTAR, CDLSHORTLINE,
|
|
CDLSPINNINGTOP, CDLSTALLEDPATTERN, CDLSTICKSANDWICH, CDLTAKURI, CDLTASUKIGAP,
|
|
CDLTHRUSTING, CDLTRISTAR, CDLUNIQUE3RIVER, CDLUPSIDEGAP2CROWS, CDLXSIDEGAP3METHODS,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared random OHLCV data (realistic OHLCV, proper H >= O,C >= L)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
RNG = np.random.default_rng(42)
|
|
N = 200
|
|
_C = 100 + np.cumsum(RNG.normal(0, 0.5, N))
|
|
_O = _C + RNG.normal(0, 0.2, N)
|
|
_H = np.maximum(np.maximum(_O, _C) + np.abs(RNG.normal(0, 0.3, N)), np.maximum(_O, _C))
|
|
_L = np.minimum(np.minimum(_O, _C) - np.abs(RNG.normal(0, 0.3, N)), np.minimum(_O, _C))
|
|
|
|
# All CDL* functions to test systematically
|
|
ALL_CDL = [
|
|
("CDL2CROWS", CDL2CROWS),
|
|
("CDL3BLACKCROWS", CDL3BLACKCROWS),
|
|
("CDL3INSIDE", CDL3INSIDE),
|
|
("CDL3LINESTRIKE", CDL3LINESTRIKE),
|
|
("CDL3OUTSIDE", CDL3OUTSIDE),
|
|
("CDL3STARSINSOUTH", CDL3STARSINSOUTH),
|
|
("CDL3WHITESOLDIERS", CDL3WHITESOLDIERS),
|
|
("CDLABANDONEDBABY", CDLABANDONEDBABY),
|
|
("CDLADVANCEBLOCK", CDLADVANCEBLOCK),
|
|
("CDLBELTHOLD", CDLBELTHOLD),
|
|
("CDLBREAKAWAY", CDLBREAKAWAY),
|
|
("CDLCLOSINGMARUBOZU", CDLCLOSINGMARUBOZU),
|
|
("CDLCONCEALBABYSWALL", CDLCONCEALBABYSWALL),
|
|
("CDLCOUNTERATTACK", CDLCOUNTERATTACK),
|
|
("CDLDARKCLOUDCOVER", CDLDARKCLOUDCOVER),
|
|
("CDLDOJI", CDLDOJI),
|
|
("CDLDOJISTAR", CDLDOJISTAR),
|
|
("CDLDRAGONFLYDOJI", CDLDRAGONFLYDOJI),
|
|
("CDLENGULFING", CDLENGULFING),
|
|
("CDLEVENINGDOJISTAR", CDLEVENINGDOJISTAR),
|
|
("CDLEVENINGSTAR", CDLEVENINGSTAR),
|
|
("CDLGAPSIDESIDEWHITE", CDLGAPSIDESIDEWHITE),
|
|
("CDLGRAVESTONEDOJI", CDLGRAVESTONEDOJI),
|
|
("CDLHAMMER", CDLHAMMER),
|
|
("CDLHANGINGMAN", CDLHANGINGMAN),
|
|
("CDLHARAMI", CDLHARAMI),
|
|
("CDLHARAMICROSS", CDLHARAMICROSS),
|
|
("CDLHIGHWAVE", CDLHIGHWAVE),
|
|
("CDLHIKKAKE", CDLHIKKAKE),
|
|
("CDLHIKKAKEMOD", CDLHIKKAKEMOD),
|
|
("CDLHOMINGPIGEON", CDLHOMINGPIGEON),
|
|
("CDLIDENTICAL3CROWS", CDLIDENTICAL3CROWS),
|
|
("CDLINNECK", CDLINNECK),
|
|
("CDLINVERTEDHAMMER", CDLINVERTEDHAMMER),
|
|
("CDLKICKING", CDLKICKING),
|
|
("CDLKICKINGBYLENGTH", CDLKICKINGBYLENGTH),
|
|
("CDLLADDERBOTTOM", CDLLADDERBOTTOM),
|
|
("CDLLONGLEGGEDDOJI", CDLLONGLEGGEDDOJI),
|
|
("CDLLONGLINE", CDLLONGLINE),
|
|
("CDLMARUBOZU", CDLMARUBOZU),
|
|
("CDLMATCHINGLOW", CDLMATCHINGLOW),
|
|
("CDLMATHOLD", CDLMATHOLD),
|
|
("CDLMORNINGDOJISTAR", CDLMORNINGDOJISTAR),
|
|
("CDLMORNINGSTAR", CDLMORNINGSTAR),
|
|
("CDLONNECK", CDLONNECK),
|
|
("CDLPIERCING", CDLPIERCING),
|
|
("CDLRICKSHAWMAN", CDLRICKSHAWMAN),
|
|
("CDLRISEFALL3METHODS", CDLRISEFALL3METHODS),
|
|
("CDLSEPARATINGLINES", CDLSEPARATINGLINES),
|
|
("CDLSHOOTINGSTAR", CDLSHOOTINGSTAR),
|
|
("CDLSHORTLINE", CDLSHORTLINE),
|
|
("CDLSPINNINGTOP", CDLSPINNINGTOP),
|
|
("CDLSTALLEDPATTERN", CDLSTALLEDPATTERN),
|
|
("CDLSTICKSANDWICH", CDLSTICKSANDWICH),
|
|
("CDLTAKURI", CDLTAKURI),
|
|
("CDLTASUKIGAP", CDLTASUKIGAP),
|
|
("CDLTHRUSTING", CDLTHRUSTING),
|
|
("CDLTRISTAR", CDLTRISTAR),
|
|
("CDLUNIQUE3RIVER", CDLUNIQUE3RIVER),
|
|
("CDLUPSIDEGAP2CROWS", CDLUPSIDEGAP2CROWS),
|
|
("CDLXSIDEGAP3METHODS", CDLXSIDEGAP3METHODS),
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Parametrised tests: all CDL patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
|
def test_cdl_output_length(name, fn):
|
|
result = fn(_O, _H, _L, _C)
|
|
assert len(result) == N, f"{name}: expected length {N}, got {len(result)}"
|
|
|
|
|
|
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
|
def test_cdl_values_in_valid_set(name, fn):
|
|
result = fn(_O, _H, _L, _C)
|
|
assert np.all(np.isin(result, [-100, 0, 100])), \
|
|
f"{name}: unexpected values {np.unique(result)}"
|
|
|
|
|
|
@pytest.mark.parametrize("name,fn", ALL_CDL)
|
|
def test_cdl_no_nan(name, fn):
|
|
result = fn(_O, _H, _L, _C)
|
|
assert np.all(np.isfinite(result.astype(float))), f"{name}: contains NaN/Inf"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Specific tests for previously untested patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCDLSPINNINGTOP:
|
|
def test_detects_pattern(self):
|
|
# Spinning top: small body, long upper and lower shadows
|
|
# open ≈ close (small body), high much higher, low much lower
|
|
o = np.array([10.0, 10.1, 10.0])
|
|
h = np.array([15.0, 15.1, 15.0])
|
|
l = np.array([5.0, 5.1, 5.0])
|
|
c = np.array([10.0, 10.0, 10.05])
|
|
result = CDLSPINNINGTOP(o, h, l, c)
|
|
assert np.all(np.isin(result, [-100, 0, 100]))
|
|
|
|
def test_output_values_random(self):
|
|
result = CDLSPINNINGTOP(_O, _H, _L, _C)
|
|
assert np.all(np.isin(result, [-100, 0, 100]))
|
|
|
|
|
|
class TestCDLEVENINGSTAR:
|
|
def test_basic_run(self):
|
|
result = CDLEVENINGSTAR(_O, _H, _L, _C)
|
|
assert len(result) == N
|
|
assert np.all(np.isin(result, [-100, 0, 100]))
|
|
|
|
def test_large_dataset_has_valid_output(self):
|
|
# On 200 bars of random data, result should be all in {-100,0,100}
|
|
result = CDLEVENINGSTAR(_O, _H, _L, _C)
|
|
assert np.all(np.isin(result, [-100, 0, 100]))
|
|
|
|
|
|
class TestCDLMORNINGSTAR:
|
|
def test_basic_run(self):
|
|
result = CDLMORNINGSTAR(_O, _H, _L, _C)
|
|
assert len(result) == N
|
|
assert np.all(np.isin(result, [-100, 0, 100]))
|
|
|
|
def test_bullish_signal_is_100(self):
|
|
# Any detected signal must be 100 (bullish)
|
|
result = CDLMORNINGSTAR(_O, _H, _L, _C)
|
|
assert np.all(result[result != 0] == 100)
|
|
|
|
|
|
class TestCDL2CROWS:
|
|
def test_basic_run(self):
|
|
result = CDL2CROWS(_O, _H, _L, _C)
|
|
assert len(result) == N
|
|
assert np.all(np.isin(result, [-100, 0, 100]))
|
|
|
|
def test_bearish_signal_is_minus_100(self):
|
|
# Any detected signal must be -100 (bearish)
|
|
result = CDL2CROWS(_O, _H, _L, _C)
|
|
assert np.all(result[result != 0] == -100)
|
|
|
|
|
|
class TestCDLDOJI:
|
|
def test_detects_doji(self):
|
|
# Exact doji: open == close
|
|
o = np.array([10.0, 10.0, 10.0])
|
|
h = np.array([12.0, 12.0, 12.0])
|
|
l = np.array([8.0, 8.0, 8.0])
|
|
c = np.array([10.0, 10.0, 10.0])
|
|
result = CDLDOJI(o, h, l, c)
|
|
assert np.all(result == 100)
|
|
|
|
def test_non_doji_returns_zero(self):
|
|
o = np.array([10.0, 11.0, 12.0])
|
|
h = np.array([15.0, 16.0, 17.0])
|
|
l = np.array([9.0, 10.0, 11.0])
|
|
c = np.array([14.0, 15.0, 16.0]) # large body, not doji
|
|
result = CDLDOJI(o, h, l, c)
|
|
assert np.all(result == 0)
|
|
|
|
|
|
class TestCDLMARUBOZU:
|
|
def test_detects_bullish_marubozu(self):
|
|
# Bullish marubozu: open == low, close == high, close > open
|
|
o = np.array([10.0, 10.0])
|
|
h = np.array([15.0, 15.0])
|
|
l = np.array([10.0, 10.0])
|
|
c = np.array([15.0, 15.0])
|
|
result = CDLMARUBOZU(o, h, l, c)
|
|
assert np.all(np.isin(result, [-100, 0, 100]))
|
|
|
|
def test_length(self):
|
|
result = CDLMARUBOZU(_O, _H, _L, _C)
|
|
assert len(result) == N
|