1use 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#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
15pub struct OptionInstrument {
16 pub instrument: Instrument,
18 pub ticker: TickerData,
20}
21
22#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
29pub struct OptionInstrumentPair {
30 pub call: Option<OptionInstrument>,
32 pub put: Option<OptionInstrument>,
34}
35
36#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
38pub struct Spread {
39 bid: Option<f64>,
41 ask: Option<f64>,
43 mid: Option<f64>,
45}
46
47#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
49pub struct BasicGreeks {
50 delta_call: Option<f64>,
52 delta_put: Option<f64>,
54 gamma: Option<f64>,
56}
57
58#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
60pub struct BasicOptionData {
61 pub strike_price: f64,
63 pub call_bid: Option<f64>,
65 pub call_ask: Option<f64>,
67 pub put_bid: Option<f64>,
69 pub put_ask: Option<f64>,
71 pub implied_volatility: (Option<f64>, Option<f64>),
73 pub delta_call: Option<f64>,
75 pub delta_put: Option<f64>,
77 pub gamma: Option<f64>,
79 pub volume: f64,
81 pub open_interest: f64,
83 pub expiration_date: Option<DateTime<Utc>>,
85 pub underlying_price: Option<f64>,
87 pub risk_free_rate: f64,
89 pub extra_fields: Option<Value>,
91}
92
93#[allow(dead_code)]
94impl OptionInstrumentPair {
95 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 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 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 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 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 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 pub fn value(&self) -> Option<Value> {
170 serde_json::to_value(self).ok()
171 }
172
173 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 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 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 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 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 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 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 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 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}