tradingSystem/NEW_POSITION_ACTION_PLAN.md

388 lines
24 KiB
Markdown
Raw Normal View History

2026-08-06 13:05:40 +08:00
# 方案(定稿):给动作引擎加「新建仓」动作
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
> V32026-08-06。四条取舍已拍板决策系统侧已评估按「不影响原有业务系统」这条原则
> 重新过了一遍,改掉与补上的地方见第三节。**待你点头即可动手。**
2026-08-06 13:05:40 +08:00
> 全部依据来自实际代码,不引用设计文档的结论。
> 部署PMS 源码打进镜像,改码必须 `make deploy`,跑完 `make test` 见 ALL SUITES PASS
> 决策系统挂载卷,`git pull` 后 `docker compose restart backend-api worker-brain` 即可。
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:55:27 +08:00
## 〇、拍板结果
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
| # | 问题 | 决定 |
|---|---|---|
2026-08-06 13:55:27 +08:00
| 1 | 档位 | **单独一个开关,默认开启**。新建仓不跟随全局档位,自己一个参数,默认自动执行 |
| 2 | 每日节流 | **不做**。逻辑进逻辑出,由既有的上限、资金、候选池与择时自己收敛 |
2026-08-06 13:05:40 +08:00
| 3 | 研判闸 | **这一轮一并加上**,双侧改到位 |
| 4 | 漂移防护 | **做**,首答锁定加偏离即停 |
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
---
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
## 一、动别的系统之前先问的三件事(新立的规矩)
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
决策系统本来就不管仓位——它是一套聚合了大量信号的判断系统,仓位是 PMS 的活。
往它里面塞仓位信息,最容易出的事不是这次的功能不好用,而是**把别的、已经在跑的逻辑
带偏**。所以凡是改 tradingSystem 以外的系统,动手前先答这三题,答不上就先别动:
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
1. **这段代码是不是只有我这条路径会走到?** 如果是共用的(提示词模板、裁决解析、
上下文组装、配置项),就不能在里面加只对我有意义的分支。
2. **我加进去的字段,会不会被别的逻辑读到并当真?** 尤其是写共享表、写共享缓存、
往共享 context 里塞键。
3. **我的东西全删掉,原系统能不能一字不差地回到今天?** 答不上「能」,就说明它不是加法。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
按这三题重新核过这次的决策系统改动,见下一节。
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:55:27 +08:00
## 二、决策系统侧:三处改动全部落在 PMS_JUDGE 块内
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
### 结论先说
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
`workers/tasks_intraday.py``process_intraday_audit` 是一个按 `direction` 分流的大函数。
PMS 相关的代码本来就已经被圈在两段 `if direction == "PMS_JUDGE":` 里面
(第 439 到 523 行的入口与素材段,第 632 到 656 行的收口与出口段)。
**我要改的三处,全部在这两段之内,一行都不碰共用代码。**
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
| 改什么 | 在哪 | 是不是共用 |
|---|---|---|
| 加一条 OPEN 建仓判据 | 第 495 到 511 行的 `_task_by_action` 字典 | **不是**。这个字典整个定义在 PMS_JUDGE 块内,别的 direction 看不见它 |
| OPEN 且现价缺失时回不可用 | 第 449 到 456 行现价链路之后,块内早退 | **不是**。早退返回的结构与既有 UNAVAILABLE 出口同形 |
| 边界声明按动作分岔 | 第 513 到 514 行的 `core_task +=` | **不是**。`core_task` 只在 PMS_JUDGE 块内被赋值 |
不碰的东西,逐条点名:第 557 到 576 行那个所有 direction 共用的提示词模板不动;
第 579 到 590 行共用的裁决解析不动;第 530 到 555 行的资金分布与板块动量注入不动
PMS_JUDGE 本来就在它们的白名单里);`decision_ledger` 一个字不写
(每晚判分是全表扫描,写进去会污染既有功能,这条第一版就核实过);
`config/settings.py` 不加任何字段。
留痕仍然只写 `strategy_audit_log`verdict 列仍然只有 `PMS_PASS` / `PMS_REJECT` /
`PMS_UNAVAILABLE` 三个值——不新增第四种。而 `pms_advisor._today_exit_verdict`
读这张表时明确跳过 `PMS_` 开头的行(第 102 行),所以我们写进去的东西不会反过来
影响择时应答。
**「全删掉能不能回到今天」这一题的答案是能**:把 `_task_by_action` 里那一项删掉、
把两段早退和分岔删掉,代码就回到今天的样子,`git revert` 一次搞定。
### 送过去的东西也要干净:仓位数字不进研判请求
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
比改哪几行更要紧的是**别把仓位概念送过去**。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
`judge.request`PMS 侧 `app/services/judge.py` 第 65 到 72 行)现在是把
`candidate["hard_numbers"]` 整个塞进请求体,而决策系统会把它逐键渲染成
「【硬数字 (PMS 已算好, 勿重算)】」贴进提示词。如果新建仓的 `hard_numbers` 里装着
名额、可投金额、批次比例、组合占比这些东西,等于**当面请一个不该管仓位的系统去看仓位**——
哪怕提示词后面跟着一句「仓位纪律不归你管」,模型该被带偏还是会被带偏。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
所以加一条 PMS 侧的过滤:**新建仓送研判时,只送定性材料,不送仓位数字。**
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
- 送:现价、候选池给的分数、主题、档位、预期空间、热度、榜内名次。
这些回答的是「这只票凭什么被选出来」,正是要它裁的问题。
- 不送:名额、可投金额、批次数量与比例、组合占比、总规模。
这些回答的是「买多少」,是 PMS 自己的活。
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
实现上在 `judge.py` 里加一个显式的键白名单常量,只对 `OPEN` 生效,
并把「为什么」写在常量旁边。账本那边不受影响——`pms_action_ledger.hard_numbers_json`
照旧存全量,判分锚一个字不少。
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
### 必答清单放在决策系统那一侧
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
DCA 那条「下跌是杀逻辑还是杀情绪」现在两边都写了一份PMS 的 `must_answer` 传一次,
决策系统的判据里又写一次)。新建仓不照抄这个做法:**必答只写在决策系统的 OPEN 判据里,
PMS 侧的 `must_answer` 对 OPEN 留空。** 道理是分工——「建仓该问什么」属于仲裁哲学,
是决策系统的知识PMS 不需要懂。
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
### 部署方式
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
只改 worker 代码,不加配置、不动 `.env`。`config/settings.py` 第 208 到 226 行那段
自己写着「全部有默认值,不新增必填 env → 重启 backend-api + worker-brain 即生效」。
2026-08-06 13:05:40 +08:00
所以走常规路径:`git pull` 之后 `docker compose restart backend-api worker-brain`
2026-08-06 13:55:27 +08:00
**哪天真要往 `.env` 里写值(比如切研判独立队列),那才必须
2026-08-06 13:05:40 +08:00
`docker compose up -d --force-recreate`。**
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
---
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
## 三、重新过一遍之后改掉与补上的V2 → V3
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
### 改掉的两处
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
**1. 去掉 `PMS_OPEN_TARGET_PCT`,落指令时不再建持仓行。**
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
V2 说落指令前先 `ensure_position` 建一行再写 `target_pct`。重想之后这条不划算:
`target_pct` 只有在它与 `PMS_STOCK_TARGET_DEFAULT` 不同时才有意义,而那种差异化
本来就该由建仓命令去表达;为它一个参数,要引入建行、写列、以及**窗口耗尽作废后清理
僵尸空持仓行**三处新逻辑。
改成:自主新建仓一律用 `PMS_STOCK_TARGET_DEFAULT`,与后续的回踩补足、盈利加仓天然同口径,
落指令时**不建行**。持仓行由首次成交时 `ledger_service._apply_action` 里既有的
`ensure_position` 自然建出来——这条路径命令驱动建仓已经走通并判收过。
少一个参数、少三处逻辑、不产生僵尸行。
**2. 去掉 `PMS_OPEN_MIN_SCORE`。**
它和候选池已有的 `PMS_PLAN_MIN_SCORE` 是同一件事。两个旋钮管一件事,
将来一定会有一次调错地方。
### 补上的六处
**3. 候选之间要滚动扣减,否则一跳内几条加起来会超上限。**
这是个真问题。`_route_one` 里每条候选各自调 `caps_ctx(view, ...)`,而 `view`
本轮开始时取的快照,**不会因为前面几条已经落了指令而更新**。后果是一次心跳产出五条新建仓,
每条单独看都不超上限,五条加起来超了,而规则闸拦不住——它每次看到的都是同一个旧快照。
命令那条路没有这个问题,因为 `plan_increase_exposure``_ctx_after` 在循环里滚动更新。
自主这条路要照做:**滚动放在 `scan_open` 里**,边遍历边扣名额、扣可投金额、累加行业市值,
并直接复用 `planner.check_all_caps`(规则闸用的就是它,口径天然一致)。
`rule_gate` 已经在 `from app.core.planner import check_all_caps`core 层内部互相引用是既有模式。
**4. 候选数的上界是剩余名额,不是候选池大小——这修正了 V2 对研判负载的估计。**
因为名额和金额在 `scan_open` 里边走边扣,产出的候选数**天生不会超过剩余名额**。
组合已建满就一条都不产、一行账本都不写;还剩三个名额就最多三条。
冷启动那天是上界:名额 20 只,但组合上限 70% × 200 万 = 140 万,单只 6% = 12 万,
**金额先约束到 11 只左右**。11 只 × 最多 90 秒研判 ≈ 990 秒,靠第五节的时间预算
摊到七八跳,也就是七八分钟做完。之后每天只会有个位数。
**5. 研判驳回要做当日去重,只对新建仓生效。**
`_rejected_today_keys` 只读 `arbiter='rule'` 的拒绝,**研判驳回不在里面**
`_inflight_keys` 只认在途提议与在途指令,被研判驳回的候选两样都不产生。
于是一条被研判驳回的新建仓候选会**每分钟重来一次**:决策系统那侧有半小时缓存,
所以不烧大模型,但 PMS 这边每分钟往 `pms_action_ledger` 写一行一模一样的驳回记录。
半小时三十行,几只票就是几百行——**这正是代码注释里记着的 2026-07-29 那个教训**
(十六只深亏票的补仓候选每分钟被拒一次,一天几千行,把判分锚淹了)。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
处理:新增一个只读函数 `judge_rejected_today`(读 `arbiter='judge'` 的当日驳回),
组装跳过集合时**只把其中动作为 `OPEN` 的键并进去**。既有四类动作的行为一个字不变——
它们「研判结论会变、有意不做当日去重」那条设计保持原样。
新建仓不一样:「这只票今天不该从零建仓」这个结论当天基本不会翻转。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
**6. 可投金额要用真实可用资金封顶。**
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
V2 只按仓位口径算可投金额(`总仓上限 × 总规模 组合市值`)。但 2026-07-30 那次教训
就是这么来的:总规模两百万而账户实际只有九十八万,方案一路排出来,规则闸一路放行,
要等下游回资金不足才被拒,而拒了不自动重发。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
所以 `scan_open` 的可投金额要在仓位口径与 `cash_avail` 之间取小,
并且新增 `PMS_OPEN_REQUIRE_WS_CASH`(默认开):**拿不到真实资金快照时不自动新建仓,
写进 skipped 留痕**。这不是节流,是和「必须实时价」同一类的输入质量闸——
建新仓是可以等的,而规则闸那条「拿不到资金只告警不拦」的降级口径,
是为已经排好的命令设计的,不适合无人值守地从零建仓。
**7. 行业名一次批量取,本轮复用。**
滚动扣减要算行业集中度,所以得先有行业名。`industry.get_many` 走
`gp_stock_category` 时是**逐只查库**(第 231 到 237 行),没有缓存。
所以在 `proposal_service` 里对候选代码**一次性取一遍**,塞进候选,
`scan_open``_route_one` 都用这一份,不重复查。
顺带提一条**建议但需要你单独拍板的**:给 `industry.get_many``gp_stock_category`
分支加按日缓存,与 `_hybk_many` 同一口径(行业归属本来就是日频的)。
收益是每跳少几十次库查询。但它改的是既有函数的时序行为,按第一节的规矩,
**这次不夹带,要做就单独一条**。
**8. 研判预算用尽的候选整条跳过,不入人工队列。**
预算用尽时,已经过了规则闸但还没送研判的候选,处理方式是**整条跳过并写 skipped**
下一跳重来。不把它们当成「研判不可用」入人工确认队列——那会在自动档位下凭空造出
一个人工队列,与这次要解决的问题正好相反。规则闸白跑一次的成本是毫秒级。
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:05:40 +08:00
## 四、选票规则:动作引擎只管名额、钱、批次
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
候选池那边已经筛过(分数降序、档位白名单、分数与来源数与预期空间下限、同主题限额、
剔除 ST、剔除已持仓、剔除黑名单。规则闸后面还会管一手整百、冻结、全局暂停买入、
2026-08-06 13:55:27 +08:00
黑名单、昨夜定性、涨停不追、当日涨幅不追高、距 MA5 不追高、组合与单股与持仓数与
行业集中度上限、预留现金、真实可用资金。择时再管现价在不在买入区间。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
`action_engine.py` 第 17 行的注释把边界钉死了——上限、一手、冻结不在动作引擎重复判。
所以新建仓求值器只回答三个问题,且**边算边扣**
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
1. **还能开几只**`最大持仓数 当前持仓数`,每产出一条减一。
2. **还有多少钱可投**`min(总仓上限 × 总规模 组合市值, 真实可用资金)`
每产出一条减去该条的实际金额。
2026-08-06 13:05:40 +08:00
3. **这一批买多少股**:按 `PMS_BATCH_SPLIT` 拆批,**只提底仓那一批**(默认 50%
`lot_qty(金额, 现价)`,不足一手就不提。
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
**只提底仓批**是刻意的:后续的回踩补足与盈利加仓,等持仓真建立起来之后由已有的
`eval_fill``eval_add` 自然接管。这条路上一条 `GATED` 方案都不产生。
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
**价格必须是实时价。** `market.plan_price` 拿不到实时价会回落昨收,
它自己的注释写着「拿昨收当现价去做不追高这类判断会出错,只给规划期定量用」。
2026-08-06 13:55:27 +08:00
自主建仓要真下单,所以走 `market.get_price`,取不到就整只跳过并写进 skipped。
**不节流的自然收敛点有两个**:一是名额与金额在求值器里边走边扣,候选数天生有界;
二是择时的买入区间——只有现价正好落在 `[支撑×0.99, 支撑+(压力−支撑)×0.3]` 里的票
才会真出手,高于上沿一律「不追,接受买不上」。每天实际建成几只,取决于当天有几只
落在自己的区间内。这正是「让系统自己判断」,第一周主要盯的就是这个数。
**一条已知的降级**新建仓在退实现B时`day.support` 是空的(新票没有参考位取数),
内置择时的「回踩带」判据用不上,只剩「现价不高于当日均价」一条,比已有持仓弱。
这是可接受的降级,但要在决策理由里写明白,别让人以为两条判据都过了。
---
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
## 五、研判的时间预算(不是节流)
`app/scheduler.py` 第 45 行给所有调度任务设了软超时 240 秒、硬超时 300 秒,
`judge.request` 是同步阻塞、单次超时上限 90 秒。**三只票送研判就是 270 秒,
已经顶破软超时。** 今天没出事,是因为送研判的只有三只持仓票且多数轮次被去重挡掉。
冷启动那天新建仓会有十来条,第一跳就会捅穿——任务被中途杀掉,
而且是在已经落了一部分表之后。
新增 `PMS_JUDGE_TICK_BUDGET_SEC`(默认 150 秒)。每次要调研判前先看这一轮花了多久,
如果「已花 + 单次超时上限」会顶破预算,本轮不再送后面的候选,写进 skipped
理由写「本轮研判时间预算用尽,下一跳继续」。
**这不是节流**:一条候选都没丢,没有任何按天计的上限,只是把一次心跳做不完的活
挪到下一分钟。候选按分数降序排,预算用尽时先做完的一定是分数最高的那些。
决策系统那侧的裁决缓存是按「日期 + 股票 + 动作」存半小时的,
所以同一批候选在半小时内的重复扫描都是秒回,真正慢的只有缓存过期后的第一跳。
决策系统那侧还预埋了研判独立队列(接口文档 §5.2)。现在不用开——
那是给 `brain_queue` 拥堵准备的,而我们的瓶颈是 PMS 自己的心跳时长,不是对端排队。
真出现频繁超时再开,而且开它要改 `.env`,必须 `force-recreate`
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:55:27 +08:00
## 六、要动的文件与改法
2026-08-06 13:05:40 +08:00
### PMS 侧(`factor@factorevaluation`,改完 `make deploy`
1. **`app/core/action_engine.py`**
2026-08-06 13:55:27 +08:00
`A_OPEN = "OPEN"`,加进 `JUDGE_ACTIONS`;新增 `eval_open()``scan_open()`
(含名额、金额、行业的滚动扣减,复用 `planner.check_all_caps`
**既有 `scan()` 一个字不动**,既有单测继续全绿。
2026-08-06 13:05:40 +08:00
2. **`app/services/proposal_service.py`**
2026-08-06 13:55:27 +08:00
- `scan_and_route` 追加:取候选池、批量取行业、算名额与可投金额、调 `scan_open`
2026-08-06 13:05:40 +08:00
两批候选合并走同一个 `_route_one`
2026-08-06 13:55:27 +08:00
- 候选池取数失败(`PlanFeedError`)时不产生新建仓候选,原因写进 skipped
已有持仓的四类动作照常扫描。
- 跳过集合并入 `judge_rejected_today` 里动作为 `OPEN` 的键。
- 研判时间预算:记本轮起始时刻,送研判前判预算,用尽则跳过并留痕。
- `_route_one``OPEN` 两处特判:现价取候选自带的(`_pos_of` 对新票没有 `price`
2026-08-06 13:05:40 +08:00
取它会是 0规则闸直接 `PRICE_MISSING`);组合上下文改成
2026-08-06 13:55:27 +08:00
`caps_ctx(view, ts_code=code, is_new_name=True, sector=<批量取到的行业>)`
——**必须显式传行业**,否则行业集中度那道硬拦截会静默跳过。
- 档位分流加 OPEN 分支(见第七节)。
3. **`app/services/judge.py`**
加 OPEN 专用的 `hard_numbers` 键白名单,只送定性材料;`must_answer` 对 OPEN 留空。
4. **`app/services/exec_advisor.py`**
`_advice` 把应答 `observed` 里的支撑、压力、买入区间存进缓存;当日首答锁进
`prog["ref_lock"]`;之后每答比对,偏离超过 `PMS_OPEN_REF_DRIFT_MAX` 就把本轮判定
改成等待,返回的决策里带 `ref_drift` 说明。**只对 `action == "OPEN"` 生效。**
5. **`app/services/executor.py`**
2026-08-06 13:05:40 +08:00
`run_tick` 的等待分支里,看到 `d.get("ref_drift")` 就额外落一条评审账本 `WARN`
2026-08-06 13:55:27 +08:00
放这里而不放 `exec_advisor`,是为了不给后者引入落表副作用。
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
6. **`app/repo/pms_repo.py`**
新增只读函数 `judge_rejected_today`;新增只读的当日新开票数统计(供页面显示,不做上限)。
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
7. **`app/services/param_store.py``config/settings.py`**
新增四个参数(第八节),同步补 `DESC` 中文说明、`_range_check` 或 `_RANGES` 校验、
以及 `FAIL_CLOSED`。`param_store.py` 第 46 行那条注释记着一次教训——
2026-08-06 13:05:40 +08:00
加参数不同步加白名单,`set_param` 只回 `ok=False` 不抛异常,调用方又把返回值丢了,
2026-08-06 13:55:27 +08:00
那条纪律就静默失效。这次一并照做。
另把 `PMS_JUDGE_ACTIONS` 默认值改成 `FILL,ADD,DCA,SWITCH,OPEN`
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
8. **测试脚本**(纯逻辑,不连库)
名额已满、组合钱不够、真实资金不足、拿不到资金快照、买不足一手、取不到实时价、
候选池取数失败、**多条候选的滚动扣减**、研判预算用尽、参考位漂移超阈值、
档位分流的三种取值、研判驳回当日去重。
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
### 决策系统侧(`192.168.16.188``git pull` + `restart backend-api worker-brain`
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
9. **`workers/tasks_intraday.py`**`_task_by_action` 加 OPEN 判据(含必答);
OPEN 且现价缺失时块内早退回 `UNAVAILABLE`;边界声明按动作分岔。
**三处全在 `if direction == "PMS_JUDGE":` 块内,不动共用代码,不动 `.env`。**
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
---
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
## 七、档位:一个开关,默认开启
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
新增 `PMS_OPEN_AUTONOMY`,取值 `full` / `propose_only` / `off`**默认 `full`**。
2026-08-06 13:55:27 +08:00
它同时充当总开关,不再单设第二个:`off` 就是不扫描新建仓。
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
`_route_one` 第 132 行那行不动,在它之外给 OPEN 加一个分支:
2026-08-06 13:55:27 +08:00
`full` 过完两道闸直接落指令;`propose_only` 落待确认队列;`off` 根本不扫描。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
三条无条件优先的规矩不变:减持方向永远自动执行;深档补仓永远要人点头;
**研判不可用时一律入队待确认**——最后这条对新建仓尤其重要,
它是「拿不到意见绝不当成通过」这条纪律在新链路上的落点。
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
**部署时机提醒**:默认 `full` 意味着 `make deploy` 一跑完,
下一个整分钟的心跳就可能真的建仓。**建议收盘后部署**
2026-08-06 13:05:40 +08:00
让第一次真实运行发生在次日开盘、你在场的时候。
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:55:27 +08:00
## 八、新增参数(四个)
2026-08-06 12:34:57 +08:00
| 键 | 默认 | 说明 |
|---|---|---|
2026-08-06 13:05:40 +08:00
| `PMS_OPEN_AUTONOMY` | `full` | 新建仓档位兼总开关full 自动执行 / propose_only 待确认 / off 不扫描 |
2026-08-06 13:55:27 +08:00
| `PMS_OPEN_REQUIRE_WS_CASH` | `True` | 拿不到真实资金快照时不自动新建仓(写 skipped 留痕) |
2026-08-06 13:05:40 +08:00
| `PMS_OPEN_REF_DRIFT_MAX` | `0.03` | 参考位盘中被改写的容忍幅度,超过则该票当日暂停新建仓 |
| `PMS_JUDGE_TICK_BUDGET_SEC` | `150` | 单轮提议扫描用于研判的时间预算(秒),用尽则剩下的候选留到下一跳 |
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
改默认值的既有参数:`PMS_JUDGE_ACTIONS` 从 `FILL,ADD,DCA,SWITCH` 改成
`FILL,ADD,DCA,SWITCH,OPEN`
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
`FAIL_CLOSED` 加一条 `PMS_OPEN_AUTONOMY: "off"`——参数表读不到时不自动建仓。
这与「默认开启」不冲突:默认值管正常情况,`FAIL_CLOSED` 管读不到参数表的故障情况。
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:55:27 +08:00
## 九、两个已知的交互
2026-08-06 12:34:57 +08:00
1. **命令驱动的 OPEN 拒绝记录会挡住自主 OPEN。**
`command_service._ledger_rejects` 写的是 `action="OPEN"`、`arbiter="rule"`、
2026-08-06 13:55:27 +08:00
`verdict="REJECT"`,而 `_rejected_today_keys` 读的正是这张表的(股票,动作)。
2026-08-06 13:05:40 +08:00
所以某只票今天因为一条升仓命令在规划期被上限拦过,自主新建仓当天不会再提它。
多数情况下这是对的(同一组约束),但两边算的目标金额不同,
2026-08-06 13:55:27 +08:00
一手不足这类拒绝口径可能不一样。先记下来,观察一天。
2026-08-06 12:34:57 +08:00
2. **升仓命令的候选没有排除已有在途方案的票。**
2026-08-06 13:55:27 +08:00
`_dispatch_planner` 算出了 `exclude = _codes_with_live_plans()` 却只传给降仓分支。
既有现象,不属本次范围;自主这条路有 `_inflight_keys` 兜底,命令那条路没有。
**这次不动,先观察。**
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:55:27 +08:00
## 十、上线与判收
2026-08-06 12:34:57 +08:00
2026-08-06 13:05:40 +08:00
判收标准是真机各见一次。
2026-08-06 12:34:57 +08:00
| 步 | 动作 | 判收标准 |
|---|---|---|
2026-08-06 13:55:27 +08:00
| 1 | 决策系统侧先上:`git pull` + `restart backend-api worker-brain` | 手工发一条 `action=OPEN` 的研判请求,回 PASS 或 REJECT理由读得出是在谈从零建仓而不是加仓请求体里确认没有仓位数字 |
2026-08-06 13:05:40 +08:00
| 2 | PMS 侧 `make deploy` + `make test`**收盘后做** | ALL SUITES PASS |
2026-08-06 13:55:27 +08:00
| 3 | 次日盘前先把 `PMS_OPEN_AUTONOMY` 设成 `propose_only` 看一轮 | 提议队列出现 OPEN理由读得懂`make t-gate` 看到 arbiter=judge 的行;多条候选时确认名额与金额是滚动扣减的 |
2026-08-06 13:05:40 +08:00
| 4 | 确认无误后改成 `full` | 一条 OPEN 提议直接落指令,`make t-ins` 看到 `origin_type=proposal`、`action=OPEN`、`is_command=false` |
2026-08-06 13:55:27 +08:00
| 5 | 盘中观察 | 现价在买入区间内时出手,`last_decision.source=A`,限价等于区间上沿 |
2026-08-06 12:34:57 +08:00
| 6 | 成交之后 | `pms_position` 出现该票、`opened_date` 有值、`status=HOLDING`;次日起 `eval_fill` 能看见它 |
2026-08-06 13:55:27 +08:00
| 7 | 研判预算与账本 | 日志里出现过「本轮研判时间预算用尽」且下一跳接上了;`intraday_exec` 一次都没触发软超时;**评审账本没有被同一条驳回刷屏** |
2026-08-06 13:05:40 +08:00
| 8 | 漂移防护 | **只靠单测判收,不在生产人为制造**(盘中跑 push-pool 触发补扫风险太大)。生产侧只观察账本里有没有 `REF_DRIFT` 的 WARN 行 |
2026-08-06 13:55:27 +08:00
第三步那轮 `propose_only` 是我加的一道自保,代价一个交易日;想直接上 `full` 就跳过,
判收从第四步开始。
2026-08-06 12:34:57 +08:00
---
2026-08-06 13:55:27 +08:00
## 十一、第一周要盯的四个数
2026-08-06 13:05:40 +08:00
2026-08-06 13:55:27 +08:00
不设人为上限之后,这四个数替代了「每天开几只」这个旋钮:
2026-08-06 12:34:57 +08:00
2026-08-06 13:55:27 +08:00
1. **每天有几只候选落在自己的买入区间内**——实际建仓速度,
2026-08-06 13:05:40 +08:00
也是「让系统自己判断」到底判成什么样的直接答案。
2026-08-06 13:55:27 +08:00
2. **研判驳回率**——驳得太狠说明 OPEN 判据写窄了,一条不驳说明写宽了。
2026-08-06 13:05:40 +08:00
3. **`intraday_exec` 一跳的耗时分布**——预算够不够,要不要开研判独立队列。
2026-08-06 13:55:27 +08:00
4. **评审账本每天的行数**——第三节第 5 条那个去重有没有真的生效。