fa45d8aa5f
- 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,直连正常
193 lines
6.7 KiB
Python
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)
|