nautilus_model/defi/pool_analysis/
quote.rs1use alloy_primitives::{Address, I256, U160, U256};
17
18use crate::{
19 defi::{
20 Pool, PoolIdentifier, PoolSwap, SharedChain, SharedDex, Token,
21 data::{
22 block::BlockPosition,
23 swap::RawSwapData,
24 swap_trade_info::{SwapTradeInfo, SwapTradeInfoCalculator},
25 },
26 tick_map::{full_math::FullMath, tick::CrossedTick},
27 },
28 identifiers::InstrumentId,
29};
30
31#[derive(Debug, Clone)]
37#[cfg_attr(
38 feature = "python",
39 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
40)]
41pub struct SwapQuote {
42 pub instrument_id: InstrumentId,
44 pub amount0: I256,
46 pub amount1: I256,
48 pub sqrt_price_before_x96: U160,
50 pub sqrt_price_after_x96: U160,
52 pub tick_before: i32,
54 pub tick_after: i32,
56 pub liquidity_after: u128,
58 pub fee_growth_global_after: U256,
60 pub lp_fee: U256,
62 pub protocol_fee: U256,
64 pub crossed_ticks: Vec<CrossedTick>,
66 pub trade_info: Option<SwapTradeInfo>,
68}
69
70impl SwapQuote {
71 #[allow(clippy::too_many_arguments)]
72 pub fn new(
78 instrument_id: InstrumentId,
79 amount0: I256,
80 amount1: I256,
81 sqrt_price_before_x96: U160,
82 sqrt_price_after_x96: U160,
83 tick_before: i32,
84 tick_after: i32,
85 liquidity_after: u128,
86 fee_growth_global_after: U256,
87 lp_fee: U256,
88 protocol_fee: U256,
89 crossed_ticks: Vec<CrossedTick>,
90 ) -> Self {
91 Self {
92 instrument_id,
93 amount0,
94 amount1,
95 sqrt_price_before_x96,
96 sqrt_price_after_x96,
97 tick_before,
98 tick_after,
99 liquidity_after,
100 fee_growth_global_after,
101 lp_fee,
102 protocol_fee,
103 crossed_ticks,
104 trade_info: None,
105 }
106 }
107
108 fn check_if_trade_info_initialized(&mut self) -> anyhow::Result<&SwapTradeInfo> {
109 if self.trade_info.is_none() {
110 anyhow::bail!(
111 "Trade info is not initialized. Please call calculate_trade_info() first."
112 );
113 }
114
115 Ok(self.trade_info.as_ref().unwrap())
116 }
117
118 pub fn calculate_trade_info(&mut self, token0: &Token, token1: &Token) -> anyhow::Result<()> {
128 let trade_info_calculator = SwapTradeInfoCalculator::new(
129 token0,
130 token1,
131 RawSwapData::new(self.amount0, self.amount1, self.sqrt_price_after_x96),
132 );
133 let trade_info = trade_info_calculator.compute(Some(self.sqrt_price_before_x96))?;
134 self.trade_info = Some(trade_info);
135
136 Ok(())
137 }
138
139 pub fn zero_for_one(&self) -> bool {
143 self.amount0.is_positive()
144 }
145
146 pub fn total_fee(&self) -> U256 {
148 self.lp_fee + self.protocol_fee
149 }
150
151 pub fn get_effective_fee_bps(&self) -> u32 {
153 let input_amount = self.get_input_amount();
154 if input_amount.is_zero() {
155 return 0;
156 }
157
158 let total_fees = self.lp_fee + self.protocol_fee;
159
160 let fee_bps =
162 FullMath::mul_div(total_fees, U256::from(10_000), input_amount).unwrap_or(U256::ZERO);
163
164 fee_bps.to::<u32>()
165 }
166
167 pub fn total_crossed_ticks(&self) -> u32 {
172 self.crossed_ticks.len() as u32
173 }
174
175 pub fn get_output_amount(&self) -> U256 {
177 if self.zero_for_one() {
178 self.amount1.unsigned_abs()
179 } else {
180 self.amount0.unsigned_abs()
181 }
182 }
183
184 pub fn get_input_amount(&self) -> U256 {
186 if self.zero_for_one() {
187 self.amount0.unsigned_abs()
188 } else {
189 self.amount1.unsigned_abs()
190 }
191 }
192
193 pub fn get_price_impact_bps(&mut self) -> anyhow::Result<u32> {
205 match self.check_if_trade_info_initialized() {
206 Ok(trade_info) => trade_info.get_price_impact_bps(),
207 Err(e) => anyhow::bail!("Failed to calculate price impact: {e}"),
208 }
209 }
210
211 pub fn get_slippage_bps(&mut self) -> anyhow::Result<u32> {
223 match self.check_if_trade_info_initialized() {
224 Ok(trade_info) => trade_info.get_slippage_bps(),
225 Err(e) => anyhow::bail!("Failed to calculate slippage: {e}"),
226 }
227 }
228
229 pub fn validate_slippage_tolerance(&mut self, max_slippage_bps: u32) -> anyhow::Result<()> {
233 let actual_slippage = self.get_slippage_bps()?;
234 if actual_slippage > max_slippage_bps {
235 anyhow::bail!(
236 "Slippage {actual_slippage} bps exceeds tolerance {max_slippage_bps} bps"
237 );
238 }
239 Ok(())
240 }
241
242 pub fn validate_exact_output(&self, amount_out_requested: U256) -> anyhow::Result<()> {
247 let actual_out = self.get_output_amount();
248 if actual_out < amount_out_requested {
249 anyhow::bail!(
250 "Insufficient liquidity: requested {amount_out_requested}, available {actual_out}"
251 );
252 }
253 Ok(())
254 }
255
256 #[allow(clippy::too_many_arguments)]
261 pub fn to_swap_event(
262 &self,
263 chain: SharedChain,
264 dex: SharedDex,
265 pool_identifier: PoolIdentifier,
266 block: BlockPosition,
267 sender: Address,
268 recipient: Address,
269 ) -> PoolSwap {
270 let instrument_id = Pool::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
271 PoolSwap::new(
272 chain,
273 dex,
274 instrument_id,
275 pool_identifier,
276 block.number,
277 block.transaction_hash,
278 block.transaction_index,
279 block.log_index,
280 None, sender,
282 recipient,
283 self.amount0,
284 self.amount1,
285 self.sqrt_price_after_x96,
286 self.liquidity_after,
287 self.tick_after,
288 )
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use std::str::FromStr;
295
296 use rstest::rstest;
297 use rust_decimal_macros::dec;
298
299 use super::*;
300 use crate::{
301 defi::{SharedPool, stubs::rain_pool},
302 enums::OrderSide,
303 };
304
305 #[rstest]
306 fn test_swap_quote_sell(rain_pool: SharedPool) {
307 let sqrt_x96_price_before = U160::from_str("76951769738874829996307631").unwrap();
309 let amount0 = I256::from_str("287175356684998201516914").unwrap();
310 let amount1 = I256::from_str("-270157537808188649").unwrap();
311
312 let mut swap_quote = SwapQuote::new(
313 rain_pool.instrument_id,
314 amount0,
315 amount1,
316 sqrt_x96_price_before,
317 U160::from_str("76812046714213096298497129").unwrap(),
318 -138746,
319 -138782,
320 292285495328044734302670,
321 U256::ZERO,
322 U256::ZERO,
323 U256::ZERO,
324 vec![],
325 );
326 swap_quote
327 .calculate_trade_info(&rain_pool.token0, &rain_pool.token1)
328 .unwrap();
329
330 if let Some(swap_trade_info) = &swap_quote.trade_info {
331 assert_eq!(swap_trade_info.order_side, OrderSide::Sell);
332 assert_eq!(swap_quote.get_input_amount(), amount0.unsigned_abs());
333 assert_eq!(swap_quote.get_output_amount(), amount1.unsigned_abs());
334 assert_eq!(
336 swap_trade_info.quantity_base.as_decimal(),
337 dec!(287175.356684998201516914)
338 );
339 assert_eq!(
340 swap_trade_info.quantity_quote.as_decimal(),
341 dec!(0.270157537808188649)
342 );
343 assert_eq!(
344 swap_trade_info.spot_price.as_decimal(),
345 dec!(0.0000009399386483)
346 );
347 assert_eq!(swap_trade_info.get_price_impact_bps().unwrap(), 36);
348 assert_eq!(swap_trade_info.get_slippage_bps().unwrap(), 28);
349 } else {
350 panic!("Trade info is None");
351 }
352 }
353
354 #[rstest]
355 fn test_swap_quote_buy(rain_pool: SharedPool) {
356 let sqrt_x96_price_before = U160::from_str("76827576486429933391429745").unwrap();
358 let amount0 = I256::from_str("-117180628248242869089291").unwrap();
359 let amount1 = I256::from_str("110241020399788696").unwrap();
360
361 let mut swap_quote = SwapQuote::new(
362 rain_pool.instrument_id,
363 amount0,
364 amount1,
365 sqrt_x96_price_before,
366 U160::from_str("76857455902960072891859299").unwrap(),
367 -138778,
368 -138770,
369 292285495328044734302670,
370 U256::ZERO,
371 U256::ZERO,
372 U256::ZERO,
373 vec![],
374 );
375 swap_quote
376 .calculate_trade_info(&rain_pool.token0, &rain_pool.token1)
377 .unwrap();
378
379 if let Some(swap_trade_info) = &swap_quote.trade_info {
380 assert_eq!(swap_trade_info.order_side, OrderSide::Buy);
381 assert_eq!(swap_quote.get_input_amount(), amount1.unsigned_abs());
382 assert_eq!(swap_quote.get_output_amount(), amount0.unsigned_abs());
383 assert_eq!(
385 swap_trade_info.quantity_base.as_decimal(),
386 dec!(117180.628248242869089291)
387 );
388 assert_eq!(
389 swap_trade_info.quantity_quote.as_decimal(),
390 dec!(0.110241020399788696)
391 );
392 assert_eq!(
393 swap_trade_info.spot_price.as_decimal(),
394 dec!(0.000000941050309)
395 );
396 assert_eq!(
397 swap_trade_info.execution_price.as_decimal(),
398 dec!(0.0000009407785403)
399 );
400 assert_eq!(swap_trade_info.get_price_impact_bps().unwrap(), 8);
401 assert_eq!(swap_trade_info.get_slippage_bps().unwrap(), 5);
402 } else {
403 panic!("Trade info is None");
404 }
405 }
406}