Skip to content

Commit

Permalink
integrate new forward & derivative functions
Browse files Browse the repository at this point in the history
  • Loading branch information
dpaiton committed Apr 20, 2024
1 parent b39bd6e commit 2236101
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 102 deletions.
53 changes: 27 additions & 26 deletions crates/hyperdrive-math/src/short/max.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use ethers::types::I256;
use eyre::{eyre, Result};
use fixed_point::FixedPoint;
use fixed_point_macros::fixed;

Expand Down Expand Up @@ -55,14 +56,14 @@ impl State {
checkpoint_exposure: I,
maybe_conservative_price: Option<FixedPoint>, // TODO: Is there a nice way of abstracting the inner type?
maybe_max_iterations: Option<usize>,
) -> FixedPoint {
) -> Result<FixedPoint> {
let budget = budget.into();
let open_vault_share_price = open_vault_share_price.into();
let checkpoint_exposure = checkpoint_exposure.into();

// If the budget is zero, then we return early.
if budget == fixed!(0) {
return fixed!(0);
return Ok(fixed!(0));
}

// Calculate the spot price and the open share price. If the open share price
Expand All @@ -84,10 +85,10 @@ impl State {
let absolute_max_deposit =
match self.calculate_open_short(max_bond_amount, open_vault_share_price) {
Ok(d) => d,
Err(_) => return max_bond_amount,
Err(_) => return Ok(max_bond_amount),
};
if absolute_max_deposit <= budget {
return max_bond_amount;
return Ok(max_bond_amount);
}

// Use Newton's method to iteratively approach a solution. We use the
Expand Down Expand Up @@ -143,8 +144,13 @@ impl State {
}

// Iteratively update max_bond_amount via newton's method.
let derivative =
self.short_deposit_derivative(max_bond_amount, spot_price, open_vault_share_price);
let derivative = self.short_deposit_derivative(
max_bond_amount,
spot_price,
open_vault_share_price,
self.vault_share_price().max(open_vault_share_price),
self.vault_share_price(),
)?;
if deposit < target_budget {
max_bond_amount += (target_budget - deposit) / derivative;
} else if deposit > target_budget {
Expand All @@ -160,20 +166,18 @@ impl State {
}

// Verify that the max short satisfies the budget.
if budget
< self
.calculate_open_short(best_valid_max_bond_amount, open_vault_share_price)
.unwrap()
{
panic!("max short exceeded budget");
if budget < self.calculate_open_short(best_valid_max_bond_amount, open_vault_share_price)? {
return Err(eyre!("max short exceeded budget"));
}

// Ensure that the max bond amount is within the absolute max bond amount.
if best_valid_max_bond_amount > absolute_max_bond_amount {
panic!("max short bond amount exceeded absolute max bond amount");
return Err(eyre!(
"max short bond amount exceeded absolute max bond amount"
));
}

best_valid_max_bond_amount
Ok(best_valid_max_bond_amount)
}

/// Calculates an initial guess for the max short calculation.
Expand Down Expand Up @@ -490,7 +494,6 @@ mod tests {
use std::panic;

use ethers::types::U256;
use eyre::Result;
use fixed_point_macros::uint256;
use hyperdrive_wrappers::wrappers::{
ihyperdrive::Checkpoint, mock_hyperdrive_math::MaxTradeParams,
Expand Down Expand Up @@ -528,15 +531,13 @@ mod tests {
};
let max_iterations = 7;
let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
let actual = panic::catch_unwind(|| {
state.calculate_max_short(
U256::MAX,
open_vault_share_price,
checkpoint_exposure,
None,
Some(max_iterations),
)
});
let actual = state.calculate_max_short(
U256::MAX,
open_vault_share_price,
checkpoint_exposure,
None,
Some(max_iterations),
);
match chain
.mock_hyperdrive_math()
.calculate_max_short(
Expand Down Expand Up @@ -628,7 +629,7 @@ mod tests {
checkpoint_exposure,
None,
None,
);
)?;
// It's known that global max short is in units of bonds,
// but we fund bob with this amount regardless, since the amount required
// for deposit << the global max short number of bonds.
Expand Down Expand Up @@ -701,7 +702,7 @@ mod tests {
checkpoint_exposure,
None,
None,
);
)?;

// Bob opens a max short position. We allow for a very small amount
// of slippage to account for interest accrual between the time the
Expand Down
178 changes: 104 additions & 74 deletions crates/hyperdrive-math/src/short/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,65 @@ impl State {
Ok(base_deposit)
}

/// Calculates the derivative of the short deposit function with respect to the
/// short amount. This allows us to use Newton's method to approximate the
/// maximum short that a trader can open.
///
/// Using this, calculating $D'(\Delta y)$ is straightforward:
///
/// $$
/// D'(\Delta y) = c \cdot (
/// P^{\prime}_{\text{short}}(\Delta y)
/// - P^{\prime}_{\text{lp}}(\Delta y)
/// + \Phi^{\prime}_{c}(\Delta y)
/// )
/// $$
pub fn short_deposit_derivative(
&self,
bond_amount: FixedPoint,
spot_price: FixedPoint,
open_vault_share_price: FixedPoint,
close_vault_share_price: FixedPoint,
vault_share_price: FixedPoint,
) -> Result<FixedPoint> {
let flat_fee = self.close_short_flat_fee(
bond_amount,
self.position_duration().into(),
self.position_duration().into(),
);

// Short circuit the derivative if the forward function returns 0.
if self.calculate_short_proceeds_up(
bond_amount,
self.calculate_short_principal(bond_amount)?
- self.open_short_curve_fee(bond_amount, spot_price),
open_vault_share_price,
close_vault_share_price,
vault_share_price,
flat_fee,
) == fixed!(0)
{
return Ok(fixed!(0));
}

// Flat fee derivative = (1 - t) * phi_f / c
// Since t=0 when closing at maturity we can use phi_f / c
let flat_fee_derivative = self.flat_fee() / vault_share_price;
let curve_fee_derivative = self.curve_fee() * (fixed!(1e18) - spot_price);
let short_principal_derivative = self.calculate_short_principal_derivative(bond_amount);
let short_proceeds_derivative = self.calculate_short_proceeds_derivative(
bond_amount,
open_vault_share_price,
close_vault_share_price,
vault_share_price,
flat_fee,
flat_fee_derivative,
);

Ok(vault_share_price
* (short_proceeds_derivative - short_principal_derivative + curve_fee_derivative))
}

/// Calculates the proceeds in shares of closing a short position. This
/// takes into account the trading profits, the interest that was
/// earned by the short, the flat fee the short pays, and the amount of
Expand Down Expand Up @@ -148,50 +207,26 @@ impl State {
}
}

/// A helper function used in calculating the short deposit.
///
/// This calculates the inner component of the `short_principal` calculation,
/// which makes the `short_principal` and `short_deposit_derivative` calculations
/// easier. $\theta(x)$ is defined as:
///
/// $$
/// \theta(x) = \tfrac{\mu}{c} \cdot (k - (y + x)^{1 - t_s})
/// $$
fn theta(&self, bond_amount: FixedPoint) -> FixedPoint {
(self.initial_vault_share_price() / self.vault_share_price())
* (self.k_down()
- (self.bond_reserves() + bond_amount).pow(fixed!(1e18) - self.time_stretch()))
}

/// Calculates the derivative of the short deposit function with respect to the
/// short amount. This allows us to use Newton's method to approximate the
/// maximum short that a trader can open.
///
/// Using this, calculating $D'(x)$ is straightforward:
///
/// $$
/// D'(x) = \tfrac{c}{c_0} - (c \cdot P'(x) - \phi_{curve} \cdot (1 - p)) + \phi_{flat}
/// $$
/// Returns the derivative of the short proceeds calculation, assuming that the interest is
/// less negative than the trading profits and margin released.
///
/// $$
/// P'(x) = \tfrac{1}{c} \cdot (y + x)^{-t_s} \cdot \left(\tfrac{\mu}{c} \cdot (k - (y + x)^{1 - t_s}) \right)^{\tfrac{t_s}{1 - t_s}}
/// P^{\prime}_{\text{short}}(\Delta y) = \tfrac{c_{1}}{c_{0} \cdot c}
/// + \tfrac{\Phi_{f}(\Delta y)}{c}
/// + \tfrac{\Delta y \cdot \Phi^{\prime}_{f}(\Delta y)}{c}
/// $$
fn short_deposit_derivative(
pub fn calculate_short_proceeds_derivative(
&self,
bond_amount: FixedPoint,
spot_price: FixedPoint,
open_vault_share_price: FixedPoint,
close_vault_share_price: FixedPoint,
vault_share_price: FixedPoint,
flat_fee: FixedPoint,
flat_fee_derivative: FixedPoint,
) -> FixedPoint {
// NOTE: The order of additions and subtractions is important to avoid underflows.
let payment_factor = (fixed!(1e18)
/ (self.bond_reserves() + bond_amount).pow(self.time_stretch()))
* self
.theta(bond_amount)
.pow(self.time_stretch() / (fixed!(1e18) - self.time_stretch()));
(self.vault_share_price() / open_vault_share_price)
+ self.flat_fee()
+ self.curve_fee() * (fixed!(1e18) - spot_price)
- payment_factor
close_vault_share_price / (open_vault_share_price * vault_share_price)
+ flat_fee / vault_share_price
+ bond_amount * flat_fee_derivative / vault_share_price
}

/// Calculates the amount of short principal that the LPs need to pay to back a
Expand All @@ -203,21 +238,24 @@ impl State {
/// $$
/// k = \tfrac{c}{\mu} \cdot (\mu \cdot (z - P(\Delta y)))^{1 - t_s} + (y + \Delta y)^{1 - t_s} \\
/// \implies \\
/// P(\Delta y) = z - \tfrac{1}{\mu} \cdot (\tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s}))^{\tfrac{1}{1 - t_s}}
/// P_{\text{lp}}(\Delta y) = z - \tfrac{1}{\mu} \cdot (
/// \tfrac{\mu}{c}
/// \cdot (k - (y + \Delta y)^{1 - t_s})
/// )^{\tfrac{1}{1 - t_s}}
/// $$
pub fn calculate_short_principal(&self, bond_amount: FixedPoint) -> Result<FixedPoint> {
self.calculate_shares_out_given_bonds_in_down_safe(bond_amount)
}

/// Calculates the derivative of the short principal $P(\Delta y)$ w.r.t. the amount of
/// bonds that are shorted $\Delta y$.
/// Calculates the derivative of the short principal $P_{\text{lp}}(\Delta y)$
/// w.r.t. the amount of bonds that are shorted $\Delta y$.
///
/// The derivative is calculated as:
///
/// $$
/// P'(\Delta y) = \tfrac{1}{c} \cdot (y + \Delta y)^{-t_s} \cdot \left(
/// \tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s})
/// \right)^{\tfrac{t_s}{1 - t_s}}
/// P^{\prime}_{\text{lp}}(\Delta y) = \tfrac{1}{c} \cdot (y + \Delta y)^{-t_s} \cdot \left(
/// \tfrac{\mu}{c} \cdot (k - (y + \Delta y)^{1 - t_s})
/// \right)^{\tfrac{t_s}{1 - t_s}}
/// $$
pub fn calculate_short_principal_derivative(&self, bond_amount: FixedPoint) -> FixedPoint {
let lhs = fixed!(1e18)
Expand All @@ -234,7 +272,7 @@ impl State {
lhs * rhs
}

/// Calculates the spot price after opening a Hyperdrive short.
/// Calculates the spot price after opening a short.
pub fn calculate_spot_price_after_short(
&self,
bond_amount: FixedPoint,
Expand Down Expand Up @@ -298,6 +336,7 @@ mod tests {
chain::{ChainClient, TestChain},
constants::{FAST_FUZZ_RUNS, FUZZ_RUNS},
};
use tracing_test::traced_test;

use super::*;

Expand Down Expand Up @@ -464,37 +503,28 @@ mod tests {
let state = rng.gen::<State>();
let amount = rng.gen_range(fixed!(10e18)..=fixed!(10_000_000e18));

let p1_result = std::panic::catch_unwind(|| {
state.calculate_open_short(
amount - empirical_derivative_epsilon,
state.vault_share_price(),
)
});
let p1_result = state.calculate_open_short(
amount - empirical_derivative_epsilon,
state.vault_share_price(),
);
let p1;
let p2;
match p1_result {
// If the amount results in the pool being insolvent, skip this iteration
Ok(p) => match p {
Ok(p) => p1 = p,
Err(_) => continue,
},
Ok(p) => p1 = p,
Err(_) => continue,
}

let p2_result = std::panic::catch_unwind(|| {
state.calculate_open_short(
amount + empirical_derivative_epsilon,
state.vault_share_price(),
)
});
let p2_result = state.calculate_open_short(
amount + empirical_derivative_epsilon,
state.vault_share_price(),
);
match p2_result {
// If the amount results in the pool being insolvent, skip this iteration
Ok(p) => match p {
Ok(p) => p2 = p,
Err(_) => continue,
},
Ok(p) => p2 = p,
Err(_) => continue,
}

// Sanity check
assert!(p2 > p1);

Expand All @@ -503,7 +533,9 @@ mod tests {
amount,
state.calculate_spot_price(),
state.vault_share_price(),
);
state.vault_share_price(),
state.vault_share_price(),
)?;

let derivative_diff;
if short_deposit_derivative >= empirical_derivative {
Expand Down Expand Up @@ -617,15 +649,13 @@ mod tests {
};
let max_iterations = 7;
let open_vault_share_price = rng.gen_range(fixed!(0)..=state.vault_share_price());
let max_trade = panic::catch_unwind(|| {
state.calculate_max_short(
U256::MAX,
open_vault_share_price,
checkpoint_exposure,
None,
Some(max_iterations),
)
});
let max_trade = state.calculate_max_short(
U256::MAX,
open_vault_share_price,
checkpoint_exposure,
None,
Some(max_iterations),
);
// Since we're fuzzing it's possible that the max can fail.
// We're only going to use it in this test if it succeeded.
match max_trade {
Expand Down
2 changes: 1 addition & 1 deletion crates/hyperdrive-math/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ pub async fn test_integration_calculate_max_short() -> Result<()> {
checkpoint_exposure,
None,
None,
);
)?;
let budget = bob.base();
let slippage_tolerance = fixed!(0.001e18);
let max_short = bob.calculate_max_short(Some(slippage_tolerance)).await?;
Expand Down
Loading

0 comments on commit 2236101

Please sign in to comment.