Skip to content

Signal Aggregation System

Module: redhound.orchestration.signal_aggregator Last Updated: 2026-02-25


Overview

The Signal Aggregation System combines deterministic signals from all analysts into a single unified recommendation with confidence scoring, conflict resolution, and regime-aware risk adjustment.

Core analysts (always included): Technical, Fundamentals, Sentiment, News, Market Context.

Edge analysts (optional, dynamically weighted when data is available): Sector, Insider, Options Flow, Short Interest, Earnings Revisions.

Key Properties

  • Quantitative decision making: Numeric signals replace qualitative text analysis
  • Entropy-based confidence: Punishes uncertain splits more accurately than simple majority voting
  • Regime-specific thresholds: BUY/SELL thresholds adjust dynamically to market regime
  • Conflict awareness: Detects and resolves analyst disagreements systematically
  • Adaptive weights: Analyst weights update weekly from tracked prediction accuracy
  • 100% deterministic: Reproducible results for backtesting and validation
  • Zero LLM cost: Pure mathematical aggregation

Architecture

Signal Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                          Analyst Layer                                      │
├────────────────────────────────────────┬────────────────────────────────────┤
│  Core Analysts (always run)            │  Edge Analysts (optional)          │
│  Technical · Fundamentals · Sentiment  │  Sector · Insider · Options Flow   │
│  News · Market Context                 │  Short Interest · Earnings Revisions│
└────┬───────────────────────────────────┴──────────────────┬─────────────────┘
     │                                                       │
     ▼                                                       ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Signal Aggregator                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│  1. Extract numeric signals from analyst metrics                            │
│  2. Apply base weights: Tech 30%, Fund 40%, Sent 20%, News 10%             │
│     (edge weights added dynamically; core weights scaled proportionally)   │
│  3. Apply market regime modifier (Market Context: −50% to +0%)             │
│  4. Calculate unified score (−100 to +100)                                 │
│  5. Compute entropy-based confidence (agreement, data quality, VIX)        │
│  6. Resolve conflicts (fundamental vs technical divergence)                 │
│  7. Generate actionable recommendation: BUY/SELL/HOLD + confidence          │
└────────────────────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                          Risk Overlay                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│  - Receives: unified_score, confidence, recommendation, breakdown           │
│  - Adjusts position size based on confidence and market regime              │
│  - Final decision: BUY/SELL/HOLD with size allocation                       │
└─────────────────────────────────────────────────────────────────────────────┘

Signal Extraction

Technical Analyst Signals

Extracts signals from technical indicators and normalizes to −100/+100 scale:

  • RSI:
  • Overbought (>70) = −30
  • Oversold (<30) = +30
  • Neutral (30–70) = 0 (lean toward 0 to reduce false positives in trending markets)

  • RSI Divergence (rsi_divergence metric):

  • Detected bullish divergence = +30
  • Detected bearish divergence = −30

  • OBV Volume Confirmation (obv_trend metric):

  • OBV rising (volume confirms price trend) = +10
  • OBV falling (volume contradicts price trend) = −10

  • Multi-Timeframe Weekly Confirmation:

  • Weekly and daily trend aligned = ±10
  • Timeframe conflict = −5 penalty

  • MACD:

  • Bullish crossover (MACD > Signal) = +25
  • Bearish crossover (MACD < Signal) = −25

  • Trend (SMA relationships):

  • Strong uptrend (Close > SMA50 > SMA200) = +30
  • Strong downtrend (Close < SMA50 < SMA200) = −30

  • Candlestick Patterns:

  • Bullish patterns = +20 (capped)
  • Bearish patterns = −20 (capped)

Composite Technical Score: Average of normalized signals.

Fundamental Analyst Signals

Weighted combination of fundamental health and valuation metrics:

  • Piotroski F-Score (30% weight):
  • 0–9 continuous scale (each point maps linearly from −40 to +40)

  • Altman Z-Score (25% weight):

  • 2.99 (Safe) = +30

  • 1.81–2.99 (Gray zone) = 0
  • <1.81 (Distress) = −30

  • DCF Margin of Safety (30% weight):

  • 20% (Significantly undervalued) = +40

  • 0–20% (Moderately undervalued) = +10
  • <0% (Overvalued) = −40

  • P/E Ratio Valuation (15% weight):

  • <15 (Undervalued) = +30
  • 15–25 (Fair) = 0
  • 25 (Overvalued) = −30

