feat(sector-radar): add weighted scores and rank-change views
This commit is contained in:
@@ -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/09-21-radar-weighted-score-rank-change/research/formula-evidence.md", "reason": "已验证的评分结构及数值证据边界"}
|
||||
{"file": ".trellis/tasks/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/09-21-radar-weighted-score-rank-change/research/formula-evidence.md", "reason": "已验证的评分结构及数值证据边界"}
|
||||
{"file": ".trellis/tasks/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 契约、前端及测试,以及历史重算命令。
|
||||
- 不包含调整证券母集、将网站当前成员回填历史、额外的榜单导出或搜索功能重写。
|
||||
- 本地实现与非破坏性验证按任务执行;生产迁移、生产历史重算、部署及提交不在本轮已获授权的执行范围内。
|
||||
|
||||
## 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`。本地实现已交付;生产迁移、重算、部署与 Git 提交均未执行。
|
||||
|
||||
用户已于本轮授权提交并推送。本任务的实现和范围内验证完成;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,40 @@
|
||||
{
|
||||
"id": "radar-weighted-score-rank-change",
|
||||
"name": "radar-weighted-score-rank-change",
|
||||
"title": "雷达加权评分与排名变化复刻",
|
||||
"description": "根据已验证的 OneChart 评分公式,为单日和波段榜增加加权评分,并复刻排名变化的排序基准、1至5日窗口和双榜交互。",
|
||||
"status": "in_progress",
|
||||
"dev_type": "fullstack",
|
||||
"scope": "sector-radar",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "yuxuanhui",
|
||||
"assignee": "yuxuanhui",
|
||||
"createdAt": "2026-09-21",
|
||||
"completedAt": null,
|
||||
"branch": "develop",
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"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": "本地实现与范围内验收完成;用户已授权提交并推送。后端雷达 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."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# 实施与验证记录
|
||||
|
||||
2026-09-21,本地实施完成。实现轮结束时尚未提交;本轮用户已授权提交并推送。生产数据库未迁移、未重算,未部署。
|
||||
|
||||
## 已实现
|
||||
|
||||
- 单日/波段流入率增加镜像加权评分列,保留 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 提交与推送,不执行线上迁移、重算或部署。
|
||||
Reference in New Issue
Block a user