Skip to main content

hypercall_vol_oracle/
routed_oracle.rs

1use std::collections::HashMap;
2
3use super::risk_oracle::{
4    RiskVolOracle, SharedVolOracle, VolLookupError, VolOracleStatus, VolSurfaceSnapshot,
5};
6
7/// Router that dispatches `get_iv` calls to one of several underlying
8/// providers, optionally with a fallback chain per underlying.
9///
10/// Each underlying maps to an ordered `Vec<String>` of provider names.
11/// `get_iv` tries each provider in order and returns the first `Ok`.
12/// On failure it records the last error and moves to the next. Only if
13/// every provider in the chain fails does the call return an error —
14/// and then it returns the *last* error seen, which is typically the
15/// most informative (the fallback's "actually broken" state rather than
16/// the primary's "waiting for data" state).
17///
18/// This is how we prevent a single unhealthy oracle (e.g. Databento on
19/// a Sunday cold start before CME opens) from cascading into a SPAN
20/// margin failure that blocks every order across every underlying.
21pub struct RoutedVolOracle {
22    providers: HashMap<String, SharedVolOracle>,
23    routes: HashMap<String, Vec<String>>,
24}
25
26impl RoutedVolOracle {
27    pub fn new(
28        providers: HashMap<String, SharedVolOracle>,
29        routes: HashMap<String, Vec<String>>,
30    ) -> Self {
31        Self { providers, routes }
32    }
33
34    pub fn empty() -> Self {
35        Self {
36            providers: HashMap::new(),
37            routes: HashMap::new(),
38        }
39    }
40}
41
42impl RiskVolOracle for RoutedVolOracle {
43    fn get_iv(&self, underlying: &str, strike: f64, expiry_ts: i64) -> Result<f64, VolLookupError> {
44        let chain =
45            self.routes
46                .get(underlying)
47                .ok_or_else(|| VolLookupError::UnsupportedUnderlying {
48                    underlying: underlying.to_string(),
49                })?;
50
51        let mut last_err: Option<VolLookupError> = None;
52        for provider_name in chain {
53            let Some(provider) = self.providers.get(provider_name) else {
54                // A named provider in the chain isn't configured — shouldn't
55                // happen if the catalog validator did its job, but degrade
56                // to the next entry rather than fail the whole call.
57                continue;
58            };
59            match provider.get_iv(underlying, strike, expiry_ts) {
60                Ok(iv) => return Ok(iv),
61                Err(err) => {
62                    last_err = Some(err);
63                }
64            }
65        }
66
67        // Every provider in the chain failed. Surface the last error
68        // (usually the fallback's "really broken" state) or synthesise
69        // an UnsupportedUnderlying if the chain was empty / all
70        // providers were missing.
71        Err(
72            last_err.unwrap_or_else(|| VolLookupError::UnsupportedUnderlying {
73                underlying: underlying.to_string(),
74            }),
75        )
76    }
77
78    fn statuses(&self) -> Vec<VolOracleStatus> {
79        let route_facing_pairs = self
80            .routes
81            .iter()
82            .flat_map(|(underlying, providers)| {
83                providers
84                    .iter()
85                    .map(|provider| (underlying.clone(), provider.clone()))
86            })
87            .collect::<std::collections::HashSet<_>>();
88        let mut statuses = self
89            .providers
90            .iter()
91            .flat_map(|(provider_name, provider)| {
92                let provider_name = provider_name.clone();
93                let route_facing_pairs = &route_facing_pairs;
94                provider.statuses().into_iter().map(move |mut status| {
95                    status.route_facing = route_facing_pairs
96                        .contains(&(status.underlying.clone(), provider_name.clone()));
97                    status
98                })
99            })
100            .collect::<Vec<_>>();
101        statuses.sort_by(|left, right| {
102            left.underlying
103                .cmp(&right.underlying)
104                .then_with(|| left.provider.as_str().cmp(right.provider.as_str()))
105        });
106        statuses
107    }
108
109    fn get_surface_snapshot(&self, underlying: &str) -> Option<VolSurfaceSnapshot> {
110        let chain = self.routes.get(underlying)?;
111        for provider_name in chain {
112            if let Some(provider) = self.providers.get(provider_name) {
113                if let Some(snapshot) = provider.get_surface_snapshot(underlying) {
114                    return Some(snapshot);
115                }
116            }
117        }
118        None
119    }
120
121    fn supports_surface_snapshots(&self) -> bool {
122        self.routes
123            .values()
124            .flatten()
125            .filter_map(|provider_name| self.providers.get(provider_name))
126            .any(|provider| provider.supports_surface_snapshots())
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::super::risk_oracle::VolProviderKind;
133    use super::*;
134    use std::sync::Arc;
135
136    /// Test oracle that returns a fixed outcome for every lookup.
137    struct StubOracle {
138        underlying: String,
139        provider: VolProviderKind,
140        outcome: Result<f64, VolLookupError>,
141        supports_surface_snapshots: bool,
142    }
143
144    impl RiskVolOracle for StubOracle {
145        fn get_iv(
146            &self,
147            _underlying: &str,
148            _strike: f64,
149            _expiry_ts: i64,
150        ) -> Result<f64, VolLookupError> {
151            self.outcome.clone()
152        }
153
154        fn statuses(&self) -> Vec<VolOracleStatus> {
155            vec![VolOracleStatus {
156                underlying: self.underlying.clone(),
157                provider: self.provider.clone(),
158                route_facing: true,
159                connected: true,
160                ready: true,
161                last_update_ts_ms: None,
162                staleness_seconds: None,
163                staleness_threshold_seconds: None,
164                surface_points: 0,
165                messages_received: 0,
166                last_error: None,
167            }]
168        }
169
170        fn supports_surface_snapshots(&self) -> bool {
171            self.supports_surface_snapshots
172        }
173    }
174
175    fn ok_stub(iv: f64) -> SharedVolOracle {
176        Arc::new(StubOracle {
177            underlying: "GOLD".to_string(),
178            provider: VolProviderKind::Databento,
179            outcome: Ok(iv),
180            supports_surface_snapshots: false,
181        })
182    }
183
184    fn err_stub(reason: &str) -> SharedVolOracle {
185        Arc::new(StubOracle {
186            underlying: "GOLD".to_string(),
187            provider: VolProviderKind::Databento,
188            outcome: Err(VolLookupError::UnhealthyProvider {
189                underlying: "GOLD".to_string(),
190                provider: VolProviderKind::Databento,
191                reason: reason.to_string(),
192            }),
193            supports_surface_snapshots: false,
194        })
195    }
196
197    fn snapshot_stub() -> SharedVolOracle {
198        Arc::new(StubOracle {
199            underlying: "GOLD".to_string(),
200            provider: VolProviderKind::Databento,
201            outcome: Ok(0.42),
202            supports_surface_snapshots: true,
203        })
204    }
205
206    #[test]
207    fn single_entry_chain_returns_provider_value() {
208        let routed = RoutedVolOracle::new(
209            HashMap::from([("databento_main".to_string(), ok_stub(0.42))]),
210            HashMap::from([("GOLD".to_string(), vec!["databento_main".to_string()])]),
211        );
212        assert_eq!(routed.get_iv("GOLD", 4700.0, 1_800_000_000).unwrap(), 0.42);
213    }
214
215    #[test]
216    fn falls_through_unhealthy_to_next_provider() {
217        let routed = RoutedVolOracle::new(
218            HashMap::from([
219                ("databento_main".to_string(), err_stub("no GC forward")),
220                ("gold_fixed".to_string(), ok_stub(0.50)),
221            ]),
222            HashMap::from([(
223                "GOLD".to_string(),
224                vec!["databento_main".to_string(), "gold_fixed".to_string()],
225            )]),
226        );
227        assert_eq!(routed.get_iv("GOLD", 4700.0, 1_800_000_000).unwrap(), 0.50);
228    }
229
230    #[test]
231    fn every_provider_failing_returns_last_error() {
232        let routed = RoutedVolOracle::new(
233            HashMap::from([
234                ("a".to_string(), err_stub("first broken")),
235                ("b".to_string(), err_stub("second broken")),
236            ]),
237            HashMap::from([("GOLD".to_string(), vec!["a".to_string(), "b".to_string()])]),
238        );
239        let err = routed
240            .get_iv("GOLD", 4700.0, 1_800_000_000)
241            .expect_err("both providers err");
242        match err {
243            VolLookupError::UnhealthyProvider { reason, .. } => {
244                assert_eq!(reason, "second broken");
245            }
246            other => panic!("expected UnhealthyProvider, got {other:?}"),
247        }
248    }
249
250    #[test]
251    fn missing_underlying_returns_unsupported() {
252        let routed = RoutedVolOracle::new(HashMap::new(), HashMap::new());
253        let err = routed
254            .get_iv("GOLD", 4700.0, 1_800_000_000)
255            .expect_err("no route");
256        assert!(matches!(err, VolLookupError::UnsupportedUnderlying { .. }));
257    }
258
259    #[test]
260    fn missing_provider_in_chain_is_skipped_not_fatal() {
261        let routed = RoutedVolOracle::new(
262            HashMap::from([("gold_fixed".to_string(), ok_stub(0.50))]),
263            HashMap::from([(
264                "GOLD".to_string(),
265                vec!["ghost".to_string(), "gold_fixed".to_string()],
266            )]),
267        );
268        assert_eq!(routed.get_iv("GOLD", 4700.0, 1_800_000_000).unwrap(), 0.50);
269    }
270
271    #[test]
272    fn snapshot_capability_only_counts_routed_providers() {
273        let routed = RoutedVolOracle::new(
274            HashMap::from([
275                ("unused_snapshot".to_string(), snapshot_stub()),
276                ("gold_fixed".to_string(), ok_stub(0.50)),
277            ]),
278            HashMap::from([("GOLD".to_string(), vec!["gold_fixed".to_string()])]),
279        );
280        assert!(!routed.supports_surface_snapshots());
281    }
282
283    #[test]
284    fn statuses_mark_only_route_providers_as_route_facing() {
285        let routed = RoutedVolOracle::new(
286            HashMap::from([
287                ("internal_source".to_string(), snapshot_stub()),
288                ("route_model".to_string(), ok_stub(0.50)),
289            ]),
290            HashMap::from([("GOLD".to_string(), vec!["route_model".to_string()])]),
291        );
292
293        let statuses = routed.statuses();
294        assert_eq!(statuses.len(), 2);
295        assert_eq!(
296            statuses.iter().filter(|status| status.route_facing).count(),
297            1
298        );
299        assert_eq!(
300            statuses
301                .iter()
302                .filter(|status| !status.route_facing)
303                .count(),
304            1
305        );
306    }
307}