Skip to main content

brokapi_core/
error.rs

1//! The error type of every connector call and the rule for what to do with each error.
2//!
3//! One enum, [`enum@Error`], for all brokers. A connector maps everything its broker or network can say
4//! into one of nine variants, so the code that handles errors does not depend on the broker.
5//! [`Error::class`] gives the coarse answer to "may I try again": see [`ErrorClass`].
6//!
7//! The one variant that needs care is [`Error::Unknown`]. It says that a command (place, cancel,
8//! replace) may or may not have reached the broker. Never repeat the command after it: look at the
9//! order first, see [`Error::Unknown`].
10
11use std::borrow::Cow;
12use std::time::Duration;
13
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17/// The `code` of an [`Error::Rejected`] that says "the broker does not know this": an order, a cid, an
18/// instrument. The same string at every broker, so a caller compares with `==` and needs no case folding.
19///
20/// # Examples
21///
22/// ```
23/// use brokapi_core::{Error, NOT_FOUND};
24///
25/// let e = Error::not_found("no such order");
26/// assert!(e.is_not_found());
27/// assert!(matches!(&e, Error::Rejected { code, .. } if code == NOT_FOUND));
28/// ```
29pub const NOT_FOUND: &str = "not_found";
30
31/// A configuration problem: a value that is missing, malformed, or of the wrong kind for this broker.
32///
33/// Carried by [`Error::Config`]. The text is for a human (a log line), it is not a stable code.
34///
35/// # Examples
36///
37/// ```
38/// use brokapi_core::{ConfigError, Error};
39///
40/// let e: Error = ConfigError::Message("token is empty".into()).into();
41/// assert_eq!(e.to_string(), "configuration error: token is empty");
42/// ```
43#[derive(Error, Debug, Clone, Serialize, Deserialize)]
44pub enum ConfigError {
45    /// What is wrong, in words.
46    #[error("{0}")]
47    Message(String),
48}
49
50/// A network-level problem: connect, TLS, timeout of a read, a broken stream, a malformed answer.
51///
52/// Carried by [`Error::Transport`]. The text is for a human (a log line), it is not a stable code.
53///
54/// # Examples
55///
56/// ```
57/// use brokapi_core::{Error, TransportError};
58///
59/// let e: Error = TransportError::Message("connection reset".into()).into();
60/// assert_eq!(e.to_string(), "transport error: connection reset");
61/// ```
62#[derive(Error, Debug, Clone, Serialize, Deserialize)]
63pub enum TransportError {
64    /// What went wrong, in words.
65    #[error("{0}")]
66    Message(String),
67}
68
69/// The coarse answer to "may I try this call again": see [`Error::class`].
70///
71/// # Examples
72///
73/// ```
74/// use brokapi_core::{Error, ErrorClass};
75///
76/// assert_eq!(Error::Unknown.class(), ErrorClass::Unknown);
77/// assert_eq!(Error::NotReconciled.class(), ErrorClass::Fatal);
78/// ```
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80#[non_exhaustive]
81pub enum ErrorClass {
82    /// The call failed for a reason that can pass: the network, a rate limit. Reading calls may be
83    /// repeated, after the delay in [`Error::RateLimited`] if there is one. For a command this class is
84    /// **not** a licence to repeat: first find out what the broker has (see [`Error::Transport`]), and
85    /// ask [`Error::retryable_for_command`] in a retry loop of commands.
86    Retryable,
87    /// The same call will fail again until something else changes: a setup, a refused request, a
88    /// missing capability, a gate that is closed. Do not repeat it unchanged.
89    Fatal,
90    /// The outcome of a command is not known: [`Error::Unknown`]. Repeating the command could create
91    /// a second order. The library never repeats it and neither should the caller before it knows
92    /// the state of the order.
93    Unknown,
94}
95
96/// What can go wrong in a connector call.
97///
98/// The texts printed by `Display` are the ones on the errors page of the book (`docs/src/errors.md`),
99/// with what they mean and what to do. A test keeps the two in step.
100///
101/// Each variant says what happened and what the caller does next. [`Error::class`] groups them.
102/// Reading calls (order book, history, portfolio) return only the variants that make sense for a
103/// read: [`Error::Config`], [`Error::Transport`], [`Error::RateLimited`], [`Error::Rejected`] and
104/// [`Error::Unsupported`]. The gate variants ([`Error::NotReconciled`], [`Error::Degraded`],
105/// [`Error::ShuttingDown`]) come from trading commands.
106///
107/// # Examples
108///
109/// ```
110/// use std::time::Duration;
111/// use brokapi_core::{Error, ErrorClass};
112///
113/// /// What a caller does with an error of a reading call.
114/// fn next_step(e: &Error) -> &'static str {
115///     match e {
116///         Error::RateLimited { retry_after } if *retry_after < Duration::from_secs(5) => "wait and repeat",
117///         Error::Transport(_) => "repeat later",
118///         Error::Unsupported { .. } => "ask the broker another way, or skip",
119///         _ => "give up and report",
120///     }
121/// }
122///
123/// let e = Error::RateLimited { retry_after: Duration::from_secs(1) };
124/// assert_eq!(e.class(), ErrorClass::Retryable);
125/// assert_eq!(next_step(&e), "wait and repeat");
126/// ```
127#[derive(Error, Debug, Clone, Serialize, Deserialize)]
128#[non_exhaustive]
129pub enum Error {
130    /// The setup is wrong: a bad config value, or an input that cannot work with this broker (for
131    /// example an [`InstrumentId`](crate::InstrumentId) whose [`NativeId`](crate::NativeId) is of
132    /// another broker's kind).
133    ///
134    /// The broker refusing the credentials (a wrong or expired token, an authentication that failed) is
135    /// this variant too, at every broker: fix the token, a repeat will not help.
136    ///
137    /// Nothing was sent. The call will fail the same way until the config or the input is fixed.
138    #[error("configuration error: {0}")]
139    Config(#[from] ConfigError),
140
141    /// The network or the broker's endpoint failed in a way that is not an answer: connect, TLS,
142    /// timeout of a read, a stream cut, an unreadable reply. (A refused authentication is
143    /// [`Error::Config`].)
144    ///
145    /// For a reading call, repeat it later. For a trading command the connector returns
146    /// [`Error::Unknown`] instead whenever the command might have reached the broker, so
147    /// `Transport` on a command means the connector has no sign that it did (for example the
148    /// authentication failed). Even then, look at the order before you send a command again, and use
149    /// a new `cid` for a new place.
150    #[error("transport error: {0}")]
151    Transport(#[from] TransportError),
152
153    /// A rate limit stopped the call: the local limiter of the connector or the broker's own answer
154    /// (HTTP 429 or the equivalent).
155    ///
156    /// Wait `retry_after`, then repeat. A command refused by the local limiter was not sent. A
157    /// connector may still have used up the `cid` (see its docs), so for a place the safe rule is a
158    /// new `cid`. The library does not wait and repeat for you.
159    #[error("rate limited, retry after {retry_after:?}")]
160    RateLimited {
161        /// How long to wait before the next try. A hint from the limiter or the broker, not a
162        /// promise that the next call passes.
163        retry_after: Duration,
164    },
165
166    /// The broker (or the connector, before sending) said no to this request, and nothing was done.
167    ///
168    /// The request is wrong or not allowed: a price outside the band, no money, an unknown order,
169    /// an order that is already filled, a missing price on a limit order. Change the request. Do not
170    /// repeat it as it is.
171    ///
172    /// `code` is a short machine-readable label that the connector sets (for example `validation`,
173    /// `missing_price`, `invalid_qty`, `not_found`, `already_filled`, `duplicate_place`, `http_400`). `message` is
174    /// the broker's text or the connector's explanation, for a human. The set of codes belongs to the
175    /// connector and is not fixed across brokers, except [`NOT_FOUND`] (the same at every broker: use
176    /// [`Error::is_not_found`]) and the codes the shared set of checks relies on (`duplicate_place`,
177    /// `invalid_qty`). Do not parse `message`.
178    #[error("rejected: {code}: {message}")]
179    Rejected {
180        /// Short label of the reason, set by the connector. Safe to compare with `==`.
181        code: String,
182        /// Explanation for a human, usually the broker's own text.
183        message: String,
184    },
185
186    /// A command (place, cancel, replace, place_stop, cancel_stop) was sent, and no answer says
187    /// whether the broker did it: a timeout, a connection cut after sending, an HTTP 5xx, an answer
188    /// that cannot be read.
189    ///
190    /// The order may exist, may be cancelled, or may never have arrived. The library NEVER repeats
191    /// the command, because a repeated place could create a second order. Do the same:
192    /// 1. Do not send the command again.
193    /// 2. Ask the broker: [`OrderExecutor::order_state`](crate::OrderExecutor::order_state) with the
194    ///    `cid`, or [`OrderExecutor::active_orders`](crate::OrderExecutor::active_orders), or look for
195    ///    the fills in [`ExecutionSource::executions`](crate::ExecutionSource::executions).
196    /// 3. Only when the broker has no such order, place a new one under a NEW `cid`.
197    ///
198    /// Never turned into [`Error::Transport`] or [`Error::Rejected`]: those would say "nothing
199    /// happened", and here that is not known.
200    #[error("command outcome unknown: do not send it again, ask the broker with order_state(cid)")]
201    Unknown,
202
203    /// This connector or this broker does not have the capability, so the call was not made.
204    ///
205    /// Returned at once, without a network call. `capability` is a short name of what is missing
206    /// (for example `stop_order_stream`). The library does not emulate a missing stream by polling.
207    /// If you need the data, poll yourself. See also [`UserDataCaps`](crate::UserDataCaps) and
208    /// [`MarketDataCaps`](crate::MarketDataCaps), which declare what a connector can do.
209    #[error("unsupported capability: {capability}")]
210    Unsupported {
211        /// Short name of the missing capability. Safe to compare with `==` against a `&str`.
212        /// A `Cow` so that the error survives serialization (for example across a process boundary).
213        capability: Cow<'static, str>,
214    },
215
216    /// The trading gate is closed: this connector has not run [`OrderExecutor::reconcile`](crate::OrderExecutor::reconcile)
217    /// successfully yet.
218    ///
219    /// Trading commands wait for the first reconcile, so that the connector knows which orders are
220    /// already at the broker before it puts a new one. Call `reconcile` with your ledger (an empty
221    /// slice if you have none), then repeat the command. Nothing was sent.
222    #[error("not reconciled: call reconcile() before trading commands")]
223    NotReconciled,
224
225    /// The trading gate is closed because the connector lost events it could not afford to lose: a
226    /// [`Backpressure::Lossless`](crate::Backpressure::Lossless) subscription overflowed (an
227    /// [`Event::Overflow`](crate::Event::Overflow) was sent).
228    ///
229    /// The local state of orders may have a hole. Drain your event channel, then call
230    /// [`OrderExecutor::reconcile`](crate::OrderExecutor::reconcile): a successful reconcile opens the gate again.
231    /// Nothing was sent.
232    #[error(
233        "connector degraded: events may be lost, read the event channel to the end and call reconcile()"
234    )]
235    Degraded,
236
237    /// The connector is shutting down and takes no new trading commands.
238    ///
239    /// Final for this connector object. Orders already at the exchange are not touched by a
240    /// shutdown. Start a new connector if you need to trade. Nothing was sent.
241    #[error("shutting down: the connector takes no new trading commands")]
242    ShuttingDown,
243}
244
245impl Error {
246    /// A [`Error::Rejected`] with the code [`NOT_FOUND`]: the broker does not know the order, the cid or
247    /// the instrument. Every connector uses this one for "not found".
248    pub fn not_found(message: impl Into<String>) -> Self {
249        Self::Rejected {
250            code: NOT_FOUND.to_string(),
251            message: message.into(),
252        }
253    }
254
255    /// Whether this is the "not found" refusal ([`Error::Rejected`] with the code [`NOT_FOUND`]).
256    ///
257    /// # Examples
258    ///
259    /// ```
260    /// use brokapi_core::Error;
261    ///
262    /// assert!(Error::not_found("x").is_not_found());
263    /// assert!(!Error::Unknown.is_not_found());
264    /// assert!(!Error::Rejected { code: "validation".into(), message: String::new() }.is_not_found());
265    /// ```
266    pub fn is_not_found(&self) -> bool {
267        matches!(self, Self::Rejected { code, .. } if code == NOT_FOUND)
268    }
269
270    /// The coarse class of this error: may the call be tried again, or not.
271    ///
272    /// | Variant | Class |
273    /// |---|---|
274    /// | [`Error::Transport`], [`Error::RateLimited`] | [`ErrorClass::Retryable`] |
275    /// | [`Error::Unknown`] | [`ErrorClass::Unknown`] |
276    /// | all others | [`ErrorClass::Fatal`] |
277    ///
278    /// The class says what is possible in principle, and it is the class of a **read**. A generic retry
279    /// loop must not use it for a trading command: [`Error::Transport`] is `Retryable` here, and a
280    /// command that failed with it may still have reached the broker. For a command ask
281    /// [`retryable_for_command`](Self::retryable_for_command). The library itself never repeats a
282    /// command, whatever the class.
283    ///
284    /// # Examples
285    ///
286    /// ```
287    /// use brokapi_core::{Error, ErrorClass, TransportError};
288    ///
289    /// let e = Error::Transport(TransportError::Message("timeout".into()));
290    /// assert_eq!(e.class(), ErrorClass::Retryable);
291    /// ```
292    pub fn class(&self) -> ErrorClass {
293        match self {
294            Self::Transport(_) | Self::RateLimited { .. } => ErrorClass::Retryable,
295            Self::Unknown => ErrorClass::Unknown,
296            Self::Config(_)
297            | Self::Rejected { .. }
298            | Self::Unsupported { .. }
299            | Self::NotReconciled
300            | Self::Degraded
301            | Self::ShuttingDown => ErrorClass::Fatal,
302        }
303    }
304
305    /// May a trading command that failed with this error be sent again, after the wait the error names?
306    ///
307    /// `true` only for [`Error::RateLimited`]: the broker (or the local limiter) refused before it acted,
308    /// so the same cancel can be sent again after `retry_after`. A place needs a **new** `cid`, because the
309    /// connector may have used the old one up. Everything else is `false`: [`Error::Transport`] and
310    /// [`Error::Unknown`] mean the command may have reached the broker, so look at the order first
311    /// ([`OrderExecutor::order_state`](crate::OrderExecutor::order_state)), and the other variants
312    /// are final until the setup, the gate or the request changes.
313    ///
314    /// Use this, not [`class`](Self::class), in the retry loop of a command.
315    ///
316    /// # Examples
317    ///
318    /// ```
319    /// use std::time::Duration;
320    /// use brokapi_core::{Error, TransportError};
321    ///
322    /// assert!(Error::RateLimited { retry_after: Duration::from_secs(1) }.retryable_for_command());
323    /// // A read may be repeated after this error, a command may not.
324    /// let cut = Error::Transport(TransportError::Message("connection reset".into()));
325    /// assert!(!cut.retryable_for_command());
326    /// assert!(!Error::Unknown.retryable_for_command());
327    /// assert!(!Error::Degraded.retryable_for_command());
328    /// ```
329    pub fn retryable_for_command(&self) -> bool {
330        matches!(self, Self::RateLimited { .. })
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn not_found_has_one_spelling() {
340        let e = Error::not_found("no such order");
341        assert!(e.is_not_found());
342        assert_eq!(e.class(), ErrorClass::Fatal);
343        assert_eq!(e.to_string(), "rejected: not_found: no such order");
344        // Other refusals and other variants are not "not found", and the old spellings are not either.
345        for other in ["NOT_FOUND", "NotFound", "validation"] {
346            let e = Error::Rejected {
347                code: other.into(),
348                message: String::new(),
349            };
350            assert!(!e.is_not_found(), "{other}");
351        }
352        assert!(!Error::Unknown.is_not_found());
353        assert!(!Error::Degraded.is_not_found());
354    }
355
356    #[test]
357    fn a_command_is_retried_only_after_a_rate_limit() {
358        use std::time::Duration;
359        let all = [
360            Error::Config(ConfigError::Message("x".into())),
361            Error::Transport(TransportError::Message("x".into())),
362            Error::RateLimited {
363                retry_after: Duration::from_secs(1),
364            },
365            Error::Rejected {
366                code: "c".into(),
367                message: "m".into(),
368            },
369            Error::Unsupported {
370                capability: "w".into(),
371            },
372            Error::NotReconciled,
373            Error::Degraded,
374            Error::ShuttingDown,
375            Error::Unknown,
376        ];
377        for e in &all {
378            assert_eq!(
379                e.retryable_for_command(),
380                matches!(e, Error::RateLimited { .. }),
381                "{e}"
382            );
383        }
384        // The class of a read stays: Transport may be repeated by a reader.
385        assert_eq!(all[1].class(), ErrorClass::Retryable);
386    }
387}