wickra_core/indicators/
liquidation_features.rs1use crate::derivatives::DerivativesTick;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
9pub struct LiquidationFeaturesOutput {
10 pub long: f64,
12 pub short: f64,
14 pub net: f64,
16 pub total: f64,
18 pub imbalance: f64,
21}
22
23#[derive(Debug, Clone, Default)]
62pub struct LiquidationFeatures {
63 has_emitted: bool,
64}
65
66impl LiquidationFeatures {
67 #[must_use]
69 pub const fn new() -> Self {
70 Self { has_emitted: false }
71 }
72}
73
74impl Indicator for LiquidationFeatures {
75 type Input = DerivativesTick;
76 type Output = LiquidationFeaturesOutput;
77
78 #[inline]
79 fn update(&mut self, tick: DerivativesTick) -> Option<LiquidationFeaturesOutput> {
80 self.has_emitted = true;
81 let long = tick.long_liquidation;
82 let short = tick.short_liquidation;
83 let net = long - short;
84 let total = long + short;
85 let imbalance = if total == 0.0 { 0.0 } else { net / total };
86 Some(LiquidationFeaturesOutput {
87 long,
88 short,
89 net,
90 total,
91 imbalance,
92 })
93 }
94
95 fn reset(&mut self) {
96 self.has_emitted = false;
97 }
98
99 #[inline]
100 fn warmup_period(&self) -> usize {
101 1
102 }
103
104 #[inline]
105 fn is_ready(&self) -> bool {
106 self.has_emitted
107 }
108
109 #[inline]
110 fn name(&self) -> &'static str {
111 "LiquidationFeatures"
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::traits::BatchExt;
119
120 fn tick(long_liq: f64, short_liq: f64) -> DerivativesTick {
121 DerivativesTick::new_unchecked(
122 0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, long_liq, short_liq, 0,
123 )
124 }
125
126 #[test]
127 fn accessors_and_metadata() {
128 let liq = LiquidationFeatures::new();
129 assert_eq!(liq.name(), "LiquidationFeatures");
130 assert_eq!(liq.warmup_period(), 1);
131 assert!(!liq.is_ready());
132 }
133
134 #[test]
135 fn decomposes_liquidations() {
136 let mut liq = LiquidationFeatures::new();
137 let out = liq.update(tick(30.0, 10.0)).unwrap();
138 assert_eq!(out.long, 30.0);
139 assert_eq!(out.short, 10.0);
140 assert_eq!(out.net, 20.0);
141 assert_eq!(out.total, 40.0);
142 assert_eq!(out.imbalance, 0.5);
143 assert!(liq.is_ready());
144 }
145
146 #[test]
147 fn short_cascade_is_negative_imbalance() {
148 let mut liq = LiquidationFeatures::new();
149 let out = liq.update(tick(0.0, 50.0)).unwrap();
150 assert_eq!(out.net, -50.0);
151 assert_eq!(out.imbalance, -1.0);
152 }
153
154 #[test]
155 fn no_liquidation_is_zero_imbalance() {
156 let mut liq = LiquidationFeatures::new();
157 let out = liq.update(tick(0.0, 0.0)).unwrap();
158 assert_eq!(out.total, 0.0);
159 assert_eq!(out.imbalance, 0.0);
160 }
161
162 #[test]
163 fn batch_equals_streaming() {
164 let ticks: Vec<DerivativesTick> = (0..20)
165 .map(|i| tick(f64::from(i % 5) * 10.0, f64::from(i % 3) * 10.0))
166 .collect();
167 let mut a = LiquidationFeatures::new();
168 let mut b = LiquidationFeatures::new();
169 assert_eq!(
170 a.batch(&ticks),
171 ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
172 );
173 }
174
175 #[test]
176 fn reset_clears_state() {
177 let mut liq = LiquidationFeatures::new();
178 liq.update(tick(30.0, 10.0));
179 assert!(liq.is_ready());
180 liq.reset();
181 assert!(!liq.is_ready());
182 }
183}