tradingSystem/AGENTS.md

149 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md
本文件是 Codex 在本仓库工作的项目指令,也是项目开发规则的唯一维护入口。`CLAUDE.md` 保留兼容入口,指向本文件。
用可读中文沟通和写文档。写完整句子,一句一事,少用符号,不自造缩写。术语先用白话解释。命令和表格内部不受此限。这些规则直接保存在本文件,不依赖开发机上缺失的输出风格文件。
## 接续开发从哪里开始
当前接续记录是 `docs/开发接续_2026-10-12.md`。接手中断的工作时先读它,再按任务查设计文档。它记录了两个 Claude 修复的整合状态、测试隔离方式、尚未执行的探针和部署判收。
开工先看 `git status --short`、`git worktree list` 和相关提交。已有的未提交文件与其他工作树属于用户正在进行的工作,不覆盖、不清理。判断是否推送要直接核对远程,不凭过期的 `origin/main` 引用下结论。
创建新分支时默认用 `codex/` 前缀。旧的 `claude/` 分支仍是有效的 Git 分支,保留名字和工作树即可接续。合并时核对测试文件是否重名,不能让一批测试覆盖另一批。
历史交接里的服务器状态只代表记录当天。过期的执行日期不会自动变成今天的执行授权。2026-10-12 用户确认先以当前仓库为准,原定 10-09 的两项探针均未执行。
## 这是什么
这是一套命令驱动的持仓管理系统,代码里和文档里都简称 PMS(Position Management System,持仓管理系统)。它在整条交易链路里只负责一段:用户在管理页面下达大方向命令,例如设定总规模、调仓位上限、升降仓、对某只股票做 T;PMS 据此把命令拆成分股分批的方案,并管理账本(批次、摊薄成本、安全垫、纪律)。
链路上还有另外两个系统,它们不在本仓库里,但 PMS 时时与它们打交道:
1. 决策系统,代码里叫 bionic。它负责研判(该不该做)与盘中择时(何时做)。PMS 不自建研判栈,研判和择时都可以委托它。
2. 下游系统,即 QMT 侧的执行服务。它只负责挂单成交。PMS 经一条 WebSocket 长连接直连它。
一句话概括分工:PMS 管「做什么、多少」,决策系统管「该不该、何时」,下游只管执行。
## 三条铁律(改任何东西前先读)
这三条写在 README 开头,是整个系统的设计前提,违反它们的改动一律是错的。
1. 命令至上。自动决策不得突破用户的命令参数;冲突时命令优先;命令之间的冲突交用户裁决。
2. 分工不越权。研判不可用时降级为保守规则加人工确认,绝不自建第二套研判。
3. 先记账后动作,故障即守成。指令先落表再下发;出了故障不产生新指令;账本与下游定期对账,以下游为实际持仓的事实源。
## 常用命令
日常操作都由 `Makefile` 包了一层,`make help` 看全部目标。下面这些是最常用的。所有 `make` 命令都在部署 PMS 的服务器上跑(源码打进容器镜像,本机直接跑 Python 会因为缺少容器环境而对不上)。
```bash
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 了没重建,跑的还是旧代码而且照样通过」)。只跑某一批时,直接把那个测试文件当脚本跑:
```bash
# 只跑某一批(例:择时实现B 与规则闸那一批)
docker compose run --rm --no-deps pms-web python scripts/unit_test_guard.py scripts/test_batch3_units.py
```
每个 `scripts/test_batch*.py` 都是能独立运行的脚本,`scripts/run_tests.py` 只是把它们逐个 subprocess 起来。测试设计为不连接外部服务,服务层用内存桩(见 `scripts/test_wiring.py` 的 `FakeRepo`)。
**单测必须隔离配置并禁止联网。** `scripts/run_tests.py` 逐批经 `scripts/unit_test_guard.py` 启动,清除业务环境变量,在没有 `.env` 的临时目录运行。各测试文件直接运行时也先安装联网护栏。Python 套接字调用会在连接前被阻断,即使业务代码吞掉异常,整批仍以失败退出。`install_fakes` 已补齐观察读数仓储与扫描中遗漏的外部依赖。新增服务调用必须补内存桩,不能依赖异常降级让测试通过。
开发机可以在单独虚拟环境安装 `requirements.txt` 后经隔离入口跑全量回归。服务器部署前仍要用匹配镜像的断网容器验证,不能把本地通过称作服务器验收。2026-09-20 留下的四行历史假数据尚未清理,修测试不会自动删除库里的旧记录。
## 改代码时最容易踩的坑
**改了 Python 代码必须重建镜像加 force-recreate,绝不能用 `docker compose restart`。** 源码是打进镜像的,`restart` 只是把老容器停了再起,跑的还是旧镜像里的旧代码,而且一点报错都没有。`make deploy` 和 `make up` 走的都是正确路径。这是本项目头号坑,`Makefile` 顶部、`scripts/deploy.sh`、`run_tests.py` 里都反复写了这一条。
**模拟仓 155 是个例外,要另记(2026-09-20 实测)。** 155 的项目目录里有一个服务器本地文件 `docker-compose.override.yml`,内容同仓库里的 `docker-compose.dev.yml`,它把整个仓库目录绑定挂载到四个容器的 `/app`。这个文件被 `.gitignore` 忽略,仓库里看不到;真实仓 188 没有它。后果有三条。一,在 155 上 `git pull` 之后,页面静态文件立刻就是新的,后端四个进程仍是旧代码,直到重启或 `make deploy`。二,`make test` 与 `make stale` 起的一次性容器读的也是工作树,所以在 155 上「指纹一致」恒成立,查不出「拉了代码没重启」。三,往 155 拉代码等于把页面先上线了,拉之前先看 `git log` 里有没有别的会话提交的、还没到上线日的改动。`make deploy` 在两台机器上都仍是对的做法。
**下发模式由参数 `PMS_DISPATCH_MODE` 决定,实际以页面顶部横幅为准,不要拿这段文字当现状。** 设置文件的初值是 `shadow`(影子模式:指令照常生成、过规则闸、记账、置为已下发,但不写下游,需人工在 QMT 侧执行,成交由回放任务按先进先出认领回账本)。但模拟仓 155 运行时已切到 `ws`,指令经 `pms-ws` 真向模拟 QMT 发出、自主动作按档位可自动执行,不再需要人工下单。真实仓 188 的 PMS 尚未追平到最新代码,上线等 QMT 侧,追平部署时按真实仓准备包的初始参数清单先进影子期。切换只是 `PMS_DISPATCH_MODE` 与 `PMS_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.py` 的 `SECRET_KEYS`,否则它会被当成可调参数暴露到页面快照里。
## 架构分层
代码在 `app/` 下按依赖方向分层,越靠上越纯。
- `app/core/`:纯逻辑,零外部依赖,可单测。系统的算数与纪律都在这里——批次拆分与组合约束(`sizer.py`)、摊薄成本与安全垫状态机(`cushion.py`)、命令目录与校验(`command_spec.py`)、方案生成(`planner.py`)、规则闸终检(`rule_gate.py`)、择时实现B(`exec_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.html`,Vue3 加 ElementPlus,无构建步骤)。
- `app/scheduler.py`:Celery beat 调度总表与调度守卫。调度位以代码和 `scripts/test_wiring.py` 中的哨兵清单为准。
进程边界值得单独理解一下。出手的 `executor` 跑在短命的 Celery worker 任务里,而 QMT 连接必须是一条常驻长连接(协议规定 PMS 只准开一条,多开会让指令乱序)。两者只经数据库耦合:worker 把待发委托写进出口表 `pms_qmt_order`,`pms-ws` 进程亚秒轮询取走并签名发出;QMT 的回报落进 `pms_qmt_inbox`,再由回放任务认领进账本。因此 `pms-ws` 这个服务绝不能扩副本。
三组容器服务:默认起 `pms-web`(管理页面,端口 38100);`--profile sched` 起 `pms-beat` 和 `pms-worker`(调度与执行);`--profile ws` 起 `pms-ws`(QMT 直连)。
## 严格单表访问守卫
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.x**:6.x 默认按 RESP3 握手,而现网的 Redis 版本偏旧会回 `unknown command HELLO`,导致行情与调度总线一起断。`requirements.txt` 已锁版本,升级依赖时别放开。
## 时区口径(很容易读错)
不能按「哪个系统」一刀切,要按「哪种时间」分。容器时钟与容器日志正文是北京时间,不要加八小时。数据库里的时间戳列(如各种 `created_at`、`updated_at`)是 UTC,读的时候要加八小时。日期列(如 `trade_date`、`audit_date`)是北京日期,不换算。写记录时一律用北京时间,引用数据库时间列先加八小时并标注已换算。完整依据见 `DEVLOG.md` 开头。
**待核实的例外。** 上段是历史约定。PMS 自有表的部分列实际是 `DATETIME`,`pms_repo.py` 与 `qmt_repo.py` 用容器内 `datetime.now()` 写值。10-08 的两个探针因此提出先用心跳和快照校准时区。用户已确认探针尚未执行,2026-10-12 部署核对已确认 155 宿主机为 UTC,容器为北京时间,`pms_ws_state.heartbeat_at` 的 DATETIME 原值与容器北京时间同步,该字段不加八小时。其他列仍要核对写入路径和实际时区,并明确标注原值与换算依据,不把两种口径混在同一份读数里。
## 前端的两个坑
管理页面是无构建步骤的单页,这两个坑都实际炸过页面,都写在 README 里。
1. 模板里不能用自闭合的自定义标签。`<el-alert .../>` 这种写法浏览器 HTML 解析器不认,会把后面所有内容当成它的子节点;若它带 `v-if` 且条件为假,整页就跟着消失。一律写成 `<el-xxx ...></el-xxx>`。
2. `setup()` 里定义给模板用的东西必须真的 `return` 出去。漏一个函数没接出去,用到它的地方会在渲染时报错并白屏。`scripts/test_page_wiring_guard.py` 就是为这个坑加的护栏(2026-09-10 白屏即漏了十一个显示函数所致)。
页面有三道静态护栏:`scripts/test_page_enum_guard.py`、`scripts/test_page_wiring_guard.py`、`scripts/test_page_copy_guard.py`。改页面后要一起跑,涉及布局和交互时还要浏览器验收。
页面措辞要专业,不出现内部设计词,不自造简写。同一概念统一用基本面、技术面、择时三种维度。看多用红色,看空用绿色,中性用灰色,无读数用虚框。说明放在悬停里,表格要能在中栏完整显示。
## 加测试时的登记纪律
`scripts/run_tests.py` 把「缺一个测试文件」当失败处理,而不是当「跳过」——因为镜像旧了的时候,新加的那批测试正好全部「不存在」,一条没跑却会报全部通过。所以新增一个 `test_batch*.py`,必须同时登记进 `scripts/run_tests.py` 的 `SUITES` 清单和它上面的例数说明表,否则会被判失败。
代码里还有若干「哨兵断言」,改了对应的东西就得回到那里改断言,断言不动就是漏了。清单写在 `run_tests.py` 顶部的注释里(DDL 表数、调度位、API 路由、密钥名单、动作求值器次序等)。改这些结构性的东西时先去那份清单对一遍。
## 部署与运维纪律
在开发机修改代码,通过git提交,然后在服务器上部署
部署方式与服务器信息以 `DEVLOG.md` 开头那段为准(PMS 跑在哪台机、决策系统与因子桥各在哪、各自怎么生效)。有模拟仓与真实仓两套实例,靠表名前缀(`PMS_TABLE_PREFIX`)与容器名前缀区分,共用同一套代码。对真实仓做写操作前先确认清楚是哪一套。
服务器上的容器操作偏敏感:不要随意重启或关停容器,涉及容器与数据库的操作先与用户确认,不碰本仓库以外的项目。登录到哪台机、面对的是哪一套实例,不清楚时先问。
每次交付写明验证命令在哪台机器上运行,以及预期结果。一个命令块只放一台机器的命令。重要节点及时追加到 `DEVLOG.md`,包含做了什么、动了哪些文件、部署方式、真机判收、还欠着什么。判收只写「真机见过」或「未判收」,本地通过不代替真机判收。
## 文档地图
仓库根目录有大量设计与交接文档,需要细节时按主题查这几份:
- `README.md`:总览,最全。上面各节多是从它摘的。
- `DEVLOG.md`:开发节点流水账,每条含「做了什么、动了哪些文件、部署方式、真机判收、还欠什么」五样。查某个改动的来龙去脉看这里。
- `docs/开发接续_2026-10-12.md`:Claude 到 Codex 的仓库迁移记录、修复整合、155 部署判收和当前待办入口。
- `POSITION_MGMT_DESIGN.md`:总体设计定稿。
- `QMT_WS_PROTOCOL.md`:PMS 与 QMT 的 WebSocket 协议,下发通道的唯一实现依据。
- `BIONIC_PMS_INTERFACE.md`:PMS 与决策系统的接口契约(研判闸与盘中择时两个接口)。
- `UPSTREAM_PLAN_API.md`:上游选股计划接口的接入记录,候选池的事实源。
- `docs/复盘决定台账.md`:近期改动按「台账」编号推进,提交信息里的台账号对应到这里。
## 提交约定
提交信息用简短中文,描述这次改动的行为而非罗列文件。近期工作按台账编号组织,提交里出现的台账号可回到 `docs/复盘决定台账.md` 对照。