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_divergencemetric): - Detected bullish divergence = +30
-
Detected bearish divergence = −30
-
OBV Volume Confirmation (
obv_trendmetric): - 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¶
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:
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%:
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_threshold → BUY; score < −buy_threshold → SELL; 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
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¶
WeightService(backend/services/weight_service.py) computes per-analyst accuracy over a rolling window of completed sessions with known outcomes.- The
AnalystWeightmodel (backend/database/models/analyst_weights.py) persists weights in theanalyst_weightstable. - 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:
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¶
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)
Related Documentation¶
- Configuration: Complete configuration reference
- Technical Indicators: Technical analyst signals
- Architecture: System architecture and data flow
- Monitoring & Metrics: Prometheus metrics and Grafana dashboards