Methodology

How VolatiAI handles uncertainty, and why every empty result is information.

The premise

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.

Empty results are information, not absence. "0 triples checked" means we looked and found nothing to check. "drifts: []" means we have one snapshot and cannot compute a drift yet. Both are true, both are displayed.

Every field, explained

On /_correlation — per pair

FieldMeaningWhy it matters
rPearson correlation on detrended residualsRemoves shared trend before correlating
nCommon daily observationsBelow 15, correlation is too noisy to trust
p_valueTwo-tailed t-approximation p-valueNaive significance; assumes normality
p_bhBenjamini-Hochberg adjusted p, q=0.05, m=~5,000Corrects for testing ~5,000 pairs at once — the honest significance
bh_significantWhether p_bh < 0.05True = survives multiple-testing correction
perm_pPermutation p-value (2,000 shuffles)Assumption-free check; does not assume normality
ci_low, ci_highFisher 95% confidence interval on rTight interval = reliable magnitude
ci_widthci_high − ci_lowAbove 0.6 means too wide to trust
provenanceTwo sources measuring the same underlying quantityDifferent from "insight" — validation, not discovery
stale_data<25% unique values across windowDetects batch-updated counters posing as correlated series
contradictionPrior violated (expected ~1.0, observed 0.777)Fires when reality disagrees with expectation — data, not noise
verdictconfirm:provenance / surface / suppress:* / dispute:*Named outcome — auditable, not a boolean filter
tierP (provenance) / A / B / C / DReliability class; P is exempt from FDR

On /_correlation?days=N — stability

FieldMeaning
days_presentHow many snapshots contained this pair
stability_pctdays_present / available_days — does it recur?
r_mean, r_min, r_maxCorrelation range across snapshots
r_ranger_max − r_min — tight range = stable
drifts[]Frobenius distance of r-vector between consecutive snapshots
paretoTrue if no other pair dominates on both stability and |r|
transitivityTriples checked for gluing obstruction (A~B, B~C, A≁C)

On /signal — per-sector delta

FieldMeaning
pct_changeRaw percentage change vs. previous observation
actual_window_daysDays between current and previous observation
log_rate (briefing)log(1 + pct/100) / days — per-day rate, sign-symmetric, window-normalized

Why log-rate, not raw percentage

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.

Why BH, not Bonferroni

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.

What "provenance" means — and doesn't

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.

What contradiction priors mean

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 scope

Contradiction detection currently covers FX only. The CONFLICT_GROUPS registry in the worker has exactly two sectors:

Both 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.

What the system does NOT do

The commitment

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.