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