feat: add Pivots & S/R indicators (B11) (#201)
Adds five support/resistance and pivot indicators, growing the catalog 462 -> 467. ## Indicators - **CentralPivotRange** (Candle -> struct) — the classic pivot `(H+L+C)/3` flanked by two central levels (TC/BC); range width gauges trending vs balanced days. - **MurreyMathLines** (Candle -> struct) — T. H. Murrey's eighths grid over a rolling high-low frame; nine levels (0/8 .. 8/8) acting as support/resistance. - **AndrewsPitchfork** (Candle -> struct) — median line and two parallels projected forward from the last three auto-detected swing pivots (symmetric fractal of half-width `strength`). - **VolumeWeightedSr** (Candle -> struct) — a band whose edges are the volume-weighted average of recent highs (resistance) and lows (support); falls back to equal weighting when window volume is zero. - **PivotReversal** (Candle -> f64) — a `+1`/`-1` breakout signal fired on the bar where price closes through the most recently confirmed swing pivot. ## Wiring Core structs with branch-complete unit tests, Python/Node/WASM bindings, fuzz drives, reference + streaming-vs-batch tests, README + docs counter sync (FAMILIES "Pivots & S/R"), and CHANGELOG entries. Verified locally: `cargo fmt`, `cargo test -p wickra-core` (3798 lib + 425 doc), `cargo clippy --workspace --all-targets --all-features -D warnings`, `npm run build && npm test` (542), `maturin develop` + `pytest` (891).
This commit is contained in:
@@ -9491,6 +9491,371 @@ impl ProjectionBandsNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Central Pivot Range ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct CentralPivotRangeValue {
|
||||
pub pivot: f64,
|
||||
pub tc: f64,
|
||||
pub bc: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "CentralPivotRange")]
|
||||
pub struct CentralPivotRangeNode {
|
||||
inner: wc::CentralPivotRange,
|
||||
}
|
||||
|
||||
impl Default for CentralPivotRangeNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl CentralPivotRangeNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: wc::CentralPivotRange::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<CentralPivotRangeValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, close, 0.0)?)
|
||||
.map(|o| CentralPivotRangeValue {
|
||||
pivot: o.pivot,
|
||||
tc: o.tc,
|
||||
bc: o.bc,
|
||||
}))
|
||||
}
|
||||
#[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 * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
|
||||
out[i * 3] = o.pivot;
|
||||
out[i * 3 + 1] = o.tc;
|
||||
out[i * 3 + 2] = o.bc;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Murrey Math Lines ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct MurreyMathLinesValue {
|
||||
#[napi(js_name = "mm8_8")]
|
||||
pub mm8_8: f64,
|
||||
#[napi(js_name = "mm7_8")]
|
||||
pub mm7_8: f64,
|
||||
#[napi(js_name = "mm6_8")]
|
||||
pub mm6_8: f64,
|
||||
#[napi(js_name = "mm5_8")]
|
||||
pub mm5_8: f64,
|
||||
#[napi(js_name = "mm4_8")]
|
||||
pub mm4_8: f64,
|
||||
#[napi(js_name = "mm3_8")]
|
||||
pub mm3_8: f64,
|
||||
#[napi(js_name = "mm2_8")]
|
||||
pub mm2_8: f64,
|
||||
#[napi(js_name = "mm1_8")]
|
||||
pub mm1_8: f64,
|
||||
#[napi(js_name = "mm0_8")]
|
||||
pub mm0_8: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "MurreyMathLines")]
|
||||
pub struct MurreyMathLinesNode {
|
||||
inner: wc::MurreyMathLines,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MurreyMathLinesNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MurreyMathLines::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<MurreyMathLinesValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, low, 0.0)?)
|
||||
.map(|o| MurreyMathLinesValue {
|
||||
mm8_8: o.mm8_8,
|
||||
mm7_8: o.mm7_8,
|
||||
mm6_8: o.mm6_8,
|
||||
mm5_8: o.mm5_8,
|
||||
mm4_8: o.mm4_8,
|
||||
mm3_8: o.mm3_8,
|
||||
mm2_8: o.mm2_8,
|
||||
mm1_8: o.mm1_8,
|
||||
mm0_8: o.mm0_8,
|
||||
}))
|
||||
}
|
||||
#[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 * 9];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) {
|
||||
out[i * 9] = o.mm8_8;
|
||||
out[i * 9 + 1] = o.mm7_8;
|
||||
out[i * 9 + 2] = o.mm6_8;
|
||||
out[i * 9 + 3] = o.mm5_8;
|
||||
out[i * 9 + 4] = o.mm4_8;
|
||||
out[i * 9 + 5] = o.mm3_8;
|
||||
out[i * 9 + 6] = o.mm2_8;
|
||||
out[i * 9 + 7] = o.mm1_8;
|
||||
out[i * 9 + 8] = o.mm0_8;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Andrews Pitchfork ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct AndrewsPitchforkValue {
|
||||
pub median: f64,
|
||||
pub upper: f64,
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "AndrewsPitchfork")]
|
||||
pub struct AndrewsPitchforkNode {
|
||||
inner: wc::AndrewsPitchfork,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl AndrewsPitchforkNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(strength: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::AndrewsPitchfork::new(strength as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<AndrewsPitchforkValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, low, 0.0)?)
|
||||
.map(|o| AndrewsPitchforkValue {
|
||||
median: o.median,
|
||||
upper: o.upper,
|
||||
lower: o.lower,
|
||||
}))
|
||||
}
|
||||
#[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 * 3];
|
||||
for i in 0..n {
|
||||
if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) {
|
||||
out[i * 3] = o.median;
|
||||
out[i * 3 + 1] = o.upper;
|
||||
out[i * 3 + 2] = o.lower;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Volume-Weighted S/R ----------
|
||||
|
||||
#[napi(object)]
|
||||
pub struct VolumeWeightedSrValue {
|
||||
pub support: f64,
|
||||
pub resistance: f64,
|
||||
}
|
||||
|
||||
#[napi(js_name = "VolumeWeightedSr")]
|
||||
pub struct VolumeWeightedSrNode {
|
||||
inner: wc::VolumeWeightedSr,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl VolumeWeightedSrNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(period: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::VolumeWeightedSr::new(period as usize).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
high: f64,
|
||||
low: f64,
|
||||
volume: f64,
|
||||
) -> napi::Result<Option<VolumeWeightedSrValue>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.update(cnd(high, low, low, volume)?)
|
||||
.map(|o| VolumeWeightedSrValue {
|
||||
support: o.support,
|
||||
resistance: o.resistance,
|
||||
}))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
volume: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if high.len() != low.len() || low.len() != volume.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"high, low, volume 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], volume[i])?) {
|
||||
out[i * 2] = o.support;
|
||||
out[i * 2 + 1] = o.resistance;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Pivot Reversal ----------
|
||||
|
||||
#[napi(js_name = "PivotReversal")]
|
||||
pub struct PivotReversalNode {
|
||||
inner: wc::PivotReversal,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PivotReversalNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(left: u32, right: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PivotReversal::new(left as usize, right 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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Double Bollinger ----------
|
||||
|
||||
#[napi(object)]
|
||||
|
||||
Reference in New Issue
Block a user