Files
stonks-oracle/services/intelligence_pipeline_v3/impact/baseline.py
T
Celes Renata a72f336ad1 feat: Intelligence Pipeline v3 — full implementation
Multi-stage evidence-grounded inference architecture replacing the
monolithic 9B model extraction pipeline. CPU-first specialist services
handle routine extraction while the 9B vLLM model is preserved for
semantic adjudication of ambiguous cases.

Key components:
- Capability-aware inference gateway (OpenAI-compatible + Ollama)
- Endpoint registry with DB migrations and REST API
- Sentence-aware document segmenter (property tests)
- Deterministic financial parsing with offset integrity
- Symbol resolution with ambiguity detection
- Specialist service (GLiNER2, dynamic batching, K8s deployment)
- Company-specific sentiment (FinBERT, calibration)
- Retrieval-based novelty and duplicate detection
- Confidence calibration pipeline
- Deterministic routing engine (property tests)
- 9B adjudication layer with VRAM gating
- Stock-specific impact model (features, labels, baseline, trained)
- Pipeline orchestrator (state machine, queues, leases, feature flags)
- Bounded parallelism (async workers, semaphore, load shedding)
- Observability (tracing, metrics, alerts)
- Compatibility adapter (v3→v2 golden mapping tests)
- Shadow/canary promotion framework
- Active learning and fine-tuning pipeline

Test results: 1,161 tests pass, ruff lint clean.
All 282 spec tasks completed.
2026-07-13 02:14:59 +00:00

327 lines
12 KiB
Python

