Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions backend_api_python/app/services/live_trading/okx.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,21 +258,15 @@ def _normalize_order_size(self, *, inst_id: str, market_type: str, size: float)
if lot_sz > 0:
req = self._floor_to_step(req, lot_sz)

# Infer precision from lotSz
# Infer precision from lotSz. Use the Decimal exponent instead of string
# parsing: normalize() renders small steps in scientific notation
# (0.00000001 -> "1E-8", no '.'), which would misread precision as 0.
size_precision = None
if lot_sz > 0:
try:
lot_sz_normalized = lot_sz.normalize()
lot_sz_str = str(lot_sz_normalized)
if '.' in lot_sz_str:
decimal_part = lot_sz_str.split('.')[1]
size_precision = len(decimal_part)
if size_precision < 0:
size_precision = 0
if size_precision > 18:
size_precision = 18
else:
size_precision = 0
exp = lot_sz.normalize().as_tuple().exponent
if isinstance(exp, int):
size_precision = min(max(0, -exp), 18)
except Exception:
pass

Expand Down
62 changes: 62 additions & 0 deletions backend_api_python/tests/test_okx_order_size_precision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""OKX order size precision inference from lotSz.

Regression test: lotSz values like "0.00000001" normalize to scientific
notation ("1E-8") under Decimal.normalize(), which the old string-based
parser misread as precision 0 — collapsing any sub-1-unit spot order to
sz="0" and triggering OKX error 51000 (Parameter sz error).
"""

import time

from app.services.live_trading.okx import OkxClient


def _client_with_instrument(inst_id: str, inst_type: str, lot_sz: str, min_sz: str) -> OkxClient:
c = OkxClient(api_key="k", secret_key="s", passphrase="p")
c._inst_cache[f"{inst_type}:{inst_id}"] = (
time.time(),
{"instId": inst_id, "lotSz": lot_sz, "minSz": min_sz},
)
return c


def test_spot_small_lot_sz_keeps_fractional_size():
# BTC-USDT spot: lotSz=0.00000001 (1E-8 after Decimal.normalize()).
c = _client_with_instrument("BTC-USDT", "SPOT", "0.00000001", "0.00001")
sz, precision = c._normalize_order_size(
inst_id="BTC-USDT", market_type="spot", size=0.00081471
)
assert precision == 8
assert c._dec_str(sz, strict_precision=precision) == "0.00081471"


def test_spot_regular_lot_sz_precision():
c = _client_with_instrument("ETH-USDT", "SPOT", "0.000001", "0.001")
sz, precision = c._normalize_order_size(
inst_id="ETH-USDT", market_type="spot", size=0.0234567891
)
assert precision == 6
# Floored to lot step, then rendered without exceeding precision.
assert c._dec_str(sz, strict_precision=precision) == "0.023456"


def test_swap_integer_lot_sz_precision_zero():
# Swap contracts: lotSz=1 must keep precision 0 (whole contracts).
c = _client_with_instrument("BTC-USDT-SWAP", "SWAP", "1", "1")
c._inst_cache["SWAP:BTC-USDT-SWAP"][1]["ctVal"] = "0.01"
sz, precision = c._normalize_order_size(
inst_id="BTC-USDT-SWAP", market_type="swap", size=0.05
)
assert precision == 0
assert c._dec_str(sz, strict_precision=precision) == "5"


def test_positive_exponent_lot_sz_clamps_to_zero_precision():
# lotSz=10 normalizes to 1E+1 (positive exponent); max(0, -exp) must
# clamp precision to 0 rather than going negative.
c = _client_with_instrument("X-USDT", "SPOT", "10", "10")
sz, precision = c._normalize_order_size(
inst_id="X-USDT", market_type="spot", size=25.0
)
assert precision == 0
assert c._dec_str(sz, strict_precision=precision) == "20"