Skip to main content

nautilus_model/defi/
dex.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
16use std::{borrow::Cow, fmt::Display, str::FromStr, sync::Arc};
17
18use alloy_primitives::{Address, keccak256};
19use nautilus_core::hex;
20use serde::{Deserialize, Serialize};
21use strum::{Display, EnumIter, EnumString};
22
23use crate::{
24    defi::{amm::Pool, chain::Chain, validation::validate_address},
25    identifiers::{InstrumentId, Symbol, Venue},
26    instruments::{Instrument, any::InstrumentAny, currency_pair::CurrencyPair},
27    types::{currency::Currency, fixed::FIXED_PRECISION, price::Price, quantity::Quantity},
28};
29
30/// Represents different types of Automated Market Makers (AMMs) in DeFi protocols.
31#[derive(
32    Debug,
33    Clone,
34    Copy,
35    Hash,
36    PartialEq,
37    Eq,
38    Serialize,
39    Deserialize,
40    strum::EnumString,
41    strum::Display,
42    strum::EnumIter,
43)]
44#[cfg_attr(
45    feature = "python",
46    pyo3::pyclass(
47        frozen,
48        eq,
49        eq_int,
50        module = "nautilus_trader.model",
51        from_py_object,
52        rename_all = "SCREAMING_SNAKE_CASE",
53    )
54)]
55#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
56#[non_exhaustive]
57pub enum AmmType {
58    /// Constant Product Automated Market Maker.
59    CPAMM,
60    /// Concentrated Liquidity Automated Market Maker.
61    CLAMM,
62    /// Concentrated liquidity AMM **with hooks** (e.g. upcoming Uniswap v4).
63    CLAMEnhanced,
64    /// Specialized Constant-Sum AMM for low-volatility assets (Curve-style “`StableSwap`”).
65    StableSwap,
66    /// AMM with customizable token weights (e.g., Balancer style).
67    WeightedPool,
68    /// Advanced pool type that can nest other pools (Balancer V3).
69    ComposablePool,
70}
71
72/// Represents different types of decentralized exchanges (DEXes) supported by Nautilus.
73#[derive(
74    Debug,
75    Clone,
76    Copy,
77    Hash,
78    PartialOrd,
79    PartialEq,
80    Ord,
81    Eq,
82    Display,
83    EnumIter,
84    EnumString,
85    Serialize,
86    Deserialize,
87)]
88#[cfg_attr(
89    feature = "python",
90    pyo3::pyclass(
91        frozen,
92        eq,
93        eq_int,
94        module = "nautilus_trader.model",
95        from_py_object,
96        rename_all = "SCREAMING_SNAKE_CASE",
97    )
98)]
99#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
100pub enum DexType {
101    AerodromeSlipstream,
102    AerodromeV1,
103    BalancerV2,
104    BalancerV3,
105    BaseSwapV2,
106    BaseX,
107    CamelotV3,
108    CurveFinance,
109    FluidDEX,
110    MaverickV1,
111    MaverickV2,
112    PancakeSwapV3,
113    SushiSwapV2,
114    SushiSwapV3,
115    UniswapV2,
116    UniswapV3,
117    UniswapV4,
118}
119
120impl DexType {
121    /// Returns a reference to the `DexType` corresponding to the given dex name, or `None` if it is not found.
122    #[must_use]
123    pub fn from_dex_name(dex_name: &str) -> Option<Self> {
124        Self::from_str(dex_name).ok()
125    }
126}
127
128/// Represents a decentralized exchange (DEX) in a blockchain ecosystem.
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[cfg_attr(
131    feature = "python",
132    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
133)]
134#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
135pub struct Dex {
136    /// The blockchain network where this DEX operates.
137    pub chain: Chain,
138    /// The variant of the DEX protocol.
139    pub name: DexType,
140    /// The blockchain address of the DEX factory contract.
141    pub factory: Address,
142    /// The block number at which the DEX factory contract was deployed.
143    pub factory_creation_block: u64,
144    /// The event signature or identifier used to detect pool creation events.
145    pub pool_created_event: Cow<'static, str>,
146    // Optional Initialize event signature emitted when pool is initialized.
147    pub initialize_event: Option<Cow<'static, str>>,
148    /// The event signature or identifier used to detect swap events.
149    pub swap_created_event: Cow<'static, str>,
150    /// The event signature or identifier used to detect mint events.
151    pub mint_created_event: Cow<'static, str>,
152    /// The event signature or identifier used to detect burn events.
153    pub burn_created_event: Cow<'static, str>,
154    /// The event signature or identifier used to detect collect fee events.
155    pub collect_created_event: Cow<'static, str>,
156    // Optional Flash event signature emitted when flash loan occurs.
157    pub flash_created_event: Option<Cow<'static, str>>,
158    // Optional SetFeeProtocol event signature emitted when the protocol-fee config changes.
159    pub fee_protocol_update_event: Option<Cow<'static, str>>,
160    // Optional CollectProtocol event signature emitted when protocol fees are withdrawn.
161    pub fee_protocol_collect_event: Option<Cow<'static, str>>,
162    /// The type of automated market maker (AMM) algorithm used by this DEX.
163    pub amm_type: AmmType,
164    /// Collection of liquidity pools managed by this DEX.
165    #[allow(dead_code)]
166    pairs: Vec<Pool>,
167}
168
169/// A thread-safe shared pointer to a `Dex`, enabling efficient reuse across multiple components.
170pub type SharedDex = Arc<Dex>;
171
172impl Dex {
173    /// Creates a new [`Dex`] instance with the specified properties.
174    ///
175    /// # Panics
176    ///
177    /// Panics if the provided factory address is invalid.
178    #[must_use]
179    #[expect(clippy::too_many_arguments)]
180    pub fn new(
181        chain: Chain,
182        name: DexType,
183        factory: &str,
184        factory_creation_block: u64,
185        amm_type: AmmType,
186        pool_created_event: &str,
187        swap_event: &str,
188        mint_event: &str,
189        burn_event: &str,
190        collect_event: &str,
191    ) -> Self {
192        let encoded_pool_created_event =
193            hex::encode_prefixed(keccak256(pool_created_event.as_bytes()));
194        let encoded_swap_event = hex::encode_prefixed(keccak256(swap_event.as_bytes()));
195        let encoded_mint_event = hex::encode_prefixed(keccak256(mint_event.as_bytes()));
196        let encoded_burn_event = hex::encode_prefixed(keccak256(burn_event.as_bytes()));
197        let encoded_collect_event = hex::encode_prefixed(keccak256(collect_event.as_bytes()));
198        let factory_address = match validate_address(factory) {
199            Ok(address) => address,
200            Err(e) => panic!(
201                "Invalid factory address for DEX {name} on chain {chain} for factory address {factory}: {e}"
202            ),
203        };
204        Self {
205            chain,
206            name,
207            factory: factory_address,
208            factory_creation_block,
209            pool_created_event: encoded_pool_created_event.into(),
210            initialize_event: None,
211            swap_created_event: encoded_swap_event.into(),
212            mint_created_event: encoded_mint_event.into(),
213            burn_created_event: encoded_burn_event.into(),
214            collect_created_event: encoded_collect_event.into(),
215            flash_created_event: None,
216            fee_protocol_update_event: None,
217            fee_protocol_collect_event: None,
218            amm_type,
219            pairs: vec![],
220        }
221    }
222
223    /// Returns a unique identifier for this DEX, combining chain and protocol name.
224    #[must_use]
225    pub fn id(&self) -> String {
226        format!("{}:{}", self.chain.name, self.name)
227    }
228
229    /// Sets the pool initialization event signature by hashing and encoding the provided event string.
230    pub fn set_initialize_event(&mut self, event: &str) {
231        self.initialize_event = Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
232    }
233
234    /// Sets the flash loan event signature by hashing and encoding the provided event string.
235    pub fn set_flash_event(&mut self, event: &str) {
236        self.flash_created_event = Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
237    }
238
239    /// Sets the protocol-fee change event signature by hashing and encoding the provided event string.
240    pub fn set_fee_protocol_update_event(&mut self, event: &str) {
241        self.fee_protocol_update_event =
242            Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
243    }
244
245    /// Sets the protocol-fee withdrawal event signature by hashing and encoding the provided event string.
246    pub fn set_fee_protocol_collect_event(&mut self, event: &str) {
247        self.fee_protocol_collect_event =
248            Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
249    }
250}
251
252impl Display for Dex {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        write!(f, "Dex(chain={}, name={})", self.chain, self.name)
255    }
256}
257
258impl From<Pool> for CurrencyPair {
259    fn from(p: Pool) -> Self {
260        let symbol = Symbol::from(format!("{}/{}", p.token0.symbol, p.token1.symbol));
261        let id = InstrumentId::new(symbol, Venue::from(p.dex.id()));
262
263        let size_precision = p.token0.decimals.min(FIXED_PRECISION);
264        let price_precision = p.token1.decimals.min(FIXED_PRECISION);
265
266        let price_increment = Price::new(10f64.powi(-i32::from(price_precision)), price_precision);
267        let size_increment = Quantity::new(10f64.powi(-i32::from(size_precision)), size_precision);
268
269        Self::new(
270            id,
271            symbol,
272            Currency::from(p.token0.symbol.as_str()),
273            Currency::from(p.token1.symbol.as_str()),
274            price_precision,
275            size_precision,
276            price_increment,
277            size_increment,
278            None, // multiplier
279            None, // lot_size
280            None, // max_quantity
281            None, // min_quantity
282            None, // max_notional
283            None, // min_notional
284            None, // max_price
285            None, // min_price
286            None, // margin_init
287            None, // margin_maint
288            None, // maker_fee
289            None, // taker_fee
290            None, // tick_scheme
291            None, // info
292            0.into(),
293            0.into(),
294        )
295    }
296}
297
298impl From<Pool> for InstrumentAny {
299    fn from(p: Pool) -> Self {
300        CurrencyPair::from(p).into_any()
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use rstest::rstest;
307
308    use super::DexType;
309
310    #[rstest]
311    fn test_dex_type_from_dex_name_valid() {
312        // Test some known DEX names
313        assert!(DexType::from_dex_name("UniswapV3").is_some());
314        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
315        assert!(DexType::from_dex_name("BalancerV2").is_some());
316        assert!(DexType::from_dex_name("CamelotV3").is_some());
317
318        // Verify specific DEX type
319        let uniswap_v3 = DexType::from_dex_name("UniswapV3").unwrap();
320        assert_eq!(uniswap_v3, DexType::UniswapV3);
321
322        // Verify compound names
323        let aerodrome_slipstream = DexType::from_dex_name("AerodromeSlipstream").unwrap();
324        assert_eq!(aerodrome_slipstream, DexType::AerodromeSlipstream);
325
326        // Verify specialized names
327        let fluid_dex = DexType::from_dex_name("FluidDEX").unwrap();
328        assert_eq!(fluid_dex, DexType::FluidDEX);
329    }
330
331    #[rstest]
332    fn test_dex_type_from_dex_name_invalid() {
333        // Test unknown DEX names
334        assert!(DexType::from_dex_name("InvalidDEX").is_none());
335        assert!(DexType::from_dex_name("").is_none());
336        assert!(DexType::from_dex_name("NonExistentDEX").is_none());
337    }
338
339    #[rstest]
340    fn test_dex_type_from_dex_name_case_sensitive() {
341        // Test case sensitivity - should be case sensitive
342        assert!(DexType::from_dex_name("UniswapV3").is_some());
343        assert!(DexType::from_dex_name("uniswapv3").is_none()); // lowercase
344        assert!(DexType::from_dex_name("UNISWAPV3").is_none()); // uppercase
345        assert!(DexType::from_dex_name("UniSwapV3").is_none()); // mixed case
346
347        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
348        assert!(DexType::from_dex_name("sushiswapv2").is_none()); // lowercase
349    }
350
351    #[rstest]
352    fn test_dex_type_all_variants_mappable() {
353        // Test that all DEX variants can be mapped from their string representation
354        let all_dex_names = vec![
355            "AerodromeSlipstream",
356            "AerodromeV1",
357            "BalancerV2",
358            "BalancerV3",
359            "BaseSwapV2",
360            "BaseX",
361            "CamelotV3",
362            "CurveFinance",
363            "FluidDEX",
364            "MaverickV1",
365            "MaverickV2",
366            "PancakeSwapV3",
367            "SushiSwapV2",
368            "SushiSwapV3",
369            "UniswapV2",
370            "UniswapV3",
371            "UniswapV4",
372        ];
373
374        for dex_name in all_dex_names {
375            assert!(
376                DexType::from_dex_name(dex_name).is_some(),
377                "DEX name '{dex_name}' should be valid but was not found",
378            );
379        }
380    }
381
382    #[rstest]
383    fn test_dex_type_display() {
384        // Test that DexType variants display correctly (using strum::Display)
385        assert_eq!(DexType::UniswapV3.to_string(), "UniswapV3");
386        assert_eq!(DexType::SushiSwapV2.to_string(), "SushiSwapV2");
387        assert_eq!(
388            DexType::AerodromeSlipstream.to_string(),
389            "AerodromeSlipstream"
390        );
391        assert_eq!(DexType::FluidDEX.to_string(), "FluidDEX");
392    }
393}