Files
stonks-oracle/.kiro/specs/signal-math-upgrade/design.md
T
Celes Renata 4e010bc048
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-2 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: signal math upgrade — probabilistic, regime-aware scoring pipeline
Implement full probabilistic signal processing pipeline gated behind
probabilistic_scoring_enabled feature flag in risk_configs:

- Bayesian log-likelihood accumulator with Beta posterior and entropy
- Regime detector (trend-following, panic, mean-reversion, uncertainty)
- Source accuracy tracker with per-source historical prediction accuracy
- Sigmoid confidence gate replacing binary gate
- Information gain surprise weighting for rare events
- Adaptive recency decay with event-specific half-lives
- Regime multiplier replacing market context multiplier
- Weighted disagreement entropy for contradiction detection
- Multiplicative macro exposure with conditional integration
- Graph-distance attenuated competitive signal propagation
- Exponentially weighted momentum with volatility scaling
- Expected value recommendation gate

All changes backward-compatible: flag=false preserves exact current behavior.
New outputs stored in existing JSONB columns (no schema changes except
source_accuracy table via migration 034).

Tests: 26 property-based tests (14 correctness properties), 99 unit tests,
1789 total tests passing with zero regressions.
2026-04-29 11:41:48 +00:00

32 KiB
Raw Blame History

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.

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.

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)

@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)

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)

@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:

@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:

@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

@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:

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

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:

{
    "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:

{
    "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:

{
    "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:

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:

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.117.7 duplicate properties already stated in requirements 115).

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)