Skip to main content

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