|
|
||
|---|---|---|
| .idea | ||
| app | ||
| config | ||
| scripts | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| POSITION_MGMT_DESIGN.md | ||
| QMT_INTERFACE_REQUIREMENTS.md | ||
| QMT_WS_PROTOCOL.md | ||
| README.md | ||
| ddl_pms_v1.sql | ||
| docker-compose.yml | ||
| requirements.txt | ||
README.md
tradingSystem — 综合交易系统(PMS 持仓管理)
命令驱动的持仓管理系统:用户通过管理页面下达大方向命令(总规模/仓位上限/升降仓/对某股做T)→ 持仓系统制订分股分批方案并管理账本(批次、摊薄成本、安全垫、纪律)→ 决策系统负责研判与择时(其形态计算、每日定时分析等核心功能照旧运行)→ 下游系统(QMT 侧)挂单成交。
架构要点:持仓系统 ↔ 决策系统直接交互;持仓系统 ↔ 下游系统交互;决策系统与下游断开——对下游的管理权限(建仓审批、卖出指挥)由决策系统移交持仓系统,其全部分析产出保留并成为持仓系统的输入。上游量化系统输出不变,持仓系统的建仓计划围绕上游输出建立。
仓位框架:百分比制(规模 200 万 / 总仓 ≤60% / 单股 ≤8% / ≤15 只,均为用户参数命令、页面可调),单股分批 50/25/25,含一手可行性检查,行业集中度硬性拦截(划分依据留接口)。决策方式:规则引擎为主,定性研判委托决策系统(持仓系统不自建研判栈)。安全垫三义:分批建仓留缓冲、浮盈垫后加仓、做T降成本(命令授权制)。
文档与交付物
| 文件 | 内容 |
|---|---|
POSITION_MGMT_DESIGN.md |
总体设计 V0.4(定稿,开发启动):命令系统与管理页面/账本/仓位框架/动作引擎/两道关口/择时执行/下游通道。功能一次性开发,上线按依赖分三步切换 |
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 张) |
config/settings.py |
配置(基础设施键名对齐 bionic;业务参数为初值,页面调参持久化到 pms_runtime_param 后优先) |
模块地图
app/
core/ 纯逻辑, 零外部依赖, 可单测 —— 系统的算数与纪律都在这里
sizer.py 批次拆分与一手合并 / 组合约束 / 风险敞口披露
cushion.py 摊薄成本 / 安全垫状态机 / 保垫触发 / 卖出核销次序
command_spec.py 命令目录(A/B/C 全量 27 类) / 参数校验 / 双状态机 / 冲突识别
planner.py 方案生成器: 降仓凑额四档 / 升仓 / 建仓 / 清仓 / 行业 / 撤单
recon.py 成交认领与入账映射 / 对账差异与修正 / 除权检测 / T+1 可用量
rule_gate.py 规则闸终检: 上限/一手/可卖/冻结/刹车/行业/不追高 (减持只放行不阻拦)
exec_timing.py 择时实现B: 分日配额 / 分笔 / 买卖出手判定 / 14:45 兜底 / 窗口收口
action_engine.py 动作引擎: FILL 回踩补足 / ADD 盈利加仓 / DCA 补仓 / TRIM 保垫减仓
signal_rules.py 决策系统两条信号流的解析与消化口径 (含置信度尺度归一)
tradedays.py 交易日历: 调度守卫与执行窗口计算
ws_codec.py QMT 协议编解码: 规范化串 / Ed25519 签名验签 / 信封 / seq 水位推进
db/session.py 三库连接 + **严格单表访问守卫** (JOIN/逗号连表/跨表子查询一律拒绝)
repo/ 单表数据访问: pms_repo (自有 10 表) / qmt_repo (ws 通道 3 表)
/ downstream_repo (下游只读三表)
services/ 编排层
param_store.py 运行参数中心 (表值优先于 settings 初值, 页面调参即时生效)
portfolio.py 组合快照 (账本+行情+行业 → 方案/规则闸/页面的统一输入)
command_service.py 命令下达→校验→冲突→生效/规划→进度推进→撤销
executor.py 方案→指令→分日出手→窗口收口 (规则闸与择时的编排落点)
dispatcher.py 下发通道两适配器: shadow(默认) / ws(落 pms_qmt_order 出口表)
proposal_service.py 自主提议: 扫描→规则闸→研判闸→按自主档位分流 (执行/入队)
judge.py 研判闸客户端 (决策系统未接通时自动降级为人工确认)
signal_service.py 盘中信号订阅 (db2 广播 + db3 风控卖出) → 卖出指令或提议
ledger_service.py 成交回放 / 对账 / 除权 / 盘前 / 日终结算 / 运营日报
market.py 行情 (Redis db13) 与参考位 (决策系统主口径 + 兜底自算)
industry.py 行业划分可插拔适配器 (custom_table / gp_stock_category / 停用)
ws/runner.py **常驻连接进程 (pms-ws)**: 握手/心跳/重连/补发 + 出口出栈 + 上行落库确认
web/ FastAPI + 单页 (Vue3 + ElementPlus),页面四块 + 运维/日报抽屉
scheduler.py Celery beat 调度总表 (设计 §10 八个调度位 + 三条守卫)
scripts/
run_tests.py 一次跑完全部单测 (见下方「Docker 部署」)
test_core_units.py 仓位与安全垫核心逻辑 14 例
test_batch2_units.py 命令 / 方案 / 回放对账 纯逻辑 35 例
test_batch3_units.py 规则闸 / 择时执行器实现B 纯逻辑 19 例
test_batch4_units.py 动作引擎 四类自主动作触发与数量口径 11 例
test_batch5_units.py 决策系统信号流解析与消化口径 8 例
test_batch6_units.py ws 通道: 协议测试向量/签名/水位/单表守卫 44 例
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 32 例
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练)
check_db.py 实机连通性与表结构自检 (需真实 .env)
gen_keys.py ws 通道密钥: 生成 / 只取公钥(--pubkey) / 自检(--check)
三条铁律
- 命令至上:自动决策不得突破用户命令参数;冲突时命令优先;命令间冲突由用户裁决。
- 分工不越权:持仓系统管「做什么、多少」,决策系统管「该不该、何时」,下游只管执行;研判不可用时降级为保守规则 + 人工确认,不自建第二套研判。
- 先记账后动作 + 故障即守成:指令先落表再下发;故障不产生新指令;账本与下游定期对账,以下游为实际持仓事实源。
Docker 部署(项目统一以容器方式构建运行)
服务共用一个镜像:pms-web(管理页面,端口 38100)+ pms-beat / pms-worker(Celery 调度与执行,挂在 sched profile 下)。
# 服务器首次部署
git clone <仓库地址> && cd tradingSystem
cp .env.example .env && vim .env # 填入真实连接串 (.env 不入库)
docker compose build # 默认走清华 PyPI 镜像; 可 --build-arg PIP_INDEX_URL=... 覆盖
# 构建验证 (不连库, 秒级): 应输出 ALL SUITES PASS
docker compose run --rm pms-web python scripts/run_tests.py
# 建表 (幂等; 不加 --yes 只演练打印)
docker compose run --rm pms-web python scripts/init_db.py --yes
# 实机自检 (连库, 需 .env): 库连通 + pms_* 十表 + 下游表完整列定义 + 行情 Redis
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
docker compose --profile ws up -d pms-ws # 启用 QMT 直连 (先配好 .env 里的两把密钥)
docker compose logs -f pms-ws
# 日常更新
git pull && docker compose build && docker compose up -d
基础镜像 python:3.11-slim 拉取慢时,先给服务器 Docker 配置 registry 镜像加速。日志落 ./logs(已挂载卷);容器时区 Asia/Shanghai。管理页面的前端资源(Vue3 / ElementPlus / axios)走 unpkg CDN,浏览器需能访问外网;若内网隔离,把页面头部三行 <script>/<link> 换成本地文件即可(页面本身无构建步骤)。
依赖版本坑:redis 必须锁在 5.x。redis-py 6.x 默认按 RESP3 握手(先发 HELLO 3),而 208/150 两台 Redis 是 6.0 以前的版本,会回 unknown command HELLO,行情与 Celery 总线一起断。requirements.txt 已锁版本,代码侧另有 protocol=2 双保险;升级依赖时别把这条放开。
调度总表(--profile sched 生效,设计 §10)
| 调度 | 时间 | 任务 | 本批状态 |
|---|---|---|---|
| 盘前准备 | 交易日 08:50 | T+1 可卖重置 / 参考位取数 / 刹车结算 | ✅ |
| 命令轮询 | 每 1 分钟(全天) | 新命令解析 → 方案生成 → 状态机推进 | ✅ |
| 成交回放 | 交易时段每 5 分钟 | trading_order 增量回放 + 盘中轻对账 |
✅ |
| 盘中执行 | 交易时段每 1 分钟 | 方案转指令 → 自主提议扫描 → 择时出手(规则闸终检 → 下发 → 记子单) | ✅ |
| 信号消化 | 交易时段每 1 分钟 | 订阅 db2 盘中广播 + db3 风控卖出 → 卖出指令或提议 | ✅ |
| T 仓平回 | 14:50 | 做T强制平回 | 🔜 二期(现只自证 T 仓为 0) |
| 日终结算 | 15:10 | 除权检测 / 全量对账 / 安全垫 / 命令进度日结 | ✅ |
| 运营日报 | 15:30 | 关注区 + 全量统计(页面「日报」按钮可查) | ✅ |
调度器三条守卫:交易日守卫、故障即守成(任务内异常吞掉记 ERROR,绝不因调度异常产生新指令)、全局暂停执行(休假模式下除对账与日报外全部跳过)。
指令下发通道(设计 §9,参数 PMS_DISPATCH_MODE,默认 shadow)
| 模式 | 行为 | 什么时候用 |
|---|---|---|
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 只管决策与账本。
当前处境(重要):旧通道(下游直接执行决策系统卖出信号、轮询 trading_buy_plan 自动挂单)已按双方约定即刻停用。新的 ws 通道代码已就绪,但 PMS_DISPATCH_MODE 仍是 shadow 且 PMS_QMT_WS_ENABLED=False,所以现在依然没有任何系统会自动下单——PMS 影子运行,指令照常生成、过闸、记账,但需人工在 QMT 侧执行。这是安全的状态,但要知道它是这个状态。切 ws 是两个开关加一次联调,不是再写代码。
影子模式下的完整闭环:页面下命令 → 方案落表 → 方案转指令 → 择时按日配额给出「今天该出多少、什么价、挂到几点」→ 你照着在 QMT 下单 → 5 分钟一次的回放把成交认领回批次账本 → 命令进度自动推进。整条链路除了「人手下单」这一步,其余与实盘接管后完全一致。
自主提议的分流(设计 §6 / §7)
动作引擎每分钟扫一遍持仓,产出 FILL / ADD / DCA / TRIM 四类候选,然后依次过三道:
- 规则闸(一级,纯代码)——不过就拒,未通过项落评审账本。自主动作受组合刹车约束(命令驱动不受)。
- 研判闸(二级,仅补足/加仓/补仓,委托决策系统)——
PMS_JUDGE_API_BASE为空即视为未接通,按设计自动降级为人工确认并记 ERROR,绝不把「研判拿不到」当成「研判通过」。 - 按自主档位分流——
full执行、propose_only入队(一期默认)、off不扫描。
两条无条件覆盖档位的规矩:减持方向不设确认门槛(TRIM 保垫减仓任何档位都直接落指令);−15% 及更深的补仓永远需用户确认(即便档位是 full 也强制入队)。同一只票的同一动作若已有在途提议或在途指令,不重复提。
已实现 / 待开发
已实现:建表 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 双向签名。
- ✅ 常驻连接进程 ——
app/ws/runner.py+ compose 的pms-ws(--profile ws,绝不可扩副本,协议 §1 只准一条连接)。四个协程:存活心跳 / 收帧落库 / 5 秒 ping / 0.5 秒出口轮询。SIGTERM 优雅退出(停取新单 → 刷水位 → 最后一次 ack → 关连接 → 置 STOPPED 并清心跳),stop_grace_period: 25s。所有 DB 调用走to_thread,不阻塞事件循环。 - ✅
dispatcher._ws()—— 落pms_qmt_order出口表置 QUEUED 即返回,签名与发送由 ws 进程做。参数不合协议(非整百、限价缺失、代码非点式)在本地就拦下。撤单把父指令名下所有在途子单标cancel_state=REQUESTED。 - ✅ 上行消费与 seq 水位 ——
pms_qmt_inbox落库(seq 主键 +trade_no唯一索引 = 协议 §6.1/§5.5 的双层去重),pms_ws_state存连续水位,按 20 条 / 2 秒发ack_seq。落库失败绝不 ack——落不了库就主动断线,让对端从last_seq+1重发,用协议自带的补发机制而不是自攒重试队列。 - 🔜
recon改为吃trade消息入账 —— 从pms_qmt_inbox里processed=0的 trade 行消费;FIFO 贪心认领退化为只处理外部/人工成交;downstream_repo.FILLED_STATUSES降为旁路校验,不再作为成交判据。 - 🔜 手续费口径 ——
fee不摊进持仓成本(对方明确逐笔费用可能有误差),只记现金流出,日终用资金快照反推校准。避免污染安全垫。 - 🔶
CANCELLED/EXPIRED自记 —— 通道层已做:pms_qmt_order.cancel_state记本地是否发过撤单,与对端回的status不一致时告警。账本侧的口径随第 4 条一起落。
部署前另有一份检查清单在协议 §10.1.1(白名单、公钥交换、Redis 持久化、密钥只走 .env)。
切 ws 通道的操作顺序
# 1. 生成本端密钥对: seed 填进 .env 的 PMS_QMT_SIGN_SEED_HEX
docker compose run --rm pms-web python scripts/gen_keys.py
# 公钥交给 QMT 侧要 PEM 形态 (他们用 load_pem_public_key 读文件):
docker compose run --rm pms-web python scripts/gen_keys.py --pem > pms_public.pem
# 2. 把 QMT 侧公钥填进 .env 的 PMS_QMT_PEER_PUBKEY_B64; 把本机内网 IP 报给对方加白
# 3. 密钥自检 (含协议 §2.1.1 测试向量, 双方各跑一次对上再联调)
docker compose run --rm pms-web python scripts/gen_keys.py --check
# 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
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 会告诉你它识别成了哪种。
任一步不放心都可以退回去:把 PMS_DISPATCH_MODE 改回 shadow 即恢复人工执行,pms-ws 停掉也只是让指令拒发(保持原状),不会产生半截状态。
待外部协商(剩余):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)。
开发约定
- 开发机与服务器经 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 修了守卫本身的一个误判:
ON DUPLICATE KEY UPDATE后面跟的是列名不是表名,原来会被当成第二张表,于是所有 upsert 一执行就抛MultiTableSQL——页面改参数、ensure_position、日报落库、行业映射导入四条路径全中。单测走内存桩不经过该函数,所以一直没暴露。回归用例在test_batch6_units.py的 [G] 组。 - 配置分两层:基础设施连接串在
.env(服务器手工维护,不入库,模板见.env.example);业务参数在config/settings.py只是初值,上线后经管理页面修改并持久化到pms_runtime_param表。业务代码禁止直接读 settings 取业务参数,一律走services/param_store.py。例外是密钥:
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] 组)——规范化串两边写不一致的话,联调只会告诉你"签名验不过",看不出差在哪一段。 - 里程碑(设计定稿、建表、各期上线)及时 git 提交。
- 接手前先读三份:本文件 →
POSITION_MGMT_DESIGN.md(V0.4 定稿,勿改设计)→QMT_WS_PROTOCOL.md(V1.0 定稿,下发通道的唯一依据)。三份读完即可开工,不需要额外的口头背景。