templar_common/
price.rs

1use std::marker::PhantomData;
2
3use primitive_types::U256;
4use templar_primitives::number::Decimal;
5
6use crate::{
7    asset::{AssetClass, BorrowAsset, CollateralAsset, FungibleAssetAmount},
8    oracle::pyth,
9};
10
11#[derive(Clone, Debug)]
12#[allow(clippy::struct_field_names)]
13pub struct Price<T: AssetClass> {
14    _asset: PhantomData<T>,
15    price: u128,
16    confidence: u128,
17    exponent: i32,
18}
19
20pub mod error {
21    use thiserror::Error;
22
23    #[derive(Clone, Debug, Error)]
24    #[error("Bad price data: {0}")]
25    pub enum PriceDataError {
26        #[error("Reported negative price")]
27        NegativePrice,
28        #[error("Confidence interval too large")]
29        ConfidenceIntervalTooLarge,
30        #[error("Exponent out of bounds")]
31        ExponentOutOfBounds,
32    }
33}
34
35fn from_pyth_price<T: AssetClass>(
36    pyth_price: &pyth::Price,
37    decimals: i32,
38) -> Result<Price<T>, error::PriceDataError> {
39    let Ok(price) = u64::try_from(pyth_price.price.0) else {
40        return Err(error::PriceDataError::NegativePrice);
41    };
42
43    if pyth_price.conf.0 >= price {
44        return Err(error::PriceDataError::ConfidenceIntervalTooLarge);
45    }
46
47    let Some(exponent) = pyth_price.expo.checked_sub(decimals) else {
48        return Err(error::PriceDataError::ExponentOutOfBounds);
49    };
50
51    Ok(Price {
52        _asset: PhantomData,
53        price: u128::from(price),
54        confidence: u128::from(pyth_price.conf.0),
55        exponent,
56    })
57}
58
59#[derive(Clone, Debug)]
60pub struct PricePair {
61    pub collateral: Price<CollateralAsset>,
62    pub borrow: Price<BorrowAsset>,
63}
64
65impl PricePair {
66    /// # Errors
67    ///
68    /// - If the price data are invalid.
69    pub fn new(
70        collateral_price: &pyth::Price,
71        collateral_decimals: i32,
72        borrow_price: &pyth::Price,
73        borrow_decimals: i32,
74    ) -> Result<Self, error::PriceDataError> {
75        Ok(Self {
76            collateral: from_pyth_price(collateral_price, collateral_decimals)?,
77            borrow: from_pyth_price(borrow_price, borrow_decimals)?,
78        })
79    }
80}
81
82pub trait Appraise<T: AssetClass> {
83    fn valuation(&self, amount: FungibleAssetAmount<T>) -> Valuation;
84}
85
86pub trait Convert<T: AssetClass, U: AssetClass> {
87    fn convert(&self, amount: FungibleAssetAmount<T>) -> Decimal;
88}
89
90impl Appraise<BorrowAsset> for PricePair {
91    fn valuation(&self, amount: FungibleAssetAmount<BorrowAsset>) -> Valuation {
92        Valuation::optimistic(amount, &self.borrow)
93    }
94}
95
96impl Convert<BorrowAsset, CollateralAsset> for PricePair {
97    fn convert(&self, amount: FungibleAssetAmount<BorrowAsset>) -> Decimal {
98        #[allow(clippy::unwrap_used, reason = "not div0")]
99        self.valuation(amount)
100            .ratio(self.valuation(FungibleAssetAmount::<CollateralAsset>::new(1)))
101            .unwrap()
102    }
103}
104
105impl Appraise<CollateralAsset> for PricePair {
106    fn valuation(&self, amount: FungibleAssetAmount<CollateralAsset>) -> Valuation {
107        Valuation::pessimistic(amount, &self.collateral)
108    }
109}
110
111impl Convert<CollateralAsset, BorrowAsset> for PricePair {
112    fn convert(&self, amount: FungibleAssetAmount<CollateralAsset>) -> Decimal {
113        #[allow(clippy::unwrap_used, reason = "not div0")]
114        self.valuation(amount)
115            .ratio(self.valuation(FungibleAssetAmount::<BorrowAsset>::new(1)))
116            .unwrap()
117    }
118}
119
120#[derive(Debug, Clone, Copy)]
121pub struct Valuation {
122    coefficient: primitive_types::U256,
123    exponent: i32,
124}
125
126impl Valuation {
127    pub fn optimistic<T: AssetClass>(amount: FungibleAssetAmount<T>, price: &Price<T>) -> Self {
128        Self {
129            coefficient: U256::from(u128::from(amount))
130                * U256::from(price.price + price.confidence), // guaranteed not to overflow
131            exponent: price.exponent,
132        }
133    }
134
135    pub fn pessimistic<T: AssetClass>(amount: FungibleAssetAmount<T>, price: &Price<T>) -> Self {
136        Self {
137            coefficient: U256::from(u128::from(amount))
138                * U256::from(price.price - price.confidence), // guaranteed not to overflow
139            exponent: price.exponent,
140        }
141    }
142
143    /// Returns the ratio between this and another `Valuation`.
144    /// When the two `Valuation`s are within a few orders of magnitude of each
145    /// other, the ratio will be as accurate as `Decimal` can represent.
146    /// Otherwise, it will return a power of two close to the correct ratio.
147    /// If the ratio is outside the representable range of `Decimal`, it will
148    /// return `Decimal::MAX` if the ratio is too large, and `Decimal::MIN`
149    /// (zero) if the ratio is too small.
150    #[allow(clippy::cast_possible_wrap)]
151    pub fn ratio(self, rhs: Self) -> Option<Decimal> {
152        if rhs.coefficient.is_zero() {
153            // div0
154            return None;
155        }
156
157        if let Some(combined_exponents) = self
158            .exponent
159            .checked_sub(rhs.exponent)
160            .and_then(|pow| Decimal::from(self.coefficient).mul_pow10(pow))
161        {
162            return Some(combined_exponents / Decimal::from(rhs.coefficient));
163        }
164
165        // Exact value calculation failed. This can happen when the difference
166        // in exponents is extremely large, or when `self.coefficient` is
167        // extremely small or extremely large.
168        //
169        // Approximate by logarithm instead.
170        //
171        // 345_060_773 / 103_873_643 (=3.321928094887362) is a close approximation of log2(10) (=3.32192809488736234...)
172        let self_log2 =
173            i64::from(self.exponent) * 345_060_773 / 103_873_643 + self.coefficient.bits() as i64;
174        let rhs_log2 =
175            i64::from(rhs.exponent) * 345_060_773 / 103_873_643 + rhs.coefficient.bits() as i64;
176
177        let result_log2 = self_log2 - rhs_log2;
178
179        Some(if result_log2 >= 0 {
180            u32::try_from(result_log2)
181                .ok()
182                .and_then(Decimal::pow2_int)
183                .unwrap_or(Decimal::MAX)
184        } else {
185            result_log2
186                .checked_neg()
187                .and_then(|n| u32::try_from(n).ok())
188                .and_then(Decimal::pow2_int)
189                .map_or(Decimal::MIN, |r| Decimal::ONE / r)
190        })
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use rstest::rstest;
197    use templar_primitives::dec;
198
199    use super::*;
200
201    #[test]
202    fn valuation_eq() {
203        let o = Valuation::optimistic(
204            1000u128.into(),
205            &Price::<BorrowAsset> {
206                _asset: PhantomData,
207                price: 250,
208                confidence: 12,
209                exponent: -5,
210            },
211        );
212
213        assert_eq!(o.coefficient, U256::from(1000 * (250 + 12)));
214        assert_eq!(o.exponent, -5);
215
216        let p = Valuation::pessimistic(
217            1000u128.into(),
218            &Price::<BorrowAsset> {
219                _asset: PhantomData,
220                price: 250,
221                confidence: 12,
222                exponent: -5,
223            },
224        );
225
226        assert_eq!(p.coefficient, U256::from(1000 * (250 - 12)));
227        assert_eq!(p.exponent, -5);
228    }
229
230    #[test]
231    fn valuation_ratio_equal() {
232        let first = Valuation::optimistic(
233            600u128.into(),
234            &Price::<BorrowAsset> {
235                _asset: PhantomData,
236                price: 100,
237                confidence: 0,
238                exponent: 4,
239            },
240        );
241        let second = Valuation::pessimistic(
242            60u128.into(),
243            &Price::<BorrowAsset> {
244                _asset: PhantomData,
245                price: 1000,
246                confidence: 0,
247                exponent: 4,
248            },
249        );
250
251        assert_eq!(first.ratio(second).unwrap(), Decimal::ONE);
252    }
253
254    #[rstest]
255    #[case(8, 1, 8, 0,      dec!("1"))]
256    #[case(1, 25, 1, -2,    dec!("4"))]
257    #[case(0, 1, 1, 0,      dec!("0"))]
258    #[case(800, 2, 4, 2,    dec!("1"))]
259    #[case(u128::MAX, 1, 1, i32::MIN, Decimal::MAX)]
260    #[case(1, 1, 1, i32::MAX, Decimal::MIN)]
261    // The following case returns a power of 2. Whereas the *correct* answer is
262    // 1e+115, the approximation 2^382 is about 9.85e+114. Keep in mind Decimal
263    // only supports a total of 115 whole decimal digits.
264    #[case(u128::MAX, u128::MAX, 1, -115, Decimal::pow2_int(382).unwrap())]
265    #[case(1, 1, 1, 39, Decimal::ZERO)]
266    #[test]
267    fn valuation_ratios(
268        #[case] value: u128,
269        #[case] divisor_value: u128,
270        #[case] divisor_price: u128,
271        #[case] divisor_exponent: i32,
272        #[case] expected_result: impl Into<Decimal>,
273    ) {
274        let dividend = Valuation::optimistic(
275            value.into(),
276            &Price::<BorrowAsset> {
277                _asset: PhantomData,
278                price: 1,
279                confidence: 0,
280                exponent: 0,
281            },
282        );
283
284        let divisor = Valuation::optimistic(
285            divisor_value.into(),
286            &Price::<BorrowAsset> {
287                _asset: PhantomData,
288                price: divisor_price,
289                confidence: 0,
290                exponent: divisor_exponent,
291            },
292        );
293
294        println!("{dividend:?}");
295        println!("{divisor:?}");
296
297        assert_eq!(dividend.ratio(divisor).unwrap(), expected_result.into());
298    }
299}