# Design Document — Signal Math Upgrade

## Overview

This design upgrades the Stonks Oracle signal processing pipeline from deterministic heuristic formulas to a probabilistic, regime-aware, and adaptive mathematical framework. The upgrade spans all pipeline stages — signal scoring, trend assembly, macro impact, competitive signals, trend projection, and recommendation generation — while preserving the existing `WeightedSignal` abstraction, three-layer architecture, database schema, and dataclass interfaces.

The core transformation replaces:
- **Binary confidence gate** → smooth sigmoid transition
- **Weighted sentiment average** → Bayesian log-likelihood accumulation with Beta posterior
- **Fixed recency decay** → adaptive event-specific half-lives
- **Linear macro exposure** → multiplicative compounding exposure
- **Additive macro integration** → conditional multiplicative modifiers
- **Simple contradiction ratio** → weighted disagreement entropy
- **Heuristic trend confidence** → Bayesian posterior variance
- **Threshold-based direction** → entropy-based mixed signal detection
- **Simple momentum** → exponentially weighted momentum with volatility scaling
- **Confidence/strength gates** → expected value recommendation gate
- **Fixed relationship transfer** → graph-distance attenuated competitive signals

All changes are gated behind a `probabilistic_scoring_enabled` feature flag in `risk_configs`, allowing incremental rollout with instant rollback. New outputs (P_bull, α, β, entropy, regime, EV) are stored in existing JSONB columns — no database migrations required.

### Design Rationale

Markets are fundamentally probabilistic and regime-dependent. The current pipeline collapses rich evidence into binary sentiment labels and fixed-weight averages, losing uncertainty structure. A Bayesian framework preserves the full posterior distribution, enabling the system to distinguish between "strongly bullish" and "weakly bullish with high uncertainty" — a distinction that directly impacts position sizing and risk management.

The regime detector adapts scoring thresholds to market conditions (panic vs. trending vs. mean-reverting), and the expected value gate ensures recommendations only proceed when the risk-adjusted outcome is positive. Together, these changes transform the pipeline from a sentiment aggregator into a probabilistic forecasting engine.

---

## Architecture

### High-Level Pipeline Flow

The upgraded pipeline maintains the existing three-layer architecture but introduces new computation stages within each layer. The feature flag controls which computation path is taken at each stage.

```mermaid
flowchart TD
    subgraph "Layer 1: Company Signals"
        A[Document Intelligence Records] --> B[Signal Scorer]
        B --> |"probabilistic=false"| C1[Binary Gate + Fixed Decay]
        B --> |"probabilistic=true"| C2[Sigmoid Gate + Adaptive Decay<br/>+ Info Gain + Source Accuracy]
        C1 --> D[WeightedSignal list]
        C2 --> D
    end

    subgraph "Layer 2: Macro Signals"
        E[Global Events] --> F[Macro Scorer]
        F --> |"probabilistic=false"| G1[Linear Weighted Sum]
        F --> |"probabilistic=true"| G2[Multiplicative Exposure]
        G1 --> H[Macro WeightedSignals]
        G2 --> H
    end

    subgraph "Layer 3: Competitive Signals"
        I[Pattern Matcher] --> J[Signal Propagation]
        J --> |"probabilistic=false"| K1[Flat Transfer Strength]
        J --> |"probabilistic=true"| K2[Graph-Distance Attenuation]
        K1 --> L[Competitive WeightedSignals]
        K2 --> L
    end

    subgraph "Regime Detection (new)"
        M[Market Data] --> N[Regime Detector]
        N --> O{Regime Classification}
        O --> P[trend-following / panic / mean-reversion / uncertainty]
    end

    subgraph "Trend Assembly"
        D --> Q[Merge Signals]
        H --> |"probabilistic=false"| Q
        H --> |"probabilistic=true"| R[Conditional Macro Modifier]
        R --> Q
        L --> Q
        Q --> S[Trend Assembler]
        S --> |"probabilistic=false"| T1[Heuristic Confidence + Threshold Direction]
        S --> |"probabilistic=true"| T2[Bayesian Posterior + Entropy Direction<br/>+ Regime-Adjusted Thresholds]
        P --> T2
        T1 --> U[TrendSummary]
        T2 --> U
    end

    subgraph "Projection"
        U --> V[Projection Engine]
        V --> |"probabilistic=false"| W1[Simple Momentum]
        V --> |"probabilistic=true"| W2[EW Momentum + Vol Scaling]
        W1 --> X[TrendProjection]
        W2 --> X
    end

    subgraph "Recommendation"
        U --> Y[Recommendation Engine]
        X --> Y
        Y --> |"probabilistic=false"| Z1[Confidence + Strength Gates]
        Y --> |"probabilistic=true"| Z2[EV Gate + Existing Gates]
        Z1 --> AA[Recommendation]
        Z2 --> AA
    end
```

