1use std::{
26 fmt::Debug,
27 num::NonZeroU32,
28 sync::{
29 Arc, LazyLock,
30 atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
31 },
32 time::{Duration, SystemTime},
33};
34
35use ahash::{AHashMap, AHashSet};
36use arc_swap::ArcSwap;
37use dashmap::DashMap;
38use futures_util::Stream;
39use nautilus_core::{
40 AtomicMap,
41 consts::NAUTILUS_USER_AGENT,
42 env::{get_env_var, get_or_env_var},
43 string::secret::REDACTED,
44};
45use nautilus_live::{
46 SocketControl,
47 task::{TaskGroup, TaskShutdownError},
48};
49use nautilus_model::{
50 data::BarType,
51 enums::{OrderSide, OrderType, PositionSide, TimeInForce, TriggerType},
52 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
53 instruments::{Instrument, InstrumentAny},
54 types::{Price, Quantity},
55};
56use nautilus_network::{
57 http::USER_AGENT,
58 mode::ConnectionMode,
59 ratelimiter::quota::Quota,
60 websocket::{
61 AUTHENTICATION_TIMEOUT_SECS, AuthTracker, SubscriptionState, TEXT_PING, TransportBackend,
62 WebSocketClient, WebSocketConfig, channel_message_handler,
63 },
64};
65use parking_lot::Mutex;
66use serde_json::Value;
67use tokio_tungstenite::tungstenite::Error;
68use tokio_util::sync::CancellationToken;
69use ustr::Ustr;
70
71use super::{
72 enums::OKXWsChannel,
73 error::OKXWsError,
74 handler::{HandlerCommand, OKXWsFeedHandler},
75 messages::{
76 OKXAuthentication, OKXAuthenticationArg, OKXSubscriptionArg, OKXWsMessage, OKXWsRequest,
77 WsAmendOrderParamsBuilder, WsAttachAlgoOrdParams, WsCancelOrderParamsBuilder,
78 WsMassCancelParams, WsPostAlgoOrderParamsBuilder, WsPostOrderParamsBuilder,
79 },
80 subscription::topic_from_subscription_arg,
81};
82use crate::common::{
83 consts::{
84 OKX_NAUTILUS_BROKER_ID, OKX_SUPPORTED_ORDER_TYPES, OKX_SUPPORTED_TIME_IN_FORCE,
85 OKX_WS_PUBLIC_URL, OKX_WS_TOPIC_DELIMITER, select_book_channel,
86 },
87 credential::Credential,
88 enums::{
89 OKXBookChannel, OKXGreeksType, OKXInstrumentType, OKXOrderType, OKXPositionSide,
90 OKXTargetCurrency, OKXTradeMode, OKXTriggerType, OKXVipLevel,
91 conditional_order_to_algo_type, is_conditional_order,
92 },
93 parse::{
94 bar_spec_as_okx_channel, okx_instrument_type, okx_instrument_type_from_symbol,
95 parse_base_quote_from_symbol,
96 },
97};
98
99pub static OKX_WS_CONNECTION_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
103 Quota::per_second(NonZeroU32::new(3).expect("non-zero")).expect("valid constant")
104});
105
106pub static OKX_WS_SUBSCRIPTION_QUOTA: LazyLock<Quota> =
111 LazyLock::new(|| Quota::per_hour(NonZeroU32::new(480).expect("non-zero")));
112
113pub static OKX_WS_ORDER_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
115 Quota::per_second(NonZeroU32::new(30).expect("non-zero")).expect("valid constant")
116});
117
118pub static OKX_WS_BATCH_ORDER_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
120 Quota::per_second(NonZeroU32::new(7).expect("non-zero")).expect("valid constant")
121});
122
123pub static OKX_WS_MASS_CANCEL_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
125 Quota::per_second(NonZeroU32::new(2).expect("non-zero")).expect("valid constant")
126});
127
128pub static OKX_WS_ALGO_ORDER_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
130 Quota::per_second(NonZeroU32::new(10).expect("non-zero")).expect("valid constant")
131});
132
133pub static OKX_WS_ALGO_CANCEL_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
135 Quota::per_second(NonZeroU32::new(1).expect("non-zero")).expect("valid constant")
136});
137
138pub static OKX_RATE_LIMIT_KEY_SUBSCRIPTION: LazyLock<[Ustr; 1]> =
143 LazyLock::new(|| [Ustr::from("subscription")]);
144
145pub static OKX_RATE_LIMIT_KEY_ORDER: LazyLock<[Ustr; 1]> = LazyLock::new(|| [Ustr::from("order")]);
149
150pub static OKX_RATE_LIMIT_KEY_BATCH_ORDER: LazyLock<[Ustr; 1]> =
154 LazyLock::new(|| [Ustr::from("batch-order")]);
155
156pub static OKX_RATE_LIMIT_KEY_CANCEL: LazyLock<[Ustr; 1]> =
160 LazyLock::new(|| [Ustr::from("cancel")]);
161
162pub static OKX_RATE_LIMIT_KEY_BATCH_CANCEL: LazyLock<[Ustr; 1]> =
166 LazyLock::new(|| [Ustr::from("batch-cancel")]);
167
168pub static OKX_RATE_LIMIT_KEY_MASS_CANCEL: LazyLock<[Ustr; 1]> =
172 LazyLock::new(|| [Ustr::from("mass-cancel")]);
173
174pub static OKX_RATE_LIMIT_KEY_AMEND: LazyLock<[Ustr; 1]> = LazyLock::new(|| [Ustr::from("amend")]);
178
179pub static OKX_RATE_LIMIT_KEY_BATCH_AMEND: LazyLock<[Ustr; 1]> =
183 LazyLock::new(|| [Ustr::from("batch-amend")]);
184
185pub static OKX_RATE_LIMIT_KEY_ALGO_ORDER: LazyLock<[Ustr; 1]> =
189 LazyLock::new(|| [Ustr::from("algo-order")]);
190
191pub static OKX_RATE_LIMIT_KEY_ALGO_CANCEL: LazyLock<[Ustr; 1]> =
195 LazyLock::new(|| [Ustr::from("algo-cancel")]);
196
197#[derive(Debug, Clone)]
201#[allow(dead_code)]
202pub(crate) struct PendingOrderInfo {
203 pub trader_id: TraderId,
204 pub strategy_id: StrategyId,
205 pub instrument_id: InstrumentId,
206}
207
208#[derive(Clone)]
210pub struct OKXWebSocketClient {
211 url: String,
212 vip_level: Arc<AtomicU8>,
213 credential: Option<Credential>,
214 heartbeat: Option<u64>,
215 auth_timeout_secs: u64,
216 auth_tracker: AuthTracker,
217 signal: Arc<AtomicBool>,
218 connection_mode: Arc<ArcSwap<AtomicU8>>,
219 cmd_tx: Arc<tokio::sync::RwLock<tokio::sync::mpsc::UnboundedSender<HandlerCommand>>>,
220 out_rx: Option<Arc<tokio::sync::mpsc::UnboundedReceiver<OKXWsMessage>>>,
221 handler_tasks: Arc<TaskGroup>,
222 connect_lock: Arc<tokio::sync::Mutex<()>>,
223 handler_abort: Arc<Mutex<CancellationToken>>,
224 subscriptions_inst_type: Arc<DashMap<OKXWsChannel, AHashSet<OKXInstrumentType>>>,
225 subscriptions_inst_family: Arc<DashMap<OKXWsChannel, AHashSet<Ustr>>>,
226 subscriptions_inst_id: Arc<DashMap<OKXWsChannel, AHashSet<Ustr>>>,
227 subscriptions_bare: Arc<DashMap<OKXWsChannel, bool>>,
228 subscriptions_state: SubscriptionState,
229 request_id_counter: Arc<AtomicU64>,
230 instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
231 inst_id_code_cache: Arc<AtomicMap<Ustr, u64>>,
232 pub(crate) pending_orders: Arc<DashMap<String, PendingOrderInfo>>,
233 pub(crate) pending_cancels: Arc<DashMap<String, PendingOrderInfo>>,
234 pub(crate) pending_amends: Arc<DashMap<String, PendingOrderInfo>>,
235 option_greeks_subs: Arc<AtomicMap<InstrumentId, AHashSet<OKXGreeksType>>>,
236 index_pair_subscribers: Arc<DashMap<Ustr, usize>>,
243 index_pair_transition: Arc<tokio::sync::Mutex<()>>,
248 transport_backend: TransportBackend,
250 proxy_url: Option<String>,
252 cancellation_token: CancellationToken,
253 socket_control: Option<Arc<SocketControl>>,
254}
255
256struct ConnectRollback {
257 handler_tasks: Arc<TaskGroup>,
258 signal: Arc<AtomicBool>,
259 handler_abort: CancellationToken,
260 socket_control: Option<Arc<SocketControl>>,
261 armed: bool,
262}
263
264impl ConnectRollback {
265 fn disarm(&mut self) {
266 self.armed = false;
267 }
268}
269
270impl Drop for ConnectRollback {
271 fn drop(&mut self) {
272 if !self.armed {
273 return;
274 }
275
276 self.handler_tasks.begin_shutdown();
277 self.signal.store(true, Ordering::Release);
278 self.handler_abort.cancel();
279
280 if let Some(control) = &self.socket_control {
281 control.deregister();
282 }
283 }
284}
285
286impl Default for OKXWebSocketClient {
287 fn default() -> Self {
288 Self::new(
289 None,
290 None,
291 None,
292 None,
293 None,
294 None,
295 None,
296 TransportBackend::default(),
297 None,
298 )
299 .unwrap()
300 }
301}
302
303impl Debug for OKXWebSocketClient {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 f.debug_struct(stringify!(OKXWebSocketClient))
306 .field("url", &self.url)
307 .field("credential", &self.credential.as_ref().map(|_| REDACTED))
308 .field("heartbeat", &self.heartbeat)
309 .finish_non_exhaustive()
310 }
311}
312
313impl OKXWebSocketClient {
314 #[allow(clippy::too_many_arguments)]
320 pub fn new(
321 url: Option<String>,
322 api_key: Option<String>,
323 api_secret: Option<String>,
324 api_passphrase: Option<String>,
325 _account_id: Option<AccountId>,
326 heartbeat: Option<u64>,
327 auth_timeout_secs: Option<u64>,
328 transport_backend: TransportBackend,
329 proxy_url: Option<String>,
330 ) -> anyhow::Result<Self> {
331 let url = url.unwrap_or(OKX_WS_PUBLIC_URL.to_string());
332 let credential = match (api_key, api_secret, api_passphrase) {
333 (Some(key), Some(secret), Some(passphrase)) => {
334 Some(Credential::new(key, secret, passphrase))
335 }
336 (None, None, None) => None,
337 _ => anyhow::bail!(
338 "`api_key`, `api_secret`, `api_passphrase` credentials must be provided together"
339 ),
340 };
341
342 let signal = Arc::new(AtomicBool::new(false));
343 let subscriptions_inst_type = Arc::new(DashMap::new());
344 let subscriptions_inst_family = Arc::new(DashMap::new());
345 let subscriptions_inst_id = Arc::new(DashMap::new());
346 let subscriptions_bare = Arc::new(DashMap::new());
347 let subscriptions_state = SubscriptionState::new(OKX_WS_TOPIC_DELIMITER);
348
349 Ok(Self {
350 url,
351 vip_level: Arc::new(AtomicU8::new(0)),
352 credential,
353 heartbeat,
354 auth_timeout_secs: auth_timeout_secs.unwrap_or(AUTHENTICATION_TIMEOUT_SECS),
355 auth_tracker: AuthTracker::new(),
356 signal,
357 connection_mode: Arc::new(ArcSwap::from_pointee(AtomicU8::new(
358 ConnectionMode::Closed.as_u8(),
359 ))),
360 cmd_tx: {
361 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
363 Arc::new(tokio::sync::RwLock::new(tx))
364 },
365 out_rx: None,
366 handler_tasks: Arc::new(TaskGroup::new()),
367 connect_lock: Arc::new(tokio::sync::Mutex::new(())),
368 handler_abort: Arc::new(Mutex::new(CancellationToken::new())),
369 subscriptions_inst_type,
370 subscriptions_inst_family,
371 subscriptions_inst_id,
372 subscriptions_bare,
373 subscriptions_state,
374 request_id_counter: Arc::new(AtomicU64::new(1)),
375 instruments_cache: Arc::new(AtomicMap::new()),
376 inst_id_code_cache: Arc::new(AtomicMap::new()),
377 pending_orders: Arc::new(DashMap::new()),
378 pending_cancels: Arc::new(DashMap::new()),
379 pending_amends: Arc::new(DashMap::new()),
380 option_greeks_subs: Arc::new(AtomicMap::new()),
381 index_pair_subscribers: Arc::new(DashMap::new()),
382 index_pair_transition: Arc::new(tokio::sync::Mutex::new(())),
383 transport_backend,
384 proxy_url,
385 cancellation_token: CancellationToken::new(),
386 socket_control: None,
387 })
388 }
389
390 #[must_use]
392 pub fn with_socket_control(mut self, control: SocketControl) -> Self {
393 self.socket_control = Some(Arc::new(control));
394 self
395 }
396
397 #[allow(clippy::too_many_arguments)]
404 pub fn with_credentials(
405 url: Option<String>,
406 api_key: Option<String>,
407 api_secret: Option<String>,
408 api_passphrase: Option<String>,
409 account_id: Option<AccountId>,
410 heartbeat: Option<u64>,
411 auth_timeout_secs: Option<u64>,
412 transport_backend: TransportBackend,
413 proxy_url: Option<String>,
414 ) -> anyhow::Result<Self> {
415 let url = url.unwrap_or(OKX_WS_PUBLIC_URL.to_string());
416 let api_key = get_or_env_var(api_key, "OKX_API_KEY")?;
417 let api_secret = get_or_env_var(api_secret, "OKX_API_SECRET")?;
418 let api_passphrase = get_or_env_var(api_passphrase, "OKX_API_PASSPHRASE")?;
419
420 Self::new(
421 Some(url),
422 Some(api_key),
423 Some(api_secret),
424 Some(api_passphrase),
425 account_id,
426 heartbeat,
427 auth_timeout_secs,
428 transport_backend,
429 proxy_url,
430 )
431 }
432
433 pub fn from_env() -> anyhow::Result<Self> {
440 let url = get_env_var("OKX_WS_URL")?;
441 let api_key = get_env_var("OKX_API_KEY")?;
442 let api_secret = get_env_var("OKX_API_SECRET")?;
443 let api_passphrase = get_env_var("OKX_API_PASSPHRASE")?;
444
445 Self::new(
446 Some(url),
447 Some(api_key),
448 Some(api_secret),
449 Some(api_passphrase),
450 None,
451 None,
452 None,
453 TransportBackend::default(),
454 None,
455 )
456 }
457
458 pub fn cancel_all_requests(&self) {
460 self.cancellation_token.cancel();
461 }
462
463 pub fn cancellation_token(&self) -> &CancellationToken {
465 &self.cancellation_token
466 }
467
468 pub fn url(&self) -> &str {
470 self.url.as_str()
471 }
472
473 pub fn api_key(&self) -> Option<&str> {
475 self.credential.as_ref().map(|c| c.api_key())
476 }
477
478 #[must_use]
480 pub fn api_key_masked(&self) -> Option<String> {
481 self.credential.as_ref().map(|c| c.api_key_masked())
482 }
483
484 pub fn is_active(&self) -> bool {
486 let connection_mode_arc = self.connection_mode.load();
487 ConnectionMode::from_atomic(&connection_mode_arc).is_active()
488 && !self.signal.load(Ordering::Acquire)
489 }
490
491 pub fn is_closed(&self) -> bool {
493 let connection_mode_arc = self.connection_mode.load();
494 ConnectionMode::from_atomic(&connection_mode_arc).is_closed()
495 || self.signal.load(Ordering::Acquire)
496 }
497
498 pub(crate) fn has_task(&self) -> bool {
500 !self.handler_tasks.is_empty()
501 }
502
503 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
507 self.instruments_cache.rcu(|m| {
508 for inst in instruments {
509 m.insert(inst.symbol().inner(), inst.clone());
510 }
511 });
512 }
513
514 pub fn cache_instrument(&self, instrument: InstrumentAny) {
518 self.instruments_cache
519 .insert(instrument.symbol().inner(), instrument);
520 }
521
522 pub fn instruments_snapshot(&self) -> AHashMap<Ustr, InstrumentAny> {
524 (**self.instruments_cache.load()).clone()
525 }
526
527 pub fn instruments_cache_arc(&self) -> Arc<AtomicMap<Ustr, InstrumentAny>> {
529 Arc::clone(&self.instruments_cache)
530 }
531
532 pub fn cache_inst_id_code(&self, inst_id: Ustr, inst_id_code: u64) {
536 self.inst_id_code_cache.insert(inst_id, inst_id_code);
537 }
538
539 pub fn cache_inst_id_codes(&self, mappings: impl IntoIterator<Item = (Ustr, u64)>) {
543 let entries: Vec<_> = mappings.into_iter().collect();
544 self.inst_id_code_cache.rcu(|m| {
545 for (inst_id, inst_id_code) in &entries {
546 m.insert(*inst_id, *inst_id_code);
547 }
548 });
549 }
550
551 #[must_use]
555 pub fn get_inst_id_code(&self, inst_id: &Ustr) -> Option<u64> {
556 self.inst_id_code_cache.load().get(inst_id).copied()
557 }
558
559 fn inst_id_symbol_and_code_from_snapshot(
560 inst_id_codes: &AHashMap<Ustr, u64>,
561 inst_id: &InstrumentId,
562 action: &str,
563 ) -> Result<(Ustr, u64), OKXWsError> {
564 let inst_id_symbol = inst_id.symbol.inner();
565 let inst_id_code = inst_id_codes.get(&inst_id_symbol).copied().ok_or_else(|| {
566 OKXWsError::ClientError(format!(
567 "No instIdCode cached for {inst_id}, cannot {action} order"
568 ))
569 })?;
570 Ok((inst_id_symbol, inst_id_code))
571 }
572
573 pub fn set_vip_level(&self, vip_level: OKXVipLevel) {
577 self.vip_level.store(vip_level as u8, Ordering::Relaxed);
578 }
579
580 pub fn vip_level(&self) -> OKXVipLevel {
582 let level = self.vip_level.load(Ordering::Relaxed);
583 OKXVipLevel::from(level)
584 }
585
586 pub async fn connect(&mut self) -> anyhow::Result<()> {
596 let connect_lock = Arc::clone(&self.connect_lock);
597 let _connect_guard = connect_lock.lock().await;
598
599 if !self.handler_tasks.is_empty() && !self.handler_tasks.all_finished() {
600 anyhow::bail!("Cannot connect while previous WebSocket handler task is still running");
601 }
602
603 if !self.handler_tasks.is_open() || !self.handler_tasks.is_empty() {
604 self.handler_tasks.begin_shutdown();
605 self.handler_tasks
606 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2))
607 .await
608 .map_err(|e| anyhow::anyhow!("Previous WebSocket handler failed: {e}"))?;
609 self.handler_tasks.start_generation().map_err(|e| {
610 anyhow::anyhow!("Failed to start WebSocket handler task generation: {e}")
611 })?;
612 }
613 let handler_spawner = self.handler_tasks.spawner().map_err(|e| {
614 anyhow::anyhow!("Failed to acquire WebSocket handler task spawner: {e}")
615 })?;
616 let handler_abort = CancellationToken::new();
617 *self.handler_abort.lock() = handler_abort.clone();
618 let mut rollback = ConnectRollback {
619 handler_tasks: Arc::clone(&self.handler_tasks),
620 signal: Arc::clone(&self.signal),
621 handler_abort: handler_abort.clone(),
622 socket_control: self.socket_control.clone(),
623 armed: true,
624 };
625
626 self.signal.store(false, Ordering::Release);
628
629 let (message_handler, raw_rx) = channel_message_handler();
630
631 let headers = vec![(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string())];
637
638 let config = WebSocketConfig {
639 url: self.url.clone(),
640 headers,
641 heartbeat_interval_secs: self.heartbeat,
642 heartbeat_payload: Some(TEXT_PING.to_string()),
643 connect_timeout_ms: Some(5_000),
644 reconnect_delay_initial_ms: None,
645 reconnect_delay_max_ms: None,
646 reconnect_backoff_factor: None,
647 reconnect_jitter_ms: None,
648 reconnect_max_attempts: None,
649 heartbeat_timeout_secs: None,
650 idle_timeout_ms: None,
651 backend: self.transport_backend,
652 proxy_url: self.proxy_url.clone(),
653 };
654
655 let keyed_quotas = vec![
656 (
657 OKX_RATE_LIMIT_KEY_SUBSCRIPTION[0].as_str().to_string(),
658 *OKX_WS_SUBSCRIPTION_QUOTA,
659 ),
660 (
661 OKX_RATE_LIMIT_KEY_ORDER[0].as_str().to_string(),
662 *OKX_WS_ORDER_QUOTA,
663 ),
664 (
665 OKX_RATE_LIMIT_KEY_BATCH_ORDER[0].as_str().to_string(),
666 *OKX_WS_BATCH_ORDER_QUOTA,
667 ),
668 (
669 OKX_RATE_LIMIT_KEY_CANCEL[0].as_str().to_string(),
670 *OKX_WS_ORDER_QUOTA,
671 ),
672 (
673 OKX_RATE_LIMIT_KEY_BATCH_CANCEL[0].as_str().to_string(),
674 *OKX_WS_BATCH_ORDER_QUOTA,
675 ),
676 (
677 OKX_RATE_LIMIT_KEY_MASS_CANCEL[0].as_str().to_string(),
678 *OKX_WS_MASS_CANCEL_QUOTA,
679 ),
680 (
681 OKX_RATE_LIMIT_KEY_AMEND[0].as_str().to_string(),
682 *OKX_WS_ORDER_QUOTA,
683 ),
684 (
685 OKX_RATE_LIMIT_KEY_BATCH_AMEND[0].as_str().to_string(),
686 *OKX_WS_BATCH_ORDER_QUOTA,
687 ),
688 (
689 OKX_RATE_LIMIT_KEY_ALGO_ORDER[0].as_str().to_string(),
690 *OKX_WS_ALGO_ORDER_QUOTA,
691 ),
692 (
693 OKX_RATE_LIMIT_KEY_ALGO_CANCEL[0].as_str().to_string(),
694 *OKX_WS_ALGO_CANCEL_QUOTA,
695 ),
696 ];
697
698 let client = WebSocketClient::builder()
699 .config(config)
700 .message_handler(message_handler)
701 .keyed_quotas(keyed_quotas)
702 .default_quota(*OKX_WS_CONNECTION_QUOTA)
703 .maybe_state_sink(self.socket_control.as_ref().map(|control| control.sink()))
704 .connect()
705 .await?;
706
707 self.connection_mode.store(client.connection_mode_atomic());
709 let reconnect_handle = client.reconnect_handle();
710
711 let (msg_tx, rx) = tokio::sync::mpsc::unbounded_channel::<OKXWsMessage>();
712
713 self.out_rx = Some(Arc::new(rx));
714
715 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel::<HandlerCommand>();
716 *self.cmd_tx.write().await = cmd_tx.clone();
717
718 let signal = self.signal.clone();
719 let auth_tracker = self.auth_tracker.clone();
720 let subscriptions_state = self.subscriptions_state.clone();
721
722 let handler_task = {
723 let auth_tracker = auth_tracker.clone();
724 let signal = signal.clone();
725 let credential = self.credential.clone();
726 let cmd_tx_for_reconnect = cmd_tx.clone();
727 let subscriptions_bare = self.subscriptions_bare.clone();
728 let subscriptions_inst_type = self.subscriptions_inst_type.clone();
729 let subscriptions_inst_family = self.subscriptions_inst_family.clone();
730 let subscriptions_inst_id = self.subscriptions_inst_id.clone();
731 let mut has_reconnected = false;
732
733 async move {
734 let mut handler = OKXWsFeedHandler::new(
735 signal.clone(),
736 cmd_rx,
737 raw_rx,
738 msg_tx,
739 auth_tracker.clone(),
740 subscriptions_state.clone(),
741 );
742
743 let resubscribe_all = || {
745 for entry in subscriptions_inst_id.iter() {
746 let (channel, inst_ids) = entry.pair();
747 for inst_id in inst_ids {
748 let arg = OKXSubscriptionArg {
749 channel: channel.clone(),
750 inst_type: None,
751 inst_family: None,
752 inst_id: Some(*inst_id),
753 };
754
755 if let Err(e) = cmd_tx_for_reconnect
756 .send(HandlerCommand::Subscribe { args: vec![arg] })
757 {
758 log::error!("Failed to send resubscribe command: error={e}");
759 }
760 }
761 }
762
763 for entry in subscriptions_bare.iter() {
764 let channel = entry.key();
765 let arg = OKXSubscriptionArg {
766 channel: channel.clone(),
767 inst_type: None,
768 inst_family: None,
769 inst_id: None,
770 };
771
772 if let Err(e) =
773 cmd_tx_for_reconnect.send(HandlerCommand::Subscribe { args: vec![arg] })
774 {
775 log::error!("Failed to send resubscribe command: error={e}");
776 }
777 }
778
779 for entry in subscriptions_inst_type.iter() {
780 let (channel, inst_types) = entry.pair();
781 for inst_type in inst_types {
782 let arg = OKXSubscriptionArg {
783 channel: channel.clone(),
784 inst_type: Some(*inst_type),
785 inst_family: None,
786 inst_id: None,
787 };
788
789 if let Err(e) = cmd_tx_for_reconnect
790 .send(HandlerCommand::Subscribe { args: vec![arg] })
791 {
792 log::error!("Failed to send resubscribe command: error={e}");
793 }
794 }
795 }
796
797 for entry in subscriptions_inst_family.iter() {
798 let (channel, inst_families) = entry.pair();
799 for inst_family in inst_families {
800 let arg = OKXSubscriptionArg {
801 channel: channel.clone(),
802 inst_type: None,
803 inst_family: Some(*inst_family),
804 inst_id: None,
805 };
806
807 if let Err(e) = cmd_tx_for_reconnect
808 .send(HandlerCommand::Subscribe { args: vec![arg] })
809 {
810 log::error!("Failed to send resubscribe command: error={e}");
811 }
812 }
813 }
814 };
815
816 loop {
817 let message = tokio::select! {
818 () = handler_abort.cancelled() => {
819 log::debug!("Handler task aborted");
820 break;
821 }
822 message = handler.next() => message,
823 };
824
825 match message {
826 Some(OKXWsMessage::Reconnected) => {
827 if signal.load(Ordering::Acquire) {
828 continue;
829 }
830
831 has_reconnected = true;
832
833 subscriptions_state.reset_after_reconnect();
834
835 if let Some(cred) = &credential {
836 log::debug!("Re-authenticating after reconnection");
837 let timestamp = std::time::SystemTime::now()
838 .duration_since(std::time::SystemTime::UNIX_EPOCH)
839 .expect("System time should be after UNIX epoch")
840 .as_secs()
841 .to_string();
842 let signature =
843 cred.sign(×tamp, "GET", "/users/self/verify", "");
844
845 let auth_message = super::messages::OKXAuthentication {
846 op: "login",
847 args: vec![super::messages::OKXAuthenticationArg {
848 api_key: cred.api_key().to_string(),
849 passphrase: cred.api_passphrase().to_string(),
850 timestamp,
851 sign: signature,
852 }],
853 };
854
855 if let Ok(payload) = serde_json::to_string(&auth_message) {
856 if let Err(e) = cmd_tx_for_reconnect
857 .send(HandlerCommand::Authenticate { payload })
858 {
859 log::error!(
860 "Failed to send reconnection auth command: error={e}"
861 );
862 }
863 } else {
864 log::error!("Failed to serialize reconnection auth message");
865 }
866 }
867
868 if credential.is_none() {
871 log::debug!(
872 "No authentication required, resubscribing immediately"
873 );
874 resubscribe_all();
875 }
876
877 if handler.send(OKXWsMessage::Reconnected).is_err() {
879 log_receiver_dropped(&signal, "Reconnected");
880 break;
881 }
882 }
883 Some(OKXWsMessage::Authenticated) => {
884 if has_reconnected {
885 resubscribe_all();
886 }
887 }
888 Some(msg) => {
889 if handler.send(msg).is_err() {
890 log_receiver_dropped(&signal, "message");
891 break;
892 }
893 }
894 None => {
895 if handler.is_stopped() {
896 log::debug!("Stop signal received, ending message processing",);
897 break;
898 }
899 log::debug!("WebSocket stream closed");
900 break;
901 }
902 }
903 }
904
905 log::debug!("Handler task exiting");
906 }
907 };
908
909 if let Err(e) = handler_spawner.spawn(handler_task) {
910 self.out_rx = None;
911 anyhow::bail!("Failed to register WebSocket handler task: {e}");
912 }
913
914 let set_client_result = {
915 let cmd_tx = self.cmd_tx.read().await;
916 cmd_tx.send(HandlerCommand::SetClient(client))
917 };
918
919 if let Err(e) = set_client_result {
920 self.handler_tasks.begin_shutdown();
921 self.signal.store(true, Ordering::Release);
922 let handler_abort = self.handler_abort.lock().clone();
923 handler_abort.cancel();
924 let shutdown_result = self.close_stream_task(Duration::from_secs(2)).await;
925 self.out_rx = None;
926 anyhow::bail!(match shutdown_result {
927 Ok(()) => format!("Failed to send WebSocket client to handler: {e}"),
928 Err(shutdown_error) => format!(
929 "Failed to send WebSocket client to handler: {e}; handler shutdown failed: \
930 {shutdown_error}"
931 ),
932 });
933 }
934
935 if let Some(control) = &self.socket_control {
936 control.register(move || reconnect_handle.request_reconnect());
937 }
938 log::debug!("Sent WebSocket client to handler");
939
940 if self.credential.is_some()
941 && let Err(e) = self.authenticate().await
942 {
943 self.handler_tasks.begin_shutdown();
944 self.request_close().await;
945 let shutdown_result = self.close_stream_task(Duration::from_secs(2)).await;
946
947 if let Some(control) = &self.socket_control {
948 control.deregister();
949 }
950 self.out_rx = None;
951
952 match shutdown_result {
953 Ok(()) => anyhow::bail!("Authentication failed: {e}"),
954 Err(shutdown_error) => anyhow::bail!(
955 "Authentication failed: {e}; handler shutdown failed: {shutdown_error}"
956 ),
957 }
958 }
959
960 rollback.disarm();
961 Ok(())
962 }
963
964 async fn authenticate(&self) -> Result<(), Error> {
966 let credential = self.credential.as_ref().ok_or_else(|| {
967 Error::Io(std::io::Error::other(
968 "API credentials not available to authenticate",
969 ))
970 })?;
971
972 let rx = self.auth_tracker.begin();
973
974 let timestamp = SystemTime::now()
975 .duration_since(SystemTime::UNIX_EPOCH)
976 .expect("System time should be after UNIX epoch")
977 .as_secs()
978 .to_string();
979 let signature = credential.sign(×tamp, "GET", "/users/self/verify", "");
980
981 let auth_message = OKXAuthentication {
982 op: "login",
983 args: vec![OKXAuthenticationArg {
984 api_key: credential.api_key().to_string(),
985 passphrase: credential.api_passphrase().to_string(),
986 timestamp,
987 sign: signature,
988 }],
989 };
990
991 let payload = serde_json::to_string(&auth_message).map_err(|e| {
992 Error::Io(std::io::Error::other(format!(
993 "Failed to serialize auth message: {e}"
994 )))
995 })?;
996
997 self.cmd_tx
998 .read()
999 .await
1000 .send(HandlerCommand::Authenticate { payload })
1001 .map_err(|e| {
1002 Error::Io(std::io::Error::other(format!(
1003 "Failed to send authenticate command: {e}"
1004 )))
1005 })?;
1006
1007 match self
1008 .auth_tracker
1009 .wait_for_result::<OKXWsError>(Duration::from_secs(self.auth_timeout_secs), rx)
1010 .await
1011 {
1012 Ok(()) => {
1013 log::debug!("WebSocket authenticated");
1014 Ok(())
1015 }
1016 Err(e) => {
1017 log::error!("WebSocket authentication failed: error={e}");
1018 Err(Error::Io(std::io::Error::other(e.to_string())))
1019 }
1020 }
1021 }
1022
1023 pub fn stream(&mut self) -> impl Stream<Item = OKXWsMessage> + 'static {
1031 let rx = self
1032 .out_rx
1033 .take()
1034 .expect("Data stream receiver already taken or not connected");
1035 let mut rx = Arc::try_unwrap(rx).expect("Cannot take ownership - other references exist");
1036 async_stream::stream! {
1037 while let Some(data) = rx.recv().await {
1038 yield data;
1039 }
1040 }
1041 }
1042
1043 pub async fn wait_until_active(&self, timeout_secs: f64) -> Result<(), OKXWsError> {
1049 let timeout = tokio::time::Duration::from_secs_f64(timeout_secs);
1050
1051 tokio::time::timeout(timeout, async {
1052 while !self.is_active() {
1053 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
1054 }
1055 })
1056 .await
1057 .map_err(|_| {
1058 OKXWsError::ClientError(format!(
1059 "WebSocket connection timeout after {timeout_secs} seconds"
1060 ))
1061 })?;
1062
1063 Ok(())
1064 }
1065
1066 pub(crate) fn begin_shutdown(&self) {
1067 self.handler_tasks.begin_shutdown();
1068 self.signal.store(true, Ordering::Release);
1069
1070 let handler_abort = self.handler_abort.lock().clone();
1071 handler_abort.cancel();
1072 }
1073
1074 pub(crate) async fn request_close(&self) {
1076 self.signal.store(true, Ordering::Release);
1077
1078 if let Err(e) = self.cmd_tx.read().await.send(HandlerCommand::Disconnect) {
1079 log::debug!("Handler channel closed before disconnect command was sent: {e}");
1080 } else {
1081 log::debug!("Sent disconnect command to handler");
1082 }
1083 }
1084
1085 pub async fn close(&mut self) -> Result<(), Error> {
1092 let connect_lock = Arc::clone(&self.connect_lock);
1093 let _connect_guard = connect_lock.lock().await;
1094
1095 self.close_locked().await
1096 }
1097
1098 async fn close_locked(&self) -> Result<(), Error> {
1099 log::debug!("Starting close process");
1100
1101 self.handler_tasks.begin_shutdown();
1102 self.request_close().await;
1103
1104 let task_result = self.close_stream_task(Duration::from_secs(2)).await;
1105
1106 self.index_pair_subscribers.clear();
1110
1111 if let Some(control) = &self.socket_control {
1112 control.deregister();
1113 }
1114
1115 log::debug!("Close process completed");
1116
1117 task_result
1118 }
1119
1120 async fn close_stream_task(&self, timeout: Duration) -> Result<(), Error> {
1121 match self.handler_tasks.finish_shutdown(timeout, timeout).await {
1122 Ok(()) => Ok(()),
1123 Err(error @ TaskShutdownError::Timeout { .. }) => Err(Error::Io(std::io::Error::new(
1124 std::io::ErrorKind::TimedOut,
1125 format!("Timed out joining WebSocket handler task after abort: {error}"),
1126 ))),
1127 Err(e) => Err(Error::Io(std::io::Error::other(format!(
1128 "WebSocket handler shutdown failed: {e}"
1129 )))),
1130 }
1131 }
1132
1133 pub fn get_subscriptions(&self, instrument_id: InstrumentId) -> Vec<OKXWsChannel> {
1135 let symbol = instrument_id.symbol.inner();
1136 let mut channels = Vec::new();
1137
1138 for entry in self.subscriptions_inst_id.iter() {
1139 let (channel, instruments) = entry.pair();
1140 if instruments.contains(&symbol) {
1141 channels.push(channel.clone());
1142 }
1143 }
1144
1145 channels
1146 }
1147
1148 fn generate_unique_request_id(&self) -> String {
1149 self.request_id_counter
1150 .fetch_add(1, Ordering::SeqCst)
1151 .to_string()
1152 }
1153
1154 async fn subscribe(&self, args: Vec<OKXSubscriptionArg>) -> Result<(), OKXWsError> {
1155 self.cmd_tx
1157 .read()
1158 .await
1159 .send(HandlerCommand::Subscribe { args: args.clone() })
1160 .map_err(|e| {
1161 OKXWsError::ClientError(format!("Failed to send subscribe command: {e}"))
1162 })?;
1163
1164 for arg in &args {
1165 let topic = topic_from_subscription_arg(arg);
1166 self.subscriptions_state.mark_subscribe(&topic);
1167
1168 if arg.inst_type.is_none() && arg.inst_family.is_none() && arg.inst_id.is_none() {
1170 self.subscriptions_bare.insert(arg.channel.clone(), true);
1171 } else {
1172 if let Some(inst_type) = &arg.inst_type {
1173 self.subscriptions_inst_type
1174 .entry(arg.channel.clone())
1175 .or_default()
1176 .insert(*inst_type);
1177 }
1178
1179 if let Some(inst_family) = &arg.inst_family {
1180 self.subscriptions_inst_family
1181 .entry(arg.channel.clone())
1182 .or_default()
1183 .insert(*inst_family);
1184 }
1185
1186 if let Some(inst_id) = &arg.inst_id {
1187 self.subscriptions_inst_id
1188 .entry(arg.channel.clone())
1189 .or_default()
1190 .insert(*inst_id);
1191 }
1192 }
1193 }
1194
1195 Ok(())
1196 }
1197
1198 #[expect(clippy::collapsible_if)]
1199 async fn unsubscribe(&self, args: Vec<OKXSubscriptionArg>) -> Result<(), OKXWsError> {
1200 self.cmd_tx
1202 .read()
1203 .await
1204 .send(HandlerCommand::Unsubscribe { args: args.clone() })
1205 .map_err(|e| {
1206 OKXWsError::ClientError(format!("Failed to send unsubscribe command: {e}"))
1207 })?;
1208
1209 for arg in &args {
1210 let topic = topic_from_subscription_arg(arg);
1211 self.subscriptions_state.mark_unsubscribe(&topic);
1212
1213 if arg.inst_type.is_none() && arg.inst_family.is_none() && arg.inst_id.is_none() {
1214 self.subscriptions_bare.remove(&arg.channel);
1215 } else {
1216 if let Some(inst_type) = &arg.inst_type {
1217 if let Some(mut entry) = self.subscriptions_inst_type.get_mut(&arg.channel) {
1218 entry.remove(inst_type);
1219 if entry.is_empty() {
1220 drop(entry);
1221 self.subscriptions_inst_type.remove(&arg.channel);
1222 }
1223 }
1224 }
1225
1226 if let Some(inst_family) = &arg.inst_family {
1227 if let Some(mut entry) = self.subscriptions_inst_family.get_mut(&arg.channel) {
1228 entry.remove(inst_family);
1229 if entry.is_empty() {
1230 drop(entry);
1231 self.subscriptions_inst_family.remove(&arg.channel);
1232 }
1233 }
1234 }
1235
1236 if let Some(inst_id) = &arg.inst_id {
1237 if let Some(mut entry) = self.subscriptions_inst_id.get_mut(&arg.channel) {
1238 entry.remove(inst_id);
1239 if entry.is_empty() {
1240 drop(entry);
1241 self.subscriptions_inst_id.remove(&arg.channel);
1242 }
1243 }
1244 }
1245 }
1246 }
1247
1248 Ok(())
1249 }
1250
1251 async fn subscribe_inst_id(
1252 &self,
1253 channel: OKXWsChannel,
1254 inst_id: Ustr,
1255 ) -> Result<(), OKXWsError> {
1256 self.subscribe(vec![OKXSubscriptionArg {
1257 channel,
1258 inst_type: None,
1259 inst_family: None,
1260 inst_id: Some(inst_id),
1261 }])
1262 .await
1263 }
1264
1265 async fn unsubscribe_inst_id(
1266 &self,
1267 channel: OKXWsChannel,
1268 inst_id: Ustr,
1269 ) -> Result<(), OKXWsError> {
1270 self.unsubscribe(vec![OKXSubscriptionArg {
1271 channel,
1272 inst_type: None,
1273 inst_family: None,
1274 inst_id: Some(inst_id),
1275 }])
1276 .await
1277 }
1278
1279 pub async fn unsubscribe_all(&self) -> Result<(), OKXWsError> {
1288 const BATCH_SIZE: usize = 256;
1289
1290 let mut all_args = Vec::new();
1291
1292 for entry in self.subscriptions_inst_type.iter() {
1293 let (channel, inst_types) = entry.pair();
1294 for inst_type in inst_types {
1295 all_args.push(OKXSubscriptionArg {
1296 channel: channel.clone(),
1297 inst_type: Some(*inst_type),
1298 inst_family: None,
1299 inst_id: None,
1300 });
1301 }
1302 }
1303
1304 for entry in self.subscriptions_inst_family.iter() {
1305 let (channel, inst_families) = entry.pair();
1306 for inst_family in inst_families {
1307 all_args.push(OKXSubscriptionArg {
1308 channel: channel.clone(),
1309 inst_type: None,
1310 inst_family: Some(*inst_family),
1311 inst_id: None,
1312 });
1313 }
1314 }
1315
1316 for entry in self.subscriptions_inst_id.iter() {
1317 let (channel, inst_ids) = entry.pair();
1318 for inst_id in inst_ids {
1319 all_args.push(OKXSubscriptionArg {
1320 channel: channel.clone(),
1321 inst_type: None,
1322 inst_family: None,
1323 inst_id: Some(*inst_id),
1324 });
1325 }
1326 }
1327
1328 for entry in self.subscriptions_bare.iter() {
1329 let channel = entry.key();
1330 all_args.push(OKXSubscriptionArg {
1331 channel: channel.clone(),
1332 inst_type: None,
1333 inst_family: None,
1334 inst_id: None,
1335 });
1336 }
1337
1338 if all_args.is_empty() {
1339 log::debug!("No active subscriptions to unsubscribe from");
1340 return Ok(());
1341 }
1342
1343 log::debug!("Batched unsubscribe from {} channels", all_args.len());
1344
1345 for chunk in all_args.chunks(BATCH_SIZE) {
1346 self.unsubscribe(chunk.to_vec()).await?;
1347 }
1348
1349 self.index_pair_subscribers.clear();
1353
1354 Ok(())
1355 }
1356
1357 pub async fn subscribe_instruments(
1369 &self,
1370 instrument_type: OKXInstrumentType,
1371 ) -> Result<(), OKXWsError> {
1372 let arg = OKXSubscriptionArg {
1373 channel: OKXWsChannel::Instruments,
1374 inst_type: Some(instrument_type),
1375 inst_family: None,
1376 inst_id: None,
1377 };
1378 self.subscribe(vec![arg]).await
1379 }
1380
1381 pub async fn subscribe_instrument(
1395 &self,
1396 instrument_id: InstrumentId,
1397 ) -> Result<(), OKXWsError> {
1398 let inst_type = okx_instrument_type_from_symbol(instrument_id.symbol.as_str());
1399 log::debug!("Subscribing to instrument type {inst_type:?} for {instrument_id}");
1400 self.subscribe_instruments(inst_type).await
1401 }
1402
1403 pub async fn subscribe_book(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
1412 self.subscribe_book_with_depth(instrument_id, 0).await
1413 }
1414
1415 pub(crate) async fn subscribe_books_channel(
1417 &self,
1418 instrument_id: InstrumentId,
1419 ) -> Result<(), OKXWsError> {
1420 self.subscribe_inst_id(OKXWsChannel::Books, instrument_id.symbol.inner())
1421 .await
1422 }
1423
1424 pub async fn subscribe_book_rpi(&self, instrument_id: InstrumentId) -> Result<(), OKXWsError> {
1430 self.subscribe_inst_id(OKXWsChannel::BooksRpi, instrument_id.symbol.inner())
1431 .await
1432 }
1433
1434 pub(crate) async fn resubscribe_book_channel(
1436 &self,
1437 instrument_id: InstrumentId,
1438 channel: OKXBookChannel,
1439 ) -> Result<(), OKXWsError> {
1440 let channel = ws_channel_for_book(channel);
1441 self.resubscribe_ws_channel(instrument_id, channel).await
1442 }
1443
1444 pub(crate) async fn resubscribe_ws_channel(
1446 &self,
1447 instrument_id: InstrumentId,
1448 channel: OKXWsChannel,
1449 ) -> Result<(), OKXWsError> {
1450 self.unsubscribe_inst_id(channel.clone(), instrument_id.symbol.inner())
1451 .await?;
1452 self.subscribe_inst_id(channel, instrument_id.symbol.inner())
1453 .await
1454 }
1455
1456 pub async fn subscribe_book_depth5(
1468 &self,
1469 instrument_id: InstrumentId,
1470 ) -> Result<(), OKXWsError> {
1471 self.subscribe_inst_id(OKXWsChannel::Books5, instrument_id.symbol.inner())
1472 .await
1473 }
1474
1475 pub async fn subscribe_book50_l2_tbt(
1487 &self,
1488 instrument_id: InstrumentId,
1489 ) -> Result<(), OKXWsError> {
1490 self.subscribe_inst_id(OKXWsChannel::Books50Tbt, instrument_id.symbol.inner())
1491 .await
1492 }
1493
1494 pub async fn subscribe_book_l2_tbt(
1506 &self,
1507 instrument_id: InstrumentId,
1508 ) -> Result<(), OKXWsError> {
1509 self.subscribe_inst_id(OKXWsChannel::BooksTbt, instrument_id.symbol.inner())
1510 .await
1511 }
1512
1513 pub async fn subscribe_book_with_depth(
1527 &self,
1528 instrument_id: InstrumentId,
1529 depth: u16,
1530 ) -> anyhow::Result<()> {
1531 let vip = self.vip_level();
1532
1533 if !matches!(depth, 0 | 50 | 400) {
1534 anyhow::bail!("Invalid depth {depth}, must be 0, 50, or 400");
1535 }
1536
1537 if depth == 50 && vip < OKXVipLevel::Vip4 {
1538 anyhow::bail!("VIP level {vip} insufficient for 50 depth subscription (requires VIP4)");
1539 }
1540
1541 let channel = select_book_channel(depth as usize, vip);
1542 self.subscribe_inst_id(ws_channel_for_book(channel), instrument_id.symbol.inner())
1543 .await?;
1544 Ok(())
1545 }
1546
1547 pub async fn subscribe_quotes(&self, instrument_id: InstrumentId) -> Result<(), OKXWsError> {
1560 self.subscribe_inst_id(OKXWsChannel::BboTbt, instrument_id.symbol.inner())
1561 .await
1562 }
1563
1564 pub async fn subscribe_trades(
1578 &self,
1579 instrument_id: InstrumentId,
1580 aggregated: bool,
1581 ) -> Result<(), OKXWsError> {
1582 let channel = if aggregated {
1583 OKXWsChannel::TradesAll
1584 } else {
1585 OKXWsChannel::Trades
1586 };
1587 self.subscribe_inst_id(channel, instrument_id.symbol.inner())
1588 .await
1589 }
1590
1591 pub async fn subscribe_ticker(&self, instrument_id: InstrumentId) -> Result<(), OKXWsError> {
1603 self.subscribe_inst_id(OKXWsChannel::Tickers, instrument_id.symbol.inner())
1604 .await
1605 }
1606
1607 pub async fn subscribe_mark_prices(
1619 &self,
1620 instrument_id: InstrumentId,
1621 ) -> Result<(), OKXWsError> {
1622 self.subscribe_inst_id(OKXWsChannel::MarkPrice, instrument_id.symbol.inner())
1623 .await
1624 }
1625
1626 pub async fn subscribe_index_prices(
1638 &self,
1639 instrument_id: InstrumentId,
1640 ) -> Result<(), OKXWsError> {
1641 let symbol = instrument_id.symbol.inner();
1643 let (base, quote) = parse_base_quote_from_symbol(symbol.as_str())
1644 .map_err(|e| OKXWsError::ClientError(e.to_string()))?;
1645 let base_pair = Ustr::from(&format!("{base}-{quote}"));
1646
1647 let _guard = self.index_pair_transition.lock().await;
1653
1654 let is_first = {
1659 let mut count = self.index_pair_subscribers.entry(base_pair).or_insert(0);
1660 *count += 1;
1661 *count == 1
1662 };
1663
1664 if !is_first {
1665 return Ok(());
1666 }
1667
1668 let arg = OKXSubscriptionArg {
1669 channel: OKXWsChannel::IndexTickers,
1670 inst_type: None,
1671 inst_family: None,
1672 inst_id: Some(base_pair),
1673 };
1674
1675 match self.subscribe(vec![arg]).await {
1676 Ok(()) => Ok(()),
1677 Err(e) => {
1678 self.index_pair_subscribers.remove(&base_pair);
1687 Err(e)
1688 }
1689 }
1690 }
1691
1692 pub async fn subscribe_option_summary(&self, inst_family: Ustr) -> Result<(), OKXWsError> {
1705 let arg = OKXSubscriptionArg {
1706 channel: OKXWsChannel::OptionSummary,
1707 inst_type: None,
1708 inst_family: Some(inst_family),
1709 inst_id: None,
1710 };
1711 self.subscribe(vec![arg]).await
1712 }
1713
1714 pub async fn subscribe_event_contract_markets(&self) -> Result<(), OKXWsError> {
1724 let arg = OKXSubscriptionArg {
1725 channel: OKXWsChannel::EventContractMarkets,
1726 inst_type: Some(OKXInstrumentType::Events),
1727 inst_family: None,
1728 inst_id: None,
1729 };
1730 self.subscribe(vec![arg]).await
1731 }
1732
1733 pub fn option_greeks_subs(&self) -> &Arc<AtomicMap<InstrumentId, AHashSet<OKXGreeksType>>> {
1737 &self.option_greeks_subs
1738 }
1739
1740 pub fn add_option_greeks_sub(&self, instrument_id: InstrumentId) {
1743 let both: AHashSet<OKXGreeksType> =
1744 [OKXGreeksType::Bs, OKXGreeksType::Pa].into_iter().collect();
1745 self.option_greeks_subs.insert(instrument_id, both);
1746 }
1747
1748 pub fn add_option_greeks_sub_with_conventions(
1751 &self,
1752 instrument_id: InstrumentId,
1753 conventions: AHashSet<OKXGreeksType>,
1754 ) {
1755 let set = if conventions.is_empty() {
1756 [OKXGreeksType::Bs, OKXGreeksType::Pa].into_iter().collect()
1757 } else {
1758 conventions
1759 };
1760 self.option_greeks_subs.insert(instrument_id, set);
1761 }
1762
1763 pub fn remove_option_greeks_sub(&self, instrument_id: &InstrumentId) {
1765 self.option_greeks_subs.remove(instrument_id);
1766 }
1767
1768 pub async fn subscribe_funding_rates(
1780 &self,
1781 instrument_id: InstrumentId,
1782 ) -> Result<(), OKXWsError> {
1783 self.subscribe_inst_id(OKXWsChannel::FundingRate, instrument_id.symbol.inner())
1784 .await
1785 }
1786
1787 pub async fn subscribe_bars(&self, bar_type: BarType) -> Result<(), OKXWsError> {
1799 let channel = bar_spec_as_okx_channel(bar_type.spec())
1801 .map_err(|e| OKXWsError::ClientError(e.to_string()))?;
1802 self.subscribe_inst_id(channel, bar_type.instrument_id().symbol.inner())
1803 .await
1804 }
1805
1806 pub async fn unsubscribe_instruments(
1812 &self,
1813 instrument_type: OKXInstrumentType,
1814 ) -> Result<(), OKXWsError> {
1815 let arg = OKXSubscriptionArg {
1816 channel: OKXWsChannel::Instruments,
1817 inst_type: Some(instrument_type),
1818 inst_family: None,
1819 inst_id: None,
1820 };
1821 self.unsubscribe(vec![arg]).await
1822 }
1823
1824 pub async fn unsubscribe_instrument(
1834 &self,
1835 instrument_id: InstrumentId,
1836 ) -> Result<(), OKXWsError> {
1837 log::debug!("Instrument unsubscribe is a no-op (shared per-type channel): {instrument_id}");
1838 Ok(())
1839 }
1840
1841 pub async fn unsubscribe_book(&self, instrument_id: InstrumentId) -> Result<(), OKXWsError> {
1847 self.unsubscribe_inst_id(OKXWsChannel::Books, instrument_id.symbol.inner())
1848 .await
1849 }
1850
1851 pub async fn unsubscribe_book_rpi(
1857 &self,
1858 instrument_id: InstrumentId,
1859 ) -> Result<(), OKXWsError> {
1860 self.unsubscribe_inst_id(OKXWsChannel::BooksRpi, instrument_id.symbol.inner())
1861 .await
1862 }
1863
1864 pub async fn unsubscribe_book_depth5(
1870 &self,
1871 instrument_id: InstrumentId,
1872 ) -> Result<(), OKXWsError> {
1873 self.unsubscribe_inst_id(OKXWsChannel::Books5, instrument_id.symbol.inner())
1874 .await
1875 }
1876
1877 pub async fn unsubscribe_book50_l2_tbt(
1883 &self,
1884 instrument_id: InstrumentId,
1885 ) -> Result<(), OKXWsError> {
1886 self.unsubscribe_inst_id(OKXWsChannel::Books50Tbt, instrument_id.symbol.inner())
1887 .await
1888 }
1889
1890 pub async fn unsubscribe_book_l2_tbt(
1896 &self,
1897 instrument_id: InstrumentId,
1898 ) -> Result<(), OKXWsError> {
1899 self.unsubscribe_inst_id(OKXWsChannel::BooksTbt, instrument_id.symbol.inner())
1900 .await
1901 }
1902
1903 pub async fn unsubscribe_quotes(&self, instrument_id: InstrumentId) -> Result<(), OKXWsError> {
1909 self.unsubscribe_inst_id(OKXWsChannel::BboTbt, instrument_id.symbol.inner())
1910 .await
1911 }
1912
1913 pub async fn unsubscribe_ticker(&self, instrument_id: InstrumentId) -> Result<(), OKXWsError> {
1919 self.unsubscribe_inst_id(OKXWsChannel::Tickers, instrument_id.symbol.inner())
1920 .await
1921 }
1922
1923 pub async fn unsubscribe_mark_prices(
1929 &self,
1930 instrument_id: InstrumentId,
1931 ) -> Result<(), OKXWsError> {
1932 self.unsubscribe_inst_id(OKXWsChannel::MarkPrice, instrument_id.symbol.inner())
1933 .await
1934 }
1935
1936 pub async fn unsubscribe_index_prices(
1949 &self,
1950 instrument_id: InstrumentId,
1951 ) -> Result<(), OKXWsError> {
1952 let symbol = instrument_id.symbol.inner();
1953 let (base, quote) = parse_base_quote_from_symbol(symbol.as_str())
1954 .map_err(|e| OKXWsError::ClientError(e.to_string()))?;
1955 let base_pair = Ustr::from(&format!("{base}-{quote}"));
1956
1957 let _guard = self.index_pair_transition.lock().await;
1960
1961 let is_last = {
1962 let Some(mut count) = self.index_pair_subscribers.get_mut(&base_pair) else {
1963 return Ok(());
1965 };
1966 *count = count.saturating_sub(1);
1967 *count == 0
1968 };
1969
1970 if !is_last {
1971 return Ok(());
1972 }
1973
1974 self.index_pair_subscribers
1975 .remove_if(&base_pair, |_, count| *count == 0);
1976
1977 let arg = OKXSubscriptionArg {
1978 channel: OKXWsChannel::IndexTickers,
1979 inst_type: None,
1980 inst_family: None,
1981 inst_id: Some(base_pair),
1982 };
1983 self.unsubscribe(vec![arg]).await
1984 }
1985
1986 pub async fn unsubscribe_option_summary(&self, inst_family: Ustr) -> Result<(), OKXWsError> {
1992 let arg = OKXSubscriptionArg {
1993 channel: OKXWsChannel::OptionSummary,
1994 inst_type: None,
1995 inst_family: Some(inst_family),
1996 inst_id: None,
1997 };
1998 self.unsubscribe(vec![arg]).await
1999 }
2000
2001 pub async fn unsubscribe_event_contract_markets(&self) -> Result<(), OKXWsError> {
2007 let arg = OKXSubscriptionArg {
2008 channel: OKXWsChannel::EventContractMarkets,
2009 inst_type: Some(OKXInstrumentType::Events),
2010 inst_family: None,
2011 inst_id: None,
2012 };
2013 self.unsubscribe(vec![arg]).await
2014 }
2015
2016 pub async fn unsubscribe_funding_rates(
2022 &self,
2023 instrument_id: InstrumentId,
2024 ) -> Result<(), OKXWsError> {
2025 self.unsubscribe_inst_id(OKXWsChannel::FundingRate, instrument_id.symbol.inner())
2026 .await
2027 }
2028
2029 pub async fn unsubscribe_trades(
2035 &self,
2036 instrument_id: InstrumentId,
2037 aggregated: bool,
2038 ) -> Result<(), OKXWsError> {
2039 let channel = if aggregated {
2040 OKXWsChannel::TradesAll
2041 } else {
2042 OKXWsChannel::Trades
2043 };
2044 self.unsubscribe_inst_id(channel, instrument_id.symbol.inner())
2045 .await
2046 }
2047
2048 pub async fn unsubscribe_bars(&self, bar_type: BarType) -> Result<(), OKXWsError> {
2054 let channel = bar_spec_as_okx_channel(bar_type.spec())
2055 .map_err(|e| OKXWsError::ClientError(e.to_string()))?;
2056 self.unsubscribe_inst_id(channel, bar_type.instrument_id().symbol.inner())
2057 .await
2058 }
2059
2060 pub async fn subscribe_orders(
2066 &self,
2067 instrument_type: OKXInstrumentType,
2068 ) -> Result<(), OKXWsError> {
2069 let arg = OKXSubscriptionArg {
2070 channel: OKXWsChannel::Orders,
2071 inst_type: Some(instrument_type),
2072 inst_family: None,
2073 inst_id: None,
2074 };
2075 self.subscribe(vec![arg]).await
2076 }
2077
2078 pub async fn unsubscribe_orders(
2084 &self,
2085 instrument_type: OKXInstrumentType,
2086 ) -> Result<(), OKXWsError> {
2087 let arg = OKXSubscriptionArg {
2088 channel: OKXWsChannel::Orders,
2089 inst_type: Some(instrument_type),
2090 inst_family: None,
2091 inst_id: None,
2092 };
2093 self.unsubscribe(vec![arg]).await
2094 }
2095
2096 pub async fn subscribe_spread_orders(&self) -> Result<(), OKXWsError> {
2102 let arg = OKXSubscriptionArg {
2103 channel: OKXWsChannel::SprdOrders,
2104 inst_type: None,
2105 inst_family: None,
2106 inst_id: None,
2107 };
2108 self.subscribe(vec![arg]).await
2109 }
2110
2111 pub async fn unsubscribe_spread_orders(&self) -> Result<(), OKXWsError> {
2117 let arg = OKXSubscriptionArg {
2118 channel: OKXWsChannel::SprdOrders,
2119 inst_type: None,
2120 inst_family: None,
2121 inst_id: None,
2122 };
2123 self.unsubscribe(vec![arg]).await
2124 }
2125
2126 pub async fn subscribe_spread_quotes(
2132 &self,
2133 instrument_id: InstrumentId,
2134 ) -> Result<(), OKXWsError> {
2135 self.subscribe_inst_id(OKXWsChannel::SprdBboTbt, instrument_id.symbol.inner())
2136 .await
2137 }
2138
2139 pub async fn subscribe_spread_book(
2145 &self,
2146 instrument_id: InstrumentId,
2147 ) -> Result<(), OKXWsError> {
2148 self.subscribe_inst_id(OKXWsChannel::SprdBooks5, instrument_id.symbol.inner())
2149 .await
2150 }
2151
2152 pub async fn subscribe_spread_trades(
2158 &self,
2159 instrument_id: InstrumentId,
2160 ) -> Result<(), OKXWsError> {
2161 self.subscribe_inst_id(OKXWsChannel::SprdPublicTrades, instrument_id.symbol.inner())
2162 .await
2163 }
2164
2165 pub async fn unsubscribe_spread_quotes(
2171 &self,
2172 instrument_id: InstrumentId,
2173 ) -> Result<(), OKXWsError> {
2174 self.unsubscribe_inst_id(OKXWsChannel::SprdBboTbt, instrument_id.symbol.inner())
2175 .await
2176 }
2177
2178 pub async fn unsubscribe_spread_book(
2184 &self,
2185 instrument_id: InstrumentId,
2186 ) -> Result<(), OKXWsError> {
2187 self.unsubscribe_inst_id(OKXWsChannel::SprdBooks5, instrument_id.symbol.inner())
2188 .await
2189 }
2190
2191 pub async fn unsubscribe_spread_trades(
2197 &self,
2198 instrument_id: InstrumentId,
2199 ) -> Result<(), OKXWsError> {
2200 self.unsubscribe_inst_id(OKXWsChannel::SprdPublicTrades, instrument_id.symbol.inner())
2201 .await
2202 }
2203
2204 pub async fn subscribe_orders_algo(
2210 &self,
2211 instrument_type: OKXInstrumentType,
2212 ) -> Result<(), OKXWsError> {
2213 let arg = OKXSubscriptionArg {
2214 channel: OKXWsChannel::OrdersAlgo,
2215 inst_type: Some(instrument_type),
2216 inst_family: None,
2217 inst_id: None,
2218 };
2219 self.subscribe(vec![arg]).await
2220 }
2221
2222 pub async fn unsubscribe_orders_algo(
2228 &self,
2229 instrument_type: OKXInstrumentType,
2230 ) -> Result<(), OKXWsError> {
2231 let arg = OKXSubscriptionArg {
2232 channel: OKXWsChannel::OrdersAlgo,
2233 inst_type: Some(instrument_type),
2234 inst_family: None,
2235 inst_id: None,
2236 };
2237 self.unsubscribe(vec![arg]).await
2238 }
2239
2240 pub async fn subscribe_algo_advance(
2246 &self,
2247 instrument_type: OKXInstrumentType,
2248 ) -> Result<(), OKXWsError> {
2249 let arg = OKXSubscriptionArg {
2250 channel: OKXWsChannel::AlgoAdvance,
2251 inst_type: Some(instrument_type),
2252 inst_family: None,
2253 inst_id: None,
2254 };
2255 self.subscribe(vec![arg]).await
2256 }
2257
2258 pub async fn unsubscribe_algo_advance(
2264 &self,
2265 instrument_type: OKXInstrumentType,
2266 ) -> Result<(), OKXWsError> {
2267 let arg = OKXSubscriptionArg {
2268 channel: OKXWsChannel::AlgoAdvance,
2269 inst_type: Some(instrument_type),
2270 inst_family: None,
2271 inst_id: None,
2272 };
2273 self.unsubscribe(vec![arg]).await
2274 }
2275
2276 pub async fn subscribe_account(&self) -> Result<(), OKXWsError> {
2282 let arg = OKXSubscriptionArg {
2283 channel: OKXWsChannel::Account,
2284 inst_type: None,
2285 inst_family: None,
2286 inst_id: None,
2287 };
2288 self.subscribe(vec![arg]).await
2289 }
2290
2291 pub async fn unsubscribe_account(&self) -> Result<(), OKXWsError> {
2297 let arg = OKXSubscriptionArg {
2298 channel: OKXWsChannel::Account,
2299 inst_type: None,
2300 inst_family: None,
2301 inst_id: None,
2302 };
2303 self.unsubscribe(vec![arg]).await
2304 }
2305
2306 pub async fn subscribe_positions(
2316 &self,
2317 inst_type: OKXInstrumentType,
2318 ) -> Result<(), OKXWsError> {
2319 let arg = OKXSubscriptionArg {
2320 channel: OKXWsChannel::Positions,
2321 inst_type: Some(inst_type),
2322 inst_family: None,
2323 inst_id: None,
2324 };
2325 self.subscribe(vec![arg]).await
2326 }
2327
2328 pub async fn unsubscribe_positions(
2334 &self,
2335 inst_type: OKXInstrumentType,
2336 ) -> Result<(), OKXWsError> {
2337 let arg = OKXSubscriptionArg {
2338 channel: OKXWsChannel::Positions,
2339 inst_type: Some(inst_type),
2340 inst_family: None,
2341 inst_id: None,
2342 };
2343 self.unsubscribe(vec![arg]).await
2344 }
2345
2346 pub async fn subscribe_liquidation_warning(
2356 &self,
2357 instrument_type: OKXInstrumentType,
2358 ) -> Result<(), OKXWsError> {
2359 let arg = OKXSubscriptionArg {
2360 channel: OKXWsChannel::LiquidationWarning,
2361 inst_type: Some(instrument_type),
2362 inst_family: None,
2363 inst_id: None,
2364 };
2365 self.subscribe(vec![arg]).await
2366 }
2367
2368 pub async fn unsubscribe_liquidation_warning(
2374 &self,
2375 instrument_type: OKXInstrumentType,
2376 ) -> Result<(), OKXWsError> {
2377 let arg = OKXSubscriptionArg {
2378 channel: OKXWsChannel::LiquidationWarning,
2379 inst_type: Some(instrument_type),
2380 inst_family: None,
2381 inst_id: None,
2382 };
2383 self.unsubscribe(vec![arg]).await
2384 }
2385
2386 async fn ws_batch_place_orders(
2392 &self,
2393 args: Vec<Value>,
2394 client_order_ids: Vec<ClientOrderId>,
2395 ) -> Result<(), OKXWsError> {
2396 let request_id = self.generate_unique_request_id();
2397 let request = OKXWsRequest::<Value> {
2398 id: Some(request_id.clone()),
2399 op: super::enums::OKXWsOperation::BatchOrders,
2400 exp_time: None,
2401 args,
2402 };
2403
2404 let payload = serde_json::to_string(&request)
2405 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize batch orders: {e}")))?;
2406
2407 let cmd = HandlerCommand::Send {
2408 payload,
2409 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_BATCH_ORDER.to_vec()),
2410 request_id: Some(request_id),
2411 client_order_ids,
2412 op: Some(super::enums::OKXWsOperation::BatchOrders),
2413 };
2414
2415 self.send_cmd(cmd).await
2416 }
2417
2418 async fn ws_batch_cancel_orders(
2424 &self,
2425 args: Vec<Value>,
2426 client_order_ids: Vec<ClientOrderId>,
2427 ) -> Result<(), OKXWsError> {
2428 let request_id = self.generate_unique_request_id();
2429 let request = OKXWsRequest::<Value> {
2430 id: Some(request_id.clone()),
2431 op: super::enums::OKXWsOperation::BatchCancelOrders,
2432 exp_time: None,
2433 args,
2434 };
2435
2436 let payload = serde_json::to_string(&request)
2437 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize batch cancel: {e}")))?;
2438
2439 let cmd = HandlerCommand::Send {
2440 payload,
2441 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_BATCH_CANCEL.to_vec()),
2442 request_id: Some(request_id),
2443 client_order_ids,
2444 op: Some(super::enums::OKXWsOperation::BatchCancelOrders),
2445 };
2446
2447 self.send_cmd(cmd).await
2448 }
2449
2450 async fn ws_batch_amend_orders(
2456 &self,
2457 args: Vec<Value>,
2458 client_order_ids: Vec<ClientOrderId>,
2459 ) -> Result<(), OKXWsError> {
2460 let request_id = self.generate_unique_request_id();
2461 let request = OKXWsRequest::<Value> {
2462 id: Some(request_id.clone()),
2463 op: super::enums::OKXWsOperation::BatchAmendOrders,
2464 exp_time: None,
2465 args,
2466 };
2467
2468 let payload = serde_json::to_string(&request)
2469 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize batch amend: {e}")))?;
2470
2471 let cmd = HandlerCommand::Send {
2472 payload,
2473 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_BATCH_AMEND.to_vec()),
2474 request_id: Some(request_id),
2475 client_order_ids,
2476 op: Some(super::enums::OKXWsOperation::BatchAmendOrders),
2477 };
2478
2479 self.send_cmd(cmd).await
2480 }
2481
2482 #[expect(clippy::too_many_arguments)]
2494 pub async fn submit_order(
2495 &self,
2496 trader_id: TraderId,
2497 strategy_id: StrategyId,
2498 instrument_id: InstrumentId,
2499 td_mode: OKXTradeMode,
2500 client_order_id: ClientOrderId,
2501 order_side: OrderSide,
2502 order_type: OrderType,
2503 quantity: Quantity,
2504 time_in_force: Option<TimeInForce>,
2505 price: Option<Price>,
2506 trigger_price: Option<Price>,
2507 post_only: Option<bool>,
2508 reduce_only: Option<bool>,
2509 quote_quantity: Option<bool>,
2510 position_side: Option<PositionSide>,
2511 attach_algo_ords: Option<Vec<WsAttachAlgoOrdParams>>,
2512 px_usd: Option<String>,
2513 px_vol: Option<String>,
2514 speed_bump: Option<String>,
2515 outcome: Option<String>,
2516 slippage_pct: Option<String>,
2517 rpi: Option<bool>,
2518 rpi_taker_access: Option<bool>,
2519 rpi_px_round: Option<bool>,
2520 ) -> Result<(), OKXWsError> {
2521 let rpi = rpi.unwrap_or(false);
2522
2523 if !OKX_SUPPORTED_ORDER_TYPES.contains(&order_type) {
2524 return Err(OKXWsError::ClientError(format!(
2525 "Unsupported order type: {order_type:?}",
2526 )));
2527 }
2528
2529 if let Some(tif) = time_in_force
2530 && !OKX_SUPPORTED_TIME_IN_FORCE.contains(&tif)
2531 {
2532 return Err(OKXWsError::ClientError(format!(
2533 "Unsupported time in force: {tif:?}",
2534 )));
2535 }
2536
2537 let mut builder = WsPostOrderParamsBuilder::default();
2538
2539 let inst_id_code = self
2540 .get_inst_id_code(&instrument_id.symbol.inner())
2541 .ok_or_else(|| {
2542 OKXWsError::ClientError(format!(
2543 "No instIdCode cached for {instrument_id}, cannot submit order"
2544 ))
2545 })?;
2546 builder.inst_id_code(inst_id_code);
2547
2548 builder.td_mode(td_mode);
2549 builder.cl_ord_id(client_order_id.as_str());
2550
2551 let (instrument_type, quote_currency) = {
2552 let instruments = self.instruments_cache.load();
2553 let symbol = instrument_id.symbol.inner();
2554 let instrument = instruments.get(&symbol).ok_or_else(|| {
2555 OKXWsError::ClientError(format!("Unknown instrument {instrument_id}"))
2556 })?;
2557 let instrument_type = okx_instrument_type(instrument)
2558 .map_err(|e| OKXWsError::ClientError(e.to_string()))?;
2559 (instrument_type, instrument.quote_currency())
2560 };
2561
2562 if instrument_type == OKXInstrumentType::Option
2564 && matches!(order_type, OrderType::Market | OrderType::MarketToLimit)
2565 {
2566 return Err(OKXWsError::ClientError(
2567 "Market orders are not supported for OKX options, use Limit orders instead"
2568 .to_string(),
2569 ));
2570 }
2571
2572 match instrument_type {
2573 OKXInstrumentType::Spot | OKXInstrumentType::Margin => {
2574 builder.ccy(quote_currency.to_string());
2576 }
2577 OKXInstrumentType::Swap | OKXInstrumentType::Futures => {
2578 builder.ccy(quote_currency.to_string());
2580
2581 if position_side.is_none() {
2584 builder.pos_side(OKXPositionSide::Net);
2585 }
2586 }
2587 OKXInstrumentType::Option => {
2588 builder.ccy(quote_currency.to_string());
2589
2590 if position_side.is_none() {
2591 builder.pos_side(OKXPositionSide::Net);
2592 }
2593 }
2595 OKXInstrumentType::Events => {}
2596 _ => {
2597 builder.ccy(quote_currency.to_string());
2598
2599 if position_side.is_none() {
2600 builder.pos_side(OKXPositionSide::Net);
2601 }
2602 }
2603 }
2604
2605 if should_send_reduce_only(instrument_type, td_mode, position_side, reduce_only) {
2606 builder.reduce_only(true);
2607 }
2608
2609 if let Some(attach_algo_ords) = attach_algo_ords {
2610 builder.attach_algo_ords(attach_algo_ords);
2611 }
2612
2613 if instrument_type == OKXInstrumentType::Spot
2620 && order_type == OrderType::Market
2621 && td_mode == OKXTradeMode::Cash
2622 {
2623 match quote_quantity {
2624 Some(true) => {
2625 builder.tgt_ccy(OKXTargetCurrency::QuoteCcy);
2626 }
2627 Some(false) if order_side == OrderSide::Buy => {
2629 builder.tgt_ccy(OKXTargetCurrency::BaseCcy);
2630 }
2631 Some(false) | None => {}
2633 }
2634 }
2635
2636 builder.side(order_side);
2637
2638 if let Some(pos_side) = position_side {
2639 builder.pos_side(pos_side);
2640 }
2641
2642 if rpi && order_type != OrderType::Limit {
2646 return Err(OKXWsError::ClientError(
2647 "OKX RPI orders require a limit order".to_string(),
2648 ));
2649 }
2650
2651 let (okx_ord_type, price) = if rpi {
2652 (OKXOrderType::Rpi, price)
2653 } else if post_only.unwrap_or(false) {
2654 (OKXOrderType::PostOnly, price)
2655 } else if let Some(tif) = time_in_force {
2656 match (order_type, tif) {
2657 (OrderType::Market, TimeInForce::Fok) => {
2658 return Err(OKXWsError::ClientError(
2659 "Market orders with FOK time-in-force are not supported by OKX. Use Limit order with FOK instead.".to_string()
2660 ));
2661 }
2662 (OrderType::Market, TimeInForce::Ioc) => {
2663 if matches!(
2665 instrument_type,
2666 OKXInstrumentType::Spot | OKXInstrumentType::Option
2667 ) {
2668 (OKXOrderType::Market, price)
2669 } else {
2670 (OKXOrderType::OptimalLimitIoc, price)
2671 }
2672 }
2673 (OrderType::Limit, TimeInForce::Fok) => {
2674 if instrument_type == OKXInstrumentType::Option {
2676 (OKXOrderType::OpFok, price)
2677 } else {
2678 (OKXOrderType::Fok, price)
2679 }
2680 }
2681 (OrderType::Limit, TimeInForce::Ioc) => (OKXOrderType::Ioc, price),
2682 _ => (OKXOrderType::from(order_type), price),
2683 }
2684 } else {
2685 (OKXOrderType::from(order_type), price)
2686 };
2687
2688 log::debug!(
2689 "Order type mapping: order_type={order_type:?}, time_in_force={time_in_force:?}, post_only={post_only:?} -> okx_ord_type={okx_ord_type:?}"
2690 );
2691
2692 let speed_bump = if instrument_type == OKXInstrumentType::Events {
2693 if outcome.is_none() {
2694 return Err(OKXWsError::ClientError(
2695 "OKX event contract orders require `outcome`".to_string(),
2696 ));
2697 }
2698
2699 if okx_ord_type == OKXOrderType::PostOnly {
2700 speed_bump
2701 } else {
2702 Some(speed_bump.unwrap_or_else(|| "1".to_string()))
2703 }
2704 } else {
2705 speed_bump
2706 };
2707
2708 if let Some(speed_bump) = speed_bump {
2709 builder.speed_bump(speed_bump);
2710 }
2711
2712 if let Some(outcome) = outcome {
2713 builder.outcome(outcome);
2714 }
2715
2716 if let Some(slippage) = slippage_pct {
2717 builder.slippage_pct(slippage);
2718 }
2719
2720 if let Some(rpi_taker_access) = rpi_taker_access {
2721 builder.rpi_taker_access(rpi_taker_access);
2722 }
2723
2724 if let Some(rpi_px_round) = rpi_px_round {
2725 builder.rpi_px_round(rpi_px_round);
2726 }
2727
2728 builder.ord_type(okx_ord_type);
2729 builder.sz(quantity.to_string());
2730
2731 if let Some(usd) = px_usd {
2733 builder.px_usd(usd);
2734 } else if let Some(vol) = px_vol {
2735 builder.px_vol(vol);
2736 } else if let Some(tp) = trigger_price {
2737 builder.px(tp.to_string());
2738 } else if let Some(p) = price {
2739 builder.px(p.to_string());
2740 }
2741
2742 builder.tag(OKX_NAUTILUS_BROKER_ID);
2743
2744 let params = builder
2745 .build()
2746 .map_err(|e| OKXWsError::ClientError(format!("Build order params error: {e}")))?;
2747
2748 let request_id = self.generate_unique_request_id();
2749 let request = OKXWsRequest {
2750 id: Some(request_id.clone()),
2751 op: super::enums::OKXWsOperation::Order,
2752 exp_time: None,
2753 args: vec![params],
2754 };
2755
2756 let payload = serde_json::to_string(&request)
2757 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize order: {e}")))?;
2758
2759 let cl_ord_key = client_order_id.to_string();
2760 self.pending_orders.insert(
2761 cl_ord_key.clone(),
2762 PendingOrderInfo {
2763 trader_id,
2764 strategy_id,
2765 instrument_id,
2766 },
2767 );
2768
2769 let cmd = HandlerCommand::Send {
2770 payload,
2771 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_ORDER.to_vec()),
2772 request_id: Some(request_id),
2773 client_order_ids: vec![client_order_id],
2774 op: Some(super::enums::OKXWsOperation::Order),
2775 };
2776
2777 let result = self.send_cmd(cmd).await;
2778
2779 if result.is_err() {
2780 self.pending_orders.remove(&cl_ord_key);
2781 }
2782
2783 result
2784 }
2785
2786 #[expect(clippy::too_many_arguments)]
2802 pub async fn modify_order(
2803 &self,
2804 trader_id: TraderId,
2805 strategy_id: StrategyId,
2806 instrument_id: InstrumentId,
2807 client_order_id: Option<ClientOrderId>,
2808 price: Option<Price>,
2809 quantity: Option<Quantity>,
2810 venue_order_id: Option<VenueOrderId>,
2811 new_px_usd: Option<String>,
2812 new_px_vol: Option<String>,
2813 speed_bump: Option<String>,
2814 rpi_taker_access: Option<bool>,
2815 rpi_px_round: Option<bool>,
2816 ) -> Result<(), OKXWsError> {
2817 let mut builder = WsAmendOrderParamsBuilder::default();
2818
2819 let inst_id_code = self
2820 .get_inst_id_code(&instrument_id.symbol.inner())
2821 .ok_or_else(|| {
2822 OKXWsError::ClientError(format!(
2823 "No instIdCode cached for {instrument_id}, cannot amend order"
2824 ))
2825 })?;
2826 builder.inst_id_code(inst_id_code);
2827
2828 if let Some(venue_order_id) = venue_order_id {
2829 builder.ord_id(venue_order_id.as_str());
2830 }
2831
2832 let cl_ord_key = client_order_id.map(|id| id.to_string());
2833
2834 if let Some(client_order_id) = client_order_id {
2835 builder.cl_ord_id(client_order_id.as_str());
2836 self.pending_amends.insert(
2837 client_order_id.to_string(),
2838 PendingOrderInfo {
2839 trader_id,
2840 strategy_id,
2841 instrument_id,
2842 },
2843 );
2844 }
2845
2846 if let Some(usd) = new_px_usd {
2848 builder.new_px_usd(usd);
2849 } else if let Some(vol) = new_px_vol {
2850 builder.new_px_vol(vol);
2851 } else if let Some(price) = price {
2852 builder.new_px(price.to_string());
2853 }
2854
2855 if let Some(quantity) = quantity {
2856 builder.new_sz(quantity.to_string());
2857 }
2858
2859 if let Some(speed_bump) = speed_bump {
2860 builder.speed_bump(speed_bump);
2861 }
2862
2863 if let Some(rpi_taker_access) = rpi_taker_access {
2864 builder.rpi_taker_access(rpi_taker_access);
2865 }
2866
2867 if let Some(rpi_px_round) = rpi_px_round {
2868 builder.rpi_px_round(rpi_px_round);
2869 }
2870
2871 let params = builder
2872 .build()
2873 .map_err(|e| OKXWsError::ClientError(format!("Build amend params error: {e}")))?;
2874
2875 let request_id = self.generate_unique_request_id();
2876 let request = OKXWsRequest {
2877 id: Some(request_id.clone()),
2878 op: super::enums::OKXWsOperation::AmendOrder,
2879 exp_time: None,
2880 args: vec![params],
2881 };
2882
2883 let payload = serde_json::to_string(&request)
2884 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize amend: {e}")))?;
2885
2886 let cmd = HandlerCommand::Send {
2887 payload,
2888 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_AMEND.to_vec()),
2889 request_id: Some(request_id),
2890 client_order_ids: client_order_id.into_iter().collect(),
2891 op: Some(super::enums::OKXWsOperation::AmendOrder),
2892 };
2893
2894 let result = self.send_cmd(cmd).await;
2895
2896 if let (Err(_), Some(key)) = (&result, &cl_ord_key) {
2897 self.pending_amends.remove(key);
2898 }
2899
2900 result
2901 }
2902
2903 pub async fn cancel_order(
2914 &self,
2915 trader_id: TraderId,
2916 strategy_id: StrategyId,
2917 instrument_id: InstrumentId,
2918 client_order_id: Option<ClientOrderId>,
2919 venue_order_id: Option<VenueOrderId>,
2920 ) -> Result<(), OKXWsError> {
2921 let mut builder = WsCancelOrderParamsBuilder::default();
2922
2923 let inst_id_code = self
2924 .get_inst_id_code(&instrument_id.symbol.inner())
2925 .ok_or_else(|| {
2926 OKXWsError::ClientError(format!(
2927 "No instIdCode cached for {instrument_id}, cannot cancel order"
2928 ))
2929 })?;
2930 builder.inst_id_code(inst_id_code);
2931
2932 if let Some(venue_order_id) = venue_order_id {
2933 builder.ord_id(venue_order_id.as_str());
2934 }
2935
2936 let cl_ord_key = client_order_id.map(|id| id.to_string());
2937
2938 if let Some(client_order_id) = client_order_id {
2939 builder.cl_ord_id(client_order_id.as_str());
2940 self.pending_cancels.insert(
2941 client_order_id.to_string(),
2942 PendingOrderInfo {
2943 trader_id,
2944 strategy_id,
2945 instrument_id,
2946 },
2947 );
2948 }
2949
2950 let params = builder
2951 .build()
2952 .map_err(|e| OKXWsError::ClientError(format!("Build cancel params error: {e}")))?;
2953
2954 let request_id = self.generate_unique_request_id();
2955 let request = OKXWsRequest {
2956 id: Some(request_id.clone()),
2957 op: super::enums::OKXWsOperation::CancelOrder,
2958 exp_time: None,
2959 args: vec![params],
2960 };
2961
2962 let payload = serde_json::to_string(&request)
2963 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize cancel: {e}")))?;
2964
2965 let cmd = HandlerCommand::Send {
2966 payload,
2967 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_CANCEL.to_vec()),
2968 request_id: Some(request_id),
2969 client_order_ids: client_order_id.into_iter().collect(),
2970 op: Some(super::enums::OKXWsOperation::CancelOrder),
2971 };
2972
2973 let result = self.send_cmd(cmd).await;
2974
2975 if let (Err(_), Some(key)) = (&result, &cl_ord_key) {
2976 self.pending_cancels.remove(key);
2977 }
2978
2979 result
2980 }
2981
2982 pub async fn mass_cancel_orders(&self, instrument_id: InstrumentId) -> Result<(), OKXWsError> {
2992 let (inst_type, inst_family) = {
2993 let instrument = self
2994 .instruments_cache
2995 .get_cloned(&instrument_id.symbol.inner())
2996 .ok_or_else(|| {
2997 OKXWsError::ClientError(format!("Unknown instrument {instrument_id}"))
2998 })?;
2999
3000 let inst_type = okx_instrument_type(&instrument)
3001 .map_err(|e| OKXWsError::ClientError(e.to_string()))?;
3002
3003 let symbol = instrument.symbol().inner();
3004 let inst_family = match &instrument {
3005 InstrumentAny::CurrencyPair(_) => symbol.as_str().to_string(),
3006 InstrumentAny::CryptoPerpetual(_) => symbol
3007 .as_str()
3008 .strip_suffix("-SWAP")
3009 .unwrap_or(symbol.as_str())
3010 .to_string(),
3011 InstrumentAny::CryptoFuture(_) => {
3012 let s = symbol.as_str();
3013 if let Some(idx) = s.rfind('-') {
3014 s[..idx].to_string()
3015 } else {
3016 s.to_string()
3017 }
3018 }
3019 _ => {
3020 return Err(OKXWsError::ClientError(
3021 "Unsupported instrument type for mass cancel".to_string(),
3022 ));
3023 }
3024 };
3025
3026 (inst_type, inst_family)
3027 };
3028
3029 let params = WsMassCancelParams {
3030 inst_type,
3031 inst_family: Ustr::from(&inst_family),
3032 };
3033
3034 let request_id = self.generate_unique_request_id();
3035 let request = OKXWsRequest {
3036 id: Some(request_id.clone()),
3037 op: super::enums::OKXWsOperation::MassCancel,
3038 exp_time: None,
3039 args: vec![
3040 serde_json::to_value(params).map_err(|e| OKXWsError::JsonError(e.to_string()))?,
3041 ],
3042 };
3043
3044 let payload = serde_json::to_string(&request)
3045 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize mass cancel: {e}")))?;
3046
3047 let cmd = HandlerCommand::Send {
3048 payload,
3049 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_MASS_CANCEL.to_vec()),
3050 request_id: Some(request_id),
3051 client_order_ids: Vec::new(),
3052 op: Some(super::enums::OKXWsOperation::MassCancel),
3053 };
3054
3055 self.send_cmd(cmd).await
3056 }
3057
3058 #[expect(clippy::type_complexity)]
3065 pub async fn batch_submit_orders(
3066 &self,
3067 orders: Vec<(
3068 OKXInstrumentType,
3069 InstrumentId,
3070 OKXTradeMode,
3071 ClientOrderId,
3072 OrderSide,
3073 Option<PositionSide>,
3074 OrderType,
3075 Quantity,
3076 Option<Price>,
3077 Option<Price>,
3078 Option<bool>,
3079 Option<bool>,
3080 Option<String>,
3081 Option<String>,
3082 Option<bool>,
3083 Option<bool>,
3084 Option<bool>,
3085 )>,
3086 ) -> Result<(), OKXWsError> {
3087 let client_order_ids: Vec<ClientOrderId> = orders.iter().map(|o| o.3).collect();
3088 let args: Vec<Value> = {
3089 let mut args = Vec::with_capacity(orders.len());
3090 let inst_id_codes = self.inst_id_code_cache.load();
3091 let instruments = self.instruments_cache.load();
3092
3093 for (
3094 inst_type,
3095 inst_id,
3096 td_mode,
3097 cl_ord_id,
3098 ord_side,
3099 pos_side,
3100 ord_type,
3101 qty,
3102 pr,
3103 tp,
3104 post_only,
3105 reduce_only,
3106 speed_bump,
3107 outcome,
3108 rpi,
3109 rpi_taker_access,
3110 rpi_px_round,
3111 ) in orders
3112 {
3113 let rpi = rpi.unwrap_or(false);
3114 let mut builder = WsPostOrderParamsBuilder::default();
3115
3116 let (inst_id_symbol, inst_id_code) = Self::inst_id_symbol_and_code_from_snapshot(
3117 &inst_id_codes,
3118 &inst_id,
3119 "submit",
3120 )?;
3121 builder.inst_id_code(inst_id_code);
3122
3123 builder.td_mode(td_mode);
3124 builder.cl_ord_id(cl_ord_id.as_str());
3125 builder.side(ord_side);
3126
3127 if inst_type != OKXInstrumentType::Events
3128 && let Some(instrument) = instruments.get(&inst_id_symbol)
3129 {
3130 builder.ccy(instrument.quote_currency().to_string());
3131 }
3132
3133 if let Some(ps) = pos_side {
3134 builder.pos_side(OKXPositionSide::from(ps));
3135 } else if matches!(
3136 inst_type,
3137 OKXInstrumentType::Swap
3138 | OKXInstrumentType::Futures
3139 | OKXInstrumentType::Option
3140 ) {
3141 builder.pos_side(OKXPositionSide::Net);
3142 }
3143
3144 if rpi && ord_type != OrderType::Limit {
3145 return Err(OKXWsError::ClientError(
3146 "OKX RPI batch orders require limit orders".to_string(),
3147 ));
3148 }
3149
3150 let okx_ord_type = if rpi {
3151 OKXOrderType::Rpi
3152 } else if post_only.unwrap_or(false) {
3153 OKXOrderType::PostOnly
3154 } else {
3155 match ord_type {
3156 OrderType::Market => OKXOrderType::Market,
3157 OrderType::Limit => OKXOrderType::Limit,
3158 OrderType::MarketToLimit => OKXOrderType::Ioc,
3159 _ => {
3160 return Err(OKXWsError::ClientError(format!(
3161 "Unsupported order type for batch submit: {ord_type:?}"
3162 )));
3163 }
3164 }
3165 };
3166
3167 builder.ord_type(okx_ord_type);
3168 builder.sz(qty.to_string());
3169
3170 if let Some(p) = pr {
3171 builder.px(p.to_string());
3172 } else if let Some(p) = tp {
3173 builder.px(p.to_string());
3174 }
3175
3176 if should_send_reduce_only(inst_type, td_mode, pos_side, reduce_only) {
3177 builder.reduce_only(true);
3178 }
3179
3180 let speed_bump = if inst_type == OKXInstrumentType::Events {
3181 if outcome.is_none() {
3182 return Err(OKXWsError::ClientError(
3183 "OKX event contract orders require `outcome`".to_string(),
3184 ));
3185 }
3186
3187 if okx_ord_type == OKXOrderType::PostOnly {
3188 speed_bump
3189 } else {
3190 Some(speed_bump.unwrap_or_else(|| "1".to_string()))
3191 }
3192 } else {
3193 speed_bump
3194 };
3195
3196 if let Some(speed_bump) = speed_bump {
3197 builder.speed_bump(speed_bump);
3198 }
3199
3200 if let Some(outcome) = outcome {
3201 builder.outcome(outcome);
3202 }
3203
3204 if let Some(rpi_taker_access) = rpi_taker_access {
3205 builder.rpi_taker_access(rpi_taker_access);
3206 }
3207
3208 if let Some(rpi_px_round) = rpi_px_round {
3209 builder.rpi_px_round(rpi_px_round);
3210 }
3211
3212 builder.tag(OKX_NAUTILUS_BROKER_ID);
3213
3214 let params = builder.build().map_err(|e| {
3215 OKXWsError::ClientError(format!("Build order params error: {e}"))
3216 })?;
3217 let val = serde_json::to_value(params)
3218 .map_err(|e| OKXWsError::JsonError(e.to_string()))?;
3219 args.push(val);
3220 }
3221 args
3222 };
3223
3224 self.ws_batch_place_orders(args, client_order_ids).await
3225 }
3226
3227 #[expect(clippy::type_complexity)]
3234 pub async fn batch_modify_orders(
3235 &self,
3236 orders: Vec<(
3237 OKXInstrumentType,
3238 InstrumentId,
3239 ClientOrderId,
3240 Option<String>,
3241 Option<Price>,
3242 Option<Quantity>,
3243 Option<String>,
3244 Option<bool>,
3245 Option<bool>,
3246 )>,
3247 ) -> Result<(), OKXWsError> {
3248 let client_order_ids: Vec<ClientOrderId> = orders.iter().map(|o| o.2).collect();
3249 let args: Vec<Value> = {
3250 let mut args = Vec::with_capacity(orders.len());
3251 let inst_id_codes = self.inst_id_code_cache.load();
3252
3253 for (
3254 _inst_type,
3255 inst_id,
3256 cl_ord_id,
3257 request_id,
3258 pr,
3259 sz,
3260 speed_bump,
3261 rpi_taker_access,
3262 rpi_px_round,
3263 ) in orders
3264 {
3265 let mut builder = WsAmendOrderParamsBuilder::default();
3266
3267 let (_, inst_id_code) =
3268 Self::inst_id_symbol_and_code_from_snapshot(&inst_id_codes, &inst_id, "amend")?;
3269 builder.inst_id_code(inst_id_code);
3270
3271 builder.cl_ord_id(cl_ord_id.as_str());
3272
3273 if let Some(request_id) = request_id {
3274 builder.req_id(request_id);
3275 }
3276
3277 if let Some(p) = pr {
3278 builder.new_px(p.to_string());
3279 }
3280
3281 if let Some(q) = sz {
3282 builder.new_sz(q.to_string());
3283 }
3284
3285 if let Some(speed_bump) = speed_bump {
3286 builder.speed_bump(speed_bump);
3287 }
3288
3289 if let Some(rpi_taker_access) = rpi_taker_access {
3290 builder.rpi_taker_access(rpi_taker_access);
3291 }
3292
3293 if let Some(rpi_px_round) = rpi_px_round {
3294 builder.rpi_px_round(rpi_px_round);
3295 }
3296
3297 let params = builder.build().map_err(|e| {
3298 OKXWsError::ClientError(format!("Build amend batch params error: {e}"))
3299 })?;
3300 let val = serde_json::to_value(params)
3301 .map_err(|e| OKXWsError::JsonError(e.to_string()))?;
3302 args.push(val);
3303 }
3304 args
3305 };
3306
3307 self.ws_batch_amend_orders(args, client_order_ids).await
3308 }
3309
3310 pub async fn batch_cancel_orders(
3323 &self,
3324 orders: Vec<(InstrumentId, Option<ClientOrderId>, Option<VenueOrderId>)>,
3325 ) -> Result<(), OKXWsError> {
3326 let client_order_ids: Vec<ClientOrderId> = orders
3327 .iter()
3328 .filter_map(|(_, cl_ord_id, _)| *cl_ord_id)
3329 .collect();
3330 let args: Vec<Value> = {
3331 let mut args = Vec::with_capacity(orders.len());
3332 let inst_id_codes = self.inst_id_code_cache.load();
3333
3334 for (inst_id, cl_ord_id, ord_id) in orders {
3335 let mut builder = WsCancelOrderParamsBuilder::default();
3336
3337 let (_, inst_id_code) = Self::inst_id_symbol_and_code_from_snapshot(
3338 &inst_id_codes,
3339 &inst_id,
3340 "cancel",
3341 )?;
3342 builder.inst_id_code(inst_id_code);
3343
3344 if let Some(c) = cl_ord_id {
3345 builder.cl_ord_id(c.as_str());
3346 }
3347
3348 if let Some(o) = ord_id {
3349 builder.ord_id(o.as_str());
3350 }
3351
3352 let params = builder.build().map_err(|e| {
3353 OKXWsError::ClientError(format!("Build cancel batch params error: {e}"))
3354 })?;
3355 let val = serde_json::to_value(params)
3356 .map_err(|e| OKXWsError::JsonError(e.to_string()))?;
3357 args.push(val);
3358 }
3359 args
3360 };
3361
3362 self.ws_batch_cancel_orders(args, client_order_ids).await
3363 }
3364
3365 #[expect(clippy::too_many_arguments)]
3376 pub async fn submit_algo_order(
3377 &self,
3378 _trader_id: TraderId,
3379 _strategy_id: StrategyId,
3380 instrument_id: InstrumentId,
3381 td_mode: OKXTradeMode,
3382 client_order_id: ClientOrderId,
3383 order_side: OrderSide,
3384 order_type: OrderType,
3385 quantity: Quantity,
3386 trigger_price: Option<Price>,
3387 trigger_type: Option<TriggerType>,
3388 limit_price: Option<Price>,
3389 reduce_only: Option<bool>,
3390 callback_ratio: Option<String>,
3391 callback_spread: Option<String>,
3392 activation_price: Option<Price>,
3393 ) -> Result<(), OKXWsError> {
3394 if !is_conditional_order(order_type) {
3395 return Err(OKXWsError::ClientError(format!(
3396 "Order type {order_type:?} is not a conditional order"
3397 )));
3398 }
3399
3400 let mut builder = WsPostAlgoOrderParamsBuilder::default();
3401
3402 if !matches!(order_side, OrderSide::Buy | OrderSide::Sell) {
3403 return Err(OKXWsError::ClientError(
3404 "Invalid order side for OKX".to_string(),
3405 ));
3406 }
3407
3408 let inst_id_code = self
3409 .get_inst_id_code(&instrument_id.symbol.inner())
3410 .ok_or_else(|| {
3411 OKXWsError::ClientError(format!(
3412 "No instIdCode cached for {instrument_id}, cannot submit algo order"
3413 ))
3414 })?;
3415 builder.inst_id_code(inst_id_code);
3416
3417 builder.td_mode(td_mode);
3418 builder.cl_ord_id(client_order_id.as_str());
3419 builder.side(order_side);
3420 builder.ord_type(
3421 conditional_order_to_algo_type(order_type)
3422 .map_err(|e| OKXWsError::ClientError(e.to_string()))?,
3423 );
3424 builder.sz(quantity.to_string());
3425
3426 if let Some(tp) = trigger_price {
3427 builder.trigger_px(tp.to_string());
3428 }
3429
3430 let okx_trigger_type = trigger_type.map_or(OKXTriggerType::Last, Into::into);
3432 builder.trigger_px_type(okx_trigger_type);
3433
3434 if matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched)
3436 && let Some(price) = limit_price
3437 {
3438 builder.order_px(price.to_string());
3439 }
3440
3441 if let Some(reduce) = reduce_only {
3442 builder.reduce_only(reduce);
3443 }
3444
3445 if let Some(ratio) = callback_ratio {
3446 builder.callback_ratio(ratio);
3447 }
3448
3449 if let Some(spread) = callback_spread {
3450 builder.callback_spread(spread);
3451 }
3452
3453 if let Some(active) = activation_price {
3454 builder.active_px(active.to_string());
3455 }
3456
3457 builder.tag(OKX_NAUTILUS_BROKER_ID);
3458
3459 let params = builder
3460 .build()
3461 .map_err(|e| OKXWsError::ClientError(format!("Build algo order params error: {e}")))?;
3462
3463 let request_id = self.generate_unique_request_id();
3464 let request = OKXWsRequest {
3465 id: Some(request_id.clone()),
3466 op: super::enums::OKXWsOperation::OrderAlgo,
3467 exp_time: None,
3468 args: vec![params],
3469 };
3470
3471 let payload = serde_json::to_string(&request)
3472 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize algo order: {e}")))?;
3473
3474 let cmd = HandlerCommand::Send {
3475 payload,
3476 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_ALGO_ORDER.to_vec()),
3477 request_id: Some(request_id),
3478 client_order_ids: vec![client_order_id],
3479 op: Some(super::enums::OKXWsOperation::OrderAlgo),
3480 };
3481
3482 self.send_cmd(cmd).await
3483 }
3484
3485 pub async fn cancel_algo_order(
3496 &self,
3497 _trader_id: TraderId,
3498 _strategy_id: StrategyId,
3499 instrument_id: InstrumentId,
3500 client_order_id: Option<ClientOrderId>,
3501 algo_order_id: Option<String>,
3502 ) -> Result<(), OKXWsError> {
3503 let mut builder = super::messages::WsCancelAlgoOrderParamsBuilder::default();
3504
3505 let inst_id_code = self
3506 .get_inst_id_code(&instrument_id.symbol.inner())
3507 .ok_or_else(|| {
3508 OKXWsError::ClientError(format!(
3509 "No instIdCode cached for {instrument_id}, cannot cancel algo order"
3510 ))
3511 })?;
3512 builder.inst_id_code(inst_id_code);
3513
3514 if let Some(algo_id) = algo_order_id {
3515 builder.algo_id(algo_id);
3516 }
3517
3518 if let Some(cl_ord_id) = client_order_id {
3519 builder.algo_cl_ord_id(cl_ord_id.to_string());
3520 }
3521
3522 let params = builder
3523 .build()
3524 .map_err(|e| OKXWsError::ClientError(format!("Build cancel algo params error: {e}")))?;
3525
3526 let request_id = self.generate_unique_request_id();
3527 let request = OKXWsRequest {
3528 id: Some(request_id.clone()),
3529 op: super::enums::OKXWsOperation::CancelAlgos,
3530 exp_time: None,
3531 args: vec![params],
3532 };
3533
3534 let payload = serde_json::to_string(&request)
3535 .map_err(|e| OKXWsError::JsonError(format!("Failed to serialize cancel algo: {e}")))?;
3536
3537 let cmd = HandlerCommand::Send {
3538 payload,
3539 rate_limit_keys: Some(OKX_RATE_LIMIT_KEY_ALGO_CANCEL.to_vec()),
3540 request_id: Some(request_id),
3541 client_order_ids: client_order_id.into_iter().collect(),
3542 op: Some(super::enums::OKXWsOperation::CancelAlgos),
3543 };
3544
3545 self.send_cmd(cmd).await
3546 }
3547
3548 async fn send_cmd(&self, cmd: HandlerCommand) -> Result<(), OKXWsError> {
3550 self.cmd_tx
3551 .read()
3552 .await
3553 .send(cmd)
3554 .map_err(|e| OKXWsError::HandlerUnavailable(e.to_string()))
3555 }
3556}
3557
3558fn should_send_reduce_only(
3559 instrument_type: OKXInstrumentType,
3560 td_mode: OKXTradeMode,
3561 position_side: Option<PositionSide>,
3562 reduce_only: Option<bool>,
3563) -> bool {
3564 if reduce_only != Some(true) {
3565 return false;
3566 }
3567
3568 match instrument_type {
3569 OKXInstrumentType::Spot | OKXInstrumentType::Margin => td_mode != OKXTradeMode::Cash,
3570 OKXInstrumentType::Swap | OKXInstrumentType::Futures => position_side.is_none(),
3571 OKXInstrumentType::Any => true,
3572 OKXInstrumentType::Option | OKXInstrumentType::Events => false,
3573 }
3574}
3575
3576fn ws_channel_for_book(channel: OKXBookChannel) -> OKXWsChannel {
3577 match channel {
3578 OKXBookChannel::Book => OKXWsChannel::Books,
3579 OKXBookChannel::BookL2Tbt => OKXWsChannel::BooksTbt,
3580 OKXBookChannel::Books50L2Tbt => OKXWsChannel::Books50Tbt,
3581 OKXBookChannel::BooksRpi => OKXWsChannel::BooksRpi,
3582 OKXBookChannel::SprdBooks5 => OKXWsChannel::SprdBooks5,
3583 }
3584}
3585
3586fn log_receiver_dropped(signal: &AtomicBool, item: &str) {
3587 if signal.load(Ordering::Acquire) {
3588 log::debug!("Receiver dropped after stop signal while forwarding {item}");
3589 } else {
3590 log::error!("Failed to send {item} through channel: receiver dropped");
3591 }
3592}
3593
3594#[cfg(test)]
3595mod tests {
3596 use nautilus_core::time::get_atomic_clock_realtime;
3597 use nautilus_live::{SocketReconnectRegistry, SocketReconnectRequestOutcome};
3598 use nautilus_model::{identifiers::ClientId, instruments::stubs::crypto_perpetual_ethusdt};
3599 use nautilus_network::RECONNECTED;
3600 use rstest::rstest;
3601 use tokio_tungstenite::tungstenite::Message;
3602
3603 use super::*;
3604 use crate::{
3605 common::{
3606 consts::{OKX_POST_ONLY_CANCEL_SOURCE, OKX_VENUE},
3607 enums::{
3608 OKXExecType, OKXOrderCategory, OKXOrderStatus, OKXPriceType, OKXQuickMarginType,
3609 OKXSelfTradePreventionMode, OKXSide,
3610 },
3611 },
3612 websocket::{
3613 handler::is_post_only_auto_cancel,
3614 messages::{OKXOrderMsg, OKXWebSocketError, OKXWsFrame},
3615 },
3616 };
3617
3618 struct DropSignal(Option<tokio::sync::oneshot::Sender<()>>);
3619
3620 impl Drop for DropSignal {
3621 fn drop(&mut self) {
3622 if let Some(sender) = self.0.take() {
3623 let _ = sender.send(());
3624 }
3625 }
3626 }
3627
3628 struct BlockingDrop(Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>);
3629
3630 impl Drop for BlockingDrop {
3631 fn drop(&mut self) {
3632 let (lock, condvar) = &*self.0;
3633 let mut released = lock.lock();
3634 condvar.wait_while(&mut released, |released| !*released);
3635 }
3636 }
3637
3638 #[rstest]
3639 #[case(OKXBookChannel::Book, OKXWsChannel::Books)]
3640 #[case(OKXBookChannel::BookL2Tbt, OKXWsChannel::BooksTbt)]
3641 #[case(OKXBookChannel::Books50L2Tbt, OKXWsChannel::Books50Tbt)]
3642 #[case(OKXBookChannel::BooksRpi, OKXWsChannel::BooksRpi)]
3643 #[case(OKXBookChannel::SprdBooks5, OKXWsChannel::SprdBooks5)]
3644 fn test_ws_channel_for_book(#[case] channel: OKXBookChannel, #[case] expected: OKXWsChannel) {
3645 assert_eq!(ws_channel_for_book(channel), expected);
3646 }
3647
3648 #[rstest]
3649 fn test_timestamp_format_for_websocket_auth() {
3650 let timestamp = SystemTime::now()
3651 .duration_since(SystemTime::UNIX_EPOCH)
3652 .expect("System time should be after UNIX epoch")
3653 .as_secs()
3654 .to_string();
3655
3656 timestamp.parse::<u64>().unwrap();
3657 assert_eq!(timestamp.len(), 10);
3658 assert!(timestamp.chars().all(|c| c.is_ascii_digit()));
3659 }
3660
3661 #[rstest]
3662 fn test_new_without_credentials() {
3663 let client = OKXWebSocketClient::default();
3664 assert!(client.credential.is_none());
3665 assert_eq!(client.api_key(), None);
3666 }
3667
3668 #[rstest]
3669 fn test_instruments_cache_arc_observes_post_clone_writes() {
3670 let client = OKXWebSocketClient::default();
3671 let cache = client.instruments_cache_arc();
3672 assert!(cache.load().is_empty());
3673
3674 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
3675 let symbol = instrument.symbol().inner();
3676 client.cache_instruments(std::slice::from_ref(&instrument));
3677
3678 let loaded = cache.load();
3679 assert_eq!(loaded.len(), 1);
3680 let stored = loaded.get(&symbol).expect("instrument not refreshed");
3681 assert_eq!(stored.id(), instrument.id());
3682 }
3683
3684 #[rstest]
3685 fn test_add_option_greeks_sub_defaults_to_both_conventions() {
3686 let client = OKXWebSocketClient::default();
3687 let instrument_id = InstrumentId::from("BTC-USD-250328-92000-C.OKX");
3688
3689 client.add_option_greeks_sub(instrument_id);
3690
3691 let subs = client.option_greeks_subs().load();
3692 let stored = subs.get(&instrument_id).expect("instrument not registered");
3693 assert_eq!(stored.len(), 2);
3694 assert!(stored.contains(&OKXGreeksType::Bs));
3695 assert!(stored.contains(&OKXGreeksType::Pa));
3696 }
3697
3698 #[rstest]
3699 #[case::bs_only(vec![OKXGreeksType::Bs])]
3700 #[case::pa_only(vec![OKXGreeksType::Pa])]
3701 #[case::both(vec![OKXGreeksType::Bs, OKXGreeksType::Pa])]
3702 fn test_add_option_greeks_sub_with_conventions_stores_requested_set(
3703 #[case] conventions: Vec<OKXGreeksType>,
3704 ) {
3705 let client = OKXWebSocketClient::default();
3706 let instrument_id = InstrumentId::from("BTC-USD-250328-92000-C.OKX");
3707 let set: AHashSet<OKXGreeksType> = conventions.iter().copied().collect();
3708
3709 client.add_option_greeks_sub_with_conventions(instrument_id, set.clone());
3710
3711 let subs = client.option_greeks_subs().load();
3712 let stored = subs.get(&instrument_id).expect("instrument not registered");
3713 assert_eq!(stored, &set);
3714 }
3715
3716 #[rstest]
3717 fn test_add_option_greeks_sub_with_empty_conventions_falls_back_to_both() {
3718 let client = OKXWebSocketClient::default();
3719 let instrument_id = InstrumentId::from("BTC-USD-250328-92000-C.OKX");
3720
3721 client.add_option_greeks_sub_with_conventions(instrument_id, AHashSet::new());
3722
3723 let subs = client.option_greeks_subs().load();
3724 let stored = subs.get(&instrument_id).expect("instrument not registered");
3725 assert_eq!(stored.len(), 2);
3726 }
3727
3728 #[rstest]
3729 fn test_remove_option_greeks_sub_clears_entry() {
3730 let client = OKXWebSocketClient::default();
3731 let instrument_id = InstrumentId::from("BTC-USD-250328-92000-C.OKX");
3732
3733 client.add_option_greeks_sub(instrument_id);
3734 client.remove_option_greeks_sub(&instrument_id);
3735
3736 let subs = client.option_greeks_subs().load();
3737 assert!(!subs.contains_key(&instrument_id));
3738 }
3739
3740 #[rstest]
3741 fn test_new_with_credentials() {
3742 let client = OKXWebSocketClient::new(
3743 None,
3744 Some("test_key".to_string()),
3745 Some("test_secret".to_string()),
3746 Some("test_passphrase".to_string()),
3747 None,
3748 None,
3749 None,
3750 TransportBackend::default(),
3751 None,
3752 )
3753 .unwrap();
3754 assert!(client.credential.is_some());
3755 assert_eq!(client.api_key(), Some("test_key"));
3756 }
3757
3758 #[rstest]
3759 fn test_new_partial_credentials_fails() {
3760 let result = OKXWebSocketClient::new(
3761 None,
3762 Some("test_key".to_string()),
3763 None,
3764 Some("test_passphrase".to_string()),
3765 None,
3766 None,
3767 None,
3768 TransportBackend::default(),
3769 None,
3770 );
3771 result.unwrap_err();
3772 }
3773
3774 #[rstest]
3775 fn test_request_id_generation() {
3776 let client = OKXWebSocketClient::default();
3777
3778 let initial_counter = client.request_id_counter.load(Ordering::SeqCst);
3779
3780 let id1 = client.request_id_counter.fetch_add(1, Ordering::SeqCst);
3781 let id2 = client.request_id_counter.fetch_add(1, Ordering::SeqCst);
3782
3783 assert_eq!(id1, initial_counter);
3784 assert_eq!(id2, initial_counter + 1);
3785 assert_eq!(
3786 client.request_id_counter.load(Ordering::SeqCst),
3787 initial_counter + 2
3788 );
3789 }
3790
3791 #[rstest]
3792 fn test_client_state_management() {
3793 let client = OKXWebSocketClient::default();
3794
3795 assert!(client.is_closed());
3796 assert!(!client.is_active());
3797
3798 let client_with_heartbeat = OKXWebSocketClient::new(
3799 None,
3800 None,
3801 None,
3802 None,
3803 None,
3804 Some(30),
3805 None,
3806 TransportBackend::default(),
3807 None,
3808 )
3809 .unwrap();
3810
3811 assert!(client_with_heartbeat.heartbeat.is_some());
3812 assert_eq!(client_with_heartbeat.heartbeat.unwrap(), 30);
3813 }
3814
3815 #[rstest]
3816 #[tokio::test]
3817 async fn begin_shutdown_stops_handler_before_bounded_close() {
3818 let client_id = ClientId::from("OKX-TEST");
3819 let endpoint = Ustr::from("okx-test-stream");
3820 let registry = SocketReconnectRegistry::default();
3821 let control =
3822 SocketControl::with_registry(client_id, Some(*OKX_VENUE), endpoint, ®istry);
3823 let _sink = control.sink();
3824 control.register(|| SocketReconnectRequestOutcome::Accepted);
3825 let mut client = OKXWebSocketClient::default().with_socket_control(control);
3826 client
3827 .connection_mode
3828 .load()
3829 .store(ConnectionMode::Active.as_u8(), Ordering::SeqCst);
3830 let (drop_tx, drop_rx) = tokio::sync::oneshot::channel();
3831 let signal = DropSignal(Some(drop_tx));
3832 let handler_abort = CancellationToken::new();
3833 *client.handler_abort.lock() = handler_abort.clone();
3834 client
3835 .handler_tasks
3836 .spawn(async move {
3837 let _signal = signal;
3838 handler_abort.cancelled().await;
3839 })
3840 .expect("handler task should register");
3841
3842 assert!(registry.handle(client_id, endpoint).is_some());
3843 client.begin_shutdown();
3844
3845 tokio::time::timeout(Duration::from_secs(1), drop_rx)
3846 .await
3847 .expect("begin shutdown must drop the handler task")
3848 .expect("drop signal");
3849 assert!(client.is_closed());
3850 assert!(!client.handler_tasks.is_open());
3851 assert!(registry.handle(client_id, endpoint).is_some());
3852
3853 client.close().await.expect("bounded close");
3854 assert!(!client.has_task());
3855 assert!(registry.handle(client_id, endpoint).is_none());
3856 }
3857
3858 #[rstest]
3859 #[tokio::test]
3860 async fn connect_rollback_closes_handler_admission_and_deregisters_socket() {
3861 let client_id = ClientId::from("OKX-CONNECT-ROLLBACK");
3862 let endpoint = Ustr::from("okx-connect-rollback");
3863 let registry = SocketReconnectRegistry::default();
3864 let control =
3865 SocketControl::with_registry(client_id, Some(*OKX_VENUE), endpoint, ®istry);
3866 control.register(|| SocketReconnectRequestOutcome::Accepted);
3867 let handler_tasks = Arc::new(TaskGroup::new());
3868 let signal = Arc::new(AtomicBool::new(false));
3869 let handler_abort = CancellationToken::new();
3870
3871 let rollback = ConnectRollback {
3872 handler_tasks: Arc::clone(&handler_tasks),
3873 signal: Arc::clone(&signal),
3874 handler_abort: handler_abort.clone(),
3875 socket_control: Some(Arc::new(control)),
3876 armed: true,
3877 };
3878
3879 drop(rollback);
3880
3881 assert!(!handler_tasks.is_open());
3882 assert!(signal.load(Ordering::Acquire));
3883 assert!(handler_abort.is_cancelled());
3884 assert!(registry.handle(client_id, endpoint).is_none());
3885 handler_tasks
3886 .finish_shutdown(Duration::ZERO, Duration::from_secs(1))
3887 .await
3888 .expect("empty handler scope should drain");
3889 }
3890
3891 #[rstest]
3892 #[tokio::test]
3893 async fn request_close_signals_before_handler_shutdown() {
3894 let mut client = OKXWebSocketClient::default();
3895 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel();
3896 client.cmd_tx = Arc::new(tokio::sync::RwLock::new(cmd_tx));
3897 client.signal.store(false, Ordering::Release);
3898
3899 client.request_close().await;
3900
3901 assert!(client.signal.load(Ordering::Acquire));
3902 assert!(matches!(cmd_rx.try_recv(), Ok(HandlerCommand::Disconnect)));
3903 }
3904
3905 #[rstest]
3906 #[tokio::test]
3907 async fn close_joins_handler_shared_with_clone() {
3908 let mut client = OKXWebSocketClient::default();
3909 let (drop_tx, drop_rx) = tokio::sync::oneshot::channel();
3910 let signal = DropSignal(Some(drop_tx));
3911 client
3912 .handler_tasks
3913 .spawn(async move {
3914 let _signal = signal;
3915 std::future::pending::<()>().await;
3916 })
3917 .expect("handler task should register");
3918 let retained = client.clone();
3919
3920 client.close().await.expect("close with retained clone");
3921
3922 tokio::time::timeout(Duration::from_secs(1), drop_rx)
3923 .await
3924 .expect("close must drop the handler task")
3925 .expect("drop signal");
3926 assert!(!client.has_task());
3927 assert!(!retained.has_task());
3928 }
3929
3930 #[rstest]
3931 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3932 async fn timeout_retains_unfinished_handler_task() {
3933 let mut client = OKXWebSocketClient::default();
3934 let release = Arc::new((parking_lot::Mutex::new(false), parking_lot::Condvar::new()));
3935 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
3936 let blocking_drop = BlockingDrop(Arc::clone(&release));
3937 client
3938 .handler_tasks
3939 .spawn(async move {
3940 let _blocking_drop = blocking_drop;
3941 started_tx.send(()).expect("started receiver");
3942 std::future::pending::<()>().await;
3943 })
3944 .expect("handler task should register");
3945 started_rx.await.expect("blocking task started");
3946 client.begin_shutdown();
3947
3948 let result = client.close_stream_task(Duration::from_millis(10)).await;
3949 let retained = client.has_task();
3950 let reconnect_result = client.connect().await;
3951
3952 let (lock, condvar) = &*release;
3953 *lock.lock() = true;
3954 condvar.notify_all();
3955
3956 client
3957 .close_stream_task(Duration::from_secs(1))
3958 .await
3959 .expect("blocking handler task terminated");
3960
3961 assert!(result.is_err());
3962 assert!(retained);
3963 assert_eq!(
3964 reconnect_result
3965 .expect_err("reconnect with unfinished handler")
3966 .to_string(),
3967 "Cannot connect while previous WebSocket handler task is still running"
3968 );
3969 assert!(!client.has_task());
3970 }
3971
3972 #[rstest]
3973 fn test_websocket_error_handling() {
3974 let clock = get_atomic_clock_realtime();
3975 let ts = clock.get_time_ns().as_u64();
3976
3977 let error = OKXWebSocketError {
3978 code: "60012".to_string(),
3979 message: "Invalid request".to_string(),
3980 conn_id: None,
3981 timestamp: ts,
3982 };
3983
3984 assert_eq!(error.code, "60012");
3985 assert_eq!(error.message, "Invalid request");
3986 assert_eq!(error.timestamp, ts);
3987
3988 let nautilus_msg = OKXWsMessage::Error(error);
3989 match nautilus_msg {
3990 OKXWsMessage::Error(e) => {
3991 assert_eq!(e.code, "60012");
3992 assert_eq!(e.message, "Invalid request");
3993 }
3994 _ => panic!("Expected Error variant"),
3995 }
3996 }
3997
3998 #[rstest]
3999 fn test_request_id_generation_sequence() {
4000 let client = OKXWebSocketClient::default();
4001
4002 let initial_counter = client
4003 .request_id_counter
4004 .load(std::sync::atomic::Ordering::SeqCst);
4005 let mut ids = Vec::new();
4006
4007 for _ in 0..10 {
4008 let id = client
4009 .request_id_counter
4010 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4011 ids.push(id);
4012 }
4013
4014 for (i, &id) in ids.iter().enumerate() {
4015 assert_eq!(id, initial_counter + i as u64);
4016 }
4017
4018 assert_eq!(
4019 client
4020 .request_id_counter
4021 .load(std::sync::atomic::Ordering::SeqCst),
4022 initial_counter + 10
4023 );
4024 }
4025
4026 #[rstest]
4027 fn test_client_state_transitions() {
4028 let client = OKXWebSocketClient::default();
4029
4030 assert!(client.is_closed());
4031 assert!(!client.is_active());
4032
4033 let client_with_heartbeat = OKXWebSocketClient::new(
4034 None,
4035 None,
4036 None,
4037 None,
4038 None,
4039 Some(30), None,
4041 TransportBackend::default(),
4042 None,
4043 )
4044 .unwrap();
4045
4046 assert!(client_with_heartbeat.heartbeat.is_some());
4047 assert_eq!(client_with_heartbeat.heartbeat.unwrap(), 30);
4048 }
4049
4050 #[rstest]
4051 fn test_websocket_error_scenarios() {
4052 let clock = get_atomic_clock_realtime();
4053 let ts = clock.get_time_ns().as_u64();
4054
4055 let error_scenarios = vec![
4056 ("60012", "Invalid request", None),
4057 ("60009", "Invalid API key", Some("conn-123".to_string())),
4058 ("60014", "Too many requests", None),
4059 ("50001", "Order not found", None),
4060 ];
4061
4062 for (code, message, conn_id) in error_scenarios {
4063 let error = OKXWebSocketError {
4064 code: code.to_string(),
4065 message: message.to_string(),
4066 conn_id: conn_id.clone(),
4067 timestamp: ts,
4068 };
4069
4070 assert_eq!(error.code, code);
4071 assert_eq!(error.message, message);
4072 assert_eq!(error.conn_id, conn_id);
4073 assert_eq!(error.timestamp, ts);
4074
4075 let nautilus_msg = OKXWsMessage::Error(error);
4076 match nautilus_msg {
4077 OKXWsMessage::Error(e) => {
4078 assert_eq!(e.code, code);
4079 assert_eq!(e.message, message);
4080 assert_eq!(e.conn_id, conn_id);
4081 }
4082 _ => panic!("Expected Error variant"),
4083 }
4084 }
4085 }
4086
4087 #[rstest]
4088 fn test_feed_handler_reconnection_detection() {
4089 let msg = Message::Text(RECONNECTED.to_string().into());
4090 let result = OKXWsFeedHandler::parse_raw_message(msg);
4091 assert!(matches!(result, Some(OKXWsFrame::Reconnected)));
4092 }
4093
4094 #[rstest]
4095 fn test_feed_handler_normal_message_processing() {
4096 let ping_msg = Message::Text(TEXT_PING.to_string().into());
4097 let result = OKXWsFeedHandler::parse_raw_message(ping_msg);
4098 assert!(matches!(result, Some(OKXWsFrame::Ping)));
4099
4100 let sub_msg = r#"{
4101 "event": "subscribe",
4102 "arg": {
4103 "channel": "tickers",
4104 "instType": "SPOT"
4105 },
4106 "connId": "a4d3ae55"
4107 }"#;
4108
4109 let sub_result =
4110 OKXWsFeedHandler::parse_raw_message(Message::Text(sub_msg.to_string().into()));
4111 assert!(matches!(sub_result, Some(OKXWsFrame::Subscription { .. })));
4112 }
4113
4114 #[rstest]
4115 fn test_feed_handler_close_message() {
4116 let result = OKXWsFeedHandler::parse_raw_message(Message::Close(None));
4117 assert!(result.is_none());
4118 }
4119
4120 #[rstest]
4121 fn test_reconnection_message_constant() {
4122 assert_eq!(RECONNECTED, "__RECONNECTED__");
4123 }
4124
4125 #[rstest]
4126 fn test_multiple_reconnection_signals() {
4127 for _ in 0..3 {
4128 let msg = Message::Text(RECONNECTED.to_string().into());
4129 let result = OKXWsFeedHandler::parse_raw_message(msg);
4130 assert!(matches!(result, Some(OKXWsFrame::Reconnected)));
4131 }
4132 }
4133
4134 #[tokio::test]
4135 async fn test_wait_until_active_timeout() {
4136 let client = OKXWebSocketClient::new(
4137 None,
4138 Some("test_key".to_string()),
4139 Some("test_secret".to_string()),
4140 Some("test_passphrase".to_string()),
4141 Some(AccountId::from("test-account")),
4142 None,
4143 None,
4144 TransportBackend::default(),
4145 None,
4146 )
4147 .unwrap();
4148
4149 let result = client.wait_until_active(0.1).await;
4150
4151 assert!(result.is_err());
4152 assert!(!client.is_active());
4153 }
4154
4155 fn sample_canceled_order_msg() -> OKXOrderMsg {
4156 OKXOrderMsg {
4157 acc_fill_sz: Some("0".to_string()),
4158 avg_px: "0".to_string(),
4159 c_time: 0,
4160 cancel_source: None,
4161 cancel_source_reason: None,
4162 category: OKXOrderCategory::Normal,
4163 ccy: Ustr::from("USDT"),
4164 cl_ord_id: "order-1".to_string(),
4165 algo_cl_ord_id: None,
4166 attach_algo_cl_ord_id: None,
4167 attach_algo_ords: Vec::new(),
4168 outcome: None,
4169 fee: None,
4170 fee_ccy: Ustr::from("USDT"),
4171 fill_px: "0".to_string(),
4172 fill_sz: "0".to_string(),
4173 fill_time: 0,
4174 inst_id: Ustr::from("ETH-USDT-SWAP"),
4175 inst_type: OKXInstrumentType::Swap,
4176 lever: "1".to_string(),
4177 ord_id: Ustr::from("123456"),
4178 ord_type: OKXOrderType::Limit,
4179 pnl: "0".to_string(),
4180 pos_side: OKXPositionSide::Net,
4181 px: "0".to_string(),
4182 reduce_only: "false".to_string(),
4183 side: OKXSide::Buy,
4184 state: OKXOrderStatus::Canceled,
4185 exec_type: OKXExecType::None,
4186 sz: "1".to_string(),
4187 td_mode: OKXTradeMode::Cross,
4188 tgt_ccy: None,
4189 trade_id: String::new(),
4190 algo_id: None,
4191 fill_fee: None,
4192 fill_fee_ccy: None,
4193 fill_mark_px: None,
4194 fill_mark_vol: None,
4195 fill_px_vol: None,
4196 fill_px_usd: None,
4197 fill_fwd_px: None,
4198 fill_notional_usd: None,
4199 fill_pnl: None,
4200 is_tp_limit: None,
4201 linked_algo_ord: None,
4202 notional_usd: None,
4203 px_type: OKXPriceType::None,
4204 px_usd: None,
4205 px_vol: None,
4206 quick_mgn_type: OKXQuickMarginType::None,
4207 rebate: None,
4208 rebate_ccy: None,
4209 sl_ord_px: None,
4210 sl_trigger_px: None,
4211 sl_trigger_px_type: None,
4212 source: None,
4213 stp_id: None,
4214 stp_mode: OKXSelfTradePreventionMode::None,
4215 tag: None,
4216 tp_ord_px: None,
4217 tp_trigger_px: None,
4218 tp_trigger_px_type: None,
4219 amend_result: None,
4220 req_id: None,
4221 code: None,
4222 msg: None,
4223 u_time: 0,
4224 }
4225 }
4226
4227 #[rstest]
4228 fn test_is_post_only_auto_cancel_detects_cancel_source() {
4229 let mut msg = sample_canceled_order_msg();
4230 msg.cancel_source = Some(OKX_POST_ONLY_CANCEL_SOURCE.to_string());
4231
4232 assert!(is_post_only_auto_cancel(&msg));
4233 }
4234
4235 #[rstest]
4236 fn test_is_post_only_auto_cancel_detects_reason() {
4237 let mut msg = sample_canceled_order_msg();
4238 msg.cancel_source_reason = Some("POST_ONLY would take liquidity".to_string());
4239
4240 assert!(is_post_only_auto_cancel(&msg));
4241 }
4242
4243 #[rstest]
4244 fn test_is_post_only_auto_cancel_false_without_markers() {
4245 let msg = sample_canceled_order_msg();
4246
4247 assert!(!is_post_only_auto_cancel(&msg));
4248 }
4249
4250 #[rstest]
4251 fn test_is_post_only_auto_cancel_false_for_order_type_only() {
4252 let mut msg = sample_canceled_order_msg();
4253 msg.ord_type = OKXOrderType::PostOnly;
4254
4255 assert!(!is_post_only_auto_cancel(&msg));
4256 }
4257
4258 #[tokio::test]
4259 async fn test_batch_cancel_orders_with_multiple_orders() {
4260 use nautilus_model::identifiers::{ClientOrderId, InstrumentId, VenueOrderId};
4261
4262 let client = OKXWebSocketClient::new(
4263 Some("wss://test.okx.com".to_string()),
4264 None,
4265 None,
4266 None,
4267 None,
4268 None,
4269 None,
4270 TransportBackend::default(),
4271 None,
4272 )
4273 .expect("Failed to create client");
4274
4275 let instrument_id = InstrumentId::from("BTC-USDT.OKX");
4276 let client_order_id1 = ClientOrderId::new("order1");
4277 let client_order_id2 = ClientOrderId::new("order2");
4278 let venue_order_id1 = VenueOrderId::new("venue1");
4279 let venue_order_id2 = VenueOrderId::new("venue2");
4280
4281 let orders = vec![
4282 (instrument_id, Some(client_order_id1), Some(venue_order_id1)),
4283 (instrument_id, Some(client_order_id2), Some(venue_order_id2)),
4284 ];
4285
4286 let result = client.batch_cancel_orders(orders).await;
4287 assert!(result.is_err());
4288 }
4289
4290 #[tokio::test]
4291 async fn test_batch_cancel_orders_with_only_client_order_id() {
4292 use nautilus_model::identifiers::{ClientOrderId, InstrumentId};
4293
4294 let client = OKXWebSocketClient::new(
4295 Some("wss://test.okx.com".to_string()),
4296 None,
4297 None,
4298 None,
4299 None,
4300 None,
4301 None,
4302 TransportBackend::default(),
4303 None,
4304 )
4305 .expect("Failed to create client");
4306
4307 let instrument_id = InstrumentId::from("BTC-USDT.OKX");
4308 let client_order_id = ClientOrderId::new("order1");
4309
4310 let orders = vec![(instrument_id, Some(client_order_id), None)];
4311
4312 let result = client.batch_cancel_orders(orders).await;
4313
4314 assert!(result.is_err());
4315 }
4316
4317 #[tokio::test]
4318 async fn test_batch_cancel_orders_with_only_venue_order_id() {
4319 use nautilus_model::identifiers::{InstrumentId, VenueOrderId};
4320
4321 let client = OKXWebSocketClient::new(
4322 Some("wss://test.okx.com".to_string()),
4323 None,
4324 None,
4325 None,
4326 None,
4327 None,
4328 None,
4329 TransportBackend::default(),
4330 None,
4331 )
4332 .expect("Failed to create client");
4333
4334 let instrument_id = InstrumentId::from("BTC-USDT.OKX");
4335 let venue_order_id = VenueOrderId::new("venue1");
4336
4337 let orders = vec![(instrument_id, None, Some(venue_order_id))];
4338
4339 let result = client.batch_cancel_orders(orders).await;
4340
4341 assert!(result.is_err());
4342 }
4343
4344 #[tokio::test]
4345 async fn test_batch_cancel_orders_with_both_ids() {
4346 use nautilus_model::identifiers::{ClientOrderId, InstrumentId, VenueOrderId};
4347
4348 let client = OKXWebSocketClient::new(
4349 Some("wss://test.okx.com".to_string()),
4350 None,
4351 None,
4352 None,
4353 None,
4354 None,
4355 None,
4356 TransportBackend::default(),
4357 None,
4358 )
4359 .expect("Failed to create client");
4360
4361 let instrument_id = InstrumentId::from("BTC-USDT-SWAP.OKX");
4362 let client_order_id = ClientOrderId::new("order1");
4363 let venue_order_id = VenueOrderId::new("venue1");
4364
4365 let orders = vec![(instrument_id, Some(client_order_id), Some(venue_order_id))];
4366
4367 let result = client.batch_cancel_orders(orders).await;
4368
4369 assert!(result.is_err());
4370 }
4371
4372 #[tokio::test]
4373 async fn test_cancel_order_fails_without_inst_id_code() {
4374 use nautilus_model::identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId};
4375
4376 let client = OKXWebSocketClient::default();
4377 let instrument_id = InstrumentId::from("BTC-USDT-SWAP.OKX");
4378
4379 let result = client
4380 .cancel_order(
4381 TraderId::from("TESTER-001"),
4382 StrategyId::from("S-001"),
4383 instrument_id,
4384 Some(ClientOrderId::new("O-001")),
4385 None,
4386 )
4387 .await;
4388
4389 assert!(result.is_err());
4390 let err = result.unwrap_err().to_string();
4391 assert!(
4392 err.contains("No instIdCode cached for BTC-USDT-SWAP.OKX"),
4393 "Expected instIdCode error, found: {err}"
4394 );
4395 }
4396
4397 #[tokio::test]
4398 async fn test_submit_order_fails_without_inst_id_code() {
4399 use nautilus_model::{
4400 enums::{OrderSide, OrderType},
4401 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
4402 types::Quantity,
4403 };
4404
4405 use crate::common::enums::OKXTradeMode;
4406
4407 let client = OKXWebSocketClient::default();
4408 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
4409
4410 let result = client
4411 .submit_order(
4412 TraderId::from("TESTER-001"),
4413 StrategyId::from("S-001"),
4414 instrument_id,
4415 OKXTradeMode::Cross,
4416 ClientOrderId::new("O-001"),
4417 OrderSide::Buy,
4418 OrderType::Limit,
4419 Quantity::from("0.01"),
4420 None,
4421 None,
4422 None,
4423 None,
4424 None,
4425 None,
4426 None,
4427 None,
4428 None,
4429 None,
4430 None,
4431 None,
4432 None,
4433 None,
4434 None,
4435 None,
4436 )
4437 .await;
4438
4439 assert!(result.is_err());
4440 let err = result.unwrap_err().to_string();
4441 assert!(
4442 err.contains("No instIdCode cached for ETH-USDT-SWAP.OKX"),
4443 "Expected instIdCode error, found: {err}"
4444 );
4445 }
4446
4447 #[tokio::test]
4448 async fn test_cancel_order_passes_inst_id_code_lookup_when_cached() {
4449 use nautilus_model::identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId};
4450 use ustr::Ustr;
4451
4452 let client = OKXWebSocketClient::default();
4453 let instrument_id = InstrumentId::from("BTC-USDT-SWAP.OKX");
4454
4455 client.cache_inst_id_code(Ustr::from("BTC-USDT-SWAP"), 10459);
4457
4458 let result = client
4459 .cancel_order(
4460 TraderId::from("TESTER-001"),
4461 StrategyId::from("S-001"),
4462 instrument_id,
4463 Some(ClientOrderId::new("O-001")),
4464 None,
4465 )
4466 .await;
4467
4468 assert!(result.is_err());
4470 let err = result.unwrap_err().to_string();
4471 assert!(
4472 !err.contains("No instIdCode cached"),
4473 "Should pass instIdCode lookup, found: {err}"
4474 );
4475 }
4476
4477 #[rstest]
4478 fn test_race_unsubscribe_failure_recovery() {
4479 let client = OKXWebSocketClient::new(
4485 Some("wss://test.okx.com".to_string()),
4486 None,
4487 None,
4488 None,
4489 None,
4490 None,
4491 None,
4492 TransportBackend::default(),
4493 None,
4494 )
4495 .expect("Failed to create client");
4496
4497 let topic = "trades:BTC-USDT-SWAP";
4498
4499 client.subscriptions_state.mark_subscribe(topic);
4501 client.subscriptions_state.confirm_subscribe(topic);
4502 assert_eq!(client.subscriptions_state.len(), 1);
4503
4504 client.subscriptions_state.mark_unsubscribe(topic);
4506 assert_eq!(client.subscriptions_state.len(), 0);
4507 assert_eq!(
4508 client.subscriptions_state.pending_unsubscribe_topics(),
4509 vec![topic]
4510 );
4511
4512 client.subscriptions_state.confirm_unsubscribe(topic); client.subscriptions_state.mark_subscribe(topic); client.subscriptions_state.confirm_subscribe(topic); assert_eq!(client.subscriptions_state.len(), 1);
4520 assert!(
4521 client
4522 .subscriptions_state
4523 .pending_unsubscribe_topics()
4524 .is_empty()
4525 );
4526 assert!(
4527 client
4528 .subscriptions_state
4529 .pending_subscribe_topics()
4530 .is_empty()
4531 );
4532
4533 let all = client.subscriptions_state.all_topics();
4535 assert_eq!(all.len(), 1);
4536 assert!(all.contains(&topic.to_string()));
4537 }
4538
4539 #[rstest]
4540 fn test_race_resubscribe_before_unsubscribe_ack() {
4541 let client = OKXWebSocketClient::new(
4545 Some("wss://test.okx.com".to_string()),
4546 None,
4547 None,
4548 None,
4549 None,
4550 None,
4551 None,
4552 TransportBackend::default(),
4553 None,
4554 )
4555 .expect("Failed to create client");
4556
4557 let topic = "books:BTC-USDT";
4558
4559 client.subscriptions_state.mark_subscribe(topic);
4561 client.subscriptions_state.confirm_subscribe(topic);
4562 assert_eq!(client.subscriptions_state.len(), 1);
4563
4564 client.subscriptions_state.mark_unsubscribe(topic);
4566 assert_eq!(client.subscriptions_state.len(), 0);
4567 assert_eq!(
4568 client.subscriptions_state.pending_unsubscribe_topics(),
4569 vec![topic]
4570 );
4571
4572 client.subscriptions_state.mark_subscribe(topic);
4574 assert_eq!(
4575 client.subscriptions_state.pending_subscribe_topics(),
4576 vec![topic]
4577 );
4578
4579 client.subscriptions_state.confirm_unsubscribe(topic);
4581 assert!(
4582 client
4583 .subscriptions_state
4584 .pending_unsubscribe_topics()
4585 .is_empty()
4586 );
4587 assert_eq!(
4588 client.subscriptions_state.pending_subscribe_topics(),
4589 vec![topic]
4590 );
4591
4592 client.subscriptions_state.confirm_subscribe(topic);
4594 assert_eq!(client.subscriptions_state.len(), 1);
4595 assert!(
4596 client
4597 .subscriptions_state
4598 .pending_subscribe_topics()
4599 .is_empty()
4600 );
4601
4602 let all = client.subscriptions_state.all_topics();
4604 assert_eq!(all.len(), 1);
4605 assert!(all.contains(&topic.to_string()));
4606 }
4607
4608 #[rstest]
4609 fn test_race_late_subscribe_confirmation_after_unsubscribe() {
4610 let client = OKXWebSocketClient::new(
4613 Some("wss://test.okx.com".to_string()),
4614 None,
4615 None,
4616 None,
4617 None,
4618 None,
4619 None,
4620 TransportBackend::default(),
4621 None,
4622 )
4623 .expect("Failed to create client");
4624
4625 let topic = "tickers:ETH-USDT";
4626
4627 client.subscriptions_state.mark_subscribe(topic);
4629 assert_eq!(
4630 client.subscriptions_state.pending_subscribe_topics(),
4631 vec![topic]
4632 );
4633
4634 client.subscriptions_state.mark_unsubscribe(topic);
4636 assert!(
4637 client
4638 .subscriptions_state
4639 .pending_subscribe_topics()
4640 .is_empty()
4641 ); assert_eq!(
4643 client.subscriptions_state.pending_unsubscribe_topics(),
4644 vec![topic]
4645 );
4646
4647 client.subscriptions_state.confirm_subscribe(topic);
4649 assert_eq!(client.subscriptions_state.len(), 0); assert_eq!(
4651 client.subscriptions_state.pending_unsubscribe_topics(),
4652 vec![topic]
4653 );
4654
4655 client.subscriptions_state.confirm_unsubscribe(topic);
4657
4658 assert!(client.subscriptions_state.is_empty());
4660 assert!(client.subscriptions_state.all_topics().is_empty());
4661 }
4662
4663 #[rstest]
4664 fn test_race_reconnection_with_pending_states() {
4665 let client = OKXWebSocketClient::new(
4667 Some("wss://test.okx.com".to_string()),
4668 Some("test_key".to_string()),
4669 Some("test_secret".to_string()),
4670 Some("test_passphrase".to_string()),
4671 Some(AccountId::new("OKX-TEST")),
4672 None,
4673 None,
4674 TransportBackend::default(),
4675 None,
4676 )
4677 .expect("Failed to create client");
4678
4679 let trade_btc = "trades:BTC-USDT-SWAP";
4682 client.subscriptions_state.mark_subscribe(trade_btc);
4683 client.subscriptions_state.confirm_subscribe(trade_btc);
4684
4685 let trade_eth = "trades:ETH-USDT-SWAP";
4687 client.subscriptions_state.mark_subscribe(trade_eth);
4688
4689 let book_btc = "books:BTC-USDT";
4691 client.subscriptions_state.mark_subscribe(book_btc);
4692 client.subscriptions_state.confirm_subscribe(book_btc);
4693 client.subscriptions_state.mark_unsubscribe(book_btc);
4694
4695 let topics_to_restore = client.subscriptions_state.all_topics();
4697
4698 assert_eq!(topics_to_restore.len(), 2);
4700 assert!(topics_to_restore.contains(&trade_btc.to_string()));
4701 assert!(topics_to_restore.contains(&trade_eth.to_string()));
4702 assert!(!topics_to_restore.contains(&book_btc.to_string())); }
4704
4705 #[rstest]
4706 fn test_race_duplicate_subscribe_messages_idempotent() {
4707 let client = OKXWebSocketClient::new(
4710 Some("wss://test.okx.com".to_string()),
4711 None,
4712 None,
4713 None,
4714 None,
4715 None,
4716 None,
4717 TransportBackend::default(),
4718 None,
4719 )
4720 .expect("Failed to create client");
4721
4722 let topic = "trades:BTC-USDT-SWAP";
4723
4724 client.subscriptions_state.mark_subscribe(topic);
4726 client.subscriptions_state.confirm_subscribe(topic);
4727 assert_eq!(client.subscriptions_state.len(), 1);
4728
4729 client.subscriptions_state.mark_subscribe(topic);
4731 assert!(
4732 client
4733 .subscriptions_state
4734 .pending_subscribe_topics()
4735 .is_empty()
4736 ); assert_eq!(client.subscriptions_state.len(), 1); client.subscriptions_state.confirm_subscribe(topic);
4741 assert_eq!(client.subscriptions_state.len(), 1);
4742
4743 let all = client.subscriptions_state.all_topics();
4745 assert_eq!(all.len(), 1);
4746 assert_eq!(all[0], topic);
4747 }
4748}