nautilus_model/defi/pool_analysis/
compare.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
16//! Pool profiler state comparison utilities.
17
18use super::{position::PoolPosition, profiler::PoolProfiler};
19use crate::defi::pool_analysis::snapshot::PoolSnapshot;
20
21/// Compares a pool profiler's internal state with on-chain state to verify consistency.
22///
23/// This function validates that the profiler's tracked state matches the actual on-chain
24/// pool state by comparing global pool parameters, tick data, and position data.
25/// Any mismatches are logged as errors, while matches are logged as info.
26///
27/// # Arguments
28///
29/// * `profiler` - The pool profiler whose state should be compared
30/// * `current_tick` - The current active tick from on-chain state
31/// * `price_sqrt_ratio_x96` - The current sqrt price ratio (Q64.96 format) from on-chain state
32/// * `fee_protocol` - The protocol fee setting from on-chain state
33/// * `liquidity` - The current liquidity from on-chain state
34/// * `ticks` - Map of tick indices to their on-chain tick data
35/// * `positions` - Vector of on-chain position data
36///
37/// # Panics
38///
39/// Panics if the profiler has not been initialized
40///
41/// # Returns
42///
43/// Returns `true` if all compared values match, `false` if any mismatches are detected.
44pub fn compare_pool_profiler(profiler: &PoolProfiler, snapshot: &PoolSnapshot) -> bool {
45    if !profiler.is_initialized {
46        panic!("Profiler is not initialized");
47    }
48
49    let mut all_match = true;
50    let total_ticks = snapshot.ticks.len();
51    let total_positions = snapshot.positions.len();
52
53    if snapshot.state.current_tick != profiler.state.current_tick {
54        tracing::error!(
55            "Tick mismatch: profiler={}, compared={}",
56            profiler.state.current_tick,
57            snapshot.state.current_tick
58        );
59        all_match = false;
60    } else {
61        tracing::info!("✓ current_tick matches: {}", snapshot.state.current_tick);
62    }
63
64    if snapshot.state.price_sqrt_ratio_x96 != profiler.state.price_sqrt_ratio_x96 {
65        tracing::error!(
66            "Sqrt ratio mismatch: profiler={}, compared={}",
67            profiler.state.price_sqrt_ratio_x96,
68            snapshot.state.price_sqrt_ratio_x96
69        );
70        all_match = false;
71    } else {
72        tracing::info!(
73            "✓ sqrt_price_x96 matches: {}",
74            profiler.state.price_sqrt_ratio_x96,
75        );
76    }
77
78    if snapshot.state.fee_protocol != profiler.state.fee_protocol {
79        tracing::error!(
80            "Fee protocol mismatch: profiler={}, compared={}",
81            profiler.state.fee_protocol,
82            snapshot.state.fee_protocol
83        );
84        all_match = false;
85    } else {
86        tracing::info!("✓ fee_protocol matches: {}", snapshot.state.fee_protocol);
87    }
88
89    if snapshot.state.liquidity != profiler.tick_map.liquidity {
90        tracing::error!(
91            "Liquidity mismatch: profiler={}, compared={}",
92            profiler.tick_map.liquidity,
93            snapshot.state.liquidity
94        );
95        all_match = false;
96    } else {
97        tracing::info!("✓ liquidity matches: {}", snapshot.state.liquidity);
98    }
99
100    // TODO add growth fee checking
101
102    // Check ticks
103    let mut tick_mismatches = 0;
104    for tick in &snapshot.ticks {
105        if let Some(profiler_tick) = profiler.get_tick(tick.value) {
106            let mut all_tick_fields_matching = true;
107            if profiler_tick.liquidity_net != tick.liquidity_net {
108                tracing::error!(
109                    "Tick {} mismatch on net liquidity: profiler={}, compared={}",
110                    tick.value,
111                    profiler_tick.liquidity_net,
112                    tick.liquidity_net
113                );
114                all_tick_fields_matching = false;
115            }
116            if profiler_tick.liquidity_gross != tick.liquidity_gross {
117                tracing::error!(
118                    "Tick {} mismatch on gross liquidity: profiler={}, compared={}",
119                    tick.value,
120                    profiler_tick.liquidity_gross,
121                    tick.liquidity_gross
122                );
123                all_tick_fields_matching = false;
124            }
125            // TODO add fees checking per tick
126
127            if !all_tick_fields_matching {
128                tick_mismatches += 1;
129                all_match = false;
130            }
131        } else {
132            tracing::error!(
133                "Tick {} not found in the profiler but provided in the compare mapping",
134                tick.value
135            );
136            all_match = false;
137        }
138    }
139
140    if tick_mismatches == 0 {
141        tracing::info!(
142            "✓ Provided {} ticks with liquidity net and gross are matching",
143            total_ticks
144        );
145    }
146
147    // Check positions
148    let mut position_mismatches = 0;
149    for position in &snapshot.positions {
150        if let Some(profiler_position) =
151            profiler.get_position(&position.owner, position.tick_lower, position.tick_upper)
152        {
153            let position_key = PoolPosition::get_position_key(
154                &position.owner,
155                position.tick_lower,
156                position.tick_upper,
157            );
158            if position.liquidity != profiler_position.liquidity {
159                tracing::error!(
160                    "Position '{}' mismatch on liquidity: profiler={}, compared={}",
161                    position_key,
162                    profiler_position.liquidity,
163                    position.liquidity
164                );
165                position_mismatches += 1;
166            }
167            // TODO add fees and tokens owned checking
168        } else {
169            tracing::error!(
170                "Position {} not found in the profiler but provided in the compare mapping",
171                position.owner
172            );
173            all_match = false;
174        }
175    }
176
177    if position_mismatches == 0 {
178        tracing::info!(
179            "✓ Provided {} active positions with liquidity are matching",
180            total_positions
181        );
182    } else {
183        all_match = false;
184    }
185
186    all_match
187}