203 lines
5.7 KiB
Python
203 lines
5.7 KiB
Python
# TradingAgents/publish/wechat_publisher.py
|
||
"""
|
||
将 TradingAgents 的股票分析结果发布到微信公众号和个人博客。
|
||
|
||
依赖:baoyu-post-to-wechat 技能(位于 workspace/skills/baoyu-post-to-wechat)
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import Dict, Any, Optional
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 技能路径(与 baoyu-post-to-wechat 在同一目录)
|
||
_SKILL_DIR = "/home/ubuntu/.nanobot/workspace/skills/baoyu-post-to-wechat"
|
||
|
||
|
||
def _build_markdown(final_state: Dict[str, Any], ticker: str, trade_date) -> str:
|
||
"""将 final_state 组装成完整 Markdown 文章。"""
|
||
date_str = trade_date if isinstance(trade_date, str) else str(trade_date)
|
||
title = f"【{ticker}】股票分析报告 - {date_str}"
|
||
|
||
lines = [
|
||
"---",
|
||
f"title: {title}",
|
||
f"date: {date_str}",
|
||
f"author: TradingAgents",
|
||
"---",
|
||
"",
|
||
f"# {title}",
|
||
"",
|
||
"## 📊 市场分析报告",
|
||
"",
|
||
final_state.get("market_report", "暂无"),
|
||
"",
|
||
"## 💬 情绪分析报告",
|
||
"",
|
||
final_state.get("sentiment_report", "暂无"),
|
||
"",
|
||
"## 📰 新闻分析报告",
|
||
"",
|
||
final_state.get("news_report", "暂无"),
|
||
"",
|
||
"## 📈 基本面分析报告",
|
||
"",
|
||
final_state.get("fundamentals_report", "暂无"),
|
||
"",
|
||
]
|
||
|
||
def _append_history(items):
|
||
"""兼容 list 和 str 类型,避免逐字符遍历字符串。
|
||
|
||
注意:debate 内容本身可能就是带 Markdown 格式的大段文章
|
||
(含标题、列表等),如果强行加 '- ' 前缀会破坏内部结构,
|
||
导致 '###' 子标题被当作列表项文本而失效。
|
||
因此直接输出原始段落,不再套一层无序列表。
|
||
"""
|
||
if isinstance(items, list):
|
||
for msg in items:
|
||
lines.append(str(msg))
|
||
lines.append("") # 段间空行,保持可读性
|
||
elif isinstance(items, str):
|
||
if items.strip():
|
||
lines.append(items.strip())
|
||
# 其他类型忽略
|
||
|
||
# 多空辩论
|
||
debate = final_state.get("investment_debate_state", {})
|
||
bull = debate.get("bull_history", [])
|
||
bear = debate.get("bear_history", [])
|
||
judge = debate.get("judge_decision", "")
|
||
|
||
lines.extend([
|
||
"## ⚖️ 多空辩论",
|
||
"",
|
||
"### 多头观点",
|
||
"",
|
||
])
|
||
_append_history(bull)
|
||
lines.extend([
|
||
"",
|
||
"### 空头观点",
|
||
"",
|
||
])
|
||
_append_history(bear)
|
||
lines.extend([
|
||
"",
|
||
"### 裁判决策",
|
||
"",
|
||
judge if judge else "暂无",
|
||
"",
|
||
])
|
||
|
||
# 风险辩论
|
||
risk = final_state.get("risk_debate_state", {})
|
||
agg = risk.get("aggressive_history", [])
|
||
cons = risk.get("conservative_history", [])
|
||
neut = risk.get("neutral_history", [])
|
||
risk_judge = risk.get("judge_decision", "")
|
||
|
||
lines.extend([
|
||
"## 🛡️ 风险辩论",
|
||
"",
|
||
"### 激进观点",
|
||
"",
|
||
])
|
||
_append_history(agg)
|
||
lines.extend([
|
||
"",
|
||
"### 保守观点",
|
||
"",
|
||
])
|
||
_append_history(cons)
|
||
lines.extend([
|
||
"",
|
||
"### 中性观点",
|
||
"",
|
||
])
|
||
_append_history(neut)
|
||
lines.extend([
|
||
"",
|
||
"### 裁判决策",
|
||
"",
|
||
risk_judge if risk_judge else "暂无",
|
||
"",
|
||
])
|
||
|
||
# 投资计划与最终决策
|
||
lines.extend([
|
||
"## 📋 投资计划",
|
||
"",
|
||
final_state.get("investment_plan", "暂无"),
|
||
"",
|
||
"## 🎯 最终交易决策",
|
||
"",
|
||
final_state.get("final_trade_decision", "暂无"),
|
||
"",
|
||
])
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def publish_analysis_result(
|
||
final_state: Dict[str, Any],
|
||
ticker: str,
|
||
trade_date,
|
||
results_dir: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
将股票分析结果发布到微信公众号和个人博客。
|
||
|
||
Args:
|
||
final_state: 图执行后的最终状态字典。
|
||
ticker: 股票代码/公司名称。
|
||
trade_date: 交易日期。
|
||
results_dir: 结果保存目录,默认使用 ~/TradingAgents/results。
|
||
|
||
Returns:
|
||
{"success": bool, ...} 发布结果字典。
|
||
"""
|
||
try:
|
||
date_str = trade_date if isinstance(trade_date, str) else str(trade_date)
|
||
safe_ticker = ticker.replace("/", "_").replace("\\", "_")
|
||
|
||
# 1. 组装 Markdown
|
||
markdown_content = _build_markdown(final_state, ticker, trade_date)
|
||
|
||
# 2. 保存到结果目录
|
||
if results_dir is None:
|
||
results_dir = os.path.join(os.path.expanduser("~"), "TradingAgents", "results")
|
||
|
||
publish_dir = Path(results_dir) / safe_ticker / "published"
|
||
publish_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
md_path = publish_dir / f"analysis_{date_str}.md"
|
||
with open(md_path, "w", encoding="utf-8") as f:
|
||
f.write(markdown_content)
|
||
|
||
logger.info("Markdown report saved: %s", md_path)
|
||
print(f"📄 Markdown report saved: {md_path}")
|
||
|
||
# 3. 导入并调用 baoyu-post-to-wechat
|
||
if _SKILL_DIR not in sys.path:
|
||
sys.path.insert(0, _SKILL_DIR)
|
||
|
||
from baoyu_optimized import WeChatPublisher
|
||
|
||
publisher = WeChatPublisher()
|
||
result = publisher.publish_markdown_with_images(
|
||
markdown_path=str(md_path),
|
||
title=f"【{ticker}】股票分析报告 - {date_str}",
|
||
author="TradingAgents",
|
||
)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.exception("Failed to publish analysis result to WeChat")
|
||
print(f"⚠️ 发布到公众号失败: {e}")
|
||
return {"success": False, "error": str(e)}
|