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:
@@ -69,6 +69,9 @@ const scalarFactories = {
|
||||
VerticalHorizontalFilter: () => new wickra.VerticalHorizontalFilter(28),
|
||||
ZScore: () => new wickra.ZScore(20),
|
||||
LinRegAngle: () => new wickra.LinRegAngle(14),
|
||||
PercentageTrailingStop: () => new wickra.PercentageTrailingStop(5),
|
||||
StepTrailingStop: () => new wickra.StepTrailingStop(1),
|
||||
RenkoTrailingStop: () => new wickra.RenkoTrailingStop(1),
|
||||
LaguerreRSI: () => new wickra.LaguerreRSI(0.5),
|
||||
ConnorsRSI: () => new wickra.ConnorsRSI(3, 2, 100),
|
||||
RVIVolatility: () => new wickra.RVIVolatility(10),
|
||||
@@ -125,6 +128,9 @@ const candleScalar = {
|
||||
VZO: { make: () => new wickra.VZO(14), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
|
||||
MarketFacilitationIndex: { make: () => new wickra.MarketFacilitationIndex(), step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
|
||||
AtrTrailingStop: { make: () => new wickra.AtrTrailingStop(14, 3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
HiLoActivator: { make: () => new wickra.HiLoActivator(3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
VoltyStop: { make: () => new wickra.VoltyStop(14, 2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
YoyoExit: { make: () => new wickra.YoyoExit(14, 2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
TypicalPrice: { make: () => new wickra.TypicalPrice(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
MedianPrice: { make: () => new wickra.MedianPrice(), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
WeightedClose: { make: () => new wickra.WeightedClose(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
@@ -173,6 +179,7 @@ const multi = {
|
||||
SuperTrend: { make: () => new wickra.SuperTrend(10, 3), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ChandelierExit: { make: () => new wickra.ChandelierExit(22, 3), fields: ['longStop', 'shortStop'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ChandeKrollStop: { make: () => new wickra.ChandeKrollStop(10, 1, 9), fields: ['stopLong', 'stopShort'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
DonchianStop: { make: () => new wickra.DonchianStop(10), fields: ['stopLong', 'stopShort'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
// Family 05: bands & channels
|
||||
MaEnvelope: { make: () => new wickra.MaEnvelope(20, 0.025), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
AccelerationBands: { make: () => new wickra.AccelerationBands(20, 0.001), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
@@ -311,6 +318,26 @@ test('LinRegAngle of a unit-slope series is 45 degrees', () => {
|
||||
assert.ok(Math.abs(out[4] - 45) < 1e-9);
|
||||
});
|
||||
|
||||
test('PercentageTrailingStop seeds and ratchets', () => {
|
||||
const s = new wickra.PercentageTrailingStop(10);
|
||||
assert.ok(Math.abs(s.update(100) - 90) < 1e-9);
|
||||
assert.ok(Math.abs(s.update(110) - 99) < 1e-9);
|
||||
});
|
||||
|
||||
test('RenkoTrailingStop only advances after a full block', () => {
|
||||
const s = new wickra.RenkoTrailingStop(1);
|
||||
assert.ok(Math.abs(s.update(100) - 99) < 1e-9);
|
||||
assert.ok(Math.abs(s.update(100.5) - 99) < 1e-9);
|
||||
assert.ok(Math.abs(s.update(101) - 100) < 1e-9);
|
||||
});
|
||||
|
||||
test('DonchianStop window extremes', () => {
|
||||
const out = new wickra.DonchianStop(5).batch([1, 2, 3, 4, 5], [0, 1, 2, 3, 4]);
|
||||
// [long0, short0, long1, short1, ...]: idx 8 (=4*2) = long_5th, idx 9 = short_5th.
|
||||
assert.ok(Math.abs(out[8] - 0) < 1e-9);
|
||||
assert.ok(Math.abs(out[9] - 5) < 1e-9);
|
||||
});
|
||||
|
||||
test('MaEnvelope reference values', () => {
|
||||
// SMA([10, 20, 30]) = 20; with percent 0.10: upper=22, lower=18.
|
||||
const out = new wickra.MaEnvelope(3, 0.10).batch([10, 20, 30]);
|
||||
|
||||
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, KVO, VolumeOscillator, NVI, PVI, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, RWI, WaveTrend, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands } = nativeBinding
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, KVO, VolumeOscillator, NVI, PVI, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, RWI, WaveTrend, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -394,6 +394,13 @@ module.exports.SuperTrend = SuperTrend
|
||||
module.exports.ChandelierExit = ChandelierExit
|
||||
module.exports.ChandeKrollStop = ChandeKrollStop
|
||||
module.exports.AtrTrailingStop = AtrTrailingStop
|
||||
module.exports.HiLoActivator = HiLoActivator
|
||||
module.exports.VoltyStop = VoltyStop
|
||||
module.exports.YoyoExit = YoyoExit
|
||||
module.exports.DonchianStop = DonchianStop
|
||||
module.exports.PercentageTrailingStop = PercentageTrailingStop
|
||||
module.exports.StepTrailingStop = StepTrailingStop
|
||||
module.exports.RenkoTrailingStop = RenkoTrailingStop
|
||||
module.exports.TypicalPrice = TypicalPrice
|
||||
module.exports.MedianPrice = MedianPrice
|
||||
module.exports.WeightedClose = WeightedClose
|
||||
|
||||
@@ -3404,6 +3404,350 @@ impl AtrTrailingStopNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== HiLo Activator ==============================
|
||||
|
||||
#[napi(js_name = "HiLoActivator")]
|
||||
pub struct HiLoActivatorNode {
|
||||
inner: wc::HiLoActivator,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl HiLoActivatorNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HiLoActivator::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Volty Stop ==============================
|
||||
|
||||
#[napi(js_name = "VoltyStop")]
|
||||
pub struct VoltyStopNode {
|
||||
inner: wc::VoltyStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VoltyStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(atr_period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VoltyStop::new(atr_period as usize, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Yo-Yo Exit ==============================
|
||||
|
||||
#[napi(js_name = "YoyoExit")]
|
||||
pub struct YoyoExitNode {
|
||||
inner: wc::YoyoExit,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl YoyoExitNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(atr_period: u32, multiplier: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::YoyoExit::new(atr_period as usize, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
|
||||
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
out.push(
|
||||
self.inner
|
||||
.update(cnd(high[i], low[i], close[i], 0.0)?)
|
||||
.unwrap_or(f64::NAN),
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
#[napi(js_name = "inTrade")]
|
||||
pub fn in_trade(&self) -> bool {
|
||||
self.inner.in_trade()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Donchian Stop ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct DonchianStopValue {
|
||||
pub stop_long: f64,
|
||||
pub stop_short: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "DonchianStop")]
|
||||
pub struct DonchianStopNode {
|
||||
inner: wc::DonchianStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl DonchianStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DonchianStop::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<DonchianStopValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, low, 0.0)?)
|
||||
.map(|o| DonchianStopValue {
|
||||
stop_long: o.stop_long,
|
||||
stop_short: o.stop_short,
|
||||
}))
|
||||
}
|
||||
/// Returns `[long0, short0, long1, short1, ...]`, length `2 * n`. Warmup
|
||||
/// positions are `NaN`.
|
||||
#[napi]
|
||||
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high and low must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let n = high.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) {
|
||||
out[i * 2] = o.stop_long;
|
||||
out[i * 2 + 1] = o.stop_short;
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Percentage Trailing Stop ==============================
|
||||
|
||||
#[napi(js_name = "PercentageTrailingStop")]
|
||||
pub struct PercentageTrailingStopNode {
|
||||
inner: wc::PercentageTrailingStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PercentageTrailingStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(percent: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PercentageTrailingStop::new(percent).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Step Trailing Stop ==============================
|
||||
|
||||
#[napi(js_name = "StepTrailingStop")]
|
||||
pub struct StepTrailingStopNode {
|
||||
inner: wc::StepTrailingStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl StepTrailingStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(step_size: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::StepTrailingStop::new(step_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Renko Trailing Stop ==============================
|
||||
|
||||
#[napi(js_name = "RenkoTrailingStop")]
|
||||
pub struct RenkoTrailingStopNode {
|
||||
inner: wc::RenkoTrailingStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl RenkoTrailingStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(block_size: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RenkoTrailingStop::new(block_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Typical Price ==============================
|
||||
|
||||
#[napi(js_name = "TypicalPrice")]
|
||||
|
||||
@@ -107,6 +107,13 @@ from ._wickra import (
|
||||
ChandelierExit,
|
||||
ChandeKrollStop,
|
||||
AtrTrailingStop,
|
||||
HiLoActivator,
|
||||
VoltyStop,
|
||||
YoyoExit,
|
||||
DonchianStop,
|
||||
PercentageTrailingStop,
|
||||
StepTrailingStop,
|
||||
RenkoTrailingStop,
|
||||
TrueRange,
|
||||
ChaikinVolatility,
|
||||
RVIVolatility,
|
||||
@@ -240,6 +247,13 @@ __all__ = [
|
||||
"ChandelierExit",
|
||||
"ChandeKrollStop",
|
||||
"AtrTrailingStop",
|
||||
"HiLoActivator",
|
||||
"VoltyStop",
|
||||
"YoyoExit",
|
||||
"DonchianStop",
|
||||
"PercentageTrailingStop",
|
||||
"StepTrailingStop",
|
||||
"RenkoTrailingStop",
|
||||
"TrueRange",
|
||||
"ChaikinVolatility",
|
||||
"RVIVolatility",
|
||||
|
||||
@@ -5763,6 +5763,436 @@ impl PyAtrTrailingStop {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== HiLo Activator ==============================
|
||||
|
||||
#[pyclass(name = "HiLoActivator", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyHiLoActivator {
|
||||
inner: wc::HiLoActivator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHiLoActivator {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=3))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::HiLoActivator::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("HiLoActivator(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Volty Stop ==============================
|
||||
|
||||
#[pyclass(name = "VoltyStop", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyVoltyStop {
|
||||
inner: wc::VoltyStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyVoltyStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (atr_period=14, multiplier=2.0))]
|
||||
fn new(atr_period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VoltyStop::new(atr_period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, f64) {
|
||||
self.inner.params()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (p, m) = self.inner.params();
|
||||
format!("VoltyStop(atr_period={p}, multiplier={m})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Yo-Yo Exit ==============================
|
||||
|
||||
#[pyclass(name = "YoyoExit", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyYoyoExit {
|
||||
inner: wc::YoyoExit,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyYoyoExit {
|
||||
#[new]
|
||||
#[pyo3(signature = (atr_period=14, multiplier=2.0))]
|
||||
fn new(atr_period: usize, multiplier: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::YoyoExit::new(atr_period, multiplier).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(h.len());
|
||||
for i in 0..h.len() {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, f64) {
|
||||
self.inner.params()
|
||||
}
|
||||
#[getter]
|
||||
fn in_trade(&self) -> bool {
|
||||
self.inner.in_trade()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (p, m) = self.inner.params();
|
||||
format!("YoyoExit(atr_period={p}, multiplier={m})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Donchian Stop ==============================
|
||||
|
||||
#[pyclass(name = "DonchianStop", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyDonchianStop {
|
||||
inner: wc::DonchianStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyDonchianStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=10))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::DonchianStop::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c).map(|o| (o.stop_long, o.stop_short)))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray2<f64>>> {
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if h.len() != l.len() {
|
||||
return Err(PyValueError::new_err("high and low must be equal length"));
|
||||
}
|
||||
let n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.stop_long;
|
||||
out[i * 2 + 1] = o.stop_short;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("DonchianStop(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Percentage Trailing Stop ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "PercentageTrailingStop",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyPercentageTrailingStop {
|
||||
inner: wc::PercentageTrailingStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPercentageTrailingStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (percent=5.0))]
|
||||
fn new(percent: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PercentageTrailingStop::new(percent).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn percent(&self) -> f64 {
|
||||
self.inner.percent()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("PercentageTrailingStop(percent={})", self.inner.percent())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Step Trailing Stop ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "StepTrailingStop",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyStepTrailingStop {
|
||||
inner: wc::StepTrailingStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyStepTrailingStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (step_size=1.0))]
|
||||
fn new(step_size: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::StepTrailingStop::new(step_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn step_size(&self) -> f64 {
|
||||
self.inner.step_size()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("StepTrailingStop(step_size={})", self.inner.step_size())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Renko Trailing Stop ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "RenkoTrailingStop",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyRenkoTrailingStop {
|
||||
inner: wc::RenkoTrailingStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRenkoTrailingStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (block_size=1.0))]
|
||||
fn new(block_size: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::RenkoTrailingStop::new(block_size).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let slice = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(slice)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn block_size(&self) -> f64 {
|
||||
self.inner.block_size()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("RenkoTrailingStop(block_size={})", self.inner.block_size())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Typical Price ==============================
|
||||
|
||||
#[pyclass(name = "TypicalPrice", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -7704,6 +8134,13 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyChandelierExit>()?;
|
||||
m.add_class::<PyChandeKrollStop>()?;
|
||||
m.add_class::<PyAtrTrailingStop>()?;
|
||||
m.add_class::<PyHiLoActivator>()?;
|
||||
m.add_class::<PyVoltyStop>()?;
|
||||
m.add_class::<PyYoyoExit>()?;
|
||||
m.add_class::<PyDonchianStop>()?;
|
||||
m.add_class::<PyPercentageTrailingStop>()?;
|
||||
m.add_class::<PyStepTrailingStop>()?;
|
||||
m.add_class::<PyRenkoTrailingStop>()?;
|
||||
m.add_class::<PyTypicalPrice>()?;
|
||||
m.add_class::<PyMedianPrice>()?;
|
||||
m.add_class::<PyWeightedClose>()?;
|
||||
|
||||
@@ -332,6 +332,78 @@ def test_obv_cumulative_known_sequence():
|
||||
np.testing.assert_allclose(out, [0.0, 20.0, -10.0, -10.0, 0.0])
|
||||
|
||||
|
||||
def test_percentage_trailing_stop_seed_and_ratchet():
|
||||
# 10% trail: first close 100 -> stop 90; next 110 -> stop max(90, 99) = 99.
|
||||
s = ta.PercentageTrailingStop(10.0)
|
||||
assert math.isclose(s.update(100.0), 90.0, abs_tol=1e-12)
|
||||
assert math.isclose(s.update(110.0), 99.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_step_trailing_stop_snaps_below_close():
|
||||
# step 1: floor((100.4 - 1) / 1) = 99.
|
||||
s = ta.StepTrailingStop(1.0)
|
||||
assert math.isclose(s.update(100.4), 99.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_renko_trailing_stop_holds_until_full_block():
|
||||
# block 1: seed 100 -> stop 99; 100.5 still 99; 101 -> stop 100.
|
||||
s = ta.RenkoTrailingStop(1.0)
|
||||
assert math.isclose(s.update(100.0), 99.0, abs_tol=1e-12)
|
||||
assert math.isclose(s.update(100.5), 99.0, abs_tol=1e-12)
|
||||
assert math.isclose(s.update(101.0), 100.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_donchian_stop_window_extremes():
|
||||
# 5-bar window of highs 1..5 and lows 0..4.
|
||||
high = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
low = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
|
||||
out = ta.DonchianStop(5).batch(high, low)
|
||||
# First 4 rows NaN, fifth row: stop_long = 0, stop_short = 5.
|
||||
for i in range(4):
|
||||
assert math.isnan(out[i, 0])
|
||||
assert math.isnan(out[i, 1])
|
||||
assert math.isclose(out[4, 0], 0.0, abs_tol=1e-12)
|
||||
assert math.isclose(out[4, 1], 5.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_hilo_activator_flat_market_holds_low_sma():
|
||||
# Flat candles H=11, L=9, C=10 -> close (10) sits between bands, so the
|
||||
# initial long seed is preserved: emitted stop = lo_sma = 9.
|
||||
h = np.full(15, 11.0)
|
||||
l = np.full(15, 9.0)
|
||||
c = np.full(15, 10.0)
|
||||
out = ta.HiLoActivator(3).batch(h, l, c)
|
||||
# warmup_period == period + 1 == 4, so indices 0..2 are NaN; index 3 onwards is 9.
|
||||
for i in range(3):
|
||||
assert math.isnan(out[i])
|
||||
for i in range(3, 15):
|
||||
assert math.isclose(out[i], 9.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_volty_stop_flat_market_constant_level():
|
||||
# ATR=2, mult=2 -> band 4; anchor stays at close 10 -> stop = 10 - 4 = 6.
|
||||
h = np.full(20, 11.0)
|
||||
l = np.full(20, 9.0)
|
||||
c = np.full(20, 10.0)
|
||||
out = ta.VoltyStop(5, 2.0).batch(h, l, c)
|
||||
for i in range(4):
|
||||
assert math.isnan(out[i])
|
||||
for i in range(4, 20):
|
||||
assert math.isclose(out[i], 6.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_yoyo_exit_flat_market_constant_level():
|
||||
# ATR=2, mult=2 -> band 4; trail = close - band = 10 - 4 = 6 and holds.
|
||||
h = np.full(20, 11.0)
|
||||
l = np.full(20, 9.0)
|
||||
c = np.full(20, 10.0)
|
||||
out = ta.YoyoExit(5, 2.0).batch(h, l, c)
|
||||
for i in range(4):
|
||||
assert math.isnan(out[i])
|
||||
for i in range(4, 20):
|
||||
assert math.isclose(out[i], 6.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def test_rvi_volatility_pure_uptrend_saturates_at_one_hundred():
|
||||
# Strictly rising closes -> every stddev sample classified as "up" ->
|
||||
# RVIVolatility saturates at 100. Renamed from the original ta.RVI in
|
||||
|
||||
@@ -73,6 +73,9 @@ SCALAR = [
|
||||
(ta.VerticalHorizontalFilter, (28,)),
|
||||
(ta.ZScore, (20,)),
|
||||
(ta.LinRegAngle, (14,)),
|
||||
(ta.PercentageTrailingStop, (5.0,)),
|
||||
(ta.StepTrailingStop, (1.0,)),
|
||||
(ta.RenkoTrailingStop, (1.0,)),
|
||||
(ta.LaguerreRSI, (0.5,)),
|
||||
(ta.ConnorsRSI, (3, 2, 100)),
|
||||
(ta.RVIVolatility, (10,)),
|
||||
@@ -199,6 +202,18 @@ CANDLE_SCALAR = {
|
||||
lambda: ta.AtrTrailingStop(14, 3.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"HiLoActivator": (
|
||||
lambda: ta.HiLoActivator(3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"VoltyStop": (
|
||||
lambda: ta.VoltyStop(14, 2.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"YoyoExit": (
|
||||
lambda: ta.YoyoExit(14, 2.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"TypicalPrice": (
|
||||
lambda: ta.TypicalPrice(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
@@ -307,6 +322,10 @@ MULTI = {
|
||||
lambda: ta.ChandeKrollStop(10, 1.0, 9),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
"DonchianStop": (
|
||||
lambda: ta.DonchianStop(10),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
),
|
||||
# Family 05 candle-input bands. Each entry is
|
||||
# `(factory, batch_call, output_arity, streaming_fields)` where
|
||||
# `streaming_fields` is the tuple shape returned by `update(...)`.
|
||||
|
||||
@@ -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