1use 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
124const LISTEN_KEY_KEEPALIVE_SECS: u64 = 30 * 60;
126
127const MAX_KEEPALIVE_FAILURES: u32 = 1;
129
130#[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 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, None, None, 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, 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 #[must_use]
263 pub fn is_hedge_mode(&self) -> bool {
264 self.is_hedge_mode.load(Ordering::Acquire)
265 }
266
267 #[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 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 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, UUID4::new(),
337 ts_now,
338 ts_now,
339 None, )
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 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 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 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 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 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 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 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 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 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 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 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, 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 match self.http_client.set_leverage(¶ms).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(¶ms).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
1056pub(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 self.cancellation_token = CancellationToken::new();
1142
1143 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 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 self.apply_futures_config()
1173 .await
1174 .context("failed to apply futures config")?;
1175
1176 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 {
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 {
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 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 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 self.recovery_tx.lock().expect(MUTEX_POISONED).take();
1402
1403 self.cancellation_token.cancel();
1405
1406 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 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 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 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 if let Some(mut ws_client) = self.ws_client.lock().await.take() {
1441 let _ = ws_client.close().await;
1442 }
1443
1444 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(¶ms).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 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(¶ms),
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(¶ms).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(¶ms).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(¶ms).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(¶ms).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 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, 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, 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 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, );
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 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 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}