### Feature Flag Control Flow

The feature flag `probabilistic_scoring_enabled` is read from the `risk_configs` table's `config` JSONB column at the start of each aggregation cycle. It propagates through all pipeline stages via the existing `AggregationConfig` dataclass.

```mermaid
sequenceDiagram
    participant W as Worker (aggregate_company)
    participant DB as PostgreSQL (risk_configs)
    participant S as Signal Scorer
    participant T as Trend Assembler
    participant R as Recommendation Engine

    W->>DB: SELECT config FROM risk_configs WHERE active=TRUE
    DB-->>W: {"macro_enabled": true, "competitive_enabled": true, "probabilistic_scoring_enabled": false}
    W->>W: Log pipeline mode (heuristic or probabilistic)
    W->>S: compute_signal_weight(..., probabilistic=flag)
    S-->>W: WeightedSignal (with or without Bayesian fields)
    W->>T: assemble_trend_summary(..., probabilistic=flag)
    T-->>W: TrendSummary (with or without entropy/regime)
    W->>R: evaluate_eligibility(..., probabilistic=flag)
    R-->>W: Recommendation (with or without EV gate)
```

---

## Components and Interfaces

### New Modules

| Module | File | Responsibility |
|--------|------|----------------|
| Bayesian Accumulator | `services/aggregation/bayesian.py` | Log-likelihood accumulation, Beta posterior, P_bull, Bayesian confidence |
| Regime Detector | `services/aggregation/regime.py` | EMA computation, volatility ratio, regime classification, threshold adjustment |
| Adaptive Decay | integrated into `scoring.py` | Event-specific half-life computation from impact, surprise, market reaction |
| Information Gain | integrated into `scoring.py` | Surprise weighting from event type base rates |
| Source Accuracy | `services/aggregation/source_accuracy.py` | Historical prediction accuracy tracking per source |
| Entropy Detector | integrated into `bayesian.py` | Shannon entropy for mixed signal detection |
| EV Gate | integrated into `eligibility.py` | Expected value computation for recommendation eligibility |

### Modified Modules

| Module | File | Changes |
|--------|------|---------|
| Signal Scorer | `services/aggregation/scoring.py` | Sigmoid gate, info gain factor, adaptive decay, regime multiplier, source accuracy factor |
| Trend Assembler | `services/aggregation/worker.py` | Bayesian confidence, entropy-based direction, regime-adjusted thresholds, entropy-based contradiction |
| Contradiction | `services/aggregation/contradiction.py` | Weighted disagreement entropy replacing minority/majority ratio |
| Macro Scorer | `services/aggregation/interpolation.py` | Multiplicative exposure formula, conditional integration mode |
| Competitive Scorer | `services/aggregation/signal_propagation.py` | Graph-distance attenuation with historical correlation |
| Projection Engine | `services/aggregation/projection.py` | Exponentially weighted momentum, volatility scaling |
| Recommendation | `services/recommendation/eligibility.py` | EV gate, P_bull-based position sizing adjustments |
| Config | `services/shared/config.py` | New probabilistic config parameters |
| Schemas | `services/shared/schemas.py` | Optional new fields on TrendSummary, Recommendation |

### Component Interface Details

#### 1. Bayesian Accumulator (`services/aggregation/bayesian.py`)

