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}