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:
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user