```python
@dataclass(frozen=True)
class BayesianPosterior:
    """Bayesian posterior state from signal accumulation."""
    p_bull: float          # σ(L_t), bullish probability [0, 1]
    alpha: float           # Beta distribution α parameter (≥ 1.0)
    beta: float            # Beta distribution β parameter (≥ 1.0)
    log_likelihood: float  # Raw log-likelihood accumulation L_t
    bayesian_confidence: float  # 1 - 4αβ/(α+β)², [0, 1]
    entropy: float         # Shannon entropy H, [0, 1]
    signal_count: int      # Number of signals processed

    # Uninformative prior (no evidence)
    PRIOR = BayesianPosterior(
        p_bull=0.5, alpha=1.0, beta=1.0,
        log_likelihood=0.0, bayesian_confidence=0.0,
        entropy=1.0, signal_count=0,
    )


def compute_bayesian_posterior(
    signals: list[WeightedSignal],
) -> BayesianPosterior:
    """Accumulate weighted signals into a Bayesian posterior.

    Computes:
    - Log-likelihood: L_t = Σ(w_i · s_i)
    - Bullish probability: P_bull = σ(L_t)
    - Beta posterior: α = 1 + W_bull, β = 1 + W_bear
    - Bayesian confidence: C = 1 - 4αβ/(α+β)²
    - Shannon entropy: H = -p·log₂(p) - (1-p)·log₂(1-p)
    """
    ...


def compute_entropy(p_bull: float) -> float:
    """Shannon entropy H = -p·log₂(p) - (1-p)·log₂(1-p).

    Returns value in [0, 1]. Maximum at p=0.5, zero at p=0 or p=1.
    Handles edge cases p=0 and p=1 by returning 0.0.
    """
    ...
```

#### 2. Regime Detector (`services/aggregation/regime.py`)

```python
class MarketRegime(str, Enum):
    TREND_FOLLOWING = "trend_following"
    PANIC = "panic"
    MEAN_REVERSION = "mean_reversion"
    UNCERTAINTY = "uncertainty"


@dataclass(frozen=True)
class RegimeClassification:
    """Result of regime detection for a ticker."""
    regime: MarketRegime
    trend_indicator: float      # R = sign(EMA_20 - EMA_100)
    volatility_ratio: float     # V_r = σ_20 / σ_100
    bullish_threshold: float    # Adjusted ±threshold for direction
    bearish_threshold: float
    contradiction_penalty_multiplier: float  # 0.4 default, 0.6 for uncertainty


@dataclass(frozen=True)
class RegimeConfig:
    ema_short_period: int = 20
    ema_long_period: int = 100
    vol_short_period: int = 20
    vol_long_period: int = 100
    panic_vol_ratio: float = 1.5
    trend_vol_ratio: float = 1.2
    mean_reversion_vol_ratio: float = 1.0
    default_threshold: float = 0.15
    panic_threshold: float = 0.10
    mean_reversion_threshold: float = 0.20
    uncertainty_contradiction_multiplier: float = 0.6


def classify_regime(
    closing_prices: list[float],
    returns: list[float],
    config: RegimeConfig = RegimeConfig(),
) -> RegimeClassification:
    """Classify market regime from price and return history.

    Requires at least 100 days of price history for EMA_100.
    Falls back to UNCERTAINTY when data is insufficient.
    """
    ...


def compute_ema(values: list[float], period: int) -> float:
    """Compute exponential moving average over the last `period` values."""
    ...
```

#### 3. Source Accuracy Tracker (`services/aggregation/source_accuracy.py`)

