use crate::math::MulAdd;
#[cfg(feature = "alloc")]
macro_rules! impl_kama {
($name:ident, $builder:ident, $ty:ty) => {
pub struct $name {
ring: *mut $ty,
window: usize,
head: usize,
value: $ty,
fast_sc: $ty,
slow_sc: $ty,
volatility_sum: $ty,
count: u64,
min_samples: u64,
}
unsafe impl Send for $name {}
impl $name {
#[inline]
fn ring(&self) -> &[$ty] {
unsafe { core::slice::from_raw_parts(self.ring, self.window) }
}
#[inline]
fn ring_mut(&mut self) -> &mut [$ty] {
unsafe { core::slice::from_raw_parts_mut(self.ring, self.window) }
}
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
window: Option<usize>,
fast_span: u64,
slow_span: u64,
min_samples: Option<u64>,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
window: Option::None,
fast_span: 2,
slow_span: 30,
min_samples: Option::None,
}
}
#[inline]
#[must_use]
pub fn update(&mut self, sample: $ty) -> Option<$ty> {
let n = self.window;
let idx = (self.count as usize) % n;
unsafe { *self.ring.add(idx) = sample; }
self.count += 1;
if self.count == 1 {
self.value = sample;
return if self.count >= self.min_samples { Option::Some(self.value) } else { Option::None };
}
if self.count <= n as u64 {
self.value = sample;
return if self.count >= self.min_samples { Option::Some(self.value) } else { Option::None };
}
let ring = unsafe { core::slice::from_raw_parts(self.ring, n) };
let oldest = (idx + 1) % n;
let mut volatility = 0.0 as $ty;
let s1 = &ring[oldest..];
for w in s1.windows(2) {
volatility += (w[1] - w[0]).abs();
}
if oldest > 0 && !s1.is_empty() {
volatility += (ring[0] - s1[s1.len() - 1]).abs();
}
let s2 = &ring[..oldest];
for w in s2.windows(2) {
volatility += (w[1] - w[0]).abs();
}
let direction = (sample - ring[oldest]).abs();
self.volatility_sum = volatility;
let er = if volatility > 0.0 as $ty {
direction / volatility
} else {
0.0 as $ty
};
let sc = er * (self.fast_sc - self.slow_sc) + self.slow_sc;
let alpha = sc * sc;
self.value = alpha.fma(sample - self.value, self.value);
if self.count >= self.min_samples {
Option::Some(self.value)
} else {
Option::None
}
}
#[inline]
#[must_use]
pub fn value(&self) -> Option<$ty> {
if self.count >= self.min_samples { Option::Some(self.value) } else { Option::None }
}
#[inline]
#[must_use]
pub fn efficiency_ratio(&self) -> Option<$ty> {
let n = self.window;
if self.count <= n as u64 {
return Option::None;
}
let newest_idx = ((self.count - 1) as usize) % n;
let oldest_idx = (self.count as usize) % n;
let ring = self.ring();
let direction = (ring[newest_idx] - ring[oldest_idx]).abs();
if self.volatility_sum > 0.0 as $ty {
Option::Some(direction / self.volatility_sum)
} else {
Option::Some(0.0 as $ty)
}
}
#[inline]
#[must_use]
pub fn window_size(&self) -> usize { self.window }
#[inline]
#[must_use]
pub fn count(&self) -> u64 { self.count }
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool { self.count >= self.min_samples }
#[inline]
pub fn reset(&mut self) {
self.ring_mut().fill(0.0 as $ty);
self.head = 0;
self.value = 0.0 as $ty;
self.volatility_sum = 0.0 as $ty;
self.count = 0;
}
}
impl $builder {
#[inline]
#[must_use]
pub fn window_size(mut self, n: usize) -> Self {
self.window = Option::Some(n);
self
}
#[inline]
#[must_use]
pub fn fast_span(mut self, n: u64) -> Self {
self.fast_span = n;
self
}
#[inline]
#[must_use]
pub fn slow_span(mut self, n: u64) -> Self {
self.slow_span = n;
self
}
#[inline]
#[must_use]
pub fn min_samples(mut self, min: u64) -> Self {
self.min_samples = Option::Some(min);
self
}
#[inline]
pub fn build(self) -> Result<$name, crate::ConfigError> {
let window = self.window.ok_or(crate::ConfigError::Missing("window_size"))?;
if window == 0 {
return Err(crate::ConfigError::Invalid("window_size must be > 0"));
}
if self.fast_span < 1 {
return Err(crate::ConfigError::Invalid("fast_span must be >= 1"));
}
if self.slow_span <= self.fast_span {
return Err(crate::ConfigError::Invalid("slow_span must be > fast_span"));
}
let min_samples = self.min_samples.unwrap_or(window as u64);
let mut vec = core::mem::ManuallyDrop::new(alloc::vec![0.0 as $ty; window]);
let ring = vec.as_mut_ptr();
Ok($name {
ring,
window,
head: 0,
value: 0.0 as $ty,
fast_sc: 2.0 as $ty / (self.fast_span as $ty + 1.0 as $ty),
slow_sc: 2.0 as $ty / (self.slow_span as $ty + 1.0 as $ty),
volatility_sum: 0.0 as $ty,
count: 0,
min_samples,
})
}
}
impl Drop for $name {
fn drop(&mut self) {
unsafe {
let _ = alloc::vec::Vec::from_raw_parts(self.ring, 0, self.window);
}
}
}
impl Clone for $name {
fn clone(&self) -> Self {
let mut vec = alloc::vec![0.0 as $ty; self.window];
vec.copy_from_slice(self.ring());
let mut cloned = core::mem::ManuallyDrop::new(vec);
let ring = cloned.as_mut_ptr();
Self {
ring,
window: self.window,
head: self.head,
value: self.value,
fast_sc: self.fast_sc,
slow_sc: self.slow_sc,
volatility_sum: self.volatility_sum,
count: self.count,
min_samples: self.min_samples,
}
}
}
impl core::fmt::Debug for $name {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct(stringify!($name))
.field("window", &self.window)
.field("count", &self.count)
.field("value", &self.value)
.finish()
}
}
};
}
#[cfg(feature = "alloc")]
impl_kama!(KamaF64, KamaF64Builder, f64);
#[cfg(feature = "alloc")]
impl_kama!(KamaF32, KamaF32Builder, f32);
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
#[test]
fn trending_signal_fast_response() {
let mut kama = KamaF64::builder().window_size(10).build().unwrap();
for i in 0..50 {
let _ = kama.update(i as f64);
}
let er = kama.efficiency_ratio().unwrap();
assert!(er > 0.5, "trending signal should have high ER, got {er}");
}
#[test]
fn noisy_signal_slow_response() {
let mut kama = KamaF64::builder().window_size(10).build().unwrap();
for i in 0..50 {
let v = if i % 2 == 0 { 100.0 } else { 0.0 };
let _ = kama.update(v);
}
let er = kama.efficiency_ratio().unwrap();
assert!(er < 0.3, "noisy signal should have low ER, got {er}");
}
#[test]
fn er_bounds() {
let mut kama = KamaF64::builder().window_size(10).build().unwrap();
for i in 0..20 {
let _ = kama.update(i as f64);
}
let er = kama.efficiency_ratio().unwrap();
assert!(
(0.0..=1.0).contains(&er),
"ER should be in [0, 1], got {er}"
);
}
#[test]
fn priming() {
let mut kama = KamaF64::builder().window_size(10).build().unwrap();
for i in 0..9 {
assert!(kama.update(i as f64).is_none());
}
assert!(kama.update(9.0).is_some());
}
#[test]
fn reset() {
let mut kama = KamaF64::builder().window_size(10).build().unwrap();
for i in 0..20 {
let _ = kama.update(i as f64);
}
kama.reset();
assert_eq!(kama.count(), 0);
}
#[test]
fn f32_basic() {
let mut kama = KamaF32::builder().window_size(5).build().unwrap();
for i in 0..10 {
let _ = kama.update(i as f32);
}
assert!(kama.value().is_some());
}
#[test]
fn window_size_accessor() {
let kama = KamaF64::builder().window_size(10).build().unwrap();
assert_eq!(kama.window_size(), 10);
}
}