Closes #633
Per-market oracle deviation is currently a single bound. This PR tracks a rolling deviation history and rejects quotes that deviate from the rolling median by more than a configurable z-multiple.
- Added
OracleQuoteOutliererror variant (code 507) with description and canonical string code
- Added
max_deviation_z_multiple: Option<u32>toGlobalOracleValidationConfig— configurable z-multiple threshold in basis points (e.g., 500 = 5%) - Added
history_size: Option<u32>toGlobalOracleValidationConfig— configurable ring-buffer depth (defaults to 10) - Same fields added to
EventOracleValidationConfigfor per-event overrides
OracleDeviationHistory: New ring-buffer type storing historical prices per market with FIFO evictionpush()— insert price; evicts oldest when at capacitypop_last()— revert the last push (used when an outlier is rejected)rolling_median()— compute median using i128 integer sort (even count → lower middle)mad()— Median Absolute Deviation for future use
OracleValidationConfigManager:- Updated
get_effective_configto pass through new fields - Added
get_or_init_history,save_history,history_keyfor per-market ring buffer storage - Modified
validate_oracle_data: whenmax_deviation_z_multipleis set, computes rolling median from the ring buffer and rejects quotes deviating beyond the threshold withOracleQuoteOutlier. Outliers are not persisted in the history. Whenmax_deviation_z_multipleisNone, falls back to legacy single-referencemax_deviation_bpscheck. - Added
get_deviation_historyandclear_deviation_historypublic helpers - Updated
validate_config_valuesto validatemax_deviation_z_multiple(rejects 0 or > 10_000 bps) - Updated
set_global_configandset_event_configto pass the new field through validation
- Updated
Comprehensive test suite (15 tests):
OracleDeviationHistoryunit tests: empty, single, odd/even median, FIFO eviction, pop_last, MAD, capacity 0, determinism- Rolling median integration tests: first price accepted, similar price accepted, outlier rejected, outlier not persisted, multiple stable prices pass, clear history
- Legacy deviation compatibility test (when rolling median disabled)
- Config validation tests (zero/threshold bounds)
- Outlier rejection deterministic (same inputs → same median)
- History size from config (defaults to 10)
- No
unwrap()introduced - Documented in
oracles.rs
cargo test -p predictify-hybrid -- oracle_rolling_deviation --nocaptureEdge cases covered:
- Empty history (median returns None)
- Single price (always accepted)
- Even/odd entry counts in median calculation
- Ring buffer eviction at capacity
- pop_last on non-empty and empty buffers
- MAD computation with < 2 entries
- Capacity=0 defaults to 1
- Deterministic median from same inputs
- Outlier not persisted in history
- Multiple stable prices accumulate in history
- Legacy deviation still works when rolling median disabled