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    #[inline]
57    fn update(&mut self, section: CrossSection) -> Option<f64> {
58        let net = section.advancers() as f64 - section.decliners() as f64;
59        self.has_emitted = true;
60        Some(net)
61    }
62
63    fn reset(&mut self) {
64        self.has_emitted = false;
65    }
66
67    #[inline]
68    fn warmup_period(&self) -> usize {
69        1
70    }
71
72    #[inline]
73    fn is_ready(&self) -> bool {
74        self.has_emitted
75    }
76
77    #[inline]
78    fn name(&self) -> &'static str {
79        "TickIndex"
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::cross_section::Member;
87    use crate::traits::BatchExt;
88
89    fn section(up: usize, down: usize) -> CrossSection {
90        let mut members = Vec::new();
91        for _ in 0..up {
92            members.push(Member::new(1.0, 10.0, false, false));
93        }
94        for _ in 0..down {
95            members.push(Member::new(-1.0, 10.0, false, false));
96        }
97        members.push(Member::new(0.0, 10.0, false, false));
98        CrossSection::new(members, 0).unwrap()
99    }
100
101    #[test]
102    fn accessors_and_metadata() {
103        let tick = TickIndex::new();
104        assert_eq!(tick.name(), "TickIndex");
105        assert_eq!(tick.warmup_period(), 1);
106        assert!(!tick.is_ready());
107    }
108
109    #[test]
110    fn positive_when_advancers_lead() {
111        let mut tick = TickIndex::new();
112        assert_eq!(tick.update(section(5, 2)), Some(3.0));
113        assert!(tick.is_ready());
114    }
115
116    #[test]
117    fn negative_when_decliners_lead() {
118        let mut tick = TickIndex::new();
119        assert_eq!(tick.update(section(2, 5)), Some(-3.0));
120    }
121
122    #[test]
123    fn does_not_accumulate() {
124        let mut tick = TickIndex::new();
125        // Each tick is independent — the second reading does not carry the first.
126        assert_eq!(tick.update(section(3, 0)), Some(3.0));
127        assert_eq!(tick.update(section(0, 1)), Some(-1.0));
128    }
129
130    #[test]
131    fn reset_clears_state() {
132        let mut tick = TickIndex::new();
133        tick.update(section(3, 0));
134        assert!(tick.is_ready());
135        tick.reset();
136        assert!(!tick.is_ready());
137    }
138
139    #[test]
140    fn batch_equals_streaming() {
141        let sections = vec![section(5, 2), section(2, 5), section(3, 0), section(0, 1)];
142        let mut a = TickIndex::new();
143        let mut b = TickIndex::new();
144        assert_eq!(
145            a.batch(&sections),
146            sections
147                .iter()
148                .map(|s| b.update(s.clone()))
149                .collect::<Vec<_>>()
150        );
151    }
152}