tradingSystem/DEVLOG.md

555 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 开发节点记录(三系统量化交易)
> 这份文件的用途只有一个:**让下一次交接不必重新查一遍**。
> 每个重要节点写一条,写完就补,不攒到最后。一条节点包含五样东西:
> 做了什么、动了哪些文件、部署方式、真机判收结果、还欠着什么。
>
> 三个系统的部署方式(改错等于白改):
> - **tradingSystemPMS** 跑在 `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。
> - **akg-factor-bridge因子桥** 在 `factorevaluation`,容器名 `akg_factor_bridge`
> API 在 8300常驻 API 改码要 compose restart。
>
> **时区2026-08-06 查实、2026-08-07 再证一次,这里写的以实测为准)**
> 不能按「哪个系统」一刀切,要按「哪种时间」分:
> - **容器时钟与容器日志正文** —— 北京时间,**不要加八小时**。
> 证据bionic 容器内 `date` 为 `15:37 CST`。
> - **数据库的时间列**`strategy_audit_log.created_at`、
> `strategy_daily_results.updated_at` 等)—— **UTC要加八小时**。
> 证据08-07 北京 09:03 时该库 `SELECT NOW()` = `2026-08-07 01:03:32`。
> - **日期列**`audit_date`、`trade_date`)—— 北京日期,不换算。
> - `docker logs -t` 自己加的那个时间戳前缀是 UTC跟正文里 Python logging
> 打的时间不是一个口径,别混着读。
>
> 记录纪律:判收状态只写「真机见过」或「未判收」两种,不写「应该没问题」。
> 时间一律写北京时间,引用数据库时间列时先加八小时并标注已换算。
---
## 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 项目目录。
**部署方式**
不涉及。
**真机判收**
未判收(尚未动码)。
**还欠着什么**
方案第十节的四件待拍板:新建仓的档位怎么给、每天最多新开几只、研判闸这一轮做不做、
盘中参考位漂移要不要加防护。拍板之后才动手。
---
## 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 侧。
---
## 2026-08-06 · 新立一条规矩:动 tradingSystem 以外的系统之前先答三题
**做了什么**
用户指出决策系统本来就不管仓位(它是聚合大量信号的判断系统),往里面塞仓位信息
会把别的在跑的逻辑带偏。据此立了一条通用规矩,并按它把方案重过一遍,改成 V3。
**规矩(改 tradingSystem 以外的系统,动手前先答,答不上就别动)**
1. 这段代码是不是只有我这条路径会走到?共用的提示词模板、裁决解析、上下文组装、
配置项,都不能在里面加只对我有意义的分支。
2. 我加进去的字段,会不会被别的逻辑读到并当真?尤其是写共享表、共享缓存、
往共享 context 里塞键。
3. 我的东西全删掉,原系统能不能一字不差地回到今天?答不上「能」就不是加法。
**按规矩核过的结论**
决策系统这三处改动全部落在两段 `if direction == "PMS_JUDGE":`
`workers/tasks_intraday.py` 439 到 523 行、632 到 656 行),不碰共用的提示词模板
557 到 576 行、共用的裁决解析579 到 590 行、资金与板块注入530 到 555 行);
不写 `decision_ledger`;不加 settings 字段;留痕仍只有 PMS_PASS / PMS_REJECT /
PMS_UNAVAILABLE 三个值,而 `pms_advisor._today_exit_verdict` 第 102 行明确跳过
`PMS_` 开头的行,所以写进去的东西不会反过来影响择时。第三题答案是能,`git revert` 一次。
**额外补的一条:仓位数字不进研判请求**
`judge.request` 现在把 `hard_numbers` 整个塞进请求体,决策系统会逐键渲染进提示词。
新建仓的 hard_numbers 里有名额、可投金额、批次比例——等于当面请一个不该管仓位的系统
去看仓位。所以在 PMS 侧加键白名单,只对 OPEN 生效:送现价、分数、主题、档位、
预期空间、热度、名次;不送名额、金额、批次、组合占比。账本照旧存全量。
**重过一遍还改掉与补上的(详见方案 V3 第三节)**
改掉两处:去掉 `PMS_OPEN_TARGET_PCT` 与建持仓行(改由首次成交时既有的
`ensure_position` 自然建行,省掉僵尸空行的清理);去掉 `PMS_OPEN_MIN_SCORE`
(与候选池的 `PMS_PLAN_MIN_SCORE` 重复)。
补上六处,其中三条是真问题:
- **候选之间要滚动扣减**。`_route_one` 每条各自用本轮开始时的旧快照算上限,
一跳内五条各自不超、加起来超,规则闸拦不住。命令那条路有 `_ctx_after` 滚动,
自主这条没有。滚动放进 `scan_open`,复用 `planner.check_all_caps`
- **研判驳回要做当日去重(只对 OPEN**。`_rejected_today_keys` 只读 arbiter='rule'
研判驳回不在里面,`_inflight_keys` 也认不出,于是被驳回的候选每分钟往账本写一行
一模一样的记录——正是 07-29 那个把判分锚淹掉的教训。
- **可投金额要用真实可用资金封顶**,并加 `PMS_OPEN_REQUIRE_WS_CASH`
拿不到资金快照就不自动新建仓。规则闸那条「拿不到资金只告警不拦」是为已排好的命令
设计的,不适合无人值守地从零建仓。
另外三条:行业名一次批量取本轮复用;研判预算用尽的候选整条跳过不入人工队列;
新建仓退实现B 时择时判据只剩均价一条(已知降级,要在理由里写明)。
**一条顺带发现、但这次不夹带的**
`industry.get_many``gp_stock_category` 时是逐只查库、没有缓存,
可以照 `_hybk_many` 加按日缓存。收益是每跳少几十次查询,但它改的是既有函数的时序行为,
按上面第 1 题的规矩不夹带,要做单独一条、单独拍板。
**动了哪些文件**
无代码改动。`NEW_POSITION_ACTION_PLAN.md` 更新为 V3。
**部署方式**
不涉及。
**真机判收**
未判收(尚未动码)。
**还欠着什么**
同上,等点头开工。
---
## 2026-08-06 · 研判闸第一次真发请求就全军覆没:请求体里的 Decimal
**这是今天最要紧的一条。**
三条新建仓提议进了人工确认队列,档位明明是 `full`。查提议的研判理由,三条一模一样:
```
研判: UNAVAILABLE | 研判请求失败: TypeError: Object of type Decimal is not JSON serializable
```
**源头**`proposal_service._recent_ledger()` 把评审账本流水塞进研判请求的 context
其中 `"price": r.get("price_at")` —— `pms_action_ledger.price_at` 是数据库的 DECIMAL 列,
读出来是 Python 的 `Decimal``requests.post(json=payload)` 序列化不了,直接抛 TypeError。
**危害是渐进的,这才是它可怕的地方**:只要一只票在评审账本里有过**任何一行**
它的研判请求就必然失败;而账本只会越积越多,于是过几天几乎所有票的研判都会失败,
全部降级成人工确认——「不用每天靠人」这件事会一点点失效,
而每一条看起来都只是「研判不可用,降级人工确认」这种系统里天天都有的正常降级,
不会有人觉得不对。今天只撞上三只,是因为多数候选还是第一次出现在账本里。
**为什么今天才暴露**:研判闸此前从没真正接通过——`PMS_JUDGE_API_BASE` 一直是空的,
`judge.available()` 为假就直接返回不可用,请求体根本没构造过。
今天是它第一次真发请求,第一次就撞上。这也意味着接口契约 §6 里 #10 那条判收
(「评审账本出现 arbiter=judge 的行」)今天才第一次真正达成。
**降级逻辑救了场**:错误没有被吞掉——报错、回 UNAVAILABLE、入人工队列、理由写进提议
一路留痕,所以一条查询就看见了。这正是「拿不到意见绝不当成通过」那条纪律的价值。
**改法**`judge.jsonable()` 递归把 Decimal 换成 float、日期换成字符串
在请求出口统一拦一道。修在出口而不是修 `_recent_ledger` 一处,是因为请求体的任何一层
将来都可能再冒出数据库类型——一次拦住比每加一个字段就想一次靠谱。
`_recent_ledger` 那边也顺手把 `price` 转成 float让日志与留痕里的数字也干净。
择时那条路(`exec_advisor._advice`)目前不受影响:它的 refs 与 position 都取自
`positions_view()`,那里每个数值都过了 `float()`/`round()`;哪天它也报同样的错,
`jsonable` 的写法在它的 `_post` 前包一层即可。
单测里放了一条**复现用例**:用 Decimal 与 date 造出当天的现场,转换后必须真的
`json.dumps` 得出来。另加一条源码检查,钉住 `_recent_ledger` 的 float 转换。
全量单测 **ALL SUITES PASS469 例**
**顺带一条结构性发现,还没处置**`300570.SZ` 是今天唯一一只既在候选池、
又被决策系统判过盘中转多的票,它倒在规则闸的 `NO_CHASE_MA5` 上(距 MA5 12.22%,上限 6%)。
而那 67 条转多理由里满是「放量突破」「量比 8.55」「站上关键位」——**按定义就是脱离均线的形态**。
所以「信号只做加速器、其余闸门一道不放宽」这个口径,在结构上就注定收效甚微:
就算把候选池调大、重合率提上去,信号票也会成片倒在不追高那道闸上。
要让信号真正有增量,只能是口径三(信号票在不追高或择时上放宽一档),
而那要碰「不追高、接受买不上」这条纪律,必须先拍板。**本轮不动。**
---
## 2026-08-06 · 新建仓盘中真跑了 40 分钟,第一份实测;跳过原因改成分得开
**这一条是实测记录,不是设计。** 新建仓在今天下午约 14:07 起真跑了四十分钟
(部署时间比原计划提前,是在盘中做的)。
**研判判据生效,方向对。** 评审账本里 11 条 `arbiter=judge、action=OPEN、verdict=REJECT`
理由**每一条都在谈「开新仓」**——「建仓缺乏资金支持」「开新仓风险过高」「新建仓证据不足」,
没有一条把它当成加仓在谈;而且确实用上了决策系统自动补的实时资金分布(主力净流占比、
主散背离)去对照「把这只票挑出来的驱动到今天还成不成立」。判收第一步通过。
**当日去重立竿见影。** 驳回集中在 14:07 到 14:38 这 31 分钟,扫完一遍候选池之后就静默了。
没有这道去重,这 22 只会每分钟重送一次研判,一个下午上千次大模型调用与上千行账本。
研判速度也对得上预期31 分钟十几只,正是 150 秒预算下一跳一到两条的节奏。
**今天一股没建,驳回率 100%。** 多条理由提到「宏观空头环境」「市场空头相位」「熊市共振」
「下降通道」,今天本来就是空头,全驳回大概率是合理判断,不是判据写窄。
**但埋了一处要盯**OPEN 判据末尾那句「这是开新仓不是加仓,没有既有仓位做缓冲,建错了
代价全额承担,标准应当比加仓类更严,不确定就 REJECT」加上边界声明里的「宁可错杀不可
错放」,**是双重保守**。若接下来两三天在正常或偏多的市场里仍接近全驳回,就是这句话把模型
推过头了,要调轻。一天的数据不足以判断。
**这份实测暴露的可读性问题,已改**22 条跳过全写着同一句「已有在途提议或指令, 或今日已被
闸门拒过」,把四种完全不同的处境揉成一句——有在途提议、有在途指令、今日被规则闸拒过、
今日被研判闸驳回过。前两种明天照样被挡,后两种日切就重新评估,而它们长得一模一样,
看的人判断不了这只票明天还会不会再被评估。这违反了 `scan` 入口自己立的那条规矩:
「什么都没发生」和「明着跳过了」必须是两回事——现在虽然明着跳过了,却说不清为什么。
改法:`skip` 从集合改成 `{(代码, 动作): 原因}` 字典。`in` 的语义不变,
新增 `action_engine.skip_why()` 取原因,**传集合进来仍然工作**(退回一句兜底话,
既有单测与临时调用不受影响)。三个来源各写各的原因,在途的排在最后覆盖被拒的——
一只票既有在途指令又今天被拒过时,「有在途」更贴近它此刻的状态。
在途那两条还带上提议号、指令号与状态,让人从这一行就能接着往下查。
**动了哪些文件**`app/core/action_engine.py``skip_why` 与两个扫描入口)、
`app/services/proposal_service.py`(三个来源改成带原因的字典);
测试 `scripts/test_batch12_units.py` 加 3 例,`scripts/test_batch4_units.py` 与
`scripts/test_wiring.py` 各改一处断言(原本钉着那句笼统话的字面量)。
全量单测 **ALL SUITES PASS467 例**
**一条要更正的承诺**:先前说过「既有 `scan()` 一个字不动」,这次动了它——为的是让跳过留痕
说得清。`in` 的语义与向后兼容都保住了,但这个承诺本身要如实更正,不该含糊过去。
**我在这一轮犯的一个误判**:账本里 14:38 有两条对没持仓票的 `PMS_REJECT`,我据此推断
「新建仓还没上线,所以不可能,应该是探活脚本」——实际是新建仓已经部署并在跑。
**推理没落地就当结论**,与 07 月那次 `audit_date` 的教训同型。该先查账本再下判断。
---
## 2026-08-06 · 收盘后四条只读诊断,外加一个由它们暴露出来的窗口末日追高
**四条诊断的结论**
1. **买入信号 67 条,落在候选池里的只有 1 只**`300570.SZ`)。
`intraday_signals:2026-08-06` 共 73 条,动作分布 BUY 67 / SELL 6。
说明**决策系统的盘中转多判断与上游选股计划的排序是两套几乎不相干的视角**——
互补而不是互相验证。所以「必须同时在候选池里」这条口径在 `PMS_PLAN_TOP_N=30`
的当前参数下,插队排序的增量只有 1/67基本无效。
顺带日志说破另一件事:`主榜正好吃满 top=300 (打分池 440)`,上游还有 140 只没拿到。
两条改法(调大 `PMS_PLAN_TOP_N`,或让只被 top_n 纯排序截断切掉的信号票凭信号捞回来)
记在下面「还欠着什么」里,等明天有真实留痕再定。
2. **`trading_buy_plan` 最新一行是 07-30七天没动。** 四个状态全是旧数据 →
老的建仓链路是死腿,**不存在两套系统同时买的风险**ENTRY_GATE 关不关都行。
3. **悬项四定案:不是口径差。** 差额 56408-05→ 5108-06 盘中)→ **4.00 元**(收盘后),
递减到接近零就是取数时点差加收盘价精度差4 元对 177,561 的持仓市值是 0.002%)。
**但「`cash_avail` 里 `+ sell_return_today` 双算」这个假设今天没被验证,只是没被触发**
——今天零卖出成交,那个加法加的是 0。真正的验证要等有卖出成交的那天。
4. **悬项八有结论:`trade_no` 是随机串**`T-SHADOW-420911c4b8`),不是确定式,
S3 收尾函那个阻断项**不闭环**。后果是幂等:同一笔成交若被重推,随机串识别不出来。
得确认回放那侧的去重键用的是 `trade_no` 还是单调的 `seq`
**由诊断暴露出来的问题:窗口末日会强制追高,而自主买入不该这样**
`ws_smoke inbox``000063.SZ` 那五笔成交是今天 **14:46**时间戳换算1700 股,
成交价 34.71 到 34.76。而交接信写的是「现价高于买入区间上沿,**不追**,窗口今天到期」。
判了不追,最后还是买了——走的是 `exec_timing.hard_gate` 买入分支的窗口末日强制完成,
`hard_gate``exec_advisor.decide` 里是**先于**咨询决策系统跑的,
14:45 之后根本不问择时,直接按 现价×1.002 追进去。
那一笔是命令驱动的(`INS_20260804_000063SZ_OPEN_003`**强制完成是对的**
用户下过「投这么多」的命令,到期必须完成。但同一段代码等新建仓上线就会作用在自主提议上,
那时它与「可以接受买不上」直接冲突,而且是无人值守。
更要紧的是 `window_verdict` 早就把口径写死了:`"PARTIAL" if is_command else "EXPIRED"`
——**自主类窗口耗尽直接作废**。既然到期就作废,就不该在到期当天先被强制完成一遍。
两处本来是矛盾的,这次是把它们对齐,不是新增策略。
**改法**
- `exec_timing.hard_gate` 新增 `is_command`**必填、故意不给默认值**——漏传立刻
`TypeError`,不许静默按某一侧走。买入的窗口末日强制完成只对命令生效,自主返回等待
并写明「到期作废」。
- **卖出侧一个字没改**,两种来源都照常兜底。这不是漏改:买入的强制完成是「多背一份风险」,
卖出的是「少背一份风险」;自主减仓若也到期作废,那是把该降的风险留在账上。
宁可买不上,但不能卖不掉。
- `exec_timing.decide``exec_advisor.decide``is_command` 默认 `True`
(命令口径 = 改动前的行为),`executor.run_tick` 从指令的 `progress.is_command` 显式传下来。
- 影响面说破:**已有的 FILL / ADD / DCA 自主买单也跟着改**——从前也会在末日强制完成,
现在到期作废。「回踩补足」在末日追高买本身就自相矛盾,所以对它们同样是修正,
但确实是既有行为的改变。
**动了哪些文件**
`app/core/exec_timing.py`、`app/services/exec_advisor.py`、`app/services/executor.py`
测试 `scripts/test_batch12_units.py` 加 6 例含一条「executor 真的把 is_command 传下去了」
的签名与源码检查,防这道分岔形同虚设),`scripts/test_batch11_units.py` 的夹具补一个参数。
全量单测 **ALL SUITES PASS464 例**
---
## 2026-08-06 · 决策系统的买入信号一直被 PMS 丢在门口,已接上
**做了什么**
用户问「新建仓能不能并入决策系统原有的信号链路」。查下来先纠正了一个前提,又撞出一个洞。
**先纠正的前提**:决策系统的**建仓侧本来就是轮询**,不是订阅。
`workers/celery_app.py:94-97``entry-gate-poll``crontab(minute="*", hour="9-11,13-14")`
每分钟去 `trading_buy_plan``is_active=7`。真正事件驱动的是告警链
`scripts/intraday_watcher.py` 常驻进程 `xreadgroup` 读上游告警流)与卖出链。
所以「PMS 只能轮询不合理」对既有架构不成立——候选池是日频静态清单,没有事件可订阅,
轮询是它唯一的读法,而且与决策系统自己的建仓入口同构。
**撞出来的洞(与 08-05 那次同型)**:决策系统盘中判出 `REVERSAL_BUY` 会往 db2 的
`intraday_signals:{日期}` 广播(`workers/tasks_intraday.py:783-785`**PMS 一直订阅得到**
`signal_service.streams()` 第一条就是它,每分钟拉一批),但走到
`signal_rules.digest`(第 106 到 108 行)被归进 `ACT_RECORD`,而
`signal_service._handle` 的 RECORD 分支**只给持仓票写账本**。
于是「决策系统今天看多了某只没持仓的票」——**正是新建仓关心的那一批**——PMS 收到了、
计了个数,然后一个字都不留:账本查不到、页面看不见,事后复盘问「那天系统看见了吗」答不上来。
当年那句注释的理由(「买什么买多少由动作引擎决定」)在动作引擎没有新建仓动作时成立,
现在失效了。
**改法(口径:信号只做加速器,不改资格)**
- `signal_rules.py`:新增 `ACT_NOTE_BUY`。BUY 信号**仍然不产生任何买入动作**
但持没持仓都要留痕。HOLD 等其余类型口径一个字未动。
- `signal_service.py`:新增 `ACT_NOTE_BUY` 分支,落 `action=SIGNAL_BUY`、`verdict=NOTE`
的账本行,按(日期,来源,股票,动作)当日去重。用 `NOTE` 不用 `PASS`——
「记下来」和「放行」在账本里必须分得开。
- `pms_repo.buy_signals_today`:只读,今天有转多留痕的票。
- `proposal_service._scan_open``action_engine.scan_open`:有信号的候选**排最前**。
**只影响先后,不影响资格**——不在候选池里的票不会因为有信号就被建仓,
候选层那一整套过滤ST、黑名单、分数下限、主题限额、预期空间一道都不绕。
- `judge.OPEN_JUDGE_KEYS` 加两个键,把「你自己今天判过这只票转多」送回决策系统,
让它拿自己的结论对照一次。这是定性材料不是仓位数字,符合那条白名单的立意。
- 新参数 `PMS_OPEN_SIGNAL_PRIORITY`(默认开),关掉即退回纯分数排序。
**为什么走账本传递而不是让 signal_service 直接调建仓**
两边各管各的一件事:信号消化管「收到了、记下来」,动作引擎管「买不买、买多少」,
中间靠账本这个既有事实源接。不新增跨模块调用、不复制闸门逻辑。
代价是最多差一分钟(两个调度位各自每分钟一跳),而后面还要等择时区间,这点延迟无所谓。
**插队为什么是实打实的增量**:候选按分数降序取,名额只剩两个时第 25 名永远轮不上,
哪怕决策系统刚刚判它转多。分数是昨夜算的静态排名,「此刻转多」是盘中才有的新信息,
两者不同量纲,折算成分数得凭空定系数;插队直接表达了这件事。
**动了哪些文件**
`app/core/signal_rules.py`、`app/services/signal_service.py`、`app/repo/pms_repo.py`、
`app/services/proposal_service.py`、`app/core/action_engine.py`、`app/services/judge.py`、
`config/settings.py`、`app/services/param_store.py`;测试 `scripts/test_batch12_units.py`
加 7 例,`scripts/test_batch5_units.py` 与 `scripts/test_wiring.py` 各改一例
(那两例原本钉着 BUY→`ACT_RECORD` 的旧行为,「不买」这条口径没变,变的是留痕范围)。
全量单测 **ALL SUITES PASS458 例**
**还欠着什么**
1. `trading_buy_plan` 四个状态都有行,说明老的建仓链路还活着。**关掉 ENTRY_GATE 之前
必须先确认那些行是不是今天的**——若老链路今天仍在挂单,而 PMS 同时开始建仓,
就是两套系统都在买。查法见交接说明。
2. 关 ENTRY_GATE 是往决策系统 `.env``ENTRY_GATE_ENABLED=False`
**这是新增 env 值,必须 `docker compose up -d --force-recreate``restart` 不重读 env。**
3. 口径三(信号票在择时上放宽一档)本轮没做,要碰「不追高、接受买不上」那条纪律,
等积累一段实证再谈。
---
## 2026-08-06 · 新建仓动作:两侧代码写完,容器内全量单测通过,待实机部署
**做了什么**
按 V3 方案把两侧代码写完了。容器里拼了一份可运行的副本跑全量单测:**ALL SUITES PASS
451 例**(新增的第十二批 26 例 + 既有 425 例,含装配自检 58 例)。**尚未在实机部署。**
**动了哪些文件**
决策系统(`bionic_trader`一个文件48 增 4 删,三处全在 `if direction == "PMS_JUDGE":` 块内:
- `workers/tasks_intraday.py``_task_by_action` 加 OPEN 建仓判据(含必答「把这只票挑出来的
驱动到今天还成不成立」OPEN 且现价缺失时块内早退回 `UNAVAILABLE`(加仓类有摊薄成本可依,
建仓没有现价锚就是盲判);边界声明按动作分岔(原句里的「配额」对新建仓不成立)。
持仓系统(`tradingSystem`)九个文件:
- `app/core/action_engine.py`:加 `A_OPEN` 并进 `JUDGE_ACTIONS`;新增 `eval_open`
`scan_open`。**既有 `scan()` 一个字未动。**
- `app/services/proposal_service.py`:新增 `_scan_open`(候选池取数、批量取行业、
真实资金封顶);`_route_one` 加新建仓特判与档位分支;研判时间预算。
- `app/services/judge.py``OPEN_JUDGE_KEYS` 键白名单,新建仓只送定性材料。
- `app/services/exec_advisor.py`:存 `observed` 的支撑压力区间;`_check_ref_drift` 首答锁定、
偏离即停。
- `app/services/executor.py`:等待分支看到 `ref_drift` 落一条账本 WARN**一天一条**。
- `app/repo/pms_repo.py`:新增只读 `judge_rejected_today``opened_names_today`
- `app/services/param_store.py`:四个新参数的说明、校验、`FAIL_CLOSED`。
- `config/settings.py`:四个新参数;`PMS_JUDGE_ACTIONS` 默认值加 `OPEN`
- `scripts/test_batch12_units.py`26 例)与 `scripts/run_tests.py`(登记新批次)。
**写代码过程中改掉的一条设计(单测逼出来的)**
原方案的取数口径抄的是命令驱动那条路:`want = min(单股目标, 剩余额度)`。单测发现它会在
可投金额只剩一万时开出一只 0.5% 的零头仓位——**拿一个持仓名额换一个永远补不到目标的半截仓,
还挡住了后面真正建得起来的票**。改成「钱不够一整只就不开」,并把与命令那条路的差别写在注释里:
命令是用户明确下了「投这么多」,最后一只缩水是命令的收尾;自主建仓没有这层意思。
**部署方式**
- 决策系统:不加配置、不动 `.env``git pull``docker compose restart backend-api worker-brain`
- 持仓系统:源码打进镜像 → `make deploy`,跑完 `make test` 要见 ALL SUITES PASS。
**新建仓档位默认就是 full自动执行`make deploy` 一跑完下一个整分钟的心跳就可能真建仓,
所以务必收盘后部署**,让第一次真实运行发生在次日开盘、人在场的时候。
**真机判收**
未判收。判收步骤见 `NEW_POSITION_ACTION_PLAN.md` 第十节,顺序是决策系统先上并手工发一条
`action=OPEN` 的研判请求,确认理由是在谈从零建仓而不是加仓、且请求体里没有仓位数字。
**还欠着什么**
1. 实机部署与八步判收。
2. 第一周要盯的四个数:每天有几只候选落在买入区间内、研判驳回率、`intraday_exec` 一跳的
耗时分布、评审账本每天的行数。
3. 未夹带的一条:给 `industry.get_many``gp_stock_category` 分支加按日缓存
(每跳省几十次库查询,但改的是既有函数的时序行为,单独提、单独拍板)。
4. 文档补账仍欠着:`WS_INTEGRATION_STATUS.md` 停在 07-30`BIONIC_PMS_INTERFACE.md`
要补新建仓这一路(研判闸第五类动作、硬数字裁剪、择时侧零改动)。
---
## 2026-08-07 · 股票评述里冒出「【手动重跑AKG_PLAN】」运维口径的话被当成风控质询
**做了什么**
页面上一些股票的评述里出现了「【手动重跑AKG_PLAN】」这种谁也看不懂的字段。第一反应会怀疑
是前一天新建仓那批改动,但**不是**:三个仓库全文检索「手动重跑」只有一个出处,
`bionic_trader/scripts/rerun_pool.py` 第 237238 行。链条如下(全部是源码,不是推测):
1. `rerun_pool.py` 下发 `analyze_strategic_plan.delay(c, reason, None, target_date)`
**第二个位置参数不是留痕字段,是 `reviewer_feedback`** —— 风控官质询通道。
2. `tasks_brain.IntradayClassifier.classify()` 第一道判断是 `if "【盘中" not in text: return None`
「【手动重跑】」不带这个前缀,返回 None。
3. 于是掉进 `elif reviewer_feedback:` 分支,被拼进提示词的
`[📢 CRITICAL CHALLENGE FROM RISK OFFICER]` 段,后面跟着
`You MUST address this in your analysis`
4. 那是一句命令。大脑照办,把这句运维口径的话写进了 `analysis`
5. `generate_chinese_report``save_to_database` → 覆盖进
`strategy_daily_results.analysis_summary`,也就是页面上的评述。
**这条真正的教训不是那个字段,是它顺带证实的悬项二。** `rerun_pool` 落库是
`INSERT ... ON DUPLICATE KEY UPDATE`,把 `signal_type / support_level / pressure_level /
analysis_summary` 一起就地覆盖。查实的落库时刻(库时钟是 UTC下面已换算成北京时间
| trade_date | 行数 | 落库时刻(北京,已换算) | 当时是什么时候 |
|---|---|---|---|
| 20260806 | 16 | 08-06 22:32 → 22:45 | 收盘后,安全 |
| 20260805 | 18 | 08-05 22:30 → **08-06 11:04** | **盘中**,改写的是当时正被读的昨夜结论 |
| 20260804 | 11 | **08-05 13:04** → 08-07 00:50 | **盘中**,补录历史日 |
08-06 早盘 11:04 与 08-05 午盘 13:04 各有一次盘中重跑。而这张表下游有两个人在读:
bionic 的 `pms_advisor`(支撑压力位算 PMS 买卖区间、`signal_type` 走 `BAD_Y_SIGNALS` 闸)、
`pool.py``BAD_SIGNALS`(决定把哪只票请出候选池)。所以「盘中不跑 push-pool」这条纪律
不只针对桥,`rerun_pool` 同样在内 —— 这次是拿实测把它钉死了,不再是推测。
顺带记一条待查的:`SZ000800` 现在有两行相互打架的定性,`trade_date=20260806` 是 `SELL`
08-06 22:38 写入),`trade_date=20260804` 是 `DROPPED`08-07 00:50 写入,晚了两个多小时)。
补录历史日会写出一行「时间上更新、日期上更旧」的结论,取数方按 `trade_date` 排还是按
`updated_at` 排会拿到不同答案。两边取数口径要对一遍,这次没做。
**改法(两处,缺一不可)**
- **根治** —— `rerun_pool.py` 下发时第二个位置参数固定传 `None``--reason` 保留,但改成
只印在脚本自己的输出里给人看。
- **兜底** —— `tasks_brain.py``_is_ops_note()` 前缀护栏,认
`【手动重跑】/【运维】/【回补】`,命中就先打一行日志再把 `reviewer_feedback` 置空。
为什么两处都要:`rerun_pool` 的用法示例第 2 条明写着鼓励人手填
`--reason "调整X因子后重跑"`,只堵住默认值,下次手填照样中招。
护栏落点选在 `analyze_strategic_plan` 打完那行「🧠 Brain 正在介入」日志之后、
`IntradayClassifier.classify` 之前。**先打日志再丢弃**是有意的:「谁在什么时候用什么理由
重跑了这只票」这条线索要留在日志里,它只是不该进大模型的证据。拦在源头一次就够,因为
下游用到 `reviewer_feedback` 的只有 classify 和风控官质询段两处。
**已经被污染的评述救不回来**:就地覆盖,没有历史表。要洗只能改完代码后按同一个池重跑一遍
让它再覆盖一次,而那又是一次全量重算、支撑压力位会再动 —— 只能收盘后做,别在盘中洗。
**动了哪些文件**(都在 bionic_traderPMS 侧一个字没动)
- `scripts/rerun_pool.py` —— 下发传 `None``--reason` 改为本地留痕;文件头补两段说明
(「什么时候不能跑」「为什么 --reason 不再下发给大脑」)。
- `workers/tasks_brain.py` —— 新增 `_OPS_NOTE_PREFIXES``_is_ops_note()`
`analyze_strategic_plan` 里加四行护栏。既有两条路径(盘中事件、常规风控意见)零改动。
- `scripts/selftest_ops_note_guard.py`新增24 项)—— 不 import `tasks_brain`
(它拖着 milvus/llm靠 AST 把 `_is_ops_note` 从源码里抠出来单独执行做真实断言,
再用 AST 校验护栏落点与 `delay` 的第二个实参。做过三次变异验证,确认它真的会失败:
说明文字塞回去、护栏只打日志不置空、前缀写宽一个字,三次都被抓住。
**部署方式**
决策系统那台:`git pull` 后 `docker compose restart worker-brain`。没有新增 env
不需要 `--force-recreate`。**只重启 worker-brain 就够**`backend-api` 没动。
**真机判收**
未判收。判收两步:① 容器里跑一次
`docker exec trader_worker_brain python scripts/selftest_ops_note_guard.py` 见「全部通过」;
② 收盘后拿任意一只票跑一次 `rerun_pool.py --group AKG_PLAN --limit 1 --reason "护栏验证"`
确认 worker 日志里有「🛡️ 运维口径说明已从提示词中剔除」这一行,且该票新落库的
`analysis_summary` 里不含「手动重跑」。
**还欠着什么**
1. 上面两步真机判收。
2. `SZ000800` 那种「补录历史日写出时间更新、日期更旧的行」,取数方按 `trade_date` 还是
`updated_at` 排要对一遍口径(`pms_advisor` 与桥 `pool.py` 两处)。
3. 已污染的三批评述16/18/11 行)要不要收盘后洗一遍,没定。
4. `rerun_pool.py``--dry-run` 的中文说明写的是「干跑」,项目里同类东西叫「试算」,
措辞不统一。这次没改,因为跟本条改动无关,单独提。
---
<!--
下一条节点从这里往下写,格式照抄上面:
## YYYY-MM-DD · 一句话标题
**做了什么** / **动了哪些文件** / **部署方式** / **真机判收** / **还欠着什么**
-->