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