From ab23dfd234d87cc8a1ff5a2a427ada485d709954 Mon Sep 17 00:00:00 2001 From: hmo Date: Tue, 30 Jun 2026 01:17:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20DSA=20full=20integration=20=E2=80=94=20?= =?UTF-8?q?mo=5Fbridge=20v2=20+=20strategy=5Flifecycle=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mo_bridge.py (rewrite): - get_stock_news(): DSA SearchService 7 engines → MoFin analysis context - get_market_review(): DSA run_market_review() with 24h cache - get_stock_analysis(): DSA AgentExecutor.run() with 15 strategies - enrich_analysis_context(): one-call context injection strategy_lifecycle.py: - reassess_with_context() now injects DSA market + news context - Auto-detects HK vs A-share region for market review - Graceful fallback if DSA unavailable --- __pycache__/price_monitor.cpython-312.pyc | Bin 26064 -> 31148 bytes .../strategy_lifecycle.cpython-312.pyc | Bin 78494 -> 78983 bytes mo_bridge.py | 335 ++++++++++++------ strategy_lifecycle.py | 11 + 4 files changed, 245 insertions(+), 101 deletions(-) diff --git a/__pycache__/price_monitor.cpython-312.pyc b/__pycache__/price_monitor.cpython-312.pyc index 1052c04c81a39f6de5dd22de8ca14069251be502..0d03c30a042364abbffa7384eb75f797613a6ad8 100644 GIT binary patch delta 9038 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 DfEXUD delta 4898 zcmds4`*R${5uUldz4t?>J6W2Sv6=;5)E3VeCqnZB2_e$P@BpV5sH@QHK64(o2MDC6z6KyiU#$T~7bZz}~) zxl~u)FMgnyS@Nl_amwgqnVqOEe#BPf_eq6@MyYTi*p}Bv1@x2CZ)gNAwA}%topq~j z9N$nh$MMYbyvN{z=V8GxWA+o~TX1bYe$*cD78|3oqH0RjbDz~eG3WlQSM_G-o5C0( zmFis`puSXBFB%jKD$DvoMWO2N7gVR}#ax-9lNDy7I-~qiN1%R_u_Y9UPyT_eu`KIQ zDvY+|Q62m@j+#oZ5gCki!qi73ch+&IBRc!$70#yKvAIoDK6VN;T)F#4SH62{=Fr)< zzjXM@iOHES+#TK?dFR}Ne>ihs=H!Vhr=FQP{rQ=D4*l`)(V0UBW{w}a^7#{g`qpFb zoI4l>s$u5LDR}Y2&L#(p8Qk5{vZ|%!zT)e6$W_vynFe?7FB(*o(d$E@JCx=DB{@9M zoJfc6xIv?h`yIDrWY!cOkkd+#r(M;)GBD?N4?g& z_)S64LGv5Fz*H|^_(0{6%FDjR2Q61+Qsy3_;rW=v|X=}JD(r$XbFf79B zXC>hhc)hSf+|Xft;d<-t>eD1LV zr5Du??EI~pigW9hY@scR-;k@s$ZO-BHB08K2vw}e=#yHYmzWu=A1ab z)zu45D7c?14DgYqF1~M>uPLw4i8WU;;lMeyuBW8lLq-)!CKnDZ(h2FHt~CxmeCM@ zd}%FLf_|QDYT>?Mz4KZOpuH@>KOOYB3qBH=QkzN3y&o69$U%;j*H>M6`$Z}97m_;>rQB{gt^lIQ?Q(k$&G zQ|8GYll39Q0QNWWXN3xG3;Ao`#`{T-YL;YLf=a%gVb6hoKK|~dExWdN z*+3jB-Fm8OcOG)qM1{fk+fzW8i`Aqd1O>hYo6`fdV1Gy+Z@>z z+aB)Sz&b&jCd4I4^L#qo+qGeHB-)#oV8w@_$&A7)Tgt1@i_DBXk&TR{CC$2FQ>-W4 z+YO_4Wmp&QYgrk+0p!7eW{(dI#S$ZuoY739{ThYWo*{icSp*f* zQH>@vAu%|hSrdct^nfhstJG+W_3^*AG+DMow@m&p>TWG_9D*i8)@hzvE(IxaoB z`k|4_?&bsQb2V)z1sdirt*E4n`PCISM|*0>`LeLPCq$-&=AJcVx~(1XLS3gP>LkCc z>AW%OCzon0J&nSpkfoYZ!7j&H#bg>i)b|VY{Xr6W5U6E*H+t%p3a5*5flH4C38&a5pUGX?!cOalw zSOx*(6}05d`Z6*wZ#03$_h?ko=&lT2?yIV*0`Ox6*x{%F?~>dXZ+MXw>lQeoy%6O5 z?0MsOs-sFg37tiLzSE!kdWTG1w?lJ1gKq)C`nf+`)A2Zxs35;0G%0Apdj~ z^Lln_&5?84Hq5sGda&>CM>-dYKLFAPW~`-8=Kj^` zp*!))1<^Q-U7kY7A$$+vEW*c3GDAAXKW>p*w`doW3OZdRL?erObX5HCY z%ersv-r74GnQMXj4*zm@xp*E%EXtkg9;SW&!=`6H$tLz=Z(0{2Wk-CX%}dk%%qBVf+4pCP<} zpxdYCsi8ywE&MMVo5hzw-k4jo>8k>LfS=iVcOe!g)`fuP1g-4beA~87;%}i5$$f3x z)Bd7K6h4gb2*TG8B0QC7TD}-a&4TGPmVvCZ#1je@n>&HMo?+fGG_jZc8p#I`j`5!- zcGA~)y%e_KR}A|VPfLp|myvvh-zPmwr+8HUEPXZilx(5&Hh#9h5w`M7f2W0GpZj?8 zKn-2Odj^8m{opj%iP0n0WUMZnslJCC7rS%~)zUbpQ7B(Fmd z5cFVRl<|ClFe>bu$ZkW9D2)w|W;BbO#!}AkEMqGXRw1lLc$&XDSl#d~B)<&+3uCgZ z*_qtW-LXVJ`#TE%L3lj1*$Zbr(X6BKeRx{3E&N>S7BK|8{@jAy8=Y`tH6TY<5W zPmDLM#Ow<$*Q~LaG?IwL^fIS;#@NtMa#u5x?--Nen@acDX`Dn4nn3yQ#v3Q}$POcS z1VOJ$IiO&B?j4P%r7;KudmKg2BG_O~W<{`L(*l&{j0}&CFr}NZ5yr-lttW}T5%iRD zBJX~LuOOIlcso)%$fp1gA*may|C)Ho6 zte=5d<^Hs3B$qEjI|wsvPnU>xk;6re)KLl9ndy#0LO|+TS1PZmI|f4IVIwv7kCvQ?4j<`rD7?+Sg2M^V6W&JLb!C8RkQ)+ zZpwz4=?driwArjMdLhLCilcNH_K9WtfagTAi$kvMQ@sGvVHFYzR6wXuv$5h39pncUG`NLG)9r}M?@d4pmia6 z%*jnkE{|0O+_WDDML)@)kE~N{;=w8UhtRM}$v?T9M4TuHa&A7V8HG_*O+`9S@lWhj z|2Ih}B%g?OMB0*VUHemsR3y45Da3jOp1gMy zWU^3`g%EEchb$9KwggpKeM;C9-A#BnrtFh#-Lb5OkHr&vlG&m}GTOBl$0EsCPpXg6 z>-n|h-^KeVL@7iF1V=k>o&|7Ao=mkj%XROGyvHYSJjMq}T)c+3 z@vSFpL#;z)CqqBF=AE~mZ5efa7dq#?=(-bHf7o{Wt(F1J155C1%SC)YwCEy!#e#Js z3}_!4Ece|NC-&WOSD)41aWDGWwfv52`CZqW1M6SuMwi|Gje&&ZzdN(!uUB4FWPgD` zZOrP5p0Q;8`$eO)`tGLw0g}kkrjkv5bg8DvzNrC?S2toixLWfb3F=uJbDj)-qD-f$zj9qp@YqwWVrW(ei5xPg>9qv@4aO84fIUm-O-=!zf{Ap zbnW46*djf9xRPMqqaGNMPCxoxy^~N5j1EnfSHa}i1p{;dG>^?O!geN5PfY}9cViUF zDPRgS2?RC#7WwNLa1_?a%S=#f3RCv?RQxA}gxqg}k02#8X80D`F8K=! z?1i^wy%m;fa&b4v8?5kAMP5ufn9j*fVkgV>bVYK*naVQnxAWWN=T`7*(v);ScGzGk zES9(0U^VQJkJ%s$tK_FPXoijQd^-eTiTsWoDrYxR`gUS7tESJOjc=zHYbmUwu%5yO z`5Qaby0?>kR-1}NBhij{mLmfni6sSoKOdH#+hI8*#%fAH%c#c*@61@E6TD9NL~gBt zkm(>bqovu?m&ku0AFBc0P6s&GnOr+~T5x_`4uqf*ZprIH@Q0=o!17|d+*}LunXOA@ Ip%&);1D6y4oB#j- delta 1567 zcmZ`(ZA@EL7(VaqZJ~vhe%*Up`UUNvd(q1bABvi#ElYg5zY7`ERs0=3!>lT>pjE29{$|6JkJ$O_~-d`F6+YoUJKy^KUHC}Aes4b zBSI%ZHsU)AlFKA>GaSo9NUc;Isy!>YyQk|+X@jC$`{!{u_zU)s7EUwmsy>Ql;S_h710t z1Ul#tJW%evb199lijL#4#~o_LC*#{3blyS9ODx2zwVoS%zQ8h84l1O(tNU{`bI%9 zt5h3=aau-a8tCcqDBGadR<{vpe$}tMrurGJ-deqrk}JdED|u`Dx|d=Ilx)6WnJqMJ z)OUcgf$baAe7&m5FZ&q}6i*$)&5mhLK(1E<6z@1)@B3+GyBau$rg=6YyCWx0P<%sW zXu|i=ly{DlZ1~%2R@?u82rSmKrriO#vDmayjusmgsbr1JC>!0T$1FF=%|oDaGlr5) zv1Xg`D~s_VF8}Em${G657^ErfX-@Nup$IA-fVU653Hf}<8$nsSUT+6dyqZup1ih_v zNbUh$eMqJeFA6kYE@}9QLMyc3Q-$}d57P&tgqYya?AfKS00f*`Zo^Z{X$X?f>~!l~ z>BYH~J_usR%k}u#Z%p{!id40mKEB&LeIGXsIQabp_orB9Y-w-;dmu=FFxc?iKt+4 zBtwQm5SR=bsk;#6lkLVrtBJ-GV&;^~Q0TIyY=$M9k?KpLITbN1g@MX+kzM@H>Lp7% zg{m-dWcKopTL#Ncwr8Su_UpARBTV88>y2;--&}u`%D~1LyocZ3_|4c$Lo%}yw<4{u zI`@$kjstYfIcnf22W{j92ZZ1d$va>Q9wCiR=&&bP-7guJe={*ajyvHr43XcR@F5%{ zlP=gaJ str: - """从 DSA 获取市场背景和新闻舆情,注入 MoFin 分析上下文。 +# ── 懒加载 DSA 模块 ────────────────────────────────────────────────── + +_dsa_search_service = None +_dsa_config = None + + +def _ensure_dsa_search(): + global _dsa_search_service + if _dsa_search_service is not None: + return _dsa_search_service + if not _HAS_DSA: + return None + try: + from src.search_service import get_search_service + _dsa_search_service = get_search_service() + except Exception as e: + logger.warning("DSA SearchService 加载失败: %s", e) + return _dsa_search_service + + +def _ensure_dsa_config(): + global _dsa_config + if _dsa_config is not None: + return _dsa_config + if not _HAS_DSA: + return None + try: + from src.config import get_config + _dsa_config = get_config() + except Exception as e: + logger.warning("DSA Config 加载失败: %s", e) + return _dsa_config + + +# ── 1. 新闻搜索 ───────────────────────────────────────────────────── + +def get_stock_news(stock_code: str, stock_name: str = "", max_results: int = 5) -> str: + """通过 DSA 的 7 个搜索引擎获取股票相关新闻。 Args: - region: cn/hk/us/both — 分析哪个市场 + stock_code: 股票代码 (如 '600519', '00700', 'AAPL') + stock_name: 股票名称 (提高搜索精度) + max_results: 最多返回条数 Returns: - str: Markdown 格式的市场背景文本(可直接注入分析 prompt) - 如果 DSA 不可用,返回空字符串 + str: Markdown 格式新闻摘要,可直接注入分析 prompt。失败时返回 ''。 """ - parts = [] + service = _ensure_dsa_search() + if not service: + return "" - # 1. 大盘复盘 - market_text = get_market_review(region) - if market_text: - parts.append(f"## 今日大盘背景\n{market_text}") + try: + intel = service.search_comprehensive_intel( + stock_code, stock_name or stock_code, max_searches=3 + ) + if not intel: + return "" + + lines = [f"## 📰 {stock_name or stock_code} 最新情报"] + + news = intel.get("latest_news") + if news and news.results: + lines.append("\n### 最新新闻") + for r in news.results[:max_results]: + date_str = f" ({r.published_date})" if r.published_date else "" + snippet = r.snippet[:150] if r.snippet else "" + lines.append(f"- **{r.title}**{date_str}: {snippet}") + + risk = intel.get("risk_check") + if risk and risk.results: + lines.append("\n### ⚠️ 风险关注") + for r in risk.results[:3]: + lines.append(f"- {r.title}: {r.snippet[:100] if r.snippet else ''}") + + return "\n".join(lines) - # 2. 搜索舆情(如果有 DSA search_service) - news_text = get_news_context() - if news_text: - parts.append(f"## 今日重要新闻\n{news_text}") - - return "\n\n".join(parts) + except Exception as e: + logger.warning("DSA 新闻搜索失败: %s", e) + return "" -def get_market_review(region: str = "cn") -> str | None: - """获取 DSA 市场复盘摘要""" +# ── 2. 大盘复盘 ───────────────────────────────────────────────────── + +def get_market_review(region: str = "cn") -> str: + """获取 DSA 的大盘复盘报告。 + 优先读本地缓存(24h内),没有则调用 DSA 实时生成。 + + Args: + region: 'cn'=A股, 'hk'=港股, 'us'=美股, 'both'=全市场 + + Returns: + str: Markdown 格式大盘复盘摘要 + """ + if not _HAS_DSA: + return "" + + # 先读缓存 + cache_dir = Path(str(_DSA_BASE)) / "data" / "market_review" + if cache_dir.exists(): + try: + files = sorted(cache_dir.glob("*.md"), key=os.path.getmtime, reverse=True) + if files: + if (datetime.now().timestamp() - os.path.getmtime(str(files[0]))) < 86400: + content = files[0].read_text(encoding="utf-8") + lines = [l for l in content.split("\n")[:30] if len(l.strip()) > 3] + return "## 📈 今日大盘背景\n" + "\n".join(lines) + except Exception: + pass + + # 实时调用 DSA + try: + from src.core.market_review import run_market_review + + class StubNotifier: + def is_available(self): return False + def send(self, *a, **kw): return True + def save_report_to_file(self, *a, **kw): return None + + config = _ensure_dsa_config() + result = run_market_review( + notifier=StubNotifier(), config=config, + override_region=region, send_notification=False, + save_report_file=False, persist_history=False, + trigger_source="mofin", + ) + if result and isinstance(result, str): + lines = [l for l in result.split("\n")[:25] if len(l.strip()) > 3] + return "## 📈 今日大盘复盘\n" + "\n".join(lines) + except Exception as e: + logger.warning("DSA 大盘复盘失败: %s", e) + + return "" + + +# ── 3. 策略问股(第二意见)─────────────────────────────────────────── + +def get_stock_analysis( + stock_code: str, + stock_name: str = "", + skills: list = None, +) -> dict | None: + """用 DSA 的 15 种内置策略独立分析一只股票。 + + Args: + stock_code: 股票代码 + stock_name: 股票名称 + skills: 策略列表,默认 ['ma_golden_cross', 'bull_trend'] + + Returns: + dict: {source, sentiment_score, operation_advice, trend_prediction, + analysis_summary, risk_warning, strategies_used, raw} + """ if not _HAS_DSA: return None + if not skills: + skills = ["ma_golden_cross", "bull_trend"] + try: - sys.path.insert(0, str(_DSA_BASE)) + from src.agent.factory import build_agent_executor - # 尝试从 DSA 的本地缓存中读取最近的市场复盘 - from src.services.daily_market_context import DailyMarketContextService - - # 先看本地是否有缓存 - cache_dir = _DSA_BASE / "data" / "market_review" - if cache_dir.exists(): - files = sorted(cache_dir.glob("*.md"), key=os.path.getmtime, reverse=True) - if files: - content = files[0].read_text(encoding="utf-8") - # 只取摘要部分(前 500 字) - lines = content.split("\n") - summary_lines = [] - for line in lines: - if len(line.strip()) > 5: - summary_lines.append(line) - if len(summary_lines) >= 20: - break - return "\n".join(summary_lines) - - # 如果没有缓存,尝试实时获取(需要 DSA 完整配置) - logger.debug("未找到 DSA 市场复盘缓存,跳过") - return None + executor = build_agent_executor(skills=skills) + result = executor.run( + task=f"分析 {stock_code} {stock_name}", + context={"stock_code": stock_code, "stock_name": stock_name, "report_language": "zh"}, + ) + if result.success and result.dashboard: + d = result.dashboard + return { + "source": "DSA", + "sentiment_score": d.get("sentiment_score", 0), + "operation_advice": d.get("operation_advice", ""), + "trend_prediction": d.get("trend_prediction", ""), + "analysis_summary": d.get("analysis_summary", ""), + "risk_warning": d.get("risk_warning", ""), + "strategies_used": skills, + "raw": result.content[:500] if result.content else "", + } except Exception as e: - logger.debug("获取 DSA 市场复盘失败: %s", e) - return None - finally: - # 清理 sys.path - if str(_DSA_BASE) in sys.path: - sys.path.remove(str(_DSA_BASE)) - - -def get_news_context() -> str | None: - """搜索今日重要财经新闻""" - # 预留接口:待 DSA 依赖安装后实现 - # 目前 MoFin 的 mofin_news.py 已覆盖基本新闻需求 + logger.warning("DSA Agent 分析 %s 失败: %s", e) + return None -def get_stock_fundamentals(code: str) -> dict | None: - """通过 DSA 获取股票基本面数据""" - if not _HAS_DSA: - return None +def get_strategy_opinion_text(opinion: dict) -> str: + """将 get_stock_analysis() 的结果格式化为可读文本""" + if not opinion: + return "" + return ( + f"## 🤖 DSA 策略参考\n" + f"- 评分: {opinion.get('sentiment_score', '?')}/100\n" + f"- 建议: {opinion.get('operation_advice', '?')}\n" + f"- 趋势: {opinion.get('trend_prediction', '?')}\n" + f"- 策略: {', '.join(opinion.get('strategies_used', []))}\n" + f"- 摘要: {opinion.get('analysis_summary', '')}\n" + f"- 风险: {opinion.get('risk_warning', '')}" + ) + + +# ── 4. 综合上下文(一键调用)───────────────────────────────────────── + +def enrich_analysis_context( + stock_code: str = "", + stock_name: str = "", + region: str = "cn", + include_news: bool = True, + include_market: bool = True, +) -> str: + """一键获取 DSA 全部分析上下文,注入 MoFin 的 LLM prompt。 - try: - sys.path.insert(0, str(_DSA_BASE)) - from mo_provider import MoDataProvider - provider = MoDataProvider() - return provider.get_fundamentals(code) - except Exception as e: - logger.debug("获取 %s 基本面失败: %s", code, e) - return None - finally: - if str(_DSA_BASE) in sys.path: - sys.path.remove(str(_DSA_BASE)) + 在 strategy_lifecycle.reassess_with_context() 或 Hermes cron job 的 prompt 前调用。 + + Returns: + str: 可直接拼接到 LLM prompt 的 Markdown 文本 + """ + parts = [] + + if include_market: + market = get_market_review(region) + if market: + parts.append(market) + + if include_news and stock_code: + news = get_stock_news(stock_code, stock_name) + if news: + parts.append(news) + + return "\n\n".join(parts) if parts else "" -# ── 便捷入口 ────────────────────────────────────────────────────────── - -def quick_summary() -> str: - """快速获取今日分析上下文(单次调用)""" - return enrich_analysis_context() - - -# ── 自检 ────────────────────────────────────────────────────────────── +# ── 自检 ───────────────────────────────────────────────────────────── if __name__ == "__main__": - print(f"DSA 可用: {_HAS_DSA}") - print(f"DSA 路径: {_DSA_BASE}") - + print(f"DSA: {'available' if _HAS_DSA else 'NOT FOUND'} ({_DSA_BASE})") if _HAS_DSA: - context = enrich_analysis_context() - if context: - print(f"\n=== 市场上下文 ({len(context)} 字符) ===") - print(context[:1000]) - else: - print("\n无可用市场上下文(DSA 缓存为空)") - else: - print("\nDSA 不可用,跳过。部署后需安装依赖:") - print(" pip install litellm akshare yfinance baostock") + print("\n--- 新闻测试 (600519) ---") + n = get_stock_news("600519", "贵州茅台", max_results=2) + print(n[:300] if n else "(无结果)") + print("\n--- 大盘测试 ---") + m = get_market_review("cn") + print(m[:300] if m else "(无结果)") diff --git a/strategy_lifecycle.py b/strategy_lifecycle.py index ea812b6..63a52fd 100644 --- a/strategy_lifecycle.py +++ b/strategy_lifecycle.py @@ -1282,6 +1282,17 @@ def reassess_with_context(code, name, price, cost, shares, current_action, news_sentiment = {} fund = {} + # ── DSA 集成:注入大盘复盘 + 新闻情报 ────────────────────────── + try: + from mo_bridge import enrich_analysis_context + region = "hk" if len(str(code)) == 5 and str(code)[0] in ('0','1') else "cn" + dsa_ctx = enrich_analysis_context(stock_code=code, stock_name=name, + region=region, include_news=True) + if dsa_ctx: + macro_desc = (macro_desc + "\n\n" + dsa_ctx).strip() + except Exception: + pass # DSA 不可用时静默跳过 + enriched, factors = enrich_timing_signal( base_signal=result.get("timing_signal", ""), macro_desc=macro_desc,