```python
@dataclass
class SourceAccuracy:
    """Per-source historical prediction accuracy."""
    source_id: str
    accuracy_ratio: float    # [0, 1] fraction of correct directional calls
    sample_count: int        # Number of signals with known outcomes
    last_updated: datetime

    @property
    def accuracy_factor(self) -> float:
        """Multiplicative factor for credibility weight.

        Returns 1.0 (neutral) when sample_count < 10.
        Otherwise scales linearly from 0.5 (0% accuracy) to 1.5 (100% accuracy).
        """
        if self.sample_count < 10:
            return 1.0
        return 0.5 + self.accuracy_ratio


async def fetch_source_accuracy(
    pool: asyncpg.Pool,
    source_ids: list[str],
) -> dict[str, SourceAccuracy]:
    """Fetch accuracy metrics for a batch of sources."""
    ...


async def update_source_accuracy(
    pool: asyncpg.Pool,
    source_id: str,
    realized_outcomes: list[tuple[str, float]],  # (predicted_direction, actual_7d_return)
) -> None:
    """Update accuracy metrics for a source based on realized price data."""
    ...
```

#### 4. Extended ScoringConfig

New fields added to the existing `ScoringConfig` dataclass in `scoring.py`:

```python
@dataclass(frozen=True)
class ScoringConfig:
    # ... existing fields preserved ...

    # Probabilistic scoring toggle (mirrors feature flag for local use)
    probabilistic: bool = False

    # Sigmoid gate parameters
    sigmoid_steepness: float = 5.0    # k in σ(k·(x - midpoint))
    sigmoid_midpoint: float = 0.5     # midpoint of sigmoid transition

    # Information gain parameters
    info_gain_lambda: float = 0.3     # scaling parameter λ
    info_gain_max: float = 3.0        # maximum clamp for info gain factor
    default_base_rate: float = 0.1    # fallback when event type rate unknown

    # Adaptive decay parameters (β scaling factors)
    adaptive_decay_impact_scale: float = 1.0   # max β_impact
    adaptive_decay_surprise_scale: float = 1.0 # max β_surprise at r=3.0
    adaptive_decay_market_scale: float = 0.5   # max β_market_reaction

    # Regime multiplier parameters
    regime_return_weight: float = 0.15   # coefficient for |z_r|
    regime_volume_weight: float = 0.10   # coefficient for |z_v|
    regime_multiplier_max: float = 2.5   # M_regime ceiling
```

#### 5. Extended WeightedSignal

The existing `WeightedSignal` dataclass gains optional fields:

```python
@dataclass
class WeightedSignal:
    """A document intelligence reference paired with its computed weight."""
    document_id: str
    weight: SignalWeight
    sentiment_value: float
    impact_score: float

    # New optional fields for probabilistic mode
    info_gain_factor: float = 1.0       # r = 1 + λ·(-log₂ P(event_type))
    source_accuracy_factor: float = 1.0 # [0.5, 1.5] from historical accuracy
    adaptive_half_life: float | None = None  # τ_i when adaptive decay is active
```

#### 6. Extended SignalWeight

```python
@dataclass
class SignalWeight:
    """Breakdown of a document's aggregation weight."""
    recency: float
    credibility: float
    novelty_bonus: float
    confidence_gate: float
    market_ctx_multiplier: float
    combined: float

    # New optional fields for probabilistic mode
    sigmoid_gate: float | None = None        # Smooth gate value [0, 1]
    info_gain_factor: float = 1.0            # Surprise multiplier
    source_accuracy_factor: float = 1.0      # Historical accuracy multiplier
    regime_multiplier: float | None = None   # M_regime replacing M_context
```

#### 7. Extended TrendSummary

New optional fields on the existing Pydantic model:

```python
class TrendSummary(BaseModel):
    # ... all existing fields preserved ...

    # New optional fields for probabilistic mode
    p_bull: float | None = None              # Bayesian bullish probability
    alpha: float | None = None               # Beta posterior α
    beta_param: float | None = None          # Beta posterior β (named to avoid shadowing)
    bayesian_confidence: float | None = None # 1 - 4αβ/(α+β)²
    entropy: float | None = None             # Shannon entropy H
    regime: str | None = None                # Market regime classification
    pipeline_mode: str = "heuristic"         # "heuristic" or "probabilistic"
```

#### 8. Extended Recommendation

```python
class Recommendation(BaseModel):
    # ... all existing fields preserved ...

    # New optional fields for probabilistic mode
    expected_value: float | None = None      # EV = P_bull·R_up - P_bear·R_down
    p_bull: float | None = None              # Bayesian bullish probability used
    pipeline_mode: str = "heuristic"         # "heuristic" or "probabilistic"
```

