数字货币跨期套利

本文介绍跨期套利策略回测的完整版本,并提供回测代码。该代码适用OKEx交易所的合约交易规则,由于各个交易所的合约交易规则并不一样,因此使用其他交易所的数据进行代入时,得到的结果极有可能是不正确的。本文主要分为以下部分:使用收益率函数进行回测的问题;正确的回测思路和代码;回测的真实结果;总结。以下开始正文。

 

# ===使用收益率函数进行回测的问题

收益率函数的推导过程是在理想状态下进行的。学过一点物理的人都知道,理想状态是不存在的,它的作用只是给我们提供一个思考的起点。

仅仅使用收益率函数进行回测,存在的主要问题如下:

  1. 没有考虑手续费(如果是股票回测的话,还要加上没有考虑印花税)
  2. 没有考虑滑点
  3. 没有考虑资金限制

 

以下详述。

第一点,没有考虑手续费。非常容易理解,在交易所交易是需要收取手续费的。在OKEx上进行合约交易的费率情况如下:

来源:OKEx关于交易手续费调整的公告

 

10000BTC在当前时间(2018.7.19)的价值约为5亿人民币。笔者暂且冒昧地认为本文读者的币圈投入并没有达到这个数字......

 

所谓的挂单和吃单是指,进行限价交易还是市价交易。众所周知,交易所最重要的是流动性。挂单(下限价单)可以达到提供流动性的效果,吃单(下市价单)可以达到提取流动性的效果。这也是挂单手续费比吃单手续费低的原因。更有甚者,如Bitmex,下限价单可以获得返佣,正是为了鼓励交易者下限价单,鼓励做市,为交易所提供流动性。

 

因此,根据谨慎性原则,回测时采用吃单手续费,即0.05%。第二点,没有考虑滑点。所谓出现滑点是指由于种种原因,导致下单价格和我们实时监测到,并准备下单的价格不一致的情况。原因有很多,例如:价格瞬息万变,在监测到价格和下单交易之间的时间里,价格发生了变化,有可能就会导致不利的情况发生;盘口深度不够,卖一量或买一量无法满足需求等等。由于我们很难获得回测期间的盘口数据,即使能获得,要完全模拟实际情况也非常难。因此,本文采取的仅仅是在平仓时对收益乘以一个系数,来粗略地模拟滑点发生。

 

本文的系数主观地采用了0.9。

第三点,没有考虑资金限制。这一点比较微妙,但是最重要的一点。在OKEx上进行合约交易的张数系统在前面几篇文章中略有描述,此处详细说明。

OKEX的合约是OKEX推出的以BTC/LTC等币种进行结算的虚拟合约产品,每一张合约分别代表100美元的BTC,或10美元的其他币种(LTC,ETH等),投资者可以通过买入做多合约来获取虚拟数字货币价格上涨的收益,或通过卖出做空来获取虚拟数字货币收益。合约的杠杆倍数为10或20倍。
——《OKEx合约简介》

 

也就是说,买卖按照合约张数计算。与传统的期货合约不同的是,一张合约并不是特定数量的“标的”,而是价值特定数量美元的“标的”,而保证金为合约标的的现货,本文采用合约标的使用的保证金即为EOS。合约进行这样的设计的原因是:

传统的比特币虚拟合约以美元计价,每一张合约代表固定数量的比特币,这种合约的最大问题在于其杠杆倍数会随着价格的变化而变化,由此带来的两个副作用:难以套保和套利,无法做长周期的合约。
针对上述问题,OKEX在设计虚拟合约时做了相应的调整,每一张合约代表价值100美元的比特币,这样的设计使得虚拟合约的杠杆倍数始终稳定在一个固定值,从而利于套保和套利。
OKEX比特币虚拟合约的杠杆表现为法币收益层面的杠杆稳定:投入100美元,所能得到的收益=100美元*比特币的涨跌幅*固定的杠杆倍数。
假设当前价格为500USD/BTC,某投资者以当前价格买入一个BTC,本金为500USD,此时投资者可以做多50张BTC虚拟合约。此时若BTC价格上涨至750美元,涨幅50%,投资者合约收益为3.3333个BTC,按照当前价格卖出后可以获得2500美元,收益为其本金投入的5倍。若价格上涨至1000美元,合约收益为5BTC,卖出后的美元收入为5000美元,为其美元收入的10倍。无论价格怎么波动,合约的杠杆都十分稳定,从而方便商家用合约进行套保,也便于普通投资者管理其仓位。
——《OKEx合约简介》

 

