myco-bonding-curve/tests/test_weighted_product.py

115 lines
4.2 KiB
Python

"""Tests for weighted constant product invariant."""
import numpy as np
import pytest
from src.primitives.weighted_product import (
compute_invariant,
spot_price,
calc_out_given_in,
calc_in_given_out,
calc_bpt_out_given_exact_tokens_in,
calc_token_out_given_exact_bpt_in,
)
class TestInvariant:
def test_two_token_equal_weight(self):
"""50/50 pool: I = sqrt(x * y)."""
balances = np.array([100.0, 100.0])
weights = np.array([0.5, 0.5])
inv = compute_invariant(balances, weights)
assert abs(inv - 100.0) < 1e-10
def test_two_token_unequal_weight(self):
"""80/20 pool."""
balances = np.array([100.0, 100.0])
weights = np.array([0.8, 0.2])
inv = compute_invariant(balances, weights)
expected = 100.0**0.8 * 100.0**0.2
assert abs(inv - expected) < 1e-10
def test_three_tokens(self):
"""33/33/34 pool."""
balances = np.array([100.0, 200.0, 300.0])
weights = np.array([0.33, 0.33, 0.34])
inv = compute_invariant(balances, weights)
expected = 100.0**0.33 * 200.0**0.33 * 300.0**0.34
assert abs(inv - expected) < 1e-8
def test_homogeneity_degree_1(self):
"""I(k*b) = k * I(b) for all k > 0."""
balances = np.array([50.0, 150.0, 75.0])
weights = np.array([0.25, 0.5, 0.25])
inv_base = compute_invariant(balances, weights)
for k in [0.5, 2.0, 10.0]:
inv_scaled = compute_invariant(k * balances, weights)
assert abs(inv_scaled - k * inv_base) < 1e-8 * k * inv_base
class TestSwaps:
def test_invariant_preserved_after_swap(self):
"""Swap should preserve the invariant."""
balances = np.array([1000.0, 1000.0])
weights = np.array([0.5, 0.5])
inv_before = compute_invariant(balances, weights)
amount_in = 100.0
amount_out = calc_out_given_in(1000.0, 0.5, 1000.0, 0.5, amount_in)
new_balances = np.array([1000.0 + amount_in, 1000.0 - amount_out])
inv_after = compute_invariant(new_balances, weights)
assert abs(inv_after - inv_before) < 1e-8
def test_round_trip(self):
"""calc_in_given_out should invert calc_out_given_in."""
amount_in = 50.0
amount_out = calc_out_given_in(1000.0, 0.6, 500.0, 0.4, amount_in)
recovered_in = calc_in_given_out(1000.0, 0.6, 500.0, 0.4, amount_out)
assert abs(recovered_in - amount_in) < 1e-8
def test_small_swap_matches_spot_price(self):
"""Very small swap should trade at approximately the spot price."""
bi, wi, bo, wo = 1000.0, 0.5, 1000.0, 0.5
sp = spot_price(bi, wi, bo, wo)
dx = 0.001
dy = calc_out_given_in(bi, wi, bo, wo, dx)
assert abs(dy / dx - sp) < 1e-4
def test_unequal_weight_swap(self):
"""80/20 swap prices should reflect weight asymmetry."""
# In an 80/20 pool, token 0 (80%) is more price-stable
sp = spot_price(1000.0, 0.8, 1000.0, 0.2)
# price = (1000/0.2) / (1000/0.8) = 4.0
assert abs(sp - 4.0) < 1e-10
class TestLiquidity:
def test_proportional_deposit(self):
"""Proportional deposit should mint BPT proportional to invariant increase."""
balances = np.array([1000.0, 1000.0])
weights = np.array([0.5, 0.5])
bpt_supply = 1000.0
# 10% proportional deposit
amounts_in = np.array([100.0, 100.0])
bpt_out = calc_bpt_out_given_exact_tokens_in(
balances, weights, amounts_in, bpt_supply
)
assert abs(bpt_out - 100.0) < 1e-8 # 10% increase
def test_single_sided_exit(self):
"""Single-token exit: burn BPT, receive one token."""
balances = np.array([1000.0, 1000.0])
weights = np.array([0.5, 0.5])
bpt_supply = 1000.0
# Burn 10% of supply, exit via token 0
token_out = calc_token_out_given_exact_bpt_in(
balances, weights, 0, 100.0, bpt_supply
)
# Should get less than 100 tokens due to single-sided penalty
assert 0 < token_out < 200.0
# For 50/50 pool: new_balance = 1000 * 0.9^(1/0.5) = 1000 * 0.81 = 810
# token_out = 1000 - 810 = 190
assert abs(token_out - 190.0) < 1e-8