2026-04-16-okx-codex-trader.md 22 KB

OKX Codex Trader Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a minimal Python CLI that fetches OKX swap candles, backtests a fixed 10/20 SMA strategy, asks local Codex CLI for a validated trading signal, and sends OKX demo orders for BTC-USDT-SWAP and ETH-USDT-SWAP.

Architecture: The project is a small package rooted at okx_codex_trader/. cli.py owns argument parsing and command dispatch, okx_client.py owns OKX HTTP signing and REST calls, backtest.py owns deterministic SMA backtesting, and codex_analyzer.py owns subprocess execution of codex plus signal validation. Shared dataclasses live in models.py, and config.py enforces the runtime contract at the boundary.

Tech Stack: Python 3.11+, pytest, requests, argparse, dataclasses, subprocess, standard library JSON and HMAC utilities


File Structure

  • Create: /home/lxy/okx-codex-trader/pyproject.toml
    • project metadata and dependencies
  • Create: /home/lxy/okx-codex-trader/README.md
    • setup, environment variables, and command examples
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/__init__.py
    • package marker
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/config.py
    • required environment variables and runtime checks
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/models.py
    • normalized dataclasses for candles, signals, orders, positions, and backtest results
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/strategy.py
    • signal validation helpers and SMA crossover helpers
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/backtest.py
    • deterministic 10/20 SMA backtest engine with 10_000 USDT initial equity
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/okx_client.py
    • signed demo REST client, instrument lookup, candle fetch, position mode check, leverage set, order placement, positions fetch
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/codex_analyzer.py
    • local codex subprocess call and JSON validation
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/cli.py
    • command entrypoint and JSON stdout formatting
  • Create: /home/lxy/okx-codex-trader/tests/test_config.py
    • config boundary tests
  • Create: /home/lxy/okx-codex-trader/tests/test_strategy.py
    • signal validation tests
  • Create: /home/lxy/okx-codex-trader/tests/test_backtest.py
    • fixed-series backtest tests
  • Create: /home/lxy/okx-codex-trader/tests/test_okx_client.py
    • signed request, demo header, sizing, and hedge-mode tests
  • Create: /home/lxy/okx-codex-trader/tests/test_codex_analyzer.py
    • subprocess and JSON parsing tests
  • Create: /home/lxy/okx-codex-trader/tests/test_cli.py
    • command parsing and dispatch tests

Task 1: Project Skeleton

Files:

  • Create: /home/lxy/okx-codex-trader/pyproject.toml
  • Create: /home/lxy/okx-codex-trader/README.md
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/__init__.py

  • [ ] Step 1: Write the failing packaging smoke test

import okx_codex_trader


def test_package_exports_version():
    assert okx_codex_trader.__version__ == "0.1.0"
  • Step 2: Run test to verify it fails

Run: pytest /home/lxy/okx-codex-trader/tests/test_config.py -k package_exports_version -v Expected: FAIL because the package module does not exist yet

  • Step 3: Write minimal package metadata
[project]
name = "okx-codex-trader"
version = "0.1.0"
dependencies = ["requests>=2.32,<3"]

[project.scripts]
okx-codex-trader = "okx_codex_trader.cli:main"
__version__ = "0.1.0"
git -C /home/lxy/okx-codex-trader init
  • Step 4: Run test to verify it passes

Run: pytest /home/lxy/okx-codex-trader/tests/test_config.py -k package_exports_version -v Expected: PASS

  • Step 5: Commit
git -C /home/lxy/okx-codex-trader add pyproject.toml README.md okx_codex_trader/__init__.py tests/test_config.py
git -C /home/lxy/okx-codex-trader commit -m "chore: scaffold okx codex trader project"

Task 2: Config And Models

Files:

  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/config.py
  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/models.py
  • Test: /home/lxy/okx-codex-trader/tests/test_config.py
  • Test: /home/lxy/okx-codex-trader/tests/test_strategy.py

  • [ ] Step 1: Write the failing config and signal validation tests

def test_load_config_requires_okx_credentials(monkeypatch):
    monkeypatch.delenv("OKX_API_KEY", raising=False)
    monkeypatch.delenv("OKX_API_SECRET", raising=False)
    monkeypatch.delenv("OKX_API_PASSPHRASE", raising=False)

    with pytest.raises(ValueError):
        load_config()


