Add B10 Ehlers / Cycle deepening (10 indicators) (#199)
Deepens the **Ehlers / Cycle (DSP)** family (B10) with ten indicators (452 -> 462): - **HighpassFilter**, **Reflex**, **Trendflex**, **CorrelationTrendIndicator**, **AdaptiveRsi**, **UniversalOscillator** — scalar (f64) Ehlers filters/oscillators. - **AdaptiveCci** — efficiency-ratio-adaptive CCI on typical price (Candle input). - **BandpassFilter**, **EvenBetterSinewave**, **AutocorrelationPeriodogram** — multi-arg scalar (hand-written bindings; the wasm variadic scalar macro covers wasm). Verified locally: 3755 core lib + 420 doc tests, clippy clean, 537 node tests, 881 pytest, counter 462.
This commit is contained in:
@@ -28,6 +28,15 @@ function num(v) {
|
||||
// --- Scalar indicators: update(value) vs batch(prices) ---
|
||||
|
||||
const scalarFactories = {
|
||||
AUTOCORRPGRAM: () => new wickra.AUTOCORRPGRAM(10, 48),
|
||||
EVENBETTERSINE: () => new wickra.EVENBETTERSINE(40, 10),
|
||||
BANDPASS: () => new wickra.BANDPASS(20, 0.3),
|
||||
UNIVERSALOSC: () => new wickra.UNIVERSALOSC(20),
|
||||
ADAPTIVERSI: () => new wickra.ADAPTIVERSI(14),
|
||||
CTI: () => new wickra.CTI(20),
|
||||
TRENDFLEX: () => new wickra.TRENDFLEX(20),
|
||||
REFLEX: () => new wickra.REFLEX(20),
|
||||
HIGHPASS: () => new wickra.HIGHPASS(48),
|
||||
SAMPLEENT: () => new wickra.SAMPLEENT(20, 2, 0.2),
|
||||
SHANNONENT: () => new wickra.SHANNONENT(20, 8),
|
||||
ROLLINGMINMAX: () => new wickra.ROLLINGMINMAX(20),
|
||||
@@ -367,6 +376,7 @@ const candleScalar = {
|
||||
TradeVolumeIndex: { make: () => new wickra.TradeVolumeIndex(0.25), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
|
||||
IntradayIntensity: { make: () => new wickra.IntradayIntensity(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||
BetterVolume: { make: () => new wickra.BetterVolume(14), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
|
||||
ADAPTIVECCI: { make: () => new wickra.ADAPTIVECCI(20), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
|
||||
Vendored
+90
@@ -1070,6 +1070,87 @@ export declare class ROLLINGMINMAX {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HighpassFilterNode = HIGHPASS
|
||||
export declare class HIGHPASS {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ReflexNode = REFLEX
|
||||
export declare class REFLEX {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TrendflexNode = TRENDFLEX
|
||||
export declare class TRENDFLEX {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CorrelationTrendIndicatorNode = CTI
|
||||
export declare class CTI {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AdaptiveRsiNode = ADAPTIVERSI
|
||||
export declare class ADAPTIVERSI {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type UniversalOscillatorNode = UNIVERSALOSC
|
||||
export declare class UNIVERSALOSC {
|
||||
constructor(period: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type BandpassFilterNode = BANDPASS
|
||||
export declare class BANDPASS {
|
||||
constructor(period: number, bandwidth: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EvenBetterSinewaveNode = EVENBETTERSINE
|
||||
export declare class EVENBETTERSINE {
|
||||
constructor(hpPeriod: number, ssfLength: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AutocorrelationPeriodogramNode = AUTOCORRPGRAM
|
||||
export declare class AUTOCORRPGRAM {
|
||||
constructor(minPeriod: number, maxPeriod: number)
|
||||
update(value: number): number | null
|
||||
batch(prices: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ShannonEntropyNode = SHANNONENT
|
||||
export declare class SHANNONENT {
|
||||
constructor(period: number, bins: number)
|
||||
@@ -1751,6 +1832,15 @@ export declare class TimeBasedStop {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AdaptiveCciNode = ADAPTIVECCI
|
||||
export declare class ADAPTIVECCI {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type StochNode = Stochastic
|
||||
export declare class Stochastic {
|
||||
constructor(kPeriod: number, dPeriod: number)
|
||||
|
||||
+11
-1
File diff suppressed because one or more lines are too long
@@ -231,6 +231,129 @@ node_scalar_indicator!(
|
||||
"ROLLINGMINMAX",
|
||||
wc::RollingMinMaxScaler
|
||||
);
|
||||
node_scalar_indicator!(HighpassFilterNode, "HIGHPASS", wc::HighpassFilter);
|
||||
node_scalar_indicator!(ReflexNode, "REFLEX", wc::Reflex);
|
||||
node_scalar_indicator!(TrendflexNode, "TRENDFLEX", wc::Trendflex);
|
||||
node_scalar_indicator!(
|
||||
CorrelationTrendIndicatorNode,
|
||||
"CTI",
|
||||
wc::CorrelationTrendIndicator
|
||||
);
|
||||
node_scalar_indicator!(AdaptiveRsiNode, "ADAPTIVERSI", wc::AdaptiveRsi);
|
||||
node_scalar_indicator!(
|
||||
UniversalOscillatorNode,
|
||||
"UNIVERSALOSC",
|
||||
wc::UniversalOscillator
|
||||
);
|
||||
|
||||
// Multi-arg Ehlers scalars: hand-written (node_scalar_indicator! is single-period).
|
||||
|
||||
#[napi(js_name = "BANDPASS")]
|
||||
pub struct BandpassFilterNode {
|
||||
inner: wc::BandpassFilter,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl BandpassFilterNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, bandwidth: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::BandpassFilter::new(period as usize, bandwidth).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "EVENBETTERSINE")]
|
||||
pub struct EvenBetterSinewaveNode {
|
||||
inner: wc::EvenBetterSinewave,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl EvenBetterSinewaveNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(hp_period: u32, ssf_length: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::EvenBetterSinewave::new(hp_period as usize, ssf_length as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "AUTOCORRPGRAM")]
|
||||
pub struct AutocorrelationPeriodogramNode {
|
||||
inner: wc::AutocorrelationPeriodogram,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AutocorrelationPeriodogramNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(min_period: u32, max_period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AutocorrelationPeriodogram::new(min_period as usize, max_period as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// Shannon Entropy / Sample Entropy: multi-arg scalar ctors, hand-written
|
||||
// (node_scalar_indicator! only generates a single-period constructor).
|
||||
@@ -3056,6 +3179,59 @@ impl TimeBasedStopNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "ADAPTIVECCI")]
|
||||
pub struct AdaptiveCciNode {
|
||||
inner: wc::AdaptiveCci,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AdaptiveCciNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AdaptiveCci::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct StochValue {
|
||||
pub k: f64,
|
||||
|
||||
@@ -25,6 +25,16 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
AUTOCORRPGRAM,
|
||||
EVENBETTERSINE,
|
||||
BANDPASS,
|
||||
ADAPTIVECCI,
|
||||
UNIVERSALOSC,
|
||||
ADAPTIVERSI,
|
||||
CTI,
|
||||
TRENDFLEX,
|
||||
REFLEX,
|
||||
HIGHPASS,
|
||||
SAMPLEENT,
|
||||
SHANNONENT,
|
||||
ROLLINGMINMAX,
|
||||
@@ -506,6 +516,16 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AUTOCORRPGRAM",
|
||||
"EVENBETTERSINE",
|
||||
"BANDPASS",
|
||||
"ADAPTIVECCI",
|
||||
"UNIVERSALOSC",
|
||||
"ADAPTIVERSI",
|
||||
"CTI",
|
||||
"TRENDFLEX",
|
||||
"REFLEX",
|
||||
"HIGHPASS",
|
||||
"SAMPLEENT",
|
||||
"SHANNONENT",
|
||||
"ROLLINGMINMAX",
|
||||
|
||||
@@ -3796,6 +3796,362 @@ impl PyRollingMinMaxScaler {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== HighpassFilter ==============================
|
||||
|
||||
#[pyclass(name = "HIGHPASS", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyHighpassFilter {
|
||||
inner: wc::HighpassFilter,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHighpassFilter {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=48))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HighpassFilter::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!("HIGHPASS(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Reflex ==============================
|
||||
|
||||
#[pyclass(name = "REFLEX", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyReflex {
|
||||
inner: wc::Reflex,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyReflex {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Reflex::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!("REFLEX(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Trendflex ==============================
|
||||
|
||||
#[pyclass(name = "TRENDFLEX", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTrendflex {
|
||||
inner: wc::Trendflex,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTrendflex {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Trendflex::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!("TRENDFLEX(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== CorrelationTrendIndicator ==============================
|
||||
|
||||
#[pyclass(name = "CTI", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyCorrelationTrendIndicator {
|
||||
inner: wc::CorrelationTrendIndicator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCorrelationTrendIndicator {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::CorrelationTrendIndicator::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!("CTI(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== AdaptiveRsi ==============================
|
||||
|
||||
#[pyclass(name = "ADAPTIVERSI", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyAdaptiveRsi {
|
||||
inner: wc::AdaptiveRsi,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAdaptiveRsi {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AdaptiveRsi::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!("ADAPTIVERSI(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== UniversalOscillator ==============================
|
||||
|
||||
#[pyclass(name = "UNIVERSALOSC", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyUniversalOscillator {
|
||||
inner: wc::UniversalOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyUniversalOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::UniversalOscillator::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!("UNIVERSALOSC(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== AdaptiveCci ==============================
|
||||
|
||||
#[pyclass(name = "ADAPTIVECCI", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyAdaptiveCci {
|
||||
inner: wc::AdaptiveCci,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAdaptiveCci {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AdaptiveCci::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!("ADAPTIVECCI(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -23001,6 +23357,169 @@ impl PyKendallTau {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Bandpass Filter ==============================
|
||||
|
||||
#[pyclass(name = "BANDPASS", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyBandpassFilter {
|
||||
inner: wc::BandpassFilter,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyBandpassFilter {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20, bandwidth=0.3))]
|
||||
fn new(period: usize, bandwidth: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::BandpassFilter::new(period, bandwidth).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 slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, f64) {
|
||||
self.inner.params()
|
||||
}
|
||||
#[getter]
|
||||
fn value(&self) -> Option<f64> {
|
||||
self.inner.value()
|
||||
}
|
||||
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 {
|
||||
let (period, bandwidth) = self.inner.params();
|
||||
format!("BANDPASS(period={period}, bandwidth={bandwidth})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Even Better Sinewave ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "EVENBETTERSINE",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyEvenBetterSinewave {
|
||||
inner: wc::EvenBetterSinewave,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEvenBetterSinewave {
|
||||
#[new]
|
||||
#[pyo3(signature = (hp_period=40, ssf_length=10))]
|
||||
fn new(hp_period: usize, ssf_length: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::EvenBetterSinewave::new(hp_period, ssf_length).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 slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, usize) {
|
||||
self.inner.params()
|
||||
}
|
||||
#[getter]
|
||||
fn value(&self) -> Option<f64> {
|
||||
self.inner.value()
|
||||
}
|
||||
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 {
|
||||
let (hp_period, ssf_length) = self.inner.params();
|
||||
format!("EVENBETTERSINE(hp_period={hp_period}, ssf_length={ssf_length})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Autocorrelation Periodogram ==============================
|
||||
|
||||
#[pyclass(name = "AUTOCORRPGRAM", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyAutocorrelationPeriodogram {
|
||||
inner: wc::AutocorrelationPeriodogram,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAutocorrelationPeriodogram {
|
||||
#[new]
|
||||
#[pyo3(signature = (min_period=10, max_period=48))]
|
||||
fn new(min_period: usize, max_period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AutocorrelationPeriodogram::new(min_period, max_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 slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn periods(&self) -> (usize, usize) {
|
||||
self.inner.periods()
|
||||
}
|
||||
#[getter]
|
||||
fn value(&self) -> Option<f64> {
|
||||
self.inner.value()
|
||||
}
|
||||
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 {
|
||||
let (min_period, max_period) = self.inner.periods();
|
||||
format!("AUTOCORRPGRAM(min_period={min_period}, max_period={max_period})")
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
@@ -23467,7 +23986,17 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyShannonEntropy>()?;
|
||||
m.add_class::<PySampleEntropy>()?;
|
||||
m.add_class::<PyKendallTau>()?;
|
||||
m.add_class::<PyBandpassFilter>()?;
|
||||
m.add_class::<PyEvenBetterSinewave>()?;
|
||||
m.add_class::<PyAutocorrelationPeriodogram>()?;
|
||||
m.add_class::<PyJarqueBera>()?;
|
||||
m.add_class::<PyRollingMinMaxScaler>()?;
|
||||
m.add_class::<PyHighpassFilter>()?;
|
||||
m.add_class::<PyReflex>()?;
|
||||
m.add_class::<PyTrendflex>()?;
|
||||
m.add_class::<PyCorrelationTrendIndicator>()?;
|
||||
m.add_class::<PyAdaptiveRsi>()?;
|
||||
m.add_class::<PyUniversalOscillator>()?;
|
||||
m.add_class::<PyAdaptiveCci>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,6 +45,15 @@ def ohlcv() -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||||
# --- Scalar (f64 -> f64) indicators ---------------------------------------
|
||||
|
||||
SCALAR = [
|
||||
(ta.AUTOCORRPGRAM, (10, 48)),
|
||||
(ta.EVENBETTERSINE, (40, 10)),
|
||||
(ta.BANDPASS, (20, 0.3)),
|
||||
(ta.UNIVERSALOSC, (20,)),
|
||||
(ta.ADAPTIVERSI, (14,)),
|
||||
(ta.CTI, (20,)),
|
||||
(ta.TRENDFLEX, (20,)),
|
||||
(ta.REFLEX, (20,)),
|
||||
(ta.HIGHPASS, (48,)),
|
||||
(ta.SAMPLEENT, (20, 2, 0.2)),
|
||||
(ta.SHANNONENT, (20, 8)),
|
||||
(ta.ROLLINGMINMAX, (20,)),
|
||||
@@ -373,6 +382,7 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"ADAPTIVECCI": (lambda: ta.ADAPTIVECCI(20), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"BetterVolume": (
|
||||
lambda: ta.BetterVolume(14),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c, v),
|
||||
|
||||
@@ -2591,6 +2591,44 @@ impl WasmTimeBasedStop {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ADAPTIVECCI)]
|
||||
pub struct WasmAdaptiveCci {
|
||||
inner: wc::AdaptiveCci,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ADAPTIVECCI)]
|
||||
impl WasmAdaptiveCci {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmAdaptiveCci, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::AdaptiveCci::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
out.push(self.inner.update(c).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = Stochastic)]
|
||||
pub struct WasmStoch {
|
||||
inner: wc::Stochastic,
|
||||
@@ -11261,6 +11299,15 @@ wasm_scalar_indicator!(WasmJarqueBera, "JARQUEBERA", wc::JarqueBera, period: usi
|
||||
wasm_scalar_indicator!(WasmRollingMinMaxScaler, "ROLLINGMINMAX", wc::RollingMinMaxScaler, period: usize);
|
||||
wasm_scalar_indicator!(WasmShannonEntropy, "SHANNONENT", wc::ShannonEntropy, period: usize, bins: usize);
|
||||
wasm_scalar_indicator!(WasmSampleEntropy, "SAMPLEENT", wc::SampleEntropy, period: usize, m: usize, r_factor: f64);
|
||||
wasm_scalar_indicator!(WasmHighpassFilter, "HIGHPASS", wc::HighpassFilter, period: usize);
|
||||
wasm_scalar_indicator!(WasmReflex, "REFLEX", wc::Reflex, period: usize);
|
||||
wasm_scalar_indicator!(WasmTrendflex, "TRENDFLEX", wc::Trendflex, period: usize);
|
||||
wasm_scalar_indicator!(WasmCorrelationTrendIndicator, "CTI", wc::CorrelationTrendIndicator, period: usize);
|
||||
wasm_scalar_indicator!(WasmAdaptiveRsi, "ADAPTIVERSI", wc::AdaptiveRsi, period: usize);
|
||||
wasm_scalar_indicator!(WasmUniversalOscillator, "UNIVERSALOSC", wc::UniversalOscillator, period: usize);
|
||||
wasm_scalar_indicator!(WasmBandpassFilter, "BANDPASS", wc::BandpassFilter, period: usize, bandwidth: f64);
|
||||
wasm_scalar_indicator!(WasmEvenBetterSinewave, "EVENBETTERSINE", wc::EvenBetterSinewave, hp_period: usize, ssf_length: usize);
|
||||
wasm_scalar_indicator!(WasmAutocorrelationPeriodogram, "AUTOCORRPGRAM", wc::AutocorrelationPeriodogram, min_period: usize, max_period: usize);
|
||||
|
||||
// --- VolatilityCone: Candle in, struct out (current/min/median/max/percentile) ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user