use super::averages::{MACD_SCRATCH, positive, shorter};
use super::bands::{positive_finite, suffixed};
use super::kernels::{
self, js_max, js_min, mean_values_reads_from, rolling_extreme, rolling_mean_values,
rolling_mean_values_into, wilder_values,
};
use super::kernels::{Ema, ma_column, rolling_extremes_strict};
use super::{MaType, Study};
use crate::error::{PondError, Result};
use crate::par;
use crate::series::TimeSeries;
study_options! {
DirectionalMovement, prefix = "dmi", reads [high, low, close] {
period: usize = 14,
}
}
study_options! {
Aroon, prefix = "aroon", reads [high, low] {
period: usize = 25,
}
}
study_options! {
Vortex, prefix = "vi", reads [high, low, close] {
period: usize = 14,
}
}
study_options! {
RandomWalkIndex, prefix = "rwi", reads [high, low, close] {
period: usize = 14,
}
}
study_options! {
SuperTrend, prefix = "st", reads [high, low, close] {
period: usize = 10,
multiplier: f64 = 3.0,
}
}
study_options! {
ParabolicSar, prefix = "psar", reads [high, low] {
step: f64 = 0.02,
max_step: f64 = 0.2,
}
}
study_options! {
AtrTrailingStop, prefix = "ats", reads [high, low, close] {
period: usize = 14,
multiplier: f64 = 3.0,
}
}
impl TimeSeries {
pub fn directional_movement(&self, options: DirectionalMovement) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::directional_movement";
positive(CALL, "period", options.period)?;
let names = suffixed(options.name(), ["PlusDi", "MinusDi", "Dx", "Adx", "Adxr"]);
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let period = options.period;
let (range, (plus, minus)) = par::join(
|| kernels::atr_values(&high, &low, &close, period),
|| smoothed_directional_movement(&high, &low, period),
);
let n = high.len();
let (mut di_plus, mut di_minus, mut dx) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
par::fill_chunks3(
&mut di_plus,
&mut di_minus,
&mut dx,
1 << 14,
|start, a, b, c| {
for k in 0..a.len() {
let i = start + k;
let p = (100.0 * plus[i]) / range[i];
let m = (100.0 * minus[i]) / range[i];
let total = p + m;
a[k] = p;
b[k] = m;
c[k] = if total == 0.0 {
0.0
} else {
(100.0 * (p - m).abs()) / total
};
}
},
);
let adx = wilder_values(&dx, period);
let back = period - 1;
let mut adxr = vec![0.0; n];
par::fill_chunks(&mut adxr, &mut [], 1 << 14, |start, out, _| {
for (k, slot) in out.iter_mut().enumerate() {
let i = start + k;
*slot = if i < back {
f64::NAN
} else {
(adx[i] + adx[i - back]) / 2.0
};
}
});
self.study_outputs(
CALL,
names.into_iter().zip([di_plus, di_minus, dx, adx, adxr]),
)
}
pub fn aroon(&self, options: Aroon) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::aroon";
positive(CALL, "period", options.period)?;
let names = suffixed(options.name(), ["Up", "Down", "Osc"]);
self.free_names(CALL, &names)?;
let [high, low] =
self.price_columns(CALL, [&options.high, &options.low], ["high", "low"])?;
let period = options.period;
let (up, down) = par::join(
|| aroon_values::<true>(&high, period),
|| aroon_values::<false>(&low, period),
);
let osc = up.iter().zip(&down).map(|(u, d)| u - d).collect();
self.study_outputs(CALL, names.into_iter().zip([up, down, osc]))
}
pub fn vortex(&self, options: Vortex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::vortex";
positive(CALL, "period", options.period)?;
let names = suffixed(options.name(), ["Plus", "Minus"]);
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let n = high.len();
let period = options.period;
let moved = |a: &[f64], b: &[f64]| -> Vec<f64> {
(0..n)
.map(|i| {
if i == 0 {
f64::NAN
} else {
(a[i] - b[i - 1]).abs()
}
})
.collect()
};
let ((mean_plus, mean_minus), mean_range) = par::join(
|| {
par::join(
|| rolling_mean_values(&moved(&high, &low), period),
|| rolling_mean_values(&moved(&low, &high), period),
)
},
|| rolling_mean_values(&kernels::true_range_values(&high, &low, &close), period),
);
let (mut plus, mut minus) = (vec![f64::NAN; n], vec![f64::NAN; n]);
for i in 0..n {
let range = mean_range[i];
if range != 0.0 {
plus[i] = mean_plus[i] / range;
minus[i] = mean_minus[i] / range;
}
}
self.study_outputs(CALL, names.into_iter().zip([plus, minus]))
}
pub fn vertical_horizontal_filter(&self, study: Study) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::vertical_horizontal_filter";
let (values, output) = self.study_input(CALL, &study, "vhf")?;
let period = study.period;
let (highest, (lowest, mean_change)) = par::join(
|| rolling_extreme::<true>(&values, period),
|| {
par::join(
|| rolling_extreme::<false>(&values, period),
|| {
let change: Vec<f64> = kernels::deltas(&values).map(f64::abs).collect();
rolling_mean_values(&change, period)
},
)
},
);
let p = period as f64;
let out = (0..values.len())
.map(|i| (highest[i] - lowest[i]) / (mean_change[i] * p))
.collect();
self.study_output(CALL, output, out)
}
pub fn random_walk_index(&self, options: RandomWalkIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::random_walk_index";
positive(CALL, "period", options.period)?;
if options.period < 2 {
return Err(PondError::InvalidData {
call: CALL,
message: format!(
"randomWalkIndex period must be at least 2 (the horizons are 2 … period); got {}",
options.period
),
});
}
let names = suffixed(options.name(), ["High", "Low"]);
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let true_range = kernels::true_range_values(&high, &low, &close);
let n = high.len();
let (mut rwi_high, mut rwi_low) = (vec![0.0; n], vec![0.0; n]);
par::fill_chunks(&mut rwi_high, &mut rwi_low, 1 << 14, |start, hi, lo| {
random_walk_into(&high, &low, &true_range, options.period, start, hi, lo)
});
self.study_outputs(CALL, names.into_iter().zip([rwi_high, rwi_low]))
}
pub fn super_trend(&self, options: SuperTrend) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::super_trend";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "superTrend multiplier", options.multiplier)?;
let names = [
options.name().to_string(),
format!("{}Trend", options.name()),
];
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let atr = kernels::atr_values(&high, &low, &close, options.period);
let k = options.multiplier;
let (mut upper, mut lower, mut up, mut prev_close) = (f64::NAN, f64::NAN, false, f64::NAN);
let (line, trend) = fold_rows([&high, &low, &close, &atr], |run, [h, l, c, a]| {
let mid = (h + l) / 2.0;
let half_width = k * a;
let basic_upper = mid + half_width;
let basic_lower = mid - half_width;
if run == 1 {
(upper, lower, up) = (basic_upper, basic_lower, false);
} else {
if basic_upper < upper || prev_close > upper {
upper = basic_upper;
}
if basic_lower > lower || prev_close < lower {
lower = basic_lower;
}
#[allow(clippy::neg_cmp_op_on_partial_ord)]
let stays_up = !(c < lower);
up = if up { stays_up } else { c > upper };
}
prev_close = c;
(if up { lower } else { upper }, if up { 1.0 } else { -1.0 })
});
self.study_outputs(CALL, names.into_iter().zip([line, trend]))
}
pub fn parabolic_sar(&self, options: ParabolicSar) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::parabolic_sar";
let (step, max_step) = (options.step, options.max_step);
positive_finite(CALL, "parabolicSar step", step)?;
positive_finite(CALL, "parabolicSar maxStep", max_step)?;
if max_step < step {
return Err(PondError::InvalidData {
call: CALL,
message: format!(
"parabolicSar maxStep ({}) must be at least step ({})",
crate::grid::js_number(max_step),
crate::grid::js_number(step)
),
});
}
let names = [
options.name().to_string(),
format!("{}Trend", options.name()),
];
self.free_names(CALL, &names)?;
let [high, low] =
self.price_columns(CALL, [&options.high, &options.low], ["high", "low"])?;
let mut state = Sar {
step,
max_step,
long: true,
sar: f64::NAN,
ep: f64::NAN,
af: step,
prev_high: f64::NAN,
prev_low: f64::NAN,
};
let (sar, trend) = fold_rows([&high, &low], |run, [h, l]| state.step(run, h, l));
self.study_outputs(CALL, names.into_iter().zip([sar, trend]))
}
pub fn atr_trailing_stop(&self, options: AtrTrailingStop) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::atr_trailing_stop";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "atrTrailingStop multiplier", options.multiplier)?;
let names = [
options.name().to_string(),
format!("{}Trend", options.name()),
];
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let atr = kernels::atr_values(&high, &low, &close, options.period);
let k = options.multiplier;
let (mut stop, mut long, mut prev_close) = (f64::NAN, true, f64::NAN);
let (stops, trend) = fold_rows([&close, &atr], |run, [c, a]| {
let distance = k * a;
if run == 1 {
(stop, long) = (c - distance, true);
} else {
let prev = stop;
if c > prev && prev_close > prev {
stop = js_max(prev, c - distance);
} else if c < prev && prev_close < prev {
stop = js_min(prev, c + distance);
} else if c > prev {
(stop, long) = (c - distance, true);
} else {
(stop, long) = (c + distance, false);
}
}
prev_close = c;
(stop, if long { 1.0 } else { -1.0 })
});
self.study_outputs(CALL, names.into_iter().zip([stops, trend]))
}
}
fn fold_rows<const N: usize>(
inputs: [&[f64]; N],
mut step: impl FnMut(usize, [f64; N]) -> (f64, f64),
) -> (Vec<f64>, Vec<f64>) {
let n = inputs.first().map_or(0, |c| c.len());
let (mut a, mut b) = (vec![f64::NAN; n], vec![f64::NAN; n]);
let mut run = 0;
for i in 0..n {
let row = inputs.map(|c| c[i]);
if row.iter().any(|v| v.is_nan()) {
run = 0;
continue;
}
run += 1;
(a[i], b[i]) = step(run, row);
}
(a, b)
}
fn smoothed_directional_movement(high: &[f64], low: &[f64], period: usize) -> (Vec<f64>, Vec<f64>) {
let n = high.len();
let (mut plus, mut minus) = (vec![f64::NAN; n], vec![f64::NAN; n]);
let moves = |i: usize| {
let up = high[i] - high[i - 1];
let dn = low[i - 1] - low[i];
if up.is_nan() || dn.is_nan() {
(f64::NAN, f64::NAN)
} else {
(
if up > dn && up > 0.0 { up } else { 0.0 },
if dn > up && dn > 0.0 { dn } else { 0.0 },
)
}
};
let first = (1..n).find(|&i| !moves(i).0.is_nan()).unwrap_or(n);
let seed_at = first.saturating_add(period - 1);
if seed_at >= n {
return (plus, minus);
}
let p = period as f64;
let (mut up_avg, mut down_avg) = (0.0, 0.0);
for i in first..=seed_at {
let (u, d) = moves(i);
up_avg += u;
down_avg += d;
}
up_avg /= p;
down_avg /= p;
(plus[seed_at], minus[seed_at]) = (up_avg, down_avg);
for i in seed_at + 1..n {
let (u, d) = moves(i);
up_avg = (up_avg * (p - 1.0) + u) / p;
down_avg = (down_avg * (p - 1.0) + d) / p;
(plus[i], minus[i]) = (up_avg, down_avg);
}
(plus, minus)
}
fn aroon_values<const MAX: bool>(values: &[f64], period: usize) -> Vec<f64> {
let p = period as f64;
let n = values.len();
let mut out = vec![f64::NAN; n];
let mut queue = std::collections::VecDeque::new();
let mut missing = 0usize;
for i in 0..n {
let value = values[i];
if i > period && !values[i - period - 1].is_finite() {
missing -= 1;
}
if !value.is_finite() {
missing += 1;
}
while queue.front().is_some_and(|&j| j + period < i) {
queue.pop_front();
}
if value.is_finite() {
while let Some(&j) = queue.back() {
let back: f64 = values[j];
if if MAX { back <= value } else { back >= value } {
queue.pop_back();
} else {
break;
}
}
queue.push_back(i);
}
if i >= period && missing == 0 {
let since = (i - queue[0]) as f64;
out[i] = (100.0 * (p - since)) / p;
}
}
out
}
fn random_walk_into(
high: &[f64],
low: &[f64],
true_range: &[f64],
period: usize,
start: usize,
high_out: &mut [f64],
low_out: &mut [f64],
) {
if period >= high.len() {
high_out.fill(f64::NAN);
low_out.fill(f64::NAN);
return;
}
high_out.fill(f64::NEG_INFINITY);
low_out.fill(f64::NEG_INFINITY);
let mut mean = vec![0.0; high_out.len()];
for horizon in 2..=period {
let from = mean_values_reads_from(start, horizon);
rolling_mean_values_into(&true_range[from..], horizon, start - from, &mut mean);
let scale = (horizon as f64).sqrt();
for k in 0..high_out.len() {
let i = start + k;
let denominator = mean[k] * scale;
let blank = i < horizon || denominator == 0.0;
let (term_high, term_low) = if blank {
(f64::NAN, f64::NAN)
} else {
(
(high[i] - low[i - horizon]) / denominator,
(high[i - horizon] - low[i]) / denominator,
)
};
high_out[k] = js_max(high_out[k], term_high);
low_out[k] = js_max(low_out[k], term_low);
}
}
}
struct Sar {
step: f64,
max_step: f64,
long: bool,
sar: f64,
ep: f64,
af: f64,
prev_high: f64,
prev_low: f64,
}
impl Sar {
fn step(&mut self, run: usize, new_high: f64, new_low: f64) -> (f64, f64) {
if run == 1 {
(self.prev_high, self.prev_low) = (new_high, new_low);
return (f64::NAN, f64::NAN);
}
if run == 2 {
let diff_plus = new_high - self.prev_high;
let diff_minus = self.prev_low - new_low;
self.long = !(diff_minus > 0.0 && diff_minus > diff_plus);
self.af = self.step;
if self.long {
(self.ep, self.sar) = (new_high, self.prev_low);
} else {
(self.ep, self.sar) = (new_low, self.prev_high);
}
(self.prev_high, self.prev_low) = (new_high, new_low);
}
let (prev_high, prev_low) = (self.prev_high, self.prev_low);
let out;
if self.long {
if new_low <= self.sar {
self.long = false;
let mut sar = self.ep;
if sar < prev_high {
sar = prev_high;
}
if sar < new_high {
sar = new_high;
}
out = sar;
self.af = self.step;
self.ep = new_low;
sar += self.af * (self.ep - sar);
if sar < prev_high {
sar = prev_high;
}
if sar < new_high {
sar = new_high;
}
self.sar = sar;
} else {
out = self.sar;
if new_high > self.ep {
self.ep = new_high;
self.af = js_min(self.af + self.step, self.max_step);
}
let mut sar = self.sar + self.af * (self.ep - self.sar);
if sar > prev_low {
sar = prev_low;
}
if sar > new_low {
sar = new_low;
}
self.sar = sar;
}
} else if new_high >= self.sar {
self.long = true;
let mut sar = self.ep;
if sar > prev_low {
sar = prev_low;
}
if sar > new_low {
sar = new_low;
}
out = sar;
self.af = self.step;
self.ep = new_high;
sar += self.af * (self.ep - sar);
if sar > prev_low {
sar = prev_low;
}
if sar > new_low {
sar = new_low;
}
self.sar = sar;
} else {
out = self.sar;
if new_low < self.ep {
self.ep = new_low;
self.af = js_min(self.af + self.step, self.max_step);
}
let mut sar = self.sar + self.af * (self.ep - self.sar);
if sar < prev_high {
sar = prev_high;
}
if sar < new_high {
sar = new_high;
}
self.sar = sar;
}
(self.prev_high, self.prev_low) = (new_high, new_low);
(out, if self.long { 1.0 } else { -1.0 })
}
}
study_options! {
ZigZag, prefix = "zz", reads [high, low] {
deviation: f64 = 5.0,
}
}
study_options! {
Ichimoku, prefix = "ichi", reads [high, low, close] {
conversion: usize = 9,
base: usize = 26,
span_b: usize = 52,
displacement: usize = 26,
}
}
impl Ichimoku {
pub fn offsets(&self) -> Result<[(String, i64); 5]> {
positive(
"Ichimoku::offsets",
"ichimokuOffsets displacement",
self.displacement,
)?;
let d = self.displacement as i64;
let [tenkan, kijun, a, b, chikou] = suffixed(self.name(), ICHIMOKU_SUFFIXES);
Ok([(tenkan, 0), (kijun, 0), (a, d), (b, d), (chikou, -d)])
}
}
const ICHIMOKU_SUFFIXES: [&str; 5] = ["Tenkan", "Kijun", "SenkouA", "SenkouB", "Chikou"];
study_options! {
ElderImpulse, output = "impulse" {
ema: usize = 13,
fast: usize = 12,
slow: usize = 26,
signal: usize = 9,
}
}
study_options! {
ElderRay, prefix = "elder", reads [high, low, close] {
period: usize = 13,
}
}
study_options! {
TrendIntensityIndex, output = "tii" {
period: usize = 30,
ma_period: usize = 60,
}
}
study_options! {
Ravi, output = "ravi" {
short: usize = 7,
long: usize = 65,
}
}
study_options! {
SwingIndex(limit: f64), output = "si", reads [open, high, low, close] {}
}
study_options! {
AccumulativeSwingIndex(limit: f64), output = "asi", reads [open, high, low, close] {}
}
const ELDER_SCRATCH: &str = "__elderMacd__";
impl TimeSeries {
pub fn zig_zag(&self, options: ZigZag) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::zig_zag";
let deviation = options.deviation;
if !deviation.is_finite() || deviation <= 0.0 {
return Err(PondError::InvalidData {
call: CALL,
message: "zigZag deviation must be a positive finite number (percent)".into(),
});
}
let names = suffixed(options.name(), ["Pivot", "Direction", "Line"]);
self.free_names(CALL, &names)?;
let [high, low] =
self.price_columns(CALL, [&options.high, &options.low], ["high", "low"])?;
let (pivot, direction, line) = zig_zag_values(&high, &low, deviation / 100.0);
self.study_outputs(CALL, names.into_iter().zip([pivot, direction, line]))
}
pub fn ichimoku(&self, options: Ichimoku) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::ichimoku";
positive(CALL, "ichimoku conversionPeriod", options.conversion)?;
positive(CALL, "ichimoku basePeriod", options.base)?;
positive(CALL, "ichimoku spanBPeriod", options.span_b)?;
positive(CALL, "ichimoku displacement", options.displacement)?;
let names = suffixed(options.name(), ICHIMOKU_SUFFIXES);
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let (tenkan, (kijun, senkou_b)) = par::join(
|| midpoint_values(&high, &low, options.conversion),
|| {
par::join(
|| midpoint_values(&high, &low, options.base),
|| midpoint_values(&high, &low, options.span_b),
)
},
);
let senkou_a = tenkan
.iter()
.zip(&kijun)
.map(|(t, k)| (t + k) / 2.0)
.collect();
let chikou = close.into_owned();
self.study_outputs(
CALL,
names
.into_iter()
.zip([tenkan, kijun, senkou_a, senkou_b, chikou]),
)
}
pub fn elder_impulse(&self, options: ElderImpulse) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::elder_impulse";
positive(CALL, "emaPeriod", options.ema)?;
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
positive(CALL, "signalPeriod", options.signal)?;
shorter(CALL, "elderImpulse", options.fast, options.slow)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
self.free_names(
CALL,
&[
format!("{ELDER_SCRATCH}Line"),
format!("{ELDER_SCRATCH}Signal"),
format!("{ELDER_SCRATCH}Hist"),
MACD_SCRATCH.to_string(),
],
)?;
let (trend, hist) = par::join(
|| kernels::ema_values(&values, options.ema),
|| macd_hist(&values, options.fast, options.slow, options.signal),
);
let n = values.len();
let mut out = vec![f64::NAN; n];
for i in 1..n {
let (e, e_prev, h, h_prev) = (trend[i], trend[i - 1], hist[i], hist[i - 1]);
if e.is_nan() || e_prev.is_nan() || h.is_nan() || h_prev.is_nan() {
continue;
}
out[i] = if e > e_prev && h > h_prev {
1.0
} else if e < e_prev && h < h_prev {
-1.0
} else {
0.0
};
}
self.study_outputs(CALL, [(output, out)])
}
pub fn elder_ray(&self, options: ElderRay) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::elder_ray";
positive(CALL, "period", options.period)?;
let names = suffixed(options.name(), ["Bull", "Bear"]);
self.free_names(CALL, &names)?;
let [close] = self.price_columns(CALL, [&options.close], ["close"])?;
let [high, low] =
self.price_columns(CALL, [&options.high, &options.low], ["high", "low"])?;
let ema = kernels::ema_values(&close, options.period);
let bull = high.iter().zip(&ema).map(|(h, e)| h - e).collect();
let bear = low.iter().zip(&ema).map(|(l, e)| l - e).collect();
self.study_outputs(CALL, names.into_iter().zip([bull, bear]))
}
pub fn trend_intensity_index(&self, options: TrendIntensityIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::trend_intensity_index";
positive(CALL, "period", options.period)?;
positive(CALL, "maPeriod", options.ma_period)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let average = ma_column(&values, options.ma_period, MaType::Sma);
let (up, down): (Vec<f64>, Vec<f64>) = values
.iter()
.zip(&average)
.map(|(v, a)| {
let deviation = v - a;
(js_max(deviation, 0.0), js_max(-deviation, 0.0))
})
.unzip();
let (up_mean, down_mean) = par::join(
|| rolling_mean_values(&up, options.period),
|| rolling_mean_values(&down, options.period),
);
let out = up_mean
.iter()
.zip(&down_mean)
.map(|(u, d)| {
let total = u + d;
if total == 0.0 {
f64::NAN
} else {
(100.0 * u) / total
}
})
.collect();
self.study_output(CALL, output, out)
}
pub fn ravi(&self, options: Ravi) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::ravi";
positive(CALL, "shortPeriod", options.short)?;
positive(CALL, "longPeriod", options.long)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let (short, long) = par::join(
|| ma_column(&values, options.short, MaType::Sma),
|| ma_column(&values, options.long, MaType::Sma),
);
let out = short
.iter()
.zip(&long)
.map(|(&s, &slow)| {
if slow == 0.0 {
f64::NAN
} else {
(100.0 * (s - slow).abs()) / slow
}
})
.collect();
self.study_output(CALL, output, out)
}
pub fn swing_index(&self, options: SwingIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::swing_index";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let out = self.swing_values(
CALL,
"swingIndex",
options.limit,
[&options.open, &options.high, &options.low, &options.close],
)?;
self.study_output(CALL, output, out)
}
pub fn accumulative_swing_index(&self, options: AccumulativeSwingIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::accumulative_swing_index";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let swings = self.swing_values(
CALL,
"accumulativeSwingIndex",
options.limit,
[&options.open, &options.high, &options.low, &options.close],
)?;
self.study_output(CALL, output, cumulative_values(&swings))
}
fn swing_values(
&self,
call: &'static str,
study: &str,
limit: f64,
names: [&Option<String>; 4],
) -> Result<Vec<f64>> {
if !limit.is_finite() || limit <= 0.0 {
return Err(PondError::InvalidData {
call,
message: format!(
"{study} limit must be a positive finite number (the instrument's limit move); got {}",
crate::grid::js_number(limit)
),
});
}
let [open, high, low, close] =
self.price_columns(call, names, ["open", "high", "low", "close"])?;
Ok(swing_index_values(&open, &high, &low, &close, limit))
}
}
fn zig_zag_values(high: &[f64], low: &[f64], threshold: f64) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let n = high.len();
let mut pivot = vec![f64::NAN; n];
let mut dir = 0i8;
let (mut ext_val, mut ext_idx) = (f64::NAN, 0usize);
let (mut ctr_val, mut ctr_idx): (f64, Option<usize>) = (f64::NAN, None);
let mut segment_end: Vec<usize> = Vec::new();
let mut pivots: Vec<(usize, f64, i8, usize)> = Vec::new();
let mut run = 0usize;
for i in 0..n {
let (h, l) = (high[i], low[i]);
if h.is_nan() || l.is_nan() {
run = 0;
continue;
}
run += 1;
if run == 1 {
segment_end.push(i);
dir = 0;
(ext_val, ext_idx) = (h, i);
(ctr_val, ctr_idx) = (l, Some(i));
continue;
}
let segment = segment_end.len() - 1;
segment_end[segment] = i;
let mut confirm = |row: usize, value: f64, dir_after: i8| {
pivots.push((row, value, dir_after, segment));
pivot[row] = value;
};
if dir == 0 {
if h > ext_val {
(ext_val, ext_idx) = (h, i);
}
let ctr = ctr_idx.expect("set while seeding");
let ctr = if l < ctr_val {
ctr_val = l;
i
} else {
ctr
};
ctr_idx = Some(ctr);
let gap = ext_val - ctr_val;
if ext_idx > ctr {
if gap >= threshold * ctr_val {
confirm(ctr, ctr_val, 1);
dir = 1;
ctr_idx = None;
}
} else if ctr > ext_idx && gap >= threshold * ext_val {
confirm(ext_idx, ext_val, -1);
dir = -1;
(ext_val, ext_idx) = (ctr_val, ctr);
ctr_idx = None;
}
continue;
}
if dir == 1 {
if h > ext_val {
(ext_val, ext_idx) = (h, i);
ctr_idx = None;
continue;
}
if ctr_idx.is_none() || l < ctr_val {
(ctr_val, ctr_idx) = (l, Some(i));
}
if ext_val - ctr_val >= threshold * ext_val {
confirm(ext_idx, ext_val, -1);
dir = -1;
(ext_val, ext_idx) = (ctr_val, ctr_idx.expect("just set"));
ctr_idx = None;
}
continue;
}
if l < ext_val {
(ext_val, ext_idx) = (l, i);
ctr_idx = None;
continue;
}
if ctr_idx.is_none() || h > ctr_val {
(ctr_val, ctr_idx) = (h, Some(i));
}
if ctr_val - ext_val >= threshold * ext_val {
confirm(ext_idx, ext_val, 1);
dir = 1;
(ext_val, ext_idx) = (ctr_val, ctr_idx.expect("just set"));
ctr_idx = None;
}
}
let mut direction = vec![f64::NAN; n];
let mut line = vec![f64::NAN; n];
for (k, &(start, from, leg_dir, segment)) in pivots.iter().enumerate() {
let next = pivots.get(k + 1).filter(|p| p.3 == segment);
let stop = next.map_or(segment_end[segment], |p| p.0);
direction[start..stop].fill(f64::from(leg_dir));
match next {
Some(&(_, to, _, _)) => {
let span = (stop - start) as f64;
line[start] = from;
line[stop] = to;
for (j, slot) in line.iter_mut().enumerate().take(stop).skip(start + 1) {
*slot = from + ((to - from) * (j - start) as f64) / span;
}
}
None => direction[stop] = f64::from(leg_dir),
}
}
(pivot, direction, line)
}
fn midpoint_values(high: &[f64], low: &[f64], period: usize) -> Vec<f64> {
let ((highest, _), (_, lowest)) = par::join(
|| rolling_extremes_strict(high, period),
|| rolling_extremes_strict(low, period),
);
highest
.iter()
.zip(&lowest)
.map(|(h, l)| (h + l) / 2.0)
.collect()
}
fn macd_hist(values: &[f64], fast: usize, slow: usize, signal: usize) -> Vec<f64> {
let (mut fast, mut slow) = (Ema::period(fast), Ema::period(slow));
let mut signal_ema = Ema::period(signal);
values
.iter()
.map(|&v| {
let line = fast.push(v) - slow.push(v);
line - signal_ema.push(line)
})
.collect()
}
fn swing_index_values(
open: &[f64],
high: &[f64],
low: &[f64],
close: &[f64],
limit: f64,
) -> Vec<f64> {
let n = close.len();
let mut out = vec![f64::NAN; n];
for i in 1..n {
let (prev_close, prev_open) = (close[i - 1], open[i - 1]);
let (c, o, h, l) = (close[i], open[i], high[i], low[i]);
let a = (h - prev_close).abs();
let b = (l - prev_close).abs();
let d = (h - l).abs();
let yesterday_body = 0.25 * (prev_close - prev_open).abs();
let r = if a >= b && a >= d {
a - 0.5 * b + yesterday_body
} else if b >= a && b >= d {
b - 0.5 * a + yesterday_body
} else {
d + yesterday_body
};
let k = js_max(a, b);
let numerator = c - prev_close + 0.5 * (c - o) + 0.25 * (prev_close - prev_open);
out[i] = if r == 0.0 {
f64::NAN
} else {
((50.0 * numerator) / r) * (k / limit)
};
}
out
}
fn cumulative_values(values: &[f64]) -> Vec<f64> {
let first = values
.iter()
.position(|v| !v.is_nan())
.unwrap_or(values.len());
let mut out = vec![f64::NAN; values.len()];
let mut sum = 0.0;
for (slot, &v) in out.iter_mut().zip(values).skip(first) {
sum += v;
*slot = sum;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_finite_limits_and_deviations_are_refused() {
let price = |k: f64| (0..4).map(|i| 10.0 + k * f64::from(i)).collect::<Vec<_>>();
let series = TimeSeries::new(
"s",
(0..4).collect(),
vec![
crate::Column::required("open", price(0.5)),
crate::Column::required("high", price(1.5)),
crate::Column::required("low", price(0.2)),
crate::Column::required("close", price(1.0)),
],
)
.unwrap();
for bad in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, 0.0, -1.0] {
let si = series.swing_index(SwingIndex::new(bad)).unwrap_err();
assert!(si.to_string().contains("swingIndex limit"), "{si}");
let asi = series
.accumulative_swing_index(AccumulativeSwingIndex::new(bad))
.unwrap_err();
assert!(
asi.to_string().contains("accumulativeSwingIndex limit"),
"{asi}"
);
let zz = series
.zig_zag(ZigZag::default().deviation(bad))
.unwrap_err();
assert!(zz.to_string().contains("zigZag deviation"), "{zz}");
}
assert!(series.swing_index(SwingIndex::new(1.0)).is_ok());
}
#[test]
fn elder_ray_reads_close_first() {
let series = TimeSeries::new(
"s",
(0..3).collect(),
vec![crate::Column::required("low", vec![1.0, 2.0, 3.0])],
)
.unwrap();
let err = series
.elder_ray(ElderRay::default())
.unwrap_err()
.to_string();
assert!(err.contains("close") && !err.contains("high"), "{err}");
}
#[test]
fn random_walk_period_past_the_series_is_all_gaps() {
let n = 40;
let col = |name: &str, f: fn(usize) -> f64| {
crate::series::Column::required(name, (0..n).map(f).collect::<Vec<f64>>())
};
let series = TimeSeries::new(
"bars",
(0..n as i64).collect(),
vec![
col("high", |i| 10.0 + (i % 7) as f64),
col("low", |i| 8.0 - (i % 3) as f64),
col("close", |i| 9.0 + (i % 5) as f64 / 2.0),
],
)
.unwrap();
let last_two = |period: usize| {
let out = series
.random_walk_index(RandomWalkIndex::default().period(period))
.unwrap();
let cols = out.columns();
cols[cols.len() - 2..]
.iter()
.map(|c| c.numbers().unwrap().iter().collect::<Vec<_>>())
.collect::<Vec<_>>()
};
let shorter = last_two(n - 1);
assert!(
shorter
.iter()
.all(|c| c[n - 1].is_some() && c[n - 2].is_none())
);
for period in [n, n + 1, 1_000_000_000_000] {
assert!(
last_two(period).iter().flatten().all(Option::is_none),
"{period}"
);
}
}
#[test]
fn random_walk_stretches_give_the_same_bits_as_one_sweep() {
let n = 3_000;
let wave = |i: usize, k: usize| match i % 97 {
5 => f64::NAN,
20..=40 => 1e9,
_ => 1e9 + ((i * k) % 101) as f64 / 7.0,
};
let high: Vec<f64> = (0..n).map(|i| wave(i, 7919) + 3.0).collect();
let low: Vec<f64> = (0..n).map(|i| wave(i, 6007) - 3.0).collect();
let close: Vec<f64> = (0..n).map(|i| wave(i, 3001)).collect();
let true_range = kernels::true_range_values(&high, &low, &close);
let bits = |v: &[f64]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
for period in [2, 3, 14, 50] {
let (mut hi, mut lo) = (vec![0.0; n], vec![0.0; n]);
random_walk_into(&high, &low, &true_range, period, 0, &mut hi, &mut lo);
for chunk in [1, 7, 64, 1000] {
let (mut hi2, mut lo2) = (vec![0.0; n], vec![0.0; n]);
for start in (0..n).step_by(chunk) {
let e = (start + chunk).min(n);
random_walk_into(
&high,
&low,
&true_range,
period,
start,
&mut hi2[start..e],
&mut lo2[start..e],
);
}
assert_eq!(bits(&hi), bits(&hi2), "high {period} {chunk}");
assert_eq!(bits(&lo), bits(&lo2), "low {period} {chunk}");
}
}
}
}