feat: init the repo
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user