nautilus_model/defi/pool_analysis/
profiler.rs

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