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