2024-10-21 15:13:16 +02:00
// 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 " )
}
2024-10-21 15:13:16 +02:00
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} "
2024-10-21 15:13:16 +02:00
) ;
}
}