添加自动建仓逻辑
This commit is contained in:
parent
1f7b3e1e82
commit
996a2f358b
50
DEVLOG.md
50
DEVLOG.md
|
|
@ -58,6 +58,56 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-08-06 · 新建仓动作:四条取舍已拍板,决策系统侧评估完毕,方案定稿
|
||||
|
||||
**做了什么**
|
||||
四条取舍拍板:新建仓单独一个档位参数且默认开启;不设每日开仓上限,由既有上限、资金、
|
||||
候选池与择时自己收敛;研判闸这一轮双侧一并加上;参考位漂移做首答锁定加偏离即停。
|
||||
按第三条读了决策系统的相关代码,方案更新为定稿版 V2。代码仍然一行未改。
|
||||
|
||||
**读决策系统代码得到的三条事实(以代码为准)**
|
||||
|
||||
1. **择时那一侧一个字都不用改。** `app/services/pms_advisor.py` 的 `_advise`
|
||||
(163 到 226 行)从头到尾没有读过 `action` 字段,只用 `ts_code`、`side`、`day.price`。
|
||||
买入区间由昨夜支撑压力推出,与这笔买单是建新仓还是加老仓无关。
|
||||
2. **研判那一侧没有任何 action 白名单。** `app/api/main.py:323` 的路由是裸 dict,
|
||||
只校验总开关与 `ts_code` 非空;worker 侧 `tasks_intraday.py:512` 是
|
||||
`_task_by_action.get(_act, 兜底文案)`。所以今天发 `action="OPEN"` 就能跑通,
|
||||
但拿到的是给加仓写的兜底文案,后面还跟着一句「仓位纪律不归你管」。
|
||||
要改的是三处,都在 `workers/tasks_intraday.py`:加一条 OPEN 判据、
|
||||
OPEN 且现价缺失时显式回 UNAVAILABLE、边界声明按动作分岔。
|
||||
3. **新票的现价链路最脆弱。** `tasks_intraday.py` 449 到 456 行,新票没有持仓快照,
|
||||
`fetch_realtime_close` 读不到分钟线时返回 `0.0` 被 `or None` 变成 None,
|
||||
提示词渲染成「当前现价 未知 元」。加仓类还有摊薄成本可依,建仓判断没有现价锚不成立。
|
||||
|
||||
**新撞出来的一条硬约束(工程事实,不是取舍)**
|
||||
`app/scheduler.py:45` 给所有调度任务设了 `task_soft_time_limit=240, task_time_limit=300`,
|
||||
而 `judge.request` 是同步阻塞、单次超时上限 90 秒。**三只票送研判就是 270 秒,已经超过
|
||||
软超时。** 今天没出事是因为送研判的只有三只持仓票且多数轮次被去重挡掉。新建仓不节流、
|
||||
候选池默认取前 30 只,第一跳就会捅穿。
|
||||
处理办法是给单轮扫描一个研判时间预算 `PMS_JUDGE_TICK_BUDGET_SEC`(默认 150 秒),
|
||||
用尽就把剩下的候选留到下一分钟的心跳,并写进 skipped 留痕。
|
||||
**这不是节流:一条候选都没丢,也没有按天计的上限**,只是让一次心跳做得完。
|
||||
|
||||
**动了哪些文件**
|
||||
无代码改动。`NEW_POSITION_ACTION_PLAN.md` 更新为定稿版 V2,已放进项目目录。
|
||||
|
||||
**部署方式**
|
||||
不涉及。定稿版里写明:决策系统这次只改 worker 代码、不动 `.env`,
|
||||
所以 `git pull` 后 `docker compose restart backend-api worker-brain` 即可;
|
||||
哪天真要往 `.env` 写值(比如切研判独立队列)才必须 `up -d --force-recreate`。
|
||||
PMS 侧因为新建仓档位默认就是自动执行,**建议收盘后 `make deploy`**,
|
||||
让第一次真实建仓发生在次日开盘、人在场的时候。
|
||||
|
||||
**真机判收**
|
||||
未判收(尚未动码)。
|
||||
|
||||
**还欠着什么**
|
||||
等点头后开工。开工顺序:决策系统侧先上并用 `pms_smoke.py` 手工发一条
|
||||
`action=OPEN` 的研判请求验证,再回来做 PMS 侧。
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
下一条节点从这里往下写,格式照抄上面:
|
||||
## YYYY-MM-DD · 一句话标题
|
||||
|
|
|
|||
|
|
@ -1,260 +1,291 @@
|
|||
# 方案:给动作引擎加「新建仓」动作
|
||||
# 方案(定稿):给动作引擎加「新建仓」动作
|
||||
|
||||
> 状态:**待拍板**,2026-08-06 收盘后出。全部依据来自实际代码,不引用设计文档的结论。
|
||||
> 涉及系统:tradingSystem(PMS,主要改动)、bionic_trader(决策系统,可选的第二步)。
|
||||
> 部署方式:PMS 源码打进镜像,改码必须 `make deploy`,跑完 `make test` 见到 ALL SUITES PASS。
|
||||
> V2,2026-08-06。四条取舍已拍板,决策系统侧已评估完毕。**待你点头即可动手。**
|
||||
> 全部依据来自实际代码,不引用设计文档的结论。
|
||||
> 部署:PMS 源码打进镜像,改码必须 `make deploy`,跑完 `make test` 见 ALL SUITES PASS;
|
||||
> 决策系统挂载卷,`git pull` 后 `docker compose restart backend-api worker-brain` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 一、先说清现状:管道其实已经铺好了,缺的只有一段
|
||||
## 〇、拍板结果(V1 的四问)
|
||||
|
||||
读代码之后,一件事和交接信里的判断略有出入,而且是好消息:**`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` 候选,然后并进现有的那条「规则闸 → 研判闸 → 档位分流」的路。** 下游一行都不用动。
|
||||
| # | 问题 | 决定 |
|
||||
|---|---|---|
|
||||
| 1 | 档位 | **单独一个开关,默认开启**。新建仓不跟随全局档位,自己一个参数,默认就是自动执行 |
|
||||
| 2 | 每日节流 | **不做**。逻辑进逻辑出,不设人为上限,由既有的上限、资金、候选池与择时自己收敛 |
|
||||
| 3 | 研判闸 | **这一轮一并加上**,双侧改到位 |
|
||||
| 4 | 漂移防护 | **做**,首答锁定加偏离即停 |
|
||||
|
||||
---
|
||||
|
||||
## 二、选票规则:动作引擎只管名额、钱、批次,不管时机
|
||||
## 一、现状:管道已经铺好,缺的只有一段
|
||||
|
||||
候选池那边已经过完一整套筛选(`plan_feed.select_candidates`):按分数降序、档位白名单、
|
||||
分数与来源数与预期空间的下限、同主题限额、剔除 ST、剔除已持仓、剔除黑名单。
|
||||
排在它后面的规则闸又会管:一手整百、冻结、全局暂停买入、黑名单、决策系统昨夜定性
|
||||
`BAD_SIGNAL`、涨停不追、当日涨幅不追高、距 MA5 不追高、组合与单股与持仓数与行业集中度
|
||||
的上限、预留现金、真实可用资金。再后面的择时又会管:现价在不在买入区间内。
|
||||
`OPEN` 这个动作名在 PMS 里已经存在,从方案到下单的整条管道是通的:
|
||||
`planner.py:31` 定义 `A_OPEN`、`executor.py:41` 的 `BUY_ACTIONS` 已含 `OPEN`、
|
||||
`command_service.py:647` 已经在用 `action="OPEN"` 写评审账本。
|
||||
|
||||
`action_engine.py` 第 17 行的注释已经把职责边界钉死了 ——「上限/一手/冻结等硬约束**不在
|
||||
这里重复判**,统一由规则闸终检(职责单一,口径唯一)」。所以新建仓求值器只回答三个问题:
|
||||
缺的只有两处:`action_engine.py:27` 的自主动作词表里没有 `OPEN`;
|
||||
`action_engine.scan()` 第 197 行是 `for p in positions or []`,输入是持仓列表,
|
||||
**结构上就不可能引入新标的**。
|
||||
|
||||
1. **还能开几只**:`min(最大持仓数 − 当前持仓数, 每日新开上限 − 今日已开)`。
|
||||
2. **还有多少钱可投**:`总仓上限 × 总规模 − 当前组合市值`,与单只的目标金额取小。
|
||||
所以这次做的事是:补一个以候选池为输入的求值器,产出 `OPEN` 候选,并进现有的
|
||||
「规则闸 → 研判闸 → 档位分流」那条路。下游一行不用改。
|
||||
|
||||
---
|
||||
|
||||
## 二、决策系统侧的评估结论(第 3 条拍板的前置)
|
||||
|
||||
读了 `workers/tasks_intraday.py`、`app/api/main.py`、`app/services/pms_advisor.py`、
|
||||
`config/settings.py`。三条结论:
|
||||
|
||||
### 1. 择时那一侧,一个字都不用改
|
||||
|
||||
`pms_advisor._advise`(第 163 到 226 行)从头到尾**没有读过 `action` 字段**,
|
||||
它只用 `ts_code`、`side`、`day.price` 三样。买入区间由昨夜结论的支撑压力推出来,
|
||||
跟这笔买单是建新仓还是加老仓无关。所以新建仓的择时天然就能用,零改动。
|
||||
|
||||
### 2. 研判那一侧,没有任何白名单,但兜底文案是给加仓写的
|
||||
|
||||
从 HTTP 入口到大模型提示词,全链路**没有一处校验 `action` 的取值**:
|
||||
`app/api/main.py:323` 的路由是裸 `dict`,只查总开关和 `ts_code` 非空;
|
||||
worker 侧 `tasks_intraday.py:512` 是
|
||||
|
||||
```python
|
||||
core_task = _task_by_action.get(_act, f"这是 PMS 的【{_act}】提议, 请按提议理由与硬数字做定性仲裁。")
|
||||
```
|
||||
|
||||
也就是说,**今天就把 `action="OPEN"` 发过去,它不会报错、不会回不可用,会照常打大模型
|
||||
并按 PASS/REJECT 收口**——但拿到的是一段没有针对性判据的兜底文案,
|
||||
后面还无条件跟着一句「仓位纪律 PMS 的规则闸已经查过,不归你管」,那是给加仓写的语境。
|
||||
让它就这么跑,等于用一个含糊的问题去问一个昂贵的模型,答出来的东西没法信。
|
||||
|
||||
所以「一并加上」要改的是三处,都在 `workers/tasks_intraday.py`:
|
||||
|
||||
- **加一条 OPEN 判据**(第 495 到 511 行的 `_task_by_action` 字典里加一项)。
|
||||
核心问题写成:这是一只从零开始建仓的新票,当初把它选进候选池的驱动,
|
||||
到今天是否仍然成立、有没有被证伪;证据不足以支持从零建仓时选驳回。
|
||||
- **现价缺失时显式回不可用**。第 449 到 456 行的现价链路:新票没有持仓快照,
|
||||
`_pos.get("price")` 拿不到,退到 `fetch_realtime_close`,而那个函数读不到分钟线时
|
||||
返回 `0.0`,被 `or None` 变成 `None`,提示词最终渲染成「当前现价 未知 元」。
|
||||
加仓类还有摊薄成本可依,**建仓判断没有现价锚基本不成立**,
|
||||
所以要给 OPEN 加一条:现价拿不到直接回 `UNAVAILABLE`,不让模型盲判。
|
||||
- **边界声明按动作分岔**。那句「仓位纪律不归你管」对 OPEN 仍然成立
|
||||
(名额、行业集中度、资金确实是 PMS 规则闸查的),但措辞要改成建仓语境,
|
||||
免得模型把它读成「这是一笔加仓」。
|
||||
|
||||
另外两件不用改、但要知道的事:提示词里会出现「(无持仓快照)」「(无近期评审流水)」
|
||||
这两个空占位(新票本来就没有),是噪声但不误导;
|
||||
`get_board_qrs(ts_code)` 那里没做代码格式转换,我核过 PMS 侧
|
||||
`command_spec.normalize_code`(第 296 到 313 行)产出的是点式,
|
||||
`get_board_qrs` 对点式解析正常,**这条路上不是 bug**。
|
||||
|
||||
### 3. 部署方式:只要不动 `.env`,restart 就够
|
||||
|
||||
`config/settings.py` 第 208 到 226 行那段 P 段自己写着「全部有默认值,不新增必填 env
|
||||
→ 重启 backend-api + worker-brain 即生效」。这次改的是 worker 代码,不加新配置,
|
||||
所以走常规路径:`git pull` 之后 `docker compose restart backend-api worker-brain`。
|
||||
**一旦哪天要往 `.env` 里真的写一个值(比如切研判独立队列),那才必须
|
||||
`docker compose up -d --force-recreate`。**
|
||||
|
||||
---
|
||||
|
||||
## 三、新发现的一条硬约束:一跳心跳里塞不下那么多次研判
|
||||
|
||||
这条是读代码时撞出来的,不是策略取舍,是工程事实,必须处理。
|
||||
|
||||
`app/scheduler.py:45` 给所有调度任务设了 `task_soft_time_limit=240, task_time_limit=300`。
|
||||
自主提议扫描跑在每分钟一跳的 `intraday_exec` 里,而 `judge.request` 是**同步阻塞**的,
|
||||
超时上限 `PMS_JUDGE_TIMEOUT` 默认 90 秒。
|
||||
|
||||
**三只票送研判就是 270 秒,已经超过 240 秒的软超时。** 今天之所以没出事,
|
||||
是因为送研判的只有已有持仓的三只票,而且多数轮次被在途去重和当日去重挡掉了。
|
||||
新建仓不节流、候选池默认取前 30 只,第一跳就会把这个洞捅穿——任务被中途杀掉,
|
||||
而且是在已经落了一部分表之后。
|
||||
|
||||
**处理办法:给单轮扫描一个研判时间预算,而不是给每天一个开仓上限。**
|
||||
|
||||
新增 `PMS_JUDGE_TICK_BUDGET_SEC`(默认 150 秒)。每次要调研判前先看这一轮已经花了多久,
|
||||
如果「已花 + 单次超时上限」会顶破预算,本轮就不再送后面的候选,
|
||||
把它们写进 `skipped`,理由写「本轮研判时间预算用尽,下一跳继续」。
|
||||
|
||||
这不是节流:**一条候选都没有被丢掉,也没有任何按天计的上限**,
|
||||
只是把一次心跳做不完的活挪到下一分钟。候选是按分数降序排的,
|
||||
所以预算用尽时先做完的一定是分数最高的那些。决策系统那侧的裁决缓存是按
|
||||
`日期 + 股票 + 动作` 存半小时的,所以同一批候选在半小时内的重复扫描都是秒回,
|
||||
真正慢的只有缓存过期后的第一跳。
|
||||
|
||||
顺带一提,决策系统那侧还预埋了研判独立队列(接口文档 §5.2)。
|
||||
现在不用开——那是给 `brain_queue` 拥堵准备的第二档,而我们这里的瓶颈是
|
||||
PMS 自己的心跳时长,不是对端排队。真出现频繁超时再开,而且开它要改 `.env`,
|
||||
必须 `force-recreate`。
|
||||
|
||||
---
|
||||
|
||||
## 四、选票规则:动作引擎只管名额、钱、批次
|
||||
|
||||
候选池那边已经筛过(分数降序、档位白名单、分数与来源数与预期空间下限、同主题限额、
|
||||
剔除 ST、剔除已持仓、剔除黑名单)。规则闸后面还会管一手整百、冻结、全局暂停买入、
|
||||
黑名单、昨夜定性 `BAD_SIGNAL`、涨停不追、当日涨幅不追高、距 MA5 不追高、
|
||||
组合与单股与持仓数与行业集中度上限、预留现金、真实可用资金。择时再管现价在不在买入区间。
|
||||
|
||||
`action_engine.py:17` 的注释已经把边界钉死了——上限、一手、冻结这些不在动作引擎重复判。
|
||||
所以新建仓求值器只回答三个问题:
|
||||
|
||||
1. **还能开几只**:`最大持仓数 − 当前持仓数`。没有额外的每日上限。
|
||||
2. **还有多少钱可投**:`总仓上限 × 总规模 − 当前组合市值`,与单只目标金额取小。
|
||||
3. **这一批买多少股**:按 `PMS_BATCH_SPLIT` 拆批,**只提底仓那一批**(默认 50%),
|
||||
数量 `lot_qty(金额, 现价)`,不足一手就不提。
|
||||
`lot_qty(金额, 现价)`,不足一手就不提。
|
||||
|
||||
**只提底仓批**这一条是刻意的:后续的回踩补足与盈利加仓,等持仓真的建立起来之后,由已有的
|
||||
`eval_fill` 与 `eval_add` 自然接管——它们本来就是干这个的。这样不需要命令驱动那套
|
||||
`GATED` 方案的解锁机制,新建仓这条路上一条 GATED 都不产生。
|
||||
**只提底仓批**是刻意的:后续的回踩补足与盈利加仓,等持仓真建立起来之后由已有的
|
||||
`eval_fill` 与 `eval_add` 自然接管。这条路上一条 `GATED` 方案都不产生。
|
||||
|
||||
**入场时机不在这里判。** 现价合不合适是择时的活;定性好不好是规则闸 `BAD_SIGNAL` 的活。
|
||||
动作引擎在这里另加一套判断,就违背了「提前计算为主、盘中监控为辅」,而且会和已有的闸重复。
|
||||
**价格必须是实时价。** `market.plan_price` 拿不到实时价会回落昨收,
|
||||
它自己的注释写着「拿昨收当现价去做不追高这类判断会出错,只给规划期定量用」。
|
||||
自主建仓要真下单,所以走 `market.get_price`,取不到就整只跳过并写进 `skipped`。
|
||||
|
||||
**价格必须是实时价。** `market.plan_price` 在拿不到实时价时会回落昨收,它自己的注释写着
|
||||
「拿昨收当现价去做不追高这类判断会出错,只给规划期定量用」。自主建仓是要真下单的,
|
||||
所以走 `market.get_price`,取不到就整只跳过并写进 `skipped`——「什么都没发生」和
|
||||
「明着跳过了」在页面上必须是两回事,这是 `scan()` 入口处已经立过的规矩。
|
||||
**不节流的一个自然收敛点**:择时的买入区间本身就是过滤器。
|
||||
只有现价正好落在 `[支撑×0.99, 支撑+(压力−支撑)×0.3]` 里的票才会真的出手,
|
||||
高于上沿一律「不追,接受买不上」。所以每天实际能建成几只,取决于当天有几只票
|
||||
落在自己的区间内——这正是「让系统自己判断」。第一周要盯的就是这个数。
|
||||
|
||||
---
|
||||
|
||||
## 三、要动的文件与改法(PMS 侧,全部是加法)
|
||||
## 五、要动的文件与改法
|
||||
|
||||
### 1. `app/core/action_engine.py`
|
||||
### PMS 侧(`factor@factorevaluation`,改完 `make deploy`)
|
||||
|
||||
- 加常量 `A_OPEN = "OPEN"`,把它加进 `JUDGE_ACTIONS`。
|
||||
- 新增 `eval_open(c, params, mkt)`:输入是一条候选(代码、现价、分数、行业),
|
||||
输出与现有四个求值器同构的候选结构。
|
||||
- 新增 `scan_open(*, candidates, params, market, room, slots, skip)`:遍历候选,
|
||||
名额与金额是全局的,所以边遍历边扣减;每一条不产出候选都要写进 `skipped` 并说明原因。
|
||||
- **现有的 `scan()` 一个字不动**,既有单测继续全绿。
|
||||
1. **`app/core/action_engine.py`**
|
||||
加 `A_OPEN = "OPEN"`,加进 `JUDGE_ACTIONS`;
|
||||
新增 `eval_open()` 与 `scan_open()`;**既有 `scan()` 一个字不动**,既有单测继续全绿。
|
||||
|
||||
### 2. `app/services/proposal_service.py`
|
||||
2. **`app/services/proposal_service.py`**
|
||||
- `scan_and_route` 在 `ae.scan(...)` 之后追加取候选池、算名额与可投金额、调 `scan_open`,
|
||||
两批候选合并走同一个 `_route_one`。
|
||||
- 候选池取数失败(`PlanFeedError`)时不产生任何新建仓候选,
|
||||
原因写进 `skipped`;已有持仓的四类动作照常扫描,不受影响。
|
||||
- 研判时间预算:`_route_one` 之外记一个本轮起始时刻,送研判前判预算。
|
||||
- `_route_one` 给 `OPEN` 三处特判:现价取候选自带的(`_pos_of` 对新票没有 `price`,
|
||||
取它会是 0,规则闸直接 `PRICE_MISSING`);组合上下文改成
|
||||
`caps_ctx(view, ts_code=code, is_new_name=True, sector=industry.get(code))`
|
||||
——**必须显式传行业**,否则行业集中度那道硬拦截会静默跳过;
|
||||
落指令前先 `pms_repo.ensure_position(code)` 建行再写 `target_pct`。
|
||||
- 档位分流加 OPEN 分支(见第六节)。
|
||||
|
||||
- `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`**
|
||||
`_advice` 把应答 `observed` 里的支撑、压力、买入区间一并存进缓存;
|
||||
当日首答把这三样锁进 `prog["ref_lock"]`;之后每答比对,
|
||||
偏离超过 `PMS_OPEN_REF_DRIFT_MAX` 就把本轮判定改成等待,
|
||||
并在返回的决策里带上 `ref_drift` 说明。**只对 `action == "OPEN"` 生效。**
|
||||
|
||||
### 3. `app/services/exec_advisor.py` 与 `app/services/executor.py`(漂移防护)
|
||||
4. **`app/services/executor.py`**
|
||||
`run_tick` 的等待分支里,看到 `d.get("ref_drift")` 就额外落一条评审账本 `WARN`。
|
||||
(放这里而不放 `exec_advisor`,是为了不给后者引入落表副作用。)
|
||||
|
||||
见第五节。
|
||||
5. **`app/repo/pms_repo.py`**
|
||||
新增只读函数,统计当日已开与在提的新仓票数(供留痕与页面显示;
|
||||
不做上限,只是让人看得见今天开了几只)。
|
||||
|
||||
### 4. `app/repo/pms_repo.py`
|
||||
6. **`app/services/param_store.py` 与 `config/settings.py`**
|
||||
新增参数(第七节),同步补 `DESC` 中文说明、`_RANGES` 或 `_range_check` 校验、
|
||||
以及 `FAIL_CLOSED`。`param_store.py:46` 那条注释记着一次血的教训——
|
||||
加参数不同步加白名单,`set_param` 只回 `ok=False` 不抛异常,调用方又把返回值丢了,
|
||||
于是那条纪律静默失效。这次一并照做。
|
||||
另外把 `PMS_JUDGE_ACTIONS` 的默认值从 `FILL,ADD,DCA,SWITCH` 改成
|
||||
`FILL,ADD,DCA,SWITCH,OPEN`。
|
||||
|
||||
- 新增一个只读函数,统计今日已经开过或正在提议中的新仓票数(`pms_instruction` 与
|
||||
`pms_proposal` 里今天 `action='OPEN'` 的 distinct 股票数)。每日节流要用。
|
||||
7. **测试脚本**
|
||||
纯逻辑用例,不连库:名额已满、组合钱不够、买不足一手、取不到实时价、
|
||||
候选池取数失败、研判预算用尽、参考位漂移超阈值、档位分流的三种取值。
|
||||
|
||||
### 5. `app/services/param_store.py` 与 `config/settings.py`
|
||||
### 决策系统侧(`192.168.16.188`,改完 `git pull` + `restart`)
|
||||
|
||||
新增参数(见第六节),并同步补 `DESC` 中文说明与 `_RANGES` 取值校验。
|
||||
`param_store.py` 第 46 到 48 行有一条血的教训写在那里:加参数表时不同步加白名单,
|
||||
`set_param` 只回 `ok=False` 不抛异常,调用方又把返回值丢了,于是那条纪律静默失效。
|
||||
这次一并照做。
|
||||
|
||||
### 6. `scripts/test_batch3_units.py`(或新增一个测试文件)
|
||||
|
||||
至少覆盖:名额已满、组合钱不够、买不足一手、取不到实时价、每日节流已用完、
|
||||
候选池取数失败、参考位漂移超阈值。**这些用例都是纯逻辑,不连库。**
|
||||
8. **`workers/tasks_intraday.py`**
|
||||
`_task_by_action` 加 OPEN 判据;OPEN 且现价缺失时回 `UNAVAILABLE`;
|
||||
边界声明按动作分岔。**不动 `.env`,不加新配置,restart 即生效。**
|
||||
|
||||
---
|
||||
|
||||
## 四、档位分流:这是要你拍板的第一件事
|
||||
## 六、档位:一个开关,默认开启
|
||||
|
||||
现有分流在 `proposal_service._route_one` 第 132 行:
|
||||
新增 `PMS_OPEN_AUTONOMY`,取值 `full` / `propose_only` / `off`,**默认 `full`**。
|
||||
它同时充当总开关,不再单设第二个开关:`off` 就是不扫描新建仓。
|
||||
|
||||
```
|
||||
auto_exec = (side == "sell") or (autonomy == AUTONOMY_FULL and not force_queue)
|
||||
```
|
||||
`_route_one` 第 132 行那行不动,在它之外给 OPEN 加一个分支:
|
||||
|
||||
新建仓是买入方向,所以在当前的 `propose_only` 档位下,它必然落进待确认队列。
|
||||
这意味着人力从「下命令 + 排方案 + 核方案」降到「点一下确认」,但仍然不是无人值守。
|
||||
- `full` → 过完两道闸就直接落指令,无人值守。
|
||||
- `propose_only` → 落待确认队列。
|
||||
- `off` → 根本不扫描,一条候选都不产生。
|
||||
|
||||
三条路:
|
||||
三条无条件优先的规矩保持不变:减持方向永远自动执行;
|
||||
深档补仓永远要人点头;**研判不可用(`degraded`)时一律入队待确认**——
|
||||
最后这条对新建仓尤其重要,它是「拿不到意见绝不当成通过」这条纪律在新链路上的落点。
|
||||
|
||||
- **甲:不动档位。** 新建仓入队待确认。改动最小,但没达到「不要每天靠人」的目标。
|
||||
- **乙:把 `PMS_AUTONOMY` 调成 `full`。** 新建仓自动了,但补足底仓、盈利加仓、
|
||||
浅档补仓也一起自动了——那是三个不同的决定,不该被一个开关捆在一起。
|
||||
- **丙(我的建议):给新建仓单独一个档位参数 `PMS_OPEN_AUTONOMY`。**
|
||||
取值 `inherit`(跟随全局,默认)/ `full`(自动执行)/ `propose_only`(待确认)。
|
||||
在 `auto_exec` 的计算里加一个针对 `OPEN` 的分支,不动既有那行的语义。
|
||||
这样可以做到「新建仓自动、其余仍待人确认」,也可以随时单独关掉新建仓而不影响别的。
|
||||
深档补仓的强制确认(`needs_user_confirm`)与研判不可用时的降级入队仍然无条件优先。
|
||||
**一条部署上的提醒**:默认 `full` 意味着 `make deploy` 一跑完,
|
||||
下一个整分钟的心跳就可能真的建仓。建议**收盘后部署**,
|
||||
让第一次真实运行发生在次日开盘、你在场的时候。
|
||||
|
||||
---
|
||||
|
||||
## 五、盘中输入漂移:这是要你拍板的第二件事
|
||||
|
||||
你点名的约束:择时读的 `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_AUTONOMY` | `full` | 新建仓档位兼总开关:full 自动执行 / propose_only 待确认 / off 不扫描 |
|
||||
| `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_MIN_SCORE` | `0` | 在候选池已有过滤之上额外的分数下限,0 = 不额外过滤 |
|
||||
| `PMS_OPEN_REF_DRIFT_MAX` | `0.03` | 参考位盘中被改写的容忍幅度,超过则该票当日暂停新建仓 |
|
||||
| `PMS_JUDGE_TICK_BUDGET_SEC` | `150` | 单轮提议扫描用于研判的时间预算(秒),用尽则剩下的候选留到下一跳 |
|
||||
|
||||
`PMS_OPEN_ENABLED` 建议同时加进 `param_store.FAIL_CLOSED`,取值 `False`——参数表读不到时
|
||||
安全方向是不建仓。
|
||||
改默认值的既有参数:`PMS_JUDGE_ACTIONS` 从 `FILL,ADD,DCA,SWITCH` 改成
|
||||
`FILL,ADD,DCA,SWITCH,OPEN`。
|
||||
|
||||
**每日只开一只**这个默认值需要你确认。理由:候选池 top 30、持仓上限 20 只,如果不限,
|
||||
第一天就会把组合一次性建满,所有仓位同一天建立、同涨同跌,而且撞上的还是交接信里
|
||||
「仓位单向增长」的另一个版本。一天一只,二十个交易日建满,节奏上更像人在做。
|
||||
`FAIL_CLOSED` 加一条 `PMS_OPEN_AUTONOMY: "off"`——参数表读不到时不自动建仓。
|
||||
这与「默认开启」不冲突:默认值管正常情况,`FAIL_CLOSED` 管读不到参数表的故障情况。
|
||||
|
||||
---
|
||||
|
||||
## 七、研判闸:这是要你拍板的第三件事
|
||||
|
||||
`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)`。所以某只票今天因为一条升仓命令在规划期被上限拦过,
|
||||
自主新建仓当天就不会再提它。**多数情况下这是对的**(同一组约束),但两边算的目标金额
|
||||
不一样,一手不足这类拒绝可能口径不同。先记下来,观察一天再看要不要分开。
|
||||
`verdict="REJECT"`,而 `_rejected_today_keys` 读的正是这张表的 `(股票, 动作)`。
|
||||
所以某只票今天因为一条升仓命令在规划期被上限拦过,自主新建仓当天不会再提它。
|
||||
多数情况下这是对的(同一组约束),但两边算的目标金额不同,
|
||||
一手不足这类拒绝口径可能不一样。先记下来,观察一天再看要不要分开。
|
||||
|
||||
2. **升仓命令的候选没有排除已有在途方案的票。**
|
||||
`command_service._dispatch_planner` 算出了 `exclude = _codes_with_live_plans()`,
|
||||
但只传给了降仓分支,升仓分支没传。这是既有的现象,不属于本次改动范围,
|
||||
但自主新建仓上线后两条路会同时挑票,撞车概率上升。
|
||||
自主这条路有 `_inflight_keys` 兜底(同票同动作有在途提议或指令就不重复提),
|
||||
命令那条路没有。**建议这次不动它,先观察**。
|
||||
但只传给了降仓分支。这是既有现象,不属于本次范围;
|
||||
自主这条路有 `_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`,限价等于区间上沿 |
|
||||
| 1 | 决策系统侧先上:`git pull` + `restart backend-api worker-brain` | `docker compose exec backend-api python scripts/pms_smoke.py` 里手工发一条 `action=OPEN` 的研判请求,回 PASS 或 REJECT,理由读得出是在谈建仓而不是加仓 |
|
||||
| 2 | PMS 侧 `make deploy` + `make test`(**收盘后做**) | ALL SUITES PASS |
|
||||
| 3 | 次日盘前把 `PMS_OPEN_AUTONOMY` 先设成 `propose_only` 看一轮 | 提议队列出现 OPEN,理由读得懂,硬数字里有名额、可投金额、批次、分数;`make t-gate` 能看到 arbiter=judge 的行 |
|
||||
| 4 | 确认无误后改成 `full` | 一条 OPEN 提议直接落指令,`make t-ins` 看到 `origin_type=proposal`、`action=OPEN`、`is_command=false` |
|
||||
| 5 | 盘中观察 | 现价在买入区间内时出手,`last_decision.source=A`,限价等于区间上沿;`make watch` 里那条指令的进度在推进 |
|
||||
| 6 | 成交之后 | `pms_position` 出现该票、`opened_date` 有值、`status=HOLDING`;次日起 `eval_fill` 能看见它 |
|
||||
| 7 | 漂移防护(若选了第五节的乙) | **建议只靠单测判收,不在生产人为制造**:盘中跑 push-pool 触发补扫风险太大。生产侧只观察账本里有没有 `REF_DRIFT` 的 WARN 行 |
|
||||
| 7 | 研判预算 | 日志里出现过「本轮研判时间预算用尽」且下一跳接上了;`intraday_exec` 一次都没有触发软超时 |
|
||||
| 8 | 漂移防护 | **只靠单测判收,不在生产人为制造**(盘中跑 push-pool 触发补扫风险太大)。生产侧只观察账本里有没有 `REF_DRIFT` 的 WARN 行 |
|
||||
|
||||
第三步那个 `propose_only` 看一轮,是我加的一道自保:默认虽然是 `full`,
|
||||
但第一次真跑之前先用一轮眼睛确认提议本身是对的,代价只有一个交易日。
|
||||
你要是想直接上 `full`,把第三步跳过即可,判收标准从第四步开始。
|
||||
|
||||
---
|
||||
|
||||
## 十、需要你拍板的四件事(其余我按上面的建议做)
|
||||
## 十、第一周要盯的三个数
|
||||
|
||||
1. **档位**:给新建仓单独一个 `PMS_OPEN_AUTONOMY`(建议),还是直接用全局 `PMS_AUTONOMY`?
|
||||
2. **每日节流**:一天最多新开几只?(建议 1)
|
||||
3. **研判闸**:这一轮 PMS 侧留口子、决策系统侧下一轮再做(建议),还是这一轮双侧一起改?
|
||||
4. **漂移防护**:做首答锁定加偏离即停(建议),还是这一轮先不做、继续靠「盘中不跑
|
||||
push-pool」的纪律?
|
||||
不设人为上限之后,这三个数替代了「每天开几只」这个旋钮,用来判断规则要不要调:
|
||||
|
||||
1. **每天有几只候选落在自己的买入区间内**——这是实际的建仓速度,
|
||||
也是「让系统自己判断」到底判成什么样的直接答案。
|
||||
2. **研判的驳回率**——驳得太狠说明 OPEN 判据写窄了,一条不驳说明写宽了。
|
||||
3. **`intraday_exec` 一跳的耗时分布**——预算够不够,要不要开研判独立队列。
|
||||
|
|
|
|||
Loading…
Reference in New Issue