CAPM模型应用策略

导语:CAPM模型公式解释了金融资产的系统性风险β,但事实上股票中还有这个公式不能解释的收益,叫作α。本文将展示如何通过α选股构建投资组合,并且获得超额收益。

 

本文中的数学符号:σsσsσs为股票标准差,βββ为股票和市场组合的相关系数,σMσMσM为市场标准差,rfrfrf为无风险利率,rsrsrs为股票收益率,rMrMrM为市场组合收益率

超额收益选股

CAPM模型(capital asset pricing model)相信大家应该都很熟悉。其中心思想是利用指数模型简化了马克维茨边界条件,推导出系统风险可以用股票与市场的相关系数和市场风险表示:σs=β⋅σMσs=β⋅σMσs=β⋅σM,并由此计算在正确定价的情况下股票的收益率与市场组合收益率的关系为:

E[rs]=rf+β(E[rM]−rf)

E[rs]=rf+β(E[rM]−rf)E[rs]=rf+β(E[rM]−rf)


那么我们知道,当一只股票的价格被正确定价的时候,它的风险溢价与市场组合的风险溢价是呈现完全的线性关系,将这样的股票收益率和市场组合的收益率做线性回归,得到的应该是一条斜率为βββ,截距为rfrfrf的拟合直线。
现实中,每只股票的收益率与市场组合的收益率进行回归的时候截距都不会严格的等于rfrfrf,此时我们定义

α=(E[rs]−rf)−β(E[rM]−rf)

α=(E[rs]−rf)−β(E[rM]−rf)α=(E[rs]−rf)−β(E[rM]−rf)


ααα被称作超额收益,意为股票除了承担系统风险所获得的收益之外的额外收益。按照CAPM模型,当正确定价时,股票的ααα值应该为零,所有ααα值不为零的股票我们都可以认为被错误的定价了。被错误定价的股票可以分为两类

