# Implementation Plan: Dual-Pipeline Signal Engine ## Overview Implement the dual-pipeline signal engine as a new service at `services/signal_engine/` that runs as an independent Kubernetes deployment. The engine evaluates both a heuristic (deterministic scoring) and probabilistic (Bayesian inference) pipeline concurrently per ticker per evaluation tick, producing independent BUY/WATCH/SKIP verdicts. Implementation proceeds incrementally: infrastructure first, then core models, signal library, pipelines, orchestration, integration, and deployment. ## Tasks - [x] 1. Project scaffolding, configuration, and data models - [x] 1.1 Create service directory structure and `__init__.py` files - Create `services/signal_engine/` with all subdirectories per the design module structure - Create `services/signal_engine/__init__.py`, `services/signal_engine/signals/__init__.py` - _Requirements: 11.1, 13.1_ - [x] 1.2 Implement `models.py` — all Pydantic data models - Define `OHLCVBar`, `NormalizedInput`, `OpenPositionState`, `SignalResult`, `SignalDirection` - Define `ConfluenceSignal`, `Verdict`, `HeuristicResult`, `LikelihoodRatio`, `ProbabilisticResult` - Define `DeltaResult`, `ExitSignal`, `ExitType`, `TradePlan`, `SignalOutput` - All models must use Pydantic `BaseModel` with proper field constraints (`ge`, `le`) - _Requirements: 1.1, 2.7, 5.7, 6.9, 9.5, 10.1, 10.5_ - [x] 1.3 Implement `config.py` — `SignalEngineConfig` and sub-configs - Define `SignalEngineConfig` dataclass with all fields from the design - Define `HardFilterConfig`, `HeuristicConfig`, `ProbabilisticConfig`, `ExitConfig` as derived sub-configs - Implement `load_config()` that reads from `risk_configs` table + environment variables - Default `dual_pipeline_enabled` to `False` (fail-safe) - _Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7_ - [x] 1.4 Add `QUEUE_SIGNAL_ENGINE` to `services/shared/redis_keys.py` - Add `QUEUE_SIGNAL_ENGINE = "signal_engine"` constant - _Requirements: 11.1_ - [x] 1.5 Write property test for `SignalOutput` round-trip serialization - **Requirement 17.6: SignalOutput round-trip serialization** - Generate arbitrary valid `SignalOutput` instances with Hypothesis - Verify `SignalOutput.model_validate_json(output.model_dump_json())` produces equivalent object - File: `tests/test_pbt_signal_engine_models.py` - _Requirements: 10.5, 17.6_ - [x] 2. Input Normalizer and Hard Filter Engine - [x] 2.1 Implement `normalizer.py` — Input Normalizer - Implement `normalize_input(pool, ticker, config) -> NormalizedInput` - Fetch OHLCV bars from `market_data_bars` for M30, H1, H4, D, W, M timeframes - Fetch fundamental metrics (valuation_score, earnings_proximity_days) from company/trend data - Fetch macro context (macro_bias) from `macro_impact_records` and `global_events` - Fetch open position state from trading engine portfolio tables - Populate sentinel values (`None`, empty list) for unavailable data with logged warnings - Validate monotonically increasing timestamps within each timeframe series - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_ - [x] 2.2 Implement `hard_filter.py` — Hard Filter Engine - Implement `evaluate_hard_filters(normalized, config) -> HardFilterResult` - Check `macro_bias == -1.0` → SKIP with reason "macro_bias_negative" - Check `valuation_score < 0.3` → SKIP with reason "valuation_below_threshold" - Check `earnings_proximity_days <= 5` → SKIP with reason "earnings_block" - Record all triggered filter reasons (not just first) - Return `HardFilterResult` with `filtered: bool` and `reasons: list[str]` - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_ - [x] 2.3 Write property tests for hard filter engine - **Requirement 17.7: Hard filter determinism** - Generate arbitrary `NormalizedInput` with `macro_bias = -1.0` → always SKIP - Generate arbitrary `NormalizedInput` with `valuation_score < 0.3` → always SKIP - Generate arbitrary `NormalizedInput` with `earnings_proximity_days <= 5` → always SKIP - Verify these hold regardless of all other input values - File: `tests/test_pbt_signal_engine_hard_filter.py` - _Requirements: 4.1, 4.2, 4.3, 17.7_ - [x] 3. Checkpoint — Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 4. Signal Library — Technical Signal Evaluators - [x] 4.1 Implement `signals/base.py` — SignalEvaluator protocol - Define `SignalEvaluator` protocol with `evaluate(bars, timeframe) -> SignalResult | None` - Define common helper functions for swing high/low detection, lookback validation - _Requirements: 2.6, 2.7_ - [x] 4.2 Implement `signals/fibonacci.py` — Fibonacci retracement evaluator - Implement `L(r) = SH - r·(SH - SL)` for ratios [0.236, 0.382, 0.5, 0.618, 0.786] - Detect swing high and swing low within the evaluation window - Produce signal strength based on proximity of current price to retracement levels - Return `None` with reason code when insufficient data - _Requirements: 2.1, 2.6, 2.7_ - [x] 4.3 Write property test for Fibonacci retracement formula - **Requirement 17.1: Fibonacci retracement bounds** - For all `r` in [0, 1] and all `SH > SL > 0`, verify `L(r)` is in [SL, SH] - File: `tests/test_pbt_signal_engine_fibonacci.py` - _Requirements: 2.1, 17.1_ - [x] 4.4 Implement `signals/ma_stack.py` — Moving average stack evaluator - Detect bullish alignment (MA_10 > MA_20 > MA_50 > MA_200) - Detect bearish alignment (MA_10 < MA_20 < MA_50 < MA_200) - Produce signal strength proportional to degree of alignment - Return `None` when insufficient bars for MA_200 calculation - _Requirements: 2.2, 2.6, 2.7_ - [x] 4.5 Implement `signals/rsi.py` — RSI evaluator - Implement standard 14-period RSI formula - Produce overbought signals (RSI > 70) and oversold signals (RSI < 30) - Scale strength by distance from threshold - Return `None` when fewer than 14 bars available - _Requirements: 2.3, 2.6, 2.7_ - [x] 4.6 Implement `signals/cup_handle.py` — Cup & Handle pattern detector - Identify cup formation (U-shaped price recovery) and handle (small consolidation) - Produce signal with confidence proportional to pattern completeness - Return `None` when insufficient data or no pattern detected - _Requirements: 2.4, 2.6, 2.7_ - [x] 4.7 Implement `signals/elliott_wave.py` — Elliott Wave detector - Identify impulse waves (5-wave structure) and corrective waves (3-wave structure) - Produce signal with current wave position and projected direction - Return `None` when insufficient data or ambiguous wave count - _Requirements: 2.5, 2.6, 2.7_ - [x] 5. Multi-Timeframe Confluence Engine - [x] 5.1 Implement `confluence.py` — Multi-Timeframe Engine - Implement `compute_confluence(signal_results, weights) -> list[ConfluenceSignal]` - Compute weighted confluence score: `C_confluence = Σ(w_tf · s_tf)` - Apply minimum confluence threshold: discard signals triggering on < 2 timeframes - Apply higher-timeframe anchor: discard signals without at least one of D, W, or M - Return `ConfluenceSignal` objects with active timeframes and per-timeframe strengths - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_ - [x] 5.2 Write property test for confluence score monotonicity - **Requirement 17.5: Confluence score monotonicity** - Verify that activating a signal on an additional timeframe with non-zero weight always increases or maintains the confluence score - File: `tests/test_pbt_signal_engine_confluence.py` - _Requirements: 3.6, 17.5_ - [x] 6. Checkpoint — Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 7. Heuristic Pipeline (Pipeline A) - [x] 7.1 Implement `heuristic.py` — Heuristic Pipeline - Implement `run_heuristic_pipeline(normalized, confluence_signals, config) -> HeuristicResult` - Compute `S_total = S_company + S_macro + S_competitive` using existing `compute_signal_weight()` - Compute confidence from source count, extraction confidence, signal agreement, contradiction penalty - BUY verdict: confidence >= 0.70 AND S_total >= 1.2 AND valuation_score >= 0.5 AND macro_bias > 0 AND earnings_proximity_days > 5 - WATCH verdict: confidence >= 0.55 AND BUY conditions not fully met - SKIP verdict: confidence < 0.55 - Emit `HeuristicResult` with all required fields and reasoning - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7_ - [x] 7.2 Write unit tests for heuristic pipeline verdict logic - Test BUY threshold conditions - Test WATCH threshold conditions - Test SKIP conditions - Test edge cases at threshold boundaries - File: `tests/test_signal_engine_heuristic.py` - _Requirements: 5.4, 5.5, 5.6_ - [x] 8. Probabilistic Pipeline (Pipeline B) and Correlation Penalty - [x] 8.1 Implement `correlation.py` — Signal cluster classification and penalty - Define `SignalCluster` enum: MOMENTUM, STRUCTURE, VOLATILITY, FUNDAMENTALS - Implement `classify_signal(signal_type) -> SignalCluster` - Implement `apply_correlation_penalty(likelihood_ratios) -> list[LikelihoodRatio]` - Within-cluster decay: strongest LR at full weight, subsequent at 0.5^(n-1) - No penalty across different clusters - Single-signal clusters receive no penalty - _Requirements: 7.1, 7.2, 7.3, 7.4_ - [x] 8.2 Implement `probabilistic.py` — Probabilistic Pipeline - Implement `run_probabilistic_pipeline(normalized, confluence_signals, regime, config) -> ProbabilisticResult` - Initialize regime-based prior: bull=0.58, range=0.50, bear=0.42 - Compute likelihood ratios: `P(sig|up) = h·s + (1-h)·(1-s)·0.5`, `LR = P(sig|up) / P(sig|down)` - Apply correlation penalty via `apply_correlation_penalty()` - Accumulate via log-odds: `logit(P_post) = logit(P_prior) + Σ log(LR_i)` - Compute Shannon entropy and apply entropy gating (H > 0.95 → SKIP) - Compute `EV_R = P_up · E[win_R] - (1 - P_up) · 1.0` - BUY: P_up >= 0.60 AND entropy <= 0.90 AND EV_R >= 1.5 AND macro_bias > 0 AND valuation_score >= 0.5 - WATCH: P_up >= 0.55 AND entropy <= 0.95 AND BUY conditions not fully met - SKIP: all other cases - Use existing `classify_regime()` from `services/aggregation/regime.py` - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 14.1, 14.2, 14.3, 14.4, 14.5_ - [x] 8.3 Write property test for Bayesian log-odds round-trip - **Requirement 17.2: Bayesian log-odds update correctness** - Verify `logit(P_post) = logit(P_prior) + Σ log(LR_i)` round-trips correctly - Converting P_prior to logit, adding log-LRs, converting back via sigmoid produces valid probability in (0, 1) - File: `tests/test_pbt_signal_engine_bayesian.py` - _Requirements: 6.3, 17.2_ - [x] 8.4 Write property test for entropy gate - **Requirement 17.3: Entropy gate properties** - Verify Shannon entropy is maximized at P_up = 0.5 - Verify entropy equals 0.0 at P_up = 0.0 or P_up = 1.0 - Verify entropy is symmetric around 0.5 - File: `tests/test_pbt_signal_engine_bayesian.py` - _Requirements: 6.4, 17.3_ - [x] 8.5 Write property test for signal correlation penalty - **Requirement 17.4: Correlation penalty reduces confidence** - Verify penalized posterior is always <= unpenalized posterior for any signal set with correlated signals - File: `tests/test_pbt_signal_engine_correlation.py` - _Requirements: 7.5, 17.4_ - [x] 8.6 Write property test for EV_R monotonicity - **Requirement 17.8: EV_R monotonically increasing with P_up** - Verify `EV_R = P_up · E[win_R] - (1 - P_up) · 1.0` is monotonically increasing with P_up for fixed E[win_R] > 0 - File: `tests/test_pbt_signal_engine_bayesian.py` - _Requirements: 6.5, 17.8_ - [x] 9. Checkpoint — Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 10. Exit Engine - [x] 10.1 Implement `exit_engine.py` — Exit Engine - Implement `evaluate_exits(positions, current_prices, config) -> list[ExitSignal]` - Check stop_loss hit → EXIT_FULL with reason "stop_hit" - Check target_1 hit → EXIT_HALF with reason "target_1_hit" - Check target_2 hit → EXIT_FULL with reason "target_2_hit" - Trailing stop: activate after EXIT_HALF at `current_price - ATR · trailing_multiplier` - Trailing stop ratchets upward only (never moves down) - Trailing stop hit → EXIT_FULL with reason "trailing_stop_hit" - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7_ - [x] 10.2 Write unit tests for exit engine - Test stop_loss trigger - Test target_1 partial exit - Test target_2 full exit - Test trailing stop activation and ratchet behavior - File: `tests/test_signal_engine_exit.py` - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_ - [x] 11. Delta Analyzer and Output Formatter - [x] 11.1 Implement `delta.py` — Delta Analyzer - Implement `analyze_delta(heuristic, probabilistic, redis, ticker) -> DeltaResult` - Compute agreement flag (both verdicts identical) - Compute confidence delta: `|heuristic_confidence - probabilistic_P_up|` - Record disagreement reasons when verdicts differ - Track rolling 100-evaluation agreement rate in Redis - Log warning when agreement rate drops below 0.50 - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6_ - [x] 11.2 Implement `formatter.py` — Output Formatter - Implement `format_output(ticker, price, heuristic, probabilistic, delta, exit_signals, config) -> SignalOutput` - Both BUY → `dual_confirmed`, full position sizing - Probabilistic-only BUY → `probabilistic_only`, 50% position sizing - Heuristic-only BUY → standard position sizing - No BUY → no trade_plan (WATCH/SKIP persisted for analysis) - Implement `signal_output_to_recommendation(output) -> Recommendation` - Map `SignalOutput` to existing `Recommendation` schema for trading engine compatibility - Dual confirmed: confidence = max(heuristic_confidence, probabilistic_P_up) - Probabilistic only: confidence = probabilistic_P_up · 0.8 (20% haircut) - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 12.1, 12.2, 12.3, 12.4, 12.5_ - [x] 11.3 Write unit tests for output formatter - Test dual_confirmed trade plan generation - Test probabilistic_only trade plan with 50% sizing - Test heuristic-only trade plan - Test no-BUY case (no trade_plan) - Test `signal_output_to_recommendation` mapping - File: `tests/test_signal_engine_formatter.py` - _Requirements: 10.2, 10.3, 10.4, 12.3, 12.4_ - [x] 12. Orchestrator, Persistence, and Main Entry Point - [x] 12.1 Implement `persistence.py` — Database persistence - Implement `persist_signal_output(pool, output) -> None` - Insert into `signal_engine_outputs` table - Log and continue on database errors (non-blocking) - _Requirements: 15.1, 15.4_ - [x] 12.2 Implement `worker.py` — Top-level orchestrator - Implement `evaluate_tick(pool, redis, ticker, config) -> SignalOutput | None` - Step 1: Normalize inputs (single fetch, shared reference) - Step 2: Evaluate exit conditions for open positions - Step 3: Run hard filters (short-circuit if filtered) - Step 4: Evaluate signals across timeframes via Signal Library - Step 5: Compute confluence - Step 6: Classify regime via existing `classify_regime()` - Step 7: Run both pipelines concurrently via `asyncio.gather` with exception handling - Step 8: Compute delta analysis - Step 9: Format output - Step 10: Persist to database and publish to Redis queue - Catch pipeline exceptions → SKIP verdict for failed pipeline, other continues - Measure and log wall-clock execution time per pipeline - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6_ - [x] 12.3 Implement `main.py` — Entry point with asyncio event loop - Connect to PostgreSQL (asyncpg pool) and Redis (redis.asyncio) - Load config from `risk_configs` table - Log active configuration at startup - Poll `stonks:queue:signal_engine` queue indefinitely - Check `dual_pipeline_enabled` flag; if disabled, sleep and retry - On config read failure, default to disabled (fail-safe) - Support shadow mode (persist but don't forward to trading queue) - _Requirements: 13.1, 13.6, 13.7, 16.1, 16.6_ - [x] 12.4 Write integration tests for worker orchestration - Test full tick evaluation with mocked DB/Redis - Test pipeline failure isolation (one fails, other completes) - Test hard filter short-circuit - Test shadow mode behavior - File: `tests/test_signal_engine_worker.py` - _Requirements: 11.3, 16.6_ - [x] 13. Checkpoint — Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 14. Database migration and infrastructure - [x] 14.1 Create database migration `infra/migrations/039_signal_engine_outputs.sql` - Create `signal_engine_outputs` table per the design schema - Create index on `(ticker, evaluated_at)` for per-ticker time-range queries - Create index on `evaluated_at` for global time-range queries - Create index on `(heuristic_verdict, probabilistic_verdict)` for verdict filtering - _Requirements: 15.1, 15.2, 15.3_ - [x] 14.2 Add signal engine service to Helm chart - Add `signalEngine` entry to `infra/helm/stonks-oracle/values.yaml` - Configure: replicas=1, command=`python -m services.signal_engine.main`, tier=processing - Set resource requests/limits per design (100m/128Mi → 500m/256Mi) - Reference existing secrets: `stonks-core-secrets`, `stonks-market-secrets` - _Requirements: 11.1, 13.1_ - [x] 15. Trading engine integration and backward compatibility - [x] 15.1 Wire signal engine output to trading engine queue - Publish `SignalOutput` (mapped to `Recommendation`) to `stonks:queue:trading_decisions` - Only publish when at least one pipeline produces BUY verdict - WATCH/SKIP verdicts persisted for analysis but not forwarded - Ensure trading engine can consume without modification via `signal_output_to_recommendation()` - _Requirements: 12.1, 12.2, 12.5, 16.2_ - [x] 15.2 Ensure backward compatibility with existing pipeline - Verify `dual_pipeline_enabled=false` means signal engine does not run - Verify existing aggregation pipeline operates unchanged when flag is off - Reuse existing `WeightedSignal`, `BayesianPosterior`, `RegimeClassification` (import, don't duplicate) - Reuse existing `compute_signal_weight`, `compute_bayesian_posterior`, `classify_regime` functions - No modifications to existing tables (new migration only adds new table) - _Requirements: 16.1, 16.2, 16.3, 16.4, 16.5_ - [x] 16. Final checkpoint — Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. ## Notes - Tasks marked with `*` are optional and can be skipped for faster MVP - Each task references specific requirements for traceability - Checkpoints ensure incremental validation between major phases - Property-based tests use Hypothesis with `@settings(max_examples=100)` per project conventions - PBT test files are prefixed `test_pbt_*` per project conventions - The service reuses existing math functions from `services/aggregation/` — no reimplementation - All configuration is loaded from `risk_configs` table with fail-safe defaults - Shadow mode allows running alongside existing pipeline without affecting trading decisions