一种简化的截面动量组合测试[Python&MATLAB]
下面的例子来源书籍<Python for Data Analysis>策略比较简单,使用N只股票构建投资组合,计算每只股票的过去LookBack(参数)日的动量,并作标准化处理作为股票权重,持有Holding(参数)日,其中权重允许为负数,即允许做空股票。简要说来就是做过过去表现强势的股票,做空过去表现弱势的股票。虽然策略比较简单,但整体的测试流程和框架很明了清晰,对类似策略的回测实现具有参考意义,整体的流程框架为:
数据获取(基于付费或者免费的数据源)——》
数据的时间轴对齐以及缺失数据填充——》
子函数编写:特定回顾期的动量计算并作标准化——》
子函数编写:给定回顾期和持有期,组合的夏普比例计算——》
计算不同回顾期和持有期下组合的夏普比例值——》
进行策略参数分布图形展示
一、Python下的实现测试。
(1)主要用到numpy、pandas、matplotlib等几个包
- # -*- coding: utf-8 -*-
- """
- SimpleMomentumPortfolioTest
- Created on 2015/05/01
- @author: LiYang(faruto)
- @group : FQuantStudio
- @contact: farutoliyang@foxmail.com
- """
- #%% import
- import numpy as np
- import pandas as pd
- import pandas.io.data as web
- import matplotlib.pylab as plt
- from pandas import Series,DataFrame
- from collections import defaultdict
(2)数据的获取基于pandas包
- #%% GetData
- names = ['AAPL', 'GOOGL', 'MSFT', 'IBM', 'GS', 'MS', 'BAC', 'C']
- def get_px(stock, start, end):
- return web.get_data_yahoo(stock, start, end)['Adj Close']
- px = DataFrame({n: get_px(n, '1/1/2005', '1/1/2015') for n in names})
- px.plot()
(3)数据的时间轴对齐以及缺失数据填充
- #%% plot
- px = px.asfreq('B').fillna(method='pad')
- rets = px.pct_change()
- retcum = (1+rets).cumprod()-1
- retcum.plot()
(4)子函数编写:特定回顾期的动量计算并作标准化
- #%% calc_mom
- def calc_mom(price, lookback, lag):
- mom_ret = price.shift(lag).pct_change(lookback)
- ranks = mom_ret.rank(axis=1, ascending=False)
- demeaned = ranks.subtract(ranks.mean(axis=1), axis=0)
- return demeaned.divide(demeaned.std(axis=1), axis=0)
当然这里是做了排序后把排序变量做标准化后返回。
(5)子函数编写:给定回顾期和持有期,组合的夏普比例计算
- #%% strat_sr
- compound = lambda x : (1 + x).prod() - 1
- daily_sr = lambda x: x.mean() / x.std()
- def strat_sr(prices, lb, hold):
- # Compute portfolio weights
- freq = '%dB' % hold
- port = calc_mom(prices, lb, lag=1)
- daily_rets = prices.pct_change()
- # Compute portfolio returns
- port = port.shift(1).resample(freq, how='first')
- returns = daily_rets.resample(freq, how=compound)
- port_rets = (port * returns).sum(axis=1)
- return daily_sr(port_rets) * np.sqrt(252 / hold)
- strat_sr(px, 70, 30)
(6)计算不同回顾期和持有期下组合的夏普比例值
- #%% calc with diff lookbacks and holdings
- lookbacks = range(20, 90, 5)
- holdings = range(20, 90, 5)
- dd = defaultdict(dict)
- for lb in lookbacks:
- for hold in holdings:
- dd[lb][hold] = strat_sr(px, lb, hold)
- ddf = DataFrame(dd)
- ddf.index.name = 'Holding Period'
- ddf.columns.name = 'Lookback Period'
- (7)进行策略参数分布图形展示
- #%% heatmap
- def heatmap(df, cmap=plt.cm.gray_r):
- fig = plt.figure()
- ax = fig.add_subplot(111)
- axim = ax.imshow(df.values, cmap=cmap, interpolation='nearest')
- ax.set_xlabel(df.columns.name)
- ax.set_xticks(np.arange(len(df.columns)))
- ax.set_xticklabels(list(df.columns))
- ax.set_ylabel(df.index.name)
- ax.set_yticks(np.arange(len(df.index)))
- ax.set_yticklabels(list(df.index))
- plt.colorbar(axim)
- heatmap(ddf)
通过上图可以看到在此例下,大概回顾期为55-60日,持有期为35-40日,会获取较高的夏普比率。
总结:可以看到在Python下使用pandas包整体的实现测试过程很简单明了。Pandas是个进行数据分析处理很赞的包。
二、MATLAB下的实现测试。
下面在来看下在MATLAB下的实现测试过程,不同的语言各有利弊,并无本质上哪个好,那个坏之说,本例中,由于Python下的pandas包的帮助,在Python下的代码更加简洁明了,MATLAB下代码稍显臃肿,当然也可以仿照Python下的pandas包,实现一个MATLAB下的pandas包,让相关的数据处理更加简洁方便。虽然MATLAB下的金融工具箱有fints(financial time series类)、timeseries类,但相关的实现和处理在效率和使用上并不是特别好,所以时序相关的处理我还是自己实现的,并没有全部使用MATLAB自带的一些包。
注:MATLAB的实现测试使用的A股的数据,数据的获取基于FQuantToolBox
(1)函数说明
- function SimpleMomentumPortfolioTest
- % by LiYang_faruto
- % Email:farutoliyang@foxmail.com
- % 2015/01/01
- %% A Little Clean Work
- % clear;
- % clc;
- % close all;
- format compact;
(2)数据的获取基于FQuantToolBox
- %% GetDataFromWeb
- tic;
- StockCodeCell = {'600588sh','sh600030','600446','300024sz','sz000001','600570sh'};
- StockNameCell = {'用友网络','中信证券','金证股份','机器人','平安银行','恒生电子'};
- BeginDate = '20100101';
- EndDate = '20150101';
- Len = length(StockCodeCell);
- StockDataCell = cell(length(StockCodeCell),1);
- for i = 1:length(StockCodeCell)
- StockCode = StockCodeCell{i};
- [StockDataCell{i}] = GetStockTSDay_Web(StockCode,BeginDate,EndDate);
- end
- toc;
- %% 前复权数据生成
- StockDataCellXRD = StockDataCell;
- for i = 1:Len
- StockData = StockDataCell{i};
- AdjFlag = 1;
- [StockDataCell{i}] = CalculateStockXRD(StockData, [], AdjFlag);
- end
- (3)数据的时间轴对齐以及缺失数据填充
- tic;
- sdate = datenum(BeginDate,'yyyymmdd');
- edate = datenum(EndDate,'yyyymmdd');
- bdates = busdays(sdate, edate, 'Daily');
- Bdates = str2num( datestr(bdates,'yyyymmdd') );
- StockDataCell_pre = StockDataCell;
- for i = 1:Len
- tMat = zeros(length(Bdates),8);
- tMat(:,1) = Bdates;
-
- tMat_pre = StockDataCell{i};
- for j = 1:length(Bdates)
- tD = Bdates(j);
- ind = find(tMat_pre(:,1)<=tD, 1,'last');
- tMat(j,2:end) = tMat_pre(ind,2:end);
- end
-
- StockDataCell{i} = tMat;
- end
- toc;
- %% 每只股票累计收益
- StockMat = zeros(length(Bdates), Len+1);
- StockMat(:,1) = Bdates;
- for i = 1:Len
- StockMat(:,i+1) = StockDataCell{i}(:,5);
- end
- Ret = tick2ret(StockMat(:,2:end));
- CumRet = cumprod((1+Ret))-1;
- scrsz = get(0,'ScreenSize');
- figure('Position',[scrsz(3)*1/4 scrsz(4)*1/6 scrsz(3)*4/5 scrsz(4)]*3/4);
- plot(CumRet,'LineWidth',1.5);
- xlim([0,length(Bdates)+1]);
- Dates = StockMat(2:end,1);
- LabelSet(gca, Dates, [], [], 1);
- M = StockNameCell;
- H = legend(M);
- H.Orientation = 'horizontal';
- H.FontWeight = 'Bold';
- H.FontSize = 12;
- H.Location = 'northoutside';
- str = '股票累计收益';
- H = title(str);
- H.FontWeight = 'Bold';
- H.FontSize = 15;
- tic;
- sdate = datenum(BeginDate,'yyyymmdd');
- edate = datenum(EndDate,'yyyymmdd');
- bdates = busdays(sdate, edate, 'Daily');
- Bdates = str2num( datestr(bdates,'yyyymmdd') );
- StockDataCell_pre = StockDataCell;
- for i = 1:Len
- tMat = zeros(length(Bdates),8);
- tMat(:,1) = Bdates;
-
- tMat_pre = StockDataCell{i};
- for j = 1:length(Bdates)
- tD = Bdates(j);
- ind = find(tMat_pre(:,1)<=tD, 1,'last');
- tMat(j,2:end) = tMat_pre(ind,2:end);
- end
-
- StockDataCell{i} = tMat;
- end
- toc;
- %% 每只股票累计收益
- StockMat = zeros(length(Bdates), Len+1);
- StockMat(:,1) = Bdates;
- for i = 1:Len
- StockMat(:,i+1) = StockDataCell{i}(:,5);
- end
- Ret = tick2ret(StockMat(:,2:end));
- CumRet = cumprod((1+Ret))-1;
- scrsz = get(0,'ScreenSize');
- figure('Position',[scrsz(3)*1/4 scrsz(4)*1/6 scrsz(3)*4/5 scrsz(4)]*3/4);
- plot(CumRet,'LineWidth',1.5);
- xlim([0,length(Bdates)+1]);
- Dates = StockMat(2:end,1);
- LabelSet(gca, Dates, [], [], 1);
- M = StockNameCell;
- H = legend(M);
- H.Orientation = 'horizontal';
- H.FontWeight = 'Bold';
- H.FontSize = 12;
- H.Location = 'northoutside';
- str = '股票累计收益';
- H = title(str);
- H.FontWeight = 'Bold';
- H.FontSize = 15;
(4)子函数编写:特定回顾期的动量计算并作标准化
- %% sub fun calc_mom
- % ---------------------------------------------------
- % calc_mom
- % ---------------------------------------------------
- function weight = calc_mom(price,lookback)
- weight = zeros(size(price));
- weight(:,1) = price(:,1);
- [m,n] = size(price);
- % weight(1:lookback,2:end) = nan;
- weight(1:lookback,2:end) = 0;
- for j = lookback+1:m
- for i = 2:n
- tData = price(:,i);
- weight(j,i) = (tData(j-1)-tData(j-lookback))/tData(j-lookback);
- end
- temp = weight(j,2:end);
- weight(j,2:end) = (temp-mean(temp))./std(temp);
- end
-
- weight(isnan(weight)) = 0;
- end
当然这里与Python下稍有不同,就使用标准化后的动量值返回。
(5)子函数编写:给定回顾期和持有期,组合的夏普比例计算
- %% sub fun strat_sr
- % ---------------------------------------------------
- % strat_sr
- % ---------------------------------------------------
- function SR = strat_sr(prices, lb, hold)
- SR = 0;
- [m,n] = size(prices);
- % 计算权重
- port = calc_mom(prices,lb);
- port(isnan(port)) = 0;
-
- % 计算组合收益
- PortResample = [];
- Returns = [];
- Ind = 1;
- for i = hold:hold:m
- PortResample(Ind,:) = port(i-hold+1,:);
- Returns(Ind,:) = prices(i,:);
- Returns(Ind,2:end) = (prices(i,2:end)-prices(i-hold+1,2:end))./prices(i-hold+1,2:end);
- Ind = Ind + 1;
- end
- port_rets = PortResample(:,2:end).*Returns(:,2:end);
- port_rets = sum(port_rets,2);
- % 计算年化Sharpe Ratio
- SR = mean(port_rets)/std(port_rets)*sqrt( 252/hold );
- end
- (6)计算不同回顾期和持有期下组合的夏普比例值
- %% calc
- tic;
- lookbacks = 20:5:90;
- holdings = 20:5:100;
- DD = zeros(length(lookbacks), length(holdings));
- for i = 1:length(lookbacks)
- for j = 1:length(holdings)
- lb = lookbacks(i);
- hold = holdings(j);
- DD(i,j) = strat_sr(StockMat, lb, hold);
- end
- end
- toc;
- (7)进行策略参数分布图形展示
- %% HeatPlot
- temp = num2cell(lookbacks);
- temp = cellfun(@num2str,temp,'UniformOutput',false);
- YVarNames = temp;
- temp = num2cell(holdings);
- temp = cellfun(@num2str,temp,'UniformOutput',false);
- XVarNames = temp;
- XLabelString = 'Holding Period';
- YLabelString = 'Lookack Period';
- Fmatrixplot(DD,'ColorBar','On','XVarNames',XVarNames,'YVarNames',YVarNames,...
- 'XLabelString',XLabelString,'YLabelString',YLabelString);
通过上图可以看到在此例下,大概回顾期为25-35日,持有期为60日,会获取较高的夏普比率。
总结
本文给出了一个简化的截面动量组合测试,虽然策略比较简单,但整体的测试流程和框架很明了清晰,对类似策略的回测实现具有参考意义,整体的流程框架为:
数据获取(基于付费或者免费的数据源)——》
数据的时间轴对齐以及缺失数据填充——》
子函数编写:特定回顾期的动量计算并作标准化——》
子函数编写:给定回顾期和持有期,组合的夏普比例计算——》
计算不同回顾期和持有期下组合的夏普比例值——》
进行策略参数分布图形展示
在Python下pandas是个数据处理非常不错的一个包,另外在Python下免费的A股数据可以通过tushare 包(作者Jimmy)获取,tushare 包下载地址:http://tushare.waditu.com/。
在MATLAB下,虽然此例的实现测试稍显臃肿,但也不是非常复杂。在MATLAB下免费的A股数据可以通过FQuantToolBox(作者faruto)获取,FQuantToolBox下载地址:
https://bbs.pinggu.org/thread-3567352-1-1.html
更多内容请点击“阅读原文” 本公众号由李洋(微信faruto)维护。更多量化投资、MATLAB应用内容可以关注:MATLAB技术论坛:
http://www.matlabsky.com/
李洋(faruto)的微博、博客
http://weibo.com/faruto
http://blog.sina.com.cn/faruto
===分享给朋友===
点击右上角,在弹出菜单中选择“发送给朋友”或“分享到朋友圈”
===订阅FQuantStudio公众号===https://bbs.pinggu.org/thread-3567352-1-1.html