Skip to main content

rustrade_data/exchange/lse/
mapper.rs

1//! The [`SubscriptionId`] a London Strategic Edge tick is filed under, and the mapper that files
2//! each subscription under it.
3
4use crate::{
5    Identifier,
6    exchange::{Connector, subscription::ExchangeSub},
7    instrument::InstrumentData,
8    subscriber::mapper::SubscriptionMapper,
9    subscription::{Map, Subscription, SubscriptionKind, SubscriptionMeta},
10};
11use fnv::FnvHashMap;
12use rustrade_integration::subscription::SubscriptionId;
13use serde::{Deserialize, Serialize};
14
15/// The [`SubscriptionId`] the provider's ticks for `symbol` arrive under: the symbol itself.
16///
17/// The only place an LSE identifier is spelled. The instrument map, the tick decoder, the resume
18/// bookkeeping and the shared connection all build it here, so none of them can disagree about
19/// which subscription a tick belongs to.
20///
21/// # Why there is no channel prefix
22/// The standard identifier is `channel|market`. This provider has one channel
23/// ([`LseChannel::Tick`](super::channel::LseChannel::Tick)), so a prefix would carry no
24/// information, and it would cost an allocation on every tick: a
25/// [`SmolStr`](smol_str::SmolStr) holds 23 bytes inline, and an option contract's symbol is its
26/// root plus fifteen characters. With a six-character root that is 21 bytes on its own, and 26
27/// with `tick|`. Bare, every symbol this feed publishes fits inline.
28///
29/// Dropping the channel does not merge the two subscription kinds: each stream carries its own
30/// instrument map for one kind, and a watermark is filed under the kind as well — see
31/// [`LseResumeKey`](super::resume::LseResumeKey).
32pub(super) fn subscription_id(symbol: &str) -> SubscriptionId {
33    SubscriptionId::from(symbol)
34}
35
36/// The [`SubscriptionMapper`] for London Strategic Edge: [`WebSocketSubMapper`] with each
37/// subscription filed under its bare symbol rather than `channel|market`.
38///
39/// # ⚠️ `ExchangeSub::id` is not this integration's identifier
40/// The blanket `Identifier<SubscriptionId>` impl on [`ExchangeSub`] still spells `tick|<symbol>`
41/// for this connector, because it serves every exchange and cannot be overridden for one. No tick
42/// arrives under that spelling. Build an identifier through this mapper instead.
43///
44/// [`WebSocketSubMapper`]: crate::subscriber::mapper::WebSocketSubMapper
45#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
46pub struct LseSubMapper;
47
48impl SubscriptionMapper for LseSubMapper {
49    fn map<Exchange, Instrument, Kind>(
50        subscriptions: &[Subscription<Exchange, Instrument, Kind>],
51    ) -> SubscriptionMeta<Instrument::Key>
52    where
53        Exchange: Connector,
54        Instrument: InstrumentData,
55        Kind: SubscriptionKind,
56        Subscription<Exchange, Instrument, Kind>:
57            Identifier<Exchange::Channel> + Identifier<Exchange::Market>,
58    {
59        let mut instrument_map = Map(FnvHashMap::with_capacity_and_hasher(
60            subscriptions.len(),
61            Default::default(),
62        ));
63
64        let exchange_subs = subscriptions
65            .iter()
66            .map(|subscription| {
67                let exchange_sub =
68                    ExchangeSub::<Exchange::Channel, Exchange::Market>::new(subscription);
69
70                instrument_map.0.insert(
71                    subscription_id(exchange_sub.market.as_ref()),
72                    subscription.instrument.key().clone(),
73                );
74
75                exchange_sub
76            })
77            .collect::<Vec<_>>();
78
79        SubscriptionMeta {
80            instrument_map,
81            ws_subscriptions: Exchange::requests(exchange_subs),
82        }
83    }
84}
85
86#[cfg(test)]
87#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
88mod tests {
89    use super::*;
90    use crate::{exchange::lse::LseOptions, subscription::trade::PublicTrades};
91    use rust_decimal_macros::dec;
92    use rustrade_instrument::instrument::{
93        kind::option::{OptionExercise, OptionKind},
94        market_data::{
95            MarketDataInstrument,
96            kind::{MarketDataInstrumentKind, MarketDataOptionContract},
97        },
98    };
99
100    /// The longest symbol the feed can publish: an OSI contract on a six-character root.
101    const LONGEST_CONTRACT: &str = "GOOGLX261231C01234500";
102
103    #[test]
104    fn the_longest_option_contract_is_filed_inline() {
105        assert_eq!(LONGEST_CONTRACT.len(), 21);
106
107        let id = subscription_id(LONGEST_CONTRACT);
108
109        assert_eq!(id.as_ref(), LONGEST_CONTRACT);
110        assert!(!id.0.is_heap_allocated());
111    }
112
113    /// The instrument map is keyed exactly as the tick decoder keys a frame, so a contract on a
114    /// root of more than three characters is filed inline and resolves.
115    #[test]
116    fn the_instrument_map_files_a_contract_under_its_bare_symbol() {
117        let subscription: Subscription<LseOptions, MarketDataInstrument, PublicTrades> =
118            Subscription::from((
119                LseOptions::default(),
120                "googl",
121                "usd",
122                MarketDataInstrumentKind::Option(MarketDataOptionContract {
123                    kind: OptionKind::Call,
124                    exercise: OptionExercise::American,
125                    expiry: "2026-09-30T20:00:00Z".parse().unwrap(),
126                    strike: dec!(700),
127                }),
128                PublicTrades,
129            ));
130
131        let SubscriptionMeta { instrument_map, .. } =
132            LseSubMapper::map(std::slice::from_ref(&subscription));
133
134        let (id, instrument) = instrument_map.0.iter().next().unwrap();
135        assert_eq!(instrument_map.0.len(), 1);
136        assert_eq!(id, &subscription_id("GOOGL260930C00700000"));
137        assert!(!id.0.is_heap_allocated());
138        assert_eq!(instrument, &subscription.instrument);
139    }
140}