diff --git a/crates/hyperdrive-math/src/short/open.rs b/crates/hyperdrive-math/src/short/open.rs index 598856a5..03d3f27f 100644 --- a/crates/hyperdrive-math/src/short/open.rs +++ b/crates/hyperdrive-math/src/short/open.rs @@ -221,9 +221,13 @@ impl State { (fixed!(1e18) + variable_apy).pow(self.annualized_position_duration()) - fixed!(1e18); let base_proceeds = bond_amount * tpy; if base_proceeds > base_paid { - Ok(I256::try_from((base_proceeds - base_paid) / base_paid)?) + Ok(I256::try_from( + (base_proceeds - base_paid) / (base_paid * self.annualized_position_duration()), + )?) } else { - Ok(-I256::try_from((base_paid - base_proceeds) / base_paid)?) + Ok(-I256::try_from( + (base_paid - base_proceeds) / (base_paid * self.annualized_position_duration()), + )?) } } @@ -754,20 +758,32 @@ mod tests { // Bob closes his short. let base_proceeds = bob.close_short(maturity_time, bond_amount, None).await?; + let annualized_position_duration = + bob.get_state().await?.annualized_position_duration(); // Ensure that the implied rate matches the realized rate from // holding the short to maturity. let realized_rate = if base_proceeds > base_paid { - I256::try_from((base_proceeds - base_paid) / base_paid)? + I256::try_from( + (base_proceeds - base_paid) / (base_paid * annualized_position_duration), + )? } else { - -I256::try_from((base_paid - base_proceeds) / base_paid)? + -I256::try_from( + (base_paid - base_proceeds) / (base_paid * annualized_position_duration), + )? }; let error = (implied_rate - realized_rate).abs(); + let scaled_tolerance = if implied_rate > int256!(1e18) { + I256::from(tolerance * implied_rate) + } else { + tolerance + }; assert!( - error < tolerance, - "error {:?} exceeds tolerance of {}", + error < scaled_tolerance, + "error {:?} exceeds tolerance of {} (scaled to {})", error, - tolerance + tolerance, + scaled_tolerance ); // Revert to the snapshot and reset the agent's wallets. diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index e2e8b250..b6098338 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -132,6 +132,38 @@ pub fn calculate_rate_given_fixed_price( (fixed!(1e18) - price) / (price * fixed_price_duration_in_years) } +/// Calculate the holding period return (HPR) given a non-compounding, annualized rate (APR). +/// +/// Since the rate is non-compounding, we calculate the hpr as: +/// +/// $$ +/// hpr = apr * t +/// $$ +/// +/// where $t$ is the holding period, in units of years. For example, if the +/// holding period is 6 months, then $t=0.5$. +pub fn calculate_hpr_given_apr(apr: FixedPoint, position_duration: FixedPoint) -> FixedPoint { + let holding_period_in_years = + position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365)); + apr * holding_period_in_years +} + +/// Calculate the holding period return (HPR) given a compounding, annualized rate (APY). +/// +/// Since the rate is compounding, we calculate the hpr as: +/// +/// $$ +/// hpr = (1 + apy) ^ (t) - 1 +/// $$ +/// +/// where $t$ is the holding period, in units of years. For example, if the +/// holding period is 6 months, then $t=0.5$. +pub fn calculate_hpr_given_apy(apy: FixedPoint, position_duration: FixedPoint) -> FixedPoint { + let holding_period_in_years = + position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365)); + (fixed!(1e18) + apy).pow(holding_period_in_years) - fixed!(1e18) +} + #[cfg(test)] mod tests { use std::panic;