use alloy_primitives::{Address, I256, U160, U256};
use crate::{
defi::{
Pool, PoolIdentifier, PoolSwap, SharedChain, SharedDex, Token,
data::{
block::BlockPosition,
swap::RawSwapData,
swap_trade_info::{SwapTradeInfo, SwapTradeInfoCalculator},
},
tick_map::{full_math::FullMath, tick::CrossedTick},
},
identifiers::InstrumentId,
};
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct SwapQuote {
pub instrument_id: InstrumentId,
pub amount0: I256,
pub amount1: I256,
pub sqrt_price_before_x96: U160,
pub sqrt_price_after_x96: U160,
pub tick_before: i32,
pub tick_after: i32,
pub liquidity_after: u128,
pub fee_growth_global_after: U256,
pub lp_fee: U256,
pub protocol_fee: U256,
pub crossed_ticks: Vec<CrossedTick>,
pub trade_info: Option<SwapTradeInfo>,
}
impl SwapQuote {
#[allow(clippy::too_many_arguments)]
pub fn new(
instrument_id: InstrumentId,
amount0: I256,
amount1: I256,
sqrt_price_before_x96: U160,
sqrt_price_after_x96: U160,
tick_before: i32,
tick_after: i32,
liquidity_after: u128,
fee_growth_global_after: U256,
lp_fee: U256,
protocol_fee: U256,
crossed_ticks: Vec<CrossedTick>,
) -> Self {
Self {
instrument_id,
amount0,
amount1,
sqrt_price_before_x96,
sqrt_price_after_x96,
tick_before,
tick_after,
liquidity_after,
fee_growth_global_after,
lp_fee,
protocol_fee,
crossed_ticks,
trade_info: None,
}
}
fn check_if_trade_info_initialized(&self) -> anyhow::Result<&SwapTradeInfo> {
if self.trade_info.is_none() {
anyhow::bail!(
"Trade info is not initialized. Please call calculate_trade_info() first."
);
}
Ok(self.trade_info.as_ref().unwrap())
}
pub fn calculate_trade_info(&mut self, token0: &Token, token1: &Token) -> anyhow::Result<()> {
let trade_info_calculator = SwapTradeInfoCalculator::new(
token0,
token1,
RawSwapData::new(self.amount0, self.amount1, self.sqrt_price_after_x96),
);
let trade_info = trade_info_calculator.compute(Some(self.sqrt_price_before_x96))?;
self.trade_info = Some(trade_info);
Ok(())
}
pub fn zero_for_one(&self) -> bool {
self.amount0.is_positive()
}
pub fn total_fee(&self) -> U256 {
self.lp_fee + self.protocol_fee
}
pub fn get_effective_fee_bps(&self) -> u32 {
let input_amount = self.get_input_amount();
if input_amount.is_zero() {
return 0;
}
let total_fees = self.lp_fee + self.protocol_fee;
let fee_bps =
FullMath::mul_div(total_fees, U256::from(10_000), input_amount).unwrap_or(U256::ZERO);
fee_bps.to::<u32>()
}
pub fn total_crossed_ticks(&self) -> u32 {
self.crossed_ticks.len() as u32
}
pub fn get_output_amount(&self) -> U256 {
if self.zero_for_one() {
self.amount1.unsigned_abs()
} else {
self.amount0.unsigned_abs()
}
}
pub fn get_input_amount(&self) -> U256 {
if self.zero_for_one() {
self.amount0.unsigned_abs()
} else {
self.amount1.unsigned_abs()
}
}
pub fn get_price_impact_bps(&mut self) -> anyhow::Result<u32> {
match self.check_if_trade_info_initialized() {
Ok(trade_info) => trade_info.get_price_impact_bps(),
Err(e) => anyhow::bail!("Failed to calculate price impact: {e}"),
}
}
pub fn get_slippage_bps(&mut self) -> anyhow::Result<u32> {
match self.check_if_trade_info_initialized() {
Ok(trade_info) => trade_info.get_slippage_bps(),
Err(e) => anyhow::bail!("Failed to calculate slippage: {e}"),
}
}
pub fn validate_slippage_tolerance(&mut self, max_slippage_bps: u32) -> anyhow::Result<()> {
let actual_slippage = self.get_slippage_bps()?;
if actual_slippage > max_slippage_bps {
anyhow::bail!(
"Slippage {actual_slippage} bps exceeds tolerance {max_slippage_bps} bps"
);
}
Ok(())
}
pub fn validate_exact_output(&self, amount_out_requested: U256) -> anyhow::Result<()> {
let actual_out = self.get_output_amount();
if actual_out < amount_out_requested {
anyhow::bail!(
"Insufficient liquidity: requested {amount_out_requested}, available {actual_out}"
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn to_swap_event(
&self,
chain: SharedChain,
dex: SharedDex,
pool_identifier: PoolIdentifier,
block: BlockPosition,
sender: Address,
recipient: Address,
) -> PoolSwap {
let instrument_id = Pool::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
PoolSwap::new(
chain,
dex,
instrument_id,
pool_identifier,
block.number,
block.transaction_hash,
block.transaction_index,
block.log_index,
None, sender,
recipient,
self.amount0,
self.amount1,
self.sqrt_price_after_x96,
self.liquidity_after,
self.tick_after,
)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use rstest::rstest;
use rust_decimal_macros::dec;
use super::*;
use crate::{
defi::{SharedPool, stubs::rain_pool},
enums::OrderSide,
};
#[rstest]
fn test_swap_quote_sell(rain_pool: SharedPool) {
let sqrt_x96_price_before = U160::from_str("76951769738874829996307631").unwrap();
let amount0 = I256::from_str("287175356684998201516914").unwrap();
let amount1 = I256::from_str("-270157537808188649").unwrap();
let mut swap_quote = SwapQuote::new(
rain_pool.instrument_id,
amount0,
amount1,
sqrt_x96_price_before,
U160::from_str("76812046714213096298497129").unwrap(),
-138746,
-138782,
292285495328044734302670,
U256::ZERO,
U256::ZERO,
U256::ZERO,
vec![],
);
swap_quote
.calculate_trade_info(&rain_pool.token0, &rain_pool.token1)
.unwrap();
if let Some(swap_trade_info) = &swap_quote.trade_info {
assert_eq!(swap_trade_info.order_side, OrderSide::Sell);
assert_eq!(swap_quote.get_input_amount(), amount0.unsigned_abs());
assert_eq!(swap_quote.get_output_amount(), amount1.unsigned_abs());
assert_eq!(
swap_trade_info.quantity_base.as_decimal(),
dec!(287175.356684998201516914)
);
assert_eq!(
swap_trade_info.quantity_quote.as_decimal(),
dec!(0.270157537808188649)
);
assert_eq!(
swap_trade_info.spot_price.as_decimal(),
dec!(0.0000009399386483)
);
assert_eq!(swap_trade_info.get_price_impact_bps().unwrap(), 36);
assert_eq!(swap_trade_info.get_slippage_bps().unwrap(), 28);
} else {
panic!("Trade info is None");
}
}
#[rstest]
fn test_swap_quote_buy(rain_pool: SharedPool) {
let sqrt_x96_price_before = U160::from_str("76827576486429933391429745").unwrap();
let amount0 = I256::from_str("-117180628248242869089291").unwrap();
let amount1 = I256::from_str("110241020399788696").unwrap();
let mut swap_quote = SwapQuote::new(
rain_pool.instrument_id,
amount0,
amount1,
sqrt_x96_price_before,
U160::from_str("76857455902960072891859299").unwrap(),
-138778,
-138770,
292285495328044734302670,
U256::ZERO,
U256::ZERO,
U256::ZERO,
vec![],
);
swap_quote
.calculate_trade_info(&rain_pool.token0, &rain_pool.token1)
.unwrap();
if let Some(swap_trade_info) = &swap_quote.trade_info {
assert_eq!(swap_trade_info.order_side, OrderSide::Buy);
assert_eq!(swap_quote.get_input_amount(), amount1.unsigned_abs());
assert_eq!(swap_quote.get_output_amount(), amount0.unsigned_abs());
assert_eq!(
swap_trade_info.quantity_base.as_decimal(),
dec!(117180.628248242869089291)
);
assert_eq!(
swap_trade_info.quantity_quote.as_decimal(),
dec!(0.110241020399788696)
);
assert_eq!(
swap_trade_info.spot_price.as_decimal(),
dec!(0.000000941050309)
);
assert_eq!(
swap_trade_info.execution_price.as_decimal(),
dec!(0.0000009407785403)
);
assert_eq!(swap_trade_info.get_price_impact_bps().unwrap(), 8);
assert_eq!(swap_trade_info.get_slippage_bps().unwrap(), 5);
} else {
panic!("Trade info is None");
}
}
}