• 基于逐笔成交的高频回测系统兼论K线回测的缺陷


    在FMZ文库发表了一篇文章,是我最近两个月研究的总结,部分解决了当前所有回测系统的一个通病,并且可用于高频交易回测和套利回测,值得参考,原文地址:https://www.fmz.com/digest-topic/5719

    我在币安做空超涨做多超跌多币种对冲策略时,同时发布了一个回测引擎。并第一篇报告基于一小时K线回测,验证了策略的有效性。但实际公开策略的休眠时间时1s,是一个相当高频的策略,用小时K线回测显然无法得出精确结果。后来补充了分钟线回测的结果,回测收益提高了很多,但还是无法确定秒级情况下应该用什么参数,对整个策略的理解也不是很清晰。主要原因是基于K线的回测的重要弊端。

    基于K线回测的问题

    首先什么是历史K线?一根K线数据包含高开低收四个价格、起始两个时间以及区间成交量。大部分量化平台和框架都是基于K线回测的,FMZ量化平台也提供了tick级回测。K线回测的速度很快,大部分情况下也没问题,但是也有非常严重的缺陷,特别是回测多品种策略和高频策略,几乎无法得出正确的结论。

    首先是时间问题,K线数据最高价和最低价的时间是没有给出的,不用考虑,但最重要的开盘和收盘价起始并不是开盘和收盘时间。即使不太冷门的交易品种,也往往十几秒都没有交易,而我们回测多品种策略时,往往默认它们的开盘价和收盘价是同时的,这也是基于收盘价回测的基础。

    想象一下用分钟线回测两个品种的套利,它们的差价通常10元,现在发现10:01时刻,A合约收盘价为100,B合约为112,差价是12元,于是策略开始对冲,某个时刻差价回归,策略赚了2元的回归利润。

    而实际情况可能是在10:00:45,A合约产生了一笔100元的成交,此后没有交易,B合约在10:00:58发生了一笔112元的成交,在10:01这一刻,两个价格都是不存在的,此时的盘口价格是多少呢,对冲能够吃到多少差价呢?都无法知道。一个可能的情况是:在10:00:58时,A合约的买一卖一盘口是101.9-102.1,根本没有2元差价。这就会对我们的策略优化产生很大的误导。

    其次是撮合问题,真实的撮合是价格优先,时间优先。如果买家超过卖一价,一般会直接以卖一价成交,反之进入订单簿等待。K线数据显然没有买一卖一价,是无法模拟细节层次的撮合。

    最后是策略本身成交对市场的影响,如果是小资金回测,影响不大。但如果成交量占比很大,会对市场产生冲击。不仅是立即成交时价格滑点会很大,如果回测你的买单成交了,实际上抢占了其它原来要买入交易者的成交,蝴蝶效应下会对市场产生影响。而这种影响无法量化给出,只能凭借经验说高频交易只能容纳小资金。

    基于实时深度和tick的回测

    FMZ提供了实盘级回测,能够获取到真实的历史20档深度,实时的秒级tick,逐笔成交等数据,并基于此做了实盘回放功能。这样的回测数据量极大,回测速度也很慢,一般只能回测两天。对于相对高频或者对时间判断要求严格的策略,实盘级回测是必须的。FMZ收集的交易对和时间并不长,但也有700多亿条历史数据。目前的撮合机制是如果买单大于卖一会不看量立即完全撮合,小于卖一进入撮合队列排队。这样的回测机制解决了K线回测的前两个问题,但还是无法解决最后一个问题。并且由于数据量实在太大,回测速度和时间范围都有限制。

    基于逐笔成交订单流的回测机制

    K线信息太少,深度也有可能是假深度,但有一种数据是市场真实的成交意愿,反映了最真实的交易历史———那就是逐笔成交。本文将提出一个基于订单流的高频回测系统,将大大减少实盘级回测的数据量,并且一定程度的模拟成交量对市场的影响。

    我下载了最近5天币安XTZ永续合约的逐笔成交(下载地址:https://www.fmz.com/upload/asset/1ff487b007e1a848ead.csv ),作为一个不算热门的品种,共有213000条数据,先看一下数据的构成:

    [['XTZ', 1590981301905, 2.905, 0.4, 'False
    '],
     ['XTZ', 1590981303044, 2.903, 3.6, 'True
    '],
     ['XTZ', 1590981303309, 2.903, 3.7, 'True
    '],
     ['XTZ', 1590981303738, 2.903, 238.1, 'True
    '],
     ['XTZ', 1590981303892, 2.904, 0.1, 'False
    '],
     ['XTZ', 1590981305250, 2.904, 0.1, 'False
    '],
     ['XTZ', 1590981305643, 2.903, 197.3, 'True
    '],
    

    数据是二维列表,按成交时间顺序排序。具体意义分别是:品种名称、成交价格、成交时间戳、成交数量、是否是卖单主动成交。有买有卖,每一笔成交都包含了买方和卖方,如果买方是做市maker,卖方是主动成交taker,则最后一个数据是True。

    首先根据成交方向,可以相当精确的推测出市场上的买一和卖一,如果是主动卖出单,则此时的买一价就是成交价,如果主动买入单,则卖一价为成交价,有新的成交就更新新的盘口,未更新的保留上一次结果。很容易的推出以上数据的最后时刻,买一价为2.903,卖一价为2.904。

    根据订单流,可以这样撮合:以一笔买单为例,价格为price,下单量为amount,此时盘口买一卖一分别为bid,ask。如果price低于ask高于bid,则先判断为maker,并且可以优先撮合成交,则此后在订单存在时间内所有的成交价低于或等于price的逐笔成交都与此订单撮合(如果price低于或等于bid,则不能优先成交,成交价低于price的订单都与此订单撮合),撮合价为price,交易量为逐笔成交的成交量,直到订单完全成交或者撤单。如果价格高于ask,判断为taker,此后在订单存在时间内所有的成交价低于或等于price的逐笔成交都与此订单撮合,撮合价为逐笔成交的成交价。区分maker和taker是因为基本上交易所鼓励挂单,有手续费的优惠,对于高频策略,必须考虑这种区别。

    可以很容易看到这种撮合的一个问题,如果订单为taker,实际情况是能立即成交,而不是等待新的订单与之撮合。首先我们并没有考虑盘口挂单量,就算有数据,直接判断成交也改变了深度,影响了市场。而基于新订单的撮合,相当于把历史中真实存在的订单替换成你的订单,无论如何也不会超出市场本身成交量的限制,最终盈利也不可能超过行情产生的最大盈利。部分的撮合机制也影响了订单的成交量,进而影响策略的收益,定量的反映出了策略容量。不会出现传统回测,资金量放大一倍收益就放大一倍的情况。

    还有一些小细节,如果订单买价等于买一,实际上仍然有一定的概率以买一价被撮合的,需要考虑挂单的优先级和成交概率等,较为复杂,这里就不考虑了。

    撮合代码

    交易所对象可以参考开头的介绍,基本不变,只添加了maker和taker手续费的区别,以及优化了回测的速度。下面将主要介绍撮合代码。

        symbol = 'XTZ'
        loop_time = 0
        intervel = 1000 #策略的休眠时间为1000ms
        init_price = data[0][2] #初始价格
        e = Exchange([symbol],initial_balance=1000000,maker_fee=maker_fee,taker_fee=taker_fee,log='') #初始化交易所
        depth = {'ask':data[0][2], 'bid':data[0][2]} #深度
        order = {'buy':{'price':0,'amount':0,'maker':False,'priority':False,'id':0},
                 'sell':{'price':0,'amount':0,'maker':False,'priority':False,'id':0}} #订单
        for tick in data:
            price = int(tick[2]/tick_sizes[symbol])*tick_sizes[symbol] #成交价格
            trade_amount = tick[3] #成交数量
            time_stamp = tick[1] #成交时间戳
            if tick[4] == 'False
    ':
                depth['ask'] = price
            else:
                depth['bid'] = price
            
            if depth['bid'] < order['buy']['price']:
                order['buy']['priority'] = True
            if depth['ask'] > order['sell']['price']:
                order['sell']['priority'] = True
            if price > order['buy']['price']:
                order['buy']['maker'] = True
            if price < order['sell']['price']:
                order['sell']['maker'] = True
            
            #订单网络延时也可以作为撮合条件之一,这里没考虑
            cond1 = order['buy']['priority'] and order['buy']['price'] >= price and order['buy']['amount'] > 0
            cond2 = not order['buy']['priority'] and order['buy']['price'] > price and order['buy']['amount'] > 0
            cond3 = order['sell']['priority'] and order['sell']['price'] <= price and order['sell']['amount'] > 0
            cond4 = not order['sell']['priority'] and order['sell']['price'] < price and order['sell']['amount'] > 0
    
            if cond1 or cond2:
                buy_price = order['buy']['price'] if order['buy']['maker'] else price
                e.Buy(symbol, buy_price, min(order['buy']['amount'],trade_amount), order['buy']['id'], order['buy']['maker'])
                order['buy']['amount'] -= min(order['buy']['amount'],trade_amount)
                e.Update(time_stamp,[symbol],{symbol:price})
            if cond3 or cond4:
                sell_price = order['sell']['price'] if order['sell']['maker'] else price
                e.Sell(symbol, sell_price, min(order['sell']['amount'],trade_amount), order['sell']['id'], order['sell']['maker'])
                order['sell']['amount'] -= min(order['sell']['amount'],trade_amount)
                e.Update(time_stamp,[symbol],{symbol:price})
    
            if time_stamp - loop_time > intervel:
                order = get_order(e,depth,order) #交易逻辑,这里未给出
                loop_time += int((time_stamp - loop_time)/intervel)*intervel
    

    几个细节要注意一下:

    • 1.当有新成交时,要先去撮合订单,再去根据最新的价格去下单。
    • 2.每个订单都有两个属性:maker——是否为maker,priority——撮合优先级,以买单为例,当买价小于卖一,标记为maker,当买价大于买一是标记为优先撮合,priority决定了价格等于买价是是否撮合,maker决定了手续费。
    • 3.订单的maker和priority是更新的,如下了一笔很大的超过盘口的买单,当出现一个价格大于买价时,此时剩余的成交量将是maker。
    • 4.策略的intervel是必须的,它可以代表行情的延时。

    网格策略的回测

    终于到了实际的回测阶段,我们这里回测一个最经典的网格策略,来看看有没有达到预期的效果。策略原理为价格每上涨1%,我们就持有一定价值的空单(反之持有多单),计算好买单卖单提前挂好。代码就不放出了。把所有代码封装到Grid('XTZ',100,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003)函数中,参数分别为:交易对,价格偏离1%的持有价值,挂单密度0.3%,休眠间隔ms,挂单手续费,吃单手续费。

    最近5天XTZ的行情处于震荡阶段,很适合网格。 

    我们先回测不同的持仓大小对收益的影响,传统的回测机制回测出来的收益肯定会随着持仓的增加等比增加。

    e1 = Grid('XTZ',100,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003)
    print(e1.account['USDT'])
    e2 = Grid('XTZ',1000,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003)
    print(e2.account['USDT'])
    e3 = Grid('XTZ',10000,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003)
    print(e3.account['USDT'])
    e4 = Grid('XTZ',100000,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003)
    print(e4.account['USDT'])
    

    共回测了四组,持仓价值分别为100,1000,10000,100000,回测总用时1.3s。结果如下:

    {'realised_profit': 28.470993031132966, 'margin': 0.7982662957624465, 'unrealised_profit': 0.0104554474048441, 'total': 10000028.481448, 'leverage': 0.0, 'fee': -0.3430967859046398, 'maker_fee': -0.36980249726699727, 'taker_fee': 0.026705711362357405}
    {'realised_profit': 275.63148945320177, 'margin': 14.346335829979132, 'unrealised_profit': 4.4382117331794045e-14, 'total': 10000275.631489, 'leverage': 0.0, 'fee': -3.3102045933457784, 'maker_fee': -3.5800688964477048, 'taker_fee': 0.2698643031019274}
    {'realised_profit': 2693.8701498889504, 'margin': 67.70120400534114, 'unrealised_profit': 0.5735269329348516, 'total': 10002694.443677, 'leverage': 0.0001, 'fee': -33.984021415250744, 'maker_fee': -34.879233866850974, 'taker_fee': 0.8952124516001403}
    {'realised_profit': 22610.231198585603, 'margin': 983.3853688758861, 'unrealised_profit': -20.529965947304365, 'total': 10022589.701233, 'leverage': 0.002, 'fee': -200.87094000385412, 'maker_fee': -261.5849078470078, 'taker_fee': 60.71396784315319}
    

    可以看到最终已实现利润分别为持仓价值的28.4%,27.5%,26.9%,22.6%。这也符合实际情况,持仓的价值越大,挂单的价值越大,越可能出现部分成交的情况,最终实现的收益相对于挂单量也就越小。下图是持仓价值分别为100和10000的相对收益对比:

    我们还可以回测不同的参数对回测收益的影响,如挂单密度、休眠时间、手续费等。以休眠时间为例,改为100ms,对比休眠时间1000ms,看看收益情况。回测结果如下:

    {'realised_profit': 29.079440803790423, 'margin': 0.7982662957624695, 'unrealised_profit': 0.0104554474048441, 'total': 10000029.089896, 'leverage': 0.0, 'fee': -0.3703702128662524, 'maker_fee': -0.37938946377435134, 'taker_fee': 0.009019250908098965}
    

    收益提高了一些,这是由于策略只挂了一组订单,会有一些订单由于来不及改变而吃不到波动的价格,休眠时间减少改善了这个问题。这也说明了网格策略挂多组订单的重要性。

    总结

    本文创新的提出了一种新的基于订单流的回测系统,可以部分模拟挂单、吃单、部分成交、延时等撮合情况,部分反映出了策略资金量对收益的影响,对于高频策略和对冲策略有重要的参考价值,高精度的回测为策略参数优化指明了方向。也经过了长期实盘验证。并且较好的控制了回测所需的数据量,回测速度也非常快。

  • 相关阅读:
    p1373【奶牛的卧室】
    p1248【交错匹配】(DP)
    QBXT模拟赛T3
    NOIP冲刺班的考试总结
    欧拉回路的一些东西
    一道dp题目
    Blocks
    玩具取名
    Y的积木
    游荡的奶牛
  • 原文地址:https://www.cnblogs.com/botvsing/p/13131477.html
Copyright © 2020-2023  润新知