Files
stonks-oracle/tests/test_pbt_v3_projection.py
T

108 lines
3.7 KiB
Python

"""Property-based tests for v3 posterior state projection.
Validates:
- Property 16: Projection evidence state decays toward zero
Requirements: 11.1, 11.3
"""
from __future__ import annotations
from hypothesis import given, settings
from hypothesis import strategies as st
from services.aggregation.projection import compute_v3_projection
from services.aggregation.regime import MarketRegime, V3RegimeClassification
# ---------------------------------------------------------------------------
# Strategies
# ---------------------------------------------------------------------------
# Initial evidence states (non-zero)
a_t_values = st.floats(
min_value=-10.0, max_value=10.0, allow_nan=False, allow_infinity=False
).filter(lambda x: abs(x) > 0.01)
# Horizons >= 1
horizons = st.integers(min_value=1, max_value=50)
# Regimes
regimes = st.sampled_from(["panic", "trend_following", "mean_reversion", "uncertainty"])
def _make_regime(regime_name: str) -> V3RegimeClassification:
"""Create a V3RegimeClassification with the given regime name."""
return V3RegimeClassification(
regime=MarketRegime(regime_name),
trend_z=0.0,
vol_ratio=1.0,
evidence_multiplier=1.0,
confidence_multiplier=1.0,
phi_decay={"panic": 0.35, "trend_following": 0.80, "mean_reversion": 0.55, "uncertainty": 0.50}[regime_name],
atr_multiplier=2.0,
)
# ---------------------------------------------------------------------------
# Feature: math-core-v3-engine, Property 16: Projection evidence state
# decays toward zero
# ---------------------------------------------------------------------------
# **Validates: Requirements 11.1, 11.3**
@settings(max_examples=100)
@given(
a_t=a_t_values,
regime_name=regimes,
h=horizons,
)
def test_property_16_projection_evidence_state_decays_toward_zero(
a_t: float,
regime_name: str,
h: int,
) -> None:
"""Property 16: Projection evidence state decays toward zero.
For any initial evidence state A_t and regime decay phi in (0, 1),
the projected state A_projected_h = phi^h * A_t SHALL have
|A_projected_h| < |A_t| for all h >= 1, converging toward 0 as h
increases.
"""
regime = _make_regime(regime_name)
# Call compute_v3_projection with a_prev=a_t, cluster_llrs=[] (no new evidence),
# known_catalyst_llr=0.0. This gives A_t = phi * a_prev (since no new LLRs).
result = compute_v3_projection(
a_prev=a_t,
cluster_llrs=[],
regime=regime,
p_prior=0.50,
projection_horizon=h,
known_catalyst_llr=0.0,
)
# After update: A_t_new = phi * a_prev + 0 = phi * a_prev
# After projection: A_projected = phi^h * A_t_new = phi^h * (phi * a_prev) = phi^(h+1) * a_prev
# The phi values are all in (0, 1), so phi^(h+1) < 1 for h >= 1
# Therefore |A_projected| < |a_prev|
phi = regime.phi_decay
# The evidence state after update (no new LLRs): A_t_new = phi * a_prev
a_t_new = result.a_t
# The projected alpha: A_projected = phi^h * A_t_new
a_projected = (phi ** h) * a_t_new
# |A_projected| must be less than |a_prev| because phi is in (0, 1)
# and A_projected = phi^(h+1) * a_prev
assert abs(a_projected) < abs(a_t), (
f"|A_projected|={abs(a_projected)} should be < |a_prev|={abs(a_t)} "
f"for phi={phi}, h={h}, regime={regime_name}"
)
# Verify convergence: larger h → smaller magnitude
# Compute projected at h+10 and verify it's smaller than at h
a_projected_larger_h = (phi ** (h + 10)) * a_t_new
assert abs(a_projected_larger_h) <= abs(a_projected), (
f"|A_projected(h+10)|={abs(a_projected_larger_h)} should be <= "
f"|A_projected(h)|={abs(a_projected)} for phi={phi}, regime={regime_name}"
)