diff --git a/.gitea/workflows/deploy-production.yaml b/.gitea/workflows/deploy-production.yaml index 938b5b8..4e8cad5 100644 --- a/.gitea/workflows/deploy-production.yaml +++ b/.gitea/workflows/deploy-production.yaml @@ -8,7 +8,7 @@ on: jobs: deploy: - runs-on: ubuntu-latest + runs-on: tencent-prod timeout-minutes: 30 env: diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/check.jsonl b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/check.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. 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."} diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/design.md b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/design.md new file mode 100644 index 0000000..ab3cc71 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/design.md @@ -0,0 +1,9 @@ +# 研究设计 + +这是只读分析任务,不进入产品实现阶段。 + +公开证据链:页面展示 → 实际引用脚本 → 实际请求的公开数据 → 评分与排名字段。数据库证据链:库表目录 → 字段及单位 → 重叠交易日原始值 → 候选公式复算 → 与公开评分比较。 + +优先检验可解释的低自由度公式。分开检验资金比率、横截面排序/归一化、时间窗口聚合;用多日和不同板块类型验证,避免单点拟合。识别数据修订、单位换算、成分股聚合与板块原始数据的口径差异。 + +数据库连接强制 default_transaction_read_only,设置查询超时。仅在本机保留任务所需数据;公开资料可以缓存供复核,凭据不落盘。报告不将无法唯一识别的参数写成确定结论。 diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/implement.jsonl b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/implement.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. 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."} diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/implement.md b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/implement.md new file mode 100644 index 0000000..14cf3a2 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/implement.md @@ -0,0 +1,9 @@ +# 分析步骤(无产品实现) + +- [x] 读取网站公开脚本和数据,记录字段、日期和来源。 +- [x] 只读确认数据库版本、数据表及覆盖范围。 +- [x] 建立日期、板块和单位映射,对比原始输入。 +- [x] 逐层验证单日评分和波段评分候选公式,记录误差。 +- [x] 核验关键结论,完成研究记录与用户答复。 + +验证使用实际数据计算与证据核对;不运行与分析无关的产品测试。任务不包含代码实施、共享知识推广、提交和发布。 diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/prd.md b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/prd.md new file mode 100644 index 0000000..d7a4d82 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/prd.md @@ -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`。本研究已完成,没有产品实施待批准。 diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database-validation.json b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database-validation.json new file mode 100644 index 0000000..63a0dc2 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database-validation.json @@ -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 + } + ] + } +} diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database-validation.md b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database-validation.md new file mode 100644 index 0000000..35daa42 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database-validation.md @@ -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 任务;未修改产品实现、共享规格或生产数据。 diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database_reproduce.py b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database_reproduce.py new file mode 100644 index 0000000..d5c2fff --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/database_reproduce.py @@ -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)) diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-normalized-inputs.json.gz b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-normalized-inputs.json.gz new file mode 100644 index 0000000..c2f7ebd Binary files /dev/null and b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-normalized-inputs.json.gz differ diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-raw-inputs.json.gz b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-raw-inputs.json.gz new file mode 100644 index 0000000..d29be18 Binary files /dev/null and b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-raw-inputs.json.gz differ diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-source-metadata.json b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-source-metadata.json new file mode 100644 index 0000000..6fc472f --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/db-source-metadata.json @@ -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 + } + ] +} diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/findings.md b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/findings.md new file mode 100644 index 0000000..0e8081d --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/findings.md @@ -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 和结果均与生产代码隔离。 diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/member-differences.json b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/member-differences.json new file mode 100644 index 0000000..31e80a8 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/member-differences.json @@ -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" + } +] diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/public-inputs.json.gz b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/public-inputs.json.gz new file mode 100644 index 0000000..28e3be5 Binary files /dev/null and b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/public-inputs.json.gz differ diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/public-validation.json b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/public-validation.json new file mode 100644 index 0000000..098214e --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/public-validation.json @@ -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 + } + } + } +} diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/radar_manifest.json b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/radar_manifest.json new file mode 100644 index 0000000..647af67 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/radar_manifest.json @@ -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"} \ No newline at end of file diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/raw-reaggregation.sql b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/raw-reaggregation.sql new file mode 100644 index 0000000..7e7805f --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/raw-reaggregation.sql @@ -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 diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/reproduce.py b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/reproduce.py new file mode 100644 index 0000000..4b99975 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/reproduce.py @@ -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)) diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/worked-examples.json b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/worked-examples.json new file mode 100644 index 0000000..3144131 --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/research/worked-examples.json @@ -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 + } +] diff --git a/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/task.json b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/task.json new file mode 100644 index 0000000..56fdd4f --- /dev/null +++ b/.trellis/tasks/archive/2026-09/09-21-onechart-score-reconstruction/task.json @@ -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 + } +} \ No newline at end of file