Skip to content

Commit b36dce9

Browse files
committed
Standardize OKX execution flows and enum conversions
1 parent 8372772 commit b36dce9

File tree

10 files changed

+85
-112
lines changed

10 files changed

+85
-112
lines changed

crates/adapters/okx/src/common/enums.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use nautilus_model::enums::{
1717
AggressorSide, LiquiditySide, OptionKind, OrderSide, OrderStatus, OrderType, PositionSide,
18+
TriggerType,
1819
};
1920
use serde::{Deserialize, Serialize};
2021
use strum::{AsRefStr, Display, EnumIter, EnumString};
@@ -554,6 +555,17 @@ pub enum OKXTriggerType {
554555
Mark,
555556
}
556557

558+
impl From<TriggerType> for OKXTriggerType {
559+
fn from(value: TriggerType) -> Self {
560+
match value {
561+
TriggerType::LastPrice => Self::Last,
562+
TriggerType::MarkPrice => Self::Mark,
563+
TriggerType::IndexPrice => Self::Index,
564+
_ => Self::Last,
565+
}
566+
}
567+
}
568+
557569
/// Represents OKX VIP level tiers for trading fee structure and API limits.
558570
///
559571
/// VIP levels determine:

crates/adapters/okx/src/common/parse.rs

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -414,26 +414,9 @@ pub fn parse_order_status_report(
414414
.map(|v| Quantity::new(v, size_precision))
415415
.unwrap_or_default();
416416
let order_side: OrderSide = order.side.into();
417-
let okx_status: OKXOrderStatus = match order.state.as_str() {
418-
"live" => OKXOrderStatus::Live,
419-
"partially_filled" => OKXOrderStatus::PartiallyFilled,
420-
"filled" => OKXOrderStatus::Filled,
421-
"canceled" => OKXOrderStatus::Canceled,
422-
"mmp_canceled" => OKXOrderStatus::MmpCanceled,
423-
_ => OKXOrderStatus::Live, // Default fallback
424-
};
417+
let okx_status: OKXOrderStatus = order.state;
425418
let order_status: OrderStatus = okx_status.into();
426-
let okx_ord_type: OKXOrderType = match order.ord_type.as_str() {
427-
"market" => OKXOrderType::Market,
428-
"limit" => OKXOrderType::Limit,
429-
"post_only" => OKXOrderType::PostOnly,
430-
"fok" => OKXOrderType::Fok,
431-
"ioc" => OKXOrderType::Ioc,
432-
"optimal_limit_ioc" => OKXOrderType::OptimalLimitIoc,
433-
"mmp" => OKXOrderType::Mmp,
434-
"mmp_and_post_only" => OKXOrderType::MmpAndPostOnly,
435-
_ => OKXOrderType::Limit, // Default fallback
436-
};
419+
let okx_ord_type: OKXOrderType = order.ord_type;
437420
let order_type: OrderType = okx_ord_type.into();
438421
// Note: OKX uses ordType for type and liquidity instructions; time-in-force not explicitly represented here
439422
let time_in_force = TimeInForce::Gtc;
@@ -476,7 +459,7 @@ pub fn parse_order_status_report(
476459
{
477460
report = report.with_avg_px(avg);
478461
}
479-
if order.ord_type == "post_only" {
462+
if order.ord_type == OKXOrderType::PostOnly {
480463
report = report.with_post_only(true);
481464
}
482465
if order.reduce_only == "true" {

crates/adapters/okx/src/http/client.rs

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ use crate::{
8585
common::{
8686
consts::{OKX_HTTP_URL, OKX_NAUTILUS_BROKER_ID, should_retry_error_code},
8787
credential::Credential,
88-
enums::{OKXInstrumentType, OKXPositionMode, OKXTradeMode},
88+
enums::{
89+
OKXAlgoOrderType, OKXInstrumentType, OKXPositionMode, OKXSide, OKXTradeMode,
90+
OKXTriggerType,
91+
},
8992
models::OKXInstrument,
9093
parse::{
9194
okx_instrument_type, parse_account_state, parse_candlestick, parse_fill_report,
@@ -2063,28 +2066,15 @@ impl OKXHttpClient {
20632066
limit_price: Option<Price>,
20642067
reduce_only: Option<bool>,
20652068
) -> Result<OKXPlaceAlgoOrderResponse, OKXHttpError> {
2066-
// Map order side to OKX format
2067-
let side_str = match order_side {
2068-
OrderSide::Buy => "buy",
2069-
OrderSide::Sell => "sell",
2070-
_ => {
2071-
return Err(OKXHttpError::ValidationError(
2072-
"Invalid order side".to_string(),
2073-
));
2074-
}
2075-
};
2069+
if !matches!(order_side, OrderSide::Buy | OrderSide::Sell) {
2070+
return Err(OKXHttpError::ValidationError(
2071+
"Invalid order side".to_string(),
2072+
));
2073+
}
2074+
let okx_side: OKXSide = order_side.into();
20762075

20772076
// Map trigger type to OKX format
2078-
let trigger_px_type_str = if let Some(trigger) = trigger_type {
2079-
match trigger {
2080-
TriggerType::LastPrice => "last",
2081-
TriggerType::MarkPrice => "mark",
2082-
TriggerType::IndexPrice => "index",
2083-
_ => "last", // Default to last for unsupported types
2084-
}
2085-
} else {
2086-
"last" // Default
2087-
};
2077+
let trigger_px_type_enum = trigger_type.map(Into::into).unwrap_or(OKXTriggerType::Last);
20882078

20892079
// Determine order price based on order type
20902080
let order_px = if matches!(order_type, OrderType::StopLimit | OrderType::LimitIfTouched) {
@@ -2096,14 +2086,14 @@ impl OKXHttpClient {
20962086

20972087
let request = OKXPlaceAlgoOrderRequest {
20982088
inst_id: instrument_id.symbol.as_str().to_string(),
2099-
td_mode: td_mode.to_string().to_lowercase(),
2100-
side: side_str.to_string(),
2101-
ord_type: "trigger".to_string(), // All conditional orders use 'trigger' type
2089+
td_mode,
2090+
side: okx_side,
2091+
ord_type: OKXAlgoOrderType::Trigger, // All conditional orders use 'trigger' type
21022092
sz: quantity.to_string(),
21032093
algo_cl_ord_id: Some(client_order_id.as_str().to_string()),
21042094
trigger_px: Some(trigger_price.to_string()),
21052095
order_px,
2106-
trigger_px_type: Some(trigger_px_type_str.to_string()),
2096+
trigger_px_type: Some(trigger_px_type_enum),
21072097
tgt_ccy: None, // Let OKX determine based on instrument
21082098
pos_side: None, // Use default position side
21092099
close_position: None,

crates/adapters/okx/src/http/models.rs

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ pub struct OKXCandlestick(
6262
);
6363

6464
use crate::common::{
65-
enums::{OKXExecType, OKXInstrumentType, OKXMarginMode, OKXPositionSide, OKXSide},
65+
enums::{
66+
OKXAlgoOrderType, OKXExecType, OKXInstrumentType, OKXMarginMode, OKXOrderStatus,
67+
OKXOrderType, OKXPositionSide, OKXSide, OKXTradeMode, OKXTriggerType,
68+
},
6669
parse::deserialize_string_to_u64,
6770
};
6871

@@ -372,13 +375,12 @@ pub struct OKXPlaceOrderResponse {
372375
pub side: Option<OKXSide>,
373376
/// Order type (optional).
374377
#[serde(default)]
375-
pub ord_type: Option<String>,
378+
pub ord_type: Option<OKXOrderType>,
376379
/// Order size (optional).
377380
#[serde(default)]
378381
pub sz: Option<String>,
379382
/// Order state (optional).
380-
#[serde(default)]
381-
pub state: Option<String>,
383+
pub state: Option<OKXOrderStatus>,
382384
/// Price (optional).
383385
#[serde(default)]
384386
pub px: Option<String>,
@@ -446,7 +448,7 @@ pub struct OKXOrderHistory {
446448
/// Instrument ID.
447449
pub inst_id: Ustr,
448450
/// Order type.
449-
pub ord_type: String,
451+
pub ord_type: OKXOrderType,
450452
/// Order size.
451453
pub sz: String,
452454
/// Price (optional).
@@ -456,13 +458,13 @@ pub struct OKXOrderHistory {
456458
/// Position side.
457459
pub pos_side: OKXPositionSide,
458460
/// Trade mode.
459-
pub td_mode: String,
461+
pub td_mode: OKXTradeMode,
460462
/// Reduce-only flag.
461463
pub reduce_only: String,
462464
/// Target currency (optional).
463465
pub tgt_ccy: String,
464466
/// Order state.
465-
pub state: String,
467+
pub state: OKXOrderStatus,
466468
/// Average price (optional).
467469
pub avg_px: String,
468470
/// Execution fee.
@@ -617,12 +619,12 @@ pub struct OKXPlaceAlgoOrderRequest {
617619
pub inst_id: String,
618620
/// Trade mode (isolated, cross, cash).
619621
#[serde(rename = "tdMode")]
620-
pub td_mode: String,
622+
pub td_mode: OKXTradeMode,
621623
/// Order side (buy, sell).
622-
pub side: String,
624+
pub side: OKXSide,
623625
/// Algo order type (trigger).
624626
#[serde(rename = "ordType")]
625-
pub ord_type: String,
627+
pub ord_type: OKXAlgoOrderType,
626628
/// Order size.
627629
pub sz: String,
628630
/// Client-supplied algo order ID.
@@ -636,13 +638,13 @@ pub struct OKXPlaceAlgoOrderRequest {
636638
pub order_px: Option<String>,
637639
/// Trigger type (last, mark, index).
638640
#[serde(rename = "triggerPxType", skip_serializing_if = "Option::is_none")]
639-
pub trigger_px_type: Option<String>,
641+
pub trigger_px_type: Option<OKXTriggerType>,
640642
/// Target currency (base_ccy or quote_ccy).
641643
#[serde(rename = "tgtCcy", skip_serializing_if = "Option::is_none")]
642644
pub tgt_ccy: Option<String>,
643645
/// Position side (net, long, short).
644646
#[serde(rename = "posSide", skip_serializing_if = "Option::is_none")]
645-
pub pos_side: Option<String>,
647+
pub pos_side: Option<OKXPositionSide>,
646648
/// Whether to close position.
647649
#[serde(rename = "closePosition", skip_serializing_if = "Option::is_none")]
648650
pub close_position: Option<bool>,
@@ -717,14 +719,14 @@ mod tests {
717719
fn test_algo_order_request_serialization() {
718720
let request = OKXPlaceAlgoOrderRequest {
719721
inst_id: "ETH-USDT-SWAP".to_string(),
720-
td_mode: "isolated".to_string(),
721-
side: "buy".to_string(),
722-
ord_type: "trigger".to_string(),
722+
td_mode: OKXTradeMode::Isolated,
723+
side: OKXSide::Buy,
724+
ord_type: OKXAlgoOrderType::Trigger,
723725
sz: "0.01".to_string(),
724726
algo_cl_ord_id: Some("test123".to_string()),
725727
trigger_px: Some("3000".to_string()),
726728
order_px: Some("-1".to_string()),
727-
trigger_px_type: Some("last".to_string()),
729+
trigger_px_type: Some(OKXTriggerType::Last),
728730
tgt_ccy: None,
729731
pos_side: None,
730732
close_position: None,
@@ -753,16 +755,16 @@ mod tests {
753755
fn test_algo_order_request_array_serialization() {
754756
let request = OKXPlaceAlgoOrderRequest {
755757
inst_id: "BTC-USDT".to_string(),
756-
td_mode: "cross".to_string(),
757-
side: "sell".to_string(),
758-
ord_type: "trigger".to_string(),
758+
td_mode: OKXTradeMode::Cross,
759+
side: OKXSide::Sell,
760+
ord_type: OKXAlgoOrderType::Trigger,
759761
sz: "0.1".to_string(),
760762
algo_cl_ord_id: None,
761763
trigger_px: Some("50000".to_string()),
762764
order_px: Some("49900".to_string()),
763-
trigger_px_type: Some("mark".to_string()),
765+
trigger_px_type: Some(OKXTriggerType::Mark),
764766
tgt_ccy: Some("base_ccy".to_string()),
765-
pos_side: Some("net".to_string()),
767+
pos_side: Some(OKXPositionSide::Net),
766768
close_position: None,
767769
tag: None,
768770
reduce_only: Some(true),

crates/adapters/okx/src/http/parse.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ mod tests {
2020

2121
use crate::{
2222
common::{
23-
enums::{OKXExecType, OKXInstrumentType, OKXMarginMode, OKXPositionSide, OKXSide},
23+
enums::{
24+
OKXExecType, OKXInstrumentType, OKXMarginMode, OKXOrderStatus, OKXPositionSide,
25+
OKXSide,
26+
},
2427
testing::load_test_json,
2528
},
2629
http::{
@@ -227,7 +230,7 @@ mod tests {
227230
assert_eq!(order.ord_id, "2497956918703120384");
228231
assert_eq!(order.fill_sz, "0.03");
229232
assert_eq!(order.acc_fill_sz, "0.03");
230-
assert_eq!(order.state, "filled");
233+
assert_eq!(order.state, OKXOrderStatus::Filled);
231234
// fill_fee was omitted in response
232235
assert!(order.fill_fee.is_none());
233236
}

crates/adapters/okx/src/http/query.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ use serde::{self, Deserialize, Serialize};
4343

4444
use crate::{
4545
common::enums::{
46-
OKXInstrumentType, OKXOrderStatus, OKXPositionMode, OKXPositionSide, OKXTradeMode,
46+
OKXInstrumentType, OKXOrderStatus, OKXOrderType, OKXPositionMode, OKXPositionSide,
47+
OKXTradeMode,
4748
},
4849
http::error::BuildError,
4950
};
@@ -315,7 +316,7 @@ pub struct GetOrderHistoryParams {
315316
pub inst_id: Option<String>,
316317
/// Order type: limit, market, post_only, fok, ioc (optional).
317318
#[serde(skip_serializing_if = "Option::is_none")]
318-
pub ord_type: Option<String>,
319+
pub ord_type: Option<OKXOrderType>,
319320
/// Order state: live, filled, canceled (optional).
320321
#[serde(skip_serializing_if = "Option::is_none")]
321322
pub state: Option<String>,

crates/adapters/okx/src/python/websocket.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ impl OKXWebSocketClient {
265265
call_python(py, &callback, py_obj);
266266
}),
267267
NautilusWsMessage::AccountUpdate(msg) => {
268-
call_python_with_data(&callback, |py| msg.py_to_dict(py));
268+
call_python_with_data(&callback, |py| msg.into_py_any(py));
269269
}
270270
NautilusWsMessage::Reconnected => {} // Nothing to handle
271271
NautilusWsMessage::Error(msg) => {

crates/adapters/okx/src/websocket/client.rs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ use crate::{
8282
},
8383
credential::Credential,
8484
enums::{
85-
OKXInstrumentType, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXSide,
86-
OKXTradeMode, OKXTriggerType, conditional_order_to_algo_type, is_conditional_order,
85+
OKXInstrumentType, OKXOrderStatus, OKXOrderType, OKXPositionSide, OKXTradeMode,
86+
OKXTriggerType, conditional_order_to_algo_type, is_conditional_order,
8787
},
8888
parse::{
8989
bar_spec_as_okx_channel, okx_instrument_type, parse_account_state,
@@ -1927,7 +1927,7 @@ impl OKXWebSocketClient {
19271927
}
19281928
// If is_quote_quantity is false, we don't set tgtCcy (defaults to base currency)
19291929

1930-
builder.side(OKXSide::from(order_side));
1930+
builder.side(order_side);
19311931

19321932
if let Some(pos_side) = position_side {
19331933
builder.pos_side(pos_side);
@@ -2206,7 +2206,7 @@ impl OKXWebSocketClient {
22062206
builder.inst_id(inst_id.symbol.inner());
22072207
builder.td_mode(td_mode);
22082208
builder.cl_ord_id(cl_ord_id.as_str());
2209-
builder.side(OKXSide::from(ord_side));
2209+
builder.side(ord_side);
22102210

22112211
if let Some(ps) = pos_side {
22122212
builder.pos_side(OKXPositionSide::from(ps));
@@ -2389,10 +2389,16 @@ impl OKXWebSocketClient {
23892389
}
23902390

23912391
let mut builder = WsPostAlgoOrderParamsBuilder::default();
2392+
if !matches!(order_side, OrderSide::Buy | OrderSide::Sell) {
2393+
return Err(OKXWsError::ClientError(
2394+
"Invalid order side for OKX".to_string(),
2395+
));
2396+
}
2397+
23922398
builder.inst_id(instrument_id.symbol.inner());
23932399
builder.td_mode(td_mode);
23942400
builder.cl_ord_id(client_order_id.as_str());
2395-
builder.side(OKXSide::from(order_side));
2401+
builder.side(order_side);
23962402
builder.ord_type(
23972403
conditional_order_to_algo_type(order_type)
23982404
.map_err(|e| OKXWsError::ClientError(e.to_string()))?,
@@ -2401,16 +2407,7 @@ impl OKXWebSocketClient {
24012407
builder.trigger_px(trigger_price.to_string());
24022408

24032409
// Map Nautilus TriggerType to OKX trigger type
2404-
let okx_trigger_type = if let Some(trigger) = trigger_type {
2405-
match trigger {
2406-
TriggerType::LastPrice => OKXTriggerType::Last,
2407-
TriggerType::MarkPrice => OKXTriggerType::Mark,
2408-
TriggerType::IndexPrice => OKXTriggerType::Index,
2409-
_ => OKXTriggerType::Last, // Default to Last for unsupported types
2410-
}
2411-
} else {
2412-
OKXTriggerType::Last // Default
2413-
};
2410+
let okx_trigger_type = trigger_type.map(Into::into).unwrap_or(OKXTriggerType::Last);
24142411
builder.trigger_px_type(okx_trigger_type);
24152412

24162413
// For stop-limit orders, set the limit price
@@ -3389,7 +3386,7 @@ mod tests {
33893386
use rstest::rstest;
33903387

33913388
use super::*;
3392-
use crate::common::enums::OKXExecType;
3389+
use crate::common::enums::{OKXExecType, OKXSide};
33933390

33943391
#[rstest]
33953392
fn test_timestamp_format_for_websocket_auth() {

crates/adapters/okx/src/websocket/parse.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -611,14 +611,7 @@ pub fn parse_algo_order_status_report(
611611
OrderType::StopLimit
612612
};
613613

614-
let status = match msg.state {
615-
OKXOrderStatus::Live => OrderStatus::Accepted,
616-
OKXOrderStatus::Canceled => OrderStatus::Canceled,
617-
OKXOrderStatus::MmpCanceled => OrderStatus::Canceled,
618-
OKXOrderStatus::Filled => OrderStatus::Filled,
619-
OKXOrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
620-
OKXOrderStatus::OrderPlaced => OrderStatus::Triggered,
621-
};
614+
let status: OrderStatus = msg.state.into();
622615

623616
let quantity = parse_quantity(msg.sz.as_str(), instrument.size_precision())?;
624617

0 commit comments

Comments
 (0)