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)]
171mod tests {
172 use alloy_primitives::address;
173
174 use super::*;
175
176 #[test]
177 fn test_new_position() {
178 let owner = address!("1234567890123456789012345678901234567890");
179 let tick_lower = -100;
180 let tick_upper = 100;
181 let liquidity = 1000i128;
182
183 let position = PoolPosition::new(owner, tick_lower, tick_upper, liquidity);
184
185 assert_eq!(position.owner, owner);
186 assert_eq!(position.tick_lower, tick_lower);
187 assert_eq!(position.tick_upper, tick_upper);
188 assert_eq!(position.liquidity, liquidity as u128);
189 assert_eq!(position.fee_growth_inside_0_last, U256::ZERO);
190 assert_eq!(position.fee_growth_inside_1_last, U256::ZERO);
191 assert_eq!(position.tokens_owed_0, 0);
192 assert_eq!(position.tokens_owed_1, 0);
193 }
194
195 #[test]
196 fn test_get_position_key() {
197 let owner = address!("1234567890123456789012345678901234567890");
198 let tick_lower = -100;
199 let tick_upper = 100;
200
201 let key = PoolPosition::get_position_key(&owner, tick_lower, tick_upper);
202 let expected = format!("{:?}:{}:{}", owner, tick_lower, tick_upper);
203 assert_eq!(key, expected);
204 }
205
206 #[test]
207 fn test_update_liquidity_positive() {
208 let owner = address!("1234567890123456789012345678901234567890");
209 let mut position = PoolPosition::new(owner, -100, 100, 1000);
210
211 position.update_liquidity(500);
212 assert_eq!(position.liquidity, 1500);
213 }
214
215 #[test]
216 fn test_update_liquidity_negative() {
217 let owner = address!("1234567890123456789012345678901234567890");
218 let mut position = PoolPosition::new(owner, -100, 100, 1000);
219
220 position.update_liquidity(-300);
221 assert_eq!(position.liquidity, 700);
222 }
223
224 #[test]
225 fn test_update_liquidity_negative_saturating() {
226 let owner = address!("1234567890123456789012345678901234567890");
227 let mut position = PoolPosition::new(owner, -100, 100, 1000);
228
229 position.update_liquidity(-2000); assert_eq!(position.liquidity, 0);
231 }
232
233 #[test]
234 fn test_update_fees() {
235 let owner = address!("1234567890123456789012345678901234567890");
236 let mut position = PoolPosition::new(owner, -100, 100, 1000);
237
238 let fee_growth_inside_0 = U256::from(100);
239 let fee_growth_inside_1 = U256::from(200);
240
241 position.update_fees(fee_growth_inside_0, fee_growth_inside_1);
242
243 assert_eq!(position.fee_growth_inside_0_last, fee_growth_inside_0);
244 assert_eq!(position.fee_growth_inside_1_last, fee_growth_inside_1);
245 }
248
249 #[test]
250 fn test_collect_fees() {
251 let owner = address!("1234567890123456789012345678901234567890");
252 let mut position = PoolPosition::new(owner, -100, 100, 1000);
253
254 position.tokens_owed_0 = 100;
256 position.tokens_owed_1 = 200;
257
258 position.collect_fees(50, 150);
260
261 assert_eq!(position.total_amount0_collected, 50);
262 assert_eq!(position.total_amount1_collected, 150);
263 assert_eq!(position.tokens_owed_0, 50);
264 assert_eq!(position.tokens_owed_1, 50);
265 }
266
267 #[test]
268 fn test_collect_fees_more_than_owed() {
269 let owner = address!("1234567890123456789012345678901234567890");
270 let mut position = PoolPosition::new(owner, -100, 100, 1000);
271
272 position.tokens_owed_0 = 100;
273 position.tokens_owed_1 = 200;
274
275 position.collect_fees(150, 300);
277
278 assert_eq!(position.total_amount0_collected, 100); assert_eq!(position.total_amount1_collected, 200);
280 assert_eq!(position.tokens_owed_0, 0);
281 assert_eq!(position.tokens_owed_1, 0);
282 }
283
284 #[test]
285 fn test_is_empty() {
286 let owner = address!("1234567890123456789012345678901234567890");
287 let mut position = PoolPosition::new(owner, -100, 100, 0);
288
289 assert!(position.is_empty());
290
291 position.liquidity = 100;
292 assert!(!position.is_empty());
293
294 position.liquidity = 0;
295 position.tokens_owed_0 = 50;
296 assert!(!position.is_empty());
297
298 position.tokens_owed_0 = 0;
299 position.tokens_owed_1 = 25;
300 assert!(!position.is_empty());
301
302 position.tokens_owed_1 = 0;
303 assert!(position.is_empty());
304 }
305}