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