1use crate::error::SettlementError;
11use crate::model::Options;
12use crate::model::types::{OptionStyle, OptionType, Side};
13use chrono::{DateTime, Utc};
14use positive::Positive;
15use rust_decimal::Decimal;
16use serde::{Deserialize, Serialize};
17use std::fmt::Display;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
22#[serde(rename_all = "snake_case")]
23#[repr(u8)]
24pub enum SettlementType {
25 Cash,
27 Physical,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
34#[serde(rename_all = "snake_case")]
35#[repr(u8)]
36pub enum SettlementEvent {
37 Expiry,
40 Exercise,
42 Assignment,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
49#[serde(deny_unknown_fields)]
50pub struct SettlementPrice {
51 pub price: Positive,
53 pub source: String,
56 pub observed_at: DateTime<Utc>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
64#[serde(deny_unknown_fields)]
65pub struct CashRounding {
66 pub scale: u32,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
74#[serde(deny_unknown_fields)]
75pub struct SettlementTerms {
76 pub settlement_type: SettlementType,
78 pub event: SettlementEvent,
80 pub price: Option<SettlementPrice>,
83 pub cutoff: DateTime<Utc>,
85 pub rounding: CashRounding,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
92pub struct SettlementObligation {
93 pub extinguished_quantity: Positive,
96 pub cash: Decimal,
98 pub underlying: Decimal,
101}
102
103#[must_use = "the settlement obligation is the only output of this computation"]
241pub fn settlement_obligation(
242 option: &Options,
243 terms: &SettlementTerms,
244) -> Result<SettlementObligation, SettlementError> {
245 check_option_type(&option.option_type)?;
246 check_event_side(terms.event, option.side)?;
247 let intrinsic = match observed_price(terms)? {
248 Some(price) => Some(intrinsic_per_unit(
249 option.option_style,
250 option.strike_price,
251 price,
252 )?),
253 None => None,
254 };
255 let size = option
256 .position_size()
257 .map_err(|error| overflow("position size", error))?
258 .to_dec();
259 let quantity = option.quantity.to_dec();
260 let multiplier = option.contract_size.to_dec();
261
262 let (cash, underlying) = match (terms.settlement_type, terms.event, intrinsic) {
263 (SettlementType::Cash, _, None)
264 | (SettlementType::Physical, SettlementEvent::Expiry, None) => {
265 return Err(SettlementError::MissingPrice {
266 settlement_type: terms.settlement_type,
267 event: terms.event,
268 });
269 }
270 (SettlementType::Cash, _, Some(intrinsic)) => {
271 let amount = rounded_amount(
272 "cash settlement",
273 intrinsic,
274 quantity,
275 multiplier,
276 terms.rounding,
277 )?;
278 (signed(amount, option.side == Side::Long), Decimal::ZERO)
279 }
280 (SettlementType::Physical, SettlementEvent::Expiry, Some(intrinsic))
281 if intrinsic.is_zero() =>
282 {
283 (Decimal::ZERO, Decimal::ZERO)
284 }
285 (SettlementType::Physical, _, _) => {
286 let strike = ExactDecimal::from_decimal(option.strike_price.to_dec())
287 .ok_or_else(|| overflow("physical cash", "the strike is out of range"))?;
288 let amount = rounded_amount(
289 "physical cash",
290 strike,
291 quantity,
292 multiplier,
293 terms.rounding,
294 )?;
295 let receives_underlying = matches!(
296 (option.side, option.option_style),
297 (Side::Long, OptionStyle::Call) | (Side::Short, OptionStyle::Put)
298 );
299 (
300 signed(amount, !receives_underlying),
301 signed(size, receives_underlying),
302 )
303 }
304 };
305
306 Ok(SettlementObligation {
307 extinguished_quantity: option.quantity,
308 cash,
309 underlying,
310 })
311}
312
313fn check_option_type(option_type: &OptionType) -> Result<(), SettlementError> {
315 match option_type {
316 OptionType::European | OptionType::American => Ok(()),
317 unsupported => Err(unsupported_option_type(unsupported)),
318 }
319}
320
321fn check_event_side(event: SettlementEvent, side: Side) -> Result<(), SettlementError> {
323 match (event, side) {
324 (SettlementEvent::Exercise, Side::Short) | (SettlementEvent::Assignment, Side::Long) => {
325 Err(SettlementError::EventSideMismatch { event, side })
326 }
327 _ => Ok(()),
328 }
329}
330
331fn observed_price(terms: &SettlementTerms) -> Result<Option<Positive>, SettlementError> {
333 match &terms.price {
334 Some(price) if price.observed_at > terms.cutoff => Err(SettlementError::PriceAfterCutoff {
335 observed_at: price.observed_at,
336 cutoff: terms.cutoff,
337 }),
338 Some(price) => Ok(Some(price.price)),
339 None => Ok(None),
340 }
341}
342
343fn intrinsic_per_unit(
346 style: OptionStyle,
347 strike: Positive,
348 price: Positive,
349) -> Result<ExactDecimal, SettlementError> {
350 let (minuend, subtrahend) = match style {
351 OptionStyle::Call => (price.to_dec(), strike.to_dec()),
352 OptionStyle::Put => (strike.to_dec(), price.to_dec()),
353 };
354 let scale = minuend.scale().max(subtrahend.scale());
355 let aligned = |value: Decimal| {
356 (value.scale()..scale).try_fold(
357 ExactUint::from_u128(value.mantissa().unsigned_abs())?,
358 |digits, _| digits.checked_mul_u32(10),
359 )
360 };
361 let difference = aligned(minuend).zip(aligned(subtrahend)).ok_or_else(|| {
362 overflow(
363 "intrinsic value",
364 format!("{minuend} minus {subtrahend} is out of range"),
365 )
366 })?;
367 Ok(ExactDecimal {
368 digits: difference
369 .0
370 .checked_sub(difference.1)
371 .unwrap_or(ExactUint::ZERO),
372 scale,
373 })
374}
375
376fn rounded_amount(
379 operation: &'static str,
380 amount: ExactDecimal,
381 quantity: Decimal,
382 multiplier: Decimal,
383 rounding: CashRounding,
384) -> Result<Decimal, SettlementError> {
385 let out_of_range = || {
386 overflow(
387 operation,
388 format!("{quantity} times {multiplier} leaves the range"),
389 )
390 };
391 let digits = amount
392 .digits
393 .checked_mul_u128(quantity.mantissa().unsigned_abs())
394 .and_then(|digits| digits.checked_mul_u128(multiplier.mantissa().unsigned_abs()))
395 .ok_or_else(out_of_range)?;
396 let scale = amount
397 .scale
398 .checked_add(quantity.scale())
399 .and_then(|scale| scale.checked_add(multiplier.scale()))
400 .ok_or_else(out_of_range)?;
401 ExactDecimal { digits, scale }
402 .rounded(rounding.scale)
403 .ok_or_else(out_of_range)
404}
405
406const EXACT_LIMBS: usize = 12;
410
411const LIMB_BITS: u32 = u32::BITS;
413
414const LIMB_MASK: u128 = 0xFFFF_FFFF;
416
417const MAX_DECIMAL_SCALE: u32 = 28;
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424struct ExactUint([u32; EXACT_LIMBS]);
425
426impl ExactUint {
427 const ZERO: Self = Self([0; EXACT_LIMBS]);
428
429 fn from_u128(value: u128) -> Option<Self> {
430 let mut limbs = [0; EXACT_LIMBS];
431 let mut rest = value;
432 for limb in limbs.iter_mut().take(4) {
433 *limb = u32::try_from(rest & LIMB_MASK).ok()?;
434 rest = rest.checked_shr(LIMB_BITS)?;
435 }
436 Some(Self(limbs))
437 }
438
439 fn to_u128(self) -> Option<u128> {
441 if self.0.iter().skip(4).any(|limb| *limb != 0) {
442 return None;
443 }
444 self.0.iter().take(4).rev().try_fold(0_u128, |value, limb| {
445 Some(value.checked_shl(LIMB_BITS)? | u128::from(*limb))
446 })
447 }
448
449 fn is_zero(self) -> bool {
450 self == Self::ZERO
451 }
452
453 fn is_odd(self) -> bool {
454 self.0.first().is_some_and(|limb| limb & 1 == 1)
455 }
456
457 fn checked_add(self, other: Self) -> Option<Self> {
458 let mut limbs = self.0;
459 let mut carry = 0_u64;
460 for (limb, addend) in limbs.iter_mut().zip(other.0) {
461 let sum = u64::from(*limb)
462 .checked_add(u64::from(addend))?
463 .checked_add(carry)?;
464 (*limb, carry) = split_limb(sum)?;
465 }
466 (carry == 0).then_some(Self(limbs))
467 }
468
469 fn checked_sub(self, other: Self) -> Option<Self> {
471 let mut limbs = self.0;
472 let mut borrow = false;
473 for (limb, subtrahend) in limbs.iter_mut().zip(other.0) {
474 let (partial, first) = limb.overflowing_sub(subtrahend);
475 let (difference, second) = partial.overflowing_sub(u32::from(borrow));
476 *limb = difference;
477 borrow = first || second;
478 }
479 (!borrow).then_some(Self(limbs))
480 }
481
482 fn checked_mul_u32(self, factor: u32) -> Option<Self> {
483 let mut limbs = self.0;
484 let mut carry = 0_u64;
485 for limb in &mut limbs {
486 let product = u64::from(*limb)
487 .checked_mul(u64::from(factor))?
488 .checked_add(carry)?;
489 (*limb, carry) = split_limb(product)?;
490 }
491 (carry == 0).then_some(Self(limbs))
492 }
493
494 fn checked_mul_u128(self, factor: u128) -> Option<Self> {
496 let factor = Self::from_u128(factor)?;
497 factor
498 .0
499 .iter()
500 .take(4)
501 .rev()
502 .try_fold(Self::ZERO, |product, limb| {
503 product
504 .shifted_one_limb()?
505 .checked_add(self.checked_mul_u32(*limb)?)
506 })
507 }
508
509 fn shifted_one_limb(self) -> Option<Self> {
510 let mut limbs = self.0;
511 limbs.rotate_right(1);
512 (limbs.first() == Some(&0)).then_some(Self(limbs))
513 }
514
515 fn divided_by_ten(self) -> Option<(Self, u32)> {
517 let mut limbs = self.0;
518 let mut remainder = 0_u64;
519 for limb in limbs.iter_mut().rev() {
520 let dividend = remainder.checked_shl(LIMB_BITS)? | u64::from(*limb);
521 *limb = u32::try_from(dividend.checked_div(10)?).ok()?;
522 remainder = dividend.checked_rem(10)?;
523 }
524 Some((Self(limbs), u32::try_from(remainder).ok()?))
525 }
526}
527
528#[inline]
530fn split_limb(value: u64) -> Option<(u32, u64)> {
531 let low = u32::try_from(value & u64::from(u32::MAX)).ok()?;
532 Some((low, value.checked_shr(LIMB_BITS)?))
533}
534
535#[derive(Debug, Clone, Copy)]
537struct ExactDecimal {
538 digits: ExactUint,
539 scale: u32,
540}
541
542impl ExactDecimal {
543 fn from_decimal(value: Decimal) -> Option<Self> {
544 Some(Self {
545 digits: ExactUint::from_u128(value.mantissa().unsigned_abs())?,
546 scale: value.scale(),
547 })
548 }
549
550 fn is_zero(self) -> bool {
551 self.digits.is_zero()
552 }
553
554 fn rounded(self, scale: u32) -> Option<Decimal> {
557 let target = scale.min(MAX_DECIMAL_SCALE);
558 let mut digits = self.digits;
559 let mut current = self.scale;
560 let mut dropped = 0;
563 let mut below_dropped = false;
564 loop {
565 if current <= target {
566 let rounds_up = dropped > 5 || (dropped == 5 && (below_dropped || digits.is_odd()));
567 let rounded = if rounds_up {
568 digits.checked_add(ExactUint::from_u128(1)?)?
569 } else {
570 digits
571 };
572 let value = rounded
573 .to_u128()
574 .and_then(|mantissa| i128::try_from(mantissa).ok())
575 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, current).ok());
576 if value.is_some() {
577 return value;
578 }
579 if current == 0 {
580 return None;
581 }
582 }
583 let (quotient, digit) = digits.divided_by_ten()?;
584 below_dropped = below_dropped || dropped != 0;
585 dropped = digit;
586 digits = quotient;
587 current = current.checked_sub(1)?;
588 }
589 }
590}
591
592#[inline]
595fn signed(magnitude: Decimal, received: bool) -> Decimal {
596 if received || magnitude.is_zero() {
597 magnitude
598 } else {
599 -magnitude
600 }
601}
602
603#[cold]
604#[inline(never)]
605fn unsupported_option_type(option_type: &OptionType) -> SettlementError {
606 SettlementError::UnsupportedOptionType {
607 option_type: option_type.clone(),
608 }
609}
610
611#[cold]
612#[inline(never)]
613fn overflow(operation: &'static str, reason: impl Display) -> SettlementError {
614 SettlementError::ArithmeticOverflow {
615 operation,
616 reason: reason.to_string(),
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use crate::model::ExpirationDate;
624 use crate::model::types::{AsianAveragingType, BarrierType};
625 use chrono::TimeDelta;
626 use rust_decimal_macros::dec;
627 use serde::de::DeserializeOwned;
628
629 const STRIKE: Decimal = dec!(100);
630 const IN_THE_MONEY_CALL: Decimal = dec!(110);
631 const IN_THE_MONEY_PUT: Decimal = dec!(90);
632 const QUANTITY: Decimal = dec!(2);
634 const MULTIPLIER: Decimal = dec!(100);
636 const SIZE: Decimal = dec!(200);
638 const CASH_AMOUNT: Decimal = dec!(2000);
640 const PHYSICAL_CASH: Decimal = dec!(20000);
642
643 fn pos(value: Decimal) -> Positive {
644 Positive::new_decimal(value).unwrap_or(Positive::ZERO)
645 }
646
647 fn cutoff() -> DateTime<Utc> {
648 DateTime::<Utc>::UNIX_EPOCH + TimeDelta::days(20_000)
649 }
650
651 fn contract(
652 option_type: OptionType,
653 side: Side,
654 style: OptionStyle,
655 strike: Decimal,
656 quantity: Decimal,
657 multiplier: Decimal,
658 ) -> Options {
659 Options::new(
660 option_type,
661 side,
662 "TEST".to_string(),
663 pos(strike),
664 ExpirationDate::Days(Positive::ONE),
665 pos(dec!(0.2)),
666 pos(quantity),
667 pos(strike),
668 dec!(0.05),
669 style,
670 Positive::ZERO,
671 None,
672 )
673 .with_contract_size(pos(multiplier))
674 }
675
676 fn fixture(side: Side, style: OptionStyle) -> Options {
677 contract(
678 OptionType::European,
679 side,
680 style,
681 STRIKE,
682 QUANTITY,
683 MULTIPLIER,
684 )
685 }
686
687 fn observed(price: Decimal) -> SettlementPrice {
688 SettlementPrice {
689 price: pos(price),
690 source: "test close".to_string(),
691 observed_at: cutoff(),
692 }
693 }
694
695 fn terms(
696 settlement_type: SettlementType,
697 event: SettlementEvent,
698 price: Option<Decimal>,
699 ) -> SettlementTerms {
700 SettlementTerms {
701 settlement_type,
702 event,
703 price: price.map(observed),
704 cutoff: cutoff(),
705 rounding: CashRounding { scale: 2 },
706 }
707 }
708
709 fn itm_price(style: OptionStyle) -> Decimal {
710 match style {
711 OptionStyle::Call => IN_THE_MONEY_CALL,
712 OptionStyle::Put => IN_THE_MONEY_PUT,
713 }
714 }
715
716 fn otm_price(style: OptionStyle) -> Decimal {
717 match style {
718 OptionStyle::Call => IN_THE_MONEY_PUT,
719 OptionStyle::Put => IN_THE_MONEY_CALL,
720 }
721 }
722
723 fn explicit_event(side: Side) -> SettlementEvent {
725 match side {
726 Side::Long => SettlementEvent::Exercise,
727 Side::Short => SettlementEvent::Assignment,
728 }
729 }
730
731 fn obligation(cash: Decimal, underlying: Decimal) -> SettlementObligation {
732 SettlementObligation {
733 extinguished_quantity: pos(QUANTITY),
734 cash,
735 underlying,
736 }
737 }
738
739 fn physical_flows(side: Side, style: OptionStyle) -> SettlementObligation {
741 match (side, style) {
742 (Side::Long, OptionStyle::Call) => obligation(-PHYSICAL_CASH, SIZE),
743 (Side::Short, OptionStyle::Call) => obligation(PHYSICAL_CASH, -SIZE),
744 (Side::Long, OptionStyle::Put) => obligation(PHYSICAL_CASH, -SIZE),
745 (Side::Short, OptionStyle::Put) => obligation(-PHYSICAL_CASH, SIZE),
746 }
747 }
748
749 fn cash_flows(side: Side) -> SettlementObligation {
751 match side {
752 Side::Long => obligation(CASH_AMOUNT, Decimal::ZERO),
753 Side::Short => obligation(-CASH_AMOUNT, Decimal::ZERO),
754 }
755 }
756
757 const SIDES: [Side; 2] = [Side::Long, Side::Short];
758 const STYLES: [OptionStyle; 2] = [OptionStyle::Call, OptionStyle::Put];
759
760 fn each_position(check: impl Fn(Side, OptionStyle)) {
761 for side in SIDES {
762 for style in STYLES {
763 check(side, style);
764 }
765 }
766 }
767
768 fn settle(side: Side, style: OptionStyle, terms: &SettlementTerms) -> SettlementObligation {
769 match settlement_obligation(&fixture(side, style), terms) {
770 Ok(settled) => settled,
771 Err(error) => panic!("{side:?} {style:?}: {error}"),
772 }
773 }
774
775 #[test]
776 fn test_settlement_expiry_cash_itm_pays_intrinsic_value() {
777 each_position(|side, style| {
778 let terms = terms(
779 SettlementType::Cash,
780 SettlementEvent::Expiry,
781 Some(itm_price(style)),
782 );
783 assert_eq!(settle(side, style, &terms), cash_flows(side));
784 });
785 }
786
787 #[test]
788 fn test_settlement_expiry_physical_itm_moves_underlying_and_cash() {
789 each_position(|side, style| {
790 let terms = terms(
791 SettlementType::Physical,
792 SettlementEvent::Expiry,
793 Some(itm_price(style)),
794 );
795 assert_eq!(settle(side, style, &terms), physical_flows(side, style));
796 });
797 }
798
799 #[test]
800 fn test_settlement_expiry_atm_extinguishes_with_no_flows() {
801 each_position(|side, style| {
802 for settlement_type in [SettlementType::Cash, SettlementType::Physical] {
803 let terms = terms(settlement_type, SettlementEvent::Expiry, Some(STRIKE));
804 assert_eq!(
805 settle(side, style, &terms),
806 obligation(Decimal::ZERO, Decimal::ZERO)
807 );
808 }
809 });
810 }
811
812 #[test]
813 fn test_settlement_expiry_otm_extinguishes_with_no_flows() {
814 each_position(|side, style| {
815 for settlement_type in [SettlementType::Cash, SettlementType::Physical] {
816 let terms = terms(
817 settlement_type,
818 SettlementEvent::Expiry,
819 Some(otm_price(style)),
820 );
821 assert_eq!(
822 settle(side, style, &terms),
823 obligation(Decimal::ZERO, Decimal::ZERO)
824 );
825 }
826 });
827 }
828
829 #[test]
830 fn test_settlement_explicit_cash_itm_pays_intrinsic_value() {
831 each_position(|side, style| {
832 let terms = terms(
833 SettlementType::Cash,
834 explicit_event(side),
835 Some(itm_price(style)),
836 );
837 assert_eq!(settle(side, style, &terms), cash_flows(side));
838 });
839 }
840
841 #[test]
842 fn test_settlement_explicit_cash_atm_has_no_cashflow() {
843 each_position(|side, style| {
844 let terms = terms(SettlementType::Cash, explicit_event(side), Some(STRIKE));
845 assert_eq!(
846 settle(side, style, &terms),
847 obligation(Decimal::ZERO, Decimal::ZERO)
848 );
849 });
850 }
851
852 #[test]
853 fn test_settlement_explicit_cash_otm_has_no_cashflow() {
854 each_position(|side, style| {
855 let terms = terms(
856 SettlementType::Cash,
857 explicit_event(side),
858 Some(otm_price(style)),
859 );
860 assert_eq!(
861 settle(side, style, &terms),
862 obligation(Decimal::ZERO, Decimal::ZERO)
863 );
864 });
865 }
866
867 #[test]
868 fn test_settlement_explicit_physical_executes_at_any_moneyness() {
869 each_position(|side, style| {
870 for price in [itm_price(style), STRIKE, otm_price(style)] {
871 let terms = terms(SettlementType::Physical, explicit_event(side), Some(price));
872 assert_eq!(settle(side, style, &terms), physical_flows(side, style));
873 }
874 });
875 }
876
877 #[test]
878 fn test_settlement_explicit_physical_without_price_executes() {
879 each_position(|side, style| {
880 let terms = terms(SettlementType::Physical, explicit_event(side), None);
881 assert_eq!(settle(side, style, &terms), physical_flows(side, style));
882 });
883 }
884
885 #[test]
886 fn test_settlement_multiplier_scales_flows_not_extinguished_quantity() {
887 let option = contract(
888 OptionType::American,
889 Side::Long,
890 OptionStyle::Call,
891 STRIKE,
892 dec!(3),
893 dec!(10),
894 );
895 let physical = terms(
896 SettlementType::Physical,
897 SettlementEvent::Exercise,
898 Some(IN_THE_MONEY_CALL),
899 );
900 assert_eq!(
901 settlement_obligation(&option, &physical),
902 Ok(SettlementObligation {
903 extinguished_quantity: pos(dec!(3)),
904 cash: dec!(-3000),
905 underlying: dec!(30),
906 })
907 );
908 let cash = terms(
909 SettlementType::Cash,
910 SettlementEvent::Exercise,
911 Some(IN_THE_MONEY_CALL),
912 );
913 assert_eq!(
914 settlement_obligation(&option, &cash),
915 Ok(SettlementObligation {
916 extinguished_quantity: pos(dec!(3)),
917 cash: dec!(300),
918 underlying: Decimal::ZERO,
919 })
920 );
921 }
922
923 #[test]
924 fn test_settlement_unit_multiplier_is_the_quantity() {
925 let option = contract(
926 OptionType::European,
927 Side::Short,
928 OptionStyle::Put,
929 STRIKE,
930 dec!(5),
931 Decimal::ONE,
932 );
933 let terms = terms(SettlementType::Physical, SettlementEvent::Assignment, None);
934 assert_eq!(
935 settlement_obligation(&option, &terms),
936 Ok(SettlementObligation {
937 extinguished_quantity: pos(dec!(5)),
938 cash: dec!(-500),
939 underlying: dec!(5),
940 })
941 );
942 }
943
944 #[test]
945 fn test_settlement_cash_rounding_midpoint_goes_to_even() {
946 for (price, expected) in [
947 (dec!(100.125), dec!(0.12)),
948 (dec!(100.135), dec!(0.14)),
949 (dec!(100.126), dec!(0.13)),
950 ] {
951 for (side, sign) in [
952 (Side::Long, Decimal::ONE),
953 (Side::Short, Decimal::NEGATIVE_ONE),
954 ] {
955 let option = contract(
956 OptionType::European,
957 side,
958 OptionStyle::Call,
959 STRIKE,
960 Decimal::ONE,
961 Decimal::ONE,
962 );
963 let terms = terms(SettlementType::Cash, SettlementEvent::Expiry, Some(price));
964 assert_eq!(
965 settlement_obligation(&option, &terms).map(|o| o.cash),
966 Ok(expected * sign)
967 );
968 }
969 }
970 }
971
972 #[test]
973 fn test_settlement_physical_cash_rounds_the_exact_triple_product() {
974 let option = contract(
977 OptionType::European,
978 Side::Long,
979 OptionStyle::Call,
980 dec!(101.015),
981 dec!(1.000000000000001),
982 dec!(0.999999999999999),
983 );
984 let terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
985
986 assert!(matches!(
987 settlement_obligation(&option, &terms),
988 Ok(SettlementObligation { cash, .. }) if cash == dec!(-101.01)
989 ));
990 }
991
992 #[test]
993 fn test_settlement_cash_settlement_rounds_the_exact_triple_product() {
994 let option = contract(
996 OptionType::European,
997 Side::Long,
998 OptionStyle::Call,
999 dec!(100),
1000 dec!(1.000000000000001),
1001 dec!(0.999999999999999),
1002 );
1003 let terms = terms(
1004 SettlementType::Cash,
1005 SettlementEvent::Expiry,
1006 Some(dec!(101.015)),
1007 );
1008
1009 assert!(matches!(
1010 settlement_obligation(&option, &terms),
1011 Ok(SettlementObligation { cash, .. }) if cash == dec!(1.01)
1012 ));
1013 }
1014
1015 #[test]
1016 fn test_settlement_cash_rounds_the_exact_intrinsic_value() {
1017 let option = contract(
1020 OptionType::European,
1021 Side::Long,
1022 OptionStyle::Call,
1023 dec!(0.0000000000000000000000000001),
1024 dec!(1),
1025 dec!(1),
1026 );
1027 let terms = terms(
1028 SettlementType::Cash,
1029 SettlementEvent::Expiry,
1030 Some(dec!(1000000000000.135)),
1031 );
1032
1033 assert!(matches!(
1034 settlement_obligation(&option, &terms),
1035 Ok(SettlementObligation { cash, .. }) if cash == dec!(1000000000000.13)
1036 ));
1037 }
1038
1039 #[test]
1040 fn test_settlement_expiry_keeps_an_exact_intrinsic_value_above_zero() {
1041 let option = contract(
1043 OptionType::European,
1044 Side::Long,
1045 OptionStyle::Call,
1046 dec!(1),
1047 dec!(1),
1048 dec!(1),
1049 );
1050 let terms = terms(
1051 SettlementType::Physical,
1052 SettlementEvent::Expiry,
1053 Some(dec!(1.0000000000000000000000000001)),
1054 );
1055
1056 assert!(matches!(
1057 settlement_obligation(&option, &terms),
1058 Ok(SettlementObligation { cash, underlying, .. })
1059 if cash == dec!(-1) && underlying == dec!(1)
1060 ));
1061 }
1062
1063 #[test]
1064 fn test_settlement_rounding_a_true_tie_still_goes_to_even() {
1065 for (strike, expected) in [
1066 (dec!(101.015), dec!(-101.02)),
1067 (dec!(101.025), dec!(-101.02)),
1068 ] {
1069 let option = contract(
1070 OptionType::European,
1071 Side::Long,
1072 OptionStyle::Call,
1073 strike,
1074 dec!(1),
1075 dec!(1),
1076 );
1077 let terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
1078
1079 assert!(matches!(
1080 settlement_obligation(&option, &terms),
1081 Ok(SettlementObligation { cash, .. }) if cash == expected
1082 ));
1083 }
1084 }
1085
1086 #[test]
1087 fn test_settlement_rounding_scale_above_decimal_precision_keeps_what_fits() {
1088 let option = contract(
1090 OptionType::European,
1091 Side::Long,
1092 OptionStyle::Call,
1093 dec!(1),
1094 dec!(1.000000000000001),
1095 dec!(0.999999999999999),
1096 );
1097 let mut terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
1098 terms.rounding = CashRounding { scale: 40 };
1099
1100 assert!(matches!(
1101 settlement_obligation(&option, &terms),
1102 Ok(SettlementObligation { cash, .. }) if cash == dec!(-1)
1103 ));
1104 }
1105
1106 #[test]
1107 fn test_settlement_large_amount_keeps_the_places_that_fit() {
1108 let option = contract(
1111 OptionType::European,
1112 Side::Long,
1113 OptionStyle::Call,
1114 dec!(8000000000000000000),
1115 dec!(1.0000000001),
1116 dec!(1),
1117 );
1118 let mut terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
1119 terms.rounding = CashRounding { scale: 28 };
1120
1121 assert!(matches!(
1122 settlement_obligation(&option, &terms),
1123 Ok(SettlementObligation { cash, .. }) if cash == dec!(-8000000000800000000)
1124 ));
1125 }
1126
1127 fn exact(value: u128) -> ExactUint {
1130 ExactUint::from_u128(value).unwrap_or(ExactUint::ZERO)
1131 }
1132
1133 #[test]
1134 fn test_exact_uint_arithmetic_matches_u128() {
1135 let left: u128 = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF;
1136 let right: u128 = 0xFFFF_FFFF;
1137 assert_eq!(exact(u128::MAX).to_u128(), Some(u128::MAX));
1138 assert_eq!(
1139 exact(left)
1140 .checked_mul_u128(right)
1141 .and_then(ExactUint::to_u128),
1142 left.checked_mul(right)
1143 );
1144 assert_eq!(
1145 exact(left)
1146 .checked_add(exact(right))
1147 .and_then(ExactUint::to_u128),
1148 left.checked_add(right)
1149 );
1150 assert_eq!(
1151 exact(left)
1152 .checked_sub(exact(right))
1153 .and_then(ExactUint::to_u128),
1154 left.checked_sub(right)
1155 );
1156 assert_eq!(exact(right).checked_sub(exact(left)), None);
1157 assert_eq!(
1158 exact(left)
1159 .divided_by_ten()
1160 .and_then(|(quotient, _)| quotient.to_u128()),
1161 Some(left / 10)
1162 );
1163 assert_eq!(
1164 exact(left)
1165 .divided_by_ten()
1166 .map(|(_, remainder)| u128::from(remainder)),
1167 Some(left % 10)
1168 );
1169 }
1170
1171 #[test]
1172 fn test_exact_uint_holds_the_largest_amount_and_refuses_more() {
1173 let mantissa: u128 = (1 << 96) - 1;
1175 let largest = (0..28)
1176 .try_fold(exact(mantissa), |digits, _| digits.checked_mul_u32(10))
1177 .and_then(|digits| digits.checked_mul_u128(mantissa))
1178 .and_then(|digits| digits.checked_mul_u128(mantissa));
1179 assert!(largest.is_some());
1180 assert_eq!(
1181 largest.and_then(|digits| digits.checked_mul_u128(1 << 96)),
1182 None
1183 );
1184 }
1185
1186 #[test]
1187 fn test_settlement_rounding_applies_to_physical_cash_only() {
1188 let option = contract(
1189 OptionType::European,
1190 Side::Long,
1191 OptionStyle::Call,
1192 dec!(33.335),
1193 dec!(0.125),
1194 dec!(8),
1195 );
1196 let terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
1197 assert_eq!(
1198 settlement_obligation(&option, &terms),
1199 Ok(SettlementObligation {
1200 extinguished_quantity: pos(dec!(0.125)),
1201 cash: dec!(-33.34),
1202 underlying: Decimal::ONE,
1203 })
1204 );
1205 }
1206
1207 #[test]
1208 fn test_settlement_rounding_scale_decides_the_decimal_places() {
1209 let option = contract(
1210 OptionType::European,
1211 Side::Long,
1212 OptionStyle::Call,
1213 STRIKE,
1214 Decimal::ONE,
1215 Decimal::ONE,
1216 );
1217 let mut terms = terms(
1218 SettlementType::Cash,
1219 SettlementEvent::Expiry,
1220 Some(dec!(101.5)),
1221 );
1222 terms.rounding = CashRounding { scale: 0 };
1223 assert_eq!(
1224 settlement_obligation(&option, &terms).map(|o| o.cash),
1225 Ok(dec!(2))
1226 );
1227 terms.rounding = CashRounding { scale: 40 };
1228 assert_eq!(
1229 settlement_obligation(&option, &terms).map(|o| o.cash),
1230 Ok(dec!(1.5))
1231 );
1232 }
1233
1234 #[test]
1235 fn test_settlement_no_flow_is_never_negative_zero() {
1236 let terms = terms(SettlementType::Cash, SettlementEvent::Expiry, Some(STRIKE));
1237 let result = settlement_obligation(&fixture(Side::Short, OptionStyle::Call), &terms);
1238 assert!(matches!(
1239 result,
1240 Ok(SettlementObligation { cash, underlying, .. })
1241 if !cash.is_sign_negative() && !underlying.is_sign_negative()
1242 ));
1243 }
1244
1245 #[test]
1246 fn test_settlement_cash_rounded_to_zero_is_not_negative_zero() {
1247 let option = contract(
1248 OptionType::European,
1249 Side::Short,
1250 OptionStyle::Call,
1251 STRIKE,
1252 Decimal::ONE,
1253 Decimal::ONE,
1254 );
1255 let terms = terms(
1256 SettlementType::Cash,
1257 SettlementEvent::Expiry,
1258 Some(dec!(100.001)),
1259 );
1260 let cash = settlement_obligation(&option, &terms).map(|o| o.cash);
1261 assert_eq!(cash, Ok(Decimal::ZERO));
1262 assert!(matches!(cash, Ok(value) if !value.is_sign_negative()));
1263 }
1264
1265 #[test]
1266 fn test_settlement_zero_quantity_has_no_flows() {
1267 let option = contract(
1268 OptionType::European,
1269 Side::Long,
1270 OptionStyle::Call,
1271 STRIKE,
1272 Decimal::ZERO,
1273 MULTIPLIER,
1274 );
1275 let terms = terms(
1276 SettlementType::Physical,
1277 SettlementEvent::Expiry,
1278 Some(IN_THE_MONEY_CALL),
1279 );
1280 let result = settlement_obligation(&option, &terms);
1281 assert!(matches!(
1282 result,
1283 Ok(SettlementObligation { extinguished_quantity, cash, underlying })
1284 if extinguished_quantity.is_zero() && cash.is_zero() && underlying.is_zero()
1285 ));
1286 }
1287
1288 #[test]
1289 fn test_settlement_american_settles_like_european() {
1290 for settlement_type in [SettlementType::Cash, SettlementType::Physical] {
1291 let terms = terms(
1292 settlement_type,
1293 SettlementEvent::Exercise,
1294 Some(IN_THE_MONEY_CALL),
1295 );
1296 let american = contract(
1297 OptionType::American,
1298 Side::Long,
1299 OptionStyle::Call,
1300 STRIKE,
1301 QUANTITY,
1302 MULTIPLIER,
1303 );
1304 assert_eq!(
1305 settlement_obligation(&american, &terms),
1306 settlement_obligation(&fixture(Side::Long, OptionStyle::Call), &terms)
1307 );
1308 }
1309 }
1310
1311 #[test]
1312 fn test_settlement_unsupported_option_type_is_rejected() {
1313 let terms = terms(
1314 SettlementType::Cash,
1315 SettlementEvent::Expiry,
1316 Some(IN_THE_MONEY_CALL),
1317 );
1318 for option_type in [
1319 OptionType::Bermuda {
1320 exercise_dates: vec![Positive::ONE],
1321 },
1322 OptionType::Asian {
1323 averaging_type: AsianAveragingType::Arithmetic,
1324 },
1325 OptionType::Barrier {
1326 barrier_type: BarrierType::UpAndIn,
1327 barrier_level: pos(dec!(120)),
1328 rebate: None,
1329 },
1330 OptionType::Cliquet {
1331 reset_dates: vec![Positive::ONE],
1332 },
1333 ] {
1334 let option = contract(
1335 option_type.clone(),
1336 Side::Long,
1337 OptionStyle::Call,
1338 STRIKE,
1339 QUANTITY,
1340 MULTIPLIER,
1341 );
1342 assert_eq!(
1343 settlement_obligation(&option, &terms),
1344 Err(SettlementError::UnsupportedOptionType { option_type })
1345 );
1346 }
1347 }
1348
1349 #[test]
1350 fn test_settlement_expiry_without_price_is_missing_price() {
1351 each_position(|side, style| {
1352 for settlement_type in [SettlementType::Cash, SettlementType::Physical] {
1353 let terms = terms(settlement_type, SettlementEvent::Expiry, None);
1354 assert_eq!(
1355 settlement_obligation(&fixture(side, style), &terms),
1356 Err(SettlementError::MissingPrice {
1357 settlement_type,
1358 event: SettlementEvent::Expiry,
1359 })
1360 );
1361 }
1362 });
1363 }
1364
1365 #[test]
1366 fn test_settlement_explicit_cash_without_price_is_missing_price() {
1367 each_position(|side, style| {
1368 let event = explicit_event(side);
1369 let terms = terms(SettlementType::Cash, event, None);
1370 assert_eq!(
1371 settlement_obligation(&fixture(side, style), &terms),
1372 Err(SettlementError::MissingPrice {
1373 settlement_type: SettlementType::Cash,
1374 event,
1375 })
1376 );
1377 });
1378 }
1379
1380 #[test]
1381 fn test_settlement_price_after_cutoff_is_rejected_for_every_event() {
1382 let observed_at = cutoff() + TimeDelta::seconds(1);
1383 each_position(|side, style| {
1384 for event in [SettlementEvent::Expiry, explicit_event(side)] {
1385 for settlement_type in [SettlementType::Cash, SettlementType::Physical] {
1386 let mut terms = terms(settlement_type, event, Some(STRIKE));
1387 if let Some(price) = terms.price.as_mut() {
1388 price.observed_at = observed_at;
1389 }
1390 assert_eq!(
1391 settlement_obligation(&fixture(side, style), &terms),
1392 Err(SettlementError::PriceAfterCutoff {
1393 observed_at,
1394 cutoff: cutoff(),
1395 })
1396 );
1397 }
1398 }
1399 });
1400 }
1401
1402 #[test]
1403 fn test_settlement_price_observed_at_or_before_cutoff_is_accepted() {
1404 for offset in [TimeDelta::zero(), TimeDelta::seconds(-1)] {
1405 let mut terms = terms(
1406 SettlementType::Cash,
1407 SettlementEvent::Expiry,
1408 Some(IN_THE_MONEY_CALL),
1409 );
1410 if let Some(price) = terms.price.as_mut() {
1411 price.observed_at = cutoff() + offset;
1412 }
1413 assert_eq!(
1414 settle(Side::Long, OptionStyle::Call, &terms),
1415 cash_flows(Side::Long)
1416 );
1417 }
1418 }
1419
1420 #[test]
1421 fn test_settlement_exercise_of_short_is_event_side_mismatch() {
1422 for style in STYLES {
1423 let terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
1424 assert_eq!(
1425 settlement_obligation(&fixture(Side::Short, style), &terms),
1426 Err(SettlementError::EventSideMismatch {
1427 event: SettlementEvent::Exercise,
1428 side: Side::Short,
1429 })
1430 );
1431 }
1432 }
1433
1434 #[test]
1435 fn test_settlement_assignment_of_long_is_event_side_mismatch() {
1436 for style in STYLES {
1437 let terms = terms(SettlementType::Physical, SettlementEvent::Assignment, None);
1438 assert_eq!(
1439 settlement_obligation(&fixture(Side::Long, style), &terms),
1440 Err(SettlementError::EventSideMismatch {
1441 event: SettlementEvent::Assignment,
1442 side: Side::Long,
1443 })
1444 );
1445 }
1446 }
1447
1448 fn overflow_operation(
1449 result: Result<SettlementObligation, SettlementError>,
1450 ) -> Option<&'static str> {
1451 match result {
1452 Err(SettlementError::ArithmeticOverflow { operation, .. }) => Some(operation),
1453 _ => None,
1454 }
1455 }
1456
1457 #[test]
1458 fn test_settlement_position_size_overflow_is_an_error() {
1459 let option = contract(
1460 OptionType::European,
1461 Side::Long,
1462 OptionStyle::Call,
1463 STRIKE,
1464 Decimal::MAX,
1465 dec!(2),
1466 );
1467 let terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
1468 assert_eq!(
1469 overflow_operation(settlement_obligation(&option, &terms)),
1470 Some("position size")
1471 );
1472 }
1473
1474 #[test]
1475 fn test_settlement_physical_cash_overflow_is_an_error() {
1476 let option = contract(
1477 OptionType::European,
1478 Side::Long,
1479 OptionStyle::Call,
1480 dec!(10000000000000000000000000000),
1481 dec!(100),
1482 Decimal::ONE,
1483 );
1484 let terms = terms(SettlementType::Physical, SettlementEvent::Exercise, None);
1485 assert_eq!(
1486 overflow_operation(settlement_obligation(&option, &terms)),
1487 Some("physical cash")
1488 );
1489 }
1490
1491 #[test]
1492 fn test_settlement_cash_overflow_is_an_error() {
1493 let option = contract(
1494 OptionType::European,
1495 Side::Long,
1496 OptionStyle::Put,
1497 dec!(10000000000000000000000000000),
1498 dec!(100),
1499 Decimal::ONE,
1500 );
1501 let terms = terms(
1502 SettlementType::Cash,
1503 SettlementEvent::Expiry,
1504 Some(Decimal::ONE),
1505 );
1506 assert_eq!(
1507 overflow_operation(settlement_obligation(&option, &terms)),
1508 Some("cash settlement")
1509 );
1510 }
1511
1512 #[test]
1513 fn test_settlement_types_serialize_as_snake_case() {
1514 assert_eq!(
1515 serde_json::to_string(&SettlementType::Physical)
1516 .ok()
1517 .as_deref(),
1518 Some("\"physical\"")
1519 );
1520 assert_eq!(
1521 serde_json::to_string(&SettlementEvent::Assignment)
1522 .ok()
1523 .as_deref(),
1524 Some("\"assignment\"")
1525 );
1526 }
1527
1528 #[test]
1529 fn test_settlement_terms_round_trip_through_json() {
1530 let terms = terms(
1531 SettlementType::Cash,
1532 SettlementEvent::Expiry,
1533 Some(IN_THE_MONEY_CALL),
1534 );
1535 let json = serde_json::to_string(&terms);
1536 assert!(json.is_ok());
1537 let parsed = json.and_then(|text| serde_json::from_str::<SettlementTerms>(&text));
1538 assert_eq!(parsed.ok(), Some(terms));
1539 }
1540
1541 #[test]
1542 fn test_settlement_terms_reject_unknown_fields() {
1543 let terms = terms(
1544 SettlementType::Cash,
1545 SettlementEvent::Expiry,
1546 Some(IN_THE_MONEY_CALL),
1547 );
1548 assert_rejects_unknown_field(&CashRounding { scale: 2 });
1549 assert_rejects_unknown_field(&observed(IN_THE_MONEY_CALL));
1550 assert_rejects_unknown_field(&terms);
1551 }
1552
1553 fn assert_rejects_unknown_field<T>(value: &T)
1556 where
1557 T: Serialize + DeserializeOwned,
1558 {
1559 let mut json = serde_json::to_value(value).unwrap_or_default();
1560 assert!(serde_json::from_value::<T>(json.clone()).is_ok());
1561 if let Some(fields) = json.as_object_mut() {
1562 fields.insert("unexpected".to_string(), serde_json::Value::Bool(true));
1563 }
1564 let error = serde_json::from_value::<T>(json)
1565 .err()
1566 .map(|e| e.to_string());
1567 assert!(
1568 error
1569 .as_deref()
1570 .is_some_and(|text| text.contains("unknown field")),
1571 "{error:?}"
1572 );
1573 }
1574}