回测慢、框架难维护,是量化开发里最耗精力的两件事。数据加载等5分钟,调个参数再等5分钟;想扩展到多资产,发现撮合、资金管理牵一发动全身。
Swordfish 回测框架正是为了让开发者把精力还给策略本身而设计的。策略逻辑、撮合、资金管理各自独立,支持股票、期货、期权、债券、加密货币等多资产类型,分钟级、快照级、逐笔级多频率数据均可驱动,开箱即用。本文用一个完整的趋势策略案例,带你把这套框架从头跑一遍。
一、框架全貌:你只管写策略逻辑
Swordfish 的设计哲学很简单:让用户专注于业务逻辑的研究,其他的事情框架来做。整体架构由四个核心模块组成,各司其职:
这种分层设计带来的最大好处是扩展时不需要动策略代码。今天你做的是股票分钟策略,明天切到期货日频,只需要换一个 Config 类和对应的 AssetMixin,主体策略逻辑通常不需要改动。
整个流程可以用5个字概括:写、配、建、跑、看。后面的实战章节会把这5步逐一跑一遍。
二、事件驱动:不是你去问数据,是数据主动找你
很多人第一次接触事件驱动回测框架,会有一个疑问:它和向量化回测有什么本质区别?
区别在于主动权在谁。
向量化回测中,用户需要主动调用数据:"现在是第几根 K 线?当前价格是多少?我的持仓有多少?" 每一个问题都要用户自己写,且需手动处理数据时序、持仓状态的同步。事件驱动回测则反过来,是框架主动通知用户:"一根新 K 线来了,用户来决定怎么处理。" 用户只需要在对应的回调函数里写逻辑,别的交给框架。
Swordfish 支持的事件回调函数覆盖从日频到逐笔的所有粒度:
这些回调按需覆盖即可——框架提供了从 initialize 到 finalize 的全套生命周期回调,但用户只需要根据策略实现自己需要的部分回调。不同回调名称对应不同的行情频率:on_bar 处理 K 线数据,适合中低频策略;on_snapshot 处理快照数据(市场某一时刻的完整状态),on_tick 处理逐笔成交数据,两者均适用于高频策略。用户需要什么频率的行情,就实现对应的回调,框架保证在用户订阅的每个行情时间点精准触发。
三、实战:一个完整的趋势策略
说再多不如跑一遍。我们用一个分钟K线趋势策略作为示例:当前收盘价低于上一根K线,判断为价格回落或回升;已有持仓且价格高于上一根K线,判断为反弹,卖出;每次固定操作1000股。
第一步:写策略类。
策略类同时继承 StrategyTemplate 和 StockOrderMixin,前者提供生命周期回调,后者提供股票下单接口。initialize 里订阅"上一根K线收盘价"这个指标,on_bar 里根据指标值和当前持仓做买卖判断:
class MyStrategy(backtest.StrategyTemplate, backtest.StockOrderMixin):
def initialize(self, context):
with sf.meta_code() as m:
lastp = F.prev(m.col("close"))
self.subscribe_indicator(backtest.MarketDataType.KLINE, {'lastp': lastp})
def on_bar(self, context, msg, indicator):
for istock in msg.keys():
prevp = indicator[istock]["lastp"]
lastPrice = msg[istock]["close"]
position = self.accounts[backtest.AccountType.DEFAULT]\
.get_position(symbol=istock)["longPosition"]
if position == 0 and lastPrice < prevp:
self.submit_stock_order(istock, context["tradeTime"], 5, lastPrice, 1000, 1, label="buy")
elif position > 0 and lastPrice > prevp:
self.submit_stock_order(istock, context["tradeTime"], 5, lastPrice, 1000, 2, label="sell")
第二步:配参数、加载数据、启动回测。
回测配置项很直观——资产类型、时间范围、初始资金、手续费,每一项都是显式的,不需要猜默认值。配好之后构建 Backtester 对象,调用 append_data 注入行情数据,回测立即开始驱动策略运行:
config = backtest.StockConfig()
config.start_date = sf.scalar("2021.01.01", type="DATE")
config.end_date = sf.scalar("2021.12.31", type="DATE")
config.asset_type = backtest.AssetType.STOCK
config.data_type = backtest.MarketType.MINUTE
config.cash = 100_000_000
config.commission = 0.00015
stocks = F.loadText('mink_data.csv') # 支持直接读取 CSV,也可通过 sf.sql 灵活做字段映射
backtester = backtest.Backtester(MyStrategy, config)
backtester.append_data(stocks)
第三步:看结果。
回测结束后,通过 account 对象获取所有结果数据。return_summary 给出累计收益、年化收益、最大回撤等汇总指标;trade_details 记录每一笔成交的时间、价格、数量;get_daily_position() 展示每日持仓变化:
account = backtester.accounts[backtest.AccountType.DEFAULT]
print(account.return_summary)
print(account.trade_details)
print(account.get_daily_position())
从策略类到拿到完整结果,全部加起来不超过50行代码。
四、进阶:自定义指标,策略更聪明
上面的例子用了内置的 prev(close) 作为信号。实际策略里,用户可能需要涨跌幅、换手率、资金流向等各种自定义指标。Swordfish 的解法是 @swordfish_udf 装饰器:把指标逻辑写成一个普通 Python 函数,加上装饰器注册,再通过 subscribe_indicator 订阅,框架就会在每个对应的行情时间点自动算好,直接送到回调函数里。
用户不需要在 on_bar 或 on_snapshot 里手动维护指标状态,也不需要担心计算时序问题——指标计算和行情触发是绑定的,框架保证指标值与当前行情时间点对齐,不需要用户手动处理时序偏移。
以涨跌幅指标为例,定义、注册、调用三步走(注册和调用分别在策略类的 initialize 和 on_snapshot 方法内):
# 第一步:在类外定义指标函数
@F.swordfish_udf(mode="translate", is_state=True)
def zdf(last_price, prev_close_price):
return last_price / prev_close_price - 1
# 第二步:在 initialize 方法内注册订阅
def initialize(self, context):
with sf.meta_code() as m:
m_zdf = zdf(m.col("lastPrice"), m.col("prevClosePrice"))
self.subscribe_indicator(backtest.MarketDataType.SNAPSHOT, {'zdf': m_zdf})
# 第三步:在 on_snapshot 方法内直接使用
def on_snapshot(self, context, msg, indicator):
if indicator["zdf"] > 0.01 and long_pos <= context["max_pos"]:
self.submit_stock_order(symbol, context["tradeTime"], 5, best_ask, 100, 1)
只需要把 subscribe_indicator 的第一个参数从 SNAPSHOT 换成 KLINE,指标就会改为在每根K线上触发计算,其他代码一行不用动。这种设计让指标和策略逻辑彻底解耦,复用起来非常方便。
五、性能实测:回测框架有多快
框架设计得再好,最终还是要看跑起来快不快。我们在真实生产环境中对 Swordfish 回测框架做了完整的性能基准测试,测试维度覆盖单策略、多策略、高并发三种典型场景。
测试场景
以单只股票2000万交易金额为基准,使用行情快照数据,采用 TWAP 算法,在不同数据量和并发度下分别记录回测耗时。
以下数据全部来自 Swordfish 回测框架(含延时设置):
从上面的数据可以读出三条信息:
单场景响应极快。 单股单策略回测一周行情仅需 1.3 秒,扩展到一个月也只要 2.0 秒。数据量放大 4 倍,耗时只增加 0.7 秒,说明框架在数据吞吐上几乎没有瓶颈。
高并发下扩展性良好。 用户数从 1 增长到 50(并发度 2→100),回测耗时从 2.6 秒增长到 7.0 秒。并发量翻了 50 倍,耗时只增加了不到 2 倍——框架的调度开销被很好地摊薄了。即使在 100 并发下,一周的回测也能在 10 秒内完成。
多策略扩展成本可控。 将策略从单个扩展为双策略同时运行,一周耗时 1.3s → 1.7s(增加 0.4s),一月耗时 2.0s → 3.7s(增加 1.7s)。耗时增量主要来自策略计算本身,而非框架调度。
不同回放速度下的结果一致性
测试过程中还验证了另一个关键特性:以 5 倍、10 倍、20 倍速从 DolphinDB 回放行情数据,各倍速下 Swordfish 的撮合结果经过逐笔校验完全一致。这意味着在实际使用中,用户可以放心地用高速回放快速验证策略逻辑,用低速逐笔观察细节,同一套策略在不同行情推送速度下不会产生结果偏差。
这套性能数据意味着:无论是研究员在本地快速迭代策略,还是生产环境批量跑组合回测,Swordfish 都能在合理的时间内给出答案。
六、三个习惯,让回测跑得更快
框架本身做了大量底层优化,但写法同样会影响性能,尤其是数据量大、标的多的时候。以下三点是效果最直接的:
1.开启指标批量优化
策略涉及多个技术指标时,建议显式开启 config.enable_indicator_optimize = True。框架会对指标依赖关系做静态分析,把可以合并的计算批量处理,避免同一段数据被重复遍历。指标越多、标的越多,这个优化的收益越明显。
2.用 context 预建对象,别在回调里反复创建
on_bar、on_tick 这类回调在高频策略里每秒可能触发上千次。充分利用 context 的最佳方式是在 initialize 里预先创建好所有容器对象,后续回调只做读写复用——这样框架可以在整个回测周期内保持稳定的内存占用,高频场景下性能表现尤为突出。
3.合理配置撮合模式
Swordfish 支持多种撮合模式,精细撮合(模拟订单队列位置、部分成交拆单)会显著增加计算量。在验证策略逻辑的早期阶段,若对成交率无严格要求,直接设置 config.matching_mode = 3,框架按委托价全量成交,撮合耗时大幅缩短。
写在最后
Swordfish 回测框架的设计目标只有一个:让策略开发者把精力集中在策略本身。撮合、资金、持仓、事件调度全部由框架接管,用户只需要在对应的回调函数里告诉它"行情来了我要做什么"。
从单标的到多标的,从分钟到逐笔,扩展时只需要换配置类和下单接口,主体策略逻辑保持不变。无论是快速验证一个新想法,还是推进到生产级别的多资产组合策略,Swordfish 都能承接得住。