feat: implement dual-pipeline signal engine service
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
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)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""RSI (Relative Strength Index) signal evaluator.
|
||||
|
||||
Computes the standard 14-period RSI using Wilder's smoothing method and
|
||||
produces overbought (RSI > 70 → BEARISH) or oversold (RSI < 30 → BULLISH)
|
||||
signals with strength scaled by distance from the threshold.
|
||||
|
||||
When RSI is between 30 and 70 (neutral zone), no signal is produced.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from services.signal_engine.models import OHLCVBar, SignalDirection, SignalResult
|
||||
from services.signal_engine.signals.base import validate_lookback
|
||||
|
||||
# Default RSI period (standard Wilder 14-period)
|
||||
DEFAULT_RSI_PERIOD: int = 14
|
||||
|
||||
# Minimum bars required: period + 1 (for initial price change calculation)
|
||||
DEFAULT_MIN_BARS: int = DEFAULT_RSI_PERIOD + 1 # 15
|
||||
|
||||
# Overbought / oversold thresholds
|
||||
OVERBOUGHT_THRESHOLD: float = 70.0
|
||||
OVERSOLD_THRESHOLD: float = 30.0
|
||||
|
||||
# Maximum possible distance from threshold (used for strength scaling)
|
||||
_MAX_DISTANCE_OVERBOUGHT: float = 100.0 - OVERBOUGHT_THRESHOLD # 30
|
||||
_MAX_DISTANCE_OVERSOLD: float = OVERSOLD_THRESHOLD - 0.0 # 30
|
||||
|
||||
# Confidence multiplier
|
||||
_CONFIDENCE_MULTIPLIER: float = 0.85
|
||||
|
||||
|
||||
def compute_rsi(bars: list[OHLCVBar], period: int = DEFAULT_RSI_PERIOD) -> float | None:
|
||||
"""Compute RSI using Wilder's smoothing method.
|
||||
|
||||
Args:
|
||||
bars: OHLCV bar series (oldest-first).
|
||||
period: RSI period (default 14).
|
||||
|
||||
Returns:
|
||||
RSI value in [0, 100], or ``None`` if insufficient data.
|
||||
"""
|
||||
min_bars = period + 1
|
||||
if len(bars) < min_bars:
|
||||
return None
|
||||
|
||||
closes = [bar.close for bar in bars]
|
||||
|
||||
# Calculate price changes
|
||||
changes = [closes[i] - closes[i - 1] for i in range(1, len(closes))]
|
||||
|
||||
# Separate gains and losses for the first `period` changes
|
||||
first_gains = [max(0.0, c) for c in changes[:period]]
|
||||
first_losses = [max(0.0, -c) for c in changes[:period]]
|
||||
|
||||
avg_gain = sum(first_gains) / period
|
||||
avg_loss = sum(first_losses) / period
|
||||
|
||||
# Apply Wilder smoothing for subsequent changes
|
||||
for c in changes[period:]:
|
||||
gain = max(0.0, c)
|
||||
loss = max(0.0, -c)
|
||||
avg_gain = (avg_gain * (period - 1) + gain) / period
|
||||
avg_loss = (avg_loss * (period - 1) + loss) / period
|
||||
|
||||
# Avoid division by zero: if avg_loss is 0, RSI is 100
|
||||
if avg_loss == 0.0:
|
||||
return 100.0
|
||||
|
||||
rs = avg_gain / avg_loss
|
||||
rsi = 100.0 - (100.0 / (1.0 + rs))
|
||||
return rsi
|
||||
|
||||
|
||||
class RSIEvaluator:
|
||||
"""RSI signal evaluator.
|
||||
|
||||
Satisfies the :class:`~services.signal_engine.signals.base.SignalEvaluator`
|
||||
protocol.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
period:
|
||||
RSI calculation period. Defaults to ``14``.
|
||||
"""
|
||||
|
||||
def __init__(self, period: int = DEFAULT_RSI_PERIOD) -> None:
|
||||
self.period = period
|
||||
self.min_bars = period + 1
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API (SignalEvaluator protocol)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
bars: list[OHLCVBar],
|
||||
timeframe: str,
|
||||
) -> SignalResult | None:
|
||||
"""Evaluate RSI on *bars* for *timeframe*.
|
||||
|
||||
Returns ``None`` when there are fewer than ``period + 1`` bars
|
||||
or when RSI is in the neutral zone (30–70).
|
||||
"""
|
||||
if not validate_lookback(bars, self.min_bars):
|
||||
return None
|
||||
|
||||
rsi = compute_rsi(bars, self.period)
|
||||
if rsi is None:
|
||||
return None
|
||||
|
||||
# Overbought: RSI > 70 → BEARISH (potential reversal down)
|
||||
if rsi > OVERBOUGHT_THRESHOLD:
|
||||
distance = rsi - OVERBOUGHT_THRESHOLD
|
||||
strength = min(1.0, max(0.0, distance / _MAX_DISTANCE_OVERBOUGHT))
|
||||
confidence = strength * _CONFIDENCE_MULTIPLIER
|
||||
return SignalResult(
|
||||
signal_type="rsi",
|
||||
timeframe=timeframe,
|
||||
strength=strength,
|
||||
direction=SignalDirection.BEARISH,
|
||||
confidence=confidence,
|
||||
metadata={
|
||||
"rsi": rsi,
|
||||
"period": self.period,
|
||||
"zone": "overbought",
|
||||
},
|
||||
)
|
||||
|
||||
# Oversold: RSI < 30 → BULLISH (potential reversal up)
|
||||
if rsi < OVERSOLD_THRESHOLD:
|
||||
distance = OVERSOLD_THRESHOLD - rsi
|
||||
strength = min(1.0, max(0.0, distance / _MAX_DISTANCE_OVERSOLD))
|
||||
confidence = strength * _CONFIDENCE_MULTIPLIER
|
||||
return SignalResult(
|
||||
signal_type="rsi",
|
||||
timeframe=timeframe,
|
||||
strength=strength,
|
||||
direction=SignalDirection.BULLISH,
|
||||
confidence=confidence,
|
||||
metadata={
|
||||
"rsi": rsi,
|
||||
"period": self.period,
|
||||
"zone": "oversold",
|
||||
},
|
||||
)
|
||||
|
||||
# Neutral zone (30 ≤ RSI ≤ 70): no signal
|
||||
return None
|
||||
Reference in New Issue
Block a user