use super::MaType;
use super::averages::positive;
use super::kernels::{median_price_values, moving_average_values, typical_price_values};
use crate::error::{PondError, Result};
use crate::series::TimeSeries;
study_options! {
TypicalPrice, output = "typicalPrice", reads [high, low, close] {}
}
study_options! {
MedianPrice, output = "medianPrice", reads [high, low] {}
}
study_options! {
WeightedClose, output = "weightedClose", reads [high, low, close] {}
}
study_options! {
AveragePrice, output = "averagePrice", reads [open, high, low, close] {}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct BalanceOfPower {
period: Option<usize>,
ma: Option<MaType>,
open: Option<String>,
high: Option<String>,
low: Option<String>,
close: Option<String>,
output: Option<String>,
}
impl BalanceOfPower {
pub fn period(mut self, period: usize) -> Self {
self.period = Some(period);
self
}
pub fn ma(mut self, ma: MaType) -> Self {
self.ma = Some(ma);
self
}
pub fn open(mut self, name: &str) -> Self {
self.open = Some(name.to_string());
self
}
pub fn high(mut self, name: &str) -> Self {
self.high = Some(name.to_string());
self
}
pub fn low(mut self, name: &str) -> Self {
self.low = Some(name.to_string());
self
}
pub fn close(mut self, name: &str) -> Self {
self.close = Some(name.to_string());
self
}
pub fn output(mut self, name: &str) -> Self {
self.output = Some(name.to_string());
self
}
}
impl TimeSeries {
pub fn typical_price(&self, options: TypicalPrice) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::typical_price";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
self.study_output(CALL, output, typical_price_values(&high, &low, &close))
}
pub fn median_price(&self, options: MedianPrice) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::median_price";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low] =
self.price_columns(CALL, [&options.high, &options.low], ["high", "low"])?;
self.study_output(CALL, output, median_price_values(&high, &low))
}
pub fn weighted_close(&self, options: WeightedClose) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::weighted_close";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, close] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close],
["high", "low", "close"],
)?;
let out = (0..high.len())
.map(|i| (high[i] + low[i] + close[i] * 2.0) / 4.0)
.collect();
self.study_output(CALL, output, out)
}
pub fn average_price(&self, options: AveragePrice) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::average_price";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [open, high, low, close] = self.price_columns(
CALL,
[&options.open, &options.high, &options.low, &options.close],
["open", "high", "low", "close"],
)?;
let out = (0..high.len())
.map(|i| (high[i] + low[i] + close[i] + open[i]) / 4.0)
.collect();
self.study_output(CALL, output, out)
}
pub fn balance_of_power(&self, options: BalanceOfPower) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::balance_of_power";
if let Some(period) = options.period {
positive(CALL, "period", period)?;
}
if options.ma.is_some() && options.period.is_none() {
return Err(PondError::InvalidData {
call: CALL,
message: "balanceOfPower maType needs a period — without one the study is the raw per-bar ratio and there is nothing to smooth".into(),
});
}
let output = options.output.as_deref().unwrap_or("bop").to_string();
self.free_names(CALL, &[&output])?;
let [open, high, low, close] = self.price_columns(
CALL,
[&options.open, &options.high, &options.low, &options.close],
["open", "high", "low", "close"],
)?;
let raw: Vec<f64> = (0..high.len())
.map(|i| {
let range = high[i] - low[i];
let body = close[i] - open[i];
if range == 0.0 {
if body.is_nan() { f64::NAN } else { 0.0 }
} else {
body / range
}
})
.collect();
let out = match options.period {
None => raw,
Some(period) => moving_average_values(&raw, period, options.ma.unwrap_or(MaType::Sma)),
};
self.study_output(CALL, output, out)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bars() -> TimeSeries {
let price = |k: f64| (0..4).map(|i| 10.0 + k * f64::from(i)).collect::<Vec<_>>();
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()
}
#[test]
fn errors_come_in_pond_ts_order() {
let s = bars();
let message = |r: Result<TimeSeries>| r.unwrap_err().to_string();
let mp = message(s.median_price(MedianPrice::default().high("h?").low("l?")));
assert!(mp.contains("h?"), "{mp}");
let tp = message(s.typical_price(TypicalPrice::default().high("h?").low("l?")));
assert!(tp.contains("h?"), "{tp}");
let tp = message(s.typical_price(TypicalPrice::default().low("l?").close("c?")));
assert!(tp.contains("l?"), "{tp}");
let wc = message(s.weighted_close(WeightedClose::default().high("h?").low("l?")));
assert!(wc.contains("h?"), "{wc}");
let wc = message(s.weighted_close(WeightedClose::default().low("l?").close("c?")));
assert!(wc.contains("l?"), "{wc}");
let ap = message(s.average_price(AveragePrice::default().high("h?").low("l?")));
assert!(ap.contains("h?"), "{ap}");
let ap = message(s.average_price(AveragePrice::default().low("l?").close("c?")));
assert!(ap.contains("l?"), "{ap}");
let bop = message(s.balance_of_power(BalanceOfPower::default().low("l?").close("c?")));
assert!(bop.contains("l?"), "{bop}");
let ap = message(s.average_price(AveragePrice::default().open("o?").high("h?")));
assert!(ap.contains("o?"), "{ap}");
let bop = message(s.balance_of_power(BalanceOfPower::default().open("o?").high("h?")));
assert!(bop.contains("o?"), "{bop}");
let bop = message(
s.balance_of_power(
BalanceOfPower::default()
.period(0)
.ma(MaType::Ema)
.output("close"),
),
);
assert!(bop.contains("period must be"), "{bop}");
let bop =
message(s.balance_of_power(BalanceOfPower::default().ma(MaType::Sma).output("close")));
assert!(bop.contains("maType needs a period"), "{bop}");
let tp = message(s.typical_price(TypicalPrice::default().output("close").high("h?")));
assert!(tp.contains("collides"), "{tp}");
}
}