feat(family-13): add Ichimoku + Heikin-Ashi (#50)
Two new indicators in a brand-new "Ichimoku & alternative charts" family: - `Ichimoku` (Ichimoku Kinko Hyo): the full five-line cloud system (Tenkan-sen, Kijun-sen, Senkou Span A/B, Chikou Span). Classic (9, 26, 52, 26) defaults; configurable. Forward displacement is handled in an O(1) ring buffer so the visible Senkou A/B at bar n are the values computed at bar n-displacement. - `HeikinAshi`: recursive candle smoothing transform emitting a four-field synthetic candle. Seeds ha_open from (open+close)/2 on the first bar. Touchpoints: core + unit tests, mod.rs/lib.rs re-exports, Python + Node + WASM bindings (multi-output via PyArray2 / interleaved Vec<f64> / Object+Float64Array), Python tests across smoke/new-indicators/ input-validation, Node parity tests, fuzz target (Candle), benches, README family table + counter (71 -> 73, 8 -> 9 families), CHANGELOG. Note: Renko, Kagi, and Point & Figure from the family-13 ideas list are intentionally skipped. They are bar generators (the bar boundary is defined by price moves, not by a fixed time interval) rather than indicators that consume a candle stream, and belong in wickra-data as candle/tick transforms alongside the existing tick-to-candle aggregator and resampler.
This commit is contained in:
@@ -7746,3 +7746,197 @@ impl FamaNode {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Ichimoku ==============================
|
||||
|
||||
/// Ichimoku output: five lines, any of which may be `NaN` while the indicator
|
||||
/// is still warming up.
|
||||
#[napi(object)]
|
||||
pub struct IchimokuValue {
|
||||
pub tenkan: f64,
|
||||
pub kijun: f64,
|
||||
#[napi(js_name = "senkouA")]
|
||||
pub senkou_a: f64,
|
||||
#[napi(js_name = "senkouB")]
|
||||
pub senkou_b: f64,
|
||||
pub chikou: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "Ichimoku")]
|
||||
pub struct IchimokuNode {
|
||||
inner: wc::Ichimoku,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl IchimokuNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(
|
||||
tenkan_period: u32,
|
||||
kijun_period: u32,
|
||||
senkou_b_period: u32,
|
||||
displacement: u32,
|
||||
) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Ichimoku::new(
|
||||
tenkan_period as usize,
|
||||
kijun_period as usize,
|
||||
senkou_b_period as usize,
|
||||
displacement as usize,
|
||||
)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<IchimokuValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| IchimokuValue {
|
||||
tenkan: o.tenkan.unwrap_or(f64::NAN),
|
||||
kijun: o.kijun.unwrap_or(f64::NAN),
|
||||
senkou_a: o.senkou_a.unwrap_or(f64::NAN),
|
||||
senkou_b: o.senkou_b.unwrap_or(f64::NAN),
|
||||
chikou: o.chikou.unwrap_or(f64::NAN),
|
||||
}))
|
||||
}
|
||||
/// Returns `[tenkan0, kijun0, senkouA0, senkouB0, chikou0, tenkan1, ...]`,
|
||||
/// length `5 * n`. Cells without a defined value are `NaN`.
|
||||
#[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 n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 5];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
if let Some(v) = o.tenkan {
|
||||
out[i * 5] = v;
|
||||
}
|
||||
if let Some(v) = o.kijun {
|
||||
out[i * 5 + 1] = v;
|
||||
}
|
||||
if let Some(v) = o.senkou_a {
|
||||
out[i * 5 + 2] = v;
|
||||
}
|
||||
if let Some(v) = o.senkou_b {
|
||||
out[i * 5 + 3] = v;
|
||||
}
|
||||
if let Some(v) = o.chikou {
|
||||
out[i * 5 + 4] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Heikin-Ashi ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct HeikinAshiValue {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "HeikinAshi")]
|
||||
pub struct HeikinAshiNode {
|
||||
inner: wc::HeikinAshi,
|
||||
}
|
||||
|
||||
impl Default for HeikinAshiNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl HeikinAshiNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::HeikinAshi::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<HeikinAshiValue>> {
|
||||
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(c).map(|o| HeikinAshiValue {
|
||||
open: o.open,
|
||||
high: o.high,
|
||||
low: o.low,
|
||||
close: o.close,
|
||||
}))
|
||||
}
|
||||
/// Returns `[open0, high0, low0, close0, open1, ...]`, length `4 * n`.
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user