1use super::DateTime;
2use crate::utils::{
3 f64_from_string, f64_nan_from_string, f64_opt_from_string, uuid_opt_from_string,
4};
5use serde::{Deserialize, Deserializer, Serialize};
6use uuid::Uuid;
7
8#[derive(Serialize, Deserialize, Debug)]
9pub struct Auth {
10 pub signature: String,
11 pub key: String,
12 pub passphrase: String,
13 pub timestamp: String,
14}
15
16#[derive(Serialize, Deserialize, Debug)]
17pub struct Subscribe {
18 #[serde(rename = "type")]
19 pub _type: SubscribeCmd,
20 pub product_ids: Vec<String>,
21 pub channels: Vec<Channel>,
22 #[serde(flatten)]
23 pub auth: Option<Auth>,
24}
25
26#[derive(Serialize, Deserialize, Debug)]
27#[serde(rename_all = "camelCase")]
28pub enum SubscribeCmd {
29 Subscribe,
30}
31
32#[derive(Serialize, Deserialize, Debug, PartialEq)]
33#[serde(untagged)]
34pub enum Channel {
35 Name(ChannelType),
36 WithProduct {
37 name: ChannelType,
38 product_ids: Vec<String>,
39 },
40}
41
42#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
43#[serde(rename_all = "camelCase")]
44pub enum ChannelType {
45 Heartbeat,
46 Status,
47 Ticker,
48 Level2,
49 Matches,
50 Full,
51 User,
52}
53
54#[derive(Deserialize, Debug)]
55#[serde(tag = "type")]
56#[serde(rename_all = "snake_case")]
57pub(crate) enum InputMessage {
58 Subscriptions {
59 channels: Vec<Channel>,
60 },
61 Heartbeat {
62 sequence: usize,
63 last_trade_id: usize,
64 product_id: String,
65 time: DateTime,
66 },
67 Status {
68 products: Vec<StatusProduct>,
69 currencies: Vec<StatusCurrency>
70 },
71 Ticker(Ticker),
72 Snapshot {
73 product_id: String,
74 bids: Vec<Level2SnapshotRecord>,
75 asks: Vec<Level2SnapshotRecord>,
76 },
77 L2update {
78 product_id: String,
79 changes: Vec<Level2UpdateRecord>,
80 time: DateTime,
81 },
82 LastMatch(Match),
83 Received(Received),
84 Open(Open),
85 Done(Done),
86 Match(Match),
87 Activate(Activate),
88 Change(Change),
89 Error {
90 message: String,
91 },
92 InternalError(crate::CBError), }
94
95#[derive(Debug, PartialEq)]
96pub enum Message {
97 Subscriptions {
98 channels: Vec<Channel>,
99 },
100 Heartbeat {
101 sequence: usize,
102 last_trade_id: usize,
103 product_id: String,
104 time: DateTime,
105 },
106 Status {
107 products: Vec<StatusProduct>,
108 currencies: Vec<StatusCurrency>
109 },
110 Ticker(Ticker),
111 Level2(Level2),
112 Match(Match),
113 Full(Full),
114 Error {
115 message: String,
116 },
117 InternalError(crate::CBError), }
119
120#[derive(Serialize, Deserialize, Debug, PartialEq)]
121pub enum Level2 {
122 Snapshot {
123 product_id: String,
124 bids: Vec<Level2SnapshotRecord>,
125 asks: Vec<Level2SnapshotRecord>,
126 },
127 L2update {
128 product_id: String,
129 changes: Vec<Level2UpdateRecord>,
130 time: DateTime,
131 },
132}
133
134impl Level2 {
135 pub fn product_id(&self) -> &str {
136 match self {
137 Level2::Snapshot { product_id, .. } => product_id,
138 Level2::L2update { product_id, .. } => product_id,
139 }
140 }
141
142 pub fn time(&self) -> Option<&DateTime> {
143 match self {
144 Level2::Snapshot { .. } => None,
145 Level2::L2update { time, .. } => Some(time),
146 }
147 }
148}
149
150#[derive(Serialize, Deserialize, Debug, PartialEq)]
151pub struct StatusProduct {
152 pub id: String,
153 pub base_currency: String,
154 pub quote_currency: String,
155 #[serde(deserialize_with = "f64_from_string")]
156 pub base_increment: f64,
157 #[serde(deserialize_with = "f64_from_string")]
158 pub quote_increment: f64,
159 pub display_name: String,
160 pub status: String,
161 pub status_message: String,
162 #[serde(deserialize_with = "f64_from_string")]
163 pub min_market_funds: f64,
164 pub post_only: bool,
165 pub limit_only: bool,
166 pub cancel_only: bool,
167 pub fx_stablecoin: bool
168}
169
170#[derive(Serialize, Deserialize, Debug, PartialEq)]
171pub struct StatusCurrency {
172 pub id: String,
173 pub name: String,
174 #[serde(deserialize_with = "f64_from_string")]
175 pub min_size: f64,
176 pub status: String,
177 pub status_message: String,
178 #[serde(deserialize_with = "f64_from_string")]
179 pub max_precision: f64,
180 pub convertible_to: Vec<String>,
181}
182
183#[derive(Serialize, Deserialize, Debug, PartialEq)]
184pub struct Level2SnapshotRecord {
185 #[serde(deserialize_with = "f64_from_string")]
186 pub price: f64,
187 #[serde(deserialize_with = "f64_from_string")]
188 pub size: f64,
189}
190
191#[derive(Serialize, Deserialize, Debug, PartialEq)]
192pub struct Level2UpdateRecord {
193 pub side: super::reqs::OrderSide,
194 #[serde(deserialize_with = "f64_from_string")]
195 pub price: f64,
196 #[serde(deserialize_with = "f64_from_string")]
197 pub size: f64,
198}
199
200#[derive(Serialize, Deserialize, Debug, PartialEq)]
201#[serde(untagged)]
202#[serde(rename_all = "camelCase")]
203pub enum Ticker {
204 Full {
205 trade_id: usize,
206 sequence: usize,
207 time: DateTime,
208 product_id: String,
209 #[serde(deserialize_with = "f64_from_string")]
210 price: f64,
211 side: super::reqs::OrderSide,
212 #[serde(deserialize_with = "f64_from_string")]
213 last_size: f64,
214 #[serde(deserialize_with = "f64_nan_from_string")]
215 best_bid: f64,
216 #[serde(deserialize_with = "f64_nan_from_string")]
217 best_ask: f64,
218 },
219 Empty {
220 sequence: usize,
221 product_id: String,
222 #[serde(deserialize_with = "f64_nan_from_string")]
223 price: f64,
224 },
225}
226
227impl Ticker {
228 pub fn price(&self) -> &f64 {
229 match self {
230 Ticker::Full { price, .. } => price,
231 Ticker::Empty { price, .. } => price,
232 }
233 }
234
235 pub fn time(&self) -> Option<&DateTime> {
236 match self {
237 Ticker::Full { time, .. } => Some(time),
238 Ticker::Empty { .. } => None,
239 }
240 }
241
242 pub fn product_id(&self) -> &str {
243 match self {
244 Ticker::Full { product_id, .. } => product_id,
245 Ticker::Empty { product_id, .. } => product_id,
246 }
247 }
248
249 pub fn sequence(&self) -> &usize {
250 match self {
251 Ticker::Full { sequence, .. } => sequence,
252 Ticker::Empty { sequence, .. } => sequence,
253 }
254 }
255
256 pub fn bid(&self) -> Option<&f64> {
257 match self {
258 Ticker::Full { best_bid, .. } => Some(best_bid),
259 Ticker::Empty { .. } => None,
260 }
261 }
262
263 pub fn ask(&self) -> Option<&f64> {
264 match self {
265 Ticker::Full { best_ask, .. } => Some(best_ask),
266 Ticker::Empty { .. } => None,
267 }
268 }
269}
270
271#[derive(Serialize, Deserialize, Debug, PartialEq)]
272pub enum Full {
273 Received(Received),
274 Open(Open),
275 Done(Done),
276 Match(Match),
277 Change(Change),
278 Activate(Activate),
279}
280
281impl Full {
282 pub fn price(&self) -> Option<&f64> {
283 match self {
284 Full::Received(Received::Limit { price, .. }) => Some(price),
285 Full::Received(Received::Market { .. }) => None,
286 Full::Open(Open { price, .. }) => Some(price),
287 Full::Done(Done::Limit { price, .. }) => Some(price),
288 Full::Done(Done::Market { .. }) => None,
289 Full::Match(Match { price, .. }) => Some(price),
290 Full::Change(Change { price, .. }) => price.as_ref(),
291 Full::Activate(Activate { .. }) => None,
292 }
293 }
294
295 pub fn time(&self) -> Option<&DateTime> {
296 match self {
297 Full::Received(Received::Limit { time, .. }) => Some(time),
298 Full::Received(Received::Market { time, .. }) => Some(time),
299 Full::Open(Open { time, .. }) => Some(time),
300 Full::Done(Done::Limit { time, .. }) => Some(time),
301 Full::Done(Done::Market { time, .. }) => Some(time),
302 Full::Match(Match { time, .. }) => Some(time),
303 Full::Change(Change { time, .. }) => Some(time),
304 Full::Activate(Activate { .. }) => None,
305 }
306 }
307
308 pub fn sequence(&self) -> Option<&usize> {
309 match self {
310 Full::Received(Received::Limit { sequence, .. }) => Some(sequence),
311 Full::Received(Received::Market { sequence, .. }) => Some(sequence),
312 Full::Open(Open { sequence, .. }) => Some(sequence),
313 Full::Done(Done::Limit { sequence, .. }) => sequence.as_ref(),
314 Full::Done(Done::Market { sequence, .. }) => Some(sequence),
315 Full::Match(Match { sequence, .. }) => Some(sequence),
316 Full::Change(Change { sequence, .. }) => Some(sequence),
317 Full::Activate(Activate { .. }) => None,
318 }
319 }
320
321 pub fn product_id(&self) -> &str {
322 match self {
323 Full::Received(Received::Limit { product_id, .. }) => product_id,
324 Full::Received(Received::Market { product_id, .. }) => product_id,
325 Full::Open(Open { product_id, .. }) => product_id,
326 Full::Done(Done::Limit { product_id, .. }) => product_id,
327 Full::Done(Done::Market { product_id, .. }) => product_id,
328 Full::Match(Match { product_id, .. }) => product_id,
329 Full::Change(Change { product_id, .. }) => product_id,
330 Full::Activate(Activate { product_id, .. }) => product_id,
331 }
332 }
333}
334
335#[derive(Serialize, Deserialize, Debug, PartialEq)]
336#[serde(tag = "order_type")]
337#[serde(rename_all = "camelCase")]
338pub enum Received {
339 Limit {
340 time: DateTime,
341 product_id: String,
342 sequence: usize,
343 order_id: Uuid,
344 #[serde(deserialize_with = "uuid_opt_from_string")]
345 client_oid: Option<Uuid>,
346 #[serde(deserialize_with = "f64_from_string")]
347 size: f64,
348 #[serde(deserialize_with = "f64_from_string")]
349 price: f64,
350 side: super::reqs::OrderSide,
351 user_id: Option<String>,
352 #[serde(default)]
353 #[serde(deserialize_with = "uuid_opt_from_string")]
354 profile_id: Option<Uuid>,
355 },
356 Market {
357 time: DateTime,
358 product_id: String,
359 sequence: usize,
360 order_id: Uuid,
361 #[serde(deserialize_with = "uuid_opt_from_string")]
362 client_oid: Option<Uuid>,
363 #[serde(default)]
364 #[serde(deserialize_with = "f64_opt_from_string")]
365 funds: Option<f64>,
366 side: super::reqs::OrderSide,
367 },
368}
369
370#[derive(Serialize, Deserialize, Debug, PartialEq)]
371pub struct Open {
372 pub time: DateTime,
373 pub product_id: String,
374 pub sequence: usize,
375 pub order_id: Uuid,
376 #[serde(deserialize_with = "f64_from_string")]
377 pub price: f64,
378 #[serde(deserialize_with = "f64_from_string")]
379 pub remaining_size: f64,
380 pub side: super::reqs::OrderSide,
381 pub user_id: Option<String>,
382 #[serde(default)]
383 #[serde(deserialize_with = "uuid_opt_from_string")]
384 pub profile_id: Option<Uuid>,
385}
386
387#[derive(Serialize, Deserialize, Debug, PartialEq)]
388#[serde(untagged)]
389pub enum Done {
390 Limit {
391 time: DateTime,
392 product_id: String,
393 sequence: Option<usize>,
394 #[serde(deserialize_with = "f64_from_string")]
395 price: f64,
396 order_id: Uuid,
397 reason: Reason,
398 side: super::reqs::OrderSide,
399 #[serde(deserialize_with = "f64_from_string")]
400 remaining_size: f64,
401 user_id: Option<String>,
402 #[serde(default)]
403 #[serde(deserialize_with = "uuid_opt_from_string")]
404 profile_id: Option<Uuid>,
405 },
406 Market {
407 time: DateTime,
408 product_id: String,
409 sequence: usize,
410 order_id: Uuid,
411 reason: Reason,
412 side: super::reqs::OrderSide,
413 },
414}
415
416#[derive(Serialize, Deserialize, Debug, PartialEq)]
417#[serde(rename_all = "camelCase")]
418pub enum Reason {
419 Filled,
420 Canceled,
421}
422
423#[derive(Serialize, Deserialize, Debug, PartialEq)]
424pub struct Match {
425 pub trade_id: usize,
426 pub sequence: usize,
427 pub maker_order_id: Uuid,
428 pub taker_order_id: Uuid,
429 pub time: DateTime,
430 pub product_id: String,
431 #[serde(deserialize_with = "f64_from_string")]
432 pub size: f64,
433 #[serde(deserialize_with = "f64_from_string")]
434 pub price: f64,
435 pub side: super::reqs::OrderSide,
436 pub taker_user_id: Option<String>,
437 pub taker_profile_id: Option<Uuid>,
438 #[serde(default)]
439 #[serde(deserialize_with = "f64_opt_from_string")]
440 pub taker_fee_rate: Option<f64>,
441
442 pub maker_user_id: Option<String>,
443 pub maker_profile_id: Option<Uuid>,
444 #[serde(default)]
445 #[serde(deserialize_with = "f64_opt_from_string")]
446 pub maker_fee_rate: Option<f64>,
447
448 pub user_id: Option<String>,
449 #[serde(default)]
450 #[serde(deserialize_with = "uuid_opt_from_string")]
451 pub profile_id: Option<Uuid>,
452}
453
454#[derive(Serialize, Deserialize, Debug, PartialEq)]
455pub struct Change {
456 pub time: DateTime,
457 pub sequence: usize,
458 pub order_id: Uuid,
459 pub product_id: String,
460 #[serde(deserialize_with = "f64_from_string")]
461 pub new_size: f64,
462 #[serde(deserialize_with = "f64_from_string")]
463 pub old_size: f64,
464 #[serde(default)]
465 #[serde(deserialize_with = "f64_opt_from_string")]
466 pub new_funds: Option<f64>,
467 #[serde(default)]
468 #[serde(deserialize_with = "f64_opt_from_string")]
469 pub old_funds: Option<f64>,
470 #[serde(default)]
471 #[serde(deserialize_with = "f64_opt_from_string")]
472 pub price: Option<f64>,
473 pub side: super::reqs::OrderSide,
474 pub user_id: Option<String>,
475 #[serde(default)]
476 #[serde(deserialize_with = "uuid_opt_from_string")]
477 pub profile_id: Option<Uuid>,
478}
479
480#[derive(Serialize, Deserialize, Debug, PartialEq)]
481pub struct Activate {
482 pub product_id: String,
483 #[serde(deserialize_with = "f64_from_string")]
484 pub timestamp: f64,
485 pub order_id: Uuid,
486 pub stop_type: StopType,
487 #[serde(deserialize_with = "f64_from_string")]
488 pub size: f64,
489 #[serde(deserialize_with = "f64_from_string")]
490 pub funds: f64,
491 #[serde(deserialize_with = "f64_from_string")]
492 pub taker_fee_rate: f64,
493 pub private: bool,
494 pub user_id: Option<String>,
495 #[serde(default)]
496 #[serde(deserialize_with = "uuid_opt_from_string")]
497 pub profile_id: Option<Uuid>,
498}
499
500#[derive(Serialize, Deserialize, Debug, PartialEq)]
501#[serde(rename_all = "camelCase")]
502pub enum StopType {
503 Entry,
504 Exit,
505}
506
507impl From<InputMessage> for Message {
508 fn from(msg: InputMessage) -> Self {
509 match msg {
510 InputMessage::Subscriptions { channels } => Message::Subscriptions { channels },
511 InputMessage::Heartbeat {
512 sequence,
513 last_trade_id,
514 product_id,
515 time,
516 } => Message::Heartbeat {
517 sequence,
518 last_trade_id,
519 product_id,
520 time,
521 },
522 InputMessage::Ticker(ticker) => Message::Ticker(ticker),
523 InputMessage::Snapshot {
524 product_id,
525 bids,
526 asks,
527 } => Message::Level2(Level2::Snapshot {
528 product_id,
529 bids,
530 asks,
531 }),
532 InputMessage::L2update {
533 product_id,
534 changes,
535 time,
536 } => Message::Level2(Level2::L2update {
537 product_id,
538 changes,
539 time,
540 }),
541 InputMessage::Status {
542 currencies,
543 products
544 } => Message::Status {
545 currencies,
546 products
547 },
548 InputMessage::LastMatch(_match) => Message::Match(_match),
549 InputMessage::Received(_match) => Message::Full(Full::Received(_match)),
550 InputMessage::Open(open) => Message::Full(Full::Open(open)),
551 InputMessage::Done(done) => Message::Full(Full::Done(done)),
552 InputMessage::Match(_match) => Message::Full(Full::Match(_match)),
553 InputMessage::Change(change) => Message::Full(Full::Change(change)),
554 InputMessage::Activate(activate) => Message::Full(Full::Activate(activate)),
555 InputMessage::Error { message } => Message::Error { message },
556 InputMessage::InternalError(err) => Message::InternalError(err),
557 }
558 }
559}
560
561impl<'de> Deserialize<'de> for Message {
562 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
563 where
564 D: Deserializer<'de>,
565 {
566 Deserialize::deserialize(deserializer).map(|input_msg: InputMessage| input_msg.into())
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use serde_json;
574 use std::str::FromStr;
575
576 #[test]
577 fn test_parse_numbers() {
578 #[derive(Serialize, Deserialize, Debug)]
579 struct S {
580 #[serde(deserialize_with = "f64_from_string")]
581 a: f64,
582 #[serde(deserialize_with = "f64_from_string")]
583 b: f64,
584 #[serde(deserialize_with = "f64_nan_from_string")]
585 c: f64,
586 #[serde(deserialize_with = "f64_opt_from_string")]
587 d: Option<f64>,
588 #[serde(deserialize_with = "f64_opt_from_string")]
589 e: Option<f64>,
590 #[serde(deserialize_with = "f64_opt_from_string")]
591 f: Option<f64>,
592 #[serde(default)]
593 #[serde(deserialize_with = "f64_opt_from_string")]
594 j: Option<f64>,
595 }
596
597 let json = r#"{
598 "a": 5.5,
599 "b":"5.5",
600 "c":"",
601 "d":"5.6",
602 "e":5.6,
603 "f":""
604 }"#;
605 let s: S = serde_json::from_str(json).unwrap();
606
607 assert_eq!(5.5, s.a);
608 assert_eq!(5.5, s.b);
609 assert!(s.c.is_nan());
610 assert_eq!(Some(5.6), s.d);
611 assert_eq!(Some(5.6), s.e);
612 assert_eq!(None, s.f);
613 assert_eq!(None, s.j);
614 }
615
616 #[test]
617 fn test_change_without_price() {
618 let json = r#"{ "type" : "change", "side" : "sell", "old_size" : "7.53424298",
619 "new_size" : "4.95057246", "order_id" : "0f352cbb-98a8-48ce-9dc6-3003870dcfd1",
620 "product_id" : "BTC-USD", "sequence" : 7053090065,
621 "time" : "2018-09-25T13:30:57.550000Z" }"#;
622
623 let m: Message = serde_json::from_str(json).unwrap();
624 let str = format!("{:?}", m);
625 assert!(str.contains("product_id: \"BTC-USD\""));
626 }
627
628 #[test]
629 fn test_canceled_order_done() {
630 let json = r#"{"type": "done", "side": "sell", "order_id": "d05c295b-af2e-4f5e-bfa0-55d93370c450",
631 "reason":"canceled","product_id":"BTC-USD","price":"10009.17000000","remaining_size":"0.00973768",
632 "user_id":"0fd194ab8a8bf175a75f8de5","profile_id":"fa94ac51-b20a-4b16-bc7a-af3c0abb7ec4",
633 "time":"2019-08-21T22:10:15.190000Z"}"#;
634 let m: Message = serde_json::from_str(json).unwrap();
635 let str = format!("{:?}", m);
636 assert!(str.contains("product_id: \"BTC-USD\""));
637 assert!(str.contains("user_id: Some"));
638 assert!(str.contains("profile_id: Some"));
639 }
640
641 #[test]
642 fn test_canceled_order_without_auth() {
643 let json = r#"{"type": "done", "side": "sell", "order_id": "d05c295b-af2e-4f5e-bfa0-55d93370c450",
644 "reason":"canceled","product_id":"BTC-USD","price":"10009.17000000","remaining_size":"0.00973768",
645 "time":"2019-08-21T22:10:15.190000Z"}"#;
646 let m: Message = serde_json::from_str(json).unwrap();
647 let str = format!("{:?}", m);
648 assert!(str.contains("product_id: \"BTC-USD\""));
649 assert!(str.contains("user_id: None"));
650 assert!(str.contains("profile_id: None"));
651 }
652
653 #[test]
654 fn test_parse_uuid() {
655 #[derive(Serialize, Deserialize, Debug)]
656 struct S {
657 #[serde(deserialize_with = "uuid_opt_from_string")]
658 uuid: Option<Uuid>,
659 }
660
661 let json = r#"{
662 "uuid":"2fec40ac-525b-4192-871a-39d784945055"
663 }"#;
664 let s: S = serde_json::from_str(json).unwrap();
665
666 assert_eq!(
667 s.uuid,
668 Some(Uuid::from_str("2fec40ac-525b-4192-871a-39d784945055").unwrap())
669 );
670
671 let json = r#"{
672 "uuid":""
673 }"#;
674 let s: S = serde_json::from_str(json).unwrap();
675
676 assert!(s.uuid.is_none());
677 }
678}