feat: add Alt-Chart Bars (Renko, Kagi, Point & Figure) via a BarBuilder trait (#146)

Introduces a BarBuilder trait for price-driven chart constructors that emit a variable number of bars per candle (deliberately not Indicator). Adds Renko (box-size bricks, 2-box reversal), Kagi (reversal-amount segments) and Point & Figure (box-size X/O columns, N-box reversal) in a new Alt-Chart Bars family, with custom Python/Node/WASM bindings, a dedicated fuzz target, tests and docs. Indicator count 292 -> 295.
This commit is contained in:
kingchenc
2026-06-02 21:56:00 +02:00
committed by GitHub
parent f37eedd44e
commit d4b3f9dbd1
20 changed files with 1692 additions and 43 deletions
+156 -1
View File
@@ -12,7 +12,7 @@
use js_sys::{Array, Float64Array, Object, Reflect};
use wasm_bindgen::prelude::*;
use wickra_core as wc;
use wickra_core::{BatchExt, Indicator};
use wickra_core::{BarBuilder, BatchExt, Indicator};
fn map_err(e: wc::Error) -> JsError {
JsError::new(&e.to_string())
@@ -8517,6 +8517,161 @@ impl WasmInformationRatio {
}
}
// ============================== Alt-Chart Bars ==============================
//
// Bar builders consume close prices and emit a variable number of completed bars
// per input. `update(close)` returns a JS array of the bars finished on that
// close; `batch` returns all completed bars concatenated.
#[wasm_bindgen(js_name = RenkoBars)]
pub struct WasmRenkoBars {
inner: wc::RenkoBars,
}
#[wasm_bindgen(js_class = RenkoBars)]
impl WasmRenkoBars {
#[wasm_bindgen(constructor)]
pub fn new(box_size: f64) -> Result<WasmRenkoBars, JsError> {
Ok(Self {
inner: wc::RenkoBars::new(box_size).map_err(map_err)?,
})
}
/// Returns an array of `{ open, close, direction }` bricks completed on this close.
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
let arr = Array::new();
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
let arr = Array::new();
for &price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"open".into(), &b.open.into()).ok();
Reflect::set(&obj, &"close".into(), &b.close.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
}
Ok(arr)
}
#[wasm_bindgen(js_name = boxSize)]
pub fn box_size(&self) -> f64 {
self.inner.box_size()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = KagiBars)]
pub struct WasmKagiBars {
inner: wc::KagiBars,
}
#[wasm_bindgen(js_class = KagiBars)]
impl WasmKagiBars {
#[wasm_bindgen(constructor)]
pub fn new(reversal: f64) -> Result<WasmKagiBars, JsError> {
Ok(Self {
inner: wc::KagiBars::new(reversal).map_err(map_err)?,
})
}
/// Returns an array of `{ start, end, direction }` segments completed on this close.
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
let arr = Array::new();
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"start".into(), &b.start.into()).ok();
Reflect::set(&obj, &"end".into(), &b.end.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
let arr = Array::new();
for &price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for b in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"start".into(), &b.start.into()).ok();
Reflect::set(&obj, &"end".into(), &b.end.into()).ok();
Reflect::set(&obj, &"direction".into(), &f64::from(b.direction).into()).ok();
arr.push(&obj);
}
}
Ok(arr)
}
pub fn reversal(&self) -> f64 {
self.inner.reversal()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = PointAndFigureBars)]
pub struct WasmPointAndFigureBars {
inner: wc::PointAndFigureBars,
}
#[wasm_bindgen(js_class = PointAndFigureBars)]
impl WasmPointAndFigureBars {
#[wasm_bindgen(constructor)]
pub fn new(box_size: f64, reversal: usize) -> Result<WasmPointAndFigureBars, JsError> {
Ok(Self {
inner: wc::PointAndFigureBars::new(box_size, reversal).map_err(map_err)?,
})
}
/// Returns an array of `{ direction, high, low }` columns completed on this close.
pub fn update(&mut self, close: f64) -> Result<Array, JsError> {
let candle = wc::Candle::new(close, close, close, close, 1.0, 0).map_err(map_err)?;
let arr = Array::new();
for col in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"direction".into(), &f64::from(col.direction).into()).ok();
Reflect::set(&obj, &"high".into(), &col.high.into()).ok();
Reflect::set(&obj, &"low".into(), &col.low.into()).ok();
arr.push(&obj);
}
Ok(arr)
}
pub fn batch(&mut self, close: &[f64]) -> Result<Array, JsError> {
let arr = Array::new();
for &price in close {
let candle = wc::Candle::new(price, price, price, price, 1.0, 0).map_err(map_err)?;
for col in self.inner.update(candle) {
let obj = Object::new();
Reflect::set(&obj, &"direction".into(), &f64::from(col.direction).into()).ok();
Reflect::set(&obj, &"high".into(), &col.high.into()).ok();
Reflect::set(&obj, &"low".into(), &col.low.into()).ok();
arr.push(&obj);
}
}
Ok(arr)
}
#[wasm_bindgen(js_name = boxSize)]
pub fn box_size(&self) -> f64 {
self.inner.box_size()
}
pub fn reversal(&self) -> usize {
self.inner.reversal()
}
pub fn reset(&mut self) {
self.inner.reset();
}
}
#[wasm_bindgen(js_name = Alpha)]
pub struct WasmAlpha {
inner: wc::Alpha,