nautilus_model/defi/tick_map/
tick.rs1use std::cmp::Ord;
17
18use alloy_primitives::U256;
19
20use crate::defi::tick_map::liquidity_math::liquidity_math_add;
21
22#[cfg_attr(
24 feature = "python",
25 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
26)]
27#[derive(
28 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
29)]
30pub struct PoolTick {
31 pub value: i32,
33 pub liquidity_gross: u128,
35 pub liquidity_net: i128,
37 pub fee_growth_outside_0: U256,
39 pub fee_growth_outside_1: U256,
41 pub initialized: bool,
43 pub last_updated_block: u64,
45 pub updates_count: usize,
47}
48
49impl PoolTick {
50 pub const MIN_TICK: i32 = -887272;
52 pub const MAX_TICK: i32 = -Self::MIN_TICK;
54
55 #[must_use]
57 pub fn new(
58 value: i32,
59 liquidity_gross: u128,
60 liquidity_net: i128,
61 fee_growth_outside_0: U256,
62 fee_growth_outside_1: U256,
63 initialized: bool,
64 last_updated_block: u64,
65 ) -> Self {
66 Self {
67 value,
68 liquidity_gross,
69 liquidity_net,
70 fee_growth_outside_0,
71 fee_growth_outside_1,
72 initialized,
73 last_updated_block,
74 updates_count: 0,
75 }
76 }
77
78 pub fn from_tick(tick: i32) -> Self {
80 Self::new(tick, 0, 0, U256::ZERO, U256::ZERO, false, 0)
81 }
82
83 pub fn update_liquidity(&mut self, liquidity_delta: i128, upper: bool) -> u128 {
85 let liquidity_gross_before = self.liquidity_gross;
86 self.liquidity_gross = liquidity_math_add(self.liquidity_gross, liquidity_delta);
87
88 if upper {
90 self.liquidity_net -= liquidity_delta;
91 } else {
92 self.liquidity_net += liquidity_delta;
93 }
94 self.updates_count += 1;
95
96 liquidity_gross_before
97 }
98
99 pub fn clear(&mut self) {
101 self.liquidity_gross = 0;
102 self.liquidity_net = 0;
103 self.fee_growth_outside_0 = U256::ZERO;
104 self.fee_growth_outside_1 = U256::ZERO;
105 self.initialized = false;
106 }
107
108 #[must_use]
110 pub fn is_active(&self) -> bool {
111 self.initialized && self.liquidity_gross > 0
112 }
113
114 pub fn update_fee_growth(&mut self, fee_growth_global_0: U256, fee_growth_global_1: U256) {
116 self.fee_growth_outside_0 = fee_growth_global_0 - self.fee_growth_outside_0;
117 self.fee_growth_outside_1 = fee_growth_global_1 - self.fee_growth_outside_1;
118 }
119
120 pub fn get_max_tick(tick_spacing: i32) -> i32 {
122 (Self::MAX_TICK / tick_spacing) * tick_spacing
124 }
125
126 pub fn get_min_tick(tick_spacing: i32) -> i32 {
128 (Self::MIN_TICK / tick_spacing) * tick_spacing
130 }
131}
132
133#[cfg(test)]
138mod tests {
139 use rstest::rstest;
140
141 use super::*;
142
143 #[rstest]
144 fn test_update_liquidity_add_remove() {
145 let mut tick = PoolTick::from_tick(100);
146 tick.initialized = true;
147
148 tick.update_liquidity(1000, false); assert_eq!(tick.liquidity_gross, 1000);
151 assert_eq!(tick.liquidity_net, 1000); assert!(tick.is_active());
153
154 tick.update_liquidity(500, false);
156 assert_eq!(tick.liquidity_gross, 1500);
157 assert_eq!(tick.liquidity_net, 1500);
158 assert!(tick.is_active());
159
160 tick.update_liquidity(-300, false);
162 assert_eq!(tick.liquidity_gross, 1200);
163 assert_eq!(tick.liquidity_net, 1200);
164 assert!(tick.is_active());
165
166 tick.update_liquidity(-1200, false);
168 assert_eq!(tick.liquidity_gross, 0);
169 assert_eq!(tick.liquidity_net, 0);
170 assert!(!tick.is_active()); }
172
173 #[rstest]
174 fn test_update_liquidity_upper_tick() {
175 let mut tick = PoolTick::from_tick(200);
176 tick.initialized = true;
177
178 tick.update_liquidity(1000, true);
180 assert_eq!(tick.liquidity_gross, 1000);
181 assert_eq!(tick.liquidity_net, -1000); assert!(tick.is_active());
183
184 tick.update_liquidity(-500, true);
186 assert_eq!(tick.liquidity_gross, 500);
187 assert_eq!(tick.liquidity_net, -500); assert!(tick.is_active());
189 }
190
191 #[rstest]
192 fn test_get_max_tick() {
193 let max_tick_1 = PoolTick::get_max_tick(1);
197 assert_eq!(max_tick_1, 887272); let max_tick_10 = PoolTick::get_max_tick(10);
201 assert_eq!(max_tick_10, 887270); assert_eq!(max_tick_10 % 10, 0);
203 assert!(max_tick_10 <= PoolTick::MAX_TICK);
204
205 let max_tick_60 = PoolTick::get_max_tick(60);
207 assert_eq!(max_tick_60, 887220); assert_eq!(max_tick_60 % 60, 0);
209 assert!(max_tick_60 <= PoolTick::MAX_TICK);
210
211 let max_tick_200 = PoolTick::get_max_tick(200);
213 assert_eq!(max_tick_200, 887200); assert_eq!(max_tick_200 % 200, 0);
215 assert!(max_tick_200 <= PoolTick::MAX_TICK);
216 }
217
218 #[rstest]
219 fn test_get_min_tick() {
220 let min_tick_1 = PoolTick::get_min_tick(1);
224 assert_eq!(min_tick_1, -887272); let min_tick_10 = PoolTick::get_min_tick(10);
228 assert_eq!(min_tick_10, -887270); assert_eq!(min_tick_10 % 10, 0);
230 assert!(min_tick_10 >= PoolTick::MIN_TICK);
231
232 let min_tick_60 = PoolTick::get_min_tick(60);
234 assert_eq!(min_tick_60, -887220); assert_eq!(min_tick_60 % 60, 0);
236 assert!(min_tick_60 >= PoolTick::MIN_TICK);
237
238 let min_tick_200 = PoolTick::get_min_tick(200);
240 assert_eq!(min_tick_200, -887200); assert_eq!(min_tick_200 % 200, 0);
242 assert!(min_tick_200 >= PoolTick::MIN_TICK);
243 }
244
245 #[rstest]
246 fn test_tick_spacing_symmetry() {
247 let spacings = [1, 10, 60, 200];
249
250 for spacing in spacings {
251 let max_tick = PoolTick::get_max_tick(spacing);
252 let min_tick = PoolTick::get_min_tick(spacing);
253
254 assert_eq!(max_tick, -min_tick, "Asymmetry for spacing {}", spacing);
256
257 assert_eq!(max_tick % spacing, 0);
259 assert_eq!(min_tick % spacing, 0);
260
261 assert!(max_tick <= PoolTick::MAX_TICK);
263 assert!(min_tick >= PoolTick::MIN_TICK);
264 }
265 }
266}