Composite Fundamental Score: Weighted average (normalized to −100/+100).

Sentiment Analyst Signals

  • Overall Sentiment Score (80% weight): Normalized −1 to +1, scaled to −100/+100.
  • Positive/Negative Ratio (20% weight):
  • Ratio >2.0 = +20 bonus
  • Ratio <0.5 = −20 penalty

News Analyst Signals

Event impact scoring based on event type and severity:

  • High-Impact Events:
  • Merger/Acquisition = +30
  • Bankruptcy/Delisting = −50
  • FDA/Regulatory Approval = +40

  • Medium-Impact Events:

  • Partnership/Expansion = +15
  • Lawsuit/Investigation = −20

  • Earnings Surprise:

  • Beat estimates = +20 (capped)
  • Miss estimates = −20 (capped)

Composite News Score: Sum of event impacts, capped at ±80.

Sector Rotation Signal

Uses monthly relative performance of sector ETFs vs SPY to detect rotation:

  • Sector ETF outperforming SPY by >5% in current month = +20
  • Sector ETF underperforming SPY by >5% in current month = −20
  • Otherwise = proportional interpolation

Capped at ±20. Signal reflects whether the stock's sector is currently favored by the market.


Confidence Calculation

Confidence quantifies the reliability of the unified signal.

Formula

confidence = (entropy_agreement × (data_quality / 100) × vix_multiplier) × 100

Components

1. Entropy-Based Agreement (0–100)

Instead of simple majority voting, the aggregator computes a normalized information-entropy score that punishes uncertain splits more accurately:

# 4-0 split (all agree) → 100%
# 3-1 split             → ~60%
# 2-2 split (maximum uncertainty) → 0%

The entropy of the analyst vote distribution is computed, then inverted and normalized to 0–100. This prevents a slim majority from appearing high-confidence.

2. Data Quality Score (0–100)

Measures completeness of analyst data:

quality = 100.0
if missing_technical:   quality -= 15
if missing_fundamental: quality -= 20  # Highest penalty
if missing_sentiment:   quality -= 10
if missing_news:        quality -= 5   # Lowest penalty

3. VIX Multiplier (0.25–1.0) — Linear Interpolation

The VIX multiplier uses linear interpolation between breakpoints to avoid cliff effects:

VIX Multiplier
≤15 1.00
25 0.85
35 0.60
50 0.40
≥80 0.25

Values between breakpoints are linearly interpolated (e.g. VIX 30 → ~0.72).

4. Pre-Earnings Staleness Discount

When earnings are within 7 days (days_to_next_earnings < 7), confidence is reduced by 15%:

confidence × 0.85

This reflects increased uncertainty around earnings events.


BUY/SELL/HOLD Thresholds

Dynamic Threshold Adjustment

Rather than fixed ±25 thresholds, thresholds adjust to market regime to reduce false positives in trending markets and capture more opportunities in recovery phases:

Base threshold: 25.0

Condition Adjustment
Bull market +5.0 (harder to trigger BUY — fewer false positives)
Bear market −3.0 (easier to trigger BUY — capture recovery signals)
Elevated/Crisis VIX +5.0 (more cautious in volatile conditions)
Defensive sector (Utilities, Health Care, Consumer Staples, Real Estate) −3.0

Logic: 1. If confidence < 50% → return HOLD regardless of score 2. Apply dynamic threshold based on regime 3. Score > buy_thresholdBUY; score < −buy_thresholdSELL; otherwise → HOLD


Conflict Resolution

Detected Conflicts

1. Fundamental-Technical Divergence

Trigger: Fundamental score < −20 AND Technical score > 20

Resolution (favor_fundamental strategy): - Reduce technical weight by 50%, redistribute to fundamental - Rationale: Technical momentum is temporary; fundamental weakness persists

Original: Tech 30%, Fund 40%, Sent 20%, News 10%
Adjusted: Tech 15%, Fund 55%, Sent 20%, News 10%

2. Sentiment-Fundamental Divergence (Contrarian Signal)

Trigger: Sentiment score < −50 AND Fundamental score > 30

Resolution: - Increase fundamental weight by 20%, reduce sentiment weight by 50% - Rationale: Market overreaction creates buying opportunity for strong fundamentals

