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!(abs_tick <= PoolTick::MAX_TICK, "Tick {tick} out of bounds");
49
50    // Equivalent: ratio = 2**128 / sqrt(1.0001) if abs_tick & 0x1 else 1 << 128
51    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    // Iterate through 1th to 19th bit of abs_tick because MAX_TICK < 2**20
58    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
141/// Returns the tick corresponding to the given sqrt ratio.
142///
143/// Converts a sqrt price ratio (as Q64.96 fixed point) back to its corresponding
144/// tick value using logarithmic calculations. This is the inverse operation of
145/// `get_sqrt_ratio_at_tick`.
146///
147/// # Panics
148/// Panics if the sqrt price is outside the valid range:
149/// - `sqrt_price_x96 < MIN_SQRT_RATIO` (too small)
150/// - `sqrt_price_x96 >= MAX_SQRT_RATIO` (too large)
151///
152/// Valid range is approximately from tick -887272 to +887272.
153pub 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    // Build log_2_x64 using U256 throughout
163    // When msb < 128, we simulate negative by subtracting from 2^256
164    let mut log_2_x64 = if msb >= 128 {
165        U256::from((msb - 128) as u64) << 64
166    } else {
167        // For negative values, use two's complement representation
168        U256::MAX - (U256::from((128 - msb) as u64) << 64) + U256::from(1)
169    };
170
171    // Calculate r for iterations
172    let mut r = if msb >= 128 {
173        ratio >> (msb - 127)
174    } else {
175        ratio << (127 - msb)
176    };
177
178    // 14 iterations to compute the fractional part
179    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    // Add fractional bits to log_2_x64
190    log_2_x64 |= decimals;
191
192    // sqrt_ratio = sqrt(1.0001^tick)
193    // tick = log_{sqrt(1.0001)}(sqrt_ratio) = log_2(sqrt_ratio) / log_2(sqrt(1.0001))
194    // 2**64 / log_2(sqrt(1.0001)) = 255738958999603826347141
195    let log_sqrt10001 = log_2_x64 * U256::from(255738958999603826347141u128);
196
197    // Calculate tick bounds using wrapping arithmetic
198    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    // Convert to i32 by directly casting
207    // The values after >> 128 should fit in i32 range
208    // For negative values, the wraparound in U256 will be preserved in the cast
209    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    // Final selection
219    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        // At tick 0, price is 1, sqrt_price is 1, sqrt_price_x96 is 1 * 2^96
241        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); // sqrt price = 1, price = 1
248        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    // Tests for get_tick_at_sqrt_ratio matching the JavaScript tests
265    #[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        // Test with the exact value from Uniswap tests for MAX_TICK - 1
308        // This value is: 1461373636630004318706518188784493106690254656249
309        let sqrt_ratio =
310            U160::from_str_radix("fffa429fbf7baeed2496f0a9f5ccf2bb4abf52f9", 16).unwrap();
311
312        // This value should work now that MAX_SQRT_RATIO has been updated
313        let result = get_tick_at_sqrt_ratio(sqrt_ratio);
314
315        // This should give us MAX_TICK - 1 (887271)
316        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        // Test the actual maximum valid sqrt_ratio
326        let sqrt_ratio = MAX_SQRT_RATIO - U160::from(1);
327        let result = get_tick_at_sqrt_ratio(sqrt_ratio);
328
329        // Verify it's a valid positive tick less than MAX_TICK
330        assert!(result > 0 && result < PoolTick::MAX_TICK);
331
332        // Verify that MAX_SQRT_RATIO itself would panic (it's exclusive upper bound)
333        // This is tested in test_get_tick_at_sqrt_ratio_throws_for_too_high
334    }
335
336    #[rstest]
337    #[case::min_sqrt_ratio(MIN_SQRT_RATIO)]
338    #[case::price_10_12_to_1(encode_sqrt_ratio_x96(1, 1000000000000))] // 10^12 / 1
339    #[case::price_10_6_to_1(encode_sqrt_ratio_x96(1, 1000000))] // 10^6 / 1
340    #[case::price_1_to_64(encode_sqrt_ratio_x96(64, 1))] // 1 / 64
341    #[case::price_1_to_8(encode_sqrt_ratio_x96(8, 1))] // 1 / 8
342    #[case::price_1_to_2(encode_sqrt_ratio_x96(2, 1))] // 1 / 2
343    #[case::price_1_to_1(encode_sqrt_ratio_x96(1, 1))] // 1 / 1
344    #[case::price_2_to_1(encode_sqrt_ratio_x96(1, 2))] // 2 / 1
345    #[case::price_8_to_1(encode_sqrt_ratio_x96(1, 8))] // 8 / 1
346    #[case::price_64_to_1(encode_sqrt_ratio_x96(1, 64))] // 64 / 1
347    #[case::price_1_to_10_6(encode_sqrt_ratio_x96(1000000, 1))] // 1 / 10^6
348    #[case::price_1_to_10_12(encode_sqrt_ratio_x96(1000000000000, 1))] // 1 / 10^12
349    #[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        // Test 1: Check that result is at most off by 1 from theoretical value
354        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        // Test 2: Check that ratio is between tick and tick+1
364        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        // Test some specific known values
381        let test_cases = vec![
382            (MIN_SQRT_RATIO, PoolTick::MIN_TICK),
383            (U160::from(1u128 << 96), 0), // sqrt price = 1, price = 1, tick = 0
384        ];
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        // Test round trip: tick -> sqrt_ratio -> tick
395        // Note: Very high ticks (above ~790227) produce sqrt_ratios >= MAX_SQRT_RATIO,
396        // so we limit our test to ticks that produce valid sqrt_ratios
397        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            // Check if the sqrt_ratio is within bounds for get_tick_at_sqrt_ratio
405            if sqrt_ratio < MAX_SQRT_RATIO {
406                let recovered_tick = get_tick_at_sqrt_ratio(sqrt_ratio);
407
408                // Should be exact for round trip
409                assert_eq!(
410                    recovered_tick, original_tick,
411                    "Round trip failed: {original_tick} -> {sqrt_ratio} -> {recovered_tick}"
412                );
413            } else {
414                // For very high ticks, the sqrt_ratio exceeds MAX_SQRT_RATIO
415                // This is expected behavior - not all ticks can round-trip
416                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        // MAX_TICK produces a value equal to MAX_SQRT_RATIO
438        let max_sqrt = get_sqrt_ratio_at_tick(PoolTick::MAX_TICK);
439
440        // Now that MAX_SQRT_RATIO has been updated to the actual max value,
441        // get_sqrt_ratio_at_tick(MAX_TICK) should equal MAX_SQRT_RATIO
442        assert_eq!(
443            max_sqrt, MAX_SQRT_RATIO,
444            "MAX_TICK should produce exactly MAX_SQRT_RATIO"
445        );
446
447        // The highest tick that can be passed to get_tick_at_sqrt_ratio is MAX_TICK - 1
448        // because get_tick_at_sqrt_ratio requires sqrt_price_x96 < MAX_SQRT_RATIO (exclusive)
449        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        // This should give us MAX_TICK - 1 (887271)
453        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}