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())