20 KiB
选股计划调整方案与规格书(2026-09-17):公司质地为排序主轴,传导降为雷达与带内次级键
2026-09-17 用户拍板通过。开发者可以是人,也可以是大模型。写法遵守
.claude/output-styles/readable-chinese.md。 改动全部落在选股系统仓库akg-factor-bridge。数据基座、择时决策系统代码不动;持仓管理系统有一条协同项,单独审批。 第一到第十节是正文,附录是实现细节,标了「可不读」,但开发者动手前必须读附录甲乙丙。 基线:选股系统仓库main停在 18c55fb(含 09-16 的小改 bb286c5),选股系统机器停在 3d3f317,等 09-18 拉。 本文件是四个仓库逐字同步的副本之一,改一份要同步其余。
一、这件事为最终目标买什么
为什么现在改。 三份读数指向同一件事。
- 09-02 那份改进方案第 1.8 节用一个月样本做过传导有效性检验:传导越强,超额越负。强传导档五日超额 −2.04,弱传导 −0.39,无传导 0.00。结论写得很清楚:传导对"该看哪个环节"的定位有信息量,但"在环节里挑还没动的补涨"这一步把信息量丢掉了。
- 08-28 的周报(08-11 到 08-28,十二个计划日)又重复了这个形状:主榜里强传导档五日超额 −1.97、十日 −4.84,无传导档 −1.09 与 −1.93;候选单整体跑输全池。样本仍然"只看方向",但方向没有变过。
- 09-11 抽查主榜前一百名:有评析的七十只里质地差五十、中十四、好六。选股确实偏题材传导。
传导分的公式本身就解释了这个结果。今天的传导分等于源数乘以"一减已动比例",它奖励的正是"环节被指向但成员还没动",也就是被证明为负信号的那一步。而"环节被指向"这一位本身定位不负。所以正确的降权不是把权重系数调小,而是把传导从"排序主轴"换成"雷达门加带内次级键":它继续决定"该看哪个环节"和"为什么是现在",不再决定"谁排前面"。
为什么是质地。 个股深度评析已经能对三百五十九家公司算出质地档(好二十一、中八十七、差二百五十一),第二批定性视角与第三批预期视角两周内上线。这是主观选股四件事(行业逻辑、公司质地、估值预期差、时点)里第二件第一次有了稳定的数据源。主观选股经理的顺序是先选公司再等催化,排序主轴换成质地,是让计划的次序回到这个顺序。
用户拍板三条(2026-09-17)。 传导有效性不足,打分权重要降低,但对系统仍有价值;直接改现有候选单,不做并行名单;公司质地成为排序主轴。
这件事买四样东西。
- 主榜、观察档、候选单三处的次序都由质地档决定,传导只在同一质地档内决定先后。持仓管理系统拿到的前三十名与择时决策系统的夜间分析池跟着变成"好公司优先"。
- 传导保留两个位置:候选卡门槛一"环节被指向"不动;同一质地档内被指向的排在前面。它的"雷达"角色完整保留。
- 一条开关逐字回旧,不改分数编码,外部平台的因子契约不受影响。
- 周报四周内并列出"新序前二十"与"旧序前二十"两份名单的读数,四周后按读数决定是否保留。
二、原则
沿用五条:加不改;开关关掉逐字回旧;同日重跑结果一致;不做回测、因子合成、IC;评价只用候选单复盘周报。本方案再加三条。
- 分数编码不动。
factors.py里"主榜分等于二百加档位乘二十加组内分"的编码注册在外部平台当因子,改它会污染平台的因子历史。次序的改动全部放在计划装配这一层,与 09-14 那次质地重排(台账 058)同一个落点。 - 质地只认新鲜的报告。质地进任何判决或排序都受同一个新鲜度守卫(一百二十天),与逻辑状态第五路和持仓管理系统的基本面立场同口径。今天质地门与加减八分不看日龄,是一处不一致,顺手对齐。
- 评析覆盖不能被排序轴锁死。评析目标名单如果只取新序前一百,新被指向的公司永远排在已评析公司后面,永远得不到报告。名单要与排序轴解耦。
三、做哪几件事
五个工作包,全在选股系统仓库;另有一条持仓管理系统的协同项,单独审批。
工作包一:排序轴换成质地(plan.py)
做什么。 主榜与观察档两个分数序列在装配时按新规则重排一次,四个出口(接口、快照、逻辑状态入库、评析名单)照旧共用这个序。
规则。
- 质地带
quality_band:好为三,中为二,无评析、证据不足、报告超过一百二十天为一,差为零。无评析放在中之下、差之上:不知道不等于差,但不该排在已核实的好公司前面。 - 主榜次序:先按质地带降序;同一带内,被传导指向的排前面;再按原有组内分(六成"还没热"加四成"还便宜")降序;最后按原始分、代码作稳定的末位键。观察档没有档位,带内直接按原始分再按代码。
- 传导在这里只剩两个作用:被指向与否是带内第一个次级键;强弱档位不再进次序。原始分与档位标签照旧输出,只是不再决定名次。
- 新增四个字段:
quality_band、rank_axis(取值 quality 或 transmission)、rank_old(同一行在旧规则下的名次,供周报对照,四周后可以停)、pointed(被指向与否,从候选卡门槛一带出)。score、score_adj、tier三个既有键的值与含义不变。 - 开关
PLAN_RANK_AXIS,默认quality;设transmission时走 09-14 那套(质地加减八分、不跨档),逐字回旧,单测钉住。
为什么是词典序不是加权。 加权要发明三个新系数,没有读数支撑,也说不清楚。词典序每一层都能在卡上用一句话解释:先看公司好不好,再看有没有被产业链指向,再看还没热且便宜。
细节见附录甲。
工作包二:候选卡内的次序与质地门(card.py)
做什么。 三门槛、一确认、三风险一个字不动(生产上门槛二"已启动"本来就关着)。改两处。
- 卡内序从"先判决、再当日涨幅"改成"先判决、再质地带、再当日涨幅"。候选单与关注单的名次由此变成质地优先。开关
CARD_SORT_BY_QUALITY,默认开,关掉回旧序。 - 质地门"差降关注"与工作包一的质地带共用同一个判定函数,带上一百二十天守卫;超期报告不再降档,也不再加减分。
为什么不把质地升成门槛。 无评析的公司还能进候选(排在后面),是为了不让评析覆盖不到的新公司被系统性排除。质地差已经进不了候选,等价于门槛,不必再改判决逻辑。
工作包三:择时决策系统的夜间分析池与评析目标名单(pool.py、review_targets.py)
- 入池来源加一个取值
POOL_SOURCE=rank:从主榜新序里取被指向的行,按名次截前POOL_TOP(机器上是五十)。原来的tier(只收强传导)与candidate两个取值保留,回旧用。夜间推理量不变,只是从"强传导前五十"变成"质地优先且被指向的前五十"。 - 评析目标名单改成两路并集:原始分序前一百(跟着雷达走,新被指向的公司先有报告)加新序前五十(好公司的报告保持新鲜),观察档前二十不变。开关
REVIEW_TARGETS_AXIS,默认both,设score回旧。
工作包四:周报对照(plan_review.py)
- 过渡期四周,第一节多两份名单:主榜新序前二十、主榜旧序前二十。旧序从快照行的
rank_old取,不另存文件。 - 第二节多一组交叉分组:主榜按"传导档位乘质地带"分格,看传导在质地档内还剩多少信息量。
- 既有的候选单按质地分组、档位表按质地分组、传导档位分组都保留。
- 四周后的复核判据写死:新序前二十相对旧序前二十的五日与十日超额都不更差,且候选单跑赢全池的占比不低于旧序。达不到就把
PLAN_RANK_AXIS切回,台账记一条。
工作包五:单测、台账、交接
- 单测新文件
test_plan_rank_axis.py,照test_plan_quality_rank.py的写法,八例见附录丙,末行 ALL OK。既有test_plan_quality_rank.py照旧全绿。 - 台账一条,编号按落笔时顺延(数据基座那四条在各自部署日先占号)。五段式写清改动、依据(上面三份读数)、预期、复核日(上线后四周)。草稿在附录丁。
- 交接文档一段;README 的计划字段表加四个新键;四仓库同步。
协同项:持仓管理系统(另一仓库,单独审批)
持仓管理系统取候选池时按原始 score 降序排,再按基本面立场分桶(plan_feed.select_candidates,参数 PMS_PLAN_RANK_BY_QUALITY 默认开)。上游次序换成质地后,它拿到的前三十名会变成质地优先,但它内部仍按原始分排。建议它加一个参数:开着"按质地排序"时改按上游的 rank 排,让两边次序一致。这条不影响本方案上线,因为它自己的看多分桶已经把好公司排在前面。
四、顺序与时间
| 日期 | 星期 | 做什么 |
|---|---|---|
| 09-17 到 09-18 | 三到四 | 开发五个工作包,本地单测全绿,一个提交,交第十节的证据 |
| 09-19 | 六 | 我复审 |
| 09-21 | 一 | 收盘后选股系统机器拉代码。计划装配代码不需要重启接口容器,次日 07:10 的计划就是新序 |
| 09-22 | 二 | 第一份新序计划。看候选单、前三十名、入池名单三处读数,交回 |
| 09-25 | 五 | 第一次带两份名单对照的周报(二十点十分由基座触发) |
| 10-16 前后 | 四周复核,按工作包四第四条决定保留或切回 |
与数据基座的部署日不冲突:09-22 基座是收盘后重建工作进程,选股系统是前一天拉代码,两台机器各做各的。09-18 那次选股系统拉代码与接口重启照旧,本方案的代码不在那次里。
五、开关与参数
| 键 | 默认 | 说明 |
|---|---|---|
PLAN_RANK_AXIS |
quality | transmission 即 09-14 行为,逐字回旧 |
CARD_SORT_BY_QUALITY |
1 | 卡内序先质地带;0 回"先判决再涨幅" |
POOL_SOURCE |
rank | 新增取值;tier 与 candidate 保留 |
REVIEW_TARGETS_AXIS |
both | score 回"只按原始分前一百" |
COMPANY_REVIEW_STALE_DAYS |
120 | 已有;从此对质地带、质地门、加减分都生效 |
PLAN_QUALITY_BONUS |
8 | 已有;只在 transmission 轴下起作用 |
机器上的 .env 不需要加任何键,默认值就是新行为。回退时才写旧值。
六、验证方式
每条命令标明在哪台机器上运行。选股系统机器指 ssh -p 2280 factor@192.168.16.155,仓库在 ~/project/akg-factor-bridge。
本地(选股系统仓库)。 预期两行都是 ALL OK。
cd /Users/baobao/Documents/work/project/akg-factor-bridge && python3 test_plan_rank_axis.py 2>&1 | tail -1 && python3 test_plan_quality_rank.py 2>&1 | tail -1
本地。 逐字回旧。预期末行 ALL OK,其中那一例断言四个开关都设旧值时装配输出与旧实现逐字节相同。
cd /Users/baobao/Documents/work/project/akg-factor-bridge && PLAN_RANK_AXIS=transmission CARD_SORT_BY_QUALITY=0 POOL_SOURCE=tier REVIEW_TARGETS_AXIS=score python3 test_plan_rank_axis.py 2>&1 | tail -1
选股系统机器(上线次日 07:15 后)。 预期候选单前几名的质地列以"好"开头;主榜前十二名里质地好或中占绝大多数;每行带 quality_band、rank_old、pointed。
cd ~/project/akg-factor-bridge && sed -n 1,30p $(ls -t data/plan/plan_*.md | head -1) && docker exec akg_factor_bridge python3 -c "import json,glob; d=json.load(open(sorted(glob.glob('data/plan/plan_*.json'))[-1])); print([(r['rank'],r['code'],r.get('quality_band'),r.get('pointed'),r.get('rank_old'),r.get('tier')) for r in d['main'][:12]])"
选股系统机器。 入池名单来源。预期干跑输出里每只票的 _pool_source 是 rank_top,且都被指向。
cd ~/project/akg-factor-bridge && docker exec akg_factor_bridge python3 run.py push-pool --dry-run 2>&1 | tail -15
选股系统机器。 评析目标名单两路并集。预期当日行数在一百到一百五十之间,source 为 main 的行里既有原始分前列也有新序前列。
cd ~/project/akg-factor-bridge && docker exec akg_factor_bridge python3 -c "import db; print(db.read_mysql('factor','SELECT source, COUNT(*) n, MIN(\`rank\`) r0, MAX(\`rank\`) r1 FROM t_akg_review_targets WHERE plan_date=(SELECT MAX(plan_date) FROM t_akg_review_targets) GROUP BY source').to_string())" 2>&1 | grep -v Warning
选股系统机器(09-25 周报后)。 预期第一节出现"主榜新序前二十"与"主榜旧序前二十"两行,第二节出现"档位×质地"分组。
cd ~/project/akg-factor-bridge && grep -n "新序前二十\|旧序前二十\|档位×质地" $(ls -t data/review/复盘_*_next_close.md | head -1) | head
七、回退
四个开关全部设回旧值(PLAN_RANK_AXIS=transmission、CARD_SORT_BY_QUALITY=0、POOL_SOURCE=tier、REVIEW_TARGETS_AXIS=score),下一次装配即逐字回旧,不用重启,不用回代码。回退也记台账。
八、拍板点
代定十条,动工前不再问。要改就改这里。
- 质地带四档:好三、中二、无评析与证据不足与超期一、差零。
- 带内次序用词典序(被指向、组内分、原始分、代码),不用加权。
- 传导强弱档位不再进次序,只保留"被指向"这一位。原始分与档位标签照旧输出。
- 三门槛一确认三风险不动;质地门保持"差降关注",加一百二十天守卫。
- 无评析的公司仍可进候选单,排在有评析的中之后。
- 入池改成新序里被指向的前五十;夜间推理量不变。
- 评析目标名单两路并集,与排序轴解耦。
- 周报四周对照,判据在工作包四第四条。
- 持仓管理系统的次序对齐作为协同项另批,不阻塞上线。
- 09-03 方案里"不改传导公式、档位、分数编码、候选卡两个旋钮"这一句,本方案守住了公式、编码、两个旋钮,改的是装配层次序。台账里写明这是对 09-03 那条的有意修订。
九、交付要求
- 一个提交,全中文、无前缀,格式"主题:改了什么",末尾带台账号。正文写清五个工作包各改了哪些函数、没改什么、为什么不合并。
- 先写逐字回旧那一例单测,再改代码。
- 所有排序带确定的末位键(代码),两次装配结果逐字节相同。
- 只加键不删键,
score、score_adj、tier、verdict、card_rank的值与含义不变。 - 台账、交接、README 三处一起改;四仓库同步。
- 交付证据:提交号、两个测试文件的末行、逐字回旧那一例的名字、
git show --stat对照附录己的文件清单。
十、评审方式
评审者只看证据。核对步骤:读 diff 对照附录己的文件清单;本地跑两个测试文件;核四个开关都设旧值时的逐字回旧断言是真断言;核卡内序、入池、评析名单三处的次级键与去重;核 _row 只加键。上线次日与 09-25 周报后再核第六节的读数。判定三档同前:通过、有条件通过(只差运行样本读数)、退回(单测红、改了既有键语义、排序没有末位键、分数编码被动)。评审记录写 docs/评审记录_<日期>.md。
附录甲(可不读):plan.py 改动
行号按选股系统仓库 main 18c55fb。
_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。 _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取值,每个键三行注释写为什么。
附录乙(可不读):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分支原样保留。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不动。
附录丙(可不读):单测清单
test_plan_rank_axis.py,合成六到八行主榜数据加候选卡字典,各一例:
- 四个开关都设旧值时,输出与
_rerank_by_quality逐字节相同(逐字回旧)。 quality轴下好在中前、中在无评析前、无评析在差前。- 同一带内被指向的在前,再按组内分。
- 两次装配顺序相同(确定性)。
- 超期一百二十天的好公司落到第一带。
- 入池
rank取值只取被指向的,数量等于top。 - 评析名单两路并集去重且按新序。
- 卡内序开关关掉回"先判决再涨幅"。
附录丁(可不读):台账草稿
- 编号顺延 · 上线日 · 选股计划排序轴换成公司质地,传导降为雷达门与带内次级键。依据:09-02 第 1.8 节、08-28 周报、09-11 抽查三份读数一致,传导分的公式奖励的正是被证明为负的"未动补涨"。预期:候选单与前三十名以质地好中为主,四周内新序前二十相对旧序前二十的超额不更差。复核日:上线后四周。旧条 09-03"不改档位"那一句在此修订。
附录己(可不读):文件清单
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 不碰。