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 SDK’s retired 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 unsupported
TrailingStop / TrailingStopLimit return OrderError::UnsupportedOrderType: the binance-sdk
margin new-order binding omits trailingDelta. See open_order.
§User-data stream (userListenToken)
account_stream is hand-rolled over the userListenToken model — the
legacy margin listen-key user-data API was retired by Binance on 2026-02-20 and the SDK binds
only the dead endpoint. 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.
§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
userLiabilityChange 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).
§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.
Trait Implementations§
Source§impl Clone for BinanceMargin
impl Clone for BinanceMargin
Source§fn clone(&self) -> BinanceMargin
fn clone(&self) -> BinanceMargin
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§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>,
) -> Option<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>
async fn open_order( &self, request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>, ) -> Option<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.- Trailing-stop kinds return
OrderError::UnsupportedOrderType(the SDK omitstrailingDeltaon the margin binding).
Source§async fn cancel_order(
&self,
request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
) -> Option<UnindexedOrderResponseCancel>
async fn cancel_order( &self, request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>, ) -> Option<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 (Design decision #2).
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 (Design decision #4).
Source§async fn fetch_trades(
&self,
time_since: DateTime<Utc>,
instruments: &[InstrumentNameExchange],
) -> Result<Vec<Trade<AssetNameExchange, InstrumentNameExchange>>, UnindexedClientError>
async fn fetch_trades( &self, time_since: DateTime<Utc>, instruments: &[InstrumentNameExchange], ) -> Result<Vec<Trade<AssetNameExchange, InstrumentNameExchange>>, UnindexedClientError>
Fetch margin trades (fills) since time_since, 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 returns an empty Vec;
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).
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).
§Debt cold-start (Design decision #4)
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; userLiabilityChange is logged observably, not applied to
balance state.
§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. Callers must also call
ExecutionClient::fetch_open_orders after each reconnect to reconcile order state — only
TRADE fills are recovered, not order-lifecycle events.
§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
type Config = BinanceMarginConfig
type AccountStream = Pin<Box<dyn Stream<Item = AccountEvent<ExchangeId, AssetNameExchange, InstrumentNameExchange>> + Send>>
fn cancel_orders<'a>( &self, requests: impl IntoIterator<Item = OrderRequestCancel<ExchangeId, &'a InstrumentNameExchange>>, ) -> impl Stream<Item = Option<UnindexedOrderResponseCancel>> + Send
fn open_orders<'a>( &self, requests: impl IntoIterator<Item = OrderRequestOpen<ExchangeId, &'a InstrumentNameExchange>>, ) -> impl Stream<Item = Option<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>> + Send
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.