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 fn update(&mut self, tick: DerivativesTick) -> Option<LiquidationFeaturesOutput> {
79 self.has_emitted = true;
80 let long = tick.long_liquidation;
81 let short = tick.short_liquidation;
82 let net = long - short;
83 let total = long + short;
84 let imbalance = if total == 0.0 { 0.0 } else { net / total };
85 Some(LiquidationFeaturesOutput {
86 long,
87 short,
88 net,
89 total,
90 imbalance,
91 })
92 }
93
94 fn reset(&mut self) {
95 self.has_emitted = false;
96 }
97
98 fn warmup_period(&self) -> usize {
99 1
100 }
101
102 fn is_ready(&self) -> bool {
103 self.has_emitted
104 }
105
106 fn name(&self) -> &'static str {
107 "LiquidationFeatures"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::traits::BatchExt;
115
116 fn tick(long_liq: f64, short_liq: f64) -> DerivativesTick {
117 DerivativesTick::new_unchecked(
118 0.0, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, long_liq, short_liq, 0,
119 )
120 }
121
122 #[test]
123 fn accessors_and_metadata() {
124 let liq = LiquidationFeatures::new();
125 assert_eq!(liq.name(), "LiquidationFeatures");
126 assert_eq!(liq.warmup_period(), 1);
127 assert!(!liq.is_ready());
128 }
129
130 #[test]
131 fn decomposes_liquidations() {
132 let mut liq = LiquidationFeatures::new();
133 let out = liq.update(tick(30.0, 10.0)).unwrap();
134 assert_eq!(out.long, 30.0);
135 assert_eq!(out.short, 10.0);
136 assert_eq!(out.net, 20.0);
137 assert_eq!(out.total, 40.0);
138 assert_eq!(out.imbalance, 0.5);
139 assert!(liq.is_ready());
140 }
141
142 #[test]
143 fn short_cascade_is_negative_imbalance() {
144 let mut liq = LiquidationFeatures::new();
145 let out = liq.update(tick(0.0, 50.0)).unwrap();
146 assert_eq!(out.net, -50.0);
147 assert_eq!(out.imbalance, -1.0);
148 }
149
150 #[test]
151 fn no_liquidation_is_zero_imbalance() {
152 let mut liq = LiquidationFeatures::new();
153 let out = liq.update(tick(0.0, 0.0)).unwrap();
154 assert_eq!(out.total, 0.0);
155 assert_eq!(out.imbalance, 0.0);
156 }
157
158 #[test]
159 fn batch_equals_streaming() {
160 let ticks: Vec<DerivativesTick> = (0..20)
161 .map(|i| tick(f64::from(i % 5) * 10.0, f64::from(i % 3) * 10.0))
162 .collect();
163 let mut a = LiquidationFeatures::new();
164 let mut b = LiquidationFeatures::new();
165 assert_eq!(
166 a.batch(&ticks),
167 ticks.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
168 );
169 }
170
171 #[test]
172 fn reset_clears_state() {
173 let mut liq = LiquidationFeatures::new();
174 liq.update(tick(30.0, 10.0));
175 assert!(liq.is_ready());
176 liq.reset();
177 assert!(!liq.is_ready());
178 }
179}