Skip to main content

optionstratlib/strategies/
base.rs

1// Scoped allow: bulk migration of unchecked `[]` indexing to
2// `.get().ok_or_else(..)` tracked as follow-ups to #341.
3#![allow(clippy::indexing_slicing)]
4
5use crate::chains::OptionData;
6use crate::constants::{STRIKE_PRICE_LOWER_BOUND_MULTIPLIER, STRIKE_PRICE_UPPER_BOUND_MULTIPLIER};
7use crate::error::strategies::BreakEvenErrorKind;
8use crate::{
9    ExpirationDate, Options,
10    chains::{StrategyLegs, chain::OptionChain, utils::OptionDataGroup},
11    error::{OperationErrorKind, position::PositionError, strategies::StrategyError},
12    greeks::Greeks,
13    model::{
14        Trade,
15        position::Position,
16        types::{Action, OptionBasicType, OptionStyle, OptionType, Side},
17    },
18    pnl::PnLCalculator,
19    pricing::payoff::Profit,
20    strategies::{
21        StrategyConstructor,
22        delta_neutral::DeltaNeutrality,
23        probabilities::core::ProbabilityAnalysis,
24        utils::{FindOptimalSide, OptimizationCriteria, calculate_price_range},
25    },
26    visualization::Graph,
27};
28use positive::Positive;
29use rust_decimal::Decimal;
30use serde::{Deserialize, Serialize};
31use std::collections::{HashMap, HashSet};
32use std::fmt;
33use std::str::FromStr;
34use tracing::{error, instrument, warn};
35use utoipa::ToSchema;
36
37/// Represents basic information about a trading strategy.
38///
39/// This struct is used to store the name, type, and description of a strategy.
40#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
41pub struct StrategyBasics {
42    /// The name of the strategy.
43    pub name: String,
44    /// The type of the strategy.  See the [`StrategyType`] enum for possible values.
45    pub kind: StrategyType,
46    /// A description of the strategy.
47    pub description: String,
48}
49
50/// This trait defines common functionalities for all trading strategies.
51/// It combines several other traits, requiring implementations for methods related to strategy
52/// information, construction, optimization, profit calculation, graphing, probability analysis,
53/// Greeks calculation, delta neutrality, and P&L calculation.
54pub trait Strategable:
55    Strategies
56    + StrategyConstructor
57    + Profit
58    + Graph
59    + ProbabilityAnalysis
60    + Greeks
61    + DeltaNeutrality
62    + PnLCalculator
63{
64    /// Returns basic information about the strategy, such as its name, type, and description.
65    ///
66    /// This method returns an error by default, as it is expected to be implemented by specific
67    /// strategy types.
68    /// The error indicates that the `info` operation is not supported for the given strategy type.
69    ///
70    /// # Returns
71    ///
72    /// A `Result` containing the `StrategyBasics` struct if successful, or a `StrategyError`
73    /// if the operation is not supported.
74    ///
75    /// # Errors
76    ///
77    /// The default implementation returns [`StrategyError::OperationError`]
78    /// with [`OperationErrorKind::NotSupported`]; concrete strategies that
79    /// override it may surface [`StrategyError::PriceError`] or
80    /// [`StrategyError::BreakEvenError`] when the underlying computations
81    /// fail.
82    fn info(&self) -> Result<StrategyBasics, StrategyError> {
83        Err(StrategyError::operation_not_supported(
84            "info",
85            std::any::type_name::<Self>(),
86        ))
87    }
88
89    /// Returns the type of the strategy.
90    ///
91    /// This method attempts to retrieve the strategy type from the `info()` method.
92    /// If `info()` returns an error (indicating it's not implemented for the specific strategy),
93    /// the default falls back to `StrategyType::Custom` and emits a warning.
94    ///
95    /// # Returns
96    ///
97    /// The `StrategyType` of the strategy, or `StrategyType::Custom` on lookup
98    /// failure.
99    fn type_name(&self) -> StrategyType {
100        match self.info() {
101            Ok(info) => info.kind,
102            Err(e) => {
103                tracing::warn!(
104                    error = %e,
105                    "type_name: info() failed; defaulting to StrategyType::Custom"
106                );
107                StrategyType::Custom
108            }
109        }
110    }
111
112    /// Returns the name of the strategy.
113    ///
114    /// This method attempts to retrieve the strategy name from the `info()` method.
115    /// If `info()` returns an error (indicating it's not implemented for the specific strategy),
116    /// the default falls back to `"Unknown"` and emits a warning.
117    ///
118    /// # Returns
119    ///
120    /// The name of the strategy as a `String`, or `"Unknown"` on lookup failure.
121    fn name(&self) -> String {
122        match self.info() {
123            Ok(info) => info.name,
124            Err(e) => {
125                tracing::warn!(
126                    error = %e,
127                    "name: info() failed; defaulting to \"Unknown\""
128                );
129                String::from("Unknown")
130            }
131        }
132    }
133}
134
135/// Represents different option trading strategies.
136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
137pub enum StrategyType {
138    /// Bull Call Spread strategy.
139    BullCallSpread,
140    /// Bear Call Spread strategy.
141    BearCallSpread,
142    /// Bull Put Spread strategy.
143    BullPutSpread,
144    /// Bear Put Spread strategy.
145    BearPutSpread,
146    /// Long Butterfly Spread strategy.
147    LongButterflySpread,
148    /// Short Butterfly Spread strategy.
149    ShortButterflySpread,
150    /// Iron Condor strategy.
151    IronCondor,
152    /// Iron Butterfly strategy.
153    IronButterfly,
154    /// Long Straddle strategy.
155    LongStraddle,
156    /// Short Straddle strategy.
157    ShortStraddle,
158    /// Long Strangle strategy.
159    LongStrangle,
160    /// Short Strangle strategy.
161    ShortStrangle,
162    /// Covered Call strategy.
163    CoveredCall,
164    /// Protective Put strategy.
165    ProtectivePut,
166    /// Collar strategy.
167    Collar,
168    /// Long Call strategy.
169    LongCall,
170    /// Long Put strategy.
171    LongPut,
172    /// Short Call strategy.
173    ShortCall,
174    /// Short Put strategy.
175    ShortPut,
176    /// Poor Man's Covered Call strategy.
177    PoorMansCoveredCall,
178    /// Call Butterfly strategy.
179    CallButterfly,
180    /// Custom strategy.
181    Custom,
182}
183
184impl FromStr for StrategyType {
185    type Err = ();
186
187    fn from_str(s: &str) -> Result<Self, Self::Err> {
188        match s {
189            "BullCallSpread" => Ok(StrategyType::BullCallSpread),
190            "BearCallSpread" => Ok(StrategyType::BearCallSpread),
191            "BullPutSpread" => Ok(StrategyType::BullPutSpread),
192            "BearPutSpread" => Ok(StrategyType::BearPutSpread),
193            "LongButterflySpread" => Ok(StrategyType::LongButterflySpread),
194            "ShortButterflySpread" => Ok(StrategyType::ShortButterflySpread),
195            "IronCondor" => Ok(StrategyType::IronCondor),
196            "IronButterfly" => Ok(StrategyType::IronButterfly),
197            "LongStraddle" => Ok(StrategyType::LongStraddle),
198            "ShortStraddle" => Ok(StrategyType::ShortStraddle),
199            "LongStrangle" => Ok(StrategyType::LongStrangle),
200            "ShortStrangle" => Ok(StrategyType::ShortStrangle),
201            "CoveredCall" => Ok(StrategyType::CoveredCall),
202            "ProtectivePut" => Ok(StrategyType::ProtectivePut),
203            "Collar" => Ok(StrategyType::Collar),
204            "LongCall" => Ok(StrategyType::LongCall),
205            "LongPut" => Ok(StrategyType::LongPut),
206            "ShortCall" => Ok(StrategyType::ShortCall),
207            "ShortPut" => Ok(StrategyType::ShortPut),
208            "PoorMansCoveredCall" => Ok(StrategyType::PoorMansCoveredCall),
209            "CallButterfly" => Ok(StrategyType::CallButterfly),
210            "Custom" => Ok(StrategyType::Custom),
211            _ => Err(()),
212        }
213    }
214}
215
216impl StrategyType {
217    /// Checks if a given string is a valid `StrategyType`.
218    ///
219    /// # Arguments
220    ///
221    /// * `strategy` - A string slice representing the strategy type.
222    ///
223    /// # Returns
224    ///
225    /// `true` if the string is a valid `StrategyType`, `false` otherwise.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use optionstratlib::strategies::base::StrategyType;
231    /// assert!(StrategyType::is_valid("BullCallSpread"));
232    /// assert!(!StrategyType::is_valid("InvalidStrategy"));
233    /// ```
234    #[inline]
235    #[must_use]
236    pub fn is_valid(strategy: &str) -> bool {
237        StrategyType::from_str(strategy).is_ok()
238    }
239}
240
241impl fmt::Display for StrategyType {
242    /// Formats the `StrategyType` for display.
243    ///
244    /// # Arguments
245    ///
246    /// * `f` - A mutable formatter.
247    ///
248    /// # Returns
249    ///
250    /// A `fmt::Result` indicating whether the formatting was successful.
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        write!(f, "{self:?}")
253    }
254}
255
256/// Represents a complete options trading strategy with risk-reward parameters.
257///
258/// A strategy encapsulates all the information needed to describe, analyze, and
259/// trade a specific options strategy. It includes identifying information, the positions
260/// that make up the strategy, and critical risk metrics such as maximum profit/loss
261/// and break-even points.
262///
263/// This structure serves as the foundation for strategy analysis, visualization,
264/// and trading execution within the options trading framework.
265///
266pub struct Strategy {
267    /// The name of the strategy, which identifies it among other strategies.
268    pub name: String,
269
270    /// The type of the strategy, categorizing it according to standard options strategies.
271    pub kind: StrategyType,
272
273    /// A textual description explaining the strategy's purpose, construction, and typical market scenarios.
274    pub description: String,
275
276    /// A collection of positions (or legs) that together form the complete strategy.
277    /// Each position represents an option contract or underlying asset position.
278    pub legs: Vec<Position>,
279
280    /// The maximum potential profit of the strategy, if limited and known.
281    /// Expressed as an absolute value, not percentage.
282    pub max_profit: Option<f64>,
283
284    /// The maximum potential loss of the strategy, if limited and known.
285    /// Expressed as an absolute value, not percentage.
286    pub max_loss: Option<f64>,
287
288    /// The price points of the underlying asset at which the strategy neither makes a profit nor a loss.
289    /// These points are crucial for strategy planning and risk management.
290    pub break_even_points: Vec<Positive>,
291}
292
293/// Creates a new `Strategy` instance.
294///
295/// This function initializes a new trading strategy with the given name, kind, and description.  The `legs`, `max_profit`, `max_loss`, and `break_even_points` are initialized as empty or `None`.
296///
297/// # Arguments
298///
299/// * `name` - The name of the strategy.
300/// * `kind` - The type of the strategy  (e.g., BullCallSpread, LongStraddle).
301/// * `description` - A description of the strategy.
302///
303/// # Returns
304///
305/// A new `Strategy` instance.
306///
307/// # Example
308///
309/// ```
310/// use optionstratlib::strategies::base::{Strategy, StrategyType};
311/// let strategy = Strategy::new(
312///     "My Strategy".to_string(),
313///     StrategyType::LongCall,
314///     "A simple long call strategy".to_string(),
315/// );
316///
317/// assert_eq!(strategy.name, "My Strategy");
318/// assert_eq!(strategy.kind, StrategyType::LongCall);
319/// assert_eq!(strategy.description, "A simple long call strategy");
320/// assert!(strategy.legs.is_empty());
321/// assert_eq!(strategy.max_profit, None);
322/// assert_eq!(strategy.max_loss, None);
323/// assert!(strategy.break_even_points.is_empty());
324/// ```
325impl Strategy {
326    /// Creates a new `Strategy` instance.
327    ///
328    /// This function initializes a new trading strategy with the given name, kind, and description.
329    /// The `legs`, `max_profit`, `max_loss`, and `break_even_points` are initialized as empty or `None`.
330    ///
331    /// # Arguments
332    ///
333    /// * `name` - The name of the strategy.
334    /// * `kind` - The type of the strategy (e.g., BullCallSpread, LongStraddle).
335    /// * `description` - A description of the strategy.
336    ///
337    /// # Returns
338    ///
339    /// A new `Strategy` instance.
340    ///
341    /// # Example
342    ///
343    /// ```
344    /// use optionstratlib::strategies::base::{Strategy, StrategyType};
345    /// let strategy = Strategy::new(
346    ///     "My Strategy".to_string(),
347    ///     StrategyType::LongCall,
348    ///     "A simple long call strategy".to_string(),
349    /// );
350    ///
351    /// assert_eq!(strategy.name, "My Strategy");
352    /// assert_eq!(strategy.kind, StrategyType::LongCall);
353    /// assert_eq!(strategy.description, "A simple long call strategy");
354    /// assert!(strategy.legs.is_empty());
355    /// assert_eq!(strategy.max_profit, None);
356    /// assert_eq!(strategy.max_loss, None);
357    /// assert!(strategy.break_even_points.is_empty());
358    /// ```
359    #[inline]
360    #[must_use]
361    pub fn new(name: String, kind: StrategyType, description: String) -> Self {
362        Strategy {
363            name,
364            kind,
365            description,
366            legs: Vec::new(),
367            max_profit: None,
368            max_loss: None,
369            break_even_points: Vec::new(),
370        }
371    }
372}
373
374/// A trait that defines basic operations and attributes for managing options-related strategies.
375///
376/// This trait provides methods to retrieve various properties and mappings
377/// associated with options, such as title, symbol, strike prices, sides, styles,
378/// expiration dates, and implied volatility.
379///
380/// # Note
381/// Most defaults return either an empty value (with a `tracing::warn!` log)
382/// or an `Err(...)` describing the unsupported operation. Concrete strategies
383/// should override the methods they need.
384///
385/// # Methods
386/// - `get_title`: Returns the title of the strategy.
387/// - `get_option_basic_type`: Retrieves a set of basic option types.
388/// - `get_symbol`: Returns the symbol associated with the option.
389/// - `get_strike`: Maps option basic types to their positive strike values.
390/// - `get_strikes`: Returns a vector of strike prices.
391/// - `get_side`: Maps option basic types to their respective sides.
392/// - `get_type`: Retrieves the type of the option.
393/// - `get_style`: Maps option basic types to their corresponding styles.
394/// - `get_expiration`: Maps option basic types to their expiration dates.
395/// - `get_implied_volatility`: Retrieves implied volatility.
396///
397/// # Panics
398/// Only `one_option` and `one_option_mut` panic on the default implementation,
399/// because their reference return types do not allow a graceful fallback.
400/// Every strategy that owns `Options` must override both methods.
401///
402pub trait BasicAble {
403    /// Retrieves the title associated with the current instance of the strategy.
404    ///
405    /// # Returns
406    /// A `String` representing the title.
407    ///
408    /// # Default
409    /// The default implementation returns an empty `String` and emits a
410    /// `tracing::warn!` log so callers can detect strategies that did not
411    /// override this method.
412    ///
413    fn get_title(&self) -> String {
414        warn!(
415            "get_title default: {} did not override; returning empty string",
416            std::any::type_name::<Self>()
417        );
418        String::new()
419    }
420    /// Retrieves a `HashSet` of `OptionBasicType` values associated with the current strategy.
421    ///
422    /// # Returns
423    /// A `HashSet` containing the `OptionBasicType` elements relevant to the strategy.
424    ///
425    /// # Default
426    /// The default implementation returns an empty `HashSet` and emits a
427    /// `tracing::warn!` log so callers can detect strategies that did not
428    /// override this method.
429    ///
430    fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
431        warn!(
432            "get_option_basic_type default: {} did not override; returning empty set",
433            std::any::type_name::<Self>()
434        );
435        HashSet::new()
436    }
437    /// Retrieves the symbol associated with the current instance by delegating the call to the `get_symbol`
438    /// method of the `one_option` object.
439    ///
440    /// # Returns
441    /// A string slice (`&str`) that represents the symbol.
442    ///
443    /// # Notes
444    /// - Assumes that `one_option()` is a method that returns an object or reference which implements
445    ///   a `get_symbol()` method.
446    /// - The returned `&str` is borrowed from the referenced object, and its lifetime is tied to the `self` instance.
447    fn get_symbol(&self) -> &str {
448        self.one_option().get_symbol()
449    }
450    /// Retrieves a mapping of option basic types to their associated positive strike values.
451    ///
452    /// This method delegates the call to the `get_strike` method of the `one_option` object,
453    /// returning a `HashMap` where each key is an `OptionBasicType` and each value is a reference
454    /// to a `Positive` strike value.
455    ///
456    /// # Returns
457    /// A `HashMap` where:
458    /// - `OptionBasicType` represents the type of the option,
459    /// - `&Positive` is a reference to the positive strike value associated with the option.
460    ///
461    /// # Notes
462    /// - Ensure that the `one_option` method returns a valid object that implements a `get_strike` method.
463    /// - The values in the returned map are references, so their lifetime is tied to the ownership of `self`.
464    ///
465    fn get_strike(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
466        self.one_option().get_strike()
467    }
468    /// Retrieves a vector of strike prices from the option types.
469    ///
470    /// This function accesses the `OptionBasicType` objects associated with the instance,
471    /// extracts their `strike_price` fields, and collects those values into a `Vec<&Positive>`.
472    ///
473    /// # Returns
474    ///
475    /// A vector containing references to the strike prices (`&Positive`) of the associated option types.
476    ///
477    /// # Notes
478    ///
479    /// - The method assumes that `self.get_option_basic_type()` returns a collection of
480    ///   objects that have a `strike_price` field.
481    /// - The `strike_price` type is `&Positive`, which implies it references a type with
482    ///   positive constraints.
483    fn get_strikes(&self) -> Vec<&Positive> {
484        self.get_option_basic_type()
485            .iter()
486            .map(|option_type| option_type.strike_price)
487            .collect()
488    }
489    /// Retrieves a `HashMap` that maps each `OptionBasicType` to its corresponding `Side`.
490    ///
491    /// This method generates a mapping by iterating over the result of `get_option_basic_type`
492    /// and pairing each `OptionBasicType` with its associated `Side`.
493    ///
494    /// # Returns
495    ///
496    /// A `HashMap` where:
497    /// - The keys are `OptionBasicType` values.
498    /// - The values are `Side` references corresponding to each `OptionBasicType`.
499    ///
500    /// # Panics
501    ///
502    /// This function assumes that `option_type.side` is valid for all elements
503    /// in the iterator returned by `get_option_basic_type`. If this assumption is violated,
504    /// the behavior is undefined.
505    ///
506    /// # Notes
507    ///
508    /// Ensure that `get_option_basic_type` is properly implemented and returns
509    /// an iterable collection of `OptionBasicType` elements that have valid `Side` associations.
510    fn get_side(&self) -> HashMap<OptionBasicType<'_>, &Side> {
511        self.get_option_basic_type()
512            .iter()
513            .map(|option_type| (*option_type, option_type.side))
514            .collect()
515    }
516    /// Retrieves the type of the option.
517    ///
518    /// This method provides access to the `OptionType` of the associated option
519    /// by calling the `get_type` method on the result of `self.one_option()`.
520    ///
521    /// # Returns
522    /// A reference to the `OptionType` of the associated option.
523    ///
524    /// # Notes
525    /// - Ensure `self.one_option()` returns a valid object with a callable `get_type`
526    ///   method to avoid runtime errors.
527    fn get_type(&self) -> &OptionType {
528        self.one_option().get_type()
529    }
530    /// Retrieves a mapping of `OptionBasicType` to their corresponding `OptionStyle`.
531    ///
532    /// This function generates a `HashMap` where each `OptionBasicType` returned by
533    /// the `get_option_basic_type` method is associated with its respective `OptionStyle`.
534    ///
535    /// # Returns
536    /// A `HashMap` where:
537    /// - The keys are `OptionBasicType` values (basic option types).
538    /// - The values are references to the associated `OptionStyle` for each option type.
539    ///
540    /// # Note
541    /// Ensure that `get_option_basic_type` returns a valid iterator of `OptionBasicType`
542    /// items before calling this function, as the result depends on its output.
543    ///
544    /// # Panics
545    /// This function will panic if the `OptionStyle` reference is invalid or not properly
546    /// initialized for any `OptionBasicType`.
547    fn get_style(&self) -> HashMap<OptionBasicType<'_>, &OptionStyle> {
548        self.get_option_basic_type()
549            .iter()
550            .map(|option_type| (*option_type, option_type.option_style))
551            .collect()
552    }
553    /// Retrieves a map of option basic types to their corresponding expiration dates.
554    ///
555    /// This method iterates over the collection of option basic types and creates a `HashMap`
556    /// where each key is an `OptionBasicType` and the value is a reference to its associated
557    /// `ExpirationDate`.
558    ///
559    /// # Returns
560    /// A `HashMap` where:
561    /// - The key is of type `OptionBasicType`.
562    /// - The value is a reference to the `ExpirationDate` associated with the option basic type.
563    ///
564    /// # Panics
565    /// This method may panic if `expiration_date` is unexpectedly `None` within the option type,
566    /// depending on your implementation of `get_option_basic_type`.
567    ///
568    /// # Notes
569    /// - Ensure `self.get_option_basic_type()` returns a valid iterable of `OptionBasicType` instances.
570    /// - The lifetime of the returned `ExpirationDate` references is tied to the lifetime of `self`.
571    fn get_expiration(&self) -> HashMap<OptionBasicType<'_>, &ExpirationDate> {
572        self.get_option_basic_type()
573            .iter()
574            .map(|option_type| (*option_type, option_type.expiration_date))
575            .collect()
576    }
577    /// Retrieves the implied volatility for the current strategy.
578    ///
579    /// # Returns
580    ///
581    /// A `HashMap` where the key is of type `OptionBasicType` and
582    /// the value is a reference to a `Positive` value. Each key-value
583    /// pair corresponds to the implied volatility associated with a
584    /// specific option type.
585    ///
586    /// # Default
587    ///
588    /// The default implementation returns an empty `HashMap` and emits a
589    /// `tracing::warn!` log so callers can detect strategies that did not
590    /// override this method.
591    fn get_implied_volatility(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
592        warn!(
593            "get_implied_volatility default: {} did not override; returning empty map",
594            std::any::type_name::<Self>()
595        );
596        HashMap::new()
597    }
598    /// Retrieves the quantity information associated with the strategy.
599    ///
600    /// # Returns
601    /// A `HashMap` that holds pairs of `OptionBasicType` (the key) and a reference
602    /// to a `Positive` value (the value). This map represents the mapping of
603    /// option basic types to their respective quantities.
604    ///
605    /// # Default
606    /// The default implementation returns an empty `HashMap` and emits a
607    /// `tracing::warn!` log so callers can detect strategies that did not
608    /// override this method.
609    ///
610    /// # Example
611    /// The function currently serves as a placeholder and should be implemented
612    /// in a specific strategy that defines its behavior.
613    fn get_quantity(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
614        warn!(
615            "get_quantity default: {} did not override; returning empty map",
616            std::any::type_name::<Self>()
617        );
618        HashMap::new()
619    }
620    /// Retrieves the underlying price of the financial instrument (e.g., option).
621    ///
622    /// This method fetches the underlying price from the associated `one_option`
623    /// instance, ensuring that the value is positive.
624    ///
625    /// # Returns
626    ///
627    /// A reference to a `Positive` value representing the underlying price.
628    ///
629    /// # Notes
630    ///
631    /// This method assumes that the underlying price is always available and valid.
632    fn get_underlying_price(&self) -> &Positive {
633        self.one_option().get_underlying_price()
634    }
635    /// Retrieves the risk-free interest rate associated with a given set of options.
636    ///
637    /// This function retrieves the risk-free rate from a single option
638    /// and returns it as a `HashMap`, where the keys correspond to the `OptionBasicType`
639    /// and the values are references to the respective `Decimal` values.
640    ///
641    /// # Returns
642    ///
643    /// A `HashMap` where:
644    /// - The key is of type `OptionBasicType`, representing the unique identifier or type of option.
645    /// - The value is a reference (`&Decimal`) to the corresponding risk-free rate.
646    ///
647    /// # Notes
648    ///
649    /// - The method relies on the `one_option()` function to retrieve the required data.
650    /// - Ensure that the `one_option()` method is implemented correctly to fetch the necessary risk-free rates.
651    ///
652    /// # Errors
653    ///
654    /// This function assumes that `one_option` and its underlying functionality
655    /// are error-free. Errors, if any, must be handled within `one_option`.
656    fn get_risk_free_rate(&self) -> HashMap<OptionBasicType<'_>, &Decimal> {
657        self.one_option().get_risk_free_rate()
658    }
659    /// Retrieves the dividend yield of a financial option.
660    ///
661    /// This method calls the `get_dividend_yield` function of the associated `one_option()`
662    /// method and returns a `HashMap` containing the dividend yield information. The keys
663    /// of the map are of type `OptionBasicType`, and the values are references to instances
664    /// of `Positive`.
665    ///
666    /// # Returns
667    /// * `HashMap<OptionBasicType<'_>, &Positive>`: A mapping of option basic types to their
668    ///   respective positive dividend yield values.
669    ///
670    /// # Note
671    /// Ensure that the associated `one_option()` method is correctly implemented
672    /// and provides the desired dividend yield information.
673    fn get_dividend_yield(&self) -> HashMap<OptionBasicType<'_>, &Positive> {
674        self.one_option().get_dividend_yield()
675    }
676    /// Retrieves a shared reference to the strategy's primary `Options` value.
677    ///
678    /// # Returns
679    /// * `&Options` - A reference to an `Options` object owned by the strategy.
680    ///
681    /// # Panics
682    /// The default implementation panics because there is no graceful fallback
683    /// for a borrowed `&Options` return. Every strategy that owns options
684    /// must override this method.
685    ///
686    /// # Note
687    /// This is a placeholder implementation and must be overridden in any
688    /// concrete strategy that holds option positions.
689    fn one_option(&self) -> &Options {
690        // INVARIANT: a `&Options` return type admits no default — we cannot
691        // materialise a safe reference out of thin air. Every strategy that
692        // owns positions overrides this; the panic fires only when a caller
693        // dispatches through the trait default on a type with no options,
694        // which is a programmer error.
695        panic!(
696            "one_option not implemented for this strategy — every strategy with options must override"
697        )
698    }
699    /// Provides a mutable reference to the strategy's primary `Options` value.
700    ///
701    /// # Panics
702    ///
703    /// The default implementation panics because there is no graceful fallback
704    /// for a borrowed `&mut Options` return. Every strategy that owns options
705    /// must override this method.
706    ///
707    /// # Returns
708    ///
709    /// A mutable reference to an `Options` instance.
710    ///
711    fn one_option_mut(&mut self) -> &mut Options {
712        // INVARIANT: same rationale as `one_option` — `&mut Options` has no
713        // safe default value, so every strategy with positions must override
714        // this method. Reaching the panic is a programmer error.
715        panic!(
716            "one_option_mut not implemented for this strategy — every strategy with options must override"
717        )
718    }
719
720    /// Sets the expiration date for the strategy.
721    ///
722    /// # Parameters
723    ///
724    /// - `_expiration_date`: The expiration date to set for the strategy,
725    ///   represented as an `ExpirationDate` object.
726    ///
727    /// # Returns
728    ///
729    /// - `Ok(())` if the operation is successful.
730    /// - `Err(StrategyError)` if the strategy does not support setting
731    ///   expiration dates.
732    ///
733    /// # Errors
734    ///
735    /// The default implementation returns
736    /// `StrategyError::OperationError(NotSupported { .. })`. Strategies that
737    /// support mutating expiration should override this method.
738    fn set_expiration_date(
739        &mut self,
740        _expiration_date: ExpirationDate,
741    ) -> Result<(), StrategyError> {
742        Err(StrategyError::operation_not_supported(
743            "set_expiration_date",
744            std::any::type_name::<Self>(),
745        ))
746    }
747    /// Sets the underlying price for this strategy.
748    ///
749    /// # Parameters
750    /// - `_price`: A reference to a `Positive` value representing the new underlying price
751    ///   to be set.
752    ///
753    /// # Returns
754    /// - `Ok(())` if the operation is successful.
755    /// - `Err(StrategyError)` if the strategy does not support setting the
756    ///   underlying price.
757    ///
758    /// # Errors
759    /// The default implementation returns
760    /// `StrategyError::OperationError(NotSupported { .. })`. Strategies that
761    /// support mutating the underlying price should override this method.
762    ///
763    fn set_underlying_price(&mut self, _price: &Positive) -> Result<(), StrategyError> {
764        Err(StrategyError::operation_not_supported(
765            "set_underlying_price",
766            std::any::type_name::<Self>(),
767        ))
768    }
769    /// Updates the volatility for the strategy.
770    ///
771    /// # Parameters
772    /// - `_volatility`: A reference to a `Positive` value representing the new volatility to set.
773    ///
774    /// # Returns
775    /// - `Ok(())`: If the update operation succeeds.
776    /// - `Err(StrategyError)`: If the strategy does not support setting the
777    ///   implied volatility.
778    ///
779    /// # Errors
780    /// The default implementation returns
781    /// `StrategyError::OperationError(NotSupported { .. })`. Strategies that
782    /// support mutating implied volatility should override this method.
783    ///
784    fn set_implied_volatility(&mut self, _volatility: &Positive) -> Result<(), StrategyError> {
785        Err(StrategyError::operation_not_supported(
786            "set_implied_volatility",
787            std::any::type_name::<Self>(),
788        ))
789    }
790}
791
792/// Defines a set of strategies for options trading.  Provides methods for calculating key metrics
793/// such as profit/loss, cost, break-even points, and price ranges.  Implementations of this trait
794/// must also implement the `Validable`, `Positionable`, and `BreakEvenable` traits.
795pub trait Strategies: Validable + Positionable + BreakEvenable + BasicAble {
796    /// Retrieves the current volume of the strategy as sum of quantities in their positions
797    ///
798    /// This function returns the volume as a `Positive` value, ensuring that the result
799    /// is always greater than zero. If the method fails to retrieve the volume, an error
800    /// of type `StrategyError` is returned.
801    ///
802    /// # Returns
803    ///
804    /// - `Ok(Positive)` - The current volume as a positive numeric value.
805    /// - `Err(StrategyError)` - An error indicating why the volume could not be retrieved.
806    ///
807    /// # Errors
808    ///
809    /// This function may return a `StrategyError` in cases such as:
810    /// - Internal issues within the strategy's calculation or storage.
811    /// - Other implementation-specific failures.
812    ///
813    fn get_volume(&mut self) -> Result<Positive, StrategyError> {
814        let quantities = self.get_quantity();
815        let mut volume = Positive::ZERO;
816        for (_, quantity) in quantities {
817            volume += *quantity;
818        }
819        Ok(volume)
820    }
821
822    /// Calculates the maximum possible profit for the strategy.
823    /// The default implementation returns an error indicating that the operation is not supported.
824    ///
825    /// # Returns
826    /// * `Ok(Positive)` - The maximum possible profit.
827    /// * `Err(StrategyError)` - If the operation is not supported for this strategy.
828    ///
829    /// # Errors
830    ///
831    /// The default implementation returns [`StrategyError::OperationError`]
832    /// with [`OperationErrorKind::NotSupported`]. Concrete strategies may
833    /// surface [`StrategyError::PriceError`] or
834    /// [`StrategyError::ProfitLossError`] when the payoff evaluation fails.
835    fn get_max_profit(&self) -> Result<Positive, StrategyError> {
836        Err(StrategyError::operation_not_supported(
837            "max_profit",
838            std::any::type_name::<Self>(),
839        ))
840    }
841
842    /// Calculates the maximum possible profit for the strategy, potentially using an iterative approach.
843    /// Defaults to calling `max_profit`.
844    ///
845    /// # Returns
846    /// * `Ok(Positive)` - The maximum possible profit.
847    /// * `Err(StrategyError)` - If the operation is not supported for this strategy.
848    ///
849    /// # Errors
850    ///
851    /// Propagates any [`StrategyError`] returned by
852    /// `Strategable::get_max_profit` on `&self`.
853    fn get_max_profit_mut(&mut self) -> Result<Positive, StrategyError> {
854        self.get_max_profit()
855    }
856
857    /// Calculates the maximum possible loss for the strategy.
858    /// The default implementation returns an error indicating that the operation is not supported.
859    ///
860    /// # Returns
861    /// * `Ok(Positive)` - The maximum possible loss.
862    /// * `Err(StrategyError)` - If the operation is not supported for this strategy.
863    ///
864    /// # Errors
865    ///
866    /// The default implementation returns [`StrategyError::OperationError`]
867    /// with [`OperationErrorKind::NotSupported`]. Concrete strategies may
868    /// surface [`StrategyError::PriceError`] or
869    /// [`StrategyError::ProfitLossError`] when the payoff evaluation fails.
870    fn get_max_loss(&self) -> Result<Positive, StrategyError> {
871        Err(StrategyError::operation_not_supported(
872            "max_loss",
873            std::any::type_name::<Self>(),
874        ))
875    }
876
877    /// Calculates the maximum possible loss for the strategy, potentially using an iterative approach.
878    /// Defaults to calling `max_loss`.
879    ///
880    /// # Returns
881    /// * `Ok(Positive)` - The maximum possible loss.
882    /// * `Err(StrategyError)` - If the operation is not supported for this strategy.
883    ///
884    /// # Errors
885    ///
886    /// Propagates any [`StrategyError`] returned by
887    /// `Strategable::get_max_loss` on `&self`.
888    fn get_max_loss_mut(&mut self) -> Result<Positive, StrategyError> {
889        self.get_max_loss()
890    }
891
892    /// Calculates the total cost of the strategy, which is the sum of the absolute cost of all positions.
893    ///
894    /// # Returns
895    /// * `Ok(Positive)` - The total cost of the strategy.
896    /// * `Err(PositionError)` - If there is an error retrieving the positions.
897    ///
898    /// # Errors
899    ///
900    /// Propagates any [`PositionError`] returned by
901    /// `Strategable::get_positions` or by
902    /// [`Position::total_cost`] when the component legs surface invalid
903    /// state.
904    fn get_total_cost(&self) -> Result<Positive, PositionError> {
905        let positions = self.get_positions()?;
906        let mut total = Positive::ZERO;
907        for p in positions {
908            total += p.total_cost()?;
909        }
910        Ok(total)
911    }
912
913    /// Calculates the net cost of the strategy, which is the sum of the costs of all positions,
914    /// considering premiums paid and received.
915    ///
916    /// # Returns
917    /// * `Ok(Decimal)` - The net cost of the strategy.
918    /// * `Err(PositionError)` - If there is an error retrieving the positions.
919    ///
920    /// # Errors
921    ///
922    /// Propagates any [`PositionError`] returned by
923    /// `Strategable::get_positions` or by
924    /// [`Position::net_cost`] when the component legs surface invalid
925    /// state.
926    fn get_net_cost(&self) -> Result<Decimal, PositionError> {
927        let positions = self.get_positions()?;
928        let mut total = Decimal::ZERO;
929        for p in positions {
930            total += p.net_cost()?;
931        }
932        Ok(total)
933    }
934
935    /// Calculates the net premium received for the strategy. This is the total premium received from short positions
936    /// minus the total premium paid for long positions. If the result is negative, it returns zero.
937    ///
938    /// # Returns
939    /// * `Ok(Positive)` - The net premium received.
940    /// * `Err(StrategyError)` - If there is an error retrieving the positions.
941    ///
942    /// # Errors
943    ///
944    /// Returns `StrategyError::from(PositionError)` when
945    /// `Strategable::get_positions` or
946    /// [`Position::net_premium_received`] fail on any leg.
947    fn get_net_premium_received(&self) -> Result<Positive, StrategyError> {
948        let positions = self.get_positions()?;
949        let mut costs = Decimal::ZERO;
950        let mut premiums = Positive::ZERO;
951        for p in positions {
952            if p.option.side == Side::Long {
953                costs += p.net_cost()?;
954            } else if p.option.side == Side::Short {
955                premiums += p.net_premium_received()?;
956            }
957        }
958        match premiums > costs {
959            true => Ok(premiums - costs),
960            false => Ok(Positive::ZERO),
961        }
962    }
963
964    /// Calculates the total fees for the strategy by summing the fees of all positions.
965    ///
966    /// # Returns
967    /// * `Ok(Positive)` - The total fees.
968    /// * `Err(StrategyError)` - If there is an error retrieving positions or calculating fees.
969    ///
970    /// # Errors
971    ///
972    /// Returns `StrategyError::from(PositionError)` when
973    /// `Strategable::get_positions` or [`Position::fees`] fail on any
974    /// leg.
975    fn get_fees(&self) -> Result<Positive, StrategyError> {
976        let mut fee = Positive::ZERO;
977        let positions = match self.get_positions() {
978            Ok(positions) => positions,
979            Err(err) => {
980                return Err(StrategyError::OperationError(
981                    OperationErrorKind::InvalidParameters {
982                        operation: "get_positions".to_string(),
983                        reason: err.to_string(),
984                    },
985                ));
986            }
987        };
988        for position in positions {
989            fee += position.fees()?;
990        }
991        Ok(fee)
992    }
993
994    /// Calculates the profit area for the strategy. The default implementation returns an error
995    /// indicating that the operation is not supported.
996    ///
997    /// # Returns
998    /// * `Ok(Decimal)` - The profit area.
999    /// * `Err(StrategyError)` - If the operation is not supported.
1000    ///
1001    /// # Errors
1002    ///
1003    /// The default implementation returns [`StrategyError::OperationError`]
1004    /// with [`OperationErrorKind::NotSupported`]. Overriding strategies may
1005    /// surface [`StrategyError::BreakEvenError`] or
1006    /// [`StrategyError::PriceError`] when the payoff integral fails.
1007    fn get_profit_area(&self) -> Result<Decimal, StrategyError> {
1008        Err(StrategyError::operation_not_supported(
1009            "profit_area",
1010            std::any::type_name::<Self>(),
1011        ))
1012    }
1013
1014    /// Calculates the profit ratio for the strategy. The default implementation returns an error
1015    /// indicating that the operation is not supported.
1016    ///
1017    /// # Returns
1018    /// * `Ok(Decimal)` - The profit ratio.
1019    /// * `Err(StrategyError)` - If the operation is not supported.
1020    ///
1021    /// # Errors
1022    ///
1023    /// The default implementation returns [`StrategyError::OperationError`]
1024    /// with [`OperationErrorKind::NotSupported`]. Overriding strategies may
1025    /// surface [`StrategyError::ProfitLossError`] when either
1026    /// `get_max_profit` or `get_max_loss` fails.
1027    fn get_profit_ratio(&self) -> Result<Decimal, StrategyError> {
1028        Err(StrategyError::operation_not_supported(
1029            "profit_ratio",
1030            std::any::type_name::<Self>(),
1031        ))
1032    }
1033
1034    /// Determines the price range to display for the strategy's profit/loss graph.  This range is
1035    /// calculated based on the break-even points, the underlying price, and the maximum and minimum
1036    /// strike prices.  The range is expanded by applying `STRIKE_PRICE_LOWER_BOUND_MULTIPLIER` and
1037    /// `STRIKE_PRICE_UPPER_BOUND_MULTIPLIER` to the minimum and maximum prices respectively.
1038    ///
1039    /// # Returns
1040    /// * `Ok((Positive, Positive))` - A tuple containing the start and end prices of the range.
1041    /// * `Err(StrategyError)` - If there is an error retrieving necessary data for the calculation.
1042    ///
1043    /// # Errors
1044    ///
1045    /// Propagates any [`StrategyError`] returned by
1046    /// `Strategable::get_break_even_points` or
1047    /// `Strategable::get_max_min_strikes`.
1048    fn get_range_to_show(&self) -> Result<(Positive, Positive), StrategyError> {
1049        let mut all_points = self.get_break_even_points()?.clone();
1050        let (first_strike, last_strike) = self.get_max_min_strikes()?;
1051        let underlying_price = self.get_underlying_price();
1052
1053        // Calculate the largest difference from the underlying price to furthest strike
1054        let max_diff = (last_strike.value() - underlying_price.value())
1055            .abs()
1056            .max((first_strike.value() - underlying_price.value()).abs());
1057
1058        // Expand range by max_diff
1059        all_points.push(
1060            (*underlying_price - max_diff)
1061                .max(Positive::ZERO)
1062                .min(first_strike),
1063        );
1064        all_points.push((*underlying_price + max_diff).max(last_strike));
1065
1066        // Sort to find min and max
1067        // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort
1068        all_points.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1069
1070        let first = all_points.first().ok_or_else(|| {
1071            StrategyError::empty_collection("get_range_to_show: all_points is empty")
1072        })?;
1073        let last = all_points.last().ok_or_else(|| {
1074            StrategyError::empty_collection("get_range_to_show: all_points is empty")
1075        })?;
1076        let start_price = *first * STRIKE_PRICE_LOWER_BOUND_MULTIPLIER;
1077        let end_price = *last * STRIKE_PRICE_UPPER_BOUND_MULTIPLIER;
1078        Ok((start_price, end_price))
1079    }
1080
1081    /// Generates a vector of prices within the display range, using a specified step.
1082    ///
1083    /// # Returns
1084    /// * `Ok(Vec<Positive>)` - A vector of prices.
1085    /// * `Err(StrategyError)` - If there is an error calculating the display range.
1086    ///
1087    /// # Errors
1088    ///
1089    /// Propagates any [`StrategyError`] returned by
1090    /// `Strategable::get_range_to_show`.
1091    fn get_best_range_to_show(&self, step: Positive) -> Result<Vec<Positive>, StrategyError> {
1092        let (start_price, end_price) = self.get_range_to_show()?;
1093        Ok(calculate_price_range(start_price, end_price, step))
1094    }
1095
1096    /// Returns the minimum and maximum strike prices from the positions in the strategy.
1097    /// Considers underlying price when applicable, ensuring the returned range includes it.
1098    ///
1099    /// # Returns
1100    /// * `Ok((Positive, Positive))` - A tuple containing the minimum and maximum strike prices.
1101    /// * `Err(StrategyError)` - If no strikes are found or if an error occurs retrieving positions.
1102    ///
1103    /// # Errors
1104    ///
1105    /// Returns [`StrategyError::PriceError`] when the strategy has no
1106    /// strikes to compare against; propagates [`StrategyError`] variants
1107    /// from `Strategable::get_positions` when position enumeration fails.
1108    fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> {
1109        let strikes: Vec<&Positive> = self.get_strikes();
1110        if strikes.is_empty() {
1111            return Err(StrategyError::OperationError(
1112                OperationErrorKind::InvalidParameters {
1113                    operation: "max_min_strikes".to_string(),
1114                    reason: "No strikes found".to_string(),
1115                },
1116            ));
1117        }
1118
1119        let min = strikes.iter().fold(Positive::INFINITY, |acc, &strike| {
1120            Positive::min(acc, *strike)
1121        });
1122        let max = strikes
1123            .iter()
1124            .fold(Positive::ZERO, |acc, &strike| Positive::max(acc, *strike));
1125
1126        let underlying_price = self.get_underlying_price();
1127        let mut min_value = min;
1128        let mut max_value = max;
1129
1130        if underlying_price != &Positive::ZERO {
1131            if min_value > *underlying_price {
1132                min_value = *underlying_price;
1133            }
1134            if *underlying_price > max_value {
1135                max_value = *underlying_price;
1136            }
1137        }
1138
1139        Ok((min_value, max_value))
1140    }
1141
1142    /// Calculates the range of prices where the strategy is profitable, based on the break-even points.
1143    ///
1144    /// # Returns:
1145    /// * `Ok(Positive)` - The difference between the highest and lowest break-even points.  Returns
1146    ///   `Positive::INFINITY` if there is only one break-even point.
1147    /// * `Err(StrategyError)` - if there are no break-even points.
1148    ///
1149    /// # Errors
1150    ///
1151    /// Returns [`StrategyError::BreakEvenError`] when the strategy has
1152    /// no break-even points, and propagates any other [`StrategyError`]
1153    /// surfaced by `Strategable::get_break_even_points`.
1154    fn get_range_of_profit(&self) -> Result<Positive, StrategyError> {
1155        let mut break_even_points = self.get_break_even_points()?.clone();
1156        match break_even_points.len() {
1157            0 => Err(StrategyError::BreakEvenError(
1158                BreakEvenErrorKind::NoBreakEvenPoints,
1159            )),
1160            1 => Ok(Positive::INFINITY),
1161            2 => Ok(break_even_points[1] - break_even_points[0]),
1162            _ => {
1163                // sort break even points and then get last minus first
1164                // SAFETY: total order on Positive; f64 fallback to Equal is safe for stable sort
1165                break_even_points
1166                    .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1167                let last = break_even_points.last().ok_or_else(|| {
1168                    StrategyError::empty_collection(
1169                        "get_range_of_profit: break_even_points is empty",
1170                    )
1171                })?;
1172                let first = break_even_points.first().ok_or_else(|| {
1173                    StrategyError::empty_collection(
1174                        "get_range_of_profit: break_even_points is empty",
1175                    )
1176                })?;
1177                Ok(*last - *first)
1178            }
1179        }
1180    }
1181
1182    /// Attempts to execute the roll-in functionality for the strategy.
1183    ///
1184    /// # Parameters
1185    /// - `&mut self`: A mutable reference to the current instance of the strategy.
1186    /// - `_position: &Position`: A reference to the `Position` object, representing the current position
1187    ///   in the market. This parameter is currently unused by the default implementation.
1188    ///
1189    /// # Returns
1190    /// - `Result<HashMap<Action, Trade>, StrategyError>`:
1191    ///   - `Ok(HashMap<Action, Trade>)`: On success, a map of actions to trades, representing the changes
1192    ///     made during the roll-in process.
1193    ///   - `Err(StrategyError)`: If the strategy does not support rolling in.
1194    ///
1195    /// # Errors
1196    /// The default implementation returns
1197    /// `StrategyError::OperationError(NotSupported { .. })`. Strategies that
1198    /// support roll-in should override this method.
1199    fn roll_in(&mut self, _position: &Position) -> Result<HashMap<Action, Trade>, StrategyError> {
1200        Err(StrategyError::operation_not_supported(
1201            "roll_in",
1202            std::any::type_name::<Self>(),
1203        ))
1204    }
1205
1206    /// Executes the roll-out strategy for the provided position.
1207    ///
1208    /// # Arguments
1209    ///
1210    /// * `_position` - A reference to a `Position` object which represents the
1211    ///   current state of a trading position.
1212    ///
1213    /// # Returns
1214    ///
1215    /// * `Result<HashMap<Action, Trade>, StrategyError>` - A `Result` object
1216    ///   containing:
1217    ///   - `Ok(HashMap<Action, Trade>)` with the mapping of actions to trades.
1218    ///   - `Err(StrategyError)` if the strategy does not support rolling out.
1219    ///
1220    /// # Errors
1221    ///
1222    /// The default implementation returns
1223    /// `StrategyError::OperationError(NotSupported { .. })`. Strategies that
1224    /// support roll-out should override this method.
1225    fn roll_out(&mut self, _position: &Position) -> Result<HashMap<Action, Trade>, StrategyError> {
1226        Err(StrategyError::operation_not_supported(
1227            "roll_out",
1228            std::any::type_name::<Self>(),
1229        ))
1230    }
1231}
1232
1233/// Trait for strategies that can calculate and update break-even points.
1234///
1235/// This trait provides methods for retrieving and updating break-even points, which are
1236/// crucial for determining profitability in various trading scenarios.
1237pub trait BreakEvenable {
1238    /// Retrieves the break-even points for the strategy.
1239    ///
1240    /// Returns a `Result` containing a reference to a vector of `Positive` values representing
1241    /// the break-even points, or a `StrategyError` if the operation is not supported for the specific strategy.
1242    ///
1243    /// The default implementation returns a `StrategyError::OperationError` with `OperationErrorKind::NotSupported`.
1244    /// Strategies implementing this trait should override this method if they support break-even point calculations.
1245    ///
1246    /// # Errors
1247    ///
1248    /// The default implementation returns [`StrategyError::OperationError`]
1249    /// with [`OperationErrorKind::NotSupported`]. Concrete strategies may
1250    /// surface [`StrategyError::BreakEvenError`] when no crossing is
1251    /// found in the payoff profile.
1252    fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
1253        Err(StrategyError::operation_not_supported(
1254            "get_break_even_points",
1255            std::any::type_name::<Self>(),
1256        ))
1257    }
1258
1259    /// Updates the break-even points for the strategy.
1260    ///
1261    /// This method is responsible for recalculating and updating the break-even points based on
1262    /// the current state of the strategy.
1263    ///
1264    /// # Errors
1265    /// The default implementation returns
1266    /// `StrategyError::OperationError(NotSupported { .. })`. Strategies
1267    /// implementing this trait should override this method to provide
1268    /// specific update logic.
1269    fn update_break_even_points(&mut self) -> Result<(), StrategyError> {
1270        Err(StrategyError::operation_not_supported(
1271            "update_break_even_points",
1272            std::any::type_name::<Self>(),
1273        ))
1274    }
1275}
1276
1277/// This trait defines a way to validate a strategy.
1278///
1279/// The default implementation panics with a message indicating that validation
1280/// is not applicable for the specific strategy.  Implementors of this trait
1281/// should override the `validate` method to provide specific validation logic.
1282pub trait Validable {
1283    /// Validates the strategy.
1284    ///
1285    /// The default implementation is a safe no-op: it emits a warning and
1286    /// returns `false` so callers treat an unoverridden strategy as invalid.
1287    /// Implementors should override this method to provide appropriate
1288    /// validation logic.
1289    ///
1290    /// Returns `true` if the strategy is valid, and `false` otherwise.
1291    fn validate(&self) -> bool {
1292        tracing::warn!(
1293            ty = std::any::type_name::<Self>(),
1294            "Validable::validate default implementation invoked; treating strategy as invalid"
1295        );
1296        false
1297    }
1298}
1299
1300/// This trait defines methods for optimizing and validating option strategies.
1301/// It combines the `Validable` and `Strategies` traits, requiring implementors
1302/// to provide functionality for both validation and strategy generation.
1303pub trait Optimizable: Validable + Strategies {
1304    /// The type of strategy associated with this optimization.
1305    type Strategy: Strategies;
1306
1307    /// Finds the best ratio-based strategy within the given `OptionChain`.
1308    ///
1309    /// # Arguments
1310    /// * `option_chain` - A reference to the `OptionChain` containing option data.
1311    /// * `side` - A `FindOptimalSide` value specifying the filtering strategy.
1312    #[instrument(skip(self, option_chain), fields(side = ?side, criteria = "Ratio"))]
1313    fn get_best_ratio(&mut self, option_chain: &OptionChain, side: FindOptimalSide) {
1314        self.find_optimal(option_chain, side, OptimizationCriteria::Ratio);
1315    }
1316
1317    /// Finds the best area-based strategy within the given `OptionChain`.
1318    ///
1319    /// # Arguments
1320    /// * `option_chain` - A reference to the `OptionChain` containing option data.
1321    /// * `side` - A `FindOptimalSide` value specifying the filtering strategy.
1322    #[instrument(skip(self, option_chain), fields(side = ?side, criteria = "Area"))]
1323    fn get_best_area(&mut self, option_chain: &OptionChain, side: FindOptimalSide) {
1324        self.find_optimal(option_chain, side, OptimizationCriteria::Area);
1325    }
1326
1327    /// Filters and generates combinations of options data from the given `OptionChain`.
1328    ///
1329    /// # Parameters
1330    /// - `&self`: A reference to the current object/context that holds the filtering logic or required data.
1331    /// - `_option_chain`: A reference to an `OptionChain` object that contains relevant financial information
1332    ///   such as options data, underlying price, and expiration date.
1333    /// - `_side`: A `FindOptimalSide` value that specifies the filtering strategy for finding combinations of
1334    ///   options. It can specify:
1335    ///     - `Upper`: Consider options higher than a certain threshold.
1336    ///     - `Lower`: Consider options lower than a certain threshold.
1337    ///     - `All`: Include all options.
1338    ///     - `Range(start, end)`: Consider options within a specified range.
1339    ///
1340    /// # Returns
1341    /// - An iterator that yields `OptionDataGroup` items. These items represent combinations of options data filtered
1342    ///   based on the given criteria. The `OptionDataGroup` can represent combinations of 2, 3, 4, or any number
1343    ///   of options depending on the grouping logic.
1344    ///
1345    /// **Note**:
1346    /// - The current implementation returns an empty iterator (`std::iter::empty()`) as a placeholder.
1347    /// - You may modify this method to implement the actual filtering and combination logic based on the
1348    ///   provided `OptionChain` and `FindOptimalSide` criteria.
1349    ///
1350    /// # See Also
1351    /// - `FindOptimalSide` for the strategy enumeration.
1352    /// - `OptionDataGroup` for the structure of grouped combinations.
1353    /// - `OptionChain` for the full structure being processed.
1354    fn filter_combinations<'a>(
1355        &'a self,
1356        _option_chain: &'a OptionChain,
1357        _side: FindOptimalSide,
1358    ) -> impl Iterator<Item = OptionDataGroup<'a>> {
1359        error!("Filter combinations is not applicable for this strategy");
1360        std::iter::empty()
1361    }
1362
1363    /// Finds the optimal strategy based on the given criteria.
1364    /// The default implementation is a safe no-op: it emits a warning and
1365    /// leaves `self` unchanged. Specific strategies should override this
1366    /// method to provide their own optimization logic.
1367    ///
1368    /// # Arguments
1369    /// * `_option_chain` - A reference to the `OptionChain` containing option data.
1370    /// * `_side` - A `FindOptimalSide` value specifying the filtering strategy.
1371    /// * `_criteria` - An `OptimizationCriteria` value indicating the optimization goal (e.g., ratio, area).
1372    fn find_optimal(
1373        &mut self,
1374        _option_chain: &OptionChain,
1375        _side: FindOptimalSide,
1376        _criteria: OptimizationCriteria,
1377    ) {
1378        tracing::warn!(
1379            ty = std::any::type_name::<Self>(),
1380            "find_optimal default implementation invoked; strategy left unchanged"
1381        );
1382    }
1383
1384    /// Checks if a long option is valid based on the given criteria.
1385    ///
1386    /// # Arguments
1387    /// * `option` - A reference to the `OptionData` to validate.
1388    /// * `side` - A reference to the `FindOptimalSide` specifying the filtering strategy.
1389    fn is_valid_optimal_option(&self, option: &OptionData, side: &FindOptimalSide) -> bool {
1390        match side {
1391            FindOptimalSide::Upper => option.strike_price >= *self.get_underlying_price(),
1392            FindOptimalSide::Lower => option.strike_price <= *self.get_underlying_price(),
1393            FindOptimalSide::All => true,
1394            FindOptimalSide::Range(start, end) => {
1395                option.strike_price >= *start && option.strike_price <= *end
1396            }
1397            FindOptimalSide::Deltable(_threshold) => true,
1398            FindOptimalSide::Center => {
1399                // `Center` is a sentinel the concrete strategy is expected to
1400                // intercept (it needs contextual state like the ATM strike to
1401                // expand into a concrete side). The default trait impl has no
1402                // such state: log and skip this candidate rather than panic.
1403                tracing::warn!(
1404                    "is_valid_optimal_option: FindOptimalSide::Center must be resolved by the concrete strategy; skipping option"
1405                );
1406                false
1407            }
1408            FindOptimalSide::DeltaRange(min, max) => {
1409                let (delta_call, delta_put) = option.current_deltas();
1410                let put_in = delta_put.is_some_and(|d| d >= *min && d <= *max);
1411                let call_in = delta_call.is_some_and(|d| d >= *min && d <= *max);
1412                put_in || call_in
1413            }
1414        }
1415    }
1416
1417    /// Checks if the prices in the given `StrategyLegs` are valid.
1418    /// Assumes the strategy consists of one long call and one short call by default.
1419    ///
1420    /// # Arguments
1421    /// * `legs` - A reference to the `StrategyLegs` containing the option data.
1422    fn are_valid_legs(&self, legs: &StrategyLegs) -> bool {
1423        // by default, we assume Options are one long call and one short call
1424        let (long, short) = match legs {
1425            StrategyLegs::TwoLegs { first, second } => (first, second),
1426            other => {
1427                tracing::warn!(
1428                    legs = ?other,
1429                    "are_valid_legs: default impl expects TwoLegs"
1430                );
1431                return false;
1432            }
1433        };
1434        long.call_ask.unwrap_or(Positive::ZERO) > Positive::ZERO
1435            && short.call_bid.unwrap_or(Positive::ZERO) > Positive::ZERO
1436    }
1437
1438    /// Creates a new strategy from the given `OptionChain` and `StrategyLegs`.
1439    ///
1440    /// Specific strategies must override this method. The default implementation
1441    /// returns `StrategyError::OperationError(NotSupported { .. })`.
1442    ///
1443    /// # Arguments
1444    /// * `_chain` - A reference to the `OptionChain` providing option data.
1445    /// * `_legs` - A reference to the `StrategyLegs` defining the strategy's components.
1446    ///
1447    /// # Errors
1448    ///
1449    /// Returns `StrategyError::OperationError` if the strategy cannot be built
1450    /// from the supplied legs (e.g., missing bid/ask quotes, invalid leg
1451    /// combination, or operation not supported by the concrete strategy).
1452    fn create_strategy(
1453        &self,
1454        _chain: &OptionChain,
1455        _legs: &StrategyLegs,
1456    ) -> Result<Self::Strategy, StrategyError> {
1457        Err(StrategyError::operation_not_supported(
1458            "create_strategy",
1459            std::any::type_name::<Self>(),
1460        ))
1461    }
1462}
1463
1464/// The `Positionable` trait defines methods for managing positions within a trading strategy.
1465/// These methods allow for adding, retrieving, and modifying positions, providing a common
1466/// interface for different strategies to interact with position data.
1467pub trait Positionable {
1468    /// Adds a position to the strategy.
1469    ///
1470    /// # Arguments
1471    ///
1472    /// * `_position` - A reference to the `Position` to be added.
1473    ///
1474    /// # Returns
1475    ///
1476    /// * `Result<(), PositionError>` - Returns `Ok(())` if the position was successfully added,
1477    ///   or a `PositionError` if the operation is not supported by the strategy.
1478    ///
1479    /// # Default Implementation
1480    ///
1481    /// The default implementation returns an error indicating that adding a position is not
1482    /// supported. Strategies that support adding positions should override this method.
1483    ///
1484    /// # Errors
1485    ///
1486    /// The default implementation returns
1487    /// [`PositionError::unsupported_operation`]. Overriding strategies may
1488    /// surface [`PositionError::ValidationError`] when the added position
1489    /// violates strategy invariants (e.g. mismatched underlying, wrong
1490    /// side or invalid quantity).
1491    fn add_position(&mut self, _position: &Position) -> Result<(), PositionError> {
1492        Err(PositionError::unsupported_operation(
1493            std::any::type_name::<Self>(),
1494            "add_position",
1495        ))
1496    }
1497
1498    /// Retrieves all positions held by the strategy.
1499    ///
1500    /// # Returns
1501    ///
1502    /// * `Result<Vec<&Position>, PositionError>` - A `Result` containing a vector of references to
1503    ///   the `Position` objects held by the strategy, or a `PositionError` if the operation is
1504    ///   not supported.
1505    ///
1506    /// # Default Implementation
1507    ///
1508    /// The default implementation returns an error indicating that getting positions is not
1509    /// supported. Strategies that manage positions should override this method.
1510    ///
1511    /// # Errors
1512    ///
1513    /// The default implementation returns
1514    /// [`PositionError::unsupported_operation`]. Overriding strategies
1515    /// typically do not fail, but may surface
1516    /// [`PositionError::ValidationError`] if the internal layout has been
1517    /// corrupted.
1518    fn get_positions(&self) -> Result<Vec<&Position>, PositionError> {
1519        Err(PositionError::unsupported_operation(
1520            std::any::type_name::<Self>(),
1521            "get_positions",
1522        ))
1523    }
1524
1525    /// Retrieves a specific position based on option style, side, and strike.
1526    ///
1527    /// # Arguments
1528    ///
1529    /// * `_option_style` - The style of the option (Call or Put).
1530    /// * `_side` - The side of the position (Long or Short).
1531    /// * `_strike` - The strike price of the option.
1532    ///
1533    /// # Returns
1534    ///
1535    /// * `Result<Vec<&mut Position>, PositionError>` - A `Result` containing a vector of mutable
1536    ///   references to the matching `Position` objects, or a `PositionError` if the operation is not supported.
1537    ///
1538    /// # Errors
1539    /// The default implementation returns
1540    /// `PositionError::unsupported_operation(..)`. Strategies that manage
1541    /// positions should override this method.
1542    fn get_position(
1543        &mut self,
1544        _option_style: &OptionStyle,
1545        _side: &Side,
1546        _strike: &Positive,
1547    ) -> Result<Vec<&mut Position>, PositionError> {
1548        Err(PositionError::unsupported_operation(
1549            std::any::type_name::<Self>(),
1550            "get_position",
1551        ))
1552    }
1553
1554    /// Retrieves a unique position based on the given option style and side.
1555    ///
1556    /// # Parameters
1557    /// - `_option_style`: A reference to an `OptionStyle` which defines the style of the options (e.g., American, European).
1558    /// - `_side`: A reference to a `Side` which specifies whether the position is on the buy or sell side.
1559    ///
1560    /// # Returns
1561    /// A mutable reference to the `Position` if found. If the position could not be determined or does not exist,
1562    /// returns a `PositionError`.
1563    ///
1564    /// # Errors
1565    /// The default implementation returns
1566    /// `PositionError::unsupported_operation(..)`. Strategies that expose a
1567    /// unique position per (style, side) pair should override this method.
1568    ///
1569    fn get_position_unique(
1570        &mut self,
1571        _option_style: &OptionStyle,
1572        _side: &Side,
1573    ) -> Result<&mut Position, PositionError> {
1574        Err(PositionError::unsupported_operation(
1575            std::any::type_name::<Self>(),
1576            "get_position_unique",
1577        ))
1578    }
1579
1580    /// Retrieves a unique option based on the given style and side.
1581    ///
1582    /// # Parameters
1583    /// - `_option_style`: A reference to an `OptionStyle` that specifies the style
1584    ///   of the option to retrieve (e.g., American, European).
1585    /// - `_side`: A reference to a `Side` that indicates the side of the option, such
1586    ///   as a call or put.
1587    ///
1588    /// # Returns
1589    /// - `Result<&mut Options, PositionError>`:
1590    ///     - On success, a mutable reference to an `Options` object.
1591    ///     - On failure, a `PositionError`.
1592    ///
1593    /// # Errors
1594    /// The default implementation returns
1595    /// `PositionError::unsupported_operation(..)`. Strategies that expose a
1596    /// unique option per (style, side) pair should override this method.
1597    ///
1598    fn get_option_unique(
1599        &mut self,
1600        _option_style: &OptionStyle,
1601        _side: &Side,
1602    ) -> Result<&mut Options, PositionError> {
1603        Err(PositionError::unsupported_operation(
1604            std::any::type_name::<Self>(),
1605            "get_option_unique",
1606        ))
1607    }
1608
1609    /// Modifies an existing position.
1610    ///
1611    /// # Arguments
1612    ///
1613    /// * `_position` - A reference to the `Position` to be modified.
1614    ///
1615    /// # Returns
1616    ///
1617    /// * `Result<(), PositionError>` - A `Result` indicating success or failure of the
1618    ///   modification, or a `PositionError` if the operation is not supported.
1619    ///
1620    /// # Errors
1621    /// The default implementation returns
1622    /// `PositionError::unsupported_operation(..)`. Strategies that allow
1623    /// in-place modification of positions should override this method.
1624    fn modify_position(&mut self, _position: &Position) -> Result<(), PositionError> {
1625        Err(PositionError::unsupported_operation(
1626            std::any::type_name::<Self>(),
1627            "modify_position",
1628        ))
1629    }
1630
1631    ///
1632    /// Attempts to replace the current position with a new position.
1633    ///
1634    /// # Parameters
1635    /// - `_position`: A reference to a `Position` object that represents the new position to replace the current one.
1636    ///
1637    /// # Returns
1638    /// - `Ok(())`: If the position replacement is successful.
1639    /// - `Err(PositionError)`: If an error occurs while replacing the position.
1640    ///
1641    /// # Errors
1642    /// The default implementation returns
1643    /// `PositionError::unsupported_operation(..)`. Strategies that allow
1644    /// replacing positions should override this method.
1645    ///
1646    fn replace_position(&mut self, _position: &Position) -> Result<(), PositionError> {
1647        Err(PositionError::unsupported_operation(
1648            std::any::type_name::<Self>(),
1649            "replace_position",
1650        ))
1651    }
1652
1653    /// Checks if all short positions have a net premium received that meets or exceeds a specified minimum.
1654    ///
1655    /// # Parameters
1656    /// - `min_premium`: A reference to a `Positive` value representing the minimum premium
1657    ///   required for the short positions to be considered valid.
1658    ///
1659    /// # Returns
1660    /// - `true` if all short positions in the portfolio have a net premium received that is greater
1661    ///   than or equal to `min_premium`.
1662    /// - `false` if any of the following conditions occur:
1663    ///   - Unable to retrieve positions (e.g., `get_positions` fails).
1664    ///   - At least one short position has a net premium less than `min_premium`.
1665    ///   - At least one short position's net premium calculation fails with an error.
1666    ///
1667    /// # Implementation Details
1668    /// - Retrieves positions using the `get_positions` method. If this operation fails, the function returns `false`.
1669    /// - Filters positions to only include shorts (based on `is_short` method).
1670    /// - For each short position, determines if the net premium received is available (`is_ok`)
1671    ///   and satisfies the minimum threshold (`>= *min_premium`).
1672    ///
1673    fn valid_premium_for_shorts(&self, min_premium: &Positive) -> bool {
1674        let positions = match self.get_positions() {
1675            Ok(positions) => positions,
1676            Err(_) => return false,
1677        };
1678        positions
1679            .iter()
1680            .filter(|position| position.is_short())
1681            .all(|p| {
1682                p.net_premium_received()
1683                    .is_ok_and(|premium| premium >= *min_premium)
1684            })
1685    }
1686}
1687
1688#[cfg(test)]
1689mod tests_strategies_extended {
1690    use super::*;
1691    use positive::pos_or_panic;
1692
1693    use crate::model::position::Position;
1694    use crate::model::types::{OptionStyle, Side};
1695    use crate::model::utils::create_sample_option_simplest;
1696
1697    #[test]
1698    fn test_strategy_enum() {
1699        assert_ne!(StrategyType::BullCallSpread, StrategyType::BearCallSpread);
1700    }
1701
1702    #[test]
1703    fn test_strategy_new_with_legs() {
1704        let mut strategy = Strategy::new(
1705            "Test Strategy".to_string(),
1706            StrategyType::BullCallSpread,
1707            "Test Description".to_string(),
1708        );
1709        let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
1710        let position = Position::new(
1711            option,
1712            Positive::ONE,
1713            Default::default(),
1714            Positive::ZERO,
1715            Positive::ZERO,
1716            None,
1717            None,
1718        );
1719
1720        strategy.legs.push(position);
1721
1722        assert_eq!(strategy.legs.len(), 1);
1723    }
1724
1725    #[test]
1726    fn test_strategies_get_legs_panic() {
1727        struct PanicStrategy;
1728        impl Validable for PanicStrategy {}
1729        impl Positionable for PanicStrategy {}
1730        impl BreakEvenable for PanicStrategy {}
1731        impl BasicAble for PanicStrategy {}
1732        impl Strategies for PanicStrategy {
1733            fn get_volume(&mut self) -> Result<Positive, StrategyError> {
1734                unreachable!()
1735            }
1736        }
1737
1738        let strategy = PanicStrategy;
1739        assert!(strategy.get_positions().is_err());
1740    }
1741
1742    #[test]
1743    fn test_strategies_break_even_panic() {
1744        struct PanicStrategy;
1745        impl Validable for PanicStrategy {}
1746        impl Positionable for PanicStrategy {}
1747        impl BreakEvenable for PanicStrategy {}
1748        impl BasicAble for PanicStrategy {}
1749        impl Strategies for PanicStrategy {
1750            fn get_volume(&mut self) -> Result<Positive, StrategyError> {
1751                unreachable!()
1752            }
1753        }
1754
1755        let strategy = PanicStrategy;
1756        assert!(strategy.get_break_even_points().is_err());
1757    }
1758
1759    #[test]
1760    fn test_strategies_net_premium_received_panic() {
1761        struct PanicStrategy;
1762        impl Validable for PanicStrategy {}
1763        impl Positionable for PanicStrategy {}
1764        impl BreakEvenable for PanicStrategy {}
1765        impl BasicAble for PanicStrategy {}
1766        impl Strategies for PanicStrategy {}
1767
1768        let strategy = PanicStrategy;
1769        assert!(strategy.get_net_premium_received().is_err());
1770    }
1771
1772    #[test]
1773    fn test_strategies_fees_panic() {
1774        struct PanicStrategy;
1775        impl Validable for PanicStrategy {}
1776        impl Positionable for PanicStrategy {}
1777        impl BreakEvenable for PanicStrategy {}
1778        impl BasicAble for PanicStrategy {}
1779        impl Strategies for PanicStrategy {}
1780
1781        let strategy = PanicStrategy;
1782        assert!(strategy.get_fees().is_err());
1783    }
1784
1785    #[test]
1786    fn test_strategies_max_profit_iter() {
1787        struct TestStrategy;
1788        impl Validable for TestStrategy {}
1789        impl Positionable for TestStrategy {}
1790        impl BreakEvenable for TestStrategy {}
1791        impl BasicAble for TestStrategy {}
1792        impl Strategies for TestStrategy {
1793            fn get_max_profit(&self) -> Result<Positive, StrategyError> {
1794                Ok(Positive::HUNDRED)
1795            }
1796        }
1797
1798        let mut strategy = TestStrategy;
1799        assert_eq!(strategy.get_max_profit_mut().unwrap().to_f64(), 100.0);
1800    }
1801
1802    #[test]
1803    fn test_strategies_max_loss_iter() {
1804        struct TestStrategy;
1805        impl Validable for TestStrategy {}
1806        impl Positionable for TestStrategy {}
1807        impl BreakEvenable for TestStrategy {}
1808        impl BasicAble for TestStrategy {}
1809        impl Strategies for TestStrategy {
1810            fn get_max_loss(&self) -> Result<Positive, StrategyError> {
1811                Ok(pos_or_panic!(50.0))
1812            }
1813        }
1814
1815        let mut strategy = TestStrategy;
1816        assert_eq!(strategy.get_max_loss_mut().unwrap().to_f64(), 50.0);
1817    }
1818
1819    #[test]
1820    fn test_strategies_empty_strikes() {
1821        struct EmptyStrategy;
1822        impl Validable for EmptyStrategy {}
1823        impl Positionable for EmptyStrategy {
1824            fn get_positions(&self) -> Result<Vec<&Position>, PositionError> {
1825                Ok(vec![])
1826            }
1827        }
1828        impl BreakEvenable for EmptyStrategy {}
1829        impl BasicAble for EmptyStrategy {
1830            fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
1831                HashSet::new()
1832            }
1833        }
1834        impl Strategies for EmptyStrategy {}
1835
1836        let strategy = EmptyStrategy;
1837        assert_eq!(strategy.get_strikes(), Vec::<&Positive>::new());
1838        assert!(strategy.get_max_min_strikes().is_err());
1839    }
1840}
1841
1842#[cfg(test)]
1843mod tests_strategy_type {
1844    use super::*;
1845
1846    #[test]
1847    fn test_strategy_type_equality() {
1848        assert_eq!(StrategyType::BullCallSpread, StrategyType::BullCallSpread);
1849        assert_ne!(StrategyType::BullCallSpread, StrategyType::BearCallSpread);
1850    }
1851
1852    #[test]
1853    fn test_strategy_type_clone() {
1854        let strategy = StrategyType::IronCondor;
1855        let cloned = strategy.clone();
1856        assert_eq!(strategy, cloned);
1857    }
1858
1859    #[test]
1860    fn test_strategy_type_debug() {
1861        let strategy = StrategyType::ShortStraddle;
1862        let debug_string = format!("{strategy:?}");
1863        assert_eq!(debug_string, "ShortStraddle");
1864    }
1865
1866    #[test]
1867    fn test_strategy_type_from_str() {
1868        assert_eq!(
1869            StrategyType::from_str("ShortStrangle"),
1870            Ok(StrategyType::ShortStrangle)
1871        );
1872        assert_eq!(
1873            StrategyType::from_str("LongCall"),
1874            Ok(StrategyType::LongCall)
1875        );
1876        assert_eq!(
1877            StrategyType::from_str("BullCallSpread"),
1878            Ok(StrategyType::BullCallSpread)
1879        );
1880        assert_eq!(StrategyType::from_str("InvalidStrategy"), Err(()));
1881    }
1882
1883    #[test]
1884    fn test_strategy_type_is_valid() {
1885        assert!(StrategyType::is_valid("ShortStrangle"));
1886        assert!(StrategyType::is_valid("LongPut"));
1887        assert!(StrategyType::is_valid("CoveredCall"));
1888        assert!(!StrategyType::is_valid("InvalidStrategy"));
1889        assert!(!StrategyType::is_valid("Random"));
1890    }
1891
1892    #[test]
1893    fn test_strategy_type_serialization() {
1894        let strategy = StrategyType::IronCondor;
1895        let serialized = serde_json::to_string(&strategy).unwrap();
1896        assert_eq!(serialized, "\"IronCondor\"");
1897
1898        let deserialized: StrategyType = serde_json::from_str(&serialized).unwrap();
1899        assert_eq!(deserialized, StrategyType::IronCondor);
1900    }
1901
1902    #[test]
1903    fn test_strategy_type_deserialization() {
1904        let json_data = "\"ShortStraddle\"";
1905        let deserialized: StrategyType = serde_json::from_str(json_data).unwrap();
1906        assert_eq!(deserialized, StrategyType::ShortStraddle);
1907    }
1908
1909    #[test]
1910    fn test_invalid_strategy_type_deserialization() {
1911        let json_data = "\"InvalidStrategy\"";
1912        let deserialized: Result<StrategyType, _> = serde_json::from_str(json_data);
1913        assert!(deserialized.is_err());
1914    }
1915}
1916
1917#[cfg(test)]
1918mod tests_best_range_to_show {
1919    use super::*;
1920    use positive::pos_or_panic;
1921
1922    struct TestStrategy {
1923        underlying_price: Positive,
1924        break_even_points: Vec<Positive>,
1925    }
1926
1927    impl TestStrategy {
1928        fn new(underlying_price: Positive, break_even_points: Vec<Positive>) -> Self {
1929            Self {
1930                underlying_price,
1931                break_even_points,
1932            }
1933        }
1934    }
1935
1936    impl Validable for TestStrategy {}
1937
1938    impl Positionable for TestStrategy {}
1939
1940    impl BreakEvenable for TestStrategy {
1941        fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
1942            Ok(&self.break_even_points)
1943        }
1944    }
1945
1946    impl BasicAble for TestStrategy {
1947        fn get_underlying_price(&self) -> &Positive {
1948            &self.underlying_price
1949        }
1950        fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
1951            HashSet::new()
1952        }
1953    }
1954
1955    impl Strategies for TestStrategy {
1956        fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> {
1957            Ok((pos_or_panic!(90.0), Positive::HUNDRED))
1958        }
1959    }
1960
1961    #[test]
1962    fn test_basic_range_with_step() {
1963        let strategy = TestStrategy::new(
1964            Positive::HUNDRED,
1965            vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
1966        );
1967        let range = strategy.get_best_range_to_show(pos_or_panic!(5.0)).unwrap();
1968        assert!(!range.is_empty());
1969        assert_eq!(range[1] - range[0], pos_or_panic!(5.0));
1970    }
1971
1972    #[test]
1973    fn test_range_with_small_step() {
1974        let strategy = TestStrategy::new(
1975            Positive::HUNDRED,
1976            vec![pos_or_panic!(95.0), pos_or_panic!(105.0)],
1977        );
1978        let range = strategy.get_best_range_to_show(Positive::ONE).unwrap();
1979        assert!(!range.is_empty());
1980        assert_eq!(range[1] - range[0], Positive::ONE);
1981    }
1982
1983    #[test]
1984    fn test_range_boundaries() {
1985        let strategy = TestStrategy::new(
1986            Positive::HUNDRED,
1987            vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
1988        );
1989        let range = strategy.get_best_range_to_show(pos_or_panic!(5.0)).unwrap();
1990        assert!(range.first().unwrap() < &pos_or_panic!(90.0));
1991        assert!(range.last().unwrap() > &pos_or_panic!(110.0));
1992    }
1993
1994    #[test]
1995    fn test_range_step_size() {
1996        let strategy = TestStrategy::new(
1997            Positive::HUNDRED,
1998            vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
1999        );
2000        let step = pos_or_panic!(5.0);
2001        let range = strategy.get_best_range_to_show(step).unwrap();
2002
2003        for i in 1..range.len() {
2004            assert_eq!(range[i] - range[i - 1], step);
2005        }
2006    }
2007
2008    #[test]
2009    fn test_range_includes_underlying() {
2010        let underlying_price = Positive::HUNDRED;
2011        let strategy = TestStrategy::new(
2012            underlying_price,
2013            vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
2014        );
2015        let range = strategy.get_best_range_to_show(pos_or_panic!(5.0)).unwrap();
2016
2017        assert!(range.iter().any(|&price| price <= underlying_price));
2018        assert!(range.iter().any(|&price| price >= underlying_price));
2019    }
2020
2021    #[test]
2022    fn test_range_with_extreme_values() {
2023        let strategy = TestStrategy::new(
2024            Positive::HUNDRED,
2025            vec![pos_or_panic!(50.0), pos_or_panic!(150.0)],
2026        );
2027        let range = strategy
2028            .get_best_range_to_show(pos_or_panic!(10.0))
2029            .unwrap();
2030
2031        assert!(range.first().unwrap() <= &pos_or_panic!(50.0));
2032        assert!(range.last().unwrap() >= &pos_or_panic!(150.0));
2033    }
2034}
2035
2036#[cfg(test)]
2037mod tests_range_to_show {
2038    use super::*;
2039    use positive::pos_or_panic;
2040
2041    struct TestStrategy {
2042        underlying_price: Positive,
2043        break_even_points: Vec<Positive>,
2044    }
2045
2046    impl TestStrategy {
2047        fn new(underlying_price: Positive, break_even_points: Vec<Positive>) -> Self {
2048            Self {
2049                underlying_price,
2050                break_even_points,
2051            }
2052        }
2053    }
2054
2055    impl Validable for TestStrategy {}
2056
2057    impl Positionable for TestStrategy {}
2058
2059    impl BreakEvenable for TestStrategy {
2060        fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
2061            Ok(&self.break_even_points)
2062        }
2063    }
2064
2065    impl BasicAble for TestStrategy {
2066        fn get_option_basic_type(&self) -> HashSet<OptionBasicType<'_>> {
2067            HashSet::new()
2068        }
2069        fn get_underlying_price(&self) -> &Positive {
2070            &self.underlying_price
2071        }
2072    }
2073
2074    impl Strategies for TestStrategy {
2075        fn get_max_min_strikes(&self) -> Result<(Positive, Positive), StrategyError> {
2076            Ok((pos_or_panic!(90.0), pos_or_panic!(110.0)))
2077        }
2078    }
2079
2080    #[test]
2081    fn test_basic_range() {
2082        let strategy = TestStrategy::new(
2083            Positive::HUNDRED,
2084            vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
2085        );
2086        let (start, end) = strategy.get_range_to_show().unwrap();
2087        assert!(start < pos_or_panic!(90.0));
2088        assert!(end > pos_or_panic!(110.0));
2089    }
2090
2091    #[test]
2092    fn test_range_with_far_strikes() {
2093        let strategy = TestStrategy::new(
2094            Positive::HUNDRED,
2095            vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
2096        );
2097        let (start, end) = strategy.get_range_to_show().unwrap();
2098        assert!(start < pos_or_panic!(90.0));
2099        assert!(end > pos_or_panic!(110.0));
2100    }
2101
2102    #[test]
2103    fn test_range_with_underlying_outside_strikes() {
2104        let strategy = TestStrategy::new(
2105            pos_or_panic!(150.0),
2106            vec![pos_or_panic!(90.0), pos_or_panic!(110.0)],
2107        );
2108        let (_start, end) = strategy.get_range_to_show().unwrap();
2109        assert!(end > pos_or_panic!(150.0));
2110    }
2111}
2112
2113#[cfg(test)]
2114mod tests_range_of_profit {
2115    use super::*;
2116    use positive::pos_or_panic;
2117
2118    struct TestStrategy {
2119        break_even_points: Vec<Positive>,
2120    }
2121
2122    impl TestStrategy {
2123        fn new(break_even_points: Vec<Positive>) -> Self {
2124            Self { break_even_points }
2125        }
2126    }
2127
2128    impl Validable for TestStrategy {}
2129
2130    impl Positionable for TestStrategy {}
2131
2132    impl BreakEvenable for TestStrategy {
2133        fn get_break_even_points(&self) -> Result<&Vec<Positive>, StrategyError> {
2134            Ok(&self.break_even_points)
2135        }
2136    }
2137
2138    impl BasicAble for TestStrategy {}
2139
2140    impl Strategies for TestStrategy {}
2141
2142    #[test]
2143    fn test_no_break_even_points() {
2144        let strategy = TestStrategy::new(vec![]);
2145        assert!(strategy.get_range_of_profit().is_err());
2146    }
2147
2148    #[test]
2149    fn test_single_break_even_point() {
2150        let strategy = TestStrategy::new(vec![Positive::HUNDRED]);
2151        assert_eq!(strategy.get_range_of_profit().unwrap(), Positive::INFINITY);
2152    }
2153
2154    #[test]
2155    fn test_two_break_even_points() {
2156        let strategy = TestStrategy::new(vec![pos_or_panic!(90.0), pos_or_panic!(110.0)]);
2157        assert_eq!(strategy.get_range_of_profit().unwrap(), pos_or_panic!(20.0));
2158    }
2159
2160    #[test]
2161    fn test_multiple_break_even_points() {
2162        let strategy = TestStrategy::new(vec![
2163            pos_or_panic!(80.0),
2164            Positive::HUNDRED,
2165            pos_or_panic!(120.0),
2166        ]);
2167        assert_eq!(strategy.get_range_of_profit().unwrap(), pos_or_panic!(40.0));
2168    }
2169
2170    #[test]
2171    fn test_unordered_break_even_points() {
2172        let strategy = TestStrategy::new(vec![
2173            pos_or_panic!(120.0),
2174            pos_or_panic!(80.0),
2175            Positive::HUNDRED,
2176        ]);
2177        assert_eq!(strategy.get_range_of_profit().unwrap(), pos_or_panic!(40.0));
2178    }
2179}
2180
2181#[cfg(test)]
2182mod tests_strategy_methods {
2183    use super::*;
2184
2185    #[test]
2186    fn test_get_underlying_price_panic() {
2187        struct TestStrategy;
2188        impl Validable for TestStrategy {}
2189        impl Positionable for TestStrategy {}
2190        impl BreakEvenable for TestStrategy {}
2191        impl BasicAble for TestStrategy {}
2192        impl Strategies for TestStrategy {}
2193        let strategy = TestStrategy;
2194        let result = std::panic::catch_unwind(|| strategy.get_underlying_price());
2195        assert!(result.is_err());
2196    }
2197}
2198
2199#[cfg(test)]
2200mod tests_optimizable {
2201    use super::*;
2202    use positive::{pos_or_panic, spos};
2203
2204    use crate::chains::OptionData;
2205
2206    use rust_decimal_macros::dec;
2207
2208    struct TestOptimizableStrategy;
2209
2210    impl Validable for TestOptimizableStrategy {
2211        fn validate(&self) -> bool {
2212            true
2213        }
2214    }
2215
2216    impl Positionable for TestOptimizableStrategy {}
2217
2218    impl BreakEvenable for TestOptimizableStrategy {}
2219
2220    impl BasicAble for TestOptimizableStrategy {}
2221
2222    impl Strategies for TestOptimizableStrategy {}
2223
2224    impl Optimizable for TestOptimizableStrategy {
2225        type Strategy = Self;
2226    }
2227
2228    #[test]
2229    fn test_is_valid_long_option() {
2230        let strategy = TestOptimizableStrategy;
2231        let option_data = OptionData::new(
2232            Positive::HUNDRED,  // strike_price
2233            spos!(5.0),         // call_bid
2234            spos!(5.5),         // call_ask
2235            spos!(4.0),         // put_bid
2236            spos!(4.5),         // put_ask
2237            pos_or_panic!(0.2), // implied_volatility
2238            Some(dec!(0.5)),    // delta
2239            Some(dec!(0.3)),
2240            Some(dec!(0.3)),
2241            spos!(1000.0), // volume
2242            Some(100),     // open_interest
2243            None,
2244            None,
2245            None,
2246            None,
2247            None,
2248            None,
2249            None,
2250        );
2251        assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::All));
2252        assert!(strategy.is_valid_optimal_option(
2253            &option_data,
2254            &FindOptimalSide::Range(pos_or_panic!(90.0), pos_or_panic!(110.0))
2255        ));
2256    }
2257
2258    #[test]
2259    #[should_panic]
2260    fn test_is_valid_long_option_upper_panic() {
2261        let strategy = TestOptimizableStrategy;
2262        let option_data = OptionData::new(
2263            Positive::HUNDRED,  // strike_price
2264            spos!(5.0),         // call_bid
2265            spos!(5.5),         // call_ask
2266            spos!(4.0),         // put_bid
2267            spos!(4.5),         // put_ask
2268            pos_or_panic!(0.2), // implied_volatility
2269            Some(dec!(0.5)),    // delta
2270            Some(dec!(0.3)),
2271            Some(dec!(0.3)),
2272            spos!(1000.0), // volume
2273            Some(100),     // open_interest
2274            None,
2275            None,
2276            None,
2277            None,
2278            None,
2279            None,
2280            None,
2281        );
2282        assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Upper));
2283    }
2284
2285    #[test]
2286    #[should_panic]
2287    fn test_is_valid_long_option_lower_panic() {
2288        let strategy = TestOptimizableStrategy;
2289        let option_data = OptionData::new(
2290            Positive::HUNDRED,  // strike_price
2291            spos!(5.0),         // call_bid
2292            spos!(5.5),         // call_ask
2293            spos!(4.0),         // put_bid
2294            spos!(4.5),         // put_ask
2295            pos_or_panic!(0.2), // implied_volatility
2296            Some(dec!(0.5)),    // delta
2297            Some(dec!(0.3)),
2298            Some(dec!(0.3)),
2299            spos!(1000.0), // volume
2300            Some(100),     // open_interest
2301            None,
2302            None,
2303            None,
2304            None,
2305            None,
2306            None,
2307            None,
2308        );
2309        assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Lower));
2310    }
2311
2312    #[test]
2313    fn test_is_valid_short_option() {
2314        let strategy = TestOptimizableStrategy;
2315        let option_data = OptionData::new(
2316            Positive::HUNDRED,  // strike_price
2317            spos!(5.0),         // call_bid
2318            spos!(5.5),         // call_ask
2319            spos!(4.0),         // put_bid
2320            spos!(4.5),         // put_ask
2321            pos_or_panic!(0.2), // implied_volatility
2322            Some(dec!(0.5)),    // delta
2323            Some(dec!(0.3)),
2324            Some(dec!(0.3)),
2325            spos!(1000.0), // volume
2326            Some(100),     // open_interest
2327            None,
2328            None,
2329            None,
2330            None,
2331            None,
2332            None,
2333            None,
2334        );
2335        assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::All));
2336        assert!(strategy.is_valid_optimal_option(
2337            &option_data,
2338            &FindOptimalSide::Range(pos_or_panic!(90.0), pos_or_panic!(110.0))
2339        ));
2340    }
2341
2342    #[test]
2343    #[should_panic]
2344    fn test_is_valid_short_option_upper_panic() {
2345        let strategy = TestOptimizableStrategy;
2346        let option_data = OptionData::new(
2347            Positive::HUNDRED,  // strike_price
2348            spos!(5.0),         // call_bid
2349            spos!(5.5),         // call_ask
2350            spos!(4.0),         // put_bid
2351            spos!(4.5),         // put_ask
2352            pos_or_panic!(0.2), // implied_volatility
2353            Some(dec!(0.5)),    // delta
2354            Some(dec!(0.3)),
2355            Some(dec!(0.3)),
2356            spos!(1000.0), // volume
2357            Some(100),     // open_interest
2358            None,
2359            None,
2360            None,
2361            None,
2362            None,
2363            None,
2364            None,
2365        );
2366        assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Upper));
2367    }
2368
2369    #[test]
2370    #[should_panic]
2371    fn test_is_valid_short_option_lower_panic() {
2372        let strategy = TestOptimizableStrategy;
2373        let option_data = OptionData::new(
2374            Positive::HUNDRED,  // strike_price
2375            spos!(5.0),         // call_bid
2376            spos!(5.5),         // call_ask
2377            spos!(4.0),         // put_bid
2378            spos!(4.5),         // put_ask
2379            pos_or_panic!(0.2), // implied_volatility
2380            Some(dec!(0.5)),    // delta
2381            Some(dec!(0.3)),
2382            Some(dec!(0.3)),
2383            spos!(1000.0), // volume
2384            Some(100),     // open_interest
2385            None,
2386            None,
2387            None,
2388            None,
2389            None,
2390            None,
2391            None,
2392        );
2393        assert!(strategy.is_valid_optimal_option(&option_data, &FindOptimalSide::Lower));
2394    }
2395}
2396
2397#[cfg(test)]
2398mod tests_strategy_net_operations {
2399    use super::*;
2400
2401    use crate::model::position::Position;
2402    use crate::model::types::{OptionStyle, Side};
2403    use crate::model::utils::create_sample_option_simplest;
2404
2405    use chrono::{TimeZone, Utc};
2406    use positive::pos_or_panic;
2407
2408    struct TestStrategy {
2409        positions: Vec<Position>,
2410    }
2411
2412    impl TestStrategy {
2413        fn new() -> Self {
2414            Self {
2415                positions: Vec::new(),
2416            }
2417        }
2418    }
2419
2420    impl Validable for TestStrategy {}
2421
2422    impl Positionable for TestStrategy {
2423        fn add_position(&mut self, position: &Position) -> Result<(), PositionError> {
2424            self.positions.push(position.clone());
2425            Ok(())
2426        }
2427
2428        fn get_positions(&self) -> Result<Vec<&Position>, PositionError> {
2429            Ok(self.positions.iter().collect())
2430        }
2431    }
2432
2433    impl BreakEvenable for TestStrategy {}
2434
2435    impl BasicAble for TestStrategy {}
2436
2437    impl Strategies for TestStrategy {}
2438
2439    #[test]
2440    fn test_net_cost_calculation() {
2441        let mut strategy = TestStrategy::new();
2442        let option_long = create_sample_option_simplest(OptionStyle::Call, Side::Long);
2443        let option_short = create_sample_option_simplest(OptionStyle::Call, Side::Short);
2444
2445        let position_long = Position::new(
2446            option_long,
2447            Positive::ONE,
2448            Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
2449            Positive::ONE,
2450            pos_or_panic!(0.5),
2451            None,
2452            None,
2453        );
2454        let position_short = Position::new(
2455            option_short,
2456            Positive::ONE,
2457            Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
2458            Positive::ONE,
2459            pos_or_panic!(0.5),
2460            None,
2461            None,
2462        );
2463
2464        strategy.add_position(&position_long).unwrap();
2465        strategy.add_position(&position_short).unwrap();
2466
2467        let result = strategy.get_net_cost().unwrap();
2468        assert!(result > Decimal::ZERO);
2469    }
2470
2471    #[test]
2472    fn test_net_premium_received_calculation() {
2473        let mut strategy = TestStrategy::new();
2474        let option_long = create_sample_option_simplest(OptionStyle::Call, Side::Long);
2475        let option_short = create_sample_option_simplest(OptionStyle::Call, Side::Short);
2476
2477        let fixed_date = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
2478        let position_long = Position::new(
2479            option_long,
2480            Positive::ONE,
2481            fixed_date,
2482            Positive::ONE,
2483            pos_or_panic!(0.5),
2484            None,
2485            None,
2486        );
2487        let position_short = Position::new(
2488            option_short,
2489            Positive::ONE,
2490            fixed_date,
2491            Positive::ONE,
2492            pos_or_panic!(0.5),
2493            None,
2494            None,
2495        );
2496
2497        strategy.add_position(&position_long).unwrap();
2498        strategy.add_position(&position_short).unwrap();
2499
2500        let result = strategy.get_net_premium_received().unwrap();
2501        assert_eq!(result, Positive::ZERO);
2502    }
2503
2504    #[test]
2505    fn test_fees_calculation() {
2506        let mut strategy = TestStrategy::new();
2507        let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
2508        let fixed_date = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
2509        let position = Position::new(
2510            option,
2511            Positive::ONE,
2512            fixed_date,
2513            Positive::ONE,
2514            pos_or_panic!(0.5),
2515            None,
2516            None,
2517        );
2518
2519        strategy.add_position(&position).unwrap();
2520
2521        let result = strategy.get_fees().unwrap();
2522        assert!(result > Positive::ZERO);
2523    }
2524}