Sfoglia il codice sorgente

fix: preserve sizing precision and leverage bounds

lxy 1 mese fa
parent
commit
0ea17308d7
2 ha cambiato i file con 61 aggiunte e 11 eliminazioni
  1. 13 10
      okx_codex_trader/okx_client.py
  2. 48 1
      tests/test_okx_client.py

+ 13 - 10
okx_codex_trader/okx_client.py

@@ -39,6 +39,14 @@ def _parse_finite_float(value: object) -> float:
     return parsed
 
 
+def _parse_valid_leverage(value: object) -> int:
+    if isinstance(value, bool) or not isinstance(value, int):
+        raise ValueError("leverage is invalid")
+    if value < 1 or value > 3:
+        raise ValueError("leverage is invalid")
+    return value
+
+
 def build_contract_size(notional: float, price: float, metadata: InstrumentMeta) -> float:
     notional_decimal = _parse_finite_decimal(notional)
     price_decimal = _parse_finite_decimal(price)
@@ -201,10 +209,7 @@ class OkxClient:
     def set_leverage(self, symbol: str, leverage: int, pos_side: str) -> None:
         if not symbol.endswith("-SWAP"):
             raise ValueError("swap instrument is required")
-        if isinstance(leverage, bool):
-            raise ValueError("leverage is invalid")
-        if leverage < 1 or leverage > 3:
-            raise ValueError("leverage is invalid")
+        leverage = _parse_valid_leverage(leverage)
         if pos_side not in {"long", "short"}:
             raise ValueError("pos_side is invalid")
         self._request(
@@ -233,12 +238,10 @@ class OkxClient:
             raise ValueError("action is invalid")
         if not symbol.endswith("-SWAP"):
             raise ValueError("swap instrument is required")
-        if isinstance(signal.leverage, bool):
-            raise ValueError("leverage is invalid")
-        if signal.leverage < 1 or signal.leverage > 3:
-            raise ValueError("leverage is invalid")
+        leverage = _parse_valid_leverage(signal.leverage)
         try:
             margin_value = _parse_finite_float(margin_usdt)
+            margin_decimal = _parse_finite_decimal(margin_usdt)
         except ValueError:
             raise ValueError("margin_usdt is invalid") from None
         if margin_value <= 0:
@@ -248,8 +251,8 @@ class OkxClient:
         price = signal.entry_price if signal.entry_price is not None else self.get_last_price(symbol)
         side = "buy" if signal.action == "long" else "sell"
         pos_side = "long" if signal.action == "long" else "short"
-        size = build_contract_size(margin_value * signal.leverage, price, metadata)
-        self.set_leverage(symbol, signal.leverage, pos_side)
+        size = build_contract_size(margin_decimal * leverage, price, metadata)
+        self.set_leverage(symbol, leverage, pos_side)
         order_type = "market" if signal.entry_price is None else "limit"
         request_body = {
             "instId": symbol,

+ 48 - 1
tests/test_okx_client.py

@@ -564,6 +564,33 @@ def test_market_order_fetches_latest_price_before_sizing():
     ]
 
 
+def test_fractional_margin_sizing_keeps_decimal_precision():
+    session = DummySession(
+        [
+            account_config_response(pos_mode="long_short_mode"),
+            instrument_response(),
+            ticker_response(last="1"),
+            leverage_response(),
+            place_order_response(),
+        ]
+    )
+    signal = TradeSignal(
+        action="long",
+        confidence=0.9,
+        leverage=3,
+        entry_price=None,
+        take_profit_price=None,
+        stop_loss_price=None,
+        reason="x",
+    )
+    client = OkxClient(config=sample_config(), session=session)
+
+    client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=0.009)
+
+    assert session.last_json_body is not None
+    assert session.last_json_body["sz"] == "27"
+
+
 def test_place_demo_order_fails_when_not_hedge_mode():
     session = DummySession(
         [
@@ -774,9 +801,9 @@ def test_get_instrument_meta_rejects_non_swap_type():
 def test_place_demo_order_raises_when_order_id_is_missing():
     session = DummySession(
         [
+            account_config_response(pos_mode="long_short_mode"),
             instrument_response(),
             ticker_response(last="25000"),
-            account_config_response(pos_mode="long_short_mode"),
             leverage_response(),
             place_order_response_without_order_id(),
         ]
@@ -785,6 +812,7 @@ def test_place_demo_order_raises_when_order_id_is_missing():
 
     with pytest.raises(ValueError, match="okx response payload is invalid"):
         client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
+    assert session.request_paths[-1] == "/api/v5/trade/order"
 
 
 def test_place_demo_order_rejects_invalid_leverage_before_okx():
@@ -805,6 +833,24 @@ def test_place_demo_order_rejects_invalid_leverage_before_okx():
     assert session.request_paths == []
 
 
+def test_place_demo_order_rejects_fractional_leverage_before_okx():
+    session = DummySession([])
+    signal = TradeSignal(
+        action="long",
+        confidence=0.9,
+        leverage=2.5,
+        entry_price=None,
+        take_profit_price=None,
+        stop_loss_price=None,
+        reason="x",
+    )
+    client = OkxClient(config=sample_config(), session=session)
+
+    with pytest.raises(ValueError, match="leverage is invalid"):
+        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
+    assert session.request_paths == []
+
+
 def test_place_demo_order_rejects_boolean_leverage_before_okx():
     session = DummySession([])
     signal = TradeSignal(
@@ -847,6 +893,7 @@ def test_place_demo_order_rejects_boolean_margin_before_okx():
     [
         ("BTC-USDT", 2, "long", "swap instrument is required"),
         ("BTC-USDT-SWAP", 4, "long", "leverage is invalid"),
+        ("BTC-USDT-SWAP", 2.5, "long", "leverage is invalid"),
         ("BTC-USDT-SWAP", 2, "net", "pos_side is invalid"),
     ],
 )