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!(
49 abs_tick <= PoolTick::MAX_TICK,
50 "Tick {} out of bounds",
51 tick
52 );
53
54 let mut ratio = if abs_tick & 0x1 != 0 {
56 U256::from_str_radix("fffcb933bd6fad37aa2d162d1a594001", 16).unwrap()
57 } else {
58 U256::from_str_radix("100000000000000000000000000000000", 16).unwrap()
59 };
60
61 if abs_tick & 0x2 != 0 {
63 ratio =
64 (ratio * U256::from_str_radix("fff97272373d413259a46990580e213a", 16).unwrap()) >> 128;
65 }
66 if abs_tick & 0x4 != 0 {
67 ratio =
68 (ratio * U256::from_str_radix("fff2e50f5f656932ef12357cf3c7fdcc", 16).unwrap()) >> 128;
69 };
70 if abs_tick & 0x8 != 0 {
71 ratio =
72 (ratio * U256::from_str_radix("ffe5caca7e10e4e61c3624eaa0941cd0", 16).unwrap()) >> 128;
73 }
74 if abs_tick & 0x10 != 0 {
75 ratio =
76 (ratio * U256::from_str_radix("ffcb9843d60f6159c9db58835c926644", 16).unwrap()) >> 128;
77 }
78 if abs_tick & 0x20 != 0 {
79 ratio =
80 (ratio * U256::from_str_radix("ff973b41fa98c081472e6896dfb254c0", 16).unwrap()) >> 128;
81 }
82 if abs_tick & 0x40 != 0 {
83 ratio =
84 (ratio * U256::from_str_radix("ff2ea16466c96a3843ec78b326b52861", 16).unwrap()) >> 128;
85 }
86 if abs_tick & 0x80 != 0 {
87 ratio =
88 (ratio * U256::from_str_radix("fe5dee046a99a2a811c461f1969c3053", 16).unwrap()) >> 128;
89 }
90 if abs_tick & 0x100 != 0 {
91 ratio =
92 (ratio * U256::from_str_radix("fcbe86c7900a88aedcffc83b479aa3a4", 16).unwrap()) >> 128;
93 }
94 if abs_tick & 0x200 != 0 {
95 ratio =
96 (ratio * U256::from_str_radix("f987a7253ac413176f2b074cf7815e54", 16).unwrap()) >> 128;
97 }
98 if abs_tick & 0x400 != 0 {
99 ratio =
100 (ratio * U256::from_str_radix("f3392b0822b70005940c7a398e4b70f3", 16).unwrap()) >> 128;
101 }
102 if abs_tick & 0x800 != 0 {
103 ratio =
104 (ratio * U256::from_str_radix("e7159475a2c29b7443b29c7fa6e889d9", 16).unwrap()) >> 128;
105 }
106 if abs_tick & 0x1000 != 0 {
107 ratio =
108 (ratio * U256::from_str_radix("d097f3bdfd2022b8845ad8f792aa5825", 16).unwrap()) >> 128;
109 }
110 if abs_tick & 0x2000 != 0 {
111 ratio =
112 (ratio * U256::from_str_radix("a9f746462d870fdf8a65dc1f90e061e5", 16).unwrap()) >> 128;
113 }
114 if abs_tick & 0x4000 != 0 {
115 ratio =
116 (ratio * U256::from_str_radix("70d869a156d2a1b890bb3df62baf32f7", 16).unwrap()) >> 128;
117 }
118 if abs_tick & 0x8000 != 0 {
119 ratio =
120 (ratio * U256::from_str_radix("31be135f97d08fd981231505542fcfa6", 16).unwrap()) >> 128;
121 }
122 if abs_tick & 0x10000 != 0 {
123 ratio =
124 (ratio * U256::from_str_radix("9aa508b5b7a84e1c677de54f3e99bc9", 16).unwrap()) >> 128;
125 }
126 if abs_tick & 0x20000 != 0 {
127 ratio =
128 (ratio * U256::from_str_radix("5d6af8dedb81196699c329225ee604", 16).unwrap()) >> 128;
129 }
130 if abs_tick & 0x40000 != 0 {
131 ratio = (ratio * U256::from_str_radix("2216e584f5fa1ea926041bedfe98", 16).unwrap()) >> 128;
132 }
133 if abs_tick & 0x80000 != 0 {
134 ratio = (ratio * U256::from_str_radix("48a170391f7dc42444e8fa2", 16).unwrap()) >> 128;
135 }
136
137 if tick.is_positive() {
138 ratio = U256::MAX / ratio;
139 }
140
141 ratio = (ratio + U256::from(0xffffffffu32)) >> 32;
142 U160::from(ratio)
143}
144
145pub fn get_tick_at_sqrt_ratio(sqrt_price_x96: U160) -> i32 {
158 assert!(
159 sqrt_price_x96 >= MIN_SQRT_RATIO && sqrt_price_x96 < MAX_SQRT_RATIO,
160 "Sqrt price out of bounds"
161 );
162
163 let ratio = U256::from(sqrt_price_x96) << 32;
164 let msb = most_significant_bit(ratio);
165
166 let mut log_2_x64 = if msb >= 128 {
169 U256::from((msb - 128) as u64) << 64
170 } else {
171 U256::MAX - (U256::from((128 - msb) as u64) << 64) + U256::from(1)
173 };
174
175 let mut r = if msb >= 128 {
177 ratio >> (msb - 127)
178 } else {
179 ratio << (127 - msb)
180 };
181
182 let mut decimals = U256::ZERO;
184 for i in (50..=63).rev() {
185 r = (r * r) >> 127;
186 let f = r >> 128;
187 if f > U256::ZERO {
188 decimals |= U256::ONE << i;
189 r >>= 1;
190 }
191 }
192
193 log_2_x64 |= decimals;
195
196 let log_sqrt10001 = log_2_x64 * U256::from(255738958999603826347141u128);
200
201 let tick_low_offset =
203 U256::from_str_radix("3402992956809132418596140100660247210", 10).unwrap();
204 let tick_hi_offset =
205 U256::from_str_radix("291339464771989622907027621153398088495", 10).unwrap();
206
207 let tick_low_u256: U256 = (log_sqrt10001 - tick_low_offset) >> 128;
208 let tick_hi_u256: U256 = (log_sqrt10001 + tick_hi_offset) >> 128;
209
210 let tick_low = tick_low_u256.as_le_bytes()[0] as i32
214 | ((tick_low_u256.as_le_bytes()[1] as i32) << 8)
215 | ((tick_low_u256.as_le_bytes()[2] as i32) << 16)
216 | ((tick_low_u256.as_le_bytes()[3] as i32) << 24);
217 let tick_hi = tick_hi_u256.as_le_bytes()[0] as i32
218 | ((tick_hi_u256.as_le_bytes()[1] as i32) << 8)
219 | ((tick_hi_u256.as_le_bytes()[2] as i32) << 16)
220 | ((tick_hi_u256.as_le_bytes()[3] as i32) << 24);
221
222 if tick_low == tick_hi {
224 tick_low
225 } else if get_sqrt_ratio_at_tick(tick_hi) <= sqrt_price_x96 {
226 tick_hi
227 } else {
228 tick_low
229 }
230}
231
232#[cfg(test)]
237mod tests {
238 use std::str::FromStr;
239
240 use rstest::rstest;
241
242 use super::*;
243 use crate::defi::tick_map::sqrt_price_math::encode_sqrt_ratio_x96;
244
245 #[rstest]
246 fn test_get_sqrt_ratio_at_tick_zero() {
247 let sqrt_ratio = get_sqrt_ratio_at_tick(0);
248 let expected = U160::from(1u128) << 96;
250 assert_eq!(sqrt_ratio, expected);
251 }
252
253 #[rstest]
254 fn test_get_tick_at_sqrt_ratio() {
255 let sqrt_ratio_u160 = U160::from(1u128 << 96); let tick = get_tick_at_sqrt_ratio(sqrt_ratio_u160);
257 assert_eq!(tick, 0);
258 }
259
260 #[rstest]
261 #[should_panic(expected = "Tick 887273 out of bounds")]
262 fn test_get_sqrt_ratio_at_tick_panics_above_max() {
263 let _ = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK + 1);
264 }
265
266 #[rstest]
267 #[should_panic(expected = "Tick -887273 out of bounds")]
268 fn test_get_sqrt_ratio_at_tick_panics_below_min() {
269 let _ = get_sqrt_ratio_at_tick(PoolTick::MIN_TICK - 1);
270 }
271
272 #[rstest]
274 #[should_panic(expected = "Sqrt price out of bounds")]
275 fn test_get_tick_at_sqrt_ratio_throws_for_too_low() {
276 let _ = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO - U160::from(1));
277 }
278
279 #[rstest]
280 #[should_panic(expected = "Sqrt price out of bounds")]
281 fn test_get_tick_at_sqrt_ratio_throws_for_too_high() {
282 let _ = get_tick_at_sqrt_ratio(MAX_SQRT_RATIO);
283 }
284
285 #[rstest]
286 fn test_get_tick_at_sqrt_ratio_min_tick() {
287 let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO);
288 assert_eq!(result, PoolTick::MIN_TICK);
289 }
290
291 #[rstest]
292 fn test_get_tick_at_sqrt_ration_various_values() {
293 assert_eq!(
294 get_tick_at_sqrt_ratio(U160::from_str("511495728837967332084595714").unwrap()),
295 -100860
296 );
297 assert_eq!(
298 get_tick_at_sqrt_ratio(U160::from_str("14464772219441977173490711849216").unwrap()),
299 104148
300 );
301 assert_eq!(
302 get_tick_at_sqrt_ratio(U160::from_str("17148448136625419841777674413284").unwrap()),
303 107552
304 );
305 }
306
307 #[rstest]
308 fn test_get_tick_at_sqrt_ratio_min_tick_plus_one() {
309 let result = get_tick_at_sqrt_ratio(U160::from(4295343490u64));
310 assert_eq!(result, PoolTick::MIN_TICK + 1);
311 }
312
313 #[rstest]
314 fn test_get_tick_at_sqrt_ratio_max_tick_minus_one() {
315 let sqrt_ratio =
318 U160::from_str_radix("fffa429fbf7baeed2496f0a9f5ccf2bb4abf52f9", 16).unwrap();
319
320 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
322
323 assert_eq!(
325 result,
326 PoolTick::MAX_TICK - 1,
327 "Uniswap test value should map to MAX_TICK - 1"
328 );
329 }
330
331 #[rstest]
332 fn test_get_tick_at_sqrt_ratio_closest_to_max_tick() {
333 let sqrt_ratio = MAX_SQRT_RATIO - U160::from(1);
335 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
336
337 assert!(result > 0 && result < PoolTick::MAX_TICK);
339
340 }
343
344 #[rstest]
345 #[case::min_sqrt_ratio(MIN_SQRT_RATIO)]
346 #[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))]
358 fn test_get_tick_at_sqrt_ratio_accuracy(#[case] ratio: U160) {
359 let tick = get_tick_at_sqrt_ratio(ratio);
360
361 let ratio_f64 = ratio.to_string().parse::<f64>().unwrap();
363 let price = (ratio_f64 / (1u128 << 96) as f64).powi(2);
364 let theoretical_tick = (price.ln() / 1.0001_f64.ln()).floor() as i32;
365 let diff = (tick - theoretical_tick).abs();
366 assert!(
367 diff <= 1,
368 "Tick {} differs from theoretical {} by more than 1",
369 tick,
370 theoretical_tick
371 );
372
373 let ratio_of_tick = U256::from(get_sqrt_ratio_at_tick(tick));
375 let ratio_of_tick_plus_one = U256::from(get_sqrt_ratio_at_tick(tick + 1));
376 let ratio_u256 = U256::from(ratio);
377
378 assert!(
379 ratio_u256 >= ratio_of_tick,
380 "Ratio {} should be >= ratio of tick {}",
381 ratio_u256,
382 ratio_of_tick
383 );
384 assert!(
385 ratio_u256 < ratio_of_tick_plus_one,
386 "Ratio {} should be < ratio of tick+1 {}",
387 ratio_u256,
388 ratio_of_tick_plus_one
389 );
390 }
391
392 #[rstest]
393 fn test_get_tick_at_sqrt_ratio_specific_values() {
394 let test_cases = vec![
396 (MIN_SQRT_RATIO, PoolTick::MIN_TICK),
397 (U160::from(1u128 << 96), 0), ];
399
400 for (sqrt_ratio, expected_tick) in test_cases {
401 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
402 assert_eq!(
403 result, expected_tick,
404 "Failed for sqrt_ratio {}",
405 sqrt_ratio
406 );
407 }
408 }
409
410 #[rstest]
411 fn test_round_trip_tick_sqrt_ratio() {
412 let test_ticks = vec![
416 -887272, -100000, -1000, -100, -1, 0, 1, 100, 1000, 100000, 700000,
417 ];
418
419 for original_tick in test_ticks {
420 let sqrt_ratio = get_sqrt_ratio_at_tick(original_tick);
421
422 if sqrt_ratio < MAX_SQRT_RATIO {
424 let recovered_tick = get_tick_at_sqrt_ratio(sqrt_ratio);
425
426 assert_eq!(
428 recovered_tick, original_tick,
429 "Round trip failed: {} -> {} -> {}",
430 original_tick, sqrt_ratio, recovered_tick
431 );
432 } else {
433 println!(
436 "Tick {} produces sqrt_ratio {} which exceeds MAX_SQRT_RATIO",
437 original_tick, sqrt_ratio
438 );
439 }
440 }
441 }
442
443 #[rstest]
444 fn test_extreme_ticks_behavior() {
445 let min_sqrt = get_sqrt_ratio_at_tick(PoolTick::MIN_TICK);
446 assert_eq!(
447 min_sqrt, MIN_SQRT_RATIO,
448 "MIN_TICK should produce MIN_SQRT_RATIO"
449 );
450 let recovered_min = get_tick_at_sqrt_ratio(min_sqrt);
451 assert_eq!(
452 recovered_min,
453 PoolTick::MIN_TICK,
454 "MIN_TICK should round-trip correctly"
455 );
456
457 let max_sqrt = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK);
459
460 assert_eq!(
463 max_sqrt, MAX_SQRT_RATIO,
464 "MAX_TICK should produce exactly MAX_SQRT_RATIO"
465 );
466
467 let max_valid_sqrt = MAX_SQRT_RATIO - U160::from(1);
470 let max_valid_tick = get_tick_at_sqrt_ratio(max_valid_sqrt);
471
472 assert_eq!(
474 max_valid_tick,
475 PoolTick::MAX_TICK - 1,
476 "MAX_SQRT_RATIO - 1 should map to MAX_TICK - 1"
477 );
478 }
479}