use std::sync::LazyLock;
use ahash::AHashSet;
use nautilus_model::{
enums::{OrderType, TimeInForce},
identifiers::{ClientId, Venue},
};
use ustr::Ustr;
use super::enums::{OKXBookChannel, OKXInstrumentType, OKXVipLevel};
pub const OKX: &str = "OKX";
pub static OKX_VENUE: LazyLock<Venue> = LazyLock::new(|| Venue::new(Ustr::from(OKX)));
pub static OKX_CLIENT_ID: LazyLock<ClientId> = LazyLock::new(|| ClientId::new(Ustr::from(OKX)));
pub const OKX_NAUTILUS_BROKER_ID: &str = "5328c82e5542BCDE";
pub const OKX_HTTP_URL: &str = "https://www.okx.com";
pub const OKX_WS_PUBLIC_URL: &str = "wss://ws.okx.com:8443/ws/v5/public";
pub const OKX_WS_PRIVATE_URL: &str = "wss://ws.okx.com:8443/ws/v5/private";
pub const OKX_WS_BUSINESS_URL: &str = "wss://ws.okx.com:8443/ws/v5/business";
pub const OKX_WS_DEMO_PUBLIC_URL: &str = "wss://wspap.okx.com:8443/ws/v5/public";
pub const OKX_WS_DEMO_PRIVATE_URL: &str = "wss://wspap.okx.com:8443/ws/v5/private";
pub const OKX_WS_DEMO_BUSINESS_URL: &str = "wss://wspap.okx.com:8443/ws/v5/business";
pub const OKX_WS_TOPIC_DELIMITER: char = ':';
pub const OKX_WS_HEARTBEAT_SECS: u64 = 20;
pub const OKX_SUCCESS_CODE: &str = "0";
pub const OKX_SERVICE_UPGRADE_RECONNECT_CODE: &str = "64008";
pub const OKX_FIELD_SCODE: &str = "sCode";
pub const OKX_FIELD_SMSG: &str = "sMsg";
pub const OKX_FIELD_SUBCODE: &str = "subCode";
pub const OKX_FIELD_CLORDID: &str = "clOrdId";
pub const OKX_MAX_CLORDID_LEN: usize = 32;
pub fn validate_okx_client_order_id(cl_ord_id: &str) -> Result<(), String> {
let len = cl_ord_id.len();
if len > OKX_MAX_CLORDID_LEN {
return Err(format!(
"OKX requires clOrdId to be at most {OKX_MAX_CLORDID_LEN} characters, was {len} ({cl_ord_id:?}); \
set `use_uuid_client_order_ids=True` and `use_hyphens_in_client_order_ids=False` on the strategy config"
));
}
if !cl_ord_id.bytes().all(|b| b.is_ascii_alphanumeric()) {
return Err(format!(
"OKX requires clOrdId to be alphanumeric only, was {cl_ord_id:?}; \
set `use_hyphens_in_client_order_ids=False` on the strategy config"
));
}
Ok(())
}
pub const OKX_SUPPORTED_TIME_IN_FORCE: &[TimeInForce] = &[
TimeInForce::Gtc, TimeInForce::Ioc, TimeInForce::Fok, ];
pub const OKX_SUPPORTED_ORDER_TYPES: &[OrderType] = &[
OrderType::Market,
OrderType::Limit,
OrderType::MarketToLimit, OrderType::StopMarket, OrderType::StopLimit, OrderType::MarketIfTouched, OrderType::LimitIfTouched, OrderType::TrailingStopMarket, ];
pub const OKX_CONDITIONAL_ORDER_TYPES: &[OrderType] = &[
OrderType::StopMarket,
OrderType::StopLimit,
OrderType::MarketIfTouched,
OrderType::LimitIfTouched,
OrderType::TrailingStopMarket,
];
pub const OKX_ADVANCE_ALGO_ORDER_TYPES: &[OrderType] = &[OrderType::TrailingStopMarket];
pub static OKX_RETRY_ERROR_CODES: LazyLock<AHashSet<&'static str>> = LazyLock::new(|| {
let mut codes = AHashSet::new();
codes.insert("50001"); codes.insert("50004"); codes.insert("50005"); codes.insert("50013"); codes.insert("50026");
codes.insert("50011"); codes.insert("50113");
codes.insert("60001"); codes.insert("60005"); codes.insert(OKX_SERVICE_UPGRADE_RECONNECT_CODE);
codes
});
pub fn should_retry_error_code(error_code: &str) -> bool {
OKX_RETRY_ERROR_CODES.contains(error_code)
}
pub const OKX_POST_ONLY_ERROR_CODE: &str = "51019";
pub const OKX_POST_ONLY_CANCEL_SOURCE: &str = "31";
pub const OKX_POST_ONLY_CANCEL_REASON: &str = "POST_ONLY would take liquidity";
pub const OKX_SLIPPAGE_EXCEEDED_ERROR_CODE: &str = "54084";
pub const OKX_SLIPPAGE_INVALID_ERROR_CODE: &str = "54085";
#[must_use]
pub fn is_slippage_rejection(error_code: &str) -> bool {
matches!(
error_code,
OKX_SLIPPAGE_EXCEEDED_ERROR_CODE | OKX_SLIPPAGE_INVALID_ERROR_CODE
)
}
pub const OKX_TARGET_CCY_BASE: &str = "base_ccy";
pub const OKX_TARGET_CCY_QUOTE: &str = "quote_ccy";
pub fn resolve_instrument_families(
configured: &Option<Vec<String>>,
inst_type: OKXInstrumentType,
) -> Option<Vec<String>> {
match (configured, inst_type) {
(Some(families), OKXInstrumentType::Option) => Some(families.clone()),
(
Some(families),
OKXInstrumentType::Futures | OKXInstrumentType::Swap | OKXInstrumentType::Events,
) => Some(families.clone()),
(None, OKXInstrumentType::Option) => {
log::warn!("Skipping OPTION type: instrument_families required but not configured");
None
}
_ => Some(vec![]),
}
}
pub fn resolve_book_depth(raw_depth: usize) -> usize {
match raw_depth {
0 | 400 => raw_depth,
1..=50 => 50,
_ => 400,
}
}
pub(crate) fn select_book_channel(depth: usize, vip: OKXVipLevel) -> OKXBookChannel {
match depth {
50 if vip >= OKXVipLevel::Vip4 => OKXBookChannel::Books50L2Tbt,
0 | 400 if vip >= OKXVipLevel::Vip5 => OKXBookChannel::BookL2Tbt,
0 | 50 | 400 => OKXBookChannel::Book,
_ => unreachable!("book depth must be resolved before channel selection"),
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::auto_default(0, OKXVipLevel::Vip0, OKXBookChannel::Book)]
#[case::auto_vip4(0, OKXVipLevel::Vip4, OKXBookChannel::Book)]
#[case::auto_vip5(0, OKXVipLevel::Vip5, OKXBookChannel::BookL2Tbt)]
#[case::depth_50_vip3(50, OKXVipLevel::Vip3, OKXBookChannel::Book)]
#[case::depth_50_vip4(50, OKXVipLevel::Vip4, OKXBookChannel::Books50L2Tbt)]
#[case::depth_400_vip4(400, OKXVipLevel::Vip4, OKXBookChannel::Book)]
#[case::depth_400_vip5(400, OKXVipLevel::Vip5, OKXBookChannel::BookL2Tbt)]
fn test_select_book_channel(
#[case] depth: usize,
#[case] vip: OKXVipLevel,
#[case] expected: OKXBookChannel,
) {
assert_eq!(select_book_channel(depth, vip), expected);
}
#[rstest]
#[case("54084", true)]
#[case("54085", true)]
#[case("51019", false)]
#[case("", false)]
fn test_is_slippage_rejection(#[case] code: &str, #[case] expected: bool) {
assert_eq!(is_slippage_rejection(code), expected);
}
#[rstest]
#[case("50001", true)]
#[case("60005", true)]
#[case(OKX_SERVICE_UPGRADE_RECONNECT_CODE, true)]
#[case("60012", false)]
fn test_should_retry_error_code(#[case] code: &str, #[case] expected: bool) {
assert_eq!(should_retry_error_code(code), expected);
}
#[rstest]
#[case("O20260101000000ABC1", true)]
#[case("aB9", true)]
#[case("abcdefghij0123456789ABCDEFGHIJ12", true)] #[case("abcdefghij0123456789ABCDEFGHIJ123", false)] #[case("O-20260101-000000-001-001-1", false)] #[case("O_20260101_000000", false)] #[case("", true)] fn test_validate_okx_client_order_id(#[case] cl_ord_id: &str, #[case] expected_ok: bool) {
assert_eq!(validate_okx_client_order_id(cl_ord_id).is_ok(), expected_ok);
}
#[rstest]
fn test_validate_okx_client_order_id_length_message() {
let cl_ord_id = "O20260522145501532392555aceLTCUSDT5";
let err = validate_okx_client_order_id(cl_ord_id).unwrap_err();
assert!(err.contains("at most 32"));
assert!(err.contains("was 35"));
assert!(err.contains("use_uuid_client_order_ids"));
}
}