Skip to main content

rustrade_data/exchange/massive/
market.rs

1//! How each Massive cluster spells a market on its WebSocket.
2
3use super::Massive;
4use crate::{
5    Identifier,
6    exchange::{ExchangeServer, osi},
7    instrument::MarketInstrumentData,
8    subscription::Subscription,
9};
10use rustrade_instrument::{
11    Keyed,
12    asset::name::AssetNameInternal,
13    instrument::market_data::{MarketDataInstrument, kind::MarketDataInstrumentKind},
14};
15use serde::{Deserialize, Serialize};
16use smol_str::{SmolStr, StrExt, format_smolstr};
17
18/// A market as a Massive WebSocket spells it: `AAPL`, `BTC-USD`, `EUR/USD` or
19/// `O:AAPL251219C00150000`, depending on the cluster.
20#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
21pub struct MassiveMarket(pub SmolStr);
22
23impl AsRef<str> for MassiveMarket {
24    fn as_ref(&self) -> &str {
25        &self.0
26    }
27}
28
29impl std::fmt::Display for MassiveMarket {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.write_str(&self.0)
32    }
33}
34
35/// How a cluster spells its markets.
36///
37/// Each spelling is the one the cluster's data messages carry, so a subscription and the messages
38/// it delivers name the market identically. Forex is the case in point: the cluster also accepts a
39/// subscription to `EUR-USD`, but delivers it as `EUR/USD`.
40#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
41pub enum MassiveSymbols {
42    /// The base asset alone, upper-case: `AAPL`.
43    Ticker,
44    /// Base and quote joined by a hyphen, upper-case: `BTC-USD`.
45    CryptoPair,
46    /// Base and quote joined by a slash, upper-case: `EUR/USD`.
47    CurrencyPair,
48    /// `O:` and the contract's OSI symbol, unpadded: `O:AAPL251219C00150000`.
49    OptionContract,
50}
51
52/// Where option contracts start.
53pub(super) const OPTION_PREFIX: &str = "O:";
54
55impl MassiveSymbols {
56    fn market(
57        self,
58        base: &AssetNameInternal,
59        quote: &AssetNameInternal,
60        kind: &MarketDataInstrumentKind,
61    ) -> MassiveMarket {
62        let base_name = base.name().to_uppercase_smolstr();
63
64        MassiveMarket(match self {
65            Self::Ticker => base_name,
66            Self::CryptoPair => {
67                format_smolstr!("{base_name}-{}", quote.name().to_uppercase_smolstr())
68            }
69            Self::CurrencyPair => {
70                format_smolstr!("{base_name}/{}", quote.name().to_uppercase_smolstr())
71            }
72            Self::OptionContract => {
73                let spelled = match kind {
74                    MarketDataInstrumentKind::Option(contract) => {
75                        osi::symbol(base.name(), contract)
76                    }
77                    _ => None,
78                };
79
80                // `Identifier::id` is infallible, so an instrument with no OSI spelling cannot fail
81                // here. It gets a symbol saying why instead, which is not an option contract, so
82                // the subscriber rejects the batch naming it before anything is sent.
83                match spelled {
84                    Some(symbol) => format_smolstr!("{OPTION_PREFIX}{symbol}"),
85                    None => format_smolstr!("{base_name} (no OSI symbol for {kind})"),
86                }
87            }
88        })
89    }
90}
91
92/// Whether `market` spells an option contract.
93pub(super) fn is_option_contract(market: &str) -> bool {
94    market
95        .strip_prefix(OPTION_PREFIX)
96        .and_then(osi::root)
97        .is_some()
98}
99
100/// A Massive WebSocket cluster: its endpoint, how it spells markets, and the channels it publishes.
101///
102/// Implemented by the four shipped clusters. A cluster reached at another endpoint — Massive's
103/// legacy `socket.polygon.io` host, say — is a type of your own implementing this and
104/// [`ExchangeServer`] with the constants of the cluster it mirrors, [`ExchangeServer::ID`] among
105/// them: the subscriber refuses a non-contract instrument before connecting only on
106/// [`ExchangeId::MassiveOptions`](rustrade_instrument::exchange::ExchangeId::MassiveOptions).
107pub trait MassiveServer: ExchangeServer {
108    /// How the cluster spells its markets.
109    const SYMBOLS: MassiveSymbols;
110
111    /// The quote channel's event: `Q`, `XQ` or `C`.
112    const QUOTES: &'static str;
113
114    /// The per-second aggregate channel's event: `A`, `XAS` or `CAS`.
115    const SECOND_AGGREGATES: &'static str;
116
117    /// The per-minute aggregate channel's event: `AM`, `XA` or `CA`.
118    const MINUTE_AGGREGATES: &'static str;
119}
120
121/// A Massive cluster that publishes trades. Forex does not.
122pub trait MassiveTradeServer: MassiveServer {
123    /// The trade channel's event: `T` or `XT`.
124    const TRADES: &'static str;
125}
126
127/// An instrument representation a Massive subscription can spell as a market.
128///
129/// Implemented for every instrument type this crate subscribes with: [`MarketDataInstrument`],
130/// [`Keyed`] over it, and [`MarketInstrumentData`].
131pub trait MassiveInstrument {
132    /// The market this instrument subscribes to on a cluster spelling markets as `symbols`.
133    fn massive_market(&self, symbols: MassiveSymbols) -> MassiveMarket;
134}
135
136impl<Server, Instrument, Kind> Identifier<MassiveMarket>
137    for Subscription<Massive<Server>, Instrument, Kind>
138where
139    Server: MassiveServer,
140    Instrument: MassiveInstrument,
141{
142    fn id(&self) -> MassiveMarket {
143        self.instrument.massive_market(Server::SYMBOLS)
144    }
145}
146
147impl MassiveInstrument for MarketDataInstrument {
148    fn massive_market(&self, symbols: MassiveSymbols) -> MassiveMarket {
149        symbols.market(&self.base, &self.quote, &self.kind)
150    }
151}
152
153impl<InstrumentKey> MassiveInstrument for Keyed<InstrumentKey, MarketDataInstrument> {
154    fn massive_market(&self, symbols: MassiveSymbols) -> MassiveMarket {
155        self.value.massive_market(symbols)
156    }
157}
158
159/// Takes the exchange-side name as given: the caller has already supplied the cluster's own
160/// spelling, and there is nothing to reconstruct.
161impl<InstrumentKey> MassiveInstrument for MarketInstrumentData<InstrumentKey> {
162    fn massive_market(&self, _symbols: MassiveSymbols) -> MassiveMarket {
163        MassiveMarket(self.name_exchange.name().clone())
164    }
165}
166
167#[cfg(test)]
168#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
169mod tests {
170    use super::*;
171    use chrono::{DateTime, Utc};
172    use rust_decimal_macros::dec;
173    use rustrade_instrument::instrument::{
174        kind::option::{OptionExercise, OptionKind},
175        market_data::kind::MarketDataOptionContract,
176    };
177
178    fn instrument(base: &str, quote: &str, kind: MarketDataInstrumentKind) -> MarketDataInstrument {
179        MarketDataInstrument::new(base, quote, kind)
180    }
181
182    fn contract() -> MarketDataInstrumentKind {
183        MarketDataInstrumentKind::Option(MarketDataOptionContract {
184            kind: OptionKind::Call,
185            exercise: OptionExercise::American,
186            expiry: "2025-12-19T21:00:00Z".parse::<DateTime<Utc>>().unwrap(),
187            strike: dec!(150),
188        })
189    }
190
191    #[test]
192    fn each_cluster_spells_markets_as_its_messages_do() {
193        let spot = |base, quote, symbols: MassiveSymbols| {
194            instrument(base, quote, MarketDataInstrumentKind::Spot)
195                .massive_market(symbols)
196                .0
197        };
198
199        assert_eq!(spot("aapl", "usd", MassiveSymbols::Ticker), "AAPL");
200        assert_eq!(spot("btc", "usd", MassiveSymbols::CryptoPair), "BTC-USD");
201        assert_eq!(spot("eur", "usd", MassiveSymbols::CurrencyPair), "EUR/USD");
202        assert_eq!(
203            instrument("aapl", "usd", contract())
204                .massive_market(MassiveSymbols::OptionContract)
205                .0,
206            "O:AAPL251219C00150000",
207        );
208    }
209
210    #[test]
211    fn an_instrument_with_no_option_symbol_is_not_mistaken_for_one() {
212        let market = instrument("aapl", "usd", MarketDataInstrumentKind::Spot)
213            .massive_market(MassiveSymbols::OptionContract);
214
215        assert!(!is_option_contract(&market.0), "{market}");
216        assert!(is_option_contract("O:AAPL251219C00150000"));
217        assert!(!is_option_contract("AAPL251219C00150000"));
218    }
219}