1use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
7use ccxt_core::{BaseExchange, ExchangeConfig, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub mod auth;
12pub mod builder;
13pub mod endpoint_router;
14pub mod error;
15pub mod exchange_impl;
16pub mod margin_impl;
17pub mod parser;
18pub mod rest;
19pub mod signed_request;
20pub mod symbol;
21pub mod ws;
22pub mod ws_exchange_impl;
23
24pub use auth::OkxAuth;
25pub use builder::OkxBuilder;
26pub use endpoint_router::{OkxChannelType, OkxEndpointRouter};
27pub use error::{OkxErrorCode, is_error_response, parse_error};
28pub use signed_request::{HttpMethod, OkxSignedRequestBuilder};
29
30#[derive(Debug)]
32pub struct Okx {
33 base: BaseExchange,
35 options: OkxOptions,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct OkxOptions {
55 pub account_mode: String,
59 #[serde(default)]
65 pub default_type: DefaultType,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub default_sub_type: Option<DefaultSubType>,
75 pub testnet: bool,
77}
78
79impl Default for OkxOptions {
80 fn default() -> Self {
81 Self {
82 account_mode: "cash".to_string(),
83 default_type: DefaultType::default(), default_sub_type: None,
85 testnet: false,
86 }
87 }
88}
89
90impl Okx {
91 pub fn builder() -> OkxBuilder {
109 OkxBuilder::new()
110 }
111
112 pub fn new(config: ExchangeConfig) -> Result<Self> {
118 let base = BaseExchange::new(config)?;
119 let options = OkxOptions::default();
120
121 Ok(Self { base, options })
122 }
123
124 pub fn new_with_options(config: ExchangeConfig, options: OkxOptions) -> Result<Self> {
133 let base = BaseExchange::new(config)?;
134 Ok(Self { base, options })
135 }
136
137 pub fn base(&self) -> &BaseExchange {
139 &self.base
140 }
141
142 pub fn base_mut(&mut self) -> &mut BaseExchange {
144 &mut self.base
145 }
146
147 pub fn options(&self) -> &OkxOptions {
149 &self.options
150 }
151
152 pub fn set_options(&mut self, options: OkxOptions) {
154 self.options = options;
155 }
156
157 pub fn id(&self) -> &'static str {
159 "okx"
160 }
161
162 pub fn name(&self) -> &'static str {
164 "OKX"
165 }
166
167 pub fn version(&self) -> &'static str {
169 "v5"
170 }
171
172 pub fn certified(&self) -> bool {
174 false
175 }
176
177 pub fn pro(&self) -> bool {
179 true
180 }
181
182 pub fn rate_limit(&self) -> u32 {
184 20
185 }
186
187 pub fn is_sandbox(&self) -> bool {
211 self.base().config.sandbox || self.options.testnet
212 }
213
214 pub fn is_testnet_trading(&self) -> bool {
244 self.base().config.sandbox || self.options.testnet
245 }
246
247 pub fn timeframes(&self) -> HashMap<String, String> {
249 let mut timeframes = HashMap::new();
250 timeframes.insert("1m".to_string(), "1m".to_string());
251 timeframes.insert("3m".to_string(), "3m".to_string());
252 timeframes.insert("5m".to_string(), "5m".to_string());
253 timeframes.insert("15m".to_string(), "15m".to_string());
254 timeframes.insert("30m".to_string(), "30m".to_string());
255 timeframes.insert("1h".to_string(), "1H".to_string());
256 timeframes.insert("2h".to_string(), "2H".to_string());
257 timeframes.insert("4h".to_string(), "4H".to_string());
258 timeframes.insert("6h".to_string(), "6Hutc".to_string());
259 timeframes.insert("12h".to_string(), "12Hutc".to_string());
260 timeframes.insert("1d".to_string(), "1Dutc".to_string());
261 timeframes.insert("1w".to_string(), "1Wutc".to_string());
262 timeframes.insert("1M".to_string(), "1Mutc".to_string());
263 timeframes
264 }
265
266 pub fn urls(&self) -> OkxUrls {
268 if self.base().config.sandbox || self.options.testnet {
269 OkxUrls::demo()
270 } else {
271 OkxUrls::production()
272 }
273 }
274
275 pub fn default_type(&self) -> DefaultType {
277 self.options.default_type
278 }
279
280 pub fn default_sub_type(&self) -> Option<DefaultSubType> {
282 self.options.default_sub_type
283 }
284
285 pub fn is_contract_type(&self) -> bool {
293 self.options.default_type.is_contract()
294 }
295
296 pub fn is_inverse(&self) -> bool {
302 matches!(self.options.default_sub_type, Some(DefaultSubType::Inverse))
303 }
304
305 pub fn is_linear(&self) -> bool {
311 !self.is_inverse()
312 }
313
314 pub fn create_ws(&self) -> ws::OkxWs {
332 let urls = self.urls();
333 ws::OkxWs::new(urls.ws_public)
334 }
335
336 pub fn create_private_ws(&self) -> ws::OkxWs {
342 let urls = self.urls();
343 ws::OkxWs::new(urls.ws_private)
344 }
345
346 pub fn signed_request(
387 &self,
388 endpoint: impl Into<String>,
389 ) -> signed_request::OkxSignedRequestBuilder<'_> {
390 signed_request::OkxSignedRequestBuilder::new(self, endpoint)
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct OkxUrls {
397 pub rest: String,
399 pub ws_public: String,
401 pub ws_private: String,
403 pub ws_business: String,
405}
406
407impl OkxUrls {
408 pub fn production() -> Self {
410 Self {
411 rest: "https://www.okx.com".to_string(),
412 ws_public: "wss://ws.okx.com:8443/ws/v5/public".to_string(),
413 ws_private: "wss://ws.okx.com:8443/ws/v5/private".to_string(),
414 ws_business: "wss://ws.okx.com:8443/ws/v5/business".to_string(),
415 }
416 }
417
418 pub fn demo() -> Self {
420 Self {
421 rest: "https://www.okx.com".to_string(),
422 ws_public: "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999".to_string(),
423 ws_private: "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999".to_string(),
424 ws_business: "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999".to_string(),
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_okx_creation() {
435 let config = ExchangeConfig {
436 id: "okx".to_string(),
437 name: "OKX".to_string(),
438 ..Default::default()
439 };
440
441 let okx = Okx::new(config);
442 assert!(okx.is_ok());
443
444 let okx = okx.unwrap();
445 assert_eq!(okx.id(), "okx");
446 assert_eq!(okx.name(), "OKX");
447 assert_eq!(okx.version(), "v5");
448 assert!(!okx.certified());
449 assert!(okx.pro());
450 }
451
452 #[test]
453 fn test_timeframes() {
454 let config = ExchangeConfig::default();
455 let okx = Okx::new(config).unwrap();
456 let timeframes = okx.timeframes();
457
458 assert!(timeframes.contains_key("1m"));
459 assert!(timeframes.contains_key("1h"));
460 assert!(timeframes.contains_key("1d"));
461 assert_eq!(timeframes.len(), 13);
462 }
463
464 #[test]
465 fn test_urls() {
466 let config = ExchangeConfig::default();
467 let okx = Okx::new(config).unwrap();
468 let urls = okx.urls();
469
470 assert!(urls.rest.contains("okx.com"));
471 assert!(urls.ws_public.contains("ws.okx.com"));
472 }
473
474 #[test]
475 fn test_sandbox_urls() {
476 let config = ExchangeConfig {
477 sandbox: true,
478 ..Default::default()
479 };
480 let okx = Okx::new(config).unwrap();
481 let urls = okx.urls();
482
483 assert!(urls.ws_public.contains("wspap.okx.com"));
484 assert!(urls.ws_public.contains("brokerId=9999"));
485 }
486
487 #[test]
488 fn test_demo_rest_url_uses_production_domain() {
489 let demo_urls = OkxUrls::demo();
492 let production_urls = OkxUrls::production();
493
494 assert_eq!(demo_urls.rest, production_urls.rest);
496 assert_eq!(demo_urls.rest, "https://www.okx.com");
497
498 assert_ne!(demo_urls.ws_public, production_urls.ws_public);
500 assert!(demo_urls.ws_public.contains("wspap.okx.com"));
501 assert!(demo_urls.ws_public.contains("brokerId=9999"));
502 }
503
504 #[test]
505 fn test_sandbox_mode_rest_url_is_production() {
506 let config = ExchangeConfig {
508 sandbox: true,
509 ..Default::default()
510 };
511 let okx = Okx::new(config).unwrap();
512 let urls = okx.urls();
513
514 assert_eq!(urls.rest, "https://www.okx.com");
516 }
517
518 #[test]
519 fn test_is_sandbox_with_config_sandbox() {
520 let config = ExchangeConfig {
521 sandbox: true,
522 ..Default::default()
523 };
524 let okx = Okx::new(config).unwrap();
525 assert!(okx.is_sandbox());
526 }
527
528 #[test]
529 fn test_is_sandbox_with_options_demo() {
530 let config = ExchangeConfig::default();
531 let options = OkxOptions {
532 testnet: true,
533 ..Default::default()
534 };
535 let okx = Okx::new_with_options(config, options).unwrap();
536 assert!(okx.is_sandbox());
537 }
538
539 #[test]
540 fn test_is_sandbox_false_by_default() {
541 let config = ExchangeConfig::default();
542 let okx = Okx::new(config).unwrap();
543 assert!(!okx.is_sandbox());
544 }
545
546 #[test]
547 fn test_is_demo_trading_with_config_sandbox() {
548 let config = ExchangeConfig {
549 sandbox: true,
550 ..Default::default()
551 };
552 let okx = Okx::new(config).unwrap();
553 assert!(okx.is_testnet_trading());
554 }
555
556 #[test]
557 fn test_is_demo_trading_with_options_demo() {
558 let config = ExchangeConfig::default();
559 let options = OkxOptions {
560 testnet: true,
561 ..Default::default()
562 };
563 let okx = Okx::new_with_options(config, options).unwrap();
564 assert!(okx.is_testnet_trading());
565 }
566
567 #[test]
568 fn test_is_demo_trading_false_by_default() {
569 let config = ExchangeConfig::default();
570 let okx = Okx::new(config).unwrap();
571 assert!(!okx.is_testnet_trading());
572 }
573
574 #[test]
575 fn test_is_demo_trading_equals_is_sandbox() {
576 let config = ExchangeConfig::default();
578 let okx = Okx::new(config).unwrap();
579 assert_eq!(okx.is_testnet_trading(), okx.is_sandbox());
580
581 let config_sandbox = ExchangeConfig {
582 sandbox: true,
583 ..Default::default()
584 };
585 let okx_sandbox = Okx::new(config_sandbox).unwrap();
586 assert_eq!(okx_sandbox.is_testnet_trading(), okx_sandbox.is_sandbox());
587 }
588
589 #[test]
590 fn test_default_options() {
591 let options = OkxOptions::default();
592 assert_eq!(options.account_mode, "cash");
593 assert_eq!(options.default_type, DefaultType::Spot);
594 assert_eq!(options.default_sub_type, None);
595 assert!(!options.testnet);
596 }
597
598 #[test]
599 fn test_okx_options_with_default_type() {
600 let options = OkxOptions {
601 default_type: DefaultType::Swap,
602 default_sub_type: Some(DefaultSubType::Linear),
603 ..Default::default()
604 };
605 assert_eq!(options.default_type, DefaultType::Swap);
606 assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
607 }
608
609 #[test]
610 fn test_okx_options_serialization() {
611 let options = OkxOptions {
612 default_type: DefaultType::Swap,
613 default_sub_type: Some(DefaultSubType::Linear),
614 ..Default::default()
615 };
616 let json = serde_json::to_string(&options).unwrap();
617 assert!(json.contains("\"default_type\":\"swap\""));
618 assert!(json.contains("\"default_sub_type\":\"linear\""));
619 }
620
621 #[test]
622 fn test_okx_options_deserialization() {
623 let json = r#"{
624 "account_mode": "cross",
625 "default_type": "swap",
626 "default_sub_type": "inverse",
627 "testnet": true
628 }"#;
629 let options: OkxOptions = serde_json::from_str(json).unwrap();
630 assert_eq!(options.account_mode, "cross");
631 assert_eq!(options.default_type, DefaultType::Swap);
632 assert_eq!(options.default_sub_type, Some(DefaultSubType::Inverse));
633 assert!(options.testnet);
634 }
635
636 #[test]
637 fn test_okx_options_deserialization_without_default_type() {
638 let json = r#"{
640 "account_mode": "cash",
641 "testnet": false
642 }"#;
643 let options: OkxOptions = serde_json::from_str(json).unwrap();
644 assert_eq!(options.default_type, DefaultType::Spot);
645 assert_eq!(options.default_sub_type, None);
646 }
647}