Skip to main content

optionstratlib/chains/
optiondata.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/3/25
5******************************************************************************/
6use crate::chains::utils::{OptionDataPriceParams, default_empty_string, empty_string_round_to_2};
7use crate::chains::{DeltasInStrike, OptionsInStrike};
8use crate::error::ChainError;
9use crate::error::chains::OptionDataErrorKind;
10use crate::greeks::{delta, gamma};
11use crate::model::Position;
12use crate::strategies::{BasicAble, FindOptimalSide};
13use crate::{ExpirationDate, OptionStyle, Options, Side};
14use chrono::{DateTime, Utc};
15use positive::Positive;
16use rust_decimal::Decimal;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::cmp::Ordering;
20use std::fmt;
21use tracing::{debug, error, trace};
22use utoipa::ToSchema;
23
24/// Struct representing a row in an option chain with detailed pricing and analytics data.
25///
26/// This struct encapsulates the complete market data for an options contract at a specific
27/// strike price, including bid/ask prices for both call and put options, implied volatility,
28/// the Greeks (delta, gamma), volume, and open interest. It provides all the essential
29/// information needed for options analysis and trading decision-making.
30///
31/// # Fields
32///
33/// * `strike_price` - The strike price of the option, represented as a positive floating-point number.
34/// * `call_bid` - The bid price for the call option, represented as an optional positive floating-point number.
35///   May be `None` if market data is unavailable.
36/// * `call_ask` - The ask price for the call option, represented as an optional positive floating-point number.
37///   May be `None` if market data is unavailable.
38/// * `put_bid` - The bid price for the put option, represented as an optional positive floating-point number.
39///   May be `None` if market data is unavailable.
40/// * `put_ask` - The ask price for the put option, represented as an optional positive floating-point number.
41///   May be `None` if market data is unavailable.
42/// * `call_middle` - The mid-price between call bid and ask, represented as an optional positive floating-point number.
43///   May be `None` if underlying bid/ask data is unavailable.
44/// * `put_middle` - The mid-price between put bid and ask, represented as an optional positive floating-point number.
45///   May be `None` if underlying bid/ask data is unavailable.
46/// * `implied_volatility` - The implied volatility of the option, represented as an optional positive floating-point number.
47///   May be `None` if it cannot be calculated from available market data.
48/// * `delta_call` - The delta of the call option, represented as an optional decimal number.
49///   Measures the rate of change of the option price with respect to changes in the underlying asset price.
50/// * `delta_put` - The delta of the put option, represented as an optional decimal number.
51///   Measures the rate of change of the option price with respect to changes in the underlying asset price.
52/// * `gamma` - The gamma of the option, represented as an optional decimal number.
53///   Measures the rate of change of delta with respect to changes in the underlying asset price.
54/// * `volume` - The trading volume of the option, represented as an optional positive floating-point number.
55///   May be `None` if data is not available.
56/// * `open_interest` - The open interest of the option, represented as an optional unsigned integer.
57///   Represents the total number of outstanding option contracts that have not been settled.
58/// * `options` - An optional boxed reference to a `FourOptions` struct that may contain
59///   the actual option contracts represented by this data. This field is not serialized.
60///
61/// # Usage
62///
63/// This struct is typically used to represent a single row in an option chain table,
64/// providing comprehensive market data for options at a specific strike price. It's
65/// useful for option pricing models, strategy analysis, and trading applications.
66///
67/// # Serialization
68///
69/// This struct implements Serialize and Deserialize traits, with fields that are `None`
70/// being skipped during serialization to produce more compact JSON output.
71#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
72pub struct OptionData {
73    /// The strike price of the option, represented as a positive floating-point number.
74    #[serde(rename = "strike_price")]
75    pub strike_price: Positive,
76
77    /// The bid price for the call option. May be `None` if market data is unavailable.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub call_bid: Option<Positive>,
80
81    /// The ask price for the call option. May be `None` if market data is unavailable.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub call_ask: Option<Positive>,
84
85    /// The bid price for the put option. May be `None` if market data is unavailable.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub put_bid: Option<Positive>,
88
89    /// The ask price for the put option. May be `None` if market data is unavailable.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub put_ask: Option<Positive>,
92
93    /// The mid-price between call bid and ask. Calculated as (bid + ask) / 2.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub call_middle: Option<Positive>,
96
97    /// The mid-price between put bid and ask. Calculated as (bid + ask) / 2.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub put_middle: Option<Positive>,
100
101    /// The `implied_volatility` field represents the implied volatility value, which is of type `Positive`.
102    /// This value is intended to store a positive number, as enforced by the `Positive` type.
103    ///
104    /// # Attributes
105    /// - `#[serde(default)]`: This attribute ensures that the field is given a default value during
106    ///   deserialization if the value is not provided. The default implementation for the `Positive` type
107    ///   is expected to supply an appropriate default positive value.
108    ///
109    /// # Type
110    /// - `Positive`: A type that ensures the value it holds is strictly positive.
111    ///
112    /// # Usage
113    /// This field is commonly utilized in contexts where implied volatility is required, such as
114    /// in financial modeling or derivative pricing calculations. Deserialization will automatically
115    /// manage its default value if it is absent from the data source.
116    #[serde(default)]
117    pub implied_volatility: Positive,
118
119    /// The delta of the call option, measuring price sensitivity to underlying changes.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub delta_call: Option<Decimal>,
122
123    /// The delta of the put option, measuring price sensitivity to underlying changes.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub delta_put: Option<Decimal>,
126
127    /// The gamma of the option, measuring the rate of change in delta.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub gamma: Option<Decimal>,
130
131    /// The trading volume of the option, indicating market activity.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub volume: Option<Positive>,
134
135    /// The open interest, representing the number of outstanding contracts.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub open_interest: Option<u64>,
138    /// The symbol of the underlying asset.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub symbol: Option<String>,
141    /// The expiration date of the option contract.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub expiration_date: Option<ExpirationDate>,
144    /// The price of the underlying asset.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub underlying_price: Option<Box<Positive>>,
147    /// The risk-free interest rate used for option pricing.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub risk_free_rate: Option<Decimal>,
150    /// The dividend yield of the underlying asset.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub dividend_yield: Option<Positive>,
153    /// The epic identifier for the option contract, used for trading platforms.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub epic: Option<String>,
156    /// Additional fields that may be included in the option data.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub extra_fields: Option<Value>,
159}
160
161impl OptionData {
162    /// Creates a new instance of `OptionData` with the given option market parameters.
163    ///
164    /// This constructor creates an `OptionData` structure that represents a single row in an options chain,
165    /// containing market data for both call and put options at a specific strike price. The middle prices
166    /// for calls and puts are initially set to `None` and can be calculated later if needed.
167    ///
168    /// # Parameters
169    ///
170    /// * `strike_price` - The strike price of the option contract, guaranteed to be positive.
171    /// * `call_bid` - The bid price for the call option. `None` if market data is unavailable.
172    /// * `call_ask` - The ask price for the call option. `None` if market data is unavailable.
173    /// * `put_bid` - The bid price for the put option. `None` if market data is unavailable.
174    /// * `put_ask` - The ask price for the put option. `None` if market data is unavailable.
175    /// * `implied_volatility` - The implied volatility derived from option prices. `None` if not calculable.
176    /// * `delta_call` - The delta of the call option, measuring price sensitivity to underlying changes.
177    /// * `delta_put` - The delta of the put option, measuring price sensitivity to underlying changes.
178    /// * `gamma` - The gamma of the option, measuring the rate of change in delta.
179    /// * `volume` - The trading volume of the option, indicating market activity.
180    /// * `open_interest` - The number of outstanding option contracts that have not been settled.
181    ///
182    /// # Returns
183    ///
184    /// A new `OptionData` instance with the specified parameters and with `call_middle`, `put_middle`,
185    /// and `options` fields initialized to `None`.
186    ///
187    /// # Note
188    ///
189    /// This function allows many optional parameters to accommodate scenarios where not all market data
190    /// is available from data providers.
191    #[allow(clippy::too_many_arguments)]
192    #[must_use]
193    pub fn new(
194        strike_price: Positive,
195        call_bid: Option<Positive>,
196        call_ask: Option<Positive>,
197        put_bid: Option<Positive>,
198        put_ask: Option<Positive>,
199        implied_volatility: Positive,
200        delta_call: Option<Decimal>,
201        delta_put: Option<Decimal>,
202        gamma: Option<Decimal>,
203        volume: Option<Positive>,
204        open_interest: Option<u64>,
205        symbol: Option<String>,
206        expiration_date: Option<ExpirationDate>,
207        underlying_price: Option<Box<Positive>>,
208        risk_free_rate: Option<Decimal>,
209        dividend_yield: Option<Positive>,
210        epic: Option<String>,
211        extra_fields: Option<Value>,
212    ) -> Self {
213        OptionData {
214            strike_price,
215            call_bid,
216            call_ask,
217            put_bid,
218            put_ask,
219            call_middle: None,
220            put_middle: None,
221            implied_volatility,
222            delta_call,
223            delta_put,
224            gamma,
225            volume,
226            open_interest,
227            symbol,
228            expiration_date,
229            underlying_price,
230            risk_free_rate,
231            dividend_yield,
232            epic,
233            extra_fields,
234        }
235    }
236
237    /// Calculates and returns the call spread as a `Positive` value if both call bid and call ask
238    /// prices are available. Otherwise, returns `None`.
239    ///
240    /// The call spread is determined as the absolute difference between the call ask price
241    /// and the call bid price.
242    ///
243    /// # Returns
244    /// - `Some(Positive)` if both `call_bid` and `call_ask` are present.
245    /// - `None` if either `call_bid` or `call_ask` is `None`.
246    ///
247    /// # Note
248    /// The `Positive` type is assumed to enforce non-negative values for correctness.
249    #[inline]
250    #[must_use]
251    pub fn get_call_spread(&self) -> Option<Positive> {
252        match (self.call_bid, self.call_ask) {
253            (Some(call_bid), Some(call_ask)) => {
254                let spread = (call_ask.to_dec() - call_bid.to_dec()).abs();
255                Positive::new_decimal(spread).ok()
256            }
257            _ => None,
258        }
259    }
260
261    /// Calculates the percentage call spread based on the bid and ask prices of a call option.
262    ///
263    /// # Formula
264    /// The call spread percentage is computed using the absolute difference between the call ask
265    /// price and the call bid price, divided by the mid price of the call. Mathematically:
266    ///
267    /// ```text
268    /// Spread% = |CallAsk - CallBid| / ((CallAsk + CallBid) / 2)
269    /// ```
270    ///
271    /// # Returns
272    /// - Returns `Some(Positive)` if both `call_bid` and `call_ask` values are available and non-negative.
273    /// - Returns `None` if either `call_bid` or `call_ask` is missing.
274    ///
275    /// # Notes
276    /// - The method assumes both `call_bid` and `call_ask` are non-negative.
277    /// - `Positive` is a custom type encapsulating non-negative values.
278    /// - The resulting percentage is always positive due to the use of `.abs()` for the spread calculation.
279    ///
280    /// # Parameters
281    /// None (relies on internal `self.call_bid` and `self.call_ask` properties).
282    ///
283    /// # Errors
284    /// This function does not return an error. It simply returns `None` if the calculation is not feasible.
285    #[inline]
286    #[must_use]
287    pub fn get_call_spread_per(&self) -> Option<Positive> {
288        match (self.call_bid, self.call_ask) {
289            (Some(call_bid), Some(call_ask)) => {
290                let spread = (call_ask.to_dec() - call_bid.to_dec()).abs();
291                let mid_price = (call_ask + call_bid) / 2.0;
292                Positive::new_decimal(spread).ok().map(|s| s / mid_price)
293            }
294            _ => None,
295        }
296    }
297
298    ///
299    /// Calculates and returns the spread between `put_bid` and `put_ask` if both values are present.
300    ///
301    /// The spread is calculated as the absolute difference between the `put_ask` price and the
302    /// `put_bid` price. The result is wrapped in a `Positive` type.
303    ///
304    /// # Returns
305    ///
306    /// - `Some(Positive)` if both `put_bid` and `put_ask` are `Some`, containing their calculated spread.
307    /// - `None` if either `put_bid` or `put_ask` is `None`.
308    ///
309    /// # Note
310    ///
311    /// The values of `put_bid` and `put_ask` must implement the `to_dec()` method
312    /// to convert to a numeric type for calculation purposes.
313    /// The spread is always represented as a positive value.
314    ///
315    #[inline]
316    #[must_use]
317    pub fn get_put_spread(&self) -> Option<Positive> {
318        match (self.put_bid, self.put_ask) {
319            (Some(put_bid), Some(put_ask)) => {
320                let spread = (put_ask.to_dec() - put_bid.to_dec()).abs();
321                Positive::new_decimal(spread).ok()
322            }
323            _ => None,
324        }
325    }
326
327    /// Calculates the percentage spread of the bid and ask prices for a put option.
328    ///
329    /// The function computes the absolute difference (spread) between the bid and ask prices.
330    /// It then divides this spread by the midpoint of the bid and ask prices to yield the
331    /// percentage spread. Only positive values are returned wrapped in the `Positive` type.
332    ///
333    /// # Returns
334    /// - `Some(Positive)` if both `put_bid` and `put_ask` are present, containing the percentage spread.
335    /// - `None` if either `put_bid` or `put_ask` is unavailable.
336    ///
337    /// # Assumptions
338    /// - `put_bid` and `put_ask` are non-negative.
339    /// - The `Positive` type is a wrapper that ensures the value is greater than zero.
340    ///
341    /// # Note
342    /// This function returns `None` if there are missing values for either `put_bid` or `put_ask`.
343    #[inline]
344    #[must_use]
345    pub fn get_put_spread_per(&self) -> Option<Positive> {
346        match (self.put_bid, self.put_ask) {
347            (Some(put_bid), Some(put_ask)) => {
348                let spread = (put_ask.to_dec() - put_bid.to_dec()).abs();
349                let mid_price = (put_ask + put_bid) / 2.0;
350                Positive::new_decimal(spread).ok().map(|s| s / mid_price)
351            }
352            _ => None,
353        }
354    }
355
356    /// Retrieves the implied volatility of the underlying asset or option.
357    ///
358    /// # Returns
359    ///
360    /// An `Option<Positive>` where:
361    /// - `Some(Positive)` contains the implied volatility if it is available.
362    /// - `None` if the implied volatility is not set or available.
363    ///
364    /// # Notes
365    ///
366    /// The implied volatility represents the market's forecast of a likely movement
367    /// in an asset's price and is often used in option pricing models.
368    ///
369    /// Ensure that the `Positive` type enforces constraints to prevent invalid values
370    /// such as negative volatility.
371    #[inline]
372    #[must_use]
373    pub fn get_volatility(&self) -> Positive {
374        self.implied_volatility
375    }
376
377    /// Sets the implied volatility for this option contract.
378    ///
379    /// # Arguments
380    /// * `volatility` - A positive decimal value representing the implied volatility.
381    #[inline]
382    pub fn set_volatility(&mut self, volatility: &Positive) {
383        self.implied_volatility = *volatility;
384    }
385
386    /// Sets additional pricing parameters for this option contract.
387    ///
388    /// This method updates the option data with the provided pricing parameters,
389    /// including underlying symbol, price, expiration date, risk-free rate, and dividend yield.
390    ///
391    /// # Arguments
392    /// * `params` - The pricing parameters to set.
393    pub fn set_extra_params(&mut self, params: OptionDataPriceParams) {
394        if let Some(symbol) = params.underlying_symbol {
395            self.symbol = Some(symbol);
396        };
397
398        if let Some(expiration_date) = params.expiration_date {
399            self.expiration_date = Some(expiration_date);
400        };
401
402        if let Some(underlying_price) = params.underlying_price {
403            self.underlying_price = Some(underlying_price);
404        };
405
406        if let Some(risk_free_rate) = params.risk_free_rate {
407            self.risk_free_rate = Some(risk_free_rate);
408        };
409
410        if let Some(dividend_yield) = params.dividend_yield {
411            self.dividend_yield = Some(dividend_yield);
412        };
413    }
414
415    /// Validates the option data to ensure it meets the required criteria for calculations.
416    ///
417    /// This method performs a series of validation checks to ensure that the option data
418    /// is complete and valid for further processing or analysis. It verifies:
419    /// 1. The strike price is not zero
420    /// 2. Implied volatility is present
421    /// 3. Call option data is valid (via `valid_call()`)
422    /// 4. Put option data is valid (via `valid_put()`)
423    ///
424    /// Each validation failure is logged as an error for debugging and troubleshooting.
425    ///
426    /// # Returns
427    ///
428    /// * `true` - If all validation checks pass, indicating the option data is valid
429    /// * `false` - If any validation check fails, indicating the option data is incomplete or invalid
430    pub fn validate(&self) -> bool {
431        if self.strike_price == Positive::ZERO {
432            error!("Error: Strike price cannot be zero");
433            return false;
434        }
435        if !self.valid_call() || !self.valid_put() {
436            error!(
437                "Error: No valid prices for call or put options {} Deltas C {:?} P {:?}",
438                self.strike_price, self.delta_call, self.delta_put
439            );
440            return false;
441        }
442        true
443    }
444
445    /// Retrieves the strike price.
446    ///
447    /// This method returns the strike price associated with the object. The strike price
448    /// is represented as a [`Positive`] value, ensuring that it is always greater than zero.
449    ///
450    /// # Returns
451    /// * [`Positive`] - The strike price of the object.
452    ///
453    /// # Notes
454    /// The method assumes that the strike price has been properly initialized and is
455    /// a valid positive number.
456    ///
457    /// [`Positive`]: struct.Positive.html
458    #[inline]
459    #[must_use]
460    pub fn strike(&self) -> Positive {
461        self.strike_price
462    }
463
464    /// Checks if this option data contains valid call option information.
465    ///
466    /// A call option is considered valid when all required data is present:
467    /// * The strike price is greater than zero
468    /// * Implied volatility is available
469    /// * Both bid and ask prices for the call option are available
470    ///
471    /// # Returns
472    ///
473    /// `true` if all required call option data is present, `false` otherwise.
474    #[inline]
475    pub(crate) fn valid_call(&self) -> bool {
476        self.strike_price > Positive::ZERO && self.call_bid.is_some() && self.call_ask.is_some()
477    }
478
479    /// Checks if this option data contains valid put option information.
480    ///
481    /// A put option is considered valid when all required data is present:
482    /// * The strike price is greater than zero
483    /// * Implied volatility is available
484    /// * Both bid and ask prices for the put option are available
485    ///
486    /// # Returns
487    ///
488    /// `true` if all required put option data is present, `false` otherwise.
489    #[inline]
490    pub(crate) fn valid_put(&self) -> bool {
491        self.strike_price > Positive::ZERO && self.put_bid.is_some() && self.put_ask.is_some()
492    }
493
494    /// Retrieves the price at which a call option can be purchased.
495    ///
496    /// This method returns the ask price for a call option, which is the price
497    /// a buyer would pay to purchase the call option.
498    ///
499    /// # Returns
500    ///
501    /// The call option's ask price as a `Positive` value, or `None` if the price is unavailable.
502    #[inline]
503    #[must_use]
504    pub fn get_call_buy_price(&self) -> Option<Positive> {
505        self.call_ask
506    }
507
508    /// Retrieves the price at which a call option can be sold.
509    ///
510    /// This method returns the bid price for a call option, which is the price
511    /// a seller would receive when selling the call option.
512    ///
513    /// # Returns
514    ///
515    /// The call option's bid price as a `Positive` value, or `None` if the price is unavailable.
516    #[inline]
517    #[must_use]
518    pub fn get_call_sell_price(&self) -> Option<Positive> {
519        self.call_bid
520    }
521
522    /// Retrieves the price at which a put option can be purchased.
523    ///
524    /// This method returns the ask price for a put option, which is the price
525    /// a buyer would pay to purchase the put option.
526    ///
527    /// # Returns
528    ///
529    /// The put option's ask price as a `Positive` value, or `None` if the price is unavailable.
530    #[inline]
531    #[must_use]
532    pub fn get_put_buy_price(&self) -> Option<Positive> {
533        self.put_ask
534    }
535
536    /// Retrieves the price at which a put option can be sold.
537    ///
538    /// This method returns the bid price for a put option, which is the price
539    /// a seller would receive when selling the put option.
540    ///
541    /// # Returns
542    ///
543    /// The put option's bid price as a `Positive` value, or `None` if the price is unavailable.
544    #[inline]
545    #[must_use]
546    pub fn get_put_sell_price(&self) -> Option<Positive> {
547        self.put_bid
548    }
549
550    /// Checks if any of the bid or ask prices for call or put options are None.
551    ///
552    /// This function returns `true` if any of the `call_bid`, `call_ask`, `put_bid`, or `put_ask`
553    /// fields of the `OptionData` struct are `None`, indicating missing price information.
554    /// It returns `false` if all four fields have valid price data.
555    ///
556    #[inline]
557    #[must_use]
558    pub fn some_price_is_none(&self) -> bool {
559        self.call_bid.is_none()
560            || self.call_ask.is_none()
561            || self.put_bid.is_none()
562            || self.put_ask.is_none()
563    }
564
565    /// Creates an option contract based on provided parameters and existing data.
566    ///
567    /// This method constructs a new `Options` instance by combining information from
568    /// the current object with the provided pricing parameters. It handles the logic
569    /// for determining the correct implied volatility to use, either from the provided
570    /// parameters or from the object's stored value.
571    ///
572    /// # Parameters
573    ///
574    /// * `price_params` - A reference to `OptionDataPriceParams` containing essential pricing
575    ///   information such as expiration date, underlying price, and risk-free rate.
576    /// * `side` - Defines the directional exposure of the option (Long or Short).
577    /// * `option_style` - Specifies the style of the option (Call or Put).
578    ///
579    /// # Returns
580    ///
581    /// * `Result<Options, ChainError>` - An `Options` instance if successful, or a `ChainError`
582    ///   if required data such as implied volatility is missing.
583    ///
584    /// # Errors
585    ///
586    /// Returns `ChainError::invalid_volatility` if neither the input parameters nor the object
587    /// itself contains a valid implied volatility value.
588    pub(super) fn get_option(
589        &self,
590        side: Side,
591        option_style: OptionStyle,
592    ) -> Result<Options, ChainError> {
593        let mut option = Options::try_from(self)
594            .map_err(|e| ChainError::OptionDataError(OptionDataErrorKind::Other(e.to_string())))?;
595        option.side = side;
596        option.option_style = option_style;
597        Ok(option)
598    }
599
600    /// Creates an option contract for implied volatility calculation with specified parameters.
601    ///
602    /// This method constructs a new European-style option contract with the given parameters
603    /// to be used in implied volatility calculations or pricing models. It initializes a properly
604    /// configured `Options` instance with all necessary values for financial calculations.
605    ///
606    /// # Parameters
607    ///
608    /// * `price_params` - Contains core pricing parameters including:
609    ///   - `expiration_date` - When the option expires
610    ///   - `underlying_price` - Current market price of the underlying asset
611    ///   - `risk_free_rate` - The risk-free interest rate used in pricing models
612    ///   - `dividend_yield` - The dividend yield of the underlying asset
613    ///
614    /// * `side` - Specifies whether this is a Long or Short position, determining
615    ///   the directional exposure of the option
616    ///
617    /// * `option_style` - Determines whether this is a Call or Put option, defining
618    ///   the fundamental right the contract provides
619    ///
620    /// * `initial_iv` - The initial implied volatility estimate to use for the option,
621    ///   which will be the starting point for IV calculation algorithms
622    ///
623    /// # Returns
624    ///
625    /// Returns a `Result` containing either:
626    /// * An `Options` instance configured with the specified parameters
627    /// * A `ChainError` if there was a problem creating the option
628    ///
629    #[allow(dead_code)]
630    fn get_option_for_iv(
631        &self,
632        side: Side,
633        option_style: OptionStyle,
634        initial_iv: Positive,
635    ) -> Result<Options, ChainError> {
636        let mut option = self.get_option(side, option_style)?;
637        let _ = option.set_implied_volatility(&initial_iv);
638        Ok(option)
639    }
640
641    /// Retrieves a `Position` based on the provided parameters, calculating the option premium using the Black-Scholes model.
642    ///
643    /// This method fetches an option based on the provided parameters, calculates its theoretical
644    /// premium using the Black-Scholes model, and constructs a `Position` struct containing the option
645    /// details, premium, opening date, and associated fees.
646    ///
647    /// # Arguments
648    ///
649    /// * `price_params` - Option pricing parameters encapsulated in `OptionDataPriceParams`.
650    /// * `side` - The side of the option, either `Side::Long` or `Side::Short`.
651    /// * `option_style` - The style of the option, either `OptionStyle::Call` or `OptionStyle::Put`.
652    /// * `date` - An optional `DateTime<Utc>` representing the opening date of the position.
653    ///   If `None`, the current UTC timestamp is used.
654    /// * `open_fee` - An optional `Positive` value representing the opening fee for the position.
655    ///   If `None`, defaults to `Positive::ZERO`.
656    /// * `close_fee` - An optional `Positive` value representing the closing fee for the position.
657    ///   If `None`, defaults to `Positive::ZERO`.
658    ///
659    /// # Returns
660    ///
661    /// * `Result<Position, ChainError>` - A `Result` containing the constructed `Position` on success,
662    ///   or a `ChainError` if any error occurred during option retrieval or premium calculation.
663    ///
664    /// # Errors
665    ///
666    /// This method can return a `ChainError` if:
667    ///
668    /// * The underlying option cannot be retrieved based on the provided parameters.
669    /// * The Black-Scholes model fails to calculate a valid option premium.
670    pub fn get_position(
671        &self,
672        side: Side,
673        option_style: OptionStyle,
674        date: Option<DateTime<Utc>>,
675        open_fee: Option<Positive>,
676        close_fee: Option<Positive>,
677    ) -> Result<Position, ChainError> {
678        let option = self.get_option(side, option_style)?;
679        let premium = match (side, option_style) {
680            (Side::Long, OptionStyle::Call) => self.get_call_buy_price(),
681            (Side::Short, OptionStyle::Call) => self.get_call_sell_price(),
682            (Side::Long, OptionStyle::Put) => self.get_put_buy_price(),
683            (Side::Short, OptionStyle::Put) => self.get_put_sell_price(),
684        };
685        let premium = match premium {
686            Some(premium) => premium,
687            None => {
688                let premium_dec = option.calculate_price_black_scholes()?.abs();
689                Positive::new_decimal(premium_dec)?
690            }
691        };
692        let date = if let Some(date) = date {
693            date
694        } else {
695            Utc::now()
696        };
697        let open_fee = if let Some(open_fee) = open_fee {
698            open_fee
699        } else {
700            Positive::ZERO
701        };
702        let close_fee = if let Some(close_fee) = close_fee {
703            close_fee
704        } else {
705            Positive::ZERO
706        };
707
708        Ok(Position::new(
709            option,
710            premium,
711            date,
712            open_fee,
713            close_fee,
714            self.epic.clone(),
715            self.extra_fields.clone(),
716        ))
717    }
718
719    pub(super) fn get_options_in_strike(&self) -> Result<OptionsInStrike, ChainError> {
720        let mut option: Options = self.get_option(Side::Long, OptionStyle::Call)?;
721        option.option_style = OptionStyle::Call;
722        option.side = Side::Long;
723        let long_call = option.clone();
724        option.side = Side::Short;
725        let short_call = option.clone();
726        option.option_style = OptionStyle::Put;
727        let short_put = option.clone();
728        option.side = Side::Long;
729        let long_put = option.clone();
730        Ok(OptionsInStrike {
731            long_call,
732            short_call,
733            long_put,
734            short_put,
735        })
736    }
737
738    /// Calculates and sets the bid and ask prices for call and put options.
739    ///
740    /// This method computes the theoretical prices for both call and put options using the
741    /// Black-Scholes pricing model, and then stores these values in the appropriate fields.
742    /// After calculating the individual bid and ask prices, it also computes and sets the
743    /// mid-prices by calling the `set_mid_prices` method.
744    ///
745    /// # Parameters
746    ///
747    /// * `price_params` - A reference to `OptionDataPriceParams` containing the necessary
748    ///   parameters for option pricing, such as underlying price, volatility, risk-free rate,
749    ///   expiration date, and dividend yield.
750    ///
751    /// * `refresh` - A boolean flag indicating whether to force recalculation of option
752    ///   contracts even if they already exist. When set to `true`, the method will recreate
753    ///   the option contracts before calculating prices.
754    ///
755    /// # Returns
756    ///
757    /// * `Result<(), ChainError>` - Returns `Ok(())` if prices are successfully calculated
758    ///   and set, or a `ChainError` if any error occurs during the process.
759    ///
760    /// # Side Effects
761    ///
762    /// Sets the following fields in the struct:
763    /// * `call_ask` - The ask price for the call option
764    /// * `call_bid` - The bid price for the call option
765    /// * `put_ask` - The ask price for the put option
766    /// * `put_bid` - The bid price for the put option
767    /// * `call_middle` and `put_middle` - The mid-prices calculated via `set_mid_prices()`
768    ///
769    /// # Errors
770    ///
771    /// May return:
772    /// * `ChainError` variants if there are issues creating the options contracts
773    /// * Errors propagated from the Black-Scholes calculation functions
774    pub fn calculate_prices(&mut self, spread: Option<Positive>) -> Result<(), ChainError> {
775        let call_option = self.get_option(Side::Long, OptionStyle::Call)?;
776        match (
777            call_option.calculate_price_black_scholes(),
778            spread.is_some(),
779        ) {
780            (Ok(price), true) => {
781                if price.is_sign_positive() {
782                    self.call_middle = Positive::new_decimal(price).ok();
783                    if let Some(s) = spread {
784                        self.apply_spread(s, 2);
785                    }
786                }
787            }
788            (Ok(price), false) => {
789                self.call_middle = Positive::new_decimal(price).ok();
790                self.call_ask = self.call_middle;
791                self.call_bid = self.call_middle;
792            }
793            _ => {
794                debug!("calculate_prices: Failed to calculate call option price");
795                self.call_middle = None;
796                self.call_ask = None;
797                self.call_bid = None;
798            }
799        };
800
801        let put_option = self.get_option(Side::Long, OptionStyle::Put)?;
802        match (put_option.calculate_price_black_scholes(), spread.is_some()) {
803            (Ok(price), true) => {
804                if price.is_sign_positive() {
805                    self.put_middle = Positive::new_decimal(price).ok();
806                    if let Some(s) = spread {
807                        self.apply_spread(s, 2);
808                    }
809                }
810            }
811            (Ok(price), false) => {
812                self.put_middle = Positive::new_decimal(price).ok();
813                self.put_ask = self.put_middle;
814                self.put_bid = self.put_middle;
815            }
816            _ => {
817                debug!("calculate_prices: Failed to calculate put option price");
818                self.put_middle = None;
819                self.put_ask = None;
820                self.put_bid = None;
821            }
822        };
823
824        Ok(())
825    }
826
827    /// Applies a spread to the bid and ask prices of call and put options, then recalculates mid prices.
828    ///
829    /// This method adjusts the bid and ask prices by half of the specified spread value,
830    /// subtracting from bid prices and adding to ask prices. It also ensures that all prices
831    /// are rounded to the specified number of decimal places. If any price becomes negative
832    /// after applying the spread, it is set to `None`.
833    ///
834    /// # Arguments
835    ///
836    /// * `spread` - A positive decimal value representing the total spread to apply
837    /// * `decimal_places` - The number of decimal places to round the adjusted prices to
838    ///
839    /// # Inner Function
840    ///
841    /// The method contains an inner function `round_to_decimal` that handles the rounding
842    /// of prices after applying a shift (half the spread).
843    ///
844    /// # Side Effects
845    ///
846    /// * Updates `call_ask`, `call_bid`, `put_ask`, and `put_bid` fields with adjusted values
847    /// * Sets adjusted prices to `None` if they would become negative after applying the spread
848    /// * Calls `set_mid_prices()` to recalculate the mid prices based on the new bid/ask values
849    pub fn apply_spread(&mut self, spread: Positive, decimal_places: u32) {
850        let half_spread: Decimal = (spread / Positive::TWO).into();
851
852        match (self.call_ask, self.call_bid, self.call_middle) {
853            (_, _, Some(call_middle)) => {
854                if call_middle > spread {
855                    self.call_ask = Some((call_middle + half_spread).round_to(decimal_places));
856                    self.call_bid = Some(
857                        call_middle
858                            .sub_or_zero(&half_spread)
859                            .round_to(decimal_places),
860                    );
861                } else {
862                    trace!(
863                        "apply_spread: Call middle price is not greater than spread, cannot apply spread"
864                    );
865                    self.call_ask = None;
866                    self.call_bid = None;
867                    self.call_middle = None;
868                }
869            }
870            (Some(call_ask), Some(call_bid), None) => {
871                trace!(
872                    "apply_spread: Call middle price is None; recomputing from bid/ask after applying spread"
873                );
874                let new_ask = (call_ask + half_spread).round_to(decimal_places);
875                let new_bid = call_bid.sub_or_zero(&half_spread).round_to(decimal_places);
876                self.call_ask = Some(new_ask);
877                self.call_bid = Some(new_bid);
878                self.call_middle =
879                    Some(((new_ask + new_bid) / Positive::TWO).round_to(decimal_places));
880            }
881            _ => {
882                trace!("apply_spread: Missing call ask or bid prices, cannot apply spread");
883                self.call_ask = None;
884                self.call_bid = None;
885            }
886        }
887
888        match (self.put_ask, self.put_bid, self.put_middle) {
889            (_, _, Some(put_middle)) => {
890                if put_middle > spread {
891                    self.put_ask = Some((put_middle + half_spread).round_to(decimal_places));
892                    self.put_bid = Some(
893                        put_middle
894                            .sub_or_zero(&half_spread)
895                            .round_to(decimal_places),
896                    );
897                } else {
898                    trace!(
899                        "apply_spread: Put middle price is not greater than spread, cannot apply spread"
900                    );
901                    self.put_ask = None;
902                    self.put_bid = None;
903                    self.put_middle = None;
904                }
905            }
906            (Some(put_ask), Some(put_bid), None) => {
907                trace!(
908                    "apply_spread: Put middle price is None; recomputing from bid/ask after applying spread"
909                );
910                let new_ask = (put_ask + half_spread).round_to(decimal_places);
911                let new_bid = put_bid.sub_or_zero(&half_spread).round_to(decimal_places);
912                self.put_ask = Some(new_ask);
913                self.put_bid = Some(new_bid);
914                self.put_middle =
915                    Some(((new_ask + new_bid) / Positive::TWO).round_to(decimal_places));
916            }
917            _ => {
918                trace!("apply_spread: Missing put ask or bid prices, cannot apply spread");
919                self.put_ask = None;
920                self.put_bid = None;
921            }
922        }
923    }
924
925    /// Calculates the delta of the option and stores it in the option data.
926    ///
927    /// Delta measures the rate of change of the option price with respect to changes in the
928    /// underlying asset's price. This method creates a Call option and uses it to calculate
929    /// the delta value.
930    pub fn calculate_delta(&mut self) {
931        let option: Options = match self.get_option(Side::Long, OptionStyle::Call) {
932            Ok(option) => option,
933            Err(e) => {
934                debug!("Failed to get option for delta calculation: {}", e);
935                return;
936            }
937        };
938        match delta(&option) {
939            Ok(d) => self.delta_call = Some(d),
940            Err(e) => {
941                debug!("Delta calculation failed: {}", e);
942                self.delta_call = None;
943            }
944        }
945
946        let option: Options = match self.get_option(Side::Long, OptionStyle::Put) {
947            Ok(option) => option,
948            Err(e) => {
949                debug!("Failed to get option for delta calculation: {}", e);
950                return;
951            }
952        };
953
954        match delta(&option) {
955            Ok(d) => self.delta_put = Some(d),
956            Err(e) => {
957                debug!("Delta calculation failed: {}", e);
958                self.delta_put = None;
959            }
960        }
961    }
962
963    /// Calculates the gamma of the option and stores it in the option data.
964    ///
965    /// Gamma measures the rate of change of delta with respect to changes in the
966    /// underlying asset's price. This method creates a Call option and uses it to calculate
967    /// the gamma value.
968    pub fn calculate_gamma(&mut self) {
969        let option: Options = match self.get_option(Side::Long, OptionStyle::Call) {
970            Ok(option) => option,
971            Err(e) => {
972                debug!("Failed to get option for delta calculation: {}", e);
973                return;
974            }
975        };
976        match gamma(&option) {
977            Ok(d) => self.gamma = Some(d),
978            Err(e) => {
979                debug!("Gamma calculation failed: {}", e);
980                self.gamma = None;
981            }
982        }
983    }
984
985    /// Retrieves delta values for options at the current strike price.
986    ///
987    /// Delta measures the rate of change of the option price with respect to changes
988    /// in the underlying asset's price. This method returns delta values for options
989    /// at the specific strike price defined in the price parameters.
990    ///
991    /// # Parameters
992    ///
993    /// * `price_params` - A reference to the pricing parameters required for option calculations,
994    ///   including underlying price, expiration date, risk-free rate and other inputs.
995    ///
996    /// # Returns
997    ///
998    /// * `Result<DeltasInStrike, ChainError>` - On success, returns a structure containing delta values
999    ///   for the options at the specified strike. On failure, returns a ChainError describing the issue.
1000    ///
1001    /// # Errors
1002    ///
1003    /// * Returns a `ChainError` if there's an issue retrieving the options or calculating their deltas.
1004    /// * Possible errors include missing option data, calculation failures, or invalid parameters.
1005    pub fn get_deltas(&self) -> Result<DeltasInStrike, ChainError> {
1006        let options_in_strike = self.get_options_in_strike()?;
1007        Ok(options_in_strike.deltas()?)
1008    }
1009
1010    /// Validates if an option strike price is valid according to the specified search strategy.
1011    ///
1012    /// This method checks whether the current option's strike price falls within the constraints
1013    /// defined by the `FindOptimalSide` parameter, relative to the given underlying asset price.
1014    ///
1015    /// # Parameters
1016    ///
1017    /// * `underlying_price` - The current market price of the underlying asset as a `Positive` value.
1018    /// * `side` - The strategy to determine valid strike prices, specifying whether to consider
1019    ///   options with strikes above, below, or within a specific range of the underlying price.
1020    ///
1021    /// # Returns
1022    ///
1023    /// `bool` - Returns true if the strike price is valid according to the specified strategy:
1024    ///   * For `Upper`: Strike price must be greater than or equal to underlying price
1025    ///   * For `Lower`: Strike price must be less than or equal to underlying price
1026    ///   * For `All`: Always returns true (all strike prices are valid)
1027    ///   * For `Range`: Strike price must fall within the specified range (inclusive)
1028    pub fn is_valid_optimal_side(
1029        &self,
1030        underlying_price: &Positive,
1031        side: &FindOptimalSide,
1032    ) -> bool {
1033        match side {
1034            FindOptimalSide::Upper => &self.strike_price >= underlying_price,
1035            FindOptimalSide::Lower => &self.strike_price <= underlying_price,
1036            FindOptimalSide::All => true,
1037            FindOptimalSide::Range(start, end) => {
1038                self.strike_price >= *start && self.strike_price <= *end
1039            }
1040            FindOptimalSide::Deltable(_threshold) => true,
1041            FindOptimalSide::Center => {
1042                tracing::warn!(
1043                    "FindOptimalSide::Center must be resolved by the concrete strategy; rejecting option"
1044                );
1045                false
1046            }
1047            FindOptimalSide::DeltaRange(min, max) => {
1048                self.delta_put.is_some_and(|d| d >= *min && d <= *max)
1049                    || self.delta_call.is_some_and(|d| d >= *min && d <= *max)
1050            }
1051        }
1052    }
1053
1054    /// Calculates and sets the mid-prices for both call and put options.
1055    ///
1056    /// This method computes the middle price between the bid and ask prices for
1057    /// both call and put options, when both bid and ask prices are available.
1058    /// The mid-price is calculated as the simple average: (bid + ask) / 2.
1059    /// If either bid or ask price is missing for an option type, the corresponding
1060    /// mid-price will be set to `None`.
1061    ///
1062    /// # Side Effects
1063    ///
1064    /// Updates the `call_middle` and `put_middle` fields with the calculated mid-prices.
1065    pub fn set_mid_prices(&mut self) {
1066        self.call_middle = match (self.call_bid, self.call_ask) {
1067            (Some(bid), Some(ask)) => Some(((bid + ask) / Positive::TWO).round_to(4)),
1068            _ => None,
1069        };
1070        self.put_middle = match (self.put_bid, self.put_ask) {
1071            (Some(bid), Some(ask)) => Some(((bid + ask) / Positive::TWO).round_to(4)),
1072            _ => None,
1073        };
1074    }
1075
1076    /// Retrieves the current mid-prices for call and put options.
1077    ///
1078    /// This method returns the calculated middle prices for both call and put options
1079    /// as a tuple. Each price may be `None` if the corresponding bid/ask prices
1080    /// were not available when `set_mid_prices()` was called.
1081    ///
1082    /// # Returns
1083    ///
1084    /// A tuple containing:
1085    /// * First element: The call option mid-price (bid+ask)/2, or `None` if not available
1086    /// * Second element: The put option mid-price (bid+ask)/2, or `None` if not available
1087    #[inline]
1088    #[must_use]
1089    pub fn get_mid_prices(&self) -> (Option<Positive>, Option<Positive>) {
1090        (self.call_middle, self.put_middle)
1091    }
1092
1093    /// Checks and corrects implied volatility if it's represented as a percentage greater than 1.0.
1094    ///
1095    /// This function checks if the `implied_volatility` field is present. If it is and its value
1096    /// is greater than 1.0, the function assumes it's represented as a percentage and divides it
1097    /// by 100.0 to convert it to a decimal value. This ensures that implied volatility is stored
1098    /// in the correct format, preventing potential misinterpretations and calculation errors.
1099    pub(super) fn check_and_convert_implied_volatility(&mut self) {
1100        if self.implied_volatility > Positive::ONE {
1101            self.implied_volatility = self.implied_volatility / Positive::HUNDRED;
1102        }
1103    }
1104
1105    /// Returns a tuple containing the current delta values for both call and put options.
1106    ///
1107    /// This method provides access to the option's delta values, which measure the rate of change
1108    /// of the option price with respect to changes in the underlying asset price. Delta values
1109    /// typically range from -1 to 1 and are a key metric for understanding option price sensitivity.
1110    ///
1111    /// # Returns
1112    ///
1113    /// A tuple containing:
1114    /// * First element: `Option<Decimal>` - The delta value for the call option. May be `None` if
1115    ///   the delta value is not available or could not be calculated.
1116    /// * Second element: `Option<Decimal>` - The delta value for the put option. May be `None` if
1117    ///   the delta value is not available or could not be calculated.
1118    ///
1119    #[inline]
1120    #[must_use]
1121    pub fn current_deltas(&self) -> (Option<Decimal>, Option<Decimal>) {
1122        (self.delta_call, self.delta_put)
1123    }
1124
1125    /// Returns the current gamma value.
1126    ///
1127    /// This function retrieves the optional `gamma` field of the struct.
1128    /// If the `gamma` field has been set, it returns a `Some(Decimal)` value;
1129    /// otherwise, it returns `None`.
1130    ///
1131    /// # Returns
1132    ///
1133    /// * `Option<Decimal>` - The current gamma value wrapped in `Some` if it exists,
1134    ///   or `None` if the gamma value is not set.
1135    ///
1136    #[inline]
1137    #[must_use]
1138    pub fn current_gamma(&self) -> Option<Decimal> {
1139        self.gamma
1140    }
1141}
1142
1143impl Default for OptionData {
1144    fn default() -> Self {
1145        OptionData {
1146            strike_price: Positive::ZERO,
1147            call_bid: None,
1148            call_ask: None,
1149            put_bid: None,
1150            put_ask: None,
1151            call_middle: None,
1152            put_middle: None,
1153            implied_volatility: Positive::ZERO,
1154            delta_call: None,
1155            delta_put: None,
1156            gamma: None,
1157            volume: None,
1158            open_interest: None,
1159            symbol: None,
1160            expiration_date: None,
1161            underlying_price: None,
1162            risk_free_rate: None,
1163            dividend_yield: None,
1164            epic: None,
1165            extra_fields: None,
1166        }
1167    }
1168}
1169
1170impl PartialOrd for OptionData {
1171    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1172        Some(self.cmp(other))
1173    }
1174}
1175
1176impl Eq for OptionData {}
1177
1178impl Ord for OptionData {
1179    fn cmp(&self, other: &Self) -> Ordering {
1180        self.strike_price.cmp(&other.strike_price)
1181    }
1182}
1183
1184impl fmt::Display for OptionData {
1185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1186        write!(
1187            f,
1188            "{:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<6}{:<7} {:.3}{:<4} {:.3}{:<5} {:.4}{:<8} {:<10} {:<10}",
1189            self.strike_price.to_string(),
1190            empty_string_round_to_2(self.call_bid),
1191            empty_string_round_to_2(self.call_ask),
1192            empty_string_round_to_2(self.call_middle),
1193            empty_string_round_to_2(self.put_bid),
1194            empty_string_round_to_2(self.put_ask),
1195            empty_string_round_to_2(self.put_middle),
1196            self.implied_volatility.format_fixed_places(3),
1197            " ".to_string(),
1198            self.delta_call.unwrap_or(Decimal::ZERO),
1199            " ".to_string(),
1200            self.delta_put.unwrap_or(Decimal::ZERO),
1201            " ".to_string(),
1202            self.gamma.unwrap_or(Decimal::ZERO) * Decimal::ONE_HUNDRED,
1203            " ".to_string(),
1204            default_empty_string(self.volume),
1205            default_empty_string(self.open_interest),
1206        )?;
1207        Ok(())
1208    }
1209}
1210
1211#[cfg(test)]
1212mod optiondata_coverage_tests {
1213    use super::*;
1214    use positive::{pos_or_panic, spos};
1215    use rust_decimal_macros::dec;
1216
1217    // Helper function to create test option data
1218    fn create_test_option_data() -> OptionData {
1219        OptionData::new(
1220            Positive::HUNDRED,
1221            spos!(9.5),
1222            spos!(10.0),
1223            spos!(8.5),
1224            spos!(9.0),
1225            pos_or_panic!(0.2),
1226            Some(dec!(-0.3)),
1227            Some(dec!(0.7)),
1228            Some(dec!(0.5)),
1229            spos!(1000.0),
1230            Some(500),
1231            Some("TEST".to_string()),                        // symbol
1232            Some(ExpirationDate::Days(pos_or_panic!(30.0))), // expiration_date
1233            Some(Box::new(Positive::HUNDRED)),               // underlying_price
1234            Some(dec!(0.05)),                                // risk_free_rate
1235            Some(pos_or_panic!(0.02)),                       // dividend_yield
1236            None,
1237            None,
1238        )
1239    }
1240
1241    #[test]
1242    fn test_current_deltas() {
1243        let option_data = create_test_option_data();
1244
1245        // Test current deltas
1246        let (call_delta, put_delta) = option_data.current_deltas();
1247
1248        assert!(call_delta.is_some());
1249        assert!(put_delta.is_some());
1250        assert_eq!(call_delta.unwrap(), dec!(-0.3));
1251        assert_eq!(put_delta.unwrap(), dec!(0.7));
1252    }
1253
1254    #[test]
1255    fn test_calculate_prices_with_refresh() {
1256        let mut option_data = create_test_option_data();
1257        option_data.set_volatility(&pos_or_panic!(0.25));
1258
1259        // Calculate prices with refresh flag set to true
1260        let result = option_data.calculate_prices(None);
1261        assert!(result.is_ok());
1262
1263        // Check that prices were updated
1264        assert!(option_data.call_bid.is_some());
1265        assert!(option_data.call_ask.is_some());
1266        assert!(option_data.put_bid.is_some());
1267        assert!(option_data.put_ask.is_some());
1268
1269        // Check that mid prices were set
1270        assert!(option_data.call_middle.is_some());
1271        assert!(option_data.put_middle.is_some());
1272    }
1273
1274    #[test]
1275    fn test_apply_spread_call() {
1276        let mut option_data = create_test_option_data();
1277        // Record original values
1278        let original_call_bid = option_data.call_bid;
1279        let original_call_ask = option_data.call_ask;
1280
1281        // Apply a spread
1282        option_data.apply_spread(pos_or_panic!(0.6), 2);
1283
1284        // Check that values were updated
1285        assert_ne!(option_data.call_bid, original_call_bid);
1286        assert_ne!(option_data.call_ask, original_call_ask);
1287
1288        // Test with a spread that would make bid negative (should set to None)
1289        let mut option_data = create_test_option_data();
1290        option_data.call_bid = spos!(0.1);
1291        option_data.apply_spread(Positive::ONE, 2);
1292
1293        // Bid should be None as it would be negative
1294        assert_eq!(option_data.call_bid, Some(Positive::ZERO));
1295    }
1296
1297    #[test]
1298    fn test_apply_spread_put() {
1299        let mut option_data = create_test_option_data();
1300        // Record original values
1301        let original_put_bid = option_data.put_bid;
1302        let original_put_ask = option_data.put_ask;
1303
1304        // Apply a spread
1305        option_data.apply_spread(pos_or_panic!(0.6), 2);
1306
1307        // Check that values were updated
1308        assert_ne!(option_data.put_bid, original_put_bid);
1309        assert_ne!(option_data.put_ask, original_put_ask);
1310
1311        // Test with a spread that would make bid negative (should set to None)
1312        let mut option_data = create_test_option_data();
1313        option_data.put_bid = spos!(0.1);
1314        option_data.apply_spread(Positive::ONE, 2);
1315
1316        // Bid should be None as it would be negative
1317        assert_eq!(option_data.put_bid, Some(Positive::ZERO));
1318    }
1319
1320    #[test]
1321    fn test_calculate_gamma_no_implied_volatility() {
1322        let mut option_data = create_test_option_data();
1323        option_data.set_volatility(&pos_or_panic!(0.2));
1324
1325        // Calculate gamma
1326        option_data.calculate_gamma();
1327
1328        // Check that gamma was set
1329        assert!(option_data.gamma.is_some());
1330    }
1331
1332    // Test for lines 1076-1077
1333    #[test]
1334    fn test_get_deltas() {
1335        let option_data = create_test_option_data();
1336
1337        // Get deltas
1338        let result = option_data.get_deltas();
1339        assert!(result.is_ok());
1340
1341        let deltas = result.unwrap();
1342
1343        // Check that all deltas are present
1344        assert!(deltas.long_call != dec!(0.0));
1345        assert!(deltas.short_call != dec!(0.0));
1346        assert!(deltas.long_put != dec!(0.0));
1347        assert!(deltas.short_put != dec!(0.0));
1348    }
1349}
1350
1351#[cfg(test)]
1352mod tests_get_position {
1353    use super::*;
1354    use crate::model::ExpirationDate;
1355    use chrono::{Duration, Utc};
1356    use positive::{assert_pos_relative_eq, pos_or_panic, spos};
1357    use rust_decimal_macros::dec;
1358
1359    // Helper function to create a standard test option data
1360    fn create_test_option_data() -> OptionData {
1361        OptionData::new(
1362            Positive::HUNDRED,                               // strike_price
1363            spos!(9.5),                                      // call_bid
1364            spos!(10.0),                                     // call_ask
1365            spos!(8.5),                                      // put_bid
1366            spos!(9.0),                                      // put_ask
1367            pos_or_panic!(0.2),                              // implied_volatility
1368            Some(dec!(-0.3)),                                // delta_call
1369            Some(dec!(0.7)),                                 // delta_put
1370            Some(dec!(0.5)),                                 // gamma
1371            spos!(1000.0),                                   // volume
1372            Some(500),                                       // open_interest
1373            Some("TEST".to_string()),                        // symbol
1374            Some(ExpirationDate::Days(pos_or_panic!(30.0))), // expiration_date
1375            Some(Box::new(Positive::HUNDRED)),               // underlying_price
1376            Some(dec!(0.05)),                                // risk_free_rate
1377            Some(pos_or_panic!(0.02)),                       // dividend_yield
1378            None,
1379            None,
1380        )
1381    }
1382
1383    // Helper function to create standard price parameters
1384    fn create_test_price_params() -> OptionDataPriceParams {
1385        OptionDataPriceParams::new(
1386            Some(Box::new(Positive::HUNDRED)),
1387            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
1388            Some(dec!(0.05)),
1389            spos!(0.02),
1390            Some("AAPL".to_string()),
1391        )
1392    }
1393
1394    #[test]
1395    fn test_get_position_long_call() {
1396        let option_data = create_test_option_data();
1397
1398        // Test getting a long call position
1399        let result = option_data.get_position(
1400            Side::Long,
1401            OptionStyle::Call,
1402            None, // Default to current date
1403            None, // Default to zero fees
1404            None, // Default to zero fees
1405        );
1406
1407        assert!(result.is_ok(), "Should successfully create position");
1408
1409        let position = result.unwrap();
1410
1411        // Verify position properties
1412        assert_eq!(position.option.side, Side::Long);
1413        assert_eq!(position.option.option_style, OptionStyle::Call);
1414        assert_eq!(position.option.strike_price, Positive::HUNDRED);
1415        assert!(
1416            position.premium > Positive::ZERO,
1417            "Premium should be positive"
1418        );
1419
1420        // Verify fees are set to zero by default
1421        assert_eq!(position.open_fee, Positive::ZERO);
1422        assert_eq!(position.close_fee, Positive::ZERO);
1423    }
1424
1425    #[test]
1426    fn test_get_position_short_put() {
1427        let option_data = create_test_option_data();
1428
1429        // Test getting a short put position
1430        let result = option_data.get_position(
1431            Side::Short,
1432            OptionStyle::Put,
1433            None, // Default to current date
1434            None, // Default to zero fees
1435            None, // Default to zero fees
1436        );
1437
1438        assert!(result.is_ok(), "Should successfully create position");
1439
1440        let position = result.unwrap();
1441
1442        // Verify position properties
1443        assert_eq!(position.option.side, Side::Short);
1444        assert_eq!(position.option.option_style, OptionStyle::Put);
1445        assert_eq!(position.option.strike_price, Positive::HUNDRED);
1446        assert!(
1447            position.premium > Positive::ZERO,
1448            "Premium should be positive"
1449        );
1450    }
1451
1452    #[test]
1453    fn test_get_position_with_custom_date() {
1454        let option_data = create_test_option_data();
1455
1456        // Create a custom date (one week ago)
1457        let custom_date = Utc::now() - Duration::days(7);
1458
1459        // Test with custom date
1460        let result =
1461            option_data.get_position(Side::Long, OptionStyle::Call, Some(custom_date), None, None);
1462
1463        assert!(result.is_ok());
1464        let position = result.unwrap();
1465
1466        // Verify the date was set correctly
1467        assert_eq!(position.date, custom_date);
1468    }
1469
1470    #[test]
1471    fn test_get_position_with_fees() {
1472        let option_data = create_test_option_data();
1473
1474        // Custom fees
1475        let open_fee = pos_or_panic!(1.5);
1476        let close_fee = Positive::TWO;
1477
1478        // Test with custom fees
1479        let result = option_data.get_position(
1480            Side::Long,
1481            OptionStyle::Put,
1482            None,
1483            Some(open_fee),
1484            Some(close_fee),
1485        );
1486
1487        assert!(result.is_ok());
1488        let position = result.unwrap();
1489
1490        // Verify fees were set correctly
1491        assert_eq!(position.open_fee, open_fee);
1492        assert_eq!(position.close_fee, close_fee);
1493    }
1494
1495    #[test]
1496    fn test_get_position_in_the_money_call() {
1497        let mut option_data = create_test_option_data();
1498
1499        // Create params with underlying price higher than strike (ITM call)
1500        let mut price_params = create_test_price_params();
1501        price_params.underlying_price = Some(Box::new(pos_or_panic!(120.0)));
1502        option_data.set_extra_params(price_params);
1503
1504        let result = option_data.get_position(Side::Long, OptionStyle::Call, None, None, None);
1505
1506        assert!(result.is_ok());
1507        let position = result.unwrap();
1508
1509        // An ITM call should have higher premium
1510        assert!(
1511            position.premium >= pos_or_panic!(10.0),
1512            "ITM call premium should be significant"
1513        );
1514    }
1515
1516    #[test]
1517    fn test_get_position_all_combinations() {
1518        let option_data = create_test_option_data();
1519
1520        // Test all combinations of Side and OptionStyle
1521        let combinations = vec![
1522            (Side::Long, OptionStyle::Call),
1523            (Side::Long, OptionStyle::Put),
1524            (Side::Short, OptionStyle::Call),
1525            (Side::Short, OptionStyle::Put),
1526        ];
1527
1528        for (side, style) in combinations {
1529            let result = option_data.get_position(side, style, None, None, None);
1530
1531            assert!(
1532                result.is_ok(),
1533                "Failed to create position: {side:?}, {style:?}"
1534            );
1535            let position = result.unwrap();
1536
1537            // Verify position properties
1538            assert_eq!(position.option.side, side);
1539            assert_eq!(position.option.option_style, style);
1540            assert!(position.premium > Positive::ZERO);
1541        }
1542    }
1543
1544    #[test]
1545    fn test_get_position_with_custom_all_params() {
1546        // This test checks that all custom parameters are correctly applied
1547        let option_data = create_test_option_data();
1548
1549        // Create a custom date
1550        let custom_date = Utc::now() - Duration::days(14);
1551
1552        // Custom fees
1553        let open_fee = pos_or_panic!(2.5);
1554        let close_fee = pos_or_panic!(1.75);
1555
1556        // Test with all custom parameters
1557        let result = option_data.get_position(
1558            Side::Short,
1559            OptionStyle::Put,
1560            Some(custom_date),
1561            Some(open_fee),
1562            Some(close_fee),
1563        );
1564
1565        assert!(result.is_ok());
1566        let position = result.unwrap();
1567
1568        // Verify all parameters were applied correctly
1569        assert_eq!(position.option.side, Side::Short);
1570        assert_eq!(position.option.option_style, OptionStyle::Put);
1571        assert_eq!(position.date, custom_date);
1572        assert_eq!(position.open_fee, open_fee);
1573        assert_eq!(position.close_fee, close_fee);
1574    }
1575
1576    #[test]
1577    fn test_get_position_uses_market_price_long_call() {
1578        let option_data = create_test_option_data();
1579
1580        // Test getting a long call position
1581        let result = option_data.get_position(
1582            Side::Long,
1583            OptionStyle::Call,
1584            None, // Default to current date
1585            None, // Default to zero fees
1586            None, // Default to zero fees
1587        );
1588
1589        assert!(result.is_ok(), "Should successfully create position");
1590
1591        let position = result.unwrap();
1592
1593        // For a long call, should use call_ask (10.0)
1594        assert_eq!(
1595            position.premium,
1596            pos_or_panic!(10.0),
1597            "Should use call_ask price for long call"
1598        );
1599    }
1600
1601    #[test]
1602    fn test_get_position_uses_market_price_short_call() {
1603        let option_data = create_test_option_data();
1604
1605        // Test getting a short call position
1606        let result = option_data.get_position(
1607            Side::Short,
1608            OptionStyle::Call,
1609            None, // Default to current date
1610            None, // Default to zero fees
1611            None, // Default to zero fees
1612        );
1613
1614        assert!(result.is_ok(), "Should successfully create position");
1615
1616        let position = result.unwrap();
1617
1618        // For a short call, should use call_bid (9.5)
1619        assert_eq!(
1620            position.premium,
1621            pos_or_panic!(9.5),
1622            "Should use call_bid price for short call"
1623        );
1624    }
1625
1626    #[test]
1627    fn test_get_position_uses_market_price_long_put() {
1628        let option_data = create_test_option_data();
1629
1630        // Test getting a long put position
1631        let result = option_data.get_position(
1632            Side::Long,
1633            OptionStyle::Put,
1634            None, // Default to current date
1635            None, // Default to zero fees
1636            None, // Default to zero fees
1637        );
1638
1639        assert!(result.is_ok(), "Should successfully create position");
1640
1641        let position = result.unwrap();
1642
1643        // For a long put, should use put_ask (9.0)
1644        assert_eq!(
1645            position.premium,
1646            pos_or_panic!(9.0),
1647            "Should use put_ask price for long put"
1648        );
1649    }
1650
1651    #[test]
1652    fn test_get_position_uses_market_price_short_put() {
1653        let option_data = create_test_option_data();
1654
1655        // Test getting a short put position
1656        let result = option_data.get_position(
1657            Side::Short,
1658            OptionStyle::Put,
1659            None, // Default to current date
1660            None, // Default to zero fees
1661            None, // Default to zero fees
1662        );
1663
1664        assert!(result.is_ok(), "Should successfully create position");
1665
1666        let position = result.unwrap();
1667
1668        // For a short put, should use put_bid (8.5)
1669        assert_eq!(
1670            position.premium,
1671            pos_or_panic!(8.5),
1672            "Should use put_bid price for short put"
1673        );
1674    }
1675
1676    #[test]
1677    fn test_get_position_fallback_to_black_scholes() {
1678        // Test with option data that doesn't have market prices
1679        let option_data = OptionData::new(
1680            Positive::HUNDRED,                               // strike_price
1681            None,                                            // call_bid (missing)
1682            None,                                            // call_ask (missing)
1683            None,                                            // put_bid (missing)
1684            None,                                            // put_ask (missing)
1685            pos_or_panic!(0.2),                              // implied_volatility
1686            Some(dec!(-0.3)),                                // delta_call
1687            Some(dec!(0.7)),                                 // delta_put
1688            Some(dec!(0.5)),                                 // gamma
1689            spos!(1000.0),                                   // volume
1690            Some(500),                                       // open_interest
1691            Some("TEST".to_string()),                        // symbol
1692            Some(ExpirationDate::Days(pos_or_panic!(30.0))), // expiration_date
1693            Some(Box::new(Positive::HUNDRED)),               // underlying_price
1694            Some(dec!(0.05)),                                // risk_free_rate
1695            Some(pos_or_panic!(0.02)),                       // dividend_yield
1696            None,
1697            None,
1698        );
1699
1700        // Test getting a long call position
1701        let result = option_data.get_position(Side::Long, OptionStyle::Call, None, None, None);
1702
1703        assert!(
1704            result.is_ok(),
1705            "Should successfully create position using Black-Scholes"
1706        );
1707
1708        let position = result.unwrap();
1709
1710        // Premium should be calculated using Black-Scholes
1711        assert!(
1712            position.premium > Positive::ZERO,
1713            "Should calculate premium using Black-Scholes"
1714        );
1715
1716        // Let's verify it matches direct Black-Scholes calculation
1717        let option = option_data
1718            .get_option(Side::Long, OptionStyle::Call)
1719            .unwrap();
1720        let bs_price = option.calculate_price_black_scholes().unwrap().abs();
1721        let bs_price_positive = Positive::new_decimal(bs_price).unwrap();
1722
1723        assert_pos_relative_eq!(position.premium, bs_price_positive, pos_or_panic!(0.00001));
1724    }
1725
1726    #[test]
1727    fn test_get_position_with_custom_date_uses_market_price() {
1728        let option_data = create_test_option_data();
1729
1730        // Create a custom date (one week ago)
1731        let custom_date = Utc::now() - Duration::days(7);
1732
1733        // Test with custom date for long call
1734        let result =
1735            option_data.get_position(Side::Long, OptionStyle::Call, Some(custom_date), None, None);
1736
1737        assert!(result.is_ok());
1738        let position = result.unwrap();
1739
1740        // Verify the date was set correctly
1741        assert_eq!(position.date, custom_date);
1742
1743        // Should still use market price (10.0 for long call)
1744        assert_eq!(
1745            position.premium,
1746            pos_or_panic!(10.0),
1747            "Should use call_ask even with custom date"
1748        );
1749    }
1750
1751    #[test]
1752    fn test_get_position_with_fees_uses_market_price() {
1753        let option_data = create_test_option_data();
1754
1755        // Custom fees
1756        let open_fee = pos_or_panic!(1.5);
1757        let close_fee = Positive::TWO;
1758
1759        // Test with custom fees for short put
1760        let result = option_data.get_position(
1761            Side::Short,
1762            OptionStyle::Put,
1763            None,
1764            Some(open_fee),
1765            Some(close_fee),
1766        );
1767
1768        assert!(result.is_ok());
1769        let position = result.unwrap();
1770
1771        // Verify fees were set correctly
1772        assert_eq!(position.open_fee, open_fee);
1773        assert_eq!(position.close_fee, close_fee);
1774
1775        // Should still use market price (8.5 for short put)
1776        assert_eq!(
1777            position.premium,
1778            pos_or_panic!(8.5),
1779            "Should use put_bid even with custom fees"
1780        );
1781    }
1782
1783    #[test]
1784    fn test_get_position_missing_specific_price() {
1785        // Test with option data missing just one price
1786        let mut option_data = create_test_option_data();
1787        option_data.call_ask = None; // Remove call ask price
1788
1789        // Try to get a long call position which needs call_ask
1790        let result = option_data.get_position(Side::Long, OptionStyle::Call, None, None, None);
1791
1792        // Should still succeed but fall back to Black-Scholes
1793        assert!(
1794            result.is_ok(),
1795            "Should fall back to Black-Scholes when specific price is missing"
1796        );
1797
1798        let position = result.unwrap();
1799
1800        // Let's verify it matches direct Black-Scholes calculation
1801        let option = option_data
1802            .get_option(Side::Long, OptionStyle::Call)
1803            .unwrap();
1804        let bs_price = option.calculate_price_black_scholes().unwrap().abs();
1805        let bs_price_positive = Positive::new_decimal(bs_price).unwrap();
1806
1807        assert_pos_relative_eq!(position.premium, bs_price_positive, pos_or_panic!(0.00001));
1808    }
1809}
1810
1811#[cfg(test)]
1812mod tests_check_convert_implied_volatility {
1813    use super::*;
1814    use positive::pos_or_panic;
1815
1816    #[test]
1817    fn test_check_and_convert_implied_volatility_over_one() {
1818        // Line 219: Test the conversion of implied volatility when it's greater than 1.0
1819        let mut option_data = OptionData::new(
1820            Positive::HUNDRED,
1821            None,
1822            None,
1823            None,
1824            None,
1825            pos_or_panic!(20.0), // This is 2000% volatility, should be converted to 20%
1826            None,
1827            None,
1828            None,
1829            None,
1830            None,
1831            None,
1832            None,
1833            None,
1834            None,
1835            None,
1836            None,
1837            None,
1838        );
1839
1840        // Call the method being tested
1841        option_data.check_and_convert_implied_volatility();
1842
1843        // Assert that the volatility is now converted to a proper decimal (e.g., 0.2 instead of 20.0)
1844        assert_eq!(option_data.implied_volatility, pos_or_panic!(0.2));
1845    }
1846
1847    #[test]
1848    fn test_check_and_convert_implied_volatility_under_one() {
1849        // Test that volatility under 1.0 is not modified
1850        let mut option_data = OptionData::new(
1851            Positive::HUNDRED,
1852            None,
1853            None,
1854            None,
1855            None,
1856            pos_or_panic!(0.15), // This is 15% volatility, should remain as is
1857            None,
1858            None,
1859            None,
1860            None,
1861            None,
1862            None,
1863            None,
1864            None,
1865            None,
1866            None,
1867            None,
1868            None,
1869        );
1870
1871        // Original implied volatility
1872        let original_iv = option_data.implied_volatility;
1873
1874        // Call the method being tested
1875        option_data.check_and_convert_implied_volatility();
1876
1877        // Assert that the volatility is unchanged
1878        assert_eq!(option_data.implied_volatility, original_iv);
1879    }
1880}
1881
1882#[cfg(test)]
1883mod tests_get_option_for_iv {
1884    use super::*;
1885    use crate::OptionType;
1886    use crate::model::ExpirationDate;
1887    use positive::{pos_or_panic, spos};
1888    use rust_decimal_macros::dec;
1889
1890    // Helper function to create a standard OptionDataPriceParams for testing
1891    fn create_test_price_params() -> OptionDataPriceParams {
1892        OptionDataPriceParams::new(
1893            Some(Box::new(Positive::HUNDRED)),
1894            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
1895            Some(dec!(0.05)),
1896            spos!(0.02),
1897            Some("AAPL".to_string()),
1898        )
1899    }
1900
1901    #[test]
1902    fn test_get_option_for_iv_success() {
1903        let mut option_data = OptionData::new(
1904            Positive::HUNDRED,
1905            spos!(5.0),
1906            spos!(5.5),
1907            spos!(4.5),
1908            spos!(5.0),
1909            pos_or_panic!(0.2),
1910            None,
1911            None,
1912            None,
1913            None,
1914            None,
1915            Some("TEST".to_string()),                        // symbol
1916            Some(ExpirationDate::Days(pos_or_panic!(30.0))), // expiration_date
1917            Some(Box::new(Positive::HUNDRED)),               // underlying_price
1918            Some(dec!(0.05)),                                // risk_free_rate
1919            Some(pos_or_panic!(0.02)),                       // dividend_yield
1920            None,
1921            None,
1922        );
1923
1924        let params = create_test_price_params();
1925        option_data.set_extra_params(params.clone());
1926        let initial_iv = pos_or_panic!(0.25); // Different from the option_data IV to confirm it's using this value
1927
1928        // Call the method being tested
1929        let result = option_data.get_option_for_iv(Side::Long, OptionStyle::Call, initial_iv);
1930
1931        // Assert success and check properties
1932        assert!(result.is_ok());
1933        let option = result.unwrap();
1934
1935        assert_eq!(option.option_type, OptionType::European);
1936        assert_eq!(option.side, Side::Long);
1937        assert_eq!(option.strike_price, Positive::HUNDRED);
1938        assert_eq!(option.expiration_date, params.expiration_date.unwrap());
1939        assert_eq!(option.implied_volatility, initial_iv.to_f64()); // Should use the provided initial_iv
1940        assert_eq!(option.quantity, Positive::ONE);
1941        assert_eq!(option.underlying_price, *params.underlying_price.unwrap());
1942        assert_eq!(option.risk_free_rate, params.risk_free_rate.unwrap());
1943        assert_eq!(option.option_style, OptionStyle::Call);
1944        assert_eq!(option.dividend_yield, params.dividend_yield.unwrap());
1945    }
1946
1947    #[test]
1948    fn test_get_option_for_iv_put() {
1949        // Test get_option_for_iv with Put option style
1950        let option_data = OptionData::new(
1951            Positive::HUNDRED,
1952            spos!(5.0),
1953            spos!(5.5),
1954            spos!(4.5),
1955            spos!(5.0),
1956            pos_or_panic!(0.2),
1957            None,
1958            None,
1959            None,
1960            None,
1961            None,
1962            Some("TEST".to_string()),                        // symbol
1963            Some(ExpirationDate::Days(pos_or_panic!(30.0))), // expiration_date
1964            Some(Box::new(Positive::HUNDRED)),               // underlying_price
1965            Some(dec!(0.05)),                                // risk_free_rate
1966            Some(pos_or_panic!(0.02)),                       // dividend_yield
1967            None,
1968            None,
1969        );
1970
1971        let initial_iv = pos_or_panic!(0.3);
1972
1973        // Call the method with Put option style
1974        let result = option_data.get_option_for_iv(Side::Long, OptionStyle::Put, initial_iv);
1975
1976        // Assert success and check option style
1977        assert!(result.is_ok());
1978        let option = result.unwrap();
1979        assert_eq!(option.option_style, OptionStyle::Put);
1980    }
1981
1982    #[test]
1983    fn test_get_option_for_iv_short() {
1984        // Test get_option_for_iv with Short side
1985        let option_data = OptionData::new(
1986            Positive::HUNDRED,
1987            spos!(5.0),
1988            spos!(5.5),
1989            spos!(4.5),
1990            spos!(5.0),
1991            pos_or_panic!(0.2),
1992            None,
1993            None,
1994            None,
1995            None,
1996            None,
1997            Some("TEST".to_string()),                        // symbol
1998            Some(ExpirationDate::Days(pos_or_panic!(30.0))), // expiration_date
1999            Some(Box::new(Positive::HUNDRED)),               // underlying_price
2000            Some(dec!(0.05)),                                // risk_free_rate
2001            Some(pos_or_panic!(0.02)),                       // dividend_yield
2002            None,
2003            None,
2004        );
2005
2006        let initial_iv = pos_or_panic!(0.2);
2007
2008        // Call the method with Short side
2009        let result = option_data.get_option_for_iv(Side::Short, OptionStyle::Call, initial_iv);
2010
2011        // Assert success and check side
2012        assert!(result.is_ok());
2013        let option = result.unwrap();
2014        assert_eq!(option.side, Side::Short);
2015    }
2016}
2017
2018#[cfg(test)]
2019mod tests_some_price_is_none {
2020    use super::*;
2021    use positive::{pos_or_panic, spos};
2022
2023    #[test]
2024    fn test_some_price_is_none_all_prices_present() {
2025        // Line 626: Test some_price_is_none when all prices are present
2026        let option_data = OptionData::new(
2027            Positive::HUNDRED,
2028            spos!(5.0),         // call_bid
2029            spos!(5.5),         // call_ask
2030            spos!(4.5),         // put_bid
2031            spos!(5.0),         // put_ask
2032            pos_or_panic!(0.2), // implied_volatility
2033            None,
2034            None,
2035            None,
2036            None,
2037            None,
2038            None,
2039            None,
2040            None,
2041            None,
2042            None,
2043            None,
2044            None,
2045        );
2046
2047        // All prices are present, should return false
2048        assert!(!option_data.some_price_is_none());
2049    }
2050
2051    #[test]
2052    fn test_some_price_is_none_with_missing_call_bid() {
2053        // Test with missing call_bid
2054        let option_data = OptionData::new(
2055            Positive::HUNDRED,
2056            None,               // call_bid is None
2057            spos!(5.5),         // call_ask
2058            spos!(4.5),         // put_bid
2059            spos!(5.0),         // put_ask
2060            pos_or_panic!(0.2), // implied_volatility
2061            None,
2062            None,
2063            None,
2064            None,
2065            None,
2066            None,
2067            None,
2068            None,
2069            None,
2070            None,
2071            None,
2072            None,
2073        );
2074
2075        // One price is missing, should return true
2076        assert!(option_data.some_price_is_none());
2077    }
2078
2079    #[test]
2080    fn test_some_price_is_none_with_missing_call_ask() {
2081        // Test with missing call_ask
2082        let option_data = OptionData::new(
2083            Positive::HUNDRED,
2084            spos!(5.0),         // call_bid
2085            None,               // call_ask is None
2086            spos!(4.5),         // put_bid
2087            spos!(5.0),         // put_ask
2088            pos_or_panic!(0.2), // implied_volatility
2089            None,
2090            None,
2091            None,
2092            None,
2093            None,
2094            None,
2095            None,
2096            None,
2097            None,
2098            None,
2099            None,
2100            None,
2101        );
2102
2103        // One price is missing, should return true
2104        assert!(option_data.some_price_is_none());
2105    }
2106
2107    #[test]
2108    fn test_some_price_is_none_with_missing_put_bid() {
2109        // Test with missing put_bid
2110        let option_data = OptionData::new(
2111            Positive::HUNDRED,
2112            spos!(5.0),         // call_bid
2113            spos!(5.5),         // call_ask
2114            None,               // put_bid is None
2115            spos!(5.0),         // put_ask
2116            pos_or_panic!(0.2), // implied_volatility
2117            None,
2118            None,
2119            None,
2120            None,
2121            None,
2122            None,
2123            None,
2124            None,
2125            None,
2126            None,
2127            None,
2128            None,
2129        );
2130
2131        // One price is missing, should return true
2132        assert!(option_data.some_price_is_none());
2133    }
2134
2135    #[test]
2136    fn test_some_price_is_none_with_missing_put_ask() {
2137        // Test with missing put_ask
2138        let option_data = OptionData::new(
2139            Positive::HUNDRED,
2140            spos!(5.0),         // call_bid
2141            spos!(5.5),         // call_ask
2142            spos!(4.5),         // put_bid
2143            None,               // put_ask is None
2144            pos_or_panic!(0.2), // implied_volatility
2145            None,
2146            None,
2147            None,
2148            None,
2149            None,
2150            None,
2151            None,
2152            None,
2153            None,
2154            None,
2155            None,
2156            None,
2157        );
2158
2159        // One price is missing, should return true
2160        assert!(option_data.some_price_is_none());
2161    }
2162
2163    #[test]
2164    fn test_some_price_is_none_with_all_prices_missing() {
2165        // Test with all prices missing
2166        let option_data = OptionData::new(
2167            Positive::HUNDRED,
2168            None,               // call_bid is None
2169            None,               // call_ask is None
2170            None,               // put_bid is None
2171            None,               // put_ask is None
2172            pos_or_panic!(0.2), // implied_volatility
2173            None,
2174            None,
2175            None,
2176            None,
2177            None,
2178            None,
2179            None,
2180            None,
2181            None,
2182            None,
2183            None,
2184            None,
2185        );
2186
2187        // All prices are missing, should return true
2188        assert!(option_data.some_price_is_none());
2189    }
2190}
2191
2192#[cfg(test)]
2193mod tests_is_valid_optimal_side_deltable {
2194    use super::*;
2195    use positive::pos_or_panic;
2196    use rust_decimal_macros::dec;
2197
2198    #[test]
2199    fn test_is_valid_optimal_side_deltable() {
2200        // Line 742-744: Test is_valid_optimal_side for Deltable threshold
2201        let option_data = OptionData::new(
2202            Positive::HUNDRED,
2203            None,
2204            None,
2205            None,
2206            None,
2207            pos_or_panic!(0.2), // implied_volatility
2208            Some(dec!(0.3)),    // delta_call
2209            Some(dec!(-0.3)),   // delta_put
2210            None,
2211            None,
2212            None,
2213            None,
2214            None,
2215            None,
2216            None,
2217            None,
2218            None,
2219            None,
2220        );
2221
2222        // Deltable should always return true
2223        let result = option_data.is_valid_optimal_side(
2224            &Positive::HUNDRED,
2225            &FindOptimalSide::Deltable(pos_or_panic!(0.5)),
2226        );
2227
2228        assert!(result);
2229    }
2230
2231    #[test]
2232    fn test_is_valid_optimal_side_center_panics() {
2233        // Lines 758-760: Test is_valid_optimal_side for Center (which should panic)
2234        let option_data = OptionData::new(
2235            Positive::HUNDRED,
2236            None,
2237            None,
2238            None,
2239            None,
2240            pos_or_panic!(0.2), // implied_volatility
2241            None,
2242            None,
2243            None,
2244            None,
2245            None,
2246            None,
2247            None,
2248            None,
2249            None,
2250            None,
2251            None,
2252            None,
2253        );
2254
2255        // `FindOptimalSide::Center` must be resolved by the concrete strategy
2256        // before reaching `is_valid_optimal_side` on a leaf `OptionData`. The
2257        // method now rejects the candidate (returns `false`) and logs a
2258        // `tracing::warn!` instead of panicking.
2259        assert!(!option_data.is_valid_optimal_side(&Positive::HUNDRED, &FindOptimalSide::Center));
2260    }
2261
2262    #[test]
2263    fn test_is_valid_optimal_side_delta_range_valid_call() {
2264        // Lines 812-814: Test is_valid_optimal_side for DeltaRange with valid call delta
2265        let option_data = OptionData::new(
2266            Positive::HUNDRED,
2267            None,
2268            None,
2269            None,
2270            None,
2271            pos_or_panic!(0.2), // implied_volatility
2272            Some(dec!(0.3)),    // delta_call within range
2273            None,
2274            None,
2275            None,
2276            None,
2277            None,
2278            None,
2279            None,
2280            None,
2281            None,
2282            None,
2283            None,
2284        );
2285
2286        // DeltaRange with min=0.2, max=0.4, which includes our delta_call=0.3
2287        let result = option_data.is_valid_optimal_side(
2288            &Positive::HUNDRED,
2289            &FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
2290        );
2291
2292        assert!(result);
2293    }
2294
2295    #[test]
2296    fn test_is_valid_optimal_side_delta_range_valid_put() {
2297        // Test is_valid_optimal_side for DeltaRange with valid put delta
2298        let option_data = OptionData::new(
2299            Positive::HUNDRED,
2300            None,
2301            None,
2302            None,
2303            None,
2304            pos_or_panic!(0.2), // implied_volatility
2305            None,
2306            Some(dec!(0.3)), // delta_put within range
2307            None,
2308            None,
2309            None,
2310            None,
2311            None,
2312            None,
2313            None,
2314            None,
2315            None,
2316            None,
2317        );
2318
2319        // DeltaRange with min=0.2, max=0.4, which includes our delta_put=0.3
2320        let result = option_data.is_valid_optimal_side(
2321            &Positive::HUNDRED,
2322            &FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
2323        );
2324
2325        assert!(result);
2326    }
2327
2328    #[test]
2329    fn test_is_valid_optimal_side_delta_range_invalid() {
2330        // Test is_valid_optimal_side for DeltaRange with invalid deltas
2331        let option_data = OptionData::new(
2332            Positive::HUNDRED,
2333            None,
2334            None,
2335            None,
2336            None,
2337            pos_or_panic!(0.2), // implied_volatility
2338            Some(dec!(0.1)),    // delta_call outside range
2339            Some(dec!(0.5)),    // delta_put outside range
2340            None,
2341            None,
2342            None,
2343            None,
2344            None,
2345            None,
2346            None,
2347            None,
2348            None,
2349            None,
2350        );
2351
2352        // DeltaRange with min=0.2, max=0.4, which excludes both delta values
2353        let result = option_data.is_valid_optimal_side(
2354            &Positive::HUNDRED,
2355            &FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
2356        );
2357
2358        assert!(!result);
2359    }
2360
2361    #[test]
2362    fn test_is_valid_optimal_side_delta_range_no_deltas() {
2363        // Test is_valid_optimal_side for DeltaRange when no deltas are present
2364        let option_data = OptionData::new(
2365            Positive::HUNDRED,
2366            None,
2367            None,
2368            None,
2369            None,
2370            pos_or_panic!(0.2), // implied_volatility
2371            None,               // No delta_call
2372            None,               // No delta_put
2373            None,
2374            None,
2375            None,
2376            None,
2377            None,
2378            None,
2379            None,
2380            None,
2381            None,
2382            None,
2383        );
2384
2385        // DeltaRange with min=0.2, max=0.4, but no deltas to check
2386        let result = option_data.is_valid_optimal_side(
2387            &Positive::HUNDRED,
2388            &FindOptimalSide::DeltaRange(dec!(0.2), dec!(0.4)),
2389        );
2390
2391        assert!(!result);
2392    }
2393}
2394
2395#[cfg(test)]
2396mod tests_set_mid_prices {
2397    use super::*;
2398    use positive::{pos_or_panic, spos};
2399
2400    #[test]
2401    fn test_set_mid_prices_with_both_call_prices() {
2402        // Line 852: Test set_mid_prices with both call bid and ask present
2403        let mut option_data = OptionData::new(
2404            Positive::HUNDRED,
2405            spos!(9.0),  // call_bid
2406            spos!(11.0), // call_ask
2407            None,
2408            None,
2409            pos_or_panic!(0.2), // implied_volatility
2410            None,
2411            None,
2412            None,
2413            None,
2414            None,
2415            None,
2416            None,
2417            None,
2418            None,
2419            None,
2420            None,
2421            None,
2422        );
2423
2424        // Call the method being tested
2425        option_data.set_mid_prices();
2426
2427        // Assert that call_middle is set to (9.0 + 11.0) / 2 = 10.0
2428        assert_eq!(option_data.call_middle, spos!(10.0));
2429        // put_middle should still be None
2430        assert_eq!(option_data.put_middle, None);
2431    }
2432
2433    #[test]
2434    fn test_set_mid_prices_with_both_put_prices() {
2435        // Test set_mid_prices with both put bid and ask present
2436        let mut option_data = OptionData::new(
2437            Positive::HUNDRED,
2438            None,
2439            None,
2440            spos!(8.0),         // put_bid
2441            spos!(12.0),        // put_ask
2442            pos_or_panic!(0.2), // implied_volatility
2443            None,
2444            None,
2445            None,
2446            None,
2447            None,
2448            None,
2449            None,
2450            None,
2451            None,
2452            None,
2453            None,
2454            None,
2455        );
2456
2457        // Call the method being tested
2458        option_data.set_mid_prices();
2459
2460        // Assert that put_middle is set to (8.0 + 12.0) / 2 = 10.0
2461        assert_eq!(option_data.put_middle, spos!(10.0));
2462        // call_middle should still be None
2463        assert_eq!(option_data.call_middle, None);
2464    }
2465
2466    #[test]
2467    fn test_set_mid_prices_with_all_prices() {
2468        // Test set_mid_prices with all prices present
2469        let mut option_data = OptionData::new(
2470            Positive::HUNDRED,
2471            spos!(9.0),         // call_bid
2472            spos!(11.0),        // call_ask
2473            spos!(8.0),         // put_bid
2474            spos!(12.0),        // put_ask
2475            pos_or_panic!(0.2), // implied_volatility
2476            None,
2477            None,
2478            None,
2479            None,
2480            None,
2481            None,
2482            None,
2483            None,
2484            None,
2485            None,
2486            None,
2487            None,
2488        );
2489
2490        // Call the method being tested
2491        option_data.set_mid_prices();
2492
2493        // Assert that both middle prices are correctly calculated
2494        assert_eq!(option_data.call_middle, spos!(10.0));
2495        assert_eq!(option_data.put_middle, spos!(10.0));
2496    }
2497
2498    #[test]
2499    fn test_set_mid_prices_with_missing_call_bid() {
2500        // Test set_mid_prices with missing call_bid
2501        let mut option_data = OptionData::new(
2502            Positive::HUNDRED,
2503            None,               // call_bid is missing
2504            spos!(11.0),        // call_ask
2505            spos!(8.0),         // put_bid
2506            spos!(12.0),        // put_ask
2507            pos_or_panic!(0.2), // implied_volatility
2508            None,
2509            None,
2510            None,
2511            None,
2512            None,
2513            None,
2514            None,
2515            None,
2516            None,
2517            None,
2518            None,
2519            None,
2520        );
2521
2522        // Call the method being tested
2523        option_data.set_mid_prices();
2524
2525        // Assert that call_middle is None due to missing bid
2526        assert_eq!(option_data.call_middle, None);
2527        // put_middle should still be calculated
2528        assert_eq!(option_data.put_middle, spos!(10.0));
2529    }
2530
2531    #[test]
2532    fn test_set_mid_prices_with_missing_call_ask() {
2533        // Test set_mid_prices with missing call_ask
2534        let mut option_data = OptionData::new(
2535            Positive::HUNDRED,
2536            spos!(9.0),         // call_bid
2537            None,               // call_ask is missing
2538            spos!(8.0),         // put_bid
2539            spos!(12.0),        // put_ask
2540            pos_or_panic!(0.2), // implied_volatility
2541            None,
2542            None,
2543            None,
2544            None,
2545            None,
2546            None,
2547            None,
2548            None,
2549            None,
2550            None,
2551            None,
2552            None,
2553        );
2554
2555        // Call the method being tested
2556        option_data.set_mid_prices();
2557
2558        // Assert that call_middle is None due to missing ask
2559        assert_eq!(option_data.call_middle, None);
2560        // put_middle should still be calculated
2561        assert_eq!(option_data.put_middle, spos!(10.0));
2562    }
2563}
2564
2565#[cfg(test)]
2566mod tests_get_mid_prices {
2567    use super::*;
2568    use positive::{pos_or_panic, spos};
2569
2570    #[test]
2571    fn test_get_mid_prices_with_both_mid_prices() {
2572        // Lines 885, 887, 889-895: Test get_mid_prices when both mid prices are set
2573        let mut option_data = OptionData::new(
2574            Positive::HUNDRED,
2575            spos!(9.0),
2576            spos!(11.0),
2577            spos!(8.0),
2578            spos!(12.0),
2579            pos_or_panic!(0.2), // implied_volatility
2580            None,
2581            None,
2582            None,
2583            None,
2584            None,
2585            None,
2586            None,
2587            None,
2588            None,
2589            None,
2590            None,
2591            None,
2592        );
2593
2594        // First set the mid prices
2595        option_data.set_mid_prices();
2596
2597        // Then test getting them
2598        let (call_mid, put_mid) = option_data.get_mid_prices();
2599
2600        // Verify returned values
2601        assert_eq!(call_mid, spos!(10.0));
2602        assert_eq!(put_mid, spos!(10.0));
2603    }
2604
2605    #[test]
2606    fn test_get_mid_prices_with_only_call_mid() {
2607        // Test get_mid_prices when only call_middle is set
2608        let mut option_data = OptionData::new(
2609            Positive::HUNDRED,
2610            spos!(9.0),
2611            spos!(11.0),
2612            None, // missing put_bid
2613            spos!(12.0),
2614            pos_or_panic!(0.2), // implied_volatility
2615            None,
2616            None,
2617            None,
2618            None,
2619            None,
2620            None,
2621            None,
2622            None,
2623            None,
2624            None,
2625            None,
2626            None,
2627        );
2628
2629        // First set the mid prices
2630        option_data.set_mid_prices();
2631
2632        // Then test getting them
2633        let (call_mid, put_mid) = option_data.get_mid_prices();
2634
2635        // Verify returned values
2636        assert_eq!(call_mid, spos!(10.0));
2637        assert_eq!(put_mid, None);
2638    }
2639
2640    #[test]
2641    fn test_get_mid_prices_with_only_put_mid() {
2642        // Test get_mid_prices when only put_middle is set
2643        let mut option_data = OptionData::new(
2644            Positive::HUNDRED,
2645            None, // missing call_bid
2646            spos!(11.0),
2647            spos!(8.0),
2648            spos!(12.0),
2649            pos_or_panic!(0.2), // implied_volatility
2650            None,
2651            None,
2652            None,
2653            None,
2654            None,
2655            None,
2656            None,
2657            None,
2658            None,
2659            None,
2660            None,
2661            None,
2662        );
2663
2664        // First set the mid prices
2665        option_data.set_mid_prices();
2666
2667        // Then test getting them
2668        let (call_mid, put_mid) = option_data.get_mid_prices();
2669
2670        // Verify returned values
2671        assert_eq!(call_mid, None);
2672        assert_eq!(put_mid, spos!(10.0));
2673    }
2674
2675    #[test]
2676    fn test_get_mid_prices_with_no_mid_prices() {
2677        // Test get_mid_prices when no mid prices are set
2678        let option_data = OptionData::new(
2679            Positive::HUNDRED,
2680            None,
2681            None,
2682            None,
2683            None,
2684            pos_or_panic!(0.2), // implied_volatility
2685            None,
2686            None,
2687            None,
2688            None,
2689            None,
2690            None,
2691            None,
2692            None,
2693            None,
2694            None,
2695            None,
2696            None,
2697        );
2698
2699        // Mid prices haven't been set, should both be None
2700        let (call_mid, put_mid) = option_data.get_mid_prices();
2701
2702        // Verify returned values
2703        assert_eq!(call_mid, None);
2704        assert_eq!(put_mid, None);
2705    }
2706}
2707
2708#[cfg(test)]
2709mod tests_current_deltas {
2710    use super::*;
2711    use positive::pos_or_panic;
2712    use rust_decimal_macros::dec;
2713
2714    #[test]
2715    fn test_current_deltas_with_both_deltas() {
2716        // Lines 933-934: Test current_deltas method when both deltas are present
2717        let option_data = OptionData::new(
2718            Positive::HUNDRED,
2719            None,
2720            None,
2721            None,
2722            None,
2723            pos_or_panic!(0.2), // implied_volatility
2724            Some(dec!(0.5)),    // delta_call
2725            Some(dec!(-0.5)),   // delta_put
2726            None,
2727            None,
2728            None,
2729            None,
2730            None,
2731            None,
2732            None,
2733            None,
2734            None,
2735            None,
2736        );
2737
2738        // Get current deltas
2739        let (call_delta, put_delta) = option_data.current_deltas();
2740
2741        // Verify returned values
2742        assert_eq!(call_delta, Some(dec!(0.5)));
2743        assert_eq!(put_delta, Some(dec!(-0.5)));
2744    }
2745
2746    #[test]
2747    fn test_current_deltas_with_only_call_delta() {
2748        // Test current_deltas with only call delta
2749        let option_data = OptionData::new(
2750            Positive::HUNDRED,
2751            None,
2752            None,
2753            None,
2754            None,
2755            pos_or_panic!(0.2), // implied_volatility
2756            Some(dec!(0.5)),    // delta_call
2757            None,               // No delta_put
2758            None,
2759            None,
2760            None,
2761            None,
2762            None,
2763            None,
2764            None,
2765            None,
2766            None,
2767            None,
2768        );
2769
2770        // Get current deltas
2771        let (call_delta, put_delta) = option_data.current_deltas();
2772
2773        // Verify returned values
2774        assert_eq!(call_delta, Some(dec!(0.5)));
2775        assert_eq!(put_delta, None);
2776    }
2777
2778    #[test]
2779    fn test_current_deltas_with_only_put_delta() {
2780        // Test current_deltas with only put delta
2781        let option_data = OptionData::new(
2782            Positive::HUNDRED,
2783            None,
2784            None,
2785            None,
2786            None,
2787            pos_or_panic!(0.2), // implied_volatility
2788            None,               // No delta_call
2789            Some(dec!(-0.5)),   // delta_put
2790            None,
2791            None,
2792            None,
2793            None,
2794            None,
2795            None,
2796            None,
2797            None,
2798            None,
2799            None,
2800        );
2801
2802        // Get current deltas
2803        let (call_delta, put_delta) = option_data.current_deltas();
2804
2805        // Verify returned values
2806        assert_eq!(call_delta, None);
2807        assert_eq!(put_delta, Some(dec!(-0.5)));
2808    }
2809
2810    #[test]
2811    fn test_current_deltas_with_no_deltas() {
2812        // Test current_deltas with no deltas
2813        let option_data = OptionData::new(
2814            Positive::HUNDRED,
2815            None,
2816            None,
2817            None,
2818            None,
2819            pos_or_panic!(0.2), // implied_volatility
2820            None,               // No delta_call
2821            None,               // No delta_put
2822            None,
2823            None,
2824            None,
2825            None,
2826            None,
2827            None,
2828            None,
2829            None,
2830            None,
2831            None,
2832        );
2833
2834        // Get current deltas
2835        let (call_delta, put_delta) = option_data.current_deltas();
2836
2837        // Verify returned values
2838        assert_eq!(call_delta, None);
2839        assert_eq!(put_delta, None);
2840    }
2841}
2842
2843#[cfg(test)]
2844mod tests_spreads {
2845    use super::*;
2846    use positive::{pos_or_panic, spos};
2847
2848    #[test]
2849    fn test_get_call_spread_some() {
2850        let option_data = OptionData::new(
2851            Positive::HUNDRED,
2852            spos!(100.0),
2853            spos!(110.0),
2854            spos!(8.5),
2855            spos!(9.0),
2856            pos_or_panic!(0.2),
2857            None,
2858            None,
2859            None,
2860            None,
2861            None,
2862            None,
2863            None,
2864            None,
2865            None,
2866            None,
2867            None,
2868            None,
2869        );
2870        assert_eq!(option_data.get_call_spread(), Some(pos_or_panic!(10.0)));
2871    }
2872
2873    #[test]
2874    fn test_get_call_spread_none_when_missing_prices() {
2875        // Missing call_bid
2876        let od1 = OptionData::new(
2877            Positive::HUNDRED,
2878            None,
2879            spos!(10.0),
2880            None,
2881            None,
2882            pos_or_panic!(0.2),
2883            None,
2884            None,
2885            None,
2886            None,
2887            None,
2888            None,
2889            None,
2890            None,
2891            None,
2892            None,
2893            None,
2894            None,
2895        );
2896        assert_eq!(od1.get_call_spread(), None);
2897
2898        // Missing call_ask
2899        let od2 = OptionData::new(
2900            Positive::HUNDRED,
2901            spos!(9.5),
2902            None,
2903            None,
2904            None,
2905            pos_or_panic!(0.2),
2906            None,
2907            None,
2908            None,
2909            None,
2910            None,
2911            None,
2912            None,
2913            None,
2914            None,
2915            None,
2916            None,
2917            None,
2918        );
2919        assert_eq!(od2.get_call_spread(), None);
2920    }
2921
2922    #[test]
2923    fn test_get_call_spread_per_some() {
2924        let option_data = OptionData::new(
2925            pos_or_panic!(95.0),
2926            spos!(95.0),
2927            spos!(105.0),
2928            None,
2929            None,
2930            pos_or_panic!(0.2),
2931            None,
2932            None,
2933            None,
2934            None,
2935            None,
2936            None,
2937            None,
2938            None,
2939            None,
2940            None,
2941            None,
2942            None,
2943        );
2944        let spread_per = option_data.get_call_spread_per().unwrap();
2945        let got = spread_per.to_f64();
2946        let expected = 0.1;
2947        assert!((got - expected).abs() < 1e-12);
2948    }
2949
2950    #[test]
2951    fn test_get_call_spread_per_none_when_missing_prices() {
2952        let od = OptionData::new(
2953            Positive::HUNDRED,
2954            None,
2955            spos!(10.0),
2956            None,
2957            None,
2958            pos_or_panic!(0.2),
2959            None,
2960            None,
2961            None,
2962            None,
2963            None,
2964            None,
2965            None,
2966            None,
2967            None,
2968            None,
2969            None,
2970            None,
2971        );
2972        assert_eq!(od.get_call_spread_per(), None);
2973    }
2974
2975    #[test]
2976    fn test_get_put_spread_some() {
2977        let option_data = OptionData::new(
2978            Positive::HUNDRED,
2979            None,
2980            None,
2981            spos!(8.5),
2982            spos!(9.0),
2983            pos_or_panic!(0.2),
2984            None,
2985            None,
2986            None,
2987            None,
2988            None,
2989            None,
2990            None,
2991            None,
2992            None,
2993            None,
2994            None,
2995            None,
2996        );
2997        assert_eq!(option_data.get_put_spread(), Some(pos_or_panic!(0.5)));
2998    }
2999
3000    #[test]
3001    fn test_get_put_spread_none_when_missing_prices() {
3002        // Missing put_bid
3003        let od1 = OptionData::new(
3004            Positive::HUNDRED,
3005            None,
3006            None,
3007            None,
3008            spos!(9.0),
3009            pos_or_panic!(0.2),
3010            None,
3011            None,
3012            None,
3013            None,
3014            None,
3015            None,
3016            None,
3017            None,
3018            None,
3019            None,
3020            None,
3021            None,
3022        );
3023        assert_eq!(od1.get_put_spread(), None);
3024
3025        // Missing put_ask
3026        let od2 = OptionData::new(
3027            Positive::HUNDRED,
3028            None,
3029            None,
3030            spos!(8.5),
3031            None,
3032            pos_or_panic!(0.2),
3033            None,
3034            None,
3035            None,
3036            None,
3037            None,
3038            None,
3039            None,
3040            None,
3041            None,
3042            None,
3043            None,
3044            None,
3045        );
3046        assert_eq!(od2.get_put_spread(), None);
3047    }
3048
3049    #[test]
3050    fn test_get_put_spread_per_some() {
3051        let option_data = OptionData::new(
3052            Positive::HUNDRED,
3053            None,
3054            None,
3055            spos!(95.0),
3056            spos!(105.0),
3057            pos_or_panic!(0.2),
3058            None,
3059            None,
3060            None,
3061            None,
3062            None,
3063            None,
3064            None,
3065            None,
3066            None,
3067            None,
3068            None,
3069            None,
3070        );
3071        let spread_per = option_data.get_put_spread_per().unwrap();
3072        let got = spread_per.to_f64();
3073        let expected = 0.1;
3074        assert!((got - expected).abs() < 1e-12);
3075    }
3076
3077    #[test]
3078    fn test_get_put_spread_per_none_when_missing_prices() {
3079        let od = OptionData::new(
3080            Positive::HUNDRED,
3081            None,
3082            None,
3083            None,
3084            spos!(9.0),
3085            pos_or_panic!(0.2),
3086            None,
3087            None,
3088            None,
3089            None,
3090            None,
3091            None,
3092            None,
3093            None,
3094            None,
3095            None,
3096            None,
3097            None,
3098        );
3099        assert_eq!(od.get_put_spread_per(), None);
3100    }
3101}
3102
3103#[cfg(test)]
3104mod tests_validate_option_data {
3105    use super::*;
3106    use positive::pos_or_panic;
3107    use positive::spos;
3108    use rust_decimal_macros::dec;
3109
3110    #[test]
3111    fn test_validate_option_data_missing_strike_price() {
3112        let option_data = OptionData::new(
3113            Positive::ZERO,     // strike_price is zero
3114            spos!(9.5),         // call_bid
3115            spos!(10.0),        // call_ask
3116            spos!(8.5),         // put_bid
3117            spos!(9.0),         // put_ask
3118            pos_or_panic!(0.2), // implied_volatility
3119            Some(dec!(-0.3)),
3120            Some(dec!(0.7)),
3121            Some(dec!(0.5)),
3122            spos!(1000.0),
3123            Some(500),
3124            Some("TEST".to_string()),
3125            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
3126            Some(Box::new(Positive::HUNDRED)),
3127            Some(dec!(0.05)),
3128            Some(pos_or_panic!(0.02)),
3129            None,
3130            None,
3131        );
3132        // Option data is not valid because the strike price is zero
3133        let is_valid = option_data.validate();
3134        assert!(!is_valid);
3135    }
3136
3137    #[test]
3138    fn test_validate_option_data_missing_call_bid() {
3139        let option_data = OptionData::new(
3140            Positive::HUNDRED,  // strike_price
3141            None,               // call_bid is not provided
3142            spos!(10.0),        // call_ask
3143            spos!(8.5),         // put_bid
3144            spos!(9.0),         // put_ask
3145            pos_or_panic!(0.2), // implied_volatility
3146            Some(dec!(-0.3)),
3147            Some(dec!(0.7)),
3148            Some(dec!(0.5)),
3149            spos!(1000.0),
3150            Some(500),
3151            Some("TEST".to_string()),
3152            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
3153            Some(Box::new(Positive::HUNDRED)),
3154            Some(dec!(0.05)),
3155            Some(pos_or_panic!(0.02)),
3156            None,
3157            None,
3158        );
3159        // Option data is not valid because the call bid price is not provided
3160        let is_valid = option_data.validate();
3161        assert!(!is_valid);
3162    }
3163
3164    #[test]
3165    fn test_validate_option_data_missing_call_ask() {
3166        let option_data = OptionData::new(
3167            Positive::HUNDRED,  // strike_price
3168            spos!(9.5),         // call_bid
3169            None,               // call_ask is not provided
3170            spos!(8.5),         // put_bid
3171            spos!(9.0),         // put_ask
3172            pos_or_panic!(0.2), // implied_volatility is zero
3173            Some(dec!(-0.3)),
3174            Some(dec!(0.7)),
3175            Some(dec!(0.5)),
3176            spos!(1000.0),
3177            Some(500),
3178            Some("TEST".to_string()),
3179            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
3180            Some(Box::new(Positive::HUNDRED)),
3181            Some(dec!(0.05)),
3182            Some(pos_or_panic!(0.02)),
3183            None,
3184            None,
3185        );
3186        // Option data is not valid because the call ask price is not provided
3187        let is_valid = option_data.validate();
3188        assert!(!is_valid);
3189    }
3190
3191    #[test]
3192    fn test_validate_option_data_missing_put_bid() {
3193        let option_data = OptionData::new(
3194            Positive::HUNDRED,  // strike_price
3195            spos!(9.5),         // call_bid
3196            spos!(10.0),        // call_ask
3197            None,               // put_bid is not provided
3198            spos!(9.0),         // put_ask
3199            pos_or_panic!(0.2), // implied_volatility
3200            Some(dec!(-0.3)),
3201            Some(dec!(0.7)),
3202            Some(dec!(0.5)),
3203            spos!(1000.0),
3204            Some(500),
3205            Some("TEST".to_string()),
3206            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
3207            Some(Box::new(Positive::HUNDRED)),
3208            Some(dec!(0.05)),
3209            Some(pos_or_panic!(0.02)),
3210            None,
3211            None,
3212        );
3213        // Option data is not valid because the put bid price is not provided
3214        let is_valid = option_data.validate();
3215        assert!(!is_valid);
3216    }
3217
3218    #[test]
3219    fn test_validate_option_data_missing_put_ask() {
3220        let option_data = OptionData::new(
3221            Positive::HUNDRED,  // strike_price
3222            spos!(9.5),         // call_bid
3223            spos!(10.0),        // call_ask
3224            spos!(8.5),         // put_bid
3225            None,               // put_ask is not provided
3226            pos_or_panic!(0.2), // implied_volatility
3227            Some(dec!(-0.3)),
3228            Some(dec!(0.7)),
3229            Some(dec!(0.5)),
3230            spos!(1000.0),
3231            Some(500),
3232            Some("TEST".to_string()),
3233            Some(ExpirationDate::Days(pos_or_panic!(30.0))),
3234            Some(Box::new(Positive::HUNDRED)),
3235            Some(dec!(0.05)),
3236            Some(pos_or_panic!(0.02)),
3237            None,
3238            None,
3239        );
3240        // Option data is not valid because the put ask price is not provided
3241        let is_valid = option_data.validate();
3242        assert!(!is_valid);
3243    }
3244}