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}