1.云服务器上安装近十GB的MATLAB过于麻烦,而Python的Anaconda发布版只有几百MB
2.Python开源免费,而正版MATLAB则价格不菲。
3.国内与Quantopian类似的在线策略编写网站开始流行,Python是它们的策略语言,建立了良性的互动交流社区,有大量的案例可参考。
4.Python有大量用于科学计算、统计分析、机器学习的开源工具库,适合用于做量化交易。
因此,已经有越来越多的用户向Python迁移。我们也紧跟趋势,在推出MATLAB版XAPI统一行情交易接口后也推出了Python版。在总结了社区中常遇到的问题后,在本文将进行专门的解答。
综合交易平台CTP(ComprehensiveTransactionPlatform)是由上海期货信息技术有限公司(上海期货交易所旗下子公司)开发的经纪业务管理系统。
目前除了在期货市场占有率第一,它还有证券版支持股票,国际版支持外盘。
期货市场占有率第二的老牌柜台系统,由于开放性不够,被CTP从第一的位置挤下,目前因历史遗留还有部分期货公司在使用。
金仕达所接的市场很多,期货、个股期权、证券,以及贵金属现货。
金仕达证券接口还是老式的不支持主动推送的接口,所以在ETF期权上市时,在期货公司推扩证券柜台也不顺利。而在贵金属现货接口上又推广不力,现已被飞鼠占去先机。
分别由中国金融期货信息技术有限公司(是中国金融期货交易所期下子公司)、大连飞创信息技术有限公司(大连商品交易所旗下子公司)、易盛信息技术有限公司(郑州商品交易所期下子公司)。
在2015年股灾之前,程序化交易监管较弱,高频交易火爆,如果使用每家专用的API,期货公司可以提供离交易所机柜更近的主机托管业务。但现在高频交易策略无法施展的情况下,用流行的CTP更省事。
他们的公司网址分别为:
恒生电子同时提供了证券、期货经纪业务解决方案的提供商,在机构用户中占有率极高。机构通过UFX接入到恒生O32系统中进行事前风控。
因为金融产品太多、业务复杂,导致字段也多。UFX更多的是类似于通讯协议数据包打包器。用户在开发时需要对照数据字典一个个自己组织请求包,使用起来比较麻烦。
基本上,所有官方提供的接口都是C++接口,少量老的证券接口还提供C接口。C++接口导出了C++类,而其它语言中的类都是自己语言中经过特殊设计的,无法直接利用C++类。因此必须将C++接口转换成简单的C接口,将原接口函数中隐式的this指针改成显式的void*指针传入,这样才能在其它语言中进行调用。
还需要解决结构体的传入、传出与回调函数的实现,然后再进一步简单的用各种语言封装调用时的请求与响应,使用起来就方便了。
目前市面上主要的开源Python封装有vn.py与XAPI两款,其它的Python开源项目,已经不再维护或用户量极少,所以这就不再介绍了,有兴趣的朋友请网上搜索。
由于vn.py在官网已经有齐全的文档,本人对它也没有更深入的研究,所以在这只介绍自己开源的XAPI。
一开始是为了对接OpenQuant3这款软件的个人项目,参考了海风开源的CTPC#版接口,不同的地方是将一些复杂的逻辑由C#层移动到C层,简化上层的开发。
为了推广OpenQuant3,所以决定将代码整理转成开源项目。由于Femas和XSpeed的推出,当时参考了CTP的封装方法,封装了其它几个接口和OpenQuant3的插件。但这种老式的封装方法工作量大,C#层和应用层有多少种接口就需要封装多少套,难于维护。
2015年同样是海风推出的新版封装里有动态延迟加载dll的示例,学习研究后,决定对接口进行重新设计,统一不同API的结构体为同一套,命名为XAPI。
后来根据网友的建议,对项目的命名空间、目录结构重新调整,升级为XAPI2。
专门设计的C接口,所以各种语言版本的封装陆续推出,.NET、Java、MATLAB、COM、Python。
1.2.4XAPI设计思想:
1.能接入主要的接口。根据流行程序来考虑是否优先接入;
2.满足基本的交易功能。抛弃了不常用的功能,如银期转账功能;
3.能支持不同的语言。一定要导出C接口;
4.简化上层代码的开发。将流控,请求ID,请求发送队列等各项基本功能都封装在C层。
XAPI的核心就是队列。XAPI的队列数据格式与只有一个XRequest导出函数的dll风格完全一样。一个字节大小的数据包类型,两个指针,两个双精度数字,三个内存区指针和三个内存区大小数值,在目前的行情交易接口开发中基本够用。
上层的函数调用被封装成数据包,添加到请求队列中,另一线程从请求队列中取内容,然后调用API的各种函数,用这种方法来解决流控,请求ID递增等问题。
收到的响应也复制打包好后放入响应队列,另一线程从响应队列中取内容,调用注册好的回调函数通知到上层,解决应用层策略耗时过久可能导致底层崩溃的问题。
由于大部分接口只推出了32位版,而在Windows中同一进程中32位与64位不能相互调用,所以Python也只能选择32位的进行安装。目前CTP接口有64位版,有兴趣的朋友可以自行编译XAPI项目为64位版。
Python2.7不再推荐,推荐使用3.6及以上版本,做量化交易要用到的库基本都已经支持Python3。
下载比同步repo的方式要快,对于没有能力贡献代码的朋友来说,zip方式最快速直接。
“缺少依赖库”是很多用户遇到最基础最常见的问题,本项目为了接入不同的API,每套API都要做一套C封装,这些封装都需要C++运行时库,为了减少发布包的大小,运行时库使用的动态编译,这样C++运行时库就能共享一套,它们默认放在了C:\Windows\System32或C:\Windows\SysWOW64\。这就导致在一台电脑上能正常使用,复制到另一台电脑上由于忘记复制C++运行时库就不能用了。
其实还有XAPI下还有一个队列库Queue_x86.dll/Queue_x86d.dll也是每个C封装都需要用到,并且它还有Debug与Release版本,由于C++中的new/delete必须与C++运行时库Debug/Release对应的问题,所以这两个版本不能混用。
同时也需要注意32位与64位的问题,很多同名文件是是同时存在32位与64位的区别的,32位版本放C:\Windows\SysWOW64\下,64位版本放C:\Windows\System32下。
图1.depends查看dll示例
图中演示的是用depends查看CTP_Trade_x86.dll,由于缺少Queue_x86d.dll,Python在调用它时会提示“找不到指定的模块”。CTP_Trade_x86.dll是32位的dll,图中显示的c:\windows\system32路径实际上是访问的c:\windows\syswow64。将Queue_x86d.dll文件复制到C:\Windows\SysWOW64下即可。
对这些细节不想了解的用户直接运行群共享提供的安装包(统一接口完整版.zip)中的“X1.复制bin和System32目录_需右键以管理员身份运行.bat”就可以省去以上所有麻烦。
默认提供了test_ctp_api.py/test_tdx_api.py两个完整的脚本用来测试,它们分别实现了期货和股票的目标调仓功能,只要在文件中设置好每个金融产品的目标持仓、多空方向和数量,就能以最快的速度调整成指定的仓位。
对于每天交易频率不高,使用日线数据计算策略,然后第二天早上进行交易的机构用户来说,这两个脚本完全可以直接使用。
目录下出现了config.py、config_default.py、config_override.py和config_tdx.py四个以config开头的文件。
config_default.py/config_override.py:分别是CTP的默认配置和CTP的特殊配置,为了实现特殊配置覆盖默认配置,config.py中实现了覆盖重复字段的功能。
config_tdx.py中没有CTP配置中那么复杂,直接就是配置信息。
#!/usr/bin/envpython
#-*-coding:utf-8-*-
configs={
#根据目录,存放交易清单和中间文件等信息
'root_dir':r'd:'+'\\test_tdx',
#交易,这下面的配置要求需要参考每种API的说明文档
'td':{
#TDX安装目录,注意最后有一个\
'ExtInfoChar128':br'D:\new_hbzq'+b'\\',
'Address':r'd:\test_tdx\Login_东方财富证券.lua'.encode('GBK'),
#资金账号
'UserID':b'123456',
#用户密码
'Password':b'654321',
#通迅密码。注意,不是验证码,不需要通迅密码的券商请留空
'ExtInfoChar64':br'',
},
}
注意配置文件中很多字符串出现了b前缀。这是因为XAPI的结构体对应字段就是C中的字数数组,b就是原始的bytes,我们将字符串直接传入。中文路径不能直接使用b前缀,需要先encode转换成bytes后才能使用。
前面在config_default.py中已经配置了root_dir文件路径为d:\test_ctp。编辑其中的target_position.csv文件,下单时将从这它读取目标仓位。
Symbol,InstrumentID,HedgeFlag,Side,Position,InstrumentName
i1709.,i1709,0,1,5.0,
i1709.,i1709,0,-1,0.0,
rb1710.,rb1710,0,-1,6.0,
Symbol:合约唯一代码,XAPI内部使用。由InstruemntID与ExchangeID组合而成;
InstrumentID:合约代码,供API使用,必须与对应API的合约代码完全一样,如果对应API区分大小写,这里也得区分;
HedgeFlag:投机套保标志,默认为0,这个功能一般是机构使用,机构可能出现同时持有投机仓位与套保仓位的情况;
Side:多空方向,1表示多,-1表示空;
Position:持仓数量,正数;
InstrumentName:合约名称。只用于显示,可为空,股票中表示股票名称。
000001,000001,0,1,400.0,平安银行
000002,000002,0,1,200.0,万科A
300001,300001,0,1,300.0,特锐德
300024,300024,0,1,200.0,机器人
对于前面提到的csv文件,如何清仓、反手、锁仓呢?
清仓:即没有项目,或对应的Position为0即可
反手:只要改对应项目的Side的正负号即可
i1709.,i1709,0,-1,5.0,
rb1710.,rb1710,0,1,6.0,
锁仓:同合约多空持仓数量相等即可
直接运行会打印一个选择菜单,然后等待用户输入。菜单的主要内容如下:
1-读取目标仓位
2-查询实盘仓位
3-订阅行情
4-计算交易清单
5-批量下单
6-需延迟通过回报批量撤单
它就是目标仓位调整的主要流程:
1-首先从本地的target_position.csv读取设置的目标仓位;
2-从实盘柜台上查询当前的持仓;
3-根据目标仓位和实盘持仓合并得到要下单的合约集合,订阅合约的行情,后面下单时需要用到最新的行情价格;
4-对比目标仓位和实盘持仓,得到交易清单,这个清单已经处理好了买卖方向与开平方向等问题,对于上期所的的今仓与昨仓分两笔下单;
5-根据上一步生成的交易清单直接下单;
6-下单后并不是瞬间成交,还需要等待几秒,然后撤单。
人工循环执行2到6步,直到交易清单为空。注意有些情况下可能成交失败,交易清单永远不为空。例如:
1.资金不够
2.涨跌停,买入或卖出无法成交
3.进入交割月了,交易手限制为整数倍,而下单手数不合要求
4.股票停牌
有时输入这些数字也可能输入错误,我们提供了一个更简化的选项:
7-顺序执行1-6
只要输入7,就会自动执行1至6三次,直到交易清单为空或出错。
21-查合约列表(至少执行一次)
22-查资金
23-取消订阅行情
24-打印订单
33–切换行情显示
q-退出
21-从柜台上查询合约列表,并保存在本地。它保存了每个合约的最小变动价位,用于计算买卖时加几跳时具体加的是多少价格。如果不查询默认最小变动价位为1。对于某些合约使用默认1将产生错误。
对于cu铜,最小变动价位是10,如果使用默认1,加2跳,价格将不满足最小变动价格整数倍的条件。
对于IF沪深300,最小变动价位是0.2,如果使用默认1,加2跳,价格实际上加了10跳。
所以这个地方至少要执行一次,每次有新品种上市时也得查一次。
22-查账号资金
33–切换行情显示。对于CTP的主推行情,由于行情一直在界面中打印,干扰使用,所以提供了一个开关进行切换。
q–人工输入模式会一直等待用户输入,输入‘q’可以退出
11-合并对冲多个组合到目标持仓
12-回写查询持仓到目标持仓
13-合并对冲目标持仓和增量仓到目标持仓
是否觉得要自己手工编写target_position.csv很麻烦呢?只要运行菜单2后再运行12就会将前一步查询出来的持仓写入到target_position.csv,然后再手工编辑少量即可。
一个实盘账号下跑了N个策略,分别生成了不同的持仓文件,能否先内部对冲一下?我们提供了11这个菜单项,它能将portfolio_1.csv/portfolio_2.csv/portfolio_3.csv三个文件中的持仓合并,对冲,然后写入到target_position.csv中。
如果投资组合数超过3个,请自己手工编辑代码支持更多组合。
每天的投资组合清单都已经生成,但我盘中想改总持仓怎么办?这个改动是算在哪个策略对应的投资组合呢?建议根据策略数量N创建N+1个投资组合,第N+1个组合文件中内容为空,当需要人工调整时仓位都放在第N+1个投资组合中。
如果我的策略逻辑并不是根据持仓数来计算,而是根据买卖数量呢?例如今天买入2手,而不是今天仓位是2手。这时就要用到菜单项13了,它会将target_position.csv和incremental_position.csv的持仓合并,不对冲,然后再写回target_position.csv。
目前Tdx接口没有实现查询股票列表功能,所以菜单项21无效。股票无法卖空,所以target_position.csv中的Side都只能填1。
原本Tdx柜台就没有实现主动推送委托回报和成交回报的功能,只能过一会后去主动查询委托列表,所以股票接口只能等待一会后直接撤单,然后直接查持仓,不再关心委托回报这些细节。
test_ctp_api.py/test_tdx_api.py默认在运行时都是手工输入,其实还提供了一个参数“—input”。
“--input=11;7”表示先运行菜单项11,然后运行菜单项7。即先从几个子组合csv文件中合并持仓,然后循环下单,下完单后退出。
REM11合并持仓
REM22查资金
REM7循环下单
setdate_Ymd=%date:~0,4%%date:~5,2%%date:~8,2%
python.exetest_ctp_api.py--input=11;22;7;221>log/%date_Ymd%.log2>&1
如何自动保存日志呢?我们使用到了DOS重定向。
<>
>:新建模式输出到文件
>>:追加模式输出到文件
>&:将一个句柄输出写入到另一个句柄的输入中
0:标准输入,令在执行时所要的输入数据通过它来取得
1:标准输出,命令执行后的输出结果从该端口送出
2:标准错误,命令执行时的错误信息通过该端口送出
1>log/%date_Ymd%.log2>&1表示将标准输出指向log日志文件,然后将标准错误都输出到标准输出中。这样标准输出和标准错误就都输出到log文件了。如果将2>&1放到前面,则达不到效果,错误还是输出到了窗口中,因为这时标准输出还没有被重定向。
pythonmail.py--username=123456--password=654321--from=123456@qq.com
--to=123456@qq.com;654321@qq.com--log=log--bat=%~f0
--username=用户名
--from=发件箱。username账号所对应的发件箱
按Win键后,输入taskschd就会定位到“任务计划程序”。
1.创建基本任务
3.操作选择“启动程序”,“程序或脚本”填为bat文件路径,“起始于”填为bat文件所在路径。由于bat和python代码中大量用到了相对路径,所以“起始于”这一项绝对不能为空。
前面介绍了基本的使用方法,对于很多用户来说就已经完全足够,但这里还是要对代码简单解读一下,方便遇到问题时自行处理。
XApi.py/XSpi.py/XStruct.py/XEnum.py是核心代码,使用ctypes模块实现Python调用C。
XEnum.py中定义了枚举类型,包含值以及对应的英文名。当在结构体中取到枚举数值时,通过这里的定义得到对应的英文名。
XStruct.py中定义了结构体,与C接口的结构体一一对应,同时还为结构体添加了一些函数,方便使用,如__str__等。
XSpi.py回调接口,提供给第三方继承使用。
MySpy.py继承了XSpi的类,目前用来进行实际目标持仓调整功能。
以CTP期货交易接口封装为例,CTP_Trade_x86.dll只导出了一个接口XRequest,需要对XRequest请求格式特别了解才能正确的调用,所以又在这之上套了一层XAPI_CPP_x86.dll,它导出了一些常用的C函数。接口封装人员可以按自己的能力选择合适自己的调用方式。
目前XApi.py中提供的方式是通过XAPI_CPP_x86.dll来调用CTP_Trade_x86.dll。
#创建XApi对象,设置服务器地址与账号
td=XApi(r'C:\ProgramFiles\SmartQuantLtd\OpenQuant2014\XAPI\x86\XAPI_CPP_x86.dll')
td.ServerInfo.Address=config['td']['Address']
td.ServerInfo.BrokerID=config['td']['BrokerID']
td.UserInfo.UserID=config['td']['UserID']
td.UserInfo.Password=config['td']['Password']
#指定加载的是CTP的交易模块,设置不同的路径可以加载其它模块
ret=td.init(br'C:\ProgramFiles\SmartQuantLtd\OpenQuant2014\XAPI\x86\CTP\CTP_Trade_x86.dll')
ifnotret:
print(td.get_last_error())
exit(-1)
print(ord(td.get_api_type()))
print(td.get_api_name())
print(td.get_api_version())
#关键一步,注册回调事件处理函数
td.register_spi(self)
td.connect()
register_spi()中需要传入继承了XSpi的类,在test_ctp_api.py中当前类MySpi继承了XSpi。
OnConnectionStatus=Initialized
OnConnectionStatus=Connecting
OnConnectionStatus=Connected
OnConnectionStatus=Logining
OnConnectionStatus=Logined
[TradingDay=20170905;LoginTime=205957;SessionID=1:1314163388;InvestorName=;XErrorID=0;RawErrorID=0;Text=]
OnConnectionStatus=Confirming
OnConnectionStatus=Confirmed
OnConnectionStatus=Done
OnConnectionStatus等一些事件响应函数中能下断点调试吗?实测在PyCharm中下断点,有输出日志,但断点完全不生效。
StackOverflow上的网友是这样解答的:在非Python线程中,你必须设置调试器机制才能正常工作(在Python线程创建时自动设置了,但在非Python线程创建时没有任何构造函数钩子,所以得自己做)。在你需要下断点的代码前加入如下代码即可。
importpydevd
pydevd.settrace(suspend=False,trace_only_current_thread=True)
你需要使用pipinstallpydevd先安装pydevd。
创建行情XAPI实例,对它设置行情接口库CTP_Quote_x86.dll,设置行情服务器的地址和端口号。不要弄混,对交易接口订阅行情是无效的。
ret=md.init(br'C:\ProgramFiles\SmartQuantLtd\OpenQuant2014\XAPI\x86\CTP\CTP_Quote_x86.dll')
订阅行情传入的合约代码也必须是有b前缀。
#可直接传字一个用;分隔的字符串
md.subscribe(b'cu1709;SR801',b'')
#或做一次转码再传入
symbols_=pd.Series(symbols).str.encode('gbk')
foriinrange(len(symbols_)):
md.subscribe(symbols_[i],b'')
会在OnRtnDepthMarketData(self,ptr1,size1)中收到行情回报,ptr1是行情数据指针,size1是行情数据大小。其它OnXxx事件,输出的参数都是Python对象,只有行情接口特殊输出的是内存指针。因为为了支持多档行情,行情结构体设计成了可变内存块。就算是股票的五档行情,在涨跌停时,这个内存块的大小都是不一样的。
那如何取数据呢?使用的ctypes的cast即可
obj=cast(ptr1,POINTER(DepthMarketDataNField)).contents
#打印行情,一般情况下都是关闭,因为内容太多了
print(obj)
#卖五价
ask_count=obj.get_ask_count()
ifask_count>0:
ask=obj.get_ask(ptr1,ask_count-1)
#买五价
bid_count=obj.get_bid_count()
ifbid_count>0:
bid=obj.get_bid(ptr1,bid_count-1)
对于CTP这种主动行情推送的接口,只要订阅了,行情有变化就会推送,OnRtnDepthMarketData会不停的被调用。而Tdx这种查询模式的接口,查一次推送一次,所以需要根据策略需求查询,不能高频率查询,否则严重影响服务器。
#查询持仓请求
query=ReqQueryField()
td.req_query(QueryType.ReqQryInvestorPosition,query)
#持仓响应
defOnRspQryInvestorPosition(self,pPosition,size1,bIsLast):
ifsize1<=>
return
#一定要用copy,不然最后一个会覆盖前面的
self.position_dict[pPosition.get_id()]=copy.copy(pPosition)
ifnotbIsLast:
当账号上没有持仓,但还是需要通知到客户端请求已经得到响应了,所以会传回一个空数据,所以需要对size1进行判断。
#查资金请求
td.req_query(QueryType.ReqQryTradingAccount,query)
#资金响应
defOnRspQryTradingAccount(self,pAccount,size1,bIsLast):
print(pAccount)
#提供临时变量用于下单
order=(OrderField*1)()
orderid=(OrderIDTypeField*1)()
orderid[0].OrderIDType=b''
#订单参数
order[0].InstrumentID=b'IF1710'
order[0].ExchangeID=b''
order[0].Type=OrderType.Limit
order[0].Side=OrderSide.Buy
order[0].Qty=1
order[0].OpenClose=OpenCloseType.Open
order[0].Price=3500.0
#下单
ret=td.send_order(order[0],orderid[0],1)
#打印
print('LocalID:%s'%ret)
注意,涉及到字符串的地方都需要b前缀。有关OrderType、OrderSide、OpenCloseType的取值,可以参考XEnum.py文件中对应部分。
下完单后回收到委托回报和成交回报,委托回报可以存储起来用于立即撤单。
#委托回报
defOnRtnOrder(self,pOrder):
self.order_dict[pOrder.get_id()]=copy.copy(pOrder)
print(pOrder)
#成交回报
defOnRtnTrade(self,pTrade):
print(pTrade)
OrderField中有三个字段跟识别订单有关:
OrderID是由交易所传回来的id。如果想比较不同柜台的速度,可以同时报单相同合约后比较OrderID的大小。
LocalID是本地XAPI在下单时立即返回的id,它只供内部临时映射时使用,Tdx这类的接口是下单返回后才能知道ID值,所以在柜台返回之前XAPI维护一个临时LocalID,等收到回报后就弃用LocalID。CTP接口中LocalID与ID是相等的,在Tdx中他们不相等
#临时变量
orderid=(OrderIDTypeField*2)()
orderid[1].OrderIDType=b''
#设置要撤单的ID
orderid[0].OrderIDType=order.ID
#撤单
td.cancel_order(orderid[0],orderid[1],1)
撤单ID最好从委托回报中取,而不是手工填,因为部分API的ID前后可能出现空格,如果缺失了空格会导致找不到订单。