- docs/USAGE.md:
· 因子层一行改为「代码注册表投影 + 参数化实例,参数写在名字里以冻结口径」;
· 新增 `GET/POST/PATCH /api/factors`、`GET /api/factors/templates` 与
`/api/condition-fields`、`/fields` 的说明;
· 新增「参数化因子(2026-10)」块:受控范围、键必须写全参数(缺项就靠可改的
默认值兜底 = 追溯改义,所以拒绝)、口径文案按代码收敛、停用 ≠ 删除、
参数化因子也能当过滤条件;
· 自检清单一并更新(pytest 500 条;verify_strategy_workspace 145 项 skip-job;
verify_ui_alignment 8 页 160 项;新增 verify_unit_conversion、verify_factor_params)。
- scripts/:新增 verify_unit_conversion.py(单位只能在给定范围里选 + 界面单位⇄
基准单位换算)、verify_factor_params.py(参数暴露/界面新建/越界拒绝/停用语义,
跑完自动清掉临时因子);verify_strategy_workspace.py 加 [5.7b] 因子参数化一节,
临时因子的清理挪进 finally(断言中途失败也不给真人库留垃圾)。
- .gitignore:docs/screenshots/ 是临时验证证据,不入库(文件留在磁盘)。
54 KiB
qlib-platform 使用说明
适用代码版本:M0–M5 + M-DB + M6 选股系统 / M7 因子层 / M8(Signal·Portfolio·Strategy·Agent·Web) 全部完成(数据库已切换 MySQL);若文档与代码不一致,以代码与 ROADMAP.md / DEV_PLAN_v2.md 为准。
本文覆盖:安装配置、数据同步、启动前后端、研究 API、研究引擎与 Qlib 接入、 AI Agent、测试门禁、已知限制。
1. 项目概览(当前完成度)
个人 A 股中低频量化研究平台:
| 模块 | 位置 / 说明 |
|---|---|
| 数据层 | backend/app/domain、infrastructure/data_sources:Tushare 首选 + 新浪备用(Failover 审计);默认 MySQL(config.yaml database.mysql)+ 日线 Parquet 导出 |
| 选股系统 | quant/selection.py + application/services/selection_service.py:条件选股(A)/ 因子评分 TopN(B),as_of 当前/历史一致;结果落库可复现、可解释 |
| 因子层 | factor_definition 入库(/api/factors 读库:代码注册表投影 + 参数化实例,参数写在名字里以冻结口径)+ Composite Engine(quant/composite.py,组合落库 /api/composites)+ 行情口径显式化(price_adjustment) |
| 交易信号 | quant/signal.py:评分排名 + 趋势规则 → BUY/WATCH/SELL + 理由,落库 /api/signals |
| 研究/回测 | backend/app/quant:ResearchSpec → 因子 → IC/RankIC/分层 → TopK 低频回测 → 标准化 BacktestResult;双引擎:LocalEngine(默认)与 QlibEngine v1 |
| 策略 | strategy 落库 + /api/strategies(命名策略,可展开为回测 spec) |
| Web | frontend/web:总览 / 股票池 / 股票筛选 / 因子研究 / 因子组合 / 交易信号 / 选股回测 / 实验 |
| 异步与归档 | Job 状态机 + Experiment 自动归档 + 一键复跑 + SSE 进度 |
| AI Agent | 14 个受控工具(v3 §25 全清单)+ LLM 编排 |
技术栈:Python 3.12(uv) · FastAPI · SQLAlchemy 2 · Alembic · MySQL(pymysql)(SQLite 兜底)· pandas · pyarrow(Parquet) · pyqlib 0.9.8.dev32(源码安装) · LightGBM · Next.js 15 · TradingView Lightweight Charts 4.2.3(唯一图表基座) · Redis(本机 127.0.0.1:6379 已就绪,触发时接入)
2. 环境准备
# 工具(本机已装则跳过)
python3 -m pip install uv
# Node 18+ 与 pnpm: https://pnpm.io/installation
# 1) 后端依赖(backend/.python-version 固定 3.12)
cd backend
uv sync # 基础依赖(含 Qlib/qlib 源码安装依赖)
uv sync --extra datasource-tushare # 数据同步需要 Tushare SDK(首次)
前端:
cd frontend/web
pnpm install # 依赖使用 npmmirror 镜像时较快
cp .env.local.example .env.local # NEXT_PUBLIC_API_BASE 默认 http://127.0.0.1:8000/api
Qlib 安装说明:本机(Linux aarch64 + CPython 3.12)经源码 git 安装并固定 commit, 见
backend/pyproject.toml与 docs/QLIB_VERIFICATION.md。
3. 配置
3.1 根目录 .env(密钥,不入库)
cp .env.example .env
| 变量 | 必填 | 说明 |
|---|---|---|
TUSHARE_TOKEN |
同步数据时必填 | Tushare Pro token |
LLM_API_KEY |
使用 Agent 时必填 | 大模型 API Key(URL/模型名在 config.yaml) |
DATABASE_URL |
否 | 默认库 = config.yaml → database.mysql(本机 MariaDB 127.0.0.1/qlib);设此项可覆盖(如切回 SQLite) |
MYSQL_PASSWORD |
使用 MySQL 默认库时必填 | MySQL 密码(config.yaml database.mysql.password_env 引用;host/db/user 在 config.yaml) |
APP_SECRET_KEY |
否 | 应用密钥(未接登录,可暂不改) |
约定:密钥只放
.env;URL、模型名等可配置项放config.yaml。.env已被 gitignore,严禁提交。
3.2 根目录 config.yaml(可配置项,可入库)
常用字段(节选,完整见仓库根 config.yaml):
app: {name, version, debug, secret_key_env}
api: {prefix: "/api"}
database:
url_env: "DATABASE_URL"
migrations_dir: ...
mysql: {enabled: true, host: "127.0.0.1", port: 3306,
db: "qlib", user: "qlib", password_env: "MYSQL_PASSWORD", charset: "utf8mb4"}
# URL 优先级:DATABASE_URL 环境变量 > database.mysql 组装 > sqlite:///./data/quant.db 兜底
data_source: {primary: "tushare", fallback: "sina", tushare_token_env: "TUSHARE_TOKEN"}
storage: {parquet_dir: "data/parquet", qlib_dir: "data/qlib", ...} # 相对项目根
agent:
llm: # ← AI Agent 模型接入
base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1" # URL 在这里配
model: "qwen-plus" # 模型名在这里配
api_key_env: "LLM_API_KEY" # Key 仍从 .env 读
- 换模型:改
config.yaml → agent.llm.model(如qwen-max/deepseek-chat等 OpenAI 兼容接口模型),并按平台同步base_url。 - 也可用
.env的LLM_BASE_URL/LLM_MODEL覆盖 yaml(env 优先)。
3.3 数据库迁移
cd backend
uv run alembic upgrade head # 在 config 指定库上建表(默认 MySQL qlib;SQLite 兜底库为 data/quant.db)
# 开发中改 Model 后:
uv run alembic revision --autogenerate -m "desc"
uv run alembic upgrade head
# 对指定库执行(不随默认配置):
DATABASE_URL='mysql+pymysql://user:pass@host/db' uv run alembic upgrade head
2026-09 已把数据从 SQLite(
data/quant.db)全量迁移至 MySQL;随后默认库切换到 本机 MariaDB 10.11(127.0.0.1:3306/qlib)(服务器主机名summer,socket/opt/local/var/run/mariadb-10.11/mysqld.sock,账号qlib@localhost;实测后端进程只有127.0.0.1:3306连接),与远端192.168.1.10/qlib为逐表一致副本(远端已禁止作为 DB 目标:assert_db_target_allowed硬拦截,见AGENT.md§0.1) (迁移当时 stock_daily 7,792,869 / adjust_factor 7,898,717;当前 Alembic heada3f8c21d9b47= 迁移链f5e0d1c2b3a4 → 22d7380706f7(daily_basic) → 7b1c4e9a52d8(result_json→MEDIUMTEXT) → a3f8c21d9b47(stock_name_history))。 迁移工具与校验见scripts/migrate_sqlite_to_mysql.py(--verify-only可复查一致性)。
4. 数据同步与导出
cd backend
uv run python -m app.cli.sync basic # 股票基础信息(在市 L)
uv run python -m app.cli.sync basic --include-delisted # 追加已退市 D / 暂停上市 P(含 delist_date)
uv run python -m app.cli.sync namechange # 名称变更历史(时点 ST 判定,按年分片)
uv run python -m app.cli.sync calendar --start 20240101 --end 20241231
uv run python -m app.cli.sync daily --symbols 600519.SH,000858.SZ --start 20230101
uv run python -m app.cli.sync daily --all --start 20240101 --resume # 全市场 + 断点续传
uv run python -m app.cli.sync financial --all # 财务指标(默认增量)
uv run python -m app.cli.sync financial --all --full # 财务指标(强制全量重拉)
uv run python -m app.cli.sync daily_basic --start 20200101 # 每日指标(股息率/PE/PB/市值)
uv run python -m app.cli.sync verify --symbol 600519.SH # 新浪交叉验证
uv run python -m app.cli.sync export [--years 2023,2024] # 日线按年导出 Parquet
-
basic --include-delisted:Tusharestock_basic默认只返回在市股票,因此delist_date恒为空、退市股整体缺失(回测幸存者偏差的根因)。加该参数后按list_status=L/P/D分别拉取并写入status/delist_date(实测 2019-12 之后退市 230 只)。 ⚠️ 退市股的历史行情必须另行同步,否则池子里有票、没行情:sync daily --symbols <退市代码> --start 20190101。时点股票池语义由filter_stocks保证(delist_date < as_of排除),无需额外配置。 -
namechange:Tusharenamechange的名称生效区间(实测 14,213 行,覆盖 1990-12 起)。 用途:把universe.exclude_st从「最新名称快照」升级为时点名称 ——stock.name是最新快照,用它判定会把「曾为高股息、后来才变 ST/退市」的标的 在整段历史里排除,而那正是股息陷阱样本(实测影响约 3.70pp 收益)。 回测在每个择股日按当时名称重判,与/api/selections的单时点口径一致; 未同步时自动回退最新名称,并在结果config_snapshot.price_basis.name_basis标注point_in_time=false(不静默)。 -
daily_basic:Tusharedaily_basic接口的每日指标(dv_ratio股息率 /dv_ttm/pe/pb/total_mv等),是高股息类策略的数据基础。按缺失交易日续跑, 可中断重跑;每 20 个交易日提交一次进度。仅 Tushare 提供,新浪不支持 (DataSourceNotSupported,不做假兜底)。 -
每次拉取写入
sync_log审计(来源 / 成功与否 / 行数 / 区间),禁止静默切换数据源。 -
daily同时写入日线与复权因子;--resume从本地最新交易日续传。financial默认增量:本地已含「最新应披露报告期」(按 A 股披露节奏推算)的股票直接跳过; 逐只落库,中断/限速后重跑同一命令即可续传补齐(只丢当前一只)。 -
新浪兜底带「两边一致」真实性校验:Tushare 报错时,只有当该股票本地历史与新浪 返回数据重叠部分一致,才把新浪新数据(本地缺失键)导入,并标记
source=sina(日线另标记adjust=qfq前复权;新浪不提供复权因子)。 校验口径:财务为重叠报告期的 eps / 销售毛利率逐期一致(ROE 两端口径不同, 不作依据);日线为最近重叠交易日的前复权价一致(老交易日在除权后不可比)。 本地无历史可对照或校验不一致 → 拒绝导入并告警,留待 Tushare 恢复后重跑补齐。 -
限速时可加
--sleep 秒数加大请求间隔;financial --full重拉全部历史并覆盖既有行。 -
新浪(
verify)仅交叉验证,返回前复权口径,不会并入不复权主库。 -
财务指标带
announce_date(公告日)与source(tushare/sina);研究侧只允许 使用已公告数据(防未来函数),对同一报告期优先消费source=tushare的行。 -
export:data/parquet/stock_daily/<year>.parquet(列:symbol/trade_date/ohlc/volume/amount), 由 pyarrow 写出,可直接用 pandas 读取分析,或作为 Qlib 等引擎的后续数据源。
5. 启动
一键脚本(后端 + 前端同时管理):
scripts/dev.sh start # 启动后端(8000) + 前端(3000)
scripts/dev.sh status # 查看状态
scripts/dev.sh restart # 重启
scripts/dev.sh stop # 停止
scripts/dev.sh logs api # 跟踪后端日志(web 同理)
# 局域网访问前端时(例如 http://192.168.1.160:3000):
# BACKEND_API_URL=http://192.168.1.160:8000 scripts/dev.sh start
手动分别启动见下:
# 后端(端口 8000)
cd backend
uv run uvicorn app.main:app --reload --port 8000
# 交互文档:http://127.0.0.1:8000/docs 健康检查:/api/health
# 前端(端口 3000)
cd frontend/web
pnpm dev
# 打开 http://127.0.0.1:3000
页面:总览 → 股票池(搜索/列表)→ 因子研究(目录 + 单因子 IC/RankIC/分层)→
回测(参数 → 净值/回撤/月度/持仓/未建模标注)→ 实验(归档列表 / 详情 / 一键复跑)。
6. 研究 API 与研究引擎
6.0 引擎分层(M6–M8)
研究/选股链路自 M6 起分层:Composite Engine(因子复合分)→ Selection Engine
(条件/评分选股)→ Signal Engine(买卖信号)→ Portfolio(组合权重)→ 回测。
当前选股与历史回测共用同一评分引擎(quant/composite.build_score_panel),
保证 v2 §25「历史回测与当前选股用同一套逻辑」(一致性由测试锁定)。
- 选股:
POST /api/selections,body 为SelectionQuery(universe / factors / top_n / as_of / method);结果候选含factor_values与selection_reason(为什么选它)。 - 信号:
POST /api/signals,body{query, rules};rules 含买入排名阈值/趋势 MA/卖出区间。 - 策略:
POST /api/strategies保存命名策略 →POST /{id}/expand补 period 展开为 spec 提交回测。
6.1 研究引擎:LocalEngine(默认)与 QlibEngine v1
服务通过 QuantEngine Protocol 注入引擎(backend/app/quant/engine.py):
- LocalEngine(默认,研究服务与 API 使用):纯 pandas —— 因子注册表计算 → 截面 z-score 复合 → TopK 月/周调仓回测。无需 Qlib 数据。
- QlibEngine v1(
quant/qlib_adapter/engine.py):同一套因子与记账规则,但行情经 QlibDataset 供给 —— 把本地行情按 qlib 官方二进制格式落盘(默认data/qlib, 可注入QlibEngine(qlib_dir=...))→qlib.init+D.features读取 close → 回测。
切换示例(代码内):
from app.quant.engine import LocalEngine # 默认
from app.quant.qlib_adapter.engine import QlibEngine # Qlib 数据管线引擎
from app.quant.service import ResearchService
engine = LocalEngine() # 或 QlibEngine()
service = ResearchService(stock_repo, daily_repo, engine)
注意:Qlib bin 以 float32 存储,QlibEngine 结果与 LocalEngine 存在极小数值差 (真实 20 股对照:-12.81% vs -12.97%),属预期精度差异,不影响结论方向。
6.2 API(curl 示例)
研究输入统一为 ResearchSpec(前端 / API / Agent 同构):
curl -X POST http://127.0.0.1:8000/api/backtests \
-H 'Content-Type: application/json' \
-d '{
"type": "backtest",
"universe": {"exclude_st": true, "min_listing_days": 0},
"factors": [{"name": "momentum_60", "weight": 1.0}],
"selection": {"top_n": 5},
"rebalance": "monthly",
"period": ["2024-03-01", "2024-12-31"]
}'
高股息案例(两级截断 + 双周期 + 复权 + 条件过滤)——同一 spec 在页面/API/脚本一致:
curl -X POST http://127.0.0.1:8000/api/jobs \
-H 'Content-Type: application/json' \
-d '{
"type": "backtest",
"universe": {"exclude_st": true, "min_listing_days": 250},
"price_adjustment": "hfq",
"factors": [{"name": "dividend_yield", "weight": 1.0}],
"conditions": [{"field": "dv_ratio", "op": "lte", "value": 30}],
"selection": {"top_n": 20, "hold_top_x": 20,
"allow_substitute": false, "defer_buy": true},
"rebalance": "monthly",
"selection_interval_months": 6,
"rebalance_interval_months": 6,
"costs": {"commission_rate": 0.0003, "stamp_tax_rate": 0.0005,
"slippage_rate": 0.001, "min_commission": 5.0},
"initial_capital": 1000000,
"period": ["2020-01-01", "2026-09-04"]
}'
一键脚本(含指标打印与结果落盘,走真实 Job 链路):
cd backend && PYTHONPATH=. .venv/bin/python ../scripts/run_dividend_case.py --help
新增 spec 字段说明:
| 字段 | 语义 | 默认 |
|---|---|---|
selection.top_n(n) |
择股条件/因子排序后候选池大小 | 30 |
selection.hold_top_x(x) |
实际持仓数,必须 ≤ n(超出直接校验失败) | = n |
selection.allow_substitute |
买不进时是否往 n 名之外替补 | true(向后兼容) |
selection.defer_buy |
买不进时顺延到之后首个不涨停交易日买入 | false |
selection_interval_months(m) |
每 m 个月择股一次(锚定起始月) | 空 = 每次调仓都择股 |
rebalance_interval_months(y) |
每 y 个月调仓一次 | = m |
conditions[] |
选股过滤条件(AND,universe 之后、排序之前) | 空 |
price_adjustment |
none / qfq / hfq(回测与成交价同源折算) |
none |
costs.min_commission |
单笔最低佣金(元) | 0 |
allow_substitute与defer_buy互斥(同时为true会被拒绝): 「换一只买」与「等它不涨停」是两种互斥的补位哲学。 顺延单只在两次调仓之间有效,到下一次调仓仍未成交则作废(资金留作现金)。
| 端点 | 说明 |
|---|---|
GET /api/health |
健康检查 |
GET /api/stocks?q=600519&limit=20 |
股票列表/搜索(单页上限 500) |
GET /api/stocks/names |
{symbol: name} 全市场名称映射(5,900+ 条,前端启动一次性拉取并缓存,避免表格逐行查名称) |
GET /api/stocks/{symbol} |
单只股票详情 |
GET /api/factors |
因子目录:代码注册表 + 参数化实例(读 factor_definition 表)。每行带 template / params / param_specs(可编辑参数与允许范围)/ label(中文名含参数)/ source / enabled / resolvable。读取时按「引擎口径字段」做差集同步(代码里新增的因子当场补进来,能算出来的行口径按代码纠正,手改会被改回);算不出来的手登记行保留但标 resolvable=false(引用即 FactorError)。稳态零写入 |
GET /api/factors/templates |
因子模板:可编辑参数(param_specs:类型/范围/枚举/默认值/说明)、公式、依赖列、已有内置实例 —— 「新建参数化因子」表单的数据源 |
POST /api/factors |
新建参数化因子,body {template, params}(缺省项取模板默认值)→ 201 返回新因子(名字里含全部参数,如 momentum(window=90,direction=higher_is_better))。越界/未知参数/未知模板/重复参数组合 → 422(detail 说明允许范围);同参数不会重复创建 |
PATCH /api/factors |
启用/停用因子,body {name, enabled}(名字含括号/等号,放 body 不放路径)。停用只影响能否被选中,历史策略/归档照旧解析;内置实例 → 422,不存在 → 404 |
GET /api/condition-fields |
字段库(过滤条件可用字段,读 condition_field 表;首次读取自动 seed,?include_disabled=false 只返回启用项) |
GET /api/condition-fields/available |
引擎支持但尚未入库的字段(「新增字段」的可选项) |
POST /api/condition-fields |
新增自定义字段;字段引擎算不出来 → 422(不假装支持) |
PUT /api/condition-fields/{name} |
改中文名 / 含义 / 分组 / 界面单位 / 启用状态(name/kind/source 不可改;单位只能取该字段 units 里列出的值,否则 422) |
DELETE /api/condition-fields/{name} |
删除自定义字段;内置字段 → 400(只能停用) |
POST /api/composites / GET /api/composites |
因子组合保存 / 列表(方向由注册表填充) |
POST /api/selections |
执行选股(method=score 评分 TopN / condition 条件)→ SelectionResult 并落库 |
GET /api/selections/{id} / GET /api/selections |
读回 / 历史选股(as_of、method 过滤) |
POST /api/signals |
生成 BUY/WATCH/SELL 信号(评分+规则)→ 落库 |
GET /api/signals/{id} / GET /api/signals |
读回 / 历史信号 |
POST /api/strategies / GET /api/strategies |
保存(description 留空时自动用推导出的一句话说明补全)/ 列表命名策略 |
GET /api/strategies/{id} / PUT /api/strategies/{id} / DELETE /api/strategies/{id} |
读取 / 原地更新(保留 id 与 created_at,重名 → 400,不存在 → 404)/ 删除 |
POST /api/strategies/{id}/expand |
展开为回测 ResearchSpec(period + 初始资金) |
GET /api/strategies/{id}/describe |
策略说明:{summary, formula, steps, warnings}(由已保存 spec 推导) |
POST /api/strategies/describe |
同上,但直接传 ResearchSpec —— 用于未保存参数的实时预览 |
POST /api/factor-tests |
同步单因子测试 → FactorTestReport;结果同样写入归档(kind=factor_test),归档 id 见响应头 X-Experiment-Id |
POST /api/backtests |
同步回测 → BacktestResult(默认 LocalEngine);结果同样写入归档,归档 id 见响应头 X-Experiment-Id |
GET /api/backtests/last |
最近一次同步回测 |
POST /api/jobs |
创建异步 Job(同 spec),返回 {"job_id","status":"queued"} |
GET /api/jobs/{id} |
查询状态;成功时内嵌 result |
GET /api/jobs/{id}/events |
SSE 进度(curl -N ...) |
GET /api/experiments |
归档列表:支持 kind(backtest/factor_test/selection)与 q(匹配 id/因子/摘要)过滤、limit/offset;过滤后总数放在 X-Total-Count 响应头(body 保持数组形状) |
GET /api/experiments/{id} |
归档详情:spec(复现依据)+ 完整 result + code_version / data_version / job_id / result_bytes |
DELETE /api/experiments/{id} |
删除一条归档(不存在 → 404)。同时等于删掉这次回测的结果:完整结果只存归档一份,Job 记录仍在但 GET /api/jobs/{id} 的 result 会变成 null(并给 result_unavailable_reason 如实说明)。要留底请先导出 JSON |
POST /api/experiments/{id}/rerun |
一键复跑 → 新 Job |
GET /api/stocks/{symbol}/chart|signals|selections |
Chart API:K线/量/MA/复权显示 + 该股历史标记 |
GET /api/backtests/{experiment_id}/stocks/{symbol}/chart|trades|positions |
回测个股图(fills/成交)/明细 |
POST /api/factor-correlations |
多因子横截面 Spearman 相关矩阵 |
POST /api/replays |
Bar Replay 线性重放(as_of 逐日,白名单≤40/区间≤90) |
GET /api/jobs / POST /api/jobs/{id}/cancel |
Job 列表 / 取消(含 stage 字段上报) |
POST /api/selections/jobs |
全市场/长任务选股异步 Job(Web 页「异步」按钮) |
POST /api/agent/chat |
AI 研究助手(见 §7,14 个受控工具) |
防未来函数与真实性:回测在调仓日收盘成交、自次日起计收益;财务数据只使用
announce_date 之前已公告内容;成本(佣金/印花税/滑点)、涨跌停、停牌已建模;
未建模约束(如开盘一字路径)会写入结果的 unimplemented 数组,前端如实展示。
6.3 Web 研究工作台:一条闭环走到底
2026-09 重构:把原来「一个策略 = 全套参数」拆成三件独立的事 —— ① 公共配置(
/settings,全局唯一):佣金 / 印花税 / 滑点 / 最低佣金 / 复权口径 / 基准; ② 选股策略(/strategies):只剩「怎么选」(股票池 + 因子 + 过滤条件), 不含资金 / 持仓数 / 持仓时间 / 调仓 / 费率 / 区间; ③ 回测组合(/backtest):引用若干选股策略 + 回测时才定的参数 (起始资金、持仓数 N、持仓天数区间 [Tmin, Tmax]、调仓时机 日/周/月、区间)。 多策略取并集后 Borda 秩和统一打分;Tmax 每个交易日强制了结,Tmin 防频繁换手。 运行时公共配置的成本/复权会快照进归档config_snapshot,保证可复现。
2026-10 过滤条件字段库(
/fields):过滤条件的字段不再是手填的裸字段名。 字段有中文名 + 含义/口径 + 单位 + 类型,在策略表单里按分组(股票基础 / 行情 / 技术指标 / 每日指标 / 财务指标 / 因子)下拉选择,选中即显示口径说明; 比较符按类型收窄(文本字段只能 等号/属于/不属于,数值字段才能比大小), 取值支持「和固定值比」或「和另一个字段比」(如close > ma60)。 字段库由代码注册表quant/condition_fields.py与引擎域校验,DB 表condition_field承接目录(seed 只补不删,改过的中文名/含义不会被覆盖); 可改文案、可停用、可新增「引擎支持但默认不入库」的字段、可删自定义字段, 内置字段不可删除(删了下次读取会自动补回)。新增时若字段引擎算不出来 → 422 拒绝 —— 否则会建出一条「永远选不出股票」的条件,这是 AGENT.md 禁止的静默失败。
单位:可选,但只在注册表给的范围内选(2026-10)——字段库里的单位分两层, 改错任何一层都会让策略静默算错,所以分开: 基准单位(
base_unit:总市值=万元、成交额=元、成交量=股…)来自数据源落库口径, 是引擎存储与比较用的单位,不可改;策略 JSON、归档spec、引擎求值一律只用它, 所以归档永远复现得出来。 界面单位(unit)只影响「输入 / 显示」,可在字段库里从注册表给的阶梯里选 (总市值 万元⇄亿元、成交额 元⇄万元⇄亿元、成交量 股⇄手⇄万手、股本 万股⇄亿股), 提交前 ×系数、回显时 ÷系数。于是「把总市值改成亿元」立刻生效:策略表单输入5存进库里是50000(万元),卡片与说明里显示「总市值 ≥ 5 亿元」——语义一致、口径不乱。 自由文本单位(如亿亿元)与不在该字段阶梯里的单位(如给总市值选元)一律 422 拒绝, 因为换算必须按固定系数做,标签与实际口径不一致就是静默错误。 百分数(%)与倍数(倍)不提供备选:换个说法只会制造误读。
打分因子 与 过滤条件 的关系(策略表单内也有同样的说明卡): 条件 = 准入(全部 AND 通过才有资格,不做排序);因子 = 优先级(横截面 z-score 加权成复合分后排序,不筛掉任何人)。执行顺序:股票池 → 过滤条件 → 因子打分 → 取 TopN; 其中 N 在回测组合里定,不属于选股策略。同一个字段两种用法都行:高股息策略里
dv_ratio ≤ 30当条件用(剔掉股息率异常偏高、多为一次性分红或股价暴跌的「高股息陷阱」样本),dividend_yield当因子用(让股息率高的排前面)。
因子怎么配(
dividend_yield就是这样一个内置因子):因子由模板 + 参数两段构成, 都在代码注册表(quant/factors.py)里声明。模板是算法家族(动量 / 波动率 / 量比 / 乖离 / 反转 / 接近新高 / 股息率…),给出公式、依赖列(requires)与可编辑参数的 允许范围;参数是每个实例的取值(窗口、方向)。
参数化因子(2026-10):参数可以改,改的方式是从模板新建一个参数化因子。 例如把动量窗口从 20 改成 90、方向改成越低越好,就新建
momentum(window=90,direction=lower_is_better)—— 参数写在因子的名字里, 所以它是一个新身份:旧因子、既有策略、已归档的实验都按各自名字里的参数计算, 不会被后来的修改改义。同一个模板的不同参数版本可以并存(momentum_20、momentum_60、momentum(window=90,…)同时可用),在策略里各自配自己的权重。
- 受控:窗口是
2 ~ 500的整数,方向只有「越高越好 / 越低越好」两个选项; 只在给定范围内选/填,越界、未知参数、乱造模板一律 422(不静默截断、不悄悄取默认值)。- 键要写全参数:
momentum(window=90)这种「漏一个参数」的写法会被拒绝,必须写成momentum(window=90,direction=higher_is_better)。原因是缺项就得靠模板默认值补齐, 而默认值是可以改的代码细节 —— 一旦改了,库里老策略的含义会追溯性地变掉。 界面里你只选参数,规范键由系统生成(目录页会实时预览)。- 口径文案仍按代码收敛:描述 / 公式 / 方向 / 回看 / 依赖列以注册表为准,手改会被下次 读取纠正回来(「文档写一套、代码跑另一套」是禁止的)。依赖列更是事实而不是配置: 它决定引擎装配哪些数据列。
- 停用 / 启用:停用只是把因子从选择列表里拿掉,既有策略/归档仍按名字解析; 想彻底换算法就改代码。内置实例(
momentum_20这类历史名)的开关由代码决定, 不能在目录里停用(要不同参数就新建一个参数化因子)。在选股策略里引用因子时,可配置的是「用哪个参数版本」+ 权重 (
score = Σ 权重 × 截面 z-score,方向由该版本的参数决定,lower_is_better由引擎自动取负号)。 新增的内置因子会在下次读取/api/factors时自动补进目录;目录里多出来的手登记行会保留, 但标resolvable=false—— 引擎算不出来就用不了,不会假装支持。 同一个字段「当条件」还是「当因子」是两种用法,不是两种字段:条件在过滤阶段筛掉, 因子在打分阶段排序。例如dv_ratio(每日指标列)与dividend_yield(因子) 今天算的是同一个数,区别只在「筛掉」还是「排序」。参数化因子也能当过滤条件:momentum(window=90,direction=higher_is_better) > 0(因子是无量纲量,不带单位)。
侧栏「策略」分组把研究闭环串成一条线,顺序即引导:
| 环节 | 页面 | 做什么 | 关键改进 |
|---|---|---|---|
| ① 出候选 | /selection |
因子评分 TopN / 条件选股(可指定历史时点) | 候选表代码 + 名称且可点击进个股页 |
| ② 定规则 | /strategies |
命名保存选股条件组合(股票池+因子+条件),增删改查 | 每个策略展示后端推导的一句话说明 + 选股口径;卡片可直接「加入回测组合」;过滤条件的字段从字段库分组下拉选择并显示口径 |
| ②″ 管字段 | /fields |
维护过滤条件的字段库:中文名 / 含义口径 / 界面单位 / 启用状态,新增与删除自定义字段 | 字段来自引擎注册表(保证真能算);比较符按类型收窄;单位下拉只列注册表给的档(如 万元/亿元)并注明基准单位;页内说明「因子 vs 条件」关系 |
| ②′ 设成本 | /settings |
维护全局唯一的费率 / 滑点 / 复权口径 / 基准 | 所有回测组合共用;改动只影响之后的回测,已归档按各自快照复现 |
| ③ 验规则 | /backtest |
勾选 ≥1 个选股策略 + 填回测参数 → 保存为组合 / 直接运行;下方「已保存的回测组合」库可载入 / 直接运行 / 删除 | 公共配置只读展示;持仓天数区间 [Tmin,Tmax] + 调仓 日/周/月;组合库让保存过的组合能找回来;结果分区锚点导航 |
| ④ 复盘 | /experiments |
勾选 2~3 次回测对比;搜索/按类型筛选后「打开归档」 | 归一化净值曲线叠加 + 指标差值表(✅/⚠️ 标注改善方向)+ config_snapshot 参数 diff |
| ⑤ 存档 | /experiments/{id} |
只读归档快照:任意时候都能翻回来看 | 单策略回测显示「选股条件 + 交易执行依据」;组合回测显示「引用的策略 + 回测参数 + 运行时配置快照」+ 完整结果 + 元数据 + 导出 JSON |
几条「直通」链路(都可分享 URL、刷新后仍生效):
/strategies ──「加入回测组合」──▶ /backtest?strategy=STG-xxxx 预先把该选股策略勾进组合
/experiments ──「以此参数再跑」─▶ /backtest?from_experiment=EXP-xxxx 复用该实验的参数快照
/backtest 组合库 ─「载入到表单」▶ /backtest 回填组合名/说明/策略/参数
/backtest 组合库 ─「直接运行」──▶ POST /api/combos/CMB-xxxx/run 用库里那份参数跑(不受表单草稿影响)
/backtest?combo=CMB-xxxx URL 直达某个已保存组合
回测跑完 ──「打开归档(完整快照)」─▶ /experiments/EXP-xxxx 直接查看这次结果的冻结快照
归档是只读的:
/experiments/{id}用 URL 表达「这就是那次回测」,刷新/换设备/分享链接都能看。
- 单策略回测(旧 ResearchSpec 路径,
/api/backtests)归档顶部两块表:① 选股条件 (股票池 / 因子与权重及方向 / 过滤条件 / 两级截断 n→x / 择股周期 m / 调仓周期 y / 买不进时怎么办) ② 交易执行依据(成交时点=调仓日收盘、复权口径、滑点后的买/卖价、佣金/印花税/最低佣金、 涨停/跌停/停牌如何拦单、顺延规则、期末是否平仓、对照基准)。- 组合回测归档顶部改为「回测组合」卡:引用的选股策略 + 回测参数(N / [Tmin,Tmax] / 调仓时机 / 资金 / 区间)+ 交易执行依据(来自运行时的公共配置快照),并明确标注 「成本与复权是运行那一刻从公共配置快照下来的,事后改公共配置不影响本归档」。
6.3.1 归档的日常操作(查看 / 导出 / 删除 / 恢复)
| 想做什么 | 在哪做 | 说明 |
|---|---|---|
| 查看 | /experiments 每行「打开归档」,或回测跑完提示条「打开归档(完整快照)」 |
进入 /experiments/{id}:选股条件 + 交易执行依据 + 完整结果 + 归档元数据。URL 可分享、刷新不丢 |
| 筛选找归档 | /experiments 顶部搜索框 + 类型下拉 |
条件是 q(匹配 id/因子/摘要)与 kind;筛选会回写 URL(/experiments?kind=backtest&q=dividend),可分享、刷新保持;总数显示「显示 N 条 / 共 M 条」,不静默截断 |
| 导出留底 | 归档页「导出完整 JSON」 | 把该归档的 spec + result 原样下载(字节级一致),是最可靠的留底方式 |
| 看原始 spec | 归档页「查看原始 spec(JSON)」 | 展开后端实际收到的请求体,逐字核对参数 |
| 复用参数再跑 | 归档页「以此参数再跑」→ /backtest?from_experiment=EXP-xxxx |
参数预填但不自动执行(长区间一次 3~5 分钟,由你决定何时跑) |
| 参与对比 | 归档页「加入对比」/ 列表勾选 | 最多 3 条,跳到 /experiments 的对比视图 |
| 删除 | 归档页「删除归档」(带确认框) | 调 DELETE /api/experiments/{id},成功后回到列表 |
删除前必须知道的事(确认框里也写了同样的内容):
- 删除后这份快照(结果 + spec)无法再查看,也不可恢复;
- 完整结果只存归档一份:删除后连该次执行的作业记录也读不回结果
(
GET /api/jobs/{id}的result变null,并给出result_unavailable_reason); - 想留底请先点「导出完整 JSON」。
但是历史归档能救回来(job 表另有副本),边界如下:
| 归档 | job.result_json |
删除后能否重建 |
|---|---|---|
| 完整存档上线前的历史归档 | 有副本(双写遗留) | 能,按原 id 重建 |
| 完整存档上线后的新归档 | NULL |
不能,工具明确拒绝,不假装能救 |
cd backend
# 保留策略:默认 dry-run,只打印将要删的清单;确认后加 --apply
PYTHONPATH=. .venv/bin/python -m app.cli.prune_experiments --keep 10
PYTHONPATH=. .venv/bin/python -m app.cli.prune_experiments --keep 10 --kind backtest --apply
# 从 Job 副本重建某条已删除的历史归档(默认 dry-run;code-version 必须显式给,不猜)
PYTHONPATH=. .venv/bin/python -m app.cli.restore_experiment_from_job \
--job-id JOB-XXXXXXXX --code-version 92627f5 # 加 --apply 才写库
归档完整度会明示:归档页顶部标注「归档完整 N/N」;若曲线因体积预算被裁剪,或该快照产生于 完整存档上线之前(个股曲线只有 60 只),页面直接显示「归档不完整」并给出差异与「以此参数重跑」入口。
图表:全站统一使用 TradingView Lightweight Charts(components/charts/LwChart.tsx),
ECharts 已从依赖中移除。买卖点标记(▲绿=买入 / ▼红=卖出)只落在该 series 真实存在的交易日上,
并按时间升序提交(Lightweight Charts 的硬约束),因此不会出现标记丢失或错位。
股票名称:后端在 SelectionCandidate / SymbolCurve / ActionRecord / RankedPick /
Position / Trade 上都填充 name(由 GET /api/stocks/names 一次性映射),
前端 SymbolLink 保证「有代码必有名称、且可点击进入个股页(基本信息 + 走势图)」;
名称确实缺失时显示灰色「—」而不猜测、不臆造。
自检脚本(接口 + 页面契约,约 2 分钟含一次真实回测):
cd backend && PYTHONPATH=. .venv/bin/python ../scripts/verify_strategy_workspace.py
# 只验接口与页面(跳过回测 Job):加 --skip-job
6.3.2 界面规范与对齐自检(控件尺寸 / 输入友好)
界面不是"能看就行":控件错位、尺寸与内容不匹配、点不中、报错说不清,都会直接变成操作错误。 因此把可度量的部分写成规范 + 自检脚本,而不是靠肉眼。
① 控件高度只有三档令牌(app/globals.css 的 --ctl-h-sm/md/lg = 28 / 34 / 38px):
| 令牌 | 用途 | 典型控件 |
|---|---|---|
--ctl-h-sm |
密集行 | 小按钮、图标按钮、可点击 chip |
--ctl-h-md |
标准 | 输入框、下拉、按钮(默认) |
--ctl-h-lg |
主操作 | 顶栏图标按钮 |
规则:同一行内的控件必须等高。做法是给 .input / select / .btn / .icon-btn 显式
height,而不是靠上下 padding 拼高度 —— 后者会得到 34 / 36.8 / 38.8 三种结果
(Chrome 的原生 date 输入还会多 2px),同一行就肉眼可见地参差。
② 尺寸与内容匹配 + 整体收束:参数表单用 .params-form 约束到 ~1100px
(否则在宽屏上铺满 ~1900px、5 列各 ~360px,控件拉得太开且数字框与下拉宽度不一);
限宽后数值/日期输入直接填满单元格,与同行下拉等宽 → 左右边缘严格对齐
(早先在未限宽时用 max-width:180px 防数字框过宽,限宽后该上限反而让数字框比
相邻下拉窄一截,故移除)。多行文本(如「一句话说明」,最长 300 字)用
整宽 <textarea> 而非单行 input —— 单行框必然截断长文案(截图实测
「例:全市场股息率最高的 2」被切掉)。选项文案长的选择器给更宽的类
(.input--picker);工具条检索/筛选用 .input--search / .input--filter,
不在 JSX 里写内联像素宽度;重复行(因子、条件)用"表头 + CSS 栅格"成列对齐
且首列封顶(因子名 ≤460px),避免一个下拉撑满整行。
③ 点击目标:控件 ≥28px;16px 的勾选框/单选按钮由外层 label(.check / .radio-row)
撑开热区,表格里的勾选框用 .check--cell 让整个单元可点。
④ 标签与无障碍名:每个输入都有可见标签或 aria-label;只有图标的按钮必须有
aria-label/title(否则读屏只会念"按钮")。
⑤ 输入友好:校验错误失焦或提交后才提示(清空重填的瞬间不标红); 提交被拦下时一次展开全部行内错误并聚焦到第一个问题字段; 运行/保存按钮不因参数非法而置灰(灰按钮不说原因属于"看起来不可点却无响应"), 而是可点击并讲清原因。
对齐自检(用系统 Chrome + 原生 CDP,仅标准库;会独占随机端口与临时 profile):
python3 scripts/verify_ui_alignment.py # 8 个页面 × 1500/375px,共 160 项检查(含 /fields、/factors)
python3 scripts/verify_ui_alignment.py --dump # 打印每行控件的宽高与字号明细
python3 scripts/verify_ui_alignment.py --pages /backtest --width 375
检查项:同排控件等高、控件高度取值归一(≤3 种)、点击目标、标签/无障碍名、 字号与圆角一致、尺寸匹配内容、无横向滚动、提示文本不被裁切。
说明:本平台为深色单主题(
color-scheme: dark),因此"深浅两套主题都测对比度" 这一条不适用;对比度按深色主题实测(WCAG 相对亮度公式,正文对背景): 正文--text-1对--bg-017.09:1、对卡片--surface-115.26:1; 标签--text-2对卡片 8.42:1;提示--text-3对卡片 6.12:1; 错误--neg对卡片 7.04:1 —— 均高于正文 4.5:1 / 次要文字 3:1 的门槛。
7. AI Agent
前置:config.yaml → agent.llm(URL/模型名默认百炼兼容端点 + qwen-plus),
.env 中设置 LLM_API_KEY(仅需 Key)。
curl -X POST http://127.0.0.1:8000/api/agent/chat \
-H 'Content-Type: application/json' \
-d '{"message": "帮我测试 momentum_60 因子并跑一次 Top5 月度回测,评估是否值得深入"}'
返回:
{
"reply": "(最终结论,中文)",
"actions": [
{"tool": "test_factor", "args": {...}, "output": "因子测试完成(Experiment EXP-…)..."},
{"tool": "run_backtest", "args": {...}, "output": "回测完成(Experiment EXP-…)..."}
]
}
Agent 能力边界(10 个内置受控工具,只读 + 受控写库):
search_stocks/get_market_data:查询test_factor/run_backtest:研究并自动归档 Experimentscreen_stocks/explain_selection:按因子评分选股(传symbols白名单避免全市场长任务)/ 解释某次选股理由generate_signals:生成 BUY/WATCH/SELL 信号create_strategy:保存命名策略get_experiment/compare_experiments:读取/对比归档
不提供 shell / 任意代码执行 / 删改数据 / 修改配置与凭证。未配置 Key 时接口返回 400 引导信息。研究纪律写入系统提示:禁止仅凭单次样本内高收益判定策略有效,需说明 样本外、过拟合、look-ahead bias、成本、参数敏感性等(未验证项要明说)。
8. 测试与质量门禁
cd backend
uv run ruff check app tests && uv run ruff format --check app tests
uv run pytest # 全量测试(每个里程碑提交前均须通过,当前 500 条)
cd frontend/web
pnpm run typecheck # tsc --noEmit,0 error
pnpm run test:charts # 图表标记逻辑单测(7 条,node --test,无需额外依赖)
pnpm run build # 13 条路由,含 /strategies /backtest /experiments /experiments/[id]
端到端契约脚本(会真实提交回测 Job,用于验证「页面实现」与「后端字段」不漂移):
cd backend
PYTHONPATH=. .venv/bin/python ../scripts/verify_strategy_workspace.py # 选股策略/字段库/因子目录+参数化/公共配置/回测组合库/归档链路(145 项 skip-job,含真实组合回测更多,约 5 分钟)
PYTHONPATH=. .venv/bin/python ../scripts/verify_backtest_page_contract.py # 回测结果结构契约(约 4 分钟)
python3 ../scripts/verify_ui_alignment.py # UI 对齐与控件一致性(8 个页面 × 2 宽度 = 160 项,约 3 分钟)
python3 ../scripts/verify_unit_conversion.py # 字段库单位:只能在给定范围内选 + 界面单位⇄基准单位换算(约 2 分钟)
python3 ../scripts/verify_factor_params.py # 因子参数化:暴露真实参数/界面新建参数化因子/越界拒绝/停用不影响历史解析(约 3 分钟)
覆盖重点:Provider 归一化与 Failover 审计、Repository 幂等与「未来函数阻断」
(as_of_date/announce_date)、Alembic 迁移、因子/IC/分层、回测记账与成本/涨跌停、
数据库目标守卫(192.168.1.10 被拒 / 本机放行 / 列表可用环境变量覆盖)、
QlibEngine 数据管线(bin 落盘格式/roundtrip/端到端回测)、Job 状态机与 Experiment
归档、Agent 工具白名单与编排、LLM 配置加载、API 端到端、策略说明推导
(describe_strategy 的分支/互斥/缺失值语义)、股票名称回填(回测与选股两侧)、
UI 对齐与控件一致性(同排等高 / 尺寸归一 / 点击目标 / 标签 / 无障碍名 / 横向溢出,
7 个页面 × 1500 与 375px 两种视口,见 §6.3.2)。
9. 已知限制与说明
-
Qlib 模型选股(Selection 模式 C)按需延后(v3 §14.1C):当前选股为条件(A)/因子评分(B); 模型预测选股(Alpha158 + LightGBM walk-forward,M8.4)为「按需」延后项 (见 docs/DEV_PLAN_v2.md §6.4)。
-
全市场选股为同步请求:无白名单的全市场选股约需 60s+;Agent 工具要求传
symbols白名单,全市场请在 Web 页执行(后续可迁异步 Job)。 -
数据规模:当前 MySQL 已同步全市场 + 退市股(stock 5903 只 = 在市 5565 + 退市 338、 stock_daily 约 805 万行/5786 只、adjust_factor 约 819 万行、daily_basic 约 800 万行 /1629 个交易日,2026-09 由 SQLite 迁移并校验一致)。
-
回测为近似建模:涨跌停按收盘相对上一有效收盘判定、成交假设调仓日收盘, Portfolio v1 仅等权(单股/行业上限约束字段已预留但未建模,设置后会在结果
unimplemented如实标注)(详见结果unimplemented)。 -
幸存者偏差(已修复):
- 退市股:
sync basic --include-delisted+ 定向sync daily,实测 338 只、24.0 万根; 时点股票池按delist_date正确纳入/排除(实测000005.SZ2024-04-26 退市: 2024-01-02 纳入 / 2024-06-03 排除)。 - ST/风险警示:
sync namechange(14,213 行名称生效区间,覆盖 1990-12 起),exclude_st在每个择股日按当时名称重判,与/api/selections同口径。 实测 2020-01-02 时点口径比快照口径多纳入 236 只(当时尚未 ST 的标的)。 案例全区间收益因此经历三次修正:+35.71%(最新名称快照)→ +32.01%(仅在池子基准日过滤) → +24.86%(逐择股日时点 ST,最终);即初始口径高估 10.85pp。 窗口内真实反例:000961.SZ阳光城 2023-05-05 起 ST、600466.SH中南建设 2024-04-24 起 ST,只按起始日过滤会让它们在变 ST 后仍被选中。 - 残余(如实标注):股票池的
min_listing_days/delist_date仍以回测起始日为基准 (池子不逐日重算),故「起始日为 ST、之后摘帽」的标的不会进入该次回测候选池; 名称历史未同步时自动回退最新名称并在name_basis标注point_in_time=false。
- 退市股:
-
老牌蓝筹缺 2020–2022 行情(已修复):实测缺口 20 只(含
600028.SH中石化、600900.SH长江电力、601398.SH工商银行等高股息主力),本地日线原自 2023-01-03 起; 已回补 19,419 根、数据现自 2019-01-02 起。另有 5 只(盈方微/盐湖股份/皇台酒业/ 深深房A/中毅达)是该区间的真实长期停牌(停牌期本就无行情),非数据缺失。 -
停牌无独立数据表:以「当日无行情」近似停牌(不可买不可卖),未接入 tushare
suspend_d明细(ARCHITECTURE.md§8 的suspend_data尚未落地),exclude_suspended仍未实现(结果unimplemented已标注)。 -
结果体积与完整存档:全市场多年回测的结果 JSON 可达数 MB,
experiment.result_json为MEDIUMTEXT(16MB)以容纳。- 个股收益曲线默认全量保存(此前只存收益绝对值最大的 60 只,是个真实缺陷):
实测案例 2020-01-01~2026-09-04 回测期内持有 135 只,归档
symbol_curves为 135 条、结果 1.86 MB,archive_meta记录{curves_stored:135, curves_total:135, truncated:false, budget_chars:12000000}。 - 上限可用
research.archive_curve_limit配置(默认不限制);只有当结果超出 体积预算(12,000,000 字符,为 MEDIUMTEXT 16MB 留余量)时才会按收益绝对值裁剪, 并同时写入archive_meta.truncated=true与unimplemented说明 —— 归档页会把「归档完整 / 归档不完整」直接标出来,不静默丢数据。 - 老归档(完整存档上线前)确实不完整:归档页对这类快照显示 「个股曲线只有 60 只(期内共持有 N 只)」并给出「以此参数重跑」入口。
- 归档列表保留策略:
python -m app.cli.prune_experiments --keep N(默认 dry-run, 加--apply才删除)。注意删除即失去结果(结果只存归档一份),建议先导出要紧的归档; 早期归档在job表另有副本,但新记录没有(job.result_json为 NULL)。 - 删除后的恢复路径:历史归档可用
python -m app.cli.restore_experiment_from_job --job-id <JOB> --code-version <rev> --apply按原 id 重建(spec/result 逐字复制、摘要按归档同口径重算、data_version留空不伪造); 新归档没有副本,工具会明确拒绝。操作细节见 §6.3.1。
- 个股收益曲线默认全量保存(此前只存收益绝对值最大的 60 只,是个真实缺陷):
实测案例 2020-01-01~2026-09-04 回测期内持有 135 只,归档
-
Agent 结论质量取决于模型:编排只保证「经受控工具 + 归档留痕」,研究有效性判断 需要人复核;接不同厂商模型请核对
config.yaml的base_url与model命名。 -
SSE 与 Job 为单进程本地执行:重启进程后未完成 Job 需重新提交(第一阶段刻意 不引入 Redis/Celery;本机 Redis 已就绪,出现排队/长任务需求时按 DEV_PLAN §7 接入)。
-
Qlib 数据为进程内单例初始化:
qlib.init以 provider_uri 为键幂等,切换不同 qlib_dir 会重新初始化;测试环境建议注入独立临时目录。 -
策略说明列宽 300(未加迁移):
strategy.description列是String(300),而describe_strategy生成的自动说明最长可达 313 字符 → 后端按 300 截断并加显式…, 前端表单同步maxLength=300并实时计数。要让长说明完整落库,需要单独做一次Model → Alembic(把列改Text)的 schema 变更。 -
异步选股不落库:
POST /api/selections/jobs只建 Job、不写selection_snapshot, 因此异步路径下/selection不渲染「按此条件回测」按钮(不给坏链接);同步POST /api/selections会落库并返回selection_id,可正常直通回测。 -
同步接口也会写归档:
POST /api/backtests与POST /api/factor-tests每次调用都会 新增一条归档(归档 id 见响应头X-Experiment-Id)。批量试参数会在库里留下大量归档, 用prune_experiments按--keep清理。归档失败不会吞掉计算结果(接口仍返回结果, 失败原因经响应头X-Archive-Error与日志如实暴露)。 -
归档的「数据版本」只对上线后产生的归档有效:老归档
data_version为空, 归档页显示「未记录(该归档早于数据指纹上线)」,并提示不要当作可严格复现的依据; 指纹形如d20260904;n≈7688k;a≈7922k;b≈7718k(d=最新交易日、n=stock_daily、a=adjust_factor、b=daily_basic,≈表示取自information_schema的近似行数)。 -
个股页「回测成交与信号」卡的数据边界:
GET /api/stocks/{symbol}/chart的fills/signals来自信号表与选股命中;回测成交要经/api/backtests/{experiment_id}/stocks/{symbol}/chart才有,因此在没有信号/命中记录时 该卡不渲染(个股页的 K 线、基本信息、复权切换不受影响)。
10. 常见问题(FAQ)
- 同步报「缺少 TUSHARE_TOKEN」:把 token 填入根目录
.env的TUSHARE_TOKEN。 - 同步报「未安装 tushare 客户端」:
cd backend && uv sync --extra datasource-tushare。 - Agent 报 400「未配置 LLM」:
.env设置LLM_API_KEY;换模型改config.yaml的agent.llm.model/base_url。 - 回测默认用哪个引擎?怎么切 Qlib 引擎:默认 LocalEngine(纯 pandas)。需要在代码
中向
ResearchService注入QlibEngine()启用 QlibDataset 数据管线(见 §6.1)。 - Parquet 导出文件在哪、怎么读:
data/parquet/stock_daily/<year>.parquet;pd.read_parquet(...)直接读取(pyarrow 已随依赖安装)。 - 前端连不上后端 / NetworkError:前端默认经 Next 同源代理访问
/api/*(next.config.ts rewrites →127.0.0.1:8000,可用BACKEND_API_URL覆盖), 后端 CORS 开发期为*。前端 dev server 监听0.0.0.0:3000(dev.sh传-H 0.0.0.0), 但访问来源必须在next.config.ts的allowedDevOrigins白名单内,否则其/_next/*请求会被 Next 15.5+ 拒绝(表现为页面白屏/资源 403);换机器访问时按需追加来源 IP。 若使用 SQLite 兜底库且日志出现database is locked,多半是正在跑全市场数据 同步(长写事务),同步结束后自动恢复(SQLite 连接已加 busy_timeout); 默认 MySQL 库无此问题。 - 数据库被改动想重置:默认库为本机 MariaDB(qlib@127.0.0.1)时重建库后
uv run alembic upgrade head(行情需重新同步或从备份恢复);若切回 SQLite 兜底库则删除data/quant.db后重建。 - 默认库已是 MySQL(config.yaml
database.mysql,密码在.env的MYSQL_PASSWORD); 想临时切回 SQLite:.env设DATABASE_URL=sqlite:///./data/quant.db。 业务层均无需改动(Repository / SQLAlchemy 已隔离方言差异)。