观潮托管记:当 AI 引擎学会自己醒来

观潮托管记:当 AI 引擎学会自己醒来

接上回:观潮自省记——52 条信号回头看,AI 引擎学会了第四条纪律。

那篇讲的是让系统回头审视自己。这篇讲的是下一步:让它自己醒来。

8 秒

每天晚上打开观潮 Dashboard,同一个流程:

点开 → 骨架屏 → "正在加载..." → 8 秒后数据刷出来。

18 天了。每天 8 秒,不多,但每次都让我意识到一件事——是我在等它,不是它在等我。

这个系统有 14 个 MCP 工具、三级股票池、LLM 叙事引擎,已经部署在云端 24 小时在线。可它不会自己动。每次都得有人按一下按钮,它才开始干活。

就像养了一条会算命的鱼,但每次想知道运势,还得自己走过去敲鱼缸。

周日晚上,我终于问出了那个问题:为什么不让它自己跑?

不是来看潮的

事情其实不是从 TideWatch 开始的。

周日晚上本来是做 Azure VM 安全体检——小龙虾扫了一圈服务器,出了份 7.5 分的报告。过程中顺手拉了 CopilotX 的日志,发现全是扫描器在敲门:

46.151.182.85 - "GET /beta/.env HTTP/1.0" 401
46.151.182.85 - "GET /.aws/credentials HTTP/1.0" 401
46.151.182.85 - "POST /vendor/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.0" 401

一个 IP,36 条请求,从 .env 扫到 phpunit RCE 再到 AWS 密钥,教科书级的漏洞扫描。还有个执着的朋友 78.153.140.156,每小时回来扫一次 .env,全天三班倒。

好消息是全部 401 挡住了。坏消息是——fail2ban 居然没装。当天已经有 273 次 SSH 暴力破解尝试。

sudo apt-get install -y fail2ban
sudo systemctl enable --now fail2ban

一分钟搞定。刚装完就 ban 了第一个 IP:92.118.39.76,一个拿 oracle、admin、sol、solana 各种用户名爆破的家伙。

顺便杀掉一个幽灵进程——旧的 Blog Backend 在端口 50206 上跑了 30 天,kill 掉之后又自己复活了。原来背后有个 fastapi.service 在守护。systemctl disable 才算彻底送走。

修着修着,我盯着 ss -tlnp 的输出看到了这一行:

LISTEN  0  2048  0.0.0.0:8889  users:(("python",pid=1276173,fd=7))

TideWatch。就在同一台机器上。localhost:8889,零延迟。

三行 cron

方案出奇地简单。

不需要 GitHub Actions,不需要调度框架,不需要消息队列。一台机器上的两个进程,一个负责跑(TideWatch MCP Server),一个负责叫它起床(cron)。

# /home/azureuser/scripts/tidewatch-daily.sh
call_mcp "scan_market" '{"tier":"holdings"}' 120
call_mcp "scan_market" '{"tier":"watchlist"}' 120
call_mcp "update_signal_outcomes" '{}' 600

call_mcp 就是一个 curl,往 localhost:8889/mcp 发 JSON-RPC 请求。没有认证绕行(本地调用不走 Nginx),没有网络开销,没有序列化损耗。

调度:

30 7 * * 1-5 /home/azureuser/scripts/tidewatch-daily.sh

UTC 07:30 = 北京时间 15:30。A 股 15:00 收盘,留 30 分钟让数据落地,然后自动扫描持仓和自选。周一到周五,一天一次。

手动验证的时候发现一个小坑:FastMCP 的 Streamable HTTP 要求请求带 Accept: application/json header,否则返回 406 Not Acceptable。加上就好了。

小龙虾:"最简单的方案就是最好的方案。"

有时候工程师的直觉是加层抽象、建个服务、搞个队列。但这个场景不需要。cron 跑了几十年了,它不会忘记叫闹钟。

0 秒

数据每天 15:30 自动预热了,但 Dashboard 还不知道这件事。

