Files
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

384 lines
11 KiB
Python

"""Outcome label generation for impact model training.
Computes leakage-safe abnormal returns and response labels at defined
event timestamps over multiple horizons.
Design reference: Section I (Impact and Horizon Model) — Labels.
Requirement 12.3.
"""
from __future__ import annotations
import math
from datetime import datetime, timedelta
from typing import Literal
from pydantic import BaseModel, Field
# Version label-generation code for reproducibility tracking
LABEL_GENERATOR_VERSION = "1.0.0"
# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------
HorizonName = Literal["intraday", "1d", "7d", "30d", "90d"]
HORIZON_DURATIONS: dict[HorizonName, timedelta] = {
"intraday": timedelta(hours=6, minutes=30), # Trading day approximation
"1d": timedelta(days=1),
"7d": timedelta(days=7),
"30d": timedelta(days=30),
"90d": timedelta(days=90),
}
class OutcomeLabel(BaseModel):
"""Single-horizon outcome label for a given event."""
horizon: HorizonName
signed_return: float = Field(
description="Signed abnormal return over the horizon window.",
)
absolute_return: float = Field(
ge=0.0,
description="Absolute abnormal return over the horizon window.",
)
abnormal_volume: float | None = Field(
default=None,
description="Volume ratio vs trailing average (None if data unavailable).",
)
time_to_peak_hours: float | None = Field(
default=None,
description="Hours from event to peak response within the horizon (None if unavailable).",
)
data_quality: Literal["full", "partial", "insufficient"] = Field(
default="full",
description="Quality indicator for this label's underlying data.",
)
class OutcomeLabelSet(BaseModel):
"""Complete label set for all horizons at a single event."""
event_time: datetime
ticker: str
benchmark_ticker: str = "SPY"
labels: list[OutcomeLabel] = Field(default_factory=list)
label_generator_version: str = LABEL_GENERATOR_VERSION
market_data_snapshot_id: str | None = Field(
default=None,
description="Reference to the market data snapshot used for label generation.",
)
# ---------------------------------------------------------------------------
# Core computation
# ---------------------------------------------------------------------------
def compute_abnormal_return(
price_series: list[tuple[datetime, float]],
benchmark_series: list[tuple[datetime, float]],
event_time: datetime,
horizon: timedelta,
) -> float:
"""Compute abnormal return of the asset relative to benchmark over a horizon.
Abnormal return = asset_return - benchmark_return
Parameters
----------
price_series
Sorted list of (timestamp, price) tuples for the asset.
benchmark_series
Sorted list of (timestamp, price) tuples for the benchmark.
event_time
When the event occurred (start of measurement window).
horizon
Duration of the measurement window.
Returns
-------
float
Abnormal return as a fraction (e.g., 0.02 = 2%).
Raises
------
ValueError
If series are empty or don't cover the required time range.
"""
if not price_series:
raise ValueError("price_series is empty")
if not benchmark_series:
raise ValueError("benchmark_series is empty")
end_time = event_time + horizon
asset_start = _get_price_at_or_before(price_series, event_time)
asset_end = _get_price_at_or_before(price_series, end_time)
bench_start = _get_price_at_or_before(benchmark_series, event_time)
bench_end = _get_price_at_or_before(benchmark_series, end_time)
if asset_start is None or asset_end is None:
raise ValueError(
f"Asset price series does not cover event_time to event_time+horizon "
f"({event_time.isoformat()} to {end_time.isoformat()})"
)
if bench_start is None or bench_end is None:
raise ValueError(
f"Benchmark series does not cover event_time to event_time+horizon "
f"({event_time.isoformat()} to {end_time.isoformat()})"
)
if asset_start == 0.0 or bench_start == 0.0:
raise ValueError("Start price cannot be zero")
asset_return = (asset_end - asset_start) / asset_start
bench_return = (bench_end - bench_start) / bench_start
return asset_return - bench_return
def compute_abnormal_volume(
volume_series: list[tuple[datetime, float]],
event_time: datetime,
horizon: timedelta,
lookback_days: int = 20,
) -> float | None:
"""Compute abnormal volume ratio relative to trailing average.
Parameters
----------
volume_series
Sorted list of (timestamp, volume) tuples.
event_time
When the event occurred.
horizon
Duration window to measure event-period volume.
lookback_days
Number of days before event_time to compute trailing average.
Returns
-------
float or None
Volume ratio (event_volume / trailing_avg_volume), or None if insufficient data.
"""
if not volume_series:
return None
lookback_start = event_time - timedelta(days=lookback_days)
end_time = event_time + horizon
# Trailing volume (pre-event)
trailing_volumes = [
v for ts, v in volume_series
if lookback_start <= ts < event_time
]
# Event-period volume
event_volumes = [
v for ts, v in volume_series
if event_time <= ts <= end_time
]
if not trailing_volumes or not event_volumes:
return None
trailing_avg = sum(trailing_volumes) / len(trailing_volumes)
if trailing_avg == 0:
return None
event_avg = sum(event_volumes) / len(event_volumes)
return event_avg / trailing_avg
def compute_time_to_peak(
price_series: list[tuple[datetime, float]],
event_time: datetime,
horizon: timedelta,
) -> float | None:
"""Compute time from event to peak absolute response within horizon.
Parameters
----------
price_series
Sorted list of (timestamp, price) tuples.
event_time
When the event occurred.
horizon
Duration window to search for peak.
Returns
-------
float or None
Hours from event to peak absolute deviation, or None if insufficient data.
"""
if not price_series:
return None
end_time = event_time + horizon
base_price = _get_price_at_or_before(price_series, event_time)
if base_price is None or base_price == 0.0:
return None
# Find the point within [event_time, end_time] with max absolute deviation
max_deviation = 0.0
peak_time = event_time
for ts, price in price_series:
if ts < event_time:
continue
if ts > end_time:
break
deviation = abs((price - base_price) / base_price)
if deviation > max_deviation:
max_deviation = deviation
peak_time = ts
if max_deviation == 0.0:
return None
hours = (peak_time - event_time).total_seconds() / 3600.0
return hours
# ---------------------------------------------------------------------------
# Label generation for all horizons
# ---------------------------------------------------------------------------
def generate_outcome_labels(
ticker: str,
event_time: datetime,
price_series: list[tuple[datetime, float]],
benchmark_series: list[tuple[datetime, float]],
volume_series: list[tuple[datetime, float]] | None = None,
benchmark_ticker: str = "SPY",
horizons: list[HorizonName] | None = None,
market_data_snapshot_id: str | None = None,
) -> OutcomeLabelSet:
"""Generate outcome labels for all configured horizons.
Parameters
----------
ticker
Asset ticker symbol.
event_time
When the event was detected.
price_series
Asset price series (sorted by timestamp).
benchmark_series
Benchmark price series (sorted by timestamp).
volume_series
Optional volume series for abnormal volume labels.
benchmark_ticker
Benchmark identifier (default SPY).
horizons
Which horizons to compute. Default is all five.
market_data_snapshot_id
Optional reference to the market data snapshot used.
Returns
-------
OutcomeLabelSet
Complete label set for the event.
"""
if horizons is None:
horizons = list(HORIZON_DURATIONS.keys())
labels: list[OutcomeLabel] = []
for horizon_name in horizons:
duration = HORIZON_DURATIONS[horizon_name]
label = _compute_single_horizon_label(
price_series=price_series,
benchmark_series=benchmark_series,
volume_series=volume_series,
event_time=event_time,
horizon_name=horizon_name,
duration=duration,
)
labels.append(label)
return OutcomeLabelSet(
event_time=event_time,
ticker=ticker,
benchmark_ticker=benchmark_ticker,
labels=labels,
label_generator_version=LABEL_GENERATOR_VERSION,
market_data_snapshot_id=market_data_snapshot_id,
)
def _compute_single_horizon_label(
price_series: list[tuple[datetime, float]],
benchmark_series: list[tuple[datetime, float]],
volume_series: list[tuple[datetime, float]] | None,
event_time: datetime,
horizon_name: HorizonName,
duration: timedelta,
) -> OutcomeLabel:
"""Compute outcome label for a single horizon."""
# Attempt abnormal return
try:
signed_return = compute_abnormal_return(
price_series, benchmark_series, event_time, duration
)
data_quality: Literal["full", "partial", "insufficient"] = "full"
except ValueError:
signed_return = float("nan")
data_quality = "insufficient"
# Absolute return
absolute_return = abs(signed_return) if not math.isnan(signed_return) else 0.0
# Abnormal volume
abnormal_volume = None
if volume_series:
abnormal_volume = compute_abnormal_volume(
volume_series, event_time, duration
)
if abnormal_volume is None and data_quality == "full":
data_quality = "partial"
# Time to peak
time_to_peak = None
try:
time_to_peak = compute_time_to_peak(price_series, event_time, duration)
except (ValueError, ZeroDivisionError):
pass
if time_to_peak is None and data_quality == "full":
data_quality = "partial"
return OutcomeLabel(
horizon=horizon_name,
signed_return=signed_return if not math.isnan(signed_return) else 0.0,
absolute_return=absolute_return,
abnormal_volume=abnormal_volume,
time_to_peak_hours=time_to_peak,
data_quality=data_quality,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _get_price_at_or_before(
series: list[tuple[datetime, float]], target: datetime
) -> float | None:
"""Get the most recent price at or before the target timestamp.
Assumes series is sorted by timestamp ascending.
"""
result = None
for ts, price in series:
if ts <= target:
result = price
else:
break
return result