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