hypercall/shared/
order_types.rs1use crate::types::OptionType;
2pub use hypercall_types::perp_underlying;
3pub use hypercall_types::ws_protocol::{WsOrderMessage, WsOrderRequest};
4use hypercall_types::{validate_price_precision, MAX_PRICE_SIGNIFICANT_FIGURES};
5use rust_decimal::prelude::ToPrimitive;
6use rust_decimal::Decimal;
7use rust_decimal_macros::dec;
8use std::str::FromStr;
9use std::time::SystemTime;
10
11#[derive(Debug, Clone)]
12pub struct ParsedSymbol {
13 pub underlying: String,
14 pub expiry: u64,
15 pub strike: Decimal,
16 pub option_type: OptionType,
17}
18
19fn parse_deribit_expiry(expiry_str: &str) -> Option<u64> {
21 if expiry_str.len() < 5 {
23 return None;
24 }
25
26 let day_end = expiry_str.chars().position(|c| !c.is_ascii_digit())?;
28 let day = expiry_str[..day_end].parse::<u32>().ok()?;
29
30 let month_str = &expiry_str[day_end..];
32 if month_str.len() < 5 {
33 return None;
35 }
36 let month_abbr = &month_str[..3];
37 let month = match month_abbr {
38 "JAN" => 1,
39 "FEB" => 2,
40 "MAR" => 3,
41 "APR" => 4,
42 "MAY" => 5,
43 "JUN" => 6,
44 "JUL" => 7,
45 "AUG" => 8,
46 "SEP" => 9,
47 "OCT" => 10,
48 "NOV" => 11,
49 "DEC" => 12,
50 _ => return None,
51 };
52
53 let year_str = &month_str[3..];
55 let year = year_str.parse::<u32>().ok()?;
56 let full_year = 2000 + year;
57
58 Some(full_year as u64 * 10000 + month as u64 * 100 + day as u64)
60}
61
62impl ParsedSymbol {
63 pub fn from_symbol(symbol: &str) -> Result<Self, String> {
64 let parts: Vec<&str> = symbol.split('-').collect();
66 if parts.len() != 4 {
67 return Err(format!("Invalid symbol format: {}", symbol));
68 }
69
70 let underlying = parts[0].to_string();
71
72 let expiry = if parts[1].chars().all(|c| c.is_ascii_digit()) {
74 parts[1]
76 .parse::<u64>()
77 .map_err(|_| format!("Invalid expiry: {}", parts[1]))?
78 } else {
79 parse_deribit_expiry(parts[1])
81 .ok_or_else(|| format!("Invalid expiry format: {}", parts[1]))?
82 };
83
84 let strike =
85 Decimal::from_str(parts[2]).map_err(|_| format!("Invalid strike: {}", parts[2]))?;
86
87 let option_type = match parts[3] {
88 "C" | "c" => OptionType::Call,
89 "P" | "p" => OptionType::Put,
90 _ => return Err(format!("Invalid option type: {}", parts[3])),
91 };
92
93 Ok(ParsedSymbol {
94 underlying,
95 expiry,
96 strike,
97 option_type,
98 })
99 }
100}
101
102pub fn validate_order_request(request: &WsOrderRequest) -> Result<ParsedSymbol, String> {
103 if request.price <= dec!(0) {
104 return Err("Price must be positive".to_string());
105 }
106
107 if request.size <= dec!(0) {
108 return Err("Size must be greater than zero".to_string());
109 }
110
111 let price_f64 = request.price.to_f64().unwrap_or(0.0);
114 validate_price_precision(price_f64, MAX_PRICE_SIGNIFICANT_FIGURES)?;
115
116 ParsedSymbol::from_symbol(&request.symbol)
117}
118
119pub fn get_timestamp_millis() -> u64 {
120 SystemTime::now()
121 .duration_since(SystemTime::UNIX_EPOCH)
122 .unwrap()
123 .as_millis() as u64
124}
125
126pub fn f64_to_decimal(value: f64) -> Decimal {
129 Decimal::from_f64_retain(value).unwrap_or(Decimal::ZERO)
130}
131
132pub fn to_contract_units_decimal(symbol: &str, size: Decimal) -> Decimal {
135 hypercall_types::to_contract_units_decimal(symbol, size)
136}
137
138pub fn to_human_readable_decimal(symbol: &str, size: Decimal) -> Decimal {
141 hypercall_types::to_human_readable_decimal(symbol, size)
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use hypercall_types::Side;
148
149 #[test]
150 fn test_parse_symbol_valid() {
151 let symbol = "BTC-20240131-100000-C";
152 let parsed = ParsedSymbol::from_symbol(symbol).unwrap();
153 assert_eq!(parsed.underlying, "BTC");
154 assert_eq!(parsed.expiry, 20240131);
155 assert_eq!(parsed.strike, dec!(100000));
156 assert_eq!(parsed.option_type, OptionType::Call);
157 }
158
159 #[test]
160 fn test_parse_symbol_put() {
161 let symbol = "ETH-20240228-3000-P";
162 let parsed = ParsedSymbol::from_symbol(symbol).unwrap();
163 assert_eq!(parsed.underlying, "ETH");
164 assert_eq!(parsed.expiry, 20240228);
165 assert_eq!(parsed.strike, dec!(3000));
166 assert_eq!(parsed.option_type, OptionType::Put);
167 }
168
169 #[test]
170 fn test_parse_symbol_invalid_format() {
171 let symbol = "BTC-100000-C";
172 assert!(ParsedSymbol::from_symbol(symbol).is_err());
173 }
174
175 #[test]
176 fn test_parse_symbol_deribit_format() {
177 let symbol = "BTC-30AUG25-95000-C";
178 let parsed = ParsedSymbol::from_symbol(symbol).unwrap();
179 assert_eq!(parsed.underlying, "BTC");
180 assert_eq!(parsed.expiry, 20250830);
181 assert_eq!(parsed.strike, dec!(95000));
182 assert_eq!(parsed.option_type, OptionType::Call);
183 }
184
185 #[test]
186 fn test_parse_symbol_deribit_put() {
187 let symbol = "ETH-6SEP25-3000-P";
188 let parsed = ParsedSymbol::from_symbol(symbol).unwrap();
189 assert_eq!(parsed.underlying, "ETH");
190 assert_eq!(parsed.expiry, 20250906);
191 assert_eq!(parsed.strike, dec!(3000));
192 assert_eq!(parsed.option_type, OptionType::Put);
193 }
194
195 #[test]
196 fn perp_underlying_handles_perp_suffix() {
197 assert_eq!(perp_underlying("BTC-PERP"), Some("BTC"));
198 assert_eq!(perp_underlying("ETH-PERP"), Some("ETH"));
199 }
200
201 #[test]
202 fn perp_underlying_handles_raw_symbol() {
203 assert_eq!(perp_underlying("BTC"), Some("BTC"));
204 assert_eq!(perp_underlying("ETH"), Some("ETH"));
205 }
206
207 #[test]
208 fn perp_underlying_rejects_option_symbols() {
209 assert_eq!(perp_underlying("BTC-20240131-100000-C"), None);
210 assert_eq!(perp_underlying("BTC-INVALID"), None);
211 }
212
213 #[test]
214 fn perp_underlying_rejects_empty() {
215 assert_eq!(perp_underlying(""), None);
216 }
217
218 #[test]
219 fn test_validate_order_request_valid() {
220 let request = WsOrderRequest {
221 price: dec!(100),
222 size: dec!(10),
223 symbol: "BTC-20240131-100000-C".to_string(),
224 side: Side::Buy,
225 tif: hypercall_types::TimeInForce::GTC,
226 };
227 assert!(validate_order_request(&request).is_ok());
228 }
229
230 #[test]
231 fn test_validate_order_request_invalid_price() {
232 let request = WsOrderRequest {
233 price: dec!(-10),
234 size: dec!(10),
235 symbol: "BTC-20240131-100000-C".to_string(),
236 side: Side::Buy,
237 tif: hypercall_types::TimeInForce::GTC,
238 };
239 assert!(validate_order_request(&request).is_err());
240 }
241
242 #[test]
243 fn test_validate_order_request_zero_size() {
244 let request = WsOrderRequest {
245 price: dec!(100),
246 size: dec!(0),
247 symbol: "BTC-20240131-100000-C".to_string(),
248 side: Side::Buy,
249 tif: hypercall_types::TimeInForce::GTC,
250 };
251 assert!(validate_order_request(&request).is_err());
252 }
253}