---

## Data Models

### Database Storage Strategy

All new mathematical outputs are stored in existing JSONB columns. No new database migrations are required.

#### trend_windows table

The `market_context` JSONB column (currently stores volatility/volume data) is extended to include probabilistic outputs:

```json
{
    "volatility": 1.23,
    "volume_change_pct": 45.2,
    "price_change_pct": -2.1,
    "probabilistic": {
        "p_bull": 0.72,
        "alpha": 8.3,
        "beta": 3.1,
        "log_likelihood": 0.94,
        "bayesian_confidence": 0.61,
        "entropy": 0.42,
        "regime": "trend_following",
        "regime_volatility_ratio": 0.85,
        "pipeline_mode": "probabilistic",
        "contradiction_entropy": 0.31,
        "macro_modifier": 1.15
    }
}
```

#### recommendations table

The existing `invalidation_conditions` JSONB column stores recommendation-level data. The new EV and probabilistic fields are stored in a new key within the existing decision trace flow. Since recommendations don't have a dedicated metadata JSONB column, we add the probabilistic fields to the thesis text and store structured data in the `risk_checks` JSONB column of the `recommendation_evaluations` table:

```json
{
    "ev": 0.0082,
    "p_bull": 0.72,
    "r_up": 0.034,
    "r_down": 0.012,
    "pipeline_mode": "probabilistic",
    "ev_threshold": 0.005
}
```

#### risk_configs table

The `config` JSONB column gains the new feature flag:

```json
{
    "macro_enabled": true,
    "competitive_enabled": true,
    "probabilistic_scoring_enabled": false
}
```

#### source_accuracy table (new — Requirement 4)

This is the one new database table required, stored via a migration:

```sql
CREATE TABLE IF NOT EXISTS source_accuracy (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    source_id VARCHAR(200) NOT NULL,
    accuracy_ratio FLOAT NOT NULL DEFAULT 0.5,
    sample_count INTEGER NOT NULL DEFAULT 0,
    last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(source_id)
);
CREATE INDEX idx_source_accuracy_source ON source_accuracy(source_id);
```

Note: This is the only schema addition. All other new outputs use existing JSONB columns.

### Event Type Base Rates

Information gain computation requires empirical base rates for event types. These are stored as a configuration constant (not in the database) and can be tuned over time:

```python
EVENT_TYPE_BASE_RATES: dict[str, float] = {
    "earnings": 0.25,        # Quarterly, common
    "product_launch": 0.10,  # Moderately rare
    "regulatory": 0.08,      # Somewhat rare
    "legal": 0.05,           # Rare
    "m_and_a": 0.03,         # Very rare
    "management_change": 0.06,
    "partnership": 0.12,
    "market_expansion": 0.09,
    "restructuring": 0.04,
    "dividend": 0.15,
}
DEFAULT_BASE_RATE = 0.1  # For unknown event types
```

### Configuration Hierarchy

```
risk_configs.config (DB, runtime)
    └── probabilistic_scoring_enabled: bool
        └── AggregationConfig.probabilistic: bool (in-memory)
            └── ScoringConfig.probabilistic: bool (per-cycle)
                ├── scoring.py: sigmoid vs binary gate
                ├── scoring.py: adaptive vs fixed decay
                ├── scoring.py: info gain factor
                ├── scoring.py: regime multiplier vs market context
                ├── worker.py: Bayesian vs heuristic confidence
                ├── worker.py: entropy vs threshold direction
                ├── contradiction.py: entropy vs ratio
                ├── interpolation.py: multiplicative vs linear
                ├── signal_propagation.py: graph-distance vs flat
                ├── projection.py: EW momentum vs simple
                └── eligibility.py: EV gate vs threshold-only
```


---

## Correctness Properties

*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*

The following properties were derived from the acceptance criteria through systematic prework analysis. Each property is universally quantified and maps to specific requirements. Redundant properties were consolidated during reflection (e.g., requirements 17.1–17.7 duplicate properties already stated in requirements 1–15).

