0b85142ad1
* feat(core): add PairwiseBeta cross-asset indicator
Rolling OLS slope of one asset's log-returns on another's. Unlike Beta,
which regresses the raw inputs it is fed, PairwiseBeta differences
consecutive prices into log-returns internally -- the conventional way to
measure cross-asset beta, where a beta on price levels would be dominated
by the shared trend.
Two-series Indicator<Input = (f64, f64)>, exposed in Rust, Python, Node
and WASM, with unit/known-value/streaming tests and a pair fuzz target.
* feat(core): add PairSpreadZScore cross-asset indicator
Standardised log-spread ln(a) - beta*ln(b) of a pair, where beta is a
rolling-OLS hedge ratio and the spread is z-scored over its own look-back.
The canonical mean-reversion / statistical-arbitrage entry signal, with
independent beta_period and z_period windows.
Two-series Indicator<Input = (f64, f64)>, exposed in Rust, Python, Node
and WASM, with sign/known-value/streaming tests and a pair fuzz target.
* feat(core): add LeadLagCrossCorrelation cross-asset indicator
Reports the integer offset k in [-max_lag, max_lag] that maximises
|corr(a[t], b[t+k])|, answering which of two assets leads the other and by
how many bars. A positive lag means a leads b. Fully causal: a's window is
held centred while b's window slides across the buffered history, so every
lag is evaluated only against data already seen.
Struct output { lag, correlation }, exposed in Rust, Python, Node and WASM
with lead-detection/streaming tests and a pair fuzz driver.
* feat(core): add Cointegration (Engle-Granger + ADF) indicator
Rolling pairs-trading screen: an OLS hedge ratio of a on b, the spread
(residual) a - (alpha + beta*b), and an augmented Dickey-Fuller t-statistic
on the spread with configurable lags. A strongly negative statistic flags a
mean-reverting, tradeable spread. Includes a small Gaussian-elimination
solver for the augmented regression.
Struct output { hedge_ratio, spread, adf_stat }, exposed in Rust, Python,
Node and WASM with stationarity/hedge-ratio/streaming tests and a pair fuzz
driver.
* feat(core): add RelativeStrengthAB cross-asset indicator
Comparative relative strength of two assets: the ratio line a/b together
with its moving average and its RSI, the classic asset-vs-asset /
asset-vs-index rotation screen. Composes the existing Sma and Rsi over the
ratio; a zero denominator or non-finite price is skipped.
Struct output { ratio, ratio_ma, ratio_rsi }, exposed in Rust, Python, Node
and WASM with flat/rising-ratio/streaming tests and a pair fuzz driver.
* test(cointegration): cover ADF guard branches
The ADF helper's short-series and degrees-of-freedom guards and the
zero-dispersion (perfect AR) path are unreachable through the public
Cointegration API (period >= 2*adf_lags + 4), so exercise them with direct
unit tests on adf_no_constant. The second linear solve cannot be singular
once the coefficient solve on the same matrix has succeeded, so it now uses
expect() instead of a dead error branch.
169 lines
4.9 KiB
Python
169 lines
4.9 KiB
Python
"""Input-validation tests: malformed NumPy inputs raise ValueError, not panics."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import wickra as ta
|
|
|
|
|
|
def test_non_contiguous_array_raises_value_error():
|
|
# A strided view is not C-contiguous; batch() must reject it cleanly.
|
|
base = np.linspace(1.0, 100.0, 60)
|
|
non_contiguous = base[::2]
|
|
assert not non_contiguous.flags["C_CONTIGUOUS"]
|
|
with pytest.raises(ValueError):
|
|
ta.SMA(5).batch(non_contiguous)
|
|
|
|
|
|
def test_ascontiguousarray_recovers():
|
|
base = np.linspace(1.0, 100.0, 60)
|
|
fixed = np.ascontiguousarray(base[::2])
|
|
out = ta.SMA(5).batch(fixed)
|
|
assert out.shape == fixed.shape
|
|
|
|
|
|
def test_unequal_length_candle_batch_raises(ohlc_series):
|
|
high, low, close = ohlc_series
|
|
short = low[:-1]
|
|
with pytest.raises(ValueError):
|
|
ta.ATR(14).batch(high, short, close)
|
|
with pytest.raises(ValueError):
|
|
ta.WilliamsR(14).batch(high, short, close)
|
|
with pytest.raises(ValueError):
|
|
ta.Aroon(14).batch(high, short)
|
|
|
|
|
|
def test_pairwise_beta_rejects_bad_period():
|
|
with pytest.raises(ValueError):
|
|
ta.PairwiseBeta(0)
|
|
with pytest.raises(ValueError):
|
|
ta.PairwiseBeta(1)
|
|
|
|
|
|
def test_unequal_length_pair_batch_raises(sine_prices):
|
|
a = np.ascontiguousarray((sine_prices + 100.0).astype(np.float64))
|
|
b = a[:-1]
|
|
with pytest.raises(ValueError):
|
|
ta.PairwiseBeta(20).batch(a, b)
|
|
with pytest.raises(ValueError):
|
|
ta.PairSpreadZScore(20, 20).batch(a, b)
|
|
|
|
|
|
def test_pair_spread_zscore_rejects_bad_periods():
|
|
with pytest.raises(ValueError):
|
|
ta.PairSpreadZScore(1, 20)
|
|
with pytest.raises(ValueError):
|
|
ta.PairSpreadZScore(20, 1)
|
|
|
|
|
|
def test_lead_lag_rejects_bad_params():
|
|
with pytest.raises(ValueError):
|
|
ta.LeadLagCrossCorrelation(1, 5)
|
|
with pytest.raises(ValueError):
|
|
ta.LeadLagCrossCorrelation(10, 0)
|
|
|
|
|
|
def test_lead_lag_unequal_length_batch_raises(sine_prices):
|
|
a = np.ascontiguousarray((sine_prices + 100.0).astype(np.float64))
|
|
b = a[:-1]
|
|
with pytest.raises(ValueError):
|
|
ta.LeadLagCrossCorrelation(12, 5).batch(a, b)
|
|
|
|
|
|
def test_cointegration_rejects_too_small_period():
|
|
# period must be >= 2*adf_lags + 4.
|
|
with pytest.raises(ValueError):
|
|
ta.Cointegration(3, 0)
|
|
with pytest.raises(ValueError):
|
|
ta.Cointegration(5, 1)
|
|
|
|
|
|
def test_cointegration_unequal_length_batch_raises(sine_prices):
|
|
a = np.ascontiguousarray((sine_prices + 100.0).astype(np.float64))
|
|
b = a[:-1]
|
|
with pytest.raises(ValueError):
|
|
ta.Cointegration(20, 1).batch(a, b)
|
|
|
|
|
|
def test_relative_strength_rejects_zero_periods():
|
|
with pytest.raises(ValueError):
|
|
ta.RelativeStrengthAB(0, 14)
|
|
with pytest.raises(ValueError):
|
|
ta.RelativeStrengthAB(20, 0)
|
|
|
|
|
|
def test_relative_strength_unequal_length_batch_raises(sine_prices):
|
|
a = np.ascontiguousarray((sine_prices + 100.0).astype(np.float64))
|
|
b = a[:-1]
|
|
with pytest.raises(ValueError):
|
|
ta.RelativeStrengthAB(10, 14).batch(a, b)
|
|
|
|
|
|
def test_roc_and_trix_have_default_periods():
|
|
# ROC/TRIX gained constructor defaults matching the TA-Lib convention.
|
|
assert ta.ROC().period == 10
|
|
assert ta.TRIX() is not None
|
|
|
|
|
|
def test_value_area_rejects_zero_period():
|
|
with pytest.raises(ValueError):
|
|
ta.ValueArea(0, 50, 0.7)
|
|
with pytest.raises(ValueError):
|
|
ta.ValueArea(20, 0, 0.7)
|
|
|
|
|
|
def test_value_area_rejects_invalid_pct():
|
|
with pytest.raises(ValueError):
|
|
ta.ValueArea(20, 50, 0.0)
|
|
with pytest.raises(ValueError):
|
|
ta.ValueArea(20, 50, 1.5)
|
|
|
|
|
|
def test_initial_balance_rejects_zero_period():
|
|
with pytest.raises(ValueError):
|
|
ta.InitialBalance(0)
|
|
|
|
|
|
def test_opening_range_rejects_zero_period():
|
|
with pytest.raises(ValueError):
|
|
ta.OpeningRange(0)
|
|
|
|
|
|
def test_value_area_unequal_length_raises():
|
|
high = np.array([1.0, 2.0, 3.0])
|
|
low = np.array([0.5, 1.5])
|
|
volume = np.array([10.0, 10.0, 10.0])
|
|
with pytest.raises(ValueError):
|
|
ta.ValueArea(2, 10, 0.7).batch(high, low, volume)
|
|
|
|
|
|
def test_ichimoku_rejects_zero_and_non_increasing_periods():
|
|
with pytest.raises(ValueError):
|
|
ta.Ichimoku(0, 26, 52, 26)
|
|
with pytest.raises(ValueError):
|
|
ta.Ichimoku(9, 26, 52, 0)
|
|
# Periods must satisfy tenkan < kijun < senkou_b.
|
|
with pytest.raises(ValueError):
|
|
ta.Ichimoku(26, 9, 52, 26)
|
|
with pytest.raises(ValueError):
|
|
ta.Ichimoku(9, 52, 52, 26)
|
|
|
|
|
|
def test_family_10_ehlers_rejects_invalid_parameters():
|
|
with pytest.raises(ValueError):
|
|
ta.SuperSmoother(0)
|
|
with pytest.raises(ValueError):
|
|
ta.FisherTransform(0)
|
|
with pytest.raises(ValueError):
|
|
ta.InverseFisherTransform(0.0)
|
|
with pytest.raises(ValueError):
|
|
ta.DecyclerOscillator(30, 10)
|
|
with pytest.raises(ValueError):
|
|
ta.RoofingFilter(48, 10)
|
|
with pytest.raises(ValueError):
|
|
ta.MAMA(0.05, 0.5)
|
|
with pytest.raises(ValueError):
|
|
ta.EmpiricalModeDecomposition(20, 0.0)
|