1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
use crate::common::{ExpirationDate, OptionType}; use chrono::NaiveDate; use num_rational::Rational64; use std::fmt; use std::str::FromStr; pub struct OptionSymbol<'a>(&'a str); impl<'a> OptionSymbol<'a> { pub fn from(s: &'a str) -> OptionSymbol<'a> { OptionSymbol(s) } pub fn quote_symbol(&self) -> String { let price = self.price_component(); let integer = price[..5].trim_start_matches('0'); let decimal = price[5..].trim_end_matches('0'); format!( ".{}{}{}{}{}{}", self.underlying_symbol(), self.date_component(), self.option_type_component(), integer, if decimal.is_empty() { "" } else { "." }, decimal, ) } fn date_component(&self) -> &str { let component = self.0.split_whitespace().nth(1); let date = component.and_then(|c| c.get(..6)); date.unwrap_or_else(|| panic!("Missing date component for symbol: {}", self.0)) } fn option_type_component(&self) -> char { self.0 .split_whitespace() .nth(1) .and_then(|s| s.chars().nth(6)) .unwrap_or_else(|| panic!("Missing option type component for symbol: {}", self.0)) } fn price_component(&self) -> &str { let component = self.0.split_whitespace().nth(1); let price = component.and_then(|c| c.get(7..)); price.unwrap_or_else(|| panic!("Missing price component for symbol: {}", self.0)) } pub fn expiration_date(&self) -> ExpirationDate { let date_str = self.date_component(); let date = NaiveDate::parse_from_str(date_str, "%y%m%d") .ok() .map(ExpirationDate); date.unwrap_or_else(|| panic!("Missing expiration date for symbol: {}", self.0)) } pub fn underlying_symbol(&self) -> &'a str { let underlying_symbol = self .0 .split_whitespace() .next() .unwrap_or_else(|| panic!("Missing underlying symbol for symbol: {}", self.0)); strip_weekly(underlying_symbol) } pub fn option_type(&self) -> OptionType { match self.option_type_component() { 'P' => OptionType::Put, 'C' => OptionType::Call, _ => unreachable!("Missing option type for symbol: {}", self.0), } } pub fn strike_price(&self) -> Rational64 { let price_str = self.price_component(); let price = i64::from_str(price_str) .ok() .map(|i| Rational64::new(i, 1000)); price.unwrap_or_else(|| panic!("Missing strike price for symbol: {}", self.0)) } } impl fmt::Display for OptionSymbol<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) } } pub struct QuoteSymbol<'a>(&'a str); impl<'a> QuoteSymbol<'a> { pub fn from(s: &'a str) -> QuoteSymbol<'a> { QuoteSymbol(s) } pub fn matches_underlying_symbol(&self, underlying_symbol: &str) -> bool { self.0 .get(1..) .filter(|s| s.starts_with(underlying_symbol)) .is_some() && self .0 .chars() .nth(underlying_symbol.len() + 1) .filter(|c| !c.is_numeric()) .is_none() } } impl fmt::Display for QuoteSymbol<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) } } pub fn strip_weekly(underlying_symbol: &str) -> &str { if underlying_symbol == "SPXW" { &underlying_symbol[0..3] } else { underlying_symbol } } #[cfg(test)] mod tests { use super::*; #[test] fn test_option_symbol_quote_symbol() { let quote_symbol = OptionSymbol::from("IQ 200918P00017500").quote_symbol(); assert_eq!(quote_symbol, ".IQ200918P17.5"); } #[test] fn test_option_symbol_option_type() { let option_type = OptionSymbol::from("IQ 200918P00017500").option_type(); assert_eq!(option_type, OptionType::Put); let option_type = OptionSymbol::from("IQ 200918C00017500").option_type(); assert_eq!(option_type, OptionType::Call); } #[test] fn test_option_symbol_strike_price() { let strike_price = OptionSymbol::from("IQ 200918P00017500").strike_price(); assert_eq!(strike_price, Rational64::new(175, 10)); } #[test] fn test_option_symbol_strike_price2() { let strike_price = OptionSymbol::from("PENN 200821C00040500").strike_price(); assert_eq!(strike_price, Rational64::new(405, 10)); } #[test] fn test_quote_symbol_matches_underlying_symbol() { let quote_symbol = QuoteSymbol::from(".IQ200918P17.5"); assert!(quote_symbol.matches_underlying_symbol("IQ")); } }