use ndarray::Array1;
use super::av;
use super::stochastic::stoch_fastk;
use crate::kernels;
pub fn llv(data: &[f64], period: usize) -> Vec<f64> {
kernels::rolling_min(av(data), period)
.into_raw_vec_and_offset()
.0
}
pub fn hhv(data: &[f64], period: usize) -> Vec<f64> {
if period == 10 && period <= data.len() && !data.iter().any(|x| x.is_nan()) {
return hhv10_no_nan(data);
}
kernels::rolling_max(av(data), period)
.into_raw_vec_and_offset()
.0
}
fn hhv10_no_nan(data: &[f64]) -> Vec<f64> {
let n = data.len();
let mut out = vec![f64::NAN; n];
let src = data.as_ptr();
let dst = out.as_mut_ptr();
let mut highest_idx = 0usize;
let mut highest = unsafe { *src };
for i in 1..10 {
let value = unsafe { *src.add(i) };
if value > highest {
highest_idx = i;
highest = value;
}
}
unsafe {
*dst.add(9) = highest;
}
for today in 10..n {
let trailing = today - 9;
let x = unsafe { *src.add(today) };
if highest_idx < trailing {
highest_idx = trailing;
highest = unsafe { *src.add(trailing) };
let mut idx = trailing + 1;
while idx <= today {
let value = unsafe { *src.add(idx) };
if value > highest {
highest_idx = idx;
highest = value;
}
idx += 1;
}
} else if x >= highest {
highest_idx = today;
highest = x;
}
unsafe {
*dst.add(today) = highest;
}
}
out
}
pub fn rsv(high: &[f64], low: &[f64], close: &[f64], period: usize) -> Vec<f64> {
let llv = kernels::rolling_min(av(low), period);
let hhv = kernels::rolling_max(av(high), period);
let n = close.len();
let mut result = vec![f64::NAN; n];
for i in 0..n {
let denom = hhv[i] - llv[i];
if denom.abs() > 1e-10 {
result[i] = (close[i] - llv[i]) / denom * 100.0;
} else {
result[i] = 0.0;
}
}
result
}
fn stochrsi_ctx_depth(fastk_period: usize, is_d: bool, fastd_period: usize) -> usize {
let k = fastk_period.saturating_sub(1);
if is_d {
k + fastd_period.saturating_sub(1)
} else {
k
}
}
pub fn stochrsi_final_state(
close: &[f64],
rsi_period: usize,
fastk_period: usize,
is_d: bool,
fastd_period: usize,
) -> Option<Vec<f64>> {
let n = close.len();
let wilder = rsi_final_state(close, rsi_period)?; let c = stochrsi_ctx_depth(fastk_period, is_d, fastd_period);
if n < c || n - c < rsi_period {
return None;
}
let rsi = rsi(close, rsi_period);
let mut state = wilder;
state.extend_from_slice(&rsi[n - c..n]);
Some(state)
}
pub fn stochrsi_resume(
close: &[f64],
rsi_period: usize,
fastk_period: usize,
is_d: bool,
fastd_period: usize,
from: usize,
state: &[f64],
) -> Option<(Vec<f64>, Vec<f64>)> {
let n = close.len();
let c = stochrsi_ctx_depth(fastk_period, is_d, fastd_period);
if state.len() != c + 2 || from == 0 || from > n || from < c {
return None;
}
let (new_rsi, new_wilder) = rsi_resume(close, rsi_period, from, &state[..2])?;
let mut buf = Vec::with_capacity(c + new_rsi.len());
buf.extend_from_slice(&state[2..]); buf.extend_from_slice(&new_rsi); if buf.iter().any(|x| x.is_nan()) {
return None;
}
let fk = stoch_fastk(&buf, &buf, &buf, fastk_period);
let line = if is_d {
super::ma(&fk, fastd_period)
} else {
fk
};
let out: Vec<f64> = line[c..].to_vec();
debug_assert_eq!(out.len(), n - from);
let mut new_state = new_wilder;
let full_rsi_len = c + new_rsi.len(); new_state.extend_from_slice(&buf[full_rsi_len - c..]);
Some((out, new_state))
}
fn wilder_gain_loss(data: &[f64], period: usize) -> (Array1<f64>, Array1<f64>) {
let n = data.len();
let delta = kernels::diff(av(data));
let mut gains = Array1::from_elem(n, f64::NAN);
let mut losses = Array1::from_elem(n, f64::NAN);
for i in 1..n {
let d = delta[i];
if d.is_nan() {
continue;
}
gains[i] = d.max(0.0);
losses[i] = (-d).max(0.0);
}
(
kernels::wilder(gains.view(), period),
kernels::wilder(losses.view(), period),
)
}
pub fn rsi(close: &[f64], period: usize) -> Vec<f64> {
let n = close.len();
let mut out = vec![f64::NAN; n];
if period == 0 || n <= period {
return out;
}
let pf = period as f64;
let p1 = pf - 1.0;
let emit = |g: f64, l: f64| {
if l.abs() < 1e-10 {
100.0
} else {
100.0 - 100.0 / (1.0 + g / l)
}
};
let mut avg_gain = 0.0;
let mut avg_loss = 0.0;
for i in 1..=period {
let d = close[i] - close[i - 1];
if d > 0.0 {
avg_gain += d;
} else {
avg_loss -= d;
}
}
avg_gain /= pf;
avg_loss /= pf;
out[period] = emit(avg_gain, avg_loss);
for i in (period + 1)..n {
let d = close[i] - close[i - 1];
let (gain, loss) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
avg_gain = (avg_gain * p1 + gain) / pf;
avg_loss = (avg_loss * p1 + loss) / pf;
out[i] = emit(avg_gain, avg_loss);
}
out
}
pub fn cmo(close: &[f64], period: usize) -> Vec<f64> {
let (sg, sl) = wilder_gain_loss(close, period);
let n = close.len();
let mut result = vec![f64::NAN; n];
for i in 0..n {
if sg[i].is_nan() || sl[i].is_nan() {
continue;
}
let denom = sg[i] + sl[i];
result[i] = if denom < 1e-14 {
0.0
} else {
100.0 * (sg[i] - sl[i]) / denom
};
}
result
}
pub fn rsi_final_state(close: &[f64], period: usize) -> Option<Vec<f64>> {
let n = close.len();
if period == 0 || n <= period {
return None;
}
let pf = period as f64;
let p1 = pf - 1.0;
let (mut avg_gain, mut avg_loss) = (0.0, 0.0);
for i in 1..=period {
let d = close[i] - close[i - 1];
if d > 0.0 {
avg_gain += d;
} else {
avg_loss -= d;
}
}
avg_gain /= pf;
avg_loss /= pf;
for i in (period + 1)..n {
let d = close[i] - close[i - 1];
let (gain, loss) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
avg_gain = (avg_gain * p1 + gain) / pf;
avg_loss = (avg_loss * p1 + loss) / pf;
}
Some(vec![avg_gain, avg_loss])
}
pub fn rsi_resume(
close: &[f64],
period: usize,
from: usize,
state: &[f64],
) -> Option<(Vec<f64>, Vec<f64>)> {
if from == 0 {
return None;
}
let n = close.len();
let pf = period as f64;
let p1 = pf - 1.0;
let emit = |g: f64, l: f64| {
if l.abs() < 1e-10 {
100.0
} else {
100.0 - 100.0 / (1.0 + g / l)
}
};
let (mut avg_gain, mut avg_loss) = (state[0], state[1]);
let mut out = Vec::with_capacity(n.saturating_sub(from));
for i in from..n {
let d = close[i] - close[i - 1];
let (gain, loss) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
avg_gain = (avg_gain * p1 + gain) / pf;
avg_loss = (avg_loss * p1 + loss) / pf;
out.push(emit(avg_gain, avg_loss));
}
Some((out, vec![avg_gain, avg_loss]))
}
pub fn cmo_final_state(close: &[f64], period: usize) -> Option<Vec<f64>> {
let n = close.len();
if period == 0 || n <= period {
return None;
}
let pf = period as f64;
let (a, b) = ((pf - 1.0) / pf, 1.0 / pf);
let (mut avg_gain, mut avg_loss) = (0.0, 0.0);
for i in 1..=period {
let d = close[i] - close[i - 1];
avg_gain += d.max(0.0);
avg_loss += (-d).max(0.0);
}
avg_gain /= pf;
avg_loss /= pf;
for i in (period + 1)..n {
let d = close[i] - close[i - 1];
avg_gain = avg_gain.mul_add(a, d.max(0.0) * b);
avg_loss = avg_loss.mul_add(a, (-d).max(0.0) * b);
}
Some(vec![avg_gain, avg_loss])
}
pub fn cmo_resume(
close: &[f64],
period: usize,
from: usize,
state: &[f64],
) -> Option<(Vec<f64>, Vec<f64>)> {
if from == 0 {
return None;
}
let n = close.len();
let pf = period as f64;
let (a, b) = ((pf - 1.0) / pf, 1.0 / pf);
let (mut avg_gain, mut avg_loss) = (state[0], state[1]);
let mut out = Vec::with_capacity(n.saturating_sub(from));
for i in from..n {
let d = close[i] - close[i - 1];
avg_gain = avg_gain.mul_add(a, d.max(0.0) * b);
avg_loss = avg_loss.mul_add(a, (-d).max(0.0) * b);
let denom = avg_gain + avg_loss;
out.push(if denom < 1e-14 {
0.0
} else {
100.0 * (avg_gain - avg_loss) / denom
});
}
Some((out, vec![avg_gain, avg_loss]))
}
pub fn rsi_resume_one(close: &[f64], period: usize, row: usize, state: &[f64]) -> Option<f64> {
if row == 0 || state.len() < 2 || row >= close.len() {
return None;
}
let pf = period as f64;
let p1 = pf - 1.0;
let d = close[row] - close[row - 1];
let (gain, loss) = if d > 0.0 { (d, 0.0) } else { (0.0, -d) };
let avg_gain = (state[0] * p1 + gain) / pf;
let avg_loss = (state[1] * p1 + loss) / pf;
Some(if avg_loss.abs() < 1e-10 {
100.0
} else {
100.0 - 100.0 / (1.0 + avg_gain / avg_loss)
})
}
pub fn cmo_resume_one(close: &[f64], period: usize, row: usize, state: &[f64]) -> Option<f64> {
if row == 0 || state.len() < 2 || row >= close.len() {
return None;
}
let pf = period as f64;
let (a, b) = ((pf - 1.0) / pf, 1.0 / pf);
let d = close[row] - close[row - 1];
let avg_gain = state[0].mul_add(a, d.max(0.0) * b);
let avg_loss = state[1].mul_add(a, (-d).max(0.0) * b);
let denom = avg_gain + avg_loss;
Some(if denom < 1e-14 {
0.0
} else {
100.0 * (avg_gain - avg_loss) / denom
})
}
pub fn donchian(high: &[f64], low: &[f64], period: usize) -> Vec<f64> {
let hhv = kernels::rolling_max(av(high), period);
let llv = kernels::rolling_min(av(low), period);
((&hhv + &llv) / 2.0).to_vec()
}
pub fn midpoint(data: &[f64], period: usize) -> Vec<f64> {
let hh = kernels::rolling_max(av(data), period);
let ll = kernels::rolling_min(av(data), period);
((&hh + &ll) / 2.0).to_vec()
}
pub fn midprice(high: &[f64], low: &[f64], period: usize) -> Vec<f64> {
let hh = kernels::rolling_max(av(high), period);
let ll = kernels::rolling_min(av(low), period);
((&hh + &ll) / 2.0).to_vec()
}
pub fn cog(close: &[f64], period: usize) -> Vec<f64> {
let n = close.len();
let mut out = vec![f64::NAN; n];
if period == 0 || period > n {
return out;
}
if close.iter().any(|x| x.is_nan()) {
#[allow(clippy::needless_range_loop)]
for i in (period - 1)..n {
out[i] = cog_window_exact(close, i, period);
}
return out;
}
let pf = period as f64;
let mut num = 0.0;
let mut den = 0.0;
let mut aden = 0.0;
for age in 0..period {
let price = close[period - 1 - age]; num += (1 + age) as f64 * price;
den += price;
aden += price.abs();
}
out[period - 1] = if den.abs() <= 1e-9 * aden {
cog_window_exact(close, period - 1, period)
} else {
-num / den
};
for i in period..n {
let leaving = close[i - period];
num = num + den - (pf + 1.0) * leaving + close[i];
den = den - leaving + close[i];
aden = aden - leaving.abs() + close[i].abs();
out[i] = if den.abs() <= 1e-9 * aden {
cog_window_exact(close, i, period)
} else {
-num / den
};
}
out
}
#[inline]
fn cog_window_exact(close: &[f64], i: usize, period: usize) -> f64 {
let mut num = 0.0; let mut den = 0.0;
for age in 0..period {
let price = close[i - age];
num += (1 + age) as f64 * price;
den += price;
}
if den != 0.0 {
-num / den
} else {
f64::NAN
}
}
pub fn rci(close: &[f64], period: usize) -> Vec<f64> {
let n = close.len();
let mut out = vec![f64::NAN; n];
if period < 2 || period > n {
return out;
}
let p = period as f64;
let t_mean = (p + 1.0) / 2.0;
let t_ss: f64 = (1..=period).map(|t| (t as f64 - t_mean).powi(2)).sum();
let mut idx: Vec<usize> = Vec::with_capacity(period);
let mut prank = vec![0.0f64; period];
for i in (period - 1)..n {
let w = &close[i + 1 - period..=i];
if w.iter().any(|x| x.is_nan()) {
continue;
}
idx.clear();
idx.extend(0..period);
idx.sort_by(|&a, &b| w[a].partial_cmp(&w[b]).unwrap_or(std::cmp::Ordering::Equal));
let mut k = 0;
while k < period {
let mut j = k + 1;
while j < period && w[idx[j]] == w[idx[k]] {
j += 1;
}
let avg = ((k + 1 + j) as f64) / 2.0;
for &pos in &idx[k..j] {
prank[pos] = avg;
}
k = j;
}
let mut cov = 0.0;
let mut p_ss = 0.0;
for (t, &pr) in prank.iter().enumerate() {
let dp = pr - t_mean;
let dt = (t + 1) as f64 - t_mean;
cov += dp * dt;
p_ss += dp * dp;
}
let denom = (p_ss * t_ss).sqrt();
out[i] = if denom > 0.0 { cov / denom * 100.0 } else { f64::NAN };
}
out
}
pub fn pivot(source: &[f64], left: usize, right: usize, high: bool) -> Vec<f64> {
let n = source.len();
let mut out = vec![f64::NAN; n];
let win = left + right;
#[allow(clippy::needless_range_loop)] for i in win..n {
let p = i - right; let cand = source[p];
if cand.is_nan() {
continue;
}
let start = i - win; let is_pivot = (start..=i).all(|j| {
j == p || if high { source[j] < cand } else { source[j] > cand }
});
if is_pivot {
out[i] = cand;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::stochrsi_fastk;
use crate::indicators::test_support::*;
#[test]
fn rci_nan_in_window_disqualifies() {
let out = rci(&[1.0, f64::NAN, 3.0, 4.0, 5.0], 3);
assert!(out[2].is_nan(), "window [1, NaN, 3] must be NaN, got {}", out[2]);
assert!(out[3].is_nan(), "window [NaN, 3, 4] must be NaN, got {}", out[3]);
assert!(out[4].is_finite(), "window [3, 4, 5] is clean -> finite, got {}", out[4]);
}
#[test]
fn stochrsi_resume_is_bit_identical_to_full() {
let close = series(200);
let (rp, fk, fd) = (14usize, 14usize, 3usize);
let k_full = stochrsi_fastk(&close, rp, fk);
let d_full = crate::indicators::ma(&k_full, fd);
for &from in &[60usize, 70, 120, 199] {
let head = &close[..from];
let st = stochrsi_final_state(head, rp, fk, false, fd).unwrap();
let (tail, _) = stochrsi_resume(&close, rp, fk, false, fd, from, &st).unwrap();
assert_bits(&tail, &k_full[from..], "stochrsi.k");
let st = stochrsi_final_state(head, rp, fk, true, fd).unwrap();
let (tail, _) = stochrsi_resume(&close, rp, fk, true, fd, from, &st).unwrap();
let want = &d_full[from..];
assert_eq!(tail.len(), want.len(), "stochrsi.d length");
for (i, (x, y)) in tail.iter().zip(want).enumerate() {
assert!(
(x - y).abs() <= 1e-9 || (x.is_nan() && y.is_nan()),
"stochrsi.d bar {i}: resume {x} != full {y}",
);
}
}
}
#[test]
fn stochrsi_guards_decline() {
let (rp, fk, fd) = (14usize, 14usize, 3usize);
let short = series(20);
assert!(stochrsi_final_state(&short, rp, fk, false, fd).is_none());
let close = series(200);
let st = stochrsi_final_state(&close[..120], rp, fk, false, fd).unwrap();
let bad = vec![0.0; 1]; assert!(stochrsi_resume(&close, rp, fk, false, fd, 120, &bad).is_none());
assert!(stochrsi_resume(&close, rp, fk, false, fd, 0, &st).is_none());
let mut nanclose = series(200);
nanclose[150] = f64::NAN; let st = stochrsi_final_state(&nanclose[..140], rp, fk, false, fd).unwrap();
assert!(stochrsi_resume(&nanclose, rp, fk, false, fd, 140, &st).is_none());
}
#[test]
fn rsi_cmo_resume_and_flat_window_arms() {
let close = series(120);
let p = 14usize;
let rsi_full = rsi(&close, p);
let cmo_full = cmo(&close, p);
for &from in &[p + 1, 30, 60, 119] {
let st = rsi_final_state(&close[..from], p).unwrap();
let (tail, _) = rsi_resume(&close, p, from, &st).unwrap();
assert_bits(&tail, &rsi_full[from..], "rsi");
let st = cmo_final_state(&close[..from], p).unwrap();
let (tail, _) = cmo_resume(&close, p, from, &st).unwrap();
assert_bits(&tail, &cmo_full[from..], "cmo");
}
let st = rsi_final_state(&close, p).unwrap();
assert!(rsi_resume(&close, p, 0, &st).is_none());
let st = cmo_final_state(&close, p).unwrap();
assert!(cmo_resume(&close, p, 0, &st).is_none());
assert!(rsi_final_state(&[1.0, 2.0], 5).is_none());
assert!(cmo_final_state(&[1.0, 2.0], 5).is_none());
let up: Vec<f64> = (0..40).map(|i| i as f64).collect();
let st = rsi_final_state(&up[..20], p).unwrap();
let (tail, _) = rsi_resume(&up, p, 20, &st).unwrap();
assert!(tail.iter().all(|&x| x == 100.0), "rsi flat-loss -> 100");
let flat = vec![7.0; 40];
let st = cmo_final_state(&flat[..20], p).unwrap();
let (tail, _) = cmo_resume(&flat, p, 20, &st).unwrap();
assert!(tail.iter().all(|&x| x == 0.0), "cmo flat-window -> 0");
}
}