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