【核心技术】量化系统之极速交易通道的实现逻辑
Kevin/核心开发
依据国内监管要求,投资者无法直连入交易所,中间必须经过证券公司的系统,即柜台系统。交易柜台是连接交易所的下单系统,通过经纪公司交易柜台将交易指令发送到交易所,然后经纪公司交易柜台再将交易所委托回报和成交回报反馈给投资者。
柜台系统有以下特点:
那么问题来了,咱们如何才能在量化系统上使用柜台系统实现向交易所报单?
那当然是需要把柜台系统的API程序库对接到我们的量化系统上。当我们要下单时,量化系统调用柜台系统API的下单接口提交下单指令到柜台服务器,柜台服务器替我们把下单指令提交给交易所,交易所处理下单请求生成一笔委托信息发送给柜台服务器,柜台API从柜台服务器里收到这笔委托信息,调用委托响应回调函数把委托信息告诉我们的量化系统。
当我们的委托在交易所成交以后,交易所会把成交数据推送给柜台服务器,服务器再把成交数据推送到柜台API,柜台API调用成交响应函数把成交信息告诉量化系统,这样量化系统就完成了一笔委托。
接下来我们聊聊这一整个流程里的具体操作,数数我们会遇到哪些问题。
创建API对象和注册SPI回调类
根据柜台API的函数接口创建一个柜台API类,将量化系统处理回调数据信息的类作为SPI注册到API实例对象里。
初始化连接
初始化连接一般只需要填写柜台服务器的ip和端口并且连接上就可以了,这里能有什么问题呢?
那就不得不说市面上不同柜台系统的接口风格多样化了,有的柜台只需要填写一下ip和port,顶多再订阅一下推送模式,调用init函数就可以了。但是有些柜台规定就比较严格,要求将IP和port信息存储在磁盘文件中,并按照指定格式填写。该格式严格且个性化,不能多出或少了任何空格,否则将无法连接。不同柜台还有不同的规定,IP和port的连接符号也各不相同。为了减轻客户的负担,我们的量化系统将统一这一格式,然后根据各个柜台的需求进行格式转换。
根据柜台的格式要修在代码里写入制定信息到磁盘文件里,调用柜台API的init函数,柜台API表示连接成功的回调函数触发后执行下一步登录
登录
初始化连接完了,网络通常了,下一步当然是要登录咱们自己的账号,密码明文传输不安全,需要先给密码加个密,再去登录,行行行,加密就加密,咱们本地和服务器都用同样的秘钥做加密,我把秘钥地址告诉你,什么?不要路径地址,必须把路径地址写在环境变量里。
为了不让客户换个机器就得手动设置一次环境变量,那咱们只能在量化系统里做这件事了,
static char env_key[data_length] = "柜台要求的环境变量名=秘钥所在路径";
// 在Windows中调用
_putenv(env_key);
// 在Linux中使用
putenv(env_key);
注: 在Windows中,env_key可以使用局部变量,调用 _putenv后释放了 env_key 不会报错。而在Linux中,在进程的运行周期内,env_key变量必须存在,否则会报错,可以将 env_key 声明成 static 或者类成员变量。
按规矩配置好登录信息,调用login函数,收到表示登陆成功的回调函数后,就可以进行查询和下单了。
查询资金和持仓
查询自己的资金和持仓信息,将这些信息从柜台数据结构类型转换成量化系统里面用来表示资金和持仓信息的数据结构类型。
对于持仓数据,有的柜台是异步一个标的一个标的推送的,在有持仓时,有的柜台最后一条数据会表示已经查完,
有的柜台会额外推送一个空数据表示查完了,在没有持仓时会推送一个空数据,
注意回调函数参数类型是指针时要做非空判断。
下单委托
下单时量化系统需要根据柜台API的接口参数填写标的号,交易所编号,价格,数量,价格类型等。同时量化系统会生成一个数据类型对象Order,用来表示一笔委托,order_id是量化系统里表示每一笔委托的唯一标识。当柜台接受这笔委托提交给交易所时,柜台服务器也会生成一个数据类型对象ExternalOrder用来表示这一笔委托,external_order_id是柜台系统里表示每一笔委托的唯一标识。
对于同步响应柜台,调用完柜台系统的下单函数后,立即得到external_order_id,此时可以用std::unordered_map来建立external_order_id和order_id的映射关系map_order,当委托成交收到成交回报ExternalTrade时,成交回报里会有external_order_id表示这笔成交属于哪一笔ExternalOrder,量化系统通过map_order用external_order_id找到order_id,根据order_id找到量化系统里的这笔委托,生成量化系统里的Trade,并且修改Order里的成交量信息,有的柜台会在推送成交回报时还会推一次委托推送ExternalOrder表示该笔委托的最新状态和已成交数量。
对于异步响应柜台,调用完柜台系统的下单函数后,需要等待委托响应函数触发才能得到external_order_id,异步下单接口会要求填写请求表示号request_id,委托响应也会返回request_id,order_id和external_order_id通过request_id作为桥梁就可以构建map映射,后续操作与同步响应柜台相同。
此处存在两个比较容易出问题的地方:
1. 多线程安全问题:下单函数时量化系统维护的线程调用,委托响应函数是柜台系统维护的线程调用,一般会是两个线程。下单线程对map<request_id,order_id>做插入时,响应线程可能同时在做查询和读取操作,下单线程因为插入操作导致容器触发扩容,响应线程读取数据的内促被释放会造成访问非法内存而进程崩溃。
2. 成交推送回调函数先于委托响应回调函数触发:当下单后立刻成交时,有可能会出现该场景,收到ExternalTrade时因为还没有建立external_order_id和order_id的映射关系,无法找到对应的Order,此时需要先将ExternalOrder暂存在map<external_order_id,vector<ExternalTrade>>里,当收到委托响应后再查找这个map做后续处理
对于问题1,可以采取两种方式解决,第一种是加锁保证同时只有一个线程在访问map<request_id,order_id>,第二种是通过才有某种方法将柜台系统线程里的数据转移给量化系统的线程来处理,保证整个进程只有一个线程会访问该map容器。
对于问题2,有的柜台系统回调函数参数类型是指针,有人可能会为了减少数据拷贝而直接将指针存入到map<external_order_id,vector<ExternalTrade*>>里,因为ExternalTrade*的内存是由柜台系统维护的,当收到委托响应时,这块内存可能已经被释放,会导致访问非法内存而进程崩溃,这里最好是将数据拷贝一份存入容器。
撤单委托
撤单委托的操作与下单委托类似,调用撤单接口请求撤销一笔未完成的委托时,有的柜台会以委托推送ExternalOrder的方式表示撤单成功,里面的委托状态变成已撤单或者部成部撤,有的柜台会以成交推送ExternalTrade的方式标识撤单成功,不管那种方式都可以修改Order的状态标识改笔委托结束。