一个字母的差距,让我的策略在市场暴跌时毫无反应
> 本文是「量化踩坑实录」系列的第五篇。前四篇聊了复权、未来函数、持仓错配、资金流,这篇讲一个更隐蔽的坑——隐蔽到排查了整整一个下午才发现。
## 数据告诉我反转检测没触发过
5月28号,我在审查策略的风控模块。策略里有一道防线叫”大盘反转检测”——当沪深300或中证500日内从高点回落超过一定幅度,说明市场在掉头,策略应该自动减仓。
def check_index_reversal(context):
“””如果沪深300或中证500从日内高点回落超阈值,触发超级卖出”””
current = data[idx].close
day_high = data[idx].high
if (day_high – current) / day_high > REVERSAL_THRESHOLD:
写完之后我一直没太关注这个模块。它属于”保险”——正常市况下不该触发,只有市场急转直下时才起作用。策略整体收益不错,+2064%(本地回测),我就默认它在正常工作。
但那天的审计让我想确认一下:这个反转检测到底触发过几次?
六个持仓周期,26次本应触发的情境(后来才知道),0次触发。
## 明明有回撤,为什么没检测到?
我开始翻交易记录。2024年2月28号,策略持有3只ETF,当天中证500从高点跌了超过2%。按策略规则,应该触发减仓。但日志显示那天只卖了一只——是因为MACD死叉,不是反转检测。
那就是说反转检测在这个交易日没看到中证500的下跌。
我去查那天的分钟数据。中证500的1分钟K线在我本地磁盘上,NPZ文件,2024年2月28号,240根1分钟线,`high`, `low`, `close` 全有。数据没问题。
那问题一定在策略读数据那块。我再去看`data[idx].close`拿到的到底是什么。
log.info(f'[REVERSAL-DEBUG] {idx}: has_data={idx in data}, ‘
f’close={data[idx].close if idx in data else “MISSING”}’)
[REVERSAL-DEBUG] 000905.SS: has_data=False, close=MISSING
[REVERSAL-DEBUG] 000905.XSHG: has_data=False, close=MISSING
[REVERSAL-DEBUG] 000905.SH: has_data=False, close=MISSING
[REVERSAL-DEBUG] 399905.SZ: has_data=False, close=MISSING
log.info(f'[REVERSAL-DEBUG] available keys containing “905”: ‘
f'{[k for k in data.keys() if “905” in k]}’)
[REVERSAL-DEBUG] available keys containing “905”: [‘000905.SZ’]
不是`.SS`,不是`.XSHG`,不是`.SH`,不是`399905.SZ`。
## 为什么我用.SS/SH,数据却是.SZ?
这不合理。中证500显然是上海市场的指数,代码后缀应该是`.SS`或者`.XSHG`才对。深交所的指数代码是399开头的。
中证500(000905)是上交所指数。按规范,在PTrade里应该是`000905.XSHG`,在TDX格式里应该是`000905.SS`。
所以我在策略里写了四个版本做fallback:`.SS`, `.XSHG`, `.SH`, `399905.SZ`。我以为万无一失。
这就要说到分钟线数据源的差异了。PTrade研究环境可以批量拉分钟线,但当时我用的本地数据来自通达信(TDX)。TDX的分钟线文件里,**中证500的代码是`000905.SZ`**。
为什么TDX把上交所指数标成`.SZ`?不知道。可能是历史原因,可能是数据提供商自己的规则。管它什么原因——数据就这么标的。
而我的fallback列表里,恰好漏了`000905.SZ`这个组合。
## 这意味着什么
← 静默跳过
… 检测逻辑
四次for循环,四次`continue`。然后返回`False`。
这不是指数下跌没触发止损——是指数下跌了,策略根本不知道指数在哪儿。
## 修复
简单。在CSI_INDICES列表最前面加上`000905.SZ`:
之前:凭记忆写后缀
CSI500_CODES = [‘000905.SS’, ‘000905.XSHG’, ‘000905.SH’, ‘399905.SZ’]
之后:本地数据文件的真实格式排第一位
CSI500_CODES = [‘000905.SZ’, ‘000905.SS’, ‘000905.XSHG’, ‘000905.SH’]
missing = [idx for idx in CSI_INDICES if all(v not in data for v in idx)]
log.error(f’【致命】风控指数无数据:{missing},可用代码:{[k for k in data if any(c in k for c in [“300″,”500″,”905”])]}’)
## 补了之后效果
改完重新跑回测。反转检测触发了18次(之前26次里有一些被MACD死叉先出货了),最大回撤从29.5%降到了23.7%。
不是策略逻辑的问题。不是因子的问题。不是参数的问题。
就是**数据源的股票代码格式和策略里硬编码的不一致**,导致整个风控防线形同虚设。
## 这个坑的根
1. **不同数据源用不同后缀规则。** PTrade OHLCV日线用`.XSHG`/`.XSHE`,TDX分钟线用`.SS`/`.SZ`。同一只股票在不同数据源里,代码可能不同。
2. **指数后缀最坑。** 股票好歹有个规律——上交所6开头→`.SS`,深交所0/3开头→`.SZ`。指数没有这个规律。中证500代码000905,看起来像深交所代码(0开头),实际是上交所指数。TDX给了它`.SZ`,PTrade给了它`.XBHS`。
3. **静默跳过是最大的风险。** 如果反转检测直接崩了抛异常,我早就发现了。它选择了静默跳过——不报错、不警告,安安静静地什么都不做。这才是最危险的。
## 经验
**第一:永远不要凭记忆写股票代码后缀。** 拿到一份新数据,先用`data.keys()`扫一遍实际的代码格式。写了一百次代码也敌不过一次数据源换了。
**第二:fallback列表第一位放最可能匹配的格式。** 不是随便排——先去数据文件里确认。特别是跨数据源使用时(OHLCV来自PTrade、分钟线来自TDX),后缀对齐是第一优先级。
**第三:风控模块不能用静默跳过。** 找不到关键数据(指数、基准)不能默默continue过去,必须报错。风控是最后一道防线,它失效了策略还在跑,就是裸奔。
现在就去做一件事:打开你的分钟线数据文件,grep一下策略里用到的所有股票代码,看后缀对不对得上。这个检查30秒。
关注公众号「量化交易者」,这个系列会持续更新策略开发中踩过的坑。
(本文为知识科普,不构成投资建议。量化交易存在风险,回测结果不代表未来表现。)