Skip to content

Commit 45bf0d7

Browse files
committed
Improve OKX margin mode setup
- Fix use of `tgtCcy` for spot margin trading - Add robust error handling for empty/invalid currency codes in balances
1 parent 6356593 commit 45bf0d7

File tree

5 files changed

+76
-15
lines changed

5 files changed

+76
-15
lines changed

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,19 +1378,58 @@ pub fn parse_account_state(
13781378
let mut balances = Vec::new();
13791379
for b in &okx_account.details {
13801380
// Skip balances with empty or whitespace-only currency codes
1381-
let ccy_str = b.ccy.trim();
1381+
let ccy_str = b.ccy.as_str().trim();
13821382
if ccy_str.is_empty() {
1383-
tracing::warn!(
1383+
tracing::debug!(
13841384
"Skipping balance detail with empty currency code (cash_bal={}, avail_bal={})",
13851385
b.cash_bal,
13861386
b.avail_bal
13871387
);
13881388
continue;
13891389
}
13901390

1391-
let currency = Currency::from(ccy_str);
1392-
let total = Money::new(b.cash_bal.parse::<f64>()?, currency);
1393-
let free = Money::new(b.avail_bal.parse::<f64>()?, currency);
1391+
// Attempt to parse the currency, skip if invalid
1392+
let currency = match Currency::from_str(ccy_str) {
1393+
Ok(c) => c,
1394+
Err(e) => {
1395+
tracing::warn!(
1396+
"Skipping balance detail with invalid currency code '{}' (cash_bal={}, avail_bal={}): {}",
1397+
ccy_str,
1398+
b.cash_bal,
1399+
b.avail_bal,
1400+
e
1401+
);
1402+
continue;
1403+
}
1404+
};
1405+
1406+
// Parse balance values, skip if invalid
1407+
let total = match b.cash_bal.parse::<f64>() {
1408+
Ok(v) => Money::new(v, currency),
1409+
Err(e) => {
1410+
tracing::warn!(
1411+
"Skipping balance detail for {} with invalid cash_bal '{}': {}",
1412+
ccy_str,
1413+
b.cash_bal,
1414+
e
1415+
);
1416+
continue;
1417+
}
1418+
};
1419+
1420+
let free = match b.avail_bal.parse::<f64>() {
1421+
Ok(v) => Money::new(v, currency),
1422+
Err(e) => {
1423+
tracing::warn!(
1424+
"Skipping balance detail for {} with invalid avail_bal '{}': {}",
1425+
ccy_str,
1426+
b.avail_bal,
1427+
e
1428+
);
1429+
continue;
1430+
}
1431+
};
1432+
13941433
let locked = total - free;
13951434
let balance = AccountBalance::new(total, locked, free);
13961435
balances.push(balance);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2377,7 +2377,11 @@ impl OKXWebSocketClient {
23772377
// OKX API default behavior for SPOT market orders:
23782378
// - BUY orders default to tgtCcy=quote_ccy (sz represents quote currency amount)
23792379
// - SELL orders default to tgtCcy=base_ccy (sz represents base currency amount)
2380-
if instrument_type == OKXInstrumentType::Spot && order_type == OrderType::Market {
2380+
// Note: tgtCcy is only supported for cash (non-margin) trading
2381+
if instrument_type == OKXInstrumentType::Spot
2382+
&& order_type == OrderType::Market
2383+
&& td_mode == OKXTradeMode::Cash
2384+
{
23812385
match quote_quantity {
23822386
Some(true) => {
23832387
// Explicitly request quote currency sizing

examples/live/okx/okx_exec_tester.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from nautilus_trader.config import TradingNodeConfig
2828
from nautilus_trader.core.nautilus_pyo3 import OKXContractType
2929
from nautilus_trader.core.nautilus_pyo3 import OKXInstrumentType
30+
from nautilus_trader.core.nautilus_pyo3 import OKXMarginMode
3031
from nautilus_trader.live.config import LiveRiskEngineConfig
3132
from nautilus_trader.live.node import TradingNode
3233
from nautilus_trader.model.enums import TimeInForce
@@ -162,7 +163,7 @@
162163
),
163164
instrument_types=instrument_types,
164165
contract_types=contract_types,
165-
# margin_mode=OKXMarginMode.ISOLATED,
166+
margin_mode=OKXMarginMode.CROSS,
166167
use_spot_margin=False,
167168
# use_mm_mass_cancel=True,
168169
is_demo=False, # If client uses the demo API

nautilus_trader/adapters/okx/config.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,15 @@ class OKXExecClientConfig(LiveExecClientConfig, frozen=True):
109109
is_demo : bool, default False
110110
If the client is connecting to the OKX demo API.
111111
margin_mode : OKXMarginMode, optional
112-
The intended OKX account margin mode for derivatives trading (SWAP/FUTURES/OPTIONS).
113-
- `ISOLATED`: Margin isolated to specific positions (default for derivatives)
114-
- `CROSS`: Margin shared across all positions
115-
Not applicable for SPOT trading (see `use_spot_margin` instead).
112+
The intended OKX account margin mode.
113+
- `ISOLATED`: Margin isolated to specific positions (default)
114+
- `CROSS`: Margin shared across all positions (enables cross margin for SPOT and derivatives)
115+
When combined with `use_spot_margin=True`, this determines the margin mode for SPOT trading.
116116
use_spot_margin : bool, default False
117-
If True, enables margin/leverage for SPOT trading (uses 'spot_isolated' trade mode).
117+
If True, enables margin/leverage for SPOT trading.
118+
The margin mode is determined by `margin_mode` (CROSS or ISOLATED).
118119
If False, uses simple SPOT trading without leverage (uses 'cash' trade mode).
119-
Only applicable when trading SPOT instruments.
120+
Note: SPOT_ISOLATED mode is only available for OKX copy traders and lead traders.
120121
max_retries : PositiveInt, default 3
121122
The maximum retry attempts for requests.
122123
retry_delay_initial_ms : PositiveInt, default 1_000

nautilus_trader/adapters/okx/execution.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,12 @@ def __init__(
180180
if account_type == AccountType.CASH:
181181
# SPOT trading
182182
if config.use_spot_margin:
183-
self._trade_mode = OKXTradeMode.SPOT_ISOLATED
183+
# Use CROSS margin mode for spot margin trading
184+
# Note: SPOT_ISOLATED is only available for copy traders
185+
if config.margin_mode == OKXMarginMode.CROSS:
186+
self._trade_mode = OKXTradeMode.CROSS
187+
else:
188+
self._trade_mode = OKXTradeMode.ISOLATED
184189
else:
185190
self._trade_mode = OKXTradeMode.CASH
186191
else:
@@ -745,8 +750,19 @@ def _get_trade_mode_for_order(
745750
return self._trade_mode
746751

747752
if isinstance(instrument, CurrencyPair):
748-
return OKXTradeMode.SPOT_ISOLATED if self._config.use_spot_margin else OKXTradeMode.CASH
753+
# SPOT trading
754+
if self._config.use_spot_margin:
755+
# Use CROSS or ISOLATED margin mode for spot margin trading
756+
# Note: SPOT_ISOLATED is only available for copy traders
757+
return (
758+
OKXTradeMode.CROSS
759+
if self._config.margin_mode == OKXMarginMode.CROSS
760+
else OKXTradeMode.ISOLATED
761+
)
762+
else:
763+
return OKXTradeMode.CASH
749764
else:
765+
# Derivatives trading
750766
return (
751767
OKXTradeMode.CROSS
752768
if self._config.margin_mode == OKXMarginMode.CROSS

0 commit comments

Comments
 (0)