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.
188 lines
6.1 KiB
Python
188 lines
6.1 KiB
Python
"""Canary signal influence — paper trading with v3 signals.
|
|
|
|
Enables v3 signals in paper trading at a small percentage, tracks
|
|
extraction correctness separately from trading outcomes, reviews
|
|
material recommendation divergences, and requires explicit owner
|
|
approval for full promotion.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import enum
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from uuid import UUID, uuid4
|
|
|
|
|
|
class PromotionStatus(str, enum.Enum):
|
|
"""Status of the canary promotion process."""
|
|
|
|
PENDING = "pending"
|
|
PAPER_TRADING = "paper_trading"
|
|
AWAITING_REVIEW = "awaiting_review"
|
|
APPROVED = "approved"
|
|
REJECTED = "rejected"
|
|
|
|
|
|
@dataclass
|
|
class DivergenceRecord:
|
|
"""Record of a material recommendation divergence between v2 and v3."""
|
|
|
|
record_id: UUID
|
|
document_id: str
|
|
timestamp: datetime
|
|
v2_recommendation: dict[str, Any]
|
|
v3_recommendation: dict[str, Any]
|
|
divergence_type: str # e.g., "direction_opposite", "magnitude_significant"
|
|
impact_estimate: float = 0.0 # Estimated impact on portfolio
|
|
reviewed: bool = False
|
|
reviewer_notes: str = ""
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
document_id: str,
|
|
v2_recommendation: dict[str, Any],
|
|
v3_recommendation: dict[str, Any],
|
|
divergence_type: str,
|
|
impact_estimate: float = 0.0,
|
|
) -> DivergenceRecord:
|
|
return cls(
|
|
record_id=uuid4(),
|
|
document_id=document_id,
|
|
timestamp=datetime.now(timezone.utc),
|
|
v2_recommendation=v2_recommendation,
|
|
v3_recommendation=v3_recommendation,
|
|
divergence_type=divergence_type,
|
|
impact_estimate=impact_estimate,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class SignalInfluenceConfig:
|
|
"""Configuration for canary signal influence in paper trading."""
|
|
|
|
enabled: bool = False
|
|
percentage: int = 5 # Start at 5% of paper trading signals
|
|
require_owner_approval: bool = True
|
|
owner_id: str = ""
|
|
|
|
# Reporting thresholds
|
|
material_divergence_threshold: float = 0.20
|
|
max_divergence_rate: float = 0.15
|
|
|
|
# Separation of concerns
|
|
report_extraction_separately: bool = True
|
|
report_trading_separately: bool = True
|
|
|
|
|
|
@dataclass
|
|
class SignalInfluenceTracker:
|
|
"""Tracks canary signal influence in paper trading.
|
|
|
|
Reports extraction correctness separately from trading outcomes.
|
|
Reviews material divergences and tracks promotion readiness.
|
|
"""
|
|
|
|
config: SignalInfluenceConfig
|
|
promotion_status: PromotionStatus = PromotionStatus.PENDING
|
|
_divergences: list[DivergenceRecord] = field(default_factory=list)
|
|
_extraction_metrics: dict[str, float] = field(default_factory=dict)
|
|
_trading_metrics: dict[str, float] = field(default_factory=dict)
|
|
_total_signals: int = 0
|
|
_v3_signals: int = 0
|
|
_approval_timestamp: datetime | None = None
|
|
_approver_id: str = ""
|
|
|
|
def start_paper_trading(self) -> None:
|
|
"""Begin paper trading with v3 signals."""
|
|
self.config.enabled = True
|
|
self.promotion_status = PromotionStatus.PAPER_TRADING
|
|
|
|
def record_signal(self, is_v3: bool = False) -> None:
|
|
"""Record a signal processed."""
|
|
self._total_signals += 1
|
|
if is_v3:
|
|
self._v3_signals += 1
|
|
|
|
def record_divergence(self, divergence: DivergenceRecord) -> None:
|
|
"""Record a material recommendation divergence."""
|
|
self._divergences.append(divergence)
|
|
|
|
def update_extraction_metrics(self, metrics: dict[str, float]) -> None:
|
|
"""Update extraction correctness metrics (separate from trading)."""
|
|
self._extraction_metrics.update(metrics)
|
|
|
|
def update_trading_metrics(self, metrics: dict[str, float]) -> None:
|
|
"""Update trading outcome metrics (separate from extraction)."""
|
|
self._trading_metrics.update(metrics)
|
|
|
|
@property
|
|
def divergence_rate(self) -> float:
|
|
if self._v3_signals == 0:
|
|
return 0.0
|
|
return len(self._divergences) / self._v3_signals
|
|
|
|
@property
|
|
def unreviewed_divergences(self) -> list[DivergenceRecord]:
|
|
return [d for d in self._divergences if not d.reviewed]
|
|
|
|
def request_approval(self) -> None:
|
|
"""Move to awaiting review status."""
|
|
self.promotion_status = PromotionStatus.AWAITING_REVIEW
|
|
|
|
def approve(self, approver_id: str) -> bool:
|
|
"""Approve promotion. Requires owner approval if configured.
|
|
|
|
Returns False if approval requirements are not met.
|
|
"""
|
|
if self.config.require_owner_approval:
|
|
if not approver_id:
|
|
return False
|
|
if self.config.owner_id and approver_id != self.config.owner_id:
|
|
return False
|
|
|
|
# Check all gates
|
|
if not self._all_gates_pass():
|
|
return False
|
|
|
|
self.promotion_status = PromotionStatus.APPROVED
|
|
self._approval_timestamp = datetime.now(timezone.utc)
|
|
self._approver_id = approver_id
|
|
return True
|
|
|
|
def reject(self, reason: str = "") -> None:
|
|
"""Reject promotion."""
|
|
self.promotion_status = PromotionStatus.REJECTED
|
|
|
|
def _all_gates_pass(self) -> bool:
|
|
"""Check if extraction correctness gates pass.
|
|
|
|
Trading outcomes explicitly do NOT override correctness gates
|
|
(Requirement 16.10).
|
|
"""
|
|
# Divergence rate must be below threshold
|
|
if self.divergence_rate > self.config.max_divergence_rate:
|
|
return False
|
|
|
|
# All divergences must be reviewed
|
|
if self.unreviewed_divergences:
|
|
return False
|
|
|
|
return True
|
|
|
|
def summary(self) -> dict[str, Any]:
|
|
return {
|
|
"enabled": self.config.enabled,
|
|
"status": self.promotion_status.value,
|
|
"percentage": self.config.percentage,
|
|
"total_signals": self._total_signals,
|
|
"v3_signals": self._v3_signals,
|
|
"divergence_count": len(self._divergences),
|
|
"divergence_rate": self.divergence_rate,
|
|
"unreviewed_divergences": len(self.unreviewed_divergences),
|
|
"extraction_metrics": self._extraction_metrics,
|
|
"trading_metrics": self._trading_metrics,
|
|
}
|