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}