Skip to main content

deribit_base/model/
options.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 9/9/25
5******************************************************************************/
6use crate::prelude::{Instrument, TickerData};
7
8use chrono::{DateTime, TimeZone, Utc};
9use pretty_simple_display::{DebugPretty, DisplaySimple};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13/// Combined option instrument data with ticker information
14#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
15pub struct OptionInstrument {
16    /// The instrument details
17    pub instrument: Instrument,
18    /// Real-time ticker data for the option
19    pub ticker: TickerData,
20}
21
22/// A pair of option instruments representing both call and put options for the same underlying asset
23///
24/// This structure groups together the call and put options for a specific underlying asset,
25/// allowing for easy access to both sides of an option strategy. Both options are optional,
26/// meaning you can have just a call, just a put, or both.
27///
28#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
29pub struct OptionInstrumentPair {
30    /// Call option instrument data, if available
31    pub call: Option<OptionInstrument>,
32    /// Put option instrument data, if available  
33    pub put: Option<OptionInstrument>,
34}
35
36/// Spread information for bid/ask prices
37#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
38pub struct Spread {
39    /// Best bid price
40    bid: Option<f64>,
41    /// Best ask price
42    ask: Option<f64>,
43    /// Mid price (average of bid and ask)
44    mid: Option<f64>,
45}
46
47/// Basic Greeks values for option pricing
48#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
49pub struct BasicGreeks {
50    /// Delta value for call option
51    delta_call: Option<f64>,
52    /// Delta value for put option
53    delta_put: Option<f64>,
54    /// Gamma value (rate of change of delta)
55    gamma: Option<f64>,
56}
57
58/// Comprehensive option data structure containing all relevant pricing and risk information
59#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
60pub struct BasicOptionData {
61    /// Strike price of the option
62    pub strike_price: f64,
63    /// Best bid price for call option
64    pub call_bid: Option<f64>,
65    /// Best ask price for call option
66    pub call_ask: Option<f64>,
67    /// Best bid price for put option
68    pub put_bid: Option<f64>,
69    /// Best ask price for put option
70    pub put_ask: Option<f64>,
71    /// Implied volatility for call and put options (call_iv, put_iv)
72    pub implied_volatility: (Option<f64>, Option<f64>),
73    /// Delta value for call option
74    pub delta_call: Option<f64>,
75    /// Delta value for put option
76    pub delta_put: Option<f64>,
77    /// Gamma value (rate of change of delta)
78    pub gamma: Option<f64>,
79    /// Total trading volume
80    pub volume: f64,
81    /// Total open interest
82    pub open_interest: f64,
83    /// Option expiration date
84    pub expiration_date: Option<DateTime<Utc>>,
85    /// Current price of the underlying asset
86    pub underlying_price: Option<f64>,
87    /// Risk-free interest rate
88    pub risk_free_rate: f64,
89    /// Additional fields as JSON value
90    pub extra_fields: Option<Value>,
91}
92
93#[allow(dead_code)]
94impl OptionInstrumentPair {
95    /// Returns the expiration date/time of the option instrument.
96    ///
97    /// Extracts the expiration timestamp from the call or put instrument
98    /// and converts it to a UTC datetime.
99    pub fn expiration(&self) -> Option<DateTime<Utc>> {
100        let expiration_timestamp = match self.instrument() {
101            Some(i) => i.expiration_timestamp,
102            None => return None,
103        };
104
105        if let Some(expiration_timestamp) = expiration_timestamp {
106            Utc.timestamp_millis_opt(expiration_timestamp).single()
107        } else {
108            None
109        }
110    }
111
112    /// Returns the first available instrument from call or put option.
113    ///
114    /// Prefers the call instrument if available, otherwise returns the put.
115    pub fn instrument(&self) -> Option<Instrument> {
116        self.call
117            .as_ref()
118            .map(|i| i.instrument.clone())
119            .or_else(|| self.put.as_ref().map(|i| i.instrument.clone()))
120    }
121
122    /// Returns the first available ticker data from call or put option.
123    ///
124    /// Prefers the call ticker if available, otherwise returns the put.
125    pub fn ticker(&self) -> Option<TickerData> {
126        self.call
127            .as_ref()
128            .map(|i| i.ticker.clone())
129            .or_else(|| self.put.as_ref().map(|i| i.ticker.clone()))
130    }
131
132    /// Calculates total trading volume across both call and put options.
133    pub fn volume(&self) -> f64 {
134        let mut volume: f64 = 0.0;
135        if let Some(call) = &self.call {
136            volume += call.ticker.stats.volume
137        }
138        if let Some(put) = &self.put {
139            volume += put.ticker.stats.volume
140        }
141        volume
142    }
143
144    /// Calculates total open interest across both call and put options.
145    pub fn open_interest(&self) -> f64 {
146        let mut open_interest: f64 = 0.0;
147        if let Some(call) = &self.call {
148            open_interest += call.ticker.open_interest.unwrap_or(0.0)
149        }
150        if let Some(put) = &self.put {
151            open_interest += put.ticker.open_interest.unwrap_or(0.0)
152        }
153        open_interest
154    }
155
156    /// Calculates total interest rate across both call and put options.
157    pub fn interest_rate(&self) -> f64 {
158        let mut interest_rate: f64 = 0.0;
159        if let Some(call) = &self.call {
160            interest_rate += call.ticker.interest_rate.unwrap_or(0.0)
161        }
162        if let Some(put) = &self.put {
163            interest_rate += put.ticker.interest_rate.unwrap_or(0.0)
164        }
165        interest_rate
166    }
167
168    /// Serializes the option pair to a JSON value.
169    pub fn value(&self) -> Option<Value> {
170        serde_json::to_value(self).ok()
171    }
172
173    /// Calculates bid-ask spread for the call option.
174    ///
175    /// Returns bid, ask, and mid prices. Mid is the average of bid and ask.
176    pub fn call_spread(&self) -> Spread {
177        if let Some(call) = &self.call {
178            let bid = call.ticker.best_bid_price;
179            let ask = call.ticker.best_ask_price;
180            let mid = match (bid, ask) {
181                (Some(b), Some(a)) => Some((b + a) / 2.0),
182                (Some(b), None) => Some(b),
183                (None, Some(a)) => Some(a),
184                (None, None) => None,
185            };
186            Spread { bid, ask, mid }
187        } else {
188            Spread {
189                bid: None,
190                ask: None,
191                mid: None,
192            }
193        }
194    }
195
196    /// Calculates bid-ask spread for the put option.
197    ///
198    /// Returns bid, ask, and mid prices. Mid is the average of bid and ask.
199    pub fn put_spread(&self) -> Spread {
200        if let Some(put) = &self.put {
201            let bid = put.ticker.best_bid_price;
202            let ask = put.ticker.best_ask_price;
203            let mid = match (bid, ask) {
204                (Some(b), Some(a)) => Some((b + a) / 2.0),
205                (Some(b), None) => Some(b),
206                (None, Some(a)) => Some(a),
207                (None, None) => None,
208            };
209            Spread { bid, ask, mid }
210        } else {
211            Spread {
212                bid: None,
213                ask: None,
214                mid: None,
215            }
216        }
217    }
218
219    /// Returns implied volatility for both call and put options.
220    ///
221    /// Returns a tuple of (call_iv, put_iv).
222    pub fn iv(&self) -> (Option<f64>, Option<f64>) {
223        let call_iv = self.call.as_ref().and_then(|c| c.ticker.mark_iv);
224        let put_iv = self.put.as_ref().and_then(|p| p.ticker.mark_iv);
225        (call_iv, put_iv)
226    }
227
228    /// Calculates basic Greeks (delta, gamma) for both call and put options.
229    pub fn greeks(&self) -> BasicGreeks {
230        let delta_call = self
231            .call
232            .as_ref()
233            .and_then(|c| c.ticker.greeks.as_ref().and_then(|g| g.delta));
234        let delta_put = self
235            .put
236            .as_ref()
237            .and_then(|p| p.ticker.greeks.as_ref().and_then(|g| g.delta));
238        let gamma = self
239            .call
240            .as_ref()
241            .and_then(|c| c.ticker.greeks.as_ref().and_then(|g| g.gamma))
242            .or_else(|| {
243                self.put
244                    .as_ref()
245                    .and_then(|p| p.ticker.greeks.as_ref().and_then(|g| g.gamma))
246            });
247        BasicGreeks {
248            delta_call,
249            delta_put,
250            gamma,
251        }
252    }
253
254    /// Extracts and consolidates all option data into a structured format.
255    ///
256    /// Includes strike price, spreads, implied volatility, and Greeks.
257    pub fn data(&self) -> BasicOptionData {
258        let strike_price: f64 = match self.instrument() {
259            Some(i) => i.strike.unwrap_or(0.0),
260            None => 0.0,
261        };
262        let call_spread = self.call_spread();
263        let call_bid: Option<f64> = call_spread.bid;
264        let call_ask: Option<f64> = call_spread.ask;
265        let put_spread = self.put_spread();
266        let put_bid: Option<f64> = put_spread.bid;
267        let put_ask: Option<f64> = put_spread.ask;
268        let implied_volatility = self.iv();
269        let greeks = self.greeks();
270        let delta_call: Option<f64> = greeks.delta_call;
271        let delta_put: Option<f64> = greeks.delta_put;
272        let gamma: Option<f64> = greeks.gamma;
273        let volume = self.volume();
274        let open_interest: f64 = self.open_interest();
275        let expiration_date: Option<DateTime<Utc>> = self.expiration();
276        let underlying_price: Option<f64> = self.ticker().and_then(|t| t.underlying_price);
277        let risk_free_rate: f64 = self.interest_rate();
278        let extra_fields: Option<Value> = self.value();
279        BasicOptionData {
280            strike_price,
281            call_bid,
282            call_ask,
283            put_bid,
284            put_ask,
285            implied_volatility,
286            delta_call,
287            delta_put,
288            gamma,
289            volume,
290            open_interest,
291            expiration_date,
292            underlying_price,
293            risk_free_rate,
294            extra_fields,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::model::ticker::{Greeks, TickerStats};
303    use serde_json;
304
305    fn create_test_instrument(name: &str, strike: f64, option_type: &str) -> Instrument {
306        use crate::model::instrument::{InstrumentKind, InstrumentType, OptionType};
307
308        Instrument {
309            instrument_name: name.to_string(),
310            strike: Some(strike),
311            option_type: Some(match option_type {
312                "call" => OptionType::Call,
313                "put" => OptionType::Put,
314                _ => OptionType::Call,
315            }),
316            expiration_timestamp: Some(1757491200000),
317            kind: Some(InstrumentKind::Option),
318            instrument_type: Some(InstrumentType::Reversed),
319            currency: Some("BTC".to_string()),
320            is_active: Some(true),
321            contract_size: Some(1.0),
322            tick_size: Some(0.0001),
323            min_trade_amount: Some(0.1),
324            settlement_currency: Some("BTC".to_string()),
325            base_currency: Some("BTC".to_string()),
326            counter_currency: Some("USD".to_string()),
327            quote_currency: Some("BTC".to_string()),
328            price_index: None,
329            maker_commission: None,
330            taker_commission: None,
331            instrument_id: None,
332            creation_timestamp: None,
333            settlement_period: None,
334            max_leverage: None,
335        }
336    }
337
338    #[allow(clippy::too_many_arguments)]
339    fn create_test_ticker(
340        instrument_name: &str,
341        last_price: f64,
342        mark_price: f64,
343        bid_price: Option<f64>,
344        ask_price: Option<f64>,
345        bid_amount: f64,
346        ask_amount: f64,
347        volume: f64,
348        open_interest: f64,
349        delta: Option<f64>,
350        gamma: Option<f64>,
351        mark_iv: Option<f64>,
352    ) -> TickerData {
353        TickerData {
354            instrument_name: instrument_name.to_string(),
355            last_price: Some(last_price),
356            mark_price,
357            best_bid_price: bid_price,
358            best_ask_price: ask_price,
359            best_bid_amount: bid_amount,
360            best_ask_amount: ask_amount,
361            timestamp: 1757476246684,
362            state: "open".to_string(),
363            stats: TickerStats {
364                volume,
365                volume_usd: Some(volume * 1000.0),
366                high: Some(0.1),
367                low: Some(0.01),
368                price_change: Some(5.0),
369            },
370            greeks: Some(Greeks {
371                delta,
372                gamma,
373                vega: Some(0.02544),
374                theta: Some(-0.84746),
375                rho: Some(0.50202),
376            }),
377            open_interest: Some(open_interest),
378            mark_iv,
379            underlying_price: Some(111421.0915),
380            interest_rate: Some(0.0),
381            volume: None,
382            volume_usd: None,
383            high: None,
384            low: None,
385            price_change: None,
386            price_change_percentage: None,
387            bid_iv: None,
388            ask_iv: None,
389            settlement_price: None,
390            index_price: None,
391            min_price: None,
392            max_price: None,
393            underlying_index: None,
394            estimated_delivery_price: None,
395        }
396    }
397
398    #[test]
399    fn test_option_instrument_creation() {
400        let instrument = create_test_instrument("BTC-10SEP25-106000-C", 106000.0, "call");
401        let ticker = create_test_ticker(
402            "BTC-10SEP25-106000-C",
403            0.047,
404            0.0487,
405            Some(0.0001),
406            Some(0.05),
407            10.0,
408            30.8,
409            104.2,
410            49.6,
411            Some(0.99972),
412            Some(0.0),
413            Some(66.62),
414        );
415
416        let option_instrument = OptionInstrument { instrument, ticker };
417
418        assert_eq!(
419            option_instrument.instrument.instrument_name,
420            "BTC-10SEP25-106000-C"
421        );
422        assert_eq!(option_instrument.instrument.strike, Some(106000.0));
423        assert_eq!(option_instrument.ticker.last_price, Some(0.047));
424        assert_eq!(option_instrument.ticker.mark_price, 0.0487);
425    }
426
427    #[test]
428    fn test_option_instrument_pair_serialization() {
429        let json_str = r#"{
430            "call": {
431                "instrument": {
432                    "instrument_name": "BTC-10SEP25-106000-C",
433                    "strike": 106000.0,
434                    "option_type": "call",
435                    "expiration_timestamp": 1757491200000
436                },
437                "ticker": {
438                    "instrument_name": "BTC-10SEP25-106000-C",
439                    "timestamp": 1757476246684,
440                    "state": "open",
441                    "last_price": 0.047,
442                    "mark_price": 0.0487,
443                    "best_bid_price": 0.0001,
444                    "best_ask_price": 0.05,
445                    "best_bid_amount": 10.0,
446                    "best_ask_amount": 30.8,
447                    "stats": {
448                        "volume": 104.2,
449                        "volume_usd": 666962.69,
450                        "high": 0.0635,
451                        "low": 0.0245,
452                        "price_change": -21.0084
453                    }
454                }
455            },
456            "put": null
457        }"#;
458
459        let pair: OptionInstrumentPair =
460            serde_json::from_str(json_str).expect("Failed to deserialize OptionInstrumentPair");
461
462        assert!(pair.call.is_some());
463        assert!(pair.put.is_none());
464
465        let call = pair.call.as_ref().unwrap();
466        assert_eq!(call.instrument.instrument_name, "BTC-10SEP25-106000-C");
467        assert_eq!(call.instrument.strike, Some(106000.0));
468    }
469
470    #[test]
471    fn test_spread_creation() {
472        let spread = Spread {
473            bid: Some(0.045),
474            ask: Some(0.055),
475            mid: Some(0.05),
476        };
477
478        assert_eq!(spread.bid, Some(0.045));
479        assert_eq!(spread.ask, Some(0.055));
480        assert_eq!(spread.mid, Some(0.05));
481    }
482
483    #[test]
484    fn test_basic_greeks_creation() {
485        let greeks = BasicGreeks {
486            delta_call: Some(0.99972),
487            delta_put: Some(-0.00077),
488            gamma: Some(0.0),
489        };
490
491        assert_eq!(greeks.delta_call, Some(0.99972));
492        assert_eq!(greeks.delta_put, Some(-0.00077));
493        assert_eq!(greeks.gamma, Some(0.0));
494    }
495
496    #[test]
497    fn test_basic_option_data_creation() {
498        let expiration = Utc.timestamp_millis_opt(1757491200000).single();
499        let option_data = BasicOptionData {
500            strike_price: 106000.0,
501            call_bid: Some(0.0001),
502            call_ask: Some(0.05),
503            put_bid: Some(0.0),
504            put_ask: Some(0.019),
505            implied_volatility: (Some(66.62), Some(107.51)),
506            delta_call: Some(0.99972),
507            delta_put: Some(-0.00077),
508            gamma: Some(0.0),
509            volume: 196.1,
510            open_interest: 75.2,
511            expiration_date: expiration,
512            underlying_price: Some(111421.0915),
513            risk_free_rate: 0.0,
514            extra_fields: None,
515        };
516
517        assert_eq!(option_data.strike_price, 106000.0);
518        assert_eq!(option_data.call_bid, Some(0.0001));
519        assert_eq!(option_data.volume, 196.1);
520        assert_eq!(option_data.open_interest, 75.2);
521    }
522
523    fn create_test_option_pair() -> OptionInstrumentPair {
524        let call_instrument = create_test_instrument("BTC-10SEP25-106000-C", 106000.0, "call");
525        let call_ticker = create_test_ticker(
526            "BTC-10SEP25-106000-C",
527            0.047,
528            0.0487,
529            Some(0.0001),
530            Some(0.05),
531            10.0,
532            30.8,
533            104.2,
534            49.6,
535            Some(0.99972),
536            Some(0.0),
537            Some(66.62),
538        );
539
540        let put_instrument = create_test_instrument("BTC-10SEP25-106000-P", 106000.0, "put");
541        let put_ticker = create_test_ticker(
542            "BTC-10SEP25-106000-P",
543            0.0002,
544            0.0,
545            Some(0.0),
546            Some(0.019),
547            0.0,
548            10.0,
549            91.9,
550            25.6,
551            Some(-0.00077),
552            Some(0.0),
553            Some(107.51),
554        );
555
556        OptionInstrumentPair {
557            call: Some(OptionInstrument {
558                instrument: call_instrument,
559                ticker: call_ticker,
560            }),
561            put: Some(OptionInstrument {
562                instrument: put_instrument,
563                ticker: put_ticker,
564            }),
565        }
566    }
567
568    #[test]
569    fn test_option_pair_expiration() {
570        let pair = create_test_option_pair();
571        let expiration = pair.expiration();
572
573        assert!(expiration.is_some());
574        let exp_date = expiration.unwrap();
575        assert_eq!(exp_date.timestamp_millis(), 1757491200000);
576    }
577
578    #[test]
579    fn test_option_pair_instrument() {
580        let pair = create_test_option_pair();
581        let instrument = pair.instrument();
582
583        assert!(instrument.is_some());
584        let inst = instrument.unwrap();
585        assert_eq!(inst.instrument_name, "BTC-10SEP25-106000-C");
586        assert_eq!(inst.strike, Some(106000.0));
587    }
588
589    #[test]
590    fn test_option_pair_ticker() {
591        let pair = create_test_option_pair();
592        let ticker = pair.ticker();
593
594        assert!(ticker.is_some());
595        let tick = ticker.unwrap();
596        assert_eq!(tick.instrument_name, "BTC-10SEP25-106000-C");
597        assert_eq!(tick.last_price, Some(0.047));
598    }
599
600    #[test]
601    fn test_option_pair_volume() {
602        let pair = create_test_option_pair();
603        let volume = pair.volume();
604
605        // Should be sum of call (104.2) + put (91.9) = 196.1
606        assert!((volume - 196.1).abs() < 1e-10);
607    }
608
609    #[test]
610    fn test_option_pair_open_interest() {
611        let pair = create_test_option_pair();
612        let open_interest = pair.open_interest();
613
614        // Should be sum of call (49.6) + put (25.6) = 75.2
615        assert_eq!(open_interest, 75.2);
616    }
617
618    #[test]
619    fn test_option_pair_interest_rate() {
620        let pair = create_test_option_pair();
621        let interest_rate = pair.interest_rate();
622
623        // Both call and put have 0.0 interest rate
624        assert_eq!(interest_rate, 0.0);
625    }
626
627    #[test]
628    fn test_option_pair_value() {
629        let pair = create_test_option_pair();
630        let value = pair.value();
631
632        assert!(value.is_some());
633        // Should be able to serialize to JSON
634        let json_value = value.unwrap();
635        assert!(json_value.is_object());
636    }
637
638    #[test]
639    fn test_option_pair_call_spread() {
640        let pair = create_test_option_pair();
641        let call_spread = pair.call_spread();
642
643        assert_eq!(call_spread.bid, Some(0.0001));
644        assert_eq!(call_spread.ask, Some(0.05));
645        assert_eq!(call_spread.mid, Some((0.0001 + 0.05) / 2.0));
646    }
647
648    #[test]
649    fn test_option_pair_put_spread() {
650        let pair = create_test_option_pair();
651        let put_spread = pair.put_spread();
652
653        assert_eq!(put_spread.bid, Some(0.0));
654        assert_eq!(put_spread.ask, Some(0.019));
655        assert_eq!(put_spread.mid, Some((0.0 + 0.019) / 2.0));
656    }
657
658    #[test]
659    fn test_option_pair_iv() {
660        let pair = create_test_option_pair();
661        let (call_iv, put_iv) = pair.iv();
662
663        assert_eq!(call_iv, Some(66.62));
664        assert_eq!(put_iv, Some(107.51));
665    }
666
667    #[test]
668    fn test_option_pair_greeks() {
669        let pair = create_test_option_pair();
670        let greeks = pair.greeks();
671
672        assert_eq!(greeks.delta_call, Some(0.99972));
673        assert_eq!(greeks.delta_put, Some(-0.00077));
674        assert_eq!(greeks.gamma, Some(0.0));
675    }
676
677    #[test]
678    fn test_option_pair_data() {
679        let pair = create_test_option_pair();
680        let data = pair.data();
681
682        assert_eq!(data.strike_price, 106000.0);
683        assert_eq!(data.call_bid, Some(0.0001));
684        assert_eq!(data.call_ask, Some(0.05));
685        assert_eq!(data.put_bid, Some(0.0));
686        assert_eq!(data.put_ask, Some(0.019));
687        assert_eq!(data.implied_volatility, (Some(66.62), Some(107.51)));
688        assert_eq!(data.delta_call, Some(0.99972));
689        assert_eq!(data.delta_put, Some(-0.00077));
690        assert_eq!(data.gamma, Some(0.0));
691        assert!((data.volume - 196.1).abs() < 1e-10);
692        assert_eq!(data.open_interest, 75.2);
693        assert_eq!(data.underlying_price, Some(111421.0915));
694        assert_eq!(data.risk_free_rate, 0.0);
695    }
696
697    #[test]
698    fn test_option_pair_with_only_call() {
699        let call_instrument = create_test_instrument("BTC-10SEP25-106000-C", 106000.0, "call");
700        let call_ticker = create_test_ticker(
701            "BTC-10SEP25-106000-C",
702            0.047,
703            0.0487,
704            Some(0.0001),
705            Some(0.05),
706            10.0,
707            30.8,
708            104.2,
709            49.6,
710            Some(0.99972),
711            Some(0.0),
712            Some(66.62),
713        );
714
715        let pair = OptionInstrumentPair {
716            call: Some(OptionInstrument {
717                instrument: call_instrument,
718                ticker: call_ticker,
719            }),
720            put: None,
721        };
722
723        assert_eq!(pair.volume(), 104.2);
724        assert_eq!(pair.open_interest(), 49.6);
725
726        let put_spread = pair.put_spread();
727        assert_eq!(put_spread.bid, None);
728        assert_eq!(put_spread.ask, None);
729        assert_eq!(put_spread.mid, None);
730
731        let (call_iv, put_iv) = pair.iv();
732        assert_eq!(call_iv, Some(66.62));
733        assert_eq!(put_iv, None);
734    }
735
736    #[test]
737    fn test_option_pair_with_only_put() {
738        let put_instrument = create_test_instrument("BTC-10SEP25-106000-P", 106000.0, "put");
739        let put_ticker = create_test_ticker(
740            "BTC-10SEP25-106000-P",
741            0.0002,
742            0.0,
743            Some(0.0),
744            Some(0.019),
745            0.0,
746            10.0,
747            91.9,
748            25.6,
749            Some(-0.00077),
750            Some(0.0),
751            Some(107.51),
752        );
753
754        let pair = OptionInstrumentPair {
755            call: None,
756            put: Some(OptionInstrument {
757                instrument: put_instrument,
758                ticker: put_ticker,
759            }),
760        };
761
762        assert_eq!(pair.volume(), 91.9);
763        assert_eq!(pair.open_interest(), 25.6);
764
765        let call_spread = pair.call_spread();
766        assert_eq!(call_spread.bid, None);
767        assert_eq!(call_spread.ask, None);
768        assert_eq!(call_spread.mid, None);
769
770        let (call_iv, put_iv) = pair.iv();
771        assert_eq!(call_iv, None);
772        assert_eq!(put_iv, Some(107.51));
773    }
774
775    #[test]
776    fn test_empty_option_pair() {
777        let pair = OptionInstrumentPair {
778            call: None,
779            put: None,
780        };
781
782        assert_eq!(pair.volume(), 0.0);
783        assert_eq!(pair.open_interest(), 0.0);
784        assert_eq!(pair.interest_rate(), 0.0);
785        assert!(pair.instrument().is_none());
786        assert!(pair.ticker().is_none());
787        assert!(pair.expiration().is_none());
788
789        let (call_iv, put_iv) = pair.iv();
790        assert_eq!(call_iv, None);
791        assert_eq!(put_iv, None);
792
793        let greeks = pair.greeks();
794        assert_eq!(greeks.delta_call, None);
795        assert_eq!(greeks.delta_put, None);
796        assert_eq!(greeks.gamma, None);
797    }
798}