From a78e737e740d857f269088b0b087cd284417f431 Mon Sep 17 00:00:00 2001 From: zlt Date: Thu, 17 Sep 2026 09:47:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B9=E6=A1=88=E5=89=AF=E6=9C=AC=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=EF=BC=9A=E9=80=89=E8=82=A1=E8=AE=A1=E5=88=92=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=96=B9=E6=A1=88=E4=BF=AE=E8=AE=A2=EF=BC=9A=E8=B4=A8?= =?UTF-8?q?=E5=9C=B0=E8=BD=B4=E4=B8=8B=E6=97=A0=E4=BC=A0=E5=AF=BC=E8=A1=8C?= =?UTF-8?q?=E5=85=8D=E4=B8=BB=E9=A2=98=E9=99=90=E9=A2=9D=E3=80=81=E5=85=A5?= =?UTF-8?q?=E6=B1=A0=E4=B8=8D=E5=8F=98=E5=BC=8F=E4=B8=8E=E5=A4=87=E7=94=A8?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E3=80=81=E6=8C=81=E4=BB=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=8D=8F=E5=90=8C=E9=A1=B9=E7=BB=86=E5=8C=96?= =?UTF-8?q?=E3=80=81=E6=8E=A5=E5=8F=A3=E5=AE=B9=E5=99=A8=E5=90=8C=E7=AA=97?= =?UTF-8?q?=E9=87=8D=E5=90=AF=E3=80=81=E9=A3=8E=E9=99=A9=E4=B8=8E=E9=A2=84?= =?UTF-8?q?=E5=88=A4=E4=B8=80=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/选股计划调整方案_2026-09-17.md | 52 ++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/选股计划调整方案_2026-09-17.md b/docs/选股计划调整方案_2026-09-17.md index 01e03d8..8e931bf 100644 --- a/docs/选股计划调整方案_2026-09-17.md +++ b/docs/选股计划调整方案_2026-09-17.md @@ -56,6 +56,8 @@ 3. 传导在这里只剩两个作用:被指向与否是带内第一个次级键;强弱档位不再进次序。原始分与档位标签照旧输出,只是不再决定名次。 4. 新增四个字段:`quality_band`、`rank_axis`(取值 quality 或 transmission)、`rank_old`(同一行在旧规则下的名次,供周报对照,四周后可以停)、`pointed`(被指向与否,从候选卡门槛一带出)。`score`、`score_adj`、`tier` 三个既有键的值与含义不变。 5. 开关 `PLAN_RANK_AXIS`,默认 `quality`;设 `transmission` 时走 09-14 那套(质地加减八分、不跨档),逐字回旧,单测钉住。 +6. 主题限额的一处例外。截断函数把没有传导主题的行归进"无传导"一个桶,每桶最多五只。旧序下无传导排在主榜末尾,碰不到这条限额;质地序下前二十有不少质地好的无传导票,若仍受五只上限,第六只起就被质地中的被指向票顶掉,主轴被限额悄悄推翻。所以质地轴下无主题的行不受主题限额,有主题的照旧每主题五只。限额防的是单板块刷屏,无传导不是板块。`transmission` 轴下原样。 +7. 逐票对账接口里"主榜第 N 名"仍按原始分序,它的语义就是 `rank_old`,应答里加一键 `rank_axis` 写明是 transmission,文案不动。 **为什么是词典序不是加权。** 加权要发明三个新系数,没有读数支撑,也说不清楚。词典序每一层都能在卡上用一句话解释:先看公司好不好,再看有没有被产业链指向,再看还没热且便宜。 @@ -72,7 +74,7 @@ ### 工作包三:择时决策系统的夜间分析池与评析目标名单(`pool.py`、`review_targets.py`) -1. 入池来源加一个取值 `POOL_SOURCE=rank`:从主榜新序里取被指向的行,按名次截前 `POOL_TOP`(机器上是五十)。原来的 `tier`(只收强传导)与 `candidate` 两个取值保留,回旧用。夜间推理量不变,只是从"强传导前五十"变成"质地优先且被指向的前五十"。 +1. 入池来源加一个取值 `POOL_SOURCE=rank`:从主榜新序里取被指向的行,按名次截前 `POOL_TOP`(机器上是五十)。原来的 `tier`(只收强传导)与 `candidate` 两个取值保留,回旧用。夜间推理量不变,只是从"强传导前五十"变成"质地优先且被指向的前五十"。入池决定谁能拿到吸筹评分,吸筹评分又是候选卡的确认线,所以池子必须与持仓管理系统看到的候选前缀同一个序,否则质地好的票永远缺确认。上线次日核一条不变式:持仓管理系统的候选都在池里。核不过就打开备用开关 `POOL_RANK_KEEP_OLD`(默认关),把旧序强传导前五十并进来,受 `POOL_MAX` 兜底。 2. 评析目标名单改成两路并集:原始分序前一百(跟着雷达走,新被指向的公司先有报告)加新序前五十(好公司的报告保持新鲜),观察档前二十不变。开关 `REVIEW_TARGETS_AXIS`,默认 `both`,设 `score` 回旧。 ### 工作包四:周报对照(`plan_review.py`) @@ -90,7 +92,7 @@ ### 协同项:持仓管理系统(另一仓库,单独审批) -持仓管理系统取候选池时按原始 `score` 降序排,再按基本面立场分桶(`plan_feed.select_candidates`,参数 `PMS_PLAN_RANK_BY_QUALITY` 默认开)。上游次序换成质地后,它拿到的前三十名会变成质地优先,但它内部仍按原始分排。建议它加一个参数:开着"按质地排序"时改按上游的 `rank` 排,让两边次序一致。这条不影响本方案上线,因为它自己的看多分桶已经把好公司排在前面。 +持仓管理系统向选股系统要三百只主榜,再自己按(基本面立场桶、原始分降序、上游名次)重排(`plan_feed.select_candidates`,参数 `PMS_PLAN_RANK_BY_QUALITY` 默认开,立场由它自己的 `fund_rules` 从评析摘要合成)。所以上游次序换掉后,它直接看到的变化只有两处:三百只前缀按新序切,页面上"候选榜第 N 名"的数字变了;它内部仍是两套质地序各排各的。要让质地序真正成为它的候选序,需要它加一条改动:计划带 `rank_axis` 为 quality 时改按上游 `rank` 排,并把自己的立场分桶关掉,三处同源。这条不阻塞本方案上线,因为它的看多分桶已经把好公司排在前面。另有一条要写进它的对接说明:质地序下强传导但质地差的票可能落在三百只前缀之外,那边不要把"强传导票变少"当故障。 --- @@ -100,7 +102,7 @@ |---|---|---| | 09-17 到 09-18 | 三到四 | 开发五个工作包,本地单测全绿,一个提交,交第十节的证据 | | 09-19 | 六 | 我复审 | -| 09-21 | 一 | 收盘后选股系统机器拉代码。计划装配代码不需要重启接口容器,次日 07:10 的计划就是新序 | +| 09-21 | 一 | 收盘后选股系统机器拉代码,次日 07:10 的计划就是新序(盘前链每步都是子进程,读挂载目录里的新代码)。接口容器仍建议同窗重启一次并预热计划接口,经审批、避开七点到七点半与八点半到九点零五:接口进程内存里持着旧的装配代码,JSON 走快照直通没问题,但 md 格式的主榜表与快照读不到时的实时回落会走旧序 | | 09-22 | 二 | 第一份新序计划。看候选单、前三十名、入池名单三处读数,交回 | | 09-25 | 五 | 第一次带两份名单对照的周报(二十点十分由基座触发) | | 10-16 前后 | | 四周复核,按工作包四第四条决定保留或切回 | @@ -119,8 +121,11 @@ | `REVIEW_TARGETS_AXIS` | both | score 回"只按原始分前一百" | | `COMPANY_REVIEW_STALE_DAYS` | 120 | 已有;从此对质地带、质地门、加减分都生效 | | `PLAN_QUALITY_BONUS` | 8 | 已有;只在 transmission 轴下起作用 | +| `POOL_RANK_KEEP_OLD` | 0 | 备用:旧序强传导前五十并进池,只在"池包含候选"不变式核不过时打开 | -机器上的 `.env` 不需要加任何键,默认值就是新行为。回退时才写旧值。 +机器上的 `.env` 不需要加任何键,默认值就是新行为(今天机器上没有设 `POOL_SOURCE`,代码默认值生效)。回退时才写旧值。一条要记住:`.env` 里新加的键子进程能读到,但容器建起时已经在 `.env` 里的键改了值要重建容器才生效。备用开关 `POOL_RANK_KEEP_OLD` 默认关,只在第六节那条不变式核不过时打开。 + +一条说明:一百二十天的新鲜度守卫眼下是空转的,评析 09-09 才上线,最老的报告不到十天,要到明年一月才会第一次命中。现在定口径,不改今天任何一只票的读数。 --- @@ -152,6 +157,17 @@ cd ~/project/akg-factor-bridge && sed -n 1,30p $(ls -t data/plan/plan_*.md | hea cd ~/project/akg-factor-bridge && docker exec akg_factor_bridge python3 run.py push-pool --dry-run 2>&1 | tail -15 ``` +**选股系统机器(上线次日 08:45 后)。** 池子包含持仓管理系统的候选。预期打出的差集为空;不为空就打开备用开关 `POOL_RANK_KEEP_OLD` 并交回读数。 + +```bash +cd ~/project/akg-factor-bridge && docker exec akg_factor_bridge python3 -c " +import json, glob, pool +d = json.load(open(sorted(glob.glob('data/plan/plan_*.json'))[-1])) +cands = {r['code'] for r in d.get('candidates') or []} +in_pool = {r['code'] for r in pool.select_plan_rows(d['main'], 50, 'rank', set())} +print('候选不在池里:', sorted(cands - in_pool))" +``` + **选股系统机器。** 评析目标名单两路并集。预期当日行数在一百到一百五十之间,`source` 为 main 的行里既有原始分前列也有新序前列。 ```bash @@ -202,7 +218,20 @@ cd ~/project/akg-factor-bridge && grep -n "新序前二十\|旧序前二十\|档 ## 十、评审方式 -评审者只看证据。核对步骤:读 diff 对照附录己的文件清单;本地跑两个测试文件;核四个开关都设旧值时的逐字回旧断言是真断言;核卡内序、入池、评析名单三处的次级键与去重;核 `_row` 只加键。上线次日与 09-25 周报后再核第六节的读数。判定三档同前:通过、有条件通过(只差运行样本读数)、退回(单测红、改了既有键语义、排序没有末位键、分数编码被动)。评审记录写 `docs/评审记录_<日期>.md`。 +评审者只看证据。核对步骤:读 diff 对照附录己的文件清单;本地跑测试文件;核四个开关都设旧值时的逐字回旧断言是真断言;核主题限额的例外只在质地轴下生效;核卡内序、入池、评析名单三处的次级键与去重;核 `_row` 只加键。上线次日与 09-25 周报后再核第六节的读数。判定三档同前:通过、有条件通过(只差运行样本读数)、退回(单测红、改了既有键语义、排序没有末位键、分数编码被动、主题限额把无传导票压在五只以内)。评审记录写 `docs/评审记录_<日期>.md`。 + +## 十一、风险与预判 + +上线前写在纸面上,上线后逐条对表。 + +1. 候选单只数基本不变,变的是次序。判决规则一条没动。 +2. 持仓管理系统看到的变化比想象的小,直到它按上游名次排。这是协同项,不阻塞。 +3. 前缀效应:质地差的强传导票可能落在持仓管理系统的三百只前缀之外,与"质地差降关注"同向,写进那边的对接说明。 +4. 入池名单从"强传导前五十"变成"质地优先且被指向的前五十",夜间推理量不变;不变式核不过就开备用开关。 +5. 名单粘性:已评公司只有三百五十九家,新序前二十会被同一批好与中的票长期占据,日间变化只剩带内传导序。周报两序重合率就是这个粘性的读数,评析铺开会稀释它。 +6. 一天滞后:新被指向的公司当天在未评带,评析名单按原始分序把它送给基座凌晨跑,最早次日进好或中带。当天它仍能进池、仍能判候选,可交易口径不受影响。 +7. 盘中重跑:基座每小时出一批评析,中午重跑装配会读到更多报告、带会变。同日重跑一致只对相同输入成立,快照是唯一底本;接口按请求补的评析摘要不改名次。 +8. 逐票对账接口的"主榜第 N 名"仍是原始分序,与计划里的 `rank` 会不一致,加 `rank_axis` 一键说明。 --- @@ -213,7 +242,9 @@ cd ~/project/akg-factor-bridge && grep -n "新序前二十\|旧序前二十\|档 - `_quality_bonus`(约 111 行)与 `_rerank_by_quality`(约 124 行)保留,作为 `transmission` 轴的实现。 - 新增 `quality_band(cr, stale_days) -> int`:读 `overall` 与 `age_days`;缺报告、证据不足、或 `age_days` 大于 `stale_days` 回一;差回零;中二;好三。同一个函数给 `card.py` 用(放在 `card.py` 里由 `plan.py` 导入也可以,取决于哪边不引入循环导入)。 - 新增 `_rerank_by_axis(ranked, cards, axis, stale_days)`:`quality` 轴按(质地带降序,被指向降序,组内分降序,原始分降序,代码升序)排。"被指向"以候选卡门槛一的判定为准,即 `cards[k]["gates"]["pointed"]`(`card.py:106-107`),不能只看传导视图的证据行,因为已启动成员在那张视图里没有证据行。组内分直接从 `score` 反解:主榜分减二百再减档位乘二十就是组内分,档位用 `int((score - 190) // 20)`;观察档没有档位,带内直接按原始分。返回值仍是分数序列,只改顺序,分值不动。 -- 调用点在约 476 到 477 行两处,换成按 `config.PLAN_RANK_AXIS` 分派:`quality` 走新函数,`transmission` 走 `_rerank_by_quality`。 +- 调用点在约 476 到 477 行两处,换成按 `config.PLAN_RANK_AXIS` 分派:`quality` 走新函数,`transmission` 走 `_rerank_by_quality`。质地轴下先用 `_rerank_by_quality` 算一遍旧序拿 `rank_old`(全量序里的名次,不是截断后的),再算新序。 +- `_pick`(约 479 到 492 行)与 `pick_rows`(约 941 到 957 行)加参数 `uncapped_no_theme`:质地轴下无主题的行不计数、不受主题限额;`transmission` 轴下原样。`pick_rows` 缺省时从行的 `rank_axis` 推断,周报重建旧序时显式传假。既有单测 `test_plan_snapshot.py` 里钉住"无主题行同桶受限额"的那条不改,旁边加一条质地轴的对照用例。 +- 返回字典只在质地轴下加 `rank_axis`、`rank_rule`(一句话写清次序规则)、`quality_band_counts`(主榜与观察档各带只数)三键。 - `_row`(约 494 行)加四键:`quality_band`(整数)、`rank_axis`(字符串)、`pointed`(布尔,来自候选卡 gates)、`rank_old`(整数或空;只在 `quality` 轴下填,由 `transmission` 轴再算一遍序得到名次;四周后可以用开关关掉填空)。 - `encoding` 那一句(约 651 行)追加"名次按质地带优先,传导只在带内作次级键,开关 PLAN_RANK_AXIS"。`card_params.rules` 不改。 - `config.py` 加 `PLAN_RANK_AXIS`、`CARD_SORT_BY_QUALITY`、`REVIEW_TARGETS_AXIS` 三个键,`POOL_SOURCE` 的注释补 `rank` 取值,每个键三行注释写为什么。 @@ -221,7 +252,8 @@ cd ~/project/akg-factor-bridge && grep -n "新序前二十\|旧序前二十\|档 ## 附录乙(可不读):`card.py`、`pool.py`、`review_targets.py`、`plan_review.py` - `card.py` 约 202 到 210 行:`cr.get("overall") == "差"` 改为质地带等于零(同一个函数,带一百二十天守卫);`sort_key`(约 247 行)在判决与涨幅之间插入负的质地带,受 `CARD_SORT_BY_QUALITY` 控,关掉时元组与旧实现逐字相同。`judge()` 的返回值加 `quality_band` 一键,`_row` 从这里取。 -- `pool.py` 约 293 到 301 行:`POOL_SOURCE == "rank"` 分支,`plan_rows = [r for r in data["main"] if r.get("pointed")][:top]`,`_pool_source` 记 `rank_top`;`tier` 与 `candidate` 分支原样保留。 +- `pool.py` 约 293 到 301 行:把选行那段抽成纯函数 `select_plan_rows(main_rows, top, source, tiers, keep_old=False)`,离线可测。`rank` 分支:`[r for r in main_rows if r.get("pointed")][:top]`,`_pool_source` 记 `rank_top`;`keep_old` 为真时再并进按 `rank_old` 排的强传导前 `top`,去重,`_pool_source` 记 `rank_old_strong`,总数受 `POOL_MAX` 兜底;`tier` 与 `candidate` 分支逐字保留。入池上下文 `strategy_context_entry` 只加 `quality_band`、`rank_axis`、`rank_old` 三键。 +- `plan_reconcile.py` 的应答加 `rank_axis: "transmission"` 一键,文案不动。 - `review_targets.py` 约 14 到 15 行与 `build_rows`:`persist(date, snap)` 拿到的 `snap["main"]` 已是新序的全部行,每行带 `score` 与 `rank`。`both` 时主榜名单取"按 `score` 降序前一百"与"按 `rank` 升序前五十"的并集,去重后按 `rank` 排;`score` 时只取按 `score` 降序前一百(逐字回旧)。观察档照旧前二十。 - `plan_review.py` 约 378 到 384 行从快照 `_full` 读主榜行;约 419 行附近加两份名单"主榜新序前二十"(按 `rank`)与"主榜旧序前二十"(按 `rank_old`,缺该键的旧快照跳过这两份名单);约 431 到 436 行的档位分组外再加档位乘质地带的交叉分组,分组名写"档位×质地=强传导×好"这样;`compare_table` 不动。 @@ -237,6 +269,10 @@ cd ~/project/akg-factor-bridge && grep -n "新序前二十\|旧序前二十\|档 6. 入池 `rank` 取值只取被指向的,数量等于 `top`。 7. 评析名单两路并集去重且按新序。 8. 卡内序开关关掉回"先判决再涨幅"。 +9. 主题限额:质地轴下五只无传导的好票全部进前二十;`uncapped_no_theme` 为假时只进五只;有主题的仍受每主题五只。 +10. 备用开关:`keep_old` 为真时旧序强传导前 `top` 全在结果里,同一票不重复,总数不超过 `POOL_MAX`。 + +既有单测要跟着动的两处:`test_plan_quality_rank.py` 的接线检查把匹配串改成新函数名;"不跨档"那一例只对 `transmission` 轴断言,质地轴本来就跨档。 ## 附录丁(可不读):台账草稿 @@ -244,4 +280,4 @@ cd ~/project/akg-factor-bridge && grep -n "新序前二十\|旧序前二十\|档 ## 附录己(可不读):文件清单 -`plan.py`、`card.py`、`pool.py`、`review_targets.py`、`plan_review.py`、`config.py`、`README.md`、新 `test_plan_rank_axis.py`、`docs/复盘决定台账.md`、交接文档。清单外一律不动;`factors.py` 与 `api.py` 不碰。 +`plan.py`、`card.py`、`pool.py`、`review_targets.py`、`plan_review.py`、`plan_reconcile.py`(只加一键)、`config.py`、`.env.example`、`README.md`、`docs/选股计划入池_对接说明.md`、`docs/选股说明_下游对接.md`、新 `test_plan_rank_axis.py`、既有测试的两处小改、`docs/复盘决定台账.md`、交接文档。清单外一律不动;`factors.py` 与 `api.py` 不碰。