chore(task): archive 09-06-capital-radar-daily-detail
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/spec/backend/market-data-sync.md", "reason": "Tushare 落库及同步契约"}
|
||||
{"file": ".trellis/spec/backend/tushare-listed-stock-universe.md", "reason": "当前上市母集约束"}
|
||||
{"file": ".trellis/spec/backend/http-api-contracts.md", "reason": "详情与排名API"}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "弹窗与榜单组件"}
|
||||
{"file": ".trellis/spec/frontend/hook-guidelines.md", "reason": "按日期的查询状态"}
|
||||
{"file": ".trellis/tasks/09-06-capital-radar-daily-detail/design.md", "reason": "本次数据及交互设计"}
|
||||
@@ -0,0 +1,21 @@
|
||||
# 资金雷达单日详情设计
|
||||
|
||||
## 数据边界
|
||||
继续使用 sector_radar bounded context。Tushare 原始响应先进入现有 snapshot 持久化链路,再扩展事实与查询投影;浏览器只访问同源 API,不直接请求 Tushare 或参考站点。扩展板块日事实保存 pct_change 与领涨标识,股票事实保存 daily.pct_chg 和独立 active_buy_net_amount_yuan。新字段可空,旧发布保持可读。moneyflow 作为详情可选来源,不将其失败当成主力净额为零;与原始 snapshot、发布日期和输入版本关联。
|
||||
|
||||
## 查询契约
|
||||
rankings 在原字段上增加 pct_change、daily_net_amount_yuan、daily_ratio 和对应侧的 30 日在榜次数;领域 percentile/coverage 保留,仅移除两种视角的展示列。避免逐板块查询历史,按日期和类型批量加载历史排名,按每日池的 TOP/BOTTOM 阈值计数。
|
||||
|
||||
新增板块 history 与 detail 读取接口,键包含 sector_type、sector_code、trade_date;返回解析后的 publication 标识和实际日期。history 含日期、三指标排名、当日池规模/百分位和缺失状态,严格截至目标日,最多 30 个交易日;同日选最新成功发布,历史版本不兼容时不混用。详情包含当日指标摘要、成员三种强弱数据、成员列表和相似板块。共享历史读取实现,避免重复计算。
|
||||
|
||||
## 指标
|
||||
沿用现有主力净额/成交额流入率及波段策略。板块涨跌幅优先使用 dc_index 源字段;成员 daily.pct_chg 独立于资金流是否缺失。主买净额来自 moneyflow.net_mf_amount,须核对官方单位后转为元,与主力 net_amount 分开。重合度为交集数量/并集数量,使用同日可确认的当前上市母集成员;未知成员不参与,空并集不计算。同分稳定按代码排列。
|
||||
|
||||
## 前端
|
||||
将两种单日榜列配置与其他视角隔离;拆分历史格子和详情弹窗组件,沿用项目 UI primitive 和 ECharts。详情请求按需触发,query key 包含类型/代码/目标日,切换日期时不显示前一天的详情。格子按日期从近到远,曲线按时间从早到晚且排名 1 在上方,缺失处断线。导出及复制仅使用 API 返回成员,生成本地 CSV/文本,CSV 对不可信字段进行转义与公式注入防护。
|
||||
|
||||
## 兼容与回滚
|
||||
采用增量 nullable 数据迁移,不删除原字段。旧发布缺详情时显式显示暂无数据;可通过重新构建目标日补齐,不在读取路径触发网络取数。新增来源失败保留最近有效发布并体现详情缺失。真实自测使用隔离的本地数据库或受控测试批次,不覆盖完整发布。若迁移/验证失败,停止新构建并保留原始快照,回退代码前确认旧代码可读取新增 nullable 列。
|
||||
|
||||
## 实施前核验
|
||||
确认 moneyflow 官方单位、当前采集权限和现有 source 失败策略;确定具体 migration 编号与可复用 UI primitive。原站私有算法不构成本次完成条件。真实全池历史不足仅报告覆盖,不人为补齐。
|
||||
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/spec/backend/market-data-sync.md", "reason": "Tushare 落库及同步契约"}
|
||||
{"file": ".trellis/spec/backend/tushare-listed-stock-universe.md", "reason": "当前上市母集约束"}
|
||||
{"file": ".trellis/spec/backend/http-api-contracts.md", "reason": "详情与排名API"}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "弹窗与榜单组件"}
|
||||
{"file": ".trellis/spec/frontend/hook-guidelines.md", "reason": "按日期的查询状态"}
|
||||
{"file": ".trellis/tasks/09-06-capital-radar-daily-detail/design.md", "reason": "本次数据及交互设计"}
|
||||
@@ -0,0 +1,17 @@
|
||||
# 实施与验证计划
|
||||
|
||||
- [x] 用户审核当前规划后,加载 Phase 1.3/1.4,校验上下文清单并进入 in_progress。
|
||||
- [x] 读取受影响层规范和确切修改代码,确认 Tushare moneyflow 字段单位、权限及本地隔离数据库方案。
|
||||
- [x] 扩展 source、事实和 PostgreSQL migration,保存板块涨跌幅与成分行情/主买净额,保持旧发布可读;更新 memory repository。
|
||||
- [x] 增加批量历史/详情读取和 HTTP 响应,补充单日榜附加字段、在榜次数、三指标历史、前后5名、成员及重合度。
|
||||
- [x] 调整前端 API/types/query,完成两种单日列、名称入口、在榜展开、详情弹窗与复制/导出。
|
||||
- [x] 后端测试重点:单位转换、来源缺失不归零、同日发布去重、30交易日边界、无未来数据、每天排名池变化、相似度与历史成员;运行 `cd zhixing-server && uv run pytest tests/unit/sector_radar tests/test_sector_radar_http.py`,有数据库时补集成测试 `tests/integration/test_sector_radar_repository.py`。
|
||||
- [x] 前端测试重点:视角切换列名、左右板块指标、展开收起、日期切换、详情指标切换、少量样本、关闭/焦点和导出内容;运行 `cd zhixing-web && pnpm test src/features/sector-radar`。
|
||||
- [x] 使用一个真实板块完成 Tushare 采集、落库、API核对,记录请求日期、条数、缺失及核对值,不记录密钥;不得将单板块测试发布为全市场榜单。
|
||||
- [x] 后端运行 `uv run ruff format --check .`、`uv run ruff check .`、`uv run pyright`;前端运行 `pnpm format:check`、`pnpm lint`、`pnpm typecheck`、`pnpm build`。按变更范围扩大回归,不重复无关检查。
|
||||
- [x] 浏览器验证本地两个面板与弹窗、滚动及窄屏。核对 API 无请求时取 Tushare 的行为。
|
||||
- [x] 主会话完成最终验证并汇报实际证据;不自动提交用户改动,不更新全局知识或 specs。
|
||||
|
||||
风险位置:sector_radar/application/build.py 的成功发布门、infrastructure/postgres.py 的快照/事实事务与新迁移、application/read.py 的日期回退。新增可选详情来源不应改变旧榜成功条件。
|
||||
|
||||
执行结果与全局既有检查例外详见 verification.md;勾选代表本步骤已执行,不表示全仓检查全部通过。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 资金雷达:单日榜单与板块详情
|
||||
|
||||
## 目标与边界
|
||||
尽可能复刻 OneChartLab 单日榜单和截图中的板块详情交互,支持解释当日资金强弱与历史持续性。所有业务数据通过 Tushare 采集落库,再由本地分析和 API 提供;参考站点仅用于交互研究。保留现有独立指标,不反演或新增原站未公开的加权评分,不改动波段榜及排名变化榜的算法,不提交已有用户改动。
|
||||
|
||||
## 已确认背景
|
||||
当前排名接口没有涨跌幅及附加指标,见 `zhixing-server/src/zhixing_server/modules/sector_radar/presentation/http.py:83`。归一化事实只存成交额和主力净额,见同模块 `domain/persistence.py:101`。但采集已请求 `dc_index.pct_change/leading_code` 和 `daily.pct_chg`,见同模块 `infrastructure/tushare.py:40`。现有 3—10 日聚合及三指标排名可复用,30 日详情接口和前端交互尚未实现。
|
||||
|
||||
## 需求与验收
|
||||
R1:单日流入率移除排名百分位和样本列,增加涨跌幅、净额、在榜;板块仅显示名称。按用户截图将“净值”解释为主力净额,金额以亿元显示。左右榜字段镜像排列,保留现有独立流入率排序。验收检查列名、数据、正负颜色和名称点击。
|
||||
|
||||
R2:单日净额移除排名百分位和样本列,增加涨跌幅、单日流入率;同样只展示板块名称。验收核对附加指标属于同一板块、同一交易日和同一发布版本。
|
||||
|
||||
R3:在榜为截至所选日近 30 个交易日内进入对应 TOP/BOTTOM 10% 的次数,不是连续天数。点击展开日期和当日排名格子,支持收起;分别用每日同类型排名池确定强弱。缺失记录显示缺失,不补零,不借用未来数据;不足 30 日标明覆盖范围。
|
||||
|
||||
R4:点击板块打开可滚动详情弹窗,包含名称、类型、代码、日期、领涨股、当日涨跌幅、波段/单日率/单日额排名摘要;三指标近 30 交易日轨迹可切换,悬停显示日期和排名;成分强弱支持涨跌幅、主力净额、主买净额和前后 5 名;提供相似板块、复制及导出成员。少于 5 个有效成员按实际数量展示;缺失字段不伪装为 0。弹窗支持关闭按钮、Escape、焦点返回和加载/失败/空状态。
|
||||
|
||||
R5:优先使用落库的 dc_index 板块涨跌幅,不以成分平均值静默替代。成分涨跌幅来自 daily,主力与主买净额分别保存。相似度使用同日成员集合 Jaccard 重合度,明确为本系统口径;最多显示四个非自身板块,允许概念与行业交叉比较。
|
||||
|
||||
R6:真实 Tushare 单板块自测先采集落库,再通过 API 读取并核对金额、涨跌幅和成员。单板块数据不能验证全市场排名或相似度,不得发布为完整排名池;全池排名/历史边界使用受控数据验证。缺权限或历史数据不足时记录实际限制,不伪造通过。
|
||||
|
||||
## 验证与风险
|
||||
后端验证单位、成员日期、历史截止日、独立发布版本、缺失和在榜计数;前端验证两面板、展开、弹窗、切换和导出交互。真实调用前核实环境与权限,密钥不进入日志。新增详情来源失败不得让已有完整榜单丢失;旧发布缺字段可读且明确为空。当前仍处规划阶段,尚未进行产品修改或真实采集。
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
"""Collect one real sector into an isolated PostgreSQL snapshot store.
|
||||
|
||||
Run with the server's uv environment from zhixing-server. No production
|
||||
publication is created; facts must be read back from this store for validation.
|
||||
"""
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
import json
|
||||
import time
|
||||
import tushare as ts
|
||||
from zhixing_server.bootstrap.config import Settings
|
||||
from zhixing_server.modules.sector_radar.domain.source import build_source_snapshot
|
||||
from zhixing_server.modules.sector_radar.infrastructure.postgres import PostgresSectorRadarRepository
|
||||
|
||||
|
||||
def main():
|
||||
"""Persist provider responses before inspecting their values; redact errors."""
|
||||
settings = Settings(_env_file='../.env')
|
||||
url = urlsplit(settings.database_url)
|
||||
host = url.netloc.rsplit('@', 1)[0] + '@127.0.0.1:5433'
|
||||
database = urlunsplit((url.scheme, host, '/radar_detail_selftest_0906', url.query, ''))
|
||||
repository = PostgresSectorRadarRepository(database, max_connections=2)
|
||||
client = ts.pro_api(settings.tushare_token)
|
||||
target = date(2026, 9, 4)
|
||||
counts = []
|
||||
|
||||
def collect(api, fields, **params):
|
||||
"""Store each raw response atomically and return persisted rows."""
|
||||
time.sleep(0.25)
|
||||
frame = client.query(api, fields=fields, **params)
|
||||
snapshot = build_source_snapshot(api_name=api, params=params,
|
||||
rows=frame.to_dict('records'), target_trade_date=target)
|
||||
repository.save_source_snapshots((snapshot,))
|
||||
counts.append({'api':api, 'rows':snapshot.row_count, 'snapshot':snapshot.snapshot_id})
|
||||
return snapshot.rows
|
||||
|
||||
try:
|
||||
collect('dc_index','ts_code,trade_date,name,idx_type,level,pct_change,leading_code',
|
||||
trade_date='20260904', ts_code='BK1147.DC')
|
||||
members = collect('dc_member','trade_date,ts_code,con_code,name',
|
||||
trade_date='20260904', ts_code='BK1147.DC')
|
||||
collect('stock_basic','ts_code,symbol,name,market,exchange,list_status,list_date,delist_date',list_status='L')
|
||||
collect('suspend_d','ts_code,trade_date,suspend_timing,suspend_type',trade_date='20260904')
|
||||
collect('trade_cal','exchange,cal_date,is_open,pretrade_date',exchange='SSE',start_date='20260720',end_date='20260904')
|
||||
for member in members:
|
||||
code = member['con_code']
|
||||
collect('daily','ts_code,trade_date,close,pre_close,pct_chg,vol,amount',trade_date='20260904',ts_code=code)
|
||||
collect('moneyflow_dc','trade_date,ts_code,name,net_amount,net_amount_rate,pct_change,close',trade_date='20260904',ts_code=code)
|
||||
collect('moneyflow','trade_date,ts_code,net_mf_amount',trade_date='20260904',ts_code=code)
|
||||
result={'status':'collected','sector':'BK1147.DC','trade_date':str(target),'members':len(members),'snapshots':counts}
|
||||
except Exception as exc:
|
||||
result={'status':'partial','snapshots':counts,'error_type':type(exc).__name__,
|
||||
'message':str(exc).replace(settings.tushare_token,'[redacted]')[:300]}
|
||||
finally:
|
||||
repository.close()
|
||||
Path('../.trellis/tasks/09-06-capital-radar-daily-detail/research/collection-result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2))
|
||||
print(json.dumps({k:v for k,v in result.items() if k!='snapshots'},ensure_ascii=False))
|
||||
print('Stored snapshots:',len(counts))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"status": "collected",
|
||||
"sector": "BK1147.DC",
|
||||
"trade_date": "2026-09-04",
|
||||
"members": 14,
|
||||
"snapshots": [
|
||||
{
|
||||
"api": "dc_index",
|
||||
"rows": 1,
|
||||
"snapshot": "73f94be4b5c94238b611396b7cdf2e314f17cdd23532c3773febf3e3277f1674"
|
||||
},
|
||||
{
|
||||
"api": "dc_member",
|
||||
"rows": 14,
|
||||
"snapshot": "2046df68966d3eda9a392b0254b84576842c42c487ea0a2f1044b6cc75de3afa"
|
||||
},
|
||||
{
|
||||
"api": "stock_basic",
|
||||
"rows": 5556,
|
||||
"snapshot": "1c87ce65e081e670add1c1167473f798e97e8c9cf41db3e4ac71df0e3de5bb90"
|
||||
},
|
||||
{
|
||||
"api": "suspend_d",
|
||||
"rows": 8,
|
||||
"snapshot": "b91d0981345538463407939a7ed863b8fb8de5e447b8ec731b8767b7ae13aa5d"
|
||||
},
|
||||
{
|
||||
"api": "trade_cal",
|
||||
"rows": 47,
|
||||
"snapshot": "afff3cba955380cc0af9cdaac1d8861c17a70b089c70ea95aadb1cc568f8a26e"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "4f2c4f92685180b3de23a26c9d0602a8acf3fedb7dbc2413ac92bda76d8fa65a"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "7c1907d19c2db8134a807edba87fe165b44a1e96d6b3731e65a26f52715f6f12"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "521254cf598c28608571973eeec1e52c56c856ee547bbd2d9ceacc4e1a29727e"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "9bf1ca69c3be480113c933c25681dab1379e0a068bdf9a31452794de8c4bff94"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "8b81006f266edabf8ac1e2aa292e9b278ca9131eea47f86fbef89373197215f6"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "e90138a791bf7e3d458d3865838ac3aee394fe491ebf93cb363425852d1bca92"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "81ca417096a073698375daa03f0a660398d53c3943b16c9598a8faa918ff177f"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "40fb4ad0d2bbe8413e6dfcc213e8efe7f3367f84f492f68e430f1a9d08e9cc51"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "47d32d618d5d8a2118253b1e3d7b354628bae22a7b10880f4cfdd3ee370dae22"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "2418e65c6800ef5633e6f260cbe617eb3b5bbda4d71e16e56589cd1603827b6b"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "d3559b1413d931dfe882fa64f501b75c76c91125fa0fbe616b385e9a3b7c9e15"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "76a6e0cbe16e92c707af828a9936d87bd810844c357256c18e4d9ef10d86e40d"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "a3307443f9e89ff5523043055509dc309c6766067a5d9eb83bdef5d19e6e44c1"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "8d4c401bf8ade3dae6c0eab35069d59e61d4c6383b190e21a420d7735d0ab5a2"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "ee3a90e0e406940f60358edfa3688f8774687bc768c4e9c069ca6d8337a39bcc"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "bf21400564f20c771905d46be0dd225eb9d0579393ab5734038ba93db63042f4"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "66611bdd15e87cd5ccec0f48d7e94282cedb210f0393a313519b6896abf0de1f"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "eb98e9d138478b7ec2ae57557a1e61d34f825fd9faf8f30ef530c4d54f5e391e"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "550627fe30dccb760e4dbc73dd0ecee85676f781dac1ba059483e7d83b701091"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "5d1e6b1222b37896ad79d759e8fb8a240fb438bf6d50eda687c56181648c3a12"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "7bc22506f40cd9c550a48699c93bee28f00c8717e304cd26761b33d9e66dbf35"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "39dda855afcfcf4e32ccd67e4cd28942f8eed13a171b82fa438000bfcff87252"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "b9042adf9d7c2823a7155691c6c349f451b6d84fe7eb2ca83ab749a51eaa6c6e"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "aed26a777f4d4eae9236b677d6ed7180a093aa49076eeee3b796325db3ce2e9f"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "26d7d7beb26a8318f9b1104b5bc7446562663bba3e7142d9957562f5dce16c64"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "560d0e8e0a1d1220acab7a4d22ba6aadb4d47555344f75bbdbaf7650d62d95bf"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "7f6759aea68eb193c271c6f8230de9237bcd25bb445eef786adc5b39d5527e08"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "467d8fa44adf1aeecb81bb970a3239db4f48c75ec680bd802310ba0a40b9a92f"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "cc80509ebf2c47e85cb1eebe0735db7c84facd01eba73212db218fbabcf6c980"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "7f145408f62626e955b83617821e4c41ce0af3353533de790de22211f02ba36c"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "e515db0257399b1433fc14c4062a020a4837dfe6ff968cd70ab2b60f1035794e"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "31aa693eb092cf03a7b1e0da65ffd46e79389831f93809e6738789bfa6e8d619"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "b4073fdbd1e5f73c52d64aae07ea3dd8a9613f12cdea316abcbbaf4bdff6f28e"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "ffd0d7d09fee0dc7e5a035c9131220042563a8d5c852ff5ca8d0a4d8b7ba8004"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "7e9bff54c0b00afc12bbcc7a98dfcd7e504ce780e47ac7a3c3443e989d9da031"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "fd045165e72b1127df96bd73487a99d2b561c641783a0dfa3b0ed8b470e4abda"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "30ccd5069b7a8c361cc5408ecb9e0013f36d314cf5dde4c6d79b9e10f8942315"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "bbcc7b25b1e2d51487dcb76b28404966db7e83c9153af8f3119e1653f20f8618"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "c210e7f840c5baecc94d6fd333f73fd98b994b65575f2ce38faed428e59b8f9f"
|
||||
},
|
||||
{
|
||||
"api": "daily",
|
||||
"rows": 1,
|
||||
"snapshot": "bbb69dfcf4862e214b3a47265ba3607476695cb9147eed3d9f8199a0e7501ff1"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow_dc",
|
||||
"rows": 1,
|
||||
"snapshot": "fce61a39b30de56dc3b898f6d8e4bf156504bedc001c5c85abfceaecbb490f36"
|
||||
},
|
||||
{
|
||||
"api": "moneyflow",
|
||||
"rows": 1,
|
||||
"snapshot": "2e0739949d25df4a56631c761e064121ff8f99ead4e95306c1c1f1bfa44111ae"
|
||||
}
|
||||
]
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"trade_date": "2026-09-04",
|
||||
"sector": "BK1147.DC",
|
||||
"members": 14,
|
||||
"pct_change": 1.82,
|
||||
"main_net_yuan": "485456500.00",
|
||||
"turnover_yuan": "2615472881.48000",
|
||||
"daily_ratio": "0.1856094564915916866216729052",
|
||||
"active_net_yuan": "-255455700.00",
|
||||
"rows_by_api": {
|
||||
"dc_index": 1,
|
||||
"dc_member": 14,
|
||||
"stock_basic": 5556,
|
||||
"suspend_d": 8,
|
||||
"trade_cal": 47,
|
||||
"daily": 14,
|
||||
"moneyflow_dc": 14,
|
||||
"moneyflow": 14
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"""Replay the persisted real SPD sample in an isolated database, with no network.
|
||||
|
||||
The one-sector publication is exclusively a local integration fixture; its ranks
|
||||
must never be interpreted as a market-wide ranking.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
import psycopg
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from zhixing_server.bootstrap.config import Settings, get_settings
|
||||
from zhixing_server.modules.sector_radar.application.build import BuildSectorRadar, BuildSectorRadarCommand
|
||||
from zhixing_server.modules.sector_radar.domain.models import SectorType
|
||||
from zhixing_server.modules.sector_radar.domain.source import (
|
||||
SourceSnapshot, SourceResult, TradeCalendarRow, SectorIndexRow, SectorMemberRow,
|
||||
StockBasicRow, SuspendRow, DailyRow, MoneyflowDcRow, MoneyflowRow, build_source_snapshot,
|
||||
)
|
||||
from zhixing_server.modules.sector_radar.infrastructure.postgres import PostgresSectorRadarRepository
|
||||
|
||||
|
||||
def database_url():
|
||||
"""Resolve only the named localhost fixture database without printing secrets."""
|
||||
s=Settings(_env_file='../.env'); u=urlsplit(s.database_url)
|
||||
return urlunsplit((u.scheme,u.netloc.rsplit('@',1)[0]+'@127.0.0.1:5433','/radar_detail_selftest_0906',u.query,''))
|
||||
|
||||
|
||||
class StoredSource:
|
||||
"""Implement provider port using already committed raw snapshots only."""
|
||||
def __init__(self, dsn):
|
||||
self.by_api={}
|
||||
with psycopg.connect(dsn) as c:
|
||||
for r in c.execute('SELECT id,api_name,normalized_params,target_trade_date,partition_key,observed_at,payload,row_count,returned_fields,content_sha256,row_limit,limit_reached FROM sector_radar_source_snapshot').fetchall():
|
||||
snap=SourceSnapshot(r[0],r[1],tuple(sorted(r[2].items())),r[3],r[4],r[5],tuple(r[6]),r[7],tuple(r[8]),r[9],r[10],r[11])
|
||||
self.by_api.setdefault(r[1],[]).append(snap)
|
||||
|
||||
def result(self, api, parser):
|
||||
snaps=tuple(self.by_api[api])
|
||||
return SourceResult(snaps,tuple(parser(row) for s in snaps for row in s.rows))
|
||||
|
||||
def fetch_trade_calendar(self,start,end):
|
||||
return self.result('trade_cal',TradeCalendarRow.from_mapping)
|
||||
def fetch_sector_indices(self,target,kind):
|
||||
if kind is SectorType.INDUSTRY:
|
||||
# Explicitly empty industry scope for this one-concept test fixture.
|
||||
snap=build_source_snapshot(api_name='dc_index',params={'test_scope':'empty_industry'},rows=(),target_trade_date=target)
|
||||
return SourceResult((snap,),())
|
||||
return self.result('dc_index',lambda row:SectorIndexRow.from_mapping(row,kind))
|
||||
def fetch_sector_members(self,target,codes):
|
||||
return self.result('dc_member',SectorMemberRow.from_mapping)
|
||||
def fetch_stock_basics(self):
|
||||
return self.result('stock_basic',StockBasicRow.from_mapping)
|
||||
def fetch_suspensions(self,target):
|
||||
return self.result('suspend_d',SuspendRow.from_mapping)
|
||||
def fetch_daily(self,target):
|
||||
return self.result('daily',DailyRow.from_mapping)
|
||||
def fetch_moneyflow_dc(self,target,codes):
|
||||
return self.result('moneyflow_dc',MoneyflowDcRow.from_mapping)
|
||||
def fetch_moneyflow(self,target):
|
||||
return self.result('moneyflow',MoneyflowRow.from_mapping)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run actual build and HTTP reads, checking independently recomputed facts."""
|
||||
dsn=database_url()
|
||||
os.environ['ZHIXING_DATABASE_URL']=dsn
|
||||
get_settings.cache_clear()
|
||||
command.upgrade(Config('alembic.ini'),'head')
|
||||
repository=PostgresSectorRadarRepository(dsn,max_connections=2)
|
||||
result=BuildSectorRadar(StoredSource(dsn),repository,today=date(2026,9,6)).execute(BuildSectorRadarCommand(trade_date=date(2026,9,4)))
|
||||
print(json.dumps(result.as_dict(),ensure_ascii=False))
|
||||
repository.close()
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
+1349
File diff suppressed because it is too large
Load Diff
+27
@@ -0,0 +1,27 @@
|
||||
"""Verify API facts against independently calculated persisted real data."""
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from urllib.request import urlopen
|
||||
|
||||
root='http://127.0.0.1:8016/api/v1/sector-radar/'
|
||||
d=json.load(urlopen(root+'sectors/concept/BK1147.DC/detail?trade_date=2026-09-04'))
|
||||
assert d['status']=='success'
|
||||
assert len(d['members'])==14
|
||||
assert Decimal(d['pct_change'])==Decimal('1.82')
|
||||
assert sum(Decimal(m['net_amount_yuan']) for m in d['members'])==Decimal('485456500')
|
||||
assert sum(Decimal(m['active_buy_net_amount_yuan']) for m in d['members'])==Decimal('-255455700')
|
||||
assert Decimal(d['summary']['amount']['metric_value'])==Decimal('4.854565')
|
||||
# Storage uses 12 fractional digits for persisted metric observations.
|
||||
assert abs(Decimal(d['summary']['ratio']['metric_value'])-Decimal('0.18560945649159168662'))<Decimal('1e-12')
|
||||
assert d['history']['available_days']==1
|
||||
assert len(d['history']['points'])==30
|
||||
assert d['summary']['swing']['missing'] is True
|
||||
assert len(d['leaders']['pct_change']['top'])==5
|
||||
for view in ['amount','ratio']:
|
||||
r=json.load(urlopen(root+'rankings?sector_type=concept&view='+view+'&side=top&trade_date=2026-09-04'))['rows'][0]
|
||||
assert Decimal(r['pct_change'])==Decimal('1.82')
|
||||
assert Decimal(r['daily_net_amount_yuan'])==Decimal('485456500')
|
||||
assert r['on_list_count']==1
|
||||
Path(__file__).with_name('verified-detail.json').write_text(json.dumps(d,ensure_ascii=False,indent=2))
|
||||
print('PASS: real persisted SPD data matches detail and both ranking APIs; 14 members, 30 slots / 1 available day, absent swing remains null.')
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "capital-radar-daily-detail",
|
||||
"name": "capital-radar-daily-detail",
|
||||
"title": "资金雷达:单日榜单与板块详情",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "yuxuanhui",
|
||||
"assignee": "yuxuanhui",
|
||||
"createdAt": "2026-09-06",
|
||||
"completedAt": "2026-09-25",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "实施及本地验证完成,详见 verification.md。代码按用户边界保持未提交,未自动归档;正常业务库尚未应用本次迁移。",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# 本地实施验证 · 2026-09-06
|
||||
|
||||
本次已实现单日流入率/净额两种表头、板块名称入口、30 交易日在榜展开、详情三指标排名曲线、成员三指标前后 5 名切换、Jaccard 相似板块和成员复制/CSV 导出。沿用当前项目主题,参考站点私有加权评分未作为此次范围。前端读取同源 API;详情和历史仅从已持久化的发布及原始快照分析,不在读请求中调用 Tushare。
|
||||
|
||||
## 真实数据核验
|
||||
|
||||
使用独立数据库 `radar_detail_selftest_0906`,未覆盖正常业务库。采集 SPD概念 `BK1147.DC` 在 2026-09-04 的 Tushare 数据,14 只成员,原始响应先入库,再从已保存快照重放构建。数据库已升级至 `0009_radar_sector_detail`。采集、独立计算和 API 核对脚本在本任务 `research/`,不包含凭据。
|
||||
|
||||
数据库独立汇总与最新 HTTP 返回一致:板块涨跌幅 1.82%,主力净额 485456500 元,成交额 2615472881.48 元,单日流入率 18.560945649159…%,主买净额 -255455700 元。两种资金指标分别保留,未互相替代。`research/verify_http.py` 在重启最新服务后通过,验证 14 个成员、三指标摘要及两种榜单附加字段。
|
||||
|
||||
该自测只有 1 个板块、1 日有效发布;近 30 个交易日中的其余日期显式缺失,波段指标保持空值。单板块第 1 名仅验证功能链路,不代表全市场排名,也不能用于检验参考站点全池历史的数值一致性。
|
||||
|
||||
## 检查结果
|
||||
|
||||
- 后端相关单元、HTTP、PostgreSQL 测试:96 passed。隔离审查库 `radar_detail_review_0906` 从空库完成迁移;覆盖最新发布、30 日边界、未来隔离、每日池变化、版本隔离、可选来源失败、历史成员、Jaccard 和字段持久化。
|
||||
- 后端全量 Ruff lint/format:通过,129 个文件格式符合。资金雷达源码及相关测试 Pyright:0 errors。
|
||||
- 前端资金雷达测试:41 passed。全量 ESLint、TypeScript、生产构建通过;资金雷达范围 Prettier 通过。
|
||||
- 浏览器已核验两个面板、名称入口、在榜展开、默认指标跟随当前面板、成员指标与前后 5 切换、复制/导出 14 名成员及弹窗下部。390px 窄屏发现的 grid/canvas 撑宽已修复,弹窗 clientWidth 和 scrollWidth 均为 358px。单点历史保留可见圆点。
|
||||
- `git diff --check` 通过。代码未提交,未执行自动归档或修改 specs。
|
||||
|
||||
## 已有全局检查问题与使用边界
|
||||
|
||||
全量后端 Pyright 仍报告 14 个错误,范围仅在未修改的 selection/application/chart.py、selection/domain/gold_brick.py 和 tests/unit/selection/test_run.py。全量前端 format:check 仍被两份已有 `.playwright-cli/page-2026-09-01T14-54-32-480Z.yml`、`page-2026-09-05T07-35-10-548Z.yml` 阻断。未扩大修复这些文件。构建另有大 chunk 提示,Alembic 有既有 path_separator 弃用提示。
|
||||
|
||||
正式业务数据库使用前需应用新迁移并按原构建流程采集/构建目标日;既有发布缺少可选主买净额时显示缺失,不通过实时请求补值。当前本地预览为 `http://127.0.0.1:5516/sector-radar`,连接上述单板块隔离库,后端端口 8016。最新截图在 `output/playwright/radar-detail.png`。
|
||||
Reference in New Issue
Block a user