【量價因子 - 結合價格和成交量構建選股策略】整理轉分享

2022年12月17日14:04:04 財經 1241

1. 概述


本文以海通證券《選股因子系列研究(十二)——「量」與「價」的結合》的研究方法為模板,試圖分析量價相關關係作為因子的效果:

  • 股票在短期內的量價走勢分類為量價背離與量價同向,並通過量價相關性來衡量量價走勢的背離與同向程度
  • 按照量價因子選股的月度多空收益在1%以上,得到了很顯著的alpha
  • 純多頭組合在六年回測中年化收益達到22.4%,信息比率達到2.22
  • 量價因子等權疊加了反轉因子後,六年回測年化收益達到26.0%,信息比率達到2.55

2. 量價因子構建


股票交易中,最顯然的指標無非價格成交量,大多經典的技術指標其實都是圍繞著價格和成交量來構建,本文中嘗試將這兩者結合起來構建量價因子。中短周期上,量價走勢分類為量價背離與量價同向,並通過量價相關性來衡量量價走勢的背離與同向的程度。因此,量價相關性,也就是本文中的量價因子,可以簡單定義為:

  • 一段時間窗口內,股票收盤價與股票日換手率之間的秩相關係數

本文中的量價相關係數計算,採取的時間窗口為15個交易日

下面給出本文中用來計算量價因子的程序代碼

import matplotlib.pyplot as plt
from matplotlib import rc
from matplotlib import dates
rc('mathtext', default='regular')
import seaborn as sns
sns.set_style('white')
import datetime
import numpy as np
import pandas as pd
import time
import scipy.stats as st
from CAL.PyCAL import *    # CAL.PyCAL中包含font
def getVolPriceCorrAll(universe, begin, end, window, file_name):
    # 計算各股票歷史區間window天窗口移動的量價相關係數
    
    # 拿取上海證券交易所日曆
    cal_dates = DataAPI.TradeCalGet(exchangeCD=u"XSHG", beginDate=begin, endDate=end).sort('calendarDate')
    cal_dates = cal_dates[cal_dates['isOpen']==1]
    all_dates = cal_dates['calendarDate'].values.tolist()                          # 工作日列表
    
    print str(window) + ' days Price-Volume-Corr will be calculated for ' + str(len(universe)) + ' stocks:'
    count = 0
    secs_time = 0
    start_time = time.time()
    
    ret_data = pd.DataFrame()   # 保存計算出來的收益率數據
    ret_data.to_csv(file_name)
    
    N = 10
    for i in range(len(universe)/N+1):
        sub_univ = universe[i*N:(i+1)*N]
        if len(sub_univ) == 0:
            continue
        data = DataAPI.MktEqudAdjGet(secID=sub_univ, beginDate=begin, endDate=end, 
                                     field='secID,tradeDate,turnoverRate,preClosePrice,closePrice')    # 拿取數據
        for stk in sub_univ:        # 對每一隻股票分別計算歷史window天前望收益率     
            tmp_ret_data = data[data.secID==stk].sort('tradeDate')
            
            corr_data = range(len(tmp_ret_data))
            for i in range(window-1, len(tmp_ret_data)):
                x = tmp_ret_data['turnoverRate'].values[i-window+1:i+1]
                y = tmp_ret_data['closePrice'].values[i-window+1:i+1]
                corr_data[i] = st.spearmanr(x, y)[0]
                
            # 計算前向收益率
            tmp_ret_data['corr'] = corr_data
            tmp_ret_data = tmp_ret_data[['tradeDate','corr']]
            tmp_ret_data.columns = ['tradeDate', stk]


            ret_data = pd.read_csv(file_name)
            if ret_data.empty:
                ret_data = tmp_ret_data
            else:
                ret_data = ret_data[ret_data.columns[1:]]
                ret_data = ret_data.merge(tmp_ret_data, on='tradeDate', how='outer')


            ret_data = ret_data.sort('tradeDate')
            ret_data.to_csv(file_name)
            
        # 列印進度部分
        count += 1
        if count > 0 and count % 2 == 0:
            finish_time = time.time()
            print count*N,
            print '  ' + str(np.round((finish_time-start_time) - secs_time, 0)) + ' seconds elapsed.'
            secs_time = (finish_time-start_time)
    return ret_data


def getBackwardReturnsAll(universe, begin, end, window, file_name):
    # 計算各股票歷史區間回報率,過去window天的收益率
    
    print str(window) + ' days backward returns will be calculated for ' + str(len(universe)) + ' stocks:'
    count = 0
    secs_time = 0
    start_time = time.time()
    
    N = 50
    ret_data = pd.DataFrame()
    for stk in universe:
        data = DataAPI.MktEqudAdjGet(secID=stk, beginDate=begin, endDate=end, 
                                     field='secID,tradeDate,closePrice')    # 拿取數據
        tmp_ret_data = data.sort('tradeDate')
        # 計算歷史窗口收益率
        tmp_ret_data['forwardReturns'] = tmp_ret_data['closePrice'] / tmp_ret_data['closePrice'].shift(window) - 1.0
        tmp_ret_data = tmp_ret_data[['tradeDate','forwardReturns']]
        tmp_ret_data.columns = ['tradeDate', stk]


        if ret_data.empty:
            ret_data = tmp_ret_data
        else:
            ret_data = ret_data.merge(tmp_ret_data, on='tradeDate', how='outer')


        # 列印進度部分
        count += 1
        if count > 0 and count % N == 0:
            finish_time = time.time()
            print count,
            print '  ' + str(np.round((finish_time-start_time) - secs_time, 0)) + ' seconds elapsed.'
            secs_time = (finish_time-start_time)
    
    ret_data.to_csv(file_name)
    return ret_data


def getForwardReturnsAll(universe, begin, end, window, file_name):
    # 計算各股票歷史區間前瞻回報率,未來window天的收益率
    
    print str(window) + ' days forward returns will be calculated for ' + str(len(universe)) + ' stocks:'
    count = 0
    secs_time = 0
    start_time = time.time()
    
    N = 50
    ret_data = pd.DataFrame()
    for stk in universe:
        data = DataAPI.MktEqudAdjGet(secID=stk, beginDate=begin, endDate=end, 
                                     field='secID,tradeDate,closePrice')    # 拿取數據
        tmp_ret_data = data.sort('tradeDate')
        # 計算歷史窗口前瞻收益率
        tmp_ret_data['forwardReturns'] = tmp_ret_data['closePrice'].shift(-window) / tmp_ret_data['closePrice']  - 1.0
        tmp_ret_data = tmp_ret_data[['tradeDate','forwardReturns']]
        tmp_ret_data.columns = ['tradeDate', stk]


        if ret_data.empty:
            ret_data = tmp_ret_data
        else:
            ret_data = ret_data.merge(tmp_ret_data, on='tradeDate', how='outer')


        # 列印進度部分
        count += 1
        if count > 0 and count % N == 0:
            finish_time = time.time()
            print count,
            print '  ' + str(np.round((finish_time-start_time) - secs_time, 0)) + ' seconds elapsed.'
            secs_time = (finish_time-start_time)
    
    ret_data.to_csv(file_name)
    return ret_data




def getMarketValueAll(universe, begin, end, file_name):
    # 獲取股票歷史每日市值
    
    print  'MarketValue will be calculated for ' + str(len(universe)) + ' stocks:'
    count = 0
    secs_time = 0
    start_time = time.time()
    
    N = 50
    ret_data = pd.DataFrame()
    for stk in universe:
        data = DataAPI.MktEqudAdjGet(secID=stk, beginDate=begin, endDate=end, 
                                     field='secID,tradeDate,marketValue')    # 拿取數據
        tmp_ret_data = data.sort('tradeDate')
        # 市值部分
        tmp_ret_data = tmp_ret_data[['tradeDate','marketValue']]
        tmp_ret_data.columns = ['tradeDate', stk]


        if ret_data.empty:
            ret_data = tmp_ret_data
        else:
            ret_data = ret_data.merge(tmp_ret_data, on='tradeDate', how='outer')


        # 列印進度部分
        count += 1
        if count > 0 and count % N == 0:
            finish_time = time.time()
            print count,
            print '  ' + str(np.round((finish_time-start_time) - secs_time, 0)) + ' seconds elapsed.'
            secs_time = (finish_time-start_time)
    
    ret_data.to_csv(file_name)
    return ret_data


