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.
51    pub attempts: u32,
52    /// Why the last read failed.
53    pub reason: FillRecoveryFailure,
54}
55
56/// Which instruments a [`FillRecoveryGap`] covers.
57#[non_exhaustive]
58#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
59pub enum FillRecoveryScope<InstrumentKey> {
60    /// These instruments. A venue never sends an empty list; note that
61    /// [`instrument_filter`](Self::instrument_filter) would pass one on as every instrument.
62    Instruments(Vec<InstrumentKey>),
63    /// Every instrument on the account: the read covered all of them, as Alpaca's does for a
64    /// stream opened without an instrument list.
65    AllInstruments,
66}
67
68impl<InstrumentKey> FillRecoveryScope<InstrumentKey> {
69    /// The instruments to pass to
70    /// [`ExecutionClient::fetch_trades`](crate::client::ExecutionClient::fetch_trades), in its
71    /// convention: an empty slice means every instrument.
72    pub fn instrument_filter(&self) -> &[InstrumentKey] {
73        match self {
74            Self::Instruments(instruments) => instruments,
75            Self::AllInstruments => &[],
76        }
77    }
78}
79
80/// Why fill recovery's last read of a [`FillRecoveryGap`] failed.
81#[non_exhaustive]
82#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Error)]
83pub enum FillRecoveryFailure {
84    /// The venue request failed.
85    #[error("request failed: {0}")]
86    Request(String),
87
88    /// The read did not finish within the recovery's time budget.
89    #[error("timed out after {timeout_secs}s")]
90    TimedOut {
91        /// The time budget, in seconds.
92        timeout_secs: u64,
93    },
94
95    /// The read stopped at the venue integration's cap on how many fills one read returns. The
96    /// fills it read were delivered, and the span starts at the last of them (at its millisecond,
97    /// on Alpaca), so fills sharing its time that the read cut off are inside it.
98    #[error("truncated after {fills_read} fills")]
99    Truncated {
100        /// How many fills the read returned before it stopped, counted across the account,
101        /// before any filter by instrument.
102        fills_read: usize,
103    },
104
105    /// The venue no longer holds the fills before `held_since`, so the span's part before it
106    /// could not be read. It is the gap: its `end` is the moment before `held_since`.
107    #[error("the venue holds fills only from {held_since}")]
108    VenueRetention {
109        /// The earliest moment the venue still holds fills from.
110        held_since: DateTime<Utc>,
111    },
112
113    /// The span reaches back further than the recovery's configured maximum span. The gap is the
114    /// part before that maximum, which was not read.
115    #[error("older than the maximum span of {max_span_secs}s")]
116    SpanLimit {
117        /// The maximum span, in seconds.
118        max_span_secs: u64,
119    },
120
121    /// The read stopped at the recovery's configured maximum number of calls. The gap is the part
122    /// not yet read.
123    #[error("stopped after {max_pages} reads")]
124    PageLimit {
125        /// The maximum number of calls.
126        max_pages: u32,
127    },
128}