Skip to main content

catalog_manager/
deribit.rs

1use chrono::{Datelike, NaiveDate, Utc};
2use serde::Deserialize;
3use std::collections::{BTreeMap, HashMap, HashSet};
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct DeribitInstrument {
7    pub instrument_name: String,
8    pub strike: Option<f64>,
9    #[serde(rename = "option_type")]
10    pub option_type: Option<String>,
11    pub expiration_timestamp: i64,
12}
13
14#[derive(Debug, Clone)]
15pub struct ExpiryGroup {
16    pub expiry_code: u32,
17    pub expiration_timestamp: i64,
18    pub paired_strikes: Vec<f64>,
19    pub all_strikes: HashSet<u64>,
20}
21
22pub fn group_instruments(instruments: Vec<DeribitInstrument>) -> HashMap<u32, ExpiryGroup> {
23    let mut by_expiry: BTreeMap<i64, Vec<DeribitInstrument>> = BTreeMap::new();
24    for instrument in instruments {
25        by_expiry
26            .entry(instrument.expiration_timestamp)
27            .or_default()
28            .push(instrument);
29    }
30
31    let mut result = HashMap::new();
32    for (expiration_timestamp, instruments) in by_expiry {
33        let Some(expiry_code) = timestamp_to_code(expiration_timestamp) else {
34            continue;
35        };
36
37        let mut strike_map: HashMap<u64, (bool, bool)> = HashMap::new();
38        let mut all_strikes = HashSet::new();
39        for instrument in instruments {
40            let Some(strike) = instrument
41                .strike
42                .filter(|strike| strike.is_finite() && *strike > 0.0)
43            else {
44                continue;
45            };
46
47            let key = strike.to_bits();
48            all_strikes.insert(key);
49            let entry = strike_map.entry(key).or_insert((false, false));
50            match instrument
51                .option_type
52                .as_deref()
53                .unwrap_or_default()
54                .to_ascii_lowercase()
55                .as_str()
56            {
57                "call" => entry.0 = true,
58                "put" => entry.1 = true,
59                _ => {}
60            }
61        }
62
63        let mut paired_strikes: Vec<f64> = strike_map
64            .iter()
65            .filter(|(_, (has_call, has_put))| *has_call && *has_put)
66            .map(|(bits, _)| f64::from_bits(*bits))
67            .collect();
68        paired_strikes.sort_by(|a, b| a.total_cmp(b));
69
70        result.insert(
71            expiry_code,
72            ExpiryGroup {
73                expiry_code,
74                expiration_timestamp,
75                paired_strikes,
76                all_strikes,
77            },
78        );
79    }
80
81    result
82}
83
84fn timestamp_to_code(timestamp_ms: i64) -> Option<u32> {
85    let datetime = chrono::DateTime::<Utc>::from_timestamp_millis(timestamp_ms)?;
86    let date = datetime.date_naive();
87    Some((date.year() as u32) * 10000 + date.month() * 100 + date.day())
88}
89
90pub fn code_to_deribit_date(code: u32) -> Option<String> {
91    let year = (code / 10000) as i32;
92    let month = (code / 100) % 100;
93    let day = code % 100;
94    let date = NaiveDate::from_ymd_opt(year, month, day)?;
95    let month_str = date.format("%b").to_string().to_uppercase();
96    let year_str = format!("{:02}", year % 100);
97    Some(format!("{}{}{}", day, month_str, year_str))
98}
99
100pub fn hypercall_to_deribit(symbol: &str) -> Option<String> {
101    let mut parts = symbol.split('-');
102    let underlying = parts.next()?.to_uppercase();
103    let expiry = parts.next()?;
104    let strike = parts.next()?;
105    let option_type = parts.next()?;
106
107    if expiry.len() != 8 {
108        return None;
109    }
110    let code: u32 = expiry.parse().ok()?;
111    let deribit_date = code_to_deribit_date(code)?;
112    let strike_value: f64 = strike.parse().ok()?;
113    let strike_display = if (strike_value - strike_value.round()).abs() < f64::EPSILON {
114        format!("{:.0}", strike_value)
115    } else {
116        strike.to_string()
117    };
118
119    Some(format!(
120        "{}-{}-{}-{}",
121        underlying,
122        deribit_date,
123        strike_display,
124        option_type.to_uppercase()
125    ))
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    fn instrument(strike: f64, option_type: &str) -> DeribitInstrument {
133        DeribitInstrument {
134            instrument_name: format!("BTC-30JAN26-{strike}-{option_type}"),
135            strike: Some(strike),
136            option_type: Some(option_type.to_string()),
137            expiration_timestamp: 1_769_762_400_000,
138        }
139    }
140
141    #[test]
142    fn groups_only_paired_strikes() {
143        let groups = group_instruments(vec![
144            instrument(100_000.0, "call"),
145            instrument(100_000.0, "put"),
146            instrument(105_000.0, "call"),
147        ]);
148        let group = groups.get(&20260130).unwrap();
149        assert_eq!(group.paired_strikes, vec![100_000.0]);
150        assert_eq!(group.all_strikes.len(), 2);
151    }
152
153    #[test]
154    fn converts_hypercall_to_deribit_symbol() {
155        assert_eq!(
156            hypercall_to_deribit("BTC-20260130-100000-C").unwrap(),
157            "BTC-30JAN26-100000-C"
158        );
159    }
160}