量化交易/算法交易之找一个交易手法

我们就用 数字货币交易策略大全! 中的套利来举例吧。

In [ ]:
# 基础库导入

from __future__ import print_function
from __future__ import division

import warnings
# warnings.filterwarnings('ignore')
# warnings.simplefilter('ignore')
import time
import moment  
import random

import pandas as pd
import numpy as np
import mplfinance as mpf
import cufflinks as cf

cf.set_config_file(theme='pearl',sharing='public',offline=True)

from tqdm.autonotebook import tqdm
tqdm.pandas(desc="progress")

import matplotlib.pyplot as plt
import ipywidgets
%matplotlib inline

import qrcode
from IPython.display import Image   

import psutil
g_cpu_cnt = psutil.cpu_count(logical=True) * 1

import os
import sys
import logging
# 使用insert 0即只使用github,避免交叉使用了pip安装的abupy,导致的版本不一致问题
sys.path.insert(0, os.path.abspath('../'))
import abupy

# from abupy import env
from abupy.CoreBu import  ABuEnv
# from abupy.AlphaBu import ABuPickTimeExecute

from abupy import AbuFactorAtrNStop, AbuFactorPreAtrNStop, AbuFactorCloseAtrNStop, AbuFactorBuyBreak
from abupy import AbuFactorSellBreak
from abupy import abu, EMarketTargetType, AbuMetricsBase, ABuMarketDrawing, ABuProgress, ABuSymbolPd
from abupy import EMarketTargetType, EDataCacheType, EMarketSourceType, EMarketDataFetchMode, EStoreAbu, AbuUmpMainMul

from abupy import AbuUmpMainDeg, AbuUmpMainJump, AbuUmpMainPrice, AbuUmpMainWave, feature, AbuFeatureDegExtend

from abupy import AbuUmpEdgeDeg, AbuUmpEdgePrice, AbuUmpEdgeWave, AbuUmpEdgeFull, AbuUmpEdgeMul, AbuUmpEegeDegExtend
from abupy import AbuUmpMainDegExtend, ump, Parallel, delayed, AbuMulPidProgress

from abupy import ABuFileUtil

pd.options.display.max_rows = 222
from IPython.display import display
from IPython.core.display import HTML
from pprint import pprint, pformat 

# 关闭沙盒数据
abupy.env.disable_example_env_ipython()

# abupy.feature.g_price_rank_keys = [12, 25, 50, 100, 200]
# abupy.feature.g_deg_keys = [12, 25, 50, 100, 200]
#os.path.join(ABuEnv.g_project_cache_dir, 'abu_socket_progress')
In [15]:
import numpy as np 
import pandas as pd
import mplfinance as mpf
from datetime import datetime as dt

import mplfinance as mpf

import  talib
from talib import MA_Type
from talib.abstract import *

pd.options.display.max_rows = 6
In [4]:
K_DEFAULT_DT_FMT = "%Y-%m-%d"

# 开高低收
def ohlc(x):
    if len(x):
        ohlc={ "open":x["open"][0],
              "high":max(x["high"]),
              "low":min(x["low"]),
              "close":x["close"][-1],
              "volume":sum(x["volume"])}
        return pd.Series(ohlc)



def fix_date(date_str):
    """
    修复日期不规范的写法:
                eg. 2016-1-1 fix 2016-01-01
                eg. 2016:01-01 fix 2016-01-01
                eg. 2016,01 01 fix 2016-01-01
                eg. 2016/01-01 fix 2016-01-01
                eg. 2016/01/01 fix 2016-01-01
                eg. 2016/1/1 fix 2016-01-01
                eg. 2016:1:1 fix 2016-01-01
                eg. 2016 1 1 fix 2016-01-01
                eg. 2016 01 01 fix 2016-01-01
                .............................
    不使用时间api,直接进行字符串解析,执行效率高,注意fix_date内部会使用fmt_date
    :param date_str: 检测需要修复的日期str对象或者int对象
    :return: 修复了的日期str对象
    """
    if date_str is not None:
        # 如果是字符串先统一把除了数字之外的都干掉,变成干净的数字串
        if isinstance(date_str, six.string_types):
            # eg, 2016:01-01, 201601-01, 2016,01 01, 2016/01-01 -> 20160101
            date_str = ''.join(list(filter(lambda c: c.isdigit(), date_str)))
        # 再统一确定%Y-%m-%d形式
        date_str = fmt_date(date_str)
        y, m, d = date_str.split('-')
        if len(m) == 1:
            # 月上补0
            m = '0{}'.format(m)
        if len(d) == 1:
            # 日上补0
            d = '0{}'.format(d)
        date_str = "%s-%s-%s" % (y, m, d)
    return date_str