def test_validate_signal_rejects_leverage_out_of_range():
    with pytest.raises(ValueError):
        validate_signal({"action": "long", "confidence": 0.9, "leverage": 4, "entry_price": None, "take_profit_price": None, "stop_loss_price": None, "reason": "x"})


def test_validate_signal_rejects_unknown_action():
    with pytest.raises(ValueError):
        validate_signal({"action": "hold", "confidence": 0.9, "leverage": 2, "entry_price": None, "take_profit_price": None, "stop_loss_price": None, "reason": "x"})


def test_validate_signal_rejects_confidence_out_of_range():
    with pytest.raises(ValueError):
        validate_signal({"action": "long", "confidence": 1.2, "leverage": 2, "entry_price": None, "take_profit_price": None, "stop_loss_price": None, "reason": "x"})


def test_validate_signal_requires_full_shape():
    with pytest.raises(ValueError):
        validate_signal({"action": "long", "confidence": 0.9, "leverage": 2})
  • Step 2: Run tests to verify they fail

Run: pytest /home/lxy/okx-codex-trader/tests/test_config.py /home/lxy/okx-codex-trader/tests/test_strategy.py -v Expected: FAIL because load_config and validate_signal are not implemented

  • Step 3: Write minimal config and model code
@dataclass(frozen=True)
class Candle:
    symbol: str
    ts: int
    open: float
    high: float
    low: float
    close: float
    volume: float


@dataclass(frozen=True)
class TradeSignal:
    action: Literal["long", "short", "flat"]
    confidence: float
    leverage: int
    entry_price: float | None
    take_profit_price: float | None
    stop_loss_price: float | None
    reason: str


@dataclass(frozen=True)
class InstrumentMeta:
    ct_val: float
    lot_sz: float
    min_sz: float


@dataclass(frozen=True)
class Position:
    symbol: str
    pos_side: str
    size: float
    avg_price: float


@dataclass(frozen=True)
class OrderResult:
    status: str
    order_id: str | None
    symbol: str
    side: str | None
    pos_side: str | None
    order_type: str | None
    size: float | None


@dataclass(frozen=True)
class BacktestTrade:
    direction: str
    entry_price: float
    exit_price: float
    margin_used: float
    ending_equity: float


@dataclass(frozen=True)
class BacktestResult:
    initial_equity: float
    ending_equity: float
    total_return: float
    max_drawdown: float
    win_rate: float
    trade_count: int
    trades: list[BacktestTrade]

    def to_dict(self) -> dict[str, object]:
        ...


@dataclass(frozen=True)
class Config:
    api_key: str
    api_secret: str
    api_passphrase: str


def load_config(env: Mapping[str, str] | None = None) -> Config:
    source = os.environ if env is None else env
    ...
  • Step 4: Run tests to verify they pass

Run: pytest /home/lxy/okx-codex-trader/tests/test_config.py /home/lxy/okx-codex-trader/tests/test_strategy.py -v Expected: PASS

  • Step 5: Commit
git -C /home/lxy/okx-codex-trader add okx_codex_trader/config.py okx_codex_trader/models.py okx_codex_trader/strategy.py tests/test_config.py tests/test_strategy.py
git -C /home/lxy/okx-codex-trader commit -m "feat: add config and signal validation models"

Task 3: Backtest Engine

Files:

  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/backtest.py
  • Modify: /home/lxy/okx-codex-trader/okx_codex_trader/strategy.py
  • Test: /home/lxy/okx-codex-trader/tests/test_backtest.py

  • [ ] Step 1: Write the failing SMA backtest test

def test_backtest_runs_fixed_sma_crossover_series():
    candles = build_crossing_series()

    result = run_backtest(candles=candles, leverage=2)

    assert result.initial_equity == 10_000
    assert result.trade_count == 2
    assert result.trades[0].entry_price == candles[21].open
    assert result.trades[0].exit_price == candles[30].open
    assert result.trades[0].margin_used == 10_000
    assert result.trades[1].margin_used == result.trades[0].ending_equity
    assert result.ending_equity == 4_888.888888888889
    assert result.total_return == -0.5111111111111112
    assert "total_return" in result.to_dict()
    assert "max_drawdown" in result.to_dict()
    assert result.win_rate == 0.5
  • Step 2: Run test to verify it fails

Run: pytest /home/lxy/okx-codex-trader/tests/test_backtest.py -v Expected: FAIL because run_backtest does not exist

  • Step 3: Write minimal backtest implementation
