找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2546

积分

0

好友

334

主题
发表于 昨天 23:24 | 查看: 1| 回复: 0

向量化回测器因其极致的速度,在量化策略的快速迭代和参数扫描中扮演着不可替代的角色。它主要分为两大类:事件驱动型回测器(event-based)和向量化回测器(vectorised)。

事件驱动型非常复杂,但作为回报,它在成交价格(fills)和交易成本的仿真上可以达到非常高的保真度。而向量化回测器则极其简单,因此往往会对成交和交易成本做很多“宽松”的假设。

尽管存在这些缺陷,许多中频交易的大型对冲基金仍然在使用各种向量化回测器的变体,因为它们使用方便、回测速度极快。一个向量化回测器可以在1秒以内处理500只股票、10年的数据。同样的数据如果用事件驱动框架,可能需要15–30分钟。

下面将介绍如何用 Python 构建一个考虑滑点、买卖价差和交易成本的向量化回测器。

架构总览

向量化回测最核心的模式非常简单:一切都围绕“bar”来做。每个bar代表一个时间周期(日K就是一个交易日,小时K就是一小时)。

Python实现

下面是实现向量化回测器的完整工作代码

import numpy as np
import pandas as pd

# 1. 计算简单收益率(对多空都正确)
data['returns'] = data['price'].pct_change()

# 2. 生成信号(示例:SMA交叉)
data['SMA1'] = data['price'].rolling(20).mean()
data['SMA2'] = data['price'].rolling(50).mean()
data['position'] = np.where(data['SMA1'] > data['SMA2'], 1, -1)
data['position'] = data['position'].where(data['SMA2'].notna(), np.nan)

# 3. 信号延迟一期(至关重要!)
positions = data['position'].shift(1)

# 4. 计算策略收益率
data['strategy'] = positions * data['returns']

# 5. 计算资金曲线
equity_curve = (1 + data['strategy']).cumprod()

向量化回测的核心公式就是:

*strategy_returns = position.shift(1) returns**

前一时刻的位置 × 当前时刻的收益率

需要注意的几个关键点

为什么要shift(1)? 如果不移位,你就在用T时刻的信息去T时刻交易,这在真实交易中是不可能的(典型的前视偏差 / look-ahead bias)。信号通常在bar收盘时产生,代码假设你在该收盘价执行。如果实际上你要等到下一bar开盘执行,那就应该用开盘价,并计算open-to-open的收益率。这短短一行代码防止了回测中最致命的作弊行为

一期延迟永远正确吗? 对于流动性好的品种 + 日频,是的,通常下一个交易日就能成交。对于流动性差的品种,或是分钟/秒级高频,实际执行可能需要更长时间,这时就要用shift(2)甚至更多。原则是:延迟多少期 = 实际执行需要多少个bar

这个模式很容易扩展到多资产组合:把DataFrame的每一列当成一个资产,所有的运算都是按列广播的。仓位权重只是再乘一个权重列。向量化优势会随着资产数量线性放大:500个资产仍然是一次运算。

向量化的魔法

速度来源于 NumPy 底层用C语言实现。当你写position * returns时,NumPy不会用Python循环逐个元素计算,而是把整个数组一次性交给预编译的C函数,利用SIMD指令(单指令多数据)并行处理。因此几百万条数据、几百只资产、十几年历史,也能在1秒内完成。

这个速度优势在参数优化时会指数级放大。比如快线20种参数 × 慢线20种参数 = 400组组合。循环回测要跑400次;向量化框架可以把400组参数直接做成DataFrame的400列,一次性跑完。这一点连很多从业者都没意识到,所以特意在这里点出来。

加入交易成本

纯向量化回测忽略了执行成本,假设零摩擦成交。但真实世界必须考虑手续费、滑点、买卖价差

最基本的成本实现方式是统计仓位变化

cost_per_trade = 0.001  # 0.1%
costs = positions.fillna(0).diff().abs() * cost_per_trade
data['strategy'] = positions * data['returns'] - costs

你可以把cost_per_trade换成不同的值,也可以进一步叠加滑点成本冲击成本买卖价差成本。只要成本能表示成一个Series,就可以直接从毛收益里减掉。

滑点(Slippage)

滑点成本通常希望建模为:随着交易金额 / 当日成交额比例上升,成本从小到大指数级增加。举例:

  • 订单 < 5% ADV → 1个基点
  • 5–10% ADV → 5个基点
  • > 10% ADV → 10个基点甚至更高
adv = (data['volume'] * data['price']).rolling(20).mean()

这只是最简单的分档方式。真实的市场冲击模型文献非常多,这里就不展开了。

买卖价差(Bid-Ask Spread)处理

向量化回测只能模拟市价单在下一bar的开盘或收盘成交,无法精确模拟限价单(因为你不知道限价是否在bar内被吃掉,这是事后才知道的信息)。

对于市价单,你需要跨越价差。为什么通常用半个价差?因为中间价(mid price)大致在收盘价附近。买入时你要从中间价向上跨越到ask,付出半个spread;卖出时从中间价向下跨越到bid,也付出半个spread。所以可以简单地把半个spread加到成本里。

局限性

向量化回测是先算收益、再扣成本;事件驱动回测是边执行边扣成本

这导致一个关键区别:如果因为滑点太高,你的策略根本不会下这个单,向量化回测是模拟不出来的,因为成本是事后扣的,策略逻辑无法对执行成本进行条件判断

其他主要局限性包括:

  • 没有部分成交:假设你下的单全部成交,现实中大单经常只能部分成交。
  • 只能在bar边界做决策:无法模拟bar内部的止损、仓位限制等。
  • 路径依赖难处理:追踪止损、动态仓位调整等策略属于路径依赖,向量化很难优雅实现。

实际应对方案是混合工作流:

  1. 向量化做快速迭代和参数扫描(几千组参数几分钟跑完);
  2. 把有潜力的策略搬到事件驱动框架做最终验证。

把向量化回测当成初筛,事件驱动回测当成终验。想了解更多关于高效计算与数据分析的讨论,欢迎来 云栈社区 交流。

总结

向量化回测可以在1秒内处理500只股票10年的数据,非常适合快速迭代和参数搜索。核心公式是position.shift(1) * returns,但必须叠加交易成本、分级滑点、买卖价差,否则收益就是在做梦。有前景的想法一定要搬到事件驱动框架再验证一遍,别直接把钱扔进幻想里。




上一篇:企业级运维:为什么生产环境仍在使用CentOS 6/7等老版本Linux?
下一篇:TVM Virtual Thread优化共享内存Bank Conflict的原理与GPU实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-27 02:54 , Processed in 0.312563 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表