|
|
@@ -0,0 +1,276 @@
|
|
|
+import json
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+
|
|
|
+REPORT_DIR = Path("reports/eth-exploration")
|
|
|
+JSON_PATH = REPORT_DIR / "okx-order-support-plan.json"
|
|
|
+MD_PATH = REPORT_DIR / "okx-order-support-plan.md"
|
|
|
+
|
|
|
+
|
|
|
+PLAN = {
|
|
|
+ "report": "okx-order-support-plan",
|
|
|
+ "scope": "static implementation plan only; no OKX request, no order, no cancel",
|
|
|
+ "readiness_gap": "ETH-focused quasi-live portfolio needs a full order lifecycle before any submit-capable runner is valid.",
|
|
|
+ "source_files_checked": [
|
|
|
+ {
|
|
|
+ "path": "okx_codex_trader/okx_client.py",
|
|
|
+ "facts": [
|
|
|
+ "Signed _request already supports GET/POST with params/json_body.",
|
|
|
+ "place_order is a single-order submitter.",
|
|
|
+ "place_order emits ordType=market or ordType=limit only.",
|
|
|
+ "No batch-orders, orders-pending, cancel-order, order status, or fills method exists.",
|
|
|
+ "get_positions reads exchange positions but is not tied to portfolio-owned state.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "path": "tests/test_okx_client.py",
|
|
|
+ "facts": [
|
|
|
+ "DummySession records request path/body and can support endpoint-level client tests.",
|
|
|
+ "Existing tests cover request signing, live/demo header, contract sizing, leverage validation, single order payload, and position parsing.",
|
|
|
+ "No tests cover post_only payloads, batch payloads, order list/cancel/status/fills, or state persistence.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "path": "okx_codex_trader/cli.py",
|
|
|
+ "facts": [
|
|
|
+ "okx-account is read-only and returns balance plus positions.",
|
|
|
+ "okx-order is submit-capable and calls place_order.",
|
|
|
+ "No read-only ETH order-intent/state command exists.",
|
|
|
+ "No quasi-live state path or append-only lifecycle log is wired.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ "official_okx_api_surface": [
|
|
|
+ {
|
|
|
+ "capability": "single post-only limit order payload",
|
|
|
+ "endpoint": "POST /api/v5/trade/order",
|
|
|
+ "boundary": "submit-capable only after read-only payload rendering is tested",
|
|
|
+ "required_fields": ["instId", "tdMode", "side", "posSide", "ordType", "px", "sz", "clOrdId"],
|
|
|
+ "fixed_eth_values": {
|
|
|
+ "instId": "ETH-USDT-SWAP",
|
|
|
+ "tdMode": "isolated",
|
|
|
+ "side": "buy",
|
|
|
+ "posSide": "long",
|
|
|
+ "ordType": "post_only",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "capability": "batch entry orders",
|
|
|
+ "endpoint": "POST /api/v5/trade/batch-orders",
|
|
|
+ "boundary": "submit-capable; read-only stage renders the exact array only",
|
|
|
+ "required_shape": "list of three post_only entry payloads with deterministic clOrdId values",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "capability": "list open orders",
|
|
|
+ "endpoint": "GET /api/v5/trade/orders-pending",
|
|
|
+ "boundary": "read-only",
|
|
|
+ "minimum_params": {"instType": "SWAP", "instId": "ETH-USDT-SWAP"},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "capability": "cancel open order",
|
|
|
+ "endpoint": "POST /api/v5/trade/cancel-order",
|
|
|
+ "boundary": "submit-capable; read-only stage renders cancel intents only",
|
|
|
+ "required_fields": ["instId", "ordId or clOrdId"],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "capability": "cancel open orders in batch",
|
|
|
+ "endpoint": "POST /api/v5/trade/cancel-batch-orders",
|
|
|
+ "boundary": "submit-capable; read-only stage renders cancel intent array only",
|
|
|
+ "required_shape": "list of tracked open order ids for ETH strategy-owned orders",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "capability": "order status",
|
|
|
+ "endpoint": "GET /api/v5/trade/order",
|
|
|
+ "boundary": "read-only",
|
|
|
+ "minimum_params": {"instId": "ETH-USDT-SWAP", "ordId_or_clOrdId": "tracked order identity"},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "capability": "recent fills",
|
|
|
+ "endpoint": "GET /api/v5/trade/fills",
|
|
|
+ "boundary": "read-only",
|
|
|
+ "minimum_params": {"instType": "SWAP", "instId": "ETH-USDT-SWAP"},
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ "state_persistence": {
|
|
|
+ "boundary": "local read/write only; not an OKX API call",
|
|
|
+ "minimum_file": "state/eth_robust_twap_15m_live.json",
|
|
|
+ "fields": [
|
|
|
+ "strategy_id",
|
|
|
+ "symbol",
|
|
|
+ "bar",
|
|
|
+ "td_mode",
|
|
|
+ "pos_side",
|
|
|
+ "leverage",
|
|
|
+ "last_confirmed_candle_ts",
|
|
|
+ "last_signal_candle_ts",
|
|
|
+ "orders",
|
|
|
+ "fills",
|
|
|
+ "position",
|
|
|
+ "planned_margin_usdt",
|
|
|
+ "max_margin_usdt",
|
|
|
+ "updated_at",
|
|
|
+ ],
|
|
|
+ "event_log": "append-only JSONL for signal, order-intent, cancel-intent, order-status, fills, and state-transition events",
|
|
|
+ },
|
|
|
+ "minimum_patch_plan_by_file": [
|
|
|
+ {
|
|
|
+ "file": "okx_codex_trader/okx_client.py",
|
|
|
+ "patch_plan": [
|
|
|
+ "Add small dataclasses or typed dicts only if needed by return values: OkxOrder, OkxFill, OkxOrderAck.",
|
|
|
+ "Add render_post_only_limit_order_payload(...) as a non-submitting function using existing _format_number and build_contract_size.",
|
|
|
+ "Add render_eth_entry_batch_payloads(...) that returns exactly three post_only payloads for offsets 0.003, 0.006, 0.009.",
|
|
|
+ "Add list_open_orders(symbol) calling GET /api/v5/trade/orders-pending with instType=SWAP and instId=symbol.",
|
|
|
+ "Add get_order(symbol, ord_id=None, client_order_id=None) calling GET /api/v5/trade/order.",
|
|
|
+ "Add list_fills(symbol) calling GET /api/v5/trade/fills with instType=SWAP and instId=symbol.",
|
|
|
+ "Add cancel_order(symbol, ord_id=None, client_order_id=None) and cancel_batch_orders(symbol, order_ids) only when moving past read-only intent.",
|
|
|
+ "Do not change existing place_order semantics for the first patch; it is not suitable for the ETH portfolio lifecycle.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "file": "okx_codex_trader/cli.py",
|
|
|
+ "patch_plan": [
|
|
|
+ "Add read-only eth-robust-twap-order-intent command that prints signal, three entry payloads, expiry, and state transition preview.",
|
|
|
+ "Add read-only eth-robust-twap-open-orders command that lists current OKX pending orders for ETH-USDT-SWAP.",
|
|
|
+ "Add read-only eth-robust-twap-reconcile command that reads state, queries open orders/status/fills, and writes a new local state snapshot plus JSONL audit event.",
|
|
|
+ "Keep okx-order unchanged and do not route portfolio intents through it.",
|
|
|
+ "Only after read-only commands pass tests, add explicit submit commands for batch entry and cancellation.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "file": "tests/test_okx_client.py",
|
|
|
+ "patch_plan": [
|
|
|
+ "Add tests for post_only payload shape: ETH-USDT-SWAP, isolated, buy, long, post_only, px, sz, deterministic clOrdId.",
|
|
|
+ "Add tests for batch payload shape: exactly three orders, offsets 0.003/0.006/0.009, each level sized independently and rounded to lotSz.",
|
|
|
+ "Add tests for GET /api/v5/trade/orders-pending params and normalized open-order state values.",
|
|
|
+ "Add tests for POST /api/v5/trade/cancel-order and /cancel-batch-orders request bodies before enabling submit commands.",
|
|
|
+ "Add tests for GET /api/v5/trade/order by ordId and by clOrdId.",
|
|
|
+ "Add tests for GET /api/v5/trade/fills parsing fillSz, fillPx, tradeId, ordId, clOrdId, fee, and fillTime.",
|
|
|
+ "Add tests that malformed OKX lifecycle payloads raise the existing stable invalid-payload error.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "file": "new okx_codex_trader/eth_robust_twap_state.py",
|
|
|
+ "patch_plan": [
|
|
|
+ "Add only the dedicated state schema and load/save functions needed by the ETH quasi-live lifecycle.",
|
|
|
+ "Persist exchange order ids, client order ids, fills, position, last confirmed candle, active state, and audit cursor.",
|
|
|
+ "Reject state whose symbol/bar/strategy_id does not match the command arguments.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "file": "new tests/test_eth_robust_twap_state.py",
|
|
|
+ "patch_plan": [
|
|
|
+ "Add load empty state test.",
|
|
|
+ "Add save/read roundtrip for one signal with three open orders and one partial fill.",
|
|
|
+ "Add state mismatch rejection tests for symbol, bar, and strategy_id.",
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ "readonly_boundary": [
|
|
|
+ "Render post_only single and batch order payloads without calling POST /api/v5/trade/order or /batch-orders.",
|
|
|
+ "Read account balance, positions, pending orders, order details, and fills.",
|
|
|
+ "Write local state snapshots and append-only JSONL audit events.",
|
|
|
+ "Render cancel intents without calling cancel endpoints.",
|
|
|
+ ],
|
|
|
+ "submit_capable_boundary": [
|
|
|
+ "POST /api/v5/trade/order with ordType=post_only.",
|
|
|
+ "POST /api/v5/trade/batch-orders.",
|
|
|
+ "POST /api/v5/trade/cancel-order.",
|
|
|
+ "POST /api/v5/trade/cancel-batch-orders.",
|
|
|
+ "Any reduce-only market close path.",
|
|
|
+ ],
|
|
|
+ "test_checklist": [
|
|
|
+ "Existing tests: rtk .venv/bin/pytest tests/test_okx_client.py",
|
|
|
+ "New client tests for render-only post_only payloads make no DummySession requests.",
|
|
|
+ "New client tests for list_open_orders/get_order/list_fills assert method/path/params and parsed output.",
|
|
|
+ "New cancel tests assert request body shape but stay behind commands that are not wired in the read-only phase.",
|
|
|
+ "New CLI tests assert read-only order-intent/reconcile commands do not call submit/cancel endpoints.",
|
|
|
+ "New state tests assert deterministic state roundtrip and event append order.",
|
|
|
+ ],
|
|
|
+ "sources": [
|
|
|
+ "https://www.okx.com/docs-v5/en/",
|
|
|
+ "reports/eth-exploration/eth-focused-portfolio-live-readiness.md",
|
|
|
+ "reports/eth-exploration/eth-robust-twap-live-plan.md",
|
|
|
+ ],
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+def _md_table(rows: list[list[str]]) -> str:
|
|
|
+ header = rows[0]
|
|
|
+ divider = ["---"] * len(header)
|
|
|
+ lines = ["| " + " | ".join(header) + " |", "| " + " | ".join(divider) + " |"]
|
|
|
+ lines.extend("| " + " | ".join(row) + " |" for row in rows[1:])
|
|
|
+ return "\n".join(lines)
|
|
|
+
|
|
|
+
|
|
|
+def render_markdown() -> str:
|
|
|
+ lines = [
|
|
|
+ "# OKX order support minimum implementation plan",
|
|
|
+ "",
|
|
|
+ "Static implementation plan only. No OKX request, order, or cancel was made.",
|
|
|
+ "",
|
|
|
+ "## Goal",
|
|
|
+ "",
|
|
|
+ PLAN["readiness_gap"],
|
|
|
+ "",
|
|
|
+ "Minimum direct path: first add read-only payload/state/reconciliation support, then add submit-capable order and cancel calls behind explicit commands.",
|
|
|
+ "",
|
|
|
+ "## Current code facts",
|
|
|
+ "",
|
|
|
+ ]
|
|
|
+ for item in PLAN["source_files_checked"]:
|
|
|
+ lines.append(f"### `{item['path']}`")
|
|
|
+ lines.extend(f"- {fact}" for fact in item["facts"])
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ lines.extend(
|
|
|
+ [
|
|
|
+ "## Required OKX API surface",
|
|
|
+ "",
|
|
|
+ _md_table(
|
|
|
+ [["Capability", "Endpoint", "Boundary"]]
|
|
|
+ + [[item["capability"], f"`{item['endpoint']}`", item["boundary"]] for item in PLAN["official_okx_api_surface"]]
|
|
|
+ ),
|
|
|
+ "",
|
|
|
+ "State persistence is local state, not an OKX API call. Minimum file: "
|
|
|
+ f"`{PLAN['state_persistence']['minimum_file']}`.",
|
|
|
+ "",
|
|
|
+ "## Read-only boundary",
|
|
|
+ "",
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ lines.extend(f"- {item}" for item in PLAN["readonly_boundary"])
|
|
|
+ lines.extend(["", "## Submit-capable boundary", ""])
|
|
|
+ lines.extend(f"- {item}" for item in PLAN["submit_capable_boundary"])
|
|
|
+
|
|
|
+ lines.extend(["", "## Minimum patch plan by file", ""])
|
|
|
+ for item in PLAN["minimum_patch_plan_by_file"]:
|
|
|
+ lines.append(f"### `{item['file']}`")
|
|
|
+ lines.extend(f"- {step}" for step in item["patch_plan"])
|
|
|
+ lines.append("")
|
|
|
+
|
|
|
+ lines.extend(["## Test checklist", ""])
|
|
|
+ lines.extend(f"- {item}" for item in PLAN["test_checklist"])
|
|
|
+ lines.extend(
|
|
|
+ [
|
|
|
+ "",
|
|
|
+ "## Sources",
|
|
|
+ "",
|
|
|
+ "- OKX API v5 docs: https://www.okx.com/docs-v5/en/",
|
|
|
+ "- Live readiness report: `reports/eth-exploration/eth-focused-portfolio-live-readiness.md`",
|
|
|
+ "- ETH live execution plan: `reports/eth-exploration/eth-robust-twap-live-plan.md`",
|
|
|
+ "",
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ return "\n".join(lines)
|
|
|
+
|
|
|
+
|
|
|
+def main() -> int:
|
|
|
+ REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
+ JSON_PATH.write_text(json.dumps(PLAN, indent=2) + "\n")
|
|
|
+ MD_PATH.write_text(render_markdown())
|
|
|
+ print(json.dumps({"json": str(JSON_PATH), "markdown": str(MD_PATH)}, indent=2))
|
|
|
+ return 0
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ raise SystemExit(main())
|