Skip to main content

rustrade_data/exchange/bitmex/
trade.rs

1use crate::{
2    event::{MarketEvent, MarketIter},
3    exchange::bitmex::message::BitmexMessage,
4    subscription::trade::PublicTrade,
5};
6use chrono::{DateTime, Utc};
7use rust_decimal::Decimal;
8use rustrade_instrument::{Side, exchange::ExchangeId};
9use serde::{Deserialize, Serialize};
10
11/// Terse type alias for an [`BitmexTrade`](BitmexTradeInner) real-time trades WebSocket message.
12pub type BitmexTrade = BitmexMessage<BitmexTradeInner>;
13
14/// ### Raw Payload Examples
15/// See docs: <https://www.bitmex.com/app/wsAPI#Response-Format>
16/// #### Trade payload
17/// ```json
18/// {
19///     "table": "trade",
20///     "action": "insert",
21///     "data": [
22///         {
23///             "timestamp": "2023-02-18T09:27:59.701Z",
24///             "symbol": "XBTUSD",
25///             "side": "Sell",
26///             "size": 200,
27///             "price": 24564.5,
28///             "tickDirection": "MinusTick",
29///             "trdMatchID": "31e50cb7-e005-a44e-f354-86e88dff52eb",
30///             "grossValue": 814184,
31///             "homeNotional": 0.00814184,
32///             "foreignNotional": 200,
33///             "trdType": "Regular"
34///         }
35///     ]
36/// }
37///```
38#[derive(Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
39pub struct BitmexTradeInner {
40    pub timestamp: DateTime<Utc>,
41    pub symbol: String,
42    pub side: Side,
43    #[serde(rename = "size")]
44    pub amount: Decimal,
45    pub price: Decimal,
46
47    #[serde(rename = "trdMatchID")]
48    pub id: String,
49}
50
51impl<InstrumentKey: Clone> From<(ExchangeId, InstrumentKey, BitmexTrade)>
52    for MarketIter<InstrumentKey, PublicTrade>
53{
54    fn from((exchange, instrument, trades): (ExchangeId, InstrumentKey, BitmexTrade)) -> Self {
55        Self(
56            trades
57                .data
58                .into_iter()
59                .map(|trade| {
60                    Ok(MarketEvent {
61                        time_exchange: trade.timestamp,
62                        time_received: Utc::now(),
63                        exchange,
64                        instrument: instrument.clone(),
65                        kind: PublicTrade {
66                            id: trade.id.into(),
67                            price: trade.price,
68                            amount: trade.amount,
69                            side: Some(trade.side),
70                        },
71                    })
72                })
73                .collect(),
74        )
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    mod de {
83        use super::*;
84        use chrono::{Duration, TimeZone};
85        use rust_decimal_macros::dec;
86        use rustrade_integration::error::SocketError;
87
88        #[test]
89        fn test_bitmex_trade() {
90            struct TestCase {
91                input: &'static str,
92                expected: Result<BitmexTradeInner, SocketError>,
93            }
94
95            let tests = vec![
96                // TC0: input BitmexTrade is deserialised
97                TestCase {
98                    input: r#"
99                    {
100                        "timestamp": "2023-02-18T09:27:59.701Z",
101                        "symbol": "XBTUSD",
102                        "side": "Sell",
103                        "size": 200,
104                        "price": 24564.5,
105                        "tickDirection": "MinusTick",
106                        "trdMatchID": "31e50cb7-e005-a44e-f354-86e88dff52eb",
107                        "grossValue": 814184,
108                        "homeNotional": 0.00814184,
109                        "foreignNotional": 200,
110                        "trdType": "Regular"
111                    }
112                    "#,
113                    expected: Ok(BitmexTradeInner {
114                        timestamp: Utc.with_ymd_and_hms(2023, 2, 18, 9, 27, 59).unwrap()
115                            + Duration::milliseconds(701),
116                        symbol: "XBTUSD".to_string(),
117                        side: Side::Sell,
118                        amount: dec!(200),
119                        price: dec!(24564.5),
120                        id: "31e50cb7-e005-a44e-f354-86e88dff52eb".to_string(),
121                    }),
122                },
123            ];
124
125            for (index, test) in tests.into_iter().enumerate() {
126                let actual = serde_json::from_str::<BitmexTradeInner>(test.input);
127                match (actual, test.expected) {
128                    (Ok(actual), Ok(expected)) => {
129                        assert_eq!(actual, expected, "TC{} failed", index)
130                    }
131                    (Err(_), Err(_)) => {
132                        // Test passed
133                    }
134                    (actual, expected) => {
135                        // Test failed
136                        panic!(
137                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
138                        );
139                    }
140                }
141            }
142        }
143
144        #[test]
145        fn test_bitmex_trade_payload() {
146            struct TestCase {
147                input: &'static str,
148                expected: Result<BitmexTrade, SocketError>,
149            }
150
151            let tests = vec![
152                // TC0: input BitmexTradePayload is deserialised
153                TestCase {
154                    input: r#"
155                    {
156                        "table": "trade",
157                        "action": "insert",
158                        "data": [
159                            {
160                                "timestamp": "2023-02-18T09:27:59.701Z",
161                                "symbol": "XBTUSD",
162                                "side": "Sell",
163                                "size": 200,
164                                "price": 24564.5,
165                                "tickDirection": "MinusTick",
166                                "trdMatchID": "31e50cb7-e005-a44e-f354-86e88dff52eb",
167                                "grossValue": 814184,
168                                "homeNotional": 0.00814184,
169                                "foreignNotional": 200,
170                                "trdType": "Regular"
171                            }
172                        ]
173                    }
174                    "#,
175                    expected: Ok(BitmexTrade {
176                        table: "trade".to_string(),
177                        data: vec![BitmexTradeInner {
178                            timestamp: Utc.with_ymd_and_hms(2023, 2, 18, 9, 27, 59).unwrap()
179                                + Duration::milliseconds(701),
180                            symbol: "XBTUSD".to_string(),
181                            side: Side::Sell,
182                            amount: dec!(200),
183                            price: dec!(24564.5),
184                            id: "31e50cb7-e005-a44e-f354-86e88dff52eb".to_string(),
185                        }],
186                    }),
187                },
188            ];
189
190            for (index, test) in tests.into_iter().enumerate() {
191                let actual = serde_json::from_str::<BitmexTrade>(test.input);
192                match (actual, test.expected) {
193                    (Ok(actual), Ok(expected)) => {
194                        assert_eq!(actual, expected, "TC{} failed", index)
195                    }
196                    (Err(_), Err(_)) => {
197                        // Test passed
198                    }
199                    (actual, expected) => {
200                        // Test failed
201                        panic!(
202                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
203                        );
204                    }
205                }
206            }
207        }
208    }
209}