use alloc::vec;
use alloc::vec::Vec;
pub fn sma(values: &[f64], period: usize) -> Vec<f64> {
let mut out = vec![f64::NAN; values.len()];
if period == 0 || period > values.len() {
return out;
}
let mut sum = 0.0;
for (index, value) in values.iter().enumerate() {
if !value.is_finite() {
sum = f64::NAN;
continue;
}
if sum.is_nan() {
let window_start = index.saturating_sub(period - 1);
let window = &values[window_start..=index];
if !window.iter().all(|v| v.is_finite()) {
continue;
}
sum = window.iter().sum();
} else {
sum += value;
if index >= period {
sum -= values[index - period];
}
}
if index + 1 >= period {
out[index] = sum / period as f64;
}
}
out
}
pub fn ema(values: &[f64], period: usize) -> Vec<f64> {
if period == 0 {
return values.to_vec();
}
let alpha = 2.0 / (period as f64 + 1.0);
let mut out = vec![f64::NAN; values.len()];
let mut previous = f64::NAN;
for (index, value) in values.iter().enumerate() {
if !value.is_finite() {
previous = f64::NAN;
continue;
}
previous =
if previous.is_nan() { *value } else { alpha * value + (1.0 - alpha) * previous };
out[index] = previous;
}
out
}
pub fn wilder_smooth(values: &[f64], period: usize) -> Vec<f64> {
let mut out = vec![f64::NAN; values.len()];
if period == 0 {
return out;
}
let mut previous = f64::NAN;
for (index, value) in values.iter().enumerate() {
if !value.is_finite() {
previous = f64::NAN;
continue;
}
previous =
if previous.is_nan() { *value } else { previous + (*value - previous) / period as f64 };
out[index] = previous;
}
out
}
pub fn rsi(values: &[f64], period: usize) -> Vec<f64> {
let mut out = vec![f64::NAN; values.len()];
if period == 0 || values.len() <= period {
return out;
}
let mut gains = vec![0.0; values.len()];
let mut losses = vec![0.0; values.len()];
for index in 1..values.len() {
let previous = values[index - 1];
let current = values[index];
if !previous.is_finite() || !current.is_finite() {
gains[index] = f64::NAN;
losses[index] = f64::NAN;
continue;
}
let change = current - previous;
gains[index] = change.max(0.0);
losses[index] = (-change).max(0.0);
}
let seed_gains = &gains[1..=period];
let seed_losses = &losses[1..=period];
if !seed_gains.iter().all(|v| v.is_finite()) || !seed_losses.iter().all(|v| v.is_finite()) {
return out;
}
let mut average_gain: f64 = seed_gains.iter().sum::<f64>() / period as f64;
let mut average_loss: f64 = seed_losses.iter().sum::<f64>() / period as f64;
out[period] = rsi_from(average_gain, average_loss);
for index in (period + 1)..values.len() {
if !gains[index].is_finite() || !losses[index].is_finite() {
return out;
}
average_gain = (average_gain * (period as f64 - 1.0) + gains[index]) / period as f64;
average_loss = (average_loss * (period as f64 - 1.0) + losses[index]) / period as f64;
out[index] = rsi_from(average_gain, average_loss);
}
out
}
fn rsi_from(average_gain: f64, average_loss: f64) -> f64 {
if average_loss == 0.0 {
return if average_gain == 0.0 { 50.0 } else { 100.0 };
}
let relative_strength = average_gain / average_loss;
100.0 - (100.0 / (1.0 + relative_strength))
}
pub fn macd(
values: &[f64],
fast_period: usize,
slow_period: usize,
signal_period: usize,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let fast = ema(values, fast_period);
let slow = ema(values, slow_period);
let line: Vec<f64> = fast
.iter()
.zip(slow.iter())
.map(|(f, s)| if f.is_finite() && s.is_finite() { f - s } else { f64::NAN })
.collect();
let signal = ema(&line, signal_period);
let histogram: Vec<f64> = line
.iter()
.zip(signal.iter())
.map(|(m, s)| if m.is_finite() && s.is_finite() { m - s } else { f64::NAN })
.collect();
(line, signal, histogram)
}
pub fn bollinger_bands(
values: &[f64],
period: usize,
multiplier: f64,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let middle = sma(values, period);
let mut lower = vec![f64::NAN; values.len()];
let mut upper = vec![f64::NAN; values.len()];
if period == 0 {
return (lower, middle, upper);
}
for index in 0..values.len() {
if !middle[index].is_finite() {
continue;
}
let Some(start) = index.checked_add(1).and_then(|end| end.checked_sub(period)) else {
continue;
};
if start > index {
continue;
}
let window = &values[start..=index];
if !window.iter().all(|v| v.is_finite()) {
continue;
}
let mean = middle[index];
let variance = window.iter().map(|v| (v - mean) * (v - mean)).sum::<f64>() / period as f64;
let deviation = variance.sqrt();
lower[index] = mean - multiplier * deviation;
upper[index] = mean + multiplier * deviation;
}
(lower, middle, upper)
}
pub fn stochastic(
highs: &[f64],
lows: &[f64],
closes: &[f64],
period: usize,
k_smoothing: usize,
d_period: usize,
) -> (Vec<f64>, Vec<f64>) {
let length = closes.len().min(highs.len()).min(lows.len());
let mut raw_k = vec![f64::NAN; length];
if period == 0 {
return (vec![f64::NAN; length], vec![f64::NAN; length]);
}
for (window_start, slot) in raw_k.iter_mut().enumerate().skip(period - 1) {
let window_high = &highs[window_start + 1 - period..=window_start];
let window_low = &lows[window_start + 1 - period..=window_start];
let close = closes[window_start];
if !window_high.iter().all(|v| v.is_finite())
|| !window_low.iter().all(|v| v.is_finite())
|| !close.is_finite()
{
continue;
}
let highest = window_high.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let lowest = window_low.iter().copied().fold(f64::INFINITY, f64::min);
let range = highest - lowest;
*slot = if range == 0.0 { 50.0 } else { (close - lowest) / range * 100.0 };
}
let k = if k_smoothing <= 1 { raw_k } else { sma(&raw_k, k_smoothing) };
let d = sma(&k, d_period);
(k, d)
}
fn true_range(highs: &[f64], lows: &[f64], closes: &[f64], length: usize) -> Vec<f64> {
let mut ranges = vec![f64::NAN; length];
for index in 1..length {
let high = highs[index];
let low = lows[index];
let previous_close = closes[index - 1];
if !high.is_finite() || !low.is_finite() || !previous_close.is_finite() {
continue;
}
ranges[index] =
(high - low).max((high - previous_close).abs()).max((low - previous_close).abs());
}
ranges
}
pub fn atr(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Vec<f64> {
let length = closes.len().min(highs.len()).min(lows.len());
let mut out = vec![f64::NAN; length];
if period == 0 || length <= period {
return out;
}
let ranges = true_range(highs, lows, closes, length);
let seed_ranges = &ranges[1..=period];
if !seed_ranges.iter().all(|v| v.is_finite()) {
return out;
}
let mut average: f64 = seed_ranges.iter().sum::<f64>() / period as f64;
out[period] = average;
for index in (period + 1)..length {
if !ranges[index].is_finite() {
return out;
}
average = (average * (period as f64 - 1.0) + ranges[index]) / period as f64;
out[index] = average;
}
out
}
pub fn trailing_extremes(highs: &[f64], lows: &[f64], period: usize) -> (Vec<f64>, Vec<f64>) {
let length = highs.len().min(lows.len());
let mut out_high = vec![f64::NAN; length];
let mut out_low = vec![f64::NAN; length];
if period == 0 || period > length {
return (out_high, out_low);
}
let mut max_deque: Vec<usize> = Vec::with_capacity(period);
let mut min_deque: Vec<usize> = Vec::with_capacity(period);
for index in 0..length {
while max_deque.first().is_some_and(|front| *front + period <= index) {
max_deque.remove(0);
}
while min_deque.first().is_some_and(|front| *front + period <= index) {
min_deque.remove(0);
}
if highs[index].is_finite() {
while max_deque.last().is_some_and(|back| highs[*back] <= highs[index]) {
max_deque.pop();
}
max_deque.push(index);
}
if lows[index].is_finite() {
while min_deque.last().is_some_and(|back| lows[*back] >= lows[index]) {
min_deque.pop();
}
min_deque.push(index);
}
if index + 1 < period {
continue;
}
let window_start = index + 1 - period;
let clean = highs[window_start..=index].iter().all(|v| v.is_finite())
&& lows[window_start..=index].iter().all(|v| v.is_finite());
if !clean {
continue;
}
if let Some(front) = max_deque.first() {
out_high[index] = highs[*front];
}
if let Some(front) = min_deque.first() {
out_low[index] = lows[*front];
}
}
(out_high, out_low)
}
pub fn donchian_channel(
highs: &[f64],
lows: &[f64],
period: usize,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let (upper, lower) = trailing_extremes(highs, lows, period);
let middle: Vec<f64> = upper
.iter()
.zip(lower.iter())
.map(|(u, l)| if u.is_finite() && l.is_finite() { (u + l) / 2.0 } else { f64::NAN })
.collect();
(lower, middle, upper)
}
pub fn vwap(
highs: &[f64],
lows: &[f64],
closes: &[f64],
volumes: &[f64],
period: usize,
) -> Vec<f64> {
let length = closes.len().min(highs.len()).min(lows.len()).min(volumes.len());
let mut out = vec![f64::NAN; length];
if period == 0 || period > length {
return out;
}
for (index, slot) in out.iter_mut().enumerate().skip(period - 1) {
let window_start = index + 1 - period;
let mut weighted = 0.0;
let mut total_volume = 0.0;
let mut clean = true;
for offset in window_start..=index {
let high = highs[offset];
let low = lows[offset];
let close = closes[offset];
let volume = volumes[offset];
if !high.is_finite() || !low.is_finite() || !close.is_finite() || !volume.is_finite() {
clean = false;
break;
}
let typical = (high + low + close) / 3.0;
weighted += typical * volume;
total_volume += volume;
}
if clean && total_volume != 0.0 {
*slot = weighted / total_volume;
}
}
out
}
pub fn money_flow_index(
highs: &[f64],
lows: &[f64],
closes: &[f64],
volumes: &[f64],
period: usize,
) -> Vec<f64> {
let length = closes.len().min(highs.len()).min(lows.len()).min(volumes.len());
let mut out = vec![f64::NAN; length];
if period == 0 || length <= period {
return out;
}
let mut positive = vec![0.0; length];
let mut negative = vec![0.0; length];
for index in 1..length {
let high = highs[index];
let low = lows[index];
let close = closes[index];
let volume = volumes[index];
if !high.is_finite() || !low.is_finite() || !close.is_finite() || !volume.is_finite() {
positive[index] = f64::NAN;
negative[index] = f64::NAN;
continue;
}
let typical = (high + low + close) / 3.0;
let previous_typical = (highs[index - 1] + lows[index - 1] + closes[index - 1]) / 3.0;
if !previous_typical.is_finite() {
positive[index] = f64::NAN;
negative[index] = f64::NAN;
continue;
}
let flow = typical * volume;
if typical > previous_typical {
positive[index] = flow;
} else if typical < previous_typical {
negative[index] = flow;
}
}
for index in period..length {
let window_start = index + 1 - period;
let positive_slice = &positive[window_start..=index];
let negative_slice = &negative[window_start..=index];
if !positive_slice.iter().all(|v| v.is_finite())
|| !negative_slice.iter().all(|v| v.is_finite())
{
continue;
}
let total_positive: f64 = positive_slice.iter().sum();
let total_negative: f64 = negative_slice.iter().sum();
out[index] = if total_negative == 0.0 {
if total_positive == 0.0 {
50.0
} else {
100.0
}
} else {
let ratio = total_positive / total_negative;
100.0 - (100.0 / (1.0 + ratio))
};
}
out
}
pub fn on_balance_volume(closes: &[f64], volumes: &[f64]) -> Vec<f64> {
let length = closes.len().min(volumes.len());
let mut out = vec![f64::NAN; length];
if length == 0 {
return out;
}
let mut total = 0.0;
for index in 0..length {
if !closes[index].is_finite() || !volumes[index].is_finite() {
continue;
}
if index == 0 {
total = volumes[index];
} else if !closes[index - 1].is_finite() {
continue;
} else if closes[index] > closes[index - 1] {
total += volumes[index];
} else if closes[index] < closes[index - 1] {
total -= volumes[index];
}
out[index] = total;
}
out
}
pub fn series_extent(values: &[f64]) -> (f64, f64) {
let mut low = f64::INFINITY;
let mut high = f64::NEG_INFINITY;
for value in values.iter().filter(|v| v.is_finite()) {
low = low.min(*value);
high = high.max(*value);
}
if low > high {
(f64::NAN, f64::NAN)
} else {
(low, high)
}
}
pub fn has_drawable_values(values: &[f64]) -> bool {
values.iter().any(|value| value.is_finite())
}
pub fn align_left(values: &[f64], length: usize) -> Vec<f64> {
if values.len() >= length {
return values[..length].to_vec();
}
let mut out = vec![f64::NAN; length];
let offset = length - values.len();
out[offset..].copy_from_slice(values);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn close(left: f64, right: f64) -> bool {
(left - right).abs() < 1e-9
}
#[test]
fn a_moving_average_warms_up_over_its_window() {
let result = sma(&[1.0, 2.0, 3.0, 4.0, 5.0], 3);
assert!(result[0].is_nan(), "one sample cannot fill a 3-period window");
assert!(result[1].is_nan(), "two samples cannot either");
assert!(close(result[2], 2.0), "mean of 1,2,3");
assert!(close(result[3], 3.0), "mean of 2,3,4");
assert!(close(result[4], 4.0), "mean of 3,4,5");
}
#[test]
fn a_moving_average_longer_than_the_series_is_all_gaps() {
let result = sma(&[1.0, 2.0], 5);
assert_eq!(result.len(), 2, "the length must always match the input");
assert!(result.iter().all(|value| value.is_nan()));
}
#[test]
fn a_zero_period_moving_average_produces_no_values() {
let result = sma(&[1.0, 2.0, 3.0], 0);
assert!(result.iter().all(|value| value.is_nan()));
}
#[test]
fn the_rolling_sum_matches_a_naive_resum() {
let values: Vec<f64> =
(0..50).map(|index| (index as f64 * 0.37).sin() * 10.0 + 20.0).collect();
let rolling = sma(&values, 7);
for index in 6..values.len() {
let naive: f64 = values[index - 6..=index].iter().sum::<f64>() / 7.0;
assert!(close(rolling[index], naive), "index {index} must agree with a re-sum");
}
}
#[test]
fn a_non_finite_sample_produces_a_gap_rather_than_a_guess() {
let result = sma(&[1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0], 3);
assert!(result[4].is_nan(), "the window containing NaN is undefined");
assert!(result[5].is_finite(), "and recovers once the bad sample leaves");
assert!(close(result[5], 5.0), "mean of 4,5,6");
}
#[test]
fn an_infinite_sample_is_treated_as_a_gap() {
let result = sma(&[1.0, f64::INFINITY, 3.0], 2);
assert!(result[1].is_nan(), "the window containing infinity is undefined");
assert!(result[2].is_nan(), "and the next window still contains it");
}
#[test]
fn an_exponential_average_starts_at_the_first_sample() {
let result = ema(&[10.0, 10.0, 10.0, 10.0], 3);
assert!(close(result[0], 10.0), "seeded with the first value");
assert!(result.iter().all(|value| close(*value, 10.0)), "a flat series stays flat");
}
#[test]
fn an_exponential_average_moves_by_its_smoothing_factor() {
let result = ema(&[0.0, 10.0], 3);
assert!(close(result[1], 5.0), "half the step, got {}", result[1]);
}
#[test]
fn a_zero_period_exponential_average_returns_the_input() {
let values = [3.0, 1.0, 4.0];
assert_eq!(ema(&values, 0), values.to_vec());
}
#[test]
fn a_gap_re_seeds_an_exponential_average() {
let result = ema(&[10.0, f64::NAN, 20.0, 20.0], 3);
assert!(close(result[2], 20.0), "the sample after a gap becomes the seed");
assert!(close(result[3], 20.0));
}
#[test]
fn wilder_smoothing_is_defined_immediately() {
let result = wilder_smooth(&[5.0, 6.0, 7.0], 3);
assert!(close(result[0], 5.0));
assert!(result[1] > 5.0 && result[1] < 6.0, "it moves towards the sample");
assert!(result[2] > result[1], "and keeps moving");
}
#[test]
fn rsi_of_a_rising_series_saturates_at_one_hundred() {
let values: Vec<f64> = (0..30).map(|index| 100.0 + index as f64).collect();
let result = rsi(&values, 14);
assert!(result[..14].iter().all(|value| value.is_nan()), "warm-up is undefined");
assert!(close(result[14], 100.0), "no losses means the maximum reading");
assert!(close(result[29], 100.0));
}
#[test]
fn rsi_of_a_falling_series_bottoms_at_zero() {
let values: Vec<f64> = (0..30).map(|index| 100.0 - index as f64).collect();
let result = rsi(&values, 14);
assert!(close(result[14], 0.0), "no gains means the minimum reading");
}
#[test]
fn rsi_of_a_flat_series_is_neutral() {
let result = rsi(&[50.0; 30], 14);
assert!(close(result[14], 50.0), "equal gains and losses is the midpoint");
assert!(close(result[29], 50.0));
}
#[test]
fn rsi_stays_within_zero_and_one_hundred() {
let values: Vec<f64> = (0..200)
.map(|index| 100.0 + (index as f64 * 0.7).sin() * 5.0 + (index as f64 * 0.13).cos())
.collect();
for value in rsi(&values, 14).iter().filter(|value| value.is_finite()) {
assert!((0.0..=100.0).contains(value), "RSI out of range: {value}");
}
}
#[test]
fn rsi_needs_more_samples_than_its_period() {
assert!(rsi(&[1.0, 2.0, 3.0], 14).iter().all(|value| value.is_nan()));
assert!(rsi(&[1.0, 2.0, 3.0], 0).iter().all(|value| value.is_nan()));
}
#[test]
fn rsi_stops_at_a_gap_rather_than_carrying_a_value_forward() {
let mut values: Vec<f64> =
(0..60).map(|index| 100.0 + (index as f64 * 0.4).sin()).collect();
assert!(rsi(&values, 14)[59].is_finite(), "the fixture is clean to begin with");
values[50] = f64::NAN;
let result = rsi(&values, 14);
assert!(result[50].is_nan(), "the window containing the gap is undefined");
assert!(result[59].is_nan(), "and the computation stops there rather than resuming");
}
#[test]
fn macd_histogram_is_the_difference_of_its_two_lines() {
let values: Vec<f64> =
(0..120).map(|index| 100.0 + (index as f64 * 0.3).sin() * 8.0).collect();
let (line, signal, histogram) = macd(&values, 12, 26, 9);
assert_eq!(line.len(), values.len());
assert_eq!(signal.len(), values.len());
assert_eq!(histogram.len(), values.len());
for index in 0..values.len() {
if line[index].is_finite() && signal[index].is_finite() {
assert!(
close(histogram[index], line[index] - signal[index]),
"index {index}: histogram must be line minus signal"
);
}
}
}
#[test]
fn macd_is_positive_while_price_rises() {
let values: Vec<f64> = (0..80).map(|index| 100.0 + index as f64 * 2.0).collect();
let (line, _, _) = macd(&values, 12, 26, 9);
assert!(
line.last().is_some_and(|value| *value > 0.0),
"a steady rise gives a positive MACD"
);
}
#[test]
fn macd_with_no_signal_period_has_a_zero_histogram() {
let values: Vec<f64> = (0..40).map(|index| 100.0 + index as f64).collect();
let (line, signal, histogram) = macd(&values, 3, 6, 0);
assert_eq!(line, signal, "a zero-period EMA is the identity");
assert!(
histogram.iter().all(|value| !value.is_finite() || close(*value, 0.0)),
"so the histogram collapses to zero"
);
}
#[test]
fn bollinger_bands_are_symmetric_about_the_middle() {
let values: Vec<f64> =
(0..60).map(|index| 100.0 + (index as f64 * 0.5).sin() * 6.0).collect();
let (lower, middle, upper) = bollinger_bands(&values, 20, 2.0);
for index in 19..values.len() {
if !middle[index].is_finite() {
continue;
}
assert!(lower[index] < middle[index], "the lower band sits below the mean");
assert!(upper[index] > middle[index], "the upper band sits above it");
assert!(
close(middle[index] - lower[index], upper[index] - middle[index]),
"index {index}: the bands must be equidistant"
);
}
}
#[test]
fn band_width_is_the_population_deviation() {
let values = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let (lower, middle, upper) = bollinger_bands(&values, 4, 1.0);
assert!(close(middle[5], 4.5), "the middle band is the mean");
assert!(close(upper[5] - 4.5, 0.5), "population deviation is 0.5, not 0.577");
assert!(close(4.5 - lower[5], 0.5));
}
#[test]
fn bollinger_bands_are_gaps_during_warm_up() {
let values: Vec<f64> = (0..10).map(|index| index as f64).collect();
let (lower, middle, upper) = bollinger_bands(&values, 5, 2.0);
assert!(lower[3].is_nan() && middle[3].is_nan() && upper[3].is_nan());
assert!(middle[4].is_finite(), "the first full window is at index 4");
}
#[test]
fn bollinger_bands_with_period_longer_than_the_series_are_all_gaps() {
let values = [1.0, 2.0, 3.0];
for period in [4, 5, 100] {
let (lower, middle, upper) = bollinger_bands(&values, period, 2.0);
assert!(
lower.iter().chain(&middle).chain(&upper).all(|v| v.is_nan()),
"period {period} exceeds the series, so every band must be a gap"
);
}
}
#[test]
fn bollinger_bands_with_period_equal_to_the_series_fills_the_last_index() {
let values = [2.0, 4.0, 4.0, 4.0];
let (lower, middle, upper) = bollinger_bands(&values, 4, 1.0);
assert!(middle[..3].iter().all(|v| v.is_nan()), "warm-up is the first period-1");
assert!(close(middle[3], 3.5), "the single full window ends at the last index");
let deviation = 0.75f64.sqrt();
assert!(
close(upper[3] - 3.5, deviation) && close(3.5 - lower[3], deviation),
"the boundary window must be measured, not skipped: got upper {} lower {}",
upper[3],
lower[3]
);
}
#[test]
fn bollinger_bands_of_a_flat_series_collapse() {
let (lower, middle, upper) = bollinger_bands(&[7.0; 20], 5, 2.0);
assert!(close(lower[10], 7.0) && close(middle[10], 7.0) && close(upper[10], 7.0));
}
#[test]
fn stochastic_reads_the_top_of_the_range() {
let highs = [10.0, 12.0, 14.0, 16.0, 18.0];
let lows = [8.0, 10.0, 12.0, 14.0, 16.0];
let closes = [18.0; 5];
let (k, _) = stochastic(&highs, &lows, &closes, 5, 1, 3);
assert!(close(k[4], 100.0), "a close at the high reads 100");
}
#[test]
fn stochastic_reads_the_bottom_of_the_range() {
let highs = [10.0, 12.0, 14.0, 16.0, 18.0];
let lows = [8.0, 10.0, 12.0, 14.0, 16.0];
let closes = [8.0; 5];
let (k, _) = stochastic(&highs, &lows, &closes, 5, 1, 3);
assert!(close(k[4], 0.0), "a close at the low reads 0");
}
#[test]
fn stochastic_of_a_zero_range_reads_neutral() {
let (k, _) = stochastic(&[5.0; 5], &[5.0; 5], &[5.0; 5], 3, 1, 3);
assert!(close(k[2], 50.0), "no range at all is the midpoint");
}
#[test]
fn stochastic_with_a_zero_period_produces_nothing() {
let (k, d) = stochastic(&[1.0; 4], &[1.0; 4], &[1.0; 4], 0, 1, 3);
assert!(k.iter().all(|value| value.is_nan()));
assert!(d.iter().all(|value| value.is_nan()));
}
#[test]
fn true_range_accounts_for_gaps() {
let ranges = true_range(&[10.0, 30.0], &[9.0, 28.0], &[9.5, 29.0], 2);
assert!(close(ranges[1], 20.5), "30 - 9.5, not the 30 - 28 span");
}
#[test]
fn atr_is_defined_after_its_period() {
let highs: Vec<f64> = (0..20).map(|index| 10.0 + index as f64).collect();
let lows: Vec<f64> = (0..20).map(|index| 9.0 + index as f64).collect();
let closes: Vec<f64> = (0..20).map(|index| 9.5 + index as f64).collect();
let result = atr(&highs, &lows, &closes, 5);
assert!(result[..5].iter().all(|value| value.is_nan()), "warm-up");
assert!(result[5].is_finite(), "the first value is at index `period`");
assert!(close(result[5], 1.5), "got {}", result[5]);
}
#[test]
fn atr_is_larger_than_the_high_low_span_when_a_gap_exists() {
let highs = [10.0, 30.0, 30.5];
let lows = [9.0, 28.0, 28.5];
let closes = [9.5, 29.0, 29.5];
let result = atr(&highs, &lows, &closes, 2);
assert!(close(result[2], 11.25), "got {}", result[2]);
}
#[test]
fn atr_needs_more_samples_than_its_period() {
let result = atr(&[1.0, 2.0], &[1.0, 2.0], &[1.0, 2.0], 5);
assert!(result.iter().all(|value| value.is_nan()));
}
#[test]
fn trailing_extremes_follow_the_window() {
let highs = [1.0, 5.0, 2.0, 2.0, 2.0];
let lows = [1.0, 4.0, 0.5, 0.5, 0.5];
let (upper, lower) = trailing_extremes(&highs, &lows, 2);
assert!(close(upper[1], 5.0), "window [1,1] includes the spike");
assert!(close(upper[2], 5.0), "window [1,2] still includes it");
assert!(close(upper[3], 2.0), "the spike has left the window");
assert!(close(lower[2], 0.5), "the low is picked up as it arrives");
}
#[test]
fn trailing_extremes_match_a_naive_scan() {
let highs: Vec<f64> =
(0..200).map(|index| 50.0 + (index as f64 * 0.31).sin() * 10.0).collect();
let lows: Vec<f64> = highs.iter().map(|high| high - 2.0).collect();
let (upper, lower) = trailing_extremes(&highs, &lows, 13);
for index in 12..highs.len() {
let expected_high =
highs[index - 12..=index].iter().copied().fold(f64::NEG_INFINITY, f64::max);
let expected_low =
lows[index - 12..=index].iter().copied().fold(f64::INFINITY, f64::min);
assert!(close(upper[index], expected_high), "index {index} high");
assert!(close(lower[index], expected_low), "index {index} low");
}
}
#[test]
fn trailing_extremes_need_a_full_window() {
let (upper, lower) = trailing_extremes(&[1.0, 2.0], &[1.0, 2.0], 5);
assert!(upper.iter().all(|value| value.is_nan()));
assert!(lower.iter().all(|value| value.is_nan()));
}
#[test]
fn donchian_middle_is_the_channel_centre() {
let highs: Vec<f64> = (0..20).map(|index| 10.0 + index as f64).collect();
let lows: Vec<f64> = (0..20).map(|index| index as f64).collect();
let (lower, middle, upper) = donchian_channel(&highs, &lows, 3);
assert!(close(middle[5], (upper[5] + lower[5]) / 2.0));
assert!(close(upper[5], 15.0), "the highest of the last three highs");
assert!(close(lower[5], 3.0), "the lowest of the last three lows");
}
#[test]
fn vwap_weights_by_volume() {
let result = vwap(&[10.0, 20.0], &[10.0, 20.0], &[10.0, 20.0], &[1.0, 9.0], 2);
assert!(close(result[1], 19.0), "got {}", result[1]);
}
#[test]
fn vwap_with_no_volume_is_undefined() {
let result = vwap(&[10.0], &[10.0], &[10.0], &[0.0], 1);
assert!(result[0].is_nan(), "no volume means no average price");
}
#[test]
fn vwap_uses_the_typical_price() {
let result = vwap(&[30.0], &[6.0], &[12.0], &[5.0], 1);
assert!(close(result[0], 16.0), "got {}", result[0]);
}
#[test]
fn money_flow_index_saturates_on_a_pure_uptrend() {
let highs: Vec<f64> = (0..20).map(|index| 11.0 + index as f64).collect();
let lows: Vec<f64> = (0..20).map(|index| 10.0 + index as f64).collect();
let closes: Vec<f64> = (0..20).map(|index| 10.5 + index as f64).collect();
let result = money_flow_index(&highs, &lows, &closes, &[100.0; 20], 5);
assert!(close(result[19], 100.0), "every bar rose, so there is no negative flow");
}
#[test]
fn money_flow_index_bottoms_on_a_pure_downtrend() {
let highs: Vec<f64> = (0..20).map(|index| 11.0 - index as f64).collect();
let lows: Vec<f64> = (0..20).map(|index| 10.0 - index as f64).collect();
let closes: Vec<f64> = (0..20).map(|index| 10.5 - index as f64).collect();
let result = money_flow_index(&highs, &lows, &closes, &[100.0; 20], 5);
assert!(close(result[19], 0.0), "no positive flow means the minimum reading");
}
#[test]
fn money_flow_index_of_a_flat_series_is_neutral() {
let result = money_flow_index(&[5.0; 20], &[5.0; 20], &[5.0; 20], &[100.0; 20], 5);
assert!(close(result[19], 50.0), "nothing moved in either direction");
}
#[test]
fn money_flow_index_stays_within_its_bounds() {
let highs: Vec<f64> =
(0..120).map(|index| 20.0 + (index as f64 * 0.4).sin() * 3.0).collect();
let lows: Vec<f64> = highs.iter().map(|high| high - 1.0).collect();
let closes: Vec<f64> = highs.iter().map(|high| high - 0.5).collect();
let volumes: Vec<f64> = (0..120).map(|index| 100.0 + (index % 7) as f64 * 10.0).collect();
for value in money_flow_index(&highs, &lows, &closes, &volumes, 14)
.iter()
.filter(|value| value.is_finite())
{
assert!((0.0..=100.0).contains(value), "MFI out of range: {value}");
}
}
#[test]
fn on_balance_volume_accumulates_by_direction() {
let result = on_balance_volume(&[10.0, 11.0, 10.0, 12.0], &[100.0, 200.0, 300.0, 400.0]);
assert!(close(result[0], 100.0), "seeded with the first bar's volume");
assert!(close(result[1], 300.0), "an up bar adds");
assert!(close(result[2], 0.0), "a down bar subtracts");
assert!(close(result[3], 400.0), "and it keeps accumulating");
}
#[test]
fn on_balance_volume_ignores_unchanged_bars() {
let result = on_balance_volume(&[10.0, 10.0, 10.0], &[50.0, 50.0, 50.0]);
assert!(result.iter().all(|value| close(*value, 50.0)), "nothing moved, nothing changed");
}
#[test]
fn on_balance_volume_reports_a_gap_rather_than_assuming_no_change() {
let result = on_balance_volume(&[10.0, f64::NAN, 12.0], &[100.0, 200.0, 300.0]);
assert!(close(result[0], 100.0), "the first bar seeds the total");
assert!(result[1].is_nan(), "the missing bar has no value");
assert!(result[2].is_nan(), "and neither has the bar whose direction needs it");
}
#[test]
fn on_balance_volume_resumes_after_a_gap() {
let closes = [10.0, f64::NAN, 12.0, 13.0];
let volumes = [100.0, 200.0, 300.0, 400.0];
let result = on_balance_volume(&closes, &volumes);
assert!(result[1].is_nan() && result[2].is_nan(), "the gap and its shadow");
assert!(close(result[3], 500.0), "12 -> 13 is an up bar, so 100 + 400");
}
#[test]
fn the_extent_ignores_gaps() {
let (low, high) = series_extent(&[1.0, f64::NAN, 5.0, f64::INFINITY]);
assert!(close(low, 1.0));
assert!(close(high, 5.0), "infinity is a data error, not a level");
}
#[test]
fn an_all_gap_series_has_no_extent() {
let (low, high) = series_extent(&[f64::NAN, f64::INFINITY]);
assert!(low.is_nan() && high.is_nan());
assert!(!has_drawable_values(&[f64::NAN]));
assert!(has_drawable_values(&[f64::NAN, 1.0]));
}
#[test]
fn align_left_pads_at_the_start() {
let aligned = align_left(&[1.0, 2.0, 3.0], 5);
assert_eq!(aligned.len(), 5);
assert!(aligned[0].is_nan() && aligned[1].is_nan(), "the gap is leading");
assert!(close(aligned[2], 1.0) && close(aligned[4], 3.0));
}
#[test]
fn align_left_truncates_a_longer_series() {
let aligned = align_left(&[1.0, 2.0, 3.0, 4.0], 2);
assert_eq!(aligned, vec![1.0, 2.0], "the leading part that fits");
}
#[test]
fn align_left_is_the_identity_at_the_right_length() {
let values = vec![1.0, 2.0, 3.0];
assert_eq!(align_left(&values, 3), values);
}
#[test]
fn every_indicator_preserves_the_input_length() {
let values: Vec<f64> = (0..40).map(|index| 10.0 + index as f64).collect();
let volumes = vec![1.0; 40];
assert_eq!(sma(&values, 5).len(), 40);
assert_eq!(ema(&values, 5).len(), 40);
assert_eq!(wilder_smooth(&values, 5).len(), 40);
assert_eq!(rsi(&values, 5).len(), 40);
let (line, signal, histogram) = macd(&values, 3, 6, 2);
assert!(line.len() == 40 && signal.len() == 40 && histogram.len() == 40);
let (lower, middle, upper) = bollinger_bands(&values, 5, 2.0);
assert!(lower.len() == 40 && middle.len() == 40 && upper.len() == 40);
let (k, d) = stochastic(&values, &values, &values, 5, 3, 3);
assert!(k.len() == 40 && d.len() == 40);
assert_eq!(atr(&values, &values, &values, 5).len(), 40);
let (upper, lower) = trailing_extremes(&values, &values, 5);
assert!(upper.len() == 40 && lower.len() == 40);
let (lower, middle, upper) = donchian_channel(&values, &values, 5);
assert!(lower.len() == 40 && middle.len() == 40 && upper.len() == 40);
assert_eq!(vwap(&values, &values, &values, &volumes, 5).len(), 40);
assert_eq!(money_flow_index(&values, &values, &values, &volumes, 5).len(), 40);
assert_eq!(on_balance_volume(&values, &volumes).len(), 40);
}
}