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