release: cut v1.0.0
Prepare the first public 1.0.0 release and finish the remaining CI hardening work. Highlights: - align Python, Rust, WASM, Conda, API, MCP, and docs version metadata to 1.0.0 - promote package metadata to Production/Stable and update stability/versioning docs for the stable series - move the accumulated Unreleased notes into a dated 1.0.0 changelog section and keep a fresh top-level Unreleased block - strengthen the changelog checker so it validates a single top-level Unreleased section - fix the CI/package support mismatch by declaring Python >=3.10 consistently and gating pandas-ta extras to Python 3.12+ - restore Sphinx autodoc compatibility for documented ferro_ta.<module> imports by registering module aliases - make the TA-Lib benchmark guardrail less flaky by checking median and tail-percentile speedups instead of failing on a single mild outlier - switch PyPI publishing to OIDC-only trusted publishing and wire the changelog check into the required CI gate - apply the Ruff-driven cleanup across the Python and test tree and refresh uv/cargo lockfiles Validated locally: - python3 scripts/check_changelog.py - uv run --with ruff ruff check python tests - uv run --with ruff ruff format --check python tests - uv lock --check - sphinx-build -b html docs docs/_build -W --keep-going - build/install the ferro_ta 1.0.0 wheel successfully
This commit is contained in:
@@ -197,9 +197,9 @@ class TestStreamingATR:
|
||||
|
||||
# Streaming
|
||||
streamer = StreamingATR(period=period)
|
||||
stream_out = np.array([
|
||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
||||
])
|
||||
stream_out = np.array(
|
||||
[streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||
)
|
||||
|
||||
# Compare only the overlap region where both arrays are valid
|
||||
mask = np.isfinite(batch_out) & np.isfinite(stream_out)
|
||||
@@ -207,9 +207,9 @@ class TestStreamingATR:
|
||||
"""ATR values should be non-negative."""
|
||||
period = 14
|
||||
streamer = StreamingATR(period=period)
|
||||
stream_out = np.array([
|
||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
||||
])
|
||||
stream_out = np.array(
|
||||
[streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||
)
|
||||
|
||||
# Filter out NaN values
|
||||
valid = stream_out[~np.isnan(stream_out)]
|
||||
@@ -222,15 +222,21 @@ class TestStreamingATR:
|
||||
streamer = StreamingATR(period=period)
|
||||
|
||||
# First pass
|
||||
first_pass = np.array([
|
||||
streamer.update(h, l, c) for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
||||
])
|
||||
first_pass = np.array(
|
||||
[
|
||||
streamer.update(h, l, c)
|
||||
for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
||||
]
|
||||
)
|
||||
|
||||
# Reset and second pass
|
||||
streamer.reset()
|
||||
second_pass = np.array([
|
||||
streamer.update(h, l, c) for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
||||
])
|
||||
second_pass = np.array(
|
||||
[
|
||||
streamer.update(h, l, c)
|
||||
for h, l, c in zip(HIGH[:50], LOW[:50], CLOSE[:50])
|
||||
]
|
||||
)
|
||||
|
||||
assert np.allclose(first_pass, second_pass, equal_nan=True, atol=1e-12)
|
||||
|
||||
@@ -253,7 +259,9 @@ class TestStreamingBBands:
|
||||
verify proximity with atol=0.2 and confirm internal consistency separately.
|
||||
"""
|
||||
# Batch
|
||||
batch_upper, batch_middle, batch_lower = ferro_ta.BBANDS(CLOSE, timeperiod=period)
|
||||
batch_upper, batch_middle, batch_lower = ferro_ta.BBANDS(
|
||||
CLOSE, timeperiod=period
|
||||
)
|
||||
|
||||
# Streaming
|
||||
streamer = StreamingBBands(period=period, nbdevup=2.0, nbdevdn=2.0)
|
||||
@@ -265,8 +273,9 @@ class TestStreamingBBands:
|
||||
# Compare only overlapping valid region
|
||||
mask = np.isfinite(batch_middle)
|
||||
# Middle band (SMA) must match exactly
|
||||
assert np.allclose(stream_middle[mask], batch_middle[mask], atol=1e-10), \
|
||||
assert np.allclose(stream_middle[mask], batch_middle[mask], atol=1e-10), (
|
||||
"BBands middle (SMA) must match batch exactly"
|
||||
)
|
||||
# Upper/lower: streaming uses sample std; batch uses population std — use atol=0.2
|
||||
assert np.allclose(stream_upper[mask], batch_upper[mask], atol=0.2)
|
||||
assert np.allclose(stream_lower[mask], batch_lower[mask], atol=0.2)
|
||||
@@ -285,7 +294,9 @@ class TestStreamingBBands:
|
||||
|
||||
# Compare all three bands
|
||||
for i in range(len(first_pass)):
|
||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
||||
assert np.allclose(
|
||||
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -341,7 +352,9 @@ class TestStreamingMACD:
|
||||
|
||||
# Compare all three outputs
|
||||
for i in range(len(first_pass)):
|
||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
||||
assert np.allclose(
|
||||
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -356,19 +369,12 @@ class TestStreamingStoch:
|
||||
"""Streaming Stochastic should match batch Stochastic."""
|
||||
# Batch
|
||||
batch_slowk, batch_slowd = ferro_ta.STOCH(
|
||||
HIGH, LOW, CLOSE,
|
||||
fastk_period=5, slowk_period=3,
|
||||
slowd_period=3
|
||||
HIGH, LOW, CLOSE, fastk_period=5, slowk_period=3, slowd_period=3
|
||||
)
|
||||
|
||||
# Streaming
|
||||
streamer = StreamingStoch(
|
||||
fastk_period=5, slowk_period=3,
|
||||
slowd_period=3
|
||||
)
|
||||
stream_results = [
|
||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
||||
]
|
||||
streamer = StreamingStoch(fastk_period=5, slowk_period=3, slowd_period=3)
|
||||
stream_results = [streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||
stream_slowk = np.array([r[0] for r in stream_results])
|
||||
stream_slowd = np.array([r[1] for r in stream_results])
|
||||
|
||||
@@ -380,13 +386,8 @@ class TestStreamingStoch:
|
||||
|
||||
def test_stoch_range_zero_to_hundred(self):
|
||||
"""Stochastic values should be in range [0, 100]."""
|
||||
streamer = StreamingStoch(
|
||||
fastk_period=5, slowk_period=3,
|
||||
slowd_period=3
|
||||
)
|
||||
stream_results = [
|
||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
||||
]
|
||||
streamer = StreamingStoch(fastk_period=5, slowk_period=3, slowd_period=3)
|
||||
stream_results = [streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||
stream_slowk = np.array([r[0] for r in stream_results])
|
||||
stream_slowd = np.array([r[1] for r in stream_results])
|
||||
|
||||
@@ -401,10 +402,7 @@ class TestStreamingStoch:
|
||||
|
||||
def test_reset_gives_same_result(self):
|
||||
"""Reset and re-feed should give identical output."""
|
||||
streamer = StreamingStoch(
|
||||
fastk_period=5, slowk_period=3,
|
||||
slowd_period=3
|
||||
)
|
||||
streamer = StreamingStoch(fastk_period=5, slowk_period=3, slowd_period=3)
|
||||
|
||||
# First pass
|
||||
first_pass = [
|
||||
@@ -419,7 +417,9 @@ class TestStreamingStoch:
|
||||
|
||||
# Compare
|
||||
for i in range(len(first_pass)):
|
||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
||||
assert np.allclose(
|
||||
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -437,9 +437,12 @@ class TestStreamingVWAP:
|
||||
|
||||
# Streaming (cumulative)
|
||||
streamer = StreamingVWAP()
|
||||
stream_out = np.array([
|
||||
streamer.update(h, l, c, v) for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
||||
])
|
||||
stream_out = np.array(
|
||||
[
|
||||
streamer.update(h, l, c, v)
|
||||
for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
||||
]
|
||||
)
|
||||
|
||||
# Compare
|
||||
assert np.allclose(stream_out, batch_out, equal_nan=True, atol=1e-10)
|
||||
@@ -451,9 +454,12 @@ class TestStreamingVWAP:
|
||||
|
||||
# Streaming (cumulative)
|
||||
streamer = StreamingVWAP()
|
||||
stream_out = np.array([
|
||||
streamer.update(h, l, c, v) for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
||||
])
|
||||
stream_out = np.array(
|
||||
[
|
||||
streamer.update(h, l, c, v)
|
||||
for h, l, c, v in zip(HIGH, LOW, CLOSE, VOLUME)
|
||||
]
|
||||
)
|
||||
|
||||
# Compare
|
||||
assert np.allclose(stream_out, batch_out, equal_nan=True, atol=1e-10)
|
||||
@@ -463,17 +469,21 @@ class TestStreamingVWAP:
|
||||
streamer = StreamingVWAP()
|
||||
|
||||
# First pass
|
||||
first_pass = np.array([
|
||||
streamer.update(h, l, c, v)
|
||||
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
||||
])
|
||||
first_pass = np.array(
|
||||
[
|
||||
streamer.update(h, l, c, v)
|
||||
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
||||
]
|
||||
)
|
||||
|
||||
# Reset and second pass
|
||||
streamer.reset()
|
||||
second_pass = np.array([
|
||||
streamer.update(h, l, c, v)
|
||||
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
||||
])
|
||||
second_pass = np.array(
|
||||
[
|
||||
streamer.update(h, l, c, v)
|
||||
for h, l, c, v in zip(HIGH[:50], LOW[:50], CLOSE[:50], VOLUME[:50])
|
||||
]
|
||||
)
|
||||
|
||||
assert np.allclose(first_pass, second_pass, equal_nan=True, atol=1e-14)
|
||||
|
||||
@@ -498,9 +508,7 @@ class TestStreamingSupertrend:
|
||||
|
||||
# Streaming
|
||||
streamer = StreamingSupertrend(period=period, multiplier=multiplier)
|
||||
stream_results = [
|
||||
streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)
|
||||
]
|
||||
stream_results = [streamer.update(h, l, c) for h, l, c in zip(HIGH, LOW, CLOSE)]
|
||||
stream_line = np.array([r[0] for r in stream_results])
|
||||
stream_dir = np.array([r[1] for r in stream_results])
|
||||
|
||||
@@ -527,4 +535,6 @@ class TestStreamingSupertrend:
|
||||
|
||||
# Compare
|
||||
for i in range(len(first_pass)):
|
||||
assert np.allclose(first_pass[i], second_pass[i], equal_nan=True, atol=1e-14)
|
||||
assert np.allclose(
|
||||
first_pass[i], second_pass[i], equal_nan=True, atol=1e-14
|
||||
)
|
||||
|
||||
@@ -181,7 +181,9 @@ class TestBBANDSVsPandasTA:
|
||||
pt_upper = pt_bbands[upper_col].to_numpy()
|
||||
|
||||
# Middle band (SMA) must be identical
|
||||
assert _allclose(ft_middle, pt_middle, atol=1e-8), "BBands middle (SMA) must match"
|
||||
assert _allclose(ft_middle, pt_middle, atol=1e-8), (
|
||||
"BBands middle (SMA) must match"
|
||||
)
|
||||
# Upper/lower: differ due to ddof=0 vs ddof=1
|
||||
assert _allclose(ft_upper, pt_upper, atol=0.1)
|
||||
assert _allclose(ft_lower, pt_lower, atol=0.1)
|
||||
@@ -259,18 +261,15 @@ class TestSTOCHVsPandasTA:
|
||||
close = ohlcv_500["close"]
|
||||
|
||||
ft_slowk, ft_slowd = ferro_ta.STOCH(
|
||||
high, low, close,
|
||||
fastk_period=14, slowk_period=3,
|
||||
slowd_period=3
|
||||
high, low, close, fastk_period=14, slowk_period=3, slowd_period=3
|
||||
)
|
||||
|
||||
# pandas-ta returns DataFrame
|
||||
pt_stoch = pandas_ta.stoch(
|
||||
pd.Series(high), pd.Series(low), pd.Series(close),
|
||||
k=14, d=3, smooth_k=3
|
||||
pd.Series(high), pd.Series(low), pd.Series(close), k=14, d=3, smooth_k=3
|
||||
)
|
||||
pt_slowk = pt_stoch[f"STOCHk_14_3_3"].to_numpy()
|
||||
pt_slowd = pt_stoch[f"STOCHd_14_3_3"].to_numpy()
|
||||
pt_slowk = pt_stoch["STOCHk_14_3_3"].to_numpy()
|
||||
pt_slowd = pt_stoch["STOCHd_14_3_3"].to_numpy()
|
||||
|
||||
assert _allclose(ft_slowk, pt_slowk, atol=1e-2, tail_fraction=0.3)
|
||||
assert _allclose(ft_slowd, pt_slowd, atol=1e-2, tail_fraction=0.3)
|
||||
@@ -291,7 +290,9 @@ class TestCCIVsPandasTA:
|
||||
# Compute CCI manually: (TP - SMA(TP)) / (0.015 * MeanAbsDev(TP))
|
||||
tp = (pd.Series(high) + pd.Series(low) + pd.Series(close)) / 3.0
|
||||
mean_tp = tp.rolling(period).mean()
|
||||
mad_tp = tp.rolling(period).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
|
||||
mad_tp = tp.rolling(period).apply(
|
||||
lambda x: np.mean(np.abs(x - x.mean())), raw=True
|
||||
)
|
||||
pt = ((tp - mean_tp) / (0.015 * mad_tp)).to_numpy()
|
||||
|
||||
assert _allclose(ft, pt, atol=1e-8)
|
||||
@@ -469,8 +470,8 @@ class TestVWAPVsPandasTA:
|
||||
n = len(tp)
|
||||
ref = np.full(n, np.nan)
|
||||
for i in range(period - 1, n):
|
||||
w = tp[i - period + 1: i + 1]
|
||||
v = vol[i - period + 1: i + 1]
|
||||
w = tp[i - period + 1 : i + 1]
|
||||
v = vol[i - period + 1 : i + 1]
|
||||
ref[i] = np.dot(w, v) / v.sum()
|
||||
|
||||
assert _allclose(ft, ref, atol=1e-8)
|
||||
@@ -522,7 +523,13 @@ class TestICHIMOKUVsPandasTA:
|
||||
close = ohlcv_500["close"]
|
||||
|
||||
ft_tenkan, ft_kijun, ft_senkou_a, ft_senkou_b, ft_chikou = ferro_ta.ICHIMOKU(
|
||||
high, low, close, tenkan_period=9, kijun_period=26, senkou_b_period=52, displacement=26
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
tenkan_period=9,
|
||||
kijun_period=26,
|
||||
senkou_b_period=52,
|
||||
displacement=26,
|
||||
)
|
||||
|
||||
df = pd.DataFrame({"high": high, "low": low, "close": close})
|
||||
@@ -547,12 +554,19 @@ class TestKELTNER_CHANNELSVsPandasTA:
|
||||
multiplier = 2.0
|
||||
|
||||
ft_upper, ft_middle, ft_lower = ferro_ta.KELTNER_CHANNELS(
|
||||
high, low, close, timeperiod=period, atr_period=atr_period, multiplier=multiplier
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
timeperiod=period,
|
||||
atr_period=atr_period,
|
||||
multiplier=multiplier,
|
||||
)
|
||||
|
||||
# Compute manually using pandas_ta EMA and ATR to match ferro_ta's exact formula
|
||||
pt_ema = pandas_ta.ema(pd.Series(close), length=period).to_numpy()
|
||||
pt_atr = pandas_ta.atr(pd.Series(high), pd.Series(low), pd.Series(close), length=atr_period).to_numpy()
|
||||
pt_atr = pandas_ta.atr(
|
||||
pd.Series(high), pd.Series(low), pd.Series(close), length=atr_period
|
||||
).to_numpy()
|
||||
pt_upper = pt_ema + multiplier * pt_atr
|
||||
pt_middle = pt_ema
|
||||
pt_lower = pt_ema - multiplier * pt_atr
|
||||
@@ -643,7 +657,9 @@ class TestCHANDELIER_EXITVsPandasTA:
|
||||
)
|
||||
|
||||
# Compute manually: long = rolling_max(H, n) - mult*ATR; short = rolling_min(L, n) + mult*ATR
|
||||
pt_atr = pandas_ta.atr(pd.Series(high), pd.Series(low), pd.Series(close), length=period).to_numpy()
|
||||
pt_atr = pandas_ta.atr(
|
||||
pd.Series(high), pd.Series(low), pd.Series(close), length=period
|
||||
).to_numpy()
|
||||
rolling_high = pd.Series(high).rolling(period).max().to_numpy()
|
||||
rolling_low = pd.Series(low).rolling(period).min().to_numpy()
|
||||
pt_long = rolling_high - multiplier * pt_atr
|
||||
|
||||
@@ -190,9 +190,7 @@ class TestSTOCHVsTA:
|
||||
close = ohlcv_500["close"]
|
||||
|
||||
ft_slowk, ft_slowd = ferro_ta.STOCH(
|
||||
high, low, close,
|
||||
fastk_period=14, slowk_period=3,
|
||||
slowd_period=3
|
||||
high, low, close, fastk_period=14, slowk_period=3, slowd_period=3
|
||||
)
|
||||
|
||||
# Values in valid region must be within [0, 100]
|
||||
@@ -200,16 +198,21 @@ class TestSTOCHVsTA:
|
||||
valid_d = ft_slowd[np.isfinite(ft_slowd)]
|
||||
assert len(valid_k) > 0, "STOCH slowk should have valid values"
|
||||
assert len(valid_d) > 0, "STOCH slowd should have valid values"
|
||||
assert np.all(valid_k >= 0.0) and np.all(valid_k <= 100.0), \
|
||||
assert np.all(valid_k >= 0.0) and np.all(valid_k <= 100.0), (
|
||||
"STOCH slowk must be in [0, 100]"
|
||||
assert np.all(valid_d >= 0.0) and np.all(valid_d <= 100.0), \
|
||||
)
|
||||
assert np.all(valid_d >= 0.0) and np.all(valid_d <= 100.0), (
|
||||
"STOCH slowd must be in [0, 100]"
|
||||
)
|
||||
|
||||
# Warm-up: TA-Lib STOCH NaN count = fastk_period + slowk_period - 1
|
||||
expected_nan = 14 + 3 + 1 - 1 # = fastk_period + slowk_period (TA-Lib convention)
|
||||
expected_nan = (
|
||||
14 + 3 + 1 - 1
|
||||
) # = fastk_period + slowk_period (TA-Lib convention)
|
||||
actual_nan_k = int(np.sum(np.isnan(ft_slowk)))
|
||||
assert actual_nan_k == expected_nan, \
|
||||
assert actual_nan_k == expected_nan, (
|
||||
f"STOCH slowk NaN warmup: expected {expected_nan}, got {actual_nan_k}"
|
||||
)
|
||||
|
||||
|
||||
class TestWILLRVsTA:
|
||||
|
||||
@@ -71,11 +71,11 @@ SIGN_AGREEMENT_THRESHOLD = 0.8
|
||||
# use lower thresholds with a documented reason.
|
||||
CDL_AGREEMENT_THRESHOLDS: dict[str, float] = {
|
||||
# Body/shadow ratio thresholds differ between ferro_ta and TA-Lib
|
||||
"CDLHIGHWAVE": 0.65, # Shadow length threshold differs; 69% observed
|
||||
"CDLHIGHWAVE": 0.65, # Shadow length threshold differs; 69% observed
|
||||
"CDLLONGLEGGEDDOJI": 0.70, # Long-leg threshold differs; 75% observed
|
||||
"CDLSHORTLINE": 0.20, # Body-size cutoff definition completely differs; 25% observed
|
||||
"CDLSPINNINGTOP": 0.75, # Body ratio threshold differs; 78% observed
|
||||
"CDLDOJI": 0.85, # Shadow ratio precision differs; 86% observed
|
||||
"CDLSHORTLINE": 0.20, # Body-size cutoff definition completely differs; 25% observed
|
||||
"CDLSPINNINGTOP": 0.75, # Body ratio threshold differs; 78% observed
|
||||
"CDLDOJI": 0.85, # Shadow ratio precision differs; 86% observed
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,9 @@ class TestEMA:
|
||||
ta = talib.EMA(CLOSE, timeperiod=5)
|
||||
# With 500 bars, compare last 30% with tighter tolerance
|
||||
tail_start = int(N * 0.7)
|
||||
assert np.allclose(ft[tail_start:], ta[tail_start:], atol=1e-5) # Tightened from 1e-3
|
||||
assert np.allclose(
|
||||
ft[tail_start:], ta[tail_start:], atol=1e-5
|
||||
) # Tightened from 1e-3
|
||||
|
||||
def test_values_finite_and_reasonable(self):
|
||||
ft = ferro_ta.EMA(CLOSE, timeperiod=5)
|
||||
@@ -266,7 +268,9 @@ class TestT3:
|
||||
ta = talib.T3(CLOSE, timeperiod=5)
|
||||
# With 500 bars, use last 30% with tighter tolerance
|
||||
tail_start = int(N * 0.7)
|
||||
assert np.allclose(ft[tail_start:], ta[tail_start:], atol=1e-3) # Tightened from 5e-2
|
||||
assert np.allclose(
|
||||
ft[tail_start:], ta[tail_start:], atol=1e-3
|
||||
) # Tightened from 5e-2
|
||||
|
||||
|
||||
class TestBBANDS:
|
||||
@@ -781,7 +785,9 @@ class TestSTOCHRSI:
|
||||
assert abs(_nan_count(ft_k) - _nan_count(ta_k)) <= 2
|
||||
|
||||
def test_range_0_to_100(self):
|
||||
ft_k, _ = ferro_ta.STOCHRSI(CLOSE, timeperiod=14, fastk_period=5, fastd_period=3)
|
||||
ft_k, _ = ferro_ta.STOCHRSI(
|
||||
CLOSE, timeperiod=14, fastk_period=5, fastd_period=3
|
||||
)
|
||||
finite = ft_k[~np.isnan(ft_k)]
|
||||
# Allow small numerical tolerance for float boundaries
|
||||
assert all(-1e-9 <= v <= 100.0 + 1e-9 for v in finite)
|
||||
@@ -834,6 +840,7 @@ class TestPPO:
|
||||
mask = _valid_mask(ppo, ta)
|
||||
corr = np.corrcoef(ppo[mask], ta[mask])[0, 1]
|
||||
assert corr > 0.85
|
||||
|
||||
"""CMO — same NaN count and shape; values may differ slightly.
|
||||
|
||||
Both libraries compute the Chande Momentum Oscillator as
|
||||
@@ -2034,9 +2041,7 @@ class TestHTTrendMode:
|
||||
mask = _valid_mask(ft, ta)
|
||||
if mask.sum() >= 5:
|
||||
agree = np.mean(ft[mask] == ta[mask])
|
||||
assert agree >= 0.50, (
|
||||
f"HT_TRENDMODE agreement {agree:.2f} < 0.50"
|
||||
)
|
||||
assert agree >= 0.50, f"HT_TRENDMODE agreement {agree:.2f} < 0.50"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2046,24 +2051,66 @@ class TestHTTrendMode:
|
||||
|
||||
# List of all candlestick patterns to test
|
||||
ALL_CDL_PATTERNS = [
|
||||
"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",
|
||||
"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",
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user