Skip to main content

nautilus_data/engine/
option_chain.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Returns whether an `OptionChainManager` exists for the given series.
34    #[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    /// Returns the count of pending option-chain bootstrap requests.
40    #[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(&current_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            // Apply the chain's own subscribes before releasing the bootstrap owner, so a sample
120            // inside the active window keeps its feed without a physical unsubscribe.
121            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        // Clone Rc to release borrow on self.option_chain_managers before accessing self.clients
153        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    /// Removes a settled/expired instrument from its option chain manager.
171    ///
172    /// Looks up the owning series via the reverse index, delegates removal to
173    /// the manager (which unregisters msgbus handlers and pushes deferred wire
174    /// unsubscribes), then drains those commands. When the series catalog
175    /// becomes empty, the entire manager is torn down.
176    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        // Drain deferred unsubscribe commands pushed by the manager
198        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    /// Drains deferred subscribe/unsubscribe commands pushed by option chain
213    /// managers (or any other component) and executes them against the appropriate
214    /// data client.
215    pub(super) fn drain_deferred_commands(&mut self) {
216        // Loop because expire_series pushes Unsubscribe commands; converges in <= 3 iterations
217        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    /// Proactively expires all instruments for a series and tears down the manager.
250    ///
251    /// `handle_instrument_expired` removes each instrument from the aggregator and pushes
252    /// deferred unsubscribe commands. `teardown` then cancels the snapshot timer and clears
253    /// the handler lists (the aggregator is already empty at that point).
254    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        // Handle edits to existing subscriptions by tearing down and re-setting up the OptionChainManager.
293        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        // For dynamic strike ranges, request a reference price from the adapter
313        // to enable instant bootstrap without waiting for the first WebSocket tick.
314        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    /// Creates and stores an `OptionChainManager` for the given subscription.
493    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        // Index all instruments for reverse lookup
519        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        // Extract info before teardown
647        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        // Remove all instruments from reverse index
659        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        // Forward wire-level unsubscribes to the data client
666        self.forward_option_chain_unsubscribes(&active_ids, venue, client_id);
667
668        log::info!("Unsubscribed option chain for {series_id}");
669    }
670
671    /// Forwards wire-level unsubscribe commands for all option chain instruments.
672    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 [&quote_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}