178 lines
6.2 KiB
Python
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
|