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")
28)]
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct PoolPosition {
31 pub owner: Address,
33 pub tick_lower: i32,
35 pub tick_upper: i32,
37 pub liquidity: u128,
39 pub fee_growth_inside_0_last: U256,
41 pub fee_growth_inside_1_last: U256,
43 pub tokens_owed_0: u128,
45 pub tokens_owed_1: u128,
47 pub total_amount0_deposited: U256,
49 pub total_amount1_deposited: U256,
51 pub total_amount0_collected: u128,
53 pub total_amount1_collected: u128,
55}
56
57impl PoolPosition {
58 #[must_use]
60 pub fn new(owner: Address, tick_lower: i32, tick_upper: i32, liquidity: i128) -> Self {
61 Self {
62 owner,
63 tick_lower,
64 tick_upper,
65 liquidity: liquidity.unsigned_abs(),
66 fee_growth_inside_0_last: U256::ZERO,
67 fee_growth_inside_1_last: U256::ZERO,
68 tokens_owed_0: 0,
69 tokens_owed_1: 0,
70 total_amount0_deposited: U256::ZERO,
71 total_amount1_deposited: U256::ZERO,
72 total_amount0_collected: 0,
73 total_amount1_collected: 0,
74 }
75 }
76
77 #[must_use]
79 pub fn get_position_key(owner: &Address, tick_lower: i32, tick_upper: i32) -> String {
80 format!("{owner}:{tick_lower}:{tick_upper}")
81 }
82
83 pub fn update_liquidity(&mut self, liquidity_delta: i128) {
88 if liquidity_delta < 0 {
89 self.liquidity = self.liquidity.saturating_sub((-liquidity_delta) as u128);
90 } else {
91 self.liquidity = self.liquidity.saturating_add(liquidity_delta as u128);
92 }
93 }
94
95 pub fn update_fees(&mut self, fee_growth_inside_0: U256, fee_growth_inside_1: U256) {
100 if self.liquidity > 0 {
101 let fee_delta_0 = fee_growth_inside_0.saturating_sub(self.fee_growth_inside_0_last);
103 let fee_delta_1 = fee_growth_inside_1.saturating_sub(self.fee_growth_inside_1_last);
104
105 let tokens_owed_0_full =
106 FullMath::mul_div(fee_delta_0, U256::from(self.liquidity), Q128)
107 .unwrap_or(U256::ZERO);
108
109 let tokens_owed_1_full =
110 FullMath::mul_div(fee_delta_1, U256::from(self.liquidity), Q128)
111 .unwrap_or(U256::ZERO);
112
113 self.tokens_owed_0 = self
114 .tokens_owed_0
115 .wrapping_add(FullMath::truncate_to_u128(tokens_owed_0_full));
116 self.tokens_owed_1 = self
117 .tokens_owed_1
118 .wrapping_add(FullMath::truncate_to_u128(tokens_owed_1_full));
119 }
120
121 self.fee_growth_inside_0_last = fee_growth_inside_0;
122 self.fee_growth_inside_1_last = fee_growth_inside_1;
123 }
124
125 pub fn collect_fees(&mut self, amount0: u128, amount1: u128) {
130 let collect_amount_0 = amount0.min(self.tokens_owed_0);
131 let collect_amount_1 = amount1.min(self.tokens_owed_1);
132
133 self.tokens_owed_0 -= collect_amount_0;
134 self.tokens_owed_1 -= collect_amount_1;
135
136 self.total_amount0_collected += collect_amount_0;
137 self.total_amount1_collected += collect_amount_1;
138 }
139
140 pub fn update_amounts(&mut self, liquidity_delta: i128, amount0: U256, amount1: U256) {
145 if liquidity_delta > 0 {
146 self.total_amount0_deposited += amount0;
148 self.total_amount1_deposited += amount1;
149 } else {
150 self.tokens_owed_0 = self
151 .tokens_owed_0
152 .wrapping_add(FullMath::truncate_to_u128(amount0));
153 self.tokens_owed_1 = self
154 .tokens_owed_1
155 .wrapping_add(FullMath::truncate_to_u128(amount1));
156 }
157 }
158
159 #[must_use]
161 pub fn is_empty(&self) -> bool {
162 self.liquidity == 0 && self.tokens_owed_0 == 0 && self.tokens_owed_1 == 0
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use alloy_primitives::address;
169 use rstest::rstest;
170
171 use super::*;
172
173 #[rstest]
174 fn test_new_position() {
175 let owner = address!("1234567890123456789012345678901234567890");
176 let tick_lower = -100;
177 let tick_upper = 100;
178 let liquidity = 1000i128;
179
180 let position = PoolPosition::new(owner, tick_lower, tick_upper, liquidity);
181
182 assert_eq!(position.owner, owner);
183 assert_eq!(position.tick_lower, tick_lower);
184 assert_eq!(position.tick_upper, tick_upper);
185 assert_eq!(position.liquidity, liquidity as u128);
186 assert_eq!(position.fee_growth_inside_0_last, U256::ZERO);
187 assert_eq!(position.fee_growth_inside_1_last, U256::ZERO);
188 assert_eq!(position.tokens_owed_0, 0);
189 assert_eq!(position.tokens_owed_1, 0);
190 }
191
192 #[rstest]
193 fn test_get_position_key() {
194 let owner = address!("1234567890123456789012345678901234567890");
195 let tick_lower = -100;
196 let tick_upper = 100;
197
198 let key = PoolPosition::get_position_key(&owner, tick_lower, tick_upper);
199 let expected = format!("{owner:?}:{tick_lower}:{tick_upper}");
200 assert_eq!(key, expected);
201 }
202
203 #[rstest]
204 fn test_update_liquidity_positive() {
205 let owner = address!("1234567890123456789012345678901234567890");
206 let mut position = PoolPosition::new(owner, -100, 100, 1000);
207
208 position.update_liquidity(500);
209 assert_eq!(position.liquidity, 1500);
210 }
211
212 #[rstest]
213 fn test_update_liquidity_negative() {
214 let owner = address!("1234567890123456789012345678901234567890");
215 let mut position = PoolPosition::new(owner, -100, 100, 1000);
216
217 position.update_liquidity(-300);
218 assert_eq!(position.liquidity, 700);
219 }
220
221 #[rstest]
222 fn test_update_liquidity_negative_saturating() {
223 let owner = address!("1234567890123456789012345678901234567890");
224 let mut position = PoolPosition::new(owner, -100, 100, 1000);
225
226 position.update_liquidity(-2000); assert_eq!(position.liquidity, 0);
228 }
229
230 #[rstest]
231 fn test_update_fees() {
232 let owner = address!("1234567890123456789012345678901234567890");
233 let mut position = PoolPosition::new(owner, -100, 100, 1000);
234
235 let fee_growth_inside_0 = U256::from(100);
236 let fee_growth_inside_1 = U256::from(200);
237
238 position.update_fees(fee_growth_inside_0, fee_growth_inside_1);
239
240 assert_eq!(position.fee_growth_inside_0_last, fee_growth_inside_0);
241 assert_eq!(position.fee_growth_inside_1_last, fee_growth_inside_1);
242 }
245
246 #[rstest]
247 fn test_collect_fees() {
248 let owner = address!("1234567890123456789012345678901234567890");
249 let mut position = PoolPosition::new(owner, -100, 100, 1000);
250
251 position.tokens_owed_0 = 100;
253 position.tokens_owed_1 = 200;
254
255 position.collect_fees(50, 150);
257
258 assert_eq!(position.total_amount0_collected, 50);
259 assert_eq!(position.total_amount1_collected, 150);
260 assert_eq!(position.tokens_owed_0, 50);
261 assert_eq!(position.tokens_owed_1, 50);
262 }
263
264 #[rstest]
265 fn test_collect_fees_more_than_owed() {
266 let owner = address!("1234567890123456789012345678901234567890");
267 let mut position = PoolPosition::new(owner, -100, 100, 1000);
268
269 position.tokens_owed_0 = 100;
270 position.tokens_owed_1 = 200;
271
272 position.collect_fees(150, 300);
274
275 assert_eq!(position.total_amount0_collected, 100); assert_eq!(position.total_amount1_collected, 200);
277 assert_eq!(position.tokens_owed_0, 0);
278 assert_eq!(position.tokens_owed_1, 0);
279 }
280
281 #[rstest]
282 fn test_is_empty() {
283 let owner = address!("1234567890123456789012345678901234567890");
284 let mut position = PoolPosition::new(owner, -100, 100, 0);
285
286 assert!(position.is_empty());
287
288 position.liquidity = 100;
289 assert!(!position.is_empty());
290
291 position.liquidity = 0;
292 position.tokens_owed_0 = 50;
293 assert!(!position.is_empty());
294
295 position.tokens_owed_0 = 0;
296 position.tokens_owed_1 = 25;
297 assert!(!position.is_empty());
298
299 position.tokens_owed_1 = 0;
300 assert!(position.is_empty());
301 }
302}