wickra_core/indicators/
pin.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::microstructure::Trade;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
48pub struct Pin {
49 window: usize,
50 sides: VecDeque<f64>,
51 buy_count: usize,
52 last: Option<f64>,
53}
54
55impl Pin {
56 pub fn new(window: usize) -> Result<Self> {
62 if window == 0 {
63 return Err(Error::PeriodZero);
64 }
65 if window > crate::error::MAX_PERIOD {
66 return Err(Error::InvalidPeriod {
67 message: crate::error::PERIOD_ABOVE_MAX,
68 });
69 }
70 Ok(Self {
71 window,
72 sides: VecDeque::with_capacity(window),
73 buy_count: 0,
74 last: None,
75 })
76 }
77
78 pub const fn window(&self) -> usize {
80 self.window
81 }
82
83 pub const fn value(&self) -> Option<f64> {
85 self.last
86 }
87}
88
89impl Indicator for Pin {
90 type Input = Trade;
91 type Output = f64;
92
93 #[inline]
94 fn update(&mut self, trade: Trade) -> Option<f64> {
95 let is_buy = trade.side.sign() > 0.0;
96 if self.sides.len() == self.window {
97 let old = self.sides.pop_front().expect("non-empty");
98 if old > 0.0 {
99 self.buy_count -= 1;
100 }
101 }
102 self.sides.push_back(if is_buy { 1.0 } else { 0.0 });
103 if is_buy {
104 self.buy_count += 1;
105 }
106 if self.sides.len() < self.window {
107 return None;
108 }
109 let buys = self.buy_count as f64;
113 let sells = self.window as f64 - buys;
114 let total = self.window as f64;
115 let pin = (buys - sells).abs() / total;
116 self.last = Some(pin);
117 Some(pin)
118 }
119
120 fn reset(&mut self) {
121 self.sides.clear();
122 self.buy_count = 0;
123 self.last = None;
124 }
125
126 #[inline]
127 fn warmup_period(&self) -> usize {
128 self.window
129 }
130
131 #[inline]
132 fn is_ready(&self) -> bool {
133 self.last.is_some()
134 }
135
136 #[inline]
137 fn name(&self) -> &'static str {
138 "PIN"
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::microstructure::Side;
146 use crate::traits::BatchExt;
147 use approx::assert_relative_eq;
148
149 fn buy() -> Trade {
150 Trade::new_unchecked(100.0, 1.0, Side::Buy, 0)
151 }
152
153 fn sell() -> Trade {
154 Trade::new_unchecked(100.0, 1.0, Side::Sell, 0)
155 }
156
157 #[test]
158 fn rejects_zero_window() {
159 assert!(matches!(Pin::new(0), Err(Error::PeriodZero)));
160 }
161
162 #[test]
163 fn accessors_and_metadata() {
164 let p = Pin::new(20).unwrap();
165 assert_eq!(p.window(), 20);
166 assert_eq!(p.warmup_period(), 20);
167 assert_eq!(p.name(), "PIN");
168 assert!(!p.is_ready());
169 assert_eq!(p.value(), None);
170 }
171
172 #[test]
173 fn first_emission_at_warmup_period() {
174 let mut p = Pin::new(4).unwrap();
175 let out = p.batch(&[buy(), buy(), buy(), buy(), buy()]);
176 for v in out.iter().take(3) {
177 assert!(v.is_none());
178 }
179 assert!(out[3].is_some());
180 }
181
182 #[test]
183 fn one_sided_flow_is_one() {
184 let mut p = Pin::new(10).unwrap();
185 let trades: Vec<Trade> = (0..20).map(|_| buy()).collect();
186 let last = p.batch(&trades).into_iter().flatten().last().unwrap();
187 assert_relative_eq!(last, 1.0, epsilon = 1e-12);
188 }
189
190 #[test]
191 fn balanced_flow_is_zero() {
192 let mut p = Pin::new(10).unwrap();
193 let trades: Vec<Trade> = (0..20)
194 .map(|i| if i % 2 == 0 { buy() } else { sell() })
195 .collect();
196 let last = p.batch(&trades).into_iter().flatten().last().unwrap();
197 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
198 }
199
200 #[test]
201 fn output_in_range() {
202 let mut p = Pin::new(16).unwrap();
203 let trades: Vec<Trade> = (0..200)
204 .map(|i| if (i * 5 % 13) < 8 { buy() } else { sell() })
205 .collect();
206 for v in p.batch(&trades).into_iter().flatten() {
207 assert!((0.0..=1.0).contains(&v));
208 }
209 }
210
211 #[test]
212 fn reset_clears_state() {
213 let mut p = Pin::new(4).unwrap();
214 p.batch(&[buy(), buy(), sell(), buy()]);
215 assert!(p.is_ready());
216 p.reset();
217 assert!(!p.is_ready());
218 assert_eq!(p.value(), None);
219 assert_eq!(p.update(buy()), None);
220 }
221
222 #[test]
223 fn batch_equals_streaming() {
224 let trades: Vec<Trade> = (0..120)
225 .map(|i| if i % 3 == 0 { sell() } else { buy() })
226 .collect();
227 let batch = Pin::new(16).unwrap().batch(&trades);
228 let mut b = Pin::new(16).unwrap();
229 let streamed: Vec<_> = trades.iter().map(|x| b.update(*x)).collect();
230 assert_eq!(batch, streamed);
231 }
232}