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 error;
14pub mod exchange_impl;
15pub mod parser;
16pub mod rest;
17pub mod symbol;
18pub mod ws;
19pub mod ws_exchange_impl;
20
21pub use auth::OkxAuth;
22pub use builder::OkxBuilder;
23pub use error::{OkxErrorCode, is_error_response, parse_error};
24
25#[derive(Debug)]
27pub struct Okx {
28 base: BaseExchange,
30 options: OkxOptions,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct OkxOptions {
50 pub account_mode: String,
54 #[serde(default)]
60 pub default_type: DefaultType,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub default_sub_type: Option<DefaultSubType>,
70 pub testnet: bool,
72}
73
74impl Default for OkxOptions {
75 fn default() -> Self {
76 Self {
77 account_mode: "cash".to_string(),
78 default_type: DefaultType::default(), default_sub_type: None,
80 testnet: false,
81 }
82 }
83}
84
85impl Okx {
86 pub fn builder() -> OkxBuilder {
104 OkxBuilder::new()
105 }
106
107 pub fn new(config: ExchangeConfig) -> Result<Self> {
113 let base = BaseExchange::new(config)?;
114 let options = OkxOptions::default();
115
116 Ok(Self { base, options })
117 }
118
119 pub fn new_with_options(config: ExchangeConfig, options: OkxOptions) -> Result<Self> {
128 let base = BaseExchange::new(config)?;
129 Ok(Self { base, options })
130 }
131
132 pub fn base(&self) -> &BaseExchange {
134 &self.base
135 }
136
137 pub fn base_mut(&mut self) -> &mut BaseExchange {
139 &mut self.base
140 }
141
142 pub fn options(&self) -> &OkxOptions {
144 &self.options
145 }
146
147 pub fn set_options(&mut self, options: OkxOptions) {
149 self.options = options;
150 }
151
152 pub fn id(&self) -> &str {
154 "okx"
155 }
156
157 pub fn name(&self) -> &str {
159 "OKX"
160 }
161
162 pub fn version(&self) -> &str {
164 "v5"
165 }
166
167 pub fn certified(&self) -> bool {
169 false
170 }
171
172 pub fn pro(&self) -> bool {
174 true
175 }
176
177 pub fn rate_limit(&self) -> u32 {
179 20
180 }
181
182 pub fn is_sandbox(&self) -> bool {
206 self.base().config.sandbox || self.options.testnet
207 }
208
209 pub fn is_testnet_trading(&self) -> bool {
239 self.base().config.sandbox || self.options.testnet
240 }
241
242 pub fn timeframes(&self) -> HashMap<String, String> {
244 let mut timeframes = HashMap::new();
245 timeframes.insert("1m".to_string(), "1m".to_string());
246 timeframes.insert("3m".to_string(), "3m".to_string());
247 timeframes.insert("5m".to_string(), "5m".to_string());
248 timeframes.insert("15m".to_string(), "15m".to_string());
249 timeframes.insert("30m".to_string(), "30m".to_string());
250 timeframes.insert("1h".to_string(), "1H".to_string());
251 timeframes.insert("2h".to_string(), "2H".to_string());
252 timeframes.insert("4h".to_string(), "4H".to_string());
253 timeframes.insert("6h".to_string(), "6Hutc".to_string());
254 timeframes.insert("12h".to_string(), "12Hutc".to_string());
255 timeframes.insert("1d".to_string(), "1Dutc".to_string());
256 timeframes.insert("1w".to_string(), "1Wutc".to_string());
257 timeframes.insert("1M".to_string(), "1Mutc".to_string());
258 timeframes
259 }
260
261 pub fn urls(&self) -> OkxUrls {
263 if self.base().config.sandbox || self.options.testnet {
264 OkxUrls::demo()
265 } else {
266 OkxUrls::production()
267 }
268 }
269
270 pub fn default_type(&self) -> DefaultType {
272 self.options.default_type
273 }
274
275 pub fn default_sub_type(&self) -> Option<DefaultSubType> {
277 self.options.default_sub_type
278 }
279
280 pub fn is_contract_type(&self) -> bool {
288 self.options.default_type.is_contract()
289 }
290
291 pub fn is_inverse(&self) -> bool {
297 matches!(self.options.default_sub_type, Some(DefaultSubType::Inverse))
298 }
299
300 pub fn is_linear(&self) -> bool {
306 !self.is_inverse()
307 }
308
309 pub fn create_ws(&self) -> ws::OkxWs {
327 let urls = self.urls();
328 ws::OkxWs::new(urls.ws_public)
329 }
330}
331
332#[derive(Debug, Clone)]
334pub struct OkxUrls {
335 pub rest: String,
337 pub ws_public: String,
339 pub ws_private: String,
341 pub ws_business: String,
343}
344
345impl OkxUrls {
346 pub fn production() -> Self {
348 Self {
349 rest: "https://www.okx.com".to_string(),
350 ws_public: "wss://ws.okx.com:8443/ws/v5/public".to_string(),
351 ws_private: "wss://ws.okx.com:8443/ws/v5/private".to_string(),
352 ws_business: "wss://ws.okx.com:8443/ws/v5/business".to_string(),
353 }
354 }
355
356 pub fn demo() -> Self {
358 Self {
359 rest: "https://www.okx.com".to_string(),
360 ws_public: "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999".to_string(),
361 ws_private: "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999".to_string(),
362 ws_business: "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999".to_string(),
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn test_okx_creation() {
373 let config = ExchangeConfig {
374 id: "okx".to_string(),
375 name: "OKX".to_string(),
376 ..Default::default()
377 };
378
379 let okx = Okx::new(config);
380 assert!(okx.is_ok());
381
382 let okx = okx.unwrap();
383 assert_eq!(okx.id(), "okx");
384 assert_eq!(okx.name(), "OKX");
385 assert_eq!(okx.version(), "v5");
386 assert!(!okx.certified());
387 assert!(okx.pro());
388 }
389
390 #[test]
391 fn test_timeframes() {
392 let config = ExchangeConfig::default();
393 let okx = Okx::new(config).unwrap();
394 let timeframes = okx.timeframes();
395
396 assert!(timeframes.contains_key("1m"));
397 assert!(timeframes.contains_key("1h"));
398 assert!(timeframes.contains_key("1d"));
399 assert_eq!(timeframes.len(), 13);
400 }
401
402 #[test]
403 fn test_urls() {
404 let config = ExchangeConfig::default();
405 let okx = Okx::new(config).unwrap();
406 let urls = okx.urls();
407
408 assert!(urls.rest.contains("okx.com"));
409 assert!(urls.ws_public.contains("ws.okx.com"));
410 }
411
412 #[test]
413 fn test_sandbox_urls() {
414 let config = ExchangeConfig {
415 sandbox: true,
416 ..Default::default()
417 };
418 let okx = Okx::new(config).unwrap();
419 let urls = okx.urls();
420
421 assert!(urls.ws_public.contains("wspap.okx.com"));
422 assert!(urls.ws_public.contains("brokerId=9999"));
423 }
424
425 #[test]
426 fn test_demo_rest_url_uses_production_domain() {
427 let demo_urls = OkxUrls::demo();
430 let production_urls = OkxUrls::production();
431
432 assert_eq!(demo_urls.rest, production_urls.rest);
434 assert_eq!(demo_urls.rest, "https://www.okx.com");
435
436 assert_ne!(demo_urls.ws_public, production_urls.ws_public);
438 assert!(demo_urls.ws_public.contains("wspap.okx.com"));
439 assert!(demo_urls.ws_public.contains("brokerId=9999"));
440 }
441
442 #[test]
443 fn test_sandbox_mode_rest_url_is_production() {
444 let config = ExchangeConfig {
446 sandbox: true,
447 ..Default::default()
448 };
449 let okx = Okx::new(config).unwrap();
450 let urls = okx.urls();
451
452 assert_eq!(urls.rest, "https://www.okx.com");
454 }
455
456 #[test]
457 fn test_is_sandbox_with_config_sandbox() {
458 let config = ExchangeConfig {
459 sandbox: true,
460 ..Default::default()
461 };
462 let okx = Okx::new(config).unwrap();
463 assert!(okx.is_sandbox());
464 }
465
466 #[test]
467 fn test_is_sandbox_with_options_demo() {
468 let config = ExchangeConfig::default();
469 let options = OkxOptions {
470 testnet: true,
471 ..Default::default()
472 };
473 let okx = Okx::new_with_options(config, options).unwrap();
474 assert!(okx.is_sandbox());
475 }
476
477 #[test]
478 fn test_is_sandbox_false_by_default() {
479 let config = ExchangeConfig::default();
480 let okx = Okx::new(config).unwrap();
481 assert!(!okx.is_sandbox());
482 }
483
484 #[test]
485 fn test_is_demo_trading_with_config_sandbox() {
486 let config = ExchangeConfig {
487 sandbox: true,
488 ..Default::default()
489 };
490 let okx = Okx::new(config).unwrap();
491 assert!(okx.is_testnet_trading());
492 }
493
494 #[test]
495 fn test_is_demo_trading_with_options_demo() {
496 let config = ExchangeConfig::default();
497 let options = OkxOptions {
498 testnet: true,
499 ..Default::default()
500 };
501 let okx = Okx::new_with_options(config, options).unwrap();
502 assert!(okx.is_testnet_trading());
503 }
504
505 #[test]
506 fn test_is_demo_trading_false_by_default() {
507 let config = ExchangeConfig::default();
508 let okx = Okx::new(config).unwrap();
509 assert!(!okx.is_testnet_trading());
510 }
511
512 #[test]
513 fn test_is_demo_trading_equals_is_sandbox() {
514 let config = ExchangeConfig::default();
516 let okx = Okx::new(config).unwrap();
517 assert_eq!(okx.is_testnet_trading(), okx.is_sandbox());
518
519 let config_sandbox = ExchangeConfig {
520 sandbox: true,
521 ..Default::default()
522 };
523 let okx_sandbox = Okx::new(config_sandbox).unwrap();
524 assert_eq!(okx_sandbox.is_testnet_trading(), okx_sandbox.is_sandbox());
525 }
526
527 #[test]
528 fn test_default_options() {
529 let options = OkxOptions::default();
530 assert_eq!(options.account_mode, "cash");
531 assert_eq!(options.default_type, DefaultType::Spot);
532 assert_eq!(options.default_sub_type, None);
533 assert!(!options.testnet);
534 }
535
536 #[test]
537 fn test_okx_options_with_default_type() {
538 let options = OkxOptions {
539 default_type: DefaultType::Swap,
540 default_sub_type: Some(DefaultSubType::Linear),
541 ..Default::default()
542 };
543 assert_eq!(options.default_type, DefaultType::Swap);
544 assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
545 }
546
547 #[test]
548 fn test_okx_options_serialization() {
549 let options = OkxOptions {
550 default_type: DefaultType::Swap,
551 default_sub_type: Some(DefaultSubType::Linear),
552 ..Default::default()
553 };
554 let json = serde_json::to_string(&options).unwrap();
555 assert!(json.contains("\"default_type\":\"swap\""));
556 assert!(json.contains("\"default_sub_type\":\"linear\""));
557 }
558
559 #[test]
560 fn test_okx_options_deserialization() {
561 let json = r#"{
562 "account_mode": "cross",
563 "default_type": "swap",
564 "default_sub_type": "inverse",
565 "testnet": true
566 }"#;
567 let options: OkxOptions = serde_json::from_str(json).unwrap();
568 assert_eq!(options.account_mode, "cross");
569 assert_eq!(options.default_type, DefaultType::Swap);
570 assert_eq!(options.default_sub_type, Some(DefaultSubType::Inverse));
571 assert!(options.testnet);
572 }
573
574 #[test]
575 fn test_okx_options_deserialization_without_default_type() {
576 let json = r#"{
578 "account_mode": "cash",
579 "testnet": false
580 }"#;
581 let options: OkxOptions = serde_json::from_str(json).unwrap();
582 assert_eq!(options.default_type, DefaultType::Spot);
583 assert_eq!(options.default_sub_type, None);
584 }
585}