check_eth_focused_portfolio_live_readiness.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #!/usr/bin/env python3
  2. from __future__ import annotations
  3. import argparse
  4. import json
  5. from datetime import UTC, datetime
  6. from pathlib import Path
  7. from typing import Any
  8. ROOT = Path(__file__).resolve().parents[1]
  9. REPORT_DIR = ROOT / "reports" / "eth-exploration"
  10. MD_OUT = REPORT_DIR / "eth-focused-portfolio-live-readiness.md"
  11. JSON_OUT = REPORT_DIR / "eth-focused-portfolio-live-readiness.json"
  12. SOURCE_FILES = {
  13. "okx_client": ROOT / "okx_codex_trader" / "okx_client.py",
  14. "cli": ROOT / "okx_codex_trader" / "cli.py",
  15. "paper_engine": ROOT / "okx_codex_trader" / "paper_engine.py",
  16. "signal_intent": ROOT / "scripts" / "build_eth_focused_portfolio_signal_intent.py",
  17. "live_plan": REPORT_DIR / "eth-robust-twap-live-plan.md",
  18. "portfolio_report": REPORT_DIR / "eth-focused-portfolio-conservative-report.md",
  19. }
  20. def read_sources() -> dict[str, str]:
  21. return {name: path.read_text(encoding="utf-8") for name, path in SOURCE_FILES.items() if path.exists()}
  22. def has_all(text: str, needles: tuple[str, ...]) -> bool:
  23. return all(needle in text for needle in needles)
  24. def item(
  25. key: str,
  26. title: str,
  27. status: str,
  28. evidence: list[str],
  29. gap: str,
  30. minimum_task: str,
  31. category: str,
  32. ) -> dict[str, Any]:
  33. return {
  34. "key": key,
  35. "title": title,
  36. "status": status,
  37. "evidence": evidence,
  38. "gap": gap,
  39. "minimum_task": minimum_task,
  40. "category": category,
  41. }
  42. def build_payload() -> dict[str, Any]:
  43. sources = read_sources()
  44. okx = sources.get("okx_client", "")
  45. cli = sources.get("cli", "")
  46. paper = sources.get("paper_engine", "")
  47. intent = sources.get("signal_intent", "")
  48. plan = sources.get("live_plan", "")
  49. checks = [
  50. item(
  51. "post_only",
  52. "OKX post-only entry support",
  53. "missing",
  54. [
  55. "Current place_order chooses ordType market when entry_price is absent and limit when entry_price is present.",
  56. "The only trade submit endpoint in code is /api/v5/trade/order.",
  57. ],
  58. "No code path emits ordType=post_only or deterministic client order ids for ETH portfolio entries.",
  59. "Add a read-only order-intent builder that renders the exact post_only order payloads for each required entry level without submitting them.",
  60. "must",
  61. ),
  62. item(
  63. "batch_orders",
  64. "Batch entry order support",
  65. "missing",
  66. [
  67. "Current OKX client has one single-order place_order method.",
  68. "No /api/v5/trade/batch-orders endpoint or multi-order method is present.",
  69. ],
  70. "The conservative portfolio can require multiple leg/order intents; the client cannot express an atomic or coordinated batch.",
  71. "Define the portfolio order-intent shape for all active legs and add a non-submitting batch payload renderer.",
  72. "must",
  73. ),
  74. item(
  75. "cancel_open_orders",
  76. "Cancel open orders",
  77. "missing",
  78. [
  79. "No cancel_order, cancel_batch_orders, or orders-pending method exists in okx_client.py.",
  80. "The signal-intent report always sets needs_cancel to False and no_cancel_submission to True.",
  81. ],
  82. "A quasi-live loop cannot expire stale maker orders or clear outstanding leg orders.",
  83. "Add read-only cancel-intent generation from tracked open order ids, then add client cancel/list methods before any live runner is enabled.",
  84. "must",
  85. ),
  86. item(
  87. "state_persistence",
  88. "State persistence",
  89. "partial",
  90. [
  91. "paper_engine.py persists paper_state.json.",
  92. "Existing paper state assumes immediate local fills and has no exchange order lifecycle.",
  93. ],
  94. "There is no dedicated ETH portfolio state containing signal clock, order ids, fills, exposure, and audit events.",
  95. "Add a dedicated ETH portfolio state schema and read/write command for quasi-live intent tracking.",
  96. "must",
  97. ),
  98. item(
  99. "position_isolation",
  100. "Position isolation",
  101. "partial",
  102. [
  103. "place_order calls ensure_hedge_mode before submitting.",
  104. "set_leverage sends mgnMode=isolated and place_order sends tdMode=isolated with posSide.",
  105. ],
  106. "Isolation exists only inside the single-order submitter; portfolio readiness still needs the same fields in generated leg/order intents and close intents.",
  107. "Require tdMode=isolated, posSide=long, and bounded leverage in every generated ETH portfolio intent.",
  108. "must",
  109. ),
  110. item(
  111. "existing_position_protection",
  112. "Existing position protection",
  113. "missing",
  114. [
  115. "okx-account reads positions.",
  116. "okx-order does not check existing ETH exposure before calling place_order.",
  117. ],
  118. "A future runner could merge with or alter pre-existing ETH-USDT-SWAP exposure in the same account.",
  119. "Before any submit-capable command, require zero conflicting ETH-USDT-SWAP exposure or an explicitly dedicated state-owned position id.",
  120. "must",
  121. ),
  122. item(
  123. "signal_scheduling",
  124. "Signal scheduling",
  125. "missing",
  126. [
  127. "build_eth_focused_portfolio_signal_intent.py evaluates cached candles once and writes dry-run output.",
  128. "No scheduler, last-confirmed-candle state, or one-cycle-per-candle guard exists in CLI code.",
  129. ],
  130. "The repo cannot run a quasi-live candle-bound signal loop for the ETH-focused portfolio.",
  131. "Add a read-only quasi-live runner that records last confirmed candle per leg and emits intent only when a leg clock advances.",
  132. "must",
  133. ),
  134. item(
  135. "logs",
  136. "Operational logs",
  137. "missing",
  138. [
  139. "No logging module usage is present in okx_codex_trader.",
  140. "Existing reports are generated snapshots, not append-only runtime logs.",
  141. ],
  142. "There is no durable audit trail for signal decisions, payloads, cancel intents, fills, or state transitions.",
  143. "Add append-only JSONL event logging for read-only signal/order/cancel intent cycles.",
  144. "must",
  145. ),
  146. ]
  147. optional_tasks = [
  148. "Add a demo-only execution adapter after read-only intent/state/logging proves one full signal cycle.",
  149. "Add reduce-only close-intent rendering and tests before adding any close submit path.",
  150. "Add portfolio-level exposure reports comparing intended weight, tracked exchange exposure, and cash limits.",
  151. ]
  152. readiness = {
  153. "ready_for_quasi_live": False,
  154. "can_submit_orders_now": has_all(okx, ('"/api/v5/trade/order"', "def place_order")),
  155. "should_submit_orders_now": False,
  156. "reason": "Required order lifecycle, state ownership, scheduling, and audit pieces are missing or only partial.",
  157. }
  158. return {
  159. "report": "eth-focused-portfolio-live-readiness",
  160. "created_at": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
  161. "scope": "read-only static repository inspection; no OKX request, no order, no cancel",
  162. "source_files": {name: str(path.relative_to(ROOT)) for name, path in SOURCE_FILES.items()},
  163. "readiness": readiness,
  164. "checklist": checks,
  165. "minimum_implementation_tasks": {
  166. "must": [check["minimum_task"] for check in checks if check["category"] == "must"],
  167. "optional": optional_tasks,
  168. },
  169. "current_code_facts": {
  170. "single_order_submitter_present": has_all(okx, ('"/api/v5/trade/order"', "def place_order")),
  171. "post_only_in_client_code": '"post_only"' in okx or "'post_only'" in okx,
  172. "batch_endpoint_in_client_code": "batch-orders" in okx,
  173. "cancel_endpoint_in_client_code": "cancel-order" in okx or "cancel-batch-orders" in okx,
  174. "paper_state_present": "def load_state" in paper and "def save_state" in paper,
  175. "portfolio_intent_is_readonly": "submitted_orders" in intent and "no_order_submission" in intent,
  176. "live_plan_mentions_required_lifecycle": "post_only" in plan and "state file" in plan.lower(),
  177. "cli_has_okx_order_command": 'subparsers.add_parser("okx-order")' in cli,
  178. },
  179. }
  180. def md_table(rows: list[list[str]]) -> str:
  181. lines = [
  182. "| " + " | ".join(rows[0]) + " |",
  183. "| " + " | ".join(["---"] * len(rows[0])) + " |",
  184. ]
  185. lines.extend("| " + " | ".join(row) + " |" for row in rows[1:])
  186. return "\n".join(lines)
  187. def render_markdown(payload: dict[str, Any]) -> str:
  188. rows = [["Check", "Status", "Category", "Gap", "Minimum task"]]
  189. for check in payload["checklist"]:
  190. rows.append(
  191. [
  192. check["title"],
  193. check["status"],
  194. check["category"],
  195. check["gap"],
  196. check["minimum_task"],
  197. ]
  198. )
  199. must_rows = [["#", "Task"]]
  200. for index, task in enumerate(payload["minimum_implementation_tasks"]["must"], start=1):
  201. must_rows.append([str(index), task])
  202. optional_rows = [["#", "Task"]]
  203. for index, task in enumerate(payload["minimum_implementation_tasks"]["optional"], start=1):
  204. optional_rows.append([str(index), task])
  205. lines = [
  206. "# ETH-focused portfolio live readiness",
  207. "",
  208. "Read-only static inspection. No OKX request, order, or cancel was made.",
  209. "",
  210. "## Topline",
  211. "",
  212. f"- Ready for quasi-live: `{payload['readiness']['ready_for_quasi_live']}`",
  213. f"- Submit-capable code exists now: `{payload['readiness']['can_submit_orders_now']}`",
  214. f"- Should submit now: `{payload['readiness']['should_submit_orders_now']}`",
  215. f"- Reason: {payload['readiness']['reason']}",
  216. "",
  217. "## Checklist",
  218. "",
  219. md_table(rows),
  220. "",
  221. "## Must implement",
  222. "",
  223. md_table(must_rows),
  224. "",
  225. "## Optional",
  226. "",
  227. md_table(optional_rows),
  228. "",
  229. "## Evidence",
  230. "",
  231. ]
  232. for check in payload["checklist"]:
  233. lines.append(f"### {check['title']}")
  234. for evidence in check["evidence"]:
  235. lines.append(f"- {evidence}")
  236. lines.append("")
  237. return "\n".join(lines).rstrip() + "\n"
  238. def main() -> int:
  239. parser = argparse.ArgumentParser(description="Check ETH-focused portfolio quasi-live readiness without trading.")
  240. parser.add_argument("--no-write", action="store_true")
  241. args = parser.parse_args()
  242. payload = build_payload()
  243. if not args.no_write:
  244. REPORT_DIR.mkdir(parents=True, exist_ok=True)
  245. JSON_OUT.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
  246. MD_OUT.write_text(render_markdown(payload), encoding="utf-8")
  247. print(json.dumps(payload, indent=2, sort_keys=True))
  248. return 0
  249. if __name__ == "__main__":
  250. raise SystemExit(main())