tradingSystem/DEVLOG.md

298 lines
19 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。
> 容器时钟是 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 项目目录。
**部署方式**
不涉及。
**真机判收**
未判收(尚未动码)。
**还欠着什么**
方案第十节的四件待拍板:新建仓的档位怎么给、每天最多新开几只、研判闸这一轮做不做、
盘中参考位漂移要不要加防护。拍板之后才动手。
---
## 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 · 决策系统的买入信号一直被 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`
要补新建仓这一路(研判闸第五类动作、硬数字裁剪、择时侧零改动)。
---
<!--
下一条节点从这里往下写,格式照抄上面:
## YYYY-MM-DD · 一句话标题
**做了什么** / **动了哪些文件** / **部署方式** / **真机判收** / **还欠着什么**
-->