Skip to main content

wickra_core/indicators/
trin.rs

1//! TRIN / Arms Index — the advance-decline ratio over the up-down volume ratio.
2
3use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6/// TRIN (Arms Index) — `(advancers / decliners) / (advancing volume / declining
7/// volume)`.
8///
9/// The TRIN compares the breadth of a move in *issues* to the breadth of the move
10/// in *volume*. A value near `1.0` means advancing issues and advancing volume are
11/// in balance; a value below `1.0` is bullish (volume is concentrated in advancing
12/// issues relative to their count); a value above `1.0` is bearish (declining
13/// issues are absorbing disproportionate volume).
14///
15/// To stay finite on degenerate ticks the decliner count is floored to one and
16/// both volume sums are floored to `1.0`, so a tick with no declining issues or no
17/// volume on one side still yields a defined reading instead of a division by
18/// zero.
19///
20/// `Input = CrossSection`, `Output = f64`, `warmup_period == 1`.
21///
22/// # Example
23///
24/// ```
25/// use wickra_core::{CrossSection, Indicator, Member, Trin};
26///
27/// let mut trin = Trin::new();
28/// // 3 advancers / 1 decliner = 3; adv vol 150 / dec vol 50 = 3; TRIN = 1.0.
29/// let tick = CrossSection::new(
30///     vec![
31///         Member::new(1.0, 50.0, false, false),
32///         Member::new(1.0, 50.0, false, false),
33///         Member::new(1.0, 50.0, false, false),
34///         Member::new(-1.0, 50.0, false, false),
35///     ],
36///     0,
37/// )
38/// .unwrap();
39/// assert_eq!(trin.update(tick), Some(1.0));
40/// ```
41#[derive(Debug, Clone, Default)]
42pub struct Trin {
43    has_emitted: bool,
44}
45
46impl Trin {
47    /// Construct a new TRIN / Arms Index indicator.
48    #[must_use]
49    pub const fn new() -> Self {
50        Self { has_emitted: false }
51    }
52}
53
54impl Indicator for Trin {
55    type Input = CrossSection;
56    type Output = f64;
57
58    #[inline]
59    fn update(&mut self, section: CrossSection) -> Option<f64> {
60        let advancers = section.advancers() as f64;
61        let decliners = section.decliners().max(1) as f64;
62        let advancing_volume = section.advancing_volume().max(1.0);
63        let declining_volume = section.declining_volume().max(1.0);
64        let ad_ratio = advancers / decliners;
65        let volume_ratio = advancing_volume / declining_volume;
66        self.has_emitted = true;
67        Some(ad_ratio / volume_ratio)
68    }
69
70    fn reset(&mut self) {
71        self.has_emitted = false;
72    }
73
74    #[inline]
75    fn warmup_period(&self) -> usize {
76        1
77    }
78
79    #[inline]
80    fn is_ready(&self) -> bool {
81        self.has_emitted
82    }
83
84    #[inline]
85    fn name(&self) -> &'static str {
86        "Trin"
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::cross_section::Member;
94    use crate::traits::BatchExt;
95
96    fn tick(items: &[(f64, f64)]) -> CrossSection {
97        CrossSection::new(
98            items
99                .iter()
100                .map(|&(change, volume)| Member::new(change, volume, false, false))
101                .collect(),
102            0,
103        )
104        .unwrap()
105    }
106
107    #[test]
108    fn accessors_and_metadata() {
109        let trin = Trin::new();
110        assert_eq!(trin.name(), "Trin");
111        assert_eq!(trin.warmup_period(), 1);
112        assert!(!trin.is_ready());
113    }
114
115    #[test]
116    fn balanced_breadth_yields_one() {
117        let mut trin = Trin::new();
118        let value = trin
119            .update(tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]))
120            .unwrap();
121        assert!((value - 1.0).abs() < 1e-9);
122        assert!(trin.is_ready());
123    }
124
125    #[test]
126    fn zero_decliners_and_volume_are_floored() {
127        let mut trin = Trin::new();
128        // 2 advancers, 0 decliners, adv vol 100, dec vol 0.
129        // ad_ratio = 2 / max(0,1) = 2; volume_ratio = 100 / max(0,1) = 100; TRIN = 0.02.
130        let value = trin.update(tick(&[(1.0, 50.0), (1.0, 50.0)])).unwrap();
131        assert!((value - 0.02).abs() < 1e-9);
132    }
133
134    #[test]
135    fn heavy_declining_volume_pushes_above_one() {
136        let mut trin = Trin::new();
137        // 2 adv / 2 dec = 1; adv vol 20 / dec vol 80 = 0.25; TRIN = 4.0.
138        let value = trin
139            .update(tick(&[
140                (1.0, 10.0),
141                (1.0, 10.0),
142                (-1.0, 40.0),
143                (-1.0, 40.0),
144            ]))
145            .unwrap();
146        assert!((value - 4.0).abs() < 1e-9);
147    }
148
149    #[test]
150    fn reset_clears_state() {
151        let mut trin = Trin::new();
152        trin.update(tick(&[(1.0, 10.0), (-1.0, 10.0)]));
153        assert!(trin.is_ready());
154        trin.reset();
155        assert!(!trin.is_ready());
156    }
157
158    #[test]
159    fn batch_equals_streaming() {
160        let sections = vec![
161            tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]),
162            tick(&[(1.0, 50.0), (1.0, 50.0)]),
163            tick(&[(1.0, 10.0), (1.0, 10.0), (-1.0, 40.0), (-1.0, 40.0)]),
164        ];
165        let mut a = Trin::new();
166        let mut b = Trin::new();
167        assert_eq!(
168            a.batch(&sections),
169            sections
170                .iter()
171                .map(|s| b.update(s.clone()))
172                .collect::<Vec<_>>()
173        );
174    }
175}