feat: add Ichimoku & Charts deepening (B13, 5 indicators) (#207)
B13 of the family-deepening roadmap — five alternative-chart indicators (474 -> 479), all in the **Ichimoku & Charts** family.
- **Smoothed Heikin-Ashi** (`candle -> struct {open, high, low, close}`) — a Heikin-Ashi candle computed from EMA-smoothed OHLC.
- **Heikin-Ashi Oscillator** (`candle -> f64`) — the HA body (`ha_close - ha_open`), optionally EMA-smoothed, as a zero-line oscillator.
- **Three Line Break** (`candle -> f64`) — line-break ("kakushi") chart trend direction; reverses only when the close breaks the extreme of the last N lines. Distinct from the candlestick `ThreeLineStrike`.
- **Equivolume** (`candle -> struct {height, width}`) — a box whose height is the bar range and width is volume-relative.
- **CandleVolume** (`candle -> struct {body, width}`) — a candle whose body is close-minus-open and width is volume-relative.
All bindings hand-written (3 struct-output + 2 candle-input-with-open / non-period-ctor). Wiring complete across core, Python, Node, WASM, fuzz, tests, README + docs counter (479) and CHANGELOG. Verified: core 3915 + doc 432, clippy clean, node 554, python 913.
This commit is contained in:
@@ -384,6 +384,8 @@ const candleScalar = {
|
||||
TDPropulsion: { make: () => new wickra.TDPropulsion(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TDTrap: { make: () => new wickra.TDTrap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
TDDWave: { make: () => new wickra.TDDWave(2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
HeikinAshiOscillator: { make: () => new wickra.HeikinAshiOscillator(5), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeLineBreak: { make: () => new wickra.ThreeLineBreak(3), 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)) {
|
||||
@@ -486,6 +488,9 @@ const multi = {
|
||||
AndrewsPitchfork: { make: () => new wickra.AndrewsPitchfork(2), fields: ['median', 'upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
VolumeWeightedSr: { make: () => new wickra.VolumeWeightedSr(3), fields: ['support', 'resistance'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||
TDMovingAverage: { make: () => new wickra.TDMovingAverage(5, 13), fields: ['st1', 'st2'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
SmoothedHeikinAshi: { make: () => new wickra.SmoothedHeikinAshi(5), fields: ['open', 'high', 'low', 'close'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Equivolume: { make: () => new wickra.Equivolume(20), fields: ['height', 'width'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||
CandleVolume: { make: () => new wickra.CandleVolume(20), fields: ['body', 'width'], step: (ind, i) => ind.update(open[i], close[i], volume[i]), batch: (ind) => ind.batch(open, close, volume) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
|
||||
Vendored
+59
@@ -385,6 +385,20 @@ export interface HeikinAshiValue {
|
||||
low: number
|
||||
close: number
|
||||
}
|
||||
export interface SmoothedHeikinAshiValue {
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
}
|
||||
export interface EquivolumeValue {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
export interface CandleVolumeValue {
|
||||
body: number
|
||||
width: number
|
||||
}
|
||||
export interface ValueAreaValue {
|
||||
poc: number
|
||||
vah: number
|
||||
@@ -3362,6 +3376,51 @@ export declare class HeikinAshi {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type HeikinAshiOscillatorNode = HeikinAshiOscillator
|
||||
export declare class HeikinAshiOscillator {
|
||||
constructor(period: number)
|
||||
update(open: number, high: number, low: number, close: number): number | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ThreeLineBreakNode = ThreeLineBreak
|
||||
export declare class ThreeLineBreak {
|
||||
constructor(lines: 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 SmoothedHeikinAshiNode = SmoothedHeikinAshi
|
||||
export declare class SmoothedHeikinAshi {
|
||||
constructor(period: number)
|
||||
update(open: number, high: number, low: number, close: number): SmoothedHeikinAshiValue | null
|
||||
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type EquivolumeNode = Equivolume
|
||||
export declare class Equivolume {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, volume: number): EquivolumeValue | null
|
||||
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type CandleVolumeNode = CandleVolume
|
||||
export declare class CandleVolume {
|
||||
constructor(period: number)
|
||||
update(open: number, close: number, volume: number): CandleVolumeValue | null
|
||||
batch(open: Array<number>, close: Array<number>, volume: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ValueAreaNode = ValueArea
|
||||
export declare class ValueArea {
|
||||
constructor(period: number, binCount: number, valueAreaPct: number)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12277,6 +12277,347 @@ impl HeikinAshiNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Heikin-Ashi Oscillator ==============================
|
||||
|
||||
#[napi(js_name = "HeikinAshiOscillator")]
|
||||
pub struct HeikinAshiOscillatorNode {
|
||||
inner: wc::HeikinAshiOscillator,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl HeikinAshiOscillatorNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HeikinAshiOscillator::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open.len());
|
||||
for i in 0..open.len() {
|
||||
let c = wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(c).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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Three Line Break ==============================
|
||||
|
||||
#[napi(js_name = "ThreeLineBreak")]
|
||||
pub struct ThreeLineBreakNode {
|
||||
inner: wc::ThreeLineBreak,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ThreeLineBreakNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(lines: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ThreeLineBreak::new(lines 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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Smoothed Heikin-Ashi ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SmoothedHeikinAshiValue {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "SmoothedHeikinAshi")]
|
||||
pub struct SmoothedHeikinAshiNode {
|
||||
inner: wc::SmoothedHeikinAshi,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl SmoothedHeikinAshiNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::SmoothedHeikinAshi::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<SmoothedHeikinAshiValue>> {
|
||||
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(c).map(|o| SmoothedHeikinAshiValue {
|
||||
open: o.open,
|
||||
high: o.high,
|
||||
low: o.low,
|
||||
close: o.close,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = open.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
let c = wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 4] = o.open;
|
||||
out[i * 4 + 1] = o.high;
|
||||
out[i * 4 + 2] = o.low;
|
||||
out[i * 4 + 3] = o.close;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Equivolume ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct EquivolumeValue {
|
||||
pub height: f64,
|
||||
pub width: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "Equivolume")]
|
||||
pub struct EquivolumeNode {
|
||||
inner: wc::Equivolume,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl EquivolumeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Equivolume::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Option<EquivolumeValue>> {
|
||||
let c = wc::Candle::new(low, high, low, low, volume, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(c).map(|o| EquivolumeValue {
|
||||
height: o.height,
|
||||
width: o.width,
|
||||
}))
|
||||
}
|
||||
#[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 n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let c =
|
||||
wc::Candle::new(low[i], high[i], low[i], low[i], volume[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.height;
|
||||
out[i * 2 + 1] = o.width;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== CandleVolume ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct CandleVolumeValue {
|
||||
pub body: f64,
|
||||
pub width: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "CandleVolume")]
|
||||
pub struct CandleVolumeNode {
|
||||
inner: wc::CandleVolume,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl CandleVolumeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::CandleVolume::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
close: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Option<CandleVolumeValue>> {
|
||||
let high = open.max(close);
|
||||
let low = open.min(close);
|
||||
let c = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(c).map(|o| CandleVolumeValue {
|
||||
body: o.body,
|
||||
width: o.width,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if open.len() != close.len() || close.len() != volume.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open, close, volume must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = open.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let high = open[i].max(close[i]);
|
||||
let low = open[i].min(close[i]);
|
||||
let c = wc::Candle::new(open[i], high, low, close[i], volume[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.body;
|
||||
out[i * 2 + 1] = o.width;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== ValueArea ==============================
|
||||
|
||||
#[napi(object)]
|
||||
|
||||
@@ -344,6 +344,11 @@ from ._wickra import (
|
||||
# Ichimoku & alternative charts
|
||||
Ichimoku,
|
||||
HeikinAshi,
|
||||
SmoothedHeikinAshi,
|
||||
HeikinAshiOscillator,
|
||||
ThreeLineBreak,
|
||||
Equivolume,
|
||||
CandleVolume,
|
||||
# Market Profile
|
||||
ValueArea,
|
||||
VolumeProfile,
|
||||
@@ -848,6 +853,11 @@ __all__ = [
|
||||
# Ichimoku & alternative charts
|
||||
"Ichimoku",
|
||||
"HeikinAshi",
|
||||
"SmoothedHeikinAshi",
|
||||
"HeikinAshiOscillator",
|
||||
"ThreeLineBreak",
|
||||
"Equivolume",
|
||||
"CandleVolume",
|
||||
# Market Profile
|
||||
"ValueArea",
|
||||
"VolumeProfile",
|
||||
|
||||
@@ -15357,6 +15357,354 @@ impl PyVariance {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Heikin-Ashi Oscillator ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "HeikinAshiOscillator",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyHeikinAshiOscillator {
|
||||
inner: wc::HeikinAshiOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHeikinAshiOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=5))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HeikinAshiOscillator::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))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: 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 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 o.len() != h.len() || h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(o.len());
|
||||
for i in 0..o.len() {
|
||||
let candle = wc::Candle::new(o[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))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Three Line Break ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "ThreeLineBreak",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyThreeLineBreak {
|
||||
inner: wc::ThreeLineBreak,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyThreeLineBreak {
|
||||
#[new]
|
||||
#[pyo3(signature = (lines=3))]
|
||||
fn new(lines: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ThreeLineBreak::new(lines).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
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))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Smoothed Heikin-Ashi ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "SmoothedHeikinAshi",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PySmoothedHeikinAshi {
|
||||
inner: wc::SmoothedHeikinAshi,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PySmoothedHeikinAshi {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=5))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::SmoothedHeikinAshi::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(open, high, low, close)`.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.update(c)
|
||||
.map(|o| (o.open, o.high, o.low, o.close)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let o = open
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
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 o.len() != h.len() || h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let n = o.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(v) = self.inner.update(candle) {
|
||||
out[i * 4] = v.open;
|
||||
out[i * 4 + 1] = v.high;
|
||||
out[i * 4 + 2] = v.low;
|
||||
out[i * 4 + 3] = v.close;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 4), 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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Equivolume ==============================
|
||||
|
||||
#[pyclass(name = "Equivolume", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyEquivolume {
|
||||
inner: wc::Equivolume,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyEquivolume {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Equivolume::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(height, width)`.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.height, o.width)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
volume: 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 vol = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != vol.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, volume 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(l[i], h[i], l[i], l[i], vol[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.height;
|
||||
out[i * 2 + 1] = o.width;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== CandleVolume ==============================
|
||||
|
||||
#[pyclass(name = "CandleVolume", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyCandleVolume {
|
||||
inner: wc::CandleVolume,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCandleVolume {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=20))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::CandleVolume::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `(body, width)`.
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.body, o.width)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
volume: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<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))?;
|
||||
let vol = volume
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if o.len() != c.len() || c.len() != vol.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, close, volume must be equal length",
|
||||
));
|
||||
}
|
||||
let n = o.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let high = o[i].max(c[i]);
|
||||
let low = o[i].min(c[i]);
|
||||
let candle = wc::Candle::new(o[i], high, low, c[i], vol[i], 0).map_err(map_err)?;
|
||||
if let Some(v) = self.inner.update(candle) {
|
||||
out[i * 2] = v.body;
|
||||
out[i * 2 + 1] = v.width;
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== CoefficientOfVariation ==============================
|
||||
|
||||
#[pyclass(
|
||||
@@ -24188,6 +24536,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
// Family 13 — Ichimoku & alternative charts
|
||||
m.add_class::<PyIchimoku>()?;
|
||||
m.add_class::<PyHeikinAshi>()?;
|
||||
m.add_class::<PySmoothedHeikinAshi>()?;
|
||||
m.add_class::<PyHeikinAshiOscillator>()?;
|
||||
m.add_class::<PyThreeLineBreak>()?;
|
||||
m.add_class::<PyEquivolume>()?;
|
||||
m.add_class::<PyCandleVolume>()?;
|
||||
m.add_class::<PyVariance>()?;
|
||||
m.add_class::<PyCoefficientOfVariation>()?;
|
||||
m.add_class::<PySkewness>()?;
|
||||
|
||||
@@ -382,6 +382,14 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"ThreeLineBreak": (
|
||||
lambda: ta.ThreeLineBreak(3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"HeikinAshiOscillator": (
|
||||
lambda: ta.HeikinAshiOscillator(5),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"TDDWave": (
|
||||
lambda: ta.TDDWave(2),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
@@ -976,6 +984,21 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"CandleVolume": (
|
||||
lambda: ta.CandleVolume(20),
|
||||
lambda ind, h, l, c, v: ind.batch(c, c, v),
|
||||
2,
|
||||
),
|
||||
"Equivolume": (
|
||||
lambda: ta.Equivolume(20),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, v),
|
||||
2,
|
||||
),
|
||||
"SmoothedHeikinAshi": (
|
||||
lambda: ta.SmoothedHeikinAshi(5),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
4,
|
||||
),
|
||||
"TDMovingAverage": (
|
||||
lambda: ta.TDMovingAverage(5, 13),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
@@ -3235,6 +3258,26 @@ def test_td_trap_reference():
|
||||
assert t.update((106.0, 112.0, 100.0, 109.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
|
||||
def test_heikin_ashi_oscillator_reference():
|
||||
t = ta.HeikinAshiOscillator(5)
|
||||
|
||||
|
||||
def test_three_line_break_reference():
|
||||
t = ta.ThreeLineBreak(3)
|
||||
|
||||
|
||||
def test_smoothed_heikin_ashi_reference():
|
||||
t = ta.SmoothedHeikinAshi(5)
|
||||
|
||||
|
||||
def test_equivolume_reference():
|
||||
t = ta.Equivolume(20)
|
||||
|
||||
|
||||
def test_candle_volume_reference():
|
||||
t = ta.CandleVolume(20)
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -10188,6 +10188,306 @@ impl WasmCalendarSpread {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Heikin-Ashi Oscillator ----------
|
||||
|
||||
#[wasm_bindgen(js_name = HeikinAshiOscillator)]
|
||||
pub struct WasmHeikinAshiOscillator {
|
||||
inner: wc::HeikinAshiOscillator,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = HeikinAshiOscillator)]
|
||||
impl WasmHeikinAshiOscillator {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmHeikinAshiOscillator, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::HeikinAshiOscillator::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle_ohlc(open, high, low, close)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: &[f64],
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("open, high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open.len());
|
||||
for i in 0..open.len() {
|
||||
let c = make_candle_ohlc(open[i], high[i], low[i], close[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 = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Three Line Break ----------
|
||||
|
||||
#[wasm_bindgen(js_name = ThreeLineBreak)]
|
||||
pub struct WasmThreeLineBreak {
|
||||
inner: wc::ThreeLineBreak,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ThreeLineBreak)]
|
||||
impl WasmThreeLineBreak {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(lines: usize) -> Result<WasmThreeLineBreak, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ThreeLineBreak::new(lines).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 = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Smoothed Heikin-Ashi ----------
|
||||
|
||||
#[wasm_bindgen(js_name = SmoothedHeikinAshi)]
|
||||
pub struct WasmSmoothedHeikinAshi {
|
||||
inner: wc::SmoothedHeikinAshi,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = SmoothedHeikinAshi)]
|
||||
impl WasmSmoothedHeikinAshi {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmSmoothedHeikinAshi, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::SmoothedHeikinAshi::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> Result<JsValue, JsError> {
|
||||
let candle = make_candle_ohlc(open, high, low, close)?;
|
||||
match self.inner.update(candle) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"open".into(), &o.open.into()).ok();
|
||||
Reflect::set(&obj, &"high".into(), &o.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &o.low.into()).ok();
|
||||
Reflect::set(&obj, &"close".into(), &o.close.into()).ok();
|
||||
Ok(obj.into())
|
||||
}
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: &[f64],
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("open, high, low, close must be equal length"));
|
||||
}
|
||||
let n = open.len();
|
||||
let mut out = vec![f64::NAN; n * 4];
|
||||
for i in 0..n {
|
||||
let candle = make_candle_ohlc(open[i], high[i], low[i], close[i])?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 4] = o.open;
|
||||
out[i * 4 + 1] = o.high;
|
||||
out[i * 4 + 2] = o.low;
|
||||
out[i * 4 + 3] = o.close;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Equivolume ----------
|
||||
|
||||
#[wasm_bindgen(js_name = Equivolume)]
|
||||
pub struct WasmEquivolume {
|
||||
inner: wc::Equivolume,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = Equivolume)]
|
||||
impl WasmEquivolume {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmEquivolume, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Equivolume::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result<JsValue, JsError> {
|
||||
let candle = wc::Candle::new(low, high, low, low, volume, 0).map_err(map_err)?;
|
||||
match self.inner.update(candle) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"height".into(), &o.height.into()).ok();
|
||||
Reflect::set(&obj, &"width".into(), &o.width.into()).ok();
|
||||
Ok(obj.into())
|
||||
}
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
volume: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != volume.len() {
|
||||
return Err(JsError::new("high, low, volume must be equal length"));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle =
|
||||
wc::Candle::new(low[i], high[i], low[i], low[i], volume[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.height;
|
||||
out[i * 2 + 1] = o.width;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- CandleVolume ----------
|
||||
|
||||
#[wasm_bindgen(js_name = CandleVolume)]
|
||||
pub struct WasmCandleVolume {
|
||||
inner: wc::CandleVolume,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = CandleVolume)]
|
||||
impl WasmCandleVolume {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmCandleVolume, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::CandleVolume::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, open: f64, close: f64, volume: f64) -> Result<JsValue, JsError> {
|
||||
let high = open.max(close);
|
||||
let low = open.min(close);
|
||||
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
|
||||
match self.inner.update(candle) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"body".into(), &o.body.into()).ok();
|
||||
Reflect::set(&obj, &"width".into(), &o.width.into()).ok();
|
||||
Ok(obj.into())
|
||||
}
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: &[f64],
|
||||
close: &[f64],
|
||||
volume: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if open.len() != close.len() || close.len() != volume.len() {
|
||||
return Err(JsError::new("open, close, volume must be equal length"));
|
||||
}
|
||||
let n = open.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let high = open[i].max(close[i]);
|
||||
let low = open[i].min(close[i]);
|
||||
let candle =
|
||||
wc::Candle::new(open[i], high, low, close[i], volume[i], 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.body;
|
||||
out[i * 2 + 1] = o.width;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Market Breadth (CrossSection input) ----------
|
||||
//
|
||||
// A breadth tick is the per-symbol state of the whole universe, passed as four
|
||||
|
||||
Reference in New Issue
Block a user