### Property 1: Sigmoid Gate Monotonicity

*For any* two extraction confidence values x₁, x₂ ∈ [0.0, 1.0] where x₁ ≤ x₂, the sigmoid gate σ(5·(x₁ - 0.5)) SHALL be less than or equal to σ(5·(x₂ - 0.5)). Higher confidence always produces equal or higher gate values.

**Validates: Requirements 2.6, 17.1**

### Property 2: Beta Posterior Evidence Accumulation

*For any* sequence of weighted signal sets where each successive set contains one additional signal, the sum α + β of the Beta posterior parameters SHALL increase monotonically. Evidence always accumulates — adding a signal never reduces the total evidence mass.

**Validates: Requirements 1.3, 17.2**

### Property 3: Bayesian Confidence Symmetry and Divergence

*For any* Beta posterior with parameters α, β ≥ 1.0, the Bayesian confidence C = 1 - 4αβ/(α+β)² SHALL equal 0.0 when α = β (maximum uncertainty) and SHALL increase monotonically as the ratio max(α/β, β/α) increases. Confidence reflects evidence concentration, not evidence volume.

**Validates: Requirements 1.4, 17.3**

### Property 4: Bayesian Posterior Round-Trip Consistency

*For any* set of weighted signals with uniform weights, computing the Beta posterior and extracting the mean P_bull = α/(α+β) SHALL produce a value within 0.05 of σ(L_t) where L_t is the log-likelihood accumulation. The two probabilistic representations are consistent.

**Validates: Requirements 1.7, 17.7**

### Property 5: Adaptive Decay Lower Bound

*For any* valid combination of impact_score ∈ [0, 1], information gain factor r ∈ [1.0, 3.0], and market context multiplier ∈ [1.0, 1.45], the adaptive half-life τ_i SHALL be greater than or equal to the base half-life τ_base. Adaptive decay is always slower or equal to fixed decay, never faster.

**Validates: Requirements 5.7, 17.4**

### Property 6: Information Gain Monotonicity

*For any* two event type base rates p₁, p₂ ∈ (0, 1] where p₁ < p₂, the information gain factor r(p₁) SHALL be greater than or equal to r(p₂). Rarer events always receive higher surprise weight.

**Validates: Requirements 3.5**

### Property 7: Multiplicative Macro Exposure Monotonicity

*For any* overlap configuration (O_geo, O_supply, O_commodity, O_sector) and any dimension k where O_k = 0, setting O_k to any positive value SHALL increase the total macro impact score. Multi-dimensional exposure always compounds — it never reduces impact.

**Validates: Requirements 10.7, 17.5**

### Property 8: Shannon Entropy Range and Maximum

*For any* bullish probability P_bull ∈ (0, 1), the Shannon entropy H = -P_bull·log₂(P_bull) - (1-P_bull)·log₂(1-P_bull) SHALL be in the range (0, 1], with the maximum value of 1.0 occurring at P_bull = 0.5.

**Validates: Requirements 9.7**

### Property 9: Contradiction Entropy Monotonicity

*For any* set of weighted signals containing both positive and negative sentiment signals, the contradiction entropy score SHALL increase monotonically as the weight distribution f_pos approaches 0.5 (equal split). More balanced disagreement always produces higher contradiction.

**Validates: Requirements 15.7**

### Property 10: Exponentially Weighted Momentum Direction

*For any* sequence of monotonically increasing signed trend strengths (each ΔS_{t-k} > 0), the exponentially weighted momentum M_t SHALL be positive. Consistently strengthening bullish trends always produce positive momentum.

**Validates: Requirements 13.6, 17.6**

### Property 11: Competitive Signal Distance Attenuation

*For any* source-target company pair with fixed source signal strength S_source and historical correlation ρ_historical, the transfer strength S_transfer SHALL decrease monotonically with increasing graph distance d_network. Closer competitors always receive stronger signal transfer.

**Validates: Requirements 12.7**

### Property 12: Expected Value Directional Consistency