def getWindowMeanTurnoverRateAll(universe, begin, end, window, file_name):
    # 獲取股票歷史滾動窗口平均換手率
    
    print  'windowMeanTurnoverRate will be calculated for ' + str(len(universe)) + ' stocks:'
    count = 0
    secs_time = 0
    start_time = time.time()
    
    N = 100
    ret_data = pd.DataFrame()
    for stk in universe:
        data = DataAPI.MktEqudAdjGet(secID=stk, beginDate=begin, endDate=end, 
                                     field='secID,tradeDate,turnoverRate')    # 拿取數據
        tmp_ret_data = data.sort('tradeDate')
        # 市值部分
        tmp_ret_data['windowMeanTurnoverRate'] = pd.rolling_mean(tmp_ret_data['turnoverRate'], window=window)
        tmp_ret_data = tmp_ret_data[['tradeDate','windowMeanTurnoverRate']]
        tmp_ret_data.columns = ['tradeDate', stk]


        if ret_data.empty:
            ret_data = tmp_ret_data
        else:
            ret_data = ret_data.merge(tmp_ret_data, on='tradeDate', how='outer')


        # 列印進度部分
        count += 1
        if count > 0 and count % N == 0:
            finish_time = time.time()
            print count,
            print '  ' + str(np.round((finish_time-start_time) - secs_time, 0)) + ' seconds elapsed.'
            secs_time = (finish_time-start_time)
    
    ret_data.to_csv(file_name)
    return ret_data

上面分別定義了計算本文關心的幾個變數的函數,其中包括:

  • 價量相關係數,getVolPriceCorrAll
  • 歷史收益率,getBackwardReturnsAll
  • 未來收益率,getForwardReturnsAll
  • 市值,getMarketValueAll
  • 歷史窗口日均換手率,getWindowMeanTurnoverRateAll

下面利用這五個函數分別計算我們需要的各種變數(我們只用了全A股中的50隻作為示例,感興趣的讀者只需要將下面cell中第5行中的universe修改即可計算更大股票池的數據),並將這些變數保存在文件中以供調用。

begin_date = '20060101'  # 開始日期
end_date = '20160802'    # 結束日期


universe = set_universe('A')      # 股票池
universe = universe[0:50]         # 計算速度緩慢,僅以部分股票池作為sample


# ----------- 計算量價相關係數部分 ----------------
window_corr = 15                
print '======================='
start_time = time.time()
forward_returns_data = getVolPriceCorrAll(universe=universe, begin=begin_date, end=end_date, window=window_corr, file_name='VolPriceCorr_W15_FullA_sample.csv')
finish_time = time.time()
print ''
print str(finish_time-start_time) + ' seconds elapsed in total.'


# ----------- 計算股票歷史窗口(一個月)收益率部分 ----------------
window_return = 20                
print '======================='
start_time = time.time()
forward_returns_data = getBackwardReturnsAll(universe=universe, begin=begin_date, end=end_date, window=window_return, file_name='BackwardReturns_W20_FullA_Sample.csv')
finish_time = time.time()
print ''
print str(finish_time-start_time) + ' seconds elapsed in total.'


# ----------- 計算股票歷史窗口(三個月)收益率部分 ----------------
window_return = 60                
print '======================='
start_time = time.time()
forward_returns_data = getBackwardReturnsAll(universe=universe, begin=begin_date, end=end_date, window=window_return, file_name='BackwardReturns_W60_FullA_Sample.csv')
finish_time = time.time()
print ''
print str(finish_time-start_time) + ' seconds elapsed in total.'


# ----------- 計算股票前瞻收益率部分 ----------------
window_return = 20                
print '======================='
start_time = time.time()
forward_returns_data = getForwardReturnsAll(universe=universe, begin=begin_date, end=end_date, window=window_return, file_name='ForwardReturns_W20_FullA_Sample.csv')
finish_time = time.time()
print ''
print str(finish_time-start_time) + ' seconds elapsed in total.'


# ----------- 計算股票歷史市值部分 ----------------
print '======================='
start_time = time.time()
forward_returns_data = getMarketValueAll(universe=universe, begin=begin_date, end=end_date, file_name='MarketValues_FullA_Sample.csv')
finish_time = time.time()
print ''
print str(finish_time-start_time) + ' seconds elapsed in total.'


# ----------- 計算歷史月度日均換手率部分 ----------------
window = 20                
print '======================='
start_time = time.time()
forward_returns_data = getWindowMeanTurnoverRateAll(universe=universe, begin=begin_date, end=end_date, window=window, file_name='TurnoverRateWindowMean_W20_FullA_Sample.csv')
finish_time = time.time()
print ''
print str(finish_time-start_time) + ' seconds elapsed in total.'

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

3. 量價因子截面特徵


3.1 首先載入計算好的數據文件:

# 提取數據
corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv')                    # 15天窗口量價相關係數
forward_20d_return_data = pd.read_csv('ForwardReturns_W20_FullA.csv')    # 未來20天收益率    
backward_20d_return_data = pd.read_csv('BackwardReturns_W20_FullA.csv')  # 過去20天收益率 
backward_60d_return_data = pd.read_csv('BackwardReturns_W60_FullA.csv')  # 過去60天收益率 
mkt_value_data = pd.read_csv('MarketValues_FullA.csv')                    # 市值數據
turnover_rate_data = pd.read_csv('TurnoverRateWindowMean_W20_FullA.csv') # 過去20天日均換手率數據


corr_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, corr_data['tradeDate']))
forward_20d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, forward_20d_return_data['tradeDate']))
backward_20d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, backward_20d_return_data['tradeDate']))
backward_60d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, backward_60d_return_data['tradeDate']))
mkt_value_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, mkt_value_data['tradeDate']))
turnover_rate_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, turnover_rate_data['tradeDate']))


corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
forward_20d_return_data = forward_20d_return_data[forward_20d_return_data.columns[1:]].set_index('tradeDate')
backward_20d_return_data = backward_20d_return_data[backward_20d_return_data.columns[1:]].set_index('tradeDate')
backward_60d_return_data = backward_60d_return_data[backward_60d_return_data.columns[1:]].set_index('tradeDate')
mkt_value_data = mkt_value_data[mkt_value_data.columns[1:]].set_index('tradeDate')
turnover_rate_data = turnover_rate_data[turnover_rate_data.columns[1:]].set_index('tradeDate')

下表中,展示了我們計算好的corr_data數據文件的一部分,主要為了說明我們接下來使用的數據dataframe的結構:

  • 每一行為日期,每個交易日均有計算數據,從2006年到2016年8月
  • 每一列為股票,股票池為全A股
corr_data.tail()

得到相關係數表:

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

3.2 量價相關因子截面特徵

接下來,我們簡單檢查一下我們計算得到的量價相關因子的截面特徵

# 量價相關性歷史表現


n_quantile = 10
# 和海通研報一樣,統計十分位數
cols_mean = ['meanQ'+str(i+1) for i in range(n_quantile)]
cols = cols_mean
corr_means = pd.DataFrame(index=corr_data.index, columns=cols)


# 計算相關係數分組平均值
for dt in corr_means.index:
    qt_mean_results = []


    # 相關係數去掉nan和絕對值大於1的
    tmp_corr = corr_data.ix[dt].dropna()
    tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)]
    
    pct_quantiles = 1.0/n_quantile
    for i in range(n_quantile):
        down = tmp_corr.quantile(pct_quantiles*i)
        up = tmp_corr.quantile(pct_quantiles*(i+1))
        mean_tmp = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].mean()
        qt_mean_results.append(mean_tmp)
    corr_means.ix[dt] = qt_mean_results


# corr_means是對歷史每一天,求量價相關係數在各個十分位裡面的平均值
corr_means.tail()



下圖給出了2006年至2016年間,在不同時點,將市場上所有股票按量價相關性分10組後,第1組、第5組以及第10組股票量價相關性的均值情況,即我們所說的量價相關性截面特徵

  • 觀察下圖可知,量價相關性的截面特徵較為穩定
# 量價相關性歷史表現作圖


fig = plt.figure(figsize=(16, 6))
ax1 = fig.add_subplot(111)


lns1 = ax1.plot(corr_means.index, corr_means.meanQ1, label='Q1')
lns2 = ax1.plot(corr_means.index, corr_means.meanQ5, label='Q5')
lns3 = ax1.plot(corr_means.index, corr_means.meanQ10, label='Q10')


