wp-solana-amm-math 0.1.1

Protocol-agnostic AMM math for Solana DEX — tick pricing, bin pricing, liquidity math, swap simulation
Documentation
use rust_decimal::Decimal;

/// Convert a Q64.64 sqrt_price to a human-readable price decimal.
///
/// The on-chain representation stores `sqrt(price) * 2^64` as a u128.
/// To recover the actual price:
///
///   price = (sqrt_price_x64 / 2^64)^2
///
/// The decimal adjustment accounts for differing token decimals:
///
///   adjusted_price = price * 10^(decimals_a - decimals_b)
///
/// This ensures the returned value represents how many units of token B
/// one unit of token A is worth, in human-readable terms.
pub fn sqrt_price_x64_to_price(sqrt_price_x64: u128, decimals_a: u8, decimals_b: u8) -> Decimal {
    // Convert sqrt_price_x64 to f64 ratio first, then square.
    // This matches the approach used by orca_whirlpools_core.
    let sqrt_ratio = sqrt_price_x64 as f64 / (1u128 << 64) as f64;
    let raw_price = sqrt_ratio * sqrt_ratio;

    // Apply decimal adjustment: multiply by 10^(decimals_a - decimals_b)
    let decimal_diff = decimals_a as i32 - decimals_b as i32;
    let adjustment = 10f64.powi(decimal_diff);
    let adjusted_price = raw_price * adjustment;

    // Convert to Decimal. We use from_f64_retain to avoid precision loss
    // from the Display round-trip that Decimal::try_from(f64) uses.
    // Fallback: if the f64 is too large or NaN, return zero.
    Decimal::try_from(adjusted_price).unwrap_or(Decimal::ZERO)
}

/// Convert a Q64.64 sqrt_price to an f64 price.
///
/// This is a convenience function matching the `orca_whirlpools_core`
/// `sqrt_price_to_price` signature exactly, for callers that only need
/// f64 precision.
pub fn sqrt_price_to_price(sqrt_price_x64: u128, decimals_a: u8, decimals_b: u8) -> f64 {
    let sqrt_ratio = sqrt_price_x64 as f64 / (1u128 << 64) as f64;
    let raw_price = sqrt_ratio * sqrt_ratio;
    let decimal_diff = decimals_a as i32 - decimals_b as i32;
    let adjustment = 10f64.powi(decimal_diff);
    raw_price * adjustment
}

/// Invert a human-readable price.
///
/// If `price` represents "X units of B per 1 A", the result represents
/// "1/X units of A per 1 B". Returns `0.0` when the input is zero.
pub fn invert_price(price: f64) -> f64 {
    if price == 0.0 {
        return 0.0;
    }
    1.0 / price
}

#[cfg(test)]
mod tests {
    use super::*;

    const Q64: u128 = 1u128 << 64;

    #[test]
    fn test_tick_zero_price_same_decimals() {
        // At tick 0, sqrt_price_x64 = 2^64, so price = 1.0
        let price = sqrt_price_x64_to_price(Q64, 6, 6);
        let diff = (price - Decimal::ONE).abs();
        assert!(
            diff < Decimal::new(1, 6), // tolerance 0.000001
            "tick 0 same decimals: expected ~1.0, got {}",
            price
        );
    }

    #[test]
    fn test_tick_zero_price_different_decimals() {
        // sqrt_price = 2^64 => raw price = 1.0
        // With decimals_a=9, decimals_b=6 => adjustment = 10^3 = 1000
        // Final price = 1000.0
        let price = sqrt_price_x64_to_price(Q64, 9, 6);
        let expected = Decimal::from(1000);
        let diff = (price - expected).abs();
        assert!(diff < Decimal::ONE, "tick 0 (9,6): expected ~1000, got {}", price);
    }

