Files
Simon 9aaca12751 fix(web): 控件对齐与输入友好整改(控件高度令牌化 + 对齐自检脚本)
问题不是"不好看",而是**可测量的错位**:同一行里原生 date 输入 38.8px、数字输入
36.8px、按钮 34px;16px 的勾选框与 36.8px 的下拉同排;因子行的下拉与权重框没有
可见标签;列表里的勾选框点不中;参数非法时「运行回测」直接置灰且不说原因。

根因:控件高度靠「上下 padding + 行高」拼出来,而 input / select / button 的原生行高
各不相同(Chrome 的 date 还会多 2px),必然参差;加上各处内联像素宽度
(style={{width:220}}、flex:1)与自搓布局,列自然对不齐。

改动:
- 新增控件高度令牌 --ctl-h-sm/md/lg(28/34/38px)与 --ctl-px,.input/select/.btn/
  .icon-btn/date 统一显式 height(不再拼 padding);原生 checkbox/radio 统一 16px,
  点击热区交给外层 label(.check/.radio-row/.check--cell),表格整格可点
- 因子行、选股条件行改为「表头 + CSS 栅格」成列对齐,列宽由样式决定,
  去掉内联像素宽度与 flex 拉伸,配 aria-label 供读屏分辨重复行
- 表单友好化:错误提示改为**失焦或提交后**才出现(清空重填的瞬间不再标红);
  提交被拦下时一次展开全部行内错误 + 自动聚焦并滚动到第一个问题字段;
  运行/保存按钮不再因参数非法而置灰(灰按钮不说原因 = 看起来不可点却无响应),
  改为可点击并讲清原因;补齐 topN/costs 两处「产生了却没人显示」的行内错误落点
- 数值字段补 inputMode/step/min/max 与单位、取值范围提示;工具条检索/筛选用
  .input--search/.input--filter/.input--picker 类,不再写内联宽度
- /experiments 筛选无结果的空态与「暂无实验」区分开(原文案会让人以为归档丢了)
- 同一页面可能挂两份表单:radio name 与 label/for 加表单实例前缀(useId),
  否则两边单选互相取消、label 指错控件
- 新增 scripts/verify_ui_alignment.py:系统 Chrome + 原生 CDP(仅标准库,
  独占随机端口与临时 profile),按 7 个页面 × 1500/375px 检查同排等高、
  高度取值归一、点击目标、标签与无障碍名、字号圆角一致、尺寸匹配内容、
  横向溢出、提示裁切;本次基线 108/32 → 现 140/140

验证:pytest 388 passed、ruff 全绿(顺带清掉 qlib_verify.py 一处死代码)、
tsc 0 错误、图表单测 7 passed、next build 成功、契约自检 59/59、
对齐自检 140/140(含 375px 小屏)。
2026-09-27 09:01:55 +08:00

