catalog_manager/
deribit.rs1use 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}