lns = lns1+lns2+lns3
labs = [l.get_label() for l in lns]
ax1.legend(lns, labs, bbox_to_anchor=[0.5, 0.1], loc='', ncol=3, mode="", borderaxespad=0., fontsize=12)
ax1.set_ylabel(u'量價相關係數', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)
ax1.set_title(u"量價相關性歷史表現", fontproperties=font, fontsize=16)
ax1.grid()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

3.3 量價因子的預測能力初探

接下來,我們計算了每一天的量價因子之後20日收益的秩相關係數

# 『過去十五天量價相關係數』和『之後20天收益』的秩相關係數計算


ic_data = pd.DataFrame(index=corr_data.index, columns=['IC','pValue'])


# 計算相關係數
for dt in ic_data.index:
    tmp_corr = corr_data.ix[dt]
    tmp_ret = forward_20d_return_data.ix[dt]
    cor = pd.DataFrame(tmp_corr)
    ret = pd.DataFrame(tmp_ret)
    cor.columns = ['corr']
    ret.columns = ['ret']
    cor['ret'] = ret['ret']
    cor = cor[~np.isnan(cor['corr'])][~np.isnan(cor['ret'])]
    if len(cor) < 5:
        continue
    # ic,p_value = st.pearsonr(q['Q'],q['ret'])                 # 計算相關係數   IC
    # ic,p_value = st.pearsonr(q['Q'].rank(),q['ret'].rank())   # 計算秩相關係數 RankIC
    ic, p_value = st.spearmanr(cor['corr'],cor['ret'])   # 計算秩相關係數 RankIC
    ic_data['IC'][dt] = ic
    ic_data['pValue'][dt] = p_value
    
# print len(ic_data['IC']), len(ic_data[ic_data.IC>0]), len(ic_data[ic_data.IC<0])
print 'mean of IC: ', ic_data['IC'].mean()
print 'median of IC: ', ic_data['IC'].median()
print 'the number of IC(all, plus, minus): ', (len(ic_data), len(ic_data[ic_data.IC>0]), len(ic_data[ic_data.IC<0]))

mean of IC:

-0.0415786327101 median of IC: -0.0477574767849 the number of IC(all, plus, minus): (2572, 778, 1760)

從上面計算結果和下圖可知,量價因子和未來20日收益的秩相關係數在大部分時間為負,量價因子對於未來20天的收益有預測性

# 『過去十五天量價相關係數』和『之後20天收益』的秩相關係數作圖


fig = plt.figure(figsize=(16, 6))
ax1 = fig.add_subplot(111)


lns1 = ax1.plot(ic_data.index, ic_data.IC, label='IC')


lns = lns1
labs = [l.get_label() for l in lns]
ax1.legend(lns, labs, bbox_to_anchor=[0.5, 0.1], loc='', ncol=3, mode="", borderaxespad=0., fontsize=12)
ax1.set_ylabel(u'相關係數', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)
ax1.set_title(u"量價因子和未來20日收益之間的秩相關係數", fontproperties=font, fontsize=16)
ax1.grid()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

4. 量價因子歷史回測概述

本節使用2006年以來的數據對於量價相關性因子歷史表現進行回測,進一步簡單設計量價因子選股的幾個風險因子暴露情況。


4.1 量價因子選股的分組超額收益

n_quantile = 10
# 和海通研報一樣,統計十分位數
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean


excess_returns_means = pd.DataFrame(index=corr_data.index, columns=cols)


# 計算相關係數分組的超額收益平均值
for dt in excess_returns_means.index:
    qt_mean_results = []
    
    # 相關係數去掉nan和絕對值大於1的
    tmp_corr = corr_data.ix[dt].dropna()
    tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)]
    tmp_return = forward_20d_return_data.ix[dt].dropna()
    tmp_return_mean = tmp_return.mean()
    
    pct_quantiles = 1.0/n_quantile
    for i in range(n_quantile):
        down = tmp_corr.quantile(pct_quantiles*i)
        up = tmp_corr.quantile(pct_quantiles*(i+1))
        i_quantile_index = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].index
        mean_tmp = tmp_return[i_quantile_index].mean() - tmp_return_mean
        qt_mean_results.append(mean_tmp)
        
    excess_returns_means.ix[dt] = qt_mean_results


excess_returns_means.dropna(inplace=True)
excess_returns_means.tail()


  • 上表計算結果,給出了2006年開始,每天進行量價因子十分位選股後,每個分組內股票在未來一個月相對於市場平均收益的超額收益均值
  • 注意:十分位分組中,量價因子由小到大排序,即第一組為量價因子最小的組
  • 下圖展示,量價因子十分位選股後,在未來一個月各個分組的超額收益,可以發現:因子多空收益明顯,且因子空頭收益更強
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)


excess_returns_means_dist = excess_returns_means.mean()
# lns1 = ax1.plot(excess_returns_means_dist.index, excess_returns_means_dist.values, '--o', label='IC')
excess_dist_plus = excess_returns_means_dist[excess_returns_means_dist>0]
excess_dist_minus = excess_returns_means_dist[excess_returns_means_dist<0]
lns2 = ax1.bar(excess_dist_plus.index, excess_dist_plus.values, align='center', color='r', width=0.35)
lns3 = ax1.bar(excess_dist_minus.index, excess_dist_minus.values, align='center', color='g', width=0.35)


ax1.set_xlim(left=0.5, right=len(excess_returns_means_dist)+0.5)
ax1.set_ylim(-0.01, 0.004)
ax1.set_ylabel(u'超額收益', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'十分位分組', fontproperties=font, fontsize=16)
ax1.set_xticks(excess_returns_means_dist.index)
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
ax1.set_yticklabels([str(x*100)+'0%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
ax1.set_title(u"量價相關性選股因子超額收益", fontproperties=font, fontsize=16)
ax1.grid()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

4.2 量價因子選股的市值分布特徵

檢查量價因子的小市值暴露情況。因為很多策略因為小市值暴露在A股市場表現優異。

n_quantile = 10
# 和海通研報一樣,統計十分位數
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean


mkt_value_means = pd.DataFrame(index=corr_data.index, columns=cols)


# 計算相關係數分組的超額收益平均值
for dt in mkt_value_means.index:
    qt_mean_results = []
    
    # 相關係數去掉nan和絕對值大於1的
    tmp_corr = corr_data.ix[dt].dropna()
    tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)]
    tmp_mkt_value = mkt_value_data.ix[dt].dropna()
    tmp_mkt_value = tmp_mkt_value.rank()/len(tmp_mkt_value)
    
    pct_quantiles = 1.0/n_quantile
    for i in range(n_quantile):
        down = tmp_corr.quantile(pct_quantiles*i)
        up = tmp_corr.quantile(pct_quantiles*(i+1))
        i_quantile_index = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].index
        mean_tmp = tmp_mkt_value[i_quantile_index].mean()
        qt_mean_results.append(mean_tmp)
        
    mkt_value_means.ix[dt] = qt_mean_results


mkt_value_means.dropna(inplace=True)
mkt_value_means.tail()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

  • 上表計算結果,給出了2006年開始,每天進行量價因子十分位選股後,每個分組內股票的市值百分位均值
  • 下圖展示,量價因子十分位選股後,各個分組的市值百分位歷史均值:量價因子有略微的大市值暴露,與市值因子負相關
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()


mkt_value_means_dist = mkt_value_means.mean()
lns1 = ax1.bar(mkt_value_means_dist.index, mkt_value_means_dist.values, align='center', width=0.35)
lns2 = ax2.plot(excess_returns_means_dist.index, excess_returns_means_dist.values, 'o-r')


ax1.legend(lns1, ['market value(left axis)'], loc=2, fontsize=12)
ax2.legend(lns2, ['excess return(right axis)'], fontsize=12)
ax1.set_ylim(0.4, 0.6)
ax2.set_ylim(-0.01, 0.004)
ax1.set_xlim(left=0.5, right=len(mkt_value_means_dist)+0.5)
ax1.set_ylabel(u'市值百分位數', fontproperties=font, fontsize=16)
ax2.set_ylabel(u'超額收益', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'十分位分組', fontproperties=font, fontsize=16)
ax1.set_xticks(mkt_value_means_dist.index)
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
ax1.set_yticklabels([str(x*100)+'%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
ax2.set_yticklabels([str(x*100)+'0%' for x in ax2.get_yticks()], fontproperties=font, fontsize=14)
ax1.set_title(u"量價相關性選股因子市值分布特徵", fontproperties=font, fontsize=16)
ax1.grid()


4.3 量價因子選股的換手率分布特徵

n_quantile = 10
# 和海通研報一樣,統計十分位數
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean
turnover_rate_means = pd.DataFrame(index=corr_data.index, columns=cols)


# 計算相關係數分組的超額收益平均值
for dt in turnover_rate_means.index:
    qt_mean_results = []
    
    # 相關係數去掉nan和絕對值大於1的
    tmp_corr = corr_data.ix[dt].dropna()
    tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)]
    tmp_turnover_rate = turnover_rate_data.ix[dt].dropna()
    
    pct_quantiles = 1.0/n_quantile
    for i in range(n_quantile):
        down = tmp_corr.quantile(pct_quantiles*i)
        up = tmp_corr.quantile(pct_quantiles*(i+1))
        i_quantile_index = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].index
        mean_tmp = tmp_turnover_rate[i_quantile_index].mean()
        qt_mean_results.append(mean_tmp)
        
    turnover_rate_means.ix[dt] = qt_mean_results


