TradingAgents/orchestrator/tests/test_application_service.py

171 lines
5.8 KiB
Python

from datetime import datetime, timezone
import pytest
import orchestrator.orchestrator as orchestrator_module
from orchestrator.config import OrchestratorConfig
from orchestrator.contracts.error_taxonomy import ReasonCode
from orchestrator.contracts.result_contract import CombinedSignalFailure
from orchestrator.signals import Signal
def _signal(
source: str,
*,
direction: int,
confidence: float,
metadata: dict | None = None,
reason_code: str | None = None,
) -> Signal:
return Signal(
ticker="AAPL",
direction=direction,
confidence=confidence,
source=source,
timestamp=datetime.now(timezone.utc),
metadata=metadata or {},
reason_code=reason_code,
)
def test_trading_orchestrator_degrades_to_llm_only_when_quant_has_error(monkeypatch):
class FakeQuantRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal("quant", direction=1, confidence=0.8, metadata={"error": "db unavailable"})
class FakeLLMRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal("llm", direction=-1, confidence=0.9)
monkeypatch.setattr(orchestrator_module, "QuantRunner", FakeQuantRunner)
monkeypatch.setattr(orchestrator_module, "LLMRunner", FakeLLMRunner)
result = orchestrator_module.TradingOrchestrator(
OrchestratorConfig(quant_backtest_path="/tmp/quant")
).get_combined_signal("AAPL", "2026-04-11")
assert result.direction == -1
assert result.quant_signal is None
assert result.llm_signal is not None
assert result.llm_signal.source == "llm"
def test_trading_orchestrator_degrades_to_quant_only_when_llm_has_error(monkeypatch):
class FakeQuantRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal("quant", direction=1, confidence=0.8)
class FakeLLMRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal("llm", direction=0, confidence=0.0, metadata={"error": "timeout"})
monkeypatch.setattr(orchestrator_module, "QuantRunner", FakeQuantRunner)
monkeypatch.setattr(orchestrator_module, "LLMRunner", FakeLLMRunner)
result = orchestrator_module.TradingOrchestrator(
OrchestratorConfig(quant_backtest_path="/tmp/quant")
).get_combined_signal("AAPL", "2026-04-11")
assert result.direction == 1
assert result.quant_signal is not None
assert result.quant_signal.source == "quant"
assert result.llm_signal is None
def test_trading_orchestrator_raises_when_both_sources_degrade(monkeypatch):
class FakeQuantRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal(
"quant",
direction=0,
confidence=0.0,
metadata={"error": "no data"},
reason_code=ReasonCode.QUANT_NO_DATA.value,
)
class FakeLLMRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal("llm", direction=0, confidence=0.0, metadata={"error": "timeout"})
monkeypatch.setattr(orchestrator_module, "QuantRunner", FakeQuantRunner)
monkeypatch.setattr(orchestrator_module, "LLMRunner", FakeLLMRunner)
with pytest.raises(CombinedSignalFailure) as exc_info:
orchestrator_module.TradingOrchestrator(
OrchestratorConfig(quant_backtest_path="/tmp/quant")
).get_combined_signal("AAPL", "2026-04-11")
assert str(exc_info.value) == "both quant and llm signals are None"
assert exc_info.value.reason_codes[0] == ReasonCode.QUANT_NO_DATA.value
assert exc_info.value.reason_codes[-1] == ReasonCode.BOTH_SIGNALS_UNAVAILABLE.value
assert exc_info.value.source_diagnostics["quant"]["reason_code"] == ReasonCode.QUANT_NO_DATA.value
def test_trading_orchestrator_surfaces_provider_mismatch_summary_when_llm_degrades(monkeypatch):
class FakeQuantRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal("quant", direction=1, confidence=0.8)
class FakeLLMRunner:
def __init__(self, _config):
pass
def get_signal(self, _ticker, _date):
return _signal(
"llm",
direction=0,
confidence=0.0,
metadata={
"error": "provider mismatch",
"data_quality": {
"state": "provider_mismatch",
"provider": "anthropic",
"backend_url": "https://api.openai.com/v1",
},
},
reason_code=ReasonCode.PROVIDER_MISMATCH.value,
)
monkeypatch.setattr(orchestrator_module, "QuantRunner", FakeQuantRunner)
monkeypatch.setattr(orchestrator_module, "LLMRunner", FakeLLMRunner)
result = orchestrator_module.TradingOrchestrator(
OrchestratorConfig(quant_backtest_path="/tmp/quant")
).get_combined_signal("AAPL", "2026-04-11")
assert result.direction == 1
assert result.quant_signal is not None
assert result.llm_signal is None
assert result.degrade_reason_codes == (ReasonCode.PROVIDER_MISMATCH.value,)
assert result.metadata["data_quality"]["state"] == "provider_mismatch"
assert result.metadata["data_quality"]["source"] == "llm"
assert result.metadata["data_quality"]["issues"] == [
{
"source": "llm",
"state": "provider_mismatch",
"provider": "anthropic",
"backend_url": "https://api.openai.com/v1",
}
]