nautilus_model/defi/pool_analysis/
position.rs1use alloy_primitives::{Address, U256};
17use serde::{Deserialize, Serialize};
18
19use crate::defi::tick_map::full_math::{FullMath, Q128};
20
21#[cfg_attr(
26 feature = "python",
27 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
28)]
29#[cfg_attr(
30 feature = "python",
31 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
32)]
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct PoolPosition {
35 pub owner: Address,
37 pub tick_lower: i32,
39 pub tick_upper: i32,
41 pub liquidity: u128,
43 pub fee_growth_inside_0_last: U256,
45 pub fee_growth_inside_1_last: U256,
47 pub tokens_owed_0: u128,
49 pub tokens_owed_1: u128,
51 pub total_amount0_deposited: U256,
53 pub total_amount1_deposited: U256,
55 pub total_amount0_collected: u128,
57 pub total_amount1_collected: u128,
59}
60
61impl PoolPosition {
62 #[must_use]
64 pub fn new(owner: Address, tick_lower: i32, tick_upper: i32, liquidity: i128) -> Self {
65 Self {
66 owner,
67 tick_lower,
68 tick_upper,
69 liquidity: liquidity.unsigned_abs(),
70 fee_growth_inside_0_last: U256::ZERO,
71 fee_growth_inside_1_last: U256::ZERO,
72 tokens_owed_0: 0,
73 tokens_owed_1: 0,
74 total_amount0_deposited: U256::ZERO,
75 total_amount1_deposited: U256::ZERO,
76 total_amount0_collected: 0,
77 total_amount1_collected: 0,
78 }
79 }
80
81 #[must_use]
83 pub fn get_position_key(owner: &Address, tick_lower: i32, tick_upper: i32) -> String {
84 format!("{owner}:{tick_lower}:{tick_upper}")
85 }
86
87 pub fn update_liquidity(&mut self, liquidity_delta: i128) {
92 if liquidity_delta < 0 {
93 self.liquidity = self.liquidity.saturating_sub((-liquidity_delta) as u128);
94 } else {
95 self.liquidity = self.liquidity.saturating_add(liquidity_delta as u128);
96 }
97 }
98
99 pub fn update_fees(&mut self, fee_growth_inside_0: U256, fee_growth_inside_1: U256) {
104 if self.liquidity > 0 {
105 let fee_delta_0 = fee_growth_inside_0.saturating_sub(self.fee_growth_inside_0_last);
107 let fee_delta_1 = fee_growth_inside_1.saturating_sub(self.fee_growth_inside_1_last);
108
109 let tokens_owed_0_full =
110 FullMath::mul_div(fee_delta_0, U256::from(self.liquidity), Q128)
111 .unwrap_or(U256::ZERO);
112
113 let tokens_owed_1_full =
114 FullMath::mul_div(fee_delta_1, U256::from(self.liquidity), Q128)
115 .unwrap_or(U256::ZERO);
116
117 self.tokens_owed_0 = self
118 .tokens_owed_0
119 .wrapping_add(FullMath::truncate_to_u128(tokens_owed_0_full));
120 self.tokens_owed_1 = self
121 .tokens_owed_1
122 .wrapping_add(FullMath::truncate_to_u128(tokens_owed_1_full));
123 }
124
125 self.fee_growth_inside_0_last = fee_growth_inside_0;
126 self.fee_growth_inside_1_last = fee_growth_inside_1;
127 }
128
129 pub fn collect_fees(&mut self, amount0: u128, amount1: u128) {
134 let collect_amount_0 = amount0.min(self.tokens_owed_0);
135 let collect_amount_1 = amount1.min(self.tokens_owed_1);
136
137 self.tokens_owed_0 -= collect_amount_0;
138 self.tokens_owed_1 -= collect_amount_1;
139
140 self.total_amount0_collected += collect_amount_0;
141 self.total_amount1_collected += collect_amount_1;
142 }
143
144 pub fn update_amounts(&mut self, liquidity_delta: i128, amount0: U256, amount1: U256) {
149 if liquidity_delta > 0 {
150 self.total_amount0_deposited += amount0;
152 self.total_amount1_deposited += amount1;
153 } else {
154 self.tokens_owed_0 = self
155 .tokens_owed_0
156 .wrapping_add(FullMath::truncate_to_u128(amount0));
157 self.tokens_owed_1 = self
158 .tokens_owed_1
159 .wrapping_add(FullMath::truncate_to_u128(amount1));
160 }
161 }
162
163 #[must_use]
165 pub fn is_empty(&self) -> bool {
166 self.liquidity == 0 && self.tokens_owed_0 == 0 && self.tokens_owed_1 == 0
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use alloy_primitives::address;
173 use rstest::rstest;
174
175 use super::*;
176
177 #[rstest]
178 fn test_new_position() {
179 let owner = address!("1234567890123456789012345678901234567890");
180 let tick_lower = -100;
181 let tick_upper = 100;
182 let liquidity = 1000i128;
183
184 let position = PoolPosition::new(owner, tick_lower, tick_upper, liquidity);
185
186 assert_eq!(position.owner, owner);
187 assert_eq!(position.tick_lower, tick_lower);
188 assert_eq!(position.tick_upper, tick_upper);
189 assert_eq!(position.liquidity, liquidity as u128);
190 assert_eq!(position.fee_growth_inside_0_last, U256::ZERO);
191 assert_eq!(position.fee_growth_inside_1_last, U256::ZERO);
192 assert_eq!(position.tokens_owed_0, 0);
193 assert_eq!(position.tokens_owed_1, 0);
194 }
195
196 #[rstest]
197 fn test_get_position_key() {
198 let owner = address!("1234567890123456789012345678901234567890");
199 let tick_lower = -100;
200 let tick_upper = 100;
201
202 let key = PoolPosition::get_position_key(&owner, tick_lower, tick_upper);
203 let expected = format!("{owner:?}:{tick_lower}:{tick_upper}");
204 assert_eq!(key, expected);
205 }
206
207 #[rstest]
208 fn test_update_liquidity_positive() {
209 let owner = address!("1234567890123456789012345678901234567890");
210 let mut position = PoolPosition::new(owner, -100, 100, 1000);
211
212 position.update_liquidity(500);
213 assert_eq!(position.liquidity, 1500);
214 }
215
216 #[rstest]
217 fn test_update_liquidity_negative() {
218 let owner = address!("1234567890123456789012345678901234567890");
219 let mut position = PoolPosition::new(owner, -100, 100, 1000);
220
221 position.update_liquidity(-300);
222 assert_eq!(position.liquidity, 700);
223 }
224
225 #[rstest]
226 fn test_update_liquidity_negative_saturating() {
227 let owner = address!("1234567890123456789012345678901234567890");
228 let mut position = PoolPosition::new(owner, -100, 100, 1000);
229
230 position.update_liquidity(-2000); assert_eq!(position.liquidity, 0);
232 }
233
234 #[rstest]
235 fn test_update_fees() {
236 let owner = address!("1234567890123456789012345678901234567890");
237 let mut position = PoolPosition::new(owner, -100, 100, 1000);
238
239 let fee_growth_inside_0 = U256::from(100);
240 let fee_growth_inside_1 = U256::from(200);
241
242 position.update_fees(fee_growth_inside_0, fee_growth_inside_1);
243
244 assert_eq!(position.fee_growth_inside_0_last, fee_growth_inside_0);
245 assert_eq!(position.fee_growth_inside_1_last, fee_growth_inside_1);
246 }
249
250 #[rstest]
251 fn test_collect_fees() {
252 let owner = address!("1234567890123456789012345678901234567890");
253 let mut position = PoolPosition::new(owner, -100, 100, 1000);
254
255 position.tokens_owed_0 = 100;
257 position.tokens_owed_1 = 200;
258
259 position.collect_fees(50, 150);
261
262 assert_eq!(position.total_amount0_collected, 50);
263 assert_eq!(position.total_amount1_collected, 150);
264 assert_eq!(position.tokens_owed_0, 50);
265 assert_eq!(position.tokens_owed_1, 50);
266 }
267
268 #[rstest]
269 fn test_collect_fees_more_than_owed() {
270 let owner = address!("1234567890123456789012345678901234567890");
271 let mut position = PoolPosition::new(owner, -100, 100, 1000);
272
273 position.tokens_owed_0 = 100;
274 position.tokens_owed_1 = 200;
275
276 position.collect_fees(150, 300);
278
279 assert_eq!(position.total_amount0_collected, 100); assert_eq!(position.total_amount1_collected, 200);
281 assert_eq!(position.tokens_owed_0, 0);
282 assert_eq!(position.tokens_owed_1, 0);
283 }
284
285 #[rstest]
286 fn test_is_empty() {
287 let owner = address!("1234567890123456789012345678901234567890");
288 let mut position = PoolPosition::new(owner, -100, 100, 0);
289
290 assert!(position.is_empty());
291
292 position.liquidity = 100;
293 assert!(!position.is_empty());
294
295 position.liquidity = 0;
296 position.tokens_owed_0 = 50;
297 assert!(!position.is_empty());
298
299 position.tokens_owed_0 = 0;
300 position.tokens_owed_1 = 25;
301 assert!(!position.is_empty());
302
303 position.tokens_owed_1 = 0;
304 assert!(position.is_empty());
305 }
306}