# Implementation Plan: Model Validation, Calibration, and Signal Quality

## Overview

Add a closed-loop model validation layer to Stonks Oracle: prediction snapshot capture, outcome evaluation, calibration/IC metrics, source/catalyst/layer attribution, Bayesian source reliability, a quality gate for live trading, 7 new API endpoints, an upgraded OpsModel dashboard, and backtest replay integration. Implementation follows the four-phase priority order from the spec, with each phase building on the previous one.

## Tasks

- [x] 1. Database migration 035 — schema foundation
  - [x] 1.1 Create `infra/migrations/035_model_validation.sql` with all tables, indexes, and views
    - Create `prediction_snapshots` table with all columns from design (id UUID PK, generated_at, ticker, window, horizon, direction, action, mode, strength, confidence, contradiction, p_bull, p_bear, score_company, score_macro, score_competitive, evidence_count, unique_source_count, duplicate_evidence_count, price_at_prediction, spy_price_at_prediction, sector_etf_price_at_prediction, metadata JSONB, created_at)
    - Create `prediction_outcomes` table with FK to prediction_snapshots (id UUID PK, prediction_id, evaluated_at, horizon, future_price, future_return, spy_future_price, spy_return, sector_etf_future_price, sector_etf_return, excess_return_vs_spy, excess_return_vs_sector, direction_correct, profitable, metadata JSONB, created_at)
    - Create `signal_evidence_links` table with FK to prediction_snapshots (id UUID PK, prediction_id, document_id, signal_id, ticker, source, source_type, catalyst_type, sentiment, impact, extraction_confidence, weight, is_duplicate, canonical_evidence_key, contribution_score, metadata JSONB, created_at)
    - Create `model_metric_snapshots` table (id UUID PK, generated_at, lookback_window, horizon, prediction_count, win_rate, directional_accuracy, information_coefficient, rank_information_coefficient, avg_return, avg_excess_return_vs_spy, avg_excess_return_vs_sector, calibration_error, brier_score, buy_win_rate, sell_win_rate, hold_win_rate, metadata JSONB, created_at)
    - Create indexes on prediction_snapshots (ticker, generated_at, horizon), prediction_outcomes (prediction_id, horizon, evaluated_at), signal_evidence_links (prediction_id, document_id, ticker), model_metric_snapshots (generated_at, lookback_window, horizon)
    - Create `v_prediction_performance` view joining prediction_snapshots with prediction_outcomes
    - Create `v_source_performance` view joining signal_evidence_links with prediction_snapshots and prediction_outcomes
    - _Requirements: 16.1, 16.2, 16.3, 16.4, 16.5, 16.6, 14.1, 14.2, 14.3, 14.4_

