buffett_service.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. """
  2. 智能股票分析助手 — 巴菲特投资评估服务层
  3. 加载巴菲特投资思维参考文件,构建价值投资评估框架,
  4. 供API路由层和智能体层调用。
  5. """
  6. import sys
  7. import os
  8. from pathlib import Path
  9. from typing import Any, Dict, Iterator, List, Optional
  10. # 确保skills路径可导入
  11. _PROJECT_ROOT = Path(__file__).parent.parent.parent.parent # backend/app/services -> project root
  12. _AGENTS_DIR = _PROJECT_ROOT / "agents"
  13. _BUFFETT_DIR = _PROJECT_ROOT / "skills" / "巴菲特投资思维" / "skills" / "buffett"
  14. for p in [_AGENTS_DIR, str(_PROJECT_ROOT)]:
  15. if str(p) not in sys.path:
  16. sys.path.insert(0, str(p))
  17. from agents.text_truncation import truncate_at_natural_boundary
  18. from app.config import settings
  19. # ====================================================================
  20. # 巴菲特投资思维核心内容(从参考文件中提取的摘要)
  21. # ====================================================================
  22. BUFFETT_FRAMEWORK = {
  23. "quick_filter": {
  24. "name": "8问快速筛选",
  25. "questions": [
  26. "能力圈:能否用一段话解释这家公司如何赚钱?",
  27. "持久性:10年后这家公司是否还会存在且更具竞争力?",
  28. "护城河:竞争对手能否通过努力复制其核心优势?",
  29. "定价权:能否提价5-10%而不丢失大量客户?",
  30. "盈利质量:利润是否真正转化为现金(而非会计技巧)?",
  31. "债务安全:在行业最差情况下(营收-30%)能否存活?",
  32. "管理层诚信:管理层是否诚实面对问题而非掩盖?",
  33. "合理价格:当前价格与内在价值的差距是否足够大?",
  34. ],
  35. "rule": "2个\"否\"需要强有力理由;4个\"否\"直接放弃",
  36. },
  37. "moat_analysis": {
  38. "name": "护城河分析",
  39. "types": [
  40. "成本优势(成本领先,规模经济)",
  41. "转换成本(客户迁移成本高)",
  42. "网络效应(用户越多价值越大)",
  43. "无形资产(品牌、专利、特许经营权)",
  44. "高效规模(天然垄断,小市场大份额)",
  45. ],
  46. "judgment": "不仅看当前状态,更关键的是趋势(拓宽/稳定/变窄)",
  47. },
  48. "management_assessment": {
  49. "name": "管理层评估三维度",
  50. "dimensions": [
  51. "诚信度(自动否决项:发现不诚信直接放弃)",
  52. "资本配置能力(能否明智分配资本:再投资/收购/回购/分红)",
  53. "所有者心态(是否像主人一样思考,不乱花股东的钱)",
  54. ],
  55. "warning": "警惕制度迫力——优秀的管理层在制度压力下也可能做出不合理决策",
  56. },
  57. "financial_metrics": {
  58. "name": "财务指标",
  59. "metrics": [
  60. "所有者收益 = 净利润 + 折旧摊销 - 维护性资本支出 - 营运资金增加",
  61. "ROIC 10年平均目标 >15%",
  62. "现金转化率目标 >90%",
  63. "透视盈余(考虑被投资公司未分配利润)",
  64. ],
  65. },
  66. "valuation": {
  67. "name": "估值与安全边际",
  68. "methods": [
  69. "现金流折现法(DCF)",
  70. "盈利倍数法(合理PE区间)",
  71. "资产价值法(净资产重估)",
  72. ],
  73. "margin_of_safety": {
  74. "高确定性(宽护城河+可预测增长)": "20-30%",
  75. "一般优秀": "30-40%",
  76. "存在不确定因素": "40-50%",
  77. "无法可靠评估": "不投资",
  78. },
  79. },
  80. "risk_analysis": {
  81. "name": "风险分类",
  82. "categories": {
  83. "结构性风险": "护城河变窄、技术颠覆、监管打击",
  84. "财务风险": "过度杠杆、现金流造假、表外负债",
  85. "行为风险": "确认偏误、沉没成本、制度迫力",
  86. },
  87. },
  88. "sell_criteria": {
  89. "name": "卖出四条标准",
  90. "criteria": [
  91. "价格严重高估(远超内在价值)",
  92. "基本面护城河遭到破坏",
  93. "管理层出现诚信问题(立即卖出)",
  94. "发现显著更好的投资机会",
  95. ],
  96. },
  97. }
  98. # 综合评估框架说明
  99. BUFFETT_FRAMEWORK_DESC = """
  100. ## 巴菲特价值投资评估框架
  101. ### 核心哲学
  102. - **内在价值 > 市场价格 → 安全边际**:只购买价格明显低于内在价值的股票
  103. - **护城河 > 一切**:持久的竞争优势是长期回报的根基
  104. - **能力圈原则**:只投资自己能理解的业务
  105. - **市场先生**:市场是为你服务的,不是指导你的
  106. - **长期持有**:以10年的视角思考,而非下一季度的股价
  107. ### 评估流程
  108. 1. **快速筛选** — 8问检查(2分钟内完成)
  109. 2. **企业质量** — 护城河类型+趋势、管理层评估
  110. 3. **财务快照** — ROIC、现金转化率、所有者收益
  111. 4. **估值分析** — 内在价值区间、安全边际计算
  112. 5. **风险评估** — 结构性/财务/行为三类风险
  113. 6. **综合判断** — 买入/不买/持有/卖出 + 建议买入价
  114. """
  115. def _mx_cell_to_str(v: Any) -> str:
  116. """妙想表格单元格转为可 JSON 序列化的字符串。"""
  117. if v is None:
  118. return ""
  119. if isinstance(v, bool):
  120. return "true" if v else "false"
  121. if isinstance(v, float):
  122. if v != v: # NaN
  123. return ""
  124. if isinstance(v, (str, int, float)):
  125. return str(v)
  126. return str(v)
  127. def _first_table_row_key_values(block: Optional[dict], max_keys: int = 48) -> dict:
  128. """取 mx_data 风格结果中首张表首行,扁平为字符串字典(减小体积、避免不可序列化对象)。"""
  129. if not isinstance(block, dict) or not block.get("success"):
  130. return {"success": False, "fields": {}}
  131. tables = block.get("tables") or []
  132. fields: dict[str, str] = {}
  133. for t in tables[:1]:
  134. names: List[str] = list(t.get("fieldnames") or t.get("fieldNames") or [])
  135. rows = t.get("rows") or []
  136. if not rows:
  137. continue
  138. row = rows[0]
  139. if isinstance(row, dict):
  140. for k, v in list(row.items())[:max_keys]:
  141. fields[str(k)] = _mx_cell_to_str(v)
  142. elif isinstance(row, list) and names:
  143. for i, name in enumerate(names):
  144. if i >= max_keys:
  145. break
  146. val = row[i] if i < len(row) else None
  147. fields[str(name)] = _mx_cell_to_str(val)
  148. break
  149. return {"success": True, "fields": fields}
  150. def slim_evaluation_context_for_api(full: dict) -> dict:
  151. """HTTP 响应用:去掉巨型 tables,保留框架与行情/财务摘要。"""
  152. if not isinstance(full, dict):
  153. return {}
  154. return {
  155. "framework": full.get("framework"),
  156. "framework_description": full.get("framework_description"),
  157. "market_snapshot": _first_table_row_key_values(full.get("market_data")),
  158. "financial_snapshot": _first_table_row_key_values(full.get("financial_data")),
  159. }
  160. def get_buffett_framework() -> dict:
  161. """获取巴菲特投资评估框架
  162. 返回完整的巴菲特投资思维体系结构,包括:
  163. - 快速筛选清单
  164. - 护城河分析框架
  165. - 管理层评估维度
  166. - 财务指标模板
  167. - 估值与安全边际计算
  168. - 风险评估分类
  169. - 卖出标准
  170. Returns:
  171. {
  172. "success": True,
  173. "framework": {...}, # 完整的评估框架
  174. "description": str, # 框架说明
  175. }
  176. """
  177. return {
  178. "success": True,
  179. "framework": BUFFETT_FRAMEWORK,
  180. "description": BUFFETT_FRAMEWORK_DESC,
  181. }
  182. def evaluate_with_buffett(stock_code: str, stock_name: str = "", data_context: dict = None) -> dict:
  183. """使用巴菲特投资思维评估股票
  184. 收集分析数据并构建巴菲特框架评估上下文,返回评估所需的数据包。
  185. Args:
  186. stock_code: 6位股票代码
  187. stock_name: 股票名称
  188. data_context: 已有的分析数据(可选),包含行情/财务/概况/舆情信息
  189. Returns:
  190. {
  191. "success": True/False,
  192. "stock_code": str,
  193. "stock_name": str,
  194. "evaluation_context": {
  195. "framework": dict, # 巴菲特评估框架
  196. "market_data": dict, # 行情数据
  197. "financial_data": dict,# 财务数据
  198. "profile_data": dict, # 公司概况
  199. "sentiment_data": dict,# 舆情数据
  200. },
  201. "report_template": str, # 评估报告模板
  202. "error": str or None
  203. }
  204. """
  205. result = {
  206. "success": False,
  207. "stock_code": stock_code,
  208. "stock_name": stock_name,
  209. "evaluation_context": {},
  210. "report_template": "",
  211. "error": None,
  212. }
  213. data_context = data_context or {}
  214. # 构建评估上下文
  215. context = {
  216. "framework": BUFFETT_FRAMEWORK,
  217. "framework_description": BUFFETT_FRAMEWORK_DESC,
  218. "market_data": data_context.get("market", {}),
  219. "financial_data": data_context.get("financial", {}),
  220. "profile_data": data_context.get("profile", {}),
  221. "sentiment_data": data_context.get("sentiment", {}),
  222. }
  223. result["success"] = True
  224. result["evaluation_context"] = context
  225. result["report_template"] = _build_buffett_report_template(stock_code, stock_name)
  226. return result
  227. def _build_buffett_report_template(stock_code: str, stock_name: str) -> str:
  228. """生成巴菲特风格评估报告模板
  229. Args:
  230. stock_code: 6位股票代码
  231. stock_name: 股票名称
  232. Returns:
  233. Markdown格式的报告模板
  234. """
  235. name_display = stock_name or stock_code
  236. template = f"""
  237. # 巴菲特价值投资评估报告
  238. ## 标的: {name_display} ({stock_code})
  239. ---
  240. ## 一、结论
  241. [买入 / 不买 / 持续观察 / 持有 / 卖出] — 一句话核心理由
  242. ---
  243. ## 二、能力圈判断
  244. [明确判断:在圈内 / 圈外 / 边界区域]
  245. 若在圈外:停止分析并诚实说明原因。
  246. ---
  247. ## 三、关键假设(3-5条)
  248. [列出决策所依赖的核心假设,供日后验证]
  249. 1.
  250. 2.
  251. 3.
  252. 4.
  253. 5.
  254. ---
  255. ## 四、快速筛选(8问检查)
  256. | # | 维度 | 结果 | 说明 |
  257. |---|------|------|------|
  258. | 1 | 能力圈 | [是/否] | |
  259. | 2 | 持久性 | [是/否] | |
  260. | 3 | 护城河 | [是/否] | |
  261. | 4 | 定价权 | [是/否] | |
  262. | 5 | 盈利质量 | [是/否] | |
  263. | 6 | 债务安全 | [是/否] | |
  264. | 7 | 管理层诚信 | [是/否] | |
  265. | 8 | 合理价格 | [是/否] | |
  266. ---
  267. ## 五、企业质量分析
  268. ### 护城河
  269. - **类型**: [成本优势/转换成本/网络效应/无形资产/高效规模]
  270. - **强度**: [强/中/弱]
  271. - **趋势**: [拓宽/稳定/变窄]
  272. ### 管理层
  273. - **诚信度**: [评估]
  274. - **资本配置能力**: [评估]
  275. - **所有者心态**: [评估]
  276. ### 商业模式
  277. - **类型**: [特许经营权型/商品型/混合型]
  278. ### 制度迫力预警
  279. - [有/无] — [依据]
  280. ---
  281. ## 六、财务快照
  282. | 指标 | 数值 | 评估 |
  283. |------|------|------|
  284. | ROIC (10年均值) | — | |
  285. | 现金转化率 | — | |
  286. | 债务安全性 | — | |
  287. | 所有者收益估算 | — | |
  288. ---
  289. ## 七、估值分析
  290. - **内在价值区间**: —
  291. - **当前安全边际**: —% (确定性水平:高/中/低)
  292. - **建议买入价**: —
  293. ---
  294. ## 八、卖出标准逐条检验
  295. | # | 标准 | 判断 | 依据 |
  296. |---|------|------|------|
  297. | 1 | 价格严重高估? | [是/否] | |
  298. | 2 | 基本面护城河破坏? | [是/否] | |
  299. | 3 | 管理层诚信问题? | [是/否] | |
  300. | 4 | 有更好的机会? | [是/否] | |
  301. ---
  302. ## 九、主要风险(最多3条)
  303. 1. **风险一**: [描述]
  304. 2. **风险二**: [描述]
  305. 3. **风险三**: [描述]
  306. ---
  307. ## 十、监控指标
  308. ### 每季度检查:
  309. - [指标1]
  310. - [指标2]
  311. ### 触发卖出信号:
  312. - [信号1]
  313. - [信号2]
  314. ---
  315. ## 十一、综合判断
  316. [以巴菲特的视角和语气,直接给出决策建议和核心理由]
  317. ---
  318. > ⚠️ 以上分析基于巴菲特价值投资理念框架,仅供参考,不构成投资建议。投资有风险,入市需谨慎。
  319. """
  320. return template
  321. def _truncate_text(s: str, max_len: int) -> str:
  322. if not s:
  323. return ""
  324. if len(s) <= max_len:
  325. return s
  326. return truncate_at_natural_boundary(s, max_len, "\n\n…(内容过长已截断)")
  327. def _ensure_hello_agents_path() -> None:
  328. _hello = _PROJECT_ROOT / "HelloAgents Optimized"
  329. if str(_hello) not in sys.path:
  330. sys.path.insert(0, str(_hello))
  331. def make_buffett_llm_client():
  332. """构造用于巴菲特长文生成的 LLM 客户端(流式/非流式共用)。"""
  333. _ensure_hello_agents_path()
  334. from hello_agents.core.llm import HelloAgentsLLM
  335. buffett_llm_timeout = max(int(settings.LLM_TIMEOUT), 180)
  336. return HelloAgentsLLM(
  337. model=settings.LLM_MODEL_ID,
  338. api_key=settings.LLM_API_KEY,
  339. base_url=settings.LLM_BASE_URL or None,
  340. provider=os.getenv("LLM_PROVIDER", "auto"),
  341. temperature=0.35,
  342. max_tokens=6144,
  343. timeout=buffett_llm_timeout,
  344. )
  345. def prepare_buffett_ai_messages(stock_code: str, stock_name: str = "") -> Dict[str, Any]:
  346. """聚合行情/财务/舆情并组装 LLM messages。
  347. Returns:
  348. 成功: {"ok": True, "messages": [...], "name": str}
  349. 失败: {"ok": False, "error": str}
  350. """
  351. if not settings.is_agent_ready():
  352. return {
  353. "ok": False,
  354. "error": "未配置有效的 LLM_API_KEY,无法一键生成 AI 评估报告",
  355. }
  356. code = (stock_code or "").strip()
  357. if len(code) < 4:
  358. return {"ok": False, "error": "请输入有效的股票代码"}
  359. try:
  360. from app.services.market_service import (
  361. get_stock_financial,
  362. get_stock_profile,
  363. get_stock_quote,
  364. )
  365. from app.services.news_service import analyze_sentiment
  366. from app.services.analysis_service import _extract_stock_name, _format_data_section
  367. quote_data = get_stock_quote(code)
  368. financial_data = get_stock_financial(code)
  369. profile_data = get_stock_profile(code)
  370. sentiment_data = analyze_sentiment(code)
  371. name = (stock_name or "").strip() or _extract_stock_name(profile_data) or code
  372. chunks = []
  373. if quote_data.get("success"):
  374. chunks.append("### 行情\n" + _format_data_section(quote_data))
  375. else:
  376. chunks.append(
  377. "### 行情\n获取失败: " + str(quote_data.get("error") or "未知错误")
  378. )
  379. if financial_data.get("success"):
  380. chunks.append("\n### 财务\n" + _format_data_section(financial_data))
  381. else:
  382. chunks.append(
  383. "\n### 财务\n获取失败: " + str(financial_data.get("error") or "未知错误")
  384. )
  385. if profile_data.get("success"):
  386. chunks.append("\n### 公司概况\n" + _format_data_section(profile_data))
  387. else:
  388. chunks.append(
  389. "\n### 公司概况\n获取失败: " + str(profile_data.get("error") or "未知错误")
  390. )
  391. if sentiment_data.get("success"):
  392. news_items = sentiment_data.get("news_items") or []
  393. report_items = sentiment_data.get("report_items") or []
  394. ann_items = sentiment_data.get("announce_items") or []
  395. chunks.append(
  396. f"\n### 舆情摘要\n新闻 {len(news_items)} / 研报 {len(report_items)} / 公告 {len(ann_items)}"
  397. )
  398. merged = (news_items + report_items + ann_items)[:12]
  399. for item in merged:
  400. title = item.get("title") or ""
  401. date = (item.get("date") or "").split()[0] if item.get("date") else ""
  402. chunks.append(f"- [{date}] {title}")
  403. else:
  404. chunks.append(
  405. "\n### 舆情\n获取失败: " + str(sentiment_data.get("error") or "未知错误")
  406. )
  407. data_bundle = _truncate_text("\n".join(chunks), 14000)
  408. framework_desc = _truncate_text(BUFFETT_FRAMEWORK_DESC.strip(), 5000)
  409. outline = _build_buffett_report_template(code, name)
  410. user_prompt = f"""请撰写完整的《巴菲特价值投资评估报告》(Markdown)。
  411. 标的:**{name}**(股票代码 {code})
  412. 【须覆盖的报告结构与要点】
  413. 以下提纲中的章节结构与顺序必须体现在你的输出中(使用 ## / ### 标题);每个章节需要实质性段落或列表,禁止只输出标题或空白占位。
  414. {outline}
  415. 【价值投资框架参考】(按需引用,勿全文照搬)
  416. {framework_desc}
  417. 【客观数据】(结论必须以此为依据,勿编造数据中不存在的精确数值)
  418. {data_bundle}
  419. 写作要求:
  420. 1. 「结论」「综合判断」中必须明确:**买入 / 不买 / 持续观察 / 持有 / 卖出** 之一,并附简短理由。
  421. 2. 「快速筛选」按 8 个维度逐条给出判断与简要依据。
  422. 3. 估值与安全边际:若数据不足以定量,请定性说明并列出需补充的信息,勿捏造 PE/PB。
  423. 4. 文末单独一行:⚠️ 以上分析仅供参考,不构成投资建议。投资有风险,入市需谨慎。
  424. """
  425. system = (
  426. "你是资深证券投资分析师,精通巴菲特与格雷厄姆的价值投资框架。"
  427. "你只输出 Markdown 正文,语气专业、审慎。"
  428. )
  429. messages = [
  430. {"role": "system", "content": system},
  431. {"role": "user", "content": user_prompt},
  432. ]
  433. return {"ok": True, "messages": messages, "name": name}
  434. except Exception as e:
  435. return {"ok": False, "error": str(e)}
  436. def iter_buffett_ai_report_events(stock_code: str, stock_name: str = "") -> Iterator[Dict[str, Any]]:
  437. """供 NDJSON 流式响应:通过巴菲特评估Agent (ReflectionAgent) 生成报告。"""
  438. prep = prepare_buffett_ai_messages(stock_code, stock_name)
  439. if not prep.get("ok"):
  440. yield {"type": "error", "message": prep.get("error") or "准备失败"}
  441. return
  442. code = (stock_code or "").strip()
  443. name = prep.get("name") or code
  444. yield {"type": "meta", "stock_code": code, "stock_name": name}
  445. try:
  446. from agents.advisor_agent import evaluate_buffett_stream
  447. for event in evaluate_buffett_stream(
  448. stock_code=code,
  449. stock_name=name,
  450. ):
  451. yield event
  452. yield {"type": "done"}
  453. except Exception as e:
  454. yield {"type": "error", "message": str(e)}
  455. def generate_buffett_ai_report(stock_code: str, stock_name: str = "") -> dict:
  456. """调用 LLM 生成填充后的巴菲特风格 Markdown 报告(同步阻塞,请在 asyncio.to_thread 中调用)。
  457. Returns:
  458. {"success": bool, "report_markdown": str | None, "error": str | None}
  459. """
  460. result: dict = {"success": False, "report_markdown": None, "error": None}
  461. prep = prepare_buffett_ai_messages(stock_code, stock_name)
  462. if not prep.get("ok"):
  463. result["error"] = prep.get("error") or "准备失败"
  464. return result
  465. try:
  466. llm = make_buffett_llm_client()
  467. md = llm.invoke(
  468. prep["messages"],
  469. max_tokens=6144,
  470. temperature=0.35,
  471. )
  472. md = (md or "").strip()
  473. if not md:
  474. result["error"] = "LLM 返回为空,请稍后重试"
  475. return result
  476. result["success"] = True
  477. result["report_markdown"] = md
  478. return result
  479. except Exception as e:
  480. result["error"] = str(e)
  481. return result
  482. def load_buffett_reference(ref_name: str) -> Optional[str]:
  483. """加载指定的巴菲特参考文件内容
  484. Args:
  485. ref_name: 参考文件名,如 "03-business-moat"
  486. Returns:
  487. 文件内容文本,若文件不存在返回 None
  488. """
  489. safe_name = ref_name.replace("..", "").replace("\\", "").replace("/", "")
  490. ref_path = _BUFFETT_DIR / "references" / f"{safe_name}.md"
  491. if not ref_path.exists():
  492. return None
  493. try:
  494. with open(ref_path, "r", encoding="utf-8") as f:
  495. return f.read()
  496. except Exception:
  497. return None