94 lines
3.9 KiB
Python
94 lines
3.9 KiB
Python
import logging
|
|
from typing import Optional
|
|
|
|
from orchestrator.config import OrchestratorConfig
|
|
from orchestrator.contracts.error_taxonomy import ReasonCode
|
|
from orchestrator.contracts.result_contract import FinalSignal, Signal, signal_reason_code
|
|
from orchestrator.signals import Signal, FinalSignal, SignalMerger
|
|
from orchestrator.quant_runner import QuantRunner
|
|
from orchestrator.llm_runner import LLMRunner
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TradingOrchestrator:
|
|
def __init__(self, config: OrchestratorConfig):
|
|
self._config = config
|
|
self._merger = SignalMerger(config)
|
|
self._quant: Optional[QuantRunner] = None
|
|
self._llm: Optional[LLMRunner] = None
|
|
self._quant_unavailable_reason: Optional[str] = None
|
|
self._llm_unavailable_reason: Optional[str] = None
|
|
|
|
# Initialize runners (quant requires quant_backtest_path)
|
|
if config.quant_backtest_path:
|
|
try:
|
|
self._quant = QuantRunner(config)
|
|
except Exception as e:
|
|
logger.warning("TradingOrchestrator: QuantRunner init failed: %s", e)
|
|
self._quant_unavailable_reason = ReasonCode.QUANT_INIT_FAILED.value
|
|
else:
|
|
self._quant_unavailable_reason = ReasonCode.QUANT_NOT_CONFIGURED.value
|
|
|
|
try:
|
|
self._llm = LLMRunner(config)
|
|
except Exception as e:
|
|
logger.warning("TradingOrchestrator: LLMRunner init failed: %s", e)
|
|
self._llm_unavailable_reason = ReasonCode.LLM_INIT_FAILED.value
|
|
|
|
def get_combined_signal(self, ticker: str, date: str) -> FinalSignal:
|
|
"""
|
|
Get merged signal for ticker on date.
|
|
Degradation:
|
|
- quant fails (error signal): use llm only with llm_solo_penalty
|
|
- llm fails (error signal): use quant only with quant_solo_penalty
|
|
- both fail: raises ValueError
|
|
"""
|
|
quant_sig: Optional[Signal] = None
|
|
llm_sig: Optional[Signal] = None
|
|
degradation_reasons: list[str] = []
|
|
|
|
if self._quant is None and self._quant_unavailable_reason:
|
|
degradation_reasons.append(self._quant_unavailable_reason)
|
|
if self._llm is None and self._llm_unavailable_reason:
|
|
degradation_reasons.append(self._llm_unavailable_reason)
|
|
|
|
# Get quant signal
|
|
if self._quant is not None:
|
|
try:
|
|
quant_sig = self._quant.get_signal(ticker, date)
|
|
if quant_sig.degraded:
|
|
degradation_reasons.append(
|
|
signal_reason_code(quant_sig) or ReasonCode.QUANT_SIGNAL_FAILED.value
|
|
)
|
|
logger.warning("TradingOrchestrator: quant signal degraded for %s %s", ticker, date)
|
|
quant_sig = None
|
|
except Exception as e:
|
|
logger.error("TradingOrchestrator: quant get_signal failed: %s", e)
|
|
degradation_reasons.append(ReasonCode.QUANT_SIGNAL_FAILED.value)
|
|
quant_sig = None
|
|
|
|
# Get llm signal
|
|
if self._llm is not None:
|
|
try:
|
|
llm_sig = self._llm.get_signal(ticker, date)
|
|
if llm_sig.degraded:
|
|
degradation_reasons.append(
|
|
signal_reason_code(llm_sig) or ReasonCode.LLM_SIGNAL_FAILED.value
|
|
)
|
|
logger.warning("TradingOrchestrator: llm signal degraded for %s %s", ticker, date)
|
|
llm_sig = None
|
|
except Exception as e:
|
|
logger.error("TradingOrchestrator: llm get_signal failed: %s", e)
|
|
degradation_reasons.append(ReasonCode.LLM_SIGNAL_FAILED.value)
|
|
llm_sig = None
|
|
|
|
# merge raises ValueError if both None
|
|
if quant_sig is None and llm_sig is None:
|
|
degradation_reasons.append(ReasonCode.BOTH_SIGNALS_UNAVAILABLE.value)
|
|
return self._merger.merge(
|
|
quant_sig,
|
|
llm_sig,
|
|
degradation_reasons=degradation_reasons,
|
|
)
|