- [x] 2. Phase 1 — Prediction capture, outcome evaluation, core metrics, and dashboard API
  - [x] 2.1 Implement Prediction Snapshot Writer (`services/validation/prediction_snapshot.py`)
    - Create `services/validation/__init__.py`
    - Define `SECTOR_ETF_MAP`, `EVALUATION_HORIZONS`, `MAX_SINGLE_DOCUMENT_WEIGHT` constants
    - Implement `PredictionSnapshot` and `SignalEvidenceLink` dataclasses
    - Implement `compute_canonical_evidence_key(title, url)` — SHA256 of normalized title + normalized URL (lowercase, strip whitespace for title; lowercase, strip query params for URL)
    - Implement `fetch_latest_close_price(pool, ticker)` — query most recent close from market_snapshots
    - Implement `create_prediction_snapshot(pool, recommendation, trend_summary, evidence_signals, evidence_docs)` — fetch prices (ticker, SPY, sector ETF), compute canonical keys, detect duplicates, clamp weights to MAX_SINGLE_DOCUMENT_WEIGHT, compute contribution scores (one-vote-per-canonical-key), persist snapshot + evidence links in a transaction
    - Implement `compute_contribution_scores(weights)` — each score = weight_i / sum(weights), sums to 1.0
    - Handle NULL prices gracefully (log warning, store NULL, don't fail)
    - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 3.1, 3.2, 3.3, 3.4_

  - [x] 2.2 Write property test for canonical evidence key determinism and idempotence
    - **Property 4: Canonical Evidence Key Determinism and Normalization Idempotence**
    - Test that same (title, url) always produces same key
    - Test that normalizing already-normalized input produces same key
    - **Validates: Requirements 2.3, 17.4**

  - [x] 2.3 Write property test for contribution score sum-to-one and range
    - **Property 7: Contribution Score Sum-to-One and Range**
    - Test that all scores in [0.0, 1.0] and sum to 1.0 (within 1e-9 tolerance)
    - Test that empty input returns empty list
    - **Validates: Requirements 2.5, 17.7**

  - [x] 2.4 Implement Outcome Evaluator (`services/validation/outcome_evaluator.py`)
    - Define `PredictionOutcome` dataclass and `HORIZON_DURATIONS` mapping
    - Implement `evaluate_matured_predictions(pool)` — find snapshots where horizon elapsed and outcome not recorded, evaluate each
    - Implement `evaluate_single_prediction(pool, snapshot, horizon)` — fetch future price at horizon endpoint, compute future_return, SPY return, sector ETF return, excess returns, direction_correct, profitable; return None if future price unavailable
    - Evaluate across all 5 horizons: 1h, 6h, 1d, 7d, 30d
    - Skip horizons where future price is unavailable (retry next run)
    - Store results in prediction_outcomes table
    - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10_

  - [x] 2.5 Implement Metrics Engine (`services/validation/metrics.py`)
    - Define `CONFIDENCE_BUCKETS`, `LOOKBACK_WINDOWS` constants
    - Define `CalibrationBucket` and `ModelMetricSnapshot` dataclasses
    - Implement `compute_calibration_error(confidences, outcomes)` — group into 5 confidence buckets, compute ECE as weighted average of |avg_conf - win_rate|, flag miscalibrated buckets (|diff| > 0.15)
    - Implement `compute_brier_score(p_bulls, outcomes)` — mean((p_bull - outcome)^2)
    - Implement `compute_information_coefficient(scores, returns)` — Pearson correlation, return None when < 30 data points
    - Implement `compute_rank_information_coefficient(scores, returns)` — Spearman rank correlation, return None when < 30 data points
    - Implement `compute_contribution_scores(weights)` — weight_i / sum(weights), sums to 1.0
    - Implement benchmark metrics: average excess return vs SPY, vs sector ETF, hit rate improvement
    - Implement `compute_and_store_metric_snapshots(pool)` — compute for all lookback/horizon combinations (4 lookbacks × 5 horizons), persist to model_metric_snapshots
    - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 6.1, 6.2, 6.3, 6.4, 6.5, 9.1, 9.2, 9.3, 9.4, 10.1, 10.2, 10.3, 10.4, 10.5_

  - [x] 2.6 Write property test for ECE range and round-trip
    - **Property 1: Calibration Error Range and Round-Trip**
    - Test ECE in [0.0, 1.0] for all valid distributions
    - Test ECE = 0.0 when every bucket's win rate matches avg confidence
    - **Validates: Requirements 5.1, 5.3, 17.1**

  - [x] 2.7 Write property test for Brier score range and perfect prediction
    - **Property 2: Brier Score Range and Perfect Prediction**
    - Test Brier in [0.0, 1.0] for all valid (p_bull, outcome) pairs
    - Test Brier = 0.0 when all predictions perfectly correct
    - **Validates: Requirements 5.4, 17.2**

  - [x] 2.8 Write property test for IC range and perfect correlation
    - **Property 3: Information Coefficient Range and Perfect Correlation**
    - Test IC in [-1.0, 1.0] for all valid (score, return) pairs with ≥30 elements
    - Test IC = 1.0 for perfectly positively correlated data
    - **Validates: Requirements 6.1, 6.2, 17.3**

  - [x] 2.9 Implement Dashboard API endpoints in `services/api/app.py`
    - Add `/api/validation/summary` GET — return latest model_metric_snapshot + gate status
    - Add `/api/validation/calibration` GET — return calibration table with buckets
    - Add `/api/validation/ic-by-horizon` GET — return IC and Rank IC per horizon
    - Add `/api/validation/gate-status` GET — return quality gate evaluation detail
    - All endpoints accept optional `lookback` (default "30d") and `horizon` (default "7d") query params
    - _Requirements: 12.1, 12.2, 12.3, 12.7_

  - [x] 2.10 Add frontend validation API hooks in `frontend/src/api/hooks.ts`
    - Add `useValidationSummary(lookback?, horizon?)` hook for `/api/validation/summary`
    - Add `useValidationCalibration(lookback?, horizon?)` hook for `/api/validation/calibration`
    - Add `useValidationICByHorizon(lookback?)` hook for `/api/validation/ic-by-horizon`
    - Add `useValidationGateStatus()` hook for `/api/validation/gate-status`
    - _Requirements: 12.1, 12.2, 12.3, 12.7_

  - [x] 2.11 Upgrade OpsModel page (`frontend/src/pages/OpsModel.tsx`) — Phase 1 dashboard
    - Add tabbed layout: existing "Extraction Performance" tab + new "Model Validation" tab
    - Add summary cards: prediction count, win rate, directional accuracy, IC, Rank IC, Brier score, ECE, avg excess return vs SPY, gate status
    - Add calibration table with confidence buckets, avg confidence, observed win rate, count, miscalibration flag
    - Highlight miscalibrated buckets (|avg_confidence - observed_win_rate| > 0.15) with warning indicator
    - Add IC-by-horizon table showing IC and Rank IC for each horizon
    - Add gate status indicator (pass/fail with threshold details)
    - _Requirements: 12.1, 12.2, 12.3, 12.7, 12.8, 12.9_

- [x] 3. Checkpoint — Phase 1 verification
  - Ensure all tests pass, ask the user if questions arise.

- [x] 4. Phase 2 — Attribution engine and source/catalyst truth tables
  - [x] 4.1 Implement Attribution Engine (`services/validation/attribution.py`)
    - Define `SourceAttribution`, `CatalystAttribution`, `LayerAttribution` dataclasses
    - Implement `compute_source_attribution(pool, lookback_days, horizon)` — join signal_evidence_links with prediction_outcomes, group by source; compute prediction count, avg weight, avg contribution score, win rate, avg future return, avg excess return vs SPY, IC, duplicate rate
    - Implement `compute_catalyst_attribution(pool, lookback_days, horizon)` — same metrics grouped by catalyst_type
    - Implement `compute_layer_attribution(pool, lookback_days, horizon)` — compute per-layer (company, macro, competitive) avg contribution %, dominant win rate (layer > 30% contribution), dominant IC
    - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7_

  - [x] 4.2 Implement Calibration Engine (`services/validation/calibration.py`)
    - Implement `compute_source_reliability(observed_win_rate, sample_count, prior_strength=30)` — Bayesian shrinkage: `0.5 + (n / (n + 30)) * (observed_win_rate - 0.5)`; return 0.5 when n=0
    - Implement `compute_adjusted_evidence_weight(base_weight, reliability)` — `base_weight * (0.5 + reliability)`, clamped to [0.1, 2.0]
    - Implement `update_source_reliabilities(pool)` — recompute from latest outcomes, update source_accuracy table
    - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_

  - [x] 4.3 Write property test for source reliability Bayesian shrinkage bounds and convergence
    - **Property 5: Source Reliability Bayesian Shrinkage Bounds and Convergence**
    - Test reliability in [0.0, 1.0] for all valid inputs
    - Test reliability = 0.5 when sample_count = 0
    - Test reliability approaches observed_win_rate as sample_count → ∞
    - **Validates: Requirements 8.1, 8.2, 17.5**

  - [x] 4.4 Add attribution API endpoints in `services/api/app.py`
    - Add `/api/validation/attribution/sources` GET — return per-source performance metrics
    - Add `/api/validation/attribution/catalysts` GET — return per-catalyst performance metrics
    - Add `/api/validation/attribution/layers` GET — return per-layer performance metrics
    - All endpoints accept optional `lookback` (default "30d") and `horizon` (default "7d") query params
    - _Requirements: 12.4, 12.5, 12.6_

  - [x] 4.5 Add frontend attribution hooks in `frontend/src/api/hooks.ts`
    - Add `useValidationAttributionSources(lookback?, horizon?)` hook
    - Add `useValidationAttributionCatalysts(lookback?, horizon?)` hook
    - Add `useValidationAttributionLayers(lookback?, horizon?)` hook
    - _Requirements: 12.4, 12.5, 12.6_

  - [x] 4.6 Extend OpsModel page with attribution tables
    - Add source performance table (source, win rate, IC, avg return, duplicate rate)
    - Add catalyst truth table (catalyst type, win rate, avg return, IC)
    - Add layer attribution table (company/macro/competitive contribution %, dominant win rate, IC)
    - _Requirements: 12.4, 12.5, 12.6, 12.8_

- [x] 5. Checkpoint — Phase 2 verification
  - Ensure all tests pass, ask the user if questions arise.

- [x] 6. Phase 3 — Quality gate, recommendation enhancements, and pipeline wiring
  - [x] 6.1 Implement Quality Gate (`services/trading/model_quality_gate.py`)
    - Define `QualityGateConfig` dataclass with default thresholds (min_prediction_count=100, min_ic=0.03, min_win_rate=0.53, max_ece=0.15, min_excess_return_vs_spy=0.0, max_snapshot_age_hours=24)
    - Define `GateThresholdResult` and `QualityGateResult` dataclasses
    - Implement `evaluate_quality_gate(pool, config)` — read most recent model_metric_snapshot (30d lookback, 7d horizon), evaluate each threshold, store result in risk_configs under 'model_quality_gate' key
    - Implement `load_gate_config_from_db(pool)` — load thresholds from risk_configs with defaults
    - Default to paper-only mode when no snapshots exist or snapshot is stale (>24h)
    - Log gate evaluation result with threshold pass/fail details
    - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_

  - [x] 6.2 Write property test for quality gate determinism and threshold monotonicity
    - **Property 6: Quality Gate Determinism and Threshold Monotonicity**
    - Test same inputs always produce same pass/fail result
    - Test relaxing any threshold never causes a previously passing gate to fail
    - **Validates: Requirements 11.1, 17.6**

  - [x] 6.3 Wire Quality Gate into aggregation cycle (`services/aggregation/worker.py`)
    - Call `evaluate_quality_gate` at the start of each aggregation cycle
    - When gate fails, force all recommendations to paper mode
    - Log gate status at cycle start
    - _Requirements: 11.2, 11.3_

  - [x] 6.4 Wire Prediction Snapshot Writer into recommendation engine
    - After recommendation is generated in `services/recommendation/eligibility.py` or the calling code, call `create_prediction_snapshot` to capture the prediction state
    - Pass recommendation, trend_summary, evidence signals, and evidence docs
    - Handle snapshot creation failure gracefully (log error, don't block recommendation)
    - _Requirements: 1.1, 1.6_

  - [x] 6.5 Enhance recommendation display on frontend
    - Update `frontend/src/pages/RecommendationDetail` (or relevant recommendation display component) to show:
      - Original confidence alongside calibrated confidence (historical win rate for that bucket)
      - Historical win rate for similar confidence levels
      - Evidence count, unique evidence count, duplicate evidence count
      - Source reliability indicator for primary contributing sources
      - Live eligibility status with reason (gate passed or which threshold failed)
    - Add warning badge when duplicate evidence count > 20% of total evidence count
    - Add warning badge when primary source reliability < 0.4
    - _Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7_

- [x] 7. Checkpoint — Phase 3 verification
  - Ensure all tests pass, ask the user if questions arise.

- [x] 8. Phase 4 — Backtest replay integration and unit tests
  - [x] 8.1 Add validation mode to BacktestReplay (`services/trading/backtest_replay.py`)
    - Add `validation_mode: bool = False` parameter to `BacktestReplay.run()`
    - When validation_mode=True, create prediction snapshots for each historical recommendation using only data available at that point in time
    - Evaluate prediction outcomes using market prices from the appropriate future horizon
    - Prevent future data leakage: no market data after prediction generation time used during snapshot creation
    - After backtest completes, trigger model metrics computation over the backtest period, tag snapshots with backtest_id
    - _Requirements: 15.1, 15.2, 15.3, 15.4, 15.5_

  - [x] 8.2 Write unit tests for prediction snapshot writer (`tests/test_model_validation_unit.py`)
    - Test canonical evidence key: known title/URL → expected SHA256, empty inputs, unicode
    - Test duplicate detection: 3 docs with 2 sharing a key → 1 marked duplicate
    - Test contribution scores: [0.5, 0.3, 0.2] → [0.5, 0.3, 0.2], single doc → [1.0]
    - Test weight clamping: weight 1.5 → clamped to 1.0
    - _Requirements: 1.1, 2.3, 2.4, 2.5, 3.3_

  - [x] 8.3 Write unit tests for outcome evaluator (`tests/test_model_validation_unit.py`)
    - Test future return computation: price 100→110 → 0.10, price 100→90 → -0.10
    - Test direction_correct logic: bullish+positive → true, bullish+negative → false
    - Test profitable logic: buy+positive → true, sell+negative → true
    - Test excess return: ticker 10%, SPY 5% → excess 5%
    - _Requirements: 4.2, 4.5, 4.6, 4.7_

  - [x] 8.4 Write unit tests for metrics engine (`tests/test_model_validation_unit.py`)
    - Test ECE specific values: perfect calibration → 0.0, all overconfident → positive ECE
    - Test Brier score: all correct at p=1.0 → 0.0, all wrong at p=1.0 → 1.0
    - Test IC: perfect correlation → 1.0, anti-correlation → -1.0, < 30 → None
    - _Requirements: 5.3, 5.4, 6.1, 6.2, 6.5_

  - [x] 8.5 Write unit tests for calibration engine (`tests/test_model_validation_unit.py`)
    - Test source reliability: n=0 → 0.5, n=1000 with wr=0.8 → ≈0.8, n=30 with wr=0.7 → 0.6
    - Test adjusted evidence weight: reliability=0.5 → base*1.0, clamping to [0.1, 2.0]
    - _Requirements: 8.1, 8.2, 8.3_

  - [x] 8.6 Write unit tests for quality gate (`tests/test_model_validation_unit.py`)
    - Test all thresholds met → pass
    - Test one threshold failed → fail with reason
    - Test fail-safe: no snapshots → paper-only, stale snapshot → paper-only
    - _Requirements: 11.1, 11.6_

  - [x] 8.7 Write frontend tests for validation dashboard (`frontend/src/test/pages.test.tsx`)
    - Add MSW mock handlers for `/api/validation/summary`, `/api/validation/calibration`, `/api/validation/gate-status`
    - Test OpsModel page renders validation tab with summary cards
    - Test calibration table renders buckets with miscalibration warning
    - Test gate status indicator renders pass/fail
    - _Requirements: 12.8, 12.9_

- [x] 9. Final checkpoint — Ensure all tests pass
  - Ensure all tests pass, ask the user if questions arise.

## Notes

- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation after each phase
- Property tests validate the 7 universal correctness properties from the design document
- Unit tests validate specific examples, edge cases, and integration points
- The design uses Python for backend and TypeScript for frontend — no language selection needed
- Migration number is 035 (existing migrations go up to 034)
- All new service modules go under `services/validation/` except the quality gate which goes in `services/trading/`
- The 7 new API endpoints are added to the existing `services/api/app.py`
- Frontend hooks follow existing patterns in `frontend/src/api/hooks.ts`
- Phase 1 delivers the core feedback loop (capture → evaluate → measure → display)
- Phase 2 adds attribution depth (which sources/catalysts/layers work best)
- Phase 3 adds safety (quality gate) and UX (recommendation warnings)
- Phase 4 adds historical analysis (backtest validation mode) and comprehensive tests
