1use crate::error::{DecimalError, PositionError};
44use crate::model::ExpirationDate;
45use crate::model::decimal::{d_add, d_div, d_mul, d_sub};
46use crate::model::expiration::resolve_expiration_date;
47use crate::model::leg::traits::{Expirable, LegAble, Marginable, liquidation_threshold};
48use crate::model::types::Side;
49use chrono::{DateTime, Utc};
50use positive::Positive;
51use rust_decimal::Decimal;
52use rust_decimal_macros::dec;
53use serde::{Deserialize, Serialize};
54
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
74#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
75pub struct FuturePosition {
76 pub symbol: String,
78
79 pub quantity: Positive,
81
82 pub entry_price: Positive,
84
85 pub side: Side,
87
88 pub expiration_date: ExpirationDate,
90
91 pub contract_size: Positive,
93
94 pub initial_margin_req: Positive,
96
97 pub maintenance_margin_req: Positive,
99
100 pub date: DateTime<Utc>,
102
103 pub fees: Positive,
105}
106
107impl FuturePosition {
108 #[must_use]
123 #[allow(clippy::too_many_arguments)]
124 pub fn new(
125 symbol: String,
126 quantity: Positive,
127 entry_price: Positive,
128 side: Side,
129 expiration_date: ExpirationDate,
130 contract_size: Positive,
131 initial_margin_req: Positive,
132 maintenance_margin_req: Positive,
133 date: DateTime<Utc>,
134 fees: Positive,
135 ) -> Self {
136 Self {
137 symbol,
138 quantity,
139 entry_price,
140 side,
141 expiration_date,
142 contract_size,
143 initial_margin_req,
144 maintenance_margin_req,
145 date,
146 fees,
147 }
148 }
149
150 #[must_use]
161 pub fn long(
162 symbol: String,
163 quantity: Positive,
164 entry_price: Positive,
165 expiration_date: ExpirationDate,
166 contract_size: Positive,
167 margin: Positive,
168 ) -> Self {
169 Self::new(
170 symbol,
171 quantity,
172 entry_price,
173 Side::Long,
174 expiration_date,
175 contract_size,
176 margin,
177 maintenance_from_initial(margin), Utc::now(),
179 Positive::ZERO,
180 )
181 }
182
183 #[must_use]
194 pub fn short(
195 symbol: String,
196 quantity: Positive,
197 entry_price: Positive,
198 expiration_date: ExpirationDate,
199 contract_size: Positive,
200 margin: Positive,
201 ) -> Self {
202 Self::new(
203 symbol,
204 quantity,
205 entry_price,
206 Side::Short,
207 expiration_date,
208 contract_size,
209 margin,
210 maintenance_from_initial(margin),
211 Utc::now(),
212 Positive::ZERO,
213 )
214 }
215
216 pub fn notional_value_at_entry(&self) -> Result<Positive, PositionError> {
226 Ok(self
227 .quantity
228 .checked_mul(&self.entry_price)?
229 .checked_mul(&self.contract_size)?)
230 }
231
232 pub fn notional_value_at_price(
243 &self,
244 current_price: Positive,
245 ) -> Result<Positive, PositionError> {
246 Ok(self
247 .quantity
248 .checked_mul(¤t_price)?
249 .checked_mul(&self.contract_size)?)
250 }
251
252 pub fn unrealized_pnl(&self, current_price: Positive) -> Result<Decimal, PositionError> {
271 let price_change = d_sub(
272 current_price.to_dec(),
273 self.entry_price.to_dec(),
274 "FuturePosition::unrealized_pnl/price_change",
275 )?;
276 let pnl = d_mul(
277 d_mul(
278 price_change,
279 self.quantity.to_dec(),
280 "FuturePosition::unrealized_pnl/quantity",
281 )?,
282 self.contract_size.to_dec(),
283 "FuturePosition::unrealized_pnl/contract_size",
284 )?;
285
286 Ok(match self.side {
287 Side::Long => pnl,
288 Side::Short => -pnl,
290 })
291 }
292
293 pub fn tick_value(&self, tick_size: Positive) -> Result<Positive, PositionError> {
304 Ok(tick_size.checked_mul(&self.contract_size)?)
305 }
306
307 pub fn total_margin_required(&self) -> Result<Positive, PositionError> {
314 Ok(self.initial_margin_req.checked_mul(&self.quantity)?)
315 }
316
317 #[must_use]
323 #[expect(
326 clippy::arithmetic_side_effects,
327 reason = "the difference of two values in [0, Decimal::MAX] is representable"
328 )]
329 pub fn basis(&self, spot_price: Positive) -> Decimal {
330 self.entry_price.to_dec() - spot_price.to_dec()
331 }
332
333 pub fn implied_leverage(&self) -> Result<Decimal, PositionError> {
343 let margin = self.total_margin_required()?;
344 if margin == Positive::ZERO {
345 return Ok(Decimal::ZERO);
346 }
347
348 Ok(d_div(
349 self.notional_value_at_entry()?.to_dec(),
350 margin.to_dec(),
351 "FuturePosition::implied_leverage",
352 )?)
353 }
354}
355
356#[expect(
361 clippy::arithmetic_side_effects,
362 reason = "0.8 times a value in [0, Decimal::MAX] stays in that range"
363)]
364fn maintenance_from_initial(margin: Positive) -> Positive {
365 margin * Decimal::new(8, 1)
366}
367
368impl LegAble for FuturePosition {
369 fn get_symbol(&self) -> &str {
370 &self.symbol
371 }
372
373 fn get_quantity(&self) -> Positive {
374 self.quantity
375 }
376
377 fn get_side(&self) -> Side {
378 self.side
379 }
380
381 fn pnl_at_price(&self, price: Positive) -> Result<Decimal, PositionError> {
382 Ok(d_sub(
383 self.unrealized_pnl(price)?,
384 self.fees.to_dec(),
385 "FuturePosition::pnl_at_price/net",
386 )?)
387 }
388
389 fn total_cost(&self) -> Result<Positive, PositionError> {
390 let margin = self
397 .initial_margin_req
398 .checked_mul(&self.quantity)
399 .map_err(PositionError::from)?;
400 Ok(margin.checked_add(&self.fees)?)
401 }
402
403 fn fees(&self) -> Result<Positive, PositionError> {
404 Ok(self.fees)
405 }
406}
407
408impl Marginable for FuturePosition {
409 fn initial_margin(&self) -> Result<Positive, PositionError> {
410 Ok(self.initial_margin_req.checked_mul(&self.quantity)?)
411 }
412
413 fn maintenance_margin(&self) -> Result<Positive, PositionError> {
414 Ok(self.maintenance_margin_req.checked_mul(&self.quantity)?)
415 }
416
417 fn leverage(&self) -> Result<Positive, PositionError> {
418 let lev = self.implied_leverage()?;
419 Ok(Positive::new_decimal(lev).unwrap_or(positive::Positive::ONE))
420 }
421
422 fn liquidation_price(
434 &self,
435 _current_price: Positive,
436 ) -> Result<Option<Positive>, PositionError> {
437 let margin_buffer = d_sub(
438 self.initial_margin()?.to_dec(),
439 self.maintenance_margin()?.to_dec(),
440 "FuturePosition::liquidation_price/margin_buffer",
441 )?;
442 let units = d_mul(
443 self.quantity.to_dec(),
444 self.contract_size.to_dec(),
445 "FuturePosition::liquidation_price/units",
446 )?;
447 let price_buffer = d_div(
448 margin_buffer,
449 units,
450 "FuturePosition::liquidation_price/price_buffer",
451 )?;
452
453 let threshold = match self.side {
454 Side::Long => d_sub(
455 self.entry_price.to_dec(),
456 price_buffer,
457 "FuturePosition::liquidation_price/long",
458 )?,
459 Side::Short => d_add(
460 self.entry_price.to_dec(),
461 price_buffer,
462 "FuturePosition::liquidation_price/short",
463 )?,
464 };
465 Ok(liquidation_threshold(threshold, self.side))
466 }
467
468 fn is_liquidation_risk(
469 &self,
470 current_price: Positive,
471 _margin_ratio: Decimal,
472 ) -> Result<bool, PositionError> {
473 let Some(liq_price) = self.liquidation_price(current_price)? else {
475 return Ok(false);
476 };
477
478 Ok(match self.side {
479 Side::Long => current_price <= liq_price,
480 Side::Short => current_price >= liq_price,
481 })
482 }
483}
484
485impl Expirable for FuturePosition {
486 fn expiration_timestamp(&self) -> Result<i64, PositionError> {
487 let date = resolve_expiration_date(&self.expiration_date).map_err(DecimalError::from)?;
491 Ok(date.timestamp())
492 }
493
494 fn days_to_expiration(&self) -> Result<Positive, PositionError> {
495 let years = self
496 .expiration_date
497 .get_years()
498 .map_err(DecimalError::from)?;
499 let days = d_mul(
500 years.to_dec(),
501 Decimal::from(365),
502 "FuturePosition::days_to_expiration",
503 )?;
504 Ok(Positive::new_decimal(days)?)
505 }
506
507 fn is_expired(&self) -> Result<bool, PositionError> {
508 let date = resolve_expiration_date(&self.expiration_date).map_err(DecimalError::from)?;
510 Ok(date < Utc::now())
511 }
512}
513
514impl std::fmt::Display for FuturePosition {
515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516 write!(
517 f,
518 "{} {} {} @ {} (size: {}, exp: {})",
519 self.side,
520 self.quantity,
521 self.symbol,
522 self.entry_price,
523 self.contract_size,
524 self.expiration_date
525 )
526 }
527}
528
529impl Default for FuturePosition {
530 fn default() -> Self {
531 Self {
532 symbol: String::new(),
533 quantity: Positive::ZERO,
534 entry_price: Positive::ZERO,
535 side: Side::Long,
536 expiration_date: ExpirationDate::Days(
541 Positive::new_decimal(dec!(30.0)).unwrap_or(Positive::ZERO),
542 ),
543 contract_size: positive::Positive::ONE,
544 initial_margin_req: Positive::ZERO,
545 maintenance_margin_req: Positive::ZERO,
546 date: Utc::now(),
547 fees: Positive::ZERO,
548 }
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use positive::pos_or_panic;
556
557 #[test]
562 fn test_future_total_cost_reports_a_margin_product_that_overflows() {
563 let future = FuturePosition::new(
564 "ES".to_string(),
565 Positive::TWO,
566 pos_or_panic!(4500.0),
567 Side::Long,
568 ExpirationDate::Days(pos_or_panic!(30.0)),
569 pos_or_panic!(50.0),
570 Positive::MAX,
571 pos_or_panic!(12000.0),
572 Utc::now(),
573 pos_or_panic!(5.0),
574 );
575
576 assert!(future.total_cost().is_err());
577 }
578
579 #[test]
580 fn test_future_position_new() {
581 let future = FuturePosition::new(
582 "ES".to_string(),
583 Positive::TWO,
584 pos_or_panic!(4500.0),
585 Side::Long,
586 ExpirationDate::Days(pos_or_panic!(30.0)),
587 pos_or_panic!(50.0),
588 pos_or_panic!(15000.0),
589 pos_or_panic!(12000.0),
590 Utc::now(),
591 pos_or_panic!(5.0),
592 );
593
594 assert_eq!(future.symbol, "ES");
595 assert_eq!(future.quantity, Positive::TWO);
596 assert_eq!(future.entry_price, pos_or_panic!(4500.0));
597 assert_eq!(future.side, Side::Long);
598 assert_eq!(future.contract_size, pos_or_panic!(50.0));
599 }
600
601 #[test]
602 fn test_future_long_convenience() {
603 let future = FuturePosition::long(
604 "ES".to_string(),
605 Positive::ONE,
606 pos_or_panic!(4500.0),
607 ExpirationDate::Days(pos_or_panic!(30.0)),
608 pos_or_panic!(50.0),
609 pos_or_panic!(15000.0),
610 );
611
612 assert_eq!(future.side, Side::Long);
613 assert_eq!(future.initial_margin_req, pos_or_panic!(15000.0));
614 }
615
616 #[test]
617 fn test_future_short_convenience() {
618 let future = FuturePosition::short(
619 "ES".to_string(),
620 Positive::ONE,
621 pos_or_panic!(4500.0),
622 ExpirationDate::Days(pos_or_panic!(30.0)),
623 pos_or_panic!(50.0),
624 pos_or_panic!(15000.0),
625 );
626
627 assert_eq!(future.side, Side::Short);
628 }
629
630 #[test]
631 fn test_notional_value() -> Result<(), Box<dyn std::error::Error>> {
632 let future = FuturePosition::long(
633 "ES".to_string(),
634 Positive::TWO,
635 pos_or_panic!(4500.0),
636 ExpirationDate::Days(pos_or_panic!(30.0)),
637 pos_or_panic!(50.0),
638 pos_or_panic!(15000.0),
639 );
640
641 assert_eq!(future.notional_value_at_entry()?, pos_or_panic!(450000.0));
642 Ok(())
643 }
644
645 #[test]
646 fn test_unrealized_pnl_long() {
647 let future = FuturePosition::long(
648 "ES".to_string(),
649 Positive::ONE,
650 pos_or_panic!(4500.0),
651 ExpirationDate::Days(pos_or_panic!(30.0)),
652 pos_or_panic!(50.0),
653 pos_or_panic!(15000.0),
654 );
655
656 let pnl = future.unrealized_pnl(pos_or_panic!(4510.0));
657 assert_eq!(pnl.ok(), Some(Decimal::from(500)));
658
659 let pnl_loss = future.unrealized_pnl(pos_or_panic!(4490.0));
660 assert_eq!(pnl_loss.ok(), Some(Decimal::from(-500)));
661 }
662
663 #[test]
664 fn test_unrealized_pnl_short() {
665 let future = FuturePosition::short(
666 "ES".to_string(),
667 Positive::ONE,
668 pos_or_panic!(4500.0),
669 ExpirationDate::Days(pos_or_panic!(30.0)),
670 pos_or_panic!(50.0),
671 pos_or_panic!(15000.0),
672 );
673
674 let pnl = future.unrealized_pnl(pos_or_panic!(4490.0));
675 assert_eq!(pnl.ok(), Some(Decimal::from(500)));
676
677 let pnl_loss = future.unrealized_pnl(pos_or_panic!(4510.0));
678 assert_eq!(pnl_loss.ok(), Some(Decimal::from(-500)));
679 }
680
681 #[test]
682 fn test_implied_leverage() -> Result<(), Box<dyn std::error::Error>> {
683 let future = FuturePosition::long(
684 "ES".to_string(),
685 Positive::ONE,
686 pos_or_panic!(4500.0),
687 ExpirationDate::Days(pos_or_panic!(30.0)),
688 pos_or_panic!(50.0),
689 pos_or_panic!(15000.0),
690 );
691
692 let leverage = future.implied_leverage()?;
693 assert_eq!(leverage, Decimal::from(15));
694 Ok(())
695 }
696
697 #[test]
698 fn test_basis() {
699 let future = FuturePosition::long(
700 "ES".to_string(),
701 Positive::ONE,
702 pos_or_panic!(4510.0),
703 ExpirationDate::Days(pos_or_panic!(30.0)),
704 pos_or_panic!(50.0),
705 pos_or_panic!(15000.0),
706 );
707
708 let basis = future.basis(pos_or_panic!(4500.0));
709 assert_eq!(basis, Decimal::from(10));
710 }
711
712 #[test]
713 fn test_tick_value() -> Result<(), Box<dyn std::error::Error>> {
714 let future = FuturePosition::long(
715 "ES".to_string(),
716 Positive::ONE,
717 pos_or_panic!(4500.0),
718 ExpirationDate::Days(pos_or_panic!(30.0)),
719 pos_or_panic!(50.0),
720 pos_or_panic!(15000.0),
721 );
722
723 let tick_val = future.tick_value(pos_or_panic!(0.25))?;
724 assert_eq!(tick_val, pos_or_panic!(12.5));
725 Ok(())
726 }
727
728 #[test]
729 fn test_total_margin_required() -> Result<(), Box<dyn std::error::Error>> {
730 let future = FuturePosition::long(
731 "ES".to_string(),
732 Positive::TWO,
733 pos_or_panic!(4500.0),
734 ExpirationDate::Days(pos_or_panic!(30.0)),
735 pos_or_panic!(50.0),
736 pos_or_panic!(15000.0),
737 );
738
739 assert_eq!(future.total_margin_required()?, pos_or_panic!(30000.0));
740 Ok(())
741 }
742
743 #[test]
744 fn test_display() {
745 let future = FuturePosition::long(
746 "ES".to_string(),
747 Positive::ONE,
748 pos_or_panic!(4500.0),
749 ExpirationDate::Days(pos_or_panic!(30.0)),
750 pos_or_panic!(50.0),
751 pos_or_panic!(15000.0),
752 );
753
754 let display = format!("{}", future);
755 assert!(display.contains("Long"));
756 assert!(display.contains("ES"));
757 assert!(display.contains("4500"));
758 }
759
760 fn future_with_margins(
761 side: Side,
762 entry_price: Positive,
763 initial_margin_req: Positive,
764 maintenance_margin_req: Positive,
765 ) -> FuturePosition {
766 FuturePosition::new(
767 "ES".to_string(),
768 Positive::ONE,
769 entry_price,
770 side,
771 ExpirationDate::Days(pos_or_panic!(30.0)),
772 Positive::ONE,
773 initial_margin_req,
774 maintenance_margin_req,
775 Utc::now(),
776 Positive::ZERO,
777 )
778 }
779
780 #[test]
781 fn test_future_liquidation_price_reachable_is_unchanged()
782 -> Result<(), Box<dyn std::error::Error>> {
783 let long = FuturePosition::long(
784 "ES".to_string(),
785 Positive::ONE,
786 pos_or_panic!(4500.0),
787 ExpirationDate::Days(pos_or_panic!(30.0)),
788 pos_or_panic!(50.0),
789 pos_or_panic!(15000.0),
790 );
791 let short = FuturePosition::short(
792 "ES".to_string(),
793 Positive::ONE,
794 pos_or_panic!(4500.0),
795 ExpirationDate::Days(pos_or_panic!(30.0)),
796 pos_or_panic!(50.0),
797 pos_or_panic!(15000.0),
798 );
799
800 assert_eq!(
802 long.liquidation_price(pos_or_panic!(4500.0))?,
803 Some(pos_or_panic!(4440.0))
804 );
805 assert_eq!(
806 short.liquidation_price(pos_or_panic!(4500.0))?,
807 Some(pos_or_panic!(4560.0))
808 );
809 assert!(long.is_liquidation_risk(pos_or_panic!(4440.0), Decimal::ZERO)?);
810 assert!(!long.is_liquidation_risk(pos_or_panic!(4441.0), Decimal::ZERO)?);
811 assert!(short.is_liquidation_risk(pos_or_panic!(4560.0), Decimal::ZERO)?);
812 assert!(!short.is_liquidation_risk(pos_or_panic!(4559.0), Decimal::ZERO)?);
813 Ok(())
814 }
815
816 #[test]
817 fn test_future_long_liquidation_price_below_zero_is_unreachable()
818 -> Result<(), Box<dyn std::error::Error>> {
819 let long = future_with_margins(
822 Side::Long,
823 pos_or_panic!(10.0),
824 pos_or_panic!(100.0),
825 pos_or_panic!(10.0),
826 );
827
828 assert_eq!(long.liquidation_price(pos_or_panic!(10.0))?, None);
829 assert!(!long.is_liquidation_risk(Positive::ZERO, Decimal::ZERO)?);
830 assert!(!long.is_liquidation_risk(pos_or_panic!(10.0), Decimal::ZERO)?);
831 Ok(())
832 }
833
834 #[test]
835 fn test_future_short_liquidation_price_below_zero_floors_at_zero()
836 -> Result<(), Box<dyn std::error::Error>> {
837 let short = future_with_margins(
840 Side::Short,
841 pos_or_panic!(10.0),
842 pos_or_panic!(10.0),
843 pos_or_panic!(100.0),
844 );
845
846 assert_eq!(
847 short.liquidation_price(pos_or_panic!(10.0))?,
848 Some(Positive::ZERO)
849 );
850 assert!(short.is_liquidation_risk(Positive::ZERO, Decimal::ZERO)?);
851 assert!(short.is_liquidation_risk(pos_or_panic!(10.0), Decimal::ZERO)?);
852 Ok(())
853 }
854
855 #[test]
856 fn test_future_days_to_expiration_is_unchanged() {
857 let future = future_with_margins(
858 Side::Long,
859 pos_or_panic!(4500.0),
860 pos_or_panic!(15000.0),
861 pos_or_panic!(12000.0),
862 );
863 let expected = ExpirationDate::Days(pos_or_panic!(30.0))
864 .get_years()
865 .expect("30 days")
866 * Decimal::from(365);
867
868 let days = future.days_to_expiration().expect("30 days");
869 assert_eq!(days, expected);
870 assert_eq!(
871 future.time_to_expiration_years().expect("30 days"),
872 expected.to_dec() / Decimal::from(365)
873 );
874 }
875
876 #[test]
877 fn test_future_days_to_expiration_overflow_is_an_error_not_zero() {
878 let mut 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 future.expiration_date = ExpirationDate::Days(Positive::MAX);
885
886 let result = future.days_to_expiration();
888 assert!(
889 matches!(
890 result,
891 Err(PositionError::DecimalError(DecimalError::Overflow { .. }))
892 ),
893 "{result:?}"
894 );
895 assert!(future.time_to_expiration_years().is_err());
896 }
897
898 #[test]
899 fn test_future_expiration_timestamp_and_is_expired_are_unchanged() {
900 let expiry = Utc::now() + chrono::Duration::days(30);
901 let mut future = future_with_margins(
902 Side::Long,
903 pos_or_panic!(4500.0),
904 pos_or_panic!(15000.0),
905 pos_or_panic!(12000.0),
906 );
907 future.expiration_date = ExpirationDate::DateTime(expiry);
908 assert_eq!(future.expiration_timestamp().ok(), Some(expiry.timestamp()));
909 assert_eq!(future.is_expired().ok(), Some(false));
910
911 let past = Utc::now() - chrono::Duration::days(1);
912 future.expiration_date = ExpirationDate::DateTime(past);
913 assert_eq!(future.expiration_timestamp().ok(), Some(past.timestamp()));
914 assert_eq!(future.is_expired().ok(), Some(true));
915 }
916
917 #[test]
918 fn test_future_unresolvable_expiration_is_an_error_not_epoch_or_live() {
919 let mut future = future_with_margins(
920 Side::Long,
921 pos_or_panic!(4500.0),
922 pos_or_panic!(15000.0),
923 pos_or_panic!(12000.0),
924 );
925 future.expiration_date = ExpirationDate::Days(Positive::MAX);
928
929 let timestamp = future.expiration_timestamp();
930 assert!(
931 matches!(
932 timestamp,
933 Err(PositionError::DecimalError(DecimalError::ExpirationDate(_)))
934 ),
935 "{timestamp:?}"
936 );
937 let expired = future.is_expired();
938 assert!(
939 matches!(
940 expired,
941 Err(PositionError::DecimalError(DecimalError::ExpirationDate(_)))
942 ),
943 "{expired:?}"
944 );
945 }
946
947 fn future_with_quantity(quantity: Positive) -> FuturePosition {
948 FuturePosition::long(
949 "ES".to_string(),
950 quantity,
951 pos_or_panic!(4500.0),
952 ExpirationDate::Days(pos_or_panic!(30.0)),
953 pos_or_panic!(50.0),
954 pos_or_panic!(15000.0),
955 )
956 }
957
958 #[test]
961 fn test_liquidation_price_zero_quantity_is_error() {
962 let future = future_with_quantity(Positive::ZERO);
963 assert!(matches!(
964 future.liquidation_price(pos_or_panic!(4500.0)),
965 Err(PositionError::DecimalError(_))
966 ));
967 assert!(
968 future
969 .is_liquidation_risk(pos_or_panic!(4500.0), Decimal::ZERO)
970 .is_err()
971 );
972 }
973
974 #[test]
977 fn test_notional_and_margins_overflow_is_error() {
978 let future = future_with_quantity(Positive::MAX);
979 assert!(future.notional_value_at_entry().is_err());
980 assert!(future.notional_value_at_price(Positive::TWO).is_err());
981 assert!(future.total_margin_required().is_err());
982 assert!(future.implied_leverage().is_err());
983 assert!(future.initial_margin().is_err());
984 assert!(future.maintenance_margin().is_err());
985 assert!(future.leverage().is_err());
986 let mut wide = future_with_quantity(Positive::ONE);
987 wide.contract_size = Positive::MAX;
988 assert!(wide.tick_value(Positive::TWO).is_err());
989 }
990}