def week_of_date(date_str, fmt=K_DEFAULT_DT_FMT, fix=True):
    """
    输入'2016-01-01' 转换为星期几,返回int 0-6分别代表周一到周日
    :param date_str: 式时间日期str对象
    :param fmt: 如date_str不是%Y-%m-%d形式,对应的格式str对象
    :param fix: 是否修复日期不规范的写法,eg. 2016-1-1 fix 2016-01-01
    :return: 返回int 0-6分别代表周一到周日
    """

    if fix and fmt == K_DEFAULT_DT_FMT:
        # 只针对%Y-%m-%d形式格式标准化日期格式
        date_str = fix_date(date_str)
    return dt.strptime(date_str, fmt).weekday()


def to_abu_df(df):
    """  转换为Abu/Bbu常用的格式"""
    df['date'] = df.index
    df['date_time'] = df.index
    # df_d.date =  pd.to_datetime(df_d['date'],format='%Y%m%d') 
    # df_d['date'] = pd.to_datetime(df_d['date'], format="%m/%d/%Y")
    """
    Changing the format but not changing the type:
    df['date'] = pd.to_datetime(df["date"].dt.strftime('%Y-%m'))
    """
    df['date'] = df['date'].dt.strftime('%Y%m%d')
    # df['date_week'] = df['date'].apply(lambda x: week_of_date(str(x), '%Y%m%d'))
    df['date_week'] = df['date'].apply(lambda x: week_of_date(str(x), '%Y%m%d'))
    df['date_time'] = df['date_time'].dt.strftime('%Y%m%d%H%M%S').astype(int)

    df['pre_close'] = df['close'].shift(1)
    df['pre_close'].fillna(df['open'], axis=0, inplace=True)

    df['p_change'] = np.where(df['pre_close'] == 0, 0,
                                   (df['close'] - df['pre_close']) / df['pre_close'] * 100)
    df['p_change'] = df['p_change'].apply(lambda x: round(x, 3))

    df  = df[['open','close','high','low','volume','date','date_time','pre_close','date_week','p_change']]
    return df

上面易大堆代码是常用库导入和定义一些下面常用到的 function,不用详细了解。

探索数据

下面是某数字货币远/近期合约的原始行情数据和K线图。

In [12]:
raw_data = pd.read_csv('/opt/notebook/data/HUOBI_ETH_CQ.csv', index_col='datetime')
raw_data['date_time'] =  pd.to_datetime(raw_data.index)
raw_data.index =  raw_data['date_time']
raw_data = raw_data.resample('2H').apply(ohlc)

# raw_data = raw_data[raw_data.index>='2020-06-15 06:00:00']
raw_data
Out[12]:
open high low close volume
date_time
2019-01-01 00:00:00 130.849 131.250 129.957 130.104 7.8744e+05
2019-01-01 02:00:00 130.130 130.600 129.000 130.375 7.2238e+05
2019-01-01 04:00:00 130.368 132.698 130.368 131.648 1.1375e+06
... ... ... ... ... ...
2021-01-22 00:00:00 1136.953 1165.410 1065.995 1155.032 8.5335e+07
2021-01-22 02:00:00 1155.032 1199.602 1146.748 1195.231 3.3181e+07
2021-01-22 04:00:00 1195.201 1224.795 1191.057 1199.057 1.0268e+07

9027 rows × 5 columns

In [6]:
tmp_df = raw_data[['volume', 'open', 'close', 'high', 'low']]
tmp_df['date_time'] = tmp_df.index

