89 lines
2.6 KiB
Python
89 lines
2.6 KiB
Python
"""Strategy registry with auto-discovery, signal computation, and role-based formatting."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import logging
|
|
import pkgutil
|
|
from typing import Any
|
|
|
|
from .base import BaseStrategy, Role, StrategySignal
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_registry: list[BaseStrategy] = []
|
|
|
|
|
|
def _discover() -> None:
|
|
"""Auto-discover BaseStrategy subclasses in this package."""
|
|
if _registry:
|
|
return
|
|
import tradingagents.strategies as pkg
|
|
|
|
for info in pkgutil.iter_modules(pkg.__path__):
|
|
if info.name in ("base", "registry", "scorecard", "__init__"):
|
|
continue
|
|
try:
|
|
mod = importlib.import_module(f"{pkg.__name__}.{info.name}")
|
|
except Exception:
|
|
logger.warning("Failed to import strategy module %s", info.name, exc_info=True)
|
|
continue
|
|
for attr in vars(mod).values():
|
|
if (
|
|
isinstance(attr, type)
|
|
and issubclass(attr, BaseStrategy)
|
|
and attr is not BaseStrategy
|
|
and attr.name
|
|
):
|
|
_registry.append(attr())
|
|
|
|
|
|
def get_registry() -> list[BaseStrategy]:
|
|
"""Return all registered strategy instances."""
|
|
_discover()
|
|
return list(_registry)
|
|
|
|
|
|
def reset_registry() -> None:
|
|
"""Clear the registry (useful for testing)."""
|
|
_registry.clear()
|
|
|
|
|
|
def compute_signals(
|
|
ticker: str, date: str, context: dict[str, Any] | None = None
|
|
) -> list[StrategySignal]:
|
|
"""Run every registered strategy and collect non-None signals."""
|
|
_discover()
|
|
signals: list[StrategySignal] = []
|
|
for strategy in _registry:
|
|
try:
|
|
sig = strategy.compute(ticker, date, context)
|
|
if sig is not None:
|
|
signals.append(sig)
|
|
except Exception:
|
|
logger.warning("Strategy %s failed for %s@%s", strategy.name, ticker, date, exc_info=True)
|
|
return signals
|
|
|
|
|
|
def format_signals_for_role(signals: list[StrategySignal], role: Role) -> str:
|
|
"""Format signals relevant to *role* as a prompt section.
|
|
|
|
Returns an empty string when no signals match the role.
|
|
"""
|
|
_discover()
|
|
# Build a set of strategy names relevant to this role
|
|
role_names: set[str] = set()
|
|
for s in _registry:
|
|
if role in s.roles:
|
|
role_names.add(s.name)
|
|
|
|
relevant = [s for s in signals if s["name"] in role_names]
|
|
if not relevant:
|
|
return ""
|
|
|
|
lines = ["## Quantitative Strategy Signals"]
|
|
for s in relevant:
|
|
strength = f"{s['signal_strength']:+.2f}"
|
|
lines.append(f"- **{s['name']}** [{s['direction']}, {strength}]: {s['detail']}")
|
|
return "\n".join(lines)
|