"""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 V3ProjectionState, 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}" )