wickra_core/indicators/
trin.rs1use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
42pub struct Trin {
43 has_emitted: bool,
44}
45
46impl Trin {
47 #[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 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 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(§ions),
165 sections
166 .iter()
167 .map(|s| b.update(s.clone()))
168 .collect::<Vec<_>>()
169 );
170 }
171}