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    fn update(&mut self, section: CrossSection) -> Option<f64> {
59        let advancers = section.advancers() as f64;
60        let decliners = section.decliners().max(1) as f64;
61        let advancing_volume = section.advancing_volume().max(1.0);
62        let declining_volume = section.declining_volume().max(1.0);
63        let ad_ratio = advancers / decliners;
64        let volume_ratio = advancing_volume / declining_volume;
65        self.has_emitted = true;
66        Some(ad_ratio / volume_ratio)
67    }
68
69    fn reset(&mut self) {
70        self.has_emitted = false;
71    }
72
73    fn warmup_period(&self) -> usize {
74        1
75    }
76
77    fn is_ready(&self) -> bool {
78        self.has_emitted
79    }
80
81    fn name(&self) -> &'static str {
82        "Trin"
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::cross_section::Member;
90    use crate::traits::BatchExt;
91
92    fn tick(items: &[(f64, f64)]) -> CrossSection {
93        CrossSection::new(
94            items
95                .iter()
96                .map(|&(change, volume)| Member::new(change, volume, false, false))
97                .collect(),
98            0,
99        )
100        .unwrap()
101    }
102
103    #[test]
104    fn accessors_and_metadata() {
105        let trin = Trin::new();
106        assert_eq!(trin.name(), "Trin");
107        assert_eq!(trin.warmup_period(), 1);
108        assert!(!trin.is_ready());
109    }
110
111    #[test]
112    fn balanced_breadth_yields_one() {
113        let mut trin = Trin::new();
114        let value = trin
115            .update(tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]))
116            .unwrap();
117        assert!((value - 1.0).abs() < 1e-9);
118        assert!(trin.is_ready());
119    }
120
121    #[test]
122    fn zero_decliners_and_volume_are_floored() {
123        let mut trin = Trin::new();
124        // 2 advancers, 0 decliners, adv vol 100, dec vol 0.
125        // ad_ratio = 2 / max(0,1) = 2; volume_ratio = 100 / max(0,1) = 100; TRIN = 0.02.
126        let value = trin.update(tick(&[(1.0, 50.0), (1.0, 50.0)])).unwrap();
127        assert!((value - 0.02).abs() < 1e-9);
128    }
129
130    #[test]
131    fn heavy_declining_volume_pushes_above_one() {
132        let mut trin = Trin::new();
133        // 2 adv / 2 dec = 1; adv vol 20 / dec vol 80 = 0.25; TRIN = 4.0.
134        let value = trin
135            .update(tick(&[
136                (1.0, 10.0),
137                (1.0, 10.0),
138                (-1.0, 40.0),
139                (-1.0, 40.0),
140            ]))
141            .unwrap();
142        assert!((value - 4.0).abs() < 1e-9);
143    }
144
145    #[test]
146    fn reset_clears_state() {
147        let mut trin = Trin::new();
148        trin.update(tick(&[(1.0, 10.0), (-1.0, 10.0)]));
149        assert!(trin.is_ready());
150        trin.reset();
151        assert!(!trin.is_ready());
152    }
153
154    #[test]
155    fn batch_equals_streaming() {
156        let sections = vec![
157            tick(&[(1.0, 50.0), (1.0, 50.0), (1.0, 50.0), (-1.0, 50.0)]),
158            tick(&[(1.0, 50.0), (1.0, 50.0)]),
159            tick(&[(1.0, 10.0), (1.0, 10.0), (-1.0, 40.0), (-1.0, 40.0)]),
160        ];
161        let mut a = Trin::new();
162        let mut b = Trin::new();
163        assert_eq!(
164            a.batch(&sections),
165            sections
166                .iter()
167                .map(|s| b.update(s.clone()))
168                .collect::<Vec<_>>()
169        );
170    }
171}