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}