chore(task): 归档 08-10 与 08-11 任务

This commit is contained in:
yuxuanhui
2026-08-11 13:32:21 +08:00
parent 8f5f504368
commit dd04933d63
33 changed files with 788 additions and 5 deletions
@@ -0,0 +1,5 @@
{"file":".trellis/spec/frontend/index.md","reason":"检查前端改动是否仍遵守 feature/shared 边界、路由和同源 API 约束。"}
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"检查 Select、DatePicker、抽屉入口、语义控件和 Tailwind 组件组合。"}
{"file":".trellis/spec/frontend/state-management.md","reason":"检查日期、URL search、抽屉局部状态与服务器结果的所有权。"}
{"file":".trellis/spec/frontend/type-safety.md","reason":"检查 Base UI/DayPicker props、日期转换和 SelectionResults 类型边界。"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"执行最终格式、lint、类型、测试、构建和审查门禁。"}
@@ -0,0 +1,113 @@
# 选股结果页筛选与状态入口技术设计
## 1. Boundaries
本任务只修改 `zhixing-web` 前端:
- `shared/ui` 提供基于 `@base-ui/react` 的通用 `Select`、`Popover`、`Calendar`、`DatePicker` 组件。
- `features/selection` 负责选项、URL search 更新、目标日期字符串转换、结果状态和抽屉入口布局。
- `SelectionResultsPage` 继续拥有服务器查询、执行/重执行状态和抽屉开关;`SelectionResultsWorkbench` 负责信号列表顶部的入口和筛选区。
- `ExecutionStatusDrawer` 负责执行摘要和未完成评估的按需展示,不请求数据、不修改 URL。
不改变后端 API、React Query query key、路由路径、`SelectionResults` 类型契约和策略执行流程。
## 2. Component Shape
```text
SelectionResultsPage
├─ PageLayout
│ ├─ actions: ExecutionToolbar
│ │ ├─ DatePicker
│ │ ├─ Select (strategy)
│ │ └─ Button (execute / rerun)
│ └─ children: result states
│ └─ SelectionResultsWorkbench
│ ├─ signal-list header
│ │ ├─ Input (search)
│ │ ├─ Select (category)
│ │ └─ Button (execution status)
│ ├─ SignalTable / SignalRecordList
│ └─ Pagination
└─ ExecutionStatusDrawer
├─ execution summary metrics
└─ IncompleteEvaluationTable
```
移除页面级 `ResultMetrics`,避免 `PageLayout.metrics` 形成中间常驻区域。执行状态按钮移动到 `SelectionResultsWorkbench` 的列表 header,页面通过 props 传入 `onOpenExecutionStatus`、`drawerOpen` 和 `triggerRef`,不把开关状态放入 Zustand。
## 3. Shared UI Design
### Select
`shared/ui/select.tsx` 对 `@base-ui/react/select` 的 Root、Trigger、Value、Portal、Positioner、Popup、List、Item、ItemIndicator 和滚动箭头做样式封装。对外保留 Base UI props、`className` 和 `data-slot`,提供可组合的 `SelectGroup`、`SelectLabel`、`SelectSeparator` 等导出。Popup 使用 portal 和 positioner,确保列表不受页面 bounded workspace 的 `overflow` 影响;`data-highlighted`、`data-selected`、`data-disabled` 和 open/closed 状态提供明确反馈。
SelectContent 对齐官方 Base UI 的默认交互:Positioner 使用 `side="bottom"`、`align="center"` 和 `alignItemWithTrigger`,Popup 使用 `w-(--anchor-width)` 跟随触发器宽度,并保留 `align`、`side`、offset 等覆盖参数。这样鼠标打开时选中项会与触发器当前值对齐,键盘打开时仍能保持常规焦点顺序。
### Popover / Calendar / DatePicker
`DatePicker` 按官方 shadcn/ui Base UI Date Picker 的组合方式实现:`Popover` 负责定位和关闭行为,`Calendar` 负责单日选择。`Calendar` 基于 `react-day-picker`,使用项目 `Button`、`cn` 和现有主题 token;`DatePicker` 对外以通用 `Date | undefined` 传值,业务 feature 在边界处把它转换为 `YYYY-MM-DD`,避免 shared 层理解交易日概念。
触发器采用官方示例的文本值、右侧下拉指示和 `justify-between` 布局,弹层使用 `w-auto` 并从触发器起始边缘对齐。Calendar 使用 `react-day-picker` 的默认 class map、`--cell-size` 和自定义 DayButton,保持日期网格、月份导航、选中态和键盘焦点与官方 Base UI 示例接近,只替换为项目主题 token。
补充 `date-fns` 与 `react-day-picker` 为显式前端依赖。日期转换使用本地年月日构造/格式化,避免直接解析 ISO 日期造成时区前移或后移。
## 4. Data Flow and State
```text
DatePicker<Date>
└─ SelectionResultsPage: Date ↔ YYYY-MM-DD
├─ useSelectionResults(strategy, targetTradeDate, resultQuery)
└─ useTriggerSelectionRun({ target_trade_date })
SelectionResultsPage
├─ resultQuery ← route search (page/pageSize/search/category)
├─ SelectionResultsWorkbench
│ ├─ Select(category) → navigate(search: { category, page: 1 })
│ └─ status button → setExecutionStatusDrawerOpen(true)
└─ ExecutionStatusDrawer(result)
├─ result summary metrics
└─ result.failures
```
### Components preview route
```text
AppLayout
├─ primaryNavigation → /components
├─ mobilePrimaryNavigation → /components
└─ ComponentsPreviewPage
├─ Button / Badge
├─ Input / Select
├─ DatePicker / Calendar
├─ Popover
└─ Separator / Skeleton
```
预览页只持有演示状态,不触碰业务 query 或 URL search;它作为 shared/ui 的人工回归入口,也作为官方示例布局对照页。
目标日期继续是页面局部状态;搜索、分类和分页继续是 TanStack Router search 状态。Select 不直接操作 URL,feature handler 负责转换和分页重置。抽屉只接收当前 `displayedResult`,切换日期或开始执行时关闭并清理已有局部执行状态。
## 5. Layout and Accessibility
- `PageLayout.metrics` 置空后,`PageLayout` 的 bounded workspace 只承载执行控件和结果区;结果列表继续沿用现有高度链和内部滚动边界。
- Workbench header 在桌面端把搜索、分类和结果数量/执行状态按钮放在同一行,在移动端自然换行;所有主要按钮保留至少 `44px` 的移动触控高度。
- 执行状态按钮使用真实 `Button`,提供 `aria-controls`、`aria-expanded`、`aria-haspopup="dialog"` 和包含状态/失败数的可访问名称。抽屉继续由 Dialog 管理焦点、Escape 和遮罩。
- Select Trigger 使用真实 button;DatePicker Trigger 使用 Button,并提供 label/placeholder 和 `aria-label`。日历日期按钮使用 `react-day-picker` 的语义和键盘导航。
## 6. Compatibility and Rollback
- API 和路由协议不变,旧结果数据可直接驱动新布局;若新 shared primitive 样式有问题,可只回退控件接线而保留抽屉数据流。
- 日期依赖只影响前端依赖和 bundle;若日期选择器有兼容问题,保留 `DatePicker` 对外接口并回退其内部实现,不改变页面与 API 的字符串边界。
- 抽屉摘要与入口位置的回滚点分别位于 `execution-status-drawer.tsx` 和 `selection-results-workbench.tsx`,不会影响 query 或策略执行。
## 7. Verification Strategy
- 页面测试:无常驻 metrics、DatePicker 选择日期、strategy/category Select 更新、状态入口出现、抽屉摘要/失败表格/空状态、关闭焦点恢复。
- shared UI 测试或页面行为测试:Select 的可见选项和键盘语义、DatePicker 的按钮和选中日期可见性。
- 质量门禁:`pnpm format:check`、`pnpm lint`、`pnpm typecheck`、`pnpm test`、`pnpm build`。
- 浏览器 mock:检查 1440×900、390×844 下筛选区域、信号列表、DatePicker/Select popup 和右侧抽屉不产生页面级水平溢出。
## 8. References
- shadcn/ui Base Select: https://ui.shadcn.com/docs/components/base/select
- shadcn/ui Base Date Picker: https://ui.shadcn.com/docs/components/base/date-picker
@@ -0,0 +1,5 @@
{"file":".trellis/spec/frontend/index.md","reason":"前端 feature/shared 边界、现有质量入口和 API/路由约束。"}
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"Select、DatePicker、Drawer、组合组件、Tailwind token 和可访问性交互规范。"}
{"file":".trellis/spec/frontend/state-management.md","reason":"保留 URL search、页面局部日期/抽屉状态和 TanStack Query 的状态边界。"}
{"file":".trellis/spec/frontend/type-safety.md","reason":"shared primitive props、日期值转换和现有 SelectionResults 类型的严格 TypeScript 约束。"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"页面行为测试、可访问性审查和格式/lint/typecheck/test/build 质量门禁。"}
@@ -0,0 +1,84 @@
# 选股结果页筛选与状态入口执行计划
## Scope
只修改 `zhixing-web` 前端和本任务规划/验证产物;不修改后端接口、数据库、策略计算或现有路由协议。
## Ordered Checklist
### 1. 开发前上下文
- [x] 运行 `task.py start` 将任务切换为 `in_progress`。
- [x] 读取 `prd.md`、`design.md`、前端规格及 shared 复用指南。
- [x] 确认工作区没有用户现有改动,记录本任务实际改动文件。
### 2. Shared UI 组件
- [x] 新增 `shared/ui/select.tsx`,封装 Base UI Select 的触发器、列表、选项、滚动和状态样式。
- [x] 新增 `shared/ui/popover.tsx`,封装 Base UI Popover 的 trigger、portal、positioner、popup 和关闭语义。
- [x] 新增 `shared/ui/calendar.tsx`,基于 `react-day-picker` 和现有 Button/cn/token 提供单日选择基础能力。
- [x] 新增 `shared/ui/date-picker.tsx`,组合 Popover + Calendar,支持受控 `Date | undefined`、禁用态、placeholder、label 语义和窄屏布局。
- [x] 按实际依赖更新 `package.json` / `pnpm-lock.yaml`,只加入 Date Picker 所需的 `date-fns` / `react-day-picker`。
### 3. 选股页面接线
- [x] 将 `ExecutionToolbar` 的目标日期 Input 替换为 DatePicker,保持 `YYYY-MM-DD` 字符串边界和现有执行状态清理逻辑。
- [x] 将策略原生 select 替换为 shared Select;将 `SelectionResultsWorkbench` 的分类原生 select 替换为 shared Select,保留 URL 更新和分页归一化。
- [x] 从 `PageLayout` 移除 `metrics={resultMetrics}` 和页面级常驻 `ResultMetrics`。
- [x] 将执行摘要指标放入 `ExecutionStatusDrawer`,并把执行状态按钮接到 `SelectionResultsWorkbench` 信号列表顶部;失败和无信号结果保留按需入口。
- [x] 保留既有失败表格、空状态、主从信号选择、移动展开、分页、重执行确认和查询状态分支。
### 3.1 Shared UI 预览入口
- [x] 新增 `/components` 组件预览路由和主导航入口,桌面侧边栏与移动底部导航均可访问。
- [x] 预览 Button、Badge、Input、Select、DatePicker、Calendar、Popover、Separator 和 Skeleton,并覆盖选择、弹层、禁用和加载状态。
- [x] 将 DatePicker/Calendar 的触发器、固定宽度和日历网格调整到接近官方 Base UI 示例的布局与交互。
- [x] 将 SelectContent 的锚点宽度、Positioner 对齐和选中项对齐行为调整到接近官方 Base UI 示例。
### 4. 测试与验证
- [x] 更新页面测试,覆盖 DatePicker 选中/清空日期、两个 Select 的可见值/回调、无常驻指标区、顶部状态按钮、抽屉摘要和焦点恢复。
- [x] 通过页面行为覆盖 shared UI 的关键交互(Select 键盘/选择、DatePicker 弹层/选择),避免复制 shared primitive 实现逻辑新增重复测试。
- [ ] 运行 `pnpm format:check`、`pnpm lint`、`pnpm typecheck`、`pnpm test`、`pnpm build`;其中完整格式检查仍被未改动的既有 `DESIGN.md` 阻断,其余门禁通过。
- [x] 使用浏览器 mock 验证桌面/移动视口、Select/DatePicker popup、抽屉、Escape/焦点恢复和页面级水平溢出。
- [x] 执行 `trellis-check`,修复质量门禁或规格偏差后重新验证。
## Validation Commands
在 `zhixing-web/` 下运行:
```bash
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm build
```
## Verification Record
- `pnpm lint`:通过。
- `pnpm typecheck`:通过。
- `pnpm test`:通过,5 个测试文件、30 项测试。
- `pnpm build`:通过;Vite 仅提示现有单 chunk 超过 500 kB。
- 变更文件 `prettier --check`:通过;完整 `pnpm format:check` 仅因未改动的 `zhixing-web/DESIGN.md` 失败。
- `git diff --check`:通过。
- 浏览器 mock:桌面与 390×844 移动视口均验证;Select 点击/键盘选择、DatePicker 弹层与中文无障碍 labels、抽屉 Escape/焦点恢复、页面级水平溢出均通过。
- `trellis-check`:通过,并修复 DatePicker 清空后日期被服务器结果回填的边界问题。
- 本轮浏览器验证:组件预览页桌面/移动截图通过;DatePicker 桌面触发器宽 212px、弹层 `w-auto` 且无页面级水平溢出;选股页桌面目标日期和策略控件均为 212px,移动端控件随容器铺满且无水平溢出。
- 本轮 Select 对齐检查:补齐 `alignItemWithTrigger`、`w-(--anchor-width)`、可用高度滚动和 offset 类型;桌面端弹层宽度跟随触发器,移动端弹层未产生水平溢出。
## Risky Files and Rollback Points
- `src/shared/ui/select.tsx`、`popover.tsx`、`calendar.tsx`、`date-picker.tsx`:新 shared primitive 和 Base UI/DayPicker API 兼容点;可独立回退并保留页面旧控件。
- `package.json`、`pnpm-lock.yaml`:日期依赖变更;若安装/构建不兼容,回退依赖和四个 shared primitive。
- `src/features/selection/pages/selection-results-page.tsx`:移除 metrics、接入日期选择器和抽屉状态;回退点不影响 API。
- `src/features/selection/components/selection-results-workbench.tsx`:顶部筛选/状态入口和 Select 接线;回退时保留结果表、分页和详情面板。
- `src/features/selection/components/execution-status-drawer.tsx`:执行摘要迁移;失败表格和 Dialog 生命周期应保持可回滚。
## Review Gates Before Start
- [ ] `prd.md` 已通过需求收敛:目标、范围、验收标准明确且无阻塞问题。
- [ ] `design.md` 已确认 shared/feature 边界、日期字符串转换、抽屉数据流和回滚点。
- [ ] `implement.jsonl` 与 `check.jsonl` 已填入真实前端规格上下文。
- [ ] 已向用户展示最终规划摘要,并获得对该摘要的明确实施批准。
@@ -0,0 +1,65 @@
# 迭代选股结果页筛选与状态入口
## Goal
让选股结果页把空间集中给筛选和信号结果:移除常驻的执行状态总览,将执行摘要改为信号列表顶部的按需入口,并通过右侧抽屉查看完整信息。同时把选股页使用的原生 `select` 和日期输入升级为项目 `shared/ui` 中基于 shadcn/ui Base UI 组合的可访问组件。
## Confirmed Facts
- [SelectionResultsPage](zhixing-web/src/features/selection/pages/selection-results-page.tsx) 当前通过 `PageLayout.metrics` 渲染常驻的 `ResultMetrics`,展示目标股票数、实际参与数、命中股票数、信号条数、数据覆盖率和执行状态(约第 126–136、305–355 行)。
- 当前目标交易日控件仍是 `Input type="date"`,策略控件和信号分类筛选仍是原生 `<select>`(约第 247–274 行,以及 [SelectionResultsWorkbench](zhixing-web/src/features/selection/components/selection-results-workbench.tsx) 约第 95–120 行)。
- 项目已有基于 `@base-ui/react` 的本地 `Button`、`Dialog`、`Input` 等封装,但尚无可复用的 `Select`、`Popover`、`Calendar` 或 `DatePicker` shared primitive。
- `SelectionResultsWorkbench` 已经拥有 URL 驱动的搜索词、分类、分页状态;`SelectionResults` 已包含执行摘要和 `failures`,本轮不需要改变 API、query key 或后端契约。
- 上一轮已经实现右侧 `ExecutionStatusDrawer`,当前抽屉仍由页面状态控制并使用执行状态入口作为焦点恢复目标;本轮沿用这条数据流,只调整入口位置并补充摘要呈现。
## Requirements
### R1. 筛选与执行控件
- 目标交易日使用 `shared/ui/date-picker`,以单日选择器呈现;业务层继续以 `YYYY-MM-DD` 字符串调用现有查询和执行接口。
- 策略选择和信号分类筛选使用 `shared/ui/select`,不再在选股页面直接渲染原生 `<select>`。
- `Select`、`DatePicker` 必须支持键盘操作、焦点可见、Escape/外部点击关闭、禁用态和窄屏不溢出;控件的业务选项和路由更新仍由 `selection` feature 持有。
- 保留现有目标交易日、策略、执行/重执行、搜索词、分类筛选和分页行为;不引入全局 UI 状态。
### R2. 移除常驻执行状态总览
- 选股结果页不再通过 `PageLayout.metrics` 渲染常驻执行状态总览,主内容区域不再为这组指标预留高度。
- 当存在可展示结果时,信号列表顶部提供一个明确的“执行状态”按钮,展示当前状态和未完成评估数量,并作为右侧抽屉的触发器。
- 抽屉继续支持关闭按钮、遮罩、Escape 和关闭后焦点返回;移动端不能产生页面级水平溢出。
- 执行摘要(目标股票数、实际参与数、命中股票数、信号条数、数据覆盖率、状态)和未完成评估表格迁移到抽屉内;无失败记录时保留可读空状态。
### R3. Shared UI 沉淀
- 在 `zhixing-web/src/shared/ui/` 新增可复用的 `Select`、`Popover`、`Calendar` 和 `DatePicker` 组合组件,底层复用现有 `@base-ui/react`、`Button`、`cn` 以及日期选择依赖。
- shared UI 只负责通用交互、语义、样式和原生属性扩展,不读取 `selection` API、路由或业务类型。
- 新组件遵循项目现有的具名导出、小写文件名、Tailwind token、`cn` 合并和严格 TypeScript 约束。
- DatePicker 的触发器、弹层宽度、日历网格密度和焦点交互应尽量贴近 shadcn/ui Base UI 官方示例;保留项目当前主题色和中文日期语义。
- Select 的弹层应遵循官方 Base UI 的锚点宽度和选中项对齐行为,避免菜单以内容最小宽度或普通居中下拉方式偏离触发器。
### R4. Shared UI 预览入口
- 增加一个可从主导航进入的“组件预览”页面,展示当前 shared/ui 的主要组件和关键状态。
- 预览页至少覆盖 Button、Badge、Input、Select、DatePicker、Calendar、Popover、Separator 和 Skeleton,并提供真实的选择、打开/关闭、禁用和加载状态交互。
- 组件预览页不依赖业务 API,不改变现有业务页面的数据流;桌面和移动端均可通过菜单访问。
## Out of Scope
- 不修改后端接口、数据库、策略计算、`SelectionResults` 字段含义或 query key。
- 不改变 `/selection` URL 结构和现有搜索词、分类、分页参数的语义;目标交易日仍保持当前页面局部状态边界。
- 不建设独立 npm/package 级 UI 库,不把 selection 业务组件迁移到 `shared/ui`,也不在本轮扩展全局表单校验或日期范围选择。
- 不重做已有结果表、移动端信号卡片、详情面板和执行抽屉的业务数据逻辑。
## Acceptance Criteria
- [x] `SelectionResultsPage` 不再给 `PageLayout.metrics` 传入常驻执行状态总览;桌面和移动页面均能把剩余空间用于结果内容。
- [x] 目标交易日使用可访问的 `DatePicker`;选中日期后仍以 `YYYY-MM-DD` 触发当前结果查询/执行逻辑,清空和禁用行为不破坏既有状态机。
- [x] 策略和信号分类使用 shared `Select`;键盘打开、上下选择、Enter 确认和 Escape 关闭均可用,分类改变仍更新既有 URL search 并重置分页到第 1 页。
- [x] 有结果时,信号列表顶部可见执行状态按钮;点击后右侧抽屉展示摘要指标和未完成评估表格,关闭后焦点回到该按钮;失败和无信号结果也保留按需入口。
- [x] 无失败记录时抽屉仍可打开并显示“暂无未完成评估”;主页面不再出现独立的常驻指标区或内联失败明细卡片。
- [x] shared UI 组件不依赖 `features/selection`,没有直接业务分支;组件和页面测试覆盖关键可见交互。
- [x] 主导航新增“组件预览”入口,页面集中展示 shared/ui 组件和关键交互状态,桌面与移动端均可访问。
- [ ] `pnpm format:check`、`pnpm lint`、`pnpm typecheck`、`pnpm test` 和 `pnpm build` 通过;lint/typecheck/test/build 与变更文件格式检查通过,完整格式检查仍被未改动的 `DESIGN.md` 阻断;浏览器验证已覆盖桌面/移动视口、Select、DatePicker、抽屉和页面级水平溢出。
## Open Questions
无。按用户要求将原总览信息整体移入右侧抽屉,保留现有后端数据流和 URL 筛选边界。
@@ -0,0 +1,26 @@
{
"id": "selection-results-filter-state-ui",
"name": "selection-results-filter-state-ui",
"title": "迭代选股结果页筛选与状态入口",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-10",
"completedAt": "2026-08-11",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,6 @@
{"file":".trellis/spec/backend/market-data-sync.md","reason":"检查并发、完整性审计和现有同步契约的集成一致性。"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"检查后端 lint、类型、测试、迁移和集成证据。"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"检查前端格式、lint、类型、测试和构建证据。"}
{"file":"docs/adr/0003-postgresql-as-market-data-store.md","reason":"检查完整性报告没有改变 PostgreSQL 事实源地位。"}
{"file":"docs/adr/0004-tushare-six-year-snapshot-sync.md","reason":"检查并发仍保持事务/CSV 发布、失败隔离和滚动边界。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/prd.md","reason":"执行父任务最终 acceptance criteria 对照。"}
@@ -0,0 +1,160 @@
# 市场数据同步性能与完整性检查设计
## 1. 目标与边界
本设计保留现有“逐股获取六年 qfq 完整快照”的业务口径,通过 8 路受控并发和 PostgreSQL
连接/写入批量化缩短日常同步。另提供不访问 Tushare、不修改市场事实的手动完整性检查。
不引入按交易日增量下载、自动完整检查、自动修复或通用任务队列。选股仍只消费已完成且
覆盖率达标的市场同步批次。
## 2. 任务拆分与依赖
父任务只负责需求、跨子任务契约和最终集成,不直接实现产品代码。
1. `08-11-market-sync-concurrency-storage`
- 交付 8 worker、共享频控、连接池、批量 item 写入和集合覆盖率。
- 无子任务依赖,必须最先完成。
2. `08-11-market-integrity-check-api`
- 依赖第 1 项提供的池化 PostgreSQL 基础设施和 advisory lock 语义。
- 交付检查领域模型、迁移、只读比较、HTTP 触发和轮询。
3. `08-11-market-integrity-check-web`
- 依赖第 2 项冻结 HTTP 字段和状态值。
- 交付 `/sync` 页面、导航、触发、轮询和报告展示。
## 3. 同步执行模块
### 3.1 外部接口
`SyncMarketData.execute(command) -> SyncBatchSummary` 保持不变。并发数、请求协调器和仓储
由组合根注入,CLI 调用方不需要了解线程、频控或连接池细节。
### 3.2 单股 worker
把当前会修改共享 `failures` / `totals` 的 `_process_bar` 改为返回不可变
`SyncItemOutcome`:股票代码、状态、`WriteResult`、fingerprint 和可选安全错误。
每个 worker 内仍严格执行:
1. 获取该股票六年 qfq;
2. 读取旧正式 CSV 并比较重叠指纹;
3. 写临时 CSV;
4. 在独立 PostgreSQL 事务中幂等 upsert;
5. 数据库提交后原子发布 CSV;
6. 返回 outcome。
主线程通过 `as_completed` 聚合 outcome、更新进度和每 100 项批量写入同步审计。单股异常只
生成失败 outcome;未发布的临时文件必须清理。结果顺序不影响计数或最终状态。
### 3.3 请求协调器
请求协调器是 Tushare 适配器内部的深模块,接口只暴露“执行一个真实供应商调用”。实现隐藏:
- 正常请求保持最多 8 路并发,不用一个全局固定间隔把真实调用重新串行化;
- 保留现有可配置的单股请求间隔作为温和节流;
- “访问频繁/请稍后/超过频率/too many requests/429/403”等频控分类;
- 频控时共享 `cooldown_until`,默认按 60、120、180 秒增长并设置上限;
- 非频控临时错误沿用有界重试和普通退避;
- 注入 monotonic clock / wait 实现确定性单元测试;
- 日志只包含方法名、尝试次数和等待时间,不包含 token 或供应商原始响应。
为保留 Tushare qfq 计算,继续调用 `ts.pro_bar(api=coordinated_client, adj="qfq", ...)`。
`coordinated_client.daily` 与 `coordinated_client.adj_factor` 在原始异常仍可见的位置经过请求
协调器;`pro_bar` 内部重试降为一次,避免隐藏重试绕开全局频控。
### 3.4 PostgreSQL
- 增加 `psycopg_pool.ConnectionPool` 依赖,池最大连接数与 worker 数匹配并留出控制连接。
- CLI 用上下文管理器打开/关闭池;HTTP 进程使用按数据库 URL 缓存的池并在进程退出时关闭。
- worker 的 bar upsert 从池中借一个连接并保持单股票事务,不在线程间共享 connection。
- 主线程调用 `record_items(batch_id, outcomes)` 分批 upsert `market_sync_item`。
- 仓储新增 `count_valid_stocks(target_trade_date)`,用一条集合 SQL 计算 active 股票中同时存在
bar 和 daily_basic 的数量;删除同步编排对逐股 `has_*` 的依赖。
- advisory lock 的连接在整个批次期间保持借出,不和 worker 连接混用。
## 4. 完整性检查模块
### 4.1 语义
“完整性检查”验证 PostgreSQL 事实与已发布 CSV 快照是否一致,并验证 CSV 可以按现有领域规则
解析。它不判断 Tushare 是否已发布某日数据,也不能发现 PostgreSQL 和 CSV 同时缺少但供应商
实际存在的数据。
检查范围:
- 当前 active 股票主数据与 `stock-basic/current.csv`;
- 最近成功同步窗口内的每股 qfq bar 与 `bars/<ts_code>.csv`;
- 同一窗口内 PostgreSQL daily_basic 与 `daily-basic/<YYYY>/<YYYYMMDD>.csv`;
- 缺失、多余、无法解析、重复键、字段内容不一致和窗口越界。
### 4.2 独立持久化模型
新增表而不复用 `market_sync_batch`:
- `market_integrity_check`
- `id`, `status`, `window_start`, `window_end`
- `target_count`, `checked_count`, `issue_count`
- `error_type`, `error_message`, `created_at`, `finished_at`
- `market_integrity_issue`
- `check_id`, `item_kind`, `item_key`, `issue_type`, `message`, `created_at`
- 复合主键包含可稳定区分同一对象多个问题的序号或 issue key。
状态固定为:`running`、`passed`、`issues_found`、`failed`。`issues_found` 表示检查完整执行但数据
存在问题,不等于检查任务执行失败。
### 4.3 只读比较
`RunMarketIntegrityCheck` 是外部应用接口:
- `prepare() -> IntegrityCheckRun` 原子创建 running 记录;已有 running 时返回冲突。
- `execute(check_id) -> None` 获取与同步相同的 advisory lock,执行比较并收敛终态。
- `get(check_id, page, page_size)` 和 `get_latest(...)` 提供轮询读模型。
PostgreSQL adapter 使用有序 server-side cursor,按股票或交易日流式产生一组记录;CSV adapter
按同样 key 顺序读取。应用模块做 merge comparison,一次只保留一个股票或一个交易日的数据,
不把约 700 万行全量装入内存。
检查持有与同步相同的 advisory lock,保证 DB 与正式 CSV 在比较期间不会被同步修改。它只向
检查表写进度和问题;市场事实表和正式 CSV 不发生写入。
### 4.4 HTTP
路由归属 market_data presentation,并由 `/api/v1/market-data` 挂载:
- `POST /integrity-checks` → `202 Accepted`,返回 check id、status、窗口;
- `GET /integrity-checks/latest` → 最近一次检查或 `no_data`;
- `GET /integrity-checks/{check_id}?page=&page_size=` → 进度、终态和分页问题。
HTTP 使用仓库已有的 FastAPI `BackgroundTasks` 模式。后台任务出现未捕获异常时必须把检查记录
收敛为 `failed`。进程重启可能中断 in-process task;下一次触发允许显式把失去 advisory lock
且超过超时阈值的 running 记录标记为 failed,避免永久阻塞。该恢复只修改检查元数据。
## 5. Web 模块
启用现有“同步任务”导航并新增 `/sync` 路由,使用独立 `features/sync` 垂直切片:
- API 类型与函数;
- 最新检查 query、单次检查 polling query、触发 mutation;
- 页面展示检查说明、报告只读提示、最近窗口、进度、状态和分页问题;
- running 时禁用重复触发并按固定间隔轮询;终态停止轮询并刷新 latest;
- `409` 显示已有检查正在运行,`503` 显示存储不可用,其他错误保留可重试入口。
页面不把服务器状态复制到 Zustand。问题明细最少显示对象类型、key、问题类型和安全说明。
## 6. 兼容、部署与回滚
- 新迁移为 `0003_market_integrity_checks`,只新增检查表/索引,不修改事实表。
- 配置默认 worker 从当前未生效的 4 调整为 8;新增频控退避配置时同步 `.env.example`、
Compose 和市场数据运维文档。
- CLI 参数和退出码保持兼容;旧批次和旧 CSV 无需迁移。
- 回滚应用版本时新增检查表可暂留;数据库 downgrade 只删除检查表,不触碰市场事实。
- 若并发上线后供应商频控持续恶化,可通过环境变量把 worker 降为 1,无需回滚代码。
## 7. 验证策略
- 确定性并发测试证明 active worker 不超过配置值,结果汇总不依赖完成顺序。
- fake clock/condition 测试证明频控触发共享冷却,普通错误不冻结其他 worker。
- PostgreSQL 集成测试验证池化并发 upsert、批量 item、集合覆盖率和事务回滚。
- 完整性检查 fixture 覆盖 passed、缺 CSV、缺 DB、内容不一致、非法 CSV、检查冲突和批次异常。
- HTTP 测试锁定 202、409、分页与状态契约;Web 测试锁定触发、轮询停止、报告只读提示和错误态。
- 无网络 5002 股票调度基准锁定线程与仓储调用数量;生产约 30 分钟目标以部署日志验收。
@@ -0,0 +1,6 @@
{"file":".trellis/spec/backend/market-data-sync.md","reason":"父任务集成时保持 qfq、事务/CSV 顺序、覆盖率和 CLI 契约。"}
{"file":".trellis/spec/backend/http-api-contracts.md","reason":"集成完整性检查 HTTP 与 Web 同源契约。"}
{"file":".trellis/spec/frontend/hook-guidelines.md","reason":"集成页面触发、轮询和终态停止行为。"}
{"file":"docs/adr/0004-tushare-six-year-snapshot-sync.md","reason":"检查三个子任务没有绕过六年快照和失败隔离决策。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/research/legacy-sync-and-current-bottlenecks.md","reason":"提供生产基线、旧项目证据与已确认技术选择。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/design.md","reason":"提供任务依赖、跨层接口、状态和回滚设计。"}
@@ -0,0 +1,39 @@
# 父任务执行计划
父任务不直接修改产品代码;按以下顺序启动、检查并集成子任务。
## 1. 子任务顺序
1. 完成 `08-11-market-sync-concurrency-storage`。
- 先锁定并发、频控、数据库池和批量接口。
- 完成后运行后端全量质量门禁和无网络基准。
2. 完成 `08-11-market-integrity-check-api`。
- 以前一子任务的连接池与 advisory lock 契约为依赖。
- 完成迁移、只读检查、后台任务和 HTTP 契约后运行后端全量门禁。
3. 完成 `08-11-market-integrity-check-web`。
- 只消费已冻结的 HTTP 模型和状态值。
- 完成导航、路由、触发/轮询和问题展示后运行前端全量门禁。
## 2. 集成检查
- 检查三个子任务的字段、状态和路径完全一致。
- 模拟同步持锁时触发检查,以及检查持锁时启动同步,确认不会并发修改/读取快照。
- 确认检查批次不影响首页最近同步、selection 数据源选择或 CLI 退出码。
- 确认运行检查时不会构造或调用 Tushare adapter。
- 运行迁移 upgrade/downgrade(测试数据库可用时)并核对 schema metadata。
- 运行根目录 `./dev.sh check` 与 `./dev.sh test`。
- 检查四种 Compose config。
## 3. 生产验收与回退
- 首次部署先执行迁移,再用少量股票/测试环境验证 8 worker 和共享频控日志。
- 生产正常批次记录总耗时、频控次数、累计冷却时间、失败数和覆盖率;目标约 30 分钟。
- 如频控导致失败率上升,通过 `ZHIXING_MARKET_DATA_MAX_WORKERS` 降低并发。
- 完整性检查先在非交易时段触发;确认只产生检查表写入,不改变事实表行数或 CSV mtime。
## 4. 完成门禁
- 三个子任务 acceptance criteria 全部有实际验证证据。
- 父 PRD 中所有 acceptance criteria 可映射到子任务测试或生产验收项。
- 完成 `trellis-check` 后再评估是否有经用户批准、值得提升到 `.trellis/spec/` 的规则。
- 不自动 commit、push、archive;这些动作仍需用户明确授权。
@@ -0,0 +1,84 @@
# 优化市场数据同步并增加完整校验入口
## Goal
把当前接近四小时的市场数据同步缩短到可接受范围,同时保留六年 qfq
历史修订检查、单股票失败隔离、PostgreSQL/CSV 发布一致性和选股覆盖率契约。
日常同步由外部调度器执行;耗时较长的完整检查改为用户在 Web 页面中主动触发并查看进度与结果。
## Background
- 2026-08-10 生产批次处理 5002 只股票,总耗时 13339.3 秒;行情阶段后半段稳定在
2.525–2.820 秒/只,属于逐股固定成本。
- 当前实现串行调用 5002 次 `pro_bar(..., adj="qfq")`。本机 Tushare 1.4.29
源码表明每次 qfq `pro_bar` 至少调用一次 `daily` 和一次 `adj_factor`。
- `Settings.market_data_max_workers` 当前默认 4,但未被同步用例或 CLI 使用。
- 旧项目 `../zgnb-project` 使用 `ThreadPoolExecutor(max_workers=8)` 逐股拉取六年
`pro_bar`;识别频控错误后按 60/120/180 秒退避,普通错误按 5/10/15 秒退避。
它是八路逐股并发,不是按交易日一次请求全市场。
- 当前 PostgreSQL 适配器在单股 upsert、单股审计记录和覆盖率存在性检查中频繁创建
新连接;bar 阶段完成后仍耗时约 110.2 秒才完成批次。
- 当前 Web 导航已有未启用的“同步任务”入口,后端已有 selection 的
`202 Accepted + BackgroundTasks + polling` 长任务模式,但生产服务重启会中断进程内任务。
## Requirements
### R1. 八路受控并发
- 需要同步单股票行情时默认使用 8 个 worker,并允许通过
`ZHIXING_MARKET_DATA_MAX_WORKERS` 配置。
- worker 必须保留单股票事务、临时 CSV、数据库提交后原子发布和单股票失败隔离语义。
- Tushare 频控由所有 worker 协同处理,不能让一个 worker 进入长退避时其他 worker
继续无界冲击接口;重试次数、退避和最终失败必须可观测且不泄露凭据。
- 同一环境仍只允许一个市场数据同步或完整检查批次持有 advisory lock。
### R2. 日常同步与完整检查分离
- 外部调度器继续触发日常同步,日常同步不依赖浏览器或 FastAPI 生命周期定时器。
- 日常同步继续按旧项目的数据口径,对全部目标股票逐股获取六年 qfq 完整快照;性能优化来自
8 路受控并发,而不是改成按目标交易日增量拉取。
- 本任务不引入 `adj_factor` 持久化或按复权因子变化选择性重建历史的增量方案。
- PostgreSQL/CSV 完整性检查不由 cron 自动触发,只允许用户从 HTTP/Web 主动发起;该检查
不调用 Tushare,也不重新下载行情。
- 完整检查需要持久化批次、目标/已检查/问题计数和安全错误,并提供轮询查询契约。
- 用户可在“同步任务”页面触发完整检查;运行中禁止重复触发,并持续展示进度和最终结果。
- 完整检查只报告问题:除检查批次和问题明细外,不修改股票主数据、行情、估值或 CSV;发现的
异常由后续日常同步修复。
### R3. PostgreSQL 批量化
- 同一个批次复用有限数量的数据库连接,避免每只股票为 upsert、审计记录和覆盖率检查
分别建立新连接。
- 同步 item 审计记录支持批量写入,同时保留失败股票的可定位记录。
- 覆盖率通过集合 SQL 一次计算,不逐股票执行 `has_bar` / `has_daily_basic`。
- 并发写入不得破坏 `(ts_code, trade_date)` 幂等约束或批次汇总计数。
### R4. 兼容性
- 保留现有 CLI 成功/部分成功/失败退出码、覆盖率阈值和失败重试语义。
- 保留六年滚动窗口、qfq 唯一口径、PostgreSQL 事实源和 CSV 恢复快照边界。
- 日常同步结果仍能作为选股批次的数据来源,完整检查不得让正在使用的最近成功批次
短暂变成不可用。
## Acceptance Criteria
- [ ] 默认并发数为 8,配置覆盖有效;并发测试能证明最多只有配置数量的单股任务同时执行。
- [ ] 模拟 Tushare 频控时,所有 worker 服从共享冷却窗口,随后成功恢复或在重试预算耗尽后记录失败。
- [ ] 单股票失败不发布其临时 CSV,不回滚其他成功股票,重复执行保持幂等。
- [ ] 日常同步与手动完整检查的职责符合最终确认的数据口径。
- [ ] `POST` 完整检查返回可轮询的批次标识;重复触发返回冲突;查询端点展示进度、状态和错误。
- [ ] “同步任务”页面可触发完整检查并展示无记录、运行、通过、发现问题、执行失败状态。
- [ ] 完整检查不调用 Tushare,不修改市场事实表或正式 CSV;有问题时只持久化安全报告。
- [ ] 覆盖率计算不再产生逐股票查询;审计记录使用批量写入;连接创建数量不随股票数线性增长。
- [ ] 5002 只股票的无网络性能测试/受控基准锁定并发调度和数据库调用数量;生产目标为正常频控条件下
完整下载约 30 分钟,结果需通过部署后日志验证,不能用本地 mock 代替生产结论。
- [ ] 后端 Ruff、Pyright、pytest,前端格式、lint、typecheck、Vitest 和构建全部通过。
## Out of Scope
- 不新增分钟级或实时行情。
- 不改变选股公式、覆盖率阈值业务含义或六年保留边界。
- 不自动安排周期性完整检查。
- 不把日常同步改造成按交易日/复权因子增量拉取。
- 不在完整检查中自动修复、重试或重新下载异常对象。
- 不在本任务中引入与市场数据无关的通用任务平台。
@@ -0,0 +1,56 @@
# 旧项目并发模式与当前性能证据
## 生产基线
- 批次 `7ca4a247-ddb8-4038-a19f-856421433a6a` 在 2026-08-10 处理 5002 只股票,
总耗时 13339.3 秒。
- 从 1800 到 5002 的日志计算得到平均 2.674 秒/只,各区段 2.525–2.820 秒/只,
说明主要成本随股票数线性增长。
- bar 阶段完成到批次完成还有 110.2 秒;当前代码在此期间按股票执行两次存在性查询。
- 4 worker 的理想线性下限约 55.1 分钟,8 worker 的理想线性下限约 27.6 分钟。
## 当前实现
- `application/sync.py:237` 使用普通 `for` 串行调用 `_process_bar`。
- `bootstrap/config.py:21` 定义 `market_data_max_workers`,但没有调用方使用。
- `infrastructure/tushare.py:109-119` 每股调用一次六年 qfq `pro_bar`;本机
Tushare 1.4.29 的 `pro_bar` 内部为 qfq 分别调用 `daily` 和 `adj_factor`。
- `infrastructure/tushare.py:153-170` 只在外层请求成功后等待固定间隔;并发 worker
之间没有共享冷却窗口。
- Tushare 1.4.29 的 `pro_bar` 会在内部捕获原始异常、打印消息并最终抛出通用
`IOError("ERROR.")`。如果只在 `pro_bar` 外层分类错误,将丢失“访问频繁/429”等信号。
- `infrastructure/postgres.py` 在 `upsert_bars`、`record_item` 和每次 `_exists` 中创建连接。
- `application/sync.py:273-278` 为覆盖率逐股票调用 `has_bar` 和 `has_daily_basic`。
- 100 只股票、每只 1500 行的本机 CSV 探针中,读取、比较、重写和发布平均
0.0399 秒/只,属于次要成本。
## 旧项目 `../zgnb-project`
- `application/pipeline.py:104-159` 默认 `workers=8`,使用
`ThreadPoolExecutor` 和 `as_completed` 逐股并发。
- `infrastructure/data_source/tushare_adapter.py:134-172` 每股仍请求六年 qfq
`pro_bar`,不是按交易日一次请求全市场。
- 旧实现识别“访问频繁、请稍后、超过频率、too many requests、429、403”;匹配后按
60/120/180 秒退避,普通错误按 5/10/15 秒退避。
- 因为 `pro_bar` 会把原始异常转换为 `IOError("ERROR.")`,旧实现的外层频控分类不能
稳定看到原始供应商消息;新实现需要在实际 `client.daily` / `client.adj_factor`
调用处协调频控,同时继续复用 `pro_bar` 的 qfq 计算。
- 旧项目 `/data/check` 只用 000001 探测 Tushare 当日是否出数,不检查本地 PostgreSQL/CSV,
因而只能参考交互入口,不能直接迁移为本任务的完整性检查。
## 当前技术选择
- 用户选择保留旧项目的数据口径:日常仍逐股请求六年 qfq,默认 8 worker。
- 用户明确 Tushare 主要限制是调用次数;本任务不改成按交易日增量方案。
- 用户选择完整性检查只报告,不自动修复,也不调用 Tushare。
- Psycopg 3 官方文档确认同步 `ConnectionPool` 可由多个线程共享;
`pool.connection()` 归还连接时自动提交或回滚,池应显式关闭或注册进程退出清理。
## 设计影响
- 单股 worker 必须返回不可变结果,由主线程汇总;不能并发修改共享 list/counter。
- Tushare 真实请求需要一个进程内共享的请求协调器:全局最小请求间隔、频控冷却窗口、
有界重试和可测试时钟。
- PostgreSQL 写路径使用大小受限的连接池;同步 item 由主线程分批落库,覆盖率改为集合 SQL。
- 完整性检查使用独立检查批次表,避免 `mode=check` 污染“最近同步批次”和选股资格查询。
- PostgreSQL 和 CSV 在同一 advisory lock 下以只读方式流式比较,避免一次加载全市场六年数据。
@@ -0,0 +1,30 @@
{
"id": "market-data-sync-performance-audit",
"name": "market-data-sync-performance-audit",
"title": "优化市场数据同步并增加完整校验入口",
"description": "将逐股六年 qfq 同步改为 8 路受控并发并批量化 PostgreSQL,同时增加用户主动触发的只读 PostgreSQL/CSV 完整性检查页面。",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-11",
"completedAt": "2026-08-11",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [
"08-11-market-sync-concurrency-storage",
"08-11-market-integrity-check-api",
"08-11-market-integrity-check-web"
],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,5 @@
{"file":".trellis/spec/backend/market-data-sync.md","reason":"检查只读审计没有破坏市场事实、CSV 或同步批次语义。"}
{"file":".trellis/spec/backend/http-api-contracts.md","reason":"检查路由挂载、模型、状态码和同源路径。"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"检查迁移、类型、测试和禁止模式。"}
{"file":".trellis/tasks/08-11-market-integrity-check-api/prd.md","reason":"逐条核对报告范围、只读性、冲突、分页和流式验收。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/design.md","reason":"检查没有污染首页/selection,并与并发子任务正确集成。"}
@@ -0,0 +1,37 @@
# 市场数据完整性检查 API 设计
## 深模块接口
`RunMarketIntegrityCheck` 对 HTTP 暴露 prepare/execute/query;调用方不需要知道流式 cursor、CSV
布局、比较算法或检查表写入细节。真实 PostgreSQL/CSV adapters 与内存测试 adapters 落在既有 ports seam。
## 数据模型
新增 `IntegrityCheckRun`、`IntegrityIssue` 和分页结果。迁移 `0003_market_integrity_checks` 创建:
- `market_integrity_check`:运行状态、窗口、target/checked/issue 计数、安全批次错误和时间;
- `market_integrity_issue`:check id、稳定 issue key、item kind/key、issue type、安全 message;
- running/created_at 和 issue check id 索引。
检查表不参与首页 overview 或 selection source 查询。
## 比较算法
在同一 advisory lock 中:
1. 读取最近已完成同步窗口和 active stock master;
2. 比较 stock CSV;
3. PostgreSQL 按 `(ts_code, trade_date)` 流式读取 bars,与逐股 CSV 归并比较;
4. PostgreSQL 按 `(trade_date, ts_code)` 流式读取 daily_basic,与逐日 CSV 归并比较;
5. 每完成一组更新 checked_count,问题分批写入;
6. 无问题为 passed,有问题为 issues_found,批次异常为 failed。
事实读取使用稳定快照且不执行 DML。CSV adapter 仅打开正式文件,不创建临时文件、不调用 publish。
## HTTP 与恢复
挂载 `/api/v1/market-data/integrity-checks`。POST 原子 claim 后通过 BackgroundTasks 执行;已有 running
返回 409。查询端点返回 Pydantic 模型并限制 page size。
prepare 时可把超过配置阈值且已不持有 advisory lock 的旧 running 标记为 failed;不能仅按页面刷新
时间误杀仍在运行的任务。后台 execute 最外层保证异常落库。
@@ -0,0 +1,5 @@
{"file":".trellis/spec/backend/market-data-sync.md","reason":"检查必须理解 PostgreSQL/CSV 快照布局、窗口和 advisory lock 契约。"}
{"file":".trellis/spec/backend/http-api-contracts.md","reason":"实现 market_data 业务路由、Pydantic 响应和 FastAPI 错误映射。"}
{"file":".trellis/spec/backend/error-handling.md","reason":"保证批次/问题错误安全、可识别且不吞异常。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/design.md","reason":"提供独立检查表、流式比较、HTTP 状态和只报告边界。"}
{"file":".trellis/tasks/08-11-market-integrity-check-api/design.md","reason":"定义本子任务的领域接口、比较算法、迁移和恢复行为。"}
@@ -0,0 +1,13 @@
# 实施计划
1. 增加领域状态、run/issue/分页模型和 ports,先写 passed/issues/failed 应用测试。
2. 增加 `0003_market_integrity_checks` 迁移并同步 `infrastructure/schema.py`。
3. 扩充只读 CSV 能力:stock、bar、daily_basic 的列举/读取,不复用写入路径产生副作用。
4. 实现 PostgreSQL 流式 snapshot reader 和 integrity check store,复用前置任务连接池。
5. 实现分组 merge comparison、问题批量落库、进度与终态收敛。
6. 实现同 advisory lock 冲突和陈旧 running 恢复。
7. 增加 market_data HTTP presentation、router 挂载、202/查询/错误映射测试。
8. 把经确认的只读检查契约列为 `.trellis/spec/` 更新候选;未经用户批准不直接提升。
9. 运行 Ruff、Pyright、pytest;配置数据库时运行迁移与完整性集成测试。
回滚点:迁移只新增检查表;应用回滚不影响事实表,downgrade 可独立删除检查表。
@@ -0,0 +1,38 @@
# 实现市场数据完整性检查 API
## Goal
提供用户主动触发、可轮询的 PostgreSQL/CSV 完整性检查;检查不访问 Tushare、不修改市场事实,
只持久化检查进度与安全问题报告。
## Dependencies
- 依赖 `08-11-market-sync-concurrency-storage` 的连接池和共享 advisory lock 契约完成。
## Requirements
- 检查 current stock master、六年 qfq bars 和六年 daily_basic 的 PostgreSQL/CSV 一致性。
- 报告缺失、多余、解析失败、重复键、内容不一致和窗口越界;不把正常停牌误报为缺历史日期。
- 使用流式/分组比较,不能一次加载全市场约 700 万行。
- 使用独立检查表和状态 `running/passed/issues_found/failed`,不复用同步批次。
- 检查与同步使用同一 advisory lock;已有检查运行时禁止重复触发。
- `POST` 返回 202,`GET latest` 和 `GET by id` 返回进度、终态及分页问题。
- 后台异常和失去 worker 的陈旧 running 记录必须能收敛为 failed。
- 除检查表外,执行前后市场事实表内容和正式 CSV 必须完全不变。
## Acceptance Criteria
- [ ] 一致 fixture 得到 `passed`;各类不一致得到 `issues_found` 和稳定问题类型。
- [ ] CSV 解析失败被报告且不会中断其余对象检查。
- [ ] 检查只使用本地 DB/CSV adapters,测试能断言 Tushare port 未被构造或调用。
- [ ] 大数据 fixture/迭代器测试证明比较按股票/交易日分组流式消费。
- [ ] 同步持锁时检查安全失败,检查持锁时同步不能进入临界区。
- [ ] HTTP 测试覆盖 202、409、404、503、latest/no_data、轮询进度和问题分页。
- [ ] 迁移 upgrade/downgrade 和 schema metadata 一致;首页/selection 查询不选择检查记录。
- [ ] 后端 Ruff、Pyright、pytest 全部通过。
## Out of Scope
- 不检查 Tushare 当日是否出数。
- 不自动修复、重试或重新下载。
- 不引入外部队列;沿用当前进程内 BackgroundTasks。
@@ -0,0 +1,26 @@
{
"id": "market-integrity-check-api",
"name": "market-integrity-check-api",
"title": "实现市场数据完整性检查 API",
"description": "实现不访问 Tushare、不修改市场事实的 PostgreSQL/CSV 完整性检查批次和 HTTP 轮询接口。",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-11",
"completedAt": "2026-08-11",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-11-market-data-sync-performance-audit",
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,5 @@
{"file":".trellis/spec/frontend/hook-guidelines.md","reason":"检查请求适配、query key、取消和 polling 终止逻辑。"}
{"file":".trellis/spec/frontend/state-management.md","reason":"检查没有复制服务器状态或引入不必要全局状态。"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"检查格式、lint、typecheck、Vitest 和 build 证据。"}
{"file":".trellis/tasks/08-11-market-integrity-check-web/prd.md","reason":"逐条核对导航、触发、轮询、恢复、报告和错误态。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/design.md","reason":"检查前端与已冻结 HTTP 契约及只报告边界一致。"}
@@ -0,0 +1,26 @@
# 同步任务检查页面设计
## Feature 接口
新增 `features/sync/api` 三件套:types、request adapters、React Query hooks。页面只调用 hooks;
query keys 统一以 `marketIntegrity` 开头。
- latest query:进入页面获取最近检查;
- check query:running 时按 id 周期轮询,终态停止;
- trigger mutation:POST 成功后写入/失效相关 query cache 并切换到该 id。
## 页面状态
- `no_data`:解释检查范围并提供“开始完整性检查”。
- `running`:进度条、checked/target、问题数、开始时间,按钮禁用。
- `passed`:完成摘要和“未发现 PostgreSQL/CSV 不一致”。
- `issues_found`:完成摘要、只报告提示和分页问题表。
- `failed`:安全错误、重新发起入口;不会声称市场数据损坏。
409 时刷新 latest 并接管已有 running;其他错误保留 ApiError 状态和用户可见重试。页面刷新通过 latest
恢复 active id,不需要 localStorage/Zustand。
## 路由与可访问性
`/sync` 加入 route tree、navigation 和 routePresentation;AppLayout active route 识别该路径。按钮、
进度、状态和问题列表使用可访问标签,焦点不因 polling 被重置。
@@ -0,0 +1,5 @@
{"file":".trellis/spec/frontend/directory-structure.md","reason":"按 sync feature 垂直切片组织 API、hooks、页面和测试。"}
{"file":".trellis/spec/frontend/hook-guidelines.md","reason":"实现 query key、AbortSignal、mutation 和条件 polling。"}
{"file":".trellis/spec/frontend/state-management.md","reason":"服务器检查状态只进入 React Query,不复制到 Zustand。"}
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"实现可访问状态、进度、按钮和分页问题展示。"}
{"file":".trellis/tasks/08-11-market-integrity-check-web/design.md","reason":"定义页面状态机、409 接管、刷新恢复和路由行为。"}
@@ -0,0 +1,10 @@
# 实施计划
1. 根据已冻结 OpenAPI/后端测试响应定义 sync types 和 API adapters,先写 request contract tests。
2. 实现 latest/check queries、trigger mutation 和终态停止 polling。
3. 新增 Sync page 的 no_data/running/passed/issues_found/failed 展示和问题分页。
4. 启用 navigation、route tree、route presentation 与 AppLayout active path。
5. 补页面刷新恢复、409 接管、503/网络错误、只报告提示和可访问性测试。
6. 运行 `pnpm format:check`、`pnpm lint`、`pnpm typecheck`、`pnpm test`、`pnpm build`。
回滚点:`/sync` route 与导航启用可独立回滚,不影响首页和 selection。
@@ -0,0 +1,35 @@
# 实现同步任务检查页面
## Goal
启用现有“同步任务”导航,让用户主动触发只读市场数据完整性检查,并持续查看进度、终态和分页问题报告。
## Dependencies
- 依赖 `08-11-market-integrity-check-api` 冻结路径、字段、状态和错误码后开始实现。
## Requirements
- 新增 `/sync` 路由和 `features/sync` 垂直切片,启用桌面/移动导航入口。
- 页面明确说明检查只比较 PostgreSQL/CSV,不访问 Tushare、不自动修复。
- 无历史检查时可触发;running 时显示进度并禁用重复触发;终态停止轮询。
- 展示 passed、issues_found、failed 状态、窗口、checked/target、问题数和时间。
- 问题报告分页展示对象类型、对象 key、问题类型和安全说明。
- 409、503、网络错误和刷新页面后的 running 恢复均有可见行为。
- 服务器状态只放 React Query,不复制到 Zustand。
## Acceptance Criteria
- [ ] 导航“同步任务”可用并正确激活 `/sync` route presentation。
- [ ] 点击检查调用一次 POST,显示 accepted/running,并轮询返回的 check id。
- [ ] running 时按钮禁用;终态停止 polling 并展示正确状态与进度。
- [ ] 页面刷新后能从 latest running 恢复轮询。
- [ ] issues_found 展示分页问题;passed 显示无问题;failed 显示安全错误和重试入口。
- [ ] 页面存在明确的“只报告、不自动修复”提示。
- [ ] API adapter、query/mutation 和页面测试通过;格式、ESLint、TypeScript、Vitest、build 全部通过。
## Out of Scope
- 不提供自动修复按钮或直接启动 Tushare 同步。
- 不在本任务中重做首页市场概览。
- 不新增全局任务中心或 Zustand 服务器状态。
@@ -0,0 +1,26 @@
{
"id": "market-integrity-check-web",
"name": "market-integrity-check-web",
"title": "实现同步任务检查页面",
"description": "启用同步任务导航,提供触发、轮询、进度和异常报告页面。",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-11",
"completedAt": "2026-08-11",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-11-market-data-sync-performance-audit",
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,5 @@
{"file":".trellis/spec/backend/market-data-sync.md","reason":"检查并发实现仍满足市场同步完整契约和测试矩阵。"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"检查 Ruff、Pyright、pytest、禁止模式和集成测试证据。"}
{"file":"docs/adr/0004-tushare-six-year-snapshot-sync.md","reason":"检查单股事务/发布顺序、失败隔离和滚动清理没有漂移。"}
{"file":".trellis/tasks/08-11-market-sync-concurrency-storage/prd.md","reason":"逐条核对并发、频控、数据库调用数量和兼容性验收条件。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/research/legacy-sync-and-current-bottlenecks.md","reason":"用生产基线和旧实现证据检查性能结论是否被夸大。"}
@@ -0,0 +1,43 @@
# 八路同步与存储批量化设计
## 模块接口
- `SyncMarketData.execute(command)`:外部接口保持不变,内部使用固定大小 executor。
- `SyncItemOutcome`:worker 返回值,封装 result/fingerprint/failure;不暴露线程实现。
- Tushare 请求协调器:适配器内部接口,执行一个真实 client 方法并隐藏间隔、重试和共享冷却。
- `MarketDataRepository.record_items(...)`:批量持久化 outcomes。
- `MarketDataRepository.count_valid_stocks(...)`:集合式覆盖率接口。
## 并发流
stock master 和 daily_basic 保持主线程顺序执行。bar 阶段把股票提交给最多 8 个 worker;每个 worker
完成获取、CSV staging、数据库事务和 CSV 发布后返回。主线程 `as_completed` 聚合并每 100 项刷新
审计和日志。批量审计写失败是批次级错误,不回滚已提交事实。
## Tushare 频控
由 coordinated client 代理 `pro_bar` 实际调用的 `daily` / `adj_factor`:
1. 请求前检查共享 cooldown;正常情况下允许最多 8 路并发;
2. 调用原始 token client;
3. 频控异常扩大共享 cooldown,唤醒/阻塞所有等待 worker;
4. 普通临时错误只退避当前调用;
5. 到达重试预算后抛出安全 `TushareSourceError`。
现有可配置请求间隔保留为单股成功后的温和节流,不对所有真实调用强加全局串行间隔。
`pro_bar(..., retry_count=1)` 避免 SDK 在看不到共享 gate 的位置自行重试。测试注入 fake clock、wait
和 client,不使用真实睡眠或网络。
## PostgreSQL
依赖调整为 Psycopg pool extra。CLI 在一次执行期间打开池并在退出时关闭;池最大连接数至少覆盖
8 个 worker、一个主线程写连接和 advisory lock 连接。connection 不跨线程共享。
`record_items` 使用 `executemany` 或 COPY/staging 一次写一批。`count_valid_stocks` 从 active 股票
连接/EXISTS 两张目标日事实表后 count,一次返回 valid count。
## 失败与回退
- executor 创建或批量审计失败记录 batch 错误并收敛状态。
- worker 异常必须转换为 outcome,不能让 future 异常跳过进度。
- worker 数可降为 1,得到与原串行流程等价的安全回退。
@@ -0,0 +1,5 @@
{"file":".trellis/spec/backend/market-data-sync.md","reason":"实施时保持六年 qfq、单股事务、CSV 原子发布、失败重试和覆盖率契约。"}
{"file":".trellis/spec/backend/configuration-and-runtime.md","reason":"新增 worker、频控和连接池配置必须通过 Settings 与部署环境传递。"}
{"file":"docs/adr/0004-tushare-six-year-snapshot-sync.md","reason":"并发化不得破坏快照比较、事务顺序、partial success 和滚动保留决策。"}
{"file":".trellis/tasks/08-11-market-data-sync-performance-audit/research/legacy-sync-and-current-bottlenecks.md","reason":"提供生产基线、旧项目 8 worker/退避模式和当前 Tushare 异常隐藏问题。"}
{"file":".trellis/tasks/08-11-market-sync-concurrency-storage/design.md","reason":"定义 worker outcome、请求协调器、连接池和批量仓储接口。"}
@@ -0,0 +1,14 @@
# 实施计划
1. 先补失败测试:worker 上限、完成顺序、单股隔离、共享频控和集合覆盖率。
2. 在 domain/application 定义 `SyncItemOutcome`,把 `_process_bar` 重构为无共享可变状态的返回式流程。
3. 在 Tushare adapter 增加 coordinated client/request coordinator,设置 SDK `retry_count=1`,补错误分类测试。
4. 把 `market_data_max_workers` 默认改为 8 并完成正整数校验;CLI 注入同步用例。
5. 引入 Psycopg connection pool,保持 advisory lock 与单股事务的独立连接语义。
6. 增加 `record_items` 和 `count_valid_stocks`,删除编排中的逐股覆盖率查询。
7. 用固定线程池执行 bar futures,主线程聚合、分批审计和记录有界进度。
8. 更新 `.env.example`、Compose 参数传递、市场同步文档和依赖锁文件。
9. 运行 `uv lock --check`、Ruff format/check、Pyright、pytest;有测试数据库时运行 market_data integration tests。
10. 运行 5002 fake 股票无网络基准,记录最大并发、耗时和 repository 调用次数。
回滚点:连接池改造和并发编排分别保持独立变更;出现供应商问题时配置 worker=1。
@@ -0,0 +1,40 @@
# 实现八路同步与存储批量化
## Goal
在不改变逐股六年 qfq、单股票事务/CSV 发布和覆盖率业务语义的前提下,把行情阶段从串行改为
默认 8 路受控并发,并消除 PostgreSQL 连接、审计写入和覆盖率查询的线性额外开销。
## Dependencies
- 无子任务依赖;本任务是完整性检查后端的前置任务。
## Requirements
- `ZHIXING_MARKET_DATA_MAX_WORKERS` 默认 8,CLI 实际传入同步用例;取值必须大于等于 1。
- bar worker 只处理一只股票并返回不可变 outcome,主线程负责计数、进度和批量审计。
- 所有 worker 共用 Tushare 请求协调器;真实 `daily` / `adj_factor` 调用能触发共享频控冷却,
正常流量不能被一个全局固定间隔重新串行化。
- 频控分类至少覆盖旧项目的中文提示、429 和 403;频控冷却默认 60/120/180 秒且可配置。
- 普通临时错误有界重试;数据验证错误不重试;所有日志安全且可统计等待时间。
- `pro_bar` 继续使用由 `pro_api(token)` 创建的 client,并保留 qfq 计算;内部隐藏重试不得绕开协调器。
- PostgreSQL 使用线程安全连接池,每个 worker 借独立连接完成单股票事务。
- `market_sync_item` 由主线程分批 upsert;覆盖率由一个集合查询计算。
- CLI 参数、退出码、失败重试、advisory lock、滚动清理和摘要字段保持兼容。
## Acceptance Criteria
- [ ] 并发测试证明默认/配置 worker 上限有效,5002 个 fake 股票不会创建 5002 个线程。
- [ ] 不同完成顺序得到相同汇总;单股票失败不影响其他股票,失败股票不发布临时 CSV。
- [ ] 一个 worker 命中频控后,其他 worker 在共享冷却截止前不启动新的真实供应商调用。
- [ ] `pro_bar` 的 `daily` 与 `adj_factor` 都经过请求协调器,token client 回归测试继续通过。
- [ ] PostgreSQL 集成测试覆盖多线程 upsert、事务回滚、批量 item 和集合覆盖率。
- [ ] 连接获取次数受池上限约束,覆盖率不再逐股票调用 `has_bar` / `has_daily_basic`。
- [ ] 同步用例、CLI 和 Tushare 单元测试通过;后端 Ruff、Pyright、pytest 全部通过。
- [ ] 无网络基准记录 5002 股票调度耗时、最大并发和 repository 调用数;生产 30 分钟目标标记为部署后验证。
## Out of Scope
- 不改成按交易日增量下载,不持久化 adj_factor。
- 不增加 HTTP 或 Web 页面。
- 不改变六年窗口、数据表业务字段或选股资格语义。
@@ -0,0 +1,26 @@
{
"id": "market-sync-concurrency-storage",
"name": "market-sync-concurrency-storage",
"title": "实现八路同步与存储批量化",
"description": "为逐股六年 qfq 同步接入 8 路受控并发、共享频控、连接池、批量审计和集合式覆盖率计算。",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-11",
"completedAt": "2026-08-11",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-11-market-data-sync-performance-audit",
"relatedFiles": [],
"notes": "",
"meta": {}
}