Skip to main content

wickra_core/indicators/
tick_index.rs

1//! TICK Index — instantaneous net advancing-minus-declining issues.
2
3use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6/// TICK Index — the instantaneous net of advancing minus declining issues across
7/// a universe, `advancers - decliners`.
8///
9/// Unlike the cumulative [`AdvanceDecline`](crate::AdvanceDecline) line, the TICK
10/// is *not* accumulated: each tick reports the breadth of that snapshot alone. It
11/// oscillates around zero — strongly positive readings mean a broad surge of
12/// upticks (often an intraday overbought extreme), strongly negative readings a
13/// broad flush. Traders fade extremes and watch the zero line for intraday bias.
14///
15/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
16///
17/// # Example
18///
19/// ```
20/// use wickra_core::{CrossSection, Indicator, Member, TickIndex};
21///
22/// let mut tick = TickIndex::new();
23/// // 2 advancers, 5 decliners -> net -3.
24/// let snapshot = CrossSection::new(
25///     vec![
26///         Member::new(1.0, 10.0, false, false),
27///         Member::new(1.0, 10.0, false, false),
28///         Member::new(-1.0, 10.0, false, false),
29///         Member::new(-1.0, 10.0, false, false),
30///         Member::new(-1.0, 10.0, false, false),
31///         Member::new(-1.0, 10.0, false, false),
32///         Member::new(-1.0, 10.0, false, false),
33///     ],
34///     0,
35/// )
36/// .unwrap();
37/// assert_eq!(tick.update(snapshot), Some(-3.0));
38/// ```
39#[derive(Debug, Clone, Default)]
40pub struct TickIndex {
41    has_emitted: bool,
42}
43
44impl TickIndex {
45    /// Construct a new TICK Index indicator.
46    #[must_use]
47    pub const fn new() -> Self {
48        Self { has_emitted: false }
49    }
50}
51
52impl Indicator for TickIndex {
53    type Input = CrossSection;
54    type Output = f64;
55
56    fn update(&mut self, section: CrossSection) -> Option<f64> {
57        let net = section.advancers() as f64 - section.decliners() as f64;
58        self.has_emitted = true;
59        Some(net)
60    }
61
62    fn reset(&mut self) {
63        self.has_emitted = false;
64    }
65
66    fn warmup_period(&self) -> usize {
67        1
68    }
69
70    fn is_ready(&self) -> bool {
71        self.has_emitted
72    }
73
74    fn name(&self) -> &'static str {
75        "TickIndex"
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::cross_section::Member;
83    use crate::traits::BatchExt;
84
85    fn section(up: usize, down: usize) -> CrossSection {
86        let mut members = Vec::new();
87        for _ in 0..up {
88            members.push(Member::new(1.0, 10.0, false, false));
89        }
90        for _ in 0..down {
91            members.push(Member::new(-1.0, 10.0, false, false));
92        }
93        members.push(Member::new(0.0, 10.0, false, false));
94        CrossSection::new(members, 0).unwrap()
95    }
96
97    #[test]
98    fn accessors_and_metadata() {
99        let tick = TickIndex::new();
100        assert_eq!(tick.name(), "TickIndex");
101        assert_eq!(tick.warmup_period(), 1);
102        assert!(!tick.is_ready());
103    }
104
105    #[test]
106    fn positive_when_advancers_lead() {
107        let mut tick = TickIndex::new();
108        assert_eq!(tick.update(section(5, 2)), Some(3.0));
109        assert!(tick.is_ready());
110    }
111
112    #[test]
113    fn negative_when_decliners_lead() {
114        let mut tick = TickIndex::new();
115        assert_eq!(tick.update(section(2, 5)), Some(-3.0));
116    }
117
118    #[test]
119    fn does_not_accumulate() {
120        let mut tick = TickIndex::new();
121        // Each tick is independent — the second reading does not carry the first.
122        assert_eq!(tick.update(section(3, 0)), Some(3.0));
123        assert_eq!(tick.update(section(0, 1)), Some(-1.0));
124    }
125
126    #[test]
127    fn reset_clears_state() {
128        let mut tick = TickIndex::new();
129        tick.update(section(3, 0));
130        assert!(tick.is_ready());
131        tick.reset();
132        assert!(!tick.is_ready());
133    }
134
135    #[test]
136    fn batch_equals_streaming() {
137        let sections = vec![section(5, 2), section(2, 5), section(3, 0), section(0, 1)];
138        let mut a = TickIndex::new();
139        let mut b = TickIndex::new();
140        assert_eq!(
141            a.batch(&sections),
142            sections
143                .iter()
144                .map(|s| b.update(s.clone()))
145                .collect::<Vec<_>>()
146        );
147    }
148}