nautilus_model/defi/pool_analysis/
quote.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
16use std::cmp::Ordering;
17
18use alloy_primitives::{Address, I256, U160, U256};
19
20use crate::defi::{
21    Pool, PoolSwap, SharedChain, SharedDex, data::block::BlockPosition, tick_map::tick::CrossedTick,
22};
23
24/// Comprehensive swap quote containing profiling metrics for a hypothetical swap.
25///
26/// This structure provides detailed analysis of what would happen if a swap were executed,
27/// including price impact, fees, slippage, and execution details, without actually
28/// modifying the pool state.
29#[derive(Debug, Clone)]
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
33)]
34pub struct SwapQuote {
35    /// Amount of token0 that would be swapped (positive = in, negative = out).
36    pub amount0: I256,
37    /// Amount of token1 that would be swapped (positive = in, negative = out).
38    pub amount1: I256,
39    /// Square root price before the swap (Q96 format).
40    pub sqrt_price_before_x96: U160,
41    /// Square root price after the swap (Q96 format).
42    pub sqrt_price_after_x96: U160,
43    /// Tick position before the swap.
44    pub tick_before: i32,
45    /// Tick position after the swap.
46    pub tick_after: i32,
47    /// Active liquidity after the swap.
48    pub liquidity_after: u128,
49    /// Fee growth global for target token after the swap (Q128.128 format).
50    pub fee_growth_global_after: U256,
51    /// Total fees paid to liquidity providers.
52    pub lp_fee: U256,
53    /// Total fees paid to the protocol.
54    pub protocol_fee: U256,
55    /// List of tick boundaries crossed during the swap, in order of crossing.
56    pub crossed_ticks: Vec<CrossedTick>,
57}
58
59impl SwapQuote {
60    #[allow(clippy::too_many_arguments)]
61    /// Creates a [`SwapQuote`] instance with comprehensive swap simulation results.
62    pub fn new(
63        amount0: I256,
64        amount1: I256,
65        sqrt_price_before_x96: U160,
66        sqrt_price_after_x96: U160,
67        tick_before: i32,
68        tick_after: i32,
69        liquidity_after: u128,
70        fee_growth_global_after: U256,
71        lp_fee: U256,
72        protocol_fee: U256,
73        crossed_ticks: Vec<CrossedTick>,
74    ) -> Self {
75        Self {
76            amount0,
77            amount1,
78            sqrt_price_before_x96,
79            sqrt_price_after_x96,
80            tick_before,
81            tick_after,
82            liquidity_after,
83            fee_growth_global_after,
84            lp_fee,
85            protocol_fee,
86            crossed_ticks,
87        }
88    }
89
90    /// Determines swap direction from tick movement or amount sign.
91    ///
92    /// Returns `true` if swapping token0 for token1 (zero_for_one),
93    /// `false` if swapping token1 for token0.
94    ///
95    /// The direction is inferred from:
96    /// 1. Tick movement (if ticks changed): downward = token0→token1
97    /// 2. Amount sign (if tick unchanged): positive amount0 = token0→token1
98    pub fn zero_for_one(&self) -> bool {
99        match self.tick_after.cmp(&self.tick_before) {
100            Ordering::Less => true,     // Tick went down, swap was token0 -> token1
101            Ordering::Greater => false, // Tick went up, swap was token1 -> token0
102            Ordering::Equal => {
103                // Tick unchanged, very small swap, we fall back to the amount sign
104                self.amount0.is_positive()
105            }
106        }
107    }
108
109    /// Returns the total fees paid (LP fees + protocol fees).
110    pub fn total_fee(&self) -> U256 {
111        self.lp_fee + self.protocol_fee
112    }
113
114    /// Returns the number of tick boundaries crossed during this swap.
115    ///
116    /// This equals the length of the `crossed_ticks` vector and indicates
117    /// how much liquidity the swap traversed.
118    pub fn total_crossed_ticks(&self) -> u32 {
119        self.crossed_ticks.len() as u32
120    }
121
122    /// Gets the output amount for the given swap direction.
123    pub fn get_output_amount(&self) -> U256 {
124        if self.zero_for_one() {
125            self.amount1.unsigned_abs()
126        } else {
127            self.amount0.unsigned_abs()
128        }
129    }
130
131    /// Validates that the quote satisfied an exact output request.
132    ///
133    /// # Errors
134    /// Returns error if the actual output is less than the requested amount.
135    pub fn validate_exact_output(&self, amount_out_requested: U256) -> anyhow::Result<()> {
136        let actual_out = self.get_output_amount();
137        if actual_out < amount_out_requested {
138            anyhow::bail!(
139                "Insufficient liquidity: requested {}, got {}",
140                amount_out_requested,
141                actual_out
142            );
143        }
144        Ok(())
145    }
146
147    /// Converts this quote into a [`PoolSwap`] event with the provided metadata.
148    ///
149    /// # Returns
150    /// A [`PoolSwap`] event containing both the quote data and provided metadata
151    #[allow(clippy::too_many_arguments)]
152    pub fn to_swap_event(
153        &self,
154        chain: SharedChain,
155        dex: SharedDex,
156        pool_address: &Address,
157        block: BlockPosition,
158        sender: Address,
159        recipient: Address,
160    ) -> PoolSwap {
161        let instrument_id = Pool::create_instrument_id(chain.name, &dex, pool_address);
162        PoolSwap::new(
163            chain,
164            dex,
165            instrument_id,
166            *pool_address,
167            block.number,
168            block.transaction_hash,
169            block.transaction_index,
170            block.log_index,
171            None, // timestamp
172            sender,
173            recipient,
174            self.amount0,
175            self.amount1,
176            self.sqrt_price_after_x96,
177            self.liquidity_after,
178            self.tick_after,
179            None,
180            None,
181            None,
182        )
183    }
184}