Skip to main content

rustrade_data/exchange/alpaca/
market.rs

1use super::{Alpaca, AlpacaServerCrypto, AlpacaServerIex, AlpacaServerSip};
2use crate::{Identifier, instrument::MarketInstrumentData, subscription::Subscription};
3use rustrade_instrument::{
4    Keyed, asset::name::AssetNameInternal, instrument::market_data::MarketDataInstrument,
5};
6use serde::{Deserialize, Serialize};
7use smol_str::{SmolStr, StrExt, format_smolstr};
8
9/// Alpaca market identifier.
10///
11/// - Equities (IEX/SIP): uppercase symbol, e.g., `"AAPL"`, `"SPY"`
12/// - Crypto: `"BASE/USD"` format, e.g., `"BTC/USD"`, `"ETH/USD"`
13#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
14pub struct AlpacaMarket(pub SmolStr);
15
16impl AsRef<str> for AlpacaMarket {
17    fn as_ref(&self) -> &str {
18        &self.0
19    }
20}
21
22fn equities_market(base: &AssetNameInternal) -> AlpacaMarket {
23    AlpacaMarket(base.name().to_uppercase_smolstr())
24}
25
26fn crypto_market(base: &AssetNameInternal, quote: &AssetNameInternal) -> AlpacaMarket {
27    AlpacaMarket(format_smolstr!(
28        "{}/{}",
29        base.name().to_uppercase_smolstr(),
30        quote.name().to_uppercase_smolstr()
31    ))
32}
33
34/// How an Alpaca feed spells the symbol it subscribes on.
35///
36/// Each feed uses exactly one spelling, so the shape is known statically from the connector type.
37#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
38pub enum AlpacaSymbolShape {
39    /// A bare uppercase ticker — the equities feeds: `AAPL`, `SPY`.
40    Ticker,
41    /// `BASE/QUOTE`, uppercase — the crypto feed: `BTC/USD`.
42    Pair,
43}
44
45impl AlpacaSymbolShape {
46    fn market(self, instrument: &MarketDataInstrument) -> AlpacaMarket {
47        match self {
48            Self::Ticker => equities_market(&instrument.base),
49            Self::Pair => crypto_market(&instrument.base, &instrument.quote),
50        }
51    }
52}
53
54/// An Alpaca market data WebSocket server, and the symbol spelling its feed uses.
55///
56/// Implemented by [`AlpacaServerIex`], [`AlpacaServerSip`] and [`AlpacaServerCrypto`]. It exists so
57/// each instrument representation spells its symbol once rather than once per server, and so each
58/// feed's spelling is declared in exactly one place.
59pub trait AlpacaServer: crate::exchange::ExchangeServer {
60    /// The spelling this feed's symbols take.
61    const SYMBOL_SHAPE: AlpacaSymbolShape;
62}
63
64impl AlpacaServer for AlpacaServerIex {
65    const SYMBOL_SHAPE: AlpacaSymbolShape = AlpacaSymbolShape::Ticker;
66}
67
68impl AlpacaServer for AlpacaServerSip {
69    const SYMBOL_SHAPE: AlpacaSymbolShape = AlpacaSymbolShape::Ticker;
70}
71
72impl AlpacaServer for AlpacaServerCrypto {
73    const SYMBOL_SHAPE: AlpacaSymbolShape = AlpacaSymbolShape::Pair;
74}
75
76/// An instrument representation an Alpaca subscription can spell as a feed symbol.
77///
78/// Implemented for every instrument type this crate subscribes with: [`MarketDataInstrument`],
79/// [`Keyed`] over it, and [`MarketInstrumentData`]. Every [`Subscription`] to an [`Alpaca`] feed over
80/// an implementor identifies its [`AlpacaMarket`] through it, so one blanket `Identifier` impl covers
81/// every feed and every representation.
82///
83/// It is also what lets [`DynamicStreams`](crate::streams::builder::dynamic::DynamicStreams) route
84/// to this integration: a bound on the instrument type alone implies the identifier for every
85/// feed, where the alternative is one bound per feed and kind.
86pub trait AlpacaInstrument {
87    /// The symbol this instrument subscribes to on a feed spelling symbols as `shape`.
88    fn alpaca_market(&self, shape: AlpacaSymbolShape) -> AlpacaMarket;
89}
90
91impl<Server, Instrument, Kind> Identifier<AlpacaMarket>
92    for Subscription<Alpaca<Server>, Instrument, Kind>
93where
94    Server: AlpacaServer,
95    Instrument: AlpacaInstrument,
96{
97    fn id(&self) -> AlpacaMarket {
98        self.instrument.alpaca_market(Server::SYMBOL_SHAPE)
99    }
100}
101
102impl AlpacaInstrument for MarketDataInstrument {
103    fn alpaca_market(&self, shape: AlpacaSymbolShape) -> AlpacaMarket {
104        shape.market(self)
105    }
106}
107
108impl<InstrumentKey> AlpacaInstrument for Keyed<InstrumentKey, MarketDataInstrument> {
109    fn alpaca_market(&self, shape: AlpacaSymbolShape) -> AlpacaMarket {
110        self.value.alpaca_market(shape)
111    }
112}
113
114/// Takes the exchange-side name as given, so it needs no per-feed shape: the caller has already
115/// supplied Alpaca's own symbol, and there is nothing to reconstruct.
116///
117/// Case is still normalised, as the reconstruction path normalises it. Alpaca spells its symbols in
118/// uppercase, and a subscribe is confirmed only once Alpaca's answer names each requested symbol
119/// exactly — so a lowercase `name_exchange` would otherwise fail the subscribe with a timeout naming
120/// it. On a correctly spelled exchange name the call is a no-op.
121impl<InstrumentKey> AlpacaInstrument for MarketInstrumentData<InstrumentKey> {
122    fn alpaca_market(&self, _shape: AlpacaSymbolShape) -> AlpacaMarket {
123        AlpacaMarket(self.name_exchange.name().to_uppercase_smolstr())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use rustrade_instrument::asset::name::AssetNameInternal;
131
132    #[test]
133    fn test_equities_market_format() {
134        let base = AssetNameInternal::new("aapl");
135        let market = equities_market(&base);
136        assert_eq!(market.as_ref(), "AAPL");
137    }
138
139    #[test]
140    fn test_crypto_market_format() {
141        let base = AssetNameInternal::new("btc");
142        let quote = AssetNameInternal::new("usd");
143        let market = crypto_market(&base, &quote);
144        assert_eq!(market.as_ref(), "BTC/USD");
145    }
146
147    #[test]
148    fn each_feed_spells_its_symbols_with_its_own_shape() {
149        use crate::subscription::trade::PublicTrades;
150        use rustrade_instrument::instrument::market_data::kind::MarketDataInstrumentKind;
151
152        fn market<Server: AlpacaServer>(base: &str, quote: &str) -> AlpacaMarket {
153            let subscription: Subscription<Alpaca<Server>, MarketDataInstrument, PublicTrades> =
154                Subscription::from((
155                    Alpaca::<Server>::default(),
156                    base,
157                    quote,
158                    MarketDataInstrumentKind::Spot,
159                    PublicTrades,
160                ));
161            subscription.id()
162        }
163
164        assert_eq!(market::<AlpacaServerIex>("aapl", "usd").as_ref(), "AAPL");
165        assert_eq!(market::<AlpacaServerSip>("aapl", "usd").as_ref(), "AAPL");
166        assert_eq!(
167            market::<AlpacaServerCrypto>("btc", "usd").as_ref(),
168            "BTC/USD"
169        );
170    }
171
172    #[test]
173    fn an_exchange_named_instrument_is_taken_verbatim() {
174        assert_eq!(
175            market_of_exchange_named::<AlpacaServerCrypto>("BTC/USD").as_ref(),
176            "BTC/USD"
177        );
178        assert_eq!(
179            market_of_exchange_named::<AlpacaServerIex>("AAPL").as_ref(),
180            "AAPL"
181        );
182    }
183
184    /// Alpaca confirms a subscribe only once its answer names each requested symbol exactly, in
185    /// its own uppercase spelling, so a lowercase name left alone would time out unconfirmed.
186    #[test]
187    fn an_exchange_named_instrument_is_uppercased_like_a_reconstructed_one() {
188        assert_eq!(
189            market_of_exchange_named::<AlpacaServerCrypto>("btc/usd").as_ref(),
190            "BTC/USD"
191        );
192        assert_eq!(
193            market_of_exchange_named::<AlpacaServerSip>("brk.b").as_ref(),
194            "BRK.B"
195        );
196    }
197
198    fn market_of_exchange_named<Server: AlpacaServer>(name_exchange: &str) -> AlpacaMarket {
199        use crate::subscription::trade::PublicTrades;
200        use rustrade_instrument::instrument::{
201            market_data::kind::MarketDataInstrumentKind, name::InstrumentNameExchange,
202        };
203
204        let subscription = Subscription {
205            exchange: Alpaca::<Server>::default(),
206            instrument: MarketInstrumentData {
207                key: 0_usize,
208                name_exchange: InstrumentNameExchange::new(name_exchange),
209                kind: MarketDataInstrumentKind::Spot,
210            },
211            kind: PublicTrades,
212        };
213
214        subscription.id()
215    }
216}