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