use crate::stocks::ta::ring::RingF64;
use crate::util::error::{require_finite, FinanceError, FinanceResult};
use crate::util::primitives::PeriodLength;
#[derive(Clone, Debug)]
pub struct SmaState {
period: usize,
ring: RingF64,
}
impl SmaState {
pub fn new(period: usize) -> FinanceResult<Self> {
let period = PeriodLength::new(period)?.get();
Ok(Self {
period,
ring: RingF64::with_capacity(period),
})
}
pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
let mut s = Self::new(period)?;
s.push_bars(closes)?;
Ok(s)
}
pub fn period(&self) -> usize {
self.period
}
pub fn reset(&mut self) {
self.ring.clear();
}
pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
require_finite("close", close)?;
self.ring.push(close);
if self.ring.is_full() {
Ok(Some(self.ring.sum() / self.period as f64))
} else {
Ok(None)
}
}
pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
let mut out = Vec::with_capacity(closes.len());
for &c in closes {
out.push(self.push(c)?);
}
Ok(out)
}
pub fn last(&self) -> Option<f64> {
if self.ring.is_full() {
Some(self.ring.sum() / self.period as f64)
} else {
None
}
}
}
#[derive(Clone, Debug)]
pub struct EmaState {
period: usize,
alpha: f64,
seed: RingF64,
value: Option<f64>,
}
impl EmaState {
pub fn new(period: usize) -> FinanceResult<Self> {
let period = PeriodLength::new(period)?.get();
Ok(Self {
period,
alpha: 2.0 / (period as f64 + 1.0),
seed: RingF64::with_capacity(period),
value: None,
})
}
pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
let mut s = Self::new(period)?;
s.push_bars(closes)?;
Ok(s)
}
pub fn period(&self) -> usize {
self.period
}
pub fn reset(&mut self) {
self.seed.clear();
self.value = None;
}
pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
require_finite("close", close)?;
if let Some(prev) = self.value {
let next = self.alpha * close + (1.0 - self.alpha) * prev;
self.value = Some(next);
return Ok(Some(next));
}
self.seed.push(close);
if self.seed.is_full() {
let seed = self.seed.sum() / self.period as f64;
self.value = Some(seed);
Ok(Some(seed))
} else {
Ok(None)
}
}
pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
let mut out = Vec::with_capacity(closes.len());
for &c in closes {
out.push(self.push(c)?);
}
Ok(out)
}
pub fn last(&self) -> Option<f64> {
self.value
}
}
pub fn sma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
validate_closes(closes)?;
let mut st = SmaState::new(period)?;
st.push_bars(closes)
}
pub fn ema(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
validate_closes(closes)?;
let mut st = EmaState::new(period)?;
st.push_bars(closes)
}
#[inline]
pub fn sma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
validate_closes(closes)?;
let mut st = SmaState::new(period)?;
for &c in closes {
st.push(c)?;
}
Ok(st.last())
}
#[inline]
pub fn ema_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
validate_closes(closes)?;
let mut st = EmaState::new(period)?;
for &c in closes {
st.push(c)?;
}
Ok(st.last())
}
#[derive(Clone, Debug)]
pub struct WmaState {
period: usize,
ring: RingF64,
weight_sum: f64,
ordered: Vec<f64>,
}
impl WmaState {
pub fn new(period: usize) -> FinanceResult<Self> {
let period = PeriodLength::new(period)?.get();
let weight_sum = (period * (period + 1)) as f64 / 2.0;
Ok(Self {
period,
ring: RingF64::with_capacity(period),
weight_sum,
ordered: Vec::with_capacity(period),
})
}
pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
let mut s = Self::new(period)?;
s.push_bars(closes)?;
Ok(s)
}
pub fn period(&self) -> usize {
self.period
}
pub fn reset(&mut self) {
self.ring.clear();
self.ordered.clear();
}
pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
require_finite("close", close)?;
self.ring.push(close);
if !self.ring.is_full() {
return Ok(None);
}
self.ring.copy_ordered(&mut self.ordered);
let mut num = 0.0;
for (i, &p) in self.ordered.iter().enumerate() {
num += (i + 1) as f64 * p;
}
Ok(Some(num / self.weight_sum))
}
pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
let mut out = Vec::with_capacity(closes.len());
for &c in closes {
out.push(self.push(c)?);
}
Ok(out)
}
pub fn last(&self) -> Option<f64> {
if !self.ring.is_full() {
return None;
}
let mut ordered = Vec::with_capacity(self.period);
self.ring.copy_ordered(&mut ordered);
let mut num = 0.0;
for (i, &p) in ordered.iter().enumerate() {
num += (i + 1) as f64 * p;
}
Some(num / self.weight_sum)
}
}
pub fn wma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
validate_closes(closes)?;
let mut st = WmaState::new(period)?;
st.push_bars(closes)
}
pub fn wma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
validate_closes(closes)?;
let mut st = WmaState::new(period)?;
for &c in closes {
st.push(c)?;
}
Ok(st.last())
}
#[derive(Clone, Debug)]
pub struct HmaState {
period: usize,
half: WmaState,
full: WmaState,
sqrt_wma: WmaState,
last: Option<f64>,
}
impl HmaState {
pub fn new(period: usize) -> FinanceResult<Self> {
let period = PeriodLength::new(period)?.get();
if period < 2 {
return Err(FinanceError::Unsolvable {
message: "HMA period must be >= 2",
});
}
let half_n = (period / 2).max(1);
let sqrt_n = ((period as f64).sqrt().floor() as usize).max(1);
Ok(Self {
period,
half: WmaState::new(half_n)?,
full: WmaState::new(period)?,
sqrt_wma: WmaState::new(sqrt_n)?,
last: None,
})
}
pub fn from_history(period: usize, closes: &[f64]) -> FinanceResult<Self> {
let mut s = Self::new(period)?;
s.push_bars(closes)?;
Ok(s)
}
pub fn period(&self) -> usize {
self.period
}
pub fn reset(&mut self) {
self.half.reset();
self.full.reset();
self.sqrt_wma.reset();
self.last = None;
}
pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
let wh = self.half.push(close)?;
let wf = self.full.push(close)?;
let out = match (wh, wf) {
(Some(h), Some(f)) => {
let raw = 2.0 * h - f;
self.sqrt_wma.push(raw)?
}
_ => None,
};
self.last = out;
Ok(out)
}
pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
let mut out = Vec::with_capacity(closes.len());
for &c in closes {
out.push(self.push(c)?);
}
Ok(out)
}
pub fn last(&self) -> Option<f64> {
self.last.or_else(|| self.sqrt_wma.last())
}
}
pub fn hma(closes: &[f64], period: usize) -> FinanceResult<Vec<Option<f64>>> {
validate_closes(closes)?;
let mut st = HmaState::new(period)?;
st.push_bars(closes)
}
pub fn hma_last(closes: &[f64], period: usize) -> FinanceResult<Option<f64>> {
validate_closes(closes)?;
let mut st = HmaState::new(period)?;
for &c in closes {
st.push(c)?;
}
Ok(st.last())
}
fn validate_closes(closes: &[f64]) -> FinanceResult<()> {
if closes.is_empty() {
return Err(FinanceError::EmptyInput { what: "closes" });
}
for &c in closes {
require_finite("close", c)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sma_constant() {
let c = [10.0; 5];
let s = sma(&c, 3).unwrap();
assert_eq!(s[2], Some(10.0));
assert_eq!(s[4], Some(10.0));
}
#[test]
fn ema_runs() {
let c: Vec<_> = (1..=30).map(|x| x as f64).collect();
let e = ema(&c, 10).unwrap();
assert!(e[8].is_none());
assert!(e[9].is_some());
}
#[test]
fn rejects_zero_period() {
assert!(sma(&[1.0, 2.0], 0).is_err());
}
#[test]
fn sma_period_one_is_identity() {
let c = [1.0, 2.0, 3.0];
let s = sma(&c, 1).unwrap();
assert_eq!(s[0], Some(1.0));
assert_eq!(s[2], Some(3.0));
}
#[test]
fn ema_seed_is_sma() {
let c = [1.0, 2.0, 3.0, 4.0, 5.0];
let e = ema(&c, 3).unwrap();
assert!((e[2].unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn empty_series_err() {
assert!(sma(&[], 3).is_err());
assert!(ema(&[], 3).is_err());
}
#[test]
fn nan_close_err() {
assert!(sma(&[1.0, f64::NAN], 2).is_err());
}
#[test]
fn last_matches_series_tail() {
let c: Vec<_> = (1..=25).map(|x| x as f64 * 0.5).collect();
let s = sma(&c, 7).unwrap();
assert_eq!(sma_last(&c, 7).unwrap(), s[24]);
let e = ema(&c, 7).unwrap();
assert_eq!(ema_last(&c, 7).unwrap(), e[24]);
}
#[test]
fn wma_weights_newest_heavier() {
let s = wma(&[1.0, 2.0, 3.0], 3).unwrap();
assert!((s[2].unwrap() - 14.0 / 6.0).abs() < 1e-12);
}
#[test]
fn hma_state_parity() {
let c: Vec<f64> = (1..=50).map(|x| 100.0 + x as f64 * 0.1).collect();
let batch = hma(&c, 16).unwrap();
let st = HmaState::from_history(16, &c).unwrap();
assert!((batch.last().unwrap().unwrap() - st.last().unwrap()).abs() < 1e-9);
}
#[test]
fn hma_rejects_period_one() {
assert!(HmaState::new(1).is_err());
}
#[test]
fn hma_tracks_rising_path() {
let c: Vec<f64> = (1..=60).map(|x| x as f64).collect();
let h = hma(&c, 9).unwrap();
let last = h.iter().rev().find_map(|x| *x).unwrap();
assert!(last > 50.0, "hma last={last}");
}
#[test]
fn wma_last_matches_series() {
let c: Vec<f64> = (1..=20).map(|x| x as f64).collect();
let s = wma(&c, 5).unwrap();
assert_eq!(wma_last(&c, 5).unwrap(), s[19]);
}
#[test]
fn hma_reset_clears() {
let c: Vec<f64> = (1..=30).map(|x| x as f64).collect();
let mut st = HmaState::from_history(9, &c).unwrap();
assert!(st.last().is_some());
st.reset();
assert!(st.last().is_none());
}
}