optionstratlib_simulation/simulation/exit.rs
1//! Exit policies for option trading strategies.
2//!
3//! This module defines various exit conditions and policies that can be used
4//! to determine when to close an option position during simulations or live trading.
5//!
6//! # Exit Policies
7//!
8//! - **Percentage-based**: Exit when profit/loss reaches a percentage threshold
9//! - **Fixed price**: Exit when the option premium reaches a specific price
10//! - **Time-based**: Exit after a certain time period or at expiration
11//! - **Delta-based**: Exit when the option's delta crosses a threshold
12//! - **Underlying price**: Exit when the underlying asset reaches a price level
13//! - **Combined conditions**: Exit when multiple conditions are met (AND/OR logic)
14
15use optionstratlib_core::model::Positive;
16use rust_decimal::Decimal;
17use rust_decimal_macros::dec;
18use serde::{Deserialize, Serialize};
19use std::cmp::Ordering;
20use std::fmt;
21
22/// Defines exit policies for option positions.
23///
24/// Exit policies determine when a position should be closed based on various
25/// market conditions, profit/loss levels, or time constraints.
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
27#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
28pub enum ExitPolicy {
29 /// Exit when profit reaches a percentage of initial premium.
30 ///
31 /// For short options: exit when premium decreases by this percentage.
32 /// For long options: exit when premium increases by this percentage.
33 ///
34 /// # Example
35 /// ```ignore
36 /// // Exit when profit reaches 50% of initial premium
37 /// ExitPolicy::ProfitPercent(dec!(0.5))
38 /// ```
39 ProfitPercent(Decimal),
40
41 /// Exit when loss reaches a percentage of initial premium.
42 ///
43 /// For short options: exit when premium increases by this percentage.
44 /// For long options: exit when premium decreases by this percentage.
45 ///
46 /// # Example
47 /// ```ignore
48 /// // Exit when loss reaches 100% of initial premium (double)
49 /// ExitPolicy::LossPercent(dec!(1.0))
50 /// ```
51 LossPercent(Decimal),
52
53 /// Exit when the option premium reaches a specific price.
54 ///
55 /// # Example
56 /// ```ignore
57 /// // Exit when premium reaches $50
58 /// ExitPolicy::FixedPrice(pos_or_panic!(50.0))
59 /// ```
60 FixedPrice(Positive),
61
62 /// Exit when the option premium falls below a minimum price.
63 ///
64 /// Useful for taking profits on short options when premium decays.
65 ///
66 /// # Example
67 /// ```ignore
68 /// // Exit when premium falls below $5
69 /// ExitPolicy::MinPrice(pos_or_panic!(5.0))
70 /// ```
71 MinPrice(Positive),
72
73 /// Exit when the option premium exceeds a maximum price.
74 ///
75 /// Useful for cutting losses on short options when premium increases.
76 ///
77 /// # Example
78 /// ```ignore
79 /// // Exit when premium exceeds $100
80 /// ExitPolicy::MaxPrice(Positive::HUNDRED)
81 /// ```
82 MaxPrice(Positive),
83
84 /// Exit after a specific number of time steps.
85 ///
86 /// # Example
87 /// ```ignore
88 /// // Exit after 1000 steps (e.g., 1000 minutes)
89 /// ExitPolicy::TimeSteps(1000)
90 /// ```
91 TimeSteps(usize),
92
93 /// Exit when days to expiration falls below a threshold.
94 ///
95 /// # Example
96 /// ```ignore
97 /// // Exit when less than 2 days remain
98 /// ExitPolicy::DaysToExpiration(Positive::TWO)
99 /// ```
100 DaysToExpiration(Positive),
101
102 /// Exit when the option's delta crosses a threshold.
103 ///
104 /// # Example
105 /// ```ignore
106 /// // Exit when delta exceeds 0.5 (deep in the money)
107 /// ExitPolicy::DeltaThreshold(dec!(0.5))
108 /// ```
109 DeltaThreshold(Decimal),
110
111 /// Exit when the underlying price reaches a specific level.
112 ///
113 /// # Example
114 /// ```ignore
115 /// // Exit when underlying reaches $4000
116 /// ExitPolicy::UnderlyingPrice(pos_or_panic!(4000.0))
117 /// ```
118 UnderlyingPrice(Positive),
119
120 /// Exit when the underlying price falls below a level.
121 ///
122 /// # Example
123 /// ```ignore
124 /// // Exit when underlying falls below $3900
125 /// ExitPolicy::UnderlyingBelow(pos_or_panic!(3900.0))
126 /// ```
127 UnderlyingBelow(Positive),
128
129 /// Exit when the underlying price exceeds a level.
130 ///
131 /// # Example
132 /// ```ignore
133 /// // Exit when underlying exceeds $4100
134 /// ExitPolicy::UnderlyingAbove(pos_or_panic!(4100.0))
135 /// ```
136 UnderlyingAbove(Positive),
137
138 /// Exit at expiration (hold until expiration).
139 ///
140 /// # Example
141 /// ```ignore
142 /// ExitPolicy::Expiration
143 /// ```
144 #[default]
145 Expiration,
146
147 /// Exit when ALL conditions are met (AND logic).
148 ///
149 /// # Example
150 /// ```ignore
151 /// // Exit when profit is 50% AND at least 2 days have passed
152 /// ExitPolicy::And(vec![
153 /// ExitPolicy::ProfitPercent(dec!(0.5)),
154 /// ExitPolicy::TimeSteps(2880), // 2 days in minutes
155 /// ])
156 /// ```
157 And(Vec<ExitPolicy>),
158
159 /// Exit when ANY condition is met (OR logic).
160 ///
161 /// # Example
162 /// ```ignore
163 /// // Exit when profit is 50% OR loss is 100%
164 /// ExitPolicy::Or(vec![
165 /// ExitPolicy::ProfitPercent(dec!(0.5)),
166 /// ExitPolicy::LossPercent(dec!(1.0)),
167 /// ])
168 /// ```
169 Or(Vec<ExitPolicy>),
170}
171
172impl ExitPolicy {
173 /// Creates a standard profit target exit policy.
174 ///
175 /// # Arguments
176 ///
177 /// * `percent` - Profit percentage (e.g., 0.5 for 50%)
178 ///
179 /// # Returns
180 ///
181 /// An `ExitPolicy::ProfitPercent` variant.
182 #[must_use]
183 pub fn profit_target(percent: Decimal) -> Self {
184 Self::ProfitPercent(percent)
185 }
186
187 /// Creates a standard stop loss exit policy.
188 ///
189 /// # Arguments
190 ///
191 /// * `percent` - Loss percentage (e.g., 1.0 for 100%)
192 ///
193 /// # Returns
194 ///
195 /// An `ExitPolicy::LossPercent` variant.
196 #[must_use]
197 pub fn stop_loss(percent: Decimal) -> Self {
198 Self::LossPercent(percent)
199 }
200
201 /// Creates a combined profit target and stop loss policy.
202 ///
203 /// # Arguments
204 ///
205 /// * `profit_percent` - Profit percentage threshold
206 /// * `loss_percent` - Loss percentage threshold
207 ///
208 /// # Returns
209 ///
210 /// An `ExitPolicy::Or` variant combining both conditions.
211 #[must_use]
212 pub fn profit_or_loss(profit_percent: Decimal, loss_percent: Decimal) -> Self {
213 Self::Or(vec![
214 Self::ProfitPercent(profit_percent),
215 Self::LossPercent(loss_percent),
216 ])
217 }
218
219 /// Creates a time-limited profit target policy.
220 ///
221 /// Exit when profit target is reached OR time limit is exceeded.
222 ///
223 /// # Arguments
224 ///
225 /// * `profit_percent` - Profit percentage threshold
226 /// * `max_steps` - Maximum number of time steps
227 ///
228 /// # Returns
229 ///
230 /// An `ExitPolicy::Or` variant combining both conditions.
231 #[must_use]
232 pub fn profit_or_time(profit_percent: Decimal, max_steps: usize) -> Self {
233 Self::Or(vec![
234 Self::ProfitPercent(profit_percent),
235 Self::TimeSteps(max_steps),
236 ])
237 }
238
239 /// Checks if this is a composite policy (And/Or).
240 ///
241 /// # Returns
242 ///
243 /// `true` if the policy contains nested policies.
244 #[must_use]
245 pub const fn is_composite(&self) -> bool {
246 matches!(self, Self::And(_) | Self::Or(_))
247 }
248
249 /// Returns the number of conditions in this policy.
250 ///
251 /// For simple policies, returns 1.
252 /// For composite policies, returns the total number of leaf conditions.
253 ///
254 /// # Returns
255 ///
256 /// The count of conditions.
257 #[must_use]
258 pub fn condition_count(&self) -> usize {
259 match self {
260 Self::And(policies) | Self::Or(policies) => {
261 policies.iter().map(Self::condition_count).sum()
262 }
263 _ => 1,
264 }
265 }
266}
267
268/// A fraction rendered as a percentage to one decimal, or the empty string
269/// when scaling it by 100 leaves the representable `Decimal` range.
270///
271/// [`fmt::Display`] has nowhere to report, and it is reached from every log
272/// line, so a threshold with no printable percentage prints as blank rather
273/// than aborting the caller inside a `format!`.
274fn percent_or_blank(fraction: Decimal) -> String {
275 fraction
276 .checked_mul(Decimal::ONE_HUNDRED)
277 .map_or_else(String::new, |value| format!("{value:.1}"))
278}
279
280impl fmt::Display for ExitPolicy {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 match self {
283 Self::ProfitPercent(pct) => {
284 write!(f, "Profit Target: {}%", percent_or_blank(*pct))
285 }
286 Self::LossPercent(pct) => write!(f, "Stop Loss: {}%", percent_or_blank(*pct)),
287 Self::FixedPrice(price) => write!(f, "Fixed Price: ${price}"),
288 Self::MinPrice(price) => write!(f, "Min Price: ${price}"),
289 Self::MaxPrice(price) => write!(f, "Max Price: ${price}"),
290 Self::TimeSteps(steps) => write!(f, "Time Steps: {steps}"),
291 Self::DaysToExpiration(days) => write!(f, "Days to Expiration: {days}"),
292 Self::DeltaThreshold(delta) => write!(f, "Delta Threshold: {delta}"),
293 Self::UnderlyingPrice(price) => write!(f, "Underlying Price: ${price}"),
294 Self::UnderlyingBelow(price) => write!(f, "Underlying Below: ${price}"),
295 Self::UnderlyingAbove(price) => write!(f, "Underlying Above: ${price}"),
296 Self::Expiration => write!(f, "Hold to Expiration"),
297 Self::And(policies) => {
298 write!(f, "AND(")?;
299 for (i, policy) in policies.iter().enumerate() {
300 if i > 0 {
301 write!(f, ", ")?;
302 }
303 write!(f, "{policy}")?;
304 }
305 write!(f, ")")
306 }
307 Self::Or(policies) => {
308 write!(f, "OR(")?;
309 for (i, policy) in policies.iter().enumerate() {
310 if i > 0 {
311 write!(f, ", ")?;
312 }
313 write!(f, "{policy}")?;
314 }
315 write!(f, ")")
316 }
317 }
318 }
319}
320
321/// Orders `current` against the threshold `initial * (1 +/- pct)` without
322/// ever forming an intermediate outside the `Decimal` range.
323///
324/// Both the factor and the product overflow for an extreme `pct`, and neither
325/// has an honest representable stand-in: clamping the threshold would fire the
326/// exit at a price the caller never set. What IS knowable is the SIGN of the
327/// unrepresentable value — the sign of a product is the product of the signs —
328/// and that decides the comparison on its own, because `current` is a
329/// `Decimal` and therefore sits below any threshold past `Decimal::MAX` and
330/// above any threshold past `Decimal::MIN`.
331///
332/// `add` selects `1 + pct` (a threshold above the premium) over `1 - pct`.
333fn cmp_to_scaled_premium(current: Decimal, initial: Decimal, pct: Decimal, add: bool) -> Ordering {
334 // A zero base scales to zero whatever the factor does.
335 if initial.is_zero() {
336 return current.cmp(&Decimal::ZERO);
337 }
338 // The sign of the product, given the sign of a factor we may not be able
339 // to represent.
340 let decide = |factor_positive: bool| {
341 if initial.is_sign_positive() == factor_positive {
342 // Threshold above `Decimal::MAX`: unreachable from above.
343 Ordering::Less
344 } else {
345 // Threshold below `Decimal::MIN`: unreachable from below.
346 Ordering::Greater
347 }
348 };
349 let factor = if add {
350 Decimal::ONE.checked_add(pct)
351 } else {
352 Decimal::ONE.checked_sub(pct)
353 };
354 let Some(factor) = factor else {
355 // `1 + pct` can only leave the range above `Decimal::MAX` (pct > 0);
356 // `1 - pct` above it for a negative `pct` and below `Decimal::MIN`
357 // for a positive one.
358 return decide(if add {
359 pct.is_sign_positive()
360 } else {
361 pct.is_sign_negative()
362 });
363 };
364 match initial.checked_mul(factor) {
365 Some(threshold) => current.cmp(&threshold),
366 None => decide(factor.is_sign_positive()),
367 }
368}
369
370/// Answers `|current - target| < 0.01` without ever forming a difference that
371/// leaves the `Decimal` range.
372///
373/// The distance itself overflows for a `Decimal::MIN` premium against any
374/// positive target, which used to abort inside a `#[must_use]` accessor that
375/// promises an `Option`. No representable stand-in is needed to decide the
376/// question: a distance too large to represent is larger than every
377/// representable one, and therefore larger than a one cent tolerance, so an
378/// overflow answers the comparison on its own. `Decimal` is sign-magnitude
379/// and its range is symmetric, so `abs` on the difference cannot overflow in
380/// turn.
381fn within_fixed_price_tolerance(current: Decimal, target: Decimal) -> bool {
382 current
383 .checked_sub(target)
384 .is_some_and(|distance| distance.abs() < dec!(0.01))
385}
386
387/// Checks if an exit policy is triggered for an option position.
388///
389/// # Parameters
390///
391/// * `policy` - The exit policy to evaluate
392/// * `initial_premium` - The initial premium (paid for long, received for short)
393/// * `current_premium` - The current premium value
394/// * `step_num` - Current step number
395/// * `days_left` - Days remaining to expiration
396/// * `underlying_price` - Current underlying price
397/// * `is_long` - `true` for long positions (bought), `false` for short positions (sold)
398///
399/// # Returns
400///
401/// `Some(policy)` if the policy is triggered, `None` otherwise
402///
403/// # Logic
404///
405/// For **Long positions** (bought options):
406/// - Profit: current_premium > initial_premium (premium increases)
407/// - Loss: current_premium < initial_premium (premium decreases)
408///
409/// For **Short positions** (sold options):
410/// - Profit: current_premium < initial_premium (premium decreases)
411/// - Loss: current_premium > initial_premium (premium increases)
412#[allow(clippy::only_used_in_recursion)]
413#[must_use]
414pub fn check_exit_policy(
415 policy: &ExitPolicy,
416 initial_premium: Decimal,
417 current_premium: Decimal,
418 step_num: usize,
419 days_left: Positive,
420 underlying_price: Positive,
421 is_long: bool,
422) -> Option<ExitPolicy> {
423 match policy {
424 ExitPolicy::ProfitPercent(pct) => {
425 if is_long {
426 // For long: profit when premium increases
427 if cmp_to_scaled_premium(current_premium, initial_premium, *pct, true)
428 != Ordering::Less
429 {
430 Some(ExitPolicy::ProfitPercent(*pct))
431 } else {
432 None
433 }
434 } else {
435 // For short: profit when premium decreases
436 if cmp_to_scaled_premium(current_premium, initial_premium, *pct, false)
437 != Ordering::Greater
438 {
439 Some(ExitPolicy::ProfitPercent(*pct))
440 } else {
441 None
442 }
443 }
444 }
445 ExitPolicy::LossPercent(pct) => {
446 if is_long {
447 // For long: loss when premium decreases
448 if cmp_to_scaled_premium(current_premium, initial_premium, *pct, false)
449 != Ordering::Greater
450 {
451 Some(ExitPolicy::LossPercent(*pct))
452 } else {
453 None
454 }
455 } else {
456 // For short: loss when premium increases
457 if cmp_to_scaled_premium(current_premium, initial_premium, *pct, true)
458 != Ordering::Less
459 {
460 Some(ExitPolicy::LossPercent(*pct))
461 } else {
462 None
463 }
464 }
465 }
466 ExitPolicy::FixedPrice(price) => {
467 if within_fixed_price_tolerance(current_premium, price.to_dec()) {
468 Some(ExitPolicy::FixedPrice(*price))
469 } else {
470 None
471 }
472 }
473 ExitPolicy::MinPrice(price) => {
474 if current_premium <= price.to_dec() {
475 Some(ExitPolicy::MinPrice(*price))
476 } else {
477 None
478 }
479 }
480 ExitPolicy::MaxPrice(price) => {
481 if current_premium >= price.to_dec() {
482 Some(ExitPolicy::MaxPrice(*price))
483 } else {
484 None
485 }
486 }
487 ExitPolicy::TimeSteps(steps) => {
488 if step_num >= *steps {
489 Some(ExitPolicy::TimeSteps(*steps))
490 } else {
491 None
492 }
493 }
494 ExitPolicy::DaysToExpiration(days) => {
495 if days_left <= *days {
496 Some(ExitPolicy::DaysToExpiration(*days))
497 } else {
498 None
499 }
500 }
501 ExitPolicy::UnderlyingPrice(price) => {
502 if within_fixed_price_tolerance(underlying_price.to_dec(), price.to_dec()) {
503 Some(ExitPolicy::UnderlyingPrice(*price))
504 } else {
505 None
506 }
507 }
508 ExitPolicy::UnderlyingBelow(price) => {
509 if underlying_price < *price {
510 Some(ExitPolicy::UnderlyingBelow(*price))
511 } else {
512 None
513 }
514 }
515 ExitPolicy::UnderlyingAbove(price) => {
516 if underlying_price > *price {
517 Some(ExitPolicy::UnderlyingAbove(*price))
518 } else {
519 None
520 }
521 }
522 ExitPolicy::Expiration => None, // Handled separately
523 ExitPolicy::DeltaThreshold(_) => None, // Not implemented in this example
524 ExitPolicy::And(policies) => {
525 let mut triggered = Vec::new();
526 for p in policies {
527 // All must be true for AND; `?` propagates the None.
528 let triggered_policy = check_exit_policy(
529 p,
530 initial_premium,
531 current_premium,
532 step_num,
533 days_left,
534 underlying_price,
535 is_long,
536 )?;
537 triggered.push(triggered_policy);
538 }
539 Some(ExitPolicy::And(triggered))
540 }
541 ExitPolicy::Or(policies) => {
542 for p in policies {
543 if let Some(triggered_policy) = check_exit_policy(
544 p,
545 initial_premium,
546 current_premium,
547 step_num,
548 days_left,
549 underlying_price,
550 is_long,
551 ) {
552 return Some(triggered_policy);
553 }
554 }
555 None
556 }
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use optionstratlib_core::pos_or_panic;
564
565 use rust_decimal_macros::dec;
566
567 #[test]
568 fn test_profit_target_creation() {
569 let policy = ExitPolicy::profit_target(dec!(0.5));
570 assert_eq!(policy, ExitPolicy::ProfitPercent(dec!(0.5)));
571 }
572
573 #[test]
574 fn test_stop_loss_creation() {
575 let policy = ExitPolicy::stop_loss(dec!(1.0));
576 assert_eq!(policy, ExitPolicy::LossPercent(dec!(1.0)));
577 }
578
579 #[test]
580 fn test_profit_or_loss_creation() {
581 let policy = ExitPolicy::profit_or_loss(dec!(0.5), dec!(1.0));
582 match policy {
583 ExitPolicy::Or(policies) => {
584 assert_eq!(policies.len(), 2);
585 assert_eq!(policies[0], ExitPolicy::ProfitPercent(dec!(0.5)));
586 assert_eq!(policies[1], ExitPolicy::LossPercent(dec!(1.0)));
587 }
588 _ => panic!("Expected Or variant"),
589 }
590 }
591
592 #[test]
593 fn test_profit_or_time_creation() {
594 let policy = ExitPolicy::profit_or_time(dec!(0.5), 1000);
595 match policy {
596 ExitPolicy::Or(policies) => {
597 assert_eq!(policies.len(), 2);
598 assert_eq!(policies[0], ExitPolicy::ProfitPercent(dec!(0.5)));
599 assert_eq!(policies[1], ExitPolicy::TimeSteps(1000));
600 }
601 _ => panic!("Expected Or variant"),
602 }
603 }
604
605 #[test]
606 fn test_is_composite() {
607 let simple = ExitPolicy::ProfitPercent(dec!(0.5));
608 assert!(!simple.is_composite());
609
610 let composite = ExitPolicy::Or(vec![
611 ExitPolicy::ProfitPercent(dec!(0.5)),
612 ExitPolicy::LossPercent(dec!(1.0)),
613 ]);
614 assert!(composite.is_composite());
615 }
616
617 #[test]
618 fn test_condition_count() {
619 let simple = ExitPolicy::ProfitPercent(dec!(0.5));
620 assert_eq!(simple.condition_count(), 1);
621
622 let composite = ExitPolicy::Or(vec![
623 ExitPolicy::ProfitPercent(dec!(0.5)),
624 ExitPolicy::LossPercent(dec!(1.0)),
625 ]);
626 assert_eq!(composite.condition_count(), 2);
627
628 let nested = ExitPolicy::And(vec![
629 ExitPolicy::Or(vec![
630 ExitPolicy::ProfitPercent(dec!(0.5)),
631 ExitPolicy::LossPercent(dec!(1.0)),
632 ]),
633 ExitPolicy::TimeSteps(1000),
634 ]);
635 assert_eq!(nested.condition_count(), 3);
636 }
637
638 #[test]
639 fn test_display_profit_percent() {
640 let policy = ExitPolicy::ProfitPercent(dec!(0.5));
641 assert_eq!(format!("{policy}"), "Profit Target: 50.0%");
642 }
643
644 #[test]
645 fn test_display_loss_percent() {
646 let policy = ExitPolicy::LossPercent(dec!(1.0));
647 assert_eq!(format!("{policy}"), "Stop Loss: 100.0%");
648 }
649
650 #[test]
651 fn test_display_fixed_price() {
652 let policy = ExitPolicy::FixedPrice(pos_or_panic!(50.0));
653 assert_eq!(format!("{policy}"), "Fixed Price: $50");
654 }
655
656 #[test]
657 fn test_display_time_steps() {
658 let policy = ExitPolicy::TimeSteps(1000);
659 assert_eq!(format!("{policy}"), "Time Steps: 1000");
660 }
661
662 #[test]
663 fn test_display_expiration() {
664 let policy = ExitPolicy::Expiration;
665 assert_eq!(format!("{policy}"), "Hold to Expiration");
666 }
667
668 #[test]
669 fn test_display_or_composite() {
670 let policy = ExitPolicy::Or(vec![
671 ExitPolicy::ProfitPercent(dec!(0.5)),
672 ExitPolicy::LossPercent(dec!(1.0)),
673 ]);
674 let display = format!("{policy}");
675 assert!(display.contains("OR("));
676 assert!(display.contains("Profit Target: 50.0%"));
677 assert!(display.contains("Stop Loss: 100.0%"));
678 }
679
680 #[test]
681 fn test_display_and_composite() {
682 let policy = ExitPolicy::And(vec![
683 ExitPolicy::ProfitPercent(dec!(0.5)),
684 ExitPolicy::TimeSteps(1000),
685 ]);
686 let display = format!("{policy}");
687 assert!(display.contains("AND("));
688 assert!(display.contains("Profit Target: 50.0%"));
689 assert!(display.contains("Time Steps: 1000"));
690 }
691
692 #[test]
693 fn test_serialization() {
694 let policy = ExitPolicy::profit_or_loss(dec!(0.5), dec!(1.0));
695 let json = serde_json::to_string(&policy).unwrap();
696 assert!(json.contains("Or"));
697 assert!(json.contains("ProfitPercent"));
698 assert!(json.contains("LossPercent"));
699 }
700
701 #[test]
702 fn test_deserialization() {
703 let json = r#"{"ProfitPercent":"0.5"}"#;
704 let policy: ExitPolicy = serde_json::from_str(json).unwrap();
705 assert_eq!(policy, ExitPolicy::ProfitPercent(dec!(0.5)));
706 }
707
708 #[test]
709 fn test_all_variants() {
710 let policies = vec![
711 ExitPolicy::ProfitPercent(dec!(0.5)),
712 ExitPolicy::LossPercent(dec!(1.0)),
713 ExitPolicy::FixedPrice(pos_or_panic!(50.0)),
714 ExitPolicy::MinPrice(pos_or_panic!(5.0)),
715 ExitPolicy::MaxPrice(Positive::HUNDRED),
716 ExitPolicy::TimeSteps(1000),
717 ExitPolicy::DaysToExpiration(Positive::TWO),
718 ExitPolicy::DeltaThreshold(dec!(0.5)),
719 ExitPolicy::UnderlyingPrice(pos_or_panic!(4000.0)),
720 ExitPolicy::UnderlyingBelow(pos_or_panic!(3900.0)),
721 ExitPolicy::UnderlyingAbove(pos_or_panic!(4100.0)),
722 ExitPolicy::Expiration,
723 ];
724
725 for policy in policies {
726 // Ensure all variants can be displayed
727 let _ = format!("{policy}");
728 // Ensure all variants can be cloned
729 let _ = policy.clone();
730 }
731 }
732}