diff --git a/DEVLOG.md b/DEVLOG.md new file mode 100644 index 0000000..de36460 --- /dev/null +++ b/DEVLOG.md @@ -0,0 +1,65 @@ +# 开发节点记录(三系统量化交易) + +> 这份文件的用途只有一个:**让下一次交接不必重新查一遍**。 +> 每个重要节点写一条,写完就补,不攒到最后。一条节点包含五样东西: +> 做了什么、动了哪些文件、部署方式、真机判收结果、还欠着什么。 +> +> 三个系统的部署方式(改错等于白改): +> - **tradingSystem(PMS)** 跑在 `factor@factorevaluation`,源码打进镜像, +> 改码必须 `make deploy`,跑完 `make test` 见 ALL SUITES PASS,盯盘 `make watch`。 +> 容器时钟是北京时间。 +> - **bionic_trader(决策系统)** 跑在 `192.168.16.188:38000`,挂载卷, +> `git pull` 后 `docker compose restart backend-api worker-brain` 即生效; +> **新增 env 必须 `docker compose up -d --force-recreate`**,restart 不重读 env。 +> 容器时钟是 UTC,看时间戳先加八小时。 +> - **akg-factor-bridge(因子桥)** 在 `factorevaluation`,容器名 `akg_factor_bridge`, +> API 在 8300,常驻 API 改码要 compose restart。容器时钟是 UTC。 +> +> 记录纪律:判收状态只写「真机见过」或「未判收」两种,不写「应该没问题」。 +> 时间一律写北京时间,引用桥或决策系统的日志时间时先加八小时并标注已换算。 + +--- + +## 2026-08-06 · 新建仓动作:方案已出,待拍板 + +**做了什么** +读完 PMS 的动作引擎主链代码,出了《给动作引擎加「新建仓」动作》的方案,等待拍板。 +一行代码都还没改。 + +**读代码得到的、与既有文档不一致的事实(以代码为准)** + +1. `OPEN` 这个动作名在 PMS 里**已经存在**,而且从方案到下单的整条管道是通的: + `planner.py:31` 定义 `A_OPEN`、`executor.py:41` 的 `BUY_ACTIONS` 已含 `OPEN`、 + `command_service.py:647` 已经在用 `action="OPEN"` 写评审账本。 + 缺的只有自主提议这一段:`action_engine.py:27` 的动作词表没有 OPEN, + `action_engine.scan()` 的输入是持仓列表,结构上不可能引入新标的。 +2. `caps_ctx(view, ts_code=...)` 对**从未持仓过的票**不会带出行业名, + 不显式传 `sector` 的话,行业集中度那道硬拦截会整段静默跳过。 +3. 从未持仓过的票在 `pms_position` 里没有行,`update_position` 会影响 0 行。 + 建行要显式调 `ensure_position`,它只写 `ts_code / status='PLANNED' / updated_at`。 +4. `command_spec.py` 里那个 `"planner": "..."` 字段**全代码库没有任何一处读它**, + 真正的分派是 `command_service._dispatch_planner` 里一串硬编码的 `if cmd_type ==`。 +5. 参考位取数(`ledger_service` 盘前那一跳)只对 `only_open=True` 的持仓取, + 候选票没有支撑压力。这对择时实现 A 无影响(它用决策系统自己库里的结论), + 但实现 B 的「回踩带」判据对新票是空的。 + +**动了哪些文件** +无。方案文件 `NEW_POSITION_ACTION_PLAN.md` 已放进 tradingSystem 项目目录。 + +**部署方式** +不涉及。 + +**真机判收** +未判收(尚未动码)。 + +**还欠着什么** +方案第十节的四件待拍板:新建仓的档位怎么给、每天最多新开几只、研判闸这一轮做不做、 +盘中参考位漂移要不要加防护。拍板之后才动手。 + +--- + + diff --git a/HANDOFF_2026-08-06.md b/HANDOFF_2026-08-06.md new file mode 100644 index 0000000..3ed017e --- /dev/null +++ b/HANDOFF_2026-08-06.md @@ -0,0 +1,150 @@ +# 交接:三系统量化交易 · 2026-08-06 收盘前 + +> 这份东西是写给下一个对话开头的。可以整段贴过去,也可以留在仓库里当阶段存档。 + +我们在推进三个相互衔接的量化交易系统。请先读项目记忆里的 **tradingsystem-bionic-bridge**、 +**clock-and-timezones**、**tradingsystem-silent-failure** 三篇(都在 08-06 更新过), +背景需要时再读 tradingsystem-ws-channel 和 quant-factor-synthesis,工作方式看 +feedback-plan-first 和 writing-style-readability。读完再看下文,然后等我给任务,不要自行开工。 + +--- + +## 一、三系统与部署方式(改错部署方式等于白改) + +**tradingSystem(PMS,持仓管理)** 跑在 `factor@factorevaluation`。源码是打进镜像的, +所以改码必须 `make deploy`,跑完再 `make test` 看到 ALL SUITES PASS;盯盘用 `make watch`。 +**容器时钟是北京时间。** + +**bionic_trader(决策系统)** 跑在 `192.168.16.188:38000`(tlaipro 机)。挂载卷, +`git pull` 之后 `docker compose restart backend-api worker-brain` 即可生效; +但**新增 env 必须 `docker compose up -d --force-recreate`,`restart` 不重读 env**。 +**容器时钟是 UTC。** + +**akg-factor-bridge(因子桥)** 在桥机 `factorevaluation`,容器名 `akg_factor_bridge`, +API 在 8300。常驻 API 改码要 compose restart。**容器时钟是 UTC。** + +线上运行中的系统只做加法。 + +## 二、规矩(不变) + +先出方案再动手;中文可读性第一,写完整自然句、不自造缩写;只做加法;宁可空池; +关键路径禁止丢弃返回值;全程 Docker,不给宿主机直跑 pip/python/psql;每条命令必须标注 +运行在哪台机器,一块代码里只放一台机器;判收的标准是真机各见一次;写回 Mac 的文件以 +written 回执加 md5 为准;**看桥和 bionic 的任何时间戳,先加八小时再和对话里的时间对齐。** + +测试脚本由我在实机上运行,跑完把结果发给你,你不用跑复杂测试。里程碑的代码和文档 +请提示我及时创建或上传到项目目录。 + +## 三、下一个任务:给动作引擎加「新建仓」动作 + +**要解决的问题**:不能每天靠人手工走一遍下命令、排方案那一串。正常应该是上游选股计划 +出现之后,PMS 按规则自己执行。 + +**已经查清的现状,不用重查**:整条链除了「下命令」这一步,其余全是自动的。调度表里 +08:40 拉候选池,08:50 盘前准备,**每分钟**跑命令轮询(把已下达的命令排成方案)、盘中执行 +(方案转指令 + 自主提议扫描 + 择时出手)、成交入账加轻对账、消化决策系统的盘中信号, +14:50 T 仓平回,15:10 日终结算加窗口收口,15:30 日报。`make t-*` 只是这些调度位的手动 +等价物,是排查用的,不是日常流程。 + +**唯一要人的就是下命令,而且是刻意的**:计划回答「能买哪些」,命令回答「今天打算投多少」。 + +**三条路,性质完全不同**: + +一是把下命令挂成定时,每天自动下一条升仓命令。改动最小,加一个调度位;代价是仓位单向 +增长到撞上组合上限,而且不看市场状态。 + +二是把自主档位调到 full。**这条解决不了问题**——它只让已有持仓的增持类提议不用人确认, +而动作引擎产出的只有补足底仓、回踩补仓、盈利加仓、减持这四类,**根本没有「新建仓」 +这个动作**。 + +三是给动作引擎加新建仓动作,让自主提议除了管已有持仓,也能从候选池里挑新票,走同样的 +规则闸、研判闸、档位分流。这才对得上要解决的问题,是新逻辑不是新调度位。 + +**我选第三条。先出方案,我拍板之后再动手。** + +**开工前要知道的约束**:择时读的那张 `strategy_daily_results` 表,在盘中跑 push-pool +触发补扫时会被就地改写(详见第六节)。开发期靠「盘中不跑 push-pool」这条纪律规避, +没有改代码。设计新建仓动作时要把这件事放进考虑,别让无人值守的建仓建在会漂移的输入上。 + +## 四、当前系统状态(2026-08-06 盘中,约 11:15) + +账户总资产 2,000,288.86,可用 1,881,764.86,组合仓位 5.92%。参数:总规模 200 万, +自主档位 propose_only,下发模式 ws,择时档位 A,组合上限 70%,单股上限 8%,最多 20 只, +执行窗口默认 3 个交易日。 + +**持仓三只**:600150.SH 一千七百股、可卖一千二、摊薄成本 34.625、安全垫 +0.16%,参考位 +支撑 34 压力 37 止损 34(来源 bionic);002335.SZ 一千九百股、可卖零、摊薄成本 30.984、 +安全垫 +0.41%;000035.SZ 一百股、可卖零、摊薄成本 5.028、安全垫 -1.55%,**决策系统对它 +的定性是 SELL**。 + +**在途四条**:000035 的减持一百股(来源自主提议,11:13 我在页面人工放行);002335 的清仓 +一千九百股(来源决策系统风控信号,置信度 95%);002518 的买入九百股(**被新装的定性闸 +拦着**,窗口今天到期);000063 的买入一千七百股(现价高于买入区间上沿,不追,窗口今天到期)。 +前两条都要等明天 T+1 才能出手。 + +## 五、08-05 到 08-06 做完的事 + +**买入侧补上了一道闸,两侧都已部署,真机判收成立。** 起因是发现候选层的结论对执行层 +完全不可见:桥用 `strategy_daily_results.signal_type` 落在 SELL、AVOID、DROPPED 就把票请出 +候选池,而决策系统的择时应答读同一个字段、放进 `observed.y_signal` 原样回传却从不用它判断, +PMS 又只解析 verdict、limit_price、reason。同一条结论一边当判决一边当摆设。 +600841 就差一根 3.3% 的阴线会被买进来。 + +改动:决策系统 `app/services/pms_advisor.py` 加 `BAD_Y_SIGNALS` 常量和买入分支的闸, +`config/settings.py` 加开关 `PMS_EXEC_BLOCK_BAD_Y_SIGNAL`;PMS 侧 `app/core/rule_gate.py` +加 `BAD_SIGNAL` 检查项并把定性无条件写进 hard_numbers,`app/services/exec_advisor.py` +把 `observed.y_signal` 存进 advice 并挂到决策上,`app/services/executor.py` 传进规则闸 flags, +`scripts/test_batch3_units.py` 加一条用例。**08-06 真机拦下第一笔:002518 停在 PROPOSED、 +成交 0、理由是新写的那句。** PMS 那道 `BAD_SIGNAL` 至今没真的触发过(决策系统先回 WAIT +就走不到规则闸),符合当初「第二道闸平时空转、只为留痕」的预期。 + +其余:600841 的在途买入指令已撤(回执是 `cancelled:0` 加「仅本地置撤销」,PROPOSED 从没 +下发过子单时走的就是这个分支);freeze 的 manifest.json 判收通过,08-03 和 08-04 两天 +都在;XXL 例行调度的根因查实——从来没有自动拉起过,日志里只有手动触发的行,配置已重新设计。 + +## 六、悬而未决(按优先级) + +1. **主线**:动作引擎加新建仓动作,见第三节。 +2. **昨夜结论会被盘中补扫就地改写**。push-pool 跑完触发增量补扫,补扫用 `trade_date=昨日` + 就地覆盖已有那行,而 `fetch_yesterday_strategy` 是取最新一行、不按日期过滤。实证: + 000035 在 09:53 成交时定性是 WATCH、压力 5.2,补扫之后变 SELL、压力 5.15;002335 的支撑 + 从 31.27 变成 29.00,差 7.3%。**开发期靠「盘中不跑 push-pool」规避,代码未改。** + 真要修,最小加法是给 `fetch_yesterday_strategy` 加「只取今天之前」的过滤,但那样盘中 + 补扫的产出就完全用不上了,是不是想要的要先拍板。 +3. **卖出侧的镜像缺口**。桥对「持仓且形态恶化」只警示、明确不出池,而 PMS 没有任何机制 + 会因为决策系统的定性去减仓——研判闸只管买入侧的补足、加仓、补仓,规则闸的冻结和黑名单 + 只挡增持。买入侧补上了,卖出侧还是空的。000035 现在就是这个状态。 +4. **总资产与持仓市值的口径**。总资产来自 QMT 的 ws 资金快照,被组合刹车当权益基准用 + (高水位与回撤,15:10 跑);持仓市值是 PMS 拿 Redis 分钟线自算的。08-05 两边差 564 元, + 08-06 盘中只差 51 元。倾向是取数时点差不是口径差,收盘后价格静止时再对一次才能定案。 +5. **时区显示错位八小时**。`_today_exit_verdict` 把 created_at 直接截成「01:35」拼进理由, + 传到 PMS 页面和北京时间戳并排显示。给 bionic 容器设 `TZ=Asia/Shanghai`(新增 env, + 必须 force-recreate)或者格式化时显式转。低优先。 +6. **600150 最后五百股买在区间上沿**。区间连续四个交易日都是 [33.66, 34.9] 没变过, + 今天现价正好等于上沿,闭区间判定放行,成交在 34.89,摊薄成本被从 34.514 抬到 34.625、 + 安全垫从峰值 +1.99% 压到 +0.01%。这是分批建仓遇上区间长期不变的固有副作用。 + 要不要改(现价等于上沿时不买,或者区间连续 N 天不变时降档)是策略取舍,未拍板。 +7. **XXL 例行调度已重新配置**,验证点是看桥的 `data/xxl_build.log` 有没有在北京时间 07:10 + 自动长出新行(日志是 UTC,会写成前一天的 23:10)。 +8. `trade_no` 格式核验(关系 S3 收尾函第一节的阻断项): + `docker compose run --rm --no-deps pms-web python scripts/ws_smoke.py inbox --type trade --width 600`。 + 是 `SHADOW-xxxx#1` 这种确定式就闭环,还是 T-SHADOW 随机串就继续追。这条一直没做。 +9. 命令目录里 `FREEZE_STOCK` 的说明写着「在途买入撤销」,与实现不符(它是参数类命令、 + 没有 planner,只让规则闸的冻结项拦),文案要补账。 +10. 文档补账:`WS_INTEGRATION_STATUS.md` 停在 07-30;`BIONIC_PMS_INTERFACE.md` 第三节要 + 加一句新闸;README 待办 9 和 10 标判收。 +11. 老悬项不变:分歧票候选层拦截未拍板;AKG_PLAN 并入 TECH_POOL 的共管规矩未拍板; + 07-29 那 19 条 SIG_INVALID 未查明。 + +## 七、这轮我犯过的三个误判,别重犯 + +**一是看见巧合就当线索。** 600150 的现价 34.900 恰好等于两笔委托的限价,我据此断定取价 +有问题,还推出「安全垫其实是负的」。实际行情源是活的、分钟线连续,34.900 就是当时的 +真实价。**先验证数据源是否在更新,再怀疑取值。** + +**二是没换算时区。** 审计行写着 01:35,我当成北京时间,于是把一个正常的四分钟赛跑 +(09:31 咨询干净、09:32 成交、09:35:51 结论落库、09:36 信号到达)判成了「老闸漏读」的 bug, +差点去改代码。**桥和 bionic 的时间戳一律先加八小时。** + +**三是推理没落地就当结论。** 我推定 `audit_date` 会因为容器在 UTC 而错位一天,实际写入侧 +用的是北京日期,洞根本不存在。**时间相关的猜测必须先查实际数据再下判断。** diff --git a/NEW_POSITION_ACTION_PLAN.md b/NEW_POSITION_ACTION_PLAN.md new file mode 100644 index 0000000..f26208c --- /dev/null +++ b/NEW_POSITION_ACTION_PLAN.md @@ -0,0 +1,260 @@ +# 方案:给动作引擎加「新建仓」动作 + +> 状态:**待拍板**,2026-08-06 收盘后出。全部依据来自实际代码,不引用设计文档的结论。 +> 涉及系统:tradingSystem(PMS,主要改动)、bionic_trader(决策系统,可选的第二步)。 +> 部署方式:PMS 源码打进镜像,改码必须 `make deploy`,跑完 `make test` 见到 ALL SUITES PASS。 + +--- + +## 一、先说清现状:管道其实已经铺好了,缺的只有一段 + +读代码之后,一件事和交接信里的判断略有出入,而且是好消息:**`OPEN` 这个动作名在 +PMS 里已经存在,而且从方案一路到下单的整条管道都是通的**,缺的只是「自主提议」这一段。 + +已经存在的部分: + +- `app/core/planner.py` 第 31 行 `A_OPEN, A_FILL, A_ADD, A_DCA = "OPEN", "FILL", "ADD", "DCA"`, + 建仓命令拆出来的底仓批产出的就是 `OPEN`。 +- `app/services/executor.py` 第 41 行 `BUY_ACTIONS = {"OPEN", "FILL", "ADD", "DCA"}`, + 动作是 `OPEN` 的指令已经能自动转成买入方向、走择时、过规则闸、下发。 +- `app/services/command_service.py` 第 647 行在候选被规划期拦下时,已经在往评审账本里 + 写 `action="OPEN"` 的记录。 +- 择时接口 `exec_advisor._advice` 把 `action` 原样透传给决策系统,动作名叫什么它都不挑。 + +缺的部分,只有两处: + +- `app/core/action_engine.py` 第 27 行的自主动作词表 `A_FILL, A_ADD, A_DCA, A_TRIM` + 里没有 `OPEN`,第 182 行的 `EVALUATORS` 也没有对应的求值器。 +- `action_engine.scan()` 第 197 行是 `for p in positions or []`,输入是 + `portfolio.positions_view()["held"]` —— **只遍历已有持仓,结构上就不可能引入新标的**。 + +所以这次要做的事可以说得很准确:**给动作引擎补一个以候选池为输入的求值器,让它产出 +`OPEN` 候选,然后并进现有的那条「规则闸 → 研判闸 → 档位分流」的路。** 下游一行都不用动。 + +--- + +## 二、选票规则:动作引擎只管名额、钱、批次,不管时机 + +候选池那边已经过完一整套筛选(`plan_feed.select_candidates`):按分数降序、档位白名单、 +分数与来源数与预期空间的下限、同主题限额、剔除 ST、剔除已持仓、剔除黑名单。 +排在它后面的规则闸又会管:一手整百、冻结、全局暂停买入、黑名单、决策系统昨夜定性 +`BAD_SIGNAL`、涨停不追、当日涨幅不追高、距 MA5 不追高、组合与单股与持仓数与行业集中度 +的上限、预留现金、真实可用资金。再后面的择时又会管:现价在不在买入区间内。 + +`action_engine.py` 第 17 行的注释已经把职责边界钉死了 ——「上限/一手/冻结等硬约束**不在 +这里重复判**,统一由规则闸终检(职责单一,口径唯一)」。所以新建仓求值器只回答三个问题: + +1. **还能开几只**:`min(最大持仓数 − 当前持仓数, 每日新开上限 − 今日已开)`。 +2. **还有多少钱可投**:`总仓上限 × 总规模 − 当前组合市值`,与单只的目标金额取小。 +3. **这一批买多少股**:按 `PMS_BATCH_SPLIT` 拆批,**只提底仓那一批**(默认 50%), + 数量 `lot_qty(金额, 现价)`,不足一手就不提。 + +**只提底仓批**这一条是刻意的:后续的回踩补足与盈利加仓,等持仓真的建立起来之后,由已有的 +`eval_fill` 与 `eval_add` 自然接管——它们本来就是干这个的。这样不需要命令驱动那套 +`GATED` 方案的解锁机制,新建仓这条路上一条 GATED 都不产生。 + +**入场时机不在这里判。** 现价合不合适是择时的活;定性好不好是规则闸 `BAD_SIGNAL` 的活。 +动作引擎在这里另加一套判断,就违背了「提前计算为主、盘中监控为辅」,而且会和已有的闸重复。 + +**价格必须是实时价。** `market.plan_price` 在拿不到实时价时会回落昨收,它自己的注释写着 +「拿昨收当现价去做不追高这类判断会出错,只给规划期定量用」。自主建仓是要真下单的, +所以走 `market.get_price`,取不到就整只跳过并写进 `skipped`——「什么都没发生」和 +「明着跳过了」在页面上必须是两回事,这是 `scan()` 入口处已经立过的规矩。 + +--- + +## 三、要动的文件与改法(PMS 侧,全部是加法) + +### 1. `app/core/action_engine.py` + +- 加常量 `A_OPEN = "OPEN"`,把它加进 `JUDGE_ACTIONS`。 +- 新增 `eval_open(c, params, mkt)`:输入是一条候选(代码、现价、分数、行业), + 输出与现有四个求值器同构的候选结构。 +- 新增 `scan_open(*, candidates, params, market, room, slots, skip)`:遍历候选, + 名额与金额是全局的,所以边遍历边扣减;每一条不产出候选都要写进 `skipped` 并说明原因。 +- **现有的 `scan()` 一个字不动**,既有单测继续全绿。 + +### 2. `app/services/proposal_service.py` + +- `scan_and_route` 在 `ae.scan(...)` 之后追加一段:取候选池、取名额与可投金额、 + 调 `ae.scan_open(...)`,把两批候选拼起来走同一个 `_route_one`。 +- 候选池取数失败(`PlanFeedError`)时**不产生任何新建仓候选,并把原因写进 `skipped`**。 + 已有持仓的四类动作不受影响,照常扫描——一条外部接口的故障不该让整轮扫描停摆。 +- `_route_one` 里给 `OPEN` 三处特判: + - 现价取候选自带的,不能取 `_pos_of` 的(没持仓的票那里没有 `price`,会是 0, + 规则闸直接 `PRICE_MISSING` 拒掉)。 + - 组合上下文改成 `portfolio.caps_ctx(view, ts_code=code, is_new_name=True, + sector=industry.get(code))`。**必须显式传行业**:`caps_ctx` 只在 + `view["positions"]` 里找得到该票时才会带出行业,新票找不到,不传的话行业集中度 + 整段静默跳过(`sizer.check_caps` 对 `sector` 为空是直接跳过的)。 + - 落指令时先 `pms_repo.ensure_position(code)` 建一行 `status='PLANNED'` 的持仓行, + 再写上 `target_pct`。不建这行的话 `bump_once_guards` 那类更新会影响 0 行。 + 建了也不会污染上限计算——`positions_view` 的 `held` 过滤 `total_qty > 0`。 +- 档位分流:见第四节。 + +### 3. `app/services/exec_advisor.py` 与 `app/services/executor.py`(漂移防护) + +见第五节。 + +### 4. `app/repo/pms_repo.py` + +- 新增一个只读函数,统计今日已经开过或正在提议中的新仓票数(`pms_instruction` 与 + `pms_proposal` 里今天 `action='OPEN'` 的 distinct 股票数)。每日节流要用。 + +### 5. `app/services/param_store.py` 与 `config/settings.py` + +新增参数(见第六节),并同步补 `DESC` 中文说明与 `_RANGES` 取值校验。 +`param_store.py` 第 46 到 48 行有一条血的教训写在那里:加参数表时不同步加白名单, +`set_param` 只回 `ok=False` 不抛异常,调用方又把返回值丢了,于是那条纪律静默失效。 +这次一并照做。 + +### 6. `scripts/test_batch3_units.py`(或新增一个测试文件) + +至少覆盖:名额已满、组合钱不够、买不足一手、取不到实时价、每日节流已用完、 +候选池取数失败、参考位漂移超阈值。**这些用例都是纯逻辑,不连库。** + +--- + +## 四、档位分流:这是要你拍板的第一件事 + +现有分流在 `proposal_service._route_one` 第 132 行: + +``` +auto_exec = (side == "sell") or (autonomy == AUTONOMY_FULL and not force_queue) +``` + +新建仓是买入方向,所以在当前的 `propose_only` 档位下,它必然落进待确认队列。 +这意味着人力从「下命令 + 排方案 + 核方案」降到「点一下确认」,但仍然不是无人值守。 + +三条路: + +- **甲:不动档位。** 新建仓入队待确认。改动最小,但没达到「不要每天靠人」的目标。 +- **乙:把 `PMS_AUTONOMY` 调成 `full`。** 新建仓自动了,但补足底仓、盈利加仓、 + 浅档补仓也一起自动了——那是三个不同的决定,不该被一个开关捆在一起。 +- **丙(我的建议):给新建仓单独一个档位参数 `PMS_OPEN_AUTONOMY`。** + 取值 `inherit`(跟随全局,默认)/ `full`(自动执行)/ `propose_only`(待确认)。 + 在 `auto_exec` 的计算里加一个针对 `OPEN` 的分支,不动既有那行的语义。 + 这样可以做到「新建仓自动、其余仍待人确认」,也可以随时单独关掉新建仓而不影响别的。 + 深档补仓的强制确认(`needs_user_confirm`)与研判不可用时的降级入队仍然无条件优先。 + +--- + +## 五、盘中输入漂移:这是要你拍板的第二件事 + +你点名的约束:择时读的 `strategy_daily_results` 在盘中跑 push-pool 触发补扫时会被就地改写。 +实证是 000035 的压力从 5.2 变 5.15、002335 的支撑从 31.27 变 29.00(差 7.3%)。 + +对新建仓的具体危害比对已有持仓大得多。已有持仓有摊薄成本和安全垫做锚,支撑压力漂一点, +补仓补足的判断不会翻转;**而新建仓的买入区间完全由支撑压力推出来**,支撑变 7.3%, +区间整体平移,一只上午判为「现价高于上沿、不追」的票,下午可能变成「在区间内、出手」—— +而这时候没有人在看。 + +两条路: + +- **甲:这一轮不做防护,靠「盘中不跑 push-pool」的纪律。** + 改动最小,但把一条口头纪律放进了无人值守的关键路径。哪天忘了,或者哪天 XXL 例行调度 + 真的自动跑起来了,没有任何东西会拦。 + +- **乙(我的建议):首答锁定 + 偏离即停,只作用于新建仓。** + 一条 `OPEN` 指令当天第一次拿到择时应答时,把应答 `observed` 里的支撑、压力、买入区间 + 存进 `progress.ref_lock`。之后每次应答都跟它比,偏离超过 `PMS_OPEN_REF_DRIFT_MAX` + (默认 3%)就把本轮判定改成等待,理由写明「昨夜结论盘中被改写:支撑 31.27 → 29.00, + 偏离 7.3%,该票新建仓当日暂停」,并在评审账本落一条 `WARN`。 + + 三个好处:纯加法、只在 PMS 侧、不需要动桥和决策系统;不需要先拍板悬项二那个 + 「要不要给 `fetch_yesterday_strategy` 加日期过滤」的取舍;而且**它顺带把漂移变成了 + 可统计的数据**——漂了几次、每次漂多少,都会留在账本里,将来真要改上游时有实证依据。 + + 代价是 `exec_advisor` 与 `executor` 各要动几行,而这两个文件是买入侧新闸刚改过的地方。 + +--- + +## 六、新增参数(全部有默认值,页面可调,默认值等于「不改变现有行为」) + +| 键 | 默认 | 说明 | +|---|---|---| +| `PMS_OPEN_ENABLED` | `False` | 自主新建仓总开关。**默认关**,部署后行为与今天一字不差 | +| `PMS_OPEN_AUTONOMY` | `inherit` | 新建仓单独档位:inherit 跟随全局 / full 自动执行 / propose_only 待确认 | +| `PMS_OPEN_MAX_PER_DAY` | `1` | 每个交易日最多自动新开几只 | +| `PMS_OPEN_MAX_NAMES` | `0` | 自主新建仓能占的持仓数上限,0 = 不额外限制(仍受 `PMS_MAX_NAMES` 约束) | +| `PMS_OPEN_TARGET_PCT` | `0` | 自主新建仓的单股目标仓位,0 = 用 `PMS_STOCK_TARGET_DEFAULT` | +| `PMS_OPEN_MIN_SCORE` | `0` | 在候选池已有过滤之上,自主建仓额外的分数下限,0 = 不额外过滤 | +| `PMS_OPEN_REF_DRIFT_MAX` | `0.03` | 参考位盘中改写的容忍幅度,超过则该票当日暂停新建仓(选了第五节的乙才需要) | + +`PMS_OPEN_ENABLED` 建议同时加进 `param_store.FAIL_CLOSED`,取值 `False`——参数表读不到时 +安全方向是不建仓。 + +**每日只开一只**这个默认值需要你确认。理由:候选池 top 30、持仓上限 20 只,如果不限, +第一天就会把组合一次性建满,所有仓位同一天建立、同涨同跌,而且撞上的还是交接信里 +「仓位单向增长」的另一个版本。一天一只,二十个交易日建满,节奏上更像人在做。 + +--- + +## 七、研判闸:这是要你拍板的第三件事 + +`judge.request` 的第一行是:动作不在 `PMS_JUDGE_ACTIONS`(页面可调,默认 +`FILL,ADD,DCA,SWITCH`)里就直接回 `PASS`。而决策系统那侧按动作类型给定性判据的那张表, +只有 FILL、ADD、DCA、SWITCH 四行,没有 `OPEN`。契约写明「越界与解析失败一律回 +`UNAVAILABLE`」,而 PMS 收到 `UNAVAILABLE` 会 `degraded=True` → 强制入队待人确认。 + +**也就是说,如果现在就把 OPEN 送研判,新建仓会 100% 落回人工确认——正好把这次要解决的 +问题原样退回去。** + +两条路: + +- **甲(我的建议):这一轮 PMS 侧把口子留好,实际先不送研判。** + `action_engine.JUDGE_ACTIONS` 加上 `A_OPEN`(让它有资格),但页面参数 + `PMS_JUDGE_ACTIONS` 不加 `OPEN`(实际不送)。下一轮决策系统侧补上 OPEN 的判据、 + 联调通过之后,在页面参数里加一个词就生效,**零部署、随时可退**。 + 这一轮新建仓只过规则闸——规则闸已经有 `BAD_SIGNAL` 那道定性闸,加上候选池本身就是 + 决策系统每晚推理覆盖过的池子,不是裸奔。 + +- **乙:这一轮双侧一起改。** 决策系统 `workers/tasks_intraday.py` 的 `PMS_JUDGE` 分支加 + OPEN 的判据(核心问题大致是「现在从零建这只票,逻辑成立吗」), + `app/services/pms_advisor.py` 与判据表同步。跨机改动,部署方式是挂载卷 `git pull` 后 + `docker compose restart backend-api worker-brain`;如果加了新的 env 就必须 + `docker compose up -d --force-recreate`。 + +--- + +## 八、两个已知的交互,先摆出来 + +1. **命令驱动的 OPEN 拒绝记录会挡住自主 OPEN。** + `command_service._ledger_rejects` 写的是 `action="OPEN"`、`arbiter="rule"`、 + `verdict="REJECT"`,而 `proposal_service._rejected_today_keys` 读的正是这张表的 + `(ts_code, action)`。所以某只票今天因为一条升仓命令在规划期被上限拦过, + 自主新建仓当天就不会再提它。**多数情况下这是对的**(同一组约束),但两边算的目标金额 + 不一样,一手不足这类拒绝可能口径不同。先记下来,观察一天再看要不要分开。 + +2. **升仓命令的候选没有排除已有在途方案的票。** + `command_service._dispatch_planner` 算出了 `exclude = _codes_with_live_plans()`, + 但只传给了降仓分支,升仓分支没传。这是既有的现象,不属于本次改动范围, + 但自主新建仓上线后两条路会同时挑票,撞车概率上升。 + 自主这条路有 `_inflight_keys` 兜底(同票同动作有在途提议或指令就不重复提), + 命令那条路没有。**建议这次不动它,先观察**。 + +--- + +## 九、分步上线与判收 + +判收纪律照旧:代码就绪不等于接通,接通不等于判收,标准是真机各见一次。 + +| 步 | 动作 | 判收标准 | +|---|---|---| +| 1 | `make deploy` + `make test`,`PMS_OPEN_ENABLED=False` | ALL SUITES PASS,且盘中行为与今天一字不差(`make watch` 看不出变化) | +| 2 | 页面开 `PMS_OPEN_ENABLED=True`,档位 `propose_only`,每日 1 只 | 提议队列出现一条 OPEN,理由读得懂,硬数字里有名额、可投金额、批次与分数 | +| 3 | 人为把 `PMS_MAX_NAMES` 调到当前持仓数 | 评审账本出现 `MAX_NAMES` 的 OPEN 拒绝行(`make t-gate` 看得到) | +| 4 | 档位改 `full` | 一条 OPEN 提议直接落指令,`make t-ins` 看到 `origin_type=proposal`、`action=OPEN`、`is_command=false` | +| 5 | 盘中观察那条指令 | 现价在买入区间内时 FIRE,`last_decision.source=A`,限价等于区间上沿 | +| 6 | 成交之后 | `pms_position` 出现该票、`opened_date` 有值、`status=HOLDING`;次日起 `eval_fill` 能看见它 | +| 7 | 漂移防护(若选了第五节的乙) | **建议只靠单测判收,不在生产人为制造**:盘中跑 push-pool 触发补扫风险太大。生产侧只观察账本里有没有 `REF_DRIFT` 的 WARN 行 | + +--- + +## 十、需要你拍板的四件事(其余我按上面的建议做) + +1. **档位**:给新建仓单独一个 `PMS_OPEN_AUTONOMY`(建议),还是直接用全局 `PMS_AUTONOMY`? +2. **每日节流**:一天最多新开几只?(建议 1) +3. **研判闸**:这一轮 PMS 侧留口子、决策系统侧下一轮再做(建议),还是这一轮双侧一起改? +4. **漂移防护**:做首答锁定加偏离即停(建议),还是这一轮先不做、继续靠「盘中不跑 + push-pool」的纪律?