首发于VeighNa量化
vn.trader的tick-to-trade延时测试

vn.trader的tick-to-trade延时测试

原创文章,转载请注明出处:用Python的交易员

tick-to-trade延时

对于量化交易平台而言,最重要的技术指标之一就是所谓的tick-to-trade延时,即从底层API收到一个tick行情推送的tick_time,到平台内部处理完毕再调用底层API发出委托的trade_time,中间所耗费的时间。

通常出于纯粹测试平台性能的目的,会采用收到行情后不进行任何策略逻辑计算,立即发出委托的方式来实现。考虑目前vn.trader的用户群中最常见的情景,本文中使用Windows系统和CTP接口来进行测试。

为了实现这种精度比较高的延时测试,需要在底层的C++ API相关的封装中加入计时相关的代码,这里选择用Windows系统提供的QueryPerformanceCounter和QueryPerformanceFrequency函数,具体可以参考这篇文章

在MdApi中测量tick_time

当行情接口MdApi的行情推送回调函数被触发时,需要立即记录下当前的时间戳。


void MdApi::OnRtnDepthMarketData(CThostFtdcDepthMarketDataField *pDepthMarketData)
{
    Task task = Task();

    LARGE_INTEGER t;
    QueryPerformanceCounter(&t);
    task.task_time = t.QuadPart;
    //task.task_time = std::clock();

    task.task_name = ONRTNDEPTHMARKETDATA;

    if (pDepthMarketData)
    {
        task.task_data = *pDepthMarketData;
    }
    else
    {
        CThostFtdcDepthMarketDataField empty_data = CThostFtdcDepthMarketDataField();
        memset(&empty_data, 0, sizeof(empty_data));
        task.task_data = empty_data;
    }
    this->task_queue.push(task);
};

然后在处理C++行情结构体并生成Python字典时,把时间戳和计时频率作为额外的数据放入字典中。


void MdApi::processRtnDepthMarketData(Task task)
{
    PyLock lock;
    CThostFtdcDepthMarketDataField task_data = any_cast<CThostFtdcDepthMarketDataField>(task.task_data);
    dict data;
    data["HighestPrice"] = task_data.HighestPrice;
    data["BidPrice5"] = task_data.BidPrice5;
    data["BidPrice4"] = task_data.BidPrice4;
    data["BidPrice1"] = task_data.BidPrice1;
    data["BidPrice3"] = task_data.BidPrice3;
    data["BidPrice2"] = task_data.BidPrice2;
    data["LowerLimitPrice"] = task_data.LowerLimitPrice;
    data["OpenPrice"] = task_data.OpenPrice;
    data["AskPrice5"] = task_data.AskPrice5;
    data["AskPrice4"] = task_data.AskPrice4;
    data["AskPrice3"] = task_data.AskPrice3;
    data["PreClosePrice"] = task_data.PreClosePrice;
    data["AskPrice1"] = task_data.AskPrice1;
    data["PreSettlementPrice"] = task_data.PreSettlementPrice;
    data["AskVolume1"] = task_data.AskVolume1;
    data["UpdateTime"] = task_data.UpdateTime;
    data["UpdateMillisec"] = task_data.UpdateMillisec;
    data["AveragePrice"] = task_data.AveragePrice;
    data["BidVolume5"] = task_data.BidVolume5;
    data["BidVolume4"] = task_data.BidVolume4;
    data["BidVolume3"] = task_data.BidVolume3;
    data["BidVolume2"] = task_data.BidVolume2;
    data["PreOpenInterest"] = task_data.PreOpenInterest;
    data["AskPrice2"] = task_data.AskPrice2;
    data["Volume"] = task_data.Volume;
    data["AskVolume3"] = task_data.AskVolume3;
    data["AskVolume2"] = task_data.AskVolume2;
    data["AskVolume5"] = task_data.AskVolume5;
    data["AskVolume4"] = task_data.AskVolume4;
    data["UpperLimitPrice"] = task_data.UpperLimitPrice;
    data["BidVolume1"] = task_data.BidVolume1;
    data["InstrumentID"] = task_data.InstrumentID;
    data["ClosePrice"] = task_data.ClosePrice;
    data["ExchangeID"] = task_data.ExchangeID;
    data["TradingDay"] = task_data.TradingDay;
    data["PreDelta"] = task_data.PreDelta;
    data["OpenInterest"] = task_data.OpenInterest;
    data["CurrDelta"] = task_data.CurrDelta;
    data["Turnover"] = task_data.Turnover;
    data["LastPrice"] = task_data.LastPrice;
    data["SettlementPrice"] = task_data.SettlementPrice;
    data["ExchangeInstID"] = task_data.ExchangeInstID;
    data["LowestPrice"] = task_data.LowestPrice;
    data["ActionDay"] = task_data.ActionDay;

    //保存测试时间
    data["tick_time"] = task.task_time;
    data["frequency_time"] = this->t.QuadPart;
    this->onRtnDepthMarketData(data);
};

在TdApi中测量trade_time

行情进入Python环境中处理完毕,会调用底层API发送委托,委托发送成功后需要立即记录当时的时间戳trade_time,并返回到Python环境中从而实现测量tick_time和trade_time之间的延时。


