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, notional and trailing-offset rules from Binance's
16//! spot `exchangeInfo` on the symbol's first order, and refuses an order that breaks them without
17//! sending it: a price 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`), then restates its
51//! balances (see [below](#balance-restatement-after-a-reconnect)).
52//!
53//! The span of fills a reconnect recovers is placed on Binance's clock, as the times Binance
54//! stamped on the stream's events show it (each event's `E`), counted on from their arrival on a
55//! monotonic clock. So a host clock ahead of Binance's does not start the span after fills Binance
56//! stamped during the outage, and one stepped back before the reconnect does not end it early.
57//! Before the stream's first event the span starts from this host's time.
58//!
59//! - **Attempts.** A dropped connection, and each reconnect that fails after it, spends one
60//! attempt: a wait, then a reconnect. A reconnect fails when its connect, its user-data
61//! subscribe, or for [`BinanceMargin`] its listen-key request fails. The waits double from 1 s
62//! up to 30 s: 1, 2, 4, 8, 16, then 30 s.
63//! - **Reset.** A heartbeat check that finds a frame or ping clears the attempts spent before it,
64//! so only failures in a row count. A connection the venue accepts and then drops or leaves
65//! silent before such a check counts as a failed reconnect. The answer to the subscribe is not
66//! a frame for this purpose.
67//! - **Giving up.** Once 10 reconnects in a row have failed, after about three minutes of waiting
68//! in all, the stream ends with
69//! [`AccountEventKind::StreamTerminated`](crate::AccountEventKind::StreamTerminated) carrying
70//! [`ReconnectBudgetExhausted`](crate::error::StreamTerminationReason::ReconnectBudgetExhausted)
71//! and the last failure.
72//! - **Rate limits.** A reconnect is never attempted during a rate-limit cooldown. A cooldown of
73//! 10 s or less is waited out without spending an attempt. A longer one ends the stream at once
74//! with [`RateLimited`](crate::error::StreamTerminationReason::RateLimited), carrying the time
75//! left. See each client's "Rate limits" docs.
76//! - **Opening is not retried.** A failed `account_stream` call returns its error. Whether and
77//! when to try again is the caller's decision, as it is after the stream ends.
78//!
79//! [`BinanceMargin`] also reconnects with a new listen key when Binance says the old one has
80//! expired, which spends an attempt like any other failure. Its key is kept alive as each
81//! connection opens and every 30 minutes after, which Binance documents as enough to keep it
82//! valid.
83//!
84//! # Balance restatement after a reconnect
85//!
86//! A balance or debt event the venue sends while an account stream is down is lost, so each
87//! reconnect restates the account's balances. Once the gap's fills have been read, the stream
88//! reads the balances over REST and delivers each of the `assets` it was opened for (all of them
89//! when empty) as a [`BalanceSnapshot`](crate::AccountEventKind::BalanceSnapshot), debt included.
90//! An isolated margin stream delivers its pairs' base and quote as
91//! [`InstrumentBalanceSnapshot`](crate::AccountEventKind::InstrumentBalanceSnapshot)s instead:
92//! each pair's base, then its quote, the pairs in no particular order.
93//!
94//! - **Stamp.** The earlier of this host's time and the venue's time when the read's request was
95//! sent, as the stream's own events and the server-time reads show the venue's clock (see
96//! below), less 5 s for Binance's REST view trailing its stream. A stream event newer than the
97//! state the read shows then still applies over it. The read starts no sooner than 5 s, on the
98//! venue's clock, after the latest balance or debt event the stream delivered, and its stamp is
99//! never earlier than that event's: an earlier one would leave the engine keeping that event's
100//! state.
101//! - **Failure.** A read that fails, or does not finish within 30 s (waits for rate limits
102//! included), is logged at `warn` and retried while connected, after 5 s and then twice as long
103//! each time, up to 2 minutes. It is never given up. A later reconnect replaces it with a read
104//! due at once.
105//! - **Fills not yet recovered.** A gap whose fill read failed does not hold the restatement
106//! back, so the balances can include fills the stream has yet to deliver. They follow when the
107//! gap's retry reads them, or never if it is given up (see
108//! [`FillRecoveryGaveUp`](crate::AccountEventKind::FillRecoveryGaveUp)).
109//! - **Retries wait on it.** The restatement, the gaps' fill reads and the order checks are retried
110//! one at a time, so a restatement read can delay a retry due meanwhile by up to its 30 s.
111//! - **Cost.** One account read per reconnect: weight 20 on spot, 10 on cross margin, and 10 per
112//! five isolated pairs.
113//!
114//! # Signed requests are stamped on Binance's clock
115//!
116//! Binance refuses a signed request stamped 1 s or more ahead of its clock, or more than
117//! `recvWindow` (5 s) behind it, with `-1021`. So both clients stamp their signed requests with
118//! Binance's clock as read from `GET /api/v3/time` (weight 1), rather than with this host's: the
119//! earliest Binance's clock can read, counted on from the read on a monotonic clock. A stamp is
120//! then never ahead of Binance's clock, trails it by about the time the read's answer took to
121//! arrive, and does not move when this host's wall clock steps. [`BinanceMargin`] reads the time
122//! from the spot API, as margin has no endpoint for it.
123//!
124//! - **First request.** Each client's first request waits for one read. If the read fails, it is
125//! logged at `warn`, and requests are stamped with this host's clock until a read succeeds.
126//! - **Refresh.** A request that finds the last read 5 minutes old starts another in the
127//! background, and is sent without waiting for it.
128//! - **Suspend.** The monotonic clock does not advance while the host is suspended. A request
129//! that finds this host's wall clock gone more than 1 s further than it since the last read
130//! waits for a new read.
131//! - **`-1021`.** The client reads again before it returns the error, and the answer replaces its
132//! reading, so a retry is stamped on the new one. Until the answer arrives, or if the read
133//! fails, the reading held stays. No read is made during a rate-limit cooldown. The error is
134//! unchanged: a transient connectivity error for a query, a rejection for an order. Whether to
135//! retry stays the caller's decision.
136//! - **Observing it.** `host_clock_lead` on each client reports how far this host's clock leads
137//! Binance's, negative when it trails. A read that finds the clocks 1 s or more apart logs at
138//! `warn`.
139//! - **Account streams.** The reads also bound Binance's clock for the account streams, which place
140//! fill recovery and balance restatement on Binance's time (see above), before a stream's first
141//! event as well as after.
142//! - **Balance reads.** `fetch_balances` and `account_snapshot` stamp each balance with the
143//! earlier of this host's time and Binance's, as the reads bound it, when the request was sent,
144//! less 5 s. A host clock ahead of Binance's then does not carry into the stamp: a stamp ahead
145//! of Binance's time would have the engine drop the stream's balance and debt events until
146//! Binance's clock caught up with it. While no read has succeeded, as after a failed first
147//! read, the stamp is taken from this host's time.
148//! - **Turning it off.** `server_time_sync` on each config, `true` by default, set to `false`
149//! stamps signed requests with this host's clock, and balance reads from it alone: a host clock
150//! ahead of Binance's then carries into their stamps, and keeping the two within bounds is the
151//! caller's obligation.
152//!
153//! # Logging: `binance_sdk` writes request credentials at `DEBUG`
154//!
155//! Both clients run on the `binance-sdk` crate, whose WebSocket API client logs every outgoing
156//! request at `DEBUG`, after it has been signed. [`BinanceSpot`] places and cancels orders and
157//! subscribes to its user-data stream with signed requests, so each of those log lines carries
158//! the account's **API key** and the request's **signature**.
159//!
160//! [`BinanceMargin`] uses only the SDK's REST client, which logs no requests. Its user-data
161//! stream sends the listen key in a subscribe frame, which is not logged, and this client masks
162//! the key in the warnings it writes.
163//!
164//! The API secret and private key are never logged. The SDK logs these lines with `tracing`
165//! under the `binance_sdk` target.
166//!
167//! This library installs no subscriber, so what reaches your logs is decided by the filter your
168//! application builds. A bare `RUST_LOG=debug` enables these lines. To keep them out by default,
169//! seed a `binance_sdk=info` directive **before** your own:
170//!
171//! ```
172//! use tracing_subscriber::EnvFilter;
173//!
174//! let user = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned());
175//! let filter = EnvFilter::try_new(format!("binance_sdk=info,{user}"))?;
176//! # Ok::<(), Box<dyn std::error::Error>>(())
177//! ```
178//!
179//! `EnvFilter` applies the most specific matching directive, and a later directive for the same
180//! target replaces an earlier one. So a bare `debug` cannot lift the seeded `binance_sdk=info`,
181//! while an explicit `RUST_LOG=binance_sdk=debug` still can when you need the SDK's traffic.
182//! Adding the directive *after* reading the environment, as in
183//! `EnvFilter::from_default_env().add_directive(..)`, gets this backwards: it silently overrides
184//! an explicit request.
185
186mod commission;
187mod margin;
188mod restatement;
189mod rules;
190mod server_time;
191mod shared;
192mod spot;
193
194pub use commission::{CommissionDiscount, CommissionPart, CommissionRates};
195pub use margin::{BinanceMargin, BinanceMarginConfig, MarginForcedLiquidation};
196pub use spot::{BinanceSpot, BinanceSpotConfig, BinanceSpotConfigError};