把CTA策略逻辑,对应合约品种,以及参数设置(可在策略文件外修改)载入到回测引擎中。

    载入历史数据

    负责载入对应品种的历史数据,大概有4个步骤:

    • 根据数据类型不同,分成K线模式和Tick模式;
    • 通过select().where()方法,有条件地从数据库中选取数据,其筛选标准包括:vt_symbol、 回测开始日期、回测结束日期、K线周期(K线模式下);
    • order_by(DbBarData.datetime)表示需要按照时间顺序载入数据;
    • 载入数据是以迭代方式进行的,数据最终存入self.history_data。
    1. def load_data(self):
    2. """"""
    3. self.output("开始加载历史数据")
    4.  
    5. if self.mode == BacktestingMode.BAR:
    6. s = (
    7. DbBarData.select()
    8. .where(
    9. (DbBarData.vt_symbol == self.vt_symbol)
    10. & (DbBarData.interval == self.interval)
    11. & (DbBarData.datetime >= self.start)
    12. & (DbBarData.datetime <= self.end)
    13. )
    14. .order_by(DbBarData.datetime)
    15. )
    16. self.history_data = [db_bar.to_bar() for db_bar in s]
    17. else:
    18. s = (
    19. DbTickData.select()
    20. .where(
    21. (DbTickData.vt_symbol == self.vt_symbol)
    22. & (DbTickData.datetime >= self.start)
    23. & (DbTickData.datetime <= self.end)
    24. )
    25. .order_by(DbTickData.datetime)
    26. )
    27. self.history_data = [db_tick.to_tick() for db_tick in s]
    28.  
    29. self.output(f"历史数据加载完成,数据量:{len(self.history_data)}")

    根据委托类型的不同,回测引擎提供2种撮合成交机制来尽量模仿真实交易环节:

    • 停止单撮合成交:(以买入方向为例)先确定是否发生成交,成交标准为委托价<= 下一根K线的最高价;然后确定成交价格,成交价格为委托价与下一根K线开盘价的最大值。

    下面展示在引擎中限价单撮合成交的流程:

    • 确定会撮合成交的价格;
    • 遍历限价单字典中的所有限价单,推送委托进入未成交队列的更新状态;
    • 判断成交状态,若出现成交,推送成交数据和委托数据;
    • 从字典中删除已成交的限价单。

    计算策略盈亏情况

    下面展示盈亏情况的计算过程

    • 浮动盈亏 = 持仓量 (当日收盘价 - 昨日收盘价) 合约规模
    • 实际盈亏 = 持仓变化量 (当时收盘价 - 开仓成交价) 合约规模
    • 总盈亏 = 浮动盈亏 + 实际盈亏
    • 净盈亏 = 总盈亏 - 总手续费 - 总滑点
    1. def calculate_pnl(
    2. self,
    3. pre_close: float,
    4. start_pos: float,
    5. size: int,
    6. rate: float,
    7. slippage: float,
    8. ):
    9. """"""
    10. self.pre_close = pre_close
    11.  
    12. # Holding pnl is the pnl from holding position at day start
    13. self.start_pos = start_pos
    14. self.end_pos = start_pos
    15. self.holding_pnl = self.start_pos * \
    16. (self.close_price - self.pre_close) * size
    17.  
    18. # Trading pnl is the pnl from new trade during the day
    19. self.trade_count = len(self.trades)
    20.  
    21. for trade in self.trades:
    22. if trade.direction == Direction.LONG:
    23. pos_change = trade.volume
    24. else:
    25. pos_change = -trade.volume
    26.  
    27. turnover = trade.price * trade.volume * size
    28.  
    29. self.trading_pnl += pos_change * \
    30. (self.close_price - trade.price) * size
    31. self.end_pos += pos_change
    32. self.commission += turnover * rate
    33. self.slippage += trade.volume * size * slippage
    34.  
    35. # Net pnl takes account of commission and slippage cost
    36. self.total_pnl = self.trading_pnl + self.holding_pnl
    37. self.net_pnl = self.total_pnl - self.commission - self.slippage

    calculate_statistics函数是基于逐日盯市盈亏情况(DateFrame格式)来计算衍生指标,如最大回撤、年化收益、盈亏比、夏普比率等。

    统计指标绘图

    • 资金曲线图
    • 资金回撤图
    • 每日盈亏图
    • 每日盈亏分布图
    1. def show_chart(self, df: DataFrame = None):
    2. """"""
    3. if not df:
    4. df = self.daily_df
    5.  
    6. if df is None:
    7. return
    8.  
    9. plt.figure(figsize=(10, 16))
    10.  
    11. balance_plot = plt.subplot(4, 1, 1)
    12. balance_plot.set_title("Balance")
    13. df["balance"].plot(legend=True)
    14.  
    15. drawdown_plot = plt.subplot(4, 1, 2)
    16. drawdown_plot.set_title("Drawdown")
    17. drawdown_plot.fill_between(range(len(df)), df["drawdown"].values)
    18.  
    19. pnl_plot = plt.subplot(4, 1, 3)
    20. pnl_plot.set_title("Daily Pnl")
    21. df["net_pnl"].plot(kind="bar", legend=False, grid=False, xticks=[])
    22.  
    23. distribution_plot = plt.subplot(4, 1, 4)
    24. distribution_plot.set_title("Daily Pnl Distribution")
    25. df["net_pnl"].hist(bins=50)
    26.  
    27. plt.show()
    • 导入回测引擎和CTA策略
    • 设置回测相关参数,如:品种、K线周期、回测开始和结束日期、手续费、滑点、合约规模、起始资金
    • 载入策略和数据到引擎中,运行回测。