108 lines
3.7 KiB
Python
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}"
|
|
)
|