tradingSystem/README.md

23 KiB
Raw Permalink Blame History

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.0A 部分(只读数据)与 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)

三条铁律

  1. 命令至上:自动决策不得突破用户命令参数;冲突时命令优先;命令间冲突由用户裁决。
  2. 分工不越权:持仓系统管「做什么、多少」,决策系统管「该不该、何时」,下游只管执行;研判不可用时降级为保守规则 + 人工确认,不自建第二套研判。
  3. 先记账后动作 + 故障即守成:指令先落表再下发;故障不产生新指令;账本与下游定期对账,以下游为实际持仓事实源。

Docker 部署(项目统一以容器方式构建运行)

服务共用一个镜像:pms-web(管理页面,端口 38100+ pms-beat / pms-workerCelery 调度与执行,挂在 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 仍是 shadowPMS_QMT_WS_ENABLED=False,所以现在依然没有任何系统会自动下单——PMS 影子运行,指令照常生成、过闸、记账,但需人工在 QMT 侧执行。这是安全的状态,但要知道它是这个状态。切 ws 是两个开关加一次联调,不是再写代码。

影子模式下的完整闭环:页面下命令 → 方案落表 → 方案转指令 → 择时按日配额给出「今天该出多少、什么价、挂到几点」→ 你照着在 QMT 下单 → 5 分钟一次的回放把成交认领回批次账本 → 命令进度自动推进。整条链路除了「人手下单」这一步,其余与实盘接管后完全一致。

自主提议的分流(设计 §6 / §7

动作引擎每分钟扫一遍持仓,产出 FILL / ADD / DCA / TRIM 四类候选,然后依次过三道:

  1. 规则闸(一级,纯代码)——不过就拒,未通过项落评审账本。自主动作受组合刹车约束(命令驱动不受)。
  2. 研判闸(二级,仅补足/加仓/补仓,委托决策系统)——PMS_JUDGE_API_BASE 为空即视为未接通,按设计自动降级为人工确认并记 ERROR绝不把「研判拿不到」当成「研判通过」。
  3. 按自主档位分流——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 双向签名。

  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_inboxprocessed=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 通道的操作顺序

# 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 的 .envPMS_QMT_PEER_PUBKEY_B64 一行字符串 裸 32 字节 base64 Oh3Cd30l...;也接受 PEM 正文那一行、PEM 全文、64 位 hex
QMT 侧的 crypto.pyload_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 同步代码;构建与运行统一走 Dockerdocker compose build → 容器内 python scripts/run_tests.pyup -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.mdV0.4 定稿,勿改设计)→ QMT_WS_PROTOCOL.mdV1.0 定稿,下发通道的唯一依据)。三份读完即可开工,不需要额外的口头背景。