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([4_295_128_739_u64, 0, 0]);
22
23pub const MAX_SQRT_RATIO: U160 = U160::from_limbs([
25 0x5d95_1d52_6398_8d26_u64, 0xefd1_fc6a_5064_8849_u64, 0xfffd_8963_u64, ]);
29
30#[inline]
45#[must_use]
46pub fn get_sqrt_ratio_at_tick(tick: i32) -> U160 {
47 assert!(
49 (PoolTick::MIN_TICK..=PoolTick::MAX_TICK).contains(&tick),
50 "Tick {tick} out of bounds"
51 );
52 let abs_tick = tick.abs();
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
67 if abs_tick & 0x4 != 0 {
68 ratio =
69 (ratio * U256::from_str_radix("fff2e50f5f656932ef12357cf3c7fdcc", 16).unwrap()) >> 128;
70 }
71
72 if abs_tick & 0x8 != 0 {
73 ratio =
74 (ratio * U256::from_str_radix("ffe5caca7e10e4e61c3624eaa0941cd0", 16).unwrap()) >> 128;
75 }
76
77 if abs_tick & 0x10 != 0 {
78 ratio =
79 (ratio * U256::from_str_radix("ffcb9843d60f6159c9db58835c926644", 16).unwrap()) >> 128;
80 }
81
82 if abs_tick & 0x20 != 0 {
83 ratio =
84 (ratio * U256::from_str_radix("ff973b41fa98c081472e6896dfb254c0", 16).unwrap()) >> 128;
85 }
86
87 if abs_tick & 0x40 != 0 {
88 ratio =
89 (ratio * U256::from_str_radix("ff2ea16466c96a3843ec78b326b52861", 16).unwrap()) >> 128;
90 }
91
92 if abs_tick & 0x80 != 0 {
93 ratio =
94 (ratio * U256::from_str_radix("fe5dee046a99a2a811c461f1969c3053", 16).unwrap()) >> 128;
95 }
96
97 if abs_tick & 0x100 != 0 {
98 ratio =
99 (ratio * U256::from_str_radix("fcbe86c7900a88aedcffc83b479aa3a4", 16).unwrap()) >> 128;
100 }
101
102 if abs_tick & 0x200 != 0 {
103 ratio =
104 (ratio * U256::from_str_radix("f987a7253ac413176f2b074cf7815e54", 16).unwrap()) >> 128;
105 }
106
107 if abs_tick & 0x400 != 0 {
108 ratio =
109 (ratio * U256::from_str_radix("f3392b0822b70005940c7a398e4b70f3", 16).unwrap()) >> 128;
110 }
111
112 if abs_tick & 0x800 != 0 {
113 ratio =
114 (ratio * U256::from_str_radix("e7159475a2c29b7443b29c7fa6e889d9", 16).unwrap()) >> 128;
115 }
116
117 if abs_tick & 0x1000 != 0 {
118 ratio =
119 (ratio * U256::from_str_radix("d097f3bdfd2022b8845ad8f792aa5825", 16).unwrap()) >> 128;
120 }
121
122 if abs_tick & 0x2000 != 0 {
123 ratio =
124 (ratio * U256::from_str_radix("a9f746462d870fdf8a65dc1f90e061e5", 16).unwrap()) >> 128;
125 }
126
127 if abs_tick & 0x4000 != 0 {
128 ratio =
129 (ratio * U256::from_str_radix("70d869a156d2a1b890bb3df62baf32f7", 16).unwrap()) >> 128;
130 }
131
132 if abs_tick & 0x8000 != 0 {
133 ratio =
134 (ratio * U256::from_str_radix("31be135f97d08fd981231505542fcfa6", 16).unwrap()) >> 128;
135 }
136
137 if abs_tick & 0x10000 != 0 {
138 ratio =
139 (ratio * U256::from_str_radix("9aa508b5b7a84e1c677de54f3e99bc9", 16).unwrap()) >> 128;
140 }
141
142 if abs_tick & 0x20000 != 0 {
143 ratio =
144 (ratio * U256::from_str_radix("5d6af8dedb81196699c329225ee604", 16).unwrap()) >> 128;
145 }
146
147 if abs_tick & 0x40000 != 0 {
148 ratio = (ratio * U256::from_str_radix("2216e584f5fa1ea926041bedfe98", 16).unwrap()) >> 128;
149 }
150
151 if abs_tick & 0x80000 != 0 {
152 ratio = (ratio * U256::from_str_radix("48a170391f7dc42444e8fa2", 16).unwrap()) >> 128;
153 }
154
155 if tick.is_positive() {
156 ratio = U256::MAX / ratio;
157 }
158
159 ratio = (ratio + U256::from(0xffff_ffff_u32)) >> 32;
160 U160::from(ratio)
161}
162
163#[must_use]
176pub fn get_tick_at_sqrt_ratio(sqrt_price_x96: U160) -> i32 {
177 assert!(
178 sqrt_price_x96 >= MIN_SQRT_RATIO && sqrt_price_x96 < MAX_SQRT_RATIO,
179 "Sqrt price out of bounds"
180 );
181
182 let ratio = U256::from(sqrt_price_x96) << 32;
183 let msb = most_significant_bit(ratio);
184
185 let mut log_2_x64 = if msb >= 128 {
188 U256::from((msb - 128) as u64) << 64
189 } else {
190 U256::MAX - (U256::from((128 - msb) as u64) << 64) + U256::from(1)
192 };
193
194 let mut r = if msb >= 128 {
196 ratio >> (msb - 127)
197 } else {
198 ratio << (127 - msb)
199 };
200
201 let mut decimals = U256::ZERO;
203
204 for i in (50..=63).rev() {
205 r = (r * r) >> 127;
206 let f = r >> 128;
207 if f > U256::ZERO {
208 decimals |= U256::ONE << i;
209 r >>= 1;
210 }
211 }
212
213 log_2_x64 |= decimals;
215
216 let log_sqrt10001 = log_2_x64 * U256::from(255_738_958_999_603_826_347_141_u128);
220
221 let tick_low_offset =
223 U256::from_str_radix("3402992956809132418596140100660247210", 10).unwrap();
224 let tick_hi_offset =
225 U256::from_str_radix("291339464771989622907027621153398088495", 10).unwrap();
226
227 let tick_low_u256: U256 = (log_sqrt10001 - tick_low_offset) >> 128;
228 let tick_hi_u256: U256 = (log_sqrt10001 + tick_hi_offset) >> 128;
229
230 let tick_low = i32::from(tick_low_u256.as_le_bytes()[0])
234 | (i32::from(tick_low_u256.as_le_bytes()[1]) << 8)
235 | (i32::from(tick_low_u256.as_le_bytes()[2]) << 16)
236 | (i32::from(tick_low_u256.as_le_bytes()[3]) << 24);
237 let tick_hi = i32::from(tick_hi_u256.as_le_bytes()[0])
238 | (i32::from(tick_hi_u256.as_le_bytes()[1]) << 8)
239 | (i32::from(tick_hi_u256.as_le_bytes()[2]) << 16)
240 | (i32::from(tick_hi_u256.as_le_bytes()[3]) << 24);
241
242 if tick_low == tick_hi {
244 tick_low
245 } else if get_sqrt_ratio_at_tick(tick_hi) <= sqrt_price_x96 {
246 tick_hi
247 } else {
248 tick_low
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use std::str::FromStr;
255
256 use rstest::rstest;
257
258 use super::*;
259 use crate::defi::tick_map::sqrt_price_math::encode_sqrt_ratio_x96;
260
261 #[rstest]
262 fn test_get_sqrt_ratio_at_tick_zero() {
263 let sqrt_ratio = get_sqrt_ratio_at_tick(0);
264 let expected = U160::from(1u128) << 96;
266 assert_eq!(sqrt_ratio, expected);
267 }
268
269 #[rstest]
270 fn test_get_tick_at_sqrt_ratio() {
271 let sqrt_ratio_u160 = U160::from(1u128 << 96); let tick = get_tick_at_sqrt_ratio(sqrt_ratio_u160);
273 assert_eq!(tick, 0);
274 }
275
276 #[rstest]
277 #[should_panic(expected = "Tick 887273 out of bounds")]
278 fn test_get_sqrt_ratio_at_tick_panics_above_max() {
279 let _ = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK + 1);
280 }
281
282 #[rstest]
283 #[should_panic(expected = "Tick -887273 out of bounds")]
284 fn test_get_sqrt_ratio_at_tick_panics_below_min() {
285 let _ = get_sqrt_ratio_at_tick(PoolTick::MIN_TICK - 1);
286 }
287
288 #[rstest]
290 #[should_panic(expected = "Sqrt price out of bounds")]
291 fn test_get_tick_at_sqrt_ratio_throws_for_too_low() {
292 let _ = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO - U160::from(1));
293 }
294
295 #[rstest]
296 #[should_panic(expected = "Sqrt price out of bounds")]
297 fn test_get_tick_at_sqrt_ratio_throws_for_too_high() {
298 let _ = get_tick_at_sqrt_ratio(MAX_SQRT_RATIO);
299 }
300
301 #[rstest]
302 fn test_get_tick_at_sqrt_ratio_min_tick() {
303 let result = get_tick_at_sqrt_ratio(MIN_SQRT_RATIO);
304 assert_eq!(result, PoolTick::MIN_TICK);
305 }
306
307 #[rstest]
308 fn test_get_tick_at_sqrt_ration_various_values() {
309 assert_eq!(
310 get_tick_at_sqrt_ratio(U160::from_str("511495728837967332084595714").unwrap()),
311 -100_860
312 );
313 assert_eq!(
314 get_tick_at_sqrt_ratio(U160::from_str("14464772219441977173490711849216").unwrap()),
315 104_148
316 );
317 assert_eq!(
318 get_tick_at_sqrt_ratio(U160::from_str("17148448136625419841777674413284").unwrap()),
319 107_552
320 );
321 }
322
323 #[rstest]
324 fn test_get_tick_at_sqrt_ratio_min_tick_plus_one() {
325 let result = get_tick_at_sqrt_ratio(U160::from(4_295_343_490_u64));
326 assert_eq!(result, PoolTick::MIN_TICK + 1);
327 }
328
329 #[rstest]
330 fn test_get_tick_at_sqrt_ratio_max_tick_minus_one() {
331 let sqrt_ratio =
334 U160::from_str_radix("fffa429fbf7baeed2496f0a9f5ccf2bb4abf52f9", 16).unwrap();
335
336 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
338
339 assert_eq!(
341 result,
342 PoolTick::MAX_TICK - 1,
343 "Uniswap test value should map to MAX_TICK - 1"
344 );
345 }
346
347 #[rstest]
348 fn test_get_tick_at_sqrt_ratio_closest_to_max_tick() {
349 let sqrt_ratio = MAX_SQRT_RATIO - U160::from(1);
351 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
352
353 assert!(result > 0 && result < PoolTick::MAX_TICK);
355
356 }
359
360 #[rstest]
361 #[case::min_sqrt_ratio(MIN_SQRT_RATIO)]
362 #[case::price_10_12_to_1(encode_sqrt_ratio_x96(1, 1_000_000_000_000))] #[case::price_10_6_to_1(encode_sqrt_ratio_x96(1, 1_000_000))] #[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(1_000_000, 1))] #[case::price_1_to_10_12(encode_sqrt_ratio_x96(1_000_000_000_000, 1))] #[case::max_sqrt_ratio_minus_one(MAX_SQRT_RATIO - U160::from(1))]
374 fn test_get_tick_at_sqrt_ratio_accuracy(#[case] ratio: U160) {
375 let tick = get_tick_at_sqrt_ratio(ratio);
376
377 let ratio_f64 = ratio.to_string().parse::<f64>().unwrap();
379 let price = (ratio_f64 / (1u128 << 96) as f64).powi(2);
380 let theoretical_tick = (price.ln() / 1.0001_f64.ln()).floor() as i32;
381 let diff = (tick - theoretical_tick).abs();
382 assert!(
383 diff <= 1,
384 "Tick {tick} differs from theoretical {theoretical_tick} by more than 1"
385 );
386
387 let ratio_of_tick = U256::from(get_sqrt_ratio_at_tick(tick));
389 let ratio_of_tick_plus_one = U256::from(get_sqrt_ratio_at_tick(tick + 1));
390 let ratio_u256 = U256::from(ratio);
391
392 assert!(
393 ratio_u256 >= ratio_of_tick,
394 "Ratio {ratio_u256} should be >= ratio of tick {ratio_of_tick}"
395 );
396 assert!(
397 ratio_u256 < ratio_of_tick_plus_one,
398 "Ratio {ratio_u256} should be < ratio of tick+1 {ratio_of_tick_plus_one}"
399 );
400 }
401
402 #[rstest]
403 fn test_get_tick_at_sqrt_ratio_specific_values() {
404 let test_cases = vec![
406 (MIN_SQRT_RATIO, PoolTick::MIN_TICK),
407 (U160::from(1u128 << 96), 0), ];
409
410 for (sqrt_ratio, expected_tick) in test_cases {
411 let result = get_tick_at_sqrt_ratio(sqrt_ratio);
412 assert_eq!(result, expected_tick, "Failed for sqrt_ratio {sqrt_ratio}");
413 }
414 }
415
416 #[rstest]
417 fn test_round_trip_tick_sqrt_ratio() {
418 let test_ticks = vec![
422 -887_272, -100_000, -1000, -100, -1, 0, 1, 100, 1000, 100_000, 700_000,
423 ];
424
425 for original_tick in test_ticks {
426 let sqrt_ratio = get_sqrt_ratio_at_tick(original_tick);
427
428 if sqrt_ratio < MAX_SQRT_RATIO {
430 let recovered_tick = get_tick_at_sqrt_ratio(sqrt_ratio);
431
432 assert_eq!(
434 recovered_tick, original_tick,
435 "Round trip failed: {original_tick} -> {sqrt_ratio} -> {recovered_tick}"
436 );
437 } else {
438 println!(
441 "Tick {original_tick} produces sqrt_ratio {sqrt_ratio} which exceeds MAX_SQRT_RATIO"
442 );
443 }
444 }
445 }
446
447 #[rstest]
448 fn test_extreme_ticks_behavior() {
449 let min_sqrt = get_sqrt_ratio_at_tick(PoolTick::MIN_TICK);
450 assert_eq!(
451 min_sqrt, MIN_SQRT_RATIO,
452 "MIN_TICK should produce MIN_SQRT_RATIO"
453 );
454 let recovered_min = get_tick_at_sqrt_ratio(min_sqrt);
455 assert_eq!(
456 recovered_min,
457 PoolTick::MIN_TICK,
458 "MIN_TICK should round-trip correctly"
459 );
460
461 let max_sqrt = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK);
463
464 assert_eq!(
467 max_sqrt, MAX_SQRT_RATIO,
468 "MAX_TICK should produce exactly MAX_SQRT_RATIO"
469 );
470
471 let max_valid_sqrt = MAX_SQRT_RATIO - U160::from(1);
474 let max_valid_tick = get_tick_at_sqrt_ratio(max_valid_sqrt);
475
476 assert_eq!(
478 max_valid_tick,
479 PoolTick::MAX_TICK - 1,
480 "MAX_SQRT_RATIO - 1 should map to MAX_TICK - 1"
481 );
482 }
483}