feat(family-09): add 7 trailing stops (HiLo, Volty, Yo-Yo, Donchian, Pct, Step, Renko) (#46)
* feat(family-09): add 7 trailing stops (HiLo, Volty, Yo-Yo, Donchian, Pct, Step, Renko)
Rounds out the Trailing Stops family from 5 to 12 indicators:
- HiLoActivator (Crabel): SMA-of-high/SMA-of-low trail with a one-bar
lag; emits the opposite-side SMA as the trailing stop.
- VoltyStop (Cynthia Kase): ATR trail anchored on the extreme close
since the trade was opened — tighter than AtrTrailingStop on
pullbacks.
- YoyoExit: long-only ATR trail with an explicit re-entry trigger at
trail + multiplier*ATR; exposes an in_trade flag.
- DonchianStop (Turtle): lowest low / highest high over the window;
multi-output {stop_long, stop_short}.
- PercentageTrailingStop: fixed-percent trail that scales across
instruments without per-asset tuning.
- StepTrailingStop: snaps to a step_size-aligned grid; mirrors
discretionary stop-by-hand workflow.
- RenkoTrailingStop: block-anchored trail; only moves on full-block
advances, ignores intra-block noise.
All seven are wired into wickra-core, the Python / Node / WASM
bindings, the indicator_update + indicator_update_candle fuzz targets,
the wickra bench harness, and the Python + Node test suites. README
counter bumps from 71 to 78; CHANGELOG entry under [Unreleased].
* fix(family-09): satisfy pedantic clippy lints
- hilo_activator: rewrite match-Some/None as if-let-else (single_match_else),
add backticks around the HiLo identifier in module/struct doc (doc_markdown).
- percentage / step / renko trailing stop tests: use f64::from(i32) instead
of `as f64` (cast_lossless).
- bench `benches()` is now >100 lines after Family 09 was wired in; allow
too_many_lines (matches the python pymodule fn).
This commit is contained in:
@@ -1829,6 +1829,251 @@ impl WasmAtrTrailingStop {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Trailing Stops (family 09) ----------
|
||||
|
||||
#[wasm_bindgen(js_name = HiLoActivator)]
|
||||
pub struct WasmHiLoActivator {
|
||||
inner: wc::HiLoActivator,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = HiLoActivator)]
|
||||
impl WasmHiLoActivator {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmHiLoActivator, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::HiLoActivator::new(period).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> {
|
||||
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::with_capacity(n);
|
||||
for i in 0..n {
|
||||
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 = VoltyStop)]
|
||||
pub struct WasmVoltyStop {
|
||||
inner: wc::VoltyStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = VoltyStop)]
|
||||
impl WasmVoltyStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(atr_period: usize, multiplier: f64) -> Result<WasmVoltyStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::VoltyStop::new(atr_period, multiplier).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> {
|
||||
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::with_capacity(n);
|
||||
for i in 0..n {
|
||||
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 = YoyoExit)]
|
||||
pub struct WasmYoyoExit {
|
||||
inner: wc::YoyoExit,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = YoyoExit)]
|
||||
impl WasmYoyoExit {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(atr_period: usize, multiplier: f64) -> Result<WasmYoyoExit, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::YoyoExit::new(atr_period, multiplier).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> {
|
||||
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::with_capacity(n);
|
||||
for i in 0..n {
|
||||
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 = inTrade)]
|
||||
pub fn in_trade(&self) -> bool {
|
||||
self.inner.in_trade()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = DonchianStop)]
|
||||
pub struct WasmDonchianStop {
|
||||
inner: wc::DonchianStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = DonchianStop)]
|
||||
impl WasmDonchianStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmDonchianStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::DonchianStop::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `{ stopLong, stopShort }` once warm, else `null`.
|
||||
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, low, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"stopLong".into(), &o.stop_long.into()).ok();
|
||||
Reflect::set(&obj, &"stopShort".into(), &o.stop_short.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[long0, short0, long1, short1, ...]`, length `2 * n`. Warmup is NaN.
|
||||
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n {
|
||||
return Err(JsError::new("high and low 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], low[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.stop_long;
|
||||
out[i * 2 + 1] = o.stop_short;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = PercentageTrailingStop)]
|
||||
pub struct WasmPercentageTrailingStop {
|
||||
inner: wc::PercentageTrailingStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = PercentageTrailingStop)]
|
||||
impl WasmPercentageTrailingStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(percent: f64) -> Result<WasmPercentageTrailingStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::PercentageTrailingStop::new(percent).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let out = flatten(self.inner.batch(prices));
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = StepTrailingStop)]
|
||||
pub struct WasmStepTrailingStop {
|
||||
inner: wc::StepTrailingStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = StepTrailingStop)]
|
||||
impl WasmStepTrailingStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(step_size: f64) -> Result<WasmStepTrailingStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::StepTrailingStop::new(step_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let out = flatten(self.inner.batch(prices));
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = RenkoTrailingStop)]
|
||||
pub struct WasmRenkoTrailingStop {
|
||||
inner: wc::RenkoTrailingStop,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = RenkoTrailingStop)]
|
||||
impl WasmRenkoTrailingStop {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(block_size: f64) -> Result<WasmRenkoTrailingStop, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::RenkoTrailingStop::new(block_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let out = flatten(self.inner.batch(prices));
|
||||
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