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")]
204 fn py_liquidity_utilization_rate(&self) -> f64 {
205 self.liquidity_utilization_rate()
206 }
207
208 #[pyo3(name = "swap_exact_in")]
213 fn py_swap_exact_in(
214 &self,
215 amount_in: &str,
216 zero_for_one: bool,
217 sqrt_price_limit_x96: Option<&str>,
218 ) -> PyResult<SwapQuote> {
219 let amount_in = U256::from_str(amount_in).map_err(to_pyvalue_err)?;
220 let sqrt_price_limit = match sqrt_price_limit_x96 {
221 Some(limit_str) => Some(U160::from_str(limit_str).map_err(to_pyvalue_err)?),
222 None => None,
223 };
224
225 self.swap_exact_in(amount_in, zero_for_one, sqrt_price_limit)
226 .map_err(to_pyvalue_err)
227 }
228
229 #[pyo3(name = "swap_exact_out")]
235 fn py_swap_exact_out(
236 &self,
237 amount_out: &str,
238 zero_for_one: bool,
239 sqrt_price_limit_x96: Option<&str>,
240 ) -> PyResult<SwapQuote> {
241 let amount_out = U256::from_str(amount_out).map_err(to_pyvalue_err)?;
242 let sqrt_price_limit = match sqrt_price_limit_x96 {
243 Some(limit_str) => Some(U160::from_str(limit_str).map_err(to_pyvalue_err)?),
244 None => None,
245 };
246
247 self.swap_exact_out(amount_out, zero_for_one, sqrt_price_limit)
248 .map_err(to_pyvalue_err)
249 }
250
251 #[pyo3(name = "size_for_impact_bps")]
266 fn py_size_for_impact_bps(&self, impact_bps: u32, zero_for_one: bool) -> PyResult<String> {
267 self.size_for_impact_bps(impact_bps, zero_for_one)
268 .map(|size| size.to_string())
269 .map_err(to_pyvalue_err)
270 }
271
272 #[pyo3(name = "size_for_impact_bps_detailed")]
286 fn py_size_for_impact_bps_detailed(
287 &self,
288 impact_bps: u32,
289 zero_for_one: bool,
290 ) -> PyResult<SizeForImpactResult> {
291 self.size_for_impact_bps_detailed(impact_bps, zero_for_one)
292 .map_err(to_pyvalue_err)
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use std::{str::FromStr, sync::Arc};
299
300 use alloy_primitives::{U160, address};
301 use nautilus_core::UnixNanos;
302 use pyo3::{
303 Python,
304 types::{PyAnyMethods, PyInt},
305 };
306 use rstest::rstest;
307
308 use crate::defi::{
309 AmmType, Blockchain, Chain, Dex, DexType, Pool, PoolIdentifier, Token,
310 pool_analysis::PoolProfiler,
311 };
312
313 #[rstest]
314 fn price_sqrt_ratio_x96_returns_python_int() {
315 let sqrt_price_x96 = U160::from_str("79228162514264337593543950336").unwrap();
316 let mut profiler = PoolProfiler::new(pool());
317 profiler.initialize(sqrt_price_x96).unwrap();
318 Python::initialize();
319
320 Python::attach(|py| {
321 let value = profiler.py_price_sqrt_ratio_x96(py).unwrap();
322 let value = value.bind(py);
323
324 assert!(value.is_instance_of::<PyInt>());
325 assert_eq!(value.str().unwrap().to_string(), sqrt_price_x96.to_string());
326 });
327 }
328
329 fn pool() -> Arc<Pool> {
330 let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1));
331 let dex = Arc::new(Dex::new(
332 (*chain).clone(),
333 DexType::UniswapV3,
334 "0x0000000000000000000000000000000000000fac",
335 1,
336 AmmType::CLAMM,
337 "PoolCreated",
338 "Swap",
339 "Mint",
340 "Burn",
341 "Collect",
342 ));
343 let token0 = Token::new(
344 chain.clone(),
345 address!("0000000000000000000000000000000000000001"),
346 "USD Coin".to_string(),
347 "USDC".to_string(),
348 6,
349 );
350 let token1 = Token::new(
351 chain.clone(),
352 address!("0000000000000000000000000000000000000002"),
353 "Wrapped Ether".to_string(),
354 "WETH".to_string(),
355 18,
356 );
357 let pool_address = address!("0000000000000000000000000000000000000003");
358
359 Arc::new(Pool::new(
360 chain,
361 dex,
362 pool_address,
363 PoolIdentifier::from_address(pool_address),
364 1,
365 token0,
366 token1,
367 Some(500),
368 Some(10),
369 UnixNanos::default(),
370 ))
371 }
372}