nautilus_model/defi/tick_map/
tick_math.rs1use alloy_primitives::{U160, U256};
17
18use crate::defi::tick_map::{bit_math::most_significant_bit, tick::PoolTick};
19
20pub const MIN_SQRT_RATIO: U160 = U160::from_limbs([4295128739u64, 0, 0]);
22
23pub const MAX_SQRT_RATIO: U160 = U160::from_limbs([
25 0x5d951d5263988d26u64, 0xefd1fc6a50648849u64, 0xfffd8963u64, ]);
29
30#[inline]
45pub fn get_sqrt_ratio_at_tick(tick: i32) -> U160 {
46 let abs_tick = tick.abs();
47
48 assert!(abs_tick <= PoolTick::MAX_TICK, "Tick {tick} out of bounds");
49
50 let mut ratio = if abs_tick & 0x1 != 0 {
52 U256::from_str_radix("fffcb933bd6fad37aa2d162d1a594001", 16).unwrap()
53 } else {
54 U256::from_str_radix("100000000000000000000000000000000", 16).unwrap()
55 };
56
57 if abs_tick & 0x2 != 0 {
59 ratio =
60 (ratio * U256::from_str_radix("fff97272373d413259a46990580e213a", 16).unwrap()) >> 128;
61 }
62 if abs_tick & 0x4 != 0 {
63 ratio =
64 (ratio * U256::from_str_radix("fff2e50f5f656932ef12357cf3c7fdcc", 16).unwrap()) >> 128;
65 };
66 if abs_tick & 0x8 != 0 {
67 ratio =
68 (ratio * U256::from_str_radix("ffe5caca7e10e4e61c3624eaa0941cd0", 16).unwrap()) >> 128;
69 }
70 if abs_tick & 0x10 != 0 {
71 ratio =
72 (ratio * U256::from_str_radix("ffcb9843d60f6159c9db58835c926644", 16).unwrap()) >> 128;
73 }
74 if abs_tick & 0x20 != 0 {
75 ratio =
76 (ratio * U256::from_str_radix("ff973b41fa98c081472e6896dfb254c0", 16).unwrap()) >> 128;
77 }
78 if abs_tick & 0x40 != 0 {
79 ratio =
80 (ratio * U256::from_str_radix("ff2ea16466c96a3843ec78b326b52861", 16).unwrap()) >> 128;
81 }
82 if abs_tick & 0x80 != 0 {
83 ratio =
84 (ratio * U256::from_str_radix("fe5dee046a99a2a811c461f1969c3053", 16).unwrap()) >> 128;
85 }
86 if abs_tick & 0x100 != 0 {
87 ratio =
88 (ratio * U256::from_str_radix("fcbe86c7900a88aedcffc83b479aa3a4", 16).unwrap()) >> 128;
89 }
90 if abs_tick & 0x200 != 0 {
91 ratio =
92 (ratio * U256::from_str_radix("f987a7253ac413176f2b074cf7815e54", 16).unwrap()) >> 128;
93 }
94 if abs_tick & 0x400 != 0 {
95 ratio =
96 (ratio * U256::from_str_radix("f3392b0822b70005940c7a398e4b70f3", 16).unwrap()) >> 128;
97 }
98 if abs_tick & 0x800 != 0 {
99 ratio =
100 (ratio * U256::from_str_radix("e7159475a2c29b7443b29c7fa6e889d9", 16).unwrap()) >> 128;
101 }
102 if abs_tick & 0x1000 != 0 {
103 ratio =
104 (ratio * U256::from_str_radix("d097f3bdfd2022b8845ad8f792aa5825", 16).unwrap()) >> 128;
105 }
106 if abs_tick & 0x2000 != 0 {
107 ratio =
108 (ratio * U256::from_str_radix("a9f746462d870fdf8a65dc1f90e061e5", 16).unwrap()) >> 128;
109 }
110 if abs_tick & 0x4000 != 0 {
111 ratio =
112 (ratio * U256::from_str_radix("70d869a156d2a1b890bb3df62baf32f7", 16).unwrap()) >> 128;
113 }
114 if abs_tick & 0x8000 != 0 {
115 ratio =
116 (ratio * U256::from_str_radix("31be135f97d08fd981231505542fcfa6", 16).unwrap()) >> 128;
117 }
118 if abs_tick & 0x10000 != 0 {
119 ratio =
120 (ratio * U256::from_str_radix("9aa508b5b7a84e1c677de54f3e99bc9", 16).unwrap()) >> 128;
121 }
122 if abs_tick & 0x20000 != 0 {
123 ratio =
124 (ratio * U256::from_str_radix("5d6af8dedb81196699c329225ee604", 16).unwrap()) >> 128;
125 }
126 if abs_tick & 0x40000 != 0 {
127 ratio = (ratio * U256::from_str_radix("2216e584f5fa1ea926041bedfe98", 16).unwrap()) >> 128;
128 }
129 if abs_tick & 0x80000 != 0 {
130 ratio = (ratio * U256::from_str_radix("48a170391f7dc42444e8fa2", 16).unwrap()) >> 128;
131 }
132
133 if tick.is_positive() {
134 ratio = U256::MAX / ratio;
135 }
136
137 ratio = (ratio + U256::from(0xffffffffu32)) >> 32;
138 U160::from(ratio)
139}
140
141pub fn get_tick_at_sqrt_ratio(sqrt_price_x96: U160) -> i32 {
154 assert!(
155 sqrt_price_x96 >= MIN_SQRT_RATIO && sqrt_price_x96 < MAX_SQRT_RATIO,
156 "Sqrt price out of bounds"
157 );
158
159 let ratio = U256::from(sqrt_price_x96) << 32;
160 let msb = most_significant_bit(ratio);
161
162 let mut log_2_x64 = if msb >= 128 {
165 U256::from((msb - 128) as u64) << 64
166 } else {
167 U256::MAX - (U256::from((128 - msb) as u64) << 64) + U256::from(1)
169 };
170
171 let mut r = if msb >= 128 {
173 ratio >> (msb - 127)
174 } else {
175 ratio << (127 - msb)
176 };
177
178 let mut decimals = U256::ZERO;
180 for i in (50..=63).rev() {
181 r = (r * r) >> 127;
182 let f = r >> 128;
183 if f > U256::ZERO {
184 decimals |= U256::ONE << i;
185 r >>= 1;
186 }
187 }
188
189 log_2_x64 |= decimals;
191
192 let log_sqrt10001 = log_2_x64 * U256::from(255738958999603826347141u128);
196
197 let tick_low_offset =
199 U256::from_str_radix("3402992956809132418596140100660247210", 10).unwrap();
200 let tick_hi_offset =
201 U256::from_str_radix("291339464771989622907027621153398088495", 10).unwrap();
202
203 let tick_low_u256: U256 = (log_sqrt10001 - tick_low_offset) >> 128;
204 let tick_hi_u256: U256 = (log_sqrt10001 + tick_hi_offset) >> 128;
205
206 let tick_low = tick_low_u256.as_le_bytes()[0] as i32
210 | ((tick_low_u256.as_le_bytes()[1] as i32) << 8)
211 | ((tick_low_u256.as_le_bytes()[2] as i32) << 16)
212 | ((tick_low_u256.as_le_bytes()[3] as i32) << 24);
213 let tick_hi = tick_hi_u256.as_le_bytes()[0] as i32
214 | ((tick_hi_u256.as_le_bytes()[1] as i32) << 8)
215 | ((tick_hi_u256.as_le_bytes()[2] as i32) << 16)
216 | ((tick_hi_u256.as_le_bytes()[3] as i32) << 24);
217
218 if tick_low == tick_hi {
220 tick_low
221 } else if get_sqrt_ratio_at_tick(tick_hi) <= sqrt_price_x96 {
222 tick_hi
223 } else {
224 tick_low
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use std::str::FromStr;
231
232 use rstest::rstest;
233
234 use super::*;
235 use crate::defi::tick_map::sqrt_price_math::encode_sqrt_ratio_x96;
236
237 #[rstest]
238 fn test_get_sqrt_ratio_at_tick_zero() {
239 let sqrt_ratio = get_sqrt_ratio_at_tick(0);
240 let expected = U160::from(1u128) << 96;
242 assert_eq!(sqrt_ratio, expected);
243 }
244
245 #[rstest]
246 fn test_get_tick_at_sqrt_ratio() {
247 let sqrt_ratio_u160 = U160::from(1u128 << 96); let tick = get_tick_at_sqrt_ratio(sqrt_ratio_u160);
249 assert_eq!(tick, 0);
250 }
251
252 #[rstest]
253 #[should_panic(expected = "Tick 887273 out of bounds")]
254 fn test_get_sqrt_ratio_at_tick_panics_above_max() {
255 let _ = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK + 1);
256 }
257
258 #[rstest]
259 #[should_panic(expected = "Tick -887273 out of bounds")]
260 fn test_get_sqrt_ratio_at_tick_panics_below_min() {
261 let _ = get_sqrt_ratio_at_tick(PoolTick::MIN_TICK - 1);
262 }
263
264 #[rstest]
266 #[should_panic(expected = "Sqrt price out of bounds")]
267 fn test_get_tick_at_sqrt_ratio_throws_for_too_low() {
268 let _ = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO - U160::from(1));
269 }
270
271 #[rstest]
272 #[should_panic(expected = "Sqrt price out of bounds")]
273 fn test_get_tick_at_sqrt_ratio_throws_for_too_high() {
274 let _ = get_tick_at_sqrt_ratio(MAX_SQRT_RATIO);
275 }
276
277 #[rstest]
278 fn test_get_tick_at_sqrt_ratio_min_tick() {
279 let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO);
280 assert_eq!(result, PoolTick::MIN_TICK);
281 }
282
283 #[rstest]
284 fn test_get_tick_at_sqrt_ration_various_values() {
285 assert_eq!(
286 get_tick_at_sqrt_ratio(U160::from_str("511495728837967332084595714").unwrap()),
287 -100860
288 );
289 assert_eq!(
290 get_tick_at_sqrt_ratio(U160::from_str("14464772219441977173490711849216").unwrap()),
291 104148
292 );
293 assert_eq!(
294 get_tick_at_sqrt_ratio(U160::from_str("17148448136625419841777674413284").unwrap()),
295 107552
296 );
297 }
298
299 #[rstest]
300 fn test_get_tick_at_sqrt_ratio_min_tick_plus_one() {
301 let result = get_tick_at_sqrt_ratio(U160::from(4295343490u64));
302 assert_eq!(result, PoolTick::MIN_TICK + 1);
303 }
304
305 #[rstest]
306 fn test_get_tick_at_sqrt_ratio_max_tick_minus_one() {
307 let sqrt_ratio =
310 U160::from_str_radix("fffa429fbf7baeed2496f0a9f5ccf2bb4abf52f9", 16).unwrap();
311
312 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
314
315 assert_eq!(
317 result,
318 PoolTick::MAX_TICK - 1,
319 "Uniswap test value should map to MAX_TICK - 1"
320 );
321 }
322
323 #[rstest]
324 fn test_get_tick_at_sqrt_ratio_closest_to_max_tick() {
325 let sqrt_ratio = MAX_SQRT_RATIO - U160::from(1);
327 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
328
329 assert!(result > 0 && result < PoolTick::MAX_TICK);
331
332 }
335
336 #[rstest]
337 #[case::min_sqrt_ratio(MIN_SQRT_RATIO)]
338 #[case::price_10_12_to_1(encode_sqrt_ratio_x96(1, 1000000000000))] #[case::price_10_6_to_1(encode_sqrt_ratio_x96(1, 1000000))] #[case::price_1_to_64(encode_sqrt_ratio_x96(64, 1))] #[case::price_1_to_8(encode_sqrt_ratio_x96(8, 1))] #[case::price_1_to_2(encode_sqrt_ratio_x96(2, 1))] #[case::price_1_to_1(encode_sqrt_ratio_x96(1, 1))] #[case::price_2_to_1(encode_sqrt_ratio_x96(1, 2))] #[case::price_8_to_1(encode_sqrt_ratio_x96(1, 8))] #[case::price_64_to_1(encode_sqrt_ratio_x96(1, 64))] #[case::price_1_to_10_6(encode_sqrt_ratio_x96(1000000, 1))] #[case::price_1_to_10_12(encode_sqrt_ratio_x96(1000000000000, 1))] #[case::max_sqrt_ratio_minus_one(MAX_SQRT_RATIO - U160::from(1))]
350 fn test_get_tick_at_sqrt_ratio_accuracy(#[case] ratio: U160) {
351 let tick = get_tick_at_sqrt_ratio(ratio);
352
353 let ratio_f64 = ratio.to_string().parse::<f64>().unwrap();
355 let price = (ratio_f64 / (1u128 << 96) as f64).powi(2);
356 let theoretical_tick = (price.ln() / 1.0001_f64.ln()).floor() as i32;
357 let diff = (tick - theoretical_tick).abs();
358 assert!(
359 diff <= 1,
360 "Tick {tick} differs from theoretical {theoretical_tick} by more than 1"
361 );
362
363 let ratio_of_tick = U256::from(get_sqrt_ratio_at_tick(tick));
365 let ratio_of_tick_plus_one = U256::from(get_sqrt_ratio_at_tick(tick + 1));
366 let ratio_u256 = U256::from(ratio);
367
368 assert!(
369 ratio_u256 >= ratio_of_tick,
370 "Ratio {ratio_u256} should be >= ratio of tick {ratio_of_tick}"
371 );
372 assert!(
373 ratio_u256 < ratio_of_tick_plus_one,
374 "Ratio {ratio_u256} should be < ratio of tick+1 {ratio_of_tick_plus_one}"
375 );
376 }
377
378 #[rstest]
379 fn test_get_tick_at_sqrt_ratio_specific_values() {
380 let test_cases = vec![
382 (MIN_SQRT_RATIO, PoolTick::MIN_TICK),
383 (U160::from(1u128 << 96), 0), ];
385
386 for (sqrt_ratio, expected_tick) in test_cases {
387 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
388 assert_eq!(result, expected_tick, "Failed for sqrt_ratio {sqrt_ratio}");
389 }
390 }
391
392 #[rstest]
393 fn test_round_trip_tick_sqrt_ratio() {
394 let test_ticks = vec![
398 -887272, -100000, -1000, -100, -1, 0, 1, 100, 1000, 100000, 700000,
399 ];
400
401 for original_tick in test_ticks {
402 let sqrt_ratio = get_sqrt_ratio_at_tick(original_tick);
403
404 if sqrt_ratio < MAX_SQRT_RATIO {
406 let recovered_tick = get_tick_at_sqrt_ratio(sqrt_ratio);
407
408 assert_eq!(
410 recovered_tick, original_tick,
411 "Round trip failed: {original_tick} -> {sqrt_ratio} -> {recovered_tick}"
412 );
413 } else {
414 println!(
417 "Tick {original_tick} produces sqrt_ratio {sqrt_ratio} which exceeds MAX_SQRT_RATIO"
418 );
419 }
420 }
421 }
422
423 #[rstest]
424 fn test_extreme_ticks_behavior() {
425 let min_sqrt = get_sqrt_ratio_at_tick(PoolTick::MIN_TICK);
426 assert_eq!(
427 min_sqrt, MIN_SQRT_RATIO,
428 "MIN_TICK should produce MIN_SQRT_RATIO"
429 );
430 let recovered_min = get_tick_at_sqrt_ratio(min_sqrt);
431 assert_eq!(
432 recovered_min,
433 PoolTick::MIN_TICK,
434 "MIN_TICK should round-trip correctly"
435 );
436
437 let max_sqrt = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK);
439
440 assert_eq!(
443 max_sqrt, MAX_SQRT_RATIO,
444 "MAX_TICK should produce exactly MAX_SQRT_RATIO"
445 );
446
447 let max_valid_sqrt = MAX_SQRT_RATIO - U160::from(1);
450 let max_valid_tick = get_tick_at_sqrt_ratio(max_valid_sqrt);
451
452 assert_eq!(
454 max_valid_tick,
455 PoolTick::MAX_TICK - 1,
456 "MAX_SQRT_RATIO - 1 should map to MAX_TICK - 1"
457 );
458 }
459}