use crate::model::order::OrderSide;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum BlockTradeRole {
#[default]
Maker,
Taker,
}
impl BlockTradeRole {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Maker => "maker",
Self::Taker => "taker",
}
}
}
impl std::fmt::Display for BlockTradeRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockTradeLeg {
pub instrument_name: String,
pub price: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<f64>,
pub direction: OrderSide,
}
impl BlockTradeLeg {
#[must_use]
pub fn new(instrument_name: String, price: f64, amount: f64, direction: OrderSide) -> Self {
Self {
instrument_name,
price,
amount: Some(amount),
direction,
}
}
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
pub struct VerifyBlockTradeRequest {
pub timestamp: i64,
pub nonce: String,
pub role: BlockTradeRole,
pub trades: Vec<BlockTradeLeg>,
}
impl VerifyBlockTradeRequest {
#[must_use]
pub fn new(
timestamp: i64,
nonce: String,
role: BlockTradeRole,
trades: Vec<BlockTradeLeg>,
) -> Self {
Self {
timestamp,
nonce,
role,
trades,
}
}
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockTradeSignature {
pub signature: String,
}
impl BlockTradeSignature {
#[must_use]
pub fn new(signature: String) -> Self {
Self { signature }
}
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecuteBlockTradeRequest {
pub timestamp: i64,
pub nonce: String,
pub role: BlockTradeRole,
pub trades: Vec<BlockTradeLeg>,
pub counterparty_signature: String,
}
impl ExecuteBlockTradeRequest {
#[must_use]
pub fn new(
timestamp: i64,
nonce: String,
role: BlockTradeRole,
trades: Vec<BlockTradeLeg>,
counterparty_signature: String,
) -> Self {
Self {
timestamp,
nonce,
role,
trades,
counterparty_signature,
}
}
#[must_use]
pub fn from_verify_request(
verify_request: VerifyBlockTradeRequest,
counterparty_signature: String,
) -> Self {
Self {
timestamp: verify_request.timestamp,
nonce: verify_request.nonce,
role: verify_request.role,
trades: verify_request.trades,
counterparty_signature,
}
}
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockTradeExecution {
pub trade_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub trade_seq: Option<i64>,
pub instrument_name: String,
pub direction: String,
pub amount: f64,
pub price: f64,
pub fee: f64,
pub fee_currency: String,
pub order_id: String,
pub order_type: String,
pub liquidity: String,
pub index_price: f64,
pub mark_price: f64,
pub block_trade_id: String,
pub timestamp: i64,
pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tick_direction: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iv: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub underlying_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockTrade {
pub id: String,
pub timestamp: i64,
pub trades: Vec<BlockTradeExecution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub broker_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub broker_name: Option<String>,
}
impl BlockTrade {
#[must_use]
pub fn trade_count(&self) -> usize {
self.trades.len()
}
#[must_use]
pub fn is_broker_trade(&self) -> bool {
self.broker_code.is_some()
}
#[must_use]
pub fn instruments(&self) -> Vec<&str> {
let mut instruments: Vec<&str> = self
.trades
.iter()
.map(|t| t.instrument_name.as_str())
.collect();
instruments.sort();
instruments.dedup();
instruments
}
#[must_use]
pub fn total_fees(&self) -> f64 {
self.trades.iter().map(|t| t.fee).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_block_trade_role_default() {
let role = BlockTradeRole::default();
assert_eq!(role, BlockTradeRole::Maker);
}
#[test]
fn test_block_trade_role_as_str() {
assert_eq!(BlockTradeRole::Maker.as_str(), "maker");
assert_eq!(BlockTradeRole::Taker.as_str(), "taker");
}
#[test]
fn test_block_trade_role_display() {
assert_eq!(format!("{}", BlockTradeRole::Maker), "maker");
assert_eq!(format!("{}", BlockTradeRole::Taker), "taker");
}
#[test]
fn test_block_trade_role_serialization() {
let maker = BlockTradeRole::Maker;
let json = serde_json::to_string(&maker).unwrap();
assert_eq!(json, "\"maker\"");
let deserialized: BlockTradeRole = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, BlockTradeRole::Maker);
}
#[test]
fn test_block_trade_leg_new() {
let leg = BlockTradeLeg::new(
"BTC-PERPETUAL".to_string(),
50000.0,
10000.0,
OrderSide::Buy,
);
assert_eq!(leg.instrument_name, "BTC-PERPETUAL");
assert!((leg.price - 50000.0).abs() < f64::EPSILON);
assert_eq!(leg.amount, Some(10000.0));
assert_eq!(leg.direction, OrderSide::Buy);
}
#[test]
fn test_block_trade_leg_serialization() {
let leg = BlockTradeLeg::new(
"BTC-PERPETUAL".to_string(),
50000.0,
10000.0,
OrderSide::Buy,
);
let json = serde_json::to_string(&leg).unwrap();
let deserialized: BlockTradeLeg = serde_json::from_str(&json).unwrap();
assert_eq!(leg, deserialized);
}
#[test]
fn test_verify_block_trade_request_new() {
let trades = vec![BlockTradeLeg::new(
"BTC-PERPETUAL".to_string(),
50000.0,
10000.0,
OrderSide::Buy,
)];
let request = VerifyBlockTradeRequest::new(
1640995200000,
"test_nonce".to_string(),
BlockTradeRole::Maker,
trades,
);
assert_eq!(request.timestamp, 1640995200000);
assert_eq!(request.nonce, "test_nonce");
assert_eq!(request.role, BlockTradeRole::Maker);
assert_eq!(request.trades.len(), 1);
}
#[test]
fn test_block_trade_signature_new() {
let sig = BlockTradeSignature::new("test_signature_123".to_string());
assert_eq!(sig.signature, "test_signature_123");
}
#[test]
fn test_block_trade_signature_serialization() {
let sig = BlockTradeSignature::new("test_signature_123".to_string());
let json = serde_json::to_string(&sig).unwrap();
let deserialized: BlockTradeSignature = serde_json::from_str(&json).unwrap();
assert_eq!(sig, deserialized);
}
#[test]
fn test_execute_block_trade_request_new() {
let trades = vec![BlockTradeLeg::new(
"BTC-PERPETUAL".to_string(),
50000.0,
10000.0,
OrderSide::Buy,
)];
let request = ExecuteBlockTradeRequest::new(
1640995200000,
"test_nonce".to_string(),
BlockTradeRole::Maker,
trades,
"counterparty_sig".to_string(),
);
assert_eq!(request.timestamp, 1640995200000);
assert_eq!(request.counterparty_signature, "counterparty_sig");
}
#[test]
fn test_execute_block_trade_request_from_verify() {
let trades = vec![BlockTradeLeg::new(
"BTC-PERPETUAL".to_string(),
50000.0,
10000.0,
OrderSide::Buy,
)];
let verify_request = VerifyBlockTradeRequest::new(
1640995200000,
"test_nonce".to_string(),
BlockTradeRole::Maker,
trades,
);
let exec_request = ExecuteBlockTradeRequest::from_verify_request(
verify_request.clone(),
"counterparty_sig".to_string(),
);
assert_eq!(exec_request.timestamp, verify_request.timestamp);
assert_eq!(exec_request.nonce, verify_request.nonce);
assert_eq!(exec_request.role, verify_request.role);
assert_eq!(exec_request.counterparty_signature, "counterparty_sig");
}
fn create_test_block_trade_execution() -> BlockTradeExecution {
BlockTradeExecution {
trade_id: "48079573".to_string(),
trade_seq: Some(30289730),
instrument_name: "BTC-PERPETUAL".to_string(),
direction: "sell".to_string(),
amount: 200000.0,
price: 8900.0,
fee: -0.00561798,
fee_currency: "BTC".to_string(),
order_id: "4009043192".to_string(),
order_type: "limit".to_string(),
liquidity: "M".to_string(),
index_price: 8900.45,
mark_price: 8895.19,
block_trade_id: "6165".to_string(),
timestamp: 1590485535978,
state: "filled".to_string(),
tick_direction: Some(0),
api: None,
post_only: Some(false),
reduce_only: Some(false),
iv: None,
underlying_price: None,
label: None,
}
}
#[test]
fn test_block_trade_execution_serialization() {
let exec = create_test_block_trade_execution();
let json = serde_json::to_string(&exec).unwrap();
let deserialized: BlockTradeExecution = serde_json::from_str(&json).unwrap();
assert_eq!(exec.trade_id, deserialized.trade_id);
assert_eq!(exec.instrument_name, deserialized.instrument_name);
}
#[test]
fn test_block_trade_trade_count() {
let block_trade = BlockTrade {
id: "6165".to_string(),
timestamp: 1590485535980,
trades: vec![
create_test_block_trade_execution(),
create_test_block_trade_execution(),
],
app_name: None,
broker_code: None,
broker_name: None,
};
assert_eq!(block_trade.trade_count(), 2);
}
#[test]
fn test_block_trade_is_broker_trade() {
let regular_trade = BlockTrade {
id: "6165".to_string(),
timestamp: 1590485535980,
trades: vec![],
app_name: None,
broker_code: None,
broker_name: None,
};
assert!(!regular_trade.is_broker_trade());
let broker_trade = BlockTrade {
id: "6165".to_string(),
timestamp: 1590485535980,
trades: vec![],
app_name: None,
broker_code: Some("BROKER123".to_string()),
broker_name: Some("Test Broker".to_string()),
};
assert!(broker_trade.is_broker_trade());
}
#[test]
fn test_block_trade_instruments() {
let mut exec1 = create_test_block_trade_execution();
exec1.instrument_name = "BTC-PERPETUAL".to_string();
let mut exec2 = create_test_block_trade_execution();
exec2.instrument_name = "BTC-28MAY20-9000-C".to_string();
let block_trade = BlockTrade {
id: "6165".to_string(),
timestamp: 1590485535980,
trades: vec![exec1, exec2],
app_name: None,
broker_code: None,
broker_name: None,
};
let instruments = block_trade.instruments();
assert_eq!(instruments.len(), 2);
assert!(instruments.contains(&"BTC-PERPETUAL"));
assert!(instruments.contains(&"BTC-28MAY20-9000-C"));
}
#[test]
fn test_block_trade_total_fees() {
let mut exec1 = create_test_block_trade_execution();
exec1.fee = 0.001;
let mut exec2 = create_test_block_trade_execution();
exec2.fee = 0.002;
let block_trade = BlockTrade {
id: "6165".to_string(),
timestamp: 1590485535980,
trades: vec![exec1, exec2],
app_name: None,
broker_code: None,
broker_name: None,
};
assert!((block_trade.total_fees() - 0.003).abs() < f64::EPSILON);
}
#[test]
fn test_block_trade_serialization() {
let block_trade = BlockTrade {
id: "6165".to_string(),
timestamp: 1590485535980,
trades: vec![create_test_block_trade_execution()],
app_name: Some("TestApp".to_string()),
broker_code: None,
broker_name: None,
};
let json = serde_json::to_string(&block_trade).unwrap();
let deserialized: BlockTrade = serde_json::from_str(&json).unwrap();
assert_eq!(block_trade.id, deserialized.id);
assert_eq!(block_trade.app_name, deserialized.app_name);
}
}