作者:张国平 ;来源:韦恩的派论坛
在尝试写tick级别的策略,由于交易反馈时间要求高,感觉需要对单个order的事有个全面了解,就花了些时间尝试性去分析了VNPY中 从发送交易指令(sendOrder())到交易所,和接收成交返回信息(onOrder()/onTrade())的代码。如果有错误或者遗漏,请指正。这里先将发送
一·在策略中,一般不直接调用sendOrder(), 而且用四个二次封装函数函数,这些都是在class CtaTemplate中定义的,这里面主要区别就是sendorder()函数的中ordertype制定不一样,用来区分是买开卖开等交易类型。
返回一个vtOrderIDList, 这个list里面包含vtOrderID,这个是个内部给号,可以用做追踪同一个order的状态。
二·接下来我们看看那sendOrder()源码,还在class CtaTemplate中定义;如果stop为True是本地停止单,这个停止单并没有发送给交易所,而是存储在内部,使用ctaEngine.sendStopOrder()函数;否则这直接发送到交易所,使用ctaEngine.sendStopOrder函数。这里会返回一个vtOrderIDList, 这个list里面包含vtOrderID,和上面说的一样。这里补充一下,对于StopOrder真是交易通常是涨停价或者跌停价发出的市价单(Market price),参数price只是触发条件;而普通sendOrder是真正按照参数price的限价单(Limit price)
三·这里我们首先看看ctaEngine.sendStopOrder()函数,在class CtaEngine中定义的,首先实例初始化时候定义了两个字典,用来存放stoporder,区别一个是停止单撤销后删除,一个不会删除;还定义了一个字典,策略对应的所有orderID。
然后在函数sendStopOrder中,首先记录给本地停止单一个专门编号,就是前缀加上顺序编号,其中STOPORDERPREFIX是 'CtaStopOrder.',那么第一条本地编码就是'CtaStopOrder.1',后面是这个单据信息;这里可以发现orderType其实是一个direction和offset的组合。交易方向direction有Long、short两个情况,交易对offset有open和close两个情况。组合就是上面买开,卖平等等。然后把这个stoporder放入字典,等待符合价格情况到达触发真正的发单。这里返回本地编码作为vtOrderIDList。
四· 下面是processStopOrder()函数,也在classCtaEngine中定义的,主要是当行情符合时候如何发送真正交易指令,因为stopOrderID不是tick交易重点,这里简单讲讲,具体请看源码。
当接收到tick时候,会查看tick.vtSymbol,是不是存在workingStopOrderDict的so.vtSymbol有一样的,如果有,再看tick.lastPrice价格是否可以满足触发阈值,如果满足,根据原来so的交易Direction,Long按照涨停价,Short按照跌停价发出委托。然后从workingStopOrderDic和strategyOrderDict移除该so,并更新so状态,并触发事件onStopOrder(so).
这里发现,so只是只是按照涨停价发单给交易所,并没有确保成绩,而且市价委托的实际交易vtOrderID也没有返回;从tick交易角度,再收到tick后再发送交易,本事也是有了延迟一tick。所以一般tick级别交易不建议使用stoporder。
五·前面说了这么多,终于到了正主sendOrder(),也在class CtaEngine中定义的。代码较长,下面做了写缩减。
1)通过mainEngine.getContract获得这个品种的合约的信息,包括这个合约的名称,接口名gateway(国内期货就是ctp),交易所,最小价格变动等信息;
2)创建一个class VtOrderReq的对象req,在vtObject.py中,这个py包括很多事务类的定义;然后赋值,包括contract获得信息,交易手数,和price,和priceType,这里只有限价单。
3)根据orderType赋值direction和offset,之前sendStopOrder中已经说了,就不重复。
4)然后跳到mainEngine.convertOrderReq(req),这里代码比较跳,分析下来,如果之前没有持仓,或者是直接返回[req];如果有持仓就调用PositionDetail.convertOrderReq(req),这个时候如果是平仓操作,就分析持仓量,和平今和平昨等不同操作返回拆分的出来[reqTd,reqYd],这里不展开。
5) 如果上一部没有返回[req],则委托有问题,直接返回控制。如果有[req],因为存在多个req情况,就遍历每个req,使用mainEngine.sendOrder发单,并保存返回的vtOrderID到orderStrategyDict[],strategyOrderDict[]两个字典;然后把vtOrderIDList返回。
六·在mainEngine.sendOrder中,这里不列举代码了,首先进行风控,如果到阈值就不发单,然后看gateway是否存在,如果存在,就调用gateway.sendOrder(orderReq)方法;下面用ctpgateway说明。class CtpGateway(VtGateway)是VtGateway是继承,把主要发单,返回上面都实现,同时对于不同的接口,比如外汇,只要用一套接口标准就可以,典型继承使用。
CtpGateway.sendOrder实际是调用class CtpTdApi(TdApi)的,这个就是一套ctp交易交口,代码很简单,最后是调用封装好C++的ctp接口reqOrderInsert()。最关键返回的vtOrderID是接口名+顺序数。
整个流程下来,不考虑stoporder,是ctaTemplate -> CtaEngine->mainEngine ->ctpgateway ->CtpTdApi, 传到C++封装的接口。返回的就是vtOrderID; 因为存在平昨,平今还有锁仓,反手等拆分情况,返回的可能是一组。
我在云服务器上配置RQData同样出现这样的问题,但是在本机(老版本)上可以正常下载。。。
估计是RQData那边版本升级出现了bug,已经联系那边的人员尝试修复问题
(继续上一章节未完成的测试)
选择标准:回归夏普比率>0.4
对初步筛选出来的样本进行2014-2016年回测,选择回归夏普比率>0.4的品种,然后构成组合,如图所示。
根据回归夏普比率>0.4的准则,筛选出了17个品种,其历史表现和2017年预测表现如图6-31所示。投资组合在2014-2016年年化收益73.7%,百分比最大回撤-22.99%,夏普比率达2.48,资金曲线平滑且整体向上,但是2017年预测表现不佳,资金曲线不断上下震荡,夏普比率仅仅是-0.23。
2015-2017年回测是最后一轮策略,选择回归夏普比率>0.4的品种,然后构成最终的海龟组合,如图所示。
根据回归夏普比率>0.4的准则,筛选出了14个品种,其历史表现和2018年预测表现如图6-33所示。投资组合在2015-2017年年化收益62.2%,百分比最大回撤-23.21%,夏普比率达1.82;但是2017年震荡向上,夏普比率达0.59,故2018年是盈利的;全时间区间的夏普比率为1.42。
若把筛选标准改成回归夏普比率>0.6后,投资组合的品种数量降低到12个,其其历史表现和2018年预测表现如图6-34所示。投资组合在2015-2017年年化收益63.36%,百分比最大回撤-20.78%,夏普比率达1.81; 2018年预测表现是先发生回撤然后行情走好,夏普比率达0.59;全时间区间的夏普比率为1.5。
若把筛选标准改成回归夏普比率>0.8后,投资组合的品种数量降低到9个,其其历史表现和2018年预测表现如图6-35所示。投资组合在2015-2017年年化收益63.85%,百分比最大回撤-24.24%,夏普比率达1.84; 2018年预测表现是先发生回撤然后行情走好,夏普比率达0.57;全时间区间的夏普比率为1.52。
选择标准是回归夏普比率>0.4,对初步筛选出来的样本进行2014-2017年回测,然后一步到位构成组合,如图所示。
根据回归夏普比率>0.4的准则,筛选出了15个品种,其历史表现和2017年预测表现如图6-37所示。投资组合在2015-2017年年化收益57.2%,百分比最大回撤-22.61%,夏普比率达1.82;但是2018年资金曲线上下震荡,夏普比率仅仅是0.38;全时间区间的夏普比率为1.61。
根据回归夏普比率>0.6的准则,剔除了鸡蛋这个品种,其历史表现和2017年预测表现如图6-38所示。投资组合在2015-2017年年化收益57.2%,百分比最大回撤-22.61%,夏普比率达1.67;但是2018年夏普比率是0.33;全时间区间的夏普比率为1.49。
根据回归夏普比率>0.8的准则,剔除了一号棉花这个品种,其历史表现和2017年预测表现如图6-39所示。投资组合在2015-2017年年化收益58.68%,百分比最大回撤-38.46%,夏普比率达1.48;但是2018年夏普比率是-0.12;全时间区间的夏普比率为1.28。
根据回归夏普比率>1.0的准则,剔除的品种包括:沪深300股指、铜、铅,其历史表现和2018年预测表现如图6-40所示。投资组合在2015-2017年年化收益53.4%,百分比最大回撤-28.38%,夏普比率达1.37; 2018年夏普比率是0.43;全时间区间的夏普比率为1.19。
在上面的测试中,尝试了通过不同的回望周期(如2年、3年、4年)和不同的筛选标准得到了10个备选的海龟组合,下面仅仅以2018年预测效果和全时间区间的夏普比率构建备选表格,如图所示。从该表格可以看出,整体表现最好的是回望周期为3年,筛选标准是回归夏普比率>0.6的组合,故以该投资组合成为最终的海龟组合,用于其他关键要素的验证,比如单位头寸限制,长短周期出入场信号,上一笔盈利过滤等等。
上面测试国内期货品种,用的是日线数据 (RQData上的期货指数数据,“88”结尾的)。
作者:viponedream ;来源:维恩的派论坛
# ----------------------------------------------------------------------
def convert_datetime(dt=None, oldtz=None, newtz=None, fmt=None, returnStr=False, withFlag=False):
"""输入一个日期,时间,返回一个新时区的datetime对象
DateTime 可以是字符串,数字,日期,时间或者日期对象
可能的值 (可以带时区,默认为UTC)
20160530 10:59:59 字符 日期 时间
20160530 字符 日期
10:59:59 字符 时间
1536249467.2627187 timestamp(可字符或数字)
传入的日期或字符串日期, 如20160530 10:59:59
或者传入一个时间戳 时间戳没有时区概念,是GMT以来的秒数
newtz 想要的新时区,有新时区则返回新时区,否则返回UTC时间
oldtz 传入日期的原来时区 可选参数 CST EST GMT UTC
withFlag
"""
"""
now(), datetime(), time()都是可以指定tzinfo信息的,而date是不行的
"""
# 定义输出格式 '%Y%m%d %H:%M:%S' '%Y%m%d %H:%M:%S.%f'
if not fmt:
fmt = '%Y%m%d %H:%M:%S.%f'
# 定义常用时区
utcZone = pytz.timezone('utc') # 标准时间 UTC, GMT
estZone = pytz.timezone('US/Eastern') # 北美 EST 时区 US/Eastern
cstZone = pytz.timezone('Asia/Shanghai') # 中国时区 CST
# 判断传入时间的时区 UTC, GMT, CST, EST
if oldtz is not None:
oldtz = oldtz.upper()
if not oldtz or oldtz in ['UTC', 'GMT']: # 没有传入默认为 UTC(GMT)
oldtz = utcZone
elif oldtz == 'CST': # 传入的是中国时区
oldtz = cstZone
elif oldtz == 'EST': # 传入的是北美时区
oldtz = estZone
else:
print(u'传入的时区没法识别')
return
# 判断要转换出去的时区 UTC, GMT, CST, EST
if newtz is not None:
newtz = newtz.upper()
if not newtz or newtz in ['UTC', 'GMT']: # 没有传入默认为 UTC(GMT)
newtz = utcZone
dtEndStr = 'GMT'
elif newtz == 'CST': # 传入的是中国时区
newtz = cstZone
dtEndStr = 'CST'
elif newtz == 'EST': # 传入的是北美时区
newtz = estZone
dtEndStr = 'EST'
else:
print(u'传入的时区没法识别')
return
newdt = None
# 先判断是否日期对象
if isinstance(dt, str):
# 2016-01-05 21:55:20
# 如果只是导入的 21:55:20,则先在前面加上日期,
dt = dt.replace('-', '')
if dt is None or not dt: # 如果dt为空或者没有传进来
now = time.time()
newdt = datetime.datetime.fromtimestamp( now, datetime.timezone.utc)
elif isinstance(dt, datetime.datetime): # 日期格式,
newdt = dt
elif isinstance(dt,datetime.date): # 只有日期
tt = datetime.time(23, 59, 59, tzinfo=oldtz)
newdt = datetime.datetime.combine(dt, tt)
elif isinstance(dt, datetime.time): #只有时间
newdt = datetime.utcnow()
newdt = newdt.replace(hour=dt.hour, minute=dt.minute, second=dt.second, tzinfo=oldtz)
elif isinstance(dt, (int, float)): # 数字,就是 timestamp
newdt = datetime.datetime.fromtimestamp(float(dt), datetime.timezone.utc)
elif isinstance(dt, str) and len(dt) == 8:
if ':' in dt: # 21:55:20
t = datetime.strptime(dt, '%H:%M:%S')
newdt = datetime.utcnow()
newdt = newdt.replace(hour=t.hour, minute=t.minute, second=t.second, tzinfo=utcZone)
else: # YYYYmmdd 20160815
y = int(dt[0:4])
m = int(dt[4:6])
d = int(dt[6:8])
tt = datetime.time(23, 59, 59, tzinfo=oldtz)
dd = datetime.date(y, m, d) # date没有时区信息
newdt = datetime.datetime.combine(dd, tt)
elif "." in dt and ':' in dt: # 20180906 16:46:20.157940
newdt = datetime.datetime.strptime(dt, '%Y%m%d %H:%M:%S.%f')
elif ':' in dt and not "." in dt: # 20180906 16:46:20
newdt = datetime.datetime.strptime(dt, '%Y%m%d %H:%M:%S')
elif dt.replace(".", '').isdigit(): # timestamp 1536249467.2627187 1536249467
newdt = datetime.datetime.fromtimestamp(float(dt), datetime.timezone.utc)
if not isinstance(newdt, datetime.datetime): # 日期格式,
print( f" MY_vtFunction 日期转化不对{dt}")
return
try:
newdt = oldtz.localize(newdt) # 设置时区
except:
pass
utcDateTime = newdt.astimezone(utcZone) # 再转为UTC标准时间
newDateTime = newtz.normalize(utcDateTime) # 转出为 newtz
# 返回时再把时区信息去掉。
newDateTime = newDateTime.replace(tzinfo=None)
if returnStr: # 返回字符串
newDateTime = newDateTime.strftime(fmt)
if withFlag:
newDateTime = ' '.join([newDateTime, dtEndStr]) # 加上时区标识
return newDateTime
初步筛选从仅仅基于历史行情外,还加多了品种波动率和自相关性的要求,故总的来说其初步筛选条件为三点:
根据初步筛选标准,剔除了不符合要求品种后,测试样本从调整前的35个缩小至27个,根据其调整后波动率比值的大小按从大到小排序,如图所示。
结合高成交量特征,一般来说,成交量高的品种,其波动率高,自相关性强,故具有正相关性。
根据交易所分类,这27个品种划分成4部分:
初步筛选之后,我们会通过不同的回望周期(如2年、3年、4年)以及基于回归夏普比率不同的筛选标准来得到若干个海龟组合备选方案,最后通过相互比较得到最终的组合。
(以下测试基于米筐RQData的小时级别期货指数数据,有兴趣的朋友可以自行验证或者使用别的数据源测试一下!)
选择标准:回归夏普比率>0.4
对初步筛选出来的样本进行2014-2015年回测,选择回归夏普比率>0.4的品种,然后构成组合,如图所示。
根据回归夏普比率>0.4的准则,筛选出了14个品种,其历史表现和2016年预测表现如图6-23所示。投资组合在2014-2015年年化收益96.46%,百分比最大回撤-32.75%,夏普比率达2.04,资金曲线平滑且整体向上,但是2016年预测表现不佳,需要剔除更多噪声因子。
下面分析一下挑选出来的品种成分,按交易所分类如下:
同样对剩下的样本进行2015-2016年回测,选择回归夏普比率>0.4的品种,然后构成组合。
经过第二轮筛选后,剩下9个品种,同样按照交易所分类,如下:
在新的投资组合中,年化收益达92.04%,百分比最大回撤是-16.8%,夏普比率达2.4,整体资金曲线比较平滑。在2017年预测表现理想,年化收益46.49%,百分比最大回撤-30.45%,夏普比率达1.09,如图所示。
最后一轮策略,将挑选出最终的品种组成海龟组合,单品种品种如图所示。
第三轮筛选后,基于回归夏普比率>0.4得到由铝、铜、锌、普麦、铁矿石、焦炭、螺纹钢组成的海龟组合,2016-2017年标准夏普达1.56,2018年预测的夏普比率是-0.12,全时间区间的夏普比率表现是1.17。投资组合效果差强人意。
基于上面2年回望周期所做展示的回测图,可以更加便捷的更改筛选标准而不用从新进行测试就得到结果,故下面把筛选标准改成回归夏普比率>0.6,其测试情况如图所示。
把筛选标准提高0.2后,得到的样本数量降低到5个,分别是铝、铜、锌、铁矿石、焦炭,2016-2017年标准夏普达1.29,2018年预测的夏普比率是0.7,全时间区间的夏普比率表现是1.22。
若把筛选标准提升至回归夏普比率>0.8,则得到4个样本品种:铜、锌、铁矿石、焦炭,2016-2017年标准夏普达1.87,2018年预测的夏普比率是-0.03,全时间区间的夏普比率表现是1.38,如图所示。
作者:爱茶语 ;来源:维恩的派论坛
结果显示尽管参数不多,但是模型还是过拟合了。但是在策略内实现Tick数据聚合成X分钟K线还是值得学习一下
# 设置回测使用的数据
engine.setBacktestingMode(engine.BAR_MODE) # 设置引擎的回测模式为K线
engine.setDatabase(MINUTE_DB_NAME, 'RB99') # RQDATA一分钟期货指数数据
engine.setStartDate('20130101') # 设置回测用的数据起始日期
# 配置回测引擎参数
engine.setSlippage(1) # 设置滑点为1跳
engine.setRate(1/1000) # 设置手续费
engine.setSize(10) # 设置合约大小
engine.setPriceTick(1) # 设置最小价格变动
engine.setCapital(200000) # 设置回测本金
策略代码如下
# encoding: UTF-8
import talib
import numpy as np
from vnpy.trader.vtObject import VtBarData
from vnpy.trader.vtConstant import EMPTY_INT,EMPTY_STRING
from vnpy.trader.app.ctaStrategy.ctaTemplate import CtaTemplate
########################################################################
class RBMAStrategy(CtaTemplate):
"""结合MA的一个30分钟线交易策略"""
className = 'RBMAStrategy'
author = 'xldistance'
#策略参数
initDays = 33 # 初始化数据所用的天数默认35
open_pos = 10 #每次交易的手数
OCM = 30 #操作分钟周期(1,60)默认30
# 策略变量
bar = None # K线对象
barMinute = EMPTY_STRING # K线当前的分钟
minutebar = None # minuteK线对象
ma_windows1 = 20 #默认20
ma_windows2 = 200 #默认200
# 参数列表,保存了参数的名称
paramList = ['name',
'className',
'author',
'vtSymbol',
'open_pos']
# 变量列表,保存了变量的名称
varList = ['inited',
'trading',
'pos',
'OCM',
'ma20_value',
'ma200_value']
#----------------------------------------------------------------------
def __init__(self, ctaEngine, setting):
"""Constructor"""
super(RBMAStrategy, self).__init__(ctaEngine, setting)
"""
如果是多合约实例的话,变量需要放在__init__里面
"""
#self.orderList = []
self.barList = []
self.bufferSize = 201 # 需要缓存的数据的大小
self.bufferCount = 0 # 目前已经缓存了的数据的计数
self.highArray = np.zeros(self.bufferSize) # K线最高价的数组
self.lowArray = np.zeros(self.bufferSize) # K线最低价的数组
self.closeArray = np.zeros(self.bufferSize) # K线收盘价的数组
self.openArray = np.zeros(self.bufferSize) # K线开盘价的数组
self.LongEnterable = False
self.ShortEnterable = False
self.ma20_value = 0
self.ma200_value = 0
def onInit(self):
self.writeCtaLog('%s策略初始化' %self.name)
# 载入历史数据,并采用回放计算的方式初始化策略数值
initData = self.loadBar(self.initDays)
for bar in initData:
self.onBar(bar)
self.putEvent()
#----------------------------------------------------------------------
def onStart(self):
"""启动策略(必须由用户继承实现)"""
self.writeCtaLog('%s策略启动' %self.name)
self.putEvent()
#----------------------------------------------------------------------
def onStop(self):
"""停止策略(必须由用户继承实现)"""
self.writeCtaLog('%s策略停止' %self.name)
self.putEvent()
#----------------------------------------------------------------------
def onTick(self, tick):
"""收到行情TICK推送(必须由用户继承实现)"""
tickMinute = tick.datetime.minute
if tickMinute != self.barMinute:
if self.bar:
self.onBar(self.bar)
bar = VtBarData()
bar.vtSymbol = tick.vtSymbol
bar.symbol = tick.symbol
bar.exchange = tick.exchange
bar.open = tick.lastPrice
bar.high = tick.lastPrice
bar.low = tick.lastPrice
bar.close = tick.lastPrice
bar.date = tick.date
bar.time = tick.time
bar.datetime = tick.datetime # K线的时间设为第一个Tick的时间
self.bar = bar # 这种写法为了减少一层访问,加快速度
self.barMinute = tickMinute # 更新当前的分钟
else: # 否则继续累加新的K线
bar = self.bar # 写法同样为了加快速度
bar.high = max(bar.high, tick.lastPrice)
bar.low = min(bar.low, tick.lastPrice)
bar.close = tick.lastPrice
#----------------------------------------------------------------------
def onBar(self, bar):
"""收到Bar推送(必须由用户继承实现)"""
if self.LongEnterable:
if self.pos == 0:# and bar.close > self.dayOpen
self.buy(bar.close,self.open_pos,True)
elif self.pos < 0 :
self.cover(bar.close,abs(self.pos),True)
if self.ShortEnterable:
if self.pos ==0:#and bar.close < self.dayOpen
self.short(bar.close,self.open_pos,True)
elif self.pos > 0:
self.sell(bar.close,abs(self.pos),True)
if bar.datetime.minute % self.OCM == 0:
# 如果已经有聚合minuteK线
if self.minutebar:
# 将最新分钟的数据更新到目前minute线中
minutebar = self.minutebar
minutebar.high = max(minutebar.high, bar.high)
minutebar.low = min(minutebar.low, bar.low)
minutebar.close = bar.close
# 推送minute线数据
self.onminutebar(minutebar)
# 清空minute线数据缓存
self.minutebar = None
else:
# 如果没有缓存则新建
if not self.minutebar:
minutebar = VtBarData()
minutebar.vtSymbol = bar.vtSymbol
minutebar.symbol = bar.symbol
minutebar.exchange = bar.exchange
minutebar.open = bar.open
minutebar.high = bar.high
minutebar.low = bar.low
minutebar.close = bar.close
minutebar.date = bar.date
minutebar.time = bar.time
minutebar.datetime = bar.datetime
self.minutebar = minutebar
else:
minutebar = self.minutebar
minutebar.high = max(minutebar.high, bar.high)
minutebar.low = min(minutebar.low, bar.low)
minutebar.close = bar.close
# 发出状态更新事件
self.putEvent()
#----------------------------------------------------------------------
def onminutebar(self,bar):
"""收到Bar推送(必须由用户继承实现)"""
# 撤销之前发出的尚未成交的委托(包括限价单和停止单)
#for orderID in self.orderList:
#self.cancelOrder(orderID)
#self.orderList = []
# 保存K线数据
self.closeArray[0:self.bufferSize-1] = self.closeArray[1:self.bufferSize]
self.highArray[0:self.bufferSize-1] = self.highArray[1:self.bufferSize]
self.lowArray[0:self.bufferSize-1] = self.lowArray[1:self.bufferSize]
self.openArray[0:self.bufferSize-1] = self.openArray[1:self.bufferSize]
self.closeArray[-1] = bar.close
self.highArray[-1] = bar.high
self.lowArray[-1] = bar.low
self.openArray[-1] = bar.open
self.bufferCount += 1
if self.bufferCount < self.bufferSize:
return
# 计算指标数值
ma_20 = talib.EMA(self.closeArray,timeperiod = self.ma_windows1)
ma_200 = talib.EMA(self.closeArray,timeperiod = self.ma_windows2)
self.ma20_value = ma_20[-1]
self.ma200_value = ma_200[-1]
self.LongEnterable = ma_20[-1] > ma_200[-1] and ma_20[-2] < ma_200[-2]
self.ShortEnterable = ma_20[-1] < ma_200[-1] and ma_20[-2] > ma_200[-2]
# 发出状态更新事件
self.putEvent()
#----------------------------------------------------------------------
def onOrder(self, order):
"""收到委托变化推送(必须由用户继承实现)"""
pass
#----------------------------------------------------------------------
def onTrade(self, trade):
# 发出状态更新事件
self.putEvent()
def onStopOrder(self, so):
"""停止单推送"""
pass
本章主要讲述构建海龟组合的痛点和解决方案
在追求策略稳健性前提下,通过传统的滚动测试的方法不能筛选出盈利组合,即这种适用于日内CTA策略的检验方法针对海龟组合是无效的。
构建海龟组合困难表现为以下几点:
a. 历史数据过于少
从2014年到2018年这5年间,其日线数据仅仅是1,200根。其数据过少导致调整回望周期的灵活性降低,即最多回望周期只能设置为2年、3年、4年;同时数据少很难检验出策略的稳健性和有效性。(原版海龟策略历史回测周期是10年)
反观传统日内CTA策略,该策略是基于分钟级别的,1年的分钟K线就接近6,000根。用传统的筛选品种的方法,2年回望周期K线达到12,000根,在数据足够大的情况下更易于体现出策略的稳健性。
b. 回撤过大
海龟策略属于高风险高收益的中低频策略,夸张的比喻就是“3年不开张,开张吃3年”,在设定回测区间可能表现出持续的亏损,但也有可能在之后出现一波大行情,短期的盈利能够快速覆盖长期的亏损。但是滚动回测筛选品种时因为夏普过低而剔除该品种,导致错过隐含策略收益。
c. 筛选指标不稳健
本次滚动回测筛选标准是夏普比率,但是夏普比率对于策略回测时间区间起始点和终点非常敏感,对于趋势的把握不足。举例说明:在2年时间区间夏普比率是1.3,若整体回测区间向后移动3个月,得到的夏普比率可能是0.9。
故夏普比率不适应于海龟策略这种波动性大的类型,必须找到一种指标可以预测其趋势,比如“新式夏普比率”,在2年间比值是1.3,整体向前或者向后滚动3个月,其比值也接近1.3。
d. 样品数量噪音过大
尽管通过历史行情,其2014年前上市品种,已经把检测样本从51个降低到35个。但是对巨大的样品数量检验也造成很大的操作负担。例如一些低流动性或者持续走震荡行数的品种,没有必要来进行海龟策略的测试。
从新对“流动性强”进行定义,在统计意义上,可以指行情波动巨大和具有大趋势,故通过2个指标来专门对35个品种重新进行筛选,这2个指标分别是:
a. 调整后波动率比值:
计算公式是收盘价的标准差/(合约最小价格变动 *100),调整后波动率比值越大,代表其品种的相对波动幅度越大,趋势跟踪策略的盈利能力越强。
举个例子说明,假设趋势跟踪策略正确预测行情的概率是x%,其固定交易成本为c,那么收益率r = (x% * 波动幅度) - 固定成本。所以当波动幅度足够大时,交易才有利可图,否则交易盈利覆盖不了交易成本,导致单子做得越多,亏的也越多。
b. ADF值
这是推论统计学概念,通过ADF检验来得出其ADF值,然后与10%进行比较,若ADF值要大于10%,证明完全不能拒绝原假设,即原假设成立。
原假设为存在单位根,有单位根代表着品种自相关性强,具有趋势行情。
总结一下,增加了两个初步筛选的标准,分别是
代码实现步骤
在jupter notebook上运行下面脚本代码,逐个测试样本
import numpy as np
import pandas as pd
import statsmodels.tsa.stattools as ts
#################################################
def volPriceTick (SYM, PT):
c=pymongo.MongoClient()
symbol = SYM #合约代号
col = c['VnTrader_Daily_Db'][symbol]
cx = col.find()
l = list(cx)
d = {}
for key in l[0].keys():
d[key] = []
for data in l:
for k, v in data.items():
d[k].append(v)
df = pd.DataFrame.from_dict(d)
PriceTick = PT #最小价格变动
close = df['close']
var = np.std(close) #收盘价的标准差
ratio = var/(PriceTick*100) # 定义调整后波动率比值
adfresult = ts.adfuller(close) # 得到ADF值
return ratio,adfresult
调用定义好的函数进行测试,如图所示,品种是沪深300股指期货(IF99),其最小价格变动是0.02。
得到的调整后波动率比值为342.19,比值远大于1说明品种价格变化挺活跃的;然后ADF值是0.36,同样大于10%说明其自相关性强,适用于趋势跟踪类型策略。
在上面已经提到,夏普比率不稳健的特点,导致其很难去识别一些行情趋势,而且对于回撤承受度不高,难于应用于像海龟策略这种高风险高收益的策略。所以产生了新筛选指标的需求。
新的指标应该具有3方面特点:
故开发“回归夏普比率”用于品种筛选,其步骤如下:
在海龟策略回测引擎的calculateResult中加入上面逻辑即可,其代码如下:
#计算回归总收益
daysList = range(1,totalDays+1)
#slope , intercept = np.polyfit (changduList , balanceList , 1) #numpy方法
slope, intercept, r_value, p_value, slope_std_error = stats.linregress(daysList , balanceList) #scipy方法
RegendBalance = intercept + totalDays * slope
RegtotalReturn = (RegendBalance / intercept - 1) * 100
计算回归年化收益
if RegtotalReturn > 0:
RegannualizedReturn = (((1+RegtotalReturn/100) **(annualDays/totalDays) )-1)*100
else:
RegannualizedReturn = -(((1-RegtotalReturn/100) **(annualDays/totalDays) )-1)*100
#计算回归日收益
RegdailyReturn = RegannualizedReturn/annualDays
#计算回归夏普比率
if returnStd:
sharpeRatio = dailyReturn / returnStd * np.sqrt(annualDays)
RegsharpeRatio = RegdailyReturn / returnStd * np.sqrt(annualDays)
else:
sharpeRatio = 0
RegsharpeRatio = 0
新指标的缺点:
综合回归夏普比率的优势和缺点,我们得知其对回撤的容忍度高,可以摒弃噪声从而捕捉到趋势,故适用于作为一个筛选标准;另一方面有时候会出现高估或者低估行情表现,故不能作为判断业绩表现的指标(如标准的夏普比率)。
把回望周期分别设置为2年,3年和4年。
理论上较短回测周期使用较为宽松的筛选标准(如回归夏普比率大于0.4),较长回测周期使用较为苛刻的筛选标准(如回归夏普比率大于0.8)。
当然为了测试的全面新,通过3个不同的回望周期(如:2年、3年、4年),通过不同的回归夏普比率(如:0.4、0.6、0.8)进行筛选,力求新的海龟组合能够较好地预测2018年行情。(即表现出较高的稳健性)
作者:赵信的派 ;来源:维恩的派论坛
ctaHistoryData可以导入历史数据,但不支持wind。这里增加wind的分钟数据导入函数,并在主程序中调用即可。
#----------------------------------------------------------------------
def loadWind(code,dbName, symbol):
"""将wind的历史数据插入到Mongo数据库中"""
from WindPy import *
import pandas as pd
from pandas import DataFrame
w.start()
start = time()
print u'开始读取wind文件%s中的数据插入到%s的%s中' %(code, dbName, symbol)
# 锁定集合,并创建索引
host, port, logging = loadMongoSetting()
client = pymongo.MongoClient(host, port)
collection = client[dbName][symbol]
collection.ensure_index([('datetime', pymongo.ASCENDING)], unique=True)
# 读取数据和插入到数据库
wsd_data = w.wsi(code, "open,high,low,close,volume,amt,oi", "2017-02-19 09:00:00", "2017-05-21 00:06:20", "")
print(wsd_data)
df1=DataFrame(wsd_data.Data,index=wsd_data.Fields,columns=wsd_data.Times)
print '1'
df1=df1.T
df_wind=df1.dropna(subset=['open'])
index=0
print df_wind
for d in df_wind.index:
bar = CtaBarData()
bar.vtSymbol = symbol
bar.symbol = symbol
bar.open = float(df_wind['open'].iloc[index])
bar.high = float(df_wind['high'].iloc[index])
bar.low = float(df_wind['low'].iloc[index])
bar.close = float(df_wind['close'].iloc[index])
print(type(d))
bar.datetime=datetime.strftime(d.to_pydatetime(),'%Y-%m-%d %H:%M:%S')
bar.date = datetime.strftime(d.to_pydatetime(),'%Y%m%d')
bar.time = datetime.strftime(d.to_pydatetime(),'%H:%M:%S')
bar.volume = df_wind['volume'].iloc[index]
flt = {'datetime': bar.datetime}
collection.update_one(flt, {'$set':bar.__dict__}, upsert=True)
print bar.datetime
#主函数
if __name__ == '__main__':
## 简单的测试脚本可以写在这里
#from time import sleep
#e = HistoryDataEngine()
#sleep(1)
#e.downloadEquityDailyBar('000001')
#e.downloadEquityDailyBarts('000001')
# 这里将项目中包含的股指日内分钟线csv导入MongoDB,作者电脑耗时大约3分钟
#loadMcCsv('IF0000_1min.csv', MINUTE_DB_NAME, 'IF0000')
start = time()
#导入wind数据
loadWind("MA709.CZC",MINUTE_DB_NAME, 'MA709')
#测时
print u'插入完毕,耗时:%s' % (time()-start)
#导入通达信历史分钟数据
#loadTdxCsv('CL8.csv', MINUTE_DB_NAME, 'c0000)
传统意义上,为保证策略的稳健性,应该进行样本内外检验,其具体步骤如下:
初步的筛选仅仅基于历史行情。
因为本次测试规定历史回测时间从2014年1月1日开始,故首先剔除在该时间点后才上市的品种,所以进行历史回测的品种从51个降低至35个。
所以一些例如苹果之类非常热门的新品种,不在本次测试范围之内。
滚动回测标准如下
对35个品种进行海龟策略的历史回测,如图所示。(下图小字“夏普统计出错”,其意思是总收益为负数,有时候转换成年化收益变成正数,导致负的夏普比率变成正数。)
根据夏普比率>0.6的准则,筛选出了17个品种,其历史表现和2016年预测表现如图所示。
投资组合在2014-2015年年化收益74.06%,百分比最大回撤-45.09%,夏普比率,1.41,整体上还可以,但是去2016年预测表现非常糟糕。这说明投资组合里噪声过多,稳健性弱。
下面分析一下挑选出来的品种成分,按交易所分类如下:
可以发现占绝大部分的是成交量巨大的品种,推测可能是流动性差的品种,如黄大豆2号等,导致其投资组合在2016年亏损,故非常有必要进行滚动回测来剔除表现不好的品种。
第一次从35个样本中筛选出17个,本次同样将继续剔除噪声因子,试图提高海龟组合的预测效果,测试如图所示。
经过第二轮筛选后,剩下11个品种,同样按照交易所分类,如下:
在新的投资组合中,年化收益达94.05%,百分比最大回撤是-19.94%,夏普比率达2.36,整体资金曲线比较平滑,但是其组合的稳健性仍未提高,在2017年仍然亏损,百分比最大回撤达43.79%,还需要在剔除无效的样本。
第三轮测试也是最后一轮测试,将决定最终海龟组合内品种的构成,其单品种测试效果如图所示。
第三轮筛选后,得到由铝、铜、锌、普麦、铁矿石、焦炭、豆粕组成的海龟组合,都符合了流动性强的特征,该组合的历史回测和2018年预测效果如图所示。
结果同样是不理想,在样本内表现出色,样本外却亏损。
作者:量化学习菜鸟 ;来源:维恩的派论坛
def onBar(self, bar):
"""收到Bar推送(必须由用户继承实现)"""
if self.zhouqi == 'm':
dt = bar.datetime.minute
kaiguan = dt % self.zhouqi_val == 0
elif self.zhouqi == 'h':
dt = bar.datetime.hour
kaiguan = dt % self.zhouqi_val == 0 and bar.datetime.minute == 0
if kaiguan:
if self.fiveBar:
fiveBar = self.fiveBar
fiveBar.high = max(fiveBar.high, bar.high)
fiveBar.low = min(fiveBar.low, bar.low)
fiveBar.close = bar.close
# 推送
self.onFiveBar(fiveBar)
# 清空缓存
self.fiveBar = None
else:
fiveBar = VtBarData()
fiveBar.vtSymbol = bar.vtSymbol
fiveBar.symbol = bar.symbol
fiveBar.exchange = bar.exchange
fiveBar.open = bar.open
fiveBar.high = bar.high
fiveBar.low = bar.low
fiveBar.close = bar.close
fiveBar.date = bar.date
fiveBar.time = bar.time
fiveBar.datetime = bar.datetime
#发出去
self.onFiveBar(fiveBar)
#清空
self.fiveBar = None
else:
# 如果没有缓存则新建
#print 'dt zhengchu//0'
if not self.fiveBar:
fiveBar = VtBarData()
fiveBar.vtSymbol = bar.vtSymbol
fiveBar.symbol = bar.symbol
fiveBar.exchange = bar.exchange
fiveBar.open = bar.open
fiveBar.high = bar.high
fiveBar.low = bar.low
fiveBar.close = bar.close
fiveBar.date = bar.date
fiveBar.time = bar.time
fiveBar.datetime = bar.datetime
self.fiveBar = fiveBar
else:
fiveBar = self.fiveBar
fiveBar.high = max(fiveBar.high, bar.high)
fiveBar.low = min(fiveBar.low, bar.low)
fiveBar.close = bar.close
作者:moonnejs ;来源:维恩的派论坛
为了研究高频策略,我优化了下vnpy的回测引擎,有错误的地方希望大家指点。
大概优化了下面这些内容:
# encoding: UTF-8
'''
本文件包含了CTA引擎中的策略开发用模板,开发策略时需要继承CtaTemplate类。
'''
import datetime
import copy
from ctaBase import *
from vtConstant import *
from vtGateway import *
########################################################################
class CtaTemplate1(object):
"""CTA策略模板"""
# 策略类的名称和作者
className = 'CtaTemplate'
author = EMPTY_UNICODE
# MongoDB数据库的名称,K线数据库默认为1分钟
tickDbName = TICK_DB_NAME
barDbName = MINUTE_DB_NAME
productClass = EMPTY_STRING # 产品类型(只有IB接口需要)
currency = EMPTY_STRING # 货币(只有IB接口需
# 策略的基本参数
name = EMPTY_UNICODE # 策略实例名称
vtSymbol = EMPTY_STRING # 交易的合约vt系统代码
vtSymbol1 = EMPTY_STRING # 交易的合约2vt系统代码
# 策略的基本变量,由引擎管理
inited = False # 是否进行了初始化
trading = False # 是否启动交易,由引擎管理
backtesting = False # 回测模式
pos = 0 # 总投机方向
pos1 = 0 # 总投机方向
tpos0L = 0 # 今持多仓
tpos0S = 0 # 今持空仓
ypos0L = 0 # 昨持多仓
ypos0S = 0 # 昨持空仓
tpos1L = 0 # 今持多仓
tpos1S = 0 # 今持空仓
ypos1L = 0 # 昨持多仓
ypos1S = 0 # 昨持空仓
# 参数列表,保存了参数的名称
paramList = ['name',
'className',
'author',
'vtSymbol']
# 变量列表,保存了变量的名称
varList = ['inited',
'trading',
'pos']
#----------------------------------------------------------------------
def __init__(self, ctaEngine, setting):
"""Constructor"""
self.ctaEngine = ctaEngine
self.productClass = EMPTY_STRING # 产品类型(只有IB接口需要)
self.currency = EMPTY_STRING # 货币(只有IB接口需
# 策略的基本变量,由引擎管理
self.inited = False # 是否进行了初始化
self.trading = False # 是否启动交易,由引擎管理
self.backtesting = False # 回测模式
self.pos = 0 # 总投机方向
self.pos1 = 0 # 总投机方向
self.tpos0L = 0 # 今持多仓
self.tpos0S = 0 # 今持空仓
self.ypos0L = 0 # 昨持多仓
self.ypos0S = 0 # 昨持空仓
self.tpos1L = 0 # 今持多仓
self.tpos1S = 0 # 今持空仓
self.ypos1L = 0 # 昨持多仓
self.ypos1S = 0 # 昨持空仓
# 设置策略的参数
if setting:
d = self.__dict__
for key in self.paramList:
if key in setting:
d[key] = setting[key]
#----------------------------------------------------------------------
def onInit(self):
"""初始化策略(必须由用户继承实现)"""
raise NotImplementedError
#----------------------------------------------------------------------
def onUpdate(self,setting):
"""刷新策略"""
if setting:
d = self.__dict__
for key in self.paramList:
if key in setting:
d[key] = setting[key]
#----------------------------------------------------------------------
def onStart(self):
"""启动策略(必须由用户继承实现)"""
raise NotImplementedError
#----------------------------------------------------------------------
def onStop(self):
"""停止策略(必须由用户继承实现)"""
raise NotImplementedError
#----------------------------------------------------------------------
def confSettle(self, vtSymbol):
"""确认结算信息"""
self.ctaEngine.confSettle(vtSymbol)
#----------------------------------------------------------------------
def onTick(self, tick):
"""收到行情TICK推送(必须由用户继承实现)"""
if not self.backtesting:
hour = datetime.datetime.now().hour
if hour >= 16 and hour <= 19:
return
if hour >= 2 and hour <= 7:
return
if hour >= 12 and hour <= 12:
return
if tick.datetime.hour == 20 and tick.datetime.minute == 59:
self.output(u'开始确认结算信息')
self.confSettle(self.vtSymbol)
if tick.datetime.hour == 20 and tick.datetime.minute == 59:
self.ypos0L += self.tpos0L
self.tpos0L = 0
self.ypos0S += self.tpos0S
self.tpos0S = 0
self.ypos1L += self.tpos1L
self.tpos1L = 0
self.ypos1S += self.tpos1S
self.tpos1S = 0
#----------------------------------------------------------------------
def onOrder(self, order):
"""收到委托变化推送(必须由用户继承实现)"""
pass
#----------------------------------------------------------------------
def onTrade(self, trade):
"""收到成交推送(必须由用户继承实现)"""
# 对于无需做细粒度委托控制的策略,可以忽略onOrder
# CTA委托类型映射
if trade != None and trade.direction == u'多':
if trade.vtSymbol == self.vtSymbol:
self.pos += trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.pos1 += trade.volume
if trade != None and trade.direction == u'空':
if trade.vtSymbol == self.vtSymbol:
self.pos -= trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.pos1 -= trade.volume
if trade != None and trade.direction == u'多' and trade.offset == u'开仓':
if trade.vtSymbol == self.vtSymbol:
self.tpos0L += trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.tpos1L += trade.volume
elif trade != None and trade.direction == u'空' and trade.offset == u'开仓':
if trade.vtSymbol == self.vtSymbol:
self.tpos0S += trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.tpos1S += trade.volume
elif trade != None and trade.direction == u'多' and trade.offset == u'平仓':
if trade.vtSymbol == self.vtSymbol:
self.ypos0S -= trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.ypos1S -= trade.volume
elif trade != None and trade.direction == u'多' and trade.offset == u'平今':
if trade.vtSymbol == self.vtSymbol:
self.tpos0S -= trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.tpos1S -= trade.volume
elif trade != None and trade.direction == u'多' and trade.offset == u'平昨':
if trade.vtSymbol == self.vtSymbol:
self.ypos0S -= trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.ypos1S -= trade.volume
elif trade != None and trade.direction == u'空' and trade.offset == u'平仓':
if trade.vtSymbol == self.vtSymbol:
self.ypos0L -= trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.ypos1L -= trade.volume
elif trade != None and trade.direction == u'空' and trade.offset == u'平今':
if trade.vtSymbol == self.vtSymbol:
self.tpos0L -= trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.tpos1L -= trade.volume
elif trade != None and trade.direction == u'空' and trade.offset == u'平昨':
if trade.vtSymbol == self.vtSymbol:
self.ypos0L -= trade.volume
elif trade.vtSymbol == self.vtSymbol1:
self.ypos1L -= trade.volume
#----------------------------------------------------------------------
def onBar(self, bar):
"""收到Bar推送(必须由用户继承实现)"""
raise NotImplementedError
#----------------------------------------------------------------------
def buy(self, price, volume, stop=False):
"""买开"""
return self.sendOrder(CTAORDER_BUY, price, volume, stop)
#----------------------------------------------------------------------
def sell(self, price, volume, stop=False):
"""卖平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos0L)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder(CTAORDER_SELL_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder(CTAORDER_SELL, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def sell_y(self, price, volume, stop=False):
"""卖平"""
return self.sendOrder(CTAORDER_SELL, price, volume, stop)
def sell_t(self, price, volume, stop=False):
"""卖平"""
return self.sendOrder(CTAORDER_SELL_TODAY, price, volume, stop)
#----------------------------------------------------------------------
def sell1_y(self, price, volume, stop=False):
"""卖平"""
return self.sendOrder1(CTAORDER_SELL, price, volume, stop)
def sell1_t(self, price, volume, stop=False):
"""卖平"""
return self.sendOrder1(CTAORDER_SELL_TODAY, price, volume, stop)
#----------------------------------------------------------------------
def short(self, price, volume, stop=False):
"""卖开"""
return self.sendOrder(CTAORDER_SHORT, price, volume, stop)
#----------------------------------------------------------------------
def cover(self, price, volume, stop=False):
"""买平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos0S)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder(CTAORDER_COVER_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder(CTAORDER_COVER, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def cover_y(self, price, volume, stop=False):
"""买平"""
return self.sendOrder(CTAORDER_COVER, price, volume, stop)
#----------------------------------------------------------------------
def cover_t(self, price, volume, stop=False):
"""买平"""
return self.sendOrder(CTAORDER_COVER_TODAY, price, volume, stop)
#----------------------------------------------------------------------
def cover1_y(self, price, volume, stop=False):
"""买平"""
return self.sendOrder1(CTAORDER_COVER, price, volume, stop)
#----------------------------------------------------------------------
def cover1_t(self, price, volume, stop=False):
"""买平"""
return self.sendOrder1(CTAORDER_COVER_TODAY, price, volume, stop)
#----------------------------------------------------------------------
def buy_fok(self, price, volume, stop=False):
"""买开"""
return self.sendOrderFOK(CTAORDER_BUY, price, volume, stop)
#----------------------------------------------------------------------
def sell_fok(self, price, volume, stop=False):
"""卖平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos0L)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrderFOK(CTAORDER_SELL_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrderFOK(CTAORDER_SELL, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def short_fok(self, price, volume, stop=False):
"""卖开"""
return self.sendOrderFOK(CTAORDER_SHORT, price, volume, stop)
#----------------------------------------------------------------------
def cover_fok(self, price, volume, stop=False):
"""买平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos0S)
y_vol = volume - t_vol
if t_vol <= 0 and y_vol <=0:
self.output("买平出错")
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrderFOK(CTAORDER_COVER_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrderFOK(CTAORDER_COVER, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def buy_fak(self, price, volume, stop=False):
"""买开"""
return self.sendOrderFAK(CTAORDER_BUY, price, volume, stop)
#----------------------------------------------------------------------
def sell_fak(self, price, volume, stop=False):
"""卖平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos0L)
y_vol = volume - t_vol
if t_vol <= 0 and y_vol <=0:
self.output("卖平出错")
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrderFAK(CTAORDER_SELL_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrderFAK(CTAORDER_SELL, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def short_fak(self, price, volume, stop=False):
"""卖开"""
return self.sendOrderFAK(CTAORDER_SHORT, price, volume, stop)
#----------------------------------------------------------------------
def cover_fak(self, price, volume, stop=False):
"""买平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos0S)
y_vol = volume - t_vol
if t_vol <= 0 and y_vol <=0:
self.output("买平出错")
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder_FAK(CTAORDER_COVER_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder_FAK(CTAORDER_COVER, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def sendOrder(self, orderType, price, volume, stop=False):
"""发送委托"""
if self.trading:
# 如果stop为True,则意味着发本地停止单
if stop:
vtOrderID = self.ctaEngine.sendStopOrder(self.vtSymbol, orderType, price, volume, self)
else:
vtOrderID = self.ctaEngine.sendOrder(self.vtSymbol, orderType, price, volume, self)
return vtOrderID
else:
return ''
#----------------------------------------------------------------------
def sendOrderFOK(self, orderType, price, volume, stop=False):
"""发送委托"""
if self.trading:
# 如果stop为True,则意味着发本地停止单
if stop:
vtOrderID = self.ctaEngine.sendStopOrder(self.vtSymbol, orderType, price, volume, self)
else:
vtOrderID = self.ctaEngine.sendOrderFOK(self.vtSymbol, orderType, price, volume, self)
return vtOrderID
else:
return ''
#----------------------------------------------------------------------
def sendOrderFAK(self, orderType, price, volume, stop=False):
"""发送委托"""
if self.trading:
# 如果stop为True,则意味着发本地停止单
if stop:
vtOrderID = self.ctaEngine.sendStopOrder(self.vtSymbol, orderType, price, volume, self)
else:
vtOrderID = self.ctaEngine.sendOrderFAK(self.vtSymbol, orderType, price, volume, self)
return vtOrderID
else:
return ''
#----------------------------------------------------------------------
def buy1(self, price, volume, stop=False):
"""买开"""
return self.sendOrder1(CTAORDER_BUY, price, volume, stop)
#----------------------------------------------------------------------
def sell1(self, price, volume, stop=False):
"""卖平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos1L)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder1(CTAORDER_SELL_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder1(CTAORDER_SELL, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def short1(self, price, volume, stop=False):
"""卖开"""
return self.sendOrder1(CTAORDER_SHORT, price, volume, stop)
#----------------------------------------------------------------------
def cover1(self, price, volume, stop=False):
"""买平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos1S)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder1(CTAORDER_COVER_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder1(CTAORDER_COVER, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def buy1_fok(self, price, volume, stop=False):
"""买开"""
return self.sendOrder1FOK(CTAORDER_BUY, price, volume, stop)
#----------------------------------------------------------------------
def sell1_fok(self, price, volume, stop=False):
"""卖平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos1L)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder1FOK(CTAORDER_SELL_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder1FOK(CTAORDER_SELL, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def short1_fok(self, price, volume, stop=False):
"""卖开"""
return self.sendOrder1FOK(CTAORDER_SHORT, price, volume, stop)
#----------------------------------------------------------------------
def cover1_fok(self, price, volume, stop=False):
"""买平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos1S)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder1FOK(CTAORDER_COVER_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder1FOK(CTAORDER_COVER, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def buy1_fak(self, price, volume, stop=False):
"""买开"""
return self.sendOrder1FAK(CTAORDER_BUY, price, volume, stop)
#----------------------------------------------------------------------
def sell1_fak(self, price, volume, stop=False):
"""卖平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos1L)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder1FAK(CTAORDER_SELL_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder1FAK(CTAORDER_SELL, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def short1_fak(self, price, volume, stop=False):
"""卖开"""
return self.sendOrder1FAK(CTAORDER_SHORT, price, volume, stop)
#----------------------------------------------------------------------
def cover1_fak(self, price, volume, stop=False):
"""买平"""
t_vol = 0
y_vol = 0
t_vol = min(volume,self.tpos1S)
y_vol = volume - t_vol
orderId = None
orderId1 = None
orderIds = []
if t_vol > 0:
orderId=self.sendOrder1FAK(CTAORDER_COVER_TODAY, price, t_vol, stop)
if t_vol ==0 and y_vol > 0:
orderId1=self.sendOrder1FAK(CTAORDER_COVER, price, y_vol, stop)
if orderId:
orderIds.append(orderId)
if orderId1:
orderIds.append(orderId1)
return orderIds
#----------------------------------------------------------------------
def sendOrder1(self, orderType, price, volume, stop=False):
"""发送委托"""
if self.trading:
# 如果stop为True,则意味着发本地停止单
if stop:
vtOrderID = self.ctaEngine.sendStopOrder(self.vtSymbol1, orderType, price, volume, self)
else:
vtOrderID = self.ctaEngine.sendOrder(self.vtSymbol1, orderType, price, volume, self)
return vtOrderID
else:
return ''
#----------------------------------------------------------------------
def sendOrder1FOK(self, orderType, price, volume, stop=False):
"""发送委托"""
if self.trading:
# 如果stop为True,则意味着发本地停止单
if stop:
vtOrderID = self.ctaEngine.sendStopOrder(self.vtSymbol1, orderType, price, volume, self)
else:
vtOrderID = self.ctaEngine.sendOrderFOK(self.vtSymbol1, orderType, price, volume, self)
return vtOrderID
else:
return ''
#----------------------------------------------------------------------
def sendOrder1FAK(self, orderType, price, volume, stop=False):
"""发送委托"""
if self.trading:
# 如果stop为True,则意味着发本地停止单
if stop:
vtOrderID = self.ctaEngine.sendStopOrder(self.vtSymbol1, orderType, price, volume, self)
else:
vtOrderID = self.ctaEngine.sendOrderFAK(self.vtSymbol1, orderType, price, volume, self)
return vtOrderID
else:
return ''
#----------------------------------------------------------------------
def cancelOrder(self, vtOrderID):
"""撤单"""
return self.ctaEngine.cancelOrder(vtOrderID)
#if STOPORDERPREFIX in vtOrderID:
# return self.ctaEngine.cancelStopOrder(vtOrderID)
#else:
# return self.ctaEngine.cancelOrder(vtOrderID)
#----------------------------------------------------------------------
def insertTick(self, tick):
"""向数据库中插入tick数据"""
self.ctaEngine.insertData(self.tickDbName, self.vtSymbol, tick)
#----------------------------------------------------------------------
def insertBar(self, bar):
"""向数据库中插入bar数据"""
self.ctaEngine.insertData(self.barDbName, self.vtSymbol, bar)
#----------------------------------------------------------------------
def loadTick(self, days):
"""读取tick数据"""
return self.ctaEngine.loadTick(self.tickDbName, self.vtSymbol, days)
#----------------------------------------------------------------------
def loadBar(self, days):
"""读取bar数据"""
return self.ctaEngine.loadBar(self.barDbName, self.vtSymbol, days)
#----------------------------------------------------------------------
def writeCtaLog(self, content):
"""记录CTA日志"""
content = self.name + ':' + content
self.ctaEngine.writeCtaLog(content)
#----------------------------------------------------------------------
def putEvent(self):
"""发出策略状态变化事件"""
self.ctaEngine.putStrategyEvent(self.name)
# encoding: UTF-8
'''
本文件中包含的是CTA模块的回测引擎,回测引擎的API和CTA引擎一致,
可以使用和实盘相同的代码进行回测。
'''
from __future__ import division
from itertools import product
import copy
import os
import csv
import multiprocessing
import json
import pymongo
import threading
from datetime import datetime
from collections import OrderedDict
from progressbar import ProgressBar
from collections import deque
from ctaBase import *
from vtConstant import *
from vtBase import VtOrderData, VtTradeData
from vtFunction import loadMongoSetting
CAPITAL_DB_NAME = 'vt_trader_cap_db'
########################################################################
class BacktestingEngine(object):
"""
CTA回测引擎
函数接口和策略引擎保持一样,
从而实现同一套代码从回测到实盘。
增加双合约回测功能
增加快速慢速切换功能(挂单策略建议使用快速模式)
"""
TICK_MODE = 'tick'
BAR_MODE = 'bar'
bufferSize = 1000
Version = 20161122
#----------------------------------------------------------------------
def __init__(self,optimism=False):
"""Constructor"""
# 本地停止单编号计数
self.stopOrderCount = 0
# stopOrderID = STOPORDERPREFIX + str(stopOrderCount)
# 本地停止单字典
# key为stopOrderID,value为stopOrder对象
self.stopOrderDict = {} # 停止单撤销后不会从本字典中删除
self.workingStopOrderDict = {} # 停止单撤销后会从本字典中删除
# 回测相关
self.strategy = None # 回测策略
self.mode = self.BAR_MODE # 回测模式,默认为K线
self.shfe = True # 上期所
self.fast = False # 是否支持排队
self.plot = True
self.plotfile = False
self.optimism = False
self.leverage = 0.07
self.slippage = 0 # 回测时假设的滑点
self.rate = 0 # 回测时假设的佣金比例(适用于百分比佣金)
self.rate1 = 0 # 回测时假设的佣金比例(适用于百分比佣金)
self.size = 1 # 合约大小,默认为1
self.size1 = 1 # 合约大小,默认为1
self.mPrice = 1 # 最小价格变动,默认为1
self.mPrice1 = 1 # 最小价格变动,默认为1
self.dbClient = None # 数据库客户端
self.mcClient = None # 数据库客户端
self.dbCursor = None # 数据库指针
self.dbCursor1 = None # 数据库指针
self.backtestingData = deque([]) # 回测用的数据
self.backtestingData1 = deque([]) # 回测用的数据
self.dataStartDate = None # 回测数据开始日期,datetime对象
self.dataEndDate = None # 回测数据结束日期,datetime对象
self.strategyStartDate = None # 策略启动日期(即前面的数据用于初始化),datetime对象
self.limitOrderDict = OrderedDict() # 限价单字典
self.workingLimitOrderDict = OrderedDict() # 活动限价单字典,用于进行撮合用
self.limitOrderCount = 0 # 限价单编号
self.limitOrderDict1 = OrderedDict() # 合约2限价单字典
self.workingLimitOrderDict1 = OrderedDict() # 合约2活动限价单字典,用于进行撮合用
self.tradeCount = 0 # 成交编号
self.tradeDict = OrderedDict() # 成交字典
self.tradeCount1 = 0 # 成交编号1
self.tradeDict1 = OrderedDict() # 成交字典1
self.tradeSnap = OrderedDict() # 主合约市场快照
self.tradeSnap1 = OrderedDict() # 副合约市场快照
self.trade1Snap = OrderedDict() # 主合约市场快照1
self.trade1Snap1 = OrderedDict() # 副合约市场快照1
self.i=0 # 主合约数据准备进度
self.j=0 # 副合约数据准备进度
self.dataClass = None
self.logList = [] # 日志记录
self.orderPrice = {} # 主合约限价单价格
self.orderVolume = {} # 副合约限价单盘口
self.orderPrice1 = {} # 限价单价格
self.orderVolume1 = {} # 限价单盘口
# 当前最新数据,用于模拟成交用
self.tick = None
self.tick1 = None
self.lasttick = None
self.lasttick1 = None
self.bar = None
self.bar1 = None
self.dt = None # 最新的时间
#----------------------------------------------------------------------
def setStartDate(self, startDate='20100416', initDays=10):
"""设置回测的启动日期
支持两种日期模式"""
if len(startDate) == 8:
self.dataStartDate = datetime.strptime(startDate, '%Y%m%d')
else:
self.dataStartDate = datetime.strptime(startDate, '%Y%m%d %H:%M:%S')
#----------------------------------------------------------------------
def setEndDate(self, endDate='20100416'):
"""设置回测的结束日期
支持两种日期模式"""
if len(endDate) == 8:
self.dataEndDate= datetime.strptime(endDate, '%Y%m%d')
else:
self.dataEndDate= datetime.strptime(endDate, '%Y%m%d %H:%M:%S')
#----------------------------------------------------------------------
def setBacktestingMode(self, mode):
"""设置回测模式"""
self.mode = mode
if self.mode == self.BAR_MODE:
self.dataClass = CtaBarData
else:
self.dataClass = CtaTickData
#----------------------------------------------------------------------
def loadHistoryData1(self, dbName, symbol):
"""载入历史数据"""
host, port = loadMongoSetting()
if not self.dbClient:
self.dbClient = pymongo.MongoClient(host, port, socketKeepAlive=True)
collection = self.dbClient[dbName][symbol]
self.output(u'开始载入合约2数据')
# 首先根据回测模式,确认要使用的数据类
if self.mode == self.BAR_MODE:
dataClass = CtaBarData
func = self.newBar1
else:
dataClass = CtaTickData
func = self.newTick1
self.output("Start : " + str(self.dataStartDate))
self.output("End : " + str(self.dataEndDate))
# 载入回测数据
if not self.dataEndDate:
flt = {'datetime':{'$gte':self.dataStartDate}} # 数据过滤条件
else:
flt = {'datetime':{'$gte':self.dataStartDate,
'$lte':self.dataEndDate}}
self.dbCursor1 = collection.find(flt,no_cursor_timeout=True).batch_size(self.bufferSize)
self.output(u'载入完成,数据量:%s' %(self.dbCursor1.count()))
self.output(u' ')
#----------------------------------------------------------------------
def loadHistoryData(self, dbName, symbol):
"""载入历史数据"""
host, port = loadMongoSetting()
if not self.dbClient:
self.dbClient = pymongo.MongoClient(host, port, socketKeepAlive=True)
collection = self.dbClient[dbName][symbol]
self.output(u'开始载入数据')
# 首先根据回测模式,确认要使用的数据类
if self.mode == self.BAR_MODE:
dataClass = CtaBarData
func = self.newBar
else:
dataClass = CtaTickData
func = self.newTick
# 载入回测数据
self.output("Start : " + str(self.dataStartDate))
self.output("End : " + str(self.dataEndDate))
if not self.dataEndDate:
flt = {'datetime':{'$gte':self.dataStartDate}} # 数据过滤条件
else:
flt = {'datetime':{'$gte':self.dataStartDate,
'$lte':self.dataEndDate}}
self.dbCursor = collection.find(flt,no_cursor_timeout=True).batch_size(self.bufferSize)
self.output(u'载入完成,数据量:%s' %(self.dbCursor.count()))
self.output(u' ')
#----------------------------------------------------------------------
def prepareData(self,dbCursor_count,dbCursor_count1):
"""数据准备线程"""
while len(self.backtestingData) < self.bufferSize and self.j < dbCursor_count:
d = self.dbCursor.next()
data = self.dataClass()
data.__dict__ = d
self.backtestingData.append(data)
self.j += 1
while len(self.backtestingData1) < self.bufferSize and self.i < dbCursor_count1:
d1 = self.dbCursor1.next()
data1 = self.dataClass()
data1.__dict__ = d1
self.backtestingData1.append(data1)
self.i += 1
#----------------------------------------------------------------------
def runBacktesting(self):
"""运行回测
判断是否双合约"""
if self.strategy.vtSymbol1 == None:
self.runBacktesting_one()
else:
self.runBacktesting_two()
#----------------------------------------------------------------------
def runBacktesting_two(self):
"""运行回测"""
if self.mode == self.BAR_MODE:
self.dataClass = CtaBarData
func = self.newBar
func1 = self.newBar1
else:
self.dataClass = CtaTickData
func = self.newTick
func1 = self.newTick1
func2 = self.newTick01
self.output(u'开始回测')
self.strategy.inited = True
self.strategy.onInit()
self.output(u'策略初始化完成')
self.strategy.trading = True
self.strategy.onStart()
self.output(u'策略启动完成')
self.output(u'开始回放数据2')
dbCursor_count=self.dbCursor.count()
dbCursor_count1=self.dbCursor1.count()
self.i = 0;
self.j = 0;
lastData = None
lastData1 = None
t = None
# 双合约回测
while (self.i < dbCursor_count1 and self.j < dbCursor_count) or (self.backtestingData and self.backtestingData1):
# 启动数据准备线程
t = threading.Thread(target=self.prepareData,args=(dbCursor_count,dbCursor_count1))
t.start()
# 模拟撮合
while self.backtestingData and self.backtestingData1:
# 考虑切片数据可能不连续,同步两个合约的数据
if self.backtestingData1[0].datetime > self.backtestingData[0].datetime:
if lastData1:
func2(self.backtestingData[0],lastData1)
lastData = self.backtestingData.popleft()
elif self.backtestingData[0].datetime > self.backtestingData1[0].datetime:
if lastData:
func2(lastData,self.backtestingData1[0])
lastData1 = self.backtestingData1.popleft()
elif self.backtestingData and self.backtestingData1 and self.backtestingData1[0].datetime == self.backtestingData[0].datetime:
func2(self.backtestingData[0],self.backtestingData1[0])
lastData = self.backtestingData.popleft()
lastData1 = self.backtestingData1.popleft()
t.join()
self.strategy.onStop()
self.output(u'数据回放结束')
#----------------------------------------------------------------------
def runBacktesting_one(self):
"""运行回测"""
if self.mode == self.BAR_MODE:
self.dataClass = CtaBarData
func = self.newBar
func1 = self.newBar1
else:
self.dataClass = CtaTickData
func = self.newTick
self.output(u'开始回测')
self.strategy.inited = True
self.strategy.onInit()
self.output(u'策略初始化完成')
self.strategy.trading = True
self.strategy.onStart()
self.output(u'策略启动完成')
self.output(u'开始回放数据1')
dbCursor_count=self.dbCursor.count()
self.j = 0;
self.i = 0;
dbCursor_count1 = 0
lastData = None
lastData1 = None
t = None
# 单合约回测
while self.j < dbCursor_count or self.backtestingData:
# 启动数据准备线程
t = threading.Thread(target=self.prepareData,args=(dbCursor_count,dbCursor_count1))
t.start()
# 模拟撮合
while self.backtestingData:
lastData = self.backtestingData.popleft()
func(lastData)
t.join()
self.strategy.onStop()
self.output(u'数据回放结束')
#----------------------------------------------------------------------
def newBar(self, bar):
"""新的K线"""
self.bar = bar
self.dt = bar.datetime
self.crossLimitOrder() # 先撮合限价单
#self.crossStopOrder() # 再撮合停止单
self.strategy.onBar(bar) # 推送K线到策略中
#----------------------------------------------------------------------
def newBar1(self, bar):
"""新的K线"""
self.bar1 = bar
self.dt = bar.datetime
self.crossLimitOrder1() # 先撮合限价单
self.strategy.onBar(bar) # 推送K线到策略中
#----------------------------------------------------------------------
def newTick(self, tick):
"""新的Tick"""
self.tick = tick
self.dt = tick.datetime
# 低速模式(延时1个Tick撮合)
self.crossLimitOrder()
self.strategy.onTick(tick)
# 高速模式(直接撮合)
if self.optimism:
self.crossLimitOrder()
self.lasttick = tick
#----------------------------------------------------------------------
def newTick1(self, tick):
"""新的Tick"""
self.tick1 = tick
self.dt = tick.datetime
# 低速模式(延时1个Tick撮合)
self.crossLimitOrder()
self.strategy.onTick(tick)
# 高速模式(直接撮合)
if self.optimism:
self.crossLimitOrder()
self.lasttick1 = tick
#----------------------------------------------------------------------
def newTick01(self, tick, tick1):
"""新的Tick"""
self.dt = tick.datetime
self.tick = tick
self.tick1 = tick1
# 低速模式(延时1个Tick撮合)
self.crossLimitOrder1()
self.crossLimitOrder()
# 没有切片的合约不发送行情(为了和实盘一致)
if tick1.datetime >= tick.datetime:
self.strategy.onTick(self.tick1)
if tick.datetime >= tick1.datetime:
self.strategy.onTick(self.tick)
# 高速模式(直接撮合)
if self.optimism:
self.crossLimitOrder1()
self.crossLimitOrder()
self.lasttick = self.tick
self.lasttick1 = self.tick1
#----------------------------------------------------------------------
def initStrategy(self, strategyClass, setting=None):
"""
初始化策略
setting是策略的参数设置,如果使用类中写好的默认设置则可以不传该参数
"""
self.strategy = strategyClass(self, setting)
#self.strategy.name = self.strategy.className
#----------------------------------------------------------------------
def sendOrder(self, vtSymbol, orderType, price, volume, strategy):
"""发单"""
self.limitOrderCount += 1
orderID = str(self.limitOrderCount)
order = VtOrderData()
order.vtSymbol = vtSymbol
order.price = price
order.priceType = PRICETYPE_LIMITPRICE
order.totalVolume = volume
order.status = STATUS_NOTTRADED # 刚提交尚未成交
order.orderID = orderID
order.vtOrderID = orderID
order.orderTime = str(self.dt)
# CTA委托类型映射
if orderType == CTAORDER_BUY:
order.direction = DIRECTION_LONG
order.offset = OFFSET_OPEN
elif orderType == CTAORDER_SELL and not self.shfe:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSE
elif orderType == CTAORDER_SELL and self.shfe:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSEYESTERDAY
elif orderType == CTAORDER_SELL_TODAY:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSETODAY
elif orderType == CTAORDER_SHORT:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_OPEN
elif orderType == CTAORDER_COVER and not self.shfe:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSE
elif orderType == CTAORDER_COVER and self.shfe:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSEYESTERDAY
elif orderType == CTAORDER_COVER_TODAY:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSETODAY
# 保存到限价单字典中
if vtSymbol == strategy.vtSymbol:
self.workingLimitOrderDict[orderID] = order
self.limitOrderDict[orderID] = order
elif vtSymbol == strategy.vtSymbol1:
self.workingLimitOrderDict1[orderID] = order
self.limitOrderDict1[orderID] = order
return orderID
#----------------------------------------------------------------------
def sendOrderFAK(self, vtSymbol, orderType, price, volume, strategy):
"""发单"""
self.limitOrderCount += 1
orderID = str(self.limitOrderCount)
order = VtOrderData()
order.vtSymbol = vtSymbol
order.price = price
order.priceType = PRICETYPE_FAK
order.totalVolume = volume
order.status = STATUS_NOTTRADED # 刚提交尚未成交
order.orderID = orderID
order.vtOrderID = orderID
order.orderTime = str(self.dt)
# CTA委托类型映射
if orderType == CTAORDER_BUY:
order.direction = DIRECTION_LONG
order.offset = OFFSET_OPEN
elif orderType == CTAORDER_SELL and not self.shfe:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSE
elif orderType == CTAORDER_SELL and self.shfe:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSEYESTERDAY
elif orderType == CTAORDER_SELL_TODAY:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSETODAY
elif orderType == CTAORDER_SHORT:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_OPEN
elif orderType == CTAORDER_COVER and not self.shfe:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSE
elif orderType == CTAORDER_COVER and self.shfe:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSEYESTERDAY
elif orderType == CTAORDER_COVER_TODAY:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSETODAY
# 保存到限价单字典中
if vtSymbol == strategy.vtSymbol:
self.workingLimitOrderDict[orderID] = order
self.limitOrderDict[orderID] = order
elif vtSymbol == strategy.vtSymbol1:
self.workingLimitOrderDict1[orderID] = order
self.limitOrderDict1[orderID] = order
return orderID
#----------------------------------------------------------------------
def sendOrderFOK(self, vtSymbol, orderType, price, volume, strategy):
"""发单"""
self.limitOrderCount += 1
orderID = str(self.limitOrderCount)
order = VtOrderData()
order.vtSymbol = vtSymbol
order.price = price
order.priceType = PRICETYPE_FOK
order.totalVolume = volume
order.status = STATUS_NOTTRADED # 刚提交尚未成交
order.orderID = orderID
order.vtOrderID = orderID
order.orderTime = str(self.dt)
# CTA委托类型映射
if orderType == CTAORDER_BUY:
order.direction = DIRECTION_LONG
order.offset = OFFSET_OPEN
elif orderType == CTAORDER_SELL and not self.shfe:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSE
elif orderType == CTAORDER_SELL and self.shfe:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSEYESTERDAY
elif orderType == CTAORDER_SELL_TODAY:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_CLOSETODAY
elif orderType == CTAORDER_SHORT:
order.direction = DIRECTION_SHORT
order.offset = OFFSET_OPEN
elif orderType == CTAORDER_COVER and not self.shfe:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSE
elif orderType == CTAORDER_COVER and self.shfe:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSEYESTERDAY
elif orderType == CTAORDER_COVER_TODAY:
order.direction = DIRECTION_LONG
order.offset = OFFSET_CLOSETODAY
# 保存到限价单字典中
if vtSymbol == strategy.vtSymbol:
self.workingLimitOrderDict[orderID] = order
self.limitOrderDict[orderID] = order
elif vtSymbol == strategy.vtSymbol1:
self.workingLimitOrderDict1[orderID] = order
self.limitOrderDict1[orderID] = order
return orderID
#----------------------------------------------------------------------
def cancelOrder(self, vtOrderID):
"""撤单"""
# 找到订单
if vtOrderID in self.workingLimitOrderDict:
order = self.workingLimitOrderDict[vtOrderID]
elif vtOrderID in self.workingLimitOrderDict1:
order = self.workingLimitOrderDict1[vtOrderID]
else:
order = None
return False
# 委托回报
if order.status == STATUS_NOTTRADED:
order.status = STATUS_CANCELLED
order.cancelTime = str(self.dt)
self.strategy.onOrder(order)
else:
order.status = STATUS_PARTTRADED_PARTCANCELLED
order.cancelTime = str(self.dt)
self.strategy.onOrder(order)
# 删除数据
if vtOrderID in self.workingLimitOrderDict:
self.removeOrder(vtOrderID)
elif vtOrderID in self.workingLimitOrderDict1:
self.removeOrder1(vtOrderID)
return True
#----------------------------------------------------------------------
def sendStopOrder(self, vtSymbol, orderType, price, volume, strategy):
"""发停止单(本地实现)"""
self.stopOrderCount += 1
stopOrderID = STOPORDERPREFIX + str(self.stopOrderCount)
so = StopOrder()
so.vtSymbol = vtSymbol
so.price = price
so.volume = volume
so.strategy = strategy
so.stopOrderID = stopOrderID
so.status = STOPORDER_WAITING
if orderType == CTAORDER_BUY:
so.direction = DIRECTION_LONG
so.offset = OFFSET_OPEN
elif orderType == CTAORDER_SELL:
so.direction = DIRECTION_SHORT
so.offset = OFFSET_CLOSE
elif orderType == CTAORDER_SHORT:
so.direction = DIRECTION_SHORT
so.offset = OFFSET_OPEN
elif orderType == CTAORDER_COVER:
so.direction = DIRECTION_LONG
so.offset = OFFSET_CLOSE
# 保存stopOrder对象到字典中
self.stopOrderDict[stopOrderID] = so
self.workingStopOrderDict[stopOrderID] = so
return stopOrderID
#----------------------------------------------------------------------
def cancelStopOrder(self, stopOrderID):
"""撤销停止单"""
# 检查停止单是否存在
if stopOrderID in self.workingStopOrderDict:
so = self.workingStopOrderDict[stopOrderID]
so.status = STOPORDER_CANCELLED
del self.workingStopOrderDict[stopOrderID]
#----------------------------------------------------------------------
def filterTradeTime(self):
"""过滤非交易时间"""
if self.dt:
hour = self.dt.hour
# 丢弃非交易时间错误数据
if (hour >= 15 and hour < 20) or (hour >= 2 and hour < 8):
return True
# 清空隔交易日订单
elif hour == 8:
self.lasttick = None
self.lasttick1 = None
for orderID in self.workingLimitOrderDict:
self.cancelOrder(orderID)
for orderID in self.workingLimitOrderDict1:
self.cancelOrder(orderID)
return True
elif hour == 20:
self.lasttick = None
self.lasttick1 = None
for orderID in self.workingLimitOrderDict:
self.cancelOrder(orderID)
for orderID in self.workingLimitOrderDict1:
self.cancelOrder(orderID)
return True
return False
#----------------------------------------------------------------------
def calcTickVolume(self,tick,lasttick,size):
"""计算两边盘口的成交量"""
if (not lasttick):
currentVolume = tick.volume
currentTurnOver = tick.turnover
pOnAsk = tick.askPrice1
pOnBid = tick.bidPrice1
else:
currentVolume = tick.volume - lasttick.volume
currentTurnOver = tick.turnover - lasttick.turnover
pOnAsk = lasttick.askPrice1
pOnBid = lasttick.bidPrice1
if lasttick and currentVolume > 0:
currentPrice = currentTurnOver/currentVolume/size
ratio = (currentPrice-lasttick.bidPrice1)/(lasttick.askPrice1-lasttick.bidPrice1)
ratio = max(ratio,0)
ratio = min(ratio,1)
volOnAsk = ratio*currentVolume
volOnBid = currentVolume - volOnAsk
else:
volOnAsk = 0
volOnBid = 0
return volOnBid,volOnAsk,pOnBid,pOnAsk
#----------------------------------------------------------------------
def removeOrder(self, orderID):
"""清除订单信息"""
del self.workingLimitOrderDict[orderID]
if orderID in self.orderPrice:
del self.orderPrice[orderID]
if orderID in self.orderVolume:
del self.orderVolume[orderID]
#----------------------------------------------------------------------
def removeOrder1(self, orderID):
"""清除订单信息"""
del self.workingLimitOrderDict1[orderID]
if orderID in self.orderPrice1:
del self.orderPrice1[orderID]
if orderID in self.orderVolume1:
del self.orderVolume1[orderID]
#----------------------------------------------------------------------
def snapMarket(self, tradeID):
"""快照市场"""
if self.mode == self.TICK_MODE:
self.tradeSnap[tradeID] = copy.copy(self.tick)
self.tradeSnap1[tradeID] = copy.copy(self.tick1)
else:
self.tradeSnap[tradeID] = copy.copy(self.bar)
self.tradeSnap1[tradeID] = copy.copy(self.bar1)
#----------------------------------------------------------------------
def strategyOnTrade(self, order, volumeTraded, priceTraded):
"""处理成交回报"""
# 推送成交数据,
self.tradeCount += 1
tradeID = str(self.tradeCount)
trade = VtTradeData()
#省略回测无关内容
#trade.tradeID = tradeID
#trade.vtTradeID = tradeID
#trade.orderID = order.orderID
#trade.vtOrderID = order.orderID
trade.dt = self.dt
trade.vtSymbol = order.vtSymbol
trade.direction = order.direction
trade.offset = order.offset
trade.tradeTime = self.dt.strftime('%Y%m%d %H:%M:%S.')+self.dt.strftime('%f')[:1]
trade.volume = volumeTraded
trade.price = priceTraded
self.strategy.onTrade(copy.copy(trade))
# 快照市场,用于计算持仓盈亏,暂不支持
# self.snapMarket(tradeID)
if trade.vtSymbol == self.strategy.vtSymbol:
self.tradeDict[tradeID] = trade
else:
self.tradeDict1[tradeID] = trade
#----------------------------------------------------------------------
def crossLimitOrder(self):
"""基于最新数据撮合限价单"""
# 缓存数据
tick = self.tick
lasttick = self.lasttick
# 过滤数据
if self.filterTradeTime():
return
# 确定撮合价格
if self.mode == self.BAR_MODE:
# Bar价格撮合,目前不支持FokopenFak
buyCrossPrice = self.bar.low # 若买入方向限价单价格高于该价格,则会成交
sellCrossPrice = self.bar.high # 若卖出方向限价单价格低于该价格,则会成交
else:
# Tick采用对价撮合,支持Fok,Fak
buyCrossPrice = tick.askPrice1 if tick.askPrice1 > 0 else tick.bidPrice1+self.mPrice
sellCrossPrice = tick.bidPrice1 if tick.bidPrice1 > 0 else tick.askPrice1-self.mPrice
# 遍历限价单字典中的所有限价单
for orderID, order in self.workingLimitOrderDict.items():
# 判断是否会成交
buyCross = order.direction==DIRECTION_LONG and order.price>=buyCrossPrice
sellCross = order.direction==DIRECTION_SHORT and order.price<=sellCrossPrice
# 如果可以对价撮合
if buyCross or sellCross:
# 计算成交量
volumeTraded = (order.totalVolume-order.tradedVolume)
if self.mode == self.TICK_MODE:
volumeTraded = min(volumeTraded, tick.askVolume1) if buyCross \
else min(volumeTraded, tick.bidVolume1)
volumeTraded = max(volumeTraded,1)
# 计算成交价
if orderID in self.orderPrice and order.tradedVolume == 0:
priceTraded = order.price
else:
priceTraded = min(order.price,buyCrossPrice) if buyCross \
else max(order.price,sellCrossPrice)
# 推送委托数据
order.tradedVolume += volumeTraded
# 分别处理普通限价,FOK,FAK订单
if order.priceType == PRICETYPE_FOK:
if order.tradedVolume < order.totalVolume:
order.status = STATUS_CANCELLED
volumeTraded = 0
else:
order.status = STATUS_ALLTRADED
elif order.priceType == PRICETYPE_FAK:
if order.tradedVolume < order.totalVolume:
order.status = STATUS_PARTTRADED_PARTCANCELLED
else:
order.status = STATUS_ALLTRADED
else:
if order.tradedVolume < order.totalVolume:
order.status = STATUS_PARTTRADED
self.orderPrice[orderID] = order.price
self.orderVolume[orderID] = 0
else:
order.status = STATUS_ALLTRADED
# 推送委托回报
self.strategy.onOrder(order)
# 推送成交回报
if volumeTraded > 0:
self.strategyOnTrade(order, volumeTraded,priceTraded)
# 处理完毕,删除数据
if not order.status == STATUS_PARTTRADED:
self.removeOrder(orderID)
# 模拟排队撮合部分,TICK模式有效(使用Tick内成交均价简单估计两边盘口的成交量)
elif self.mode == self.TICK_MODE and not self.fast:
# 计算估计的两边盘口的成交量
volOnBid,volOnAsk,pOnBid,pOnAsk = self.calcTickVolume(tick, lasttick, self.size)
# 排队队列维护
if orderID in self.orderPrice:
# 非首次进入队列
if orderID not in self.orderVolume:
if order.price == sellCrossPrice and order.direction==DIRECTION_LONG:
self.orderVolume[orderID] = tick.bidVolume1
elif order.price == buyCrossPrice and order.direction==DIRECTION_SHORT:
self.orderVolume[orderID] = tick.askVolume1
# 首先排队进入,然后被打穿(不允许直接在买卖盘中间成交)
elif order.price > sellCrossPrice and order.direction==DIRECTION_LONG:
self.orderVolume[orderID] = 0
elif order.price < buyCrossPrice and order.direction==DIRECTION_SHORT:
self.orderVolume[orderID] = 0
# 更新排队值
elif order.price == pOnBid and order.direction==DIRECTION_LONG:
self.orderVolume[orderID] -= volOnBid
elif order.price == pOnAsk and order.direction==DIRECTION_SHORT:
self.orderVolume[orderID] -= volOnAsk
else:
# 首次进入队列
self.orderPrice[orderID] = order.price
if order.direction==DIRECTION_SHORT and order.price == tick.askPrice1:
self.orderVolume[orderID] = tick.askVolume1
elif order.direction==DIRECTION_LONG and order.price == tick.bidPrice1:
self.orderVolume[orderID] = tick.bidVolume1
# 排队成交,注意,目前简单一次性全部成交!!
if orderID in self.orderVolume and self.orderVolume[orderID] <= 0:
# 推送委托数据
priceTraded = order.price
volumeTraded = order.totalVolume - order.tradedVolume
order.tradedVolume = order.totalVolume
order.status = STATUS_ALLTRADED
self.strategy.onOrder(order)
# 推送成交回报
self.strategyOnTrade(order, volumeTraded, priceTraded)
# 从字典中删除该限价单
self.removeOrder(orderID)
else:
order.tradedVolume = 0
order.status = STATUS_NOTTRADED
if order.priceType == PRICETYPE_FOK or order.priceType == PRICETYPE_FAK:
order.status = STATUS_CANCELLED
self.removeOrder(orderID)
self.strategy.onOrder(order)
#----------------------------------------------------------------------
def crossLimitOrder1(self):
"""基于最新数据撮合限价单"""
# 缓存数据
lasttick1 = self.lasttick1
tick1 = self.tick1
bar1 = self.bar1
if self.filterTradeTime():
return
# 区分K线撮合和TICK撮合模式
if self.mode == self.BAR_MODE:
buyCrossPrice = bar1.low # 若买入方向限价单价格高于该价格,则会成交
sellCrossPrice = bar1.high # 若卖出方向限价单价格低于该价格,则会成交
else:
# TICK对价撮合,并过滤涨跌停板
buyCrossPrice = tick1.askPrice1 if tick1.askPrice1 > 0 else tick1.bidPrice1+self.mPrice1
sellCrossPrice = tick1.bidPrice1 if tick1.bidPrice1 > 0 else tick1.askPrice1-self.mPrice1
# 遍历限价单字典中的所有限价单
for orderID, order in self.workingLimitOrderDict1.items():
# 判断是否对价直接成交
buyCross = order.direction==DIRECTION_LONG and order.price>=buyCrossPrice
sellCross = order.direction==DIRECTION_SHORT and order.price<=sellCrossPrice
# 如果直接对价成交
if buyCross or sellCross:
# 计算成交量
volumeTraded = (order.totalVolume-order.tradedVolume)
if self.mode == self.TICK_MODE:
volumeTraded = min(volumeTraded, tick1.askVolume1) if buyCross \
else min(volumeTraded, tick1.bidVolume1)
volumeTraded = max(volumeTraded,1)
# 计算成交价
if orderID in self.orderPrice1 and order.tradedVolume == 0:
priceTraded = order.price
else:
priceTraded = min(order.price,buyCrossPrice) if buyCross else max(order.price,sellCrossPrice)
# 委托回报,区分普通限价单,FOK,FAK
order.tradedVolume += volumeTraded
if order.priceType == PRICETYPE_FOK:
if order.tradedVolume < order.totalVolume:
order.status = STATUS_CANCELLED
volumeTraded = 0
else:
order.status = STATUS_ALLTRADED
elif order.priceType == PRICETYPE_FAK:
if order.tradedVolume < order.totalVolume:
order.status = STATUS_PARTTRADED_PARTCANCELLED
else:
order.status = STATUS_ALLTRADED
else:
if order.tradedVolume < order.totalVolume:
order.status = STATUS_PARTTRADED
self.orderPrice1[orderID] = order.price
self.orderVolume1[orderID] = 0
else:
order.status = STATUS_ALLTRADED
# 推送委托回报
self.strategy.onOrder(order)
# 推送成交回报
if volumeTraded > 0:
self.strategyOnTrade(order, volumeTraded, priceTraded)
# 清除订单信息
if not order.status == STATUS_PARTTRADED:
self.removeOrder1(orderID)
# 模拟排队撮合部分,只在TICK模式有效
elif self.mode == self.TICK_MODE and not self.fast:
# 计算两边盘口的成交量
volOnBid,volOnAsk,pOnBid,pOnAsk = self.calcTickVolume(tick1, lasttick1,self.size1)
# 排队队列维护
if orderID in self.orderPrice1:
# 非首次进入队列
if orderID not in self.orderVolume1:
if order.price == sellCrossPrice and order.direction==DIRECTION_LONG:
self.orderVolume1[orderID] = tick1.bidVolume1
elif order.price == buyCrossPrice and order.direction==DIRECTION_SHORT:
self.orderVolume1[orderID] = tick1.askVolume1
# 首先排队进入,然后被打穿(不允许直接在买卖盘中间成交)
elif order.price > sellCrossPrice and order.direction==DIRECTION_LONG:
self.orderVolume1[orderID] = 0
elif order.price < buyCrossPrice and order.direction==DIRECTION_SHORT:
self.orderVolume1[orderID] = 0
# 更新排队值
elif order.price == pOnBid and order.direction==DIRECTION_LONG:
self.orderVolume1[orderID] -= volOnBid
elif order.price == pOnAsk and order.direction==DIRECTION_SHORT:
self.orderVolume1[orderID] -= volOnAsk
else:
# 首次进入队列
self.orderPrice1[orderID] = order.price
if order.direction==DIRECTION_SHORT and order.price == tick1.askPrice1:
self.orderVolume1[orderID] = tick1.askVolume1
elif order.direction==DIRECTION_LONG and order.price == tick1.bidPrice1:
self.orderVolume1[orderID] = tick1.bidVolume1
# 排队成功,注意,目前模拟为一次性成交所有订单量!!
if orderID in self.orderVolume1 and self.orderVolume1[orderID] <= 0:
# 推送委托数据
priceTraded = order.price
volumeTraded = order.totalVolume - order.tradedVolume
order.tradedVolume = order.totalVolume
order.status = STATUS_ALLTRADED
self.strategy.onOrder(order)
# 推送成交回报
self.strategyOnTrade(order, volumeTraded, priceTraded)
# 从字典中删除该限价单
self.removeOrder1(orderID)
else:
order.tradedVolume = 0
order.status = STATUS_NOTTRADED
if order.priceType == PRICETYPE_FOK or order.priceType == PRICETYPE_FAK:
order.status = STATUS_CANCELLED
self.removeOrder1(orderID)
self.strategy.onOrder(order)
#----------------------------------------------------------------------
def crossStopOrder(self):
"""基于最新数据撮合停止单"""
# 停止单撮合未更新
# 先确定会撮合成交的价格,这里和限价单规则相反
if self.mode == self.BAR_MODE:
buyCrossPrice = self.bar.high # 若买入方向停止单价格低于该价格,则会成交
sellCrossPrice = self.bar.low # 若卖出方向限价单价格高于该价格,则会成交
bestCrossPrice = self.bar.open # 最优成交价,买入停止单不能低于,卖出停止单不能高于
else:
buyCrossPrice = self.tick.lastPrice
sellCrossPrice = self.tick.lastPrice
bestCrossPrice = self.tick.lastPrice
# 遍历停止单字典中的所有停止单
for stopOrderID, so in self.workingStopOrderDict.items():
# 判断是否会成交
buyCross = so.direction==DIRECTION_LONG and so.price<=buyCrossPrice
sellCross = so.direction==DIRECTION_SHORT and so.price>=sellCrossPrice
# 如果发生了成交
if buyCross or sellCross:
# 推送成交数据
self.tradeCount += 1
tradeID = str(self.tradeCount)
trade = VtTradeData()
trade.vtSymbol = so.vtSymbol
trade.tradeID = tradeID
trade.vtTradeID = tradeID
if buyCross:
trade.price = max(bestCrossPrice, so.price)
else:
trade.price = min(bestCrossPrice, so.price)
self.limitOrderCount += 1
orderID = str(self.limitOrderCount)
trade.orderID = orderID
trade.vtOrderID = orderID
trade.direction = so.direction
trade.offset = so.offset
trade.volume = so.volume
trade.tradeTime = self.dt.strftime('%Y%m%d %H:%M:%S.') + self.dt.strftime('%f')[:1]
trade.dt = self.dt
self.strategy.onTrade(copy.copy(trade))
self.tradeDict[tradeID] = trade
# 推送委托数据
so.status = STOPORDER_TRIGGERED
order = VtOrderData()
order.vtSymbol = so.vtSymbol
order.symbol = so.vtSymbol
order.orderID = orderID
order.vtOrderID = orderID
order.direction = so.direction
order.offset = so.offset
order.price = so.price
order.totalVolume = so.volume
order.tradedVolume = so.volume
order.status = STATUS_ALLTRADED
order.orderTime = trade.tradeTime
self.strategy.onOrder(order)
self.limitOrderDict[orderID] = order
# 从字典中删除该限价单
del self.workingStopOrderDict[stopOrderID]
#----------------------------------------------------------------------
def insertData(self, dbName, collectionName, data):
"""考虑到回测中不允许向数据库插入数据,防止实盘交易中的一些代码出错"""
pass
#----------------------------------------------------------------------
def writeCtaLog(self, content):
"""记录日志"""
log = str(self.dt) + ' ' + content
self.logList.append(log)
#----------------------------------------------------------------------
def output(self, content):
"""输出内容"""
if not self.plot:
return
if self.plotfile:
print content.encode('utf8')
else:
print content
#----------------------------------------------------------------------
def makeRecord(self, tradeTime, offset, direction, price, pnl):
"""记录成交内容"""
resDict = {}
resDict[u'datetime'] = tradeTime
resDict[u'price'] = price
resDict[u'contract0'] = self.strategy.vtSymbol
resDict[u'contract1'] = self.strategy.vtSymbol
resDict[u'offset'] = offset
resDict[u'direction'] = direction
resDict[u'pnl'] = pnl
if self.strategy.vtSymbol1:
resDict[u'contract1'] = self.strategy.vtSymbol1
return resDict
#----------------------------------------------------------------------
def calculateBacktestingResult(self, detial = False):
"""
计算回测结果
"""
self.output(u'按逐笔对冲计算回测结果')
# 首先基于回测后的成交记录,计算每笔交易的盈亏
pnlDict = OrderedDict() # 每笔盈亏的记录
longTrade = deque([]) # 未平仓的多头交易
shortTrade = deque([]) # 未平仓的空头交易
longTrade1 = deque([]) # 合约2未平仓的多头交易
shortTrade1 = deque([]) # 合约2未平仓的空头交易
resList = [{"name":self.strategy.name}]
# 计算滑点,一个来回包括两次
totalSlippage = self.slippage * 2
self.output(u'总交易量 : '+str(len(self.tradeDict)))
self.output(u'总交易量1 : '+str(len(self.tradeDict1)))
leg2 = True
if self.tradeDict.values():
dict_trade = self.tradeDict.values()
else:
dict_trade = self.tradeDict1.values()
leg2 = False
if self.tradeDict1.values():
dict_trade1 = self.tradeDict1.values()
else:
dict_trade1 = self.tradeDict.values()
leg2 = False
for trade1 in dict_trade1:
# 多头交易
if trade1.direction == DIRECTION_LONG:
# 当前多头交易为平空
untraded=True
while (shortTrade1 and untraded):
entryTrade = shortTrade1[0]
exitTrade = trade1
volume = min(entryTrade.volume,exitTrade.volume)
entryTrade.volume = entryTrade.volume-volume
exitTrade.volume = exitTrade.volume-volume
if entryTrade.volume == 0:
shortTrade1.popleft()
if exitTrade.volume == 0:
untraded = False
if exitTrade.dt not in pnlDict:
pnlDict[exitTrade.dt] = TradingResult(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
-volume, self.rate1, self.slippage1, self.size1)
elif leg2:
pnlDict[exitTrade.dt].add(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
-volume, self.rate1, self.slippage1, self.size1)
if exitTrade.dt in pnlDict and leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL1(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
elif not leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL1(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
# 如果尚无空头交易
if untraded:
longTrade1.append(trade1)
# 空头交易
else:
# 当前空头交易为平多
untraded=True
while(untraded and longTrade1):
entryTrade = longTrade1[0]
exitTrade = trade1
volume = min(entryTrade.volume,exitTrade.volume)
entryTrade.volume = entryTrade.volume-volume
exitTrade.volume = exitTrade.volume-volume
if entryTrade.volume == 0:
longTrade1.popleft()
if exitTrade.volume == 0:
untraded = False
if exitTrade.dt not in pnlDict:
pnlDict[exitTrade.dt] = TradingResult(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
volume, self.rate1, self.slippage1, self.size1)
elif leg2:
pnlDict[exitTrade.dt].add(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
volume, self.rate1, self.slippage1, self.size1)
if exitTrade.dt in pnlDict and leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL1(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
elif not leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL1(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
# 如果尚无多头交易
if untraded:
shortTrade1.append(trade1)
for trade in dict_trade:
# 多头交易
if trade.direction == DIRECTION_LONG:
# 当前多头交易为平空
untraded = True
while (shortTrade and untraded):
entryTrade = shortTrade[0]
exitTrade = trade
# 计算比例佣金
volume = min(entryTrade.volume,exitTrade.volume)
entryTrade.volume = entryTrade.volume-volume
exitTrade.volume = exitTrade.volume-volume
if entryTrade.volume == 0:
shortTrade.popleft()
if exitTrade.volume == 0:
untraded = False
if exitTrade.dt not in pnlDict:
pnlDict[exitTrade.dt] = TradingResult(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
-volume, self.rate, self.slippage, self.size)
elif leg2:
pnlDict[exitTrade.dt].add(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
-volume, self.rate, self.slippage, self.size)
if exitTrade.dt in pnlDict and leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
elif not leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
pnl = pnlDict[exitTrade.dt].pnl
# 记录用来可视化的成交内容
resDict = self.makeRecord(entryTrade.tradeTime,u'开',u'卖',entryTrade.price,pnl)
resList.append(resDict)
resDict = self.makeRecord(exitTrade.tradeTime,u'平',u'买',exitTrade.price,pnl)
resList.append(resDict)
# 如果尚无空头交易
if untraded:
longTrade.append(trade)
# 空头交易
else:
# 当前空头交易为平多
untraded=True
while (longTrade and untraded):
entryTrade = longTrade[0]
exitTrade = trade
# 计算比例佣金
volume = min(entryTrade.volume,exitTrade.volume)
entryTrade.volume = entryTrade.volume-volume
exitTrade.volume = exitTrade.volume-volume
if entryTrade.volume == 0:
longTrade.popleft()
if exitTrade.volume == 0:
untraded = False
if exitTrade.dt not in pnlDict:
pnlDict[exitTrade.dt] = TradingResult(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
volume, self.rate, self.slippage, self.size)
elif leg2:
pnlDict[exitTrade.dt].add(entryTrade.price, entryTrade.dt,
exitTrade.price, exitTrade.dt,
volume, self.rate, self.slippage, self.size)
if exitTrade.dt in pnlDict and leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
elif not leg2:
pnlDict[exitTrade.dt].posPnl = self.calcPosPNL(exitTrade.tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2)
pnl = pnlDict[exitTrade.dt].pnl
# 记录用来可视化的成交内容
resDict = self.makeRecord(entryTrade.tradeTime,u'开',u'买',entryTrade.price,pnl)
resList.append(resDict)
resDict = self.makeRecord(exitTrade.tradeTime,u'平',u'卖',exitTrade.price,pnl)
resList.append(resDict)
# 如果尚无多头交易
if untraded:
shortTrade.append(trade)
# 计算剩余持仓盈亏
while (shortTrade):
entryTrade = shortTrade.popleft()
volume = entryTrade.volume
if self.mode == self.TICK_MODE:
exitTime = self.tick.datetime
exitPrice = self.tick.askPrice1
else:
exitTime = self.bar.datetime
exitPrice = self.bar.close
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate, self.slippage, self.size)
pnl = pnlDict[exitTime].pnl
elif leg2:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate, self.slippage, self.size)
pnl = pnlDict[exitTime].pnl
# 记录用来可视化的成交内容
resDict = self.makeRecord(entryTrade.tradeTime,u'开持',u'卖',entryTrade.price,pnl)
resList.append(resDict)
resDict = self.makeRecord(str(exitTime),u'平持',u'买',exitPrice,pnl)
resList.append(resDict)
while (longTrade):
entryTrade = longTrade.popleft()
volume = entryTrade.volume
if self.mode == self.TICK_MODE:
exitTime = self.tick.datetime
exitPrice = self.tick.bidPrice1
else:
exitTime = self.bar.datetime
exitPrice = self.bar.close
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate, self.slippage, self.size)
pnl = pnlDict[exitTime].pnl
elif leg2:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate, self.slippage, self.size)
pnl = pnlDict[exitTime].pnl
# 记录用来可视化的成交内容
resDict = self.makeRecord(entryTrade.tradeTime,u'开持',u'买',entryTrade.price,pnl)
resList.append(resDict)
resDict = self.makeRecord(str(exitTime),u'平持',u'卖',exitPrice,pnl)
resList.append(resDict)
while (leg2 and shortTrade1):
entryTrade = shortTrade1.popleft()
volume = entryTrade.volume
if self.mode == self.TICK_MODE:
exitTime = self.tick1.datetime
exitPrice = self.tick1.askPrice1
else:
exitTime = self.bar1.datetime
exitPrice = self.bar1.close
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate1, self.slippage1, self.size1)
pnl = pnlDict[exitTime].pnl
else:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate1, self.slippage1, self.size1)
pnl = pnlDict[exitTime].pnl
# 记录用来可视化的成交内容
resDict = self.makeRecord(entryTrade.tradeTime,u'开持',u'卖',entryTrade.price,pnl)
resList.append(resDict)
resDict = self.makeRecord(str(exitTime),u'平持',u'买',exitPrice,pnl)
resList.append(resDict)
while (leg2 and longTrade1):
entryTrade = longTrade1.popleft()
volume = entryTrade.volume
if self.mode == self.TICK_MODE:
exitTime = self.tick1.datetime
exitPrice = self.tick1.bidPrice1
else:
exitTime = self.bar.datetime
exitPrice = self.bar.close
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate1, self.slippage1, self.size1)
else:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate1, self.slippage1, self.size1)
pnl = pnlDict[exitTime].pnl
# 记录用来可视化的成交内容
resDict = self.makeRecord(entryTrade.tradeTime,u'开持',u'买',entryTrade.price,pnl)
resList.append(resDict)
resDict = self.makeRecord(exitTime,u'平持',u'卖',exitPrice,pnl)
resList.append(resDict)
# 由于双合约的问题,需要整理时间序列和结果序列
timeList = [] # 时间序列
resultList = [] # 交易结果序列
pnlDict0 = sorted(pnlDict.iteritems(),key=lambda d:d[0])
for k,v in pnlDict0:
timeList.append(k)
resultList.append(v)
# 然后基于每笔交易的结果,我们可以计算具体的盈亏曲线和最大回撤等
timeList = [] # 时间序列
pnlList = [] # 每笔盈亏序列
capital = 0 # 资金
maxCapital = 0 # 资金最高净值
drawdown = 0 # 回撤
totalResult = 0 # 总成交数量
totalTurnover = 0 # 总成交金额(合约面值)
totalCommission = 0 # 总手续费
totalSlippage = 0 # 总滑点
capitalList = [] # 盈亏汇总的时间序列
drawdownList = [] # 回撤的时间序列
winningResult = 0 # 盈利次数
losingResult = 0 # 亏损次数
totalWinning = 0 # 总盈利金额
totalLosing = 0 # 总亏损金额
for result in resultList:
capital += result.pnl
maxCapital = max(capital+result.posPnl, maxCapital)
drawdown = round(capital+result.posPnl-maxCapital,2)
pnlList.append(result.pnl)
timeList.append(result.exitDt) # 交易的时间戳使用平仓时间
capitalList.append(capital+result.posPnl)
drawdownList.append(drawdown)
totalResult += 1
totalTurnover += result.turnover
totalCommission += result.commission
totalSlippage += result.slippage
if result.pnl >= 0:
winningResult += 1
totalWinning += result.pnl
else:
losingResult += 1
totalLosing += result.pnl
# 计算盈亏相关数据
if totalResult:
winningRate = winningResult*1.0/totalResult*100 # 胜率
else:
winningRate = 0
averageWinning = 0 # 这里把数据都初始化为0
averageLosing = 0
profitLossRatio = 0
if winningResult:
averageWinning = totalWinning/winningResult # 平均每笔盈利
else:
averageWinning = 0
if losingResult:
averageLosing = totalLosing/losingResult # 平均每笔亏损
else:
averageLosing = 0
if averageLosing:
profitLossRatio = -averageWinning/averageLosing # 盈亏比
else:
profitLossRatio = 0
# 返回回测结果
d = {}
d['capital'] = capital
d['maxCapital'] = maxCapital
d['drawdown'] = drawdown
d['totalResult'] = totalResult
d['totalTurnover'] = totalTurnover
d['totalCommission'] = totalCommission
d['totalSlippage'] = totalSlippage
d['timeList'] = timeList
d['pnlList'] = pnlList
d['capitalList'] = capitalList
d['drawdownList'] = drawdownList
d['winningRate'] = winningRate
d['averageWinning'] = averageWinning
d['averageLosing'] = averageLosing
d['profitLossRatio'] = profitLossRatio
d['resList'] = resList
return d
#----------------------------------------------------------------------
def calcPosPNL(self,tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2):
"""
根据市场快照,计算每笔成交时间的持仓盈亏(按对价结算并扣除了手续费和滑点)
"""
# 判断是否有持仓,加快无持仓策略的计算速度
return 0
allPos0 = len(shortTrade)+len(longTrade)
if allPos0 == 0:
return 0
pnlDict = OrderedDict() # 每笔盈亏的记录
if tradeID in self.tradeSnap:
tick = self.tradeSnap[tradeID] # 主合约行情
tick1 = self.tradeSnap1[tradeID] # 副合约行情
elif tradeID in self.trade1Snap:
tick = self.trade1Snap[tradeID] # 主合约行情
tick1 = self.trade1Snap1[tradeID] # 副合约行情
else:
tick = self.tradeSnap[tradeID] # 主合约行情
tick1 = self.tradeSnap1[tradeID] # 副合约行情
for entryTrade in shortTrade:
volume = entryTrade.volume
exitTime = tick.datetime
exitPrice = tick.askPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate, self.slippage, self.size)
elif leg2:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate, self.slippage, self.size)
for entryTrade in longTrade:
volume = entryTrade.volume
exitTime = tick.datetime
exitPrice = tick.bidPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate, self.slippage, self.size)
elif leg2:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate, self.slippage, self.size)
for entryTrade in shortTrade1:
volume = entryTrade.volume
exitTime = tick1.datetime
exitPrice = tick1.askPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate1, self.slippage1, self.size1)
else:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate1, self.slippage1, self.size1)
for entryTrade in longTrade1:
volume = entryTrade.volume
exitTime = tick1.datetime
exitPrice = tick1.bidPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate1, self.slippage1, self.size1)
else:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate1, self.slippage1, self.size1)
result = 0
for v in pnlDict.values():
result += v.pnl
return result
#----------------------------------------------------------------------
def calcPosPNL1(self,tradeID,shortTrade,longTrade,shortTrade1,longTrade1,leg2):
"""
根据市场快照,计算每笔成交时间的持仓盈亏(按对价结算并扣除了手续费和滑点)
"""
return 0
# 判断是否有持仓,加快无持仓策略的计算速度
allPos1 = len(shortTrade1)+len(longTrade1)
if allPos1 == 0:
return 0
pnlDict = OrderedDict() # 每笔盈亏的记录
if tradeID in self.trade1Snap:
tick = self.trade1Snap[tradeID] # 主合约行情
tick1 = self.trade1Snap1[tradeID] # 副合约行情
elif tradeID in self.tradeSnap:
tick = self.tradeSnap[tradeID] # 主合约行情
tick1 = self.tradeSnap1[tradeID] # 副合约行情
else:
return 0
for entryTrade in shortTrade:
volume = entryTrade.volume
exitTime = tick.datetime
exitPrice = tick.askPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate, self.slippage, self.size)
elif leg2:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate, self.slippage, self.size)
for entryTrade in longTrade:
volume = entryTrade.volume
exitTime = tick.datetime
exitPrice = tick.bidPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate, self.slippage, self.size)
elif leg2:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate, self.slippage, self.size)
for entryTrade in shortTrade1:
volume = entryTrade.volume
exitTime = tick1.datetime
exitPrice = tick1.askPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate1, self.slippage1, self.size1)
else:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
-volume, self.rate1, self.slippage1, self.size1)
for entryTrade in longTrade1:
volume = entryTrade.volume
exitTime = tick1.datetime
exitPrice = tick1.bidPrice1
if exitTime not in pnlDict:
pnlDict[exitTime] = TradingResult(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate1, self.slippage1, self.size1)
else:
pnlDict[exitTime].add(entryTrade.price, entryTrade.dt,
exitPrice, exitTime,
volume, self.rate1, self.slippage1, self.size1)
result = 0
for v in pnlDict.values():
result += v.pnl
return result
#----------------------------------------------------------------------
def showBacktestingResult(self):
"""
显示回测结果
"""
d = self.calculateBacktestingResult()
timeList = d['timeList']
pnlList = d['pnlList']
capitalList = d['capitalList']
drawdownList = d['drawdownList']
resList = d['resList']
self.output(u'显示回测结果')
# 输出
if len(resList)>1:
import codecs
settingFileName = self.strategy.name+'_DT_setting.json'
if os.path.exists(os.getcwd() + '\\..\\dataViewer\\data\\'):
settingFileName = os.getcwd() + '\\..\\dataViewer\\data\\' + settingFileName
else:
settingFileName = os.getcwd() + '\\dataViewer\\data\\' + settingFileName
f = codecs.open(settingFileName,'w', 'utf-8')
f.write(json.dumps(resList,indent=1,ensure_ascii=False))
f.close()
if len(timeList)>0:
self.output('-' * 30)
self.output(u'第一笔交易:\t%s' % d['timeList'][0])
self.output(u'最后一笔交易:\t%s' % d['timeList'][-1])
self.output(u'总交易次数:\t%s' % formatNumber(d['totalResult']))
self.output(u'总盈亏:\t%s' % formatNumber(d['capital']))
self.output(u'最大回撤: \t%s' % formatNumber(min(d['drawdownList'])))
self.output(u'平均每笔盈利:\t%s' %formatNumber(d['capital']/d['totalResult']))
self.output(u'平均每笔滑点:\t%s' %formatNumber(d['totalSlippage']/d['totalResult']))
self.output(u'平均每笔佣金:\t%s' %formatNumber(d['totalCommission']/d['totalResult']))
self.output(u'胜率\t\t%s%%' %formatNumber(d['winningRate']))
self.output(u'平均每笔盈利\t%s' %formatNumber(d['averageWinning']))
self.output(u'平均每笔亏损\t%s' %formatNumber(d['averageLosing']))
self.output(u'盈亏比:\t%s' %formatNumber(d['profitLossRatio']))
# 资金曲线插入数据库
lastTime = None
lastCap = 0
lastDayCap = 0
lastDraw = 0
for (time,cap,drawdown) in zip(timeList,capitalList,drawdownList):
if lastTime and time.day != lastTime.day:
capData = CtaCapData()
capData.name = self.strategy.name
capData.datetime = lastTime
capData.start = self.dataStartDate
capData.date = capData.datetime.replace(hour =0,minute\
=0,second = 0,microsecond = 0)
capData.cap = lastCap
capData.pnl = lastCap - lastDayCap
capData.drawdown = lastDraw
self.insertCap(CAPITAL_DB_NAME,self.strategy.name,capData)
lastDayCap = lastCap
lastTime = time
lastCap = cap
lastDraw = drawdown
capData = CtaCapData()
capData.name = self.strategy.name
capData.datetime = lastTime
capData.start = self.dataStartDate
capData.date = capData.datetime.replace(hour =0,minute\
=0,second = 0,microsecond = 0)
capData.cap = lastCap
capData.pnl = lastCap - lastDayCap
capData.drawdown = lastDraw
self.insertCap(CAPITAL_DB_NAME,self.strategy.name,capData)
# 绘图
import matplotlib.pyplot as plt
from matplotlib.dates import AutoDateLocator, DateFormatter
plt.close()
autodates = AutoDateLocator()
yearsFmt = DateFormatter('%m-%d')
#yearsFmt = DateFormatter('%Y-%m-%d')
pCapital = plt.subplot(3, 1, 1)
pCapital.set_ylabel("capital")
pCapital.plot(timeList,capitalList)
plt.title(self.strategy.name)
plt.gcf().autofmt_xdate() #设置x轴时间外观
plt.gcf().subplots_adjust(bottom=0.1)
plt.gca().xaxis.set_major_locator(autodates) #设置时间间隔
plt.gca().xaxis.set_major_formatter(yearsFmt) #设置时间显示格式
pDD = plt.subplot(3, 1, 2)
pDD.set_ylabel("DD")
pDD.bar(range(len(drawdownList)), drawdownList)
pPnl = plt.subplot(3, 1, 3)
pPnl.set_ylabel("pnl")
pPnl.hist(pnlList, bins=20)
plt.show()
#----------------------------------------------------------------------
def insertCap(self, dbName, collectionName, d):
"""插入数据到数据库(这里的data可以是CtaTickData或者CtaBarData)"""
host, port = loadMongoSetting()
if not self.dbClient:
self.dbClient = pymongo.MongoClient(host, port, socketKeepAlive=True)
db = self.dbClient[dbName]
collection = db[collectionName]
collection.ensure_index([('date', pymongo.ASCENDING)], unique=True)
flt = {'date': d.date}
collection.update_one(flt, {'$set':d.__dict__}, upsert=True)
#----------------------------------------------------------------------
def showBacktestingResult_nograph(self,filepath):
"""
显示回测结果
"""
d = self.calculateBacktestingResult()
timeList = d['timeList']
pnlList = d['pnlList']
capitalList = d['capitalList']
drawdownList = d['drawdownList']
self.output(u'显示回测结果')
# 输出
if len(timeList)>0:
self.output('-' * 30)
self.output(u'第一笔交易:\t%s' % d['timeList'][0])
self.output(u'最后一笔交易:\t%s' % d['timeList'][-1])
self.output(u'总交易次数:\t%s' % formatNumber(d['totalResult']))
self.output(u'总盈亏:\t%s' % formatNumber(d['capital']))
self.output(u'最大回撤: \t%s' % formatNumber(min(d['drawdownList'])))
self.output(u'平均每笔盈利:\t%s' %formatNumber(d['capital']/d['totalResult']))
self.output(u'平均每笔滑点:\t%s' %formatNumber(d['totalSlippage']/d['totalResult']))
self.output(u'平均每笔佣金:\t%s' %formatNumber(d['totalCommission']/d['totalResult']))
self.output(u'胜率\t\t%s%%' %formatNumber(d['winningRate']))
self.output(u'平均每笔盈利\t%s' %formatNumber(d['averageWinning']))
self.output(u'平均每笔亏损\t%s' %formatNumber(d['averageLosing']))
self.output(u'盈亏比:\t%s' %formatNumber(d['profitLossRatio']))
self.output(u'显示回测结果')
# 资金曲线插入数据库
lastTime = None
lastCap = 0
lastDayCap = 0
lastDraw = 0
for (time,cap,drawdown) in zip(timeList,capitalList,drawdownList):
if lastTime and time.day != lastTime.day:
capData = CtaCapData()
capData.name = self.strategy.name
capData.datetime = lastTime
capData.start = self.dataStartDate
capData.date = capData.datetime.replace(hour =0,minute\
=0,second = 0,microsecond = 0)
capData.cap = lastCap
capData.pnl = lastCap - lastDayCap
capData.drawdown = lastDraw
self.insertCap(CAPITAL_DB_NAME,self.strategy.name,capData)
lastDayCap = lastCap
lastTime = time
lastCap = cap
lastDraw = drawdown
# 绘图
import matplotlib
matplotlib.use('Qt4Agg')
import matplotlib.pyplot as plt
from matplotlib.dates import AutoDateLocator, DateFormatter
autodates = AutoDateLocator()
yearsFmt = DateFormatter('%m-%d')
pCapital = plt.subplot(3, 1, 1)
pCapital.set_ylabel("capital")
pCapital.plot(timeList,capitalList)
plt.gcf().autofmt_xdate() #设置x轴时间外观
plt.gcf().subplots_adjust(bottom=0.1)
plt.gca().xaxis.set_major_locator(autodates) #设置时间间隔
plt.gca().xaxis.set_major_formatter(yearsFmt) #设置时间显示格式
pDD = plt.subplot(3, 1, 2)
pDD.set_ylabel("DD")
pDD.bar(range(len(drawdownList)), drawdownList)
pPnl = plt.subplot(3, 1, 3)
pPnl.set_ylabel("pnl")
pPnl.hist(pnlList, bins=20)
plt.savefig(filepath)
plt.close()
#----------------------------------------------------------------------
def putStrategyEvent(self, name):
"""发送策略更新事件,回测中忽略"""
pass
#----------------------------------------------------------------------
def confSettle(self, name):
"""确认结算单,回测中忽略"""
pass
#----------------------------------------------------------------------
def setSlippage(self, slippage):
"""设置滑点"""
self.slippage = slippage
self.slippage1 = slippage
#----------------------------------------------------------------------
def setSize(self, size):
"""设置合约大小"""
self.size = size
#----------------------------------------------------------------------
def setSize1(self, size):
"""设置合约大小"""
self.size1 = size
#----------------------------------------------------------------------
def setRate(self, rate):
"""设置佣金比例"""
self.rate = rate
#----------------------------------------------------------------------
def setRate1(self, rate):
"""设置佣金比例"""
self.rate1 = rate
#----------------------------------------------------------------------
def setLeverage(self, leverage):
"""设置杠杆比率"""
self.leverage = leverage
#----------------------------------------------------------------------
def setPrice(self, price):
"""设置合约大小"""
self.mPrice = price
#----------------------------------------------------------------------
def setPrice1(self, price):
"""设置合约大小"""
self.mPrice1 = price
#----------------------------------------------------------------------
def loadTick(self, dbName, collectionName, days):
"""从数据库中读取Tick数据,startDate是datetime对象"""
startDate = datetime.now()
d = {'datetime':{'$lte':startDate}}
host, port = loadMongoSetting()
client = pymongo.MongoClient(host,port)
collection = client[dbName][collectionName]
cursor = collection.find(d).limit(days*10*60*120)
l = []
if cursor:
for d in cursor:
tick = CtaTickData()
tick.__dict__ = d
l.append(tick)
return l
#----------------------------------------------------------------------
def loadBar(self, dbName, collectionName, days):
"""从数据库中读取Tick数据,startDate是datetime对象"""
startDate = datetime.now()
d = {'datetime':{'$lte':startDate}}
host, port = loadMongoSetting()
client = pymongo.MongoClient(host,port)
collection = client[dbName][collectionName]
cursor = collection.find(d).limit(days*10*60)
l = []
if cursor:
for d in cursor:
bar = CtaBarData()
bar.__dict__ = d
l.append(bar)
return l
#----------------------------------------------------------------------
def runOptimization(self, strategyClass, setting_c, optimizationSetting):
"""串行优化"""
# 获取优化设置
settingList = optimizationSetting.generateSetting()
targetName = optimizationSetting.optimizeTarget
# 检查参数设置问题
if not settingList or not targetName:
self.output(u'优化设置有问题,请检查')
vtSymbol = setting_c['vtSymbol']
if 'vtSymbol1' in setting_c:
vtSymbol1 = setting_c['vtSymbol1']
else:
vtSymbol1 = None
# 遍历优化
resultList = []
opResults = []
for setting in settingList:
self.clearBacktestingResult()
self.loadHistoryData(TICK_DB_NAME, vtSymbol)
if vtSymbol1:
self.loadHistoryData1(TICK_DB_NAME, vtSymbol1)
self.output('-' * 30)
self.output('setting: %s' %str(setting))
self.initStrategy(strategyClass, setting_c)
self.strategy.onUpdate(setting)
self.runBacktesting()
opResult = {}
d = self.calculateBacktestingResult()
for key in setting:
opResult[key] = setting[key]
opResult['totalResult']=d['totalResult']
opResult['capital']=d['capital']
if d['totalResult'] > 0:
opResult['maxDrawdown']=min(d['drawdownList'])
opResult['winPerT']=d['capital']/d['totalResult']
opResult['splipPerT']=d['totalSlippage']/d['totalResult']
opResult['commiPerT']=d['totalCommission']/d['totalResult']
else:
opResult['maxDrawdown']=0
opResult['winPerT']=0
opResult['splipPerT']=0
opResult['commiPerT']=0
opResult['winningRate']=d['winningRate']
opResult['averageWinning']=d['averageWinning']
opResult['averageLosing']=d['averageLosing']
opResult['profitLossRatio']=d['profitLossRatio']
try:
targetValue = d[targetName]
except KeyError:
targetValue = 0
resultList.append(([str(setting)], targetValue))
opResults.append(opResult)
# 显示结果
if os.path.exists('.\\ctaStrategy\\opResults\\'):
filepath = '.\\ctaStrategy\\opResults\\'
else:
filepath = '.\\opResults\\'
with open(filepath+self.strategy.name+'.csv','wb') as csvfile:
fieldnames = opResult.keys()
writer = csv.DictWriter(csvfile,fieldnames)
writer.writeheader()
for opDict in opResults:
writer.writerow(opDict)
resultList.sort(reverse=True, key=lambda result:result[1])
self.output('-' * 30)
self.output(u'优化结果:')
for result in resultList:
self.output(u'%s: %s' %(result[0], result[1]))
return result
#----------------------------------------------------------------------
def clearBacktestingResult(self):
"""清空之前回测的结果"""
# 交易行情相关
self.dt = None
self.backtestingData = deque([])
self.backtestingData1 = deque([])
self.tick = None
self.tick1 = None
self.bar = None
self.bar1 = None
self.lasttick = None
self.lasttick1 = None
self.logList = [] # 日志记录
# 清空限价单相关
self.limitOrderCount = 0
self.limitOrderDict.clear()
self.limitOrderDict1.clear()
self.workingLimitOrderDict.clear()
self.workingLimitOrderDict1.clear()
self.orderPrice = {} # 限价单价格
self.orderVolume = {} # 限价单盘口
self.orderPrice1 = {} # 限价单价格
self.orderVolume1 = {} # 限价单盘口
# 清空停止单相关
self.stopOrderCount = 0
self.stopOrderDict.clear()
self.workingStopOrderDict.clear()
# 清空成交相关
self.tradeCount = 0
self.tradeDict.clear()
self.tradeSnap.clear()
self.tradeSnap1.clear()
self.tradeCount1 = 0
self.tradeDict1.clear()
self.trade1Snap.clear()
self.trade1Snap1.clear()
########################################################################
class TradingResult(object):
"""每笔交易的结果"""
#----------------------------------------------------------------------
def __init__(self, entryPrice, entryDt, exitPrice,
exitDt, volume, rate, slippage, size):
"""Constructor"""
self.entryPrice = entryPrice # 开仓价格
self.exitPrice = exitPrice # 平仓价格
self.entryDt = entryDt # 开仓时间
self.exitDt = exitDt # 平仓时间
self.volume = volume # 交易数量(+/-代表方向)
self.turnover = (self.entryPrice+self.exitPrice)*size*abs(volume) # 成交金额
self.commission = self.turnover*rate # 手续费成本
self.slippage = slippage*2*size*abs(volume) # 滑点成本
self.pnl = ((self.exitPrice - self.entryPrice) * volume * size
- self.commission - self.slippage) # 净盈亏
self.posPnl = 0 # 当时持仓盈亏
#----------------------------------------------------------------------
def add(self, entryPrice, entryDt, exitPrice,
exitDt, volume, rate, slippage, size):
"""Constructor"""
self.entryPrice = entryPrice # 开仓价格
self.exitPrice = exitPrice # 平仓价格
self.entryDt = entryDt # 开仓时间datetime
self.exitDt = exitDt # 平仓时间
self.volume += volume # 交易数量(+/-代表方向)
turnover = (self.entryPrice+self.exitPrice)*size*abs(volume)
self.turnover += turnover # 成交金额
commission = turnover*rate
self.commission += commission # 手续费成本
slippage0 = slippage*2*size*abs(volume)
self.slippage += slippage0 # 滑点成本
self.pnl += ((self.exitPrice - self.entryPrice) * volume * size
- commission - slippage0) # 净盈亏
########################################################################
class OptimizationSetting(object):
"""优化设置"""
#----------------------------------------------------------------------
def __init__(self):
"""Constructor"""
self.paramDict = OrderedDict()
self.optimizeTarget = '' # 优化目标字段
#----------------------------------------------------------------------
def addParameter(self, name, start, end, step):
"""增加优化参数"""
if end <= start:
print u'参数起始点必须小于终止点'
return
if step <= 0:
print u'参数步进必须大于0'
return
l = []
param = start
while param <= end:
l.append(param)
param += step
self.paramDict[name] = l
#----------------------------------------------------------------------
def generateSetting(self):
"""生成优化参数组合"""
# 参数名的列表
nameList = self.paramDict.keys()
paramList = self.paramDict.values()
# 使用迭代工具生产参数对组合
productList = list(product(*paramList))
# 把参数对组合打包到一个个字典组成的列表中
settingList = []
for p in productList:
d = dict(zip(nameList, p))
settingList.append(d)
return settingList
#----------------------------------------------------------------------
def setOptimizeTarget(self, target):
"""设置优化目标字段"""
self.optimizeTarget = target
#---------------------------------------------------------------------------------------
def backtesting(setting_c, StartTime = '', EndTime = '', slippage = 0, optimism = False, mode = 'T'):
"""读取策略配置"""
setting_c[u'backtesting'] = True
import sys
import re
from ctaSetting import STRATEGY_CLASS
from ctaBacktesting import BacktestingEngine
vtSymbol = setting_c[u'vtSymbol']
if u'vtSymbol1' in setting_c:
vtSymbol1 = setting_c[u'vtSymbol1']
else:
vtSymbol1 = None
className = setting_c[u'className']
with open("CTA_v_setting.json") as f:
l = json.load(f)
for setting in l:
name = setting[u'name']
match = re.search('^'+name+'[0-9]',vtSymbol)
if match:
rate = setting[u'mRate']
price = setting[u'mPrice']
size = setting[u'mSize']
level = setting[u'mLevel']
if vtSymbol1:
match = re.search('^'+name+'[0-9]',vtSymbol1)
if match:
rate1 = setting[u'mRate']
price1 = setting[u'mPrice']
size1 = setting[u'mSize']
level1 = setting[u'mLevel']
output_s=sys.stdout
sys.stderr = output_s
engine=BacktestingEngine()
engine.optimism = optimism
# 设置引擎的回测模式为TICK
if mode == 'T':
engine.setBacktestingMode(engine.TICK_MODE)
dbName = TICK_DB_NAME
elif mode == 'B':
engine.setBacktestingMode(engine.BAR_MODE)
dbName = MINUTE_DB_NAME
elif mode == 'D':
engine.setBacktestingMode(engine.BAR_MODE)
dbName = DAILY_DB_NAME
if not StartTime:
StartTime = str(setting_c[u'StartTime'])
if not EndTime:
EndTime = str(setting_c[u'EndTime'])
# 设置回测用的数据起始日期
engine.setStartDate(StartTime,1)
engine.setEndDate(EndTime)
# 载入历史数据到引擎中
engine.loadHistoryData(dbName, vtSymbol)
if vtSymbol1:
engine.loadHistoryData1(dbName, vtSymbol1)
# 设置产品相关参数
engine.setSlippage(slippage) # 滑点
engine.setRate(rate) # 万1.1
engine.setSize(size) # 合约大小
engine.setPrice(price) # 最小价格变动
if vtSymbol1:
engine.setRate1(rate1) # 万1.1
engine.setSize1(size1) # 合约大小
engine.setPrice1(price1) # 最小价格变动
else:
engine.setRate1(rate) # 万1.1
engine.setSize1(size) # 合约大小
engine.setPrice1(price) # 最小价格变动
engine.setLeverage(level) # 合约杠杆
engine.initStrategy(STRATEGY_CLASS[className],setting_c)
engine.runBacktesting()
engine.showBacktestingResult()
sys.stdout=output_s
#----------------------------------------------------------------------
def runParallelOptimization(setting_c, optimizationSetting, optimism=False, startTime='', endTime='', slippage=0,mode='T'):
"""并行优化参数"""
# 获取优化设置
global p
global currentP
print(u'开始优化策略 : '+setting_c['name'])
print(u' ')
settingList = optimizationSetting.generateSetting()
print(u'总共'+str(len(settingList))+u'个优化')
targetName = optimizationSetting.optimizeTarget
p = ProgressBar(maxval=len(settingList))
p.start()
currentP=0
# 检查参数设置问题
if not settingList or not targetName:
print(u'优化设置有问题,请检查')
# 多进程优化,启动一个对应CPU核心数量的进程池
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count()-1)
l = []
for setting in settingList:
l.append(pool.apply_async(optimize, args=(setting_c,setting,targetName,optimism,startTime,endTime,slippage,mode), callback=showProcessBar))
pool.close()
pool.join()
p.finish()
# 显示结果
resultList = [res.get() for res in l]
print('-' * 30)
print(u'优化结果:')
if os.path.exists('.\\ctaStrategy\\opResults\\'):
filepath = '.\\ctaStrategy\\opResults\\'
else:
filepath = '.\\opResults\\'
with open(filepath+setting_c['name']+'.csv','wb') as csvfile:
fieldnames = resultList[0][1].keys()
fieldnames.sort()
writer = csv.DictWriter(csvfile,fieldnames)
writer.writeheader()
setting_t = {}
value_t = -99999
for (setting,opDict) in resultList:
writer.writerow(opDict)
if opDict[targetName] > value_t:
setting_t = setting
value_t = opDict[targetName]
print(str(setting_t)+':'+str(value_t))
print(u'优化结束')
print(u' ')
#----------------------------------------------------------------------
def showProcessBar(result):
"""显示进度条"""
global p
global currentP
currentP+=1
p.update(currentP)
#----------------------------------------------------------------------
def getSetting(name):
"""获取策略基础配置"""
setting_c = {}
settingFileName = '.\\json\\CTA_setting.json'
with open(settingFileName) as f:
l = json.load(f)
for setting in l:
if setting['name'] == name:
setting_c = setting
setting_c[u'backtesting'] = True
return setting_c
#----------------------------------------------------------------------
def formatNumber(n):
"""格式化数字到字符串"""
rn = round(n, 2) # 保留两位小数
return format(rn, ',') # 加上千分符
#----------------------------------------------------------------------
def optimize(setting_c, setting, targetName, optimism, startTime='', endTime='', slippage=0, mode = 'T'):
"""多进程优化时跑在每个进程中运行的函数"""
setting_c[u'backtesting'] = True
import re
from ctaSetting import STRATEGY_CLASS
from ctaBacktesting import BacktestingEngine
vtSymbol = setting_c[u'vtSymbol']
if u'vtSymbol1' in setting_c:
vtSymbol1 = setting_c[u'vtSymbol1']
else:
vtSymbol1 = None
className = setting_c[u'className']
if os.path.exists("CTA_v_setting.json"):
fileName = "CTA_v_setting.json"
else:
fileName = "..\\CTA_v_setting.json"
with open(fileName) as f:
l = json.load(f)
for setting_x in l:
name = setting_x[u'name']
match = re.search('^'+name+'[0-9]',vtSymbol)
if match:
rate = setting_x[u'mRate']
price = setting_x[u'mPrice']
size = setting_x[u'mSize']
level = setting_x[u'mLevel']
if vtSymbol1:
match = re.search('^'+name+'[0-9]',vtSymbol1)
if match:
rate1 = setting_x[u'mRate']
price1 = setting_x[u'mPrice']
size1 = setting_x[u'mSize']
level1 = setting_x[u'mLevel']
name = setting_c[u'name']
engine=BacktestingEngine()
#engine.plot = False
#engine.fast = True
engine.plot = True
engine.optimism = optimism
# 设置引擎的回测模式
if mode=='T':
engine.setBacktestingMode(engine.TICK_MODE)
dbName = TICK_DB_NAME
elif mode=='B':
engine.setBacktestingMode(engine.BAR_MODE)
dbName = MINUTE_DB_NAME
elif mode=='D':
engine.setBacktestingMode(engine.BAR_MODE)
dbName = DAILY_DB_NAME
# 设置回测用的数据起始日期
if not startTime:
startTime = str(setting_c[u'StartTime'])
if not endTime:
endTime = str(setting_c[u'EndTime'])
engine.setStartDate(startTime,1)
engine.setEndDate(endTime)
engine.loadHistoryData(dbName, vtSymbol)
if vtSymbol1:
engine.loadHistoryData1(dbName, vtSymbol1)
# 设置产品相关参数
engine.setSlippage(slippage) # 滑点
engine.setRate(rate) # 手续费
engine.setSize(size) # 合约大小
engine.setPrice(price) # 最小价格变动
if vtSymbol1:
engine.setSize1(size1) # 合约大小
engine.setRate1(rate1) # 手续费
engine.setPrice1(price1) # 最小价格变动
else:
engine.setSize1(size) # 合约大小
engine.setRate1(rate) # 手续费
engine.setPrice1(price) # 最小价格变动
engine.setLeverage(level) # 合约杠杆
engine.initStrategy(STRATEGY_CLASS[className], setting_c)
engine.strategy.onUpdate(setting)
engine.runBacktesting()
opResult = {}
d = engine.calculateBacktestingResult()
try:
targetValue = d[targetName]
except KeyError:
targetValue = 0
for key in setting:
opResult[key] = setting[key]
opResult['totalResult']=d['totalResult']
opResult['capital']=round(d['capital'],2)
if d['totalResult'] > 0:
opResult['maxDrawdown']=min(d['drawdownList'])
opResult['winPerT']=round(d['capital']/d['totalResult'],2)
opResult['splipPerT']=round(d['totalSlippage']/d['totalResult'],2)
opResult['commiPerT']=round(d['totalCommission']/d['totalResult'],2)
else:
opResult['maxDrawdown']=0
opResult['winPerT']=0
opResult['splipPerT']=0
opResult['commiPerT']=0
opResult['winningRate']=round(d['winningRate'],2)
opResult['averageWinning']=round(d['averageWinning'],2)
opResult['averageLosing']=round(d['averageLosing'],2)
opResult['profitLossRatio']=round(d['profitLossRatio'],2)
return (setting,opResult)
if __name__ == '__main__':
# 建议使用ipython notebook或者spyder来做回测
"""读取策略配置"""
begin = datetime.now()
# 回测策略选择
name = 'tl for rb'
# 回测模式设置
opt = False
# 回测参数设置
# 策略参数设置
#optimizationSetting = OptimizationSetting()
#optimizationSetting.addParameter('wLimit', 3, 6, 1)
# 确认检查
print(u'即将开始优化回测,请确认下面的信息正确后开始回测:')
print(u'1.回测引擎是正确的稳定版本')
print(u'2.(乐观\悲观)模式选择正确')
print(u'3.策略逻辑正确')
print(u'4.策略参数初始化无遗漏')
print(u'5.策略参数传递无遗漏')
print(u'6.策略单次回测交割单检查正确')
print(u'7.参数扫描区间合理')
print(u'8.关闭结果文件')
print(u'y/n:')
choice = raw_input(u'')
if not choice == 'y':
exit(0)
# 开始回测
setting_c=getSetting(name)
runParallelOptimization(setting_c,optimizationSetting,optimism=opt)
end = datetime.now()
print(end-begin)
#outfile.close
首先进入“examples\TurtleStrategy”文件夹,通过Jupyter Notebook中打开run.ipynb可以执行策略回测。
1)调用海龟回测引擎
%matplotlib inline
from datetime import datetime
import numpy as np
import matplotlib.pyplot as plt
from turtleEngine import BacktestingEngine
2) 设置回测时间区间和起始资金,读取Csv文件的合约信息进行策略回测,然后显示逐日统计的相关指标和资金图,如图所示。
engine = BacktestingEngine()
engine.setPeriod(datetime(2014, 1, 1), datetime(2018, 12, 30))
engine.initPortfolio('setting.csv', 10000000)
engine.loadData()
engine.runBacktesting()
engine.showResult()
3)针对投资组合里面单个品种,查阅其逐步开平仓记录,如图所示。
tradeList = engine.getTradeData('J99')
for trade in tradeList:
print '%s %s %s %s@%s' %(trade.vtSymbol, trade.direction, trade.offset,
trade.volume, trade.price)
在运行海龟策略回测会读取在同一文件夹内的Csv文件,下面以setting.csv为例说明一下,如下图
需要配置的合约信息包括:合约品种、合约规模、最小价格变动、手续费率(如每一手0.00003)、固定手续费(如每一手12块钱)、滑点。其中手续费率与固定手续费是二选一关系。以PTA合约为例,其品种信息为TA99,合约规模是5吨,最小价格变动是2元/吨,手续费率为0,固定手续费为12块,滑点为2元。
(《海龟交易法则》明确表示其交易信号源于期货指数合约,故用“99”结尾的RQData合约进行策略,等挑选完投资组合品种后,再用主连合约(“88”和“888”结尾)测试观察其差异性。)
原版海龟策略选择标准主要是流动性强品种,若简单地理解为交易所成交量巨大的热门品种。
根据交易所分类所构建的组合历史回测如图所示(测试环境是无手续费,无滑点),图中显示
总体来看,原版海龟测试夏普比率都不错,有着一定的稳健性。
因为国内四大交易所其品种包含了金融产品,工业品,农业品,金属,化工等不同品种分类,为了分散投资组合各个头寸的风险,从而提高组合的夏普比率,故海龟策略投资组合品种必须涵盖四大交易所的品种,现在简单的把四大交易所热门品种组合起来进行测试,其效果如图1-14所示。
新组合的夏普比率达到1.34,要高于上面四个组合,年化收益43.91%,百分比最大回撤达到-29.84%,表现出来高风险高收益的特点,与原版海龟策略基本吻合。
当前品种选择的检验非常顺利,那么就有一个问题:能否在新组合中继续筛选,剔除一下表现不好的品种,去构建一个具有更高夏普比率的组合呢?
答案是否定的。这是一个思维误区,对过去历史表现进行优化,然后筛选出拟合历史行情最优品种,显然没有注意到未来函数的过拟合的问题。
那么,下一章将讲述通过样本内外测试来筛选海龟组合。
作者:张国平 ;来源:维恩的派论坛; vn,py版本:2018年7月版
VNPY中,大多策略都是基于bar分钟级别;国内tick是一秒两笔,频率不算太高。
这里尝试做了一个Tick基本准高频交易策略,只是为了实现思路。可以回测,不要直接用。。
代码如下
# encoding: UTF-8
"""
策略逻辑:
入场: 每两秒读一次,分析过去N1个tick的的总计,如果买量大于卖量K倍,且lastprice向上(最后一个值的lastprice大于或等于10个中的N2个tick的lastprice)
空单反之
下单价格是档期tick,bid or ask价格加一个点位的市价单,(止损)同时开反向2个点为的阻止单;
离场:如果已经是买入价格正向N3个点,再吃判断趋势,如果已经不符合,市价卖出。如何持有,清掉之前阻止单,改挂当前价位反向2个点阻止单。
"""
from __future__ import division
from vnpy.trader.vtGateway import *
from datetime import datetime, time
from vnpy.trader.vtObject import VtBarData
from vnpy.trader.vtConstant import EMPTY_STRING
import numpy as np
from vnpy.trader.app.ctaStrategy.ctaTemplate import (CtaTemplate,
BarGenerator,
ArrayManager)
########################################################################
class TickOneStrategy(CtaTemplate):
"""基于Tick的交易策略"""
className = 'TickOneStrategy'
author = u'BillyZhang'
# 策略参数
fixedSize = 1
Ticksize = 10
initDays = 0
DAY_START = time(9, 00) # 日盘启动和停止时间
DAY_END = time(14, 58)
NIGHT_START = time(21, 00) # 夜盘启动和停止时间
NIGHT_END = time(22, 58)
# 策略变量
posPrice = 0 # 持仓价格
pos = 0 # 持仓数量
# 参数列表,保存了参数的名称
paramList = ['name',
'className',
'author',
'vtSymbol',
'initDays',
'Ticksize',
'fixedSize'
]
# 变量列表,保存了变量的名称
varList = ['inited',
'trading',
'pos',
'posPrice'
]
# 同步列表,保存了需要保存到数据库的变量名称
syncList = ['pos',
'posPrice',
'intraTradeHigh',
'intraTradeLow']
# ----------------------------------------------------------------------
def __init__(self, ctaEngine, setting):
"""Constructor"""
super(TickOneStrategy, self).__init__(ctaEngine, setting)
#创建Array队列
self.tickArray = TickArrayManager(self.Ticksize)
# ----------------------------------------------------------------------
def onminBarClose(self, bar):
""""""
# ----------------------------------------------------------------------
def onInit(self):
"""初始化策略(必须由用户继承实现)"""
self.writeCtaLog(u'%s策略初始化' % self.name)
#tick级别交易,不需要过往历史数据
self.putEvent()
# ----------------------------------------------------------------------
def onStart(self):
"""启动策略(必须由用户继承实现)"""
self.writeCtaLog(u'%s策略启动' % self.name)
self.putEvent()
# ----------------------------------------------------------------------
def onStop(self):
"""停止策略(必须由用户继承实现)"""
self.writeCtaLog(u'%s策略停止' % self.name)
self.putEvent()
# ----------------------------------------------------------------------
def onTick(self, tick):
"""收到行情TICK推送(必须由用户继承实现)"""
# currentTime = datetime.now().time()
currentTime = time(9,20)
# 平当日仓位, 如果当前时间是结束前日盘15点28分钟,或者夜盘10点58分钟,如果有持仓,平仓。
if ((currentTime >= self.DAY_START and currentTime <= self.DAY_END) or
(currentTime >= self.NIGHT_START and currentTime <= self.NIGHT_END)):
TA = self.tickArray
TA.updateTick(tick)
if not TA.inited:
return
if self.pos == 0:
# 如果空仓,分析过去10个对比,ask卖方多下空单,bid买方多下多单,并防止两个差价阻止单
if TA.askBidVolumeDif() > 0:
self.short(tick.lastPrice, self.fixedSize, False)
self.cover(tick.lastPrice + 2,self.fixedSize, True)
elif TA.askBidVolumeDif() < 0:
self.buy(tick.lastPrice, self.fixedSize, False)
self.sell(tick.lastPrice - 2, self.fixedSize, True)
elif self.pos > 0:
# 如果持有多单,如果已经是买入价格正向N3个点,再次判断趋势,如果已经不符合,市价卖出。如何持有,清掉之前阻止单,改挂当前价位反向2个点阻止单。
if tick.lastPrice - self.posPrice >= 3:
if TA.askBidVolumeDif() < 0:
self.cancelAll()
self.sell(tick.lastPrice - 2, self.fixedSize, True)
else:
self.cancelAll()
self.sell(tick.lastPrice, self.fixedSize, False)
elif self.pos < 0:
# 如果持有空单,如果已经是买入价格反向N3个点,再次判断趋势,如果已经不符合,市价卖出。如何持有,清掉之前阻止单,改挂当前价位反向2个点阻止单。
if tick.lastPrice - self.posPrice <= -3:
if TA.askBidVolumeDif() > 0:
self.cancelAll()
self.cover(tick.lastPrice + 2, self.fixedSize, True)
else:
self.cancelAll()
self.cover(tick.lastPrice, self.fixedSize, False)
else:
if self.pos > 0:
self.sell(tick.close, abs(self.pos),False)
elif self.pos < 0:
self.cover(tick.close, abs(self.pos),False)
elif self.pos == 0:
return
# ----------------------------------------------------------------------
def onBar(self, bar):
"""收到Bar推送(必须由用户继承实现)"""
# ----------------------------------------------------------------------
def onXminBar(self, bar):
"""收到X分钟K线"""
# ----------------------------------------------------------------------
def onOrder(self, order):
"""收到委托变化推送(必须由用户继承实现)"""
pass
# ----------------------------------------------------------------------
def onTrade(self, trade):
self.posPrice = trade.price
# 同步数据到数据库
self.saveSyncData()
# 发出状态更新事件
self.putEvent()
# ----------------------------------------------------------------------
def onStopOrder(self, so):
"""停止单推送"""
pass
class TickArrayManager(object):
"""
Tick序列管理工具,负责:
1. Tick时间序列的维护
2. 常用技术指标的计算
"""
# ----------------------------------------------------------------------
def __init__(self, size=10):
"""Constructor"""
self.count = 0 # 缓存计数
self.size = size # 缓存大小
self.inited = False # True if count>=size
self.TicklastPriceArray = np.zeros(self.size)
self.TickaskVolume1Array = np.zeros(self.size)
self.TickbidVolume1Array = np.zeros(self.size)
self.TickaskPrice1Array = np.zeros(self.size)
self.TickbidPrice1Array = np.zeros(self.size)
self.TickopenInterestArray = np.zeros(self.size)
self.TickvolumeArray = np.zeros(self.size)
# ----------------------------------------------------------------------
def updateTick(self, tick):
"""更新tick Array"""
self.count += 1
if not self.inited and self.count >= self.size:
self.inited = True
self.TicklastPriceArray[0:self.size - 1] = self.TicklastPriceArray[1:self.size]
self.TickaskVolume1Array[0:self.size - 1] = self.TickaskVolume1Array[1:self.size]
self.TickbidVolume1Array[0:self.size - 1] = self.TickbidVolume1Array[1:self.size]
self.TickaskPrice1Array[0:self.size - 1] = self.TickaskPrice1Array[1:self.size]
self.TickbidPrice1Array[0:self.size - 1] = self.TickbidPrice1Array[1:self.size]
self.TickopenInterestArray[0:self.size - 1] = self.TickopenInterestArray[1:self.size]
self.TickvolumeArray[0:self.size - 1] = self.TickvolumeArray[1:self.size]
self.TicklastPriceArray[-1] = tick.lastPrice
self.TickaskVolume1Array[-1] = tick.askVolume1
self.TickbidVolume1Array[-1] = tick.bidVolume1
self.TickaskPrice1Array[-1] = tick.askPrice1
self.TickbidPrice1Array[-1] = tick.bidPrice1
self.TickopenInterestArray[-1] = tick.openInterest
self.TickvolumeArray[-1] = tick.volume
def askBidVolumeDif(self):
return (self.TickaskVolume1Array.sum() - self.TickbidVolume1Array.sum())
海龟策略7大要素分别是品种选择、头寸规模、单位头寸限制、入场信号、逐步建仓、止损、止盈。由于品种选择无法通过不属于交易策略内容,故只对后面5大要素的进行代码解析。
头寸规模=(1%账户资金)/(ATR 合约规模),其代码如下
class TurtlePortfolio(object):
......
#----------------------------------------------------------------------
def newSignal(self, signal, direction, offset, price, volume):
......
riskValue = self.portfolioValue * 0.01
multiplier = riskValue / (signal.atrVolatility * size)
multiplier = int(round(multiplier, 0)) # multiplier即为头寸规模
self.multiplierDict[signal.vtSymbol] = multiplier
......
#----------------------------------------------------------------------
def sendOrder(self, vtSymbol, direction, offset, price, volume, multiplier):
""""""
# 计算合约持仓
if direction == DIRECTION_LONG:
self.unitDict[vtSymbol] += volume # volume即为单位头寸
self.posDict[vtSymbol] += volume * multiplier
else:
self.unitDict[vtSymbol] -= volume
self.posDict[vtSymbol] -= volume * multiplier # 实际持仓=单位头寸 * 头寸规模
原版海龟策略规定了4个维度的单位头寸限制,分别是
基于高度关联市场和松散关联市场判断起来都非常主观,并无统一标准。在客观上的层面只能实现单个市场和单个方向的头寸限制。
在开仓交易前,需要检查上一笔交易是否盈利,若是盈利则直接返回,不进行买卖操作(仅仅适用于短周期版本的入场策略,即profitCheck=True;长周期版本对应的是profitCheck=False)
MAX_PRODUCT_POS = 4 # 单品种最大持仓
MAX_DIRECTION_POS = 12 # 单方向最大持仓
......
class TurtlePortfolio(object):
......
#----------------------------------------------------------------------
def newSignal(self, signal, direction, offset, price, volume):
......
# 开仓
if offset == OFFSET_OPEN:
# 检查上一次是否为盈利
if signal.profitCheck:
pnl = signal.getLastPnl()
if pnl > 0:
return
# 买入
if direction == DIRECTION_LONG:
# 组合持仓不能超过上限
if self.totalLong >= MAX_DIRECTION_POS:
return
# 单品种持仓不能超过上限
if self.unitDict[signal.vtSymbol] >= MAX_PRODUCT_POS:
return
# 卖出
else:
if self.totalShort <= -MAX_DIRECTION_POS:
return
if self.unitDict[signal.vtSymbol] <= -MAX_PRODUCT_POS:
return
原版海龟策略提供2个版本的入场和止盈信号,分别是长周期版本和短周期版本的唐奇安通道突破。
逐步建仓的规则是每隔0.5*ATR幅度慢慢加满至4个单位头寸,止损也相应根据0.5*ATR的步进移动。
class TurtleSignal(object):
#----------------------------------------------------------------------
def __init__(self, portfolio, vtSymbol,
entryWindow, exitWindow, atrWindow,
profitCheck=False):
......
#----------------------------------------------------------------------
def onBar(self, bar):
self.bar = bar
self.am.bar
if not self.am.inited:
return
self.generateSignal(bar)
self.calculateIndicator()
#----------------------------------------------------------------------
def generateSignal(self, bar):
"""
判断交易信号
"""
# 如果指标尚未初始化,则忽略
if not self.longEntry1:
return
# 优先检查平仓
if self.unit > 0:
longExit = max(self.longStop, self.exitDown)
if bar.low <= longExit:
self.sell(longExit)
return
elif self.unit < 0:
shortExit = min(self.shortStop, self.exitUp)
if bar.high >= shortExit:
self.cover(shortExit)
return
# 没有仓位或者持有多头仓位的时候,可以做多(加仓)
if self.unit >= 0:
trade = False
if bar.high >= self.longEntry1 and self.unit < 1:
self.buy(self.longEntry1, 1)
trade = True
if bar.high >= self.longEntry2 and self.unit < 2:
self.buy(self.longEntry2, 1)
trade = True
if bar.high >= self.longEntry3 and self.unit < 3:
self.buy(self.longEntry3, 1)
trade = True
if bar.high >= self.longEntry4 and self.unit < 4:
self.buy(self.longEntry4, 1)
trade = True
if trade:
return
# 没有仓位或者持有空头仓位的时候,可以做空(加仓)
if self.unit <= 0:
if bar.low <= self.shortEntry1 and self.unit > -1:
self.short(self.shortEntry1, 1)
if bar.low <= self.shortEntry2 and self.unit > -2:
self.short(self.shortEntry2, 1)
if bar.low <= self.shortEntry3 and self.unit > -3:
self.short(self.shortEntry3, 1)
if bar.low <= self.shortEntry4 and self.unit > -4:
self.short(self.shortEntry4, 1)
#----------------------------------------------------------------------
def calculateIndicator(self):
"""计算技术指标"""
self.entryUp, self.entryDown = self.am.donchian(self.entryWindow)
self.exitUp, self.exitDown = self.am.donchian(self.exitWindow)
# 有持仓后,ATR波动率和入场位等都不再变化
if not self.unit:
self.atrVolatility = self.am.atr(self.atrWindow)
self.longEntry1 = self.entryUp
self.longEntry2 = self.entryUp + self.atrVolatility * 0.5
self.longEntry3 = self.entryUp + self.atrVolatility * 1
self.longEntry4 = self.entryUp + self.atrVolatility * 1.5
self.longStop = 0
self.shortEntry1 = self.entryDown
self.shortEntry2 = self.entryDown - self.atrVolatility * 0.5
self.shortEntry3 = self.entryDown - self.atrVolatility * 1
self.shortEntry4 = self.entryDown - self.atrVolatility * 1.5
self.shortStop = 0
......
class TurtlePortfolio(object):
......
#----------------------------------------------------------------------
def init(self, portfolioValue, vtSymbolList, sizeDict):
""""""
self.portfolioValue = portfolioValue
self.sizeDict = sizeDict
for vtSymbol in vtSymbolList:
signal1 = TurtleSignal(self, vtSymbol, 20, 10, 20, True)
signal2 = TurtleSignal(self, vtSymbol, 55, 20, 20, False)
l = self.signalDict[vtSymbol]
l.append(signal1)
l.append(signal2)
self.unitDict[vtSymbol] = 0
self.posDict[vtSymbol] = 0
#----------------------------------------------------------------------
def onBar(self, bar):
""""""
for signal in self.signalDict[bar.vtSymbol]:
signal.onBar(bar)
展示完毕。
作者: lijiang ;来源:维恩的派论坛 ;应用版本 vn.demo (2016年版本)
启动vn.demo/ctpdemo/demoMain.py,点击选择系统、登录账号,在代码框敲入合约代码+enter键。
效果图如下:
代码实现如下:
# encoding: UTF-8
"""
该文件中包含的是交易平台的上层UI部分,
通过图形界面调用中间层的主动函数,并监控相关数据更新。
Monitor主要负责监控数据,有部分包含主动功能。
Widget主要用于调用主动功能,有部分包含数据监控。
"""
from __future__ import division
import time
import sys
import shelve
from collections import OrderedDict
import sip
from PyQt4 import QtCore, QtGui
import pyqtgraph as pg
import numpy as np
from eventEngine import *
from pymongo import MongoClient
from pymongo.errors import *
from datetime import datetime, timedelta
########################################################################
class LogMonitor(QtGui.QTableWidget):
"""用于显示日志"""
signal = QtCore.pyqtSignal(type(Event()))
#----------------------------------------------------------------------
def __init__(self, eventEngine, parent=None):
"""Constructor"""
super(LogMonitor, self).__init__(parent)
self.__eventEngine = eventEngine
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
"""初始化界面"""
self.setWindowTitle(u'日志')
self.setColumnCount(2)
self.setHorizontalHeaderLabels([u'时间', u'日志'])
self.verticalHeader().setVisible(False) # 关闭左边的垂直表头
self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 设为不可编辑状态
# 自动调整列宽
self.horizontalHeader().setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
self.horizontalHeader().setResizeMode(1, QtGui.QHeaderView.Stretch)
#----------------------------------------------------------------------
def registerEvent(self):
"""注册事件监听"""
# Qt图形组件的GUI更新必须使用Signal/Slot机制,否则有可能导致程序崩溃
# 因此这里先将图形更新函数作为Slot,和信号连接起来
# 然后将信号的触发函数注册到事件驱动引擎中
self.signal.connect(self.updateLog)
self.__eventEngine.register(EVENT_LOG, self.signal.emit)
#----------------------------------------------------------------------
def updateLog(self, event):
"""更新日志"""
# 获取当前时间和日志内容
t = time.strftime('%H:%M:%S',time.localtime(time.time()))
log = event.dict_['log']
# 在表格最上方插入一行
self.insertRow(0)
# 创建单元格
cellTime = QtGui.QTableWidgetItem(t)
cellLog = QtGui.QTableWidgetItem(log)
# 将单元格插入表格
self.setItem(0, 0, cellTime)
self.setItem(0, 1, cellLog)
########################################################################
class AccountMonitor(QtGui.QTableWidget):
"""用于显示账户"""
signal = QtCore.pyqtSignal(type(Event()))
dictLabels = OrderedDict()
dictLabels['AccountID'] = u'投资者账户'
dictLabels['PreBalance'] = u'昨结'
dictLabels['CloseProfit'] = u'平仓盈亏'
dictLabels['PositionProfit'] = u'持仓盈亏'
dictLabels['Commission'] = u'手续费'
dictLabels['CurrMargin'] = u'当前保证金'
dictLabels['Balance'] = u'账户资金'
dictLabels['Available'] = u'可用资金'
dictLabels['WithdrawQuota'] = u'可取资金'
dictLabels['FrozenCash'] = u'冻结资金'
dictLabels['FrozenMargin'] = u'冻结保证金'
dictLabels['FrozenCommission'] = u'冻结手续费'
dictLabels['Withdraw'] = u'出金'
dictLabels['Deposit'] = u'入金'
#----------------------------------------------------------------------
def __init__(self, eventEngine, parent=None):
"""Constructor"""
super(AccountMonitor, self).__init__(parent)
self.__eventEngine = eventEngine
self.dictAccount = {} # 用来保存账户对应的单元格
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
""""""
self.setWindowTitle(u'账户')
self.setColumnCount(len(self.dictLabels))
self.setHorizontalHeaderLabels(self.dictLabels.values())
self.verticalHeader().setVisible(False) # 关闭左边的垂直表头
self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 设为不可编辑状态
#----------------------------------------------------------------------
def registerEvent(self):
""""""
self.signal.connect(self.updateAccount)
self.__eventEngine.register(EVENT_ACCOUNT, self.signal.emit)
#----------------------------------------------------------------------
def updateAccount(self, event):
""""""
data = event.dict_['data']
accountid = data['AccountID']
# 如果之前已经收到过这个账户的数据, 则直接更新
if accountid in self.dictAccount:
d = self.dictAccount[accountid]
for label, cell in d.items():
cell.setText(str(data[label]))
# 否则插入新的一行,并更新
else:
self.insertRow(0)
d = {}
for col, label in enumerate(self.dictLabels.keys()):
cell = QtGui.QTableWidgetItem(str(data[label]))
self.setItem(0, col, cell)
d[label] = cell
self.dictAccount[accountid] = d
########################################################################
class TradeMonitor(QtGui.QTableWidget):
"""用于显示成交记录"""
signal = QtCore.pyqtSignal(type(Event()))
dictLabels = OrderedDict()
dictLabels['InstrumentID'] = u'合约代码'
dictLabels['ExchangeID'] = u'交易所'
dictLabels['Direction'] = u'方向'
dictLabels['OffsetFlag'] = u'开平'
dictLabels['TradeID'] = u'成交编号'
dictLabels['TradeTime'] = u'成交时间'
dictLabels['Volume'] = u'数量'
dictLabels['Price'] = u'价格'
dictLabels['OrderRef'] = u'报单号'
dictLabels['OrderSysID'] = u'报单系统号'
dictDirection = {}
dictDirection['0'] = u'买'
dictDirection['1'] = u' 卖'
dictDirection['2'] = u'ETF申购'
dictDirection['3'] = u'ETF赎回'
dictDirection['4'] = u'ETF现金替代'
dictDirection['5'] = u'债券入库'
dictDirection['6'] = u'债券出库'
dictDirection['7'] = u'配股'
dictDirection['8'] = u'转托管'
dictDirection['9'] = u'信用账户配股'
dictDirection['A'] = u'担保品买入'
dictDirection['B'] = u'担保品卖出'
dictDirection['C'] = u'担保品转入'
dictDirection['D'] = u'担保品转出'
dictDirection['E'] = u'融资买入'
dictDirection['F'] = u'融资卖出'
dictDirection['G'] = u'卖券还款'
dictDirection['H'] = u'买券还券'
dictDirection['I'] = u'直接还款'
dictDirection['J'] = u'直接换券'
dictDirection['K'] = u'余券划转'
dictDirection['L'] = u'OF申购'
dictDirection['M'] = u'OF赎回'
dictDirection['N'] = u'SF拆分'
dictDirection['O'] = u'SF合并'
dictDirection['P'] = u'备兑'
dictDirection['Q'] = u'证券冻结/解冻'
dictDirection['R'] = u'行权'
dictOffset = {}
dictOffset['0'] = u'开仓'
dictOffset['1'] = u' 平仓'
dictOffset['2'] = u' 强平'
dictOffset['3'] = u' 平今'
dictOffset['4'] = u' 平昨'
dictOffset['5'] = u' 强减'
dictOffset['6'] = u' 本地强平'
#----------------------------------------------------------------------
def __init__(self, eventEngine, parent=None):
"""Constructor"""
super(TradeMonitor, self).__init__(parent)
self.__eventEngine = eventEngine
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
""""""
self.setWindowTitle(u'成交')
self.setColumnCount(len(self.dictLabels))
self.setHorizontalHeaderLabels(self.dictLabels.values())
self.verticalHeader().setVisible(False) # 关闭左边的垂直表头
self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 设为不可编辑状态
#----------------------------------------------------------------------
def registerEvent(self):
""""""
self.signal.connect(self.updateTrade)
self.__eventEngine.register(EVENT_TRADE, self.signal.emit)
#----------------------------------------------------------------------
def updateTrade(self, event):
""""""
data = event.dict_['data']
self.insertRow(0)
for col, label in enumerate(self.dictLabels.keys()):
if label == 'Direction':
try:
value = self.dictDirection[data[label]]
except KeyError:
value = u'未知类型'
elif label == 'OffsetFlag':
try:
value = self.dictOffset[data[label]]
except KeyError:
value = u'未知类型'
else:
value = str(data[label])
cell = QtGui.QTableWidgetItem(value)
self.setItem(0, col, cell)
########################################################################
class PositionMonitor(QtGui.QTableWidget):
"""用于显示持仓"""
signal = QtCore.pyqtSignal(type(Event()))
dictLabels = OrderedDict()
dictLabels['InstrumentID'] = u'合约代码'
dictLabels['PosiDirection'] = u'方向'
dictLabels['Position'] = u'持仓'
dictLabels['PositionCost'] = u'持仓成本'
dictLabels['PositionProfit'] = u'持仓盈亏'
dictPosiDirection = {}
dictPosiDirection['1'] = u' 净'
dictPosiDirection['2'] = u'多'
dictPosiDirection['3'] = u' 空'
#----------------------------------------------------------------------
def __init__(self, eventEngine, parent=None):
"""Constructor"""
super(PositionMonitor, self).__init__(parent)
self.__eventEngine = eventEngine
self.dictPosition = {} # 用来保存持仓对应的单元格
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
""""""
self.setWindowTitle(u'持仓')
self.setColumnCount(len(self.dictLabels))
self.setHorizontalHeaderLabels(self.dictLabels.values())
self.verticalHeader().setVisible(False) # 关闭左边的垂直表头
self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 设为不可编辑状态
#----------------------------------------------------------------------
def registerEvent(self):
""""""
self.signal.connect(self.updatePosition)
self.__eventEngine.register(EVENT_POSITION, self.signal.emit)
#----------------------------------------------------------------------
def updatePosition(self, event):
""""""
data = event.dict_['data']
# 过滤返回值为空的情况
if data['InstrumentID']:
posid = data['InstrumentID'] + '.' + data['PosiDirection']
# 如果之前已经收到过这个账户的数据, 则直接更新
if posid in self.dictPosition:
d = self.dictPosition[posid]
for label, cell in d.items():
if label == 'PosiDirection':
try:
value = self.dictPosiDirection[data[label]]
except KeyError:
value = u'未知类型'
else:
value = str(data[label])
cell.setText(value)
# 否则插入新的一行,并更新
else:
self.insertRow(0)
d = {}
for col, label in enumerate(self.dictLabels.keys()):
if label == 'PosiDirection':
try:
value = self.dictPosiDirection[data[label]]
except KeyError:
value = u'未知类型'
else:
value = str(data[label])
cell = QtGui.QTableWidgetItem(value)
self.setItem(0, col, cell)
d[label] = cell
self.dictPosition[posid] = d
########################################################################
class OrderMonitor(QtGui.QTableWidget):
"""用于显示所有报单"""
signal = QtCore.pyqtSignal(type(Event()))
dictLabels = OrderedDict()
dictLabels['OrderRef'] = u'报单号'
dictLabels['InsertTime'] = u'委托时间'
dictLabels['InstrumentID'] = u'合约代码'
dictLabels['Direction'] = u'方向'
dictLabels['CombOffsetFlag'] = u'开平'
dictLabels['LimitPrice'] = u'价格'
dictLabels['VolumeTotalOriginal'] = u'委托数量'
dictLabels['VolumeTraded'] = u'成交数量'
dictLabels['StatusMsg'] = u'状态信息'
dictLabels['OrderSysID'] = u'系统编号'
dictDirection = {}
dictDirection['0'] = u'买'
dictDirection['1'] = u' 卖'
dictDirection['2'] = u'ETF申购'
dictDirection['3'] = u'ETF赎回'
dictDirection['4'] = u'ETF现金替代'
dictDirection['5'] = u'债券入库'
dictDirection['6'] = u'债券出库'
dictDirection['7'] = u'配股'
dictDirection['8'] = u'转托管'
dictDirection['9'] = u'信用账户配股'
dictDirection['A'] = u'担保品买入'
dictDirection['B'] = u'担保品卖出'
dictDirection['C'] = u'担保品转入'
dictDirection['D'] = u'担保品转出'
dictDirection['E'] = u'融资买入'
dictDirection['F'] = u'融资卖出'
dictDirection['G'] = u'卖券还款'
dictDirection['H'] = u'买券还券'
dictDirection['I'] = u'直接还款'
dictDirection['J'] = u'直接换券'
dictDirection['K'] = u'余券划转'
dictDirection['L'] = u'OF申购'
dictDirection['M'] = u'OF赎回'
dictDirection['N'] = u'SF拆分'
dictDirection['O'] = u'SF合并'
dictDirection['P'] = u'备兑'
dictDirection['Q'] = u'证券冻结/解冻'
dictDirection['R'] = u'行权'
dictOffset = {}
dictOffset['0'] = u'开仓'
dictOffset['1'] = u' 平仓'
dictOffset['2'] = u' 强平'
dictOffset['3'] = u' 平今'
dictOffset['4'] = u' 平昨'
dictOffset['5'] = u' 强减'
dictOffset['6'] = u' 本地强平'
#----------------------------------------------------------------------
def __init__(self, eventEngine, mainEngine, parent=None):
"""Constructor"""
super(OrderMonitor, self).__init__(parent)
self.__eventEngine = eventEngine
self.__mainEngine = mainEngine
self.dictOrder = {} # 用来保存报单号对应的单元格对象
self.dictOrderData = {} # 用来保存报单数据
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
""""""
self.setWindowTitle(u'报单')
self.setColumnCount(len(self.dictLabels))
self.setHorizontalHeaderLabels(self.dictLabels.values())
self.verticalHeader().setVisible(False) # 关闭左边的垂直表头
self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 设为不可编辑状态
#----------------------------------------------------------------------
def registerEvent(self):
""""""
self.signal.connect(self.updateOrder)
self.__eventEngine.register(EVENT_ORDER, self.signal.emit)
self.itemDoubleClicked.connect(self.cancelOrder)
#----------------------------------------------------------------------
def updateOrder(self, event):
""""""
data = event.dict_['data']
orderref = data['OrderRef']
self.dictOrderData[orderref] = data
# 如果之前已经收到过这个账户的数据, 则直接更新
if orderref in self.dictOrder:
d = self.dictOrder[orderref]
for label, cell in d.items():
if label == 'Direction':
try:
value = self.dictDirection[data[label]]
except KeyError:
value = u'未知类型'
elif label == 'CombOffsetFlag':
try:
value = self.dictOffset[data[label]]
except KeyError:
value = u'未知类型'
elif label == 'StatusMsg':
value = data[label].decode('gbk')
else:
value = str(data[label])
cell.setText(value)
# 否则插入新的一行,并更新
else:
self.insertRow(0)
d = {}
for col, label in enumerate(self.dictLabels.keys()):
if label == 'Direction':
try:
value = self.dictDirection[data[label]]
except KeyError:
value = u'未知类型'
elif label == 'CombOffsetFlag':
try:
value = self.dictOffset[data[label]]
except KeyError:
value = u'未知类型'
elif label == 'StatusMsg':
value = data[label].decode('gbk')
else:
value = str(data[label])
cell = QtGui.QTableWidgetItem(value)
self.setItem(0, col, cell)
d[label] = cell
cell.orderref = orderref # 动态绑定报单号到单元格上
self.dictOrder[orderref] = d
#----------------------------------------------------------------------
def cancelOrder(self, cell):
"""双击撤单"""
orderref = cell.orderref
order = self.dictOrderData[orderref]
# 撤单前检查报单是否已经撤销或者全部成交
if not (order['OrderStatus'] == '0' or order['OrderStatus'] == '5'):
self.__mainEngine.cancelOrder(order['InstrumentID'],
order['ExchangeID'],
orderref,
order['FrontID'],
order['SessionID'])
#----------------------------------------------------------------------
def cancelAll(self):
"""全撤"""
for order in self.dictOrderData.values():
if not (order['OrderStatus'] == '0' or order['OrderStatus'] == '5'):
self.__mainEngine.cancelOrder(order['InstrumentID'],
order['ExchangeID'],
order['OrderRef'],
order['FrontID'],
order['SessionID'])
########################################################################
class MarketDataMonitor(QtGui.QTableWidget):
"""用于显示行情"""
signal = QtCore.pyqtSignal(type(Event()))
dictLabels = OrderedDict()
dictLabels['Name'] = u'合约名称'
dictLabels['InstrumentID'] = u'合约代码'
dictLabels['ExchangeInstID'] = u'合约交易所代码'
dictLabels['BidPrice1'] = u'买一价'
dictLabels['BidVolume1'] = u'买一量'
dictLabels['AskPrice1'] = u'卖一价'
dictLabels['AskVolume1'] = u'卖一量'
dictLabels['LastPrice'] = u'最新价'
dictLabels['Volume'] = u'成交量'
dictLabels['UpdateTime'] = u'更新时间'
#----------------------------------------------------------------------
def __init__(self, eventEngine, mainEngine, parent=None):
"""Constructor"""
super(MarketDataMonitor, self).__init__(parent)
self.__eventEngine = eventEngine
self.__mainEngine = mainEngine
self.dictData = {}
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
""""""
self.setWindowTitle(u'行情')
self.setColumnCount(len(self.dictLabels))
self.setHorizontalHeaderLabels(self.dictLabels.values())
self.verticalHeader().setVisible(False) # 关闭左边的垂直表头
self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 设为不可编辑状态
#----------------------------------------------------------------------
def registerEvent(self):
""""""
self.signal.connect(self.updateData)
self.__eventEngine.register(EVENT_MARKETDATA, self.signal.emit)
#----------------------------------------------------------------------
def updateData(self, event):
""""""
data = event.dict_['data']
instrumentid = data['InstrumentID']
# 如果之前已经收到过这个账户的数据, 则直接更新
if instrumentid in self.dictData:
d = self.dictData[instrumentid]
for label, cell in d.items():
if label != 'Name':
value = str(data[label])
else:
value = self.getName(data['InstrumentID'])
cell.setText(value)
# 否则插入新的一行,并更新
else:
row = self.rowCount()
self.insertRow(row)
d = {}
for col, label in enumerate(self.dictLabels.keys()):
if label != 'Name':
value = str(data[label])
cell = QtGui.QTableWidgetItem(value)
self.setItem(row, col, cell)
d[label] = cell
else:
name = self.getName(data['InstrumentID'])
cell = QtGui.QTableWidgetItem(name)
self.setItem(row, col, cell)
d[label] = cell
self.dictData[instrumentid] = d
#----------------------------------------------------------------------
def getName(self, instrumentid):
"""获取名称"""
instrument = self.__mainEngine.selectInstrument(instrumentid)
if instrument:
return instrument['InstrumentName'].decode('GBK')
else:
return ''
########################################################################
class LoginWidget(QtGui.QDialog):
"""登录"""
#----------------------------------------------------------------------
def __init__(self, mainEngine, parent=None):
"""Constructor"""
super(LoginWidget, self).__init__()
self.__mainEngine = mainEngine
self.initUi()
self.loadData()
#----------------------------------------------------------------------
def initUi(self):
"""初始化界面"""
self.setWindowTitle(u'登录')
# 设置组件
labelUserID = QtGui.QLabel(u'账号:')
labelPassword = QtGui.QLabel(u'密码:')
labelMdAddress = QtGui.QLabel(u'行情服务器:')
labelTdAddress = QtGui.QLabel(u'交易服务器:')
labelBrokerID = QtGui.QLabel(u'经纪商代码')
self.editUserID = QtGui.QLineEdit()
self.editPassword = QtGui.QLineEdit()
self.editMdAddress = QtGui.QLineEdit()
self.editTdAddress = QtGui.QLineEdit()
self.editBrokerID = QtGui.QLineEdit()
self.editUserID.setMinimumWidth(200)
self.editPassword.setEchoMode(QtGui.QLineEdit.Password)
buttonLogin = QtGui.QPushButton(u'登录')
buttonCancel = QtGui.QPushButton(u'取消')
buttonLogin.clicked.connect(self.login)
buttonCancel.clicked.connect(self.close)
# 设置布局
buttonHBox = QtGui.QHBoxLayout()
buttonHBox.addStretch()
buttonHBox.addWidget(buttonLogin)
buttonHBox.addWidget(buttonCancel)
grid = QtGui.QGridLayout()
grid.addWidget(labelUserID, 0, 0)
grid.addWidget(labelPassword, 1, 0)
grid.addWidget(labelMdAddress, 2, 0)
grid.addWidget(labelTdAddress, 3, 0)
grid.addWidget(labelBrokerID, 4, 0)
grid.addWidget(self.editUserID, 0, 1)
grid.addWidget(self.editPassword, 1, 1)
grid.addWidget(self.editMdAddress, 2, 1)
grid.addWidget(self.editTdAddress, 3, 1)
grid.addWidget(self.editBrokerID, 4, 1)
grid.addLayout(buttonHBox, 5, 0, 1, 2)
self.setLayout(grid)
#----------------------------------------------------------------------
def login(self):
"""登录"""
userid = str(self.editUserID.text())
password = str(self.editPassword.text())
mdAddress = str(self.editMdAddress.text())
tdAddress = str(self.editTdAddress.text())
brokerid = str(self.editBrokerID.text())
self.__mainEngine.login(userid, password, brokerid, mdAddress, tdAddress)
self.close()
#----------------------------------------------------------------------
def loadData(self):
"""读取数据"""
f = shelve.open('setting.vn')
try:
setting = f['login']
userid = setting['userid']
password = setting['password']
mdAddress = setting['mdAddress']
tdAddress = setting['tdAddress']
brokerid = setting['brokerid']
self.editUserID.setText(userid)
self.editPassword.setText(password)
self.editMdAddress.setText(mdAddress)
self.editTdAddress.setText(tdAddress)
self.editBrokerID.setText(brokerid)
except KeyError:
pass
f.close()
#----------------------------------------------------------------------
def saveData(self):
"""保存数据"""
setting = {}
setting['userid'] = str(self.editUserID.text())
setting['password'] = str(self.editPassword.text())
setting['mdAddress'] = str(self.editMdAddress.text())
setting['tdAddress'] = str(self.editTdAddress.text())
setting['brokerid'] = str(self.editBrokerID.text())
f = shelve.open('setting.vn')
f['login'] = setting
f.close()
#----------------------------------------------------------------------
def closeEvent(self, event):
"""关闭事件处理"""
# 当窗口被关闭时,先保存登录数据,再关闭
self.saveData()
event.accept()
########################################################################
class ControlWidget(QtGui.QWidget):
"""调用查询函数"""
#----------------------------------------------------------------------
def __init__(self, mainEngine, parent=None):
"""Constructor"""
super(ControlWidget, self).__init__()
self.__mainEngine = mainEngine
self.initUi()
#----------------------------------------------------------------------
def initUi(self):
""""""
self.setWindowTitle(u'测试')
buttonAccount = QtGui.QPushButton(u'查询账户')
buttonInvestor = QtGui.QPushButton(u'查询投资者')
buttonPosition = QtGui.QPushButton(u'查询持仓')
buttonAccount.clicked.connect(self.__mainEngine.getAccount)
buttonInvestor.clicked.connect(self.__mainEngine.getInvestor)
buttonPosition.clicked.connect(self.__mainEngine.getPosition)
hBox = QtGui.QHBoxLayout()
hBox.addWidget(buttonAccount)
hBox.addWidget(buttonInvestor)
hBox.addWidget(buttonPosition)
self.setLayout(hBox)
########################################################################
class TradingWidget(QtGui.QWidget):
"""交易"""
signal = QtCore.pyqtSignal(type(Event()))
dictDirection = OrderedDict()
dictDirection['0'] = u'买'
dictDirection['1'] = u'卖'
dictOffset = OrderedDict()
dictOffset['0'] = u'开仓'
dictOffset['1'] = u'平仓'
dictOffset['3'] = u'平今'
dictPriceType = OrderedDict()
dictPriceType['1'] = u'任意价'
dictPriceType['2'] = u'限价'
dictPriceType['3'] = u'最优价'
dictPriceType['4'] = u'最新价'
# 反转字典
dictDirectionReverse = {value:key for key,value in dictDirection.items()}
dictOffsetReverse = {value:key for key, value in dictOffset.items()}
dictPriceTypeReverse = {value:key for key, value in dictPriceType.items()}
#----------------------------------------------------------------------
def __init__(self, eventEngine, mainEngine, orderMonitor, parent=None):
"""Constructor"""
super(TradingWidget, self).__init__()
self.__eventEngine = eventEngine
self.__mainEngine = mainEngine
self.__orderMonitor = orderMonitor
self.instrumentid = ''
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
"""初始化界面"""
self.setWindowTitle(u'交易')
# 左边部分
labelID = QtGui.QLabel(u'代码')
labelName = QtGui.QLabel(u'名称')
labelDirection = QtGui.QLabel(u'委托类型')
labelOffset = QtGui.QLabel(u'开平')
labelPrice = QtGui.QLabel(u'价格')
labelVolume = QtGui.QLabel(u'数量')
labelPriceType = QtGui.QLabel(u'价格类型')
self.lineID = QtGui.QLineEdit()
self.lineName = QtGui.QLineEdit()
self.comboDirection = QtGui.QComboBox()
self.comboDirection.addItems(self.dictDirection.values())
self.comboOffset = QtGui.QComboBox()
self.comboOffset.addItems(self.dictOffset.values())
self.spinPrice = QtGui.QDoubleSpinBox()
self.spinPrice.setDecimals(4)
self.spinPrice.setMinimum(0)
self.spinPrice.setMaximum(10000)
self.spinVolume = QtGui.QSpinBox()
self.spinVolume.setMinimum(0)
self.spinVolume.setMaximum(1000000)
self.comboPriceType = QtGui.QComboBox()
self.comboPriceType.addItems(self.dictPriceType.values())
gridleft = QtGui.QGridLayout()
gridleft.addWidget(labelID, 0, 0)
gridleft.addWidget(labelName, 1, 0)
gridleft.addWidget(labelDirection, 2, 0)
gridleft.addWidget(labelOffset, 3, 0)
gridleft.addWidget(labelPrice, 4, 0)
gridleft.addWidget(labelVolume, 5, 0)
gridleft.addWidget(labelPriceType, 6, 0)
gridleft.addWidget(self.lineID, 0, 1)
gridleft.addWidget(self.lineName, 1, 1)
gridleft.addWidget(self.comboDirection, 2, 1)
gridleft.addWidget(self.comboOffset, 3, 1)
gridleft.addWidget(self.spinPrice, 4, 1)
gridleft.addWidget(self.spinVolume, 5, 1)
gridleft.addWidget(self.comboPriceType, 6, 1)
# 右边部分
labelBid1 = QtGui.QLabel(u'买一')
labelBid2 = QtGui.QLabel(u'买二')
labelBid3 = QtGui.QLabel(u'买三')
labelBid4 = QtGui.QLabel(u'买四')
labelBid5 = QtGui.QLabel(u'买五')
labelAsk1 = QtGui.QLabel(u'卖一')
labelAsk2 = QtGui.QLabel(u'卖二')
labelAsk3 = QtGui.QLabel(u'卖三')
labelAsk4 = QtGui.QLabel(u'卖四')
labelAsk5 = QtGui.QLabel(u'卖五')
self.labelBidPrice1 = QtGui.QLabel()
self.labelBidPrice2 = QtGui.QLabel()
self.labelBidPrice3 = QtGui.QLabel()
self.labelBidPrice4 = QtGui.QLabel()
self.labelBidPrice5 = QtGui.QLabel()
self.labelBidVolume1 = QtGui.QLabel()
self.labelBidVolume2 = QtGui.QLabel()
self.labelBidVolume3 = QtGui.QLabel()
self.labelBidVolume4 = QtGui.QLabel()
self.labelBidVolume5 = QtGui.QLabel()
self.labelAskPrice1 = QtGui.QLabel()
self.labelAskPrice2 = QtGui.QLabel()
self.labelAskPrice3 = QtGui.QLabel()
self.labelAskPrice4 = QtGui.QLabel()
self.labelAskPrice5 = QtGui.QLabel()
self.labelAskVolume1 = QtGui.QLabel()
self.labelAskVolume2 = QtGui.QLabel()
self.labelAskVolume3 = QtGui.QLabel()
self.labelAskVolume4 = QtGui.QLabel()
self.labelAskVolume5 = QtGui.QLabel()
labelLast = QtGui.QLabel(u'最新')
self.labelLastPrice = QtGui.QLabel()
self.labelReturn = QtGui.QLabel()
self.labelLastPrice.setMinimumWidth(60)
self.labelReturn.setMinimumWidth(60)
gridRight = QtGui.QGridLayout()
gridRight.addWidget(labelAsk5, 0, 0)
gridRight.addWidget(labelAsk4, 1, 0)
gridRight.addWidget(labelAsk3, 2, 0)
gridRight.addWidget(labelAsk2, 3, 0)
gridRight.addWidget(labelAsk1, 4, 0)
gridRight.addWidget(labelLast, 5, 0)
gridRight.addWidget(labelBid1, 6, 0)
gridRight.addWidget(labelBid2, 7, 0)
gridRight.addWidget(labelBid3, 8, 0)
gridRight.addWidget(labelBid4, 9, 0)
gridRight.addWidget(labelBid5, 10, 0)
gridRight.addWidget(self.labelAskPrice5, 0, 1)
gridRight.addWidget(self.labelAskPrice4, 1, 1)
gridRight.addWidget(self.labelAskPrice3, 2, 1)
gridRight.addWidget(self.labelAskPrice2, 3, 1)
gridRight.addWidget(self.labelAskPrice1, 4, 1)
gridRight.addWidget(self.labelLastPrice, 5, 1)
gridRight.addWidget(self.labelBidPrice1, 6, 1)
gridRight.addWidget(self.labelBidPrice2, 7, 1)
gridRight.addWidget(self.labelBidPrice3, 8, 1)
gridRight.addWidget(self.labelBidPrice4, 9, 1)
gridRight.addWidget(self.labelBidPrice5, 10, 1)
gridRight.addWidget(self.labelAskVolume5, 0, 2)
gridRight.addWidget(self.labelAskVolume4, 1, 2)
gridRight.addWidget(self.labelAskVolume3, 2, 2)
gridRight.addWidget(self.labelAskVolume2, 3, 2)
gridRight.addWidget(self.labelAskVolume1, 4, 2)
gridRight.addWidget(self.labelReturn, 5, 2)
gridRight.addWidget(self.labelBidVolume1, 6, 2)
gridRight.addWidget(self.labelBidVolume2, 7, 2)
gridRight.addWidget(self.labelBidVolume3, 8, 2)
gridRight.addWidget(self.labelBidVolume4, 9, 2)
gridRight.addWidget(self.labelBidVolume5, 10, 2)
# 发单按钮
buttonSendOrder = QtGui.QPushButton(u'发单')
buttonCancelAll = QtGui.QPushButton(u'全撤')
# 整合布局
hbox = QtGui.QHBoxLayout()
hbox.addLayout(gridleft)
hbox.addLayout(gridRight)
vbox = QtGui.QVBoxLayout()
vbox.addLayout(hbox)
vbox.addWidget(buttonSendOrder)
vbox.addWidget(buttonCancelAll)
self.setLayout(vbox)
# 关联更新
buttonSendOrder.clicked.connect(self.sendOrder)
buttonCancelAll.clicked.connect(self.__orderMonitor.cancelAll)
self.lineID.returnPressed.connect(self.updateID)
#----------------------------------------------------------------------
def updateID(self):
"""合约变化"""
instrumentid = str(self.lineID.text())
# 获取合约
instrument = self.__mainEngine.selectInstrument(instrumentid)
if instrument:
self.lineName.setText(instrument['InstrumentName'].decode('GBK'))
# 清空价格数量
self.spinPrice.setValue(0)
self.spinVolume.setValue(0)
# 清空行情显示
self.labelBidPrice1.setText('')
self.labelBidPrice2.setText('')
self.labelBidPrice3.setText('')
self.labelBidPrice4.setText('')
self.labelBidPrice5.setText('')
self.labelBidVolume1.setText('')
self.labelBidVolume2.setText('')
self.labelBidVolume3.setText('')
self.labelBidVolume4.setText('')
self.labelBidVolume5.setText('')
self.labelAskPrice1.setText('')
self.labelAskPrice2.setText('')
self.labelAskPrice3.setText('')
self.labelAskPrice4.setText('')
self.labelAskPrice5.setText('')
self.labelAskVolume1.setText('')
self.labelAskVolume2.setText('')
self.labelAskVolume3.setText('')
self.labelAskVolume4.setText('')
self.labelAskVolume5.setText('')
self.labelLastPrice.setText('')
self.labelReturn.setText('')
# 重新注册事件监听
self.__eventEngine.unregister(EVENT_MARKETDATA_CONTRACT+self.instrumentid, self.signal.emit)
self.__eventEngine.register(EVENT_MARKETDATA_CONTRACT+instrumentid, self.signal.emit)
# 订阅合约
self.__mainEngine.subscribe(instrumentid, instrument['ExchangeID'])
# 更新目前的合约
self.instrumentid = instrumentid
#----------------------------------------------------------------------
def updateMarketData(self, event):
"""更新行情"""
data = event.dict_['data']
if data['InstrumentID'] == self.instrumentid:
self.labelBidPrice1.setText(str(data['BidPrice1']))
self.labelAskPrice1.setText(str(data['AskPrice1']))
self.labelBidVolume1.setText(str(data['BidVolume1']))
self.labelAskVolume1.setText(str(data['AskVolume1']))
if data['BidVolume2']:
self.labelBidPrice2.setText(str(data['BidPrice2']))
self.labelBidPrice3.setText(str(data['BidPrice3']))
self.labelBidPrice4.setText(str(data['BidPrice4']))
self.labelBidPrice5.setText(str(data['BidPrice5']))
self.labelAskPrice2.setText(str(data['AskPrice2']))
self.labelAskPrice3.setText(str(data['AskPrice3']))
self.labelAskPrice4.setText(str(data['AskPrice4']))
self.labelAskPrice5.setText(str(data['AskPrice5']))
self.labelBidVolume2.setText(str(data['BidVolume2']))
self.labelBidVolume3.setText(str(data['BidVolume3']))
self.labelBidVolume4.setText(str(data['BidVolume4']))
self.labelBidVolume5.setText(str(data['BidVolume5']))
self.labelAskVolume2.setText(str(data['AskVolume2']))
self.labelAskVolume3.setText(str(data['AskVolume3']))
self.labelAskVolume4.setText(str(data['AskVolume4']))
self.labelAskVolume5.setText(str(data['AskVolume5']))
self.labelLastPrice.setText(str(data['LastPrice']))
rt = (data['LastPrice']/data['PreClosePrice'])-1
self.labelReturn.setText(('%.2f' %(rt*100))+'%')
#----------------------------------------------------------------------
def registerEvent(self):
"""注册事件监听"""
self.signal.connect(self.updateMarketData)
#----------------------------------------------------------------------
def sendOrder(self):
"""发单"""
instrumentid = str(self.lineID.text())
instrument = self.__mainEngine.selectInstrument(instrumentid)
if instrument:
exchangeid = instrument['ExchangeID']
direction = self.dictDirectionReverse[unicode(self.comboDirection.currentText())]
offset = self.dictOffsetReverse[unicode(self.comboOffset.currentText())]
price = float(self.spinPrice.value())
volume = int(self.spinVolume.value())
pricetype = self.dictPriceTypeReverse[unicode(self.comboPriceType.currentText())]
self.__mainEngine.sendOrder(instrumentid, exchangeid, price, pricetype, volume ,direction, offset)
########################################################################
class AboutWidget(QtGui.QDialog):
"""显示关于信息"""
#----------------------------------------------------------------------
def __init__(self, parent):
"""Constructor"""
super(AboutWidget, self).__init__(parent)
self.initUi()
#----------------------------------------------------------------------
def initUi(self):
""""""
self.setWindowTitle(u'关于')
text = u"""
vn.py框架Demo
完成日期:2015/4/17
作者:用Python的交易员
License:MIT
主页:vnpy.org
Github:github.com/vnpy/vnpy
QQ交流群:262656087
开发环境
操作系统:Windows 7 专业版 64位
Python发行版:Python 2.7.6 (Anaconda 1.9.2 Win-32)
图形库:PyQt4 4.11.3 Py2.7-x32
交易接口:vn.lts/vn.ctp
事件驱动引擎:vn.event
开发环境:WingIDE 5.0.6
EXE打包:Nuitka 0.5.12.1 Python2.7 32 bit MSI
"""
label = QtGui.QLabel()
label.setText(text)
label.setMinimumWidth(450)
vbox = QtGui.QVBoxLayout()
vbox.addWidget(label)
self.setLayout(vbox)
########################################################################
class PriceWidget(QtGui.QWidget):
"""用于显示价格走势图"""
signal = QtCore.pyqtSignal(type(Event()))
# tick图的相关参数、变量
listlastPrice = np.empty(1000)
fastMA = 0
midMA = 0
slowMA = 0
listfastMA = np.empty(1000)
listmidMA = np.empty(1000)
listslowMA = np.empty(1000)
tickFastAlpha = 0.0333 # 快速均线的参数,30
tickMidAlpha = 0.0167 # 中速均线的参数,60
tickSlowAlpha = 0.0083 # 慢速均线的参数,120
ptr = 0
ticktime = None # tick数据时间
# K线图EMA均线的参数、变量
EMAFastAlpha = 0.0167 # 快速EMA的参数,60
EMASlowAlpha = 0.0083 # 慢速EMA的参数,120
fastEMA = 0 # 快速EMA的数值
slowEMA = 0 # 慢速EMA的数值
listfastEMA = []
listslowEMA = []
# K线缓存对象
barOpen = 0
barHigh = 0
barLow = 0
barClose = 0
barTime = None
barOpenInterest = 0
num = 0
# 保存K线数据的列表对象
listBar = []
listClose = []
listHigh = []
listLow = []
listOpen = []
listOpenInterest = []
# 是否完成了历史数据的读取
initCompleted = False
# 初始化时读取的历史数据的起始日期(可以选择外部设置)
startDate = None
symbol = 'SR701'
class CandlestickItem(pg.GraphicsObject):
def __init__(self, data):
pg.GraphicsObject.__init__(self)
self.data = data ## data must have fields: time, open, close, min, max
self.generatePicture()
def generatePicture(self):
## pre-computing a QPicture object allows paint() to run much more quickly,
## rather than re-drawing the shapes every time.
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen(color='w', width=0.4)) # 0.4 means w*2
# w = (self.data[1][0] - self.data[0][0]) / 3.
w = 0.2
for (t, open, close, min, max) in self.data:
p.drawLine(QtCore.QPointF(t, min), QtCore.QPointF(t, max))
if open > close:
p.setBrush(pg.mkBrush('g'))
else:
p.setBrush(pg.mkBrush('r'))
p.drawRect(QtCore.QRectF(t-w, open, w*2, close-open))
p.end()
def paint(self, p, *args):
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
## boundingRect _must_ indicate the entire area that will be drawn on
## or else we will get artifacts and possibly crashing.
## (in this case, QPicture does all the work of computing the bouning rect for us)
return QtCore.QRectF(self.picture.boundingRect())
#----------------------------------------------------------------------
def __init__(self, eventEngine, mainEngine, parent=None):
"""Constructor"""
super(PriceWidget, self).__init__(parent)
self.__eventEngine = eventEngine
self.__mainEngine = mainEngine
# MongoDB数据库相关
self.__mongoConnected = False
self.__mongoConnection = None
self.__mongoTickDB = None
# 调用函数
self.__connectMongo()
self.initUi(startDate=None)
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self, startDate=None):
"""初始化界面"""
self.setWindowTitle(u'Price')
self.vbl_1 = QtGui.QVBoxLayout()
self.initplotTick() # plotTick初始化
self.vbl_2 = QtGui.QVBoxLayout()
self.initplotKline() # plotKline初始化
self.initplotTendency() # plot分时图的初始化
# 整体布局
self.hbl = QtGui.QHBoxLayout()
self.hbl.addLayout(self.vbl_1)
self.hbl.addLayout(self.vbl_2)
self.setLayout(self.hbl)
self.initHistoricalData() # 下载历史数据
#----------------------------------------------------------------------
def initplotTick(self):
""""""
self.pw1 = pg.PlotWidget(name='Plot1')
self.vbl_1.addWidget(self.pw1)
self.pw1.setRange(xRange=[-360, 0])
self.pw1.setLimits(xMax=5)
self.pw1.setDownsampling(mode='peak')
self.pw1.setClipToView(True)
self.curve1 = self.pw1.plot()
self.curve2 = self.pw1.plot()
self.curve3 = self.pw1.plot()
self.curve4 = self.pw1.plot()
#----------------------------------------------------------------------
def initplotKline(self):
"""Kline"""
self.pw2 = pg.PlotWidget(name='Plot2') # K线图
self.vbl_2.addWidget(self.pw2)
self.pw2.setDownsampling(mode='peak')
self.pw2.setClipToView(True)
self.curve5 = self.pw2.plot()
self.curve6 = self.pw2.plot()
self.candle = self.CandlestickItem(self.listBar)
self.pw2.addItem(self.candle)
## Draw an arrowhead next to the text box
# self.arrow = pg.ArrowItem()
# self.pw2.addItem(self.arrow)
#----------------------------------------------------------------------
def initplotTendency(self):
""""""
self.pw3 = pg.PlotWidget(name='Plot3')
self.vbl_2.addWidget(self.pw3)
self.pw3.setDownsampling(mode='peak')
self.pw3.setClipToView(True)
self.pw3.setMaximumHeight(200)
self.pw3.setXLink('Plot2') # X linked with Plot2
self.curve7 = self.pw3.plot()
#----------------------------------------------------------------------
def initHistoricalData(self,startDate=None):
"""初始历史数据"""
td = timedelta(days=1) # 读取3天的历史TICK数据
if startDate:
cx = self.loadTick(self.symbol, startDate-td)
else:
today = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
cx = self.loadTick(self.symbol, today-td)
if cx:
for data in cx:
tick = Tick(data['InstrumentID'])
tick.openPrice = data['OpenPrice']
tick.highPrice = data['HighestPrice']
tick.lowPrice = data['LowestPrice']
tick.lastPrice = data['LastPrice']
tick.volume = data['Volume']
tick.openInterest = data['OpenInterest']
tick.upperLimit = data['UpperLimitPrice']
tick.lowerLimit = data['LowerLimitPrice']
tick.time = data['UpdateTime']
tick.ms = data['UpdateMillisec']
tick.bidPrice1 = data['BidPrice1']
tick.bidPrice2 = data['BidPrice2']
tick.bidPrice3 = data['BidPrice3']
tick.bidPrice4 = data['BidPrice4']
tick.bidPrice5 = data['BidPrice5']
tick.askPrice1 = data['AskPrice1']
tick.askPrice2 = data['AskPrice2']
tick.askPrice3 = data['AskPrice3']
tick.askPrice4 = data['AskPrice4']
tick.askPrice5 = data['AskPrice5']
tick.bidVolume1 = data['BidVolume1']
tick.bidVolume2 = data['BidVolume2']
tick.bidVolume3 = data['BidVolume3']
tick.bidVolume4 = data['BidVolume4']
tick.bidVolume5 = data['BidVolume5']
tick.askVolume1 = data['AskVolume1']
tick.askVolume2 = data['AskVolume2']
tick.askVolume3 = data['AskVolume3']
tick.askVolume4 = data['AskVolume4']
tick.askVolume5 = data['AskVolume5']
self.onTick(tick)
self.initCompleted = True # 读取历史数据完成
# pprint('load historic data completed')
#----------------------------------------------------------------------
def plotTick(self):
"""画tick图"""
if self.initCompleted:
self.curve1.setData(self.listlastPrice[:self.ptr])
self.curve2.setData(self.listfastMA[:self.ptr], pen=(255, 0, 0), name="Red curve")
self.curve3.setData(self.listmidMA[:self.ptr], pen=(0, 255, 0), name="Green curve")
self.curve4.setData(self.listslowMA[:self.ptr], pen=(0, 0, 255), name="Blue curve")
self.curve1.setPos(-self.ptr, 0)
self.curve2.setPos(-self.ptr, 0)
self.curve3.setPos(-self.ptr, 0)
self.curve4.setPos(-self.ptr, 0)
#----------------------------------------------------------------------
def plotKline(self):
"""K线图"""
if self.initCompleted:
# 均线
self.curve5.setData(self.listfastEMA, pen=(255, 0, 0), name="Red curve")
self.curve6.setData(self.listslowEMA, pen=(0, 255, 0), name="Green curve")
# 画K线
self.pw2.removeItem(self.candle)
self.candle = self.CandlestickItem(self.listBar)
self.pw2.addItem(self.candle)
self.plotText() # 显示开仓信号位置
#----------------------------------------------------------------------
def plotTendency(self):
""""""
if self.initCompleted:
self.curve7.setData(self.listOpenInterest, pen=(255, 255, 255), name="White curve")
#----------------------------------------------------------------------
def plotText(self):
lenClose = len(self.listClose)
if lenClose >= 5: # Fractal Signal
if self.listClose[-1] > self.listClose[-2] and self.listClose[-3] > self.listClose[-2] and self.listClose[-4] > self.listClose[-2] and self.listClose[-5] > self.listClose[-2] and self.listfastEMA[-1] > self.listslowEMA[-1]:
## Draw an arrowhead next to the text box
# self.pw2.removeItem(self.arrow)
self.arrow = pg.ArrowItem(pos=(lenClose-1, self.listLow[-1]), angle=90, brush=(255, 0, 0))
self.pw2.addItem(self.arrow)
elif self.listClose[-1] < self.listClose[-2] and self.listClose[-3] < self.listClose[-2] and self.listClose[-4] < self.listClose[-2] and self.listClose[-5] < self.listClose[-2] and self.listfastEMA[-1] < self.listslowEMA[-1]:
## Draw an arrowhead next to the text box
# self.pw2.removeItem(self.arrow)
self.arrow = pg.ArrowItem(pos=(lenClose-1, self.listHigh[-1]), angle=-90, brush=(0, 255, 0))
self.pw2.addItem(self.arrow)
#----------------------------------------------------------------------
def updateMarketData(self, event):
"""更新行情"""
data = event.dict_['data']
symbol = data['InstrumentID']
tick = Tick(symbol)
tick.openPrice = data['OpenPrice']
tick.highPrice = data['HighestPrice']
tick.lowPrice = data['LowestPrice']
tick.lastPrice = data['LastPrice']
tick.volume = data['Volume']
tick.openInterest = data['OpenInterest']
tick.upperLimit = data['UpperLimitPrice']
tick.lowerLimit = data['LowerLimitPrice']
tick.time = data['UpdateTime']
tick.ms = data['UpdateMillisec']
tick.bidPrice1 = data['BidPrice1']
tick.bidPrice2 = data['BidPrice2']
tick.bidPrice3 = data['BidPrice3']
tick.bidPrice4 = data['BidPrice4']
tick.bidPrice5 = data['BidPrice5']
tick.askPrice1 = data['AskPrice1']
tick.askPrice2 = data['AskPrice2']
tick.askPrice3 = data['AskPrice3']
tick.askPrice4 = data['AskPrice4']
tick.askPrice5 = data['AskPrice5']
tick.bidVolume1 = data['BidVolume1']
tick.bidVolume2 = data['BidVolume2']
tick.bidVolume3 = data['BidVolume3']
tick.bidVolume4 = data['BidVolume4']
tick.bidVolume5 = data['BidVolume5']
tick.askVolume1 = data['AskVolume1']
tick.askVolume2 = data['AskVolume2']
tick.askVolume3 = data['AskVolume3']
tick.askVolume4 = data['AskVolume4']
tick.askVolume5 = data['AskVolume5']
self.onTick(tick) # tick数据更新
# # 将数据插入MongoDB数据库,实盘建议另开程序记录TICK数据
# self.__recordTick(data)
#----------------------------------------------------------------------
def onTick(self, tick):
"""tick数据更新"""
from datetime import time
# 首先生成datetime.time格式的时间(便于比较),从字符串时间转化为time格式的时间
hh, mm, ss = tick.time.split(':')
self.ticktime = time(int(hh), int(mm), int(ss), microsecond=tick.ms)
# 计算tick图的相关参数
if self.ptr == 0:
self.fastMA = tick.lastPrice
self.midMA = tick.lastPrice
self.slowMA = tick.lastPrice
else:
self.fastMA = (1-self.tickFastAlpha) * self.fastMA + self.tickFastAlpha * tick.lastPrice
self.midMA = (1-self.tickMidAlpha) * self.midMA + self.tickMidAlpha * tick.lastPrice
self.slowMA = (1-self.tickSlowAlpha) * self.slowMA + self.tickSlowAlpha * tick.lastPrice
self.listlastPrice[self.ptr] = tick.lastPrice
self.listfastMA[self.ptr] = self.fastMA
self.listmidMA[self.ptr] = self.midMA
self.listslowMA[self.ptr] = self.slowMA
self.ptr += 1
# pprint("----------")
# pprint(self.ptr)
if self.ptr >= self.listlastPrice.shape[0]:
tmp = self.listlastPrice
self.listlastPrice = np.empty(self.listlastPrice.shape[0] * 2)
self.listlastPrice[:tmp.shape[0]] = tmp
tmp = self.listfastMA
self.listfastMA = np.empty(self.listfastMA.shape[0] * 2)
self.listfastMA[:tmp.shape[0]] = tmp
tmp = self.listmidMA
self.listmidMA = np.empty(self.listmidMA.shape[0] * 2)
self.listmidMA[:tmp.shape[0]] = tmp
tmp = self.listslowMA
self.listslowMA = np.empty(self.listslowMA.shape[0] * 2)
self.listslowMA[:tmp.shape[0]] = tmp
# K线数据
# 假设是收到的第一个TICK
if self.barOpen == 0:
# 初始化新的K线数据
self.barOpen = tick.lastPrice
self.barHigh = tick.lastPrice
self.barLow = tick.lastPrice
self.barClose = tick.lastPrice
self.barTime = self.ticktime
self.barOpenInterest = tick.openInterest
self.onBar(self.num, self.barOpen, self.barClose, self.barLow, self.barHigh, self.barOpenInterest)
else:
# 如果是当前一分钟内的数据
if self.ticktime.minute == self.barTime.minute:
if self.ticktime.second >= 30 and self.barTime.second < 30: # 判断30秒周期K线
# 先保存K线收盘价
self.num += 1
self.onBar(self.num, self.barOpen, self.barClose, self.barLow, self.barHigh, self.barOpenInterest)
# 初始化新的K线数据
self.barOpen = tick.lastPrice
self.barHigh = tick.lastPrice
self.barLow = tick.lastPrice
self.barClose = tick.lastPrice
self.barTime = self.ticktime
self.barOpenInterest = tick.openInterest
# 汇总TICK生成K线
self.barHigh = max(self.barHigh, tick.lastPrice)
self.barLow = min(self.barLow, tick.lastPrice)
self.barClose = tick.lastPrice
self.barTime = self.ticktime
self.listBar.pop()
self.listfastEMA.pop()
self.listslowEMA.pop()
self.listOpen.pop()
self.listClose.pop()
self.listHigh.pop()
self.listLow.pop()
self.listOpenInterest.pop()
self.onBar(self.num, self.barOpen, self.barClose, self.barLow, self.barHigh, self.barOpenInterest)
# 如果是新一分钟的数据
else:
# 先保存K线收盘价
self.num += 1
self.onBar(self.num, self.barOpen, self.barClose, self.barLow, self.barHigh, self.barOpenInterest)
# 初始化新的K线数据
self.barOpen = tick.lastPrice
self.barHigh = tick.lastPrice
self.barLow = tick.lastPrice
self.barClose = tick.lastPrice
self.barTime = self.ticktime
self.barOpenInterest = tick.openInterest
#----------------------------------------------------------------------
def onBar(self, n, o, c, l, h, oi):
self.listBar.append((n, o, c, l, h))
self.listOpen.append(o)
self.listClose.append(c)
self.listHigh.append(h)
self.listLow.append(l)
self.listOpenInterest.append(oi)
#计算K线图EMA均线
if self.fastEMA:
self.fastEMA = c*self.EMAFastAlpha + self.fastEMA*(1-self.EMAFastAlpha)
self.slowEMA = c*self.EMASlowAlpha + self.slowEMA*(1-self.EMASlowAlpha)
else:
self.fastEMA = c
self.slowEMA = c
self.listfastEMA.append(self.fastEMA)
self.listslowEMA.append(self.slowEMA)
# 调用画图函数
self.plotTick() # tick图
self.plotKline() # K线图
self.plotTendency() # K线副图,持仓量
#----------------------------------------------------------------------
def __connectMongo(self):
"""连接MongoDB数据库"""
try:
self.__mongoConnection = MongoClient()
self.__mongoConnected = True
self.__mongoTickDB = self.__mongoConnection['TickDB']
except ConnectionFailure:
pass
#----------------------------------------------------------------------
def __recordTick(self, data):
"""将Tick数据插入到MongoDB中"""
if self.__mongoConnected:
symbol = data['InstrumentID']
data['date'] = self.today
self.__mongoTickDB[symbol].insert(data)
#----------------------------------------------------------------------
def loadTick(self, symbol, startDate, endDate=None):
"""从MongoDB中读取Tick数据"""
if self.__mongoConnected:
collection = self.__mongoTickDB[symbol]
# 如果输入了读取TICK的最后日期
if endDate:
cx = collection.find({'date': {'$gte': startDate, '$lte': endDate}})
else:
cx = collection.find({'date': {'$gte': startDate}})
return cx
else:
return None
#----------------------------------------------------------------------
def registerEvent(self):
"""注册事件监听"""
self.signal.connect(self.updateMarketData)
self.__eventEngine.register(EVENT_MARKETDATA, self.signal.emit)
########################################################################
class MainWindow(QtGui.QMainWindow):
"""主窗口"""
signalInvestor = QtCore.pyqtSignal(type(Event()))
signalLog = QtCore.pyqtSignal(type(Event()))
#----------------------------------------------------------------------
def __init__(self, eventEngine, mainEngine):
"""Constructor"""
super(MainWindow, self).__init__()
self.__eventEngine = eventEngine
self.__mainEngine = mainEngine
self.initUi()
self.registerEvent()
#----------------------------------------------------------------------
def initUi(self):
""""""
# 设置名称
self.setWindowTitle(u'欢迎使用vn.py框架Demo')
# 布局设置
self.logM = LogMonitor(self.__eventEngine, self)
self.accountM = AccountMonitor(self.__eventEngine, self)
self.positionM = PositionMonitor(self.__eventEngine, self)
self.tradeM = TradeMonitor(self.__eventEngine, self)
self.orderM = OrderMonitor(self.__eventEngine, self.__mainEngine, self)
self.marketdataM = MarketDataMonitor(self.__eventEngine, self.__mainEngine, self)
self.tradingW = TradingWidget(self.__eventEngine, self.__mainEngine, self.orderM, self)
self.PriceW = PriceWidget(self.__eventEngine, self.__mainEngine, self)
righttab = QtGui.QTabWidget()
righttab.addTab(self.accountM, u'账户')
righttab.addTab(self.positionM, u'持仓')
lefttab = QtGui.QTabWidget()
lefttab.addTab(self.logM, u'日志')
lefttab.addTab(self.orderM, u'报单')
lefttab.addTab(self.tradeM, u'成交')
mkttab = QtGui.QTabWidget()
mkttab.addTab(self.PriceW, u'Price')
mkttab.addTab(self.marketdataM, u'行情')
self.tradingW.setMaximumWidth(400)
tradingVBox = QtGui.QVBoxLayout()
tradingVBox.addWidget(self.tradingW)
tradingVBox.addStretch()
upHBox = QtGui.QHBoxLayout()
upHBox.addLayout(tradingVBox)
upHBox.addWidget(mkttab)
downHBox = QtGui.QHBoxLayout()
downHBox.addWidget(lefttab)
downHBox.addWidget(righttab)
vBox = QtGui.QVBoxLayout()
vBox.addLayout(upHBox)
vBox.addLayout(downHBox)
centralwidget = QtGui.QWidget()
centralwidget.setLayout(vBox)
self.setCentralWidget(centralwidget)
# 设置状态栏
self.bar = self.statusBar()
self.bar.showMessage(u'启动Demo')
# 设置菜单栏
actionLogin = QtGui.QAction(u'登录', self)
actionLogin.triggered.connect(self.openLoginWidget)
actionExit = QtGui.QAction(u'退出', self)
actionExit.triggered.connect(self.close)
actionAbout = QtGui.QAction(u'关于', self)
actionAbout.triggered.connect(self.openAboutWidget)
menubar = self.menuBar()
sysMenu = menubar.addMenu(u'系统')
sysMenu.addAction(actionLogin)
sysMenu.addAction(actionExit)
helpMenu = menubar.addMenu(u'帮助')
helpMenu.addAction(actionAbout)
#----------------------------------------------------------------------
def registerEvent(self):
""""""
self.signalInvestor.connect(self.updateInvestor)
self.signalLog.connect(self.updateLog)
self.__eventEngine.register(EVENT_INVESTOR, self.signalInvestor.emit)
self.__eventEngine.register(EVENT_LOG, self.signalLog.emit)
#----------------------------------------------------------------------
def updateInvestor(self, event):
""""""
data = event.dict_['data']
self.setWindowTitle(u'欢迎使用vn.py框架Demo ' + data['InvestorName'].decode('GBK'))
#----------------------------------------------------------------------
def updateLog(self, event):
""""""
log = event.dict_['log']
self.bar.showMessage(log)
#----------------------------------------------------------------------
def openLoginWidget(self):
"""打开登录"""
try:
self.loginW.show()
except AttributeError:
self.loginW = LoginWidget(self.__mainEngine, self)
self.loginW.show()
#----------------------------------------------------------------------
def openAboutWidget(self):
"""打开关于"""
try:
self.aboutW.show()
except AttributeError:
self.aboutW = AboutWidget(self)
self.aboutW.show()
#----------------------------------------------------------------------
def closeEvent(self, event):
"""退出事件处理"""
reply = QtGui.QMessageBox.question(self, u'退出',
u'确认退出?', QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No, QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
self.__mainEngine.exit()
event.accept()
else:
event.ignore()
class Tick:
"""Tick数据对象"""
#----------------------------------------------------------------------
def __init__(self, symbol):
"""Constructor"""
self.symbol = symbol # 合约代码
self.openPrice = 0 # OHLC
self.highPrice = 0
self.lowPrice = 0
self.lastPrice = 0
self.volume = 0 # 成交量
self.openInterest = 0 # 持仓量
self.upperLimit = 0 # 涨停价
self.lowerLimit = 0 # 跌停价
self.time = '' # 更新时间和毫秒
self.ms = 0
self.bidPrice1 = 0 # 深度行情
self.bidPrice2 = 0
self.bidPrice3 = 0
self.bidPrice4 = 0
self.bidPrice5 = 0
self.askPrice1 = 0
self.askPrice2 = 0
self.askPrice3 = 0
self.askPrice4 = 0
self.askPrice5 = 0
self.bidVolume1 = 0
self.bidVolume2 = 0
self.bidVolume3 = 0
self.bidVolume4 = 0
self.bidVolume5 = 0
self.askVolume1 = 0
self.askVolume2 = 0
self.askVolume3 = 0
self.askVolume4 = 0
self.askVolume5 = 0
在运行策略回测前,至少要对策略代码有一个整体的概念,便于以后的策略调试以及改进。
v1.9.1提供了两个版本的海龟策略,分别是基于ctaTemple开发的,针对单标的的简化版,以及针对多标的的完整版。
这里只介绍完整版的海龟策略,该模块在"examples\TurtleStrategy"文件夹下,打开如图2-4所示。在这里只需关注4个文件
现在只关注turleStrategy.py这个文件。海龟策略由3个类构成,分别是TurtleResult、TurtleSignal、TurtlePortfolio。阅读顺序并不是简单的从上到下,而是具有镶嵌结构。
所以呢,刚阅读时候容易一脸懵逼,一脸懵逼。。。
下面整理了策略代码的结构图,如图,箭头方向代表先定义,后调用。
用于计算单笔开平仓交易盈亏,是海龟策略中判断“若上一笔盈利当前信号无效”的基础
用于产生海龟策略交易信号,包括入场,止损,止盈委托价格与目标仓位
根据账户资金和品种合约规模,产生具体交易委托
作者:爱谁谁 ;来源:维恩的派论坛
原文标题《新手瞎掰掰之回测引擎代码分析流程图》
关于ctaBacktesting.py的流程图
上一篇介绍了海龟策略在实现中遇到的困难。
本章主要讲其解决方案,那就是vn.py啦!
vn.py1.9.1新增完整的投资组合级别的海龟策略实现,经过多次测试发现,这一次海龟策略本地化实现的完成度很高。其投资组合回测资金曲线如下。
投资品种选择了12个,分别是:
回测时间是2014--2018,百分比最大回撤是-29.46%,年化收益45.11%,夏普比率达1.5。从资金曲线的形态上看,虽然时不时发生回撤,但是整体趋势是向上的。故从回测上看,已经非常符合原版海龟策略的特点了。
(因为考虑到海龟策略是日K线中低频策略,故手续费和滑点在回测中都设置成零,以方便操作。但就算加上手续费和滑点,资金曲线的形态不会发生变化,夏普比率也仅仅是稍微减少,这在以后章节详细说明。)
下面介绍如何通过vn.py实现完整的海龟策略。
Window系统RQData安装,输入下面命令即可。
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --extra-index-url https://rquser:ricequant99@py.r
icequant.com/simple/ rqdatac==1.0.0a29
在"examples\DataService\RqdataDataService"文件夹内的config.json上填写RQData的账号和密码,保存退出后启动downloadData.py下载全期货品种的日线级别数据。启动操作是按住"Shift"并且鼠标点击右键,选择“在此处打开命令窗口”,然后输入下面命令即可。
python downloadData.py
本次投资组合回测时间区间是2014年1月1日以后的数据,故在该回测上会剔除一下比较新的品种,在下面的代码中以注释出来。
其代码如下
"""
立即下载数据到数据库中,用于手动执行更新操作。
"""
from dataService import *
if __name__ == '__main__':
#中金所
downloadDailyBarBySymbol('IF99')
downloadDailyBarBySymbol('IC99') #中证500 从2015开始
downloadDailyBarBySymbol('IH99') #上证50 从2015开始
#上期所
downloadDailyBarBySymbol('CU99')
downloadDailyBarBySymbol('AL99')
downloadDailyBarBySymbol('ZN99')
downloadDailyBarBySymbol('PB99')
downloadDailyBarBySymbol('NI99') #镍 从2015开始
downloadDailyBarBySymbol('AU99')
downloadDailyBarBySymbol('SN99') #锡 从2015开始
downloadDailyBarBySymbol('AG99')
downloadDailyBarBySymbol('RB99')
downloadDailyBarBySymbol('WR99')
downloadDailyBarBySymbol('HC99') #热轧卷板 从2014 3月开始
downloadDailyBarBySymbol('SC99') #燃油 从2018开始
downloadDailyBarBySymbol('BU99')
downloadDailyBarBySymbol('RU99')
#大商所
downloadDailyBarBySymbol('C99')
downloadDailyBarBySymbol('CS99') #玉米淀粉 从2014年12月
downloadDailyBarBySymbol('A99')
downloadDailyBarBySymbol('B99')
downloadDailyBarBySymbol('M99')
downloadDailyBarBySymbol('Y99')
downloadDailyBarBySymbol('P99')
downloadDailyBarBySymbol('FB99')
downloadDailyBarBySymbol('BB99')
downloadDailyBarBySymbol('JD99')
downloadDailyBarBySymbol('L99')
downloadDailyBarBySymbol('V99')
downloadDailyBarBySymbol('PP99') #聚丙烯 从2014年2月
downloadDailyBarBySymbol('J99')
downloadDailyBarBySymbol('JM99')
downloadDailyBarBySymbol('I99')
#郑商所
downloadDailyBarBySymbol('TA99')
downloadDailyBarBySymbol('MA99') #甲醇 从2014年6月
downloadDailyBarBySymbol('FG99')
downloadDailyBarBySymbol('SF99')
downloadDailyBarBySymbol('SM99')
downloadDailyBarBySymbol('ZC99') #动力煤 从2015年
downloadDailyBarBySymbol('WH99')
downloadDailyBarBySymbol('PM99')
downloadDailyBarBySymbol('CF99')
downloadDailyBarBySymbol('SR99')
downloadDailyBarBySymbol('OI99')
downloadDailyBarBySymbol('RI99')
downloadDailyBarBySymbol('RS99')
downloadDailyBarBySymbol('RM99')
downloadDailyBarBySymbol('JR99')
downloadDailyBarBySymbol('LR99') #晚籼稻 从2014年7月
downloadDailyBarBySymbol('CY99') #棉花 从2017年
downloadDailyBarBySymbol('AP99') #苹果 从2017年
数据成功下载界面如图所示
RqData分3种数据,分别是以88或888结尾的代表主力连续合约数据和以99结尾的指数连续合约,下面以IF合约为例子进行详细说明。
(主力合约定义:合约首次上市时,以当日收盘同品种持仓量最大者作为从第二个交易日开始的主力合约。当同品种其他合约持仓量在收盘后超过当前主力合约1.1倍时,从第二个交易日开始进行主力合约的切换。日内不会进行主力合约的切换。)