use crate::error::Result as DeribitFixResult;
use crate::message::builder::MessageBuilder;
use crate::model::types::MsgType;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub use super::mm_protection_limits::{MMProtectionAction, MMProtectionScope};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MMProtectionResultStatus {
Accepted,
Rejected,
Completed,
PartiallyCompleted,
Pending,
}
impl From<MMProtectionResultStatus> for i32 {
fn from(status: MMProtectionResultStatus) -> Self {
match status {
MMProtectionResultStatus::Accepted => 0,
MMProtectionResultStatus::Rejected => 1,
MMProtectionResultStatus::Completed => 2,
MMProtectionResultStatus::PartiallyCompleted => 3,
MMProtectionResultStatus::Pending => 4,
}
}
}
impl TryFrom<i32> for MMProtectionResultStatus {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(MMProtectionResultStatus::Accepted),
1 => Ok(MMProtectionResultStatus::Rejected),
2 => Ok(MMProtectionResultStatus::Completed),
3 => Ok(MMProtectionResultStatus::PartiallyCompleted),
4 => Ok(MMProtectionResultStatus::Pending),
_ => Err(format!("Invalid MMProtectionResultStatus: {}", value)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MMProtectionRejectReason {
UnknownRequest,
InvalidAction,
InvalidScope,
UnknownSymbol,
InvalidLimits,
InsufficientPermissions,
SystemError,
RequestTimeout,
LimitsAlreadyExist,
LimitsNotFound,
Other,
}
impl From<MMProtectionRejectReason> for i32 {
fn from(reason: MMProtectionRejectReason) -> Self {
match reason {
MMProtectionRejectReason::UnknownRequest => 1,
MMProtectionRejectReason::InvalidAction => 2,
MMProtectionRejectReason::InvalidScope => 3,
MMProtectionRejectReason::UnknownSymbol => 4,
MMProtectionRejectReason::InvalidLimits => 5,
MMProtectionRejectReason::InsufficientPermissions => 6,
MMProtectionRejectReason::SystemError => 7,
MMProtectionRejectReason::RequestTimeout => 8,
MMProtectionRejectReason::LimitsAlreadyExist => 9,
MMProtectionRejectReason::LimitsNotFound => 10,
MMProtectionRejectReason::Other => 99,
}
}
}
impl TryFrom<i32> for MMProtectionRejectReason {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
1 => Ok(MMProtectionRejectReason::UnknownRequest),
2 => Ok(MMProtectionRejectReason::InvalidAction),
3 => Ok(MMProtectionRejectReason::InvalidScope),
4 => Ok(MMProtectionRejectReason::UnknownSymbol),
5 => Ok(MMProtectionRejectReason::InvalidLimits),
6 => Ok(MMProtectionRejectReason::InsufficientPermissions),
7 => Ok(MMProtectionRejectReason::SystemError),
8 => Ok(MMProtectionRejectReason::RequestTimeout),
9 => Ok(MMProtectionRejectReason::LimitsAlreadyExist),
10 => Ok(MMProtectionRejectReason::LimitsNotFound),
99 => Ok(MMProtectionRejectReason::Other),
_ => Err(format!("Invalid MMProtectionRejectReason: {}", value)),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct MMProtectionLimitsResult {
pub mm_protection_req_id: String,
pub mm_protection_action: MMProtectionAction,
pub mm_protection_scope: MMProtectionScope,
pub mm_protection_result_status: MMProtectionResultStatus,
pub mm_protection_reject_reason: Option<MMProtectionRejectReason>,
pub symbol: Option<String>,
pub underlying_symbol: Option<String>,
pub instrument_group: Option<String>,
pub current_max_position_limit: Option<f64>,
pub current_max_order_qty_limit: Option<f64>,
pub current_max_orders_limit: Option<i32>,
pub current_time_window_seconds: Option<i32>,
pub current_delta_limit: Option<f64>,
pub current_vega_limit: Option<f64>,
pub current_gamma_limit: Option<f64>,
pub current_theta_limit: Option<f64>,
pub current_total_risk_limit: Option<f64>,
pub current_valid_from: Option<DateTime<Utc>>,
pub current_valid_until: Option<DateTime<Utc>>,
pub processing_time: DateTime<Utc>,
pub affected_instruments_count: Option<i32>,
pub account: Option<String>,
pub parties: Option<String>,
pub text: Option<String>,
pub deribit_label: Option<String>,
}
impl MMProtectionLimitsResult {
pub fn new(
mm_protection_req_id: String,
mm_protection_action: MMProtectionAction,
mm_protection_scope: MMProtectionScope,
mm_protection_result_status: MMProtectionResultStatus,
) -> Self {
Self {
mm_protection_req_id,
mm_protection_action,
mm_protection_scope,
mm_protection_result_status,
mm_protection_reject_reason: None,
symbol: None,
underlying_symbol: None,
instrument_group: None,
current_max_position_limit: None,
current_max_order_qty_limit: None,
current_max_orders_limit: None,
current_time_window_seconds: None,
current_delta_limit: None,
current_vega_limit: None,
current_gamma_limit: None,
current_theta_limit: None,
current_total_risk_limit: None,
current_valid_from: None,
current_valid_until: None,
processing_time: Utc::now(),
affected_instruments_count: None,
account: None,
parties: None,
text: None,
deribit_label: None,
}
}
pub fn accepted(
mm_protection_req_id: String,
mm_protection_action: MMProtectionAction,
mm_protection_scope: MMProtectionScope,
) -> Self {
Self::new(
mm_protection_req_id,
mm_protection_action,
mm_protection_scope,
MMProtectionResultStatus::Accepted,
)
}
pub fn rejected(
mm_protection_req_id: String,
mm_protection_action: MMProtectionAction,
mm_protection_scope: MMProtectionScope,
reject_reason: MMProtectionRejectReason,
text: Option<String>,
) -> Self {
let mut result = Self::new(
mm_protection_req_id,
mm_protection_action,
mm_protection_scope,
MMProtectionResultStatus::Rejected,
);
result.mm_protection_reject_reason = Some(reject_reason);
result.text = text;
result
}
pub fn completed(
mm_protection_req_id: String,
mm_protection_action: MMProtectionAction,
mm_protection_scope: MMProtectionScope,
) -> Self {
Self::new(
mm_protection_req_id,
mm_protection_action,
mm_protection_scope,
MMProtectionResultStatus::Completed,
)
}
pub fn with_symbol(mut self, symbol: String) -> Self {
self.symbol = Some(symbol);
self
}
pub fn with_underlying_symbol(mut self, underlying_symbol: String) -> Self {
self.underlying_symbol = Some(underlying_symbol);
self
}
pub fn with_instrument_group(mut self, instrument_group: String) -> Self {
self.instrument_group = Some(instrument_group);
self
}
#[allow(clippy::too_many_arguments)]
pub fn with_current_limits(
mut self,
position_limit: Option<f64>,
order_qty_limit: Option<f64>,
max_orders_limit: Option<i32>,
time_window_seconds: Option<i32>,
total_risk_limit: Option<f64>,
) -> Self {
self.current_max_position_limit = position_limit;
self.current_max_order_qty_limit = order_qty_limit;
self.current_max_orders_limit = max_orders_limit;
self.current_time_window_seconds = time_window_seconds;
self.current_total_risk_limit = total_risk_limit;
self
}
pub fn with_current_greeks_limits(
mut self,
delta: Option<f64>,
vega: Option<f64>,
gamma: Option<f64>,
theta: Option<f64>,
) -> Self {
self.current_delta_limit = delta;
self.current_vega_limit = vega;
self.current_gamma_limit = gamma;
self.current_theta_limit = theta;
self
}
pub fn with_current_validity_period(
mut self,
from: DateTime<Utc>,
until: DateTime<Utc>,
) -> Self {
self.current_valid_from = Some(from);
self.current_valid_until = Some(until);
self
}
pub fn with_affected_instruments_count(mut self, count: i32) -> Self {
self.affected_instruments_count = Some(count);
self
}
pub fn with_account(mut self, account: String) -> Self {
self.account = Some(account);
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 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::MmProtectionLimitsResult)
.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(9001, self.mm_protection_req_id.clone()) .field(9002, i32::from(self.mm_protection_action).to_string()) .field(9003, i32::from(self.mm_protection_scope).to_string()) .field(
9017,
i32::from(self.mm_protection_result_status).to_string(),
) .field(
9018,
self.processing_time
.format("%Y%m%d-%H:%M:%S%.3f")
.to_string(),
);
if let Some(reject_reason) = &self.mm_protection_reject_reason {
builder = builder.field(9019, i32::from(*reject_reason).to_string());
}
if let Some(symbol) = &self.symbol {
builder = builder.field(55, symbol.clone());
}
if let Some(underlying_symbol) = &self.underlying_symbol {
builder = builder.field(311, underlying_symbol.clone());
}
if let Some(instrument_group) = &self.instrument_group {
builder = builder.field(9004, instrument_group.clone());
}
if let Some(current_max_position_limit) = &self.current_max_position_limit {
builder = builder.field(9020, current_max_position_limit.to_string());
}
if let Some(current_max_order_qty_limit) = &self.current_max_order_qty_limit {
builder = builder.field(9021, current_max_order_qty_limit.to_string());
}
if let Some(current_max_orders_limit) = &self.current_max_orders_limit {
builder = builder.field(9022, current_max_orders_limit.to_string());
}
if let Some(current_time_window_seconds) = &self.current_time_window_seconds {
builder = builder.field(9023, current_time_window_seconds.to_string());
}
if let Some(current_delta_limit) = &self.current_delta_limit {
builder = builder.field(9024, current_delta_limit.to_string());
}
if let Some(current_vega_limit) = &self.current_vega_limit {
builder = builder.field(9025, current_vega_limit.to_string());
}
if let Some(current_gamma_limit) = &self.current_gamma_limit {
builder = builder.field(9026, current_gamma_limit.to_string());
}
if let Some(current_theta_limit) = &self.current_theta_limit {
builder = builder.field(9027, current_theta_limit.to_string());
}
if let Some(current_total_risk_limit) = &self.current_total_risk_limit {
builder = builder.field(9028, current_total_risk_limit.to_string());
}
if let Some(current_valid_from) = &self.current_valid_from {
builder = builder.field(
9029,
current_valid_from.format("%Y%m%d-%H:%M:%S%.3f").to_string(),
);
}
if let Some(current_valid_until) = &self.current_valid_until {
builder = builder.field(
9030,
current_valid_until
.format("%Y%m%d-%H:%M:%S%.3f")
.to_string(),
);
}
if let Some(affected_instruments_count) = &self.affected_instruments_count {
builder = builder.field(9031, affected_instruments_count.to_string());
}
if let Some(account) = &self.account {
builder = builder.field(1, account.clone());
}
if let Some(parties) = &self.parties {
builder = builder.field(453, parties.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());
}
Ok(builder.build()?.to_string())
}
}
impl_json_display!(MMProtectionLimitsResult);
impl_json_debug_pretty!(MMProtectionLimitsResult);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mm_protection_limits_result_creation() {
let result = MMProtectionLimitsResult::new(
"MMP123".to_string(),
MMProtectionAction::SetLimits,
MMProtectionScope::AllInstruments,
MMProtectionResultStatus::Accepted,
);
assert_eq!(result.mm_protection_req_id, "MMP123");
assert_eq!(result.mm_protection_action, MMProtectionAction::SetLimits);
assert_eq!(
result.mm_protection_scope,
MMProtectionScope::AllInstruments
);
assert_eq!(
result.mm_protection_result_status,
MMProtectionResultStatus::Accepted
);
assert!(result.mm_protection_reject_reason.is_none());
}
#[test]
fn test_mm_protection_limits_result_accepted() {
let result = MMProtectionLimitsResult::accepted(
"MMP456".to_string(),
MMProtectionAction::UpdateLimits,
MMProtectionScope::SpecificInstrument,
);
assert_eq!(
result.mm_protection_result_status,
MMProtectionResultStatus::Accepted
);
assert_eq!(
result.mm_protection_action,
MMProtectionAction::UpdateLimits
);
assert_eq!(
result.mm_protection_scope,
MMProtectionScope::SpecificInstrument
);
}
#[test]
fn test_mm_protection_limits_result_rejected() {
let result = MMProtectionLimitsResult::rejected(
"MMP789".to_string(),
MMProtectionAction::SetLimits,
MMProtectionScope::SpecificInstrument,
MMProtectionRejectReason::UnknownSymbol,
Some("Symbol not found".to_string()),
);
assert_eq!(
result.mm_protection_result_status,
MMProtectionResultStatus::Rejected
);
assert_eq!(
result.mm_protection_reject_reason,
Some(MMProtectionRejectReason::UnknownSymbol)
);
assert_eq!(result.text, Some("Symbol not found".to_string()));
}
#[test]
fn test_mm_protection_limits_result_completed() {
let result = MMProtectionLimitsResult::completed(
"MMP999".to_string(),
MMProtectionAction::QueryLimits,
MMProtectionScope::AllInstruments,
);
assert_eq!(
result.mm_protection_result_status,
MMProtectionResultStatus::Completed
);
assert_eq!(result.mm_protection_action, MMProtectionAction::QueryLimits);
}
#[test]
fn test_mm_protection_limits_result_with_options() {
let valid_from = Utc::now();
let valid_until = valid_from + chrono::Duration::hours(24);
let result = MMProtectionLimitsResult::completed(
"MMP111".to_string(),
MMProtectionAction::SetLimits,
MMProtectionScope::SpecificInstrument,
)
.with_symbol("BTC-PERPETUAL".to_string())
.with_current_limits(
Some(1000.0),
Some(100.0),
Some(50),
Some(300),
Some(10000.0),
)
.with_current_greeks_limits(Some(10.0), Some(5.0), Some(2.0), Some(-1.0))
.with_current_validity_period(valid_from, valid_until)
.with_affected_instruments_count(1)
.with_account("ACC123".to_string())
.with_text("Limits updated successfully".to_string())
.with_label("test-result".to_string());
assert_eq!(result.symbol, Some("BTC-PERPETUAL".to_string()));
assert_eq!(result.current_max_position_limit, Some(1000.0));
assert_eq!(result.current_max_order_qty_limit, Some(100.0));
assert_eq!(result.current_max_orders_limit, Some(50));
assert_eq!(result.current_time_window_seconds, Some(300));
assert_eq!(result.current_delta_limit, Some(10.0));
assert_eq!(result.current_vega_limit, Some(5.0));
assert_eq!(result.current_gamma_limit, Some(2.0));
assert_eq!(result.current_theta_limit, Some(-1.0));
assert_eq!(result.current_total_risk_limit, Some(10000.0));
assert_eq!(result.current_valid_from, Some(valid_from));
assert_eq!(result.current_valid_until, Some(valid_until));
assert_eq!(result.affected_instruments_count, Some(1));
assert_eq!(result.account, Some("ACC123".to_string()));
assert_eq!(result.text, Some("Limits updated successfully".to_string()));
assert_eq!(result.deribit_label, Some("test-result".to_string()));
}
#[test]
fn test_mm_protection_limits_result_to_fix_message() {
let result = MMProtectionLimitsResult::accepted(
"MMP123".to_string(),
MMProtectionAction::SetLimits,
MMProtectionScope::SpecificInstrument,
)
.with_symbol("BTC-PERPETUAL".to_string())
.with_current_limits(Some(500.0), Some(50.0), None, None, None)
.with_label("test-label".to_string());
let fix_message = result.to_fix_message("SENDER", "TARGET", 1).unwrap();
assert!(fix_message.contains("35=MR")); assert!(fix_message.contains("9001=MMP123")); assert!(fix_message.contains("9002=1")); assert!(fix_message.contains("9003=2")); assert!(fix_message.contains("9017=0")); assert!(fix_message.contains("55=BTC-PERPETUAL")); assert!(fix_message.contains("9020=500")); assert!(fix_message.contains("9021=50")); assert!(fix_message.contains("100010=test-label")); }
#[test]
fn test_mm_protection_result_status_conversions() {
assert_eq!(i32::from(MMProtectionResultStatus::Accepted), 0);
assert_eq!(i32::from(MMProtectionResultStatus::Rejected), 1);
assert_eq!(i32::from(MMProtectionResultStatus::Completed), 2);
assert_eq!(i32::from(MMProtectionResultStatus::PartiallyCompleted), 3);
assert_eq!(i32::from(MMProtectionResultStatus::Pending), 4);
assert_eq!(
MMProtectionResultStatus::try_from(0).unwrap(),
MMProtectionResultStatus::Accepted
);
assert_eq!(
MMProtectionResultStatus::try_from(1).unwrap(),
MMProtectionResultStatus::Rejected
);
assert_eq!(
MMProtectionResultStatus::try_from(2).unwrap(),
MMProtectionResultStatus::Completed
);
assert_eq!(
MMProtectionResultStatus::try_from(3).unwrap(),
MMProtectionResultStatus::PartiallyCompleted
);
assert_eq!(
MMProtectionResultStatus::try_from(4).unwrap(),
MMProtectionResultStatus::Pending
);
assert!(MMProtectionResultStatus::try_from(99).is_err());
}
#[test]
fn test_mm_protection_reject_reason_conversions() {
assert_eq!(i32::from(MMProtectionRejectReason::UnknownRequest), 1);
assert_eq!(i32::from(MMProtectionRejectReason::InvalidAction), 2);
assert_eq!(i32::from(MMProtectionRejectReason::UnknownSymbol), 4);
assert_eq!(i32::from(MMProtectionRejectReason::Other), 99);
assert_eq!(
MMProtectionRejectReason::try_from(1).unwrap(),
MMProtectionRejectReason::UnknownRequest
);
assert_eq!(
MMProtectionRejectReason::try_from(2).unwrap(),
MMProtectionRejectReason::InvalidAction
);
assert_eq!(
MMProtectionRejectReason::try_from(4).unwrap(),
MMProtectionRejectReason::UnknownSymbol
);
assert_eq!(
MMProtectionRejectReason::try_from(99).unwrap(),
MMProtectionRejectReason::Other
);
assert!(MMProtectionRejectReason::try_from(50).is_err());
}
}