Skip to content

Commit

Permalink
cleanup and protect against abs max
Browse files Browse the repository at this point in the history
  • Loading branch information
dpaiton committed Oct 2, 2024
1 parent 58f14ae commit 9da937f
Showing 1 changed file with 154 additions and 108 deletions.
262 changes: 154 additions & 108 deletions crates/hyperdrive-math/src/short/max.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use ethers::types::{I256, U256};
use std::thread::current;

use ethers::{
providers::maybe,
types::{I256, U256},
};
use eyre::{eyre, Result};
use fixedpointmath::{fixed, fixed_u256, FixedPoint};
use fixedpointmath::{fixed, fixed_i256, fixed_u256, FixedPoint};

use crate::{calculate_effective_share_reserves, State, YieldSpace};

Expand Down Expand Up @@ -38,146 +43,132 @@ impl State {
.pow(self.time_stretch())
}

/// Use SGD with rate reduction to find the amount of bonds shorted for a
/// given base deposit amount.
/// Use Newton's method with rate reduction to find the amount of bonds
/// shorted for a given base deposit amount.
pub fn calculate_short_bonds_given_deposit(
&self,
target_base_amount: FixedPoint<U256>,
open_vault_share_price: FixedPoint<U256>,
absolute_max_bond_amount: FixedPoint<U256>,
maybe_tolerance: Option<FixedPoint<U256>>,
maybe_max_iterations: Option<usize>,
) -> Result<FixedPoint<U256>> {
let tolerance = maybe_tolerance.unwrap_or(fixed!(1e9));
let max_iterations = maybe_max_iterations.unwrap_or(10_000);
let max_iterations = maybe_max_iterations.unwrap_or(500);

// Start with a conservative estimate of the bonds shorted & base paid.
// By approximating the LP short principal as some relized price times
// the bond amount, we can calculate a lower bound using a conservative
// price.
let mut last_good_bond_amount = self.calculate_approximate_short_bonds_given_base_deposit(
target_base_amount,
open_vault_share_price,
)?;
let mut last_good_base_amount =
self.calculate_open_short(last_good_bond_amount, open_vault_share_price)?;
println!("target_base_amount = {:#?}", target_base_amount);
println!("last_good_base_amount = {:#?}", last_good_base_amount);
println!("last_good_bond_amount = {:#?}", last_good_bond_amount);
println!(
"last_good_base_amount / last_good_bond_amount = {:#?}",
last_good_base_amount / last_good_bond_amount
);
// Sanity check; our initial guess should be less than the target.
if last_good_base_amount > target_base_amount {
// The max bond amount might be below the pool's minimum.
// If so, no short can be opened.
if absolute_max_bond_amount < self.minimum_transaction_amount() {
return Err(eyre!("No solvent short is possible."));
}

// Figure out the base deposit for the absolute max bond amount.
let absolute_max_base_amount =
self.calculate_open_short(absolute_max_bond_amount, open_vault_share_price)?;
if target_base_amount > absolute_max_base_amount {
return Err(eyre!(
"Initial guess was too large.
initial_guess={} > target_base_amount={}",
last_good_base_amount,
target_base_amount
"Input too large.
target_base_amount={:#?}
max_base_amount ={:#?}",
target_base_amount,
absolute_max_base_amount
));
}
// Short circuit if we somehow hit it with the guess.
else if (target_base_amount - last_good_base_amount) <= tolerance {
return Ok(last_good_bond_amount);
// If the absolute max is within tolerance, return it.
if absolute_max_base_amount - target_base_amount <= tolerance {
return Ok(absolute_max_bond_amount);
}

// Compute a conservative estimate of the bonds shorted & base paid.
let mut last_good_bond_amount = self.calculate_approximate_short_bonds_given_base_deposit(
target_base_amount,
open_vault_share_price,
)?;

// Run Newton's Method to adjust the bond amount.
let mut loss = FixedPoint::from(U256::MAX);
// Run Stochastic Gradient Descent to adjust the bond amount.
for iter in 0..max_iterations {
println!("\n----\niter {:#?}", iter);

// Calculate the current deposit.
let current_base_amount =
self.calculate_open_short(last_good_bond_amount, open_vault_share_price)?;
println!("target_base_amount ={:#?}", target_base_amount);
println!("current_base_amount={:#?}", current_base_amount);
println!("target_base_amount={:#?}", target_base_amount);

// Calculate the current loss.
let old_loss = loss.clone();
// TODO: Loss should **always** go down, but it does not.
// Ignoring because fuzz tests are passing; but need to investigate.
loss = if current_base_amount < target_base_amount {
// It's possible that a nudge from failure cases in the previous
// iteration put us within the tolerance.
if (target_base_amount - current_base_amount) <= tolerance {
return Ok(last_good_bond_amount);
}
target_base_amount - current_base_amount
} else {
current_base_amount - target_base_amount
};
if loss > old_loss {
return Err(eyre!(
"Loss did not decrease.
new_loss={:#?}
old_loss={:#?}",
loss,
old_loss
));
}
println!("loss={:#?}", loss);

// Calculate the current gradient.
// Calculate the update amount.
let base_amount_derivative = self.calculate_open_short_derivative(
last_good_bond_amount,
open_vault_share_price,
Some(self.calculate_spot_price()?),
)?;
let dy = loss / base_amount_derivative;
println!("base_amount_derivative={:#?}", base_amount_derivative);
println!("dy={:#?}", dy);
let dy = loss.div_up(base_amount_derivative); // div up to discourage dy == 0

// Calculate the new bond amount.
// The update rule is: y_1 = y_0 - L(x, x_t) / dL(x, x_t)/dy,
// where y is the bond amount, x is the base deposit returned by
// calculate_open_short(y), x_t is the target deposit, and L is the
// loss (x_t - x). The derivative of L(x, x_t) wrt y is
// -base_amount_derivative, so we add here instead of subtracting a
// negative.

// calculate_open_short(y), x_t is the target deposit, L is the loss
// (x_t - x), and dL(x, x_t)/dy = -base_amount_derivative. We avoid
// negative numbers using a conditional.
let new_bond_amount = if current_base_amount < target_base_amount {
last_good_bond_amount + dy
} else {
last_good_bond_amount - dy
};
println!("last_good_bond_amount={:#?}", last_good_bond_amount);
println!("new_bond_amount ={:#?}", new_bond_amount);

// If we overshot, lower the learning rate and try again.
// Otherwise, check convergence to either return or continue.
// Check solvency with the latest bond amount.
match self.calculate_open_short(new_bond_amount, open_vault_share_price) {
Ok(new_base_amount) => {
println!("new_base_amount={:#?}", new_base_amount);
if new_base_amount <= target_base_amount {
last_good_base_amount = new_base_amount;
last_good_bond_amount = new_bond_amount;
// Check for convergence.
if (target_base_amount - last_good_base_amount) <= tolerance {
println!(
"
CONVERGED!
"
);
// Within tolerance, but bond amount must be >= 0.
return Ok(last_good_bond_amount.max(fixed!(0)));
}
println!("Undershot zero-crossing; iterating...");
// We overshot the zero crossing & amount was solvent.
} else {
println!("Overshot zero-crossing; iterating...");
last_good_base_amount = new_base_amount;
last_good_bond_amount = new_bond_amount;
}
// We overshot the zero crossing & the amount was solvent.
// It's possible that we are so close in base amounts that
// dy is zero, but we are still overshooting. In this case,
// nudge the bond amount down by the error and continue.
else {
let error = new_base_amount - target_base_amount;
last_good_bond_amount = new_bond_amount - error;
}
}
// New bond amount is not solvent.
// New bond amount is not solvent. Start again from slightly
// below the absolute max.
Err(_) => {
return Err(eyre!("New bond amount is insolvent!"));
// We know abs max is solvent and we know the target bond
// amount is less than the absolute max. So we overshot, but
// we can safely overshoot by less.
if new_bond_amount >= absolute_max_bond_amount {
last_good_bond_amount = absolute_max_bond_amount - tolerance;
} else {
return Err(eyre!(
"current_bond_amount={:#?} is less than the expected absolute_max={:#?}, but still not solvent.",
new_bond_amount,
absolute_max_bond_amount
));
}
}
}
}
// Did not hit tolerance in max iterations.
return Err(eyre!(
"Could not converge to a bond amount given max iterations = {}.
Target base deposit = {}.
Final base deposit = {}.
Final bond amount = {}.",
"Could not converge to a bond amount given max iterations = {:#?}.
Target base deposit = {:#?}
Error = {:#?}
Tolerance = {:#?}",
max_iterations,
target_base_amount,
last_good_base_amount,
last_good_bond_amount,
loss,
tolerance,
));
}

Expand Down Expand Up @@ -717,13 +708,11 @@ mod tests {

#[tokio::test]
async fn fuzz_calculate_short_bonds_given_deposit() -> Result<()> {
let test_tolerance = fixed!(1e15);
let max_iterations = 10_000;
let test_tolerance = fixed!(1e9);
let max_iterations = 500;
let mut rng = thread_rng();
for fuzz_iter in 0..*FAST_FUZZ_RUNS {
println!("fuzz_iter {:#?}", fuzz_iter);
for _ in 0..*FUZZ_RUNS {
let state = rng.gen::<State>();
let open_vault_share_price = rng.gen_range(fixed!(1e5)..=state.vault_share_price());
let checkpoint_exposure = {
let value = rng.gen_range(fixed!(0)..=FixedPoint::from(U256::from(U128::MAX)));
if rng.gen() {
Expand All @@ -732,22 +721,79 @@ mod tests {
I256::try_from(value)?
}
};
let max_short_trade = match get_max_short(state.clone(), checkpoint_exposure, None) {
Ok(max_short_trade) => max_short_trade,
Err(_) => continue,
};
let target_base_amount =
rng.gen_range(state.minimum_transaction_amount()..=max_short_trade);
let bond_amount = state.calculate_short_bonds_given_deposit(
target_base_amount,
open_vault_share_price,
Some(test_tolerance),
Some(max_iterations),
)?;
let computed_base_amount =
state.calculate_open_short(bond_amount, open_vault_share_price)?;
assert!(target_base_amount >= computed_base_amount);
assert!(target_base_amount - computed_base_amount <= test_tolerance);
// TODO: Absolute max doesn't always work. Sometimes the random
// state causes an overflow when calculating absolute max.
// Unlikely: fix the state generator so that the random state always has a valid max.
// Likely: fix absolute max short such that the output is guaranteed to be solvent.
match panic::catch_unwind(|| {
state.calculate_absolute_max_short(
state.calculate_spot_price()?,
checkpoint_exposure,
Some(max_iterations),
)
}) {
Ok(max_bond_no_panic) => {
match max_bond_no_panic {
Ok(absolute_max_bond_amount) => {
// Get random input parameters.
let open_vault_share_price =
rng.gen_range(fixed!(1e5)..=state.vault_share_price());
match panic::catch_unwind(|| {
state.calculate_open_short(
absolute_max_bond_amount,
open_vault_share_price,
)
}) {
Ok(result_no_panic) => match result_no_panic {
Ok(_) => (),
Err(_) => continue,
},
Err(_) => continue,
}
let max_short_bonds =
match get_max_short(state.clone(), checkpoint_exposure, None) {
Ok(max_short_trade) => {
max_short_trade.min(absolute_max_bond_amount)
}
Err(_) => continue,
};
let max_short_base = state
.calculate_open_short(max_short_bonds, open_vault_share_price)?;
let target_base_amount =
rng.gen_range(state.minimum_transaction_amount()..=max_short_base);
// Run the function to be tested.
let bond_amount = state.calculate_short_bonds_given_deposit(
target_base_amount,
open_vault_share_price,
absolute_max_bond_amount,
Some(test_tolerance),
Some(max_iterations),
)?;
// Verify outputs.
let computed_base_amount =
state.calculate_open_short(bond_amount, open_vault_share_price)?;
assert!(
target_base_amount >= computed_base_amount,
"target is less than computed base amount:
target_base_amount ={:#?}
computed_base_amount={:#?}",
target_base_amount,
computed_base_amount
);
assert!(
target_base_amount - computed_base_amount <= test_tolerance,
"target - computed base amounts are greater than tolerance:
error = {:#?}
tolerance = {:#?}",
target_base_amount - computed_base_amount,
test_tolerance
);
}
Err(_) => continue, // absolute max threw an error
}
}
Err(_) => continue, // absolute max threw a panic
}
}
Ok(())
}
Expand Down

0 comments on commit 9da937f

Please sign in to comment.