myco-bonding-curve/tests/test_subscription_dca.py

312 lines
11 KiB
Python

"""Tests for subscription/donation DCA integration."""
import numpy as np
import pytest
from src.composed.subscription_dca import (
SubscriptionDCAConfig,
DonationDCAConfig,
SubscriptionDCAOrder,
DonationDCAOrder,
SubscriptionDCAState,
create_subscription_dca_order,
create_donation_dca_order,
execute_subscription_chunk,
execute_donation_chunk,
pending_chunks,
simulate_subscription_dca,
)
from src.composed.dca_executor import DCAOrder
from src.composed.myco_surface import MycoSystem, MycoSystemConfig
from src.commitments.subscription import (
Subscription,
SubscriptionTier,
create_default_tiers,
)
from src.primitives.twap_oracle import (
TWAPOracleParams,
TWAPOracleState,
create_oracle,
record_observation,
)
# --- Helpers ---
def _make_system() -> MycoSystem:
config = MycoSystemConfig(n_reserve_assets=3)
system = MycoSystem(config)
# Bootstrap
system.deposit(np.array([1000.0, 1000.0, 1000.0]), 0.0)
return system
def _make_oracle() -> TWAPOracleState:
oracle = create_oracle(TWAPOracleParams(default_window=24.0))
oracle = record_observation(oracle, 1.0, 0.0)
oracle = record_observation(oracle, 1.0, 1.0)
return oracle
def _make_subscription() -> tuple[Subscription, SubscriptionTier]:
tier = SubscriptionTier(
name="Sustainer",
payment_per_period=50.0,
period_length=30.0,
base_mint_rate=1.8,
loyalty_multiplier_max=1.5,
loyalty_halflife=120.0,
)
sub = Subscription(
subscriber="alice",
tier="sustainer",
start_time=0.0,
last_payment_time=0.0,
total_paid=0.0,
total_minted=0.0,
periods_paid=0,
)
return sub, tier
# --- TestSubscriptionDCAOrder ---
class TestSubscriptionDCAOrder:
def test_creates_correct_chunks(self):
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(n_chunks=5, spread_fraction=0.8)
order = create_subscription_dca_order(sub, tier, config, 10.0, 1.2, 0)
assert order.order.params.n_chunks == 5
assert abs(order.order.params.total_amount - 50.0) < 1e-10
def test_interval_respects_spread(self):
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(n_chunks=5, spread_fraction=0.8)
order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0)
expected_interval = (30.0 * 0.8) / 5
assert abs(order.order.params.interval - expected_interval) < 1e-10
def test_loyalty_multiplier_stored(self):
sub, tier = _make_subscription()
config = SubscriptionDCAConfig()
order = create_subscription_dca_order(sub, tier, config, 0.0, 1.35, 2)
assert abs(order.loyalty_multiplier - 1.35) < 1e-10
assert order.period_index == 2
def test_strategy_propagated(self):
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(strategy="twap_aware")
order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0)
assert order.order.params.strategy == "twap_aware"
# --- TestDonationDCAOrder ---
class TestDonationDCAOrder:
def test_dca_enabled_multi_chunk(self):
config = DonationDCAConfig(enable_dca=True, n_chunks=5, interval=2.0)
order = create_donation_dca_order("bob", 500.0, config, 0.0)
assert order.order.params.n_chunks == 5
assert abs(order.donation_amount - 500.0) < 1e-10
def test_dca_disabled_single_chunk(self):
config = DonationDCAConfig(enable_dca=False, n_chunks=5)
order = create_donation_dca_order("bob", 500.0, config, 0.0)
assert order.order.params.n_chunks == 1
def test_single_chunk_when_n_chunks_one(self):
config = DonationDCAConfig(enable_dca=True, n_chunks=1)
order = create_donation_dca_order("bob", 100.0, config, 0.0)
assert order.order.params.n_chunks == 1
assert order.order.params.interval == 0.0
def test_donor_stored(self):
config = DonationDCAConfig()
order = create_donation_dca_order("carol", 200.0, config, 5.0)
assert order.donor == "carol"
assert abs(order.order.start_time - 5.0) < 1e-10
# --- TestChunkExecution ---
class TestChunkExecution:
def test_subscription_chunk_mints_tokens(self):
system = _make_system()
oracle = _make_oracle()
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(n_chunks=3)
order = create_subscription_dca_order(sub, tier, config, 1.0, 1.0, 0)
state = SubscriptionDCAState(
subscription_orders={"alice": [order]},
)
state, system, oracle, tokens, bonus = execute_subscription_chunk(
state, "alice", system, oracle, 1.0,
)
assert tokens > 0
assert bonus == 0.0 # loyalty_multiplier == 1.0
def test_loyalty_bonus_computed(self):
system = _make_system()
oracle = _make_oracle()
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(n_chunks=3)
order = create_subscription_dca_order(sub, tier, config, 1.0, 1.5, 0)
state = SubscriptionDCAState(
subscription_orders={"alice": [order]},
)
state, system, oracle, tokens, bonus = execute_subscription_chunk(
state, "alice", system, oracle, 1.0,
)
assert tokens > 0
# bonus = tokens * (1.5 - 1.0) = tokens * 0.5
assert abs(bonus - tokens * 0.5) < 1e-10
def test_donation_chunk_mints_tokens(self):
system = _make_system()
oracle = _make_oracle()
config = DonationDCAConfig(enable_dca=True, n_chunks=3, interval=1.0)
don_order = create_donation_dca_order("bob", 300.0, config, 1.0)
state = SubscriptionDCAState(donation_orders=[don_order])
state, system, oracle, tokens = execute_donation_chunk(
state, 0, system, oracle, 1.0,
)
assert tokens > 0
def test_complete_order_returns_zero(self):
system = _make_system()
oracle = _make_oracle()
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(n_chunks=1)
order = create_subscription_dca_order(sub, tier, config, 1.0, 1.0, 0)
state = SubscriptionDCAState(
subscription_orders={"alice": [order]},
)
# Execute the single chunk
state, system, oracle, t1, b1 = execute_subscription_chunk(
state, "alice", system, oracle, 1.0,
)
assert t1 > 0
# Second execution should return zero
state, system, oracle, t2, b2 = execute_subscription_chunk(
state, "alice", system, oracle, 2.0,
)
assert t2 == 0.0
assert b2 == 0.0
def test_nonexistent_subscriber_returns_zero(self):
system = _make_system()
oracle = _make_oracle()
state = SubscriptionDCAState()
state, system, oracle, tokens, bonus = execute_subscription_chunk(
state, "nobody", system, oracle, 1.0,
)
assert tokens == 0.0
def test_out_of_range_donation_index_returns_zero(self):
system = _make_system()
oracle = _make_oracle()
state = SubscriptionDCAState()
state, system, oracle, tokens = execute_donation_chunk(
state, 99, system, oracle, 1.0,
)
assert tokens == 0.0
# --- TestPendingChunks ---
class TestPendingChunks:
def test_lists_due_chunks(self):
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(n_chunks=5, spread_fraction=1.0)
order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0)
state = SubscriptionDCAState(
subscription_orders={"alice": [order]},
)
# interval = 30.0 / 5 = 6.0, so at t=7 chunk 0 and 1 are due
result = pending_chunks(state, "alice", 7.0)
assert len(result) >= 1
def test_no_pending_when_complete(self):
sub, tier = _make_subscription()
config = SubscriptionDCAConfig(n_chunks=1)
order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0)
order.order.is_complete = True
state = SubscriptionDCAState(
subscription_orders={"alice": [order]},
)
result = pending_chunks(state, "alice", 100.0)
assert len(result) == 0
def test_empty_for_unknown_subscriber(self):
state = SubscriptionDCAState()
result = pending_chunks(state, "nobody", 0.0)
assert result == []
# --- TestBackwardCompat ---
class TestBackwardCompat:
"""Ensure the new modules don't break existing DCA or subscription imports."""
def test_dca_executor_still_works(self):
from src.composed.dca_executor import DCAParams, simulate_dca
config = MycoSystemConfig(n_reserve_assets=3)
params = DCAParams(
total_amount=100.0, n_chunks=5,
interval=1.0, asset_index=0,
)
result = simulate_dca(config, params)
assert result.order.total_tokens_received > 0
def test_subscription_module_still_works(self):
from src.commitments.subscription import (
SubscriptionSystem, create_subscription,
process_payment, create_default_tiers,
)
tiers = create_default_tiers()
system = SubscriptionSystem(tiers=tiers)
system, sub = create_subscription(system, "alice", "supporter", 0.0)
system, tokens = process_payment(system, "alice", 31.0)
assert tokens > 0
def test_myco_system_signal_routing(self):
"""Test the apply_signal_routing integration on MycoSystem."""
from src.primitives.signal_router import SignalRouterConfig
config = MycoSystemConfig(
n_reserve_assets=3,
twap_oracle_params=TWAPOracleParams(),
)
system = MycoSystem(config)
system.deposit(np.array([1000.0, 1000.0, 1000.0]), 0.0)
# Do a few deposits to build oracle history
for i in range(1, 6):
system.deposit(np.array([10.0, 10.0, 10.0]), float(i))
router_config = SignalRouterConfig()
adapted = system.apply_signal_routing(router_config)
assert adapted.flow_threshold > 0
assert adapted.pamm_alpha_bar > 0
def test_simulation_runs(self):
tiers = create_default_tiers()
config = SubscriptionDCAConfig(n_chunks=3)
sys_config = MycoSystemConfig(n_reserve_assets=3)
subscribers = [("alice", "supporter", 0.0)]
result = simulate_subscription_dca(
tiers, config, sys_config, subscribers, duration=60.0, dt=5.0,
)
assert "times" in result
assert len(result["times"]) == 12