tradingSystem/CLAUDE.md

13 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

本文件用可读中文写成,正文遵循 .claude/output-styles/readable-chinese.md 的规矩:完整句子、一句一事、少用符号。命令和表格内部不受此限。

这是什么

这是一套命令驱动的持仓管理系统,代码里和文档里都简称 PMSPosition Management System持仓管理系统。它在整条交易链路里只负责一段用户在管理页面下达大方向命令例如设定总规模、调仓位上限、升降仓、对某只股票做 TPMS 据此把命令拆成分股分批的方案,并管理账本(批次、摊薄成本、安全垫、纪律)。

链路上还有另外两个系统,它们不在本仓库里,但 PMS 时时与它们打交道:

  1. 决策系统,代码里叫 bionic。它负责研判该不该做与盘中择时何时做。PMS 不自建研判栈,研判和择时都可以委托它。
  2. 下游系统,即 QMT 侧的执行服务。它只负责挂单成交。PMS 经一条 WebSocket 长连接直连它。

一句话概括分工PMS 管「做什么、多少」,决策系统管「该不该、何时」,下游只管执行。

三条铁律(改任何东西前先读)

这三条写在 README 开头,是整个系统的设计前提,违反它们的改动一律是错的。

  1. 命令至上。自动决策不得突破用户的命令参数;冲突时命令优先;命令之间的冲突交用户裁决。
  2. 分工不越权。研判不可用时降级为保守规则加人工确认,绝不自建第二套研判。
  3. 先记账后动作,故障即守成。指令先落表再下发;出了故障不产生新指令;账本与下游定期对账,以下游为实际持仓的事实源。

常用命令

日常操作都由 Makefile 包了一层,make help 看全部目标。下面这些是最常用的。所有 make 命令都在部署 PMS 的服务器上跑(源码打进容器镜像,本机直接跑 Python 会因为缺少容器环境而对不上)。

make deploy        # 一句话部署git pull → 重建镜像 → 重建容器force-recreate→ 建表
make deploy-local  # 同上但跳过 git pull本地已经改好时用
make test          # 跑全部单测(不连库,秒级)。应输出 ALL SUITES PASS
make watch         # 盯盘看这一个:通道/账户/在途指令/出口委托/账本持仓 一屏看完,只读
make health        # 页面健康检查(配置装载自证 + 库连通自证)
make check         # 实机连通性与表结构自检(需要真实 .env

跑单元测试有两种粒度。跑全部用 make test它会先比对容器镜像与工作树的代码指纹对不上就报警防「git pull 了没重建,跑的还是旧代码而且照样通过」)。只跑某一批时,直接把那个测试文件当脚本跑:

# 只跑某一批择时实现B 与规则闸那一批)
docker compose run --rm --no-deps pms-web python scripts/test_batch3_units.py

每个 scripts/test_batch*.py 都是能独立运行的脚本,run_tests.py 只是把它们逐个 subprocess 起来。测试全部零外部依赖、不连库,服务层用内存桩(见 scripts/test_wiring.pyFakeRepo)。

改代码时最容易踩的坑

改了 Python 代码必须重建镜像加 force-recreate绝不能用 docker compose restart 源码是打进镜像的,restart 只是把老容器停了再起,跑的还是旧镜像里的旧代码,而且一点报错都没有。make deploymake up 走的都是正确路径。这是本项目头号坑,Makefile 顶部、scripts/deploy.shrun_tests.py 里都反复写了这一条。

模拟仓 155 是个例外要另记2026-09-20 实测)。 155 的项目目录里有一个服务器本地文件 docker-compose.override.yml,内容同仓库里的 docker-compose.dev.yml,它把整个仓库目录绑定挂载到四个容器的 /app。这个文件被 .gitignore 忽略,仓库里看不到;真实仓 188 没有它。后果有三条。一,在 155 上 git pull 之后,页面静态文件立刻就是新的,后端四个进程仍是旧代码,直到重启或 make deploy。二,make testmake stale 起的一次性容器读的也是工作树,所以在 155 上「指纹一致」恒成立,查不出「拉了代码没重启」。三,往 155 拉代码等于把页面先上线了,拉之前先看 git log 里有没有别的会话提交的、还没到上线日的改动。make deploy 在两台机器上都仍是对的做法。