"""Deterministic impact baseline model.
Provides conservative, rule-based impact predictions when no trained
model is available or approved. Maps event class + sentiment + magnitude
+ novelty to signed impact and horizon predictions.
Design reference: Section I (Impact and Horizon Model) — Model family.
Requirement 12.4, 12.8.
"""
from __future__ import annotations
import math
from services.intelligence_pipeline_v3.impact.features import ImpactFeatureSet
# ---------------------------------------------------------------------------
# Impact prediction output (shared with trained model)
# ---------------------------------------------------------------------------
class ImpactPrediction:
"""Result of an impact model prediction.
Attributes
----------
direction_probabilities : dict
Probabilities for positive, negative, neutral outcomes.
expected_magnitude : float
Expected absolute move magnitude.
signed_magnitude : float
Direction-weighted expected magnitude.
horizon_probabilities : dict
Probability distribution over horizons.
uncertainty : float
Model uncertainty estimate (higher = less confident).
model_source : str
Which model produced this prediction.
"""
def __init__(
self,
direction_probabilities: dict[str, float],
expected_magnitude: float,
signed_magnitude: float,
horizon_probabilities: dict[str, float],
uncertainty: float,
model_source: str = "deterministic_baseline",
) -> None:
self.direction_probabilities = direction_probabilities
self.expected_magnitude = expected_magnitude
self.signed_magnitude = signed_magnitude
self.horizon_probabilities = horizon_probabilities
self.uncertainty = uncertainty
self.model_source = model_source
def to_dict(self) -> dict:
return {
"direction_probabilities": self.direction_probabilities,
"expected_magnitude": self.expected_magnitude,
"signed_magnitude": self.signed_magnitude,
"horizon_probabilities": self.horizon_probabilities,
"uncertainty": self.uncertainty,
"model_source": self.model_source,
}
# ---------------------------------------------------------------------------
# Event class impact mappings (conservative)
# ---------------------------------------------------------------------------
# Base magnitude for each event class (conservative estimates)
EVENT_CLASS_BASE_MAGNITUDE: dict[str, float] = {
"earnings_beat": 0.04,
"earnings_miss": 0.05,
"guidance_raise": 0.03,
"guidance_cut": 0.04,
"product_launch": 0.02,
"legal_regulatory": 0.03,
"ma_announcement": 0.06,
"supply_chain": 0.02,
"rating_change": 0.02,
"macro_event": 0.01,
"management_change": 0.02,
"dividend_change": 0.01,
"buyback": 0.01,
}
# Default direction bias for event classes (positive, negative, neutral)
EVENT_CLASS_DIRECTION: dict[str, tuple[float, float, float]] = {
"earnings_beat": (0.70, 0.10, 0.20),
"earnings_miss": (0.10, 0.70, 0.20),
"guidance_raise": (0.65, 0.10, 0.25),
"guidance_cut": (0.10, 0.65, 0.25),
"product_launch": (0.50, 0.15, 0.35),
"legal_regulatory": (0.15, 0.55, 0.30),
"ma_announcement": (0.40, 0.25, 0.35),
"supply_chain": (0.15, 0.50, 0.35),
"rating_change": (0.45, 0.30, 0.25),
"macro_event": (0.30, 0.30, 0.40),
"management_change": (0.30, 0.30, 0.40),
"dividend_change": (0.50, 0.20, 0.30),
"buyback": (0.55, 0.15, 0.30),
}
# Default horizon distribution for event classes
EVENT_CLASS_HORIZON: dict[str, dict[str, float]] = {
"earnings_beat": {"intraday": 0.40, "1d": 0.30, "7d": 0.15, "30d": 0.10, "90d": 0.05},
"earnings_miss": {"intraday": 0.45, "1d": 0.30, "7d": 0.15, "30d": 0.07, "90d": 0.03},
"guidance_raise": {"intraday": 0.25, "1d": 0.30, "7d": 0.20, "30d": 0.15, "90d": 0.10},
"guidance_cut": {"intraday": 0.30, "1d": 0.30, "7d": 0.20, "30d": 0.13, "90d": 0.07},
"product_launch": {"intraday": 0.15, "1d": 0.20, "7d": 0.25, "30d": 0.25, "90d": 0.15},
"legal_regulatory": {"intraday": 0.20, "1d": 0.20, "7d": 0.20, "30d": 0.20, "90d": 0.20},
"ma_announcement": {"intraday": 0.35, "1d": 0.30, "7d": 0.20, "30d": 0.10, "90d": 0.05},
"supply_chain": {"intraday": 0.10, "1d": 0.15, "7d": 0.25, "30d": 0.30, "90d": 0.20},
"rating_change": {"intraday": 0.35, "1d": 0.30, "7d": 0.20, "30d": 0.10, "90d": 0.05},
"macro_event": {"intraday": 0.15, "1d": 0.20, "7d": 0.25, "30d": 0.25, "90d": 0.15},
"management_change": {"intraday": 0.15, "1d": 0.20, "7d": 0.25, "30d": 0.25, "90d": 0.15},
"dividend_change": {"intraday": 0.20, "1d": 0.25, "7d": 0.25, "30d": 0.20, "90d": 0.10},
"buyback": {"intraday": 0.15, "1d": 0.20, "7d": 0.25, "30d": 0.25, "90d": 0.15},
}
# Default for unknown event classes
_DEFAULT_DIRECTION = (0.30, 0.30, 0.40)
_DEFAULT_MAGNITUDE = 0.015
_DEFAULT_HORIZON = {"intraday": 0.20, "1d": 0.20, "7d": 0.20, "30d": 0.20, "90d": 0.20}
# ---------------------------------------------------------------------------
# Baseline model
# ---------------------------------------------------------------------------
class DeterministicImpactBaseline:
"""Rule-based impact prediction using event class + sentiment + magnitude + novelty.
This is the fallback model used when no trained model has been approved.
It produces conservative, explainable predictions based on fixed mappings.
The baseline NEVER uses a generative model's self-scored impact.
"""
MODEL_VERSION = "1.0.0"
def predict(self, features: ImpactFeatureSet) -> ImpactPrediction:
"""Produce an impact prediction from pre-event features.
Parameters
----------
features
The event-time feature snapshot.
Returns
-------
ImpactPrediction
Conservative direction, magnitude, and horizon prediction.
"""
# Determine primary event class (highest probability)
primary_event = self._get_primary_event_class(features)
# Get base predictions from event class
base_direction = EVENT_CLASS_DIRECTION.get(primary_event, _DEFAULT_DIRECTION)
base_magnitude = EVENT_CLASS_BASE_MAGNITUDE.get(primary_event, _DEFAULT_MAGNITUDE)
base_horizon = EVENT_CLASS_HORIZON.get(primary_event, _DEFAULT_HORIZON)
# Adjust direction by sentiment
direction = self._adjust_direction_by_sentiment(base_direction, features)
# Adjust magnitude by surprise, novelty, and evidence coverage
magnitude = self._adjust_magnitude(base_magnitude, features)
# Compute signed magnitude
signed_magnitude = magnitude * (direction[0] - direction[1])
# Adjust horizon by event directness
horizon = self._adjust_horizon(base_horizon, features)
# Compute uncertainty (higher for unknown/speculative events)
uncertainty = self._compute_uncertainty(features, primary_event)
return ImpactPrediction(
direction_probabilities={
"positive": direction[0],
"negative": direction[1],
"neutral": direction[2],
},
expected_magnitude=magnitude,
signed_magnitude=signed_magnitude,
horizon_probabilities=horizon,
uncertainty=uncertainty,
model_source=f"deterministic_baseline_v{self.MODEL_VERSION}",
)
def _get_primary_event_class(self, features: ImpactFeatureSet) -> str:
"""Get the highest-probability event class."""
if not features.event_class_probabilities:
return "unknown"
return max(
features.event_class_probabilities,
key=lambda k: features.event_class_probabilities[k],
)
def _adjust_direction_by_sentiment(
self,
base_direction: tuple[float, float, float],
features: ImpactFeatureSet,
) -> tuple[float, float, float]:
"""Blend event-class direction with calibrated sentiment.
Uses a 60/40 split: 60% event class prior, 40% sentiment signal.
"""
event_weight = 0.6
sentiment_weight = 0.4
pos = event_weight * base_direction[0] + sentiment_weight * features.sentiment_positive
neg = event_weight * base_direction[1] + sentiment_weight * features.sentiment_negative
neu = event_weight * base_direction[2] + sentiment_weight * features.sentiment_neutral
# Normalize to sum to 1.0
total = pos + neg + neu
if total > 0:
pos, neg, neu = pos / total, neg / total, neu / total
else:
pos, neg, neu = 0.33, 0.33, 0.34
return (pos, neg, neu)
def _adjust_magnitude(
self,
base_magnitude: float,
features: ImpactFeatureSet,
) -> float:
"""Adjust base magnitude by surprise, novelty, and evidence coverage.
Higher surprise/novelty/evidence → higher magnitude.
Conservative: never more than 2x base.
"""
multiplier = 1.0
# Surprise amplification (NaN means no surprise data → neutral)
if not math.isnan(features.surprise):
# surprise is normalized, values > 0.5 indicate above-average surprise
multiplier *= 1.0 + 0.5 * max(0.0, features.surprise - 0.5)
# Novelty amplification (novel events have more impact)
multiplier *= 1.0 + 0.3 * features.novelty_score
# Evidence coverage: less evidence → discount magnitude
multiplier *= 0.5 + 0.5 * features.evidence_coverage
# Cap at 2x base (conservative)
multiplier = min(multiplier, 2.0)
return base_magnitude * multiplier
def _adjust_horizon(
self,
base_horizon: dict[str, float],
features: ImpactFeatureSet,
) -> dict[str, float]:
"""Adjust horizon by event directness.
- Direct events: shift probability toward shorter horizons.
- Second-order/speculative: shift toward longer horizons.
"""
horizon = dict(base_horizon)
if features.event_directness == "direct":
# Shift mass toward shorter horizons
shift = 0.05
horizon["intraday"] = horizon.get("intraday", 0.2) + shift
horizon["1d"] = horizon.get("1d", 0.2) + shift * 0.5
horizon["90d"] = max(0.0, horizon.get("90d", 0.2) - shift)
horizon["30d"] = max(0.0, horizon.get("30d", 0.2) - shift * 0.5)
elif features.event_directness in ("second_order", "speculative"):
# Shift mass toward longer horizons
shift = 0.05
horizon["90d"] = horizon.get("90d", 0.2) + shift
horizon["30d"] = horizon.get("30d", 0.2) + shift * 0.5
horizon["intraday"] = max(0.0, horizon.get("intraday", 0.2) - shift)
horizon["1d"] = max(0.0, horizon.get("1d", 0.2) - shift * 0.5)
# Normalize to sum to 1.0
total = sum(horizon.values())
if total > 0:
horizon = {k: v / total for k, v in horizon.items()}
return horizon
def _compute_uncertainty(
self,
features: ImpactFeatureSet,
primary_event: str,
) -> float:
"""Compute prediction uncertainty.
Higher uncertainty when:
- Event class is unknown or low-confidence
- Low evidence coverage
- Source credibility is low
- Market regime is unknown
"""
uncertainty = 0.5 # Base uncertainty for deterministic model
# Unknown event class increases uncertainty
if primary_event == "unknown":
uncertainty += 0.2
# Low event class confidence increases uncertainty
max_event_prob = max(features.event_class_probabilities.values()) if features.event_class_probabilities else 0.0
uncertainty += 0.1 * (1.0 - max_event_prob)
# Low evidence coverage increases uncertainty
uncertainty += 0.1 * (1.0 - features.evidence_coverage)
# Low source credibility increases uncertainty
if not math.isnan(features.source_credibility):
uncertainty += 0.05 * (1.0 - features.source_credibility)
# Unknown market regime increases uncertainty
if features.broad_market_regime == "unknown":
uncertainty += 0.05
# Clamp to [0, 1]
return max(0.0, min(1.0, uncertainty))