1. 为什么A股回测绕不开事件驱动这套机制
做量化回测的人,迟早会撞上一堵墙:用向量化方式跑出来的收益曲线漂亮得不像话,一上模拟盘就原形毕露。这个问题在A股尤其突出,因为A股的交易规则里藏着几个向量化框架很难处理的硬约束——T+1、涨跌停、停牌、集合竞价,还有分红送转带来的复权跳变。你如果只是拿收盘价做矩阵运算,这些细节全被抹平了,回测结果自然失真。
RQAlpha这个框架,就是冲着解决这类问题去的。它的核心设计思路是事件驱动,而不是很多人习惯的向量化。所谓事件驱动,说白了就是把回测过程拆成一个个按时间顺序发生的事件:行情来了、订单下了、成交回报到了、日终结算了。框架维护一个事件队列,按时间戳依次处理,每个事件触发对应的回调函数。这种机制天然适合处理A股那些"条件触发"的逻辑,比如"涨停板不成交""停牌期间不能下单""T+1当天买入的不能卖"。
我第一次接触RQAlpha是在做一个多因子选股策略的时候。当时用向量化框架跑出来的年化收益有30%多,换到RQAlpha上重跑,直接掉到18%。一开始以为是框架有问题,后来逐笔对账才发现,向量化版本在涨停日依然按收盘价买入了,而RQAlpha正确地判定为无法成交。这12个点的差距,就是事件驱动带来的真实性。
这篇文章适合几类人看:一是刚接触量化、想搞明白回测框架底层逻辑的新手;二是从向量化框架迁移过来、被A股规则坑过的老手;三是想基于RQAlpha做二次开发、需要理解其架构设计的工程师。我会从核心机制讲到实操细节,把我在实际使用中踩过的坑和总结的技巧都摊开来说。
2. RQAlpha的事件循环到底在转什么
2.1 事件队列的运转逻辑
RQAlpha的引擎本质上是一个离散事件模拟器。它把回测区间内的每一个交易日、每一个时间切片都抽象成事件,然后按时间顺序推入队列。核心事件类型包括:
- BEFORE_TRADING:盘前,通常用来做选股、生成目标持仓
- BAR:行情切片到达,分钟回测下每分钟触发一次,日回测下每天触发一次
- AFTER_TRADING:盘后,用来做结算、记录净值
- SETTLEMENT:日终结算,处理持仓盈亏、资金变动
- ORDER_PENDING_NEW / ORDER_CREATED / ORDER_CANCELLED:订单生命周期事件
- TRADE:成交回报事件
这个顺序不是随便定的。盘前事件先跑,你在这个阶段算出今天要买什么;然后行情事件逐个到达,策略根据最新价格决定是否下单;订单事件触发撮合逻辑;盘后做结算。整个流程像一条流水线,每个环节的输入输出都是明确的。
我见过有人把选股逻辑写在BAR事件里,结果每分钟都在重新选股,性能直接崩掉。正确的做法是把选股放在BEFORE_TRADING,BAR里只做下单判断。这个区分很关键,因为BEFORE_TRADING一天只触发一次,BAR在分钟回测下一天要触发240次。
2.2 撮合引擎的A股适配
RQAlpha的撮合引擎是它区别于通用回测框架的核心。它内置了A股的交易规则:
| 规则 | 处理方式 | 影响 |
|---|---|---|
| T+1 | 买入持仓当日不可卖 | 日内策略无法实现 |
| 涨跌停 | 触及涨跌停价时订单不成交 | 涨停追入无法成交 |
| 停牌 | 停牌期间订单挂起或拒绝 | 持仓无法调整 |
| 最小交易单位 | 买入100股整数倍 | 小资金组合受限 |
| 印花税/佣金 | 按配置扣除 | 高频策略成本敏感 |
这些规则在向量化框架里通常需要手动处理,而在RQAlpha里是引擎自动执行的。你不需要在策略代码里写"如果涨停就不买",撮合引擎会帮你判断。但这也意味着你得理解它的判定逻辑,否则会出现"明明信号触发了却没成交"的困惑。
举个例子,RQAlpha判断涨停的逻辑是:如果当前BAR的最高价等于涨停价,且你的买入价格大于等于涨停价,则判定为无法成交。这个逻辑在日回测下有个细节——它用的是当日最高价来判断是否触及涨停,而不是收盘价。所以有些股票盘中触及涨停但收盘回落,RQAlpha依然会判定你的涨停价买单无法成交。这个设计偏保守,但更接近真实情况,因为涨停板上的排队单确实很难成交。
2.3 数据模型与复权处理
RQAlpha的数据层用了一个叫DataProxy的抽象,把行情数据、财务数据、分红除权数据统一封装。你在策略里调用self.get_bar()或者bar_dict[order_book_id],拿到的都是经过复权处理的价格。
复权这块是A股回测的重灾区。前复权、后复权、不复权,三种方式算出来的收益率完全不同。RQAlpha默认使用后复权价格,也就是以上市首日为基准向后调整。这样做的好处是历史价格不会因为新的分红而改变,回测结果可复现。但如果你要对比实盘价格,需要自己做转换。
我在实际使用中发现一个容易忽略的点:RQAlpha的复权因子是动态计算的,当你回测区间跨越了分红除权日,框架会自动调整持仓成本和当前价格。这意味着你不需要在策略里手动处理分红,但如果你自己记录了买入成本,可能会和框架的持仓成本对不上。建议始终用self.position来获取持仓信息,不要自己维护成本价。
3. 从零搭一个能跑的策略:模块拆解与代码落地
3.1 策略骨架的四个必备方法
RQAlpha的策略文件遵循固定的接口约定,核心是四个方法:
def init(context): # 策略初始化,只执行一次 context.stock = '000001.XSHE' context.buy_price = None def before_trading_start(context): # 盘前,每个交易日执行一次 pass def handle_bar(context, bar_dict): # 行情事件,分钟或日频触发 pass def after_trading_end(context): # 盘后,每个交易日执行一次 passinit里做的是全局配置,比如设定股票池、初始化变量。注意这里的context是一个全局对象,你在其他方法里对它的修改会持久化。我习惯把所有状态变量都挂在context上,避免用全局变量,这样多策略并行时不会互相污染。
handle_bar是核心,所有交易逻辑都在这里。bar_dict是一个字典-like的对象,用bar_dict[order_book_id]可以拿到当前BAR的行情。这里有个性能陷阱:如果你在handle_bar里遍历整个股票池去取行情,分钟回测下会非常慢。正确的做法是在before_trading_start里把当天要关注的股票列表算好,handle_bar里只处理这个列表。
3.2 下单接口的选型与参数
RQAlpha提供了几个下单函数,常用的有:
order_shares(id_or_ins, amount):按股数下单order_lots(id_or_ins, amount):按手数下单order_value(id_or_ins, cash_amount):按金额下单order_percent(id_or_ins, percent):按总资产比例下单order_target_percent(id_or_ins, percent):调仓到目标比例
新手最容易犯的错是用order_value时传了一个超过可用资金的数值,结果订单被拒绝。RQAlpha不会自动帮你截断,它会在日志里报一个OrderRejected。我建议在实盘策略里始终用order_target_percent,因为它会自动计算需要买卖的数量,避免手动算错。
还有一个细节:order_shares的amount参数,正数表示买入,负数表示卖出。但卖出时如果数量超过持仓,会被拒绝。如果你不确定持仓数量,先用context.portfolio.positions[id].quantity查一下。
3.3 滑点与手续费的配置
回测的真实性很大程度上取决于滑点和手续费的设置。RQAlpha的配置在config.yml里:
base: slippage: 0.002 commission_multiplier: 1.0 tax_multiplier: 1.0slippage是滑点比例,0.002表示千分之二。A股的实际滑点因股票流动性而异,大盘股可能只有千分之一,小盘股可能到千分之五。我一般对沪深300成分股设0.001,对中证500设0.002,对小盘股设0.003。
手续费方面,RQAlpha默认佣金是万分之三,印花税是千分之一(卖出时收取)。这个默认值偏保守,实际券商佣金可以谈到万分之二甚至万分之一。但回测时我建议用偏高的值,这样策略上线后不会因为成本超预期而亏损。
注意:滑点的计算方式是在成交价上加上一个偏移量,买入时加,卖出时减。这意味着滑点对高频策略的伤害是线性的,交易越频繁,成本越高。
4. 那些文档里不会写的踩坑记录
4.1 停牌股票的持仓处理
A股停牌是常态,尤其是2015年之后。RQAlpha对停牌的处理是:停牌期间行情事件依然会触发,但价格不变,且订单无法成交。这会导致一个问题——你的策略可能在停牌期间反复发出卖出信号,但一直无法成交,日志里全是OrderRejected。
我踩过的坑是:在handle_bar里没有判断停牌状态,导致停牌股每天触发几十次下单尝试,回测速度被拖慢了好几倍。后来加了一个判断:
if bar_dict[stock].is_trading == False: returnis_trading这个属性在文档里提得不多,但非常实用。它返回False表示当前BAR该股票不可交易(停牌或未上市)。加上这个判断后,回测速度提升了30%以上。
4.2 分红除权导致的持仓数量跳变
这个坑更隐蔽。假设你持有某股票1000股,某天它10送10,你的持仓会变成2000股,但成本价减半。RQAlpha会自动处理这个调整,但如果你在策略里硬编码了持仓数量,就会出错。
我的做法是:所有涉及持仓数量的计算,都用context.portfolio.positions[stock].quantity动态获取,绝不缓存。另外,在after_trading_end里打印持仓时,要注意除权日的数量变化,否则会以为框架算错了。
4.3 分钟回测的内存爆炸
分钟回测的数据量是日回测的240倍。如果你回测3年、股票池有500只,内存占用会非常可观。RQAlpha默认会把所有数据加载到内存,这在股票池大的时候会直接OOM。
解决方案有两个:一是用--stock-pool参数限制股票池,只回测你真正关注的股票;二是用RQAlpha的--data-bundle机制,把数据预编译成二进制格式,减少加载时间。我实测下来,预编译后加载速度能提升5到10倍。
还有一个技巧:如果策略只需要日频数据,就不要用分钟回测。很多人为了"更精确"选择分钟回测,但实际上如果策略逻辑是日频的,分钟回测除了慢没有任何好处。
4.4 回测结果与实盘的偏差来源
即使RQAlpha已经处理了大部分A股规则,回测和实盘之间依然会有偏差。主要来源有:
- 成交假设:回测假设你的订单能以当前BAR的价格成交,但实盘中大单会冲击价格
- 信息延迟:回测中你用的是当前BAR的收盘价,实盘中你是在收盘前下单,价格可能已经变了
- 涨跌停排队:回测中涨停不成交,但实盘中如果你排队早,可能成交
- 数据质量:回测用的历史数据经过清洗,实盘数据可能有缺失或错误
我的经验是:回测收益打个七折,再考虑实盘。如果打七折后还能接受,这个策略才值得上。
5. 把RQAlpha用出花:进阶配置与扩展思路
5.1 自定义数据源的接入
RQAlpha默认使用米筐的数据格式,但你可以接入自己的数据。核心是实现一个BaseDataProxy的子类,覆盖get_bar、get_fundamentals等方法。我做过一个项目,把本地CSV数据接入RQAlpha,大概花了半天时间。
关键点是数据格式要对齐。RQAlpha期望的BAR数据包含open、high、low、close、volume、total_turnover等字段,且索引是order_book_id和datetime。如果你的数据格式不同,需要在DataProxy里做转换。
5.2 多策略并行回测
RQAlpha支持在一个进程中跑多个策略,通过--strategy参数指定多个策略文件。但要注意,多个策略共享同一个数据源和事件队列,如果策略之间有状态依赖,可能会互相干扰。
我的做法是:每个策略用独立的context,不共享任何变量。如果确实需要共享数据,通过文件或数据库传递,不要用全局变量。
5.3 与Backtrader的对比选型
很多人会拿RQAlpha和Backtrader比。我的看法是:
| 维度 | RQAlpha | Backtrader |
|---|---|---|
| A股规则适配 | 内置,开箱即用 | 需要自己实现 |
| 数据源 | 绑定米筐 | 灵活,支持多种 |
| 社区活跃度 | 国内为主 | 国际为主 |
| 学习曲线 | 中等 | 较陡 |
| 扩展性 | 中等 | 高 |
如果你主要做A股,RQAlpha的规则适配能省你很多事。如果你做多市场或者需要高度定制,Backtrader更合适。我自己是两个都用,A股用RQAlpha,港股美股用Backtrader。
5.4 实盘对接的注意事项
RQAlpha本身是回测框架,不直接支持实盘。但它的策略代码可以迁移到实盘系统,只要实盘系统提供类似的接口。迁移时要注意:
- 回测中的
handle_bar在实盘中对应的是行情推送回调 - 回测中的
order_shares在实盘中对应的是交易API的下单接口 - 回测中的
context.portfolio在实盘中需要自己维护
我一般会把策略逻辑写成纯函数,输入是行情和持仓,输出是目标持仓。这样回测和实盘可以共用同一套逻辑,只是数据源和执行层不同。
6. 一些让回测更靠谱的实操习惯
跑了几十个策略之后,我养成了几个习惯,分享出来供参考。
第一,始终用样本外数据验证。RQAlpha支持指定回测区间,我一般把数据分成三段:前60%做参数优化,中间20%做验证,最后20%做样本外测试。如果样本外表现和优化段差距超过30%,这个策略大概率是过拟合了。
第二,记录每一笔交易的明细。RQAlpha的--output参数可以导出交易记录,我会用pandas做二次分析,看看盈亏分布、持仓周期、换手率这些指标。很多时候收益曲线好看,但交易明细一拉出来,发现是靠几笔运气单撑起来的。
第三,关注最大回撤而不是年化收益。A股波动大,一个年化30%但最大回撤50%的策略,实盘中很少有人能拿住。我一般要求最大回撤控制在20%以内,夏普比率大于1。
第四,定期重新回测。市场结构在变,两年前有效的因子现在可能失效了。我每季度会把所有在跑的策略重新回测一遍,看看逻辑是否还成立。
第五,不要迷信框架。RQAlpha再完善,也只是工具。策略的核心逻辑、对市场的理解、风险控制,这些才是决定成败的东西。我见过太多人花大量时间调框架参数,却不肯花时间研究行业和公司。
最后说一个我自己的教训。早期我做回测时,总想把收益曲线调得越漂亮越好,参数优化到小数点后两位。后来实盘一跑,发现完全不是那么回事。现在我更看重策略的逻辑是否清晰、是否可解释。如果一个策略我说不清楚它为什么赚钱,那我宁可不做。RQAlpha给了我一个真实的回测环境,但真实的市场永远比回测复杂。保持敬畏,控制仓位,活得久比赚得快重要。