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