turnover_rate_means.dropna(inplace=True)
turnover_rate_means.tail()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

  • 上表計算結果,給出了2006年開始,每天進行量價因子十分位選股後,每個分組內股票的前一個月日均換手率的均值
  • 下圖展示,量價因子十分位選股後,各個分組的1個月日均換手率均值:量價因子對於低換手率有一定風險暴露,換手率隨組別上升而逐漸升高
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()


turnover_rate_means_dist = turnover_rate_means.mean()
lns1 = ax1.bar(turnover_rate_means_dist.index, turnover_rate_means_dist.values, align='center', width=0.35)
lns2 = ax2.plot(excess_returns_means_dist.index, excess_returns_means_dist.values, 'o-r')


ax1.legend(lns1, ['turnover rate(left axis)'], loc=2, fontsize=12)
ax2.legend(lns2, ['excess return(right axis)'], fontsize=12)
ax1.set_ylim(0, 0.05)
ax2.set_ylim(-0.01, 0.004)
ax1.set_xlim(left=0.5, right=len(turnover_rate_means_dist)+0.5)
ax1.set_ylabel(u'換手率', fontproperties=font, fontsize=16)
ax2.set_ylabel(u'超額收益', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'十分位分組', fontproperties=font, fontsize=16)
ax1.set_xticks(turnover_rate_means_dist.index)
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
ax1.set_yticklabels([str(x*100)+'%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
ax2.set_yticklabels([str(x*100)+'0%' for x in ax2.get_yticks()], fontproperties=font, fontsize=14)
ax1.set_title(u"量價相關性選股因子換手率分布特徵", fontproperties=font, fontsize=16)
ax1.grid()


4.4 量價因子選股的一個月反轉分布特徵

n_quantile = 10
# 和海通研報一樣,統計十分位數
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean
hist_returns_means = pd.DataFrame(index=corr_data.index, columns=cols)


# 計算相關係數分組的超額收益平均值
for dt in hist_returns_means.index:
    qt_mean_results = []
    
    # 相關係數去掉nan和絕對值大於1的
    tmp_corr = corr_data.ix[dt].dropna()
    tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)]
    tmp_return = backward_20d_return_data.ix[dt].dropna()
    tmp_return_mean = tmp_return.mean()
    
    pct_quantiles = 1.0/n_quantile
    for i in range(n_quantile):
        down = tmp_corr.quantile(pct_quantiles*i)
        up = tmp_corr.quantile(pct_quantiles*(i+1))
        i_quantile_index = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].index
        mean_tmp = tmp_return[i_quantile_index].mean() - tmp_return_mean
        qt_mean_results.append(mean_tmp)
        
    hist_returns_means.ix[dt] = qt_mean_results


hist_returns_means.dropna(inplace=True)
hist_returns_means.tail()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

  • 上表計算結果,給出了2006年開始,每天進行量價因子十分位選股後,每個分組內股票的前一個月超額漲幅(超出市場平均值)的均值
  • 下圖展示,量價因子十分位選股後,各個分組的前一個月超額漲幅均值:量價因子對於一個月反轉因子有一定風險暴露(多頭組合即第一組中的股票前一個月平均跑輸市場)
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()


hist_returns_means_dist = hist_returns_means.mean()
lns1 = ax1.bar(hist_returns_means_dist.index, hist_returns_means_dist.values, align='center', width=0.35)
lns2 = ax2.plot(excess_returns_means_dist.index, excess_returns_means_dist.values, 'o-r')


ax1.legend(lns1, ['20 day return(left axis)'], loc=2, fontsize=12)
ax2.legend(lns2, ['excess return(right axis)'], fontsize=12)
ax1.set_ylim(-0.03, 0.07)
ax2.set_ylim(-0.01, 0.004)
ax1.set_xlim(left=0.5, right=len(hist_returns_means_dist)+0.5)
ax1.set_ylabel(u'歷史一個月收益率', fontproperties=font, fontsize=16)
ax2.set_ylabel(u'超額收益', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'十分位分組', fontproperties=font, fontsize=16)
ax1.set_xticks(hist_returns_means_dist.index)
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
ax1.set_yticklabels([str(x*100)+'%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
ax2.set_yticklabels([str(x*100)+'0%' for x in ax2.get_yticks()], fontproperties=font, fontsize=14)
ax1.set_title(u"量價相關性選股因子一個月歷史收益率(一個月反轉因子)分布特徵", fontproperties=font, fontsize=16)
ax1.grid()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

4.5 量價因子選股的三個月反轉分布特徵


n_quantile = 10
# 和海通研報一樣,統計十分位數
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean
hist_returns_means = pd.DataFrame(index=corr_data.index, columns=cols)


# 計算相關係數分組的超額收益平均值
for dt in hist_returns_means.index:
    qt_mean_results = []
    
    # 相關係數去掉nan和絕對值大於1的
    tmp_corr = corr_data.ix[dt].dropna()
    tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)]
    tmp_return = backward_60d_return_data.ix[dt].dropna()
    tmp_return_mean = tmp_return.mean()
    
    pct_quantiles = 1.0/n_quantile
    for i in range(n_quantile):
        down = tmp_corr.quantile(pct_quantiles*i)
        up = tmp_corr.quantile(pct_quantiles*(i+1))
        i_quantile_index = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].index
        mean_tmp = tmp_return[i_quantile_index].mean() - tmp_return_mean
        qt_mean_results.append(mean_tmp)
        
    hist_returns_means.ix[dt] = qt_mean_results


hist_returns_means.dropna(inplace=True)
hist_returns_means.tail()

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

  • 上表計算結果,給出了2006年開始,每天進行量價因子十分位選股後,每個分組內股票的前三個月超額漲幅(超出市場平均值)的均值
  • 下圖展示,量價因子十分位選股後,各個分組的前三個月超額漲幅均值:股票分組在三個月漲幅上的分布並未呈現出明顯的單調性,僅呈現出「兩頭高,中間低」的特點
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()


hist_returns_means_dist = hist_returns_means.mean()
lns1 = ax1.bar(hist_returns_means_dist.index, hist_returns_means_dist.values, align='center', width=0.35)
lns2 = ax2.plot(excess_returns_means_dist.index, excess_returns_means_dist.values, 'o-r')


