Skip to main content

rustrade_data/exchange/lse/
transformer.rs

1//! The London Strategic Edge stream transformer.
2//!
3//! Decoding is delegated wholesale to [`StatelessTransformer`] — one provider frame maps to one
4//! event and nothing about it needs state. What this type adds is the consuming half of
5//! [`resume`](super::resume): skipping the events a reconnect's replay re-delivers; and, on the
6//! options dataset, the count of prints for contracts nobody registered.
7
8use super::{
9    live::subscribes_per_underlying,
10    resume::{LseResumeKey, LseResumeState, LseWatermark},
11    tick::LseMessage,
12};
13use crate::exchange::osi;
14use crate::{
15    Identifier,
16    error::DataError,
17    event::{MarketEvent, MarketIter},
18    exchange::Connector,
19    subscriber::shared_stream::AttachedTransformer,
20    subscription::{Map, SubscriptionKind},
21    transformer::{ExchangeTransformer, stateless::StatelessTransformer},
22};
23use chrono::{DateTime, Utc};
24use fnv::{FnvHashMap, FnvHashSet};
25use rustrade_instrument::exchange::ExchangeId;
26use rustrade_integration::{
27    Transformer, protocol::websocket::WsMessage, subscription::SubscriptionId,
28};
29use serde::Deserialize;
30use smol_str::SmolStr;
31use std::{
32    cmp::Ordering,
33    collections::BTreeMap,
34    sync::Arc,
35    time::{Duration, Instant},
36};
37use tokio::sync::mpsc;
38use tracing::{info, warn};
39
40/// How often the options dataset reports the prints it dropped for unregistered contracts.
41///
42/// A minute keeps a busy connection to one line per underlying per minute — at one registered
43/// contract on a busy index ETF, that line stands in for thousands of dropped prints.
44pub const UNREGISTERED_REPORT_INTERVAL: Duration = Duration::from_secs(60);
45
46/// Transformer for every London Strategic Edge subscription kind.
47///
48/// Carries no resume state unless the caller opted in via
49/// [`LseSubscriber::with_resume`](super::live::LseSubscriber::with_resume); without it this is a
50/// direct pass-through to [`StatelessTransformer`].
51#[derive(Debug)]
52pub struct LseTransformer<Exchange, InstrumentKey, Kind> {
53    inner: StatelessTransformer<Exchange, InstrumentKey, Kind, LseMessage>,
54    resume: Option<ResumeTracker>,
55
56    /// Present on the options dataset only, where most of what arrives was never registered.
57    unregistered: Option<UnregisteredContracts>,
58}
59
60/// The options dataset's count of prints for contracts nobody registered.
61///
62/// # Why a count and not an error per print
63/// One subscribe delivers an underlying's whole chain, so a stream registering a handful of
64/// contracts receives prints for thousands it did not — measured at around 45 a second on one busy
65/// underlying. Resolved like any other frame, each would become an "unidentifiable" error, drowning
66/// every real one. They are expected, so they are dropped; but silently dropping them would hide
67/// the one case that is a mistake — a registered contract spelled differently from the one that
68/// trades, whose prints would all land here — so they are counted per underlying and reported
69/// every [`UNREGISTERED_REPORT_INTERVAL`].
70#[derive(Debug)]
71struct UnregisteredContracts {
72    exchange: ExchangeId,
73    registered: FnvHashSet<SubscriptionId>,
74
75    /// Ordered so the report lists underlyings in the same order every time.
76    tally: BTreeMap<SmolStr, UnregisteredTally>,
77    window_start: Instant,
78}
79
80/// What one underlying delivered for unregistered contracts since the last report.
81#[derive(Debug, Default)]
82struct UnregisteredTally {
83    prints: u64,
84    contracts: FnvHashSet<SubscriptionId>,
85}
86
87/// What a stream needs to resume: the shared watermarks, the kind they are filed under, and the
88/// replay windows the connection opened for it.
89#[derive(Debug)]
90pub(super) struct ResumeContext {
91    pub(super) state: Arc<LseResumeState>,
92
93    /// [`SubscriptionKind::as_str`] of the stream — see [`LseResumeKey`].
94    pub(super) kind: &'static str,
95
96    /// Each subscription a replay window was opened for, and the instant it opens at.
97    ///
98    /// Only these expect a replay. A subscription the connection already held when the stream
99    /// attached was never re-sent, so the provider replays nothing for it, and expecting one would
100    /// read every live tick as a replay that fell short.
101    pub(super) starts: FnvHashMap<SubscriptionId, DateTime<Utc>>,
102}
103
104/// Per-connection resume bookkeeping: the shared watermark, and what this connection still owes
105/// the replay window.
106#[derive(Debug)]
107struct ResumeTracker {
108    state: Arc<LseResumeState>,
109
110    /// The dataset this stream serves, which partitions the shared state alongside the kind — see
111    /// [`LseResumeKey`]. Held as a value because the transformer's `Exchange` type parameter is
112    /// only reachable where its bounds are in scope, which the per-tick path is not.
113    exchange: ExchangeId,
114
115    /// The kind this stream serves, which partitions the shared state — see
116    /// [`LseResumeKey`]. Held as a string because the transformer's `Kind` type parameter has no
117    /// value to call [`SubscriptionKind::as_str`] on.
118    kind: &'static str,
119
120    /// Keyed by subscription alone: the dataset and the kind are this stream's, so both are
121    /// constant across every entry here.
122    pending: FnvHashMap<SubscriptionId, PendingDrop>,
123}
124
125/// Events still expected to arrive twice for one subscription, and the instant they carry.
126#[derive(Copy, Clone, Debug)]
127struct PendingDrop {
128    time_exchange: DateTime<Utc>,
129    remaining: usize,
130
131    /// Whether the window was opened *before* this stream's own watermark.
132    ///
133    /// A symbol has one replay window per connection, opened at the earliest watermark among the
134    /// streams holding it, so a stream that had delivered further receives ticks it already
135    /// delivered before reaching its own watermark. Those are dropped silently, until the first
136    /// tick past the watermark closes the window. Where the window opened exactly at the watermark,
137    /// an older tick contradicts the inclusive `start` and is reported instead.
138    opened_earlier: bool,
139
140    /// Whether a tick past the watermark has arrived, closing the replay window for good.
141    passed: bool,
142
143    /// Whether the out-of-contract-ordering warning has already fired for this subscription.
144    ///
145    /// A provider replaying from *before* the requested instant would otherwise warn once per
146    /// replayed tick, which on a busy symbol is tens of thousands of identical lines.
147    warned_older: bool,
148
149    /// Whether the provider announced a replay window opening later than the one requested.
150    ///
151    /// A clamp explains a shortfall at the resume instant completely — the window simply does not
152    /// reach it — so the shortfall must not also be reported as the provider's history changing
153    /// under the subscription. Two reports with different causes for one event is worse than one,
154    /// because only one of them is true.
155    ///
156    /// Set when the boundary frame is read, which the provider was measured to send before the
157    /// window it announces. The suppression is therefore ordered, not absolute: see the
158    /// `Ordering::Greater` arm of [`should_drop`](ResumeTracker::should_drop) for what happens
159    /// under the reverse order, and why it is tolerated.
160    clamped: bool,
161}
162
163impl<Exchange, InstrumentKey, Kind> LseTransformer<Exchange, InstrumentKey, Kind>
164where
165    Exchange: Connector + Send,
166    InstrumentKey: Clone + Send + Sync,
167    Kind: SubscriptionKind + Send,
168    Kind::Event: Sync,
169    MarketIter<InstrumentKey, Kind::Event>: From<(ExchangeId, InstrumentKey, LseMessage)>,
170{
171    /// Construct a transformer, optionally resuming from previously delivered events.
172    ///
173    /// `resume` carries the kind this stream serves alongside the shared state, because the state
174    /// is partitioned by dataset *and* kind — see [`LseResumeKey`] — and this type's `Kind`
175    /// parameter has no value to derive the latter from. The dataset comes from `Exchange::ID`.
176    ///
177    /// The drop counters are read here, once per resumed subscription, rather than consulted per
178    /// tick. Recording still takes the shared lock once per emitted tick; the critical section
179    /// there is a hash lookup and a field update.
180    pub(super) async fn new(
181        instrument_map: Map<InstrumentKey>,
182        initial_snapshots: &[MarketEvent<InstrumentKey, Kind::Event>],
183        ws_sink_tx: mpsc::UnboundedSender<WsMessage>,
184        resume: Option<ResumeContext>,
185    ) -> Result<Self, DataError> {
186        let unregistered = subscribes_per_underlying(Exchange::ID).then(|| {
187            UnregisteredContracts::new(Exchange::ID, instrument_map.0.keys().cloned().collect())
188        });
189
190        let inner =
191            StatelessTransformer::init(instrument_map, initial_snapshots, ws_sink_tx).await?;
192
193        Ok(Self {
194            inner,
195            resume: resume.map(Self::tracker),
196            unregistered,
197        })
198    }
199}
200
201impl<Exchange, InstrumentKey, Kind> LseTransformer<Exchange, InstrumentKey, Kind>
202where
203    Exchange: Connector,
204{
205    /// The bookkeeping for resuming from `context`.
206    fn tracker(
207        ResumeContext {
208            state,
209            kind,
210            starts,
211        }: ResumeContext,
212    ) -> ResumeTracker {
213        ResumeTracker {
214            pending: starts
215                .into_iter()
216                .filter_map(|(subscription, start)| {
217                    let key = LseResumeKey::new(Exchange::ID, subscription.clone(), kind);
218                    let watermark = state.watermark(&key)?;
219                    Some((subscription, PendingDrop::new(watermark, start)))
220                })
221                .collect(),
222            state,
223            exchange: Exchange::ID,
224            kind,
225        }
226    }
227}
228
229/// What the shared connection hands a London Strategic Edge stream on attaching: what it needs to
230/// resume, if it resumes.
231///
232/// Built by the connection from the subscriber's resume state and the replay windows it opened for
233/// the stream, and handed to the stream's [`LseTransformer`] before any frame reaches it.
234#[derive(Debug, Default)]
235pub struct LseResume(pub(super) Option<ResumeContext>);
236
237/// Resume from what the connection handed over, if anything.
238///
239/// Replaces any resume state the transformer was built with: the connection's is the one that
240/// knows which replay windows it opened.
241impl<Exchange, InstrumentKey, Kind> AttachedTransformer<LseResume>
242    for LseTransformer<Exchange, InstrumentKey, Kind>
243where
244    Exchange: Connector,
245{
246    fn attached(&mut self, LseResume(resume): LseResume) {
247        self.resume = resume.map(Self::tracker);
248    }
249}
250
251impl PendingDrop {
252    /// What remains to skip of a window opened at `start` for a stream last delivered at
253    /// `watermark`.
254    fn new(watermark: LseWatermark, start: DateTime<Utc>) -> Self {
255        Self {
256            time_exchange: watermark.time_exchange,
257            remaining: watermark.count_at_time,
258            opened_earlier: start < watermark.time_exchange,
259            passed: false,
260            warned_older: false,
261            clamped: false,
262        }
263    }
264}
265
266impl<Exchange, InstrumentKey, Kind> ExchangeTransformer<Exchange, InstrumentKey, Kind>
267    for LseTransformer<Exchange, InstrumentKey, Kind>
268where
269    Exchange: Connector + Send,
270    InstrumentKey: Clone + Send + Sync,
271    Kind: SubscriptionKind + Send,
272    Kind::Event: Sync,
273    MarketIter<InstrumentKey, Kind::Event>: From<(ExchangeId, InstrumentKey, LseMessage)>,
274{
275    /// Initialise without resumption.
276    ///
277    /// This is the trait's only seam and it is a static function, so it cannot carry the caller's
278    /// resume state; the stream that wants resumption bypasses it and constructs the transformer
279    /// directly.
280    ///
281    /// Initial snapshots are forwarded rather than dropped here, so this layer stops being the
282    /// place they are lost. That is as far as forwarding reaches: the inner
283    /// [`StatelessTransformer`]'s own `init` discards the argument unconditionally, as every
284    /// stateless connector's does, and this provider's streams fetch no snapshot for it to
285    /// discard. A future LSE kind that does fetch one would
286    /// need a transformer not backed by `StatelessTransformer` before the snapshot reached it —
287    /// forwarding is what makes that a visible gap rather than one hidden behind a `&[]` at the
288    /// call site.
289    async fn init(
290        instrument_map: Map<InstrumentKey>,
291        initial_snapshots: &[MarketEvent<InstrumentKey, Kind::Event>],
292        ws_sink_tx: mpsc::UnboundedSender<WsMessage>,
293    ) -> Result<Self, DataError> {
294        Self::new(instrument_map, initial_snapshots, ws_sink_tx, None).await
295    }
296}
297
298impl<Exchange, InstrumentKey, Kind> Transformer for LseTransformer<Exchange, InstrumentKey, Kind>
299where
300    Exchange: Connector,
301    InstrumentKey: Clone,
302    Kind: SubscriptionKind,
303    MarketIter<InstrumentKey, Kind::Event>: From<(ExchangeId, InstrumentKey, LseMessage)>,
304{
305    type Error = DataError;
306    type Input = LseMessage;
307    type Output = MarketEvent<InstrumentKey, Kind::Event>;
308    type OutputIter = Vec<Result<Self::Output, Self::Error>>;
309
310    fn transform(&mut self, input: Self::Input) -> Self::OutputIter {
311        // The replay boundary is consumed here rather than forwarded. It is the only evidence that
312        // a requested window was clamped to the provider's retention, and it cannot be caught
313        // during the handshake -- see `LseMessage::ReplayStarted`. No decoder has a market event to
314        // build from it either way.
315        if let LseMessage::ReplayStarted {
316            subscription_id,
317            from,
318        } = &input
319        {
320            let (subscription_id, from) = (subscription_id.clone(), *from);
321
322            if let Some(resume) = self.resume.as_mut() {
323                resume.warn_if_clamped(&subscription_id, from);
324            }
325
326            return vec![];
327        }
328
329        // A rejection. The shared connection consumes these rather than routing them -- one names
330        // no symbol, so there is no stream to route it to -- but one arriving here by any other
331        // route must still be reported: nothing downstream can act on it, and the stream simply
332        // stops producing events for whatever was dropped, so a rejection is the last thing that
333        // may reach the consumer as silence.
334        if let LseMessage::Error { code, message } = &input {
335            warn!(
336                exchange = %Exchange::ID,
337                code = code.as_deref().unwrap_or("unknown"),
338                message = message.as_deref().unwrap_or("no message"),
339                "London Strategic Edge rejected something after the handshake completed; the \
340                 rejection does not name a symbol, so check whether a subscription has gone quiet",
341            );
342
343            return vec![];
344        }
345
346        // A print for an option contract nobody registered: counted, not converted into an error.
347        // The report is checked on every option tick, registered or not, so it keeps its cadence
348        // however few unregistered prints arrive.
349        if let Some(unregistered) = self.unregistered.as_mut()
350            && let LseMessage::Tick(tick) = &input
351        {
352            let dropped = unregistered.absorb(&tick.subscription_id);
353            unregistered.report_if_due(Instant::now());
354
355            if dropped {
356                return vec![];
357            }
358        }
359
360        // Nothing to track when the caller did not opt in, and a control frame carries no instant
361        // to track. Both go straight through.
362        let Some((subscription_id, time_exchange, replay)) =
363            self.resume.as_ref().and_then(|_| match &input {
364                LseMessage::Tick(tick) => Some((
365                    tick.subscription_id.clone(),
366                    tick.time_exchange,
367                    tick.replay,
368                )),
369                _ => None,
370            })
371        else {
372            return self.inner.transform(input);
373        };
374
375        if let Some(resume) = self.resume.as_mut()
376            && resume.should_drop(&subscription_id, time_exchange, replay)
377        {
378            return vec![];
379        }
380
381        let output = self.inner.transform(input);
382
383        // Record against what was actually emitted. An unidentifiable instrument yields an error
384        // rather than an event, and a watermark advanced past an event nobody received would
385        // leave a gap on the next reconnect.
386        if let Some(resume) = self.resume.as_ref()
387            && output.iter().any(Result::is_ok)
388        {
389            resume.record(subscription_id, time_exchange);
390        }
391
392        output
393    }
394}
395
396impl ResumeTracker {
397    /// Whether this event was already delivered by the connection this one replaced.
398    fn should_drop(
399        &mut self,
400        subscription_id: &SubscriptionId,
401        time_exchange: DateTime<Utc>,
402        replay: bool,
403    ) -> bool {
404        let exchange = self.exchange;
405        let Some(pending) = self.pending.get_mut(subscription_id) else {
406            return false;
407        };
408
409        match time_exchange.cmp(&pending.time_exchange) {
410            // Inside the instant the watermark names, and the previous connection is known to have
411            // delivered this many events there. `start` is inclusive, so the provider replays them
412            // all and they are skipped by position.
413            Ordering::Equal if pending.remaining > 0 => {
414                pending.remaining -= 1;
415
416                // The skip stays positional and does NOT require `replay`, deliberately: the flag
417                // is absent on live ticks, so a provider that stopped stamping replayed ones would
418                // turn every reconnect into a duplicate flood if the drop depended on it. But a
419                // tick arriving unstamped inside the window is a live event being discarded as a
420                // duplicate, which only happens if the replay under-delivered at this instant, so
421                // it is reported. Bounded by the count the watermark carries.
422                if !replay {
423                    warn!(
424                        %exchange,
425                        subscription = %subscription_id,
426                        instant = %time_exchange,
427                        "London Strategic Edge delivered an unreplayed tick inside the resume \
428                         window and it was skipped as an already-delivered duplicate; a genuinely \
429                         new tick may have been lost",
430                    );
431                }
432
433                true
434            }
435            // The instant is exhausted; anything further carrying it is genuinely new.
436            Ordering::Equal => false,
437            Ordering::Greater => {
438                let undelivered = pending.remaining;
439                let instant = pending.time_exchange;
440
441                // The window is closed for good, but the entry is KEPT. It carries the instant that
442                // went out as `start`, which is the only record of what was requested, and the
443                // clamp check reads it out of here when the boundary frame arrives -- removing it
444                // would make a clamp undetectable for any subscription whose first replayed tick
445                // beat its own `replay_started`. Zeroing the count closes the drop window just as
446                // removal did: `Ordering::Equal` with nothing remaining emits.
447                pending.remaining = 0;
448                pending.passed = true;
449
450                // The replay held fewer events at the watermark's instant than were delivered from
451                // it. Positional skipping assumes the replay reproduces what went out live -- an
452                // assumption that holds on every measurement taken, including one instant carrying
453                // 141 events -- so a shortfall means the provider's history changed underneath the
454                // subscription. No event is lost by it, but the assumption is load-bearing enough
455                // that its failure must be visible rather than inferred from a gap much later.
456                //
457                // Unless the window was clamped, which explains the shortfall completely: the
458                // window does not reach the resume instant at all, so of course nothing replayed at
459                // it. The clamp is reported with the right cause by `warn_if_clamped`.
460                //
461                // `clamped` is only set once the boundary frame has been seen, so the suppression
462                // holds for the measured order -- boundary first -- and not for its reverse. Should
463                // a replayed tick beat its own boundary frame, this warning fires before the clamp
464                // is known and BOTH lines appear, of which only the clamp's is true. That is
465                // tolerated rather than closed: the alternative is to defer this warning until
466                // something proves no clamp is coming, and the only frame that could prove it is
467                // `replay_complete`, whose delivery and ordering are unmeasured -- so deferring
468                // would risk losing a true report entirely to avoid an extra false one. A redundant
469                // line is the cheaper failure.
470                if undelivered > 0 && !pending.clamped {
471                    warn!(
472                        %exchange,
473                        subscription = %subscription_id,
474                        instant = %instant,
475                        undelivered,
476                        "London Strategic Edge replay held fewer ticks at the resume instant than \
477                         were delivered from it; the provider's history changed under the resume",
478                    );
479                }
480
481                false
482            }
483            // Older than the watermark, inside a window the connection opened earlier than it on
484            // another stream's behalf: this stream delivered it before the connection was lost.
485            Ordering::Less if pending.opened_earlier && !pending.passed => true,
486            // Older than the watermark. `start` is inclusive, so the provider should never send
487            // this; emitting is the safe answer, since dropping would discard a real event on the
488            // strength of an assumption that has already been contradicted.
489            Ordering::Less => {
490                // The mirror image of the shortfall above, and the same load-bearing assumption
491                // contradicted in the other direction -- most plausibly a `start` filter flooring
492                // to a coarser resolution than the microseconds it was given. Its consequence is
493                // worse, because everything between the served instant and the watermark is a
494                // duplicate delivered into consumer state, so it must not be the quiet one. Fired
495                // once per subscription: the condition holds for every replayed tick below the
496                // watermark, and on a busy symbol that is tens of thousands of them.
497                //
498                // The entry now outlives the replay window -- it is kept so a late boundary frame
499                // can still be recognised as a clamp -- so this arm is also reachable long after
500                // the replay ended, for a live tick stamped before the resume instant. That is a
501                // different cause with the same consequence, and the wording names neither rather
502                // than asserting the wrong one.
503                if !pending.warned_older {
504                    pending.warned_older = true;
505
506                    warn!(
507                        %exchange,
508                        subscription = %subscription_id,
509                        instant = %time_exchange,
510                        watermark = %pending.time_exchange,
511                        "London Strategic Edge delivered a tick older than the resume instant, \
512                         which an inclusive `start` should make impossible during the replay and \
513                         a monotonic feed should make impossible after it; it is emitted rather \
514                         than dropped, so expect duplicates back to the instant shown",
515                    );
516                }
517
518                false
519            }
520        }
521    }
522
523    /// Warn when the provider opened a replay window later than the one asked for.
524    ///
525    /// # ⚠️ Silent clamping is the failure this exists to surface
526    /// The provider retains 24 hours. A `start` older than that is **moved forward and served with
527    /// no error** — a window requested 48 hours back was answered with one beginning exactly 24
528    /// hours back. The `from` of the [`ReplayStarted`](LseMessage::ReplayStarted) frame is the only
529    /// signal that it happened.
530    ///
531    /// The watermark is what went out as `start`, so it *is* the requested instant and no separate
532    /// record of the request is needed. A window opened exactly at it is not a clamp: `start` is
533    /// inclusive, so that is precisely the window asked for.
534    ///
535    /// A detected clamp is also recorded against the subscription, because it explains away the
536    /// shortfall [`should_drop`](Self::should_drop) would otherwise report with a different and
537    /// wrong cause.
538    fn warn_if_clamped(&mut self, subscription_id: &SubscriptionId, from: DateTime<Utc>) {
539        let Some(requested) = self.clamped_from(subscription_id, from) else {
540            return;
541        };
542
543        if let Some(pending) = self.pending.get_mut(subscription_id) {
544            pending.clamped = true;
545        }
546
547        warn!(
548            exchange = %self.exchange,
549            subscription = %subscription_id,
550            requested = %requested,
551            serving_from = %from,
552            "London Strategic Edge clamped the requested replay window; events between the two \
553             instants are beyond the provider's retention and are permanently lost",
554        );
555    }
556
557    /// The instant a window was requested from, if the provider opened it later than that.
558    ///
559    /// `None` for a subscription this stream holds no watermark for — one that asked for no window.
560    /// An entry is never removed once made, so a boundary frame that arrives *after* the replay has
561    /// already passed the watermark is still answered correctly: the provider is measured to
562    /// announce the window first, on both a crypto and an FX symbol, but under a clamp every
563    /// replayed tick sits past the watermark by definition, so a single reordered frame would
564    /// otherwise be enough to lose the only evidence the window was cut short.
565    fn clamped_from(
566        &self,
567        subscription_id: &SubscriptionId,
568        from: DateTime<Utc>,
569    ) -> Option<DateTime<Utc>> {
570        let requested = self.pending.get(subscription_id)?.time_exchange;
571
572        // A window opened *at* the requested instant is not a clamp: `start` is inclusive, so that
573        // is precisely the window that was asked for, and warning on it would make this fire on
574        // every ordinary resume.
575        (from > requested).then_some(requested)
576    }
577
578    /// Advance the shared watermark for an event this stream emitted.
579    fn record(&self, subscription_id: SubscriptionId, time_exchange: DateTime<Utc>) {
580        self.state.record(
581            &LseResumeKey::new(self.exchange, subscription_id, self.kind),
582            time_exchange,
583        );
584    }
585}
586
587impl UnregisteredContracts {
588    fn new(exchange: ExchangeId, registered: FnvHashSet<SubscriptionId>) -> Self {
589        Self {
590            exchange,
591            registered,
592            tally: BTreeMap::new(),
593            window_start: Instant::now(),
594        }
595    }
596
597    /// Count a print if nobody registered its contract. `true` means it was counted and must be
598    /// dropped; `false` means it is registered and passes through.
599    fn absorb(&mut self, subscription_id: &SubscriptionId) -> bool {
600        if self.registered.contains(subscription_id) {
601            return false;
602        }
603
604        // A subscription identifier is the symbol, and every symbol on this channel is an OSI
605        // contract, so the root is always found; were one not, it is tallied under itself rather
606        // than lost.
607        let symbol = subscription_id.as_ref();
608        let underlying = osi::root(symbol).unwrap_or(symbol);
609
610        // Looked up before inserting so the common case -- an underlying already tallied this
611        // window -- builds no owned key.
612        let tally = match self.tally.get_mut(underlying) {
613            Some(tally) => tally,
614            None => self.tally.entry(SmolStr::new(underlying)).or_default(),
615        };
616        tally.prints += 1;
617        tally.contracts.insert(subscription_id.clone());
618
619        true
620    }
621
622    /// Report what was counted and start a new window, if the current one has run its course.
623    fn report_if_due(&mut self, now: Instant) {
624        let window = now.saturating_duration_since(self.window_start);
625        if window < UNREGISTERED_REPORT_INTERVAL {
626            return;
627        }
628
629        for (underlying, tally) in std::mem::take(&mut self.tally) {
630            info!(
631                exchange = %self.exchange,
632                %underlying,
633                prints = tally.prints,
634                contracts = tally.contracts.len(),
635                window_secs = window.as_secs(),
636                "London Strategic Edge delivered option prints for contracts nobody registered, \
637                 and dropped them; register a contract to receive it, and if one you registered on \
638                 this underlying is silent, check its OSI spelling, since a misspelled contract's \
639                 prints are counted here",
640            );
641        }
642
643        self.window_start = now;
644    }
645}
646
647/// Bound required by [`StatelessTransformer`]'s decode path, restated here so this module's own
648/// bounds stay legible.
649const _: fn() = || {
650    fn assert_identifiable<T: Identifier<Option<SubscriptionId>> + for<'de> Deserialize<'de>>() {}
651    assert_identifiable::<LseMessage>();
652};
653
654#[cfg(test)]
655#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
656mod tests {
657    use super::*;
658    use crate::{exchange::lse::LseCrypto, subscription::trade::PublicTrades};
659    use rustrade_integration::subscription::SubscriptionId;
660
661    type Subject = LseTransformer<LseCrypto, u8, PublicTrades>;
662
663    /// The kind these tests serve, and the partition their watermarks are filed under.
664    const KIND: &str = "public_trades";
665
666    fn id() -> SubscriptionId {
667        super::super::mapper::subscription_id("BTC/USD")
668    }
669
670    fn key() -> LseResumeKey {
671        LseResumeKey::new(ExchangeId::LseCrypto, id(), KIND)
672    }
673
674    /// A live tick, which carries no `replay` key at all.
675    fn tick(ts: &str, price: &str) -> LseMessage {
676        serde_json::from_str(&format!(
677            r#"{{"type":"tick","symbol":"BTC/USD","ts":"{ts}","price":{price},
678                "bid":{price},"ask":42001.0,"volume":0.00155}}"#
679        ))
680        .unwrap()
681    }
682
683    /// A tick served from the provider's replay buffer, which the provider stamps.
684    fn replayed_tick(ts: &str, price: &str) -> LseMessage {
685        serde_json::from_str(&format!(
686            r#"{{"type":"tick","symbol":"BTC/USD","ts":"{ts}","replay":true,"price":{price},
687                "bid":{price},"ask":42001.0,"volume":0.00155}}"#
688        ))
689        .unwrap()
690    }
691
692    /// A stream on a connection of its own: any window opens exactly at its own watermark.
693    async fn subject(resume: Option<Arc<LseResumeState>>) -> Subject {
694        let starts = resume
695            .as_ref()
696            .and_then(|state| state.watermark(&key()))
697            .map(|watermark| (id(), watermark.time_exchange));
698
699        subject_with(resume, starts).await
700    }
701
702    /// A stream whose window for [`id`] the connection opened at `start`, or never opened.
703    async fn subject_with(
704        resume: Option<Arc<LseResumeState>>,
705        start: Option<(SubscriptionId, DateTime<Utc>)>,
706    ) -> Subject {
707        let map = Map([(id(), 1_u8)]
708            .into_iter()
709            .collect::<FnvHashMap<SubscriptionId, u8>>());
710        let (tx, _rx) = mpsc::unbounded_channel();
711
712        let resume = resume.map(|state| ResumeContext {
713            state,
714            kind: KIND,
715            starts: start.into_iter().collect(),
716        });
717
718        Subject::new(map, &[], tx, resume).await.unwrap()
719    }
720
721    /// A symbol has one replay window per connection, opened at the earliest watermark among the
722    /// streams holding it. A stream that had delivered further drops what it already delivered —
723    /// silently, and exactly — and emits everything past its own watermark.
724    #[tokio::test]
725    async fn a_window_opened_before_the_watermark_drops_what_this_stream_already_delivered() {
726        let state = Arc::new(LseResumeState::new());
727
728        let mut first = subject(Some(Arc::clone(&state))).await;
729        first.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
730        first.transform(tick("2026-01-02T09:00:00.000002Z", "2.0"));
731        first.transform(tick("2026-01-02T09:00:00.000002Z", "3.0"));
732
733        // Another stream on the connection resumes from further back.
734        let mut second = subject_with(
735            Some(Arc::clone(&state)),
736            Some((id(), at("2026-01-02T09:00:00Z"))),
737        )
738        .await;
739
740        for (ts, price) in [
741            ("2026-01-02T09:00:00.000000Z", "0.5"),
742            ("2026-01-02T09:00:00.000001Z", "1.0"),
743            ("2026-01-02T09:00:00.000002Z", "2.0"),
744            ("2026-01-02T09:00:00.000002Z", "3.0"),
745        ] {
746            assert!(
747                second.transform(replayed_tick(ts, price)).is_empty(),
748                "{ts} was delivered before the connection was lost"
749            );
750        }
751
752        assert_eq!(
753            second
754                .transform(replayed_tick("2026-01-02T09:00:00.000003Z", "4.0"))
755                .len(),
756            1,
757            "the first tick past this stream's own watermark is new"
758        );
759    }
760
761    /// The silent drop covers the replay window only. Once a tick passes the watermark, an older
762    /// one is the monotonicity contradiction it always was: emitted, not swallowed.
763    #[tokio::test]
764    async fn an_older_tick_after_the_window_closes_is_emitted_even_when_it_opened_earlier() {
765        let state = Arc::new(LseResumeState::new());
766
767        let mut first = subject(Some(Arc::clone(&state))).await;
768        first.transform(tick("2026-01-02T09:00:00.000002Z", "1.0"));
769
770        let mut second = subject_with(
771            Some(Arc::clone(&state)),
772            Some((id(), at("2026-01-02T09:00:00Z"))),
773        )
774        .await;
775        assert_eq!(
776            second
777                .transform(tick("2026-01-02T09:00:00.000003Z", "2.0"))
778                .len(),
779            1
780        );
781        assert_eq!(
782            second
783                .transform(tick("2026-01-02T09:00:00.000001Z", "0.5"))
784                .len(),
785            1,
786            "the window is closed; an older tick must be emitted, not dropped"
787        );
788    }
789
790    /// A stream joining a symbol the connection already streams is given no replay, so it must not
791    /// expect one: nothing is skipped at its watermark, and nothing is reported as a shortfall.
792    #[tokio::test]
793    async fn a_subscription_with_no_window_opened_skips_nothing() {
794        let state = Arc::new(LseResumeState::new());
795
796        let mut first = subject(Some(Arc::clone(&state))).await;
797        first.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
798
799        let mut joined = subject_with(Some(Arc::clone(&state)), None).await;
800        assert!(joined.resume.as_ref().unwrap().pending.is_empty());
801        assert_eq!(
802            joined
803                .transform(tick("2026-01-02T09:00:00.000001Z", "1.0"))
804                .len(),
805            1
806        );
807    }
808
809    #[tokio::test]
810    async fn without_resume_every_tick_is_emitted() {
811        let mut subject = subject(None).await;
812
813        assert_eq!(
814            subject.transform(tick("2026-01-02T09:00:00Z", "1.0")).len(),
815            1
816        );
817        assert_eq!(
818            subject.transform(tick("2026-01-02T09:00:00Z", "1.0")).len(),
819            1
820        );
821    }
822
823    #[tokio::test]
824    async fn with_resume_the_watermark_advances_on_emitted_ticks() {
825        let state = Arc::new(LseResumeState::new());
826        let mut subject = subject(Some(Arc::clone(&state))).await;
827
828        subject.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
829        subject.transform(tick("2026-01-02T09:00:00.000001Z", "2.0"));
830
831        let watermark = state.watermark(&key()).unwrap();
832        assert_eq!(
833            watermark.time_exchange,
834            "2026-01-02T09:00:00.000001Z"
835                .parse::<DateTime<Utc>>()
836                .unwrap()
837        );
838        assert_eq!(watermark.count_at_time, 2);
839    }
840
841    /// The core of the mechanism: a reconnect replays the whole of the watermark's instant because
842    /// `start` is inclusive, and exactly the already-delivered prefix is skipped.
843    #[tokio::test]
844    async fn a_replay_skips_exactly_the_ticks_already_delivered_at_the_resume_instant() {
845        let state = Arc::new(LseResumeState::new());
846
847        // First connection delivers three ticks sharing one instant.
848        let mut first = subject(Some(Arc::clone(&state))).await;
849        for price in ["1.0", "2.0", "3.0"] {
850            first.transform(tick("2026-01-02T09:00:00.000001Z", price));
851        }
852        assert_eq!(state.watermark(&key()).unwrap().count_at_time, 3);
853
854        // Reconnect: the provider replays all three, then a fourth that is genuinely new.
855        let mut second = subject(Some(Arc::clone(&state))).await;
856        for price in ["1.0", "2.0", "3.0"] {
857            assert!(
858                second
859                    .transform(replayed_tick("2026-01-02T09:00:00.000001Z", price))
860                    .is_empty(),
861                "an already-delivered tick must not be emitted twice"
862            );
863        }
864
865        assert_eq!(
866            second
867                .transform(tick("2026-01-02T09:00:00.000001Z", "4.0"))
868                .len(),
869            1,
870            "a fourth tick at the same instant is new and must be emitted"
871        );
872        assert_eq!(state.watermark(&key()).unwrap().count_at_time, 4);
873    }
874
875    /// The gap half of the contract: everything after the resume instant is new, always.
876    #[tokio::test]
877    async fn a_replay_emits_every_tick_past_the_resume_instant() {
878        let state = Arc::new(LseResumeState::new());
879
880        let mut first = subject(Some(Arc::clone(&state))).await;
881        first.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
882
883        let mut second = subject(Some(Arc::clone(&state))).await;
884        assert!(
885            second
886                .transform(replayed_tick("2026-01-02T09:00:00.000001Z", "1.0"))
887                .is_empty()
888        );
889        assert_eq!(
890            second
891                .transform(tick("2026-01-02T09:00:00.000002Z", "2.0"))
892                .len(),
893            1,
894            "one microsecond later is past the resume instant and must be emitted"
895        );
896    }
897
898    /// The drop window closes for good once the instant is passed, so a later tick that happens to
899    /// carry the resume instant again cannot be swallowed.
900    #[tokio::test]
901    async fn the_drop_window_does_not_reopen_after_the_resume_instant_is_passed() {
902        let state = Arc::new(LseResumeState::new());
903
904        let mut first = subject(Some(Arc::clone(&state))).await;
905        first.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
906        first.transform(tick("2026-01-02T09:00:00.000001Z", "2.0"));
907
908        let mut second = subject(Some(Arc::clone(&state))).await;
909        // Replay is short by one, then moves past the instant -- the window closes here.
910        assert!(
911            second
912                .transform(replayed_tick("2026-01-02T09:00:00.000001Z", "1.0"))
913                .is_empty()
914        );
915        assert_eq!(
916            second
917                .transform(tick("2026-01-02T09:00:00.000002Z", "2.0"))
918                .len(),
919            1
920        );
921        assert_eq!(
922            second
923                .transform(tick("2026-01-02T09:00:00.000001Z", "3.0"))
924                .len(),
925            1,
926            "the window is closed; a tick at the old instant must be emitted, not dropped"
927        );
928    }
929
930    /// A subscription with no history resumes as a fresh subscription would.
931    #[tokio::test]
932    async fn a_subscription_with_no_watermark_drops_nothing() {
933        let state = Arc::new(LseResumeState::new());
934        let mut subject = subject(Some(state)).await;
935
936        assert_eq!(
937            subject.transform(tick("2026-01-02T09:00:00Z", "1.0")).len(),
938            1
939        );
940    }
941
942    #[tokio::test]
943    async fn a_control_frame_is_neither_emitted_nor_recorded() {
944        let state = Arc::new(LseResumeState::new());
945        let mut subject = subject(Some(Arc::clone(&state))).await;
946
947        let frame: LseMessage =
948            serde_json::from_str(r#"{"type":"replay_complete","symbol":"BTC/USD","rows":41}"#)
949                .unwrap();
950
951        assert!(subject.transform(frame).is_empty());
952        assert_eq!(state.watermark(&key()), None);
953    }
954
955    /// The no-dedupe lock, restated at the transformer: identical repeats are genuine distinct
956    /// prints and only the resume prefix may ever be skipped.
957    #[tokio::test]
958    async fn identical_live_ticks_are_never_deduplicated() {
959        let state = Arc::new(LseResumeState::new());
960        let mut subject = subject(Some(state)).await;
961
962        let first = subject.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
963        let second = subject.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
964
965        assert_eq!(first.len(), 1);
966        assert_eq!(second.len(), 1, "an identical repeat is a distinct print");
967    }
968
969    /// An inclusive `start` says this cannot happen, so it means the assumption behind positional
970    /// skipping has been contradicted — most plausibly a `start` filter flooring to a coarser
971    /// resolution. Emitting is the safe answer: dropping would discard a real event on the strength
972    /// of an assumption already known to be wrong. A `warn!` carries the signal.
973    #[tokio::test]
974    async fn a_replayed_tick_older_than_the_resume_instant_is_emitted_rather_than_dropped() {
975        let state = Arc::new(LseResumeState::new());
976
977        let mut first = subject(Some(Arc::clone(&state))).await;
978        first.transform(tick("2026-01-02T09:00:00.000002Z", "1.0"));
979
980        let mut second = subject(Some(Arc::clone(&state))).await;
981        assert_eq!(
982            second
983                .transform(replayed_tick("2026-01-02T09:00:00.000001Z", "0.5"))
984                .len(),
985            1,
986            "a tick below the watermark must be emitted, not silently swallowed"
987        );
988
989        // The window itself is untouched by it: the tick at the watermark is still skipped.
990        assert!(
991            second
992                .transform(replayed_tick("2026-01-02T09:00:00.000002Z", "1.0"))
993                .is_empty()
994        );
995    }
996
997    /// The skip stays positional rather than conditioning on `replay`, because the flag is absent
998    /// on live ticks: a provider that stopped stamping replayed ones would otherwise turn every
999    /// reconnect into a duplicate flood. The cost is this case, which is reported by a `warn!`
1000    /// rather than changed.
1001    #[tokio::test]
1002    async fn an_unstamped_tick_inside_the_resume_window_is_still_skipped() {
1003        let state = Arc::new(LseResumeState::new());
1004
1005        let mut first = subject(Some(Arc::clone(&state))).await;
1006        first.transform(tick("2026-01-02T09:00:00.000001Z", "1.0"));
1007
1008        let mut second = subject(Some(Arc::clone(&state))).await;
1009        assert!(
1010            second
1011                .transform(tick("2026-01-02T09:00:00.000001Z", "1.0"))
1012                .is_empty()
1013        );
1014    }
1015
1016    /// A resumed subject whose `pending` holds a watermark at `instant`, as a reconnect's would.
1017    async fn resumed_at(state: &Arc<LseResumeState>, instant: &str) -> Subject {
1018        let mut first = subject(Some(Arc::clone(state))).await;
1019        first.transform(tick(instant, "1.0"));
1020
1021        subject(Some(Arc::clone(state))).await
1022    }
1023
1024    fn at(spelling: &str) -> DateTime<Utc> {
1025        spelling.parse::<DateTime<Utc>>().unwrap()
1026    }
1027
1028    /// The boundary frame announcing a window opening at `from`.
1029    fn boundary(from: &str) -> LseMessage {
1030        serde_json::from_str(&format!(
1031            r#"{{"type":"replay_started","symbol":"BTC/USD","from":"{from}"}}"#
1032        ))
1033        .unwrap()
1034    }
1035
1036    /// This subject's per-connection bookkeeping for the one subscription these tests use.
1037    fn pending_for(subject: &Subject) -> PendingDrop {
1038        *subject
1039            .resume
1040            .as_ref()
1041            .unwrap()
1042            .pending
1043            .get(&id())
1044            .unwrap_or_else(|| panic!("the resumed subject must hold an entry for {}", id()))
1045    }
1046
1047    /// The provider retains 24 hours and moves an older `start` forward with no error, so the
1048    /// boundary frame's `from` is the only evidence the requested window was not the served one.
1049    #[tokio::test]
1050    async fn a_replay_window_opened_later_than_requested_is_reported() {
1051        let state = Arc::new(LseResumeState::new());
1052        let subject = resumed_at(&state, "2026-08-12T10:00:00Z").await;
1053
1054        assert_eq!(
1055            subject
1056                .resume
1057                .as_ref()
1058                .unwrap()
1059                .clamped_from(&id(), at("2026-08-13T10:00:00Z")),
1060            Some(at("2026-08-12T10:00:00Z")),
1061        );
1062    }
1063
1064    /// `start` is inclusive, so a window opened exactly where it was asked for is the window that
1065    /// was asked for — reporting it would make the check noise on every ordinary resume.
1066    #[tokio::test]
1067    async fn a_replay_window_opened_where_requested_is_not_a_clamp() {
1068        let state = Arc::new(LseResumeState::new());
1069        let subject = resumed_at(&state, "2026-08-14T10:16:55.161234Z").await;
1070
1071        assert_eq!(
1072            subject
1073                .resume
1074                .as_ref()
1075                .unwrap()
1076                .clamped_from(&id(), at("2026-08-14T10:16:55.161234Z")),
1077            None,
1078        );
1079    }
1080
1081    /// A subscription that asked for no window cannot have had one clamped.
1082    #[tokio::test]
1083    async fn a_replay_boundary_for_a_subscription_with_no_watermark_is_ignored() {
1084        let state = Arc::new(LseResumeState::new());
1085        let subject = subject(Some(state)).await;
1086
1087        assert_eq!(
1088            subject
1089                .resume
1090                .as_ref()
1091                .unwrap()
1092                .clamped_from(&id(), at("2026-08-13T10:00:00Z")),
1093            None,
1094        );
1095    }
1096
1097    /// A clamp explains the shortfall at the resume instant completely — the window does not reach
1098    /// it — so the shortfall must not *also* be reported as the provider's history changing under
1099    /// the subscription. Two reports with different causes for one event is worse than one, because
1100    /// only one of them is true.
1101    #[tokio::test]
1102    async fn a_clamped_window_is_not_also_reported_as_a_changed_history() {
1103        let state = Arc::new(LseResumeState::new());
1104        let mut subject = resumed_at(&state, "2026-08-12T10:00:00Z").await;
1105
1106        // The provider answers with a window opening a day later than asked for.
1107        subject.transform(boundary("2026-08-13T10:00:00Z"));
1108
1109        let pending = pending_for(&subject);
1110        assert!(
1111            pending.clamped,
1112            "the clamp must be recorded, not only logged"
1113        );
1114        assert_eq!(
1115            pending.remaining, 1,
1116            "the drop window is still open until a tick passes the resume instant",
1117        );
1118
1119        // Every replayed tick under a clamp sits past the watermark by definition, so the first one
1120        // closes the window with the count unspent. That shortfall has one cause and it is the
1121        // clamp.
1122        assert_eq!(
1123            subject
1124                .transform(replayed_tick("2026-08-13T10:00:00.000001Z", "1.0"))
1125                .len(),
1126            1,
1127        );
1128
1129        let pending = pending_for(&subject);
1130        assert_eq!(pending.remaining, 0, "the drop window must be closed");
1131        assert!(
1132            pending.clamped,
1133            "the clamp flag is what suppresses the wrong-cause report and must survive the frame \
1134             that closes the window",
1135        );
1136    }
1137
1138    /// The boundary frame is measured to arrive first, but a single reordered frame must not lose
1139    /// the only evidence the window was cut short. The entry is therefore kept rather than removed
1140    /// once the replay passes the watermark, so the clamp is still detected when the first replayed
1141    /// tick beats its own boundary frame.
1142    #[tokio::test]
1143    async fn a_clamp_is_still_detected_when_a_replayed_tick_beats_the_boundary_frame() {
1144        let state = Arc::new(LseResumeState::new());
1145        let mut subject = resumed_at(&state, "2026-08-12T10:00:00Z").await;
1146
1147        // The tick arrives first and closes the drop window, as every tick under a clamp does.
1148        assert_eq!(
1149            subject
1150                .transform(replayed_tick("2026-08-13T10:00:00.000001Z", "1.0"))
1151                .len(),
1152            1,
1153        );
1154
1155        // The boundary frame follows. The requested instant must still be readable.
1156        assert_eq!(
1157            subject
1158                .resume
1159                .as_ref()
1160                .unwrap()
1161                .clamped_from(&id(), at("2026-08-13T10:00:00Z")),
1162            Some(at("2026-08-12T10:00:00Z")),
1163            "the requested instant is the only record of what was asked for and must outlive the \
1164             drop window",
1165        );
1166    }
1167
1168    /// A rejection raised after the handshake stopped reading carries no instrument and no instant.
1169    /// It is consumed for its `warn!` and must not reach a decoder or the watermark.
1170    #[tokio::test]
1171    async fn a_rejection_yields_no_market_event_and_does_not_advance_the_watermark() {
1172        let state = Arc::new(LseResumeState::new());
1173        let mut subject = subject(Some(Arc::clone(&state))).await;
1174
1175        let rejection: LseMessage = serde_json::from_str(
1176            r#"{"type":"error","code":"INVALID_START","message":"Invalid start"}"#,
1177        )
1178        .unwrap();
1179
1180        assert!(subject.transform(rejection).is_empty());
1181        assert_eq!(state.watermark(&key()), None);
1182    }
1183
1184    /// The frame is the only evidence a requested window was clamped, and it reaches the stream
1185    /// rather than the handshake. It must yield no market event of its own.
1186    mod unregistered_contracts {
1187        use super::*;
1188        use crate::exchange::lse::LseOptions;
1189
1190        type OptionsSubject = LseTransformer<LseOptions, u8, PublicTrades>;
1191
1192        const REGISTERED: &str = "SPY260930C00700000";
1193
1194        fn option_tick(symbol: &str) -> LseMessage {
1195            serde_json::from_str(&format!(
1196                r#"{{"type":"tick","symbol":"{symbol}","ts":"2026-01-02T15:00:00+00:00",
1197                    "price":1.25,"bid":null,"ask":null,"volume":3,"name":"label"}}"#
1198            ))
1199            .unwrap()
1200        }
1201
1202        async fn options_subject() -> OptionsSubject {
1203            let map = Map([(
1204                super::super::super::mapper::subscription_id(REGISTERED),
1205                1_u8,
1206            )]
1207            .into_iter()
1208            .collect::<FnvHashMap<SubscriptionId, u8>>());
1209            let (tx, _rx) = mpsc::unbounded_channel();
1210
1211            OptionsSubject::new(map, &[], tx, None).await.unwrap()
1212        }
1213
1214        fn tally(subject: &OptionsSubject) -> Vec<(String, u64, usize)> {
1215            subject
1216                .unregistered
1217                .as_ref()
1218                .unwrap()
1219                .tally
1220                .iter()
1221                .map(|(underlying, tally)| {
1222                    (underlying.to_string(), tally.prints, tally.contracts.len())
1223                })
1224                .collect()
1225        }
1226
1227        #[tokio::test]
1228        async fn a_registered_contract_is_emitted() {
1229            let mut subject = options_subject().await;
1230
1231            let output = subject.transform(option_tick(REGISTERED));
1232            assert_eq!(output.len(), 1);
1233            assert!(output[0].is_ok());
1234            assert!(tally(&subject).is_empty());
1235        }
1236
1237        /// The chain delivers thousands of contracts nobody asked for. Each one surfaced as an
1238        /// error would bury every real error on the stream.
1239        #[tokio::test]
1240        async fn an_unregistered_contract_is_counted_rather_than_raised() {
1241            let mut subject = options_subject().await;
1242
1243            assert!(
1244                subject
1245                    .transform(option_tick("SPY260930C00701000"))
1246                    .is_empty()
1247            );
1248            assert!(
1249                subject
1250                    .transform(option_tick("SPY260930C00701000"))
1251                    .is_empty()
1252            );
1253            assert!(
1254                subject
1255                    .transform(option_tick("SPY260930P00650000"))
1256                    .is_empty()
1257            );
1258            assert!(
1259                subject
1260                    .transform(option_tick("QQQ260930C00500000"))
1261                    .is_empty()
1262            );
1263
1264            assert_eq!(
1265                tally(&subject),
1266                [("QQQ".to_owned(), 1, 1), ("SPY".to_owned(), 3, 2)]
1267            );
1268        }
1269
1270        #[tokio::test]
1271        async fn the_count_is_kept_until_the_interval_has_run() {
1272            let mut subject = options_subject().await;
1273            subject.transform(option_tick("SPY260930C00701000"));
1274
1275            let unregistered = subject.unregistered.as_mut().unwrap();
1276            let opened = unregistered.window_start;
1277
1278            unregistered.report_if_due(opened + UNREGISTERED_REPORT_INTERVAL / 2);
1279            assert_eq!(tally(&subject).len(), 1);
1280        }
1281
1282        #[tokio::test]
1283        async fn a_report_starts_a_new_window() {
1284            let mut subject = options_subject().await;
1285            subject.transform(option_tick("SPY260930C00701000"));
1286
1287            let unregistered = subject.unregistered.as_mut().unwrap();
1288            let due = unregistered.window_start + UNREGISTERED_REPORT_INTERVAL;
1289
1290            unregistered.report_if_due(due);
1291            assert!(tally(&subject).is_empty());
1292            assert_eq!(subject.unregistered.as_ref().unwrap().window_start, due);
1293        }
1294
1295        /// Only the options dataset fans out, so only it counts. Elsewhere an unregistered symbol
1296        /// is a genuine mismatch and stays an error.
1297        #[tokio::test]
1298        async fn other_datasets_keep_no_count() {
1299            assert!(subject(None).await.unregistered.is_none());
1300        }
1301    }
1302
1303    #[tokio::test]
1304    async fn a_replay_boundary_is_consumed_without_emitting_an_event() {
1305        let state = Arc::new(LseResumeState::new());
1306        let mut subject = subject(Some(Arc::clone(&state))).await;
1307
1308        let boundary: LseMessage = serde_json::from_str(
1309            r#"{"type":"replay_started","symbol":"BTC/USD","from":"2026-01-02T09:00:00Z"}"#,
1310        )
1311        .unwrap();
1312
1313        assert!(subject.transform(boundary).is_empty());
1314        assert_eq!(
1315            state.watermark(&key()),
1316            None,
1317            "a boundary frame must not advance the watermark"
1318        );
1319    }
1320}