1use crate::model::order::OrderSide;
16use pretty_simple_display::{DebugPretty, DisplaySimple};
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
23#[serde(rename_all = "lowercase")]
24pub enum BlockTradeRole {
25 #[default]
27 Maker,
28 Taker,
30}
31
32impl BlockTradeRole {
33 #[must_use]
35 pub fn as_str(&self) -> &'static str {
36 match self {
37 Self::Maker => "maker",
38 Self::Taker => "taker",
39 }
40 }
41}
42
43impl std::fmt::Display for BlockTradeRole {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}", self.as_str())
46 }
47}
48
49#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
54pub struct BlockTradeLeg {
55 pub instrument_name: String,
57 pub price: f64,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub amount: Option<f64>,
62 pub direction: OrderSide,
64}
65
66impl BlockTradeLeg {
67 #[must_use]
69 pub fn new(instrument_name: String, price: f64, amount: f64, direction: OrderSide) -> Self {
70 Self {
71 instrument_name,
72 price,
73 amount: Some(amount),
74 direction,
75 }
76 }
77}
78
79#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
84pub struct VerifyBlockTradeRequest {
85 pub timestamp: i64,
87 pub nonce: String,
89 pub role: BlockTradeRole,
91 pub trades: Vec<BlockTradeLeg>,
93}
94
95impl VerifyBlockTradeRequest {
96 #[must_use]
98 pub fn new(
99 timestamp: i64,
100 nonce: String,
101 role: BlockTradeRole,
102 trades: Vec<BlockTradeLeg>,
103 ) -> Self {
104 Self {
105 timestamp,
106 nonce,
107 role,
108 trades,
109 }
110 }
111}
112
113#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct BlockTradeSignature {
119 pub signature: String,
121}
122
123impl BlockTradeSignature {
124 #[must_use]
126 pub fn new(signature: String) -> Self {
127 Self { signature }
128 }
129}
130
131#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
137pub struct ExecuteBlockTradeRequest {
138 pub timestamp: i64,
140 pub nonce: String,
142 pub role: BlockTradeRole,
144 pub trades: Vec<BlockTradeLeg>,
146 pub counterparty_signature: String,
148}
149
150impl ExecuteBlockTradeRequest {
151 #[must_use]
153 pub fn new(
154 timestamp: i64,
155 nonce: String,
156 role: BlockTradeRole,
157 trades: Vec<BlockTradeLeg>,
158 counterparty_signature: String,
159 ) -> Self {
160 Self {
161 timestamp,
162 nonce,
163 role,
164 trades,
165 counterparty_signature,
166 }
167 }
168
169 #[must_use]
171 pub fn from_verify_request(
172 verify_request: VerifyBlockTradeRequest,
173 counterparty_signature: String,
174 ) -> Self {
175 Self {
176 timestamp: verify_request.timestamp,
177 nonce: verify_request.nonce,
178 role: verify_request.role,
179 trades: verify_request.trades,
180 counterparty_signature,
181 }
182 }
183}
184
185#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
189pub struct BlockTradeExecution {
190 pub trade_id: String,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub trade_seq: Option<i64>,
195 pub instrument_name: String,
197 pub direction: String,
199 pub amount: f64,
201 pub price: f64,
203 pub fee: f64,
205 pub fee_currency: String,
207 pub order_id: String,
209 pub order_type: String,
211 pub liquidity: String,
213 pub index_price: f64,
215 pub mark_price: f64,
217 pub block_trade_id: String,
219 pub timestamp: i64,
221 pub state: String,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub tick_direction: Option<i32>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub api: Option<bool>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub post_only: Option<bool>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub reduce_only: Option<bool>,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub iv: Option<f64>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub underlying_price: Option<f64>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub label: Option<String>,
244}
245
246#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
251pub struct BlockTrade {
252 pub id: String,
254 pub timestamp: i64,
256 pub trades: Vec<BlockTradeExecution>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub app_name: Option<String>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub broker_code: Option<String>,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub broker_name: Option<String>,
267}
268
269impl BlockTrade {
270 #[must_use]
272 pub fn trade_count(&self) -> usize {
273 self.trades.len()
274 }
275
276 #[must_use]
278 pub fn is_broker_trade(&self) -> bool {
279 self.broker_code.is_some()
280 }
281
282 #[must_use]
284 pub fn instruments(&self) -> Vec<&str> {
285 let mut instruments: Vec<&str> = self
286 .trades
287 .iter()
288 .map(|t| t.instrument_name.as_str())
289 .collect();
290 instruments.sort();
291 instruments.dedup();
292 instruments
293 }
294
295 #[must_use]
297 pub fn total_fees(&self) -> f64 {
298 self.trades.iter().map(|t| t.fee).sum()
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_block_trade_role_default() {
308 let role = BlockTradeRole::default();
309 assert_eq!(role, BlockTradeRole::Maker);
310 }
311
312 #[test]
313 fn test_block_trade_role_as_str() {
314 assert_eq!(BlockTradeRole::Maker.as_str(), "maker");
315 assert_eq!(BlockTradeRole::Taker.as_str(), "taker");
316 }
317
318 #[test]
319 fn test_block_trade_role_display() {
320 assert_eq!(format!("{}", BlockTradeRole::Maker), "maker");
321 assert_eq!(format!("{}", BlockTradeRole::Taker), "taker");
322 }
323
324 #[test]
325 fn test_block_trade_role_serialization() {
326 let maker = BlockTradeRole::Maker;
327 let json = serde_json::to_string(&maker).unwrap();
328 assert_eq!(json, "\"maker\"");
329
330 let deserialized: BlockTradeRole = serde_json::from_str(&json).unwrap();
331 assert_eq!(deserialized, BlockTradeRole::Maker);
332 }
333
334 #[test]
335 fn test_block_trade_leg_new() {
336 let leg = BlockTradeLeg::new(
337 "BTC-PERPETUAL".to_string(),
338 50000.0,
339 10000.0,
340 OrderSide::Buy,
341 );
342 assert_eq!(leg.instrument_name, "BTC-PERPETUAL");
343 assert!((leg.price - 50000.0).abs() < f64::EPSILON);
344 assert_eq!(leg.amount, Some(10000.0));
345 assert_eq!(leg.direction, OrderSide::Buy);
346 }
347
348 #[test]
349 fn test_block_trade_leg_serialization() {
350 let leg = BlockTradeLeg::new(
351 "BTC-PERPETUAL".to_string(),
352 50000.0,
353 10000.0,
354 OrderSide::Buy,
355 );
356 let json = serde_json::to_string(&leg).unwrap();
357 let deserialized: BlockTradeLeg = serde_json::from_str(&json).unwrap();
358 assert_eq!(leg, deserialized);
359 }
360
361 #[test]
362 fn test_verify_block_trade_request_new() {
363 let trades = vec![BlockTradeLeg::new(
364 "BTC-PERPETUAL".to_string(),
365 50000.0,
366 10000.0,
367 OrderSide::Buy,
368 )];
369 let request = VerifyBlockTradeRequest::new(
370 1640995200000,
371 "test_nonce".to_string(),
372 BlockTradeRole::Maker,
373 trades,
374 );
375 assert_eq!(request.timestamp, 1640995200000);
376 assert_eq!(request.nonce, "test_nonce");
377 assert_eq!(request.role, BlockTradeRole::Maker);
378 assert_eq!(request.trades.len(), 1);
379 }
380
381 #[test]
382 fn test_block_trade_signature_new() {
383 let sig = BlockTradeSignature::new("test_signature_123".to_string());
384 assert_eq!(sig.signature, "test_signature_123");
385 }
386
387 #[test]
388 fn test_block_trade_signature_serialization() {
389 let sig = BlockTradeSignature::new("test_signature_123".to_string());
390 let json = serde_json::to_string(&sig).unwrap();
391 let deserialized: BlockTradeSignature = serde_json::from_str(&json).unwrap();
392 assert_eq!(sig, deserialized);
393 }
394
395 #[test]
396 fn test_execute_block_trade_request_new() {
397 let trades = vec![BlockTradeLeg::new(
398 "BTC-PERPETUAL".to_string(),
399 50000.0,
400 10000.0,
401 OrderSide::Buy,
402 )];
403 let request = ExecuteBlockTradeRequest::new(
404 1640995200000,
405 "test_nonce".to_string(),
406 BlockTradeRole::Maker,
407 trades,
408 "counterparty_sig".to_string(),
409 );
410 assert_eq!(request.timestamp, 1640995200000);
411 assert_eq!(request.counterparty_signature, "counterparty_sig");
412 }
413
414 #[test]
415 fn test_execute_block_trade_request_from_verify() {
416 let trades = vec![BlockTradeLeg::new(
417 "BTC-PERPETUAL".to_string(),
418 50000.0,
419 10000.0,
420 OrderSide::Buy,
421 )];
422 let verify_request = VerifyBlockTradeRequest::new(
423 1640995200000,
424 "test_nonce".to_string(),
425 BlockTradeRole::Maker,
426 trades,
427 );
428 let exec_request = ExecuteBlockTradeRequest::from_verify_request(
429 verify_request.clone(),
430 "counterparty_sig".to_string(),
431 );
432 assert_eq!(exec_request.timestamp, verify_request.timestamp);
433 assert_eq!(exec_request.nonce, verify_request.nonce);
434 assert_eq!(exec_request.role, verify_request.role);
435 assert_eq!(exec_request.counterparty_signature, "counterparty_sig");
436 }
437
438 fn create_test_block_trade_execution() -> BlockTradeExecution {
439 BlockTradeExecution {
440 trade_id: "48079573".to_string(),
441 trade_seq: Some(30289730),
442 instrument_name: "BTC-PERPETUAL".to_string(),
443 direction: "sell".to_string(),
444 amount: 200000.0,
445 price: 8900.0,
446 fee: -0.00561798,
447 fee_currency: "BTC".to_string(),
448 order_id: "4009043192".to_string(),
449 order_type: "limit".to_string(),
450 liquidity: "M".to_string(),
451 index_price: 8900.45,
452 mark_price: 8895.19,
453 block_trade_id: "6165".to_string(),
454 timestamp: 1590485535978,
455 state: "filled".to_string(),
456 tick_direction: Some(0),
457 api: None,
458 post_only: Some(false),
459 reduce_only: Some(false),
460 iv: None,
461 underlying_price: None,
462 label: None,
463 }
464 }
465
466 #[test]
467 fn test_block_trade_execution_serialization() {
468 let exec = create_test_block_trade_execution();
469 let json = serde_json::to_string(&exec).unwrap();
470 let deserialized: BlockTradeExecution = serde_json::from_str(&json).unwrap();
471 assert_eq!(exec.trade_id, deserialized.trade_id);
472 assert_eq!(exec.instrument_name, deserialized.instrument_name);
473 }
474
475 #[test]
476 fn test_block_trade_trade_count() {
477 let block_trade = BlockTrade {
478 id: "6165".to_string(),
479 timestamp: 1590485535980,
480 trades: vec![
481 create_test_block_trade_execution(),
482 create_test_block_trade_execution(),
483 ],
484 app_name: None,
485 broker_code: None,
486 broker_name: None,
487 };
488 assert_eq!(block_trade.trade_count(), 2);
489 }
490
491 #[test]
492 fn test_block_trade_is_broker_trade() {
493 let regular_trade = BlockTrade {
494 id: "6165".to_string(),
495 timestamp: 1590485535980,
496 trades: vec![],
497 app_name: None,
498 broker_code: None,
499 broker_name: None,
500 };
501 assert!(!regular_trade.is_broker_trade());
502
503 let broker_trade = BlockTrade {
504 id: "6165".to_string(),
505 timestamp: 1590485535980,
506 trades: vec![],
507 app_name: None,
508 broker_code: Some("BROKER123".to_string()),
509 broker_name: Some("Test Broker".to_string()),
510 };
511 assert!(broker_trade.is_broker_trade());
512 }
513
514 #[test]
515 fn test_block_trade_instruments() {
516 let mut exec1 = create_test_block_trade_execution();
517 exec1.instrument_name = "BTC-PERPETUAL".to_string();
518
519 let mut exec2 = create_test_block_trade_execution();
520 exec2.instrument_name = "BTC-28MAY20-9000-C".to_string();
521
522 let block_trade = BlockTrade {
523 id: "6165".to_string(),
524 timestamp: 1590485535980,
525 trades: vec![exec1, exec2],
526 app_name: None,
527 broker_code: None,
528 broker_name: None,
529 };
530
531 let instruments = block_trade.instruments();
532 assert_eq!(instruments.len(), 2);
533 assert!(instruments.contains(&"BTC-PERPETUAL"));
534 assert!(instruments.contains(&"BTC-28MAY20-9000-C"));
535 }
536
537 #[test]
538 fn test_block_trade_total_fees() {
539 let mut exec1 = create_test_block_trade_execution();
540 exec1.fee = 0.001;
541
542 let mut exec2 = create_test_block_trade_execution();
543 exec2.fee = 0.002;
544
545 let block_trade = BlockTrade {
546 id: "6165".to_string(),
547 timestamp: 1590485535980,
548 trades: vec![exec1, exec2],
549 app_name: None,
550 broker_code: None,
551 broker_name: None,
552 };
553
554 assert!((block_trade.total_fees() - 0.003).abs() < f64::EPSILON);
555 }
556
557 #[test]
558 fn test_block_trade_serialization() {
559 let block_trade = BlockTrade {
560 id: "6165".to_string(),
561 timestamp: 1590485535980,
562 trades: vec![create_test_block_trade_execution()],
563 app_name: Some("TestApp".to_string()),
564 broker_code: None,
565 broker_name: None,
566 };
567 let json = serde_json::to_string(&block_trade).unwrap();
568 let deserialized: BlockTrade = serde_json::from_str(&json).unwrap();
569 assert_eq!(block_trade.id, deserialized.id);
570 assert_eq!(block_trade.app_name, deserialized.app_name);
571 }
572}