如果你炒股、或者只是跟着定投过日子,你一定见过这类文章:某条「跌破 200 日均线就减半仓」的规则,配上一条漂亮曲线,结论是「回撤小了一大截,收益还没少」。
问题在于,这类文章几乎从不告诉你:那条曲线是在哪个时间段、从哪一天开始算的。
今天这个项目,是我目前见过对这个问题最老实的一个。它叫 riskbound,一个 GitHub 上刚建仓一天的仓位规则工具,7 个 Star。它的独特之处不在规则本身(波动率目标 + 200 日均线,都是十几年老东西),而在于它把「哪些结论有证据、哪些没有」写进了每一次命令行输出里——它甚至主动把自己默认的那套参数标成「未检验」。
但它最扎我的一点是:我按它自己文档写的方法,把同一套规则换了四种起算方式重跑,它标为「通过」的那格结论,只有两种口径下站得住。
先说它是什么
一句话:你告诉它一只股票代码,它告诉你「按这条事先写死的规则,今天收盘后你该持有计划满仓的百分之多少」,然后附上一段证据说明——这段数字背后有哪些结论经过检验、哪些没有。
它只有两条规则,都是公开写法:
| 规则 | 一句话解释 |
|---|---|
| 波动率目标(V) | 最近 20 天的波动率高于长期中位数时降低仓位,低时满仓。意思是「市场越颠,你拿得越少」 |
| 200 日线过滤(F) | 收盘价在 200 日均线之上拿满仓,跌破就拿半仓 |
参数是这样冻结的:最近 20 个交易日的日收益波动(σ̂)、长期波动基准用「扩张中位数」(σ_tgt,需要累积 250 个有效观测才有定义)、200 日均线(SMA200)。仓位在当天收盘算好,第二天开盘执行——这条「不能用当天收盘价成交」的自我约束,是它整套证据可信的前提。
规模不大:Python 源码 2114 行、测试 2686 行(测试比源码还多)、文档 530 行,MIT 开源许可,一个版本号 0.1.0.dev0。建仓后一天里一共 4 个提交:2 个由作者本人写(建仓、修一个观测逻辑),2 个由 GitHub Actions 机器人自动提交(追加前瞻记录、记录推送)。
它凭什么说自己「有证据边界」
因为它把证据边界做成了产品的一部分。它的每一次输出,末尾都固定附一个证据块,我原文抄一段(跑它自带的 riskbound evidence 命令):
| 它输出的一行 | 意思 |
|---|---|
config_status: untested_parameters |
你当前这套参数,历史上没检验过 |
deviations: vol_floor=0.75 (descriptive comparison only) |
那个 0.75 的权重下限,只在「描述性对比」里出现过,不进检验 |
V2 ... -> passed |
波动率目标 V 比「同等仓位的买入持有」最大回撤更小:通过 |
V1 ... -> not established |
收益效率(Sharpe)改善:未确立 |
- VF: drawdown reduction -> never tested |
两条规则组合起来的回撤改善:从未检验 |
注意最后两行。它的默认配置是两条规则组合(叫 vf75),而「组合能不能降回撤」这一格,它自己在检验家族里根本没设。也就是说,它默认给你的那个数字,恰好是它最没有证据的那个。它还专门在 deviations 里写明:这个默认值来自一次「15 只事后选出的自选股」的描述性对比,不是检验。
我核对了这套文本与它文档的一致性:docs/evidence.md、中英文 README、给别的 AI 助手看的 skill 说明书,四份文件里的定级表数字与代码里的结论文本逐格一致,而且那份管文档的测试文件会真的去比对——我把 docs/evidence.md 里的 +5.92 改成 +5.02,测试立刻报「V2 那一格对不上」失败;我把代码里的一处数字改掉,文档测试同时红了 4 个。这个「文档和代码锁在一起」的做法,比它那两条规则本身更值得看。
我实测的第一件事:379 个测试全绿是真的
我在沙箱里把它拷下来跑了一遍它自己的测试。
| 项目 | 实测结果 |
|---|---|
| 测试总数 | 379 个,25.6 秒跑完,全绿 |
| 其中「反例测试」 | 139 个 |
| 那份管文档的测试文件 | 177 个(文档一致性 + 违禁词 + AI 说明书行为规则) |
| 黄金夹具 | tests/fixtures/golden_rules.json,700 KB,来自它私有回测代码的逐位导出 |
「反例测试」是它最讲究的地方:它不只测「正确的东西通过」,还测「错误的东西必须被拦下」。我确认这些反例是真在跑的——往 README 里塞一句「推荐买入」,那条管违禁词的测试立刻失败;这说明那个违禁词表(甚至会拦「买点」「会员」「课程」这类词)是真的生效,而不是写在文档里的漂亮话。
那个 700 KB 的夹具特别值得说。它导出的不是「结果差不多」,而是要求逐位相同(同一个浮点数一个 bit 都不能差),覆盖 5 条合成价格序列 × 11 个窗口 × 4 个预设 = 44 个用例(另加 1 条夹具结构断言)。它把自己的规则实现与一份私有回测代码锁成了「改一个字就红」。这解决了「用代码跑投资策略」这类项目的一个老大难问题:回测代码和实盘代码越写越不一样,最后没人知道线上跑的是什么。它选择了最笨但最可靠的办法——把两边锁死。
我还独立验证了它的前瞻记录(那个每天追加一行的 track/daily.jsonl):用公开行情接口自己重新下载日线,按它记录里的参数重新算了一遍当天 25 只股票的仓位。结果是 25 只的收盘价与它记录的逐位相同(连小数最后一位都一样);仓位值 20 只逐位相同,另外 5 只只差 1e-7 量级。这个尾数差异我追到了来源:那 5 只最近 20 天的波动率我算出来的值与它记录的完全一致,只有 200 日均线差了最后几位——说明差异出在更早那批复权价上(分红拆股会让历史复权价被回头改写,即所谓的「复权漂移」,它自己在提醒里也写了这一点),不是规则实现的问题。
我实测的第二件事:换个起算日,结论就翻
这一段是我写这篇文章的真正原因。
它自己的文档里其实留了一句极其关键的话,藏在「前提与提醒」的第 6 条:实时版把波动率基准的起点锚定在 2019-06-01(late 期回测的起点),而 early 期回测从 2014-06 起算——「两者不是同一条策略」。
这句话的意思是:同一个规则,换一个起点算起,它就不是同一条策略了。它把这条写在了提醒里,但没有给出具体数字。我把这个数字算出来了。
先说为什么起算日会影响结果:那条波动率规则要用到一个长期波动基准 σ_tgt,定义是从起点开始的「扩张中位数」,而且要求累积够 250 个有效观测才第一次有定义。没定义的时候,规则按满仓处理。所以「起点选在哪」直接决定了在市场最颠的那段时间里,规则到底是满仓还是减仓。
我按四种起算口径,用它自己公开发布的检验工具(riskbound.harness,也就是它开源出来的那套跑检验的外壳程序)重跑了 V 规则那条「通过」的结论。数据是公开行情,两期、25 只股票、成本 10 个基点、零现金收益,都与它发布的一致:
| 起算口径 | late 期最大回撤优势 | early 期最大回撤优势 | 定级 |
|---|---|---|---|
| ① 从数据起点 2014-06-02(它的检验工具默认契约) | +2.67pp | +5.92pp | 方向一致,符合预测 |
| ② 每期从该期起点算 | -0.85pp | +6.36pp | 无法估计(方向不一致) |
③ 固定锚 2019-06-01(它命令行 --preset v 的实时口径) |
+2.06pp | -0.76pp | 无法估计(方向不一致) |
| ④ 每期从该期缓存起点(它文档写的回测口径) | +2.06pp | +5.92pp | 方向一致,符合预测 |
| 它发布的数字 | +2.09pp | +5.92pp | 方向一致,符合预测 |
(pp = 百分点,+2pp 表示策略的最大回撤比对照小 2 个百分点。① 是四种里最宽松的一种:它让 early 期也从 2014-06 开始预热,相当于多给了一年多的「适应期」;照着它检验工具的文档直接用,得到的就是这一种。)
读法很清楚:四种口径里只有两种稳得住,另外两种一换就翻。 而它的实时版本用的那个锚(③,命令行和每天的前瞻记录都是这个锚)恰好落在「early 期翻车」的那一种上。它的文档口径(④)算出来与它发布的几乎一致(late 期差 0.03pp,early 期在它发布的位数上完全相同),所以我先排除了「它自己算错」这种可能——它的数字是自洽的。
问题出在口径本身没有被当成结论的一部分。
机制我也定位到了。以苹果为例,late 期窗口 1076 个交易日:
| 对比项 | 锚在 2019-06-01(它的实时口径) | 锚在 late 期起点 |
|---|---|---|
| late 期平均目标仓位 | 0.915 | 0.943 |
| 满仓天数 | 653 / 1076 | 778 / 1076 |
| 2022 年(这轮最大回撤发生的地方) | 平均 0.811 仓位,65 天满仓 | 平均 0.932 仓位,182 天满仓 |
2022 年是美股这轮最深的回撤(苹果一年内自峰值跌了 30.3%)。两种口径在那一年的平均仓位差了 12 个百分点。「回撤更小」这个结论,很大程度上取决于你 2022 年手里拿着多少——而拿着多少,取决于你把起算日放在哪。放在 2019 年,规则在 2022 年有一半时间在减仓;放在期初,规则前 250 个交易日按满仓算,正好错过了那一年最该减仓的时候。
这不是它算错了,是「结论的适用范围」被起算日偷偷切了一刀,而这一刀没有写进结论里。
顺手拿它的工具做了两个对照实验
它把检验工具开源了(就是上面那套外壳程序),说你可以拿它检验自己的规则。我把它用在了两件事上。
第一件,验证它自己划的边界。它的提醒第一条写着「只在美股科技大盘股日线上检验过,换行业不保证成立」。我换成 25 只非科技大盘股(消费、医药、金融、能源、工业、电信、零售),两期不变,用同一套工具重跑:
| 格子 | 科技 25 只(它发布的宇宙) | 非科技 25 只(我换的) |
|---|---|---|
| V 的回撤优势(late / early) | +2.06 / +5.92 → 方向一致 | -1.21 / +6.99 → 方向不一致 |
| V 的收益效率 Sharpe(late / early) | +0.042 / +0.063 → 无法估计 | -0.062 / +0.072 → 方向不一致 |
| F 的回撤优势(late / early) | +5.88 / +3.24 → 方向一致 | +1.47 / +3.91 → 方向一致 |
它那句话不算客气话:换宇宙之后,V 那条「通过」的结论直接翻转成「方向不一致」;F 的结论还在,但 late 期的优势从 +5.88pp 掉到 +1.47pp(约四分之一),early 期反而略升(+3.24 → +3.91)。
第二件,验证它说的「没检验」是真的没检验。前面表里有一行是「VF 组合的回撤改善:从未检验」。我用工具照它另外两格的定义补跑了这一格:结果 +4.14 / +5.41,看着挺好——但这一格本来就不在它的检验家族里,跑出来的数字不构成任何检验结论(它自己的规矩就是「看了结果再补格子」不算预登记)。我把这件事写出来只是为了说明:它说「从未检验」的时候,是没有藏着一张好看的结果不拿出来。
不完美的地方
这是一天的项目,7 个 Star。没有实盘记录,前瞻记录只有一天。它自己在提醒里写明:全部是历史回测、没有实盘记录;而且前瞻记录里只有「前瞻」状态的行才计入前瞻证据。我核了那批前瞻记录,25 行全部是「前瞻」状态,也就是每一行都有 GitHub 记录的推送时间撑着,不是发布后补造的。
「回撤优势的绝对值只有 2–6pp」。这是它自己写的。2 个百分点的回撤改善,配上「长牛里大概率跑输满仓」的代价,对绝大多数人来说不构成换策略的理由。它没把这句话藏起来。
证据链的上游不公开。它的定级表来自两份「私有研究档案」(2026-10-05),文档里写明「两份档案不随本仓库发布」。所以那 5 个格子的数字,我只能核到「代码里的文本 = 文档里的表格」,核不到「这个数字是怎么算出来的」。反过来,它至少在文档里说清了这一点,还给了复现路径(用它的检验工具跑你自己的规则)。
前瞻记录的观测天然落后一轮。 它的时间证明机制设计得挺讲究:提交时间可以被提交者随手改,所以它不用提交时间,只用 GitHub 记录的推送到达时间(带事件 id,第三方可以拿同一个接口独立核对),而且明说「每天必须运行一次,错过了不能事后补」。我实测这个判定是真的会咬:把覆盖这些行的观测去掉(只留建仓期那两条),那 25 行立刻全部从「前瞻」降级成「已公开但无推送观测」,永久不计入前瞻证据。有一处缺口是设计使然、不是隐瞒:观测记录里目前 4 条,而 GitHub 上共有 5 次推送——缺的那条是机器人自己那次提交的推送,一次运行看不到自己刚推上去的东西,要等下一次才补上。我手动补跑了一次它的观测命令,那条随即补上了。
它标为「通过」的那一格,不该被当成投资理由。结合我上面测出来的起算口径敏感性,这一格我个人的读法是:方向上可能有东西,但 2 个百分点的幅度配不上「拿它当依据换策略」这件事。
谁该看它,谁别拿它当决策工具
该看的:正在自己写回测、或者在做「用代码跑投资策略」这类小工具的人。它最值钱的部分不是那两条规则,而是三个工程做法——① 把「哪些结论有证据」做成机器可读的结构化输出,而不是写在文章里;② 把文档与代码锁在一起,改文档数字就红;③ 用 GitHub 的推送时间而不是提交时间来证明「何时公开」,并且明说「只追加可核验、不是不可篡改」。这三条都能直接搬到自己项目里。
别拿它当决策工具的:想用它决定明天买多少的人。理由有三个,且都是它自己写出来的——默认参数未检验、回撤优势只有 2–6pp、长牛里大概率跑输满仓。今天它给出的规则本身也没有新东西:200 日均线半仓、波动率目标,都是十年前教科书里的写法。
一个额外的小建议给作者:那 5 个格子的定级表建议把起算口径作为一列写进去(四种口径下分别是多少),或者至少把现在这个「early 期从 2014-06 起算、late 期从 2019-06 起算」的口径差异标注在定级表旁边。按我测的结果,口径敏感度和结论本身一样重要。
我的判断:这个 7 Star 的项目在「知道自己不知道什么」这件事上,比很多十万 Star 的项目做得都干净。但它自己的默认配置恰好落在它最没证据的那一格上,而它标为「通过」的那格结论又对起算日敏感——这两件事加起来,说明「有证据边界」这个框架本身是对的,但边界的粒度还差一层:口径也算边界的一部分,不能被省略。
关注 AI商业快讯,每天一篇 AI 热点深度解读。
相关阅读:laya 深度评测:7 天 2.35 万 Star 的决策引擎,6 张基准表我逐格复现,头条 0.766 却查无实据 · Hindsight 深度评测:4.6 万 Star 的 Agent 记忆系统,2538 条行为快照我重跑零处不一致,5 条日期误判却被冻在测试里