def run_backtest(candles: list[Candle], leverage: int) -> BacktestResult:
    fast = simple_moving_average(candles, 10)
    slow = simple_moving_average(candles, 20)
    ...
  • Step 4: Run test to verify it passes

Run: pytest /home/lxy/okx-codex-trader/tests/test_backtest.py -v Expected: PASS

  • Step 5: Commit
git -C /home/lxy/okx-codex-trader add okx_codex_trader/backtest.py okx_codex_trader/strategy.py tests/test_backtest.py
git -C /home/lxy/okx-codex-trader commit -m "feat: add deterministic sma backtest engine"

Task 4: OKX Demo Client

Files:

  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/okx_client.py
  • Test: /home/lxy/okx-codex-trader/tests/test_okx_client.py

  • [ ] Step 1: Write the failing client tests

def test_signed_demo_request_attaches_headers():
    session = DummySession()
    client = OkxClient(config=sample_config(), session=session)

    client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=20)

    request = session.last_request
    assert request.headers["x-simulated-trading"] == "1"
    assert request.headers["OK-ACCESS-KEY"] == "key"


def test_build_contract_size_rounds_down_to_lot_size():
    metadata = InstrumentMeta(ct_val=0.01, lot_sz=0.1, min_sz=0.1)
    assert build_contract_size(notional=251, price=25_000, metadata=metadata) == 1.0


def test_build_contract_size_fails_below_min_size():
    metadata = InstrumentMeta(ct_val=0.01, lot_sz=1, min_sz=5)
    with pytest.raises(ValueError):
        build_contract_size(notional=250, price=25_100, metadata=metadata)


def test_market_order_fetches_latest_price_before_sizing():
    session = DummySession([
        instrument_response(),
        ticker_response(last="25000"),
        account_config_response(pos_mode="long_short_mode"),
        leverage_response(),
        place_order_response(),
    ])
    client = OkxClient(config=sample_config(), session=session)

    client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)

    assert session.request_paths == [
        "/api/v5/public/instruments",
        "/api/v5/market/ticker",
        "/api/v5/account/config",
        "/api/v5/account/set-leverage",
        "/api/v5/trade/order",
    ]


def test_place_demo_order_fails_when_not_hedge_mode():
    session = DummySession([
        instrument_response(),
        ticker_response(last="25000"),
        account_config_response(pos_mode="net_mode"),
    ])
    client = OkxClient(config=sample_config(), session=session)

    with pytest.raises(ValueError):
        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)


def test_limit_short_order_uses_sell_and_short_pos_side():
    session = DummySession([
        instrument_response(),
        account_config_response(pos_mode="long_short_mode"),
        leverage_response(),
        place_order_response(),
    ])
    client = OkxClient(config=sample_config(), session=session)

    client.place_demo_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)

    order_request = session.last_json_body
    assert order_request["ordType"] == "limit"
    assert order_request["side"] == "sell"
    assert order_request["posSide"] == "short"
    assert order_request["px"] == "25000"
    assert session.request_bodies[2]["lever"] == "2"
    assert session.request_bodies[2]["mgnMode"] == "isolated"


def test_flat_signal_returns_noop_without_order_submission():
    session = DummySession([])
    client = OkxClient(config=sample_config(), session=session)

    result = client.place_demo_order(symbol="BTC-USDT-SWAP", signal=flat_signal(), margin_usdt=100)

    assert result.status == "noop"
    assert session.request_paths == []


def test_place_demo_order_sends_computed_sz_and_ignores_tp_sl_fields():
    session = DummySession([
        instrument_response(),
        ticker_response(last="25000"),
        account_config_response(pos_mode="long_short_mode"),
        leverage_response(),
        place_order_response(),
    ])
    client = OkxClient(config=sample_config(), session=session)

    client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)

    order_request = session.last_json_body
    assert order_request["sz"] == "8"
    assert "tpTriggerPx" not in order_request
    assert "slTriggerPx" not in order_request


def test_okx_error_payload_raises_value_error():
    session = DummySession([error_response(code="51000", msg="parameter error")])
    client = OkxClient(config=sample_config(), session=session)

    with pytest.raises(ValueError):
        client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=20)


def test_get_positions_returns_normalized_positions():
    session = DummySession([positions_response()])
    client = OkxClient(config=sample_config(), session=session)

    positions = client.get_positions(symbol="BTC-USDT-SWAP")

    assert positions[0].symbol == "BTC-USDT-SWAP"
  • Step 2: Run tests to verify they fail