*For any* Bayesian bullish probability P_bull > 0.5 and estimated returns where R_up > R_down, the expected value EV = P_bull · R_up - (1 - P_bull) · R_down SHALL be positive. When the model is bullish and upside exceeds downside, EV is always positive.

**Validates: Requirements 17.8**

### Property 13: Bayesian Confidence Monotonic with Agreeing Signals

*For any* set of weighted signals where all signals agree on direction (all positive or all negative), adding one more agreeing signal SHALL increase the Bayesian confidence C. More agreeing evidence always increases confidence.

**Validates: Requirements 8.6**

### Property 14: Numerical Stability Across All Formulas

*For any* valid input combination to any formula in the probabilistic pipeline (sigmoid gate, Beta posterior, Bayesian confidence, adaptive decay, regime multiplier, Shannon entropy, multiplicative exposure, EW momentum, expected value), the output SHALL be a finite float (not NaN, not infinity) within the documented range for that formula. This includes regime multiplier M_regime ∈ [1.0, 2.5], entropy H ∈ [0, 1], P_bull ∈ [0, 1], confidence ∈ [0, 1], and M_adj ∈ [-2.0, 2.0].

**Validates: Requirements 17.9, 6.4**

---

## Error Handling

### Numerical Edge Cases

| Scenario | Handling |
|----------|----------|
| P_bull = 0.0 or 1.0 (entropy undefined) | Return H = 0.0 (no uncertainty at extremes) |
| σ_20 = 0.0 (zero volatility for momentum scaling) | Use floor max(σ_20, 0.01) per Req 13.4 |
| σ_20 = 0.0 or σ_100 = 0.0 (volatility ratio) | Default to uncertainty regime |
| log₂(0) in entropy computation | Guard with `if p <= 0 or p >= 1: return 0.0` |
| log₂(0) in information gain (base_rate = 0) | Base rates must be > 0; use default 0.1 for unknown |
| Division by zero in z-score (σ = 0) | Use M_regime = 1.0 when σ = 0 |
| Empty signal list | Return uninformative prior (P_bull=0.5, α=1, β=1, C=0) |
| All neutral signals (no positive or negative) | Contradiction = 0.0, direction = neutral |
| Extremely large weights (overflow risk) | Python floats handle up to ~1.8e308; clamp combined weight if needed |
| NaN from upstream data | Validate inputs; skip signals with NaN weight or sentiment |

### Feature Flag Failure Modes

| Failure | Behavior |
|---------|----------|
| `risk_configs` table unreachable | Default to `probabilistic_scoring_enabled = false` (heuristic mode) |
| `config` JSONB missing the key | Default to `false` |
| Invalid value type for flag | Default to `false`, log warning |
| Flag changes mid-cycle | Flag is read once at cycle start; change takes effect next cycle |

### Source Accuracy Failures

| Failure | Behavior |
|---------|----------|
| `source_accuracy` table unreachable | Use neutral factor 1.0 for all sources |
| Accuracy update fails | Log error, continue with stale accuracy data |
| Corrupted accuracy data (ratio > 1.0 or < 0.0) | Clamp to [0.0, 1.0] |

### Regime Detection Failures

| Failure | Behavior |
|---------|----------|
| Market data unavailable | Default to uncertainty regime with default thresholds |
| Insufficient price history (< 100 days) | Default to uncertainty regime |
| Price data contains gaps | Use available data; EMA computation handles gaps gracefully |

---

## Testing Strategy

### Dual Testing Approach

The signal math upgrade requires both property-based tests (for mathematical correctness) and example-based unit tests (for specific behaviors and integration points). Property-based testing is highly appropriate here because the feature consists primarily of pure mathematical functions with clear input/output behavior, universal properties that hold across wide input spaces, and well-defined range invariants.

### Property-Based Testing

**Library:** Hypothesis (already in use per `.hypothesis/` directory and project conventions)

**Configuration:**
- Minimum 100 iterations per property: `@settings(max_examples=100)`
- File naming: `test_pbt_signal_math.py` (or split by module)
- Tag format: `# Feature: signal-math-upgrade, Property N: <title>`

**Property tests to implement (one test per correctness property):**