ax1.legend(lns1, ['60 day return(left axis)'], loc=2, fontsize=12)
ax2.legend(lns2, ['excess return(right axis)'], fontsize=12)
ax1.set_ylim(-0.02, 0.04)
ax2.set_ylim(-0.01, 0.004)
ax1.set_xlim(left=0.5, right=len(hist_returns_means_dist)+0.5)
ax1.set_ylabel(u'歷史三個月收益率', fontproperties=font, fontsize=16)
ax2.set_ylabel(u'超額收益', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'十分位分組', fontproperties=font, fontsize=16)
ax1.set_xticks(hist_returns_means_dist.index)
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
ax1.set_yticklabels([str(x*100)+'%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
ax2.set_yticklabels([str(x*100)+'0%' for x in ax2.get_yticks()], fontproperties=font, fontsize=14)
ax1.set_title(u"量價相關性選股因子三個月歷史收益率(三個月反轉因子)分布特徵", fontproperties=font, fontsize=16)
ax1.grid()


5. 量價因子歷史回測凈值表現


接下來,考察上述量價因子的選股能力的回測效果。歷史回測的基本設置如下:

  • 回測時段為2010年1月1日至2016年8月1日
  • 股票池為A股全部股票
  • 組合每15個交易日調倉,交易費率設為雙邊萬分之二
  • 調倉時,漲停、停牌不買入,跌停、停牌不賣出;
  • 每月底調倉時,選擇股票池中量價因子最小的20%的股票;

5.1 量價因子最小20%股票

start = '2010-01-01'                       # 回測起始時間
end = '2016-08-01'                         # 回測結束時間
benchmark = 'ZZ500'                        # 策略參考標準
universe = set_universe('A')               # 證券池,支持股票和基金
capital_base = 10000000                    # 起始資金
freq = 'd'                                 # 策略類型,'d'表示日間策略使用日線回測
refresh_rate = 15                          # 調倉頻率,表示執行handle_data的時間間隔


corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv')     # 讀取量價因子數據
corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
corr_dates = corr_data.index.values


quantile_five = 1                           # 選取股票的量價因子五分位數,1表示選取股票池中因子最小的10%的股票
commission = Commission(0.0002,0.0002)      # 交易費率設為雙邊萬分之二


def initialize(account):                   # 初始化虛擬賬戶狀態
    pass


def handle_data(account):                  # 每個交易日的買入賣出指令
    pre_date = account.previous_date.strftime("%Y-%m-%d")
    if pre_date not in corr_dates:            # 只在計算過量價因子的交易日調倉
        return
    
    # 拿取調倉日前一個交易日的量價因子,並按照相應十分位選擇股票
    pre_corr = corr_data.ix[pre_date]
    pre_corr = pre_corr.dropna()
    pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)]
    
    pre_corr_min = pre_corr.quantile((quantile_five-1)*0.2)
    pre_corr_max = pre_corr.quantile(quantile_five*0.2)
    my_univ = pre_corr[pre_corr>=pre_corr_min][pre_corr<pre_corr_max].index.values
    
    # 調倉邏輯
    univ = [x for x in my_univ if x in account.universe]
    
    # 不在股票池中的,清倉
    for stk in account.valid_secpos:
        if stk not in univ:
            order_to(stk, 0)
    # 在目標股票池中的,等權買入
    for stk in univ:
        order_pct_to(stk, 1.1/len(univ))


bt_all = {}   # 用來保存三個策略運行結果:量價因子,20日反轉因子,量價因子與20日反轉因子等權重疊加
bt_all['corr'] = bt   # 保存量價因子回測結果

5.2 一個月反轉因子最小(近一個月漲幅最低的)20%股票

start = '2010-01-01'                       # 回測起始時間
end = '2016-08-01'                         # 回測結束時間
benchmark = 'ZZ500'                        # 策略參考標準
universe = set_universe('A')               # 證券池,支持股票和基金
capital_base = 10000000                    # 起始資金
freq = 'd'                                 # 策略類型,'d'表示日間策略使用日線回測
refresh_rate = 15                          # 調倉頻率,表示執行handle_data的時間間隔


revs_data = pd.read_csv('BackwardReturns_W20_FullA.csv')     # 讀取反轉因子數據
revs_data = revs_data[revs_data.columns[1:]].set_index('tradeDate')
revs_dates = revs_data.index.values


quantile_five = 1                           # 選取股票的20日反轉因子的五分位數,1表示選取股票池中因子最小的20%的股票
commission = Commission(0.0002,0.0002)     # 交易費率設為雙邊萬分之二


def initialize(account):                   # 初始化虛擬賬戶狀態
    pass


def handle_data(account):                  # 每個交易日的買入賣出指令
    pre_date = account.previous_date.strftime("%Y-%m-%d")
    if pre_date not in revs_dates:            # 只在計算過反轉因子的交易日調倉
        return
    
    # 拿取調倉日前一個交易日的反轉因子,並按照相應十分位選擇股票
    pre_revs = revs_data.ix[pre_date]
    pre_revs = pre_revs.dropna()
    
    pre_revs_min = pre_revs.quantile((quantile_five-1)*0.2)
    pre_revs_max = pre_revs.quantile(quantile_five*0.2)
    my_univ = pre_revs[pre_revs>=pre_revs_min][pre_revs<pre_revs_max].index.values
    
    # 調倉邏輯
    univ = [x for x in my_univ if x in account.universe]
    
    # 不在股票池中的,清倉
    for stk in account.valid_secpos:
        if stk not in univ:
            order_to(stk, 0)
    # 在目標股票池中的,等權買入
    for stk in univ:
        order_pct_to(stk, 1.1/len(univ))


bt_all['revs'] = bt  # 保存一個月反轉因子回測結果

5.3 量價因子疊加反轉因子選股

  • 量價因子和反轉因子分別標準化,之後相加生成疊加因子,選疊加因子最小的20%股票
start = '2010-01-01'                       # 回測起始時間
end = '2016-08-01'                         # 回測結束時間
benchmark = 'ZZ500'                        # 策略參考標準
universe = set_universe('A')               # 證券池,支持股票和基金
capital_base = 10000000                    # 起始資金
freq = 'd'                                 # 策略類型,'d'表示日間策略使用日線回測
refresh_rate = 15                          # 調倉頻率,表示執行handle_data的時間間隔


corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv')     # 讀取量價因子數據
corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
corr_dates = corr_data.index.values


revs_data = pd.read_csv('BackwardReturns_W20_FullA.csv')     # 讀取反轉因子數據
revs_data = revs_data[revs_data.columns[1:]].set_index('tradeDate')


quantile_five = 1                           # 選取股票的因子五分位數,1表示選取股票池中因子最小的20%的股票
commission = Commission(0.0002,0.0002)     # 交易費率設為雙邊萬分之二


def initialize(account):                   # 初始化虛擬賬戶狀態
    pass


def handle_data(account):                  # 每個交易日的買入賣出指令
    pre_date = account.previous_date.strftime("%Y-%m-%d")
    if pre_date not in corr_dates:            # 只在計算過量價因子的交易日調倉
        return


    # 拿取調倉日前一個交易日的量價因子和反轉因子,並按照相應分位選擇股票
    pre_corr = corr_data.ix[pre_date]
    pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)]
    pre_revs = revs_data.ix[pre_date]
    
    # 量價因子和反轉因子只做簡單的等權疊加
    pre_data = pd.Series(standardize(pre_corr.to_dict())) + pd.Series(standardize(pre_revs.to_dict()))   # 因子標準化使用了uqer的函數standardize
    pre_data = pre_data.dropna()
    
    pre_data_min = pre_data.quantile((quantile_five-1)*0.2)
    pre_data_max = pre_data.quantile(quantile_five*0.2)
    my_univ = pre_data[pre_data>=pre_data_min][pre_data<pre_data_max].index.values
    
    # 調倉邏輯
    univ = [x for x in my_univ if x in account.universe]
    
    # 不在股票池中的,清倉
    for stk in account.valid_secpos:
        if stk not in univ:
            order_to(stk, 0)
    # 在目標股票池中的,等權買入
    for stk in univ:
        order_pct_to(stk, 1.1/len(univ))


bt_all['corr + revs'] = bt

5.4 上述三個組合對比

此處對比,量價因子、反轉因子、量價因子疊加反轉因子這三個組合的回測結果

results = {}
for x in bt_all.keys():
    results[x] = {}
    results[x]['bt'] = bt_all[x]
fig = plt.figure(figsize=(10,8))
fig.set_tight_layout(True)
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
ax1.grid()
ax2.grid()


for qt in ['corr','revs','corr + revs']:
    bt = results[qt]['bt']


    data = bt[[u'tradeDate',u'portfolio_value',u'benchmark_return']]
    data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0   # 總頭寸每日回報率
    data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/  10000000.0 - 1.0
    data['excess_return'] = data.portfolio_return - data.benchmark_return                 # 總頭寸每日超額回報率
    data['excess'] = data.excess_return + 1.0
    data['excess'] = data.excess.cumprod()                # 總頭寸對沖指數後的凈值序列
    data['portfolio'] = data.portfolio_return + 1.0     
    data['portfolio'] = data.portfolio.cumprod()          # 總頭寸不對沖時的凈值序列
    data['benchmark'] = data.benchmark_return + 1.0
    data['benchmark'] = data.benchmark.cumprod()          # benchmark的凈值序列
    results[qt]['hedged_max_drawdown'] = max([1 - v/max(1, max(data['excess'][:i+1])) for i,v in enumerate(data['excess'])])  # 對沖後凈值最大回撤
    results[qt]['hedged_volatility'] = np.std(data['excess_return'])*np.sqrt(252)
    results[qt]['hedged_annualized_return'] = (data['excess'].values[-1])**(252.0/len(data['excess'])) - 1.0
    # data[['portfolio','benchmark','excess']].plot(figsize=(12,8))
    # ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))
    ax1.plot(data['tradeDate'], data[['portfolio']], label=str(qt))
    ax2.plot(data['tradeDate'], data[['excess']], label=str(qt))
    


