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
/home/lxy/okx-codex-trader/pyproject.toml
/home/lxy/okx-codex-trader/README.md
/home/lxy/okx-codex-trader/okx_codex_trader/__init__.py
/home/lxy/okx-codex-trader/okx_codex_trader/config.py
/home/lxy/okx-codex-trader/okx_codex_trader/models.py
/home/lxy/okx-codex-trader/okx_codex_trader/strategy.py
/home/lxy/okx-codex-trader/okx_codex_trader/backtest.py
10/20 SMA backtest engine with 10_000 USDT initial equity/home/lxy/okx-codex-trader/okx_codex_trader/okx_client.py
/home/lxy/okx-codex-trader/okx_codex_trader/codex_analyzer.py
codex subprocess call and JSON validation/home/lxy/okx-codex-trader/okx_codex_trader/cli.py
/home/lxy/okx-codex-trader/tests/test_config.py
/home/lxy/okx-codex-trader/tests/test_strategy.py
/home/lxy/okx-codex-trader/tests/test_backtest.py
/home/lxy/okx-codex-trader/tests/test_okx_client.py
/home/lxy/okx-codex-trader/tests/test_codex_analyzer.py
/home/lxy/okx-codex-trader/tests/test_cli.py
Files:
/home/lxy/okx-codex-trader/pyproject.toml/home/lxy/okx-codex-trader/README.mdCreate: /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"
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
[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
Run: pytest /home/lxy/okx-codex-trader/tests/test_config.py -k package_exports_version -v
Expected: PASS
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"
Files:
/home/lxy/okx-codex-trader/okx_codex_trader/config.py/home/lxy/okx-codex-trader/okx_codex_trader/models.py/home/lxy/okx-codex-trader/tests/test_config.pyTest: /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})
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
@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
...
Run: pytest /home/lxy/okx-codex-trader/tests/test_config.py /home/lxy/okx-codex-trader/tests/test_strategy.py -v
Expected: PASS
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"
Files:
/home/lxy/okx-codex-trader/okx_codex_trader/backtest.py/home/lxy/okx-codex-trader/okx_codex_trader/strategy.pyTest: /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
Run: pytest /home/lxy/okx-codex-trader/tests/test_backtest.py -v
Expected: FAIL because run_backtest does not exist
def run_backtest(candles: list[Candle], leverage: int) -> BacktestResult:
fast = simple_moving_average(candles, 10)
slow = simple_moving_average(candles, 20)
...
Run: pytest /home/lxy/okx-codex-trader/tests/test_backtest.py -v
Expected: PASS
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"
Files:
/home/lxy/okx-codex-trader/okx_codex_trader/okx_client.pyTest: /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"
Run: pytest /home/lxy/okx-codex-trader/tests/test_okx_client.py -v
Expected: FAIL because OkxClient and sizing helpers do not exist
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]: ...
Run: pytest /home/lxy/okx-codex-trader/tests/test_okx_client.py -v
Expected: PASS
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"
Files:
/home/lxy/okx-codex-trader/okx_codex_trader/codex_analyzer.py/home/lxy/okx-codex-trader/okx_codex_trader/strategy.pyTest: /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)
Run: pytest /home/lxy/okx-codex-trader/tests/test_codex_analyzer.py -v
Expected: FAIL because analyze_with_codex does not exist
def analyze_with_codex(...):
command = ["codex", "exec", prompt]
completed = runner(command, capture_output=True, text=True, check=False)
...
Run: pytest /home/lxy/okx-codex-trader/tests/test_codex_analyzer.py -v
Expected: PASS
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"
Files:
/home/lxy/okx-codex-trader/okx_codex_trader/cli.py/home/lxy/okx-codex-trader/README.mdTest: /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"])
Run: pytest /home/lxy/okx-codex-trader/tests/test_cli.py -v
Expected: FAIL because main and command dispatch do not exist
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
...
Run: pytest /home/lxy/okx-codex-trader/tests/test_cli.py -v
Expected: PASS
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"
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
Run: pytest /home/lxy/okx-codex-trader/tests -v
Expected: PASS
Run: python -m okx_codex_trader.cli --help
Expected: usage text printed successfully
Expected note:
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"