1use super::options_volume::Snapshot;
4use super::{Datafeed, FeedError};
5use chrono::{DateTime, NaiveDate, Utc};
6use std::ops::RangeInclusive;
7use std::sync::{Arc, Weak};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11#[non_exhaustive]
12pub struct OptionChainQuery {
13 pub symbol: String,
15 pub expiries: RangeInclusive<NaiveDate>,
17 pub as_of: Option<DateTime<Utc>>,
19}
20
21impl OptionChainQuery {
22 pub fn new(symbol: impl Into<String>, expiries: RangeInclusive<NaiveDate>) -> Self {
24 Self {
25 symbol: symbol.into(),
26 expiries,
27 as_of: None,
28 }
29 }
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum OptionKind {
36 Call,
38 Put,
40}
41
42#[derive(Clone, Debug, PartialEq)]
46#[non_exhaustive]
47pub struct OptionQuote {
48 pub symbol: String,
50 pub expiry: NaiveDate,
52 pub strike: f64,
54 pub kind: OptionKind,
56 pub multiplier: Option<f64>,
58 pub time: DateTime<Utc>,
60 pub bid: Option<f64>,
62 pub ask: Option<f64>,
64 pub last: Option<f64>,
66 pub volume: Option<f64>,
68 pub open_interest: Option<f64>,
70 pub implied_volatility: Option<f64>,
72 pub delta: Option<f64>,
74 pub gamma: Option<f64>,
76 pub theta: Option<f64>,
78 pub vega: Option<f64>,
81 pub underlying_price: Option<f64>,
83}
84
85impl OptionQuote {
86 pub fn new(
88 symbol: impl Into<String>,
89 expiry: NaiveDate,
90 strike: f64,
91 kind: OptionKind,
92 time: DateTime<Utc>,
93 ) -> Self {
94 Self {
95 symbol: symbol.into(),
96 expiry,
97 strike,
98 kind,
99 multiplier: None,
100 time,
101 bid: None,
102 ask: None,
103 last: None,
104 volume: None,
105 open_interest: None,
106 implied_volatility: None,
107 delta: None,
108 gamma: None,
109 theta: None,
110 vega: None,
111 underlying_price: None,
112 }
113 }
114
115 fn validate(&self, query: &OptionChainQuery) -> Result<(), FeedError> {
116 let invalid = |field| FeedError::InvalidData(format!("contract {}: {field}", self.symbol));
117 if !query.expiries.contains(&self.expiry) {
118 return Err(invalid("expiry outside query range"));
119 }
120 for (field, value) in [
121 ("strike", Some(self.strike)),
122 ("multiplier", self.multiplier),
123 ] {
124 if value.is_some_and(|value| !value.is_finite() || value <= 0.0) {
125 return Err(invalid(field));
126 }
127 }
128 for (field, value) in [
129 ("bid", self.bid),
130 ("ask", self.ask),
131 ("last", self.last),
132 ("volume", self.volume),
133 ("open_interest", self.open_interest),
134 ("implied_volatility", self.implied_volatility),
135 ("underlying_price", self.underlying_price),
136 ] {
137 if value.is_some_and(|value| !value.is_finite() || value < 0.0) {
138 return Err(invalid(field));
139 }
140 }
141 for (field, value) in [
142 ("delta", self.delta),
143 ("gamma", self.gamma),
144 ("theta", self.theta),
145 ("vega", self.vega),
146 ] {
147 if value.is_some_and(|value| !value.is_finite()) {
148 return Err(invalid(field));
149 }
150 }
151 Ok(())
152 }
153}
154
155struct ChainState {
156 query: OptionChainQuery,
157 rows: Arc<[OptionQuote]>,
158 error: Option<FeedError>,
159 finished: bool,
160}
161
162#[derive(Clone)]
165pub struct OptionChainSink(Weak<Snapshot<ChainState>>);
166
167impl OptionChainSink {
168 pub fn push(&self, rows: Vec<OptionQuote>) -> Result<(), FeedError> {
174 let Some(snapshot) = self.0.upgrade() else {
175 return Ok(());
176 };
177 let result = snapshot.update(|state| {
178 if state.finished {
179 return None;
180 }
181 let result = if rows.len() > 100_000 {
182 Err(FeedError::InvalidData(format!(
183 "contract {}: rows exceeds 100,000",
184 rows[100_000].symbol
185 )))
186 } else {
187 rows.iter().try_for_each(|row| row.validate(&state.query))
188 };
189 Some(match result {
190 Ok(()) => {
191 state.rows = rows.into();
192 state.error = None;
193 state.finished = state.query.as_of.is_some();
194 Ok(())
195 }
196 Err(error) => {
197 state.error = Some(error.clone());
198 Err(error)
199 }
200 })
201 });
202 result.unwrap_or(Ok(()))
203 }
204
205 pub fn report_error(&self, error: FeedError) {
208 if let Some(snapshot) = self.0.upgrade() {
209 snapshot.update(|state| {
210 if state.finished {
211 return None;
212 }
213 state.error = Some(error);
214 state.finished = state.query.as_of.is_some();
215 Some(())
216 });
217 }
218 }
219
220 pub fn is_active(&self) -> bool {
223 self.0
224 .upgrade()
225 .is_some_and(|snapshot| snapshot.read(|state| !state.finished))
226 }
227}
228
229#[derive(Clone)]
232pub struct OptionChain(Arc<Snapshot<ChainState>>);
233
234impl OptionChain {
235 pub fn new(
239 feed: &dyn Datafeed,
240 query: OptionChainQuery,
241 wake: impl Fn() + Send + Sync + 'static,
242 ) -> Option<Self> {
243 if query.symbol.is_empty() || query.expiries.is_empty() {
244 return None;
245 }
246 let snapshot = Snapshot::new(
247 ChainState {
248 query: query.clone(),
249 rows: Arc::from([]),
250 error: None,
251 finished: false,
252 },
253 wake,
254 );
255 feed.option_chain(query, OptionChainSink(Arc::downgrade(&snapshot)));
256 Some(Self(snapshot))
257 }
258
259 pub fn rows(&self) -> Arc<[OptionQuote]> {
262 self.0.read(|state| Arc::clone(&state.rows))
263 }
264
265 pub fn error(&self) -> Option<FeedError> {
267 self.0.read(|state| state.error.clone())
268 }
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
273#[non_exhaustive]
274pub enum OptionExposureKind {
275 Gamma,
277 Delta,
279 Vanna,
281 Charm,
283}
284
285#[derive(Clone, Debug, PartialEq, Eq)]
287#[non_exhaustive]
288pub struct OptionExposureQuery {
289 pub symbol: String,
291 pub expiries: RangeInclusive<NaiveDate>,
293 pub kind: OptionExposureKind,
295 pub as_of: Option<DateTime<Utc>>,
297}
298
299impl OptionExposureQuery {
300 pub fn new(
302 symbol: impl Into<String>,
303 expiries: RangeInclusive<NaiveDate>,
304 kind: OptionExposureKind,
305 ) -> Self {
306 Self {
307 symbol: symbol.into(),
308 expiries,
309 kind,
310 as_of: None,
311 }
312 }
313}
314
315#[derive(Clone, Debug, PartialEq)]
317#[non_exhaustive]
318pub struct OptionExposureRow {
319 pub expiry: NaiveDate,
321 pub strike: f64,
323 pub calls: Option<f64>,
325 pub puts: Option<f64>,
327 pub net: Option<f64>,
329}
330
331impl OptionExposureRow {
332 pub fn new(expiry: NaiveDate, strike: f64) -> Self {
334 Self {
335 expiry,
336 strike,
337 calls: None,
338 puts: None,
339 net: None,
340 }
341 }
342}
343
344#[derive(Clone, Debug, PartialEq)]
346#[non_exhaustive]
347pub struct OptionExposure {
348 pub kind: OptionExposureKind,
350 pub time: DateTime<Utc>,
352 pub unit: String,
354 pub model: Option<String>,
356 pub rows: Vec<OptionExposureRow>,
358 pub missing_contracts: usize,
360}
361
362impl OptionExposure {
363 pub fn new(
365 kind: OptionExposureKind,
366 time: DateTime<Utc>,
367 unit: impl Into<String>,
368 rows: Vec<OptionExposureRow>,
369 ) -> Self {
370 Self {
371 kind,
372 time,
373 unit: unit.into(),
374 model: None,
375 rows,
376 missing_contracts: 0,
377 }
378 }
379
380 pub fn gamma<'a>(
390 quotes: impl IntoIterator<Item = &'a OptionQuote>,
391 ) -> Result<Option<Self>, FeedError> {
392 let mut time = None;
393 let mut missing_contracts = 0;
394 let mut contributions = Vec::new();
395 for quote in quotes {
396 time = Some(time.map_or(quote.time, |time: DateTime<Utc>| time.max(quote.time)));
397 let invalid =
398 |field| FeedError::InvalidData(format!("contract {}: {field}", quote.symbol));
399 if !quote.strike.is_finite() || quote.strike <= 0.0 {
400 return Err(invalid("strike"));
401 }
402 let (Some(gamma), Some(interest), Some(multiplier), Some(price)) = (
403 quote.gamma,
404 quote.open_interest,
405 quote.multiplier,
406 quote.underlying_price,
407 ) else {
408 missing_contracts += 1;
409 continue;
410 };
411 for (field, value, invalid_sign) in [
412 ("gamma", gamma, false),
413 ("open_interest", interest, interest < 0.0),
414 ("multiplier", multiplier, multiplier <= 0.0),
415 ("underlying_price", price, price < 0.0),
416 ] {
417 if !value.is_finite() || invalid_sign {
418 return Err(invalid(field));
419 }
420 }
421 let value = if gamma == 0.0 || interest == 0.0 || price == 0.0 {
422 0.0
423 } else {
424 gamma * interest * multiplier * price * price * 0.01
425 };
426 if !value.is_finite() {
427 return Err(invalid("gamma exposure overflow"));
428 }
429 let mut row = OptionExposureRow::new(quote.expiry, quote.strike);
430 match quote.kind {
431 OptionKind::Call => row.calls = Some(value),
432 OptionKind::Put => row.puts = Some(-value),
433 }
434 contributions.push(row);
435 }
436 let Some(time) = time else {
437 return Ok(None);
438 };
439 contributions.sort_by(|a, b| {
440 a.expiry
441 .cmp(&b.expiry)
442 .then_with(|| a.strike.total_cmp(&b.strike))
443 });
444 let mut rows: Vec<OptionExposureRow> = Vec::new();
445 for contribution in contributions {
446 if let Some(row) = rows.last_mut()
447 && row.expiry == contribution.expiry
448 && row.strike == contribution.strike
449 {
450 if let Some(value) = contribution.calls {
451 row.calls = Some(row.calls.unwrap_or(0.0) + value);
452 }
453 if let Some(value) = contribution.puts {
454 row.puts = Some(row.puts.unwrap_or(0.0) + value);
455 }
456 } else {
457 rows.push(contribution);
458 }
459 let row = rows.last_mut().unwrap();
460 row.net = Some(row.calls.unwrap_or(0.0) + row.puts.unwrap_or(0.0));
461 if [row.calls, row.puts, row.net]
462 .into_iter()
463 .flatten()
464 .any(|value| !value.is_finite())
465 {
466 return Err(FeedError::InvalidData(format!(
467 "exposure {} at {}: sum overflow",
468 row.expiry, row.strike
469 )));
470 }
471 }
472 let mut exposure = Self::new(
473 OptionExposureKind::Gamma,
474 time,
475 "price currency per 1% move",
476 rows,
477 );
478 exposure.missing_contracts = missing_contracts;
479 Ok(Some(exposure))
480 }
481
482 fn validate(&self, query: &OptionExposureQuery) -> Result<(), FeedError> {
483 if self.kind != query.kind {
484 return Err(FeedError::InvalidData(
485 "exposure kind differs from query".into(),
486 ));
487 }
488 if self.rows.len() > 100_000 {
489 return Err(FeedError::InvalidData(
490 "exposure rows exceeds 100,000".into(),
491 ));
492 }
493 for row in &self.rows {
494 let invalid = |field| {
495 FeedError::InvalidData(format!(
496 "exposure {} at {}: {field}",
497 row.expiry, row.strike
498 ))
499 };
500 if !query.expiries.contains(&row.expiry) {
501 return Err(invalid("expiry outside query range"));
502 }
503 if !row.strike.is_finite() || row.strike <= 0.0 {
504 return Err(invalid("strike"));
505 }
506 for (field, value) in [("calls", row.calls), ("puts", row.puts), ("net", row.net)] {
507 if value.is_some_and(|value| !value.is_finite()) {
508 return Err(invalid(field));
509 }
510 }
511 }
512 Ok(())
513 }
514}
515
516struct ExposureState {
517 query: OptionExposureQuery,
518 value: Option<Arc<OptionExposure>>,
519 error: Option<FeedError>,
520 finished: bool,
521}
522
523#[derive(Clone)]
526pub struct OptionExposureSink(Weak<Snapshot<ExposureState>>);
527
528impl OptionExposureSink {
529 pub fn push(&self, value: OptionExposure) -> Result<(), FeedError> {
534 let Some(snapshot) = self.0.upgrade() else {
535 return Ok(());
536 };
537 snapshot
538 .update(|state| {
539 if state.finished {
540 return None;
541 }
542 Some(match value.validate(&state.query) {
543 Ok(()) => {
544 state.value = Some(Arc::new(value));
545 state.error = None;
546 state.finished = state.query.as_of.is_some();
547 Ok(())
548 }
549 Err(error) => {
550 state.error = Some(error.clone());
551 Err(error)
552 }
553 })
554 })
555 .unwrap_or(Ok(()))
556 }
557
558 pub fn report_error(&self, error: FeedError) {
561 if let Some(snapshot) = self.0.upgrade() {
562 snapshot.update(|state| {
563 if state.finished {
564 return None;
565 }
566 state.error = Some(error);
567 state.finished = state.query.as_of.is_some();
568 Some(())
569 });
570 }
571 }
572
573 pub fn is_active(&self) -> bool {
575 self.0
576 .upgrade()
577 .is_some_and(|snapshot| snapshot.read(|state| !state.finished))
578 }
579}
580
581#[derive(Clone)]
584pub struct OptionExposureHandle(Arc<Snapshot<ExposureState>>);
585
586impl OptionExposureHandle {
587 pub fn new(
591 feed: &dyn Datafeed,
592 query: OptionExposureQuery,
593 wake: impl Fn() + Send + Sync + 'static,
594 ) -> Option<Self> {
595 if query.symbol.is_empty() || query.expiries.is_empty() {
596 return None;
597 }
598 let snapshot = Snapshot::new(
599 ExposureState {
600 query: query.clone(),
601 value: None,
602 error: None,
603 finished: false,
604 },
605 wake,
606 );
607 feed.option_exposure(query, OptionExposureSink(Arc::downgrade(&snapshot)));
608 Some(Self(snapshot))
609 }
610
611 pub fn snapshot(&self) -> Option<Arc<OptionExposure>> {
613 self.0.read(|state| state.value.clone())
614 }
615
616 pub fn snapshot_or_gamma(
621 &self,
622 quotes: &[OptionQuote],
623 ) -> Result<Option<Arc<OptionExposure>>, FeedError> {
624 let (value, query) = self
625 .0
626 .read(|state| (state.value.clone(), state.query.clone()));
627 if value.is_some() || query.kind != OptionExposureKind::Gamma {
628 return Ok(value);
629 }
630 OptionExposure::gamma(
631 quotes
632 .iter()
633 .filter(|quote| query.expiries.contains("e.expiry)),
634 )
635 .map(|value| value.map(Arc::new))
636 }
637
638 pub fn error(&self) -> Option<FeedError> {
640 self.0.read(|state| state.error.clone())
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use crate::data::inbox::Inbox;
648 use crate::data::{HistoryPage, HistoryRequest, Reply};
649 use chrono::TimeZone;
650 use std::sync::Mutex;
651 use std::sync::atomic::{AtomicUsize, Ordering};
652
653 #[derive(Default)]
654 struct Feed {
655 chain: Mutex<Option<(OptionChainQuery, OptionChainSink)>>,
656 exposure: Mutex<Option<(OptionExposureQuery, OptionExposureSink)>>,
657 }
658
659 impl Datafeed for Feed {
660 fn history(&self, _: HistoryRequest, reply: Reply<HistoryPage>) {
661 reply.send(Ok(HistoryPage::default()));
662 }
663
664 fn option_chain(&self, query: OptionChainQuery, sink: OptionChainSink) {
665 *self.chain.lock().unwrap() = Some((query, sink));
666 }
667
668 fn option_exposure(&self, query: OptionExposureQuery, sink: OptionExposureSink) {
669 *self.exposure.lock().unwrap() = Some((query, sink));
670 }
671 }
672
673 fn query() -> OptionChainQuery {
674 let start = NaiveDate::from_ymd_opt(2026, 12, 18).unwrap();
675 let end = NaiveDate::from_ymd_opt(2027, 1, 15).unwrap();
676 OptionChainQuery::new("SAMPLE", start..=end)
677 }
678
679 fn quote(symbol: &str) -> OptionQuote {
680 OptionQuote::new(
681 symbol,
682 *query().expiries.start(),
683 100.0,
684 OptionKind::Call,
685 Utc.with_ymd_and_hms(2026, 11, 2, 12, 0, 0).unwrap(),
686 )
687 }
688
689 #[test]
690 fn gamma_keeps_expiries_separate_counts_missing_inputs_and_preserves_zero_interest() {
691 let e1 = *query().expiries.start();
692 let e2 = *query().expiries.end();
693 let mut quotes: Vec<_> = [
694 (e2, 100.0, OptionKind::Call, 200.0, Some(0.01), 4.0),
695 (e1, 110.0, OptionKind::Call, 100.0, Some(0.01), 0.0),
696 (e1, 100.0, OptionKind::Put, 100.0, Some(0.01), 5.0),
697 (e1, 100.0, OptionKind::Call, 100.0, Some(0.02), 10.0),
698 (e1, 120.0, OptionKind::Call, 100.0, None, 10.0),
699 ]
700 .into_iter()
701 .map(|(expiry, strike, kind, price, gamma, interest)| {
702 let mut row = quote("CONTRACT");
703 row.expiry = expiry;
704 row.strike = strike;
705 row.kind = kind;
706 row.underlying_price = Some(price);
707 row.gamma = gamma;
708 row.open_interest = Some(interest);
709 row.multiplier = Some(100.0);
710 row
711 })
712 .collect();
713 let exposure = OptionExposure::gamma("es).unwrap().unwrap();
714 assert_eq!(exposure.kind, OptionExposureKind::Gamma);
715 assert_eq!(exposure.time, quotes[0].time);
716 assert_eq!(exposure.unit, "price currency per 1% move");
717 assert_eq!(exposure.model, None);
718 assert_eq!(exposure.missing_contracts, 1);
719 let expected = [
720 (e1, 100.0, Some(2000.0), Some(-500.0), 1500.0),
721 (e1, 110.0, Some(0.0), None, 0.0),
722 (e2, 100.0, Some(1600.0), None, 1600.0),
723 ];
724 assert_eq!(exposure.rows.len(), expected.len());
725 for (row, (expiry, strike, calls, puts, net)) in exposure.rows.iter().zip(expected) {
726 assert_eq!((row.expiry, row.strike), (expiry, strike));
727 assert_eq!((row.calls, row.puts, row.net), (calls, puts, Some(net)));
728 }
729
730 type Field = fn(&mut OptionQuote) -> &mut Option<f64>;
731 for select in [
732 (|r| &mut r.gamma) as Field,
733 (|r| &mut r.open_interest) as Field,
734 (|r| &mut r.multiplier) as Field,
735 (|r| &mut r.underlying_price) as Field,
736 ] {
737 let mut row = quotes[0].clone();
738 *select(&mut row) = None;
739 let exposure = OptionExposure::gamma([&row]).unwrap().unwrap();
740 assert!(exposure.rows.is_empty());
741 assert_eq!(exposure.missing_contracts, 1);
742 }
743 assert_eq!(OptionExposure::gamma(&[]).unwrap(), None);
744
745 quotes[0].time += chrono::TimeDelta::seconds(1);
746 quotes.push(quotes[3].clone());
747 quotes.push(quotes[2].clone());
748 let exposure = OptionExposure::gamma("es).unwrap().unwrap();
749 assert_eq!(exposure.time, quotes[0].time);
750 assert_eq!(exposure.rows[0].calls, Some(4000.0));
751 assert_eq!(exposure.rows[0].puts, Some(-1000.0));
752 assert_eq!(exposure.rows[0].net, Some(3000.0));
753
754 for (gamma, interest, multiplier, price, succeeds) in [
755 (f64::NAN, 1.0, 100.0, 100.0, false),
756 (0.01, -1.0, 100.0, 100.0, false),
757 (0.01, 1.0, 0.0, 100.0, false),
758 (0.01, 1.0, 100.0, f64::INFINITY, false),
759 (1.0, 1.0, 100.0, f64::MAX, false),
760 (1.0, 0.0, 100.0, f64::MAX, true),
761 (0.01, 1.0, 100.0, 0.0, true),
762 (f64::MAX, f64::MAX, 100.0, 0.0, true),
763 ] {
764 let mut row = quotes[0].clone();
765 row.gamma = Some(gamma);
766 row.open_interest = Some(interest);
767 row.multiplier = Some(multiplier);
768 row.underlying_price = Some(price);
769 let result = OptionExposure::gamma([&row]);
770 if succeeds {
771 assert_eq!(result.unwrap().unwrap().rows[0].calls, Some(0.0));
772 } else {
773 assert!(matches!(result, Err(FeedError::InvalidData(_))));
774 }
775 }
776 let mut row = quotes[0].clone();
777 row.gamma = Some(1.0e308);
778 row.open_interest = Some(1.0);
779 row.multiplier = Some(1.0);
780 row.underlying_price = Some(1.0);
781 assert!(matches!(
782 OptionExposure::gamma(std::iter::repeat_n(&row, 200)),
783 Err(FeedError::InvalidData(_))
784 ));
785 }
786
787 #[test]
788 fn supplied_exposure_replaces_gamma_and_invalid_snapshots_preserve_the_whole_observation() {
789 for kind in [
790 OptionExposureKind::Gamma,
791 OptionExposureKind::Delta,
792 OptionExposureKind::Vanna,
793 OptionExposureKind::Charm,
794 ] {
795 let feed = Feed::default();
796 let query = OptionExposureQuery::new("SAMPLE", query().expiries, kind);
797 let handle = OptionExposureHandle::new(&feed, query.clone(), || {}).unwrap();
798 let (received, sink) = feed.exposure.lock().unwrap().clone().unwrap();
799 assert_eq!(received, query);
800 assert!(handle.snapshot().is_none());
801 let mut contract = quote("CALL");
802 contract.gamma = Some(0.02);
803 contract.open_interest = Some(10.0);
804 contract.multiplier = Some(100.0);
805 contract.underlying_price = Some(100.0);
806 let fallback = handle
807 .snapshot_or_gamma(std::slice::from_ref(&contract))
808 .unwrap();
809 assert_eq!(fallback.is_some(), kind == OptionExposureKind::Gamma);
810 if let Some(fallback) = fallback {
811 assert_eq!(fallback.rows[0].calls, Some(2000.0));
812 }
813 contract.expiry = query.expiries.start().pred_opt().unwrap();
814 assert!(handle.snapshot_or_gamma(&[contract]).unwrap().is_none());
815
816 let mut row = OptionExposureRow::new(*query.expiries.end(), 110.0);
817 row.puts = Some(17.0);
818 row.net = Some(-42.0);
819 let mut supplied =
820 OptionExposure::new(kind, quote("CALL").time, "host units", vec![row]);
821 supplied.model = Some("host model".into());
822 supplied.missing_contracts = 3;
823 sink.push(supplied.clone()).unwrap();
824 let held = handle.snapshot().unwrap();
825 let mut invalid_quote = quote("INVALID");
827 invalid_quote.strike = f64::NAN;
828 let chosen = handle.snapshot_or_gamma(&[invalid_quote]).unwrap().unwrap();
829 assert!(Arc::ptr_eq(&held, &chosen));
830 assert_eq!(*chosen, supplied);
831
832 type Field = fn(&mut OptionExposureRow) -> &mut Option<f64>;
833 let mut invalid = Vec::new();
834 for select in [
835 (|r| &mut r.calls) as Field,
836 (|r| &mut r.puts) as Field,
837 (|r| &mut r.net) as Field,
838 ] {
839 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
840 let mut bad = supplied.clone();
841 *select(&mut bad.rows[0]) = Some(value);
842 bad.rows
843 .insert(0, OptionExposureRow::new(*query.expiries.start(), 90.0));
844 invalid.push(bad);
845 }
846 }
847 for strike in [f64::NAN, f64::INFINITY, -1.0, 0.0] {
848 let mut bad = supplied.clone();
849 bad.rows[0].strike = strike;
850 invalid.push(bad);
851 }
852 let mut bad = supplied.clone();
853 bad.rows[0].expiry = query.expiries.end().succ_opt().unwrap();
854 invalid.push(bad);
855 let mut bad = supplied.clone();
856 bad.kind = if kind == OptionExposureKind::Gamma {
857 OptionExposureKind::Delta
858 } else {
859 OptionExposureKind::Gamma
860 };
861 invalid.push(bad);
862 let mut bad = supplied.clone();
863 bad.rows = vec![bad.rows[0].clone(); 100_001];
864 invalid.push(bad);
865 for bad in invalid {
866 let error = sink.push(bad).unwrap_err();
867 assert!(matches!(error, FeedError::InvalidData(_)));
868 assert_eq!(handle.error(), Some(error));
869 assert!(Arc::ptr_eq(&handle.snapshot().unwrap(), &held));
870 }
871 sink.report_error(FeedError::Network("offline".into()));
872 assert!(Arc::ptr_eq(&handle.snapshot().unwrap(), &held));
873 supplied.rows.clear();
874 sink.push(supplied.clone()).unwrap();
875 assert_eq!(*handle.snapshot_or_gamma(&[]).unwrap().unwrap(), supplied);
876 assert_eq!(handle.error(), None);
877 }
878 }
879
880 #[test]
881 fn exposure_snapshots_stop_after_historical_answers_or_the_last_consumer() {
882 for historical in [false, true] {
883 for fail in [false, true] {
884 let feed = Feed::default();
885 let mut query =
886 OptionExposureQuery::new("SAMPLE", query().expiries, OptionExposureKind::Gamma);
887 query.as_of = historical.then_some(quote("CALL").time);
888 let wakes = Arc::new(AtomicUsize::new(0));
889 let counter = wakes.clone();
890 let handle = OptionExposureHandle::new(&feed, query, move || {
891 counter.fetch_add(1, Ordering::Relaxed);
892 })
893 .unwrap();
894 let (_, sink) = feed.exposure.lock().unwrap().clone().unwrap();
895 let clone = handle.clone();
896 let supplied = OptionExposure::new(
897 OptionExposureKind::Gamma,
898 quote("CALL").time,
899 "host units",
900 Vec::new(),
901 );
902 if fail {
903 sink.report_error(FeedError::NotSupported);
904 } else {
905 sink.push(supplied.clone()).unwrap();
906 }
907 assert_eq!(sink.is_active(), !historical);
908 assert_eq!(wakes.load(Ordering::Relaxed), 1);
909 sink.push(supplied).unwrap();
910 sink.report_error(FeedError::Network("late".into()));
911 assert_eq!(
912 wakes.load(Ordering::Relaxed),
913 if historical { 1 } else { 3 }
914 );
915 assert_eq!(handle.snapshot(), clone.snapshot());
916 if historical {
917 assert_eq!(handle.snapshot().is_none(), fail);
918 assert_eq!(handle.error(), fail.then_some(FeedError::NotSupported));
919 }
920 drop(handle);
921 assert_eq!(sink.is_active(), !historical);
922 drop(clone);
923 assert!(!sink.is_active());
924 let before = wakes.load(Ordering::Relaxed);
925 sink.report_error(FeedError::NotSupported);
926 assert_eq!(wakes.load(Ordering::Relaxed), before);
927 }
928 }
929 }
930
931 #[test]
932 fn invalid_snapshots_name_the_contract_and_field_and_preserve_all_held_rows() {
933 let feed = Feed::default();
934 let chain = OptionChain::new(&feed, query(), || {}).unwrap();
935 let (_, sink) = feed.chain.lock().unwrap().clone().unwrap();
936 let mut put = quote("PUT");
937 put.kind = OptionKind::Put;
938 put.expiry = *query().expiries.end();
939 sink.push(vec![quote("CALL"), put]).unwrap();
940 let held = chain.rows();
941
942 type Field = fn(&mut OptionQuote) -> &mut Option<f64>;
943 let mut cases = Vec::new();
944 for (field, select, positive, signed) in [
945 ("bid", (|r| &mut r.bid) as Field, false, false),
946 ("ask", (|r| &mut r.ask) as Field, false, false),
947 ("last", (|r| &mut r.last) as Field, false, false),
948 ("volume", (|r| &mut r.volume) as Field, false, false),
949 (
950 "open_interest",
951 (|r| &mut r.open_interest) as Field,
952 false,
953 false,
954 ),
955 (
956 "implied_volatility",
957 (|r| &mut r.implied_volatility) as Field,
958 false,
959 false,
960 ),
961 (
962 "underlying_price",
963 (|r| &mut r.underlying_price) as Field,
964 false,
965 false,
966 ),
967 ("multiplier", (|r| &mut r.multiplier) as Field, true, false),
968 ("delta", (|r| &mut r.delta) as Field, false, true),
969 ("gamma", (|r| &mut r.gamma) as Field, false, true),
970 ("theta", (|r| &mut r.theta) as Field, false, true),
971 ("vega", (|r| &mut r.vega) as Field, false, true),
972 ] {
973 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0, 0.0] {
974 if value.is_finite() && (signed || (value == 0.0 && !positive)) {
975 continue;
976 }
977 let mut row = quote("INVALID");
978 *select(&mut row) = Some(value);
979 cases.push((field, vec![quote("CHANGED"), row]));
980 }
981 }
982 for strike in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0, 0.0] {
983 let mut row = quote("INVALID");
984 row.strike = strike;
985 cases.push(("strike", vec![quote("CHANGED"), row]));
986 }
987 for expiry in [
988 query().expiries.start().pred_opt().unwrap(),
989 query().expiries.end().succ_opt().unwrap(),
990 ] {
991 let mut row = quote("INVALID");
992 row.expiry = expiry;
993 cases.push(("expiry", vec![quote("CHANGED"), row]));
994 }
995 cases.push(("rows", vec![quote("INVALID"); 100_001]));
996
997 for (field, rows) in cases {
998 let error = sink.push(rows).unwrap_err();
999 assert!(
1000 matches!(&error, FeedError::InvalidData(message)
1001 if message.contains("INVALID") && message.contains(field)),
1002 "{field}: {error:?}"
1003 );
1004 assert_eq!(chain.error(), Some(error));
1005 assert!(Arc::ptr_eq(&chain.rows(), &held), "{field}");
1006 }
1007 sink.push((0..100_000).map(|i| quote(&format!("CALL-{i}"))).collect())
1008 .unwrap();
1009 assert_eq!(chain.rows().len(), 100_000);
1010 assert_eq!(chain.error(), None);
1011 }
1012
1013 #[test]
1014 fn snapshots_replace_without_filling_missing_figures_and_end_with_the_last_consumer() {
1015 for historical in [false, true] {
1016 let feed = Feed::default();
1017 let mut query = query();
1018 query.as_of = historical.then_some(quote("CALL").time);
1019 let wakes = Arc::new(AtomicUsize::new(0));
1020 let counter = Arc::clone(&wakes);
1021 let chain = OptionChain::new(&feed, query.clone(), move || {
1022 counter.fetch_add(1, Ordering::Relaxed);
1023 })
1024 .unwrap();
1025 let (received, sink) = feed.chain.lock().unwrap().clone().unwrap();
1026 assert_eq!(received, query);
1027 assert!(chain.rows().is_empty());
1028 assert_eq!(chain.error(), None);
1029 assert!(sink.is_active());
1030 let clone = chain.clone();
1031
1032 sink.report_error(FeedError::Network("offline".into()));
1033 assert_eq!(chain.error(), Some(FeedError::Network("offline".into())));
1034 if historical {
1035 assert!(!sink.is_active());
1036 sink.push(vec![quote("LATE")]).unwrap();
1037 sink.report_error(FeedError::NotSupported);
1038 assert!(chain.rows().is_empty());
1039 assert_eq!(chain.error(), Some(FeedError::Network("offline".into())));
1040 assert_eq!(wakes.load(Ordering::Relaxed), 1);
1041 continue;
1042 }
1043 for interest in [0.0, 1200.0] {
1044 let mut row = quote("CALL");
1045 row.bid = Some(0.0);
1046 row.open_interest = Some(interest);
1047 row.gamma = Some(-0.04);
1048 row.theta = Some(-0.5);
1049 sink.push(vec![row.clone()]).unwrap();
1050 assert_eq!(chain.rows().as_ref(), [row]);
1051 let rows = chain.rows();
1052 assert!(
1053 [
1054 rows[0].multiplier,
1055 rows[0].ask,
1056 rows[0].last,
1057 rows[0].volume,
1058 rows[0].implied_volatility,
1059 rows[0].delta,
1060 rows[0].vega,
1061 rows[0].underlying_price,
1062 ]
1063 .iter()
1064 .all(Option::is_none)
1065 );
1066 assert_eq!(clone.rows(), chain.rows());
1067 assert_eq!(chain.error(), None);
1068 assert!(sink.is_active());
1069 }
1070 sink.push(Vec::new()).unwrap();
1071 assert!(chain.rows().is_empty());
1072 assert_eq!(wakes.load(Ordering::Relaxed), 4);
1073 drop(chain);
1074 assert!(sink.is_active());
1075 drop(clone);
1076 assert!(!sink.is_active());
1077 let before = wakes.load(Ordering::Relaxed);
1078 sink.push(vec![quote("LATE")]).unwrap();
1079 sink.report_error(FeedError::NotSupported);
1080 assert_eq!(wakes.load(Ordering::Relaxed), before);
1081 }
1082
1083 let feed = Feed::default();
1084 let mut query = query();
1085 query.as_of = Some(quote("CALL").time);
1086 let wakes = Arc::new(AtomicUsize::new(0));
1087 let counter = Arc::clone(&wakes);
1088 let chain = OptionChain::new(&feed, query, move || {
1089 counter.fetch_add(1, Ordering::Relaxed);
1090 })
1091 .unwrap();
1092 let (_, sink) = feed.chain.lock().unwrap().clone().unwrap();
1093 sink.push(vec![quote("CALL")]).unwrap();
1094 let held = chain.rows();
1095 assert!(!sink.is_active());
1096 sink.push(Vec::new()).unwrap();
1097 sink.report_error(FeedError::NotSupported);
1098 assert!(Arc::ptr_eq(&chain.rows(), &held));
1099 assert_eq!(chain.error(), None);
1100 assert_eq!(wakes.load(Ordering::Relaxed), 1);
1101 }
1102
1103 #[test]
1104 fn history_only_feeds_report_unsupported_options_and_bad_queries_never_subscribe() {
1105 struct HistoryOnly;
1106 impl Datafeed for HistoryOnly {
1107 fn history(&self, _: HistoryRequest, reply: Reply<HistoryPage>) {
1108 reply.send(Ok(HistoryPage::default()));
1109 }
1110 }
1111
1112 let inbox = Arc::new(Inbox::default());
1113 HistoryOnly.option_expiries("SAMPLE", inbox.reply(0));
1114 let answers = inbox.drain().answers;
1115 assert_eq!(answers.len(), 1);
1116 assert_eq!(
1117 *answers
1118 .into_iter()
1119 .next()
1120 .unwrap()
1121 .1
1122 .downcast::<Result<Vec<NaiveDate>, FeedError>>()
1123 .unwrap(),
1124 Err(FeedError::NotSupported)
1125 );
1126 for historical in [false, true] {
1127 let mut query = query();
1128 query.as_of = historical.then_some(quote("CALL").time);
1129 let chain = OptionChain::new(&HistoryOnly, query.clone(), || {}).unwrap();
1130 assert!(chain.rows().is_empty());
1131 assert_eq!(chain.error(), Some(FeedError::NotSupported));
1132 for kind in [
1133 OptionExposureKind::Gamma,
1134 OptionExposureKind::Delta,
1135 OptionExposureKind::Vanna,
1136 OptionExposureKind::Charm,
1137 ] {
1138 let mut exposure_query =
1139 OptionExposureQuery::new("SAMPLE", query.expiries.clone(), kind);
1140 exposure_query.as_of = query.as_of;
1141 let handle =
1142 OptionExposureHandle::new(&HistoryOnly, exposure_query, || {}).unwrap();
1143 assert!(handle.snapshot().is_none());
1144 assert_eq!(handle.error(), Some(FeedError::NotSupported));
1145 }
1146 }
1147
1148 let feed = Feed::default();
1149 let mut empty_symbol = query();
1150 empty_symbol.symbol.clear();
1151 let mut reversed = query();
1152 reversed.expiries = *reversed.expiries.end()..=*reversed.expiries.start();
1153 for query in [empty_symbol, reversed] {
1154 let exposure_query = OptionExposureQuery::new(
1155 &query.symbol,
1156 query.expiries.clone(),
1157 OptionExposureKind::Gamma,
1158 );
1159 assert!(OptionChain::new(&feed, query, || {}).is_none());
1160 assert!(OptionExposureHandle::new(&feed, exposure_query, || {}).is_none());
1161 }
1162 assert!(feed.chain.lock().unwrap().is_none());
1163 assert!(feed.exposure.lock().unwrap().is_none());
1164 }
1165}