LONGLONG TdApi::reqOrderInsert(dict req, int nRequestID)
{
    CThostFtdcInputOrderField myreq = CThostFtdcInputOrderField();
    memset(&myreq, 0, sizeof(myreq));
    getChar(req, "ContingentCondition", &myreq.ContingentCondition);
    getStr(req, "CombOffsetFlag", myreq.CombOffsetFlag);
    getStr(req, "UserID", myreq.UserID);
    getDouble(req, "LimitPrice", &myreq.LimitPrice);
    getInt(req, "UserForceClose", &myreq.UserForceClose);
    getChar(req, "Direction", &myreq.Direction);
    getInt(req, "IsSwapOrder", &myreq.IsSwapOrder);
    getInt(req, "VolumeTotalOriginal", &myreq.VolumeTotalOriginal);
    getChar(req, "OrderPriceType", &myreq.OrderPriceType);
    getChar(req, "TimeCondition", &myreq.TimeCondition);
    getInt(req, "IsAutoSuspend", &myreq.IsAutoSuspend);
    getDouble(req, "StopPrice", &myreq.StopPrice);
    getStr(req, "InstrumentID", myreq.InstrumentID);
    getStr(req, "ExchangeID", myreq.ExchangeID);
    getInt(req, "MinVolume", &myreq.MinVolume);
    getChar(req, "ForceCloseReason", &myreq.ForceCloseReason);
    getStr(req, "BrokerID", myreq.BrokerID);
    getStr(req, "CombHedgeFlag", myreq.CombHedgeFlag);
    getStr(req, "GTDDate", myreq.GTDDate);
    getStr(req, "BusinessUnit", myreq.BusinessUnit);
    getStr(req, "OrderRef", myreq.OrderRef);
    getStr(req, "InvestorID", myreq.InvestorID);
    getChar(req, "VolumeCondition", &myreq.VolumeCondition);
    getInt(req, "RequestID", &myreq.RequestID);

    int i = this->api->ReqOrderInsert(&myreq, nRequestID);

    //延时测试相关
    LARGE_INTEGER order_time;
    QueryPerformanceCounter(&order_time);
    return order_time.QuadPart;
};

感谢Boost.Python的强大封装功能,可以自动实现long long(LONGLONG)类型到Python整数类型的转换,用户无需另外处理。

在Python环境中计算tick-to-trade延时

在Python环境中,我们已经通过行情推送的tick字典获取了tick_time,又通过调用发送委托请求后的返回数值获取了trade_time,此时在Python环境中计算两者之间的差值即我们需要的tick_to_trade延时。


#----------------------------------------------------------------------
def orderTest(self, event):
    """"""
    tick = event.dict_['data']

    req = VtOrderReq()
    req.symbol = tick.symbol
    req.price = tick.lastPrice
    req.direction = DIRECTION_LONG
    req.offset = OFFSET_OPEN
    req.priceType = PRICETYPE_LIMITPRICE
    req.volume = 1

    order_time = self.sendOrder(req, tick.gatewayName)

    print u'耗时:', (order_time - tick.tick_time)*1000/tick.frequency_time, u'毫秒'

在主引擎MainEngine中添加该事件处理函数,监听所有的EVENT_TICK类型的事件,收到事件推送后立即发送一个开多1手的限价委托。需要注意的是通过QueryPerformanceCounter获取的tick_time和trade_time是系统时间的计数,需要除以计时频率才能转换成具体的时间(秒),乘以1000后转化为毫秒时间。

以上函数在主引擎初始化时注册到了事件引擎中。


########################################################################
class MainEngine(object):
    """主引擎"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        # 创建事件引擎
        self.eventEngine = EventEngine2()
        self.eventEngine.start()

        # 创建数据引擎
        self.dataEngine = DataEngine(self.eventEngine)

        # MongoDB数据库相关
        self.dbClient = None    # MongoDB客户端对象

        # 调用一个个初始化函数
        self.initGateway()

        # 扩展模块
        self.ctaEngine = CtaEngine(self, self.eventEngine)
        self.drEngine = DrEngine(self, self.eventEngine)
        self.rmEngine = RmEngine(self, self.eventEngine)

        self.eventEngine.register(EVENT_TICK, self.orderTest)

注意需要手动在TradingWidget中订阅一些合约来收到tick推送。

测试结果

作者测试机器配置:Core i7-6700K 4.0G/16G/Windows 7

测试结果:228个数据点,平均tick_to_trade延时22.6毫秒

当vn.trader底层的行情接口收到一个新的行情数据推送后,最快差不多只需要百分之二秒的时间就可以完成事件引擎处理,并调用交易接口发出委托。这个速度对于追求微秒级交易延时的超高频策略而言可能无法满足,但对于大部分目标延时在毫秒级以上的常规高频策略应该说是基本没有问题。

同时考虑到测试时使用的是Windows系统,且带GUI图形界面的形式,其实还存在着相当大的提升空间,有兴趣的朋友不妨自己试试看,欢迎把测试结果分享给我。

文章中的所有测试代码可以在vn.py项目的Github仓库下载。

编辑于 2016-12-07 22:47