Skip to main content

nautilus_binance/futures/
execution.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Live execution client implementation for the Binance Futures adapter.
17
18use std::{
19    future::Future,
20    sync::{
21        Arc, Mutex, RwLock,
22        atomic::{AtomicBool, Ordering},
23    },
24    time::Duration,
25};
26
27use anyhow::Context;
28use async_trait::async_trait;
29use dashmap::DashMap;
30use nautilus_common::{
31    cache::fifo::FifoCache,
32    clients::ExecutionClient,
33    live::{get_runtime, runner::get_exec_event_sender},
34    messages::execution::{
35        BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
36        GenerateOrderStatusReport, GenerateOrderStatusReports, GenerateOrderStatusReportsBuilder,
37        GeneratePositionStatusReports, GeneratePositionStatusReportsBuilder, ModifyOrder,
38        QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
39    },
40};
41use nautilus_core::{
42    AtomicSet, MUTEX_POISONED, UUID4, UnixNanos,
43    datetime::{NANOSECONDS_IN_MILLISECOND, mins_to_nanos},
44    time::{AtomicTime, get_atomic_clock_realtime},
45};
46use nautilus_live::{ExecutionClientCore, ExecutionEventEmitter};
47use nautilus_model::{
48    accounts::AccountAny,
49    enums::{
50        AccountType, ContingencyType, OmsType, OrderType, PositionSideSpecified,
51        TrailingOffsetType, TriggerType,
52    },
53    events::{
54        AccountState, OrderCancelRejected, OrderCanceled, OrderEventAny, OrderModifyRejected,
55        OrderRejected, OrderUpdated,
56    },
57    identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, Venue, VenueOrderId},
58    instruments::Instrument,
59    orders::{Order, OrderAny},
60    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
61    types::{AccountBalance, Currency, MarginBalance, Money, Quantity},
62};
63use rust_decimal::Decimal;
64use tokio::{sync::Mutex as TokioMutex, task::JoinHandle};
65use tokio_util::sync::CancellationToken;
66
67use super::{
68    http::{
69        BinanceFuturesHttpError,
70        client::{BinanceFuturesHttpClient, BinanceFuturesInstrument, is_algo_order_type},
71        models::{BatchOrderResult, BinancePositionRisk},
72        query::{
73            BatchCancelItem, BinanceAllOrdersParamsBuilder, BinanceOpenOrdersParamsBuilder,
74            BinanceOrderQueryParamsBuilder, BinancePositionRiskParamsBuilder,
75            BinanceSetLeverageParams, BinanceSetMarginTypeParams, BinanceUserTradesParamsBuilder,
76        },
77    },
78    websocket::{
79        streams::{
80            client::BinanceFuturesWebSocketClient,
81            dispatch::{DispatchCtx, dispatch_user_stream_message, spawn_user_stream_dispatch},
82            recovery::{
83                RecoveryCtx, WsBuildParams, build_and_connect_user_stream, run_recovery_driver,
84            },
85        },
86        trading::{client::BinanceFuturesWsTradingClient, dispatch::dispatch_ws_trading_message},
87    },
88};
89use crate::{
90    common::{
91        consts::{
92            BINANCE_FUTURES_DUAL_SIDE_SYNC_REJECT_CODE, BINANCE_FUTURES_USD_WS_API_TESTNET_URL,
93            BINANCE_FUTURES_USD_WS_API_URL, BINANCE_GTX_ORDER_REJECT_CODE,
94            BINANCE_NAUTILUS_FUTURES_BROKER_ID, BINANCE_STATUS_UNKNOWN_CODE,
95            BINANCE_UNEXPECTED_RESPONSE_CODE, BINANCE_VENUE,
96        },
97        credential::resolve_credentials,
98        dispatch::{OrderIdentity, PendingOperation, PendingRequest, WsDispatchState},
99        encoder::encode_broker_id,
100        enums::{
101            BinanceEnvironment, BinanceFuturesOrderType, BinancePositionSide, BinancePriceMatch,
102            BinanceProductType, BinanceSide, BinanceTimeInForce, BinanceWorkingType,
103        },
104        symbol::format_binance_symbol,
105        urls::{get_usdm_ws_route_base_url, get_ws_private_base_url},
106    },
107    config::BinanceExecClientConfig,
108    futures::{
109        conversions::{
110            determine_position_side, normalize_futures_asset, reduce_only_param,
111            trailing_offset_to_callback_rate, trailing_offset_to_callback_rate_string,
112        },
113        http::{
114            client::order_type_to_binance_futures,
115            models::BinanceFuturesAccountInfo,
116            query::{
117                BatchOrderItem, BinanceCancelOrderParamsBuilder, BinanceModifyOrderParamsBuilder,
118                BinanceNewOrderParams,
119            },
120        },
121    },
122};
123
124/// Listen key keepalive interval (30 minutes).
125const LISTEN_KEY_KEEPALIVE_SECS: u64 = 30 * 60;
126
127/// Consecutive keepalive failures before a listenKey rotation is triggered.
128const MAX_KEEPALIVE_FAILURES: u32 = 1;
129
130/// Live execution client for Binance Futures trading.
131///
132/// Implements the [`ExecutionClient`] trait for order management on Binance
133/// USD-M and COIN-M Futures markets. Uses HTTP API for order operations and
134/// WebSocket for real-time order updates via user data stream.
135///
136/// Uses a two-tier architecture with an execution handler that maintains
137/// pending order maps for correlating WebSocket updates with order context.
138#[derive(Debug)]
139pub struct BinanceFuturesExecutionClient {
140    core: ExecutionClientCore,
141    clock: &'static AtomicTime,
142    config: BinanceExecClientConfig,
143    emitter: ExecutionEventEmitter,
144    dispatch_state: Arc<WsDispatchState>,
145    product_type: BinanceProductType,
146    http_client: BinanceFuturesHttpClient,
147    ws_client: Arc<TokioMutex<Option<BinanceFuturesWebSocketClient>>>,
148    ws_trading_client: Option<BinanceFuturesWsTradingClient>,
149    ws_trading_handle: Mutex<Option<JoinHandle<()>>>,
150    listen_key: Arc<RwLock<Option<String>>>,
151    cancellation_token: CancellationToken,
152    triggered_algo_order_ids: Arc<AtomicSet<ClientOrderId>>,
153    algo_client_order_ids: Arc<AtomicSet<ClientOrderId>>,
154    ws_task: Arc<Mutex<Option<JoinHandle<()>>>>,
155    keepalive_task: Mutex<Option<JoinHandle<()>>>,
156    recovery_task: Mutex<Option<JoinHandle<()>>>,
157    recovery_lock: Arc<TokioMutex<()>>,
158    recovery_tx: Mutex<Option<tokio::sync::mpsc::UnboundedSender<()>>>,
159    pending_tasks: Mutex<Vec<JoinHandle<()>>>,
160    is_hedge_mode: AtomicBool,
161}
162
163impl BinanceFuturesExecutionClient {
164    /// Creates a new [`BinanceFuturesExecutionClient`].
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if the HTTP client fails to initialize, credentials are
169    /// missing, or the product type is not a futures type (UsdM or CoinM).
170    pub fn new(core: ExecutionClientCore, config: BinanceExecClientConfig) -> anyhow::Result<Self> {
171        let product_type = config.product_type;
172        match product_type {
173            BinanceProductType::UsdM | BinanceProductType::CoinM => {}
174            _ => {
175                anyhow::bail!(
176                    "BinanceFuturesExecutionClient requires UsdM or CoinM product type, was {product_type:?}"
177                );
178            }
179        }
180
181        let (api_key, api_secret) = resolve_credentials(
182            config.api_key.clone(),
183            config.api_secret.clone(),
184            config.environment,
185            product_type,
186        )?;
187
188        let clock = get_atomic_clock_realtime();
189
190        let http_client = BinanceFuturesHttpClient::new(
191            product_type,
192            config.environment,
193            clock,
194            Some(api_key.clone()),
195            Some(api_secret.clone()),
196            config.base_url_http.clone(),
197            None, // recv_window
198            None, // timeout_secs
199            None, // proxy_url
200            config.treat_expired_as_canceled,
201        )
202        .context("failed to construct Binance Futures HTTP client")?;
203
204        let ws_trading_client = if config.use_ws_trading && product_type == BinanceProductType::UsdM
205        {
206            let ws_trading_url =
207                config
208                    .base_url_ws_trading
209                    .clone()
210                    .or_else(|| match config.environment {
211                        BinanceEnvironment::Testnet | BinanceEnvironment::Demo => {
212                            Some(BINANCE_FUTURES_USD_WS_API_TESTNET_URL.to_string())
213                        }
214                        _ => Some(BINANCE_FUTURES_USD_WS_API_URL.to_string()),
215                    });
216
217            Some(BinanceFuturesWsTradingClient::new(
218                ws_trading_url,
219                api_key,
220                api_secret,
221                None, // heartbeat
222                config.transport_backend,
223            ))
224        } else {
225            None
226        };
227
228        let emitter = ExecutionEventEmitter::new(
229            clock,
230            core.trader_id,
231            core.account_id,
232            core.account_type,
233            core.base_currency,
234        );
235
236        Ok(Self {
237            core,
238            clock,
239            config,
240            emitter,
241            dispatch_state: Arc::new(WsDispatchState::default()),
242            product_type,
243            http_client,
244            ws_client: Arc::new(TokioMutex::new(None)),
245            ws_trading_client,
246            ws_trading_handle: Mutex::new(None),
247            listen_key: Arc::new(RwLock::new(None)),
248            cancellation_token: CancellationToken::new(),
249            triggered_algo_order_ids: Arc::new(AtomicSet::new()),
250            algo_client_order_ids: Arc::new(AtomicSet::new()),
251            ws_task: Arc::new(Mutex::new(None)),
252            keepalive_task: Mutex::new(None),
253            recovery_task: Mutex::new(None),
254            recovery_lock: Arc::new(TokioMutex::new(())),
255            recovery_tx: Mutex::new(None),
256            pending_tasks: Mutex::new(Vec::new()),
257            is_hedge_mode: AtomicBool::new(false),
258        })
259    }
260
261    /// Returns whether the account is in hedge mode (dual side position).
262    #[must_use]
263    pub fn is_hedge_mode(&self) -> bool {
264        self.is_hedge_mode.load(Ordering::Acquire)
265    }
266
267    /// Returns a clone of the HTTP client's instruments cache Arc.
268    #[doc(hidden)]
269    #[must_use]
270    pub fn instruments_cache(&self) -> Arc<DashMap<ustr::Ustr, BinanceFuturesInstrument>> {
271        self.http_client.instruments_cache()
272    }
273
274    /// Converts Binance futures account info to Nautilus account state.
275    fn create_account_state(&self, account_info: &BinanceFuturesAccountInfo) -> AccountState {
276        Self::create_account_state_from(
277            account_info,
278            self.core.account_id,
279            self.core.account_type,
280            self.config.bnfcr_currency,
281            self.clock,
282        )
283    }
284
285    fn create_account_state_from(
286        account_info: &BinanceFuturesAccountInfo,
287        account_id: AccountId,
288        account_type: AccountType,
289        bnfcr_currency: Currency,
290        clock: &'static AtomicTime,
291    ) -> AccountState {
292        let ts_now = clock.get_time_ns();
293
294        let balances: Vec<AccountBalance> = account_info
295            .assets
296            .iter()
297            .filter_map(|b| {
298                if b.wallet_balance.is_zero() {
299                    return None;
300                }
301
302                let currency = normalize_futures_asset(b.asset, bnfcr_currency);
303                AccountBalance::from_total_and_free(b.wallet_balance, b.available_balance, currency)
304                    .ok()
305            })
306            .collect();
307
308        // Emit account-wide (cross-margin) margin balances per collateral asset.
309        // Binance reports per-asset `initialMargin` / `maintMargin` which covers both
310        // USDT-M (typically USDT, or USDT+BNB under multi-assets mode) and COIN-M
311        // (one entry per base coin, e.g. BTC / ETH).
312        let mut margins: Vec<MarginBalance> = Vec::new();
313
314        for asset in &account_info.assets {
315            let initial_dec = asset.initial_margin.unwrap_or_default();
316            let maint_dec = asset.maint_margin.unwrap_or_default();
317
318            if initial_dec.is_zero() && maint_dec.is_zero() {
319                continue;
320            }
321
322            let currency = normalize_futures_asset(asset.asset, bnfcr_currency);
323            let initial = Money::from_decimal(initial_dec, currency)
324                .unwrap_or_else(|_| Money::zero(currency));
325            let maintenance =
326                Money::from_decimal(maint_dec, currency).unwrap_or_else(|_| Money::zero(currency));
327            margins.push(MarginBalance::new(initial, maintenance, None));
328        }
329
330        AccountState::new(
331            account_id,
332            account_type,
333            balances,
334            margins,
335            true, // reported
336            UUID4::new(),
337            ts_now,
338            ts_now,
339            None, // base currency
340        )
341    }
342
343    async fn refresh_account_state(&self) -> anyhow::Result<AccountState> {
344        let account_info = match self.http_client.query_account().await {
345            Ok(info) => info,
346            Err(e) => {
347                log::error!("Binance Futures account state request failed: {e}");
348                anyhow::bail!("Binance Futures account state request failed: {e}");
349            }
350        };
351
352        Ok(self.create_account_state(&account_info))
353    }
354
355    fn update_account_state(&self) {
356        let http_client = self.http_client.clone();
357        let account_id = self.core.account_id;
358        let account_type = self.core.account_type;
359        let bnfcr_currency = self.config.bnfcr_currency;
360        let emitter = self.emitter.clone();
361        let clock = self.clock;
362
363        self.spawn_task("query_account", async move {
364            let account_info = http_client
365                .query_account()
366                .await
367                .context("Binance Futures account state request failed")?;
368            let account_state = Self::create_account_state_from(
369                &account_info,
370                account_id,
371                account_type,
372                bnfcr_currency,
373                clock,
374            );
375            let ts_now = clock.get_time_ns();
376            emitter.emit_account_state(
377                account_state.balances.clone(),
378                account_state.margins.clone(),
379                account_state.is_reported,
380                ts_now,
381            );
382            Ok(())
383        });
384    }
385
386    async fn init_hedge_mode(&self) -> anyhow::Result<bool> {
387        let response = self.http_client.query_hedge_mode().await?;
388        Ok(response.dual_side_position)
389    }
390
391    /// Returns whether the WS trading client is connected and active.
392    fn ws_trading_active(&self) -> bool {
393        self.ws_trading_client
394            .as_ref()
395            .is_some_and(|c| c.is_active())
396    }
397
398    fn submit_order_internal(&self, cmd: &SubmitOrder) -> anyhow::Result<()> {
399        let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
400
401        let emitter = self.emitter.clone();
402        let trader_id = self.core.trader_id;
403        let account_id = self.core.account_id;
404        let clock = self.clock;
405        let client_order_id = order.client_order_id();
406        let strategy_id = order.strategy_id();
407        let instrument_id = order.instrument_id();
408        let order_side = order.order_side();
409        let order_type = order.order_type();
410        let quantity = order.quantity();
411        let time_in_force = order.time_in_force();
412        let price = order.price();
413        let trigger_price = order.trigger_price();
414        let reduce_only = order.is_reduce_only();
415        let post_only = order.is_post_only();
416        let activation_price = order.activation_price();
417        let trailing_offset = order.trailing_offset();
418        let trigger_type = order.trigger_type();
419        let position_side = determine_position_side(self.is_hedge_mode(), order_side, reduce_only);
420
421        // Register identity for tracked/external dispatch routing
422        self.dispatch_state.order_identities.insert(
423            client_order_id,
424            OrderIdentity {
425                instrument_id,
426                strategy_id,
427                order_side,
428                order_type,
429                price,
430                quantity,
431            },
432        );
433
434        let use_algo_api = is_algo_order_type(order_type);
435
436        let close_position = cmd
437            .params
438            .as_ref()
439            .and_then(|p| p.get_bool("close_position"))
440            .unwrap_or(false);
441
442        let price_match = cmd
443            .params
444            .as_ref()
445            .and_then(|p| p.get_str("price_match"))
446            .map(BinancePriceMatch::from_param)
447            .transpose()?;
448
449        let callback_rate = trailing_offset
450            .map(trailing_offset_to_callback_rate_string)
451            .transpose()?;
452
453        let working_type = match trigger_type {
454            Some(TriggerType::MarkPrice) => Some(BinanceWorkingType::MarkPrice),
455            Some(TriggerType::LastPrice | TriggerType::Default) => {
456                Some(BinanceWorkingType::ContractPrice)
457            }
458            _ => None,
459        };
460
461        // Non-algo orders can route through WS trading API when active
462        if self.ws_trading_active() && !use_algo_api {
463            let ws_client = self.ws_trading_client.as_ref().unwrap().clone();
464            let dispatch_state = self.dispatch_state.clone();
465
466            let symbol = format_binance_symbol(&instrument_id);
467            let binance_side = BinanceSide::try_from(order_side)?;
468            let binance_order_type = order_type_to_binance_futures(order_type)?;
469            let binance_tif = if post_only {
470                BinanceTimeInForce::Gtx
471            } else {
472                BinanceTimeInForce::try_from(time_in_force)?
473            };
474
475            let requires_time_in_force = matches!(
476                order_type,
477                OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
478            );
479
480            let client_id_str =
481                encode_broker_id(&client_order_id, BINANCE_NAUTILUS_FUTURES_BROKER_ID);
482
483            let params = BinanceNewOrderParams {
484                symbol,
485                side: binance_side,
486                order_type: binance_order_type,
487                time_in_force: if requires_time_in_force {
488                    Some(binance_tif)
489                } else {
490                    None
491                },
492                quantity: Some(quantity.to_string()),
493                price: if price_match.is_some() {
494                    None
495                } else {
496                    price.map(|p| p.to_string())
497                },
498                new_client_order_id: Some(client_id_str),
499                stop_price: trigger_price.map(|p| p.to_string()),
500                reduce_only: reduce_only_param(reduce_only, position_side),
501                position_side,
502                close_position: None,
503                activation_price: activation_price.map(|p| p.to_string()),
504                callback_rate,
505                working_type,
506                price_protect: None,
507                new_order_resp_type: None,
508                good_till_date: None,
509                recv_window: None,
510                price_match,
511                self_trade_prevention_mode: None,
512            };
513
514            // Pre-register before sending to avoid response racing the insert
515            let request_id = ws_client.next_request_id();
516            dispatch_state.pending_requests.insert(
517                request_id.clone(),
518                PendingRequest {
519                    client_order_id,
520                    venue_order_id: None,
521                    operation: PendingOperation::Place,
522                },
523            );
524
525            self.spawn_task("submit_order_ws", async move {
526                if let Err(e) = ws_client
527                    .place_order_with_id(request_id.clone(), params)
528                    .await
529                {
530                    dispatch_state.pending_requests.remove(&request_id);
531                    log::error!("WS submit request failed for {client_order_id}: {e}");
532                    anyhow::bail!("WS submit order failed: {e}");
533                }
534                Ok(())
535            });
536
537            return Ok(());
538        }
539
540        let http_client = self.http_client.clone();
541
542        self.spawn_task("submit_order", async move {
543            let result = if use_algo_api {
544                http_client
545                    .submit_algo_order(
546                        account_id,
547                        instrument_id,
548                        client_order_id,
549                        order_side,
550                        order_type,
551                        quantity,
552                        time_in_force,
553                        price,
554                        trigger_price,
555                        reduce_only,
556                        close_position,
557                        position_side,
558                        activation_price,
559                        callback_rate,
560                        working_type,
561                    )
562                    .await
563            } else {
564                http_client
565                    .submit_order(
566                        account_id,
567                        instrument_id,
568                        client_order_id,
569                        order_side,
570                        order_type,
571                        quantity,
572                        time_in_force,
573                        price,
574                        trigger_price,
575                        reduce_only,
576                        post_only,
577                        position_side,
578                        price_match,
579                    )
580                    .await
581            };
582
583            match result {
584                Ok(report) => {
585                    log::debug!(
586                        "Order submit accepted: client_order_id={}, venue_order_id={}",
587                        client_order_id,
588                        report.venue_order_id
589                    );
590                }
591                Err(e) => {
592                    // Keep order registered - if HTTP failed due to timeout but order
593                    // reached Binance, WebSocket updates will still arrive. The order
594                    // will be cleaned up via WebSocket rejection or reconciliation.
595                    if is_ambiguous_submit_error(&e) {
596                        log::warn!(
597                            "Ambiguous submit failure for {client_order_id}, awaiting reconciliation: {e}"
598                        );
599                    } else if is_structured_venue_rejection(&e) {
600                        let due_post_only = classify_submit_order_error(&e);
601                        let ts_now = clock.get_time_ns();
602
603                        let rejected = OrderRejected::new(
604                            trader_id,
605                            strategy_id,
606                            instrument_id,
607                            client_order_id,
608                            account_id,
609                            format!("submit-order-error: {e}").into(),
610                            UUID4::new(),
611                            ts_now,
612                            ts_now,
613                            false,
614                            due_post_only,
615                        );
616
617                        emitter.send_order_event(OrderEventAny::Rejected(rejected));
618                    } else {
619                        log::warn!(
620                            "Ambiguous submit failure for {client_order_id}, awaiting reconciliation: {e}"
621                        );
622                    }
623
624                    return Err(e);
625                }
626            }
627
628            Ok(())
629        });
630
631        Ok(())
632    }
633
634    fn cancel_order_internal(&self, cmd: &CancelOrder) {
635        let command = cmd.clone();
636
637        // Non-triggered algo orders use algo cancel endpoint, triggered use regular
638        let is_algo = self
639            .core
640            .cache()
641            .order(&command.client_order_id)
642            .is_some_and(|order| is_algo_order_type(order.order_type()));
643        let is_triggered = self
644            .triggered_algo_order_ids
645            .contains(&command.client_order_id);
646        let use_algo_cancel = is_algo && !is_triggered;
647
648        let emitter = self.emitter.clone();
649        let trader_id = self.core.trader_id;
650        let account_id = self.core.account_id;
651        let clock = self.clock;
652        let instrument_id = command.instrument_id;
653        let venue_order_id = command.venue_order_id;
654        let client_order_id = command.client_order_id;
655
656        // Non-algo cancels can route through WS trading API when active
657        if self.ws_trading_active() && !use_algo_cancel {
658            let ws_client = self.ws_trading_client.as_ref().unwrap().clone();
659            let dispatch_state = self.dispatch_state.clone();
660
661            let mut cancel_builder = BinanceCancelOrderParamsBuilder::default();
662            cancel_builder.symbol(format_binance_symbol(&instrument_id));
663
664            if let Some(venue_id) = venue_order_id {
665                match venue_id.inner().parse::<i64>() {
666                    Ok(order_id) => {
667                        cancel_builder.order_id(order_id);
668                    }
669                    Err(e) => {
670                        log::warn!(
671                            "Unable to parse venue_order_id {venue_id} for cancel {client_order_id}, canceling by client_order_id: {e}"
672                        );
673                    }
674                }
675            }
676
677            cancel_builder.orig_client_order_id(encode_broker_id(
678                &client_order_id,
679                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
680            ));
681
682            let params = cancel_builder.build().unwrap();
683
684            // Pre-register before sending to avoid response racing the insert
685            let request_id = ws_client.next_request_id();
686            dispatch_state.pending_requests.insert(
687                request_id.clone(),
688                PendingRequest {
689                    client_order_id,
690                    venue_order_id,
691                    operation: PendingOperation::Cancel,
692                },
693            );
694
695            self.spawn_task("cancel_order_ws", async move {
696                if let Err(e) = ws_client
697                    .cancel_order_with_id(request_id.clone(), params)
698                    .await
699                {
700                    dispatch_state.pending_requests.remove(&request_id);
701                    log::error!("WS cancel request failed for {client_order_id}: {e}");
702                    anyhow::bail!("WS cancel order failed: {e}");
703                }
704                Ok(())
705            });
706
707            return;
708        }
709
710        let http_client = self.http_client.clone();
711
712        self.spawn_task("cancel_order", async move {
713            let result = if use_algo_cancel {
714                // Try algo cancel first; if it fails, the order may have been triggered
715                // before this session started, so fall back to regular cancel
716                match http_client.cancel_algo_order(client_order_id).await {
717                    Ok(()) => Ok(()),
718                    Err(algo_err) => {
719                        log::debug!("Algo cancel failed, trying regular cancel: {algo_err}");
720                        http_client
721                            .cancel_order(instrument_id, venue_order_id, Some(client_order_id))
722                            .await
723                            .map(|_| ())
724                    }
725                }
726            } else {
727                http_client
728                    .cancel_order(instrument_id, venue_order_id, Some(client_order_id))
729                    .await
730                    .map(|_| ())
731            };
732
733            match result {
734                Ok(()) => {
735                    log::debug!("Cancel request accepted: client_order_id={client_order_id}");
736                }
737                Err(e) => {
738                    if is_structured_venue_rejection(&e) {
739                        let ts_now = clock.get_time_ns();
740
741                        let rejected = OrderCancelRejected::new(
742                            trader_id,
743                            command.strategy_id,
744                            command.instrument_id,
745                            client_order_id,
746                            format!("cancel-order-error: {e}").into(),
747                            UUID4::new(),
748                            ts_now,
749                            ts_now,
750                            false,
751                            command.venue_order_id,
752                            Some(account_id),
753                        );
754
755                        emitter.send_order_event(OrderEventAny::CancelRejected(rejected));
756                    } else if is_local_command_failure(&e) {
757                        log::warn!(
758                            "Cancel command failed local validation for {client_order_id}: {e}"
759                        );
760                    } else {
761                        log::warn!(
762                            "Ambiguous cancel failure for {client_order_id}, awaiting reconciliation: {e}"
763                        );
764                    }
765
766                    return Err(e);
767                }
768            }
769
770            Ok(())
771        });
772    }
773
774    fn spawn_task<F>(&self, description: &'static str, fut: F)
775    where
776        F: Future<Output = anyhow::Result<()>> + Send + 'static,
777    {
778        crate::common::execution::spawn_task(&self.pending_tasks, description, fut);
779    }
780
781    fn abort_pending_tasks(&self) {
782        crate::common::execution::abort_pending_tasks(&self.pending_tasks);
783    }
784
785    /// Returns the (price_precision, size_precision) for an instrument.
786    fn get_instrument_precision(&self, instrument_id: InstrumentId) -> (u8, u8) {
787        let cache = self.core.cache();
788        cache
789            .instrument(&instrument_id)
790            .map_or((8, 8), |i| (i.price_precision(), i.size_precision()))
791    }
792
793    /// Creates a position status report from Binance position risk data.
794    fn create_position_report(
795        &self,
796        position: &BinancePositionRisk,
797        instrument_id: InstrumentId,
798        size_precision: u8,
799    ) -> anyhow::Result<PositionStatusReport> {
800        let position_amount: Decimal = position
801            .position_amt
802            .parse()
803            .context("invalid position_amt")?;
804
805        if position_amount.is_zero() {
806            anyhow::bail!("Position is flat");
807        }
808
809        let entry_price: Decimal = position
810            .entry_price
811            .parse()
812            .context("invalid entry_price")?;
813
814        let position_side = if position_amount > Decimal::ZERO {
815            PositionSideSpecified::Long
816        } else {
817            PositionSideSpecified::Short
818        };
819
820        let ts_now = self.clock.get_time_ns();
821
822        Ok(PositionStatusReport::new(
823            self.core.account_id,
824            instrument_id,
825            position_side,
826            Quantity::from_decimal_dp(position_amount.abs(), size_precision)?,
827            ts_now,
828            ts_now,
829            Some(UUID4::new()),
830            None, // venue_position_id
831            Some(entry_price),
832        ))
833    }
834
835    async fn apply_futures_config(&self) -> anyhow::Result<()> {
836        if let Some(ref leverages) = self.config.futures_leverages {
837            for (symbol, leverage) in leverages {
838                let params = BinanceSetLeverageParams {
839                    symbol: symbol.clone(),
840                    leverage: *leverage,
841                    recv_window: None,
842                };
843                // Best-effort: a venue reject is non-fatal, but transport errors
844                // propagate so an unhealthy connection still surfaces.
845                match self.http_client.set_leverage(&params).await {
846                    Ok(response) => {
847                        log::info!("Set leverage {} {}X", response.symbol, response.leverage);
848                    }
849                    Err(BinanceFuturesHttpError::BinanceError { code, message }) => {
850                        log::warn!(
851                            "Unable to set leverage for {symbol} to {leverage}x: [{code}] {message}; skipping (leverage init is best-effort)"
852                        );
853                    }
854                    Err(e) => {
855                        return Err(e).context(format!("failed to set leverage for {symbol}"));
856                    }
857                }
858            }
859        }
860
861        if let Some(ref margin_types) = self.config.futures_margin_types {
862            for (symbol, margin_type) in margin_types {
863                let params = BinanceSetMarginTypeParams {
864                    symbol: symbol.clone(),
865                    margin_type: *margin_type,
866                    recv_window: None,
867                };
868
869                match self.http_client.set_margin_type(&params).await {
870                    Ok(_) => {
871                        log::info!("Set {symbol} margin type to {margin_type:?}");
872                    }
873                    Err(e) => {
874                        let err_str = format!("{e}");
875                        if err_str.contains("-4046") {
876                            log::debug!("{symbol} margin type already {margin_type:?}");
877                        } else {
878                            return Err(e)
879                                .context(format!("failed to set margin type for {symbol}"));
880                        }
881                    }
882                }
883            }
884        }
885
886        Ok(())
887    }
888}
889
890fn build_futures_order_list_batch(
891    orders: &[OrderAny],
892    is_hedge_mode: bool,
893    close_position: bool,
894    price_match: Option<BinancePriceMatch>,
895) -> Result<Vec<BatchOrderItem>, String> {
896    if orders.len() > 5 {
897        return Err(format!(
898            "Binance Futures batch order submission supports at most 5 orders, was {}",
899            orders.len()
900        ));
901    }
902
903    if close_position {
904        return Err(
905            "`close_position` is not supported for Binance Futures batch order submission"
906                .to_string(),
907        );
908    }
909
910    if orders.iter().any(is_grouped_order) {
911        return Err(
912            "Binance Futures linked order-list contingencies require adapter-level OCO-on-fill state"
913                .to_string(),
914        );
915    }
916
917    if let Some(order) = orders
918        .iter()
919        .find(|order| is_algo_order_type(order.order_type()))
920    {
921        return Err(format!(
922            "Binance Futures batch order submission does not support conditional order type {:?}",
923            order.order_type()
924        ));
925    }
926
927    let mut batch_items = Vec::with_capacity(orders.len());
928    for order in orders {
929        if price_match.is_some() && order.is_post_only() {
930            return Err("price_match cannot be combined with post-only orders".to_string());
931        }
932
933        if price_match.is_some() && order.order_type() != OrderType::Limit {
934            return Err(format!(
935                "price_match is not supported for order type {:?}",
936                order.order_type()
937            ));
938        }
939
940        let binance_side = BinanceSide::try_from(order.order_side()).map_err(|e| e.to_string())?;
941        let binance_order_type =
942            order_type_to_binance_futures(order.order_type()).map_err(|e| e.to_string())?;
943        let binance_tif = if order.is_post_only() {
944            BinanceTimeInForce::Gtx
945        } else {
946            BinanceTimeInForce::try_from(order.time_in_force()).map_err(|e| e.to_string())?
947        };
948        let position_side =
949            determine_position_side(is_hedge_mode, order.order_side(), order.is_reduce_only());
950        let requires_time_in_force = matches!(order.order_type(), OrderType::Limit);
951
952        batch_items.push(BatchOrderItem {
953            symbol: format_binance_symbol(&order.instrument_id()),
954            side: binance_side_wire(binance_side).to_string(),
955            order_type: binance_futures_order_type_wire(binance_order_type).to_string(),
956            time_in_force: if requires_time_in_force {
957                Some(binance_time_in_force_wire(binance_tif).to_string())
958            } else {
959                None
960            },
961            quantity: Some(order.quantity().to_string()),
962            price: if price_match.is_some() {
963                None
964            } else {
965                order.price().map(|price| price.to_string())
966            },
967            reduce_only: reduce_only_param(order.is_reduce_only(), position_side),
968            new_client_order_id: Some(encode_broker_id(
969                &order.client_order_id(),
970                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
971            )),
972            stop_price: None,
973            position_side: position_side.map(|side| binance_position_side_wire(side).to_string()),
974            activation_price: None,
975            callback_rate: None,
976            working_type: None,
977            price_protect: None,
978            close_position: None,
979            good_till_date: None,
980            price_match: price_match
981                .and_then(binance_price_match_wire)
982                .map(str::to_string),
983            self_trade_prevention_mode: None,
984        });
985    }
986
987    Ok(batch_items)
988}
989
990fn is_grouped_order(order: &OrderAny) -> bool {
991    matches!(
992        order.contingency_type(),
993        Some(contingency_type) if contingency_type != ContingencyType::NoContingency
994    ) || order
995        .linked_order_ids()
996        .is_some_and(|linked_order_ids| !linked_order_ids.is_empty())
997}
998
999fn binance_side_wire(side: BinanceSide) -> &'static str {
1000    match side {
1001        BinanceSide::Buy => "BUY",
1002        BinanceSide::Sell => "SELL",
1003    }
1004}
1005
1006fn binance_futures_order_type_wire(order_type: BinanceFuturesOrderType) -> &'static str {
1007    match order_type {
1008        BinanceFuturesOrderType::Limit => "LIMIT",
1009        BinanceFuturesOrderType::Market => "MARKET",
1010        BinanceFuturesOrderType::Stop => "STOP",
1011        BinanceFuturesOrderType::StopMarket => "STOP_MARKET",
1012        BinanceFuturesOrderType::TakeProfit => "TAKE_PROFIT",
1013        BinanceFuturesOrderType::TakeProfitMarket => "TAKE_PROFIT_MARKET",
1014        BinanceFuturesOrderType::TrailingStopMarket => "TRAILING_STOP_MARKET",
1015        BinanceFuturesOrderType::Liquidation => "LIQUIDATION",
1016        BinanceFuturesOrderType::Adl => "ADL",
1017        BinanceFuturesOrderType::Unknown => "UNKNOWN",
1018    }
1019}
1020
1021fn binance_time_in_force_wire(time_in_force: BinanceTimeInForce) -> &'static str {
1022    match time_in_force {
1023        BinanceTimeInForce::Gtc => "GTC",
1024        BinanceTimeInForce::Ioc => "IOC",
1025        BinanceTimeInForce::Fok => "FOK",
1026        BinanceTimeInForce::Gtx => "GTX",
1027        BinanceTimeInForce::Gtd => "GTD",
1028        BinanceTimeInForce::Rpi => "RPI",
1029        BinanceTimeInForce::Unknown => "UNKNOWN",
1030    }
1031}
1032
1033fn binance_position_side_wire(position_side: BinancePositionSide) -> &'static str {
1034    match position_side {
1035        BinancePositionSide::Both => "BOTH",
1036        BinancePositionSide::Long => "LONG",
1037        BinancePositionSide::Short => "SHORT",
1038        BinancePositionSide::Unknown => "UNKNOWN",
1039    }
1040}
1041
1042fn binance_price_match_wire(price_match: BinancePriceMatch) -> Option<&'static str> {
1043    match price_match {
1044        BinancePriceMatch::None | BinancePriceMatch::Unknown => None,
1045        BinancePriceMatch::Opponent => Some("OPPONENT"),
1046        BinancePriceMatch::Opponent5 => Some("OPPONENT_5"),
1047        BinancePriceMatch::Opponent10 => Some("OPPONENT_10"),
1048        BinancePriceMatch::Opponent20 => Some("OPPONENT_20"),
1049        BinancePriceMatch::Queue => Some("QUEUE"),
1050        BinancePriceMatch::Queue5 => Some("QUEUE_5"),
1051        BinancePriceMatch::Queue10 => Some("QUEUE_10"),
1052        BinancePriceMatch::Queue20 => Some("QUEUE_20"),
1053    }
1054}
1055
1056/// Classifies a submit-order error for the rejection event.
1057///
1058/// Returns `true` when the venue indicated a post-only (GTX) rejection. Logs
1059/// a hint when the error matches the UM/CM `dualSidePosition` sync rejection
1060/// (`-4531`), which is an account/setup mismatch rather than a routing fault.
1061pub(crate) fn classify_submit_order_error(err: &anyhow::Error) -> bool {
1062    let venue_code = err
1063        .downcast_ref::<BinanceFuturesHttpError>()
1064        .and_then(|be| match be {
1065            BinanceFuturesHttpError::BinanceError { code, .. } => Some(*code),
1066            _ => None,
1067        });
1068
1069    if venue_code == Some(BINANCE_FUTURES_DUAL_SIDE_SYNC_REJECT_CODE) {
1070        log::warn!(
1071            "Order rejected by Binance Futures with code -4531 \
1072             (UM/CM dualSidePosition sync); confirm Portfolio Margin hedge mode \
1073             matches the order positionSide before resubmitting"
1074        );
1075    }
1076    venue_code == Some(BINANCE_GTX_ORDER_REJECT_CODE)
1077}
1078
1079fn is_structured_venue_rejection(err: &anyhow::Error) -> bool {
1080    err.downcast_ref::<BinanceFuturesHttpError>()
1081        .is_some_and(|be| matches!(be, BinanceFuturesHttpError::BinanceError { .. }))
1082}
1083
1084fn is_ambiguous_submit_error(err: &anyhow::Error) -> bool {
1085    err.downcast_ref::<BinanceFuturesHttpError>()
1086        .is_some_and(|be| {
1087            matches!(
1088                be,
1089                BinanceFuturesHttpError::BinanceError {
1090                    code: BINANCE_UNEXPECTED_RESPONSE_CODE | BINANCE_STATUS_UNKNOWN_CODE,
1091                    ..
1092                }
1093            )
1094        })
1095}
1096
1097fn is_local_command_failure(err: &anyhow::Error) -> bool {
1098    err.downcast_ref::<BinanceFuturesHttpError>()
1099        .is_some_and(is_local_http_command_failure)
1100}
1101
1102fn is_local_http_command_failure(err: &BinanceFuturesHttpError) -> bool {
1103    matches!(
1104        err,
1105        BinanceFuturesHttpError::MissingCredentials | BinanceFuturesHttpError::ValidationError(_)
1106    )
1107}
1108
1109#[async_trait(?Send)]
1110impl ExecutionClient for BinanceFuturesExecutionClient {
1111    fn is_connected(&self) -> bool {
1112        self.core.is_connected()
1113    }
1114
1115    fn client_id(&self) -> ClientId {
1116        self.core.client_id
1117    }
1118
1119    fn account_id(&self) -> AccountId {
1120        self.core.account_id
1121    }
1122
1123    fn venue(&self) -> Venue {
1124        *BINANCE_VENUE
1125    }
1126
1127    fn oms_type(&self) -> OmsType {
1128        self.core.oms_type
1129    }
1130
1131    fn get_account(&self) -> Option<AccountAny> {
1132        self.core.cache().account_owned(&self.core.account_id)
1133    }
1134
1135    async fn connect(&mut self) -> anyhow::Result<()> {
1136        if self.core.is_connected() {
1137            return Ok(());
1138        }
1139
1140        // Reinitialize cancellation token in case of reconnection
1141        self.cancellation_token = CancellationToken::new();
1142
1143        // Check hedge mode
1144        let is_hedge_mode = self
1145            .init_hedge_mode()
1146            .await
1147            .context("failed to query hedge mode")?;
1148        self.is_hedge_mode.store(is_hedge_mode, Ordering::Release);
1149        log::info!("Hedge mode (dual side position): {is_hedge_mode}");
1150
1151        // Load instruments if not already done
1152        let _instruments = if self.core.instruments_initialized() {
1153            Vec::new()
1154        } else {
1155            let instruments = self
1156                .http_client
1157                .request_instruments()
1158                .await
1159                .context("failed to request Binance Futures instruments")?;
1160
1161            if instruments.is_empty() {
1162                log::warn!("No instruments returned for Binance Futures");
1163            } else {
1164                log::debug!("Loaded {} Futures instruments", instruments.len());
1165            }
1166
1167            self.core.set_instruments_initialized();
1168            instruments
1169        };
1170
1171        // Apply configured leverage and margin types
1172        self.apply_futures_config()
1173            .await
1174            .context("failed to apply futures config")?;
1175
1176        // Create listen key for user data stream
1177        log::debug!("Creating listen key for user data stream...");
1178        let listen_key_response = self
1179            .http_client
1180            .create_listen_key()
1181            .await
1182            .context("failed to create listen key")?;
1183        let listen_key = listen_key_response.listen_key;
1184        log::debug!("Listen key created successfully");
1185
1186        {
1187            let mut key_guard = self.listen_key.write().expect(MUTEX_POISONED);
1188            *key_guard = Some(listen_key.clone());
1189        }
1190
1191        let (api_key, api_secret) = resolve_credentials(
1192            self.config.api_key.clone(),
1193            self.config.api_secret.clone(),
1194            self.config.environment,
1195            self.product_type,
1196        )?;
1197
1198        let private_base_url = self.config.base_url_ws.clone().map_or_else(
1199            || get_ws_private_base_url(self.product_type, self.config.environment).to_string(),
1200            |url| {
1201                if self.product_type == BinanceProductType::UsdM
1202                    && self.config.environment == BinanceEnvironment::Live
1203                {
1204                    get_usdm_ws_route_base_url(&url, "private")
1205                } else {
1206                    url
1207                }
1208            },
1209        );
1210
1211        let (recovery_tx, recovery_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
1212        *self.recovery_tx.lock().expect(MUTEX_POISONED) = Some(recovery_tx.clone());
1213
1214        let seen_trade_ids: Arc<Mutex<FifoCache<(ustr::Ustr, i64), 10_000>>> =
1215            Arc::new(Mutex::new(FifoCache::new()));
1216
1217        let dispatch_ctx = Arc::new(DispatchCtx {
1218            emitter: self.emitter.clone(),
1219            http_client: self.http_client.clone(),
1220            account_id: self.core.account_id,
1221            product_type: self.product_type,
1222            clock: self.clock,
1223            dispatch_state: self.dispatch_state.clone(),
1224            triggered_algo_ids: self.triggered_algo_order_ids.clone(),
1225            algo_client_ids: self.algo_client_order_ids.clone(),
1226            use_position_ids: self.config.use_position_ids,
1227            default_taker_fee: self.config.default_taker_fee,
1228            bnfcr_currency: self.config.bnfcr_currency,
1229            treat_expired_as_canceled: self.config.treat_expired_as_canceled,
1230            use_trade_lite: self.config.use_trade_lite,
1231            seen_trade_ids,
1232            cancellation_token: self.cancellation_token.clone(),
1233        });
1234
1235        let ws_build_params = WsBuildParams {
1236            product_type: self.product_type,
1237            environment: self.config.environment,
1238            api_key: api_key.clone(),
1239            api_secret: api_secret.clone(),
1240            private_base_url: private_base_url.clone(),
1241            transport_backend: self.config.transport_backend,
1242        };
1243
1244        let ws_client = build_and_connect_user_stream(&ws_build_params, &listen_key).await?;
1245        let stream = ws_client.stream();
1246        *self.ws_client.lock().await = Some(ws_client);
1247
1248        let ws_task = spawn_user_stream_dispatch(
1249            stream,
1250            dispatch_ctx.clone(),
1251            recovery_tx.clone(),
1252            dispatch_user_stream_message,
1253        );
1254        *self.ws_task.lock().expect(MUTEX_POISONED) = Some(ws_task);
1255
1256        // Start listen key keepalive task
1257        {
1258            let http_client = self.http_client.clone();
1259            let listen_key_ref = self.listen_key.clone();
1260            let cancel = self.cancellation_token.clone();
1261            let recovery_tx = recovery_tx.clone();
1262
1263            let keepalive_task = get_runtime().spawn(async move {
1264                let mut interval =
1265                    tokio::time::interval(Duration::from_secs(LISTEN_KEY_KEEPALIVE_SECS));
1266                let mut consecutive_failures: u32 = 0;
1267
1268                loop {
1269                    tokio::select! {
1270                        _ = interval.tick() => {
1271                            let key = {
1272                                let guard = listen_key_ref.read().expect(MUTEX_POISONED);
1273                                guard.clone()
1274                            };
1275
1276                            if let Some(ref key) = key {
1277                                match http_client.keepalive_listen_key(key).await {
1278                                    Ok(()) => {
1279                                        log::debug!("Listen key keepalive sent successfully");
1280                                        consecutive_failures = 0;
1281                                    }
1282                                    Err(e) => {
1283                                        consecutive_failures += 1;
1284                                        log::warn!(
1285                                            "Listen key keepalive failed ({consecutive_failures}/{MAX_KEEPALIVE_FAILURES}): {e}",
1286                                        );
1287
1288                                        if consecutive_failures >= MAX_KEEPALIVE_FAILURES
1289                                            && recovery_tx.send(()).is_err()
1290                                        {
1291                                            log::warn!(
1292                                                "Recovery channel closed, keepalive exiting",
1293                                            );
1294                                            break;
1295                                        }
1296                                    }
1297                                }
1298                            }
1299                        }
1300                        () = cancel.cancelled() => {
1301                            log::debug!("Listen key keepalive task cancelled");
1302                            break;
1303                        }
1304                    }
1305                }
1306            });
1307            *self.keepalive_task.lock().expect(MUTEX_POISONED) = Some(keepalive_task);
1308        }
1309
1310        // Start listen key recovery driver task
1311        {
1312            let recovery_ctx = RecoveryCtx {
1313                http_client: self.http_client.clone(),
1314                listen_key: self.listen_key.clone(),
1315                ws_client: self.ws_client.clone(),
1316                ws_task: self.ws_task.clone(),
1317                recovery_lock: self.recovery_lock.clone(),
1318                ws_build_params,
1319                dispatch_ctx,
1320                recovery_tx: recovery_tx.clone(),
1321            };
1322            let cancel = self.cancellation_token.clone();
1323
1324            let recovery_task = get_runtime().spawn(async move {
1325                run_recovery_driver(
1326                    recovery_ctx,
1327                    recovery_rx,
1328                    cancel,
1329                    dispatch_user_stream_message,
1330                )
1331                .await;
1332            });
1333            *self.recovery_task.lock().expect(MUTEX_POISONED) = Some(recovery_task);
1334        }
1335
1336        // Request initial account state
1337        let account_state = self
1338            .refresh_account_state()
1339            .await
1340            .context("failed to request Binance Futures account state")?;
1341
1342        if !account_state.balances.is_empty() {
1343            log::debug!(
1344                "Received account state with {} balance(s) and {} margin(s)",
1345                account_state.balances.len(),
1346                account_state.margins.len()
1347            );
1348        }
1349
1350        self.emitter.send_account_state(account_state);
1351
1352        crate::common::execution::await_account_registered(&self.core, self.core.account_id, 30.0)
1353            .await?;
1354
1355        // Connect WS trading client (primary order transport for USD-M)
1356        if let Some(ref mut ws_trading) = self.ws_trading_client {
1357            match ws_trading.connect().await {
1358                Ok(()) => {
1359                    log::debug!("Connected to Binance Futures WS trading API");
1360
1361                    let ws_trading_clone = ws_trading.clone();
1362                    let emitter = self.emitter.clone();
1363                    let account_id = self.core.account_id;
1364                    let clock = self.clock;
1365                    let dispatch_state = self.dispatch_state.clone();
1366
1367                    let handle = get_runtime().spawn(async move {
1368                        while let Some(msg) = ws_trading_clone.recv().await {
1369                            dispatch_ws_trading_message(
1370                                msg,
1371                                &emitter,
1372                                account_id,
1373                                clock,
1374                                &dispatch_state,
1375                            );
1376                        }
1377                    });
1378
1379                    *self.ws_trading_handle.lock().expect(MUTEX_POISONED) = Some(handle);
1380                }
1381                Err(e) => {
1382                    log::error!(
1383                        "Failed to connect WS trading API: {e}. \
1384                         Order operations will use HTTP fallback"
1385                    );
1386                }
1387            }
1388        }
1389
1390        self.core.set_connected();
1391        log::info!("Connected: client_id={}", self.core.client_id);
1392        Ok(())
1393    }
1394
1395    async fn disconnect(&mut self) -> anyhow::Result<()> {
1396        if self.core.is_disconnected() {
1397            return Ok(());
1398        }
1399
1400        // Drop the recovery tx so the driver exits its recv loop
1401        self.recovery_tx.lock().expect(MUTEX_POISONED).take();
1402
1403        // Cancel all background tasks
1404        self.cancellation_token.cancel();
1405
1406        // Abort WS trading task and disconnect
1407        if let Some(handle) = self.ws_trading_handle.lock().expect(MUTEX_POISONED).take() {
1408            handle.abort();
1409        }
1410
1411        if let Some(ref mut ws_trading) = self.ws_trading_client {
1412            ws_trading.disconnect().await;
1413        }
1414
1415        // Wait for WebSocket task to complete
1416        let ws_task = self.ws_task.lock().expect(MUTEX_POISONED).take();
1417        if let Some(task) = ws_task {
1418            let _ = task.await;
1419        }
1420
1421        // Abort the keepalive task. An in-flight keepalive_listen_key HTTP
1422        // call ignores the cancellation token until it returns, so awaiting
1423        // without aborting can stall disconnect for the full HTTP timeout.
1424        let keepalive_task = self.keepalive_task.lock().expect(MUTEX_POISONED).take();
1425        if let Some(task) = keepalive_task {
1426            task.abort();
1427            let _ = task.await;
1428        }
1429
1430        // Abort the recovery driver task. Waiting would block disconnect until
1431        // any in-flight HTTP or WebSocket call inside recover_user_data_stream
1432        // returns, which can be many seconds under a network outage.
1433        let recovery_task = self.recovery_task.lock().expect(MUTEX_POISONED).take();
1434        if let Some(task) = recovery_task {
1435            task.abort();
1436            let _ = task.await;
1437        }
1438
1439        // Close WebSocket
1440        if let Some(mut ws_client) = self.ws_client.lock().await.take() {
1441            let _ = ws_client.close().await;
1442        }
1443
1444        // Close listen key
1445        let listen_key = self.listen_key.read().expect(MUTEX_POISONED).clone();
1446        if let Some(ref key) = listen_key
1447            && let Err(e) = self.http_client.close_listen_key(key).await
1448        {
1449            log::warn!("Failed to close listen key: {e}");
1450        }
1451        *self.listen_key.write().expect(MUTEX_POISONED) = None;
1452
1453        self.abort_pending_tasks();
1454
1455        self.core.set_disconnected();
1456        log::info!("Disconnected: client_id={}", self.core.client_id);
1457        Ok(())
1458    }
1459
1460    async fn generate_order_status_report(
1461        &self,
1462        cmd: &GenerateOrderStatusReport,
1463    ) -> anyhow::Result<Option<OrderStatusReport>> {
1464        let Some(instrument_id) = cmd.instrument_id else {
1465            log::warn!("generate_order_status_report requires instrument_id: {cmd:?}");
1466            return Ok(None);
1467        };
1468
1469        let symbol = format_binance_symbol(&instrument_id);
1470        let order_id = cmd
1471            .venue_order_id
1472            .as_ref()
1473            .map(|id| {
1474                id.inner()
1475                    .parse::<i64>()
1476                    .context("failed to parse venue_order_id as numeric")
1477            })
1478            .transpose()?;
1479        let orig_client_order_id = cmd
1480            .client_order_id
1481            .map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
1482
1483        let mut builder = BinanceOrderQueryParamsBuilder::default();
1484        builder.symbol(symbol);
1485
1486        if let Some(oid) = order_id {
1487            builder.order_id(oid);
1488        }
1489
1490        if let Some(ref coid) = orig_client_order_id {
1491            builder.orig_client_order_id(coid.clone());
1492        }
1493        let params = builder.build().map_err(|e| anyhow::anyhow!("{e}"))?;
1494
1495        let (_, size_precision) = self.get_instrument_precision(instrument_id);
1496        let ts_init = self.clock.get_time_ns();
1497
1498        match self.http_client.query_order(&params).await {
1499            Ok(order) => {
1500                let report = order.to_order_status_report(
1501                    self.core.account_id,
1502                    instrument_id,
1503                    size_precision,
1504                    self.config.treat_expired_as_canceled,
1505                    ts_init,
1506                )?;
1507                Ok(Some(report))
1508            }
1509            Err(BinanceFuturesHttpError::BinanceError { code: -2013, .. }) => {
1510                // Order not found in regular API, try algo order API
1511                let Some(client_order_id) = cmd.client_order_id else {
1512                    return Ok(None);
1513                };
1514
1515                match self.http_client.query_algo_order(client_order_id).await {
1516                    Ok(algo_order) => {
1517                        let report = algo_order.to_order_status_report(
1518                            self.core.account_id,
1519                            instrument_id,
1520                            size_precision,
1521                            ts_init,
1522                        )?;
1523                        Ok(Some(report))
1524                    }
1525                    Err(e) => {
1526                        log::debug!("Algo order query also failed: {e}");
1527                        Ok(None)
1528                    }
1529                }
1530            }
1531            Err(e) => Err(e.into()),
1532        }
1533    }
1534
1535    async fn generate_order_status_reports(
1536        &self,
1537        cmd: &GenerateOrderStatusReports,
1538    ) -> anyhow::Result<Vec<OrderStatusReport>> {
1539        let ts_init = self.clock.get_time_ns();
1540        let mut reports = Vec::new();
1541
1542        if cmd.open_only {
1543            let symbol = cmd.instrument_id.map(|id| format_binance_symbol(&id));
1544            let mut builder = BinanceOpenOrdersParamsBuilder::default();
1545
1546            if let Some(s) = symbol {
1547                builder.symbol(s);
1548            }
1549            let params = builder.build().map_err(|e| anyhow::anyhow!("{e}"))?;
1550
1551            let (orders, algo_orders) = tokio::try_join!(
1552                self.http_client.query_open_orders(&params),
1553                self.http_client.query_open_algo_orders(cmd.instrument_id),
1554            )?;
1555
1556            for order in orders {
1557                if let Some(instrument_id) = cmd.instrument_id {
1558                    let (_, size_precision) = self.get_instrument_precision(instrument_id);
1559
1560                    if let Ok(report) = order.to_order_status_report(
1561                        self.core.account_id,
1562                        instrument_id,
1563                        size_precision,
1564                        self.config.treat_expired_as_canceled,
1565                        ts_init,
1566                    ) {
1567                        reports.push(report);
1568                    }
1569                } else {
1570                    let cache = self.core.cache();
1571                    if let Some(instrument) = cache
1572                        .instruments(&BINANCE_VENUE, None)
1573                        .into_iter()
1574                        .find(|i| i.raw_symbol().as_str() == order.symbol.as_str())
1575                        && let Ok(report) = order.to_order_status_report(
1576                            self.core.account_id,
1577                            instrument.id(),
1578                            instrument.size_precision(),
1579                            self.config.treat_expired_as_canceled,
1580                            ts_init,
1581                        )
1582                    {
1583                        reports.push(report);
1584                    }
1585                }
1586            }
1587
1588            for algo_order in algo_orders {
1589                if let Some(instrument_id) = cmd.instrument_id {
1590                    let (_, size_precision) = self.get_instrument_precision(instrument_id);
1591
1592                    if let Ok(report) = algo_order.to_order_status_report(
1593                        self.core.account_id,
1594                        instrument_id,
1595                        size_precision,
1596                        ts_init,
1597                    ) {
1598                        reports.push(report);
1599                    }
1600                } else {
1601                    let cache = self.core.cache();
1602                    if let Some(instrument) = cache
1603                        .instruments(&BINANCE_VENUE, None)
1604                        .into_iter()
1605                        .find(|i| i.raw_symbol().as_str() == algo_order.symbol.as_str())
1606                        && let Ok(report) = algo_order.to_order_status_report(
1607                            self.core.account_id,
1608                            instrument.id(),
1609                            instrument.size_precision(),
1610                            ts_init,
1611                        )
1612                    {
1613                        reports.push(report);
1614                    }
1615                }
1616            }
1617        } else if let Some(instrument_id) = cmd.instrument_id {
1618            let symbol = format_binance_symbol(&instrument_id);
1619            let start_time = cmd
1620                .start
1621                .map(|t| t.as_i64() / NANOSECONDS_IN_MILLISECOND as i64);
1622            let end_time = cmd
1623                .end
1624                .map(|t| t.as_i64() / NANOSECONDS_IN_MILLISECOND as i64);
1625
1626            let mut builder = BinanceAllOrdersParamsBuilder::default();
1627            builder.symbol(symbol);
1628
1629            if let Some(st) = start_time {
1630                builder.start_time(st);
1631            }
1632
1633            if let Some(et) = end_time {
1634                builder.end_time(et);
1635            }
1636            let params = builder.build().map_err(|e| anyhow::anyhow!("{e}"))?;
1637
1638            let orders = self.http_client.query_all_orders(&params).await?;
1639            let (_, size_precision) = self.get_instrument_precision(instrument_id);
1640
1641            for order in orders {
1642                if let Ok(report) = order.to_order_status_report(
1643                    self.core.account_id,
1644                    instrument_id,
1645                    size_precision,
1646                    self.config.treat_expired_as_canceled,
1647                    ts_init,
1648                ) {
1649                    reports.push(report);
1650                }
1651            }
1652        }
1653
1654        Ok(reports)
1655    }
1656
1657    async fn generate_fill_reports(
1658        &self,
1659        cmd: GenerateFillReports,
1660    ) -> anyhow::Result<Vec<FillReport>> {
1661        let Some(instrument_id) = cmd.instrument_id else {
1662            log::warn!("generate_fill_reports requires instrument_id for Binance Futures");
1663            return Ok(Vec::new());
1664        };
1665
1666        let symbol = format_binance_symbol(&instrument_id);
1667        let start_time = cmd
1668            .start
1669            .map(|t| t.as_i64() / NANOSECONDS_IN_MILLISECOND as i64);
1670        let end_time = cmd
1671            .end
1672            .map(|t| t.as_i64() / NANOSECONDS_IN_MILLISECOND as i64);
1673
1674        let mut builder = BinanceUserTradesParamsBuilder::default();
1675        builder.symbol(symbol);
1676
1677        if let Some(st) = start_time {
1678            builder.start_time(st);
1679        }
1680
1681        if let Some(et) = end_time {
1682            builder.end_time(et);
1683        }
1684        let params = builder.build().map_err(|e| anyhow::anyhow!("{e}"))?;
1685
1686        let trades = self.http_client.query_user_trades(&params).await?;
1687        let (price_precision, size_precision) = self.get_instrument_precision(instrument_id);
1688        let ts_init = self.clock.get_time_ns();
1689
1690        let mut reports = Vec::new();
1691
1692        for trade in trades {
1693            if let Ok(report) = trade.to_fill_report(
1694                self.core.account_id,
1695                instrument_id,
1696                price_precision,
1697                size_precision,
1698                self.config.bnfcr_currency,
1699                ts_init,
1700            ) {
1701                reports.push(report);
1702            }
1703        }
1704
1705        Ok(reports)
1706    }
1707
1708    async fn generate_position_status_reports(
1709        &self,
1710        cmd: &GeneratePositionStatusReports,
1711    ) -> anyhow::Result<Vec<PositionStatusReport>> {
1712        let symbol = cmd.instrument_id.map(|id| format_binance_symbol(&id));
1713
1714        let mut builder = BinancePositionRiskParamsBuilder::default();
1715
1716        if let Some(s) = symbol {
1717            builder.symbol(s);
1718        }
1719        let params = builder.build().map_err(|e| anyhow::anyhow!("{e}"))?;
1720
1721        let positions = self.http_client.query_positions(&params).await?;
1722
1723        let mut reports = Vec::new();
1724
1725        for position in positions {
1726            let position_amt = match position.position_amt.parse::<Decimal>() {
1727                Ok(value) => value,
1728                Err(e) => {
1729                    log::warn!(
1730                        "Failed to parse Futures position_amt for symbol={}: {e}",
1731                        position.symbol
1732                    );
1733                    continue;
1734                }
1735            };
1736
1737            if position_amt.is_zero() {
1738                continue;
1739            }
1740
1741            let cache = self.core.cache();
1742            if let Some(instrument) = cache
1743                .instruments(&BINANCE_VENUE, None)
1744                .into_iter()
1745                .find(|i| i.raw_symbol().as_str() == position.symbol.as_str())
1746            {
1747                match self.create_position_report(
1748                    &position,
1749                    instrument.id(),
1750                    instrument.size_precision(),
1751                ) {
1752                    Ok(report) => reports.push(report),
1753                    Err(e) => {
1754                        log::warn!(
1755                            "Failed to create Futures position report for symbol={}: {e}",
1756                            position.symbol
1757                        );
1758                    }
1759                }
1760            }
1761        }
1762
1763        Ok(reports)
1764    }
1765
1766    async fn generate_mass_status(
1767        &self,
1768        lookback_mins: Option<u64>,
1769    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1770        log::info!("Generating ExecutionMassStatus (lookback_mins={lookback_mins:?})");
1771
1772        let ts_now = self.clock.get_time_ns();
1773
1774        let start = lookback_mins.map(|mins| {
1775            let lookback_ns = mins_to_nanos(mins);
1776            UnixNanos::from(ts_now.as_u64().saturating_sub(lookback_ns))
1777        });
1778
1779        let order_cmd = GenerateOrderStatusReportsBuilder::default()
1780            .ts_init(ts_now)
1781            .open_only(true)
1782            .start(start)
1783            .build()
1784            .map_err(|e| anyhow::anyhow!("{e}"))?;
1785
1786        let position_cmd = GeneratePositionStatusReportsBuilder::default()
1787            .ts_init(ts_now)
1788            .start(start)
1789            .build()
1790            .map_err(|e| anyhow::anyhow!("{e}"))?;
1791
1792        let (order_reports, position_reports) = tokio::try_join!(
1793            self.generate_order_status_reports(&order_cmd),
1794            self.generate_position_status_reports(&position_cmd),
1795        )?;
1796
1797        log::info!("Received {} OrderStatusReports", order_reports.len());
1798        log::info!("Received {} PositionReports", position_reports.len());
1799
1800        let mut mass_status = ExecutionMassStatus::new(
1801            self.core.client_id,
1802            self.core.account_id,
1803            *BINANCE_VENUE,
1804            ts_now,
1805            None,
1806        );
1807
1808        mass_status.add_order_reports(order_reports);
1809        mass_status.add_position_reports(position_reports);
1810
1811        Ok(Some(mass_status))
1812    }
1813
1814    fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
1815        self.update_account_state();
1816        Ok(())
1817    }
1818
1819    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
1820        log::debug!("query_order: client_order_id={}", cmd.client_order_id);
1821
1822        let http_client = self.http_client.clone();
1823        let command = cmd;
1824        let emitter = self.emitter.clone();
1825        let account_id = self.core.account_id;
1826        let clock = self.clock;
1827
1828        let symbol = format_binance_symbol(&command.instrument_id);
1829        let order_id = command
1830            .venue_order_id
1831            .map(|id| {
1832                id.inner()
1833                    .parse::<i64>()
1834                    .map_err(|e| anyhow::anyhow!("failed to parse venue_order_id: {e}"))
1835            })
1836            .transpose()?;
1837        let orig_client_order_id = Some(encode_broker_id(
1838            &command.client_order_id,
1839            BINANCE_NAUTILUS_FUTURES_BROKER_ID,
1840        ));
1841        let (_, size_precision) = self.get_instrument_precision(command.instrument_id);
1842        let treat_expired_as_canceled = self.config.treat_expired_as_canceled;
1843
1844        self.spawn_task("query_order", async move {
1845            let mut builder = BinanceOrderQueryParamsBuilder::default();
1846            builder.symbol(symbol.clone());
1847
1848            if let Some(oid) = order_id {
1849                builder.order_id(oid);
1850            }
1851
1852            if let Some(coid) = orig_client_order_id {
1853                builder.orig_client_order_id(coid);
1854            }
1855            let params = builder
1856                .build()
1857                .map_err(|e| anyhow::anyhow!("failed to build order query params: {e}"))?;
1858
1859            let result = http_client.query_order(&params).await;
1860
1861            match result {
1862                Ok(order) => {
1863                    let ts_init = clock.get_time_ns();
1864                    let report = order.to_order_status_report(
1865                        account_id,
1866                        command.instrument_id,
1867                        size_precision,
1868                        treat_expired_as_canceled,
1869                        ts_init,
1870                    )?;
1871
1872                    emitter.send_order_status_report(report);
1873                }
1874                Err(e) => log::warn!("Failed to query order status: {e}"),
1875            }
1876
1877            Ok(())
1878        });
1879
1880        Ok(())
1881    }
1882
1883    fn generate_account_state(
1884        &self,
1885        balances: Vec<AccountBalance>,
1886        margins: Vec<MarginBalance>,
1887        reported: bool,
1888        ts_event: UnixNanos,
1889    ) -> anyhow::Result<()> {
1890        self.emitter
1891            .emit_account_state(balances, margins, reported, ts_event);
1892        Ok(())
1893    }
1894
1895    fn start(&mut self) -> anyhow::Result<()> {
1896        if self.core.is_started() {
1897            return Ok(());
1898        }
1899
1900        self.emitter.set_sender(get_exec_event_sender());
1901        self.core.set_started();
1902
1903        let http_client = self.http_client.clone();
1904
1905        get_runtime().spawn(async move {
1906            match http_client.request_instruments().await {
1907                Ok(instruments) => {
1908                    if instruments.is_empty() {
1909                        log::warn!("No instruments returned for Binance Futures");
1910                    } else {
1911                        log::debug!("Loaded {} Futures instruments", instruments.len());
1912                    }
1913                }
1914                Err(e) => {
1915                    log::error!("Failed to request Binance Futures instruments: {e}");
1916                }
1917            }
1918        });
1919
1920        log::info!(
1921            "Started: client_id={}, account_id={}, account_type={:?}, environment={:?}",
1922            self.core.client_id,
1923            self.core.account_id,
1924            self.core.account_type,
1925            self.config.environment,
1926        );
1927        Ok(())
1928    }
1929
1930    fn stop(&mut self) -> anyhow::Result<()> {
1931        if self.core.is_stopped() {
1932            return Ok(());
1933        }
1934
1935        self.cancellation_token.cancel();
1936
1937        if let Some(handle) = self.ws_trading_handle.lock().expect(MUTEX_POISONED).take() {
1938            handle.abort();
1939        }
1940
1941        if let Some(handle) = self.ws_task.lock().expect(MUTEX_POISONED).take() {
1942            handle.abort();
1943        }
1944
1945        if let Some(handle) = self.keepalive_task.lock().expect(MUTEX_POISONED).take() {
1946            handle.abort();
1947        }
1948
1949        self.recovery_tx.lock().expect(MUTEX_POISONED).take();
1950        if let Some(handle) = self.recovery_task.lock().expect(MUTEX_POISONED).take() {
1951            handle.abort();
1952        }
1953
1954        self.abort_pending_tasks();
1955        self.core.set_stopped();
1956        self.core.set_disconnected();
1957        log::info!("Stopped: client_id={}", self.core.client_id);
1958        Ok(())
1959    }
1960
1961    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
1962        let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
1963
1964        if order.is_closed() {
1965            let client_order_id = order.client_order_id();
1966            log::warn!("Cannot submit closed order {client_order_id}");
1967            return Ok(());
1968        }
1969
1970        // Validate before submission (Initialized -> Denied is valid,
1971        // but Submitted -> Denied is not, so validate before emitting OrderSubmitted)
1972        if let Some(offset_type) = order.trailing_offset_type() {
1973            if offset_type != TrailingOffsetType::BasisPoints {
1974                anyhow::bail!(
1975                    "Binance only supports TrailingOffsetType::BasisPoints, received {offset_type:?}"
1976                );
1977            }
1978
1979            if let Some(offset) = order.trailing_offset() {
1980                trailing_offset_to_callback_rate(offset)?;
1981            }
1982        }
1983
1984        let close_position = cmd
1985            .params
1986            .as_ref()
1987            .and_then(|p| p.get_bool("close_position"))
1988            .unwrap_or(false);
1989
1990        if close_position {
1991            let order_type = order.order_type();
1992
1993            if !matches!(
1994                order_type,
1995                OrderType::StopMarket | OrderType::MarketIfTouched
1996            ) {
1997                anyhow::bail!(
1998                    "`close_position` is not supported for order type {order_type:?} on Binance"
1999                );
2000            }
2001
2002            if order.is_reduce_only() {
2003                anyhow::bail!("`close_position` cannot be combined with `reduce_only` on Binance");
2004            }
2005        }
2006
2007        if let Some(pm_str) = cmd.params.as_ref().and_then(|p| p.get_str("price_match")) {
2008            BinancePriceMatch::from_param(pm_str)?;
2009            let order_type = order.order_type();
2010            anyhow::ensure!(
2011                !order.is_post_only(),
2012                "price_match cannot be combined with post-only orders"
2013            );
2014            anyhow::ensure!(
2015                order_type == OrderType::Limit,
2016                "price_match is not supported for order type {order_type:?}"
2017            );
2018        }
2019
2020        log::debug!("OrderSubmitted client_order_id={}", order.client_order_id());
2021        self.emitter.emit_order_submitted(&order);
2022
2023        self.submit_order_internal(&cmd)
2024    }
2025
2026    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
2027        if cmd.order_list.client_order_ids.is_empty() {
2028            log::debug!("submit_order_list called with empty order list");
2029            return Ok(());
2030        }
2031
2032        let orders = self.core.get_orders_for_list(&cmd.order_list)?;
2033
2034        if let Some(order) = orders.iter().find(|order| order.is_closed()) {
2035            let reason = format!("Cannot submit closed order {}", order.client_order_id());
2036            for order in &orders {
2037                self.emitter.emit_order_denied(order, &reason);
2038            }
2039            return Ok(());
2040        }
2041
2042        let close_position = cmd
2043            .params
2044            .as_ref()
2045            .and_then(|p| p.get_bool("close_position"))
2046            .unwrap_or(false);
2047        let price_match = match cmd
2048            .params
2049            .as_ref()
2050            .and_then(|p| p.get_str("price_match"))
2051            .map(BinancePriceMatch::from_param)
2052            .transpose()
2053        {
2054            Ok(price_match) => price_match,
2055            Err(e) => {
2056                for order in &orders {
2057                    self.emitter.emit_order_denied(order, &e.to_string());
2058                }
2059                return Ok(());
2060            }
2061        };
2062
2063        let batch_items = match build_futures_order_list_batch(
2064            &orders,
2065            self.is_hedge_mode(),
2066            close_position,
2067            price_match,
2068        ) {
2069            Ok(batch_items) => batch_items,
2070            Err(reason) => {
2071                for order in &orders {
2072                    self.emitter.emit_order_denied(order, &reason);
2073                }
2074                return Ok(());
2075            }
2076        };
2077
2078        for order in &orders {
2079            self.dispatch_state.order_identities.insert(
2080                order.client_order_id(),
2081                OrderIdentity {
2082                    instrument_id: order.instrument_id(),
2083                    strategy_id: order.strategy_id(),
2084                    order_side: order.order_side(),
2085                    order_type: order.order_type(),
2086                    price: order.price(),
2087                    quantity: order.quantity(),
2088                },
2089            );
2090            self.emitter.emit_order_submitted(order);
2091        }
2092
2093        let http_client = self.http_client.clone();
2094        let emitter = self.emitter.clone();
2095        let trader_id = self.core.trader_id;
2096        let account_id = self.core.account_id;
2097        let clock = self.clock;
2098
2099        self.spawn_task("submit_order_list", async move {
2100            match http_client.submit_order_list(&batch_items).await {
2101                Ok(results) => {
2102                    for (order, result) in orders.iter().zip(results.iter()) {
2103                        match result {
2104                            BatchOrderResult::Success(response) => {
2105                                log::debug!(
2106                                    "Order-list leg submit accepted: client_order_id={}, venue_order_id={}",
2107                                    order.client_order_id(),
2108                                    response.order_id
2109                                );
2110                            }
2111                            BatchOrderResult::Error(error) => {
2112                                let ts_now = clock.get_time_ns();
2113                                let rejected = OrderRejected::new(
2114                                    trader_id,
2115                                    order.strategy_id(),
2116                                    order.instrument_id(),
2117                                    order.client_order_id(),
2118                                    account_id,
2119                                    format!(
2120                                        "submit-order-list-error: code={}, msg={}",
2121                                        error.code, error.msg
2122                                    )
2123                                    .into(),
2124                                    UUID4::new(),
2125                                    ts_now,
2126                                    ts_now,
2127                                    false,
2128                                    false,
2129                                );
2130                                emitter.send_order_event(OrderEventAny::Rejected(rejected));
2131                            }
2132                        }
2133                    }
2134                }
2135                Err(e) => {
2136                    let e = anyhow::Error::new(e);
2137
2138                    if is_ambiguous_submit_error(&e) {
2139                        log::warn!(
2140                            "Ambiguous order-list submit failure, awaiting reconciliation: {e}"
2141                        );
2142                    } else if is_structured_venue_rejection(&e) {
2143                        let ts_now = clock.get_time_ns();
2144                        let due_post_only = classify_submit_order_error(&e);
2145
2146                        for order in &orders {
2147                            let rejected = OrderRejected::new(
2148                                trader_id,
2149                                order.strategy_id(),
2150                                order.instrument_id(),
2151                                order.client_order_id(),
2152                                account_id,
2153                                format!("submit-order-list-error: {e}").into(),
2154                                UUID4::new(),
2155                                ts_now,
2156                                ts_now,
2157                                false,
2158                                due_post_only,
2159                            );
2160                            emitter.send_order_event(OrderEventAny::Rejected(rejected));
2161                        }
2162                    } else if is_local_command_failure(&e) {
2163                        log::warn!(
2164                            "Order-list submit command failed local validation for {} orders: {e}",
2165                            orders.len()
2166                        );
2167                    } else {
2168                        log::warn!(
2169                            "Ambiguous order-list submit failure, awaiting reconciliation: {e}"
2170                        );
2171                    }
2172
2173                    return Err(e);
2174                }
2175            }
2176            Ok(())
2177        });
2178
2179        Ok(())
2180    }
2181
2182    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
2183        let order = {
2184            let cache = self.core.cache();
2185            cache.order(&cmd.client_order_id).map(|o| o.clone())
2186        };
2187
2188        let Some(order) = order else {
2189            log::warn!(
2190                "Cannot modify order {}: not found in cache",
2191                cmd.client_order_id
2192            );
2193            let ts_init = self.clock.get_time_ns();
2194
2195            let rejected = OrderModifyRejected::new(
2196                self.core.trader_id,
2197                cmd.strategy_id,
2198                cmd.instrument_id,
2199                cmd.client_order_id,
2200                "Order not found in cache for modify".into(),
2201                UUID4::new(),
2202                ts_init, // no venue timestamp, rejected locally
2203                ts_init,
2204                false,
2205                cmd.venue_order_id,
2206                Some(self.core.account_id),
2207            );
2208
2209            self.emitter
2210                .send_order_event(OrderEventAny::ModifyRejected(rejected));
2211            return Ok(());
2212        };
2213
2214        let http_client = self.http_client.clone();
2215        let emitter = self.emitter.clone();
2216        let trader_id = self.core.trader_id;
2217        let account_id = self.core.account_id;
2218        let instrument_id = cmd.instrument_id;
2219        let venue_order_id = cmd.venue_order_id;
2220        let client_order_id = Some(cmd.client_order_id);
2221        let order_side = order.order_side();
2222        let quantity = cmd.quantity.unwrap_or_else(|| order.quantity());
2223        let price = cmd.price.or_else(|| order.price());
2224
2225        let Some(price) = price else {
2226            log::warn!(
2227                "Cannot modify order {}: price required",
2228                cmd.client_order_id
2229            );
2230            let ts_init = self.clock.get_time_ns();
2231
2232            let rejected = OrderModifyRejected::new(
2233                self.core.trader_id,
2234                cmd.strategy_id,
2235                cmd.instrument_id,
2236                cmd.client_order_id,
2237                "Price required for order modification".into(),
2238                UUID4::new(),
2239                ts_init, // no venue timestamp, rejected locally
2240                ts_init,
2241                false,
2242                cmd.venue_order_id,
2243                Some(self.core.account_id),
2244            );
2245
2246            self.emitter
2247                .send_order_event(OrderEventAny::ModifyRejected(rejected));
2248            return Ok(());
2249        };
2250        let command = cmd;
2251        let clock = self.clock;
2252
2253        if self.ws_trading_active() {
2254            let ws_client = self.ws_trading_client.as_ref().unwrap().clone();
2255            let dispatch_state = self.dispatch_state.clone();
2256
2257            let binance_side = BinanceSide::try_from(order_side)?;
2258            let orig_client_order_id =
2259                client_order_id.map(|id| encode_broker_id(&id, BINANCE_NAUTILUS_FUTURES_BROKER_ID));
2260
2261            let mut modify_builder = BinanceModifyOrderParamsBuilder::default();
2262            modify_builder
2263                .symbol(format_binance_symbol(&instrument_id))
2264                .side(binance_side)
2265                .quantity(quantity.to_string())
2266                .price(price.to_string());
2267
2268            if let Some(venue_id) = venue_order_id {
2269                let order_id: i64 = venue_id
2270                    .inner()
2271                    .parse()
2272                    .context("failed to parse venue_order_id as numeric")?;
2273                modify_builder.order_id(order_id);
2274            }
2275
2276            if let Some(client_id) = orig_client_order_id {
2277                modify_builder.orig_client_order_id(client_id);
2278            }
2279
2280            let params = modify_builder
2281                .build()
2282                .context("failed to build modify params")?;
2283
2284            // Pre-register before sending to avoid response racing the insert
2285            let request_id = ws_client.next_request_id();
2286            dispatch_state.pending_requests.insert(
2287                request_id.clone(),
2288                PendingRequest {
2289                    client_order_id: command.client_order_id,
2290                    venue_order_id,
2291                    operation: PendingOperation::Modify,
2292                },
2293            );
2294
2295            self.spawn_task("modify_order_ws", async move {
2296                if let Err(e) = ws_client
2297                    .modify_order_with_id(request_id.clone(), params)
2298                    .await
2299                {
2300                    dispatch_state.pending_requests.remove(&request_id);
2301                    log::error!(
2302                        "WS modify request failed for {}: {e}",
2303                        command.client_order_id
2304                    );
2305                    anyhow::bail!("WS modify order failed: {e}");
2306                }
2307                Ok(())
2308            });
2309
2310            return Ok(());
2311        }
2312
2313        self.spawn_task("modify_order", async move {
2314            let result = http_client
2315                .modify_order(
2316                    account_id,
2317                    instrument_id,
2318                    venue_order_id,
2319                    client_order_id,
2320                    order_side,
2321                    quantity,
2322                    price,
2323                )
2324                .await;
2325
2326            match result {
2327                Ok(report) => {
2328                    let ts_now = clock.get_time_ns();
2329                    let updated_event = OrderUpdated::new(
2330                        trader_id,
2331                        command.strategy_id,
2332                        command.instrument_id,
2333                        command.client_order_id,
2334                        quantity,
2335                        UUID4::new(),
2336                        ts_now,
2337                        ts_now,
2338                        false,
2339                        Some(report.venue_order_id),
2340                        Some(account_id),
2341                        Some(price),
2342                        None,
2343                        None,
2344                        false, // is_quote_quantity
2345                    );
2346
2347                    emitter.send_order_event(OrderEventAny::Updated(updated_event));
2348                }
2349                Err(e) => {
2350                    if is_structured_venue_rejection(&e) {
2351                        let ts_now = clock.get_time_ns();
2352
2353                        let rejected = OrderModifyRejected::new(
2354                            trader_id,
2355                            command.strategy_id,
2356                            command.instrument_id,
2357                            command.client_order_id,
2358                            format!("modify-order-failed: {e}").into(),
2359                            UUID4::new(),
2360                            ts_now,
2361                            ts_now,
2362                            false,
2363                            command.venue_order_id,
2364                            Some(account_id),
2365                        );
2366
2367                        emitter.send_order_event(OrderEventAny::ModifyRejected(rejected));
2368                    } else {
2369                        log::warn!(
2370                            "Ambiguous modify failure for {}, awaiting reconciliation: {e}",
2371                            command.client_order_id
2372                        );
2373                    }
2374
2375                    anyhow::bail!("Modify order failed: {e}");
2376                }
2377            }
2378
2379            Ok(())
2380        });
2381
2382        Ok(())
2383    }
2384
2385    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
2386        self.cancel_order_internal(&cmd);
2387        Ok(())
2388    }
2389
2390    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
2391        let http_client = self.http_client.clone();
2392        let instrument_id = cmd.instrument_id;
2393
2394        // USD-M Futures WS Trading API does not expose an openOrders.cancelAll
2395        // method, so regular and algo cancel-all both go through HTTP.
2396        self.spawn_task("cancel_all_orders", async move {
2397            match http_client.cancel_all_orders(instrument_id).await {
2398                Ok(_) => {
2399                    log::debug!("Cancel all regular orders request accepted for {instrument_id}");
2400                }
2401                Err(e) => {
2402                    log::error!("Failed to cancel all regular orders for {instrument_id}: {e}");
2403                }
2404            }
2405
2406            match http_client.cancel_all_algo_orders(instrument_id).await {
2407                Ok(()) => {
2408                    log::debug!("Cancel all algo orders request accepted for {instrument_id}");
2409                }
2410                Err(e) => {
2411                    log::error!("Failed to cancel all algo orders for {instrument_id}: {e}");
2412                }
2413            }
2414
2415            Ok(())
2416        });
2417
2418        Ok(())
2419    }
2420
2421    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
2422        const BATCH_SIZE: usize = 10;
2423
2424        if cmd.cancels.is_empty() {
2425            return Ok(());
2426        }
2427
2428        let http_client = self.http_client.clone();
2429        let command = cmd;
2430
2431        let emitter = self.emitter.clone();
2432        let trader_id = self.core.trader_id;
2433        let account_id = self.core.account_id;
2434        let clock = self.clock;
2435
2436        self.spawn_task("batch_cancel_orders", async move {
2437            let symbol = format_binance_symbol(&command.instrument_id);
2438
2439            for chunk in command.cancels.chunks(BATCH_SIZE) {
2440                let mut order_id_batch = Vec::new();
2441                let mut client_order_id_batch = Vec::new();
2442
2443                for cancel in chunk {
2444                    if let Some(venue_order_id) = cancel.venue_order_id {
2445                        let order_id = venue_order_id.inner().parse::<i64>().unwrap_or(0);
2446                        if order_id != 0 {
2447                            order_id_batch.push((
2448                                BatchCancelItem::by_order_id(symbol.clone(), order_id),
2449                                cancel.clone(),
2450                            ));
2451                            continue;
2452                        }
2453                    }
2454
2455                    client_order_id_batch.push((
2456                        BatchCancelItem::by_client_order_id(
2457                            symbol.clone(),
2458                            encode_broker_id(
2459                                &cancel.client_order_id,
2460                                BINANCE_NAUTILUS_FUTURES_BROKER_ID,
2461                            ),
2462                        ),
2463                        cancel.clone(),
2464                    ));
2465                }
2466
2467                for batch in [order_id_batch, client_order_id_batch] {
2468                    if batch.is_empty() {
2469                        continue;
2470                    }
2471
2472                    let batch_len = batch.len();
2473                    let (batch_items, batch_cancels): (Vec<_>, Vec<_>) =
2474                        batch.into_iter().unzip();
2475
2476                    match http_client.batch_cancel_orders(&batch_items).await {
2477                        Ok(results) => {
2478                            for (cancel, result) in batch_cancels.iter().zip(results.iter()) {
2479                                match result {
2480                                    BatchOrderResult::Success(response) => {
2481                                        let venue_order_id =
2482                                            VenueOrderId::new(response.order_id.to_string());
2483                                        let canceled_event = OrderCanceled::new(
2484                                            trader_id,
2485                                            cancel.strategy_id,
2486                                            cancel.instrument_id,
2487                                            cancel.client_order_id,
2488                                            UUID4::new(),
2489                                            cancel.ts_init,
2490                                            clock.get_time_ns(),
2491                                            false,
2492                                            Some(venue_order_id),
2493                                            Some(account_id),
2494                                        );
2495
2496                                        emitter.send_order_event(OrderEventAny::Canceled(
2497                                            canceled_event,
2498                                        ));
2499                                    }
2500                                    BatchOrderResult::Error(error) => {
2501                                        let rejected = OrderCancelRejected::new(
2502                                            trader_id,
2503                                            cancel.strategy_id,
2504                                            cancel.instrument_id,
2505                                            cancel.client_order_id,
2506                                            format!(
2507                                                "batch-cancel-error: code={}, msg={}",
2508                                                error.code, error.msg
2509                                            )
2510                                            .into(),
2511                                            UUID4::new(),
2512                                            clock.get_time_ns(),
2513                                            cancel.ts_init,
2514                                            false,
2515                                            cancel.venue_order_id,
2516                                            Some(account_id),
2517                                        );
2518
2519                                        emitter.send_order_event(OrderEventAny::CancelRejected(
2520                                            rejected,
2521                                        ));
2522                                    }
2523                                }
2524                            }
2525                        }
2526                        Err(e) => {
2527                            if is_local_http_command_failure(&e) {
2528                                log::warn!(
2529                                    "Batch cancel command failed local validation for {batch_len} orders: {e}",
2530                                );
2531                            } else {
2532                                log::warn!(
2533                                    "Ambiguous batch cancel request failure for {batch_len} orders, awaiting reconciliation: {e}",
2534                                );
2535                            }
2536                            return Err(e.into());
2537                        }
2538                    }
2539                }
2540            }
2541
2542            Ok(())
2543        });
2544
2545        Ok(())
2546    }
2547}
2548
2549#[cfg(test)]
2550mod tests {
2551    use rstest::rstest;
2552
2553    use super::*;
2554
2555    fn http_error(code: i64) -> anyhow::Error {
2556        anyhow::Error::new(BinanceFuturesHttpError::BinanceError {
2557            code,
2558            message: format!("test error {code}"),
2559        })
2560    }
2561
2562    #[rstest]
2563    fn test_classify_submit_order_error_gtx_is_post_only() {
2564        let err = http_error(BINANCE_GTX_ORDER_REJECT_CODE);
2565        assert!(classify_submit_order_error(&err));
2566    }
2567
2568    #[rstest]
2569    fn test_classify_submit_order_error_dual_side_sync_is_not_post_only() {
2570        // -4531 is a hedge-mode/account-setup issue, not a post-only rejection.
2571        // Make sure the new classifier branch does not mark it as post-only.
2572        let err = http_error(BINANCE_FUTURES_DUAL_SIDE_SYNC_REJECT_CODE);
2573        assert!(!classify_submit_order_error(&err));
2574    }
2575
2576    #[rstest]
2577    fn test_classify_submit_order_error_other_venue_code_is_not_post_only() {
2578        let err = http_error(-2010);
2579        assert!(!classify_submit_order_error(&err));
2580    }
2581
2582    #[rstest]
2583    fn test_classify_submit_order_error_non_binance_error_is_not_post_only() {
2584        let err = anyhow::anyhow!("network failure");
2585        assert!(!classify_submit_order_error(&err));
2586    }
2587
2588    #[rstest]
2589    #[case(BINANCE_UNEXPECTED_RESPONSE_CODE)]
2590    #[case(BINANCE_STATUS_UNKNOWN_CODE)]
2591    fn test_unknown_status_submit_error_is_ambiguous(#[case] code: i64) {
2592        let err = http_error(code);
2593        assert!(is_ambiguous_submit_error(&err));
2594        assert!(is_structured_venue_rejection(&err));
2595    }
2596
2597    #[rstest]
2598    fn test_other_structured_submit_error_is_not_ambiguous() {
2599        let err = http_error(BINANCE_GTX_ORDER_REJECT_CODE);
2600        assert!(!is_ambiguous_submit_error(&err));
2601        assert!(is_structured_venue_rejection(&err));
2602    }
2603}