From c1fce5b55161a561e170e2cb9779da68bc5de0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:07:38 +0800 Subject: [PATCH 1/2] fix: OKX order size precision misread as 0 for small lotSz Decimal.normalize() renders lotSz values like 0.00000001 in scientific notation (1E-8). The string-based precision parser found no '.' in '1E-8' and fell back to precision 0, so any spot order below one whole unit was quantized to sz="0" and rejected by OKX with error 51000 (Parameter sz error). Derive precision from the Decimal exponent instead, and add regression tests. --- .../app/services/live_trading/okx.py | 18 +++---- .../tests/test_okx_order_size_precision.py | 51 +++++++++++++++++++ 2 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 backend_api_python/tests/test_okx_order_size_precision.py diff --git a/backend_api_python/app/services/live_trading/okx.py b/backend_api_python/app/services/live_trading/okx.py index 4ad919087..d05c41e79 100644 --- a/backend_api_python/app/services/live_trading/okx.py +++ b/backend_api_python/app/services/live_trading/okx.py @@ -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 diff --git a/backend_api_python/tests/test_okx_order_size_precision.py b/backend_api_python/tests/test_okx_order_size_precision.py new file mode 100644 index 000000000..4254f159c --- /dev/null +++ b/backend_api_python/tests/test_okx_order_size_precision.py @@ -0,0 +1,51 @@ +"""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" From 0f779ecff2f9c6d449f34233b2dba23ecb8fd9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:27:30 +0800 Subject: [PATCH 2/2] test: cover positive-exponent lotSz (10) precision clamp --- .../tests/test_okx_order_size_precision.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend_api_python/tests/test_okx_order_size_precision.py b/backend_api_python/tests/test_okx_order_size_precision.py index 4254f159c..1957d7a84 100644 --- a/backend_api_python/tests/test_okx_order_size_precision.py +++ b/backend_api_python/tests/test_okx_order_size_precision.py @@ -49,3 +49,14 @@ def test_swap_integer_lot_sz_precision_zero(): ) 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"