feat: model validation, calibration, and signal quality layer
ci/woodpecker/push/test Pipeline failed
ci/woodpecker/push/build-1 unknown status
ci/woodpecker/push/build-3 unknown status
ci/woodpecker/push/build-2 unknown status
ci/woodpecker/push/finalize unknown status
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

- Migration 035: prediction_snapshots, prediction_outcomes, signal_evidence_links, model_metric_snapshots tables + SQL views
- Prediction snapshot writer with canonical evidence keys, duplicate detection, contribution scores
- Outcome evaluator across 5 horizons (1h, 6h, 1d, 7d, 30d)
- Metrics engine: ECE, Brier score, IC, Rank IC, benchmark comparison
- Attribution engine: per-source, per-catalyst, per-layer performance
- Calibration engine: Bayesian shrinkage source reliability
- Quality gate for live trading eligibility with configurable thresholds
- 7 new /api/validation/* endpoints
- Upgraded OpsModel dashboard with validation tab
- Enhanced recommendation display with calibration context
- Backtest replay validation mode
- 86 Python tests (unit + property-based), 179 frontend tests passing
This commit is contained in:
Celes Renata
2026-05-01 03:04:58 +00:00
parent 5d2ffd9163
commit 7fcc8a6c07
23 changed files with 7554 additions and 9 deletions
@@ -0,0 +1,260 @@
# 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