#!/usr/bin/env python3 from __future__ import annotations import argparse import json from datetime import UTC, datetime from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] REPORT_DIR = ROOT / "reports" / "eth-exploration" MD_OUT = REPORT_DIR / "eth-focused-portfolio-live-readiness.md" JSON_OUT = REPORT_DIR / "eth-focused-portfolio-live-readiness.json" SOURCE_FILES = { "okx_client": ROOT / "okx_codex_trader" / "okx_client.py", "cli": ROOT / "okx_codex_trader" / "cli.py", "paper_engine": ROOT / "okx_codex_trader" / "paper_engine.py", "signal_intent": ROOT / "scripts" / "build_eth_focused_portfolio_signal_intent.py", "live_plan": REPORT_DIR / "eth-robust-twap-live-plan.md", "portfolio_report": REPORT_DIR / "eth-focused-portfolio-conservative-report.md", } def read_sources() -> dict[str, str]: return {name: path.read_text(encoding="utf-8") for name, path in SOURCE_FILES.items() if path.exists()} def has_all(text: str, needles: tuple[str, ...]) -> bool: return all(needle in text for needle in needles) def item( key: str, title: str, status: str, evidence: list[str], gap: str, minimum_task: str, category: str, ) -> dict[str, Any]: return { "key": key, "title": title, "status": status, "evidence": evidence, "gap": gap, "minimum_task": minimum_task, "category": category, } def build_payload() -> dict[str, Any]: sources = read_sources() okx = sources.get("okx_client", "") cli = sources.get("cli", "") paper = sources.get("paper_engine", "") intent = sources.get("signal_intent", "") plan = sources.get("live_plan", "") checks = [ item( "post_only", "OKX post-only entry support", "missing", [ "Current place_order chooses ordType market when entry_price is absent and limit when entry_price is present.", "The only trade submit endpoint in code is /api/v5/trade/order.", ], "No code path emits ordType=post_only or deterministic client order ids for ETH portfolio entries.", "Add a read-only order-intent builder that renders the exact post_only order payloads for each required entry level without submitting them.", "must", ), item( "batch_orders", "Batch entry order support", "missing", [ "Current OKX client has one single-order place_order method.", "No /api/v5/trade/batch-orders endpoint or multi-order method is present.", ], "The conservative portfolio can require multiple leg/order intents; the client cannot express an atomic or coordinated batch.", "Define the portfolio order-intent shape for all active legs and add a non-submitting batch payload renderer.", "must", ), item( "cancel_open_orders", "Cancel open orders", "missing", [ "No cancel_order, cancel_batch_orders, or orders-pending method exists in okx_client.py.", "The signal-intent report always sets needs_cancel to False and no_cancel_submission to True.", ], "A quasi-live loop cannot expire stale maker orders or clear outstanding leg orders.", "Add read-only cancel-intent generation from tracked open order ids, then add client cancel/list methods before any live runner is enabled.", "must", ), item( "state_persistence", "State persistence", "partial", [ "paper_engine.py persists paper_state.json.", "Existing paper state assumes immediate local fills and has no exchange order lifecycle.", ], "There is no dedicated ETH portfolio state containing signal clock, order ids, fills, exposure, and audit events.", "Add a dedicated ETH portfolio state schema and read/write command for quasi-live intent tracking.", "must", ), item( "position_isolation", "Position isolation", "partial", [ "place_order calls ensure_hedge_mode before submitting.", "set_leverage sends mgnMode=isolated and place_order sends tdMode=isolated with posSide.", ], "Isolation exists only inside the single-order submitter; portfolio readiness still needs the same fields in generated leg/order intents and close intents.", "Require tdMode=isolated, posSide=long, and bounded leverage in every generated ETH portfolio intent.", "must", ), item( "existing_position_protection", "Existing position protection", "missing", [ "okx-account reads positions.", "okx-order does not check existing ETH exposure before calling place_order.", ], "A future runner could merge with or alter pre-existing ETH-USDT-SWAP exposure in the same account.", "Before any submit-capable command, require zero conflicting ETH-USDT-SWAP exposure or an explicitly dedicated state-owned position id.", "must", ), item( "signal_scheduling", "Signal scheduling", "missing", [ "build_eth_focused_portfolio_signal_intent.py evaluates cached candles once and writes dry-run output.", "No scheduler, last-confirmed-candle state, or one-cycle-per-candle guard exists in CLI code.", ], "The repo cannot run a quasi-live candle-bound signal loop for the ETH-focused portfolio.", "Add a read-only quasi-live runner that records last confirmed candle per leg and emits intent only when a leg clock advances.", "must", ), item( "logs", "Operational logs", "missing", [ "No logging module usage is present in okx_codex_trader.", "Existing reports are generated snapshots, not append-only runtime logs.", ], "There is no durable audit trail for signal decisions, payloads, cancel intents, fills, or state transitions.", "Add append-only JSONL event logging for read-only signal/order/cancel intent cycles.", "must", ), ] optional_tasks = [ "Add a demo-only execution adapter after read-only intent/state/logging proves one full signal cycle.", "Add reduce-only close-intent rendering and tests before adding any close submit path.", "Add portfolio-level exposure reports comparing intended weight, tracked exchange exposure, and cash limits.", ] readiness = { "ready_for_quasi_live": False, "can_submit_orders_now": has_all(okx, ('"/api/v5/trade/order"', "def place_order")), "should_submit_orders_now": False, "reason": "Required order lifecycle, state ownership, scheduling, and audit pieces are missing or only partial.", } return { "report": "eth-focused-portfolio-live-readiness", "created_at": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"), "scope": "read-only static repository inspection; no OKX request, no order, no cancel", "source_files": {name: str(path.relative_to(ROOT)) for name, path in SOURCE_FILES.items()}, "readiness": readiness, "checklist": checks, "minimum_implementation_tasks": { "must": [check["minimum_task"] for check in checks if check["category"] == "must"], "optional": optional_tasks, }, "current_code_facts": { "single_order_submitter_present": has_all(okx, ('"/api/v5/trade/order"', "def place_order")), "post_only_in_client_code": '"post_only"' in okx or "'post_only'" in okx, "batch_endpoint_in_client_code": "batch-orders" in okx, "cancel_endpoint_in_client_code": "cancel-order" in okx or "cancel-batch-orders" in okx, "paper_state_present": "def load_state" in paper and "def save_state" in paper, "portfolio_intent_is_readonly": "submitted_orders" in intent and "no_order_submission" in intent, "live_plan_mentions_required_lifecycle": "post_only" in plan and "state file" in plan.lower(), "cli_has_okx_order_command": 'subparsers.add_parser("okx-order")' in cli, }, } def md_table(rows: list[list[str]]) -> str: lines = [ "| " + " | ".join(rows[0]) + " |", "| " + " | ".join(["---"] * len(rows[0])) + " |", ] lines.extend("| " + " | ".join(row) + " |" for row in rows[1:]) return "\n".join(lines) def render_markdown(payload: dict[str, Any]) -> str: rows = [["Check", "Status", "Category", "Gap", "Minimum task"]] for check in payload["checklist"]: rows.append( [ check["title"], check["status"], check["category"], check["gap"], check["minimum_task"], ] ) must_rows = [["#", "Task"]] for index, task in enumerate(payload["minimum_implementation_tasks"]["must"], start=1): must_rows.append([str(index), task]) optional_rows = [["#", "Task"]] for index, task in enumerate(payload["minimum_implementation_tasks"]["optional"], start=1): optional_rows.append([str(index), task]) lines = [ "# ETH-focused portfolio live readiness", "", "Read-only static inspection. No OKX request, order, or cancel was made.", "", "## Topline", "", f"- Ready for quasi-live: `{payload['readiness']['ready_for_quasi_live']}`", f"- Submit-capable code exists now: `{payload['readiness']['can_submit_orders_now']}`", f"- Should submit now: `{payload['readiness']['should_submit_orders_now']}`", f"- Reason: {payload['readiness']['reason']}", "", "## Checklist", "", md_table(rows), "", "## Must implement", "", md_table(must_rows), "", "## Optional", "", md_table(optional_rows), "", "## Evidence", "", ] for check in payload["checklist"]: lines.append(f"### {check['title']}") for evidence in check["evidence"]: lines.append(f"- {evidence}") lines.append("") return "\n".join(lines).rstrip() + "\n" def main() -> int: parser = argparse.ArgumentParser(description="Check ETH-focused portfolio quasi-live readiness without trading.") parser.add_argument("--no-write", action="store_true") args = parser.parse_args() payload = build_payload() if not args.no_write: REPORT_DIR.mkdir(parents=True, exist_ok=True) JSON_OUT.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") MD_OUT.write_text(render_markdown(payload), encoding="utf-8") print(json.dumps(payload, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())