下发模式由参数 PMS_DISPATCH_MODE 决定,实际以页面顶部横幅为准,不要拿这段文字当现状。 设置文件的初值是 shadow(影子模式:指令照常生成、过规则闸、记账、置为已下发,但不写下游,需人工在 QMT 侧执行,成交由回放任务按先进先出认领回账本)。但模拟仓 155 运行时已切到 ws,指令经 pms-ws 真向模拟 QMT 发出、自主动作按档位可自动执行,不再需要人工下单。真实仓 188 的 PMS 尚未追平到最新代码,上线等 QMT 侧,追平部署时按真实仓准备包的初始参数清单先进影子期。切换只是 PMS_DISPATCH_MODEPMS_QMT_WS_ENABLED 两个开关加一次联调,不是再写代码。改动涉及下单链路时,务必先确认自己面对的是哪一套实例、此刻是哪个模式。

业务参数一律走 ParamStore 读,不要直接读 settings。 config/settings.py 里的业务参数(PMS_TOTAL_SCALE、各种档位阈值等)只是「初值」。运行时以数据库表 pms_runtime_param 的值为准,页面改参数即时生效。取业务参数统一经 app/services/param_store.py,直接读 settings 会让页面调参失效。基础设施类配置(连接串、密钥)才从 settings 与 .env 读。

密钥只从 .env 注入,不入库、不进 ParamStore、不上页面。 会话密钥、QMT 通道的两把 Ed25519 密钥都属此类。新增密钥类配置时,要同时登记进 param_store.pySECRET_KEYS,否则它会被当成可调参数暴露到页面快照里。

架构分层

代码在 app/ 下按依赖方向分层,越靠上越纯。

  • app/core/:纯逻辑,零外部依赖,可单测。系统的算数与纪律都在这里——批次拆分与组合约束(sizer.py)、摊薄成本与安全垫状态机(cushion.py)、命令目录与校验(command_spec.py)、方案生成(planner.py)、规则闸终检(rule_gate.py、择时实现Bexec_timing.py)、动作引擎(action_engine.py、QMT 协议编解码与签名(ws_codec.py)等。改核心逻辑先在这一层,它有最密的单测覆盖。
  • app/db/session.py:三个数据库的连接,外加一道严格单表访问守卫(下面单列一节)。
  • app/repo/:单表数据访问层。每个函数只碰一张表。
  • app/services/:编排层,把 core 的纯逻辑接上 repo 的取数与落表。命令下达在 command_service.py,方案转指令与出手在 executor.py,下发通道适配器在 dispatcher.py,运行参数中心在 param_store.py,组合快照在 portfolio.py
  • app/ws/runner.py:常驻的 QMT 连接进程,容器名 pms-ws。它只做通道,不碰账本。
  • app/web/FastAPI 后端加一个单页前端(static/index.htmlVue3 加 ElementPlus无构建步骤
  • app/scheduler.pyCelery beat 调度总表,九个调度位加三条守卫。

进程边界值得单独理解一下。出手的 executor 跑在短命的 Celery worker 任务里,而 QMT 连接必须是一条常驻长连接(协议规定 PMS 只准开一条多开会让指令乱序。两者只经数据库耦合worker 把待发委托写进出口表 pms_qmt_orderpms-ws 进程亚秒轮询取走并签名发出QMT 的回报落进 pms_qmt_inbox,再由回放任务认领进账本。因此 pms-ws 这个服务绝不能扩副本。

三组容器服务:默认起 pms-web(管理页面,端口 38100--profile schedpms-beatpms-worker(调度与执行);--profile wspms-wsQMT 直连)。

严格单表访问守卫

153 代理库不支持多表联查,写错了要到线上才报错。为把这条纪律钉死,app/db/session.py 在每条 SQL 的执行入口做静态检查,出现 JOIN、逗号连表、跨表子查询一律拒绝抛异常。写数据访问代码时一条 SQL 只能碰一张表;要联的数据在 service 层分两次查再在内存里拼。upsert 语句里的 ON DUPLICATE KEY UPDATE 是这道守卫的历史误伤点,改 upsert 时留意。

数据库与外部依赖

三个 MySQL 库(键名与 bionic 对齐,同一份 .env 可服务两个系统):PROXY_DB_URL 是 153 代理,放 PMS 全部自有表加决策系统结论表;SOURCE_DB_EXT_DSN 是因子库,当前 PMS 不用;DB_MYSQL_URL 是大盘指数库,只做页面区制提示。建表脚本是 scripts/init_db.py,建表语句在 ddl_pms_v1.sql

Redis 有几路决策系统的盘中信号流与风控卖出流、实时行情、PMS 自己的 Celery 总线。redis 依赖必须锁在 5.x6.x 默认按 RESP3 握手,而现网的 Redis 版本偏旧会回 unknown command HELLO,导致行情与调度总线一起断。requirements.txt 已锁版本,升级依赖时别放开。

时区口径(很容易读错)

不能按「哪个系统」一刀切,要按「哪种时间」分。容器时钟与容器日志正文是北京时间,不要加八小时。数据库里的时间戳列(如各种 created_atupdated_at)是 UTC读的时候要加八小时。日期列trade_dateaudit_date)是北京日期,不换算。写记录时一律用北京时间,引用数据库时间列先加八小时并标注已换算。完整依据见 DEVLOG.md 开头。

前端的两个坑

管理页面是无构建步骤的单页,这两个坑都实际炸过页面,都写在 README 里。

  1. 模板里不能用自闭合的自定义标签。<el-alert .../> 这种写法浏览器 HTML 解析器不认,会把后面所有内容当成它的子节点;若它带 v-if 且条件为假,整页就跟着消失。一律写成 <el-xxx ...></el-xxx>
  2. setup() 里定义给模板用的东西必须真的 return 出去。漏一个函数没接出去,用到它的地方会在渲染时报错并白屏。scripts/test_page_wiring_guard.py 就是为这个坑加的护栏2026-09-10 白屏即漏了十一个显示函数所致)。

index.html 没有别的自动化测试,页面的两道静态护栏是 test_page_enum_guard.pytest_page_wiring_guard.py,改页面后要一起跑。

加测试时的登记纪律

run_tests.py 把「缺一个测试文件」当失败处理,而不是当「跳过」——因为镜像旧了的时候,新加的那批测试正好全部「不存在」,一条没跑却会报全部通过。所以新增一个 test_batch*.py,必须同时登记进 run_tests.py 顶部的 SUITES 清单和它上面的例数说明表,否则会被判失败。

代码里还有若干「哨兵断言」,改了对应的东西就得回到那里改断言,断言不动就是漏了。清单写在 run_tests.py 顶部的注释里DDL 表数、调度位、API 路由、密钥名单、动作求值器次序等)。改这些结构性的东西时先去那份清单对一遍。

