接上回:观潮诞生记——两个半小时搭了个 MCP-Native 的 AI 投研引擎。
那篇文章的结尾我说了,Phase 2 只做三件事:信号追踪、行为护栏、全市场扫描。实际上,第二天我做了远不止三件事。
24 个 commit,2495 行代码,从本地的 stdio 工具变成了 https://tidewatch.polly.wang/mcp 上的云端服务。这篇记录一下这个进化过程。
Phase 2:让引擎有记忆、有底线
信号追踪:时间的朋友
第一版观潮每次分析完就忘。分析了 14 只票,哪些涨了哪些跌了,全靠我自己记。
信号追踪系统用 SQLite 解决了这个问题。每次 analyze_stock 出结论,自动记一条:
#14 002111 看空(-32) @ 10.59 2026-03-12
5、10、20 日后自动回填实际走势,算胜率。这是时间的朋友——分析 100 只票之后,才能知道引擎到底准不准。
为了防止焦虑式重复分析(同一只票 5 分钟内看 3 次),加了 5 分钟去重窗口。
行为护栏:阻止你犯蠢
最好的交易系统不是告诉你买什么,而是阻止你犯蠢。
三条规则:
| 护栏 | 触发条件 | 提醒 |
|---|---|---|
| 追高检测 | 20 日新高 + 涨幅 > 5% | ⚠️ "追高风险极高" |
| 分析频次 | 当天分析 > 10 只票 | 📊 "是不是有点焦虑?聚焦 2-3 只最有把握的" |
| 连续看空 | 连续 3 次以上看空 | 🔴 "要么市场真的不好,要么心态不对" |
那天下午我连着分析了 13 只票,护栏真的弹出了:
📊 今天已经分析了 13 只票(002402, 512400, 601127, 601899, 300750...),是不是有点焦虑?分析太多反而犹豫不决。聚焦 2-3 只最有把握的,其他的放一放。
被自己的工具教育了一把。
Phase 3:让引擎会说话
LLM 叙事润色
Phase 1 的报告是模板拼接的——虽然信息密度高,但读起来像机器吐出来的表格:
"评分+17,偏多。均线上行。RSI 中性。主力净流入 466 万。"
加了 LLM 润色之后(通过 CopilotX API 调 Claude Sonnet 4),同样的数据变成了:
"002111 现在 10.59,这票有点纠结啊。技术面确实不太好看,跌破了 5 日线,离 20 日线也有点远,MACD 绿柱还在放大。但是!主力资金今天净流入 466 万,技术面在跌但资金在进,这种背离要么是洗盘,要么就是诱多。"
关键设计:LLM 失败时 fallback 到模板叙事,不能因为 API 抽风就整个分析挂掉。
ETF 也要分析
有人问了一句"帮我看看有色金属 ETF(512400)"——然后报错了。
原来 AKShare 的 A 股历史接口 stock_zh_a_hist 不支持 ETF,得用 fund_etf_hist_em。加了一个 _is_etf() 方法,根据代码前缀(51/15/16/56/58)自动路由到 ETF 接口。ETF 没有资金流向和新闻(东方财富不提供),分析时自动跳过。
Phase 4:从本地到云端
为什么要部署到云
两个原因。
第一,本地的 MCP Server 是 stdio 模式——VS Code 启动它、VS Code 关掉它。换台电脑或者用手机就没法用了。
第二,有个 bug 让我以为部署到服务器能修好(剧透:不能,后面会说)。
FastMCP 的优雅
Stylist-MCP-Server(我的另一个项目)用的是原生 MCP SDK + Starlette + 手写路由。观潮用的是 FastMCP 2.x,部署比 Stylist 简单多了。
核心代码只有这几行:
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--http", action="store_true")
parser.add_argument("--port", type=int, default=8889)
args = parser.parse_args()
if args.http:
mcp.run(transport="streamable-http", port=args.port,
middleware=_build_middleware(), stateless_http=True)
else:
mcp.run() # stdio
同一个引擎,poetry run tidewatch 是本地模式,加个 --http 就是远程 HTTP 服务。
API Key 认证
远程服务必须有认证。写了一个纯 ASGI 中间件,支持 X-API-Key header 和 Bearer token,/health 端点免认证:
无 Key → POST /mcp → 401 Unauthorized
错 Key → POST /mcp → 401 Unauthorized
对的 Key → POST /mcp → MCP 正常握手 ✅
无需 Key → GET /health → 200 {"status":"healthy"} ✅
setup.sh 用 openssl rand -hex 12 自动生成 API Key,不用人工编密码。
部署五步走
客户端 (VS Code / Claude Desktop)
│ HTTPS + API Key
▼
tidewatch.polly.wang:443 (Nginx + Let's Encrypt SSL)
│ proxy_pass
▼
127.0.0.1:8889 (FastMCP Streamable HTTP)
│ systemd 守护
▼
Poetry venv (Python 3.12 via miniforge3)
实际部署过程比画架构图精彩——Azure VM 上的 Python 是 3.8(Poetry 2.x 要求 3.10+),好在 Stylist 的 venv 里有 miniforge3 的 Python 3.12,借来用了。systemd 的 ExecStart 也踩了坑:Poetry 装在 miniforge3/bin/ 而不是 .local/bin/,第一次启动 status=203/EXEC,调了两版才跑起来。
最后的验证:
curl https://tidewatch.polly.wang/health
# {"status":"healthy","server":"TideWatch-观潮","version":"0.2.0",
# "tools_count":10,"auth_enabled":true}
10 个工具,认证开启,HTTPS,在线。
一个反转的 Bug 故事
这个 debug 过程值得单独讲。
问题
scan_market(全市场扫描)在本地一直报 ConnectionError。AKShare 的 stock_zh_a_spot_em() 接口请求 push2.eastmoney.com,SSL 握手成功但返回 Empty reply(空响应)。
第一次误诊:DNS
用 nslookup 发现,Google DNS (8.8.8.8) 把 push2.eastmoney.com 解析到了腾讯云 IP 43.144.251.121,而国内 DNS (114.114.114.114) 解析到阿里云 IP 47.112.165.11。中间还有个 Azure Traffic Manager(push2ipv6.trafficmanager.cn)做区域路由。
我当时的结论:DNS 问题,部署到 Azure VM 上就好了。
这成了我推动云端部署的动力之一。
反转
部署完之后,兴冲冲地在 VM 上跑 scan_market——
{"error": "全市场数据获取失败,请稍后重试"}
VM 上也是一样的 Empty reply。DNS 解析到了不同的 IP(61.129.129.196,上海电信),但照样空回复。
第二次尝试:模拟浏览器
给 requests session 加了完整的浏览器 headers(User-Agent、Referer、Accept-Language)——还是空回复。
真相
push2.eastmoney.com 不是在检查 DNS 或 User-Agent。它对非浏览器环境的请求做了更深层的反爬限制——可能是 TLS 指纹、IP 信誉,或者其他我们看不到的机制。
有趣的是,AKShare 最常用的其他接口全部正常:
| API | 域名 | 状态 |
|---|---|---|
日K线 stock_zh_a_hist | finance.eastmoney.com | ✅ 正常 |
| 资金流向 | push2his.eastmoney.com | ✅ 正常 |
| 新闻 | search-api-web.eastmoney.com | ✅ 正常 |
全市场实时 stock_zh_a_spot_em | push2.eastmoney.com | ❌ 被封 |
只有 push2(实时行情推送服务)做了反爬。这是它的特殊性——实时行情有商业价值,东方财富不希望被程序化抓取。
解决方案
既然绕不过 push2,那就不用它。下一版 scan_market 会改成板块扫描模式:通过行业板块接口(走 datacenter-web,可用)获取热门行业的成分股,再批量拉 K 线做技术评分。不需要全市场 5000 只,扫热门板块 200-300 只就够了。
教训:先验证核心假设再做大改动。 如果我在 VM 上先测过 push2 再决定部署,可以省掉一些不必要的期待。但也没关系——云端部署本来就是该做的,只是动力来源搞反了。
回望
两天的 commit 历史铺开来看:
| 时间 | 里程碑 |
|---|---|
| 3/11 23:36 | 🌊 Phase 1:MCP 引擎诞生,5 模块 7 工具 |
| 3/12 00:15 | Phase 2:信号追踪 + 护栏 + 全市场扫描 |
| 3/12 00:33 | Phase 3:LLM 叙事润色(CopilotX + Claude Sonnet 4) |
| 3/12 11:23 | ETF 支持(自动路由 stock vs fund API) |
| 3/12 22:54 | Phase 4:HTTP 双模式 + API Key 认证 |
| 3/12 23:35 | ✅ 云端部署完成:tidewatch.polly.wang/mcp |
24 个 commit,从 1618 行到 2495 行,从 7 个工具到 10 个。从一个在 VS Code 里跑的本地脚本,变成了一个有认证、有 SSL、有 systemd 守护的云端投研引擎。
下一步?先让 scan_market 用板块扫描绕过反爬,然后积累足够的信号数据,看看这个引擎到底准不准。
毕竟,引擎是否有价值,不是我说了算,是时间和市场说了算。

