f468e30af0
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
New service at services/signal_engine/ implementing concurrent heuristic (deterministic scoring) and probabilistic (Bayesian inference) pipelines that evaluate technical signals across 6 timeframes (M30-M) and produce independent BUY/WATCH/SKIP verdicts per ticker per evaluation tick. Components: - Input Normalizer: multi-source data assembly with sentinel fallbacks - Signal Library: Fibonacci, MA Stack, RSI, Cup & Handle, Elliott Wave - Multi-Timeframe Confluence Engine: weighted scoring with D/W/M anchors - Hard Filter Engine: macro_bias, valuation, earnings proximity gating - Heuristic Pipeline: S_total scoring with confidence-gated verdicts - Probabilistic Pipeline: Bayesian log-odds with regime priors, entropy gating, EV_R calculation, and signal correlation penalty - Exit Engine: stop-loss, targets, trailing ATR-based stops - Delta Analyzer: pipeline agreement tracking with rolling Redis metrics - Output Formatter: SignalOutput contract + Recommendation schema mapping - Worker orchestrator: concurrent pipelines with failure isolation - Main entry point: queue polling with fail-safe config loading Infrastructure: - Migration 039: signal_engine_outputs table with 3 indexes - Helm chart: signalEngine service entry (processing tier) - Redis key: QUEUE_SIGNAL_ENGINE constant Tests: 390 tests (unit + property-based) covering all components Config: dual_pipeline_enabled=false by default (safe rollout)
145 lines
4.7 KiB
Python
145 lines
4.7 KiB
Python
# 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)}"
|
|
)
|