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:
@@ -0,0 +1,353 @@
|
||||
//! Andrews Pitchfork — median line and parallels off the last three swing pivots.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Output of [`AndrewsPitchfork`]: the three pitchfork lines projected to the
|
||||
/// current bar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct AndrewsPitchforkOutput {
|
||||
/// The median line — from the handle pivot through the midpoint of the other two.
|
||||
pub median: f64,
|
||||
/// The upper parallel (through the higher of the two anchor pivots).
|
||||
pub upper: f64,
|
||||
/// The lower parallel (through the lower of the two anchor pivots).
|
||||
pub lower: f64,
|
||||
}
|
||||
|
||||
/// A confirmed swing pivot: its bar index and price.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Pivot {
|
||||
index: f64,
|
||||
price: f64,
|
||||
is_high: bool,
|
||||
}
|
||||
|
||||
/// Andrews Pitchfork — Alan Andrews' median-line tool drawn from the three most
|
||||
/// recent **swing pivots**, projected forward to the current bar.
|
||||
///
|
||||
/// ```text
|
||||
/// detect alternating swing highs/lows with a `strength`-bar fractal
|
||||
/// P0 = handle (oldest of the last three), P1, P2 = the next two
|
||||
/// M = midpoint of P1 and P2
|
||||
/// median(t) = P0 + slope·(t − t0) slope = (M − P0) / (M_t − t0)
|
||||
/// upper / lower = median(t) offset by the vertical gap to the higher / lower anchor
|
||||
/// ```
|
||||
///
|
||||
/// The pitchfork projects a "fork" of three parallel lines: a central **median
|
||||
/// line** drawn from a starting pivot through the midpoint of a later swing, plus
|
||||
/// two parallels passing through that swing's high and low. Price tends to
|
||||
/// oscillate around the median line and find support/resistance at the parallels.
|
||||
/// This streaming version detects the pivots automatically with a symmetric
|
||||
/// fractal of half-width `strength` (so each pivot is confirmed `strength` bars
|
||||
/// late) and keeps the three most recent alternating swings.
|
||||
///
|
||||
/// Because it depends on swing structure, readiness is **data-dependent**: the
|
||||
/// first output appears once three alternating pivots have been confirmed.
|
||||
/// `warmup_period` returns the minimum bars to confirm a single pivot. Each
|
||||
/// `update` is O(`strength`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, AndrewsPitchfork};
|
||||
///
|
||||
/// let mut indicator = AndrewsPitchfork::new(2).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..120 {
|
||||
/// let base = 100.0 + (f64::from(i) * 0.4).sin() * 10.0;
|
||||
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
|
||||
/// last = indicator.update(c);
|
||||
/// }
|
||||
/// // A swinging series eventually establishes a pitchfork.
|
||||
/// let _ = last;
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AndrewsPitchfork {
|
||||
strength: usize,
|
||||
window: VecDeque<Candle>,
|
||||
pivots: Vec<Pivot>,
|
||||
count: usize,
|
||||
last: Option<AndrewsPitchforkOutput>,
|
||||
}
|
||||
|
||||
impl AndrewsPitchfork {
|
||||
/// Construct an Andrews Pitchfork with the given fractal `strength` (bars on
|
||||
/// each side of a pivot).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `strength == 0`.
|
||||
pub fn new(strength: usize) -> Result<Self> {
|
||||
if strength == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
strength,
|
||||
window: VecDeque::with_capacity(2 * strength + 1),
|
||||
pivots: Vec::new(),
|
||||
count: 0,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured fractal strength.
|
||||
pub const fn strength(&self) -> usize {
|
||||
self.strength
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<AndrewsPitchforkOutput> {
|
||||
self.last
|
||||
}
|
||||
|
||||
/// Record a freshly confirmed pivot, keeping the last three alternating swings.
|
||||
fn record_pivot(&mut self, pivot: Pivot) {
|
||||
if let Some(last) = self.pivots.last_mut() {
|
||||
if last.is_high == pivot.is_high {
|
||||
// Same kind: keep the more extreme one (and its index).
|
||||
let more_extreme = if pivot.is_high {
|
||||
pivot.price > last.price
|
||||
} else {
|
||||
pivot.price < last.price
|
||||
};
|
||||
if more_extreme {
|
||||
*last = pivot;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.pivots.push(pivot);
|
||||
if self.pivots.len() > 3 {
|
||||
self.pivots.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn project(&self, tc: f64) -> Option<AndrewsPitchforkOutput> {
|
||||
let [p0, p1, p2] = self.pivots.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let mid_t = f64::midpoint(p1.index, p2.index);
|
||||
let mid_p = f64::midpoint(p1.price, p2.price);
|
||||
let slope = (mid_p - p0.price) / (mid_t - p0.index);
|
||||
let median = p0.price + slope * (tc - p0.index);
|
||||
let off1 = p1.price - (p0.price + slope * (p1.index - p0.index));
|
||||
let off2 = p2.price - (p0.price + slope * (p2.index - p0.index));
|
||||
Some(AndrewsPitchforkOutput {
|
||||
median,
|
||||
upper: median + off1.max(off2),
|
||||
lower: median + off1.min(off2),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for AndrewsPitchfork {
|
||||
type Input = Candle;
|
||||
type Output = AndrewsPitchforkOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<AndrewsPitchforkOutput> {
|
||||
self.count += 1;
|
||||
let span = 2 * self.strength + 1;
|
||||
if self.window.len() == span {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(candle);
|
||||
if self.window.len() == span {
|
||||
let center = self.window[self.strength];
|
||||
let is_high = self
|
||||
.window
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, c)| i == self.strength || c.high < center.high);
|
||||
let is_low = self
|
||||
.window
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, c)| i == self.strength || c.low > center.low);
|
||||
// Absolute index of the center bar (1-based count minus the right span).
|
||||
let center_index = (self.count - 1 - self.strength) as f64;
|
||||
if is_high && !is_low {
|
||||
self.record_pivot(Pivot {
|
||||
index: center_index,
|
||||
price: center.high,
|
||||
is_high: true,
|
||||
});
|
||||
} else if is_low && !is_high {
|
||||
self.record_pivot(Pivot {
|
||||
index: center_index,
|
||||
price: center.low,
|
||||
is_high: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
let tc = (self.count - 1) as f64;
|
||||
if let Some(out) = self.project(tc) {
|
||||
self.last = Some(out);
|
||||
return Some(out);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.pivots.clear();
|
||||
self.count = 0;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2 * self.strength + 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AndrewsPitchfork"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(high: f64, low: f64) -> Candle {
|
||||
Candle::new_unchecked(
|
||||
f64::midpoint(high, low),
|
||||
high,
|
||||
low,
|
||||
f64::midpoint(high, low),
|
||||
1_000.0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
/// A clean zig-zag that prints alternating swing highs and lows.
|
||||
fn zigzag() -> Vec<Candle> {
|
||||
let mut out = Vec::new();
|
||||
for i in 0..120 {
|
||||
let base = 100.0 + (f64::from(i) * 0.5).sin() * 10.0;
|
||||
out.push(c(base + 1.0, base - 1.0));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_strength() {
|
||||
assert!(matches!(AndrewsPitchfork::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let p = AndrewsPitchfork::new(2).unwrap();
|
||||
assert_eq!(p.strength(), 2);
|
||||
assert_eq!(p.warmup_period(), 5);
|
||||
assert_eq!(p.name(), "AndrewsPitchfork");
|
||||
assert!(!p.is_ready());
|
||||
assert_eq!(p.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_before_three_pivots() {
|
||||
let mut p = AndrewsPitchfork::new(2).unwrap();
|
||||
// Too few bars to ever confirm three alternating pivots.
|
||||
let out = p.batch(&[c(101.0, 99.0), c(102.0, 100.0), c(101.0, 99.0)]);
|
||||
assert!(out.iter().all(Option::is_none));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eventually_emits_on_swings() {
|
||||
let mut p = AndrewsPitchfork::new(2).unwrap();
|
||||
let out = p.batch(&zigzag());
|
||||
assert!(
|
||||
out.iter().any(Option::is_some),
|
||||
"a swinging series should form a pitchfork"
|
||||
);
|
||||
assert!(p.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upper_at_or_above_lower() {
|
||||
let mut p = AndrewsPitchfork::new(2).unwrap();
|
||||
for o in p.batch(&zigzag()).into_iter().flatten() {
|
||||
assert!(
|
||||
o.upper >= o.lower,
|
||||
"upper {} below lower {}",
|
||||
o.upper,
|
||||
o.lower
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut p = AndrewsPitchfork::new(2).unwrap();
|
||||
p.batch(&zigzag());
|
||||
assert!(p.is_ready());
|
||||
p.reset();
|
||||
assert!(!p.is_ready());
|
||||
assert_eq!(p.value(), None);
|
||||
assert_eq!(p.strength(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_pivot_keeps_more_extreme_same_kind() {
|
||||
let mut p = AndrewsPitchfork::new(2).unwrap();
|
||||
p.record_pivot(Pivot {
|
||||
index: 0.0,
|
||||
price: 100.0,
|
||||
is_high: true,
|
||||
});
|
||||
// A higher high of the same kind replaces the stored one.
|
||||
p.record_pivot(Pivot {
|
||||
index: 1.0,
|
||||
price: 105.0,
|
||||
is_high: true,
|
||||
});
|
||||
assert_eq!(p.pivots.len(), 1);
|
||||
assert_eq!(p.pivots[0].price, 105.0);
|
||||
// A lower high of the same kind is ignored.
|
||||
p.record_pivot(Pivot {
|
||||
index: 2.0,
|
||||
price: 102.0,
|
||||
is_high: true,
|
||||
});
|
||||
assert_eq!(p.pivots.len(), 1);
|
||||
assert_eq!(p.pivots[0].price, 105.0);
|
||||
// A low pivot of the other kind is appended.
|
||||
p.record_pivot(Pivot {
|
||||
index: 3.0,
|
||||
price: 90.0,
|
||||
is_high: false,
|
||||
});
|
||||
assert_eq!(p.pivots.len(), 2);
|
||||
// A lower low of the same kind replaces the stored low.
|
||||
p.record_pivot(Pivot {
|
||||
index: 4.0,
|
||||
price: 85.0,
|
||||
is_high: false,
|
||||
});
|
||||
assert_eq!(p.pivots[1].price, 85.0);
|
||||
// A higher low of the same kind is ignored.
|
||||
p.record_pivot(Pivot {
|
||||
index: 5.0,
|
||||
price: 88.0,
|
||||
is_high: false,
|
||||
});
|
||||
assert_eq!(p.pivots[1].price, 85.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles = zigzag();
|
||||
let batch = AndrewsPitchfork::new(2).unwrap().batch(&candles);
|
||||
let mut b = AndrewsPitchfork::new(2).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! Central Pivot Range (CPR) — the pivot plus its two central levels.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Output of [`CentralPivotRange`]: the pivot and the two central lines that
|
||||
/// bracket it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CentralPivotRangeOutput {
|
||||
/// Pivot point `(high + low + close) / 3`.
|
||||
pub pivot: f64,
|
||||
/// Top central line — the higher of the two central levels.
|
||||
pub tc: f64,
|
||||
/// Bottom central line — the lower of the two central levels.
|
||||
pub bc: f64,
|
||||
}
|
||||
|
||||
/// Central Pivot Range (CPR) — the classic pivot point flanked by two "central"
|
||||
/// levels whose separation gauges the day's expected character.
|
||||
///
|
||||
/// ```text
|
||||
/// pivot = (high + low + close) / 3
|
||||
/// bc' = (high + low) / 2
|
||||
/// tc' = 2·pivot − bc'
|
||||
/// TC = max(tc', bc'), BC = min(tc', bc')
|
||||
/// ```
|
||||
///
|
||||
/// The CPR is computed from the **previous** period's bar (feed it completed
|
||||
/// daily/weekly bars). The width of the range `TC − BC` is the headline read: a
|
||||
/// **narrow** CPR signals a likely trending day (price has little balance area to
|
||||
/// chew through), while a **wide** CPR signals a likely range-bound, balanced
|
||||
/// day. Price opening above the whole range is bullish, below it bearish, inside
|
||||
/// it neutral. The `tc'`/`bc'` formulas are symmetric about the pivot; this
|
||||
/// implementation labels the larger as `TC` and the smaller as `BC` so `TC >= BC`
|
||||
/// always holds.
|
||||
///
|
||||
/// There are no parameters and no warmup — each completed bar yields one CPR.
|
||||
/// Each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, CentralPivotRange};
|
||||
///
|
||||
/// let mut indicator = CentralPivotRange::new();
|
||||
/// let prev_day = Candle::new(101.0, 110.0, 90.0, 105.0, 1_000.0, 0).unwrap();
|
||||
/// let cpr = indicator.update(prev_day).unwrap();
|
||||
/// assert!(cpr.tc >= cpr.bc);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CentralPivotRange {
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl CentralPivotRange {
|
||||
/// Construct a new Central Pivot Range. The indicator is parameter-free.
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { ready: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for CentralPivotRange {
|
||||
type Input = Candle;
|
||||
type Output = CentralPivotRangeOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<CentralPivotRangeOutput> {
|
||||
let pivot = (candle.high + candle.low + candle.close) / 3.0;
|
||||
let bc_raw = f64::midpoint(candle.high, candle.low);
|
||||
let tc_raw = 2.0 * pivot - bc_raw;
|
||||
let tc = tc_raw.max(bc_raw);
|
||||
let bc = tc_raw.min(bc_raw);
|
||||
self.ready = true;
|
||||
Some(CentralPivotRangeOutput { pivot, tc, bc })
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ready = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ready
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"CentralPivotRange"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(high: f64, low: f64, close: f64) -> Candle {
|
||||
Candle::new_unchecked(close, high, low, close, 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let cpr = CentralPivotRange::new();
|
||||
assert_eq!(cpr.warmup_period(), 1);
|
||||
assert_eq!(cpr.name(), "CentralPivotRange");
|
||||
assert!(!cpr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formula_reference_values() {
|
||||
// H=110, L=90, C=105 -> pivot = 305/3; bc' = 100; tc' = 2*pivot - 100.
|
||||
let out = CentralPivotRange::new()
|
||||
.update(c(110.0, 90.0, 105.0))
|
||||
.unwrap();
|
||||
let pivot = 305.0 / 3.0;
|
||||
let bc_raw = 100.0;
|
||||
let tc_raw = 2.0 * pivot - bc_raw;
|
||||
assert!((out.pivot - pivot).abs() < 1e-12);
|
||||
assert!((out.tc - tc_raw.max(bc_raw)).abs() < 1e-12);
|
||||
assert!((out.bc - tc_raw.min(bc_raw)).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tc_never_below_bc() {
|
||||
let out = CentralPivotRange::new()
|
||||
.update(c(200.0, 100.0, 150.0))
|
||||
.unwrap();
|
||||
assert!(out.tc >= out.bc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_bar_collapses_range() {
|
||||
// H = L = C -> pivot = bc' = tc' = the price; range collapses.
|
||||
let out = CentralPivotRange::new()
|
||||
.update(c(50.0, 50.0, 50.0))
|
||||
.unwrap();
|
||||
assert_eq!(out.pivot, 50.0);
|
||||
assert_eq!(out.tc, 50.0);
|
||||
assert_eq!(out.bc, 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ready_after_first_update() {
|
||||
let mut cpr = CentralPivotRange::new();
|
||||
assert!(!cpr.is_ready());
|
||||
cpr.update(c(11.0, 9.0, 10.0));
|
||||
assert!(cpr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut cpr = CentralPivotRange::new();
|
||||
cpr.update(c(11.0, 9.0, 10.0));
|
||||
assert!(cpr.is_ready());
|
||||
cpr.reset();
|
||||
assert!(!cpr.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
|
||||
.collect();
|
||||
let batch = CentralPivotRange::new().batch(&candles);
|
||||
let mut b = CentralPivotRange::new();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ mod alpha;
|
||||
mod amihud_illiquidity;
|
||||
mod anchored_rsi;
|
||||
mod anchored_vwap;
|
||||
mod andrews_pitchfork;
|
||||
mod apo;
|
||||
mod aroon;
|
||||
mod aroon_oscillator;
|
||||
@@ -68,6 +69,7 @@ mod calmar_ratio;
|
||||
mod camarilla_pivots;
|
||||
mod cci;
|
||||
mod center_of_gravity;
|
||||
mod central_pivot_range;
|
||||
mod cfo;
|
||||
mod chaikin_oscillator;
|
||||
mod chaikin_volatility;
|
||||
@@ -257,6 +259,7 @@ mod modified_ma_stop;
|
||||
mod mom;
|
||||
mod morning_doji_star;
|
||||
mod morning_evening_star;
|
||||
mod murrey_math_lines;
|
||||
mod natr;
|
||||
mod new_highs_new_lows;
|
||||
mod nrtr;
|
||||
@@ -286,6 +289,7 @@ mod percent_b;
|
||||
mod percentage_trailing_stop;
|
||||
mod pgo;
|
||||
mod piercing_dark_cloud;
|
||||
mod pivot_reversal;
|
||||
mod plus_di;
|
||||
mod plus_dm;
|
||||
mod pmo;
|
||||
@@ -446,6 +450,7 @@ mod volume_oscillator;
|
||||
mod volume_profile;
|
||||
mod volume_rsi;
|
||||
mod volume_weighted_macd;
|
||||
mod volume_weighted_sr;
|
||||
mod vortex;
|
||||
mod vpin;
|
||||
mod vpt;
|
||||
@@ -494,6 +499,7 @@ pub use alpha::Alpha;
|
||||
pub use amihud_illiquidity::AmihudIlliquidity;
|
||||
pub use anchored_rsi::AnchoredRsi;
|
||||
pub use anchored_vwap::AnchoredVwap;
|
||||
pub use andrews_pitchfork::{AndrewsPitchfork, AndrewsPitchforkOutput};
|
||||
pub use apo::Apo;
|
||||
pub use aroon::{Aroon, AroonOutput};
|
||||
pub use aroon_oscillator::AroonOscillator;
|
||||
@@ -530,6 +536,7 @@ pub use calmar_ratio::CalmarRatio;
|
||||
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
|
||||
pub use cci::Cci;
|
||||
pub use center_of_gravity::CenterOfGravity;
|
||||
pub use central_pivot_range::{CentralPivotRange, CentralPivotRangeOutput};
|
||||
pub use cfo::Cfo;
|
||||
pub use chaikin_oscillator::ChaikinOscillator;
|
||||
pub use chaikin_volatility::ChaikinVolatility;
|
||||
@@ -719,6 +726,7 @@ pub use modified_ma_stop::{ModifiedMaStop, ModifiedMaStopOutput};
|
||||
pub use mom::Mom;
|
||||
pub use morning_doji_star::MorningDojiStar;
|
||||
pub use morning_evening_star::MorningEveningStar;
|
||||
pub use murrey_math_lines::{MurreyMathLines, MurreyMathLinesOutput};
|
||||
pub use natr::Natr;
|
||||
pub use new_highs_new_lows::NewHighsNewLows;
|
||||
pub use nrtr::{Nrtr, NrtrOutput};
|
||||
@@ -748,6 +756,7 @@ pub use percent_b::PercentB;
|
||||
pub use percentage_trailing_stop::PercentageTrailingStop;
|
||||
pub use pgo::Pgo;
|
||||
pub use piercing_dark_cloud::PiercingDarkCloud;
|
||||
pub use pivot_reversal::PivotReversal;
|
||||
pub use plus_di::PlusDi;
|
||||
pub use plus_dm::PlusDm;
|
||||
pub use pmo::Pmo;
|
||||
@@ -908,6 +917,7 @@ pub use volume_oscillator::VolumeOscillator;
|
||||
pub use volume_profile::{VolumeProfile, VolumeProfileOutput};
|
||||
pub use volume_rsi::VolumeRsi;
|
||||
pub use volume_weighted_macd::{VolumeWeightedMacd, VolumeWeightedMacdOutput};
|
||||
pub use volume_weighted_sr::{VolumeWeightedSr, VolumeWeightedSrOutput};
|
||||
pub use vortex::{Vortex, VortexOutput};
|
||||
pub use vpin::Vpin;
|
||||
pub use vpt::VolumePriceTrend;
|
||||
@@ -1272,6 +1282,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"DemarkPivots",
|
||||
"WilliamsFractals",
|
||||
"ZigZag",
|
||||
"CentralPivotRange",
|
||||
"MurreyMathLines",
|
||||
"AndrewsPitchfork",
|
||||
"VolumeWeightedSr",
|
||||
"PivotReversal",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -1540,6 +1555,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 462, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 467, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
//! Murrey Math Lines — the eighths grid over the recent trading range.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Output of [`MurreyMathLines`]: the nine Murrey Math levels from the bottom
|
||||
/// (`mm0_8`, ultimate support) to the top (`mm8_8`, ultimate resistance).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MurreyMathLinesOutput {
|
||||
/// 8/8 — ultimate resistance (top of the frame).
|
||||
pub mm8_8: f64,
|
||||
/// 7/8 — "weak, stall and reverse" (overbought).
|
||||
pub mm7_8: f64,
|
||||
/// 6/8 — upper pivot / reversal line.
|
||||
pub mm6_8: f64,
|
||||
/// 5/8 — top of the normal trading range.
|
||||
pub mm5_8: f64,
|
||||
/// 4/8 — the major pivot (mean) line.
|
||||
pub mm4_8: f64,
|
||||
/// 3/8 — bottom of the normal trading range.
|
||||
pub mm3_8: f64,
|
||||
/// 2/8 — lower pivot / reversal line.
|
||||
pub mm2_8: f64,
|
||||
/// 1/8 — "weak, stall and reverse" (oversold).
|
||||
pub mm1_8: f64,
|
||||
/// 0/8 — ultimate support (bottom of the frame).
|
||||
pub mm0_8: f64,
|
||||
}
|
||||
|
||||
/// Murrey Math Lines — T. H. Murrey's grid that divides the recent trading range
|
||||
/// into eighths, each acting as support/resistance.
|
||||
///
|
||||
/// ```text
|
||||
/// HH = highest high over `period`, LL = lowest low over `period`
|
||||
/// step = (HH − LL) / 8
|
||||
/// mm{i}_8 = LL + i · step for i = 0..8
|
||||
/// ```
|
||||
///
|
||||
/// Murrey Math (a Gann-derived framework) holds that price gravitates to and
|
||||
/// reverses at the eighth divisions of its range. The **4/8** line is the major
|
||||
/// pivot (mean); **0/8** and **8/8** are the strongest support and resistance;
|
||||
/// **3/8** and **5/8** bound the "normal" trading range, while **1/8**/**7/8** are
|
||||
/// the weak "stall and reverse" lines. This implementation uses the price-derived
|
||||
/// eighths over a rolling high-low frame (the practical core of the method) rather
|
||||
/// than Murrey's full octave-quantised frame sizing, so the levels track the
|
||||
/// instrument's actual recent range.
|
||||
///
|
||||
/// The first value lands after `period` inputs; each `update` rescans the frame in
|
||||
/// O(`period`). A degenerate flat frame (`HH == LL`) collapses every line onto the
|
||||
/// price.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, MurreyMathLines};
|
||||
///
|
||||
/// let mut indicator = MurreyMathLines::new(64).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..120 {
|
||||
/// let base = 100.0 + (f64::from(i) * 0.3).sin() * 10.0;
|
||||
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
|
||||
/// last = indicator.update(c);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MurreyMathLines {
|
||||
period: usize,
|
||||
highs: VecDeque<f64>,
|
||||
lows: VecDeque<f64>,
|
||||
last: Option<MurreyMathLinesOutput>,
|
||||
}
|
||||
|
||||
impl MurreyMathLines {
|
||||
/// Construct Murrey Math Lines over a `period`-bar high-low frame.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
highs: VecDeque::with_capacity(period),
|
||||
lows: VecDeque::with_capacity(period),
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured frame period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<MurreyMathLinesOutput> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MurreyMathLines {
|
||||
type Input = Candle;
|
||||
type Output = MurreyMathLinesOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<MurreyMathLinesOutput> {
|
||||
if self.highs.len() == self.period {
|
||||
self.highs.pop_front();
|
||||
self.lows.pop_front();
|
||||
}
|
||||
self.highs.push_back(candle.high);
|
||||
self.lows.push_back(candle.low);
|
||||
if self.highs.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let hh = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
|
||||
let ll = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
|
||||
let step = (hh - ll) / 8.0;
|
||||
let level = |i: f64| ll + i * step;
|
||||
let out = MurreyMathLinesOutput {
|
||||
mm0_8: level(0.0),
|
||||
mm1_8: level(1.0),
|
||||
mm2_8: level(2.0),
|
||||
mm3_8: level(3.0),
|
||||
mm4_8: level(4.0),
|
||||
mm5_8: level(5.0),
|
||||
mm6_8: level(6.0),
|
||||
mm7_8: level(7.0),
|
||||
mm8_8: level(8.0),
|
||||
};
|
||||
self.last = Some(out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.highs.clear();
|
||||
self.lows.clear();
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MurreyMathLines"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64) -> Candle {
|
||||
Candle::new_unchecked(low, high, low, f64::midpoint(high, low), 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(MurreyMathLines::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let m = MurreyMathLines::new(64).unwrap();
|
||||
assert_eq!(m.period(), 64);
|
||||
assert_eq!(m.warmup_period(), 64);
|
||||
assert_eq!(m.name(), "MurreyMathLines");
|
||||
assert!(!m.is_ready());
|
||||
assert_eq!(m.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut m = MurreyMathLines::new(4).unwrap();
|
||||
let candles: Vec<Candle> = (0..6)
|
||||
.map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
|
||||
.collect();
|
||||
let out = m.batch(&candles);
|
||||
for v in out.iter().take(3) {
|
||||
assert!(v.is_none());
|
||||
}
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eighths_are_evenly_spaced() {
|
||||
// Frame [100, 180] over the window -> step = 10.
|
||||
let mut m = MurreyMathLines::new(2).unwrap();
|
||||
let out = m
|
||||
.batch(&[c(180.0, 100.0), c(180.0, 100.0)])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(out.mm0_8, 100.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.mm4_8, 140.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.mm8_8, 180.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.mm1_8 - out.mm0_8, 10.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn levels_are_ordered() {
|
||||
let mut m = MurreyMathLines::new(10).unwrap();
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
c(
|
||||
110.0 + (f64::from(i) * 0.3).sin() * 8.0,
|
||||
90.0 + (f64::from(i) * 0.3).cos() * 8.0,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for o in m.batch(&candles).into_iter().flatten() {
|
||||
assert!(o.mm0_8 <= o.mm4_8 && o.mm4_8 <= o.mm8_8);
|
||||
assert!(o.mm3_8 <= o.mm5_8);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_frame_collapses() {
|
||||
let mut m = MurreyMathLines::new(3).unwrap();
|
||||
let out = m
|
||||
.batch(&[c(50.0, 50.0), c(50.0, 50.0), c(50.0, 50.0)])
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.unwrap();
|
||||
assert_relative_eq!(out.mm0_8, 50.0, epsilon = 1e-12);
|
||||
assert_relative_eq!(out.mm8_8, 50.0, epsilon = 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut m = MurreyMathLines::new(4).unwrap();
|
||||
m.batch(
|
||||
&(0..6)
|
||||
.map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i)))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert!(m.is_ready());
|
||||
m.reset();
|
||||
assert!(!m.is_ready());
|
||||
assert_eq!(m.value(), None);
|
||||
assert_eq!(m.update(c(101.0, 99.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..120)
|
||||
.map(|i| {
|
||||
c(
|
||||
110.0 + (f64::from(i) * 0.25).sin() * 9.0,
|
||||
90.0 + (f64::from(i) * 0.25).cos() * 9.0,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let batch = MurreyMathLines::new(64).unwrap().batch(&candles);
|
||||
let mut b = MurreyMathLines::new(64).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
//! Pivot Reversal — a breakout signal off the most recent confirmed swing pivots.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Pivot Reversal — emits a reversal **breakout signal** when price closes through
|
||||
/// the most recently confirmed swing pivot.
|
||||
///
|
||||
/// ```text
|
||||
/// pivot high: a bar whose high is strictly above the `left` bars before and the
|
||||
/// `right` bars after it (confirmed `right` bars late)
|
||||
/// pivot low : the mirror on lows
|
||||
/// signal = +1 when close crosses above the last confirmed pivot high
|
||||
/// signal = −1 when close crosses below the last confirmed pivot low
|
||||
/// signal = 0 otherwise
|
||||
/// ```
|
||||
///
|
||||
/// Unlike [`WilliamsFractals`](crate::WilliamsFractals), which merely *marks* the
|
||||
/// swing points, Pivot Reversal turns them into an actionable entry: once a swing
|
||||
/// high is confirmed it becomes a breakout trigger — a close back above it signals
|
||||
/// a bullish reversal — and likewise a close below a confirmed swing low signals a
|
||||
/// bearish reversal. This is the logic of the classic "Pivot Reversal" strategy.
|
||||
/// Signals fire only on the **crossing** bar, not while price sits beyond the
|
||||
/// level.
|
||||
///
|
||||
/// The first signal can appear once `left + right + 1` bars exist (a pivot needs
|
||||
/// neighbours on both sides). The output is `+1` / `0` / `−1`. Each `update` is
|
||||
/// O(`left + right`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, PivotReversal};
|
||||
///
|
||||
/// let mut indicator = PivotReversal::new(2, 2).unwrap();
|
||||
/// let mut fired = false;
|
||||
/// for i in 0..60 {
|
||||
/// let base = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
|
||||
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
|
||||
/// match indicator.update(c) {
|
||||
/// Some(s) if s != 0.0 => fired = true,
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// }
|
||||
/// let _ = fired;
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PivotReversal {
|
||||
left: usize,
|
||||
right: usize,
|
||||
window: VecDeque<Candle>,
|
||||
pivot_high: Option<f64>,
|
||||
pivot_low: Option<f64>,
|
||||
prev_close: Option<f64>,
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
impl PivotReversal {
|
||||
/// Construct a Pivot Reversal with `left` bars before and `right` bars after
|
||||
/// the pivot.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `left` or `right` is `0`.
|
||||
pub fn new(left: usize, right: usize) -> Result<Self> {
|
||||
if left == 0 || right == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
left,
|
||||
right,
|
||||
window: VecDeque::with_capacity(left + right + 1),
|
||||
pivot_high: None,
|
||||
pivot_low: None,
|
||||
prev_close: None,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured `(left, right)` strengths.
|
||||
pub const fn params(&self) -> (usize, usize) {
|
||||
(self.left, self.right)
|
||||
}
|
||||
|
||||
/// Most recent confirmed pivot-high level, if any.
|
||||
pub const fn pivot_high(&self) -> Option<f64> {
|
||||
self.pivot_high
|
||||
}
|
||||
|
||||
/// Most recent confirmed pivot-low level, if any.
|
||||
pub const fn pivot_low(&self) -> Option<f64> {
|
||||
self.pivot_low
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<f64> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for PivotReversal {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let close = candle.close;
|
||||
if self.window.len() == self.left + self.right + 1 {
|
||||
self.window.pop_front();
|
||||
}
|
||||
self.window.push_back(candle);
|
||||
if self.window.len() < self.left + self.right + 1 {
|
||||
self.prev_close = Some(close);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Confirm the pivot candidate sitting `right` bars back.
|
||||
let cand = self.window[self.left];
|
||||
let is_high = self
|
||||
.window
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, c)| i == self.left || c.high < cand.high);
|
||||
let is_low = self
|
||||
.window
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, c)| i == self.left || c.low > cand.low);
|
||||
if is_high {
|
||||
self.pivot_high = Some(cand.high);
|
||||
}
|
||||
if is_low {
|
||||
self.pivot_low = Some(cand.low);
|
||||
}
|
||||
|
||||
// Breakout crossing of the latest confirmed pivots by the current close.
|
||||
let mut signal = 0.0;
|
||||
if let (Some(ph), Some(prev)) = (self.pivot_high, self.prev_close) {
|
||||
if close > ph && prev <= ph {
|
||||
signal = 1.0;
|
||||
}
|
||||
}
|
||||
if let (Some(pl), Some(prev)) = (self.pivot_low, self.prev_close) {
|
||||
if close < pl && prev >= pl {
|
||||
signal = -1.0;
|
||||
}
|
||||
}
|
||||
self.prev_close = Some(close);
|
||||
self.last = Some(signal);
|
||||
Some(signal)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.window.clear();
|
||||
self.pivot_high = None;
|
||||
self.pivot_low = None;
|
||||
self.prev_close = None;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.left + self.right + 1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"PivotReversal"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
|
||||
fn c(high: f64, low: f64, close: f64) -> Candle {
|
||||
Candle::new_unchecked(close, high, low, close, 1_000.0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_params() {
|
||||
assert!(matches!(PivotReversal::new(0, 2), Err(Error::PeriodZero)));
|
||||
assert!(matches!(PivotReversal::new(2, 0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let p = PivotReversal::new(2, 2).unwrap();
|
||||
assert_eq!(p.params(), (2, 2));
|
||||
assert_eq!(p.warmup_period(), 5);
|
||||
assert_eq!(p.name(), "PivotReversal");
|
||||
assert!(!p.is_ready());
|
||||
assert_eq!(p.value(), None);
|
||||
assert_eq!(p.pivot_high(), None);
|
||||
assert_eq!(p.pivot_low(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut p = PivotReversal::new(1, 1).unwrap();
|
||||
let out = p.batch(&[c(10.0, 9.0, 9.5), c(12.0, 11.0, 11.5), c(10.0, 9.0, 9.5)]);
|
||||
assert!(out[0].is_none());
|
||||
assert!(out[1].is_none());
|
||||
assert!(out[2].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirms_pivot_high() {
|
||||
// bar1 is a local high; once bar2 arrives it is confirmed.
|
||||
let mut p = PivotReversal::new(1, 1).unwrap();
|
||||
p.batch(&[c(10.0, 9.0, 9.5), c(12.0, 11.0, 11.5), c(10.0, 9.0, 9.5)]);
|
||||
assert_eq!(p.pivot_high(), Some(12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirms_pivot_low() {
|
||||
let mut p = PivotReversal::new(1, 1).unwrap();
|
||||
p.batch(&[c(12.0, 11.0, 11.5), c(10.0, 8.0, 8.5), c(12.0, 11.0, 11.5)]);
|
||||
assert_eq!(p.pivot_low(), Some(8.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breakout_above_pivot_high_signals_plus_one() {
|
||||
let mut p = PivotReversal::new(1, 1).unwrap();
|
||||
// Form a pivot high at 12, then a close above 12 crosses it.
|
||||
let candles = [
|
||||
c(10.0, 9.0, 9.5), // index 0
|
||||
c(12.0, 11.0, 11.5), // pivot-high candidate
|
||||
c(10.0, 9.0, 9.5), // confirms pivot high = 12
|
||||
c(11.0, 9.0, 9.0), // close 9.0 (below 12)
|
||||
c(14.0, 12.5, 13.0), // close 13.0 > 12 and prev 9.0 <= 12 -> +1
|
||||
];
|
||||
let out = p.batch(&candles);
|
||||
assert_eq!(out.last().unwrap(), &Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breakdown_below_pivot_low_signals_minus_one() {
|
||||
let mut p = PivotReversal::new(1, 1).unwrap();
|
||||
let candles = [
|
||||
c(12.0, 11.0, 11.5),
|
||||
c(10.0, 8.0, 8.5), // pivot-low candidate
|
||||
c(12.0, 11.0, 11.5), // confirms pivot low = 8
|
||||
c(12.0, 9.0, 11.0), // close 11 (above 8)
|
||||
c(9.0, 6.0, 7.0), // close 7 < 8 and prev 11 >= 8 -> -1
|
||||
];
|
||||
let out = p.batch(&candles);
|
||||
assert_eq!(out.last().unwrap(), &Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_break_is_zero() {
|
||||
let mut p = PivotReversal::new(1, 1).unwrap();
|
||||
let candles = [
|
||||
c(10.0, 9.0, 9.5),
|
||||
c(12.0, 11.0, 11.5),
|
||||
c(10.0, 9.0, 9.5),
|
||||
c(10.5, 9.0, 9.8),
|
||||
];
|
||||
let out = p.batch(&candles);
|
||||
assert_eq!(out.last().unwrap(), &Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut p = PivotReversal::new(1, 1).unwrap();
|
||||
p.batch(&[c(10.0, 9.0, 9.5), c(12.0, 11.0, 11.5), c(10.0, 9.0, 9.5)]);
|
||||
assert!(p.is_ready());
|
||||
p.reset();
|
||||
assert!(!p.is_ready());
|
||||
assert_eq!(p.value(), None);
|
||||
assert_eq!(p.pivot_high(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..80)
|
||||
.map(|i| {
|
||||
let base = 100.0 + (f64::from(i) * 0.4).sin() * 6.0;
|
||||
c(base + 1.0, base - 1.0, base)
|
||||
})
|
||||
.collect();
|
||||
let batch = PivotReversal::new(2, 2).unwrap().batch(&candles);
|
||||
let mut b = PivotReversal::new(2, 2).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
//! Volume-Weighted Support/Resistance — a volume-weighted high/low band.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Output of [`VolumeWeightedSr`]: the volume-weighted support and resistance
|
||||
/// levels over the lookback.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct VolumeWeightedSrOutput {
|
||||
/// Volume-weighted average low — the support level.
|
||||
pub support: f64,
|
||||
/// Volume-weighted average high — the resistance level.
|
||||
pub resistance: f64,
|
||||
}
|
||||
|
||||
/// Volume-Weighted Support/Resistance — a band whose edges are the
|
||||
/// **volume-weighted** average of the recent highs (resistance) and lows
|
||||
/// (support), so the levels gravitate toward the prices where trading actually
|
||||
/// happened.
|
||||
///
|
||||
/// ```text
|
||||
/// support = Σ(low_i · volume_i) / Σ volume_i over the window
|
||||
/// resistance = Σ(high_i · volume_i) / Σ volume_i over the window
|
||||
/// ```
|
||||
///
|
||||
/// Plain high/low channels (e.g. [`Donchian`](crate::Donchian)) weight every bar
|
||||
/// equally, so a thin spike sets the boundary. Volume-weighting pulls the support
|
||||
/// and resistance toward the highs and lows that carried real volume — the prices
|
||||
/// the market agreed mattered — giving levels that tend to hold better. The
|
||||
/// distance between the two is a volume-aware range estimate. If the window's
|
||||
/// volume is all zero the band falls back to the equal-weighted average high and
|
||||
/// low.
|
||||
///
|
||||
/// The first value lands after `period` inputs; each `update` is O(1).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, VolumeWeightedSr};
|
||||
///
|
||||
/// let mut indicator = VolumeWeightedSr::new(20).unwrap();
|
||||
/// let mut last = None;
|
||||
/// for i in 0..40 {
|
||||
/// let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
|
||||
/// let c = Candle::new(base, base + 2.0, base - 2.0, base, 1_000.0 + f64::from(i), 0).unwrap();
|
||||
/// last = indicator.update(c);
|
||||
/// }
|
||||
/// assert!(last.is_some());
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolumeWeightedSr {
|
||||
period: usize,
|
||||
highs: VecDeque<f64>,
|
||||
lows: VecDeque<f64>,
|
||||
volumes: VecDeque<f64>,
|
||||
sum_hv: f64,
|
||||
sum_lv: f64,
|
||||
sum_v: f64,
|
||||
sum_h: f64,
|
||||
sum_l: f64,
|
||||
last: Option<VolumeWeightedSrOutput>,
|
||||
}
|
||||
|
||||
impl VolumeWeightedSr {
|
||||
/// Construct a volume-weighted S/R band over `period` bars.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::PeriodZero`] if `period == 0`.
|
||||
pub fn new(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
highs: VecDeque::with_capacity(period),
|
||||
lows: VecDeque::with_capacity(period),
|
||||
volumes: VecDeque::with_capacity(period),
|
||||
sum_hv: 0.0,
|
||||
sum_lv: 0.0,
|
||||
sum_v: 0.0,
|
||||
sum_h: 0.0,
|
||||
sum_l: 0.0,
|
||||
last: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured lookback period.
|
||||
pub const fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
/// Current value if available.
|
||||
pub const fn value(&self) -> Option<VolumeWeightedSrOutput> {
|
||||
self.last
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for VolumeWeightedSr {
|
||||
type Input = Candle;
|
||||
type Output = VolumeWeightedSrOutput;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<VolumeWeightedSrOutput> {
|
||||
if self.highs.len() == self.period {
|
||||
let h = self.highs.pop_front().expect("non-empty");
|
||||
let l = self.lows.pop_front().expect("non-empty");
|
||||
let v = self.volumes.pop_front().expect("non-empty");
|
||||
self.sum_hv -= h * v;
|
||||
self.sum_lv -= l * v;
|
||||
self.sum_v -= v;
|
||||
self.sum_h -= h;
|
||||
self.sum_l -= l;
|
||||
}
|
||||
self.highs.push_back(candle.high);
|
||||
self.lows.push_back(candle.low);
|
||||
self.volumes.push_back(candle.volume);
|
||||
self.sum_hv += candle.high * candle.volume;
|
||||
self.sum_lv += candle.low * candle.volume;
|
||||
self.sum_v += candle.volume;
|
||||
self.sum_h += candle.high;
|
||||
self.sum_l += candle.low;
|
||||
if self.highs.len() < self.period {
|
||||
return None;
|
||||
}
|
||||
let n = self.period as f64;
|
||||
let (support, resistance) = if self.sum_v > 0.0 {
|
||||
(self.sum_lv / self.sum_v, self.sum_hv / self.sum_v)
|
||||
} else {
|
||||
(self.sum_l / n, self.sum_h / n)
|
||||
};
|
||||
let out = VolumeWeightedSrOutput {
|
||||
support,
|
||||
resistance,
|
||||
};
|
||||
self.last = Some(out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.highs.clear();
|
||||
self.lows.clear();
|
||||
self.volumes.clear();
|
||||
self.sum_hv = 0.0;
|
||||
self.sum_lv = 0.0;
|
||||
self.sum_v = 0.0;
|
||||
self.sum_h = 0.0;
|
||||
self.sum_l = 0.0;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.last.is_some()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"VolumeWeightedSr"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::BatchExt;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
fn c(high: f64, low: f64, volume: f64) -> Candle {
|
||||
Candle::new_unchecked(low, high, low, f64::midpoint(high, low), volume, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(matches!(VolumeWeightedSr::new(0), Err(Error::PeriodZero)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let v = VolumeWeightedSr::new(20).unwrap();
|
||||
assert_eq!(v.period(), 20);
|
||||
assert_eq!(v.warmup_period(), 20);
|
||||
assert_eq!(v.name(), "VolumeWeightedSr");
|
||||
assert!(!v.is_ready());
|
||||
assert_eq!(v.value(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_emission_at_warmup_period() {
|
||||
let mut v = VolumeWeightedSr::new(4).unwrap();
|
||||
let candles: Vec<Candle> = (0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect();
|
||||
let out = v.batch(&candles);
|
||||
for o in out.iter().take(3) {
|
||||
assert!(o.is_none());
|
||||
}
|
||||
assert!(out[3].is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn support_below_resistance() {
|
||||
let mut v = VolumeWeightedSr::new(10).unwrap();
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
c(
|
||||
110.0 + (f64::from(i) * 0.3).sin() * 5.0,
|
||||
90.0 + (f64::from(i) * 0.3).cos() * 5.0,
|
||||
1_000.0 + f64::from(i),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for o in v.batch(&candles).into_iter().flatten() {
|
||||
assert!(o.support <= o.resistance);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weights_toward_high_volume_bars() {
|
||||
// Three low-volume bars at [98,102] and one heavy bar at [108,112]; the
|
||||
// resistance should be pulled toward the heavy bar's high.
|
||||
let mut v = VolumeWeightedSr::new(4).unwrap();
|
||||
let candles = [
|
||||
c(102.0, 98.0, 100.0),
|
||||
c(102.0, 98.0, 100.0),
|
||||
c(102.0, 98.0, 100.0),
|
||||
c(112.0, 108.0, 9_000.0),
|
||||
];
|
||||
let out = v.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
// Volume-weighted resistance sits much closer to 112 than the simple mean (104.5).
|
||||
assert!(
|
||||
out.resistance > 108.0,
|
||||
"resistance {} should lean to the heavy bar",
|
||||
out.resistance
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_volume_falls_back_to_equal_weight() {
|
||||
let mut v = VolumeWeightedSr::new(3).unwrap();
|
||||
let candles = [
|
||||
c(102.0, 98.0, 0.0),
|
||||
c(104.0, 96.0, 0.0),
|
||||
c(106.0, 94.0, 0.0),
|
||||
];
|
||||
let out = v.batch(&candles).into_iter().flatten().last().unwrap();
|
||||
// Equal-weight averages: high mean = 104, low mean = 96.
|
||||
assert_relative_eq!(out.resistance, 104.0, epsilon = 1e-9);
|
||||
assert_relative_eq!(out.support, 96.0, epsilon = 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut v = VolumeWeightedSr::new(4).unwrap();
|
||||
v.batch(&(0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect::<Vec<_>>());
|
||||
assert!(v.is_ready());
|
||||
v.reset();
|
||||
assert!(!v.is_ready());
|
||||
assert_eq!(v.value(), None);
|
||||
assert_eq!(v.update(c(102.0, 98.0, 1_000.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..120)
|
||||
.map(|i| {
|
||||
c(
|
||||
110.0 + (f64::from(i) * 0.25).sin() * 9.0,
|
||||
90.0 + (f64::from(i) * 0.25).cos() * 9.0,
|
||||
1_000.0 + f64::from(i),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let batch = VolumeWeightedSr::new(20).unwrap().batch(&candles);
|
||||
let mut b = VolumeWeightedSr::new(20).unwrap();
|
||||
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
}
|
||||
@@ -60,14 +60,15 @@ pub use indicators::{
|
||||
AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCci, AdaptiveCycle,
|
||||
AdaptiveLaguerreFilter, AdaptiveRsi, Adl, AdvanceBlock, AdvanceDecline, AdvanceDeclineRatio,
|
||||
Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma, Alpha, AmihudIlliquidity, AnchoredRsi,
|
||||
AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput,
|
||||
AtrRatchet, AtrRatchetOutput, AtrTrailingStop, AutoFib, AutoFibOutput, Autocorrelation,
|
||||
AutocorrelationPeriodogram, AverageDailyRange, AverageDrawdown, AvgPrice, AwesomeOscillator,
|
||||
AwesomeOscillatorHistogram, BalanceOfPower, BandpassFilter, Bat, BeltHold, Beta,
|
||||
BetaNeutralSpread, BetterVolume, BipowerVariation, BodySizePct, BollingerBands,
|
||||
BollingerBandwidth, BollingerOutput, BomarBands, BomarBandsOutput, BreadthThrust, Breakaway,
|
||||
BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput,
|
||||
Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility,
|
||||
AnchoredVwap, AndrewsPitchfork, AndrewsPitchforkOutput, Apo, Aroon, AroonOscillator,
|
||||
AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrRatchet, AtrRatchetOutput, AtrTrailingStop,
|
||||
AutoFib, AutoFibOutput, Autocorrelation, AutocorrelationPeriodogram, AverageDailyRange,
|
||||
AverageDrawdown, AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower,
|
||||
BandpassFilter, Bat, BeltHold, Beta, BetaNeutralSpread, BetterVolume, BipowerVariation,
|
||||
BodySizePct, BollingerBands, BollingerBandwidth, BollingerOutput, BomarBands, BomarBandsOutput,
|
||||
BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio,
|
||||
Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, CentralPivotRange,
|
||||
CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility,
|
||||
ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex,
|
||||
ClassicPivots, ClassicPivotsOutput, CloseVsOpen, ClosingMarubozu, Cmo, CoefficientOfVariation,
|
||||
Cointegration, CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi,
|
||||
@@ -107,25 +108,25 @@ pub use indicators::{
|
||||
MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex,
|
||||
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
|
||||
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
|
||||
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nrtr,
|
||||
NrtrOutput, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta,
|
||||
OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap,
|
||||
OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore,
|
||||
PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB,
|
||||
PercentageTrailingStop, Pgo, PiercingDarkCloud, PlusDi, PlusDm, Pmo, PointAndFigureBars,
|
||||
PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, ProjectionBands,
|
||||
ProjectionBandsOutput, ProjectionOscillator, Psar, Pvi, Qqe, QqeOutput, Qstick, QuartileBands,
|
||||
QuartileBandsOutput, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility,
|
||||
RecoveryFactor, RectangleRange, Reflex, RegimeLabel, RelativeStrengthAB,
|
||||
RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, RisingThreeMethods, Rmi,
|
||||
Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure, RollingCorrelation,
|
||||
RollingCovariance, RollingIqr, RollingMinMaxScaler, RollingPercentileRank, RollingQuantile,
|
||||
RollingVwap, RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput, SampleEntropy,
|
||||
SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionHighLowOutput, SessionRange,
|
||||
SessionRangeOutput, SessionVwap, ShannonEntropy, Shark, SharpeRatio, ShootingStar, ShortLine,
|
||||
SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma, SortinoRatio,
|
||||
SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands,
|
||||
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
|
||||
MurreyMathLinesOutput, Natr, NewHighsNewLows, Nrtr, NrtrOutput, Nvi, OIPriceDivergence,
|
||||
OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange,
|
||||
OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
|
||||
OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
|
||||
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
|
||||
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
|
||||
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
|
||||
PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar,
|
||||
Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared,
|
||||
RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel,
|
||||
RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan,
|
||||
RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure,
|
||||
RollingCorrelation, RollingCovariance, RollingIqr, RollingMinMaxScaler, RollingPercentileRank,
|
||||
RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput,
|
||||
SampleEntropy, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionHighLowOutput,
|
||||
SessionRange, SessionRangeOutput, SessionVwap, ShannonEntropy, Shark, SharpeRatio,
|
||||
ShootingStar, ShortLine, SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma,
|
||||
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands,
|
||||
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
|
||||
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
|
||||
StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, SuperSmoother,
|
||||
@@ -143,12 +144,12 @@ pub use indicators::{
|
||||
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone,
|
||||
VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile,
|
||||
VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
|
||||
VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, Vortex,
|
||||
VortexOutput, Vpin, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm,
|
||||
WaveTrend, WaveTrendOutput, Wedge, WeightedClose, WickRatio, WilliamsFractals,
|
||||
WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots, WoodiePivotsOutput,
|
||||
YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput,
|
||||
Zlema, FAMILIES, T3,
|
||||
VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr,
|
||||
VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
|
||||
WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
|
||||
WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
|
||||
ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||
// line so the indicator-count tooling (which scans the braced block above and
|
||||
|
||||
Reference in New Issue
Block a user