
1. 从 PanWatch 这个名字说起一个盯盘 Agent 的完整落地思路第一次看到 PanWatch 这个项目名我脑子里蹦出来的画面很直接一个能自己盯着盘面、按预设规则判断、该提醒时提醒、该记录时记录的自动化助手。Pan 是盘面Watch 是盯梢合起来就是“帮你盯盘”。但真动手做起来你会发现它远不止一个定时脚本那么简单——它本质上是一个TradingAgents 思路下的轻量级 Agent 系统涉及数据采集、状态管理、决策触发、消息推送、容器化部署、PWA 前端这一整条链路。我前后用 Docker 部署过好几版类似的盯盘工具踩过的坑从“容器时区不对导致定时任务全乱”到“PWA 缓存了旧版本死活刷不出来”都有。这篇就把 PanWatch 这类项目的完整实现思路拆开讲从架构选型到 Docker 部署从 Agent 决策逻辑到 PWA 前端尽量把每个“为什么这么选”讲透。适合两类人看一类是想自己搭一套盯盘助手的个人开发者另一类是想借这个项目理解 Agent 系统怎么从概念落到可运行代码的初学者。哪怕你之前只写过脚本、没碰过 Agent 框架跟着思路走也能搭出个能跑的版本。核心关键词先摆出来PanWatch、TradingAgents、Agent、Docker、PWA。这五个词基本覆盖了项目的全部技术面——业务逻辑是 TradingAgents 式的多角色协作运行形态是 Agent部署方式是 Docker前端交付是 PWA。下面逐层拆。2. 整体架构设计为什么是 Agent 而不是一堆定时脚本2.1 从“定时脚本”到“Agent”的思维转变很多人做盯盘工具的第一反应是写个 cron 脚本每 5 分钟拉一次行情超过阈值就发通知。这个方案能跑但很快就会遇到瓶颈你想加一个“连续三根阴线且成交量放大才提醒”的规则脚本里就得堆一堆 if-else你想让不同标的用不同策略代码就开始失控你想让系统自己判断“现在是不是该提醒”而不是死板地比大小脚本就彻底不够用了。Agent 的思路不一样。它把“盯盘”拆成几个有明确职责的角色数据采集 Agent负责拿行情分析 Agent负责判断信号决策 Agent负责决定要不要动作执行 Agent负责推送和记录。每个角色只干一件事通过一个共享的状态层通信。这就是 TradingAgents 那套多角色协作的简化版——原版 TradingAgents 是学术项目角色更多、流程更重PanWatch 这种个人项目没必要照搬取其“角色分离、状态共享”的内核就够了。提示不要一上来就追求多 Agent 协作。如果你的规则不超过 5 条单 Agent 加规则引擎完全够用。多 Agent 的价值在于规则复杂、需要多维度交叉判断的场景过早引入只会增加调试成本。2.2 技术选型背后的取舍逻辑选型这块我踩过不少坑直接说结论和理由。后端语言选 Python。原因很实际行情数据处理的生态pandas、numpy成熟Agent 框架LangChain、AutoGen 这类也是 Python 优先而且写策略逻辑快。如果你团队是 Java 背景用 Spring 也能做但 Agent 相关的现成轮子少很多很多逻辑得自己造。部署用 Docker。这个几乎是必选项。盯盘工具通常要 7x24 运行直接跑在宿主机上环境依赖、Python 版本、时区这些问题会把你折磨疯。Docker 把运行环境打包换台机器docker compose up就能起来。而且 Docker 的网络隔离特性让你可以很方便地把数据库、缓存、应用分成不同容器各自独立升级。前端用 PWA。盯盘工具的使用场景是“随时随地看一眼”手机浏览器打开就能用不用装 App这是 PWA 最大的优势。加上 Service Worker 做离线缓存网络不好时也能看到上次的数据。相比原生 AppPWA 的开发成本低一个数量级一个人就能维护。数据存储用 SQLite Redis 组合。SQLite 存历史行情、交易记录、配置这些需要持久化的数据单文件、零配置、备份就是复制文件。Redis 存实时状态、Agent 之间的消息队列、去重标记这些高频读写的数据。这个组合对个人项目来说性价比极高等数据量真上来了再换 PostgreSQL 也不迟。组件选型核心理由替代方案后端语言Python 3.11生态成熟、Agent 框架支持好Java / Node.js部署方式Docker Compose环境隔离、一键启动裸机 systemd前端形态PWA免安装、跨平台、离线可用原生 App / 纯网页持久化SQLite零配置、单文件备份PostgreSQL实时状态Redis高频读写、支持发布订阅内存字典不持久2.3 数据流是怎么跑通的整个系统的数据流可以这样理解数据采集 Agent 按固定频率比如每 10 秒从行情源拉数据写入 Redis 的实时状态分析 Agent 订阅这个状态每次更新就触发一次判断判断结果如果是“需要动作”就丢一条消息到决策队列决策 Agent 消费队列做最终确认比如去重、限流然后调用执行 Agent执行 Agent 负责发通知邮件、Webhook、Telegram Bot 等并写 SQLite 留痕。这个链路的关键在于解耦。采集和分析之间通过 Redis 状态解耦分析和决策之间通过消息队列解耦。好处是任何一环挂了其他环节还能继续跑不会整个系统雪崩。比如推送服务挂了采集和分析照常运行等推送恢复后补发就行。3. 核心细节拆解Agent 决策逻辑与状态管理3.1 分析 Agent 的判断逻辑怎么写才不失控分析 Agent 是整个系统的大脑也是最容易写乱的地方。我的经验是把判断逻辑和阈值配置彻底分离。代码里只写“怎么判断”配置文件里写“判断什么”。举个具体例子。假设你要判断“放量突破”代码逻辑是这样的def check_volume_breakout(current, history, config): avg_volume sum(h[volume] for h in history[-config[lookback]:]) / config[lookback] volume_ratio current[volume] / avg_volume price_breakout current[price] max(h[high] for h in history[-config[lookback]:]) return volume_ratio config[volume_threshold] and price_breakout而配置文件里是这样的strategies: volume_breakout: enabled: true lookback: 20 volume_threshold: 2.5 notify_channels: [email, webhook]这样改策略不用动代码改配置重启就行。更重要的是你可以给不同标的配不同参数——大盘股用 2.0 倍量小盘股用 3.0 倍量互不干扰。注意阈值配置一定要有默认值和边界校验。我见过有人把 volume_threshold 配成 0.1结果系统每 10 秒推一次通知直接把邮箱刷爆。配置加载时加一层校验比如阈值必须在 1.0 到 10.0 之间超出就拒绝启动并报错。3.2 状态管理Agent 的“记忆”放在哪Agent 要有记忆才能做连续判断。比如“连续三根阴线”这种规则Agent 必须记住前两根的状态。这个记忆放哪我的做法是分两层短期记忆放 Redis。用 Redis 的 List 结构存最近 N 个周期的状态设置过期时间比如 1 小时。这样 Agent 每次判断时读取这个 List判断完把当前状态 push 进去。Redis 的读写性能足够支撑秒级频率而且过期机制自动清理旧数据不用手动维护。长期记忆放 SQLite。所有触发过的信号、执行过的动作、推送记录都写 SQLite。这张表既是审计日志也是后续复盘的数据源。表结构大概是这样CREATE TABLE signals ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol TEXT NOT NULL, strategy TEXT NOT NULL, trigger_time DATETIME NOT NULL, price REAL, volume REAL, detail TEXT, notified INTEGER DEFAULT 0 ); CREATE INDEX idx_symbol_time ON signals(symbol, trigger_time);索引很关键。没有索引的话数据量上到几万条后查询“某标的最近 7 天的信号”会慢到无法接受。idx_symbol_time这个复合索引能覆盖大部分查询场景。3.3 去重与限流别让通知变成骚扰这是盯盘工具最容易被忽视、但用户体验最致命的一环。同一个信号在短时间内反复触发如果不做去重你的手机就会被通知淹没。我的做法是三层防护第一层信号指纹去重。把 symbol strategy 触发条件的关键参数拼成一个字符串算 MD5存 Redis 并设 30 分钟过期。同一个指纹在 30 分钟内只处理一次。第二层冷却时间。每个策略配一个 cooldown 参数比如 300 秒。同一个策略对同一个标的两次通知之间至少间隔 300 秒。第三层每日上限。每个推送渠道设一个每日最大推送条数比如 50 条。超过就只记录不推送第二天重置。def should_notify(signal, redis_client, config): fingerprint f{signal[symbol]}:{signal[strategy]}:{signal[key_params]} fp_key fnotify:fp:{hashlib.md5(fingerprint.encode()).hexdigest()} if redis_client.exists(fp_key): return False cooldown_key fnotify:cd:{signal[symbol]}:{signal[strategy]} if redis_client.exists(cooldown_key): return False daily_key fnotify:daily:{signal[channel]}:{date.today()} if int(redis_client.get(daily_key) or 0) config[daily_limit]: return False return True这三层下来通知的精准度会高很多。实测下来从每天几十条噪音通知降到三五条真正有价值的提醒。4. Docker 部署实操从零到跑起来4.1 环境准备与 Docker 安装要点Docker 部署的第一步是装 Docker。Windows 用户装 Docker DesktopLinux 用户装 Docker Engine。这里有几个高频坑点必须提前说。Windows 上装 Docker Desktop最常见的报错是Virtualization support not detected。这个报错的意思是 BIOS 里的虚拟化开关没打开。重启进 BIOS找到 Intel VT-x 或 AMD-V 选项设为 Enabled。另一个常见报错是Docker Desktop failed to start because virtualization support not detected同样是虚拟化没开或者 Hyper-V 和 WSL2 冲突。Windows 10/11 建议用 WSL2 后端性能比 Hyper-V 好兼容性也好。Linux 上装 Docker用官方脚本最省事curl -fsSL https://get.docker.com | sh sudo systemctl enable docker sudo systemctl start docker sudo usermod -aG docker $USER最后一行把自己加进 docker 组这样不用每次敲 sudo。加完要重新登录才生效很多人忘了这步然后一直纠结为什么还要 sudo。提示国内环境拉镜像慢的话配置镜像加速器。在/etc/docker/daemon.json里加 registry-mirrors然后sudo systemctl restart docker。具体加速地址自己找可用的这里不展开。4.2 docker-compose 编排文件怎么写PanWatch 这类项目涉及多个容器应用本体、Redis、可能还有定时任务容器。用 docker-compose 编排最清晰。下面是一个可直接参考的模板version: 3.8 services: redis: image: redis:7-alpine container_name: panwatch-redis restart: unless-stopped volumes: - ./data/redis:/data command: redis-server --appendonly yes networks: - panwatch-net app: build: . container_name: panwatch-app restart: unless-stopped depends_on: - redis environment: - TZAsia/Shanghai - REDIS_HOSTredis - REDIS_PORT6379 - DB_PATH/app/data/panwatch.db volumes: - ./data/app:/app/data - ./config:/app/config ports: - 8080:8080 networks: - panwatch-net networks: panwatch-net: driver: bridge几个关键点解释一下。restart: unless-stopped保证容器挂了自动重启盯盘工具必须 7x24 运行这个不能省。TZAsia/Shanghai设置时区不设的话容器默认 UTC你的定时任务会全部偏移 8 小时这个坑我踩过不止一次。depends_on只保证启动顺序不保证 Redis 就绪应用里要做重试连接。数据卷挂载到宿主机容器删了数据还在升级镜像时不会丢数据。4.3 启动、验证与常见启动失败排查编排文件写好后启动就一条命令docker compose up -d-d是后台运行。启动后先看容器状态docker compose ps如果某个容器状态是Restarting或Exited看日志docker compose logs -f app启动失败最常见的几类原因我整理成速查表报错现象可能原因排查方法容器反复重启应用启动报错docker compose logs app看堆栈连不上 Redis网络不通或 host 写错进 app 容器ping redis端口被占用宿主机 8080 已被用netstat -tlnp | grep 8080时区不对没设 TZ 环境变量docker exec app date看时间数据丢失没挂载 volumedocker inspect看 Mountsfailed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen这个报错通常是 Docker Desktop 的 WSL 集成没开或者 Docker Desktop 没启动。检查 Docker Desktop 设置里的 Resources - WSL Integration把对应发行版打开。4.4 数据库容器的补充说明如果你的 PanWatch 需要更重的数据库比如要存大量历史 K 线可以考虑上 MySQL 或 PostgreSQL。用 Docker 装 MySQL 8.0 的典型配置mysql: image: mysql:8.0 container_name: panwatch-mysql restart: unless-stopped environment: - MYSQL_ROOT_PASSWORDyour_strong_password - MYSQL_DATABASEpanwatch - TZAsia/Shanghai volumes: - ./data/mysql:/var/lib/mysql command: --default-authentication-pluginmysql_native_password --character-set-serverutf8mb4 networks: - panwatch-net--character-set-serverutf8mb4这个参数很重要不设的话中文和特殊字符会乱码。mysql_native_password是为了兼容一些老客户端新版 MySQL 默认用 caching_sha2_password部分驱动连不上。5. PWA 前端让盯盘工具随手可用5.1 PWA 的三个核心文件PWA 能“像 App 一样”运行靠的是三个东西manifest.json、Service Worker、HTTPS或 localhost。manifest 定义应用名称、图标、启动方式Service Worker 拦截网络请求做缓存HTTPS 是浏览器允许 Service Worker 的前提本地开发用 localhost 例外。manifest.json 最小可用版本{ name: PanWatch, short_name: PanWatch, start_url: /, display: standalone, background_color: #1a1a1a, theme_color: #1a1a1a, icons: [ { src: /icon-192.png, sizes: 192x192, type: image/png }, { src: /icon-512.png, sizes: 512x512, type: image/png } ] }display: standalone是关键它让应用打开时没有浏览器地址栏看起来就是个原生 App。图标至少要准备 192 和 512 两个尺寸少了某些浏览器不认。5.2 Service Worker 缓存策略怎么选Service Worker 的缓存策略直接决定用户体验。盯盘工具的数据特点是“实时性要求高但离线时也要能看”所以不能一刀切。我的策略是分类处理静态资源HTML、CSS、JS、图标Cache First优先读缓存后台更新。这些文件不常变缓存优先能大幅提升加载速度。API 数据行情、信号列表Network First优先请求网络失败时降级读缓存。保证在线时数据最新离线时至少能看到上次的数据。推送相关不缓存直接走网络。self.addEventListener(fetch, (event) { const url new URL(event.request.url); if (url.pathname.startsWith(/api/)) { event.respondWith( fetch(event.request) .then((response) { const clone response.clone(); caches.open(api-cache).then((cache) cache.put(event.request, clone)); return response; }) .catch(() caches.match(event.request)) ); } else { event.respondWith( caches.match(event.request).then((cached) cached || fetch(event.request)) ); } });注意Service Worker 更新后不会立即生效要等所有标签页关闭再打开。开发时经常遇到“改了代码没生效”八成是 Service Worker 缓存了旧版本。调试时在 DevTools 的 Application 面板里勾选 “Update on reload”或者手动 Unregister 再刷新。5.3 移动端适配的几个实操细节盯盘工具大概率在手机上看移动端适配不能马虎。几个我踩过坑的点viewport 必须设对。meta nameviewport contentwidthdevice-width, initial-scale1, viewport-fitcoverviewport-fitcover是为了适配 iPhone 的刘海屏不加的话内容会被刘海挡住。点击区域至少 44x44 像素。这是苹果的人机交互指南建议的最小值小于这个尺寸手指点不准。按钮、列表项都要注意。数字用等宽字体。行情数字跳动时如果字体不等宽数字宽度变化会导致整行抖动看起来很难受。CSS 里加font-variant-numeric: tabular-nums就能解决。深色模式优先。盯盘的人经常晚上看浅色背景刺眼。用prefers-color-scheme媒体查询自动切换或者直接默认深色。6. 常见问题与排查技巧实录6.1 Agent 执行报错怎么定位agent execution terminated due to error这类报错信息太笼统光看这一句根本不知道哪出问题。我的排查套路是三步第一步看完整堆栈。日志里往前翻找到第一个Traceback那才是根因。后面的报错往往是连锁反应。第二步确认是数据问题还是逻辑问题。如果是KeyError、TypeError大概率是数据格式变了比如行情源返回的字段名改了。如果是TimeoutError是网络或数据源的问题。第三步复现。把出错时的输入数据 dump 出来写个最小复现脚本单独跑。这一步能省掉大量猜测时间。6.2 Docker 网络不通的排查顺序容器之间连不上是高频问题。排查顺序建议这样先确认在同一个 network 里docker network inspect panwatch-net看两个容器是不是都在。进容器 ping 对方docker exec -it panwatch-app ping redis。ping 不通就是网络层问题。ping 通但连不上端口docker exec -it panwatch-app nc -zv redis 6379。端口不通可能是服务没起来。服务起来了还连不上检查应用配置里的 host 是不是写的容器名。容器间通信用服务名不是 localhost。提示容器里的 localhost 指的是容器自己不是宿主机。这个坑新手必踩。要连宿主机的服务用host.docker.internalDocker Desktop或宿主机的实际 IP。6.3 定时任务不执行的排查清单盯盘工具的定时任务不执行原因通常在这几个地方时区问题容器 UTC你按北京时间配的 cron实际执行时间偏移 8 小时。docker exec app date确认。进程模型问题如果用 cron 但容器里没跑 cron daemon任务不会触发。建议用应用内的调度器如 APScheduler别依赖系统 cron。异常吞掉任务函数里 try-except 把异常吞了任务静默失败。日志里加足够的 info 级别输出。单例问题多副本部署时每个副本都跑定时任务导致重复执行。要么只跑单副本要么用 Redis 分布式锁。6.4 性能问题的几个信号系统跑一段时间后变慢通常是这几个原因症状可能原因解决方向内存持续增长状态数据没清理给 Redis key 设 TTL查询变慢缺索引分析慢查询加复合索引CPU 飙高判断逻辑太频繁降低采集频率或加节流磁盘写满日志没轮转配 logrotate 或限制日志大小SQLite 的写入性能在数据量大时会明显下降。如果 signals 表超过百万行考虑按时间分表或者迁移到 PostgreSQL。迁移成本不高SQL 基本兼容。7. 从 PanWatch 延伸Agent 项目的通用经验做这类 Agent 项目有几个经验是通用的跟具体业务无关。先跑通最小闭环再加功能。我见过太多人一上来就设计复杂的多 Agent 架构结果卡在第一个 Agent 的调试上。正确做法是先让“采集-判断-推送”这条最短链路跑通哪怕判断逻辑只有一个 if推送只发邮件。闭环跑通了再往里加策略、加渠道、加前端。日志要足够详细但要能关。开发阶段日志越详细越好每个 Agent 的输入输出都打出来。上线后要能通过配置调低日志级别不然日志文件几天就撑爆磁盘。用结构化日志JSON 格式方便后续用工具分析。配置和代码分离但配置要有 schema。配置文件方便改但没校验的配置是灾难。用 pydantic 这类库给配置定义 schema加载时自动校验配错了直接报错别等到运行时才炸。监控 Agent 本身。Agent 系统最怕的是“静默失败”——它不报错但也不干活。加一个心跳机制Agent 每隔一段时间往 Redis 写个时间戳另起一个监控任务检查这个时间戳超过阈值没更新就告警。这个监控任务本身要足够简单简单到不会出错。版本升级要能回滚。Docker 镜像打 tag别用 latest。升级前备份 SQLite 文件。出问题docker compose down然后指定旧 tag 重新 up几分钟就能回滚。这个习惯能救命。最后分享一个我在实际部署中总结的小技巧把 PanWatch 的配置目录单独挂载出来用 Git 管理。每次改配置都 commit出问题能 diff 出改了什么也能一键回滚到上个版本。配置文件版本化这件事做起来成本极低收益极高。