312 lines
11 KiB
Python
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
|