因此,下单的单位以张数计算,对于EOS来说每张10美元。也就是说,每次下单至少价值10美元的EOS。这会导致如果账户里的钱并不能被100%地利用。距离来说,当前EOS当周合约价格8.43美元,而交易者手中有657个EOS,下单张数就是 int(657\times8.43\div10) 即553张。剩余的EOS是不能作为保证金用来下单的,因为不足一张。

 

考虑到这三点,前一篇文章数字货币跨期套利(回测·简单版)的方法过于粗略,一般来说,会高估收益。

 

# ===正确的回测思路和代码

实际上,关于仓位的产生,并没有太大的问题, 根据仓位产生的交易信号,也不需要改。和前一篇文章相比,需要进行改动的只有计算收益的部分。这一次,我们采取模拟交易的方法。

 

首先,用手中的USDT换取EOS(计入手续费),用远期合约对冲掉EOS(计入手续费),防止未开仓时承受价格波动的风险。获取到开仓信号时,程序会计算当前持有的eos数量,根据相应的比例计算各个合约应该开的张数,记录成交的价格(计入手续费);获取到平仓信号时,根据开仓价格和张数计算每个合约的盈亏情况(计入手续费、滑点),得到这一次开仓——平仓操作收益的EOS,并在下一次开仓信号发出前,用季度合约把持有的EOS对冲掉(计入手续费),防止承受价格波动造成的风险;没有获取到信号时,计算合约的未实现盈亏,计算当前的美元价值。在回测的最后,将所有的仓位清空,计算最终的EOS和其美元价值。

 

代码如下:

配置部分

# -*- coding=utf-8 -*-

import os

# 获取当前程序的地址
current_file = __file__

# 程序根目录地址
root_path = os.path.abspath(os.path.join(current_file, os.pardir, os.pardir))

# 输入数据根目录地址
input_data_path = os.path.abspath(os.path.join(root_path, 'data', 'input_data'))

# 输出数据根目录地址
output_data_path = os.path.abspath(os.path.join(root_path, 'data', 'output_data'))

 

函数部分

# -*- coding:utf-8 -*-

import pandas as pd
import os
from program import config
commission = 0.0005
pd.set_option('expand_frame_repr', False)

# 获取数据
def get_data():
    for roots, dirs, files in os.walk(config.input_data_path):
        if files:
            if files[0].startswith('okex_spot_future'):
                file_name = files[0]
    data_path = os.path.join(config.input_data_path, file_name)
    df = pd.read_csv(data_path)
    return(df)

# 产生网格
def gen_grid(number, start, down_step, up_step):
    grid_list = []
    for i in range(number):
        grid_list.append({'down': start + i * down_step, 'up': start + (i+1) * up_step})
    return grid_list

# 计算仓位
def posit (df_sample, number, grid_list):
    '''
    :param df_sample:
    :param number: 网格数量
    :param start_point: 起始点
    :return:
    '''
    for i in range(number):
        for indexs in df_sample.index:
            # 价差大于建仓阈值
            condition1 = (df_sample.at[indexs, 'gap'] >= grid_list[i]['up'])
            if condition1:
                df_sample.at[indexs, 'l_%d_position' % i] = 1
            # 价差小于平仓阈值
            condition2 = (df_sample.at[indexs, 'gap'] <= grid_list[i]['down'])
            if condition2:
                df_sample.at[indexs, 'l_%d_position' % i] = 0

            # 周五下午4:00强制平仓
            condition3 = (df_sample.at[indexs,'candle_begin_time'].weekday() == 4)and\
                         (df_sample.at[indexs,'candle_begin_time'].hour == 15)and\
                         (df_sample.at[indexs, 'candle_begin_time'].minute == 59) and\
                         (df_sample.at[indexs, 'candle_begin_time'].second == 00)
            if condition3:
                df_sample.at[indexs, 'l_%d_position' % i] = 0

            # 最后一分钟,强平
            condition4 = (indexs == (df_sample.shape[0]-1))
            if condition4:
                df_sample.at[indexs, 'l_%d_position' % i] = 0

    df_sample.fillna(method = 'ffill', inplace = True)
    df_sample.fillna(0, inplace=True)
    return(df_sample)

# 计算单个合约收益
def cal_yield(amount, open_price, settle_price, type):
    '''
    :param amount: 张数
    :param open_price: 开仓价
    :param settle_price: 结算价
    :param type: 合约方向
    :return:
    '''
    if open_price != 0:
        if type == 'long':
            earned = amount * (10/open_price - 10/settle_price) * (1-commission)
        if type == 'short':
            earned = amount * (10/settle_price - 10/open_price) * (1-commission)
        return(earned)
    else:
        return(0)

