| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- 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": "ethnm-1000-1-open",
- },
- }
- ]
- 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"] == "ethnm-1000-1-open"
- 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()
|