defituna_client/implementation/
tuna_position.rs

1use crate::accounts::*;
2use crate::consts::HUNDRED_PERCENT;
3use crate::math::fixed::Rounding;
4use crate::math::orca::liquidity::get_amounts_for_liquidity;
5use crate::math::{sqrt_price_x64_to_price_x64, Fixed128};
6use crate::types::*;
7use crate::TunaError as ErrorCode;
8use fixed::types::U64F64;
9use orca_whirlpools_core::{tick_index_to_sqrt_price, MAX_TICK_INDEX, MIN_TICK_INDEX};
10use std::fmt;
11
12impl TunaPosition {
13    /// Returns the total position balance.
14    pub fn get_total_balance(&self, sqrt_price: u128) -> Result<(u64, u64), ErrorCode> {
15        let lower_sqrt_price_x64 = tick_index_to_sqrt_price(self.tick_lower_index);
16        let upper_sqrt_price_x64 = tick_index_to_sqrt_price(self.tick_upper_index);
17        get_amounts_for_liquidity(sqrt_price, lower_sqrt_price_x64, upper_sqrt_price_x64, self.liquidity)
18    }
19
20    /// Returns the current position total and debt size.
21    pub fn compute_total_and_debt(&self, sqrt_price: u128, vault_a: &Vault, vault_b: &Vault) -> Result<(u64, u64), ErrorCode> {
22        let (mut total_a, mut total_b) = self.get_total_balance(sqrt_price)?;
23
24        // Add leftovers to the total position amount.
25        total_a = total_a.checked_add(self.leftovers_a).ok_or(ErrorCode::MathOverflow)?;
26        total_b = total_b.checked_add(self.leftovers_b).ok_or(ErrorCode::MathOverflow)?;
27
28        let price = sqrt_price_x64_to_price_x64(sqrt_price)?;
29
30        let total = U64F64::from(total_a)
31            .checked_mul(price)
32            .ok_or(ErrorCode::MathOverflow)?
33            .to_num::<u64>()
34            .checked_add(total_b)
35            .ok_or(ErrorCode::MathOverflow)?;
36
37        let debt_a = vault_a.calculate_borrowed_funds(self.loan_shares_a, Rounding::Up)?;
38        let debt_b = vault_b.calculate_borrowed_funds(self.loan_shares_b, Rounding::Up)?;
39
40        let debt = U64F64::from(debt_a)
41            .checked_mul(price)
42            .ok_or(ErrorCode::MathOverflow)?
43            .to_num::<u64>()
44            .checked_add(debt_b)
45            .ok_or(ErrorCode::MathOverflow)?;
46
47        Ok((total, debt))
48    }
49
50    /// Returns if the position is healthy or not. Vaults must be passed with accrued interest.
51    pub fn is_healthy(&self, sqrt_price: u128, market: &Market, vault_a: &Vault, vault_b: &Vault) -> Result<(bool, u32), ErrorCode> {
52        if (self.loan_shares_a == 0 && self.loan_shares_b == 0) || self.liquidity == 0 {
53            return Ok((true, 0));
54        }
55
56        if vault_a.mint != self.mint_a || vault_b.mint != self.mint_b {
57            return Err(ErrorCode::InvalidInstructionArguments.into());
58        }
59
60        let (total, debt) = self.compute_total_and_debt(sqrt_price, vault_a, vault_b)?;
61
62        // Compute if the position is healthy. Can't overflow because liquidation_threshold <= 1e6 and total is a little bigger than u64::MAX.
63        let healthy = total == 0 || debt <= (total as u128 * market.liquidation_threshold as u128 / HUNDRED_PERCENT as u128) as u64;
64        let ratio = if total == 0 {
65            0
66        } else {
67            (debt as u128 * HUNDRED_PERCENT as u128 / total as u128) as u32
68        };
69        Ok((healthy, ratio))
70    }
71
72    pub fn is_liquidated(&self) -> bool {
73        self.state != TunaPositionState::Normal
74    }
75
76    pub fn is_limit_order_reached(&self, sqrt_price: u128) -> bool {
77        // Old positions don't have limit orders.
78        if self.version < 4 {
79            return false;
80        }
81
82        if self.tick_stop_loss_index >= MIN_TICK_INDEX {
83            let stop_loss_sqrt_price = tick_index_to_sqrt_price(self.tick_stop_loss_index);
84            if sqrt_price <= stop_loss_sqrt_price {
85                return true;
86            }
87        }
88
89        if self.tick_take_profit_index <= MAX_TICK_INDEX {
90            let take_profit_sqrt_price = tick_index_to_sqrt_price(self.tick_take_profit_index);
91            if sqrt_price >= take_profit_sqrt_price {
92                return true;
93            }
94        }
95
96        false
97    }
98
99    /// Returns the current leverage of a position. Vaults must be passed with accrued interest.
100    pub fn compute_leverage(&self, sqrt_price: u128, vault_a: &Vault, vault_b: &Vault) -> Result<Fixed128, ErrorCode> {
101        let (total, debt) = self.compute_total_and_debt(sqrt_price, vault_a, vault_b)?;
102
103        // We assume that the leverage of an empty position is always 1.0x.
104        if total == 0 {
105            return Ok(Fixed128::ONE);
106        }
107
108        if debt >= total {
109            return Err(ErrorCode::LeverageIsOutOfRange.into());
110        }
111
112        let leverage = Fixed128::from_num(total) / Fixed128::from_num(total - debt);
113        Ok(leverage)
114    }
115
116    pub fn get_pool_key(&self) -> String {
117        self.pool.to_string()
118    }
119}
120
121impl fmt::Display for TunaPosition {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(
124            f,
125            "L={}; shares=[{}; {}]; rng=[{}; {}]; sl/tp=[{}; {}]; pool={}",
126            self.liquidity,
127            self.loan_shares_a,
128            self.loan_shares_b,
129            self.tick_lower_index,
130            self.tick_upper_index,
131            if self.tick_stop_loss_index == i32::MIN {
132                "--".to_string()
133            } else {
134                self.tick_stop_loss_index.to_string()
135            },
136            if self.tick_take_profit_index == i32::MAX {
137                "--".to_string()
138            } else {
139                self.tick_take_profit_index.to_string()
140            },
141            self.pool.to_string()
142        )
143    }
144}