python实现资产配置(1)----Markowitz 投资组合模型

1. 问题描述

现假设有A, B, C, D, E五只股票的收益率数据((第二日收盘价-第一日收盘价)/第一日收盘价)), 如果投资人的目标是达到20%的年收益率,那么该如何进行资产配置,才能使得投资的风险最低?

更一般的问题,假设现有x1,x2,...,xn, n支风险资产,且收益率已知,如果投资人的预期收益为goalRet,那么该如何进行资产配置,才能使得投资的风险最低?

2. Markowitz 投资组合理论简介

1952年,芝加哥大学的Markowitz提出现代资产组合理论(Modern Portfolio Theory,简称MPT),为现代西方证券投资理论奠定了基础。其基本思想是,证券投资的风险在于证券投资收益的不确定性。如果将收益率视为一个数学上的随机变量的话,证券的期望收益是该随机变量的数学期望(均值),而风险可以用该随机变量的方差来表示。

对于投资组合而言,如何分配各种证券上的投资比例,从而使风险最小而收益最大?

答案是将投资比例设定为变量,通过数学规划,对每一固定收益率求最小方差,对每一个固定的方差求最大收益率,这个多元方程的解可以决定一条曲线,这条曲线上的每一个点都对应着最优投资组合,即在给定风险水平下,收益率最大,这条曲线称作“有效前沿” (Efficient Frontier)。

对投资者而言,不存在比有效前沿更优的投资组合,只需要根据自己的风险偏好在有效前沿上寻找最优策略。
简化后的公式为:


公式(1)

其中 \overline{R}p 为投资人的投资目标,即投资人期待的投资组合的期望值. 目标函数说明投资人资产分配的原则是在达成投资目标\overline{R}p的前提下,要将资产组合的风险最小化,这个公式就是Markowitz在1952年发表的'Portfolio Selection'一文的精髓,该文奠定了现代投资组合理论的基础,也为Markowitz赢得了1990年的诺贝尔经济学奖. 公式(1)中的决策变量为wi, i = 1,...,N, 整个数学形式是二次规划(Quadratic Programming)问题,在允许卖空的情况下(即wi可以为负,只有等式约束)时,可以用拉格朗日(Lagrange)方法求解。

有效前缘曲线如下图:

只考虑风险资产的效率前缘

3. 二次规划求解方法介绍

3.1 等式约束凸二次规划的解法

我们考虑如下的二次规划问题


运用拉格朗日方法求解,可以得到

再看公式(1),则将目标函数由 min WT\SigmaW 调整为 min 1/2(WT\SigmaW), 两问题等价,写出的求解矩阵为:

3.2 含有不等式约束的凸二次规划的解法


关于该问题的解法,可利用KKT条件来进行求解,具体的方法可以参考:
拉格朗日乘子法和KKT条件,这里给大家简单介绍一下python中求解二次规划问题的包 CVXOPT。

工具包: CVXOPT python凸优化包
函数原型: CVXOPT.solvers.qp(P,q,G,h,A,b)

二次规划问题的标准形式

求解时,将对应的P,q,G,h,A,b写出,带入求解函数即可.值得注意的是输入的矩阵必须使用CVXOPT 中的matrix函数转化,输出的结果要使用 print(CVXOPT.solvers.qp(P,q,G,h,A,b)['x']) 函数才能输出。

4. 实例

这里选取五支股票2014-01-01到2015-01-01的收益率数据进行分析.
选取的五支股票分别为: 白云机场, 华夏银行, 浙能电力, 福建高速, 生益科技

先大体了解一下五支股票的收益率情况:


日收益率曲线
累计收益率曲线

看来,20%的预期收益是达不到了。

接下来,我们来看五支股票的相关系数矩阵:

五支股票的收益率相关系数矩阵

可以看出,白云机场和福建高速的相关性较高,因为二者同属于交通版块。在资产配置时,不利于降低非系统性风险。

接下来编写一个MeanVariance类,对于传入的收益率数据,可以进行给定预期收益的最佳持仓配比求解以及有效前缘曲线的绘制。

绘制的有效前缘曲线为:

有效前缘曲线

将数据分为训练集和测试集,并将随机模拟的资产配比求得的累计收益与测试集的数据进行对比,得到:

收益率对比

可以看出,在前半段大部分时间用Markowitz模型计算出的收益率要高于随机模拟的组合,然而在后半段却不如随机模拟的数据,可能是训练的数据不够或者没有动态调仓造成的,在后面写策略的时候,我会加入动态调仓的部分。

5. 代码

股票分析部分:

# -*- coding: utf-8 -*-
# @Time    : 2019/2/9 0:05
# @Author  : Arron Zhang
# @Email   : 549144697@qq.com
# @File    : plot return rate.py
# @Software: PyCharm

import pandas as pd
import baostock as bs
import matplotlib
from matplotlib import pyplot as plt
import numpy as np

#获取给定时间段的股票交易信息
def get_stock_data(t1,t2,stock_name):
    lg = bs.login()
    print('login respond error_code:' + lg.error_code)
    print('login respond  error_msg:' + lg.error_msg)

    #### 获取沪深A股历史K线数据 ####
    # 详细指标参数,参见“历史行情指标参数”章节
    rs = bs.query_history_k_data(stock_name,
                                 "date,code,open,high,low,close,preclose,volume,amount,adjustflag,turn,tradestatus,pctChg,isST",
                                 start_date=t1, end_date=t2,
                                 frequency="d", adjustflag="3")
    print('query_history_k_data respond error_code:' + rs.error_code)
    print('query_history_k_data respond  error_msg:' + rs.error_msg)

    #### 打印结果集 ####
    data_list = []
    while (rs.error_code == '0') & rs.next():
        # 获取一条记录,将记录合并在一起
        data_list.append(rs.get_row_data())
    result = pd.DataFrame(data_list, columns=rs.fields)
    print(result)

    #### 结果集输出到csv文件 ####
    result.to_csv("D:\stockdata\history_A_stock_k_data.csv", index=False)
    print(result)

    #### 登出系统 ####
    bs.logout()
    result['date'] = pd.to_datetime(result['date'])
    result.set_index("date", inplace=True)
    return result

