Skip to main content

alpaca_option/
types.rs

1use alpaca_core::float;
2use rust_decimal::prelude::ToPrimitive;
3use rust_decimal::Decimal;
4use serde::{Deserialize, Deserializer, Serialize};
5use ts_rs::TS;
6
7use crate::contract;
8use crate::error::{OptionError, OptionResult};
9use crate::rate::risk_free_rate_for_years;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
12#[serde(rename_all = "lowercase")]
13pub enum OptionRight {
14    Call,
15    Put,
16}
17
18impl Default for OptionRight {
19    fn default() -> Self {
20        Self::Call
21    }
22}
23
24impl OptionRight {
25    pub fn from_str(input: &str) -> OptionResult<Self> {
26        match input.trim().to_ascii_lowercase().as_str() {
27            "call" => Ok(Self::Call),
28            "put" => Ok(Self::Put),
29            "c" => Ok(Self::Call),
30            "p" => Ok(Self::Put),
31            _ => Err(OptionError::new(
32                "invalid_option_right",
33                format!("invalid option right: {input}"),
34            )),
35        }
36    }
37
38    pub fn as_str(&self) -> &'static str {
39        match self {
40            Self::Call => "call",
41            Self::Put => "put",
42        }
43    }
44
45    pub fn from_code(code: char) -> OptionResult<Self> {
46        match code {
47            'C' => Ok(Self::Call),
48            'P' => Ok(Self::Put),
49            _ => Err(OptionError::new(
50                "invalid_option_right_code",
51                format!("invalid option right code: {code}"),
52            )),
53        }
54    }
55
56    pub fn code(&self) -> char {
57        match self {
58            Self::Call => 'C',
59            Self::Put => 'P',
60        }
61    }
62
63    pub fn code_string(&self) -> OptionRightCode {
64        match self {
65            Self::Call => OptionRightCode::C,
66            Self::Put => OptionRightCode::P,
67        }
68    }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub enum OptionRightCode {
73    C,
74    P,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "lowercase")]
79pub enum OrderSide {
80    Buy,
81    Sell,
82}
83
84impl OrderSide {
85    pub fn from_str(input: &str) -> OptionResult<Self> {
86        match input.trim().to_ascii_lowercase().as_str() {
87            "buy" => Ok(Self::Buy),
88            "sell" => Ok(Self::Sell),
89            _ => Err(OptionError::new(
90                "invalid_order_side",
91                format!("invalid order side: {input}"),
92            )),
93        }
94    }
95
96    pub fn as_str(&self) -> &'static str {
97        match self {
98            Self::Buy => "buy",
99            Self::Sell => "sell",
100        }
101    }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "lowercase")]
106pub enum PositionSide {
107    Long,
108    Short,
109}
110
111impl PositionSide {
112    pub fn from_str(input: &str) -> OptionResult<Self> {
113        match input {
114            "long" => Ok(Self::Long),
115            "short" => Ok(Self::Short),
116            _ => Err(OptionError::new(
117                "invalid_position_side",
118                format!("invalid position side: {input}"),
119            )),
120        }
121    }
122
123    pub fn as_str(&self) -> &'static str {
124        match self {
125            Self::Long => "long",
126            Self::Short => "short",
127        }
128    }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "lowercase")]
133pub enum ExecutionAction {
134    Open,
135    Close,
136}
137
138impl ExecutionAction {
139    pub fn from_str(input: &str) -> OptionResult<Self> {
140        match input {
141            "open" => Ok(Self::Open),
142            "close" => Ok(Self::Close),
143            _ => Err(OptionError::new(
144                "invalid_execution_quote_input",
145                format!("invalid execution action: {input}"),
146            )),
147        }
148    }
149
150    pub fn as_str(&self) -> &'static str {
151        match self {
152            Self::Open => "open",
153            Self::Close => "close",
154        }
155    }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum PositionIntent {
161    BuyToOpen,
162    SellToOpen,
163    BuyToClose,
164    SellToClose,
165}
166
167impl PositionIntent {
168    pub fn from_str(input: &str) -> OptionResult<Self> {
169        match input.trim().to_ascii_lowercase().as_str() {
170            "buy_to_open" => Ok(Self::BuyToOpen),
171            "sell_to_open" => Ok(Self::SellToOpen),
172            "buy_to_close" => Ok(Self::BuyToClose),
173            "sell_to_close" => Ok(Self::SellToClose),
174            _ => Err(OptionError::new(
175                "invalid_position_intent",
176                format!("invalid position intent: {input}"),
177            )),
178        }
179    }
180
181    pub fn as_str(&self) -> &'static str {
182        match self {
183            Self::BuyToOpen => "buy_to_open",
184            Self::SellToOpen => "sell_to_open",
185            Self::BuyToClose => "buy_to_close",
186            Self::SellToClose => "sell_to_close",
187        }
188    }
189
190    pub fn is_close(&self) -> bool {
191        matches!(self, Self::BuyToClose | Self::SellToClose)
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "lowercase")]
197pub enum MoneynessLabel {
198    Itm,
199    Atm,
200    Otm,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum AssignmentRiskLevel {
206    Danger,
207    Critical,
208    High,
209    Medium,
210    Low,
211    Safe,
212}
213
214impl AssignmentRiskLevel {
215    pub fn as_str(&self) -> &'static str {
216        match self {
217            Self::Danger => "danger",
218            Self::Critical => "critical",
219            Self::High => "high",
220            Self::Medium => "medium",
221            Self::Low => "low",
222            Self::Safe => "safe",
223        }
224    }
225}
226
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
228pub struct OptionContract {
229    pub underlying_symbol: String,
230    pub expiration_date: String,
231    pub strike: f64,
232    pub option_right: OptionRight,
233    pub occ_symbol: String,
234}
235
236impl Default for OptionContract {
237    fn default() -> Self {
238        Self {
239            underlying_symbol: String::new(),
240            expiration_date: String::new(),
241            strike: 0.0,
242            option_right: OptionRight::default(),
243            occ_symbol: String::new(),
244        }
245    }
246}
247
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
249pub struct OptionQuote {
250    pub bid: Option<f64>,
251    pub ask: Option<f64>,
252    pub mark: Option<f64>,
253    pub last: Option<f64>,
254}
255
256impl Default for OptionQuote {
257    fn default() -> Self {
258        Self {
259            bid: None,
260            ask: None,
261            mark: None,
262            last: None,
263        }
264    }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct ContractDisplay {
269    pub strike: String,
270    pub expiration: String,
271    pub compact: String,
272    pub option_right_code: OptionRightCode,
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
276pub struct Greeks {
277    pub delta: f64,
278    pub gamma: f64,
279    pub vega: f64,
280    pub theta: f64,
281    pub rho: f64,
282}
283
284impl Default for Greeks {
285    fn default() -> Self {
286        Self {
287            delta: 0.0,
288            gamma: 0.0,
289            vega: 0.0,
290            theta: 0.0,
291            rho: 0.0,
292        }
293    }
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct BlackScholesInput {
298    pub spot: f64,
299    pub strike: f64,
300    pub years: f64,
301    pub rate: f64,
302    pub dividend_yield: f64,
303    pub volatility: f64,
304    pub option_right: OptionRight,
305}
306
307impl BlackScholesInput {
308    pub fn new(
309        spot: f64,
310        strike: f64,
311        years: f64,
312        dividend_yield: f64,
313        volatility: f64,
314        option_right: OptionRight,
315    ) -> Self {
316        Self {
317            spot,
318            strike,
319            years,
320            rate: risk_free_rate_for_years(years),
321            dividend_yield,
322            volatility,
323            option_right,
324        }
325    }
326
327    pub fn with_rate(mut self, rate: f64) -> Self {
328        self.rate = rate;
329        self
330    }
331}
332
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334pub struct BlackScholesImpliedVolatilityInput {
335    pub target_price: f64,
336    pub spot: f64,
337    pub strike: f64,
338    pub years: f64,
339    pub rate: f64,
340    pub dividend_yield: f64,
341    pub option_right: OptionRight,
342    pub lower_bound: Option<f64>,
343    pub upper_bound: Option<f64>,
344    pub tolerance: Option<f64>,
345    pub max_iterations: Option<usize>,
346}
347
348impl BlackScholesImpliedVolatilityInput {
349    pub fn new(
350        target_price: f64,
351        spot: f64,
352        strike: f64,
353        years: f64,
354        dividend_yield: f64,
355        option_right: OptionRight,
356    ) -> Self {
357        Self {
358            target_price,
359            spot,
360            strike,
361            years,
362            rate: risk_free_rate_for_years(years),
363            dividend_yield,
364            option_right,
365            lower_bound: None,
366            upper_bound: None,
367            tolerance: None,
368            max_iterations: None,
369        }
370    }
371
372    pub fn with_rate(mut self, rate: f64) -> Self {
373        self.rate = rate;
374        self
375    }
376}
377
378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
379pub struct OptionSnapshot {
380    pub as_of: String,
381    pub contract: OptionContract,
382    pub quote: OptionQuote,
383    pub greeks: Option<Greeks>,
384    pub implied_volatility: Option<f64>,
385    pub underlying_price: Option<f64>,
386}
387
388impl Default for OptionSnapshot {
389    fn default() -> Self {
390        Self {
391            as_of: String::new(),
392            contract: OptionContract::default(),
393            quote: OptionQuote::default(),
394            greeks: None,
395            implied_volatility: None,
396            underlying_price: None,
397        }
398    }
399}
400
401fn parse_snapshot_number(input: &str) -> Option<f64> {
402    let trimmed = input.trim();
403    if trimmed.is_empty() {
404        return None;
405    }
406
407    let value = trimmed.parse::<f64>().ok()?;
408    value.is_finite().then_some(value)
409}
410
411fn format_snapshot_number(value: f64) -> String {
412    float::round(value, 2).to_string()
413}
414
415fn normalized_quote_price(quote: &OptionQuote) -> f64 {
416    if let Some(mark) = quote.mark.filter(|value| value.is_finite()) {
417        return mark;
418    }
419
420    match (
421        quote.bid.filter(|value| value.is_finite()),
422        quote.ask.filter(|value| value.is_finite()),
423    ) {
424        (Some(bid), Some(ask)) => float::round((bid + ask) / 2.0, 12),
425        (Some(bid), None) => bid,
426        (None, Some(ask)) => ask,
427        (None, None) => quote.last.filter(|value| value.is_finite()).unwrap_or(0.0),
428    }
429}
430
431fn canonical_contract_or_fallback(occ_symbol: &str) -> OptionContract {
432    let normalized = occ_symbol.trim().to_ascii_uppercase();
433    contract::parse_occ_symbol(&normalized).unwrap_or(OptionContract {
434        occ_symbol: normalized,
435        ..OptionContract::default()
436    })
437}
438
439impl OptionSnapshot {
440    pub fn is_empty(&self) -> bool {
441        self.as_of.trim().is_empty()
442            && self.contract.occ_symbol.trim().is_empty()
443            && self.quote == OptionQuote::default()
444            && self.greeks.is_none()
445            && self.implied_volatility.is_none()
446            && self.underlying_price.is_none()
447    }
448
449    pub fn occ_symbol(&self) -> &str {
450        &self.contract.occ_symbol
451    }
452
453    pub fn timestamp(&self) -> &str {
454        &self.as_of
455    }
456
457    pub fn bid(&self) -> f64 {
458        self.quote
459            .bid
460            .filter(|value| value.is_finite())
461            .unwrap_or(0.0)
462    }
463
464    pub fn ask(&self) -> f64 {
465        self.quote
466            .ask
467            .filter(|value| value.is_finite())
468            .unwrap_or(0.0)
469    }
470
471    pub fn price(&self) -> f64 {
472        normalized_quote_price(&self.quote)
473    }
474
475    pub fn iv(&self) -> f64 {
476        self.implied_volatility
477            .filter(|value| value.is_finite())
478            .unwrap_or(0.0)
479    }
480
481    pub fn delta(&self) -> f64 {
482        self.greeks_or_default().delta
483    }
484
485    pub fn gamma(&self) -> f64 {
486        self.greeks_or_default().gamma
487    }
488
489    pub fn vega(&self) -> f64 {
490        self.greeks_or_default().vega
491    }
492
493    pub fn theta(&self) -> f64 {
494        self.greeks_or_default().theta
495    }
496
497    pub fn rho(&self) -> f64 {
498        self.greeks_or_default().rho
499    }
500
501    pub fn underlying_price(&self) -> f64 {
502        self.underlying_price
503            .filter(|value| value.is_finite())
504            .unwrap_or(0.0)
505    }
506
507    pub fn greeks_or_default(&self) -> Greeks {
508        self.greeks.clone().unwrap_or_default()
509    }
510}
511
512#[derive(Debug, Clone, PartialEq, Serialize, TS)]
513pub struct OptionPosition {
514    pub contract: String,
515    pub snapshot: OptionSnapshot,
516    pub qty: i32,
517    #[serde(with = "alpaca_core::decimal::price_string_contract")]
518    #[ts(type = "string")]
519    pub avg_cost: Decimal,
520    pub leg_type: String,
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub option_right: Option<OptionRight>,
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub strike: Option<f64>,
525    #[serde(default, skip_serializing_if = "Option::is_none")]
526    pub valuation_years: Option<f64>,
527}
528
529#[derive(Debug, Deserialize)]
530struct OptionPositionWire {
531    contract: String,
532    #[serde(default)]
533    snapshot: Option<OptionSnapshot>,
534    qty: i32,
535    #[serde(with = "alpaca_core::decimal::price_string_contract")]
536    avg_cost: Decimal,
537    leg_type: String,
538    #[serde(default)]
539    option_right: Option<OptionRight>,
540    #[serde(default)]
541    strike: Option<f64>,
542    #[serde(default)]
543    valuation_years: Option<f64>,
544}
545
546impl<'de> Deserialize<'de> for OptionPosition {
547    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
548    where
549        D: Deserializer<'de>,
550    {
551        let wire = OptionPositionWire::deserialize(deserializer)?;
552        Ok(Self {
553            contract: wire.contract,
554            snapshot: wire.snapshot.unwrap_or_default(),
555            qty: wire.qty,
556            avg_cost: wire.avg_cost,
557            leg_type: wire.leg_type,
558            option_right: wire.option_right,
559            strike: wire.strike,
560            valuation_years: wire.valuation_years,
561        })
562    }
563}
564
565fn position_side_from_qty_and_leg_type(qty: i32, leg_type: &str) -> PositionSide {
566    if qty < 0 {
567        PositionSide::Short
568    } else if qty > 0 {
569        PositionSide::Long
570    } else if leg_type.trim().to_ascii_lowercase().starts_with("short") {
571        PositionSide::Short
572    } else {
573        PositionSide::Long
574    }
575}
576
577impl OptionPosition {
578    pub fn from_snapshot(
579        snapshot: &OptionSnapshot,
580        qty: i32,
581        avg_cost: Decimal,
582        leg_type: impl Into<String>,
583    ) -> Self {
584        Self {
585            contract: snapshot.occ_symbol().to_string(),
586            snapshot: snapshot.clone(),
587            qty,
588            avg_cost,
589            leg_type: leg_type.into(),
590            option_right: None,
591            strike: None,
592            valuation_years: None,
593        }
594    }
595
596    pub fn occ_symbol(&self) -> &str {
597        self.contract.trim()
598    }
599
600    pub fn qty(&self) -> i32 {
601        self.qty
602    }
603
604    pub fn contract_info(&self) -> OptionContract {
605        canonical_contract_or_fallback(&self.contract)
606    }
607
608    pub fn position_side(&self) -> PositionSide {
609        position_side_from_qty_and_leg_type(self.qty, &self.leg_type())
610    }
611
612    pub fn quantity(&self) -> u32 {
613        self.qty.unsigned_abs()
614    }
615
616    pub fn snapshot_ref(&self) -> Option<&OptionSnapshot> {
617        (!self.snapshot.is_empty()).then_some(&self.snapshot)
618    }
619
620    pub fn avg_cost(&self) -> f64 {
621        self.avg_cost.to_f64().unwrap_or(0.0)
622    }
623
624    pub fn leg_type(&self) -> String {
625        if !self.leg_type.trim().is_empty() {
626            return self.leg_type.trim().to_ascii_lowercase();
627        }
628
629        let contract = self.contract_info();
630        format!(
631            "{}{}",
632            self.position_side().as_str(),
633            contract.option_right.as_str()
634        )
635    }
636
637    pub fn cost(&self) -> Decimal {
638        self.avg_cost * Decimal::from(self.qty) * Decimal::from(100)
639    }
640
641    pub fn value(&self) -> Decimal {
642        alpaca_core::decimal::from_f64(self.snapshot.price(), 2)
643            * Decimal::from(self.qty)
644            * Decimal::from(100)
645    }
646
647    pub fn marked_value(&self) -> Decimal {
648        self.value()
649    }
650
651    pub fn with_model_inputs(
652        &self,
653        implied_volatility: f64,
654        underlying_price: Option<f64>,
655    ) -> Self {
656        let mut position = self.clone();
657        position.snapshot.implied_volatility = Some(implied_volatility);
658        if let Some(underlying_price) = underlying_price {
659            position.snapshot.underlying_price = Some(underlying_price);
660        }
661        position
662    }
663
664    pub fn with_qty_multiplier(&self, multiplier: i32) -> Self {
665        let mut position = self.clone();
666        position.qty *= multiplier;
667        position
668    }
669
670    pub fn effective_iv_or(&self, fallback_iv: Option<f64>, default_iv: f64) -> f64 {
671        let snapshot_iv = self.snapshot.iv();
672        if snapshot_iv.is_finite() && snapshot_iv > 0.0 {
673            snapshot_iv
674        } else if let Some(fallback_iv) =
675            fallback_iv.filter(|value| value.is_finite() && *value > 0.0)
676        {
677            fallback_iv
678        } else {
679            default_iv
680        }
681    }
682}
683
684impl Default for OptionPosition {
685    fn default() -> Self {
686        Self {
687            contract: String::new(),
688            snapshot: OptionSnapshot::default(),
689            qty: 0,
690            avg_cost: Decimal::ZERO,
691            leg_type: String::new(),
692            option_right: None,
693            strike: None,
694            valuation_years: None,
695        }
696    }
697}
698
699#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
700pub struct ShortItmPosition {
701    pub contract: OptionContract,
702    pub quantity: u32,
703    pub option_price: f64,
704    pub intrinsic: f64,
705    pub extrinsic: f64,
706}
707
708#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
709pub struct StrategyLegInput {
710    pub contract: OptionContract,
711    pub order_side: OrderSide,
712    pub ratio_quantity: u32,
713    pub premium_per_contract: Option<f64>,
714}
715
716#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
717pub struct QuotedLeg {
718    pub contract: OptionContract,
719    pub order_side: OrderSide,
720    pub ratio_quantity: u32,
721    pub quote: OptionQuote,
722    pub snapshot: Option<OptionSnapshot>,
723}
724
725#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
726pub struct GreeksInput {
727    pub delta: Option<f64>,
728    pub gamma: Option<f64>,
729    pub vega: Option<f64>,
730    pub theta: Option<f64>,
731    pub rho: Option<f64>,
732}
733
734#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
735pub struct ExecutionSnapshot {
736    pub contract: String,
737    pub timestamp: String,
738    pub bid: String,
739    pub ask: String,
740    pub price: String,
741    pub greeks: Greeks,
742    pub iv: f64,
743}
744
745impl From<ExecutionSnapshot> for OptionSnapshot {
746    fn from(value: ExecutionSnapshot) -> Self {
747        Self {
748            as_of: value.timestamp.trim().to_string(),
749            contract: canonical_contract_or_fallback(&value.contract),
750            quote: OptionQuote {
751                bid: parse_snapshot_number(&value.bid),
752                ask: parse_snapshot_number(&value.ask),
753                mark: parse_snapshot_number(&value.price),
754                last: parse_snapshot_number(&value.price),
755            },
756            greeks: Some(value.greeks),
757            implied_volatility: value.iv.is_finite().then_some(value.iv),
758            underlying_price: None,
759        }
760    }
761}
762
763impl From<&ExecutionSnapshot> for OptionSnapshot {
764    fn from(value: &ExecutionSnapshot) -> Self {
765        Self::from(value.clone())
766    }
767}
768
769impl From<&OptionSnapshot> for ExecutionSnapshot {
770    fn from(value: &OptionSnapshot) -> Self {
771        Self {
772            contract: value.occ_symbol().to_string(),
773            timestamp: value.timestamp().to_string(),
774            bid: format_snapshot_number(value.bid()),
775            ask: format_snapshot_number(value.ask()),
776            price: format_snapshot_number(value.price()),
777            greeks: value.greeks_or_default(),
778            iv: value.iv(),
779        }
780    }
781}
782
783impl From<OptionSnapshot> for ExecutionSnapshot {
784    fn from(value: OptionSnapshot) -> Self {
785        Self::from(&value)
786    }
787}
788
789impl From<&OptionSnapshot> for OptionChainRecord {
790    fn from(value: &OptionSnapshot) -> Self {
791        Self {
792            as_of: value.timestamp().to_string(),
793            underlying_symbol: value.contract.underlying_symbol.clone(),
794            occ_symbol: value.occ_symbol().to_string(),
795            expiration_date: value.contract.expiration_date.clone(),
796            option_right: value.contract.option_right.clone(),
797            strike: value.contract.strike,
798            underlying_price: value.underlying_price.filter(|number| number.is_finite()),
799            bid: value.quote.bid.filter(|number| number.is_finite()),
800            ask: value.quote.ask.filter(|number| number.is_finite()),
801            mark: value.quote.mark.filter(|number| number.is_finite()),
802            last: value.quote.last.filter(|number| number.is_finite()),
803            implied_volatility: value.implied_volatility.filter(|number| number.is_finite()),
804            delta: value
805                .greeks
806                .as_ref()
807                .map(|greeks| greeks.delta)
808                .filter(|number| number.is_finite()),
809            gamma: value
810                .greeks
811                .as_ref()
812                .map(|greeks| greeks.gamma)
813                .filter(|number| number.is_finite()),
814            vega: value
815                .greeks
816                .as_ref()
817                .map(|greeks| greeks.vega)
818                .filter(|number| number.is_finite()),
819            theta: value
820                .greeks
821                .as_ref()
822                .map(|greeks| greeks.theta)
823                .filter(|number| number.is_finite()),
824            rho: value
825                .greeks
826                .as_ref()
827                .map(|greeks| greeks.rho)
828                .filter(|number| number.is_finite()),
829        }
830    }
831}
832
833impl From<OptionSnapshot> for OptionChainRecord {
834    fn from(value: OptionSnapshot) -> Self {
835        Self::from(&value)
836    }
837}
838
839impl From<&OptionChainRecord> for OptionSnapshot {
840    fn from(value: &OptionChainRecord) -> Self {
841        Self {
842            as_of: value.as_of.trim().to_string(),
843            contract: canonical_contract_or_fallback(&value.occ_symbol),
844            quote: OptionQuote {
845                bid: value.bid.filter(|number| number.is_finite()),
846                ask: value.ask.filter(|number| number.is_finite()),
847                mark: value
848                    .mark
849                    .filter(|number| number.is_finite() && *number > 0.0),
850                last: value
851                    .last
852                    .filter(|number| number.is_finite() && *number > 0.0),
853            },
854            greeks: Some(Greeks {
855                delta: value
856                    .delta
857                    .filter(|number| number.is_finite())
858                    .unwrap_or(0.0),
859                gamma: value
860                    .gamma
861                    .filter(|number| number.is_finite())
862                    .unwrap_or(0.0),
863                vega: value
864                    .vega
865                    .filter(|number| number.is_finite())
866                    .unwrap_or(0.0),
867                theta: value
868                    .theta
869                    .filter(|number| number.is_finite())
870                    .unwrap_or(0.0),
871                rho: value.rho.filter(|number| number.is_finite()).unwrap_or(0.0),
872            }),
873            implied_volatility: value.implied_volatility.filter(|number| number.is_finite()),
874            underlying_price: value.underlying_price.filter(|number| number.is_finite()),
875        }
876    }
877}
878
879impl From<OptionChainRecord> for OptionSnapshot {
880    fn from(value: OptionChainRecord) -> Self {
881        Self::from(&value)
882    }
883}
884
885#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
886pub struct ExecutionLeg {
887    pub symbol: String,
888    pub ratio_qty: String,
889    pub side: OrderSide,
890    pub position_intent: PositionIntent,
891    pub leg_type: String,
892    pub snapshot: Option<ExecutionSnapshot>,
893}
894
895#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
896pub struct RollLegSelection {
897    pub leg_type: String,
898    pub quantity: Option<u32>,
899}
900
901#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
902pub struct RollRequest {
903    pub current_contract: String,
904    pub leg_type: Option<String>,
905    pub qty: u32,
906    pub new_strike: Option<f64>,
907    pub new_expiration: String,
908}
909
910#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
911pub struct ExecutionLegInput {
912    pub action: ExecutionAction,
913    pub leg_type: String,
914    pub contract: String,
915    pub quantity: Option<u32>,
916    pub snapshot: Option<ExecutionSnapshot>,
917    pub timestamp: Option<String>,
918    pub bid: Option<f64>,
919    pub ask: Option<f64>,
920    pub price: Option<f64>,
921    pub spread_percent: Option<f64>,
922    pub greeks: Option<GreeksInput>,
923    pub iv: Option<f64>,
924}
925
926#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
927pub struct ExecutionQuoteRange {
928    pub best_price: f64,
929    pub worst_price: f64,
930}
931
932#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
933pub struct ScaledExecutionQuote {
934    pub structure_quantity: u32,
935    pub price: f64,
936    pub total_price: f64,
937    pub total_dollars: f64,
938}
939
940#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
941pub struct ScaledExecutionQuoteRange {
942    pub structure_quantity: u32,
943    pub per_structure: ExecutionQuoteRange,
944    pub per_order: ExecutionQuoteRange,
945    pub dollars: ExecutionQuoteRange,
946}
947
948#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
949pub struct ParsedOptionStratUrl {
950    pub underlying_display_symbol: String,
951    pub leg_fragments: Vec<String>,
952}
953
954#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
955pub struct OptionStratLegInput {
956    pub occ_symbol: String,
957    pub underlying_symbol: Option<String>,
958    pub expiration_date: Option<String>,
959    pub strike: Option<f64>,
960    pub option_right: Option<String>,
961    pub quantity: i32,
962    pub premium_per_contract: Option<f64>,
963}
964
965#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
966pub struct OptionStratStockInput {
967    pub underlying_symbol: String,
968    pub quantity: i32,
969    pub cost_per_share: f64,
970}
971
972#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
973pub struct OptionStratUrlInput {
974    pub underlying_display_symbol: String,
975    #[serde(default)]
976    pub legs: Vec<OptionStratLegInput>,
977    #[serde(default)]
978    pub stocks: Vec<OptionStratStockInput>,
979}
980
981#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
982pub struct OptionChain {
983    pub underlying_symbol: String,
984    pub as_of: String,
985    pub snapshots: Vec<OptionSnapshot>,
986}
987
988#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
989pub struct OptionChainRecord {
990    pub as_of: String,
991    pub underlying_symbol: String,
992    pub occ_symbol: String,
993    pub expiration_date: String,
994    pub option_right: OptionRight,
995    pub strike: f64,
996    pub underlying_price: Option<f64>,
997    pub bid: Option<f64>,
998    pub ask: Option<f64>,
999    pub mark: Option<f64>,
1000    pub last: Option<f64>,
1001    pub implied_volatility: Option<f64>,
1002    pub delta: Option<f64>,
1003    pub gamma: Option<f64>,
1004    pub vega: Option<f64>,
1005    pub theta: Option<f64>,
1006    pub rho: Option<f64>,
1007}
1008
1009impl OptionChainRecord {
1010    pub fn is_delta_valid(&self) -> bool {
1011        self.delta
1012            .map(|delta| {
1013                let abs_delta = delta.abs();
1014                abs_delta >= 0.05 && abs_delta <= 0.95
1015            })
1016            .unwrap_or(false)
1017    }
1018}
1019
1020#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1021pub struct MarketStructureOptionRecord {
1022    pub as_of: String,
1023    pub underlying_symbol: String,
1024    pub occ_symbol: String,
1025    pub expiration_date: String,
1026    pub option_right: OptionRight,
1027    pub strike: f64,
1028    pub underlying_price: Option<f64>,
1029    pub bid: Option<f64>,
1030    pub ask: Option<f64>,
1031    pub mark: Option<f64>,
1032    pub last: Option<f64>,
1033    pub implied_volatility: Option<f64>,
1034    pub delta: Option<f64>,
1035    pub gamma: Option<f64>,
1036    pub vega: Option<f64>,
1037    pub theta: Option<f64>,
1038    pub rho: Option<f64>,
1039    pub open_interest: Option<f64>,
1040    pub open_interest_date: Option<String>,
1041    pub multiplier: Option<f64>,
1042    pub minute_volume: Option<u64>,
1043    pub daily_volume: Option<u64>,
1044    pub latest_trade_size: Option<u64>,
1045    pub bid_size: Option<u64>,
1046    pub ask_size: Option<u64>,
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
1050pub struct MarketStructureFilters {
1051    pub expiration_date: Option<String>,
1052    pub dte_min: Option<f64>,
1053    pub dte_max: Option<f64>,
1054    pub strike_price_gte: Option<f64>,
1055    pub strike_price_lte: Option<f64>,
1056    pub option_right: Option<OptionRight>,
1057    #[serde(default)]
1058    pub require_open_interest: bool,
1059}
1060
1061#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1062pub struct MarketStructureLevel {
1063    pub strike: f64,
1064    pub call_open_interest: f64,
1065    pub put_open_interest: f64,
1066    pub total_open_interest: f64,
1067    pub call_gamma_exposure: f64,
1068    pub put_gamma_exposure: f64,
1069    pub net_gamma_exposure: f64,
1070    pub absolute_gamma_exposure: f64,
1071    pub call_volume: u64,
1072    pub put_volume: u64,
1073    pub total_volume: u64,
1074    pub labels: Vec<String>,
1075}
1076
1077#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
1078pub struct MarketStructureAnalysis {
1079    pub underlying_price: Option<f64>,
1080    pub records_count: usize,
1081    pub open_interest_coverage: f64,
1082    pub call_wall: Option<MarketStructureLevel>,
1083    pub put_wall: Option<MarketStructureLevel>,
1084    pub absolute_gamma_exposure_wall: Option<MarketStructureLevel>,
1085    pub net_gamma_exposure: f64,
1086    pub absolute_gamma_exposure: f64,
1087    pub levels: Vec<MarketStructureLevel>,
1088    pub warnings: Vec<String>,
1089}
1090
1091#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
1092#[serde(rename_all = "snake_case")]
1093pub enum MarketStructureExposureMode {
1094    GexProxy,
1095    DealerView,
1096}
1097
1098impl Default for MarketStructureExposureMode {
1099    fn default() -> Self {
1100        Self::GexProxy
1101    }
1102}
1103
1104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
1105pub struct MarketStructureAnalysisOptions {
1106    #[serde(default)]
1107    pub mode: MarketStructureExposureMode,
1108}
1109
1110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1111pub struct PayoffLegInput {
1112    pub option_right: OptionRight,
1113    pub position_side: PositionSide,
1114    pub strike: f64,
1115    pub premium: f64,
1116    pub quantity: u32,
1117}
1118
1119impl PayoffLegInput {
1120    pub fn new(
1121        option_right: &str,
1122        position_side: &str,
1123        strike: f64,
1124        premium: f64,
1125        quantity: u32,
1126    ) -> OptionResult<Self> {
1127        if quantity == 0 {
1128            return Err(OptionError::new(
1129                "invalid_payoff_input",
1130                format!("quantity must be greater than zero: {quantity}"),
1131            ));
1132        }
1133
1134        Ok(Self {
1135            option_right: OptionRight::from_str(option_right)?,
1136            position_side: PositionSide::from_str(position_side)?,
1137            strike,
1138            premium,
1139            quantity,
1140        })
1141    }
1142}
1143
1144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1145pub struct OptionStrategyInput {
1146    pub positions: Vec<OptionPosition>,
1147    pub qty: i32,
1148    pub evaluation_time: Option<String>,
1149    pub entry_cost: Option<f64>,
1150    pub dividend_yield: Option<f64>,
1151}
1152
1153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1154pub struct OptionStrategyCurvePoint {
1155    pub underlying_price: f64,
1156    pub mark_value: f64,
1157    pub pnl: f64,
1158}
1159
1160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1161pub struct StrategyPnlInput {
1162    pub positions: Vec<OptionPosition>,
1163    pub qty: i32,
1164    pub underlying_price: f64,
1165    pub evaluation_time: String,
1166    pub entry_cost: Option<f64>,
1167    pub dividend_yield: Option<f64>,
1168}
1169
1170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1171pub struct StrategyBreakEvenInput {
1172    pub positions: Vec<OptionPosition>,
1173    pub qty: i32,
1174    pub evaluation_time: String,
1175    pub entry_cost: Option<f64>,
1176    pub dividend_yield: Option<f64>,
1177    pub lower_bound: f64,
1178    pub upper_bound: f64,
1179    pub scan_step: Option<f64>,
1180    pub tolerance: Option<f64>,
1181    pub max_iterations: Option<usize>,
1182}
1183
1184#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1185pub struct StrategyBreakEvenSideInput {
1186    pub pivot: f64,
1187    pub boundary: f64,
1188    pub scan_step: f64,
1189    pub tolerance: Option<f64>,
1190    pub max_iterations: Option<usize>,
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1194pub struct StrategyPnlPeakSearchInput {
1195    pub current_price: f64,
1196    pub step_hint: Option<f64>,
1197    pub left_boundary: f64,
1198    pub right_boundary: f64,
1199    pub tolerance: Option<f64>,
1200    pub max_search_steps: Option<usize>,
1201}
1202
1203#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
1204pub struct StrategyPnlPeak {
1205    pub spot: f64,
1206    pub pnl: f64,
1207}
1208
1209#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
1210pub struct StrategyPositionTotals {
1211    pub value: Decimal,
1212    pub cost: Decimal,
1213    pub spread: Decimal,
1214    pub spread_rate: Option<f64>,
1215}