#[derive(Debug, Clone, PartialEq)]
pub enum AmmType {
UniswapV2,
StableSwap,
Concentrated,
}
#[derive(Debug, Clone)]
pub struct LiquidityPool {
pub token_a: String,
pub token_b: String,
pub reserve_a: f64,
pub reserve_b: f64,
pub fee_bps: u32,
pub amm_type: AmmType,
pub total_supply: f64,
}
impl LiquidityPool {
#[must_use]
pub fn new(
token_a: impl Into<String>,
token_b: impl Into<String>,
reserve_a: f64,
reserve_b: f64,
fee_bps: u32,
amm_type: AmmType,
) -> Self {
let total_supply = (reserve_a * reserve_b).sqrt().max(0.0);
Self {
token_a: token_a.into(),
token_b: token_b.into(),
reserve_a,
reserve_b,
fee_bps,
amm_type,
total_supply,
}
}
#[must_use]
pub fn spot_price(&self) -> f64 {
if self.reserve_a == 0.0 {
return 0.0;
}
self.reserve_b / self.reserve_a
}
#[must_use]
pub fn get_amount_out(&self, amount_in: f64, a_to_b: bool) -> f64 {
if amount_in <= 0.0 {
return 0.0;
}
let fee = amount_in * self.fee_bps as f64 / 10_000.0;
let amount_in_after_fee = amount_in - fee;
if a_to_b {
if self.reserve_a == 0.0 || self.reserve_b == 0.0 {
return 0.0;
}
let k = self.reserve_a * self.reserve_b;
let new_reserve_a = self.reserve_a + amount_in_after_fee;
let new_reserve_b = k / new_reserve_a;
(self.reserve_b - new_reserve_b).max(0.0)
} else {
if self.reserve_a == 0.0 || self.reserve_b == 0.0 {
return 0.0;
}
let k = self.reserve_a * self.reserve_b;
let new_reserve_b = self.reserve_b + amount_in_after_fee;
let new_reserve_a = k / new_reserve_b;
(self.reserve_a - new_reserve_a).max(0.0)
}
}
pub fn add_liquidity(&mut self, amount_a: f64, amount_b: f64) -> f64 {
if self.reserve_a == 0.0 || self.reserve_b == 0.0 || self.total_supply == 0.0 {
let lp = (amount_a * amount_b).sqrt();
self.reserve_a += amount_a;
self.reserve_b += amount_b;
self.total_supply += lp;
return lp;
}
let ratio_a = amount_a / self.reserve_a;
let ratio_b = amount_b / self.reserve_b;
let lp_tokens = ratio_a.min(ratio_b) * self.total_supply;
self.reserve_a += amount_a;
self.reserve_b += amount_b;
self.total_supply += lp_tokens;
lp_tokens
}
pub fn remove_liquidity(&mut self, lp_tokens: f64) -> (f64, f64) {
if self.total_supply == 0.0 || lp_tokens <= 0.0 {
return (0.0, 0.0);
}
let share = (lp_tokens / self.total_supply).min(1.0);
let amount_a = self.reserve_a * share;
let amount_b = self.reserve_b * share;
self.reserve_a -= amount_a;
self.reserve_b -= amount_b;
self.total_supply -= lp_tokens.min(self.total_supply);
(amount_a, amount_b)
}
}
#[must_use]
pub fn price_impact(amount_in: f64, a_to_b: bool, pool: &LiquidityPool) -> f64 {
let old_price = pool.spot_price();
if old_price == 0.0 {
return 0.0;
}
let fee = amount_in * pool.fee_bps as f64 / 10_000.0;
let amount_in_after_fee = amount_in - fee;
let new_price = if a_to_b {
let new_reserve_a = pool.reserve_a + amount_in_after_fee;
if new_reserve_a == 0.0 {
return 0.0;
}
pool.reserve_b / new_reserve_a
} else {
let new_reserve_b = pool.reserve_b + amount_in_after_fee;
if new_reserve_b == 0.0 {
return 0.0;
}
pool.reserve_b / pool.reserve_a };
let new_price = if !a_to_b {
let k = pool.reserve_a * pool.reserve_b;
let new_reserve_b = pool.reserve_b + amount_in_after_fee;
if new_reserve_b == 0.0 {
return 0.0;
}
let new_reserve_a = k / new_reserve_b;
if new_reserve_a == 0.0 {
return 0.0;
}
new_reserve_b / new_reserve_a
} else {
new_price
};
(new_price - old_price).abs() / old_price
}
#[must_use]
pub fn impermanent_loss(price_ratio_change: f64) -> f64 {
let r = price_ratio_change;
if r <= 0.0 {
return 0.0;
}
2.0 * r.sqrt() / (1.0 + r) - 1.0
}
#[derive(Debug, Clone)]
pub struct LpPosition {
pub pool_id: String,
pub lp_tokens: f64,
pub entry_price_ratio: f64,
pub entry_value_usd: f64,
}
#[must_use]
pub fn current_value(pos: &LpPosition, current_price_ratio: f64, token_a_price_usd: f64) -> f64 {
if pos.entry_price_ratio == 0.0 {
return pos.entry_value_usd;
}
let r = current_price_ratio / pos.entry_price_ratio;
if r <= 0.0 {
return 0.0;
}
let il = impermanent_loss(r); let hodl_value = pos.entry_value_usd * (current_price_ratio / pos.entry_price_ratio + 1.0) / 2.0
* (token_a_price_usd / (pos.entry_value_usd / 2.0).max(1e-12));
let _ = hodl_value;
pos.entry_value_usd * (1.0 + il) * (r).sqrt()
}
#[must_use]
pub fn il_pct(pos: &LpPosition, current_price_ratio: f64) -> f64 {
if pos.entry_price_ratio == 0.0 {
return 0.0;
}
let r = current_price_ratio / pos.entry_price_ratio;
impermanent_loss(r)
}
#[derive(Debug, Clone)]
pub struct ArbitrageOpportunity {
pub pool_a_price: f64,
pub pool_b_price: f64,
pub profit_pct: f64,
pub direction: bool,
}
#[must_use]
pub fn find_arbitrage(
pool_a: &LiquidityPool,
pool_b: &LiquidityPool,
) -> Option<ArbitrageOpportunity> {
let price_a = pool_a.spot_price();
let price_b = pool_b.spot_price();
if price_a == 0.0 || price_b == 0.0 {
return None;
}
let combined_fee = (pool_a.fee_bps + pool_b.fee_bps) as f64 / 10_000.0;
let min_price = price_a.min(price_b);
let price_diff_pct = (price_a - price_b).abs() / min_price;
if price_diff_pct > combined_fee {
let profit_pct = price_diff_pct - combined_fee;
let direction = price_a < price_b; Some(ArbitrageOpportunity {
pool_a_price: price_a,
pool_b_price: price_b,
profit_pct,
direction,
})
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_pool(ra: f64, rb: f64, fee_bps: u32) -> LiquidityPool {
LiquidityPool::new("ETH", "USDC", ra, rb, fee_bps, AmmType::UniswapV2)
}
#[test]
fn spot_price_basic() {
let pool = make_pool(100.0, 200_000.0, 30);
assert!((pool.spot_price() - 2000.0).abs() < 1e-9);
}
#[test]
fn spot_price_zero_reserve() {
let pool = make_pool(0.0, 100.0, 30);
assert_eq!(pool.spot_price(), 0.0);
}
#[test]
fn constant_product_formula() {
let pool = make_pool(100.0, 200_000.0, 30);
let out = pool.get_amount_out(1.0, true);
assert!(out > 0.0);
assert!(out < 2000.0, "output must be less than spot * amount_in");
let k_before = 100.0 * 200_000.0;
let fee = 1.0 * 30.0 / 10_000.0;
let amount_in_net = 1.0 - fee;
let new_ra = 100.0 + amount_in_net;
let new_rb = 200_000.0 - out;
assert!((new_ra * new_rb - k_before).abs() < 1e-3);
}
#[test]
fn get_amount_out_zero_input() {
let pool = make_pool(100.0, 200_000.0, 30);
assert_eq!(pool.get_amount_out(0.0, true), 0.0);
}
#[test]
fn price_impact_increases_with_size() {
let pool = make_pool(1000.0, 1_000_000.0, 30);
let small = price_impact(1.0, true, &pool);
let large = price_impact(100.0, true, &pool);
assert!(large > small, "larger trade should have more price impact");
}
#[test]
fn impermanent_loss_at_2x_price_change() {
let il = impermanent_loss(2.0);
assert!((il - (-0.05719_f64)).abs() < 1e-4, "IL at 2x ≈ -5.72%, got {il}");
assert!(il < 0.0, "IL must be negative");
}
#[test]
fn impermanent_loss_no_change() {
let il = impermanent_loss(1.0);
assert!(il.abs() < 1e-10);
}
#[test]
fn impermanent_loss_zero_ratio() {
assert_eq!(impermanent_loss(0.0), 0.0);
}
#[test]
fn add_and_remove_liquidity() {
let mut pool = make_pool(1000.0, 1_000_000.0, 30);
let initial_supply = pool.total_supply;
let lp = pool.add_liquidity(10.0, 10_000.0);
assert!(lp > 0.0);
assert!(pool.total_supply > initial_supply);
let (a, b) = pool.remove_liquidity(lp);
assert!(a > 0.0 && b > 0.0);
}
#[test]
fn arbitrage_detection_significant_spread() {
let pool_a = make_pool(100.0, 200_000.0, 30);
let pool_b = make_pool(100.0, 210_000.0, 30);
let arb = find_arbitrage(&pool_a, &pool_b);
assert!(arb.is_some());
let arb = arb.unwrap();
assert!(arb.profit_pct > 0.0);
assert!(arb.direction, "should buy in pool_a (cheaper)");
}
#[test]
fn arbitrage_detection_no_opportunity() {
let pool_a = make_pool(100.0, 200_000.0, 30);
let pool_b = make_pool(100.0, 200_060.0, 30); let arb = find_arbitrage(&pool_a, &pool_b);
assert!(arb.is_none());
}
}