pub struct BinanceMargin { /* private fields */ }Expand description
Binance Cross Margin execution client using the official binance-sdk.
Places orders and queries account state over the margin REST API
(margin_trading::rest_api), and streams live account events via a hand-rolled
userListenToken flow over the WS API, not the retired /sapi/v1/userDataStream listen-key
path.
See BinanceMarginConfig::testnet for the no-testnet caveat. The behaviour a caller most needs to know
is summarised below, with links to the authoritative detail.
§Borrow/repay (sideEffectType)
Fixed once per client via BinanceMarginConfig::side_effect / MarginSideEffect. The
default MarginSideEffect::AutoBorrowRepay makes shorting work out of the box but lets a
mis-sized order silently borrow — use MarginSideEffect::NoBorrow to opt out. Per-order
borrow intent is intentionally not modelled (it would require position tracking); see
MarginSideEffect for the rationale and upgrade path.
§Cross vs. isolated margin
The mode is fixed per client via BinanceMarginConfig::is_isolated:
- Cross (
is_isolated = false,isIsolated = "FALSE", account-wide collateral): per-asset balances (incl. debt) are surfaced account-wide in the top-levelbalances. - Isolated (
is_isolated = true): per-pair sub-accounts. Balances + risk are attached per-instrument viaInstrumentAccountSnapshot::isolated(the asset-keyed top-levelbalancesis left empty, since(pair, asset)slots would collide), and per-symbol queries are scoped to the configuredBinanceMarginConfig::isolated_symbols. Seeaccount_snapshotfor the full per-method semantics.
§Trailing stops
As on spot, a TrailingStop is placed as a STOP_LOSS with a trailingDelta, and a
TrailingStopFixedLimit as a STOP_LOSS_LIMIT with a trailingDelta and its fixed limit as
price, for BasisPoints and Percentage offsets that come to a whole number of basis
points. An activation price is sent as stopPrice. Absolute offsets and TrailingStopLimit
return OrderError::UnsupportedOrderType. See open_order.
§Conditional orders read back over REST
Binance margin accepts a trailingDelta on every conditional type (STOP_LOSS,
STOP_LOSS_LIMIT, TAKE_PROFIT, TAKE_PROFIT_LIMIT), but its REST order queries, as
binance-sdk models them, do not report one (#541). A conditional order read over REST could
therefore be a fixed or a trailing order, so fetch_open_orders and
account_snapshot leave a live one out rather than describe it
wrongly. account_snapshot reports such a listing incomplete; fetch_open_orders has no way
to, so there the omission shows only in the log. The account_stream
reports these orders in full: its executionReport carries the trailing delta.
An order that has ended is still reported as ended by
fetch_ended_orders and by the stream’s
check after a reconnect, so that a stop that fired is not held as live. Its kind is the one this
client holds it with: every order it placed, listed or saw on its stream while live. An order
it does not hold is reported with its fixed-trigger kind at stopPrice, which a trailing order
it really was does not match, and one of those without a positive stopPrice cannot be
described at all and reads as not ended, with a warning.
§User-data stream (userListenToken)
account_stream is hand-rolled over the userListenToken model — the
legacy margin listen-key user-data API (/sapi/v1/userDataStream) was retired by Binance on
2026-02-20, and binance-sdk has no binding for its replacement. There is no keepalive ping
(the retired listen-key PUT mechanism): instead the token (~24h validity) is re-acquired and
re-subscribed before its expirationTime, transparently across reconnects.
The Risk Data Stream a cross account stream also reads (see
account_stream’s “Margin notices”) is a different, current API: its
listen key comes from /sapi/v1/margin/listen-key, which binance-sdk does bind, and is kept
alive every 30 minutes.
§Margin balances & debt-freshness
Balances carry per-asset margin debt: Balance::net_asset
returns total - borrowed, with borrowed/interest exposed via
MarginDetails. Authoritative debt totals come from the REST
account_snapshot (BalanceSnapshot); the WS stream keeps
free/locked live via BalanceStreamUpdate but never clobbers or re-establishes debt, and
USER_LIABILITY_CHANGE is surfaced as an observable log only, never accumulated into state.
Consequently net_asset reflects debt only as fresh as the last account_snapshot for that
asset — call it at startup and refresh on demand (see account_stream’s
cold-start note).
As Binance documents it, margin interest is charged hourly on what is borrowed and accrues as
the asset’s unpaid interest, which comes out of total only when it is repaid. net_asset
deducts borrowed alone, so until then interest is the only figure that holds it, as fresh
as the rest of the debt. Fills carry no interest, so PnL computed from them excludes it.
§Rate limits
Once a /sapi response reports at least 90% of the documented per-IP weight limit (12000 per
minute) used, queries wait for the next minute, so the rest is left for orders and cancels,
which never wait for it. A query, including a fetch_open_orders
after a reconnect, can therefore take up to about a minute longer. A reconnect’s fill recovery
and its userListenToken request do not wait for it either, since a pause could outlast the
recovery’s 30 s and delay its fills to a retry. The per-UID /sapi limit is not tracked.
After Binance answers with a rate-limit error, every REST call waits out a cooldown.
§One client per engine (ExchangeId)
All emitted events — cross and isolated alike — are stamped ExchangeId::BinanceMargin, and
the engine routes AssetStates / ConnectivityStates by ExchangeId. Running two
BinanceMargin clients in a single engine (e.g. one cross, one isolated) therefore collides
their exchange identity. A single engine should run at most one BinanceMargin client; a
consumer needing cross and isolated concurrently runs them in separate engines.
§Fills of other clients’ orders
The account stream, fill recovery and fetch_trades report every fill on the account, whoever
placed the order, and Binance does not say who did. A consumer that also trades the account
outside the engine filters those fills itself, for example by
Trade::client_order_id. See the
module docs.
Implementations§
Source§impl BinanceMargin
impl BinanceMargin
Sourcepub async fn fetch_margin_interest(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Result<Vec<CashFlow<AssetNameExchange, InstrumentNameExchange>>, UnindexedClientError>
pub async fn fetch_margin_interest( &self, start: DateTime<Utc>, end: DateTime<Utc>, ) -> Result<Vec<CashFlow<AssetNameExchange, InstrumentNameExchange>>, UnindexedClientError>
Read the interest Binance charged the account from start to end, each charge as a
CashFlow of MarginInterest, oldest first.
A cross-margin client reads the cross account. An isolated one reads each pair in its
isolated_symbols, and attributes each
charge to its pair through CashFlow::instrument; a cross charge is attributed to none.
§What a charge is
Binance charges interest hourly, and once when a loan is taken, by adding it to the loan
rather than taking it from the account’s free balance. A charge is cash only once the loan
is repaid. The flow’s negative amount is the cost charged at
time_exchange, not a balance that moved then: the account’s
balance shows it as interest owed on the asset.
assetis theassetfield of Binance’s record. Binance charges interest in the asset borrowed, or converts it into BNB for an account that pays interest in BNB. A converted record also names the asset borrowed, in a separate field, and Binance does not document which of the two fields holds BNB. Check a converted record before relying on its asset.principalis the loan the charge was on.rateis the rate charged for the period, hourly for an hourly charge, as Binance quotes it.idis Binance’s transaction id for the charge.
The engine adds a flow attributed to an instrument to the carry of its open position only when the flow is in the instrument’s quote asset. Interest on a short’s borrowed base asset, or in BNB, is not: the engine leaves it out of carry and says so. To count it, convert it into the quote asset first.
§Delivery
Nothing is pushed: the caller decides when to read, and must drop the charges it has
already counted, by id, before applying a read’s flows. Within one read
each charge appears once: a charge read twice, as one stamped on a window’s boundary is, is
kept the first time its id is seen in its asset and pair, or, without an id, once per
identical record.
§Span
- Binance serves interest charged in the last 90 days only. A
startbefore that is moved up to a minute inside that limit, clear of any skew between the clocks. Astartmore than a minute before the limit is logged at WARN; one within a minute of it, on either side, as a request for the last 90 days is, is moved silently. - Charges are read up to
end, or up to the local clock at the call if that is earlier. - Binance serves at most 30 days per request, so a span is read in windows of 29 days, each paged by 100 charges. A read costs a request weight of 1 per page, at least 1 per window, and, on an isolated client, as much again for each pair.
§Records left out
A record without an asset, an amount or a time that parses is left out, and logged at WARN.
A rate or principal that does not parse is left None, and logged at WARN.
§Errors
Fails if any request fails, or if one window of one account holds more than 50,000 charges. Nothing is returned from a read that failed part way.
Sourcepub async fn fetch_forced_liquidations(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Result<Vec<MarginForcedLiquidation>, UnindexedClientError>
pub async fn fetch_forced_liquidations( &self, start: DateTime<Utc>, end: DateTime<Utc>, ) -> Result<Vec<MarginForcedLiquidation>, UnindexedClientError>
Read the orders Binance placed from start to end to liquidate the account, oldest
first.
A cross-margin client reads the cross account. An isolated one reads each pair in its
isolated_symbols.
Binance marks no liquidation on the account stream, so this client reports a liquidation
order’s fills as TradeOrigin::Order. A caller that
learns the account is being liquidated, as from a margin-level notice, can read these
records and match their order_ids against the fills
it has, with the caveat on that field.
§Span
Records are read up to end, or up to the local clock at the call if that is earlier, in
pages of 100. Binance documents no limit on the span or on how far back it serves; a
two-year span was accepted when tried in October 2026, which is no guarantee. A read
costs a request weight of 1 per page and, on an isolated client, as much again for each
pair.
§Records left out
- A record without an order id, a pair, a side, a filled quantity or a time that parses is
left out, and logged at WARN. A price or quantity that does not parse is left
None, and logged at WARN. - On a cross client, a record Binance marks as isolated is left out, and logged at WARN: the cross query should not return one. An isolated client asks for one pair at a time, and keeps what Binance returns for it.
§Errors
Fails if any request fails, or if one account holds more than 50,000 records in the span. Nothing is returned from a read that failed part way.
Trait Implementations§
Source§impl BorrowCapacityClient for BinanceMargin
impl BorrowCapacityClient for BinanceMargin
Source§async fn fetch_borrow_capacity(
&self,
instrument: &InstrumentNameExchange,
side: Side,
) -> Result<BorrowCapacity, UnindexedClientError>
async fn fetch_borrow_capacity( &self, instrument: &InstrumentNameExchange, side: Side, ) -> Result<BorrowCapacity, UnindexedClientError>
Reads the pair, then /sapi/v1/margin/maxBorrowable for the asset an order on side
borrows, on this client’s cross margin or on the pair’s isolated account. They cost 51 of
Binance’s per-IP request weight on cross margin and 60 on isolated.
borrowable_now is Binance’s amount, which it describes as the account’s “max
borrowable amount with sufficient system availability”, so it is also capped by what
Binance has to lend. account_limit is its borrowLimit, the most the account’s VIP level
allows.
§Errors
ApiError::InstrumentInvalidfor a pair Binance does not trade on this client’s cross or isolated margin.ApiError::RequestRejectedwith Binance’s-11001on isolated margin, for a pair this account has no isolated margin account for. Open one on Binance, or ask a cross-margin client.
Source§impl Clone for BinanceMargin
impl Clone for BinanceMargin
Source§impl Debug for BinanceMargin
impl Debug for BinanceMargin
Source§impl ExecutionClient for BinanceMargin
impl ExecutionClient for BinanceMargin
Source§fn new(config: Self::Config) -> Self
fn new(config: Self::Config) -> Self
Construct a BinanceMargin client from its configuration.
§Panics
Panics if:
- the binance-sdk configuration builder fails (e.g. empty or malformed API key/secret),
matching
BinanceSpot’s startup contract; or is_isolated = truebutisolated_symbolsis empty — an isolated client with no configured pairs has nothing to snapshot or stream. This gate lives here (not only inBinanceMarginConfig::isolated) because the config isDeserialize-only and a deserialized config bypasses the named constructor. TheExecutionClient::newsignature returnsSelf, notResult, so an unusable config is a fail-fast panic (consistent with the credentialexpects above), not a recoverable error.
Source§async fn open_order(
&self,
request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>
async fn open_order( &self, request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>, ) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>
Submit a margin order over the SAPI POST /sapi/v1/margin/order endpoint.
Mirrors BinanceSpot::open_order’s contract: never returns
None (every failure is folded into the returned Order’s state as
OrderState::inactive), so the engine always sees a definitive outcome.
Margin specifics:
sideEffectTypeis the client-levelMarginSideEffect(borrow/repay policy).isIsolatedis config-driven ("TRUE"for isolated,"FALSE"for cross).autoRepayAtCancelis set only underMarginSideEffect::AutoBorrowRepay: aNoBorrowclient takes no loan, so requesting repay-on-cancel would be incoherent.- A
TrailingStopis sent as aSTOP_LOSSwithtrailingDeltain basis points, and aTrailingStopFixedLimitas aSTOP_LOSS_LIMITwithtrailingDeltaand its fixed limit asprice; an activation price goes asstopPrice. An Absolute offset, andTrailingStopLimit, returnOrderError::UnsupportedOrderType; an offset that is not a positive whole number of basis points returnsOrderError::InvalidPrecision(see the type-level docs). A non-positive activation price, or aTrailingStopFixedLimitwithout a positiveprice, is refused unsent asOrderError::Rejected.
Source§async fn cancel_order(
&self,
request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
) -> UnindexedOrderResponseCancel
async fn cancel_order( &self, request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>, ) -> UnindexedOrderResponseCancel
Cancel a resting margin order via DELETE /sapi/v1/margin/order.
Cancels by exchange orderId when present and parseable, otherwise by the originating
client order id. isIsolated is config-driven. Mirrors
BinanceSpot::cancel_order: every failure is folded into the
returned response’s state as an Err.
The margin cancel response carries no transactTime, so the cancellation timestamp is the
local receive time.
Source§async fn account_snapshot(
&self,
assets: &[AssetNameExchange],
instruments: &[InstrumentNameExchange],
) -> Result<UnindexedAccountSnapshot, UnindexedClientError>
async fn account_snapshot( &self, assets: &[AssetNameExchange], instruments: &[InstrumentNameExchange], ) -> Result<UnindexedAccountSnapshot, UnindexedClientError>
Fetch a full margin account snapshot: balances plus open orders per instrument.
Cross (is_isolated = false): account-wide per-asset balances from
query_cross_margin_account_details (carrying borrowed/interest → Balance::new_margin)
in the top-level balances, plus open orders per requested instrument — mirroring
BinanceSpot::account_snapshot.
Isolated (is_isolated = true): per-pair balances + risk from
query_isolated_margin_account_info (chunked ≤5 symbols/request), attached per-instrument
via InstrumentAccountSnapshot::isolated rather than folded into the asset-keyed
top-level balances (which is left empty) — isolated sub-accounts are per-(pair, asset)
and would collide in the asset-keyed model. The instrument set is the effective isolated set
(isolated_symbols, or instruments ∩ isolated_symbols with out-of-set instruments skipped);
each instrument’s open orders and isolated balances are fetched over that same set.
Source§async fn fetch_balances(
&self,
assets: &[AssetNameExchange],
) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError>
async fn fetch_balances( &self, assets: &[AssetNameExchange], ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError>
Fetch current margin balances (incl. borrowed/interest debt) for the requested assets.
Cross: account-wide per-asset balances; an empty assets slice is the “return all”
sentinel.
Isolated: returns an empty Vec. Isolated balances are per-(pair, asset) and the
asset-keyed return type cannot carry them without collision — they are surfaced per-instrument
via account_snapshot’s InstrumentAccountSnapshot::isolated
instead.
Source§async fn fetch_open_orders(
&self,
instruments: &[InstrumentNameExchange],
) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError>
async fn fetch_open_orders( &self, instruments: &[InstrumentNameExchange], ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError>
Fetch currently open margin orders, optionally filtered by instrument.
Cross: honours the ExecutionClient::fetch_open_orders “return all” sentinel — an empty
instruments slice is served by a single no-symbol query_margin_accounts_open_orders call
(each order’s instrument taken from its own symbol); a non-empty slice fetches the listed
instruments concurrently, per-symbol.
Isolated: per-symbol on the venue — always iterates the effective isolated set (empty
instruments → configured isolated_symbols; out-of-set instruments skipped with a warning);
never issues a no-symbol isolated call.
Conditional orders are omitted (STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT,
TAKE_PROFIT_LIMIT, so every Stop, StopLimit, TakeProfit, TakeProfitLimit and
TrailingStop): the REST row cannot tell a fixed from a trailing one (#541). This returns
no completeness flag, so the omission shows only in the log. A caller that needs to know
whether the result is the whole book reads account_snapshot,
which reports such a listing incomplete, and tracks these orders from the
account_stream. See the type-level docs.
Source§async fn fetch_trades(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
instruments: &[InstrumentNameExchange],
) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError>
async fn fetch_trades( &self, start: DateTime<Utc>, end: DateTime<Utc>, instruments: &[InstrumentNameExchange], ) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError>
Read margin trades (fills) from start to end, optionally filtered by instrument.
Documented deviation from the ExecutionClient::fetch_trades “return all” contract:
Binance’s margin trade-list endpoint (myTrades) requires a symbol — there is no no-symbol
“all trades” query (unlike open orders).
Cross: an empty instruments slice has nothing to query and reads nothing; callers
wanting all trades must enumerate instruments explicitly.
Isolated: the empty sentinel resolves to the configured isolated_symbols (the effective
isolated set; out-of-set instruments skipped with a warning), iterated per-symbol (Design
decision #4).
Complete: each instrument is read to the span’s end within the call, with no page cap,
so the read is always complete (resume: None).
Lookback cost: Binance serves margin trades by time only in spans under 24 hours, so
each instrument is read in 23-hour windows from start until its first trade, one
request (weight 10) per window, then by trade id. The cost grows with the lookback and
has no cap: a start 30 days back with no trades costs about 32 requests per
instrument, and one at the Unix epoch about 21,000. Pass the most recent start that
covers what you need.
Upper bound: trades are read up to end, or up to the local clock at the call if that
is earlier. A trade stamped after the local clock, as one inside any skew between it and
Binance’s can be, is left to the next read, unless a window before the last one held a
trade (or the last one read a full page) and the walk went on to page by id, which reads
on to end.
Source§async fn account_stream(
&self,
_assets: &[AssetNameExchange],
instruments: &[InstrumentNameExchange],
) -> Result<Self::AccountStream, UnindexedClientError>
async fn account_stream( &self, _assets: &[AssetNameExchange], instruments: &[InstrumentNameExchange], ) -> Result<Self::AccountStream, UnindexedClientError>
Live stream of account events (fills, order updates, balance changes) over the hand-rolled
userListenToken user-data stream.
Acquires a userListenToken (signed POST /sapi/v1/userListenToken, cross = no params),
subscribes over the WS API (userDataStream.subscribe.listenToken), and keeps the stream
live with auto-reconnect, exponential backoff, heartbeat monitoring, and fill recovery
(mirroring BinanceSpot). The token is re-acquired and
re-subscribed before its ~24h expiry — there is no listen-key keepalive (that API is
retired).
As on spot, each instrument’s gap after a reconnect is kept until its fills are forwarded:
a gap whose read failed or timed out is retried after 1, 2, 4, 8 and 16 minutes, connected
or not in between, reading only the gap. After five failed retries it is given up, logged
at error and reported as an AccountEventKind::FillRecoveryGaveUp;
ExecutionClient::fetch_trades can still read its fills.
§Debt cold-start
This method does not seed balances. Margin debt (borrowed/interest) is correct only
if the caller invokes ExecutionClient::account_snapshot at startup (the BalanceSnapshot
that populates margin). WS thereafter keeps free/locked live via BalanceStreamUpdate
but never re-establishes debt; USER_LIABILITY_CHANGE is logged observably, not applied to
balance state.
§Margin notices
Binance’s MARGIN_LEVEL_STATUS_CHANGE is delivered as an
AccountEventKind::Notice. Its status maps to a
NoticeKind: MARGIN_CALL → MarginCall, PRE_LIQUIDATION →
LiquidationWarning, FORCE_LIQUIDATION → Liquidation, NORMAL and EXCESSIVE →
MarginRestored, anything else → Other, with Binance’s status verbatim in
AccountNotice::status and its margin level (l) in
margin_level.
Binance’s own SDKs read the event from the Risk Data Stream
(wss://margin-stream.binance.com, keyed by POST /sapi/v1/margin/listen-key). Whether the
userListenToken stream sends it too is not documented, so:
- Cross reads both, and a notice sent on both is delivered once (matched on status and
Binance’s event time; a frame with no time is stamped on arrival, so its two copies both
arrive). Notices name no instrument. The Risk Data Stream is a side channel, in a task of
its own: if it cannot get a listen key or connect, that is logged at
warnand retried with backoff up to 5 minutes, without limit, and the account stream carries on. Its key is kept alive every 30 minutes (weight 1) and left to lapse when the stream ends. - Isolated reads only the
userListenTokenstream, so receives notices only if Binance sends them there (unconfirmed). A notice names the pair whose subscription it came on, or no instrument, with awarn, when the frame’ssubscriptionIdis missing or unmapped.
Each arrival is logged at info, naming the stream, before the duplicate is dropped. A
notice sent while a stream is disconnected is not recovered.
USER_LIABILITY_CHANGE, on either stream, is logged at info and not forwarded.
§Startup race window
Like spot, fills arriving between subscribe and the listener being registered may be missed;
callers requiring startup fill completeness must call ExecutionClient::fetch_trades with
a ~1s lookback after this returns.
A recovered fill advances the order too. A live fill carries the order’s cumulative filled
quantity in Trade::order_filled_quantity (executionReport’s z); REST myTrades
carries none, so recovery rebuilds the same figure by reading each recovered order’s
executions from its first: one extra request per order (another per further 1,000
executions), up to four orders at a time per instrument. Those lookups have their own
time budget inside the recovery timeout, so they can never cost a fill: a fill whose order
was not looked up in time, or whose lookup failed, goes out with order_filled_quantity: None, logged at warn, and advances the position but not the order.
§Orders that ended while disconnected
A reconnect also reports how each order the client holds as live on one of the stream’s
instruments ended, where it did, as an AccountEventKind::OrderSnapshot of its inactive
state: filled, cancelled with what filled before, expired, or rejected. The stream’s
instruments are instruments on cross and
isolated_symbols on isolated, as for fill
recovery. Binance’s order history filters on when an order was created, so such an order is
found by asking about it, not by time.
- Which orders. The client holds an order as live from the response to placing it,
from a listing of open orders (
account_snapshot,fetch_open_orders), and from its live reports on any of its account streams, until it sees the order end. It holds up to 4,096 orders and forgets the oldest past that, logged atwarn. An order placed outside this client and never listed or reported to it is not covered. - Cost. One
GET /sapi/v1/margin/openOrdersper instrument with an order held (weight 10), then oneGET /sapi/v1/margin/order(weight 10) for each held order the listing no longer shows, all in the configured cross or isolated account. - Fills first. An instrument is checked only once its fill gap is recovered or given up, so an order’s recovered fills arrive before how it ended. A fill that brings an order to its full quantity ends it, and that order is not reported again.
- Keys. Each snapshot carries
StrategyId::unknown, since Binance records no strategy. The engine matches it to the order it tracks by client order id. - Failures. Each order’s lookup is settled as it ends, and each instrument as soon as its
check ends. One whose listing or any lookup fails, or whose check is still running when a
pass reaches 30 s, is retried on the fill gaps’ schedule (1, 2, 4, 8 and 16 minutes),
asking only about the orders still held, then given up, logged at
error. Its orders are asked about again at the next reconnect. An order Binance does not know (-2013) stops being held, logged atwarn; one it still reports live, or in a state this version cannot read, stays held.
The same lookup is public as OrderStatusClient::fetch_ended_orders.
§Isolated mode (multiplexed userListenToken)
Under is_isolated = true a separate manager drives the stream (the cross path above is
left untouched). It acquires one per-symbol userListenToken for each
BinanceMarginConfig::isolated_symbols entry and multiplexes all N subscriptions onto a
single WS-API socket. Every token is acquired, the socket connected, and all subscriptions
confirmed before this method returns — if the connect or any subscribe fails, it
returns Err with nothing spawned (preserving the “can’t-start vs started-then-dropped”
distinction). The instruments argument does not drive the isolated token set: the stream
always covers exactly isolated_symbols.
§Live per-pair balances — InstrumentBalanceUpdate
The isolated stream delivers live fills and order updates (routed by the inner symbol) and
live per-pair free/locked balances, emitted as
AccountEventKind::InstrumentBalanceUpdate
(base + quote per pair). Debt totals (borrowed/interest) stay REST-BalanceSnapshot-fresh
per the debt-freshness contract; the stream keeps only free/locked live.
Consumption contract (important): the engine deliberately does not store
InstrumentBalanceUpdate — it falls through update_from_account’s _ => None wildcard,
mirroring the snapshot’s
InstrumentAccountSnapshot::isolated. It is
therefore never in EngineState, and a StateReplicaManager replica replays the same
wildcard so it is not in replica state either. A consumer obtains live per-pair balances
only by inspecting the raw account event off the stream (AccountStreamEvent::Item(..) /
audit_tick.event) and matching the variant itself — a consumer reading solely replica
EngineState will see nothing and wrongly conclude the feature is broken. A wrapper wanting
engine-queryable per-instrument balances uses the InstrumentData extension point instead.
§First-prod-run check — subscriptionId → symbol routing
outboundAccountPosition frames carry no symbol inline; on the multiplexed socket their pair
is recovered via a subscriptionId → symbol map. This correlation is the one assumption no
offline source could validate — verify it on the first production run. Fills and order
updates are unaffected (they self-identify by symbol); if the correlation does not hold,
per-pair balance frames are dropped with a warn! (never mis-applied) and balances degrade to
snapshot-polling. The documented fallback is one socket per isolated symbol (symbol implied by
the connection).
const EXCHANGE: ExchangeId = ExchangeId::BinanceMargin
Source§const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant]
const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant]
InstrumentKindDiscriminants this client can trade. Read moretype Config = BinanceMarginConfig
type AccountStream = Pin<Box<dyn Stream<Item = AccountEvent<ExchangeId, AssetNameExchange, InstrumentNameExchange>> + Send>>
Source§fn validate_config(
config: &Self::Config,
instruments: &[ClientInstrument<'_>],
) -> Result<(), String>
fn validate_config( config: &Self::Config, instruments: &[ClientInstrument<'_>], ) -> Result<(), String>
fn cancel_orders<'a>( &self, requests: impl IntoIterator<Item = OrderRequestCancel<ExchangeId, &'a InstrumentNameExchange>>, ) -> impl Stream<Item = UnindexedOrderResponseCancel> + Send
fn open_orders<'a>( &self, requests: impl IntoIterator<Item = OrderRequestOpen<ExchangeId, &'a InstrumentNameExchange>>, ) -> impl Stream<Item = Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>> + Send
Source§impl OrderStatusClient for BinanceMargin
Looks each order up with GET /sapi/v1/margin/order by symbol and client order id, in the
cross or isolated account as BinanceMarginConfig::is_isolated says, weight 10 each, up to
eight at a time. Binance’s FILLED is reported with its average price, CANCELED with what
filled before it, EXPIRED and EXPIRED_IN_MATCH (self-trade prevention) as expired, and
REJECTED as OpenFailed. An order
Binance does not know under the key’s symbol (-2013, or -1121 for a symbol that does not
exist) is omitted. A row whose status this version does not know is omitted too, with a
warning, as is a PENDING_CANCEL one.
impl OrderStatusClient for BinanceMargin
Looks each order up with GET /sapi/v1/margin/order by symbol and client order id, in the
cross or isolated account as BinanceMarginConfig::is_isolated says, weight 10 each, up to
eight at a time. Binance’s FILLED is reported with its average price, CANCELED with what
filled before it, EXPIRED and EXPIRED_IN_MATCH (self-trade prevention) as expired, and
REJECTED as OpenFailed. An order
Binance does not know under the key’s symbol (-2013, or -1121 for a symbol that does not
exist) is omitted. A row whose status this version does not know is omitted too, with a
warning, as is a PENDING_CANCEL one.
In isolated mode a key’s symbol is asked about whether or not it is one of
isolated_symbols, since an order can be placed on any
pair. Any error other than those two codes fails the call, including one Binance gives for a
pair with no isolated account.
The account stream runs the same lookup itself after a reconnect; see
account_stream.
Source§async fn fetch_ended_orders(
&self,
orders: &[UnindexedOrderKey],
) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError>
async fn fetch_ended_orders( &self, orders: &[UnindexedOrderKey], ) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError>
orders ended, for those that have. Read moreSource§impl ShortabilityClient for BinanceMargin
impl ShortabilityClient for BinanceMargin
Source§async fn fetch_shortability(
&self,
instrument: &InstrumentNameExchange,
) -> Result<Shortability, UnindexedClientError>
async fn fetch_shortability( &self, instrument: &InstrumentNameExchange, ) -> Result<Shortability, UnindexedClientError>
Reads the pair, its base asset and the base asset’s next hourly interest rate, which cost 102 of Binance’s per-IP request weight on cross margin and 111 on isolated, almost all of it the interest rate.
shortableis whether the pair is traded on margin and may be sold (/sapi/v1/margin/allPairs, orisolated/allPairson isolated margin), and its base asset may be borrowed (/sapi/v1/margin/allAssets). Any of them false makes it false. Binance’s delisting schedule (delistTime) is not read, as Binance documents neither its unit nor when it is present, so a pair due to be delisted reads shortable until Binance clears these flags.fee_rateis the base asset’s next hourly interest rate (/sapi/v1/margin/next-hourly-interest-rate, for this client’s cross or isolated margin) times 24 × 365. It is this account’s rate, at its VIP level, for the coming hour only.easy_to_borrowis unknown: Binance has no such distinction.availableis unknown: Binance does not say how much it has to lend in total, only how much this account can borrow, whichfetch_borrow_capacityreports.
§Errors
ApiError::InstrumentInvalid for a pair Binance does not trade on this client’s cross
or isolated margin.
Auto Trait Implementations§
impl !Freeze for BinanceMargin
impl !RefUnwindSafe for BinanceMargin
impl !UnwindSafe for BinanceMargin
impl Send for BinanceMargin
impl Sync for BinanceMargin
impl Unpin for BinanceMargin
impl UnsafeUnpin for BinanceMargin
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> FmtForward for T
impl<T> FmtForward for T
Source§fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
self to use its Binary implementation when Debug-formatted.Source§fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
self to use its Display implementation when
Debug-formatted.Source§fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
self to use its LowerExp implementation when
Debug-formatted.Source§fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
self to use its LowerHex implementation when
Debug-formatted.Source§fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
self to use its Octal implementation when Debug-formatted.Source§fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
self to use its Pointer implementation when
Debug-formatted.Source§fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
self to use its UpperExp implementation when
Debug-formatted.Source§fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
self to use its UpperHex implementation when
Debug-formatted.Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreimpl<T> JsonSchemaMaybe for T
Source§impl<T> Pipe for Twhere
T: ?Sized,
impl<T> Pipe for Twhere
T: ?Sized,
Source§fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
Source§fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read moreSource§fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
Source§fn pipe_borrow_mut<'a, B, R>(
&'a mut self,
func: impl FnOnce(&'a mut B) -> R,
) -> R
fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
Source§fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
self, then passes self.as_ref() into the pipe function.Source§fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
self, then passes self.as_mut() into the pipe
function.Source§fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
self, then passes self.deref() into the pipe function.Source§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
Source§impl<T> Tap for T
impl<T> Tap for T
Source§fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
Borrow<B> of a value. Read moreSource§fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
BorrowMut<B> of a value. Read moreSource§fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
AsRef<R> view of a value. Read moreSource§fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
AsMut<R> view of a value. Read moreSource§fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
Deref::Target of a value. Read moreSource§fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
.tap() only in debug builds, and is erased in release builds.Source§fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
.tap_mut() only in debug builds, and is erased in release
builds.Source§fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
.tap_borrow() only in debug builds, and is erased in release
builds.Source§fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
.tap_borrow_mut() only in debug builds, and is erased in release
builds.Source§fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
.tap_ref() only in debug builds, and is erased in release
builds.Source§fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
.tap_ref_mut() only in debug builds, and is erased in release
builds.Source§fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
.tap_deref() only in debug builds, and is erased in release
builds.