Skip to main content

nautilus_model/defi/tick_map/
tick_math.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use alloy_primitives::{U160, U256};
17
18use crate::defi::tick_map::{bit_math::most_significant_bit, tick::PoolTick};
19
20/// The minimum value that can be returned from `get_sqrt_ratio_at_tick`
21pub const MIN_SQRT_RATIO: U160 = U160::from_limbs([4_295_128_739_u64, 0, 0]);
22
23/// The maximum value that can be returned from `get_sqrt_ratio_at_tick`
24pub const MAX_SQRT_RATIO: U160 = U160::from_limbs([
25    0x5d95_1d52_6398_8d26_u64, // Lower 64 bits
26    0xefd1_fc6a_5064_8849_u64, // Middle 64 bits
27    0xfffd_8963_u64,           // Upper 32 bits
28]);
29
30/// Returns the sqrt ratio as a Q64.96 for the given tick. The sqrt ratio is computed as
31/// sqrt(1.0001)^tick.
32///
33/// ## Arguments
34///
35/// * `tick`: the tick for which to compute the sqrt ratio
36///
37/// ## Returns
38///
39/// The sqrt ratio as a Q64.96
40///
41/// # Panics
42///
43/// Panics if the absolute tick exceeds [`PoolTick::MAX_TICK`].
44#[inline]
45#[must_use]
46pub fn get_sqrt_ratio_at_tick(tick: i32) -> U160 {
47    // Validate range before abs() to avoid overflow panic on i32::MIN
48    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    // Equivalent: ratio = 2**128 / sqrt(1.0001) if abs_tick & 0x1 else 1 << 128
55    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    // Iterate through 1th to 19th bit of abs_tick because MAX_TICK < 2**20
62    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/// Returns the tick corresponding to the given sqrt ratio.
164///
165/// Converts a sqrt price ratio (as Q64.96 fixed point) back to its corresponding
166/// tick value using logarithmic calculations. This is the inverse operation of
167/// `get_sqrt_ratio_at_tick`.
168///
169/// # Panics
170/// Panics if the sqrt price is outside the valid range:
171/// - `sqrt_price_x96 < MIN_SQRT_RATIO` (too small)
172/// - `sqrt_price_x96 >= MAX_SQRT_RATIO` (too large)
173///
174/// Valid range is approximately from tick `-887272` to `+887272`.
175#[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    // Build log_2_x64 using U256 throughout
186    // When msb < 128, we simulate negative by subtracting from 2^256
187    let mut log_2_x64 = if msb >= 128 {
188        U256::from((msb - 128) as u64) << 64
189    } else {
190        // For negative values, use two's complement representation
191        U256::MAX - (U256::from((128 - msb) as u64) << 64) + U256::from(1)
192    };
193
194    // Calculate r for iterations
195    let mut r = if msb >= 128 {
196        ratio >> (msb - 127)
197    } else {
198        ratio << (127 - msb)
199    };
200
201    // 14 iterations to compute the fractional part
202    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    // Add fractional bits to log_2_x64
214    log_2_x64 |= decimals;
215
216    // sqrt_ratio = sqrt(1.0001^tick)
217    // tick = log_{sqrt(1.0001)}(sqrt_ratio) = log_2(sqrt_ratio) / log_2(sqrt(1.0001))
218    // 2**64 / log_2(sqrt(1.0001)) = 255738958999603826347141
219    let log_sqrt10001 = log_2_x64 * U256::from(255_738_958_999_603_826_347_141_u128);
220
221    // Calculate tick bounds using wrapping arithmetic
222    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    // Convert to i32 by directly casting
231    // The values after >> 128 should fit in i32 range
232    // For negative values, the wraparound in U256 will be preserved in the cast
233    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    // Final selection
243    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        // At tick 0, price is 1, sqrt_price is 1, sqrt_price_x96 is 1 * 2^96
265        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); // sqrt price = 1, price = 1
272        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    // Tests for get_tick_at_sqrt_ratio matching the JavaScript tests
289    #[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        // Test with the exact value from Uniswap tests for MAX_TICK - 1
332        // This value is: 1461373636630004318706518188784493106690254656249
333        let sqrt_ratio =
334            U160::from_str_radix("fffa429fbf7baeed2496f0a9f5ccf2bb4abf52f9", 16).unwrap();
335
336        // This value should work now that MAX_SQRT_RATIO has been updated
337        let result = get_tick_at_sqrt_ratio(sqrt_ratio);
338
339        // This should give us MAX_TICK - 1 (887271)
340        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        // Test the actual maximum valid sqrt_ratio
350        let sqrt_ratio = MAX_SQRT_RATIO - U160::from(1);
351        let result = get_tick_at_sqrt_ratio(sqrt_ratio);
352
353        // Verify it's a valid positive tick less than MAX_TICK
354        assert!(result > 0 && result < PoolTick::MAX_TICK);
355
356        // Verify that MAX_SQRT_RATIO itself would panic (it's exclusive upper bound)
357        // This is tested in test_get_tick_at_sqrt_ratio_throws_for_too_high
358    }
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))] // 10^12 / 1
363    #[case::price_10_6_to_1(encode_sqrt_ratio_x96(1, 1_000_000))] // 10^6 / 1
364    #[case::price_1_to_64(encode_sqrt_ratio_x96(64, 1))] // 1 / 64
365    #[case::price_1_to_8(encode_sqrt_ratio_x96(8, 1))] // 1 / 8
366    #[case::price_1_to_2(encode_sqrt_ratio_x96(2, 1))] // 1 / 2
367    #[case::price_1_to_1(encode_sqrt_ratio_x96(1, 1))] // 1 / 1
368    #[case::price_2_to_1(encode_sqrt_ratio_x96(1, 2))] // 2 / 1
369    #[case::price_8_to_1(encode_sqrt_ratio_x96(1, 8))] // 8 / 1
370    #[case::price_64_to_1(encode_sqrt_ratio_x96(1, 64))] // 64 / 1
371    #[case::price_1_to_10_6(encode_sqrt_ratio_x96(1_000_000, 1))] // 1 / 10^6
372    #[case::price_1_to_10_12(encode_sqrt_ratio_x96(1_000_000_000_000, 1))] // 1 / 10^12
373    #[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        // Test 1: Check that result is at most off by 1 from theoretical value
378        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        // Test 2: Check that ratio is between tick and tick+1
388        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        // Test some specific known values
405        let test_cases = vec![
406            (MIN_SQRT_RATIO, PoolTick::MIN_TICK),
407            (U160::from(1u128 << 96), 0), // sqrt price = 1, price = 1, tick = 0
408        ];
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        // Test round trip: tick -> sqrt_ratio -> tick
419        // Note: Very high ticks (above ~790227) produce sqrt_ratios >= MAX_SQRT_RATIO,
420        // so we limit our test to ticks that produce valid sqrt_ratios
421        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            // Check if the sqrt_ratio is within bounds for get_tick_at_sqrt_ratio
429            if sqrt_ratio < MAX_SQRT_RATIO {
430                let recovered_tick = get_tick_at_sqrt_ratio(sqrt_ratio);
431
432                // Should be exact for round trip
433                assert_eq!(
434                    recovered_tick, original_tick,
435                    "Round trip failed: {original_tick} -> {sqrt_ratio} -> {recovered_tick}"
436                );
437            } else {
438                // For very high ticks, the sqrt_ratio exceeds MAX_SQRT_RATIO
439                // This is expected behavior - not all ticks can round-trip
440                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        // MAX_TICK produces a value equal to MAX_SQRT_RATIO
462        let max_sqrt = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK);
463
464        // Now that MAX_SQRT_RATIO has been updated to the actual max value,
465        // get_sqrt_ratio_at_tick(MAX_TICK) should equal MAX_SQRT_RATIO
466        assert_eq!(
467            max_sqrt, MAX_SQRT_RATIO,
468            "MAX_TICK should produce exactly MAX_SQRT_RATIO"
469        );
470
471        // The highest tick that can be passed to get_tick_at_sqrt_ratio is MAX_TICK - 1
472        // because get_tick_at_sqrt_ratio requires sqrt_price_x96 < MAX_SQRT_RATIO (exclusive)
473        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        // This should give us MAX_TICK - 1 (887271)
477        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}