Run: pytest /home/lxy/okx-codex-trader/tests/test_okx_client.py -v Expected: FAIL because OkxClient and sizing helpers do not exist

  • Step 3: Write minimal client implementation
class OkxClient:
    base_url = "https://www.okx.com"

    def get_candles(self, symbol: str, bar: str, limit: int) -> list[Candle]: ...
    def get_instrument_meta(self, symbol: str) -> InstrumentMeta: ...
    def get_last_price(self, symbol: str) -> float: ...
    def ensure_hedge_mode(self) -> None: ...
    def set_leverage(self, symbol: str, leverage: int, pos_side: str) -> None: ...
    def place_demo_order(self, symbol: str, signal: TradeSignal, margin_usdt: float) -> OrderResult: ...
    def get_positions(self, symbol: str) -> list[Position]: ...
  • Step 4: Run tests to verify they pass

Run: pytest /home/lxy/okx-codex-trader/tests/test_okx_client.py -v Expected: PASS

  • Step 5: Commit
git -C /home/lxy/okx-codex-trader add okx_codex_trader/okx_client.py tests/test_okx_client.py
git -C /home/lxy/okx-codex-trader commit -m "feat: add okx demo client and contract sizing"

Task 5: Codex Analyzer

Files:

  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/codex_analyzer.py
  • Modify: /home/lxy/okx-codex-trader/okx_codex_trader/strategy.py
  • Test: /home/lxy/okx-codex-trader/tests/test_codex_analyzer.py

  • [ ] Step 1: Write the failing analyzer tests

def test_analyzer_fails_when_codex_is_missing():
    with pytest.raises(FileNotFoundError):
        analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", which=missing_which)


def test_analyzer_rejects_non_json_output():
    runner = fake_runner(stdout="not json")
    with pytest.raises(ValueError):
        analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner)


def test_analyzer_rejects_json_leverage_out_of_range():
    runner = fake_runner(stdout='{"action":"long","confidence":0.8,"leverage":4,"entry_price":null,"take_profit_price":null,"stop_loss_price":null,"reason":"x"}')
    with pytest.raises(ValueError):
        analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner)
  • Step 2: Run tests to verify they fail

Run: pytest /home/lxy/okx-codex-trader/tests/test_codex_analyzer.py -v Expected: FAIL because analyze_with_codex does not exist

  • Step 3: Write minimal analyzer implementation
def analyze_with_codex(...):
    command = ["codex", "exec", prompt]
    completed = runner(command, capture_output=True, text=True, check=False)
    ...
  • Step 4: Run tests to verify they pass

Run: pytest /home/lxy/okx-codex-trader/tests/test_codex_analyzer.py -v Expected: PASS

  • Step 5: Commit
git -C /home/lxy/okx-codex-trader add okx_codex_trader/codex_analyzer.py okx_codex_trader/strategy.py tests/test_codex_analyzer.py
git -C /home/lxy/okx-codex-trader commit -m "feat: add codex analyzer integration"

Task 6: CLI Commands

Files:

  • Create: /home/lxy/okx-codex-trader/okx_codex_trader/cli.py
  • Modify: /home/lxy/okx-codex-trader/README.md
  • Test: /home/lxy/okx-codex-trader/tests/test_cli.py

  • [ ] Step 1: Write the failing CLI tests

def build_main_with_stubs():
    client = fake_client()
    main = main_factory(
        load_config=lambda: sample_config(),
        client_factory=lambda config: client,
        analyze_fn=fake_analyze_with_codex,
        write_text=real_write_text,
    )
    return main, client


def test_fetch_history_prints_candle_json(capsys):
    main, client = build_main_with_stubs()
    exit_code = main(["fetch-history", "--symbol", "BTC-USDT-SWAP", "--bar", "1H", "--limit", "20"])
    assert exit_code == 0
    assert client.get_candles_called_with == ("BTC-USDT-SWAP", "1H", 20)
    assert '"symbol": "BTC-USDT-SWAP"' in capsys.readouterr().out


def test_backtest_prints_summary_json(capsys):
    main, client = build_main_with_stubs()
    exit_code = main(["backtest", "--symbol", "BTC-USDT-SWAP", "--bar", "1H", "--limit", "50", "--leverage", "2"])
    assert exit_code == 0
    assert client.get_candles_called_with == ("BTC-USDT-SWAP", "1H", 50)
    assert '"trade_count"' in capsys.readouterr().out


