defituna_core/quote/
tuna_spot_position.rs

1use crate::{CoreError, PoolTokenFacade, HUNDRED_PERCENT, INVALID_ARGUMENTS};
2use fusionamm_core::{sqrt_price_to_price, swap_quote_by_input_token, swap_quote_by_output_token, try_mul_div, FusionPoolFacade, TickArrays};
3use libm::{ceil, round};
4
5#[cfg(feature = "wasm")]
6use fusionamm_macros::wasm_expose;
7
8#[cfg_attr(feature = "wasm", wasm_expose)]
9pub struct IncreaseSpotPositionQuoteResult {
10    /** Required collateral amount */
11    pub collateral: u64,
12    /** Required amount to borrow */
13    pub borrow: u64,
14    /** Estimated position size in the position token. */
15    pub estimated_amount: u64,
16    /** Swap input amount. */
17    pub swap_input_amount: u64,
18    /** Protocol fee in token A */
19    pub protocol_fee_a: u64,
20    /** Protocol fee in token B */
21    pub protocol_fee_b: u64,
22    /** Price impact in percents */
23    pub price_impact: f64,
24}
25
26/// Spot position increase quote
27///
28/// # Parameters
29/// - `increase_amount`: Position total size in the collateral_token.
30/// - `collateral_token`: Collateral token.
31/// - `position_token`: Token of the position.
32/// - `leverage`: Leverage (1.0 or higher).
33/// - `protocol_fee_rate`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
34/// - `protocol_fee_rate_on_collateral`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
35/// - `fusion_pool`: Fusion pool.
36/// - `tick_arrays`: Five tick arrays around the current pool price.
37///
38/// # Returns
39/// - `IncreaseSpotPositionQuoteResult`: quote result
40#[cfg_attr(feature = "wasm", wasm_expose)]
41pub fn get_increase_spot_position_quote(
42    increase_amount: u64,
43    collateral_token: PoolTokenFacade,
44    position_token: PoolTokenFacade,
45    leverage: f64,
46    protocol_fee_rate: u32,
47    protocol_fee_rate_on_collateral: u32,
48    fusion_pool: FusionPoolFacade,
49    tick_arrays: TickArrays,
50) -> Result<IncreaseSpotPositionQuoteResult, CoreError> {
51    if leverage < 1.0 {
52        return Err(INVALID_ARGUMENTS.into());
53    }
54
55    if protocol_fee_rate >= HUNDRED_PERCENT {
56        return Err(INVALID_ARGUMENTS.into());
57    }
58
59    if protocol_fee_rate_on_collateral >= HUNDRED_PERCENT {
60        return Err(INVALID_ARGUMENTS.into());
61    }
62
63    let borrow: u64;
64    let mut collateral: u64;
65    let estimated_amount: u64;
66    let swap_input_amount: u64;
67    let mut next_sqrt_price = fusion_pool.sqrt_price;
68
69    let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
70
71    if position_token != collateral_token {
72        borrow = ceil((increase_amount as f64 * (leverage - 1.0)) / leverage) as u64;
73        collateral = increase_amount - apply_swap_fee(apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?, fusion_pool.fee_rate, false)?;
74        collateral = reverse_apply_swap_fee(collateral, fusion_pool.fee_rate, false)?;
75        collateral = reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
76
77        swap_input_amount = increase_amount;
78        estimated_amount = round(if collateral_token == PoolTokenFacade::A {
79            increase_amount as f64 * price
80        } else {
81            increase_amount as f64 / price
82        }) as u64;
83    } else {
84        let position_to_borrowed_token_price = if collateral_token == PoolTokenFacade::A { price } else { 1.0 / price };
85        let borrow_in_position_token = ceil((increase_amount as f64 * (leverage - 1.0)) / leverage);
86
87        borrow = ceil(borrow_in_position_token * position_to_borrowed_token_price) as u64;
88
89        let borrow_in_position_token_with_fees_applied =
90            apply_swap_fee(apply_tuna_protocol_fee(borrow_in_position_token as u64, protocol_fee_rate, false)?, fusion_pool.fee_rate, false)?;
91
92        collateral = increase_amount - borrow_in_position_token_with_fees_applied;
93        collateral = reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
94
95        swap_input_amount = apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?;
96        estimated_amount = increase_amount;
97    }
98
99    if swap_input_amount > 0 {
100        let is_token_a = position_token == PoolTokenFacade::B;
101        next_sqrt_price = swap_quote_by_input_token(swap_input_amount, is_token_a, 0, fusion_pool, tick_arrays, None, None)?.next_sqrt_price;
102    }
103
104    let mut protocol_fee_a = 0;
105    let mut protocol_fee_b = 0;
106
107    if collateral_token == PoolTokenFacade::A {
108        protocol_fee_a += collateral - apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
109    } else {
110        protocol_fee_b += collateral - apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
111    }
112
113    if position_token == PoolTokenFacade::B {
114        protocol_fee_a += borrow - apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?;
115    } else {
116        protocol_fee_b += borrow - apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?;
117    }
118
119    let new_price = sqrt_price_to_price(next_sqrt_price.into(), 1, 1);
120    let price_impact = (new_price / price - 1.0).abs() * 100.0;
121
122    Ok(IncreaseSpotPositionQuoteResult {
123        collateral,
124        borrow,
125        estimated_amount,
126        swap_input_amount,
127        protocol_fee_a,
128        protocol_fee_b,
129        price_impact,
130    })
131}
132
133#[cfg_attr(feature = "wasm", wasm_expose)]
134pub struct DecreaseSpotPositionQuoteResult {
135    /** Position decrease percentage */
136    pub decrease_percent: u32,
137    /** Collateral token of the new position */
138    pub collateral_token: PoolTokenFacade,
139    /** Token of the new position */
140    pub position_token: PoolTokenFacade,
141    /** Required additional collateral amount */
142    pub collateral: u64,
143    /** Required amount to borrow */
144    pub borrow: u64,
145    /** Swap input amount. */
146    pub swap_input_amount: u64,
147    /** Estimated total amount of the new position */
148    pub estimated_amount: u64,
149    /** Protocol fee in token A */
150    pub protocol_fee_a: u64,
151    /** Protocol fee in token B */
152    pub protocol_fee_b: u64,
153    /** Price impact in percents */
154    pub price_impact: f64,
155}
156
157/// Spot position decrease quote
158///
159/// # Parameters
160/// - `increase_amount`: Position total size in the collateral_token.
161/// - `collateral_token`: Collateral token.
162/// - `position_token`: Token of the position.
163/// - `leverage`: Leverage (1.0 or higher).
164/// - `position_amount`: Existing position amount in the position_token.
165/// - `position_debt`: Existing position debt in the token opposite to the position_token.
166/// - `reduce_only`: Only allow reducing the existing position.
167/// - `protocol_fee_rate`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
168/// - `protocol_fee_rate_on_collateral`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
169/// - `fusion_pool`: Fusion pool.
170/// - `tick_arrays`: Five tick arrays around the current pool price.
171///
172/// # Returns
173/// - `DecreaseSpotPositionQuoteResult`: quote result
174#[cfg_attr(feature = "wasm", wasm_expose)]
175pub fn get_decrease_spot_position_quote(
176    decrease_amount: u64,
177    collateral_token: PoolTokenFacade,
178    position_token: PoolTokenFacade,
179    leverage: f64,
180    position_amount: u64,
181    position_debt: u64,
182    reduce_only: bool,
183    protocol_fee_rate: u32,
184    protocol_fee_rate_on_collateral: u32,
185    fusion_pool: FusionPoolFacade,
186    tick_arrays: TickArrays,
187) -> Result<DecreaseSpotPositionQuoteResult, CoreError> {
188    if leverage < 1.0 {
189        return Err(INVALID_ARGUMENTS.into());
190    }
191
192    if protocol_fee_rate >= HUNDRED_PERCENT {
193        return Err(INVALID_ARGUMENTS.into());
194    }
195
196    if protocol_fee_rate_on_collateral >= HUNDRED_PERCENT {
197        return Err(INVALID_ARGUMENTS.into());
198    }
199
200    let mut collateral = 0;
201    let mut borrow = 0;
202    let mut swap_input_amount = 0;
203    let estimated_amount: u64;
204    let mut next_sqrt_price = fusion_pool.sqrt_price;
205    let mut new_position_token = position_token;
206    let decrease_percent: u32;
207
208    // Current pool price
209    let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
210    let position_to_opposite_token_price = if position_token == PoolTokenFacade::A { price } else { 1.0 / price };
211
212    let mut decrease_amount_in_position_token = if collateral_token == position_token {
213        decrease_amount
214    } else {
215        round(decrease_amount as f64 / position_to_opposite_token_price) as u64
216    };
217
218    if reduce_only && decrease_amount_in_position_token > position_amount {
219        decrease_amount_in_position_token = position_amount;
220    }
221
222    if decrease_amount_in_position_token <= position_amount {
223        decrease_percent = ((decrease_amount_in_position_token * HUNDRED_PERCENT as u64 / position_amount) as u32).min(HUNDRED_PERCENT);
224
225        estimated_amount = position_amount - decrease_amount_in_position_token;
226
227        if collateral_token == position_token {
228            if position_debt > 0 {
229                let swap_out = position_debt * decrease_percent as u64 / HUNDRED_PERCENT as u64;
230                let swap_quote = swap_quote_by_output_token(swap_out, position_token == PoolTokenFacade::B, 0, fusion_pool, tick_arrays, None, None)?;
231                next_sqrt_price = swap_quote.next_sqrt_price;
232                swap_input_amount = swap_quote.token_est_in;
233            }
234        } else {
235            swap_input_amount = position_amount - position_amount * (HUNDRED_PERCENT - decrease_percent) as u64 / HUNDRED_PERCENT as u64;
236            let swap_quote =
237                swap_quote_by_input_token(swap_input_amount, position_token == PoolTokenFacade::A, 0, fusion_pool, tick_arrays, None, None)?;
238            next_sqrt_price = swap_quote.next_sqrt_price;
239        }
240    } else {
241        decrease_percent = HUNDRED_PERCENT;
242        new_position_token = if position_token == PoolTokenFacade::A {
243            PoolTokenFacade::B
244        } else {
245            PoolTokenFacade::A
246        };
247        let increase_amount = decrease_amount_in_position_token - position_amount;
248
249        if position_token == collateral_token {
250            // Example:
251            // collateral_token: A
252            // position_token: A
253            // position_debt: B
254            // new_position_token: B
255            // newBorrowedToken: A
256
257            // B
258            estimated_amount = round(increase_amount as f64 * position_to_opposite_token_price) as u64;
259
260            // A
261            borrow = round((increase_amount as f64 * (leverage - 1.0)) / leverage) as u64;
262            let borrow_with_fees_applied = apply_swap_fee(apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?, fusion_pool.fee_rate, false)?;
263
264            collateral = increase_amount - borrow_with_fees_applied;
265
266            // B->A
267            if position_debt > 0 {
268                let swap_quote = swap_quote_by_output_token(
269                    position_debt,
270                    position_token != PoolTokenFacade::A,
271                    0,
272                    fusion_pool,
273                    tick_arrays.clone().into(),
274                    None,
275                    None,
276                )?;
277                swap_input_amount = swap_quote.token_est_in;
278            }
279
280            swap_input_amount += collateral + apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?;
281            let swap_quote =
282                swap_quote_by_input_token(swap_input_amount, position_token == PoolTokenFacade::A, 0, fusion_pool, tick_arrays, None, None)?;
283            next_sqrt_price = swap_quote.next_sqrt_price;
284
285            collateral = reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
286        } else {
287            // Example:
288            // collateral_token: B
289            // position_token: A
290            // new_position_token: B
291            // newBorrowedToken: A
292
293            // B
294            estimated_amount = round(increase_amount as f64 * position_to_opposite_token_price) as u64;
295
296            borrow = round((increase_amount as f64 * (leverage - 1.0)) / leverage) as u64;
297            let borrow_with_fees_applied = apply_swap_fee(apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?, fusion_pool.fee_rate, false)?;
298
299            collateral = increase_amount - borrow_with_fees_applied;
300            collateral = round(collateral as f64 * position_to_opposite_token_price) as u64;
301            collateral = reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
302
303            // A->B
304            swap_input_amount = position_amount + apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?;
305            let swap_quote =
306                swap_quote_by_input_token(swap_input_amount, position_token == PoolTokenFacade::A, 0, fusion_pool, tick_arrays, None, None)?;
307            next_sqrt_price = swap_quote.next_sqrt_price;
308        }
309    }
310
311    let mut protocol_fee_a = 0;
312    let mut protocol_fee_b = 0;
313
314    if collateral_token == PoolTokenFacade::A {
315        protocol_fee_a += collateral - apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
316    } else {
317        protocol_fee_b += collateral - apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
318    }
319
320    if position_token == PoolTokenFacade::B {
321        protocol_fee_a += borrow - apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?;
322    } else {
323        protocol_fee_b += borrow - apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?;
324    }
325
326    let new_price = sqrt_price_to_price(next_sqrt_price.into(), 1, 1);
327    let price_impact = (new_price / price - 1.0).abs() * 100.0;
328
329    Ok(DecreaseSpotPositionQuoteResult {
330        decrease_percent,
331        collateral_token,
332        position_token: new_position_token,
333        collateral,
334        borrow,
335        swap_input_amount,
336        estimated_amount,
337        protocol_fee_a,
338        protocol_fee_b,
339        price_impact,
340    })
341}
342
343/// Returns the liquidation price
344///
345/// # Parameters
346/// - `position_token`: Token of the position
347/// - `amount`: Position total size (decimal)
348/// - `debt`: Position total debt (decimal)
349/// - `liquidation_threshold`: Liquidation threshold of the market (decimal)
350///
351/// # Returns
352/// - `f64`: Decimal liquidation price
353#[cfg_attr(feature = "wasm", wasm_expose)]
354pub fn get_liquidation_price(position_token: PoolTokenFacade, amount: f64, debt: f64, liquidation_threshold: f64) -> Result<f64, CoreError> {
355    if debt < 0.0 || amount < 0.0 {
356        return Err(INVALID_ARGUMENTS);
357    }
358
359    if liquidation_threshold <= 0.0 || liquidation_threshold >= 1.0 {
360        return Err(INVALID_ARGUMENTS);
361    }
362
363    if debt == 0.0 || amount == 0.0 {
364        return Ok(0.0);
365    }
366
367    if position_token == PoolTokenFacade::A {
368        Ok(debt / (amount * liquidation_threshold))
369    } else {
370        Ok((amount * liquidation_threshold) / debt)
371    }
372}
373
374/// Calculates the maximum tradable amount in the collateral token.
375///
376/// # Parameters
377/// - `collateral_token`: Collateral token.
378/// - `available_balance`: Available wallet balance in the collateral_token.
379/// - `leverage`: Leverage (1.0 or higher).
380/// - `new_position_token`: Token of the new position.
381/// - `position_token`: Token of the existing position. Should be set to new_position_token if position_amount is zero.
382/// - `position_amount`: Existing position amount in the position_token.
383/// - `position_debt`: Existing position debt in the token opposite to the position_token.
384/// - `reduce_only`: Only allow reducing the existing position.///
385/// - `protocol_fee_rate`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
386/// - `protocol_fee_rate_on_collateral`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
387/// - `fusion_pool`: Fusion pool.
388/// - `tick_arrays`: Five tick arrays around the current pool price.
389///
390/// # Returns
391/// - `u64`: the maximum tradable amount
392#[cfg_attr(feature = "wasm", wasm_expose)]
393pub fn get_tradable_amount(
394    collateral_token: PoolTokenFacade,
395    available_balance: u64,
396    leverage: f64,
397    new_position_token: PoolTokenFacade,
398    position_token: PoolTokenFacade,
399    position_amount: u64,
400    position_debt: u64,
401    reduce_only: bool,
402    protocol_fee_rate: u32,
403    protocol_fee_rate_on_collateral: u32,
404    fusion_pool: FusionPoolFacade,
405    tick_arrays: TickArrays,
406) -> Result<u64, CoreError> {
407    if leverage < 1.0 {
408        return Err(INVALID_ARGUMENTS.into());
409    }
410
411    if protocol_fee_rate >= HUNDRED_PERCENT {
412        return Err(INVALID_ARGUMENTS.into());
413    }
414
415    if protocol_fee_rate_on_collateral >= HUNDRED_PERCENT {
416        return Err(INVALID_ARGUMENTS.into());
417    }
418
419    if position_amount == 0 && new_position_token != position_token {
420        return Err(INVALID_ARGUMENTS.into());
421    }
422
423    // T = C⋅Fc⋅Fs + B⋅Fb⋅Fs, where: Fc/Fb/Fs - collateral/borrow/swap fee multiplier
424    // B = T⋅(L - 1) / L
425    // => T = C⋅Fc⋅Fs / (1 - Fb⋅Fs⋅(L - 1) / L)
426    let add_leverage = |collateral: u64| -> Result<u64, CoreError> {
427        let mut collateral = apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
428        if collateral_token != new_position_token {
429            collateral = apply_swap_fee(collateral, fusion_pool.fee_rate, false)?;
430        }
431
432        let fee_multiplier = (1.0 - protocol_fee_rate as f64 / HUNDRED_PERCENT as f64) * (1.0 - fusion_pool.fee_rate as f64 / 1_000_000.0);
433        let total = (collateral as f64 / (1.0 - (fee_multiplier * (leverage - 1.0)) / leverage)) as u64;
434        Ok(total)
435    };
436
437    let available_to_trade = if new_position_token == position_token {
438        add_leverage(available_balance)?
439    } else {
440        let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
441        let position_to_opposite_token_price = if position_token == PoolTokenFacade::A { price } else { 1.0 / price };
442
443        if reduce_only {
444            if collateral_token == position_token {
445                position_amount
446            } else {
447                round(position_amount as f64 * position_to_opposite_token_price) as u64
448            }
449        } else {
450            let position_amount_in_collateral_token = if collateral_token == position_token {
451                position_amount
452            } else {
453                round(position_amount as f64 * position_to_opposite_token_price) as u64
454            };
455
456            let position_collateral = if collateral_token == position_token {
457                let swap_in = if position_debt > 0 {
458                    swap_quote_by_output_token(position_debt, position_token == PoolTokenFacade::B, 0, fusion_pool, tick_arrays, None, None)?
459                        .token_est_in
460                } else {
461                    0
462                };
463                position_amount - swap_in
464            } else {
465                if position_amount > 0 {
466                    let swap_quote =
467                        swap_quote_by_input_token(position_amount, position_token == PoolTokenFacade::A, 0, fusion_pool, tick_arrays, None, None)?;
468                    swap_quote.token_est_out - position_debt
469                } else {
470                    0
471                }
472            };
473
474            // Add the refunded collateral to the available balance
475            position_amount_in_collateral_token + add_leverage(available_balance + position_collateral)?
476        }
477    };
478
479    Ok(available_to_trade)
480}
481
482pub fn apply_tuna_protocol_fee(amount: u64, protocol_fee_rate: u32, round_up: bool) -> Result<u64, CoreError> {
483    try_mul_div(amount, HUNDRED_PERCENT as u128 - protocol_fee_rate as u128, HUNDRED_PERCENT as u128, round_up)
484}
485
486pub fn reverse_apply_tuna_protocol_fee(amount: u64, protocol_fee_rate: u32, round_up: bool) -> Result<u64, CoreError> {
487    try_mul_div(amount, HUNDRED_PERCENT as u128, HUNDRED_PERCENT as u128 - protocol_fee_rate as u128, round_up)
488}
489
490pub fn apply_swap_fee(amount: u64, fee_rate: u16, round_up: bool) -> Result<u64, CoreError> {
491    try_mul_div(amount, 1_000_000 - fee_rate as u128, 1_000_000, round_up)
492}
493
494pub fn reverse_apply_swap_fee(amount: u64, fee_rate: u16, round_up: bool) -> Result<u64, CoreError> {
495    try_mul_div(amount, 1_000_000, 1_000_000 - fee_rate as u128, round_up)
496}
497
498#[cfg(all(test, not(feature = "wasm")))]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn test_get_liquidation_price() {
504        assert_eq!(get_liquidation_price(PoolTokenFacade::A, 5.0, 0.0, 0.85), Ok(0.0));
505        assert_eq!(get_liquidation_price(PoolTokenFacade::A, 0.0, 5.0, 0.85), Ok(0.0));
506        assert_eq!(get_liquidation_price(PoolTokenFacade::A, 5.0, 800.0, 0.85), Ok(188.23529411764707));
507        assert_eq!(get_liquidation_price(PoolTokenFacade::B, 1000.0, 4.0, 0.85), Ok(212.5));
508    }
509}