Bläddra i källkod

feat: add codex analyzer integration

lxy 1 månad sedan
förälder
incheckning
e668d93fac
2 ändrade filer med 114 tillägg och 0 borttagningar
  1. 44 0
      okx_codex_trader/codex_analyzer.py
  2. 70 0
      tests/test_codex_analyzer.py

+ 44 - 0
okx_codex_trader/codex_analyzer.py

@@ -0,0 +1,44 @@
+import json
+import shutil
+import subprocess
+from dataclasses import asdict
+from typing import Callable
+
+from okx_codex_trader.models import Candle, TradeSignal
+from okx_codex_trader.strategy import validate_signal
+
+
+def analyze_with_codex(
+    candles: list[Candle],
+    symbol: str,
+    bar: str,
+    runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
+    which: Callable[[str], str | None] = shutil.which,
+) -> TradeSignal:
+    if which("codex") is None:
+        raise FileNotFoundError("codex executable was not found on PATH")
+
+    prompt = (
+        "Analyze these market candles and return exactly one JSON object.\n"
+        'Do not output markdown, prose, or code fences.\n'
+        'The JSON keys must be exactly: "action", "confidence", "leverage", '
+        '"entry_price", "take_profit_price", "stop_loss_price", "reason".\n'
+        'Valid actions are "long", "short", and "flat".\n'
+        "Confidence must be a number from 0 to 1.\n"
+        "Leverage must be an integer from 1 to 3.\n"
+        "Price fields must be numbers or null.\n"
+        f"symbol: {symbol}\n"
+        f"bar: {bar}\n"
+        f"candles: {json.dumps([asdict(candle) for candle in candles], separators=(',', ':'))}"
+    )
+    completed = runner(["codex", "exec", prompt], capture_output=True, text=True, check=False)
+
+    try:
+        payload = json.loads(completed.stdout)
+    except json.JSONDecodeError as exc:
+        raise ValueError("codex output is not valid JSON") from exc
+
+    if not isinstance(payload, dict):
+        raise ValueError("codex output is not valid JSON")
+
+    return validate_signal(payload)

+ 70 - 0
tests/test_codex_analyzer.py

@@ -0,0 +1,70 @@
+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 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")
+
+    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)
+
+
+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"}'
+    )
+
+    signal = analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner)
+
+    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 runner.calls
+    assert runner.calls[0][0][0:2] == ["codex", "exec"]
+    assert runner.calls[0][1:] == (True, True, False)