1use crate::constants::ZERO;
2use crate::error::{OptionsError, OptionsResult};
3use crate::model::ExpirationDate;
4use crate::model::payoff::{Payoff, PayoffInfo};
5use crate::model::types::{OptionStyle, OptionType, Side};
6use positive::Positive;
7#[cfg(test)]
8use positive::pos_or_panic;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use tracing::error;
12
13#[cold]
30#[inline(never)]
31fn payoff_out_of_range(context: &'static str, payoff: Decimal, size: Positive) -> OptionsError {
32 OptionsError::PayoffError {
33 reason: format!(
34 "{context}: payoff {payoff} times position size {size} is not representable as a Decimal (out of range)"
35 ),
36 }
37}
38
39#[cold]
42#[inline(never)]
43fn position_size_out_of_range(quantity: Positive, contract_size: Positive) -> OptionsError {
44 OptionsError::ValidationError {
45 field: "contract_size".to_string(),
46 reason: format!(
47 "quantity {quantity} times contract size {contract_size} is not representable as a Positive (out of range)"
48 ),
49 }
50}
51
52#[inline]
55#[must_use]
56fn default_contract_size() -> Positive {
57 Positive::ONE
58}
59
60#[derive(Clone, Default, PartialEq, Serialize, Deserialize, Debug)]
68#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
69pub struct ExoticParams {
70 pub spot_prices: Option<Vec<Positive>>, pub spot_min: Option<Decimal>, pub spot_max: Option<Decimal>, pub cliquet_local_cap: Option<Decimal>, pub cliquet_local_floor: Option<Decimal>, pub cliquet_global_cap: Option<Decimal>, pub cliquet_global_floor: Option<Decimal>, pub rainbow_second_asset_price: Option<Positive>, pub rainbow_second_asset_volatility: Option<Positive>, pub rainbow_second_asset_dividend: Option<Positive>, pub rainbow_correlation: Option<Decimal>, pub spread_second_asset_volatility: Option<Positive>, pub spread_second_asset_dividend: Option<Positive>, pub spread_correlation: Option<Decimal>, pub quanto_fx_volatility: Option<Positive>, pub quanto_fx_correlation: Option<Decimal>, pub quanto_foreign_rate: Option<Decimal>, pub foreign_rate: Option<Decimal>, pub exchange_second_asset_volatility: Option<Positive>, pub exchange_second_asset_dividend: Option<Positive>, pub exchange_correlation: Option<Decimal>, }
147
148#[derive(Clone, PartialEq, Serialize, Deserialize)]
178#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
179pub struct Options {
180 pub option_type: OptionType,
182
183 pub side: Side,
186
187 pub underlying_symbol: String,
189
190 pub strike_price: Positive,
193
194 pub expiration_date: ExpirationDate,
196
197 pub implied_volatility: Positive,
200
201 pub quantity: Positive,
203
204 #[serde(default = "default_contract_size")]
215 pub contract_size: Positive,
216
217 pub underlying_price: Positive,
219
220 pub risk_free_rate: Decimal,
224
225 pub option_style: OptionStyle,
228
229 pub dividend_yield: Positive,
232
233 pub exotic_params: Option<ExoticParams>,
236}
237
238impl Options {
239 #[allow(clippy::too_many_arguments)]
270 #[must_use]
271 pub fn new(
272 option_type: OptionType,
273 side: Side,
274 underlying_symbol: String,
275 strike_price: Positive,
276 expiration_date: ExpirationDate,
277 implied_volatility: Positive,
278 quantity: Positive,
279 underlying_price: Positive,
280 risk_free_rate: Decimal,
281 option_style: OptionStyle,
282 dividend_yield: Positive,
283 exotic_params: Option<ExoticParams>,
284 ) -> Self {
285 Options {
286 option_type,
287 side,
288 underlying_symbol,
289 strike_price,
290 expiration_date,
291 implied_volatility,
292 quantity,
293 contract_size: default_contract_size(),
294 underlying_price,
295 risk_free_rate,
296 option_style,
297 dividend_yield,
298 exotic_params,
299 }
300 }
301
302 #[must_use = "with_contract_size returns the updated option and leaves the original untouched"]
310 #[inline]
311 pub fn with_contract_size(mut self, contract_size: Positive) -> Self {
312 self.contract_size = contract_size;
313 self
314 }
315
316 #[inline]
328 pub fn position_size(&self) -> OptionsResult<Positive> {
329 self.quantity
330 .checked_mul(&self.contract_size)
331 .map_err(|_| position_size_out_of_range(self.quantity, self.contract_size))
332 }
333
334 pub fn time_to_expiration(&self) -> OptionsResult<Positive> {
352 Ok(self.expiration_date.get_years()?)
353 }
354
355 #[must_use]
365 pub fn is_long(&self) -> bool {
366 matches!(self.side, Side::Long)
367 }
368
369 #[must_use]
379 pub fn is_short(&self) -> bool {
380 matches!(self.side, Side::Short)
381 }
382
383 pub fn payoff(&self) -> OptionsResult<Decimal> {
429 let payoff_info = PayoffInfo {
430 spot: self.underlying_price,
431 strike: self.strike_price,
432 style: self.option_style,
433 side: self.side,
434 spot_prices: None,
435 spot_min: None,
436 spot_max: None,
437 exotic_params: self.exotic_params.clone(),
438 };
439 let payoff = self.option_type.payoff(&payoff_info)?;
440 let size = self.position_size()?;
441 payoff
442 .checked_mul(size.to_dec())
443 .ok_or_else(|| payoff_out_of_range("Options::payoff", payoff, size))
444 }
445
446 pub fn payoff_at_price(&self, price: &Positive) -> OptionsResult<Decimal> {
472 let payoff_info = PayoffInfo {
473 spot: *price,
474 strike: self.strike_price,
475 style: self.option_style,
476 side: self.side,
477 spot_prices: None,
478 spot_min: None,
479 spot_max: None,
480 exotic_params: self.exotic_params.clone(),
481 };
482 let payoff = self.option_type.payoff(&payoff_info)?;
483 let size = self.position_size()?;
484 payoff
485 .checked_mul(size.to_dec())
486 .ok_or_else(|| payoff_out_of_range("Options::payoff_at_price", payoff, size))
487 }
488
489 pub fn intrinsic_value(&self, underlying_price: Positive) -> OptionsResult<Decimal> {
513 let payoff_info = PayoffInfo {
514 spot: underlying_price,
515 strike: self.strike_price,
516 style: self.option_style,
517 side: self.side,
518 spot_prices: None,
519 spot_min: None,
520 spot_max: None,
521 exotic_params: self.exotic_params.clone(),
522 };
523 let payoff = self.option_type.payoff(&payoff_info)?;
524 let size = self.position_size()?;
525 payoff
526 .checked_mul(size.to_dec())
527 .ok_or_else(|| payoff_out_of_range("Options::intrinsic_value", payoff, size))
528 }
529
530 #[must_use]
541 pub fn is_in_the_money(&self) -> bool {
542 match self.option_style {
543 OptionStyle::Call => self.underlying_price >= self.strike_price,
544 OptionStyle::Put => self.underlying_price <= self.strike_price,
545 }
546 }
547
548 pub(crate) fn validate(&self) -> bool {
568 if self.underlying_symbol == *"" {
569 error!("Underlying symbol is empty");
570 return false;
571 }
572 if self.implied_volatility < ZERO {
573 error!("Implied volatility is less than zero");
574 return false;
575 }
576 if self.quantity == ZERO {
577 error!("Quantity is equal to zero");
578 return false;
579 }
580 if self.contract_size == Positive::ZERO {
581 error!("Contract size is equal to zero");
582 return false;
583 }
584 if self.strike_price == Positive::ZERO {
585 error!("Strike is zero");
586 return false;
587 }
588 if self.underlying_price == Positive::ZERO {
589 error!("Underlying price is zero");
590 return false;
591 }
592 true
593 }
594}
595
596impl Default for Options {
597 fn default() -> Self {
598 Options {
599 option_type: OptionType::European,
600 side: Side::Long,
601 underlying_symbol: "".to_string(),
602 strike_price: Positive::ZERO,
603 expiration_date: ExpirationDate::Days(Positive::ZERO),
604 implied_volatility: Positive::ZERO,
605 quantity: Positive::ZERO,
606 contract_size: default_contract_size(),
607 underlying_price: Positive::ZERO,
608 risk_free_rate: Decimal::ZERO,
609 option_style: OptionStyle::Call,
610 dividend_yield: Positive::ZERO,
611 exotic_params: None,
612 }
613 }
614}
615
616#[cfg(test)]
617mod tests_options {
618 use super::*;
619 use crate::model::utils::create_sample_option_simplest;
620
621 use approx::assert_relative_eq;
622 use chrono::{Duration, Utc};
623 use num_traits::ToPrimitive;
624 use rust_decimal_macros::dec;
625
626 #[test]
627 fn test_new_option() {
628 let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
629 assert_eq!(option.underlying_symbol, "AAPL");
630 assert_eq!(option.strike_price, 100.0);
631 assert_eq!(option.implied_volatility, 0.2);
632 }
633
634 #[test]
635 fn test_time_to_expiration() {
636 let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
637 assert_relative_eq!(
638 option.time_to_expiration().unwrap().to_f64(),
639 30.0 / 365.0,
640 epsilon = 0.0001
641 );
642
643 let future_date = Utc::now() + Duration::days(60);
644 let option_with_datetime = Options::new(
645 OptionType::European,
646 Side::Long,
647 "AAPL".to_string(),
648 Positive::HUNDRED,
649 ExpirationDate::DateTime(future_date),
650 pos_or_panic!(0.2),
651 Positive::ONE,
652 pos_or_panic!(105.0),
653 dec!(0.05),
654 OptionStyle::Call,
655 pos_or_panic!(0.01),
656 None,
657 );
658 assert!(option_with_datetime.time_to_expiration().unwrap() >= 59.0 / 365.0);
659 assert!(option_with_datetime.time_to_expiration().unwrap() < 61.0 / 365.0);
660 }
661
662 #[test]
663 fn test_is_long_and_short() {
664 let long_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
665 assert!(long_option.is_long());
666 assert!(!long_option.is_short());
667
668 let short_option = Options::new(
669 OptionType::European,
670 Side::Short,
671 "AAPL".to_string(),
672 Positive::HUNDRED,
673 ExpirationDate::Days(pos_or_panic!(30.0)),
674 pos_or_panic!(0.2),
675 Positive::ONE,
676 pos_or_panic!(105.0),
677 dec!(0.05),
678 OptionStyle::Call,
679 pos_or_panic!(0.01),
680 None,
681 );
682 assert!(!short_option.is_long());
683 assert!(short_option.is_short());
684 }
685
686 #[test]
687 fn test_payoff_european_call_long() {
688 let call_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
689 let call_payoff = call_option.payoff().unwrap();
690 assert_eq!(call_payoff, Decimal::ZERO); let put_option = Options::new(
693 OptionType::European,
694 Side::Long,
695 "AAPL".to_string(),
696 Positive::HUNDRED,
697 ExpirationDate::Days(pos_or_panic!(30.0)),
698 pos_or_panic!(0.2),
699 Positive::ONE,
700 pos_or_panic!(95.0),
701 dec!(0.05),
702 OptionStyle::Put,
703 pos_or_panic!(0.01),
704 None,
705 );
706 let put_payoff = put_option.payoff().unwrap();
707 assert_eq!(put_payoff.to_f64().unwrap(), 5.0); }
709}
710
711#[cfg(test)]
712mod tests_valid_option {
713 use super::*;
714
715 use rust_decimal_macros::dec;
716
717 fn create_valid_option() -> Options {
718 Options {
719 option_type: OptionType::European,
720 side: Side::Long,
721 underlying_symbol: "AAPL".to_string(),
722 strike_price: Positive::HUNDRED,
723 expiration_date: ExpirationDate::Days(pos_or_panic!(30.0)),
724 implied_volatility: pos_or_panic!(0.2),
725 quantity: Positive::ONE,
726 contract_size: Positive::ONE,
727 underlying_price: pos_or_panic!(105.0),
728 risk_free_rate: dec!(0.05),
729 option_style: OptionStyle::Call,
730 dividend_yield: pos_or_panic!(0.01),
731 exotic_params: None,
732 }
733 }
734
735 #[test]
736 fn test_valid_option() {
737 let option = create_valid_option();
738 assert!(option.validate());
739 }
740
741 #[test]
742 fn test_empty_underlying_symbol() {
743 let mut option = create_valid_option();
744 option.underlying_symbol = "".to_string();
745 assert!(!option.validate());
746 }
747
748 #[test]
749 fn test_zero_strike_price() {
750 let mut option = create_valid_option();
751 option.strike_price = Positive::ZERO;
752 assert!(!option.validate());
753 }
754
755 #[test]
756 fn test_zero_quantity() {
757 let mut option = create_valid_option();
758 option.quantity = Positive::ZERO;
759 assert!(!option.validate());
760 }
761
762 #[test]
763 fn test_zero_underlying_price() {
764 let mut option = create_valid_option();
765 option.underlying_price = Positive::ZERO;
766 assert!(!option.validate());
767 }
768
769 #[test]
773 fn test_options_validate_negative_risk_free_rate_is_valid() {
774 for rate in [dec!(-0.01), Decimal::ZERO, dec!(-0.0075), dec!(-0.25)] {
775 let mut option = create_valid_option();
776 option.risk_free_rate = rate;
777 assert!(option.validate(), "r = {rate}");
778 }
779 }
780
781 #[test]
783 fn test_options_validate_negative_rate_keeps_other_checks() {
784 let mut option = create_valid_option();
785 option.risk_free_rate = dec!(-0.01);
786 option.quantity = Positive::ZERO;
787 assert!(!option.validate());
788
789 let mut option = create_valid_option();
790 option.risk_free_rate = dec!(-0.01);
791 option.underlying_symbol = String::new();
792 assert!(!option.validate());
793
794 let mut option = create_valid_option();
795 option.risk_free_rate = dec!(-0.01);
796 option.underlying_price = Positive::ZERO;
797 assert!(!option.validate());
798 }
799}
800
801#[cfg(test)]
802mod tests_options_payoffs {
803 use super::*;
804 use crate::model::utils::create_sample_option_simplest_strike;
805
806 use rust_decimal_macros::dec;
807
808 #[test]
809 fn test_payoff_european_call_long() {
810 let call_option = create_sample_option_simplest_strike(
811 Side::Long,
812 OptionStyle::Call,
813 pos_or_panic!(95.0),
814 );
815 let call_payoff = call_option.payoff().unwrap();
816 assert_eq!(call_payoff, dec!(5.0)); let call_option_otm = create_sample_option_simplest_strike(
819 Side::Long,
820 OptionStyle::Call,
821 pos_or_panic!(105.0),
822 );
823 let call_payoff_otm = call_option_otm.payoff().unwrap();
824 assert_eq!(call_payoff_otm, Decimal::ZERO); }
826
827 #[test]
828 fn test_payoff_european_call_short() {
829 let call_option = create_sample_option_simplest_strike(
830 Side::Short,
831 OptionStyle::Call,
832 pos_or_panic!(95.0),
833 );
834 let call_payoff = call_option.payoff().unwrap();
835 assert_eq!(call_payoff, dec!(-5.0)); let call_option_otm = create_sample_option_simplest_strike(
838 Side::Short,
839 OptionStyle::Call,
840 pos_or_panic!(105.0),
841 );
842 let call_payoff_otm = call_option_otm.payoff().unwrap();
843 assert_eq!(call_payoff_otm, Decimal::ZERO); }
845
846 #[test]
847 fn test_payoff_european_put_long() {
848 let put_option = create_sample_option_simplest_strike(
849 Side::Long,
850 OptionStyle::Put,
851 pos_or_panic!(105.0),
852 );
853 let put_payoff = put_option.payoff().unwrap();
854 assert_eq!(put_payoff, dec!(5.0)); let put_option_otm =
857 create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(95.0));
858 let put_payoff_otm = put_option_otm.payoff().unwrap();
859 assert_eq!(put_payoff_otm, Decimal::ZERO); }
861
862 #[test]
863 fn test_payoff_european_put_short() {
864 let put_option = create_sample_option_simplest_strike(
865 Side::Short,
866 OptionStyle::Put,
867 pos_or_panic!(105.0),
868 );
869 let put_payoff = put_option.payoff().unwrap();
870 assert_eq!(put_payoff, dec!(-5.0)); let put_option_otm = create_sample_option_simplest_strike(
873 Side::Short,
874 OptionStyle::Put,
875 pos_or_panic!(95.0),
876 );
877 let put_payoff_otm = put_option_otm.payoff().unwrap();
878 assert_eq!(put_payoff_otm, Decimal::ZERO); }
880
881 #[test]
884 fn test_payoff_out_of_range_reports_an_error_instead_of_zero() {
885 let mut option = create_sample_option_simplest_strike(
886 Side::Long,
887 OptionStyle::Call,
888 pos_or_panic!(95.0),
889 );
890 option.strike_price = Positive::ZERO;
891 option.underlying_price = Positive::MAX;
892 option.quantity = Positive::TWO;
893
894 assert!(matches!(
897 option.payoff(),
898 Err(OptionsError::PayoffError { .. })
899 ));
900 assert!(matches!(
901 option.payoff_at_price(&Positive::MAX),
902 Err(OptionsError::PayoffError { .. })
903 ));
904 assert!(matches!(
905 option.intrinsic_value(Positive::MAX),
906 Err(OptionsError::PayoffError { .. })
907 ));
908 }
909
910 #[test]
913 fn test_payoff_out_of_the_money_still_returns_zero() {
914 let mut option =
915 create_sample_option_simplest_strike(Side::Long, OptionStyle::Put, pos_or_panic!(95.0));
916 option.strike_price = Positive::ZERO;
917 option.underlying_price = Positive::MAX;
918 option.quantity = Positive::ONE;
919
920 assert_eq!(option.payoff().unwrap(), Decimal::ZERO);
921 assert_eq!(
922 option.payoff_at_price(&Positive::MAX).unwrap(),
923 Decimal::ZERO
924 );
925 assert_eq!(
926 option.intrinsic_value(Positive::MAX).unwrap(),
927 Decimal::ZERO
928 );
929 }
930}
931
932#[cfg(test)]
933mod tests_options_payoff_at_price {
934 use super::*;
935 use crate::model::utils::create_sample_option_simplest;
936
937 use rust_decimal_macros::dec;
938
939 #[test]
940 fn test_payoff_european_call_long() {
941 let call_option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
942 let call_payoff = call_option.payoff_at_price(&pos_or_panic!(105.0)).unwrap();
943 assert_eq!(call_payoff, dec!(5.0)); let call_option_otm = create_sample_option_simplest(OptionStyle::Call, Side::Long);
946 let call_payoff_otm = call_option_otm
947 .payoff_at_price(&pos_or_panic!(95.0))
948 .unwrap();
949 assert_eq!(call_payoff_otm, Decimal::ZERO); }
951
952 #[test]
953 fn test_payoff_european_call_short() {
954 let call_option = create_sample_option_simplest(OptionStyle::Call, Side::Short);
955 let call_payoff = call_option.payoff_at_price(&pos_or_panic!(105.0)).unwrap();
956 assert_eq!(call_payoff, dec!(-5.0)); let call_option_otm = create_sample_option_simplest(OptionStyle::Call, Side::Short);
959 let call_payoff_otm = call_option_otm
960 .payoff_at_price(&pos_or_panic!(95.0))
961 .unwrap();
962 assert_eq!(call_payoff_otm, Decimal::ZERO); }
964
965 #[test]
966 fn test_payoff_european_put_long() {
967 let put_option = create_sample_option_simplest(OptionStyle::Put, Side::Long);
968 let put_payoff = put_option.payoff_at_price(&pos_or_panic!(95.0)).unwrap();
969 assert_eq!(put_payoff, dec!(5.0)); let put_option_otm = create_sample_option_simplest(OptionStyle::Put, Side::Long);
972 let put_payoff_otm = put_option_otm
973 .payoff_at_price(&pos_or_panic!(105.0))
974 .unwrap();
975 assert_eq!(put_payoff_otm, Decimal::ZERO); }
977
978 #[test]
979 fn test_payoff_european_put_short() {
980 let put_option = create_sample_option_simplest(OptionStyle::Put, Side::Short);
981 let put_payoff = put_option.payoff_at_price(&pos_or_panic!(95.0)).unwrap();
982 assert_eq!(put_payoff, dec!(-5.0)); let put_option_otm = create_sample_option_simplest(OptionStyle::Put, Side::Short);
985 let put_payoff_otm = put_option_otm
986 .payoff_at_price(&pos_or_panic!(105.0))
987 .unwrap();
988 assert_eq!(put_payoff_otm, Decimal::ZERO); }
990}
991
992#[cfg(test)]
993mod tests_options_payoffs_with_quantity {
994 use super::*;
995 use crate::model::utils::create_sample_option;
996
997 use num_traits::ToPrimitive;
998 use rust_decimal_macros::dec;
999
1000 #[test]
1001 fn test_payoff_call_long() {
1002 let option = create_sample_option(
1003 OptionStyle::Call,
1004 Side::Long,
1005 pos_or_panic!(105.0),
1006 pos_or_panic!(10.0),
1007 Positive::HUNDRED,
1008 pos_or_panic!(0.02),
1009 );
1010 assert_eq!(option.payoff().unwrap().to_f64().unwrap(), 50.0);
1011
1012 let option_otm = create_sample_option(
1013 OptionStyle::Call,
1014 Side::Long,
1015 pos_or_panic!(95.0),
1016 pos_or_panic!(4.0),
1017 Positive::HUNDRED,
1018 pos_or_panic!(0.02),
1019 );
1020 assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
1021 }
1022
1023 #[test]
1024 fn test_payoff_call_short() {
1025 let option = create_sample_option(
1026 OptionStyle::Call,
1027 Side::Short,
1028 pos_or_panic!(105.0),
1029 pos_or_panic!(3.0),
1030 Positive::HUNDRED,
1031 pos_or_panic!(0.02),
1032 );
1033 assert_eq!(option.payoff().unwrap().to_f64().unwrap(), -15.0);
1034
1035 let option_otm = create_sample_option(
1036 OptionStyle::Call,
1037 Side::Short,
1038 pos_or_panic!(95.0),
1039 pos_or_panic!(7.0),
1040 Positive::HUNDRED,
1041 pos_or_panic!(0.02),
1042 );
1043 assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
1044 }
1045
1046 #[test]
1047 fn test_payoff_put_long() {
1048 let option = create_sample_option(
1049 OptionStyle::Put,
1050 Side::Long,
1051 pos_or_panic!(95.0),
1052 Positive::TWO,
1053 Positive::HUNDRED,
1054 pos_or_panic!(0.02),
1055 );
1056 assert_eq!(option.payoff().unwrap().to_f64().unwrap(), 10.0);
1057
1058 let option_otm = create_sample_option(
1059 OptionStyle::Put,
1060 Side::Long,
1061 pos_or_panic!(105.0),
1062 pos_or_panic!(7.0),
1063 Positive::HUNDRED,
1064 pos_or_panic!(0.02),
1065 );
1066 assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
1067 }
1068
1069 #[test]
1070 fn test_payoff_put_short() {
1071 let option = create_sample_option(
1072 OptionStyle::Put,
1073 Side::Short,
1074 pos_or_panic!(95.0),
1075 pos_or_panic!(3.0),
1076 Positive::HUNDRED,
1077 pos_or_panic!(0.02),
1078 );
1079 assert_eq!(option.payoff().unwrap().to_f64().unwrap(), -15.0);
1080
1081 let option_otm = create_sample_option(
1082 OptionStyle::Put,
1083 Side::Short,
1084 pos_or_panic!(105.0),
1085 pos_or_panic!(3.0),
1086 Positive::HUNDRED,
1087 pos_or_panic!(0.02),
1088 );
1089 assert_eq!(option_otm.payoff().unwrap(), Decimal::ZERO);
1090 }
1091
1092 #[test]
1093 fn test_payoff_with_quantity() {
1094 let option = create_sample_option(
1095 OptionStyle::Call,
1096 Side::Long,
1097 pos_or_panic!(110.0),
1098 pos_or_panic!(3.0),
1099 Positive::HUNDRED,
1100 pos_or_panic!(0.02),
1101 );
1102 assert_eq!(option.payoff().unwrap().to_f64().unwrap(), 30.0); }
1104
1105 #[test]
1106 fn test_intrinsic_value_call_long() {
1107 let option = create_sample_option(
1108 OptionStyle::Call,
1109 Side::Long,
1110 Positive::HUNDRED,
1111 pos_or_panic!(11.0),
1112 Positive::HUNDRED,
1113 pos_or_panic!(0.02),
1114 );
1115 assert_eq!(
1116 option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
1117 dec!(55.0)
1118 );
1119 assert_eq!(
1120 option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
1121 Decimal::ZERO
1122 );
1123 }
1124
1125 #[test]
1126 fn test_intrinsic_value_call_short() {
1127 let option = create_sample_option(
1128 OptionStyle::Call,
1129 Side::Short,
1130 Positive::HUNDRED,
1131 pos_or_panic!(13.0),
1132 Positive::HUNDRED,
1133 pos_or_panic!(0.02),
1134 );
1135 assert_eq!(
1136 option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
1137 dec!(-65.0)
1138 );
1139 assert_eq!(
1140 option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
1141 Decimal::ZERO
1142 );
1143 }
1144
1145 #[test]
1146 fn test_intrinsic_value_put_long() {
1147 let option = create_sample_option(
1148 OptionStyle::Put,
1149 Side::Long,
1150 Positive::HUNDRED,
1151 pos_or_panic!(17.0),
1152 Positive::HUNDRED,
1153 pos_or_panic!(0.02),
1154 );
1155 assert_eq!(
1156 option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
1157 dec!(85.0)
1158 );
1159 assert_eq!(
1160 option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
1161 Decimal::ZERO
1162 );
1163 }
1164
1165 #[test]
1166 fn test_intrinsic_value_put_short() {
1167 let option = create_sample_option(
1168 OptionStyle::Put,
1169 Side::Short,
1170 Positive::HUNDRED,
1171 pos_or_panic!(19.0),
1172 Positive::HUNDRED,
1173 pos_or_panic!(0.02),
1174 );
1175 assert_eq!(
1176 option.intrinsic_value(pos_or_panic!(95.0)).unwrap(),
1177 dec!(-95.0)
1178 );
1179 assert_eq!(
1180 option.intrinsic_value(pos_or_panic!(105.0)).unwrap(),
1181 Decimal::ZERO
1182 );
1183 }
1184
1185 #[test]
1186 fn test_intrinsic_value_with_quantity() {
1187 let option = create_sample_option(
1188 OptionStyle::Call,
1189 Side::Long,
1190 Positive::HUNDRED,
1191 pos_or_panic!(23.0),
1192 Positive::HUNDRED,
1193 pos_or_panic!(0.02),
1194 );
1195 assert_eq!(
1196 option.intrinsic_value(pos_or_panic!(110.0)).unwrap(),
1197 dec!(230.0)
1198 ); }
1200}
1201
1202#[cfg(test)]
1203mod tests_in_the_money {
1204 use super::*;
1205 use crate::model::utils::create_sample_option;
1206
1207 #[test]
1208 fn test_call_in_the_money() {
1209 let mut option = create_sample_option(
1210 OptionStyle::Call,
1211 Side::Long,
1212 pos_or_panic!(110.0),
1213 Positive::ONE,
1214 pos_or_panic!(110.0),
1215 pos_or_panic!(0.02),
1216 );
1217 option.strike_price = Positive::HUNDRED;
1218 assert!(option.is_in_the_money());
1219 }
1220
1221 #[test]
1222 fn test_call_at_the_money() {
1223 let mut option = create_sample_option(
1224 OptionStyle::Call,
1225 Side::Long,
1226 Positive::HUNDRED,
1227 Positive::ONE,
1228 pos_or_panic!(110.0),
1229 pos_or_panic!(0.02),
1230 );
1231 option.strike_price = Positive::HUNDRED;
1232 assert!(option.is_in_the_money());
1233 }
1234
1235 #[test]
1236 fn test_call_out_of_the_money() {
1237 let mut option = create_sample_option(
1238 OptionStyle::Call,
1239 Side::Long,
1240 pos_or_panic!(90.0),
1241 Positive::ONE,
1242 pos_or_panic!(110.0),
1243 pos_or_panic!(0.02),
1244 );
1245 option.strike_price = Positive::HUNDRED;
1246 assert!(!option.is_in_the_money());
1247 }
1248
1249 #[test]
1250 fn test_put_in_the_money() {
1251 let mut option = create_sample_option(
1252 OptionStyle::Put,
1253 Side::Long,
1254 pos_or_panic!(90.0),
1255 Positive::ONE,
1256 pos_or_panic!(110.0),
1257 pos_or_panic!(0.02),
1258 );
1259 option.strike_price = Positive::HUNDRED;
1260 assert!(option.is_in_the_money());
1261 }
1262
1263 #[test]
1264 fn test_put_at_the_money() {
1265 let mut option = create_sample_option(
1266 OptionStyle::Put,
1267 Side::Long,
1268 Positive::HUNDRED,
1269 Positive::ONE,
1270 pos_or_panic!(110.0),
1271 pos_or_panic!(0.02),
1272 );
1273 option.strike_price = Positive::HUNDRED;
1274 assert!(option.is_in_the_money());
1275 }
1276
1277 #[test]
1278 fn test_put_out_of_the_money() {
1279 let mut option = create_sample_option(
1280 OptionStyle::Put,
1281 Side::Long,
1282 pos_or_panic!(110.0),
1283 Positive::ONE,
1284 pos_or_panic!(110.0),
1285 pos_or_panic!(0.02),
1286 );
1287 option.strike_price = Positive::HUNDRED;
1288 assert!(!option.is_in_the_money());
1289 }
1290}
1291
1292#[cfg(test)]
1293mod tests_serialize_deserialize {
1294 use super::*;
1295 use crate::model::utils::create_sample_option_simplest_strike;
1296
1297 #[test]
1298 fn test_serialize_deserialize_options() {
1299 let options = create_sample_option_simplest_strike(
1300 Side::Long,
1301 OptionStyle::Call,
1302 pos_or_panic!(95.0),
1303 );
1304 let serialized = serde_json::to_string(&options).expect("Failed to serialize");
1305 let deserialized: Options =
1306 serde_json::from_str(&serialized).expect("Failed to deserialize");
1307 assert_eq!(options, deserialized);
1308 }
1309}
1310
1311#[cfg(test)]
1312mod tests_contract_size {
1313 use super::*;
1314 use crate::model::utils::create_sample_option;
1315 use rust_decimal_macros::dec;
1316
1317 fn call(side: Side, spot: Positive, quantity: Positive) -> Options {
1318 create_sample_option(
1319 OptionStyle::Call,
1320 side,
1321 spot,
1322 quantity,
1323 Positive::HUNDRED,
1324 pos_or_panic!(0.2),
1325 )
1326 }
1327
1328 #[test]
1329 fn test_options_contract_size_defaults_to_one() {
1330 let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
1331 assert_eq!(option.contract_size, Positive::ONE);
1332 assert_eq!(Options::default().contract_size, Positive::ONE);
1333 assert_eq!(option.position_size().ok(), Some(Positive::ONE));
1334 }
1335
1336 #[test]
1337 fn test_options_with_contract_size_sets_the_multiplier() {
1338 let option = call(Side::Long, pos_or_panic!(110.0), Positive::TWO)
1339 .with_contract_size(Positive::HUNDRED);
1340 assert_eq!(option.contract_size, Positive::HUNDRED);
1341 assert_eq!(option.position_size().ok(), Some(pos_or_panic!(200.0)));
1342 }
1343
1344 #[test]
1345 fn test_options_position_size_overflow_is_an_error() {
1346 let option = call(Side::Long, pos_or_panic!(110.0), Positive::MAX)
1347 .with_contract_size(Positive::HUNDRED);
1348 assert!(matches!(
1349 option.position_size(),
1350 Err(OptionsError::ValidationError { .. })
1351 ));
1352 assert!(option.payoff().is_err());
1353 }
1354
1355 #[test]
1356 fn test_options_payoff_scales_by_contract_size() {
1357 let one = call(Side::Long, pos_or_panic!(110.0), Positive::TWO);
1359 let sized = one.clone().with_contract_size(Positive::HUNDRED);
1360 assert_eq!(one.payoff().ok(), Some(dec!(20)));
1361 assert_eq!(sized.payoff().ok(), Some(dec!(2000)));
1362 assert_eq!(
1363 sized.payoff_at_price(&pos_or_panic!(120.0)).ok(),
1364 Some(dec!(4000))
1365 );
1366 assert_eq!(
1367 sized.intrinsic_value(pos_or_panic!(105.0)).ok(),
1368 Some(dec!(1000))
1369 );
1370 assert_eq!(
1371 sized.intrinsic_value(pos_or_panic!(90.0)).ok(),
1372 Some(Decimal::ZERO)
1373 );
1374 }
1375
1376 #[test]
1377 fn test_options_short_payoff_scales_by_contract_size() {
1378 let sized = call(Side::Short, pos_or_panic!(110.0), Positive::ONE)
1379 .with_contract_size(Positive::HUNDRED);
1380 assert_eq!(sized.payoff().ok(), Some(dec!(-1000)));
1381 }
1382
1383 #[test]
1384 fn test_options_validate_rejects_zero_contract_size() {
1385 let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
1386 assert!(option.validate());
1387 assert!(!option.with_contract_size(Positive::ZERO).validate());
1388 }
1389
1390 #[test]
1391 fn test_options_deserialize_without_contract_size_defaults_to_one() {
1392 let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
1393 let mut value = serde_json::to_value(&option).expect("serialize");
1394 let removed = value
1395 .as_object_mut()
1396 .and_then(|map| map.remove("contract_size"));
1397 assert!(removed.is_some(), "contract_size is serialized");
1398 let restored: Options = serde_json::from_value(value).expect("deserialize");
1399 assert_eq!(restored.contract_size, Positive::ONE);
1400 assert_eq!(restored, option);
1401 }
1402
1403 #[test]
1404 fn test_options_contract_size_round_trips_through_serde() {
1405 let option = call(Side::Long, pos_or_panic!(110.0), Positive::ONE)
1406 .with_contract_size(Positive::HUNDRED);
1407 let json = serde_json::to_string(&option).expect("serialize");
1408 let restored: Options = serde_json::from_str(&json).expect("deserialize");
1409 assert_eq!(restored.contract_size, Positive::HUNDRED);
1410 }
1411
1412 #[test]
1413 fn test_options_format_shows_contract_size_only_when_not_one() {
1414 let one = call(Side::Long, pos_or_panic!(110.0), Positive::ONE);
1415 assert!(!format!("{one}").contains("Contract Size"));
1416 assert!(!format!("{one:?}").contains("contract_size"));
1417 let sized = one.with_contract_size(Positive::HUNDRED);
1418 assert!(format!("{sized}").contains("Contract Size: 100"));
1419 assert!(format!("{sized:?}").contains("contract_size: 100"));
1420 }
1421}