rustrade_execution/client/binance/mod.rs
1//! Binance execution clients.
2//!
3//! - [`BinanceSpot`] — Binance Spot `ExecutionClient` (REST + signed WebSocket API).
4//! - [`BinanceMargin`] — Binance Margin (cross and isolated) `ExecutionClient` (REST
5//! orders/queries + the margin user-data stream, read with a listen key). Configured via
6//! [`BinanceMarginConfig`] and [`MarginEffect`](crate::margin::MarginEffect).
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//! # Orders are checked against the symbol's rules before they are sent
14//!
15//! Each client reads the symbol's tick, lot and notional rules from Binance's spot `exchangeInfo`
16//! on the symbol's first order, and refuses an order that breaks them without sending it: a price
17//! or quantity off its grid as [`OrderError::InvalidPrecision`](crate::error::OrderError::InvalidPrecision)
18//! with [`NotOnGrid`](crate::error::PrecisionLimit::NotOnGrid), and one outside its range as
19//! [`OrderError::OutOfBounds`](crate::error::OrderError::OutOfBounds). Nothing is rounded.
20//!
21//! - **What is checked** is in [`OrderRules::check`](crate::rules::OrderRules::check). A pass is
22//! not a guarantee: the filters that need a reference price or account state stay the venue's.
23//! - **Loading.** Rules refresh hourly. A failed refresh keeps the old rules for up to 24 hours,
24//! and a symbol with no rules is sent unchecked, with a warning. `load_rules` on each client
25//! loads a set of symbols ahead of their first order.
26//! - **Margin** has no rules endpoint of its own, so [`BinanceMargin`] uses the pair's spot
27//! rules, which a margin order obeys.
28//! - **Turning it off.** `pre_send_rules` on each config takes
29//! [`PreSendRules::Off`](crate::rules::PreSendRules::Off) to send every order and leave the
30//! rules to Binance, for when the loaded rules could be out of date.
31//!
32//! # Every fill on the account is delivered
33//!
34//! Both clients' account streams report every fill on the account, whoever placed the order:
35//! this client, another API client, another program, or the venue's own app. Binance marks none
36//! of them as another client's, so each arrives as [`TradeOrigin::Order`](crate::trade::TradeOrigin::Order).
37//! Fill recovery after a reconnect and `fetch_trades` read the account's fills the same way.
38//!
39//! A consumer that trades the account outside the engine it feeds therefore filters those fills
40//! out itself. [`Trade::client_order_id`](crate::trade::Trade::client_order_id) carries the client
41//! order id each fill's order was placed with, so orders placed outside the engine under ids
42//! of their own, such as with a prefix, can be recognised by their fills.
43//!
44//! # Account stream reconnection
45//!
46//! Both clients' account streams reconnect by themselves, with the same budget, when the
47//! connection drops or goes silent. The heartbeat is checked every 30 s: a check that finds no
48//! frame or ping since the last one reconnects, except the first check on each connection, which
49//! is a grace. Silence is therefore noticed after 30 to 60 s. Each reconnect recovers the fills
50//! and order endings the stream missed (see each client's `account_stream`).
51//!
52//! - **Attempts.** A dropped connection, and each reconnect that fails after it, spends one
53//! attempt: a wait, then a reconnect. A reconnect fails when its connect, its user-data
54//! subscribe, or for [`BinanceMargin`] its listen-key request fails. The waits double from 1 s
55//! up to 30 s: 1, 2, 4, 8, 16, then 30 s.
56//! - **Reset.** A heartbeat check that finds a frame or ping clears the attempts spent before it,
57//! so only failures in a row count. A connection the venue accepts and then drops or leaves
58//! silent before such a check counts as a failed reconnect. The answer to the subscribe is not
59//! a frame for this purpose.
60//! - **Giving up.** Once 10 reconnects in a row have failed, after about three minutes of waiting
61//! in all, the stream ends with
62//! [`AccountEventKind::StreamTerminated`](crate::AccountEventKind::StreamTerminated) carrying
63//! [`ReconnectBudgetExhausted`](crate::error::StreamTerminationReason::ReconnectBudgetExhausted)
64//! and the last failure.
65//! - **Rate limits.** A reconnect is never attempted during a rate-limit cooldown. A cooldown of
66//! 10 s or less is waited out without spending an attempt. A longer one ends the stream at once
67//! with [`RateLimited`](crate::error::StreamTerminationReason::RateLimited), carrying the time
68//! left. See each client's "Rate limits" docs.
69//! - **Opening is not retried.** A failed `account_stream` call returns its error. Whether and
70//! when to try again is the caller's decision, as it is after the stream ends.
71//!
72//! [`BinanceMargin`] also reconnects with a new listen key when Binance says the old one has
73//! expired, which spends an attempt like any other failure. Its key is kept alive as each
74//! connection opens and every 30 minutes after, which Binance documents as enough to keep it
75//! valid.
76//!
77//! # Logging: `binance_sdk` writes request credentials at `DEBUG`
78//!
79//! Both clients run on the `binance-sdk` crate, whose WebSocket API client logs every outgoing
80//! request at `DEBUG`, after it has been signed. [`BinanceSpot`] places and cancels orders and
81//! subscribes to its user-data stream with signed requests, so each of those log lines carries
82//! the account's **API key** and the request's **signature**.
83//!
84//! [`BinanceMargin`] uses only the SDK's REST client, which logs no requests. Its user-data
85//! stream sends the listen key in a subscribe frame, which is not logged, and this client masks
86//! the key in the warnings it writes.
87//!
88//! The API secret and private key are never logged. The SDK logs these lines with `tracing`
89//! under the `binance_sdk` target.
90//!
91//! This library installs no subscriber, so what reaches your logs is decided by the filter your
92//! application builds. A bare `RUST_LOG=debug` enables these lines. To keep them out by default,
93//! seed a `binance_sdk=info` directive **before** your own:
94//!
95//! ```
96//! use tracing_subscriber::EnvFilter;
97//!
98//! let user = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned());
99//! let filter = EnvFilter::try_new(format!("binance_sdk=info,{user}"))?;
100//! # Ok::<(), Box<dyn std::error::Error>>(())
101//! ```
102//!
103//! `EnvFilter` applies the most specific matching directive, and a later directive for the same
104//! target replaces an earlier one. So a bare `debug` cannot lift the seeded `binance_sdk=info`,
105//! while an explicit `RUST_LOG=binance_sdk=debug` still can when you need the SDK's traffic.
106//! Adding the directive *after* reading the environment, as in
107//! `EnvFilter::from_default_env().add_directive(..)`, gets this backwards: it silently overrides
108//! an explicit request.
109
110mod margin;
111mod rules;
112mod shared;
113mod spot;
114
115pub use margin::{BinanceMargin, BinanceMarginConfig, MarginForcedLiquidation};
116pub use spot::{BinanceSpot, BinanceSpotConfig, BinanceSpotConfigError};