How VolatiAI handles uncertainty, and why every empty result is information.
Public data is fragmented: same metric under different names, different update cadences, different levels of trust, different definitions of what counts as the same thing. Most correlation dashboards hide that fragmentation and present a single number as if it were a fact.
VolatiAI does the opposite. Every claim carries its own uncertainty — p-value, FDR-corrected significance, confidence interval, stability score, provenance class. When the system cannot verify something, it returns that fact explicitly.
/_correlation — per pair| Field | Meaning | Why it matters |
|---|---|---|
r | Pearson correlation on detrended residuals | Removes shared trend before correlating |
n | Common daily observations | Below 15, correlation is too noisy to trust |
p_value | Two-tailed t-approximation p-value | Naive significance; assumes normality |
p_bh | Benjamini-Hochberg adjusted p, q=0.05, m=~5,000 | Corrects for testing ~5,000 pairs at once — the honest significance |
bh_significant | Whether p_bh < 0.05 | True = survives multiple-testing correction |
perm_p | Permutation p-value (2,000 shuffles) | Assumption-free check; does not assume normality |
ci_low, ci_high | Fisher 95% confidence interval on r | Tight interval = reliable magnitude |
ci_width | ci_high − ci_low | Above 0.6 means too wide to trust |
provenance | Two sources measuring the same underlying quantity | Different from "insight" — validation, not discovery |
stale_data | <25% unique values across window | Detects batch-updated counters posing as correlated series |
contradiction | Prior violated (expected ~1.0, observed 0.777) | Fires when reality disagrees with expectation — data, not noise |
verdict | confirm:provenance / surface / suppress:* / dispute:* | Named outcome — auditable, not a boolean filter |
tier | P (provenance) / A / B / C / D | Reliability class; P is exempt from FDR |
/_correlation?days=N — stability| Field | Meaning |
|---|---|
days_present | How many snapshots contained this pair |
stability_pct | days_present / available_days — does it recur? |
r_mean, r_min, r_max | Correlation range across snapshots |
r_range | r_max − r_min — tight range = stable |
drifts[] | Frobenius distance of r-vector between consecutive snapshots |
pareto | True if no other pair dominates on both stability and |r| |
transitivity | Triples checked for gluing obstruction (A~B, B~C, A≁C) |
/signal — per-sector delta| Field | Meaning |
|---|---|
pct_change | Raw percentage change vs. previous observation |
actual_window_days | Days between current and previous observation |
log_rate (briefing) | log(1 + pct/100) / days — per-day rate, sign-symmetric, window-normalized |
Raw percentage is incomparable across different comparison windows. "+390% over 9.2 days" and "−91% over 10.1 days" have different denominators — the second is actually a faster per-day change despite the smaller magnitude.
log_rate(+390% / 9.2d) = log(4.9) / 9.2 = 0.173
log_rate( −91% / 10.1d) = −log(0.089) / 10.1 = 0.239
Ranked by log-rate, the drop is #1 — because it IS the largest rate of change. Ranked by raw percentage, it would be last. The system ranks by log-rate because that's what "biggest change" actually means once you account for window length.
With ~5,000 tested pairs, raw p < 0.05 would produce ~250 false positives. Bonferroni (α/m) is too strict — it controls the probability of *any* false positive, which is overkill. BH-FDR controls the *expected fraction* of false positives among discoveries, which is what actually matters when ranking candidates.
Bonferroni threshold at m=5,000: 0.05/5000 = 1e-5
BH rank-1 threshold at q=0.05: (1/5000)*0.05 = 1e-5
BH rank-100 threshold: (100/5000)*0.05 = 1e-3
BH is less strict at higher ranks — appropriate for a discovery-ranked list where the top result gets more scrutiny than the 100th.
A provenance pair is two sectors that measure the same underlying quantity under different names, from different sources. treasuries and treasuries-gov both report yield_10y — different endpoints, same metric. cisa-kev-count and cisa-kev-total both count KEV entries.
These pairs are not insights. They are controls — validation that the pipeline can still detect a known-same pair after all the noise filtering. They get tier: P and bypass multiple-testing correction because the null hypothesis "these are unrelated" is known to be false by construction.
A prior encodes what the relationship should look like, independent of observed data:
{ expect: "identical", tolerance: 0.25 } // r should be ≈ 1.0
{ expect: "independent", tolerance: 0.40 } // r should be ≈ 0.0
{ expect: "inverse", tolerance: 0.10 } // r should be ≈ −1.0
If observed r falls outside the tolerance, contradiction fires with the gap. A contradiction is data — either a real coupling, a bug in one collector, or a wrong prior. Each is worth knowing.
Contradiction detection currently covers FX only. The CONFLICT_GROUPS registry in the worker has exactly two sectors:
currency-ex → fx_usd_eur groupexchange-rate → fx_usd_eur groupBoth measure USD↔EUR exchange rate from different sources. When they disagree by more than 20% after reciprocal normalization, a metric_divergence contradiction fires. When they move in opposite directions by more than 1%, a directional_conflict fires.
This is honest scope, not a bug. When the digest says "0 contradictions", that means "the two FX sources agree today" — not "no semantic contradictions exist anywhere". Every other sector is single-source and cannot contradict itself.
Expanding this registry to include other multi-source metrics is a separate project. The semantic registry at config/semantic_registry.json documents the intent; wiring it into CONFLICT_GROUPS is planned for a future iteration. Candidate pairs for expansion are found by scripts/find_candidates.py.
Every claim on this site can be independently verified. The code is public. The data is public. The computation is deterministic. When the system is wrong, the same numbers that made it wrong are visible.
Snapshots will be cryptographically signed (Ed25519) once the pipeline has accumulated a week of real cron-written history. Then any past claim can be verified byte-for-byte against the signed record.