| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- import subprocess
- import pytest
- from okx_codex_trader.codex_analyzer import analyze_with_codex
- from okx_codex_trader.models import Candle
- def sample_candles() -> list[Candle]:
- return [
- Candle(symbol="BTC-USDT-SWAP", ts=1, open=100.0, high=105.0, low=99.0, close=104.0, volume=10.0),
- Candle(symbol="BTC-USDT-SWAP", ts=2, open=104.0, high=106.0, low=103.0, close=105.0, volume=12.0),
- Candle(symbol="BTC-USDT-SWAP", ts=3, open=105.0, high=107.0, low=104.0, close=106.0, volume=11.0),
- ]
- def fake_runner(stdout: str):
- calls: list[tuple[object, bool, bool, bool]] = []
- def runner(command, capture_output: bool, text: bool, check: bool):
- calls.append((command, capture_output, text, check))
- return subprocess.CompletedProcess(command, 0, stdout=stdout, stderr="")
- runner.calls = calls
- return runner
- def fake_which(path: str = "/tmp/fake-codex"):
- calls: list[str] = []
- def which(name: str) -> str:
- calls.append(name)
- return path
- which.calls = calls
- return which
- def missing_which(name: str) -> None:
- assert name == "codex"
- return None
- 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")
- which = fake_which()
- with pytest.raises(ValueError):
- analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner, which=which)
- assert which.calls == ["codex"]
- 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"}'
- )
- which = fake_which()
- with pytest.raises(ValueError):
- analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner, which=which)
- assert which.calls == ["codex"]
- def test_analyzer_returns_valid_trade_signal():
- runner = fake_runner(
- stdout='{"action":"short","confidence":0.6,"leverage":2,"entry_price":101.5,"take_profit_price":99.0,"stop_loss_price":103.0,"reason":"trend"}'
- )
- which = fake_which()
- signal = analyze_with_codex(
- candles=sample_candles(),
- symbol="BTC-USDT-SWAP",
- bar="1H",
- runner=runner,
- which=which,
- )
- assert signal.action == "short"
- assert signal.confidence == 0.6
- assert signal.leverage == 2
- assert signal.entry_price == 101.5
- assert signal.take_profit_price == 99.0
- assert signal.stop_loss_price == 103.0
- assert signal.reason == "trend"
- assert which.calls == ["codex"]
- assert runner.calls
- assert runner.calls[0][0][0:2] == ["codex", "exec"]
- assert runner.calls[0][1:] == (True, True, False)
- prompt = runner.calls[0][0][2]
- assert "return exactly one JSON object" in prompt
- assert "Do not output markdown, prose, or code fences." in prompt
- assert '"action", "confidence", "leverage", "entry_price", "take_profit_price", "stop_loss_price", "reason"' in prompt
- assert 'Valid actions are "long", "short", and "flat".' in prompt
- assert 'candles: [{"symbol":"BTC-USDT-SWAP"' in prompt
|