Compare commits
10 Commits
bd84ddd2f2
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| be9c35eadd | |||
| 4af96cee4b | |||
| ca778fe38f | |||
| 62c66f02ad | |||
| 1197b78f9a | |||
| 39f8fe5fbf | |||
| d79e78a411 | |||
| a3e51e2dae | |||
| fad932b0de | |||
| f020362fb0 |
@@ -8,8 +8,8 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
runs-on: tencent-prod
|
||||
timeout-minutes: 60
|
||||
|
||||
env:
|
||||
COMPOSE_PROJECT_NAME: zhixing-system
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"name": "optimize-stock-list-radar",
|
||||
"title": "优化选股列表与资金雷达看板布局",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
@@ -11,7 +11,7 @@
|
||||
"creator": "codex",
|
||||
"assignee": "codex",
|
||||
"createdAt": "2026-09-01",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-09-25",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"name": "add-gold-brick-strategy",
|
||||
"title": "新增金砖共振选股策略",
|
||||
"description": "按收盘后、qfq、沪深非ST口径新增金砖共振策略,复用B1指标并增加可复制诊断日志。",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
@@ -11,7 +11,7 @@
|
||||
"creator": "yuxuanhui",
|
||||
"assignee": "yuxuanhui",
|
||||
"createdAt": "2026-09-04",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-09-25",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"name": "selection-layout-sector-filter",
|
||||
"title": "选股页面迭代:布局重构与板块筛选",
|
||||
"description": "",
|
||||
"status": "planning",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
@@ -11,7 +11,7 @@
|
||||
"creator": "yuxuanhui",
|
||||
"assignee": "yuxuanhui",
|
||||
"createdAt": "2026-09-05",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-09-25",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"name": "capital-radar-daily-detail",
|
||||
"title": "资金雷达:单日榜单与板块详情",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
@@ -11,7 +11,7 @@
|
||||
"creator": "yuxuanhui",
|
||||
"assignee": "yuxuanhui",
|
||||
"createdAt": "2026-09-06",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-09-25",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"name": "api-performance-diagnosis",
|
||||
"title": "接口性能诊断与缓存方案评估",
|
||||
"description": "优化资金雷达详情、历史与榜单读取;无 Redis、无迁移,待用户发布验证。",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
@@ -11,7 +11,7 @@
|
||||
"creator": "yuxuanhui",
|
||||
"assignee": "yuxuanhui",
|
||||
"createdAt": "2026-09-07",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-09-25",
|
||||
"branch": "develop",
|
||||
"base_branch": "develop",
|
||||
"worktree_path": null,
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 研究设计
|
||||
|
||||
这是只读分析任务,不进入产品实现阶段。
|
||||
|
||||
公开证据链:页面展示 → 实际引用脚本 → 实际请求的公开数据 → 评分与排名字段。数据库证据链:库表目录 → 字段及单位 → 重叠交易日原始值 → 候选公式复算 → 与公开评分比较。
|
||||
|
||||
优先检验可解释的低自由度公式。分开检验资金比率、横截面排序/归一化、时间窗口聚合;用多日和不同板块类型验证,避免单点拟合。识别数据修订、单位换算、成分股聚合与板块原始数据的口径差异。
|
||||
|
||||
数据库连接强制 default_transaction_read_only,设置查询超时。仅在本机保留任务所需数据;公开资料可以缓存供复核,凭据不落盘。报告不将无法唯一识别的参数写成确定结论。
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 分析步骤(无产品实现)
|
||||
|
||||
- [x] 读取网站公开脚本和数据,记录字段、日期和来源。
|
||||
- [x] 只读确认数据库版本、数据表及覆盖范围。
|
||||
- [x] 建立日期、板块和单位映射,对比原始输入。
|
||||
- [x] 逐层验证单日评分和波段评分候选公式,记录误差。
|
||||
- [x] 核验关键结论,完成研究记录与用户答复。
|
||||
|
||||
验证使用实际数据计算与证据核对;不运行与分析无关的产品测试。任务不包含代码实施、共享知识推广、提交和发布。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 还原 OneChart 波段与单日资金流评分
|
||||
|
||||
## Goal
|
||||
|
||||
分析 https://onechartlab.com/ 板块资金雷达的波段流入率、单日流入率及其加权评分,结合用户本机 PostgreSQL 数据给出可复核的公式证据、复算结果和不确定性。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 区分原始资金比率、评分和排名,明确时间窗口、权重、标准化、排名池和缺失数据规则。
|
||||
- 优先读取网站实际公开的 HTML、脚本和数据;不将本项目独立指标策略视为该站点真实公式。
|
||||
- 数据库仅使用只读连接及有范围限制的 SELECT,先确认可用库、表、字段与日期。
|
||||
- 使用相同日期、板块标识和数据口径进行多样本验证,记录误差及候选公式可识别性。
|
||||
- 用户已同意创建任务并记录分析。只修改当前任务记录,不修改产品代码、数据库或共享规格,不提交或发布。
|
||||
- 凭据不写入任务记录、研究脚本、结果文件或报告;本机数据不传给外部服务。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] 列出评分相关公开字段及来源,说明公式是否直接公开。
|
||||
- [x] 给出单日与波段评分的可验证公式,或明确最有依据的候选公式和未解决参数。
|
||||
- [x] 用数据库与网站重叠样本核验,报告样本范围、误差和差异原因。
|
||||
- [x] 保存必要分析记录与可复算证据,最终回答清楚区分事实、推断和限制。
|
||||
|
||||
## 结果
|
||||
|
||||
公开原始字段可精确重现最近 12 日 9,492 条记录,两种评分最大绝对误差约 3.41e-13;数据库最新日原始快照试算平均误差为单日 0.9783 分、波段 1.9076 分,个别板块仍有较大输入差异。详见 `research/findings.md` 和 `research/database-validation.md`。本研究已完成,没有产品实施待批准。
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"normalized_aggregate": {
|
||||
"joined_rows": 12654,
|
||||
"joined_dates": 16,
|
||||
"latest_rows": 791,
|
||||
"Ratio": {
|
||||
"checked_rows": 791,
|
||||
"mean_absolute_error": 4.66428796165067,
|
||||
"max_absolute_error": 129.76729492953035,
|
||||
"same_one_decimal_display": 204,
|
||||
"same_final_rank": 215
|
||||
},
|
||||
"Swing": {
|
||||
"checked_rows": 791,
|
||||
"mean_absolute_error": 4.803044287121097,
|
||||
"max_absolute_error": 179.7496672672861,
|
||||
"same_one_decimal_display": 96,
|
||||
"same_final_rank": 199
|
||||
},
|
||||
"turnover_within_1_01_yuan": 294,
|
||||
"weight_within_1e_10": 280,
|
||||
"examples": [
|
||||
{
|
||||
"ts_code": "BK0581.DC",
|
||||
"index_name": "智能电网 (概念)",
|
||||
"ratio_db": 0.01288923245126,
|
||||
"weight_db": 1.08086957077924,
|
||||
"pred_Ratio_Score": 684.0285689472486,
|
||||
"Ratio_Score": 789.4114202884311,
|
||||
"pred_Swing_Score": 386.3978175732549,
|
||||
"Swing_Score": 433.9148866486079
|
||||
},
|
||||
{
|
||||
"ts_code": "BK0615.DC",
|
||||
"index_name": "中药概念 (概念)",
|
||||
"ratio_db": 0.080175906138025,
|
||||
"weight_db": 1.041522634544339,
|
||||
"pred_Ratio_Score": 1036.4911242325306,
|
||||
"Ratio_Score": 1036.9806099966886,
|
||||
"pred_Swing_Score": 825.1676911365778,
|
||||
"Swing_Score": 823.0404356041679
|
||||
},
|
||||
{
|
||||
"ts_code": "BK0653.DC",
|
||||
"index_name": "养老概念 (概念)",
|
||||
"ratio_db": 0.065820711061371,
|
||||
"weight_db": 1.050692084122948,
|
||||
"pred_Ratio_Score": 1038.0025661987577,
|
||||
"Ratio_Score": 1035.4711369170884,
|
||||
"pred_Swing_Score": 1005.0098195958633,
|
||||
"Swing_Score": 1005.0161034783504
|
||||
},
|
||||
{
|
||||
"ts_code": "BK1657.DC",
|
||||
"index_name": "病原体防治 (概念)",
|
||||
"ratio_db": 0.064807487656449,
|
||||
"weight_db": 1.05614239070725,
|
||||
"pred_Ratio_Score": 1040.8359792477243,
|
||||
"Ratio_Score": 1038.383599887465,
|
||||
"pred_Swing_Score": 829.0972873909569,
|
||||
"Swing_Score": 830.4517488043484
|
||||
}
|
||||
]
|
||||
},
|
||||
"raw_snapshot_reaggregation": {
|
||||
"joined_rows": 8701,
|
||||
"joined_dates": 11,
|
||||
"latest_rows": 791,
|
||||
"Ratio": {
|
||||
"checked_rows": 791,
|
||||
"mean_absolute_error": 0.9783318452555938,
|
||||
"max_absolute_error": 105.32457821281037,
|
||||
"same_one_decimal_display": 581,
|
||||
"same_final_rank": 576
|
||||
},
|
||||
"Swing": {
|
||||
"checked_rows": 791,
|
||||
"mean_absolute_error": 1.9075528008117222,
|
||||
"max_absolute_error": 150.16516952571226,
|
||||
"same_one_decimal_display": 318,
|
||||
"same_final_rank": 363
|
||||
},
|
||||
"turnover_within_1_01_yuan": 610,
|
||||
"weight_within_1e_10": 496,
|
||||
"examples": [
|
||||
{
|
||||
"ts_code": "BK0581.DC",
|
||||
"index_name": "智能电网 (概念)",
|
||||
"ratio_db": 0.012878069596386,
|
||||
"weight_db": 1.080961651218729,
|
||||
"pred_Ratio_Score": 684.0868420756208,
|
||||
"Ratio_Score": 789.4114202884311,
|
||||
"pred_Swing_Score": 387.73624445889186,
|
||||
"Swing_Score": 433.9148866486079
|
||||
},
|
||||
{
|
||||
"ts_code": "BK0615.DC",
|
||||
"index_name": "中药概念 (概念)",
|
||||
"ratio_db": 0.079794956978515,
|
||||
"weight_db": 1.041976772408121,
|
||||
"pred_Ratio_Score": 1036.9430681935887,
|
||||
"Ratio_Score": 1036.9806099966886,
|
||||
"pred_Swing_Score": 823.0106390759795,
|
||||
"Swing_Score": 823.0404356041679
|
||||
},
|
||||
{
|
||||
"ts_code": "BK0653.DC",
|
||||
"index_name": "养老概念 (概念)",
|
||||
"ratio_db": 0.065820711061371,
|
||||
"weight_db": 1.050692084122948,
|
||||
"pred_Ratio_Score": 1035.4646626139197,
|
||||
"Ratio_Score": 1035.4711369170884,
|
||||
"pred_Swing_Score": 1005.0098195958633,
|
||||
"Swing_Score": 1005.0161034783504
|
||||
},
|
||||
{
|
||||
"ts_code": "BK1657.DC",
|
||||
"index_name": "病原体防治 (概念)",
|
||||
"ratio_db": 0.064737394636734,
|
||||
"weight_db": 1.05622244732493,
|
||||
"pred_Ratio_Score": 1038.3636136745088,
|
||||
"Ratio_Score": 1038.383599887465,
|
||||
"pred_Swing_Score": 829.160133769571,
|
||||
"Swing_Score": 830.4517488043484
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# PostgreSQL 独立核验
|
||||
|
||||
## 查询与范围
|
||||
|
||||
数据库:`zhixing-system`,PostgreSQL 18.4。本次通过用户授权的 SSH 隧道访问;连接参数强制 `default_transaction_read_only=on`、`statement_timeout=30000`、`lock_timeout=2000`,实测 `transaction_read_only=on`。只执行目录检查和 SELECT/CTE;没有数据库写入。凭据和私钥内容不保存在任务文件中。
|
||||
|
||||
读取了以下数据:
|
||||
|
||||
- `sector_radar_publication`:为每个交易日选取最近成功发布。
|
||||
- `sector_radar_daily_aggregate`:2026-08-28 至 2026-09-21,16 个有成功发布的交易日、16,000 行。9 月 7 日没有成功发布对应的汇总,因此未用别日汇总冒充。
|
||||
- `sector_radar_source_snapshot`:保存的原始 `dc_member`、`daily`、`moneyflow_dc`。对 2026-09-07 至 2026-09-21 的 11 日重新聚合,共 11,000 个板块日;SQL 保存在 `raw-reaggregation.sql`。
|
||||
- 小范围检查 `sector_radar_stock_fact` 状态,以及四个板块的当日成员快照。
|
||||
|
||||
原始快照重聚合按日期和分区选择最近观测,成员使用逐板块分区,股票字段按日期/代码去重;`daily.amount × 1000` 与 `moneyflow_dc.net_amount × 10000` 统一为元。原始重聚合没有沿用产品的沪深 A 股过滤,目的仅是调查网站口径,未改变产品规则。
|
||||
|
||||
股票名单、金额数据仅在本机处理,没有传给外部文档查询或其他服务。
|
||||
|
||||
## 验证设计
|
||||
|
||||
对齐网站实际的日期、板块代码与类型,使用数据库提供的净额与成交额独立计算 r、3/10 日均值及 5 日成交额权重。预测阶段不使用网站的比率、分数或最终排名。
|
||||
|
||||
本次试算将数据库聚合成交额向下取整到元,再使用 `r = 净额/(成交额+100)`、`W = log10(1+MA5(成交额))/10`。这是与公开数值关系一致的候选输入口径,不能把该试算本身当作后端代码证据。
|
||||
|
||||
计算百分位前特意限制到网站的板块池。数据库有概念 504、行业 496,共 1000 个板块;网站是概念 414、行业 377,共 791 个。即使原始金额相同,在不同 N 和不同成员的池中计算百分位也不能复刻网站分数。
|
||||
|
||||
## 最新日结果
|
||||
|
||||
2026-09-21 共 791 条对齐记录:
|
||||
|
||||
| 数据输入 | 单日评分 MAE | 单日最大误差 | 单日一位小数一致 | 波段评分 MAE | 波段最大误差 | 波段一位小数一致 |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| 当前产品规范化汇总 | 4.6643 | 129.7673 | 204/791 | 4.8030 | 179.7497 | 96/791 |
|
||||
| 库中原始快照重新聚合 | 0.9783 | 105.3246 | 581/791 | 1.9076 | 150.1652 | 318/791 |
|
||||
|
||||
原始快照重聚合后,610/791 个最新日成交额与从网站金额/比率反求的成交额相差不超过 1.01 元;496/791 个近五日成交额权重达到 `1e-10` 内一致。这为“近五日成交额取对数作为权重”提供了不依赖网站评分预测输入的数据库佐证。
|
||||
|
||||
实际例子:
|
||||
|
||||
| 板块 | 数据库单日复算 | 网站单日分数 | 数据库波段复算 | 网站波段分数 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 中药概念 | 1036.9431 | 1036.9806 | 823.0106 | 823.0404 |
|
||||
| 养老概念 | 1035.4647 | 1035.4711 | 1005.0098 | 1005.0161 |
|
||||
| 病原体防治 | 1038.3636 | 1038.3836 | 829.1601 | 830.4517 |
|
||||
| 智能电网 | 684.0868 | 789.4114 | 387.7362 | 433.9149 |
|
||||
|
||||
不能只报告平均误差而忽略个别大偏差;数据库原始快照尚未逐板块精确复刻网站输入。本次请求中的评分机制已完成数值还原,但输入采集、板块池和成员版本的完整复刻属于进一步工作。
|
||||
|
||||
## 已确认的输入差异
|
||||
|
||||
1. **股票范围不同。** 产品 `normalize.py:242` 起要求当前上市、沪深证券,排除北交所/B 股;`facts.py:72` 起只将 `AVAILABLE` 股票累加到板块金额。最新日事实中有 `lifecycle_invalid=435`、`suspended=12`、`available=5209`。改用原始快照后误差显著减少,但没有完全消失。
|
||||
2. **板块池不同。** 1000 与 791 的差异已在上述比较中控制;真正独立生产还需要明确网站选择这 791 个板块的规则。
|
||||
3. **公开成员表与数据库当日快照不同。** 按股票代码去除交易所后缀再比较:
|
||||
|
||||
| 板块 | 库内成员 | 网站公开成员 | 交集 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 智能电网 | 197 | 195 | 190 |
|
||||
| 中药概念 | 146 | 145 | 144 |
|
||||
| 碳交易 | 142 | 138 | 137 |
|
||||
| 超跌股 | 167 | 22 | 7 |
|
||||
|
||||
智能电网库内独有 `002851/003043/301236/301669/605336/688187/920222`;网站公开表独有 `001388/002063/300140/600522/920375`。完整差集见 `member-differences.json`。网站这份 `CONSTITUENT_MAP` 不是按交易日分片的历史成员证据,不能据此认定所有历史评分都使用同一份名单;这里证明的是输入版本确实存在差异,不宣称它解释了每一分残差。
|
||||
|
||||
4. **上游净额也不完全一致。** 即使成交额近似一致,个股资金流按万元提供的小数精度、板块级净额来源、成员与观测时点仍可能造成净额差异。网站页脚同时提及东财板块日线资金流;本数据库没有对应 `moneyflow_ind_dc` 快照,不能将两种来源强行视为逐值相同。
|
||||
5. **9 月 7 日原始资金流不完整。** 该日重聚合样本的资金流覆盖明显不足,不把它用于声称全部 11 日的评分准确度;最终 9 月 21 日的最近十日窗口从 9 月 8 日开始。
|
||||
|
||||
第 1–3 项有直接目录、代码和数值证据;第 4 项中的具体上游精度与发布时间机制尚未取得网站构建端证据,因此保留为差异候选原因。
|
||||
|
||||
## 保存与复现
|
||||
|
||||
- `db-raw-inputs.json.gz`:原始快照重聚合结果。
|
||||
- `db-normalized-inputs.json.gz`:当前产品汇总,作为对照。
|
||||
- `db-source-metadata.json`:库版本、只读设置及各数据源覆盖范围。
|
||||
- `database_reproduce.py`:只读取固定文件,在对齐的排名池中独立计算。
|
||||
- `database-validation.json`:精确误差、显示与排名一致数量。
|
||||
|
||||
运行 `database_reproduce.py` 不需要数据库凭据或在线连接。所有产物限于当前 Trellis 任务;未修改产品实现、共享规格或生产数据。
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"""使用已读取的 PostgreSQL 金额快照独立算分,无数据库连接和凭据。"""
|
||||
|
||||
import gzip
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from reproduce import ROOT
|
||||
|
||||
|
||||
def load(name: str) -> list[dict]:
|
||||
return json.loads(gzip.decompress((ROOT / name).read_bytes()))["rows"]
|
||||
|
||||
|
||||
def compare(public: pd.DataFrame, rows: list[dict], normalized: bool) -> dict:
|
||||
"""对齐网站实际排名池;评分只使用库内净额和成交额构造。"""
|
||||
source = pd.DataFrame(rows)
|
||||
source["ts_code"] = source["sector_code"]
|
||||
keys = ["trade_date", "ts_code"]
|
||||
if normalized:
|
||||
source["type"] = source["sector_type"].map({"concept": "概念板块", "industry": "行业板块"})
|
||||
keys.append("type")
|
||||
for field in ["net_amount_yuan", "turnover_yuan"]:
|
||||
source[field] = pd.to_numeric(source[field])
|
||||
frame = public.merge(source, on=keys, validate="one_to_one").sort_values(["ts_code", "trade_date"])
|
||||
frame["amount_db"] = np.floor(frame["turnover_yuan"])
|
||||
frame["ratio_db"] = frame["net_amount_yuan"] / (frame["amount_db"] + 100)
|
||||
for field, windows in [("amount_db", [5]), ("ratio_db", [3, 10])]:
|
||||
for window in windows:
|
||||
frame[f"{field}{window}"] = frame.groupby("ts_code")[field].transform(
|
||||
lambda values: values.rolling(window, min_periods=window).mean()
|
||||
)
|
||||
frame["weight_db"] = np.log10(frame["amount_db5"] + 1) / 10
|
||||
groups = frame.groupby(["trade_date", "type"])
|
||||
frame["pred_Ratio_Score"] = 1000 * groups["ratio_db"].rank(pct=True) * frame["weight_db"]
|
||||
frame["pred_Swing_Score"] = (
|
||||
500 * (groups["ratio_db3"].rank(pct=True) + groups["ratio_db10"].rank(pct=True))
|
||||
* frame["weight_db"]
|
||||
)
|
||||
latest = frame[frame["trade_date"].eq("2026-09-21")].copy()
|
||||
result = {"joined_rows": len(frame), "joined_dates": frame["trade_date"].nunique(), "latest_rows": len(latest)}
|
||||
for metric in ["Ratio", "Swing"]:
|
||||
expected, prediction = f"{metric}_Score", f"pred_{metric}_Score"
|
||||
errors = (latest[prediction] - latest[expected]).abs()
|
||||
ranks = latest.groupby("type")[prediction].rank(ascending=False)
|
||||
result[metric] = {
|
||||
"checked_rows": int(errors.notna().sum()), "mean_absolute_error": errors.mean(),
|
||||
"max_absolute_error": errors.max(),
|
||||
"same_one_decimal_display": int(latest[prediction].round(1).eq(latest[expected].round(1)).sum()),
|
||||
"same_final_rank": int(ranks.eq(latest[f"{metric}_RankPos"]).sum()),
|
||||
}
|
||||
# 从网站两个原始字段得到的成交额仅用于末端验证,不参与库内评分预测。
|
||||
target_turnover = latest["Amount_Raw_BN"] * 1e8 / latest["Ratio_Raw_Pct"] - 100
|
||||
result["turnover_within_1_01_yuan"] = int(latest["turnover_yuan"].sub(target_turnover).abs().lt(1.01).sum())
|
||||
website_weight = latest["Ratio_Score"] / (1000 * latest.groupby("type")["Ratio_Raw_Pct"].rank(pct=True))
|
||||
result["weight_within_1e_10"] = int(latest["weight_db"].sub(website_weight).abs().lt(1e-10).sum())
|
||||
examples = latest[latest["ts_code"].isin(["BK0615.DC", "BK0653.DC", "BK1657.DC", "BK0581.DC"])][[
|
||||
"ts_code", "index_name", "ratio_db", "weight_db", "pred_Ratio_Score", "Ratio_Score", "pred_Swing_Score", "Swing_Score"
|
||||
]]
|
||||
result["examples"] = json.loads(examples.to_json(orient="records", force_ascii=False, double_precision=15))
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
public = pd.DataFrame(load("public-inputs.json.gz"))
|
||||
result = {
|
||||
"normalized_aggregate": compare(public, load("db-normalized-inputs.json.gz"), True),
|
||||
"raw_snapshot_reaggregation": compare(public, load("db-raw-inputs.json.gz"), False),
|
||||
}
|
||||
(ROOT / "database-validation.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"postgres_version": "18.4",
|
||||
"transaction_read_only": "on",
|
||||
"sources": [
|
||||
{
|
||||
"api_name": "daily",
|
||||
"first_date": "2026-08-28",
|
||||
"last_date": "2026-09-21",
|
||||
"snapshot_count": 17,
|
||||
"source_rows": 94336
|
||||
},
|
||||
{
|
||||
"api_name": "dc_index",
|
||||
"first_date": "2026-08-28",
|
||||
"last_date": "2026-09-21",
|
||||
"snapshot_count": 34,
|
||||
"source_rows": 17000
|
||||
},
|
||||
{
|
||||
"api_name": "dc_member",
|
||||
"first_date": "2026-08-28",
|
||||
"last_date": "2026-09-21",
|
||||
"snapshot_count": 17105,
|
||||
"source_rows": 1678901
|
||||
},
|
||||
{
|
||||
"api_name": "moneyflow",
|
||||
"first_date": "2026-09-07",
|
||||
"last_date": "2026-09-21",
|
||||
"snapshot_count": 11,
|
||||
"source_rows": 61054
|
||||
},
|
||||
{
|
||||
"api_name": "moneyflow_dc",
|
||||
"first_date": "2026-08-28",
|
||||
"last_date": "2026-09-21",
|
||||
"snapshot_count": 102,
|
||||
"source_rows": 101285
|
||||
},
|
||||
{
|
||||
"api_name": "suspend_d",
|
||||
"first_date": "2026-08-28",
|
||||
"last_date": "2026-09-21",
|
||||
"snapshot_count": 17,
|
||||
"source_rows": 193
|
||||
},
|
||||
{
|
||||
"api_name": "trade_cal",
|
||||
"first_date": "2026-08-28",
|
||||
"last_date": "2026-09-21",
|
||||
"snapshot_count": 17,
|
||||
"source_rows": 787
|
||||
}
|
||||
]
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# OneChart 波段与单日加权评分还原
|
||||
|
||||
研究日期:2026-09-21。这是一次只读算法研究,不包含产品修改或数据库写入。
|
||||
|
||||
## 结论与证据等级
|
||||
|
||||
已找到一组低自由度、可直接执行的公式,使用网站公开的原始流入率和净额,精确重现 2026-09-04 至 2026-09-21 共 12 个交易日、9,492 条记录的单日评分、波段评分及最终排名。两个评分最大绝对误差均为 `3.410605131648481e-13`,即浮点运算误差。
|
||||
|
||||
这是**对观测数据的数值还原**,不是取得网站后端源码。不能据此保证所有历史版本、未来版本及无观测的边界情况都采用相同实现。旧研究未识别出权重的结论不再适用于本次验证区间,但本任务没有修改旧报告或共享规格。
|
||||
|
||||
## 公式
|
||||
|
||||
对每个板块及日期,定义:
|
||||
|
||||
- `r = Ratio_Raw_Pct`,以小数表示的当日流入率,页面显示时乘 100。
|
||||
- `F = Amount_Raw_BN × 100000000`,主力净流入金额,单位元。虽然字段包含 `BN`,网站实际展示单位是亿元。
|
||||
- `MA_n(x)`:按板块、日期排序后最近 n 条有效观测的简单均值,包含当日;不擅自补齐公开历史中的缺失记录。稳定验证区间每天都公开了 791 个板块,但部分窗口的前置历史仍有日期缺失。
|
||||
- `P_t(x)`:**同日、同板块类型**的升序排名百分位,`rank(x)/N`,取值从 `1/N` 到 1;概念和行业各自排名。该排名由原始指标计算,不使用网站最终 `*_RankPct` 作为输入。
|
||||
|
||||
### 单日评分
|
||||
|
||||
```text
|
||||
Ratio_Score = 1000 × P_t(r) × W
|
||||
```
|
||||
|
||||
### 波段评分
|
||||
|
||||
```text
|
||||
R3 = MA_3(r)
|
||||
R10 = MA_10(r)
|
||||
Swing_Score = 1000 × [0.5 × P_t(R3) + 0.5 × P_t(R10)] × W
|
||||
```
|
||||
|
||||
关键是**先分别求 3 日、10 日流入率均值的横截面排名,再各乘 50%**。如果改成先把两个均值合成波段流入率,再对合成值排名,就会得到不同评分;2026-09-21 该错误方法平均偏差约 62.54 分。
|
||||
|
||||
### 页面显示的波段流入率与波段净额
|
||||
|
||||
```text
|
||||
Swing_Ratio_Val = 0.5 × MA_3(r) + 0.5 × MA_10(r)
|
||||
Swing_Amount_Val = 0.5 × MA_3(Amount_Raw_BN)
|
||||
+ 0.5 × MA_10(Amount_Raw_BN)
|
||||
```
|
||||
|
||||
所以页面所称“3–10 日多周期协同加权”,在验证数据中可具体化为 **3 日与 10 日两个窗口,各占 50%**。没有证据表明必须引入 4、5、6、7、8、9 日窗口。该波段流入率展开到每日后,最近 3 日每一天占 `13/60 ≈ 21.6667%`,再往前 7 日每一天占 5%;最近三日合计占 65%。这种每日线性展开仅适用于原始波段流入率,不能直接替代带横截面排名的波段评分。
|
||||
|
||||
### 成交额权重 W
|
||||
|
||||
从公开字段可以精确验证的表达式是:
|
||||
|
||||
```text
|
||||
V_proxy = F / r
|
||||
W = log10(MA_5(V_proxy) - 99) / 10
|
||||
```
|
||||
|
||||
若定义与网站计算口径对应的成交额 `A = V_proxy - 100`(元),则等价于:
|
||||
|
||||
```text
|
||||
W = log10(1 + MA_5(A)) / 10
|
||||
r = F / (A + 100)
|
||||
```
|
||||
|
||||
`-99` 由公开数据中的金额/比率关系定位,在 791 个最新日样本中,反求的 5 日成交额与 `MA_5(F/r)` 的差均为约 99 元;使用该修正后,评分误差降至机器精度。单凭公开字段,不能证明后端源码里真的写了“分母加 100 元”,也不能断言这是防零分母常量而不是单位换算产生的等价结果;原始金额、成员和取整口径还需结合数据库核对。
|
||||
|
||||
经济含义是用成交活跃程度调节排名得分,采用对数使规模差异的影响较温和。近 5 日平均成交额为 1 亿、10 亿、100 亿、1000 亿元时,W 约为 0.8、0.9、1.0、1.1。因而评分可以超过 1000;它不是限定在 0–1000 的百分制,也不是收益概率。
|
||||
|
||||
作为交叉校验,JSON 中虽然未用于单日净额榜默认排序的 `Amount_Score` 也满足:
|
||||
|
||||
```text
|
||||
Amount_Score = 1000 × P_t(Amount_Raw_BN) × W
|
||||
```
|
||||
|
||||
## 最新日计算例子
|
||||
|
||||
2026-09-21,概念池 N=414,病原体防治(BK1657.DC):
|
||||
|
||||
| 项目 | 数值 |
|
||||
| --- | ---: |
|
||||
| 单日流入率 | 6.47373795% |
|
||||
| 单日流入率原始百分位 | 407/414 = 0.9830917874 |
|
||||
| 3 日均值的百分位 | 376/414 = 0.9082125604 |
|
||||
| 10 日均值的百分位 | 275/414 = 0.6642512077 |
|
||||
| 近 5 日成交额权重 W | 约 1.056243 |
|
||||
| 单日评分 | 1038.383599887465 |
|
||||
| 波段评分 | 830.4517488043484 |
|
||||
| 网站最终单日/波段排名 | 1 / 47 |
|
||||
|
||||
```text
|
||||
单日 = 1000 × (407/414) × W = 1038.3836 → 页面 1038.4
|
||||
波段 = 1000 × [(376/414 + 275/414)/2] × W = 830.4517 → 页面 830.5
|
||||
```
|
||||
|
||||
它的单日原始流入率并非全池最高,成交额权重加成后,单日综合评分可以排到第一。完整的三板块样例保存在 `worked-examples.json`。
|
||||
|
||||
## 验证范围与限制
|
||||
|
||||
公开数据总计 23,613 行、30 个交易日,覆盖 2026-08-11 至 2026-09-21。最新日 791 个板块:概念 414、行业 377。
|
||||
|
||||
| 验证对象 | 稳定区间样本 | 最大绝对误差 |
|
||||
| --- | ---: | ---: |
|
||||
| Ratio_Score | 9,492 | 3.41e-13 |
|
||||
| Swing_Score | 9,492 | 3.41e-13 |
|
||||
| Swing_Ratio_Val | 9,492 | 2.78e-17 |
|
||||
| Swing_Amount_Val(亿元) | 9,492 | 5.68e-14 |
|
||||
| Amount_Score(交叉校验) | 9,492 | 4.55e-13 |
|
||||
|
||||
最终排名按分数降序完全吻合;`RankPct = 100 × (N - RankPos + 1) / N`。金额榜的最终排名依据是原始净额,不是 Amount_Score。
|
||||
|
||||
复算过程中仅使用日期、板块代码、类型、原始单日流入率和净额;评分、最终排名只在最后比较时读取。因此没有用答案反过来构造预测输入。主会话执行了 `reproduce.py`;独立代理 `/root/score_formula_audit` 已完成同样输入边界下的核验,结果一致。公开前端取证由 `/root/onechart_public_evidence` 完成。
|
||||
|
||||
不能把上述准确度推广到整份 30 日历史:
|
||||
|
||||
- 最早 4/9 个观测缺少足够的 5/10 日前置历史,分别无法计算成交额权重/波段窗口。
|
||||
- 单日评分在 2026-08-17 至 08-26 有差异,最大约 6.680462 分;8 月 27 日起可计算样本吻合。
|
||||
- 波段评分在 2026-08-24 至 09-03 有差异,最大约 261.703574 分。8 月 27 日公开板块只有 707 条,部分后续窗口的输入不全;早期还存在板块集合或数据修订差异,尚未逐项确认原因。
|
||||
- 稳定验证区间没有原始比率或分数并列,不能确定后端的并列排名规则。脚本选用 `average` 仅作为明确的复算约定。
|
||||
- 全量没有 `r=0`,不能由本样本识别零净流入、零成交额、极低流动性和无历史板块的所有边界策略。
|
||||
|
||||
## 公开实现证据
|
||||
|
||||
- [首页](https://onechartlab.com/) 只读取 `${activeTab}_Score`,提示“后端特征算法算出的综合加权值”;本次缓存 `index.html:800–805`。前端没有公开评分构造函数。
|
||||
- 首页 `index.html:1032` 说明“3-10 个交易日多周期协同加权”;具体 50%/50% 来自数值检验,而非这句话本身。
|
||||
- 首页 `index.html:1253–1257` 按板块类型过滤;`1292–1314` 指定 Swing/Ratio 默认按各自分数排序,并用最终 RankPct 切前后 10%。
|
||||
- [radar_manifest.json](https://onechartlab.com/radar_manifest.json) 列出日期分片;已保存本次 manifest。
|
||||
- [完整公开 JSON](https://onechartlab.com/radar_data_latest.json) 和 [2026-09-21 分片](https://onechartlab.com/radar_data/dates/2026-09-21.22531a461a8b.json) 给出数值证据。`public-inputs.json.gz` 保存了所需字段和原始文件 SHA-256,避免未来网站更新导致样本改变。
|
||||
- [Tushare daily](https://tushare.pro/document/2?doc_id=27) 的 amount 单位为千元;[moneyflow_dc](https://tushare.pro/document/2?doc_id=349) 的 net_amount 单位为万元。数据库重聚合分别乘 1000 和 10000 后统一成元。
|
||||
|
||||
## 复现
|
||||
|
||||
在仓库根目录运行:
|
||||
|
||||
```bash
|
||||
zhixing-server/.venv/bin/python .trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/reproduce.py
|
||||
```
|
||||
|
||||
脚本读取固定样本,不需要网络或数据库凭据,输出 `public-validation.json`。本次使用 Python 3.12.11、pandas 3.0.5、NumPy 2.5.1。
|
||||
|
||||
数据库的独立核对另见本目录 `database-validation.md`;研究 SQL 和结果均与生产代码隔离。
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
[
|
||||
{
|
||||
"code": "BK0581.DC",
|
||||
"name": "智能电网",
|
||||
"db_count": 197,
|
||||
"site_count": 195,
|
||||
"intersection_count": 190,
|
||||
"db_only": [
|
||||
"002851",
|
||||
"003043",
|
||||
"301236",
|
||||
"301669",
|
||||
"605336",
|
||||
"688187",
|
||||
"920222"
|
||||
],
|
||||
"site_only": [
|
||||
"001388",
|
||||
"002063",
|
||||
"300140",
|
||||
"600522",
|
||||
"920375"
|
||||
],
|
||||
"observed_at": "2026-09-21 10:40:57.972405+00:00"
|
||||
},
|
||||
{
|
||||
"code": "BK0615.DC",
|
||||
"name": "中药概念",
|
||||
"db_count": 146,
|
||||
"site_count": 145,
|
||||
"intersection_count": 144,
|
||||
"db_only": [
|
||||
"000626",
|
||||
"920367"
|
||||
],
|
||||
"site_only": [
|
||||
"300391"
|
||||
],
|
||||
"observed_at": "2026-09-21 10:41:08.518051+00:00"
|
||||
},
|
||||
{
|
||||
"code": "BK0966.DC",
|
||||
"name": "碳交易",
|
||||
"db_count": 142,
|
||||
"site_count": 138,
|
||||
"intersection_count": 137,
|
||||
"db_only": [
|
||||
"000875",
|
||||
"002734",
|
||||
"600389",
|
||||
"601678",
|
||||
"603612"
|
||||
],
|
||||
"site_only": [
|
||||
"600028"
|
||||
],
|
||||
"observed_at": "2026-09-21 10:43:25.392556+00:00"
|
||||
},
|
||||
{
|
||||
"code": "BK1671.DC",
|
||||
"name": "超跌股",
|
||||
"db_count": 167,
|
||||
"site_count": 22,
|
||||
"intersection_count": 7,
|
||||
"db_only": [
|
||||
"000002",
|
||||
"000010",
|
||||
"000016",
|
||||
"000639",
|
||||
"000677",
|
||||
"002104",
|
||||
"002217",
|
||||
"002227",
|
||||
"002368",
|
||||
"002514",
|
||||
"002542",
|
||||
"002547",
|
||||
"002657",
|
||||
"002691",
|
||||
"002731",
|
||||
"002869",
|
||||
"002891",
|
||||
"300045",
|
||||
"300068",
|
||||
"300100",
|
||||
"300245",
|
||||
"300255",
|
||||
"300290",
|
||||
"300352",
|
||||
"300396",
|
||||
"300430",
|
||||
"300465",
|
||||
"300484",
|
||||
"300492",
|
||||
"300530",
|
||||
"300539",
|
||||
"300584",
|
||||
"300652",
|
||||
"300663",
|
||||
"300682",
|
||||
"300703",
|
||||
"300723",
|
||||
"300779",
|
||||
"300844",
|
||||
"300879",
|
||||
"300896",
|
||||
"300918",
|
||||
"300940",
|
||||
"300995",
|
||||
"301000",
|
||||
"301052",
|
||||
"301076",
|
||||
"301139",
|
||||
"301325",
|
||||
"301498",
|
||||
"301590",
|
||||
"301601",
|
||||
"301622",
|
||||
"301632",
|
||||
"600053",
|
||||
"600180",
|
||||
"600325",
|
||||
"600363",
|
||||
"600418",
|
||||
"600491",
|
||||
"600530",
|
||||
"600702",
|
||||
"600745",
|
||||
"601127",
|
||||
"601865",
|
||||
"601929",
|
||||
"603008",
|
||||
"603189",
|
||||
"603200",
|
||||
"603300",
|
||||
"603359",
|
||||
"603370",
|
||||
"603382",
|
||||
"603392",
|
||||
"603567",
|
||||
"603630",
|
||||
"603718",
|
||||
"603767",
|
||||
"603815",
|
||||
"603848",
|
||||
"605499",
|
||||
"688013",
|
||||
"688066",
|
||||
"688068",
|
||||
"688089",
|
||||
"688121",
|
||||
"688166",
|
||||
"688189",
|
||||
"688201",
|
||||
"688303",
|
||||
"688408",
|
||||
"688496",
|
||||
"688499",
|
||||
"688500",
|
||||
"688567",
|
||||
"688573",
|
||||
"688577",
|
||||
"688588",
|
||||
"688631",
|
||||
"688639",
|
||||
"688648",
|
||||
"688658",
|
||||
"688775",
|
||||
"920001",
|
||||
"920005",
|
||||
"920007",
|
||||
"920056",
|
||||
"920061",
|
||||
"920075",
|
||||
"920090",
|
||||
"920101",
|
||||
"920106",
|
||||
"920108",
|
||||
"920112",
|
||||
"920145",
|
||||
"920146",
|
||||
"920184",
|
||||
"920237",
|
||||
"920239",
|
||||
"920247",
|
||||
"920252",
|
||||
"920263",
|
||||
"920271",
|
||||
"920273",
|
||||
"920274",
|
||||
"920346",
|
||||
"920351",
|
||||
"920375",
|
||||
"920392",
|
||||
"920395",
|
||||
"920414",
|
||||
"920429",
|
||||
"920454",
|
||||
"920469",
|
||||
"920505",
|
||||
"920508",
|
||||
"920522",
|
||||
"920523",
|
||||
"920533",
|
||||
"920578",
|
||||
"920579",
|
||||
"920627",
|
||||
"920634",
|
||||
"920689",
|
||||
"920693",
|
||||
"920719",
|
||||
"920720",
|
||||
"920770",
|
||||
"920781",
|
||||
"920807",
|
||||
"920896",
|
||||
"920906",
|
||||
"920914",
|
||||
"920925",
|
||||
"920926",
|
||||
"920932",
|
||||
"920942",
|
||||
"920982",
|
||||
"920985",
|
||||
"920992"
|
||||
],
|
||||
"site_only": [
|
||||
"000004",
|
||||
"000056",
|
||||
"000638",
|
||||
"002630",
|
||||
"300081",
|
||||
"300344",
|
||||
"300561",
|
||||
"600355",
|
||||
"600599",
|
||||
"600696",
|
||||
"603369",
|
||||
"605199",
|
||||
"688287",
|
||||
"920130",
|
||||
"920305"
|
||||
],
|
||||
"observed_at": "2026-09-21 10:50:20.423741+00:00"
|
||||
}
|
||||
]
|
||||
BIN
Binary file not shown.
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"rows": 23613,
|
||||
"dates": 30,
|
||||
"windows": {
|
||||
"all_available": {
|
||||
"rows": 23613,
|
||||
"dates": 30,
|
||||
"Ratio_Score": {
|
||||
"checked_rows": 20449,
|
||||
"mean_absolute_error": 0.18366711208184938,
|
||||
"max_absolute_error": 6.680461750893414,
|
||||
"errors_above_1e-8": 1651
|
||||
},
|
||||
"Swing_Score": {
|
||||
"checked_rows": 16494,
|
||||
"mean_absolute_error": 0.5097113919412869,
|
||||
"max_absolute_error": 261.7035740929656,
|
||||
"errors_above_1e-8": 3199
|
||||
},
|
||||
"Amount_Score": {
|
||||
"checked_rows": 20449,
|
||||
"mean_absolute_error": 0.1882733317113178,
|
||||
"max_absolute_error": 7.649258428013809,
|
||||
"errors_above_1e-8": 1651
|
||||
},
|
||||
"Swing_Ratio_Val": {
|
||||
"checked_rows": 16494,
|
||||
"mean_absolute_error": 6.7217831480409905e-06,
|
||||
"max_absolute_error": 0.012044989384502386,
|
||||
"errors_above_1e-8": 28
|
||||
},
|
||||
"Swing_Amount_Val": {
|
||||
"checked_rows": 16494,
|
||||
"mean_absolute_error": 0.008108515254276354,
|
||||
"max_absolute_error": 24.526995094,
|
||||
"errors_above_1e-8": 28
|
||||
}
|
||||
},
|
||||
"2026-09-04_to_2026-09-21": {
|
||||
"rows": 9492,
|
||||
"dates": 12,
|
||||
"Ratio_Score": {
|
||||
"checked_rows": 9492,
|
||||
"mean_absolute_error": 3.2767252412841164e-14,
|
||||
"max_absolute_error": 3.410605131648481e-13,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Swing_Score": {
|
||||
"checked_rows": 9492,
|
||||
"mean_absolute_error": 3.441106555949961e-14,
|
||||
"max_absolute_error": 3.410605131648481e-13,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Amount_Score": {
|
||||
"checked_rows": 9492,
|
||||
"mean_absolute_error": 3.245420975553957e-14,
|
||||
"max_absolute_error": 4.547473508864641e-13,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Swing_Ratio_Val": {
|
||||
"checked_rows": 9492,
|
||||
"mean_absolute_error": 4.942276978457954e-18,
|
||||
"max_absolute_error": 2.7755575615628914e-17,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Swing_Amount_Val": {
|
||||
"checked_rows": 9492,
|
||||
"mean_absolute_error": 2.053051893003829e-15,
|
||||
"max_absolute_error": 5.684341886080802e-14,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Ratio_ranking": {
|
||||
"rank_position_mismatches": 0,
|
||||
"rank_percentile_max_error": 1.4210854715202004e-14
|
||||
},
|
||||
"Swing_ranking": {
|
||||
"rank_position_mismatches": 0,
|
||||
"rank_percentile_max_error": 1.4210854715202004e-14
|
||||
}
|
||||
},
|
||||
"2026-09-14_to_2026-09-21": {
|
||||
"rows": 4746,
|
||||
"dates": 6,
|
||||
"Ratio_Score": {
|
||||
"checked_rows": 4746,
|
||||
"mean_absolute_error": 3.302167267444722e-14,
|
||||
"max_absolute_error": 3.410605131648481e-13,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Swing_Score": {
|
||||
"checked_rows": 4746,
|
||||
"mean_absolute_error": 3.4959814225990485e-14,
|
||||
"max_absolute_error": 3.410605131648481e-13,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Amount_Score": {
|
||||
"checked_rows": 4746,
|
||||
"mean_absolute_error": 3.260761983972608e-14,
|
||||
"max_absolute_error": 4.547473508864641e-13,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Swing_Ratio_Val": {
|
||||
"checked_rows": 4746,
|
||||
"mean_absolute_error": 4.952800461538844e-18,
|
||||
"max_absolute_error": 2.7755575615628914e-17,
|
||||
"errors_above_1e-8": 0
|
||||
},
|
||||
"Swing_Amount_Val": {
|
||||
"checked_rows": 4746,
|
||||
"mean_absolute_error": 2.056700338399156e-15,
|
||||
"max_absolute_error": 5.684341886080802e-14,
|
||||
"errors_above_1e-8": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"AVAILABLE_DATES":["2026-08-11","2026-08-12","2026-08-13","2026-08-14","2026-08-17","2026-08-18","2026-08-19","2026-08-20","2026-08-21","2026-08-24","2026-08-25","2026-08-26","2026-08-27","2026-08-28","2026-08-31","2026-09-01","2026-09-02","2026-09-03","2026-09-04","2026-09-07","2026-09-08","2026-09-09","2026-09-10","2026-09-11","2026-09-14","2026-09-15","2026-09-16","2026-09-17","2026-09-18","2026-09-21"],"DATA_SOURCE":"","LATEST_DATE":"2026-09-21","files":{"constituents":"radar_data/constituents.25ffae2b8f53.json","dates":{"2026-08-11":"radar_data/dates/2026-08-11.3ed082fdf5aa.json","2026-08-12":"radar_data/dates/2026-08-12.a61e39835037.json","2026-08-13":"radar_data/dates/2026-08-13.db6f36c2dd06.json","2026-08-14":"radar_data/dates/2026-08-14.fb36a77c6ae4.json","2026-08-17":"radar_data/dates/2026-08-17.d69d703faef6.json","2026-08-18":"radar_data/dates/2026-08-18.2c339eebabd6.json","2026-08-19":"radar_data/dates/2026-08-19.fca1cdf24170.json","2026-08-20":"radar_data/dates/2026-08-20.88ff543739cc.json","2026-08-21":"radar_data/dates/2026-08-21.a08c301a8dd7.json","2026-08-24":"radar_data/dates/2026-08-24.9135129011a3.json","2026-08-25":"radar_data/dates/2026-08-25.9393e22d8754.json","2026-08-26":"radar_data/dates/2026-08-26.f49c721846fb.json","2026-08-27":"radar_data/dates/2026-08-27.2dc5f5627eeb.json","2026-08-28":"radar_data/dates/2026-08-28.c92100ea9ae1.json","2026-08-31":"radar_data/dates/2026-08-31.c094ac3098c9.json","2026-09-01":"radar_data/dates/2026-09-01.e6253d4364f6.json","2026-09-02":"radar_data/dates/2026-09-02.1e4a8e6102d5.json","2026-09-03":"radar_data/dates/2026-09-03.ec356ce27a57.json","2026-09-04":"radar_data/dates/2026-09-04.deaf468364a2.json","2026-09-07":"radar_data/dates/2026-09-07.874c2aaa1925.json","2026-09-08":"radar_data/dates/2026-09-08.f93f7b9b2eab.json","2026-09-09":"radar_data/dates/2026-09-09.7a9919c800d9.json","2026-09-10":"radar_data/dates/2026-09-10.29e5d3c66a03.json","2026-09-11":"radar_data/dates/2026-09-11.738e1fec6654.json","2026-09-14":"radar_data/dates/2026-09-14.6b21a73a38f6.json","2026-09-15":"radar_data/dates/2026-09-15.9d5afec9a676.json","2026-09-16":"radar_data/dates/2026-09-16.48d01087bfbd.json","2026-09-17":"radar_data/dates/2026-09-17.04d53b204a8f.json","2026-09-18":"radar_data/dates/2026-09-18.b7a298aedf8d.json","2026-09-21":"radar_data/dates/2026-09-21.22531a461a8b.json"},"rank_history":"radar_data/rank_history.1d389cbfa3e1.json"},"schema_version":1,"version":"7a4fc78a8366"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
WITH s AS MATERIALIZED (SELECT DISTINCT ON (api_name,target_trade_date,partition_key) api_name,target_trade_date,partition_key,payload,observed_at FROM sector_radar_source_snapshot WHERE target_trade_date BETWEEN '2026-09-07' AND '2026-09-21' AND api_name IN ('dc_member','daily','moneyflow_dc') ORDER BY api_name,target_trade_date,partition_key,observed_at DESC), d AS (SELECT DISTINCT ON (target_trade_date,e->>'ts_code') target_trade_date,e->>'ts_code' AS code,(e->>'amount')::numeric*1000 AS amount FROM s CROSS JOIN LATERAL jsonb_array_elements(payload) e WHERE api_name='daily' ORDER BY target_trade_date,e->>'ts_code',observed_at DESC), f AS (SELECT DISTINCT ON (target_trade_date,e->>'ts_code') target_trade_date,e->>'ts_code' AS code,(e->>'net_amount')::numeric*10000 AS net FROM s CROSS JOIN LATERAL jsonb_array_elements(payload) e WHERE api_name='moneyflow_dc' ORDER BY target_trade_date,e->>'ts_code',observed_at DESC), m AS (SELECT DISTINCT target_trade_date,e->>'ts_code' AS sector,e->>'con_code' AS code FROM s CROSS JOIN LATERAL jsonb_array_elements(payload) e WHERE api_name='dc_member' AND partition_key<>'all') SELECT m.target_trade_date AS trade_date,m.sector AS sector_code,count(*) AS member_count,count(d.amount) AS daily_count,count(f.net) AS flow_count,sum(d.amount) AS turnover_yuan,sum(f.net) AS net_amount_yuan,sum(d.amount) FILTER (WHERE m.code LIKE '%.BJ') AS bj_turnover_yuan FROM m LEFT JOIN d ON d.target_trade_date=m.target_trade_date AND d.code=m.code LEFT JOIN f ON f.target_trade_date=m.target_trade_date AND f.code=m.code GROUP BY m.target_trade_date,m.sector ORDER BY m.target_trade_date,m.sector
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
"""从固定公开输入复算 OneChart 指标;预测阶段不使用网站评分或排名。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def calculate(rows: list[dict]) -> pd.DataFrame:
|
||||
"""按板块有观测的历史记录计算;预热不足保留 NaN,不填补未知输入。"""
|
||||
frame = pd.DataFrame(rows).sort_values(["ts_code", "trade_date"])
|
||||
if frame.duplicated(["trade_date", "type", "ts_code"]).any():
|
||||
raise ValueError("公开输入存在重复主键")
|
||||
if frame["Ratio_Raw_Pct"].eq(0).any():
|
||||
raise ValueError("零流入率无法通过金额/比率反求成交额;需要原始成交额")
|
||||
|
||||
frame["turnover_proxy_yuan"] = (
|
||||
frame["Amount_Raw_BN"] * 1e8 / frame["Ratio_Raw_Pct"]
|
||||
)
|
||||
for field in ["Ratio_Raw_Pct", "Amount_Raw_BN", "turnover_proxy_yuan"]:
|
||||
for window in [3, 5, 10]:
|
||||
frame[f"{field}_ma{window}"] = frame.groupby("ts_code")[field].transform(
|
||||
lambda series: series.rolling(window, min_periods=window).mean()
|
||||
)
|
||||
|
||||
# -99 是公开金额/比率所能直接验证的代数修正,不把它当成原始成交额。
|
||||
frame["liquidity_weight"] = (
|
||||
np.log10(frame["turnover_proxy_yuan_ma5"] - 99) / 10
|
||||
)
|
||||
groups = frame.groupby(["trade_date", "type"])
|
||||
for field in ["Ratio_Raw_Pct", "Ratio_Raw_Pct_ma3", "Ratio_Raw_Pct_ma10", "Amount_Raw_BN"]:
|
||||
frame[f"{field}_percentile"] = groups[field].rank(method="average", pct=True)
|
||||
|
||||
frame["pred_Ratio_Score"] = (
|
||||
1000 * frame["Ratio_Raw_Pct_percentile"] * frame["liquidity_weight"]
|
||||
)
|
||||
frame["pred_Swing_Score"] = (
|
||||
1000
|
||||
* 0.5
|
||||
* (frame["Ratio_Raw_Pct_ma3_percentile"] + frame["Ratio_Raw_Pct_ma10_percentile"])
|
||||
* frame["liquidity_weight"]
|
||||
)
|
||||
frame["pred_Amount_Score"] = (
|
||||
1000 * frame["Amount_Raw_BN_percentile"] * frame["liquidity_weight"]
|
||||
)
|
||||
frame["pred_Swing_Ratio_Val"] = (
|
||||
0.5 * (frame["Ratio_Raw_Pct_ma3"] + frame["Ratio_Raw_Pct_ma10"])
|
||||
)
|
||||
frame["pred_Swing_Amount_Val"] = (
|
||||
0.5 * (frame["Amount_Raw_BN_ma3"] + frame["Amount_Raw_BN_ma10"])
|
||||
)
|
||||
return frame
|
||||
|
||||
|
||||
def validate(frame: pd.DataFrame) -> dict:
|
||||
"""比较固定公式的预测与网站输出,报告预热、全历史与稳定覆盖窗口。"""
|
||||
metrics = ["Ratio_Score", "Swing_Score", "Amount_Score", "Swing_Ratio_Val", "Swing_Amount_Val"]
|
||||
report = {"rows": len(frame), "dates": frame["trade_date"].nunique(), "windows": {}}
|
||||
for name, subset in [
|
||||
("all_available", frame),
|
||||
("2026-09-04_to_2026-09-21", frame[frame["trade_date"].ge("2026-09-04")]),
|
||||
("2026-09-14_to_2026-09-21", frame[frame["trade_date"].ge("2026-09-14")]),
|
||||
]:
|
||||
result = {"rows": len(subset), "dates": subset["trade_date"].nunique()}
|
||||
for metric in metrics:
|
||||
errors = (subset[metric] - subset[f"pred_{metric}"]).abs().dropna()
|
||||
result[metric] = {
|
||||
"checked_rows": len(errors), "mean_absolute_error": errors.mean(),
|
||||
"max_absolute_error": errors.max(), "errors_above_1e-8": int(errors.gt(1e-8).sum()),
|
||||
}
|
||||
if name == "2026-09-04_to_2026-09-21":
|
||||
groups = subset.groupby(["trade_date", "type"])
|
||||
count = groups["ts_code"].transform("size")
|
||||
for metric in ["Ratio", "Swing"]:
|
||||
ranks = groups[f"pred_{metric}_Score"].rank(ascending=False)
|
||||
percentiles = 100 * (count - ranks + 1) / count
|
||||
result[f"{metric}_ranking"] = {
|
||||
"rank_position_mismatches": int(ranks.ne(subset[f"{metric}_RankPos"]).sum()),
|
||||
"rank_percentile_max_error": percentiles.sub(subset[f"{metric}_RankPct"]).abs().max(),
|
||||
}
|
||||
report["windows"][name] = result
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fixture = json.loads(gzip.decompress((ROOT / "public-inputs.json.gz").read_bytes()))
|
||||
calculated = calculate(fixture["rows"])
|
||||
result = validate(calculated)
|
||||
(ROOT / "public-validation.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
[
|
||||
{
|
||||
"index_name":"中药概念 (概念)",
|
||||
"ts_code":"BK0615.DC",
|
||||
"type":"概念板块",
|
||||
"Ratio_Raw_Pct":0.079794960955587,
|
||||
"Ratio_Raw_Pct_ma3":0.033754406576305,
|
||||
"Ratio_Raw_Pct_ma10":-0.005866010935792,
|
||||
"Ratio_Raw_Pct_percentile":0.995169082125604,
|
||||
"Ratio_Raw_Pct_ma3_percentile":0.929951690821256,
|
||||
"Ratio_Raw_Pct_ma10_percentile":0.64975845410628,
|
||||
"turnover_proxy_yuan_ma5":26311461138.200000762939453,
|
||||
"liquidity_weight":1.042014496452983,
|
||||
"pred_Ratio_Score":1036.9806099966886,
|
||||
"Ratio_Score":1036.9806099966886,
|
||||
"pred_Swing_Score":823.040435604167897,
|
||||
"Swing_Score":823.040435604167897,
|
||||
"Ratio_RankPos":2,
|
||||
"Swing_RankPos":49
|
||||
},
|
||||
{
|
||||
"index_name":"养老概念 (概念)",
|
||||
"ts_code":"BK0653.DC",
|
||||
"type":"概念板块",
|
||||
"Ratio_Raw_Pct":0.065820703429991,
|
||||
"Ratio_Raw_Pct_ma3":0.037193483577325,
|
||||
"Ratio_Raw_Pct_ma10":0.006875783666745,
|
||||
"Ratio_Raw_Pct_percentile":0.985507246376812,
|
||||
"Ratio_Raw_Pct_ma3_percentile":0.949275362318841,
|
||||
"Ratio_Raw_Pct_ma10_percentile":0.963768115942029,
|
||||
"turnover_proxy_yuan_ma5":32135609228.599998474121094,
|
||||
"liquidity_weight":1.050698653636457,
|
||||
"pred_Ratio_Score":1035.471136917088188,
|
||||
"Ratio_Score":1035.471136917088415,
|
||||
"pred_Swing_Score":1005.016103478350374,
|
||||
"Swing_Score":1005.016103478350374,
|
||||
"Ratio_RankPos":3,
|
||||
"Swing_RankPos":2
|
||||
},
|
||||
{
|
||||
"index_name":"病原体防治 (概念)",
|
||||
"ts_code":"BK1657.DC",
|
||||
"type":"概念板块",
|
||||
"Ratio_Raw_Pct":0.064737379547795,
|
||||
"Ratio_Raw_Pct_ma3":0.029828519014713,
|
||||
"Ratio_Raw_Pct_ma10":-0.005451455299003,
|
||||
"Ratio_Raw_Pct_percentile":0.983091787439614,
|
||||
"Ratio_Raw_Pct_ma3_percentile":0.908212560386474,
|
||||
"Ratio_Raw_Pct_ma10_percentile":0.664251207729468,
|
||||
"turnover_proxy_yuan_ma5":36511340146.0,
|
||||
"liquidity_weight":1.056242777281107,
|
||||
"pred_Ratio_Score":1038.383599887464925,
|
||||
"Ratio_Score":1038.383599887464925,
|
||||
"pred_Swing_Score":830.451748804348426,
|
||||
"Swing_Score":830.451748804348426,
|
||||
"Ratio_RankPos":1,
|
||||
"Swing_RankPos":47
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "onechart-score-reconstruction",
|
||||
"name": "onechart-score-reconstruction",
|
||||
"title": "还原 OneChart 波段与单日资金流评分",
|
||||
"description": "只读数值还原 OneChart 单日与波段资金评分;12 日 9492 条公开样本精确重现,并完成 PostgreSQL 原始快照对照。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "yuxuanhui",
|
||||
"assignee": "yuxuanhui",
|
||||
"createdAt": "2026-09-21",
|
||||
"completedAt": "2026-09-21",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "研究完成。没有实施产品修改、修改数据库、共享规格推广或提交。输入版本与成员口径差异详见 research/database-validation.md。",
|
||||
"meta": {
|
||||
"work_kind": "read_only_research",
|
||||
"public_validation_rows": 9492,
|
||||
"public_score_max_abs_error": 3.410605131648481e-13,
|
||||
"database_latest_rows": 791,
|
||||
"database_ratio_mae": 0.9783318452555938,
|
||||
"database_swing_mae": 1.9075528008117222
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
<!-- @@@auto:current-status -->
|
||||
- **Active File**: `journal-1.md`
|
||||
- **Total Sessions**: 16
|
||||
- **Last Active**: 2026-09-21
|
||||
- **Total Sessions**: 17
|
||||
- **Last Active**: 2026-09-25
|
||||
<!-- @@@/auto:current-status -->
|
||||
|
||||
---
|
||||
@@ -19,7 +19,7 @@
|
||||
<!-- @@@auto:active-documents -->
|
||||
| File | Lines | Status |
|
||||
|------|-------|--------|
|
||||
| `journal-1.md` | ~387 | Active |
|
||||
| `journal-1.md` | ~420 | Active |
|
||||
<!-- @@@/auto:active-documents -->
|
||||
|
||||
---
|
||||
@@ -29,6 +29,7 @@
|
||||
<!-- @@@auto:session-history -->
|
||||
| # | Date | Title | Commits | Branch |
|
||||
|---|------|-------|---------|--------|
|
||||
| 17 | 2026-09-25 | 部署运行器调整与全部存量任务收尾 | `f020362` | `develop` |
|
||||
| 16 | 2026-09-21 | 雷达加权评分与排名变化上线准备 | `b9981aa` | `develop` |
|
||||
| 15 | 2026-09-01 | 选股模块技术图表迭代 | `9145fe4`, `feb8fd4` | `-` |
|
||||
| 14 | 2026-09-01 | 完成板块资金雷达双榜交互对齐 | `744cb76` | `develop` |
|
||||
|
||||
@@ -385,3 +385,36 @@
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
|
||||
## Session 17: 部署运行器调整与全部存量任务收尾
|
||||
|
||||
**Date**: 2026-09-25
|
||||
**Task**: 部署运行器调整与全部存量任务收尾
|
||||
**Branch**: `develop`
|
||||
|
||||
### Summary
|
||||
|
||||
生产部署运行器改为 tencent-prod,本地 origin 更新至 my-home/zhixing-system;按用户明确要求将全部 5 个未归档任务标记 completed 并归档,清理空 Redis 任务目录及残留会话引用。
|
||||
|
||||
### Main Changes
|
||||
|
||||
- 归档:09-01-optimize-stock-list-radar、09-04-add-gold-brick-strategy、09-05-selection-layout-sector-filter、09-06-capital-radar-daily-detail、09-07-api-performance-diagnosis。
|
||||
- 本次为用户要求的统一完结;保留原始 PRD、验收勾选、验证记录和历史限制,不将归档等同于全部功能重新验收通过。
|
||||
- 两份 .zcode/plans/ 文件的既有删除未修改、未纳入提交;未推送远端或执行部署。
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `f020362` | (see git log) |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] 部署配置:YAML 解析、main 触发与 tencent-prod 标签断言、生产 Compose 默认及 jobs 配置校验、git diff --check 均通过。
|
||||
- [OK] 归档核验:5 个任务元数据仅修改 status/completedAt;其余任务文件哈希与归档前一致;全部活动任务清空,残留当前任务引用已清除。
|
||||
- [OK] 本轮未重跑产品测试或验证历史任务的线上验收;已有验证限制保留在归档文档中。
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
## 调研结论(可行性:具备)
|
||||
|
||||
**现状**
|
||||
- 详情面板 `zhixing-web/src/features/selection/components/signal-detail-panel.tsx` 只展示名称/代码/价格/指标,无行业板块;`SelectionStockResult` 类型也无相关字段。
|
||||
- 后端 `market_stock` 表没同步 tushare `stock_basic.industry`;但项目已有 **sector_radar 模块**,通过 tushare `dc_index`/`dc_member` 每日同步"行业板块 + 概念板块"的成分股快照到 PostgreSQL 表 `sector_radar_membership`(trade_date + sector_type=industry/concept + sector_code/name + stock_code),只是目前只暴露了板块级 rankings 端点,**没有"按股票反查所属板块"的接口**——这就是缺口的全部。
|
||||
- 已确认决策:数据口径复用 sector_radar(DC 行业+概念);仅详情面板展示,列表行不动。
|
||||
|
||||
## 实施方案
|
||||
|
||||
### 第 0 步:数据验证(前置)
|
||||
启动 docker compose 的 PostgreSQL,检查 `sector_radar_membership` 按交易日的覆盖情况。若选股目标日期无快照,运行现有 CLI `sector-radar-build` 回补。若 dc_member 接口因 tushare 积分不足失败,回退方案是给 `market_stock` 加 `stock_basic.industry` 列(Alembic 迁移),届时向你说明。
|
||||
|
||||
### 后端(sector_radar 模块内新增"按股票反查",不跨模块读表)
|
||||
1. `modules/sector_radar/infrastructure/postgres.py`:新增读查询——按 `stock_code` 查 `membership_status='available'` 且 `trade_date <= 目标日` 的最近可用快照(带出实际 membership 的 trade_date,point-in-time 语义,不用最新日期冒充历史)。
|
||||
2. `modules/sector_radar/application/read.py`:新增用例,输入 `ts_code + trade_date`,返回按类型分组的板块列表(industry 全部、concept 排序后默认截断 20 个)。
|
||||
3. `modules/sector_radar/presentation/http.py`:新增端点
|
||||
`GET /api/v1/sector-radar/stocks/{ts_code}/membership?trade_date=YYYY-MM-DD&limit=20`
|
||||
响应:`{ ts_code, trade_date, membership_trade_date, industries: [{code,name}], concepts: [{code,name}], concept_total }`,遵循 http-api-contracts spec(Pydantic 边界模型)。
|
||||
|
||||
### 前端(selection feature)
|
||||
4. 新增 API 调用 + TanStack Query hook(`enabled: !!stock`,按 `stock.target_trade_date` 查询,随详情面板选中切换)。
|
||||
5. `signal-detail-panel.tsx`:股票名/代码/价格下方新增两行展示——
|
||||
- **行业**:行业标签(通常 1 个)
|
||||
- **所属板块**:概念板块 chips,最多显示 8 个 + "等 N 个"
|
||||
加载中显示占位文案,无数据时静默隐藏(不打扰无板块数据的股票)。
|
||||
|
||||
### 测试与验证
|
||||
- 后端:读用例单测(命中/无快照/截断)+ http 端点测试,参照 sector_radar 现有测试。
|
||||
- 前端:vitest 组件测试 + typecheck/lint。
|
||||
- 端到端:起 server + web,在 `/selection` 选一只股票确认行业与板块正确显示、无数据股票正常降级。
|
||||
|
||||
### 涉及文件
|
||||
- `zhixing-server/.../modules/sector_radar/{application/read.py, infrastructure/postgres.py, presentation/http.py}`
|
||||
- `zhixing-web/src/features/selection/api/*`(新 query)
|
||||
- `zhixing-web/src/features/selection/components/signal-detail-panel.tsx`
|
||||
@@ -1,42 +0,0 @@
|
||||
# 选股页面迭代:上搜索 + 左列表/右详情布局 & 板块筛选
|
||||
|
||||
## 现状与关键结论
|
||||
|
||||
- 布局:`selection-results-workbench.tsx` 目前是左右两栏 grid(左 320px 筛选+列表 / 右详情),筛选栏嵌在左栏顶部。
|
||||
- 板块数据:选股结果表(`selection_run_item`)没有板块字段;板块归属在 `sector_radar_membership` 表,现有 membership 接口只支持单股查询。**板块聚合必须由后端新增**(前端逐股请求既慢又只覆盖已加载分页,计数不准)。
|
||||
- 已确认口径(用户未答,按推荐执行):概念板块(`sector_type='concept'`,与详情面板"板块"标签一致),单选;接口保留 `sector_type` 参数,后续扩展行业零成本。排序 = 按 `stock_count` 倒序(选项旁展示数量)。
|
||||
|
||||
## 一、后端(zhixing-server)
|
||||
|
||||
遵循 bounded-context-first(ADR 0001):selection 不直接 join sector_radar 表,通过应用层端口调用 sector_radar 的读服务。
|
||||
|
||||
1. **sector_radar 模块**(`application/read.py` + `infrastructure/postgres.py`):
|
||||
- 仓储新增两个只读方法(复用已有的 `get_last_good_publication` 解析快照日):
|
||||
- `load_sector_counts(stock_codes, snapshot_date, sector_type)`:`SELECT sector_code, sector_name, COUNT(*) FROM sector_radar_membership WHERE stock_code = ANY(%s) AND trade_date = %s AND sector_type = %s AND membership_status='available' GROUP BY 1,2`
|
||||
- `load_sector_member_codes(snapshot_date, sector_code, sector_type)`:返回该板块成员股票代码列表
|
||||
- 读服务 `ReadSectorRadar` 新增:`sector_counts(stock_codes, trade_date, sector_type)`(内部解析 last-good publication,返回按 stock_count 降序、名称升序)和 `sector_member_codes(trade_date, sector_code, sector_type)`。
|
||||
2. **selection 模块**:
|
||||
- `domain/runs.py`:`SelectionResultQuery` 增加 `sector: str | None`;新增 `SelectionSectorCount(sector_code, sector_name, stock_count)` 值对象;定义端口协议 `SectorMembershipReader`(`sector_counts` / `sector_member_codes` 两个方法)。
|
||||
- `application/run.py`:新增 `list_sector_counts(strategy, target_trade_date, sector_type)` —— 取 latest run,收集 `status='selected' AND signal_count>0` 的 ts_code,调端口聚合;`get_latest`/`get` 结果查询在 `query.sector` 有值时先调端口取成员代码,空则直接返回空页,否则把代码数组传入仓储。
|
||||
- `infrastructure/postgres_runs.py`:`_stock_filter` 增加子句 `item.ts_code = ANY(%s)`(参数由应用层传入)。
|
||||
- `presentation/http.py`:
|
||||
- 新端点 `GET /api/v1/selection/sectors?strategy=&target_trade_date=§or_type=` → `{ sector_type, snapshot_trade_date, sectors: [{sector_code, sector_name, stock_count}] }`
|
||||
- `GET /results` 与 `GET /runs/{id}` 增加 `sector` query 参数校验(去空格、限长)。
|
||||
- 组合根(router/依赖装配处)把 sector_radar 的读服务适配为 selection 的端口注入。
|
||||
3. **测试**:`tests/unit/sector_radar/`(新仓储方法)、`tests/unit/selection/test_postgres_runs.py`(sector 过滤、ANY 数组、空成员空页)、`tests/test_selection_http.py`(新端点契约 + results 带 sector)。跑 `./dev.sh check`(ruff/pyright/pytest)。
|
||||
|
||||
## 二、前端(zhixing-web)
|
||||
|
||||
1. **布局重构** `selection-results-workbench.tsx`:
|
||||
- 外层改为 `flex flex-col`:顶部一个全宽 section 放搜索栏(关键词 / 信号分类 / **板块(新增)** / 排序 / 筛选结果计数),带 `rounded-md border bg-card` 与现有一致;
|
||||
- 下方 `md:grid md:grid-cols-[320px_minmax(0,1fr)]` 左列表右详情;移动端纵向堆叠为 搜索 → 列表 → 详情。
|
||||
2. **类型与 API**(`selection.types.ts` / `selection.api.ts`):新增 `SelectionSectorAggregate`;`SelectionResultsQuery` 加 `sector?`;`getSelectionResultSectors()`;`buildSelectionQueryParams` 带 sector。
|
||||
3. **查询层**(`selection.query.ts`):`useSelectionResultSectors`(key:strategy+date,结果就绪后启用);`selectionResultsQueryKey`/`selectionRunQueryKey` 加入 sector 使筛选变化触发重新请求。
|
||||
4. **路由**(`routes/route-tree.tsx`):selectionRoute `validateSearch` 增加 `sector`(string,限长,默认 undefined)。
|
||||
5. **页面接线**(`selection-results-page.tsx`):把 URL 中的 sector 传入 `useSelectionResults`;页面层调用 `useSelectionResultSectors` 并把聚合结果传给 workbench。
|
||||
6. **workbench 板块下拉**:选项 = "全部板块" + 聚合数据,item 渲染 `名称 + 数量徽标`(数量 tabular-nums,倒序由后端保证);选择写 URL;聚合加载后若当前 sector 不在列表中(如切换策略/日期)自动重置为全部;板块无数据时下拉仅剩"全部板块"并禁用。
|
||||
|
||||
## 三、执行方式
|
||||
|
||||
- 按仓库 Trellis 工作流建任务目录并加载 `.trellis/spec/backend`(selection.md 契约:查询不触发重算、错误矩阵等)与前端规范后再动手;先后端(接口+测试)再前端接线,最后 `./dev.sh check` + 前端 lint/tsc 全量质量门禁。
|
||||
- 语义说明:板块数量 = 当次 run 全部选中股票中归属该板块的数量(不随关键词/信号分类变化);列表"筛选结果 N 只" = 包含板块在内的全部过滤叠加后的 `stocks_total`。
|
||||
@@ -61,3 +61,8 @@ select = ["B", "E", "F", "I", "SIM", "UP"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "tuna"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
default = true
|
||||
|
||||
Generated
+378
-378
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user