1use crate::error::{DecimalError, PositionError};
46use crate::model::ExpirationDate;
47use crate::model::decimal::{d_add, d_div, d_mul, d_sub};
48use crate::model::expiration::resolve_expiration_date;
49use crate::model::leg::traits::{Expirable, LegAble, Marginable, liquidation_threshold};
50use crate::model::types::Side;
51#[cfg(feature = "clock")]
52use crate::utils::time::wall_clock;
53use crate::utils::time::{evaluation_time, years_to_expiration};
54use chrono::{DateTime, Utc};
55use positive::Positive;
56use rust_decimal::Decimal;
57#[cfg(feature = "clock")]
58use rust_decimal_macros::dec;
59use serde::{Deserialize, Serialize};
60
61#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
81pub struct FuturePosition {
82 pub symbol: String,
84
85 pub quantity: Positive,
87
88 pub entry_price: Positive,
90
91 pub side: Side,
93
94 pub expiration_date: ExpirationDate,
96
97 pub contract_size: Positive,
99
100 pub initial_margin_req: Positive,
102
103 pub maintenance_margin_req: Positive,
105
106 pub date: DateTime<Utc>,
108
109 pub fees: Positive,
111}
112
113impl FuturePosition {
114 #[must_use]
129 #[allow(clippy::too_many_arguments)]
130 pub fn new(
131 symbol: String,
132 quantity: Positive,
133 entry_price: Positive,
134 side: Side,
135 expiration_date: ExpirationDate,
136 contract_size: Positive,
137 initial_margin_req: Positive,
138 maintenance_margin_req: Positive,
139 date: DateTime<Utc>,
140 fees: Positive,
141 ) -> Self {
142 Self {
143 symbol,
144 quantity,
145 entry_price,
146 side,
147 expiration_date,
148 contract_size,
149 initial_margin_req,
150 maintenance_margin_req,
151 date,
152 fees,
153 }
154 }
155
156 #[cfg(feature = "clock")]
167 #[must_use]
168 pub fn long(
169 symbol: String,
170 quantity: Positive,
171 entry_price: Positive,
172 expiration_date: ExpirationDate,
173 contract_size: Positive,
174 margin: Positive,
175 ) -> Self {
176 Self::new(
177 symbol,
178 quantity,
179 entry_price,
180 Side::Long,
181 expiration_date,
182 contract_size,
183 margin,
184 maintenance_from_initial(margin), wall_clock(),
186 Positive::ZERO,
187 )
188 }
189
190 #[cfg(feature = "clock")]
201 #[must_use]
202 pub fn short(
203 symbol: String,
204 quantity: Positive,
205 entry_price: Positive,
206 expiration_date: ExpirationDate,
207 contract_size: Positive,
208 margin: Positive,
209 ) -> Self {
210 Self::new(
211 symbol,
212 quantity,
213 entry_price,
214 Side::Short,
215 expiration_date,
216 contract_size,
217 margin,
218 maintenance_from_initial(margin),
219 wall_clock(),
220 Positive::ZERO,
221 )
222 }
223
224 pub fn notional_value_at_entry(&self) -> Result<Positive, PositionError> {
234 Ok(self
235 .quantity
236 .checked_mul(&self.entry_price)?
237 .checked_mul(&self.contract_size)?)
238 }
239
240 pub fn notional_value_at_price(
251 &self,
252 current_price: Positive,
253 ) -> Result<Positive, PositionError> {
254 Ok(self
255 .quantity
256 .checked_mul(¤t_price)?
257 .checked_mul(&self.contract_size)?)
258 }
259
260 pub fn unrealized_pnl(&self, current_price: Positive) -> Result<Decimal, PositionError> {
279 let price_change = d_sub(
280 current_price.to_dec(),
281 self.entry_price.to_dec(),
282 "FuturePosition::unrealized_pnl/price_change",
283 )?;
284 let pnl = d_mul(
285 d_mul(
286 price_change,
287 self.quantity.to_dec(),
288 "FuturePosition::unrealized_pnl/quantity",
289 )?,
290 self.contract_size.to_dec(),
291 "FuturePosition::unrealized_pnl/contract_size",
292 )?;
293
294 Ok(match self.side {
295 Side::Long => pnl,
296 Side::Short => -pnl,
298 })
299 }
300
301 pub fn tick_value(&self, tick_size: Positive) -> Result<Positive, PositionError> {
312 Ok(tick_size.checked_mul(&self.contract_size)?)
313 }
314
315 pub fn total_margin_required(&self) -> Result<Positive, PositionError> {
322 Ok(self.initial_margin_req.checked_mul(&self.quantity)?)
323 }
324
325 #[must_use]
331 #[expect(
334 clippy::arithmetic_side_effects,
335 reason = "the difference of two values in [0, Decimal::MAX] is representable"
336 )]
337 pub fn basis(&self, spot_price: Positive) -> Decimal {
338 self.entry_price.to_dec() - spot_price.to_dec()
339 }
340
341 pub fn implied_leverage(&self) -> Result<Decimal, PositionError> {
351 let margin = self.total_margin_required()?;
352 if margin == Positive::ZERO {
353 return Ok(Decimal::ZERO);
354 }
355
356 Ok(d_div(
357 self.notional_value_at_entry()?.to_dec(),
358 margin.to_dec(),
359 "FuturePosition::implied_leverage",
360 )?)
361 }
362}
363
364#[cfg(feature = "clock")]
369#[expect(
370 clippy::arithmetic_side_effects,
371 reason = "0.8 times a value in [0, Decimal::MAX] stays in that range"
372)]
373fn maintenance_from_initial(margin: Positive) -> Positive {
374 margin * Decimal::new(8, 1)
375}
376
377impl LegAble for FuturePosition {
378 fn get_symbol(&self) -> &str {
379 &self.symbol
380 }
381
382 fn get_quantity(&self) -> Positive {
383 self.quantity
384 }
385
386 fn get_side(&self) -> Side {
387 self.side
388 }
389
390 fn pnl_at_price(&self, price: Positive) -> Result<Decimal, PositionError> {
391 Ok(d_sub(
392 self.unrealized_pnl(price)?,
393 self.fees.to_dec(),
394 "FuturePosition::pnl_at_price/net",
395 )?)
396 }
397
398 fn total_cost(&self) -> Result<Positive, PositionError> {
399 let margin = self
406 .initial_margin_req
407 .checked_mul(&self.quantity)
408 .map_err(PositionError::from)?;
409 Ok(margin.checked_add(&self.fees)?)
410 }
411
412 fn fees(&self) -> Result<Positive, PositionError> {
413 Ok(self.fees)
414 }
415}
416
417impl Marginable for FuturePosition {
418 fn initial_margin(&self) -> Result<Positive, PositionError> {
419 Ok(self.initial_margin_req.checked_mul(&self.quantity)?)
420 }
421
422 fn maintenance_margin(&self) -> Result<Positive, PositionError> {
423 Ok(self.maintenance_margin_req.checked_mul(&self.quantity)?)
424 }
425
426 fn leverage(&self) -> Result<Positive, PositionError> {
427 let lev = self.implied_leverage()?;
428 Ok(Positive::new_decimal(lev).unwrap_or(positive::Positive::ONE))
429 }
430
431 fn liquidation_price(
443 &self,
444 _current_price: Positive,
445 ) -> Result<Option<Positive>, PositionError> {
446 let margin_buffer = d_sub(
447 self.initial_margin()?.to_dec(),
448 self.maintenance_margin()?.to_dec(),
449 "FuturePosition::liquidation_price/margin_buffer",
450 )?;
451 let units = d_mul(
452 self.quantity.to_dec(),
453 self.contract_size.to_dec(),
454 "FuturePosition::liquidation_price/units",
455 )?;
456 let price_buffer = d_div(
457 margin_buffer,
458 units,
459 "FuturePosition::liquidation_price/price_buffer",
460 )?;
461
462 let threshold = match self.side {
463 Side::Long => d_sub(
464 self.entry_price.to_dec(),
465 price_buffer,
466 "FuturePosition::liquidation_price/long",
467 )?,
468 Side::Short => d_add(
469 self.entry_price.to_dec(),
470 price_buffer,
471 "FuturePosition::liquidation_price/short",
472 )?,
473 };
474 Ok(liquidation_threshold(threshold, self.side))
475 }
476
477 fn is_liquidation_risk(
478 &self,
479 current_price: Positive,
480 _margin_ratio: Decimal,
481 ) -> Result<bool, PositionError> {
482 let Some(liq_price) = self.liquidation_price(current_price)? else {
484 return Ok(false);
485 };
486
487 Ok(match self.side {
488 Side::Long => current_price <= liq_price,
489 Side::Short => current_price >= liq_price,
490 })
491 }
492}
493
494impl Expirable for FuturePosition {
495 fn expiration_timestamp(&self) -> Result<i64, PositionError> {
496 let date = resolve_expiration_date(&self.expiration_date).map_err(DecimalError::from)?;
500 Ok(date.timestamp())
501 }
502
503 fn days_to_expiration(&self) -> Result<Positive, PositionError> {
504 let years = years_to_expiration(&self.expiration_date).map_err(DecimalError::from)?;
505 let days = d_mul(
506 years.to_dec(),
507 Decimal::from(365),
508 "FuturePosition::days_to_expiration",
509 )?;
510 Ok(Positive::new_decimal(days)?)
511 }
512
513 fn is_expired(&self) -> Result<bool, PositionError> {
514 let date = resolve_expiration_date(&self.expiration_date).map_err(DecimalError::from)?;
516 Ok(date < evaluation_time()?)
517 }
518}
519
520impl std::fmt::Display for FuturePosition {
521 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522 write!(
523 f,
524 "{} {} {} @ {} (size: {}, exp: {})",
525 self.side,
526 self.quantity,
527 self.symbol,
528 self.entry_price,
529 self.contract_size,
530 self.expiration_date
531 )
532 }
533}
534
535#[cfg(feature = "clock")]
536impl Default for FuturePosition {
537 fn default() -> Self {
538 Self {
539 symbol: String::new(),
540 quantity: Positive::ZERO,
541 entry_price: Positive::ZERO,
542 side: Side::Long,
543 expiration_date: ExpirationDate::Days(
548 Positive::new_decimal(dec!(30.0)).unwrap_or(Positive::ZERO),
549 ),
550 contract_size: positive::Positive::ONE,
551 initial_margin_req: Positive::ZERO,
552 maintenance_margin_req: Positive::ZERO,
553 date: wall_clock(),
554 fees: Positive::ZERO,
555 }
556 }
557}
558
559#[cfg(all(test, feature = "clock"))]
560mod tests {
561 use super::*;
562 use positive::pos_or_panic;
563
564 #[cfg(feature = "clock")]
569 #[test]
570 fn test_future_total_cost_reports_a_margin_product_that_overflows() {
571 let future = FuturePosition::new(
572 "ES".to_string(),
573 Positive::TWO,
574 pos_or_panic!(4500.0),
575 Side::Long,
576 ExpirationDate::Days(pos_or_panic!(30.0)),
577 pos_or_panic!(50.0),
578 Positive::MAX,
579 pos_or_panic!(12000.0),
580 wall_clock(),
581 pos_or_panic!(5.0),
582 );
583
584 assert!(future.total_cost().is_err());
585 }
586
587 #[cfg(feature = "clock")]
588 #[test]
589 fn test_future_position_new() {
590 let future = FuturePosition::new(
591 "ES".to_string(),
592 Positive::TWO,
593 pos_or_panic!(4500.0),
594 Side::Long,
595 ExpirationDate::Days(pos_or_panic!(30.0)),
596 pos_or_panic!(50.0),
597 pos_or_panic!(15000.0),
598 pos_or_panic!(12000.0),
599 wall_clock(),
600 pos_or_panic!(5.0),
601 );
602
603 assert_eq!(future.symbol, "ES");
604 assert_eq!(future.quantity, Positive::TWO);
605 assert_eq!(future.entry_price, pos_or_panic!(4500.0));
606 assert_eq!(future.side, Side::Long);
607 assert_eq!(future.contract_size, pos_or_panic!(50.0));
608 }
609
610 #[cfg(feature = "clock")]
611 #[test]
612 fn test_future_long_convenience() {
613 let future = FuturePosition::long(
614 "ES".to_string(),
615 Positive::ONE,
616 pos_or_panic!(4500.0),
617 ExpirationDate::Days(pos_or_panic!(30.0)),
618 pos_or_panic!(50.0),
619 pos_or_panic!(15000.0),
620 );
621
622 assert_eq!(future.side, Side::Long);
623 assert_eq!(future.initial_margin_req, pos_or_panic!(15000.0));
624 }
625
626 #[cfg(feature = "clock")]
627 #[test]
628 fn test_future_short_convenience() {
629 let future = FuturePosition::short(
630 "ES".to_string(),
631 Positive::ONE,
632 pos_or_panic!(4500.0),
633 ExpirationDate::Days(pos_or_panic!(30.0)),
634 pos_or_panic!(50.0),
635 pos_or_panic!(15000.0),
636 );
637
638 assert_eq!(future.side, Side::Short);
639 }
640
641 #[cfg(feature = "clock")]
642 #[test]
643 fn test_notional_value() -> Result<(), Box<dyn std::error::Error>> {
644 let future = FuturePosition::long(
645 "ES".to_string(),
646 Positive::TWO,
647 pos_or_panic!(4500.0),
648 ExpirationDate::Days(pos_or_panic!(30.0)),
649 pos_or_panic!(50.0),
650 pos_or_panic!(15000.0),
651 );
652
653 assert_eq!(future.notional_value_at_entry()?, pos_or_panic!(450000.0));
654 Ok(())
655 }
656
657 #[cfg(feature = "clock")]
658 #[test]
659 fn test_unrealized_pnl_long() {
660 let future = FuturePosition::long(
661 "ES".to_string(),
662 Positive::ONE,
663 pos_or_panic!(4500.0),
664 ExpirationDate::Days(pos_or_panic!(30.0)),
665 pos_or_panic!(50.0),
666 pos_or_panic!(15000.0),
667 );
668
669 let pnl = future.unrealized_pnl(pos_or_panic!(4510.0));
670 assert_eq!(pnl.ok(), Some(Decimal::from(500)));
671
672 let pnl_loss = future.unrealized_pnl(pos_or_panic!(4490.0));
673 assert_eq!(pnl_loss.ok(), Some(Decimal::from(-500)));
674 }
675
676 #[cfg(feature = "clock")]
677 #[test]
678 fn test_unrealized_pnl_short() {
679 let future = FuturePosition::short(
680 "ES".to_string(),
681 Positive::ONE,
682 pos_or_panic!(4500.0),
683 ExpirationDate::Days(pos_or_panic!(30.0)),
684 pos_or_panic!(50.0),
685 pos_or_panic!(15000.0),
686 );
687
688 let pnl = future.unrealized_pnl(pos_or_panic!(4490.0));
689 assert_eq!(pnl.ok(), Some(Decimal::from(500)));
690
691 let pnl_loss = future.unrealized_pnl(pos_or_panic!(4510.0));
692 assert_eq!(pnl_loss.ok(), Some(Decimal::from(-500)));
693 }
694
695 #[cfg(feature = "clock")]
696 #[test]
697 fn test_implied_leverage() -> Result<(), Box<dyn std::error::Error>> {
698 let future = FuturePosition::long(
699 "ES".to_string(),
700 Positive::ONE,
701 pos_or_panic!(4500.0),
702 ExpirationDate::Days(pos_or_panic!(30.0)),
703 pos_or_panic!(50.0),
704 pos_or_panic!(15000.0),
705 );
706
707 let leverage = future.implied_leverage()?;
708 assert_eq!(leverage, Decimal::from(15));
709 Ok(())
710 }
711
712 #[cfg(feature = "clock")]
713 #[test]
714 fn test_basis() {
715 let future = FuturePosition::long(
716 "ES".to_string(),
717 Positive::ONE,
718 pos_or_panic!(4510.0),
719 ExpirationDate::Days(pos_or_panic!(30.0)),
720 pos_or_panic!(50.0),
721 pos_or_panic!(15000.0),
722 );
723
724 let basis = future.basis(pos_or_panic!(4500.0));
725 assert_eq!(basis, Decimal::from(10));
726 }
727
728 #[cfg(feature = "clock")]
729 #[test]
730 fn test_tick_value() -> Result<(), Box<dyn std::error::Error>> {
731 let future = FuturePosition::long(
732 "ES".to_string(),
733 Positive::ONE,
734 pos_or_panic!(4500.0),
735 ExpirationDate::Days(pos_or_panic!(30.0)),
736 pos_or_panic!(50.0),
737 pos_or_panic!(15000.0),
738 );
739
740 let tick_val = future.tick_value(pos_or_panic!(0.25))?;
741 assert_eq!(tick_val, pos_or_panic!(12.5));
742 Ok(())
743 }
744
745 #[cfg(feature = "clock")]
746 #[test]
747 fn test_total_margin_required() -> Result<(), Box<dyn std::error::Error>> {
748 let future = FuturePosition::long(
749 "ES".to_string(),
750 Positive::TWO,
751 pos_or_panic!(4500.0),
752 ExpirationDate::Days(pos_or_panic!(30.0)),
753 pos_or_panic!(50.0),
754 pos_or_panic!(15000.0),
755 );
756
757 assert_eq!(future.total_margin_required()?, pos_or_panic!(30000.0));
758 Ok(())
759 }
760
761 #[cfg(feature = "clock")]
762 #[test]
763 fn test_display() {
764 let future = FuturePosition::long(
765 "ES".to_string(),
766 Positive::ONE,
767 pos_or_panic!(4500.0),
768 ExpirationDate::Days(pos_or_panic!(30.0)),
769 pos_or_panic!(50.0),
770 pos_or_panic!(15000.0),
771 );
772
773 let display = format!("{}", future);
774 assert!(display.contains("Long"));
775 assert!(display.contains("ES"));
776 assert!(display.contains("4500"));
777 }
778
779 fn future_with_margins(
780 side: Side,
781 entry_price: Positive,
782 initial_margin_req: Positive,
783 maintenance_margin_req: Positive,
784 ) -> FuturePosition {
785 FuturePosition::new(
786 "ES".to_string(),
787 Positive::ONE,
788 entry_price,
789 side,
790 ExpirationDate::Days(pos_or_panic!(30.0)),
791 Positive::ONE,
792 initial_margin_req,
793 maintenance_margin_req,
794 wall_clock(),
795 Positive::ZERO,
796 )
797 }
798
799 #[cfg(feature = "clock")]
800 #[test]
801 fn test_future_liquidation_price_reachable_is_unchanged()
802 -> Result<(), Box<dyn std::error::Error>> {
803 let long = FuturePosition::long(
804 "ES".to_string(),
805 Positive::ONE,
806 pos_or_panic!(4500.0),
807 ExpirationDate::Days(pos_or_panic!(30.0)),
808 pos_or_panic!(50.0),
809 pos_or_panic!(15000.0),
810 );
811 let short = FuturePosition::short(
812 "ES".to_string(),
813 Positive::ONE,
814 pos_or_panic!(4500.0),
815 ExpirationDate::Days(pos_or_panic!(30.0)),
816 pos_or_panic!(50.0),
817 pos_or_panic!(15000.0),
818 );
819
820 assert_eq!(
822 long.liquidation_price(pos_or_panic!(4500.0))?,
823 Some(pos_or_panic!(4440.0))
824 );
825 assert_eq!(
826 short.liquidation_price(pos_or_panic!(4500.0))?,
827 Some(pos_or_panic!(4560.0))
828 );
829 assert!(long.is_liquidation_risk(pos_or_panic!(4440.0), Decimal::ZERO)?);
830 assert!(!long.is_liquidation_risk(pos_or_panic!(4441.0), Decimal::ZERO)?);
831 assert!(short.is_liquidation_risk(pos_or_panic!(4560.0), Decimal::ZERO)?);
832 assert!(!short.is_liquidation_risk(pos_or_panic!(4559.0), Decimal::ZERO)?);
833 Ok(())
834 }
835
836 #[test]
837 fn test_future_long_liquidation_price_below_zero_is_unreachable()
838 -> Result<(), Box<dyn std::error::Error>> {
839 let long = future_with_margins(
842 Side::Long,
843 pos_or_panic!(10.0),
844 pos_or_panic!(100.0),
845 pos_or_panic!(10.0),
846 );
847
848 assert_eq!(long.liquidation_price(pos_or_panic!(10.0))?, None);
849 assert!(!long.is_liquidation_risk(Positive::ZERO, Decimal::ZERO)?);
850 assert!(!long.is_liquidation_risk(pos_or_panic!(10.0), Decimal::ZERO)?);
851 Ok(())
852 }
853
854 #[test]
855 fn test_future_short_liquidation_price_below_zero_floors_at_zero()
856 -> Result<(), Box<dyn std::error::Error>> {
857 let short = future_with_margins(
860 Side::Short,
861 pos_or_panic!(10.0),
862 pos_or_panic!(10.0),
863 pos_or_panic!(100.0),
864 );
865
866 assert_eq!(
867 short.liquidation_price(pos_or_panic!(10.0))?,
868 Some(Positive::ZERO)
869 );
870 assert!(short.is_liquidation_risk(Positive::ZERO, Decimal::ZERO)?);
871 assert!(short.is_liquidation_risk(pos_or_panic!(10.0), Decimal::ZERO)?);
872 Ok(())
873 }
874
875 #[cfg(feature = "clock")]
876 #[test]
877 fn test_future_days_to_expiration_is_unchanged() {
878 let future = future_with_margins(
879 Side::Long,
880 pos_or_panic!(4500.0),
881 pos_or_panic!(15000.0),
882 pos_or_panic!(12000.0),
883 );
884 let expected = ExpirationDate::Days(pos_or_panic!(30.0))
885 .get_years()
886 .expect("30 days")
887 * Decimal::from(365);
888
889 let days = future.days_to_expiration().expect("30 days");
890 assert_eq!(days, expected);
891 assert_eq!(
892 future.time_to_expiration_years().expect("30 days"),
893 expected.to_dec() / Decimal::from(365)
894 );
895 }
896
897 #[test]
898 fn test_future_days_to_expiration_overflow_is_an_error_not_zero() {
899 let mut future = future_with_margins(
900 Side::Long,
901 pos_or_panic!(4500.0),
902 pos_or_panic!(15000.0),
903 pos_or_panic!(12000.0),
904 );
905 future.expiration_date = ExpirationDate::Days(Positive::MAX);
906
907 let result = future.days_to_expiration();
909 assert!(
910 matches!(
911 result,
912 Err(PositionError::DecimalError(DecimalError::Overflow { .. }))
913 ),
914 "{result:?}"
915 );
916 assert!(future.time_to_expiration_years().is_err());
917 }
918
919 #[cfg(feature = "clock")]
920 #[test]
921 fn test_future_expiration_timestamp_and_is_expired_are_unchanged() {
922 let expiry = Utc::now() + chrono::Duration::days(30);
923 let mut future = future_with_margins(
924 Side::Long,
925 pos_or_panic!(4500.0),
926 pos_or_panic!(15000.0),
927 pos_or_panic!(12000.0),
928 );
929 future.expiration_date = ExpirationDate::DateTime(expiry);
930 assert_eq!(future.expiration_timestamp().ok(), Some(expiry.timestamp()));
931 assert_eq!(future.is_expired().ok(), Some(false));
932
933 let past = Utc::now() - chrono::Duration::days(1);
934 future.expiration_date = ExpirationDate::DateTime(past);
935 assert_eq!(future.expiration_timestamp().ok(), Some(past.timestamp()));
936 assert_eq!(future.is_expired().ok(), Some(true));
937 }
938
939 #[test]
940 fn test_future_unresolvable_expiration_is_an_error_not_epoch_or_live() {
941 let mut future = future_with_margins(
942 Side::Long,
943 pos_or_panic!(4500.0),
944 pos_or_panic!(15000.0),
945 pos_or_panic!(12000.0),
946 );
947 future.expiration_date = ExpirationDate::Days(Positive::MAX);
950
951 let timestamp = future.expiration_timestamp();
952 assert!(
953 matches!(
954 timestamp,
955 Err(PositionError::DecimalError(DecimalError::ExpirationDate(_)))
956 ),
957 "{timestamp:?}"
958 );
959 let expired = future.is_expired();
960 assert!(
961 matches!(
962 expired,
963 Err(PositionError::DecimalError(DecimalError::ExpirationDate(_)))
964 ),
965 "{expired:?}"
966 );
967 }
968
969 fn future_with_quantity(quantity: Positive) -> FuturePosition {
970 FuturePosition::long(
971 "ES".to_string(),
972 quantity,
973 pos_or_panic!(4500.0),
974 ExpirationDate::Days(pos_or_panic!(30.0)),
975 pos_or_panic!(50.0),
976 pos_or_panic!(15000.0),
977 )
978 }
979
980 #[test]
983 fn test_liquidation_price_zero_quantity_is_error() {
984 let future = future_with_quantity(Positive::ZERO);
985 assert!(matches!(
986 future.liquidation_price(pos_or_panic!(4500.0)),
987 Err(PositionError::DecimalError(_))
988 ));
989 assert!(
990 future
991 .is_liquidation_risk(pos_or_panic!(4500.0), Decimal::ZERO)
992 .is_err()
993 );
994 }
995
996 #[test]
999 fn test_notional_and_margins_overflow_is_error() {
1000 let future = future_with_quantity(Positive::MAX);
1001 assert!(future.notional_value_at_entry().is_err());
1002 assert!(future.notional_value_at_price(Positive::TWO).is_err());
1003 assert!(future.total_margin_required().is_err());
1004 assert!(future.implied_leverage().is_err());
1005 assert!(future.initial_margin().is_err());
1006 assert!(future.maintenance_margin().is_err());
1007 assert!(future.leverage().is_err());
1008 let mut wide = future_with_quantity(Positive::ONE);
1009 wide.contract_size = Positive::MAX;
1010 assert!(wide.tick_value(Positive::TWO).is_err());
1011 }
1012}