nautilus_model/python/defi/
profiler.rs1use std::str::FromStr;
19
20use alloy_primitives::{U160, U256};
21use nautilus_core::python::to_pyvalue_err;
22use pyo3::{prelude::*, types::PyModule};
23
24use crate::{
25 defi::{
26 Pool,
27 pool_analysis::{PoolProfiler, quote::SwapQuote, size_estimator::SizeForImpactResult},
28 },
29 identifiers::InstrumentId,
30};
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl PoolProfiler {
35 #[getter]
36 #[pyo3(name = "pool")]
37 fn py_pool(&self) -> Pool {
38 self.pool.as_ref().clone()
39 }
40
41 #[getter]
42 #[pyo3(name = "instrument_id")]
43 fn py_instrument_id(&self) -> InstrumentId {
44 self.pool.instrument_id
45 }
46
47 #[getter]
48 #[pyo3(name = "is_initialized")]
49 fn py_is_initialized(&self) -> bool {
50 self.is_initialized
51 }
52
53 #[getter]
54 #[pyo3(name = "current_tick")]
55 fn py_current_tick(&self) -> i32 {
56 self.state.current_tick
57 }
58
59 #[getter]
60 #[pyo3(name = "price_sqrt_ratio_x96")]
61 #[gen_stub(override_return_type(type_repr = "int"))]
62 fn py_price_sqrt_ratio_x96(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
63 Ok(PyModule::import(py, "builtins")?
64 .getattr("int")?
65 .call1((self.state.price_sqrt_ratio_x96.to_string(),))?
66 .unbind())
67 }
68
69 #[getter]
70 #[pyo3(name = "total_amount0_deposited")]
71 fn py_total_amount0_deposited(&self) -> String {
72 self.analytics.total_amount0_deposited.to_string()
73 }
74
75 #[getter]
76 #[pyo3(name = "total_amount1_deposited")]
77 fn py_total_amount1_deposited(&self) -> String {
78 self.analytics.total_amount1_deposited.to_string()
79 }
80
81 #[getter]
82 #[pyo3(name = "total_amount0_collected")]
83 fn py_total_amount0_collected(&self) -> String {
84 self.analytics.total_amount0_collected.to_string()
85 }
86
87 #[getter]
88 #[pyo3(name = "total_amount1_collected")]
89 fn py_total_amount1_collected(&self) -> String {
90 self.analytics.total_amount1_collected.to_string()
91 }
92
93 #[getter]
94 #[pyo3(name = "protocol_fees_token0")]
95 fn py_protocol_fees_token0(&self) -> String {
96 self.state.protocol_fees_token0.to_string()
97 }
98
99 #[getter]
100 #[pyo3(name = "protocol_fees_token1")]
101 fn py_protocol_fees_token1(&self) -> String {
102 self.state.protocol_fees_token1.to_string()
103 }
104
105 #[getter]
106 #[pyo3(name = "fee_protocol")]
107 fn py_fee_protocol(&self) -> u8 {
108 self.state.fee_protocol
109 }
110
111 #[getter]
112 #[pyo3(name = "fee_protocol0_basis_points")]
113 fn py_fee_protocol0_basis_points(&self) -> Option<u32> {
114 self.state.fee_protocol0_basis_points
115 }
116
117 #[getter]
118 #[pyo3(name = "fee_protocol1_basis_points")]
119 fn py_fee_protocol1_basis_points(&self) -> Option<u32> {
120 self.state.fee_protocol1_basis_points
121 }
122
123 #[pyo3(name = "get_active_liquidity")]
132 fn py_get_active_liquidity(&self) -> u128 {
133 self.get_active_liquidity()
134 }
135
136 #[pyo3(name = "get_active_tick_count")]
138 fn py_get_active_tick_count(&self) -> usize {
139 self.get_active_tick_count()
140 }
141
142 #[pyo3(name = "get_total_tick_count")]
150 fn py_get_total_tick_count(&self) -> usize {
151 self.get_total_tick_count()
152 }
153
154 #[pyo3(name = "get_total_active_positions")]
159 fn py_get_total_active_positions(&self) -> usize {
160 self.get_total_active_positions()
161 }
162
163 #[pyo3(name = "get_total_inactive_positions")]
168 fn py_get_total_inactive_positions(&self) -> usize {
169 self.get_total_inactive_positions()
170 }
171
172 #[pyo3(name = "estimate_balance_of_token0")]
179 fn py_estimate_balance_of_token0(&self) -> String {
180 self.estimate_balance_of_token0().to_string()
181 }
182
183 #[pyo3(name = "estimate_balance_of_token1")]
190 fn py_estimate_balance_of_token1(&self) -> String {
191 self.estimate_balance_of_token1().to_string()
192 }
193
194 #[pyo3(name = "get_total_liquidity")]
195 fn py_get_total_liquidity_all_positions(&self) -> String {
196 self.get_total_liquidity().to_string()
197 }
198
199 #[pyo3(name = "liquidity_utilization_rate")]
207 fn py_liquidity_utilization_rate(&self) -> f64 {
208 self.liquidity_utilization_rate()
209 }
210
211 #[pyo3(name = "swap_exact_in")]
216 fn py_swap_exact_in(
217 &self,
218 amount_in: &str,
219 zero_for_one: bool,
220 sqrt_price_limit_x96: Option<&str>,
221 ) -> PyResult<SwapQuote> {
222 let amount_in = U256::from_str(amount_in).map_err(to_pyvalue_err)?;
223 let sqrt_price_limit = match sqrt_price_limit_x96 {
224 Some(limit_str) => Some(U160::from_str(limit_str).map_err(to_pyvalue_err)?),
225 None => None,
226 };
227
228 self.swap_exact_in(amount_in, zero_for_one, sqrt_price_limit)
229 .map_err(to_pyvalue_err)
230 }
231
232 #[pyo3(name = "swap_exact_out")]
238 fn py_swap_exact_out(
239 &self,
240 amount_out: &str,
241 zero_for_one: bool,
242 sqrt_price_limit_x96: Option<&str>,
243 ) -> PyResult<SwapQuote> {
244 let amount_out = U256::from_str(amount_out).map_err(to_pyvalue_err)?;
245 let sqrt_price_limit = match sqrt_price_limit_x96 {
246 Some(limit_str) => Some(U160::from_str(limit_str).map_err(to_pyvalue_err)?),
247 None => None,
248 };
249
250 self.swap_exact_out(amount_out, zero_for_one, sqrt_price_limit)
251 .map_err(to_pyvalue_err)
252 }
253
254 #[pyo3(name = "size_for_impact_bps")]
269 fn py_size_for_impact_bps(&self, impact_bps: u32, zero_for_one: bool) -> PyResult<String> {
270 self.size_for_impact_bps(impact_bps, zero_for_one)
271 .map(|size| size.to_string())
272 .map_err(to_pyvalue_err)
273 }
274
275 #[pyo3(name = "size_for_impact_bps_detailed")]
289 fn py_size_for_impact_bps_detailed(
290 &self,
291 impact_bps: u32,
292 zero_for_one: bool,
293 ) -> PyResult<SizeForImpactResult> {
294 self.size_for_impact_bps_detailed(impact_bps, zero_for_one)
295 .map_err(to_pyvalue_err)
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use std::{str::FromStr, sync::Arc};
302
303 use alloy_primitives::{U160, address};
304 use nautilus_core::UnixNanos;
305 use pyo3::{
306 Python,
307 types::{PyAnyMethods, PyInt},
308 };
309 use rstest::rstest;
310
311 use crate::defi::{
312 AmmType, Blockchain, Chain, Dex, DexType, Pool, PoolIdentifier, Token,
313 pool_analysis::PoolProfiler,
314 };
315
316 #[rstest]
317 fn price_sqrt_ratio_x96_returns_python_int() {
318 let sqrt_price_x96 = U160::from_str("79228162514264337593543950336").unwrap();
319 let mut profiler = PoolProfiler::new(pool());
320 profiler.initialize(sqrt_price_x96).unwrap();
321 Python::initialize();
322
323 Python::attach(|py| {
324 let value = profiler.py_price_sqrt_ratio_x96(py).unwrap();
325 let value = value.bind(py);
326
327 assert!(value.is_instance_of::<PyInt>());
328 assert_eq!(value.str().unwrap().to_string(), sqrt_price_x96.to_string());
329 });
330 }
331
332 fn pool() -> Arc<Pool> {
333 let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1));
334 let dex = Arc::new(Dex::new(
335 (*chain).clone(),
336 DexType::UniswapV3,
337 "0x0000000000000000000000000000000000000fac",
338 1,
339 AmmType::CLAMM,
340 "PoolCreated",
341 "Swap",
342 "Mint",
343 "Burn",
344 "Collect",
345 ));
346 let token0 = Token::new(
347 Arc::clone(&chain),
348 address!("0000000000000000000000000000000000000001"),
349 "USD Coin".to_string(),
350 "USDC".to_string(),
351 6,
352 );
353 let token1 = Token::new(
354 Arc::clone(&chain),
355 address!("0000000000000000000000000000000000000002"),
356 "Wrapped Ether".to_string(),
357 "WETH".to_string(),
358 18,
359 );
360 let pool_address = address!("0000000000000000000000000000000000000003");
361
362 Arc::new(Pool::new(
363 chain,
364 dex,
365 pool_address,
366 PoolIdentifier::from_address(pool_address),
367 1,
368 token0,
369 token1,
370 Some(500),
371 Some(10),
372 UnixNanos::default(),
373 ))
374 }
375}