use crate::model::types::Direction;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum BlockRfqState {
#[default]
Open,
Filled,
Traded,
Cancelled,
Expired,
Closed,
Created,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum BlockRfqRole {
#[default]
Taker,
Maker,
Any,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum QuoteState {
#[default]
Open,
Filled,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionInstruction {
AllOrNone,
#[default]
AnyPartOf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum BlockRfqTimeInForce {
#[default]
FillOrKill,
GoodTilCancelled,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqLeg {
pub instrument_name: String,
pub direction: Direction,
#[serde(default)]
pub ratio: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
#[serde(default)]
pub price: Option<f64>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqHedge {
pub instrument_name: String,
pub direction: Direction,
pub price: f64,
pub amount: f64,
}
#[skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockRfqBidAsk {
#[serde(default)]
pub maker: Option<String>,
pub price: f64,
#[serde(default)]
pub last_update_timestamp: Option<i64>,
#[serde(default)]
pub execution_instruction: Option<ExecutionInstruction>,
#[serde(default)]
pub amount: Option<f64>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockRfqTradeAllocation {
#[serde(default)]
pub user_id: Option<i64>,
#[serde(default)]
pub client_info: Option<BlockRfqClientInfo>,
pub amount: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockRfqClientInfo {
pub client_id: String,
#[serde(default)]
pub user_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IndexPrices {
#[serde(default)]
pub btc_usd: Option<f64>,
#[serde(default)]
pub btc_usdc: Option<f64>,
#[serde(default)]
pub eth_usd: Option<f64>,
#[serde(default)]
pub eth_usdc: Option<f64>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfq {
pub block_rfq_id: i64,
pub state: BlockRfqState,
pub role: BlockRfqRole,
pub amount: f64,
#[serde(default)]
pub min_trade_amount: Option<f64>,
#[serde(default)]
pub combo_id: Option<String>,
pub legs: Vec<BlockRfqLeg>,
#[serde(default)]
pub hedge: Option<BlockRfqHedge>,
pub creation_timestamp: i64,
pub expiration_timestamp: i64,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub makers: Option<Vec<String>>,
#[serde(default)]
pub taker_rating: Option<String>,
#[serde(default)]
pub bids: Option<Vec<BlockRfqBidAsk>>,
#[serde(default)]
pub asks: Option<Vec<BlockRfqBidAsk>>,
#[serde(default)]
pub mark_price: Option<f64>,
#[serde(default)]
pub trades: Option<Vec<BlockRfqTradeInfo>>,
}
impl BlockRfq {
#[must_use]
pub fn is_open(&self) -> bool {
self.state == BlockRfqState::Open
}
#[must_use]
pub fn is_filled(&self) -> bool {
self.state == BlockRfqState::Filled
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.state == BlockRfqState::Cancelled
}
#[must_use]
pub fn is_taker(&self) -> bool {
self.role == BlockRfqRole::Taker
}
#[must_use]
pub fn is_maker(&self) -> bool {
self.role == BlockRfqRole::Maker
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqTradeInfo {
pub price: f64,
pub amount: f64,
pub direction: Direction,
#[serde(default)]
pub hedge_amount: Option<f64>,
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqQuote {
pub block_rfq_quote_id: i64,
pub block_rfq_id: i64,
pub quote_state: QuoteState,
pub price: f64,
pub amount: f64,
pub direction: Direction,
#[serde(default)]
pub filled_amount: Option<f64>,
pub legs: Vec<BlockRfqLeg>,
#[serde(default)]
pub hedge: Option<BlockRfqHedge>,
#[serde(default)]
pub execution_instruction: Option<ExecutionInstruction>,
pub creation_timestamp: i64,
pub last_update_timestamp: i64,
#[serde(default)]
pub replaced: Option<bool>,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub app_name: Option<String>,
#[serde(default)]
pub cancel_reason: Option<String>,
}
impl BlockRfqQuote {
#[must_use]
pub fn is_open(&self) -> bool {
self.quote_state == QuoteState::Open
}
#[must_use]
pub fn is_filled(&self) -> bool {
self.quote_state == QuoteState::Filled
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.quote_state == QuoteState::Cancelled
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqPublicTrade {
pub id: i64,
pub timestamp: i64,
#[serde(default)]
pub combo_id: Option<String>,
pub legs: Vec<BlockRfqLeg>,
pub amount: f64,
pub direction: Direction,
#[serde(default)]
pub mark_price: Option<f64>,
#[serde(default)]
pub trades: Option<Vec<BlockRfqTradeInfo>>,
#[serde(default)]
pub hedge: Option<BlockRfqHedge>,
#[serde(default)]
pub index_prices: Option<IndexPrices>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqTradesResponse {
#[serde(default)]
pub continuation: Option<String>,
pub block_rfqs: Vec<BlockRfqPublicTrade>,
}
impl BlockRfqTradesResponse {
#[must_use]
pub fn is_empty(&self) -> bool {
self.block_rfqs.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.block_rfqs.len()
}
#[must_use]
pub fn has_more(&self) -> bool {
self.continuation.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqsResponse {
#[serde(default)]
pub continuation: Option<String>,
pub block_rfqs: Vec<BlockRfq>,
}
impl BlockRfqsResponse {
#[must_use]
pub fn is_empty(&self) -> bool {
self.block_rfqs.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.block_rfqs.len()
}
#[must_use]
pub fn has_more(&self) -> bool {
self.continuation.is_some()
}
}
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqAcceptTrade {
pub trade_id: String,
#[serde(default)]
pub trade_seq: Option<i64>,
pub instrument_name: String,
pub timestamp: i64,
pub state: String,
#[serde(default)]
pub fee: Option<f64>,
#[serde(default)]
pub fee_currency: Option<String>,
pub amount: f64,
pub direction: Direction,
pub price: f64,
#[serde(default)]
pub index_price: Option<f64>,
#[serde(default)]
pub mark_price: Option<f64>,
#[serde(default)]
pub profit_loss: Option<f64>,
#[serde(default)]
pub order_id: Option<String>,
#[serde(default)]
pub order_type: Option<String>,
#[serde(default)]
pub tick_direction: Option<i32>,
#[serde(default)]
pub combo_id: Option<String>,
#[serde(default)]
pub block_rfq_id: Option<i64>,
#[serde(default)]
pub block_trade_id: Option<String>,
#[serde(default)]
pub block_trade_leg_count: Option<i32>,
#[serde(default)]
pub api: Option<bool>,
#[serde(default)]
pub contracts: Option<f64>,
#[serde(default)]
pub post_only: Option<bool>,
#[serde(default)]
pub mmp: Option<bool>,
#[serde(default)]
pub risk_reducing: Option<bool>,
#[serde(default)]
pub reduce_only: Option<bool>,
#[serde(default)]
pub self_trade: Option<bool>,
#[serde(default)]
pub liquidity: Option<String>,
#[serde(default)]
pub matching_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockRfqAcceptBlockTrade {
pub id: String,
pub timestamp: i64,
pub trades: Vec<BlockRfqAcceptTrade>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcceptBlockRfqResponse {
pub block_trades: Vec<BlockRfqAcceptBlockTrade>,
}
impl AcceptBlockRfqResponse {
#[must_use]
pub fn total_trades(&self) -> usize {
self.block_trades.iter().map(|bt| bt.trades.len()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_block_rfq_state_deserialization() {
let json = r#""open""#;
let state: BlockRfqState = serde_json::from_str(json).unwrap();
assert_eq!(state, BlockRfqState::Open);
let json = r#""cancelled""#;
let state: BlockRfqState = serde_json::from_str(json).unwrap();
assert_eq!(state, BlockRfqState::Cancelled);
}
#[test]
fn test_block_rfq_role_deserialization() {
let json = r#""taker""#;
let role: BlockRfqRole = serde_json::from_str(json).unwrap();
assert_eq!(role, BlockRfqRole::Taker);
let json = r#""maker""#;
let role: BlockRfqRole = serde_json::from_str(json).unwrap();
assert_eq!(role, BlockRfqRole::Maker);
}
#[test]
fn test_block_rfq_leg_deserialization() {
let json = r#"{
"instrument_name": "BTC-PERPETUAL",
"direction": "buy",
"ratio": 1,
"price": 70000
}"#;
let leg: BlockRfqLeg = serde_json::from_str(json).unwrap();
assert_eq!(leg.instrument_name, "BTC-PERPETUAL");
assert!(matches!(leg.direction, Direction::Buy));
assert_eq!(leg.ratio, Some(1.0));
assert_eq!(leg.price, Some(70000.0));
}
#[test]
fn test_block_rfq_deserialization() {
let json = r#"{
"block_rfq_id": 507,
"state": "created",
"role": "taker",
"amount": 20000,
"combo_id": "BTC-15NOV24",
"legs": [
{
"direction": "sell",
"instrument_name": "BTC-15NOV24",
"ratio": 1
}
],
"creation_timestamp": 1731062187555,
"expiration_timestamp": 1731062487555,
"bids": [],
"asks": [],
"makers": ["MAKER1"]
}"#;
let rfq: BlockRfq = serde_json::from_str(json).unwrap();
assert_eq!(rfq.block_rfq_id, 507);
assert_eq!(rfq.state, BlockRfqState::Created);
assert!(rfq.is_taker());
assert_eq!(rfq.amount, 20000.0);
assert_eq!(rfq.legs.len(), 1);
}
#[test]
fn test_block_rfq_quote_deserialization() {
let json = r#"{
"block_rfq_quote_id": 8,
"block_rfq_id": 3,
"quote_state": "open",
"price": 69600,
"amount": 10000,
"direction": "buy",
"legs": [
{
"direction": "buy",
"price": 69600,
"instrument_name": "BTC-15NOV24",
"ratio": 1
}
],
"creation_timestamp": 1731076586371,
"last_update_timestamp": 1731076586371,
"replaced": false,
"filled_amount": 0,
"execution_instruction": "all_or_none"
}"#;
let quote: BlockRfqQuote = serde_json::from_str(json).unwrap();
assert_eq!(quote.block_rfq_quote_id, 8);
assert!(quote.is_open());
assert_eq!(
quote.execution_instruction,
Some(ExecutionInstruction::AllOrNone)
);
}
#[test]
fn test_block_rfq_trades_response_deserialization() {
let json = r#"{
"continuation": "1739739009234:6570",
"block_rfqs": [
{
"id": 6611,
"timestamp": 1739803305362,
"combo_id": "BTC-CS-28FEB25-100000_106000",
"legs": [
{
"price": 0.1,
"direction": "buy",
"instrument_name": "BTC-28FEB25-100000-C",
"ratio": 1
}
],
"amount": 12.5,
"direction": "sell",
"mark_price": 0.010356754
}
]
}"#;
let response: BlockRfqTradesResponse = serde_json::from_str(json).unwrap();
assert!(response.has_more());
assert_eq!(response.len(), 1);
assert_eq!(response.block_rfqs[0].id, 6611);
}
}