# 关于延时

There are three ways to make a living in this business: Be first, be smarter, or cheat.
Now, I don’t cheat.
And although I like to think, we’ve some pretty smart people in this building.
It sure is a hell of a lot easier to just be first.
——《Margin Call》

之所以越来越多的交易者选择自主开发交易程序,是因为对交易环节的控制能力在交易过程中越来越重要。虽然有很多交易者在量化投资领域无私的分享研究成果,但是关于交易延时的系统化的内容仍然很少。借AlgoPlus项目的机会,我对延时问题进行了深入探究。给大家提供参考的同时,也希望能有更多的朋友参与研究、讨论与分享。

# 延时种类

# 网络延时

信息通过网络在客户端、期货公司前置、交易所主机之间传输的时间。

期货公司交易前置到交易所主机的延时是由期货公司系统决定。同一个期货公司,交易者可申请接入高速行情、高速席位。不同期货公司的技术、硬件设备差异,可对比之后选择。

交易者客户端到期货公司交易前置的延时是由网络环境决定。互联网环境,交易者需要择优选择宽带服务商。如果要进一步降低网络延时,可以将程序托管到期货公司机房的内网环境。

# 交易请求与回调延时

从产生交易请求(买卖撤查)信号开始,经过逐层传递,到最终调用CTP官方API将信息发出的时间。以及,从CTP官方API接收到回报、通知开始,经过逐层传递,到最终在策略中接收到信息的时间。

这种延时是由交易系统设计决定的,一般来说编译型语言比解释型语言执行效率更高。

# 策略内延时

策略被计时器、行情通知、订单状态等引擎驱动一次的时间。

主要由逻辑算法决定。交易者需要综合分析算法瓶颈,将高耗时操作转换为低耗时操作。

# 行情分发延时

行情进程将数据共享给策略进程的时间。

主要由使用的在多进程间共享数据的技术有关。尤其涉及多合约、多策略、多账户时,进程数量越多。

除上述因素外,硬件对延时也有至关重要的影响。我在硬件方面经验有限,所以不做这方面讨论。希望有经验的朋友可以补充。

# CTP的特性

使用CTP后台的账户可以有多个在多个登录点同时在线,simnow模拟账户最多支持2个登陆点,实盘账户支持的更多(有些期货公司可以支持8个)。

TraderApi具备完善的回调机制,所有在线的登录点都能同时接收到回报、通知等数据。所以,低延时策略应该充分利用回调信息维护本地数据,例如实时持仓数量、实时盈亏等,避免主动查询。不建议主动查询的另一个原因是CTP每秒只响应一次主动查询。

如果单账户策略数量少于最大登录点,可以为每个策略开启一个独立进程,使用OrderRef字段作为标识,过滤不相关的数据,由CTP接口回调事件驱动策略运行。单账户策略比较多的场景,可以分配N1个延时敏感进程,N2个次延时敏感进程,N3个延时不敏感进程,不同的策略通过数据共享机制与其相对应的进程通信。总之,TraderApi已经提供了多线程/进程间数据共享的机制,无论任何时候都应该是我们的首选,只有当CTP的共享线程数量不足时,我们才考虑其他共享技术进行配置,例如Queue、Redis、MMAP等。

MdApi只能接收到线程/进程内订阅的合约的行情通知,多合约场景,通过创建不同延时级别的线程/进程收发行情可以有效避免多合约的延时。

AlgoPlus就是充分利用TraderApi和MdApi这些特性来实现多账户、多合约、多策略的(低延时)应用设计。

MdApi和TraderApi之间需要外部数据共享方案,AlgoPlus推荐的是Queue,所以大家会在很多例子中看到以下启动策略的代码:

创建一/多个交易进程和行情进程,行情进程将收到的行情通知放到队列里,交易进程从队列里把和自己相关的行情取出来,从而驱动交易逻辑的判断,最终完成交易。

之所有应用多进程而不是多线程,是因为python的进程使用的解释器是相互独立的,所以不会受限于GIL,可以充分利用多核CPU的优势。

# Cython封装C

.pxd是与.h类似的头文件声明(目录\AlgoPlus\src\CTP\cython2c中的文件); .pyx是与.c类似的关于声明的具体实现(目录\AlgoPlus\src\AlgoPlus\CTP中的MdApi.pyx与TraderApi.pyx); cimport将.pxd的声明导入.pyx中; cdef用来声明类、方法、变量; 语句 with nogil: 声明了一个不需要GIL就能执行的代码块。 在这个块中,不能有任何的普通Python对象——只能使用被声明为cdef的对象和被显示声明为可以不持有GIL执行的函数。 完全兼容python语法; 编译后得到.pyd文件,可以被作为库导入到python程序中。 以TraderApi的确认结算单为例:

def ReqOrderInsert(self, pInputOrder):
    cdef int result = -1
    cdef int nRequestID
    cdef size_t address = 0
    try:
        nRequestID = self.Inc_RequestID()
        if self._api is not NULL:
            address = addressof(pInputOrder)
            with nogil:
                result = self._api.ReqOrderInsert(<CThostFtdcInputOrderField *> address, nRequestID)
    except Exception as err_msg:
        self._write_log("ReqOrderInsert", err_msg)
    finally:
        return result

# time.perf_counter()

time.perf_counter()返回计时器的精准时间(系统的运行时间),包含整个系统的睡眠时间,单位是秒。使用范例:

from time import perf_counter as timer
start_time = timer()
# 待测试的代码
anchor_time = timer()
print(anchor_time - start_time) # 时间差