229 lines
8.6 KiB
Python
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)
|