nautilus_model/defi/
amm.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! Data types specific to automated-market-maker (AMM) protocols.
17
18use std::{fmt::Display, sync::Arc};
19
20use alloy_primitives::Address;
21use nautilus_core::UnixNanos;
22use serde::{Deserialize, Serialize};
23
24use crate::{
25    data::HasTsInit,
26    defi::{Blockchain, SharedDex, chain::SharedChain, dex::Dex, token::Token},
27    identifiers::{InstrumentId, Symbol, Venue},
28};
29
30/// Represents a liquidity pool in a decentralized exchange.
31///
32/// The instrument ID encodes with the following components:
33/// `symbol` – The pool address.
34/// `venue`  – The chain name plus DEX ID.
35///
36/// The string representation therefore has the form:
37/// `<POOL_ADDRESS>.<CHAIN_NAME>:<DEX_ID>`
38///
39/// Example:
40/// `0x11b815efB8f581194ae79006d24E0d814B7697F6.Ethereum:UniswapV3`
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
44)]
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct Pool {
47    /// The blockchain network where this pool exists.
48    pub chain: SharedChain,
49    /// The decentralized exchange protocol that created and manages this pool.
50    pub dex: SharedDex,
51    /// The blockchain address of the pool smart contract.
52    pub address: Address,
53    /// The instrument ID for the pool.
54    pub instrument_id: InstrumentId,
55    /// The block number when this pool was created on the blockchain.
56    pub creation_block: u64,
57    /// The first token in the trading pair.
58    pub token0: Token,
59    /// The second token in the trading pair.
60    pub token1: Token,
61    /// The trading fee tier used by the pool expressed in hundred-thousandths
62    /// (1e-6) of one unit – identical to Uniswap-V3’s fee representation.
63    ///
64    /// Examples:
65    /// • `500`   →  0.05 %  (5 bps)
66    /// • `3_000` →  0.30 %  (30 bps)
67    /// • `10_000`→  1.00 %
68    pub fee: Option<u32>,
69    /// The minimum tick spacing for positions in concentrated liquidity AMMs.
70    pub tick_spacing: Option<u32>,
71    /// UNIX timestamp (nanoseconds) when the instance was created.
72    pub ts_init: UnixNanos,
73}
74
75/// A thread-safe shared pointer to a `Pool`, enabling efficient reuse across multiple components.
76pub type SharedPool = Arc<Pool>;
77
78impl Pool {
79    /// Creates a new [`Pool`] instance with the specified properties.
80    #[must_use]
81    #[allow(clippy::too_many_arguments)]
82    pub fn new(
83        chain: SharedChain,
84        dex: SharedDex,
85        address: Address,
86        creation_block: u64,
87        token0: Token,
88        token1: Token,
89        fee: Option<u32>,
90        tick_spacing: Option<u32>,
91        ts_init: UnixNanos,
92    ) -> Self {
93        let instrument_id = Self::create_instrument_id(chain.name, &dex, &address);
94
95        Self {
96            chain,
97            dex,
98            address,
99            instrument_id,
100            creation_block,
101            token0,
102            token1,
103            fee,
104            tick_spacing,
105            ts_init,
106        }
107    }
108
109    /// Returns a formatted string representation of the pool for display purposes.
110    pub fn to_full_spec_string(&self) -> String {
111        format!(
112            "{}/{}-{}.{}",
113            self.token0.symbol,
114            self.token1.symbol,
115            self.fee.unwrap_or(0),
116            self.instrument_id.venue
117        )
118    }
119
120    pub fn create_instrument_id(chain: Blockchain, dex: &Dex, address: &Address) -> InstrumentId {
121        let symbol = Symbol::new(address.to_string());
122        let venue = Venue::new(format!("{}:{}", chain, dex.name));
123        InstrumentId::new(symbol, venue)
124    }
125}
126
127impl Display for Pool {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        write!(
130            f,
131            "Pool(instrument_id={}, dex={}, fee={}, address={})",
132            self.instrument_id,
133            self.dex.name,
134            self.fee
135                .map_or("None".to_string(), |fee| format!("fee={}, ", fee)),
136            self.address
137        )
138    }
139}
140
141impl HasTsInit for Pool {
142    fn ts_init(&self) -> UnixNanos {
143        self.ts_init
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use std::sync::Arc;
150
151    use rstest::rstest;
152
153    use super::*;
154    use crate::defi::{
155        chain::chains,
156        dex::{AmmType, Dex, DexType},
157        token::Token,
158    };
159
160    #[rstest]
161    fn test_pool_constructor_and_methods() {
162        let chain = Arc::new(chains::ETHEREUM.clone());
163        let dex = Dex::new(
164            chains::ETHEREUM.clone(),
165            DexType::UniswapV3,
166            "0x1F98431c8aD98523631AE4a59f267346ea31F984",
167            0,
168            AmmType::CLAMM,
169            "PoolCreated(address,address,uint24,int24,address)",
170            "Swap(address,address,int256,int256,uint160,uint128,int24)",
171            "Mint(address,address,int24,int24,uint128,uint256,uint256)",
172            "Burn(address,int24,int24,uint128,uint256,uint256)",
173            "Collect(address,address,int24,int24,uint128,uint128)",
174        );
175
176        let token0 = Token::new(
177            chain.clone(),
178            "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
179                .parse()
180                .unwrap(),
181            "Wrapped Ether".to_string(),
182            "WETH".to_string(),
183            18,
184        );
185
186        let token1 = Token::new(
187            chain.clone(),
188            "0xdAC17F958D2ee523a2206206994597C13D831ec7"
189                .parse()
190                .unwrap(),
191            "Tether USD".to_string(),
192            "USDT".to_string(),
193            6,
194        );
195
196        let pool_address = "0x11b815efB8f581194ae79006d24E0d814B7697F6"
197            .parse()
198            .unwrap();
199        let ts_init = UnixNanos::from(1_234_567_890_000_000_000u64);
200
201        let pool = Pool::new(
202            chain.clone(),
203            Arc::new(dex),
204            pool_address,
205            12345678,
206            token0,
207            token1,
208            Some(3000),
209            Some(60),
210            ts_init,
211        );
212
213        assert_eq!(pool.chain.chain_id, chain.chain_id);
214        assert_eq!(pool.dex.name, DexType::UniswapV3);
215        assert_eq!(pool.address, pool_address);
216        assert_eq!(pool.creation_block, 12345678);
217        assert_eq!(pool.token0.symbol, "WETH");
218        assert_eq!(pool.token1.symbol, "USDT");
219        assert_eq!(pool.fee.unwrap(), 3000);
220        assert_eq!(pool.tick_spacing.unwrap(), 60);
221        assert_eq!(pool.ts_init, ts_init);
222        assert_eq!(
223            pool.instrument_id.symbol.as_str(),
224            "0x11b815efB8f581194ae79006d24E0d814B7697F6"
225        );
226        assert_eq!(pool.instrument_id.venue.as_str(), "Ethereum:UniswapV3");
227    }
228
229    #[rstest]
230    fn test_pool_instrument_id_format() {
231        let chain = Arc::new(chains::ETHEREUM.clone());
232        let factory_address = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
233
234        let dex = Dex::new(
235            chains::ETHEREUM.clone(),
236            DexType::UniswapV3,
237            factory_address,
238            0,
239            AmmType::CLAMM,
240            "PoolCreated(address,address,uint24,int24,address)",
241            "Swap(address,address,int256,int256,uint160,uint128,int24)",
242            "Mint(address,address,int24,int24,uint128,uint256,uint256)",
243            "Burn(address,int24,int24,uint128,uint256,uint256)",
244            "Collect(address,address,int24,int24,uint128,uint128)",
245        );
246
247        let token0 = Token::new(
248            chain.clone(),
249            "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
250                .parse()
251                .unwrap(),
252            "Wrapped Ether".to_string(),
253            "WETH".to_string(),
254            18,
255        );
256
257        let token1 = Token::new(
258            chain.clone(),
259            "0xdAC17F958D2ee523a2206206994597C13D831ec7"
260                .parse()
261                .unwrap(),
262            "Tether USD".to_string(),
263            "USDT".to_string(),
264            6,
265        );
266
267        let pool = Pool::new(
268            chain.clone(),
269            Arc::new(dex),
270            "0x11b815efB8f581194ae79006d24E0d814B7697F6"
271                .parse()
272                .unwrap(),
273            0,
274            token0,
275            token1,
276            Some(3000),
277            Some(60),
278            UnixNanos::default(),
279        );
280
281        assert_eq!(
282            pool.instrument_id.to_string(),
283            "0x11b815efB8f581194ae79006d24E0d814B7697F6.Ethereum:UniswapV3"
284        );
285    }
286}