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