1use nautilus_core::correctness::check_valid_string_utf8;
17
18use super::{
19 ClientId, DataEngine, Debug, DeferredCommand, DurationNanos, Instrument, InstrumentAny,
20 InstrumentClass, InstrumentId, OptionChainManager, OptionChainReferencePriceResponse,
21 OptionGreeks, OptionSeriesId, Price, Rc, RefCell, RequestCommand,
22 RequestOptionChainReferencePrice, StrikeRange, SubscribeCommand, SubscribeOptionChain,
23 SubscribeOptionGreeks, SubscriptionKey, TimeEvent, TimeEventCallback, TypedHandler, UUID4,
24 UnixNanos, UnsubscribeCommand, UnsubscribeInstrumentStatus, UnsubscribeOptionChain,
25 UnsubscribeOptionGreeks, UnsubscribeQuotes, VecDeque, Venue, WeakCell, mem, msgbus,
26 publish_external_data_command, switchboard,
27};
28
29const OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT: DurationNanos = DurationNanos::from_secs(30);
30const OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER: &str = "option-chain-reference-price-timeout";
31
32impl DataEngine {
33 #[must_use]
35 pub fn has_option_chain_manager(&self, series_id: &OptionSeriesId) -> bool {
36 self.option_chain_managers.contains_key(series_id)
37 }
38
39 #[must_use]
41 pub fn pending_option_chain_request_count(&self) -> usize {
42 self.pending_option_chain_requests.len()
43 }
44
45 pub(super) fn retain_external_option_chain(
46 &mut self,
47 client_id: ClientId,
48 command: &SubscribeOptionChain,
49 subscribe: &SubscribeCommand,
50 ) {
51 let owner_id = command.correlation_id.unwrap_or(command.command_id);
52 let current_key = (client_id, SubscriptionKey::OptionChain(command.series_id));
53
54 let previous_key = self
55 .subscriptions_external
56 .iter()
57 .find_map(|(key, active)| {
58 (matches!(
59 &key.1,
60 SubscriptionKey::OptionChain(series_id) if *series_id == command.series_id
61 ) && active.acquisitions.contains(&owner_id))
62 .then(|| key.clone())
63 });
64
65 if let Some(previous_key) = previous_key
66 && previous_key != current_key
67 {
68 let release = {
69 let active = self
70 .subscriptions_external
71 .get_mut(&previous_key)
72 .expect("external option chain owner was present");
73 active.acquisitions.remove(&owner_id);
74 active.owners = active.owners.saturating_sub(1);
75 active.owners == 0
76 };
77
78 if release {
79 let active = self
80 .subscriptions_external
81 .remove(&previous_key)
82 .expect("external option chain was present for final release");
83 let unsubscribe =
84 active
85 .command
86 .into_unsubscribe(UUID4::new(), command.ts_init, Some(owner_id));
87 publish_external_data_command(previous_key.0, &unsubscribe);
88 }
89 }
90
91 self.subscriptions_external
92 .retain(current_key.clone(), owner_id, subscribe.clone());
93 self.subscriptions_external
94 .get_mut(¤t_key)
95 .expect("external option chain was retained")
96 .command = subscribe.clone();
97 }
98
99 pub(super) fn feed_option_greeks_to_pre_bootstrap_chain(&mut self, greeks: &OptionGreeks) {
100 let Some(series_id) = self
101 .option_chain_instrument_index
102 .get(&greeks.instrument_id)
103 .copied()
104 else {
105 return;
106 };
107
108 let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
109 return;
110 };
111
112 if manager_rc.borrow().is_bootstrapped() {
113 return;
114 }
115
116 manager_rc.borrow_mut().handle_greeks(greeks);
117
118 if manager_rc.borrow().is_bootstrapped() {
119 self.drain_deferred_commands();
122 self.stop_option_chain_greeks_bootstrap(series_id);
123 }
124 }
125
126 pub(super) fn update_option_chains(&mut self, instrument: &InstrumentAny) {
127 let Some(underlying) = instrument.underlying() else {
128 return;
129 };
130
131 let Some(expiration_ns) = instrument.expiration_ns() else {
132 return;
133 };
134
135 let Some(strike) = instrument.strike_price() else {
136 return;
137 };
138
139 let Some(kind) = instrument.option_kind() else {
140 return;
141 };
142
143 if let Err(e) = check_valid_string_utf8(underlying, "underlying") {
144 log::warn!("Skipping option-chain update for {}: {e}", instrument.id());
145 return;
146 }
147
148 let venue = instrument.id().venue;
149 let settlement = instrument.settlement_currency().code;
150 let series_id = OptionSeriesId::new_derived(venue, underlying, settlement, expiration_ns);
151
152 let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
154 return;
155 };
156
157 let clock = Rc::clone(&self.clock);
158 let client_id = manager_rc.borrow().client_id();
159 let client = self.get_command_client(client_id.as_ref(), Some(&venue));
160
161 if manager_rc
162 .borrow_mut()
163 .add_instrument(instrument.id(), strike, kind, client, &clock)
164 {
165 self.option_chain_instrument_index
166 .insert(instrument.id(), series_id);
167 }
168 }
169
170 pub(super) fn expire_option_chain_instrument(&mut self, instrument_id: InstrumentId) {
177 let Some(series_id) = self.option_chain_instrument_index.remove(&instrument_id) else {
178 return;
179 };
180
181 if self
182 .option_chain_greeks_bootstraps
183 .get(&series_id)
184 .is_some_and(|bootstrap| bootstrap.instrument_id == instrument_id)
185 {
186 self.stop_option_chain_greeks_bootstrap(series_id);
187 }
188
189 let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
190 return;
191 };
192
193 let series_empty = manager_rc
194 .borrow_mut()
195 .handle_instrument_expired(&instrument_id);
196
197 self.drain_deferred_commands();
199
200 log::info!(
201 "Expired instrument {instrument_id} from option chain {series_id} (series_empty={series_empty})",
202 );
203
204 if series_empty {
205 manager_rc.borrow_mut().teardown(&self.clock);
206 self.option_chain_managers.remove(&series_id);
207
208 log::info!("Torn down empty option chain manager for {series_id}");
209 }
210 }
211
212 pub(super) fn drain_deferred_commands(&mut self) {
216 loop {
218 let commands: VecDeque<DeferredCommand> =
219 std::mem::take(&mut *self.deferred_cmd_queue.borrow_mut());
220
221 if commands.is_empty() {
222 break;
223 }
224
225 for cmd in commands {
226 match cmd {
227 DeferredCommand::Subscribe(sub) => {
228 let client = self.get_command_client(sub.client_id(), sub.venue());
229 if let Some(client) = client {
230 client.execute_subscribe(sub);
231 }
232 }
233 DeferredCommand::Unsubscribe(unsub) => {
234 if let Err(e) = self.execute_unsubscribe(&unsub) {
235 log::error!("Failed to execute deferred unsubscribe: {e}");
236 }
237 }
238 DeferredCommand::ExpireInstrument(instrument_id) => {
239 self.expire_option_chain_instrument(instrument_id);
240 }
241 DeferredCommand::ExpireSeries(series_id) => {
242 self.expire_series(series_id);
243 }
244 }
245 }
246 }
247 }
248
249 fn expire_series(&mut self, series_id: OptionSeriesId) {
255 self.stop_option_chain_greeks_bootstrap(series_id);
256
257 let Some(manager_rc) = self.option_chain_managers.get(&series_id).cloned() else {
258 return;
259 };
260
261 let instrument_ids: Vec<InstrumentId> = self
262 .option_chain_instrument_index
263 .iter()
264 .filter(|(_, sid)| **sid == series_id)
265 .map(|(id, _)| *id)
266 .collect();
267
268 for id in &instrument_ids {
269 self.option_chain_instrument_index.remove(id);
270 manager_rc.borrow_mut().handle_instrument_expired(id);
271 }
272
273 manager_rc.borrow_mut().teardown(&self.clock);
274 self.option_chain_managers.remove(&series_id);
275
276 log::info!("Proactively torn down expired option chain {series_id}");
277 }
278
279 pub(super) fn subscribe_option_chain(&mut self, cmd: &SubscribeOptionChain) {
280 if !cmd.series_id.is_derived() {
281 log::error!(
282 "Cannot subscribe option chain {}: explicit reference instruments are unsupported",
283 cmd.series_id,
284 );
285 return;
286 }
287
288 self.drain_deferred_commands();
289 let series_id = cmd.series_id;
290 self.stop_option_chain_greeks_bootstrap(series_id);
291
292 if let Some(old) = self.option_chain_managers.remove(&series_id) {
294 log::info!("Re-subscribing option chain for {series_id}, tearing down previous");
295
296 let (active_ids, old_venue, old_client_id) = {
297 let old = old.borrow();
298 let active_ids = old
299 .all_instrument_ids()
300 .into_iter()
301 .filter(|instrument_id| old.is_instrument_active(instrument_id))
302 .collect::<Vec<_>>();
303 (active_ids, old.venue(), old.client_id())
304 };
305
306 old.borrow_mut().teardown(&self.clock);
307 self.forward_option_chain_unsubscribes(&active_ids, old_venue, old_client_id);
308 }
309
310 self.cancel_pending_option_chain_requests(Some(series_id));
311
312 if !matches!(cmd.strike_range, StrikeRange::Fixed(_))
315 && let Some(client_id) = self
316 .get_client(cmd.client_id.as_ref(), Some(&series_id.venue))
317 .map(|c| c.client_id)
318 && let Some(instrument_id) = {
319 let cache = self.cache.borrow();
320 cache
321 .instruments(&series_id.venue, Some(&series_id.underlying))
322 .iter()
323 .filter(|i| {
324 i.instrument_class() == InstrumentClass::Option
325 && i.expiration_ns() == Some(series_id.expiration_ns)
326 && i.settlement_currency().code == series_id.settlement_currency
327 })
328 .min_by_key(|i| i.id())
329 .map(|i| i.id())
330 }
331 {
332 let request_id = UUID4::new();
333 let ts_init = self.clock.borrow().timestamp_ns();
334
335 let request = RequestOptionChainReferencePrice::new(
336 series_id,
337 instrument_id,
338 Some(client_id),
339 request_id,
340 ts_init,
341 None,
342 );
343 let deadline_ns = ts_init.saturating_add(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT);
344 self.pending_option_chain_requests.insert(
345 request_id,
346 PendingOptionChainRequest {
347 command: cmd.clone(),
348 sample_instrument_id: instrument_id,
349 deadline_ns,
350 },
351 );
352
353 if !self.schedule_option_chain_reference_price_timeout() {
354 self.bootstrap_all_pending_option_chains();
355 return;
356 }
357
358 let req_cmd = RequestCommand::OptionChainReferencePrice(request);
359 if let Err(e) = self.execute_request(req_cmd) {
360 log::warn!("Failed to request option-chain reference price for {series_id}: {e}");
361
362 if let Some(pending) = self.pending_option_chain_requests.remove(&request_id) {
363 self.maintain_option_chain_reference_price_timeout();
364 self.create_option_chain_manager_with_greeks_bootstrap(pending);
365 }
366 }
367
368 return;
369 }
370
371 self.create_option_chain_manager(cmd, None);
372 }
373
374 fn schedule_option_chain_reference_price_timeout(&self) -> bool {
375 let Some(deadline_ns) = self
376 .pending_option_chain_requests
377 .values()
378 .map(|pending| pending.deadline_ns)
379 .min()
380 else {
381 self.clock
382 .borrow_mut()
383 .cancel_timer(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER);
384 return true;
385 };
386
387 if self
388 .clock
389 .borrow()
390 .next_time_ns(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER)
391 == Some(deadline_ns)
392 {
393 return true;
394 }
395
396 let Some(bootstrapper) = self.option_chain_bootstrapper.clone() else {
397 log::error!(
398 "Cannot schedule option-chain reference price timeout: data engine message bus handlers are not registered"
399 );
400 return false;
401 };
402
403 let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_| {
404 bootstrapper.handle_timeout();
405 });
406
407 let callback = TimeEventCallback::from(callback_fn);
408
409 if let Err(e) = self.clock.borrow_mut().set_time_alert_ns(
410 OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER,
411 deadline_ns,
412 Some(callback),
413 Some(true),
414 ) {
415 log::error!("Failed to schedule option-chain reference price timeout: {e}");
416 return false;
417 }
418
419 true
420 }
421
422 fn maintain_option_chain_reference_price_timeout(&mut self) {
423 if !self.schedule_option_chain_reference_price_timeout() {
424 self.bootstrap_all_pending_option_chains();
425 }
426 }
427
428 fn bootstrap_all_pending_option_chains(&mut self) {
429 self.clock
430 .borrow_mut()
431 .cancel_timer(OPTION_CHAIN_REFERENCE_PRICE_TIMEOUT_TIMER);
432 let mut pending: Vec<PendingOptionChainRequest> =
433 mem::take(&mut self.pending_option_chain_requests)
434 .into_values()
435 .collect();
436 pending.sort_unstable_by_key(|pending| pending.command.series_id);
437 for pending in pending {
438 self.create_option_chain_manager_with_greeks_bootstrap(pending);
439 }
440 }
441
442 pub(super) fn cancel_pending_option_chain_requests(
443 &mut self,
444 series_id: Option<OptionSeriesId>,
445 ) -> bool {
446 let request_ids: Vec<UUID4> = self
447 .pending_option_chain_requests
448 .iter()
449 .filter_map(|(request_id, pending)| {
450 (series_id.is_none() || series_id == Some(pending.command.series_id))
451 .then_some(*request_id)
452 })
453 .collect();
454
455 for request_id in &request_ids {
456 self.pending_option_chain_requests.remove(request_id);
457 }
458
459 self.maintain_option_chain_reference_price_timeout();
460
461 !request_ids.is_empty()
462 }
463
464 fn handle_option_chain_reference_price_timeout(&mut self) {
465 let now_ns = self.clock.borrow().timestamp_ns();
466
467 let mut requests: Vec<(OptionSeriesId, UUID4)> = self
468 .pending_option_chain_requests
469 .iter()
470 .filter_map(|(request_id, pending)| {
471 (pending.deadline_ns <= now_ns).then_some((pending.command.series_id, *request_id))
472 })
473 .collect();
474
475 requests.sort_unstable_by_key(|(series_id, _)| *series_id);
476
477 for (_, request_id) in requests {
478 let Some(pending) = self.pending_option_chain_requests.remove(&request_id) else {
479 continue;
480 };
481
482 let series_id = pending.command.series_id;
483 log::warn!(
484 "Option-chain reference price request timed out for {series_id}; bootstrapping from live data"
485 );
486 self.create_option_chain_manager_with_greeks_bootstrap(pending);
487 }
488
489 self.maintain_option_chain_reference_price_timeout();
490 }
491
492 fn create_option_chain_manager(
494 &mut self,
495 cmd: &SubscribeOptionChain,
496 initial_atm_price: Option<Price>,
497 ) -> Rc<RefCell<OptionChainManager>> {
498 let series_id = cmd.series_id;
499 let cache = Rc::clone(&self.cache);
500 let clock = Rc::clone(&self.clock);
501 let priority = self.msgbus_priority;
502 let deferred_cmd_queue = Rc::clone(&self.deferred_cmd_queue);
503
504 let manager_rc = {
505 let client = self.get_command_client(cmd.client_id.as_ref(), Some(&series_id.venue));
506 OptionChainManager::create_and_setup(
507 series_id,
508 &cache,
509 cmd,
510 &clock,
511 priority,
512 client,
513 initial_atm_price,
514 deferred_cmd_queue,
515 )
516 };
517
518 for id in manager_rc.borrow().all_instrument_ids() {
520 self.option_chain_instrument_index.insert(id, series_id);
521 }
522
523 self.option_chain_managers
524 .insert(series_id, Rc::clone(&manager_rc));
525 manager_rc
526 }
527
528 fn create_option_chain_manager_with_greeks_bootstrap(
529 &mut self,
530 pending: PendingOptionChainRequest,
531 ) {
532 let cmd = pending.command;
533 let series_id = cmd.series_id;
534 let instrument_id = pending.sample_instrument_id;
535 let manager = self.create_option_chain_manager(&cmd, None);
536
537 let Some(client_id) = manager.borrow().client_id() else {
538 return;
539 };
540
541 let ownership_handler = TypedHandler::from(|_: &OptionGreeks| {});
542 let topic = switchboard::get_option_greeks_topic(instrument_id);
543 msgbus::subscribe_option_greeks(
544 topic.into(),
545 ownership_handler.clone(),
546 Some(self.msgbus_priority),
547 );
548
549 let replaced = self.option_chain_greeks_bootstraps.insert(
550 series_id,
551 OptionChainGreeksBootstrap {
552 instrument_id,
553 client_id,
554 venue: series_id.venue,
555 ownership_handler,
556 },
557 );
558
559 debug_assert!(
560 replaced.is_none(),
561 "Invariant: each option series has at most one Greeks bootstrap subscription"
562 );
563
564 let ts_init = self.clock.borrow().timestamp_ns();
565
566 if let Err(e) =
567 self.execute_subscribe(SubscribeCommand::OptionGreeks(SubscribeOptionGreeks::new(
568 instrument_id,
569 Some(client_id),
570 Some(series_id.venue),
571 UUID4::new(),
572 ts_init,
573 None,
574 None,
575 )))
576 {
577 log::error!("Failed to subscribe option-chain bootstrap Greeks for {series_id}: {e}");
578 }
579 }
580
581 fn stop_option_chain_greeks_bootstrap(&mut self, series_id: OptionSeriesId) -> bool {
582 let Some(bootstrap) = self.remove_option_chain_greeks_bootstrap(series_id) else {
583 return false;
584 };
585
586 self.release_option_chain_greeks_bootstrap(&bootstrap);
587 true
588 }
589
590 fn remove_option_chain_greeks_bootstrap(
591 &mut self,
592 series_id: OptionSeriesId,
593 ) -> Option<OptionChainGreeksBootstrap> {
594 let bootstrap = self.option_chain_greeks_bootstraps.remove(&series_id)?;
595 let topic = switchboard::get_option_greeks_topic(bootstrap.instrument_id);
596 msgbus::unsubscribe_option_greeks(topic.into(), &bootstrap.ownership_handler);
597 Some(bootstrap)
598 }
599
600 fn release_option_chain_greeks_bootstrap(&mut self, bootstrap: &OptionChainGreeksBootstrap) {
601 let cmd = UnsubscribeCommand::OptionGreeks(UnsubscribeOptionGreeks::new(
602 bootstrap.instrument_id,
603 Some(bootstrap.client_id),
604 Some(bootstrap.venue),
605 UUID4::new(),
606 self.clock.borrow().timestamp_ns(),
607 None,
608 None,
609 ));
610
611 if let Err(e) = self.execute_unsubscribe(&cmd) {
612 log::error!(
613 "Failed to unsubscribe option-chain bootstrap Greeks for {}: {e}",
614 bootstrap.instrument_id
615 );
616 }
617 }
618
619 pub(super) fn clear_option_chain_greeks_bootstraps(&mut self) {
620 let bootstraps = mem::take(&mut self.option_chain_greeks_bootstraps);
621 for bootstrap in bootstraps.into_values() {
622 let topic = switchboard::get_option_greeks_topic(bootstrap.instrument_id);
623 msgbus::unsubscribe_option_greeks(topic.into(), &bootstrap.ownership_handler);
624 }
625 }
626
627 pub(super) fn unsubscribe_option_chain(&mut self, cmd: &UnsubscribeOptionChain) {
628 self.drain_deferred_commands();
629 let series_id = cmd.series_id;
630 let topic = switchboard::get_option_chain_topic(series_id);
631 if msgbus::exact_subscriber_count_option_chain(topic) > 0 {
632 return;
633 }
634
635 let canceled_pending = self.cancel_pending_option_chain_requests(Some(series_id));
636 let canceled_greeks_bootstrap = self.stop_option_chain_greeks_bootstrap(series_id);
637
638 let Some(manager_rc) = self.option_chain_managers.remove(&series_id) else {
639 if !canceled_pending && !canceled_greeks_bootstrap {
640 log::warn!("Cannot unsubscribe option chain for {series_id}: not subscribed");
641 }
642
643 return;
644 };
645
646 let (all_ids, active_ids, venue, client_id) = {
648 let manager = manager_rc.borrow();
649 let all_ids = manager.all_instrument_ids();
650 let active_ids = all_ids
651 .iter()
652 .filter(|instrument_id| manager.is_instrument_active(instrument_id))
653 .copied()
654 .collect::<Vec<_>>();
655 (all_ids, active_ids, manager.venue(), manager.client_id())
656 };
657
658 for id in &all_ids {
660 self.option_chain_instrument_index.remove(id);
661 }
662
663 manager_rc.borrow_mut().teardown(&self.clock);
664
665 self.forward_option_chain_unsubscribes(&active_ids, venue, client_id);
667
668 log::info!("Unsubscribed option chain for {series_id}");
669 }
670
671 fn forward_option_chain_unsubscribes(
673 &mut self,
674 instrument_ids: &[InstrumentId],
675 venue: Venue,
676 client_id: Option<ClientId>,
677 ) {
678 let ts_init = self.clock.borrow().timestamp_ns();
679
680 for instrument_id in instrument_ids {
681 let quote_cmd = UnsubscribeCommand::Quotes(UnsubscribeQuotes::new(
682 *instrument_id,
683 client_id,
684 Some(venue),
685 UUID4::new(),
686 ts_init,
687 None,
688 None,
689 ));
690 let greeks_cmd = UnsubscribeCommand::OptionGreeks(UnsubscribeOptionGreeks::new(
691 *instrument_id,
692 client_id,
693 Some(venue),
694 UUID4::new(),
695 ts_init,
696 None,
697 None,
698 ));
699 let status_cmd =
700 UnsubscribeCommand::InstrumentStatus(UnsubscribeInstrumentStatus::new(
701 *instrument_id,
702 client_id,
703 Some(venue),
704 UUID4::new(),
705 ts_init,
706 None,
707 None,
708 ));
709
710 for cmd in ["e_cmd, &greeks_cmd, &status_cmd] {
711 if let Err(e) = self.execute_unsubscribe(cmd) {
712 log::error!("Failed to execute option chain unsubscribe: {e}");
713 }
714 }
715 }
716 }
717
718 pub(super) fn handle_option_chain_reference_price_response(
719 &mut self,
720 correlation_id: &UUID4,
721 resp: &OptionChainReferencePriceResponse,
722 ) {
723 let Some(pending) = self.pending_option_chain_requests.get(correlation_id) else {
724 log::debug!(
725 "No pending option chain request for correlation_id={correlation_id}, ignoring"
726 );
727 return;
728 };
729
730 if resp.series_id != pending.command.series_id {
731 log::warn!(
732 "Ignoring option-chain reference price response for {}: pending series is {}",
733 resp.series_id,
734 pending.command.series_id,
735 );
736 return;
737 }
738
739 let pending = self
740 .pending_option_chain_requests
741 .remove(correlation_id)
742 .expect("checked above");
743 self.maintain_option_chain_reference_price_timeout();
744 let series_id = pending.command.series_id;
745
746 if let Some(price) = resp.price {
747 log::info!("Reference price for {series_id}: {price} (instant bootstrap)");
748 } else {
749 log::info!(
750 "No reference price available for {series_id}, will bootstrap from live data",
751 );
752 }
753
754 if resp.price.is_some() {
755 self.create_option_chain_manager(&pending.command, resp.price);
756 } else {
757 self.create_option_chain_manager_with_greeks_bootstrap(pending);
758 }
759 }
760}
761
762#[derive(Debug)]
763pub(super) struct OptionChainBootstrapper {
764 engine: WeakCell<DataEngine>,
765}
766
767impl OptionChainBootstrapper {
768 pub(super) fn new(engine: &Rc<RefCell<DataEngine>>) -> Self {
769 Self {
770 engine: WeakCell::from(Rc::downgrade(engine)),
771 }
772 }
773
774 fn handle_timeout(&self) {
775 if let Some(engine) = self.engine.upgrade() {
776 engine
777 .borrow_mut()
778 .handle_option_chain_reference_price_timeout();
779 }
780 }
781}
782
783#[derive(Debug)]
784pub(super) struct PendingOptionChainRequest {
785 command: SubscribeOptionChain,
786 sample_instrument_id: InstrumentId,
787 deadline_ns: UnixNanos,
788}
789
790#[derive(Debug)]
791pub(super) struct OptionChainGreeksBootstrap {
792 instrument_id: InstrumentId,
793 client_id: ClientId,
794 venue: Venue,
795 ownership_handler: TypedHandler<OptionGreeks>,
796}