F10: add Chaikin Money Flow, Chaikin Oscillator, Force Index and Ease of Movement
- Rust core: cmf.rs (Chaikin Money Flow — summed money-flow volume over summed volume, bounded to [-1, +1]), chaikin_oscillator.rs (Chaikin Oscillator — the MACD of the ADL, EMA(ADL, fast) - EMA(ADL, slow)), force_index.rs (Elder's Force Index — EMA of price change scaled by volume), ease_of_movement.rs (Arms' Ease of Movement — SMA of distance travelled per unit of volume). Each with a full Indicator impl, runnable doctest and reference / property / warmup / reset / batch==streaming tests. - Python: PyChaikinMoneyFlow / PyChaikinOscillator / PyForceIndex / PyEaseOfMovement PyO3 classes + module registration + .pyi stubs. - Node: explicit ChaikinMoneyFlowNode / ChaikinOscillatorNode / ForceIndexNode / EaseOfMovementNode; index.d.ts and index.js updated. - WASM: WasmChaikinMoneyFlow / WasmChaikinOscillator / WasmForceIndex / WasmEaseOfMovement. - Wiki: Indicator-ChaikinMoneyFlow/ChaikinOscillator/ForceIndex/ EaseOfMovement.md plus a new "Oscillators" sub-table in Indicators-Overview.md and entries in Home.md. cargo fmt + clippy (core/wickra/data/wasm/node) clean; 402 core tests, 25 data tests and 57 doctests green.
This commit is contained in:
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, T3, VWMA, MOM, CMO, TSI, PMO, StochRSI, UltimateOscillator, PPO, DPO, Coppock, AroonOscillator, Vortex, MassIndex, NATR, StdDev, UlcerIndex, HistoricalVolatility, BollingerBandwidth, PercentB, ADL, VolumePriceTrend, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, AwesomeOscillator, Aroon, KAMA } = nativeBinding
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, T3, VWMA, MOM, CMO, TSI, PMO, StochRSI, UltimateOscillator, PPO, DPO, Coppock, AroonOscillator, Vortex, MassIndex, NATR, StdDev, UlcerIndex, HistoricalVolatility, BollingerBandwidth, PercentB, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, AwesomeOscillator, Aroon, KAMA } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -347,6 +347,10 @@ module.exports.BollingerBandwidth = BollingerBandwidth
|
||||
module.exports.PercentB = PercentB
|
||||
module.exports.ADL = ADL
|
||||
module.exports.VolumePriceTrend = VolumePriceTrend
|
||||
module.exports.ChaikinMoneyFlow = ChaikinMoneyFlow
|
||||
module.exports.ChaikinOscillator = ChaikinOscillator
|
||||
module.exports.ForceIndex = ForceIndex
|
||||
module.exports.EaseOfMovement = EaseOfMovement
|
||||
module.exports.MACD = MACD
|
||||
module.exports.BollingerBands = BollingerBands
|
||||
module.exports.ATR = ATR
|
||||
|
||||
@@ -1271,6 +1271,235 @@ impl VolumePriceTrendNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chaikin Money Flow ==============================
|
||||
|
||||
#[napi(js_name = "ChaikinMoneyFlow")]
|
||||
pub struct ChaikinMoneyFlowNode {
|
||||
inner: wc::ChaikinMoneyFlow,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ChaikinMoneyFlowNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ChaikinMoneyFlow::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, volume)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() || close.len() != volume.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close, volume 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], volume[i])?)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chaikin Oscillator ==============================
|
||||
|
||||
#[napi(js_name = "ChaikinOscillator")]
|
||||
pub struct ChaikinOscillatorNode {
|
||||
inner: wc::ChaikinOscillator,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ChaikinOscillatorNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(fast: u32, slow: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ChaikinOscillator::new(fast as usize, slow as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, volume)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() || close.len() != volume.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close, volume 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], volume[i])?)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Force Index ==============================
|
||||
|
||||
#[napi(js_name = "ForceIndex")]
|
||||
pub struct ForceIndexNode {
|
||||
inner: wc::ForceIndex,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ForceIndexNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ForceIndex::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, close: f64, volume: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(close, close, close, volume)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, close: Vec<f64>, volume: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if close.len() != volume.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"close and volume must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(close.len());
|
||||
for i in 0..close.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(close[i], close[i], close[i], volume[i])?)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Ease of Movement ==============================
|
||||
|
||||
#[napi(js_name = "EaseOfMovement")]
|
||||
pub struct EaseOfMovementNode {
|
||||
inner: wc::EaseOfMovement,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl EaseOfMovementNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, divisor: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::EaseOfMovement::with_divisor(period as usize, divisor).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, low, volume)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != volume.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, volume 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], low[i], volume[i])?)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Bollinger Bandwidth ==============================
|
||||
|
||||
#[napi(js_name = "BollingerBandwidth")]
|
||||
|
||||
@@ -106,6 +106,69 @@ class VolumePriceTrend:
|
||||
@property
|
||||
def value(self) -> Optional[float]: ...
|
||||
|
||||
class ChaikinMoneyFlow:
|
||||
def __init__(self, period: int = 20) -> None: ...
|
||||
def update(self, candle: CandleLike) -> Optional[float]: ...
|
||||
def batch(
|
||||
self,
|
||||
high: NDArray[np.float64],
|
||||
low: NDArray[np.float64],
|
||||
close: NDArray[np.float64],
|
||||
volume: NDArray[np.float64],
|
||||
) -> NDArray[np.float64]: ...
|
||||
def reset(self) -> None: ...
|
||||
def is_ready(self) -> bool: ...
|
||||
def warmup_period(self) -> int: ...
|
||||
@property
|
||||
def period(self) -> int: ...
|
||||
|
||||
class ChaikinOscillator:
|
||||
def __init__(self, fast: int = 3, slow: int = 10) -> None: ...
|
||||
def update(self, candle: CandleLike) -> Optional[float]: ...
|
||||
def batch(
|
||||
self,
|
||||
high: NDArray[np.float64],
|
||||
low: NDArray[np.float64],
|
||||
close: NDArray[np.float64],
|
||||
volume: NDArray[np.float64],
|
||||
) -> NDArray[np.float64]: ...
|
||||
def reset(self) -> None: ...
|
||||
def is_ready(self) -> bool: ...
|
||||
def warmup_period(self) -> int: ...
|
||||
@property
|
||||
def periods(self) -> Tuple[int, int]: ...
|
||||
|
||||
class ForceIndex:
|
||||
def __init__(self, period: int = 13) -> None: ...
|
||||
def update(self, candle: CandleLike) -> Optional[float]: ...
|
||||
def batch(
|
||||
self,
|
||||
close: NDArray[np.float64],
|
||||
volume: NDArray[np.float64],
|
||||
) -> NDArray[np.float64]: ...
|
||||
def reset(self) -> None: ...
|
||||
def is_ready(self) -> bool: ...
|
||||
def warmup_period(self) -> int: ...
|
||||
@property
|
||||
def period(self) -> int: ...
|
||||
|
||||
class EaseOfMovement:
|
||||
def __init__(self, period: int = 14, divisor: float = 100000000.0) -> None: ...
|
||||
def update(self, candle: CandleLike) -> Optional[float]: ...
|
||||
def batch(
|
||||
self,
|
||||
high: NDArray[np.float64],
|
||||
low: NDArray[np.float64],
|
||||
volume: NDArray[np.float64],
|
||||
) -> NDArray[np.float64]: ...
|
||||
def reset(self) -> None: ...
|
||||
def is_ready(self) -> bool: ...
|
||||
def warmup_period(self) -> int: ...
|
||||
@property
|
||||
def period(self) -> int: ...
|
||||
@property
|
||||
def divisor(self) -> float: ...
|
||||
|
||||
class BollingerBandwidth:
|
||||
def __init__(self, period: int = 20, multiplier: float = 2.0) -> None: ...
|
||||
def update(self, value: float) -> Optional[float]: ...
|
||||
|
||||
@@ -2992,6 +2992,291 @@ impl PyTrima {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chaikin Money Flow ==============================
|
||||
|
||||
#[pyclass(name = "ChaikinMoneyFlow", module = "wickra._wickra")]
|
||||
#[derive(Clone)]
|
||||
struct PyChaikinMoneyFlow {
|
||||
inner: wc::ChaikinMoneyFlow,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyChaikinMoneyFlow {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ChaikinMoneyFlow::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, volume (all equal length).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: 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))?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close, volume 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], v[i], 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray_bound(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!("ChaikinMoneyFlow(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chaikin Oscillator ==============================
|
||||
|
||||
#[pyclass(name = "ChaikinOscillator", module = "wickra._wickra")]
|
||||
#[derive(Clone)]
|
||||
struct PyChaikinOscillator {
|
||||
inner: wc::ChaikinOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyChaikinOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (fast=3, slow=10))]
|
||||
fn new(fast: usize, slow: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ChaikinOscillator::new(fast, slow).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, volume (all equal length).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: 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))?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close, volume 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], v[i], 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray_bound(py))
|
||||
}
|
||||
#[getter]
|
||||
fn periods(&self) -> (usize, usize) {
|
||||
self.inner.periods()
|
||||
}
|
||||
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 (fast, slow) = self.inner.periods();
|
||||
format!("ChaikinOscillator(fast={fast}, slow={slow})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Force Index ==============================
|
||||
|
||||
#[pyclass(name = "ForceIndex", module = "wickra._wickra")]
|
||||
#[derive(Clone)]
|
||||
struct PyForceIndex {
|
||||
inner: wc::ForceIndex,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyForceIndex {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=13))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ForceIndex::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 close + volume arrays (both 1-D, equal length).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if c.len() != v.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"close and volume must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(c.len());
|
||||
for i in 0..c.len() {
|
||||
let candle = wc::Candle::new(c[i], c[i], c[i], c[i], v[i], 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray_bound(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!("ForceIndex(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Ease of Movement ==============================
|
||||
|
||||
#[pyclass(name = "EaseOfMovement", module = "wickra._wickra")]
|
||||
#[derive(Clone)]
|
||||
struct PyEaseOfMovement {
|
||||
inner: wc::EaseOfMovement,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEaseOfMovement {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14, divisor=100_000_000.0))]
|
||||
fn new(period: usize, divisor: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::EaseOfMovement::with_divisor(period, divisor).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, volume (all equal length).
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
volume: 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 v = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != v.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, volume must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(l[i], h[i], l[i], l[i], v[i], 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray_bound(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
#[getter]
|
||||
fn divisor(&self) -> f64 {
|
||||
self.inner.divisor()
|
||||
}
|
||||
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!(
|
||||
"EaseOfMovement(period={}, divisor={})",
|
||||
self.inner.period(),
|
||||
self.inner.divisor()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Module ==============================
|
||||
|
||||
#[pymodule]
|
||||
@@ -3047,5 +3332,9 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyPercentB>()?;
|
||||
m.add_class::<PyAdl>()?;
|
||||
m.add_class::<PyVolumePriceTrend>()?;
|
||||
m.add_class::<PyChaikinMoneyFlow>()?;
|
||||
m.add_class::<PyChaikinOscillator>()?;
|
||||
m.add_class::<PyForceIndex>()?;
|
||||
m.add_class::<PyEaseOfMovement>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -470,6 +470,174 @@ impl WasmVolumePriceTrend {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ChaikinMoneyFlow)]
|
||||
pub struct WasmChaikinMoneyFlow {
|
||||
inner: wc::ChaikinMoneyFlow,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ChaikinMoneyFlow)]
|
||||
impl WasmChaikinMoneyFlow {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmChaikinMoneyFlow, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ChaikinMoneyFlow::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, volume)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
volume: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n || volume.len() != n {
|
||||
return Err(JsError::new(
|
||||
"high, low, close, volume must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], volume[i])?;
|
||||
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 = ChaikinOscillator)]
|
||||
pub struct WasmChaikinOscillator {
|
||||
inner: wc::ChaikinOscillator,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ChaikinOscillator)]
|
||||
impl WasmChaikinOscillator {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(fast: usize, slow: usize) -> Result<WasmChaikinOscillator, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ChaikinOscillator::new(fast, slow).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, volume)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
volume: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n || volume.len() != n {
|
||||
return Err(JsError::new(
|
||||
"high, low, close, volume must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], volume[i])?;
|
||||
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 = ForceIndex)]
|
||||
pub struct WasmForceIndex {
|
||||
inner: wc::ForceIndex,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ForceIndex)]
|
||||
impl WasmForceIndex {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmForceIndex, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ForceIndex::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, close: f64, volume: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(close, close, close, volume)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(&mut self, close: &[f64], volume: &[f64]) -> Result<Float64Array, JsError> {
|
||||
if close.len() != volume.len() {
|
||||
return Err(JsError::new("close and volume must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(close.len());
|
||||
for i in 0..close.len() {
|
||||
let c = make_candle(close[i], close[i], close[i], volume[i])?;
|
||||
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 = EaseOfMovement)]
|
||||
pub struct WasmEaseOfMovement {
|
||||
inner: wc::EaseOfMovement,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = EaseOfMovement)]
|
||||
impl WasmEaseOfMovement {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, divisor: f64) -> Result<WasmEaseOfMovement, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::EaseOfMovement::with_divisor(period, divisor).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, low, volume)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
volume: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || volume.len() != n {
|
||||
return Err(JsError::new("high, low, volume must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], low[i], volume[i])?;
|
||||
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 = NATR)]
|
||||
pub struct WasmNatr {
|
||||
inner: wc::Natr,
|
||||
|
||||
Reference in New Issue
Block a user