Skip to main content

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}