Add B7 Trailing Stops family (6 indicators) (#193)
Adds the **Trailing Stops** family deepening (B7), six new indicators (434 -> 440):
- **KaseDevStop** — Cynthia Kase's volatility stop on the standard deviation of the two-bar true range.
- **ElderSafeZone** — Alexander Elder's stop offset by a multiple of average market noise.
- **AtrRatchet** — Kaufman ATR ratchet that tightens its multiple by a per-bar increment.
- **Nrtr** — Nick Rypock Trailing Reverse (percentage band).
- **TimeBasedStop** — exits after a fixed number of bars (scalar fraction of elapsed life).
- **ModifiedMaStop** — moving-average based trailing stop.
("Wilder Volatility System" is intentionally skipped — it overlaps the existing VoltyStop/Psar/SarExt.)
Each takes Candle input; the five band/structure stops emit a {value, direction} struct, TimeBasedStop a scalar. Wired across core, Python/Node/WASM bindings, fuzz target and tests. Verified locally: 3560 core lib + 398 doc tests, clippy clean, 515 node tests, 852 pytest, counter 440.
This commit is contained in:
@@ -2552,6 +2552,44 @@ impl WasmProjectionOscillator {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = TimeBasedStop)]
|
||||
pub struct WasmTimeBasedStop {
|
||||
inner: wc::TimeBasedStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = TimeBasedStop)]
|
||||
impl WasmTimeBasedStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(max_bars: usize) -> Result<WasmTimeBasedStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::TimeBasedStop::new(max_bars).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 = Stochastic)]
|
||||
pub struct WasmStoch {
|
||||
inner: wc::Stochastic,
|
||||
@@ -3833,6 +3871,275 @@ impl WasmRenkoTrailingStop {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = KaseDevStop)]
|
||||
pub struct WasmKaseDevStop {
|
||||
inner: wc::KaseDevStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = KaseDevStop)]
|
||||
impl WasmKaseDevStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, dev: f64) -> Result<WasmKaseDevStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::KaseDevStop::new(period, dev).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ value, 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, &"value".into(), &o.value.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &o.direction.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup is NaN.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
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.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ElderSafeZone)]
|
||||
pub struct WasmElderSafeZone {
|
||||
inner: wc::ElderSafeZone,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ElderSafeZone)]
|
||||
impl WasmElderSafeZone {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize, coeff: f64) -> Result<WasmElderSafeZone, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ElderSafeZone::new(period, coeff).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ value, 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, &"value".into(), &o.value.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &o.direction.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup is NaN.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
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.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = AtrRatchet)]
|
||||
pub struct WasmAtrRatchet {
|
||||
inner: wc::AtrRatchet,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = AtrRatchet)]
|
||||
impl WasmAtrRatchet {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
atr_period: usize,
|
||||
start_mult: f64,
|
||||
increment: f64,
|
||||
) -> Result<WasmAtrRatchet, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::AtrRatchet::new(atr_period, start_mult, increment).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ value, 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, &"value".into(), &o.value.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &o.direction.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup is NaN.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
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.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = Nrtr)]
|
||||
pub struct WasmNrtr {
|
||||
inner: wc::Nrtr,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = Nrtr)]
|
||||
impl WasmNrtr {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(pct: f64) -> Result<WasmNrtr, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Nrtr::new(pct).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ value, 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, &"value".into(), &o.value.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &o.direction.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup is NaN.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
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.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ModifiedMaStop)]
|
||||
pub struct WasmModifiedMaStop {
|
||||
inner: wc::ModifiedMaStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ModifiedMaStop)]
|
||||
impl WasmModifiedMaStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmModifiedMaStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::ModifiedMaStop::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ value, 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, &"value".into(), &o.value.into()).ok();
|
||||
Reflect::set(&obj, &"direction".into(), &o.direction.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup is NaN.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
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.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = TypicalPrice)]
|
||||
pub struct WasmTypicalPrice {
|
||||
inner: wc::TypicalPrice,
|
||||
|
||||
Reference in New Issue
Block a user