Original: Tech 30%, Fund 40%, Sent 20%, News 10%
Adjusted: Tech 30%, Fund 48%, Sent 10%, News 12% (normalized)

3. Crisis Regime Override

Trigger: Market Context regime = "Crisis"

Resolution: - Force recommendation to HOLD, confidence = 0% - Rationale: Systematic risk overrides idiosyncratic signals


Market Regime Modifiers

Regime VIX Level Multiplier Effect
Risk-On <25 1.0× No reduction
Elevated Volatility 25–35 0.85× Reduce by 15%
Risk-Off Variable 0.75× Reduce by 25%
Crisis >35 0.50× Reduce by 50%

Adaptive Weights

Analyst weights update automatically each week based on historical prediction accuracy, replacing the fixed defaults when performance data is available.

Mechanism

  1. WeightService (backend/services/weight_service.py) computes per-analyst accuracy over a rolling window of completed sessions with known outcomes.
  2. The AnalystWeight model (backend/database/models/analyst_weights.py) persists weights in the analyst_weights table.
  3. On startup (and weekly via APScheduler), signal_aggregator.update_weights() injects the latest weights.
# Update weights programmatically
aggregator.update_weights({
    "technical":   0.28,
    "fundamental": 0.42,
    "sentiment":   0.18,
    "news":        0.12,
})

Weights are auto-normalized to sum to 1.0. If no adaptive weights are available, the system falls back to defaults (Tech 30%, Fund 40%, Sent 20%, News 10%).

Scheduler

A weekly job runs every Sunday at 02:00 ET to recalculate and persist adaptive weights:

scheduler.add_job(adaptive_weight_update, trigger="cron", day_of_week="sun", hour=2)

Earnings Proximity Effects

When earnings are approaching, the system applies conservative adjustments:

Condition Effect
days_to_next_earnings < 7 −15% confidence reduction
days_to_next_earnings < 7 Position capped at 50% of normal size (via Risk Overlay)

The near_earnings flag and earnings_surprise_pct outcome are tracked per signal for adaptive learning.


Backtesting

Features

  • Historical signal validation: Test signals on past market data
  • Trade simulation: Entry/exit price tracking with stop-loss and take-profit
  • Performance metrics: Win rate, Sharpe ratio, max drawdown, profit factor
  • MAE/MFE per trade: Maximum Adverse/Favorable Excursion analysis
  • Walk-forward validation: Sliding train/test windows for out-of-sample testing
  • Weight optimization: Grid search over analyst weight combinations

Walk-Forward Validation

results = engine.run_walk_forward(
    symbols=["AAPL", "MSFT", "GOOGL"],
    start_date="2022-01-01",
    end_date="2024-01-01",
    train_window_days=365,
    test_window_days=90,
)
# Returns: folds, oos_sharpe, oos_win_rate, n_folds, overfitting_ratio

MAE/MFE Analysis

Each trade tracks: - MAE (Maximum Adverse Excursion): Worst unrealized loss % during holding period - MFE (Maximum Favorable Excursion): Best unrealized gain % during holding period - mae_day / mfe_day: Trading day within the holding period at which extremes occurred - optimal_entry_delay_days: Suggested entry delay to improve average fill

Usage

from backend.services.backtesting import BacktestEngine
from backend.orchestration.signal_aggregator import SignalAggregator
from backend.config.settings import DEFAULT_CONFIG

aggregator = SignalAggregator(DEFAULT_CONFIG)
engine = BacktestEngine(aggregator, DEFAULT_CONFIG)

results = engine.run_backtest(
    symbols=["AAPL", "MSFT", "GOOGL", "TSLA"],
    start_date="2023-01-01",
    end_date="2024-01-01",
    analyst_data_fetcher=my_data_fetcher,
)

print(f"Win Rate: {results['win_rate']:.1f}%")
print(f"Sharpe Ratio: {results['sharpe_ratio']:.2f}")
print(f"Max Drawdown: {results['max_drawdown']:.1f}%")

Unified Signal Structure

Output Format

{
    "unified_score": 42.5,          # −100 to +100
    "recommendation": "BUY",         # BUY/SELL/HOLD
    "confidence": 78.3,              # 0 to 100
    "breakdown": {
        "technical": 35.0,           # Per-analyst contribution
        "fundamental": 50.0,
        "sentiment": 40.0,
        "news": 25.0
    },
    "adjusted_weights": {
        "technical": 0.30,           # After conflict resolution
        "fundamental": 0.40,
        "sentiment": 0.20,
        "news": 0.10
    },
    "regime_modifier": 1.0,          # Market context multiplier
    "conflicts": [],                 # Detected conflicts (empty = none)
    "data_quality": 100.0            # 0 to 100
}