def test_analyze_writes_output_file_and_stdout(tmp_path, capsys):
    main, client = build_main_with_stubs()
    output_file = tmp_path / "signal.json"
    exit_code = main([
        "analyze",
        "--symbol", "BTC-USDT-SWAP",
        "--bar", "1H",
        "--limit", "20",
        "--output-file", str(output_file),
    ])
    assert exit_code == 0
    assert output_file.exists()
    assert client.get_candles_called_with == ("BTC-USDT-SWAP", "1H", 20)
    assert '"action"' in capsys.readouterr().out


def test_paper_order_reads_signal_file_and_outputs_order_json(tmp_path, capsys):
    main, client = build_main_with_stubs()
    signal_file = tmp_path / "signal.json"
    signal_file.write_text(json.dumps(valid_signal()))

    exit_code = main([
        "paper-order",
        "--symbol", "BTC-USDT-SWAP",
        "--signal-file", str(signal_file),
        "--margin-usdt", "100",
    ])

    assert exit_code == 0
    assert client.place_demo_order_called
    assert '"status"' in capsys.readouterr().out


def test_positions_prints_position_json(capsys):
    main, client = build_main_with_stubs()
    exit_code = main(["positions", "--symbol", "BTC-USDT-SWAP"])
    assert exit_code == 0
    assert client.get_positions_called_with == "BTC-USDT-SWAP"
    assert '"symbol": "BTC-USDT-SWAP"' in capsys.readouterr().out


def test_cli_rejects_unsupported_symbol():
    main, _ = build_main_with_stubs()
    with pytest.raises(SystemExit):
        main(["fetch-history", "--symbol", "SOL-USDT-SWAP", "--bar", "1H", "--limit", "20"])


def test_cli_rejects_leverage_out_of_range():
    main, _ = build_main_with_stubs()
    with pytest.raises(SystemExit):
        main(["backtest", "--symbol", "BTC-USDT-SWAP", "--bar", "1H", "--limit", "50", "--leverage", "4"])
  • Step 2: Run tests to verify they fail

Run: pytest /home/lxy/okx-codex-trader/tests/test_cli.py -v Expected: FAIL because main and command dispatch do not exist

  • Step 3: Write minimal CLI implementation
def main(argv: Sequence[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    ...
  • Step 4: Run tests to verify they pass

Run: pytest /home/lxy/okx-codex-trader/tests/test_cli.py -v Expected: PASS

  • Step 5: Run the full test suite and commit

Run: pytest /home/lxy/okx-codex-trader/tests -v Expected: PASS

git -C /home/lxy/okx-codex-trader add okx_codex_trader/cli.py README.md tests/test_cli.py
git -C /home/lxy/okx-codex-trader commit -m "feat: add cli commands for okx codex trader"

Task 7: End-To-End Local Verification

Files:

  • Modify: /home/lxy/okx-codex-trader/README.md

  • [ ] Step 1: Document the exact runtime commands

python -m okx_codex_trader.cli fetch-history --symbol BTC-USDT-SWAP --bar 1H --limit 50
python -m okx_codex_trader.cli backtest --symbol BTC-USDT-SWAP --bar 1H --limit 200 --leverage 2
python -m okx_codex_trader.cli analyze --symbol BTC-USDT-SWAP --bar 1H --limit 50 --output-file signal.json
python -m okx_codex_trader.cli paper-order --symbol BTC-USDT-SWAP --signal-file signal.json --margin-usdt 100
python -m okx_codex_trader.cli positions --symbol BTC-USDT-SWAP
  • Step 2: Run the test suite one more time

Run: pytest /home/lxy/okx-codex-trader/tests -v Expected: PASS

  • Step 3: Run one dry local command that does not require OKX credentials

Run: python -m okx_codex_trader.cli --help Expected: usage text printed successfully

  • Step 4: Record any unverified external dependencies

Expected note:

  • OKX demo credentials were not exercised in automated tests
  • local codex runtime behavior outside mocked subprocess tests still requires manual verification

  • [ ] Step 5: Commit

git -C /home/lxy/okx-codex-trader add README.md
git -C /home/lxy/okx-codex-trader commit -m "docs: add local verification commands"