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