use super::MaType;
use super::averages::{OscillatorMode, oscillator_values, positive, shorter};
use super::kernels::{
Ema, bar_range_values, clv, clv_values, cumulative_values, ema_values, js_max, js_min, js_sign,
median_price_values, money_flow_volume_values, moving_average_values, percent_change_values,
rolling_mean_sd, rolling_mean_values, typical_price_values, wilder_values_from,
};
use crate::error::{PondError, Result};
use crate::par;
use crate::series::TimeSeries;
study_options! {
Obv, output = "obv", reads [close, volume] {}
}
study_options! {
AccumulationDistribution, output = "ad", reads [high, low, close, volume] {}
}
study_options! {
PriceVolumeTrend, output = "pvt", reads [close, volume] {}
}
study_options! {
ChaikinMoneyFlow, output = "cmf", reads [high, low, close, volume] {
period: usize = 20,
}
}
study_options! {
ChaikinOscillator, output = "chaikinOsc", reads [high, low, close, volume] {
fast: usize = 3,
slow: usize = 10,
}
}
study_options! {
MoneyFlowIndex, output = "mfi", reads [high, low, close, volume] {
period: usize = 14,
}
}
study_options! {
TwiggsMoneyFlow, output = "tmf", reads [high, low, close, volume] {
period: usize = 21,
}
}
study_options! {
ForceIndex, output = "force", reads [close, volume] {
period: usize = 13,
}
}
study_options! {
EaseOfMovement, output = "eom", reads [high, low, volume] {
period: usize = 14,
ma: MaType = MaType::Sma,
scale: f64 = 100_000_000.0,
}
}
study_options! {
Klinger, prefix = "kvo", reads [high, low, close, volume] {
fast: usize = 34,
slow: usize = 55,
signal: usize = 13,
}
}
study_options! {
VolumeOscillator, output = "volOsc", reads [volume] {
fast: usize = 5,
slow: usize = 10,
ma: MaType = MaType::Sma,
}
}
study_options! {
TradeVolumeIndex(min_tick: f64), output = "tvi", reads [column, volume] {}
}
study_options! {
NegativeVolumeIndex, output = "nvi", reads [column, volume] {
start: f64 = 1000.0,
}
}
study_options! {
PositiveVolumeIndex, output = "pvi", reads [column, volume] {
start: f64 = 1000.0,
}
}
study_options! {
MarketFacilitationIndex, output = "bwmfi", reads [high, low, volume] {}
}
study_options! {
ShinoharaIntensityRatio, prefix = "sir", reads [open, high, low, close] {
period: usize = 26,
}
}
study_options! {
Vwap(period: usize), output = "vwap", reads [high, low, close, volume] {}
}
impl TimeSeries {
pub fn obv(&self, options: Obv) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::obv";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [close, volume] =
self.price_columns(CALL, [&options.close, &options.volume], ["close", "volume"])?;
let n = close.len();
let first = (0..n)
.find(|&i| !close[i].is_nan() && !volume[i].is_nan())
.unwrap_or(n);
let mut out = vec![f64::NAN; n];
if first < n {
let mut sum = 0.0;
sum += volume[first];
out[first] = sum;
for i in first + 1..n {
sum += js_sign(close[i] - close[i - 1]) * volume[i];
out[i] = sum;
}
}
self.study_output(CALL, output, out)
}
pub fn accumulation_distribution(
&self,
options: AccumulationDistribution,
) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::accumulation_distribution";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, close, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close, &options.volume],
["high", "low", "close", "volume"],
)?;
let flow = money_flow_volume_values(&high, &low, &close, &volume);
self.study_output(CALL, output, cumulative_values(&flow))
}
pub fn price_volume_trend(&self, options: PriceVolumeTrend) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::price_volume_trend";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [close, volume] =
self.price_columns(CALL, [&options.close, &options.volume], ["close", "volume"])?;
let mut terms = percent_change_values(&close, 1);
for (t, v) in terms.iter_mut().zip(volume.iter()) {
*t = (*t / 100.0) * v;
}
self.study_output(CALL, output, cumulative_values(&terms))
}
pub fn chaikin_money_flow(&self, options: ChaikinMoneyFlow) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::chaikin_money_flow";
positive(CALL, "period", options.period)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, close, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close, &options.volume],
["high", "low", "close", "volume"],
)?;
let clv = clv_values(&high, &low, &close);
let out = rolling_weighted_mean_values(&clv, &volume, options.period);
self.study_output(CALL, output, out)
}
pub fn chaikin_oscillator(&self, options: ChaikinOscillator) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::chaikin_oscillator";
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
shorter(CALL, "chaikinOscillator", options.fast, options.slow)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, close, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close, &options.volume],
["high", "low", "close", "volume"],
)?;
let ad = cumulative_values(&money_flow_volume_values(&high, &low, &close, &volume));
let (fast, slow) = par::join(
|| ema_values(&ad, options.fast),
|| ema_values(&ad, options.slow),
);
let out = fast.iter().zip(&slow).map(|(f, s)| f - s).collect();
self.study_output(CALL, output, out)
}
pub fn money_flow_index(&self, options: MoneyFlowIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::money_flow_index";
positive(CALL, "period", options.period)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, close, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close, &options.volume],
["high", "low", "close", "volume"],
)?;
let typical = typical_price_values(&high, &low, &close);
let n = typical.len();
let mut rising = vec![f64::NAN; n];
let mut falling = vec![f64::NAN; n];
for i in 1..n {
let d = typical[i] - typical[i - 1];
let flow = typical[i] * volume[i];
if (d + flow).is_nan() {
continue;
}
rising[i] = if d > 0.0 { flow } else { 0.0 };
falling[i] = if d < 0.0 { flow } else { 0.0 };
}
let (up, down) = par::join(
|| rolling_mean_values(&rising, options.period),
|| rolling_mean_values(&falling, options.period),
);
let out = up
.iter()
.zip(&down)
.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 twiggs_money_flow(&self, options: TwiggsMoneyFlow) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::twiggs_money_flow";
positive(CALL, "period", options.period)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [close, volume, high, low] = self.price_columns(
CALL,
[&options.close, &options.volume, &options.high, &options.low],
["close", "volume", "high", "low"],
)?;
let n = close.len();
let mut flow = vec![f64::NAN; n];
let mut weight = vec![f64::NAN; n];
for i in 1..n {
let h = js_max(high[i], close[i - 1]);
let l = js_min(low[i], close[i - 1]);
let f = clv(h, l, close[i]) * volume[i];
flow[i] = f;
weight[i] = if f.is_nan() { f64::NAN } else { volume[i] };
}
let (numerator, denominator) = par::join(
|| wilder_values_from(&flow, options.period, 1),
|| wilder_values_from(&weight, options.period, 1),
);
let out = numerator
.iter()
.zip(&denominator)
.map(|(&f, &d)| if d == 0.0 { f64::NAN } else { f / d })
.collect();
self.study_output(CALL, output, out)
}
pub fn force_index(&self, options: ForceIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::force_index";
positive(CALL, "period", options.period)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [close, volume] =
self.price_columns(CALL, [&options.close, &options.volume], ["close", "volume"])?;
let mut ema = Ema::period(options.period);
let out = (0..close.len())
.map(|i| {
if i == 0 {
ema.push(f64::NAN)
} else {
ema.push((close[i] - close[i - 1]) * volume[i])
}
})
.collect();
self.study_output(CALL, output, out)
}
pub fn ease_of_movement(&self, options: EaseOfMovement) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::ease_of_movement";
positive(CALL, "period", options.period)?;
let scale = options.scale;
if !scale.is_finite() || scale <= 0.0 {
return Err(PondError::InvalidData {
call: CALL,
message: format!(
"easeOfMovement scale must be a positive finite number, got {}",
crate::grid::js_number(scale)
),
});
}
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.volume],
["high", "low", "volume"],
)?;
let mid = median_price_values(&high, &low);
let mut raw = vec![f64::NAN; mid.len()];
for i in 1..raw.len() {
let range = high[i] - low[i];
let v = volume[i];
if range == 0.0 || v == 0.0 {
continue;
}
raw[i] = ((mid[i] - mid[i - 1]) * range * scale) / v;
}
let out = moving_average_values(&raw, options.period, options.ma);
self.study_output(CALL, output, out)
}
pub fn klinger(&self, options: Klinger) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::klinger";
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
positive(CALL, "signalPeriod", options.signal)?;
shorter(CALL, "klinger", options.fast, options.slow)?;
let prefix = options.name();
let names = [prefix.to_string(), format!("{prefix}Signal")];
self.free_names(CALL, &names)?;
let [high, low, close, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close, &options.volume],
["high", "low", "close", "volume"],
)?;
let n = high.len();
let mut force = vec![f64::NAN; n];
let (mut first, mut trend, mut cm, mut prev_hlc, mut prev_dm) = (true, 0.0, 0.0, 0.0, 0.0);
for i in 0..n {
let (h, l, c, v) = (high[i], low[i], close[i], volume[i]);
if h.is_nan() || l.is_nan() || c.is_nan() || v.is_nan() {
first = true;
continue;
}
let hlc = h + l + c;
let dm = h - l;
if first {
first = false;
(trend, cm) = (0.0, 0.0);
} else {
let t = if hlc > prev_hlc { 1.0 } else { -1.0 };
cm = if t == trend { cm + dm } else { prev_dm + dm };
trend = t;
force[i] = v * (2.0 * (dm / cm) - 2.0).abs() * t * 100.0;
}
(prev_hlc, prev_dm) = (hlc, dm);
}
let (fast, slow) = par::join(
|| ema_values(&force, options.fast),
|| ema_values(&force, options.slow),
);
let line: Vec<f64> = fast.iter().zip(&slow).map(|(f, s)| f - s).collect();
let signal = ema_values(&line, options.signal);
self.study_outputs(CALL, names.into_iter().zip([line, signal]))
}
pub fn volume_oscillator(&self, options: VolumeOscillator) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::volume_oscillator";
shorter(CALL, "volumeOscillator", options.fast, options.slow)?;
positive(CALL, "fastPeriod", options.fast)?;
positive(CALL, "slowPeriod", options.slow)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [volume] = self.price_columns(CALL, [&options.volume], ["volume"])?;
let out = oscillator_values(
&volume,
options.fast,
options.slow,
options.ma,
OscillatorMode::Percent,
);
self.study_output(CALL, output, out)
}
pub fn trade_volume_index(&self, options: TradeVolumeIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::trade_volume_index";
let tick = options.min_tick;
if !tick.is_finite() || tick <= 0.0 {
return Err(PondError::InvalidData {
call: CALL,
message: format!(
"tradeVolumeIndex minTick must be a positive finite number (the instrument's minimum tick); got {}",
crate::grid::js_number(tick)
),
});
}
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [close, volume] = self.price_columns(
CALL,
[&options.column, &options.volume],
[super::DEFAULT_SOURCE, "volume"],
)?;
let n = close.len();
let mut out = vec![f64::NAN; n];
let (mut first, mut started) = (true, false);
let (mut value, mut direction, mut prev) = (f64::NAN, 0.0, 0.0);
for i in 0..n {
let (c, v) = (close[i], volume[i]);
if c.is_nan() || v.is_nan() {
first = true;
continue;
}
if first {
first = false;
value = if started { f64::NAN } else { 0.0 };
started = true;
direction = 0.0;
} else {
let step = c - prev;
if step > tick {
direction = 1.0;
} else if step < -tick {
direction = -1.0;
}
value += direction * v;
}
out[i] = value;
prev = c;
}
self.study_output(CALL, output, out)
}
pub fn negative_volume_index(&self, options: NegativeVolumeIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::negative_volume_index";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
volume_index_start(CALL, "negativeVolumeIndex", options.start)?;
let [close, volume] = self.price_columns(
CALL,
[&options.column, &options.volume],
[super::DEFAULT_SOURCE, "volume"],
)?;
let out = volume_index_values(&close, &volume, options.start, true);
self.study_output(CALL, output, out)
}
pub fn positive_volume_index(&self, options: PositiveVolumeIndex) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::positive_volume_index";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
volume_index_start(CALL, "positiveVolumeIndex", options.start)?;
let [close, volume] = self.price_columns(
CALL,
[&options.column, &options.volume],
[super::DEFAULT_SOURCE, "volume"],
)?;
let out = volume_index_values(&close, &volume, options.start, false);
self.study_output(CALL, output, out)
}
pub fn market_facilitation_index(
&self,
options: MarketFacilitationIndex,
) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::market_facilitation_index";
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.volume],
["high", "low", "volume"],
)?;
let range = bar_range_values(&high, &low);
let out = range
.iter()
.zip(volume.iter())
.map(|(&r, &v)| if v == 0.0 { f64::NAN } else { r / v })
.collect();
self.study_output(CALL, output, out)
}
pub fn shinohara_intensity_ratio(
&self,
options: ShinoharaIntensityRatio,
) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::shinohara_intensity_ratio";
positive(CALL, "period", options.period)?;
let prefix = options.name();
let names = [format!("{prefix}Strong"), format!("{prefix}Weak")];
self.free_names(CALL, &names)?;
let [open, high, low, close] = self.price_columns(
CALL,
[&options.open, &options.high, &options.low, &options.close],
["open", "high", "low", "close"],
)?;
let period = options.period;
let (strong, weak) = par::join(
|| {
let up: Vec<f64> = high.iter().zip(open.iter()).map(|(h, o)| h - o).collect();
let down: Vec<f64> = open.iter().zip(low.iter()).map(|(o, l)| o - l).collect();
intensity_ratio(&up, &down, period)
},
|| {
let prev = || std::iter::once(f64::NAN).chain(close.iter().copied());
let up: Vec<f64> = high.iter().zip(prev()).map(|(h, c)| h - c).collect();
let down: Vec<f64> = prev().zip(low.iter()).map(|(c, l)| c - l).collect();
intensity_ratio(&up, &down, period)
},
);
self.study_outputs(CALL, names.into_iter().zip([strong, weak]))
}
pub fn vwap(&self, options: Vwap) -> Result<TimeSeries> {
const CALL: &str = "TimeSeries::vwap";
positive(CALL, "period", options.period)?;
let output = options.name().to_string();
self.free_names(CALL, &[&output])?;
let [high, low, close, volume] = self.price_columns(
CALL,
[&options.high, &options.low, &options.close, &options.volume],
["high", "low", "close", "volume"],
)?;
let typical = typical_price_values(&high, &low, &close);
let out = rolling_weighted_mean_values(&typical, &volume, options.period);
self.study_output(CALL, output, out)
}
}
fn volume_index_start(call: &'static str, study: &str, start: f64) -> Result<()> {
if !start.is_finite() || start <= 0.0 {
return Err(PondError::InvalidData {
call,
message: format!("{study} start must be a positive finite number"),
});
}
Ok(())
}
fn volume_index_values(close: &[f64], volume: &[f64], start: f64, on_fall: bool) -> Vec<f64> {
let n = close.len();
let mut out = vec![f64::NAN; n];
let (mut first, mut value, mut prev_close, mut prev_volume) =
(true, f64::NAN, f64::NAN, f64::NAN);
for i in 0..n {
let (c, v) = (close[i], volume[i]);
if c.is_nan() || v.is_nan() {
first = true;
continue;
}
if first {
first = false;
value = start;
} else if on_fall == (v < prev_volume) && v != prev_volume {
value = if prev_close == 0.0 {
f64::NAN
} else {
value * (1.0 + (c - prev_close) / prev_close)
};
}
out[i] = value;
(prev_close, prev_volume) = (c, v);
}
out
}
fn intensity_ratio(up: &[f64], down: &[f64], period: usize) -> Vec<f64> {
let (numerator, denominator) = par::join(
|| rolling_mean_values(up, period),
|| rolling_mean_values(down, period),
);
numerator
.iter()
.zip(&denominator)
.map(|(&u, &d)| if d == 0.0 { f64::NAN } else { (100.0 * u) / d })
.collect()
}
fn rolling_weighted_mean_values(values: &[f64], weights: &[f64], period: usize) -> Vec<f64> {
rolling_weighted_mean_chunked(values, weights, period, 1 << 16)
}
fn rolling_weighted_mean_chunked(
values: &[f64],
weights: &[f64],
period: usize,
chunk: usize,
) -> Vec<f64> {
let n = values.len();
let (mut product, mut masked) = (vec![0.0; n], vec![0.0; n]);
par::fill_chunks(
&mut product,
&mut masked,
chunk,
|start, product, masked| {
for (k, (p, m)) in product.iter_mut().zip(masked).enumerate() {
let w = weights[start + k];
*p = values[start + k] * w;
*m = if p.is_finite() { w } else { f64::NAN };
}
},
);
let (mut numerator, denominator) = par::join(
|| rolling_mean_sd(&product, period, false).0,
|| rolling_mean_sd(&masked, period, false).0,
);
par::fill_chunks(&mut numerator, &mut [], chunk, |start, numerator, _| {
for (k, x) in numerator.iter_mut().enumerate() {
*x /= denominator[start + k];
}
});
numerator
}
#[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)),
crate::Column::required("volume", price(100.0)),
],
)
.unwrap()
}
fn message(r: Result<TimeSeries>) -> String {
r.unwrap_err().to_string()
}
#[test]
fn non_finite_ticks_and_starts_are_refused() {
let s = bars();
for (bad, text) in [
(f64::INFINITY, "Infinity"),
(f64::NEG_INFINITY, "-Infinity"),
(f64::NAN, "NaN"),
(0.0, "0"),
] {
let tvi = message(s.trade_volume_index(TradeVolumeIndex::new(bad)));
assert!(
tvi.ends_with(&format!("(the instrument's minimum tick); got {text}")),
"{tvi}"
);
let nvi = message(s.negative_volume_index(NegativeVolumeIndex::default().start(bad)));
assert!(nvi.contains("negativeVolumeIndex start"), "{nvi}");
let pvi = message(s.positive_volume_index(PositiveVolumeIndex::default().start(bad)));
assert!(pvi.contains("positiveVolumeIndex start"), "{pvi}");
}
}
#[test]
fn errors_come_in_pond_ts_order() {
let s = bars();
let tvi = message(s.trade_volume_index(TradeVolumeIndex::new(0.0).output("close")));
assert!(tvi.contains("minTick"), "{tvi}");
let nvi = message(
s.negative_volume_index(NegativeVolumeIndex::default().start(0.0).output("close")),
);
assert!(nvi.contains("collides"), "{nvi}");
let kvo = message(s.klinger(Klinger::default().fast(0).slow(0)));
assert!(kvo.contains("fastPeriod must be"), "{kvo}");
let kvo = message(s.klinger(Klinger::default().fast(5).slow(4).signal(0)));
assert!(kvo.contains("signalPeriod must be"), "{kvo}");
let vo = message(s.volume_oscillator(VolumeOscillator::default().fast(0).slow(0)));
assert!(vo.contains("volumeOscillator fastPeriod (0)"), "{vo}");
let vo = message(s.volume_oscillator(VolumeOscillator::default().fast(0).slow(3)));
assert!(vo.contains("fastPeriod must be"), "{vo}");
let sir = message(
s.shinohara_intensity_ratio(ShinoharaIntensityRatio::default().period(0).prefix("c")),
);
assert!(sir.contains("period must be"), "{sir}");
let vwap = message(s.vwap(Vwap::new(3).output("close").volume("nope")));
assert!(vwap.contains("collides"), "{vwap}");
let bwmfi = message(
s.market_facilitation_index(
MarketFacilitationIndex::default()
.output("close")
.volume("nope"),
),
);
assert!(bwmfi.contains("collides"), "{bwmfi}");
let pvi = message(
s.positive_volume_index(PositiveVolumeIndex::default().start(-1.0).volume("nope")),
);
assert!(pvi.contains("positiveVolumeIndex start"), "{pvi}");
}
#[test]
fn weighted_mean_in_stretches_matches_one_piece() {
let mut x = 0x9E37_79B9_7F4A_7C15u64;
let mut next = || {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
};
let n = 5_000;
let (mut values, mut weights) = (Vec::with_capacity(n), Vec::with_capacity(n));
for i in 0..n {
let (a, b) = (next(), next());
values.push(match i {
_ if (700..730).contains(&i) => f64::NAN,
_ if i % 1_009 == 3 => f64::NAN,
_ => (a % 2_001) as f64 / 1_000.0 - 1.0,
});
weights.push(match i {
_ if i % 997 == 11 => f64::NAN,
_ if (2_000..2_010).contains(&i) => 0.0,
_ => 1_000.0 + (b % 900) as f64,
});
}
for period in [1, 3, 20] {
let want = rolling_weighted_mean_chunked(&values, &weights, period, n);
assert!(want.iter().filter(|v| v.is_finite()).count() > 4_000);
for chunk in [1, 2, 7, 64, 999] {
let got = rolling_weighted_mean_chunked(&values, &weights, period, chunk);
assert!(
want.iter()
.zip(&got)
.all(|(a, b)| a.to_bits() == b.to_bits()),
"period {period}, chunk {chunk}"
);
}
}
}
}