|
|
@@ -25,6 +25,17 @@ def fake_runner(stdout: str):
|
|
|
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
|
|
|
@@ -37,26 +48,39 @@ def test_analyzer_fails_when_codex_is_missing():
|
|
|
|
|
|
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)
|
|
|
+ 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)
|
|
|
+ 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"}'
|
|
|
)
|
|
|
-
|
|
|
- signal = analyze_with_codex(candles=sample_candles(), symbol="BTC-USDT-SWAP", bar="1H", runner=runner)
|
|
|
+ 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
|
|
|
@@ -65,6 +89,13 @@ def test_analyzer_returns_valid_trade_signal():
|
|
|
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
|