Skip to main content

rustrade_execution/client/binance/
mod.rs

1//! Binance execution clients.
2//!
3//! - [`BinanceSpot`] — Binance Spot `ExecutionClient` (REST + signed WebSocket API).
4//! - [`BinanceMargin`] — Binance Cross Margin `ExecutionClient` (REST orders/queries +
5//!   a hand-rolled `userListenToken` user-data stream). Configured via
6//!   [`BinanceMarginConfig`]/[`MarginSideEffect`].
7//!
8//! Both clients share exchange-agnostic infrastructure (reconnect/backoff,
9//! rate-limit tracking, event deduplication, error parsing, and the
10//! Binance-string parsers) from the `shared` module, so resilience behaviour is
11//! implemented once and reused rather than duplicated per client.
12//!
13//! # Every fill on the account is delivered
14//!
15//! Both clients' account streams report every fill on the account, whoever placed the order:
16//! this client, another API client, another program, or the venue's own app. Binance marks none
17//! of them as another client's, so each arrives as [`TradeOrigin::Order`](crate::trade::TradeOrigin::Order).
18//! Fill recovery after a reconnect and `fetch_trades` read the account's fills the same way.
19//!
20//! A consumer that trades the account outside the engine it feeds therefore filters those fills
21//! out itself. [`Trade::client_order_id`](crate::trade::Trade::client_order_id) carries the client
22//! order id each fill's order was placed with, so orders placed outside the engine under ids
23//! of their own, such as with a prefix, can be recognised by their fills.
24//!
25//! # Account stream reconnection
26//!
27//! Both clients' account streams reconnect by themselves, with the same budget, when the
28//! connection drops or goes silent. The heartbeat is checked every 30 s: a check that finds no
29//! frame or ping since the last one reconnects, except the first check on each connection, which
30//! is a grace. Silence is therefore noticed after 30 to 60 s. Each reconnect recovers the fills
31//! and order endings the stream missed (see each client's `account_stream`).
32//!
33//! - **Attempts.** A dropped connection, and each reconnect that fails after it, spends one
34//!   attempt: a wait, then a reconnect. A reconnect fails when its connect, its user-data
35//!   subscribe, or for [`BinanceMargin`] its listen-token request fails. The waits double from 1 s
36//!   up to 30 s: 1, 2, 4, 8, 16, then 30 s.
37//! - **Reset.** A heartbeat check that finds a frame or ping clears the attempts spent before it,
38//!   so only failures in a row count. A connection the venue accepts and then drops or leaves
39//!   silent before such a check counts as a failed reconnect. The answer to the subscribe is not
40//!   a frame for this purpose.
41//! - **Giving up.** Once 10 reconnects in a row have failed, after about three minutes of waiting
42//!   in all, the stream ends with
43//!   [`AccountEventKind::StreamTerminated`](crate::AccountEventKind::StreamTerminated) carrying
44//!   [`ReconnectBudgetExhausted`](crate::error::StreamTerminationReason::ReconnectBudgetExhausted)
45//!   and the last failure.
46//! - **Rate limits.** A reconnect is never attempted during a rate-limit cooldown. A cooldown of
47//!   10 s or less is waited out without spending an attempt. A longer one ends the stream at once
48//!   with [`RateLimited`](crate::error::StreamTerminationReason::RateLimited), carrying the time
49//!   left. See each client's "Rate limits" docs.
50//! - **Opening is not retried.** A failed `account_stream` call returns its error. Whether and
51//!   when to try again is the caller's decision, as it is after the stream ends.
52//!
53//! [`BinanceMargin`] also reconnects shortly before its listen token expires. That planned
54//! reconnect spends no attempt.
55//!
56//! # Logging: `binance_sdk` writes request credentials at `DEBUG`
57//!
58//! Both clients run on the `binance-sdk` crate, whose WebSocket API client logs every outgoing
59//! request at `DEBUG`, after it has been signed:
60//!
61//! - [`BinanceSpot`] places and cancels orders and subscribes to its user-data stream with signed
62//!   requests, so each of those log lines carries the account's **API key** and the request's
63//!   **signature**.
64//! - [`BinanceMargin`] subscribes to its user-data stream with an unsigned request whose only
65//!   credential is the **listen token**, which is logged in the same way.
66//!
67//! The API secret and private key are never logged. The SDK logs these lines with `tracing`
68//! under the `binance_sdk` target.
69//!
70//! This library installs no subscriber, so what reaches your logs is decided by the filter your
71//! application builds. A bare `RUST_LOG=debug` enables these lines. To keep them out by default,
72//! seed a `binance_sdk=info` directive **before** your own:
73//!
74//! ```
75//! use tracing_subscriber::EnvFilter;
76//!
77//! let user = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned());
78//! let filter = EnvFilter::try_new(format!("binance_sdk=info,{user}"))?;
79//! # Ok::<(), Box<dyn std::error::Error>>(())
80//! ```
81//!
82//! `EnvFilter` applies the most specific matching directive, and a later directive for the same
83//! target replaces an earlier one. So a bare `debug` cannot lift the seeded `binance_sdk=info`,
84//! while an explicit `RUST_LOG=binance_sdk=debug` still can when you need the SDK's traffic.
85//! Adding the directive *after* reading the environment, as in
86//! `EnvFilter::from_default_env().add_directive(..)`, gets this backwards: it silently overrides
87//! an explicit request.
88
89mod margin;
90mod margin_risk;
91mod shared;
92mod spot;
93
94pub use margin::{BinanceMargin, BinanceMarginConfig, MarginForcedLiquidation, MarginSideEffect};
95pub use spot::{BinanceSpot, BinanceSpotConfig, BinanceSpotConfigError};