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"));
    }
}