Files
MoFin/venv/lib/python3.12/site-packages/alphasift/dsa_provider.py
T
知微 fa45d8aa5f fix: 小果地址统一node122(兼容LAN+EasyTier)
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
  Privoxy对node122:18003返回500,直连正常
2026-06-30 02:56:35 +08:00

193 lines
6.7 KiB
Python

# -*- coding: utf-8 -*-
"""DSA provider-context bridge.
This module consumes DSA-owned callables passed through ``context["dsa"]``.
It is intentionally best-effort: AlphaSift can use richer DSA data when
available, but screening should continue when one provider is slow or broken.
"""
from __future__ import annotations
import logging
from typing import Any, Callable
from alphasift.models import Pick
logger = logging.getLogger(__name__)
DSA_PROVIDER_MAX_CANDIDATES = 5
def apply_dsa_provider_context(
picks: list[Pick],
context: dict[str, Any] | None,
*,
max_candidates: int = DSA_PROVIDER_MAX_CANDIDATES,
) -> list[str]:
"""Attach DSA context to top candidates before LLM ranking."""
provider = _extract_provider_context(context)
if not picks or not provider:
return []
limit = min(len(picks), max(max_candidates, 0))
if limit <= 0:
return []
enriched_count = 0
errors: list[str] = []
for pick in picks[:limit]:
try:
payload = _fetch_candidate_context(provider, pick)
if not payload:
continue
normalized = _normalize_candidate_payload(payload, pick)
if not normalized:
continue
pick.dsa_context = normalized["context"]
pick.dsa_news = normalized["news"]
pick.dsa_analysis_summary = normalized["summary"]
if _is_enriched_context(pick.dsa_context, pick.dsa_news):
enriched_count += 1
except Exception as exc: # noqa: BLE001 - external DSA providers are optional.
message = f"{pick.code}: {exc}"
errors.append(message)
pick.dsa_context = {
"enriched": False,
"warnings": [message],
}
logger.warning("DSA provider context failed for %s: %s", pick.code, exc)
notes = [f"DSA provider context applied {enriched_count} of {limit} candidates"]
if errors:
sample = " | ".join(errors[:5])
suffix = f" | +{len(errors) - 5} more" if len(errors) > 5 else ""
notes.append(f"DSA provider context row errors: {sample}{suffix}")
return notes
def _extract_provider_context(context: dict[str, Any] | None) -> dict[str, Any]:
if not isinstance(context, dict):
return {}
provider = context.get("dsa")
return provider if isinstance(provider, dict) else {}
def _fetch_candidate_context(provider: dict[str, Any], pick: Pick) -> dict[str, Any]:
candidate_getter = provider.get("get_candidate_context")
if callable(candidate_getter):
payload = _call_candidate_getter(candidate_getter, pick)
return payload if isinstance(payload, dict) else {}
quote = _call_optional_provider(provider.get("get_realtime_quote"), pick.code)
fundamentals = _call_optional_provider(provider.get("get_fundamental_context"), pick.code)
news = _call_news_provider(provider.get("search_stock_news"), pick)
return {
"enriched": bool(quote or fundamentals or _news_results(news)),
"quote": quote,
"fundamentals": fundamentals,
"news": news,
"warnings": [],
}
def _call_candidate_getter(getter: Callable[..., Any], pick: Pick) -> Any:
try:
return getter(pick.code, pick.name)
except TypeError:
return getter(pick.code)
def _call_optional_provider(provider: Any, stock_code: str) -> dict[str, Any]:
if not callable(provider):
return {}
payload = provider(stock_code)
return payload if isinstance(payload, dict) else {}
def _call_news_provider(provider: Any, pick: Pick) -> dict[str, Any]:
if not callable(provider):
return {"success": False, "results": []}
try:
payload = provider(pick.code, pick.name, max_results=3)
except TypeError:
try:
payload = provider(pick.code, pick.name)
except TypeError:
payload = provider(pick.code)
return payload if isinstance(payload, dict) else {"success": False, "results": []}
def _normalize_candidate_payload(payload: dict[str, Any], pick: Pick) -> dict[str, Any]:
full_payload = payload
context = payload.get("dsa_context") if isinstance(payload.get("dsa_context"), dict) else payload
if not isinstance(context, dict):
return {}
news = payload.get("dsa_news")
if not isinstance(news, list):
news = _news_results(context.get("news"))
news = [item for item in news if isinstance(item, dict)]
summary = str(payload.get("dsa_analysis_summary") or "").strip()
if not summary:
summary = _build_dsa_summary(pick, context, news)
normalized_context = dict(context)
normalized_context.setdefault("enriched", _is_enriched_context(normalized_context, news))
if full_payload is not context and "dsa_context" in full_payload:
normalized_context.setdefault("source_payload", "dsa_candidate_context")
return {
"context": normalized_context,
"news": news,
"summary": summary,
}
def _is_enriched_context(context: dict[str, Any], news: list[dict[str, Any]]) -> bool:
return bool(
context.get("enriched")
or context.get("quote")
or context.get("fundamentals")
or news
)
def _news_results(news_payload: Any) -> list[dict[str, Any]]:
if isinstance(news_payload, dict) and isinstance(news_payload.get("results"), list):
return [item for item in news_payload["results"] if isinstance(item, dict)]
if isinstance(news_payload, list):
return [item for item in news_payload if isinstance(item, dict)]
return []
def _build_dsa_summary(pick: Pick, context: dict[str, Any], news: list[dict[str, Any]]) -> str:
parts: list[str] = []
quote = context.get("quote") if isinstance(context.get("quote"), dict) else {}
price = quote.get("price") if quote else pick.price
change_pct = quote.get("change_pct") if quote else pick.change_pct
if price not in (None, ""):
text = f"DSA行情: 现价 {price}"
if change_pct not in (None, ""):
text += f", 涨跌幅 {change_pct}%"
parts.append(text)
fundamentals = context.get("fundamentals")
coverage = fundamentals.get("coverage") if isinstance(fundamentals, dict) else {}
if isinstance(coverage, dict):
available = [
str(key)
for key, value in coverage.items()
if str(value).lower() in {"available", "partial"}
]
if available:
parts.append(f"DSA基本面覆盖: {', '.join(available[:4])}")
titles = [
str(item.get("title") or "").strip()
for item in news
if isinstance(item.get("title"), str) and item.get("title")
]
if titles:
parts.append(f"DSA新闻: {'; '.join(titles[:2])}")
return "; ".join(parts)