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:
Pratik Bhadane
2026-03-23 23:57:30 +05:30
parent 7a5a220dfe
commit 307beeca02
47 changed files with 1822 additions and 573 deletions
+15 -3
View File
@@ -1,8 +1,14 @@
"""Unit tests for ferro_ta.indicators.cycle"""
import numpy as np
import pytest
from ferro_ta.indicators.cycle import (
HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDLINE, HT_TRENDMODE,
HT_DCPERIOD,
HT_DCPHASE,
HT_PHASOR,
HT_SINE,
HT_TRENDLINE,
HT_TRENDMODE,
)
# ---------------------------------------------------------------------------
@@ -11,7 +17,7 @@ from ferro_ta.indicators.cycle import (
N = 200
t = np.linspace(0, 10 * np.pi, N)
SINE_CLOSE = 100 + 10 * np.sin(t) # clean sine wave
SINE_CLOSE = 100 + 10 * np.sin(t) # clean sine wave
def _warmup_end(arr):
@@ -24,6 +30,7 @@ def _warmup_end(arr):
# HT_DCPERIOD
# ---------------------------------------------------------------------------
class TestHT_DCPERIOD:
def test_length(self):
result = HT_DCPERIOD(SINE_CLOSE)
@@ -53,6 +60,7 @@ class TestHT_DCPERIOD:
# HT_DCPHASE
# ---------------------------------------------------------------------------
class TestHT_DCPHASE:
def test_length(self):
assert len(HT_DCPHASE(SINE_CLOSE)) == N
@@ -72,6 +80,7 @@ class TestHT_DCPHASE:
# HT_PHASOR
# ---------------------------------------------------------------------------
class TestHT_PHASOR:
def test_returns_two_arrays(self):
result = HT_PHASOR(SINE_CLOSE)
@@ -98,6 +107,7 @@ class TestHT_PHASOR:
# HT_SINE
# ---------------------------------------------------------------------------
class TestHT_SINE:
def test_returns_two_arrays(self):
result = HT_SINE(SINE_CLOSE)
@@ -130,6 +140,7 @@ class TestHT_SINE:
# HT_TRENDLINE
# ---------------------------------------------------------------------------
class TestHT_TRENDLINE:
def test_length(self):
assert len(HT_TRENDLINE(SINE_CLOSE)) == N
@@ -157,6 +168,7 @@ class TestHT_TRENDLINE:
# HT_TRENDMODE
# ---------------------------------------------------------------------------
class TestHT_TRENDMODE:
def test_length(self):
assert len(HT_TRENDMODE(SINE_CLOSE)) == N
+27 -5
View File
@@ -1,9 +1,18 @@
"""Unit tests for ferro_ta.indicators.extended"""
import numpy as np
import pytest
from ferro_ta.indicators.extended import (
VWAP, SUPERTREND, ICHIMOKU, DONCHIAN, PIVOT_POINTS,
KELTNER_CHANNELS, HULL_MA, CHANDELIER_EXIT, VWMA, CHOPPINESS_INDEX,
CHANDELIER_EXIT,
CHOPPINESS_INDEX,
DONCHIAN,
HULL_MA,
ICHIMOKU,
KELTNER_CHANNELS,
PIVOT_POINTS,
SUPERTREND,
VWAP,
VWMA,
)
# ---------------------------------------------------------------------------
@@ -23,6 +32,7 @@ _VOL = RNG.uniform(1000, 5000, N)
# VWAP
# ---------------------------------------------------------------------------
class TestVWAP:
def test_length(self):
result = VWAP(_H, _L, _C, _VOL)
@@ -46,6 +56,7 @@ class TestVWAP:
# SUPERTREND
# ---------------------------------------------------------------------------
class TestSUPERTREND:
def test_returns_two_arrays(self):
result = SUPERTREND(_H, _L, _C)
@@ -69,6 +80,7 @@ class TestSUPERTREND:
# ICHIMOKU
# ---------------------------------------------------------------------------
class TestICHIMOKU:
def test_returns_five_arrays(self):
result = ICHIMOKU(_H, _L, _C)
@@ -80,7 +92,9 @@ class TestICHIMOKU:
assert len(arr) == N
def test_tenkan_warmup(self):
tenkan, kijun, senkou_a, senkou_b, chikou = ICHIMOKU(_H, _L, _C, tenkan_period=9)
tenkan, kijun, senkou_a, senkou_b, chikou = ICHIMOKU(
_H, _L, _C, tenkan_period=9
)
assert np.all(np.isnan(tenkan[:8]))
def test_finite_after_warmup(self):
@@ -94,6 +108,7 @@ class TestICHIMOKU:
# DONCHIAN
# ---------------------------------------------------------------------------
class TestDONCHIAN:
def test_returns_three_arrays(self):
result = DONCHIAN(_H, _L)
@@ -126,6 +141,7 @@ class TestDONCHIAN:
# PIVOT_POINTS
# ---------------------------------------------------------------------------
class TestPIVOT_POINTS:
def test_returns_five_arrays(self):
result = PIVOT_POINTS(_H, _L, _C)
@@ -138,7 +154,7 @@ class TestPIVOT_POINTS:
def test_classic_pivot_formula(self):
# PP = (H + L + C) / 3
pp, r1, s1, r2, s2 = PIVOT_POINTS(_H, _L, _C, method='classic')
pp, r1, s1, r2, s2 = PIVOT_POINTS(_H, _L, _C, method="classic")
valid = ~np.isnan(pp)
expected_pp = (_H[:-1] + _L[:-1] + _C[:-1]) / 3.0
np.testing.assert_allclose(pp[valid], expected_pp[valid[1:]], rtol=1e-6)
@@ -152,6 +168,7 @@ class TestPIVOT_POINTS:
# KELTNER_CHANNELS
# ---------------------------------------------------------------------------
class TestKELTNER_CHANNELS:
def test_returns_three_arrays(self):
result = KELTNER_CHANNELS(_H, _L, _C)
@@ -175,6 +192,7 @@ class TestKELTNER_CHANNELS:
# HULL_MA
# ---------------------------------------------------------------------------
class TestHULL_MA:
def test_length(self):
assert len(HULL_MA(_C, timeperiod=16)) == N
@@ -199,6 +217,7 @@ class TestHULL_MA:
# CHANDELIER_EXIT
# ---------------------------------------------------------------------------
class TestCHANDELIER_EXIT:
def test_returns_two_arrays(self):
result = CHANDELIER_EXIT(_H, _L, _C)
@@ -223,6 +242,7 @@ class TestCHANDELIER_EXIT:
# VWMA
# ---------------------------------------------------------------------------
class TestVWMA:
def test_length(self):
assert len(VWMA(_C, _VOL, timeperiod=20)) == N
@@ -241,6 +261,7 @@ class TestVWMA:
vol = np.ones(N) * 1000.0
vwma = VWMA(_C, vol, timeperiod=20)
from ferro_ta.indicators.overlap import SMA
sma = SMA(_C, timeperiod=20)
valid = ~np.isnan(vwma) & ~np.isnan(sma)
np.testing.assert_allclose(vwma[valid], sma[valid], rtol=1e-8)
@@ -250,6 +271,7 @@ class TestVWMA:
# CHOPPINESS_INDEX
# ---------------------------------------------------------------------------
class TestCHOPPINESS_INDEX:
def test_length(self):
assert len(CHOPPINESS_INDEX(_H, _L, _C, timeperiod=14)) == N
+39 -6
View File
@@ -1,10 +1,32 @@
"""Unit tests for ferro_ta.indicators.math_ops"""
import numpy as np
import pytest
from ferro_ta.indicators.math_ops import (
ADD, SUB, MULT, DIV, SUM, MAX, MIN, MAXINDEX, MININDEX,
ACOS, ASIN, ATAN, CEIL, COS, COSH, EXP, FLOOR,
LN, LOG10, SIN, SINH, SQRT, TAN, TANH,
ACOS,
ADD,
ASIN,
ATAN,
CEIL,
COS,
COSH,
DIV,
EXP,
FLOOR,
LN,
LOG10,
MAX,
MAXINDEX,
MIN,
MININDEX,
MULT,
SIN,
SINH,
SQRT,
SUB,
SUM,
TAN,
TANH,
)
# ---------------------------------------------------------------------------
@@ -14,17 +36,18 @@ from ferro_ta.indicators.math_ops import (
A3 = np.array([1.0, 2.0, 3.0])
B3 = np.array([4.0, 5.0, 6.0])
TRIG = np.array([0.0, np.pi / 6, np.pi / 4, np.pi / 3, np.pi / 2])
UNIT = np.array([0.0, 0.25, 0.5, 0.75, 1.0]) # values in [0,1] for ASIN/ACOS
UNIT = np.array([0.0, 0.25, 0.5, 0.75, 1.0]) # values in [0,1] for ASIN/ACOS
RNG = np.random.default_rng(17)
N = 100
_ARR = 1.0 + RNG.random(N) * 9.0 # positive values in (1, 10]
_ARR = 1.0 + RNG.random(N) * 9.0 # positive values in (1, 10]
# ---------------------------------------------------------------------------
# ADD
# ---------------------------------------------------------------------------
class TestADD:
def test_known_values(self):
result = ADD(A3, B3)
@@ -41,6 +64,7 @@ class TestADD:
# SUB
# ---------------------------------------------------------------------------
class TestSUB:
def test_known_values(self):
result = SUB(B3, A3)
@@ -54,6 +78,7 @@ class TestSUB:
# MULT
# ---------------------------------------------------------------------------
class TestMULT:
def test_known_values(self):
result = MULT(A3, B3)
@@ -70,6 +95,7 @@ class TestMULT:
# DIV
# ---------------------------------------------------------------------------
class TestDIV:
def test_known_values(self):
result = DIV(B3, A3)
@@ -86,6 +112,7 @@ class TestDIV:
# SUM
# ---------------------------------------------------------------------------
class TestSUM:
def test_known_values(self):
arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
@@ -106,6 +133,7 @@ class TestSUM:
# MAX
# ---------------------------------------------------------------------------
class TestMAX:
def test_known_values(self):
arr = np.array([1.0, 3.0, 2.0, 5.0, 4.0])
@@ -127,6 +155,7 @@ class TestMAX:
# MIN
# ---------------------------------------------------------------------------
class TestMIN:
def test_known_values(self):
arr = np.array([5.0, 3.0, 4.0, 1.0, 2.0])
@@ -143,6 +172,7 @@ class TestMIN:
# MAXINDEX
# ---------------------------------------------------------------------------
class TestMAXINDEX:
def test_known_values(self):
arr = np.array([1.0, 5.0, 3.0, 2.0, 4.0])
@@ -162,6 +192,7 @@ class TestMAXINDEX:
# MININDEX
# ---------------------------------------------------------------------------
class TestMININDEX:
def test_known_values(self):
arr = np.array([5.0, 1.0, 3.0, 2.0, 4.0])
@@ -181,6 +212,7 @@ class TestMININDEX:
# Trig functions
# ---------------------------------------------------------------------------
class TestSIN:
def test_known_values(self):
angles = np.array([0.0, np.pi / 2, np.pi])
@@ -236,6 +268,7 @@ class TestTANH:
# Rounding/exponential
# ---------------------------------------------------------------------------
class TestCEIL:
def test_known_values(self):
arr = np.array([1.1, 2.5, 3.9, -0.5])
+54 -6
View File
@@ -1,12 +1,35 @@
"""Unit tests for ferro_ta.indicators.momentum"""
import numpy as np
import pytest
from ferro_ta.indicators.momentum import (
RSI, STOCH, STOCHF, STOCHRSI,
ADX, ADXR, CCI, WILLR, AROON, AROONOSC,
MFI, MOM, ROC, ROCP, ROCR, ROCR100,
CMO, DX, MINUS_DI, MINUS_DM, PLUS_DI, PLUS_DM,
PPO, APO, TRIX, ULTOSC, BOP,
ADX,
ADXR,
APO,
AROON,
AROONOSC,
BOP,
CCI,
CMO,
DX,
MFI,
MINUS_DI,
MINUS_DM,
MOM,
PLUS_DI,
PLUS_DM,
PPO,
ROC,
ROCP,
ROCR,
ROCR100,
RSI,
STOCH,
STOCHF,
STOCHRSI,
TRIX,
ULTOSC,
WILLR,
)
# ---------------------------------------------------------------------------
@@ -32,6 +55,7 @@ SMALL5_V = np.array([1000.0, 2000.0, 3000.0, 4000.0, 5000.0])
# RSI
# ---------------------------------------------------------------------------
class TestRSI:
def test_nan_warmup(self):
result = RSI(_CLOSE, timeperiod=14)
@@ -50,6 +74,7 @@ class TestRSI:
# STOCH
# ---------------------------------------------------------------------------
class TestSTOCH:
def test_returns_two_arrays(self):
result = STOCH(_HIGH, _LOW, _CLOSE)
@@ -70,6 +95,7 @@ class TestSTOCH:
# STOCHF
# ---------------------------------------------------------------------------
class TestSTOCHF:
def test_returns_two_arrays(self):
result = STOCHF(_HIGH, _LOW, _CLOSE)
@@ -99,6 +125,7 @@ class TestSTOCHF:
# STOCHRSI
# ---------------------------------------------------------------------------
class TestSTOCHRSI:
def test_returns_two_arrays(self):
result = STOCHRSI(_CLOSE)
@@ -119,6 +146,7 @@ class TestSTOCHRSI:
# ADX
# ---------------------------------------------------------------------------
class TestADX:
def test_nan_warmup(self):
result = ADX(_HIGH, _LOW, _CLOSE, timeperiod=14)
@@ -137,6 +165,7 @@ class TestADX:
# ADXR
# ---------------------------------------------------------------------------
class TestADXR:
def test_length(self):
assert len(ADXR(_HIGH, _LOW, _CLOSE, 14)) == N
@@ -151,6 +180,7 @@ class TestADXR:
# CCI
# ---------------------------------------------------------------------------
class TestCCI:
def test_known_constant_mean_dev(self):
# Constant typical price → CCI = 0 after warmup
@@ -181,6 +211,7 @@ class TestCCI:
# WILLR
# ---------------------------------------------------------------------------
class TestWILLR:
def test_range(self):
result = WILLR(_HIGH, _LOW, _CLOSE, 14)
@@ -195,6 +226,7 @@ class TestWILLR:
# AROON
# ---------------------------------------------------------------------------
class TestAROON:
def test_returns_two_arrays(self):
result = AROON(_HIGH, _LOW, 14)
@@ -215,6 +247,7 @@ class TestAROON:
# AROONOSC
# ---------------------------------------------------------------------------
class TestAROONOSC:
def test_known_values(self):
h = np.array([12.0, 13.0, 14.0, 15.0, 16.0])
@@ -242,6 +275,7 @@ class TestAROONOSC:
# MFI
# ---------------------------------------------------------------------------
class TestMFI:
def test_range(self):
result = MFI(_HIGH, _LOW, _CLOSE, _VOL, 14)
@@ -271,6 +305,7 @@ class TestMFI:
# MOM
# ---------------------------------------------------------------------------
class TestMOM:
def test_known_values(self):
result = MOM(SMALL5, timeperiod=2)
@@ -286,6 +321,7 @@ class TestMOM:
# ROC
# ---------------------------------------------------------------------------
class TestROC:
def test_known_values(self):
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
@@ -301,6 +337,7 @@ class TestROC:
# ROCP
# ---------------------------------------------------------------------------
class TestROCP:
def test_known_values(self):
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
@@ -316,6 +353,7 @@ class TestROCP:
# ROCR
# ---------------------------------------------------------------------------
class TestROCR:
def test_known_values(self):
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
@@ -336,6 +374,7 @@ class TestROCR:
# ROCR100
# ---------------------------------------------------------------------------
class TestROCR100:
def test_known_values(self):
arr = np.array([10.0, 11.0, 12.0, 13.0, 14.0])
@@ -357,6 +396,7 @@ class TestROCR100:
# CMO
# ---------------------------------------------------------------------------
class TestCMO:
def test_range(self):
result = CMO(_CLOSE, 14)
@@ -371,6 +411,7 @@ class TestCMO:
# DX
# ---------------------------------------------------------------------------
class TestDX:
def test_range(self):
result = DX(_HIGH, _LOW, _CLOSE, 14)
@@ -385,6 +426,7 @@ class TestDX:
# MINUS_DI / MINUS_DM
# ---------------------------------------------------------------------------
class TestMINUS:
def test_minus_di_range(self):
result = MINUS_DI(_HIGH, _LOW, _CLOSE, 14)
@@ -405,6 +447,7 @@ class TestMINUS:
# PLUS_DI / PLUS_DM
# ---------------------------------------------------------------------------
class TestPLUS:
def test_plus_di_range(self):
result = PLUS_DI(_HIGH, _LOW, _CLOSE, 14)
@@ -425,6 +468,7 @@ class TestPLUS:
# PPO
# ---------------------------------------------------------------------------
class TestPPO:
def test_returns_three_arrays(self):
result = PPO(_CLOSE, fastperiod=12, slowperiod=26)
@@ -448,6 +492,7 @@ class TestPPO:
# APO
# ---------------------------------------------------------------------------
class TestAPO:
def test_known_direction(self):
# Rising close → fast EMA > slow EMA → APO > 0 after warmup
@@ -468,6 +513,7 @@ class TestAPO:
# TRIX
# ---------------------------------------------------------------------------
class TestTRIX:
def test_length(self):
assert len(TRIX(_CLOSE, 10)) == N
@@ -494,6 +540,7 @@ class TestTRIX:
# BOP
# ---------------------------------------------------------------------------
class TestBOP:
def test_known_values(self):
o = np.array([10.0, 11.0])
@@ -526,6 +573,7 @@ class TestBOP:
# ULTOSC
# ---------------------------------------------------------------------------
class TestULTOSC:
def test_range(self):
result = ULTOSC(_HIGH, _LOW, _CLOSE, 7, 14, 28)
+43 -7
View File
@@ -1,10 +1,27 @@
"""Unit tests for ferro_ta.indicators.overlap"""
import numpy as np
import pytest
from ferro_ta.indicators.overlap import (
SMA, EMA, WMA, DEMA, TEMA, TRIMA, KAMA, T3, MA,
MACD, MACDFIX, MACDEXT, BBANDS, SAR, SAREXT,
MAMA, MAVP, MIDPOINT, MIDPRICE,
BBANDS,
DEMA,
EMA,
KAMA,
MA,
MACD,
MACDEXT,
MACDFIX,
MAMA,
MAVP,
MIDPOINT,
MIDPRICE,
SAR,
SAREXT,
SMA,
T3,
TEMA,
TRIMA,
WMA,
)
# ---------------------------------------------------------------------------
@@ -26,6 +43,7 @@ SMALL5_LOW = np.array([9.0, 10.0, 11.0, 12.0, 13.0])
# SMA
# ---------------------------------------------------------------------------
class TestSMA:
def test_known_values(self):
result = SMA(SMALL5, timeperiod=3)
@@ -50,6 +68,7 @@ class TestSMA:
# EMA
# ---------------------------------------------------------------------------
class TestEMA:
def test_known_values(self):
# k = 2/(3+1) = 0.5; seed = SMA(3) = 11.0
@@ -80,13 +99,14 @@ class TestEMA:
# WMA
# ---------------------------------------------------------------------------
class TestWMA:
def test_known_values(self):
arr = np.arange(1.0, 6.0)
result = WMA(arr, timeperiod=3)
# weights 1,2,3 / 6
expected_2 = (1*1 + 2*2 + 3*3) / 6.0 # 14/6
expected_3 = (1*2 + 2*3 + 3*4) / 6.0 # 20/6
expected_2 = (1 * 1 + 2 * 2 + 3 * 3) / 6.0 # 14/6
expected_3 = (1 * 2 + 2 * 3 + 3 * 4) / 6.0 # 20/6
assert np.isnan(result[0]) and np.isnan(result[1])
np.testing.assert_allclose(result[2], expected_2, rtol=1e-10)
np.testing.assert_allclose(result[3], expected_3, rtol=1e-10)
@@ -103,10 +123,11 @@ class TestWMA:
# DEMA
# ---------------------------------------------------------------------------
class TestDEMA:
def test_nan_warmup(self):
result = DEMA(_CLOSE, timeperiod=5)
assert np.all(np.isnan(result[:8])) # DEMA needs 2*(tp-1) bars
assert np.all(np.isnan(result[:8])) # DEMA needs 2*(tp-1) bars
def test_length(self):
assert len(DEMA(_CLOSE, 5)) == N
@@ -131,6 +152,7 @@ class TestDEMA:
# TEMA
# ---------------------------------------------------------------------------
class TestTEMA:
def test_nan_warmup(self):
result = TEMA(_CLOSE, timeperiod=5)
@@ -150,6 +172,7 @@ class TestTEMA:
# TRIMA
# ---------------------------------------------------------------------------
class TestTRIMA:
def test_known_values(self):
arr = np.arange(1.0, 11.0)
@@ -171,6 +194,7 @@ class TestTRIMA:
# KAMA
# ---------------------------------------------------------------------------
class TestKAMA:
def test_nan_warmup(self):
result = KAMA(_CLOSE, timeperiod=10)
@@ -195,6 +219,7 @@ class TestKAMA:
# T3
# ---------------------------------------------------------------------------
class TestT3:
def test_nan_warmup(self):
arr = np.linspace(10.0, 30.0, 100)
@@ -223,6 +248,7 @@ class TestT3:
# MA
# ---------------------------------------------------------------------------
class TestMA:
def test_default_is_sma(self):
result_ma = MA(_CLOSE, timeperiod=10, matype=0)
@@ -242,6 +268,7 @@ class TestMA:
# MACD
# ---------------------------------------------------------------------------
class TestMACD:
def test_returns_three_arrays(self):
result = MACD(_CLOSE, 12, 26, 9)
@@ -266,6 +293,7 @@ class TestMACD:
# MACDFIX
# ---------------------------------------------------------------------------
class TestMACDFIX:
def test_returns_three_arrays(self):
result = MACDFIX(_CLOSE)
@@ -285,6 +313,7 @@ class TestMACDFIX:
# MACDEXT
# ---------------------------------------------------------------------------
class TestMACDEXT:
def test_returns_three_arrays(self):
result = MACDEXT(_CLOSE)
@@ -303,6 +332,7 @@ class TestMACDEXT:
# BBANDS
# ---------------------------------------------------------------------------
class TestBBANDS:
def test_returns_three_arrays(self):
result = BBANDS(_CLOSE, 20)
@@ -331,6 +361,7 @@ class TestBBANDS:
# SAR
# ---------------------------------------------------------------------------
class TestSAR:
def test_length(self):
result = SAR(_HIGH, _LOW)
@@ -349,6 +380,7 @@ class TestSAR:
# SAREXT
# ---------------------------------------------------------------------------
class TestSAREXT:
def test_length(self):
result = SAREXT(_HIGH, _LOW)
@@ -367,6 +399,7 @@ class TestSAREXT:
# MAMA
# ---------------------------------------------------------------------------
class TestMAMA:
def test_returns_two_arrays(self):
result = MAMA(_CLOSE)
@@ -393,6 +426,7 @@ class TestMAMA:
# MAVP
# ---------------------------------------------------------------------------
class TestMAVP:
def test_length(self):
arr = np.linspace(10.0, 30.0, 50)
@@ -412,6 +446,7 @@ class TestMAVP:
# MIDPOINT
# ---------------------------------------------------------------------------
class TestMIDPOINT:
def test_known_values(self):
arr = np.array([10.0, 12.0, 14.0, 16.0, 18.0])
@@ -433,6 +468,7 @@ class TestMIDPOINT:
# MIDPRICE
# ---------------------------------------------------------------------------
class TestMIDPRICE:
def test_known_values(self):
result = MIDPRICE(SMALL5_HIGH, SMALL5_LOW, timeperiod=3)
+67 -14
View File
@@ -1,20 +1,70 @@
"""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,
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,
)
# ---------------------------------------------------------------------------
@@ -98,6 +148,7 @@ ALL_CDL = [
# 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)
@@ -107,8 +158,9 @@ def test_cdl_output_length(name, fn):
@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])), \
assert np.all(np.isin(result, [-100, 0, 100])), (
f"{name}: unexpected values {np.unique(result)}"
)
@pytest.mark.parametrize("name,fn", ALL_CDL)
@@ -121,6 +173,7 @@ def test_cdl_no_nan(name, fn):
# Specific tests for previously untested patterns
# ---------------------------------------------------------------------------
class TestCDLSPINNINGTOP:
def test_detects_pattern(self):
# Spinning top: small body, long upper and lower shadows
@@ -1,6 +1,7 @@
"""Unit tests for ferro_ta.indicators.price_transform"""
import numpy as np
import pytest
from ferro_ta.indicators.price_transform import AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE
# ---------------------------------------------------------------------------
@@ -17,6 +18,7 @@ C = np.array([11.0, 12.0, 13.0, 14.0])
# AVGPRICE
# ---------------------------------------------------------------------------
class TestAVGPRICE:
def test_known_formula(self):
result = AVGPRICE(O, H, L, C)
@@ -39,6 +41,7 @@ class TestAVGPRICE:
# MEDPRICE
# ---------------------------------------------------------------------------
class TestMEDPRICE:
def test_known_formula(self):
result = MEDPRICE(H, L)
@@ -61,6 +64,7 @@ class TestMEDPRICE:
# TYPPRICE
# ---------------------------------------------------------------------------
class TestTYPPRICE:
def test_known_formula(self):
result = TYPPRICE(H, L, C)
@@ -83,6 +87,7 @@ class TestTYPPRICE:
# WCLPRICE
# ---------------------------------------------------------------------------
class TestWCLPRICE:
def test_known_formula(self):
result = WCLPRICE(H, L, C)
@@ -99,7 +104,6 @@ class TestWCLPRICE:
def test_close_weight_double(self):
# WCLPRICE weights close twice vs TYPPRICE
typ = TYPPRICE(H, L, C)
wcl = WCLPRICE(H, L, C)
# On a rising series (H > L > 0), WCLPRICE > TYPPRICE when C > (H+L)/2
# Just verify formula correctness already done above
+21 -5
View File
@@ -1,10 +1,17 @@
"""Unit tests for ferro_ta.indicators.statistic"""
import numpy as np
import pytest
from ferro_ta.indicators.statistic import (
STDDEV, VAR, BETA, CORREL,
LINEARREG, LINEARREG_ANGLE, LINEARREG_INTERCEPT, LINEARREG_SLOPE,
BETA,
CORREL,
LINEARREG,
LINEARREG_ANGLE,
LINEARREG_INTERCEPT,
LINEARREG_SLOPE,
STDDEV,
TSF,
VAR,
)
# ---------------------------------------------------------------------------
@@ -16,14 +23,15 @@ N = 100
_A = 100 + np.cumsum(RNG.normal(0, 0.5, N))
_B = 100 + np.cumsum(RNG.normal(0, 0.5, N))
LINDATA = np.arange(1.0, 6.0) # [1,2,3,4,5]
CONSTDATA = np.ones(10) # all 1.0
LINDATA = np.arange(1.0, 6.0) # [1,2,3,4,5]
CONSTDATA = np.ones(10) # all 1.0
# ---------------------------------------------------------------------------
# STDDEV
# ---------------------------------------------------------------------------
class TestSTDDEV:
def test_constant_is_zero(self):
result = STDDEV(CONSTDATA, timeperiod=5)
@@ -52,6 +60,7 @@ class TestSTDDEV:
# VAR
# ---------------------------------------------------------------------------
class TestVAR:
def test_constant_is_zero(self):
result = VAR(CONSTDATA, timeperiod=5)
@@ -77,6 +86,7 @@ class TestVAR:
# LINEARREG
# ---------------------------------------------------------------------------
class TestLINEARREG:
def test_perfect_line(self):
# For [1,2,3,4,5] over window 5, forecast = 5.0
@@ -95,6 +105,7 @@ class TestLINEARREG:
# LINEARREG_SLOPE
# ---------------------------------------------------------------------------
class TestLINEARREG_SLOPE:
def test_perfect_line_slope_one(self):
result = LINEARREG_SLOPE(LINDATA, timeperiod=5)
@@ -113,6 +124,7 @@ class TestLINEARREG_SLOPE:
# LINEARREG_INTERCEPT
# ---------------------------------------------------------------------------
class TestLINEARREG_INTERCEPT:
def test_perfect_line_intercept_one(self):
# y = [1,2,3,4,5] with x=[0,1,2,3,4] → y = 1 + 1*x → intercept = 1.0
@@ -127,6 +139,7 @@ class TestLINEARREG_INTERCEPT:
# LINEARREG_ANGLE
# ---------------------------------------------------------------------------
class TestLINEARREG_ANGLE:
def test_slope_one_gives_45_degrees(self):
result = LINEARREG_ANGLE(LINDATA, timeperiod=5)
@@ -146,6 +159,7 @@ class TestLINEARREG_ANGLE:
# BETA
# ---------------------------------------------------------------------------
class TestBETA:
def test_nan_warmup(self):
result = BETA(_A, _B, timeperiod=5)
@@ -170,6 +184,7 @@ class TestBETA:
# CORREL
# ---------------------------------------------------------------------------
class TestCOREL:
def test_self_correlation_is_one(self):
result = CORREL(_A, _A, timeperiod=10)
@@ -195,6 +210,7 @@ class TestCOREL:
# TSF
# ---------------------------------------------------------------------------
class TestTSF:
def test_perfect_line(self):
arr = np.arange(1.0, 10.0)
+5 -1
View File
@@ -1,6 +1,7 @@
"""Unit tests for ferro_ta.indicators.volatility"""
import numpy as np
import pytest
from ferro_ta.indicators.volatility import ATR, NATR, TRANGE
# ---------------------------------------------------------------------------
@@ -23,6 +24,7 @@ SMALL_C = np.array([11.0, 12.0, 13.0, 14.0, 15.0])
# TRANGE
# ---------------------------------------------------------------------------
class TestTRANGE:
def test_known_values_constant_range(self):
result = TRANGE(SMALL_H, SMALL_L, SMALL_C)
@@ -66,6 +68,7 @@ class TestTRANGE:
# ATR
# ---------------------------------------------------------------------------
class TestATR:
def test_timeperiod_1_equals_trange(self):
atr = ATR(SMALL_H, SMALL_L, SMALL_C, timeperiod=1)
@@ -99,6 +102,7 @@ class TestATR:
# NATR
# ---------------------------------------------------------------------------
class TestNATR:
def test_nan_warmup(self):
result = NATR(_HIGH, _LOW, _CLOSE, timeperiod=14)
+6 -2
View File
@@ -1,6 +1,7 @@
"""Unit tests for ferro_ta.indicators.volume"""
import numpy as np
import pytest
from ferro_ta.indicators.volume import AD, ADOSC, OBV
# ---------------------------------------------------------------------------
@@ -24,6 +25,7 @@ SMALL_V = np.array([1000.0, 2000.0, 3000.0, 4000.0, 5000.0])
# OBV
# ---------------------------------------------------------------------------
class TestOBV:
def test_known_values_rising(self):
# Rising close: OBV accumulates all volume
@@ -64,6 +66,7 @@ class TestOBV:
# AD
# ---------------------------------------------------------------------------
class TestAD:
def test_known_formula(self):
# AD = cumsum(CLV * volume)
@@ -72,7 +75,7 @@ class TestAD:
l = np.array([10.0])
c = np.array([12.0])
v = np.array([1000.0])
clv = ((12 - 10) - (15 - 12)) / (15 - 10) # (2 - 3) / 5 = -0.2
clv = ((12 - 10) - (15 - 12)) / (15 - 10) # (2 - 3) / 5 = -0.2
expected = clv * 1000.0
result = AD(h, l, c, v)
np.testing.assert_allclose(result[0], expected, rtol=1e-10)
@@ -94,6 +97,7 @@ class TestAD:
# ADOSC
# ---------------------------------------------------------------------------
class TestADOSC:
def test_nan_warmup(self):
result = ADOSC(_HIGH, _LOW, _CLOSE, _VOL, fastperiod=3, slowperiod=10)