run_eth_nextgen_micro_executor.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from __future__ import annotations
  2. import argparse
  3. import json
  4. import os
  5. import sys
  6. from dataclasses import asdict
  7. from datetime import UTC, datetime
  8. from pathlib import Path
  9. sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
  10. from okx_codex_trader import eth_nextgen_micro
  11. from okx_codex_trader.config import load_config
  12. from okx_codex_trader.live_execution import (
  13. TargetPosition,
  14. current_position_from_okx,
  15. load_runtime_state,
  16. plan_position_delta,
  17. render_market_order_bodies,
  18. target_from_signal,
  19. )
  20. from okx_codex_trader.okx_client import OkxClient
  21. ROOT = Path(__file__).resolve().parents[1]
  22. STATE_DIR = ROOT / "var" / "eth-nextgen-micro"
  23. SYMBOL = "ETH-USDT-SWAP"
  24. LEVERAGE = 3
  25. def now_iso() -> str:
  26. return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z")
  27. def unknown_current_position(reason: str) -> TargetPosition:
  28. return TargetPosition(side="flat", unit=0.0, known=False, reason=reason)
  29. def account_current_position(margin_per_unit_usdt: float) -> tuple[TargetPosition, dict[str, object]]:
  30. try:
  31. client = OkxClient(load_config())
  32. except ValueError as exc:
  33. return unknown_current_position(str(exc)), {"account_error": str(exc)}
  34. try:
  35. positions = client.get_positions(SYMBOL)
  36. metadata = client.get_instrument_meta(SYMBOL)
  37. mark_price = client.get_last_price(SYMBOL)
  38. current = current_position_from_okx(
  39. positions=positions,
  40. mark_price=mark_price,
  41. metadata=metadata,
  42. leverage=LEVERAGE,
  43. margin_per_unit_usdt=margin_per_unit_usdt,
  44. )
  45. return current, {
  46. "positions": [asdict(position) for position in positions],
  47. "instrument_meta": asdict(metadata),
  48. "mark_price": mark_price,
  49. }
  50. except ValueError as exc:
  51. return unknown_current_position(str(exc)), {"account_error": str(exc)}
  52. EXECUTOR_STATE_FILENAME = "executor-runtime-state.json"
  53. def build_snapshot(*, state_dir: Path, margin_per_unit_usdt: float, max_new_margin_usdt: float, max_total_margin_usdt: float) -> dict[str, object]:
  54. state_dir.mkdir(parents=True, exist_ok=True)
  55. payload = eth_nextgen_micro.build_payload()
  56. previous_state = load_runtime_state(state_dir / EXECUTOR_STATE_FILENAME)
  57. next_state, target = target_from_signal(payload, previous_state)
  58. current, account = account_current_position(margin_per_unit_usdt)
  59. plan = plan_position_delta(current, target)
  60. orders = ()
  61. if current.known and target.known:
  62. orders = render_market_order_bodies(
  63. plan=plan,
  64. symbol=SYMBOL,
  65. mark_price=float(account["mark_price"]),
  66. metadata=client_metadata(account),
  67. leverage=LEVERAGE,
  68. margin_per_unit_usdt=margin_per_unit_usdt,
  69. max_new_margin_usdt=max_new_margin_usdt,
  70. max_total_margin_usdt=max_total_margin_usdt,
  71. client_order_id_prefix=f"ethnm-{target_candle_ts(payload)}",
  72. )
  73. return {
  74. "created_at": now_iso(),
  75. "mode": "dry_run_executor",
  76. "orders_submitted": 0,
  77. "signal": payload["decision"],
  78. "execution_intent": payload["execution_intent"],
  79. "previous_state": asdict(previous_state),
  80. "next_state": asdict(next_state),
  81. "current_position": asdict(current),
  82. "target_position": asdict(target),
  83. "execution_plan": {
  84. "current": asdict(plan.current),
  85. "target": asdict(plan.target),
  86. "actions": [asdict(action) for action in plan.actions],
  87. },
  88. "rendered_orders": [asdict(order) for order in orders],
  89. "account": account,
  90. "risk_limits": {
  91. "submit_enabled": False,
  92. "max_new_margin_usdt": max_new_margin_usdt,
  93. "max_total_margin_usdt": max_total_margin_usdt,
  94. "margin_per_unit_usdt": margin_per_unit_usdt,
  95. "state_write_required_before_live": True,
  96. },
  97. }
  98. def risk_arg(value: float | None, env_name: str) -> float:
  99. if value is not None:
  100. return value
  101. raw = os.environ.get(env_name)
  102. if raw is None or raw == "":
  103. raise ValueError(f"{env_name} is required")
  104. return float(raw)
  105. def target_candle_ts(payload: dict[str, object]) -> int:
  106. if payload["decision"]["active_engine"] == "nextgen":
  107. return int(payload["nextgen"]["data"]["decision_candle_ts"])
  108. return int(payload["micro"]["decision_candle_ts"])
  109. def client_metadata(account: dict[str, object]):
  110. from okx_codex_trader.models import InstrumentMeta
  111. meta = account["instrument_meta"]
  112. return InstrumentMeta(ct_val=float(meta["ct_val"]), lot_sz=float(meta["lot_sz"]), min_sz=float(meta["min_sz"]))
  113. def main() -> int:
  114. parser = argparse.ArgumentParser(description="Build ETH nextgen+micro live execution dry-run snapshot.")
  115. parser.add_argument("--state-dir", type=Path, default=STATE_DIR)
  116. parser.add_argument("--margin-per-unit-usdt", type=float)
  117. parser.add_argument("--max-new-margin-usdt", type=float)
  118. parser.add_argument("--max-total-margin-usdt", type=float)
  119. args = parser.parse_args()
  120. snapshot = build_snapshot(
  121. state_dir=args.state_dir,
  122. margin_per_unit_usdt=risk_arg(args.margin_per_unit_usdt, "ETH_NEXTGEN_MARGIN_PER_UNIT_USDT"),
  123. max_new_margin_usdt=risk_arg(args.max_new_margin_usdt, "ETH_NEXTGEN_MAX_NEW_MARGIN_USDT"),
  124. max_total_margin_usdt=risk_arg(args.max_total_margin_usdt, "ETH_NEXTGEN_MAX_TOTAL_MARGIN_USDT"),
  125. )
  126. print(json.dumps(snapshot, indent=2, sort_keys=True))
  127. return 0
  128. if __name__ == "__main__":
  129. raise SystemExit(main())