use super::MaType;
use super::averages::positive;
use super::kernels::{self, ma_column, moving_average_values, rolling_extreme, rolling_mean_sd};
use crate::error::{PondError, Result};
use crate::par;
use crate::series::TimeSeries;
study_options! {
Bollinger(period: usize), prefix = "bb" {
std_dev: f64 = 2.0,
}
}
study_options! {
BollingerBandwidth, output = "bbWidth" {
period: usize = 20,
std_dev: f64 = 2.0,
}
}
study_options! {
BollingerPercentB, output = "percentB" {
period: usize = 20,
std_dev: f64 = 2.0,
}
}
study_options! {
Keltner, prefix = "kc", reads [high, low, close] {
period: usize = 20,
atr_period: usize = 10,
multiplier: f64 = 2.0,
ma: MaType = MaType::Ema,
}
}
study_options! {
StarcBands, prefix = "starc", reads [high, low, close] {
period: usize = 20,
atr_period: usize = 15,
multiplier: f64 = 2.0,
ma: MaType = MaType::Sma,
}
}
study_options! {
AtrBands, prefix = "atrb", reads [column, high, low, close] {
period: usize = 14,
multiplier: f64 = 2.0,
}
}
study_options! {
Donchian, prefix = "dc", reads [high, low] {
period: usize = 20,
}
}
study_options! {
HighLowBands, prefix = "hlb", reads [high, low] {
period: usize = 10,
percent: f64 = 1.0,
ma: MaType = MaType::Trima,
}
}
study_options! {
PrimeNumberBands, prefix = "pnb", reads [high, low] {}
}
study_options! {
Envelope(period: usize), prefix = "env" {
percent: f64 = 2.5,
ma: MaType = MaType::Sma,
}
}
impl TimeSeries {
pub fn bollinger(&self, options: Bollinger) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::bollinger";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "bollinger stdDev", options.std_dev)?;
let names = suffixed(options.name(), ["Middle", "Upper", "Lower"]);
self.free_names(CALL, &names)?;
let values = self.study_source(CALL, options.source())?;
let (middle, sd) = mean_and_sd(&values, options.period);
let (upper, lower) = bands(&middle, &sd, options.std_dev);
self.study_outputs(CALL, names.into_iter().zip([middle, upper, lower]))
}
pub fn bollinger_bandwidth(&self, options: BollingerBandwidth) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::bollinger_bandwidth";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "bollingerBandwidth stdDev", options.std_dev)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let (middle, sd) = mean_and_sd(&values, options.period);
let k = options.std_dev;
let out = middle
.iter()
.zip(&sd)
.map(|(&m, &d)| {
let upper = m + k * d;
let lower = m + -k * d;
if m == 0.0 {
f64::NAN
} else {
(100.0 * (upper - lower)) / m
}
})
.collect();
self.study_output(CALL, output, out)
}
pub fn bollinger_percent_b(&self, options: BollingerPercentB) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::bollinger_percent_b";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "bollingerPercentB stdDev", options.std_dev)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let values = self.study_source(CALL, options.source())?;
let (middle, sd) = mean_and_sd(&values, options.period);
let k = options.std_dev;
let out = (0..values.len())
.map(|i| {
let (m, d) = (middle[i], sd[i]);
let upper = m + k * d;
let lower = m + -k * d;
if d == 0.0 {
f64::NAN
} else {
(values[i] - lower) / (upper - lower)
}
})
.collect();
self.study_output(CALL, output, out)
}
pub fn keltner(&self, options: Keltner) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::keltner";
positive(CALL, "period", options.period)?;
positive(CALL, "atrPeriod", options.atr_period)?;
positive_finite(CALL, "keltner multiplier", options.multiplier)?;
let names = suffixed(options.name(), ["Middle", "Upper", "Lower"]);
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let (middle, atr) = par::join(
|| {
let typical = kernels::typical_price_values(&high, &low, &close);
moving_average_values(&typical, options.period, options.ma)
},
|| kernels::atr_values(&high, &low, &close, options.atr_period),
);
let (upper, lower) = bands(&middle, &atr, options.multiplier);
self.study_outputs(CALL, names.into_iter().zip([middle, upper, lower]))
}
pub fn starc_bands(&self, options: StarcBands) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::starc_bands";
positive(CALL, "period", options.period)?;
positive(CALL, "atrPeriod", options.atr_period)?;
positive_finite(CALL, "starcBands multiplier", options.multiplier)?;
let names = suffixed(options.name(), ["Middle", "Upper", "Lower"]);
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let (middle, atr) = par::join(
|| ma_column(&close, options.period, options.ma),
|| kernels::atr_values(&high, &low, &close, options.atr_period),
);
let (upper, lower) = bands(&middle, &atr, options.multiplier);
self.study_outputs(CALL, names.into_iter().zip([middle, upper, lower]))
}
pub fn atr_bands(&self, options: AtrBands) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::atr_bands";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "atrBands multiplier", options.multiplier)?;
let names = suffixed(options.name(), ["Upper", "Lower"]);
self.free_names(CALL, &names)?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let column = (options.column.as_deref())
.unwrap_or_else(|| options.close.as_deref().unwrap_or("close"));
let field = self.study_source(CALL, column)?;
let atr = kernels::atr_values(&high, &low, &close, options.period);
let (upper, lower) = bands(&field, &atr, options.multiplier);
self.study_outputs(CALL, names.into_iter().zip([upper, lower]))
}
pub fn donchian(&self, options: Donchian) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::donchian";
positive(CALL, "period", options.period)?;
let names = suffixed(options.name(), ["Upper", "Lower", "Middle"]);
self.free_names(CALL, &names)?;
let high = self.study_source(CALL, options.high.as_deref().unwrap_or("high"))?;
let low = self.study_source(CALL, options.low.as_deref().unwrap_or("low"))?;
let (highest, lowest) = par::join(
|| rolling_extreme::<true>(&high, options.period),
|| rolling_extreme::<false>(&low, options.period),
);
let middle = highest
.iter()
.zip(&lowest)
.map(|(h, l)| (h + l) / 2.0)
.collect();
self.study_outputs(CALL, names.into_iter().zip([highest, lowest, middle]))
}
pub fn high_low_bands(&self, options: HighLowBands) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::high_low_bands";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "highLowBands percent", options.percent)?;
let names = suffixed(options.name(), ["Middle", "Upper", "Lower"]);
self.free_names(CALL, &names)?;
let high = self.study_source(CALL, options.high.as_deref().unwrap_or("high"))?;
let low = self.study_source(CALL, options.low.as_deref().unwrap_or("low"))?;
let median = kernels::median_price_values(&high, &low);
let middle = moving_average_values(&median, options.period, options.ma);
let (upper, lower) = scaled(&middle, options.percent);
self.study_outputs(CALL, names.into_iter().zip([middle, upper, lower]))
}
pub fn prime_number_bands(&self, options: PrimeNumberBands) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::prime_number_bands";
let names = suffixed(options.name(), ["Upper", "Lower"]);
self.free_names(CALL, &names)?;
let high = self.study_source(CALL, options.high.as_deref().unwrap_or("high"))?;
let low = self.study_source(CALL, options.low.as_deref().unwrap_or("low"))?;
let mut upper = vec![0.0; high.len()];
let mut lower = vec![0.0; high.len()];
par::fill_chunks(&mut upper, &mut lower, 1 << 14, |start, upper, lower| {
let mut primes = kernels::PrimeMemo::new();
for (k, (u, l)) in upper.iter_mut().zip(lower.iter_mut()).enumerate() {
*u = primes.at_or_above(high[start + k]);
*l = primes.at_or_below(low[start + k]);
}
});
self.study_outputs(CALL, names.into_iter().zip([upper, lower]))
}
pub fn envelope(&self, options: Envelope) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::envelope";
positive(CALL, "period", options.period)?;
positive_finite(CALL, "envelope percent", options.percent)?;
let names = suffixed(options.name(), ["Middle", "Upper", "Lower"]);
self.free_names(CALL, &names)?;
let values = self.study_source(CALL, options.source())?;
let middle = ma_column(&values, options.period, options.ma);
let (upper, lower) = scaled(&middle, options.percent);
self.study_outputs(CALL, names.into_iter().zip([middle, upper, lower]))
}
}
fn mean_and_sd(values: &[f64], period: usize) -> (Vec<f64>, Vec<f64>) {
let (mean, sd) = rolling_mean_sd(values, period, true);
(mean, sd.expect("asked for the spread"))
}
fn bands(centre: &[f64], width: &[f64], k: f64) -> (Vec<f64>, Vec<f64>) {
let mut upper = vec![0.0; centre.len()];
let mut lower = vec![0.0; centre.len()];
par::fill_chunks(&mut upper, &mut lower, 1 << 16, |start, upper, lower| {
for (j, (u, l)) in upper.iter_mut().zip(lower.iter_mut()).enumerate() {
let (c, w) = (centre[start + j], width[start + j]);
*u = c + k * w;
*l = c + -k * w;
}
});
(upper, lower)
}
fn scaled(centre: &[f64], percent: f64) -> (Vec<f64>, Vec<f64>) {
let f = percent / 100.0;
let (up, down) = (1.0 + f, 1.0 - f);
let mut upper = vec![0.0; centre.len()];
let mut lower = vec![0.0; centre.len()];
par::fill_chunks(&mut upper, &mut lower, 1 << 16, |start, upper, lower| {
for (j, (u, l)) in upper.iter_mut().zip(lower.iter_mut()).enumerate() {
*u = centre[start + j] * up;
*l = centre[start + j] * down;
}
});
(upper, lower)
}
pub(super) fn suffixed<const N: usize>(prefix: &str, suffixes: [&str; N]) -> [String; N] {
suffixes.map(|s| format!("{prefix}{s}"))
}
pub(super) fn positive_finite(call: &'static str, what: &str, value: f64) -> Result<()> {
if !value.is_finite() || value <= 0.0 {
return Err(PondError::InvalidData {
call,
message: format!("{what} must be a positive finite number"),
});
}
Ok(())
}