myco-bonding-curve/tests/test_dca_executor.py

229 lines
8.6 KiB
Python

"""Tests for DCA execution engine."""
import numpy as np
import pytest
from src.composed.dca_executor import (
DCAParams, DCAOrder, DCAResult,
create_dca_order, compute_chunk_size, execute_chunk, simulate_dca,
)
from src.composed.myco_surface import MycoSystem, MycoSystemConfig
from src.primitives.twap_oracle import (
TWAPOracleParams, create_oracle, record_observation,
)
def _bootstrap_system(n_assets=3, amount=10000.0):
"""Helper: create and bootstrap a MycoSystem."""
config = MycoSystemConfig(n_reserve_assets=n_assets)
system = MycoSystem(config)
bootstrap = np.full(n_assets, amount / n_assets)
system.deposit(bootstrap, 0.0)
return system
class TestFixedDCA:
def test_equal_chunks(self):
"""Fixed strategy splits into equal chunks."""
params = DCAParams(total_amount=1000.0, n_chunks=10, interval=1.0)
order = create_dca_order(params, start_time=1.0)
oracle = create_oracle()
chunk = compute_chunk_size(order, oracle)
assert chunk == pytest.approx(100.0, abs=0.01)
def test_budget_exhaustion(self):
"""All chunks together should spend the full budget."""
system = _bootstrap_system()
params = DCAParams(total_amount=1000.0, n_chunks=5, interval=1.0)
order = create_dca_order(params, start_time=1.0)
oracle = create_oracle()
for i in range(5):
t = 1.0 + i * 1.0
system, order, oracle, tokens = execute_chunk(system, order, oracle, t)
assert order.is_complete
assert order.total_spent == pytest.approx(1000.0, abs=0.01)
assert order.chunks_executed == 5
def test_token_tracking(self):
"""Total tokens received should be positive and match history."""
system = _bootstrap_system()
params = DCAParams(total_amount=500.0, n_chunks=5, interval=1.0)
order = create_dca_order(params, start_time=1.0)
oracle = create_oracle()
total_from_chunks = 0.0
for i in range(5):
t = 1.0 + i * 1.0
system, order, oracle, tokens = execute_chunk(system, order, oracle, t)
total_from_chunks += tokens
assert order.total_tokens_received > 0
assert order.total_tokens_received == pytest.approx(total_from_chunks, rel=1e-10)
assert len(order.history) == 5
def test_avg_price_computed(self):
"""Average price should be total_spent / total_tokens."""
system = _bootstrap_system()
params = DCAParams(total_amount=1000.0, n_chunks=4, interval=1.0)
order = create_dca_order(params, start_time=1.0)
oracle = create_oracle()
for i in range(4):
system, order, oracle, _ = execute_chunk(
system, order, oracle, 1.0 + i,
)
expected_avg = order.total_spent / order.total_tokens_received
assert order.avg_price == pytest.approx(expected_avg, rel=1e-10)
def test_no_execution_after_complete(self):
"""Executing on a complete order returns 0 tokens."""
system = _bootstrap_system()
params = DCAParams(total_amount=100.0, n_chunks=1, interval=1.0)
order = create_dca_order(params, start_time=1.0)
oracle = create_oracle()
system, order, oracle, t1 = execute_chunk(system, order, oracle, 1.0)
assert order.is_complete
system, order, oracle, t2 = execute_chunk(system, order, oracle, 2.0)
assert t2 == 0.0
class TestTWAPAwareDCA:
def _make_oracle_with_history(self, base_price=1.0, n=10):
"""Create oracle with price history at base_price."""
oracle = create_oracle(TWAPOracleParams(default_window=100.0))
for t in range(n):
oracle = record_observation(oracle, base_price, float(t))
return oracle
def test_buys_more_when_cheap(self):
"""When spot < TWAP, chunk should be larger than base."""
# Oracle has TWAP ~ 10.0
oracle = self._make_oracle_with_history(base_price=10.0)
# Spot drops to 8.0
oracle = record_observation(oracle, 8.0, 11.0)
params = DCAParams(
total_amount=1000.0, n_chunks=10, interval=1.0,
strategy="twap_aware", max_deviation=0.3,
)
order = create_dca_order(params, start_time=12.0)
chunk = compute_chunk_size(order, oracle)
base = 1000.0 / 10
assert chunk > base
def test_buys_less_when_expensive(self):
"""When spot > TWAP, chunk should be smaller than base."""
oracle = self._make_oracle_with_history(base_price=10.0)
# Spot rises to 12.0
oracle = record_observation(oracle, 12.0, 11.0)
params = DCAParams(
total_amount=1000.0, n_chunks=10, interval=1.0,
strategy="twap_aware", max_deviation=0.3,
)
order = create_dca_order(params, start_time=12.0)
chunk = compute_chunk_size(order, oracle)
base = 1000.0 / 10
assert chunk < base
def test_equal_when_spot_equals_twap(self):
"""When spot == TWAP, chunk == base."""
oracle = self._make_oracle_with_history(base_price=10.0)
params = DCAParams(
total_amount=1000.0, n_chunks=10, interval=1.0,
strategy="twap_aware", max_deviation=0.3,
)
order = create_dca_order(params, start_time=11.0)
chunk = compute_chunk_size(order, oracle)
base = 1000.0 / 10
assert chunk == pytest.approx(base, rel=0.05)
def test_chunk_clamped_to_remaining(self):
"""Chunk size never exceeds remaining budget."""
oracle = self._make_oracle_with_history(base_price=10.0)
oracle = record_observation(oracle, 5.0, 11.0) # Very cheap
params = DCAParams(
total_amount=100.0, n_chunks=10, interval=1.0,
strategy="twap_aware", max_deviation=0.3,
)
order = create_dca_order(params, start_time=12.0)
order.total_spent = 95.0 # Only 5 remaining
chunk = compute_chunk_size(order, oracle)
assert chunk <= 5.0
class TestDCAvsLumpSum:
def test_simulation_runs(self):
"""Basic DCA simulation completes without error."""
config = MycoSystemConfig(n_reserve_assets=3)
params = DCAParams(
total_amount=5000.0, n_chunks=10, interval=1.0,
)
result = simulate_dca(config, params)
assert result.order.is_complete
assert result.order.total_tokens_received > 0
assert result.lump_sum_tokens > 0
def test_dca_advantage_computed(self):
"""DCA advantage is a finite number."""
config = MycoSystemConfig(n_reserve_assets=3)
params = DCAParams(
total_amount=5000.0, n_chunks=10, interval=1.0,
)
result = simulate_dca(config, params)
assert np.isfinite(result.dca_advantage)
def test_twap_aware_simulation(self):
"""TWAP-aware strategy also completes."""
config = MycoSystemConfig(n_reserve_assets=3)
params = DCAParams(
total_amount=5000.0, n_chunks=10, interval=1.0,
strategy="twap_aware", twap_window=50.0,
)
result = simulate_dca(config, params)
assert result.order.is_complete
assert result.order.total_tokens_received > 0
class TestEdgeCases:
def test_single_chunk_equals_lump_sum(self):
"""With n_chunks=1, DCA is equivalent to lump sum."""
config = MycoSystemConfig(n_reserve_assets=3)
params_dca = DCAParams(
total_amount=5000.0, n_chunks=1, interval=1.0,
)
result = simulate_dca(config, params_dca)
# With one chunk, DCA and lump sum should get similar tokens
# (not exact due to different bootstrap timing)
ratio = result.order.total_tokens_received / result.lump_sum_tokens
assert 0.95 < ratio < 1.05
def test_zero_bootstrap(self):
"""DCA on a fresh system (bootstrap via first chunk)."""
config = MycoSystemConfig(n_reserve_assets=3)
params = DCAParams(
total_amount=1000.0, n_chunks=5, interval=1.0,
)
# Small bootstrap so system exists
result = simulate_dca(config, params, bootstrap_amount=100.0)
assert result.order.is_complete
assert result.order.total_tokens_received > 0
def test_large_dca_relative_to_system(self):
"""DCA for an amount much larger than bootstrap."""
config = MycoSystemConfig(n_reserve_assets=3)
params = DCAParams(
total_amount=50000.0, n_chunks=20, interval=1.0,
)
result = simulate_dca(config, params, bootstrap_amount=1000.0)
assert result.order.is_complete
assert result.order.total_spent == pytest.approx(50000.0, abs=1.0)