Merge branch 'develop' into codex/point

This commit is contained in:
yuxuanhui
2026-08-31 16:14:35 +08:00
86 changed files with 13384 additions and 217 deletions
+2
View File
@@ -9,6 +9,7 @@
| [目录与模块边界](./directory-structure.md) | 包结构、bounded context 和导入边界 |
| [配置与运行时](./configuration-and-runtime.md) | `Settings`、应用工厂和部署环境 |
| [市场数据同步](./market-data-sync.md) | Tushare qfq、PostgreSQL、CSV 快照和一次性 Job 契约 |
| [Tushare 当前上市股票范围](./tushare-listed-stock-universe.md) | 所有股票型功能统一只使用构建时 `stock_basic(list_status=L)` 母集 |
| [历史选股](./selection.md) | selection bounded context、目标交易日、qfq 读取和信号结果契约 |
| [HTTP 契约](./http-api-contracts.md) | 路由组合、响应模型和同源 API 路径 |
| [错误处理](./error-handling.md) | 当前 FastAPI 错误行为及跨层错误传递 |
@@ -17,6 +18,7 @@
## 开发前检查
- 先阅读 `docs/adr/0001-bounded-context-first-modular-monolith.md`,确认新业务是否有清晰的语言和所有权边界。
- 涉及 Tushare 个股数据时先阅读 `tushare-listed-stock-universe.md`,所有新功能都必须从当前 `L` 股票母集继续缩小范围,禁止重新引入 `D/P/G/UN`。
- 先阅读目标上下文的 `modules/<bounded_context>/README.md`(如已存在),再决定 domain、application、infrastructure、presentation 的位置。
- 变更 HTTP 字段时同时检查 `zhixing-server/tests/`、前端 feature API 类型以及 `docs/adr/0002-use-a-same-origin-browser-api.md`。
- 不要为了“未来可能需要”创建空的数据库、服务或日志层;当前仓库没有这些实现。
@@ -0,0 +1,103 @@
# Tushare 当前上市股票范围
## Scenario: 所有股票型功能统一使用当前 `L` 股票池
### 1. Scope / Trigger
- 触发:新增或修改任何通过 Tushare 获取个股基础资料、行情、资金流、板块成员、财务或估值数据的后端功能。
- 目标:所有功能统一以构建时 `stock_basic(list_status="L")` 返回的当前上市股票为证券母集,禁止为了历史回溯获取 `D/P/G/UN`。
- 历史语义:功能上线日视为最早业务历史日期;以后重跑旧日期仍使用重跑当时的当前 `L` 股票池,不保证还原目标日的退市证券。
- 边界:指数、基金、期货、宏观等非个股数据不适用本股票状态契约;若未来产品必须恢复历史时点证券生命周期,必须先显式修改本规格及对应任务设计,不能在单个 adapter 内局部绕过。
### 2. Signatures
所有直接读取股票基础档案的 Tushare adapter 必须显式传入 `list_status="L"`:
```python
client.query(
"stock_basic",
exchange="",
list_status="L",
fields="ts_code,symbol,name,market,exchange,list_status,list_date,delist_date",
)
```
应用层不得通过 adapter 隐式缓存推断股票范围;需要候选股票的端口必须显式接收已经排序、去重并与当前 `L` 股票池相交的代码集合,例如:
```python
def fetch_moneyflow_dc(
trade_date: date,
candidate_codes: Sequence[str],
) -> SourceResult[MoneyflowDcRow]: ...
```
### 3. Contracts
- `stock_basic` 请求必须显式设置 `list_status="L"`,不能依赖供应商默认值,也不能循环请求 `D/P/G/UN`。
- 当前股票母集至少以 `ts_code` 唯一;返回的非 `L` 记录不得进入业务目标集合。严格 source adapter 应将与请求分区不符的状态视为来源契约错误,已有宽松同步边界至少必须在领域过滤时排除。
- 股票型功能可以继续执行自身既有的市场边界,例如沪深 A 股、B 股、北交所、ST 或风险警示过滤;这些过滤只能缩小 `L` 母集,不能重新引入其他上市状态。
- `daily`、`moneyflow_dc`、`dc_member` 等不支持 `list_status` 的接口可以按其最有效的方式获取原始响应,但进入计算、排名、覆盖率、缺口补拉或持久化业务事实前,候选代码必须与当前 `L` 母集取交集。
- 为审计保存的全市场原始 snapshot 可以包含非 `L` 行;非 `L` 行不得进入规范化事实、策略计算或“应覆盖股票数”。
- 当前 `L` 股票池必须带有构建时来源快照或等价审计信息。重试若复用旧下游 snapshot,必须确认它仍覆盖本轮候选集合;候选扩大时应在同一次重试中刷新相应下游来源。
- 本契约不要求各 bounded context 共享数据库表、缓存或 Tushare client;共享的是证券范围语义,而不是运行时耦合。
### 4. Validation & Error Matrix
| 条件 | 必须行为 |
| --- | --- |
| `stock_basic` 请求未显式传 `list_status="L"` | 测试失败;不得发布该功能 |
| `L` 分区返回 `D/P/G/UN` | 严格 adapter 抛来源契约错误,或在既有宽松边界明确排除;非 `L` 不得进入业务集合 |
| 板块成员包含非当前 `L` 股票 | 保留原始成员审计,计算候选与当前 `L` 集合取交集 |
| 目标日期早于当前 `L` 股票的 `list_date` | 从该目标日候选集合排除 |
| 行情或资金流全市场响应包含非 `L` 股票 | 原始 snapshot 可保留,规范化事实和覆盖率忽略这些股票 |
| 缺失补拉收到不在请求候选集合内的代码 | 按来源契约错误 fail closed,禁止合并 |
| 重试时成员恢复导致当前候选集合扩大 | 检查旧下游 snapshot 覆盖;不足时同轮刷新,不能先发布一次可预见的 `partial` |
| 新需求要求历史退市股票或历史时点生命周期 | 先修改本规格并完成独立设计评审,禁止直接请求 `D/P/G/UN` |
### 5. Good/Base/Bad Cases
- Good:资金雷达只请求一次 `stock_basic(list_status="L")`,将有效板块成员与当前沪深 A 股交集传给资金流 source;全市场原始资金流即使含额外股票,也只补拉和计算交集内代码。
- Base:普通行情同步从 `L` 股票池再排除 ST、北交所或不属于目标市场的证券;这是允许的模块级缩小,不改变全局母集。
- Good:重试刷新成员后发现新增两个当前 `L` 候选,旧资金流 checkpoint 少两只,于是同一次重试只刷新资金流来源组并恢复成功。
- Bad:为了回填旧日期,将 `stock_basic` 改为循环获取 `L/D/P/G/UN`,或者直接把 `dc_member` 的全部代码作为资金流覆盖分母。
- Bad:看到全市场原始 snapshot 含非 `L` 股票便将它们写入策略事实,造成候选数量、覆盖率或排名口径漂移。
### 6. Tests Required
- Tushare adapter 测试必须断言 `stock_basic` 的调用参数包含且只包含 `list_status="L"`,并断言非 `L` 返回记录不会进入结果。
- 应用编排测试必须构造板块成员、未来上市记录和当前 `L` 记录,断言传给下游 source 的候选集合是稳定排序后的交集。
- 规范化或策略测试必须断言非 `L`、目标日尚未上市、B 股或模块已排除市场不会贡献金额、覆盖率或排名。
- 重试测试必须覆盖“成员刷新后候选扩大但旧下游 checkpoint 不完整”,断言同一次重试刷新必要来源组。
- 新增股票型 bounded context 时,至少有一个边界测试证明它没有请求或引入 `D/P/G/UN`。
### 7. Wrong vs Correct
#### Wrong
```python
# 禁止:为历史回填循环获取全部生命周期状态。
rows = tuple(
client.query("stock_basic", list_status=status)
for status in ("L", "D", "P", "G", "UN")
)
candidate_codes = tuple(member.stock_code for member in memberships)
```
#### Correct
```python
# 正确:当前 L 是唯一母集,模块规则只能继续缩小它。
listed = client.query("stock_basic", list_status="L")
listed_codes = {
row.ts_code
for row in listed
if row.list_status == "L" and is_module_eligible(row, target_trade_date)
}
candidate_codes = tuple(
sorted(
member.stock_code
for member in memberships
if member.stock_code in listed_codes
)
)
```
@@ -0,0 +1,8 @@
{"file": ".trellis/spec/backend/index.md", "reason": "检查后端模块边界与开发清单"}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "检查 Ruff、Pyright、pytest 与禁止模式"}
{"file": ".trellis/spec/frontend/index.md", "reason": "检查前端 feature 与质量清单"}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "检查格式、lint、类型、测试、构建和可访问性"}
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "检查后端响应、前端类型、query 与页面一致性"}
{"file": ".trellis/tasks/08-28-sector-capital-radar/research/implementation-evidence.md", "reason": "检查独立指标声明、单位、空值和凭据边界"}
{"file": "docs/research/onechartlab-sector-capital-radar.md", "reason": "禁止把未知公式伪装为原站公式"}
{"file": ".trellis/tasks/08-28-sector-capital-radar/research/tushare-radar-contract.md", "reason": "检查接口、PIT、单位、覆盖率和 last-good 质量语义"}
@@ -0,0 +1,118 @@
# 板块资金雷达技术设计
## 设计结论
首个 MVP 建设 Tushare 独立生产链,生产运行时不读取 OneChartLab。后端新增 `sector_radar` bounded context,通过一次性 CLI 采集七类最小事实、保存 point-in-time 输入、计算三个带版本的知行独立指标、生成横截面排名并发布;FastAPI 只读取已发布结果。前端新增 `/sector-radar` feature,提供日期、板块类型、指标、强弱榜、排名变化、搜索和分页。
首个安全里程碑不实现成分股详情、历史轨迹图、导出、`moneyflow` 主买净额、`daily_basic` 流动性增强或 `moneyflow_ind_dc` 对账。架构为这些能力保留数据版本与指标策略 seam,但不创建空实现。
## Module 与 seam
### `sector_radar` bounded context
外部 interface 保持两个深模块:
1. `BuildSectorRadar.execute(command) -> BuildSummary` 隐藏目标交易日解析、采集、校验、持久化、指标计算、排名和发布切换。
2. `ReadSectorRadar.list_dates()` 与 `ReadSectorRadar.query(query) -> RankingPage` 隐藏 last-good 选择、筛选、排序和分页。
内部保留三个真实 seam:
- `SectorRadarSource`:Tushare 生产 adapter 与测试 fake adapter;负责 `trade_cal`、`dc_index`、`dc_member`、`stock_basic`、`suspend_d`、`daily`、`moneyflow_dc`。
- `SectorRadarRepository`:PostgreSQL 生产 adapter 与 application 测试 fake adapter;负责输入版本、发布批次和读取投影。
- `MetricStrategy`:金额、单日资金率和波段资金率是三个实际可替换 adapter;每个结果必须带 `metric_version`、显示标签、排序值、单位和质量状态。
Tushare 请求协调能力已有第二个真实消费者后,将 `RequestCoordinator` 从 `market_data` infrastructure 提升为无业务所有权的小型 shared 基础能力;原市场同步与雷达 adapter 同时复用,且保留现有行为测试。
## 领域模型
- `SectorType`:`concept` 或 `industry`;地域板块不进入 MVP。
- `SectorMembershipSnapshot`:`trade_date + sector_code + stock_code`,只表示该交易日的成员事实。
- `StockDailyFact`:股票在交易日的生命周期、停牌、成交额和主力净额状态;缺行、NULL、0 和无效成员不同义。
- `MetricObservation`:策略版本生成的值、单位、有效样本数、成员覆盖率和质量状态。
- `RadarPublication`:针对一个目标交易日的不可变构建版本,状态为 `running|success|partial|failed`,包含输入 hash、universe 版本、指标版本集合、覆盖率、开始/完成时间和安全错误摘要。
- `RadarRanking`:属于某个 publication 和排名池的指标值、1 基排名、排名百分位及 1—5 日变化。
- `LastGoodPublication`:不使用可被失败构建覆盖的可变字段;读取时选择最近一个 `status=success` 的 publication。失败与 partial 批次仍保留审计。
## 数据采集与质量
每日 Job 的目标交易日由 `trade_cal` 确认。目标日采集 `dc_index` 的概念与行业、`dc_member`、全部上市状态的 `stock_basic`、`suspend_d`、`daily` 和 `moneyflow_dc`。首次初始化或补算按交易日顺序执行,至少准备 10 个交易日才能产生完整波段指标,至少保留 30 个已发布交易日供排名变化和后续轨迹使用。
原始响应按 `api_name + normalized_params + observed_at` 保存 JSONB、行数和 SHA-256;规范表保存 point-in-time 成员与股票事实。响应接近官方单次上限或覆盖率不足时必须分片重拉,不能接受可能截断的成功响应。
有效股票候选满足目标日生命周期并属于沪深 A 股,排除北交所与 B 股;不沿用选股模块的 ST 排除规则。停牌且无 `daily` 的股票标记为 suspended,不作为应有行情缺失;应有 `daily` 或 `moneyflow_dc` 却缺失的股票进入覆盖率缺口,不能补 0。
发布成功门使用可配置的全局事实覆盖率,默认沿用项目的 `0.99`。成员快照必须完整;任一必需接口硬失败、重复业务键、日期错误、非有限数、单位校验失败或覆盖率低于门槛,当前 publication 为 `partial` 或 `failed`,不得成为 last-good。每个板块仍输出有效样本数、成员覆盖率和质量状态;少于 5 个有效成员标记 `available_limited_sample`。
## 独立指标策略
对板块 `s`、交易日 `t`,使用当日 point-in-time 有效成员:
```text
Net(s,t) = sum(moneyflow_dc.net_amount) × 10_000 元
Turnover(s,t) = sum(daily.amount) × 1_000 元
```
- `zhixing_amount_net_bn_v1 = Net(s,t) / 100_000_000`,排序值为亿元净额。
- `zhixing_ratio_turnover_v1 = Net(s,t) / Turnover(s,t)`;分母为 0 或输入不完整时为 NULL。
- 对每个 `w ∈ [3,10]`,`WindowRatio_w = sum(Net(s,d)) / sum(Turnover(s,d))`,其中每个 `d` 使用自己的成员快照;`zhixing_swing_equal_3_10_v1` 是八个完整 `WindowRatio_w` 的算术平均。任一窗口不完整时为 NULL。
这些名称和页面标签都明确写“知行独立实现”。接口不暴露原站的 `Ratio_Score` 或 `Swing_Score` 字段名,而统一返回 `metric_value`、`metric_version`、`unit` 和 `implementation_kind=independent`。
## 排名契约
概念与行业分别成池。NULL 指标不进入排名,但作为 unavailable 记录保留质量信息。非 NULL 值按 `metric_value DESC, sector_code ASC` 排序,后者是知行独立稳定键,不宣称原站并列规则。
```text
RankPct = 100 * (N - RankPos + 1) / N
Top = RankPct >= 90
Bottom = RankPct <= 10
RankChg = PastRank - CurrentRank
```
排名变化按过去第 1—5 个已发布交易日计算;过去或当前排名缺失时返回 NULL,而不是伪造 0。前端明确显示暂无可比历史。
## 持久化
新增一条 Alembic 迁移,至少包含:
- `sector_radar_source_snapshot`:接口、参数、目标日、原始 JSONB、行数、hash、观测时间;
- `sector_radar_membership`:来源快照、交易日、板块类型、板块代码、股票代码及展示名;
- `sector_radar_stock_fact`:来源快照集合、交易日、股票代码、生命周期/停牌状态、成交额、主力净额和数据状态;
- `sector_radar_publication`:构建身份、状态、版本、覆盖率、input hash、时间和错误摘要;
- `sector_radar_ranking`:publication、板块身份、指标版本、数值、单位、质量、排名百分位和历史变化。
业务唯一键必须包含交易日和来源/发布版本,允许同一交易日修订共存。数值使用有限 `NUMERIC`/`Decimal`;批量写入沿用 staging + COPY + 幂等 upsert。原始 token、完整请求头和未经净化的异常不得持久化。
## CLI 与发布流程
新增 `sector-radar-build` 一次性 CLI:
- 默认构建最近一个已收盘交易日;
- `--trade-date YYYY-MM-DD` 构建单日;
- `--start-date/--end-date` 按交易日顺序初始化或回填;
- `--retry-publication-id` 只重试失败来源分片。
Job 使用独立 advisory lock。顺序为准备 running publication、采集并保存原始版本、规范化与质量屏障、计算指标与排名、事务写入、标记 success。任何阶段失败都保留原 publication 审计,读取端继续选择 last-good。FastAPI 不启动定时器;生产 Compose 增加 job service,实际定时继续由外部调度器负责。
## HTTP 契约
挂载前缀 `/api/v1/sector-radar`:
- `GET /dates`:返回可用成功日期、当前尝试状态、last-good 日期和发布时间;
- `GET /rankings`:参数为 `trade_date`(缺省 last-good)、`sector_type`、`view=amount|ratio|swing|rank_change`、`rank_change_metric`、`rank_change_days=1..5`、`side=top|bottom|all`、`search`、`page` 和 `page_size`。
响应包含 publication 元数据、指标定义与独立实现声明、分页信息和排名行。没有成功发布时返回稳定的 `no_data` 成功响应;非法参数由 Pydantic/FastAPI 返回 422;数据库不可用映射 503。外部源异常只发生在 Job,不从读取端点实时透传。
## 前端
新增 `features/sector-radar` 垂直切片以及 `/sector-radar` 路由和导航入口。URL 保存日期、类型、视角、榜侧、搜索和分页;服务器数据只进入 React Query。页面使用现有 `PageLayout`、`Card`、`Input`、`Select`、`Badge`、`Pagination` 和语义 table,显式显示 loading、error、no-data、stale/partial/success。
首个 MVP 使用排名表和状态摘要,不引入图表库。金额显示亿元,比率显示百分比,指标旁始终显示策略版本或“知行独立实现”。
## 兼容、回滚与风险
- 新表、新路由和新 feature 不改变现有 market-data/selection 契约;shared 请求协调器移动必须先保持现有测试通过。
- 数据库迁移 downgrade 只删除新上下文表,不触碰现有市场数据。
- 外部调度在 job 验证稳定前保持关闭;手工构建与读取验证通过后再启用。
- 主要风险是目标账号实际权限/限流、`dc_member` 分片完整性和数据到达时间。首次实现必须提供不泄密的 capability probe 与覆盖率报告;没有 live token 时以 fake/golden 完成自动化验证,但不得声称生产采集通过。
- OneChartLab 对账差异只记录为研究数据,不自动覆盖本地结果。
@@ -0,0 +1,8 @@
{"file": ".trellis/spec/backend/index.md", "reason": "后端 bounded context、HTTP、市场同步与质量规范入口"}
{"file": ".trellis/spec/backend/market-data-sync.md", "reason": "复用 Tushare、批次、幂等、单位和发布约束"}
{"file": ".trellis/spec/backend/http-api-contracts.md", "reason": "新增 sector radar 同源 HTTP 契约"}
{"file": ".trellis/spec/frontend/index.md", "reason": "前端 feature、类型、query 与页面规范入口"}
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "保持 Pydantic 到前端页面的跨层字段一致"}
{"file": ".trellis/tasks/08-28-sector-capital-radar/research/implementation-evidence.md", "reason": "实现范围、仓库复用点、Tushare 客户端和独立指标契约"}
{"file": "docs/research/onechartlab-sector-capital-radar.md", "reason": "公开确认排名算法与未知公式边界"}
{"file": ".trellis/tasks/08-28-sector-capital-radar/research/tushare-radar-contract.md", "reason": "Radar MVP 所需 Tushare 接口、字段、单位、PIT 与质量规则"}
@@ -0,0 +1,73 @@
# 板块资金雷达执行计划
## 开始前门禁
- [ ] 用户审阅并明确批准 `prd.md`、`design.md` 和本计划后,运行 `task.py start`。
- [ ] 从 `develop@ad9545e` 创建/切换 `codex/sector-capital-radar`,写入任务 branch/base-branch 元数据;保留旧调研任务不变。
- [ ] 确认 `implement.jsonl` 与 `check.jsonl` 均含真实 spec/research 条目。
## 1. 纯领域安全里程碑
- [x] 在新的 `sector_radar` bounded context 定义板块类型、成员快照、股票事实、指标观察、发布与排名模型。
- [x] 先写固定人工样本测试,再实现 `zhixing_amount_net_bn_v1`、`zhixing_ratio_turnover_v1`、`zhixing_swing_equal_3_10_v1`。
- [x] 实现概念/行业分池、稳定并列键、1 基排名、百分位、TOP/BOTTOM 和 1—5 日排名变化。
- [x] 覆盖乱序输入、NULL/0、非有限数、空池、单元素、并列、历史缺失、停牌和 point-in-time 成员变化。
- [x] 运行 `uv run --directory zhixing-server pytest tests/unit/sector_radar`、Ruff 与 Pyright。此步绿灯是第一个可回滚安全点。
阶段结果(2026-08-29):三个透明指标策略、point-in-time 事实聚合、发布生命周期和横截面排名 seam 均已实现;13 个板块雷达领域测试通过。完整后端门禁为 96 passed、2 skipped,两个跳过项均为需要 `ZHIXING_TEST_DATABASE_URL` 的既有 PostgreSQL 集成测试。
## 2. Tushare 输入与持久化
- [x] 把已有 RequestCoordinator 提升到 shared 基础设施,保持 market-data 适配器及测试行为不变。
- [x] 定义 `SectorRadarSource` 与 Tushare adapter,显式请求七类接口及 fields;token 仅由 `Settings` 注入。
- [x] 实现服务端错误分类、有限重试、行数上限检测、`dc_member` 分片和账号 capability probe;输出不得包含 token。
- [x] 新增 Alembic 表、约束、索引和 downgrade,保存原始 JSONB/hash、成员快照、股票事实、publication 与 ranking。
- [x] 实现 PostgreSQL staging/COPY、幂等重跑、同日多修订、advisory lock 和 last-good 查询。
- [x] 为 repository fake、Tushare fake、迁移和 PostgreSQL 集成补测试;仅在 `ZHIXING_TEST_DATABASE_URL` 存在时执行数据库集成测试。
- [x] 若运行环境存在 `ZHIXING_TUSHARE_TOKEN`,执行只读 capability probe 并记录接口成功、字段和行数,不打印原始凭据;否则明确记录 live 验证未执行。
阶段结果(2026-08-29):七接口 source 契约、共享限流协调、源快照 hash、point-in-time 规范化、五张 PostgreSQL 表、COPY staging、同日修订与严格 `success` last-good 已落地。完整后端门禁为 109 passed、3 skipped;当前环境未设置 `ZHIXING_TUSHARE_TOKEN` 和 `ZHIXING_TEST_DATABASE_URL`,因此 live capability probe 与三项 PostgreSQL 集成测试未执行,未将其误报为通过。
## 3. 构建 Job
- [x] 实现 `BuildSectorRadar.execute` 的单日与日期区间编排、质量屏障、publication 状态和失败保留 last-good。
- [x] 新增 `sector-radar-build` CLI 及退出码;支持目标日、回填区间和失败 publication 重试。
- [x] 增加 Compose job service,但不启用生产定时;更新运行文档与无凭据示例。
- [x] 用 fake/golden 验证完整成功、部分数据、截断响应、重复运行、输入修订、并发锁和失败降级。
阶段结果(2026-08-29):单日/区间构建、上海时区最近已收盘日、provisional publication、同日锁、遗留 running 接管、内容 hash 去重、严格 last-good 与来源组检查点均已落地。failed 重试只补未完成来源组,partial 只刷新显式覆盖缺口;规范事实、日聚合、排名与 terminal publication 由 PostgreSQL 单事务完成。CLI、开发/生产 Compose entrypoint 和运行文档已提供。完整后端门禁为 122 passed、3 skipped;迁移头与离线升级 SQL、四种 Compose config 和 CLI help 已通过。当前未设置 `ZHIXING_TEST_DATABASE_URL`,三项真实 PostgreSQL 集成测试未执行;真实 Tushare capability 与数据到达时点也未在本阶段宣称通过。
## 4. HTTP 读取链
- [x] 实现 `ReadSectorRadar` 查询模块以及 `/dates`、`/rankings` Pydantic 契约。
- [x] 在路由目录挂载 `/api/v1/sector-radar`;实现筛选、分页、搜索、rank-change 参数和 `no_data`/503 行为。
- [x] 使用真实 `create_app()` 与 fake application dependency 写黑盒 HTTP 契约测试。
阶段结果(2026-08-29):读取端严格区分最新尝试、指定日期成功修订和全局 last-good;概念/行业分池支持 amount、ratio、swing、rank_change、普通百分位强弱榜、排名变化强弱榜、搜索与分页。响应携带 publication/source/universe/metric 版本、单位、质量与“知行独立实现”声明;无成功发布稳定返回 200 `no_data`,参数错误返回 422,存储错误返回脱敏 503。完整后端门禁为 134 passed、3 skipped;跳过项仍为需要 `ZHIXING_TEST_DATABASE_URL` 的真实 PostgreSQL 集成测试。
## 5. 前端 MVP
- [x] 新建 feature API types、adapter 与 React Query hooks;API 边界校验稳定枚举和关键字段。
- [x] 新增 `/sector-radar` 路由、导航、URL search 校验和活动路由映射。
- [x] 实现状态摘要、筛选工具栏、排名表和分页;显示单位、质量状态、数据日期、last-good/stale 和独立指标版本。
- [x] 页面测试覆盖成功、筛选、rank-change、loading、error、no-data、stale/partial;adapter 测试覆盖 URL、参数和 AbortSignal。
- [x] 不引入图表依赖,不实现成分详情、历史轨迹或导出。
阶段结果(2026-08-29):新增独立 `features/sector-radar` API、运行时契约解析、React Query hooks、URL search 驱动的筛选与分页、桌面/移动排名表、发布来源与质量摘要,以及 `/sector-radar` 导航入口。页面明确区分初次加载、致命错误、无数据、后台刷新、后台刷新失败、partial/failed/running 新尝试和 success,并始终保留“知行独立实现”与版本声明;排名变化缺少历史时显示“暂无可比历史”。前端 lint、typecheck、全量 63 项 Vitest 和生产 build 通过,变更文件的 Prettier 检查通过;完整 `pnpm format:check` 仍被未修改的既有 `zhixing-web/DESIGN.md` 格式问题阻挡。浏览器已在默认桌面视口与 390×844 移动视口验证导航、筛选布局、错误降级和 rank-change URL 状态;本地后端未运行,因此成功数据态的视觉行为由页面测试覆盖,未声称真实数据库页面已验证。
## 6. 全量验证与审查
- [x] 后端:`uv run ruff format --check .`、`uv run ruff check .`、`uv run pyright`、`uv run pytest`。
- [x] 前端:`pnpm format:check`、`pnpm lint`、`pnpm typecheck`、`pnpm test`、`pnpm build`。
- [x] 根级:`./dev.sh check`、`./dev.sh test`;验证开发和生产 Compose config。
- [x] 使用 `trellis-check` 做全范围规范、PRD、跨层字段、单位、空值、版本声明和凭据泄漏检查,并修复发现项。
- [x] 评估是否有经用户批准才应提升到 `.trellis/spec/` 的新知识;未经批准不写 Trellis spec。
阶段结果(2026-08-29):全范围终审补齐三项契约:`SourceSnapshot` identity 绑定返回字段、行上限和截断状态;显式空 `dc_member` 分区持久化为 `membership_unknown`,生成 unavailable 聚合并强制 publication 为 partial,只重试成员来源且绝不替换 last-good;HTTP 与前端把排名百分位统一收紧为 `(0, 100]`。新增迁移 head `0006_membership_unknown`,离线升级 SQL 已核对。最终后端 Ruff、Pyright 和全量测试为 139 passed、3 skipped,跳过项均需要 `ZHIXING_TEST_DATABASE_URL`;前端 format、lint、typecheck、全量 64 项 Vitest 与 build 通过,build 仅有既有单包大于 500 kB 的非阻塞提示;`./dev.sh check`、`./dev.sh test` 和开发/生产、默认/jobs 四种 Compose `config --quiet` 均通过。Compose 验证显式清空 `ZHIXING_TUSHARE_TOKEN` 并使用无敏感信息的占位数据库 URL。真实 PostgreSQL 集成、真实 Tushare capability、生产网络和部署权限仍未在本机环境验证,不将其误报为通过。全范围只读复核最终为 no blocking findings。经 `trellis-update-spec` 评估,unknown-membership 与快照 identity 属于可提升的候选知识,但用户未批准写 `.trellis/spec/`,本任务仅在设计、测试和本执行记录中保存。
## 风险与回滚点
- RequestCoordinator 提升后若现有 market-data 检查失败,先还原该重构,雷达 adapter 暂时内部组合相同行为,不改变现有同步。
- 数据库迁移与 Job 在 HTTP/前端之前独立落地;迁移失败可 downgrade 新表,不能修改现有市场数据表。
- live Tushare 调用只用于只读能力与数据质量验证;权限或到达时间不满足时,保留 fake/golden 里程碑并报告阻塞,不降低质量门或把缺失补 0。
- 前端只读取 success/last-good;后端发布未稳定前不启用外部定时任务。
@@ -0,0 +1,74 @@
# 板块资金雷达模块
## Goal
在知行系统中提供一个收盘后可查询的板块资金雷达,使用户能够按交易日分别查看概念板块与行业板块的资金强弱、前后榜和排名变化,并能辨认数据新鲜度、覆盖率与指标来源。实现以公开证据可复现为首要目标;任何未公开公式都必须采用有独立名称、版本和说明的可替换策略,不能宣称为 OneChartLab 原站公式。
## Background
- OneChartLab 当前产品是收盘后的板块横截面排名系统,不是盘中实时雷达。公开证据已确认概念与行业为两个独立排名池,排名键分别为 `Swing_Score`、`Ratio_Score` 与 `Amount_Raw_BN`,排名百分位、前后 10% 榜以及 1—5 日排名变化算法可确定性复现。依据:`docs/research/onechartlab-sector-capital-radar.md`。
- `Ratio_Score`、`Amount_Score`、3—10 日权重、`Swing_Score` 及相关异常值处理没有公开公式。独立实现不得根据字段名、线性拟合或作者口述伪造等价公式。依据:`docs/research/onechartlab-sector-capital-radar.md`、`docs/research/onechartlab-tushare-data-requirements.md`。
- 公开消费层与独立生产层是两个不同目标。前者可原样保存 OneChartLab manifest、日期分片、排名历史和成分详情以复现当前页面;后者需要 Tushare 的 point-in-time 板块成员和资金事实,并只能先生成明确标注的替代指标。依据:`docs/research/onechartlab-tushare-data-requirements.md`。
- 当前仓库已有 FastAPI 模块化单体、PostgreSQL 批次审计、Tushare 请求协调、原子 CSV 发布、同源 `/api/v1`、React Query 垂直切片及前后端测试模式,但尚无板块身份、板块成员快照、资金事实、雷达发布、显式 `last_good` 或图表基础设施。
- 当前 checkout 为 detached HEAD,提交 `ad9545e` 同时是本地与远端 `develop` 的头;新任务 `task.json` 尚未设置工作分支。进入实现前应创建或切换到任务分支并写入任务元数据,不改动现有 `08-27-onechartlab-research` 调研任务。
- 用户已选择 Tushare 独立生产 MVP,并确认具备所需 Tushare 权限。OneChartLab 公开 payload 只作为对账证据和测试样本来源,不作为生产运行时事实源。
## Requirements
### R1. 可追溯发布
每个可查询发布版本必须至少携带交易日、观测/发布时间、来源类型、来源版本、内容 hash、指标策略版本、universe 版本、数据状态和覆盖率。相同交易日允许存在修订版本,读取端必须能区分当前发布与最近一个有效发布。
### R2. 排名池与确定性算法
概念和行业必须独立排名,不能硬编码板块数量或成员关系。对公开证据已确认的算法必须提供确定性实现和回归测试:
- `Swing` 按 `Swing_Score` 降序,`Ratio` 按 `Ratio_Score` 降序,`Amount` 按 `Amount_Raw_BN` 降序;
- `RankPct = 100 * (N - rank + 1) / N`,排名从 1 开始;
- 普通强榜使用 `RankPct >= 90`,弱榜使用 `RankPct <= 10`;
- 排名变化为 `PastRank - CurrentRank`,正数表示上升;1—5 日历史不足或缺失时按明确契约处理;
- 排序必须确定性且不依赖输入遍历顺序;由于原站并列规则未知,本项目稳定键必须以独立实现契约命名并测试,不能标注为原站规则。
### R3. 未公开指标隔离
`Ratio`、`Swing` 及任何自建 score 的计算必须位于可替换指标策略 seam 后,并返回明确的 `metric_version`、参数、质量状态和所需输入。公开 payload 的预计算 score 与本项目独立策略不得混写为同一来源或同一版本。
### R4. Point-in-time 与空值语义
板块成员必须按交易日保存;缺失成员快照应为 `membership_unknown`,不得用当前成员回填历史。供应商缺行、NULL、数值 0、停牌、生命周期无效与低流动性必须保持不同语义,缺失资金流不得转成 0。
### R5. 单位与质量门
原始层保留供应商单位,规范层显式换算。MVP 使用的 `moneyflow_dc.net_amount` 为万元,`daily.amount` 为千元,两者必须先统一为元再计算比例。发布至少输出有效样本数、成员覆盖率、资金覆盖率和质量状态;未达完整性门槛时不得覆盖最近有效发布。
### R6. 读取契约与页面
后端在独立 bounded context 中提供稳定 Pydantic 响应,并经 `/api/v1` 同源路由暴露。前端在独立 feature 中使用 `requestJson`、React Query 和 URL/局部状态,显式呈现加载、错误、无数据、stale/partial 与成功状态。页面最少支持交易日、板块类型、指标视角、强弱榜切换、搜索和排名表。首个 MVP 不包含成分股详情、历史轨迹图或数据导出,但应保存足以计算 1—5 日排名变化的历史发布结果。
### R7. 收盘后执行与降级
生产流程沿用外部调度的一次性 Job,不在 FastAPI 生命周期内启动定时器。发布过程必须幂等并使用锁避免同一目标日期并发构建;上游未到齐或校验失败时保留 `last_good`,并把当前状态标记为 stale/partial/failed,不能发布假完整结果。
### R8. 安全与凭据
Tushare token 只从 `Settings`/环境注入,不得写入源码、日志、响应、测试 fixture 或 Trellis 任务文档。公开站点输入必须进行 schema 与有限数校验,不能仅依靠 TypeScript 泛型断言。
## Acceptance Criteria
- [x] AC1:给定固定样本和乱序输入,概念/行业的三套排名、排名百分位、普通前后榜和 1—5 日排名变化结果可重复,且测试覆盖空池、单元素池、并列值、历史缺失和非有限数。
- [x] AC2:每条雷达结果可追溯到唯一发布版本、来源版本、universe 版本和指标策略版本;响应和页面明确标注“知行独立实现”,不暴露或暗示原站 `Ratio_Score`、`Swing_Score` 字段。
- [x] AC3:缺失资金流、成员未知、低流动性、部分覆盖与失败发布不会被展示成完整的零值结果;失败构建不覆盖 `last_good`。
- [x] AC4:后端 HTTP 契约测试锁定筛选、分页/榜单、数据状态和错误行为;前端类型、API adapter、query 与页面测试覆盖 loading/error/no-data/stale/partial/success。
- [x] AC5:页面可分别浏览概念与行业排名池,并按交易日、指标视角和强弱榜筛选;金额、比例、策略版本与排名变化的单位和方向符合本任务契约。
- [x] AC6:收盘后 Job 可幂等重复执行,重复内容不产生无意义修订;同一日期并发执行被锁阻止,失败时保留最近有效发布。
- [x] AC7:运行相关后端 Ruff、Pyright、pytest 与前端 format、lint、typecheck、Vitest、build;跨层链路通过根级检查。没有实际运行的检查不得标记为通过。
- [x] AC8:生产运行时不请求 OneChartLab;Tushare 原始响应、point-in-time 成员和规范化股票事实足以重放同一指标策略版本,且公开样本只用于对账,不覆盖本地事实。
## Out of Scope
- 盘中实时资金流、WebSocket 推送或分钟级雷达。
- 在没有第一方公式和 point-in-time 输入证据时宣称完全复刻 `Ratio_Score`、`Swing_Score`、`Amount_Score` 或 3—10 日权重。
- 使用当前板块成员回填历史、把缺失值补 0,或把 OneChartLab 当前 universe 数量硬编码进实现。
- 宏观择时模块、交易执行、收益承诺和投资建议。
- 在首个安全里程碑中一次性实现研究报告列出的全部 17 个 Tushare 接口。
- 首个 MVP 的成分股详情、排名轨迹图、导出、盘中刷新、`daily_basic` 流动性增强和 `moneyflow_ind_dc` 三方对账;这些作为后续增量,不阻塞主榜生产。
@@ -0,0 +1,32 @@
# 板块资金雷达实现依据
## 已选择的产品边界
用户选择 Tushare 独立生产 MVP,并确认具备 Tushare 权限。生产运行时不依赖 OneChartLab;公开 payload 只用于验证公开契约、构造固定样本和对账。未公开公式必须使用知行系统自己的策略名称与版本。
## 仓库复用点
- 后端采用 `modules/<bounded_context>/{domain,application,infrastructure,presentation}`,依据 `docs/adr/0001-bounded-context-first-modular-monolith.md` 和 `.trellis/spec/backend/directory-structure.md`。雷达应创建独立 bounded context。
- `market_data.infrastructure.tushare.RequestCoordinator` 已实现供应商请求冷却、退避和有限重试;`TushareAdapter` 已使用注入的 `pro_api(token)` client。相关实现位于 `zhixing-server/src/zhixing_server/modules/market_data/infrastructure/tushare.py:40-126,235-377`。
- `SyncMarketData` 已实现 advisory lock、批次审计、部分成功和定向重试,位于 `zhixing-server/src/zhixing_server/modules/market_data/application/sync.py:150-165,255-261,408-532`。
- PostgreSQL 适配器已有连接池、事务、staging + COPY 和幂等 upsert 模式,位于 `zhixing-server/src/zhixing_server/modules/market_data/infrastructure/postgres.py:26-57,296-401,769-925`。
- FastAPI 业务路由由 `zhixing-server/src/zhixing_server/interfaces/http/router.py:10-18` 统一挂载;浏览器固定使用同源 `/api/v1`。
- 前端垂直切片、路由和页面状态模式分别见 `zhixing-web/src/routes/route-tree.tsx:17-70`、`zhixing-web/src/features/home/api/`、`zhixing-web/src/features/home/pages/home-page.tsx`。当前没有图表依赖,因此首个 MVP 不加入轨迹图。
- `.codegraph/` 不存在,跨文件影响分析只能使用源码、测试和 `rg`。
## 当前 Tushare 客户端核验
项目声明 `tushare>=1.4.24`,见 `zhixing-server/pyproject.toml:7-16`。2026-08-28 通过 Context7 解析 `/waditu/tushare` 与 `/websites/tushare_pro`,确认:
- `ts.pro_api(token)` 创建 `DataApi`;客户端 `query(api_name, fields, **kwargs)` 把接口名、token、参数和字段列表发送到服务端。
- 客户端本身不执行积分、权限、频率或行数限制;这些限制由 Tushare 服务端返回。因此采集 Job 必须记录安全错误类别、响应行数和覆盖率,并在返回数接近单次上限时分片重拉。
- 不采用 `set_token()` 的用户目录持久化方式;项目继续通过 `Settings` 注入 token,避免凭据落盘或进入任务文档。
Context7 对具体板块接口的字段覆盖有限,接口字段、单位、历史边界和 2026-08-28 权限快照继续以 `docs/research/onechartlab-tushare-data-requirements.md` 所列 Tushare 第一方页面为实现依据。上线前由目标账号执行能力探测,不能把文档积分视为账号实测结果。
## 独立指标契约
- `zhixing_amount_net_bn_v1`:对当日有效成员的 `moneyflow_dc.net_amount` 求和,由万元除以 10,000 转为亿元;它是待公开样本对账的独立聚合,不宣称原站等价。
- `zhixing_ratio_turnover_v1`:先统一为元,再计算板块 `sum(net_amount) / sum(daily.amount)`;分母为零或输入缺失时返回 NULL。
- `zhixing_swing_equal_3_10_v1`:对窗口 3—10 个交易日分别计算 `sum(net_amount) / sum(turnover)`,再对八个完整窗口等权平均。任何窗口不完整时该指标不可用。该公式是透明、可替换的知行独立实现,不是 OneChartLab 的 3—10 日权重或 `Swing_Score`。
- 横截面排名按指标值降序、板块代码升序稳定打破并列;概念与行业独立成池。`RankPct`、TOP/BOTTOM 阈值和 `PastRank - CurrentRank` 复用公开确认算法。
@@ -0,0 +1,35 @@
# Tushare 板块雷达最小契约
本文件从 `docs/research/onechartlab-tushare-data-requirements.md` 提炼首个 Radar MVP 实际需要的接口,避免实现上下文被宏观择时和后续增强接口稀释。上线前仍以目标账号的只读 capability probe 为准。
## 最小接口
| 接口 | 作用 | 调用与必要字段 | 业务键与边界 |
| --- | --- | --- | --- |
| `trade_cal` | 确认开市日和窗口 | `exchange,start_date,end_date`;`exchange,cal_date,is_open,pretrade_date` | `(exchange,cal_date)`;初始化后按年刷新 |
| `dc_index` | 当日概念/行业 universe | `trade_date,idx_type`;`ts_code,trade_date,name,idx_type,level,pct_change,leading_code` | `(trade_date,ts_code)`;概念与行业分别拉取,单次上限公开页为 5,000 |
| `dc_member` | 当日 point-in-time 成员 | 优先 `trade_date`,必要时按 `ts_code` 分片;`trade_date,ts_code,con_code,name` | `(trade_date,ts_code,con_code)`;单次上限 5,000,命中上限或覆盖不足必须分片,不得用当前成员补历史 |
| `stock_basic` | 生命周期与市场过滤 | 分别拉 `list_status=L,D,P,G,UN`;`ts_code,symbol,name,market,exchange,list_status,list_date,delist_date` | `ts_code + observed_at`;默认只返回 L,不能漏掉其他状态 |
| `suspend_d` | 区分停牌与缺数 | `trade_date`;`ts_code,trade_date,suspend_timing,suspend_type` | `(ts_code,trade_date,suspend_type,suspend_timing)`;官方称不定期修订,需重叠回拉 |
| `daily` | 成交额、涨跌幅和行情可用性 | `trade_date`;`ts_code,trade_date,close,pre_close,pct_chg,vol,amount` | `(ts_code,trade_date)`;单次上限 6,000,停牌期间不返回;`amount` 单位千元 |
| `moneyflow_dc` | 个股东财口径主力净额 | `trade_date`;`trade_date,ts_code,name,net_amount,net_amount_rate,pct_change,close` | `(ts_code,trade_date)`;单次上限 6,000,历史始于 2023-09-11;`net_amount` 单位万元 |
## 单位与规范化
- `moneyflow_dc.net_amount` 万元转元时乘 `10_000`,转亿元时除 `10_000`。
- `daily.amount` 千元转元时乘 `1_000`,转亿元时除 `100_000`。
- `daily.pct_chg=1.5` 表示 1.5%;不与 OneChartLab 小数比例字段直接混算。
- 空字符串、`None` 和 `NaN` 规范为 NULL;`inf`、`-inf`、重复业务键和错误交易日属于硬错误。
- 缺失资金流不是 0。只有生命周期有效、非停牌且源接口应有记录的股票进入缺失率分母。
## Universe 与质量
- 只纳入沪深 A 股,排除北交所与沪深 B 股;OneChartLab Radar 契约未声明排除 ST,因此不能复用现有选股股票池的 ST 过滤。
- `dc_member` 某日缺失时标记 `membership_unknown`,不能向前或向后填充。
- 全局分别计算成员、`daily` 和 `moneyflow_dc` 覆盖率;publication 只有在全部必需接口通过、成员完整且事实覆盖率达到配置门槛时才为 success。
- 板块结果输出成员数、有效样本数、成交额、成员覆盖率与资金覆盖率;有效样本少于 5 时标记 `available_limited_sample`。
- 初始回填必须按交易日顺序完成至少 10 日,才能生成完整 3—10 日独立波段指标;`moneyflow_dc` 的最早日期是硬边界。
## 客户端与安全
项目使用 `ts.pro_api(token)` 返回的 `DataApi`,动态调用最终进入 `query(api_name, fields, **params)`。客户端不执行权限、积分、频率或行数保护,采集 adapter 必须处理服务端错误、退避、返回行数和覆盖率。token 只从 `Settings` 注入,不调用 `set_token()` 写用户目录,不写入日志、原始请求清单或错误摘要。
@@ -0,0 +1,26 @@
{
"id": "sector-capital-radar",
"name": "sector-capital-radar",
"title": "板块资金雷达模块",
"description": "基于 Tushare point-in-time 事实独立生产收盘后板块资金排名、版本化指标、last-good API 与前端页面。",
"status": "completed",
"dev_type": null,
"scope": "fullstack",
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-28",
"completedAt": "2026-08-29",
"branch": "codex/zijin",
"base_branch": "develop",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,7 @@
{"file":".trellis/spec/backend/index.md","reason":"核验后端模块边界与开发规范"}
{"file":".trellis/spec/backend/market-data-sync.md","reason":"核验共享 coordinator 未改变 market-data 语义"}
{"file":".trellis/spec/backend/tushare-listed-stock-universe.md","reason":"核验所有业务候选与当前 L 股票母集相交"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"执行完整后端质量门禁"}
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"核验共享能力没有越界或重复实现"}
{"file":".trellis/tasks/08-31-sector-radar-listed-moneyflow-recovery/research/radar-build-tushare-call-chain.md","reason":"核验 source group、候选集与 retry/checkpoint 契约"}
{"file":".trellis/tasks/08-31-sector-radar-listed-moneyflow-recovery/research/tushare-global-start-interval.md","reason":"核验两路 worker、共享启动间隔及确定性测试覆盖"}
@@ -0,0 +1,29 @@
# 设计:当前上市股票池与 `moneyflow_dc` 缺口补拉
## 边界与契约
应用层在完成板块成员与 `stock_basic(L)` 采集后,先规范化成员并计算“有效成员代码与当前 L 股票代码的交集”,再调用扩展后的 `SectorRadarSource.fetch_moneyflow_dc(trade_date, candidate_codes)`。候选集合通过端口显式传递;adapter 不缓存先前 `fetch_stock_basics` 的响应,因此 publication replay 和 retry 仍是无隐式状态的。
`stock_basic` source 由五分区请求收敛为单一 `L` 分区。领域层继续用现有代码、市场和 `list_date` 规则处理当前上市候选,不新增 ST 过滤或历史退市语义。
## 资金流数据流
adapter 首先保存按 `trade_date` 获取的全市场 snapshot。它对 rows 执行 typed parsing、目标日期和 `ts_code` 唯一性校验;初始 snapshot 达到 6000 行只表示全市场可能截断,不再单独构成失败。随后计算 `candidate_codes - returned_codes`。
缺失集合为空时返回首批 snapshot 与 rows。存在缺失时,按排序后的 `ts_code` 使用固定两路 executor 请求 `moneyflow_dc(trade_date=..., ts_code=...)`。每个成功分片形成独立、带 `partition_key=ts_code` 的 snapshot;分片只允许为空或返回所请求股票在目标日期的唯一记录。空分片以及重试耗尽的普通 provider 异常不产生伪造 snapshot/row,并保留为覆盖缺口;来源 schema、日期、代码、唯一键或 row-limit 契约错误立即上浮。
主线程按输入代码顺序汇总 future,保证 `source_order=0` 始终是全市场 snapshot,后续分片按 `ts_code` 稳定排列。合并 rows 后再次验证 `(trade_date, ts_code)` 唯一,防止首批与分片重叠。所有 snapshot 继续归入 `PublicationSourceGroup.MONEYFLOW_DC`,现有数据库模型无需迁移。
## 并发与限流
共享 `RequestCoordinator` 增加默认值为 0 的 `request_interval_seconds` 和受现有 `threading.Condition` 保护的下次启动时刻。每次 attempt 在调用 provider 前原子等待 cooldown 并预约请求启动槽,预约完成后释放锁,再执行真实请求。资金雷达默认 coordinator 接收现有的 0.2 秒配置,移除 adapter 请求完成后的独立 sleep;因此两个 worker可重叠网络等待,但同一 adapter 中任意两次请求的启动时间仍至少相隔 0.2 秒。
当前锁定的 Tushare 1.4.29 `DataApi.query` 只读取 client 的 token、URL 和 timeout,在局部变量中构造参数并调用模块级 `requests.post`,未维护单次请求可变状态。两路 worker 共享该 client 的风险可接受,并由并发单元测试约束;该结论不扩展为 Tushare SDK 的通用线程安全保证。
## 兼容性、失败与回滚
`RequestCoordinator` 的新参数默认关闭,market-data bounded context 行为不变。端口签名变化同步更新 fake source 和 CLI/build 测试。普通补拉调用在 coordinator 的有限 retry 后仍失败时记录安全日志并留下覆盖缺口;契约错误保持 hard failure。日志不得包含 token 或完整 payload。
publication retry 只有在已保存的 `MONEYFLOW_DC` rows 仍覆盖本轮候选代码时才重放该来源组。若 `MEMBERS` 刷新后候选集合扩大,旧资金流 checkpoint 不足以覆盖新增候选,则在同一次 retry 中刷新 `MONEYFLOW_DC`,避免先发布一次可预见的 partial 再要求第二次重试。
回滚只需恢复 adapter 的单次 `moneyflow_dc` 请求、旧端口签名和协调器调用方式,不涉及 schema 或数据迁移。已生成的分片 snapshots 使用现有通用存储格式,旧版本即使不能主动生成,也仍可按 source group replay。
@@ -0,0 +1,9 @@
{"file":".trellis/spec/backend/index.md","reason":"后端模块边界、开发前检查和质量入口"}
{"file":".trellis/spec/backend/directory-structure.md","reason":"共享协调器与 sector_radar bounded context 的所有权边界"}
{"file":".trellis/spec/backend/configuration-and-runtime.md","reason":"复用现有请求间隔配置并避免新增环境读取"}
{"file":".trellis/spec/backend/market-data-sync.md","reason":"现有 Tushare coordinator、并发与 checkpoint 相邻契约"}
{"file":".trellis/spec/backend/tushare-listed-stock-universe.md","reason":"所有股票型功能只使用构建时当前 L 股票母集"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"后端 Ruff、Pyright 和 pytest 门禁"}
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"评估 RequestCoordinator 共享原语扩展"}
{"file":".trellis/tasks/08-31-sector-radar-listed-moneyflow-recovery/research/radar-build-tushare-call-chain.md","reason":"资金雷达调用顺序、候选集与 checkpoint 证据"}
{"file":".trellis/tasks/08-31-sector-radar-listed-moneyflow-recovery/research/tushare-global-start-interval.md","reason":"两路 worker 与全局启动间隔的适配点和测试模式"}
@@ -0,0 +1,25 @@
# 实施计划
1. 创建 `codex/sector-radar-listed-moneyflow-recovery` 分支并读取目标 backend/shared、sector radar 代码及相关规格。
2. 扩展 `RequestCoordinator`,实现线程安全的共享请求启动间隔,保留默认关闭与现有 cooldown/retry 行为;补充确定性单元测试。
3. 将 `stock_basic` 收敛为单一 `L` 分区,并调整 source 测试与返回状态契约。
4. 前移候选股票形成步骤,扩展 `SectorRadarSource.fetch_moneyflow_dc` 端口,向 adapter 显式传递稳定的当前 L 候选代码。
5. 在 Tushare adapter 内实现全市场首拉、候选覆盖检查、两路缺失代码补拉、分片契约校验、稳定 snapshot/row 汇总和安全错误日志。
6. 更新 FakeRadarSource、build/retry 测试和 CLI 组合测试,覆盖成功、6000 行、空分片、瞬时失败、错误日期/代码、重复键、分片触顶、两路 worker 与 checkpoint 重试。
7. 更新必要的运维说明,明确当前 L 股票池、候选覆盖语义、同一 adapter 的 0.2 秒共享间隔以及不同定时任务不得重叠。
8. 依次运行定向 pytest、Ruff format/lint、Pyright、完整 pytest,并由独立 Trellis check 代理核验规格和实现;修复所有本任务引入的问题后提交本地分支。
## 风险点与回滚检查
- `RequestCoordinator` 是共享模块,必须证明默认参数不改变 market-data 并发。
- worker 完成顺序不能进入 publication source order 或 input hash。
- 不能把普通 provider 异常与来源契约错误混为一类,也不能用空行伪造成功分片。
- 端口签名变化必须同步所有 fake/replay 路径,完整测试前不得仅凭 source 单测判定完成。
- 无数据库迁移;若验证失败,可按步骤分别回滚协调器启动槽和资金流分片逻辑。
## 验证结果
- `uv lock --check`、Ruff format/check、Pyright strict 全部通过。
- 后端完整测试 `159 passed, 3 skipped`;跳过项均要求显式设置 `ZHIXING_TEST_DATABASE_URL`。
- 根目录 `./dev.sh check` 与 `./dev.sh test` 通过;前端 `64 passed`。
- 未执行真实 Tushare 账号并发调用与真实 PostgreSQL 集成测试,留待部署后的 capability/生产批次验证。
@@ -0,0 +1,40 @@
# 资金雷达当前上市股票池与资金流缺口补拉
## Goal
让板块资金雷达以“构建时当前上市股票”为唯一证券范围,并在 Tushare `moneyflow_dc` 单日响应触及 6000 行上限时,仍能安全验证和补齐雷达候选股票,而不是直接失败或接受可能截断的数据。
## Background
- 生产构建目标日 `2026-08-28` 已在 `moneyflow_dc` 来源组因响应达到供应商 6000 行上限而失败。
- Tushare 官方接口说明确认 `moneyflow_dc` 单次最多返回 6000 条,并支持按日期或股票代码循环提取。
- 用户明确不要求历史时点证券生命周期还原;功能上线日视为最早历史日期,当前及未来均只研究构建时 `stock_basic(list_status=L)` 返回的股票。
- Tushare 账号频率限制为 500 次/分钟;用户批准资金流缺口补拉使用 2 个 worker,但两个 worker必须共享同一请求启动限流器。
## Requirements
1. `stock_basic` 只请求 `list_status=L`,不再请求 `D/P/G/UN`。保留资金雷达既有的沪深 A 股、B 股/北交所排除和上市日期校验,不额外引入 `market-data-sync` 的 ST 过滤语义。
2. 资金流完整性只针对有效板块成员与当前 `L` 股票的交集。应用层必须在请求 `moneyflow_dc` 前形成稳定、去重的候选代码集合,并通过显式端口参数传给 source adapter,禁止依赖 adapter 内部调用顺序或缓存状态。
3. `moneyflow_dc` 首次仍按目标交易日请求全市场。首次响应即使达到 6000 行,也必须先校验目标日期和业务唯一键,再检查候选股票覆盖率,不能直接接受或直接报截断。
4. 首次响应缺少候选股票时,只按稳定排序后的缺失 `ts_code` 补拉。每个分片必须同时传入 `trade_date` 和 `ts_code`,并校验返回日期、返回代码、唯一键以及供应商是否忽略了分片参数。
5. 缺口补拉固定使用 2 个 worker。同一 adapter 的所有首次请求、补拉请求及 retry 共享请求启动间隔,默认相邻请求启动至少间隔 0.2 秒;普通 provider 调用允许重叠,不得把整个请求放在协调器锁内。
6. 空分片或重试耗尽的瞬时请求失败保留为真实缺口,不补零;构建继续走现有覆盖率逻辑并可发布 `partial`。日期错误、返回错误股票代码、重复业务键或分片再次触及供应商上限属于来源契约错误,必须 fail closed。
7. 全市场首批 snapshot 与每个成功分片 snapshot 都属于现有 `MONEYFLOW_DC` source group,并按确定性顺序保存。重试继续复用已完成来源组,只刷新资金流来源组,不新增数据库表或 publication group。
8. `RequestCoordinator` 的请求启动间隔默认关闭,只有资金雷达通过现有 `sector_radar_request_interval_seconds` 启用,不能改变 `market-data-sync` 当前八路并发语义。
## Acceptance Criteria
- [x] 资金雷达构建只发出一次 `stock_basic(list_status=L)` 请求,并拒绝该分区返回非 `L` 状态。
- [x] `moneyflow_dc` 首批低于或等于 6000 行且覆盖全部候选股票时均可成功解析;达到 6000 行本身不再导致 `SourceTruncatedError`。
- [x] 首批未覆盖候选股票时,仅补拉缺失代码,调用总数为 `1 + 缺失代码数`,最终 snapshot 顺序与 worker 完成顺序无关。
- [x] 两个补拉请求可以处于并发等待状态,但共享协调器记录的请求启动时间间隔不小于配置值;默认配置下理论总速率不超过约 300 次/分钟。
- [x] 空补拉和瞬时请求失败不会被补零或伪装成完整覆盖;错误日期、错误代码、重复键和分片触顶会阻止发布错误结果。
- [x] failed/partial publication 重试仍复用既有 source checkpoints,并只刷新需要重取的 `MONEYFLOW_DC` group。
- [x] 共享协调器、sector radar source/build/CLI 相关单元测试、Ruff、Pyright 和完整后端 pytest 通过;需要真实 PostgreSQL 的测试若未配置,必须明确报告跳过状态。
## Out of Scope
- 不保证历史日期按当时上市状态精确重建,也不保留已退市股票进入未来重跑结果。
- 不复用 `market-data-sync` 的数据库股票池、行情或 Tushare client。
- 不实现跨进程或跨定时任务的分布式限流;运维上仍要求 `market-data-sync` 与 `sector-radar-build` 不重叠运行。
- 不改变板块评分公式、前端展示、数据库 schema 或其他 Tushare 来源组的请求策略。
@@ -0,0 +1,85 @@
# Research: 板块资金雷达 build 到 Tushare source 调用链
- Query: 定位板块资金雷达从 application build 到 Tushare source 的完整调用链,解释 `stock_basic` 为什么请求 `L/D/P/G/UN`、`moneyflow_dc` 在哪里按 6000 行拒绝、候选股票集合何时形成,以及重试时 publication/source checkpoint 如何复用。
- Scope: internal
- Date: 2026-08-31
## Findings
### 1. 完整调用链
生产入口由 `zhixing-server/pyproject.toml:36-38` 将 `sector-radar-build` 绑定到 `presentation.cli:main`。CLI 在 `zhixing-server/src/zhixing_server/modules/sector_radar/presentation/cli.py:42-47` 构造 `BuildSectorRadarCommand`,在同文件 `:62-77` 用 token 创建 `TushareSectorRadarAdapter`、创建 PostgreSQL repository,并调用 `BuildSectorRadar(...).execute(command)`。
application 层从 `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:199-222` 的 `BuildSectorRadar.execute` 开始。普通单日/区间模式先由 `_resolve_targets` 调 `source.fetch_trade_calendar` 解析目标交易日(`:224-247`);retry 模式则直接读取原 publication 并复用其目标交易日(`:225-231`)。随后 `_build_target` 获取按交易日的 advisory lock(`:257-271`),`_build_locked` 恢复遗留 running publication、创建新的 running publication、加载可复用来源组,再进入 `_collect`(`:285-310`)。
`_collect` 以固定顺序调用 `_fetch_group`:calendar、concept indices、industry indices、members、stock basics、suspensions、daily、moneyflow_dc,见 `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:498-563`。application 依赖的 source port 定义在 `zhixing-server/src/zhixing_server/modules/sector_radar/domain/ports.py:24-47`;生产实现是 `TushareSectorRadarAdapter`。每个 adapter 方法最终进入 `TushareSectorRadarAdapter._fetch_snapshot`,它组装显式 fields 后优先调用 `client.query(api_name, fields=..., **params)`,见 `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:368-405`。因此资金流主链是 `BuildSectorRadar.execute -> _build_target -> _build_locked -> _collect -> _fetch_group(MONEYFLOW_DC) -> SectorRadarSource.fetch_moneyflow_dc -> TushareSectorRadarAdapter.fetch_moneyflow_dc -> _fetch_snapshot -> client.query("moneyflow_dc", trade_date=..., fields=...)`。
`_fetch_group` 不只是调用 source:新拉或重放成功后,它立即保存 content-addressed raw snapshot,并以 `(publication_id, source_group, source_order)` 建立 publication checkpoint 链接,见 `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:457-496`。这意味着失败发生前已完成的每一组都已经具备可恢复检查点。
### 2. `stock_basic` 为什么请求 `L/D/P/G/UN`
adapter 明确说明不能依赖 Tushare 默认只返回 `L`,并逐一请求五个文档化生命周期分区,见 `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:267-285`。每个响应还校验返回 `list_status` 必须与请求分区一致(`:280-282`),最后跨分区校验 `ts_code` 唯一(`:284`)。现有回归测试固定了五次请求顺序与五种状态均被汇总,见 `zhixing-server/tests/unit/sector_radar/test_tushare_source.py:308-333`。
业务原因是 radar 需要按目标交易日判断 point-in-time 生命周期,而不是只看“当前仍上市”的默认集合。`StockBasicRow` 保存 `list_date`/`delist_date`,见 `zhixing-server/src/zhixing_server/modules/sector_radar/domain/source.py:338-364`;真正的生命周期判断在 `zhixing-server/src/zhixing_server/modules/sector_radar/domain/normalize.py:200-210`,要求沪深 A 股、非 B 股/北交所、`list_date <= target` 且目标日不晚于 `delist_date`。因此完整状态分区主要用于避免历史目标日漏掉目前已退市/暂停等股票,并使未上市/过会等记录由日期规则明确排除。`list_status` 本身目前不直接决定资格,资格由代码、市场及上市/退市日期决定。
### 3. `moneyflow_dc` 的 6000 行拒绝点
`ROW_LIMITS["moneyflow_dc"]` 在 `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:73-81` 固定为 `6_000`。`_fetch_snapshot` 把该上限传给 `build_source_snapshot`(同文件 `:397-405`);snapshot builder 在 `zhixing-server/src/zhixing_server/modules/sector_radar/domain/source.py:147-160` 计算 `row_count`,并以 `row_count >= row_limit` 标记 `limit_reached=True`,所以恰好返回 6000 行也视为可能截断。
具体拒绝发生在 `TushareSectorRadarAdapter.fetch_moneyflow_dc`:取到 snapshot 后立刻调用 `_reject_limit`,见 `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:318-330`;`_reject_limit` 在同文件 `:465-468` 抛出 `SourceTruncatedError("moneyflow_dc reached its provider row limit")`。这里没有像 `dc_member` 那样的分区补拉逻辑;错误经 `_fetch_group` 和 `_build_locked` 上浮,最终 publication 被记为 failed(`application/build.py:391-421`)。
### 4. 候选股票集合形成时点
候选集不是在请求 `moneyflow_dc` 之前形成。`_collect` 先完成全部八个来源组,包括 full-market `daily` 与 `moneyflow_dc`(`zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:550-563`),然后才调用 `normalize_memberships`,从状态为 `AVAILABLE` 的板块成员记录中取非空 `stock_code`、去重并排序为 `candidate_codes`(`:565-574`)。随后 `candidate_codes` 才传入 `normalize_stock_facts`(`:575-582`)。
这个集合此时只是“当日概念/行业成员股票并集”,尚未完成生命周期过滤。`normalize_stock_facts` 在遍历候选代码时才逐只调用生命周期规则;不合法者被保留为 `LIFECYCLE_INVALID` fact,见 `zhixing-server/src/zhixing_server/modules/sector_radar/domain/normalize.py:159-170`。所以当前调用顺序无法用候选集缩小或分片本轮 `moneyflow_dc` 请求,这是本次“当前上市股票池与资金流缺口补拉”设计需要显式调整的结构性边界。
### 5. publication/source checkpoint 的重试复用
retry 命令只接受 `partial` 或 `failed` publication,并复用旧 publication 的目标交易日,见 `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:224-231`。实际重试不会继续写旧 publication,而是在持有日期锁后新建一个 running publication,再从旧 publication 加载 source checkpoints(`:285-310`)。
checkpoint 的领域模型是八个稳定的 `PublicationSourceGroup` 和有序 `PublicationSourceRecord`,后者持有 raw `SourceSnapshot` 与 `refresh_on_retry` 标志,见 `zhixing-server/src/zhixing_server/modules/sector_radar/domain/persistence.py:131-160`。数据库表以 `(publication_id, source_group, source_order)` 为主键,raw snapshot 外键采用 `RESTRICT`,见 `zhixing-server/migrations/versions/0005_radar_daily_aggregate.py:17-55`;repository 加载时 join raw snapshot 并按 group/order 返回,见 `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/postgres.py:192-222`。
`_reusable_sources` 会忽略 `refresh_on_retry=True` 的记录;其余记录按 `source_order` 排序,并要求编号从 0 连续,否则拒绝重放,见 `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:433-455`。`_fetch_group` 命中 reusable group 时不访问 Tushare,而是从保存的 snapshot rows 重新执行 typed parser;未命中时才调用 source。无论重放还是新拉,snapshot 都会再次链接到新的 running publication,见同文件 `:457-496`。
两类失败的复用语义不同。对于完整采集后因覆盖率不足形成的 partial,`_retry_source_groups` 根据 `membership_unknown`、缺失/空 `daily`、缺失/空 `moneyflow` 精确选择需刷新的来源组,见 `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:683-704`;`finalize_publication` 在同一事务中把这些组标记为 `refresh_on_retry=TRUE` 后再结束 publication,见 `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/postgres.py:562-571`。对于采集中途 hard failure,成功组已经由 `_fetch_group` 即时 checkpoint,失败组及其后的组没有记录;因此 retry 自动重放所有已完成组并从首个未完成组继续。测试证明 partial 资金缺口只再次调用 `moneyflow_dc`(`zhixing-server/tests/unit/sector_radar/test_build.py:389-406`),而 daily hard failure 后会复用此前六组,只再次调用 `daily` 和尚未执行的 `moneyflow_dc`(`:409-426`)。
收集完成后,所有 snapshot id 与指标版本参与 `input_hash`(`zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:706-716`);若已存在同目标日、同 input hash 的 publication,新 running publication 会被丢弃并返回 existing publication,见同文件 `:310-327`。这是 publication 级幂等复用,与 source-group 级断点重放互补。
## Files Found
- `zhixing-server/src/zhixing_server/modules/sector_radar/presentation/cli.py`:生产 CLI 组合根,创建 Tushare adapter、PostgreSQL repository 与 application use case。
- `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py`:目标日期解析、来源组采集顺序、候选集生成、publication 生命周期及重试复用核心。
- `zhixing-server/src/zhixing_server/modules/sector_radar/domain/ports.py`:application 到 source adapter 的端口契约。
- `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py`:七类 Tushare 接口、状态分区、行数上限与 `client.query` 边界。
- `zhixing-server/src/zhixing_server/modules/sector_radar/domain/source.py`:raw snapshot 的上限标记、typed row 解析与 source contract errors。
- `zhixing-server/src/zhixing_server/modules/sector_radar/domain/normalize.py`:成员候选并集之后的生命周期、停牌、行情与资金流事实归一化。
- `zhixing-server/src/zhixing_server/modules/sector_radar/domain/persistence.py`:source checkpoint 分组和值对象契约。
- `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/postgres.py`:checkpoint 的保存、加载与 partial 刷新标记事务。
- `zhixing-server/migrations/versions/0005_radar_daily_aggregate.py`:publication-source checkpoint 表结构及完整性约束。
- `zhixing-server/tests/unit/sector_radar/test_build.py`:partial/failed 重试只刷新未完成来源组的可执行证据。
- `zhixing-server/tests/unit/sector_radar/test_tushare_source.py`:五种 `stock_basic` 状态分区请求的回归证据。
## Code Patterns
- Port/adapter:application 只依赖 `SectorRadarSource`,CLI 注入 `TushareSectorRadarAdapter`(`domain/ports.py:24-47`;`presentation/cli.py:62-77`)。
- Point-in-time master data:显式拉取所有生命周期状态,再按目标日 `list_date`/`delist_date` 判定(`infrastructure/tushare.py:267-285`;`domain/normalize.py:200-210`)。
- Fail closed on provider limit:snapshot 以 `>=` 标记触顶,不能将潜在截断当成功(`domain/source.py:158-160`;`infrastructure/tushare.py:465-468`)。
- Immediate source checkpoint:每个来源组一成功就保存 raw snapshot 及 publication link,而不是等待整个 publication 完成(`application/build.py:486-496`)。
- Selective retry:partial 显式标记缺口组;failed 依赖已完成组存在、未完成组缺席来恢复(`application/build.py:433-475,683-704`)。
## External References
- 本次为内部调用链研究,未新增外部资料检索。既有已归档研究 `.trellis/tasks/archive/2026-08/08-28-sector-capital-radar/research/tushare-radar-contract.md:7-15` 记录了原实现采用的 Tushare 接口边界:`stock_basic` 默认只返回 `L`,`moneyflow_dc` 单次上限 6000;上线前仍应以目标账号 capability probe 和当时官方文档为准。
## Related Specs
- `.trellis/spec/backend/market-data-sync.md`:一次性 Tushare Job、可恢复 snapshot、失败保留旧发布的相邻上下文规范;sector radar 有独立 bounded context,不能直接套用选股/ST 股票池规则。
- `.trellis/spec/backend/selection.md`:selection 的当前沪深非 ST 股票池契约不等于 radar 的 point-in-time 板块成员 universe。
- `.trellis/tasks/archive/2026-08/08-28-sector-capital-radar/design.md:36-44`:原 radar 设计要求全部上市状态、目标日生命周期、沪深 A 股过滤及行数触顶时不得接受截断响应。
## Caveats / Not Found
- 当前 `moneyflow_dc` 没有按候选股票或代码分片的实现;达到 6000 行只会 hard fail。`dc_member` 有按板块分区补拉,可作为模式参考,但不能直接证明 Tushare `moneyflow_dc` 支持同样的参数或批量行为。
- 当前候选集形成得晚于 `moneyflow_dc` 请求,并且成员并集与“生命周期有效股票池”是两个阶段;讨论修复时必须明确要前移哪一个集合,避免误把所有板块成员都视为当前上市股票。
- 代码中的 `ROW_LIMITS` 是本地契约常量,不是运行时从供应商元数据发现;若要改变请求策略,需要重新核对当前 Tushare `moneyflow_dc` 的可用过滤参数、单次限制及积分权限。
@@ -0,0 +1,92 @@
# Research: Tushare 两路补拉的全局请求启动间隔
- Query: 检索仓库现有 Tushare 限流、并发 worker、线程安全、测试夹具与 `sector_radar` source 测试模式,定位实现“2 个 worker 共享全局 0.2 秒请求启动间隔”的最小适配点。
- Scope: internal
- Date: 2026-08-31
## Findings
### 结论与最小适配面
最小且边界清晰的实现是扩展共享的 `RequestCoordinator`,让它可选地协调“请求启动槽”,然后仅让 `TushareSectorRadarAdapter` 启用现有的 `request_interval_seconds=0.2`。两路 `moneyflow_dc` 补拉 worker 共享同一个 adapter,而该 adapter 已经只持有一个 `_coordinator`,因此不需要新建进程级 singleton,也不需要新增环境变量。
具体适配点如下。
1. 在 `zhixing-server/src/zhixing_server/shared/request_coordinator.py:35-66` 的 `RequestCoordinator` 增加默认关闭的启动间隔参数及 `_next_request_at` 状态;继续复用现有 `threading.Condition`,在同一临界区内读取单调时钟、计算 `max(_cooldown_until, _next_request_at)`、等待并预约下一启动时刻。只有“预约”需要持锁,真实 provider 请求必须在锁外执行,才能保持两个 worker 的请求重叠能力。
2. 在 `zhixing-server/src/zhixing_server/shared/request_coordinator.py:75-82` 的每次 attempt 开始前,把当前只等待 cooldown 的 `_wait_for_cooldown` 收敛成“等待 cooldown 并原子预约启动槽”。预约完成时令 `_next_request_at = actual_start + interval`。这样初次请求和 retry 都服从同一个启动间隔;当 403/429 创建 cooldown 后,等待中的 worker 还会在醒来时重新检查 cooldown。
3. 新参数必须默认 `0.0`。`RequestCoordinator` 还被 market-data 使用,而且它的现有契约明确是“普通请求不串行,只共享命中限流后的 cooldown”(`zhixing-server/src/zhixing_server/shared/request_coordinator.py:35-41`;`docs/market-data-sync.md:65-69`)。默认关闭可避免顺带改变八路行情同步语义。
4. 在 `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:92-116` 构造默认 coordinator 时,把已经存在的 `request_interval_seconds` 传入协调器;删除或停用 `_fetch_snapshot` 成功返回后的逐线程休眠(`zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:368-389`)。当前休眠发生在请求完成后,两个线程可以同时启动,不能表达“全局请求启动间隔”。
5. `request_interval_seconds` 的配置链已经完整:`Settings.sector_radar_request_interval_seconds` 默认 0.2(`zhixing-server/src/zhixing_server/bootstrap/config.py:27-31`),CLI 将它传给 `TushareSectorRadarAdapter.from_token`(`zhixing-server/src/zhixing_server/modules/sector_radar/presentation/cli.py:62-67`)。因此不需要改 `.env`、Compose 或配置模型。
这里的“全局”只能可靠地解释为“同一 adapter/coordinator 实例覆盖的两个 worker”。现有 coordinator 不是模块 singleton,也不能跨进程协调;market-data job、sector-radar job 或两个独立进程各自创建 coordinator。若需求是全系统或跨进程的 5 requests/s,则本方案不满足,需要外部/分布式限流器,这会明显扩大范围。
### 两个 worker 的落点
`moneyflow_dc` 的当前全市场入口完全串行,只请求一次并校验结果(`zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:318-330`)。补拉属于 Tushare 响应分区细节,最小落点是该 infrastructure adapter 内部:先保留全市场快照,再对缺失的当前上市股票代码使用 `ThreadPoolExecutor(max_workers=2)` 发起按 `ts_code` 分区请求。worker 共享 `self._coordinator`,所以每一个 `_fetch_snapshot` 最终都经过同一个启动槽。
仓库已有 worker 写法可复用:`SyncMarketData` 在 `zhixing-server/src/zhixing_server/modules/market_data/application/sync.py:342-361` 使用具名的 `ThreadPoolExecutor` 和 future-to-business-key 映射;并发测试用带 `threading.Lock` 的 fake 统计 active/max-active(`zhixing-server/tests/unit/market_data/test_sync_concurrency.py:22-59`),并断言两路上限(`zhixing-server/tests/unit/market_data/test_sync_concurrency.py:156-180`)。sector-radar 不宜照搬其数据库副作用模型,只应复用“有界 executor + 主线程汇总”的形状。
如果补拉需要由当前上市股票池驱动,应用层已经先得到 `stock_basics`、后取 `moneyflow`(`zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:536-563`)。最小跨层契约是从 `stock_basics.rows` 中取 `list_status == "L"` 的代码并传给 `fetch_moneyflow_dc`;这会同步影响 `SectorRadarSource.fetch_moneyflow_dc`(`zhixing-server/src/zhixing_server/modules/sector_radar/domain/ports.py:24-47`)和测试 fake。不要让 adapter 缓存上一次 `fetch_stock_basics` 的结果,否则 retry/replay 和调用顺序会形成隐式状态。
worker 完成顺序不得直接决定 snapshot 顺序。publication checkpoint 会按 `result.snapshots` 的枚举顺序持久化(`zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:486-495`),重放又要求 `source_order` 从 0 连续并按序恢复(`zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py:433-455`)。因此 future 结果应按输入股票代码或明确排序后汇总;虽然单个 snapshot 内部的 hash 已对 rows 做顺序稳定化(`zhixing-server/src/zhixing_server/modules/sector_radar/domain/source.py:134-163`),snapshot 元组自身仍需稳定。
固定“两路”不要求新增 `Settings`。最小做法是在 adapter 内使用命名常量或默认值为 2 的构造参数;只有产品要求运行时可调时,才需要扩展 config、CLI、`.env.example` 和 Compose。仓库现有可调 worker 的完整链路可参考 `Settings.market_data_max_workers`(`zhixing-server/src/zhixing_server/bootstrap/config.py:20-25`)和 `SyncMarketData(max_workers=...)`(`zhixing-server/src/zhixing_server/modules/market_data/application/sync.py:131-143`)。
### 现有限流与线程安全证据
共享协调器已经用 `threading.Condition` 保护 `_cooldown_until` 和 `_rate_limit_count`(`zhixing-server/src/zhixing_server/shared/request_coordinator.py:43-66`),读取、创建 cooldown 和成功后清理也都在该条件锁内(同文件 `:68-73`、`:127-152`)。403、429 和稳定中英文提示的分类位于同文件 `:13-28`、`:154-163`,cooldown 阶梯为 60/120/180 秒(`:13`)。这正是承载全 worker 启动槽的现有线程安全原语。
当前 `call` 明确允许普通请求并发(`zhixing-server/src/zhixing_server/shared/request_coordinator.py:35-41`),而 `_wait_for_cooldown` 在锁外调用 `wait_fn`(`:127-138`),不会把 provider 调用包在全局锁中。新增启动间隔也应保持这一点;如果把整次 `client.query` 放进锁中,虽然间隔成立,但会把两个 worker 退化为串行请求。
`TushareSectorRadarAdapter` 对同一个 SDK client 调用 `client.query` 或接口方法(`zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py:378-385`)。仓库内没有 Tushare SDK client 线程安全保证,也没有为 `_client` 加锁。锁定版本是 Tushare 1.4.29(`zhixing-server/uv.lock:731-739`)。因此两路并发是否可共享同一 SDK client 是实现前仍需确认的风险;启动间隔只保护频率状态,不等于保证 SDK client 内部线程安全。若无法确认,选择独立 client 会需要 token/factory 生命周期改造,选择锁住整个 client 请求则无法获得网络调用并发收益。
### 测试模式与建议入口
仓库没有 `tests/**/conftest.py` 或 sector-radar pytest fixture。`zhixing-server/tests/unit/sector_radar/test_tushare_source.py:23-44` 采用文件内 `QueryClient` 和 `make_adapter`:响应按 `(api_name, ts_code/list_status)` 分区,adapter 关闭 retry 和真实 sleep,并固定 `now_fn`。`dc_member` 达上限后按分区补拉的测试(同文件 `:216-274`)是 moneyflow 缺失补拉最接近的现有测试模板;当前上市状态查询测试在 `:308-333`。
启动间隔的直接先例是 `zhixing-server/tests/unit/market_data/test_tushare.py:53-87`:用可注入 fake monotonic clock 和 wait 函数验证一个请求触发的 cooldown 会阻塞后续请求。新增测试应延续 fake clock,而不是用真实 `sleep(0.2)` 和宽松 wall-clock 断言,以避免并发测试抖动。
建议最少覆盖两层行为:
- 在共享 coordinator 的单元测试中让两个线程共享一个 coordinator,用 `threading.Event` 保持首个 request 未完成,fake clock/wait 将第二个启动推进到 0.2;记录两个真实 request callback 的开始时刻并断言差值为 0.2。该形状能证明“请求可重叠,但启动槽不重叠”,也能避免单纯顺序调用掩盖线程竞态。
- 在 `test_tushare_source.py` 增加 moneyflow 缺失回补测试:全市场响应遗漏若干 `list_status=L` 代码,按 `ts_code` 的 fake 分区返回补拉结果,断言 executor 最大 active 不超过 2、最终 rows 和 snapshots 顺序稳定、非上市状态不补拉。并发 fake 的 `calls`、`active` 和 `max_active` 必须用 `threading.Lock`;现有 `QueryClient.calls.append`(`:23-34`)只适合串行测试。
现有测试入口为:
```bash
cd zhixing-server
uv run pytest tests/unit/market_data/test_tushare.py
uv run pytest tests/unit/sector_radar/test_tushare_source.py
uv run pytest tests/unit/sector_radar/test_build.py
uv run pytest tests/unit/sector_radar/test_cli.py
```
共享 coordinator 改动至少应运行前两个入口;若 `fetch_moneyflow_dc` 端口增加上市代码参数,还必须运行后两个入口以覆盖 `FakeRadarSource`、应用编排和 CLI 组合。完整后端门禁由 `.trellis/spec/backend/quality-guidelines.md:3-20` 和 `zhixing-server/pyproject.toml:40-49` 定义,包括 Ruff format/lint、Pyright strict 和完整 pytest。
### Files found
- `zhixing-server/src/zhixing_server/shared/request_coordinator.py`:跨 bounded context 的 retry、限流识别和共享 cooldown 协调器,是全局启动槽的最小所有权位置。
- `zhixing-server/src/zhixing_server/modules/sector_radar/infrastructure/tushare.py`:sector-radar Tushare adapter、source 分区与当前逐请求休眠位置。
- `zhixing-server/src/zhixing_server/modules/sector_radar/application/build.py`:stock basics 到 moneyflow 的调用顺序、source checkpoint 稳定顺序契约。
- `zhixing-server/src/zhixing_server/modules/sector_radar/domain/ports.py`:`fetch_moneyflow_dc` 的应用端口签名。
- `zhixing-server/src/zhixing_server/modules/market_data/application/sync.py`:仓库现有有界 `ThreadPoolExecutor` 模式。
- `zhixing-server/tests/unit/market_data/test_tushare.py`:fake clock/wait 的 coordinator 测试模式。
- `zhixing-server/tests/unit/market_data/test_sync_concurrency.py`:两路 worker 上限与加锁 fake 的测试模式。
- `zhixing-server/tests/unit/sector_radar/test_tushare_source.py`:source fake、分区响应、禁用真实 sleep 及 schema/limit 测试入口。
- `zhixing-server/tests/unit/sector_radar/test_build.py`:应用端口 fake 和 source-group replay/retry 覆盖。
- `zhixing-server/src/zhixing_server/bootstrap/config.py`、`zhixing-server/src/zhixing_server/modules/sector_radar/presentation/cli.py`:现有 0.2 秒配置传递链。
### Related specs
- `.trellis/spec/backend/directory-structure.md`:无业务归属的小型跨上下文能力应放在 `shared/`;Tushare 请求启动协调符合这一边界。
- `.trellis/spec/backend/market-data-sync.md`:Tushare client、共享限流和后端测试门禁的既有契约。
- `.trellis/spec/backend/configuration-and-runtime.md`:运行时配置只能通过 `Settings` 注入;本最小方案复用既有配置,不新增环境读取。
- `.trellis/spec/backend/quality-guidelines.md`:Pyright strict、pytest 严格模式和后端质量命令。
- `.trellis/spec/guides/code-reuse-thinking-guide.md`:跨上下文且无业务所有权的原语才进入 `shared/`,并要求复用前先核对生命周期与错误语义。
## Caveats / Not Found
- 未在仓库中找到 Tushare 1.4.29 对 `pro_api` client 的线程安全声明;不能仅凭 Python 对 `list.append` 或对象读取的实现细节宣称 SDK client 可安全并发。
- 未找到 sector-radar 专用 `conftest.py`、pytest fixture 或现成的 request-start 间隔测试;需要沿用文件内 fake 和 coordinator fake clock 模式。
- 当前 PRD 仍为 TBD,未定义“全局”是否跨 adapter/进程,也未定义单只股票补拉失败是整组失败还是保留部分回补。以上结论按“一个 sector-radar adapter 内两路 worker、任一补拉失败则 source group 失败”的最小解释给出。
- 本次只读研究未运行 pytest;研究代理只写入本文件,未修改产品代码或测试代码。
@@ -0,0 +1,26 @@
{
"id": "sector-radar-listed-moneyflow-recovery",
"name": "sector-radar-listed-moneyflow-recovery",
"title": "资金雷达当前上市股票池与资金流缺口补拉",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P1",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-31",
"completedAt": "2026-08-31",
"branch": null,
"base_branch": "develop",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
+5 -3
View File
@@ -8,8 +8,8 @@
<!-- @@@auto:current-status -->
- **Active File**: `journal-1.md`
- **Total Sessions**: 9
- **Last Active**: 2026-08-12
- **Total Sessions**: 11
- **Last Active**: 2026-08-31
<!-- @@@/auto:current-status -->
---
@@ -19,7 +19,7 @@
<!-- @@@auto:active-documents -->
| File | Lines | Status |
|------|-------|--------|
| `journal-1.md` | ~243 | Active |
| `journal-1.md` | ~291 | Active |
<!-- @@@/auto:active-documents -->
---
@@ -29,6 +29,8 @@
<!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------|
| 11 | 2026-08-31 | 资金雷达当前上市股票资金流补拉 | `2ffd016` | `codex/sector-radar-listed-moneyflow-recovery` |
| 10 | 2026-08-29 | 完成板块资金雷达 Tushare 独立生产 MVP | `3789008`, `284c480`, `d9bae72`, `efc4c3d`, `8e96e64`, `23493fa`, `2fd16e5` | `codex/zijin` |
| 9 | 2026-08-12 | 完成选股执行性能优化 | `8963c06` | `develop` |
| 8 | 2026-08-11 | 完成市场数据同步与完整性检查 | `7ce1154`, `8f5f504` | `develop` |
| 7 | 2026-08-10 | 完成选股执行状态抽屉与紧凑布局 | `17237e0` | `develop` |
+48
View File
@@ -241,3 +241,51 @@
### Status
[OK] **Completed**
## Session 10: 完成板块资金雷达 Tushare 独立生产 MVP
**Date**: 2026-08-29
**Task**: 完成板块资金雷达 Tushare 独立生产 MVP
**Branch**: `codex/zijin`
### Summary
完成版本化独立指标、Tushare point-in-time 输入、可恢复构建 Job、last-good 查询 API 和前端排名工作台;补齐 membership_unknown、快照 identity、跨层百分位与完整验证契约。
### Git Commits
| Hash | Message |
|------|---------|
| `3789008` | (see git log) |
| `284c480` | (see git log) |
| `d9bae72` | (see git log) |
| `efc4c3d` | (see git log) |
| `8e96e64` | (see git log) |
| `23493fa` | (see git log) |
| `2fd16e5` | (see git log) |
### Status
[OK] **Completed**
## Session 11: 资金雷达当前上市股票资金流补拉
**Date**: 2026-08-31
**Task**: 资金雷达当前上市股票资金流补拉
**Branch**: `codex/sector-radar-listed-moneyflow-recovery`
### Summary
资金雷达统一使用构建时当前 L 股票池,moneyflow_dc 达到单次上限后按候选缺口使用两路共享限流 worker 补拉;补充 checkpoint 重试兼容、长期 Tushare 股票范围规范及完整测试。
### Git Commits
| Hash | Message |
|------|---------|
| `2ffd016` | (see git log) |
### Status
[OK] **Completed**