nautilus_model/defi/pool_analysis/
compare.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
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    assert!(profiler.is_initialized, "Profiler is not initialized");
46
47    let mut all_match = true;
48    let total_ticks = snapshot.ticks.len();
49    let total_positions = snapshot.positions.len();
50
51    if snapshot.state.current_tick == profiler.state.current_tick {
52        log::info!("✓ current_tick matches: {}", snapshot.state.current_tick);
53    } else {
54        log::error!(
55            "Tick mismatch: profiler={}, compared={}",
56            profiler.state.current_tick,
57            snapshot.state.current_tick
58        );
59        all_match = false;
60    }
61
62    if snapshot.state.price_sqrt_ratio_x96 == profiler.state.price_sqrt_ratio_x96 {
63        log::info!(
64            "✓ sqrt_price_x96 matches: {}",
65            profiler.state.price_sqrt_ratio_x96,
66        );
67    } else {
68        log::error!(
69            "Sqrt ratio mismatch: profiler={}, compared={}",
70            profiler.state.price_sqrt_ratio_x96,
71            snapshot.state.price_sqrt_ratio_x96
72        );
73        all_match = false;
74    }
75
76    if snapshot.state.fee_protocol == profiler.state.fee_protocol {
77        log::info!("✓ fee_protocol matches: {}", snapshot.state.fee_protocol);
78    } else {
79        log::error!(
80            "Fee protocol mismatch: profiler={}, compared={}",
81            profiler.state.fee_protocol,
82            snapshot.state.fee_protocol
83        );
84        all_match = false;
85    }
86
87    if snapshot.state.liquidity == profiler.tick_map.liquidity {
88        log::info!("✓ liquidity matches: {}", snapshot.state.liquidity);
89    } else {
90        log::error!(
91            "Liquidity mismatch: profiler={}, compared={}",
92            profiler.tick_map.liquidity,
93            snapshot.state.liquidity
94        );
95        all_match = false;
96    }
97
98    // TODO add growth fee checking
99
100    // Check ticks
101    let mut tick_mismatches = 0;
102    for tick in &snapshot.ticks {
103        if let Some(profiler_tick) = profiler.get_tick(tick.value) {
104            let mut all_tick_fields_matching = true;
105            if profiler_tick.liquidity_net != tick.liquidity_net {
106                log::error!(
107                    "Tick {} mismatch on net liquidity: profiler={}, compared={}",
108                    tick.value,
109                    profiler_tick.liquidity_net,
110                    tick.liquidity_net
111                );
112                all_tick_fields_matching = false;
113            }
114            if profiler_tick.liquidity_gross != tick.liquidity_gross {
115                log::error!(
116                    "Tick {} mismatch on gross liquidity: profiler={}, compared={}",
117                    tick.value,
118                    profiler_tick.liquidity_gross,
119                    tick.liquidity_gross
120                );
121                all_tick_fields_matching = false;
122            }
123            // TODO add fees checking per tick
124
125            if !all_tick_fields_matching {
126                tick_mismatches += 1;
127                all_match = false;
128            }
129        } else {
130            log::error!(
131                "Tick {} not found in the profiler but provided in the compare mapping",
132                tick.value
133            );
134            all_match = false;
135        }
136    }
137
138    if tick_mismatches == 0 {
139        log::info!("✓ Provided {total_ticks} ticks with liquidity net and gross are matching");
140    }
141
142    // Check positions
143    let mut position_mismatches = 0;
144    for position in &snapshot.positions {
145        if let Some(profiler_position) =
146            profiler.get_position(&position.owner, position.tick_lower, position.tick_upper)
147        {
148            let position_key = PoolPosition::get_position_key(
149                &position.owner,
150                position.tick_lower,
151                position.tick_upper,
152            );
153            if position.liquidity != profiler_position.liquidity {
154                log::error!(
155                    "Position '{}' mismatch on liquidity: profiler={}, compared={}",
156                    position_key,
157                    profiler_position.liquidity,
158                    position.liquidity
159                );
160                position_mismatches += 1;
161            }
162            // TODO add fees and tokens owned checking
163        } else {
164            log::error!(
165                "Position {} not found in the profiler but provided in the compare mapping",
166                position.owner
167            );
168            all_match = false;
169        }
170    }
171
172    if position_mismatches == 0 {
173        log::info!("✓ Provided {total_positions} active positions with liquidity are matching");
174    } else {
175        all_match = false;
176    }
177
178    all_match
179}