1use std::collections::HashMap;
2
3use super::risk_oracle::{
4 RiskVolOracle, SharedVolOracle, VolLookupError, VolOracleStatus, VolSurfaceSnapshot,
5};
6
7pub 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 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 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 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}