用 pre-commit 给量化脚本上「提交门禁」:ruff + pytest 的十分钟落地实践
数据流水线能自动跑,不代表代码能放心改。这篇文章记录我给本地量化复盘项目加提交门禁的过程:让每次
git commit都自动跑一遍静态检查和核心测试。
一、为什么量化脚本特别需要门禁
量化项目有个特点:代码量不大,但改动的破坏力不小。改一行指标计算的边界条件,可能让过去两年的回测结果全变,而且不会报任何错——它只会安静地给出一个不同的答案。
我踩过的三个坑比较典型:
- 调试代码混进仓库:回测时临时写的
print(df.head())、pd.set_option('display.max_rows', None)忘记删,日志里全是噪声; - 依赖顺序被破坏:数据清洗函数在指标计算之后调用,本地跑得通只是因为当时缓存了中间文件,换台机器直接崩;
- 改坏了没人发现:没有测试覆盖的情况下,一次「顺手优化」可能让某个因子的空值处理逻辑失效,几周后才在实盘数据上暴露。
这几类问题有个共同点:它们都能在提交之前被拦住。所以我把检查放进 Git 的 pre-commit 钩子,而不是等到 CI 报错再回头改。
二、十分钟落地:配置文件
先安装并启用:
pip install pre-commit ruff pytest
pre-commit install # 写入 .git/hooks/pre-commit
核心是仓库根目录的 .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
args: [--fix] # 自动修可修的问题
- id: ruff-format # 统一格式,替代 black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-added-large-files
args: [--maxkb=512] # 别把行情 CSV 提交进来
- repo: local
hooks:
- id: pytest-core
name: pytest core
entry: pytest tests/core -q
language: system
pass_filenames: false
stages: [pre-commit]
第一次跑建议全量扫一遍,之后每次提交只检查改动的文件:
pre-commit run --all-files
三、几个值得注意的细节
1. ruff --fix 改完文件不等于通过。 ruff 修完会直接改磁盘上的文件,然后返回非 0 让提交失败,这是故意的——它要你确认自动改动后再重新 git add。刚上手时容易误以为「怎么修了还报错」。
2. 测试钩子用 local + language: system。 这样 pre-commit 不会去建独立虚拟环境,直接复用你当前环境里的 pytest 和项目依赖。代价是换机器要自己保证依赖装好,换来的是执行速度和依赖一致性,本地项目我觉得划算。
3. pass_filenames: false 很关键。 默认 pre-commit 会把改动文件名作为参数传给钩子,pytest 会把它当成测试用例路径去匹配,结果要么报「file not found」,要么干脆一条都没跑。加上这个参数,钩子就只执行 entry 里那条命令。
4. check-added-large-files 对量化项目是刚需。 日线数据、因子快照这类文件动辄几十 MB,一旦提交进去,git clone 会变得极慢,后面清理历史也很麻烦。数据应该走对象存储或者干脆脚本重新生成,不要进仓库。
5. 紧急情况可以跳过,但要留痕。 git commit --no-verify 能绕过钩子,我的原则是只在改文档、改注释这类零风险操作上用,涉及计算逻辑一律不跳。宁可多等三十秒,也不要赌一次「应该没问题」。
四、小结
加了门禁之后最直接的变化不是「代码变漂亮了」,而是改核心逻辑时的心理负担小了很多。提交前自动跑一遍核心测试,相当于给每次改动垫了一层安全网,出问题也能立刻定位到是哪次提交引入的。
整套配置写下来不到十分钟,之后基本零维护,只需要在 rev 里偶尔升级一下版本号。如果你的项目也是「小代码、高风险」的类型——量化脚本、数据处理流水线、运维工具都算——强烈建议试一试。
- 点赞
- 收藏
- 关注作者
评论(0)