140 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# qlib-platform
个人 A 股量化研究平台:**股票筛选 · 因子研究 · 交易信号 · 选股回测**,面向低频 / 中低频交易研究。
> 核心量化引擎:[Qlib](https://github.com/microsoft/qlib) · 首选数据源:Tushare(新浪财经为备用)· 当前数据库:MySQL(业务层无感切换;SQLite 仅兜底)
## 定位
- 个人开发项目,中低频选股 / 因子 / 组合回测
- **不做**高频交易、不做过度的微服务化
- 以 Qlib 为研究引擎,但 Qlib 只是计算引擎而不是整个系统
- AI Agent(Phase 5)只能通过受控 Tool 调用研究能力
## 架构
详见 [AGENT.md](./AGENT.md)(开发约束)与 [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)(架构文档),**改代码前必须阅读**。
```text
Web 前端 (frontend/web:总览 / 股票池 / 股票筛选 / 因子研究 / 因子组合 / 交易信号 / 选股回测 / 实验 / 归档详情)
↓ REST / SSE(异步 Job 状态机)
FastAPI (backend:业务对象 API,见下「核心能力」)
↓ Research Specification(统一研究契约)
Application Service(SelectionService / SignalService / ResearchService / Strategy …)
├── Selection Engine(条件选股 / 因子评分 / as_of 历史与当前一致)
├── Signal Engine(BUY / WATCH / SELL + 理由)
├── Portfolio Engine(等权;约束预留并如实标注)
└── Quant Service ── Composite Engine ── Qlib Adapter ── Qlib
↓ ↓
Repository / DAO MySQL(默认)/ Parquet
```
### 目录结构
```text
qlib/
├── backend/ # FastAPI 后端(uv 管理,Python 3.12)
│ ├── app/
│ │ ├── api/ # HTTP API 层(面向业务对象,禁止暴露 Qlib/SQL)
│ │ ├── application/ # 用例 / 应用服务(编排,不含框架细节)
│ │ ├── domain/ # 领域实体 + Repository Protocol
│ │ ├── infrastructure/ # SQLAlchemy / Alembic 等基础设施实现
│ │ ├── quant/ # 因子 / composite / universe / selection / signal / portfolio 引擎
│ │ │ └── qlib_adapter/ # Qlib 适配层(业务禁止直接 import qlib)
│ │ ├── agent/ # AI Research Agent(Phase 5,仅 Tool 访问)
│ │ └── core/ # 配置、通用组件
│ └── tests/
├── frontend/web/ # React / Next.js 前端(Phase 3 初始化)
├── data/ # raw / normalized / parquet / qlib(不入库,gitignore)
├── experiments/ # 实验产物(不入库,gitignore)
├── scripts/ # 数据同步、运维脚本
├── docker/ # 部署(Phase 后引入,第一阶段不过度工程)
├── docs/
├── AGENT.md
├── config.yaml # 可配置项(无密钥)
└── .env.example # 密钥模板(复制为 .env,勿提交)
```
## 使用说明
完整使用文档(安装 / 配置 / 数据同步 / API / Agent / 常见问题)见 **[docs/USAGE.md](./docs/USAGE.md)**。
## 快速开始(后端)
前置:安装 [uv](https://docs.astral.sh/uv/)(`pip install uv` 或官方脚本)。
```bash
# 1. 配置密钥(复制模板,填入 TUSHARE_TOKEN 等)
cp .env.example .env
# 2. 安装依赖(自动使用 Python 3.12,见 backend/.python-version)
cd backend
uv sync # 含 pyqlib(GitHub 源码依赖,固定 commit)。若网络下载困难/超时,按 AGENT.md §0 设置代理 192.168.1.160:3128 后重试
# 3. 运行测试
uv run pytest # 全量 388 条
uv run ruff check app tests
# 3b. 端到端契约自检(真实提交回测 Job,验证页面↔后端字段不漂移)
PYTHONPATH=. .venv/bin/python ../scripts/verify_strategy_workspace.py # 策略库/说明/名称/选股直通/归档链路(59 项)
PYTHONPATH=. .venv/bin/python ../scripts/verify_backtest_page_contract.py # 回测结果结构契约
python3 ../scripts/verify_ui_alignment.py # UI 对齐与控件一致性(140 项,需前端已启动)
# 4. 启动开发服务
uv run uvicorn app.main:app --reload --port 8000
# 健康检查:http://127.0.0.1:8000/api/health
# 交互文档:http://127.0.0.1:8000/docs
```
### 数据库(默认 MySQL)
- 连接在根目录 `config.yaml → database.mysql`(host/port/db/user/charset),密码放 `.env` 的
`MYSQL_PASSWORD`;URL 优先级:`DATABASE_URL` 环境变量 > `database.mysql` > SQLite 兜底。
- 结构变更(Model → Migration → Test):
```bash
cd backend
uv run alembic upgrade head # 在当前配置指向的库上建表(默认 MySQL qlib)
uv run alembic revision --autogenerate -m "add xxx table"
```
## 核心能力(现状)
| 能力 | 说明 / 入口 |
|---|---|
| 股票筛选 | `POST /api/selections`(A 条件/B 评分,`as_of` 历史/当前,可解释);`POST /api/selections/jobs` 全市场异步;Web `/selection` |
| 因子目录 | `factor_definition` 落库;`GET /api/factors`;组合可保存复用 `POST /api/composites` |
| 因子研究 | Job 异步 IC / RankIC / 分层测试(`/api/jobs`) |
| 交易信号 | `POST /api/signals`:评分排名 + 趋势规则 → BUY/WATCH/SELL + 理由;Web `/signals` |
| 选股回测 | `POST /api/backtests`(成本/涨跌停近似;与当前选股共用同一评分引擎,v2 §25 一致性) |
| 定期调仓回测 | **两级截断(候选池 n → 持仓 x)+ 双周期(每 m 月择股 / 每 y 月调仓)+ 复权口径(none/qfq/hfq)+ 组合条件过滤 + 最低佣金**;输出净值/个股曲线并标注买卖点;Web `/backtest`(含高股息案例预设)、`scripts/run_dividend_case.py` |
| 每日指标 | `daily_basic`(股息率 `dv_ratio`/`dv_ttm`、PE/PB/市值)+ `dividend_yield` 因子;`sync daily_basic` 回补 |
| 退市股与时点股票池 | `sync basic --include-delisted` 入库已退市/暂停上市(`status`/`delist_date`)+ 定向 `sync daily` 补行情;`filter_stocks` 按 `as_of` 正确纳入/排除(实测 338 只) |
| 时点 ST / 名称历史 | `sync namechange` 入库名称生效区间(14,213 行);`exclude_st` 在**每个择股日**按当时名称判定(回测与 `/api/selections` 同口径),消除「曾高股息后 ST」的股息陷阱隐藏偏差(实测 3.70pp) |
| 策略库 | `strategy` 落库 + `/api/strategies` CRUD/`PUT` 原地更新/展开为回测 spec;**每个策略有后端推导的「一句话说明 + 计算公式 + 执行步骤 + 注意事项」**(`describe_strategy`,与引擎实执行规则同源);一键回测;Web `/strategies` |
| Experiment / 归档 | **完整存档**:每次回测(异步 Job 与同步接口都算)把结果 + spec + `code_version` + `data_version` 数据快照指纹写入 `experiment` 表,个股收益曲线默认**全量保存**(超出体积预算才裁剪并显式标注);列表支持 `kind`/`q` 过滤且总数经 `X-Total-Count` 暴露(不再静默截断);`DELETE /api/experiments/{id}` + `python -m app.cli.prune_experiments --keep N`(默认 dry-run,删除即失去结果,建议先导出)治理体积;**实验对比**:勾选 2~3 个 → 归一化净值曲线叠加 + 指标差值表 + `config_snapshot` 参数 diff;**归档详情页 `/experiments/{id}`**(Server Component)只读复看完整结果,并明确回答「**选股条件**」与「**交易执行依据**」;归档页可**导出完整 JSON / 以此参数再跑 / 删除**(确认框写明后果);体积用 `app.cli.prune_experiments`(默认 dry-run)治理,删错的历史归档可用 `app.cli.restore_experiment_from_job` 从 Job 副本按原 id 重建 |
| 图表基座 | **统一 TradingView Lightweight Charts 4.2.3**:`components/charts/LwChart.tsx`(折线/面积/柱状 + 买卖点标记 + tooltip + 可点击图例)与 `components/StockChart/CandleChartLW.tsx`(个股 K 线 + 成交量 + MA);ECharts 已完全下线(`package.json`、`pnpm-lock.yaml`、`node_modules`、文档与架构图标注均已清除)。实测个股页图表根节点是 Lightweight Charts 自身的 `div.tv-lightweight-charts`,页面 canvas 无一来自其它图表库 |
| 代码即名称 | 任何出现股票代码的位置都成对显示股票名称且可点击进入个股页(基本信息 + 走势图);后端在 `SelectionCandidate/SymbolCurve/ActionRecord/RankedPick/Position/Trade` 上填充 `name`,前端 `GET /api/stocks/names` 一次性缓存兜底 |
| AI Agent | `POST /api/agent/chat`,**14 个受控 Tool**(v3 §25 全清单) |
## 里程碑(详见 [ROADMAP.md](./docs/ROADMAP.md) 与 [docs/DEV_PLAN_v2.md](./docs/DEV_PLAN_v2.md))
- **M0–M5**:工程骨架 → 数据层 → 研究引擎 → Web → Experiment/Job → AI Agent
- **M-DB**:SQLite 全量迁移 MySQL(config.yaml 配置化 + 迁移工具 + 一致性校验)
- **M6**:选股系统主线(Universe + Selection A/B + 落库/API + 回测共用 + Web)
- **M7**:因子层(因子定义入库 + Composite 模块化 + 行情口径显式化)
- **M8**:Signal / Portfolio / Strategy / Agent 工具 / Web 做实(M8.4 Qlib 模型选股按需延后)
- **V3**(见 DEV_PLAN_v3):Chart Service/K线与成交点、Signal↔Fill 区分、个股研究页、
复权口径、Bar Replay、指数历史成分、因子相关性、组合单股上限、Job stage/取消、
选股异步化、Agent 14 工具(B2/B3 停牌/ST/报表与 C3 模型选股/D3 Redis 按需延后)
## 约定速查
- 回答与文档使用中文
- 一切时间相关数据防「未来函数」:财务数据区分 `report_date` / `announce_date`,查询支持 `as_of_date`
- 数据源必须经 `MarketDataProvider`(Tushare 优先,Sina 兜底且记录来源),业务层禁止直接 import 新浪 / Tushare 实现
- 业务层禁止直接操作 sqlite3 / SQL / SQLAlchemy Session,只能走 Repository
- 新表:Model → Alembic Migration → Test
- 禁止修改 site-packages/qlib 源码,一律走 Adapter / Wrapper
- API 输入输出用 Pydantic DTO,禁止把 ORM Model 直接暴露给前端