| Property | Test File | Key Generators |
|----------|-----------|----------------|
| 1: Sigmoid monotonicity | `test_pbt_signal_math.py` | `st.floats(0.0, 1.0)` pairs |
| 2: Evidence accumulation | `test_pbt_signal_math.py` | `st.lists(weighted_signal_strategy)` |
| 3: Confidence symmetry/divergence | `test_pbt_signal_math.py` | `st.floats(1.0, 100.0)` for α, β |
| 4: Posterior round-trip | `test_pbt_signal_math.py` | `st.lists(uniform_weight_signal_strategy)` |
| 5: Adaptive decay lower bound | `test_pbt_signal_math.py` | `st.floats` for impact, surprise, market |
| 6: Info gain monotonicity | `test_pbt_signal_math.py` | `st.floats(0.001, 1.0)` pairs |
| 7: Macro exposure monotonicity | `test_pbt_signal_math.py` | `st.floats(0.0, 1.0)` for overlaps |
| 8: Entropy range/maximum | `test_pbt_signal_math.py` | `st.floats(0.001, 0.999)` for P_bull |
| 9: Contradiction monotonicity | `test_pbt_signal_math.py` | Signal sets with varying weight splits |
| 10: EW momentum direction | `test_pbt_signal_math.py` | `st.lists(st.floats)` monotonic sequences |
| 11: Distance attenuation | `test_pbt_signal_math.py` | `st.integers(1, 3)` for distance |
| 12: EV directional consistency | `test_pbt_signal_math.py` | `st.floats(0.5, 1.0)` for P_bull |
| 13: Confidence with agreeing signals | `test_pbt_signal_math.py` | Growing lists of same-direction signals |
| 14: Numerical stability | `test_pbt_signal_math.py` | Broad `st.floats` for all formula inputs |

### Example-Based Unit Tests

**File:** `test_signal_math_unit.py`

| Test Area | Examples |
|-----------|----------|
| Sigmoid gate specific values | x=0.5→0.5, x=0.2→<0.05, x=0.8→>0.95 |
| Uninformative prior | Empty signals → P_bull=0.5, α=1, β=1, C=0 |
| Default base rate | Unknown event type → base_rate=0.1 |
| Info gain clamp | Very rare event → factor ≤ 3.0 |
| Source accuracy threshold | sample_count < 10 → factor=1.0 |
| Adaptive decay edge cases | All zeros → τ_base, all max → 6×τ_base |
| Regime classification | Specific (R, V_r) → expected regime |
| Regime thresholds | panic→0.10, mean_reversion→0.20, etc. |
| Entropy direction mapping | H>0.9→mixed, P_bull>0.65→bullish, etc. |
| Zero overlap → zero impact | All overlaps zero → S_macro=0 |
| Max overlap value | All overlaps 1.0 → ≈severity×0.724 |
| Macro fallback behaviors | Only macro → additive, only company → no modifier |
| Graph distance cutoff | d>3 → no propagation |
| Momentum fallback | <2 cycles → heuristic fallback |
| EV threshold behavior | EV>0.005→proceed, EV≤0.005→informational |
| Feature flag behaviors | flag=false→heuristic, flag=true→probabilistic |
| Heuristic equivalence | flag=false produces identical outputs to current system |

### Integration Tests

| Test Area | Scope |
|-----------|-------|
| Source accuracy persistence | Write/read from source_accuracy table |
| Regime persistence | Store/retrieve regime in JSONB |
| EV persistence | Store/retrieve EV in recommendation_evaluations |
| Feature flag reading | Read probabilistic_scoring_enabled from risk_configs |
| End-to-end pipeline | Full aggregation cycle with probabilistic=true |

### Test Organization

```
tests/
├── test_pbt_signal_math.py          # All 14 property-based tests
├── test_signal_math_unit.py         # Example-based unit tests
├── test_bayesian.py                 # Bayesian accumulator unit tests
├── test_regime.py                   # Regime detector unit tests
├── test_source_accuracy.py          # Source accuracy tracker tests
└── test_signal_math_integration.py  # Integration tests (DB required)
```
