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]
34impl Chain {
35    #[new]
36    fn py_new(name: Blockchain, chain_id: u32) -> Self {
37        Self::new(name, chain_id)
38    }
39
40    fn __str__(&self) -> String {
41        self.to_string()
42    }
43
44    fn __repr__(&self) -> String {
45        format!("{self:?}")
46    }
47
48    fn __hash__(&self) -> u64 {
49        let mut hasher = DefaultHasher::new();
50        self.chain_id.hash(&mut hasher);
51        hasher.finish()
52    }
53
54    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
55        match op {
56            CompareOp::Eq => self == other,
57            CompareOp::Ne => self != other,
58            _ => panic!("Unsupported comparison for Chain"),
59        }
60    }
61
62    #[getter]
63    #[pyo3(name = "name")]
64    fn py_name(&self) -> Blockchain {
65        self.name
66    }
67
68    #[getter]
69    #[pyo3(name = "chain_id")]
70    fn py_chain_id(&self) -> u32 {
71        self.chain_id
72    }
73
74    #[getter]
75    #[pyo3(name = "hypersync_url")]
76    fn py_hypersync_url(&self) -> &str {
77        &self.hypersync_url
78    }
79
80    #[getter]
81    #[pyo3(name = "rpc_url")]
82    fn py_rpc_url(&self) -> Option<&str> {
83        self.rpc_url.as_deref()
84    }
85
86    #[getter]
87    #[pyo3(name = "native_currency_decimals")]
88    fn py_native_currency_decimals(&self) -> u8 {
89        self.native_currency_decimals
90    }
91
92    #[pyo3(name = "set_rpc_url")]
93    fn py_set_rpc_url(&mut self, rpc_url: String) {
94        self.set_rpc_url(rpc_url);
95    }
96
97    #[staticmethod]
98    #[pyo3(name = "from_chain_name")]
99    fn py_from_chain_name(chain_name: &str) -> PyResult<Self> {
100        Self::from_chain_name(chain_name).cloned().ok_or_else(|| {
101            pyo3::exceptions::PyValueError::new_err(format!(
102                "`chain_name` '{chain_name}' is not recognized",
103            ))
104        })
105    }
106
107    #[staticmethod]
108    #[pyo3(name = "from_chain_id")]
109    fn py_from_chain_id(chain_id: u32) -> Option<Self> {
110        Self::from_chain_id(chain_id).cloned()
111    }
112
113    #[staticmethod]
114    #[pyo3(name = "ARBITRUM")]
115    fn py_arbitrum_chain() -> Self {
116        chains::ARBITRUM.clone()
117    }
118}
119
120#[pymethods]
121impl Token {
122    #[new]
123    fn py_new(
124        chain: Chain,
125        address: String,
126        name: String,
127        symbol: String,
128        decimals: u8,
129    ) -> PyResult<Self> {
130        let address = address.parse().map_err(to_pyvalue_err)?;
131        Ok(Self::new(Arc::new(chain), address, name, symbol, decimals))
132    }
133
134    fn __str__(&self) -> String {
135        self.to_string()
136    }
137
138    fn __repr__(&self) -> String {
139        format!("{self:?}")
140    }
141
142    fn __hash__(&self) -> u64 {
143        let mut hasher = DefaultHasher::new();
144        self.chain.chain_id.hash(&mut hasher);
145        self.address.hash(&mut hasher);
146        hasher.finish()
147    }
148
149    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
150        match op {
151            CompareOp::Eq => self == other,
152            CompareOp::Ne => self != other,
153            _ => panic!("Unsupported comparison for Token"),
154        }
155    }
156
157    #[getter]
158    #[pyo3(name = "chain")]
159    fn py_chain(&self) -> PyResult<Chain> {
160        Ok(self.chain.as_ref().clone())
161    }
162
163    #[getter]
164    #[pyo3(name = "address")]
165    fn py_address(&self) -> String {
166        self.address.to_string()
167    }
168
169    #[getter]
170    #[pyo3(name = "name")]
171    fn py_name(&self) -> &str {
172        &self.name
173    }
174
175    #[getter]
176    #[pyo3(name = "symbol")]
177    fn py_symbol(&self) -> &str {
178        &self.symbol
179    }
180
181    #[getter]
182    #[pyo3(name = "decimals")]
183    fn py_decimals(&self) -> u8 {
184        self.decimals
185    }
186}
187
188#[pymethods]
189impl Dex {
190    #[new]
191    #[allow(clippy::too_many_arguments)]
192    fn py_new(
193        chain: Chain,
194        name: String,
195        factory: String,
196        factory_creation_block: u64,
197        amm_type: String,
198        pool_created_event: &str,
199        swap_event: &str,
200        mint_event: &str,
201        burn_event: &str,
202        collect_event: &str,
203    ) -> PyResult<Self> {
204        let amm_type = AmmType::from_str(&amm_type).map_err(to_pyvalue_err)?;
205        let dex_type = DexType::from_dex_name(&name)
206            .ok_or_else(|| to_pyvalue_err(format!("Invalid DEX name: {name}")))?;
207        Ok(Self::new(
208            chain,
209            dex_type,
210            &factory,
211            factory_creation_block,
212            amm_type,
213            pool_created_event,
214            swap_event,
215            mint_event,
216            burn_event,
217            collect_event,
218        ))
219    }
220
221    fn __str__(&self) -> String {
222        self.to_string()
223    }
224
225    fn __repr__(&self) -> String {
226        format!("{self:?}")
227    }
228
229    fn __hash__(&self) -> u64 {
230        let mut hasher = DefaultHasher::new();
231        self.chain.chain_id.hash(&mut hasher);
232        self.name.hash(&mut hasher);
233        self.factory.hash(&mut hasher);
234        hasher.finish()
235    }
236
237    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
238        match op {
239            CompareOp::Eq => self == other,
240            CompareOp::Ne => self != other,
241            _ => panic!("Unsupported comparison for Dex"),
242        }
243    }
244
245    #[getter]
246    #[pyo3(name = "chain")]
247    fn py_chain(&self) -> Chain {
248        self.chain.clone()
249    }
250
251    #[getter]
252    #[pyo3(name = "name")]
253    fn py_name(&self) -> DexType {
254        self.name
255    }
256
257    #[getter]
258    #[pyo3(name = "factory")]
259    fn py_factory(&self) -> String {
260        self.factory.to_string()
261    }
262
263    #[getter]
264    #[pyo3(name = "factory_creation_block")]
265    fn py_factory_creation_block(&self) -> u64 {
266        self.factory_creation_block
267    }
268
269    #[getter]
270    #[pyo3(name = "pool_created_event")]
271    fn py_pool_created_event(&self) -> &str {
272        &self.pool_created_event
273    }
274
275    #[getter]
276    #[pyo3(name = "swap_created_event")]
277    fn py_swap_created_event(&self) -> &str {
278        &self.swap_created_event
279    }
280
281    #[getter]
282    #[pyo3(name = "mint_created_event")]
283    fn py_mint_created_event(&self) -> &str {
284        &self.mint_created_event
285    }
286
287    #[getter]
288    #[pyo3(name = "burn_created_event")]
289    fn py_burn_created_event(&self) -> &str {
290        &self.burn_created_event
291    }
292
293    #[getter]
294    #[pyo3(name = "amm_type")]
295    fn py_amm_type(&self) -> AmmType {
296        self.amm_type
297    }
298}
299
300#[pymethods]
301impl Pool {
302    #[new]
303    #[allow(clippy::too_many_arguments)]
304    fn py_new(
305        chain: Chain,
306        dex: Dex,
307        address: String,
308        pool_identifier: String,
309        creation_block: u64,
310        token0: Token,
311        token1: Token,
312        fee: Option<u32>,
313        tick_spacing: Option<u32>,
314        ts_init: u64,
315    ) -> PyResult<Self> {
316        let address = address.parse().map_err(to_pyvalue_err)?;
317        let pool_identifier = pool_identifier.parse().map_err(to_pyvalue_err)?;
318        Ok(Self::new(
319            Arc::new(chain),
320            Arc::new(dex),
321            address,
322            pool_identifier,
323            creation_block,
324            token0,
325            token1,
326            fee,
327            tick_spacing,
328            ts_init.into(),
329        ))
330    }
331
332    fn __str__(&self) -> String {
333        self.to_string()
334    }
335
336    fn __repr__(&self) -> String {
337        format!("{self:?}")
338    }
339
340    fn __hash__(&self) -> u64 {
341        let mut hasher = DefaultHasher::new();
342        self.chain.chain_id.hash(&mut hasher);
343        self.address.hash(&mut hasher);
344        hasher.finish()
345    }
346
347    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
348        match op {
349            CompareOp::Eq => self == other,
350            CompareOp::Ne => self != other,
351            _ => panic!("Unsupported comparison for Pool"),
352        }
353    }
354
355    #[getter]
356    #[pyo3(name = "chain")]
357    fn py_chain(&self) -> PyResult<Chain> {
358        Ok(self.chain.as_ref().clone())
359    }
360
361    #[getter]
362    #[pyo3(name = "dex")]
363    fn py_dex(&self) -> PyResult<Dex> {
364        Ok(self.dex.as_ref().clone())
365    }
366
367    #[getter]
368    #[pyo3(name = "instrument_id")]
369    fn py_instrument_id(&self) -> InstrumentId {
370        self.instrument_id
371    }
372
373    #[getter]
374    #[pyo3(name = "address")]
375    fn py_address(&self) -> String {
376        self.address.to_string()
377    }
378
379    #[getter]
380    #[pyo3(name = "creation_block")]
381    fn py_creation_block(&self) -> u64 {
382        self.creation_block
383    }
384
385    #[getter]
386    #[pyo3(name = "token0")]
387    fn py_token0(&self) -> Token {
388        self.token0.clone()
389    }
390
391    #[getter]
392    #[pyo3(name = "token1")]
393    fn py_token1(&self) -> Token {
394        self.token1.clone()
395    }
396
397    #[getter]
398    #[pyo3(name = "fee")]
399    fn py_fee(&self) -> Option<u32> {
400        self.fee
401    }
402
403    #[getter]
404    #[pyo3(name = "tick_spacing")]
405    fn py_tick_spacing(&self) -> Option<u32> {
406        self.tick_spacing
407    }
408
409    #[getter]
410    #[pyo3(name = "ts_init")]
411    fn py_ts_init(&self) -> u64 {
412        self.ts_init.as_u64()
413    }
414}