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>> {
96 let expiration_timestamp = match self.instrument() {
97 Some(i) => i.expiration_timestamp,
98 None => return None,
99 };
100
101 if let Some(expiration_timestamp) = expiration_timestamp {
102 Utc.timestamp_millis_opt(expiration_timestamp).single()
103 } else {
104 None
105 }
106 }
107 pub fn instrument(&self) -> Option<Instrument> {
108 self.call
109 .as_ref()
110 .map(|i| i.instrument.clone())
111 .or_else(|| self.put.as_ref().map(|i| i.instrument.clone()))
112 }
113 pub fn ticker(&self) -> Option<TickerData> {
114 self.call
115 .as_ref()
116 .map(|i| i.ticker.clone())
117 .or_else(|| self.put.as_ref().map(|i| i.ticker.clone()))
118 }
119 pub fn volume(&self) -> f64 {
120 let mut volume: f64 = 0.0;
121 if let Some(call) = &self.call {
122 volume += call.ticker.stats.volume
123 }
124 if let Some(put) = &self.put {
125 volume += put.ticker.stats.volume
126 }
127 volume
128 }
129 pub fn open_interest(&self) -> f64 {
130 let mut open_interest: f64 = 0.0;
131 if let Some(call) = &self.call {
132 open_interest += call.ticker.open_interest.unwrap_or(0.0)
133 }
134 if let Some(put) = &self.put {
135 open_interest += put.ticker.open_interest.unwrap_or(0.0)
136 }
137 open_interest
138 }
139
140 pub fn interest_rate(&self) -> f64 {
141 let mut interest_rate: f64 = 0.0;
142 if let Some(call) = &self.call {
143 interest_rate += call.ticker.interest_rate.unwrap_or(0.0)
144 }
145 if let Some(put) = &self.put {
146 interest_rate += put.ticker.interest_rate.unwrap_or(0.0)
147 }
148 interest_rate
149 }
150
151 pub fn value(&self) -> Option<Value> {
152 serde_json::to_value(self).ok()
153 }
154
155 pub fn call_spread(&self) -> Spread {
156 if let Some(call) = &self.call {
157 let bid = call.ticker.best_bid_price;
158 let ask = call.ticker.best_ask_price;
159 let mid = match (bid, ask) {
160 (Some(b), Some(a)) => Some((b + a) / 2.0),
161 (Some(b), None) => Some(b),
162 (None, Some(a)) => Some(a),
163 (None, None) => None,
164 };
165 Spread { bid, ask, mid }
166 } else {
167 Spread {
168 bid: None,
169 ask: None,
170 mid: None,
171 }
172 }
173 }
174
175 pub fn put_spread(&self) -> Spread {
176 if let Some(put) = &self.put {
177 let bid = put.ticker.best_bid_price;
178 let ask = put.ticker.best_ask_price;
179 let mid = match (bid, ask) {
180 (Some(b), Some(a)) => Some((b + a) / 2.0),
181 (Some(b), None) => Some(b),
182 (None, Some(a)) => Some(a),
183 (None, None) => None,
184 };
185 Spread { bid, ask, mid }
186 } else {
187 Spread {
188 bid: None,
189 ask: None,
190 mid: None,
191 }
192 }
193 }
194
195 pub fn iv(&self) -> (Option<f64>, Option<f64>) {
196 let call_iv = self.call.as_ref().and_then(|c| c.ticker.mark_iv);
197 let put_iv = self.put.as_ref().and_then(|p| p.ticker.mark_iv);
198 (call_iv, put_iv)
199 }
200
201 pub fn greeks(&self) -> BasicGreeks {
202 let delta_call = self
203 .call
204 .as_ref()
205 .and_then(|c| c.ticker.greeks.as_ref().and_then(|g| g.delta));
206 let delta_put = self
207 .put
208 .as_ref()
209 .and_then(|p| p.ticker.greeks.as_ref().and_then(|g| g.delta));
210 let gamma = self
211 .call
212 .as_ref()
213 .and_then(|c| c.ticker.greeks.as_ref().and_then(|g| g.gamma))
214 .or_else(|| {
215 self.put
216 .as_ref()
217 .and_then(|p| p.ticker.greeks.as_ref().and_then(|g| g.gamma))
218 });
219 BasicGreeks {
220 delta_call,
221 delta_put,
222 gamma,
223 }
224 }
225
226 pub fn data(&self) -> BasicOptionData {
227 let strike_price: f64 = match self.instrument() {
228 Some(i) => i.strike.unwrap_or(0.0),
229 None => 0.0,
230 };
231 let call_spread = self.call_spread();
232 let call_bid: Option<f64> = call_spread.bid;
233 let call_ask: Option<f64> = call_spread.ask;
234 let put_spread = self.put_spread();
235 let put_bid: Option<f64> = put_spread.bid;
236 let put_ask: Option<f64> = put_spread.ask;
237 let implied_volatility = self.iv();
238 let greeks = self.greeks();
239 let delta_call: Option<f64> = greeks.delta_call;
240 let delta_put: Option<f64> = greeks.delta_put;
241 let gamma: Option<f64> = greeks.gamma;
242 let volume = self.volume();
243 let open_interest: f64 = self.open_interest();
244 let expiration_date: Option<DateTime<Utc>> = self.expiration();
245 let underlying_price: Option<f64> = self.ticker().and_then(|t| t.underlying_price);
246 let risk_free_rate: f64 = self.interest_rate();
247 let extra_fields: Option<Value> = self.value();
248 BasicOptionData {
249 strike_price,
250 call_bid,
251 call_ask,
252 put_bid,
253 put_ask,
254 implied_volatility,
255 delta_call,
256 delta_put,
257 gamma,
258 volume,
259 open_interest,
260 expiration_date,
261 underlying_price,
262 risk_free_rate,
263 extra_fields,
264 }
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::model::ticker::{Greeks, TickerStats};
272 use serde_json;
273
274 fn create_test_instrument(name: &str, strike: f64, option_type: &str) -> Instrument {
275 use crate::model::instrument::{InstrumentKind, InstrumentType, OptionType};
276
277 Instrument {
278 instrument_name: name.to_string(),
279 strike: Some(strike),
280 option_type: Some(match option_type {
281 "call" => OptionType::Call,
282 "put" => OptionType::Put,
283 _ => OptionType::Call,
284 }),
285 expiration_timestamp: Some(1757491200000),
286 kind: Some(InstrumentKind::Option),
287 instrument_type: Some(InstrumentType::Reversed),
288 currency: Some("BTC".to_string()),
289 is_active: Some(true),
290 contract_size: Some(1.0),
291 tick_size: Some(0.0001),
292 min_trade_amount: Some(0.1),
293 settlement_currency: Some("BTC".to_string()),
294 base_currency: Some("BTC".to_string()),
295 counter_currency: Some("USD".to_string()),
296 quote_currency: Some("BTC".to_string()),
297 price_index: None,
298 maker_commission: None,
299 taker_commission: None,
300 instrument_id: None,
301 creation_timestamp: None,
302 settlement_period: None,
303 max_leverage: None,
304 }
305 }
306
307 #[allow(clippy::too_many_arguments)]
308 fn create_test_ticker(
309 instrument_name: &str,
310 last_price: f64,
311 mark_price: f64,
312 bid_price: Option<f64>,
313 ask_price: Option<f64>,
314 bid_amount: f64,
315 ask_amount: f64,
316 volume: f64,
317 open_interest: f64,
318 delta: Option<f64>,
319 gamma: Option<f64>,
320 mark_iv: Option<f64>,
321 ) -> TickerData {
322 TickerData {
323 instrument_name: instrument_name.to_string(),
324 last_price: Some(last_price),
325 mark_price,
326 best_bid_price: bid_price,
327 best_ask_price: ask_price,
328 best_bid_amount: bid_amount,
329 best_ask_amount: ask_amount,
330 timestamp: 1757476246684,
331 state: "open".to_string(),
332 stats: TickerStats {
333 volume,
334 volume_usd: Some(volume * 1000.0),
335 high: Some(0.1),
336 low: Some(0.01),
337 price_change: Some(5.0),
338 },
339 greeks: Some(Greeks {
340 delta,
341 gamma,
342 vega: Some(0.02544),
343 theta: Some(-0.84746),
344 rho: Some(0.50202),
345 }),
346 open_interest: Some(open_interest),
347 mark_iv,
348 underlying_price: Some(111421.0915),
349 interest_rate: Some(0.0),
350 volume: None,
351 volume_usd: None,
352 high: None,
353 low: None,
354 price_change: None,
355 price_change_percentage: None,
356 bid_iv: None,
357 ask_iv: None,
358 settlement_price: None,
359 index_price: None,
360 min_price: None,
361 max_price: None,
362 underlying_index: None,
363 estimated_delivery_price: None,
364 }
365 }
366
367 #[test]
368 fn test_option_instrument_creation() {
369 let instrument = create_test_instrument("BTC-10SEP25-106000-C", 106000.0, "call");
370 let ticker = create_test_ticker(
371 "BTC-10SEP25-106000-C",
372 0.047,
373 0.0487,
374 Some(0.0001),
375 Some(0.05),
376 10.0,
377 30.8,
378 104.2,
379 49.6,
380 Some(0.99972),
381 Some(0.0),
382 Some(66.62),
383 );
384
385 let option_instrument = OptionInstrument { instrument, ticker };
386
387 assert_eq!(
388 option_instrument.instrument.instrument_name,
389 "BTC-10SEP25-106000-C"
390 );
391 assert_eq!(option_instrument.instrument.strike, Some(106000.0));
392 assert_eq!(option_instrument.ticker.last_price, Some(0.047));
393 assert_eq!(option_instrument.ticker.mark_price, 0.0487);
394 }
395
396 #[test]
397 fn test_option_instrument_pair_serialization() {
398 let json_str = r#"{
399 "call": {
400 "instrument": {
401 "instrument_name": "BTC-10SEP25-106000-C",
402 "strike": 106000.0,
403 "option_type": "call",
404 "expiration_timestamp": 1757491200000
405 },
406 "ticker": {
407 "instrument_name": "BTC-10SEP25-106000-C",
408 "timestamp": 1757476246684,
409 "state": "open",
410 "last_price": 0.047,
411 "mark_price": 0.0487,
412 "best_bid_price": 0.0001,
413 "best_ask_price": 0.05,
414 "best_bid_amount": 10.0,
415 "best_ask_amount": 30.8,
416 "stats": {
417 "volume": 104.2,
418 "volume_usd": 666962.69,
419 "high": 0.0635,
420 "low": 0.0245,
421 "price_change": -21.0084
422 }
423 }
424 },
425 "put": null
426 }"#;
427
428 let pair: OptionInstrumentPair =
429 serde_json::from_str(json_str).expect("Failed to deserialize OptionInstrumentPair");
430
431 assert!(pair.call.is_some());
432 assert!(pair.put.is_none());
433
434 let call = pair.call.as_ref().unwrap();
435 assert_eq!(call.instrument.instrument_name, "BTC-10SEP25-106000-C");
436 assert_eq!(call.instrument.strike, Some(106000.0));
437 }
438
439 #[test]
440 fn test_spread_creation() {
441 let spread = Spread {
442 bid: Some(0.045),
443 ask: Some(0.055),
444 mid: Some(0.05),
445 };
446
447 assert_eq!(spread.bid, Some(0.045));
448 assert_eq!(spread.ask, Some(0.055));
449 assert_eq!(spread.mid, Some(0.05));
450 }
451
452 #[test]
453 fn test_basic_greeks_creation() {
454 let greeks = BasicGreeks {
455 delta_call: Some(0.99972),
456 delta_put: Some(-0.00077),
457 gamma: Some(0.0),
458 };
459
460 assert_eq!(greeks.delta_call, Some(0.99972));
461 assert_eq!(greeks.delta_put, Some(-0.00077));
462 assert_eq!(greeks.gamma, Some(0.0));
463 }
464
465 #[test]
466 fn test_basic_option_data_creation() {
467 let expiration = Utc.timestamp_millis_opt(1757491200000).single();
468 let option_data = BasicOptionData {
469 strike_price: 106000.0,
470 call_bid: Some(0.0001),
471 call_ask: Some(0.05),
472 put_bid: Some(0.0),
473 put_ask: Some(0.019),
474 implied_volatility: (Some(66.62), Some(107.51)),
475 delta_call: Some(0.99972),
476 delta_put: Some(-0.00077),
477 gamma: Some(0.0),
478 volume: 196.1,
479 open_interest: 75.2,
480 expiration_date: expiration,
481 underlying_price: Some(111421.0915),
482 risk_free_rate: 0.0,
483 extra_fields: None,
484 };
485
486 assert_eq!(option_data.strike_price, 106000.0);
487 assert_eq!(option_data.call_bid, Some(0.0001));
488 assert_eq!(option_data.volume, 196.1);
489 assert_eq!(option_data.open_interest, 75.2);
490 }
491
492 fn create_test_option_pair() -> OptionInstrumentPair {
493 let call_instrument = create_test_instrument("BTC-10SEP25-106000-C", 106000.0, "call");
494 let call_ticker = create_test_ticker(
495 "BTC-10SEP25-106000-C",
496 0.047,
497 0.0487,
498 Some(0.0001),
499 Some(0.05),
500 10.0,
501 30.8,
502 104.2,
503 49.6,
504 Some(0.99972),
505 Some(0.0),
506 Some(66.62),
507 );
508
509 let put_instrument = create_test_instrument("BTC-10SEP25-106000-P", 106000.0, "put");
510 let put_ticker = create_test_ticker(
511 "BTC-10SEP25-106000-P",
512 0.0002,
513 0.0,
514 Some(0.0),
515 Some(0.019),
516 0.0,
517 10.0,
518 91.9,
519 25.6,
520 Some(-0.00077),
521 Some(0.0),
522 Some(107.51),
523 );
524
525 OptionInstrumentPair {
526 call: Some(OptionInstrument {
527 instrument: call_instrument,
528 ticker: call_ticker,
529 }),
530 put: Some(OptionInstrument {
531 instrument: put_instrument,
532 ticker: put_ticker,
533 }),
534 }
535 }
536
537 #[test]
538 fn test_option_pair_expiration() {
539 let pair = create_test_option_pair();
540 let expiration = pair.expiration();
541
542 assert!(expiration.is_some());
543 let exp_date = expiration.unwrap();
544 assert_eq!(exp_date.timestamp_millis(), 1757491200000);
545 }
546
547 #[test]
548 fn test_option_pair_instrument() {
549 let pair = create_test_option_pair();
550 let instrument = pair.instrument();
551
552 assert!(instrument.is_some());
553 let inst = instrument.unwrap();
554 assert_eq!(inst.instrument_name, "BTC-10SEP25-106000-C");
555 assert_eq!(inst.strike, Some(106000.0));
556 }
557
558 #[test]
559 fn test_option_pair_ticker() {
560 let pair = create_test_option_pair();
561 let ticker = pair.ticker();
562
563 assert!(ticker.is_some());
564 let tick = ticker.unwrap();
565 assert_eq!(tick.instrument_name, "BTC-10SEP25-106000-C");
566 assert_eq!(tick.last_price, Some(0.047));
567 }
568
569 #[test]
570 fn test_option_pair_volume() {
571 let pair = create_test_option_pair();
572 let volume = pair.volume();
573
574 assert!((volume - 196.1).abs() < 1e-10);
576 }
577
578 #[test]
579 fn test_option_pair_open_interest() {
580 let pair = create_test_option_pair();
581 let open_interest = pair.open_interest();
582
583 assert_eq!(open_interest, 75.2);
585 }
586
587 #[test]
588 fn test_option_pair_interest_rate() {
589 let pair = create_test_option_pair();
590 let interest_rate = pair.interest_rate();
591
592 assert_eq!(interest_rate, 0.0);
594 }
595
596 #[test]
597 fn test_option_pair_value() {
598 let pair = create_test_option_pair();
599 let value = pair.value();
600
601 assert!(value.is_some());
602 let json_value = value.unwrap();
604 assert!(json_value.is_object());
605 }
606
607 #[test]
608 fn test_option_pair_call_spread() {
609 let pair = create_test_option_pair();
610 let call_spread = pair.call_spread();
611
612 assert_eq!(call_spread.bid, Some(0.0001));
613 assert_eq!(call_spread.ask, Some(0.05));
614 assert_eq!(call_spread.mid, Some((0.0001 + 0.05) / 2.0));
615 }
616
617 #[test]
618 fn test_option_pair_put_spread() {
619 let pair = create_test_option_pair();
620 let put_spread = pair.put_spread();
621
622 assert_eq!(put_spread.bid, Some(0.0));
623 assert_eq!(put_spread.ask, Some(0.019));
624 assert_eq!(put_spread.mid, Some((0.0 + 0.019) / 2.0));
625 }
626
627 #[test]
628 fn test_option_pair_iv() {
629 let pair = create_test_option_pair();
630 let (call_iv, put_iv) = pair.iv();
631
632 assert_eq!(call_iv, Some(66.62));
633 assert_eq!(put_iv, Some(107.51));
634 }
635
636 #[test]
637 fn test_option_pair_greeks() {
638 let pair = create_test_option_pair();
639 let greeks = pair.greeks();
640
641 assert_eq!(greeks.delta_call, Some(0.99972));
642 assert_eq!(greeks.delta_put, Some(-0.00077));
643 assert_eq!(greeks.gamma, Some(0.0));
644 }
645
646 #[test]
647 fn test_option_pair_data() {
648 let pair = create_test_option_pair();
649 let data = pair.data();
650
651 assert_eq!(data.strike_price, 106000.0);
652 assert_eq!(data.call_bid, Some(0.0001));
653 assert_eq!(data.call_ask, Some(0.05));
654 assert_eq!(data.put_bid, Some(0.0));
655 assert_eq!(data.put_ask, Some(0.019));
656 assert_eq!(data.implied_volatility, (Some(66.62), Some(107.51)));
657 assert_eq!(data.delta_call, Some(0.99972));
658 assert_eq!(data.delta_put, Some(-0.00077));
659 assert_eq!(data.gamma, Some(0.0));
660 assert!((data.volume - 196.1).abs() < 1e-10);
661 assert_eq!(data.open_interest, 75.2);
662 assert_eq!(data.underlying_price, Some(111421.0915));
663 assert_eq!(data.risk_free_rate, 0.0);
664 }
665
666 #[test]
667 fn test_option_pair_with_only_call() {
668 let call_instrument = create_test_instrument("BTC-10SEP25-106000-C", 106000.0, "call");
669 let call_ticker = create_test_ticker(
670 "BTC-10SEP25-106000-C",
671 0.047,
672 0.0487,
673 Some(0.0001),
674 Some(0.05),
675 10.0,
676 30.8,
677 104.2,
678 49.6,
679 Some(0.99972),
680 Some(0.0),
681 Some(66.62),
682 );
683
684 let pair = OptionInstrumentPair {
685 call: Some(OptionInstrument {
686 instrument: call_instrument,
687 ticker: call_ticker,
688 }),
689 put: None,
690 };
691
692 assert_eq!(pair.volume(), 104.2);
693 assert_eq!(pair.open_interest(), 49.6);
694
695 let put_spread = pair.put_spread();
696 assert_eq!(put_spread.bid, None);
697 assert_eq!(put_spread.ask, None);
698 assert_eq!(put_spread.mid, None);
699
700 let (call_iv, put_iv) = pair.iv();
701 assert_eq!(call_iv, Some(66.62));
702 assert_eq!(put_iv, None);
703 }
704
705 #[test]
706 fn test_option_pair_with_only_put() {
707 let put_instrument = create_test_instrument("BTC-10SEP25-106000-P", 106000.0, "put");
708 let put_ticker = create_test_ticker(
709 "BTC-10SEP25-106000-P",
710 0.0002,
711 0.0,
712 Some(0.0),
713 Some(0.019),
714 0.0,
715 10.0,
716 91.9,
717 25.6,
718 Some(-0.00077),
719 Some(0.0),
720 Some(107.51),
721 );
722
723 let pair = OptionInstrumentPair {
724 call: None,
725 put: Some(OptionInstrument {
726 instrument: put_instrument,
727 ticker: put_ticker,
728 }),
729 };
730
731 assert_eq!(pair.volume(), 91.9);
732 assert_eq!(pair.open_interest(), 25.6);
733
734 let call_spread = pair.call_spread();
735 assert_eq!(call_spread.bid, None);
736 assert_eq!(call_spread.ask, None);
737 assert_eq!(call_spread.mid, None);
738
739 let (call_iv, put_iv) = pair.iv();
740 assert_eq!(call_iv, None);
741 assert_eq!(put_iv, Some(107.51));
742 }
743
744 #[test]
745 fn test_empty_option_pair() {
746 let pair = OptionInstrumentPair {
747 call: None,
748 put: None,
749 };
750
751 assert_eq!(pair.volume(), 0.0);
752 assert_eq!(pair.open_interest(), 0.0);
753 assert_eq!(pair.interest_rate(), 0.0);
754 assert!(pair.instrument().is_none());
755 assert!(pair.ticker().is_none());
756 assert!(pair.expiration().is_none());
757
758 let (call_iv, put_iv) = pair.iv();
759 assert_eq!(call_iv, None);
760 assert_eq!(put_iv, None);
761
762 let greeks = pair.greeks();
763 assert_eq!(greeks.delta_call, None);
764 assert_eq!(greeks.delta_put, None);
765 assert_eq!(greeks.gamma, None);
766 }
767}