1use 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, PoolIdentifier, 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#[cfg_attr(
58 feature = "python",
59 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
60)]
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct Pool {
63 pub chain: SharedChain,
65 pub dex: SharedDex,
67 pub address: Address,
69 pub pool_identifier: PoolIdentifier,
71 pub instrument_id: InstrumentId,
73 pub creation_block: u64,
75 pub token0: Token,
77 pub token1: Token,
79 pub fee: Option<u32>,
87 pub tick_spacing: Option<u32>,
89 pub initial_tick: Option<i32>,
91 pub initial_sqrt_price_x96: Option<U160>,
93 pub hooks: Option<Address>,
96 pub ts_init: UnixNanos,
98}
99
100pub type SharedPool = Arc<Pool>;
102
103impl Pool {
104 #[must_use]
106 #[allow(clippy::too_many_arguments)]
107 pub fn new(
108 chain: SharedChain,
109 dex: SharedDex,
110 address: Address,
111 pool_identifier: PoolIdentifier,
112 creation_block: u64,
113 token0: Token,
114 token1: Token,
115 fee: Option<u32>,
116 tick_spacing: Option<u32>,
117 ts_init: UnixNanos,
118 ) -> Self {
119 let instrument_id = Self::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
120
121 Self {
122 chain,
123 dex,
124 address,
125 pool_identifier,
126 instrument_id,
127 creation_block,
128 token0,
129 token1,
130 fee,
131 tick_spacing,
132 initial_tick: None,
133 initial_sqrt_price_x96: None,
134 hooks: None,
135 ts_init,
136 }
137 }
138
139 pub fn to_full_spec_string(&self) -> String {
141 format!(
142 "{}/{}-{}.{}",
143 self.token0.symbol,
144 self.token1.symbol,
145 self.fee.unwrap_or(0),
146 self.instrument_id.venue
147 )
148 }
149
150 pub fn initialize(&mut self, sqrt_price_x96: U160, tick: i32) {
159 let calculated_tick = get_tick_at_sqrt_ratio(sqrt_price_x96);
160
161 assert_eq!(
162 tick, calculated_tick,
163 "Provided tick {tick} does not match calculated tick {calculated_tick} for sqrt_price_x96 {sqrt_price_x96}",
164 );
165
166 self.initial_sqrt_price_x96 = Some(sqrt_price_x96);
167 self.initial_tick = Some(tick);
168 }
169
170 pub fn set_hooks(&mut self, hooks: Address) {
174 self.hooks = Some(hooks);
175 }
176
177 pub fn create_instrument_id(
178 chain: Blockchain,
179 dex: &Dex,
180 pool_identifier: &str,
181 ) -> InstrumentId {
182 let symbol = Symbol::new(pool_identifier);
183 let venue = Venue::new(format!("{}:{}", chain, dex.name));
184 InstrumentId::new(symbol, venue)
185 }
186
187 pub fn get_base_token(&self) -> &Token {
194 let priority0 = self.token0.get_token_priority();
195 let priority1 = self.token1.get_token_priority();
196
197 if priority0 < priority1 {
198 &self.token1
199 } else {
200 &self.token0
201 }
202 }
203
204 pub fn get_quote_token(&self) -> &Token {
210 let priority0 = self.token0.get_token_priority();
211 let priority1 = self.token1.get_token_priority();
212
213 if priority0 < priority1 {
214 &self.token0
215 } else {
216 &self.token1
217 }
218 }
219
220 pub fn is_base_quote_inverted(&self) -> bool {
230 let priority0 = self.token0.get_token_priority();
231 let priority1 = self.token1.get_token_priority();
232
233 priority0 < priority1
235 }
236}
237
238impl Display for Pool {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 write!(
241 f,
242 "Pool(instrument_id={}, dex={}, fee={}, address={})",
243 self.instrument_id,
244 self.dex.name,
245 self.fee
246 .map_or("None".to_string(), |fee| format!("fee={fee}, ")),
247 self.address
248 )
249 }
250}
251
252impl HasTsInit for Pool {
253 fn ts_init(&self) -> UnixNanos {
254 self.ts_init
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use std::sync::Arc;
261
262 use rstest::rstest;
263
264 use super::*;
265 use crate::defi::{
266 chain::chains,
267 dex::{AmmType, Dex, DexType},
268 token::Token,
269 };
270
271 #[rstest]
272 fn test_pool_constructor_and_methods() {
273 let chain = Arc::new(chains::ETHEREUM.clone());
274 let dex = Dex::new(
275 chains::ETHEREUM.clone(),
276 DexType::UniswapV3,
277 "0x1F98431c8aD98523631AE4a59f267346ea31F984",
278 0,
279 AmmType::CLAMM,
280 "PoolCreated(address,address,uint24,int24,address)",
281 "Swap(address,address,int256,int256,uint160,uint128,int24)",
282 "Mint(address,address,int24,int24,uint128,uint256,uint256)",
283 "Burn(address,int24,int24,uint128,uint256,uint256)",
284 "Collect(address,address,int24,int24,uint128,uint128)",
285 );
286
287 let token0 = Token::new(
288 chain.clone(),
289 "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
290 .parse()
291 .unwrap(),
292 "Wrapped Ether".to_string(),
293 "WETH".to_string(),
294 18,
295 );
296
297 let token1 = Token::new(
298 chain.clone(),
299 "0xdAC17F958D2ee523a2206206994597C13D831ec7"
300 .parse()
301 .unwrap(),
302 "Tether USD".to_string(),
303 "USDT".to_string(),
304 6,
305 );
306
307 let pool_address: Address = "0x11b815efB8f581194ae79006d24E0d814B7697F6"
308 .parse()
309 .unwrap();
310 let pool_identifier = PoolIdentifier::from_address(pool_address);
311 let ts_init = UnixNanos::from(1_234_567_890_000_000_000u64);
312
313 let pool = Pool::new(
314 chain.clone(),
315 Arc::new(dex),
316 pool_address,
317 pool_identifier,
318 12345678,
319 token0,
320 token1,
321 Some(3000),
322 Some(60),
323 ts_init,
324 );
325
326 assert_eq!(pool.chain.chain_id, chain.chain_id);
327 assert_eq!(pool.dex.name, DexType::UniswapV3);
328 assert_eq!(pool.address, pool_address);
329 assert_eq!(pool.creation_block, 12345678);
330 assert_eq!(pool.token0.symbol, "WETH");
331 assert_eq!(pool.token1.symbol, "USDT");
332 assert_eq!(pool.fee.unwrap(), 3000);
333 assert_eq!(pool.tick_spacing.unwrap(), 60);
334 assert_eq!(pool.ts_init, ts_init);
335 assert_eq!(
336 pool.instrument_id.symbol.as_str(),
337 "0x11b815efB8f581194ae79006d24E0d814B7697F6"
338 );
339 assert_eq!(pool.instrument_id.venue.as_str(), "Ethereum:UniswapV3");
340 assert_eq!(pool.get_base_token().symbol, "WETH");
342 assert_eq!(pool.get_quote_token().symbol, "USDT");
343 assert!(!pool.is_base_quote_inverted());
344 assert_eq!(
345 pool.to_full_spec_string(),
346 "WETH/USDT-3000.Ethereum:UniswapV3"
347 );
348 }
349
350 #[rstest]
351 fn test_pool_instrument_id_format() {
352 let chain = Arc::new(chains::ETHEREUM.clone());
353 let factory_address = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
354
355 let dex = Dex::new(
356 chains::ETHEREUM.clone(),
357 DexType::UniswapV3,
358 factory_address,
359 0,
360 AmmType::CLAMM,
361 "PoolCreated(address,address,uint24,int24,address)",
362 "Swap(address,address,int256,int256,uint160,uint128,int24)",
363 "Mint(address,address,int24,int24,uint128,uint256,uint256)",
364 "Burn(address,int24,int24,uint128,uint256,uint256)",
365 "Collect(address,address,int24,int24,uint128,uint128)",
366 );
367
368 let token0 = Token::new(
369 chain.clone(),
370 "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
371 .parse()
372 .unwrap(),
373 "Wrapped Ether".to_string(),
374 "WETH".to_string(),
375 18,
376 );
377
378 let token1 = Token::new(
379 chain.clone(),
380 "0xdAC17F958D2ee523a2206206994597C13D831ec7"
381 .parse()
382 .unwrap(),
383 "Tether USD".to_string(),
384 "USDT".to_string(),
385 6,
386 );
387
388 let pool_address = "0x11b815efB8f581194ae79006d24E0d814B7697F6"
389 .parse()
390 .unwrap();
391 let pool = Pool::new(
392 chain,
393 Arc::new(dex),
394 pool_address,
395 PoolIdentifier::from_address(pool_address),
396 0,
397 token0,
398 token1,
399 Some(3000),
400 Some(60),
401 UnixNanos::default(),
402 );
403
404 assert_eq!(
405 pool.instrument_id.to_string(),
406 "0x11b815efB8f581194ae79006d24E0d814B7697F6.Ethereum:UniswapV3"
407 );
408 }
409}