securities_identifiery/options/
mod.rs1use fancy_regex::Regex;
2
3use strum_macros::{EnumString, Display};
4
5
6use std::{fmt, str::FromStr};
7
8const OCC_OSI_REGEX: &str = r"^(?=.{16,21}$)(?P<symbol>[\w]{1,6})\s{0,5}(?P<year>\d{2})(?P<month>0\d|1[0-2])(?P<day>0[1-9]|[12]\d|3[01])(?P<contract>C|P|c|p)(?P<price>\d{8})$";
9const IB_ACTIVITY_STATEMENT_TRADES: &str = r"^(?P<symbol>[\w]{1,6})\s(?P<day>0[1-9]|[12]\d|3[01])(?P<month>\w{3})(?P<year>\d{2})\s(?P<price>\d*[.]?\d+)\s(?P<contract>C|P|c|p)$"; #[derive(Debug, Eq, PartialEq, EnumString, Display)]
12enum Month3Letter {
13 JAN = 1,
14 FEB,
15 MAR,
16 APR,
17 MAY,
18 JUN,
19 JUL,
20 AUG,
21 SEP,
22 OCT,
23 NOV,
24 DEC,
25}
26
27
28
29#[derive(Debug, PartialEq)]
31pub struct OptionData {
32 pub symbol: String,
34 expiration_year: i32,
36 expiration_month: i32,
38 expiration_day: i32,
40 pub strike_price: f64,
41 pub contract_type: ContractType,
42}
43
44#[derive(Debug, PartialEq)]
46pub enum ContractType {
47 Call,
48 Put,
49}
50
51impl fmt::Display for ContractType {
52 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
53 match self {
54 ContractType::Call => write!(f, "C"),
55 ContractType::Put => write!(f, "P"),
56 }
57 }
58}
59
60#[derive(Debug)]
62pub enum Error {
63 NoResult,
64 YearOutOfRange,
65 MonthOutOfRange,
66 DayOutOfRange,
67 RegexError(fancy_regex::Error),
68}
69
70impl ::std::error::Error for Error {}
71
72impl fmt::Display for Error {
73 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74 match self {
77 Error::NoResult => write!(f, "No Result for parsing String"),
78 Error::RegexError(e) => write!(f, "RegexError: {}", e),
79 Error::YearOutOfRange => write!(f, "Supplied year is out of range and not >2000"),
80 Error::MonthOutOfRange => write!(
81 f,
82 "Supplied month is out of range and not between 1 and 12 "
83 ),
84 Error::DayOutOfRange => {
85 write!(f, "Supplied Year is out of range and not between 1 and 31")
86 }
87 }
88 }
89}
90
91impl OptionData {
92 pub fn parse_osi(osi: &str) -> Result<OptionData, Error> {
94 let re = Regex::new(OCC_OSI_REGEX);
95 let re = match re {
96 Ok(r) => r,
97 Err(e) => return Err(Error::RegexError(e)),
98 };
99
100 let result = re.captures(osi);
101 let result = match result {
102 Ok(r) => r,
103 Err(e) => return Err(Error::RegexError(e)),
104 };
105 if result.is_none() {
106 return Err(Error::NoResult);
107 }
108 let cap = result.unwrap();
109
110 Ok(OptionData {
111 expiration_year: 2000 + cap.name("year").unwrap().as_str().parse::<i32>().unwrap(),
112 expiration_month: cap.name("month").unwrap().as_str().parse().unwrap(),
113 expiration_day: cap.name("day").unwrap().as_str().parse().unwrap(),
114
115 symbol: cap.name("symbol").unwrap().as_str().parse().unwrap(),
116 contract_type: match cap.name("contract").unwrap().as_str() {
117 "P" | "p" => ContractType::Put,
118 "C" | "c" => ContractType::Call,
119 _ => panic!(),
120 },
121 strike_price: cap.name("price").unwrap().as_str().parse::<i32>().unwrap() as f64
122 / (1000 as f64),
123 })
124 }
125
126 pub fn parse_ib_activity_statement_trades_symbol(osi: &str) -> Result<OptionData, Error> {
127 let re = Regex::new(IB_ACTIVITY_STATEMENT_TRADES);
128 let re = match re {
129 Ok(r) => r,
130 Err(e) => return Err(Error::RegexError(e)),
131 };
132
133 let result = re.captures(osi);
134 let result = match result {
135 Ok(r) => r,
136 Err(e) => return Err(Error::RegexError(e)),
137 };
138 if result.is_none() {
139 return Err(Error::NoResult);
140 }
141 let cap = result.unwrap();
142
143 Ok(OptionData {
144 expiration_year: 2000 + cap.name("year").unwrap().as_str().parse::<i32>().unwrap(),
145 expiration_month: Month3Letter::from_str(cap.name("month").unwrap().as_str()).unwrap() as i32,
146 expiration_day: cap.name("day").unwrap().as_str().parse().unwrap(),
147
148 symbol: cap.name("symbol").unwrap().as_str().parse().unwrap(),
149 contract_type: match cap.name("contract").unwrap().as_str() {
150 "P" | "p" => ContractType::Put,
151 "C" | "c" => ContractType::Call,
152 _ => panic!(),
153 },
154 strike_price: cap.name("price").unwrap().as_str().parse::<f64>().unwrap(),
155 })
156
157 }
158
159 pub fn to_osi_string(&self) -> String {
161 format!(
162 "{symbol:<6}{year:0>2}{month:0>2}{day:0>2}{contract}{price:0>8}",
163 symbol = self.symbol,
164 day = self.expiration_day,
165 month = self.expiration_month,
166 year = self.expiration_year - 2000,
167 contract = self.contract_type,
168 price = self.strike_price * 1000 as f64
169 )
170 .to_string()
171 }
172
173 pub fn to_osi_string_no_symbol_padding(&self) -> String {
175 format!(
176 "{symbol}{year:0>2}{month:0>2}{day:0>2}{contract}{price:0>8}",
177 symbol = self.symbol,
178 day = self.expiration_day,
179 month = self.expiration_month,
180 year = self.expiration_year - 2000,
181 contract = self.contract_type,
182 price = self.strike_price * 1000 as f64
183 )
184 .to_string()
185 }
186
187 pub fn to_schwab_string(&self) -> String {
189 format!(
190 "{symbol} {month:0>2}/{day:0>2}/{year:0>4} {price:.2} {contract}",
191 symbol = self.symbol,
192 day = self.expiration_day,
193 month = self.expiration_month,
194 year = self.expiration_year,
195 contract = self.contract_type,
196 price = self.strike_price as f64
197 )
198 .to_string()
199 }
200
201 pub fn get_expiration_year(&self) -> i32 {
202 self.expiration_year
203 }
204
205 pub fn get_expiration_month(&self) -> i32 {
206 self.expiration_month
207 }
208
209 pub fn get_expiration_day(&self) -> i32 {
210 self.expiration_day
211 }
212
213 pub fn set_ymd(&self, year: i32, month: i32, day: i32) -> Result<(), Error> {
214 if !year > 2000 {
215 return Err(Error::YearOutOfRange);
216 }
217 if !(month >= 1 && month <= 12) {
218 return Err(Error::MonthOutOfRange);
219 }
220 if !is_day_in_month_and_year(year, month, day) {
221 return Err(Error::DayOutOfRange);
222 }
223 Ok(())
224 }
225}
226
227fn is_leap_year(year: i32) -> bool {
229 return (year % 4 == 0 && !(year % 100 == 0)) || year % 400 == 0;
230}
231
232const MONTH_WITH_31_DAYS: [i32; 7] = [1, 3, 5, 7, 8, 10, 12];
233fn is_day_in_month_and_year(year: i32, month: i32, day: i32) -> bool {
235 return day > 0
236 && (
237 (month == 2 &&
238 (day <= 28 || day == 29 && is_leap_year(year)))
239 || (month != 2 &&
240 ( day <= 30 || day == 31 && MONTH_WITH_31_DAYS.contains(&month)))
241 );
242}
243
244#[cfg(test)]
245mod tests;