|
|
@@ -0,0 +1,668 @@
|
|
|
+# 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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```toml
|
|
|
+[project]
|
|
|
+name = "okx-codex-trader"
|
|
|
+version = "0.1.0"
|
|
|
+dependencies = ["requests>=2.32,<3"]
|
|
|
+
|
|
|
+[project.scripts]
|
|
|
+okx-codex-trader = "okx_codex_trader.cli:main"
|
|
|
+```
|
|
|
+
|
|
|
+```python
|
|
|
+__version__ = "0.1.0"
|
|
|
+```
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+@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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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**
|
|
|
+
|
|
|
+```python
|
|
|
+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
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```md
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+git -C /home/lxy/okx-codex-trader add README.md
|
|
|
+git -C /home/lxy/okx-codex-trader commit -m "docs: add local verification commands"
|
|
|
+```
|