1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::types::{Candle, Tick};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum Side {
12 Buy,
14 Sell,
16}
17
18impl Side {
19 pub fn opposite(self) -> Self {
21 match self {
22 Self::Buy => Self::Sell,
23 Self::Sell => Self::Buy,
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36#[serde(transparent)]
37pub struct Symbol(pub String);
38
39impl Symbol {
40 #[inline]
42 pub fn new(s: impl Into<String>) -> Self {
43 Self(s.into())
44 }
45
46 #[inline]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51}
52
53impl From<&str> for Symbol {
54 fn from(s: &str) -> Self {
55 Self(s.to_string())
56 }
57}
58
59impl From<String> for Symbol {
60 fn from(s: String) -> Self {
61 Self(s)
62 }
63}
64
65impl AsRef<str> for Symbol {
66 #[inline]
67 fn as_ref(&self) -> &str {
68 &self.0
69 }
70}
71
72impl std::borrow::Borrow<str> for Symbol {
73 #[inline]
74 fn borrow(&self) -> &str {
75 &self.0
76 }
77}
78
79impl PartialEq<str> for Symbol {
80 fn eq(&self, other: &str) -> bool {
81 self.0 == other
82 }
83}
84
85impl PartialEq<&str> for Symbol {
86 fn eq(&self, other: &&str) -> bool {
87 self.0 == *other
88 }
89}
90
91impl std::fmt::Display for Symbol {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 self.0.fmt(f)
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub struct Exchange(pub String);
100
101impl From<&str> for Exchange {
102 fn from(s: &str) -> Self {
103 Self(s.to_string())
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum MarketDataEvent {
113 Ticker {
115 exchange: Exchange,
117 symbol: Symbol,
119 tick: Tick,
121 },
122 Candle {
124 exchange: Exchange,
126 symbol: Symbol,
128 candle: Candle,
130 },
131 Trade {
133 exchange: Exchange,
135 symbol: Symbol,
137 side: Side,
139 price: f64,
141 size: f64,
143 timestamp: DateTime<Utc>,
145 },
146}
147
148impl MarketDataEvent {
149 pub fn symbol(&self) -> &Symbol {
151 match self {
152 Self::Ticker { symbol, .. }
153 | Self::Candle { symbol, .. }
154 | Self::Trade { symbol, .. } => symbol,
155 }
156 }
157
158 pub fn exchange(&self) -> &Exchange {
160 match self {
161 Self::Ticker { exchange, .. }
162 | Self::Candle { exchange, .. }
163 | Self::Trade { exchange, .. } => exchange,
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::types::{Candle, Price, Tick, Volume};
172 use chrono::TimeZone;
173
174 #[test]
175 fn side_opposite_is_involutive() {
176 assert_eq!(Side::Buy.opposite(), Side::Sell);
177 assert_eq!(Side::Sell.opposite(), Side::Buy);
178 assert_eq!(Side::Buy.opposite().opposite(), Side::Buy);
179 assert_eq!(Side::Sell.opposite().opposite(), Side::Sell);
180 }
181
182 #[test]
183 fn symbol_as_str_borrow_and_display() {
184 let s = Symbol::new("BTCUSDT");
185 assert_eq!(s.as_str(), "BTCUSDT");
186 assert_eq!(s.as_ref(), "BTCUSDT");
187 assert_eq!(format!("{s}"), "BTCUSDT");
188
189 assert_eq!(s, "BTCUSDT");
191 assert_ne!(s, "ETHUSDT");
192
193 use std::collections::HashMap;
195 let mut m: HashMap<Symbol, i32> = HashMap::new();
196 m.insert(Symbol::new("BTCUSDT"), 1);
197 assert_eq!(m.get("BTCUSDT").copied(), Some(1));
198 }
199
200 #[test]
201 fn symbol_serde_transparent() {
202 let s = Symbol::new("ETHUSDT");
203 let json = serde_json::to_string(&s).unwrap();
204 assert_eq!(json, "\"ETHUSDT\"");
205 let back: Symbol = serde_json::from_str(&json).unwrap();
206 assert_eq!(back, s);
207 }
208
209 fn ev_ticker() -> MarketDataEvent {
210 MarketDataEvent::Ticker {
211 exchange: Exchange::from("kucoin"),
212 symbol: Symbol::new("XBTUSDTM"),
213 tick: Tick {
214 symbol: Symbol::new("XBTUSDTM"),
215 timestamp: Utc.timestamp_opt(0, 0).unwrap(),
216 bid: Price(1.0),
217 ask: Price(1.0),
218 bid_size: Volume(1.0),
219 ask_size: Volume(1.0),
220 last_price: None,
221 last_size: None,
222 },
223 }
224 }
225
226 fn ev_candle() -> MarketDataEvent {
227 MarketDataEvent::Candle {
228 exchange: Exchange::from("binance"),
229 symbol: Symbol::new("BTCUSDT"),
230 candle: Candle {
231 time: 0,
232 open: 1.0,
233 high: 1.0,
234 low: 1.0,
235 close: 1.0,
236 volume: 1.0,
237 },
238 }
239 }
240
241 fn ev_trade() -> MarketDataEvent {
242 MarketDataEvent::Trade {
243 exchange: Exchange::from("bybit"),
244 symbol: Symbol::new("ETHUSDT"),
245 side: Side::Buy,
246 price: 1.0,
247 size: 1.0,
248 timestamp: Utc.timestamp_opt(0, 0).unwrap(),
249 }
250 }
251
252 #[test]
253 fn market_data_event_accessors_cover_all_variants() {
254 let t = ev_ticker();
255 assert_eq!(t.symbol(), &Symbol::new("XBTUSDTM"));
256 assert_eq!(t.exchange(), &Exchange::from("kucoin"));
257
258 let c = ev_candle();
259 assert_eq!(c.symbol(), &Symbol::new("BTCUSDT"));
260 assert_eq!(c.exchange(), &Exchange::from("binance"));
261
262 let tr = ev_trade();
263 assert_eq!(tr.symbol(), &Symbol::new("ETHUSDT"));
264 assert_eq!(tr.exchange(), &Exchange::from("bybit"));
265 }
266}