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}