rustrade_execution/fill_recovery.rs
1//! What an account stream reports when its fill recovery gives up on a span of fills.
2//!
3//! Venues that reconnect internally (Binance Spot and Margin, Alpaca) read the fills they missed
4//! while disconnected by REST after each reconnect. When that read fails for good, the fills in
5//! its span never reach the consumer, and the stream says so in-band with
6//! [`AccountEventKind::FillRecoveryGaveUp`](crate::AccountEventKind::FillRecoveryGaveUp), carrying
7//! a [`FillRecoveryGap`].
8
9use chrono::{DateTime, Utc};
10use derive_more::Constructor;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14/// A span of fills an account stream's recovery gave up on: fills in it may not have been
15/// delivered.
16///
17/// Sent as [`AccountEventKind::FillRecoveryGaveUp`](crate::AccountEventKind::FillRecoveryGaveUp),
18/// once per failed read. The library does not act on it beyond reporting it. What to do is the
19/// consumer's policy: reconcile, alert, or halt.
20///
21/// # Reconciling
22///
23/// Read the span with
24/// [`ExecutionClient::fetch_trades`](crate::client::ExecutionClient::fetch_trades), passing
25/// `start`, `end` and [`FillRecoveryScope::instrument_filter`], and read on from each call's
26/// [`resume`](crate::trade::TradesRead::resume) until it is `None`: a busy span can take more
27/// than one call.
28///
29/// The span can overlap fills that were delivered, before the read failed or live around the
30/// reconnect, so match what the reads return against the fills already seen by instrument and
31/// [`TradeId`](crate::trade::TradeId). A fill has one `TradeId` whether the stream or
32/// `fetch_trades` delivers it.
33///
34/// # Delivery
35///
36/// The event is sent on the account stream like any other, so it is lost if the consumer has
37/// dropped the stream. On a [`Truncated`](FillRecoveryFailure::Truncated) read, the fills that
38/// were read are sent first and the event after them.
39#[non_exhaustive]
40#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Constructor)]
41pub struct FillRecoveryGap<InstrumentKey> {
42 /// Which instruments' fills the span covers.
43 pub scope: FillRecoveryScope<InstrumentKey>,
44 /// The first moment of the span, inclusive.
45 pub start: DateTime<Utc>,
46 /// The last moment of the span, inclusive.
47 pub end: DateTime<Utc>,
48 /// How many times the span was read before it was given up: 1 when the venue does not retry
49 /// it. Retries within one read, such as after a rate limit, are not counted.
50 pub attempts: u32,
51 /// Why the last read failed.
52 pub reason: FillRecoveryFailure,
53}
54
55/// Which instruments a [`FillRecoveryGap`] covers.
56#[non_exhaustive]
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
58pub enum FillRecoveryScope<InstrumentKey> {
59 /// These instruments. A venue never sends an empty list; note that
60 /// [`instrument_filter`](Self::instrument_filter) would pass one on as every instrument.
61 Instruments(Vec<InstrumentKey>),
62 /// Every instrument on the account: the read covered all of them, as Alpaca's does for a
63 /// stream opened without an instrument list.
64 AllInstruments,
65}
66
67impl<InstrumentKey> FillRecoveryScope<InstrumentKey> {
68 /// The instruments to pass to
69 /// [`ExecutionClient::fetch_trades`](crate::client::ExecutionClient::fetch_trades), in its
70 /// convention: an empty slice means every instrument.
71 pub fn instrument_filter(&self) -> &[InstrumentKey] {
72 match self {
73 Self::Instruments(instruments) => instruments,
74 Self::AllInstruments => &[],
75 }
76 }
77}
78
79/// Why fill recovery's last read of a [`FillRecoveryGap`] failed.
80#[non_exhaustive]
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Error)]
82pub enum FillRecoveryFailure {
83 /// The venue request failed.
84 #[error("request failed: {0}")]
85 Request(String),
86
87 /// The read did not finish within the recovery's time budget.
88 #[error("timed out after {timeout_secs}s")]
89 TimedOut {
90 /// The time budget, in seconds.
91 timeout_secs: u64,
92 },
93
94 /// The read stopped at the venue integration's cap on how many fills one read returns. The
95 /// fills it read were delivered, and the span starts at the last of them (at its millisecond,
96 /// on Alpaca), so fills sharing its time that the read cut off are inside it.
97 #[error("truncated after {fills_read} fills")]
98 Truncated {
99 /// How many fills the read returned before it stopped, counted across the account,
100 /// before any filter by instrument.
101 fills_read: usize,
102 },
103}