feat(family-11): add DeMark suite (TD Setup, Sequential, DeMarker, REI, Pressure) (#48)

* feat(family-11): add DeMark suite (TD Setup, Sequential, DeMarker, REI, Pressure)

Family 11 (DeMark) was previously empty; this PR adds five
streaming-first DeMark indicators in one batch.

- **TD Setup** (`TdSetup`): parameterised buy/sell setup counter.
  Counts consecutive bars whose close is less-than (buy) or
  greater-than (sell) the close `lookback` bars earlier, saturating
  at `target`. Emits a signed `f64` so callers read direction from
  the sign and run length from the magnitude. Classic config:
  `lookback = 4`, `target = 9`.

- **TD Sequential** (`TdSequential`): the canonical Setup + Countdown
  exhaustion pattern. Output struct `{ setup, countdown, direction }`
  exposes both phase counts as signed numbers plus the active
  countdown direction (+1 buy / -1 sell / 0 none). Countdown
  activates when a setup completes and tracks the close-vs-high/low
  comparison `countdown_lookback` bars back, capped at
  `countdown_target`. Classic: 4/9/2/13.

- **TD DeMarker** (`TdDeMarker`): bounded [0, 1] oscillator from the
  rolling average of upward high expansion (DeMax) and downward low
  expansion (DeMin). Falls back to the neutral 0.5 on a flat market
  (denominator zero).

- **TD REI** (`TdRei`): Range Expansion Index, bounded [-100, 100].
  Per-bar numerator gated on a range-overlap condition vs the bars
  5 and 6 back, normalised by a `period`-bar sum of absolute moves.
  Classic period = 5. Saturates at +100 in a slow steady uptrend
  and at -100 in the mirror downtrend; emits 0 on a flat market.

- **TD Pressure** (`TdPressure`): volume-weighted buying / selling
  pressure normalised to [-100, 100]. Per-bar pressure is the
  intra-bar close-vs-open ratio scaled by volume; the output is the
  rolling mean divided by the rolling mean volume. Zero-range bars
  contribute zero (avoid the undefined ratio) and a flat zero-volume
  window falls back to 0.

Bindings: all five exposed in Python (`ta.TDSetup`, `ta.TDSequential`,
`ta.TDDeMarker`, `ta.TDREI`, `ta.TDPressure`), Node (`wickra.TDSetup`
etc.), and WASM. Multi-output classes (`TDSequential`) return either
a struct `{ setup, countdown, direction }` per bar (streaming) or a
flat interleaved Float64Array of length `3 * n` (batch).

Tests: 47 unit tests across the five new core files (pure-trend
saturation, flat-market neutral fallback, batch-equals-streaming,
zero-parameter rejection, reset semantics, accessors). Python
test_new_indicators.py picks up all five plus a multi-output TD
Sequential block. Node indicators.test.js picks up all five.
Reference values added to test_known_values.py.

Fuzz: candle fuzz target sweeps all five DeMark indicators with the
existing `Vec<f64>` -> `Vec<Candle>` driver.

Benches: BTCUSDT 1-minute dataset benches for each DeMark indicator
in `crates/wickra/benches/indicators.rs`.

Docs: README family table gains a "DeMark" row; indicator counter
bumped 71 -> 76. CHANGELOG entry added under [Unreleased]. Wiki
drafts (deep-dive pages + Sidebar / Overview / Warmup-Periods / Home
deltas) live under `indicator-ideas/families/wiki/family-11-demark/`
for manual merge into the wiki repo.

* feat(family-11): add 7 missing DeMark indicators

Complete the DeMark suite (family 11) with the seven indicators not
covered by the first commit: TD Combo, TD Countdown, TD Lines (TDST),
TD Range Projection, TD Differential, TD Open, and TD Risk Level.

- TdCombo: aggressive countdown variant with three strictness rules
  on top of the classic close-vs-low/high lookback rule (monotone
  low/high, monotone close vs prior bar).
- TdCountdown: standalone 13-bar countdown packaging only the signed
  countdown count (the setup machine runs internally).
- TdLines: TDST horizontal support/resistance levels from the
  highest-high / lowest-low bars of the most-recently-completed
  setup, exposed as a multi-output struct.
- TdRangeProjection: DeMark X-projection of the next bar's high and
  low from the current bar's OHLC via an open-vs-close-weighted
  pivot (three branches: close<open, close>open, close==open).
- TdDifferential: two-bar buying-pressure vs selling-pressure
  reversal pattern emitting +1/-1/0.
- TdOpen: gap-and-fade reversal pattern (open outside prior range
  with subsequent recovery into it) emitting +1/-1/0.
- TdRiskLevel: protective stop levels derived from the setup
  extreme bar +/- its true range.

All seven are wired through Rust core, Python, Node and WASM
bindings, registered in the candle-stream fuzz target, given
benchmark entries on the BTCUSDT 1-minute dataset, and covered by
streaming-vs-batch equivalence, reference-value, lifecycle and
input-validation tests on the Python and Node sides. README counter
moves 76 -> 83 and the CHANGELOG "family 11" entry is extended to
list all twelve indicators.

* fix(td_risk_level tests): check first emission at idx 12, not last bar

TdRiskLevel re-ratchets the sell-risk level on each subsequent setup
completion, so a strictly rising series produces 22.0 at idx 19 (latest
setup) rather than 15.0 (first setup). The test comment already named
idx 12 as the reference; switch the assertion from out[-1] to out[12]
to match the reference computation.

* test(family-11): cover buy-direction branches in TD indicators

Add downtrend tests to TdSequential, TdCombo and TdCountdown so the
buy-side countdown/combo increment branches are exercised; remove an
empty `if buy_countdown == target {}` block in TdSequential whose
behavior is already enforced by the outer strict `<` guard.

Closes codecov/patch gaps reported on PR #48 (10 missed lines across
the three files).
This commit is contained in:
kingchenc
2026-05-25 20:36:36 +02:00
committed by GitHub
parent 7e1e988596
commit 4f9ed34884
26 changed files with 6130 additions and 17 deletions
+690
View File
@@ -4645,6 +4645,696 @@ impl WasmZigZag {
self.inner.warmup_period()
}
}
// ---------- TD Setup ----------
#[wasm_bindgen(js_name = TDSetup)]
pub struct WasmTdSetup {
inner: wc::TdSetup,
}
#[wasm_bindgen(js_class = TDSetup)]
impl WasmTdSetup {
#[wasm_bindgen(constructor)]
pub fn new(lookback: usize, target: usize) -> Result<WasmTdSetup, JsError> {
Ok(Self {
inner: wc::TdSetup::new(lookback, target).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()
}
}
// ---------- TD Sequential ----------
#[wasm_bindgen(js_name = TDSequential)]
pub struct WasmTdSequential {
inner: wc::TdSequential,
}
#[wasm_bindgen(js_class = TDSequential)]
impl WasmTdSequential {
#[wasm_bindgen(constructor)]
pub fn new(
setup_lookback: usize,
setup_target: usize,
countdown_lookback: usize,
countdown_target: usize,
) -> Result<WasmTdSequential, JsError> {
Ok(Self {
inner: wc::TdSequential::new(
setup_lookback,
setup_target,
countdown_lookback,
countdown_target,
)
.map_err(map_err)?,
})
}
/// Streaming update. Returns `{ setup, countdown, direction }` once warm,
/// else `null`.
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"setup".into(), &o.setup.into()).ok();
Reflect::set(&obj, &"countdown".into(), &o.countdown.into()).ok();
Reflect::set(&obj, &"direction".into(), &o.direction.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
/// Batch returns a flat `Float64Array` `[setup0, countdown0, direction0, ...]`.
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 n = high.len();
let mut out = vec![f64::NAN; n * 3];
for i in 0..n {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
if let Some(o) = self.inner.update(c) {
out[i * 3] = o.setup;
out[i * 3 + 1] = o.countdown;
out[i * 3 + 2] = o.direction;
}
}
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()
}
}
// ---------- TD DeMarker ----------
#[wasm_bindgen(js_name = TDDeMarker)]
pub struct WasmTdDeMarker {
inner: wc::TdDeMarker,
}
#[wasm_bindgen(js_class = TDDeMarker)]
impl WasmTdDeMarker {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmTdDeMarker, JsError> {
Ok(Self {
inner: wc::TdDeMarker::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, low, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
if high.len() != low.len() {
return Err(JsError::new("high and low 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], low[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()
}
}
// ---------- TD REI ----------
#[wasm_bindgen(js_name = TDREI)]
pub struct WasmTdRei {
inner: wc::TdRei,
}
#[wasm_bindgen(js_class = TDREI)]
impl WasmTdRei {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmTdRei, JsError> {
Ok(Self {
inner: wc::TdRei::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, low, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
if high.len() != low.len() {
return Err(JsError::new("high and low 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], low[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()
}
}
// ---------- TD Pressure ----------
#[wasm_bindgen(js_name = TDPressure)]
pub struct WasmTdPressure {
inner: wc::TdPressure,
}
#[wasm_bindgen(js_class = TDPressure)]
impl WasmTdPressure {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmTdPressure, JsError> {
Ok(Self {
inner: wc::TdPressure::new(period).map_err(map_err)?,
})
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> Result<Option<f64>, JsError> {
let c = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
open: &[f64],
high: &[f64],
low: &[f64],
close: &[f64],
volume: &[f64],
) -> Result<Float64Array, JsError> {
if open.len() != high.len()
|| high.len() != low.len()
|| low.len() != close.len()
|| close.len() != volume.len()
{
return Err(JsError::new(
"open, high, low, close, volume must be equal length",
));
}
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], volume[i], 0)
.map_err(map_err)?;
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()
}
}
// ---------- TD Combo ----------
#[wasm_bindgen(js_name = TDCombo)]
pub struct WasmTdCombo {
inner: wc::TdCombo,
}
#[wasm_bindgen(js_class = TDCombo)]
impl WasmTdCombo {
#[wasm_bindgen(constructor)]
pub fn new(
setup_lookback: usize,
setup_target: usize,
countdown_lookback: usize,
countdown_target: usize,
) -> Result<WasmTdCombo, JsError> {
Ok(Self {
inner: wc::TdCombo::new(
setup_lookback,
setup_target,
countdown_lookback,
countdown_target,
)
.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()
}
}
// ---------- TD Countdown ----------
#[wasm_bindgen(js_name = TDCountdown)]
pub struct WasmTdCountdown {
inner: wc::TdCountdown,
}
#[wasm_bindgen(js_class = TDCountdown)]
impl WasmTdCountdown {
#[wasm_bindgen(constructor)]
pub fn new(
setup_lookback: usize,
setup_target: usize,
countdown_lookback: usize,
countdown_target: usize,
) -> Result<WasmTdCountdown, JsError> {
Ok(Self {
inner: wc::TdCountdown::new(
setup_lookback,
setup_target,
countdown_lookback,
countdown_target,
)
.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()
}
}
// ---------- TD Lines ----------
#[wasm_bindgen(js_name = TDLines)]
pub struct WasmTdLines {
inner: wc::TdLines,
}
#[wasm_bindgen(js_class = TDLines)]
impl WasmTdLines {
#[wasm_bindgen(constructor)]
pub fn new(lookback: usize, target: usize) -> Result<WasmTdLines, JsError> {
Ok(Self {
inner: wc::TdLines::new(lookback, target).map_err(map_err)?,
})
}
/// Streaming update. Returns `{ resistance, support }` once warm, else `null`.
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"resistance".into(), &o.resistance.into()).ok();
Reflect::set(&obj, &"support".into(), &o.support.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
/// Batch returns a flat `Float64Array` `[resistance0, support0, ...]`.
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 n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.resistance;
out[i * 2 + 1] = o.support;
}
}
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()
}
}
// ---------- TD Range Projection ----------
#[wasm_bindgen(js_name = TDRangeProjection)]
pub struct WasmTdRangeProjection {
inner: wc::TdRangeProjection,
}
#[wasm_bindgen(js_class = TDRangeProjection)]
impl WasmTdRangeProjection {
#[wasm_bindgen(constructor)]
#[allow(clippy::new_without_default)]
pub fn new() -> WasmTdRangeProjection {
Self {
inner: wc::TdRangeProjection::new(),
}
}
/// Streaming update. Returns `{ high, low }` projected for the next bar.
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> Result<JsValue, JsError> {
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"high".into(), &o.high.into()).ok();
Reflect::set(&obj, &"low".into(), &o.low.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
/// Batch returns a flat `Float64Array` `[projHigh0, projLow0, ...]`.
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 * 2];
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(p) = self.inner.update(c) {
out[i * 2] = p.high;
out[i * 2 + 1] = p.low;
}
}
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()
}
}
// ---------- TD Differential ----------
#[wasm_bindgen(js_name = TDDifferential)]
pub struct WasmTdDifferential {
inner: wc::TdDifferential,
}
#[wasm_bindgen(js_class = TDDifferential)]
impl WasmTdDifferential {
#[wasm_bindgen(constructor)]
#[allow(clippy::new_without_default)]
pub fn new() -> WasmTdDifferential {
Self {
inner: wc::TdDifferential::new(),
}
}
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()
}
}
// ---------- TD Open ----------
#[wasm_bindgen(js_name = TDOpen)]
pub struct WasmTdOpen {
inner: wc::TdOpen,
}
#[wasm_bindgen(js_class = TDOpen)]
impl WasmTdOpen {
#[wasm_bindgen(constructor)]
#[allow(clippy::new_without_default)]
pub fn new() -> WasmTdOpen {
Self {
inner: wc::TdOpen::new(),
}
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> Result<Option<f64>, JsError> {
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
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 = 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(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()
}
}
// ---------- TD Risk Level ----------
#[wasm_bindgen(js_name = TDRiskLevel)]
pub struct WasmTdRiskLevel {
inner: wc::TdRiskLevel,
}
#[wasm_bindgen(js_class = TDRiskLevel)]
impl WasmTdRiskLevel {
#[wasm_bindgen(constructor)]
pub fn new(lookback: usize, target: usize) -> Result<WasmTdRiskLevel, JsError> {
Ok(Self {
inner: wc::TdRiskLevel::new(lookback, target).map_err(map_err)?,
})
}
/// Streaming update. Returns `{ buyRisk, sellRisk }` once warm, else `null`.
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(match self.inner.update(c) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"buyRisk".into(), &o.buy_risk.into()).ok();
Reflect::set(&obj, &"sellRisk".into(), &o.sell_risk.into()).ok();
obj.into()
}
None => JsValue::NULL,
})
}
/// Batch returns a flat `Float64Array` `[buyRisk0, sellRisk0, ...]`.
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 n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.buy_risk;
out[i * 2 + 1] = o.sell_risk;
}
}
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()
}
}
#[cfg(test)]
mod tests {
use super::*;