1use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11
12#[derive(Debug, Clone, Serialize)]
17pub struct ChallengeRequest {
18 pub event: &'static str,
20 pub api_key: String,
22}
23
24impl ChallengeRequest {
25 pub fn new(api_key: impl Into<String>) -> Self {
27 Self {
28 event: "challenge",
29 api_key: api_key.into(),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize)]
36pub struct SubscribeRequest {
37 pub event: &'static str,
39 pub feed: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub product_ids: Option<Vec<String>>,
44}
45
46impl SubscribeRequest {
47 pub fn public(feed: impl Into<String>, product_ids: Vec<String>) -> Self {
49 Self {
50 event: "subscribe",
51 feed: feed.into(),
52 product_ids: if product_ids.is_empty() {
53 None
54 } else {
55 Some(product_ids)
56 },
57 }
58 }
59
60 pub fn all(feed: impl Into<String>) -> Self {
62 Self {
63 event: "subscribe",
64 feed: feed.into(),
65 product_ids: None,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize)]
72pub struct PrivateSubscribeRequest {
73 pub event: &'static str,
75 pub feed: String,
77 pub original_challenge: String,
79 pub signed_challenge: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub product_ids: Option<Vec<String>>,
84}
85
86impl PrivateSubscribeRequest {
87 pub fn new(
89 feed: impl Into<String>,
90 original_challenge: String,
91 signed_challenge: String,
92 ) -> Self {
93 Self {
94 event: "subscribe",
95 feed: feed.into(),
96 original_challenge,
97 signed_challenge,
98 product_ids: None,
99 }
100 }
101
102 pub fn with_product_ids(mut self, product_ids: Vec<String>) -> Self {
104 self.product_ids = Some(product_ids);
105 self
106 }
107}
108
109#[derive(Debug, Clone, Serialize)]
111pub struct UnsubscribeRequest {
112 pub event: &'static str,
114 pub feed: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub product_ids: Option<Vec<String>>,
119}
120
121impl UnsubscribeRequest {
122 pub fn new(feed: impl Into<String>, product_ids: Vec<String>) -> Self {
124 Self {
125 event: "unsubscribe",
126 feed: feed.into(),
127 product_ids: if product_ids.is_empty() {
128 None
129 } else {
130 Some(product_ids)
131 },
132 }
133 }
134}
135
136
137#[derive(Debug, Clone, Deserialize)]
142pub struct ChallengeResponse {
143 pub event: String,
145 pub message: String,
147}
148
149#[derive(Debug, Clone, Deserialize)]
151pub struct SubscribedResponse {
152 pub event: String,
154 pub feed: String,
156 #[serde(default)]
158 pub product_ids: Option<Vec<String>>,
159}
160
161#[derive(Debug, Clone, Deserialize)]
163pub struct UnsubscribedResponse {
164 pub event: String,
166 pub feed: String,
168 #[serde(default)]
170 pub product_ids: Option<Vec<String>>,
171}
172
173#[derive(Debug, Clone, Deserialize)]
175pub struct ErrorResponse {
176 pub event: String,
178 pub message: String,
180}
181
182#[derive(Debug, Clone, Deserialize)]
184pub struct InfoResponse {
185 pub event: String,
187 pub message: String,
189 #[serde(default)]
191 pub version: Option<String>,
192}
193
194
195#[derive(Debug, Clone, Deserialize)]
200pub struct BookMessage {
201 pub feed: String,
203 pub product_id: String,
205 #[serde(default)]
207 pub seq: Option<u64>,
208 #[serde(default)]
210 pub timestamp: Option<u64>,
211 #[serde(default)]
213 pub bids: Vec<BookLevel>,
214 #[serde(default)]
216 pub asks: Vec<BookLevel>,
217}
218
219#[derive(Debug, Clone, Deserialize)]
221pub struct BookSnapshotMessage {
222 pub feed: String,
224 pub product_id: String,
226 #[serde(default)]
228 pub seq: Option<u64>,
229 #[serde(default)]
231 pub timestamp: Option<u64>,
232 #[serde(default)]
234 pub bids: Vec<BookLevel>,
235 #[serde(default)]
237 pub asks: Vec<BookLevel>,
238}
239
240#[derive(Debug, Clone, Deserialize)]
242pub struct BookLevel {
243 pub price: Decimal,
245 pub qty: Decimal,
247}
248
249#[derive(Debug, Clone, Deserialize)]
251pub struct TickerMessage {
252 pub feed: String,
254 pub product_id: String,
256 #[serde(default)]
258 pub time: Option<u64>,
259 #[serde(default)]
261 pub bid: Option<Decimal>,
262 #[serde(default)]
264 pub bid_size: Option<Decimal>,
265 #[serde(default)]
267 pub ask: Option<Decimal>,
268 #[serde(default)]
270 pub ask_size: Option<Decimal>,
271 #[serde(default)]
273 pub last: Option<Decimal>,
274 #[serde(default)]
276 pub last_size: Option<Decimal>,
277 #[serde(default)]
279 pub volume: Option<Decimal>,
280 #[serde(default, rename = "markPrice")]
282 pub mark_price: Option<Decimal>,
283 #[serde(default, rename = "openInterest")]
285 pub open_interest: Option<Decimal>,
286 #[serde(default)]
288 pub funding_rate: Option<Decimal>,
289 #[serde(default)]
291 pub funding_rate_prediction: Option<Decimal>,
292 #[serde(default)]
294 pub change: Option<Decimal>,
295 #[serde(default)]
297 pub premium: Option<Decimal>,
298 #[serde(default)]
300 pub index: Option<Decimal>,
301 #[serde(default)]
303 pub post_only: Option<bool>,
304 #[serde(default)]
306 pub suspended: Option<bool>,
307}
308
309#[derive(Debug, Clone, Deserialize)]
311pub struct TradeMessage {
312 pub feed: String,
314 pub product_id: String,
316 #[serde(default)]
318 pub uid: Option<String>,
319 #[serde(default)]
321 pub side: Option<String>,
322 #[serde(rename = "type", default)]
324 pub trade_type: Option<String>,
325 #[serde(default)]
327 pub price: Option<Decimal>,
328 #[serde(default)]
330 pub qty: Option<Decimal>,
331 #[serde(default)]
333 pub time: Option<u64>,
334 #[serde(default)]
336 pub seq: Option<u64>,
337}
338
339#[derive(Debug, Clone, Deserialize)]
341pub struct TradesSnapshotMessage {
342 pub feed: String,
344 pub product_id: String,
346 pub trades: Vec<TradeItem>,
348}
349
350#[derive(Debug, Clone, Deserialize)]
352pub struct TradeItem {
353 #[serde(default)]
355 pub uid: Option<String>,
356 pub side: String,
358 #[serde(rename = "type", default)]
360 pub trade_type: Option<String>,
361 pub price: Decimal,
363 pub qty: Decimal,
365 pub time: u64,
367 #[serde(default)]
369 pub seq: Option<u64>,
370}
371
372
373#[derive(Debug, Clone, Deserialize)]
378pub struct OpenOrdersMessage {
379 pub feed: String,
381 #[serde(default)]
383 pub orders: Option<Vec<WsOrder>>,
384 #[serde(flatten)]
386 pub order: Option<WsOrder>,
387}
388
389#[derive(Debug, Clone, Deserialize)]
391pub struct WsOrder {
392 #[serde(default)]
394 pub order_id: Option<String>,
395 #[serde(default)]
397 pub cli_ord_id: Option<String>,
398 #[serde(default)]
400 pub instrument: Option<String>,
401 #[serde(default)]
403 pub side: Option<String>,
404 #[serde(default)]
406 pub order_type: Option<String>,
407 #[serde(default)]
409 pub limit_price: Option<Decimal>,
410 #[serde(default)]
412 pub stop_price: Option<Decimal>,
413 #[serde(default)]
415 pub qty: Option<Decimal>,
416 #[serde(default)]
418 pub filled: Option<Decimal>,
419 #[serde(default)]
421 pub reduce_only: Option<bool>,
422 #[serde(default)]
424 pub time: Option<u64>,
425 #[serde(default)]
427 pub last_update_time: Option<u64>,
428 #[serde(default)]
430 pub status: Option<String>,
431 #[serde(default)]
433 pub reason: Option<String>,
434}
435
436#[derive(Debug, Clone, Deserialize)]
438pub struct FillsMessage {
439 pub feed: String,
441 #[serde(default)]
443 pub fills: Option<Vec<WsFill>>,
444 #[serde(flatten)]
446 pub fill: Option<WsFill>,
447}
448
449#[derive(Debug, Clone, Deserialize)]
451pub struct WsFill {
452 #[serde(default)]
454 pub fill_id: Option<String>,
455 #[serde(default)]
457 pub order_id: Option<String>,
458 #[serde(default)]
460 pub cli_ord_id: Option<String>,
461 #[serde(default)]
463 pub instrument: Option<String>,
464 #[serde(default)]
466 pub side: Option<String>,
467 #[serde(default)]
469 pub price: Option<Decimal>,
470 #[serde(default)]
472 pub qty: Option<Decimal>,
473 #[serde(default)]
475 pub fill_type: Option<String>,
476 #[serde(default)]
478 pub fee_paid: Option<Decimal>,
479 #[serde(default)]
481 pub fee_currency: Option<String>,
482 #[serde(default)]
484 pub time: Option<u64>,
485}
486
487#[derive(Debug, Clone, Deserialize)]
489pub struct OpenPositionsMessage {
490 pub feed: String,
492 #[serde(default)]
494 pub account: Option<String>,
495 #[serde(default)]
497 pub positions: Option<Vec<WsPosition>>,
498 #[serde(flatten)]
500 pub position: Option<WsPosition>,
501}
502
503#[derive(Debug, Clone, Deserialize)]
505pub struct WsPosition {
506 #[serde(default)]
508 pub instrument: Option<String>,
509 #[serde(default)]
511 pub balance: Option<Decimal>,
512 #[serde(default)]
514 pub entry_price: Option<Decimal>,
515 #[serde(default)]
517 pub mark_price: Option<Decimal>,
518 #[serde(default)]
520 pub index_price: Option<Decimal>,
521 #[serde(default)]
523 pub pnl: Option<Decimal>,
524 #[serde(default)]
526 pub effective_leverage: Option<Decimal>,
527 #[serde(default)]
529 pub initial_margin: Option<Decimal>,
530 #[serde(default)]
532 pub maintenance_margin: Option<Decimal>,
533 #[serde(default)]
535 pub return_on_equity: Option<Decimal>,
536}
537
538#[derive(Debug, Clone, Deserialize)]
540pub struct BalancesMessage {
541 pub feed: String,
543 #[serde(default)]
545 pub account: Option<String>,
546 #[serde(default)]
548 pub seq: Option<u64>,
549 #[serde(default)]
551 pub balance: Option<Decimal>,
552 #[serde(default)]
554 pub available: Option<Decimal>,
555 #[serde(default)]
557 pub margin: Option<Decimal>,
558 #[serde(default)]
560 pub pnl: Option<Decimal>,
561 #[serde(default)]
563 pub flex_futures: Option<FlexFuturesBalance>,
564}
565
566#[derive(Debug, Clone, Deserialize)]
568pub struct FlexFuturesBalance {
569 #[serde(default)]
571 pub currencies: Option<serde_json::Value>,
572 #[serde(default)]
574 pub portfolio_value: Option<Decimal>,
575 #[serde(default)]
577 pub available_margin: Option<Decimal>,
578 #[serde(default)]
580 pub initial_margin: Option<Decimal>,
581 #[serde(default)]
583 pub maintenance_margin: Option<Decimal>,
584 #[serde(default)]
586 pub unrealized_pnl: Option<Decimal>,
587}
588
589
590#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn test_challenge_request_serialization() {
599 let req = ChallengeRequest::new("my_api_key");
600 let json = serde_json::to_string(&req).unwrap();
601 assert!(json.contains("\"event\":\"challenge\""));
602 assert!(json.contains("\"api_key\":\"my_api_key\""));
603 }
604
605 #[test]
606 fn test_subscribe_request_serialization() {
607 let req = SubscribeRequest::public("book", vec!["PI_XBTUSD".into()]);
608 let json = serde_json::to_string(&req).unwrap();
609 assert!(json.contains("\"event\":\"subscribe\""));
610 assert!(json.contains("\"feed\":\"book\""));
611 assert!(json.contains("\"product_ids\":[\"PI_XBTUSD\"]"));
612 }
613
614 #[test]
615 fn test_challenge_response_deserialization() {
616 let json = r#"{"event":"challenge","message":"123e4567-e89b-12d3-a456-426614174000"}"#;
617 let resp: ChallengeResponse = serde_json::from_str(json).unwrap();
618 assert_eq!(resp.event, "challenge");
619 assert_eq!(resp.message, "123e4567-e89b-12d3-a456-426614174000");
620 }
621
622 #[test]
623 fn test_book_message_deserialization() {
624 let json = r#"{
625 "feed": "book",
626 "product_id": "PI_XBTUSD",
627 "seq": 1234,
628 "timestamp": 1640000000000,
629 "bids": [{"price": "50000.0", "qty": "1.5"}],
630 "asks": [{"price": "50001.0", "qty": "2.0"}]
631 }"#;
632 let msg: BookMessage = serde_json::from_str(json).unwrap();
633 assert_eq!(msg.feed, "book");
634 assert_eq!(msg.product_id, "PI_XBTUSD");
635 assert_eq!(msg.seq, Some(1234));
636 assert_eq!(msg.bids.len(), 1);
637 assert_eq!(msg.asks.len(), 1);
638 }
639
640 #[test]
641 fn test_ticker_message_deserialization() {
642 let json = r#"{
643 "feed": "ticker",
644 "product_id": "PI_XBTUSD",
645 "bid": "50000.0",
646 "ask": "50001.0",
647 "last": "50000.5",
648 "volume": "1000.0",
649 "funding_rate": "0.0001"
650 }"#;
651 let msg: TickerMessage = serde_json::from_str(json).unwrap();
652 assert_eq!(msg.feed, "ticker");
653 assert_eq!(msg.product_id, "PI_XBTUSD");
654 assert!(msg.bid.is_some());
655 assert!(msg.ask.is_some());
656 }
657
658 #[test]
659 fn test_private_subscribe_request() {
660 let req = PrivateSubscribeRequest::new(
661 "open_orders",
662 "challenge-uuid".to_string(),
663 "signed-challenge".to_string(),
664 );
665 let json = serde_json::to_string(&req).unwrap();
666 assert!(json.contains("\"event\":\"subscribe\""));
667 assert!(json.contains("\"feed\":\"open_orders\""));
668 assert!(json.contains("\"original_challenge\":\"challenge-uuid\""));
669 assert!(json.contains("\"signed_challenge\":\"signed-challenge\""));
670 }
671}