| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- #!/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())
|