chore(task): archive 09-21-radar-weighted-score-rank-change

This commit is contained in:
yuxuanhui
2026-09-21 23:58:39 +08:00
parent b9981aa48d
commit 30bf94c908
10 changed files with 15 additions and 14 deletions
@@ -0,0 +1,9 @@
{"file": ".trellis/spec/backend/index.md", "reason": "后端包边界与必需检查"}
{"file": ".trellis/spec/backend/tushare-listed-stock-universe.md", "reason": "继续遵守当前上市证券母集,不能为对齐网站擅自改变范围"}
{"file": ".trellis/spec/backend/http-api-contracts.md", "reason": "评分与三指标变化字段的端到端契约"}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "镜像榜单、控件与可访问性"}
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "API nullable 字段与严格解析"}
{"file": ".trellis/tasks/archive/2026-09/09-21-radar-weighted-score-rank-change/research/formula-evidence.md", "reason": "已验证的评分结构及数值证据边界"}
{"file": ".trellis/tasks/archive/2026-09/09-21-radar-weighted-score-rank-change/research/code-evidence.md", "reason": "现有实现定位与需保持的版本/持久化行为"}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "后端 Ruff、Pyright、pytest 和持久化检查"}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "前端格式、lint、类型、行为与构建检查"}
@@ -0,0 +1,48 @@
# 设计:加权评分与排名变化
## 计算与领域边界
评分留在 sector_radar 领域/发布阶段,不在 React 中根据当前页数据计算百分位。当前每板块 MetricStrategy 只能得到该板块历史,因此新增全池评分步骤,接收同一目标日所有板块的原始特征,按日期、类型分池计算。
新版本拟为 `zhixing_ratio_weighted_v2` 和 `zhixing_swing_weighted_v2`。设 A 为现有聚合成交额元、F 为聚合主力净额元;按前置研究的可复算输入约定取 `r=F/(A+100)`,A<=0 或输入未知时不制造比率。W=`log10(1+MA5(A))/10`;P 为原始指标升序平均名次/N。
```text
单日原值 = r
单日评分 = 1000 * P(r) * W
波段原值 = .5*MA3(r) + .5*MA10(r)
波段评分 = 1000*(.5*P(MA3(r)) + .5*P(MA10(r)))*W
```
使用 Decimal 计算金额、比率和对数;仅在输出展示时保留 1 位小数。P 的并列值采用平均名次,最终分数并列沿用 sector_code 升序作为稳定破同分规则。原始值与 weighted_score 分离;窗口不足时保留可确定的原始值,评分和对应名次为空,不以原始值替代缺失评分。质量状态和覆盖率继续传播。
新版本使用保存的交易日历确定窗口与对比日期,缺少某个应有交易日输入时保留未知;不得悄悄以更早成功发布替代缺失交易日。第一个可复算日期受库内真实历史覆盖限制。
## 排名、历史与版本
`rank_metric_observations` 对金额继续使用原始金额,对新版本单日/波段使用 weighted_score。上下普通榜筛选使用最终百分位;底榜排序也须与评分键一致。排名变化在同版本最终名次上计算 `past_rank-current_rank`;取 ceil(有效排名池大小×10%),历史不可比者不进入变化候选。
一行排名变化响应提供所选基准兼容字段 rank_change,以及 amount/ratio/swing 三个变化值,供中心主列和两侧辅助列复用。查询选出的前后榜由所选基准确定;点击辅助列只改变该侧当前候选排列。字段均允许 null。
旧发布按其实际 metric_versions 解析定义与历史,不用新版本常量把旧数据过滤成空,也不比较 v1 与 v2 名次。榜单、详情和历史折线共享版本解析与排名事实。首次切换期间旧发布评分显示“—”;历史重算完成后,同日最新成功发布提供新评分。
## 持久化与历史重算
新增 Alembic migration,为 `sector_radar_ranking` 增加 nullable NUMERIC weighted_score,保持 metric_value 原义。同步所有 INSERT、SELECT、序列化和反序列化;内存仓库保持同等语义。
发布构建使用统一评分服务,版本参与 input_hash。增加离线重算入口,仅使用已落库聚合事实和原发布的来源关联,不初始化 Tushare 客户端。按交易日先后创建新派生发布,保留旧发布与原始快照;固定源 publication IDs 避免重算期间新发布改变本轮输入。复用现有按日锁、事务和 last-good 规则,重算幂等键包含源发布身份/输入指纹和新策略版本。
重算时必须复制/关联原聚合的 pct_change、leading_code 及来源组,使新发布的详情仍可读取;不能只写 ranking 而产生空详情。适配器需要提供 publication 精确的 aggregate records 与来源读取,避免现有 history 方法丢失这些元信息。
生产数据库的迁移和重算是独立运行步骤;在本地代码和验证准备完成前不执行外部写入。回退可恢复旧代码版本并重新选择保留的旧发布;nullable 新列本身保持兼容。
## HTTP 与前端
- 在榜单行和必要详情摘要中扩展 weighted_score;排名变化补充三指标变化映射与对比日期信息,同步 Pydantic、TypeScript、解析器和 query 测试。
- 单日/波段左右增加评分列,1 位小数、缺失“—”;其余原始百分比保留独立展示。
- rank_change 使用独立次行,左侧三枚排序基准按钮,右侧近 1–5 日下拉。默认波段率和 1 日,仅在 URL 没有显式值时采用默认。
- 标题为排名飙升榜/排名暴跌榜,中央为“{基准全名}排名变化”;主列显示所选指标变化,辅助列显示另外两项,增加涨跌幅。
- 延续现有主题、表格镜像、板块详情入口和响应式横向滚动;不添加未要求的全局导出、搜索改版或主题重制。
## 主要风险
新算法会改变单日/波段榜及历史名次;必须依靠新版本与离线重算切换,不能给旧名次套新评分。知行的 1000 个板块及按日成员与 OneChart 的 791 板块输入不同,算法结构一致不保证逐值一致。原站并列、缺失与极低成交额策略未完全公开,本系统明确采用上述确定规则。
@@ -0,0 +1,7 @@
{"file": ".trellis/spec/backend/index.md", "reason": "后端包边界与必需检查"}
{"file": ".trellis/spec/backend/tushare-listed-stock-universe.md", "reason": "继续遵守当前上市证券母集,不能为对齐网站擅自改变范围"}
{"file": ".trellis/spec/backend/http-api-contracts.md", "reason": "评分与三指标变化字段的端到端契约"}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "镜像榜单、控件与可访问性"}
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "API nullable 字段与严格解析"}
{"file": ".trellis/tasks/archive/2026-09/09-21-radar-weighted-score-rank-change/research/formula-evidence.md", "reason": "已验证的评分结构及数值证据边界"}
{"file": ".trellis/tasks/archive/2026-09/09-21-radar-weighted-score-rank-change/research/code-evidence.md", "reason": "现有实现定位与需保持的版本/持久化行为"}
@@ -0,0 +1,39 @@
# 执行计划
状态:本地实现与范围内验收完成;全仓既有失败已在实施前版本复现。详细结果及生产应用命令见 `validation.md`。
1. 完整阅读要修改的文件;确认已保存的评分研究、接口契约、版本及缺失语义。核对需要使用的 Alembic/Pydantic 等当前版本与官方文档。
2. 实现版本化评分特征和全池计算,分离 raw value/score;更换单日/波段排序键,补独立数值样例、并列、未来数据排除及缺失窗口测试。
3. 新增 nullable score 列迁移;同步 PostgreSQL/内存仓库所有读写与 historical ranking 查询。补充真实数据库往返、旧行 NULL 和事务失败测试。
4. 接入发布构建与 input_hash;实现不访问 Tushare 的历史重算入口、新旧版本解析及严格对比日期。验证幂等、旧发布保留、失败 last-good 与详情来源完整。
5. 扩展榜单/详情 HTTP 输出和前端类型解析,支持评分、三指标变化值及对比日期;覆盖不匹配版本与缺失历史。
6. 单日/波段表格添加评分列与侧内排序;按截图调整排名变化次级控制行、双榜标题、中央标题、涨跌幅和两项辅助变化列,保持 URL/查询联动。
7. 执行领域、发布、读取、HTTP、仓库、前端 API 和页面行为测试;用本地测试数据库验证迁移和离线重算,避免使用用户生产库进行测试写入。
8. 完成后端 Ruff/Pyright/pytest 与前端格式、lint、typecheck、Vitest/build。浏览器实际检查三基准、1–5 日、空历史、评分列和窄屏;保存必要截图和验证结果。
9. 检查 diff 范围、已有用户改动和未验证事项;交付本地成果及明确的迁移/重算命令,不自行提交、部署或写入生产库。
## 验证命令
```bash
cd zhixing-server
uv run ruff format --check .
uv run ruff check .
uv run pyright
uv run pytest
```
```bash
cd zhixing-web
pnpm check
pnpm build
```
实现期间先执行相关测试,最后执行项目规定完整检查;通过后仅在新改动或未解决问题需要时重复。
## 高风险核验点
- P 必须来自完整同类池,不能来自当前页面 TOP10% 子集。
- Swing 分数是两次排名后合成,不是合成 raw 后再排名。
- 历史不足时不能伪造评分/0变化;不能跨失败交易日跳位或跨版本相减。
- migration 列顺序涉及所有 ranking SELECT 与 fake row,不得只改写入。
- 离线重算的新发布必须可继续打开详情,且不得重新请求上游或改变旧快照。
@@ -0,0 +1,46 @@
# 雷达加权评分与排名变化复刻
## Goal
根据已验证的 OneChart 评分公式,为单日和波段榜增加加权评分,并复刻排名变化的排序基准、1至5日窗口和双榜交互。
## Requirements
- R1:单日流入率与波段流入率双榜各增加“加权评分”列,左右镜像排列,保留 1 位小数,支持表头排序;原始流入率仍单独展示。
- R2:采用本轮前置研究验证的评分结构。单日和波段榜按各自加权评分形成最终排名;波段原始流入率采用 3 日、10 日单日流入率均值各 50%。单日净额榜按净额形成排名。
- R3:排名变化视图按参考截图提供独立次级控制行:左侧“排序基准”含波段率、单日率、单日额,右侧“统计天数”含近 1–5 日;首次进入默认波段率、近 1 日,并保留 URL 中显式指定的选择。
- R4:排名变化视图展示“排名飙升榜 TOP 10% / 排名暴跌榜 BOTTOM 10%”,中央标题随基准切换;每行显示涨跌幅、所选基准的名次变化以及其余两个指标的名次变化。上下榜由所选基准决定,表头排序只重排该榜现有候选。
- R5:名次变化为过去名次减当前名次,统计基于交易日,概念与行业分池。历史缺失、算法版本不兼容时显示“—”,不得伪造 0 或混用新旧算法名次。
- R6:评分、最终名次、排名变化和详情历史使用相同算法版本;提供基于已有聚合事实重算历史的能力,旧发布保持可追溯。已有旧版本数据升级前仍能安全读取。
- R7:继续使用知行当前数据库、板块池、按日成员快照与当前上市股票范围。复刻评分结构与交互,不以抓取 OneChart 结果替代业务计算,也不承诺与不同输入口径的网站逐值相等。
## 范围边界
- 本任务包含必要的领域计算、数据库派生字段迁移、HTTP 契约、前端及测试,以及历史重算命令。
- 不包含调整证券母集、将网站当前成员回填历史、额外的榜单导出或搜索功能重写。
- 本地实现与非破坏性验证按任务执行;生产迁移、生产历史重算、部署不在本轮执行范围内;Git 提交与推送已由后续用户请求明确授权。
## Acceptance Criteria
- [x] AC1/R1:单日/波段榜显示左右“加权评分”列,有限值为 1 位小数,缺失为“—”,排序与表头状态正确。
- [x] AC2/R2:独立样例验证百分位、5 日成交额权重、3/10 日各 50% 及“先分别排名再合成”;高原始比率但低评分的反例能按评分正确排名。
- [x] AC3/R3–R4:三基准 × 五窗口切换正确更新双榜、中央标题和辅助两列,URL 刷新可恢复;正负方向、镜像布局和窄屏横向滚动正确。
- [x] AC4/R5:交易日跨周末、缺发布、缺板块、零变化、并列及新旧版本不匹配均有测试,榜单选取为对应池 ceil(N×10%) 个有效变化候选的上限。
- [x] AC5/R6:旧行 weighted_score 为空时可读取;历史重算创建可追溯新发布且幂等,不请求外部数据;失败不影响最近成功发布,详情与榜单的当前名次一致。
- [ ] AC6/R7:沿用当前股票/板块数据范围;单元、HTTP、前端、持久化及浏览器验证覆盖变更,项目规定的质量检查通过。
## 已确认的现状与依据
- 前置研究:`research/formula-evidence.md`。公开输入 9,492 条样本的评分与最终名次可精确重现;数据库输入存在成员/板块池差异。
- 当前单日/波段只有原始 metric_value;旧波段策略实际为 3 至 10 日八个净额/成交额窗口等权,并非新研究中的两个窗口,见 `domain/metrics.py:140` 起。
- 当前排名变化已有基础 API 与 1–5 日变化值,但 UI 控件、标题、辅助列不匹配截图,且历史名次基于旧原始指标,定位见 `research/code-evidence.md`。
## 决策状态
用户已明确回复“同意,开始实施”,批准本版方案。执行现有数据口径下的评分和交互升级;按个人约定由主代理编码与最终验证,子代理仅承担只读探索和独立核验。
## 本轮验收状态
AC1–AC5 已完成。AC6 的变更范围检查和浏览器验收通过;全仓检查仍有在实施前 HEAD 上独立复现的既有失败,因此保留未完全通过状态。详见 `validation.md`。本地实现已交付;功能代码已提交为 `b9981aa`,生产迁移、重算和部署未执行。
用户已于本轮授权提交并推送。本任务的实现和范围内验证完成;AC6 的全仓基线失败仍明确保留为已知限制,不扩展修复选股/行情模块。线上重算说明已补入 `docs/market-data-sync.md`。
@@ -0,0 +1,36 @@
# 现有实现与改动定位
只读研究已完成:`/root/radar_storage_research`、`/root/radar_ui_research`,主会话另行阅读领域算法、models、read、build 和 HTTP 相关契约。
## 领域与应用
- `domain/metrics.py:140` 起,旧 SwingEqualThreeToTenStrategy 取 3–10 日八个窗口的累计净额/累计成交额,再等权平均;需采用新版本,不能把它误认作前置研究的两个窗口。
- `domain/ranking.py:42` 的排序键按 observation.value 排序;`with_rank_changes` 同版本过去名次减当前名次;select_rank_change_side 已实现 ceil(N×10%),不需另写重复算法。
- `application/build.py:694` 的 _rank 读取 9 个历史成功日期再拼当前日;历史排名取前 5 次成功发布,并非严格前 5 交易日。
- `application/build.py:753` 的 input_hash 包含策略版本;重算可沿用版本隔离与幂等思路。
- `application/read.py` 的 _METRIC_DEFINITIONS 和 `application/details.py:25` 的 _METRIC_VERSIONS 都硬编码当前策略;升级必须让旧发布保持可读。
- `application/read.py:289` 的 extras 只服务 amount/ratio,swing/rank_change 的涨跌幅、净额和辅助字段需要补齐。
- `application/details.py:173` 的 history_data 已按发布来源中的交易日历补出缺口,适合统一严格交易日语义;必须避免每行单独查库。
## 存储
- `domain/persistence.py:294–355` 定义 aggregate、ranking、history 读写契约。
- `infrastructure/postgres.py:532–710` 为事务发布,`:741–778` 为 ranking 批写,`:1145–1173` 为序列化,`:1239–1267` 为固定列反序列化。
- ranking SELECT 位于 `postgres.py:871–964`、`:996–1033`,当前只含 metric_value,没有 score。
- `postgres.py:966–994` 的 aggregate history 只返回原始金额/coverage,不包含 publication_id、pct_change、leading_code,离线重算需要精确来源记录。
- 最新迁移为 `0009_radar_sector_detail`。新增 score 应采用独立 migration,旧值可空。
- 单测 `tests/unit/sector_radar/test_postgres_repository.py:43–65` 使用 17 列 fake row,必须随查询同步。现有集成测试尚未覆盖 ranking/aggregate 往返。
## 前端
- `pages/sector-radar-page.tsx:209–229` 已有两个下拉,分别为变化指标/对比区间;改为截图的按钮组与统计天数次行。
- `pages/sector-radar-page.tsx:406–428` 顶部双榜和中央标题仍是普通资金榜文案。
- `pages/sector-radar-page.tsx:430–463`、`:643–708` 是单日/波段镜像列,无 score。
- `pages/sector-radar-page.tsx:465–500` 的变化榜目前只显示样本、排名百分位与一个变化值;应展示涨跌幅及另外两项指标变化。
- `api/sector-radar.types.ts:61–114` 与 `api/sector-radar.api.ts:277–377` 仅承载单个 rank_change,需扩展。
- 路由与 query key 已保存基准/天数,保留这一结构;URL 显式值优先。
- 页面测试 `sector-radar-page.test.tsx:654–683` 已覆盖正变化/缺历史,需增加评分、三基准五窗口、镜像辅助列、侧内排序与截图文案。
## 参考站核对
本会话下载的 `/tmp/onechart-public-evidence/index.html` 中:`:1197` 默认 Swing/1 日,`:1380` 附近 mirrorMetricLabel 随基准变,`:1397–1427` 的 mirrorAuxColumns 在变化榜显示另外两个基准,在单日/波段榜显示 score/净额/在榜。前置研究完整来源和数值证据位于已归档的评分研究任务。
@@ -0,0 +1,19 @@
# 加权公式与验证边界
前置研究在 2026-09-04 至 2026-09-21 的 9,492 条公开样本上复现单日、波段评分与最终排名;分数最大误差 3.41e-13,属于浮点运算误差。这是观测数据还原,不是原站源码证据。
对本项目已有聚合数据,F 为主力净额元、A 为成交额元:
```text
r = F / (A + 100)
W = log10(1 + MA5(A)) / 10
单日评分 = 1000 × P(r) × W
波段评分 = 500 × (P(MA3(r)) + P(MA10(r))) × W
波段原值 = (MA3(r) + MA10(r)) / 2
```
P 为同日、同类型全池升序平均名次 / N。波段必须先分别排名,再合成。最终评分并列按板块代码稳定排序。权重可能大于 1,分数不是固定上限 1000 的百分制。
公开字段可以确认的等价权重是 `log10(MA5(F/r)-99)/10`;`A=F/r-100` 是本任务采用的可复算口径,不能证明原站源码中具体常量的用途。原站边界与并列规则未完全公开,本地明确使用 design.md 中的完整交易日窗口、平均并列名次及缺失语义。
公开验证池为 791 个板块,本地数据库池和按日成员不同。本项目保留当前上市股票与已保存历史输入,因此不承诺与网站逐值相同。原始研究数据和复算快照保留在本地前置研究任务中,不是运行本功能的依赖。
@@ -0,0 +1,166 @@
"""Synthetic browser fixtures; restricted to the disposable local radar test DB."""
import hashlib
import math
import os
from dataclasses import replace
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal
from urllib.parse import urlsplit
import psycopg
from zhixing_server.modules.sector_radar.application.scoring import calculate_rankings
from zhixing_server.modules.sector_radar.domain.metrics import (
AmountNetStrategy,
RatioTurnoverStrategy,
SwingEqualThreeToTenStrategy,
)
from zhixing_server.modules.sector_radar.domain.models import (
PublicationStatus,
RadarPublication,
SectorDailyAggregate,
SectorType,
)
from zhixing_server.modules.sector_radar.domain.persistence import (
DailyAggregateRecord,
PublicationSourceGroup,
PublicationSourceRecord,
RankingRecord,
)
from zhixing_server.modules.sector_radar.domain.source import build_source_snapshot
from zhixing_server.modules.sector_radar.infrastructure.postgres import (
PostgresSectorRadarRepository,
)
url = os.environ["ZHIXING_TEST_DATABASE_URL"]
location = urlsplit(url)
if (location.hostname, location.port, location.path) != ("127.0.0.1", 55439, "/radar_test"):
raise SystemExit("Preview seeding is restricted to the disposable local radar test database")
with psycopg.connect(url) as connection:
connection.execute("TRUNCATE sector_radar_publication CASCADE")
start = date(2026, 8, 10)
days = tuple(
start + timedelta(days=n) for n in range(43) if (start + timedelta(days=n)).weekday() < 5
)
strategies = (AmountNetStrategy(), RatioTurnoverStrategy(), SwingEqualThreeToTenStrategy())
names = (
"人工智能",
"机器人",
"商业航天",
"低空经济",
"半导体",
"创新药",
"新能源",
"电力设备",
"数字经济",
"消费电子",
)
now = datetime.now(UTC)
calendar = build_source_snapshot(
api_name="trade_cal",
params={"fixture": "weighted-radar-preview"},
rows=[{"exchange": "SSE", "cal_date": day, "is_open": 1} for day in days],
target_trade_date=days[-1],
observed_at=now,
)
repository = PostgresSectorRadarRepository(url)
history = []
try:
repository.save_source_snapshots((calendar,))
for index, day in enumerate(days):
aggregates = []
for sector_type, size in ((SectorType.CONCEPT, 300), (SectorType.INDUSTRY, 100)):
for n in range(size):
turnover = Decimal(
str(10 ** (8 + n % 4) * (1 + 0.2 * math.cos(n + index)))
).quantize(Decimal(".01"))
ratio = Decimal(
str(
0.09 * math.sin(n * 1.37 + index * 0.43) + 0.02 * math.cos(n * 0.71 + index)
)
)
aggregates.append(
SectorDailyAggregate(
day,
sector_type,
f"TEST-{sector_type.value[0]}-{n:04}",
f"{names[n % len(names)]} {n + 1}",
10,
10,
(turnover * ratio).quantize(Decimal(".01")),
turnover,
Decimal(1),
Decimal(1),
)
)
publication = RadarPublication(
f"preview-{day}",
day,
PublicationStatus.RUNNING,
"local-preview-v1",
"synthetic-preview-v1",
tuple(strategy.metric_version for strategy in strategies),
None,
Decimal(1),
now,
)
repository.create_publication(publication)
repository.save_publication_sources(
(
PublicationSourceRecord(
publication.publication_id, PublicationSourceGroup.CALENDAR, 0, calendar
),
)
)
for sector_type, group in (
(SectorType.CONCEPT, PublicationSourceGroup.CONCEPT_INDICES),
(SectorType.INDUSTRY, PublicationSourceGroup.INDUSTRY_INDICES),
):
snapshot = build_source_snapshot(
api_name="dc_index",
params={"fixture": "preview", "type": sector_type.value},
rows=[
{
"trade_date": day,
"ts_code": row.sector_code,
"name": row.sector_name,
"pct_change": Decimal(str(3 * math.sin(n + index))).quantize(
Decimal(".01")
),
}
for n, row in enumerate(aggregates)
if row.sector_type is sector_type
],
target_trade_date=day,
observed_at=now,
)
repository.save_source_snapshots((snapshot,))
repository.save_publication_sources(
(PublicationSourceRecord(publication.publication_id, group, 0, snapshot),)
)
ranks = calculate_rankings(day, aggregates, history, days, strategies)
repository.finalize_publication(
replace(
publication,
status=PublicationStatus.SUCCESS,
input_hash=hashlib.sha256(str(day).encode()).hexdigest(),
finished_at=now,
),
memberships=(),
stock_facts=(),
daily_aggregates=(
DailyAggregateRecord(
publication.publication_id,
row,
Decimal(str(3 * math.sin(n + index))).quantize(Decimal(".01")),
)
for n, row in enumerate(aggregates)
),
rankings=(RankingRecord(publication.publication_id, row) for row in ranks),
)
history.extend(aggregates)
print(f"Seeded {len(days)} days × 400 synthetic sectors; no production data or provider calls.")
finally:
repository.close()
@@ -0,0 +1,41 @@
{
"id": "radar-weighted-score-rank-change",
"name": "radar-weighted-score-rank-change",
"title": "雷达加权评分与排名变化复刻",
"description": "根据已验证的 OneChart 评分公式,为单日和波段榜增加加权评分,并复刻排名变化的排序基准、1至5日窗口和双榜交互。",
"status": "completed",
"dev_type": "fullstack",
"scope": "sector-radar",
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-09-21",
"completedAt": "2026-09-21",
"branch": "develop",
"base_branch": "develop",
"worktree_path": null,
"commit": "b9981aa48de32dc1fac03f2febfbdd6786ff0f72",
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"zhixing-server/src/zhixing_server/modules/sector_radar/domain/weighted.py",
"zhixing-server/src/zhixing_server/modules/sector_radar/application/recompute.py",
"zhixing-server/migrations/versions/0010_radar_weighted_score.py",
"zhixing-web/src/features/sector-radar/pages/sector-radar-page.tsx",
"zhixing-web/src/features/sector-radar/components/radar-table-sort.ts"
],
"notes": "功能代码提交 b9981aa;后端雷达 110 项、前端雷达 74 项及浏览器 30 组合通过。独立检查未发现新增阻塞。全仓既有失败见 validation.md;原始研究快照未纳入提交,生产迁移、重算、部署未执行。",
"meta": {
"planning_status": "approved",
"planning_summary_version": 1,
"production_writes_authorized": false,
"implementation_status": "complete",
"validation_status": "scoped_pass_with_verified_baseline_failures",
"commit_push_authorized": true,
"spec_sync": "No shared spec promotion; reviewed formula and limitations retained in task artifacts.",
"accepted_baseline_limitations": true
}
}
@@ -0,0 +1,71 @@
# 实施与验证记录
2026-09-21,本地实施完成。功能代码已提交为 `b9981aa`;本轮用户已授权推送。生产数据库未迁移、未重算,未部署。
## 已实现
- 单日/波段流入率增加镜像加权评分列,保留 1 位小数,原始率独立展示。左右列头分别排序,加载该侧完整候选榜后应用排序,NULL 始终置后。
- 新算法版本为 `zhixing_ratio_weighted_v2`、`zhixing_swing_weighted_v2`。全池平均名次百分位与 5 日成交额权重在后端计算,波段使用 MA3/MA10 分别排名后各占 50%。
- 排名变化提供波段率/单日率/单日额、近 1–5 天、涨跌幅、另外两项指标的变化、镜像双榜和随基准更新的标题。URL 无显式值时默认波段率、近 1 天。
- 对比使用保存的交易日历和同版本名次。缺发布、缺板块、历史不足或版本不匹配时保持未知,不跳过缺失交易日、不伪造零。
- 迁移 `0010_radar_weighted_score` 增加 nullable NUMERIC(28,12) 和有限值约束。全部排名读写路径、HTTP、前端解析与详情历史同步扩展。
- `sector-radar-build --recompute` 只读取已有聚合事实与源快照,按日期生成新发布,保留原发布。当前名次、历史和详情使用相同版本。
## 实际验证
| 检查 | 结果 |
| --- | --- |
| 后端雷达领域、构建、读取、HTTP、PostgreSQL 集成 | 110 passed |
| 前端雷达 API、query、详情、页面 | 74 passed |
| 后端 Ruff format/check | 通过 |
| 本次后端模块、测试和迁移 Pyright | 0 errors |
| 前端格式、ESLint、TypeScript 和生产 build | 通过;Vite 仍提示既有大 chunk |
| migration 0010 → 0009 → 0010 | 288 条原始排名保留,新增评分为 NULL |
| 真实 PostgreSQL 重算 | 保留旧发布、评分在各读取路径一致、详情来源完整、失败不替换 last-good |
| CLI 完整重算 | 31 日 × 400 个合成板块;首次 success,重复 31 日均 unchanged |
| 浏览器 2 类型 × 3 基准 × 5 天数 | 30 组合通过;首行变化与 API 一致,刷新恢复 URL 选择 |
| 浏览器评分排序 | 左榜完整 31 个、右榜完整 30 个候选;升降序正确且两侧独立 |
| 浏览器窄屏与缺历史 | 390px 页面无整页横向溢出;960px 表格可横向滚动;空历史文案完整可见 |
| Git diff/context manifests | diff --check 与 7/9 条上下文清单校验通过 |
数据库验证均在单独创建的本地 PostgreSQL 17 容器执行,未连接生产数据库。浏览器预览为合成测试数据;`research/seed_local_preview.py` 限制只能写入本地测试库。控制台仅观察到既有 favicon.ico 404,没有本次页面异常。
## 全仓基线问题
完整命令已执行,但不能报告全仓检查通过。已用 `git archive HEAD`(实施前 HEAD `669e89d`)及相同依赖独立复现:
- 后端全量 Pyright:14 个相同错误,位于 selection 的 `chart.py`、`gold_brick.py`、`test_run.py`。
- 后端全量 pytest:本次 230 passed / 6 failed,修改前 221 passed / 同样 6 failed。既有 market_data 集成测试调用不存在的 Connection.executemany;旧迁移测试日志配置导致后续 5 个日志断言失败。雷达相关 110 项在独立运行时全部通过。
- 前端 `pnpm check` 的格式、lint 和类型均通过;全量 Vitest 为 148 passed / 4 failed。修改前 selection 页面同样 4 个执行状态弹窗测试失败(其余 20 个通过)。
这些既有选股/行情问题未在本任务扩展修复。前端 `.prettierignore` 补充已被 Git 忽略的 `.playwright-cli`,避免旧浏览器快照影响格式检查。
## 生产应用步骤(尚未执行)
生产 Compose 的完整构建、迁移、重算顺序见 `docs/market-data-sync.md`。以下为直接使用 Python 环境时的等价命令。
在目标环境确认 `ZHIXING_DATABASE_URL` 指向正确数据库,先执行迁移,再上线代码并重算现有日期区间:
```bash
cd zhixing-server
uv run alembic upgrade head
uv run sector-radar-build --recompute --start-date 2026-08-28 --end-date 2026-09-21
```
日期应覆盖需要展示的已有历史;命令只处理区间内已有成功发布。单日评分需要连续 5 个交易日成交额,波段评分需要连续 10 个交易日输入;要比较近 5 日波段名次,还需要相应更早窗口。缺失日期保持未知,重算不补造原始行情或成员数据。
新列允许旧行为 NULL,旧发布保留追溯。回退需切回保留的旧版本发布;不能在仍运行新代码时直接删列。当前代码库没有自动生产回退动作,本任务没有执行任何生产状态变更。
## 预览证据
- `output/playwright/radar-rank-change-desktop.png`
- `output/playwright/radar-swing-desktop.png`
- `output/playwright/radar-ratio-desktop.png`
- `output/playwright/radar-rank-change-mobile.png`
- `output/playwright/radar-missing-history-mobile.png`
本地预览:`http://127.0.0.1:15173/sector-radar?view=rank_change&tradeDate=2026-09-21`。前端端口 15173,后端端口 18081,数据库容器 `zhixing-radar-score-test`(本地端口 55439);均用于隔离验收。
## 提交前复核
本轮独立只读检查由 `/root/radar_commit_review` 完成,未发现新增正确性或部署阻塞;范围内 Ruff/Pyright 和 diff 检查通过。Compose Job 的 entrypoint 与新版 CLI 参数已核对,并使用无生产凭据的配置通过 `docker compose config --quiet`。线上操作说明已补入 `docs/market-data-sync.md`;本轮只执行 Git 提交与推送,不执行线上迁移、重算或部署。