261 lines
8.1 KiB
Python
261 lines
8.1 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
|