nautilus_model/defi/tick_map/
tick_math.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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([4295128739u64, 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    0x5d951d5263988d26u64, // Lower 64 bits
26    0xefd1fc6a50648849u64, // Middle 64 bits
27    0xfffd8963u64,         // 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]
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    // 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    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
145/// Returns the tick corresponding to the given sqrt ratio.
146///
147/// Converts a sqrt price ratio (as Q64.96 fixed point) back to its corresponding
148/// tick value using logarithmic calculations. This is the inverse operation of
149/// `get_sqrt_ratio_at_tick`.
150///
151/// # Panics
152/// Panics if the sqrt price is outside the valid range:
153/// - `sqrt_price_x96 < MIN_SQRT_RATIO` (too small)
154/// - `sqrt_price_x96 >= MAX_SQRT_RATIO` (too large)
155///
156/// Valid range is approximately from tick -887272 to +887272.
157pub 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    // Build log_2_x64 using U256 throughout
167    // When msb < 128, we simulate negative by subtracting from 2^256
168    let mut log_2_x64 = if msb >= 128 {
169        U256::from((msb - 128) as u64) << 64
170    } else {
171        // For negative values, use two's complement representation
172        U256::MAX - (U256::from((128 - msb) as u64) << 64) + U256::from(1)
173    };
174
175    // Calculate r for iterations
176    let mut r = if msb >= 128 {
177        ratio >> (msb - 127)
178    } else {
179        ratio << (127 - msb)
180    };
181
182    // 14 iterations to compute the fractional part
183    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    // Add fractional bits to log_2_x64
194    log_2_x64 |= decimals;
195
196    // sqrt_ratio = sqrt(1.0001^tick)
197    // tick = log_{sqrt(1.0001)}(sqrt_ratio) = log_2(sqrt_ratio) / log_2(sqrt(1.0001))
198    // 2**64 / log_2(sqrt(1.0001)) = 255738958999603826347141
199    let log_sqrt10001 = log_2_x64 * U256::from(255738958999603826347141u128);
200
201    // Calculate tick bounds using wrapping arithmetic
202    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    // Convert to i32 by directly casting
211    // The values after >> 128 should fit in i32 range
212    // For negative values, the wraparound in U256 will be preserved in the cast
213    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    // Final selection
223    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////////////////////////////////////////////////////////////////////////////////
233// Tests
234////////////////////////////////////////////////////////////////////////////////
235
236#[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        // At tick 0, price is 1, sqrt_price is 1, sqrt_price_x96 is 1 * 2^96
249        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); // sqrt price = 1, price = 1
256        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    // Tests for get_tick_at_sqrt_ratio matching the JavaScript tests
273    #[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        // Test with the exact value from Uniswap tests for MAX_TICK - 1
316        // This value is: 1461373636630004318706518188784493106690254656249
317        let sqrt_ratio =
318            U160::from_str_radix("fffa429fbf7baeed2496f0a9f5ccf2bb4abf52f9", 16).unwrap();
319
320        // This value should work now that MAX_SQRT_RATIO has been updated
321        let result = get_tick_at_sqrt_ratio(sqrt_ratio);
322
323        // This should give us MAX_TICK - 1 (887271)
324        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        // Test the actual maximum valid sqrt_ratio
334        let sqrt_ratio = MAX_SQRT_RATIO - U160::from(1);
335        let result = get_tick_at_sqrt_ratio(sqrt_ratio);
336
337        // Verify it's a valid positive tick less than MAX_TICK
338        assert!(result > 0 && result < PoolTick::MAX_TICK);
339
340        // Verify that MAX_SQRT_RATIO itself would panic (it's exclusive upper bound)
341        // This is tested in test_get_tick_at_sqrt_ratio_throws_for_too_high
342    }
343
344    #[rstest]
345    #[case::min_sqrt_ratio(MIN_SQRT_RATIO)]
346    #[case::price_10_12_to_1(encode_sqrt_ratio_x96(1, 1000000000000))] // 10^12 / 1
347    #[case::price_10_6_to_1(encode_sqrt_ratio_x96(1, 1000000))] // 10^6 / 1
348    #[case::price_1_to_64(encode_sqrt_ratio_x96(64, 1))] // 1 / 64
349    #[case::price_1_to_8(encode_sqrt_ratio_x96(8, 1))] // 1 / 8
350    #[case::price_1_to_2(encode_sqrt_ratio_x96(2, 1))] // 1 / 2
351    #[case::price_1_to_1(encode_sqrt_ratio_x96(1, 1))] // 1 / 1
352    #[case::price_2_to_1(encode_sqrt_ratio_x96(1, 2))] // 2 / 1
353    #[case::price_8_to_1(encode_sqrt_ratio_x96(1, 8))] // 8 / 1
354    #[case::price_64_to_1(encode_sqrt_ratio_x96(1, 64))] // 64 / 1
355    #[case::price_1_to_10_6(encode_sqrt_ratio_x96(1000000, 1))] // 1 / 10^6
356    #[case::price_1_to_10_12(encode_sqrt_ratio_x96(1000000000000, 1))] // 1 / 10^12
357    #[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        // Test 1: Check that result is at most off by 1 from theoretical value
362        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        // Test 2: Check that ratio is between tick and tick+1
374        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        // Test some specific known values
395        let test_cases = vec![
396            (MIN_SQRT_RATIO, PoolTick::MIN_TICK),
397            (U160::from(1u128 << 96), 0), // sqrt price = 1, price = 1, tick = 0
398        ];
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        // Test round trip: tick -> sqrt_ratio -> tick
413        // Note: Very high ticks (above ~790227) produce sqrt_ratios >= MAX_SQRT_RATIO,
414        // so we limit our test to ticks that produce valid sqrt_ratios
415        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            // Check if the sqrt_ratio is within bounds for get_tick_at_sqrt_ratio
423            if sqrt_ratio < MAX_SQRT_RATIO {
424                let recovered_tick = get_tick_at_sqrt_ratio(sqrt_ratio);
425
426                // Should be exact for round trip
427                assert_eq!(
428                    recovered_tick, original_tick,
429                    "Round trip failed: {} -> {} -> {}",
430                    original_tick, sqrt_ratio, recovered_tick
431                );
432            } else {
433                // For very high ticks, the sqrt_ratio exceeds MAX_SQRT_RATIO
434                // This is expected behavior - not all ticks can round-trip
435                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        // MAX_TICK produces a value equal to MAX_SQRT_RATIO
458        let max_sqrt = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK);
459
460        // Now that MAX_SQRT_RATIO has been updated to the actual max value,
461        // get_sqrt_ratio_at_tick(MAX_TICK) should equal MAX_SQRT_RATIO
462        assert_eq!(
463            max_sqrt, MAX_SQRT_RATIO,
464            "MAX_TICK should produce exactly MAX_SQRT_RATIO"
465        );
466
467        // The highest tick that can be passed to get_tick_at_sqrt_ratio is MAX_TICK - 1
468        // because get_tick_at_sqrt_ratio requires sqrt_price_x96 < MAX_SQRT_RATIO (exclusive)
469        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        // This should give us MAX_TICK - 1 (887271)
473        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}