ax1.legend(loc=0, fontsize=12)
ax2.legend(loc=0, fontsize=12)
ax1.set_ylabel(u"凈值", fontproperties=font, fontsize=16)
ax2.set_ylabel(u"對沖凈值", fontproperties=font, fontsize=16)
ax1.set_title(u"量價因子和反轉因子選股能力對比 - 凈值走勢", fontproperties=font, fontsize=16)
ax2.set_title(u"量價因子和反轉因子選股能力對比 - 對沖中證500指數後凈值走勢", fontproperties=font, fontsize=16)


上圖中可以發現:

  • 藍色曲線為量價因子,綠色為反轉因子,紅色為量價因子疊加反轉因子
  • 量價因子的漫長的熊市中走勢穩健,並一直打敗反轉因子
  • 反轉因子在15年之後表現出色
  • 量價因子疊加反轉因子,能起到意想不到的疊加效果

5.5 量價因子選股 —— 不同五分位數組合回測走勢比較


# 可編輯部分與 strategy 模式一樣,其餘部分按本例代碼編寫即可


# -----------回測參數部分開始,可編輯------------
start = '2010-01-01'                       # 回測起始時間
end = '2016-08-01'                         # 回測結束時間
benchmark = 'ZZ500'                        # 策略參考標準
universe = set_universe('A')               # 證券池,支持股票和基金
capital_base = 10000000                    # 起始資金
freq = 'd'                                 # 策略類型,'d'表示日間策略使用日線回測
refresh_rate = 15                          # 調倉頻率,表示執行handle_data的時間間隔


corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv')     # 讀取量價因子數據
corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
corr_dates = corr_data.index.values
# ---------------回測參數部分結束----------------




# 把回測參數封裝到 SimulationParameters 中,供 quick_backtest 使用
sim_params = quartz.SimulationParameters(start, end, benchmark, universe, capital_base)
# 獲取回測行情數據
idxmap, data = quartz.get_daily_data(sim_params)
# 運行結果
results_corr = {}


# 調整參數(選取股票的量價因子五分位數),進行快速回測
for quantile_five in range(1, 6):
    
    # ---------------策略邏輯部分----------------
    commission = Commission(0.0002,0.0002)      # 交易費率設為雙邊萬分之二


    def initialize(account):                   # 初始化虛擬賬戶狀態
        pass


    def handle_data(account):                  # 每個交易日的買入賣出指令
        pre_date = account.previous_date.strftime("%Y-%m-%d")
        if pre_date not in corr_dates:            # 只在計算過量價因子的交易日調倉
            return


        # 拿取調倉日前一個交易日的量價因子,並按照相應十分位選擇股票
        pre_corr = corr_data.ix[pre_date]
        pre_corr = pre_corr.dropna()
        pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)]


        pre_corr_min = pre_corr.quantile((quantile_five-1)*0.2)
        pre_corr_max = pre_corr.quantile(quantile_five*0.2)
        my_univ = pre_corr[pre_corr>=pre_corr_min][pre_corr<pre_corr_max].index.values


        # 調倉邏輯
        univ = [x for x in my_univ if x in account.universe]


        # 不在股票池中的,清倉
        for stk in account.valid_secpos:
            if stk not in univ:
                order_to(stk, 0)
        # 在目標股票池中的,等權買入
        for stk in univ:
            order_pct_to(stk, 1.1/len(univ))
    # ---------------策略邏輯部分結束----------------


    # 把回測邏輯封裝到 TradingStrategy 中,供 quick_backtest 使用
    strategy = quartz.TradingStrategy(initialize, handle_data)
    # 回測部分
    bt, acct = quartz.quick_backtest(sim_params, strategy, idxmap, data, refresh_rate=refresh_rate, commission=commission)


    # 對於回測的結果,可以通過 perf_parse 函數計算風險指標
    perf = quartz.perf_parse(bt, acct)


    # 保存運行結果
    tmp = {}
    tmp['bt'] = bt
    tmp['annualized_return'] = perf['annualized_return']
    tmp['volatility'] = perf['volatility']
    tmp['max_drawdown'] = perf['max_drawdown']
    tmp['alpha'] = perf['alpha']
    tmp['beta'] = perf['beta']
    tmp['sharpe'] = perf['sharpe']
    tmp['information_ratio'] = perf['information_ratio']
    
    results_corr[quantile_five] = tmp
    print str(quantile_five),
print 'done'

1

2

3

4

5 done

fig = plt.figure(figsize=(10,8))
fig.set_tight_layout(True)
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
ax1.grid()
ax2.grid()


for qt in results_corr:
    bt = results_corr[qt]['bt']


    data = bt[[u'tradeDate',u'portfolio_value',u'benchmark_return']]
    data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0   # 總頭寸每日回報率
    data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/  10000000.0 - 1.0
    data['excess_return'] = data.portfolio_return - data.benchmark_return                 # 總頭寸每日超額回報率
    data['excess'] = data.excess_return + 1.0
    data['excess'] = data.excess.cumprod()                # 總頭寸對沖指數後的凈值序列
    data['portfolio'] = data.portfolio_return + 1.0     
    data['portfolio'] = data.portfolio.cumprod()          # 總頭寸不對沖時的凈值序列
    data['benchmark'] = data.benchmark_return + 1.0
    data['benchmark'] = data.benchmark.cumprod()          # benchmark的凈值序列
    results_corr[qt]['hedged_max_drawdown'] = max([1 - v/max(1, max(data['excess'][:i+1])) for i,v in enumerate(data['excess'])])  # 對沖後凈值最大回撤
    results_corr[qt]['hedged_volatility'] = np.std(data['excess_return'])*np.sqrt(252)
    results_corr[qt]['hedged_annualized_return'] = (data['excess'].values[-1])**(252.0/len(data['excess'])) - 1.0
    # data[['portfolio','benchmark','excess']].plot(figsize=(12,8))
    # ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))
    ax1.plot(data['tradeDate'], data[['portfolio']], label=str(qt))
    ax2.plot(data['tradeDate'], data[['excess']], label=str(qt))
    


ax1.legend(loc=0, fontsize=12)
ax2.legend(loc=0, fontsize=12)
ax1.set_ylabel(u"凈值", fontproperties=font, fontsize=16)
ax2.set_ylabel(u"對沖凈值", fontproperties=font, fontsize=16)
ax1.set_title(u"量價因子 - 不同五分位數分組選股凈值走勢", fontproperties=font, fontsize=16)
ax2.set_title(u"量價因子 - 不同五分位數分組選股對沖中證500指數後凈值走勢", fontproperties=font, fontsize=16)

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

上面的圖片顯示「量價因子-不同五分位數分組選股」的凈值走勢,其中下面一張圖片展示出各組頭寸對沖完中證500指數後的凈值走勢,可以看到:

  • 不同的五分位數組對應的凈值走勢順序區分度很高!

下面的表格展示出不同分位數組合的各項風險指標,每次調倉均買入量價因子最小的20%股票的策略,即最小分位數的組合(組合1)各項指標表現都非常出色:

# results 轉換為 DataFrame
import pandas
results_pd = pandas.DataFrame(results_corr).T.sort_index()


results_pd = results_pd[[u'alpha', u'beta', u'information_ratio', u'sharpe', 
                        u'annualized_return', u'max_drawdown', u'volatility', 
                         u'hedged_annualized_return', u'hedged_max_drawdown', u'hedged_volatility']]


for col in results_pd.columns:
    results_pd[col] = [np.round(x, 3) for x in results_pd[col]]
    
cols = [(u'風險指標', u'Alpha'), (u'風險指標', u'Beta'), (u'風險指標', u'信息比率'), (u'風險指標', u'夏普比率'),
        (u'純股票多頭時', u'年化收益'), (u'純股票多頭時', u'最大回撤'), (u'純股票多頭時', u'收益波動率'), 
        (u'對沖後', u'年化收益'), (u'對沖後', u'最大回撤'), 
        (u'對沖後', u'收益波動率')]
results_pd.columns = pd.MultiIndex.from_tuples(cols)
results_pd.index.name = u'五分位組別'
results_pd

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

5.6 量價因子疊加反轉因子選股 —— 不同五分位數組合回測走勢比較

  • 量價因子和反轉因子分別標準化,之後直接等權相加生成疊加因子