byjc = get_stock_data('2014-1-1','2015-1-1','sh.600004')
hxyh = get_stock_data('2014-1-1','2015-1-1','sh.600015')
zndl = get_stock_data('2014-1-1','2015-1-1','sh.600023')
fjgs = get_stock_data('2014-1-1','2015-1-1','sh.600033')
sykj = get_stock_data('2014-1-1','2015-1-1','sh.600183')


by = byjc['pctChg']
by.name = 'byjc'
by = pd.DataFrame(by,dtype=np.float)/100


hx = hxyh['pctChg']
hx.name = 'hxyh'
hx = pd.DataFrame(hx,dtype=np.float)/100

zn = zndl['pctChg']
zn.name = 'zndl'
zn = pd.DataFrame(zn,dtype=np.float)/100

fj = fjgs['pctChg']
fj.name = 'fjgs'
fj = pd.DataFrame(fj,dtype=np.float)/100

sy = sykj['pctChg']
sy.name = 'sykj'
sy = pd.DataFrame(sy,dtype=np.float)/100

sh_return = pd.concat([by,fj,hx,sy,zn],axis=1)

#了解各股票的收益率状况,并作图
sh_return = sh_return.dropna()
cumreturn = (1+ sh_return).cumprod()
sh_return.plot()
plt.title('Daily Return of 5 Stocks(2014-2015)')
plt.legend(bbox_to_anchor = (0.5,-0.3),ncol = 5,fancybox = True,shadow = True)
cumreturn.plot()
plt.title('Cumulative Return of 5 stocks(2014-2015)')

Markowitz 投资组合模型求解

# -*- coding: utf-8 -*-
# @Time    : 2019/2/11 11:10
# @Author  : Arron Zhang
# @Email   : 549144697@qq.com
# @File    : Markowitz portfolio selection.py
# @Software: PyCharm

#构建一个MeanVariance类,该类可以根据输入的收益率序列,求解二次规划问题,计算出最优资产比例,并绘制最小方差前缘曲线
#定义 MeanVariance类
from matplotlib import pyplot as plt
from scipy import linalg
import numpy as np
import ffn
import cvxopt
from cvxopt import matrix
class MeanVariance:
    #定义构造器,传入收益率数据(dataframe格式的每日收益率)
    def __init__(self,returns):
        self.returns = returns

    #定义最小化方差的函数,即求解二次规划
    def minVar(self,goalRet):
        covs = np.array(self.returns.cov())
        means = np.array(self.returns.mean())
        L1 = np.append(np.append(covs.swapaxes(0,1),[means],axis=0),
                       [np.ones(len(means))],axis=0).swapaxes(0,1)

        L2 = list(np.ones(len(means)))
        L2.extend([0,0])
        L3 = list(means)
        L3.extend([0,0])
        L4 = np.array([L2,L3])
        L = np.append(L1,L4,axis=0)
        results = linalg.solve(L,np.append(np.zeros(len(means)),[1,goalRet]))

        return np.array([list(self.returns.columns), results[:-2]])

    #定义绘制最小方差前缘曲线函数
    def frontierCurve(self):
        goals = [x/500000 for x in range(-100,4000)]
        variances = list(map(lambda x: self.calVar(self.minVar(x)[1,:].astype(np.float)),goals))
        plt.plot(variances,goals)

    #给定各资产的比例,计算收益率的均值
    def meanRet(self,fracs):
        meanRisky = ffn.to_returns(self.returns).mean()
        #不符合条件时,弹出错误
        assert len(meanRisky) == len(fracs), 'length of fractions must be equal to number of assets'
        return np.sum(np.multiply(meanRisky,np.array(fracs)))

    #给定各资产的比例,计算收益率方差
    def calVar(self,fracs):
        #np.dot 可以将dataframe类型与矩阵直接相乘,得到的结果是array
        return (np.dot(np.dot(fracs,self.returns.cov()),fracs))

    #sim_weight = np.apply_along_axis(lambda x: x/sum(x),1,xim_weight)
    #apply_along_axis可以将函数作用于矩阵的行或者列
    #lambda函数的作用是对传入的参数直接给出结果,如果需要传入多个参数,可以写成array的形式传入,此案列中应用到了对数据的标准化中

    def solve_quadratic_problem(self,goal):
        covs = np.array(self.returns.cov())
        means = np.array(self.returns.mean())
        P = matrix(np.dot(2,covs))
        Q = matrix(np.zeros((len(means),1)))
        G = -matrix(np.zeros((len(means),len(means))))
        A = matrix(np.append([np.ones(len(means))],[means],axis=0))
        h = matrix(np.zeros((len(means),1)))
        b = matrix(np.array([[1,goal]]).swapaxes(0,1))
        sol = cvxopt.solvers.qp(P, Q, G , h, A, b)
        return sol

6. 参考资料

蔡立耑:量化投资——以python为工具. 电子工业出版社

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容