Skip to main content

nautilus_model/python/defi/
types.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 types.
17
18use std::{
19    collections::hash_map::DefaultHasher,
20    hash::{Hash, Hasher},
21    str::FromStr,
22    sync::Arc,
23};
24
25use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
26use pyo3::{basic::CompareOp, prelude::*};
27
28use crate::{
29    defi::{
30        AmmType, Blockchain, Chain, Dex, DexType, Pool, Token, chain::chains,
31        validation::validate_address,
32    },
33    identifiers::InstrumentId,
34};
35
36#[pymethods]
37#[pyo3_stub_gen::derive::gen_stub_pymethods]
38impl Chain {
39    /// Defines a blockchain with its unique identifiers and connection details for network interaction.
40    #[new]
41    fn py_new(name: Blockchain, chain_id: u32) -> Self {
42        Self::new(name, chain_id)
43    }
44
45    fn __str__(&self) -> String {
46        self.to_string()
47    }
48
49    fn __repr__(&self) -> String {
50        format!("{self:?}")
51    }
52
53    fn __hash__(&self) -> u64 {
54        let mut hasher = DefaultHasher::new();
55        self.chain_id.hash(&mut hasher);
56        hasher.finish()
57    }
58
59    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
60        match op {
61            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
62            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
63            _ => py.NotImplemented(),
64        }
65    }
66
67    #[getter]
68    #[pyo3(name = "name")]
69    fn py_name(&self) -> Blockchain {
70        self.name
71    }
72
73    #[getter]
74    #[pyo3(name = "chain_id")]
75    fn py_chain_id(&self) -> u32 {
76        self.chain_id
77    }
78
79    #[getter]
80    #[pyo3(name = "hypersync_url")]
81    fn py_hypersync_url(&self) -> &str {
82        &self.hypersync_url
83    }
84
85    #[getter]
86    #[pyo3(name = "rpc_url")]
87    fn py_rpc_url(&self) -> Option<&str> {
88        self.rpc_url.as_deref()
89    }
90
91    #[getter]
92    #[pyo3(name = "native_currency_decimals")]
93    fn py_native_currency_decimals(&self) -> u8 {
94        self.native_currency_decimals
95    }
96
97    /// Sets the RPC URL endpoint.
98    #[pyo3(name = "set_rpc_url")]
99    fn py_set_rpc_url(&mut self, rpc_url: String) {
100        self.set_rpc_url(rpc_url);
101    }
102
103    /// Returns a reference to the `Chain` corresponding to the given chain name, or `None` if it is not found.
104    ///
105    /// String matching is case-insensitive.
106    #[staticmethod]
107    #[pyo3(name = "from_chain_name")]
108    fn py_from_chain_name(chain_name: &str) -> PyResult<Self> {
109        Self::from_chain_name(chain_name)
110            .cloned()
111            .ok_or_else(|| to_pyvalue_err(format!("`chain_name` '{chain_name}' is not recognized")))
112    }
113
114    /// Returns a reference to the `Chain` corresponding to the given `chain_id`, or `None` if it is not found.
115    #[staticmethod]
116    #[pyo3(name = "from_chain_id")]
117    fn py_from_chain_id(chain_id: u32) -> Option<Self> {
118        Self::from_chain_id(chain_id).cloned()
119    }
120
121    #[staticmethod]
122    #[pyo3(name = "ARBITRUM")]
123    fn py_arbitrum_chain() -> Self {
124        chains::ARBITRUM.clone()
125    }
126}
127
128#[pymethods]
129#[pyo3_stub_gen::derive::gen_stub_pymethods]
130impl Token {
131    /// Represents a cryptocurrency token on a blockchain network.
132    #[new]
133    #[expect(clippy::needless_pass_by_value)]
134    fn py_new(
135        chain: Chain,
136        address: String,
137        name: String,
138        symbol: String,
139        decimals: u8,
140    ) -> PyResult<Self> {
141        let address = address.parse().map_err(to_pyvalue_err)?;
142        Ok(Self::new(Arc::new(chain), address, name, symbol, decimals))
143    }
144
145    fn __str__(&self) -> String {
146        self.to_string()
147    }
148
149    fn __repr__(&self) -> String {
150        format!("{self:?}")
151    }
152
153    fn __hash__(&self) -> u64 {
154        let mut hasher = DefaultHasher::new();
155        self.chain.chain_id.hash(&mut hasher);
156        self.address.hash(&mut hasher);
157        hasher.finish()
158    }
159
160    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
161        match op {
162            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
163            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
164            _ => py.NotImplemented(),
165        }
166    }
167
168    #[getter]
169    #[pyo3(name = "chain")]
170    fn py_chain(&self) -> Chain {
171        self.chain.as_ref().clone()
172    }
173
174    #[getter]
175    #[pyo3(name = "address")]
176    fn py_address(&self) -> String {
177        self.address.to_string()
178    }
179
180    #[getter]
181    #[pyo3(name = "name")]
182    fn py_name(&self) -> &str {
183        &self.name
184    }
185
186    #[getter]
187    #[pyo3(name = "symbol")]
188    fn py_symbol(&self) -> &str {
189        &self.symbol
190    }
191
192    #[getter]
193    #[pyo3(name = "decimals")]
194    fn py_decimals(&self) -> u8 {
195        self.decimals
196    }
197}
198
199#[pymethods]
200#[pyo3_stub_gen::derive::gen_stub_pymethods]
201impl Dex {
202    /// Represents a decentralized exchange (DEX) in a blockchain ecosystem.
203    #[new]
204    #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
205    fn py_new(
206        chain: Chain,
207        name: String,
208        factory: String,
209        factory_creation_block: u64,
210        amm_type: String,
211        pool_created_event: &str,
212        swap_event: &str,
213        mint_event: &str,
214        burn_event: &str,
215        collect_event: &str,
216    ) -> PyResult<Self> {
217        let amm_type = AmmType::from_str(&amm_type).map_err(to_pyvalue_err)?;
218        let dex_type = DexType::from_dex_name(&name)
219            .ok_or_else(|| to_pyvalue_err(format!("Invalid DEX name: {name}")))?;
220
221        validate_address(&factory).map_err(to_pyvalue_err)?;
222
223        Ok(Self::new(
224            chain,
225            dex_type,
226            &factory,
227            factory_creation_block,
228            amm_type,
229            pool_created_event,
230            swap_event,
231            mint_event,
232            burn_event,
233            collect_event,
234        ))
235    }
236
237    fn __str__(&self) -> String {
238        self.to_string()
239    }
240
241    fn __repr__(&self) -> String {
242        format!("{self:?}")
243    }
244
245    fn __hash__(&self) -> u64 {
246        let mut hasher = DefaultHasher::new();
247        self.chain.chain_id.hash(&mut hasher);
248        self.name.hash(&mut hasher);
249        self.factory.hash(&mut hasher);
250        hasher.finish()
251    }
252
253    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
254        match op {
255            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
256            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
257            _ => py.NotImplemented(),
258        }
259    }
260
261    #[getter]
262    #[pyo3(name = "chain")]
263    fn py_chain(&self) -> Chain {
264        self.chain.clone()
265    }
266
267    #[getter]
268    #[pyo3(name = "name")]
269    fn py_name(&self) -> DexType {
270        self.name
271    }
272
273    #[getter]
274    #[pyo3(name = "factory")]
275    fn py_factory(&self) -> String {
276        self.factory.to_string()
277    }
278
279    #[getter]
280    #[pyo3(name = "factory_creation_block")]
281    fn py_factory_creation_block(&self) -> u64 {
282        self.factory_creation_block
283    }
284
285    #[getter]
286    #[pyo3(name = "pool_created_event")]
287    fn py_pool_created_event(&self) -> &str {
288        &self.pool_created_event
289    }
290
291    #[getter]
292    #[pyo3(name = "swap_created_event")]
293    fn py_swap_created_event(&self) -> &str {
294        &self.swap_created_event
295    }
296
297    #[getter]
298    #[pyo3(name = "mint_created_event")]
299    fn py_mint_created_event(&self) -> &str {
300        &self.mint_created_event
301    }
302
303    #[getter]
304    #[pyo3(name = "burn_created_event")]
305    fn py_burn_created_event(&self) -> &str {
306        &self.burn_created_event
307    }
308
309    #[getter]
310    #[pyo3(name = "amm_type")]
311    fn py_amm_type(&self) -> AmmType {
312        self.amm_type
313    }
314}
315
316#[pymethods]
317#[pyo3_stub_gen::derive::gen_stub_pymethods]
318impl Pool {
319    /// Represents a liquidity pool in a decentralized exchange.
320    ///
321    /// ## Pool Identification Architecture
322    ///
323    /// Pools are identified differently depending on the DEX protocol version:
324    ///
325    /// **UniswapV2/V3**: Each pool has its own smart contract deployed at a unique address.
326    /// - `address` = pool contract address
327    /// - `pool_identifier` = same as address (hex string)
328    ///
329    /// **`UniswapV4`**: All pools share a singleton `PoolManager` contract. Pools are distinguished
330    /// by a unique Pool ID (keccak256 hash of currencies, fee, tick spacing, and hooks).
331    /// - `address` = `PoolManager` contract address (shared by all pools)
332    /// - `pool_identifier` = Pool ID (bytes32 as hex string)
333    ///
334    /// ## Instrument ID Format
335    ///
336    /// The instrument ID encodes with the following components:
337    /// - `symbol` - The pool identifier (address for V2/V3, Pool ID for V4)
338    /// - `venue`  - The chain name plus DEX ID
339    ///
340    /// String representation: `<POOL_IDENTIFIER>.<CHAIN_NAME>:<DEX_ID>`
341    ///
342    /// Example: `0x11b815efB8f581194ae79006d24E0d814B7697F6.Ethereum:UniswapV3`
343    #[new]
344    #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
345    fn py_new(
346        chain: Chain,
347        dex: Dex,
348        address: String,
349        pool_identifier: String,
350        creation_block: u64,
351        token0: Token,
352        token1: Token,
353        fee: Option<u32>,
354        tick_spacing: Option<u32>,
355        ts_init: u64,
356    ) -> PyResult<Self> {
357        let address = address.parse().map_err(to_pyvalue_err)?;
358        let pool_identifier = pool_identifier.parse().map_err(to_pyvalue_err)?;
359        Ok(Self::new(
360            Arc::new(chain),
361            Arc::new(dex),
362            address,
363            pool_identifier,
364            creation_block,
365            token0,
366            token1,
367            fee,
368            tick_spacing,
369            ts_init.into(),
370        ))
371    }
372
373    fn __str__(&self) -> String {
374        self.to_string()
375    }
376
377    fn __repr__(&self) -> String {
378        format!("{self:?}")
379    }
380
381    fn __hash__(&self) -> u64 {
382        let mut hasher = DefaultHasher::new();
383        self.chain.chain_id.hash(&mut hasher);
384        self.address.hash(&mut hasher);
385        hasher.finish()
386    }
387
388    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
389        match op {
390            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
391            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
392            _ => py.NotImplemented(),
393        }
394    }
395
396    #[getter]
397    #[pyo3(name = "chain")]
398    fn py_chain(&self) -> Chain {
399        self.chain.as_ref().clone()
400    }
401
402    #[getter]
403    #[pyo3(name = "dex")]
404    fn py_dex(&self) -> Dex {
405        self.dex.as_ref().clone()
406    }
407
408    #[getter]
409    #[pyo3(name = "instrument_id")]
410    fn py_instrument_id(&self) -> InstrumentId {
411        self.instrument_id
412    }
413
414    #[getter]
415    #[pyo3(name = "address")]
416    fn py_address(&self) -> String {
417        self.address.to_string()
418    }
419
420    #[getter]
421    #[pyo3(name = "creation_block")]
422    fn py_creation_block(&self) -> u64 {
423        self.creation_block
424    }
425
426    #[getter]
427    #[pyo3(name = "token0")]
428    fn py_token0(&self) -> Token {
429        self.token0.clone()
430    }
431
432    #[getter]
433    #[pyo3(name = "token1")]
434    fn py_token1(&self) -> Token {
435        self.token1.clone()
436    }
437
438    #[getter]
439    #[pyo3(name = "fee")]
440    fn py_fee(&self) -> Option<u32> {
441        self.fee
442    }
443
444    #[getter]
445    #[pyo3(name = "tick_spacing")]
446    fn py_tick_spacing(&self) -> Option<u32> {
447        self.tick_spacing
448    }
449
450    #[getter]
451    #[pyo3(name = "ts_event")]
452    fn py_ts_event(&self) -> u64 {
453        self.ts_event.as_u64()
454    }
455
456    #[getter]
457    #[pyo3(name = "ts_init")]
458    fn py_ts_init(&self) -> u64 {
459        self.ts_init.as_u64()
460    }
461}