# 可編輯部分與 strategy 模式一樣,其餘部分按本例代碼編寫即可


# -----------回測參數部分開始,可編輯------------
start = '2010-01-01'                       # 回測起始時間
end = '2016-08-01'                         # 回測結束時間
benchmark = 'ZZ500'                        # 策略參考標準
universe = set_universe('A')               # 證券池,支持股票和基金
capital_base = 10000000                    # 起始資金
freq = 'd'                                 # 策略類型,'d'表示日間策略使用日線回測
refresh_rate = 15                          # 調倉頻率,表示執行handle_data的時間間隔


corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv')     # 讀取量價因子數據
corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
corr_dates = corr_data.index.values


revs_data = pd.read_csv('BackwardReturns_W20_FullA.csv')     # 讀取反轉因子數據
revs_data = revs_data[revs_data.columns[1:]].set_index('tradeDate')


# ---------------回測參數部分結束----------------




# 把回測參數封裝到 SimulationParameters 中,供 quick_backtest 使用
sim_params = quartz.SimulationParameters(start, end, benchmark, universe, capital_base)
# 獲取回測行情數據
idxmap, data = quartz.get_daily_data(sim_params)
# 運行結果
results_corrPlusRevs = {}


# 調整參數(選取股票的因子五分位數),進行快速回測
for quantile_five in range(1, 6):
    
    # ---------------策略邏輯部分----------------
    commission = Commission(0.0002,0.0002)      # 交易費率設為雙邊萬分之二


    def initialize(account):                   # 初始化虛擬賬戶狀態
        pass


    def handle_data(account):                  # 每個交易日的買入賣出指令
        pre_date = account.previous_date.strftime("%Y-%m-%d")
        if pre_date not in corr_dates:            # 只在計算過量價因子的交易日調倉
            return


        # 拿取調倉日前一個交易日的量價因子和反轉因子,並按照相應分位選擇股票
        pre_corr = corr_data.ix[pre_date]
        pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)]
        pre_revs = revs_data.ix[pre_date]


        # 量價因子和反轉因子只做簡單的等權疊加
        pre_data = pd.Series(standardize(pre_corr.to_dict())) + pd.Series(standardize(pre_revs.to_dict()))
        pre_data = pre_data.dropna()


        pre_data_min = pre_data.quantile((quantile_five-1)*0.2)
        pre_data_max = pre_data.quantile(quantile_five*0.2)
        my_univ = pre_data[pre_data>=pre_data_min][pre_data<pre_data_max].index.values


        # 調倉邏輯
        univ = [x for x in my_univ if x in account.universe]


        # 不在股票池中的,清倉
        for stk in account.valid_secpos:
            if stk not in univ:
                order_to(stk, 0)
        # 在目標股票池中的,等權買入
        for stk in univ:
            order_pct_to(stk, 1.1/len(univ))
    # ---------------策略邏輯部分結束----------------


    # 把回測邏輯封裝到 TradingStrategy 中,供 quick_backtest 使用
    strategy = quartz.TradingStrategy(initialize, handle_data)
    # 回測部分
    bt, acct = quartz.quick_backtest(sim_params, strategy, idxmap, data, refresh_rate=refresh_rate, commission=commission)


    # 對於回測的結果,可以通過 perf_parse 函數計算風險指標
    perf = quartz.perf_parse(bt, acct)


    # 保存運行結果
    tmp = {}
    tmp['bt'] = bt
    tmp['annualized_return'] = perf['annualized_return']
    tmp['volatility'] = perf['volatility']
    tmp['max_drawdown'] = perf['max_drawdown']
    tmp['alpha'] = perf['alpha']
    tmp['beta'] = perf['beta']
    tmp['sharpe'] = perf['sharpe']
    tmp['information_ratio'] = perf['information_ratio']
    
    results_corrPlusRevs[quantile_five] = tmp
    print str(quantile_five),
print 'done'

1

2

3

4

5 done

fig = plt.figure(figsize=(10,8))
fig.set_tight_layout(True)
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
ax1.grid()
ax2.grid()


for qt in results_corrPlusRevs:
    bt = results_corrPlusRevs[qt]['bt']


    data = bt[[u'tradeDate',u'portfolio_value',u'benchmark_return']]
    data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0   # 總頭寸每日回報率
    data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/  10000000.0 - 1.0
    data['excess_return'] = data.portfolio_return - data.benchmark_return                 # 總頭寸每日超額回報率
    data['excess'] = data.excess_return + 1.0
    data['excess'] = data.excess.cumprod()                # 總頭寸對沖指數後的凈值序列
    data['portfolio'] = data.portfolio_return + 1.0     
    data['portfolio'] = data.portfolio.cumprod()          # 總頭寸不對沖時的凈值序列
    data['benchmark'] = data.benchmark_return + 1.0
    data['benchmark'] = data.benchmark.cumprod()          # benchmark的凈值序列
    results_corrPlusRevs[qt]['hedged_max_drawdown'] = max([1 - v/max(1, max(data['excess'][:i+1])) for i,v in enumerate(data['excess'])])  # 對沖後凈值最大回撤
    results_corrPlusRevs[qt]['hedged_volatility'] = np.std(data['excess_return'])*np.sqrt(252)
    results_corrPlusRevs[qt]['hedged_annualized_return'] = (data['excess'].values[-1])**(252.0/len(data['excess'])) - 1.0
    # data[['portfolio','benchmark','excess']].plot(figsize=(12,8))
    # ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))
    ax1.plot(data['tradeDate'], data[['portfolio']], label=str(qt))
    ax2.plot(data['tradeDate'], data[['excess']], label=str(qt))
    


ax1.legend(loc=0, fontsize=12)
ax2.legend(loc=0, fontsize=12)
ax1.set_ylabel(u"凈值", fontproperties=font, fontsize=16)
ax2.set_ylabel(u"對沖凈值", fontproperties=font, fontsize=16)
ax1.set_title(u"量價因子與反轉因子等權疊加選股 - 不同五分位數分組選股凈值走勢", fontproperties=font, fontsize=16)
ax2.set_title(u"量價因子與反轉因子等權疊加選股 - 不同五分位數分組選股對沖中證500指數後凈值走勢", fontproperties=font, fontsize=16)


上面的圖片顯示「量價因子疊加反轉因子-不同五分位數分組選股」的凈值走勢,其中下面一張圖片展示出各組頭寸對沖完中證500指數後的凈值走勢,可以看到:

  • 不同的五分位數組對應的凈值走勢順序區分度很高!

下面的表格展示出不同分位數組合的各項風險指標,每次調倉均買入量價因子反轉因子疊加後最小的20%股票的策略,即最小分位數的組合(組合1)各項指標表現都非常出色:

# results 轉換為 DataFrame
import pandas
results_pd = pandas.DataFrame(results_corrPlusRevs).T.sort_index()


results_pd = results_pd[[u'alpha', u'beta', u'information_ratio', u'sharpe', 
                        u'annualized_return', u'max_drawdown', u'volatility', 
                         u'hedged_annualized_return', u'hedged_max_drawdown', u'hedged_volatility']]


for col in results_pd.columns:
    results_pd[col] = [np.round(x, 3) for x in results_pd[col]]
    
cols = [(u'風險指標', u'Alpha'), (u'風險指標', u'Beta'), (u'風險指標', u'信息比率'), (u'風險指標', u'夏普比率'),
        (u'純股票多頭時', u'年化收益'), (u'純股票多頭時', u'最大回撤'), (u'純股票多頭時', u'收益波動率'), 
        (u'對沖後', u'年化收益'), (u'對沖後', u'最大回撤'), 
        (u'對沖後', u'收益波動率')]
results_pd.columns = pd.MultiIndex.from_tuples(cols)
results_pd.index.name = u'五分位組別'
results_pd

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

5.7 更長回測時間 —— 06年開始回測

  • 量價因子和反轉因子分別標準化,之後直接等權相加生成疊加因子
  • 此處選擇疊加因子最小的20%股票作為持倉組合
start = '2006-01-01'                       # 回測起始時間
end = '2016-08-01'                         # 回測結束時間
benchmark = 'ZZ500'                        # 策略參考標準
universe = set_universe('A')               # 證券池,支持股票和基金
capital_base = 2000000                     # 起始資金
freq = 'd'                                 # 策略類型,'d'表示日間策略使用日線回測
refresh_rate = 15                          # 調倉頻率,表示執行handle_data的時間間隔


corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv')     # 讀取量價因子數據
corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
corr_dates = corr_data.index.values