部署与运维纪律

在开发机修改代码通过git提交然后在服务器上部署

部署方式与服务器信息以 DEVLOG.md 开头那段为准PMS 跑在哪台机、决策系统与因子桥各在哪、各自怎么生效)。有模拟仓与真实仓两套实例,靠表名前缀(PMS_TABLE_PREFIX)与容器名前缀区分,共用同一套代码。对真实仓做写操作前先确认清楚是哪一套。

服务器上的容器操作偏敏感:不要随意重启或关停容器,涉及容器与数据库的操作先与用户确认,不碰本仓库以外的项目。登录到哪台机、面对的是哪一套实例,不清楚时先问。

文档地图

仓库根目录有大量设计与交接文档,需要细节时按主题查这几份:

  • README.md:总览,最全。上面各节多是从它摘的。
  • DEVLOG.md:开发节点流水账,每条含「做了什么、动了哪些文件、部署方式、真机判收、还欠什么」五样。查某个改动的来龙去脉看这里。
  • POSITION_MGMT_DESIGN.md:总体设计定稿。
  • QMT_WS_PROTOCOL.mdPMS 与 QMT 的 WebSocket 协议,下发通道的唯一实现依据。
  • BIONIC_PMS_INTERFACE.mdPMS 与决策系统的接口契约(研判闸与盘中择时两个接口)。
  • UPSTREAM_PLAN_API.md:上游选股计划接口的接入记录,候选池的事实源。
  • docs/复盘决定台账.md:近期改动按「台账」编号推进,提交信息里的台账号对应到这里。

提交约定

提交信息用简短中文,描述这次改动的行为而非罗列文件。近期工作按台账编号组织,提交里出现的台账号可回到 docs/复盘决定台账.md 对照。