coinbase_pro_rs/structs/
wsfeed.rs

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), // in futures 0.3 probably TryStream
93}
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), // in futures 0.3 probably TryStream
118}
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}