今天我们来用WonderTrader的python
子框架wtpy
来实际编写一个期货日内交易的策略。然后我们会先设定一组参数进行第一轮测试,再根据第一轮测试的结果,调整好参数以后,再进行第二轮测试。借此来演示一下wtpy
中策略如何编写以及回测。
准备工作
-
安装
wtpy
。在安装了python3.6
以上的计算机上执行一下命令。$ pip install wtpy
或者直接下载
whl
文件到本地进行安装
阿里云镜像地址:https://mirrors.aliyun.com/py...pipy
地址:https://pypi.org/project/wtpy... - 从
github
复制demo
我们选用期货回测demo来作为基准的demo进行修改。
期货回测demo下载地址:https://github.com/wondertrad... - 准备历史回测数据
我们直接使用demo中自带的股指期货主力合约5分钟数据进行回测,文件名为CFFEX.IF.HOT_m5.csv
。为了提高测试效率,我们只选取最后近两个月时间的数据进行回测,具体为2019年9月10日到2019年10月31日。
股指期货主力合约2019年9月10日-2019年10月31日走势
2019年9月10日的开盘价为3976.2,2019年10月31日收盘价为3879.0,区间涨幅为-2.44%。
确定策略算法
- 选择策略算法
我们选择比较经典的DualThrust
作为我们策略的算法。一方面DualThrust
流传了很久了,曾经有很多大机构都用这个模型获取到了足够多的收益;另一方面,DualThrust
的算法复杂度比较低,比较适合我们作为演示策略来使用。 -
DualThrust
的算法逻辑如下
用MAX(HH-LC,HC-LL)
,作为计算上下边界的基准值,用今日开盘价
作为基准价,然后用上边界系数
和下边界系数
,分别计算出上边界的价格和下边界的价格,当最新价突破上边界或者下边界的时候,就是我们发出信号的时候。
但是在策略的实现中,我们还需要考虑到已有持仓的时候如何处理,所以最终的策略逻辑如下:当持仓为0的时候,价格突破上边界时,开多进场,价格突破下边界时,开空进场
当持仓为多的时候,价格突破上边界时,保持仓位,价格突破下边界时,多反空
当持仓为空的时候,价格突破上边界时,空反多,价格突破下边界时,保持仓位
策略实现
-
参数说明
确定了策略的算法以后,我们需要确定策略模块的参数。参数的设置,要综合考虑策略本身的参数,以及模块使用的参数。最终我们确定了如下的参数:name 策略实例名称 code 回测使用的合约代码 barCnt 要拉取的K线条数 period 要使用的K线周期,采用周期类型+周期倍数的形式,如m5表示5分钟线,d3表示3日线 days 策略算法参数,算法引用的历史数据条数 k1 策略算法参数,上边界系数 k2 策略算法参数,下边界系数 isForStk DualThrust策略用于控制交易品种的代码
我们还可以将基本手数作为参数传递给策略模型,这样的话通用性更强。不过我们这里就不再增设参数了,默认手数都是1手。
-
最终策略源码如下
from wtpy import BaseStrategy from wtpy import Context class StraDualThrust(BaseStrategy): def __init__(self, name:str, code:str, barCnt:int, period:str, days:int, k1:float, k2:float, isForStk:bool = False): BaseStrategy.__init__(self, name) self.__days__ = days self.__k1__ = k1 self.__k2__ = k2 self.__period__ = period self.__bar_cnt__ = barCnt self.__code__ = code self.__is_stk__ = isForStk def on_init(self, context:Context): code = self.__code__ #品种代码 if self.__is_stk__: code = code + "Q" context.stra_get_bars(code, self.__period__, self.__bar_cnt__, isMain = True) context.stra_log_text("DualThrust inited") def on_calculate(self, context:Context): ''' 策略主调函数,所有的计算逻辑都在这里完成 ''' code = self.__code__ #品种代码 # 交易单位,主要用于股票的适配 trdUnit = 1 if self.__is_stk__: trdUnit = 100 #读取最近50条1分钟线(dataframe对象) theCode = code if self.__is_stk__: theCode = theCode + "Q" df_bars = context.stra_get_bars(theCode, self.__period__, self.__bar_cnt__, isMain = True) #把策略参数读进来,作为临时变量,方便引用 days = self.__days__ k1 = self.__k1__ k2 = self.__k2__ #平仓价序列、最高价序列、最低价序列 closes = df_bars["close"] highs = df_bars["high"] lows = df_bars["low"] #读取days天之前到上一个交易日位置的数据 hh = highs[-days:-1].max() hc = closes[-days:-1].max() ll = lows[-days:-1].min() lc = closes[-days:-1].min() #读取今天的开盘价、最高价和最低价 lastBar = df_bars.iloc[-1] openpx = lastBar["open"] highpx = lastBar["high"] lowpx = lastBar["low"] ''' !!!!!这里是重点 1、首先根据最后一条K线的时间,计算当前的日期 2、根据当前的日期,对日线进行切片,并截取所需条数 3、最后在最终切片内计算所需数据 ''' #确定上轨和下轨 upper_bound = openpx + k1* max(hh-lc,hc-ll) lower_bound = openpx - k2* max(hh-lc,hc-ll) #读取当前仓位 curPos = context.stra_get_position(code)/trdUnit if curPos == 0: if highpx >= upper_bound: context.stra_enter_long(code, 1*trdUnit, 'enterlong') context.stra_log_text("向上突破%.2f>=%.2f,多仓进场" % (highpx, upper_bound)) #修改并保存 self.xxx = 1 context.user_save_data('xxx', self.xxx) return if lowpx <= lower_bound and not self.__is_stk__: context.stra_enter_short(code, 1*trdUnit, 'entershort') context.stra_log_text("向下突破%.2f<=%.2f,空仓进场" % (lowpx, lower_bound)) return elif curPos > 0: if lowpx <= lower_bound: context.stra_exit_long(code, 1*trdUnit, 'exitlong') context.stra_log_text("向下突破%.2f<=%.2f,多仓出场" % (lowpx, lower_bound)) #raise Exception("except on purpose") return else: if highpx >= upper_bound and not self.__is_stk__: context.stra_exit_short(code, 1*trdUnit, 'exitshort') context.stra_log_text("向上突破%.2f>=%.2f,空仓出场" % (highpx, upper_bound)) return def on_tick(self, context:Context, stdCode:str, newTick:dict): return
第一轮回测
- 确定参数
我们采用股指期货主力合约5分钟K线进行回测,每次读取50条历史K线,用最近30条K线计算上下突破的边界,上边界系数初步定为0.1,下边界系数也初步定为0.1。 -
修改
runBT.py
中策略的参数,然后运行runBT.py
。from wtpy import WtBtEngine from wtpy.backtest import WtBtAnalyst from Strategies.DualThrust import StraDualThrust if __name__ == "__main__": #创建一个运行环境,并加入策略 engine = WtBtEngine() engine.init('.\\Common\\', "configbt.json") engine.configBacktest(201909100930,201910311500) engine.configBTStorage(mode="csv", path=".\\storage\\") engine.commitBTConfig() #代码里的配置项,会覆盖配置文件configbt.json里的配置项 ''' 创建DualThrust策略的一个实例 name 策略实例名称 code 回测使用的合约代码 barCnt 要拉取的K线条数 period 要使用的K线周期,m表示分钟线 days 策略算法参数,算法引用的历史数据条数 k1 策略算法参数,上边界系数 k2 策略算法参数,下边界系数 isForStk DualThrust策略用于控制交易品种的代码 ''' straInfo = StraDualThrust(name='pydt_IF', code="CFFEX.IF.HOT", barCnt=50, period="m5", days=30, k1=0.1, k2=0.1, isForStk=False) engine.set_strategy(straInfo) #开始运行回测 engine.run_backtest() #创建绩效分析模块 analyst = WtBtAnalyst() #将回测的输出数据目录传递给绩效分析模块 #init_capital为初始资金规模 #rf为无风险收益率 #annual_trading_days为每年的交易日天数,用于计算年化收益 analyst.add_strategy("pydt_IF", folder="./outputs_bt/pydt_IF/", init_capital=500000, rf=0.02, annual_trading_days=240) #运行绩效模块 analyst.run() kw = input('press any key to exit\n') engine.release_backtest()
- 回测执行结束以后,打开自从生成的绩效分析报告文件,查看绩效分析结果
回测绩效概览
然后打开成交日志文件,查看成交明细
…… - 回测结果分析
从上面的绩效报告可以看出,在这组参数下,时间从2019年9月10日到2019年10月31日下午收盘截止,总共产生了370笔交易,即完整的开平370个回合,换算成成交的话,就是740次成交。
虽然交易次数很多,但是收益却很不理想,2个月不到的时间,50w的资金,总共亏损了近10w,约20%。从上面的截图,我们可以看到,在最后一笔出场的时候,总盈亏是4,980.00,也就是说策略的逻辑到最后是盈利的,但是盈利的金额很小。让我们再看一下每天结算的资金情况。
从上图我们可以看到,2个月的时间,佣金一共花费了104,775.82,所以账户总盈亏是-99,795.82。这样我们就可以大致得出一个结论:因为上下边界不够宽,所以有很多噪音信号,也触发了买卖的逻辑,从而导致买卖频繁,佣金过高,最终导致亏损。
第二轮回测
- 重新调整参数
根据上一轮的结果分析,我们需要把上边界和下边界拓宽,从而过滤掉一些噪声波动,减少信号个数。所以我们第二轮回测,将上边界系数改成0.5,将下边界系数改成0.3。 -
修改runBT.py,然后运行runBT.py进行回测
straInfo = StraDualThrust(name='pydt_IF', code="CFFEX.IF.HOT", barCnt=50, period="m5", days=30, k1=0.5, k2=0.3, isForStk=False)
- 再查看绩效报告
回测绩效概览
成交明细
每日资金结算 - 再分析结果
从第二轮的绩效报告可以看出,当边界拓宽以后,交易信号减少到了14个回合,但是交易收益却达到了24,060.00,约为第一轮收益的5倍,而佣金却只有1,453.02元,比第一轮的佣金低了两个量级。最终累计收益也由第一轮的-19.96%提升到盈利4.31%,相对于-2.44%的基准收益率,相对收益率达到6.75%。(这个算法对不对?:orz:)
结束语
上面演示了在WonderTrader
上构建一个期货日内交易策略的基本过程。回测稳定以后,策略就可以不作任何修改的直接放到实盘里去运行了。希望能够对大家有所启发。
最后再打一波广告:WonderTrader
的github
地址:https://github.com/wondertrad...WonderTrader
官网地址:https://wondertrader.github.io
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。