feat(indicators): B3 Trend & Directional batch (413 -> 420) (#181)
Adds the **B3 — Trend & Directional** batch: seven new indicators, taking the catalog from 413 to 420 (Trend & Directional family). | Indicator | Input → Output | Summary | |-----------|----------------|---------| | `Qstick` | candle → f64 | Chande's SMA of the candle body (close − open) | | `TtmTrend` | candle → f64 (±1) | John Carter close-vs-median-SMA trend filter | | `TrendStrengthIndex` | f64 → f64 | signed r² of an OLS regression of price vs time | | `PolarizedFractalEfficiency` | f64 → f64 | Hannula directional trend efficiency | | `WavePm` | f64 → f64 | Kase variance-normalised peak-momentum statistic (reconstruction) | | `GatorOscillator` | candle → struct | Bill Williams Alligator convergence/divergence histogram | | `KasePermissionStochastic` | candle → struct | double-smoothed stochastic permission filter | Note: the roadmap's "Directional Indicator +DI/−DI" item is already covered by the existing standalone `PlusDi` / `MinusDi` / `Dx`, so it is intentionally not re-added. All touchpoints wired: core (every-branch unit tests), Python/Node/WASM bindings, fuzz drivers, Python test registries + reference tests, Node factories, README/CHANGELOG counters. Local verify: `cargo test -p wickra-core` (lib 3389 + doc 378), `cargo clippy --workspace --all-targets --all-features -- -D warnings`, node build + 495 tests, maturin + 815 pytest, counter 420 == 420.
This commit is contained in:
@@ -25,6 +25,13 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
Qstick,
|
||||
GatorOscillator,
|
||||
KasePermissionStochastic,
|
||||
WAVE_PM,
|
||||
POLARIZED_FRACTAL_EFFICIENCY,
|
||||
TREND_STRENGTH_INDEX,
|
||||
TTM_TREND,
|
||||
QQE,
|
||||
IMI,
|
||||
ElderRay,
|
||||
@@ -466,6 +473,13 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Qstick",
|
||||
"GatorOscillator",
|
||||
"KasePermissionStochastic",
|
||||
"WAVE_PM",
|
||||
"POLARIZED_FRACTAL_EFFICIENCY",
|
||||
"TREND_STRENGTH_INDEX",
|
||||
"TTM_TREND",
|
||||
"QQE",
|
||||
"IMI",
|
||||
"ElderRay",
|
||||
|
||||
@@ -2866,6 +2866,435 @@ impl PyStochasticCci {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TtmTrend ==============================
|
||||
|
||||
#[pyclass(name = "TTM_TREND", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTtmTrend {
|
||||
inner: wc::TtmTrend,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTtmTrend {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=6))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TtmTrend::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
/// Batch over numpy columns: high, low, close (all 1-D, equal length).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TTM_TREND(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TrendStrengthIndex ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "TREND_STRENGTH_INDEX",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyTrendStrengthIndex {
|
||||
inner: wc::TrendStrengthIndex,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTrendStrengthIndex {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TrendStrengthIndex::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TREND_STRENGTH_INDEX(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Qstick ==============================
|
||||
|
||||
#[pyclass(name = "Qstick", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyQstick {
|
||||
inner: wc::Qstick,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyQstick {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=10))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Qstick::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
/// Batch over open/close numpy columns (Qstick reads the body close-open).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let o = open
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if o.len() != c.len() {
|
||||
return Err(PyValueError::new_err("open, close must be equal length"));
|
||||
}
|
||||
let n = o.len();
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let hi = o[i].max(c[i]);
|
||||
let lo = o[i].min(c[i]);
|
||||
let candle = wc::Candle::new(o[i], hi, lo, c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== PolarizedFractalEfficiency ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "POLARIZED_FRACTAL_EFFICIENCY",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyPolarizedFractalEfficiency {
|
||||
inner: wc::PolarizedFractalEfficiency,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPolarizedFractalEfficiency {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=10, smoothing=5))]
|
||||
fn new(period: usize, smoothing: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PolarizedFractalEfficiency::new(period, smoothing).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.periods().0
|
||||
}
|
||||
#[getter]
|
||||
fn smoothing(&self) -> usize {
|
||||
self.inner.periods().1
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== WavePm ==============================
|
||||
|
||||
#[pyclass(name = "WAVE_PM", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyWavePm {
|
||||
inner: wc::WavePm,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyWavePm {
|
||||
#[new]
|
||||
#[pyo3(signature = (length=32, smoothing=3))]
|
||||
fn new(length: usize, smoothing: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::WavePm::new(length, smoothing).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn length(&self) -> usize {
|
||||
self.inner.periods().0
|
||||
}
|
||||
#[getter]
|
||||
fn smoothing(&self) -> usize {
|
||||
self.inner.periods().1
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== GatorOscillator ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "GatorOscillator",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyGatorOscillator {
|
||||
inner: wc::GatorOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGatorOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (jaw_period=13, teeth_period=8, lips_period=5))]
|
||||
fn new(jaw_period: usize, teeth_period: usize, lips_period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::GatorOscillator::new(jaw_period, teeth_period, lips_period)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.upper, o.lower)))
|
||||
}
|
||||
/// Batch over high/low/close numpy columns. Returns shape `(n, 2)` for
|
||||
/// `[upper, lower]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.upper;
|
||||
out[i * 2 + 1] = o.lower;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== KasePermissionStochastic ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "KasePermissionStochastic",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyKasePermissionStochastic {
|
||||
inner: wc::KasePermissionStochastic,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyKasePermissionStochastic {
|
||||
#[new]
|
||||
#[pyo3(signature = (length=9, smooth=3))]
|
||||
fn new(length: usize, smooth: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KasePermissionStochastic::new(length, smooth).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.fast, o.slow)))
|
||||
}
|
||||
/// Batch over high/low/close numpy columns. Returns shape `(n, 2)` for
|
||||
/// `[fast, slow]`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.fast;
|
||||
out[i * 2 + 1] = o.slow;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -20986,5 +21415,12 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyRsx>()?;
|
||||
m.add_class::<PyDynamicMomentumIndex>()?;
|
||||
m.add_class::<PyStochasticCci>()?;
|
||||
m.add_class::<PyTtmTrend>()?;
|
||||
m.add_class::<PyTrendStrengthIndex>()?;
|
||||
m.add_class::<PyQstick>()?;
|
||||
m.add_class::<PyPolarizedFractalEfficiency>()?;
|
||||
m.add_class::<PyWavePm>()?;
|
||||
m.add_class::<PyGatorOscillator>()?;
|
||||
m.add_class::<PyKasePermissionStochastic>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.WAVE_PM, (32, 3)),
|
||||
(ta.POLARIZED_FRACTAL_EFFICIENCY, (10, 5)),
|
||||
(ta.TREND_STRENGTH_INDEX, (20,)),
|
||||
(ta.DerivativeOscillator, (14, 5, 3, 9)),
|
||||
(ta.RMI, (14, 5)),
|
||||
(ta.DynamicMomentumIndex, (14,)),
|
||||
@@ -355,6 +358,7 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"TTM_TREND": (lambda: ta.TTM_TREND(6), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"StochasticCCI": (lambda: ta.StochasticCCI(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
# Per-bar OHLC transforms (open matters). The streaming harness feeds
|
||||
# open == close, so batch passes the close column in for open to match.
|
||||
@@ -892,6 +896,16 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"KasePermissionStochastic": (
|
||||
lambda: ta.KasePermissionStochastic(9, 3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"GatorOscillator": (
|
||||
lambda: ta.GatorOscillator(13, 8, 5),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"ElderRay": (
|
||||
lambda: ta.ElderRay(13),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
@@ -2779,6 +2793,75 @@ def test_imi_reference():
|
||||
assert math.isnan(out[1])
|
||||
assert out[2] == pytest.approx(75.0)
|
||||
|
||||
|
||||
def test_qstick_reference():
|
||||
q = ta.Qstick(3)
|
||||
open_ = np.array([10.0, 10.0, 10.0])
|
||||
close = np.array([11.0, 11.0, 11.0])
|
||||
out = q.batch(open_, close)
|
||||
# Each body is close - open = 1; SMA(3) of [1, 1, 1] = 1.
|
||||
assert math.isnan(out[0])
|
||||
assert math.isnan(out[1])
|
||||
assert out[2] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_ttm_trend_reference():
|
||||
t = ta.TTM_TREND(3)
|
||||
high = np.array([13.0, 13.0, 13.0])
|
||||
low = np.array([9.0, 9.0, 9.0])
|
||||
close = np.array([12.0, 12.0, 12.0])
|
||||
out = t.batch(high, low, close)
|
||||
# Median (13 + 9) / 2 = 11; close 12 is above the SMA(3) reference -> +1.
|
||||
assert math.isnan(out[0])
|
||||
assert out[2] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_trend_strength_index_reference():
|
||||
tsi = ta.TREND_STRENGTH_INDEX(10)
|
||||
closes = np.arange(10, dtype=float)
|
||||
out = tsi.batch(closes)
|
||||
# A clean ramp is a perfect uptrend -> signed r^2 = +1.
|
||||
assert math.isclose(out[-1], 1.0, abs_tol=1e-9)
|
||||
|
||||
|
||||
def test_polarized_fractal_efficiency_reference():
|
||||
pfe = ta.POLARIZED_FRACTAL_EFFICIENCY(5, 3)
|
||||
closes = np.arange(20, dtype=float)
|
||||
out = pfe.batch(closes)
|
||||
# On a straight ramp the path equals the diagonal -> efficiency 1 -> +100.
|
||||
assert math.isclose(out[-1], 100.0, abs_tol=1e-9)
|
||||
|
||||
|
||||
def test_wave_pm_reference():
|
||||
wpm = ta.WAVE_PM(10, 3)
|
||||
closes = np.arange(60, dtype=float) * 5.0
|
||||
out = wpm.batch(closes)
|
||||
# Constant-slope ramp: momentum equals its energy -> 100 * (1 - e^-0.5).
|
||||
baseline = 100.0 * (1.0 - math.exp(-0.5))
|
||||
assert math.isclose(out[-1], baseline, abs_tol=1e-9)
|
||||
|
||||
|
||||
def test_gator_oscillator_reference():
|
||||
g = ta.GatorOscillator(13, 8, 5)
|
||||
n = 40
|
||||
high = np.full(n, 11.0)
|
||||
low = np.full(n, 9.0)
|
||||
close = np.full(n, 10.0)
|
||||
out = g.batch(high, low, close)
|
||||
# Constant median collapses all three Alligator lines -> both bars zero.
|
||||
assert out[-1][0] == pytest.approx(0.0)
|
||||
assert out[-1][1] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_kase_permission_stochastic_reference():
|
||||
k = ta.KasePermissionStochastic(4, 2)
|
||||
n = 20
|
||||
flat = np.full(n, 10.0)
|
||||
out = k.batch(flat, flat, flat)
|
||||
# HH == LL -> raw %K defaults to the neutral 50 -> both lines at 50.
|
||||
assert out[-1][0] == pytest.approx(50.0)
|
||||
assert out[-1][1] == pytest.approx(50.0)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user