import importlib.util import sys from pathlib import Path import pytest from okx_codex_trader.models import OrderResult from okx_codex_trader.live_execution import TargetPosition def load_executor(): path = Path(__file__).resolve().parents[1] / "scripts" / "run_eth_nextgen_micro_executor.py" spec = importlib.util.spec_from_file_location("run_eth_nextgen_micro_executor", path) assert spec is not None module = importlib.util.module_from_spec(spec) assert spec.loader is not None sys.modules[spec.name] = module spec.loader.exec_module(module) return module executor = load_executor() def payload_with_nextgen_long(): return { "decision": {"active_engine": "nextgen", "selected_signal": "long"}, "execution_intent": {"entry_signal": "long", "entry_unit": 0.5, "target_position_known": False, "target_position": None}, "nextgen": { "data": {"decision_candle_ts": 1000}, "legs": [ {"leg_id": "a", "suggested_weight": 0.5, "signal": True, "exit_signal": False}, {"leg_id": "b", "suggested_weight": 0.5, "signal": False, "exit_signal": False}, ], }, } def test_executor_snapshot_does_not_render_orders_when_current_position_is_unknown(monkeypatch, tmp_path): monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr( executor, "account_current_position", lambda _: (TargetPosition(side="flat", unit=0.0, known=False, reason="no credentials"), {"account_error": "no credentials"}), ) snapshot = executor.build_snapshot(state_dir=tmp_path, margin_per_unit_usdt=1000.0, max_new_margin_usdt=500.0, max_total_margin_usdt=1000.0) assert snapshot["orders_submitted"] == 0 assert snapshot["target_position"]["known"] is True assert snapshot["target_position"]["unit"] == 0.5 assert snapshot["current_position"]["known"] is False assert snapshot["rendered_orders"] == [] def test_executor_snapshot_renders_order_body_when_positions_are_known(monkeypatch, tmp_path): monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr( executor, "account_current_position", lambda _: ( TargetPosition(side="flat", unit=0.0, known=True, reason="flat"), {"mark_price": 3000.0, "instrument_meta": {"ct_val": 0.1, "lot_sz": 1.0, "min_sz": 1.0}}, ), ) snapshot = executor.build_snapshot(state_dir=tmp_path, margin_per_unit_usdt=1000.0, max_new_margin_usdt=500.0, max_total_margin_usdt=1000.0) assert snapshot["orders_submitted"] == 0 assert snapshot["rendered_orders"] == [ { "action": "open", "margin_usdt": 500.0, "body": { "instId": "ETH-USDT-SWAP", "tdMode": "isolated", "side": "buy", "posSide": "long", "ordType": "market", "sz": "5", "clOrdId": "ethnm10001open", }, } ] def test_executor_uses_executor_state_file(monkeypatch, tmp_path): monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr( executor, "account_current_position", lambda _: ( TargetPosition(side="flat", unit=0.0, known=True, reason="flat"), {"mark_price": 3000.0, "instrument_meta": {"ct_val": 0.1, "lot_sz": 1.0, "min_sz": 1.0}}, ), ) (tmp_path / "runtime-state.json").write_text('{"last_candle_ts":1000,"micro_side":null,"nextgen_active_legs":[]}\n', encoding="utf-8") snapshot = executor.build_snapshot(state_dir=tmp_path, margin_per_unit_usdt=1000.0, max_new_margin_usdt=500.0, max_total_margin_usdt=1000.0) assert snapshot["target_position"]["unit"] == 0.5 class FakeClient: def __init__(self, *, fail_submit=False, positions=None): self.fail_submit = fail_submit self.positions = positions or [] self.submitted = [] self.leverage = [] def get_positions(self, symbol): return self.positions def get_instrument_meta(self, symbol): from okx_codex_trader.models import InstrumentMeta return InstrumentMeta(ct_val=0.1, lot_sz=1.0, min_sz=1.0) def get_last_price(self, symbol): return 3000.0 def ensure_hedge_mode(self): return None def set_leverage(self, *, symbol, leverage, pos_side): self.leverage.append((symbol, leverage, pos_side)) def submit_market_order_body(self, body): if self.fail_submit: raise ValueError("submit failed") self.submitted.append(body) return OrderResult( status="placed", order_id="1", symbol=body["instId"], side=body["side"], pos_side=body["posSide"], order_type=body["ordType"], size=float(body["sz"]), ) def test_live_submit_requires_both_flags(monkeypatch, tmp_path): monkeypatch.setenv("ETH_NEXTGEN_MARGIN_PER_UNIT_USDT", "100") monkeypatch.setenv("ETH_NEXTGEN_MAX_NEW_MARGIN_USDT", "100") monkeypatch.setenv("ETH_NEXTGEN_MAX_TOTAL_MARGIN_USDT", "200") monkeypatch.setattr(sys, "argv", ["executor", "--state-dir", str(tmp_path), "--submit-live"]) with pytest.raises(ValueError, match="must be used together"): executor.main() def test_dry_run_does_not_write_runtime_state(monkeypatch, tmp_path): monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr( executor, "account_current_position", lambda _: ( TargetPosition(side="flat", unit=0.0, known=True, reason="flat"), {"mark_price": 3000.0, "instrument_meta": {"ct_val": 0.1, "lot_sz": 1.0, "min_sz": 1.0}}, ), ) executor.build_snapshot(state_dir=tmp_path, margin_per_unit_usdt=1000.0, max_new_margin_usdt=500.0, max_total_margin_usdt=1000.0) assert not (tmp_path / executor.EXECUTOR_STATE_FILENAME).exists() def test_live_noop_writes_state_and_submits_zero_orders(monkeypatch, tmp_path): from okx_codex_trader.models import Position client = FakeClient(positions=[Position(symbol="ETH-USDT-SWAP", pos_side="long", size=1.0, avg_price=3000.0)]) monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr(executor, "OkxClient", lambda _: client) monkeypatch.setattr(executor, "load_config", lambda: object()) (tmp_path / executor.EXECUTOR_STATE_FILENAME).write_text( '{"last_candle_ts":1000,"micro_side":null,"nextgen_active_legs":["a"]}\n', encoding="utf-8", ) snapshot = executor.execute_live_once( state_dir=tmp_path, margin_per_unit_usdt=200.0, max_new_margin_usdt=100.0, max_total_margin_usdt=200.0, ) assert snapshot["orders_submitted"] == 0 assert client.submitted == [] assert (tmp_path / executor.EXECUTOR_STATE_FILENAME).exists() def test_live_submit_calls_okx_and_writes_state_after_success(monkeypatch, tmp_path): client = FakeClient() monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr(executor, "OkxClient", lambda _: client) monkeypatch.setattr(executor, "load_config", lambda: object()) snapshot = executor.execute_live_once( state_dir=tmp_path, margin_per_unit_usdt=200.0, max_new_margin_usdt=100.0, max_total_margin_usdt=200.0, ) assert snapshot["orders_submitted"] == 1 assert client.submitted[0]["clOrdId"] == "ethnm10001open" assert client.leverage == [("ETH-USDT-SWAP", 3, "long")] assert (tmp_path / executor.EXECUTOR_STATE_FILENAME).exists() assert (tmp_path / executor.EXECUTOR_EVENTS_FILENAME).exists() def test_live_submit_failure_does_not_write_runtime_state(monkeypatch, tmp_path): client = FakeClient(fail_submit=True) monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr(executor, "OkxClient", lambda _: client) monkeypatch.setattr(executor, "load_config", lambda: object()) with pytest.raises(ValueError, match="submit failed"): executor.execute_live_once( state_dir=tmp_path, margin_per_unit_usdt=200.0, max_new_margin_usdt=100.0, max_total_margin_usdt=200.0, ) assert not (tmp_path / executor.EXECUTOR_STATE_FILENAME).exists() assert (tmp_path / executor.EXECUTOR_EVENTS_FILENAME).exists() def test_live_unknown_position_does_not_write_runtime_state(monkeypatch, tmp_path): client = FakeClient() monkeypatch.setattr(executor.eth_nextgen_micro, "build_payload", payload_with_nextgen_long) monkeypatch.setattr(executor, "OkxClient", lambda _: client) monkeypatch.setattr(executor, "load_config", lambda: object()) monkeypatch.setattr( executor, "account_current_position_with_client", lambda _client, _margin: ( TargetPosition(side="flat", unit=0.0, known=False, reason="unknown"), {"account_error": "unknown"}, ), ) with pytest.raises(ValueError, match="must both be known"): executor.execute_live_once( state_dir=tmp_path, margin_per_unit_usdt=200.0, max_new_margin_usdt=100.0, max_total_margin_usdt=200.0, ) assert not (tmp_path / executor.EXECUTOR_STATE_FILENAME).exists() assert (tmp_path / executor.EXECUTOR_EVENTS_FILENAME).exists()