Skip to main content

hypercall_settlement/
symbol.rs

1use rust_decimal::Decimal;
2use std::str::FromStr;
3
4use crate::{OptionType, SettlementError};
5
6/// Parsed components of an option symbol used by settlement.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ParsedSettlementSymbol {
9    pub underlying: String,
10    pub expiry: u64,
11    pub expiry_ts: i64,
12    pub strike: Decimal,
13    pub option_type: OptionType,
14}
15
16impl ParsedSettlementSymbol {
17    /// Parse a settlement option symbol.
18    ///
19    /// Supports `UNDERLYING-YYYYMMDD-STRIKE-C/P` and Deribit-style
20    /// `UNDERLYING-DDMMMYY-STRIKE-C/P`.
21    pub fn from_symbol(symbol: &str) -> Result<Self, SettlementError> {
22        let parts: Vec<&str> = symbol.split('-').collect();
23        if parts.len() != 4 {
24            return Err(SettlementError::new(format!(
25                "Invalid symbol format: {}",
26                symbol
27            )));
28        }
29
30        let underlying = parts[0].to_string();
31        let expiry = parse_expiry(parts[1])?;
32        let expiry_ts = hypercall_types::expiry_date_to_timestamp_checked(&underlying, expiry)
33            .map_err(|e| SettlementError::new(format!("Invalid expiry in {}: {}", symbol, e)))?
34            as i64;
35        let strike = Decimal::from_str(parts[2]).map_err(|_| {
36            SettlementError::new(format!("Invalid strike in {}: {}", symbol, parts[2]))
37        })?;
38        let option_type = match parts[3] {
39            "C" | "c" => OptionType::Call,
40            "P" | "p" => OptionType::Put,
41            _ => {
42                return Err(SettlementError::new(format!(
43                    "Invalid option type in {}: {}",
44                    symbol, parts[3]
45                )))
46            }
47        };
48
49        Ok(Self {
50            underlying,
51            expiry,
52            expiry_ts,
53            strike,
54            option_type,
55        })
56    }
57}
58
59fn parse_expiry(expiry: &str) -> Result<u64, SettlementError> {
60    if expiry.chars().all(|c| c.is_ascii_digit()) {
61        return expiry
62            .parse::<u64>()
63            .map_err(|_| SettlementError::new(format!("Invalid expiry: {}", expiry)));
64    }
65
66    parse_deribit_expiry(expiry)
67        .ok_or_else(|| SettlementError::new(format!("Invalid expiry format: {}", expiry)))
68}
69
70fn parse_deribit_expiry(expiry: &str) -> Option<u64> {
71    if !(6..=7).contains(&expiry.len()) {
72        return None;
73    }
74
75    let day_len = expiry.len() - 5;
76    let day = expiry[..day_len].parse::<u32>().ok()?;
77    if !(1..=31).contains(&day) {
78        return None;
79    }
80
81    let month = match &expiry[day_len..day_len + 3] {
82        "JAN" => 1,
83        "FEB" => 2,
84        "MAR" => 3,
85        "APR" => 4,
86        "MAY" => 5,
87        "JUN" => 6,
88        "JUL" => 7,
89        "AUG" => 8,
90        "SEP" => 9,
91        "OCT" => 10,
92        "NOV" => 11,
93        "DEC" => 12,
94        _ => return None,
95    };
96    let year = expiry[day_len + 3..].parse::<u32>().ok()?;
97    Some((2000 + year) as u64 * 10000 + month as u64 * 100 + day as u64)
98}
99
100/// Instrument metadata needed to settle a symbol.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct SettlementInstrument {
103    pub symbol: String,
104    pub underlying: String,
105    pub expiry: u64,
106    pub expiry_ts: i64,
107    pub strike: Decimal,
108    pub option_type: OptionType,
109}
110
111impl SettlementInstrument {
112    pub fn from_symbol(symbol: &str) -> Result<Self, SettlementError> {
113        let parsed = ParsedSettlementSymbol::from_symbol(symbol)?;
114        Ok(Self {
115            symbol: symbol.to_string(),
116            underlying: parsed.underlying,
117            expiry: parsed.expiry,
118            expiry_ts: parsed.expiry_ts,
119            strike: parsed.strike,
120            option_type: parsed.option_type,
121        })
122    }
123}