use crate::error::Result as DeribitFixResult;
use crate::message::builder::MessageBuilder;
use crate::message::orders::OrderSide;
use crate::model::types::MsgType;
use chrono::Utc;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum QuoteCancelType {
Quit,
CancelForSymbol,
CancelForSecurityType,
CancelForUnderlyingSymbol,
CancelAll,
}
impl From<QuoteCancelType> for i32 {
fn from(cancel_type: QuoteCancelType) -> Self {
match cancel_type {
QuoteCancelType::Quit => 1,
QuoteCancelType::CancelForSymbol => 2,
QuoteCancelType::CancelForSecurityType => 3,
QuoteCancelType::CancelForUnderlyingSymbol => 4,
QuoteCancelType::CancelAll => 5,
}
}
}
impl TryFrom<i32> for QuoteCancelType {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
1 => Ok(QuoteCancelType::Quit),
2 => Ok(QuoteCancelType::CancelForSymbol),
3 => Ok(QuoteCancelType::CancelForSecurityType),
4 => Ok(QuoteCancelType::CancelForUnderlyingSymbol),
5 => Ok(QuoteCancelType::CancelAll),
_ => Err(format!("Invalid QuoteCancelType: {}", value)),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct QuoteCancelEntry {
pub quote_entry_id: String,
pub symbol: String,
pub side: Option<OrderSide>,
pub quote_entry_reject_reason: Option<i32>,
}
impl QuoteCancelEntry {
pub fn new(quote_entry_id: String, symbol: String) -> Self {
Self {
quote_entry_id,
symbol,
side: None,
quote_entry_reject_reason: None,
}
}
pub fn with_side(quote_entry_id: String, symbol: String, side: OrderSide) -> Self {
Self {
quote_entry_id,
symbol,
side: Some(side),
quote_entry_reject_reason: None,
}
}
pub fn set_side(mut self, side: OrderSide) -> Self {
self.side = Some(side);
self
}
pub fn set_reject_reason(mut self, reason: i32) -> Self {
self.quote_entry_reject_reason = Some(reason);
self
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct QuoteCancel {
pub quote_req_id: Option<String>,
pub quote_id: String,
pub quote_cancel_type: QuoteCancelType,
pub quote_resp_level: Option<i32>,
pub parties: Option<String>,
pub account: Option<String>,
pub acc_id_source: Option<String>,
pub account_type: Option<i32>,
pub quote_set_id: Option<String>,
pub underlying_symbol: Option<String>,
pub tot_quote_entries: Option<i32>,
pub quote_cancel_entries: Vec<QuoteCancelEntry>,
pub trading_session_id: Option<String>,
pub trading_session_sub_id: Option<String>,
pub text: Option<String>,
pub deribit_label: Option<String>,
pub use_standard_repeating_groups: bool,
}
impl QuoteCancel {
pub fn new(quote_id: String, quote_cancel_type: QuoteCancelType) -> Self {
Self {
quote_req_id: None,
quote_id,
quote_cancel_type,
quote_resp_level: None,
parties: None,
account: None,
acc_id_source: None,
account_type: None,
quote_set_id: None,
underlying_symbol: None,
tot_quote_entries: None,
quote_cancel_entries: Vec::new(),
trading_session_id: None,
trading_session_sub_id: None,
text: None,
deribit_label: None,
use_standard_repeating_groups: false, }
}
pub fn cancel_all(quote_id: String) -> Self {
Self::new(quote_id, QuoteCancelType::CancelAll)
}
pub fn cancel_for_symbol(quote_id: String, symbol: String) -> Self {
let mut cancel = Self::new(quote_id, QuoteCancelType::CancelForSymbol);
cancel
.quote_cancel_entries
.push(QuoteCancelEntry::new("".to_string(), symbol));
cancel.tot_quote_entries = Some(1);
cancel
}
pub fn with_entries(
quote_id: String,
quote_cancel_type: QuoteCancelType,
entries: Vec<QuoteCancelEntry>,
) -> Self {
let tot_quote_entries = entries.len() as i32;
Self {
quote_req_id: None,
quote_id,
quote_cancel_type,
quote_resp_level: None,
parties: None,
account: None,
acc_id_source: None,
account_type: None,
quote_set_id: None,
underlying_symbol: None,
tot_quote_entries: Some(tot_quote_entries),
quote_cancel_entries: entries,
trading_session_id: None,
trading_session_sub_id: None,
text: None,
deribit_label: None,
use_standard_repeating_groups: false, }
}
pub fn add_quote_cancel_entry(mut self, entry: QuoteCancelEntry) -> Self {
self.quote_cancel_entries.push(entry);
self.tot_quote_entries = Some(self.quote_cancel_entries.len() as i32);
self
}
pub fn with_quote_req_id(mut self, quote_req_id: String) -> Self {
self.quote_req_id = Some(quote_req_id);
self
}
pub fn with_quote_resp_level(mut self, quote_resp_level: i32) -> Self {
self.quote_resp_level = Some(quote_resp_level);
self
}
pub fn with_account(mut self, account: String) -> Self {
self.account = Some(account);
self
}
pub fn with_quote_set_id(mut self, quote_set_id: String) -> Self {
self.quote_set_id = Some(quote_set_id);
self
}
pub fn with_underlying_symbol(mut self, underlying_symbol: String) -> Self {
self.underlying_symbol = Some(underlying_symbol);
self
}
pub fn with_trading_session_id(mut self, trading_session_id: String) -> Self {
self.trading_session_id = Some(trading_session_id);
self
}
pub fn with_text(mut self, text: String) -> Self {
self.text = Some(text);
self
}
pub fn with_label(mut self, label: String) -> Self {
self.deribit_label = Some(label);
self
}
pub fn enable_standard_repeating_groups(mut self) -> Self {
self.use_standard_repeating_groups = true;
self
}
pub fn disable_standard_repeating_groups(mut self) -> Self {
self.use_standard_repeating_groups = false;
self
}
pub fn to_fix_message(
&self,
sender_comp_id: &str,
target_comp_id: &str,
msg_seq_num: u32,
) -> DeribitFixResult<String> {
let mut builder = MessageBuilder::new()
.msg_type(MsgType::QuoteCancel)
.sender_comp_id(sender_comp_id.to_string())
.target_comp_id(target_comp_id.to_string())
.msg_seq_num(msg_seq_num)
.sending_time(Utc::now());
builder = builder
.field(117, self.quote_id.clone()) .field(298, i32::from(self.quote_cancel_type).to_string());
if let Some(quote_req_id) = &self.quote_req_id {
builder = builder.field(131, quote_req_id.clone());
}
if let Some(quote_resp_level) = &self.quote_resp_level {
builder = builder.field(301, quote_resp_level.to_string());
}
if let Some(account) = &self.account {
builder = builder.field(1, account.clone());
}
if let Some(quote_set_id) = &self.quote_set_id {
builder = builder.field(302, quote_set_id.clone());
}
if let Some(underlying_symbol) = &self.underlying_symbol {
builder = builder.field(311, underlying_symbol.clone());
}
if let Some(tot_quote_entries) = &self.tot_quote_entries {
builder = builder.field(295, tot_quote_entries.to_string());
}
if let Some(trading_session_id) = &self.trading_session_id {
builder = builder.field(336, trading_session_id.clone());
}
if let Some(trading_session_sub_id) = &self.trading_session_sub_id {
builder = builder.field(625, trading_session_sub_id.clone());
}
if let Some(text) = &self.text {
builder = builder.field(58, text.clone());
}
if let Some(deribit_label) = &self.deribit_label {
builder = builder.field(100010, deribit_label.clone());
}
if self.use_standard_repeating_groups {
builder = builder.field(295, self.quote_cancel_entries.len().to_string());
for entry in &self.quote_cancel_entries {
builder = builder.field(299, entry.quote_entry_id.clone()); builder = builder.field(55, entry.symbol.clone());
if let Some(side) = &entry.side {
builder = builder.field(54, char::from(*side).to_string()); }
if let Some(quote_entry_reject_reason) = &entry.quote_entry_reject_reason {
builder = builder.field(368, quote_entry_reject_reason.to_string()); }
}
} else {
for (i, entry) in self.quote_cancel_entries.iter().enumerate() {
let base_tag = 4000 + (i * 100);
if !entry.quote_entry_id.is_empty() {
builder = builder.field(base_tag as u32, entry.quote_entry_id.clone()); }
builder = builder.field((base_tag + 1) as u32, entry.symbol.clone());
if let Some(side) = &entry.side {
builder = builder.field((base_tag + 2) as u32, char::from(*side).to_string());
}
if let Some(quote_entry_reject_reason) = &entry.quote_entry_reject_reason {
builder =
builder.field((base_tag + 3) as u32, quote_entry_reject_reason.to_string());
}
}
}
Ok(builder.build()?.to_string())
}
}
impl_json_display!(QuoteCancel, QuoteCancelEntry);
impl_json_debug_pretty!(QuoteCancel, QuoteCancelEntry);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quote_cancel_entry_creation() {
let entry = QuoteCancelEntry::new("QCE123".to_string(), "BTC-PERPETUAL".to_string());
assert_eq!(entry.quote_entry_id, "QCE123");
assert_eq!(entry.symbol, "BTC-PERPETUAL");
assert!(entry.side.is_none());
assert!(entry.quote_entry_reject_reason.is_none());
}
#[test]
fn test_quote_cancel_entry_with_side() {
let entry = QuoteCancelEntry::with_side(
"QCE456".to_string(),
"ETH-PERPETUAL".to_string(),
OrderSide::Buy,
);
assert_eq!(entry.quote_entry_id, "QCE456");
assert_eq!(entry.symbol, "ETH-PERPETUAL");
assert_eq!(entry.side, Some(OrderSide::Buy));
}
#[test]
fn test_quote_cancel_creation() {
let cancel = QuoteCancel::new("QC123".to_string(), QuoteCancelType::CancelAll);
assert_eq!(cancel.quote_id, "QC123");
assert_eq!(cancel.quote_cancel_type, QuoteCancelType::CancelAll);
assert!(cancel.quote_cancel_entries.is_empty());
}
#[test]
fn test_quote_cancel_all() {
let cancel = QuoteCancel::cancel_all("QC456".to_string());
assert_eq!(cancel.quote_id, "QC456");
assert_eq!(cancel.quote_cancel_type, QuoteCancelType::CancelAll);
}
#[test]
fn test_quote_cancel_for_symbol() {
let cancel =
QuoteCancel::cancel_for_symbol("QC789".to_string(), "BTC-PERPETUAL".to_string());
assert_eq!(cancel.quote_cancel_type, QuoteCancelType::CancelForSymbol);
assert_eq!(cancel.tot_quote_entries, Some(1));
assert_eq!(cancel.quote_cancel_entries.len(), 1);
assert_eq!(cancel.quote_cancel_entries[0].symbol, "BTC-PERPETUAL");
}
#[test]
fn test_quote_cancel_with_entries() {
let entry1 = QuoteCancelEntry::new("QCE1".to_string(), "BTC-PERPETUAL".to_string());
let entry2 = QuoteCancelEntry::new("QCE2".to_string(), "ETH-PERPETUAL".to_string());
let cancel = QuoteCancel::with_entries(
"QC999".to_string(),
QuoteCancelType::CancelForSymbol,
vec![entry1, entry2],
);
assert_eq!(cancel.tot_quote_entries, Some(2));
assert_eq!(cancel.quote_cancel_entries.len(), 2);
}
#[test]
fn test_quote_cancel_add_entry() {
let entry = QuoteCancelEntry::new("QCE1".to_string(), "BTC-PERPETUAL".to_string());
let cancel = QuoteCancel::new("QC123".to_string(), QuoteCancelType::CancelForSymbol)
.add_quote_cancel_entry(entry);
assert_eq!(cancel.tot_quote_entries, Some(1));
assert_eq!(cancel.quote_cancel_entries.len(), 1);
}
#[test]
fn test_quote_cancel_with_options() {
let cancel = QuoteCancel::new("QC888".to_string(), QuoteCancelType::CancelAll)
.with_quote_req_id("QR123".to_string())
.with_quote_resp_level(1)
.with_account("ACC123".to_string())
.with_quote_set_id("QS456".to_string())
.with_underlying_symbol("BTC".to_string())
.with_trading_session_id("SESSION1".to_string())
.with_text("Cancel all quotes".to_string())
.with_label("test-cancel".to_string());
assert_eq!(cancel.quote_req_id, Some("QR123".to_string()));
assert_eq!(cancel.quote_resp_level, Some(1));
assert_eq!(cancel.account, Some("ACC123".to_string()));
assert_eq!(cancel.quote_set_id, Some("QS456".to_string()));
assert_eq!(cancel.underlying_symbol, Some("BTC".to_string()));
assert_eq!(cancel.trading_session_id, Some("SESSION1".to_string()));
assert_eq!(cancel.text, Some("Cancel all quotes".to_string()));
assert_eq!(cancel.deribit_label, Some("test-cancel".to_string()));
}
#[test]
fn test_quote_cancel_to_fix_message() {
let entry = QuoteCancelEntry::new("QCE1".to_string(), "BTC-PERPETUAL".to_string())
.set_side(OrderSide::Buy);
let cancel = QuoteCancel::with_entries(
"QC123".to_string(),
QuoteCancelType::CancelForSymbol,
vec![entry],
)
.with_label("test-label".to_string());
let fix_message = cancel.to_fix_message("SENDER", "TARGET", 1).unwrap();
assert!(fix_message.contains("35=Z")); assert!(fix_message.contains("117=QC123")); assert!(fix_message.contains("298=2")); assert!(fix_message.contains("295=1")); assert!(fix_message.contains("100010=test-label"));
assert!(fix_message.contains("4000=QCE1")); assert!(fix_message.contains("4001=BTC-PERPETUAL")); assert!(fix_message.contains("4002=1")); }
#[test]
fn test_quote_cancel_all_to_fix_message() {
let cancel = QuoteCancel::cancel_all("QC456".to_string())
.with_text("Cancel all active quotes".to_string());
let fix_message = cancel.to_fix_message("SENDER", "TARGET", 2).unwrap();
assert!(fix_message.contains("35=Z")); assert!(fix_message.contains("117=QC456")); assert!(fix_message.contains("298=5")); assert!(fix_message.contains("58=Cancel all active quotes"));
assert!(!fix_message.contains("295=")); assert!(!fix_message.contains("4000=")); }
#[test]
fn test_quote_cancel_type_conversions() {
assert_eq!(i32::from(QuoteCancelType::Quit), 1);
assert_eq!(i32::from(QuoteCancelType::CancelForSymbol), 2);
assert_eq!(i32::from(QuoteCancelType::CancelForSecurityType), 3);
assert_eq!(i32::from(QuoteCancelType::CancelForUnderlyingSymbol), 4);
assert_eq!(i32::from(QuoteCancelType::CancelAll), 5);
assert_eq!(QuoteCancelType::try_from(1).unwrap(), QuoteCancelType::Quit);
assert_eq!(
QuoteCancelType::try_from(2).unwrap(),
QuoteCancelType::CancelForSymbol
);
assert_eq!(
QuoteCancelType::try_from(3).unwrap(),
QuoteCancelType::CancelForSecurityType
);
assert_eq!(
QuoteCancelType::try_from(4).unwrap(),
QuoteCancelType::CancelForUnderlyingSymbol
);
assert_eq!(
QuoteCancelType::try_from(5).unwrap(),
QuoteCancelType::CancelAll
);
assert!(QuoteCancelType::try_from(99).is_err());
}
#[test]
fn test_quote_cancel_standard_repeating_groups() {
let entries = vec![
QuoteCancelEntry::with_side(
"QCE123".to_string(),
"BTC-PERPETUAL".to_string(),
OrderSide::Buy,
),
QuoteCancelEntry::new("QCE456".to_string(), "ETH-PERPETUAL".to_string()),
];
let quote_cancel =
QuoteCancel::with_entries("QC789".to_string(), QuoteCancelType::CancelAll, entries)
.enable_standard_repeating_groups();
let fix_message = quote_cancel
.to_fix_message("SENDER", "TARGET", 123)
.unwrap();
assert!(fix_message.contains("295=2")); assert!(fix_message.contains("299=QCE456")); assert!(fix_message.contains("55=ETH-PERPETUAL"));
assert!(fix_message.contains("299=")); assert!(fix_message.contains("55="));
assert!(!fix_message.contains("4000="));
assert!(!fix_message.contains("4001="));
}
#[test]
fn test_quote_cancel_simplified_custom_tags() {
let entries = vec![
QuoteCancelEntry::with_side(
"QCE123".to_string(),
"BTC-PERPETUAL".to_string(),
OrderSide::Buy,
),
QuoteCancelEntry::new("QCE456".to_string(), "ETH-PERPETUAL".to_string()),
];
let quote_cancel =
QuoteCancel::with_entries("QC789".to_string(), QuoteCancelType::CancelAll, entries)
.disable_standard_repeating_groups();
let fix_message = quote_cancel
.to_fix_message("SENDER", "TARGET", 123)
.unwrap();
assert!(fix_message.contains("4000=QCE123")); assert!(fix_message.contains("4001=BTC-PERPETUAL")); assert!(fix_message.contains("4002=1")); assert!(fix_message.contains("4100=QCE456")); assert!(fix_message.contains("4101=ETH-PERPETUAL"));
assert!(!fix_message.contains("299=")); assert!(!fix_message.contains("55=BTC-PERPETUAL")); }
#[test]
fn test_quote_cancel_builder_methods() {
let quote_cancel = QuoteCancel::new("QC123".to_string(), QuoteCancelType::CancelAll);
assert!(!quote_cancel.use_standard_repeating_groups);
let enabled = quote_cancel.clone().enable_standard_repeating_groups();
assert!(enabled.use_standard_repeating_groups);
let disabled = enabled.disable_standard_repeating_groups();
assert!(!disabled.use_standard_repeating_groups);
}
}