tmp_df.columns = [ 'Volume', 'Open', 'Close', 'High', 'Low', 'Date']
# qf=cf.QuantFig(tmp_df, title='ETH 当季', legend='top',name='GS')
# qf.add_bollinger_bands(boll_std=2.0)
# qf.iplot()
tmp_df
Out[6]:
Volume Open Close High Low Date
date_time
2020-06-15 06:00:00 119830.0 224.421 224.266 224.422 224.043 2020-06-15 06:00:00
2020-06-15 06:01:00 374252.0 224.266 225.198 225.244 224.143 2020-06-15 06:01:00
2020-06-15 06:02:00 147036.0 225.200 224.873 225.352 224.869 2020-06-15 06:02:00
... ... ... ... ... ... ...
2021-01-22 04:42:00 95380.0 1204.536 1202.693 1206.171 1201.423 2021-01-22 04:42:00
2021-01-22 04:43:00 132294.0 1202.398 1200.542 1203.411 1200.000 2021-01-22 04:43:00
2021-01-22 04:44:00 116020.0 1200.542 1199.057 1201.802 1198.306 2021-01-22 04:44:00

318165 rows × 6 columns

In [7]:
raw_data_next = pd.read_csv('/opt/notebooks/data/HUOBI_ETH_NQ.csv', index_col='datetime')
raw_data_next['date_time'] =  pd.to_datetime(raw_data_next.index)
raw_data_next.index =  raw_data_next['date_time']
raw_data_next = raw_data_next.resample('2H').apply(ohlc)


tmp_df = raw_data_next[['volume', 'open', 'close', 'high', 'low']]
tmp_df['date_time'] = tmp_df.index


tmp_df.columns = [ 'Volume', 'Open', 'Close', 'High', 'Low', 'Date']
qf=cf.QuantFig(tmp_df, title='ETH 下季', legend='top',name='GS')
qf.add_bollinger_bands(boll_std=2.0)
qf.iplot()

raw_data_next
Out[7]:
open high low close volume
date_time
2020-06-15 06:00:00 226.433 226.907 222.322 226.907 23034.0
2020-06-15 08:00:00 226.356 227.361 225.412 226.651 13998.0
2020-06-15 10:00:00 226.660 226.938 224.768 226.140 15572.0
... ... ... ... ... ...
2021-01-22 00:00:00 1166.658 1198.236 1094.071 1187.318 5684482.0
2021-01-22 02:00:00 1187.319 1227.398 1176.083 1222.945 5866562.0
2021-01-22 04:00:00 1223.833 1254.621 1217.327 1224.108 1324586.0

2652 rows × 5 columns

什么都没看出来

就一些行情数据/ohlc数据,什么也看不出来啊。 但是如果我们:

In [13]:
kl_cur = raw_data[raw_data.index>='2020-06-15 06:00:00']
kl_next = raw_data_next[raw_data_next.index<='2021-01-22 04:00:00']

print('看看长度是否一致:' , len(kl_cur), len(kl_next))
kl = kl_cur.copy()
看看长度是否一致: 2652 2652
In [17]:
kl = kl_cur.copy()
kl.loc[:,'close'] = (kl_next.loc[:,'close'] - kl_cur.loc[:,'close']) / kl_cur.loc[:,'low']

kl['bollUPPER'], kl['bollMIDDLE'], kl['bollLOWER']  = talib.BBANDS(
                    #  close narray
                    kl['close'],
                    #  time default 20
                    timeperiod=200,
                    # number of non-biased standard deviations from the mean
                    nbdevup=2,  # 2,
                    nbdevdn=2,  # 2,
                    # Moving average type: simple moving average here
                    matype=0)

