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