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", from_py_object)
60)]
61#[cfg_attr(
62 feature = "python",
63 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
64)]
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct Pool {
67 pub chain: SharedChain,
69 pub dex: SharedDex,
71 pub address: Address,
73 pub pool_identifier: PoolIdentifier,
75 pub instrument_id: InstrumentId,
77 pub creation_block: u64,
79 pub token0: Token,
81 pub token1: Token,
83 pub fee: Option<u32>,
91 pub tick_spacing: Option<u32>,
93 pub initial_tick: Option<i32>,
95 pub initial_sqrt_price_x96: Option<U160>,
97 pub hooks: Option<Address>,
100 #[serde(default)]
102 pub ts_event: UnixNanos,
103 pub ts_init: UnixNanos,
105}
106
107pub type SharedPool = Arc<Pool>;
109
110impl Pool {
111 #[must_use]
113 #[expect(clippy::too_many_arguments)]
114 pub fn new(
115 chain: SharedChain,
116 dex: SharedDex,
117 address: Address,
118 pool_identifier: PoolIdentifier,
119 creation_block: u64,
120 token0: Token,
121 token1: Token,
122 fee: Option<u32>,
123 tick_spacing: Option<u32>,
124 ts_init: UnixNanos,
125 ) -> Self {
126 let instrument_id = Self::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
127
128 Self {
129 chain,
130 dex,
131 address,
132 pool_identifier,
133 instrument_id,
134 creation_block,
135 token0,
136 token1,
137 fee,
138 tick_spacing,
139 initial_tick: None,
140 initial_sqrt_price_x96: None,
141 hooks: None,
142 ts_event: ts_init,
143 ts_init,
144 }
145 }
146
147 #[must_use]
149 pub fn to_full_spec_string(&self) -> String {
150 format!(
151 "{}/{}-{}.{}",
152 self.token0.symbol,
153 self.token1.symbol,
154 self.fee.unwrap_or(0),
155 self.instrument_id.venue
156 )
157 }
158
159 pub fn initialize(&mut self, sqrt_price_x96: U160, tick: i32) {
168 let calculated_tick = get_tick_at_sqrt_ratio(sqrt_price_x96);
169
170 assert_eq!(
171 tick, calculated_tick,
172 "Provided tick {tick} does not match calculated tick {calculated_tick} for sqrt_price_x96 {sqrt_price_x96}",
173 );
174
175 self.initial_sqrt_price_x96 = Some(sqrt_price_x96);
176 self.initial_tick = Some(tick);
177 }
178
179 pub fn set_hooks(&mut self, hooks: Address) {
183 self.hooks = Some(hooks);
184 }
185
186 #[must_use]
187 pub fn create_instrument_id(
188 chain: Blockchain,
189 dex: &Dex,
190 pool_identifier: &str,
191 ) -> InstrumentId {
192 let symbol = Symbol::new(pool_identifier);
193 let venue = Venue::new(format!("{}:{}", chain, dex.name));
194 InstrumentId::new(symbol, venue)
195 }
196
197 #[must_use]
204 pub fn get_base_token(&self) -> &Token {
205 let priority0 = self.token0.get_token_priority();
206 let priority1 = self.token1.get_token_priority();
207
208 if priority0 < priority1 {
209 &self.token1
210 } else {
211 &self.token0
212 }
213 }
214
215 #[must_use]
221 pub fn get_quote_token(&self) -> &Token {
222 let priority0 = self.token0.get_token_priority();
223 let priority1 = self.token1.get_token_priority();
224
225 if priority0 < priority1 {
226 &self.token0
227 } else {
228 &self.token1
229 }
230 }
231
232 #[must_use]
242 pub fn is_base_quote_inverted(&self) -> bool {
243 let priority0 = self.token0.get_token_priority();
244 let priority1 = self.token1.get_token_priority();
245
246 priority0 < priority1
248 }
249}
250
251impl Display for Pool {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 write!(
254 f,
255 "Pool(instrument_id={}, dex={}, fee={}, address={})",
256 self.instrument_id,
257 self.dex.name,
258 self.fee
259 .map_or("None".to_string(), |fee| format!("fee={fee}, ")),
260 self.address
261 )
262 }
263}
264
265impl HasTsInit for Pool {
266 fn ts_init(&self) -> UnixNanos {
267 self.ts_init
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use std::sync::Arc;
274
275 use rstest::rstest;
276
277 use super::*;
278 use crate::defi::{
279 chain::chains,
280 dex::{AmmType, Dex, DexType},
281 token::Token,
282 };
283
284 #[rstest]
285 fn test_pool_constructor_and_methods() {
286 let chain = Arc::new(chains::ETHEREUM.clone());
287 let dex = Dex::new(
288 chains::ETHEREUM.clone(),
289 DexType::UniswapV3,
290 "0x1F98431c8aD98523631AE4a59f267346ea31F984",
291 0,
292 AmmType::CLAMM,
293 "PoolCreated(address,address,uint24,int24,address)",
294 "Swap(address,address,int256,int256,uint160,uint128,int24)",
295 "Mint(address,address,int24,int24,uint128,uint256,uint256)",
296 "Burn(address,int24,int24,uint128,uint256,uint256)",
297 "Collect(address,address,int24,int24,uint128,uint128)",
298 );
299
300 let token0 = Token::new(
301 chain.clone(),
302 "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
303 .parse()
304 .unwrap(),
305 "Wrapped Ether".to_string(),
306 "WETH".to_string(),
307 18,
308 );
309
310 let token1 = Token::new(
311 chain.clone(),
312 "0xdAC17F958D2ee523a2206206994597C13D831ec7"
313 .parse()
314 .unwrap(),
315 "Tether USD".to_string(),
316 "USDT".to_string(),
317 6,
318 );
319
320 let pool_address: Address = "0x11b815efB8f581194ae79006d24E0d814B7697F6"
321 .parse()
322 .unwrap();
323 let pool_identifier = PoolIdentifier::from_address(pool_address);
324 let ts_init = UnixNanos::from(1_234_567_890_000_000_000u64);
325
326 let pool = Pool::new(
327 chain.clone(),
328 Arc::new(dex),
329 pool_address,
330 pool_identifier,
331 12_345_678,
332 token0,
333 token1,
334 Some(3000),
335 Some(60),
336 ts_init,
337 );
338
339 assert_eq!(pool.chain.chain_id, chain.chain_id);
340 assert_eq!(pool.dex.name, DexType::UniswapV3);
341 assert_eq!(pool.address, pool_address);
342 assert_eq!(pool.creation_block, 12_345_678);
343 assert_eq!(pool.token0.symbol, "WETH");
344 assert_eq!(pool.token1.symbol, "USDT");
345 assert_eq!(pool.fee.unwrap(), 3000);
346 assert_eq!(pool.tick_spacing.unwrap(), 60);
347 assert_eq!(pool.ts_init, ts_init);
348 assert_eq!(
349 pool.instrument_id.symbol.as_str(),
350 "0x11b815efB8f581194ae79006d24E0d814B7697F6"
351 );
352 assert_eq!(pool.instrument_id.venue.as_str(), "Ethereum:UniswapV3");
353 assert_eq!(pool.get_base_token().symbol, "WETH");
355 assert_eq!(pool.get_quote_token().symbol, "USDT");
356 assert!(!pool.is_base_quote_inverted());
357 assert_eq!(
358 pool.to_full_spec_string(),
359 "WETH/USDT-3000.Ethereum:UniswapV3"
360 );
361 }
362
363 #[rstest]
364 fn test_pool_instrument_id_format() {
365 let chain = Arc::new(chains::ETHEREUM.clone());
366 let factory_address = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
367
368 let dex = Dex::new(
369 chains::ETHEREUM.clone(),
370 DexType::UniswapV3,
371 factory_address,
372 0,
373 AmmType::CLAMM,
374 "PoolCreated(address,address,uint24,int24,address)",
375 "Swap(address,address,int256,int256,uint160,uint128,int24)",
376 "Mint(address,address,int24,int24,uint128,uint256,uint256)",
377 "Burn(address,int24,int24,uint128,uint256,uint256)",
378 "Collect(address,address,int24,int24,uint128,uint128)",
379 );
380
381 let token0 = Token::new(
382 chain.clone(),
383 "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
384 .parse()
385 .unwrap(),
386 "Wrapped Ether".to_string(),
387 "WETH".to_string(),
388 18,
389 );
390
391 let token1 = Token::new(
392 chain.clone(),
393 "0xdAC17F958D2ee523a2206206994597C13D831ec7"
394 .parse()
395 .unwrap(),
396 "Tether USD".to_string(),
397 "USDT".to_string(),
398 6,
399 );
400
401 let pool_address = "0x11b815efB8f581194ae79006d24E0d814B7697F6"
402 .parse()
403 .unwrap();
404
405 let pool = Pool::new(
406 chain,
407 Arc::new(dex),
408 pool_address,
409 PoolIdentifier::from_address(pool_address),
410 0,
411 token0,
412 token1,
413 Some(3000),
414 Some(60),
415 UnixNanos::default(),
416 );
417
418 assert_eq!(
419 pool.instrument_id.to_string(),
420 "0x11b815efB8f581194ae79006d24E0d814B7697F6.Ethereum:UniswapV3"
421 );
422 }
423}