nautilus_model/defi/pool_analysis/
profiler.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 profiling utilities for analyzing DeFi pool event data.
17
18use ahash::AHashMap;
19use alloy_primitives::{Address, I256, U160, U256};
20
21use crate::defi::{
22    PoolLiquidityUpdate, PoolSwap, SharedPool,
23    data::{
24        DexPoolData, PoolFeeCollect, PoolLiquidityUpdateType, block::BlockPosition,
25        flash::PoolFlash,
26    },
27    pool_analysis::{
28        position::PoolPosition,
29        quote::SwapQuote,
30        size_estimator,
31        snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
32        swap_math::compute_swap_step,
33    },
34    reporting::{BlockchainSyncReportItems, BlockchainSyncReporter},
35    tick_map::{
36        TickMap,
37        full_math::{FullMath, Q128},
38        liquidity_math::liquidity_math_add,
39        sqrt_price_math::{get_amount0_delta, get_amount1_delta, get_amounts_for_liquidity},
40        tick::{CrossedTick, PoolTick},
41        tick_math::{
42            MAX_SQRT_RATIO, MIN_SQRT_RATIO, get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio,
43        },
44    },
45};
46
47/// A DeFi pool state tracker and event processor for UniswapV3-style AMM pools.
48///
49/// The `PoolProfiler` provides complete pool state management including:
50/// - Liquidity position tracking and management.
51/// - Tick crossing and price movement simulation.
52/// - Fee accumulation and distribution tracking.
53/// - Protocol fee calculation.
54/// - Pool state validation and maintenance.
55///
56/// This profiler can both process historical events and execute new operations,
57/// making it suitable for both backtesting and simulation scenarios.
58///
59/// # Usage
60///
61/// Create a new profiler with a pool definition, initialize it with a starting price,
62/// then either process historical events or execute new pool operations to simulate
63/// trading activity and analyze pool behavior.
64#[derive(Debug, Clone)]
65#[cfg_attr(
66    feature = "python",
67    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
68)]
69pub struct PoolProfiler {
70    /// Pool definition.
71    pub pool: SharedPool,
72    /// Position tracking by position key (owner:tick_lower:tick_upper).
73    positions: AHashMap<String, PoolPosition>,
74    /// Tick map managing liquidity distribution across price ranges.
75    pub tick_map: TickMap,
76    /// Global pool state including current price, tick, and cumulative flows with fees.
77    pub state: PoolState,
78    /// Analytics counters tracking pool operations and performance metrics.
79    pub analytics: PoolAnalytics,
80    /// The block position of the last processed event.
81    pub last_processed_event: Option<BlockPosition>,
82    /// Flag indicating whether the pool has been initialized with a starting price.
83    pub is_initialized: bool,
84    /// Optional progress reporter for tracking event processing.
85    reporter: Option<BlockchainSyncReporter>,
86    /// The last block number that was reported (used for progress tracking).
87    last_reported_block: u64,
88}
89
90impl PoolProfiler {
91    /// Creates a new [`PoolProfiler`] instance for tracking pool state and events.
92    ///
93    /// # Panics
94    ///
95    /// Panics if the pool's tick spacing is not set.
96    #[must_use]
97    pub fn new(pool: SharedPool) -> Self {
98        let tick_spacing = pool.tick_spacing.expect("Pool tick spacing must be set");
99        Self {
100            pool,
101            positions: AHashMap::new(),
102            tick_map: TickMap::new(tick_spacing),
103            state: PoolState::default(),
104            analytics: PoolAnalytics::default(),
105            last_processed_event: None,
106            is_initialized: false,
107            reporter: None,
108            last_reported_block: 0,
109        }
110    }
111
112    /// Initializes the pool with a starting price and activates the profiler.
113    ///
114    /// # Panics
115    ///
116    /// This function panics if:
117    /// - Pool is already initialized (checked via `is_initialized` flag)
118    /// - Calculated tick from price doesn't match pool's `initial_tick` (if set)
119    pub fn initialize(&mut self, price_sqrt_ratio_x96: U160) {
120        if self.is_initialized {
121            panic!("Pool already initialized");
122        }
123
124        let calculated_tick = get_tick_at_sqrt_ratio(price_sqrt_ratio_x96);
125        if let Some(initial_tick) = self.pool.initial_tick {
126            assert_eq!(
127                initial_tick, calculated_tick,
128                "Calculated tick does not match pool initial tick"
129            );
130        }
131
132        tracing::info!(
133            "Initializing pool profiler with tick {} and price sqrt ratio {}",
134            calculated_tick,
135            price_sqrt_ratio_x96
136        );
137
138        self.state.current_tick = calculated_tick;
139        self.state.price_sqrt_ratio_x96 = price_sqrt_ratio_x96;
140        self.is_initialized = true;
141    }
142
143    /// Verifies that the pool has been initialized.
144    ///
145    /// # Panics
146    ///
147    /// Panics if the pool hasn't been initialized with a starting price via [`initialize()`](Self::initialize).
148    pub fn check_if_initialized(&self) {
149        if !self.is_initialized {
150            panic!("Pool is not initialized");
151        }
152    }
153
154    /// Processes a historical pool event and updates internal state.
155    ///
156    /// Handles all types of pool events (swaps, mints, burns, fee collections),
157    /// and updates the profiler's internal state accordingly. This is the main
158    /// entry point for processing historical blockchain events.
159    ///
160    /// # Errors
161    ///
162    /// This function returns an error if:
163    /// - Pool is not initialized.
164    /// - Event contains invalid data (tick ranges, amounts).
165    /// - Mathematical operations overflow.
166    pub fn process(&mut self, event: &DexPoolData) -> anyhow::Result<()> {
167        if self.check_if_already_processed(
168            event.block_number(),
169            event.transaction_index(),
170            event.log_index(),
171        ) {
172            return Ok(());
173        }
174
175        match event {
176            DexPoolData::Swap(swap) => self.process_swap(swap)?,
177            DexPoolData::LiquidityUpdate(update) => match update.kind {
178                PoolLiquidityUpdateType::Mint => self.process_mint(update)?,
179                PoolLiquidityUpdateType::Burn => self.process_burn(update)?,
180            },
181            DexPoolData::FeeCollect(collect) => self.process_collect(collect)?,
182            DexPoolData::Flash(flash) => self.process_flash(flash)?,
183        }
184        self.update_reporter_if_enabled(event.block_number());
185
186        Ok(())
187    }
188
189    // Checks if we need to skip events at or before the last processed event to prevent double-processing.
190    fn check_if_already_processed(&self, block: u64, tx_idx: u32, log_idx: u32) -> bool {
191        if let Some(last_event) = &self.last_processed_event {
192            let should_skip = block < last_event.number
193                || (block == last_event.number && tx_idx < last_event.transaction_index)
194                || (block == last_event.number
195                    && tx_idx == last_event.transaction_index
196                    && log_idx <= last_event.log_index);
197
198            if should_skip {
199                tracing::debug!(
200                    "Skipping already processed event at block {} tx {} log {}",
201                    block,
202                    tx_idx,
203                    log_idx
204                );
205            }
206            return should_skip;
207        }
208
209        false
210    }
211
212    /// Auto-updates reporter if it's enabled.
213    fn update_reporter_if_enabled(&mut self, current_block: u64) {
214        // Auto-update reporter if enabled
215        if let Some(reporter) = &mut self.reporter {
216            let blocks_processed = current_block.saturating_sub(self.last_reported_block);
217
218            if blocks_processed > 0 {
219                reporter.update(blocks_processed as usize);
220                self.last_reported_block = current_block;
221
222                if reporter.should_log_progress(current_block, current_block) {
223                    reporter.log_progress(current_block);
224                }
225            }
226        }
227    }
228
229    /// Processes a historical swap event from blockchain data.
230    ///
231    /// Replays the swap by simulating it through [`Self::simulate_swap_through_ticks`],
232    /// then verifies the simulation results against the actual event data. If mismatches
233    /// are detected (tick or liquidity), the pool state is corrected to match the event
234    /// values and warnings are logged.
235    ///
236    /// This self-healing approach ensures pool state stays synchronized with on-chain
237    /// reality even if simulation logic differs slightly from actual contract behavior.
238    ///
239    /// # Use Case
240    ///
241    /// Historical event processing when rebuilding pool state from blockchain events.
242    ///
243    /// # Errors
244    ///
245    /// This function returns an error if:
246    /// - Pool initialization checks fail.
247    /// - Swap simulation fails (see [`Self::simulate_swap_through_ticks`] errors).
248    ///
249    /// # Panics
250    ///
251    /// Panics if the pool has not been initialized.
252    pub fn process_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
253        self.check_if_initialized();
254        if self.check_if_already_processed(swap.block, swap.transaction_index, swap.log_index) {
255            return Ok(());
256        }
257
258        let zero_for_one = swap.amount0.is_positive();
259        let amount_specified = if zero_for_one {
260            swap.amount0
261        } else {
262            swap.amount1
263        };
264        // For price limit use the final sqrt price from swap, which is a
265        // good proxy to price limit
266        let sqrt_price_limit_x96 = swap.sqrt_price_x96;
267        let swap_quote =
268            self.simulate_swap_through_ticks(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
269        self.apply_swap_quote(&swap_quote);
270
271        // Verify simulation against event data - correct with event values if mismatch detected
272        if swap.tick != self.state.current_tick {
273            tracing::error!(
274                "Inconsistency in swap processing: Current tick mismatch: simulated {}, event {} on block {}",
275                self.state.current_tick,
276                swap.tick,
277                swap.block
278            );
279            self.state.current_tick = swap.tick;
280        }
281        if swap.liquidity != self.tick_map.liquidity {
282            tracing::error!(
283                "Inconsistency in swap processing: Active liquidity mismatch: simulated {}, event {} on block {}",
284                self.tick_map.liquidity,
285                swap.liquidity,
286                swap.block
287            );
288            self.tick_map.liquidity = swap.liquidity;
289        }
290
291        self.last_processed_event = Some(BlockPosition::new(
292            swap.block,
293            swap.transaction_hash.clone(),
294            swap.transaction_index,
295            swap.log_index,
296        ));
297        self.update_reporter_if_enabled(swap.block);
298        self.update_liquidity_analytics();
299
300        Ok(())
301    }
302
303    /// Executes a new simulated swap and returns the resulting event.
304    ///
305    /// This is the public API for forward simulation of swap operations. It delegates
306    /// the core swap mathematics to [`Self::simulate_swap_through_ticks`], then wraps
307    /// the results in a [`PoolSwap`] event structure with full metadata.
308    ///
309    /// # Errors
310    ///
311    /// Returns errors from [`Self::simulate_swap_through_ticks`]:
312    /// - Pool metadata missing or invalid
313    /// - Price limit violations
314    /// - Arithmetic overflow in fee or liquidity calculations
315    ///
316    /// # Panics
317    ///
318    /// This function panics if:
319    /// - Pool fee is not initialized
320    /// - Pool is not initialized
321    pub fn execute_swap(
322        &mut self,
323        sender: Address,
324        recipient: Address,
325        block: BlockPosition,
326        zero_for_one: bool,
327        amount_specified: I256,
328        sqrt_price_limit_x96: U160,
329    ) -> anyhow::Result<PoolSwap> {
330        self.check_if_initialized();
331        let swap_quote =
332            self.simulate_swap_through_ticks(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
333        self.apply_swap_quote(&swap_quote);
334
335        let swap_event = PoolSwap::new(
336            self.pool.chain.clone(),
337            self.pool.dex.clone(),
338            self.pool.instrument_id,
339            self.pool.pool_identifier,
340            block.number,
341            block.transaction_hash,
342            block.transaction_index,
343            block.log_index,
344            None,
345            sender,
346            recipient,
347            swap_quote.amount0,
348            swap_quote.amount1,
349            self.state.price_sqrt_ratio_x96,
350            self.tick_map.liquidity,
351            self.state.current_tick,
352        );
353        Ok(swap_event)
354    }
355
356    /// Core **read-only** swap simulation engine implementing UniswapV3 mathematics.
357    ///
358    /// This method performs a complete swap simulation without modifying pool state,
359    /// working entirely on stack-allocated local copies of state variables. It returns
360    /// a comprehensive [`SwapQuote`] containing all swap results and profiling data,
361    /// including a complete audit trail of crossed ticks.
362    ///
363    ///
364    /// # Algorithm Overview
365    ///
366    /// 1. **Iterative price curve traversal**: Walks through liquidity ranges until
367    ///    the input/output amount is exhausted or the price limit is reached
368    /// 2. **Tick crossing tracking**: When crossing initialized tick boundaries, records
369    ///    the crossing in `crossed_ticks` vector with complete state snapshot (tick, direction, fee growth)
370    /// 3. **Local liquidity updates**: Tracks liquidity changes in local variables by reading
371    ///    `liquidity_net` from tick map (read-only, no mutations)
372    /// 4. **Fee calculation**: Splits fees between LPs and protocol, accumulates in local variables
373    /// 5. **Quote assembly**: Returns [`SwapQuote`] with amounts, prices, fees, and crossed tick data
374    ///
375    /// # Errors
376    ///
377    /// Returns error if:
378    /// - Pool fee is not configured
379    /// - Fee growth arithmetic overflows when scaling by liquidity
380    /// - Swap step calculations fail
381    ///
382    /// # Panics
383    ///
384    /// Panics if pool is not initialized
385    pub fn simulate_swap_through_ticks(
386        &self,
387        amount_specified: I256,
388        zero_for_one: bool,
389        sqrt_price_limit_x96: U160,
390    ) -> anyhow::Result<SwapQuote> {
391        let mut current_sqrt_price = self.state.price_sqrt_ratio_x96;
392        let mut current_tick = self.state.current_tick;
393        let mut current_active_liquidity = self.tick_map.liquidity;
394        let exact_input = amount_specified.is_positive();
395        let mut amount_specified_remaining = amount_specified;
396        let mut amount_calculated = I256::ZERO;
397        let mut protocol_fee = U256::ZERO;
398        let mut lp_fee = U256::ZERO;
399        let mut crossed_ticks = Vec::new();
400        let fee_tier = self.pool.fee.expect("Pool fee should be initialized");
401        // Swapping cache variables
402        let fee_protocol = if zero_for_one {
403            // Extract lower 4 bits for token0 protocol fee
404            self.state.fee_protocol % 16
405        } else {
406            // Extract upper 4 bits for token1 protocol fee
407            self.state.fee_protocol >> 4
408        };
409
410        // Track current fee growth during swap
411        let mut current_fee_growth_global = if zero_for_one {
412            self.state.fee_growth_global_0
413        } else {
414            self.state.fee_growth_global_1
415        };
416
417        // Continue swapping as long as we haven't used the entire input/output or haven't reached the price limit
418        while amount_specified_remaining != I256::ZERO && sqrt_price_limit_x96 != current_sqrt_price
419        {
420            let sqrt_price_start_x96 = current_sqrt_price;
421
422            let (mut tick_next, initialized) = self
423                .tick_map
424                .next_initialized_tick(current_tick, zero_for_one);
425
426            // Make sure we do not overshoot MIN/MAX tick
427            tick_next = tick_next.clamp(PoolTick::MIN_TICK, PoolTick::MAX_TICK);
428
429            // Get the price for the next tick
430            let sqrt_price_next = get_sqrt_ratio_at_tick(tick_next);
431
432            // Compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
433            let sqrt_price_target = if (zero_for_one && sqrt_price_next < sqrt_price_limit_x96)
434                || (!zero_for_one && sqrt_price_next > sqrt_price_limit_x96)
435            {
436                sqrt_price_limit_x96
437            } else {
438                sqrt_price_next
439            };
440            let swap_step_result = compute_swap_step(
441                current_sqrt_price,
442                sqrt_price_target,
443                current_active_liquidity,
444                amount_specified_remaining,
445                fee_tier,
446            )?;
447
448            // Update current price to the new price after this swap step (BEFORE amount updates, matching Solidity)
449            current_sqrt_price = swap_step_result.sqrt_ratio_next_x96;
450
451            // Update amounts based on swap direction and type
452            if exact_input {
453                // For exact input swaps: subtract input amount and fees from remaining, subtract output from calculated
454                amount_specified_remaining -= FullMath::truncate_to_i256(
455                    swap_step_result.amount_in + swap_step_result.fee_amount,
456                );
457                amount_calculated -= FullMath::truncate_to_i256(swap_step_result.amount_out);
458            } else {
459                // For exact output swaps: add output to remaining, add input and fees to calculated
460                amount_specified_remaining +=
461                    FullMath::truncate_to_i256(swap_step_result.amount_out);
462                amount_calculated += FullMath::truncate_to_i256(
463                    swap_step_result.amount_in + swap_step_result.fee_amount,
464                );
465            }
466
467            // Calculate protocol fee if enabled
468            let mut step_fee_amount = swap_step_result.fee_amount;
469            if fee_protocol > 0 {
470                let protocol_fee_delta = swap_step_result.fee_amount / U256::from(fee_protocol);
471                step_fee_amount -= protocol_fee_delta;
472                protocol_fee += protocol_fee_delta;
473            }
474
475            // Accumulate LP fee (protocol fee is already deducted if it exists).
476            lp_fee += step_fee_amount;
477
478            // Update global fee tracker
479            if current_active_liquidity > 0 {
480                let fee_growth_delta =
481                    FullMath::mul_div(step_fee_amount, Q128, U256::from(current_active_liquidity))?;
482                current_fee_growth_global += fee_growth_delta;
483            }
484
485            // Shift tick if we reached the next price
486            if swap_step_result.sqrt_ratio_next_x96 == sqrt_price_next {
487                // We have swapped all the way to the boundary of the next tick.
488                // Time to handle crossing into the next tick, which may change liquidity.
489                // If the tick is initialized, run the tick transition logic (liquidity changes, fee accumulators, etc.).
490                if initialized {
491                    crossed_ticks.push(CrossedTick::new(
492                        tick_next,
493                        zero_for_one,
494                        if zero_for_one {
495                            current_fee_growth_global
496                        } else {
497                            self.state.fee_growth_global_0
498                        },
499                        if zero_for_one {
500                            self.state.fee_growth_global_1
501                        } else {
502                            current_fee_growth_global
503                        },
504                    ));
505
506                    // Update local liquidity tracking when crossing ticks
507                    if let Some(tick_data) = self.tick_map.get_tick(tick_next) {
508                        let liquidity_net = tick_data.liquidity_net;
509                        current_active_liquidity = if zero_for_one {
510                            liquidity_math_add(current_active_liquidity, -liquidity_net)
511                        } else {
512                            liquidity_math_add(current_active_liquidity, liquidity_net)
513                        };
514                    }
515                }
516
517                current_tick = if zero_for_one {
518                    tick_next - 1
519                } else {
520                    tick_next
521                };
522            } else if swap_step_result.sqrt_ratio_next_x96 != sqrt_price_start_x96 {
523                // The price moved during this swap step, but didn't reach a tick boundary.
524                // So, update the tick to match the new price.
525                current_tick = get_tick_at_sqrt_ratio(current_sqrt_price);
526            }
527        }
528
529        // Calculate final amounts
530        let (amount0, amount1) = if zero_for_one == exact_input {
531            (
532                amount_specified - amount_specified_remaining,
533                amount_calculated,
534            )
535        } else {
536            (
537                amount_calculated,
538                amount_specified - amount_specified_remaining,
539            )
540        };
541
542        let quote = SwapQuote::new(
543            self.pool.instrument_id,
544            amount0,
545            amount1,
546            self.state.price_sqrt_ratio_x96,
547            current_sqrt_price,
548            self.state.current_tick,
549            current_tick,
550            current_active_liquidity,
551            current_fee_growth_global,
552            lp_fee,
553            protocol_fee,
554            crossed_ticks,
555        );
556        Ok(quote)
557    }
558
559    /// Applies a swap quote to the pool state (mutations only, no simulation).
560    ///
561    /// This private method takes a [`SwapQuote`] generated by [`Self::simulate_swap_through_ticks`]
562    /// and applies its state changes to the pool, including:
563    /// - Price and tick updates
564    /// - Fee growth and protocol fee accumulation
565    /// - Tick crossing mutations (updating tick fee accumulators and active liquidity)
566    pub fn apply_swap_quote(&mut self, swap_quote: &SwapQuote) {
567        // Update price and tick.
568        self.state.current_tick = swap_quote.tick_after;
569        self.state.price_sqrt_ratio_x96 = swap_quote.sqrt_price_after_x96;
570
571        // Update fee growth and protocol fees based on swap direction.
572        if swap_quote.zero_for_one() {
573            self.state.fee_growth_global_0 = swap_quote.fee_growth_global_after;
574            self.state.protocol_fees_token0 += swap_quote.protocol_fee;
575        } else {
576            self.state.fee_growth_global_1 = swap_quote.fee_growth_global_after;
577            self.state.protocol_fees_token1 += swap_quote.protocol_fee;
578        }
579
580        // Apply tick crossings efficiently - only update crossed ticks
581        for crossed in &swap_quote.crossed_ticks {
582            let liquidity_net =
583                self.tick_map
584                    .cross_tick(crossed.tick, crossed.fee_growth_0, crossed.fee_growth_1);
585
586            // Update active liquidity based on crossing direction
587            self.tick_map.liquidity = if crossed.zero_for_one {
588                liquidity_math_add(self.tick_map.liquidity, -liquidity_net)
589            } else {
590                liquidity_math_add(self.tick_map.liquidity, liquidity_net)
591            };
592        }
593        self.analytics.total_swaps += 1;
594
595        debug_assert_eq!(
596            self.tick_map.liquidity, swap_quote.liquidity_after,
597            "Liquidity mismatch in apply_swap_quote: computed={}, quote={}",
598            self.tick_map.liquidity, swap_quote.liquidity_after
599        );
600    }
601
602    /// Returns a comprehensive swap quote without modifying pool state.
603    ///
604    /// This method simulates a swap and provides detailed profiling metrics including:
605    /// - Amounts of tokens that would be exchanged
606    /// - Price before and after the swap
607    /// - Fee breakdown (LP fees and protocol fees)
608    /// - List of crossed ticks with state snapshots
609    ///
610    /// # Errors
611    ///
612    /// Returns error if:
613    /// - Pool fee is not configured
614    /// - Fee growth arithmetic overflows when scaling by liquidity
615    /// - Swap step calculations fail
616    ///
617    /// # Panics
618    ///
619    /// Panics if pool is not initialized
620    pub fn quote_swap(
621        &self,
622        amount_specified: I256,
623        zero_for_one: bool,
624        sqrt_price_limit_x96: Option<U160>,
625    ) -> anyhow::Result<SwapQuote> {
626        self.check_if_initialized();
627        if amount_specified.is_zero() {
628            anyhow::bail!("Cannot quote swap with zero amount");
629        }
630
631        if let Some(price_limit) = sqrt_price_limit_x96 {
632            self.validate_price_limit(price_limit, zero_for_one)?;
633        }
634
635        let limit = sqrt_price_limit_x96.unwrap_or_else(|| {
636            if zero_for_one {
637                MIN_SQRT_RATIO + U160::from(1)
638            } else {
639                MAX_SQRT_RATIO - U160::from(1)
640            }
641        });
642
643        self.simulate_swap_through_ticks(amount_specified, zero_for_one, limit)
644    }
645
646    /// Simulates an exact input swap (know input amount, calculate output amount).
647    ///
648    /// # Errors
649    /// Returns error if pool is not initialized, input is zero, or price limit is invalid
650    pub fn swap_exact_in(
651        &self,
652        amount_in: U256,
653        zero_for_one: bool,
654        sqrt_price_limit_x96: Option<U160>,
655    ) -> anyhow::Result<SwapQuote> {
656        // Positive = exact input.
657        let amount_specified = I256::from(amount_in);
658        let quote = self.quote_swap(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
659
660        Ok(quote)
661    }
662
663    /// Simulates an exact output swap (know output amount, calculate required input amount).
664    ///
665    /// # Errors
666    /// Returns error if pool is not initialized, output is zero, price limit is invalid,
667    /// or insufficient liquidity exists to fulfill the exact output amount
668    pub fn swap_exact_out(
669        &self,
670        amount_out: U256,
671        zero_for_one: bool,
672        sqrt_price_limit_x96: Option<U160>,
673    ) -> anyhow::Result<SwapQuote> {
674        // Negative = exact output.
675        let amount_specified = -I256::from(amount_out);
676        let quote = self.quote_swap(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
677        quote.validate_exact_output(amount_out)?;
678
679        Ok(quote)
680    }
681
682    /// Simulates a swap to move the pool price down to a target price.
683    ///
684    /// # Errors
685    /// Returns error if pool is not initialized or price limit is invalid.
686    pub fn swap_to_lower_sqrt_price(
687        &self,
688        sqrt_price_limit_x96: U160,
689    ) -> anyhow::Result<SwapQuote> {
690        self.quote_swap(I256::MAX, true, Some(sqrt_price_limit_x96))
691    }
692
693    /// Simulates a swap to move the pool price up to a target price.
694    ///
695    /// # Errors
696    /// Returns error if pool is not initialized or price limit is invalid.
697    pub fn swap_to_higher_sqrt_price(
698        &self,
699        sqrt_price_limit_x96: U160,
700    ) -> anyhow::Result<SwapQuote> {
701        self.quote_swap(I256::MAX, false, Some(sqrt_price_limit_x96))
702    }
703
704    /// Finds the maximum trade size that produces a target slippage (including fees).
705    ///
706    /// Uses binary search to find the largest trade size that results in slippage
707    /// at or below the target. The method iteratively simulates swaps at different
708    /// sizes until it converges to the optimal size within the specified tolerance.
709    ///
710    /// # Returns
711    /// The maximum trade size (U256) that produces the target slippage
712    ///
713    /// # Errors
714    /// Returns error if:
715    /// - Impact is zero or exceeds 100% (10000 bps)
716    /// - Pool is not initialized
717    /// - Swap simulations fail
718    ///
719    /// # Panics
720    /// Panics if pool is not initialized
721    pub fn size_for_impact_bps(&self, impact_bps: u32, zero_for_one: bool) -> anyhow::Result<U256> {
722        let config = size_estimator::EstimationConfig::default();
723        size_estimator::size_for_impact_bps(self, impact_bps, zero_for_one, &config)
724    }
725
726    /// Finds the maximum trade size with comprehensive search diagnostics.
727    /// This is the detailed version of [`Self::size_for_impact_bps`] that returns
728    /// extensive information about the search process.It is useful for debugging,
729    /// monitoring, and analyzing search behavior in production.
730    ///
731    /// # Returns
732    /// Detailed result with size and comprehensive search diagnostics
733    ///
734    /// # Errors
735    /// Returns error if:
736    /// - Impact is zero or exceeds 100% (10000 bps)
737    /// - Pool is not initialized
738    /// - Swap simulations fail
739    pub fn size_for_impact_bps_detailed(
740        &self,
741        impact_bps: u32,
742        zero_for_one: bool,
743    ) -> anyhow::Result<size_estimator::SizeForImpactResult> {
744        let config = size_estimator::EstimationConfig::default();
745        size_estimator::size_for_impact_bps_detailed(self, impact_bps, zero_for_one, &config)
746    }
747
748    /// Validates that the price limit is in the correct direction for the swap.
749    ///
750    /// # Errors
751    /// Returns error if price limit violates swap direction constraints.
752    fn validate_price_limit(
753        &self,
754        limit_price_sqrt: U160,
755        zero_for_one: bool,
756    ) -> anyhow::Result<()> {
757        if zero_for_one {
758            // Swapping token0 for token1: price must decrease
759            if limit_price_sqrt >= self.state.price_sqrt_ratio_x96 {
760                anyhow::bail!("Price limit must be less than current price for zero_for_one swaps");
761            }
762        } else {
763            // Swapping token1 for token0: price must increase
764            if limit_price_sqrt <= self.state.price_sqrt_ratio_x96 {
765                anyhow::bail!(
766                    "Price limit must be greater than current price for one_for_zero swaps"
767                );
768            }
769        }
770
771        Ok(())
772    }
773
774    /// Processes a mint (liquidity add) event from historical data.
775    ///
776    /// Updates pool state when liquidity is added to a position, validates ticks,
777    /// and delegates to internal liquidity management methods.
778    ///
779    /// # Errors
780    ///
781    /// This function returns an error if:
782    /// - Pool is not initialized.
783    /// - Tick range is invalid or not properly spaced.
784    /// - Position updates fail.
785    pub fn process_mint(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
786        self.check_if_initialized();
787        if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
788        {
789            return Ok(());
790        }
791
792        self.validate_ticks(update.tick_lower, update.tick_upper)?;
793        self.add_liquidity(
794            &update.owner,
795            update.tick_lower,
796            update.tick_upper,
797            update.position_liquidity,
798            update.amount0,
799            update.amount1,
800        )?;
801
802        self.analytics.total_mints += 1;
803        self.last_processed_event = Some(BlockPosition::new(
804            update.block,
805            update.transaction_hash.clone(),
806            update.transaction_index,
807            update.log_index,
808        ));
809        self.update_reporter_if_enabled(update.block);
810        self.update_liquidity_analytics();
811
812        Ok(())
813    }
814
815    /// Internal helper to add liquidity to a position.
816    ///
817    /// Updates position state, tracks deposited amounts, and manages tick maps.
818    /// Called by both historical event processing and simulated operations.
819    fn add_liquidity(
820        &mut self,
821        owner: &Address,
822        tick_lower: i32,
823        tick_upper: i32,
824        liquidity: u128,
825        amount0: U256,
826        amount1: U256,
827    ) -> anyhow::Result<()> {
828        self.update_position(
829            owner,
830            tick_lower,
831            tick_upper,
832            liquidity as i128,
833            amount0,
834            amount1,
835        )?;
836
837        // Track deposited amounts
838        self.analytics.total_amount0_deposited += amount0;
839        self.analytics.total_amount1_deposited += amount1;
840
841        Ok(())
842    }
843
844    /// Executes a simulated mint (liquidity addition) operation.
845    ///
846    /// Calculates required token amounts for the specified liquidity amount,
847    /// updates pool state, and returns the resulting mint event.
848    ///
849    /// # Errors
850    ///
851    /// This function returns an error if:
852    /// - Pool is not initialized.
853    /// - Tick range is invalid.
854    /// - Amount calculations fail.
855    ///
856    /// # Panics
857    ///
858    /// Panics if the current sqrt price has not been initialized.
859    pub fn execute_mint(
860        &mut self,
861        recipient: Address,
862        block: BlockPosition,
863        tick_lower: i32,
864        tick_upper: i32,
865        liquidity: u128,
866    ) -> anyhow::Result<PoolLiquidityUpdate> {
867        self.check_if_initialized();
868        self.validate_ticks(tick_lower, tick_upper)?;
869        let (amount0, amount1) = get_amounts_for_liquidity(
870            self.state.price_sqrt_ratio_x96,
871            tick_lower,
872            tick_upper,
873            liquidity,
874            true,
875        );
876        self.add_liquidity(
877            &recipient, tick_lower, tick_upper, liquidity, amount0, amount1,
878        )?;
879
880        self.analytics.total_mints += 1;
881        let event = PoolLiquidityUpdate::new(
882            self.pool.chain.clone(),
883            self.pool.dex.clone(),
884            self.pool.instrument_id,
885            self.pool.pool_identifier,
886            PoolLiquidityUpdateType::Mint,
887            block.number,
888            block.transaction_hash,
889            block.transaction_index,
890            block.log_index,
891            None,
892            recipient,
893            liquidity,
894            amount0,
895            amount1,
896            tick_lower,
897            tick_upper,
898            None,
899        );
900
901        Ok(event)
902    }
903
904    /// Processes a burn (liquidity removal) event from historical data.
905    ///
906    /// Updates pool state when liquidity is removed from a position. Uses negative
907    /// liquidity delta to reduce the position size and tracks withdrawn amounts.
908    ///
909    /// # Errors
910    ///
911    /// This function returns an error if:
912    /// - Pool is not initialized.
913    /// - Tick range is invalid.
914    /// - Position updates fail.
915    pub fn process_burn(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
916        self.check_if_initialized();
917        if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
918        {
919            return Ok(());
920        }
921        self.validate_ticks(update.tick_lower, update.tick_upper)?;
922
923        // Update the position with a negative liquidity delta for the burn.
924        self.update_position(
925            &update.owner,
926            update.tick_lower,
927            update.tick_upper,
928            -(update.position_liquidity as i128),
929            update.amount0,
930            update.amount1,
931        )?;
932
933        self.analytics.total_burns += 1;
934        self.last_processed_event = Some(BlockPosition::new(
935            update.block,
936            update.transaction_hash.clone(),
937            update.transaction_index,
938            update.log_index,
939        ));
940        self.update_reporter_if_enabled(update.block);
941        self.update_liquidity_analytics();
942
943        Ok(())
944    }
945
946    /// Executes a simulated burn (liquidity removal) operation.
947    ///
948    /// Calculates token amounts that would be withdrawn for the specified liquidity,
949    /// updates pool state, and returns the resulting burn event.
950    ///
951    /// # Errors
952    ///
953    /// This function returns an error if:
954    /// - Pool is not initialized.
955    /// - Tick range is invalid.
956    /// - Amount calculations fail.
957    /// - Insufficient liquidity in position.
958    ///
959    /// # Panics
960    ///
961    /// Panics if the current sqrt price has not been initialized.
962    pub fn execute_burn(
963        &mut self,
964        recipient: Address,
965        block: BlockPosition,
966        tick_lower: i32,
967        tick_upper: i32,
968        liquidity: u128,
969    ) -> anyhow::Result<PoolLiquidityUpdate> {
970        self.check_if_initialized();
971        self.validate_ticks(tick_lower, tick_upper)?;
972        let (amount0, amount1) = get_amounts_for_liquidity(
973            self.state.price_sqrt_ratio_x96,
974            tick_lower,
975            tick_upper,
976            liquidity,
977            false,
978        );
979
980        // Update the position with a negative liquidity delta for the burn
981        self.update_position(
982            &recipient,
983            tick_lower,
984            tick_upper,
985            -(liquidity as i128),
986            amount0,
987            amount1,
988        )?;
989
990        self.analytics.total_burns += 1;
991        let event = PoolLiquidityUpdate::new(
992            self.pool.chain.clone(),
993            self.pool.dex.clone(),
994            self.pool.instrument_id,
995            self.pool.pool_identifier,
996            PoolLiquidityUpdateType::Burn,
997            block.number,
998            block.transaction_hash,
999            block.transaction_index,
1000            block.log_index,
1001            None,
1002            recipient,
1003            liquidity,
1004            amount0,
1005            amount1,
1006            tick_lower,
1007            tick_upper,
1008            None,
1009        );
1010
1011        Ok(event)
1012    }
1013
1014    /// Processes a fee collect event from historical data.
1015    ///
1016    /// Updates position state when accumulated fees are collected. Finds the
1017    /// position and delegates fee collection to the position object.
1018    ///
1019    /// Note: Tick validation is intentionally skipped to match Uniswap V3 behavior.
1020    /// Invalid positions have no fees to collect, so they're silently ignored.
1021    ///
1022    /// # Errors
1023    ///
1024    /// This function returns an error if:
1025    /// - Pool is not initialized.
1026    pub fn process_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
1027        self.check_if_initialized();
1028        if self.check_if_already_processed(
1029            collect.block,
1030            collect.transaction_index,
1031            collect.log_index,
1032        ) {
1033            return Ok(());
1034        }
1035        let position_key =
1036            PoolPosition::get_position_key(&collect.owner, collect.tick_lower, collect.tick_upper);
1037        if let Some(position) = self.positions.get_mut(&position_key) {
1038            position.collect_fees(collect.amount0, collect.amount1);
1039        }
1040
1041        // Cleanup position if it became empty after collecting all fees
1042        self.cleanup_position_if_empty(&position_key);
1043
1044        self.analytics.total_amount0_collected += U256::from(collect.amount0);
1045        self.analytics.total_amount1_collected += U256::from(collect.amount1);
1046
1047        self.analytics.total_fee_collects += 1;
1048        self.last_processed_event = Some(BlockPosition::new(
1049            collect.block,
1050            collect.transaction_hash.clone(),
1051            collect.transaction_index,
1052            collect.log_index,
1053        ));
1054        self.update_reporter_if_enabled(collect.block);
1055        self.update_liquidity_analytics();
1056
1057        Ok(())
1058    }
1059
1060    /// Processes a flash loan event from historical data.
1061    ///
1062    /// # Errors
1063    ///
1064    /// Returns an error if:
1065    /// - Pool has no active liquidity.
1066    /// - Fee growth arithmetic overflows.
1067    ///
1068    /// # Panics
1069    ///
1070    /// Panics if the pool has not been initialized.
1071    pub fn process_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
1072        self.check_if_initialized();
1073        if self.check_if_already_processed(flash.block, flash.transaction_index, flash.log_index) {
1074            return Ok(());
1075        }
1076
1077        self.update_flash_state(flash.paid0, flash.paid1)?;
1078
1079        self.analytics.total_flashes += 1;
1080        self.last_processed_event = Some(BlockPosition::new(
1081            flash.block,
1082            flash.transaction_hash.clone(),
1083            flash.transaction_index,
1084            flash.log_index,
1085        ));
1086        self.update_reporter_if_enabled(flash.block);
1087        self.update_liquidity_analytics();
1088
1089        Ok(())
1090    }
1091
1092    /// Executes a simulated flash loan operation and returns the resulting event.
1093    ///
1094    /// # Errors
1095    ///
1096    /// Returns an error if:
1097    /// - Mathematical operations overflow when calculating fees.
1098    /// - Pool has no active liquidity.
1099    /// - Fee growth arithmetic overflows.
1100    ///
1101    /// # Panics
1102    ///
1103    /// Panics if:
1104    /// - Pool is not initialized
1105    /// - Pool fee is not set
1106    pub fn execute_flash(
1107        &mut self,
1108        sender: Address,
1109        recipient: Address,
1110        block: BlockPosition,
1111        amount0: U256,
1112        amount1: U256,
1113    ) -> anyhow::Result<PoolFlash> {
1114        self.check_if_initialized();
1115        let fee_tier = self.pool.fee.expect("Pool fee should be initialized");
1116
1117        // Calculate fees or paid0/paid1
1118        let paid0 = if amount0 > U256::ZERO {
1119            FullMath::mul_div_rounding_up(amount0, U256::from(fee_tier), U256::from(1_000_000))?
1120        } else {
1121            U256::ZERO
1122        };
1123
1124        let paid1 = if amount1 > U256::ZERO {
1125            FullMath::mul_div_rounding_up(amount1, U256::from(fee_tier), U256::from(1_000_000))?
1126        } else {
1127            U256::ZERO
1128        };
1129
1130        self.update_flash_state(paid0, paid1)?;
1131        self.analytics.total_flashes += 1;
1132
1133        let flash_event = PoolFlash::new(
1134            self.pool.chain.clone(),
1135            self.pool.dex.clone(),
1136            self.pool.instrument_id,
1137            self.pool.pool_identifier,
1138            block.number,
1139            block.transaction_hash,
1140            block.transaction_index,
1141            block.log_index,
1142            None,
1143            sender,
1144            recipient,
1145            amount0,
1146            amount1,
1147            paid0,
1148            paid1,
1149        );
1150
1151        Ok(flash_event)
1152    }
1153
1154    /// Core flash loan state update logic.
1155    ///
1156    /// # Errors
1157    ///
1158    /// Returns error if:
1159    /// - No active liquidity in pool
1160    /// - Fee growth arithmetic overflows
1161    fn update_flash_state(&mut self, paid0: U256, paid1: U256) -> anyhow::Result<()> {
1162        let liquidity = self.tick_map.liquidity;
1163        if liquidity == 0 {
1164            anyhow::bail!("No liquidity")
1165        }
1166
1167        let fee_protocol_0 = self.state.fee_protocol % 16;
1168        let fee_protocol_1 = self.state.fee_protocol >> 4;
1169
1170        // Process token0 fees
1171        if paid0 > U256::ZERO {
1172            let protocol_fee_0 = if fee_protocol_0 > 0 {
1173                paid0 / U256::from(fee_protocol_0)
1174            } else {
1175                U256::ZERO
1176            };
1177
1178            if protocol_fee_0 > U256::ZERO {
1179                self.state.protocol_fees_token0 += protocol_fee_0;
1180            }
1181
1182            let lp_fee_0 = paid0 - protocol_fee_0;
1183            let delta = FullMath::mul_div(lp_fee_0, Q128, U256::from(liquidity))?;
1184            self.state.fee_growth_global_0 += delta;
1185        }
1186
1187        // Process token1 fees
1188        if paid1 > U256::ZERO {
1189            let protocol_fee_1 = if fee_protocol_1 > 0 {
1190                paid1 / U256::from(fee_protocol_1)
1191            } else {
1192                U256::ZERO
1193            };
1194
1195            if protocol_fee_1 > U256::ZERO {
1196                self.state.protocol_fees_token1 += protocol_fee_1;
1197            }
1198
1199            let lp_fee_1 = paid1 - protocol_fee_1;
1200            let delta = FullMath::mul_div(lp_fee_1, Q128, U256::from(liquidity))?;
1201            self.state.fee_growth_global_1 += delta;
1202        }
1203
1204        Ok(())
1205    }
1206
1207    /// Updates position state and tick maps when liquidity changes.
1208    ///
1209    /// Core internal method that handles position updates for both mints and burns.
1210    /// Updates tick maps, position tracking, fee growth, and active liquidity.
1211    fn update_position(
1212        &mut self,
1213        owner: &Address,
1214        tick_lower: i32,
1215        tick_upper: i32,
1216        liquidity_delta: i128,
1217        amount0: U256,
1218        amount1: U256,
1219    ) -> anyhow::Result<()> {
1220        let current_tick = self.state.current_tick;
1221        let position_key = PoolPosition::get_position_key(owner, tick_lower, tick_upper);
1222        let position = self
1223            .positions
1224            .entry(position_key)
1225            .or_insert(PoolPosition::new(*owner, tick_lower, tick_upper, 0));
1226
1227        // Only validate when burning (negative liquidity_delta)
1228        if liquidity_delta < 0 {
1229            let burn_amount = liquidity_delta.unsigned_abs();
1230            if position.liquidity < burn_amount {
1231                anyhow::bail!(
1232                    "Position liquidity {} is less than the requested burn amount of {}",
1233                    position.liquidity,
1234                    burn_amount
1235                );
1236            }
1237        }
1238
1239        // Update tickmaps.
1240        let flipped_lower = self.tick_map.update(
1241            tick_lower,
1242            current_tick,
1243            liquidity_delta,
1244            false,
1245            self.state.fee_growth_global_0,
1246            self.state.fee_growth_global_1,
1247        );
1248        let flipped_upper = self.tick_map.update(
1249            tick_upper,
1250            current_tick,
1251            liquidity_delta,
1252            true,
1253            self.state.fee_growth_global_0,
1254            self.state.fee_growth_global_1,
1255        );
1256
1257        let (fee_growth_inside_0, fee_growth_inside_1) = self.tick_map.get_fee_growth_inside(
1258            tick_lower,
1259            tick_upper,
1260            current_tick,
1261            self.state.fee_growth_global_0,
1262            self.state.fee_growth_global_1,
1263        );
1264        position.update_liquidity(liquidity_delta);
1265        position.update_fees(fee_growth_inside_0, fee_growth_inside_1);
1266        position.update_amounts(liquidity_delta, amount0, amount1);
1267
1268        // Update active liquidity if this position spans the current tick
1269        if tick_lower <= current_tick && current_tick < tick_upper {
1270            self.tick_map.liquidity = ((self.tick_map.liquidity as i128) + liquidity_delta) as u128;
1271        }
1272
1273        // Clear the ticks if they are flipped and burned
1274        if liquidity_delta < 0 && flipped_lower {
1275            self.tick_map.clear(tick_lower);
1276        }
1277        if liquidity_delta < 0 && flipped_upper {
1278            self.tick_map.clear(tick_upper);
1279        }
1280
1281        Ok(())
1282    }
1283
1284    /// Removes position from tracking if it's completely empty.
1285    ///
1286    /// This prevents accumulation of positions in the memory that are not used anymore.
1287    fn cleanup_position_if_empty(&mut self, position_key: &str) {
1288        if let Some(position) = self.positions.get(position_key)
1289            && position.is_empty()
1290        {
1291            tracing::debug!(
1292                "CLEANING UP EMPTY POSITION: owner={}, ticks=[{}, {}]",
1293                position.owner,
1294                position.tick_lower,
1295                position.tick_upper,
1296            );
1297            self.positions.remove(position_key);
1298        }
1299    }
1300
1301    /// Calculates the liquidity utilization rate for the pool.
1302    ///
1303    /// The utilization rate measures what percentage of total deployed liquidity
1304    /// is currently active (in-range and earning fees) at the current price tick.
1305    pub fn liquidity_utilization_rate(&self) -> f64 {
1306        let total_liquidity = self.get_total_liquidity();
1307        let active_liquidity = self.get_active_liquidity();
1308
1309        if total_liquidity == U256::ZERO {
1310            return 0.0;
1311        }
1312
1313        // 6 decimal places
1314        const PRECISION: u32 = 1_000_000;
1315        let ratio = FullMath::mul_div(
1316            U256::from(active_liquidity),
1317            U256::from(PRECISION),
1318            total_liquidity,
1319        )
1320        .unwrap_or(U256::ZERO);
1321
1322        // Safe to cast to u64: Since active_liquidity <= total_liquidity,
1323        // the ratio is guaranteed to be <= PRECISION (1_000_000), which fits in u64
1324        ratio.to::<u64>() as f64 / PRECISION as f64
1325    }
1326
1327    /// Validates tick range for position operations.
1328    ///
1329    /// Ensures ticks are properly ordered, aligned to tick spacing, and within
1330    /// valid bounds. Used by all position-related operations.
1331    ///
1332    /// # Errors
1333    ///
1334    /// This function returns an error if:
1335    /// - `tick_lower >= tick_upper` (invalid range).
1336    /// - Ticks are not multiples of pool's tick spacing.
1337    /// - Ticks are outside MIN_TICK/MAX_TICK bounds.
1338    fn validate_ticks(&self, tick_lower: i32, tick_upper: i32) -> anyhow::Result<()> {
1339        if tick_lower >= tick_upper {
1340            anyhow::bail!("Invalid tick range: {tick_lower} >= {tick_upper}")
1341        }
1342
1343        if tick_lower % self.pool.tick_spacing.unwrap() as i32 != 0
1344            || tick_upper % self.pool.tick_spacing.unwrap() as i32 != 0
1345        {
1346            anyhow::bail!(
1347                "Ticks {tick_lower} and {tick_upper} must be multiples of the tick spacing"
1348            )
1349        }
1350
1351        if tick_lower < PoolTick::MIN_TICK || tick_upper > PoolTick::MAX_TICK {
1352            anyhow::bail!("Invalid tick bounds for {tick_lower} and {tick_upper}");
1353        }
1354        Ok(())
1355    }
1356
1357    /// Updates all liquidity analytics.
1358    fn update_liquidity_analytics(&mut self) {
1359        self.analytics.liquidity_utilization_rate = self.liquidity_utilization_rate();
1360    }
1361
1362    /// Returns the pool's active liquidity tracked by the tick map.
1363    ///
1364    /// This represents the effective liquidity available for trading at the current price.
1365    /// The tick map maintains this value efficiently by updating it during tick crossings
1366    /// as the price moves through different ranges.
1367    ///
1368    /// # Returns
1369    /// The active liquidity (u128) at the current tick from the tick map
1370    #[must_use]
1371    pub fn get_active_liquidity(&self) -> u128 {
1372        self.tick_map.liquidity
1373    }
1374
1375    /// Calculates total liquidity by summing all individual positions at the current tick.
1376    ///
1377    /// This computes liquidity by iterating through all positions and summing those that
1378    /// span the current tick. Unlike [`Self::get_active_liquidity`], which returns the maintained
1379    /// tick map value, this method performs a fresh calculation from position data.
1380    #[must_use]
1381    pub fn get_total_liquidity_from_active_positions(&self) -> u128 {
1382        self.positions
1383            .values()
1384            .filter(|position| {
1385                position.liquidity > 0
1386                    && position.tick_lower <= self.state.current_tick
1387                    && self.state.current_tick < position.tick_upper
1388            })
1389            .map(|position| position.liquidity)
1390            .sum()
1391    }
1392
1393    /// Calculates total liquidity across all positions, regardless of range status.
1394    #[must_use]
1395    pub fn get_total_liquidity(&self) -> U256 {
1396        self.positions
1397            .values()
1398            .map(|position| U256::from(position.liquidity))
1399            .fold(U256::ZERO, |acc, liq| acc + liq)
1400    }
1401
1402    /// Restores the profiler state from a saved snapshot.
1403    ///
1404    /// This method allows resuming profiling from a previously saved state,
1405    /// enabling incremental processing without reprocessing all historical events.
1406    ///
1407    /// # Errors
1408    ///
1409    /// Returns an error if:
1410    /// - Tick insertion into the tick map fails.
1411    ///
1412    /// # Panics
1413    ///
1414    /// Panics if the pool's tick spacing is not set.
1415    pub fn restore_from_snapshot(&mut self, snapshot: PoolSnapshot) -> anyhow::Result<()> {
1416        let liquidity = snapshot.state.liquidity;
1417
1418        // Restore state
1419        self.state = snapshot.state;
1420
1421        // Restore analytics (skip duration fields as they're debug-only)
1422        self.analytics.total_amount0_deposited = snapshot.analytics.total_amount0_deposited;
1423        self.analytics.total_amount1_deposited = snapshot.analytics.total_amount1_deposited;
1424        self.analytics.total_amount0_collected = snapshot.analytics.total_amount0_collected;
1425        self.analytics.total_amount1_collected = snapshot.analytics.total_amount1_collected;
1426        self.analytics.total_swaps = snapshot.analytics.total_swaps;
1427        self.analytics.total_mints = snapshot.analytics.total_mints;
1428        self.analytics.total_burns = snapshot.analytics.total_burns;
1429        self.analytics.total_fee_collects = snapshot.analytics.total_fee_collects;
1430        self.analytics.total_flashes = snapshot.analytics.total_flashes;
1431
1432        // Rebuild positions AHashMap
1433        self.positions.clear();
1434        for position in snapshot.positions {
1435            let key = PoolPosition::get_position_key(
1436                &position.owner,
1437                position.tick_lower,
1438                position.tick_upper,
1439            );
1440            self.positions.insert(key, position);
1441        }
1442
1443        // Rebuild tick_map
1444        self.tick_map = TickMap::new(
1445            self.pool
1446                .tick_spacing
1447                .expect("Pool tick spacing must be set"),
1448        );
1449        for tick in snapshot.ticks {
1450            self.tick_map.restore_tick(tick);
1451        }
1452
1453        // Restore active liquidity
1454        self.tick_map.liquidity = liquidity;
1455
1456        // Set last processed event
1457        self.last_processed_event = Some(snapshot.block_position);
1458
1459        // Mark as initialized
1460        self.is_initialized = true;
1461
1462        // Recalculate analytics
1463        self.update_liquidity_analytics();
1464
1465        Ok(())
1466    }
1467
1468    /// Gets a list of all initialized tick values.
1469    ///
1470    /// Returns tick values that have been initialized (have liquidity positions).
1471    /// Useful for understanding the liquidity distribution across price ranges.
1472    pub fn get_active_tick_values(&self) -> Vec<i32> {
1473        self.tick_map
1474            .get_all_ticks()
1475            .iter()
1476            .filter(|(_, tick)| self.tick_map.is_tick_initialized(tick.value))
1477            .map(|(tick_value, _)| *tick_value)
1478            .collect()
1479    }
1480
1481    /// Gets the number of active ticks.
1482    #[must_use]
1483    pub fn get_active_tick_count(&self) -> usize {
1484        self.tick_map.active_tick_count()
1485    }
1486
1487    /// Gets tick information for a specific tick value.
1488    ///
1489    /// Returns the tick data structure containing liquidity and fee information
1490    /// for the specified tick, if it exists.
1491    pub fn get_tick(&self, tick: i32) -> Option<&PoolTick> {
1492        self.tick_map.get_tick(tick)
1493    }
1494
1495    /// Gets the current tick position of the pool.
1496    ///
1497    /// Returns the tick that corresponds to the current pool price.
1498    /// The pool must be initialized before calling this method.
1499    pub fn get_current_tick(&self) -> i32 {
1500        self.state.current_tick
1501    }
1502
1503    /// Gets the total number of ticks tracked by the tick map.
1504    ///
1505    /// Returns count of all ticks that have ever been initialized,
1506    /// including those that may no longer have active liquidity.
1507    ///
1508    /// # Returns
1509    /// Total tick count in the tick map
1510    pub fn get_total_tick_count(&self) -> usize {
1511        self.tick_map.total_tick_count()
1512    }
1513
1514    /// Gets position information for a specific owner and tick range.
1515    ///
1516    /// Looks up a position by its unique key (owner + tick range) and returns
1517    /// the position data if it exists.
1518    pub fn get_position(
1519        &self,
1520        owner: &Address,
1521        tick_lower: i32,
1522        tick_upper: i32,
1523    ) -> Option<&PoolPosition> {
1524        let position_key = PoolPosition::get_position_key(owner, tick_lower, tick_upper);
1525        self.positions.get(&position_key)
1526    }
1527
1528    /// Returns a list of all currently active positions.
1529    ///
1530    /// Active positions are those with liquidity > 0 whose tick range includes
1531    /// the current pool tick, meaning they have tokens actively deployed in the pool
1532    /// and are earning fees from trades at the current price.
1533    ///
1534    /// # Returns
1535    ///
1536    /// A vector of references to active [`PoolPosition`] objects.
1537    pub fn get_active_positions(&self) -> Vec<&PoolPosition> {
1538        self.positions
1539            .values()
1540            .filter(|position| {
1541                let current_tick = self.get_current_tick();
1542                position.liquidity > 0
1543                    && position.tick_lower <= current_tick
1544                    && current_tick < position.tick_upper
1545            })
1546            .collect()
1547    }
1548
1549    /// Returns a list of all positions tracked by the profiler.
1550    ///
1551    /// This includes both active and inactive positions, regardless of their
1552    /// liquidity or tick range relative to the current pool tick.
1553    ///
1554    /// # Returns
1555    ///
1556    /// A vector of references to all [`PoolPosition`] objects.
1557    pub fn get_all_positions(&self) -> Vec<&PoolPosition> {
1558        self.positions.values().collect()
1559    }
1560
1561    /// Returns position keys for all tracked positions.
1562    pub fn get_all_position_keys(&self) -> Vec<(Address, i32, i32)> {
1563        self.get_all_positions()
1564            .iter()
1565            .map(|position| (position.owner, position.tick_lower, position.tick_upper))
1566            .collect()
1567    }
1568
1569    /// Extracts a complete snapshot of the current pool state.
1570    ///
1571    /// Extracts and bundles the complete pool state including global variables,
1572    /// all liquidity positions, and the full tick distribution into a portable
1573    /// [`PoolSnapshot`] structure. This snapshot can be serialized, persisted
1574    /// to database, or used to restore pool state later.
1575    ///
1576    /// # Panics
1577    ///
1578    /// Panics if no events have been processed yet.
1579    pub fn extract_snapshot(&self) -> PoolSnapshot {
1580        let positions: Vec<_> = self.positions.values().cloned().collect();
1581        let ticks: Vec<_> = self.tick_map.get_all_ticks().values().copied().collect();
1582
1583        let mut state = self.state.clone();
1584        state.liquidity = self.tick_map.liquidity;
1585
1586        PoolSnapshot::new(
1587            self.pool.instrument_id,
1588            state,
1589            positions,
1590            ticks,
1591            self.analytics.clone(),
1592            self.last_processed_event
1593                .clone()
1594                .expect("No events processed yet"),
1595        )
1596    }
1597
1598    /// Gets the count of positions that are currently active.
1599    ///
1600    /// Active positions are those with liquidity > 0 and whose tick range
1601    /// includes the current pool tick (meaning they have tokens in the pool).
1602    pub fn get_total_active_positions(&self) -> usize {
1603        self.positions
1604            .iter()
1605            .filter(|(_, position)| {
1606                let current_tick = self.get_current_tick();
1607                position.liquidity > 0
1608                    && position.tick_lower <= current_tick
1609                    && current_tick < position.tick_upper
1610            })
1611            .count()
1612    }
1613
1614    /// Gets the count of positions that are currently inactive.
1615    ///
1616    /// Inactive positions are those that exist but don't span the current tick,
1617    /// meaning their liquidity is entirely in one token or the other.
1618    pub fn get_total_inactive_positions(&self) -> usize {
1619        self.positions.len() - self.get_total_active_positions()
1620    }
1621
1622    /// Estimates the total amount of token0 in the pool.
1623    ///
1624    /// Calculates token0 balance by summing:
1625    /// - Token0 amounts from all active liquidity positions
1626    /// - Accumulated trading fees (approximated from fee growth)
1627    /// - Protocol fees collected
1628    pub fn estimate_balance_of_token0(&self) -> U256 {
1629        let mut total_amount0 = U256::ZERO;
1630        let current_sqrt_price = self.state.price_sqrt_ratio_x96;
1631        let current_tick = self.state.current_tick;
1632        let mut total_fees_0_collected: u128 = 0;
1633
1634        // 1. Calculate token0 from active liquidity positions
1635        for position in self.positions.values() {
1636            if position.liquidity > 0 {
1637                if position.tick_upper <= current_tick {
1638                    // Position is below current price - no token0
1639                    continue;
1640                } else if position.tick_lower > current_tick {
1641                    // Position is above current price - all token0
1642                    let sqrt_ratio_a = get_sqrt_ratio_at_tick(position.tick_lower);
1643                    let sqrt_ratio_b = get_sqrt_ratio_at_tick(position.tick_upper);
1644                    let amount0 =
1645                        get_amount0_delta(sqrt_ratio_a, sqrt_ratio_b, position.liquidity, true);
1646                    total_amount0 += amount0;
1647                } else {
1648                    // Position is active - token0 from current price to upper tick
1649                    let sqrt_ratio_upper = get_sqrt_ratio_at_tick(position.tick_upper);
1650                    let amount0 = get_amount0_delta(
1651                        current_sqrt_price,
1652                        sqrt_ratio_upper,
1653                        position.liquidity,
1654                        true,
1655                    );
1656                    total_amount0 += amount0;
1657                }
1658            }
1659
1660            total_fees_0_collected += position.total_amount0_collected;
1661        }
1662
1663        // 2. Add accumulated swap fees (fee_growth_global represents total fees accumulated)
1664        // Note: In a real pool, fees are distributed as liquidity, but for balance estimation
1665        // we can use a simplified approach by converting fee growth to token amounts
1666        let fee_growth_0 = self.state.fee_growth_global_0;
1667        if fee_growth_0 > U256::ZERO {
1668            // Convert fee growth to actual token amount using FullMath for precision
1669            // Fee growth is in Q128.128 format, so we need to scale it properly
1670            let active_liquidity = self.get_active_liquidity();
1671            if active_liquidity > 0 {
1672                // fee_growth_global is fees per unit of liquidity in Q128.128
1673                // To get total fees: mul_div(fee_growth, liquidity, 2^128)
1674                if let Ok(total_fees_0) =
1675                    FullMath::mul_div(fee_growth_0, U256::from(active_liquidity), Q128)
1676                {
1677                    total_amount0 += total_fees_0;
1678                }
1679            }
1680        }
1681
1682        let total_fees_0_left = fee_growth_0 - U256::from(total_fees_0_collected);
1683
1684        // 4. Add protocol fees
1685        total_amount0 += self.state.protocol_fees_token0;
1686
1687        total_amount0 + total_fees_0_left
1688    }
1689
1690    /// Estimates the total amount of token1 in the pool.
1691    ///
1692    /// Calculates token1 balance by summing:
1693    /// - Token1 amounts from all active liquidity positions
1694    /// - Accumulated trading fees (approximated from fee growth)
1695    /// - Protocol fees collected
1696    pub fn estimate_balance_of_token1(&self) -> U256 {
1697        let mut total_amount1 = U256::ZERO;
1698        let current_sqrt_price = self.state.price_sqrt_ratio_x96;
1699        let current_tick = self.state.current_tick;
1700        let mut total_fees_1_collected: u128 = 0;
1701
1702        // 1. Calculate token1 from active liquidity positions
1703        for position in self.positions.values() {
1704            if position.liquidity > 0 {
1705                if position.tick_lower > current_tick {
1706                    // Position is above current price - no token1
1707                    continue;
1708                } else if position.tick_upper <= current_tick {
1709                    // Position is below current price - all token1
1710                    let sqrt_ratio_a = get_sqrt_ratio_at_tick(position.tick_lower);
1711                    let sqrt_ratio_b = get_sqrt_ratio_at_tick(position.tick_upper);
1712                    let amount1 =
1713                        get_amount1_delta(sqrt_ratio_a, sqrt_ratio_b, position.liquidity, true);
1714                    total_amount1 += amount1;
1715                } else {
1716                    // Position is active - token1 from lower tick to current price
1717                    let sqrt_ratio_lower = get_sqrt_ratio_at_tick(position.tick_lower);
1718                    let amount1 = get_amount1_delta(
1719                        sqrt_ratio_lower,
1720                        current_sqrt_price,
1721                        position.liquidity,
1722                        true,
1723                    );
1724                    total_amount1 += amount1;
1725                }
1726            }
1727
1728            // Sum collected fees
1729            total_fees_1_collected += position.total_amount1_collected;
1730        }
1731
1732        // 2. Add accumulated swap fees for token1
1733        let fee_growth_1 = self.state.fee_growth_global_1;
1734        if fee_growth_1 > U256::ZERO {
1735            let active_liquidity = self.get_active_liquidity();
1736            if active_liquidity > 0 {
1737                // Convert fee growth to actual token amount using FullMath
1738                if let Ok(total_fees_1) =
1739                    FullMath::mul_div(fee_growth_1, U256::from(active_liquidity), Q128)
1740                {
1741                    total_amount1 += total_fees_1;
1742                }
1743            }
1744        }
1745
1746        let total_fees_1_left = fee_growth_1 - U256::from(total_fees_1_collected);
1747
1748        // 4. Add protocol fees
1749        total_amount1 += self.state.protocol_fees_token1;
1750
1751        total_amount1 + total_fees_1_left
1752    }
1753
1754    /// Sets the global fee growth for both tokens.
1755    ///
1756    /// This is primarily used for testing to simulate specific fee growth scenarios.
1757    /// In production, fee growth is updated through swap operations.
1758    ///
1759    /// # Arguments
1760    /// * `fee_growth_global_0` - New global fee growth for token0
1761    /// * `fee_growth_global_1` - New global fee growth for token1
1762    pub fn set_fee_growth_global(&mut self, fee_growth_global_0: U256, fee_growth_global_1: U256) {
1763        self.state.fee_growth_global_0 = fee_growth_global_0;
1764        self.state.fee_growth_global_1 = fee_growth_global_1;
1765    }
1766
1767    /// Returns the total number of events processed.
1768    pub fn get_total_events(&self) -> u64 {
1769        self.analytics.total_swaps
1770            + self.analytics.total_mints
1771            + self.analytics.total_burns
1772            + self.analytics.total_fee_collects
1773            + self.analytics.total_flashes
1774    }
1775
1776    /// Enables progress reporting for pool profiler event processing.
1777    ///
1778    /// When enabled, the profiler will automatically track and log progress
1779    /// as events are processed through the `process()` method.
1780    pub fn enable_reporting(&mut self, from_block: u64, total_blocks: u64, update_interval: u64) {
1781        self.reporter = Some(BlockchainSyncReporter::new(
1782            BlockchainSyncReportItems::PoolProfiling,
1783            from_block,
1784            total_blocks,
1785            update_interval,
1786        ));
1787        self.last_reported_block = from_block;
1788    }
1789
1790    /// Finalizes reporting and logs final statistics.
1791    ///
1792    /// Should be called after all events have been processed to output
1793    /// the final summary of the profiler bootstrap operation.
1794    pub fn finalize_reporting(&mut self) {
1795        if let Some(reporter) = &self.reporter {
1796            reporter.log_final_stats();
1797        }
1798        self.reporter = None;
1799    }
1800}