前面两篇有了基础知识的准备,这一篇讲通过CTP API获取实时行情,录入csv,实时合成k线。github上开源了录入csv及合成k线代码,后台回复pyctp可获取。先上两张效果图:
图1 csv数据
图2 1分钟K线图
一、CTP行情API介绍
CTP API分为行情和交易两类,两者是完全独立的,所以如果只对行情感兴趣的话,只用如下行情API相关部分三个文件就可以。
thosttraderapi.py
_thostmduserapi.pyd
thostmduserapi_se.dll
API中的重点函数,请求:
//登录函数,确认连上CTP后首先需要登录
def ReqUserLogin(self, pReqUserLoginField: 'CThostFtdcReqUserLoginField', nRequestID: 'int') -> "int":
//订阅函数,即通过这个函数来向CTP请求订阅哪些合约的实时行情
//第一个参数类型为list,写成["au1912","IC1909"]的形式,第二个参数必须为前面list的长度
def SubscribeMarketData(self, ppInstrumentID: 'char *[]', nCount: 'int') -> "int":
回报:
//行情回报函数,其中pDepthMarketData类内即为每次实时行情的相关数据
def OnRtnDepthMarketData(self, pDepthMarketData: 'CThostFtdcDepthMarketDataField') -> "void":
那么究竟可以获取到合约的哪些行情数据呢?从类型CThostFtdcDepthMarketDataField 中的字段就可以看出来,在thosttraderapi.py文件中搜CThostFtdcDepthMarketDataField类型即可看出有哪些字段,主要有更新时间UpdateTime,最新成交价LastPrice ,买卖一档的价格及数量BidPrice1,BidVolume1,AskPrice1,AskVolume1,累计成交量Volume等。
二、订阅获取行情的步骤
代码非常简单,50行内即可订阅全市场行情。通过上一章的学习应该知道CTP的API是异步回调的机制,底层dll在客户订阅成功后会自动推送订阅合约的实时行情。代码逻辑时序图如下:
图3 订阅行情时序图
对应的主函数如下:
def main():mduserapi=mdapi.CThostFtdcMdApi_CreateFtdcMdApi() #第1步mduserspi=CFtdcMdSpi(mduserapi) #第2步'''以下是生产环境'''#mduserapi.RegisterFront("tcp://180.168.146.187:10101") #第3步'''以下是7*24小时环境'''mduserapi.RegisterFront("tcp://180.168.146.187:10131")mduserapi.RegisterSpi(mduserspi) #第4步mduserapi.Init() #第5步,API正式启动,dll底层会自动去连上面注册的地址mduserapi.Join() #join的目的是为了阻塞主线程,可以用sleep代替
回调实例类CFtdcMdSpi如下:
import thostmduserapi as mdapi
'''需要订阅的合约list'''
subID=["au1912","IC1909","i2001","TA001"]class CFtdcMdSpi(mdapi.CThostFtdcMdSpi): #继承自spi基类mdapi.CThostFtdcMdSpidef __init__(self,tapi):mdapi.CThostFtdcMdSpi.__init__(self)self.tapi=tapidef OnFrontConnected(self) -> "void":print ("OnFrontConnected")loginfield = mdapi.CThostFtdcReqUserLoginField()loginfield.BrokerID="8000"loginfield.UserID="000005"loginfield.Password="123456"loginfield.UserProductInfo="python dll"self.tapi.ReqUserLogin(loginfield,0) #实现onfrontconnect函数,在里面调用登录,第7步def OnRspUserLogin(self, pRspUserLogin: 'CThostFtdcRspUserLoginField', pRspInfo: 'CThostFtdcRspInfoField', nRequestID: 'int', bIsLast: 'bool') -> "void":print (f"OnRspUserLogin, SessionID={pRspUserLogin.SessionID},ErrorID={pRspInfo.ErrorID},ErrorMsg={pRspInfo.ErrorMsg}")#继承实现登录回调,登录成功后去订阅,第9步ret=self.tapi.SubscribeMarketData([id.encode('utf-8') for id in subID],len(subID)) def OnRtnDepthMarketData(self, pDepthMarketData: 'CThostFtdcDepthMarketDataField') -> "void":#继承收取订阅行情,第11步,在这里将pDepthMarketData数据存入csv即可录得数据print(f"InstrumentID={pDepthMarketData.InstrumentID},LastPrice={pDepthMarketData.LastPrice}")
看总共就30+行代码,就完成了订阅收取行情的工作。如果将subID列表中填入入全市场合约,就能订阅得到全市场的行情,是不是很简单?
四、由CTP API得到K线数据
首先需要区分下tick数据和切片(快照)数据有什么区别。
tick数据一般是指市场上的逐笔数据,例如一笔委托会产生一笔行情,一笔成交也会产生一笔行情。目前国内期货交易所还不支持推送这种逐笔的数据,只推送切片(快照)数据。
切片数据是指将一定时间内的逐笔数据统计成一个快照发出,一般是1秒2笔。但郑商所有点特殊,可能是1s多笔,就不展开来讲了。
CTP发出的行情正是转发的交易所的行情,所以也是500ms一次快照。一般业内也将这个快照数据称之为tick,虽然这不是真正的tick,但我们依照惯例,下面都称之为tick数据。
很多客户做交易更关心K线数据,用K线数据计算信号。CTP不推送K线数据,所以需要客户自己根据tick数据计算得出。
K线数据的基本要素有Time、Volume、Price、Open、High、Low、Close这6个值,可以根据这个周期内CTP的tick数据中的UpdateTime、 LastPrice、 Volume三个字段算出。我们以1分钟K线为例,逻辑如下:
#根据行情中的UpdateTime字段判断是否为新1分钟
st= pDepthMarketData.UpdateTime.split(':')
if not self.bar:newMinitue = True
else:if int(st[1]) == self.bar.updateTime.minute :newMinitue = Falseelse:newMinitue = True
#如果是新1分钟,生成一个新k线变量,CBarData结构体中有OHLC,time等K线字段
if newMinitue :self.bar = CBarData()self.bar.instrumentID = pDepthMarketData.InstrumentIDself.bar.exchangeID = pDepthMarketData.ExchangeIDself.bar.updateTime = time(int(st[0]),int(st[1]),0,0)self.bar.volume = 0self.bar.openInterest = pDepthMarketData.OpenInterestself.bar.openPrice = pDepthMarketData.LastPriceself.bar.highPrice = pDepthMarketData.LastPriceself.bar.lowPrice = pDepthMarketData.LastPriceself.bar.closePrice = pDepthMarketData.LastPrice
else :
#如果不是新1分钟,将最新价与HL价相比然后更新,更新C价 self.bar.highPrice = max(self.bar.highPrice, pDepthMarketData.LastPrice)self.bar.lowPrice = min(self.bar.lowPrice, pDepthMarketData.LastPrice)self.bar.closePrice = pDepthMarketData.LastPriceself.bar.openInterest = pDepthMarketData.OpenInterest
#注意Volume字段是累计成交量,所以这个时间段内成交量为该值与上一时间段末成交量的差值
if not self.lastVolume:self.bar.volume += max(pDepthMarketData.Volume-self.lastVolume,0)self.lastVolume = pDepthMarketData.Volume#打印实时k线数据 print(f"{bar update[pDepthMarketData.UpdateTime],O[self.bar.openPrice],H[self.bar.highPrice],L[self.bar.lowPrice],C[self.bar.closePrice]}")
有这一段代码加入到上面的OnRtnDepthMarketData函数中,就能获得1分钟K线数据了。其余的3、5、10、15、30分钟这类的K线数据获取方式原理也相似。
当然要得到令自己满意的k线数据还是有很多坑要自己踩过才知道,每个人对K线的要求也不一样,这里提几点思考,就不一一列举解答了。
- 根据最新价LastPrice更新得到的highPrice一定是真的最高价吗?
- 上下午收盘分别是11:30和15:00,那收到11:30:00.500和15:30:00.500ms的行情如何处理?
- 非主力合约有的很长时间才来一个tick,如何处理?
建议大家可以一边做一边对应快期等终端对比,得到自己满意的k线数据。
一般有k线数据就可以直接计算指标得到信号量便于交易,为了更直观地看到K线,这里也提供下PyQt + PyQtGraph实现的K线图,源码一样提供在github上。
代码参考了github上uiKLine和vnpy两个开源项目,大家可以看我github上fork的这两个项目。感谢两位作者!
下节预告:
CTP API获取行情常见问题及解答
关注公众号,一起学习程序化交易!