securities_identifiery/options/
mod.rs

1use 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)$"; //KO 28MAY21 32.01 C 
10
11#[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/// Struct representing a complete option contract
30#[derive(Debug, PartialEq)]
31pub struct OptionData {
32    /// ticker symbol
33    pub symbol: String,
34    /// 4 digit year -> e.g. 2021
35    expiration_year: i32,
36    /// expiration month 1->12
37    expiration_month: i32,
38    /// expiration day  1->31
39    expiration_day: i32,
40    pub strike_price: f64,
41    pub contract_type: ContractType,
42}
43
44/// Enum if it is a Call or a Put
45#[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/// Error type which wraps [fancy_regex::Error]
61#[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        // We should make these more helpful, e.g. by including the parts of the regex that lead to
75        // the error.
76        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    ///parse a string which is OSI compliant to [OptionData]
93    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    /// serializes [OptionData] to a OSI compliant string like described here [https://ibkr.info/node/972]
160    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    /// serializes [OptionData] to a OSI compliant string like described here [https://ibkr.info/node/972] but without padding of the symbol to 6 chars
174    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    /// serializes [OptionData] to a Schwab compliant string like described here [http://www.schwabcontent.com/symbology/int_eng/key_details.html]
188    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
227/// leap year is every 4 years but not every 100 still every 400
228fn 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];
233/// checks if the day of month fits the month and year
234fn 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;