Skip to main content

deribit_fix/message/user/
user_response.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 12/8/25
5******************************************************************************/
6
7//! User Response FIX Message Implementation
8
9use crate::error::Result as DeribitFixResult;
10use crate::message::builder::MessageBuilder;
11use crate::model::types::MsgType;
12use base64::{Engine as _, engine::general_purpose};
13use chrono::Utc;
14use serde::{Deserialize, Serialize};
15
16// Re-export UserStatus from user_request module
17pub use super::user_request::UserStatus;
18
19/// User Response message (MsgType = 'BF')
20#[derive(Clone, PartialEq, Serialize, Deserialize)]
21pub struct UserResponse {
22    /// User request ID
23    pub user_request_id: String,
24    /// Username
25    pub username: String,
26    /// User status
27    pub user_status: UserStatus,
28    /// User status text
29    pub user_status_text: Option<String>,
30    /// Raw data length
31    pub raw_data_length: Option<i32>,
32    /// Raw data
33    pub raw_data: Option<Vec<u8>>,
34    /// Custom label
35    pub deribit_label: Option<String>,
36    /// User equity (tag 100001)
37    pub user_equity: Option<f64>,
38    /// User balance (tag 100002)
39    pub user_balance: Option<f64>,
40    /// User initial margin (tag 100003)
41    pub user_initial_margin: Option<f64>,
42    /// User maintenance margin (tag 100004)
43    pub user_maintenance_margin: Option<f64>,
44    /// Unrealized P/L (tag 100005)
45    pub unrealized_pl: Option<f64>,
46    /// Realized P/L (tag 100006)
47    pub realized_pl: Option<f64>,
48    /// Total P/L (tag 100011)
49    pub total_pl: Option<f64>,
50    /// Margin balance for cross collateral (tag 100013)
51    pub margin_balance: Option<f64>,
52}
53
54impl UserResponse {
55    /// Create a new user response
56    pub fn new(user_request_id: String, username: String, user_status: UserStatus) -> Self {
57        Self {
58            user_request_id,
59            username,
60            user_status,
61            user_status_text: None,
62            raw_data_length: None,
63            raw_data: None,
64            deribit_label: None,
65            user_equity: None,
66            user_balance: None,
67            user_initial_margin: None,
68            user_maintenance_margin: None,
69            unrealized_pl: None,
70            realized_pl: None,
71            total_pl: None,
72            margin_balance: None,
73        }
74    }
75
76    /// Create a successful login response
77    pub fn logged_in(user_request_id: String, username: String) -> Self {
78        let mut response = Self::new(user_request_id, username, UserStatus::LoggedIn);
79        response.user_status_text = Some("User logged in successfully".to_string());
80        response
81    }
82
83    /// Create a successful logout response
84    pub fn logged_out(user_request_id: String, username: String) -> Self {
85        let mut response = Self::new(user_request_id, username, UserStatus::NotLoggedIn);
86        response.user_status_text = Some("User logged out successfully".to_string());
87        response
88    }
89
90    /// Create a password changed response
91    pub fn password_changed(user_request_id: String, username: String) -> Self {
92        let mut response = Self::new(user_request_id, username, UserStatus::PasswordChanged);
93        response.user_status_text = Some("Password changed successfully".to_string());
94        response
95    }
96
97    /// Create an error response for unrecognized user
98    pub fn user_not_recognised(user_request_id: String, username: String) -> Self {
99        let mut response = Self::new(user_request_id, username, UserStatus::UserNotRecognised);
100        response.user_status_text = Some("User not recognised".to_string());
101        response
102    }
103
104    /// Create an error response for incorrect password
105    pub fn password_incorrect(user_request_id: String, username: String) -> Self {
106        let mut response = Self::new(user_request_id, username, UserStatus::PasswordIncorrect);
107        response.user_status_text = Some("Password incorrect".to_string());
108        response
109    }
110
111    /// Create a generic error response
112    pub fn error(user_request_id: String, username: String, error_text: String) -> Self {
113        let mut response = Self::new(user_request_id, username, UserStatus::Other);
114        response.user_status_text = Some(error_text);
115        response
116    }
117
118    /// Set user status text
119    pub fn with_user_status_text(mut self, user_status_text: String) -> Self {
120        self.user_status_text = Some(user_status_text);
121        self
122    }
123
124    /// Set raw data
125    pub fn with_raw_data(mut self, raw_data: Vec<u8>) -> Self {
126        self.raw_data_length = Some(raw_data.len() as i32);
127        self.raw_data = Some(raw_data);
128        self
129    }
130
131    /// Set custom label
132    pub fn with_label(mut self, label: String) -> Self {
133        self.deribit_label = Some(label);
134        self
135    }
136
137    /// Set user equity (tag 100001)
138    #[must_use]
139    pub fn with_user_equity(mut self, equity: f64) -> Self {
140        self.user_equity = Some(equity);
141        self
142    }
143
144    /// Set user balance (tag 100002)
145    #[must_use]
146    pub fn with_user_balance(mut self, balance: f64) -> Self {
147        self.user_balance = Some(balance);
148        self
149    }
150
151    /// Set user initial margin (tag 100003)
152    #[must_use]
153    pub fn with_user_initial_margin(mut self, initial_margin: f64) -> Self {
154        self.user_initial_margin = Some(initial_margin);
155        self
156    }
157
158    /// Set user maintenance margin (tag 100004)
159    #[must_use]
160    pub fn with_user_maintenance_margin(mut self, maintenance_margin: f64) -> Self {
161        self.user_maintenance_margin = Some(maintenance_margin);
162        self
163    }
164
165    /// Set unrealized P/L (tag 100005)
166    #[must_use]
167    pub fn with_unrealized_pl(mut self, unrealized_pl: f64) -> Self {
168        self.unrealized_pl = Some(unrealized_pl);
169        self
170    }
171
172    /// Set realized P/L (tag 100006)
173    #[must_use]
174    pub fn with_realized_pl(mut self, realized_pl: f64) -> Self {
175        self.realized_pl = Some(realized_pl);
176        self
177    }
178
179    /// Set total P/L (tag 100011)
180    #[must_use]
181    pub fn with_total_pl(mut self, total_pl: f64) -> Self {
182        self.total_pl = Some(total_pl);
183        self
184    }
185
186    /// Set margin balance for cross collateral (tag 100013)
187    #[must_use]
188    pub fn with_margin_balance(mut self, margin_balance: f64) -> Self {
189        self.margin_balance = Some(margin_balance);
190        self
191    }
192
193    /// Convert to FIX message
194    pub fn to_fix_message(
195        &self,
196        sender_comp_id: &str,
197        target_comp_id: &str,
198        msg_seq_num: u32,
199    ) -> DeribitFixResult<String> {
200        let mut builder = MessageBuilder::new()
201            .msg_type(MsgType::UserResponse)
202            .sender_comp_id(sender_comp_id.to_string())
203            .target_comp_id(target_comp_id.to_string())
204            .msg_seq_num(msg_seq_num)
205            .sending_time(Utc::now());
206
207        // Required fields
208        builder = builder
209            .field(923, self.user_request_id.clone()) // UserRequestID
210            .field(553, self.username.clone()) // Username
211            .field(926, i32::from(self.user_status).to_string()); // UserStatus
212
213        // Optional fields
214        if let Some(user_status_text) = &self.user_status_text {
215            builder = builder.field(927, user_status_text.clone());
216        }
217
218        if let Some(raw_data_length) = &self.raw_data_length {
219            builder = builder.field(95, raw_data_length.to_string());
220        }
221
222        if let Some(raw_data) = &self.raw_data {
223            // Convert raw data to base64 for FIX transmission
224            let encoded_data = general_purpose::STANDARD.encode(raw_data);
225            builder = builder.field(96, encoded_data);
226        }
227
228        if let Some(deribit_label) = &self.deribit_label {
229            builder = builder.field(100010, deribit_label.clone());
230        }
231
232        // Account info fields (Deribit custom tags)
233        if let Some(equity) = self.user_equity {
234            builder = builder.field(100001, equity.to_string());
235        }
236
237        if let Some(balance) = self.user_balance {
238            builder = builder.field(100002, balance.to_string());
239        }
240
241        if let Some(initial_margin) = self.user_initial_margin {
242            builder = builder.field(100003, initial_margin.to_string());
243        }
244
245        if let Some(maintenance_margin) = self.user_maintenance_margin {
246            builder = builder.field(100004, maintenance_margin.to_string());
247        }
248
249        if let Some(unrealized) = self.unrealized_pl {
250            builder = builder.field(100005, unrealized.to_string());
251        }
252
253        if let Some(realized) = self.realized_pl {
254            builder = builder.field(100006, realized.to_string());
255        }
256
257        if let Some(total) = self.total_pl {
258            builder = builder.field(100011, total.to_string());
259        }
260
261        if let Some(margin) = self.margin_balance {
262            builder = builder.field(100013, margin.to_string());
263        }
264
265        Ok(builder.build()?.to_string())
266    }
267}
268
269impl_json_display!(UserResponse);
270impl_json_debug_pretty!(UserResponse);
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_user_response_creation() {
278        let response = UserResponse::new(
279            "UR123".to_string(),
280            "testuser".to_string(),
281            UserStatus::LoggedIn,
282        );
283
284        assert_eq!(response.user_request_id, "UR123");
285        assert_eq!(response.username, "testuser");
286        assert_eq!(response.user_status, UserStatus::LoggedIn);
287        assert!(response.user_status_text.is_none());
288        assert!(response.raw_data.is_none());
289    }
290
291    #[test]
292    fn test_user_response_logged_in() {
293        let response = UserResponse::logged_in("UR456".to_string(), "user1".to_string());
294
295        assert_eq!(response.user_status, UserStatus::LoggedIn);
296        assert_eq!(response.username, "user1");
297        assert_eq!(
298            response.user_status_text,
299            Some("User logged in successfully".to_string())
300        );
301    }
302
303    #[test]
304    fn test_user_response_logged_out() {
305        let response = UserResponse::logged_out("UR789".to_string(), "user2".to_string());
306
307        assert_eq!(response.user_status, UserStatus::NotLoggedIn);
308        assert_eq!(response.username, "user2");
309        assert_eq!(
310            response.user_status_text,
311            Some("User logged out successfully".to_string())
312        );
313    }
314
315    #[test]
316    fn test_user_response_password_changed() {
317        let response = UserResponse::password_changed("UR999".to_string(), "user3".to_string());
318
319        assert_eq!(response.user_status, UserStatus::PasswordChanged);
320        assert_eq!(response.username, "user3");
321        assert_eq!(
322            response.user_status_text,
323            Some("Password changed successfully".to_string())
324        );
325    }
326
327    #[test]
328    fn test_user_response_user_not_recognised() {
329        let response =
330            UserResponse::user_not_recognised("UR111".to_string(), "unknown_user".to_string());
331
332        assert_eq!(response.user_status, UserStatus::UserNotRecognised);
333        assert_eq!(response.username, "unknown_user");
334        assert_eq!(
335            response.user_status_text,
336            Some("User not recognised".to_string())
337        );
338    }
339
340    #[test]
341    fn test_user_response_password_incorrect() {
342        let response = UserResponse::password_incorrect("UR222".to_string(), "user4".to_string());
343
344        assert_eq!(response.user_status, UserStatus::PasswordIncorrect);
345        assert_eq!(response.username, "user4");
346        assert_eq!(
347            response.user_status_text,
348            Some("Password incorrect".to_string())
349        );
350    }
351
352    #[test]
353    fn test_user_response_error() {
354        let response = UserResponse::error(
355            "UR333".to_string(),
356            "user5".to_string(),
357            "System temporarily unavailable".to_string(),
358        );
359
360        assert_eq!(response.user_status, UserStatus::Other);
361        assert_eq!(response.username, "user5");
362        assert_eq!(
363            response.user_status_text,
364            Some("System temporarily unavailable".to_string())
365        );
366    }
367
368    #[test]
369    fn test_user_response_with_options() {
370        let raw_data = vec![10, 20, 30, 40];
371        let response = UserResponse::new(
372            "UR444".to_string(),
373            "user6".to_string(),
374            UserStatus::LoggedIn,
375        )
376        .with_user_status_text("Custom login message".to_string())
377        .with_raw_data(raw_data.clone())
378        .with_label("test-user-response".to_string());
379
380        assert_eq!(
381            response.user_status_text,
382            Some("Custom login message".to_string())
383        );
384        assert_eq!(response.raw_data, Some(raw_data));
385        assert_eq!(response.raw_data_length, Some(4));
386        assert_eq!(
387            response.deribit_label,
388            Some("test-user-response".to_string())
389        );
390    }
391
392    #[test]
393    fn test_user_response_to_fix_message() {
394        let response = UserResponse::logged_in("UR123".to_string(), "testuser".to_string())
395            .with_label("test-label".to_string());
396
397        let fix_message = response.to_fix_message("SENDER", "TARGET", 1).unwrap();
398
399        // Check that the message contains required fields
400        assert!(fix_message.contains("35=BF")); // MsgType
401        assert!(fix_message.contains("923=UR123")); // UserRequestID
402        assert!(fix_message.contains("553=testuser")); // Username
403        assert!(fix_message.contains("926=1")); // UserStatus=LoggedIn
404        assert!(fix_message.contains("927=User logged in successfully")); // UserStatusText
405        assert!(fix_message.contains("100010=test-label")); // Custom label
406    }
407
408    #[test]
409    fn test_user_response_minimal_fix_message() {
410        let response = UserResponse::new(
411            "UR456".to_string(),
412            "user".to_string(),
413            UserStatus::NotLoggedIn,
414        );
415
416        let fix_message = response.to_fix_message("SENDER", "TARGET", 2).unwrap();
417
418        // Check required fields only
419        assert!(fix_message.contains("35=BF")); // MsgType
420        assert!(fix_message.contains("923=UR456")); // UserRequestID
421        assert!(fix_message.contains("553=user")); // Username
422        assert!(fix_message.contains("926=2")); // UserStatus=NotLoggedIn
423
424        // Check optional fields are not present when not set
425        // Use SOH character (\x01) to be more precise and avoid false matches
426        assert!(!fix_message.contains("\x01927=")); // UserStatusText field not set
427        assert!(!fix_message.contains("\x0195=")); // RawDataLength field not set
428        assert!(!fix_message.contains("\x0196=")); // RawData field not set
429    }
430
431    #[test]
432    fn test_user_response_with_raw_data() {
433        let raw_data = vec![0xFF, 0xFE, 0xFD];
434        let response = UserResponse::new(
435            "UR789".to_string(),
436            "datauser".to_string(),
437            UserStatus::LoggedIn,
438        )
439        .with_raw_data(raw_data.clone());
440
441        let fix_message = response.to_fix_message("SENDER", "TARGET", 3).unwrap();
442
443        // Check that raw data fields are present
444        assert!(fix_message.contains("95=3")); // RawDataLength
445        assert!(fix_message.contains("96=")); // RawData field should be present (base64 encoded)
446    }
447
448    #[test]
449    fn test_user_response_with_account_info() {
450        let response = UserResponse::new(
451            "UR_ACCT".to_string(),
452            "trader".to_string(),
453            UserStatus::LoggedIn,
454        )
455        .with_user_equity(10000.50)
456        .with_user_balance(9500.25)
457        .with_user_initial_margin(500.0)
458        .with_user_maintenance_margin(250.0)
459        .with_unrealized_pl(100.25)
460        .with_realized_pl(50.0)
461        .with_total_pl(150.25)
462        .with_margin_balance(8000.0);
463
464        assert_eq!(response.user_equity, Some(10000.50));
465        assert_eq!(response.user_balance, Some(9500.25));
466        assert_eq!(response.user_initial_margin, Some(500.0));
467        assert_eq!(response.user_maintenance_margin, Some(250.0));
468        assert_eq!(response.unrealized_pl, Some(100.25));
469        assert_eq!(response.realized_pl, Some(50.0));
470        assert_eq!(response.total_pl, Some(150.25));
471        assert_eq!(response.margin_balance, Some(8000.0));
472    }
473
474    #[test]
475    fn test_user_response_account_info_fix_message() {
476        let response = UserResponse::new(
477            "UR_FIX".to_string(),
478            "user".to_string(),
479            UserStatus::LoggedIn,
480        )
481        .with_user_equity(5000.0)
482        .with_user_balance(4500.0)
483        .with_user_initial_margin(300.0)
484        .with_user_maintenance_margin(150.0);
485
486        let fix_message = response.to_fix_message("SENDER", "TARGET", 1).unwrap();
487
488        // Check account info tags are present
489        assert!(fix_message.contains("100001=5000")); // DeribitUserEquity
490        assert!(fix_message.contains("100002=4500")); // DeribitUserBalance
491        assert!(fix_message.contains("100003=300")); // DeribitUserInitialMargin
492        assert!(fix_message.contains("100004=150")); // DeribitUserMaintenanceMargin
493    }
494}