kl[['close', 'bollUPPER', 'bollMIDDLE', 'bollLOWER']].plot( figsize=(16,8))
Out[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f42798c75c0>

上面有一行关键的代码

kl.loc[:,'close'] = (kl_next.loc[:,'close'] - kl_cur.loc[:,'close']) / kl_cur.loc[:,'low']

它的关键作用就是拿下期合约收盘价减去当期合约收盘价,再除以当期合约的收盘价 ,最后计算出来的就是上图中的蓝线-close, 它围绕着绿色线上下波动。

我们还看到, 蓝色收盘价线80% - 90%的时间都是在橙色线和红色线圈出来的范围内波动的,很少出轨,即便偶尔出轨,也很大机率回到绿线附近 —— —— 就好比男人偶尔出轨,最终还是要回归家庭的。当然跟小三私奔可能小概率发生。

那么每次价格出轨时,我们可不可以赌它稍晚的时候会回归到绿线附近呢? 听起来是个好主意。 在回答“可不可以”之前,我想先回答另一问题。

“为什么搞量化交易这么复杂! 想炒个币还要会编程吗?!!”

回答是要的, 下面表格中的 1、2、3、4、5、6…… 都是需要会编程的, 上面的例子仅是下表的第3点。或者读完这篇文章后你可以想一下:刚才上图我们称之为“跟踪远近期合约的价格变化的衍生物” 这个需要自己写代码计算出来和画图的,在流行的炒币app找不到这种图,不会编程你怎么玩?

而且交易所也没有“跟踪远近期合约的价格变化的衍生物”这种衍生物产品可以直接交易,需要“逆向工程”为买卖远近期合约来达到交易这种衍生物的效果。

要解决的问题 \各派的做法 量化交易的做法 传统主观交易的做法
1.下海交易前如何知道即将使用的手法是否能获利? 回测(下文会讲到) 直接拿真金白银下海,生死由命
2.如何应对7 X 24不间断是市场? 都是计算机程序在执行 不吃不喝不睡呗
3.有些想知道数据和想看的图表在已有交易app中找不到怎么办? 自己写点代码呗 啊!这个真没办法。。。
4.币市价格变化太大,垃圾币0.x就能从黄金变成废纸怎么应对? 计算机cpu够快就行 啊!破产,玩完,本剧终。。
5.如何扩大规模(管理更多资金/账户)? 增加计算机/服务器

【表1:量化交易 vs 传统主观交易】

上面例子只是量化交易在的小儿科了,在做更深度的分析的时候,还需要掌握人工智能:

人工智能系的同学和距离成为有闲阶级一步之遥2

当然这又是另外一个比较深入的话题了,笔者有另外一个 notebook 专门讲这个的。

编程+机器学习这两个知识点就够做量化金融了吗? 在上面的小例子中,怎么画出橙色线和红色线 把80% - 90%时间的价格都圈在它们的范围内? 这就需要数学了。

那么随便找到一个会编程的程序员就能搞量化交易了吗? 这个图是量化交易员的面试/机试题 能回答这个问题。

ok, 回归正题: 那么每次价格出轨我们可不可以赌它稍晚的时候会回归到绿线附近呢? 这就回到 表1中第1项 —— 回测。回测也称作历史回测,就是按照某些交易逻辑(选股/标的、择时、仓位管理等)在历史数据上再走一次,输出收益曲线各项度量指标。 同样的交易手法在未来的交易中不一定和回测结果保持高度一致,但是往往很类似————基于人性不变、历史重演的基本事实。

给程序员和数据分析师的5分钟极速入坑量化交易课程 介绍了一个简单的回测过程。

以下就是根据上面“那么每次价格出轨我们可不可以赌它稍晚的时候会回归到绿线附近呢?” 设计的一个交易策略的回测结果 (因子代码和详细研究过程不透露哈):

In [8]:
ABuEnv.project_name = '_CQvsNQ_'
_g_project_id = ABuEnv.project_name


def fixed_commission(trade_cnt, price):
    global trade_cnts, prices, commissions
    trade_cnts.append(trade_cnt)
    prices.append(price)
    commission = trade_cnt * price * 0.002 
    commissions.append(commission)
    
    return 40
    
#     return trade_cnt * price * 0.002 * 6

    

# 手续费
commission_dict = {'buy_commission_func': fixed_commission,
                  'sell_commission_func': fixed_commission}

def run_load_train():
    global abu_result_tuple
    abu_result_tuple = abu.load_abu_result_tuple(n_folds=6.2, store_type=EStoreAbu.E_STORE_CUSTOM_NAME, 
                                 custom_name= _g_project_id + 'btcCQvsNQ-1H')
    AbuMetricsBase.show_general(*abu_result_tuple, only_show_returns=False).plot_max_draw_down()

def select(select):
    if select == 'run loop back':
        run_loop_back()
    else:
        run_load_train()

# _ = ipywidgets.interact_manual(select, select=[ 'load train data', 'run loop back'])
run_load_train()
买入后卖出的交易数量:127
买入后尚未卖出的交易数量:0
胜率:61.4173%
平均获利期望:6.8853%
平均亏损期望:-4.2872%
盈亏比:2.5983
策略收益: 196.1047%
基准收益: 572.6836%
策略年化收益: 9.9373%
基准年化收益: 29.0200%
策略买入成交比例:91.3386%
策略资金利用率比例:2.0963%
策略共执行4973个交易日