1use pretty_simple_display::{DebugPretty, DisplaySimple};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
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
28#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
30pub struct MassQuoteRequest {
31 pub mmp_group: String,
33 pub quotes: Vec<Quote>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub quote_id: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub detailed: Option<bool>,
41}
42
43#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
45pub struct MassQuoteResult {
46 pub success_count: u32,
48 pub error_count: u32,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub errors: Option<Vec<QuoteError>>,
53}
54
55#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
57pub struct QuoteError {
58 pub instrument_name: String,
60 pub side: String,
62 pub error_code: i32,
64 pub error_message: String,
66}
67
68#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
70pub struct CancelQuotesRequest {
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub currency: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub kind: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub instrument_name: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub quote_set_id: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub delta_range: Option<(f64, f64)>,
86}
87
88#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
90pub struct CancelQuotesResponse {
91 pub cancelled_count: u32,
93}
94
95#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
97pub struct MmpGroupConfig {
98 pub mmp_group: String,
100 pub quantity_limit: f64,
102 pub delta_limit: f64,
104 pub interval: u64,
106 pub frozen_time: u64,
108 pub enabled: bool,
110}
111
112#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
114pub struct MmpGroupStatus {
115 pub mmp_group: String,
117 pub config: MmpGroupConfig,
119 pub reserved_margin: f64,
121 pub active_quotes: u32,
123 pub is_frozen: bool,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub freeze_end_time: Option<u64>,
128}
129
130#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
132pub struct QuoteInfo {
133 pub quote_id: String,
135 pub instrument_name: String,
137 pub side: String,
139 pub amount: f64,
141 pub price: f64,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub quote_set_id: Option<String>,
146 pub mmp_group: String,
148 pub creation_timestamp: u64,
150 pub state: String,
152 pub filled_amount: f64,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub average_price: Option<f64>,
157 pub priority: u64,
159}
160
161#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
163pub struct MmpTrigger {
164 pub currency: String,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub mmp_group: Option<String>,
169 pub timestamp: u64,
171 pub reason: String,
173 pub frozen_time: u64,
175}
176
177impl Quote {
178 pub fn buy(instrument_name: String, amount: f64, price: f64) -> Self {
180 Self {
181 instrument_name,
182 side: "buy".to_string(),
183 amount,
184 price,
185 quote_set_id: None,
186 post_only: None,
187 time_in_force: None,
188 }
189 }
190
191 pub fn sell(instrument_name: String, amount: f64, price: f64) -> Self {
193 Self {
194 instrument_name,
195 side: "sell".to_string(),
196 amount,
197 price,
198 quote_set_id: None,
199 post_only: None,
200 time_in_force: None,
201 }
202 }
203
204 pub fn with_quote_set_id(mut self, quote_set_id: String) -> Self {
206 self.quote_set_id = Some(quote_set_id);
207 self
208 }
209
210 pub fn with_post_only(mut self, post_only: bool) -> Self {
212 self.post_only = Some(post_only);
213 self
214 }
215
216 pub fn with_time_in_force(mut self, time_in_force: String) -> Self {
218 self.time_in_force = Some(time_in_force);
219 self
220 }
221}
222
223impl MassQuoteRequest {
224 pub fn new(mmp_group: String, quotes: Vec<Quote>) -> Self {
226 Self {
227 mmp_group,
228 quotes,
229 quote_id: None,
230 detailed: None,
231 }
232 }
233
234 pub fn with_quote_id(mut self, quote_id: String) -> Self {
236 self.quote_id = Some(quote_id);
237 self
238 }
239
240 pub fn with_detailed_errors(mut self) -> Self {
242 self.detailed = Some(true);
243 self
244 }
245
246 pub fn validate(&self) -> Result<(), String> {
248 if self.quotes.is_empty() {
249 return Err("Mass quote request must contain at least one quote".to_string());
250 }
251
252 if self.quotes.len() > 100 {
253 return Err("Mass quote request cannot contain more than 100 quotes".to_string());
254 }
255
256 let mut currencies = std::collections::HashSet::new();
258 for quote in &self.quotes {
259 let currency = quote
260 .instrument_name
261 .split('-')
262 .next()
263 .ok_or("Invalid instrument name format")?;
264 currencies.insert(currency);
265 }
266
267 if currencies.len() > 1 {
268 return Err(
269 "All quotes in a mass quote request must be for the same currency".to_string(),
270 );
271 }
272
273 let mut seen = std::collections::HashSet::new();
275 for quote in &self.quotes {
276 let key = ("e.instrument_name, "e.side, quote.price as u64);
277 if !seen.insert(key) {
278 return Err(format!(
279 "Duplicate quote found for {} {} at price {}",
280 quote.instrument_name, quote.side, quote.price
281 ));
282 }
283 }
284
285 Ok(())
286 }
287}
288
289impl CancelQuotesRequest {
290 pub fn all() -> Self {
292 Self {
293 currency: None,
294 kind: None,
295 instrument_name: None,
296 quote_set_id: None,
297 delta_range: None,
298 }
299 }
300
301 pub fn by_currency(currency: String) -> Self {
303 Self {
304 currency: Some(currency),
305 kind: None,
306 instrument_name: None,
307 quote_set_id: None,
308 delta_range: None,
309 }
310 }
311
312 pub fn by_instrument(instrument_name: String) -> Self {
314 Self {
315 currency: None,
316 kind: None,
317 instrument_name: Some(instrument_name),
318 quote_set_id: None,
319 delta_range: None,
320 }
321 }
322
323 pub fn by_quote_set_id(quote_set_id: String) -> Self {
325 Self {
326 currency: None,
327 kind: None,
328 instrument_name: None,
329 quote_set_id: Some(quote_set_id),
330 delta_range: None,
331 }
332 }
333
334 pub fn by_delta_range(min_delta: f64, max_delta: f64) -> Self {
336 Self {
337 currency: None,
338 kind: None,
339 instrument_name: None,
340 quote_set_id: None,
341 delta_range: Some((min_delta, max_delta)),
342 }
343 }
344}
345
346impl MmpGroupConfig {
347 pub fn new(
349 mmp_group: String,
350 quantity_limit: f64,
351 delta_limit: f64,
352 interval: u64,
353 frozen_time: u64,
354 ) -> Result<Self, String> {
355 if delta_limit >= quantity_limit {
356 return Err("Delta limit must be less than quantity limit".to_string());
357 }
358
359 let currency = mmp_group.split('_').next().unwrap_or("");
361 let max_limit = match currency.to_uppercase().as_str() {
362 "BTC" => 500.0,
363 "ETH" => 5000.0,
364 _ => 500.0, };
366
367 if quantity_limit > max_limit {
368 return Err(format!(
369 "Quantity limit {} exceeds maximum allowed {} for {}",
370 quantity_limit, max_limit, currency
371 ));
372 }
373
374 Ok(Self {
375 mmp_group,
376 quantity_limit,
377 delta_limit,
378 interval,
379 frozen_time,
380 enabled: true,
381 })
382 }
383
384 pub fn disable(mut self) -> Self {
386 self.interval = 0;
387 self.enabled = false;
388 self
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_quote_creation() {
398 let quote = Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0)
399 .with_quote_set_id("set1".to_string())
400 .with_post_only(true);
401
402 assert_eq!(quote.instrument_name, "BTC-PERPETUAL");
403 assert_eq!(quote.side, "buy");
404 assert_eq!(quote.amount, 1.0);
405 assert_eq!(quote.price, 50000.0);
406 assert_eq!(quote.quote_set_id, Some("set1".to_string()));
407 assert_eq!(quote.post_only, Some(true));
408 }
409
410 #[test]
411 fn test_mass_quote_validation() {
412 let quotes = vec![
413 Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0),
414 Quote::sell("BTC-PERPETUAL".to_string(), 1.0, 51000.0),
415 ];
416
417 let request = MassQuoteRequest::new("btc_group".to_string(), quotes);
418 assert!(request.validate().is_ok());
419 }
420
421 #[test]
422 fn test_mass_quote_validation_different_currencies() {
423 let quotes = vec![
424 Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0),
425 Quote::sell("ETH-PERPETUAL".to_string(), 1.0, 3000.0),
426 ];
427
428 let request = MassQuoteRequest::new("mixed_group".to_string(), quotes);
429 assert!(request.validate().is_err());
430 }
431
432 #[test]
433 fn test_mass_quote_validation_duplicate_quotes() {
434 let quotes = vec![
435 Quote::buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0),
436 Quote::buy("BTC-PERPETUAL".to_string(), 2.0, 50000.0), ];
438
439 let request = MassQuoteRequest::new("btc_group".to_string(), quotes);
440 assert!(request.validate().is_err());
441 }
442
443 #[test]
444 fn test_mmp_group_config_validation() {
445 let config = MmpGroupConfig::new("btc_group".to_string(), 100.0, 50.0, 1000, 5000);
446 assert!(config.is_ok());
447
448 let invalid_config = MmpGroupConfig::new(
449 "btc_group".to_string(),
450 50.0,
451 100.0, 1000,
453 5000,
454 );
455 assert!(invalid_config.is_err());
456 }
457
458 #[test]
459 fn test_cancel_quotes_builders() {
460 let cancel_all = CancelQuotesRequest::all();
461 assert!(cancel_all.currency.is_none());
462
463 let cancel_btc = CancelQuotesRequest::by_currency("BTC".to_string());
464 assert_eq!(cancel_btc.currency, Some("BTC".to_string()));
465
466 let cancel_instrument = CancelQuotesRequest::by_instrument("BTC-PERPETUAL".to_string());
467 assert_eq!(
468 cancel_instrument.instrument_name,
469 Some("BTC-PERPETUAL".to_string())
470 );
471
472 let cancel_set = CancelQuotesRequest::by_quote_set_id("set1".to_string());
473 assert_eq!(cancel_set.quote_set_id, Some("set1".to_string()));
474
475 let cancel_delta = CancelQuotesRequest::by_delta_range(0.3, 0.7);
476 assert_eq!(cancel_delta.delta_range, Some((0.3, 0.7)));
477 }
478}