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}