141 lines
4.6 KiB
Rust
141 lines
4.6 KiB
Rust
// 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())
|
|
}
|
|
|
|
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),
|
|
"expected {expected_apy} != actual {pool_apy}"
|
|
);
|
|
}
|
|
}
|