这个徽章的含义是窄的:它只代表不碰数据库的那组单元测试在干净机器上通过了。 需要
data/market.db的真数据自检不在 CI 里跑(跑了必然 skip,而跳过不是通过)。详见 §四。
许可:本仓库公开,但不是开源项目。 公开仅供阅读、学习与评估; 未授予任何使用、复制、修改、分发的许可(详见
LICENSE)。 研究结论与证据链欢迎引用(注明出处),代码本身不授权再分发或商用。
一个以「证伪」为目标的量化研究项目:不是找能赚钱的策略,而是建立一套能判定一个想法是真是假的可复现流程,然后用它把流行想法逐个检验一遍。
零依赖、断网可开 —— 没有 CDN、没有外链图片、没有一行脚本,一个 HTML 文件自带全部图表。 因为
data/*.db不进版本库(可再生),任何人 clone 下来是跑不了任何脚本的; 这个页面就是给那一刻准备的。内容:六次策略否证的现场 → 第二轮的 6 个参数格 → 工程正确性(记账 / 无前视 / 数据源)→ 真跑的自检结果。 (第七次否证是方法论层面的,不在这个页面里 → 见 §二 末)
重新生成:
.venv/bin/python demo/build_demo.py(会真跑 6 份自检、读脚本自报的检查数,并回查页面里每一个数字 —— 抠不到就拒绝生成)
规模:5,620 行 Python(scripts/ + strategies/)| 25 个脚本 | 6 份自检脚本(91 项 + 4 组,实测全部通过)| 12 份报告 | 2 份事前登记 | 6 张数据表(122,694 行)
| 是 | 不是 |
|---|---|
| 一套可复现的回测工程(份额模型记账 + 严格无前视 + 成本按实际换手) | 一个「能赚钱的策略」 |
| 一套事前登记 → 执行 → 自检 → 判定的研究流程 | 一个信号订阅服务 |
| 7 次否证的记录(6 次策略 + 1 次方法论路径,含证据与边界) | 一个投资建议 |
核心主张:「我证否了什么」比「我发现了什么」更可信 —— 因为前者可复现,后者大概率是过拟合。
前六次否的是「某个策略有没有用」;第七次否的是一条「怎么找策略」的方法论路径。 两者性质不同,不要混为一谈 —— 下表只放前六次(都是策略),第七次单独见本节末。
| # | 被检验的想法 | 判定 | 关键证据 |
|---|---|---|---|
| 1 | 双均线择时 | 否证 | 23 组参数仅 4 组跑赢(17%);决定成败的是交易次数(相关 −0.668),不是在场时间 |
| 2 | 双均线(样本外) | 否证 | 样本内最优参数在样本外跑赢的标的数 0/3;「跑赢比例」57%/70% → 0%/0% |
| 3 | 双均线(滚动前进) | 否证 | 5 段×3 标的:15 段赢 1 平 7 输 7,0/3 跑赢,平均每年少赚 12~15pp |
| 4 | 趋势跟踪 | 否证 | 4 标的 20/20 参数格全输;成本降到 0 仍输 3.00pp ⇒「是费用磨掉的」不成立 |
| 5 | 横截面动量(A股行业 20 只) | 未发现 | 判据 3 差 0.6pp(94.4% vs 95%) |
| 6 | 横截面动量(换 universe 重跑) | 否证 | 三条判据全挂;★ 上一轮的「几乎通过」没重现(+9.24% → +2.82%) |
唯一站得住的:再平衡降回撤(四资产 12/12 起步回撤改善)—— 价值是压波动,不是增收益。
第 5 项「差 0.6pp」如果没有事前写死判据,几乎必然会被讲成一个发现。第 6 项换一批评分标准重跑,同一个方法结论就翻了 —— 这就是**「事后挑选」的实测代价**。
→ 完整记录:reports/2026-09-25_横截面动量检验结论_第二轮.md
前六次问的都是「这个策略有没有用」。第七次不是。
| 被否的 | 「让 LLM 生成因子表达式,从 A 股价量数据里挖出新信息」这条路(含「再生成 30 个就有新东西」这个数量假设) |
| 实验 | 30 个 LLM 生成的候选因子一次性冻结 → 不看收益做结构去重(横截面 Spearman,|r|>0.7 判同一维度)→ PCA 交叉验证 |
| 实测 | 13~15 个独立维度(我先预判 ≤3~5 → 预判被推翻);435 对里只有 21 对(4.8%) 超 0.7 |
| 单个最有价值的发现 | VWAPDEV_21(听起来是独立的「量价」因子)是 BIAS_21 乖离率的 0.974 复制品 —— 不看收益看不出来 |
| 核心结论 | 独立维度数 = 独立检验次数 ⇒ 15 个维度不是 5 次检验,是 ≈15 次;α 要压到 0.0033。挖得越多,门槛越高 ⇒ 结构问题,不是实现问题 |
| 为什么停 | 要区分「真独立」还是「纯噪声」必须算 IC —— 那是这条方向的红线,所以停在这 |
| 没否掉的 | LLM 会写因子(能力是真的,只是不够);LLM 做信息处理、做文本因子(需文本数据)一轮都没碰过 |
它仍是「否证」,不是「失败」:被证否的是一条具体做法, 不是「投资不能量化」,也不是「LLM 碰不了金融」—— 同一顶帽子前六次也戴过。
完整记录(含两条我自己披露的分析缺陷:单链接链式串联、阈值敏感) →
作品集/证伪记录/第七次_LLM挖因子.md原始脚本与输出存档 →作品集/证据/2026-09-28_LLM因子结构去重/
| 文件 | 行数 | 作用 |
|---|---|---|
scripts/backtest.py |
290 | 单标的事件驱动引擎(信号延后一天 + 成本 + 买入持有对照) |
scripts/cross_section.py |
226 | 横截面排序引擎(月度调仓、top/bottom/等权/随机四种选手) |
scripts/portfolio.py |
403 | 份额模型组合引擎(时变目标权重 + 5 种再平衡规则 + 现金收益) |
scripts/trend_follow.py |
468 | 统一组合引擎 simulate_combo(择时 × 再平衡四格对比) |
scripts/walk_forward.py |
354 | 滚动前进框架(训练/验证分段,支持锚定扩展) |
- 份额模型:
shares[i] = w[i] / 首价,每日V = Σ shares × 价,权重自己漂 —— 不能用「对日收益取平均再复利」,那等价于每天偷偷再平衡。 实测证据:真实「一直持有」回撤是 −23.7%,不是错误算法的 −15.9%;且回撤峰值日不同(06-12 vs 06-08)。 - 成本按实际换手:
cost = 2 × 买入额 × 成本bp(买卖双边),不是拍一个固定值。 - 严格无前视:信号只用
t及之前的数据,收益取t → t1。 - 跨引擎不可比:
run_backtest的买入持有第 0 天空仓,份额模型当天建仓,差 2.49% ⇒ 策略与基准必须在同一引擎内比。
- 三次换源(yfinance → 东财 push2his → 腾讯 qfq),每次都记录为什么换与怎么发现它坏了:
- Yahoo 在本网络 429/403
- 东财被 SNI 层掐断(TLS 能通、之后服务端直接断流;IPv4/IPv6/三镜像全一样)
- 新浪不可用 = 不复权丢分红 —— 用货币 ETF 做试金石实测差 41.38pp,若不验这一步,「配债是拖累」的反向结论就会被写进报告
- 工程质量:分页(单次上限 800,靠 end 日期翻页)、限速 4 秒、断点续传、新表隔离(绝不覆盖已发布报告依赖的表)、WAF 501 立即停手不重试
- 字段序实测确认:
[日期, 开, 收, 高, 低, 量]—— 第 3 位是收盘 - ★ 第二个数据源(币安公开归档 →
crypto_prices):A 股日历在这个仓库里是硬编码的、从未被第二种市场检验过,所以拿一个 7×24 市场来压它。币安提供逐文件.CHECKSUM(腾讯源从来没有),218 个归档逐个核过 sha256 —— 本仓库第一次能证明「这串字节是原样的」。代价与边界见 §七。- ★ 字段序陷阱:币安是
[开, **高, 低, 收**],腾讯是[日期, **开, 收, 高, 低**, 量]—— 后三位正好反序。项目里有四份复制粘贴的腾讯解析器,照搬过来实测 3302/3302 根全违反 OHLC 自洽(正确序 0/3302)。 - ★ 时间戳单位中途变过:币安在 2025-01 把 kline 的
open_time从毫秒(13 位)静默换成微秒(16 位)。坏在语义上、响应 200、zip 能开、CSV 看着完全正常 —— 所以取数器按位数分派并加三道独立守卫。 ⚠️ crypto_prices.date是 UTC 日历日,而daily_prices是美股交易日、etf/xs/xs2_prices是上海交易日 ⇒ 跨市场按date直接 join 不是日历对齐的。这条写进 docstring 与meta,也写进 §七。
- ★ 字段序陷阱:币安是
实测全部通过(退出码全 0) —— 数量取自脚本自身的汇总输出,不是静态数出来的:
| 脚本 | 检查数(自报) | 覆盖 |
|---|---|---|
audit_backtest.py |
14 项(4 组) | 引擎正确性、成本单调、无前视 |
audit_cross_section.py |
9 项 | K=1 退化对撞、等权对撞、无前视(双向) |
audit_portfolio.py |
19 项 | 闭式解对撞、多路径逐位相同、无未来函数 |
audit_trend.py |
16 项 | 合成序列退化测试、干旱期、四格对比 |
audit_walk_forward.py |
4 组(组内按标的多项) | 分段无前视(双向) |
audit_crypto.py |
33 项 | 第二数据源:校验和溯源、7×24 日历(含 A 股阳性对照)、年化常数低报、跨市场缺口语义 |
★ 无前视的验证范式(两条缺一不可):
把未来价格改得面目全非后重跑 → ① 之前的段必须逐位相同(否则用了未来数据) ② 之后的段必须确实变了(否则这个测试是空的)
★ 闭环对撞而非「看着差不多」:同一逻辑两条路径实现,比到 0.00e+00 / 4.44e-16 这一级。
★ 报告里的每个数字另写脚本回查(如 verify_xs2_report.py,32/32)。
★ 作品集文档里的数字同样回查:作品集/verify_claims.py(79/79,它会真跑一遍上面 6 份自检并读脚本自报的数 —— 静态数正则不准,各脚本命名与循环调用都不一样)。
其中一条检查专查这个数本身:README 与差距表里写的项数必须等于实测值,两处都要对 ——
这个数原来一直没人查,于是 README 停在三年前的 43/43、差距表停在 32/32,没有任何东西会因此报错。
prereg/ 事前登记(假设/参数格/判据/结论可能性,全部在取数之前提交)
↓ git 提交时间戳 = 「参数先于结果」的唯一证据
scripts/ 执行
↓
audit_* 自检(含双向无前视)
↓
reports/ 结论(含边界、失败项、未做的事)
只想看结果:双击 demo/index.html —— 零依赖,不用装任何东西。重新生成它:
.venv/bin/python demo/build_demo.py # 真跑 6 份自检 + 回查页面里每个数字(约 2 分钟)
.venv/bin/python demo/build_demo.py --fast # 跳过跑自检,复用上次页面里的结果跑完整项目(下面每一步都实测跑过,不是示意):
cd ~/quant-project
python3 -m venv .venv # 报 ensurepip is not available 的话见下面 ⚠️
# 装依赖 —— 两条路等价,任选一条(**都要在仓库根目录执行**):
# ① 不装包,只装依赖(最省事)
.venv/bin/pip install -r requirements.txt # 只有 numpy + pandas(运行项目要的)
.venv/bin/pip install -r requirements-dev.txt # 只有 pytest(**跑测试**才要的;只用项目可跳过)
# ② 装成包(等价,顺带把 scripts/ strategies/ 装成可 import 的包、元数据进标准位置)
.venv/bin/pip install -e ".[dev]"
# ① 一键跑全部检查(推荐)—— 单元测试 + 下面那 7 份自检
.venv/bin/python -m pytest # 全量。约 5 秒
.venv/bin/python -m pytest -m "not slow" # 只跑单元测试:**不碰数据库,clone 下来立刻能跑**
# ⚠️ 没取数时那些自检会显示 skipped 而不是 failed ——
# "跳过"不是"通过",看到 skipped 就先把 ② 跑了。
# ② 取数。data/*.db 不进版本库(可再生),所以 clone 下来必须先跑这一步。
# 五个脚本写**五张不同的表**,各自独立 —— 缺哪张,用到它的脚本就退不出来。
.venv/bin/python scripts/fetch_data.py # → daily_prices backtest / walk_forward / audit_backtest / audit_walk_forward
.venv/bin/python scripts/fetch_etf.py # → etf_prices portfolio / trend_follow / audit_portfolio / audit_trend
.venv/bin/python scripts/fetch_xs.py # → xs_prices cross_section / audit_cross_section
.venv/bin/python scripts/fetch_xs2.py # → xs2_prices 第二轮的横截面(audit_xs2 用)
.venv/bin/python scripts/fetch_crypto.py # → crypto_prices 币安公开归档(audit_crypto 用)。约 6600 行 / 34 秒
# ⚠️ 需要联网。**别大范围反复抓** —— 实测被腾讯 WAF 按 IP 封过接口。
# fetch_crypto 走的是 data.binance.vision 的静态归档,不是交易 API,请求之间也留了间隔。
# ③ 跑检验
.venv/bin/python scripts/backtest.py
.venv/bin/python scripts/walk_forward.py
.venv/bin/python scripts/portfolio.py --robust
.venv/bin/python scripts/trend_follow.py
.venv/bin/python scripts/cross_section.py
# ④ 自检:手动逐份跑(`pytest` 里包的就是这些,想单独看某一份的输出时用)
.venv/bin/python scripts/audit_backtest.py # 4 组共 14 项
.venv/bin/python scripts/audit_cross_section.py # 9 项
.venv/bin/python scripts/audit_portfolio.py # 19 项
.venv/bin/python scripts/audit_trend.py # 16 项
.venv/bin/python scripts/audit_walk_forward.py # 4 组
.venv/bin/python scripts/audit_crypto.py # 33 项 —— 以上 6 份合计 91 项 + 4 组
.venv/bin/python scripts/audit_xs2.py A # 8/9(★ 见下)
.venv/bin/python scripts/audit_xs2.py B # 9/9
# ④b 只有这一步会联网:重下真字节,端到端复核时间戳单位边界(约 2 秒)
.venv/bin/python scripts/audit_crypto.py --rescan-formatCI(GitHub Actions)跑的是什么 —— 边界写清楚:
.github/workflows/test.yml只跑上面那条pytest -m "not slow",也就是不碰数据库的那组单元测试。 需要data/market.db的真数据自检(7 份audit_*.py)不在 CI 里跑 —— CI 机器上没有data/*.db(不进版本库),跑了必然 skip,而跳过不是通过。作品集/verify_claims.py同理进不去(它开头就连data/market.db,且写死.venv/bin/python)。 ⇒ 那个绿勾的含义是窄的、但很硬:一台干净机器上,clone 下来开箱可跑的那部分确实是绿的。 ★ 反过来,开发机上跑出来的不算数 —— 那台机器的.venv里躺着 26 个包,其中 24 个是历次实验的残留。
★
audit_xs2.py A是 8/9,那一项不过是有记录的已知状态,不是待修的缺陷。 根因:sh513660在2019-05-31缺一根(源数据缺口)。引擎对缺价按 0.0 收益计, 而独立重算要求三期都有价 ⇒ 那一期两边各算各的。 缺口在收益端、不参与排序,不改变任何一期的持仓;上界< 0.01pp/年,不可能翻转任何判据。 完整根因与影响面见reports/2026-09-25_横截面动量检验结论_第二轮.md§六。tests/test_audits.py把这条钉死成"恰好挂这一项" —— 换成别的项挂会失败(那是新问题), 变成 9/9 也会失败(那说明该回去改报告那一节了)。
⚠️ 建 venv 时报ensurepip is not available? 那是系统缺python3-venv包 (Ubuntu 最小化安装默认没带),不是本项目的问题。 正解:sudo apt install python3.14-venv(版本号对齐python3 -V的输出),再重建。 本项目开发机当时正是这个情况、且没有管理员权限,所以.venv是python3 -m venv --without-pip .venv建出来、再手动把 pip 装进去的 —— 那条路能走通,但只在拿不到 sudo 时才用,正常机器按上面三行就能建好。
只想跑一部分? 每张表是独立的:只要有
daily_prices就能跑audit_backtest, 不必把五个取数脚本全跑一遍。
⚠️ 但别只跑audit_portfolio/audit_trend/audit_cross_section这三份 —— 那样合计只有 44 项,而 README 开头写的是 91 项 + 4 组(来自全部 6 份)。
关于依赖:运行只要 numpy + pandas,跑测试才要 pytest。
现在有三份声明(requirements.txt / requirements-dev.txt / pyproject.toml)——
看着冗余,但多一份清单 = 多一个会静默漂移的地方,所以 verify_claims.py 第【5.7】节
把它们含版本范围逐字对撞(numpy>=2.0 改成 numpy>=1.0 也会红),
外加原有 5 个方向(代码真 import 的 ↔ 两份 requirements,不许少、不许多、不重叠)。
关于「可安装」:pip install -e ".[dev]" 实测可装(quant-project-0.1.0,
装完可从任意目录 import scripts.cross_section)。
⚠️ 但「装了就不用 cd」是假的 —— 别信。 实测从/tmp跑 7 份自检:audit_backtest/audit_crypto/audit_portfolio/audit_trend/audit_walk_forward本来就过; 只有audit_cross_section/audit_xs2挂在unable to open database file。 (audit_crypto过是因为它拿的库路径是从__file__推的绝对路径 —— 这不是「修好了」,只是它没染上那个病。) 根因不是sys.path.insert,而是cross_section.py里的data/相对路径—— 而那个文件的字节被 sha256 冻结(d58e088bd2e2e56b,报告用它证明「结论出自哪一版引擎」), 改不动:一改,报告里那句「引擎 hash 冻结」就变成假话。 ⇒ 仍然请在仓库根目录按上面的路径跑。 这是可复现性冻结的真实代价, 不是没做完的待办。tests/test_engine_freeze.py把那个冻结值钉住了,防止它被悄悄改掉。
⚠️ 若pip install -e .卡在下载setuptools:那是本机连不上 PyPI (本仓库开发机实测如此,WSL 也够不着 Windows 的代理),不是打包写错了。 有网机器上它自己会装;没网时可用 ① 那条路。
| 目录 / 文件 | 用途 |
|---|---|
demo/ |
3 分钟入口(自包含单页 + 生成器;页面进版本库,clone 即可看,不需要数据库) |
tests/ |
pytest 测试(不碰数据库的单元测试 + 把 7 份 audit_*.py 包起来的真数据测试) |
pyproject.toml |
打包与依赖声明(pip install -e ".[dev]" 可装;依赖与 requirements*.txt 逐字对撞,由 verify_claims.py 第【5.7】节守) |
data/ |
SQLite 行情库(数据库不进版本库:可再生、每次取数都变) |
scripts/ |
全部引擎、取数、自检、诊断脚本 |
strategies/ |
策略定义 |
prereg/ |
事前登记(先于取数提交) |
reports/ |
结论报告(含边界与失败项) |
作品集/ |
作品集材料:能力差距表、面试准备 · 证伪记录/(第七次否证) · 证据/(原始脚本与输出存档) |
logs/ |
运行日志 |
signals/ |
每日信号 |
risk/ |
风控模块 |
risk/目录是本项目的安全底线:任何自动生成的代码都不得写入或改写该目录。 该约束通过 Claude Code 的 PreToolUse 钩子(~/.claude/hooks/gate-rules.json)在工具层强制,不依赖自觉。- 数据库不进版本库,避免
git status长期脏 → 人就会学会忽略它。 - 新数据一律写新表,绝不改已发布报告依赖的表。
- 结论与自检脚本一起提交,报告里的数字可被脚本回查。
- 没有基本面数据(只有价格)⇒ 价值/质量类因子从未被检验过(是「没做」,不是「否了」)
- 样本期短、重叠严重 ⇒ 分辨不出 ±5pp 的真效应
- universe 由人选定 ⇒ 存在幸存者偏差
- 未做容量 / 执行 / 冲击成本建模
- ★ 不做加密交易,一条也不做:不注册账号、不申请 API key、不下单、不碰资金。
crypto_prices只用于压这个仓库自己的日历假设,不是交易系统的前身。⚠️ 边界是「我们自己守的」,不是网络拦的 —— 实测data.binance.vision可达, 而api.binance.com/fapi/stream在本机超时,但testnet.binance.vision/api/v3/ping返回 200。 所以不能拿「反正连不上」当免责:连得上,是选择不去碰。 - ★ 跨市场不能按
date直接 join:crypto_prices是 UTC 日历日(7×24 无假日,3302 根 = 3302 天), 而daily_prices是美股交易日、etf/xs/xs2_prices是上海交易日。三个日历互不对齐。 - ★ 年化常数
TRADING_DAYS = 252是写死的,且刻意不改:在 7×24 数据上它让年化波动/夏普 低报 16.94%(比值√(252/365.2425) = 0.8306)、CAGR 低报(收益 > 1 时);最大回撤不受影响。 实测同一条 BTC 序列:波动 55.68% → 67.04%、夏普 0.681 → 0.819、CAGR 24.86% → 37.96%。 不改的理由:一改就会动到每一个已发布的数字,那是另一件事、要另一次事前登记。 这一条由audit_crypto.py第【3】组守着(恒等式断言到 1e-12)。 顺带一条只报告的观察:A 股本仓样本实测约 243 根/年,252本身就高约 3.7%。 - 月档滞后:币安当月归档不生成,所以覆盖到上一个月末为止(当前
2026-08-31),而日档已到昨天。已知边界,不是 bug。 - ★ 首根不是 2017-08-01:BTC/ETH 现货都是 2017-08-17 上线(3302 根)。取数器卡的是「首根落在 2017 年内」这个双边区间 —— 上界挡归档静默缺失,下界挡时间戳单位算错(后者会把整段推到 1970 年,而五条常规校验一条都不响)。
- 所有历史结论不代表未来,本项目不构成任何投资建议