Skip to main content

nautilus_model/python/defi/
profiler.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for DeFi pool profiler.
17
18use std::str::FromStr;
19
20use alloy_primitives::{U160, U256};
21use nautilus_core::python::to_pyvalue_err;
22use pyo3::prelude::*;
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    fn py_price_sqrt_ratio_x96(&self) -> String {
62        self.state.price_sqrt_ratio_x96.to_string()
63    }
64
65    #[getter]
66    #[pyo3(name = "total_amount0_deposited")]
67    fn py_total_amount0_deposited(&self) -> String {
68        self.analytics.total_amount0_deposited.to_string()
69    }
70
71    #[getter]
72    #[pyo3(name = "total_amount1_deposited")]
73    fn py_total_amount1_deposited(&self) -> String {
74        self.analytics.total_amount1_deposited.to_string()
75    }
76
77    #[getter]
78    #[pyo3(name = "total_amount0_collected")]
79    fn py_total_amount0_collected(&self) -> String {
80        self.analytics.total_amount0_collected.to_string()
81    }
82
83    #[getter]
84    #[pyo3(name = "total_amount1_collected")]
85    fn py_total_amount1_collected(&self) -> String {
86        self.analytics.total_amount1_collected.to_string()
87    }
88
89    #[getter]
90    #[pyo3(name = "protocol_fees_token0")]
91    fn py_protocol_fees_token0(&self) -> String {
92        self.state.protocol_fees_token0.to_string()
93    }
94
95    #[getter]
96    #[pyo3(name = "protocol_fees_token1")]
97    fn py_protocol_fees_token1(&self) -> String {
98        self.state.protocol_fees_token1.to_string()
99    }
100
101    #[getter]
102    #[pyo3(name = "fee_protocol")]
103    fn py_fee_protocol(&self) -> u8 {
104        self.state.fee_protocol
105    }
106
107    /// Returns the pool's active liquidity tracked by the tick map.
108    ///
109    /// This represents the effective liquidity available for trading at the current price.
110    /// The tick map maintains this value efficiently by updating it during tick crossings
111    /// as the price moves through different ranges.
112    ///
113    /// # Returns
114    /// The active liquidity (u128) at the current tick from the tick map
115    #[pyo3(name = "get_active_liquidity")]
116    fn py_get_active_liquidity(&self) -> u128 {
117        self.get_active_liquidity()
118    }
119
120    /// Gets the number of active ticks.
121    #[pyo3(name = "get_active_tick_count")]
122    fn py_get_active_tick_count(&self) -> usize {
123        self.get_active_tick_count()
124    }
125
126    /// Gets the total number of ticks tracked by the tick map.
127    ///
128    /// Returns count of all ticks that have ever been initialized,
129    /// including those that may no longer have active liquidity.
130    ///
131    /// # Returns
132    /// Total tick count in the tick map
133    #[pyo3(name = "get_total_tick_count")]
134    fn py_get_total_tick_count(&self) -> usize {
135        self.get_total_tick_count()
136    }
137
138    /// Gets the count of positions that are currently active.
139    ///
140    /// Active positions are those with liquidity > 0 and whose tick range
141    /// includes the current pool tick (meaning they have tokens in the pool).
142    #[pyo3(name = "get_total_active_positions")]
143    fn py_get_total_active_positions(&self) -> usize {
144        self.get_total_active_positions()
145    }
146
147    /// Gets the count of positions that are currently inactive.
148    ///
149    /// Inactive positions are those that exist but don't span the current tick,
150    /// meaning their liquidity is entirely in one token or the other.
151    #[pyo3(name = "get_total_inactive_positions")]
152    fn py_get_total_inactive_positions(&self) -> usize {
153        self.get_total_inactive_positions()
154    }
155
156    /// Estimates the total amount of token0 in the pool.
157    ///
158    /// Calculates token0 balance by summing:
159    /// - Token0 amounts from all active liquidity positions
160    /// - Accumulated trading fees (approximated from fee growth)
161    /// - Protocol fees collected
162    #[pyo3(name = "estimate_balance_of_token0")]
163    fn py_estimate_balance_of_token0(&self) -> String {
164        self.estimate_balance_of_token0().to_string()
165    }
166
167    /// Estimates the total amount of token1 in the pool.
168    ///
169    /// Calculates token1 balance by summing:
170    /// - Token1 amounts from all active liquidity positions
171    /// - Accumulated trading fees (approximated from fee growth)
172    /// - Protocol fees collected
173    #[pyo3(name = "estimate_balance_of_token1")]
174    fn py_estimate_balance_of_token1(&self) -> String {
175        self.estimate_balance_of_token1().to_string()
176    }
177
178    #[pyo3(name = "get_total_liquidity")]
179    fn py_get_total_liquidity_all_positions(&self) -> String {
180        self.get_total_liquidity().to_string()
181    }
182
183    /// Calculates the liquidity utilization rate for the pool.
184    ///
185    /// The utilization rate measures what percentage of total deployed liquidity
186    /// is currently active (in-range and earning fees) at the current price tick.
187    #[pyo3(name = "liquidity_utilization_rate")]
188    fn py_liquidity_utilization_rate(&self) -> f64 {
189        self.liquidity_utilization_rate()
190    }
191
192    /// Simulates an exact input swap (know input amount, calculate output amount).
193    #[pyo3(name = "swap_exact_in")]
194    fn py_swap_exact_in(
195        &self,
196        amount_in: &str,
197        zero_for_one: bool,
198        sqrt_price_limit_x96: Option<&str>,
199    ) -> PyResult<SwapQuote> {
200        let amount_in = U256::from_str(amount_in).map_err(to_pyvalue_err)?;
201        let sqrt_price_limit = match sqrt_price_limit_x96 {
202            Some(limit_str) => Some(U160::from_str(limit_str).map_err(to_pyvalue_err)?),
203            None => None,
204        };
205
206        self.swap_exact_in(amount_in, zero_for_one, sqrt_price_limit)
207            .map_err(to_pyvalue_err)
208    }
209
210    /// Simulates an exact output swap (know output amount, calculate required input amount).
211    #[pyo3(name = "swap_exact_out")]
212    fn py_swap_exact_out(
213        &self,
214        amount_out: &str,
215        zero_for_one: bool,
216        sqrt_price_limit_x96: Option<&str>,
217    ) -> PyResult<SwapQuote> {
218        let amount_out = U256::from_str(amount_out).map_err(to_pyvalue_err)?;
219        let sqrt_price_limit = match sqrt_price_limit_x96 {
220            Some(limit_str) => Some(U160::from_str(limit_str).map_err(to_pyvalue_err)?),
221            None => None,
222        };
223
224        self.swap_exact_out(amount_out, zero_for_one, sqrt_price_limit)
225            .map_err(to_pyvalue_err)
226    }
227
228    /// Finds the maximum trade size that produces a target slippage (including fees).
229    ///
230    /// Uses binary search to find the largest trade size that results in slippage
231    /// at or below the target. The method iteratively simulates swaps at different
232    /// sizes until it converges to the optimal size within the specified tolerance.
233    ///
234    /// # Returns
235    /// The maximum trade size (U256) that produces the target slippage
236    ///
237    /// # Errors
238    /// Returns error if:
239    /// - Impact is zero or exceeds 100% (10000 bps)
240    /// - Pool is not initialized
241    /// - Swap simulations fail
242    #[pyo3(name = "size_for_impact_bps")]
243    fn py_size_for_impact_bps(&self, impact_bps: u32, zero_for_one: bool) -> PyResult<String> {
244        self.size_for_impact_bps(impact_bps, zero_for_one)
245            .map(|size| size.to_string())
246            .map_err(to_pyvalue_err)
247    }
248
249    /// Finds the maximum trade size with search diagnostics.
250    /// This is the detailed version of `Self.size_for_impact_bps` that returns
251    /// extensive information about the search process.It is useful for debugging,
252    /// monitoring, and analyzing search behavior in production.
253    ///
254    /// # Returns
255    /// Detailed result with size and search diagnostics
256    #[pyo3(name = "size_for_impact_bps_detailed")]
257    fn py_size_for_impact_bps_detailed(
258        &self,
259        impact_bps: u32,
260        zero_for_one: bool,
261    ) -> PyResult<SizeForImpactResult> {
262        self.size_for_impact_bps_detailed(impact_bps, zero_for_one)
263            .map_err(to_pyvalue_err)
264    }
265}