策略规格书:一句话策略有 16 种合法读法
同一句"5日上穿20日买入",最好的读法年化 3.74%,最差 -0.06%——而 16 种全部跑输买入持有
"5 日均线上穿 20 日均线买入,下穿卖出。"
这句话听起来精确得像一行代码。但它不是一个策略,是一族策略。
因为它没说:均线是 SMA 还是 EMA?上穿要不要确认?买多少仓位?对什么价取平均?
四个问题各有两种都合法的答案 → 16 种实现。同一份数据、同一个引擎跑完:
最好读法 SMA|确认1日|仓位100%|hlc3 年化 3.74% 期末 1,783,804
最差读法 EMA|确认2日|仓位100%|hlc3 年化 -0.06% 期末 990,332
读法差异 = 3.80 个百分点年化
而买入持有是 4.40%——16 种读法一种都没跑赢。
这一节讲的就是这件事:在你写下任何参数、做任何优化之前,这 3.80 个百分点已经存在了。
菜谱写"盐少许"。
十个人做出来是十道菜,而且他们都没做错——"少许"本来就没有唯一解。
区别在于:做菜错了你当场尝得出来,策略错了要几个月甚至几年才知道,而且你会以为那是市场的问题。
一、四个维度,16 种读法
每个维度都是原句没说清楚的,而两种读法都合法:
| 维度 | 读法 A | 读法 B | 原句哪里没说 |
|---|---|---|---|
| 均线类型 | SMA | EMA | "均线"没说是哪种 |
| 信号确认 | 当日站稳即可 | 连续 2 日站稳 | "上穿"要不要确认 |
| 仓位 | 100% | 50% | "买入"没说买多少 |
| 价格基准 | 收盘价 | (H+L+C)/3 | "均线"没说对什么价取平均 |
实测(sh.600000,2010-01-04 ~ 2026-07-29,3976 个交易日,初始资金 100 万):
| 读法 | 年化% | 最大回撤% | 夏普 | 期末权益 |
|---|---|---|---|---|
| SMA|确认1日|仓位100%|hlc3 | 3.74 | -34.8 | 0.291 | 1,783,804 |
| SMA|确认1日|仓位100%|close | 2.98 | -37.9 | 0.252 | 1,590,083 |
| SMA|确认1日|仓位50%|hlc3 | 2.29 | -18.8 | 0.292 | 1,428,893 |
| EMA|确认1日|仓位100%|close | 2.08 | -39.7 | 0.203 | 1,383,631 |
| …(中间 11 种略) | ||||
| EMA|确认2日|仓位100%|hlc3 | -0.06 | -42.6 | 0.085 | 990,332 |
参照:同期买入持有年化 4.40%。
16 种读法的最好那一种(3.74%)仍然低于买入持有。
二、哪个维度最要命?
把 16 条结果按维度分组求均值:
| 维度 | 读法 A 均值 | 读法 B 均值 | 均值差 |
|---|---|---|---|
| 均线类型 | SMA 2.34% | EMA 0.86% | 1.49 pp |
| 信号确认 | 确认1日 2.09% | 确认2日 1.11% | 0.98 pp |
| 仓位 | 100% 1.86% | 50% 1.34% | 0.51 pp |
| 价格基准 | close 1.55% | hlc3 1.64% | 0.09 pp |
贡献最大的是"均线类型"——而这恰恰是最少被写进策略描述里的一项。
大部分人会认真讨论"用 5/20 还是 10/30",却默认"均线"就是 SMA。在这四个维度里,这个从没人提的默认值,影响是最大的。
注意口径:这四个维度都是原句没说清楚的东西。本实验没有改动 5/20 这两个参数——参数敏感性是 p5.4 的题目,这里比的是"同一套参数下,读法能差多少"。
三、不是个例:8 只股票
| 标的 | 最好年化% | 最差年化% | 极差 pp | 买入持有% |
|---|---|---|---|---|
| sh.600000 | 3.74 | -0.06 | 3.80 | 4.40 |
| sh.600009 | 0.69 | -1.81 | 2.51 | 3.31 |
| sh.600010 | 8.36 | 0.97 | 7.38 | 2.07 |
| sh.600011 | 2.30 | -6.01 | 8.30 | 2.72 |
| sh.600015 | 1.77 | -1.47 | 3.24 | 4.54 |
| sh.600016 | 1.13 | -2.39 | 3.52 | 2.67 |
| sh.600018 | 1.30 | -1.06 | 2.36 | 2.57 |
| sh.600019 | 0.51 | -3.44 | 3.95 | 1.93 |
| 中位数 | 3.66 |
"我挑最好的那种读法"——这句话本身就是 p3.4 讲过的多重检验。
你有 16 种读法,挑出年化最高的那一条,然后说"这个策略年化 3.74%"。
这和从 16 个随机策略里挑最好的一个,在统计上是同一件事:你报告的不是这条策略的期望表现,而是 16 次抽样的最大值。
p3.4 的结论在这里直接适用:报告"我试了 16 种,最好的是 X"比报告"X 的年化是 3.74%"诚实得多——前者读者能自己打折,后者不能。
而本例里更尴尬的是:打完折之后,16 种全部跑输买入持有(4.40%)。
🔧 动手做:量一量"没写清楚"值多少钱(5 分钟)
跑 python spec_ambiguity.py——同一句话策略,脚本枚举了 4 个维度的 16 种合法读法,全跑一遍。
你会看到(实测):8 只股票上,同一句话的读法年化极差中位数 3.66 个百分点。
想明白:在你写下任何参数、做任何优化之前,光是"没说清楚"就已经制造了 3.66pp 的分歧。你以为在调策略,其实一半的收益差来自那些你没写明的默认选择(下单时点、确认规则、价格基准)。所以:先写规格书,再写代码。
四、解法:规格书是一个可执行对象
规格书不是一段散文,是一个能哈希、能复现、能自检的对象:
@dataclass(frozen=True)
class StrategySpec:
# ① 数据口径
symbol: str; start: str; end: str
adjust: str = "后复权"; source: str = "baostock"
# ② 信号定义
ma_type: str = "SMA"; fast: int = 5; slow: int = 20
price_base: str = "close"; confirm_days: int = 1
# ③ 进场
entry: str = "fast>slow"
entry_timing: str = "信号次日开盘" # 引擎结构保证(p4.3)
order_type: str = "市价单"
# ④ 出场
exit: str = "fast<=slow"
stop_loss: str = "无" # 没有也必须显式写「无」
take_profit: str = "无"
# ⑤ 仓位
target_weight: float = 1.0; max_weight: float = 1.0
rebalance: str = "信号变化时"
# ⑥ 成本
commission_bp: float = 2.5; stamp_tax_bp: float = 5.0; slippage_bp: float = 5.0
# ⑦ 异常处理
on_suspended: str = "不成交,顺延至下一可交易日"
on_limit: str =
on_insufficient_cash: =
on_missing_data: =
() -> :
blob = json.dumps(asdict(), ensure_ascii=, sort_keys=)
hashlib.sha256(blob.encode()).hexdigest()[:]
七类必答问题
| 类别 | 少写会怎样 | |
|---|---|---|
| ① | 数据口径 | 前复权还是后复权,结果完全不同(p1.3) |
| ② | 信号定义 | 就是本节那 16 种读法 |
| ③ | 进场 | "买入"在收盘价还是次日开盘?p4.1 实测差异可达数十个百分点 |
| ④ | 出场 | 没有止损也必须写"无",否则读者会自己补一个 |
| ⑤ | 仓位 | 满仓和半仓的回撤差一倍(-34.8% vs -18.8%) |
| ⑥ | 成本 | p4.2 实测:成本假设值 74 个百分点 |
| ⑦ | 异常 | 停牌/涨跌停/没钱怎么办,不写就是"静默跳过"(p4.1 那个 bug) |
规格 → 结果,中间没有自由裁量
规格指纹 fingerprint = fc64bd6362ab
同一份规格跑两次:1,783,804.11 / 1,783,804.11 净值序列逐位相同 True
改一个字段,指纹就变,结果必须重跑:
| 改动 | 新指纹 | 期末权益 | 相对基准 |
|---|---|---|---|
ma_type → EMA | 8388597a4bad | 1,214,129 | -569,675 |
confirm_days → 2 | 0746520602ec | 1,536,114 | -247,690 |
target_weight → 0.5 | 18577270a5d6 | 1,428,893 | -354,911 |
指纹的作用不是防伪,是防止你自己记混。三个月后你看到一条净值曲线,唯一能确认"它是用哪份规格跑的"的东西,就是这十二个字符。
五、先写规格,再写代码
顺序不能反。理由不是"工程规范",是本节这张表:
如果你先写代码,那 16 种读法里你选了哪一种,是被你当时的手感决定的——而你不会记得。
📌 免责:本课为技术教学,标的为沪深300 成分股按代码序取样(非精选),所有回测为历史模拟,16 种读法均为演示口径,不构成投资建议。
三件事:
- 一句话不是策略,是一族策略。四个维度各两种合法读法 → 16 种实现,年化从 3.74% 到 -0.06%,极差 3.80 个百分点;8 只股票上的极差中位数 3.66 pp。而 16 种全部跑输买入持有(4.40%)
- 贡献最大的维度是"均线类型"(1.49 pp)——恰恰是最少被写进策略描述、最容易被默认掉的那一项
- 规格书是一个可执行对象:七类必答问题(数据/信号/进场/出场/仓位/成本/异常)、规格指纹、改一个字段指纹就变。没有止损也必须显式写"无"
外加一条统计纪律:"我挑最好的那种读法"就是 p3.4 的多重检验。报告"我试了 16 种,最好的是 X",比报告"X 年化 3.74%"诚实得多。
下一节把镜头拉远:五个策略族(趋势跟随/均值回归/时序动量/波动率目标/买入持有)在同一批标的上跑,再按市场状态拆开——你会看到,"熊市表现好"这句话,用事后标签和用交易时能拿到的标签,得出的是两个不同的结论。
🔎 来源与核验· 4 条,点开核对
