# Feature: dual-pipeline-signal-engine, Property: SignalOutput round-trip serialization """Property-based tests for SignalOutput round-trip serialization. Feature: dual-pipeline-signal-engine Tests the SignalOutput round-trip serialization property from the design specification: for any valid SignalOutput instance, serializing to JSON via model_dump_json() and deserializing back via model_validate_json() SHALL produce a SignalOutput object equivalent to the original. """ from __future__ import annotations from datetime import datetime, timezone from hypothesis import given, settings from hypothesis import strategies as st from services.signal_engine.models import ( ExitSignal, ExitType, SignalOutput, TradePlan, ) # --------------------------------------------------------------------------- # Property: SignalOutput Round-Trip Serialization # Validates: Requirements 10.5, 17.6 # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Hypothesis strategies # --------------------------------------------------------------------------- _finite_float = st.floats(allow_nan=False, allow_infinity=False) _non_negative_finite_float = st.floats( min_value=0.0, allow_nan=False, allow_infinity=False, ) _unit_float = st.floats( min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False, ) _aware_datetime_strategy = st.datetimes( min_value=datetime(2020, 1, 1), max_value=datetime(2030, 12, 31), timezones=st.just(timezone.utc), ) _ticker_strategy = st.text( alphabet=st.characters(whitelist_categories=("Lu",)), min_size=1, max_size=5, ) _verdict_strategy = st.sampled_from(["BUY", "WATCH", "SKIP"]) _pipeline_mode_strategy = st.sampled_from(["dual_pipeline", "heuristic_only", "probabilistic_only"]) # --- TradePlan strategy --- _trade_plan_strategy = st.builds( TradePlan, entry_price=_finite_float, stop_loss=_finite_float, target_1=_finite_float, target_2=_finite_float, position_size_pct=_unit_float, max_loss_pct=_unit_float, dual_confirmed=st.booleans(), probabilistic_only=st.booleans(), ) # --- ExitSignal strategy --- _exit_signal_strategy = st.builds( ExitSignal, position_id=st.uuids().map(str), ticker=_ticker_strategy, exit_type=st.sampled_from(list(ExitType)), reason=st.sampled_from(["stop_hit", "target_1_hit", "target_2_hit", "trailing_stop_hit"]), price=_finite_float, ) # --- Simple dict strategies for detail payloads --- _simple_detail_strategy = st.fixed_dictionaries( {}, optional={ "score": _finite_float, "label": st.text(max_size=20), "count": st.integers(min_value=0, max_value=1000), }, ) # --- SignalOutput strategy --- _signal_output_strategy = st.builds( SignalOutput, output_id=st.uuids().map(str), ticker=_ticker_strategy, timestamp=_aware_datetime_strategy, price=_finite_float, heuristic_verdict=_verdict_strategy, heuristic_confidence=_unit_float, heuristic_s_total=_finite_float, probabilistic_verdict=_verdict_strategy, probabilistic_p_up=_unit_float, probabilistic_entropy=_unit_float, probabilistic_ev_r=_finite_float, delta_agreement=st.booleans(), delta_confidence_delta=_non_negative_finite_float, delta_reasons=st.lists(st.text(min_size=1, max_size=50), min_size=0, max_size=5), trade_plan=st.one_of(st.none(), _trade_plan_strategy), exit_signals=st.lists(_exit_signal_strategy, min_size=0, max_size=3), heuristic_detail=_simple_detail_strategy, probabilistic_detail=_simple_detail_strategy, pipeline_mode=_pipeline_mode_strategy, shadow_mode=st.booleans(), ) # --------------------------------------------------------------------------- # Property test # --------------------------------------------------------------------------- @given(output=_signal_output_strategy) @settings(max_examples=100) def test_signal_output_round_trip_serialization(output: SignalOutput) -> None: """**Validates: Requirements 10.5, 17.6** For any valid SignalOutput instance, serializing to JSON and then deserializing back SHALL produce a SignalOutput object equivalent to the original. """ json_str = output.model_dump_json() restored = SignalOutput.model_validate_json(json_str) assert restored == output, ( f"Round-trip failed: deserialized SignalOutput differs from original.\n" f" ticker: {output.ticker}\n" f" heuristic_verdict: {output.heuristic_verdict}\n" f" probabilistic_verdict: {output.probabilistic_verdict}\n" f" trade_plan present: {output.trade_plan is not None}\n" f" exit_signals count: {len(output.exit_signals)}" )