templar_common/
price.rs

1use std::marker::PhantomData;
2
3use primitive_types::U256;
4
5use crate::{
6    asset::{AssetClass, BorrowAsset, CollateralAsset, FungibleAssetAmount},
7    number::Decimal,
8    oracle::pyth,
9};
10
11#[derive(Clone, Debug)]
12pub struct Price<T: AssetClass> {
13    _asset: PhantomData<T>,
14    price: u128,
15    confidence: u128,
16    exponent: i32,
17}
18
19pub mod error {
20    use thiserror::Error;
21
22    #[derive(Clone, Debug, Error)]
23    #[error("Bad price data: {0}")]
24    pub enum PriceDataError {
25        #[error("Reported negative price")]
26        NegativePrice,
27        #[error("Confidence interval too large")]
28        ConfidenceIntervalTooLarge,
29        #[error("Exponent out of bounds")]
30        ExponentOutOfBounds,
31    }
32}
33
34fn from_pyth_price<T: AssetClass>(
35    pyth_price: &pyth::Price,
36    decimals: i32,
37) -> Result<Price<T>, error::PriceDataError> {
38    let Ok(price) = u64::try_from(pyth_price.price.0) else {
39        return Err(error::PriceDataError::NegativePrice);
40    };
41
42    if pyth_price.conf.0 >= price {
43        return Err(error::PriceDataError::ConfidenceIntervalTooLarge);
44    }
45
46    let Some(exponent) = pyth_price.expo.checked_sub(decimals) else {
47        return Err(error::PriceDataError::ExponentOutOfBounds);
48    };
49
50    Ok(Price {
51        _asset: PhantomData,
52        price: u128::from(price),
53        confidence: u128::from(pyth_price.conf.0),
54        exponent,
55    })
56}
57
58#[derive(Clone, Debug)]
59pub struct PricePair {
60    pub collateral: Price<CollateralAsset>,
61    pub borrow: Price<BorrowAsset>,
62}
63
64impl PricePair {
65    /// # Errors
66    ///
67    /// - If the price data are invalid.
68    pub fn new(
69        collateral_price: &pyth::Price,
70        collateral_decimals: i32,
71        borrow_price: &pyth::Price,
72        borrow_decimals: i32,
73    ) -> Result<Self, error::PriceDataError> {
74        Ok(Self {
75            collateral: from_pyth_price(collateral_price, collateral_decimals)?,
76            borrow: from_pyth_price(borrow_price, borrow_decimals)?,
77        })
78    }
79}
80
81pub trait Appraise<T: AssetClass> {
82    fn valuation(&self, amount: FungibleAssetAmount<T>) -> Valuation;
83}
84
85pub trait Convert<T: AssetClass, U: AssetClass> {
86    fn convert(&self, amount: FungibleAssetAmount<T>) -> Decimal;
87}
88
89impl Appraise<BorrowAsset> for PricePair {
90    fn valuation(&self, amount: FungibleAssetAmount<BorrowAsset>) -> Valuation {
91        Valuation::optimistic(amount, &self.borrow)
92    }
93}
94
95impl Convert<BorrowAsset, CollateralAsset> for PricePair {
96    fn convert(&self, amount: FungibleAssetAmount<BorrowAsset>) -> Decimal {
97        #[allow(clippy::unwrap_used, reason = "not div0")]
98        self.valuation(amount)
99            .ratio(self.valuation(FungibleAssetAmount::<CollateralAsset>::new(1)))
100            .unwrap()
101    }
102}
103
104impl Appraise<CollateralAsset> for PricePair {
105    fn valuation(&self, amount: FungibleAssetAmount<CollateralAsset>) -> Valuation {
106        Valuation::pessimistic(amount, &self.collateral)
107    }
108}
109
110impl Convert<CollateralAsset, BorrowAsset> for PricePair {
111    fn convert(&self, amount: FungibleAssetAmount<CollateralAsset>) -> Decimal {
112        #[allow(clippy::unwrap_used, reason = "not div0")]
113        self.valuation(amount)
114            .ratio(self.valuation(FungibleAssetAmount::<BorrowAsset>::new(1)))
115            .unwrap()
116    }
117}
118
119#[derive(Debug, Clone, Copy)]
120pub struct Valuation {
121    coefficient: primitive_types::U256,
122    exponent: i32,
123}
124
125impl Valuation {
126    pub fn optimistic<T: AssetClass>(amount: FungibleAssetAmount<T>, price: &Price<T>) -> Self {
127        Self {
128            coefficient: U256::from(u128::from(amount))
129                * U256::from(price.price + price.confidence), // guaranteed not to overflow
130            exponent: price.exponent,
131        }
132    }
133
134    pub fn pessimistic<T: AssetClass>(amount: FungibleAssetAmount<T>, price: &Price<T>) -> Self {
135        Self {
136            coefficient: U256::from(u128::from(amount))
137                * U256::from(price.price - price.confidence), // guaranteed not to overflow
138            exponent: price.exponent,
139        }
140    }
141
142    /// Returns the ratio between this and another `Valuation`.
143    /// When the two `Valuation`s are within a few orders of magnitude of each
144    /// other, the ratio will be as accurate as `Decimal` can represent.
145    /// Otherwise, it will return a power of two close to the correct ratio.
146    /// If the ratio is outside the representable range of `Decimal`, it will
147    /// return `Decimal::MAX` if the ratio is too large, and `Decimal::MIN`
148    /// (zero) if the ratio is too small.
149    #[allow(clippy::cast_possible_wrap)]
150    pub fn ratio(self, rhs: Self) -> Option<Decimal> {
151        if rhs.coefficient.is_zero() {
152            // div0
153            return None;
154        }
155
156        if let Some(combined_exponents) = self
157            .exponent
158            .checked_sub(rhs.exponent)
159            .and_then(|pow| Decimal::from(self.coefficient).mul_pow10(pow))
160        {
161            return Some(combined_exponents / Decimal::from(rhs.coefficient));
162        }
163
164        // Exact value calculation failed. This can happen when the difference
165        // in exponents is extremely large, or when `self.coefficient` is
166        // extremely small or extremely large.
167        //
168        // Approximate by logarithm instead.
169        //
170        // 345_060_773 / 103_873_643 (=3.321928094887362) is a close approximation of log2(10) (=3.32192809488736234...)
171        let self_log2 =
172            i64::from(self.exponent) * 345_060_773 / 103_873_643 + self.coefficient.bits() as i64;
173        let rhs_log2 =
174            i64::from(rhs.exponent) * 345_060_773 / 103_873_643 + rhs.coefficient.bits() as i64;
175
176        let result_log2 = self_log2 - rhs_log2;
177
178        Some(if result_log2 >= 0 {
179            u32::try_from(result_log2)
180                .ok()
181                .and_then(Decimal::pow2_int)
182                .unwrap_or(Decimal::MAX)
183        } else {
184            result_log2
185                .checked_neg()
186                .and_then(|n| u32::try_from(n).ok())
187                .and_then(Decimal::pow2_int)
188                .map_or(Decimal::MIN, |r| Decimal::ONE / r)
189        })
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use rstest::rstest;
196
197    use crate::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}