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)
575 lines
20 KiB
Python
575 lines
20 KiB
Python
"""Unit tests for services.signal_engine.formatter — Output Formatter.
|
|
|
|
Tests trade plan generation for dual_confirmed, probabilistic_only,
|
|
heuristic-only, and no-BUY cases. Also tests the
|
|
``signal_output_to_recommendation`` mapping to the existing
|
|
``Recommendation`` schema.
|
|
|
|
Requirements: 10.2, 10.3, 10.4, 12.3, 12.4
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from services.shared.schemas import ActionType, RecommendationMode
|
|
from services.signal_engine.config import SignalEngineConfig
|
|
from services.signal_engine.formatter import (
|
|
format_output,
|
|
signal_output_to_recommendation,
|
|
)
|
|
from services.signal_engine.models import (
|
|
DeltaResult,
|
|
ExitSignal,
|
|
ExitType,
|
|
HeuristicResult,
|
|
ProbabilisticResult,
|
|
Verdict,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _default_config() -> SignalEngineConfig:
|
|
return SignalEngineConfig()
|
|
|
|
|
|
def _heuristic(
|
|
verdict: Verdict = Verdict.BUY,
|
|
confidence: float = 0.85,
|
|
s_total: float = 1.5,
|
|
) -> HeuristicResult:
|
|
return HeuristicResult(
|
|
verdict=verdict,
|
|
confidence=confidence,
|
|
s_total=s_total,
|
|
s_company=1.0,
|
|
s_macro=0.3,
|
|
s_competitive=0.2,
|
|
signal_weights=[],
|
|
reasoning=[f"{verdict.value} verdict"],
|
|
)
|
|
|
|
|
|
def _probabilistic(
|
|
verdict: Verdict = Verdict.BUY,
|
|
p_up: float = 0.75,
|
|
entropy: float = 0.6,
|
|
ev_r: float = 2.0,
|
|
) -> ProbabilisticResult:
|
|
return ProbabilisticResult(
|
|
verdict=verdict,
|
|
p_up=p_up,
|
|
entropy=entropy,
|
|
ev_r=ev_r,
|
|
prior=0.58,
|
|
posterior=0.75,
|
|
likelihood_ratios=[],
|
|
regime="bull",
|
|
reasoning=[f"{verdict.value} verdict"],
|
|
)
|
|
|
|
|
|
def _delta(
|
|
agreement: bool = True,
|
|
confidence_delta: float = 0.1,
|
|
reasons: list[str] | None = None,
|
|
) -> DeltaResult:
|
|
return DeltaResult(
|
|
agreement=agreement,
|
|
confidence_delta=confidence_delta,
|
|
heuristic_verdict="BUY",
|
|
probabilistic_verdict="BUY",
|
|
disagreement_reasons=reasons or [],
|
|
rolling_agreement_rate=0.85,
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# 1. Dual confirmed trade plan (Requirement 10.4)
|
|
# ===========================================================================
|
|
|
|
|
|
class TestDualConfirmed:
|
|
"""Both pipelines BUY → dual_confirmed, full position sizing."""
|
|
|
|
def test_dual_confirmed_trade_plan(self) -> None:
|
|
"""Both BUY → trade_plan with dual_confirmed=True."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is not None
|
|
assert output.trade_plan.dual_confirmed is True
|
|
assert output.trade_plan.probabilistic_only is False
|
|
|
|
def test_dual_confirmed_full_position_sizing(self) -> None:
|
|
"""Dual confirmed → position_size_pct=0.02, max_loss_pct=0.005."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is not None
|
|
assert output.trade_plan.position_size_pct == 0.02
|
|
assert output.trade_plan.max_loss_pct == 0.005
|
|
|
|
def test_dual_confirmed_price_levels(self) -> None:
|
|
"""Trade plan price levels: stop=95%, target_1=105%, target_2=110%."""
|
|
price = 200.0
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=price,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
tp = output.trade_plan
|
|
assert tp is not None
|
|
assert tp.entry_price == price
|
|
assert abs(tp.stop_loss - price * 0.95) < 1e-6
|
|
assert abs(tp.target_1 - price * 1.05) < 1e-6
|
|
assert abs(tp.target_2 - price * 1.10) < 1e-6
|
|
|
|
|
|
# ===========================================================================
|
|
# 2. Probabilistic-only trade plan (Requirement 10.3)
|
|
# ===========================================================================
|
|
|
|
|
|
class TestProbabilisticOnly:
|
|
"""Probabilistic BUY, heuristic not BUY → probabilistic_only, 50% sizing."""
|
|
|
|
def test_probabilistic_only_trade_plan(self) -> None:
|
|
"""Probabilistic BUY + heuristic WATCH → probabilistic_only flag."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is not None
|
|
assert output.trade_plan.probabilistic_only is True
|
|
assert output.trade_plan.dual_confirmed is False
|
|
|
|
def test_probabilistic_only_reduced_sizing(self) -> None:
|
|
"""Probabilistic-only → position_size_pct=0.01 (50% of standard)."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.SKIP, confidence=0.40),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is not None
|
|
assert output.trade_plan.position_size_pct == 0.01
|
|
assert output.trade_plan.max_loss_pct == 0.005
|
|
|
|
def test_probabilistic_only_price_levels(self) -> None:
|
|
"""Price levels are the same regardless of confirmation mode."""
|
|
price = 100.0
|
|
output = format_output(
|
|
ticker="MSFT",
|
|
price=price,
|
|
heuristic=_heuristic(Verdict.WATCH),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
tp = output.trade_plan
|
|
assert tp is not None
|
|
assert tp.entry_price == price
|
|
assert abs(tp.stop_loss - price * 0.95) < 1e-6
|
|
assert abs(tp.target_1 - price * 1.05) < 1e-6
|
|
assert abs(tp.target_2 - price * 1.10) < 1e-6
|
|
|
|
|
|
# ===========================================================================
|
|
# 3. Heuristic-only trade plan (Requirement 10.2)
|
|
# ===========================================================================
|
|
|
|
|
|
class TestHeuristicOnly:
|
|
"""Heuristic BUY, probabilistic not BUY → standard position sizing."""
|
|
|
|
def test_heuristic_only_trade_plan(self) -> None:
|
|
"""Heuristic BUY + probabilistic WATCH → standard trade plan."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is not None
|
|
assert output.trade_plan.dual_confirmed is False
|
|
assert output.trade_plan.probabilistic_only is False
|
|
|
|
def test_heuristic_only_full_sizing(self) -> None:
|
|
"""Heuristic-only → position_size_pct=0.02 (full standard)."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is not None
|
|
assert output.trade_plan.position_size_pct == 0.02
|
|
assert output.trade_plan.max_loss_pct == 0.005
|
|
|
|
|
|
# ===========================================================================
|
|
# 4. No BUY — no trade plan (Requirement 10.4 inverse)
|
|
# ===========================================================================
|
|
|
|
|
|
class TestNoBuy:
|
|
"""Neither pipeline BUY → no trade_plan."""
|
|
|
|
def test_both_watch_no_trade_plan(self) -> None:
|
|
"""Both WATCH → no trade_plan."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
|
|
probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is None
|
|
|
|
def test_both_skip_no_trade_plan(self) -> None:
|
|
"""Both SKIP → no trade_plan."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.SKIP, confidence=0.30),
|
|
probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is None
|
|
|
|
def test_watch_and_skip_no_trade_plan(self) -> None:
|
|
"""WATCH + SKIP → no trade_plan."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
|
|
probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.trade_plan is None
|
|
|
|
def test_no_buy_still_has_pipeline_data(self) -> None:
|
|
"""Even without trade_plan, pipeline data is populated for analysis."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
|
|
probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.heuristic_verdict == "WATCH"
|
|
assert output.probabilistic_verdict == "WATCH"
|
|
assert output.heuristic_confidence == 0.60
|
|
assert output.probabilistic_p_up == 0.57
|
|
assert output.delta_agreement is True
|
|
|
|
|
|
# ===========================================================================
|
|
# 5. signal_output_to_recommendation mapping (Requirements 12.3, 12.4)
|
|
# ===========================================================================
|
|
|
|
|
|
class TestSignalOutputToRecommendation:
|
|
"""Map SignalOutput to existing Recommendation schema."""
|
|
|
|
def test_dual_confirmed_confidence(self) -> None:
|
|
"""Dual confirmed → confidence = max(heuristic, probabilistic)."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY, confidence=0.85),
|
|
probabilistic=_probabilistic(Verdict.BUY, p_up=0.75),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.confidence == max(0.85, 0.75)
|
|
assert rec.confidence == 0.85
|
|
|
|
def test_probabilistic_only_confidence_haircut(self) -> None:
|
|
"""Probabilistic only → confidence = P_up * 0.8 (20% haircut)."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
|
|
probabilistic=_probabilistic(Verdict.BUY, p_up=0.75),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert abs(rec.confidence - 0.75 * 0.8) < 1e-9
|
|
assert abs(rec.confidence - 0.6) < 1e-9
|
|
|
|
def test_heuristic_only_confidence(self) -> None:
|
|
"""Heuristic only → confidence = heuristic_confidence."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY, confidence=0.80),
|
|
probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.confidence == 0.80
|
|
|
|
def test_buy_action_mapping(self) -> None:
|
|
"""Any BUY verdict → ActionType.BUY."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
|
|
delta=_delta(agreement=False),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.action == ActionType.BUY
|
|
|
|
def test_watch_action_mapping(self) -> None:
|
|
"""Both WATCH → ActionType.WATCH."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
|
|
probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.action == ActionType.WATCH
|
|
|
|
def test_skip_action_mapping(self) -> None:
|
|
"""Both SKIP → ActionType.HOLD."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.SKIP, confidence=0.30),
|
|
probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.action == ActionType.HOLD
|
|
|
|
def test_recommendation_mode_paper_eligible(self) -> None:
|
|
"""All recommendations use PAPER_ELIGIBLE mode."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.mode == RecommendationMode.PAPER_ELIGIBLE
|
|
|
|
def test_recommendation_position_sizing_from_trade_plan(self) -> None:
|
|
"""Position sizing in recommendation matches trade plan."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.position_sizing.portfolio_pct == 0.02
|
|
assert rec.position_sizing.max_loss_pct == 0.005
|
|
|
|
def test_recommendation_probabilistic_fields(self) -> None:
|
|
"""Recommendation includes probabilistic pipeline fields."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY, p_up=0.75, ev_r=2.0),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.p_bull == 0.75
|
|
assert rec.expected_value == 2.0
|
|
assert rec.pipeline_mode == "dual_pipeline"
|
|
|
|
def test_recommendation_ticker_and_id(self) -> None:
|
|
"""Recommendation inherits ticker and output_id."""
|
|
output = format_output(
|
|
ticker="MSFT",
|
|
price=300.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
rec = signal_output_to_recommendation(output)
|
|
|
|
assert rec.ticker == "MSFT"
|
|
assert rec.recommendation_id == output.output_id
|
|
|
|
|
|
# ===========================================================================
|
|
# 6. SignalOutput structure and metadata
|
|
# ===========================================================================
|
|
|
|
|
|
class TestSignalOutputStructure:
|
|
"""Verify SignalOutput has all required fields populated."""
|
|
|
|
def test_output_has_all_pipeline_data(self) -> None:
|
|
"""SignalOutput contains heuristic, probabilistic, and delta sections."""
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY, confidence=0.85, s_total=1.5),
|
|
probabilistic=_probabilistic(Verdict.BUY, p_up=0.75, entropy=0.6, ev_r=2.0),
|
|
delta=_delta(agreement=True, confidence_delta=0.1),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.ticker == "AAPL"
|
|
assert output.price == 150.0
|
|
assert output.heuristic_verdict == "BUY"
|
|
assert output.heuristic_confidence == 0.85
|
|
assert output.heuristic_s_total == 1.5
|
|
assert output.probabilistic_verdict == "BUY"
|
|
assert output.probabilistic_p_up == 0.75
|
|
assert output.probabilistic_entropy == 0.6
|
|
assert output.probabilistic_ev_r == 2.0
|
|
assert output.delta_agreement is True
|
|
assert output.delta_confidence_delta == 0.1
|
|
assert output.pipeline_mode == "dual_pipeline"
|
|
|
|
def test_output_includes_exit_signals(self) -> None:
|
|
"""Exit signals are passed through to the output."""
|
|
exits = [
|
|
ExitSignal(
|
|
position_id="pos-1",
|
|
ticker="AAPL",
|
|
exit_type=ExitType.EXIT_HALF,
|
|
reason="target_1_hit",
|
|
price=157.5,
|
|
),
|
|
]
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=exits,
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert len(output.exit_signals) == 1
|
|
assert output.exit_signals[0].position_id == "pos-1"
|
|
assert output.exit_signals[0].reason == "target_1_hit"
|
|
|
|
def test_output_shadow_mode(self) -> None:
|
|
"""Shadow mode flag is propagated from config."""
|
|
config = SignalEngineConfig(shadow_mode=True)
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=_heuristic(Verdict.BUY),
|
|
probabilistic=_probabilistic(Verdict.BUY),
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=config,
|
|
)
|
|
|
|
assert output.shadow_mode is True
|
|
|
|
def test_output_detail_payloads(self) -> None:
|
|
"""Heuristic and probabilistic detail payloads are populated for audit."""
|
|
h = _heuristic(Verdict.BUY, confidence=0.85)
|
|
p = _probabilistic(Verdict.BUY, p_up=0.75)
|
|
output = format_output(
|
|
ticker="AAPL",
|
|
price=150.0,
|
|
heuristic=h,
|
|
probabilistic=p,
|
|
delta=_delta(agreement=True),
|
|
exit_signals=[],
|
|
config=_default_config(),
|
|
)
|
|
|
|
assert output.heuristic_detail["verdict"] == "BUY"
|
|
assert output.heuristic_detail["confidence"] == 0.85
|
|
assert output.probabilistic_detail["verdict"] == "BUY"
|
|
assert output.probabilistic_detail["p_up"] == 0.75
|