2026-07-27 15:50:57 +08:00
|
|
|
|
# tradingSystem — 综合交易系统(PMS 持仓管理)
|
|
|
|
|
|
|
|
|
|
|
|
命令驱动的持仓管理系统:**用户通过管理页面下达大方向命令**(总规模/仓位上限/升降仓/对某股做T)→ **持仓系统制订分股分批方案并管理账本**(批次、摊薄成本、安全垫、纪律)→ **决策系统负责研判与择时**(其形态计算、每日定时分析等核心功能照旧运行)→ **下游系统(QMT 侧)挂单成交**。
|
|
|
|
|
|
|
|
|
|
|
|
架构要点:持仓系统 ↔ 决策系统直接交互;持仓系统 ↔ 下游系统交互;**决策系统与下游断开**——对下游的管理权限(建仓审批、卖出指挥)由决策系统移交持仓系统,其全部分析产出保留并成为持仓系统的输入。上游量化系统输出不变,持仓系统的建仓计划围绕上游输出建立。
|
|
|
|
|
|
|
|
|
|
|
|
仓位框架:百分比制(规模 200 万 / 总仓 ≤60% / 单股 ≤8% / ≤15 只,均为用户参数命令、页面可调),单股分批 50/25/25,含一手可行性检查,行业集中度硬性拦截(划分依据留接口)。决策方式:规则引擎为主,定性研判委托决策系统(持仓系统不自建研判栈)。安全垫三义:分批建仓留缓冲、浮盈垫后加仓、做T降成本(命令授权制)。
|
|
|
|
|
|
|
|
|
|
|
|
## 文档与交付物
|
|
|
|
|
|
|
|
|
|
|
|
| 文件 | 内容 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `POSITION_MGMT_DESIGN.md` | 总体设计 **V0.4(定稿,开发启动)**:命令系统与管理页面/账本/仓位框架/动作引擎/两道关口/择时执行/下游通道。功能一次性开发,上线按依赖分三步切换 |
|
2026-07-28 15:48:57 +08:00
|
|
|
|
| `QMT_WS_PROTOCOL.md` | **PMS ↔ QMT WebSocket 指令与回报协议 V1.0(定稿)**:传输与重连、Ed25519 签名与幂等、消息集、状态机、断线补发与对账兜底、部署前检查清单。**这是下发通道的唯一实现依据** |
|
|
|
|
|
|
| `QMT_INTERFACE_REQUIREMENTS.md` | 与 QMT 侧的数据与接口需求清单 **V2.0**:A 部分(只读数据)与 C 部分(切换约定)有效;**B 部分的表通道已废止**,改由上面的 ws 协议承担 |
|
|
|
|
|
|
| `ddl_pms_v1.sql` | PMS 全部自有表建表语句(153 代理侧,**13 张**:设计 §11 的 10 张 + ws 通道 3 张) |
|
2026-07-27 15:50:57 +08:00
|
|
|
|
| `config/settings.py` | 配置(基础设施键名对齐 bionic;业务参数为初值,页面调参持久化到 `pms_runtime_param` 后优先) |
|
2026-07-27 17:12:09 +08:00
|
|
|
|
|
|
|
|
|
|
## 模块地图
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
app/
|
|
|
|
|
|
core/ 纯逻辑, 零外部依赖, 可单测 —— 系统的算数与纪律都在这里
|
|
|
|
|
|
sizer.py 批次拆分与一手合并 / 组合约束 / 风险敞口披露
|
|
|
|
|
|
cushion.py 摊薄成本 / 安全垫状态机 / 保垫触发 / 卖出核销次序
|
|
|
|
|
|
command_spec.py 命令目录(A/B/C 全量 27 类) / 参数校验 / 双状态机 / 冲突识别
|
|
|
|
|
|
planner.py 方案生成器: 降仓凑额四档 / 升仓 / 建仓 / 清仓 / 行业 / 撤单
|
|
|
|
|
|
recon.py 成交认领与入账映射 / 对账差异与修正 / 除权检测 / T+1 可用量
|
2026-07-28 09:10:07 +08:00
|
|
|
|
rule_gate.py 规则闸终检: 上限/一手/可卖/冻结/刹车/行业/不追高 (减持只放行不阻拦)
|
|
|
|
|
|
exec_timing.py 择时实现B: 分日配额 / 分笔 / 买卖出手判定 / 14:45 兜底 / 窗口收口
|
2026-07-28 09:29:53 +08:00
|
|
|
|
action_engine.py 动作引擎: FILL 回踩补足 / ADD 盈利加仓 / DCA 补仓 / TRIM 保垫减仓
|
2026-07-28 11:14:02 +08:00
|
|
|
|
signal_rules.py 决策系统两条信号流的解析与消化口径 (含置信度尺度归一)
|
2026-07-27 17:12:09 +08:00
|
|
|
|
tradedays.py 交易日历: 调度守卫与执行窗口计算
|
2026-07-28 15:48:57 +08:00
|
|
|
|
ws_codec.py QMT 协议编解码: 规范化串 / Ed25519 签名验签 / 信封 / seq 水位推进
|
2026-07-27 17:12:09 +08:00
|
|
|
|
db/session.py 三库连接 + **严格单表访问守卫** (JOIN/逗号连表/跨表子查询一律拒绝)
|
2026-07-28 15:48:57 +08:00
|
|
|
|
repo/ 单表数据访问: pms_repo (自有 10 表) / qmt_repo (ws 通道 3 表)
|
|
|
|
|
|
/ downstream_repo (下游只读三表)
|
2026-07-27 17:12:09 +08:00
|
|
|
|
services/ 编排层
|
|
|
|
|
|
param_store.py 运行参数中心 (表值优先于 settings 初值, 页面调参即时生效)
|
|
|
|
|
|
portfolio.py 组合快照 (账本+行情+行业 → 方案/规则闸/页面的统一输入)
|
|
|
|
|
|
command_service.py 命令下达→校验→冲突→生效/规划→进度推进→撤销
|
2026-07-28 09:10:07 +08:00
|
|
|
|
executor.py 方案→指令→分日出手→窗口收口 (规则闸与择时的编排落点)
|
2026-07-28 15:48:57 +08:00
|
|
|
|
dispatcher.py 下发通道两适配器: shadow(默认) / ws(落 pms_qmt_order 出口表)
|
2026-07-28 09:29:53 +08:00
|
|
|
|
proposal_service.py 自主提议: 扫描→规则闸→研判闸→按自主档位分流 (执行/入队)
|
|
|
|
|
|
judge.py 研判闸客户端 (决策系统未接通时自动降级为人工确认)
|
2026-07-28 11:14:02 +08:00
|
|
|
|
signal_service.py 盘中信号订阅 (db2 广播 + db3 风控卖出) → 卖出指令或提议
|
2026-07-27 17:12:09 +08:00
|
|
|
|
ledger_service.py 成交回放 / 对账 / 除权 / 盘前 / 日终结算 / 运营日报
|
|
|
|
|
|
market.py 行情 (Redis db13) 与参考位 (决策系统主口径 + 兜底自算)
|
|
|
|
|
|
industry.py 行业划分可插拔适配器 (custom_table / gp_stock_category / 停用)
|
2026-07-28 15:48:57 +08:00
|
|
|
|
ws/runner.py **常驻连接进程 (pms-ws)**: 握手/心跳/重连/补发 + 出口出栈 + 上行落库确认
|
2026-07-27 17:12:09 +08:00
|
|
|
|
web/ FastAPI + 单页 (Vue3 + ElementPlus),页面四块 + 运维/日报抽屉
|
|
|
|
|
|
scheduler.py Celery beat 调度总表 (设计 §10 八个调度位 + 三条守卫)
|
|
|
|
|
|
scripts/
|
|
|
|
|
|
run_tests.py 一次跑完全部单测 (见下方「Docker 部署」)
|
|
|
|
|
|
test_core_units.py 仓位与安全垫核心逻辑 14 例
|
|
|
|
|
|
test_batch2_units.py 命令 / 方案 / 回放对账 纯逻辑 35 例
|
2026-07-28 15:48:57 +08:00
|
|
|
|
test_batch3_units.py 规则闸 / 择时执行器实现B 纯逻辑 19 例
|
2026-07-28 09:29:53 +08:00
|
|
|
|
test_batch4_units.py 动作引擎 四类自主动作触发与数量口径 11 例
|
2026-07-28 11:14:02 +08:00
|
|
|
|
test_batch5_units.py 决策系统信号流解析与消化口径 8 例
|
2026-07-29 09:30:22 +08:00
|
|
|
|
test_batch6_units.py ws 通道: 测试向量/签名/公钥形态/水位/DDL体检 51 例
|
2026-07-29 10:01:00 +08:00
|
|
|
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 33 例
|
|
|
|
|
|
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
|
2026-07-27 17:12:09 +08:00
|
|
|
|
check_db.py 实机连通性与表结构自检 (需真实 .env)
|
2026-07-29 10:01:00 +08:00
|
|
|
|
gen_keys.py ws 通道密钥: 生成 / 只取公钥(--pubkey) / PEM(--pem) / 自检(--check)
|
|
|
|
|
|
reset_ledger.py 清空账本并把回放游标对齐到当前 (影子运行期重来一次; 不碰下游表)
|
2026-07-27 17:12:09 +08:00
|
|
|
|
```
|
2026-07-27 15:50:57 +08:00
|
|
|
|
|
|
|
|
|
|
## 三条铁律
|
|
|
|
|
|
|
|
|
|
|
|
1. **命令至上**:自动决策不得突破用户命令参数;冲突时命令优先;命令间冲突由用户裁决。
|
|
|
|
|
|
2. **分工不越权**:持仓系统管「做什么、多少」,决策系统管「该不该、何时」,下游只管执行;研判不可用时降级为保守规则 + 人工确认,不自建第二套研判。
|
|
|
|
|
|
3. **先记账后动作 + 故障即守成**:指令先落表再下发;故障不产生新指令;账本与下游定期对账,以下游为实际持仓事实源。
|
|
|
|
|
|
|
2026-07-27 16:02:43 +08:00
|
|
|
|
## Docker 部署(项目统一以容器方式构建运行)
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
服务共用一个镜像:`pms-web`(管理页面,端口 38100)+ `pms-beat` / `pms-worker`(Celery 调度与执行,挂在 `sched` profile 下)。
|
2026-07-27 16:02:43 +08:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 服务器首次部署
|
|
|
|
|
|
git clone <仓库地址> && cd tradingSystem
|
|
|
|
|
|
cp .env.example .env && vim .env # 填入真实连接串 (.env 不入库)
|
|
|
|
|
|
docker compose build # 默认走清华 PyPI 镜像; 可 --build-arg PIP_INDEX_URL=... 覆盖
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
# 构建验证 (不连库, 秒级): 应输出 ALL SUITES PASS
|
|
|
|
|
|
docker compose run --rm pms-web python scripts/run_tests.py
|
2026-07-27 17:31:34 +08:00
|
|
|
|
# 建表 (幂等; 不加 --yes 只演练打印)
|
|
|
|
|
|
docker compose run --rm pms-web python scripts/init_db.py --yes
|
|
|
|
|
|
# 实机自检 (连库, 需 .env): 库连通 + pms_* 十表 + 下游表完整列定义 + 行情 Redis
|
2026-07-27 17:12:09 +08:00
|
|
|
|
docker compose run --rm pms-web python scripts/check_db.py
|
|
|
|
|
|
|
|
|
|
|
|
docker compose up -d # 管理页面
|
|
|
|
|
|
curl http://127.0.0.1:38100/health # 健康检查 + 配置装载自证 + 库连通自证
|
|
|
|
|
|
# 浏览器打开 http://<服务器IP>:38100/ → 参数设置 / 命令台 / 持仓与账本 / 提议确认
|
|
|
|
|
|
|
|
|
|
|
|
docker compose --profile sched up -d # 启用调度器 (beat + worker)
|
|
|
|
|
|
docker compose logs -f pms-beat pms-worker
|
2026-07-27 16:02:43 +08:00
|
|
|
|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
docker compose --profile ws up -d pms-ws # 启用 QMT 直连 (先配好 .env 里的两把密钥)
|
|
|
|
|
|
docker compose logs -f pms-ws
|
|
|
|
|
|
|
2026-07-27 16:02:43 +08:00
|
|
|
|
# 日常更新
|
|
|
|
|
|
git pull && docker compose build && docker compose up -d
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-29 10:01:00 +08:00
|
|
|
|
基础镜像 `python:3.11-slim` 拉取慢时,先给服务器 Docker 配置 registry 镜像加速。日志落 `./logs`(已挂载卷);容器时区 Asia/Shanghai。
|
|
|
|
|
|
|
|
|
|
|
|
**管理页面的两个坑**(都踩过,写在这里):
|
|
|
|
|
|
|
|
|
|
|
|
1. **前端资源走 unpkg CDN**,浏览器需能访问外网。上不了外网的表现不是报错,而是**只剩一个头部、下面全空**——`el-*` 组件全部渲染不出来。离线办法:把 Vue3 / ElementPlus / axios 三个库放进 `app/web/static/vendor/`,再把 `index.html` 头部那 5 个 URL 换成 `/static/vendor/xxx`(后端已把 `static` 挂在 `/static`,页面本身无构建步骤)。
|
|
|
|
|
|
2. **模板里不能用自闭合的自定义标签**。`<el-alert ... />` 这种写法浏览器 HTML 解析器**不认**——只有 `br`/`img`/`input` 这类 void 元素和 SVG 才认自闭合。写成 `<el-xxx/>` 会被当成开标签,**后面所有内容都变成它的子节点**;若它带 `v-if` 且条件为假,整页就跟着消失。2026-07-29 页面白屏即此故(75 处自闭合,第一处 `<el-alert v-if="err"/>` 把整页吞掉了)。一律写成 `<el-xxx ...></el-xxx>`。
|
2026-07-27 17:12:09 +08:00
|
|
|
|
|
2026-07-27 17:31:34 +08:00
|
|
|
|
**依赖版本坑**:`redis` 必须锁在 5.x。redis-py 6.x 默认按 RESP3 握手(先发 `HELLO 3`),而 208/150 两台 Redis 是 6.0 以前的版本,会回 `unknown command HELLO`,行情与 Celery 总线一起断。`requirements.txt` 已锁版本,代码侧另有 `protocol=2` 双保险;升级依赖时别把这条放开。
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
## 调度总表(`--profile sched` 生效,设计 §10)
|
|
|
|
|
|
|
|
|
|
|
|
| 调度 | 时间 | 任务 | 本批状态 |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| 盘前准备 | 交易日 08:50 | T+1 可卖重置 / 参考位取数 / 刹车结算 | ✅ |
|
|
|
|
|
|
| 命令轮询 | 每 1 分钟(全天) | 新命令解析 → 方案生成 → 状态机推进 | ✅ |
|
|
|
|
|
|
| 成交回放 | 交易时段每 5 分钟 | `trading_order` 增量回放 + 盘中轻对账 | ✅ |
|
2026-07-28 09:29:53 +08:00
|
|
|
|
| 盘中执行 | 交易时段每 1 分钟 | 方案转指令 → 自主提议扫描 → 择时出手(规则闸终检 → 下发 → 记子单) | ✅ |
|
2026-07-28 11:14:02 +08:00
|
|
|
|
| 信号消化 | 交易时段每 1 分钟 | 订阅 db2 盘中广播 + db3 风控卖出 → 卖出指令或提议 | ✅ |
|
2026-07-27 17:12:09 +08:00
|
|
|
|
| T 仓平回 | 14:50 | 做T强制平回 | 🔜 二期(现只自证 T 仓为 0) |
|
|
|
|
|
|
| 日终结算 | 15:10 | 除权检测 / 全量对账 / 安全垫 / 命令进度日结 | ✅ |
|
|
|
|
|
|
| 运营日报 | 15:30 | 关注区 + 全量统计(页面「日报」按钮可查) | ✅ |
|
|
|
|
|
|
|
|
|
|
|
|
调度器三条守卫:交易日守卫、故障即守成(任务内异常吞掉记 ERROR,绝不因调度异常产生新指令)、全局暂停执行(休假模式下除对账与日报外全部跳过)。
|
|
|
|
|
|
|
2026-07-28 09:10:07 +08:00
|
|
|
|
## 指令下发通道(设计 §9,参数 `PMS_DISPATCH_MODE`,默认 `shadow`)
|
|
|
|
|
|
|
|
|
|
|
|
| 模式 | 行为 | 什么时候用 |
|
|
|
|
|
|
|---|---|---|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
| `shadow`(默认) | 指令照常过规则闸、照常置 DISPATCHED,但**不写下游**。你在 QMT 侧人工执行,成交由回放按 FIFO 认领回账本 | **当前仍是这个状态**(切 ws 需先配密钥并跑完 S1/S2 联调) |
|
|
|
|
|
|
| `ws` | WebSocket 直连 QMT 执行服务,协议见 `QMT_WS_PROTOCOL.md` V1.0 | 目标形态。**通道已实现**(2026-07-28),联调通过即可切 |
|
|
|
|
|
|
|
|
|
|
|
|
**ws 模式的进程边界**(理解这条通道的关键):连接是一条常驻长连接,且协议 §1 规定 PMS 只准开一条(多开会让指令乱序),所以它由独立的 `pms-ws` 进程独占;而 `executor` 跑在 celery worker 这种短命任务进程里。两者**只经数据库耦合**:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
executor (worker) ──写 pms_qmt_order(QUEUED)──▶ pms-ws ──签名──▶ QMT
|
|
|
|
|
|
│
|
|
|
|
|
|
QMT ──trade/order_update──▶ pms-ws ──落 pms_qmt_inbox──▶ 回放任务(worker) ──▶ 账本
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
出口队列落在业务表而不是内存或 Redis,是因为「先记账后动作」这条铁律在这里可以字面成立——**落表就是记账**;ws 进程崩了重启队列还在,页面查 `pms_qmt_order` 就能看到在途委托,也不引入任何新中间件。代价是亚秒级轮询延迟,而择时本就是分钟级节奏。
|
|
|
|
|
|
|
|
|
|
|
|
`pms-ws` **只做通道**,不碰账本:成交入账仍然发生在 worker 里,账本变更保持单一来源,也不至于让一个慢查询把心跳拖到 15 秒超时断连。
|
|
|
|
|
|
|
|
|
|
|
|
**放不放行**(协议 §6.3「故障即守成」的落点):
|
|
|
|
|
|
|
|
|
|
|
|
| ws 进程 | 连接 | 买入 | 卖出 |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| 心跳陈旧(进程没了) | — | 拒发 | 拒发(排进队列也没人发) |
|
|
|
|
|
|
| 在线 | 断开/重连中 | 拒发 | **照常入队**,重连后立即发出 |
|
|
|
|
|
|
| 在线 | 已连接 | 放行 | 放行 |
|
|
|
|
|
|
|
|
|
|
|
|
> 原 `plan_x` / `channel_y` 两个适配器已于 2026-07-28 删除。前者写 `trading_buy_plan(is_active=6)`,那个 6 本身就是错的;后者写 `pms_order_request` 表由下游轮询,在通道改定 WebSocket 后作废。`pms_order_request` 表保留未用,不必删表。留着作废路径比删掉更危险——后来者会以为它可用。
|
|
|
|
|
|
|
|
|
|
|
|
**目标架构(2026-07-28 已定)**:`trading_service` 全量退出业务,只保留看板与统计展示;新写一个 QMT 直连服务承担挂单与订单/持仓/资金回写,PMS 经 WebSocket 与它直连;PMS 只管决策与账本。
|
2026-07-28 11:14:02 +08:00
|
|
|
|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
**当前处境(重要)**:旧通道(下游直接执行决策系统卖出信号、轮询 `trading_buy_plan` 自动挂单)已按双方约定**即刻停用**。新的 ws 通道代码已就绪,但 `PMS_DISPATCH_MODE` 仍是 `shadow` 且 `PMS_QMT_WS_ENABLED=False`,所以现在**依然没有任何系统会自动下单**——PMS 影子运行,指令照常生成、过闸、记账,但需人工在 QMT 侧执行。这是安全的状态,但要知道它是这个状态。切 ws 是**两个开关加一次联调**,不是再写代码。
|
2026-07-28 09:10:07 +08:00
|
|
|
|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
影子模式下的完整闭环:页面下命令 → 方案落表 → 方案转指令 → 择时按日配额给出「今天该出多少、什么价、挂到几点」→ 你照着在 QMT 下单 → 5 分钟一次的回放把成交认领回批次账本 → 命令进度自动推进。整条链路除了「人手下单」这一步,其余与实盘接管后完全一致。
|
2026-07-28 09:10:07 +08:00
|
|
|
|
|
2026-07-29 10:01:00 +08:00
|
|
|
|
### 回放游标:首次启动不追认历史
|
|
|
|
|
|
|
|
|
|
|
|
`trading_order` 里躺着旧系统多年的成交。PMS 刚上线时一条在途指令都没有,若从表头开始回放,每条历史成交都会被判成「外部成交」并入 BASE 批次——账本上凭空长出一堆早已清掉的持仓,**摊薄成本与安全垫全错**,而补仓、盈利加仓、保垫减仓都挂在安全垫上,一错就是整条纪律链。
|
|
|
|
|
|
|
|
|
|
|
|
所以 `PMS_REPLAY_CURSOR` 为空时,`replay_fills` **只把游标对齐到当前最新成交、一条都不入账**,并打一条 WARNING 说明。要补历史(页面「参数设置」改这个参数):
|
|
|
|
|
|
|
|
|
|
|
|
| 游标值 | 行为 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 留空 | 首次启动自动对齐到最新成交,不追认历史 |
|
|
|
|
|
|
| 某个 `order_id` | 从它之后开始回放 |
|
|
|
|
|
|
| `ALL` | 从头全量回放 |
|
|
|
|
|
|
|
|
|
|
|
|
已经被历史成交污染的账本,用 `scripts/reset_ledger.py` 清掉重来(只删 PMS 自有表,绝不碰下游 `trading_*`),然后跑一次 `POST /api/ops/reconcile?apply_fix=true` 让对账以下游为准把真实持仓补回来。
|
|
|
|
|
|
|
2026-07-28 09:29:53 +08:00
|
|
|
|
## 自主提议的分流(设计 §6 / §7)
|
|
|
|
|
|
|
|
|
|
|
|
动作引擎每分钟扫一遍持仓,产出 FILL / ADD / DCA / TRIM 四类候选,然后依次过三道:
|
|
|
|
|
|
|
|
|
|
|
|
1. **规则闸**(一级,纯代码)——不过就拒,未通过项落评审账本。自主动作受组合刹车约束(命令驱动不受)。
|
|
|
|
|
|
2. **研判闸**(二级,仅补足/加仓/补仓,委托决策系统)——`PMS_JUDGE_API_BASE` 为空即视为未接通,按设计**自动降级为人工确认**并记 ERROR,绝不把「研判拿不到」当成「研判通过」。
|
|
|
|
|
|
3. **按自主档位分流**——`full` 执行、`propose_only` 入队(一期默认)、`off` 不扫描。
|
|
|
|
|
|
|
|
|
|
|
|
两条无条件覆盖档位的规矩:**减持方向不设确认门槛**(TRIM 保垫减仓任何档位都直接落指令);**−15% 及更深的补仓永远需用户确认**(即便档位是 full 也强制入队)。同一只票的同一动作若已有在途提议或在途指令,不重复提。
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
## 已实现 / 待开发
|
|
|
|
|
|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
**已实现**:建表 DDL 与建表脚本;配置与运行参数中心;仓位规划器与安全垫账;命令系统(27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B(分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器八个调度位;**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 163 例**。
|
|
|
|
|
|
|
|
|
|
|
|
### 下一步(按可动工顺序)
|
|
|
|
|
|
|
|
|
|
|
|
| # | 事项 | 状态 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| 1 | **ws 通道的账本侧改造**(清单 4~6) | 可立即开工,见下方「ws 通道实现清单」 |
|
|
|
|
|
|
| 2 | ws 通道联调(协议 §9 的 S1/S2/S3) | 需 QMT 侧配合:公钥交换 + IP 加白 |
|
|
|
|
|
|
| 3 | T0 做T(二期) | 可做,设计已有,无外部依赖 |
|
|
|
|
|
|
| 4 | 择时实现 A(委托决策系统盘中择时) | 阻塞:等 bionic 侧接口 |
|
|
|
|
|
|
| 5 | 研判闸接通 | 阻塞:等 bionic 侧 `process_intraday_audit` 新增 PMS 请求 direction。客户端已就位,接口好了在页面填 `PMS_JUDGE_API_BASE` 即通 |
|
|
|
|
|
|
| 6 | bionic 侧配套改造(出口改道 + PMS direction) | 另一仓库 |
|
|
|
|
|
|
|
|
|
|
|
|
### ws 通道实现清单
|
|
|
|
|
|
|
|
|
|
|
|
协议 `QMT_WS_PROTOCOL.md` V1.0 已定稿,端点 `ws://192.168.16.98:8080`,明文 ws + Ed25519 双向签名。
|
|
|
|
|
|
|
|
|
|
|
|
1. ✅ **常驻连接进程** —— `app/ws/runner.py` + compose 的 `pms-ws`(`--profile ws`,**绝不可扩副本**,协议 §1 只准一条连接)。四个协程:存活心跳 / 收帧落库 / 5 秒 ping / 0.5 秒出口轮询。SIGTERM 优雅退出(停取新单 → 刷水位 → 最后一次 ack → 关连接 → 置 STOPPED 并清心跳),`stop_grace_period: 25s`。所有 DB 调用走 `to_thread`,不阻塞事件循环。
|
|
|
|
|
|
2. ✅ **`dispatcher._ws()`** —— 落 `pms_qmt_order` 出口表置 QUEUED 即返回,签名与发送由 ws 进程做。参数不合协议(非整百、限价缺失、代码非点式)在本地就拦下。撤单把父指令名下所有在途子单标 `cancel_state=REQUESTED`。
|
|
|
|
|
|
3. ✅ **上行消费与 seq 水位** —— `pms_qmt_inbox` 落库(seq 主键 + `trade_no` 唯一索引 = 协议 §6.1/§5.5 的双层去重),`pms_ws_state` 存连续水位,按 20 条 / 2 秒发 `ack_seq`。**落库失败绝不 ack**——落不了库就主动断线,让对端从 `last_seq+1` 重发,用协议自带的补发机制而不是自攒重试队列。
|
|
|
|
|
|
4. 🔜 **`recon` 改为吃 `trade` 消息入账** —— 从 `pms_qmt_inbox` 里 `processed=0` 的 trade 行消费;FIFO 贪心认领退化为只处理外部/人工成交;`downstream_repo.FILLED_STATUSES` 降为旁路校验,不再作为成交判据。
|
|
|
|
|
|
5. 🔜 **手续费口径** —— `fee` 不摊进持仓成本(对方明确逐笔费用可能有误差),只记现金流出,日终用资金快照反推校准。避免污染安全垫。
|
|
|
|
|
|
6. 🔶 **`CANCELLED` / `EXPIRED` 自记** —— 通道层已做:`pms_qmt_order.cancel_state` 记本地是否发过撤单,与对端回的 `status` 不一致时告警。账本侧的口径随第 4 条一起落。
|
|
|
|
|
|
|
|
|
|
|
|
部署前另有一份检查清单在协议 §10.1.1(白名单、公钥交换、Redis 持久化、密钥只走 `.env`)。
|
|
|
|
|
|
|
|
|
|
|
|
### 切 ws 通道的操作顺序
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-07-28 17:12:49 +08:00
|
|
|
|
# 1. 生成本端密钥对: seed 填进 .env 的 PMS_QMT_SIGN_SEED_HEX
|
2026-07-28 16:06:16 +08:00
|
|
|
|
docker compose run --rm pms-web python scripts/gen_keys.py
|
2026-07-28 17:12:49 +08:00
|
|
|
|
# 公钥交给 QMT 侧要 PEM 形态 (他们用 load_pem_public_key 读文件):
|
|
|
|
|
|
docker compose run --rm pms-web python scripts/gen_keys.py --pem > pms_public.pem
|
2026-07-28 15:48:57 +08:00
|
|
|
|
# 2. 把 QMT 侧公钥填进 .env 的 PMS_QMT_PEER_PUBKEY_B64; 把本机内网 IP 报给对方加白
|
2026-07-28 16:06:16 +08:00
|
|
|
|
# 3. 密钥自检 (含协议 §2.1.1 测试向量, 双方各跑一次对上再联调)
|
|
|
|
|
|
docker compose run --rm pms-web python scripts/gen_keys.py --check
|
2026-07-28 15:48:57 +08:00
|
|
|
|
# 4. 建新表 (幂等) 并自检
|
|
|
|
|
|
docker compose run --rm pms-web python scripts/init_db.py --yes
|
|
|
|
|
|
docker compose run --rm pms-web python scripts/check_db.py # [6] 段看通道状态
|
|
|
|
|
|
# 5. 起进程 (此时 PMS_DISPATCH_MODE 仍是 shadow, 只连不发)
|
|
|
|
|
|
docker compose --profile ws up -d pms-ws && docker compose logs -f pms-ws
|
|
|
|
|
|
# 6. 页面把 PMS_QMT_WS_ENABLED 打开, 观察 /api/ws-channel 的 conn_state 与 seq 水位
|
|
|
|
|
|
# 7. S2/S3 联调通过后, 页面把 PMS_DISPATCH_MODE 改成 ws
|
|
|
|
|
|
```
|
2026-07-27 17:12:09 +08:00
|
|
|
|
|
2026-07-28 17:12:49 +08:00
|
|
|
|
> `scripts/gen_keys.py` 还有 `--pubkey`(打印当前私钥对应的公钥两种形态,不打印私钥)。**别用 `python -c` 加 shell 续行 `\` 拼这段**:续行会把下一行的行首缩进一起带进 Python 源码字符串,直接 `IndentationError: unexpected indent`。
|
|
|
|
|
|
|
|
|
|
|
|
**公钥的两种形态**(2026-07-28 交换密钥时踩到,两边要的不是同一种):
|
|
|
|
|
|
|
|
|
|
|
|
| 谁 | 要什么 | 长什么样 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| PMS 的 `.env`(`PMS_QMT_PEER_PUBKEY_B64`) | 一行字符串 | 裸 32 字节 base64 `Oh3Cd30l...`;也接受 PEM 正文那一行、PEM 全文、64 位 hex |
|
|
|
|
|
|
| QMT 侧的 `crypto.py`(`load_pem_public_key`) | 一个 `.pem` 文件 | `-----BEGIN PUBLIC KEY-----` 三行 |
|
|
|
|
|
|
|
|
|
|
|
|
两者装的是同一把钥匙:PEM 正文解出来是 44 字节 = 12 字节 SPKI 固定头 + 32 字节裸公钥。协议 §2.1.1 正文用的是裸 base64,所以把 PEM 正文原样粘进 `.env` 会报「44 字节,长度不对」。`ws_codec.normalize_pubkey()` 现在四种形态全收并自动归一——对方发来什么直接粘上去即可,`gen_keys.py --check` 会告诉你它识别成了哪种。
|
2026-07-28 16:06:16 +08:00
|
|
|
|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
任一步不放心都可以退回去:把 `PMS_DISPATCH_MODE` 改回 `shadow` 即恢复人工执行,`pms-ws` 停掉也只是让指令拒发(保持原状),不会产生半截状态。
|
2026-07-27 17:12:09 +08:00
|
|
|
|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
**待外部协商(剩余)**:`QMT_INTERFACE_REQUIREMENTS.md` 的 A/C/D 各项。已闭环的有:A2 状态枚举与成交均价、A3 资金快照、A4 逐笔成交、Q1~Q11 与 R1/R2(详见协议 §10)。已知存疑项:`trading_log.extra_data` 里的 `total_filled` 口径与示例数据矛盾,PMS 事后核对绕开该列,只读 `traded_volume / traded_price / traded_amount`(协议 §10.2)。
|
2026-07-27 16:02:43 +08:00
|
|
|
|
|
2026-07-27 15:50:57 +08:00
|
|
|
|
## 开发约定
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
- 开发机与服务器经 git 同步代码;**构建与运行统一走 Docker**(`docker compose build` → 容器内 `python scripts/run_tests.py` → `up -d`),测试结果回传后迭代。
|
|
|
|
|
|
- 153 代理侧数据库严格单表访问;该纪律已落到 `app/db/session.py` 的静态守卫,违规 SQL 在执行前抛 `MultiTableSQL`。持仓系统内部代码统一 Tushare 点式(`600000.SH`),读决策系统结论表时转前缀式。
|
2026-07-28 15:48:57 +08:00
|
|
|
|
> 2026-07-28 修了守卫本身的一个误判:`ON DUPLICATE KEY UPDATE` 后面跟的是列名不是表名,原来会被当成第二张表,于是**所有 upsert 一执行就抛 `MultiTableSQL`**——页面改参数、`ensure_position`、日报落库、行业映射导入四条路径全中。单测走内存桩不经过该函数,所以一直没暴露。回归用例在 `test_batch6_units.py` 的 [G] 组。
|
2026-07-27 17:12:09 +08:00
|
|
|
|
- 配置分两层:基础设施连接串在 `.env`(服务器手工维护,不入库,模板见 `.env.example`);业务参数在 `config/settings.py` 只是初值,上线后经管理页面修改并持久化到 `pms_runtime_param` 表。**业务代码禁止直接读 settings 取业务参数**,一律走 `services/param_store.py`。
|
2026-07-28 15:48:57 +08:00
|
|
|
|
> 例外是**密钥**:`PMS_QMT_SIGN_SEED_HEX` / `PMS_QMT_PEER_PUBKEY_B64` 只走 `.env`(协议 §10.1.1),已列入 `param_store.SECRET_KEYS`——页面读不到、改不了、快照里也不出现。新增密钥类配置记得同步加进那个元组,否则它会被当成普通业务参数显示在参数设置页上。
|
|
|
|
|
|
- 新增纯逻辑一律进 `app/core/`(零外部依赖 + 配套单测);需要连库的编排进 `app/services/`,并保证连库失败时降级而非崩页。ws 通道的协议编解码在 `app/core/ws_codec.py`,改动后**必须先跑通协议 §2.1.1 的测试向量**(`test_batch6_units.py` 的 [A] 组)——规范化串两边写不一致的话,联调只会告诉你"签名验不过",看不出差在哪一段。
|
2026-07-27 15:50:57 +08:00
|
|
|
|
- 里程碑(设计定稿、建表、各期上线)及时 git 提交。
|
2026-07-28 15:48:57 +08:00
|
|
|
|
- **接手前先读三份**:本文件 → `POSITION_MGMT_DESIGN.md`(V0.4 定稿,**勿改设计**)→ `QMT_WS_PROTOCOL.md`(V1.0 定稿,下发通道的唯一依据)。三份读完即可开工,不需要额外的口头背景。
|