打开页面它照样去调 scan_market,再等 8 秒。这就像冰箱里已经有做好的饭,但你每次回家还是先点外卖,等外卖到了才想起来冰箱里有。

Polly 改了四件事:

智能缓存判断:休盘期间如果本地缓存不到 4 小时——说明 cron 已经跑过了——直接用缓存,跳过 scan_market 调用。

cron 数据检测:如果缓存的时间戳落在 15:20-15:50 之间,自动识别为定时扫描数据,显示一个蓝色的 定时 徽章。

后台预加载:页面打开 3 秒后,静默拉取信号回填数据。等切到"信号复盘"tab 时直接渲染,不用再等。

休盘停止刷新:自动刷新定时器加了 isMarketOpen() 守卫,收盘后不再空转。

小龙虾 Review 给了 8.5 分,挑出一个边界 case:周末。

周五 15:30 的 cron 数据,到周六打开时已经超过 20 小时了,4 小时的缓存 TTL 兜不住,Dashboard 会尝试调 scan_market ——但周末没有新数据,白等。

修复很简单:周末给 48 小时的缓存窗口。

function isCacheFreshEnough(cacheTime) {
  const ageMs = Date.now() - cacheTime;
  if (isMarketOpen()) return ageMs < 5 * 60 * 1000;      // 盘中:5 分钟
  const day = new Date().getDay();
  if (day === 0 || day === 6) return ageMs < 48 * 3600000; // 周末:48 小时
  return ageMs < 4 * 3600000;                              // 工作日休盘:4 小时
}

最终缓存策略:

场景TTL说明
盘中5 分钟实时数据为主
工作日休盘4 小时覆盖 15:30→晚间
周末48 小时周五 cron 数据用到周日

小龙虾终审 9 分。

三层分工

退一步看,观潮现在有了清晰的三层架构:

┌─────────────────────────────────┐
│  Dashboard(展示层)              │  ← 你打开时看到的
│  智能缓存 · 相对时间 · 秒渲染     │
├─────────────────────────────────┤
│  Cron(调度层)                   │  ← 每天 15:30 自动触发
│  scan_market · signal backfill  │
├─────────────────────────────────┤
│  MCP Server(逻辑层)             │  ← 14 个工具,24h 在线
│  分析 · 评分 · 叙事 · 回填       │
└─────────────────────────────────┘

MCP Server 负责所有逻辑和计算。Cron 负责定期触发,累积数据。Dashboard 负责展示。

这是第一版——最小可行的自动化。下一步可以把 cron 换成 Agent,让它不只是按时间跑,而是根据市场状态决定什么时候跑、跑什么、跑完之后要不要主动通知我。

但那是以后的事。现在,它每天 15:30 准时醒来,扫完持仓,写好日志,然后继续等。等我晚上打开 Dashboard 的时候,数据已经在那里了。

今晚的运维清单

顺带记一下今晚的 VM 运维成果,因为这些也是让观潮"活得更稳"的基础设施:

  • fail2ban 安装启用 → SSH 暴力破解自动 ban
  • 旧 Blog Backend 关停 → 端口 50206 释放,systemd 彻底 disable
  • apt upgrade → 34 个包更新(含 grub + docker)
  • OpenClaw 升级 → 2026.2.14 → 2026.3.28,模型切换到 claude-opus-4.6

一个投研系统不只是算法和数据。它还需要一台安全的机器、一个稳定的运行时、一个不会在半夜被扫描器打穿的端口。

潮来找我

回到开头的 8 秒。

明天周一开盘,15:30 收盘后,cron 会第一次正式运行。扫描持仓,扫描自选,回填信号。等我晚上打开 Dashboard,数据已经在那里等我了。

8 秒变 0 秒。不是因为更快了,是因为视角变了。

以前是我去看潮。现在是潮来找我。


观潮系列:诞生记 → 进化记 → 实战记 → 验证记 → 出海记 → 锐评记 → 回填解读 → 自省记 → 托管记

Comments