问题不是"不好看",而是**可测量的错位**:同一行里原生 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 小屏)。
140 lines
10 KiB
Markdown
140 lines
10 KiB
Markdown
# 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 直接暴露给前端
|