# 调仓
def change(equity, type, spot_price, week_price, quarter_price):
    '''
    :param equity: {'long_amount':0, 'short_amount':0, 'hedge_amount':0, 'week_open_price':0, 'quarter_open_price':0, 'eos':currency}
    :param type:
    :param spot_price:
    :param week_price:
    :param quarter_price:
    :return:
    '''

    # 开仓,记录信息
    if type == 'open':
        equity['week_open_price'] = week_price
        equity['quarter_open_price'] = quarter_price
        long_margin = equity['eos'] * 19 / 40
        short_margin = equity['eos'] * 19 / 40
        hedge_margin = equity['eos'] * 2 / 40
        equity['long_amount'] = int(long_margin * 20 * week_price * (1-commission) / 10) # 当周开仓张数
        equity['short_amount'] = int(short_margin * 20 * quarter_price * (1-commission) / 10) # 季度开仓张数
        equity['hedge_amount'] = int(hedge_margin * 20 * quarter_price * (1-commission) / 10) # 对冲开仓张数
        equity['eos'] *= (1 - commission)

    # 平仓
    if type == 'close':
        earned_week = cal_yield(equity['long_amount'], equity['week_open_price'], week_price, 'long')
        earned_quarter = cal_yield(equity['short_amount'], equity['quarter_open_price'], quarter_price, 'short')
        earned_hedge = cal_yield(equity['hedge_amount'], equity['quarter_open_price'], quarter_price, 'short')
        equity['long_amount'] = 0
        equity['short_amount'] = 0
        equity['hedge_amount'] = 0
        equity['week_open_price'] = 0
        equity['quarter_open_price'] = 0
        equity['eos'] += (earned_week + earned_quarter + earned_hedge) * 0.9

    # 对冲
    if type == 'hedge':
        equity['week_open_price'] = week_price
        equity['quarter_open_price'] = quarter_price
        hedge_margin = equity['eos'] / 20
        equity['long_amount'] = 0 # 当周开仓张数
        equity['short_amount'] = 0 # 季度开仓张数
        equity['hedge_amount'] = int(hedge_margin * 20 * quarter_price * (1-commission) / 10) # 对冲开仓张数
        equity['eos'] = (equity['eos'] - hedge_margin) + hedge_margin * (1-commission)

    return equity

# 计算资金曲线完整版
def equity_curve_complete(df_sample, principal, number):
    '''
    :param df_sample:
    :param principal: 本金
    :param number: 账户数量
    :return:
    '''
    # 每个账户的初始资金,usdt
    money_each = principal / number

    # 逐个账户遍历
    for account in range(number):

        # 买币
        commision = 0.0005
        currency = money_each / df_sample.iloc[0,1] * (1- commision)
        # usdt_left = 0 # 剩余usdt为0
        equity = {'long_amount':0, 'short_amount':0, 'hedge_amount':0, 'week_open_price':0, 'quarter_open_price':0, 'eos':currency}

        # 如果初始仓位为1,建仓
        if df_sample.at[0, 'l_%d_position' % account] == 1:
            equity = change(equity, 'open', df_sample.iloc[0, 1], df_sample.iloc[0, 2], df_sample.iloc[0, 3])

        # 如果初始仓位为0,买入季度合约对冲保证金,记录在equity的hedge里
        if df_sample.at[0, 'l_%d_position' % account] == 0:
            equity = change(equity, 'hedge', df_sample.iloc[0, 1], df_sample.iloc[0, 2], df_sample.iloc[0, 3])
        df_sample.at[0, 'l_%d_eos' % account] = equity['eos']

        # 模拟该账户的持仓变化
        for indexs in df_sample.index:
            spot_price = df_sample.at[indexs, 'EOS_cash_close']
            week_price = df_sample.at[indexs, 'EOS_this_week_close']
            quarter_price = df_sample.at[indexs, 'EOS_quarter_close']
            # 不是第一行的部分,模拟交易
            if indexs > 0:
                # 如果仓位和昨天不同,进行调仓
                if df_sample.at[indexs,'l_%d_position' % account] != df_sample.at[indexs-1,'l_%d_position' % account]:

                    # 今天仓位为1,建仓
                    if df_sample.at[indexs, 'l_%d_position' % account] == 1:
                        # 先平掉所有持仓
                        equity = change(equity, 'close', spot_price, week_price, quarter_price)
                        # 建仓
                        equity = change(equity, 'open', spot_price, week_price, quarter_price)
                        # 记录
                        df_sample.at[indexs, 'l_%d_eos' % account] = equity['eos']

                    # 今天仓位为0,平仓
                    if df_sample.at[indexs, 'l_%d_position' % account] == 0:
                        # 平掉所有持仓
                        equity = change(equity, 'close', spot_price, week_price, quarter_price)
                        # 对冲
                        equity = change(equity, 'hedge', spot_price, week_price, quarter_price)
                        # 记录
                        df_sample.at[indexs, 'l_%d_eos' % account] = equity['eos']

                # 如果仓位和昨天相同,计算当前合约持仓盈亏
                if df_sample.at[indexs, 'l_%d_position' % account] == df_sample.at[indexs-1, 'l_%d_position' % account]:
                    # 计算未实现盈亏
                    earned_week = cal_yield(equity['long_amount'], equity['week_open_price'], week_price, 'long')
                    earned_quarter = cal_yield(equity['short_amount'], equity['quarter_open_price'], quarter_price, 'short')
                    earned_hedge = cal_yield(equity['hedge_amount'], equity['quarter_open_price'], quarter_price, 'short')
                    df_sample.at[indexs, 'l_%d_eos' % account] = equity['eos'] + earned_week + earned_quarter + earned_hedge


    # 计算总eos和usd总价值
    for indexs in df_sample.index:
        sum = 0
        for account in range(number):
            sum += df_sample.at[indexs, 'l_%d_eos' % account]
        df_sample.at[indexs, 'total_eos'] = sum
        df_sample.at[indexs, 'total_usd'] = sum * df_sample.at[indexs, 'EOS_cash_close']
    return(df_sample)

 

