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 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), 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), exponent: price.exponent,
139 }
140 }
141
142 #[allow(clippy::cast_possible_wrap)]
150 pub fn ratio(self, rhs: Self) -> Option<Decimal> {
151 if rhs.coefficient.is_zero() {
152 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 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 #[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}