    #[test]
    fn test_tick_positive_100() {
        // tick=100 => sqrt_price = 2^64 * 1.0001^50
        // price = 1.0001^100 ~ 1.01005
        use crate::tick_math::tick_to_sqrt_price_x64;
        let sqrt = tick_to_sqrt_price_x64(100).unwrap();
        let price = sqrt_price_x64_to_price(sqrt, 6, 6);
        let expected = Decimal::new(101005, 5); // 1.01005
        let diff = (price - expected).abs();
        assert!(
            diff < Decimal::new(1, 3), // tolerance 0.001
            "tick +100: expected ~1.01005, got {}",
            price
        );
    }

    #[test]
    fn test_tick_negative_100() {
        // tick=-100 => price = 1.0001^(-100) ~ 0.99005
        use crate::tick_math::tick_to_sqrt_price_x64;
        let sqrt = tick_to_sqrt_price_x64(-100).unwrap();
        let price = sqrt_price_x64_to_price(sqrt, 6, 6);
        let expected = Decimal::new(99005, 5); // 0.99005
        let diff = (price - expected).abs();
        assert!(diff < Decimal::new(1, 3), "tick -100: expected ~0.99005, got {}", price);
    }

    #[test]
    fn test_f64_variant_matches_decimal() {
        use crate::tick_math::tick_to_sqrt_price_x64;
        let sqrt = tick_to_sqrt_price_x64(10000).unwrap();
        let price_f64 = sqrt_price_to_price(sqrt, 9, 6);
        let price_dec = sqrt_price_x64_to_price(sqrt, 9, 6);
        let diff = (price_dec - Decimal::try_from(price_f64).unwrap()).abs();
        assert!(
            diff < Decimal::ONE,
            "f64 and Decimal variants diverge: f64={}, dec={}",
            price_f64,
            price_dec
        );
    }

    #[test]
    fn test_sol_usdc_realistic_price() {
        // SOL/USDC at ~$100: sqrt_price for price=100 with (9,6) decimals
        // raw_price = 100 / 10^(9-6) = 0.1
        // sqrt(0.1) * 2^64 ~ 5832617287695007744
        let sqrt_price_x64: u128 = 5_832_617_287_695_007_744;
        let price = sqrt_price_x64_to_price(sqrt_price_x64, 9, 6);
        let expected = Decimal::from(100);
        let tolerance = Decimal::from(5);
        assert!((price - expected).abs() < tolerance, "SOL/USDC: expected ~100, got {}", price);
    }

    #[test]
    fn test_price_symmetry() {
        // price(tick) * price(-tick) ~ 1.0 (for same decimals)
        use crate::tick_math::tick_to_sqrt_price_x64;
        for tick in [100, 1000, 10000] {
            let sqrt_pos = tick_to_sqrt_price_x64(tick).unwrap();
            let sqrt_neg = tick_to_sqrt_price_x64(-tick).unwrap();
            let price_pos = sqrt_price_to_price(sqrt_pos, 6, 6);
            let price_neg = sqrt_price_to_price(sqrt_neg, 6, 6);
            let product = price_pos * price_neg;
            assert!(
                (product - 1.0).abs() < 0.001,
                "symmetry broken for tick {}: product={}",
                tick,
                product
            );
        }
    }

    #[test]
    fn test_invert_price_zero() {
        assert_eq!(invert_price(0.0), 0.0);
    }

    #[test]
    fn test_invert_price_one() {
        assert!((invert_price(1.0) - 1.0).abs() < 1e-15);
    }

    #[test]
    fn test_invert_price_typical() {
        let price = 150.0; // SOL/USDC
        let inverted = invert_price(price);
        assert!((inverted - 1.0 / 150.0).abs() < 1e-15);
        // Double inversion returns the original
        assert!((invert_price(inverted) - price).abs() < 1e-10);
    }

    #[test]
    fn test_invert_price_small() {
        let price = 0.00001;
        let inverted = invert_price(price);
        assert!((inverted - 100_000.0).abs() < 1e-5);
    }
}