use super::kernels::{self, Ema, ma_column, moving_average_values};
use super::{MaType, Study};
use crate::error::{PondError, Result};
use crate::par;
use crate::series::TimeSeries;
study_options! {
Macd, prefix = "macd" {
fast: usize = 12,
slow: usize = 26,
signal: usize = 9,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OscillatorMode {
Percent,
Absolute,
}
study_options! {
PriceOscillator, output = "priceOsc" {
fast: usize = 12,
slow: usize = 26,
ma: MaType = MaType::Ema,
mode: OscillatorMode = OscillatorMode::Percent,
}
}
study_options! {
MovingAverageCross, output = "maCross" {
fast: usize = 10,
slow: usize = 30,
ma: MaType = MaType::Sma,
}
}
study_options! {
Guppy, prefix = "gmma" {
ma: MaType = MaType::Ema,
}
}
study_options! {
Rainbow, prefix = "rainbow" {
period: usize = 2,
ma: MaType = MaType::Sma,
}
}
study_options! {
RainbowOscillator, prefix = "rbo" {
period: usize = 2,
lookback: usize = 10,
ma: MaType = MaType::Sma,
}
}
study_options! {
Trix, prefix = "trix" {
period: usize = 15,
signal: usize = 9,
}
}
study_options! {
SchaffTrendCycle, output = "stc" {
fast: usize = 23,
slow: usize = 50,
cycle: usize = 10,
}
}
study_options! {
Coppock, output = "coppock" {
long: usize = 14,
short: usize = 11,
wma_period: usize = 10,
}
}
study_options! {
Kst, prefix = "kst" {
signal: usize = 9,
}
}
study_options! {
SpecialK, output = "specialK" {}
}
study_options! {
PriceMomentumOscillator, prefix = "pmo" {}
}
const GUPPY: [(char, usize); 12] = [
('S', 3),
('S', 5),
('S', 8),
('S', 10),
('S', 12),
('S', 15),
('L', 30),
('L', 35),
('L', 40),
('L', 45),
('L', 50),
('L', 60),
];
const RAINBOW_DEPTH: usize = 10;
const ROWS: usize = 1 << 16;
const KST: [(usize, usize, f64); 4] = [(10, 10, 1.0), (15, 10, 2.0), (20, 10, 3.0), (30, 15, 4.0)];
const SPECIAL_K: [(usize, usize, f64); 12] = [
(10, 10, 1.0),
(15, 10, 2.0),
(20, 10, 3.0),
(30, 15, 4.0),
(40, 50, 1.0),
(65, 65, 2.0),
(75, 75, 3.0),
(100, 100, 4.0),
(195, 130, 1.0),
(265, 130, 2.0),
(390, 130, 3.0),
(530, 195, 4.0),
];
pub(super) const MACD_SCRATCH: &str = "__macdLine__";
impl TimeSeries {
pub fn macd(&self, options: Macd) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::macd";
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
positive(CALL, "signalPeriod", options.signal)?;
shorter(CALL, "macd", options.fast, options.slow)?;
let prefix = options.name();
let names = [
format!("{prefix}Line"),
format!("{prefix}Signal"),
format!("{prefix}Hist"),
];
self.free_names(CALL, &names)?;
self.free_names(CALL, &[MACD_SCRATCH])?;
let values = self.study_source(CALL, options.source())?;
let (mut fast, mut slow) = (Ema::period(options.fast), Ema::period(options.slow));
let mut signal_ema = Ema::period(options.signal);
let n = values.len();
let (mut line, mut signal, mut hist) = (vec![0.0; n], vec![0.0; n], vec![0.0; n]);
for (i, &v) in values.iter().enumerate() {
let l = fast.push(v) - slow.push(v);
let s = signal_ema.push(l);
(line[i], signal[i], hist[i]) = (l, s, l - s);
}
self.study_outputs(CALL, names.into_iter().zip([line, signal, hist]))
}
pub fn price_oscillator(&self, options: PriceOscillator) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::price_oscillator";
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
shorter(CALL, "priceOscillator", options.fast, options.slow)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let out = oscillator_values(
&values,
options.fast,
options.slow,
options.ma,
options.mode,
);
self.study_outputs(CALL, [(output, out)])
}
pub fn moving_average_cross(&self, options: MovingAverageCross) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::moving_average_cross";
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
shorter(CALL, "movingAverageCross", options.fast, options.slow)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let (fast, slow) = par::join(
|| ma_column(&values, options.fast, options.ma),
|| ma_column(&values, options.slow, options.ma),
);
let mut out = vec![f64::NAN; values.len()];
let (mut run, mut last_sign) = (0usize, 0.0);
for i in 0..out.len() {
if fast[i].is_nan() || slow[i].is_nan() {
run = 0;
continue;
}
run += 1;
let difference = fast[i] - slow[i];
let sign = if difference > 0.0 {
1.0
} else if difference < 0.0 {
-1.0
} else {
0.0
};
if run == 1 {
last_sign = sign;
} else if sign == 0.0 {
out[i] = 0.0;
} else if last_sign == 0.0 {
last_sign = sign;
out[i] = 0.0;
} else if sign != last_sign {
last_sign = sign;
out[i] = sign;
} else {
out[i] = 0.0;
}
}
self.study_outputs(CALL, [(output, out)])
}
pub fn moving_average_deviation(&self, kind: MaType, study: Study) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::moving_average_deviation";
let (values, output) = self.study_input(CALL, &study, "maDev")?;
let ma = ma_column(&values, study.period, kind);
let out = values.iter().zip(&ma).map(|(p, m)| p - m).collect();
self.study_output(CALL, output, out)
}
pub fn disparity_index(&self, kind: MaType, study: Study) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::disparity_index";
let (values, output) = self.study_input(CALL, &study, "disparity")?;
let ma = ma_column(&values, study.period, kind);
let out = values
.iter()
.zip(&ma)
.map(|(&p, &m)| {
if m == 0.0 {
f64::NAN
} else {
(100.0 * (p - m)) / m
}
})
.collect();
self.study_output(CALL, output, out)
}
pub fn detrended_price_oscillator(&self, kind: MaType, study: Study) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::detrended_price_oscillator";
let (values, output) = self.study_input(CALL, &study, "dpo")?;
let ma = ma_column(&values, study.period, kind);
let shift = study.period / 2 + 1;
let out = (0..values.len())
.map(|i| {
if i < shift {
f64::NAN
} else {
values[i] - ma[i - shift]
}
})
.collect();
self.study_output(CALL, output, out)
}
pub fn guppy(&self, options: Guppy) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::guppy";
let prefix = options.name();
let names: Vec<String> = GUPPY
.iter()
.map(|(group, period)| format!("{prefix}{group}{period}"))
.collect();
self.free_names(CALL, &names)?;
let values = self.study_source(CALL, options.source())?;
let columns = par::map(&GUPPY, |&(_, period)| {
ma_column(&values, period, options.ma)
});
self.study_outputs(CALL, names.into_iter().zip(columns))
}
pub fn rainbow(&self, options: Rainbow) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::rainbow";
positive(CALL, "period", options.period)?;
let prefix = options.name();
let names: Vec<String> = (1..=RAINBOW_DEPTH)
.map(|stage| format!("{prefix}{stage}"))
.collect();
self.free_names(CALL, &names)?;
let values = self.study_source(CALL, options.source())?;
let stack = rainbow_stack(&values, options.period, options.ma);
self.study_outputs(CALL, names.into_iter().zip(stack))
}
pub fn rainbow_oscillator(&self, options: RainbowOscillator) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::rainbow_oscillator";
positive(CALL, "period", options.period)?;
positive(CALL, "lookback", options.lookback)?;
let prefix = options.name();
let names = [
prefix.to_string(),
format!("{prefix}Upper"),
format!("{prefix}Lower"),
];
self.free_names(CALL, &names)?;
let price = self.study_source(CALL, options.source())?;
let (stack, (highest, lowest)) = par::join(
|| rainbow_stack(&price, options.period, options.ma),
|| {
par::join(
|| kernels::rolling_extreme::<true>(&price, options.lookback),
|| kernels::rolling_extreme::<false>(&price, options.lookback),
)
},
);
let n = price.len();
let (mut line, mut upper) = (vec![0.0; n], vec![0.0; n]);
par::fill_chunks(&mut line, &mut upper, ROWS, |start, line, upper| {
for (k, (line, upper)) in line.iter_mut().zip(upper.iter_mut()).enumerate() {
let i = start + k;
let (mut sum, mut high, mut low) = (0.0, f64::NEG_INFINITY, f64::INFINITY);
for stage in &stack {
let v = stage[i];
sum += v;
if v > high {
high = v;
}
if v < low {
low = v;
}
}
let range = highest[i] - lowest[i];
(*line, *upper) = if sum.is_nan() || range == 0.0 {
(f64::NAN, f64::NAN)
} else {
(
(100.0 * (price[i] - sum / RAINBOW_DEPTH as f64)) / range,
(100.0 * (high - low)) / range,
)
};
}
});
let mut lower = vec![0.0; n];
par::fill_chunks(&mut lower, &mut [], ROWS, |start, lower, _| {
for (l, u) in lower.iter_mut().zip(&upper[start..]) {
*l = -u;
}
});
self.study_outputs(CALL, names.into_iter().zip([line, upper, lower]))
}
pub fn trix(&self, options: Trix) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::trix";
positive(CALL, "period", options.period)?;
positive(CALL, "signalPeriod", options.signal)?;
let prefix = options.name();
let names = [prefix.to_string(), format!("{prefix}Signal")];
self.free_names(CALL, &names)?;
let values = self.study_source(CALL, options.source())?;
let mut e1 = Ema::period(options.period);
let (mut e2, mut e3) = (e1.clone(), e1.clone());
let mut signal_ema = Ema::period(options.signal);
let n = values.len();
let (mut line, mut signal) = (vec![0.0; n], vec![0.0; n]);
let mut previous = f64::NAN;
for (i, &v) in values.iter().enumerate() {
let third = e3.push(e2.push(e1.push(v)));
let l = if i < 1 {
f64::NAN
} else {
kernels::percent_change(previous, third)
};
previous = third;
(line[i], signal[i]) = (l, signal_ema.push(l));
}
self.study_outputs(CALL, names.into_iter().zip([line, signal]))
}
pub fn schaff_trend_cycle(&self, options: SchaffTrendCycle) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::schaff_trend_cycle";
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
positive(CALL, "cyclePeriod", options.cycle)?;
shorter(CALL, "schaffTrendCycle", options.fast, options.slow)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let (mut fast, mut slow) = (Ema::period(options.fast), Ema::period(options.slow));
let macd: Vec<f64> = values
.iter()
.map(|&v| fast.push(v) - slow.push(v))
.collect();
let pass = |values: &[f64]| {
let (highest, lowest) = kernels::rolling_extremes_strict(values, options.cycle);
kernels::stochastic_half_smooth(&highest, &lowest, values)
};
let stc = pass(&pass(&macd));
self.study_outputs(CALL, [(output, stc)])
}
pub fn coppock(&self, options: Coppock) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::coppock";
positive(CALL, "longPeriod", options.long)?;
positive(CALL, "shortPeriod", options.short)?;
positive(CALL, "wmaPeriod", options.wma_period)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let (long, short) = par::join(
|| kernels::percent_change_values(&values, options.long),
|| kernels::percent_change_values(&values, options.short),
);
let sum: Vec<f64> = long.iter().zip(&short).map(|(l, s)| l + s).collect();
let curve = kernels::wma_values(&sum, options.wma_period);
self.study_outputs(CALL, [(output, curve)])
}
pub fn kst(&self, options: Kst) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::kst";
positive(CALL, "signalPeriod", options.signal)?;
let prefix = options.name();
let names = [prefix.to_string(), format!("{prefix}Signal")];
self.free_names(CALL, &names)?;
let values = self.study_source(CALL, options.source())?;
let line = weighted_rates(&values, &KST);
let signal = kernels::rolling_mean_values(&line, options.signal);
self.study_outputs(CALL, names.into_iter().zip([line, signal]))
}
pub fn special_k(&self, options: SpecialK) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::special_k";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let line = weighted_rates(&values, &SPECIAL_K);
self.study_outputs(CALL, [(output, line)])
}
pub fn price_momentum_oscillator(
&self,
options: PriceMomentumOscillator,
) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::price_momentum_oscillator";
let prefix = options.name();
let names = [prefix.to_string(), format!("{prefix}Signal")];
self.free_names(CALL, &names)?;
let values = self.study_source(CALL, options.source())?;
let (mut first, mut second) = (Ema::new(2.0 / 35.0, 35), Ema::new(2.0 / 20.0, 20));
let mut signal_ema = Ema::period(10);
let n = values.len();
let (mut line, mut signal) = (vec![0.0; n], vec![0.0; n]);
for i in 0..n {
let roc = kernels::percent_change_at(&values, 1, i);
let l = second.push(10.0 * first.push(roc));
(line[i], signal[i]) = (l, signal_ema.push(l));
}
self.study_outputs(CALL, names.into_iter().zip([line, signal]))
}
}
fn rainbow_stack(values: &[f64], period: usize, kind: MaType) -> Vec<Vec<f64>> {
let mut stack: Vec<Vec<f64>> = Vec::with_capacity(RAINBOW_DEPTH);
for stage in 0..RAINBOW_DEPTH {
let input = if stage == 0 {
values
} else {
&stack[stage - 1]
};
let next = moving_average_values(input, period, kind);
stack.push(next);
}
stack
}
fn weighted_rates(values: &[f64], terms: &[(usize, usize, f64)]) -> Vec<f64> {
let longest = terms.iter().map(|t| t.1).max().unwrap_or(1);
weighted_rates_chunked(values, terms, ROWS.max(longest * 8))
}
fn weighted_rates_chunked(values: &[f64], terms: &[(usize, usize, f64)], chunk: usize) -> Vec<f64> {
let mut line = vec![0.0; values.len()];
par::fill_chunks(&mut line, &mut [], chunk, |start, line, _| {
let end = start + line.len();
let mut smoothed = vec![0.0; line.len()];
let mut changes = Vec::new();
for &(change, smooth, weight) in terms {
let from = kernels::mean_values_reads_from(start, smooth);
changes.clear();
changes.extend((from..end).map(|i| kernels::percent_change_at(values, change, i)));
kernels::rolling_mean_values_into(&changes, smooth, start - from, &mut smoothed);
for (l, s) in line.iter_mut().zip(&smoothed) {
*l += weight * s;
}
}
});
line
}
pub(super) fn oscillator_values(
values: &[f64],
fast: usize,
slow: usize,
ma: MaType,
mode: OscillatorMode,
) -> Vec<f64> {
let (fast, slow) = par::join(
|| ma_column(values, fast, ma),
|| ma_column(values, slow, ma),
);
fast.iter()
.zip(&slow)
.map(|(&f, &s)| match mode {
OscillatorMode::Absolute => f - s,
OscillatorMode::Percent if s == 0.0 => f64::NAN,
OscillatorMode::Percent => (100.0 * (f - s)) / s,
})
.collect()
}
pub(super) fn positive(call: &'static str, name: &str, period: usize) -> Result<()> {
if period == 0 {
return Err(PondError::InvalidData {
call,
message: format!("{name} must be a positive integer (bar count)"),
});
}
Ok(())
}
pub(super) fn shorter(call: &'static str, study: &str, fast: usize, slow: usize) -> Result<()> {
if fast >= slow {
return Err(PondError::InvalidData {
call,
message: format!(
"{study} fastPeriod ({fast}) must be shorter than slowPeriod ({slow})"
),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weighted_rates_in_stretches_match_whole_columns() {
let mut x = 0x9E37_79B9_7F4A_7C15u64;
let values: Vec<f64> = (0..6_000)
.map(|i| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
match i {
_ if (600..640).contains(&i) => f64::NAN,
_ if i % 2_503 == 17 => f64::NAN,
350 => 0.0,
_ => 50.0 + (x % 10_007) as f64 / 97.0,
}
})
.collect();
for terms in [&KST[..], &SPECIAL_K[..]] {
let mut want = vec![0.0; values.len()];
for &(change, smooth, weight) in terms {
let smoothed = kernels::rolling_mean_values(
&kernels::percent_change_values(&values, change),
smooth,
);
for (w, s) in want.iter_mut().zip(&smoothed) {
*w += weight * s;
}
}
let finite = want.iter().filter(|v| v.is_finite()).count();
assert!(finite > 1_000, "{} terms: {finite} values", terms.len());
for chunk in [1, 2, 7, 64, 195, 999, 6_000] {
let got = weighted_rates_chunked(&values, terms, chunk);
let same = want
.iter()
.zip(&got)
.all(|(a, b)| a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan()));
assert!(same, "{} terms, stretch {chunk}", terms.len());
}
}
}
}