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