Skip to main content

rustrade_core/
market.rs

1//! Exchange-agnostic market data primitives.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::types::{Candle, Tick};
7
8/// Side of a trade or order.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum Side {
12    /// Buying side — long entries and short exits.
13    Buy,
14    /// Selling side — short entries and long exits.
15    Sell,
16}
17
18impl Side {
19    /// The opposite side — used to construct closing orders.
20    pub fn opposite(self) -> Self {
21        match self {
22            Self::Buy => Self::Sell,
23            Self::Sell => Self::Buy,
24        }
25    }
26}
27
28/// Identifies an exchange-symbol pair.
29///
30/// `Symbol` is a thin wrapper over `String`. Symbols are exchange-specific
31/// (e.g. `"XBTUSDTM"` on KuCoin, `"BTCUSDT"` on Binance) so the framework
32/// stays string-typed rather than pretending to enumerate them, but the
33/// newtype prevents accidental confusion with other free-form `String`
34/// fields (URLs, account ids, error messages, …).
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36#[serde(transparent)]
37pub struct Symbol(pub String);
38
39impl Symbol {
40    /// Construct a new `Symbol`.
41    #[inline]
42    pub fn new(s: impl Into<String>) -> Self {
43        Self(s.into())
44    }
45
46    /// Borrow the underlying string slice.
47    #[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/// Opaque identifier for which exchange produced this data.
98#[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/// A normalized market-data event that can come from any exchange.
108///
109/// Exchange adapters parse their native formats into this enum so the
110/// downstream brain and risk layers are exchange-agnostic.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum MarketDataEvent {
113    /// Best-bid/best-ask update.
114    Ticker {
115        /// Exchange this ticker came from.
116        exchange: Exchange,
117        /// Symbol the ticker is for.
118        symbol: Symbol,
119        /// The tick itself.
120        tick: Tick,
121    },
122    /// A completed candle (fully closed bar).
123    Candle {
124        /// Exchange this candle came from.
125        exchange: Exchange,
126        /// Symbol the candle is for.
127        symbol: Symbol,
128        /// The OHLCV candle.
129        candle: Candle,
130    },
131    /// An individual trade print.
132    Trade {
133        /// Exchange this trade was reported by.
134        exchange: Exchange,
135        /// Symbol the trade was for.
136        symbol: Symbol,
137        /// Aggressor side of the trade.
138        side: Side,
139        /// Trade price in quote currency.
140        price: f64,
141        /// Trade size in base-asset units or contracts.
142        size: f64,
143        /// Time the trade occurred.
144        timestamp: DateTime<Utc>,
145    },
146}
147
148impl MarketDataEvent {
149    /// Borrow the event's [`Symbol`] regardless of variant.
150    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    /// Borrow the event's source [`Exchange`] regardless of variant.
159    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        // PartialEq<&str> for ergonomic comparison.
190        assert_eq!(s, "BTCUSDT");
191        assert_ne!(s, "ETHUSDT");
192
193        // Borrow<str> so HashMap<Symbol, _> can be looked up by &str.
194        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}