riftenlabs-indexer/src/rpc/apy/poolperiod.rs

142 lines
4.6 KiB
Rust
Raw Normal View History

// Copyright (C) 2024 Riften Labs AS
//
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use anyhow::{bail, Context, Result};
use rust_decimal::MathematicalOps;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use rust_decimal_macros::dec;
use super::PoolSnapshot;
const SECONDS_IN_DAY: Decimal = dec!(86400.0);
const DAYS_IN_YEAR: Decimal = dec!(365.25);
#[derive(Debug, Clone)]
pub struct PoolPeriod {
pub start: PoolSnapshot,
pub end: PoolSnapshot,
start_k: Decimal,
end_k: Decimal,
}
impl PoolPeriod {
pub fn new(start: PoolSnapshot, end: PoolSnapshot) -> Result<Self> {
if start.timestamp > end.timestamp {
bail!(
"start timestamp ({}) > end timestamp ({})",
start.timestamp,
end.timestamp
)
}
let start_k = Decimal::from_u64(start.sats * start.token_amount)
.context("failed to convert inital_k to decimal")?;
let end_k = Decimal::from_u64(end.sats * end.token_amount)
.context("failed to convert final_k to decimal")?;
Ok(Self {
start,
end,
start_k,
end_k,
})
}
/// Duration in seconds of this pool period
/// starting: If provided; we assume that when the real starting position was BEFORE this timestamp,
/// then this was also the state of the pool at this time.
pub fn duration(&self, starting: &Option<u64>) -> u64 {
let start = match *starting {
Some(starting) => {
if starting > self.start.timestamp {
starting
} else {
self.start.timestamp
}
}
None => self.start.timestamp,
};
self.end.timestamp.saturating_sub(start)
}
pub fn durationd(&self, starting: &Option<u64>) -> Result<Decimal> {
Decimal::from_u64(self.duration(starting)).context("duration to decimal")
}
pub fn duration_days(&self, starting: &Option<u64>) -> Result<Decimal> {
Ok(self.durationd(starting)? / SECONDS_IN_DAY)
}
pub fn days_over_year(&self, starting: &Option<u64>) -> Result<Decimal> {
Ok(DAYS_IN_YEAR
.checked_div(self.duration_days(starting)?)
.unwrap_or_default())
}
2024-11-13 22:32:32 +01:00
pub fn end_k_sqrt(&self) -> Result<Decimal> {
self.end_k.sqrt().context("failed to sqrt end")
}
pub fn pool_yield(&self) -> Result<Decimal> {
let start_k_sr = self.start_k.sqrt().context("failed to sqrt start")?;
let end_k_sr = self.end_k.sqrt().context("failed to sqrt end")?;
Ok(((end_k_sr - start_k_sr) / start_k_sr) * Decimal::ONE_HUNDRED)
}
pub fn yield_and_apy(&self, starting: &Option<u64>) -> Result<(Decimal, Decimal)> {
let pool_yield = self.pool_yield()?;
let years_elapsed = self.days_over_year(starting)?;
// to avoid powd overflow; don't calculate for pools < 6 hour old
if self.duration(starting) < 3600 * 6 {
return Ok((pool_yield, Decimal::ZERO));
}
let apy = if years_elapsed.is_zero() {
Decimal::ZERO
} else {
(((pool_yield / Decimal::ONE_HUNDRED) + Decimal::ONE)
.checked_powd(years_elapsed)
.context("powd overflow")?
- Decimal::ONE)
* Decimal::ONE_HUNDRED
};
Ok((pool_yield, apy))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apy_and_yield() {
// http://localhost:3000/position/f67ff489fb3b8efaf5db1a2cf9e3faa07fdfd7903079262a534c921e7e7d0d2c17
// APY: 0.2815061492818 numberOfDaysInPeriod: 28.668907708332494 first timestamp 1727201259 last trade 1729678252.626 first sats 1648241n first tokens 116674414n last sats 1759576n last tokens 109340225n yield 0.02206714530974295
// compare numbers to our existing implementation
let start = PoolSnapshot::dummy(1727201259, 1648241, 116674414);
let end = PoolSnapshot::dummy(1729678252, 1759576, 109340225);
let expected_yield = dec!(0.02206714530974295);
let expected_apy = dec!(0.2815061492818);
let period = PoolPeriod::new(start, end).unwrap();
let (pool_yield, pool_apy) = period.yield_and_apy(&None).unwrap();
assert!((expected_yield - pool_yield).abs() < dec!(1e-12));
assert!(
(expected_apy - pool_apy).abs() < dec!(1e-7),
2025-07-16 09:31:14 +02:00
"expected {expected_apy} != actual {pool_apy}"
);
}
}