use crate::cross_section::CrossSection;
use crate::error::Result;
use crate::traits::Indicator;
use crate::Ema;
#[derive(Debug, Clone)]
pub struct BreadthThrust {
ema: Ema,
}
impl BreadthThrust {
pub fn new(period: usize) -> Result<Self> {
Ok(Self {
ema: Ema::new(period)?,
})
}
#[must_use]
pub const fn period(&self) -> usize {
self.ema.period()
}
}
impl Indicator for BreadthThrust {
type Input = CrossSection;
type Output = f64;
#[inline]
fn update(&mut self, section: CrossSection) -> Option<f64> {
let advancers = section.advancers();
let decliners = section.decliners();
let participating = (advancers + decliners).max(1) as f64;
let share = advancers as f64 / participating;
self.ema.update(share)
}
fn reset(&mut self) {
self.ema.reset();
}
#[inline]
fn warmup_period(&self) -> usize {
self.ema.period()
}
#[inline]
fn is_ready(&self) -> bool {
self.ema.value().is_some()
}
#[inline]
fn name(&self) -> &'static str {
"BreadthThrust"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cross_section::Member;
use crate::error::Error;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn section(up: usize, down: usize) -> CrossSection {
let mut members = Vec::new();
for _ in 0..up {
members.push(Member::new(1.0, 10.0, false, false));
}
for _ in 0..down {
members.push(Member::new(-1.0, 10.0, false, false));
}
members.push(Member::new(0.0, 10.0, false, false));
CrossSection::new(members, 0).unwrap()
}
#[test]
fn accessors_and_metadata() {
let bt = BreadthThrust::new(10).unwrap();
assert_eq!(bt.name(), "BreadthThrust");
assert_eq!(bt.warmup_period(), 10);
assert_eq!(bt.period(), 10);
assert!(!bt.is_ready());
}
#[test]
fn rejects_zero_period() {
assert!(matches!(BreadthThrust::new(0), Err(Error::PeriodZero)));
}
#[test]
fn smooths_the_advancing_share() {
let mut bt = BreadthThrust::new(2).unwrap();
assert_eq!(bt.update(section(8, 2)), None);
let value = bt.update(section(6, 4)).unwrap();
assert!((value - 0.7).abs() < 1e-9);
assert!(bt.is_ready());
let value = bt.update(section(5, 5)).unwrap();
assert!((value - (0.7 + 2.0 / 3.0 * (0.5 - 0.7))).abs() < 1e-9);
}
#[test]
fn empty_participation_floors_to_zero_share() {
let mut bt = BreadthThrust::new(1).unwrap();
assert_eq!(bt.update(section(0, 0)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut bt = BreadthThrust::new(2).unwrap();
bt.update(section(8, 2));
bt.update(section(6, 4));
assert!(bt.is_ready());
bt.reset();
assert!(!bt.is_ready());
assert_eq!(bt.update(section(8, 2)), None);
}
#[test]
fn batch_equals_streaming() {
let sections = vec![section(8, 2), section(6, 4), section(5, 5), section(0, 0)];
let mut a = BreadthThrust::new(2).unwrap();
let mut b = BreadthThrust::new(2).unwrap();
assert_eq!(
a.batch(§ions),
sections
.iter()
.map(|s| b.update(s.clone()))
.collect::<Vec<_>>()
);
}
fn mixed_sections() -> Vec<CrossSection> {
(0..30_usize)
.map(|i| section((i * 7) % 11, (i * 3) % 8))
.collect()
}
#[test]
fn rejects_oversized_period() {
let too_long = crate::error::MAX_PERIOD + 1;
assert!(matches!(
BreadthThrust::new(too_long),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn first_value_lands_exactly_at_warmup_index() {
let sections = mixed_sections();
let mut bt = BreadthThrust::new(10).unwrap();
let warmup = bt.warmup_period();
let out = bt.batch(§ions);
assert!(out.iter().take(warmup - 1).all(Option::is_none));
assert!(out.iter().skip(warmup - 1).all(Option::is_some));
}
#[test]
fn reset_replays_identically_to_fresh_instance() {
let sections = mixed_sections();
let mut used = BreadthThrust::new(10).unwrap();
used.batch(§ions);
used.reset();
let replay = used.batch(§ions);
assert_eq!(replay, BreadthThrust::new(10).unwrap().batch(§ions));
}
#[test]
fn batch_equals_streaming_bit_identical() {
let sections = mixed_sections();
let batch = BreadthThrust::new(10).unwrap().batch(§ions);
let mut streamer = BreadthThrust::new(10).unwrap();
let identical = sections
.iter()
.zip(&batch)
.all(|(s, b)| streamer.update(s.clone()).map(f64::to_bits) == b.map(f64::to_bits));
assert!(identical);
}
#[test]
fn reference_values_ema_ten() {
let mut bt = BreadthThrust::new(10).unwrap();
for _ in 0..5 {
assert_eq!(bt.update(section(3, 7)), None);
}
for _ in 0..4 {
assert_eq!(bt.update(section(4, 6)), None);
}
assert_relative_eq!(bt.update(section(4, 6)).unwrap(), 0.35, epsilon = 1e-12);
assert_relative_eq!(bt.update(section(9, 1)).unwrap(), 0.45, epsilon = 1e-12);
assert_relative_eq!(
bt.update(section(0, 0)).unwrap(),
0.45 * 9.0 / 11.0,
epsilon = 1e-12
);
}
#[test]
fn unanimous_breadth_pins_the_extremes() {
let mut up = BreadthThrust::new(3).unwrap();
let ups = up.batch(&[section(5, 0), section(7, 0), section(1, 0), section(2, 0)]);
assert_eq!(ups, vec![None, None, Some(1.0), Some(1.0)]);
let mut down = BreadthThrust::new(3).unwrap();
let downs = down.batch(&[section(0, 5), section(0, 7), section(0, 1)]);
assert_eq!(downs, vec![None, None, Some(0.0)]);
}
}