主程序部分

# -*- coding:utf-8 -*-

import pandas as pd
from program import function as fct
from program import config


pd.set_option('expand_frame_repr', False)

# 读取数据
df = fct.get_data()
df = df.iloc[25920:] # 只用数据的后半部分进行回测,因为前半部分用来推测网格参数了
df.reset_index(inplace = True, drop = True)


# 获取交易合约的品种
sample_list = []
for keys in df:
    if keys.endswith('close'):
        if keys[:3] not in sample_list:
            sample_list.append(keys[:3])

# 对每个品种进行回测
sample_list = ['EOS']
for sample in sample_list:
    # 获取df中该sample的部分
    df_sample = df[['candle_begin_time', sample + '_cash_close', sample +'_this_week_close', sample + '_quarter_close']]

    # 把日期转换为时间变量
    df_sample['candle_begin_time']=pd.to_datetime(df_sample['candle_begin_time'])

    # 计算价差
    df_sample['gap'] = (df[sample + '_quarter_close'] - df[sample +'_this_week_close']) / df[sample +'_this_week_close']

    # 网格参数
    number = 5  # 网格数量
    start_point = 0  # 网格最低点
    down_step = 0.01  # 下界步长
    up_step = 0.01 # 上界步长

    # 产生网格
    grid_list = fct.gen_grid(number, start_point, down_step, up_step)

    print(grid_list)
    # 计算仓位
    df_sample = fct.posit(df_sample, number, grid_list)


    # 计算资金曲线 完整版
    principal = 1000000
    df_sample = fct.equity_curve_complete(df_sample, principal, number)
    print(df_sample)


    df_sample.to_csv(config.output_data_path + '/curve.csv')

 

 

# ===回测的真实结果

最终回测得到的资金曲线如下:

其中极其引人注目的是,有一个巨大的回撤。这是因为当时的价差一路上升,扩大到了10%以上,明显超出了我们界定的5%。不过,在全仓模式下,并没有爆仓。价差的扩大是不可持续的,同时,如果价差扩大到例如15%,使得网格交易的账户发生了整体性的爆仓,那么,单纯的做空价差也将变得非常有利可图。15%的价差一旦缩小到2%左右,就能有130%的收益。也就是说,投入新的资金做空价差将成为另一个策略,而且其盈利足以弥补在网格交易上的亏损。

最终的回测收益约为14%,和前一篇文章的超过20%相比,低了不少,但依然非常之高。仅仅半个月的时间就能有如此不菲的收益,其原因就在于价差的波动。

 

# ===总结

网格交易实质上是放弃了对当前价差位置的判断,而从价差的波动中获利。降低收益,但也降低了风险。能够准确判断趋势当然是最好的,但无法确信自己正确与否的时候,“赚胜率高的小钱”,才是量化的意义

免责声明:信息仅供参考,不构成投资及交易建议。投资者据此操作,风险自担。
如果觉得文章对你有用,请随意赞赏收藏
相关推荐
相关下载
登录后评论
Copyright © 2019 宽客在线