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:
@@ -356,6 +356,7 @@ const candleScalar = {
|
||||
Qstick: { make: () => new wickra.Qstick(10), step: (ind, i) => ind.update(open[i], close[i]), batch: (ind) => ind.batch(open, close) },
|
||||
VolatilityRatio: { make: () => new wickra.VolatilityRatio(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ProjectionOscillator: { make: () => new wickra.ProjectionOscillator(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
TimeBasedStop: { make: () => new wickra.TimeBasedStop(5), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
@@ -447,6 +448,11 @@ const multi = {
|
||||
BomarBands: { make: () => new wickra.BomarBands(4, 0.85), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
MedianChannel: { make: () => new wickra.MedianChannel(5, 2.0), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
|
||||
ProjectionBands: { make: () => new wickra.ProjectionBands(3), fields: ['upper', 'middle', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
|
||||
KaseDevStop: { make: () => new wickra.KaseDevStop(3, 1.0), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ElderSafeZone: { make: () => new wickra.ElderSafeZone(14, 2.0), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
AtrRatchet: { make: () => new wickra.AtrRatchet(14, 4.0, 0.1), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
Nrtr: { make: () => new wickra.Nrtr(2.0), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
ModifiedMaStop: { make: () => new wickra.ModifiedMaStop(14), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(multi)) {
|
||||
|
||||
Vendored
+94
@@ -149,6 +149,26 @@ export interface DonchianStopValue {
|
||||
stopLong: number
|
||||
stopShort: number
|
||||
}
|
||||
export interface KaseDevStopValue {
|
||||
value: number
|
||||
direction: number
|
||||
}
|
||||
export interface ElderSafeZoneValue {
|
||||
value: number
|
||||
direction: number
|
||||
}
|
||||
export interface AtrRatchetValue {
|
||||
value: number
|
||||
direction: number
|
||||
}
|
||||
export interface NrtrValue {
|
||||
value: number
|
||||
direction: number
|
||||
}
|
||||
export interface ModifiedMaStopValue {
|
||||
value: number
|
||||
direction: number
|
||||
}
|
||||
/** Vortex Indicator pair: `VI+` and `VI-`. */
|
||||
export interface VortexValue {
|
||||
plus: number
|
||||
@@ -1668,6 +1688,15 @@ export declare class ProjectionOscillator {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TimeBasedStopNode = TimeBasedStop
|
||||
export declare class TimeBasedStop {
|
||||
constructor(maxBars: number)
|
||||
update(high: number, low: number, close: number): number | null
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type StochNode = Stochastic
|
||||
export declare class Stochastic {
|
||||
constructor(kPeriod: number, dPeriod: number)
|
||||
@@ -2328,6 +2357,71 @@ export declare class RenkoTrailingStop {
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type KaseDevStopNode = KaseDevStop
|
||||
export declare class KaseDevStop {
|
||||
constructor(period: number, dev: number)
|
||||
update(high: number, low: number, close: number): KaseDevStopValue | null
|
||||
/**
|
||||
* Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
* Warmup positions are `NaN`.
|
||||
*/
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ElderSafeZoneNode = ElderSafeZone
|
||||
export declare class ElderSafeZone {
|
||||
constructor(period: number, coeff: number)
|
||||
update(high: number, low: number, close: number): ElderSafeZoneValue | null
|
||||
/**
|
||||
* Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
* Warmup positions are `NaN`.
|
||||
*/
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type AtrRatchetNode = AtrRatchet
|
||||
export declare class AtrRatchet {
|
||||
constructor(atrPeriod: number, startMult: number, increment: number)
|
||||
update(high: number, low: number, close: number): AtrRatchetValue | null
|
||||
/**
|
||||
* Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
* Warmup positions are `NaN`.
|
||||
*/
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type NrtrNode = Nrtr
|
||||
export declare class Nrtr {
|
||||
constructor(pct: number)
|
||||
update(high: number, low: number, close: number): NrtrValue | null
|
||||
/**
|
||||
* Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
* Warmup positions are `NaN`.
|
||||
*/
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type ModifiedMaStopNode = ModifiedMaStop
|
||||
export declare class ModifiedMaStop {
|
||||
constructor(period: number)
|
||||
update(high: number, low: number, close: number): ModifiedMaStopValue | null
|
||||
/**
|
||||
* Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
* Warmup positions are `NaN`.
|
||||
*/
|
||||
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
|
||||
reset(): void
|
||||
isReady(): boolean
|
||||
warmupPeriod(): number
|
||||
}
|
||||
export type TypicalPriceNode = TypicalPrice
|
||||
export declare class TypicalPrice {
|
||||
constructor()
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2922,6 +2922,59 @@ impl ProjectionOscillatorNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "TimeBasedStop")]
|
||||
pub struct TimeBasedStopNode {
|
||||
inner: wc::TimeBasedStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl TimeBasedStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(max_bars: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TimeBasedStop::new(max_bars 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
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct StochValue {
|
||||
pub k: f64,
|
||||
@@ -6591,6 +6644,372 @@ impl RenkoTrailingStopNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Kase DevStop ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct KaseDevStopValue {
|
||||
pub value: f64,
|
||||
pub direction: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "KaseDevStop")]
|
||||
pub struct KaseDevStopNode {
|
||||
inner: wc::KaseDevStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl KaseDevStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, dev: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KaseDevStop::new(period as usize, dev).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<KaseDevStopValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| KaseDevStopValue {
|
||||
value: o.value,
|
||||
direction: o.direction,
|
||||
}))
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup positions are `NaN`.
|
||||
#[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 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], close[i], 0.0)?) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Elder SafeZone ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ElderSafeZoneValue {
|
||||
pub value: f64,
|
||||
pub direction: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "ElderSafeZone")]
|
||||
pub struct ElderSafeZoneNode {
|
||||
inner: wc::ElderSafeZone,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ElderSafeZoneNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32, coeff: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ElderSafeZone::new(period as usize, coeff).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<ElderSafeZoneValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| ElderSafeZoneValue {
|
||||
value: o.value,
|
||||
direction: o.direction,
|
||||
}))
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup positions are `NaN`.
|
||||
#[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 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], close[i], 0.0)?) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== ATR Ratchet ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct AtrRatchetValue {
|
||||
pub value: f64,
|
||||
pub direction: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "AtrRatchet")]
|
||||
pub struct AtrRatchetNode {
|
||||
inner: wc::AtrRatchet,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AtrRatchetNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(atr_period: u32, start_mult: f64, increment: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AtrRatchet::new(atr_period as usize, start_mult, increment)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<AtrRatchetValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| AtrRatchetValue {
|
||||
value: o.value,
|
||||
direction: o.direction,
|
||||
}))
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup positions are `NaN`.
|
||||
#[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 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], close[i], 0.0)?) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== NRTR ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct NrtrValue {
|
||||
pub value: f64,
|
||||
pub direction: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "Nrtr")]
|
||||
pub struct NrtrNode {
|
||||
inner: wc::Nrtr,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl NrtrNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(pct: f64) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Nrtr::new(pct).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<NrtrValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| NrtrValue {
|
||||
value: o.value,
|
||||
direction: o.direction,
|
||||
}))
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup positions are `NaN`.
|
||||
#[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 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], close[i], 0.0)?) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Modified MA Stop ==============================
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ModifiedMaStopValue {
|
||||
pub value: f64,
|
||||
pub direction: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "ModifiedMaStop")]
|
||||
pub struct ModifiedMaStopNode {
|
||||
inner: wc::ModifiedMaStop,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ModifiedMaStopNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ModifiedMaStop::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<ModifiedMaStopValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| ModifiedMaStopValue {
|
||||
value: o.value,
|
||||
direction: o.direction,
|
||||
}))
|
||||
}
|
||||
/// Returns `[value0, direction0, value1, direction1, ...]`, length `2 * n`.
|
||||
/// Warmup positions are `NaN`.
|
||||
#[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 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], close[i], 0.0)?) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Typical Price ==============================
|
||||
|
||||
#[napi(js_name = "TypicalPrice")]
|
||||
|
||||
@@ -25,6 +25,7 @@ from __future__ import annotations
|
||||
|
||||
from ._wickra import (
|
||||
__version__,
|
||||
TimeBasedStop,
|
||||
ProjectionOscillator,
|
||||
VolatilityCone,
|
||||
VolatilityRatio,
|
||||
@@ -168,6 +169,11 @@ from ._wickra import (
|
||||
HistoricalVolatility,
|
||||
BollingerBandwidth,
|
||||
PercentB,
|
||||
# Trailing Stops
|
||||
ModifiedMaStop,
|
||||
Nrtr,
|
||||
AtrRatchet,
|
||||
ElderSafeZone,
|
||||
SuperTrend,
|
||||
ChandelierExit,
|
||||
ChandeKrollStop,
|
||||
@@ -179,6 +185,7 @@ from ._wickra import (
|
||||
PercentageTrailingStop,
|
||||
StepTrailingStop,
|
||||
RenkoTrailingStop,
|
||||
KaseDevStop,
|
||||
TrueRange,
|
||||
ChaikinVolatility,
|
||||
RVIVolatility,
|
||||
@@ -487,6 +494,7 @@ from ._wickra import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TimeBasedStop",
|
||||
"ProjectionOscillator",
|
||||
"VolatilityCone",
|
||||
"VolatilityRatio",
|
||||
@@ -631,6 +639,11 @@ __all__ = [
|
||||
"HistoricalVolatility",
|
||||
"BollingerBandwidth",
|
||||
"PercentB",
|
||||
# Trailing Stops
|
||||
"ModifiedMaStop",
|
||||
"Nrtr",
|
||||
"AtrRatchet",
|
||||
"ElderSafeZone",
|
||||
"SuperTrend",
|
||||
"ChandelierExit",
|
||||
"ChandeKrollStop",
|
||||
@@ -642,6 +655,7 @@ __all__ = [
|
||||
"PercentageTrailingStop",
|
||||
"StepTrailingStop",
|
||||
"RenkoTrailingStop",
|
||||
"KaseDevStop",
|
||||
"TrueRange",
|
||||
"ChaikinVolatility",
|
||||
"RVIVolatility",
|
||||
|
||||
@@ -3631,6 +3631,75 @@ impl PyProjectionOscillator {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TimeBasedStop ==============================
|
||||
|
||||
#[pyclass(name = "TimeBasedStop", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTimeBasedStop {
|
||||
inner: wc::TimeBasedStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTimeBasedStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (max_bars=5))]
|
||||
fn new(max_bars: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TimeBasedStop::new(max_bars).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
/// Batch over numpy columns: high, low, close (all 1-D, equal length).
|
||||
/// Ignores price; counts bars. Returns progress in `[0, 1]`.
|
||||
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 max_bars(&self) -> usize {
|
||||
self.inner.max_bars()
|
||||
}
|
||||
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!("TimeBasedStop(max_bars={})", self.inner.max_bars())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -9813,6 +9882,390 @@ impl PyRenkoTrailingStop {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Kase DevStop ==============================
|
||||
|
||||
#[pyclass(name = "KaseDevStop", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyKaseDevStop {
|
||||
inner: wc::KaseDevStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyKaseDevStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=30, dev=1.0))]
|
||||
fn new(period: usize, dev: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::KaseDevStop::new(period, dev).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.value, o.direction)))
|
||||
}
|
||||
/// Batch over numpy columns high, low, close. Returns shape `(n, 2)` with
|
||||
/// columns `[value, direction]`; warmup rows are `NaN`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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))?;
|
||||
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 n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.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 (period, dev) = self.inner.params();
|
||||
format!("KaseDevStop(period={period}, dev={dev})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Elder SafeZone ==============================
|
||||
|
||||
#[pyclass(name = "ElderSafeZone", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyElderSafeZone {
|
||||
inner: wc::ElderSafeZone,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyElderSafeZone {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14, coeff=2.0))]
|
||||
fn new(period: usize, coeff: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ElderSafeZone::new(period, coeff).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.value, o.direction)))
|
||||
}
|
||||
/// Batch over numpy columns high, low, close. Returns shape `(n, 2)` with
|
||||
/// columns `[value, direction]`; warmup rows are `NaN`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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))?;
|
||||
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 n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.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 (period, coeff) = self.inner.params();
|
||||
format!("ElderSafeZone(period={period}, coeff={coeff})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== ATR Ratchet ==============================
|
||||
|
||||
#[pyclass(name = "AtrRatchet", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyAtrRatchet {
|
||||
inner: wc::AtrRatchet,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAtrRatchet {
|
||||
#[new]
|
||||
#[pyo3(signature = (atr_period=14, start_mult=4.0, increment=0.1))]
|
||||
fn new(atr_period: usize, start_mult: f64, increment: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AtrRatchet::new(atr_period, start_mult, increment).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.value, o.direction)))
|
||||
}
|
||||
/// Batch over numpy columns high, low, close. Returns shape `(n, 2)` with
|
||||
/// columns `[value, direction]`; warmup rows are `NaN`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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))?;
|
||||
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 n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn params(&self) -> (usize, f64, 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 (atr_period, start_mult, increment) = self.inner.params();
|
||||
format!(
|
||||
"AtrRatchet(atr_period={atr_period}, start_mult={start_mult}, increment={increment})"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== NRTR ==============================
|
||||
|
||||
#[pyclass(name = "Nrtr", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyNrtr {
|
||||
inner: wc::Nrtr,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyNrtr {
|
||||
#[new]
|
||||
#[pyo3(signature = (pct=2.0))]
|
||||
fn new(pct: f64) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::Nrtr::new(pct).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.value, o.direction)))
|
||||
}
|
||||
/// Batch over numpy columns high, low, close. Returns shape `(n, 2)` with
|
||||
/// columns `[value, direction]`; warmup rows are `NaN`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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))?;
|
||||
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 n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
|
||||
.expect("shape consistent")
|
||||
.into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn pct(&self) -> f64 {
|
||||
self.inner.pct()
|
||||
}
|
||||
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!("Nrtr(pct={})", self.inner.pct())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Modified MA Stop ==============================
|
||||
|
||||
#[pyclass(
|
||||
name = "ModifiedMaStop",
|
||||
module = "wickra._wickra",
|
||||
skip_from_py_object
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
struct PyModifiedMaStop {
|
||||
inner: wc::ModifiedMaStop,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyModifiedMaStop {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::ModifiedMaStop::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.value, o.direction)))
|
||||
}
|
||||
/// Batch over numpy columns high, low, close. Returns shape `(n, 2)` with
|
||||
/// columns `[value, direction]`; warmup rows are `NaN`.
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: 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))?;
|
||||
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 n = h.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
if let Some(o) = self.inner.update(candle) {
|
||||
out[i * 2] = o.value;
|
||||
out[i * 2 + 1] = o.direction;
|
||||
}
|
||||
}
|
||||
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!("ModifiedMaStop(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Typical Price ==============================
|
||||
|
||||
#[pyclass(name = "TypicalPrice", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -21894,6 +22347,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyPercentageTrailingStop>()?;
|
||||
m.add_class::<PyStepTrailingStop>()?;
|
||||
m.add_class::<PyRenkoTrailingStop>()?;
|
||||
m.add_class::<PyKaseDevStop>()?;
|
||||
m.add_class::<PyElderSafeZone>()?;
|
||||
m.add_class::<PyAtrRatchet>()?;
|
||||
m.add_class::<PyNrtr>()?;
|
||||
m.add_class::<PyModifiedMaStop>()?;
|
||||
m.add_class::<PyTypicalPrice>()?;
|
||||
m.add_class::<PyMedianPrice>()?;
|
||||
m.add_class::<PyWeightedClose>()?;
|
||||
@@ -22238,5 +22696,6 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyVolatilityOfVolatility>()?;
|
||||
m.add_class::<PyVolatilityCone>()?;
|
||||
m.add_class::<PyProjectionOscillator>()?;
|
||||
m.add_class::<PyTimeBasedStop>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -368,6 +368,7 @@ def test_relative_strength_streaming_matches_batch():
|
||||
# 6-tuple candle; the batch helper takes only the columns it needs.
|
||||
|
||||
CANDLE_SCALAR = {
|
||||
"TimeBasedStop": (lambda: ta.TimeBasedStop(5), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"ProjectionOscillator": (lambda: ta.ProjectionOscillator(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"VolatilityRatio": (lambda: ta.VolatilityRatio(14), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
"TTM_TREND": (lambda: ta.TTM_TREND(6), lambda ind, h, l, c, v: ind.batch(h, l, c)),
|
||||
@@ -908,6 +909,31 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
|
||||
# --- Candle-input, multi-output indicators --------------------------------
|
||||
|
||||
MULTI = {
|
||||
"ModifiedMaStop": (
|
||||
lambda: ta.ModifiedMaStop(14),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"Nrtr": (
|
||||
lambda: ta.Nrtr(2.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"AtrRatchet": (
|
||||
lambda: ta.AtrRatchet(14, 4.0, 0.1),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"ElderSafeZone": (
|
||||
lambda: ta.ElderSafeZone(14, 2.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"KaseDevStop": (
|
||||
lambda: ta.KaseDevStop(3, 1.0),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
2,
|
||||
),
|
||||
"ProjectionBands": (
|
||||
lambda: ta.ProjectionBands(3),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l),
|
||||
@@ -2967,6 +2993,53 @@ def test_projection_oscillator_reference():
|
||||
assert t.update((9.0, 12.0, 9.0, 11.0, 1.0, 1)) is None
|
||||
assert t.update((10.0, 11.0, 10.0, 11.0, 1.0, 2)) == pytest.approx(40.0)
|
||||
|
||||
|
||||
def test_kase_devstop_reference():
|
||||
t = ta.KaseDevStop(3, 1.0)
|
||||
assert t.update((100.0, 101.0, 99.0, 100.0, 1.0, 0)) is None
|
||||
assert t.update((101.0, 102.0, 100.0, 101.0, 1.0, 1)) is None
|
||||
assert t.update((102.0, 103.0, 101.0, 102.0, 1.0, 2)) is None
|
||||
assert t.update((102.5, 104.0, 102.0, 103.0, 1.0, 3)) == pytest.approx((101.0, 1.0))
|
||||
|
||||
|
||||
def _stop_candles(n):
|
||||
# Gently rising, valid OHLC: high >= open/close, low <= open/close.
|
||||
return [(100.0 + i, 101.5 + i, 98.5 + i, 100.5 + i, 1.0, i) for i in range(n)]
|
||||
|
||||
|
||||
def test_elder_safezone_reference():
|
||||
t = ta.ElderSafeZone(14, 2.0)
|
||||
candles = _stop_candles(15)
|
||||
for c in candles[:14]:
|
||||
assert t.update(c) is None
|
||||
assert t.update(candles[14]) == pytest.approx((112.5, 1.0))
|
||||
|
||||
|
||||
def test_atr_ratchet_reference():
|
||||
t = ta.AtrRatchet(14, 4.0, 0.1)
|
||||
candles = _stop_candles(14)
|
||||
for c in candles[:13]:
|
||||
assert t.update(c) is None
|
||||
assert t.update(candles[13]) == pytest.approx((101.5, 1.0))
|
||||
|
||||
|
||||
def test_nrtr_reference():
|
||||
t = ta.Nrtr(2.0)
|
||||
assert t.update((100.0, 100.0, 100.0, 100.0, 1.0, 0)) == pytest.approx((98.0, 1.0))
|
||||
|
||||
|
||||
def test_time_based_stop_reference():
|
||||
t = ta.TimeBasedStop(5)
|
||||
assert t.update((100.0, 101.0, 99.0, 100.0, 1.0, 0)) == pytest.approx(0.2)
|
||||
|
||||
|
||||
def test_modified_ma_stop_reference():
|
||||
t = ta.ModifiedMaStop(14)
|
||||
candles = _stop_candles(14)
|
||||
for c in candles[:13]:
|
||||
assert t.update(c) is None
|
||||
assert t.update(candles[13]) == pytest.approx((107.0, 1.0))
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -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