我们先看一个代码
engine = BacktestingEngine()
engine.set_parameters(
vt_symbol="IF88.CFFEX",
interval="1m",
start=datetime(2021, 1, 15),
end=datetime(2022, 1, 7),
rate=0,
slippage=0,
pricetick=1,
size=300,
capital=1_000_000,
)
engine.add_strategy(DKXStrategy, {})
engine.load_data()
engine.run_backtesting()
df = engine.calculate_result()
engine.calculate_statistics()
engine.show_chart()
在回测里没有使用MainEngine,直接使用了BacktestingEngine,因为不需要使用gateway等相关功能
- BacktestingEngine
- CtaEngine
这两个Engine对应的功能基本是类似的,只是实现不一样。
刚才的方法里的主要是:
- 设置参数
- 添加策略
- 加载数据
- 执行回测
- 执行计算
- 统计各种指标
- 显示图形
我们主要看下run_backtesting方法
def run_backtesting(self):
""""""
if self.mode == BacktestingMode.BAR:
func = self.new_bar
else:
func = self.new_tick
self.strategy.on_init()
# Use the first [days] of history data for initializing strategy
day_count = 0
ix = 0
for ix, data in enumerate(self.history_data):
if self.datetime and data.datetime.day != self.datetime.day:
day_count += 1
if day_count >= self.days:
break
self.datetime = data.datetime
try:
self.callback(data)
except Exception:
self.output("触发异常,回测终止")
self.output(traceback.format_exc())
return
self.strategy.inited = True
self.output("策略初始化完成")
self.strategy.on_start()
self.strategy.trading = True
self.output("开始回放历史数据")
# Use the rest of history data for running backtesting
backtesting_data = self.history_data[ix:]
if len(backtesting_data) <= 1:
self.output("历史数据不足,回测终止")
return
total_size = len(backtesting_data)
batch_size = max(int(total_size / 10), 1)
for ix, i in enumerate(range(0, total_size, batch_size)):
batch_data = backtesting_data[i: i + batch_size]
for data in batch_data:
try:
func(data)
except Exception:
self.output("触发异常,回测终止")
self.output(traceback.format_exc())
return
progress = min(ix / 10, 1)
progress_bar = "=" * (ix + 1)
self.output(f"回放进度:{progress_bar} [{progress:.0%}]")
self.strategy.on_stop()
self.output("历史数据回放结束")
我们看下这段代码:
for ix, data in enumerate(self.history_data):
if self.datetime and data.datetime.day != self.datetime.day:
day_count += 1
if day_count >= self.days:
break
self.datetime = data.datetime
try:
self.callback(data)
except Exception:
self.output("触发异常,回测终止")
self.output(traceback.format_exc())
return
这个数据是loadbar历史数据的实现, self.days 就是等于loadBar天数参数。self.callback,也是在loadbar里传入的,如果没有传则调用on_bar,因为这里是加载准备数据,所以跟后面的数据回放不一样,少了计算单子是否成交等等逻辑。
for ix, i in enumerate(range(0, total_size, batch_size)):
batch_data = backtesting_data[i: i + batch_size]
for data in batch_data:
try:
func(data)
except Exception:
self.output("触发异常,回测终止")
self.output(traceback.format_exc())
return
progress = min(ix / 10, 1)
progress_bar = "=" * (ix + 1)
self.output(f"回放进度:{progress_bar} [{progress:.0%}]")
这里开始数据回放,调用的func,func根据回测是bar,还是tick进行决定的。
def new_bar(self, bar: BarData):
""""""
self.bar = bar
self.datetime = bar.datetime
self.cross_limit_order()
self.cross_stop_order()
self.strategy.on_bar(bar)
self.update_daily_close(bar.close_price)
这个代码主要就是撮合:
self.cross_limit_order()
self.cross_stop_order()
判断是不是有成交的单子
def cross_limit_order(self):
"""
Cross limit order with last bar/tick data.
"""
if self.mode == BacktestingMode.BAR:
long_cross_price = self.bar.low_price
short_cross_price = self.bar.high_price
long_best_price = self.bar.open_price
short_best_price = self.bar.open_price
else:
long_cross_price = self.tick.ask_price_1
short_cross_price = self.tick.bid_price_1
long_best_price = long_cross_price
short_best_price = short_cross_price
for order in list(self.active_limit_orders.values()):
# Push order update with status "not traded" (pending).
if order.status == Status.SUBMITTING:
order.status = Status.NOTTRADED
self.strategy.on_order(order)
# Check whether limit orders can be filled.
long_cross = (
order.direction == Direction.LONG
and order.price >= long_cross_price
and long_cross_price > 0
)
short_cross = (
order.direction == Direction.SHORT
and order.price <= short_cross_price
and short_cross_price > 0
)
if not long_cross and not short_cross:
continue
# Push order udpate with status "all traded" (filled).
order.traded = order.volume
order.status = Status.ALLTRADED
self.strategy.on_order(order)
if order.vt_orderid in self.active_limit_orders:
self.active_limit_orders.pop(order.vt_orderid)
# Push trade update
self.trade_count += 1
if long_cross:
trade_price = min(order.price, long_best_price)
pos_change = order.volume
else:
trade_price = max(order.price, short_best_price)
pos_change = -order.volume
trade = TradeData(
symbol=order.symbol,
exchange=order.exchange,
orderid=order.orderid,
tradeid=str(self.trade_count),
direction=order.direction,
offset=order.offset,
price=trade_price,
volume=order.volume,
datetime=self.datetime,
gateway_name=self.gateway_name,
)
self.strategy.pos += pos_change
self.strategy.on_trade(trade)
self.trades[trade.vt_tradeid] = trade
这里生成trade,并存储起来,在后面的计算盈亏使用到