203 lines
7.6 KiB
Python
203 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
|
|
from langchain_core.messages import HumanMessage
|
|
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
from tradingagents.llm_clients.base_client import normalize_content
|
|
from tradingagents.llm_clients.factory import create_llm_client
|
|
from tradingagents.llm_clients.model_catalog import MODEL_OPTIONS
|
|
|
|
# 仪表盘 LLM 默认:SiliconFlow + catalog 中首个 quick(与 Web 表单选 SiliconFlow 时第一项一致)
|
|
_DASHBOARD_DEFAULT_PROVIDER = "siliconflow"
|
|
_DASHBOARD_DEFAULT_QUICK = MODEL_OPTIONS[_DASHBOARD_DEFAULT_PROVIDER]["quick"][0][1]
|
|
_PROVIDER_URL = {
|
|
"openai": "https://api.openai.com/v1",
|
|
"siliconflow": "https://api.siliconflow.cn/v1",
|
|
"google": "https://generativelanguage.googleapis.com/v1",
|
|
"anthropic": "https://api.anthropic.com/",
|
|
"xai": "https://api.x.ai/v1",
|
|
"openrouter": "https://openrouter.ai/api/v1",
|
|
"ollama": "http://localhost:11434/v1",
|
|
}
|
|
|
|
|
|
def build_llm_config(last_params: dict[str, Any] | None) -> dict[str, Any]:
|
|
cfg = DEFAULT_CONFIG.copy()
|
|
cfg["llm_provider"] = _DASHBOARD_DEFAULT_PROVIDER
|
|
cfg["quick_think_llm"] = _DASHBOARD_DEFAULT_QUICK
|
|
cfg["backend_url"] = _PROVIDER_URL[_DASHBOARD_DEFAULT_PROVIDER]
|
|
if last_params:
|
|
cfg["llm_provider"] = last_params.get("llm_provider", cfg["llm_provider"])
|
|
cfg["quick_think_llm"] = last_params.get("quick_model", cfg["quick_think_llm"])
|
|
cfg["backend_url"] = last_params.get("backend_url", cfg["backend_url"])
|
|
if last_params.get("google_thinking_level"):
|
|
cfg["google_thinking_level"] = last_params.get("google_thinking_level")
|
|
if last_params.get("openai_reasoning_effort"):
|
|
cfg["openai_reasoning_effort"] = last_params.get("openai_reasoning_effort")
|
|
if last_params.get("anthropic_effort"):
|
|
cfg["anthropic_effort"] = last_params.get("anthropic_effort")
|
|
return cfg
|
|
|
|
|
|
def _quick_llm_kwargs(cfg: dict[str, Any]) -> dict[str, Any]:
|
|
out: dict[str, Any] = {}
|
|
if cfg.get("google_thinking_level"):
|
|
out["google_thinking_level"] = cfg["google_thinking_level"]
|
|
if cfg.get("openai_reasoning_effort"):
|
|
out["openai_reasoning_effort"] = cfg["openai_reasoning_effort"]
|
|
if cfg.get("anthropic_effort"):
|
|
out["anthropic_effort"] = cfg["anthropic_effort"]
|
|
return out
|
|
|
|
|
|
def _get_llm(cfg: dict[str, Any]):
|
|
client = create_llm_client(
|
|
provider=cfg["llm_provider"],
|
|
model=cfg["quick_think_llm"],
|
|
base_url=cfg.get("backend_url"),
|
|
**_quick_llm_kwargs(cfg),
|
|
)
|
|
return client.get_llm()
|
|
|
|
|
|
def _msg_text(resp: Any) -> str:
|
|
if hasattr(resp, "content"):
|
|
normalize_content(resp)
|
|
c = resp.content
|
|
return c if isinstance(c, str) else str(c)
|
|
return str(resp)
|
|
|
|
|
|
def _parse_json_array(text: str) -> list[dict[str, Any]]:
|
|
text = text.strip()
|
|
try:
|
|
data = json.loads(text)
|
|
if isinstance(data, list):
|
|
return data
|
|
except json.JSONDecodeError:
|
|
pass
|
|
m = re.search(r"\[[\s\S]*\]", text)
|
|
if m:
|
|
return json.loads(m.group(0))
|
|
raise ValueError("no JSON array in model output")
|
|
|
|
|
|
def enrich_news_opinions(
|
|
news_items: list[dict[str, Any]],
|
|
cfg: dict[str, Any],
|
|
) -> list[dict[str, Any]]:
|
|
"""为每条新闻补充 stance + llm_summary(批量一次调用)。"""
|
|
if not news_items:
|
|
return news_items
|
|
llm = _get_llm(cfg)
|
|
lines = []
|
|
for i, n in enumerate(news_items):
|
|
title = (n.get("title") or "")[:500]
|
|
summary = (n.get("summary") or "")[:800]
|
|
lines.append(f'{i}. title: {title}\n summary: {summary}')
|
|
block = "\n".join(lines)
|
|
prompt = f"""你是财经新闻简评助手。根据下列新闻标题与摘要,对每条给出市场情绪判断与一句中文短评。
|
|
|
|
新闻列表:
|
|
{block}
|
|
|
|
请只输出一个 JSON 数组,长度与新闻条数相同,不要 markdown 围栏。每个元素格式:
|
|
{{"stance":"bullish"|"bearish"|"neutral","summary":"一句中文,不超过80字"}}
|
|
|
|
stance 含义:bullish=偏多/利好倾向,bearish=偏空/利空倾向,neutral=中性或信息不足。
|
|
"""
|
|
resp = llm.invoke([HumanMessage(content=prompt)])
|
|
raw = _msg_text(resp)
|
|
arr = _parse_json_array(raw)
|
|
out = []
|
|
for i, n in enumerate(news_items):
|
|
row = dict(n)
|
|
if i < len(arr) and isinstance(arr[i], dict):
|
|
row["stance"] = arr[i].get("stance", "neutral")
|
|
row["llm_summary"] = (arr[i].get("summary") or "").strip()
|
|
else:
|
|
row["stance"] = "neutral"
|
|
row["llm_summary"] = ""
|
|
out.append(row)
|
|
return out
|
|
|
|
|
|
def generate_market_digest(
|
|
snapshot_context: dict[str, Any],
|
|
news_items: list[dict[str, Any]],
|
|
cfg: dict[str, Any],
|
|
) -> str:
|
|
"""生成仪表盘顶部大盘总结(Markdown)。"""
|
|
llm = _get_llm(cfg)
|
|
macro = snapshot_context.get("macro_strip") or []
|
|
macro_lines = []
|
|
for m in macro:
|
|
ch = m.get("change_pct")
|
|
ch_s = f"{ch:.2f}" if ch is not None else "N/A"
|
|
macro_lines.append(
|
|
f"- {m.get('label', m.get('ticker'))}: "
|
|
f"价/收益率 {m.get('display_value', 'N/A')} "
|
|
f"日涨跌 {ch_s}%"
|
|
)
|
|
idx_lines = []
|
|
for r in snapshot_context.get("indexes") or []:
|
|
if hasattr(r, "ticker"):
|
|
t, p, ch = r.ticker, r.price, r.change_pct
|
|
else:
|
|
t = r.get("ticker")
|
|
p = r.get("price")
|
|
ch = r.get("change_pct")
|
|
ch_s = f"{ch:.2f}" if ch is not None else "N/A"
|
|
p_s = f"{p:.2f}" if p is not None else "N/A"
|
|
idx_lines.append(f"- {t}: {p_s} ({ch_s}%)")
|
|
news_brief = "\n".join(
|
|
f"- {(n.get('title') or '')[:120]}" for n in (news_items or [])[:10]
|
|
)
|
|
prompt = f"""你是美股市场复盘助手。根据下列**结构化数据**(可能不完整),用中文 Markdown 输出。
|
|
|
|
【硬性格式 — 必须严格遵守】
|
|
1. **禁止**在第一个以 `##### ` 开头的标题行之前输出任何正文、引言或列表(不要先写一段再写标题)。
|
|
2. 全文必须且只能按下面 **5 个小节** 依次展开;每一节**单独一行**以 `##### ` 开头(五个 # 加一个空格),紧接着下一行起写该节正文。
|
|
3. 小节标题请尽量使用下列措辞(可微调个别用词,但必须保留 `##### ` 前缀):
|
|
- `##### 经济数据说明了什么`
|
|
- `##### 大盘数据说明了什么`
|
|
- `##### 新闻数据说明了什么`
|
|
- `##### 整体结论`
|
|
- `##### 牛熊判断与数据论据`
|
|
4. 「经济数据」节的正文必须写在 `##### 经济数据说明了什么` 标题**下面**,基于美债收益率、美元、TIP 等代理指标推断;勿编造未提供的官方 CPI 数字。
|
|
5. 「牛熊判断」节请说明当前美股更偏牛市/熊市/震荡,并列出**可核对的数据论据**(条目列表)。
|
|
|
|
【输出结构示例】(模仿此结构,把括号说明换成你的实质分析):
|
|
|
|
##### 经济数据说明了什么
|
|
(本节正文:基于代理指标的分析。)
|
|
|
|
##### 大盘数据说明了什么
|
|
(本节正文:指数与 VIX。)
|
|
|
|
##### 新闻数据说明了什么
|
|
(本节正文:聚合倾向,勿逐条复述。)
|
|
|
|
##### 整体结论
|
|
(本节正文。)
|
|
|
|
##### 牛熊判断与数据论据
|
|
(本节正文:条目列表。)
|
|
|
|
---
|
|
宏观代理指标:
|
|
{chr(10).join(macro_lines) if macro_lines else '(无)'}
|
|
|
|
主要指数:
|
|
{chr(10).join(idx_lines) if idx_lines else '(无)'}
|
|
|
|
新闻标题摘要:
|
|
{news_brief if news_brief else '(无)'}
|
|
"""
|
|
resp = llm.invoke([HumanMessage(content=prompt)])
|
|
return _msg_text(resp).strip()
|