revs_data = pd.read_csv('BackwardReturns_W20_FullA.csv')     # 讀取反轉因子數據
revs_data = revs_data[revs_data.columns[1:]].set_index('tradeDate')


quantile_five = 1                           # 選取股票的因子五分位數,1表示選取股票池中因子最小的20%的股票
commission = Commission(0.0002,0.0002)     # 交易費率設為雙邊萬分之二


def initialize(account):                   # 初始化虛擬賬戶狀態
    pass


def handle_data(account):                  # 每個交易日的買入賣出指令
    pre_date = account.previous_date.strftime("%Y-%m-%d")
    if pre_date not in corr_dates:            # 只在計算過量價因子的交易日調倉
        return


    # 拿取調倉日前一個交易日的量價因子和反轉因子,並按照相應分位選擇股票
    pre_corr = corr_data.ix[pre_date]
    pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)]
    pre_revs = revs_data.ix[pre_date]
    
    # 量價因子和反轉因子只做簡單的等權疊加
    pre_data = pd.Series(standardize(pre_corr.to_dict())) + pd.Series(standardize(pre_revs.to_dict()))
    pre_data = pre_data.dropna()
    
    pre_data_min = pre_data.quantile((quantile_five-1)*0.2)
    pre_data_max = pre_data.quantile(quantile_five*0.2)
    my_univ = pre_data[pre_data>=pre_data_min][pre_data<pre_data_max].index.values
    
    # 調倉邏輯
    univ = [x for x in my_univ if x in account.universe]
    
    # 不在股票池中的,清倉
    for stk in account.valid_secpos:
        if stk not in univ:
            order_to(stk, 0)
    # 在目標股票池中的,等權買入
    for stk in univ:
        order_pct_to(stk, 1.1/len(univ))

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

fig = plt.figure(figsize=(12,5))
fig.set_tight_layout(True)
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax1.grid()


bt_quantile = bt
data = bt_quantile[[u'tradeDate',u'portfolio_value',u'benchmark_return']]
data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0
data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/  2000000.0 - 1.0
data['excess_return'] = data.portfolio_return - data.benchmark_return
data['excess'] = data.excess_return + 1.0
data['excess'] = data.excess.cumprod()
data['portfolio'] = data.portfolio_return + 1.0
data['portfolio'] = data.portfolio.cumprod()
data['benchmark'] = data.benchmark_return + 1.0
data['benchmark'] = data.benchmark.cumprod()
# ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))
ax1.plot(data['tradeDate'], data[['portfolio']], label='portfolio(left)')
ax1.plot(data['tradeDate'], data[['benchmark']], label='benchmark(left)')
ax2.plot(data['tradeDate'], data[['excess']], label='hedged(right)', color='r')


ax1.legend(loc=2)
ax2.legend(loc=0)
ax2.set_ylim(bottom=0.5, top=5)
ax1.set_ylabel(u"凈值", fontproperties=font, fontsize=16)
ax2.set_ylabel(u"對沖指數凈值", fontproperties=font, fontsize=16)
ax2.set_ylabel(u"對沖指數凈值", fontproperties=font, fontsize=16)
ax1.set_title(u"量價因子反轉因子疊加選股的前20%股票回測走勢", fontproperties=font, fontsize=16)

【量價因子 - 結合價格和成交量構建選股策略】整理轉分享 - 天天要聞

  • 上圖可以看到從06年起的回測結果,展示出量價因子反轉因子疊加後的穩定的alpha輸出

我們根據量價因子疊加反轉因子選取股票組合,表現最好的組合其06年以來年化收益達到41.4%,alpha達到22.6%,beta僅為0.88,展示出穩定盈利的能力。

財經分類資訊推薦

618還在糾結選什麼手機?建議看看華為nova 12系列 - 天天要聞

618還在糾結選什麼手機?建議看看華為nova 12系列

轉眼間, 618購物狂歡季已經進入尾聲階段,大家是否還在為如何挑選一款合適的手機而猶豫不決呢?事實上,買手機其實沒必要糾結太多,畢竟對於大多數人而言,日常使用手機無非就是通訊交流、查詢信息和拍照記錄生活等,所以我們更應重點關注的是手機的通信和影像實力。今天這期
海信星光S1 Pro於北京中塔蘇寧易家超級體驗店首發上市 - 天天要聞

海信星光S1 Pro於北京中塔蘇寧易家超級體驗店首發上市

據悉,此次活動在北京中塔蘇寧易家超級體驗店舉辦,現場設置各類用戶交互活動。其中,歐洲杯主題展區的互動體驗,足球、人形牆、防藍光飛行棋等吸引了大量周邊居民及旅客。多重新品首發豪禮也通過現場認籌、購買等方式兌現。
長續航+大屏幕+大音量,父親節禮物就選華為暢享 70系列 - 天天要聞

長續航+大屏幕+大音量,父親節禮物就選華為暢享 70系列

不知不覺就到了父親節,你準備好禮物送給老爸了嗎?個人認為,挑選一款功能豐富、使用便捷的手機,這樣既能滿足老爸的日常需求又能體現我們作為子女的一片孝心,可謂一舉兩得。那麼,市面上哪款手機具有上述特徵呢?答案是華為暢享 70S和華為暢享 70 Pro。
20億元項目黃了,1.6億元保證金難收回 勘設股份擬發起訴訟 涉及兩家遵義城建城投公司 - 天天要聞

20億元項目黃了,1.6億元保證金難收回 勘設股份擬發起訴訟 涉及兩家遵義城建城投公司

每經記者:陳晴    每經編輯:張海妮20億元大單黃了,甚至此前向發包人支付的1.6億元履約保證金也遲遲要不回來,無奈之下,勘設股份(SH603458,股價4.61元,市值14.35億元)欲提起訴訟。6月14日晚間,勘設股份公告稱,擬對遵義市城建(集團)有限責任公司(曾用名:遵義市保障性住房建設投資開發有限責任公司)、遵義...
銀行存款利率持續下降,還應不應該把錢存銀行? - 天天要聞

銀行存款利率持續下降,還應不應該把錢存銀行?

在當前全球經濟環境下,銀行存款利率的持續下降已成為一個不容忽視的現象。隨著這一變化的不斷發酵,人們不禁要問:在利率持續走低的背景下,是否還應該將錢存入銀行?理解銀行存款利率下降的原因對於解答這一問題至關重要。一般而言,銀行存款利率的下降與中
6月以來中國經濟有何邊際變化 - 天天要聞

6月以來中國經濟有何邊際變化

平安首經團隊:鍾正生  投資諮詢資格編號:S1060520090001常藝馨  投資諮詢資格編號:S1060522080003核心觀點6月以來,中國經濟增長斜率邊際放緩。工業生產恢復動能不足,因基建地產用量和國內消費需求不佳,部分行業受貿易摩擦升級的衝擊,「5·17」新政對房地產銷售的拉動放緩,但國債發行融資提速,對經濟的支撐有望...
周末重磅!轉融通引發市場下跌?證監會回應 - 天天要聞

周末重磅!轉融通引發市場下跌?證監會回應

6月16日晚間,證監會新聞發言人就融券與轉融券有關情況答記者問。問:近日,有自媒體稱「轉融通瘋狂報復,難怪市場持續下跌,轉融通(6月12日)一天新增近1.7億股」。請問證監會怎麼看?
湖南中西部風險較高!中央氣象台發布漬澇風險氣象預報 - 天天要聞

湖南中西部風險較高!中央氣象台發布漬澇風險氣象預報

受強降雨的影響,預計6月16日20時至17日20時,浙江南部、江西東部、福建北部、湖南中西部、廣西北部、貴州東南部等地部分地區發生漬澇的氣象風險較高,其中,浙江南部、江西東部、福建北部、廣西北部等地部分地區發生漬澇的氣象風險高,易形成城市內澇和農田漬害,需加強防範。來源:中央氣象台爆料、維權通道:應用市場下...
打造全球投資首選地,「數字龍華」向企業發出盛情邀請 - 天天要聞

打造全球投資首選地,「數字龍華」向企業發出盛情邀請

6月14日,「投資龍華 共贏未來」——上市企業走進深圳龍華專場活動暨2024年龍華招商大會正式開幕,活動邀請30餘家上市企業走訪轄區產業一線,促成龍華區與國內多家企業簽訂戰略合作協議,構築了一場資本市場與實體經濟深度融合的高端對話,更是「數字龍華」向世界發出的最強音——這裡,是全球投資的優選之地,是共創輝煌的...