Skip to main content

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, and so does the execution manager when it
5//! reopens an account stream that ended. When that read fails for good, or the venue no longer
6//! holds the fills, they never reach the consumer, and the stream says so in-band with
7//! [`AccountEventKind::FillRecoveryGaveUp`](crate::AccountEventKind::FillRecoveryGaveUp), carrying
8//! a [`FillRecoveryGap`].
9
10use chrono::{DateTime, Utc};
11use derive_more::Constructor;
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15/// A span of fills an account stream's recovery gave up on: fills in it may not have been
16/// delivered.
17///
18/// Sent as [`AccountEventKind::FillRecoveryGaveUp`](crate::AccountEventKind::FillRecoveryGaveUp),
19/// once per failed read. The library does not act on it beyond reporting it. What to do is the
20/// consumer's policy: reconcile, alert, or halt.
21///
22/// # Reconciling
23///
24/// Read the span with
25/// [`ExecutionClient::fetch_trades`](crate::client::ExecutionClient::fetch_trades), passing
26/// `start`, `end` and [`FillRecoveryScope::instrument_filter`], and read on from each call's
27/// [`resume`](crate::trade::TradesRead::resume) until it is `None`: a busy span can take more
28/// than one call.
29///
30/// The span can overlap fills that were delivered, before the read failed or live around the
31/// reconnect, so match what the reads return against the fills already seen by instrument and
32/// [`TradeId`](crate::trade::TradeId). A fill has one `TradeId` whether the stream or
33/// `fetch_trades` delivers it.
34///
35/// # Delivery
36///
37/// The event is sent on the account stream like any other, so it is lost if the consumer has
38/// dropped the stream. On a [`Truncated`](FillRecoveryFailure::Truncated) read, the fills that
39/// were read are sent first and the event after them.
40#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Constructor)]
42pub struct FillRecoveryGap<InstrumentKey> {
43    /// Which instruments' fills the span covers.
44    pub scope: FillRecoveryScope<InstrumentKey>,
45    /// The first moment of the span, inclusive.
46    pub start: DateTime<Utc>,
47    /// The last moment of the span, inclusive.
48    pub end: DateTime<Utc>,
49    /// How many times the span was read before it was given up: 1 when the venue does not retry
50    /// it. Retries within one read, such as after a rate limit, are not counted. For a
51    /// [`Processing`](FillRecoveryFailure::Processing) gap, how many times the client tried to
52    /// process the fill.
53    pub attempts: u32,
54    /// Why the last read failed.
55    pub reason: FillRecoveryFailure,
56}
57
58/// Which instruments a [`FillRecoveryGap`] covers.
59#[non_exhaustive]
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
61pub enum FillRecoveryScope<InstrumentKey> {
62    /// These instruments. A venue never sends an empty list; note that
63    /// [`instrument_filter`](Self::instrument_filter) would pass one on as every instrument.
64    Instruments(Vec<InstrumentKey>),
65    /// Every instrument on the account: the read covered all of them, as Alpaca's does for a
66    /// stream opened without an instrument list.
67    AllInstruments,
68}
69
70impl<InstrumentKey> FillRecoveryScope<InstrumentKey> {
71    /// The instruments to pass to
72    /// [`ExecutionClient::fetch_trades`](crate::client::ExecutionClient::fetch_trades), in its
73    /// convention: an empty slice means every instrument.
74    pub fn instrument_filter(&self) -> &[InstrumentKey] {
75        match self {
76            Self::Instruments(instruments) => instruments,
77            Self::AllInstruments => &[],
78        }
79    }
80}
81
82/// Why fill recovery's last read of a [`FillRecoveryGap`] failed.
83#[non_exhaustive]
84#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Error)]
85pub enum FillRecoveryFailure {
86    /// The venue request failed.
87    #[error("request failed: {0}")]
88    Request(String),
89
90    /// The read did not finish within the recovery's time budget.
91    #[error("timed out after {timeout_secs}s")]
92    TimedOut {
93        /// The time budget, in seconds.
94        timeout_secs: u64,
95    },
96
97    /// The read stopped at the venue integration's cap on how many fills one read returns. The
98    /// fills it read were delivered, and the span starts at the last of them (at its millisecond,
99    /// on Alpaca), so fills sharing its time that the read cut off are inside it.
100    #[error("truncated after {fills_read} fills")]
101    Truncated {
102        /// How many fills the read returned before it stopped, counted across the account,
103        /// before any filter by instrument.
104        fills_read: usize,
105    },
106
107    /// The venue no longer holds the fills before `held_since`, so the span's part before it
108    /// could not be read. It is the gap: its `end` is the moment before `held_since`.
109    #[error("the venue holds fills only from {held_since}")]
110    VenueRetention {
111        /// The earliest moment the venue still holds fills from.
112        held_since: DateTime<Utc>,
113    },
114
115    /// The span reaches back further than the recovery's configured maximum span. The gap is the
116    /// part before that maximum, which was not read.
117    #[error("older than the maximum span of {max_span_secs}s")]
118    SpanLimit {
119        /// The maximum span, in seconds.
120        max_span_secs: u64,
121    },
122
123    /// The read stopped at the recovery's configured maximum number of calls. The gap is the part
124    /// not yet read.
125    #[error("stopped after {max_pages} reads")]
126    PageLimit {
127        /// The maximum number of calls.
128        max_pages: u32,
129    },
130
131    /// A fill was delivered, but a client's
132    /// [`process_account_events`](crate::client::ExecutionClient::process_account_events) could not
133    /// process it and gave up on it, so it did not reach the consumer. The gap's scope is the
134    /// fill's instrument, its span the fill's time, and its `attempts` the client's tries.
135    #[error("the client could not process a fill: {0}")]
136    Processing(String),
137}