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 #[pyo3(name = "get_active_liquidity")]
120 fn py_get_active_liquidity(&self) -> u128 {
121 self.get_active_liquidity()
122 }
123
124 #[pyo3(name = "get_active_tick_count")]
126 fn py_get_active_tick_count(&self) -> usize {
127 self.get_active_tick_count()
128 }
129
130 #[pyo3(name = "get_total_tick_count")]
138 fn py_get_total_tick_count(&self) -> usize {
139 self.get_total_tick_count()
140 }
141
142 #[pyo3(name = "get_total_active_positions")]
147 fn py_get_total_active_positions(&self) -> usize {
148 self.get_total_active_positions()
149 }
150
151 #[pyo3(name = "get_total_inactive_positions")]
156 fn py_get_total_inactive_positions(&self) -> usize {
157 self.get_total_inactive_positions()
158 }
159
160 #[pyo3(name = "estimate_balance_of_token0")]
167 fn py_estimate_balance_of_token0(&self) -> String {
168 self.estimate_balance_of_token0().to_string()
169 }
170
171 #[pyo3(name = "estimate_balance_of_token1")]
178 fn py_estimate_balance_of_token1(&self) -> String {
179 self.estimate_balance_of_token1().to_string()
180 }
181
182 #[pyo3(name = "get_total_liquidity")]
183 fn py_get_total_liquidity_all_positions(&self) -> String {
184 self.get_total_liquidity().to_string()
185 }
186
187 #[pyo3(name = "liquidity_utilization_rate")]
192 fn py_liquidity_utilization_rate(&self) -> f64 {
193 self.liquidity_utilization_rate()
194 }
195
196 #[pyo3(name = "swap_exact_in")]
198 fn py_swap_exact_in(
199 &self,
200 amount_in: &str,
201 zero_for_one: bool,
202 sqrt_price_limit_x96: Option<&str>,
203 ) -> PyResult<SwapQuote> {
204 let amount_in = U256::from_str(amount_in).map_err(to_pyvalue_err)?;
205 let sqrt_price_limit = match sqrt_price_limit_x96 {
206 Some(limit_str) => Some(U160::from_str(limit_str).map_err(to_pyvalue_err)?),
207 None => None,
208 };
209
210 self.swap_exact_in(amount_in, zero_for_one, sqrt_price_limit)
211 .map_err(to_pyvalue_err)
212 }
213
214 #[pyo3(name = "swap_exact_out")]
216 fn py_swap_exact_out(
217 &self,
218 amount_out: &str,
219 zero_for_one: bool,
220 sqrt_price_limit_x96: Option<&str>,
221 ) -> PyResult<SwapQuote> {
222 let amount_out = U256::from_str(amount_out).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_out(amount_out, zero_for_one, sqrt_price_limit)
229 .map_err(to_pyvalue_err)
230 }
231
232 #[pyo3(name = "size_for_impact_bps")]
247 fn py_size_for_impact_bps(&self, impact_bps: u32, zero_for_one: bool) -> PyResult<String> {
248 self.size_for_impact_bps(impact_bps, zero_for_one)
249 .map(|size| size.to_string())
250 .map_err(to_pyvalue_err)
251 }
252
253 #[pyo3(name = "size_for_impact_bps_detailed")]
261 fn py_size_for_impact_bps_detailed(
262 &self,
263 impact_bps: u32,
264 zero_for_one: bool,
265 ) -> PyResult<SizeForImpactResult> {
266 self.size_for_impact_bps_detailed(impact_bps, zero_for_one)
267 .map_err(to_pyvalue_err)
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use std::{str::FromStr, sync::Arc};
274
275 use alloy_primitives::{U160, address};
276 use nautilus_core::UnixNanos;
277 use pyo3::{
278 Python,
279 types::{PyAnyMethods, PyInt},
280 };
281 use rstest::rstest;
282
283 use crate::defi::{
284 AmmType, Blockchain, Chain, Dex, DexType, Pool, PoolIdentifier, Token,
285 pool_analysis::PoolProfiler,
286 };
287
288 #[rstest]
289 fn price_sqrt_ratio_x96_returns_python_int() {
290 let sqrt_price_x96 = U160::from_str("79228162514264337593543950336").unwrap();
291 let mut profiler = PoolProfiler::new(pool());
292 profiler.initialize(sqrt_price_x96).unwrap();
293 Python::initialize();
294
295 Python::attach(|py| {
296 let value = profiler.py_price_sqrt_ratio_x96(py).unwrap();
297 let value = value.bind(py);
298
299 assert!(value.is_instance_of::<PyInt>());
300 assert_eq!(value.str().unwrap().to_string(), sqrt_price_x96.to_string());
301 });
302 }
303
304 fn pool() -> Arc<Pool> {
305 let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1));
306 let dex = Arc::new(Dex::new(
307 (*chain).clone(),
308 DexType::UniswapV3,
309 "0x0000000000000000000000000000000000000fac",
310 1,
311 AmmType::CLAMM,
312 "PoolCreated",
313 "Swap",
314 "Mint",
315 "Burn",
316 "Collect",
317 ));
318 let token0 = Token::new(
319 chain.clone(),
320 address!("0000000000000000000000000000000000000001"),
321 "USD Coin".to_string(),
322 "USDC".to_string(),
323 6,
324 );
325 let token1 = Token::new(
326 chain.clone(),
327 address!("0000000000000000000000000000000000000002"),
328 "Wrapped Ether".to_string(),
329 "WETH".to_string(),
330 18,
331 );
332 let pool_address = address!("0000000000000000000000000000000000000003");
333
334 Arc::new(Pool::new(
335 chain,
336 dex,
337 pool_address,
338 PoolIdentifier::from_address(pool_address),
339 1,
340 token0,
341 token1,
342 Some(500),
343 Some(10),
344 UnixNanos::default(),
345 ))
346 }
347}