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