1use deribit_base::{impl_json_debug_pretty, impl_json_display};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Serialize, Deserialize, PartialEq)]
8pub struct Quote {
9 pub instrument_name: String,
11 pub side: String,
13 pub amount: f64,
15 pub price: f64,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub quote_set_id: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub post_only: Option<bool>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub time_in_force: Option<String>,
26}
27
28impl_json_display!(Quote);
29impl_json_debug_pretty!(Quote);
30
31#[derive(Clone, Serialize, Deserialize)]
33pub struct MassQuoteRequest {
34 pub mmp_group: String,
36 pub quotes: Vec<Quote>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub quote_id: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub detailed: Option<bool>,
44}
45
46impl_json_display!(MassQuoteRequest);
47impl_json_debug_pretty!(MassQuoteRequest);
48
49#[derive(Clone, Serialize, Deserialize)]
51pub struct MassQuoteResult {
52 pub success_count: u32,
54 pub error_count: u32,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub errors: Option<Vec<QuoteError>>,
59}
60
61impl_json_display!(MassQuoteResult);
62impl_json_debug_pretty!(MassQuoteResult);
63
64#[derive(Clone, Serialize, Deserialize)]
66pub struct QuoteError {
67 pub instrument_name: String,
69 pub side: String,
71 pub error_code: i32,
73 pub error_message: String,
75}
76
77impl_json_display!(QuoteError);
78impl_json_debug_pretty!(QuoteError);
79
80#[derive(Clone, Serialize, Deserialize)]
82pub struct CancelQuotesRequest {
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub currency: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub kind: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub instrument_name: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub quote_set_id: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub delta_range: Option<(f64, f64)>,
98}
99
100impl_json_display!(CancelQuotesRequest);
101impl_json_debug_pretty!(CancelQuotesRequest);
102
103#[derive(Clone, Serialize, Deserialize)]
105pub struct CancelQuotesResponse {
106 pub cancelled_count: u32,
108}
109
110impl_json_display!(CancelQuotesResponse);
111impl_json_debug_pretty!(CancelQuotesResponse);
112
113#[derive(Clone, Serialize, Deserialize)]
115pub struct MmpGroupConfig {
116 pub mmp_group: String,
118 pub quantity_limit: f64,
120 pub delta_limit: f64,
122 pub interval: u64,
124 pub frozen_time: u64,
126 pub enabled: bool,
128}
129
130impl_json_display!(MmpGroupConfig);
131impl_json_debug_pretty!(MmpGroupConfig);
132
133#[derive(Clone, Serialize, Deserialize)]
135pub struct MmpGroupStatus {
136 pub mmp_group: String,
138 pub config: MmpGroupConfig,
140 pub reserved_margin: f64,
142 pub active_quotes: u32,
144 pub is_frozen: bool,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub freeze_end_time: Option<u64>,
149}
150
151impl_json_display!(MmpGroupStatus);
152impl_json_debug_pretty!(MmpGroupStatus);
153
154#[derive(Clone, Serialize, Deserialize)]
156pub struct QuoteInfo {
157 pub quote_id: String,
159 pub instrument_name: String,
161 pub side: String,
163 pub amount: f64,
165 pub price: f64,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub quote_set_id: Option<String>,
170 pub mmp_group: String,
172 pub creation_timestamp: u64,
174 pub state: String,
176 pub filled_amount: f64,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub average_price: Option<f64>,
181 pub priority: u64,
183}
184
185impl_json_display!(QuoteInfo);
186impl_json_debug_pretty!(QuoteInfo);
187
188#[derive(Clone, Serialize, Deserialize)]
190pub struct MmpTrigger {
191 pub currency: String,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub mmp_group: Option<String>,
196 pub timestamp: u64,
198 pub reason: String,
200 pub frozen_time: u64,
202}
203
204impl_json_display!(MmpTrigger);
205impl_json_debug_pretty!(MmpTrigger);
206
207impl Quote {
208 pub fn buy(instrument_name: String, amount: f64, price: f64) -> Self {
210 Self {
211 instrument_name,
212 side: "buy".to_string(),
213 amount,
214 price,
215 quote_set_id: None,
216 post_only: None,
217 time_in_force: None,
218 }
219 }
220
221 pub fn sell(instrument_name: String, amount: f64, price: f64) -> Self {
223 Self {
224 instrument_name,
225 side: "sell".to_string(),
226 amount,
227 price,
228 quote_set_id: None,
229 post_only: None,
230 time_in_force: None,
231 }
232 }
233
234 pub fn with_quote_set_id(mut self, quote_set_id: String) -> Self {
236 self.quote_set_id = Some(quote_set_id);
237 self
238 }
239
240 pub fn with_post_only(mut self, post_only: bool) -> Self {
242 self.post_only = Some(post_only);
243 self
244 }
245
246 pub fn with_time_in_force(mut self, time_in_force: String) -> Self {
248 self.time_in_force = Some(time_in_force);
249 self
250 }
251}
252
253impl MassQuoteRequest {
254 pub fn new(mmp_group: String, quotes: Vec<Quote>) -> Self {
256 Self {
257 mmp_group,
258 quotes,
259 quote_id: None,
260 detailed: None,
261 }
262 }
263
264 pub fn with_quote_id(mut self, quote_id: String) -> Self {
266 self.quote_id = Some(quote_id);
267 self
268 }
269
270 pub fn with_detailed_errors(mut self) -> Self {
272 self.detailed = Some(true);
273 self
274 }
275
276 pub fn validate(&self) -> Result<(), String> {
278 if self.quotes.is_empty() {
279 return Err("Mass quote request must contain at least one quote".to_string());
280 }
281
282 if self.quotes.len() > 100 {
283 return Err("Mass quote request cannot contain more than 100 quotes".to_string());
284 }
285
286 let mut currencies = std::collections::HashSet::new();
288 for quote in &self.quotes {
289 let currency = quote
290 .instrument_name
291 .split('-')
292 .next()
293 .ok_or("Invalid instrument name format")?;
294 currencies.insert(currency);
295 }
296
297 if currencies.len() > 1 {
298 return Err(
299 "All quotes in a mass quote request must be for the same currency".to_string(),
300 );
301 }
302
303 let mut seen = std::collections::HashSet::new();
305 for quote in &self.quotes {
306 let key = ("e.instrument_name, "e.side, quote.price as u64);
307 if !seen.insert(key) {
308 return Err(format!(
309 "Duplicate quote found for {} {} at price {}",
310 quote.instrument_name, quote.side, quote.price
311 ));
312 }
313 }
314
315 Ok(())
316 }
317}
318
319impl CancelQuotesRequest {
320 pub fn all() -> Self {
322 Self {
323 currency: None,
324 kind: None,
325 instrument_name: None,
326 quote_set_id: None,
327 delta_range: None,
328 }
329 }
330
331 pub fn by_currency(currency: String) -> Self {
333 Self {
334 currency: Some(currency),
335 kind: None,
336 instrument_name: None,
337 quote_set_id: None,
338 delta_range: None,
339 }
340 }
341
342 pub fn by_instrument(instrument_name: String) -> Self {
344 Self {
345 currency: None,
346 kind: None,
347 instrument_name: Some(instrument_name),
348 quote_set_id: None,
349 delta_range: None,
350 }
351 }
352
353 pub fn by_quote_set_id(quote_set_id: String) -> Self {
355 Self {
356 currency: None,
357 kind: None,
358 instrument_name: None,
359 quote_set_id: Some(quote_set_id),
360 delta_range: None,
361 }
362 }
363
364 pub fn by_delta_range(min_delta: f64, max_delta: f64) -> Self {
366 Self {
367 currency: None,
368 kind: None,
369 instrument_name: None,
370 quote_set_id: None,
371 delta_range: Some((min_delta, max_delta)),
372 }
373 }
374}
375
376impl MmpGroupConfig {
377 pub fn new(
379 mmp_group: String,
380 quantity_limit: f64,
381 delta_limit: f64,
382 interval: u64,
383 frozen_time: u64,
384 ) -> Result<Self, String> {
385 if delta_limit >= quantity_limit {
386 return Err("Delta limit must be less than quantity limit".to_string());
387 }
388
389 let currency = mmp_group.split('_').next().unwrap_or("");
391 let max_limit = match currency.to_uppercase().as_str() {
392 "BTC" => 500.0,
393 "ETH" => 5000.0,
394 _ => 500.0, };
396
397 if quantity_limit > max_limit {
398 return Err(format!(
399 "Quantity limit {} exceeds maximum allowed {} for {}",
400 quantity_limit, max_limit, currency
401 ));
402 }
403
404 Ok(Self {
405 mmp_group,
406 quantity_limit,
407 delta_limit,
408 interval,
409 frozen_time,
410 enabled: true,
411 })
412 }
413
414 pub fn disable(mut self) -> Self {
416 self.interval = 0;
417 self.enabled = false;
418 self
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn test_quote_creation() {
428 let quote = Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0)
429 .with_quote_set_id("set1".to_string())
430 .with_post_only(true);
431
432 assert_eq!(quote.instrument_name, "BTC-PERPETUAL");
433 assert_eq!(quote.side, "buy");
434 assert_eq!(quote.amount, 1.0);
435 assert_eq!(quote.price, 50000.0);
436 assert_eq!(quote.quote_set_id, Some("set1".to_string()));
437 assert_eq!(quote.post_only, Some(true));
438 }
439
440 #[test]
441 fn test_mass_quote_validation() {
442 let quotes = vec![
443 Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0),
444 Quote::sell("BTC-PERPETUAL".to_string(), 1.0, 51000.0),
445 ];
446
447 let request = MassQuoteRequest::new("btc_group".to_string(), quotes);
448 assert!(request.validate().is_ok());
449 }
450
451 #[test]
452 fn test_mass_quote_validation_different_currencies() {
453 let quotes = vec![
454 Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0),
455 Quote::sell("ETH-PERPETUAL".to_string(), 1.0, 3000.0),
456 ];
457
458 let request = MassQuoteRequest::new("mixed_group".to_string(), quotes);
459 assert!(request.validate().is_err());
460 }
461
462 #[test]
463 fn test_mass_quote_validation_duplicate_quotes() {
464 let quotes = vec![
465 Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0),
466 Quote::buy("BTC-PERPETUAL".to_string(), 2.0, 50000.0), ];
468
469 let request = MassQuoteRequest::new("btc_group".to_string(), quotes);
470 assert!(request.validate().is_err());
471 }
472
473 #[test]
474 fn test_mmp_group_config_validation() {
475 let config = MmpGroupConfig::new("btc_group".to_string(), 100.0, 50.0, 1000, 5000);
476 assert!(config.is_ok());
477
478 let invalid_config = MmpGroupConfig::new(
479 "btc_group".to_string(),
480 50.0,
481 100.0, 1000,
483 5000,
484 );
485 assert!(invalid_config.is_err());
486 }
487
488 #[test]
489 fn test_cancel_quotes_builders() {
490 let cancel_all = CancelQuotesRequest::all();
491 assert!(cancel_all.currency.is_none());
492
493 let cancel_btc = CancelQuotesRequest::by_currency("BTC".to_string());
494 assert_eq!(cancel_btc.currency, Some("BTC".to_string()));
495
496 let cancel_instrument = CancelQuotesRequest::by_instrument("BTC-PERPETUAL".to_string());
497 assert_eq!(
498 cancel_instrument.instrument_name,
499 Some("BTC-PERPETUAL".to_string())
500 );
501
502 let cancel_set = CancelQuotesRequest::by_quote_set_id("set1".to_string());
503 assert_eq!(cancel_set.quote_set_id, Some("set1".to_string()));
504
505 let cancel_delta = CancelQuotesRequest::by_delta_range(0.3, 0.7);
506 assert_eq!(cancel_delta.delta_range, Some((0.3, 0.7)));
507 }
508}