wickra_core/indicators/
kyles_lambda.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedPairMoments;
7use crate::microstructure::TradeQuote;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
62pub struct KylesLambda {
63 window: usize,
64 prev_mid: Option<f64>,
65 pairs: VecDeque<(f64, f64)>,
66 moments: ShiftedPairMoments,
67}
68
69impl KylesLambda {
70 pub fn new(window: usize) -> Result<Self> {
77 if window < 2 {
78 return Err(Error::InvalidPeriod {
79 message: "kyle's lambda needs window >= 2",
80 });
81 }
82 if window > crate::error::MAX_PERIOD {
83 return Err(Error::InvalidPeriod {
84 message: crate::error::PERIOD_ABOVE_MAX,
85 });
86 }
87 Ok(Self {
88 window,
89 prev_mid: None,
90 pairs: VecDeque::with_capacity(window),
91 moments: ShiftedPairMoments::new(),
92 })
93 }
94
95 pub const fn window(&self) -> usize {
97 self.window
98 }
99
100 fn push_pair(&mut self, signed_vol: f64, delta_mid: f64) -> Option<f64> {
101 if self.pairs.len() == self.window {
102 let (old_q, old_dm) = self.pairs.pop_front().expect("non-empty");
103 self.moments.evict(old_q, old_dm);
104 }
105 self.pairs.push_back((signed_vol, delta_mid));
106 self.moments.push(signed_vol, delta_mid);
107 if self.moments.needs_reseed(self.window) {
108 self.moments.reseed(self.pairs.iter().copied());
109 }
110 if self.pairs.len() < self.window {
111 return None;
112 }
113 let var_q = self.moments.var_a(self.window);
114 if var_q == 0.0 {
115 return Some(0.0);
117 }
118 Some(self.moments.cov(self.window) / var_q)
119 }
120}
121
122impl Indicator for KylesLambda {
123 type Input = TradeQuote;
124 type Output = f64;
125
126 #[inline]
127 fn update(&mut self, quote: TradeQuote) -> Option<f64> {
128 let mid = quote.mid;
129 let signed_vol = quote.trade.size * quote.trade.side.sign();
130 let Some(prev) = self.prev_mid else {
131 self.prev_mid = Some(mid);
132 return None;
133 };
134 self.prev_mid = Some(mid);
135 self.push_pair(signed_vol, mid - prev)
136 }
137
138 fn reset(&mut self) {
139 self.prev_mid = None;
140 self.pairs.clear();
141 self.moments.reset();
142 }
143
144 #[inline]
145 fn warmup_period(&self) -> usize {
146 self.window + 1
147 }
148
149 #[inline]
150 fn is_ready(&self) -> bool {
151 self.pairs.len() == self.window
152 }
153
154 #[inline]
155 fn name(&self) -> &'static str {
156 "KylesLambda"
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::microstructure::{Side, Trade};
164 use crate::traits::BatchExt;
165 use approx::assert_relative_eq;
166
167 fn quotes_with_impact(n: usize, impact: f64) -> Vec<TradeQuote> {
168 let mut mid = 100.0;
169 (0..n)
170 .map(|i| {
171 let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
172 let size = 1.0 + (i % 3) as f64;
173 let signed = size * side.sign();
174 mid += impact * signed;
175 let trade = Trade::new(mid, size, side, 0).unwrap();
176 TradeQuote::new(trade, mid).unwrap()
177 })
178 .collect()
179 }
180
181 #[test]
182 fn rejects_window_below_two() {
183 assert!(KylesLambda::new(0).is_err());
184 assert!(KylesLambda::new(1).is_err());
185 assert!(KylesLambda::new(2).is_ok());
186 }
187
188 #[test]
189 fn accessors_and_metadata() {
190 let kl = KylesLambda::new(14).unwrap();
191 assert_eq!(kl.name(), "KylesLambda");
192 assert_eq!(kl.window(), 14);
193 assert_eq!(kl.warmup_period(), 15);
194 assert!(!kl.is_ready());
195 }
196
197 #[test]
198 fn recovers_constant_impact_slope() {
199 let last = KylesLambda::new(6)
201 .unwrap()
202 .batch("es_with_impact(20, 0.5))
203 .into_iter()
204 .flatten()
205 .last()
206 .unwrap();
207 assert_relative_eq!(last, 0.5, epsilon = 1e-9);
208 }
209
210 #[test]
211 fn negative_impact_reads_negative() {
212 let last = KylesLambda::new(6)
213 .unwrap()
214 .batch("es_with_impact(20, -0.3))
215 .into_iter()
216 .flatten()
217 .last()
218 .unwrap();
219 assert_relative_eq!(last, -0.3, epsilon = 1e-9);
220 }
221
222 #[test]
223 fn constant_signed_volume_is_zero() {
224 let mut mid = 100.0;
226 let quotes: Vec<TradeQuote> = (0..10)
227 .map(|_| {
228 mid += 0.01;
229 let trade = Trade::new(mid, 1.0, Side::Buy, 0).unwrap();
230 TradeQuote::new(trade, mid).unwrap()
231 })
232 .collect();
233 let last = KylesLambda::new(5)
234 .unwrap()
235 .batch("es)
236 .into_iter()
237 .flatten()
238 .last()
239 .unwrap();
240 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
241 }
242
243 #[test]
244 fn warms_up_after_window_plus_one() {
245 let mut kl = KylesLambda::new(3).unwrap();
246 let quotes = quotes_with_impact(4, 0.2);
247 assert_eq!(kl.update(quotes[0]), None); assert_eq!(kl.update(quotes[1]), None);
249 assert_eq!(kl.update(quotes[2]), None);
250 assert!(!kl.is_ready());
251 assert!(kl.update(quotes[3]).is_some());
252 assert!(kl.is_ready());
253 }
254
255 #[test]
256 fn batch_equals_streaming() {
257 let quotes = quotes_with_impact(40, 0.15);
258 let batch = KylesLambda::new(10).unwrap().batch("es);
259 let mut kl = KylesLambda::new(10).unwrap();
260 let streamed: Vec<_> = quotes.iter().map(|q| kl.update(*q)).collect();
261 assert_eq!(batch, streamed);
262 }
263
264 #[test]
265 fn reset_clears_state() {
266 let mut kl = KylesLambda::new(3).unwrap();
267 for q in quotes_with_impact(6, 0.2) {
268 kl.update(q);
269 }
270 assert!(kl.is_ready());
271 kl.reset();
272 assert!(!kl.is_ready());
273 assert_eq!(kl.update(quotes_with_impact(1, 0.2)[0]), None);
274 }
275
276 #[test]
282 fn one_sided_flow_at_size_matches_a_two_pass_reference() {
283 const WINDOW: usize = 20;
284 const BARS: i64 = 600;
285
286 let mut ind = KylesLambda::new(WINDOW).unwrap();
287 let (mut qs, mut deltas): (Vec<f64>, Vec<f64>) = (Vec::new(), Vec::new());
288 let mut prev_mid = 100.0;
289 let mut compared = 0_usize;
290 for i in 0..BARS {
291 let t = i as f64;
292 let side = if (t * 0.37).sin() > -0.99 {
294 Side::Buy
295 } else {
296 Side::Sell
297 };
298 let size = 1e8 * (1.0 + 0.02 * (t * 0.19).cos());
299 let mid = 100.0 + 0.5 * (t * 0.05).sin();
300 let quote = TradeQuote::new(Trade::new(mid, size, side, i).unwrap(), mid).unwrap();
301 let got = ind.update(quote);
302 qs.push(if matches!(side, Side::Buy) {
303 size
304 } else {
305 -size
306 });
307 deltas.push(mid - prev_mid);
308 prev_mid = mid;
309 let Some(lambda) = got else { continue };
310 let k = qs.len();
311 let (xs, ys) = (&qs[k - WINDOW..], &deltas[k - WINDOW..]);
312 let n = WINDOW as f64;
313 let mean_q = xs.iter().sum::<f64>() / n;
314 let mean_d = ys.iter().sum::<f64>() / n;
315 let var_q = xs.iter().map(|v| (v - mean_q) * (v - mean_q)).sum::<f64>() / n;
316 let cov = xs
317 .iter()
318 .zip(ys)
319 .map(|(u, v)| (u - mean_q) * (v - mean_d))
320 .sum::<f64>()
321 / n;
322 compared += 1;
323 assert_relative_eq!(lambda, cov / var_q, max_relative = 1e-10);
324 }
325 assert_eq!(compared, BARS as usize - ind.warmup_period() + 1);
326 }
327}