写在前面
星期天又快结束了,好久没有一个周末的情绪如此跌宕起伏,从开始对于技术研究方面的期待,到目前对于Quicklib作者一些行为的懵逼.
这次直接合并发完金融民工对于Quicklib源代码的分析文章的3和4。
正文
作者:金融民工
原文3
今天继续带大家一起来看看神奇的quicklib。来看看它的交易部分。
vs打开之后是这样。
从目录结构上看,交易部分就是包装CTP,外加一个gzip(鬼知道gzip有没有用到?), 在加一个共享内存(鬼知道有没有用到)。
习惯性的grep一下,咦似乎也没什么地方用到gzip啊....然后又grep了一下,发现也没什么地方用共享内存啊......无伤大雅我们继续。
交易这边的套路和行情那边是一致的,CTPTradeSpi对应的头文件和源文件对应CThostFtdcTraderSpi的派生类QLCTPTraderSpi的声明和实现。QLCTPInterface.h 和QLCTPTradeAgenter.cpp对应把各种函数包装一次然后提供给Python那边调用。
这一篇具体讲代码之前先来设定几个阅读的小目标:
Python怎么调用C++的代码?CTP里面C++的结构体是怎么暴露给Python这边用的?
Quicklib对外宣传那么屌, 有什么黑科技吗?
这玩意能上实盘使用吗?好了正式开始,先看看quicklib里面是怎么样用Python调用C++的。在此之前我们得现有一些前置知识,Python可以使用ctypes很方便的调用C的动态链接库,举个例子:
//add.c
int add(int a, int b)
{
return a+b;
}
compile成动态库,以gcc为例:
gcc -o libadd.so -shared -fPIC add.c
如果是用g++来编译,那么add.c里面需要用extern "C"包起来,类似这样,至于为什么需要用这个extern "C",问题太基础,就不在这里浪费唇舌了,有兴趣可以看看这里。
extern "C"
{
int add(int a, int b)
{
return a+b;
}
}
然后python里面通过ctypes来调用:
import ctypes
lib = ctypes.cdll.LoadLibrary("./libadd.so")
print lib.add(1, 2)
好了,这是调用C的那么调用C++呢? 很简单啊,再给包一层C函数,来个例子:
class Test
{
public:
int add(int a, int b)
{
return a+b;
}
}
extern "C"
{
int add(int a, int b)
{
Test t;
return t.add(a, b);
}
}
g++来编译一下:
g++ -o libtest.so -shared -fPIC test.cpp
Python这边同样是使用ctypes来加载就可以用了:
import ctypes
lib = ctypes.cdll.LoadLibrary("./libtest.so")
print lib.add(1, 2)
一切都很简单对吧,没什么黑科技,quicklib里面也是这么做的,只是写起来就丑了很多。
第二个问题,CTP里面C++的struct,怎么传递给Python里面使用呢,还是用的ctypes啊。可以看它的CTPTradeType.py这里,没什么黑科技。那么Quicklib使用这一种方式来让Python调用C++存在哪些槽点呢? 来理一下从C++到Python都经历了哪些内容:
CTP代码是C++的,为了让ctpyes调用,必须把类的成员函数都重新用C包了一层,变成一个个普通的c函数。因为load动态库都是一个一个函数,所以在Python这边又把这一个一个函数拼成一个类(代码在CTPTrader.py这里);
听起来感觉没有哪里不对是吧,但实际上这里是又非常多的重复性工作。如果quicklib的作者懂boost::python,懂pybind11这样的东西的话,我相信代码不会是这样,作为一个自称写了十多年C++的所谓"资深"程序员,不懂boost感觉怎么都说不过去呐......传闻中一年经验用十年,大抵便是如此吧。
第三个问题,我觉得不应该在回答这个问题了......
第四个问题,号称多家机构在用啊,不知道是这些机构蠢呢,还是作者坏呢。test都没有的玩意(我尽力找了,一段类似test之类的代码都没有,有人找到了的话能告诉我吗......),真有人敢拿来实盘?当真金白银不是钱啊......
这几个问题之后想必对quicklib应该有一个比较初步的了解了,了解了大概的轮廓就可以详细的看代码了。先来看C++派生CTPCThostFtdcTraderSpi的这部分。QLCTPTraderSpi这个类里面,包含两方面的内容:
override CThostFtdcTraderSpi的几个virtual函数, 这几个函数都是回调函数(OnRsp*, OnRtn*)调用CThostFtdcTraderApi的几个对外请求的函数
正准备讲下单函数, 咦貌似没有定义:
好吧,到对应的源文件搜索reqorderinsert也没搜到,下单都没有啊......还交易框架呢,没关系看看其他的,来看一下撤单的逻辑(DeleteOrder):
int QLCTPTraderSpi::DeleteOrder(char *InstrumentID, DWORD orderRef)
{
//错误返回-1是和正常冲突的吗?
if (!InstrumentID)
{
return -1;
}
std::cout << __FUNCTION__ << std::endl;
CThostFtdcInputOrderActionField ReqDel;
::ZeroMemory(&ReqDel, sizeof(ReqDel));
strcpy(ReqDel.BrokerID, gBrokerID.c_str());
strcpy(ReqDel.InvestorID, gUserID.c_str());
strcpy(ReqDel.InstrumentID, InstrumentID);
::sprintf(ReqDel.OrderRef, "2d", orderRef);
ReqDel.FrontID = FRONT_ID;
ReqDel.SessionID = SESSION_ID;
ReqDel.ActionFlag = THOST_FTDC_AF_Delete;
int iResult = mpUserApi->ReqOrderAction(&ReqDel, ++(iRequestID));
if (iResult != 0)
cerr << "Failer: 撤单 : " << ((iResult == 0) ? "成功" : "失败(") << iResult << ")" << endl;
else
cerr << "Scuess: 撤单 : 成功" << endl;
return iResult;
}
之前就吐槽过这种随意的,随处可见的cout,cerr,就这样的代码你吹高性能,不知道那种对此深信不疑的萌新到底有没有最基本的辨识能力。
槽点太多, 挑个重点讲。iRequestID这个是个int,请自行补脑一下,以下语句多线程下会出现什么:
int iResult = mpUserApi->ReqOrderAction(&ReqDel, ++(iRequestID));
作者基础还是很欠缺火候啊...
再来看看查询持仓,OnRspQryInvestorPosition这里。代码太长就不全贴出来,入眼第一眼就有问题啊,这个函数第二个参数CThostFtdcRspInfoField *pRspInfo直接就略过不用了,然而作者不知道即便是返回错误信息的时候pInvestorPosition也有可能不为nullptr吗。这个地方得加一段:
if(pRsoInfo != nullptr && pRsoInfo.ErrorID != 0) return;
然后下面的具体查询仓位的代码,粗略看一遍就发现,这里根本没有考虑不同交易所对昨仓的处理。这是一个很严重的问题啊,请自行补脑一下,当自己的程序化交易软件认为的仓位和实际的仓位不符的时候,是多麽容易照成交易事故的。
这又暴露了另外一个问题,作者对业务逻辑了解得也很差啊...... 技术不行,业务也不行,这又的产物有人敢实盘,单就这份勇气,也是值得赞赏的。
看点简单的好了,来看看OnRspUserLogin,用户login后产生的回调:
CThostFtdcRspUserLoginField tn;
memset(&tn, 0, sizeof(CThostFtdcRspUserLoginField));
咦,这里用memset,之前DeleteOrder的时候用的是ZeroMemory呢:
CThostFtdcInputOrderActionField ReqDel;
::ZeroMemory(&ReqDel, sizeof(ReqDel));
突然让我回忆起以前读中学的时候,英文老师总说多用高级词汇不要整天but,but,but要用however!
然后下面的:
EnterCriticalSection(&g_csdata);
loginlist.push_back(tn);
LeaveCriticalSection(&g_csdata);
SetEvent(hEvent[EID_OnRspUserLogin_Scuess]);
lock一下,再弄个信号,这部分在讲行情的时候讲过了。再接下来:
ReqSettlementInfoConfirm();
Sleep(3000);
这个Sleep(3000)是何意...不是说好了和nodejs一样全异步吗
再来看看init函数, 其中注册柜台前置
mpUserApi->RegisterFront((char *)gTDFrontAddr[0].c_str());
mpUserApi->RegisterFront((char *)gTDFrontAddr[1].c_str());
mpUserApi->RegisterFront((char *)gTDFrontAddr[2].c_str());
擦这个gTDFrontAddr不能是一个set吗, 然后for一下, 非要这样写.
再来看看OnRtnOrder:
void QLCTPTraderSpi::OnRtnOrder(CThostFtdcOrderField *pOrder)
{
if (!pOrder)
{
return;
}
std::cout << __FUNCTION__ << std::endl;
int orderRef = ::atoi(pOrder->OrderRef);
::WaitForSingleObject(ghTradedVolMutex, INFINITE);
gOrderRef2TradedVol[orderRef] = pOrder->VolumeTraded;
::ReleaseMutex(ghTradedVolMutex);
CThostFtdcOrderField tn;
memset(&tn, 0, sizeof(CThostFtdcOrderField));
memcpy_s(&tn, sizeof(tn), pOrder, sizeof(CThostFtdcOrderField));
EnterCriticalSection(&g_csdata);
orderlist.push_back(tn);
LeaveCriticalSection(&g_csdata);
SetEvent(hEvent[EID_OnRspOrder]);
}
gOrderRef2TradedVol[orderRef] = pOrder->VolumeTraded; 这个地方看着就有点奇怪,点到定义发现是这样的:
std::map<
int, int> gOrderRef2TradedVol;
使用到的地方主要在这里QryTradedVol这个函数:
int QryTradedVol(int OrderRef)
{
int ret = -1;
::WaitForSingleObject(ghTradedVolMutex, INFINITE);
if (gOrderRef2TradedVol.find(OrderRef) != gOrderRef2TradedVol.end())
{
ret = gOrderRef2TradedVol[OrderRef];
}
::ReleaseMutex(ghTradedVolMutex);
return ret;
}
看逻辑是根据orderRef查询成交数量的。那么这么写有什么问题呢? 问题大大的有,我们下一个单的数量可能是大于1的,而大于1的时候,是又可能出现部分成交的情况,而这里的代码丝毫没有考虑部分成交的情况的处理,这里只是处理了上一次成交,这有事一个会使程序认为的仓位和实际仓位不匹配的情况。
哎......我已经无力吐槽了。
这代码问题层出不穷,漏洞百出,真是看不下去了,今天先到这里吧,太恶心了.....
原文4
出门运动了一圈,缓了一下,决定还是继续写......讲代码之前说一些无关技术的东西吧。
也许有人想问,这个源码分析系列的写作动机是什么?是为了黑同行露个脸蹭热度吗?
不不,一切只为了心中那股浩然正气,金融圈子到处充斥着各类骗子,私以为出了传销之外,骗子最多的行业非金融行业莫属,然作为一个有正气的社会主义接班人,实在没有办法眼睁睁的看着这种骗子到处坑蒙拐骗。
虽然厌恨骗子,但是为了尊重个人隐私,QQ已经涂抹掉了。
做人就不能脚踏实地吗......去年赚300亿啊,比进去的徐总还屌呢。编笑话能认真严肃一点吗? 类似的言论挺多的,就不一一列举了。现在九年义务教育都普及了,为什么还那么多毫无辨识能力的脑残粉啊......
另外有人私信我说:"你屌为什么你不fork帮人家改改? 为什么只嘴炮不做实事?"
"你屌为什么自己不开源个东西? "
这类问题我原本是拒绝回答的,结果这样问的人还挺多,就在这里统一回答一下。
不是太明白,为什么要帮个骗子改代码呢,帮他我岂不是共犯? 帮凶? 助纣为虐? 我不理解问这种问题的人到底是怎么想的,所以无论如何,鄙人都不会帮这类骗子改代码的,提这种问题的人也是搞笑!
而对于自己为什么不开源个东西,保密协议在身,是不会有什么金融相关的开源项目的,大家也别期待。
好了继续来撸代码,C++部分看不下去了,换个口味看看Python部分好了。
同样的, 开始之前先设几个小目标:
对外宣传的低cpu占用是什么原理?低cpu占用就是所谓的高性能吗?
这同学经常吹说quicklib订阅多少多少个行情才占用百分之多少的cpu,我们来一探究竟代码里面是如何实现的,期货行情/Release/QuickLibDemo.py这里。
直接看main函数好了,删除了注释后,剩余代码如下:
def main():
#除了从配置文件读取注册服务器地址外,还可用RegisterFront添加注册多个行情服务器IP地址。稍后给出不读配置文件,只使用RegisterFront注册行情服务器地址的例子
if False:
#已从配置文件读取3个服务器地址,这里可以不添加注册了
market.RegisterFront(u"tcp://180.168.146.187:10031") #添加注册行情服务器地址1
market.RegisterFront(u"tcp://180.168.146.187:10031") #添加注册行情服务器地址2
market.RegisterFront(u"tcp://180.168.146.187:10031") #添加注册行情服务器地址3
if True:
#订阅品种zn1610,接收Tick数据,不根据Tick生成其他周期价格数据,但可根据AddPeriod函数添加周期价格数据的设置
market.Subcribe('zn1705')
market.Subcribe('ag1706')
else:
#配置文件订阅函数下个版本修复,本例暂时采用Subcribe方法订阅
market.ReadInstrumentIni()
#给au1612添加根据tick生成的周期,尽量避免添加不适用的周期数据,可以降低CPU和内存占用
market.AddPeriod('ag1706',YT_M1) #添加M3周期(未在Subcribe、Subcribe1~Subcribe8函数中指定的周期,可以在本函数补充该品种周期,可多次调用函数设置同时保存多个周期)
market.AddPeriod('ag1706',YT_M3) #添加M3周期(未在Subcribe、Subcribe1~Subcribe8函数中指定的周期,可以在本函数补充该品种周期,可多次调用函数设置同时保存多个周期)
print ('number:', market.InstrumentNum)
#字典保存各个合约tick计数,用于判断多少次tick计算1次策略,默认为0,从0开始计数
perdealdict={"zn1701":0,"cu1701":0,"rb1706":0}
while(1): #死循环
#打印自动生成的1分钟周期的数据
print u'最近5个1分钟周期值'
print u'-----------begin--------------'
#打印该品种显示3分钟周期最近5个周期的收盘价格,若价格>0表示已接收到数据,否则还未接收到足够的Tick生成周期K线的数据
for i in range(0, 5):
tempclose=market.GetPeriodData('ag1706',YT_M1,YT_CLOSE,i)
if tempclose>0:
print tempclose
print u'-----------end----------------'
time.sleep(5)
其他内容先别看了cpu占用之谜在于time.sleep(5)这一句,这里是sleep 5s啊......什么都不做5s啊,500ms一次tick数据,这其中你错过了10次啊。cpu占用低说明cpu没事干,这一点从来就不是评判性能高低的凭据。我们来搜索一下sleep有多少地方用呢?
基本上所有地方都有在用呢,就问你怕不怕.。
那么cpu占用率低就是所谓的高性能吗?不! 这从来就不是一个充要条件,倘若你的处理逻辑时耗足够低,调用次数不频繁的时候,那么是可以看到较低的cpu占用的,而quicklib这里用sleep是几个意思呢?sleep后thread挂起,这期间什么都干不了,而时间再流逝,行情在发过来,你遗漏掉了那么多行情没有处理,却在鼓吹高性能? 妈的真是又蠢又坏啊......
再来看看期货行情的example/读两种下单交易例子/QuickLibDemo.py这里的main.py:
def main():
retLogin = trader.Login() #调用交易接口元素,通过 “ 接口变量.元素(接口类内部定义的方法或变量) ” 形式调用
if retLogin==0:
print u'登陆交易成功'
else:
print u'登陆交易失败'
#读取合约订阅的配置文件
market.ReadInstrumentIni()
#设置拒绝接收行情服务器数据的时间,有时候(特别是模拟盘)在早晨6-8点会发送前一天的行情数据,若不拒收的话,会导致历史数据错误,本方法最多可以设置4个时间段进行拒收数据
market.SetRejectdataTime(0.0400, 0.0840, 0.1530, 0.2030, NULL, NULL, NULL, NULL)
print ('number:', market.InstrumentNum)
while(1): #死循环,反复执行
print(u"Wait for a New Cmd(MD)\n")
#判断是否有新Tick数据,while循环不需要Sleep,当没有新Tick时,会处在阻塞状态
mddict[market.OnCmd()]()
print(u"Get A New cmd(MD)\n")
# 品种代码 多空方向 开仓还是平仓 市价或现价 价格 下单数量
#下单函数原型 InsertOrder(self, instrumentID, direction, offsetFlag, priceType, price, num):
OrderRef = trader.InsertOrder('zn1705', QL_D_Buy, QL_OF_Open, QL_OPT_LimitPrice, market.LastPrice('zn1705')+10, 1)
#交易记录写日志文件
#market.LogFile(u'traderecord.csv',u'交易下单 zn1705 QL_D_Buy')
time.sleep(1)
#撤销所有未成交的委托单
ret = trader.DeleteOrder('zn1705', OrderRef)
OrderRef = trader.InsertOrderByRate('zn1705', QL_D_Buy, QL_OF_Open, QL_OPT_LimitPrice, market.LastPrice('zn1705')+10, 0.25,QL_Dynamic_Capital,5)
#交易记录写日志文件
#market.LogFile(u'traderecord.csv',u'交易下单 zn1705 QL_D_Buy')
time.sleep(1)
#撤销所有未成交的委托单
ret = trader.DeleteOrder('zn1705', OrderRef)
这里有一个挺有趣的设置时间的函数:
market.SetRejectdataTime(0.0400, 0.0840, 0.1530, 0.2030, NULL, NULL, NULL, NULL)
真棒,用double来表示时间......真是天马行空的设计啊。
这一段代码存在的问题,其他示例里面都存在,比如没有个正经的timer,只会一股脑sleep。到处sleep的代码同步的log,到处都有的std::cout, prinf, std::cerr竟然对外宣传说"最大的特点就是采用异步式 I/O 与事件驱动的架构设计"...真是挺可怕的,你当std::cout这些就不是io了吗? 为什么人能够厚颜无耻到这般地步, 又蠢又坏啊......
挣扎了一下, 还是没有办法继续看下去......
写在后面
看到这里,我首先必须对金融民工表示一下感谢:这种质量的代码都看完了,还写了非常全面的4篇分析文章,要是我自己全看完估计得吐了,这种毅力不服不行!
之前虽然对Quicklib作者的行为感觉挺Low,但他一直信誓旦旦坚称Quicklib的性能显著超过vn.py的自信,让我一度以为人家只是一位性格上比较怪的“技术大神”,希望自己可以通过一个系列的分析文章能取到点经,也来改进下vn.py。
不过到现在我基本已经绝望了,浪费了包括一个周末在内前后4天的时间,可能得不到什么有价值的技术改进。从民工的分析中可以很明显看出Quicklib作者的两个情况:
1. 没有实盘交易经验,程序内大量的代码存在交易业务逻辑上的错误,这还是相对比较简单的CTP期货接口而已;
2. 没有写过生产环境中的量化交易系统,代码逻辑稳健性很低,最多是业余爱好者的范畴,弄了个map来根据委托号查询成交数量的逻辑我真是醉了。
同时这几天社区用户和量化圈内的一些朋友,也通过各种渠道告诉了我Quicklib作者以前的一些黑历史,结合自己这几天见识到的他的行为,基本也有了个数。本着专业性的原则就不在我的专栏里说了,有兴趣的朋友可以在这个问题下的回答里看到一些情况:如何评价最近vn.py作者和Quicklib作者关于Python量化交易系统在架构设计方面的争论?
之前已经有不少人提醒我Quicklib作者搞的整个事情是想蹭热度给自己增加曝光度,我开始还觉得不至于。vn.py项目从一开始的时候我就已经表达过会永久保持开源做下去,现在项目的成熟度我觉得也就算上路而已,谈不上有什么成就。但是Quicklib的作者这几天在我的专栏文章和问题回答下面,发一些高度重复的评论,今天已经让人无语到开始直接复制粘贴刷屏,我还真是不得不信了。也懒得在这件事情上再浪费时间,决定直接加黑了事。
苏州麻将这款游戏的游戏规则其实还是非常简单的,而且在游戏中,还有28支花,这款游戏在玩的过程中是可以带花腔的博彩游戏,而且玩家可以凭借自己的本事和经验“炸胡”,但是这并不是游戏中所提倡的,所以说,如果玩家在游戏中遇到了这样的情况,是可以举报的,那位炸胡的玩家就会给其他的三位玩家进行一定的赔偿的,不过,至于赔偿多少的积分,这要根据各自的玩家进行决定的。