18. 回测框架:构建异常点检测策略回测引擎
做量化交易这些年,我越来越觉得回测框架就像是一把手术刀。刀好不好用,直接决定了你能不能精准地切开市场这团乱麻。今天咱们聊的,就是专门为波动率曲面异常点检测量身打造的回测引擎。
说实话,市面上现成的回测框架很多,比如Backtrader、Zipline。但为什么我还要自己搭一个?因为异常点检测策略太特殊了。你想想看,普通策略关注的是价格涨跌,而我们关注的是曲面上的「畸形」——那些不该出现的凸起或凹陷。通用框架处理不了这种三维结构的数据。
18.1 回测引擎的核心设计思路
我个人习惯把回测引擎拆成三个模块:数据层、策略层、风控层。数据层负责喂入波动率曲面数据,策略层负责检测异常点并生成信号,风控层负责控制仓位和止损。
这里有个关键点:异常点检测策略的回测,不能像普通策略那样按「日线」或「分钟线」来跑。为什么?因为波动率曲面是动态更新的,每个期权到期日、每个行权价都在变化。我建议按「事件驱动」的方式来设计——每当曲面数据更新,就触发一次检测。
核心设计原则:
- 数据驱动:曲面数据到达即触发回测
- 状态隔离:每次回测独立,不污染历史数据
- 可插拔:异常点检测算法可以随时替换
18.2 数据层:波动率曲面的输入与清洗
数据层是回测引擎的「胃」。吃进去的东西不干净,后面全白搭。我在项目中遇到过最头疼的问题——曲面数据里混入了「伪异常点」。比如某个深度虚值期权,因为流动性差,报价直接跳了10个vol。这种点不是真正的市场异常,而是数据噪音。
所以数据层要做两件事:
- 曲面重构:把离散的期权报价,通过插值方法(比如样条插值)还原成连续的曲面
- 噪音过滤:剔除那些明显由流动性不足导致的异常报价
class VolSurfaceDataLoader:
def __init__(self, data_path):
self.data_path = data_path
self.surface_cache = {}
def load_and_clean(self, timestamp):
# 加载原始期权报价
raw_data = self._load_raw(timestamp)
# 剔除流动性不足的合约(成交量 < 100手)
clean_data = raw_data[raw_data['volume'] >= 100]
# 插值重构曲面
surface = self._interpolate_surface(clean_data)
return surface
def _interpolate_surface(self, data):
# 使用双三次样条插值
from scipy.interpolate import RectBivariateSpline
# ... 插值逻辑
pass
避坑指南:我曾经因为没做流动性过滤,回测结果看起来年化收益30%,实盘一跑直接亏了5%。原因就是那些「异常点」根本没法成交。记住:回测里能检测到的异常点,实盘里不一定能交易。
18.3 策略层:异常点检测与信号生成
策略层是整个引擎的大脑。它负责接收清洗后的曲面数据,运行异常点检测算法,然后输出交易信号。
我常用的检测方法有两种:
- 局部离群因子(LOF):适合检测曲面上的孤立异常点
- 孤立森林:适合检测成片的异常区域
但这里有个坑:你不能直接对整个曲面跑LOF。因为曲面的维度太高(到期日×行权价),直接跑会陷入「维度灾难」。我的做法是——先降维,再检测。
class AnomalyDetector:
def __init__(self, method='lof'):
self.method = method
self.model = None
def detect(self, surface):
# 降维:提取主成分
from sklearn.decomposition import PCA
pca = PCA(n_components=10)
reduced = pca.fit_transform(surface)
# 异常点检测
if self.method == 'lof':
from sklearn.neighbors import LocalOutlierFactor
self.model = LocalOutlierFactor(contamination=0.05)
labels = self.model.fit_predict(reduced)
elif self.method == 'isolation_forest':
from sklearn.ensemble import IsolationForest
self.model = IsolationForest(contamination=0.05)
labels = self.model.fit_predict(reduced)
# 将异常标签映射回原始曲面
anomaly_map = labels.reshape(surface.shape)
return anomaly_map
注意:降维会丢失信息。如果你发现检测结果总是漏掉某些异常点,试试保留更多主成分。我一般保留能解释95%方差的主成分数量。
18.4 风控层:仓位管理与止损逻辑
风控层是回测引擎的「刹车」。没有它,再好的策略也会翻车。异常点检测策略有个特点——信号频率低,但一旦出现信号,波动往往很大。所以仓位管理要格外小心。
我设计的风控逻辑很简单:
- 单笔仓位上限:不超过总资金的5%
- 止损线:异常点消失或亏损达到2%即平仓
- 相关性限制:同一板块的异常点信号,总仓位不超过10%
| 风控参数 | 默认值 | 说明 |
|---|---|---|
| 单笔仓位上限 | 5% | 防止单次异常点信号导致过大亏损 |
| 止损线 | 2% | 异常点消失后市场可能快速回归 |
| 最大持仓数 | 5个 | 避免同时持有过多异常点头寸 |
18.5 回测引擎的完整流程
好了,三个模块都讲完了。把它们串起来,就是完整的回测流程。我画了一张图,方便你理解整个数据流向:
这张图展示了数据从输入到输出的完整路径。你注意看那个虚线反馈循环——回测结果会反过来优化参数。我一般会跑三轮:第一轮用默认参数,第二轮根据结果调整检测阈值,第三轮微调风控参数。
18.6 实战:跑一个完整的回测
最后,咱们用代码把整个引擎跑一遍。假设我们有一周的历史曲面数据:
# 初始化回测引擎
engine = BacktestEngine(
data_loader=VolSurfaceDataLoader('data/vol_surface.csv'),
detector=AnomalyDetector(method='isolation_forest'),
risk_params={'max_position': 0.05, 'stop_loss': 0.02}
)
# 运行回测
results = engine.run(
start_date='2024-01-01',
end_date='2024-01-07',
initial_capital=1000000
)
# 输出结果
print(f"总收益率: {results['total_return']:.2%}")
print(f"最大回撤: {results['max_drawdown']:.2%}")
print(f"夏普比率: {results['sharpe_ratio']:.2f}")
print(f"交易次数: {results['trade_count']}")
跑完之后,你会发现一个有意思的现象:异常点检测策略的胜率通常不高(可能只有40%),但盈亏比很高(能达到3:1以上)。为什么?因为真正的市场异常点很少出现,但一旦出现,往往意味着巨大的定价错误。
核心要点回顾:
- 回测引擎要按事件驱动设计,不能按固定时间间隔
- 数据清洗比检测算法更重要——垃圾进,垃圾出
- 降维是必须的,但要注意信息保留率
- 风控层要严格,异常点策略的波动性远超普通策略
嗯,回测框架就聊到这儿。记住一点:回测只是起点,不是终点。再漂亮的回测曲线,也抵不过实盘的一次滑点。下一章咱们会深入聊聊如何用这个框架做参数敏感性分析——说白了,就是看看你的策略到底有多「皮实」。
公众号:蓝海数据掘金营,微信deep3321