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}