Quellcode durchsuchen

feat: add okx order support payload builders

lxy vor 1 Monat
Ursprung
Commit
51edef8cf0

+ 75 - 0
okx_codex_trader/okx_client.py

@@ -92,6 +92,81 @@ class OkxClient:
             raise self._invalid_payload()
         return item
 
+    @staticmethod
+    def build_post_only_limit_order_body(
+        *,
+        symbol: str,
+        action: str,
+        price: object,
+        size: object,
+        client_order_id: str,
+    ) -> dict[str, str]:
+        if action not in {"long", "short"}:
+            raise ValueError("action is invalid")
+        return {
+            "instId": symbol,
+            "tdMode": "isolated",
+            "side": "buy" if action == "long" else "sell",
+            "posSide": action,
+            "ordType": "post_only",
+            "px": _format_number(price),
+            "sz": _format_number(size),
+            "clOrdId": client_order_id,
+        }
+
+    @staticmethod
+    def build_entry_batch_order_body(
+        *,
+        symbol: str,
+        action: str,
+        reference_price: object,
+        margin_usdt: object,
+        leverage: object,
+        metadata: InstrumentMeta,
+        client_order_id_prefix: str,
+    ) -> list[dict[str, str]]:
+        reference_price_decimal = _parse_finite_decimal(reference_price)
+        notional_per_order = _parse_finite_decimal(margin_usdt) * _parse_finite_decimal(leverage) / Decimal("3")
+        bodies = []
+        for index, offset in enumerate((Decimal("0.003"), Decimal("0.006"), Decimal("0.009")), start=1):
+            multiplier = Decimal("1") - offset if action == "long" else Decimal("1") + offset
+            price = reference_price_decimal * multiplier
+            size = build_contract_size(notional_per_order, price, metadata)
+            bodies.append(
+                OkxClient.build_post_only_limit_order_body(
+                    symbol=symbol,
+                    action=action,
+                    price=price,
+                    size=size,
+                    client_order_id=f"{client_order_id_prefix}-{index}",
+                )
+            )
+        return bodies
+
+    @staticmethod
+    def build_cancel_order_body(
+        *,
+        symbol: str,
+        order_id: str | None = None,
+        client_order_id: str | None = None,
+    ) -> dict[str, str]:
+        if bool(order_id) == bool(client_order_id):
+            raise ValueError("exactly one order identifier is required")
+        body = {"instId": symbol}
+        if order_id:
+            body["ordId"] = order_id
+        if client_order_id:
+            body["clOrdId"] = client_order_id
+        return body
+
+    @staticmethod
+    def build_pending_orders_params(*, symbol: str) -> dict[str, str]:
+        return {"instType": "SWAP", "instId": symbol}
+
+    @staticmethod
+    def build_fills_params(*, symbol: str) -> dict[str, str]:
+        return {"instType": "SWAP", "instId": symbol}
+
     def _request(
         self,
         method: str,

+ 30 - 0
reports/eth-exploration/okx-order-support-formal-implementation.json

@@ -0,0 +1,30 @@
+{
+  "scope": "OKX order support formal production builder implementation",
+  "network_requests_made": 0,
+  "modified_production_surface": {
+    "class": "OkxClient",
+    "methods_added": [
+      "build_post_only_limit_order_body",
+      "build_entry_batch_order_body",
+      "build_cancel_order_body",
+      "build_pending_orders_params",
+      "build_fills_params"
+    ]
+  },
+  "execution_wiring": {
+    "place_order_changed": false,
+    "cli_added": false,
+    "trading_execution_changed": false
+  },
+  "verified_shapes": [
+    "post_only_limit_order_body",
+    "entry_batch_order_body",
+    "cancel_order_body",
+    "pending_orders_params",
+    "fills_params"
+  ],
+  "verification": {
+    "command": "rtk .venv/bin/pytest -q tests/test_okx_client.py",
+    "result": "75 passed in 0.06s"
+  }
+}

+ 37 - 0
reports/eth-exploration/okx-order-support-formal-implementation.md

@@ -0,0 +1,37 @@
+# OKX order support formal implementation
+
+No OKX request, order, or cancel was made.
+
+## Implemented production surface
+
+`OkxClient` now has non-submitting static builders for:
+
+- `build_post_only_limit_order_body`
+- `build_entry_batch_order_body`
+- `build_cancel_order_body`
+- `build_pending_orders_params`
+- `build_fills_params`
+
+Existing `place_order` behavior is unchanged. No CLI command was added. No trading execution path calls the new builders.
+
+## Verified shapes
+
+- Single isolated `post_only` limit order body.
+- Three independent isolated `post_only` entry bodies at 0.3%, 0.6%, and 0.9% offsets.
+- Cancel body with exactly one order identifier.
+- Pending-orders params with `instType=SWAP` and `instId`.
+- Fills params with `instType=SWAP` and `instId`.
+
+## Verification
+
+Command:
+
+```bash
+rtk .venv/bin/pytest -q tests/test_okx_client.py
+```
+
+Result:
+
+```text
+75 passed in 0.06s
+```

+ 107 - 0
tests/test_okx_client.py

@@ -640,6 +640,113 @@ def test_build_contract_size_rejects_boolean_inputs():
         build_contract_size(notional=100, price=25_000, metadata=InstrumentMeta(ct_val=True, lot_sz=1, min_sz=1))
 
 
+def test_post_only_limit_order_body_is_exact_okx_payload():
+    body = OkxClient.build_post_only_limit_order_body(
+        symbol="ETH-USDT-SWAP",
+        action="long",
+        price="2991.0000",
+        size="2.000",
+        client_order_id="eth-twap-1",
+    )
+
+    assert body == {
+        "instId": "ETH-USDT-SWAP",
+        "tdMode": "isolated",
+        "side": "buy",
+        "posSide": "long",
+        "ordType": "post_only",
+        "px": "2991",
+        "sz": "2",
+        "clOrdId": "eth-twap-1",
+    }
+
+
+def test_short_post_only_limit_order_body_uses_sell_short():
+    body = OkxClient.build_post_only_limit_order_body(
+        symbol="ETH-USDT-SWAP",
+        action="short",
+        price="3018",
+        size="1.5",
+        client_order_id="eth-twap-short-2",
+    )
+
+    assert body["side"] == "sell"
+    assert body["posSide"] == "short"
+    assert body["ordType"] == "post_only"
+
+
+def test_entry_batch_order_body_uses_three_independent_post_only_levels():
+    metadata = InstrumentMeta(ct_val=0.01, lot_sz=0.1, min_sz=0.1)
+
+    body = OkxClient.build_entry_batch_order_body(
+        symbol="ETH-USDT-SWAP",
+        action="long",
+        reference_price="3000",
+        margin_usdt="90",
+        leverage="2",
+        metadata=metadata,
+        client_order_id_prefix="eth-twap-20260430T000000Z",
+    )
+
+    assert body == [
+        {
+            "instId": "ETH-USDT-SWAP",
+            "tdMode": "isolated",
+            "side": "buy",
+            "posSide": "long",
+            "ordType": "post_only",
+            "px": "2991",
+            "sz": "2",
+            "clOrdId": "eth-twap-20260430T000000Z-1",
+        },
+        {
+            "instId": "ETH-USDT-SWAP",
+            "tdMode": "isolated",
+            "side": "buy",
+            "posSide": "long",
+            "ordType": "post_only",
+            "px": "2982",
+            "sz": "2",
+            "clOrdId": "eth-twap-20260430T000000Z-2",
+        },
+        {
+            "instId": "ETH-USDT-SWAP",
+            "tdMode": "isolated",
+            "side": "buy",
+            "posSide": "long",
+            "ordType": "post_only",
+            "px": "2973",
+            "sz": "2",
+            "clOrdId": "eth-twap-20260430T000000Z-3",
+        },
+    ]
+
+
+def test_cancel_order_body_uses_exactly_one_identifier():
+    assert OkxClient.build_cancel_order_body(symbol="ETH-USDT-SWAP", order_id="123") == {
+        "instId": "ETH-USDT-SWAP",
+        "ordId": "123",
+    }
+    assert OkxClient.build_cancel_order_body(symbol="ETH-USDT-SWAP", client_order_id="eth-twap-1") == {
+        "instId": "ETH-USDT-SWAP",
+        "clOrdId": "eth-twap-1",
+    }
+
+
+def test_pending_orders_query_params_are_minimal_params():
+    assert OkxClient.build_pending_orders_params(symbol="ETH-USDT-SWAP") == {
+        "instType": "SWAP",
+        "instId": "ETH-USDT-SWAP",
+    }
+
+
+def test_fills_query_params_are_minimal_params():
+    assert OkxClient.build_fills_params(symbol="ETH-USDT-SWAP") == {
+        "instType": "SWAP",
+        "instId": "ETH-USDT-SWAP",
+    }
+
+
 @pytest.mark.parametrize(
     ("price", "metadata"),
     [