test_codex_analyzer.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import subprocess
  2. import pytest
  3. from okx_codex_trader.codex_analyzer import analyze_with_codex
  4. from okx_codex_trader.models import Candle
  5. def sample_candles() -> list[Candle]:
  6. return [
  7. Candle(symbol="BTC-USDT-SWAP", ts=1, open=100.0, high=105.0, low=99.0, close=104.0, volume=10.0),
  8. Candle(symbol="BTC-USDT-SWAP", ts=2, open=104.0, high=106.0, low=103.0, close=105.0, volume=12.0),
  9. Candle(symbol="BTC-USDT-SWAP", ts=3, open=105.0, high=107.0, low=104.0, close=106.0, volume=11.0),
  10. ]
  11. def fake_runner(stdout: str, returncode: int = 0):
  12. calls: list[tuple[object, bool, bool, bool]] = []
  13. def runner(command, capture_output: bool, text: bool, check: bool):
  14. calls.append((command, capture_output, text, check))
  15. return subprocess.CompletedProcess(command, returncode, stdout=stdout, stderr="")
  16. runner.calls = calls
  17. return runner
  18. def fake_which(path: str = "/tmp/fake-codex"):
  19. calls: list[str] = []
  20. def which(name: str) -> str:
  21. calls.append(name)
  22. return path
  23. which.calls = calls
  24. return which
  25. def missing_which(name: str) -> None:
  26. assert name == "codex"
  27. return None
  28. def test_analyzer_fails_when_codex_is_missing():
  29. with pytest.raises(FileNotFoundError):
  30. analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", which=missing_which)
  31. def test_analyzer_rejects_non_json_output():
  32. runner = fake_runner(stdout="not json")
  33. which = fake_which()
  34. with pytest.raises(ValueError):
  35. analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner, which=which)
  36. assert which.calls == ["codex"]
  37. def test_analyzer_rejects_json_leverage_out_of_range():
  38. runner = fake_runner(
  39. stdout='{"action":"long","confidence":0.8,"leverage":4,"entry_price":null,"take_profit_price":null,"stop_loss_price":null,"reason":"x"}'
  40. )
  41. which = fake_which()
  42. with pytest.raises(ValueError):
  43. analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner, which=which)
  44. assert which.calls == ["codex"]
  45. def test_analyzer_rejects_non_zero_exit_with_non_json_stdout():
  46. runner = fake_runner(stdout="not json", returncode=1)
  47. which = fake_which()
  48. with pytest.raises(ValueError, match="codex execution failed"):
  49. analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner, which=which)
  50. assert which.calls == ["codex"]
  51. def test_analyzer_rejects_non_zero_exit_with_json_stdout():
  52. runner = fake_runner(
  53. stdout='{"action":"long","confidence":0.8,"leverage":2,"entry_price":null,"take_profit_price":null,"stop_loss_price":null,"reason":"x"}',
  54. returncode=1,
  55. )
  56. which = fake_which()
  57. with pytest.raises(ValueError, match="codex execution failed"):
  58. analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner, which=which)
  59. assert which.calls == ["codex"]
  60. def test_analyzer_returns_valid_trade_signal():
  61. runner = fake_runner(
  62. stdout='{"action":"short","confidence":0.6,"leverage":2,"entry_price":101.5,"take_profit_price":99.0,"stop_loss_price":103.0,"reason":"trend"}'
  63. )
  64. which = fake_which()
  65. signal = analyze_with_codex(
  66. candles=sample_candles(),
  67. symbol="BTC-USDT-SWAP",
  68. bar="1H",
  69. runner=runner,
  70. which=which,
  71. )
  72. assert signal.action == "short"
  73. assert signal.confidence == 0.6
  74. assert signal.leverage == 2
  75. assert signal.entry_price == 101.5
  76. assert signal.take_profit_price == 99.0
  77. assert signal.stop_loss_price == 103.0
  78. assert signal.reason == "trend"
  79. assert which.calls == ["codex"]
  80. assert runner.calls
  81. assert runner.calls[0][0][0:2] == ["codex", "exec"]
  82. assert runner.calls[0][1:] == (True, True, False)
  83. prompt = runner.calls[0][0][2]
  84. assert "return exactly one JSON object" in prompt
  85. assert "Do not output markdown, prose, or code fences." in prompt
  86. assert '"action", "confidence", "leverage", "entry_price", "take_profit_price", "stop_loss_price", "reason"' in prompt
  87. assert 'Valid actions are "long", "short", and "flat".' in prompt
  88. assert 'candles: [{"symbol":"BTC-USDT-SWAP"' in prompt