diff --git a/orchestrator/__init__.py b/orchestrator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestrator/config.py b/orchestrator/config.py new file mode 100644 index 00000000..0beb6c5e --- /dev/null +++ b/orchestrator/config.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field + + +@dataclass +class OrchestratorConfig: + quant_backtest_path: str = "/Users/chenshaojie/Downloads/quant_backtest" + trading_agents_config: dict = field(default_factory=dict) + quant_weight_cap: float = 0.8 # quant 置信度上限 + llm_weight_cap: float = 0.9 # llm 置信度上限 + llm_batch_days: int = 7 # LLM 每隔几天运行一次(节省 API) + cache_dir: str = "orchestrator/cache" # LLM 信号缓存目录 diff --git a/orchestrator/signals.py b/orchestrator/signals.py new file mode 100644 index 00000000..f9549dcc --- /dev/null +++ b/orchestrator/signals.py @@ -0,0 +1,100 @@ +import logging +from dataclasses import dataclass, field +from datetime import datetime +from typing import Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class Signal: + ticker: str + direction: int # +1 买入, -1 卖出, 0 持有 + confidence: float # 0.0 ~ 1.0 + source: str # "quant" | "llm" + timestamp: datetime + metadata: dict = field(default_factory=dict) # 原始输出,用于调试 + + +@dataclass +class FinalSignal: + ticker: str + direction: int # sign(quant_dir×quant_conf + llm_dir×llm_conf),sign(0)→0(HOLD) + confidence: float # abs(weighted_sum) / total_conf + quant_signal: Optional[Signal] + llm_signal: Optional[Signal] + timestamp: datetime + + +def _sign(x: float) -> int: + """Return +1, -1, or 0.""" + if x > 0: + return 1 + elif x < 0: + return -1 + return 0 + + +class SignalMerger: + def merge(self, quant: Optional[Signal], llm: Optional[Signal]) -> FinalSignal: + now = datetime.utcnow() + + # 两者均失败 + if quant is None and llm is None: + ticker = "" + return FinalSignal( + ticker=ticker, + direction=0, + confidence=0.0, + quant_signal=None, + llm_signal=None, + timestamp=now, + ) + + ticker = (quant or llm).ticker # type: ignore[union-attr] + + # 只有 LLM(quant 失败) + if quant is None: + assert llm is not None + return FinalSignal( + ticker=ticker, + direction=llm.direction, + confidence=llm.confidence * 0.7, + quant_signal=None, + llm_signal=llm, + timestamp=now, + ) + + # 只有 Quant(llm 失败) + if llm is None: + return FinalSignal( + ticker=ticker, + direction=quant.direction, + confidence=quant.confidence * 0.8, + quant_signal=quant, + llm_signal=None, + timestamp=now, + ) + + # 两者都有:加权合并 + weighted_sum = ( + quant.direction * quant.confidence + + llm.direction * llm.confidence + ) + final_direction = _sign(weighted_sum) + if final_direction == 0: + logger.info( + "SignalMerger: weighted_sum=0 for %s — signals cancel out, HOLD", + ticker, + ) + total_conf = quant.confidence + llm.confidence + final_confidence = abs(weighted_sum) / total_conf if total_conf > 0 else 0.0 + + return FinalSignal( + ticker=ticker, + direction=final_direction, + confidence=final_confidence, + quant_signal=quant, + llm_signal=llm, + timestamp=now, + )