myco-bonding-curve/tests/test_n_dimensional.py

178 lines
6.2 KiB
Python

"""Tests for N-dimensional ellipsoidal bonding surface."""
import numpy as np
import pytest
from src.primitives.n_dimensional_surface import (
NDSurfaceParams,
NDSurfaceState,
create_params,
compute_invariant,
calc_out_given_in,
calc_in_given_out,
mint,
redeem,
spot_prices,
)
from src.utils.linear_algebra import random_rotation_matrix
class TestInvariant:
def test_2d_positive(self):
"""2D surface should have positive invariant."""
params = create_params(2)
balances = np.array([1000.0, 1000.0])
r = compute_invariant(balances, params)
assert r > 0
def test_3d_positive(self):
"""3D surface should have positive invariant."""
params = create_params(3)
balances = np.array([1000.0, 1000.0, 1000.0])
r = compute_invariant(balances, params)
assert r > 0
def test_5d_positive(self):
"""5D surface should work."""
params = create_params(5)
balances = np.array([100.0, 200.0, 300.0, 400.0, 500.0])
r = compute_invariant(balances, params)
assert r > 0
def test_invariant_increases_with_reserves(self):
"""More reserves → larger invariant."""
params = create_params(3)
r1 = compute_invariant(np.array([100.0, 100.0, 100.0]), params)
r2 = compute_invariant(np.array([200.0, 200.0, 200.0]), params)
assert r2 > r1
def test_stretched_surface(self):
"""Surface with stretch factors should compute invariant."""
params = create_params(3, lambdas=np.array([1.0, 5.0, 10.0]))
balances = np.array([1000.0, 1000.0, 1000.0])
r = compute_invariant(balances, params)
assert r > 0
class TestSwaps:
def test_swap_preserves_invariant(self):
"""Swap should preserve invariant."""
params = create_params(3)
balances = np.array([1000.0, 1000.0, 1000.0])
r_before = compute_invariant(balances, params)
dy = calc_out_given_in(balances, params, 0, 1, 50.0)
new_balances = balances.copy()
new_balances[0] += 50.0
new_balances[1] -= dy
r_after = compute_invariant(new_balances, params)
assert abs(r_after - r_before) / r_before < 1e-4
def test_round_trip(self):
"""calc_in_given_out inverts calc_out_given_in."""
params = create_params(3)
balances = np.array([1000.0, 1000.0, 1000.0])
amount_in = 30.0
amount_out = calc_out_given_in(balances, params, 0, 2, amount_in)
recovered = calc_in_given_out(balances, params, 0, 2, amount_out)
assert abs(recovered - amount_in) < 0.1 # Slightly looser tolerance for N-D
def test_swap_positive_output(self):
"""Swap should produce positive output."""
params = create_params(4)
balances = np.array([1000.0, 1000.0, 1000.0, 1000.0])
dy = calc_out_given_in(balances, params, 0, 3, 100.0)
assert dy > 0
def test_all_pairs(self):
"""Should be able to swap between any pair."""
params = create_params(3)
balances = np.array([1000.0, 1000.0, 1000.0])
for i in range(3):
for j in range(3):
if i != j:
dy = calc_out_given_in(balances, params, i, j, 10.0)
assert dy > 0
class TestMintRedeem:
def test_mint_increases_supply(self):
"""Minting should increase supply."""
params = create_params(3)
state = NDSurfaceState(
balances=np.array([1000.0, 1000.0, 1000.0]),
invariant=compute_invariant(np.array([1000.0, 1000.0, 1000.0]), params),
supply=1000.0,
)
amounts_in = np.array([100.0, 100.0, 100.0])
new_state, minted = mint(state, params, amounts_in)
assert minted > 0
assert new_state.supply > state.supply
assert np.all(new_state.balances > state.balances)
def test_redeem_proportional(self):
"""Proportional redeem should return proportional assets."""
params = create_params(3)
balances = np.array([1000.0, 2000.0, 3000.0])
state = NDSurfaceState(
balances=balances,
invariant=compute_invariant(balances, params),
supply=1000.0,
)
new_state, amounts_out = redeem(state, params, 100.0)
# Should get 10% of each asset
assert abs(amounts_out[0] - 100.0) < 1e-6
assert abs(amounts_out[1] - 200.0) < 1e-6
assert abs(amounts_out[2] - 300.0) < 1e-6
assert abs(new_state.supply - 900.0) < 1e-6
def test_initial_mint(self):
"""First mint with zero supply should bootstrap."""
params = create_params(2)
state = NDSurfaceState(
balances=np.array([0.0, 0.0]),
invariant=0.0,
supply=0.0,
)
# Bootstrap with initial reserves
amounts_in = np.array([1000.0, 1000.0])
# Need to handle zero invariant case
state.balances = amounts_in
state.invariant = compute_invariant(amounts_in, params)
state.supply = state.invariant
assert state.supply > 0
def test_mint_then_redeem_roundtrip(self):
"""Mint then full redeem should return ~original reserves."""
params = create_params(2)
initial_balances = np.array([1000.0, 1000.0])
state = NDSurfaceState(
balances=initial_balances.copy(),
invariant=compute_invariant(initial_balances, params),
supply=1000.0,
)
# Mint 10%
new_state, minted = mint(state, params, np.array([100.0, 100.0]))
# Redeem all minted
final_state, out = redeem(new_state, params, minted)
# Should be close to original + deposit - withdrawal ≈ original
np.testing.assert_allclose(final_state.balances, initial_balances, rtol=1e-4)
class TestSpotPrices:
def test_symmetric_prices(self):
"""Balanced pool with identity Q should have ~equal prices."""
params = create_params(3)
balances = np.array([1000.0, 1000.0, 1000.0])
prices = spot_prices(balances, params, numeraire=0)
assert abs(prices[0] - 1.0) < 1e-10
assert abs(prices[1] - 1.0) < 0.1
assert abs(prices[2] - 1.0) < 0.1