Files
stonks-oracle/.kiro/specs/dual-pipeline-signal-engine/design.md
T
Celes Renata 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
feat: implement dual-pipeline signal engine service
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)
2026-05-02 07:32:26 +00:00

724 lines
23 KiB
Markdown

# Design Document — Dual-Pipeline Signal Engine
## Overview
The dual-pipeline signal engine is a new service at `services/signal_engine/` that runs as an independent Kubernetes deployment alongside the existing aggregation → recommendation pipeline. It implements a concurrent dual-pipeline architecture where both a heuristic (deterministic scoring) and probabilistic (Bayesian inference) pipeline evaluate the same normalized inputs per ticker per evaluation tick, producing independent BUY/WATCH/SKIP verdicts. A delta analyzer compares the two verdicts, and an output formatter assembles a structured `SignalOutput` contract published to the existing `trading_decisions` Redis queue.
The engine introduces several new components — Input Normalizer, Signal Library (Fibonacci, MA Stack, RSI, Cup & Handle, Elliott Wave), Multi-Timeframe Engine, Hard Filter Engine, Exit Engine, Delta Analyzer, and Output Formatter — while reusing existing infrastructure: `compute_signal_weight`, `compute_bayesian_posterior`, `classify_regime`, `WeightedSignal`, `BayesianPosterior`, and `RegimeClassification` from `services/aggregation/`.
The service is toggled via `dual_pipeline_enabled` in the `risk_configs` table (default: false, fail-safe). When disabled, the existing pipeline operates unchanged. When enabled, the signal engine runs alongside the existing pipeline with support for shadow mode (dual-pipeline output persisted but not forwarded to trading).
### Design Rationale
- **Separate service, not inline extension**: The signal engine has a fundamentally different evaluation cadence (multi-timeframe technical signals) and data flow (OHLCV bars, not document intelligence). Embedding it in the aggregation worker would couple two distinct concerns.
- **Reuse existing math**: The Bayesian posterior, regime classification, and signal weighting functions are battle-tested. The probabilistic pipeline wraps them with regime-based priors and likelihood ratio accumulation rather than reimplementing.
- **Concurrent pipelines via asyncio.gather**: Both pipelines share the same `NormalizedInput` reference and run concurrently. If one fails, the other completes normally with the failed pipeline producing a SKIP verdict.
- **Signal clustering for correlation penalty**: The Bayesian pipeline groups signals into four clusters (momentum, structure, volatility, fundamentals) and applies exponential decay within each cluster to prevent likelihood ratio stacking inflation from correlated signals.
---
## Architecture
### High-Level Flow
```mermaid
graph TD
A[Evaluation Tick<br/>Redis queue: signal_engine] --> B[Input Normalizer]
B --> C[Hard Filter Engine]
C -->|filtered out| D[SKIP verdict for both pipelines]
C -->|passed| E[Signal Library]
E --> F[Multi-Timeframe Engine]
F --> G{asyncio.gather}
G --> H[Heuristic Pipeline]
G --> I[Probabilistic Pipeline]
H --> J[Delta Analyzer]
I --> J
J --> K[Output Formatter]
K --> L[SignalOutput]
L --> M[Redis: trading_decisions queue]
L --> N[PostgreSQL: signal_engine_outputs]
subgraph Exit Path
B --> O[Exit Engine]
O --> K
end
```
### Trigger Mechanism
The signal engine polls a new Redis queue `stonks:queue:signal_engine`. Evaluation ticks are enqueued by the scheduler service after aggregation completes for a ticker. The queue message contains `{"ticker": "AAPL", "triggered_at": "2024-01-15T10:00:00Z"}`.
### Integration Points
| Component | Integration | Direction |
|---|---|---|
| Scheduler | Enqueues ticks to `signal_engine` queue | Scheduler → Signal Engine |
| Market data tables | OHLCV bars, closing prices, returns | Signal Engine reads |
| `macro_impact_records` | Macro bias computation | Signal Engine reads |
| `trend_windows` | Fundamental/valuation context | Signal Engine reads |
| `risk_configs` | Feature flags, thresholds | Signal Engine reads |
| `classify_regime()` | Regime classification for priors | Signal Engine calls |
| `compute_signal_weight()` | Heuristic signal weighting | Signal Engine calls |
| `compute_bayesian_posterior()` | Bayesian accumulation | Signal Engine calls |
| Redis `trading_decisions` | SignalOutput publication | Signal Engine → Trading Engine |
| `signal_engine_outputs` table | Persistence for audit | Signal Engine writes |
| Redis rolling agreement | Delta analyzer metrics | Signal Engine writes |
---
## Components and Interfaces
### Module Structure
```
services/signal_engine/
├── __init__.py
├── main.py # Entry point: asyncio event loop, queue polling
├── worker.py # Top-level orchestrator per evaluation tick
├── config.py # SignalEngineConfig, loaded from risk_configs + env
├── models.py # All Pydantic models (NormalizedInput, SignalResult, etc.)
├── normalizer.py # Input Normalizer — fetches and assembles NormalizedInput
├── signals/
│ ├── __init__.py
│ ├── base.py # SignalEvaluator protocol, SignalResult model
│ ├── fibonacci.py # Fibonacci retracement evaluator
│ ├── ma_stack.py # Moving average stack evaluator
│ ├── rsi.py # RSI evaluator
│ ├── cup_handle.py # Cup & Handle pattern detector
│ └── elliott_wave.py # Elliott Wave detector
├── confluence.py # Multi-Timeframe Confluence Engine
├── hard_filter.py # Hard Filter Engine
├── heuristic.py # Heuristic Pipeline (Pipeline A)
├── probabilistic.py # Probabilistic Pipeline (Pipeline B)
├── correlation.py # Signal cluster classification + correlation penalty
├── exit_engine.py # Exit Engine — position-level exit management
├── delta.py # Delta Analyzer
├── formatter.py # Output Formatter
└── persistence.py # Database persistence for signal_engine_outputs
```
### Key Function Signatures
#### `main.py` — Entry Point
```python
async def main() -> None:
"""Start the signal engine worker loop.
Connects to PostgreSQL and Redis, loads config from risk_configs,
and polls the signal_engine queue indefinitely.
"""
```
#### `worker.py` — Orchestrator
```python
async def evaluate_tick(
pool: asyncpg.Pool,
redis: redis.asyncio.Redis,
ticker: str,
config: SignalEngineConfig,
) -> SignalOutput | None:
"""Run a full evaluation tick for a single ticker.
1. Normalize inputs
2. Evaluate exit conditions for open positions
3. Run hard filters
4. Evaluate signals across timeframes
5. Run both pipelines concurrently
6. Compute delta analysis
7. Format and publish output
Returns None if the ticker is hard-filtered or both pipelines fail.
"""
```
#### `normalizer.py` — Input Normalizer
```python
async def normalize_input(
pool: asyncpg.Pool,
ticker: str,
config: SignalEngineConfig,
) -> NormalizedInput:
"""Fetch and assemble all data needed for a single evaluation tick.
Sources:
- OHLCV bars from market_data_bars (M30, H1, H4, D, W, M)
- Fundamental metrics from trend_windows + companies
- Macro context from macro_impact_records + global_events
- Open position state from the trading engine's portfolio
Missing data sources produce sentinel values (None/empty list)
with a logged warning.
"""
```
#### `signals/base.py` — Signal Evaluator Protocol
```python
from typing import Protocol
class SignalEvaluator(Protocol):
"""Protocol for all signal evaluators in the Signal Library."""
def evaluate(
self,
bars: list[OHLCVBar],
timeframe: str,
) -> SignalResult | None:
"""Evaluate a signal on a single timeframe's bar data.
Returns None when insufficient data is available.
"""
...
```
#### `confluence.py` — Multi-Timeframe Engine
```python
def compute_confluence(
signal_results: dict[str, dict[str, SignalResult]],
weights: dict[str, float],
) -> list[ConfluenceSignal]:
"""Compute weighted confluence scores across timeframes.
Args:
signal_results: {signal_type: {timeframe: SignalResult}}
weights: {timeframe: weight} e.g. {"M30": 0.03, "D": 0.30, ...}
Returns:
List of ConfluenceSignal objects that pass the minimum
confluence threshold (≥2 timeframes, ≥1 of D/W/M).
"""
```
#### `hard_filter.py` — Hard Filter Engine
```python
def evaluate_hard_filters(
normalized: NormalizedInput,
config: HardFilterConfig,
) -> HardFilterResult:
"""Evaluate pre-pipeline hard filters.
Checks:
- macro_bias == -1.0 → SKIP
- valuation_score < threshold → SKIP
- earnings_proximity_days <= threshold → SKIP
Returns HardFilterResult with filtered=True/False and all triggered reasons.
"""
```
#### `heuristic.py` — Heuristic Pipeline
```python
def run_heuristic_pipeline(
normalized: NormalizedInput,
confluence_signals: list[ConfluenceSignal],
config: HeuristicConfig,
) -> HeuristicResult:
"""Run the deterministic heuristic pipeline.
Computes S_total = S_company + S_macro + S_competitive using
existing compute_signal_weight() and weighted sentiment averaging.
Produces BUY/WATCH/SKIP verdict based on confidence and score thresholds.
"""
```
#### `probabilistic.py` — Probabilistic Pipeline
```python
def run_probabilistic_pipeline(
normalized: NormalizedInput,
confluence_signals: list[ConfluenceSignal],
regime: RegimeClassification,
config: ProbabilisticConfig,
) -> ProbabilisticResult:
"""Run the Bayesian probabilistic pipeline.
1. Initialize regime-based prior (bull=0.58, range=0.50, bear=0.42)
2. Compute likelihood ratios per signal with correlation penalty
3. Accumulate via log-odds: logit(P_post) = logit(P_prior) + Σ log(LR_i)
4. Apply entropy gating
5. Compute EV_R = P_up · E[win_R] - (1 - P_up) · 1.0
6. Produce BUY/WATCH/SKIP verdict
"""
```
#### `correlation.py` — Signal Correlation Penalty
```python
class SignalCluster(str, Enum):
MOMENTUM = "momentum" # MA stack, RSI
STRUCTURE = "structure" # Fibonacci, Elliott Wave
VOLATILITY = "volatility" # ATR-based, Bollinger-derived
FUNDAMENTALS = "fundamentals" # valuation, earnings, macro
def classify_signal(signal_type: str) -> SignalCluster:
"""Map a signal type to its correlation cluster."""
def apply_correlation_penalty(
likelihood_ratios: list[LikelihoodRatio],
) -> list[LikelihoodRatio]:
"""Apply within-cluster decay penalty to correlated signals.
Within each cluster, signals are ranked by LR magnitude.
The strongest contributes at full weight; subsequent signals
contribute at 0.5^(n-1) decay.
Cross-cluster signals are independent (no penalty).
"""
```
#### `exit_engine.py` — Exit Engine
```python
def evaluate_exits(
positions: list[OpenPositionState],
current_prices: dict[str, float],
config: ExitConfig,
) -> list[ExitSignal]:
"""Evaluate exit conditions for all open positions.
Checks: stop_loss hit, target_1 hit (EXIT_HALF), target_2 hit (EXIT_FULL),
trailing stop hit (EXIT_FULL for remaining).
Trailing stop activates after EXIT_HALF and ratchets upward only.
"""
```
#### `delta.py` — Delta Analyzer
```python
async def analyze_delta(
heuristic: HeuristicResult,
probabilistic: ProbabilisticResult,
redis: redis.asyncio.Redis,
ticker: str,
) -> DeltaResult:
"""Compare pipeline verdicts and track agreement metrics.
Computes agreement flag, confidence delta, disagreement reasons.
Updates rolling 100-evaluation agreement rate in Redis.
Logs warning when agreement rate drops below 0.50.
"""
```
#### `formatter.py` — Output Formatter
```python
def format_output(
ticker: str,
price: float,
heuristic: HeuristicResult,
probabilistic: ProbabilisticResult,
delta: DeltaResult,
exit_signals: list[ExitSignal],
config: SignalEngineConfig,
) -> SignalOutput:
"""Assemble the structured SignalOutput contract.
Populates trade_plan based on verdict combination:
- 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)
"""
def signal_output_to_recommendation(output: SignalOutput) -> Recommendation:
"""Map a SignalOutput to the existing Recommendation schema.
Enables the trading engine to consume dual-pipeline outputs
without modification to its core evaluate_recommendation logic.
"""
```
#### `persistence.py` — Database Persistence
```python
async def persist_signal_output(
pool: asyncpg.Pool,
output: SignalOutput,
) -> None:
"""Persist a SignalOutput to the signal_engine_outputs table.
Logs and continues on database errors (persistence failure
does not block signal emission to the trading queue).
"""
```
---
## Data Models
All new data models are Pydantic `BaseModel` subclasses defined in `services/signal_engine/models.py`. Existing models (`WeightedSignal`, `BayesianPosterior`, `RegimeClassification`, `TrendSummary`, `Recommendation`, `PositionSizing`) are imported from `services/aggregation/` and `services/shared/schemas.py`.
### OHLCVBar
```python
class OHLCVBar(BaseModel):
"""Single OHLCV bar for a timeframe."""
timestamp: datetime
open: float
high: float
low: float
close: float
volume: float
```
### NormalizedInput
```python
class NormalizedInput(BaseModel):
"""Unified input structure consumed by both pipelines."""
ticker: str
evaluated_at: datetime
# Multi-timeframe OHLCV bars
bars: dict[str, list[OHLCVBar]] # {"M30": [...], "H1": [...], ...}
# Fundamental metrics
valuation_score: float | None = None # [0.0, 1.0]
earnings_proximity_days: int | None = None
# Macro context
macro_bias: float = 0.0 # [-1.0, 1.0]
# Open position state (for exit engine)
open_positions: list[OpenPositionState] = Field(default_factory=list)
# Market data for regime classification
closing_prices: list[float] = Field(default_factory=list)
returns: list[float] = Field(default_factory=list)
# Current price (latest close from shortest available timeframe)
current_price: float | None = None
```
### OpenPositionState
```python
class OpenPositionState(BaseModel):
"""Snapshot of an open position for exit evaluation."""
position_id: str
ticker: str
entry_price: float
current_price: float
stop_loss: float
target_1: float
target_2: float
trailing_stop: float | None = None
partial_exit_done: bool = False
atr: float | None = None
```
### SignalResult
```python
class SignalDirection(str, Enum):
BULLISH = "bullish"
BEARISH = "bearish"
NEUTRAL = "neutral"
class SignalResult(BaseModel):
"""Output from a single signal evaluator on a single timeframe."""
signal_type: str # e.g. "fibonacci", "ma_stack", "rsi"
timeframe: str # e.g. "D", "H4"
strength: float = Field(ge=0.0, le=1.0)
direction: SignalDirection
confidence: float = Field(ge=0.0, le=1.0)
metadata: dict = Field(default_factory=dict) # signal-specific details
```
### ConfluenceSignal
```python
class ConfluenceSignal(BaseModel):
"""A signal that passed multi-timeframe confluence filtering."""
signal_type: str
direction: SignalDirection
confluence_score: float # weighted sum across timeframes
active_timeframes: list[str] # which timeframes triggered
per_timeframe: dict[str, float] # {timeframe: strength}
```
### Verdict
```python
class Verdict(str, Enum):
BUY = "BUY"
WATCH = "WATCH"
SKIP = "SKIP"
```
### HeuristicResult
```python
class HeuristicResult(BaseModel):
"""Output from the heuristic (deterministic) pipeline."""
verdict: Verdict
confidence: float = Field(ge=0.0, le=1.0)
s_total: float
s_company: float
s_macro: float
s_competitive: float
signal_weights: list[dict] = Field(default_factory=list)
reasoning: list[str] = Field(default_factory=list)
```
### LikelihoodRatio
```python
class LikelihoodRatio(BaseModel):
"""A single signal's likelihood ratio for Bayesian updating."""
signal_type: str
cluster: str # SignalCluster value
lr: float # P(sig|up) / P(sig|down)
log_lr: float # log(lr)
penalized_log_lr: float # after correlation penalty
hit_rate: float
strength: float
```
### ProbabilisticResult
```python
class ProbabilisticResult(BaseModel):
"""Output from the probabilistic (Bayesian) pipeline."""
verdict: Verdict
p_up: float = Field(ge=0.0, le=1.0)
entropy: float = Field(ge=0.0, le=1.0)
ev_r: float
prior: float
posterior: float
likelihood_ratios: list[LikelihoodRatio] = Field(default_factory=list)
regime: str
reasoning: list[str] = Field(default_factory=list)
```
### DeltaResult
```python
class DeltaResult(BaseModel):
"""Output from the delta analyzer comparing both pipelines."""
agreement: bool
confidence_delta: float
heuristic_verdict: str
probabilistic_verdict: str
disagreement_reasons: list[str] = Field(default_factory=list)
rolling_agreement_rate: float | None = None
```
### ExitSignal
```python
class ExitType(str, Enum):
EXIT_HALF = "EXIT_HALF"
EXIT_FULL = "EXIT_FULL"
class ExitSignal(BaseModel):
"""An exit signal for an open position."""
position_id: str
ticker: str
exit_type: ExitType
reason: str # "stop_hit", "target_1_hit", "target_2_hit", "trailing_stop_hit"
price: float
```
### TradePlan
```python
class TradePlan(BaseModel):
"""Optional trade plan attached to a BUY signal."""
entry_price: float
stop_loss: float
target_1: float
target_2: float
position_size_pct: float = Field(ge=0.0, le=1.0)
max_loss_pct: float = Field(ge=0.0, le=1.0)
dual_confirmed: bool = False
probabilistic_only: bool = False
```
### SignalOutput
```python
class SignalOutput(BaseModel):
"""The structured output contract consumed by the trading engine and audit systems."""
output_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
ticker: str
timestamp: datetime
price: float
# Heuristic pipeline results
heuristic_verdict: str
heuristic_confidence: float
heuristic_s_total: float
# Probabilistic pipeline results
probabilistic_verdict: str
probabilistic_p_up: float
probabilistic_entropy: float
probabilistic_ev_r: float
# Delta analysis
delta_agreement: bool
delta_confidence_delta: float
delta_reasons: list[str] = Field(default_factory=list)
# Optional trade plan (populated when at least one pipeline says BUY)
trade_plan: TradePlan | None = None
# Exit signals for open positions
exit_signals: list[ExitSignal] = Field(default_factory=list)
# Full pipeline results for audit (stored as JSONB)
heuristic_detail: dict = Field(default_factory=dict)
probabilistic_detail: dict = Field(default_factory=dict)
# Pipeline mode metadata
pipeline_mode: str = "dual_pipeline"
shadow_mode: bool = False
```
### SignalEngineConfig
```python
@dataclass
class SignalEngineConfig:
"""Configuration loaded from risk_configs + environment."""
dual_pipeline_enabled: bool = False
heuristic_pipeline_enabled: bool = True
probabilistic_pipeline_enabled: bool = True
shadow_mode: bool = False
# Timeframe weights
timeframe_weights: dict[str, float] = field(default_factory=lambda: {
"M30": 0.03, "H1": 0.07, "H4": 0.15,
"D": 0.30, "W": 0.30, "M": 0.15,
})
# Hard filter thresholds
hard_filter_valuation_min: float = 0.3
hard_filter_earnings_days: int = 5
hard_filter_macro_bias_skip: float = -1.0
# Heuristic verdict thresholds
heuristic_buy_confidence: float = 0.70
heuristic_buy_s_total: float = 1.2
heuristic_buy_valuation_min: float = 0.5
heuristic_watch_confidence: float = 0.55
# Probabilistic verdict thresholds
prob_buy_p_up: float = 0.60
prob_buy_entropy_max: float = 0.90
prob_buy_ev_r_min: float = 1.5
prob_buy_valuation_min: float = 0.5
prob_watch_p_up: float = 0.55
prob_watch_entropy_max: float = 0.95
prob_entropy_skip: float = 0.95
# Regime priors
regime_prior_bull: float = 0.58
regime_prior_range: float = 0.50
regime_prior_bear: float = 0.42
# Exit engine
trailing_stop_atr_multiplier: float = 2.0
# Polling
polling_interval_seconds: int = 30
```
### HardFilterConfig / HeuristicConfig / ProbabilisticConfig / ExitConfig
These are derived from `SignalEngineConfig` fields for cleaner function signatures — simple `@dataclass` wrappers over the relevant subset of config values.
---
### Database Migration (039)
```sql
-- Migration 039: Signal Engine Outputs
-- Creates the signal_engine_outputs table for persisting dual-pipeline evaluations.
CREATE TABLE IF NOT EXISTS signal_engine_outputs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ticker TEXT NOT NULL,
evaluated_at TIMESTAMPTZ NOT NULL,
price NUMERIC NOT NULL,
-- Heuristic pipeline
heuristic_verdict TEXT NOT NULL,
heuristic_confidence NUMERIC NOT NULL,
heuristic_s_total NUMERIC NOT NULL,
-- Probabilistic pipeline
probabilistic_verdict TEXT NOT NULL,
probabilistic_p_up NUMERIC NOT NULL,
probabilistic_entropy NUMERIC NOT NULL,
probabilistic_ev_r NUMERIC NOT NULL,
-- Delta analysis
delta_agreement BOOLEAN NOT NULL,
delta_confidence_delta NUMERIC NOT NULL,
delta_reasons JSONB NOT NULL DEFAULT '[]'::jsonb,
-- Trade plan (null when no BUY verdict)
trade_plan JSONB,
-- Full output for audit
full_output JSONB NOT NULL,
-- Exit signals
exit_signals JSONB NOT NULL DEFAULT '[]'::jsonb,
-- Metadata
pipeline_mode TEXT NOT NULL DEFAULT 'dual_pipeline',
shadow_mode BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for per-ticker time-range queries
CREATE INDEX IF NOT EXISTS idx_signal_engine_outputs_ticker_time
ON signal_engine_outputs (ticker, evaluated_at);
-- Index for global time-range queries
CREATE INDEX IF NOT EXISTS idx_signal_engine_outputs_evaluated
ON signal_engine_outputs (evaluated_at);
-- Index for filtering by verdict
CREATE INDEX IF NOT EXISTS idx_signal_engine_outputs_verdicts
ON signal_engine_outputs (heuristic_verdict, probabilistic_verdict);
```
### Helm / Deployment Configuration
Add to `values.yaml` under `services:`:
```yaml
signalEngine:
replicas: 1
pipeline: true
image: signal-engine
command: "python -m services.signal_engine.main"
tier: processing
secrets: [stonks-core-secrets, stonks-market-secrets]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
```
Add to `redis_keys.py`:
```python
QUEUE_SIGNAL_ENGINE = "signal_engine"
```
The service uses the existing `stonks-config` ConfigMap and `stonks-core-secrets` for database/Redis credentials. No new ingress or network policy is needed — the signal engine is a queue-polling worker with no HTTP interface.
---