{α<0,此时股票的价格被低估了α>0,此时股票的价格被高估了

{α<0,此时股票的价格被低估了α>0,此时股票的价格被高估了{α<0,此时股票的价格被低估了α>0,此时股票的价格被高估了

 

假设CAPM理论是成立,这些错误定价的股票都会最终回到正确的定价上去,因此,我们在选股时应该买入ααα更低的股票,这就是我们策略构造的基础。

策略和回测

我们构建策略的是,如下:
∙∙∙ 设定某一股票池,并选定一指数作为市场组合(一般情况下股票池应该是该市场组合的成分股);
∙∙∙ 选定参数:调仓周期tc、计算α所使用的收盘价天数N、持仓的股票数量num;
∙∙∙ 每隔tc个交易日执行:
−−− 使用过去N天收益率数据进行线性回归,计算股票池中所有股票对应市场组合的α;
−−− 选择α最小的num支股票,调仓持有这些股票,持有比例为α越小权重越大。

下面的回测使用股票池为沪深300成分股,对应的市场组合是沪深300指数;参数有tc=15、N=60、num=20。调用2012年12月至今的数据进行回测的结果如下:
CAPM模型应用策略
我们可以看到,策略收益始终在市场组合之上,是因为我们购买的20支股票基本上已经将风险足够分散化,并且持有15天之后,这些被暂时错误定价的股票都回到了正确的定价,所以策略的收益与指数的收益是同步的,但是始终比指数的收益要高。

函数和变量说明书

函数说明书
CAPM模型应用策略

全局变量说明书
CAPM模型应用策略

 

 

import numpy as np
from scipy import stats
import pylab

def initialize(context):
    set_params()        #1设置策参数
    set_variables()     #设置中间变量
    set_backtest()    # 3设置回测条件
#1设置策参数
def set_params():
    g.index = '000300.XSHG'
    #每次取alpha最小的20支股票
    g.num = 20
    #每次回归调用前61天的数据
    g.days = 61
    #设定股票池为沪深300中的股票
    g.security = get_index_stocks('000300.XSHG')
    g.tc = 15
    g.N = 60          # 需要前多少天的数据
    g.rf=0.04/252           #无风险利率
    
#2设置中间变量
def set_variables():
    g.t=0               #记录连续回测天数
    g.if_trade=False    #当天是否交易
    g.feasible_stocks=[]
    
#3设置回测条件
def set_backtest():
    set_benchmark('000300.XSHG')
    set_option('use_real_price',True) # 用真实价格交易
    log.set_level('order','error')    # 设置报错等级
    
'''
=====================================================================
每天回测前
=====================================================================
'''
def before_trading_start(context):
    if g.t % g.tc ==0:
        #每g.tc天,交易一次
        g.if_trade=True 
        # 设置手续费与手续费
        set_slip_fee(context) 
         # 设置可行股票池:获得当前开盘的沪深300股票池并剔除当前或者计算样本期间停牌的股票
        g.feasible_stocks = set_feasible_stocks(get_index_stocks('000300.XSHG'),g.N,context)
    g.t+=1
    
#4 设置可行股票池
def set_feasible_stocks(stock_list,days,context):
    # 得到是否停牌信息的dataframe,停牌的1,未停牌得0
    suspened_info_df = get_price(list(stock_list), start_date=context.current_dt, end_date=context.current_dt, frequency='daily', fields='paused')['paused'].T
    # 过滤停牌股票 返回dataframe
    unsuspened_index = suspened_info_df.iloc[:,0]<1
    # 得到当日未停牌股票的代码list:
    unsuspened_stocks = suspened_info_df[unsuspened_index].index
    # 进一步,筛选出前days天未曾停牌的股票list:
    feasible_stocks=[]
    current_data=get_current_data()
    for stock in unsuspened_stocks:
        if not(isnan(attribute_history(stock, days, unit='1d',fields=('close'),skip_paused=True).iloc[0,0])):
            feasible_stocks.append(stock)
    return feasible_stocks    

#5 根据不同的时间段设置滑点与手续费
def set_slip_fee(context):
    # 将滑点设置为0
    set_slippage(FixedSlippage(0)) 
    # 根据不同的时间段设置手续费
    dt=context.current_dt
    log.info(type(context.current_dt))
    
    if dt>datetime.datetime(2013,1, 1):
        set_commission(PerTrade(buy_cost=0.0003, sell_cost=0.0013, min_cost=5)) 
        
    elif dt>datetime.datetime(2011,1, 1):
        set_commission(PerTrade(buy_cost=0.001, sell_cost=0.002, min_cost=5))
            
    elif dt>datetime.datetime(2009,1, 1):
        set_commission(PerTrade(buy_cost=0.002, sell_cost=0.003, min_cost=5))
                
    else:
        set_commission(PerTrade(buy_cost=0.003, sell_cost=0.004, min_cost=5))


'''
=====================================================================
每天回测时
=====================================================================
'''
def handle_data(context, data):
    to_sell, to_buy = get_signal(context)
    sell_and_buy_stocks(context, to_sell, to_buy)

# 6
#将股票价格转化给收益率,输入为61天的股票收盘价,输出为60天的收益率,数据类型为list
def price2ret(price):
    ret = []
    rf = g.rf
    for i in range(len(price)-1):
        ret.append((price[i+1] - price[i])/price[i] - rf)
    return ret

#7 获得调仓信号
def get_signal(context):
    if g.if_trade == True:
        num = g.num
    #stocks为沪深300股票池中的股票代码list
        stocks = g.feasible_stocks
    #取61个交易日的数据
        days = g.days
    #security为沪深300的指数代码
        security = g.index
    #取得沪深300指数的收盘价
        marketporfolio = attribute_history(security, days, '1d', 'close')
    #计算沪深300指数的收益率
        marketreturn = price2ret(marketporfolio['close'])
        alpha=[]
    #用for循环计算沪深300股票池中的每个股票的alpha
        for stock in stocks:
            stockprice = attribute_history(stock, days, '1d', 'close')
            stockreturn = price2ret(stockprice['close'])
            beta, stockalpha, r_value, p_value, slope_std_error = stats.linregress(marketreturn, stockreturn)
            alpha.append([stock,stockalpha])
    #对每只股票的alpha进行排序
        sortedalpha = sorted(alpha,key=lambda X:X[1])
    #取alpha最小的20支股票——————————————
        targetstocks = sortedalpha[0:num]
        
    #——————————————————————————————————
        targetstock = []
        taralpha = []
    #用for循环得到目标股票的代码和对应的alpha
        for k1 in range(len(targetstocks)):
            targetstock.append(targetstocks[k1][0])
            taralpha.append(targetstocks[k1][1])
        taralpha = [(max(taralpha) - p)/(max(taralpha) - min(taralpha)) for p in taralpha]
        totalalpha = sum(taralpha)
    #计算每个股票的持仓权重:根据alpha来配权
        weights = [x/totalalpha for x in taralpha]
    #得到当前以持有的股票池
        present = context.portfolio.positions
    #得到当前股票池中的股票代码
        stocksnow = present.keys()
   
    #如果当前股票池中有股票,得到每个股票对应的仓位市值,数据结构为字典
        if len(stocksnow)>0:
            valuenow = []
            stockandvalue = {}
            for stock in stocksnow:
                s_amount = context.portfolio.positions[stock].sellable_amount
                l_s_p = context.portfolio.positions[stock].last_sale_price
                valuenow = s_amount*l_s_p
                stockandvalue[stock] = valuenow
    #得到当前股票组合和剩余资金总和
        capital = context.portfolio.portfolio_value
    #得到按照目标权重配股时对应的股票配资
        tarcapallocation = [capital*x for x in weights]
        tarstoandcap = dict(zip(targetstock, tarcapallocation))
        to_sell = {}
    #若当前持有股票市值大于目标市值,则进行卖出
        for stock in stocksnow:
            if stock in targetstock:
                gap = tarstoandcap[stock] - stockandvalue[stock]
                if gap<0:
                    to_sell[stock] = tarstoandcap[stock]
            else:
                to_sell[stock] = 0
    
    #按照目标配比对目标股票组合进行买入
        to_buy = {}
        for stock in targetstock:
            to_buy[stock] = tarstoandcap[stock]
        #print(to_sell, to_buy)
        return to_sell, to_buy
    else:
        return {},{}
#8 调整仓位
def sell_and_buy_stocks(context, to_sell, to_buy):
    if g.if_trade:
        for stock in to_sell.keys():
            order_target_value(stock, to_sell[stock])
        for stock in to_buy.keys():
            order_target_value(stock, to_buy[stock])
    g.if_trade = False
 

上一篇:python获取股票和基金等数据


下一篇:实习爬虫示例