You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 lines
15 KiB
Markdown

2 years ago
# 26量化实战从0到1搭建起一套简单的量化投资系统
你好,我是王喆。
在“投资进阶篇”这个模块里我们已经学习了价值投资、趋势跟踪等6种投资策略。到了最后两讲就是我们在实战中把这些策略融会贯通的时候了。我会带你应用最近学习的投资策略搭建起一个简单的量化投资系统。
认真学习了前面课程的同学看到这里,应该会会心一笑:这一讲又到了发挥我们程序员技术优势的时候了。我们会在 [19讲](https://time.geekbang.org/column/article/410443) 介绍的量化投资框架基础上,搭建出一个包含了管理模块、策略模块、回测平台、数据中心、业绩分析模块和模拟交易模块在内的一整套量化投资系统。
当然,跟专业的系统比起来,它还稍显稚嫩,但也可以做到“麻雀虽小,五脏俱全”。我们这两节实战课,不是为了构建一个大而全的系统,而是要帮助有志于此的程序员同学迈上更高的台阶。学完这两讲,你可以**熟悉量化投资系统的框架,掌握关键模块的基本原理**,并以此为出发点,按照你自己的投资思想完善更多的细节。
## 温故知新:量化投资项目框架图
首先来和我一起回顾下量化投资系统的框架吧。下面的图1是我们在 [19讲](https://time.geekbang.org/column/article/410443) 介绍过的典型量化投资系统的框架它包括6个主要的模块。
其中“**管理模块**”是整个系统的管理者,负责调度所有的模块执行相应的任务;“**策略模块**”用来实现并执行不同的投资策略;“**交易模块**”执行具体的交易操作;“**回测平台**”类似于机器学习中的离线测试平台,用来试验各种新策略;“**业绩分析模块**”用来评判各个策略的盈亏情况和各个环节的运行情况;“**数据中心**”用来存储整个系统的所有基础数据。
![](https://static001.geekbang.org/resource/image/ea/3f/ea5eb9ddaf5130c37e7928699a1fce3f.jpg?wh=2248x1887 "图1 量化投资系统的框架图")
在接下来要讲的实战项目中我们将利用之前讲解过的日历策略以及二八轮动策略的满仓版本和可空仓版本构建针对沪深300、中证500以及中证1000三个指数的量化投资系统。这个系统的具体技术框图如图2所示
![](https://static001.geekbang.org/resource/image/71/9f/713a019ac14c696c6532d39f1e80729f.jpg?wh=2248x1855 "图2 量化投资项目的技术框图")
在具体的实现中我们将用Python实现所有的逻辑。为了方便你获取数据我们用Excel作为数据中心保存所有的行情和账户数据。整个项目的代码和数据我已经上传到了我们的[开源项目](https://github.com/wzhe06/SmartInvest/tree/master/quantTrading)中。为了方便你熟悉项目中的细节我建议你先把项目clone下来然后安装[Jupyter notebook](https://jupyter.org/install)在Jupyter中边测试代码边听我讲解。
## 先睹为快:来看下我们的成果
在开始讲细节之前,我想先给你看看构建这个量化系统能够得到的成果,让你有一个明确的目标。
你可以把我们的整个量化投资系统看成一只基金我们就叫它“量化一号”吧。那么我和李腾就是“量化一号”的“基金经理”通过多策略之间的量化操作帮你管钱。那这时你就要问了这只“量化一号”基金的收益率和风险特性怎么样呢我们来看一下它跟沪深300、中证500这些经典指数的对比情况
![图片](https://static001.geekbang.org/resource/image/f6/df/f6f05f0d8433ab946db1b195e31de6df.png?wh=1884x954)
![图片](https://static001.geekbang.org/resource/image/yy/e0/yyaf4208f382f4a1aab60a7c628fbde0.png?wh=864x230 "图3 量化一号account与沪深300指数hs300、中证500指数csi500的效果对比")
从图中的对比结果来看量化一号的表现还是非常优异的年化收益达到了18.8%大幅超过了沪深300的9.9%和中证500的12.2%而且最大回撤仅有29.8%也明显好于两个指数70%以上的最大回撤。那接下来,就跟着我一步步操作,了解量化一号的实现细节吧。
## 数据中心:熟悉我们的数据
首先我们来看一下数据中心。相关的数据你可以从GitHub项目中的[basic\_data地址](https://github.com/wzhe06/SmartInvest/blob/master/quantTrading/basic_data.csv)直接下载。这个行情数据分别保存了沪深300、中证500、中证1000、上证综指近20年来的日级数据。下图展示了行情数据的具体格式
![图片](https://static001.geekbang.org/resource/image/b4/6e/b4bcb6ff11421cffb170cb47dd23186e.png?wh=1920x1036 "图4 数据中心的行情数据格式")
你可以看到,这个数据格式非常简单直观,按天记录了四个指数的净值数据。我们后面的量化策略也会完全基于这四个指数的行情数据进行构建。
在量化系统的具体实现中我们将使用get\_hist\_data函数对行情数据进行读取。这个函数会根据你输入的指数代码列表和截止日期从basic\_data.csv中读取相应的数据以pandas dataframe的形式返回供其他模块使用具体的实现请参照下面的代码。
```python
# 数据接口
def get_hist_data(index_ids=None, end_date=None):
    """
    读取指数历史数据到指定截止日
    Input:
        index_ids: list of str, 指数代码列表, like ['hs300', 'csi500']
        end_date: datetime.date, 截止日期
    Output:
        data: df(date*, index1, index2, ...), 多个指数的历史收盘价序列
    """
    # 从csv文件获取指数价格数据
    data = pd.read_csv('basic_data.csv').set_index('datetime')
    data.index = [datestr2dtdate(e) for e in data.index]
    print('基础数据起止日期:%s%s' % (data.index[0],data.index[-1]))
    if index_ids is not None:
        data = data.loc[:,index_ids]
    if end_date is not None:
        data = data.loc[:end_date,:]
    return data
```
## 策略模块:探索策略实现的细节
我要讲的第二个模块是策略模块。我们将在系统中实现三个策略,分别是我们在[18讲](https://time.geekbang.org/column/article/409852)介绍的日历策略,以及在[22讲](https://time.geekbang.org/column/article/412855)介绍的二八轮动策略的满仓版本和可空仓版本。如果你觉得自己还不太熟悉这些策略的原理,可以先回到相应的那一讲复习下。
这里,我以二八轮动的可空仓版本为例,来讲解一下策略实现的细节。
我们首先需要明确的是rotation\_stgy1这个二八轮动策略函数的输入输出。输入有四个参数分别是我们在上一步准备好的历史行情数据data策略开始的起始时间start\_date和结束时间end\_date以及策略中需要的参数params。在这个策略中通过params传入策略函数中的参数是N它代表了我们在进行二八轮动时需要查看过去多少天的历史行情数据。
```python
# 轮动策略(可空仓版)
def rotation_stgy1(data, start_date, end_date, params):
    """
    开盘前调用,返回目标组合权重
    Input:
        data: df(date*, index1, index2, ...), basic data
        start_date, end_date: 'yyyy-mm-dd' or datetime.date
        params: dict, format {'index_list':['N':20}
    Output: 
        target_wgt: df(trade_date*, index1, index2, ...) 目标权重
    """
    if type(start_date) is str:
        start_date = datestr2dtdate(start_date)
    if type(end_date) is str:
        end_date = datestr2dtdate(end_date)
    N = params['N']
    
    start_date0 = start_date - datetime.timedelta(N)*2
    dates0 = get_trading_dates(start_date0, end_date)
    data0 = data.reindex(index=dates0)
    N_day_ret = data0.shift(1) / data0.shift(N+1) - 1 # 截止昨收的最近N个交易日涨幅
    target_wgt = pd.DataFrame(0, index=data0.index, columns=data0.columns)
    for i in range(1,len(target_wgt)):
        t = target_wgt.index[i]
        t0 = target_wgt.index[i-1]
        if N_day_ret.loc[t0,'hs300'] >= N_day_ret.loc[t0,'csi500'] and N_day_ret.loc[t0,'hs300']>0:
            target_wgt.loc[t,'hs300'] = 1
        elif N_day_ret.loc[t0,'hs300'] < N_day_ret.loc[t0,'csi500'] and N_day_ret.loc[t0,'csi500']>0:
            target_wgt.loc[t,'csi500'] = 1
    target_wgt = target_wgt.loc[start_date:end_date].fillna(0)
    
    return target_wgt
```
那么上面的代码中策略函数的返回值target\_wgt是什么呢如下面的图5所示它其实是一个存储了策略相关各标的仓位的dataframe。比如图片中的2021年7月28号这一天只有中证500指数的仓位是0.5这就意味着在那天我们应该把资金的50%配置在中证500指数上而空仓其他指数。
![](https://static001.geekbang.org/resource/image/ed/8c/edec7463bac9266a711b44745c86b68c.png?wh=2248x2471 "图5 策略函数的返回值")
清楚了输入和输出,下一步我们要搞清楚的就是实现策略的核心代码。
在上面的代码中我们要重点看的是第26行和28行的两个条件判断表达式。其中t0是上一个交易日的时间标识N\_day\_ret是最近N个交易日的区间涨跌幅所以第一个表达式的含义就是最近N个交易日区间上沪深300的涨跌幅高于中证500的涨跌幅并且沪深300的涨跌幅为正值。此时我们就应该满仓沪深300。
相应的第二个表达式的含义就是最近N个交易日区间上中证500的涨跌幅高于沪深300的涨跌幅并且中证500的涨跌幅为正值。此时我们就满仓中证500。对于其他情形则一律空仓也就是既不持有中证500也不持有沪深300。所以直观来说就是要看最近N个交易日的涨跌幅哪边高投哪边但如果高的那边也是负收益我们就空仓。
到这里,我就讲清楚了二八轮动可空仓策略的核心逻辑。对于其他两个策略,你也可以用类似的方式,去代码中弄清楚它们的细节。
## 回测平台:找到策略的最优参数
为了确定各个策略的参数比如二八轮动中的参数N我们还需要构建一个回测平台通过不断的回测来找到参数的最佳值。
下面的代码就展示了回测平台的主要逻辑,我已经在关键的地方添加了注释,建议你先读一遍注释和代码,再跟我一起梳理下关键的逻辑。
```python
# 设置回测参数
start_date = datetime.date(2004,12,31) # 回测起始日期
end_date = datetime.date(2021,7,31) # 回测截止日期
# 读取基础数据
data = get_hist_data(end_date=end_date) 
# 调用策略模块生成目标组合权重
target_wgt1 = calendar_stgy(data, start_date, end_date, params={'index_id':'hs300', 't1':1, 't2':5})
target_wgt2 = calendar_stgy(data, start_date, end_date, params={'index_id':'csi1000', 't1':1, 't2':5})
target_wgt3 = rotation_stgy(data, start_date, end_date, params={'N':20})
target_wgt4 = rotation_stgy1(data, start_date, end_date, params={'N':20})
target_wgt = 0*target_wgt1 + 0.5*target_wgt2 + 0*target_wgt3 + 0.5*target_wgt4 # 多策略目标组合整合
# 产生每日持仓权重
hold_wgt = target_wgt # 假设每天都可以准确地执行交易计划
# 计算组合业绩
asset_ret = data.pct_change().loc[start_date:end_date]
res = (1+asset_ret).cumprod()
res['account'] = (1+(hold_wgt.shift(1) * asset_ret).sum(axis=1)).cumprod()
# 展示净值曲线图和业绩指标表
res.loc[:,['hs300','csi500','account']].plot(figsize=(16,8), grid=True)
cal_period_perf_indicator(res.loc[:,['hs300','csi500','account']])
```
在回测过程中,我们首先要做的当然是**准备数据**。这里我们利用get\_hist\_data函数准备好了起止时间区间内的历史数据。在真实的回测过程中你也可以根据特定策略来选择起止时间比如对于一些时效性强的策略就不太适宜使用过长的历史数据。
回测的第二步是**调用不同的策略函数,来生成不同策略的历史仓位记录**。比如这里我调用了两个日历策略calendar\_stgy和两个轮动策略rotation\_stgy和rotation\_stgy1生成了它们的历史仓位记录。回测平台是我们专门用于策略调参的地方。你可以输入不同的策略参数来生成大量的策略持仓记录然后通过后续的业绩分析模块来进行业绩对比最终确定最优的策略参数。
第三步是**针对组合策略进行操作**。如果你想开发的不是单一的策略而是由几个策略混合起来的组合策略那么就还需要确定不同策略之间的权重占比。比如程序中的最终执行仓位target\_wgt就是由50%的日历策略仓位target\_wgt2和50%的可空仓轮动策略仓位target\_wgt4混合而成的。在真实策略的回测过程中这里的权重也是需要通过大量的回测和业绩对比来决定的。
第四步是**进行业绩分析并显示结果**。这里我们根据上面计算好的策略历史仓位和数据中心提供的历史行情数据就可以计算出各策略的净值数据和业绩指标然后调用Python的Matplotlib把曲线画出来就行了。至于分析业绩的详细过程我会在下一讲介绍业绩分析模块的逻辑时讲解。
## 小结
今天我们就先学到这里。这一讲,我讲解了量化系统的三个模块,会在下一讲继续进行量化投资系统中管理模块、业绩分析模块和模拟交易模块的讲解。在这里,我总结了今天这一讲的几个要点,供你再温习一遍:
1. 我们要实现的量化投资系统包含了**管理模块**、**策略模块**、**回测平台**、**数据中心**、**业绩分析和模拟交易**六大模块。
2. 我们的系统会利用日历策略以及二八轮动策略的满仓版本和可空仓版本针对沪深300指数、中证500指数和中证1000指数构建量化策略。
3. 通过对16年数据的回测可以看到我们构建的“量化一号”实现了18.8%的年化收益率和29.8%的最大回撤和中证500、沪深300的原始指数相比均有较大改善。
4. 今天我重点讲解的是数据中心、策略模块、回测平台这三个模块的实现细节,希望你能够对照源码再过一遍,加深理解。
下一讲,我们将继续进行量化实战项目的讲解,期待和你一起完成一个简单但功能完备的量化投资系统。
## 思考题
你能通过修改代码验证二八轮动策略在不同参数下的效果吗比如可空仓版本的二八轮动策略在N=10N=20N=30时的年化收益分别是多少
欢迎你在留言区与我交流讨论,我们下一讲见。