use alloy::primitives::U256;
use crate::constants::Q96;
use crate::errors::{PerpCityError, Result};
use super::tick::get_sqrt_ratio_at_tick;
pub fn estimate_liquidity(
tick_lower: i32,
tick_upper: i32,
usd_amount_scaled: u128,
) -> Result<U256> {
if tick_lower >= tick_upper {
return Err(PerpCityError::InvalidTickRange {
lower: tick_lower,
upper: tick_upper,
});
}
if usd_amount_scaled == 0 {
return Err(PerpCityError::InvalidMargin {
reason: "USD amount must be non-zero".into(),
});
}
let sqrt_lower = get_sqrt_ratio_at_tick(tick_lower)?;
let sqrt_upper = get_sqrt_ratio_at_tick(tick_upper)?;
let delta = sqrt_upper - sqrt_lower;
if delta.is_zero() {
return Err(PerpCityError::Overflow {
context: "sqrtPrice delta is zero".into(),
});
}
let numerator = U256::from(usd_amount_scaled) * Q96;
Ok(numerator / delta)
}
pub fn liquidity_for_target_ratio(
margin_scaled: u128,
tick_lower: i32,
tick_upper: i32,
current_sqrt_price_x96: U256,
target_margin_ratio: f64,
) -> Result<u128> {
if tick_lower >= tick_upper {
return Err(PerpCityError::InvalidTickRange {
lower: tick_lower,
upper: tick_upper,
});
}
if target_margin_ratio <= 0.0 || target_margin_ratio >= 1.0 {
return Err(PerpCityError::InvalidLeverage {
reason: format!("target_margin_ratio must be in (0, 1), got {target_margin_ratio}"),
});
}
if margin_scaled == 0 {
return Err(PerpCityError::InvalidMargin {
reason: "margin must be non-zero".into(),
});
}
let sqrt_lower_x96 = get_sqrt_ratio_at_tick(tick_lower)?;
let sqrt_upper_x96 = get_sqrt_ratio_at_tick(tick_upper)?;
let q96_f = crate::constants::Q96_U128 as f64;
let to_f64 = |v: U256| -> Result<f64> {
u128::try_from(v)
.map(|n| n as f64)
.map_err(|_| PerpCityError::Overflow {
context: "sqrtPriceX96 exceeds u128 range".into(),
})
};
let sqrt_lower_f = to_f64(sqrt_lower_x96)? / q96_f;
let sqrt_upper_f = to_f64(sqrt_upper_x96)? / q96_f;
let sqrt_current_f = to_f64(current_sqrt_price_x96)? / q96_f;
let quote_per_liq = if sqrt_current_f <= sqrt_lower_f {
sqrt_upper_f - sqrt_lower_f
} else if sqrt_current_f >= sqrt_upper_f {
0.0
} else {
sqrt_upper_f - sqrt_current_f
};
if quote_per_liq <= 0.0 {
return Err(PerpCityError::Overflow {
context: "quote_per_liq is zero (price above range)".into(),
});
}
let margin_f = margin_scaled as f64;
let liquidity_f = margin_f / (target_margin_ratio * quote_per_liq);
if !liquidity_f.is_finite() || liquidity_f <= 0.0 {
return Err(PerpCityError::Overflow {
context: format!("computed liquidity is not finite: {liquidity_f}"),
});
}
Ok(liquidity_f as u128)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn estimate_liquidity_basic() {
let liq = estimate_liquidity(-100, 100, 1_000_000).unwrap();
assert!(!liq.is_zero(), "liquidity should be positive");
}
#[test]
fn estimate_liquidity_wider_range_gives_less_liquidity() {
let narrow = estimate_liquidity(-100, 100, 1_000_000).unwrap();
let wide = estimate_liquidity(-1000, 1000, 1_000_000).unwrap();
assert!(
narrow > wide,
"narrower range should concentrate more liquidity: narrow={narrow}, wide={wide}"
);
}
#[test]
fn estimate_liquidity_more_usd_gives_more_liquidity() {
let small = estimate_liquidity(-100, 100, 1_000_000).unwrap();
let large = estimate_liquidity(-100, 100, 10_000_000).unwrap();
assert!(
large > small,
"more USD should give more liquidity: large={large}, small={small}"
);
}
#[test]
fn estimate_liquidity_proportional_to_usd() {
let base = estimate_liquidity(-1000, 1000, 1_000_000).unwrap();
let doubled = estimate_liquidity(-1000, 1000, 2_000_000).unwrap();
let diff = doubled.abs_diff(base * U256::from(2u64));
assert!(
diff <= U256::from(1u64),
"expected proportional within ±1, got diff={diff}"
);
}
#[test]
fn estimate_liquidity_rejects_equal_ticks() {
assert!(estimate_liquidity(100, 100, 1_000_000).is_err());
}
#[test]
fn estimate_liquidity_rejects_inverted_ticks() {
assert!(estimate_liquidity(200, 100, 1_000_000).is_err());
}
#[test]
fn estimate_liquidity_rejects_zero_amount() {
assert!(estimate_liquidity(-100, 100, 0).is_err());
}
#[test]
fn target_ratio_basic() {
let liq = liquidity_for_target_ratio(
1_000_000, -1000, 1000, Q96, 0.1, )
.unwrap();
assert!(liq > 0, "liquidity should be positive");
}
#[test]
fn target_ratio_higher_ratio_gives_less_liquidity() {
let low_ratio = liquidity_for_target_ratio(1_000_000, -1000, 1000, Q96, 0.05).unwrap();
let high_ratio = liquidity_for_target_ratio(1_000_000, -1000, 1000, Q96, 0.2).unwrap();
assert!(
low_ratio > high_ratio,
"lower ratio needs more liquidity: low={low_ratio}, high={high_ratio}"
);
}
#[test]
fn target_ratio_more_margin_gives_more_liquidity() {
let small = liquidity_for_target_ratio(1_000_000, -1000, 1000, Q96, 0.1).unwrap();
let large = liquidity_for_target_ratio(10_000_000, -1000, 1000, Q96, 0.1).unwrap();
assert!(
large > small,
"more margin should give more liquidity: large={large}, small={small}"
);
}
#[test]
fn target_ratio_rejects_invalid_tick_range() {
assert!(liquidity_for_target_ratio(1_000_000, 100, 100, Q96, 0.1).is_err());
}
#[test]
fn target_ratio_rejects_zero_ratio() {
assert!(liquidity_for_target_ratio(1_000_000, -100, 100, Q96, 0.0).is_err());
}
#[test]
fn target_ratio_rejects_ratio_at_one() {
assert!(liquidity_for_target_ratio(1_000_000, -100, 100, Q96, 1.0).is_err());
}
#[test]
fn target_ratio_rejects_negative_ratio() {
assert!(liquidity_for_target_ratio(1_000_000, -100, 100, Q96, -0.1).is_err());
}
#[test]
fn target_ratio_rejects_zero_margin() {
assert!(liquidity_for_target_ratio(0, -100, 100, Q96, 0.1).is_err());
}
#[test]
fn target_ratio_price_above_range() {
let sqrt_above = super::super::tick::get_sqrt_ratio_at_tick(2000).unwrap();
assert!(liquidity_for_target_ratio(1_000_000, -1000, -500, sqrt_above, 0.1).is_err());
}
}