前阵子发了个小频率动量策略,当时回测看着还行,但实盘一跑就开始露问题。 最近刚好在看《打开量化投资的黑箱》第九章,且根据 v 友建议,顺手把自己那一套研究流程从头捋了一遍,相当于是给之前那个策略“补课”。
- 策略起点 之前做动量,基于一个很直觉的想法: 👉 涨得多的继续涨 当时没太多犹豫,直接写代码+回测。 现在回头看,更像是一个“没被明确表达的假设”。 现在会刻意把这一步写清楚一点:
是基于经济逻辑? 还是纯数据挖出来的?
简单给自己加了一条约束: if 没有清晰逻辑: 回测再好也不直接用
- 数据 之前策略有个问题其实一直没认真处理数据。 现在回头看,这一块基本决定了上限。 ( 1 )幸存者偏差 用当前成分股回测历史,这个坑就不展开了。 ( 2 )时间对齐 之前直接拼不同频率数据,本质就是未来函数。 现在我给自己加了个很简单的检查: 👉 每一份数据都问一句:当时能不能拿到 顺手也把数据源换成了直接拉 API ,至少链路是“接近实盘”的: import requests import pandas as pd
API_KEY = "YOUR_ALLTICK_API_KEY"
def get_price_data(symbol="AAPL"): url = "https://api.alltick.co/marketdata/stock/history" params = { "symbol": symbol, "start_date": "2020-01-01", "end_date": "2023-01-01", "interval": "1d", "apikey": API_KEY } res = requests.get(url, params=params) data = res.json()["data"] df = pd.DataFrame(data) df["date"] = pd.to_datetime(df["date"]) return df
df = get_price_data()
基础清洗
df = df.sort_values("date") df["close"] = df["close"].fillna(method="ffill") df = df[df["volume"] > 0]
这块其实没什么技术含量,但好处是: 👉 回测用的数据结构,跟以后实盘用的是同一套来源 3. 回测:开始老老实实加“现实约束” 之前那版动量策略的问题是: 👉 默认理想成交 现在基本都会强制加:
成本 简单滑点 信号延迟
举个最简单的均线例子: df['ma20'] = df['close'].rolling(20).mean()
df['signal'] = (df['close'] > df['ma20']).astype(int)
df['returns'] = df['close'].pct_change() df['strategy'] = df['signal'].shift(1) * df['returns']
cost = 0.0005 df['strategy_net'] = df['strategy'] - cost * df['signal'].diff().abs()
一旦把成本加进去,很多策略当场变脸。 现在对回测的理解更偏向: 👉 这是一个“历史条件下的生存测试” 4. 参数优化:开始刻意“反着来” 之前是找最优参数,现在是故意破坏它: for p in [17, 20, 23]: 跑一下
不稳的策略,一动就崩。 再加一个基本操作: train = df[df['date'] < '2021-01-01'] test = df[df['date'] >= '2021-01-01']
test 基本只看一次。 慢慢变成一个很简单的判断:
能扛参数扰动的 → 再看 需要精调的 → 基本放弃
这一轮下来,有个挺明显的变化: 👉 不太会被“完美回测”骗了 以前是:
曲线好看 = 可以上
现在更像是: if 解释不了 + 不抗扰动: 默认不可用
有点反直觉的是: 能用的策略变少了, 但心里反而更踏实一点。