alpaca_websocket/
messages.rs

1//! WebSocket message types for Alpaca streaming.
2
3#![allow(missing_docs)]
4
5use alpaca_base::types::*;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// WebSocket message types
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "T")]
12pub enum WebSocketMessage {
13    /// Authentication message
14    #[serde(rename = "auth")]
15    Auth(AuthMessage),
16
17    /// Subscription message
18    #[serde(rename = "subscribe")]
19    Subscribe(SubscribeMessage),
20
21    /// Unsubscription message
22    #[serde(rename = "unsubscribe")]
23    Unsubscribe(UnsubscribeMessage),
24
25    /// Market data messages
26    #[serde(rename = "t")]
27    Trade(TradeMessage),
28
29    #[serde(rename = "q")]
30    Quote(QuoteMessage),
31
32    #[serde(rename = "b")]
33    Bar(BarMessage),
34
35    /// Trading messages
36    #[serde(rename = "trade_updates")]
37    TradeUpdate(Box<TradeUpdateMessage>),
38
39    /// Status messages
40    #[serde(rename = "success")]
41    Success(SuccessMessage),
42
43    #[serde(rename = "error")]
44    Error(ErrorMessage),
45
46    /// Connection status
47    #[serde(rename = "connection")]
48    Connection(ConnectionMessage),
49}
50
51/// Authentication message
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct AuthMessage {
54    pub key: String,
55    pub secret: String,
56}
57
58/// Subscription message
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SubscribeMessage {
61    pub trades: Option<Vec<String>>,
62    pub quotes: Option<Vec<String>>,
63    pub bars: Option<Vec<String>>,
64    pub trade_updates: Option<bool>,
65}
66
67/// Unsubscription message
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct UnsubscribeMessage {
70    pub trades: Option<Vec<String>>,
71    pub quotes: Option<Vec<String>>,
72    pub bars: Option<Vec<String>>,
73    pub trade_updates: Option<bool>,
74}
75
76/// Trade message from WebSocket
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct TradeMessage {
79    #[serde(rename = "S")]
80    pub symbol: String,
81    #[serde(rename = "t")]
82    pub timestamp: DateTime<Utc>,
83    #[serde(rename = "p")]
84    pub price: f64,
85    #[serde(rename = "s")]
86    pub size: u32,
87    #[serde(rename = "x")]
88    pub exchange: String,
89    #[serde(rename = "c")]
90    pub conditions: Vec<String>,
91    #[serde(rename = "i")]
92    pub id: u64,
93}
94
95/// Quote message from WebSocket
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct QuoteMessage {
98    #[serde(rename = "S")]
99    pub symbol: String,
100    #[serde(rename = "t")]
101    pub timestamp: DateTime<Utc>,
102    #[serde(rename = "bp")]
103    pub bid_price: f64,
104    #[serde(rename = "bs")]
105    pub bid_size: u32,
106    #[serde(rename = "ap")]
107    pub ask_price: f64,
108    #[serde(rename = "as")]
109    pub ask_size: u32,
110    #[serde(rename = "bx")]
111    pub bid_exchange: String,
112    #[serde(rename = "ax")]
113    pub ask_exchange: String,
114}
115
116/// Bar message from WebSocket
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct BarMessage {
119    #[serde(rename = "S")]
120    pub symbol: String,
121    #[serde(rename = "t")]
122    pub timestamp: DateTime<Utc>,
123    #[serde(rename = "o")]
124    pub open: f64,
125    #[serde(rename = "h")]
126    pub high: f64,
127    #[serde(rename = "l")]
128    pub low: f64,
129    #[serde(rename = "c")]
130    pub close: f64,
131    #[serde(rename = "v")]
132    pub volume: u64,
133    #[serde(rename = "n")]
134    pub trade_count: Option<u64>,
135    #[serde(rename = "vw")]
136    pub vwap: Option<f64>,
137}
138
139/// Trade update message
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct TradeUpdateMessage {
142    pub event: TradeUpdateEvent,
143    pub order: Order,
144    pub timestamp: DateTime<Utc>,
145    pub position_qty: Option<String>,
146    pub price: Option<String>,
147    pub qty: Option<String>,
148}
149
150/// Trade update event types
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum TradeUpdateEvent {
154    New,
155    Fill,
156    PartialFill,
157    Canceled,
158    Expired,
159    DoneForDay,
160    Replaced,
161    Rejected,
162    PendingNew,
163    Stopped,
164    PendingCancel,
165    PendingReplace,
166    Calculated,
167    Suspended,
168    OrderReplacePending,
169    OrderCancelPending,
170}
171
172/// Success message
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct SuccessMessage {
175    pub msg: String,
176}
177
178/// Error message
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ErrorMessage {
181    pub code: u16,
182    pub msg: String,
183}
184
185/// Connection status message
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ConnectionMessage {
188    pub status: ConnectionStatus,
189}
190
191/// Connection status
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum ConnectionStatus {
195    Connected,
196    Authenticated,
197    AuthenticationFailed,
198    Disconnected,
199    Reconnecting,
200}
201
202/// Subscription request builder
203#[derive(Debug, Default)]
204pub struct SubscriptionBuilder {
205    trades: Vec<String>,
206    quotes: Vec<String>,
207    bars: Vec<String>,
208    trade_updates: bool,
209}
210
211impl SubscriptionBuilder {
212    /// Create a new subscription builder
213    pub fn new() -> Self {
214        Self::default()
215    }
216
217    /// Subscribe to trades for symbols
218    pub fn trades<I, S>(mut self, symbols: I) -> Self
219    where
220        I: IntoIterator<Item = S>,
221        S: Into<String>,
222    {
223        self.trades.extend(symbols.into_iter().map(|s| s.into()));
224        self
225    }
226
227    /// Subscribe to quotes for symbols
228    pub fn quotes<I, S>(mut self, symbols: I) -> Self
229    where
230        I: IntoIterator<Item = S>,
231        S: Into<String>,
232    {
233        self.quotes.extend(symbols.into_iter().map(|s| s.into()));
234        self
235    }
236
237    /// Subscribe to bars for symbols
238    pub fn bars<I, S>(mut self, symbols: I) -> Self
239    where
240        I: IntoIterator<Item = S>,
241        S: Into<String>,
242    {
243        self.bars.extend(symbols.into_iter().map(|s| s.into()));
244        self
245    }
246
247    /// Subscribe to trade updates
248    pub fn trade_updates(mut self) -> Self {
249        self.trade_updates = true;
250        self
251    }
252
253    /// Build the subscription message
254    pub fn build(self) -> SubscribeMessage {
255        SubscribeMessage {
256            trades: if self.trades.is_empty() {
257                None
258            } else {
259                Some(self.trades)
260            },
261            quotes: if self.quotes.is_empty() {
262                None
263            } else {
264                Some(self.quotes)
265            },
266            bars: if self.bars.is_empty() {
267                None
268            } else {
269                Some(self.bars)
270            },
271            trade_updates: if self.trade_updates { Some(true) } else { None },
272        }
273    }
274}
275
276impl From<TradeMessage> for Trade {
277    fn from(msg: TradeMessage) -> Self {
278        Trade {
279            timestamp: msg.timestamp,
280            price: msg.price,
281            size: msg.size,
282            exchange: msg.exchange,
283            conditions: msg.conditions,
284            id: msg.id,
285        }
286    }
287}
288
289impl From<QuoteMessage> for Quote {
290    fn from(msg: QuoteMessage) -> Self {
291        Quote {
292            timestamp: msg.timestamp,
293            timeframe: "real-time".to_string(),
294            bid_price: msg.bid_price,
295            bid_size: msg.bid_size,
296            ask_price: msg.ask_price,
297            ask_size: msg.ask_size,
298            bid_exchange: msg.bid_exchange,
299            ask_exchange: msg.ask_exchange,
300        }
301    }
302}
303
304impl From<BarMessage> for Bar {
305    fn from(msg: BarMessage) -> Self {
306        Bar {
307            timestamp: msg.timestamp,
308            open: msg.open,
309            high: msg.high,
310            low: msg.low,
311            close: msg.close,
312            volume: msg.volume,
313            trade_count: msg.trade_count,
314            vwap: msg.vwap,
315        }
316    }
317}
318
319// ============================================================================
320// Enhanced WebSocket Message Types
321// ============================================================================
322
323/// Crypto trade message from WebSocket.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct CryptoTradeMessage {
326    /// Symbol.
327    #[serde(rename = "S")]
328    pub symbol: String,
329    /// Timestamp.
330    #[serde(rename = "t")]
331    pub timestamp: DateTime<Utc>,
332    /// Trade price.
333    #[serde(rename = "p")]
334    pub price: f64,
335    /// Trade size.
336    #[serde(rename = "s")]
337    pub size: f64,
338    /// Taker side (buy or sell).
339    #[serde(rename = "tks")]
340    pub taker_side: String,
341    /// Trade ID.
342    #[serde(rename = "i")]
343    pub id: u64,
344}
345
346/// Crypto quote message from WebSocket.
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct CryptoQuoteMessage {
349    /// Symbol.
350    #[serde(rename = "S")]
351    pub symbol: String,
352    /// Timestamp.
353    #[serde(rename = "t")]
354    pub timestamp: DateTime<Utc>,
355    /// Bid price.
356    #[serde(rename = "bp")]
357    pub bid_price: f64,
358    /// Bid size.
359    #[serde(rename = "bs")]
360    pub bid_size: f64,
361    /// Ask price.
362    #[serde(rename = "ap")]
363    pub ask_price: f64,
364    /// Ask size.
365    #[serde(rename = "as")]
366    pub ask_size: f64,
367}
368
369/// Crypto bar message from WebSocket.
370#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct CryptoBarMessage {
372    /// Symbol.
373    #[serde(rename = "S")]
374    pub symbol: String,
375    /// Timestamp.
376    #[serde(rename = "t")]
377    pub timestamp: DateTime<Utc>,
378    /// Open price.
379    #[serde(rename = "o")]
380    pub open: f64,
381    /// High price.
382    #[serde(rename = "h")]
383    pub high: f64,
384    /// Low price.
385    #[serde(rename = "l")]
386    pub low: f64,
387    /// Close price.
388    #[serde(rename = "c")]
389    pub close: f64,
390    /// Volume.
391    #[serde(rename = "v")]
392    pub volume: f64,
393    /// Number of trades.
394    #[serde(rename = "n")]
395    pub trade_count: Option<u64>,
396    /// Volume-weighted average price.
397    #[serde(rename = "vw")]
398    pub vwap: Option<f64>,
399}
400
401/// Options trade message from WebSocket.
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct OptionTradeMessage {
404    /// Option symbol (OCC format).
405    #[serde(rename = "S")]
406    pub symbol: String,
407    /// Timestamp.
408    #[serde(rename = "t")]
409    pub timestamp: DateTime<Utc>,
410    /// Trade price.
411    #[serde(rename = "p")]
412    pub price: f64,
413    /// Trade size (number of contracts).
414    #[serde(rename = "s")]
415    pub size: u32,
416    /// Exchange.
417    #[serde(rename = "x")]
418    pub exchange: String,
419    /// Trade conditions.
420    #[serde(rename = "c", default)]
421    pub conditions: Option<String>,
422}
423
424/// Options quote message from WebSocket.
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct OptionQuoteMessage {
427    /// Option symbol (OCC format).
428    #[serde(rename = "S")]
429    pub symbol: String,
430    /// Timestamp.
431    #[serde(rename = "t")]
432    pub timestamp: DateTime<Utc>,
433    /// Bid price.
434    #[serde(rename = "bp")]
435    pub bid_price: f64,
436    /// Bid size.
437    #[serde(rename = "bs")]
438    pub bid_size: u32,
439    /// Ask price.
440    #[serde(rename = "ap")]
441    pub ask_price: f64,
442    /// Ask size.
443    #[serde(rename = "as")]
444    pub ask_size: u32,
445    /// Bid exchange.
446    #[serde(rename = "bx")]
447    pub bid_exchange: String,
448    /// Ask exchange.
449    #[serde(rename = "ax")]
450    pub ask_exchange: String,
451}
452
453/// News message from WebSocket.
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct NewsMessage {
456    /// News ID.
457    pub id: u64,
458    /// Headline.
459    pub headline: String,
460    /// Summary.
461    pub summary: Option<String>,
462    /// Author.
463    pub author: Option<String>,
464    /// Creation timestamp.
465    pub created_at: DateTime<Utc>,
466    /// Update timestamp.
467    pub updated_at: DateTime<Utc>,
468    /// URL to full article.
469    pub url: Option<String>,
470    /// Related symbols.
471    pub symbols: Vec<String>,
472    /// Source.
473    pub source: String,
474}
475
476/// Limit Up Limit Down (LULD) message from WebSocket.
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct LuldMessage {
479    /// Symbol.
480    #[serde(rename = "S")]
481    pub symbol: String,
482    /// Timestamp.
483    #[serde(rename = "t")]
484    pub timestamp: DateTime<Utc>,
485    /// LULD indicator.
486    #[serde(rename = "i")]
487    pub indicator: String,
488    /// Limit up price.
489    #[serde(rename = "u")]
490    pub limit_up_price: f64,
491    /// Limit down price.
492    #[serde(rename = "d")]
493    pub limit_down_price: f64,
494}
495
496/// Trading status message from WebSocket.
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct TradingStatusMessage {
499    /// Symbol.
500    #[serde(rename = "S")]
501    pub symbol: String,
502    /// Timestamp.
503    #[serde(rename = "t")]
504    pub timestamp: DateTime<Utc>,
505    /// Status code.
506    #[serde(rename = "sc")]
507    pub status_code: String,
508    /// Status message.
509    #[serde(rename = "sm")]
510    pub status_message: String,
511    /// Reason code.
512    #[serde(rename = "rc")]
513    pub reason_code: String,
514    /// Reason message.
515    #[serde(rename = "rm")]
516    pub reason_message: String,
517}
518
519/// Trade correction message from WebSocket.
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct CorrectionMessage {
522    /// Symbol.
523    #[serde(rename = "S")]
524    pub symbol: String,
525    /// Timestamp.
526    #[serde(rename = "t")]
527    pub timestamp: DateTime<Utc>,
528    /// Original trade ID.
529    #[serde(rename = "x")]
530    pub original_id: u64,
531    /// Original price.
532    #[serde(rename = "op")]
533    pub original_price: f64,
534    /// Original size.
535    #[serde(rename = "os")]
536    pub original_size: u32,
537    /// Corrected price.
538    #[serde(rename = "cp")]
539    pub corrected_price: f64,
540    /// Corrected size.
541    #[serde(rename = "cs")]
542    pub corrected_size: u32,
543}
544
545/// Cancel error message from WebSocket.
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct CancelErrorMessage {
548    /// Symbol.
549    #[serde(rename = "S")]
550    pub symbol: String,
551    /// Timestamp.
552    #[serde(rename = "t")]
553    pub timestamp: DateTime<Utc>,
554    /// Trade ID that was canceled in error.
555    #[serde(rename = "i")]
556    pub id: u64,
557    /// Original price.
558    #[serde(rename = "p")]
559    pub price: f64,
560    /// Original size.
561    #[serde(rename = "s")]
562    pub size: u32,
563}
564
565/// Daily bar message from WebSocket.
566#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct DailyBarMessage {
568    /// Symbol.
569    #[serde(rename = "S")]
570    pub symbol: String,
571    /// Timestamp.
572    #[serde(rename = "t")]
573    pub timestamp: DateTime<Utc>,
574    /// Open price.
575    #[serde(rename = "o")]
576    pub open: f64,
577    /// High price.
578    #[serde(rename = "h")]
579    pub high: f64,
580    /// Low price.
581    #[serde(rename = "l")]
582    pub low: f64,
583    /// Close price.
584    #[serde(rename = "c")]
585    pub close: f64,
586    /// Volume.
587    #[serde(rename = "v")]
588    pub volume: u64,
589    /// Volume-weighted average price.
590    #[serde(rename = "vw")]
591    pub vwap: Option<f64>,
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597
598    #[test]
599    fn test_subscription_builder() {
600        let sub = SubscriptionBuilder::new()
601            .trades(["AAPL", "MSFT"])
602            .quotes(["GOOGL"])
603            .trade_updates()
604            .build();
605
606        assert_eq!(
607            sub.trades,
608            Some(vec!["AAPL".to_string(), "MSFT".to_string()])
609        );
610        assert_eq!(sub.quotes, Some(vec!["GOOGL".to_string()]));
611        assert_eq!(sub.trade_updates, Some(true));
612    }
613
614    #[test]
615    fn test_trade_update_event_serialization() {
616        let event = TradeUpdateEvent::Fill;
617        let json = serde_json::to_string(&event).unwrap();
618        assert_eq!(json, "\"fill\"");
619    }
620
621    #[test]
622    fn test_connection_status_serialization() {
623        let status = ConnectionStatus::Connected;
624        let json = serde_json::to_string(&status).unwrap();
625        assert_eq!(json, "\"connected\"");
626    }
627}