feat: model validation, calibration, and signal quality layer

- 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
+49
View File
@@ -169,6 +169,55 @@ describe('Global Events page', () => {
});
});
describe('OpsModel validation tab', () => {
it('renders Model Validation tab with summary cards', async () => {
renderRoute('/ops/model');
await waitFor(() => expect(screen.getByText('Model Performance')).toBeInTheDocument());
// The tab buttons should be present
expect(screen.getByText('Extraction Performance')).toBeInTheDocument();
expect(screen.getByText('Model Validation')).toBeInTheDocument();
// Click the Model Validation tab button
await userEvent.click(screen.getByText('Model Validation'));
// Summary cards should render key metric labels unique to the validation summary
await waitFor(() => {
expect(screen.getByText('Brier Score')).toBeInTheDocument();
expect(screen.getByText('ECE')).toBeInTheDocument();
expect(screen.getByText('Directional Accuracy')).toBeInTheDocument();
expect(screen.getByText('Excess vs SPY')).toBeInTheDocument();
});
}, 10000);
it('renders calibration table with miscalibration warning', async () => {
renderRoute('/ops/model');
await waitFor(() => expect(screen.getByText('Model Performance')).toBeInTheDocument());
await userEvent.click(screen.getByText('Model Validation'));
await waitFor(() => {
expect(screen.getByText('Calibration by Confidence Bucket')).toBeInTheDocument();
});
// Miscalibrated buckets should show warning text
const miscalWarnings = screen.getAllByText('Miscalibrated');
expect(miscalWarnings.length).toBeGreaterThanOrEqual(1);
}, 10000);
it('renders gate status pass/fail indicator', async () => {
renderRoute('/ops/model');
await waitFor(() => expect(screen.getByText('Model Performance')).toBeInTheDocument());
await userEvent.click(screen.getByText('Model Validation'));
// The gate-status endpoint returns passed: false
await waitFor(() => {
expect(screen.getByText(/Live Trading Gate: FAIL/)).toBeInTheDocument();
});
}, 10000);
});
describe('Agents page', () => {
it('renders agent list in sidebar', async () => {
renderRoute('/agents');