tastyworks 0.27.0

Unofficial Tastyworks API
Documentation
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(),
            if integer.is_empty() { "0" } else { 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");

        let quote_symbol = OptionSymbol::from("SNDL  210416C00000500").quote_symbol();
        assert_eq!(quote_symbol, ".SNDL210416C0.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"));
    }
}