Skip to main content

alpaca_option/
types.rs

1use alpaca_core::float;
2use rust_decimal::Decimal;
3use rust_decimal::prelude::ToPrimitive;
4use serde::{Deserialize, Deserializer, Serialize};
5use ts_rs::TS;
6
7use crate::contract;
8use crate::error::{OptionError, OptionResult};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
11#[serde(rename_all = "lowercase")]
12pub enum OptionRight {
13    Call,
14    Put,
15}
16
17impl Default for OptionRight {
18    fn default() -> Self {
19        Self::Call
20    }
21}
22
23impl OptionRight {
24    pub fn from_str(input: &str) -> OptionResult<Self> {
25        match input.trim().to_ascii_lowercase().as_str() {
26            "call" => Ok(Self::Call),
27            "put" => Ok(Self::Put),
28            "c" => Ok(Self::Call),
29            "p" => Ok(Self::Put),
30            _ => Err(OptionError::new(
31                "invalid_option_right",
32                format!("invalid option right: {input}"),
33            )),
34        }
35    }
36
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::Call => "call",
40            Self::Put => "put",
41        }
42    }
43
44    pub fn from_code(code: char) -> OptionResult<Self> {
45        match code {
46            'C' => Ok(Self::Call),
47            'P' => Ok(Self::Put),
48            _ => Err(OptionError::new(
49                "invalid_option_right_code",
50                format!("invalid option right code: {code}"),
51            )),
52        }
53    }
54
55    pub fn code(&self) -> char {
56        match self {
57            Self::Call => 'C',
58            Self::Put => 'P',
59        }
60    }
61
62    pub fn code_string(&self) -> OptionRightCode {
63        match self {
64            Self::Call => OptionRightCode::C,
65            Self::Put => OptionRightCode::P,
66        }
67    }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub enum OptionRightCode {
72    C,
73    P,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "lowercase")]
78pub enum OrderSide {
79    Buy,
80    Sell,
81}
82
83impl OrderSide {
84    pub fn from_str(input: &str) -> OptionResult<Self> {
85        match input.trim().to_ascii_lowercase().as_str() {
86            "buy" => Ok(Self::Buy),
87            "sell" => Ok(Self::Sell),
88            _ => Err(OptionError::new(
89                "invalid_order_side",
90                format!("invalid order side: {input}"),
91            )),
92        }
93    }
94
95    pub fn as_str(&self) -> &'static str {
96        match self {
97            Self::Buy => "buy",
98            Self::Sell => "sell",
99        }
100    }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum PositionSide {
106    Long,
107    Short,
108}
109
110impl PositionSide {
111    pub fn from_str(input: &str) -> OptionResult<Self> {
112        match input {
113            "long" => Ok(Self::Long),
114            "short" => Ok(Self::Short),
115            _ => Err(OptionError::new(
116                "invalid_position_side",
117                format!("invalid position side: {input}"),
118            )),
119        }
120    }
121
122    pub fn as_str(&self) -> &'static str {
123        match self {
124            Self::Long => "long",
125            Self::Short => "short",
126        }
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "lowercase")]
132pub enum ExecutionAction {
133    Open,
134    Close,
135}
136
137impl ExecutionAction {
138    pub fn from_str(input: &str) -> OptionResult<Self> {
139        match input {
140            "open" => Ok(Self::Open),
141            "close" => Ok(Self::Close),
142            _ => Err(OptionError::new(
143                "invalid_execution_quote_input",
144                format!("invalid execution action: {input}"),
145            )),
146        }
147    }
148
149    pub fn as_str(&self) -> &'static str {
150        match self {
151            Self::Open => "open",
152            Self::Close => "close",
153        }
154    }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum PositionIntent {
160    BuyToOpen,
161    SellToOpen,
162    BuyToClose,
163    SellToClose,
164}
165
166impl PositionIntent {
167    pub fn from_str(input: &str) -> OptionResult<Self> {
168        match input.trim().to_ascii_lowercase().as_str() {
169            "buy_to_open" => Ok(Self::BuyToOpen),
170            "sell_to_open" => Ok(Self::SellToOpen),
171            "buy_to_close" => Ok(Self::BuyToClose),
172            "sell_to_close" => Ok(Self::SellToClose),
173            _ => Err(OptionError::new(
174                "invalid_position_intent",
175                format!("invalid position intent: {input}"),
176            )),
177        }
178    }
179
180    pub fn as_str(&self) -> &'static str {
181        match self {
182            Self::BuyToOpen => "buy_to_open",
183            Self::SellToOpen => "sell_to_open",
184            Self::BuyToClose => "buy_to_close",
185            Self::SellToClose => "sell_to_close",
186        }
187    }
188
189    pub fn is_close(&self) -> bool {
190        matches!(self, Self::BuyToClose | Self::SellToClose)
191    }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(rename_all = "lowercase")]
196pub enum MoneynessLabel {
197    Itm,
198    Atm,
199    Otm,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum AssignmentRiskLevel {
205    Danger,
206    Critical,
207    High,
208    Medium,
209    Low,
210    Safe,
211}
212
213impl AssignmentRiskLevel {
214    pub fn as_str(&self) -> &'static str {
215        match self {
216            Self::Danger => "danger",
217            Self::Critical => "critical",
218            Self::High => "high",
219            Self::Medium => "medium",
220            Self::Low => "low",
221            Self::Safe => "safe",
222        }
223    }
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
227pub struct OptionContract {
228    pub underlying_symbol: String,
229    pub expiration_date: String,
230    pub strike: f64,
231    pub option_right: OptionRight,
232    pub occ_symbol: String,
233}
234
235impl Default for OptionContract {
236    fn default() -> Self {
237        Self {
238            underlying_symbol: String::new(),
239            expiration_date: String::new(),
240            strike: 0.0,
241            option_right: OptionRight::default(),
242            occ_symbol: String::new(),
243        }
244    }
245}
246
247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
248pub struct OptionQuote {
249    pub bid: Option<f64>,
250    pub ask: Option<f64>,
251    pub mark: Option<f64>,
252    pub last: Option<f64>,
253}
254
255impl Default for OptionQuote {
256    fn default() -> Self {
257        Self {
258            bid: None,
259            ask: None,
260            mark: None,
261            last: None,
262        }
263    }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct ContractDisplay {
268    pub strike: String,
269    pub expiration: String,
270    pub compact: String,
271    pub option_right_code: OptionRightCode,
272}
273
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
275pub struct Greeks {
276    pub delta: f64,
277    pub gamma: f64,
278    pub vega: f64,
279    pub theta: f64,
280    pub rho: f64,
281}
282
283impl Default for Greeks {
284    fn default() -> Self {
285        Self {
286            delta: 0.0,
287            gamma: 0.0,
288            vega: 0.0,
289            theta: 0.0,
290            rho: 0.0,
291        }
292    }
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
296pub struct BlackScholesInput {
297    pub spot: f64,
298    pub strike: f64,
299    pub years: f64,
300    pub rate: f64,
301    pub dividend_yield: f64,
302    pub volatility: f64,
303    pub option_right: OptionRight,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct BlackScholesImpliedVolatilityInput {
308    pub target_price: f64,
309    pub spot: f64,
310    pub strike: f64,
311    pub years: f64,
312    pub rate: f64,
313    pub dividend_yield: f64,
314    pub option_right: OptionRight,
315    pub lower_bound: Option<f64>,
316    pub upper_bound: Option<f64>,
317    pub tolerance: Option<f64>,
318    pub max_iterations: Option<usize>,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
322pub struct OptionSnapshot {
323    pub as_of: String,
324    pub contract: OptionContract,
325    pub quote: OptionQuote,
326    pub greeks: Option<Greeks>,
327    pub implied_volatility: Option<f64>,
328    pub underlying_price: Option<f64>,
329}
330
331impl Default for OptionSnapshot {
332    fn default() -> Self {
333        Self {
334            as_of: String::new(),
335            contract: OptionContract::default(),
336            quote: OptionQuote::default(),
337            greeks: None,
338            implied_volatility: None,
339            underlying_price: None,
340        }
341    }
342}
343
344fn parse_snapshot_number(input: &str) -> Option<f64> {
345    let trimmed = input.trim();
346    if trimmed.is_empty() {
347        return None;
348    }
349
350    let value = trimmed.parse::<f64>().ok()?;
351    value.is_finite().then_some(value)
352}
353
354fn format_snapshot_number(value: f64) -> String {
355    float::round(value, 2).to_string()
356}
357
358fn normalized_quote_price(quote: &OptionQuote) -> f64 {
359    if let Some(mark) = quote.mark.filter(|value| value.is_finite()) {
360        return mark;
361    }
362
363    match (
364        quote.bid.filter(|value| value.is_finite()),
365        quote.ask.filter(|value| value.is_finite()),
366    ) {
367        (Some(bid), Some(ask)) => float::round((bid + ask) / 2.0, 12),
368        (Some(bid), None) => bid,
369        (None, Some(ask)) => ask,
370        (None, None) => quote.last.filter(|value| value.is_finite()).unwrap_or(0.0),
371    }
372}
373
374fn canonical_contract_or_fallback(occ_symbol: &str) -> OptionContract {
375    let normalized = occ_symbol.trim().to_ascii_uppercase();
376    contract::parse_occ_symbol(&normalized).unwrap_or(OptionContract {
377        occ_symbol: normalized,
378        ..OptionContract::default()
379    })
380}
381
382impl OptionSnapshot {
383    pub fn is_empty(&self) -> bool {
384        self.as_of.trim().is_empty()
385            && self.contract.occ_symbol.trim().is_empty()
386            && self.quote == OptionQuote::default()
387            && self.greeks.is_none()
388            && self.implied_volatility.is_none()
389            && self.underlying_price.is_none()
390    }
391
392    pub fn occ_symbol(&self) -> &str {
393        &self.contract.occ_symbol
394    }
395
396    pub fn timestamp(&self) -> &str {
397        &self.as_of
398    }
399
400    pub fn bid(&self) -> f64 {
401        self.quote
402            .bid
403            .filter(|value| value.is_finite())
404            .unwrap_or(0.0)
405    }
406
407    pub fn ask(&self) -> f64 {
408        self.quote
409            .ask
410            .filter(|value| value.is_finite())
411            .unwrap_or(0.0)
412    }
413
414    pub fn price(&self) -> f64 {
415        normalized_quote_price(&self.quote)
416    }
417
418    pub fn iv(&self) -> f64 {
419        self.implied_volatility
420            .filter(|value| value.is_finite())
421            .unwrap_or(0.0)
422    }
423
424    pub fn delta(&self) -> f64 {
425        self.greeks_or_default().delta
426    }
427
428    pub fn gamma(&self) -> f64 {
429        self.greeks_or_default().gamma
430    }
431
432    pub fn vega(&self) -> f64 {
433        self.greeks_or_default().vega
434    }
435
436    pub fn theta(&self) -> f64 {
437        self.greeks_or_default().theta
438    }
439
440    pub fn rho(&self) -> f64 {
441        self.greeks_or_default().rho
442    }
443
444    pub fn underlying_price(&self) -> f64 {
445        self.underlying_price
446            .filter(|value| value.is_finite())
447            .unwrap_or(0.0)
448    }
449
450    pub fn greeks_or_default(&self) -> Greeks {
451        self.greeks.clone().unwrap_or_default()
452    }
453}
454
455fn deserialize_position_snapshot<'de, D>(deserializer: D) -> Result<OptionSnapshot, D::Error>
456where
457    D: Deserializer<'de>,
458{
459    Ok(Option::<OptionSnapshot>::deserialize(deserializer)?.unwrap_or_default())
460}
461
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
463pub struct OptionPosition {
464    pub contract: String,
465    #[serde(default, deserialize_with = "deserialize_position_snapshot")]
466    pub snapshot: OptionSnapshot,
467    pub qty: i32,
468    #[serde(with = "alpaca_core::decimal::price_string_contract")]
469    #[ts(type = "string")]
470    pub avg_cost: Decimal,
471    pub leg_type: String,
472}
473
474fn position_side_from_qty_and_leg_type(qty: i32, leg_type: &str) -> PositionSide {
475    if qty < 0 {
476        PositionSide::Short
477    } else if qty > 0 {
478        PositionSide::Long
479    } else if leg_type.trim().to_ascii_lowercase().starts_with("short") {
480        PositionSide::Short
481    } else {
482        PositionSide::Long
483    }
484}
485
486impl OptionPosition {
487    pub fn occ_symbol(&self) -> &str {
488        self.contract.trim()
489    }
490
491    pub fn qty(&self) -> i32 {
492        self.qty
493    }
494
495    pub fn contract_info(&self) -> OptionContract {
496        canonical_contract_or_fallback(&self.contract)
497    }
498
499    pub fn position_side(&self) -> PositionSide {
500        position_side_from_qty_and_leg_type(self.qty, &self.leg_type())
501    }
502
503    pub fn quantity(&self) -> u32 {
504        self.qty.unsigned_abs()
505    }
506
507    pub fn snapshot_ref(&self) -> Option<&OptionSnapshot> {
508        (!self.snapshot.is_empty()).then_some(&self.snapshot)
509    }
510
511    pub fn avg_cost(&self) -> f64 {
512        self.avg_cost.to_f64().unwrap_or(0.0)
513    }
514
515    pub fn leg_type(&self) -> String {
516        if !self.leg_type.trim().is_empty() {
517            return self.leg_type.trim().to_ascii_lowercase();
518        }
519
520        let contract = self.contract_info();
521        format!(
522            "{}{}",
523            self.position_side().as_str(),
524            contract.option_right.as_str()
525        )
526    }
527
528    pub fn cost(&self) -> Decimal {
529        self.avg_cost * Decimal::from(self.qty) * Decimal::from(100)
530    }
531
532    pub fn value(&self) -> Decimal {
533        alpaca_core::decimal::from_f64(self.snapshot.price(), 2)
534            * Decimal::from(self.qty)
535            * Decimal::from(100)
536    }
537
538    pub fn marked_value(&self) -> Decimal {
539        self.value()
540    }
541}
542
543impl Default for OptionPosition {
544    fn default() -> Self {
545        Self {
546            contract: String::new(),
547            snapshot: OptionSnapshot::default(),
548            qty: 0,
549            avg_cost: Decimal::ZERO,
550            leg_type: String::new(),
551        }
552    }
553}
554
555impl TryFrom<&OptionPosition> for StrategyValuationPosition {
556    type Error = OptionError;
557
558    fn try_from(value: &OptionPosition) -> Result<Self, Self::Error> {
559        let contract = contract::parse_occ_symbol(value.occ_symbol()).ok_or_else(|| {
560            OptionError::new(
561                "invalid_occ_symbol",
562                format!("invalid occ symbol: {}", value.occ_symbol()),
563            )
564        })?;
565
566        Ok(Self {
567            contract,
568            quantity: value.qty,
569            avg_entry_price: Some(value.avg_cost()),
570            implied_volatility: value.snapshot_ref().map(|snapshot| snapshot.iv()),
571            mark_price: value.snapshot_ref().map(|snapshot| snapshot.price()),
572            reference_underlying_price: value
573                .snapshot_ref()
574                .map(|snapshot| snapshot.underlying_price()),
575        })
576    }
577}
578
579#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
580pub struct ShortItmPosition {
581    pub contract: OptionContract,
582    pub quantity: u32,
583    pub option_price: f64,
584    pub intrinsic: f64,
585    pub extrinsic: f64,
586}
587
588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
589pub struct StrategyLegInput {
590    pub contract: OptionContract,
591    pub order_side: OrderSide,
592    pub ratio_quantity: u32,
593    pub premium_per_contract: Option<f64>,
594}
595
596#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
597pub struct QuotedLeg {
598    pub contract: OptionContract,
599    pub order_side: OrderSide,
600    pub ratio_quantity: u32,
601    pub quote: OptionQuote,
602    pub snapshot: Option<OptionSnapshot>,
603}
604
605#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
606pub struct GreeksInput {
607    pub delta: Option<f64>,
608    pub gamma: Option<f64>,
609    pub vega: Option<f64>,
610    pub theta: Option<f64>,
611    pub rho: Option<f64>,
612}
613
614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
615pub struct ExecutionSnapshot {
616    pub contract: String,
617    pub timestamp: String,
618    pub bid: String,
619    pub ask: String,
620    pub price: String,
621    pub greeks: Greeks,
622    pub iv: f64,
623}
624
625impl From<ExecutionSnapshot> for OptionSnapshot {
626    fn from(value: ExecutionSnapshot) -> Self {
627        Self {
628            as_of: value.timestamp.trim().to_string(),
629            contract: canonical_contract_or_fallback(&value.contract),
630            quote: OptionQuote {
631                bid: parse_snapshot_number(&value.bid),
632                ask: parse_snapshot_number(&value.ask),
633                mark: parse_snapshot_number(&value.price),
634                last: parse_snapshot_number(&value.price),
635            },
636            greeks: Some(value.greeks),
637            implied_volatility: value.iv.is_finite().then_some(value.iv),
638            underlying_price: None,
639        }
640    }
641}
642
643impl From<&ExecutionSnapshot> for OptionSnapshot {
644    fn from(value: &ExecutionSnapshot) -> Self {
645        Self::from(value.clone())
646    }
647}
648
649impl From<&OptionSnapshot> for ExecutionSnapshot {
650    fn from(value: &OptionSnapshot) -> Self {
651        Self {
652            contract: value.occ_symbol().to_string(),
653            timestamp: value.timestamp().to_string(),
654            bid: format_snapshot_number(value.bid()),
655            ask: format_snapshot_number(value.ask()),
656            price: format_snapshot_number(value.price()),
657            greeks: value.greeks_or_default(),
658            iv: value.iv(),
659        }
660    }
661}
662
663impl From<OptionSnapshot> for ExecutionSnapshot {
664    fn from(value: OptionSnapshot) -> Self {
665        Self::from(&value)
666    }
667}
668
669impl From<&OptionSnapshot> for OptionChainRecord {
670    fn from(value: &OptionSnapshot) -> Self {
671        Self {
672            as_of: value.timestamp().to_string(),
673            underlying_symbol: value.contract.underlying_symbol.clone(),
674            occ_symbol: value.occ_symbol().to_string(),
675            expiration_date: value.contract.expiration_date.clone(),
676            option_right: value.contract.option_right.clone(),
677            strike: value.contract.strike,
678            underlying_price: value.underlying_price.filter(|number| number.is_finite()),
679            bid: value.quote.bid.filter(|number| number.is_finite()),
680            ask: value.quote.ask.filter(|number| number.is_finite()),
681            mark: value.quote.mark.filter(|number| number.is_finite()),
682            last: value.quote.last.filter(|number| number.is_finite()),
683            implied_volatility: value.implied_volatility.filter(|number| number.is_finite()),
684            delta: value
685                .greeks
686                .as_ref()
687                .map(|greeks| greeks.delta)
688                .filter(|number| number.is_finite()),
689            gamma: value
690                .greeks
691                .as_ref()
692                .map(|greeks| greeks.gamma)
693                .filter(|number| number.is_finite()),
694            vega: value
695                .greeks
696                .as_ref()
697                .map(|greeks| greeks.vega)
698                .filter(|number| number.is_finite()),
699            theta: value
700                .greeks
701                .as_ref()
702                .map(|greeks| greeks.theta)
703                .filter(|number| number.is_finite()),
704            rho: value
705                .greeks
706                .as_ref()
707                .map(|greeks| greeks.rho)
708                .filter(|number| number.is_finite()),
709        }
710    }
711}
712
713impl From<OptionSnapshot> for OptionChainRecord {
714    fn from(value: OptionSnapshot) -> Self {
715        Self::from(&value)
716    }
717}
718
719impl From<&OptionChainRecord> for OptionSnapshot {
720    fn from(value: &OptionChainRecord) -> Self {
721        Self {
722            as_of: value.as_of.trim().to_string(),
723            contract: canonical_contract_or_fallback(&value.occ_symbol),
724            quote: OptionQuote {
725                bid: value.bid.filter(|number| number.is_finite()),
726                ask: value.ask.filter(|number| number.is_finite()),
727                mark: value
728                    .mark
729                    .filter(|number| number.is_finite() && *number > 0.0),
730                last: value
731                    .last
732                    .filter(|number| number.is_finite() && *number > 0.0),
733            },
734            greeks: Some(Greeks {
735                delta: value
736                    .delta
737                    .filter(|number| number.is_finite())
738                    .unwrap_or(0.0),
739                gamma: value
740                    .gamma
741                    .filter(|number| number.is_finite())
742                    .unwrap_or(0.0),
743                vega: value
744                    .vega
745                    .filter(|number| number.is_finite())
746                    .unwrap_or(0.0),
747                theta: value
748                    .theta
749                    .filter(|number| number.is_finite())
750                    .unwrap_or(0.0),
751                rho: value.rho.filter(|number| number.is_finite()).unwrap_or(0.0),
752            }),
753            implied_volatility: value.implied_volatility.filter(|number| number.is_finite()),
754            underlying_price: value.underlying_price.filter(|number| number.is_finite()),
755        }
756    }
757}
758
759impl From<OptionChainRecord> for OptionSnapshot {
760    fn from(value: OptionChainRecord) -> Self {
761        Self::from(&value)
762    }
763}
764
765#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
766pub struct ExecutionLeg {
767    pub symbol: String,
768    pub ratio_qty: String,
769    pub side: OrderSide,
770    pub position_intent: PositionIntent,
771    pub leg_type: String,
772    pub snapshot: Option<ExecutionSnapshot>,
773}
774
775#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
776pub struct RollLegSelection {
777    pub leg_type: String,
778    pub quantity: Option<u32>,
779}
780
781#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
782pub struct RollRequest {
783    pub current_contract: String,
784    pub leg_type: Option<String>,
785    pub qty: u32,
786    pub new_strike: Option<f64>,
787    pub new_expiration: String,
788}
789
790#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
791pub struct ExecutionLegInput {
792    pub action: ExecutionAction,
793    pub leg_type: String,
794    pub contract: String,
795    pub quantity: Option<u32>,
796    pub snapshot: Option<ExecutionSnapshot>,
797    pub timestamp: Option<String>,
798    pub bid: Option<f64>,
799    pub ask: Option<f64>,
800    pub price: Option<f64>,
801    pub spread_percent: Option<f64>,
802    pub greeks: Option<GreeksInput>,
803    pub iv: Option<f64>,
804}
805
806#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
807pub struct ExecutionQuoteRange {
808    pub best_price: f64,
809    pub worst_price: f64,
810}
811
812#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
813pub struct ScaledExecutionQuote {
814    pub structure_quantity: u32,
815    pub price: f64,
816    pub total_price: f64,
817    pub total_dollars: f64,
818}
819
820#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
821pub struct ScaledExecutionQuoteRange {
822    pub structure_quantity: u32,
823    pub per_structure: ExecutionQuoteRange,
824    pub per_order: ExecutionQuoteRange,
825    pub dollars: ExecutionQuoteRange,
826}
827
828#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
829pub struct ParsedOptionStratUrl {
830    pub underlying_display_symbol: String,
831    pub leg_fragments: Vec<String>,
832}
833
834#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
835pub struct OptionStratLegInput {
836    pub occ_symbol: String,
837    pub underlying_symbol: Option<String>,
838    pub expiration_date: Option<String>,
839    pub strike: Option<f64>,
840    pub option_right: Option<String>,
841    pub quantity: i32,
842    pub premium_per_contract: Option<f64>,
843}
844
845#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
846pub struct OptionStratStockInput {
847    pub underlying_symbol: String,
848    pub quantity: i32,
849    pub cost_per_share: f64,
850}
851
852#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
853pub struct OptionStratUrlInput {
854    pub underlying_display_symbol: String,
855    #[serde(default)]
856    pub legs: Vec<OptionStratLegInput>,
857    #[serde(default)]
858    pub stocks: Vec<OptionStratStockInput>,
859}
860
861#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
862pub struct OptionChain {
863    pub underlying_symbol: String,
864    pub as_of: String,
865    pub snapshots: Vec<OptionSnapshot>,
866}
867
868#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
869pub struct OptionChainRecord {
870    pub as_of: String,
871    pub underlying_symbol: String,
872    pub occ_symbol: String,
873    pub expiration_date: String,
874    pub option_right: OptionRight,
875    pub strike: f64,
876    pub underlying_price: Option<f64>,
877    pub bid: Option<f64>,
878    pub ask: Option<f64>,
879    pub mark: Option<f64>,
880    pub last: Option<f64>,
881    pub implied_volatility: Option<f64>,
882    pub delta: Option<f64>,
883    pub gamma: Option<f64>,
884    pub vega: Option<f64>,
885    pub theta: Option<f64>,
886    pub rho: Option<f64>,
887}
888
889impl OptionChainRecord {
890    pub fn is_delta_valid(&self) -> bool {
891        self.delta
892            .map(|delta| {
893                let abs_delta = delta.abs();
894                abs_delta >= 0.05 && abs_delta <= 0.95
895            })
896            .unwrap_or(false)
897    }
898}
899
900#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
901pub struct PayoffLegInput {
902    pub option_right: OptionRight,
903    pub position_side: PositionSide,
904    pub strike: f64,
905    pub premium: f64,
906    pub quantity: u32,
907}
908
909impl PayoffLegInput {
910    pub fn new(
911        option_right: &str,
912        position_side: &str,
913        strike: f64,
914        premium: f64,
915        quantity: u32,
916    ) -> OptionResult<Self> {
917        if quantity == 0 {
918            return Err(OptionError::new(
919                "invalid_payoff_input",
920                format!("quantity must be greater than zero: {quantity}"),
921            ));
922        }
923
924        Ok(Self {
925            option_right: OptionRight::from_str(option_right)?,
926            position_side: PositionSide::from_str(position_side)?,
927            strike,
928            premium,
929            quantity,
930        })
931    }
932}
933
934#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
935pub struct StrategyValuationPosition {
936    pub contract: OptionContract,
937    pub quantity: i32,
938    pub avg_entry_price: Option<f64>,
939    pub implied_volatility: Option<f64>,
940    pub mark_price: Option<f64>,
941    pub reference_underlying_price: Option<f64>,
942}
943
944#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
945pub struct StrategyPnlInput {
946    pub positions: Vec<StrategyValuationPosition>,
947    pub underlying_price: f64,
948    pub evaluation_time: String,
949    pub entry_cost: Option<f64>,
950    pub rate: f64,
951    pub dividend_yield: Option<f64>,
952    pub long_volatility_shift: Option<f64>,
953}
954
955#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
956pub struct StrategyBreakEvenInput {
957    pub positions: Vec<StrategyValuationPosition>,
958    pub evaluation_time: String,
959    pub entry_cost: Option<f64>,
960    pub rate: f64,
961    pub dividend_yield: Option<f64>,
962    pub long_volatility_shift: Option<f64>,
963    pub lower_bound: f64,
964    pub upper_bound: f64,
965    pub scan_step: Option<f64>,
966    pub tolerance: Option<f64>,
967    pub max_iterations: Option<usize>,
968}