Integration with Risk Overlay

The Risk Overlay receives the unified signal and uses it for position sizing:

  • High confidence (>75%): standard position size
  • Medium confidence (50–75%): 75% position size
  • Low confidence (<50%): 50% position size or HOLD
  • Crisis regime: maximum 50% position size override
  • Near-earnings (near_earnings=True): position capped at 50%

For the full Risk Overlay logic, see Architecture.


Performance Monitoring

Prometheus metrics: - redhound_signal_aggregation_seconds — Processing latency - redhound_signal_confidence — Per-signal confidence gauge - redhound_analyst_agreement_percentage — Analyst agreement rate - redhound_signal_conflicts_total — Conflict detection counter - redhound_unified_signals_total — Total signals generated - redhound_backtest_win_rate_percentage — Backtest win rate - redhound_backtest_sharpe_ratio — Backtest Sharpe ratio


API Reference

SignalAggregator Class

class SignalAggregator:
    def __init__(self, config: dict | None = None)

    def aggregate(
        self,
        technical_metrics: dict | None = None,
        fundamental_metrics: dict | None = None,
        sentiment_metrics: dict | None = None,
        news_metrics: dict | None = None,
        market_context_metrics: dict | None = None,
    ) -> dict[str, Any]

    def update_weights(self, weights: dict[str, float]) -> None
    """Inject adaptive weights (auto-normalized to sum=1.0)."""

BacktestEngine Class

class BacktestEngine:
    def __init__(self, signal_aggregator, config: dict | None = None)

    def run_backtest(
        self,
        symbols: list[str],
        start_date: str,
        end_date: str,
        analyst_data_fetcher: callable | None = None,
    ) -> dict[str, Any]

    def run_walk_forward(
        self,
        symbols: list[str],
        start_date: str,
        end_date: str,
        train_window_days: int = 365,
        test_window_days: int = 90,
        data_fetcher: callable | None = None,
        progress_callback: callable | None = None,
    ) -> dict[str, Any]

    def optimize_weights(
        self,
        symbols: list[str],
        train_start: str,
        train_end: str,
        analyst_data_fetcher: callable | None = None,
    ) -> dict[str, Any]

Node Factory

def create_signal_aggregator_node(config: dict | None = None) -> Callable

Configuration

"signal_aggregation": {
    "enabled": True,
    "weights": {"technical": 0.30, "fundamental": 0.40, "sentiment": 0.20, "news": 0.10},
    "regime_modifiers": {"Crisis": 0.50, "Risk-Off": 0.75, "Elevated": 0.85, "Risk-On": 1.0},
    "confidence_thresholds": {"high": 80.0, "medium": 50.0, "low": 0.0},
    "conflict_resolution": {"fundamental_vs_technical": "favor_fundamental"},
    "adaptive_weights_enabled": True,
}

See Configuration Reference for environment variable names.


Troubleshooting

Low Confidence Scores

Possible causes: 1. Analyst disagreement (divergent signals) — check breakdown field 2. Missing metrics (incomplete data) — check data_quality field 3. High VIX — VIX multiplier linearly reduces confidence above VIX 15 4. Pre-earnings staleness discount active

Solutions: - Verify all analysts return complete metrics - Review conflicts field for systematic divergences - Check VIX level and whether earnings are imminent

Unexpected Recommendations

Possible causes: 1. Conflict resolution overriding adjusted weights 2. Regime modifier reducing score below dynamic threshold 3. Low confidence (<50%) forcing HOLD

Solutions: - Review breakdown and adjusted_weights to see conflict adjustments - Check regime_modifier for market context effects - Review confidence breakdown (entropy agreement, data quality, VIX)

Poor Backtest Performance

Possible causes: 1. Suboptimal weights for the historical period 2. Overfitting to in-sample data (use walk-forward validation) 3. Data quality issues

Solutions: - Use run_walk_forward() to assess out-of-sample performance - Run optimize_weights() on a training period, validate on held-out data - Check overfitting_ratio in walk-forward results (ideal < 1.5)