Python-量化金融第二版-一-

Python 量化金融第二版(一)

原文:zh.annas-archive.org/md5/25334ce178953792df9684c784953114

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我们坚信,任何一位雄心勃勃的金融专业学生都应该学习至少一门计算机语言。基本原因是我们已经进入了所谓的大数据时代。在金融领域,我们有大量的数据,并且大部分数据是公开的,可以免费获得。为了高效利用这些丰富的数据源,我们需要一个工具。在众多潜在的候选工具中,Python 是最佳选择之一。

关于第二版的几点话

对于第二版,我们重新组织了本书的结构,增加了更多与金融相关的章节。这是对众多读者反馈的认可和回应。第二版的前两章专门介绍 Python,之后的所有章节都与金融相关。再次强调,本书中的 Python 主要作为工具,帮助读者更好地学习和理解金融理论。为了满足各种量化程序、商业分析程序和金融工程程序对各类数据的使用需求,我们新增了第四章,数据来源。由于这种结构调整,本版更加适合如定量金融、使用 Python 进行金融分析和商业分析等为期一个学期的课程。宾州州立大学的 Premal P. Vora 教授和西敏大学的 Sheng Xiao 教授已经将第一版作为他们的教材。希望更多的金融、会计学教授会发现第二版更适合他们的学生,特别是金融工程、商业分析和其他定量领域的学生。

为什么选择 Python?

使用 Python 有多种理由。首先,Python 是免费的,无需授权费用。Python 可在所有主要操作系统上运行,如 Windows、Linux/Unix、OS/2、Mac 和 Amiga 等。免费这一点有很多好处。当学生毕业后,他们可以将学到的知识应用到任何地方,金融领域也是如此。相比之下,SAS 和 MATLAB 并非如此。其次,Python 功能强大、灵活且易学。它几乎能够解决我们所有的金融和经济估算问题。第三,Python 可应用于大数据。Dasgupta(2013)认为,R 和 Python 是两种最受欢迎的开源数据分析编程语言。第四,Python 中有许多有用的模块,每个模块都是为特定目的开发的。本书中,我们重点介绍了 NumPy、SciPy、Matplotlib、Statsmodels 和 Pandas 模块。

一本由金融教授编写的编程书籍

毫无疑问,大多数编程书籍都是由计算机科学的教授编写的。由一位金融学教授编写编程书籍似乎有些奇怪。可以理解的是,重点会有所不同。如果是计算机科学的讲师在编写这本书,自然重点会放在 Python 上,而真正的重点应该是金融。书名《Python 金融应用》应该可以让人一目了然。此书旨在改变目前许多服务于金融界的编程书籍过于注重编程语言本身,而忽视了金融内容的现状。书中的另一个独特之处是,它使用了大量与经济学、金融和会计相关的公共数据,详见第四章,数据来源部分。

本书内容简介

第一章,Python 基础,提供了简短的介绍,并解释了如何安装 Python、如何启动和退出 Python、变量赋值、向量、矩阵和元组、调用嵌入式函数、编写自己的函数、从输入文件读取数据、简单的数据处理、输出数据和结果,以及生成一个带有 pickle 扩展的 Python 数据集。

第二章,Python 模块介绍,讨论了模块的含义、如何导入模块、显示导入模块中包含的所有函数、为导入的模块取一个简短的别名、对比 import math 和 from math import、删除已导入的模块、仅导入模块中的某些函数、NumPy、SciPy、matplotlib、statsmodels、pandas 和 Pandas_reader 的介绍,如何查找所有内置模块及所有可用的(预安装的)模块,以及如何查找特定的未安装模块。

第三章,货币的时间价值,介绍并讨论了与金融相关的各种基本概念和公式,如单一未来现金流的现值、(增长型)永续年金的现值、年金的现值和未来值、永续年金与即付永续年金的区别、年金与即付年金的区别、SciPy 和 numpy.lib.financial 子模块中的相关函数、一个用 Python 编写的免费的金融计算器、净现值(NPV)的定义及其相关规则、内部收益率(IRR)的定义及其相关规则、时间价值的 Python 图形展示,以及 NPV 曲线。

第四章,数据来源,讨论了如何从各种公共来源获取数据,如 Yahoo!Finance、Google finance、FRED(美联储经济数据图书馆)、French 教授的数据图书馆、BLS(美国劳工统计局)和人口普查局。此外,还会讨论各种输入数据的方法,例如 csv、txt、pkl、Matlab、SAS 或 Excel 格式的文件。

第五章, 债券与股票估值,介绍了利率及其相关概念,如年利率(APR)、有效年利率(EAR)、复利频率,如何将一种有效利率转换为另一种,利率期限结构,如何估算普通债券的售价,如何使用所谓的折现股息模型来估算股票价格等。

第六章, 资本资产定价模型,展示了如何从 Yahoo!Finance 下载数据以运行 CAPM 的线性回归,滚动 beta,几个 Python 程序来估算多只股票的 beta、调整 beta 和投资组合 beta 估算,Scholes 和 Williams(1977)Dimson(1979)提出的两种 beta 调整方法。

第七章, 多因子模型与业绩衡量,展示了如何将第六章中描述的单因子模型,资本资产定价模型,扩展到多因子和复杂模型,如 Fama-French 三因子模型、Fama-French-Carhart 四因子模型、Fama-French 五因子模型,以及业绩衡量指标,如 Sharpe 比率、Treynor 比率、Sortino 比率和 Jensen's alpha。

第八章, 时间序列分析,展示了如何设计良好的日期变量,通过该日期变量合并数据集,正态分布、正态性检验、利率期限结构、52 周高低交易策略、收益估算、将日收益转换为月度或年度收益、T 检验、F 检验、Durbin-Watson 自相关检验、Fama-MacBeth 回归、Roll(1984)价差、Amihud(2002)流动性测量、Pastor 和 Stambaugh(2003)流动性指标、1 月效应、星期效应、从 Google Finance 和 Hasbrouck 教授的 TORQ 数据库(交易、订单、报告和报价)中检索高频数据,以及介绍 CRSP(证券价格研究中心)数据库。

第九章, 投资组合理论,讨论了 2 只股票投资组合、N 只股票投资组合的均值和风险估算,相关性与多样化效应,如何生成收益矩阵,基于 Sharpe 比率、Treynor 比率和 Sortino 比率生成最佳投资组合;如何构建有效前沿;Modigliani 和 Modigliani 业绩衡量(M2 衡量);以及如何使用市值加权和等权重方法估算投资组合收益。

第十章,期权与期货,讨论了看涨期权和看跌期权的收益和盈亏函数及其图示表示,欧洲期权与美式期权;正态分布;标准正态分布;累积正态分布;著名的布莱克-斯科尔斯-莫顿期权模型(有无红利);各种交易策略及其可视化表示,如备兑看涨、跨式期权、蝶式期权和日历差价期权;希腊字母;看跌-看涨平价及其图形表示;单步和两步二项树模型的图形表示;如何使用二项树方法定价欧洲期权和美式期权;以及隐含波动率、波动率微笑和波动率偏斜。

第十一章,风险价值,首先回顾了正态分布的密度函数和累积分布函数,然后讨论了基于正态性假设估算 VaR 的第一种方法,如何从 1 天风险转换为 n 天风险,从 1 天 VaR 转换为 n 天 VaR,正态性检验,偏度和峰度的影响,如何通过包括偏度和峰度来修正 VaR 度量,基于历史收益的第二种 VaR 估算方法,如何通过蒙特卡洛模拟将两种方法链接起来,回测和压力测试。

第十二章,蒙特卡洛模拟,讨论了如何通过蒙特卡洛模拟估算π值;使用对数正态分布模拟股票价格波动;构建有效的投资组合和有效前沿;通过模拟复制布莱克-斯科尔斯-莫顿期权模型;定价多种奇异期权,如浮动行权价的回顾期权;有放回/无放回的自助法;长期预期收益预测及其相关效率,准蒙特卡洛模拟和索博尔序列。

第十三章,信用风险分析,讨论了穆迪、标准普尔和惠誉的信用评级、信用利差、1 年和 5 年迁移矩阵、利率期限结构、阿尔特曼 Z 分数用于预测企业破产、KMV 模型用于估算总资产及其波动性、违约概率和违约距离,以及信用违约掉期。

第十四章,奇异期权,首先比较了我们在第九章,投资组合理论 中学到的欧洲期权和美式期权与伯穆达期权,然后讨论了定价简单选择期权、喊叫期权、彩虹期权和二元期权的方法;平均价格期权;障碍期权,如向上进场期权和向上退场期权;以及障碍期权,如向下进场和向下退场期权。

第十五章,波动率、隐含波动率、ARCH 与 GARCH,重点讨论两个问题:波动率度量和 ARCH/GARCH 模型。

面向小程序

根据作者在七所学校的教学经验,包括加拿大的麦吉尔大学和威尔弗里德·劳里大学、新加坡的南洋理工大学,以及美国的洛约拉大学、马里兰大学大学城分校、霍夫斯特拉大学和卡尼修斯学院,再加上他在沃顿商学院的八年咨询经验,他知道许多金融学生喜欢解决单一特定任务的小程序。大多数编程书籍仅提供几个完整且复杂的程序,程序的数量远远不足。采取这种方法有两个副作用。首先,金融学生会被编程细节淹没,感到畏惧,最终失去学习计算机语言的兴趣。其次,他们没有学会如何应用所学的知识,例如运行资本资产定价模型(CAPM)来估算 1990 年到 2013 年间 IBM 的贝塔值。本书提供了约 300 个完整的 Python 程序,涵盖许多金融话题。

使用真实数据

大多数编程书籍的另一个缺点是它们使用假设数据。而在本书中,我们使用真实世界的数据来探讨各类金融话题。例如,我没有仅仅展示如何运行 CAPM 来估算贝塔(市场风险),而是向你展示如何估算 IBM、苹果或沃尔玛的贝塔。与仅呈现公式来估算投资组合的收益和风险不同,本书提供了 Python 程序来下载真实数据、构建各种投资组合,然后估算它们的收益和风险,包括风险价值(VaR)。当我还是一名博士生时,我学到了波动率微笑的基本概念。然而,直到写这本书时,我才有机会下载真实世界的数据来绘制 IBM 的波动率微笑。

本书所需内容

在这里,我们通过几个具体的例子来展示读者在认真阅读本书后能够取得的成就。

首先,在阅读完前两章后,读者/学生应该能够使用 Python 计算现值、未来值、年金现值、内部收益率(IRR)以及许多其他金融公式。换句话说,我们可以将 Python 作为一个免费的普通计算器来解决许多金融问题。其次,在完成前三章后,读者/学生或金融教师能够构建一个免费的金融计算器,即将数十个小的 Python 程序组合成一个大的 Python 程序。这个大程序的表现就像是任何其他人编写的模块。第三,读者将学习如何编写 Python 程序,下载并处理来自各种公共数据源的金融数据,例如 Yahoo! Finance、Google Finance、美国联邦储备数据库和法国教授的数据库。

第四,读者将理解与模块相关的基本概念,模块是专家、其他用户或我们为特定目的编写的包。第五,理解了 Matplotlib 模块后,读者可以生成各种图表。例如,读者可以通过结合基础股票和期权,使用图表展示基于不同交易策略的收益/利润结果。第六,读者将能够从 Yahoo! Finance 下载 IBM 的每日价格、标准普尔 500 指数价格,并通过应用资本资产定价模型(CAPM)来估算其市场风险(贝塔系数)。他们还将能够形成包含不同证券的投资组合,例如无风险资产、债券和股票。然后,他们可以通过应用 Markowitz 的均值-方差模型来优化他们的投资组合。此外,读者将知道如何估算其投资组合的风险价值(VaR)。

第七,读者应该能够通过应用 Black-Scholes-Merton 期权模型(仅适用于欧洲期权)和蒙特卡罗模拟(适用于欧洲期权和美国期权)来定价欧洲期权和美国期权。最后但同样重要的是,读者将学习几种衡量波动率的方法。特别是,他们将学习如何使用自回归条件异方差(ARCH)模型和广义自回归条件异方差(GARCH)模型。

本书适合谁阅读

如果你是金融专业的研究生,特别是学习计算金融、金融建模、金融工程或商业分析的学生,那么本书将对你大有裨益。这里有两个例子:宾夕法尼亚州立大学的 Premal P. Vora 教授在他的课程 数据科学与金融 中使用了本书,而西敏寺学院的 Sheng Xiao 教授则在他的课程 金融分析 中使用了本书。如果你是专业人士,你可以学习 Python 并将其应用于许多金融项目。如果你是个人投资者,阅读本书同样也会让你受益。

约定

本书中,你将发现几种文本样式,用于区分不同类型的信息。以下是这些样式的一些示例,并解释它们的含义。

文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入以及 Twitter 用户名通常显示如下:“sqrt() 函数,平方根,包含在 math 模块中。”

一段代码如下所示:

>>>sqrt(2)
NameError: name 'sqrt' is not defined
>>> Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
math.sqrt(2)
1.4142135623730951
>>>

任何命令行输入或输出如下所示:

help(pv_f)

新术语重要词汇以粗体显示。在屏幕上看到的单词,例如菜单或对话框中的单词,将以这种形式出现在文本中:“要编写 Python 程序,我们点击 文件,然后点击 新建文件。”

注意

警告或重要说明会显示在像这样的框中。

提示

提示和技巧如下所示。

读者反馈

我们总是欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或可能不喜欢的部分。读者的反馈对我们开发出让你真正受益的书籍至关重要。

若要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提到书籍标题。

如果你在某个领域有专长,并且有兴趣为书籍撰写或贡献内容,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在,你已经是一本 Packt 书籍的骄傲拥有者,我们提供了许多资源帮助你从购买中获得最大价值。

下载示例代码

你可以从你的账户中下载本书的示例代码文件,访问:www.packtpub.com。如果你从其他地方购买了这本书,可以访问:www.packtpub.com/support,并注册后直接将文件通过电子邮件发送给你。

你可以按照以下步骤下载代码文件:

  1. 你可以按照以下步骤下载代码文件:

  2. 使用你的电子邮件地址和密码登录或注册我们的网站。

  3. 将鼠标指针悬停在顶部的SUPPORT标签上。

  4. 点击Code Downloads & Errata

  5. Search框中输入书名。

  6. 选择你想要下载代码文件的书籍。

  7. 从下拉菜单中选择你购买此书的地方。

  8. 点击Code Download

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址为:github.com/PacktPublishing/Python-for-Finance-Second-Edition。我们还提供来自我们丰富书籍和视频目录的其他代码包,地址为:github.com/PacktPublishing/。快去看看吧!

勘误表

尽管我们已经尽一切努力确保内容的准确性,但错误还是可能发生。如果您在我们的一本书中发现错误——无论是文本错误还是代码错误——我们将非常感激您能报告给我们。这样,您可以帮助其他读者避免困扰,并帮助我们改进书籍的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata 提交,选择您的书籍,点击 勘误 提交 表单 链接,并填写勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误将在我们的网站上上传,或加入到该书籍现有的勘误列表中,位于该书籍的勘误部分。您可以通过访问 www.packtpub.com/support 来查看任何现有的勘误。

盗版

网络上侵犯版权的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现任何非法的我们作品的复制品,无论其形式如何,请立即向我们提供位置地址或网站名称,以便我们采取相应措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

感谢您的帮助,帮助我们保护作者,并支持我们为您提供有价值的内容。

问题

如果您在书籍的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们会尽力解决。

第一章:Python 基础

本章将讨论基本概念和一些与 Python 相关的广泛使用的函数。本章以及下一章(第二章,Python 模块简介)是唯一完全基于 Python 技术的章节。这两章作为对有一定 Python 基础的读者的复习内容。没有任何 Python 基础的初学者,仅通过阅读这两章是不可能掌握 Python 的。对于想要更详细学习 Python 的新手,他们可以找到许多优秀的书籍。从第三章,货币时间价值开始,我们将使用 Python 来解释或演示各种金融概念、运行回归分析以及处理与经济学、金融和会计相关的数据。因此,我们将在接下来的每一章中提供更多与 Python 相关的技术和使用方法。

本章特别讨论以下主题:

  • Python 安装

  • 变量赋值、空格和编写我们自己的程序

  • 编写 Python 函数

  • 数据输入

  • 数据处理

  • 数据输出

Python 安装

本节将讨论如何安装 Python。更具体地说,我们将讨论两种方法:通过 Anaconda 安装 Python 和直接安装 Python。

有几个原因说明为什么首选第一种方法:

  • 首先,我们可以使用一个名为 Spyder 的 Python 编辑器,它对于编写和编辑我们的 Python 程序非常方便。例如,它有几个窗口(面板):一个用于控制台,我们可以在其中直接输入命令;一个用于程序编辑器,我们可以在其中编写和编辑我们的程序;一个用于 变量浏览器,我们可以查看我们的变量及其值;还有一个用于帮助,我们可以在其中寻求帮助。

  • 第二,不同颜色的代码或注释行将帮助我们避免一些明显的拼写错误和失误。

  • 第三,安装 Anaconda 时,许多模块会同时安装。模块是由专家、专业人士或任何围绕特定主题的人编写的一组程序。它可以看作是某个特定任务的工具箱。为了加速新工具的开发过程,一个新的模块通常依赖于其他已经开发的模块中的功能。这被称为模块依赖性。这样的模块依赖性有一个缺点,就是如何同时安装它们。有关更多信息,请参阅第二章,Python 模块简介

通过 Anaconda 安装 Python

我们可以通过几种方式安装 Python。结果是我们将有不同的环境来编写和运行 Python 程序。

以下是一个简单的两步法。首先,我们访问continuum.io/downloads并找到合适的安装包;请参见以下截图:

通过 Anaconda 安装 Python

对于 Python,不同版本共存。从前面的截图中,我们看到有两个版本,分别是 3.5 和 2.7。

对于本书,版本并不是非常关键。旧版本问题较少,而新版本通常有新的改进。同样,模块依赖可能会成为一个大难题;详见第二章,Python 模块简介,以获取更多信息。Anaconda 的版本是 4.2.0。由于我们将通过 Spyder 启动 Python,因此它也可能有不同的版本。

通过 Spyder 启动 Python

通过 Anaconda 安装 Python 后,我们可以导航到Start(Windows 版本)|All Programs|Anaconda3(32-bit),如以下截图所示:

通过 Spyder 启动 Python

在我们点击Spyder,即前面截图中的最后一个条目后,下面将显示四个面板:

通过 Spyder 启动 Python

左上方的面板(窗口)是我们的程序编辑器,在这里我们编写程序。右下方的面板是 IPython 控制台,我们可以在其中输入简单的命令。IPython 是默认的控制台。要了解更多关于 IPython 的信息,只需输入一个问号;请参见以下截图:

通过 Spyder 启动 Python

另外,我们可以通过点击菜单栏上的Consoles,然后选择Open a Python console来启动 Python 控制台。之后,以下窗口将出现:

通过 Spyder 启动 Python

在四个面板的图像中,右上方的面板是我们的帮助窗口,在这里我们可以寻求帮助。中间的面板称为变量浏览器,其中显示了变量的名称及其值。根据个人喜好,用户可以调整这些面板的大小或重新组织它们。

直接安装 Python

对于大多数用户来说,了解如何通过 Anaconda 安装 Python 已经足够了。为了完整性,下面展示了安装 Python 的第二种方式。

以下是涉及的步骤:

  1. 首先,访问www.python.org/download直接安装 Python

  2. 根据你的电脑选择合适的安装包,例如 Python 版本 3.5.2。对于本书而言,Python 的版本并不重要。在这个阶段,新的用户可以直接安装最新版本的 Python。安装后,我们将看到以下 Windows 版本的条目:直接安装 Python

  3. 要启动 Python,我们可以点击IDLE (Python 3.5\. 32 bit)并看到以下界面:直接安装 Python

  4. 从截图中显示的四个面板中的 IPython,或者从 Python 控制台面板,或从之前显示 Python Shell 的截图中,我们可以输入各种命令,如下所示:

    >>>pv=100
    >>>pv*(1+0.1)**20
    672.7499949325611
    >>> import math
    >>>math.sqrt(3)
    1.7320508075688772
    >>>
    
    
  5. 要编写一个 Python 程序,我们点击文件,然后点击新建文件直接安装 Python

  6. 输入这个程序并保存:直接安装 Python

  7. 点击运行,然后点击运行模块。如果没有错误发生,我们就可以像使用其他内置函数一样使用该函数,如下所示:直接安装 Python

变量赋值、空格和编写我们自己的程序

首先,对于 Python 语言,空格或空格非常重要。例如,如果我们在输入pv=100之前不小心多了一个空格,我们将看到如下错误信息:

变量赋值、空格和编写我们自己的程序

错误的名称是IndentationError。原因是,对于 Python 来说,缩进非常重要。在本章稍后的内容中,我们将学习如何通过适当的缩进来规范/定义我们编写的函数,或者为何一组代码属于特定的主题、函数或循环。

假设我们今天在银行存入 100 美元。如果银行提供我们年存款利率 1.5%,那么 3 年后的价值是多少?相关代码如下所示:

>>>pv=100
>>>pv
    100
>>>pv*(1+0.015)**3
    104.56783749999997
>>>

在前面的代码中,**表示幂运算。例如,2**3的值是8。要查看变量的值,我们只需输入变量名;请参见前面的例子。使用的公式如下所示:

变量赋值、空格和编写我们自己的程序

在这里,FV是未来值,PV是现值,R是周期存款利率,而n是周期数。在这个例子中,R是年利率0.015,而n3。此时,读者应该专注于简单的 Python 概念和操作。

在第三章中,时间价值,该公式将详细解释。由于 Python 是区分大小写的,如果我们输入PV而不是pv,就会弹出错误信息;请参见以下代码:

>>>PV
NameError: name 'PV' is not defined
>>>Traceback (most recent call last):
  File "<stdin>", line 1, in <module>

与一些语言(如 C 和 FORTRAN)不同,Python 中变量不需要在赋值之前定义。要显示所有变量或函数,我们使用dir()函数:

>>>dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'pv']
>>>

要查看所有内置函数,我们输入dir(__builtings__)。输出如下所示:

变量赋值、空格和编写我们自己的程序

编写一个 Python 函数

假设我们有兴趣为方程式(1)编写一个 Python 函数。

启动 Spyder 后,点击文件,然后点击新建文件。我们编写以下两行代码,如左侧面板所示。关键字def表示函数,fv_f是函数名称,括号中的三个值pvrn是输入变量。

冒号(:)表示函数尚未结束。当我们按下 Enter 键时,下一行将自动缩进。

当我们输入 return pv*(1+r)**n 并按下 Enter 键两次时,这个简单的程序就完成了。显然,在第二行中,** 代表了一个幂运算。

假设我们将其保存为 c:/temp/temp.py

编写 Python 函数

要运行或调试程序,请点击菜单栏下方 Run 旁的箭头键;见前面的右上方图像。编译结果由右下方的图像显示(右上方第二张图)。现在,我们可以通过传入三个输入值轻松调用这个函数:

>>>fv_f(100,0.1,2)
     121.00000000000001
>>>fv_f(100,0.02,20)
    148.59473959783548

如果在程序中添加一些注释,解释输入变量的含义、使用的公式以及一些示例,将对其他用户或程序员非常有帮助。请查看以下带注释的程序:

def pv_f(fv,r,n):
    """Objective: estimate present value
                     fv
    formula  : pv=-------------
                   (1+r)^n
          fv: fture value
          r : discount periodic rate
          n : number of periods

    Example #1  >>>pv_f(100,0.1,1)
                   90.9090909090909

    Example #2: >>>pv_f(r=0.1,fv=100,n=1)
                    90.9090909090909
    """
    return fv/(1+r)**n

注释或解释内容包含在一对三重双引号中("""""")。注释中的缩进不重要。在编译时,底层软件会忽略所有注释。这些注释的美妙之处在于,我们可以通过 help(pv_f) 查看它们,如下所示:

编写 Python 函数

在 第二章,Python 模块介绍 中,我们将展示如何上传用 Python 编写的金融计算器;在 第三章,货币的时间价值 中,我们将解释如何生成这样的金融计算器。

Python 循环

在本节中,我们讨论一个非常重要的概念:循环。循环用于重复执行相同的任务,只是输入或其他因素略有不同。

Python 循环,if...else 条件

让我们来看一个简单的循环,遍历数组中的所有数据项:

>>>import numpy as np
>>>cashFlows=np.array([-100,50,40,30])
>>>for cash in cashFlows:
...    print(cash)
... 
-100
50
40
30

一种数据类型叫做元组,我们使用一对圆括号 () 来包含所有输入值。元组变量的一个特点是我们不能修改它的值。如果有些变量永远不应该被修改,这个特殊的属性可能非常有用。元组与字典不同,字典通过键值对存储数据。字典是无序的,并且要求键是可哈希的。与元组不同,字典的值是可以修改的。

注意,对于 Python,向量或元组的下标是从 0 开始的。如果 x 的长度为 3,那么下标将是 012

>>> x=[1,2,3]
>>>x[0]=2
>>>x
>>>
     [2, 2, 3]
>>> y=(7,8,9)
>>>y[0]=10
>>>
TypeError: 'tuple' object does not support item assignment
>>>Traceback (most recent call last):
  File "<stdin>", line 1, in <module>

>>>type(x)
>>>
<class'list'>
>>>type(y)
>>>
<class'tuple'>
>>>

假设我们今天投资 $100,并且明年再投资 $30,那么未来 5 年每年年末的现金流入将分别是 $10、$40、$50、$45 和 $20,从第二年年末开始;见下方的时间轴及其对应的现金流:

-100    -30       10       40        50         45       20
|--------|---------|--------|---------|----------|--------|
0        1         2        3         4          5        6

如果折现率为 3.5%,净现值NPV)是多少?NPV 定义为所有收益的现值减去所有成本的现值。如果现金流入为正,现金流出为负,那么 NPV 可以方便地定义为所有现金流现值的总和。一个未来价值的现值是通过应用以下公式估算的:

Python 循环,if...else 条件

这里,PV 是现值,FV 是未来值,R 是期间折现率,n 是期数。在第三章,货币的时间价值中,我们将更详细地解释这个公式的意义。目前,我们只想编写一个npv_f()函数,应用上述公式n 次,其中 n 是现金流的数量。完整的 NPV 程序如下所示:

def npv_f(rate, cashflows):
       total = 0.0
       for i in range(0,len(cashflows)):
             total += cashflows[i] / (1 + rate)**i
       return total

在程序中,我们使用了for循环。同样,Python 中正确的缩进非常重要。第 2 到第 5 行都缩进了一个单位,因此它们属于同一个函数,名为npv_f。类似地,第 4 行缩进了两个单位,也就是在第二个冒号(:)后,它属于for循环。total +=a 命令等价于 total=total +a

对于 NPV 函数,我们使用for循环。注意,Python 中向量的索引从零开始,临时变量i也从零开始。我们可以通过输入两组值来轻松调用此函数。输出如下所示:

>>>r=0.035
>>>cashflows=[-100,-30,10,40,50,45,20]
>>>npv_f(r,cashflows)
14.158224763725372 

这是另一个带有enumerate()函数的npv_f()函数。这个函数将生成一对索引,从0开始,并返回其对应的值:

def npv_f(rate, cashflows):
      total = 0.0
      for i, cashflow in enumerate(cashflows):
               total += cashflow / (1 + rate)**i
      return total

这里是一个使用enumerate()的例子:

x=["a","b","z"]
for i, value in enumerate(x):
      print(i, value)

与之前指定的npv_f函数不同,Microsoft Excel 中的 NPV 函数实际上是一个 PV 函数,这意味着它只能应用于未来值。其等效的 Python 程序,称为 npv_Excel,如下所示:

def npv_Excel(rate, cashflows):
       total = 0.0
       for i, cashflow in enumerate(cashflows):
                total += cashflow / (1 + rate)**(i+1)
       return total

比较结果如下表所示。左侧面板显示了 Python 程序的结果,右侧面板显示了调用 Excel NPV 函数的结果。请特别注意前面的程序以及如何调用这样的函数:

Python 循环,if...else 条件

通过使用循环,我们可以用不同的输入重复相同的任务。例如,我们计划打印一组值。以下是一个while循环的例子:

i=1
while(i<10):
      print(i)
      i+=1

以下程序将报告一个折现率(或多个折现率),使其相应的 NPV 等于零。假设现金流分别为 550-500-500-5001000,时间点为 0,并且接下来 4 年每年的年末。有关此练习的概念,我们将在第三章中进一步解释。

编写一个 Python 程序,找出哪个折扣率使得 NPV 等于零。由于现金流的方向变化两次,我们可能有两个不同的折扣率使得 NPV 为零:

cashFlows=(550,-500,-500,-500,1000)
r=0
while(r<1.0):
     r+=0.000001
     npv=npv_f(r,cashFlows)
     if(abs(npv)<=0.0001):
            print(r)

相应的输出如下:

0.07163900000005098
0.33673299999790873

在本章后面,将使用for循环来估算一个项目的 NPV。

当我们需要使用一些数学函数时,可以先导入math模块:

>>>import math
>>>dir(math)
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc']
>>>math.pi
3.141592653589793
>>>

sqrt()平方根函数包含在math模块中。因此,要使用sqrt()函数,我们需要使用math.sqrt();请参见以下代码:

>>>sqrt(2)
NameError: name 'sqrt' is not defined
>>>Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
math.sqrt(2)
1.4142135623730951
>>>

如果我们希望直接调用这些函数,可以使用from math import *;请参见以下代码:

>>>from math import *
>>>sqrt(3)
1.7320508075688772
>>>

要了解各个嵌入函数,我们可以使用help()函数;请参见以下代码:

>>>help(len)
Help on built-in function len in module builtins:
len(obj, /)
    Return the number of items in a container.
>>>

数据输入

首先,我们生成一个非常简单的输入数据集,如下所示。它的名称和位置为c:/temp/test.txt。数据集的格式是文本:

a b
1 2
3 4

代码如下:

>>>f=open("c:/temp/test.txt","r")
>>>x=f.read()
>>>f.close()

可以使用print()函数来显示x的值:

>>>print(x)
a b
1 2
3 4
>>>

对于第二个示例,首先让我们从Yahoo!Finance下载 IBM 的每日历史价格数据。为此,我们访问finance.yahoo.com

数据输入

输入IBM以找到其相关网页。然后点击历史数据,再点击下载

数据输入

假设我们将每日数据保存为ibm.csv,并放在c:/temp/目录下。前五行如下所示:

Date,Open,High,Low,Close,Volume,Adj Close
2016-11-04,152.399994,153.639999,151.869995,152.429993,2440700,152.429993
2016-11-03,152.509995,153.740005,151.800003,152.369995,2878800,152.369995
2016-11-02,152.479996,153.350006,151.669998,151.949997,3074400,151.949997
2016-11-01,153.50,153.910004,151.740005,152.789993,3191900,152.789993

第一行显示了变量名:日期、开盘价、交易日内的最高价、交易日内的最低价、交易日内最后一次交易的收盘价、交易量和交易日的调整价格。分隔符是逗号。加载文本文件有几种方法,这里讨论了一些方法:

  • 方法一:我们可以使用pandas模块中的read_csv

    >>> import pandas as pd
    >>> x=pd.read_csv("c:/temp/ibm.csv")
    >>>x[1:3]
             Date        Open        High         Low       Close   Volume  \
    1  2016-11-02  152.479996  153.350006  151.669998  151.949997  3074400   
    2  2016-11-01  153.500000  153.910004  151.740005  152.789993  3191900   
    
    Adj.Close
    1  151.949997
    2  152.789993>>>
    
  • 方法二:我们可以使用pandas模块中的read_table;请参见以下代码:

    >>> import pandas as pd
    >>> x=pd.read_table("c:/temp/ibm.csv",sep=',')
    

另外,我们可以直接从 Yahoo!Finance 下载 IBM 的每日价格数据;请参见以下代码:

>>> import pandas as pd
>>>url=url='http://canisius.edu/~yany/data/ibm.csv'
>>> x=pd.read_csv(url)
>>>x[1:5]
         Date        Open        High         Low       Close   Volume  \
1  2016-11-03  152.509995  153.740005  151.800003  152.369995  2843600   
2  2016-11-02  152.479996  153.350006  151.669998  151.949997  3074400   
3  2016-11-01  153.500000  153.910004  151.740005  152.789993  3191900   
4  2016-10-31  152.759995  154.330002  152.759995  153.690002  3553200   

Adj Close  
1  152.369995
2  151.949997
3  152.789993
4  153.690002>>>

我们可以通过使用pandas模块中的ExcelFile()函数从 Excel 文件中检索数据。首先,我们生成一个包含少量观测值的 Excel 文件;请参见以下截图:

数据输入

假设我们将此 Excel 文件命名为stockReturns.xlxs,并保存到c:/temp/。Python 代码如下:

>>>infile=pd.ExcelFile("c:/temp/stockReturns.xlsx")
>>> x=infile.parse("Sheet1")
>>>x
date  returnAreturnB
0  2001     0.10     0.12
1  2002     0.03     0.05
2  2003     0.12     0.15
3  2004     0.20     0.22
>>>

要检索扩展名为.pkl.pickle的 Python 数据集,我们可以使用以下代码。首先,我们从作者的网页www3.canisius.edu/~yany/python/ffMonthly.pkl下载名为ffMonthly.pkl的 Python 数据集。

假设数据集保存在c:/temp/目录下。可以使用pandas模块中名为read_pickle()的函数加载扩展名为.pkl.pickle的数据集:

>>> import pandas as pd
>>> x=pd.read_pickle("c:/temp/ffMonthly.pkl")
>>>x[1:3]
>>>
Mkt_RfSMBHMLRf
196308  0.0507 -0.0085  0.0163  0.0042
196309 -0.0157 -0.0050  0.0019 -0.0080
>>>

以下是最简单的 if 函数:当我们的利率为负时,打印一个警告信息:

if(r<0):
    print("interest rate is less than zero")

与逻辑 ANDOR 相关的条件如下所示:

>>>if(a>0 and b>0):
  print("both positive")
>>>if(a>0 or b>0):
  print("at least one is positive")

对于多个 if...elif 条件,以下程序通过将数字等级转换为字母等级来说明其应用:

grade=74
if grade>=90:
    print('A')
elif grade >=85:
    print('A-')
elif grade >=80:
    print('B+')
elif grade >=75:
    print('B')
elif grade >=70:
    print('B-')
elif grade>=65:
    print('C+')
else:
    print('D')

请注意,对于这种多重 if...elif 函数,最好以 else 条件结尾,因为如果没有满足这些条件,我们确切知道结果是什么。

数据操作

数据有很多不同的类型,比如整数、实数或字符串。以下表格列出了这些数据类型:

数据类型 描述
Bool 布尔值(TRUEFALSE)以字节存储
Int 平台整数(通常为 int32int64
int8 字节(-128127
int16 整数(-3276832767
int32 整数(-21474836482147483647
int64 整数(92233720368547758089223372036854775807
unit8 无符号整数(0255
unit16 无符号整数(065535
unit32 无符号整数(04294967295
unit64 无符号整数(018446744073709551615
float 短浮点数,用于 float6
float32 单精度浮点数:符号位 bit23 位尾数;8 位指数
float64 52 位尾数
complex complex128 的简写
complex64 复数;由两个 32 位浮点数表示(实部和虚部)
complex128 复数;由两个 64 位浮点数表示(实部和虚部)

表 1.1 不同数据类型的列表

在以下示例中,我们将一个标量值赋给 r,并将多个值赋给 pv,它是一个数组(向量)。type() 函数用于显示它们的类型:

>>> import numpy as np
>>> r=0.023
>>>pv=np.array([100,300,500])
>>>type(r)
<class'float'>
>>>type(pv)
<class'numpy.ndarray'>

为了选择合适的决策,我们使用 round() 函数;见以下示例:

>>> 7/3
2.3333333333333335
>>>round(7/3,5)
2.33333
>>>

对于数据操作,我们来看一些简单的操作:

>>>import numpy as np
>>>a=np.zeros(10)                      # array with 10 zeros 
>>>b=np.zeros((3,2),dtype=float)       # 3 by 2 with zeros 
>>>c=np.ones((4,3),float)              # 4 by 3 with all ones 
>>>d=np.array(range(10),float)         # 0,1, 2,3 .. up to 9 
>>>e1=np.identity(4)                   # identity 4 by 4 matrix 
>>>e2=np.eye(4)                        # same as above 
>>>e3=np.eye(4,k=1)                    # 1 start from k 
>>>f=np.arange(1,20,3,float)           # from 1 to 19 interval 3 
>>>g=np.array([[2,2,2],[3,3,3]])       # 2 by 3 
>>>h=np.zeros_like(g)                  # all zeros 
>>>i=np.ones_like(g)                   # all ones

一些所谓的 dot 函数非常方便和有用:

>>> import numpy as np
>>> x=np.array([10,20,30])
>>>x.sum()
60

任何在 # 符号后面的内容都是注释。数组是另一种重要的数据类型:

>>>import numpy as np
>>>x=np.array([[1,2],[5,6],[7,9]])      # a 3 by 2 array
>>>y=x.flatten()
>>>x2=np.reshape(y,[2,3]              ) # a 2 by 3 array

我们可以将字符串赋值给变量:

>>> t="This is great"
>>>t.upper()
'THIS IS GREAT'
>>>

为了找出所有与字符串相关的函数,我们使用 dir('');见以下代码:

>>>dir('')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>>

例如,从前面的列表中,我们看到一个名为 split 的函数。输入 help(''.split) 后,我们将得到相关的帮助信息:

>>>help(''.split)
Help on built-in function split:

split(...) method of builtins.str instance
S.split(sep=None, maxsplit=-1) -> list of strings

    Return a list of the words in S, using sep as the
delimiter string. If maxsplit is given, at most maxsplit
splits are done. If sep is not specified or is None, any
whitespace string is a separator and empty strings are
removed from the result.
>>>

我们可以尝试以下示例:

>>> x="this is great"
>>>x.split()
['this', 'is', 'great']
>>>

矩阵操作在我们处理各种矩阵时非常重要:

数据操作

方程(3)的条件是矩阵 AB 应该有相同的维度。对于两个矩阵的乘积,我们有以下方程:

数据操作

这里,A 是一个 nk 列的矩阵(n 行和 k 列),而 B 是一个 km 列的矩阵。记住,第一个矩阵的第二维度应该与第二个矩阵的第一维度相同。在这种情况下,它是 k。如果我们假设 CAB 中的各个数据项分别为 Ci,j(第 i 行和第 j 列)、Ai,jBi,j,我们可以得到它们之间的关系:

数据处理

可以使用 NumPy 模块中的 dot() 函数进行上述矩阵乘法:

>>>a=np.array([[1,2,3],[4,5,6]],float)    # 2 by 3
>>>b=np.array([[1,2],[3,3],[4,5]],float)  # 3 by 2
>>>np.dot(a,b)                            # 2 by 2
>>>print(np.dot(a,b))
array([[ 19.,  23.],
[ 43.,  53.]])
>>>

我们可以手动计算 c(1,1): 11 + 23 + 34=19*。

在获取数据或从互联网下载数据后,我们需要处理这些数据。这种处理各种类型原始数据的技能对于金融学学生和从事金融行业的专业人士来说至关重要。这里我们将展示如何下载价格数据并估算收益率。

假设我们有 nx1x2、… 和 xn 的值。存在两种类型的均值:算术均值和几何均值;请参阅它们的基因定义:

数据处理数据处理

假设存在三个值 234。它们的算术均值和几何均值在此计算:

>>>(2+3+4)/3.
>>>3.0
>>>geo_mean=(2*3*4)**(1./3)
>>>round(geo_mean,4) 
2.8845

对于收益率,算术平均数的定义保持不变,而几何平均数的定义则不同;请参阅以下公式:

数据处理数据处理

在第三章中,我们将再次讨论这两种均值。

我们可以说,NumPy 是一个基础模块,而 SciPy 是一个更高级的模块。NumPy 尝试保留其前辈所支持的所有特性,而大多数新特性属于 SciPy,而不是 NumPy。另一方面,NumPy 和 SciPy 在金融功能方面有许多重叠的特性。关于这两种定义,请参阅以下示例:

>>> import scipy as sp
>>> ret=sp.array([0.1,0.05,-0.02])
>>>sp.mean(ret)
0.043333333333333342
>>>pow(sp.prod(ret+1),1./len(ret))-1 
0.042163887067679262

我们的第二个示例与处理 Fama-French 三因子时间序列相关。由于此示例比前一个更复杂,如果用户觉得难以理解,可以简单跳过此示例。首先,可以从 French 教授的数据库下载名为 F-F_Research_Data_Factor_TXT.zip 的 ZIP 文件。解压后,去除前几行和年度数据集,我们将得到一个月度的 Fama-French 因子时间序列。这里展示了前几行和后几行:

DATE    MKT_RFSMBHMLRF
192607    2.96   -2.30   -2.87    0.22
192608    2.64   -1.40    4.19    0.25
192609    0.36   -1.32    0.01    0.23

201607    3.95    2.90   -0.98    0.02
201608    0.49    0.94    3.18    0.02
201609    0.25    2.00   -1.34    0.02

假设最终文件名为 ffMonthly.txt,位于 c:/temp/ 目录下。以下程序用于检索和处理数据:

import numpy as np
import pandas as pd
file=open("c:/temp/ffMonthly.txt","r")
data=file.readlines()
f=[]
index=[]
for i in range(1,np.size(data)):
    t=data[i].split()
    index.append(int(t[0]))
    for j in range(1,5):
        k=float(t[j])
        f.append(k/100)
n=len(f) 
f1=np.reshape(f,[n/4,4])
ff=pd.DataFrame(f1,index=index,columns=['Mkt_Rf','SMB','HML','Rf'])

要查看名为 ff 的数据集的前几行和后几行观察值,可以使用 .head().tail() 函数:

数据处理

数据输出

最简单的示例如下:

>>>f=open("c:/temp/out.txt","w")
>>>x="This is great"
>>>f.write(x)
>>>f.close()

对于下一个示例,我们首先下载历史股票价格数据,然后将数据写入输出文件:

import re
from matplotlib.finance import quotes_historical_yahoo_ochl
ticker='dell'
outfile=open("c:/temp/dell.txt","w")
begdate=(2013,1,1)
enddate=(2016,11,9)
p=quotes_historical_yahoo_ochl
(ticker,begdate,enddate,asobject=True,adjusted=True)
outfile.write(str(p))
outfile.close()

为了检索文件,我们有以下代码:

>>>infile=open("c:/temp/dell.txt","r")
>>>x=infile.read()

一个问题是,前面保存的文本文件包含了许多不必要的字符,比如 []。我们可以应用一个名为 sub() 的替换函数,它包含在 Python 模块中;请参见这里给出的最简单示例:

>>> import re
>>>re.sub("a","9","abc")
>>>
'9bc'
>>>

在前面的示例中,我们将字母 a 替换为 9。感兴趣的读者可以尝试以下两行代码来运行前面的程序:

p2= re.sub('[\(\)\{\}\.<>a-zA-Z]','', p)
outfile.write(p2)

使用扩展名 .pickle 生成 Python 数据集是一个好主意,因为我们可以高效地检索此类数据。以下是生成 ffMonthly.pickle 的完整 Python 代码。在这里,我们展示了如何下载价格数据并估算收益:

import numpy as np
import pandas as pd
file=open("c:/temp/ffMonthly.txt","r")
data=file.readlines()
f=[]
index=[]
for i in range(1,np.size(data)):
    t=data[i].split()
    index.append(int(t[0]))
    for j in range(1,5):
        k=float(t[j])
        f.append(k/100)
n=len(f)
f1=np.reshape(f,[n/4,4])
ff=pd.DataFrame(f1,index=index,columns=['Mkt_Rf','SMB','HML','Rf'])
ff.to_pickle("c:/temp/ffMonthly.pickle")

练习

  1. 你可以在哪里下载并安装 Python?

  2. Python 是否区分大小写?

  3. 如何将一组值以元组的形式赋值给 pv?在赋值之后,我们可以更改其值吗?

  4. 如果直径为 9.7,使用 Python 估算圆的面积。

  5. 如何为一个新变量赋值?

  6. 如何找到与 Python 相关的一些示例代码?

  7. 如何启动 Python 的帮助函数?

  8. 如何获得更多关于某个特定函数的信息,例如 print()

  9. 内置函数的定义是什么?

  10. pow() 是一个内置函数吗?我们该如何使用它?

  11. 如何找到所有内置函数?内置函数共有多少个?

  12. 当我们估算 3 的平方根时,应该使用哪个 Python 函数?

  13. 假设永久年金的现值为 124 美元,年现金流为 50 美元;那么相应的贴现率是多少?公式如下:Exercises

  14. 根据上一个问题的解答,什么是相应的季度利率?

  15. 对于永久年金,现金流在相同的时间间隔内永远发生。增长的永久年金定义如下:未来的现金流将永远以固定的增长率增长。如果第一个现金流发生在第一个期间结束时,我们有以下公式:Exercises

    在这里,PV 是现值,C 是下一个期间的现金流,g 是增长率,R 是贴现率。如果第一个现金流为 12.50 美元,常数增长率为 2.5%,贴现率为 8.5%,那么这个持续增长的永久年金的现值是多少?

  16. 对于 n 天的方差,我们有以下公式:Exercises

    这里 Exercises 是日波动率,! Exercises 是日标准差(波动率)。如果某只股票的波动率(每日标准差)为 0.2,那么它的 10 天波动率是多少?

  17. 我们预计在 5 年内将有 25,000 美元。如果年存款利率为 4.5%,我们今天需要存入多少金额?

  18. 这个名为 sub() 的替换函数来自一个 Python 模块。找出该模块包含了多少个函数。

  19. 编写一个 Python 程序,通过使用以下公式,将基于日数据或月数据估算的标准差转换为年标准差:ExercisesExercises

  20. 夏普比率是衡量投资(如投资组合)在收益(超额收益)与成本(总风险)之间权衡的一种指标。编写一个 Python 程序,通过以下公式估算夏普比率:Exercises

    这里,Exercises是投资组合的平均收益,Exercises是无风险利率的平均值,而σ是投资组合的风险。同样,在此时,读者不理解该比率的经济含义是完全可以接受的,因为夏普比率将在第七章,多因子模型与绩效衡量中进行更详细的讨论。

总结

在本章中,我们讨论了许多与 Python 相关的基本概念和几个广泛使用的函数。在第二章,Python 模块介绍中,我们将讨论 Python 语言的一个关键组成部分:Python 模块及其相关问题。模块是由专家、专业人士或任何围绕特定主题的人编写的一组程序。模块可以视为完成特定任务的工具箱。本章将重点介绍五个最重要的模块:NumPy、SciPy、matplotlibstatsmodelspandas

第二章:Python 模块简介

在本章中,我们将讨论与 Python 模块相关的最重要问题,这些模块是由专家或任何个人编写的,用于特定目的。在本书中,我们将使用大约十几个模块。因此,模块相关的知识对于我们理解 Python 及其在金融中的应用至关重要。特别地,在本章中,我们将涵盖以下主题:

  • Python 模块简介

  • NumPy 简介

  • SciPy 简介

  • matplotlib简介

  • statsmodels简介

  • pandas 简介

  • 与金融相关的 Python 模块

  • pandas_reader 模块简介

  • 用 Python 编写的两个金融计算器

  • 如何安装 Python 模块

  • 模块依赖关系

什么是 Python 模块?

模块是由专家、用户甚至是初学者编写的一组程序或软件包,通常这些人精通某个特定领域,且为了特定的目的而编写这些程序。

例如,一个名为 quant 的 Python 模块用于定量金融分析。quant 结合了 SciPy 和DomainModel两个模块。该模块包含一个领域模型,其中有交易所、符号、市场和历史价格等内容。模块在 Python 中非常重要。在本书中,我们将或多或少地讨论十几个模块。特别地,我们将详细讲解五个模块:NumPy、SciPy、matplotlibstatsmodels和 Pandas。

注意

截至 2016 年 11 月 16 日,Python 包索引中有 92,872 个不同领域的 Python 模块(包)。

对于金融和保险行业,目前有 384 个模块可供使用。

假设我们想使用sqrt()函数估算3的平方根。然而,在执行以下代码后,我们将遇到错误消息:

>>>sqrt(3)
SyntaxError: invalid syntax
>>>

原因在于sqrt()函数不是内置函数。内置函数可以视为在 Python 启动时就已存在的函数。要使用sqrt()函数,我们需要先导入 math 模块,如下所示:

>>>import math
>>>x=math.sqrt(3)
>>>round(x,4)
1.7321

要使用sqrt()函数,如果我们使用import math命令导入 math 模块,则必须输入math.sqrt()。在前面的代码中,round()函数用于控制小数位数。此外,在执行dir()命令后,我们将看到 math 模块的存在,它是此处输出中的最后一个:

>>>dir()
['__builtins__', '__doc__', '__name__', '__package__', 'math']

此外,当一个模块是预安装时,我们可以使用import x_module来上传它。例如,math 模块是预安装的。在本章后面,我们将看到如何查找所有内置模块。在前面的输出中,发出dir()命令后,我们还观察到__builtins__。在builtin前后都有两个下划线。这个__builtins__模块不同于其他内置模块,例如math模块。它包含所有内置函数和其他对象。再次发出dir(__builtins__)命令可以列出所有内置函数,如以下代码所示:

>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'debugfile', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'evalsc', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'open_in_spyder', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'runfile', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

从前面的输出中,我们发现一个名为pow()的函数。可以使用help(pow)命令来查找有关这个特定函数的更多信息;见下文:

>>> help(pow)
Help on built-in function pow in module builtins:
pow(x, y, z=None, /)
Equivalent to x**y (with two arguments) or x**y % z 
(with three arguments) 
Some types, such as ints, are able to use a more 
efficient algorithm when invoked using the three argument form.
>> > 

为了方便,建议为导入的模块采用简短名称。为了在编程时减少一些输入工作量,我们可以使用import x_module as short_name命令,如以下代码所示:

>>>import sys as s
>>>import time as tt
>>>import numpy as np
>>>import matplotlib as mp

在调用导入模块中包含的特定函数时,我们使用模块的简短名称,如以下代码所示:

>>> import time as tt
>>> tt.localtime()
time.struct_time(tm_year=2016, tm_mon=11, tm_mday=21, tm_hour=10, tm_min=58, tm_sec=33, tm_wday=0, tm_yday=326, tm_isdst=0)
>>>

虽然用户可以自由选择任何简短的名称来导入模块,但遵循一些约定是个不错的主意,例如使用np表示 NumPy,使用sp表示 SciPy。使用这些常用的简短名称的一个额外好处是能使我们的程序对他人更具可读性。要显示导入模块中的所有函数,可以使用dir(module)命令,如以下代码所示:

>>>import math
>>>dir(math)
['__doc__', '__loader__', '__name__', '__package__', 'acos', 'acosh',
'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos',
'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs',
'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'hypot',
'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc']
>>>

回想一下在第一章,Python 基础中,比较了import mathfrom math import *。一般来说,为了简化程序,可以使用from math import *。这对于刚开始学习 Python 编程的初学者尤其适用。让我们看一下以下代码:

>>>from math import *
>>>sqrt(3)
   1.7320508075688772

现在,模块中包含的所有函数将可以直接使用。另一方面,如果我们使用import math,我们必须将模块名作为前缀,例如math.sqrt()而不是sqrt()。熟悉 Python 后,建议使用导入模块的格式,而不是使用from module import *。这种偏好背后有两个原因:

  • 首先,用户可以准确知道函数来自哪个模块。

  • 其次,我们可能已经编写了一个与另一个模块中函数同名的函数。模块名加在函数前面可以将其与我们自己的函数区分开,如以下代码所示:

    >>>import math
    >>>math.sqrt(3)
        1.7320508075688772
    

del()函数用于移除一个被认为不再需要的导入/上传模块,如以下代码所示:

>>>import math
>>>dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', 'math']
>>>del math
>>>dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__']

另一方面,如果我们使用from math import *,我们不能通过del math来移除所有函数。必须单独移除这些函数。以下两个命令演示了这种效果:

>>>from math import *
>>>del math
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
del math NameError: name 'math' is not defined

为了方便,我们可以只导入几个需要的函数。为了定价一个欧洲看涨期权,需要几个函数,比如log()exp()sqrt()cdf()cdf()是累积标准正态分布函数。为了使这四个函数可用,我们指定它们的名称,如下代码所示:

From scipy import log,exp,sqrt,stats

这里给出了定价 Black-Scholes-Merton 看涨期权的完整代码:

def bsCall(S,X,T,r,sigma):
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    d2 = d1-sigma*sqrt(T)
    return S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2) 

这里给出了调用bsCall函数的一个示例:

>>> bsCall(40,40,0.1,0.05,0.2)
1.1094616585675574

要查找所有可用模块,首先应激活帮助窗口。之后,输入modules命令。结果如下所示:

>>> help()
>>> 
Welcome to Python 3.5's help utility!

如果这是你第一次使用 Python,应该一定访问互联网上的教程:docs.python.org/3.5/tutorial/

输入任何模块、关键字或主题的名称,即可获得关于编写 Python 程序和使用 Python 模块的帮助。要退出该帮助工具并返回解释器,只需输入quit

要获取可用模块、关键字、符号或主题的列表,输入moduleskeywordssymbolstopics。每个模块还附有一行总结其功能;要列出名称或总结包含给定字符串(如spam)的模块,输入modules spam

help>

然后,我们在 Python 的help>提示符下输入modules,如下截图所示(为节省空间,这里仅展示了前部分):

什么是 Python 模块?

要查找特定模块,我们只需输入modules后跟模块名称。假设我们对名为cmd的模块感兴趣,那么我们在帮助窗口中输入modules cmd;请参见以下截图:

什么是 Python 模块?

要获得更多有关模块的信息,请导航至所有程序 | Python 3.5 | Python 3.5 Module Docs,如下截图所示:

什么是 Python 模块?

点击Python 3.5 Module Docs (32-bit)后,我们将获得更多信息。

NumPy 简介

在以下示例中,NumPy 中的np.size()函数显示了数组的数据项数量,np.std()函数用于计算标准差:

>>>import numpy as np
>>>x= np.array([[1,2,3],[3,4,6]])     # 2 by 3 matrix
>>>np.size(x)                         # number of data items
6
>>>np.size(x,1)                       # show number of columns
3
>>>np.std(x)
1.5723301886761005
>>>np.std(x,1)
Array([ 0.81649658, 1.24721913]
>>>total=x.sum()                      # attention to the format
>>>z=np.random.rand(50)               #50 random obs from [0.0, 1)
>>>y=np.random.normal(size=100)       # from standard normal
>>>r=np.array(range(0,100),float)/100 # from 0, .01,to .99

与 Python 数组相比,NumPy 数组是一个连续的内存块,直接传递给 LAPACK,这是一个用于数值线性代数的底层软件库,因此在 Python 中矩阵操作非常快速。NumPy 中的数组就像 MATLAB 中的矩阵。与 Python 中的列表不同,数组应该包含相同的数据类型,如下代码所示:

>>>np.array([100,0.1,2],float)

实际数据类型是float64,数值的默认类型也是float64

在前面的示例中,我们可以看到np.array()函数将一个相同数据类型的列表(此例中为整数)转换为数组。要更改数据类型,应使用第二个输入值dtype进行指定,如下代码所示:

>>>x=[1,2,3,20]
>>>y=np.array(x1,dtype=float)
>>>y
array([ 1., 2., 3., 20.])

在前一个示例中,dtype是指定数据类型的关键字。对于列表,不同的数据类型可以共存而不会引发问题。然而,当将包含不同数据类型的列表转换为数组时,会出现错误信息,如以下代码所示:

>>>x2=[1,2,3,"good"]
>>>x2
[1, 2, 3, 'good']
>>>y3=np.array(x2,float)
Traceback (most recent call last):
File "<pyshell#25>", line 1, in <module>
y3=np.array(x2,float)
ValueError: could not convert string to float: 'good'
. ]])

为了显示 Numpy 中包含的所有函数,在导入 Numpy 模块后使用dir(np)

以下显示的是前几行代码:

>>> import numpy as np
>>> dir(np)
['ALLOW_THREADS', 'BUFSIZE', 'CLIP', 'ComplexWarning', 'DataSource', 'ERR_CALL', 'ERR_DEFAULT', 'ERR_IGNORE', 'ERR_LOG', 'ERR_PRINT', 'ERR_RAISE', 'ERR_WARN', 'FLOATING_POINT_SUPPORT', 'FPE_DIVIDEBYZERO', 'FPE_INVALID', 'FPE_OVERFLOW', 'FPE_UNDERFLOW', 'False_', 'Inf', 'Infinity', 'MAXDIMS', 'MAY_SHARE_BOUNDS', 'MAY_SHARE_EXACT', 'MachAr', 'ModuleDeprecationWarning', 'NAN', 'NINF', 'NZERO', 'NaN', 'PINF', 'PZERO', 'PackageLoader', 'RAISE', 'RankWarning', 'SHIFT_DIVIDEBYZERO', 'SHIFT_INVALID', 'SHIFT_OVERFLOW', 'SHIFT_UNDERFLOW', 'ScalarType', 'Tester', 'TooHardError', 'True_', 'UFUNC_BUFSIZE_DEFAULT', 'UFUNC_PYVALS_NAME', 'VisibleDeprecationWarning', 'WRAP', '_NoValue', '__NUMPY_SETUP__', '__all__', '__builtins__', '__cached__', '__config__', '__doc__', '__file__', '__git_revision__', '__loader__', '__mkl_version__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_import_tools', '_mat', 'abs', 'absolute', 'absolute_import', 'add', 'add_docstring', 'add_newdoc', 'add_newdoc_ufunc', 'add_newdocs', 'alen', 'all', 'allclose', 'alltrue', 'alterdot', 'amax', 'amin', 'angle', 'any', 'append', 'apply_along_axis', 'apply_over_axes', 'arange', 'arccos', 'arccosh', 'arcsin', 'arcsinh', 'arctan', 'arctan2', 'arctanh', 'argmax', 'argmin', 'argpartition', 'argsort', 'argwhere', 'around', 'array', 'array2string', 'array_equal', 'array_equiv', 'array_repr', 'array_split', 'array_str', 'asanyarray',

实际上,更好的方法是生成一个包含所有函数的数组,如下所示:

>>> x=np.array(dir(np))
>>> len(x)
598

要显示200250的函数,可以输入x[200:250];请参见以下代码:

>>> x[200:250]
array(['disp', 'divide', 'division', 'dot', 'double', 'dsplit', 'dstack',
       'dtype', 'e', 'ediff1d', 'einsum', 'emath', 'empty', 'empty_like',
       'equal', 'errstate', 'euler_gamma', 'exp', 'exp2', 'expand_dims',
       'expm1', 'extract', 'eye', 'fabs', 'fastCopyAndTranspose', 'fft',
       'fill_diagonal', 'find_common_type', 'finfo', 'fix', 'flatiter',
       'flatnonzero', 'flexible', 'fliplr', 'flipud', 'float', 'float16',
       'float32', 'float64', 'float_', 'floating', 'floor', 'floor_divide',
       'fmax', 'fmin', 'fmod', 'format_parser', 'frexp', 'frombuffer',
       'fromfile'], 
      dtype='<U25')
>> > 

查找特定函数的更多信息非常简单。执行dir(np)后,std()函数等将会出现。要获取该函数的更多信息,可以使用help(np.std)。为了简洁起见,以下仅显示部分代码:

>>>import numpy as np
>>>help(np.std)
Help on function std in module numpy.core.fromnumeric:

std(a, axis=None, dtype=None, out=None, ddof=0, keepdims=False)
    Compute the standard deviation along the specified axis.

该函数返回标准差,它是衡量分布离散程度的指标,表示数组元素的标准差。默认情况下,标准差是针对展平后的数组计算的,或者根据指定的轴进行计算:


    Parameters
    ----------
   a : array_like
      Calculate the standard deviation of these values.
   axis : None or int or tuple of ints, optional
Axis or axes along which the standard deviation is computed. The
default is to compute the standard deviation of the flattened array.

        .. versionadded: 1.7.0

SciPy 简介

以下是几个基于 SciPy 模块中函数的示例。sp.npv()函数估算给定现金流的现值,第一个现金流发生在时间零。第一个输入值是贴现率,第二个输入值是所有现金流的数组。

以下是一个示例。请注意,sp.npv()函数与 Excel 中的npv()函数不同。我们将在第三章中详细解释原因,货币的时间价值

>>>import scipy as sp
>>>cashflows=[-100,50,40,20,10,50]
>>>x=sp.npv(0.1,cashflows)
>>>round(x,2)
>>>31.41

sp.pmt()函数用于解答以下问题。

每月现金流是多少,用于偿还一笔$250,000 的抵押贷款,贷款期为 30 年,年利率(APR)为 4.5%,按月复利?以下代码显示了答案:

>>>payment=sp.pmt(0.045/12,30*12,250000)
>>>round(payment,2)
-1266.71

基于前面的结果,每月支付金额为$1,266.71。出现负值可能让人感到奇怪。实际上,sp.pmt()函数模拟了 Excel 中的等效函数,正如我们在以下截图中看到的那样:

SciPy 简介

输入值包括:有效期利率、期数和现值。顺便提一下,括号中的数字表示负数。

此时,暂时忽略负号。在第三章,货币的时间价值中,将更详细讨论这一所谓的 Excel 约定。

类似地,sp.pv() 函数复制了 Excel 的 PV() 函数。对于 sp.pv() 函数,其输入格式为 sp.pv(rate, nper, pmt, fv=0.0, when='end'),其中 rate 是折现率,nper 是期数,pmt 是期支付额,fv 是未来值,默认为零。最后一个输入变量指定现金流是在每个时间段的末尾还是开始时进行。默认为每期末尾。以下命令演示了如何调用此函数:

>>>pv1=sp.pv(0.1,5,0,100) # pv of one future cash flow
>>>round(pv1,2)
-92.09
>>>pv2=sp.pv(0.1,5,100)   # pv of annuity
>>>round(pv2,2)
-379.08

sp.fv() 函数的设置类似于 sp.pv()。在金融领域,我们估算算术平均值和几何平均值,定义在以下公式中。

对于 nx 数字,即 x1x2x3xn,我们有以下公式:

SciPy 介绍SciPy 介绍

这里,SciPy 介绍SciPy 介绍。假设我们有三个数字 abc,那么它们的算术平均值是 (a+b+c)/3,而它们的几何平均值是 (abc)^(1/3)。对于 234 三个值,我们得到以下两种均值:

>>>(2+3+4)/3.
>>>3.0
>>>geo_mean=(2*3*4)**(1./3)
>>>round(geo_mean,4)
2.8845

如果给定 n 个回报,估算其算术平均值的公式保持不变。然而,回报的几何平均值公式不同,如下所示:

SciPy 介绍SciPy 介绍

要估算几何平均值,可以使用 sp.prod() 函数。该函数为我们提供所有数据项的乘积;请参见以下代码:

>>>import scipy as sp
>>>ret=sp.array([0.1,0.05,-0.02])
>>>sp.mean(ret)                      # arithmetic mean
0.04333
>>>pow(sp.prod(ret+1),1./len(ret))-1 # geometric mean
0.04216

实际上,可以通过仅写两行简单的 Python 函数来计算一组给定回报的几何平均值;请参见以下代码:

def geoMeanReturn(ret):
    return pow(sp.prod(ret+1),1./len(ret))-1

调用前面的函数非常简单;请参见以下代码:

>>> import scipy as sp
>>> ret=sp.array([0.1,0.05,-0.02])
>>> geoMeanReturn(ret)
0.042163887067679262

另外两个有用的函数是 sp.unique()sp.median(),如以下代码所示:

>>>sp.unique([2,3,4,6,6,4,4])
Array([2,3,4,6])
>>>sp.median([1,2,3,4,5])
3.0

Python 的 sp.pv()sp.fv()sp.pmt() 函数分别与 Excel 的 pv()fv()pmt() 函数行为相似。它们具有相同的符号约定:现值的符号与未来值相反。

在以下示例中,假设我们输入一个正的未来值来估算现值,最终会得到一个负的现值:

>>>import scipy as sp
>>>round(sp.pv(0.1,5,0,100),2)
>>>-62.09
>>>round(sp.pv(0.1,5,0,-100),2)
>>>62.09

有多种方法可以查找 SciPy 模块中包含的所有函数。

首先,我们可以阅读相关手册。其次,我们可以发出以下代码行:

>>>import numpy as np
>>>dir(np)

为了节省空间,以下代码仅显示部分输出:

>>> import scipy as sp
>>> dir(sp)
'ALLOW_THREADS', 'BUFSIZE', 'CLIP', 'ComplexWarning', 'DataSource', 'ERR_CALL', 'ERR_DEFAULT', 'ERR_IGNORE', 'ERR_LOG', 'ERR_PRINT', 'ERR_RAISE', 'ERR_WARN', 'FLOATING_POINT_SUPPORT', 'FPE_DIVIDEBYZERO', 'FPE_INVALID', 'FPE_OVERFLOW', 'FPE_UNDERFLOW', 'False_', 'Inf', 'Infinity', 'MAXDIMS', 'MAY_SHARE_BOUNDS', 'MAY_SHARE_EXACT', 'MachAr', 'ModuleDeprecationWarning', 'NAN', 'NINF', 'NZERO', 'NaN', 'PINF', 'PZERO', 'PackageLoader', 'RAISE', 'RankWarning', 'SHIFT_DIVIDEBYZERO', 'SHIFT_INVALID', 'SHIFT_OVERFLOW', 'SHIFT_UNDERFLOW', 'ScalarType', 'Tester', 'TooHardError', 'True_', 'UFUNC_BUFSIZE_DEFAULT', 'UFUNC_PYVALS_NAME', 'VisibleDeprecationWarning', 'WRAP', '__SCIPY_SETUP__', '__all__', '__builtins__', '__cached__', '__config__', '__doc__', '__file__', '__loader__', '__name__', '__numpy_version__', '__package__', '__path__', '__spec__', '__version__', '_lib', 'absolute', 'absolute_import', 'add', 'add_docstring', 'add_newdoc', 'add_newdoc_ufunc', 'add_newdocs', 'alen', 'all', 'allclose', 'alltrue', 'alterdot', 'amax', 'amin', 'angle', 'any', 'append', 'apply_along_axis', 'apply_over_axes', 'arange', 'arccos', 'arccosh', 'arcsin', 'arcsinh', 'arctan', 'arctan2', 'arctanh', 'argmax', 'argmin', 'argpartition', 'argsort', 'argwhere', 'around', 'array', 'array2string', 'array_equal', 'array_equiv', 'array_repr', 'array_split', 'array_str', 'asanyarray', 'asarray', 'asarray_chkfinite', 'ascontiguousarray', 'asfarray', 'asfortranarray', 'asmatrix', 'asscalar', 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'bartlett',

类似地,我们可以将所有函数保存到一个向量(数组)中;请参见以下代码:

>>>import scipy as sp
>>> x=dir(sp)
>>> len(x)
588
>>>

matplotlib 介绍

图表和其他可视化表示在解释许多复杂的金融概念、交易策略和公式中变得更加重要。

在这一部分,我们讨论了 matplotlib 模块,它用于创建各种类型的图形。此外,模块将在 [第十章,期权与期货 中得到广泛应用,当时我们将讨论著名的 Black-Scholes-Merton 期权模型以及各种交易策略。matplotlib 模块旨在生成出版质量的图形和图表。matplotlib 模块依赖于 NumPy 和 SciPy,这些在前面的章节中已经讨论过。为了保存生成的图形,有多种输出格式可供选择,如 PDF、Postscript、SVG 和 PNG。

如何安装 matplotlib

如果 Python 是通过 Anaconda 超级包安装的,那么 matplotlib 已经预先安装好了。启动 Spyder 后,输入以下命令进行测试。如果没有错误,说明我们已经成功导入/上传了该模块。这就是使用像 Anaconda 这样超级包的好处:

>>> import matplotlib

若要单独安装 matplotlib 模块或其他模块,请参见 模块依赖 - 如何安装模块 部分。

使用 matplotlib 的几种图形展示

理解 matplotlib 模块的最佳方式是通过示例。以下示例可能是最简单的,因为它仅包含三行 Python 代码。目标是连接几个点。默认情况下,matplotlib 模块假设 x 轴从零开始,并且数组的每个元素增加 1。

以下命令行截图说明了这一情况:

使用 matplotlib 的几种图形展示

在输入最后一个命令 show() 并按下 Enter 键后,上图右侧的图形将出现。在图形顶部,有一组图标(功能)可供选择。点击它们,我们可以调整图像或保存图像。关闭上述图形后,我们可以返回到 Python 提示符。另一方面,如果我们第二次输入 show(),则什么也不会发生。要重新显示上面的图形,我们必须同时输入 plot([1,2,3,9])show()。可以为 x 轴和 y 轴添加两个标签,如下所示。

相应的图形显示在右侧的以下截图中:

使用 matplotlib 的几种图形展示

下一个例子展示了两个余弦函数:

使用 matplotlib 的几种图形展示

在上述代码中,linspace() 函数有四个输入值:startstopnumendpoint。在前面的示例中,我们将从 -3.1415916 开始,到 3.1415926 结束,中间有 256 个值。此外,端点将被包括在内。顺便提一下,num 的默认值是 50。以下示例显示了散点图。首先,使用 np.random.normal() 函数生成两组随机数。由于 n1024,所以 XY 变量都有 1,024 个观测值。关键函数是 scatter(X,Y),如下所示:

使用 matplotlib 的多种图形展示

这是一个更复杂的图,展示了股票的波动。我们先看看代码:

import datetime
import matplotlib.pyplot as plt
from matplotlib.finance import quotes_historical_yahoo_ochl
from matplotlib.dates import MonthLocator,DateFormatter
ticker='AAPL'
begdate= datetime.date( 2012, 1, 2 )
enddate = datetime.date( 2013, 12,5)
months = MonthLocator(range(1,13), bymonthday=1, interval=3) # every 3rd month
monthsFmt = DateFormatter("%b '%Y")
x = quotes_historical_yahoo_ochl(ticker, begdate, enddate)
if len(x) == 0:
    print ('Found no quotes')
    raise SystemExit
dates = [q[0] for q in x]
closes = [q[4] for q in x]
fig, ax = plt.subplots()
ax.plot_date(dates, closes, '-')
ax.xaxis.set_major_locator(months)
ax.xaxis.set_major_formatter(monthsFmt)
ax.xaxis.set_minor_locator(mondays)
ax.autoscale_view()
ax.grid(True)
fig.autofmt_xdate()

相应的图表如下所示:

使用 matplotlib 的多种图形展示

介绍 statsmodels

statsmodels 是一个强大的 Python 包,适用于多种类型的统计分析。同样,如果通过 Anaconda 安装了 Python,那么该模块也会随之安装。在统计学中,普通最小二乘法 (OLS) 回归是一种估计线性回归模型中未知参数的方法。它通过最小化观测值与线性近似预测值之间的垂直距离的平方和来进行优化。OLS 方法在金融领域被广泛使用。假设我们有如下方程,其中 y 是一个 n1 列的向量(数组),x 是一个 n(m+1) 列的矩阵,表示回报矩阵(nm 列),加上一个仅包含 1 的向量。n 是观测值的数量,m 是独立变量的数量:

statsmodels 介绍

在以下程序中,生成 xy 向量后,我们运行一个 OLS 回归(线性回归)。xy 是人工数据。最后一行只打印参数(截距为 1.28571420,斜率为 0.35714286):

>>> import numpy as np
>>> import statsmodels.api as sm
>>> y=[1,2,3,4,2,3,4]
>>> x=range(1,8)
>>> x=sm.add_constant(x)
>>> results=sm.OLS(y,x).fit()
>>> print(results.params)
     [ 1.28571429  0.35714286]

若要了解有关此模块的更多信息,可以使用 dir() 函数:

>>> import statsmodels as sm
>>> dir(sm)
['CacheWriteWarning', 'ConvergenceWarning', 'InvalidTestWarning', 'IterationLimitWarning', 'NoseWrapper', 'Tester', '__builtins__', '__cached__', '__doc__', '__docformat__', '__file__', '__init__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', 'api', 'base', 'compat', 'datasets', 'discrete', 'distributions', 'duration', 'emplike', 'errstate', 'formula', 'genmod', 'graphics', 'info', 'iolib', 'nonparametric', 'print_function', 'regression', 'robust', 'sandbox', 'simplefilter', 'stats', 'test', 'tools', 'tsa', 'version']

对于各种子模块,也可以使用 dir();请参见这里的示例:

>>> import statsmodels.api as api
>>> dir(api)               
['Categorical', 'CategoricalIndex', 'DataFrame', 'DateOffset', 'DatetimeIndex', 'ExcelFile', 'ExcelWriter', 'Expr', 'Float64Index', 'Grouper', 'HDFStore', 'Index', 'IndexSlice', 'Int64Index', 'MultiIndex', 'NaT', 'Panel', 'Panel4D', 'Period', 'PeriodIndex', 'RangeIndex', 'Series', 'SparseArray', 'SparseDataFrame', 'SparseList', 'SparsePanel', 'SparseSeries', 'SparseTimeSeries', 'Term', 'TimeGrouper', 'TimeSeries', 'Timedelta', 'TimedeltaIndex', 'Timestamp', 'WidePanel', '__builtins__', '__cached__', '__doc__', '__docformat__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_np_version_under1p10', '_np_version_under1p11', '_np_version_under1p12', '_np_version_under1p8', '_np_version_under1p9', '_period', '_sparse', '_testing', '_version', 'algos', 'bdate_range', 'compat', 'computation', 'concat', 'core', 'crosstab', 'cut', 'date_range', 'datetime', 'datetools', 'dependency', 'describe_option', 'eval', 'ewma', 'ewmcorr', 'ewmcov', 'ewmstd', 'ewmvar', 'ewmvol', 'expanding_apply', 'expanding_corr', 'expanding_count', 'expanding_cov', 'expanding_kurt', 'expanding_max', 'expanding_mean', 'expanding_median', 'expanding_min', 'expanding_quantile', 'expanding_skew', 'expanding_std', 'expanding_sum', 'expanding_var', 'factorize', 'fama_macbeth', 'formats', 'get_dummies', 'get_option', 'get_store', 'groupby', 'hard_dependencies', 'hashtable', 'index', 'indexes', 'infer_freq', 'info', 'io', 'isnull', 'json', 'lib', 'lreshape', 'match', 'melt', 'merge', 'missing_dependencies', 'msgpack', 'notnull', 'np', 'offsets', 'ols', 'option_context', 'options', 'ordered_merge', 'pandas', 'parser', 'period_range', 'pivot', 'pivot_table', 'plot_params', 'pnow', 'qcut', 'read_clipboard', 'read_csv', 'read_excel', 'read_fwf', 'read_gbq', 'read_hdf', 'read_html', 'read_json', 'read_msgpack', 'read_pickle', 'read_sas', 'read_sql', 'read_sql_query', 'read_sql_table', 'read_stata', 'read_table', 'reset_option', 'rolling_apply', 'rolling_corr', 'rolling_count', 'rolling_cov', 'rolling_kurt', 'rolling_max', 'rolling_mean', 'rolling_median', 'rolling_min', 'rolling_quantile', 'rolling_skew', 'rolling_std', 'rolling_sum', 'rolling_var', 'rolling_window', 'scatter_matrix', 'set_eng_float_format', 'set_option', 'show_versions', 'sparse', 'stats', 'test', 'timedelta_range', 'to_datetime', 'to_msgpack', 'to_numeric', 'to_pickle', 'to_timedelta', 'tools', 'tseries', 'tslib', 'types', 'unique', 'util', 'value_counts', 'wide_to_long']

从前面的输出可以看出,有 16 个函数以 read 开头;请参见下表:

名称 描述
read_clipboard 从剪贴板输入数据
read_csv 从 CSV(逗号分隔值)输入数据
read_excel 从 Excel 文件输入数据
read_fwf 输入定宽数据
read_gbq 从 Google BigQuery 加载数据
read_hdf 读取 HDF5 格式的数据
read_html 从网页输入数据
read_json 读取 JSON(JavaScript 对象表示法)数据
read_msgpack MessagePack 是一种快速、紧凑的二进制序列化格式,适用于类似 JSON 的数据
read_pickle 输入一个 Python 数据集,称为 pickle
read_sas 从 SAS 数据集输入数据
read_sql 从 SQL 数据库输入数据
read_sql_query 从查询中输入数据
read_sql_table 将 SQL 数据库表读入 DataFrame
read_stata 从 Stata 数据集输入数据
read_table 从文本文件输入数据

表 2.1 输入数据所用的函数列表

pandas 简介

pandas模块是一个强大的工具,用于处理各种类型的数据,包括经济、金融和会计数据。如果你通过 Anaconda 在你的机器上安装了 Python,那么pandas模块已经安装好了。如果你执行以下命令且没有错误提示,则说明pandas模块已经安装:

>>>import pandas as pd

在以下示例中,我们生成了两个从 2013 年 1 月 1 日开始的时间序列。这两列时间序列的名称分别是AB

import numpy as np
import pandas as pd
dates=pd.date_range('20160101',periods=5)
np.random.seed(12345)
x=pd.DataFrame(np.random.rand(5,2),index=dates,columns=('A','B'))

首先,我们导入了 NumPy 和pandas模块。pd.date_range()函数用于生成索引数组。x变量是一个以日期为索引的 pandas DataFrame。稍后我们将在本章中讨论pd.DataFrame()函数。columns()函数定义了列的名称。由于程序中使用了seed()函数,任何人都可以生成相同的随机值。describe()函数提供了这两列的属性,例如均值和标准差。再次调用这样的函数,如下所示:

>>> x
                   A         B
2016-01-01  0.929616  0.316376
2016-01-02  0.183919  0.204560
2016-01-03  0.567725  0.595545
2016-01-04  0.964515  0.653177
2016-01-05  0.748907  0.653570
>>>
>>> x.describe()
              A         B
count  5.000000  5.000000
mean   0.678936  0.484646
std    0.318866  0.209761
min    0.183919  0.204560
25%    0.567725  0.316376
50%    0.748907  0.595545
75%    0.929616  0.653177
max    0.964515  0.653570
>>>

为了显示pandas模块中包含的所有函数,在导入该模块后,使用dir(pd)命令;请参见以下代码及相应的输出:

>>> import pandas as pd
>>> dir(pd)
['Categorical', 'CategoricalIndex', 'DataFrame', 'DateOffset', 'DatetimeIndex', 'ExcelFile', 'ExcelWriter', 'Expr', 'Float64Index', 'Grouper', 'HDFStore', 'Index', 'IndexSlice', 'Int64Index', 'MultiIndex', 'NaT', 'Panel', 'Panel4D', 'Period', 'PeriodIndex', 'RangeIndex', 'Series', 'SparseArray', 'SparseDataFrame', 'SparseList', 'SparsePanel', 'SparseSeries', 'SparseTimeSeries', 'Term', 'TimeGrouper', 'TimeSeries', 'Timedelta', 'TimedeltaIndex', 'Timestamp', 'WidePanel', '__builtins__', '__cached__', '__doc__', '__docformat__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_np_version_under1p10', '_np_version_under1p11', '_np_version_under1p12', '_np_version_under1p8', '_np_version_under1p9', '_period', '_sparse', '_testing', '_version', 'algos', 'bdate_range', 'compat', 'computation', 'concat', 'core', 'crosstab', 'cut', 'date_range', 'datetime', 'datetools', 'dependency', 'describe_option', 'eval', 'ewma', 'ewmcorr', 'ewmcov', 'ewmstd', 'ewmvar', 'ewmvol', 'expanding_apply', 'expanding_corr', 'expanding_count', 'expanding_cov', 'expanding_kurt', 'expanding_max', 'expanding_mean', 'expanding_median', 'expanding_min', 'expanding_quantile', 'expanding_skew', 'expanding_std', 'expanding_sum', 'expanding_var', 'factorize', 'fama_macbeth', 'formats', 'get_dummies', 'get_option', 'get_store', 'groupby', 'hard_dependencies', 'hashtable', 'index', 'indexes', 'infer_freq', 'info', 'io', 'isnull', 'json', 'lib', 'lreshape', 'match', 'melt', 'merge', 'missing_dependencies', 'msgpack', 'notnull', 'np', 'offsets', 'ols', 'option_context', 'options', 'ordered_merge', 'pandas', 'parser', 'period_range', 'pivot', 'pivot_table', 'plot_params', 'pnow', 'qcut', 'read_clipboard', 'read_csv', 'read_excel', 'read_fwf', 'read_gbq', 'read_hdf', 'read_html', 'read_json', 'read_msgpack', 'read_pickle', 'read_sas', 'read_sql', 'read_sql_query', 'read_sql_table', 'read_stata', 'read_table', 'reset_option', 'rolling_apply', 'rolling_corr', 'rolling_count', 'rolling_cov', 'rolling_kurt', 'rolling_max', 'rolling_mean', 'rolling_median', 'rolling_min', 'rolling_quantile', 'rolling_skew', 'rolling_std', 'rolling_sum', 'rolling_var', 'rolling_window', 'scatter_matrix', 'set_eng_float_format', 'set_option', 'show_versions', 'sparse', 'stats', 'test', 'timedelta_range', 'to_datetime', 'to_msgpack', 'to_numeric', 'to_pickle', 'to_timedelta', 'tools', 'tseries', 'tslib', 'types', 'unique', 'util', 'value_counts', 'wide_to_long']

仔细查看前面的列表,我们会看到与statsmodels模块中包含的相同函数,它们以read_开头,如表 2.1 所示。这种重复使我们的程序工作变得稍微简单一点。假设我们计划用时间序列的均值替换缺失值(NaN)。这时使用的两个函数是mean()fillna()

>>> import pandas as pd
>>> import numpy as np
>>> x=pd.Series([1,4,-3,np.nan,5])
>>> x
0    1.0
1    4.0
2   -3.0
3    NaN
4    5.0
dtype: float64
>>> m=np.mean(x)
>>> m
1.75
>>> x.fillna(m)
0    1.00
1    4.00
2   -3.00
3    1.75
4    5.00
dtype: float64>> >

从右侧的输出中可以看到,第四个观察值NaN被替换为均值 1.75。在以下代码中,我们通过使用pandas模块中包含的dataFrame()函数生成了一个 DataFrame:

import pandas as pd
import numpy as np
np.random.seed(123)
df = pd.DataFrame(np.random.randn(10, 4))

由于程序中使用了numpy.random.seed()函数,不同的用户将得到相同的随机数:

>>> df
>>> 
          0         1         2         3
0 -1.085631  0.997345  0.282978 -1.506295
1 -0.578600  1.651437 -2.426679 -0.428913
2  1.265936 -0.866740 -0.678886 -0.094709
3  1.491390 -0.638902 -0.443982 -0.434351
4  2.205930  2.186786  1.004054  0.386186
5  0.737369  1.490732 -0.935834  1.175829
6 -1.253881 -0.637752  0.907105 -1.428681
7 -0.140069 -0.861755 -0.255619 -2.798589
8 -1.771533 -0.699877  0.927462 -0.173636
9  0.002846  0.688223 -0.879536  0.283627
>>>

目前,读者可能会感到困惑,为什么在尝试获取一组随机数时,我们会得到相同的随机值。这个问题将在第十二章,蒙特卡洛模拟中进行更详细的讨论和解释。在以下代码中,展示了如何使用不同的方法进行插值:

import pandas as pd
import numpy as np
np.random.seed(123)                   # fix the random numbers 
x=np.arange(1, 10.1, .25)**2      
n=np.size(x)
y = pd.Series(x + np.random.randn(n))
bad=np.array([4,13,14,15,16,20,30])   # generate a few missing values
x[bad] = np.nan                       # missing code is np.nan
methods = ['linear', 'quadratic', 'cubic']
df = pd.DataFrame({m: x.interpolate(method=m) for m in methods})
df.plot()

相应的图表如下截图所示:

pandas 简介

通常,不同的编程语言有各自的类型数据集。

例如,SAS 有自己的数据集,其扩展名为.sas7bdat

对于 R,其扩展名可能是.RData.rda.rds。Python 也有自己的数据集格式。一种数据集类型的扩展名是.pickle.pkl。让我们生成一个 pickle 数据集;请查看以下代码:

import numpy as np
import pandas as pd
np.random.seed(123)
df=pd.Series(np.random.randn(100))
df.to_pickle('test.pkl')

最后一条命令将变量保存为一个名为test.pkl的 pickle 数据集,保存在当前工作目录下。要将 pickle 数据集保存到特定地址的文件中,即绝对路径,我们有以下代码:

df.to_pickle('test.pkl')

要读取 pickle 数据集,使用pd.read_pickle()函数:

>>>import pandas as pd
>>>x=pd.read_pickle("c:/temp/test.pkl")
>>>x[:5]
>>> 
>>> 
0   -1.085631
1    0.997345
2    0.282978
3   -1.506295
4   -0.578600
dtype: float64
>>>

合并两个不同的数据集是研究人员常做的常见操作。以下程序的目的是根据它们的公共变量key合并两个数据集:

import numpy as np
import pandas as pd
x = pd.DataFrame({'key':['A','B','C','D'],'value': [0.1,0.2,-0.5,0.9]})
y = pd.DataFrame({'key':['B','D','D','E'],'value': [2, 3, 4, 6]})
z=pd.merge(x, y, on='key')

以下代码展示了xy的初始值,以及合并后的数据集z

>>> x
  key  value
0   A    0.1
1   B    0.2
2   C   -0.5
3   D    0.9
>>> y
  key  value
0   B      2
1   D      3
2   D      4
3   E      6numpy as np
>>>z
  key  value_x  value_y
0   B      0.2        2
1   D      0.9        3
2   D      0.9        4
>>>

对于金融领域,时间序列占据了独特的地位,因为许多数据集是以时间序列的形式存在的,例如股价和回报。因此,了解如何定义date变量并研究相关函数,对于处理经济、金融和会计数据至关重要。我们来看一些例子:

>>> date1=pd.datetime(2010,2,3)
>>> date1
datetime.datetime(2010, 2, 3, 0, 0)

两个日期之间的差异可以轻松估算;请查看以下代码:

>>>date1=pd.datetime(2010,2,3)
>>>date2=pd.datetime(2010,3,31)
>>> date2-date1
datetime.timedelta(56)

来自pandas模块的一个子模块datetools非常有用;请查看其中包含的函数列表:

>>> dir(pd.datetools)
>>> 
['ABCDataFrame', 'ABCIndexClass', 'ABCSeries', 'AmbiguousTimeError', 'BDay', 'BMonthBegin', 'BMonthEnd', 'BQuarterBegin', 'BQuarterEnd', 'BYearBegin', 'BYearEnd', 'BusinessDay', 'BusinessHour', 'CBMonthBegin', 'CBMonthEnd', 'CDay', 'CustomBusinessDay', 'CustomBusinessHour', 'DAYS', 'D_RESO', 'DateOffset', 'DateParseError', 'Day', 'Easter', 'FY5253', 'FY5253Quarter', 'FreqGroup', 'H_RESO', 'Hour', 'LastWeekOfMonth', 'MONTHS', 'MS_RESO', 'Micro', 'Milli', 'Minute', 'MonthBegin', 'MonthEnd', 'MutableMapping', 'Nano', 'OLE_TIME_ZERO', 'QuarterBegin', 'QuarterEnd', 'Resolution', 'S_RESO', 'Second', 'T_RESO', 'Timedelta', 'US_RESO', 'Week', 'WeekOfMonth', 'YearBegin', 'YearEnd', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'algos', 'bday', 'bmonthBegin', 'bmonthEnd', 'bquarterEnd', 'businessDay', 'byearEnd', 'cache_readonly', 'cbmonthBegin', 'cbmonthEnd', 'cday', 'com', 'compat', 'customBusinessDay', 'customBusinessMonthBegin', 'customBusinessMonthEnd', 'datetime', 'day', 'deprecate_kwarg', 'format', 'getOffset', 'get_base_alias', 'get_freq', 'get_freq_code', 'get_freq_group', 'get_legacy_offset_name', 'get_offset', 'get_offset_name', 'get_period_alias', 'get_standard_freq', 'get_to_timestamp_base', 'infer_freq', 'isBMonthEnd', 'isBusinessDay', 'isMonthEnd', 'is_subperiod', 'is_superperiod', 'lib', 'long', 'monthEnd', 'need_suffix', 'normalize_date', 'np', 'offsets', 'ole2datetime', 'opattern', 'parse_time_string', 'prefix_mapping', 'quarterEnd', 'range', 're', 'thisBMonthEnd', 'thisBQuarterEnd', 'thisMonthEnd', 'thisQuarterEnd', 'thisYearBegin', 'thisYearEnd', 'time', 'timedelta', 'to_datetime', 'to_offset', 'to_time', 'tslib', 'unique', 'warnings', 'week', 'yearBegin', 'yearEnd', 'zip']
>>>

这是一个使用pandas模块中weekday()函数的例子。该函数在进行所谓的周效应测试时非常重要。该测试将在第四章 数据来源中详细解释。让我们来看一下以下代码:

>>import pandas as pd
>>>date1=pd.datetime(2010,10,10)
>>>date1.weekday()
6

在某些情况下,用户可能希望将数据堆叠在一起或反过来;请查看以下代码:

import pandas as pd
import numpy as np
np.random.seed(1256)
df=pd.DataFrame(np.random.randn(4,2),columns=['Stock A','Stock B'])
df2=df.stack()

原始数据集与堆叠数据集的比较如下。左侧是原始数据集:

>>> df
    Stock A   Stock B
0  0.452820 -0.892822
1 -0.476880  0.393239
2  0.961438 -1.797336
3 -1.168289  0.187016
>>>
>>> df2
>>> 
0  Stock A    0.452820
   Stock B   -0.892822
1  Stock A   -0.476880
   Stock B    0.393239
2  Stock A    0.961438
   Stock B   -1.797336
3  Stock A   -1.168289
   Stock B    0.187016
dtype: float64>> >

股票的反操作是应用unstack()函数;请查看以下代码:

>>> k=df2.unstack()
>>> k
    Stock A   Stock B
0  0.452820 -0.892822
1 -0.476880  0.393239
2  0.961438 -1.797336
3 -1.168289  0.187016

如果输入数据集按股票 ID 和日期排序,即视为按顺序堆叠每只股票,那么此操作可以用于生成回报矩阵。

与金融相关的 Python 模块

由于本书是将 Python 应用于金融领域,因此与金融相关的模块(包)将是我们的首要任务。

下表列出了约十个与金融相关的 Python 模块或子模块:

名称 描述
Numpy.lib.financial 提供许多公司财务和财务管理相关的函数。
pandas_datareader 从 Google、Yahoo! Finance、FRED、Fama-French 因子获取数据。
googlefinance Python 模块,用于通过 Google Finance API 获取实时(无延迟)股票数据。
yahoo-finance Python 模块,用于从 Yahoo! Finance 获取股票数据。
Python_finance 下载并分析 Yahoo! Finance 数据,并开发交易策略。
tstockquote 从 Yahoo! Finance 获取股票报价数据。
finance 财务风险计算。通过类构造和运算符重载优化,便于使用。
quant 用于财务定量分析的企业架构。
tradingmachine 一个用于金融算法回测的工具。
economics 经济数据的函数和数据处理。有关更好理解,请访问以下链接:github.com/tryggvib/economics
FinDates 处理财务中的日期。

表 2.2 与财务相关的模块或子模块列表

若要了解更多关于经济学、财务或会计的信息,请访问以下网页:

名称 位置
Python 模块索引(v3.5) docs.python.org/3/py-modindex.html
PyPI – Python 包索引 pypi.python.org/pypi
Python 模块索引(v2.7) docs.python.org/2/py-modindex.html

表 2.3 与 Python 模块(包)相关的网站

pandas_reader 模块介绍

通过该模块,用户可以从 Yahoo! Finance、Google Finance、联邦储备经济数据FRED)和 Fama-French 因子下载各种经济和财务数据。

假设已安装pandas_reader模块。有关如何安装此模块的详细信息,请参阅如何安装 Python 模块部分。首先,让我们看一个最简单的例子,只需两行代码即可获取 IBM 的交易数据;请见下文:

import pandas_datareader.data as web
df=web.get_data_google("ibm")

我们可以使用点头和点尾显示部分结果;请见以下代码:

>>> df.head()
>>> 
                  Open        High         Low       Close   Volume  
Date                                                                  
2010-01-04  131.179993  132.970001  130.850006  132.449997  6155300   
2010-01-05  131.679993  131.850006  130.100006  130.850006  6841400   
2010-01-06  130.679993  131.490005  129.809998  130.000000  5605300   
2010-01-07  129.869995  130.250000  128.910004  129.550003  5840600   
2010-01-08  129.070007  130.919998  129.050003  130.850006  4197200   

             Adj Close  
Date                    
2010-01-04  112.285875  
2010-01-05  110.929466  
2010-01-06  110.208865  
2010-01-07  109.827375  
2010-01-08  110.929466 
 >> >df.tail()
>>> 
                  Open        High         Low       Close   Volume  
Date                                                                  
2016-11-16  158.460007  159.550003  158.029999  159.289993  2244100   
2016-11-17  159.220001  159.929993  158.850006  159.800003  2256400   
2016-11-18  159.800003  160.720001  159.210007  160.389999  2958700   
2016-11-21  160.690002  163.000000  160.369995  162.770004  4601900   
2016-11-22  163.000000  163.000000  161.949997  162.669998  2707900   

             Adj Close  
Date                    
2016-11-16  159.289993  
2016-11-17  159.800003  
2016-11-18  160.389999  
2016-11-21  162.770004  
2016-11-22  162.669998  
>>>

本模块将在第四章,数据来源中再次进行更详细的解释。

两个财务计算器

在下一章中,将介绍并讨论许多基本的财务概念和公式。通常,在学习企业财务或财务管理时,学生依赖 Excel 或财务计算器来进行估算。由于 Python 是计算工具,因此用 Python 编写的财务计算器无疑将加深我们对财务和 Python 的理解。

这是第一个用 Python 编写的财务计算器,来自Numpy.lib.financial;请见以下代码:

>>> import numpy.lib.financial as fin
>>> dir(fin)
['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_convert_when', '_g_div_gp', '_rbl', '_when_to_num', 'absolute_import', 'division', 'fv', 'ipmt', 'irr', 'mirr', 'np', 'nper', 'npv', 'pmt', 'ppmt', 'print_function', 'pv', 'rate']
>>>

在第三章,时间价值中将使用并讨论的函数包括fv()irr()nper()npv()pmt()pv()rate()。以下代码展示了使用pv()的一个示例:

>>> import numpy.lib.financial as fin
>>> fin.pv(0.1,1,0,100)
-90.909090909090907
>>>

第二个财务计算器由作者提供。使用这个第二个财务计算器有许多优点。首先,所有功能都采用与教科书中公式相同的格式。

换句话说,这里没有 Excel 的符号约定。

例如,pv_f()函数将依赖于以下公式:

两个财务计算器

名为pvAnnuity()的函数基于以下公式:

两个金融计算器

第二,估算一个未来现金流的现值的公式与估算年金现值的公式是分开的。这将帮助学生,特别是初学者,避免不必要的困惑。

为了进行对比,numpy.lib.financial.pv()函数实际上结合了公式(6)和(7)。我们将在第三章,货币的时间价值中详细讨论这一点。第三,对于每个函数,提供了许多示例。这意味着用户花费更少的时间去理解各个函数的含义。第四,这个第二个金融计算器提供的功能比numpy.lib.financial子模块能提供的更多。最后但同样重要的是,用户最终会学会如何用 Python 编写自己的金融计算器。更多细节,请参见第三章,货币的时间价值中的最后一节。

要使用这样的金融计算器,用户应从作者的网站下载名为fincal.cpython-35.syc的文件(canisius.edu/~yany/fincal.cpython-35.pyc)。假设可执行文件已保存在c:/temp/目录下。要将c:/temp/添加到 Python 路径中,请点击菜单栏最右侧的 Python 徽标;请参见下图:

两个金融计算器

点击前面截图中所示的徽标后,用户将看到以下截图中左侧的屏幕:

两个金融计算器

点击添加路径后,输入c:/temp/;请参见前面截图中右侧的屏幕。现在,我们可以使用import fincal来使用模块中的所有函数。在第三章,货币的时间价值中,我们展示了如何生成这样的fincal模块:

>>>import fincal
>>>dir(fincal)
['CND', 'EBITDA_value', 'IRR_f', 'IRRs_f', 'NPER', 'PMT', 'Rc_f', 'Rm_f', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__request', '__spec__', 'bondPrice', 'bsCall', 'convert_B_M', 'duration', 'exp', 'fincalHelp', 'fvAnnuity', 'fv_f', 'get_200day_moving_avg', 'get_50day_moving_avg', 'get_52week_high', 'get_52week_low', 'get_EBITDA', 'get_all', 'get_avg_daily_volume', 'get_book_value', 'get_change', 'get_dividend_per_share', 'get_dividend_yield', 'get_earnings_per_share', 'get_historical_prices', 'get_market_cap', 'get_price', 'get_price_book_ratio', 'get_price_earnings_growth_ratio', 'get_price_earnings_ratio', 'get_price_sales_ratio', 'get_short_ratio', 'get_stock_exchange', 'get_volume', 'log', 'market_cap', 'mean', 'modified_duration', 'n_annuity', 'npv_f', 'payback_', 'payback_period', 'pi', 'pvAnnuity', 'pvAnnuity_k_period_from_today', 'pvGrowPerpetuity', 'pvGrowingAnnuity', 'pvPerpetuity', 'pvPerpetuityDue', 'pv_excel', 'pv_f', 'r_continuous', 're', 'sign', 'sqrt', 'urllib']

要查找每个函数的用法,请使用help()函数;请参见以下示例:

>>> import fincal
>>> help(fincal.pv_f)
Help on function pv_f in module fincal:

pv_f(fv, r, n)
    Objective: estimate present value
           fv: fture value
           r : discount period rate
           n : number of periods
     formula : fv/(1+r)**n      
         e.g.,
         >>>pv_f(100,0.1,1)
         90.9090909090909
         >>>pv_f(r=0.1,fv=100,n=1)
         90.9090909090909
         >>>pv_f(n=1,fv=100,r=0.1)
         90.9090909090909
>>>

从前面的信息中,用户可以知道该函数的目标、三个输入值的定义、使用的公式以及一些示例。

如何安装 Python 模块

如果 Python 是通过 Anaconda 安装的,那么很可能本书中讨论的许多模块已经随着 Python 一起安装。如果 Python 是独立安装的,用户可以使用 PyPi 来安装或更新。

例如,我们对安装 NumPy 感兴趣。在 Windows 上,我们使用以下代码:

python -m pip install -U pip numpy

如果Python.exe在路径中,我们可以先打开一个 DOS 窗口,然后输入前述命令。如果Python.exe不在路径中,我们需要打开一个 DOS 窗口,然后移动到Python.exe文件的位置;例如,见下图:

如何安装 Python 模块

对于 Mac,我们有以下代码。有时,在运行前述命令后,你可能会收到以下信息,提示更新 PiP:

如何安装 Python 模块

这里给出了更新 pip 的命令行:

python –m pip install –upgrade pip

请参见以下截图显示的结果:

如何安装 Python 模块

要独立安装 NumPy,在 Linux 或 OS X 上,我们执行以下命令:

pip install -U pip numpy

要为 Anaconda 安装一个新的 Python 模块,我们有以下列表。另见链接:conda.pydata.org/docs/using/pkgs.html

命令 描述
conda list 列出活动环境中的所有软件包
conda list -n snowflakes 列出安装到名为 snowflakes 的非活动环境中的所有软件包
conda search beautiful-soup 使用 conda install 将如Beautiful Soup等包安装到当前环境
conda install --name bunnies quant 安装名为quant的 Python 模块(包)
conda info 获取更多信息

表 2.4 使用 conda 安装新软件包的命令列表

以下截图展示了执行 conda info 命令后你将看到的内容:

如何安装 Python 模块

以下示例与安装名为pandas_datareader的 Python 模块有关:

如何安装 Python 模块

在回答y后,模块完成安装后,以下结果将显示:

如何安装 Python 模块

要获取各种模块的版本,我们有以下代码:

>>>import numpy as np
>>> np.__version__
'1.11.1'
>>> import scipy as sp
>>> sp.__version__
'0.18.1'
>>>import pandas as pd
>>> pd.__version__
'0.18.1'

模块依赖性

在本书的最开始,我们提到使用 Python 的一个优势是它提供了数百个被称为模块的特殊软件包。

为了避免重复劳动并节省开发新模块的时间,后续模块选择使用早期模块开发的函数;也就是说,它们依赖于早期模块。

这个优势显而易见,因为开发者在构建和测试新模块时可以节省大量时间和精力。然而,一个缺点是安装变得更加困难。

有两种竞争方法:

  • 第一种方法是将所有内容打包在一起,确保各部分能够协调运行,从而避免独立安装n个软件包的麻烦。假设它能正常工作,这非常棒。但潜在的问题是,单个模块的更新可能不会反映在超级包中。

  • 第二种方法是使用最小依赖项。这对包的维护者来说会减少麻烦,但对于需要安装多个组件的用户来说,可能会更加麻烦。Linux 有更好的方法:使用包管理器。包的发布者可以声明依赖项,系统会追踪它们,前提是它们在 Linux 仓库中。SciPy、NumPy 和 quant 都是这样设置的,效果很好。

练习

  1. 如果我们的 Python 是通过 Anaconda 安装的,是否需要单独安装 NumPy?

  2. 使用超级包同时安装多个模块有哪些优点?

  3. 如何查找 NumPy 或 SciPy 中包含的所有函数?

  4. 有多少种方法可以导入 SciPy 中包含的特定函数?

  5. 以下操作有什么问题?

    >>>x=[1,2,3]
    >>>x.sum()
    
  6. 如何打印给定数组的所有数据项?

  7. 以下代码行有什么问题?

    >>>import np
    >>>x=np.array([True,false,true,false],bool)
    
  8. 查找 stats 子模块(SciPy)中包含的 skewtest 函数的含义,并给出一个使用该函数的示例。

  9. 算术平均数和几何平均数有什么区别?

  10. 调试以下代码行,用于估算给定收益集的几何平均数:

    >>>import scipy as sp
    >>>ret=np.array([0.05,0.11,-0.03])
    >>>pow(np.prod(ret+1),1/len(ret))-1
    
  11. 编写一个 Python 程序,估算给定收益集的算术平均数和几何平均数。

  12. 查找 stats 子模块(SciPy)中包含的 zscore() 函数的含义,并提供一个使用该函数的简单示例。

  13. 以下代码行有什么问题?

    >>>c=20
    >>>npv=np.npv(0.1,c)
    
  14. 什么是模块依赖性,如何处理它?

  15. 编写依赖其他模块的模块有哪些优缺点?

  16. 如何使用 NumPy 中包含的财务函数;例如,pv()fv() 函数?

  17. 对于 numpy.lib.financial 中包含的函数,SciPy 中是否有类似的函数?

  18. 如何使用作者生成的 fincal 模块中包含的函数?

  19. 在哪里可以找到所有 Python 模块的列表?

  20. 如何查找与财务相关的 Python 模块的更多信息?

总结

在本章中,我们讨论了 Python 最重要的特性之一:模块。模块是由专家或任何个人编写的包,用于服务于特定的目的。与模块相关的知识对于我们理解 Python 及其在金融中的应用至关重要。特别地,我们介绍并讨论了最重要的模块,如 NumPy、SciPy、matplotlibstatsmodelspandaspandas_reader。此外,我们简要提及了模块依赖关系和其他问题。还介绍了两个用 Python 编写的金融计算器。在第三章中,货币的时间价值,我们将讨论许多与金融相关的基本概念,如单个未来现金流的现值、永续年金的现值、成长永续年金的现值、年金现值以及与未来价值相关的公式。此外,我们将讨论净现值NPV)、内部收益率IRR)和回收期的定义。之后,我们将解释几个投资决策规则。

第三章:时间价值

就金融本身而言,本章并不依赖于前两章。由于本书中使用 Python 作为计算工具来解决各种金融问题,因此最低要求是读者应安装 Python 以及 NumPy 和 SciPy。如果读者通过 Anaconda 安装了 Python,实际上可以不读前两章。另外,读者可以阅读附录 A 了解如何安装 Python。

本章将介绍并详细讨论与金融相关的各种概念和公式。由于这些概念和公式非常基础,曾学习过一门金融课程的读者,或在金融行业工作了几年的专业人士,可以快速浏览本章内容。再次强调,本书的一个特点与典型金融教材有所不同,即使用 Python 作为计算工具。特别是,以下主题将会涉及:

  • 单一未来现金流的现值与永续年金的现值

  • 增长永续年金的现值

  • 年金的现值与未来值

  • 永续年金与永续年金到期,年金与年金到期的区别

  • SciPy 中包含的相关函数以及 numpy.lib.financial 子模块

  • 一个用 Python 编写的免费财务计算器,名为 fincal

  • NPV 和 NPV 法则的定义

  • IRR 和 IRR 法则的定义

  • 时间价值与 NPV 图形展示

  • 回收期和回收期法则的定义

  • 如何使用 Python 编写自己的财务计算器

时间价值引论

让我们用一个非常简单的例子来说明。假设今天将 $100 存入银行,年利率为 10%。一年后存款的价值是多少?以下是包含日期和现金流的时间线:

时间价值引论

显然,我们的年利息支付将是 $10,即 1000.1=10*。因此,总价值将是 110,即 100 + 10。原始的 $100 是本金。或者,我们有以下结果:

时间价值引论

假设 $100 将以相同的 10% 年利率在银行存两年。两年末的未来价值是多少?

时间价值引论

由于在第一年末,我们有 $110,并且应用相同的逻辑,第二年末的未来价值应该是:

时间价值引论

由于 110 = 100(1+0.1)*,所以我们有以下表达式:

时间价值引论

如果 $100 存入银行五年,年利率为 10%,那么五年末的未来价值是多少?根据前述逻辑,我们可以得到以下公式:

时间价值引论

一般化得出我们用来估算给定现值的未来值的第一个公式:

货币时间价值简介

这里,FV是未来值,PV是现值,R是期利率,n是期数。在前面的例子中,R是年利率,n是年数。Rn的频率应当一致。这意味着,如果R是年利率(月利率/季利率/日利率),那么n必须是年数(月数/季数/天数)。对应的函数是 SciPy 模块中的fv(),可用来估算未来值;请参阅以下代码。若要估算在年末以 10%年利率计算的未来值,代码如下:

>>>import scipy as sp
>>> sp.fv(0.1,2,0,100)
-121.00000000000001

对于该函数,输入格式为sp.fv(rate,nper,pmt,pv=0,when='end')。目前,暂时忽略最后一个名为 when 的变量。对于方程式 (1),没有 pmt,因此第三个输入应为零。请注意先前结果中的负号。原因在于scipy.fv()函数遵循 Excel 符号约定:正的未来值对应负的现值,反之亦然。要了解更多关于此函数的信息,我们可以输入help(sp.fv),查看以下几行内容:

>>> help(sp.fv)

numpy.lib.financial模块中fv函数的帮助文档:

fv(rate, nper, pmt, pv, when='end')

计算未来值。

如果我们不小心输入sp.fv(0.1,2,100,0),结果和相应的现金流如下所示:

>>>import scipy as sp
>>> sp.fv(0.1,2,100,0) 
    -210.0000000000002
       >>>

本章稍后将展示,sp.fv(0.1,2,100,0)对应的是两个相等的 100 美元在第一年和第二年年末发生的现值。从方程式 (1),我们可以轻松推导出第二个公式:

货币时间价值简介

PVFVRn的符号与方程式 (1)中的符号保持一致。如果我们计划在第五年末得到 234 美元,并且年利率为 1.45%,那么我们今天需要存入多少?应用方程式 (2) 手动计算后的结果如下图所示:

>>> 234/(1+0.0145)**5
     217.74871488824184
>>> sp.pv(0.0145,5,0,234)
     -217.74871488824184

另外,也可以使用sp.pv()函数,参见右侧的结果。要了解更多关于sp.pv()函数的信息,我们可以使用help(sp.pv),查看以下输出的一部分:

>>>import scipy as sp
>>> help(sp.pv)

货币时间价值简介

请注意,对于输入变量集的第四个输入变量,scipy.fv()scipy.pv()函数的行为不同:scipy.fv(0.1,1,100)会给出错误消息,而scipy.pv(0.1,1,100)则可以正常工作。原因是scipy.pv()函数的第四个输入变量默认值为零,而scipy.fv()函数没有第四个输入变量的默认值。这是 Python 编程中的一种不一致之处。

在金融学中,广为人知的是,今天收到的 100 美元比一年后收到的 100 美元更有价值,而一年后收到的 100 美元又比两年后收到的 100 美元更有价值。如果使用不同的大小来表示相对价值,我们将得到以下图形。第一个蓝色圆圈是今天 100 美元的现值,第二个是第一年末 100 美元的现值,依此类推。生成该图像的 Python 程序见附录 B

货币时间价值介绍

下一个概念是永续年金,它被定义为相同的恒定现金流,按相同的间隔永远支付。这里是时间线以及这些恒定现金流:

货币时间价值介绍

请注意,在前面的例子中,第一个现金流发生在第一个周期结束时。我们可以有其他永续年金,其第一个现金流发生在其他周期的结束。我们先研究这个例子,稍后在本章中,我们会进行一个简单的扩展。当周期贴现率为R时,如何计算这种永续年金的现值?

首先,方程式(2)可以应用于每一个未来现金流。因此,所有这些现值的总和将是解:

货币时间价值介绍

为了简化我们的推导,永续年金现值(PV)被替换为PV。我们称之为方程式(I):

货币时间价值介绍

为了推导公式,方程式(I)的两边都乘以1/(1+R);见下式。我们称之为方程式(II)

货币时间价值介绍

方程式(I)减去方程式(II)得到下式:

货币时间价值介绍

将两边乘以(1+R),我们得到:

货币时间价值介绍

重组前面的结果,最终我们得到了估算永续年金现值的公式:

货币时间价值介绍

这里有一个例子。约翰计划每年捐赠 3,000 美元给他的母校,用于为即将到来的 MBA 学生举办迎新派对,且每年都会举行。如果年贴现率为 2.5%,且第一次派对将在第一年末举行,那么他今天应该捐赠多少?通过应用前面的公式,答案是 120,000 美元:

>>> 3000/0.025
   120000.0

假设第一个现金流为C,且随后的现金流享有恒定的增长率 g;见下方时间线和现金流:

货币时间价值介绍

如果贴现率为R,那么估算成长型永续年金现值的公式如下:

货币时间价值介绍

同样,CRg 的频率应该保持一致,也就是说,它们的频率应该相同。章节末尾有一个问题要求读者证明方程 (4)。以约翰的 MBA 欢迎派对捐款为例,每年需要 $3,000 的费用是基于零通货膨胀的。假设年通货膨胀率为 1%,他今天需要捐赠多少钱?每年所需的金额如下所示:

货币时间价值介绍

以下结果表明,他今天需要 $200,000:

>>> 3000/(0.025-0.01)
199999.99999999997

对于永续年金,如果第一笔现金流发生在第 k 期末,我们有以下公式:

货币时间价值介绍

显然,当第一笔现金流发生在第一个时期末时,方程 (5) 会简化为方程 (3)。年金被定义为在 n 期内相同时间间隔内的相同现金流。如果第一笔现金流发生在第一个时期末,则年金的现值通过以下公式估算:

货币时间价值介绍

在这里,C 是在每个时期末发生的递归现金流,R 是期间折现率,n 是期数。方程 (5) 比其他方程要复杂。然而,稍加想象,方程 (6) 可以通过结合方程 (2) 和 (3) 推导出来;有关更多细节,请参见附录 C

要估算年金的未来价值,我们有以下公式:

货币时间价值介绍

从概念上讲,我们可以将方程 (7) 看作是方程 (6) 和 (1) 的组合。在之前与永续年金或年金相关的公式中,假设所有现金流都发生在期末。对于年金或永续年金,当现金流发生在每个时间段的开始时,它们被称为期初年金或期初永续年金。计算其现值有三种方法。

对于第一种方法,scipy.pv()numpy.lib.financial.pv() 中的最后一个输入值将取值为 1。

假设折现率为每年 1%。接下来的 10 年每年现金流为 $20。第一笔现金流今天支付。这些现金流的现值是多少?结果如下所示:

>>>import numpy.lib.financial as fin
>>> fin.pv(0.01,10,20,0,1)
-191.32035152017377

请注意,numpy.lib.financial.pv() 函数的输入格式为 ratenperpmtfvwhen。最后一个变量 when 的默认值为零,即发生在期末。当 when 取值为 1 时,表示为期初年金。

对于第二种方法,以下公式可以应用:

货币时间价值介绍

这里是方法:将期初年金视为普通年金,然后将结果乘以 (1+R)。应用如下所示:

>>>import numpy.lib.financial as fin
>>> fin.pv(0.01,10,20,0)*(1+0.01)
-191.3203515201738

对于第三种方法,我们使用名为 fincal.pvAnnuityDue() 的函数,该函数包含在用 Python 编写的财务计算器 fincal 包中;请参见以下结果:

>>> import fincal
>>> fincal.pvAnnuityDue(0.01,10,20)
191.32035152017383

有关如何下载 fincal 模块,请参见 附录 D - 如何下载一个用 Python 编写的免费财务计算器。要获取有关此函数的更多信息,可以使用 help() 函数;请参见以下代码:

>>>import fincal
>>>help(fincal.pvAnnuityDue)
Help on function pvAnnuityDue in module __main__:

pvAnnuityDue(r, n, c)
     Objective : estimate present value of annuity due
          r    : period rate 
          n    : number of periods    
          c    : constant cash flow 

货币时间价值介绍

Example 1: >>>pvAnnuityDue(0.1,10,20)
                     135.1804763255031

    Example #2:>>> pvAnnuityDue(c=20,n=10,r=0.1)
                     135.1804763255031
>>>

有关名为 fincal 的财务计算器的更多详细信息,请参见下一节。如果现金流将以 g 的固定速度增长,我们有以下增长年金的公式:

货币时间价值介绍

这些函数在 SciPy 或 numpy.lib.financial 中没有对应的函数。幸运的是,我们有一个名为 fincal 的财务计算器,它包含了 pvGrowingAnnuity()fvGrowingAnnuity() 函数;有关更多详细信息,请参见以下代码:

>>> import fincal
>>> fincal.pvGrowingAnnuity(0.1,10,20,0.03)
137.67487382555464
>>>

要获取有关此函数的更多信息,请输入 help(fincal.pvGrowingAnnuity);请参见以下代码:

>>> import fincal
>>> help(fincal.pvGrowingAnnuity)
Help on function pvGrowingAnnuity in module fincal:
pvGrowingAnnuity(r, n, c, g)
     Objective: estimate present value of a growting annuity    
         r    : period discount rate
         n    : number of periods 
         c    : period payment
         g    : period growth rate  (g<r)

货币时间价值介绍

Example #1 >>>pvGrowingAnnuity(0.1,30,10000,0.05)
                    150463.14700582038

    Example #2: >>> pvGrowingAnnuity(g=0.05,r=0.1,c=10000,n=30)
                      150463.14700582038
>> >

用 Python 编写财务计算器

在讨论货币时间价值的各种概念时,学习者需要一个财务计算器或 Excel 来解决各种相关问题。

从前面的示例可以看出,像 scipy.pv() 这样的多个函数可以用来估算一个未来现金流的现值或年金现值。实际上,SciPy 模块中与财务相关的函数来自 numpy.lib.financial 子模块:

>>> import numpy.lib.financial as fin
>>> dir(fin)
['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_convert_when', '_g_div_gp', '_rbl', '_when_to_num', 'absolute_import', 'division', 'fv', 'ipmt', 'irr', 'mirr', 'np', 'nper', 'npv', 'pmt', 'ppmt', 'print_function', 'pv', 'rate']
>>>
Below are a few examples, below. 
>>>import numpy.lib.financial as fin
>>> fin.pv(0.1,3,0,100)      # pv of one future cash flow
-75.131480090157751
>>> fin.pv(0.1,5,100)        # pv of annuity
-379.07867694084507
>>> fin.pv(0.1,3,100,100)    # pv of annuity plus pv of one fv
-323.81667918858022
>>>

首先,我们导入与各种财务函数相关的两个模块。

>>>import scipy as sp
>>>import numpy.lib.financial as fin

下表总结了这些函数:

函数 输入格式
sp.fv() fin.fv()
sp.pv() fin.pv()
sp.pmt() fin.pmt()
sp.npv() fin.npv()
sp.rate() fin.rate()
sp.nper() fin.nper()
sp.irr() fin.irr()
sp.mirr() fin.mirr()
sp.ipmt() fin.ipmt()
sp.ppmt() fin.ppmt()

表 3.1 Scipy 和 numpy.lib.financial 中包含的函数列表

另一个财务计算器是由本书作者编写的。附录 B 显示了如何下载它。以下是函数列表:

>>> import fincal
>>> dir(fincal)
 ['CND', 'EBITDA_value', 'IRR_f', 'IRRs_f', 'NPER', 'PMT', 'Rc_f', 'Rm_f', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__request', '__spec__', 'bondPrice', 'bsCall', 'convert_B_M', 'duration', 'exp', 'fincalHelp', 'fvAnnuity', 'fv_f', 'get_200day_moving_avg', 'get_50day_moving_avg', 'get_52week_high', 'get_52week_low', 'get_EBITDA', 'get_all', 'get_avg_daily_volume', 'get_book_value', 'get_change', 'get_dividend_per_share', 'get_dividend_yield', 'get_earnings_per_share', 'get_historical_prices', 'get_market_cap', 'get_price', 'get_price_book_ratio', 'get_price_earnings_growth_ratio', 'get_price_earnings_ratio', 'get_price_sales_ratio', 'get_short_ratio', 'get_stock_exchange', 'get_volume', 'log', 'market_cap', 'mean', 'modified_duration', 'n_annuity', 'npv_f', 'payback_', 'payback_period', 'pi', 'pvAnnuity', 'pvAnnuityDue', 'pvAnnuity_k_period_from_today', 'pvGrowingAnnuity', 'pvGrowingPerpetuity', 'pvPerpetuity', 'pvPerpetuityDue', 'pv_excel', 'pv_f', 'r_continuous', 're', 'sign', 'sqrt', 'urllib']

使用这个财务计算器有几个优点,相比于 SciPy 模块和numpy.lib.financial子模块中包含的函数。首先,对于三种现值,pv(单笔现金流)pv(年金)pv(年金到期),分别有三个对应的函数,分别是pv_f()pvAnnuity()pvAnnuityDue()。因此,对于一个对金融知识了解较少的新学习者来说,他/她更不容易产生困惑。其次,对于每个函数,如单笔未来现金流的现值,输出与典型教材中显示的公式完全一致;请参见以下公式:

用 Python 编写财务计算器

换句话说,没有 Excel 的符号约定。对于fv=100r=0.1n=1,根据之前的公式,我们应该得到一个 90.91 的值。通过以下代码,我们展示了没有符号约定和有符号约定的结果:

>>>import fincal 
>>> fincal.pv_f(0.1,1100)
90.9090909090909
>>> import scipy as sp
>>> sp.pv(0.1,1,0,100)
    -90.909090909090907

第三,对于fincal中的每个函数,我们可以找出使用的公式,并附上一些示例:

>>>import fincal
>>> help(fincal.pv_f)
Help on function pv_f in module __main__:

pv_f(r, n, fv)
    Objective: estimate present value
           r : period rate
           n : number of periods
          fv : future value

用 Python 编写财务计算器

Example 1: >>>pv_f(0.1,1,100)        # meanings of input variables 
                 90.9090909090909        # based on their input order

    Example #2 >>>pv_f(r=0.1,fv=100,n=1) # meanings based on keywords
                 90.9090909090909
>>>

最后但同样重要的是,新的学习者可以自己编写财务计算器!更多细节,请参见《用 Python 编写你自己的财务计算器》章节和附录 H

从前面的讨论中,我们知道,对于年金的现值,可以使用以下公式:

用 Python 编写财务计算器

在上述公式中,我们有四个变量:pvcRn。为了估算现值,我们给定了cRn。实际上,对于任何一组三个值,我们都可以估算出第四个值。让我们使用 SciPy 和 NumPy 中相同的符号:

用 Python 编写财务计算器

四个对应的函数是:sp.pv()sp.pmt()sp.rate()sp.nper()。这是一个例子。约翰计划购买一辆二手车,价格为 $5,000。假设他将支付 $1,000 作为首付,其余部分借款。车贷的年利率为 1.9%,按月复利计算。如果他打算在三年内偿还贷款,那么他的月供是多少?我们可以手动计算月供;请参见以下代码:

>>> r=0.019/12
>>> pv=4000
>>> n=3*12
>>> pv*r/(1-1/(1+r)**n)
114.39577546409993

由于年利率是按月复利计算的,实际月利率为 0.019/12。在第五章《债券与股票估值》中,将更详细地讨论如何转换不同的有效利率。根据之前的结果,约翰的月供为 $114.40。或者,我们可以使用scipy.pmt()函数;请参见以下代码:

>>import scipy as sp
>>> sp.pmt(0.019/12,3*12,4000)
-114.39577546409993

类似地,对于前面公式中的利率,可以使用scipy.rate()numpy.lib.rate()函数。这里是一个例子。某公司计划为其 CEO 租赁一辆豪华轿车。如果接下来的三年每月付款$2,000,且汽车的现值为$50,000,隐含的年利率是多少?

>>>import scipy as sp
>>>r=sp.rate(3*12,2000,-50000,0)   # monthly effective rate
>>>r
  0.021211141641636025
>>> r*12
  0.2545336996996323               # annual percentage rate

月有效利率为 2.12%,年利率为 25.45%。

按照相同的逻辑,对于前面公式中的nper,可以使用scipy.nper()numpy.lib.financial.nper()函数。

这里是一个例子。Peter 借了$5,000 来支付获得 Python 证书的费用。如果月利率为 0.25%,他计划每月偿还$200,他需要多少个月才能偿还贷款?

>>>import scipy as sp
>>> sp.nper(0.012,200,-5000,0)
29.900894915842475

基于前面的结果,他大约需要 30 个月才能偿还全部贷款。在前面的两个例子中,未来价值为零。按照相同的逻辑,对于未来价值年金,我们有以下公式:

在 Python 中编写财务计算器

如果使用与 SciPy 和numpy.lib.financial相同的符号,我们得到以下公式:

在 Python 中编写财务计算器

scipy.pmt()scipy.rate()scipy.nper()numy.lib.financial.pmt()numpy.lib.financial.rate()numpy.lib.financial.nper()函数可以用来估算这些值。我们将在Scipy 和numpy.lib.financial中的常用公式部分进一步讨论这些公式。

净现值和 NPV 规则的定义

净现值NPV)由以下公式定义:

净现值和 NPV 规则的定义

这里是一个例子。初始投资为$100。接下来五年的现金流入分别是$50、$60、$70、$100 和$20,从第一年开始。如果折现率为 11.2%,该项目的 NPV 值是多少?由于只有六个现金流,我们可以手动计算:

>>> r=0.112
>>> -100+50/(1+r)+60/(1+r)**2+70/(1+r)**3+100/(1+r)**4+20/(1+r)**5
121.55722687966407
Using the scipy.npv() function, the estimation process could be simplified dramatically:
>>> import scipy as sp
>>> cashflows=[-100,50,60,70,100,20]
>>> sp.npv(0.112,cashflows)
121.55722687966407

根据前面的结果,该项目的 NPV 为$121.56。正常项目定义如下:首先是现金流出,其次是现金流入。任何其他情况都是不正常项目。对于正常项目,其 NPV 与折现率呈负相关;见下图。原因是当折现率上升时,未来现金流(大多数情况下是收益)的现值会比当前或最早的现金流(大多数情况下是成本)减少得更多。NPV 曲线描述了 NPV 与折现率之间的关系,见下图。有关生成图表的 Python 程序,请参阅附录 Ey轴为 NPV,x轴为折现率:

净现值和 NPV 规则的定义

为了估算一个项目的净现值(NPV),我们可以调用npv()函数,该函数包含在 SciPy 或numpy.lib.financial库中;请参阅以下代码:

>>>import scipy as sp
>>>cashflows=[-100,50,60,70]
>>>rate=0.1
>>>npv=sp.npv(rate,cashflows)
>>>round(npv,2)
47.62

scipy.npv() 函数估算给定现金流集的现值。第一个输入变量是折现率,第二个输入是现金流数组。注意,这个现金流数组中的第一个现金流发生在时间零。这个 scipy.npv() 函数不同于 Excel 的 NPV 函数,后者并不是一个真正的 NPV 函数。实际上,Excel 的 NPV 是一个 PV 函数。它通过假设第一个现金流发生在第一个期间结束时,来估算未来现金流的现值。使用 Excel npv() 函数的示例如下:

NPV 和 NPV 规则的定义

在仅使用一个未来现金流时,scipy.npv() 函数的意义通过以下代码行变得更加清晰:

>>>c=[100]
>>>x=np.npv(0.1,c)
>>>round(x,2)
>>>100.0

相关的 Excel 函数及其输出如图所示:

NPV 和 NPV 规则的定义

对于只有一个未来现金流,基于 Excel 的 npv() 函数的结果如前图所示。对于 numpy.lib.financial.npv() 函数,唯一的现金流 $100 会发生在今天,而对于 Excel 的 npv() 函数,唯一的现金流 $100 会发生在一个期间后。因此,100/(1+0.1) 得出 90.91。

NPV 规则如下:

NPV 和 NPV 规则的定义

IRR 和 IRR 规则的定义

内部收益率IRR)被定义为使 NPV 等于零的折现率。假设我们今天投资 $100,未来四年的现金流分别为 $30、$40、$40 和 $50。假设所有现金流都发生在年底,那么该投资的 IRR 是多少?在以下程序中,应用了 scipy.irr() 函数:

>>>import scipy as sp
>>> cashflows=[-100,30,40,40,50]
>>> sp.irr(cashflows)
       0.2001879105140867

我们可以验证这样的利率是否使 NPV 等于零。由于 NPV 为零,20.02% 确实是一个 IRR:

>>> r=sp.irr(cashflows)
>>> sp.npv(r,cashflows)
    1.7763568394002505e-14
>>>

对于一个正常项目,IRR 规则如下:

IRR 和 IRR 规则的定义

这里,Rc 是资本成本。这个 IRR 规则仅适用于正常项目。我们来看下面的投资机会。今天的初始投资是 $100,明年的投资是 $50。未来五年的现金流入分别为 $50、$70、$100、$90 和 $20。如果资本成本是 10%,我们应该接受这个项目吗?时间线及相应的现金流如下:

IRR 和 IRR 规则的定义

这里给出了 Python 代码:

>>>import scipy as sp
>>> cashflows=[-100,-50,50,70,100,90,20] 
>>> sp.irr(cashflows)
0.25949919326073245

由于 IRR 为 25.9%,高于 10% 的资本成本,我们应该根据 IRR 规则接受该项目。在前面的例子中,这是一个正常项目。对于不正常项目或具有多个 IRR 的项目,我们不能应用 IRR 规则。当现金流方向发生超过一次变化时,我们可能会有多个 IRR。假设我们的现金流为 504,-432,-432,-432843,从今天开始:

>>>import scipy as sp
>>> cashflows=[504, -432,-432, -432,843]
>>> sp.irr(cashflows)
    0.14277225152187745

相关图表如图所示:

IRR 和 IRR 规则的定义

由于我们的现金流方向发生了两次变化,该项目可能会有两个不同的 IRR。前述右侧的图像显示了这是这种情况。对于 Python 程序绘制前述的 NPV 曲线,参见附录 F。使用spicy.npv()函数,我们只得到了一个 IRR。从fincal.IRRs_f()函数中,我们可以得到两个 IRR;请参见以下代码:

>>>import fincal
>>> cashflows=[504, -432,-432, -432,843]
>>> fincal.IRRs_f(cashflows)
 [0.143, 0.192]

回收期和回收期规则的定义

回收期定义为收回初始投资所需的年数。假设初始投资为$100。如果每年公司能够回收$30,那么回收期为 3.3 年:

>>import fincal
>>>cashflows=[-100,30,30,30,30,30]
>>> fincal.payback_period(cashflows)
    3.3333333333333335

回收期规则的决策规则如下所示:

回收期和回收期规则的定义

在这里,T是项目的回收期,而Tc是收回初始投资所需的最大年数。因此,如果Tc是四年,那么回收期为 3.3 年的前述项目应该被接受。

回收期规则的主要优点是其简单性。然而,这种规则也存在许多缺点。首先,它没有考虑货币的时间价值。在前面的案例中,第一年末收到的$30 与今天收到的$30 是一样的。其次,回收期之后的任何现金流都被忽略了。这种偏差不利于具有较长未来现金流的项目。最后但同样重要的是,没有理论基础来定义一个好的Tc截止点。换句话说,没有合理的理由来解释为什么四年的截止点比五年更好。

用 Python 编写你自己的财务计算器

当一个新的 Python 学习者能够编写自己的财务计算器时,这可以视为一个巨大的成就。做到这一点的基本知识包括以下内容:

  • 如何编写一个函数的知识

  • 相关的财务公式是什么?

对于后者,我们已经从前面的章节中学到了一些内容,比如计算单一未来现金流现值的公式。现在我们来写一个最简单的 Python 函数来对输入值进行加倍:

def dd(x):
    return 2*x

这里,def是编写函数的关键字,dd是函数名,括号中的x是输入变量。对于 Python,缩进是至关重要的。前面的缩进表示第二行是dd函数的一部分。调用这个函数与调用其他内建的 Python 函数相同:

>>>dd(5)
 10
>>>dd(3.42)
 6.84

现在,让我们编写最简单的财务计算器。首先,启动 Python 并使用其编辑器输入以下代码:

def pvFunction(fv,r,n):
    return fv/(1+r)**n
def pvPerpetuity(c,r):
    return c/r
def pvPerpetuityDue(c,r):
    return c/r*(1+r)

为了简单起见,前述三个函数中的每个函数只有两行代码。在通过运行整个程序激活这些函数后,可以使用dir()函数来显示它们的存在:

>>> dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'pvFunction', 'pvPerpetuity','pvPerpetuityDue']
>>>

调用这个自生成的财务计算器是微不足道的;请参见以下代码:

>>> pvFunction(100,0.1,1)
90.9090909090909
>>> pvFunction(n=1,r=0.1,fv=100)
90.9090909090909
>>> pvFunction(n=1,fv=100,r=0.1)
90.9090909090909
>>>

同样,在输入值时,可以使用两种方法:输入变量的含义取决于它们的顺序,见第一次调用;或者使用关键字,见最后两个示例。

编写个人财务计算器的一种更优雅的方法见于附录 G

多个函数的两个通用公式

这一节是可选的,因为在数学表达式上比较复杂。跳过这一节不会影响对其他章节的理解。因此,这一节适合进阶学习者。目前为止,在本章中,我们已经学习了 SciPy 模块或 numpy.lib.financial 子模块中包含的几个函数的使用方法,如 pv()fv()nper()pmt()rate()。第一个通用公式与现值有关:

多个函数的两个通用公式

在前述公式的右侧,第一个部分是单个未来现金流的现值,而第二部分是年金的现值。变量type的值为零(默认值);它表示普通年金的现值,而如果type的值为 1,则表示到期年金。负号用于符号约定。如果使用与 SciPy 和 numpy.lib.financial 中函数相同的符号,我们有以下公式:

多个函数的两个通用公式

这里有几个使用公式(14)和 pv() 函数(来自 SciPy)示例。James 打算今天投资 x 美元,投资期限为 10 年。他的年回报率是 5%。在接下来的 10 年里,他将在每年年初提取 5,000 美元。此外,他希望在投资期满时能有 7,000 美元。今天他必须投资多少,即 x 的值是多少?通过手动应用前述公式,我们得到了以下结果。请注意负号:

>>> -(7000/(1+0.05)**10 + 5000/0.05*(1-1/(1+0.05)**10)*(1+0.05))
-44836.501153005614 

结果与调用 scipy.pv() 函数时相同;见以下代码:

>>> import scipy as sp
>>> sp.pv(0.05,10,5000,7000,1)
-44836.5011530056

为了将普通年金与到期年金分开,我们有以下两个公式。对于普通年金,我们有以下公式:

多个函数的两个通用公式

对于到期年金,我们有以下公式:

多个函数的两个通用公式

类似地,对于未来值,我们有以下通用公式:

多个函数的两个通用公式

如果使用与 SciPy 和 numpy.lib.financial 中相同的符号,我们有以下公式:

多个函数的两个通用公式

类似地,我们可以将年金与到期年金分开。对于普通年金,我们有以下公式:

多个函数的两个通用公式

对于到期年金,我们有以下公式:

多个函数的两个通用公式

在以下方程中,现值pv)出现了两次。然而,它们的含义大不相同。同样,未来值也出现了两次,且含义不同:

许多函数的两个通用公式

让我们用一个简单的例子来解释这两个方程之间的联系。首先,通过去掉符号约定来简化我们的函数,并假设是普通年金,即不考虑期初年金:

许多函数的两个通用公式

实际上,我们将有三个pv(现值)和三个fv(未来值)。我们投资$100,投资期为三年。此外,在接下来的三年里,每年年底我们再投资$20。如果年回报率为 4%,那么我们的投资的未来值是多少?

许多函数的两个通用公式

显然,我们可以应用最后一个方程来得出我们的答案:

>>> 100*(1+0.04)**3+20/0.04*((1+0.04)**3-1)
     174.91840000000005
>>> import scipy as sp
>>> sp.fv(0.04,3,20,100)
     -174.91840000000005

实际上,我们有三个未来值。我们将它们称为FV(total)FV( annuity)FV(one PV)。它们之间的关系如下:

许多函数的两个通用公式

以下代码展示了如何计算年金的未来值和一个现值的未来值:

>>> fv_annuity=20/0.04*((1+0.04)**3-1)
>>> fv_annuity
62.432000000000045
>>>fv_one_PV=100*(1+0.04)**3
>>> fv_one_PV
112.4864

总未来值是这两个未来值的总和:62.4320+ 112.4864=174.92。现在,让我们看看如何得到三个对应的现值。我们将它们称为PV(total)PV( annuity)PV(one PV)。它们之间的关系如下:

许多函数的两个通用公式

我们使用之前展示的相同现金流。显然,第一个$100 本身就是现值。三个$20 的现值可以手动计算;请参见以下代码:

>>>20/0.04*(1-1/(1+0.04)**3)
55.501820664542564

因此,总现值将是100 + 55.51=155.51。另外,我们可以应用 scipy.pv() 来估算年金的现值;请参见以下代码:

>>>import scipy as sp
>>> sp.pv(0.04,3,20)
   -55.501820664542592
>>>import fincal
>>> fincal.pvAnnuity(0.04,3,20)
    55.501820664542564

总未来值(174.92)和总现值(155.51)之间的关系如下:

>>>174.92/(1+0.04)**3
155.5032430587164

总结一下,当调用 scipy.pv()scipy.fv() 函数时,scipy.pv() 函数中的 fv 含义与 scipy.fv() 函数中的最终值是不同的。读者需要理解总未来值、一个现值的未来值和年金的未来值之间的区别。这对于 scipy.fv() 函数中的 pv 变量和调用 scipy.pv() 函数后的最终结果也适用。

附录 A – Python、NumPy 和 SciPy 的安装

通过 Anaconda 安装 Python,我们需要以下步骤:

  1. 访问continuum.io/downloads

  2. 找到合适的软件包;请参见以下截图:附录 A – Python、NumPy 和 SciPy 的安装

对于 Python,不同版本并存。从前面的截图中,我们可以看到存在 3.52.7 两个版本。对于本书来说,版本并不是那么关键。旧版本问题较少,而新版本通常有新的改进。在通过 Anaconda 安装 Python 后,NumPy 和 SciPy 将同时安装。启动 Python 并通过 Spyder 发出以下两行命令。如果没有错误,说明这两个模块已预安装:

>>> import numpy as np
>>> import scipy as sp

另一种方法是直接安装 Python。

访问 www.python.org/download。根据你的计算机,选择合适的安装包,例如 Python 3.5.2 版本。关于安装模块,请查阅 Python 文档。以下命令将从Python 包索引PIP)安装模块及其依赖项的最新版本:

python -m pip install SomePackage

对于 POSIX 用户(包括 Mac OS X 和 Linux 用户),本指南中的示例假定使用虚拟环境。要安装特定版本,请参见以下代码:

python -m pip install SomePackage==1.0.4    # specific version
python -m pip install "SomePackage>=1.0.4"  # minimum version

通常,如果已经安装了适当的模块,再次尝试安装它不会有任何效果。升级现有模块必须明确请求:

python -m pip install --upgrade SomePackage

附录 B – 时间价值的直观展示

如果读者在理解以下代码时有困难,可以忽略这一部分。在金融学中,我们知道今天收到的 $100 比一年后收到的 $100 更有价值。如果我们用大小来表示差异,我们可以通过以下 Python 程序来表示相同的概念:

from matplotlib.pyplot import *
fig1 = figure(facecolor='white')
ax1 = axes(frameon=False)
ax1.set_frame_on(False)
ax1.get_xaxis().tick_bottom()
ax1.axes.get_yaxis().set_visible(False)
x=range(0,11,2)	
x1=range(len(x),0,-1)
y = [0]*len(x);
name="Today's value of $100 received today"
annotate(name,xy=(0,0),xytext=(2,0.001),arrowprops=dict(facecolor='black',shrink=0.02))
s = [50*2.5**n for n in x1];
title("Time value of money ")
xlabel("Time (number of years)")
scatter(x,y,s=s);
show()

这里显示了图表。第一个蓝色圆圈是现值,而第二个是同样 $100 在第二年末的现值:

附录 B – 时间价值的直观展示

附录 C – 从未来现金流的现值和永久年金的现值推导年金现值

首先,我们有以下两个公式:

附录 C – 从未来现金流的现值和永久年金的现值推导年金现值

这里,FV 是未来值,R 是折现期利率,n 是期数,C 是在每个期末发生的相同现金流,第一个现金流发生在第一期末。

年金被定义为一组未来发生的等值现金流。如果第一个现金流发生在第一期末,则年金的现值由以下公式给出:

附录 C – 从未来现金流的现值和永久年金的现值推导年金现值

这里,C是每期末发生的递归现金流,R是期的折现率,n是期数。方程(3)相当复杂。然而,稍加想象,我们可以通过将方程(1)和(2)结合,推导出方程(3)。这可以通过将年金分解为两个永续年金来完成:

附录 C – 通过现值的未来单笔现金流和永续年金的现值推导年金现值

这相当于以下两个永续年金:

附录 C – 通过现值的未来单笔现金流和永续年金的现值推导年金现值

从概念上讲,我们可以这样考虑:玛丽将在未来 10 年内每年收到 20 美元。这相当于两个永续年金:她将永远每年收到 20 美元,并且从第 11 年开始,每年支付 20 美元。因此,她的年金现值将是第一个永续年金的现值减去第二个永续年金的现值:

附录 C – 通过现值的未来单笔现金流和永续年金的现值推导年金现值

如果相同的现金流在相同的时间间隔内永远发生,则称为永续年金。如果折现率是恒定的,并且第一笔现金流发生在第一个周期结束时,则其现值如下所示。

附录 D – 如何下载免费财务计算器

可执行文件位于canisius.edu/~yany/fincal.pyc。假设它保存在c:/temp/中。更改你的路径;请参见以下截图:

附录 D – 如何下载免费财务计算器

这是一个例子:

>>>import fincal 
>>> fincal.pv_f(0.1,1,100)
90.9090909090909

要找出所有包含的函数,可以使用dir()函数;请参见以下代码:

>>> import fincal
>>> dir(fincal)
['CND', 'EBITDA_value', 'IRR_f', 'IRRs_f', 'NPER', 'PMT', 'Rc_f', 'Rm_f', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__request', '__spec__', 'bondPrice', 'bsCall', 'convert_B_M', 'duration', 'exp', 'fincalHelp', 'fvAnnuity', 'fvAnnuityDue', 'fv_f', 'get_200day_moving_avg', 'get_50day_moving_avg', 'get_52week_high', 'get_52week_low', 'get_EBITDA', 'get_all', 'get_avg_daily_volume', 'get_book_value', 'get_change', 'get_dividend_per_share', 'get_dividend_yield', 'get_earnings_per_share', 'get_historical_prices', 'get_market_cap', 'get_price', 'get_price_book_ratio', 'get_price_earnings_growth_ratio', 'get_price_earnings_ratio', 'get_price_sales_ratio', 'get_short_ratio', 'get_stock_exchange', 'get_volume', 'log', 'market_cap', 'mean', 'modified_duration', 'n_annuity', 'npv_f', 'payback_', 'payback_period', 'pi', 'pvAnnuity', 'pvAnnuityDue', 'pvAnnuity_k_period_from_today', 'pvGrowingAnnuity', 'pvGrowingPerpetuity', 'pvPerpetuity', 'pvPerpetuityDue', 'pv_excel', 'pv_f', 'r_continuous', 're', 'sign', 'sqrt', 'urllib']

要了解每个函数的用途,可以使用help()函数:

>>> help(fincal.pv_f)
Help on function pv_f in module fincal:
pv_f(r, n, fv)
    Objective: estimate present value
           r : period rate
           n : number of periods
          fv : future value

附录 D – 如何下载免费财务计算器

Example 1: >>>pv_f(0.1,1,100)        # meanings of input variables 
                 90.9090909090909        # based on their input order

    Example #2 >>>pv_f(r=0.1,fv=100,n=1) # meanings based on keywords
                 90.9090909090909
>> >

附录 E – NPV 与 R 关系的图形表示

NPV 曲线是项目的净现值与其折现率(资本成本)之间的关系。对于正常项目(即现金流先为支出后为收入),其净现值将是折现率的递减函数;请参见以下代码:

import scipy as sp
from matplotlib.pyplot import *
cashflows=[-120,50,60,70]
rate=[]
npv =[]
for i in range(1,70):
    rate.append(0.01*i)
    npv.append(sp.npv(0.01*i,cashflows))

plot(rate,npv)
show()

相关图表如下所示:

附录 E – NPV 与 R 关系的图形表示

为了使我们的图表更好,我们可以添加标题、标签以及一条水平线;请参见以下代码:

import scipy as sp
from matplotlib.pyplot import *
cashflows=[-120,50,60,70]
rate=[]
npv=[]
x=(0,0.7)
y=(0,0)
for i in range(1,70):
    rate.append(0.01*i)
    npv.append(sp.npv(0.01*i,cashflows))

title("NPV profile")
xlabel("Discount Rate")
ylabel("NPV (Net Present Value)")
plot(rate,npv)
plot(x,y)
show()

输出如下所示:

附录 E – NPV 与 R 关系的图形表示

附录 F – 带有两个 IRR 的净现值(NPV)曲线图

由于现金流的方向发生了两次变化,我们可能会有两个内部收益率(IRR):

import scipy as sp
import matplotlib.pyplot as plt
cashflows=[504,-432,-432,-432,832]
rate=[]
npv=[]
x=[0,0.3]
y=[0,0]
for i in range(1,30): 
    rate.append(0.01*i)
    npv.append(sp.npv(0.01*i,cashflows))

plt.plot(x,y),plt.plot(rate,npv)
plt.show()

相应的图表如下所示:

附录 F – 带有两个内部收益率的净现值(NPV)图形展示

附录 G – 用 Python 编写你自己的财务计算器

现在,让我们编写我们最简单的财务计算器。首先,启动 Python 并使用编辑器输入以下代码。为简单起见,每个前面 10 个函数的函数体只有两行。再次强调,正确的缩进非常重要。因此,每个函数的第二行应该有缩进:

def pvFunction(fv,r,n):
    return fv/(1+r)**n
def pvPerpetuity(c,r):
    return c/r
def pvPerpetuityDue(c,r):
    return c/r*(1+r)
def pvAnnuity(c,r,n):
    return c/r*(1-1/(1+r)**n)
def pvAnnuityDue(c,r,n):
    return c/r*(1-1/(1+r)**n)*(1+r)
def pvGrowingAnnuity(c,r,n,g):
    return c/(r-g)*(1-(1+g)**n/(1+r)**n)
def fvFunction(pv,r,n):
    return pv*(1+r)**n
def fvAnnuity(cv,r,n):
    return c/r*((1+r)**n-1)
def fvAnnuityDue(cv,r,n):
    return c/r*((1+r)**n-1)*(1+r)
def fvGrowingAnnuity(cv,r,n):
    return c/(r-g)*((1+r)**n-(1+g)*n)

假设前面的程序名为myCalculator

以下程序将生成一个可执行文件,名为myCalculator.cpython-35.pyc

>>> import py_compile
>>> py_compile.compile('myCalculator.py')
'__pycache__\\myCalculator.cpython-35.pyc'
>>> __pycache__
py_compile.compile('c:/temp/myCalculator.py')

习题

  1. 如果年折现率为 2.5%,那么 10 年后收到 206 美元的现值是多少?

  2. 永续年金的未来价值是多少,如果年付款为 1 美元,年折现率为 2.4%?

  3. 对于一个普通项目,其净现值(NPV)与折现率负相关,为什么?

  4. 约翰在银行存入 5,000 美元,存期 25 年。如果年利率为每年 0.25%,未来值是多少?

  5. 如果年付款为 55 美元,剩余期限为 20 年,年折现率为 5.41%,按半年复利计算,现值是多少?

  6. 如果玛丽计划在第 5 年末拥有 2,400 美元,她每年需要存多少钱,如果对应的年利率为 3.12%?

  7. 为什么在以下代码中我们得到了负的期数?

    >>>import scipy as sp
    >>> sp.nper(0.012,200,5000,0)
    -21.99461003591637
    
  8. 如果一家公司每股收益从 2 美元增长到 4 美元,增长周期为 9 年(总增长为 100%),其年增长率是多少?

  9. 在本章中,在编写现值函数时,我们使用了pv_f()。为什么不使用pv(),它和以下公式一样呢?!习题

    这里,PV 是现值,FV 是未来值,R 是周期折现率,n 是期数。

  10. 一个项目在第一年和第二年分别产生 5,000 美元和 8,000 美元的现金流入。初期投资为 3,000 美元。第一年和第二年的折现率分别为 10%和 12%。该项目的净现值(NPV)是多少?

  11. A 公司将发行年票息为 80 美元,面值为 1,000 美元的新债券。利息支付为半年付,债券将在 2 年后到期。第一年的现货利率为 10%。第一年末,1 年期现货利率预计为 12%:

    • 债券的现值是多少?

    • 如果你愿意在第二年末接受一次性支付的金额是多少?

  12. 彼得的富有叔叔承诺如果他在四年内完成大学学业,将支付他 4,000 美元。理查德刚刚完成了非常艰难的二年级(大二),包括修读几门金融课程。理查德非常希望能够享受一个长假。适当的折现率为 10%,半年复利。彼得如果现在去度假,将放弃什么价值?

  13. 今天,您有 5000 美元可供投资,投资期限为 25 年。您被提供一个投资计划,未来 10 年每年回报 6%,接下来的 15 年每年回报 9%。在 25 年结束时,您将拥有多少资金?您的年平均回报率是多少?

  14. 使用默认输入值的优点和缺点是什么?

  15. 我们知道,增长永续年金的现值公式为:Exercises

    证明这一点。

  16. 今天,简已经 32 岁了。她计划在 65 岁时退休,届时储蓄达到 250 万美元。如果她每年能获得 3.41%的复利回报(按月复利),她每月需要存多少钱?

  17. 假设我们有一组小程序组成的文件,名为fin101.pyimport fin101from fin101 import *这两个 Python 命令有什么区别?

  18. 如何防止输入错误,如负利率?

  19. 编写一个 Python 程序来估算回收期。例如,初始投资为 256 美元,预计未来 7 年内的现金流分别为 34 美元、44 美元、55 美元、67 美元、92 美元、70 美元和 50 美元。该项目的回收期是多少年?

  20. 在前面的练习中,如果贴现率是每年 7.7%,则折现回收期是多少?注意:折现回收期是通过检查未来现金流的现值之和来计算如何收回初始投资。

摘要

本章介绍了许多与金融相关的基本概念,如单一未来现金流的现值、永续年金的现值、年金的现值、单一现金流/年金的未来值,以及应付年金现值的概念。详细讨论了几种决策规则,如净现值(NPV)规则、内部收益率(IRR)规则和回收期规则。在下一章中,我们将讨论如何从几个开放资源(如 Yahoo!Finance、Google Finance、Prof. French 的数据图书馆和联邦研究的经济数据图书馆)中检索与经济学、金融学和会计学相关的数据。

第四章:数据来源

自从我们的社会进入所谓的信息时代以来,我们就被大量的信息或数据所包围。正因如此,数据处理技能的人才需求日益增长,例如数据科学家或商业分析专业的毕业生。Kane(2006)提出了一个开源金融概念,包含三个组成部分:

  • 开源软件在测试假设和实施投资策略中的应用

  • 低成本获取财务数据

  • 复制以确认已发布的研究结果

本书中,这三个组成部分简单地称为:开源软件、开源数据和开源代码。Python 是最著名的开源软件之一。目前,公共数据的使用与当前环境不完全一致。在本书中,我们使用了大量数据,特别是公共数据。本章将涵盖以下主题:

  • 开源金融

  • 宏观经济数据来源

  • 会计数据来源

  • 财务数据来源

  • 其他数据来源

深入探讨更深层次的概念

本章的重点将是如何检索与经济、金融和会计相关的数据,尤其是公共数据。例如,雅虎财经提供了丰富的数据,如历史交易价格、当前价格、期权数据、年度和季度财务报表以及债券数据。此类公开可用的数据可用于估算 β(市场风险)、波动性(总风险)、夏普比率、詹森的α、特雷诺比率、流动性、交易成本,并进行财务报表分析(比率分析)和绩效评估。在未来的章节中,将更详细地讨论这些主题。关于经济学、金融和会计的公共数据,有许多优秀的来源,见下表:

名称 数据类型
雅虎财经 历史价格、年度和季度财务报表等
谷歌财经 当前和历史交易价格
美联储经济数据 利率、AAA、AA 评级债券的利率
法国教授数据图书馆 法马-法国因子时间序列、市场指数收益、无风险利率、行业分类
人口普查局 人口普查数据
美国财政部 美国财政收益
劳工统计局 通货膨胀、就业、失业、薪酬和福利
美国经济分析局 国内生产总值(GDP)等
国家经济研究局 经济周期、重要统计数据、总统报告

表 4.1:开放数据源列表

通常,有两种方式来检索数据:

  • 手动从特定位置下载数据,然后编写 Python 程序以检索和处理数据

  • 使用各种 Python 模块中包含的功能,例如 matplotlib.finance 子模块中的 quotes_historical_yahoo_ohlc() 函数

对于这两种方法,各有优缺点。第一种方法的主要优点是我们知道数据的来源。此外,由于我们编写自己的程序来下载和处理数据,这些程序的逻辑更加清晰。第二种方法的优点是获取数据快捷方便。从某种意义上说,用户甚至不需要知道数据的来源以及原始数据集的结构。缺点是使用的函数可能会发生变化,这可能导致某些问题。例如,quotes_historical_yahoo_ohlc()的旧版本是quotes_historical_yahoo()

为了从前述数据源中提取有用的信息,可以使用两个子模块:pandas_datareader.datamatplotlib.financial。要查看pandas_datareader.data中包含的函数,可以使用dir()函数:

深入探索更深层的概念

从前面的输出结果来看,似乎我们有八个与 YahooFinance 相关的函数,比如YahooDailyReader()YahooActionReader()YahooOptions()YahooQuotesReader()get_components_yahoo()get_data_yahoo()get_data_yahoo_actions()get_quote_yahoo()。实际上,我们也可以使用theDataReader()函数。类似地,还有一些函数可以用于从 Google、FRED 以及弗朗西斯教授的数据库中提取数据。

要查看各个函数的用法,可以使用help()函数。下面以前述输出中的第一个函数DataReader()为例:

深入探索更深层的概念

从输出结果中可以看到,该函数可以用来从 YahooFinance、Google Finance、圣路易斯联邦储备(FRED)以及弗朗西斯教授的数据库中提取数据。要查看matplotlib.finance子模块中包含的所有函数,请参考以下代码:

深入探索更深层的概念

细心的读者会发现这些名称的定义存在一些不一致之处;请注意某些函数名称的最后四个字母,即ochlohlcoclh

从 Yahoo!Finance 提取数据

Yahoo!Finance 提供历史市场数据、近期的财务报表、几年的财务报告、当前报价、分析师建议、期权数据等。历史交易数据包括每日、每周、每月数据以及股息数据。历史数据包括多个变量:开盘价、最高价、最低价、交易量、收盘价和调整后的收盘价(已对拆股和股息进行了调整)。历史报价通常不会追溯到 1960 年以前。这里我们展示如何手动获取 IBM 的月度数据:

  1. 访问finance.yahoo.com/

  2. 在搜索框中输入IBM

  3. 点击中间的历史价格

  4. 选择月度数据,然后点击应用

  5. 应用下点击下载数据

这里展示了开头和结尾的几行:

从 Yahoo!Finance 获取数据

假设上述下载的数据保存在c:/temp下,可以使用以下代码来检索它:

>>>import pandas as pd
>>>x=pd.read_csv("c:/temp/ibm.csv")

若要查看前几行和后几行观察值,可以使用.head().tail()函数。默认情况下,这两个函数的值为 5。在下面,x.head()命令会输出前五行,而x.tail(2)则会输出最后两行:

从 Yahoo!Finance 获取数据

更好的方法是使用包含在各种模块或子模块中的特定函数。这里是一个最简单的例子,只有两行代码即可获取 IBM 的交易数据,见以下代码:

>>>import pandas_datareader.data as getData
df = getData.get_data_google("IBM")

同样,.head().tail()函数可以用来展示结果的一部分,见以下代码:

>>>df.head(2)
>>>
                  Open        High         Low       Close   Volume  \
Date                                                                  
2010-01-04  131.179993  132.970001  130.850006  132.449997  6155300   
2010-01-05  131.679993  131.850006  130.100006  130.850006  6841400   
Adj Close  
Date                    
2010-01-04  112.285875
2010-01-05  110.929466
>>>df.tail(2)
                  Open        High         Low       Close   Volume  \
Date                                                                  
2016-12-08  164.869995  166.000000  164.220001  165.360001  3259700   
2016-12-09  165.179993  166.720001  164.600006  166.520004  3143900   
Adj Close  
Date                    
2016-12-08  165.360001
2016-12-09  166.520004
>>>

如果需要更长的时间周期,应该指定起始和结束输入变量,见以下代码:

>>>import pandas_datareader.data as getData
>>>import datetime
>>>begdate = datetime.datetime(1962, 11, 1)
>>>enddate = datetime.datetime(2016, 11, 7)
df = getData.get_data_google("IBM",begdate, enddate)

在前面的代码中,名为datetime.datetime()的函数定义了一个真实的日期变量。在本章后续部分,将展示如何从这样的变量中提取年份和月份。前两条观察结果在这里给出:

>>>df[0:2]
                Open        High         Low       Close   Volume  AdjClose
Date                                                                          
1962-11-01  345.999992  351.999986  341.999996  351.999986  1992000   1.391752
1962-11-02 351.999986369.875014 346.999991 357.249999  3131200   1.412510
>>>

仔细的读者应该会发现数据的顺序是不同的。当手动下载数据时,数据的顺序是从最新的(如昨天)向历史回溯。然而,当通过函数提取数据时,最旧的日期会排在最前面。大多数金融数据库采用相同的排序顺序:从最旧到最新。

以下程序使用了另一个名为quotes_historical_yahoo_ochl的函数。这个程序是最简单的,仅有两行:

>>>from matplotlib.finance import quotes_historical_yahoo_ochl as getData
>>>p=getData("IBM", (2015,1,1),(2015,12,31),asobject=True,adjusted=True)

在前面的程序中,第一行导入了一个名为quotes_historical_yahoo_ochl()的函数,它包含在matplotlib.finance中。此外,为了方便输入,长函数名被重命名为getData。用户也可以使用其他更方便的名称。第二行通过指定的股票代码从 Yahoo!Finance 网页上获取数据,数据的时间范围由起始和结束日期定义。为了展示前几行,我们输入p[0:4]

>>>p[0:4]
rec.array([ (datetime.date(2015, 1, 2), 2015, 1, 2, 735600.0, 150.47501253708967, 151.174636, 152.34067510485053, 150.1858367047493, 5525500.0, 151.174636),
 (datetime.date(2015, 1, 5), 2015, 1, 5, 735603.0, 150.43770546142676, 148.795914, 150.43770546142676, 148.497414517829, 4880400.0, 148.795914),
 (datetime.date(2015, 1, 6), 2015, 1, 6, 735604.0, 148.9451702494383, 145.586986, 149.215699719094, 144.7474294432884, 6146700.0, 145.586986),
 (datetime.date(2015, 1, 7), 2015, 1, 7, 735605.0, 146.64107567217212, 144.635494, 146.64107567217212, 143.68400235493388, 4701800.0, 144.635494),
dtype=[('date', 'O'), ('year', '<i2'), ('month', 'i1'), ('day', 'i1'), ('d', '<f8'), ('open', '<f8'), ('close', '<f8'), ('high', '<f8'), ('low', '<f8'), ('volume', '<f8'), ('aclose', '<f8')])>>>

最后几行展示了数据集的结构。例如,O表示 Python 对象,i2表示整数,f8表示浮点数。此时,完全理解这些数据类型的含义并不是那么关键。

为了理解如何从价格数组中估算回报率,我们来看一个简单的例子。假设我们有五个价格,它们的时间线是tt+1t+2t+3t+4

>>> import numpy as np
>>>price=np.array([10,10.2,10.1,10.22,9])
>>>price[1:]
array([ 10.2 ,  10.1 ,  10.22,   9\.  ])
>>>price[:-1]
array([ 10\.  ,  10.2 ,  10.1 ,  10.22])
>>> (price[1:]-price[:-1])/price[:-1]
array([ 0.02      , -0.00980392,  0.01188119, -0.11937378])
>>>

对于一个由 np.array() 定义的 NumPy 数组,例如之前定义的价格,我们使用 price[1:] 来表示从第二项到最后一项,即所有数据项除了第一项。请记住,NumPy 数组的下标是从 0 开始的。对于 price[:-1],它表示所有数据项,除了最后一项。我们可以手动验证这些回报值;请参见以下代码,查看前两个回报:

>>> (10.2-10)/10
0.019999999999999928
>>>
>>> (10.1-10.2)/10.2
-0.009803921568627416

这是另一个例子:

>>>import scipy as sp
>>>sp.random.seed(123)
>>>price=sp.random.random_sample(10)*15
>>>price
array([ 10.44703778,   4.29209002,   3.4027718 ,   8.26972154,
        10.79203455,   6.3465969 ,  14.71146298,  10.27244608,
         7.21397852,   5.88176277])
>>>price[1:]/price[:-1]-1
array([-0.58915722, -0.20719934,  1.43028978,  0.3050058 , -0.4119184 ,
        1.31800809, -0.30173864, -0.29773508, -0.18467143])
>>>

请注意,如果价格数组的排序方式相反:从最新到最旧,那么回报估算应该是 price[:-1]/price[1:]-1。根据之前的逻辑,以下程序计算了回报:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
ticker='IBM'
begdate=(2015,1,1) 
enddate=(2015,11,9)
p = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
ret = p.aclose[1:]/p.aclose[:-1]-1

为了使我们的程序更具通用性,在之前的程序中,添加了三个新变量,分别为 begdateenddateticker。请注意命令的最后一行。对于给定的两种价格,p1p2,假设 p2p1 之后。我们可以通过两种方式来估算回报:(p2-p1)/p1p2/p1-1。前者在概念上更清晰,而后者使我们的程序更不容易出错。同样,我们可以手动验证几个回报值:

>>>p.aclose[0:4]
array([ 151.174636,  148.795914,  145.586986,  144.635494])>>>
>>>ret[0:3]
array([-0.01573493, -0.02122663, -0.00629399])
>>> (p.aclose[1]-p.aclose[0])/p.aclose[0]
-0.01573492791475934

对于以下示例,首先下载了从 2011 年 1 月 1 日到 2015 年 12 月 31 日的 IBM 每日价格数据。然后,计算每日回报。平均日回报为 0.011%:

from scipy import stats
import numpy as np
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
ticker='ibm'
begdate=(2011,1,1)
enddate=(2015,12,31)
p=getData(ticker,begdate,enddate,asobject=True, adjusted=True)
ret=p.aclose[1:]/p.aclose[:-1]-1
mean=np.mean(ret)
print('   Mean '  )
print(round(mean,5))
>>>
   Mean 
>>>
0.00011

为了回答这个问题,即 0.00011 的平均日回报是否与零在统计学上有显著差异,可以使用 stats 模块中的 ttest_1samp() 函数:

0.00011
print(' T-test result: T-value and P-value'  )
print(stats.ttest_1samp(ret,0))
>>>
 T-test result: T-value and P-value
>>>
Ttest_1sampResult(statistic=0.3082333300938474, pvalue=0.75795590301241988)
>>>

由于 T 值为 0.31,P 值为 0.76,我们接受零假设。换句话说,从 2011 年到 2015 年,IBM 的日均回报在统计学上与零相同。要获取更多关于此函数的信息,可以使用 help() 函数。为了节省空间,这里仅显示了前几行:

>>>import scipy.stats
>>>help(stats.ttest_1samp)
Help on function ttest_1samp in module scipy.stats.stats:

ttest_1samp(a, popmean, axis=0, nan_policy='propagate')

它计算了单组数据的均值 T 检验。

这是一个双侧检验,用于检验独立观测样本 a 的期望值(均值)是否等于给定的总体均值 popmean

以下程序测试了两只股票的相等回报:IBMMSFT

import scipy.stats as stats
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
begdate=(2013,1,1)
enddate=(2016,12,9)

def ret_f(ticker,begdate,enddate):
    p = getData(ticker,begdate,
enddate,asobject=True,adjusted=True)
    ret=p.aclose[1:]/p.aclose[:-1]-1
    return(ret)

a=ret_f('IBM',begdate,enddate)
b=ret_f('MSFT',begdate,enddate)

这两种回报的计算方法如下所示:

>>>a.mean()*100
0.0022164073263915601
>>>b.mean()*100
0.10399096829827408
>>>

请注意,在之前的代码中,使用了 .mean() 而不是 scipy.mean()。为了进行均值相等的 T 检验,调用了 ttest_ind() 函数;请参见以下代码:

>>>print(stats.ttest_ind(a,b))
Ttest_indResult(statistic=-1.652826053660396, pvalue=0.09852448906883747)

假设存在两个价格,p1p2。以下方程定义了一个百分比回报 (R) 和对数回报:

从 Yahoo!Finance 获取数据

……..(1)

从 Yahoo!Finance 获取数据

……..(2)

这两者之间的关系如下所示:

从 Yahoo!Finance 获取数据

……..(3)

从 Yahoo!Finance 获取数据

……..(4)

对数回报的一个优点是,较长周期的回报是短期回报的总和。这意味着年化对数回报是季度对数回报的总和,季度对数回报是月度对数回报的总和。这一性质使我们的编程更加简洁。这里是一个更一般的公式:

从 Yahoo!Finance 获取数据

……..(5)

对于年化对数回报,我们可以应用以下公式:

从 Yahoo!Finance 获取数据

……..(6)

以下代码用于将日回报转化为月回报:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd
ticker='IBM'
begdate=(2015,1,1)
enddate=(2015,12,31)
x = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
logret = np.log(x.aclose[1:]/x.aclose[:-1])

date=[]
d0=x.date
for i in range(0,np.size(logret)):
    date.append(''.join([d0[i].strftime("%Y"),d0[i].strftime("%m")]))

y=pd.DataFrame(logret,date,columns=['retMonthly'])
retMonthly=y.groupby(y.index).sum()

在前面的程序中,strftime("%Y")命令用于提取年份的字符串,如2016。这里显示了一个更简单的例子:

>>>import pandas as pd
>>> x=pd.datetime(2016,1,1)
>>>x
datetime.datetime(2016, 1, 1, 0, 0)
>>>x.strftime("%Y")
'2016'

同样,strftime("%m")命令将提取月份的字符串。要查找前两条和最后两条月度回报,可以使用.head().tail()函数;请参见以下代码:

>>>retMonthly.head(2)
>>>
retMonthly
201501   -0.046737
201502    0.043930
>>>
>>>retMonthly.tail(2)
>>>
retMonthly
201511    0.015798
201512   -0.026248
>>>

同理,以下代码将日回报转化为年回报:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd
ticker='IBM'
begdate=(1980,1,1)
enddate=(2012,12,31)
x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
logret = np.log(x.aclose[1:]/x.aclose[:-1])

date=[]
d0=x.date
for i in range(0,np.size(logret)):
      date.append(d0[i].strftime("%Y"))
#
y=pd.DataFrame(logret,date,columns=['retAnnual'])
ret_annual=exp(y.groupby(y.index).sum())-1

这里显示了几条年度回报数据:

>>>ret_annual[0:5]
retAnnual
1980  0.167561
1981 -0.105577
1982  0.679136
1983  0.352488
1984  0.028644
>>>
>>>ret_annual.tail(2)
>>>
retAnnual
2011   0.284586
2012   0.045489
>>>

在金融中,标准差和方差用于衡量风险。为了判断哪只股票风险较高,可以比较它们的方差或标准差。以下程序测试了 IBM 和微软的方差是否相等:

import scipy as sp
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
begdate=(2013,1,1)
enddate=(2015,12,31)
def ret_f(ticker,begdate,enddate):
    p = getData(ticker,begdate,
enddate,asobject=True,adjusted=True)
    return(p.aclose[1:]/p.aclose[:-1]-1)
y=ret_f('IBM',begdate,enddate)
x=ret_f('MSFT',begdate,enddate)

scipy.stats模块中调用的函数bartlett()被使用。以下输出结果表明这两家公司具有不同的方差,因为 F 值为 44.39,而 P 值几乎为零:

>>>print(sp.stats.bartlett(x,y))
BartlettResult(statistic=44.392308291526497, pvalue=2.6874090005526671e-11)

要获取更多关于此函数的信息,可以使用help()函数。

为节省空间,这里只显示了前几行:

  1. scipy.stats.morestats模块中bartlett函数的帮助:

    bartlett(*args)
    
  2. 执行 Bartlett 检验以检查方差是否相等。

    注意

    Bartlett 检验的零假设是所有输入样本来自方差相等的总体。

    对于来自显著非正态分布的样本,Levene 检验(levene)更为稳健。

在金融中,我们有一个非常重要的假设:股票回报遵循正态分布。因此,最好通过图形展示股票回报的分布;请参见以下图片。附录 A 中的代码相对复杂。本章并不要求理解该程序。对几个描述的程序来说也是如此。

以下图表展示了 IBM 回报的分布情况,并与正态分布进行对比。价格时刻点显示在右侧,相关的 Python 程序可以在附录 A 中找到:

从 Yahoo!Finance 获取数据

所谓的蜡烛图可以生动地展示股票价格或交易量,如以下截图所示。相应的 Python 程序可以在附录 C 中找到:

从 Yahoo!Finance 获取数据

右上角的图片非常复杂。由于初学者不需要理解它,本书没有包括该程序。如果读者感兴趣,完整的程序可以在两个位置找到。以下是链接:matplotlib.org/examples/pylab_examples/finance_work2.htmlcanisius.edu/~yany/python/finance_work2.txt

以下是另一个示例,通过调用pandas_datareader.data子模块中的DataReader()函数,从 Yahoo! Finance 获取 IBM 的每日数据:

>>>import pandas_datareader.data as getData
>>>x = getData.DataReader('IBM', data_source='yahoo', start='2004/1/30')
>>>x[1:5]
                  Open        High        Low       Close   Volume  Adj Close
Date                                                                         
2004-02-02   99.150002   99.940002  98.500000   99.389999  6200000  77.666352
2004-02-03   99.000000  100.000000  98.949997  100.000000  5604300  78.143024
2004-02-04   99.379997  100.430000  99.300003  100.190002  8387500  78.291498
2004-02-05  100.000000  100.089996  98.260002   98.860001  5975000  77.252194
>>>

从 Google 财经获取数据

和 Yahoo Finance 一样,Google 财经提供大量公共信息,如新闻、期权链、相关公司(有助于竞争对手和行业分析)、历史价格和财务数据(包括利润表、资产负债表和现金流量表)。我们可以通过直接访问 Google 财经手动下载数据。或者,为了从 Google 财经获取数据,可以使用pandas_datareader子模块中的DataReader()函数:

>>>import pandas_datareader.data as getData
>>>aapl =getData.DataReader("AAPL", "google") 
>>>aapl.head(2)
>>>
             Open   High    Low  Close     Volume
Date                                             
2010-01-04  30.49  30.64  30.34  30.57  123432050
2010-01-05  30.66  30.80  30.46  30.63  150476004
>>>aapl.tail(2)
              Open    High     Low   Close    Volume
Date                                                
2016-12-08  110.86  112.43  110.60  112.12  27068316
2016-12-09  112.31  114.70  112.31  113.95  34402627
>>>

以下截图显示了股票的日内波动。相关的 Python 程序包括在附录 C 中:

从 Google 财经获取数据

从 FRED 获取数据

美国联邦储备银行有许多与当前经济和历史时间序列相关的数据集。例如,他们有关于利率的数据,如欧元-美元存款利率。获取此类利率数据有两种方式。首先,我们可以使用他们的数据下载程序,如以下步骤所示:

  1. 访问美国联邦储备银行的网页链接:www.federalreserve.gov/econresdata/default.html

  2. 点击www.federalreserve.gov/data.htm上的数据下载程序

  3. 选择合适的数据项。

  4. 点击前往下载

例如,我们选择联邦基金利率。前几行如下所示:

"Series Description","Federal funds effective rate"
"Unit:","Percent:_Per_Year"
"Multiplier:","1"
"Currency:","NA"
"Unique Identifier: ","H15/H15/RIFSPFF_N.D"
"Time Period","RIFSPFF_N.D"
1954-07-01,1.13
1954-07-02,1.25
1954-07-03,1.25
1954-07-04,1.25
1954-07-05,0.88
1954-07-06,0.25
1954-07-07,1.00
1954-07-08,1.25

以下程序可用于获取下载的数据。这里假设数据集保存在c:/temp/目录下:

import pandas as pd
importnumpy as np
file=open("c:/temp/fedFundRate.csv","r")
data=pd.read_csv(file,skiprows=6)

另外,可以使用pandas_datareader模块中的DataReader()函数。这里给出了一个示例:

>>>import pandas_datareader.data as getData
>>>vix = DataReader("VIXCLS", "fred")
>>>vis.head()
VIXCLS
DATE              
2010-01-01     NaN
2010-01-04   20.04
2010-01-05   19.35
2010-01-06   19.16
2010-01-07   19.06
>>>

从法兰西教授的数据库获取数据

French 教授拥有一个非常好且广泛使用的数据库。你可以通过访问这个链接:mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html获取更多信息。它包含了 Fama-French 因子的日度、周度和月度数据以及其他有用的数据集。点击Fama-French 因子后,可以下载名为F-F_Research_Data_Factors.zip的 ZIP 文件。解压后,我们将得到一个名为F_F_Research_Data_Factors.txt的文本文件,里面包括了从 1926 年 7 月开始的月度和年度 Fama-French 因子。以下是前几行的内容。更多细节请参见第七章,多因子模型与绩效衡量,夏普比率、特雷诺比率和詹森α。

该文件是由CMPT_ME_BEME_RETS使用201012 CRSP数据库创建的:

The 1-month TBill return is from Ibbotson and Associates, Inc.
Mkt-RFSMBHMLRF
192607    2.62   -2.16   -2.92    0.22
192608    2.56   -1.49    4.88    0.25
192609    0.36   -1.38   -0.01    0.23
192610   -3.43    0.04    0.71    0.32
192611    2.44   -0.24   -0.31    0.31

假设数据保存在C:/temp/下。在运行以下代码之前,请记得删除文件底部的年度数据:

>>>import pandas as pd
>>>file=open("c:/temp/ffMonthly.txt","r")
>>>data=file.readlines()

这里展示了前 10 条观测值:

>>>data[0:10]
['DATE    MKT_RFSMBHMLRF\n', '192607    2.96   -2.30   -2.87    0.22\n', '192608    2.64   -1.40    4.19    0.25\n', '192609    0.36   -1.32    0.01    0.23\n', '192610   -3.24    0.04    0.51    0.32\n', '192611    2.53   -0.20   -0.35    0.31\n', '192612    2.62   -0.04   -0.02    0.28\n', '192701   -0.06   -0.56    4.83    0.25\n', '192702    4.18   -0.10    3.17    0.26\n', '192703    0.13   -1.60   -2.67    0.30\n']
>>>

另外,我们也可以编写一个 Python 程序来获取 Fama-French 月度时间序列:

import pandas_datareader.data as getData
ff =getData.DataReader("F-F_Research_Data_Factors", "famafrench")

再次使用pandas_datareader()模块的优点在于,我们可以利用.head().tail()函数来查看获取到的数据集。现在给出几个更多的示例:

import pandas_datareader.data as pdata
ff2=web.DataReader("F-F_Research_Data_Factors_weekly", "famafrench")
ff3 =web.DataReader("6_Portfolios_2x3", "famafrench")
ff4=web.DataReader("F-F_ST_Reversal_Factor", "famafrench")

从人口普查局、财政部和劳工统计局获取数据

在这一部分,我们简要展示了如何从美国人口普查局获取数据。你可以在www.census.gov/compendia/statab/hist_stats.html了解更多信息。当我们进入人口普查的历史数据后,以下窗口将弹出。这是链接:www.census.gov/econ/census/data/historical_data.html。以下截图展示了我们可以下载的历史数据类型:

从人口普查局、财政部和劳工统计局获取数据

假设我们对61 教育服务感兴趣。点击该链接后,我们可以选择一个时间序列进行下载。点击下载图标后,将会下载一个包含四个文件的 ZIP 文件。

下一个示例展示了如何从劳工统计局网站获取数据。首先,访问相关网页:www.bls.gov/,然后点击菜单栏上的数据工具

从人口普查局、财政部和劳工统计局获取数据

点击通货膨胀与价格,再点击CPI;我们将被引导到一个页面,在那里我们可以下载相关的数据集,如这个链接所示:download.bls.gov/pub/time.series/cu/

生成二十多个数据集

为了帮助本书的读者,生成了许多数据集。首先,让我们看一个简单的示例,下载并加载一个名为ffMonthly.pkl的 Python 数据集。有关该数据集的更多信息,请访问以下链接:canisius.edu/~yany/python/ffMonthly.pkl

该数据集基于每月的 Fama-French 三因子时间序列生成。假设数据集保存在c:/temp/目录下,可以使用以下 Python 程序加载它:

>>>import pandas as pd
>>>ff=pd.read_pickle("c:/temp/ffMonthly.pkl")

我们可以查看前几行和最后几行:

>>>import pandas as pd
>>>ff=pd.read_pickle("c:/temp/ffMonthly.pkl")

更好的方式是使用.head().tail()函数;请参见以下代码:

>>>import pandas as pd
>>>ff=pd.read_pickle("c:/temp/ffMonthly.pkl")
>>>ff.head(5)
DATE  MKT_RFSMBHMLRF
1  1926-10-01 -0.0324  0.0004  0.0051  0.0032
2  1926-11-01  0.0253  -0.002 -0.0035  0.0031
3  1926-12-01  0.0262 -0.0004 -0.0002  0.0028
4  1927-01-01 -0.0006 -0.0056  0.0483  0.0025
5  1927-02-01  0.0418  -0.001  0.0317  0.0026
>>>ff.tail(3)
DATE  MKT_RFSMBHMLRF
1078  2016-07-01  0.0395   0.029 -0.0098  0.0002
1079  2016-08-01  0.0049  0.0094  0.0318  0.0002
1080  2016-09-01  0.0025    0.02 -0.0134  0.0002
>>>

ff.head(5)命令将显示前五行,而ff.tail(3)将显示最后三行。date变量对于时间序列至关重要,主要原因是我们处理的是时间序列数据。在合并不同的数据集时,date变量是最常用的合并变量。以下示例展示了如何定义这种date变量:

>>>import pandas as pd
>>>from datetime import timedelta
>>>a=pd.to_datetime('12/2/2016', format='%m/%d/%Y')
>>>a+timedelta(40)
>>>
Timestamp('2017-01-11 00:00:00')
>>> b=a+timedelta(40)
>>>b.date()
datetime.date(2017, 1, 11)

为了帮助本书的读者,作者生成了大约二十个 Python 数据集,扩展名为.pkl。这些数据集来自前面提到的公共数据源,例如来自 Fama 教授的数据集和 Hasbrouck 教授的 TORQ 数据集,后者包含了 1990 年 11 月至 1991 年 1 月期间,144 只纽约证券交易所(NYSE)股票的交易、报价、订单处理数据和审计跟踪数据。为了方便下载,提供了一个名为loadYan.py的 Python 程序。你可以在以下链接找到更多信息:caniisus.edu/~yany/loadYan.py

运行程序后,可以输入help(loadYan)来查看所有已生成的数据集;请参见以下代码:

>>>help(loadYan)
Help on function loadYan in module __main__:

loadYan(i, loc='c:/temp/temp.pkl')
    Objective: download datasets with an extension of .pkl
i     : an integer 
loc   : a temporary location, such as c:/temp/temp.pkl

i  dataset           description 
     --- -------            ------------------
1  ffMonthlyFama-French 3 factors monthly 
2  ffDailyFama-French 3 factors daily 
3  ffMonthly5Fama-French 5 factors monthly 
4  ffDaily5Fama-French 5 factors daily 
5  sp500listsCurrent S&P 500 constituents 
6  tradingDaysMonthly trading days monthly 
7  tradingDaysDaily   trading days daily 
8  usGDPannual        US GDP annual 
9  usGDPmonthly       US GDP monthly 
10  usCPI              US Consumer Price Index
11  dollarIndex        US dollar index
12  goldPriceMonthly   gold price monthly 
13  goldPriceDaily     gold price daily 
14  spreadAAA          Moody's spread for AAA rated bonds
15  spreadBBB          Moody's spread for BBB rated bonds
16  spreadCCC          Moody's spread for CCC rated bonds
17  TORQctTORQ Consolidated Trade 
18  TORQcqTORQ Consolidated Quote  
19  TORQcodTORQ Consolidated Order 
20  DTAQibmCTTAQ Consolidated Trade for IBM (one day)
21  DTAQibmCQDTAQ Consolidated Quote for IBM (one day)
22  DTAQ50CTDTAQ Consolidated Trade for 50  (one day)
23  DTAQ50CQDTAQ Consolidated Quote for 50  (one day)
24  spreadCredit   Spreads based on credit ratings
25journalRankings  A list of journals

    Example 1:
>>>x=loadYan(1)
>>>x.head(2)
DATE  MKT_RFSMBHMLRF
1  1926-10-01 -0.0324  0.0004  0.0051  0.0032
2  1926-11-01  0.0253  -0.002 -0.0035  0.0031

>>>x.tail(2)
DATE  MKT_RFSMBHMLRF
1079  2016-08-01  0.0049  0.0094  0.0318  0.0002
1080  2016-09-01  0.0025    0.02 -0.0134  0.0002
>>>

与 CRSP 和 Compustat 相关的几个数据集

证券价格研究中心CRSP)包含所有交易数据,例如收盘价、交易量、流通股数等,从 1926 年起涵盖美国所有上市股票。由于其数据质量高且历史悠久,它已被学术研究者和实践者广泛使用。该数据库由芝加哥大学生成并维护,访问地址为:www.crsp.com/。生成了大约 100 个 Python 数据集,见下表:

名称 描述
crspInfo.pkl 包含 PERMNO、CUSIP 头、股票交易所以及起始和结束交易日期
stockMonthly.pkl 每月股票文件,包含 PERMNO、日期、收益、价格、交易量和流通股数
indexMonthly.pkl 每月频率的指数文件
indexDaily.pkl 每日频率的指数文件
tradingDaysMonthly.pkl 1926 年到 2015 年 12 月 31 日的每月交易日数据
tradingDaysDaily.pkl 1926 年到 2015 年 12 月 31 日的每日交易日数据
sp500add.pkl 标准普尔 500 成分股,即每只股票何时被加入指数以及何时从指数中移除
sp500daily.pkl 标准普尔 500 日常指数水平及回报
sp500monthly.pkl 标准普尔 500 月度指数水平及回报
d1925.pkl 1925 年每日股票价格文件
d1926.pkl 1926 年每日股票价格文件
[更多内容,请见 1926 年至 2014 年之间的数据]
d2014.pkl 2014 年每日股票价格文件
d2015.pkl 2015 年每日股票价格文件

表 4.2:与 CRSP 相关的 Python 数据集列表

加载数据非常简单,可以使用 pandas.read_pickle() 函数:

>>>import pandas as pd
>>>crspInfo=pd.read_pickle("c:/temp/crspInfo.pkl")

要查看前几个和后几个观测值,可以使用 .head().tail() 函数:

>>>crspInfo.shape
     (31218, 8)
>>>crspInfo.head()
PERMNOPERMCOCUSIP                         NAME TICKER  EX   BEGDATE  \
0   10001    7953  6720410               AS NATURAL INCEGAS   2  19860131   
1   10002    7954  5978R10ANCTRUST FINANCIAL GROUP IN   BTFG   3  19860131   
2   10003    7957  9031810REAT COUNTRY BKASONIA CT   GCBK   3  19860131   
3   10005    7961  5815510ESTERN ENERGY RESOURCES INCWERC   3  19860131   
4   10006   22156  0080010           C F INDUSTRIES INCACF   1  19251231   
ENDDATE
0  20151231
1  20130228
2  19951229
3  19910731
4  19840629
>>>crspInfo.tail(3)
PERMNOPERMCOCUSIP                  NAME TICKER  EX   BEGDATE  \
31215   93434   53427  8513510& W SEED CO   SANW   3  20100630   
31216   93435   53452  2936G20INO CLEAN ENERGY INCSCEI   3  20100630   
31217   93436   53453  8160R10ESLA MOTORS INCTSLA   3  20100630   
ENDDATE
31215  20151231
31216  20120531
31217  20151231>>>

PERMNO 是 CRSP 的股票 IDPERMCO 是公司 ID,Name 是公司的当前名称,Ticker 是股票代码,也就是当前的股票符号,EX 是交易所代码(1 表示纽约证券交易所,2 表示美国证券交易所,3 表示纳斯达克),BEGDATE 是首次交易日,而 ENDDATE 是某个给定 PERMNO 的最后交易日。对于 pandas 模块,选择列是通过将列名的列表传递给我们的 DataFrame 来完成的。

例如,要选择 PERMNOBEGDATEENDDATE 三列,我们可以使用以下代码:

>>>myColumn=['PERMNO','BEGDATE','ENDDATE']
>>>crspInfo[myColumn].head(6)
>>>
PERMNOBEGDATEENDDATE
0   10001  19860131  20151231
1   10002  19860131  20130228
2   10003  19860131  19951229
3   10005  19860131  19910731
4   10006  19251231  19840629
5   10007  19860131  19901031
>>>

Compustat(CapitalIQ) 数据库提供了自 1960 年以来美国上市公司如资产负债表、利润表和现金流量表等财务报表。该数据库由标准普尔公司生成。你可以在 marketintelligence.spglobal.com/our-capabilities/our-capabilities.html?product=compustat-research-insight 找到更多信息。以下表格列出了几个相关的 Python 数据集:

Name 描述
compInfo.pkl 所有公司关键头文件
varDefinitions.pkl 数据集中的所有变量定义
deletionCodes.pkl 显示某公司何时从数据库中删除以及原因
acc1950.pkl 1950 年年度财务报表
acc1951.pkl 1951 年年度财务报表
acc2014.pkl 2014 年年度财务报表
acc2015.pkl 2015 年年度财务报表

表 4.3:与 Compustat 相关的 Python 数据集列表

请注意,由于 CRSP 和 Compustat 都是专有数据库,相关数据集不会在作者的网站上提供。如果教师对这些数据感兴趣,请直接联系作者。以下是一些高频数据集的列表:

Name 描述
TORQct.pkl TORQ 数据库的合并交易
TORQcq.pkl TORQ 数据库的合并报价
TORQcod.pkl TORQ 数据库的 COD
DTAQibmCT DTAQ 代表每日交易与报价,是毫秒级的交易数据,提供 IBM 的一天数据
DTAQibmCQ IBM 的单日数据,综合报价
DTAQ50CT 50 只股票的单日数据(综合交易)
DTAQ50CQ 50 只股票的一日数据(综合报价)

表 4.4:与高频交易数据相关的 Python 数据集列表

假设TORQcq.pkl文件保存在c:/temp/目录下。我们可以查看它的前几条和后几条观测数据:

>>>import pandas as pd
>>>x=pd.read_pickle("c:/temp/TORQcq.pkl")
>>>x.head()
>>>
  SYMBOL      DATE      TIME     BID     OFRBIDSIZOFRSIZ  MODE  QSEQ EX
0     AC  19901101   9:30:44  12.875  13.125      32       5    10  1586  N
1     AC  19901101   9:30:47  12.750  13.250       1       1    12     0  M
2     AC  19901101   9:30:51  12.750  13.250       1       1    12     0  B
3     AC  19901101   9:30:52  12.750  13.250       1       1    12     0  X
4     AC  19901101  10:40:13  12.750  13.125       2       2    12     0  
>>>x.tail()
        SYMBOL      DATE      TIME     BID     OFRBIDSIZOFRSIZ  MODE  \
1111220    ZNT  19910131  13:31:06  12.375  12.875       1       1    12   
1111221    ZNT  19910131  13:31:06  12.375  12.875       1       1    12   
1111222    ZNT  19910131  16:08:44  12.500  12.750       1       1     3   
1111223    ZNT  19910131  16:08:49  12.375  12.875       1       1    12   
1111224    ZNT  19910131  16:16:54  12.375  12.875       1       1     3   
QSEQ EX  
1111220       0  B
1111221       0  X
1111222  237893  N  
1111223       0  X
1111224       0  X
>>>M

以下表格显示了不同格式(如 SAS、Matlab 和 Excel)数据的获取示例:

格式 代码
>>>import pandas as pd
CSV >>>a=pd.read_csv("c:/temp/ffMonthly.csv",skip=4)
文本 >>>b=pd.read_table("c:/temp/ffMonthly.txt",skip=4)
Pickle >>>c=pd.read_pickle("c:/temp/ffMonthly.pkl")
SAS >>>d= sp.read_sas('c:/temp/ffMonthly.sas7bdat')
Matlab >>>import scipy.io as sio``>>>e= sio.loadmat('c:/temp/ffMonthly.mat')
Excel >>>infile=pd.ExcelFile("c:/temp/ffMonthly.xlsx")``>>>f=infile.parse("ffMonthly",header=T)

表 4.5:使用不同格式获取数据

为了帮助本章的读者,前述表格中的所有输入文件都可以获取。有关更多信息,请参见此链接:canisius.edu/~yany/ffMonthly.zip

注意

参考文献

Kane, David, 2006,《开源金融》,工作论文,哈佛大学,SSRN 链接见papers.ssrn.com/sol3/papers.cfm?abstract_id=966354

附录 A – Python 程序用于回报分布与正态分布的比较

from matplotlib.pyplot import *
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import matplotlib.mlab as mlab

ticker='IBM'
begdate=(2015,1,1) 
enddate=(2015,11,9)
p = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
ret = (p.aclose[1:] - p.aclose[:-1])/p.aclose[:1] 
[n,bins,patches] = hist(ret, 100)
mu = np.mean(ret) 
sigma = np.std(ret)
x = mlab.normpdf(bins, mu, sigma) 
plot(bins, x, color='red', lw=2) 
title("IBM return distribution") 
xlabel("Returns") 
ylabel("Frequency")
show()

相应的图表如下所示:

附录 A – Python 程序用于回报分布与正态分布的比较

附录 B – Python 程序绘制蜡烛图

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter, WeekdayLocator
from matplotlib.dates import HourLocator,DayLocator, MONDAY
from matplotlib.finance import candlestick_ohlc,plot_day_summary_oclh
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
date1 = ( 2013, 10, 20)
date2 = ( 2013, 11, 10 )
ticker='IBM'
mondays = WeekdayLocator(MONDAY)       # major ticks on the mondays
alldays = DayLocator()                 # minor ticks on the days
weekFormatter = DateFormatter('%b %d') # e.g., Jan 12
dayFormatter = DateFormatter('%d')     # e.g., 12
quotes = getData(ticker, date1, date2)
if len(quotes) == 0:
     raiseSystemExit
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.2)
ax.xaxis.set_major_locator(mondays)
ax.xaxis.set_minor_locator(alldays)
ax.xaxis.set_major_formatter(weekFormatter)
ax.xaxis.set_minor_formatter(dayFormatter)
plot_day_summary_oclh(ax, quotes, ticksize=3)
candlestick_ohlc(ax, quotes, width=0.6)
ax.xaxis_date()
ax.autoscale_view()
plt.setp(plt.gca().get_xticklabels(), rotation=80,horizontalalignment='right')
plt.figtext(0.35,0.45, '10/29: Open, High, Low, Close')
plt.figtext(0.35,0.42, ' 177.62, 182.32, 177.50, 182.12')
plt.figtext(0.35,0.32, 'Black ==> Close > Open ')
plt.figtext(0.35,0.28, 'Red ==> Close < Open ')
plt.title('Candlesticks for IBM from 10/20/2013 to 11/10/2013')
plt.ylabel('Price')
plt.xlabel('Date')
plt.show()

图像如下所示:

附录 B – Python 程序绘制蜡烛图

附录 C – Python 程序用于价格波动

import datetime
import matplotlib.pyplot as plt
from matplotlib.finance import quotes_historical_yahoo_ochl
from matplotlib.dates import MonthLocator,DateFormatter
ticker='AAPL'
begdate= datetime.date( 2012, 1, 2 )
enddate = datetime.date( 2013, 12,4)

months= MonthLocator(range(1,13), bymonthday=1, interval=3)# 3rd month
monthsFmt = DateFormatter("%b '%Y")
x = quotes_historical_yahoo_ochl(ticker, begdate, enddate) 
if len(x) == 0:
     print ('Found no quotes')
     raiseSystemExit
dates = [q[0] for q in x] 
closes = [q[4] for q in x] 
fig, ax = plt.subplots()
ax.plot_date(dates, closes, '-') 
ax.xaxis.set_major_locator(months) 
ax.xaxis.set_major_formatter(monthsFmt)
ax.autoscale_view()
ax.grid(True)
fig.autofmt_xdate()
plt.show()

相应的图表如下:

附录 C – Python 程序用于价格波动

附录 D – Python 程序展示股票的日内波动图

import numpy as np
import pandas as pd
import datetime as datetime
import matplotlib.pyplot as plt
ticker='AAPL'
path='http://www.google.com/finance/getprices?q=ttt&i=60&p=1d&f=d,o,h,l,c,v'
p=np.array(pd.read_csv(path.replace('ttt',ticker),skiprows=7,header=None))
#
date=[]
for i in np.arange(0,len(p)): 
    if p[i][0][0]=='a':
        t= datetime.datetime.fromtimestamp(int(p[i][0].replace('a',''))) 
        date.append(t)
    else:
        date.append(t+datetime.timedelta(minutes =int(p[i][0])))
#
final=pd.DataFrame(p,index=date) 
final.columns=['a','Open','High','Low','Close','Vol'] 
del final['a']
#
x=final.index
y=final.Close
#
plt.title('Intraday price pattern for ttt'.replace('ttt',ticker)) 
plt.xlabel('Price of stock')
plt.ylabel('Intro-day price pattern') 
plt.plot(x,y)	
plt.show()

相应的图表如下所示:

附录 D – Python 程序展示股票的日内波动图

附录 E – pandas DataFrame 的属性

首先,让我们从canisius.edu/~yany/python/ffMonthly.pickle下载一个名为ffMonthly.pickl e的 Python 数据集。假设该数据集保存在c:/temp目录下:

>>>
>>>import pandas as pd
>>>ff=pd.read_pickle("c:/temp/ffMonthly.pickle")
>>>type(ff)
<class'pandas.core.frame.DataFrame'>
>>>

最后的结果显示,ff数据集的类型是 pandas DataFrame。因此,获取更多有关此数据类型的信息可能是个好主意。当我们输入ff.时,可以看到一个下拉列表;请参见以下截图:

附录 E – pandas DataFrame 的属性

我们可以找到一个名为hist()的函数;请参见以下代码中的使用方法:

>>>import pandas as pd
>>>infile=("c:/temp/ffMonthly.pickle")
>>>ff=pd.read_pickle(infile)
>>>ff.hist()

附录 E – pandas DataFrame 的属性

欲了解更多详情,请参见相关链接:pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html

附录 F – 如何生成扩展名为 .pkl 或 .pickle 的 Python 数据集

首先,让我们看一下最简单的数据集:

>>>import pandas as pd
>>>import numpy.ranom  as random
>>>x=random.randn(10)
>>>y=pd.DataFrame(x)
>>>y.to_pickle("c:/temp/test.pkl")

读取扩展名为 .pkl.pickle 的 Python 数据集时,我们使用 pd.read_pickle() 函数:

>>> import pandas as pd
>>>kk=pd.read_pickle("c:/temp/test.pkl")

接下来,展示用于生成 ffMonthly.pkl 数据集的 Python 程序:

import pandas as pd
import numpy as np
file=open("c:/temp/ffMonthly.txt","r")
data=file.readlines()
dd=mkt=smb=hml=rf=[]
n=len(data)
index=range(1,n-3)
#
for i in range(4,n):
     t=data[i].split()
     dd.append(pd.to_datetime(t[0]+'01', format='%Y%m%d').date())
     mkt.append(float(t[1])/100)
     smb.append(float(t[2])/100)
     hml.append(float(t[3])/100)
      rf.append(float(t[4])/100)
#
d=np.transpose([dd,mkt,smb,hml,rf])
ff=pd.DataFrame(d,index=index,columns=['DATE','MKT_RF','SMB','HML','RF'])
ff.to_pickle("c:/temp/ffMonthly.pkl")

以下是第一个和最后几个观测值:

>>>ff.head(2)
DATE  MKT_RFSMBHML
1  1926-10-01 -0.0324  0.0004  0.0051
2  1926-11-01  0.0253  -0.002 -0.0035
>>>ff.tail(2)
DATE  MKT_RFSMBHML
1079  2016-08-01  0.0049  0.0094  0.0318
1080  2016-09-01  0.0025    0.02 -0.0134

附录 G – 数据案例 #1 - 生成多个 Python 数据集

对于此数据案例,学生需要生成大约五个 Python 数据集,扩展名为 .pkl

>>import pandas as pd
>>>a = pd.Series(['12/1/2014', '1/1/2015'])
>>>b= pd.to_datetime(a, format='%m/%d/%Y')
>>>b
0   2014-12-01
1   2015-01-01
dtype: datetime64[ns]
>>>

请使用 Python 格式生成以下数据集,格式为 .pickle.pkl.pickle):

# 数据集名称 描述
1 ffDaily 每日 Fama 和 French 3 因子时间序列
2 ffMonthly5 每月 Fama 和 French 5 因子时间序列
3 usGDPannual 美国年度 GDP(国内生产总值)
4 usGDPquarterly 美国季度 GDP(国内生产总值)
5 dollarIndex 美元指数
6 goldPriceMonthly 每月黄金价格
7 goldPriceDaily 每日黄金价格
8 tradingDaysMonthly 每月时间序列的交易日
9 tradingDaysDaily 每日数据的交易日
10 spreadAAA 穆迪 AAA 评级债券的利差

练习题

  1. 我们可以从哪里获取每日股票价格数据?

  2. 我们能否直接下载收益率数据?

  3. 手动下载 CitiGroup 的每月和每日价格数据。

  4. 将 CitiGroup 的每日价格数据转换为每日收益率。

  5. 将每月价格转换为每月收益率,并将每日收益率转换为每月收益率。它们相同吗?

  6. 以下两行代码是否等效?

    >>>ret = p.aclose[1:]/p.aclose[:-1]-1     
    >>>ret = (p.aclose[1:]-p.aclose[:-1]/p.aclose[1:]
    
  7. 使用公共股票数据与使用私有股票数据(例如来自某些金融数据库)的优缺点是什么?

  8. 查找订阅Compustat的年费用,涉及会计信息,以及与交易数据相关的 CRSP。

  9. 从 Yahoo Finance 下载 IBM 的每月数据。估算其从 2000 年 1 月到 2004 年 12 月的标准差和夏普比率。

  10. 2001 到 2010 年间,IBM、DELL 和 MSFT 的年 Beta 值是多少?

  11. 2006 到 2010 年间,IBM 和 DELL 之间的相关性是多少?

  12. 估算 IBM 的平均工作日收益率。你是否观察到工作日效应?

  13. 波动性是否随着年份的推移而下降?例如,你可以选择 IBM、DELL 和 MSFT 来研究这个假设。

  14. S&P500 与 DJI(道琼斯工业平均指数)之间的相关性是多少?注意:在 Yahoo Finance 中,S&P500 指数的代码是 ^GSPCDJI 的代码是 ^DJI

  15. 如何下载 n 个给定股票代码的数据?

  16. 编写 R 程序以从输入文件中输入 n 个股票代码。

  17. 美国股市(S&P500)与香港市场(恒生指数)之间的相关系数是多少?

  18. 新加坡股市是否与日本股市的相关性比与美国股市的相关性更强?

  19. 如何下载 50 只股票的每日价格数据并将其保存到一个文本文件中?

  20. 从 Yahoo!Finance 下载数据后,假设p向量包含所有的每日价格数据。以下两行代码的含义是什么?我们应该在什么时候应用它们?

    >>> ret = p.aclose[1:]/p.aclose[:-1]-1     
    >>> ret = p.aclose[:-1]/p.aclose[1:]-1    
    

总结

在本章中,我们讨论了经济学、金融和会计学的各种公共数据来源。对于经济学,我们可以访问联邦储备银行的数据库、French 教授的数据库,获取许多有用的时间序列数据。对于金融学,我们可以使用 Yahoo!Finance 和 Google Finance 下载历史价格数据。对于会计信息,比如最新几年的资产负债表和利润表,我们可以使用 Yahoo!Finance、Google Finance 和 SEC 的文件。下一章,我们将解释与利率相关的许多概念,之后我们会解释如何定价债券和股票。

第五章:债券与股票估值

债券或固定收益证券与股票是两种广泛使用的投资工具。因此,它们值得进行详细讨论。在讨论债券或股票估值之前,我们必须讨论利率及其相关概念,如年百分比率APR)、有效年利率EAR)、复利频率、如何将一种有效利率转换为另一种、利率期限结构、如何估算普通债券的售出价格、如何使用所谓的折现股息模型估算股票价格等。特别是,本章将涵盖以下主题:

  • 利率介绍

  • 各种有效利率之间的转换,APR

  • 利率期限结构

  • 债券估值与到期收益率(YTM)

  • 信用评级与违约利差

  • 久期和修正久期的定义

  • 股票估值、总回报、资本利得收益率和股息收益率

  • 一种新的数据类型——字典

利率介绍

毫无疑问,利率在我们的经济中起着重要作用。当经济扩张时,由于资本需求的增加,利率往往会上升,从而推高借贷利率。此外,通货膨胀也可能上升。在这种情况下,中央银行将尽力将通货膨胀控制在适当的水平。应对潜在通胀上涨的一项工具是提高银行的贷款利率。另一方面,债券价格与利率呈负相关关系。

很可能,本书的许多读者对单利与复利之间的区别感到困惑。单利不考虑利息的利息,而复利则会考虑这一点。假设我们今天借款 1,000 美元,借款期限为 10 年。如果年利率为 8%,那么每年末的未来价值将是多少?假设这一年利率既是单利利率,也是复利利率。它们的对应公式如下:

利率介绍利率介绍

这里,PV 是今天的贷款额,R 是周期利率,n 是周期数。下图展示了本金、单利情况下的未来价值以及复利情况下的未来价值。相关的 Python 程序在附录 A中。顶部红色线(复利情况下的未来价值)与中间线(单利情况下的未来价值)之间的差异即为利息的利息:

利率介绍

在第三章,时间价值中,我们已经学习了时间价值的概念。我们用同样的简单例子来开始。

今天,100 美元存入银行,年利率为 10%。一年后它会是多少?我们知道它将变成 110 美元。100 美元是我们的本金,而 10 美元是利息支付。或者,可以应用以下公式:

利率介绍

在这里,FV是未来值,PV是现值,R是每期有效利率,n是期数。结果是:100(1+0.1)=110。与第三章中讨论的货币的时间价值相比,细心的读者会发现,R在这里被定义为有效期利率,而不是期利率。添加了有效这个关键词。在之前的章节中,所有公式中的R,比如FV(一个PV),PV(一个FV),PV(年金),PV(到期年金),PV(增长年金),FV(年金),FV(到期年金)和FV(增长年金),这些公式中的R*实际上是有效利率。在这里,我们解释了这一重要概念。

首先,让我们看看估算给定年百分比率APR)和复利频率(m)的有效利率的常用方法:

利率介绍

在这里,利率介绍是关于特定周期(由m表示)的有效期利率,APR是年百分比率,m是复利频率。m的值可以是 1(年复利),2(半年复利),4(季复利),12(月复利),365(日复利)。如果 APR 为 10%,并且是半年复利,则有效半年利率为 5%(=0.10/2)。另一方面,如果 APR 为 0.08,且是季复利,则有效季利率为 2%(=0.08/4)。

这里有一个与房屋按揭相关的例子。John Doe 打算在纽约布法罗购买一套价格为 240,000 美元的房子。他计划支付房屋价格的 20%作为首付,其余部分向 M&T 银行借款。对于 30 年的按揭贷款,银行提供的年利率为 4.25%。他的每月按揭还款额是多少?如第三章中所讨论的,货币的时间价值,可以在这里使用scipy.pmt()函数:

>>> import scipy as sp
>>>sp.pmt(0.045/12,30*12,240000*0.8)
-972.83579486570068

在上述代码中,有效月利率为 0.045/12。之所以这样计算,是因为假设复利频率为月复利,因为这是一个有规律月还款的按揭贷款。基于这个结果,John 每月需要支付 972.84 美元。

要比较两个具有不同复利频率的利率,我们必须先将它们转换为相同的利率,然后才能进行比较。一种这样的有效利率被称为有效年利率EAR)。对于给定的 APR 和复利频率m,其EAR可以通过以下公式计算:

利率介绍

假设某公司计划借款 1000 万美元用于长期投资项目。A 银行提供年利率 8%,按半年复利,而 B 银行提供年利率 7.9%,按季度复利。对于公司而言,哪个借款利率更便宜?通过应用前面的公式,我们得出以下结果。由于 8.137%低于 8.160%,所以 B 银行的报价更优惠:

>>> (1+0.08/2)**2-1
0.08160000000000012
>>> (1+0.079/4)**4-1
0.08137134208625363

显然,我们可以有其他基准。例如,我们知道 A 银行提供的有效半年利率是 4%(=0.08/2)。那么我们会问:B 银行的等效有效季度利率是多少?换句话说,我们比较两个有效的半年利率。为了将一个有效利率转换为另一个,我们引入了所谓的两步法

  1. 给定的有效利率是多少?为了解答这个问题,我们只需应用公式(4)。金融机构以这种方式报价,并没有背后的合理性。假设年利率为 10%,按半年复利。有效半年利率为 5%,即0.1/2=0.05。如果 APR 为 8%,按月复利,那么有效月利率为 0.833%,即0.08/12=0.006666667

  2. 如何将一个给定的有效利率转换为另一个目标有效利率?如果给定的有效半年利率为 5%,那么等效的有效季度利率是多少?我们绘制了一年的时间线,并设定两种频率。上方是给定的有效利率及其对应的复利频率。在这种情况下,5%和 2 期(Rsemi=5%和 n1=2):利率介绍

在底部,我们有我们打算估算的有效利率及其对应的频率(Rn2=4)。然后,我们通过使用不同输入值两次PV=1,应用未来公式!利率介绍:

利率介绍

设其相等,即!利率介绍 通过求解R,我们得到R=(1+0.05)**(2/4)-1。结果如下所示:

>>> (1+0.05)**(2/4)-1
  0.02469508

有效季度利率为 2.469508%。这种方法的优点是我们不需要记住其他公式,除了FV=PV(1+R)n。顺便提一句,这一步与第一步之间没有任何联系。

或者,我们可以直接应用某些公式。在这里,我们展示了如何推导出两个公式:从APRRm和从APR1APR2。两个年利率之间的公式APR1(m1)APR2(m2)如下所示:

利率介绍

这里,APR1APR2)是第一个(第二个)APR 年利率,而m1m2)是其对应的年复利频率。根据前面的公式,我们有以下公式来计算给定 APR(APR1)及其对应频率(m1)下,在新的复利频率(m2)下的有效利率:

利率介绍

对于同一个例子,假设一家银行提供 10%的年利率,半年复利。那么它的等效有效季度利率是多少?通过应用公式(7)并设置输入值APR1=0.10,m1=2,和m2=4,参见以下代码:

>>> (1+0.10/2)**(2/4)-1
>>>
0.02469507659595993

我们得到的结果与 2 步法得到的结果相同。实际上,我们可以基于公式(7)编写一个简单的 Python 函数,参见以下代码:

def APR2Rm(APR1,m1,m2):
        return (1+APR1/m1)**(m1/m2)-1

调用这个函数很简单,正如我们在下面的代码中看到的那样:

>>> APR2Rm(0.1,2,4)
      0.02469507659595993
>>> APR2Rm(0.08,2,12)
0.008164846051901042

加入一些注释,例如这三个输入的定义,估算目标有效利率的公式,再加上一些例子,程序将更加清晰,参见以下代码:

def APR2Rm(APR1,m1,m2):
"""

目标:将一个 APR 转换为另一个有效利率Rm

         APR1: annual percentage rate
           m1: compounding frequency for APR1
           m2: effective period rate of our target effective rate

使用的公式:Rm=(1+APR1/m1)**(m1/m2)-1

    Example #1>>>APR2Rm(0.1,2,4)
                0.02469507659595993
"""
    return (1+APR1/m1)**(m1/m2)-1

为了获得给定 APR 及其对应频率的第二个 APR(APR2),我们有以下公式:

利率介绍

通过应用公式(8),我们得到了 APR2 的结果:

>>>Rs=(1+0.05/2)**(2/12)-1
>>>Rs*2
0.008247830930288469
>>>

对应的 Python 程序行数如下所示。为了节省空间,程序没有额外的解释或注释:

def APR2APR(APR1,m1,m2):
    return m2*((1+APR1/m1)**(m1/m2)-1)

对于连续复利利率,可以采用不同的方法来解释这个困惑的概念。首先,我们通过增加* m*的复利频率,应用有效年利率EAR)公式:

利率介绍

例如,如果 APR 为 10%,且按半年复利,则 EAR 为 10.25%:

>>> (1+0.1/2)**2-1
>>>
0.10250000000000004

由于这个函数相当简单,我们可以改用 Python 函数来编写,参见以下程序:

def EAR_f(APR,m):
    return (1+APR/m)**m-1

接下来,假设 APR 为 10%,我们增加复利频率,参见以下程序:

import numpy as np
d=365
h=d*24
m=h*60
s=m*60
ms=s*1000
x=np.array([1,2,4,12,d,h,m,s,ms])
APR=0.1
for i in x:
    print(EAR_f(APR,i))

以下是输出图像:

利率介绍

实际上,当复利频率接近无穷大时,极限将是我们的连续复利率,其公式为EAR=exp(Rc)-1,参见以下代码:

>>>exp(0.1)-1
  0.10517091807564771

解释连续复利率公式的第二种方法是记住另一种计算一个现值现金流未来价值的方法。在第三章,货币的时间价值中,我们有以下公式来计算给定现值的未来价值:

利率介绍

这里,FV是未来价值,PV是现值,R是有效期利率,n是期数。计算一个现值的未来价值的另一种方法是使用连续复利率,Rc。其公式如下:

利率介绍

这里,Rc是连续复利率,T是计算未来价值的时间(以年为单位)。如果我们选择 1 年作为T,并将$1 作为PV,使得前面两个公式相等,则得到如下公式:

利率介绍

注意,Rm=APR/m 来自 方程(4)。然后求解上述方程得到 Rc。最后,对于给定的 APR 和 m(复利频率),我们有以下公式来估算 Rc

利率介绍

在这里,log() 是自然对数函数。假设年利率(APR)为 2.34%,按半年复利计算。它的等效利率 Rc 是多少?

>>>from math import log
>>>2*log(1+0.0234/2)
0.023264168459415393

或者,我们可以基于前述公式编写一个 2 行的 Python 函数,将 APR 转换为 Rc:

def APR2Rc(APR,m):
       return m*log(1+APR/m)

输出结果如下:

>>> APR2Rc(0.0234,2)
0.023264168459415393

同样,对于给定的 Rc,我们有以下公式来计算其对应的 APR

利率介绍

相关的 Python 函数如下所示:

def Rc2APR(Rc,m):
       return m*(exp(Rc/m)-1)

输出结果如下所示:

>>> Rc2APR(0.02,2)
0.020100334168335898

对于有效期利率,我们有以下方程:

利率介绍

函数和示例如下代码所示:

def Rc2Rm(Rc,m):
       return exp(Rc/m)-1

输出结果如下所示:

>>> Rc2Rm(0.02,2)
0.010050167084167949

在这里,取款$100 的类比与有效利率的概念进行了比较。假设我们去银行取款$100。以下七种组合是等效的:

纸币面额 纸币数量
100 1
50 2
20 5
10 10
5 20
2 50
1 100

表 5.1 提取$100 的纸币面额和数量

现在,让我们看看与不同的 APR 和复利频率(m)组合相关的有效利率的类似情况。APR 为 10%,按半年复利计算。以下 11 个利率是等效的,其中NA表示不可用:

利率报价 M
年利率为 10%,按半年复利计算 2
年利率为 10.25%,按年复利计算 1
年利率为 9.87803063838397%,按季度复利计算 4
年利率为 9.79781526228125%,按月复利计算 12
年利率为 9.75933732280154%,按日复利计算 365
年有效利率为 0.1025 NA
半年有效利率为 0.05 NA
季度有效利率为 0.0246950765959599 NA
每月有效利率为 0.00816484605190104 NA
每日有效利率为 0.000267379104734289 NA
持续复利率为 0.0975803283388641 NA

表 5.2 即使 APR 和复利频率不同,它们也是等效的

让我们来看一个不同的类比。玛丽的月薪是$5,000。这样,她的年薪就是 $60,000 (=50,000 * 12)。这是我们通常的月薪与年薪计算方式。现在,我们来做一个简单的改变。公司告诉玛丽,她将会在年底一次性拿到所有薪水。与此同时,她可以从公司财务部借到原本的月薪,公司会承担相关费用。从字面上看,这两种情况没有区别。假设月利率为 0.25%。这意味着在 1 月,玛丽会借 5,000 美元,借期为 11 个月,因为她将在年底偿还。这对于 2 月及其他月份同样适用。回顾第三章,货币的时间价值,这代表了年金的未来价值。对于这个情况,可以使用 scipy.fv() 函数:

>>> import scipy as sp
>>>sp.fv(0.0025,12,5000,0)
>>>
-60831.913827013472

结果表明,每月领取 5,000 美元共 12 个月与年底一次性领取$60,831.91 是等价的。显然,与原来的$60,000 年薪相比,额外的$831.91 是用于支付利息。

利率期限结构

利率期限结构被定义为无风险利率与时间之间的关系。无风险利率通常定义为无违约的国债利率。从许多来源中,我们可以获得当前的利率期限结构。例如,在 2016 年 12 月 21 日,从 Yahoo!财经 finance.yahoo.com/bonds,我们可以获得以下信息。

绘制的利率期限结构可能更具吸引力;请见以下图片:

利率期限结构

根据前面图片提供的信息,我们有如下代码来绘制所谓的收益率曲线:

from matplotlib.pyplot import *
time=[3/12,6/12,2,3,5,10,30]
rate=[0.47,0.6,1.18,1.53,2,2.53,3.12]
title("Term Structure of Interest Rate ")
xlabel("Time ")
ylabel("Risk-free rate (%)")
plot(time,rate)
show()

相关图表如下:

利率期限结构

向上倾斜的利率期限结构意味着长期利率高于短期利率。由于利率期限结构有很多缺失的数值,可以使用 pandas 模块中的 .interpolate() 函数来进行插值,见下例,其中我们在 26 之间有两个缺失值:

>>>import pandas as pd
>>>import numpy as np
>>>x=pd.Series([1,2,np.nan,np.nan,6])
>>>x.interpolate()

相关输出如下:

>>>
01.000000
12.000000
23.333333
34.666667
46.000000

我们可以手动计算这些缺失的值。首先,估算一个Δ:

利率期限结构

这里,Δ是 v2(结束值)与 v1(起始值)之间的增量值,n 是这两个值之间的间隔数量。上述案例中的Δ是 (6-2)/3=1.33333。因此,下一个值将是 v1+Δ=2+1.33333=3.33333

对于前面的例子,涉及到利率期限结构,在第 6 年到第 9 年之间没有数据。代码和输出如下:

>>> import pandas as pd
>>> import numpy as np
>>> nan=np.nan
>>> x=pd.Series([2,nan,nan,nan,nan,2.53])
>>>x.interpolate()

输出如下:

>>>
0    2.000
1    2.106
2    2.212
3    2.318
4    2.424
5    2.530
dtype: float64
>>>

利率期限结构非常重要,因为它作为一个基准,用于估算公司债券的到期收益率YTM)。到期收益率是指如果债券持有人持有至债券到期的期间回报。从技术上讲,到期收益率与内部收益率IRR)是相同的。在金融行业中,利差定义为公司债券的到期收益率与无风险利率之间的差异,用于估算公司债券的折现率。利差是衡量违约风险的一个指标。因此,它应该与公司的信用评级和债券的信用评级密切相关。

因此,一个名为spreadBasedOnCreditRating.pkl的 Python 数据集被用来解释违约利差与信用评级之间的关系。该数据集可以从作者的网页下载:canisius.edu/~yany/python/spreadBasedOnCreditRating.pkl。以下程序用于检索并打印数据。假设数据集位于c:/temp/目录中:

>>>import pandas as pd
>>>spread=pd.read_pickle("c:/temp/spreadBasedOnCreditRating.pkl")
>>> spread
                   1       2       3       5       7      10     30 
Rating                                                                
Aaa/AAA          5.00    8.00   12.00   18.00   28.00   42.00   65.00
Aa1/AA+         10.00   18.00   25.00   34.00   42.00   54.00   77.00
Aa2/AA          14.00   29.00   38.00   50.00   57.00   65.00   89.00
Aa3/AA-         19.00   34.00   43.00   54.00   61.00   69.00   92.00
A1/A+           23.00   39.00   47.00   58.00   65.00   72.00   95.00
A2/A            24.00   39.00   49.00   61.00   69.00   77.00  103.00
A3/A-           32.00   49.00   59.00   72.00   80.00   89.00  117.00
Baa1/BBB+       38.00   61.00   75.00   92.00  103.00  115.00  151.00
Baa2/BBB        47.00   75.00   89.00  107.00  119.00  132.00  170.00
Baa3/BBB-       83.00  108.00  122.00  140.00  152.00  165.00  204.00
Ba1/BB+        157.00  182.00  198.00  217.00  232.00  248.00  286.00
Ba2/BB         231.00  256.00  274.00  295.00  312.00  330.00  367.00
Ba3/BB-        305.00  330.00  350.00  372.00  392.00  413.00  449.00
B1/B+          378.00  404.00  426.00  450.00  472.00  495.00  530.00
B2/B           452.00  478.00  502.00  527.00  552.00  578.00  612.00
B3/B-          526.00  552.00  578.00  604.00  632.00  660.00  693.00
Caa/CCC+       600.00  626.00  653.00  682.00  712.00  743.00  775.00
Treasury-Yield  0.13    0.45    0.93    1.74    2.31    2.73  3.55
>>>

指数列是基于穆迪和标准普尔信用评级标准的信用评级。除了最后一行“美国国债收益率”外,数据集中的所有值的单位是基点,每个基点等于 0.01%。换句话说,每个值应该被除以 100 两次。例如,对于一个AA评级的债券,其在第 5 年的利差是 50 个基点,即0.005 (=50/10000)。如果一个 5 年期零息债券的无风险利率是 2%,那么一个公司债券(AA 评级)的相应利率将是2.5% (2.5%+ 0.5%)

久期是风险分析和对冲中非常重要的概念。久期的定义是:收回初始投资所需的年数。让我们看一个简单的例子:一个零息债券。今天,我们购买一个 1 年期零息债券。一年后,我们将收到其面值 100 美元。其时间表和现金流如下所示:

利率期限结构

显然,我们需要等待一年才能收回最初的投资。因此,这个 1 年期债券的久期是 1。对于零息债券,债券的久期与其到期时间相同:

利率期限结构

这里,D是久期,T是零息债券的到期时间(以年为单位)。让我们看第二个例子,其中我们将在前两年末各收到 100 美元的现金流:

利率期限结构

我们需要等待多少年才能收回初始投资?事实上,我们需要等待 1 年才能收到第一个 100 美元,并等待 2 年才能收到第二个 100 美元。因此,第一次猜测可能是 1.5 年。然而,阅读了第三章后,货币的时间价值,我们知道第 2 年收到的 100 美元与第 1 年收到的 100 美元是不等价的。如果以第 1 年末作为我们的基准,第二个 100 美元的等价值如下所示:

>>> 100/(1+0.05)
95.23809523809524

现在,我们可以说我们需要等待 1 年才能收到 100 美元,并等待 2 年才能收到 95.24 美元。平均而言,我们需要等待多少年?解决方案应该是加权平均。两个 100 美元的权重如下所示:

> pv2<-100/(1+0.05)
>w1=100/(100+pv2)
>>>w1
 0.5121951
>>>w2= pv2/(100+pv2)
>>>w2
 0.4878049
>>>w1*1 + w2*2
    1.487281

最后,我们得到D=w1T1+w2T2=w11+w22=0.51221 + 0.4878052=1.487。答案是我们需要等待 1.487 年才能收回初始投资。在上述推导中,我们将第二个 100 美元折现到第 1 年末,以获得我们的答案。

或者,我们可以将第一个 100 美元复利到第 2 年末,然后进行比较,请参见以下代码:

>>>fv=100*(1+0.05)
>>>fv
   105

对应的权重如下所示:

> w1=105/(100+105)
> w1
[1] 0.5121951
> w2=100/(100+105)
> w2
[1] 0.4878049
>

解决方案应该与之前相同,因为权重与之前相同。这表明我们可以使用任何时间点来估算那些在不同时间点发生的现金流的权重。通常,现值被用作基准,请参见以下代码:

>>> pv1=100/(1+0.05)
>>> pv2=100/(1+0.05)**2
>>>w1= pv1/(pv1+pv2)
>>>w1
0.5121951219512195
>>>1-w1
0.4878048780487805

再次,两个权重保持不变。使用现值作为基准的另一个优点是我们也可以估算总现值。总现值如下所示。我们可以说,如果今天投资 185.94 美元,我们将在第 1 年回收 51.2%,其余部分将在第 2 年末回收。因此,平均而言,我们需要等待 1.487 年:

> pv1+pv2
[1] 185.941

用于估算n个未来现金流的持续时间的一般公式如下所示:

利率期限结构

D是持续时间,n是现金流的数量,wi是第 i 个现金流的权重,wi定义为第 i 个现金流的现值与所有现金流现值之比,Ti是第 i 个现金流的时间点(以年为单位)。在这里,写了一个名为duration的 Python 函数:

def duration(t,cash_flow,y):
    n=len(t)
B,D=0,0
for i in range(n):
        B+=cash_flow[i]*exp(-y*t[i])
for i in range(n):
        D+=t[i]*cash_flow[i]*exp(-y*t[i])/B
    return D

如果我们添加一个标题,程序将更具帮助性,请参见以下代码:

def duration(t,cash_flow,y):
    n=len(t)
    B=0     # B is the bond's present value
    for i in range(n):
        B+=cash_flow[i]*exp(-y*t[i])

    D=0     # D is the duration
    for i in range(n):
        D+=t[i]*cash_flow[i]*exp(-y*t[i])/B
    return D

债券评估

债券也被称为固定收益证券。根据不同的分类方式,债券可以分为短期、中期和长期。对于美国国债,T-bills 是由财政部发行的、到期时间少于 1 年的证券;T-notes 是期限超过 1 年但少于 10 年的政府债券;T-bonds 是到期时间超过 10 年的国债。根据票息支付方式,债券可分为零息债券和票息债券。当债券为中央政府债券时,我们称其为无风险债券,因为中央政府通常拥有印钞的权力,也就是默认的,风险为零。

如果债券持有人可以在到期前将债券转换为预定数量的股票,这种债券称为可转换债券。如果债券发行人可以在债券到期前赎回或回购债券,则称为可赎回债券。另一方面,如果债券购买者可以在到期前将债券卖回给原发行人,则称为可回售债券。零息债券的现金流如下所示:

债券估值

这里,FV 是面值,n 是到期时间(以年为单位)。为了估算这种零息债券的价格,我们可以很容易地应用单一未来现金流的现值。换句话说,我们可以使用 scipy.pv() 函数。

对于票息债券,我们期望定期获得一系列的票息支付。定期的票息支付可以通过以下公式估算:

债券估值

这里,FV 是债券的面值,frequency 是每年支付票息的次数。我们来看一个 3 年期的票息债券。其面值为 100 美元,年票息率为 8%。票息支付为年度支付。接下来的三年中,每年的票息支付为 8 美元,投资者在到期时还将收到 100 美元的面值。这只票息债券的时间线和相关未来现金流如下所示:

债券估值

回顾一下,对于单一未来现金流的现值和年金的现值,我们有以下两个公式:

债券估值

这里,C 是一个固定的现金流,n 是支付期数。票息债券的价格是这两种类型支付的组合:

债券估值

scipy.pv() 函数可以用来计算债券的价格。假设年有效利率为 2.4%:

>>> import scipy as sp
>>>sp.pv(0.024,3,0.08*100,100)
-116.02473258972169

根据上述结果,这只 3 年期的票息债券价格为 116.02 美元。

由于债券价格是其所有未来现金流的现值,因此债券价格应与贴现率呈负相关关系。换句话说,如果利率上升,债券价格将下降,反之亦然。

到期收益率YTM)与国际回报率IRR)是相同的概念。假设我们以 717.25 美元购买了一只零息债券,债券的面值为 1000 美元,且将在 10 年后到期。它的 YTM 是多少?对于零息债券,我们有以下 YTM 公式:

债券评估

这里,FV是面值,PV是零息债券的价格,n是年限(到期时间)。通过应用该公式,我们得到717.25(1+YTM)¹⁰=1000*。因此,我们得到以下结果:

>>> (1000/717.25)**(1/10)-1
>>>
0.033791469771228044

假设我们今天以 825 美元购买了一只债券,债券的到期时间为 5 年,票息率为 3%,并且每年支付一次利息。如果面值为 1000 美元,YTM 是多少?可以使用scipy.rate()函数来估算 YTM:

>>> import scipy as sp
>>> sp.rate(5,0.03*1000,-818,1000)
0.074981804314870726

根据这个结果,YTM 为 7.498%。债券价格、票息率和面值之间的关系如下表所示:

条件 债券价格与面值的关系 溢价、按面值和折价
票息率 > YTM 债券价格 > 面值 溢价
票息率 = YTM 债券价格 = 面值 按面值发行
票息率 < YTM 债券价格 < 面值 折价

表 5.3:债券价格、票息率和面值之间的关系

显然,对于两只零息债券,期限越长,风险越大。原因是,对于长期的零息债券,我们需要等待更长时间才能收回最初的投资。而对于相同期限的附息债券,票息率越高,债券越安全,因为我们可以更早地收到更多的利息支付。不同期限的零息债券和附息债券又会怎样呢?

这里有一个例子,我们有一只 15 年期零息债券,面值为 100 美元,和一只 30 年期附息债券。票息率为 9%,每年支付一次利息。那么哪只债券风险更大呢?如果当前收益率从 4%跃升至 5%,两者的百分比变化是多少?当收益率波动时,风险更大的债券将会有更大的百分比变化:

# for zero-coupon bond
>> p0=sp.pv(0.04,15,0,-100)
>>> p1=sp.pv(0.05,15,0,-100)
>>> (p1-p0)/p0
-0.1337153811552842

相关输出如下所示:

>>> p0
>>> 55.526450271327484
>>> p1
48.101709809096995

对于附息债券,我们得到了以下结果:

>>> p0
>>> p0=sp.pv(0.04,30,-0.09*100,-100)
>>> p1=sp.pv(0.05,30,-0.09*100,-100)
>>> (p1-p0)/p0
>>>
    -0.13391794539315816
>>> p0
    186.46016650332245
>>> p1
    161.48980410753134

根据以上结果,30 年期附息债券的风险高于 15 年期零息债券,因为前者的百分比变化更大。对于 15 年期零息债券,它的久期为 15 年。那么,前述的 30 年期附息债券如何呢?以下结果显示其久期为 17 年。请注意,p4f是作者编写的一组 Python 程序:

>>>import p4f
>>>p4f.durationBond(0.04,0.09,30)
>>>
17.036402239014734

请注意,为了使用名为p4f的模型,本书读者可以在canisius.edu/~yany/python/p4f.cpython-35.pyc下载该程序。债券价格的百分比变化与 YTM 变化之间的关系如下:

债券评估

这里,B是债券价格,ΔB 是债券价格的变化,y是到期收益率(YTM),m是相应的复利频率。修改久期在此定义:

债券评估债券评估

对于银行来说,存款通常是短期的,而贷款(放贷)通常是长期的。因此,银行面临利率风险。一种对冲策略叫做久期匹配,即将负债的久期与资产的久期匹配。

股票估值

有几种方法可以估算股票价格。一种方法叫做股息折现模型。其逻辑是,今天的股票价格仅仅是所有未来股息的现值总和。我们用最简单的单期模型来说明。我们预期明年末会有$1 的股息,且预计卖出价格为$50。如果适当的股本成本为 12%,那么今天的股票价格是多少?时间线和未来现金流如下所示:

股票估值

股票的价格仅仅是这两个未来现金流的现值,$45.54:

>> (1+50)/(1+0.12)
>>>
     45.535714285714285
>>> import scipy as sp
>>>sp.pv(0.12,1,1+50)
     -45.53571428571432

让我们来看一个两期模型。我们预期在接下来的两年内分别会有$1.5 和$2 的股息。此外,预计卖出价格为$78。那么今天的价格是多少?

股票估值

假设对于这只股票,适当的折现率为 14%。那么股票的现值为$62.87:

>>>1.5/(1+0.14)+(2+78)/(1+0.14)**2
62.873191751308084

同样地,如果给定现值和未来值,我们也可以估算股本成本。如果当前价格是$30,并且预期一年后的卖出价格是$35:

股票估值

然后我们可以估算总回报率:

>>> (35-30+1)/30
0.2

总回报率,股本成本(Re),由两个部分组成:资本增值收益和股息收益:

股票估值

资本增值收益率为 16.667%,而股息收益率为 3.333%。另一种可能的情况是,股票可能享有一个恒定的股息增长率。公司 A 预计明年支付$4 股息,并且其之后的股息增长率为 2%。如果股本成本为 18%,那么今天的股票价格是多少?从第三章,货币的时间价值,我们知道可以应用增长永续年金的现值公式:

股票估值

通过使用正确的符号,即将P0表示为今天的股票价格,d1表示第一个预期股息,我们可以得到以下等价的定价公式:

股票估值

从以下结果中,我们知道今天的价格应该是$25:

>>> 4/(0.18-0.02)
>>>
25.0

许多年轻的小公司在成立初期不会发放股息,因为它们可能非常需要资金。在成功的阶段后,这些公司可能会享有超常的增长。之后,这些公司通常会进入长期的正常增长阶段。对于这种情况,我们可以应用 n 期模型。对于 n 期模型,我们有n+1个未来现金流:n 个股息加上 1 个售价。因此,我们可以得到 n 期模型的一般公式:

股票估值

第 n 期结束时的售价如下所示:

股票估值

让我们用一个例子来说明如何应用这个 n 期模型。假设一家公司去年发放了 1.5 美元的股息。未来 5 年,股息将享有 20%、15%、10%、9%和 8%的增长率。之后,增长率将降至 3%的长期增长率,永远不变。如果这种类型的股票的回报率为 18.2%,那么今天的股票价格是多少?以下表格显示了时间期数和增长率:

期数=> 1 2 3 4 5 6
增长率 0.2 0.15 0.1 0.09 0.08 0.04

作为我们的第一步,应该问一下 n 期模型需要多少期数?经验法则是股息享有长期增长率的年份减去 1 期。在此情况下,我们可以选择 5:

期数=> 1 2 3 4 5 6
增长率 0.2 0.15 0.1 0.09 0.08 0.04
股息 1.80 2.07 2.277 2.48193 2.680 2.7877

第一个股息 1.8 来自于1.5(1+0.2)*。为了解决这个问题,我们有以下代码:

>>>import scipy as sp
>>>dividends=[1.80,2.07,2.277,2.48193,2.680,2.7877]
>>>R=0.182
>>>g=0.03
>>>sp.npv(R,dividends[:-1])*(1+R)
>>>
9.5233173204508681
>>>sp.pv(R,5,0,2.7877/(R-g))
>>>
-7.949046992374841

在前面的代码中,我们去掉了最后的现金流,因为它用于计算 P5 的售价。由于scipy.npv()将第一个现金流视为发生在零时刻,因此我们必须通过将其乘以(1+R)来调整结果。计算五个未来股息的现值,并与出售价格的现值计算分开,是为了提醒读者所谓的 Excel 符号约定的存在。股票价格为17.47(=9.52+7.95)。另外,我们也可以使用p4f.pvPriceNperiodModel()函数,见以下代码。Python 程序包含在附录 D中:

>>>import p4f
>>> r=0.182
>>> g=0.03
>>> d=[1.8,2.07,2.277,2.48193,2.68,2.7877]
>>> p4f.pvValueNperiodModel(r,g,d)
          17.472364312825711

前面的模型依赖于一个重要假设,即股份数量是恒定的。因此,如果公司利用部分收益回购股份,这一假设就被破坏了。因此,我们不能使用股息折现模型。对于这种情况,我们可以应用所谓的股票回购和总支付模型。公式如下。首先计算公司的所有股权现值,而不是单一股权:

股票估值

逻辑解决方案公司预计年末的总收益约为 4 亿美元。该公司计划支付 45%的总收益:其中 30%用于分红,15%用于股票回购。如果公司的长期增长率为 3%,股本成本为 18%,且流通股本为 5000 万股,那么今天的股价是多少?解决方案如下:

>>> 400*0.45/(0.18-0.03)/50
>>>
24.0

第三种方法是估算公司的总价值,即企业价值。然后我们估算股本的总价值。最后,我们将股本的总价值除以流通股本数量,得出股价。企业价值在这里定义:

股票估值

这里,Equity 是股本的市场价值,Debt 是债务的总账面价值,Cash 是现金持有量。企业价值可以视为我们收购整个公司所需的总资本。我们来看一个简单的例子。假设一家公司的市场价值为 600 万美元,总债务为 400 万美元,现金持有量为 100 万美元。似乎投资者需要 1000 万美元才能收购整家公司,因为她需要 600 万美元购买所有的股份,并承担 400 万美元的债务。然而,实际上,由于新所有者可以使用 100 万美元的现金,她只需要筹集 900 万美元。获得企业价值后,下面的公式用于计算每股价格:

股票估值

这里 V0 是企业价值,Debt 是当前的债务,Cash 是当前的现金。V0 可以视为公司由股东和债权人(债券持有者)共同拥有的总价值:

股票估值

自由现金流在时间 t 的定义为:

股票估值

FCFt 是第 t 年的自由现金流,NIt 是第 t 年的净收入,Dt 是第 t 年的折旧,CapExt 是第 t 年的资本支出,且 股票估值 是第 t 年净营运资金的变动。净营运资金是流动资产和流动负债之间的差额。由此得出的公式如下:

股票估值

WACC 是加权平均资本成本。原因是我们估算的是整个公司的总价值,因此使用股本成本作为折现率并不合适:

股票估值

……………(31)

其中 We (Re) 是股本的权重(成本),Wd (Rd) 是债务的权重(税前成本),Tc 是公司税率。由于 Re 是税后股本成本,我们必须通过乘以 (1-Tc) 将 Rd(税前股本成本)转换为税后债务成本。Vn 可以视为整个公司的售价:

股票估值

估算当前股票价格的另一种方法是基于某些倍数,例如行业市盈率。该方法简单明了。假设某公司明年的预期每股收益为 $4。如果行业平均市盈率为 10,那么今天的股票价格是多少?今天的股票价格是 $40。

一种新数据类型 – 字典

字典是无序数据集,通过键访问,而不是通过位置访问。字典是一种关联数组(也称为哈希)。字典中的任何键都与一个值相关联(或映射)。第一个变量是 key,第二个变量是 value;请参见以下示例。使用花括号表示。第二个值可以是任何数据类型,如字符串、整数或实数:

>>>houseHold={"father":"John","mother":"Mary","daughter":"Jane"}
>>> household
{'father': 'John', 'daughter': 'Jane','mother': 'Mary'}
>>> type(houseHold)
<class 'dict'>
>>>houseHold['father']
'John'

附录 A – 单利率与复利率的对比

单利率的支付公式如下:

附录 A – 单利率与复利率的对比

复利的未来价值公式如下:

附录 A – 单利率与复利率的对比

这里,PV 是现值,R 是周期利率,n 是期数。因此,这两个未来的值将分别为 $1,800 和 $2,158.93。

以下程序提供了本金、单利支付和未来价值的图形表示:

import numpy as np 
from matplotlib.pyplot import * 
from pylab import * 
pv=1000 
r=0.08 
n=10  
t=linspace(0,n,n) 
y1=np.ones(len(t))*pv # a horizontal line 
y2=pv*(1+r*t) 
y3=pv*(1+r)**t 
title('Simple vs. compounded interest rates') 
xlabel('Number of years') 
ylabel('Values') 
xlim(0,11) 
ylim(800,2200) 
plot(t, y1, 'b-') 
plot(t, y2, 'g--') 
plot(t, y3, 'r-') 
show()

相关图表如下所示:

附录 A – 单利率与复利率的对比

在上述程序中,xlim() 函数用于设置 x 轴的范围。ylim() 函数也适用。xlim()ylim() 函数的第三个输入变量是颜色和线条设置。字母 b 代表黑色,g 代表绿色,r 代表红色。

附录 B – 与利息转换相关的几个 Python 函数

def APR2Rm(APR1,m1,m2):
"""
    Objective: convert one APR to another Rm
         APR1: annual percentage rate
           m1:  compounding frequency 
           m2:  effective period rate with this compounding

    Formula used: Rm=(1+APR1/m1)**(m1/m2)-1

    Example #1>>>APR2Rm(0.1,2,4)
                0.02469507659595993
"""
    return (1+APR/m1)**(m1/m2)-1

def APR2APR(APR1,m1,m2):
"""
    Objective: convert one APR to another Rm
         APR1: annual percentage rate
           m1:  compounding frequency 
           m2:  effective period rate with this compounding

    Formula used: Rm=(1+APR1/m1)**(m1/m2)-1

    Example #1>>>APR2APR(0.1,2,4)
                0.09878030638383972
"""
   return m2*((1+APR/m1)**(m1/m2)-1)

def APR2Rc(APR,m):
    return m*log(1+APR/m)

def Rc2Rm(Rc,m):
       return exp(Rc/m)-1

def Rc2APR(Rc,m):
       return m*(exp(Rc/m)-1)

附录 C – rateYan.py 的 Python 程序

def rateYan(APR,type):
"""Objective: from one APR to another effective rate and APR2
         APR : value of the given Annual Percentage Rate
        type : Converting method, e.g., 's2a', 's2q', 's2c'
's2a' means from semi-annual to annual
a for annual
                 s for semi-annual
                 q for quarterly
                 m for monthly
                 d for daily
                 c for continuously
    Example #1>>>rateYan(0.1,'s2a')
                [0.10250000000000004, 0.10250000000000004]
    Example #2>>>rateYan(0.1,'q2c')
                   0.098770450361485657
"""
    import scipy as sp
    rate=[]
    if(type[0]=='a'):
        n1=1
elif(type[0]=='s'):
        n1=2
elif(type[0]=='q'):
        n1=4
elif(type[0]=='m'):
        n1=12
elif(type[0]=='d'):
        n1=365
    else:        
        n1=-9
    if(type[2]=='a'):
        n2=1
elif(type[2]=='s'):
        n2=2
elif(type[2]=='q'):
        n2=4
elif(type[2]=='m'):
        n2=12
elif(type[2]=='d'):
        n2=365
    else:        
        n2=-9       
    if(n1==-9 and n2==-9):
        return APR           
elif(n1==-9 and not(n2==-9)):
effectiveRate=sp.exp(APR/n2)-1
        APR2=n2*effectiveRate
rate.append(effectiveRate)
rate.append(APR2)
        return rate        
elif(n2==-9 and not(n1==-9)):
Rc=n1*sp.log(1+APR/n1)
        return Rc
    else:
effectiveRate=(1+APR/n1)**(n1/n2)-1
        APR2=n2*effectiveRate
rate.append(effectiveRate)
rate.append(APR2)
        return rate   

附录 D – 基于 n 期模型估算股票价格的 Python 程序

对于 n 期模型,我们有 n+1 个未来现金流:n 个股息加一个卖出价格:

附录 D – 基于 n 期模型估算股票价格的 Python 程序

在 n 期结束时的卖出价格如下所示:

附录 D – 基于 n 期模型估算股票价格的 Python 程序

请参见以下代码,用于估算从今天起第一笔现金流 n+1 的成长型永续年金现值:

def pvValueNperiodModel(r,longTermGrowthRate,dividendNplus1):
"""Objective: estimate stock price based on an n-period model
                         r: discount rate 
LongTermGrowhRate: long term dividend growth rate
         dividendsNpus1   : a dividend vector n + 1

         PV    = d1/(1+R) + d2/(1+R)**2 + .... + dn/(1+R)**n + 
sellingPrice/(1+R)**n
sellingPrice= d(n+1)/(r-g)
             where g is long term growth rate

    Example #1: >>> r=0.182
>>> g=0.03
>>> d=[1.8,2.07,2.277,2.48193,2.68,2.7877]
>>>pvValueNperiodModel(r,g,d)
                   17.472364312825711
"""
    import scipy as sp
    d=dividendNplus1
    n=len(d)-1
    g=longTermGrowthRate
pv=sp.npv(r,d[:-1])*(1+r)
sellingPrice=d[n]/(r-g)
pv+=sp.pv(r,n,0,-sellingPrice)
    return pv

附录 E – 估算债券久期的 Python 程序

def durationBond(rate,couponRate,maturity):
"""Objective : estimte the durtion for a given bond
       rate      : discount rate
couponRate: coupon rate 
      maturity   : number of years 

       Example 1: >>>discountRate=0.1
>>>couponRate=0.04
>>> n=4
>>>durationBond(rate,couponRate,n)
                      3.5616941835365492

       Example #2>>>durationBond(0.1,0.04,4)
                     3.7465335177625576                   
"""
    import scipy as sp
    d=0
    n=maturity
    for i in sp.arange(n):
        d+=(i+1)*sp.pv(rate,i+1,0,-couponRate)
    d+=n*sp.pv(rate,nper,0,-1)
    return d/sp.pv(rate,n,-couponRate,-1)

附录 F – 数据案例 #2 – 从新发行的债券筹集的资金

目前,您正在 国际商业机器公司(IBM)担任金融分析师。该公司计划在美国发行总面值为 6000 万美元的 30 年期公司债券。每张债券的面值为 1000 美元。年利率为 3.5%。公司计划每年支付一次利息,支付时间为每年的年末。请回答以下三个问题:

  1. 如果你的公司发行 30 年期债券,今天能够获得多少资金?

  2. 这只债券的到期收益率(YTM)是多少?

  3. 如果你的公司成功提高其信用评级一个等级,你的公司还能获得多少额外的资金?

债券的价格是其所有未来现金流的折现和:

附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

找出每个未来现金流的适当折现率:

附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

这里,Ri 是第 i 年的折现率,Rf,i 是无风险利率,来自政府国债的利率结构(收益曲线)第 i 年的利率,而 Si 是信用利差,取决于你公司 的信用评级。利差基于 Python 数据集 calledspreadBasedOnCreditRating.pkl。该 Python 数据集可在以下网站找到:canisius.edu/~yany/python/spreadBasedOnCreditRating.pkl

>>>import pandas as pd
>>>spread=pd.read_pickle("c:/temp/spreadBasedOnCreditRating.pkl")
>>> spread
                     1       2       3       5       7      10     30 
Rating                                                         
Aaa/AAA          5.00    8.00   12.00   18.00   28.00   42.00   65.00
Aa1/AA+         10.00   18.00   25.00   34.00   42.00   54.00   77.00
Aa2/AA          14.00   29.00   38.00   50.00   57.00   65.00   89.00
Aa3/AA-         19.00   34.00   43.00   54.00   61.00   69.00   92.00
A1/A+           23.00   39.00   47.00   58.00   65.00   72.00   95.00
A2/A            24.00   39.00   49.00   61.00   69.00   77.00  103.00
A3/A-           32.00   49.00   59.00   72.00   80.00   89.00  117.00
Baa1/BBB+       38.00   61.00   75.00   92.00  103.00  115.00  151.00
Baa2/BBB        47.00   75.00   89.00  107.00  119.00  132.00  170.00
Baa3/BBB-       83.00  108.00  122.00  140.00  152.00  165.00  204.00
Ba1/BB+        157.00  182.00  198.00  217.00  232.00  248.00  286.00
Ba2/BB         231.00  256.00  274.00  295.00  312.00  330.00  367.00
Ba3/BB-        305.00  330.00  350.00  372.00  392.00  413.00  449.00
B1/B+          378.00  404.00  426.00  450.00  472.00  495.00  530.00
B2/B           452.00  478.00  502.00  527.00  552.00  578.00  612.00
B3/B-          526.00  552.00  578.00  604.00  632.00  660.00  693.00
Caa/CCC+       600.00  626.00  653.00  682.00  712.00  743.00  775.00
US Treasury Yield  0.13    0.45    0.93    1.74    2.31    2.73  3.55
>>>

对于第 5 年和双 AA 评级,利差为 55 个基点。每个基点是 1% 的 1/100。换句话说,我们应该将 55 除以 100 两次,即 55/10000=0.0055

线性插值的过程如下所示:

  1. 首先,让我用一个简单的例子。假设 5 年期债券的到期收益率为 5%,10 年期债券的到期收益率为 10%。那么 6、7、8、9 年期债券的到期收益率是多少?

  2. 一个快速答案是,6 年期债券的收益率为 6%,7 年期债券为 7%,8 年期债券为 8%,9 年期债券为 9%。基本思路是增量值相等。

  3. 假设 5 年期债券的到期收益率为 R5,10 年期债券的到期收益率为 R10。在第 5 年和第 10 年之间有五个间隔。因此,每年的增量值为 附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

    • 对于一只 6 年期债券,其价值为 附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

    • 对于一只 7 年期债券,其价值为 附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

    • 对于一只 8 年期债券,其价值为 附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

    • 对于一只 9 年期债券,其价值为 附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

这里是更详细的解释。如果已知的两个点的坐标为 附录 F - 数据案例 #2 - 通过新债券发行筹集的资金附录 F - 数据案例 #2 - 通过新债券发行筹集的资金,线性插值就是这两点之间的直线。对于区间内的一个值 x,直线上的值 y 可以通过以下公式计算:

附录 F - 数据案例 #2 - 通过新债券发行筹集的资金

这一点可以从右侧的图形中几何地推导出来。这是多项式插值的一个特例,n=1

解这个方程,得到未知值 y,在 x 处的值为:

附录 F – 数据案例 #2 – 新债券发行筹集的资金

这是在区间(x0, x1)内进行线性插值的公式。

总结

在本章中,我们涵盖了与利率相关的各种概念,例如年利率APR)、实际年利率EAR)、复利频率,如何将一种利率转换为另一种不同复利频率的利率,以及利率的期限结构。然后,我们讨论了如何估算普通债券的销售价格,如何估算到期收益率YTM)和久期。为了获得股票价格,可以应用所谓的折现股息模型。

在下一章,我们将讨论资本资产定价模型(CAPM),它可能是资产定价中最广泛使用的模型。在讨论其基本形式后,我们展示了如何下载上市公司和市场指数的历史价格数据。我们将演示如何估算回报并进行线性回归,以计算股票的市场风险。

第六章:资本资产定价模型

资本资产定价模型CAPM)可能是资产定价中使用最广泛的模型。其流行背后有几个原因。首先,它非常简单,因为它是一个单因素线性模型。其次,实施这种单因素模型非常容易。任何感兴趣的读者都可以下载上市公司的历史价格数据和市场指数数据,首先计算回报率,然后估计股票的市场风险。第三,这种最简单的单因素资产定价模型可以作为其他更高级模型的第一模型,例如第七章介绍的 Fama-French 三因子模型、Fama-French-Carhart 四因子模型和五因子模型(第七章,多因子模型和绩效衡量)。在本章中,将涵盖以下主题:

  • CAPM 介绍

  • 如何从 Yahoo Finance 下载数据

  • 滚动贝塔

  • 多只股票的贝塔估计的几个 Python 程序

  • 调整贝塔和投资组合贝塔估计

  • Scholes 和 Williams(1977 年)对贝塔的调整

  • Dimson(1979 年)对贝塔的调整

  • 输出数据到各种类型的外部文件

  • 简单的字符串操作

  • Canopy 平台上的 Python

CAPM 介绍

根据著名的 CAPM,股票的预期回报与预期市场回报线性相关。在此,我们以国际商业机器(IBM)的股票为例,其股票代码为 IBM,这个线性单因素资产定价模型可适用于任何其他股票或投资组合。公式如下:

Introduction to CAPM

这里,E()表示期望值,E(R[IBM])是 IBM 的预期回报,R[f]是无风险利率,E(R[mkt])是预期市场回报。例如,标准普尔 500 指数可以作为市场指数。前述方程的斜率或Introduction to CAPM是 IBM 的市场风险的度量。为了简化符号,可以省略期望值符号:

Introduction to CAPM

实际上,我们可以考虑股票超额回报与市场超额回报之间的关系。下面的公式本质上与前述公式相同,但解释更好更清晰:

Introduction to CAPM

回想一下,在第三章,时间价值,我们学到,股票预期回报与无风险利率之间的差异称为风险溢价。这对个别股票和市场指数都是如此。因此,方程(3)的含义非常容易解释:个别股票的风险溢价取决于两个因素:其市场风险和市场风险溢价。

在数学上,前述线性回归的斜率可以写成如下形式:

Introduction to CAPM

这里 CAPM 简介 是 IBM 股票回报与市场指数回报之间的协方差,! CAPM 简介 是市场回报的方差。由于 CAPM 简介,其中 CAPM 简介 是 IBM 回报与指数回报之间的相关性,上述方程可以写成以下形式:

CAPM 简介

贝塔值的含义是,当预期的市场风险溢价增加 1%时,个别股票的预期回报将增加β%;反之亦然。因此,贝塔(市场风险)可以视为一个放大器。所有股票的平均贝塔值为 1。因此,如果某只股票的贝塔值大于 1,则意味着其市场风险高于平均股票的市场风险。

以下几行代码是此示例的一个实现:

>>> import numpy as np
>>> import statsmodels.api as sm
>>> y=[1,2,3,4,2,3,4]
>>> x=range(1,8)
>>> x=sm.add_constant(x)
>>> results=sm.OLS(y,x).fit()
>>> print(results.params)
     [ 1.28571429  0.35714286]

要查看 OLS 结果的所有信息,我们将使用 print(results.summary()) 命令,参见以下屏幕截图:

CAPM 简介

此时,读者可以关注两个系数的值及其相应的 T 值和 P 值。我们将在第八章,时间序列分析中讨论其他结果,例如 Durbin-Watson 统计量和 Jarque-Bera 正态性检验。贝塔值为 0.3571,对应的 T 值为 2.152。由于 T 值大于 2,我们可以声明其显著不同于零。或者,根据 0.084 的 P 值,如果我们选择 10%作为临界点,我们也会得出相同的结论。以下是第二个示例:

>>> from scipy import stats 
>>> ret = [0.065, 0.0265, -0.0593, -0.001,0.0346] 
>>> mktRet = [0.055, -0.09, -0.041,0.045,0.022] 
>>>(beta, alpha, r_value,p_value,std_err)=stats.linregress(ret,mktRet)

对应的结果如下所示:

>>> print(beta, alpha) 
0.507743187877 -0.00848190035246
>>> print("R-squared=", r_value**2)
R-squared= 0.147885662966
>>> print("p-value =", p_value)
p-value = 0.522715523909

再次使用 help() 函数可以获取更多关于此函数的信息,参见以下的前几行:

>>>help(stats.linregress)

scipy.stats._stats_mstats_common 模块中 linregress 函数的帮助:

linregress(x, y=None)

计算两组测量数据的线性最小二乘回归。

参数 xy:类似数组的两个测量集合。两个数组的长度应相同。如果只给定 x(且 y=None),则它必须是一个二维数组,其中一个维度的长度为 2。然后,通过沿着长度为 2 的维度拆分数组来获得两个测量集合。

对于第三个示例,我们生成一个已知截距和斜率的 yx 观测值集,例如 alpha=1beta=0.8,参见以下公式:

CAPM 简介

这里,yi 是因变量 y 的第 i 个观测值,1 是截距,0.8 是斜率(贝塔值),xi 是自变量 x 的第 i 个观测值,且

CAPM 简介

是随机值。对于上述方程,在生成了一组 yx 数据后,我们可以进行线性回归。为此,使用了一组随机数:

from scipy import stats 
import scipy as sp
sp.random.seed(12456)
alpha=1
beta=0.8
n=100
x=sp.arange(n)
y=alpha+beta*x+sp.random.rand(n)
(beta,alpha,r_value,p_value,std_err)=stats.linregress(y,x) 
print(alpha,beta) 
print("R-squared=", r_value**2)
print("p-value =", p_value)

在上述代码中,sp.random.rand()函数将会调用一组随机数。为了得到相同的一组随机数,使用了sp.random.seed()函数。换句话说,每当使用相同的种子时,任何程序员都会得到相同的随机数。这将在第十二章,蒙特卡洛模拟中详细讨论。结果如下:

%run "C:/yan/teaching/Python2/codes/c6_02_random_OLS.py"
(-1.9648401142472594,1.2521836174247121,)
('R-squared=', 0.99987143193925765)
('p-value =', 1.7896498998980323e-192)

现在,让我们看看如何估算微软的贝塔值(市场风险)。假设我们对 2012 年 1 月 1 日到 2016 年 12 月 31 日这一时期的数据感兴趣,共计五年的数据。完整的 Python 程序如下:

from scipy import stats 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
begdate=(2012,1,1)
enddate=(2016,12,31)

ticker='MSFT'
p =getData(ticker, begdate, enddate,asobject=True,adjusted=True)
retIBM = p.aclose[1:]/p.aclose[:1]-1

ticker='^GSPC'
p2 = getData(ticker, begdate, enddate,asobject=True,adjusted=True)
retMkt = p2.aclose[1:]/p2.aclose[:1]-1
(beta,alpha,r_value,p_value,std_err)=stats.linregress(retMkt,retIBM) 
print(alpha,beta) 
print("R-squared=", r_value**2)
print("p-value =", p_value)

要使用五年数据估算 IBM 的贝塔值,在前面的 Python 程序中,下载历史价格数据的主要函数是matplotlib.finance.quotes_historical_yahoo_ochl。这里是相关链接:matplotlib.org/api/finance_api.html^GSPC的股票代码代表 S&P500 市场指数。结果如下:

CAPM 简介

根据之前的结果,IBM 的贝塔值为 0.41,截距为 0.004。此外,R²为 0.36,P 值几乎为零。在前面的程序中,风险无关利率被忽略了。忽略它对贝塔值(斜率)的影响很小。在下一章,我们将展示如何在讨论 Fama-French 三因素模型时考虑风险无关利率。要获取更多关于quotes_historical_yahoo_ochl的信息,可以使用帮助函数:

help(quotes_historical_yahoo_ochl)
Help on function quotes_historical_yahoo_ochl in 
module matplotlib.finance:
quotes_historical_yahoo_ochl(ticker, date1, date2, asobject=False, adjusted=True, cachename=None)
 Get historical data for ticker between date1 and date2.

See :func:`parse_yahoo_historical` for explanation of 
output formats and the *asobject* and *adjusted* kwargs.
Parameters
    ----------
ticker : str   stock ticker
date1 : sequence of form (year, month, day), `datetime`, 
           or `date` start date
date2 : sequence of form (year, month, day), `datetime`, or 
              `date`  end date
  cachename : str or `None`
            is the name of the local file cache.  If None, will
            default to the md5 hash or the url (which incorporates 
            the ticker and date range)
   Examples
    --------
    sp=f.quotes_historical_yahoo_ochl('^GSPC',d1,d2,asobject=True,
        adjusted=True)
      returns = (sp.open[1:] - sp.open[:-1])/sp.open[1:]
     [n,bins,patches] = hist(returns, 100)
     mu = mean(returns)
     sigma = std(returns)
     x = normpdf(bins, mu, sigma)
     plot(bins, x, color='red', lw=2)

显然,编写一个函数,通过三个输入值:股票代码、起始日期和结束日期,来获取数据是一个好主意,参见以下代码:

from scipy import stats 
from matplotlib.finance import quotes_historical_yahoo_ochl as aa 
#
def dailyReturn(ticker,begdate,enddate):
     p = aa(ticker, begdate,enddate,asobject=True,adjusted=True)
     return p.aclose[1:]/p.aclose[:-1]-1
#
begdate=(2012,1,1)
enddate=(2017,1,9)
retIBM=dailyReturn("wmt",begdate,enddate)
retMkt=dailyReturn("^GSPC",begdate,enddate)
outputs=stats.linregress(retMkt,retIBM) 
print(outputs)

沃尔玛的贝塔值(市场风险)的输出结果如下:

CAPM 简介

另外,我们可以调用p4f.dailyReturnYahoo()函数,参见以下代码:

import p4f
x=dailyReturn("ibm",(2016,1,1),(2016,1,10))
print(x)
Out[51]: array([-0.0007355 , -0.00500558, -0.01708957, -0.00925784])

移动贝塔值

有时,研究人员需要基于例如三年的移动窗口生成贝塔时间序列。在这种情况下,我们可以编写一个循环或双重循环。让我们来看一个更简单的例子:估算 IBM 在若干年的年化贝塔值。首先,让我们看看从日期变量中获取年份的两种方式:

import datetime
today=datetime.date.today()
year=today.year                   # Method I
print(year)
2017
print(today.strftime("%Y"))       # Method II
 '2017'

用于估算年化贝塔值的 Python 程序如下:

import numpy as np
import scipy as sp
import pandas as pd
from scipy import stats 
from matplotlib.finance import quotes_historical_yahoo_ochl 

def ret_f(ticker,begdate, enddate):
    p = quotes_historical_yahoo_ochl(ticker, begdate,    
    enddate,asobject=True,adjusted=True)
    return((p.aclose[1:] - p.aclose[:-1])/p.aclose[:-1])
#
begdate=(2010,1,1)
enddate=(2016,12,31)
#
y0=pd.Series(ret_f('IBM',begdate,enddate))
x0=pd.Series(ret_f('^GSPC',begdate,enddate))
#
d=quotes_historical_yahoo_ochl('^GSPC', begdate, enddate,asobject=True,adjusted=True).date[0:-1]
lag_year=d[0].strftime("%Y")
y1=[]
x1=[]
beta=[]
index0=[]
for i in sp.arange(1,len(d)):
    year=d[i].strftime("%Y")
    if(year==lag_year):
       x1.append(x0[i])
       y1.append(y0[i])
    else:
       (beta,alpha,r_value,p_value,std_err)=stats.linregress(y1,x1) 
       alpha=round(alpha,8)
       beta=round(beta,3)
       r_value=round(r_value,3)
       p_vaue=round(p_value,3)
       print(year,alpha,beta,r_value,p_value)
       x1=[]
       y1=[]
       lag_year=year

相应的输出结果如下:

移动贝塔值

调整后的贝塔值

许多研究人员和专业人士发现,贝塔值具有均值回归的趋势。这意味着,如果本期的贝塔值小于 1,那么下期贝塔值较高的概率较大。相反,如果当前贝塔值大于 1,下期贝塔值可能会较小。调整后的贝塔值的公式如下:

调整后的贝塔值

这里,βadj是调整后的贝塔值,β是我们估算的贝塔值。一个投资组合的贝塔值是投资组合中各个股票贝塔值的加权平均值:

调整后的贝塔值

这里调整后的贝塔是投资组合的贝塔,wi (βi)是其股票的权重(贝塔),n是投资组合中的股票数量。wi的权重按照以下公式计算:

调整后的贝塔

这里vi是股票i的价值,前面公式中的分母是所有vi的和,即投资组合的价值。

Scholes 和 William 调整后的贝塔

许多研究人员发现,β对于频繁交易的股票会存在上行偏差,而对于不常交易的股票则会存在下行偏差。为了克服这一点,Scholes 和 Williams 建议如下调整方法:

Scholes 和 William 调整后的贝塔

这里,β是股票或投资组合的贝塔,ρm是市场回报的自相关性。前面公式中的三个贝塔由以下三个方程定义:

Scholes 和 William 调整后的贝塔

这里,我们来看一下如何给数组添加滞后。程序在左侧面板,输出结果显示在右侧面板:

import pandas as pd
import scipy as sp
x=sp.arange(1,5,0.5)
y=pd.DataFrame(x,columns=['Ret'])
y['Lag']=y.shift(1)
print(y)

在前面的程序中应用了.shift()函数。由于我们需要市场回报滞后一周期,我们可以在.shift()函数中指定一个负值-1,见以下代码:

import pandas as pd
import scipy as sp
x=sp.arange(1,5,0.5)
y=pd.DataFrame(x,columns=['Ret'])
y['Lag']=y.shift(1)
y['Forward']=y['Ret'].shift(-1)
print(y)

    Ret Lag  Forward
0  1.0  NaN      1.5
1  1.5  1.0      2.0
2  2.0  1.5      2.5
3  2.5  2.0      3.0
4  3.0  2.5      3.5
5  3.5  3.0      4.0
6  4.0  3.5      4.5
7  4.5  4.0      NaN

输出结果如下:

Scholes 和 William 调整后的贝塔

首先,看看一个与每月数据相关的 Python 数据集,文件名为yanMonthly.pklcanisius.edu/~yany/python/yanMonthly.pkl。以下代码将读取该数据集:

import pandas as pd
x=pd.read_pickle("c:/temp/yanMonthly.pkl")
print(x[0:10])

相关输出显示如下:

Scholes 和 William 调整后的贝塔

让我们看看这个每月数据集中包含了哪些证券,见以下输出:

import pandas as pd
import numpy as np
df=pd.read_pickle("c:/temp/yanMonthly.pkl")
unique=np.unique(df.index)
print(len(unique))
print(unique)

从这里显示的输出结果中,我们可以看到有129只证券:

Scholes 和 William 调整后的贝塔

为了获取 S&P500 数据,我们将使用^GSPC,因为这是 Yahoo!Finance 使用的股票代码:

import pandas as pd
import numpy as np
df=pd.read_pickle("c:/temp/yanMonthly.pkl")
sp500=df[df.index=='^GSPC']
print(sp500[0:5])
ret=sp500['VALUE'].diff()/sp500['VALUE'].shift(1)
print(ret[0:5])

这里显示的是前 10 行:

Scholes 和 William 调整后的贝塔

在估计回报后,我们可以估计其滞后和领先,并进行三次不同的回归来估计这三个贝塔。

同样,Dimson(1979)建议以下方法来调整贝塔:

Scholes 和 William 调整后的贝塔

最常用的k值是1。因此,我们有以下公式:

Scholes 和 William 调整后的贝塔

由于这相当于运行一个三因子线性模型,我们将在下一章中讨论(第七章, 多因子模型与业绩衡量)。

提取输出数据

在本节中,我们将讨论将输出数据提取到不同文件格式的不同方法。

输出数据到文本文件

以下代码将下载微软的每日价格数据,并保存到一个文本文件中:

import pandas_datareader.data as getData
import re
ticker='msft'
f=open("c:/temp/msft.txt","w")
p = getData.DataReader(ticker, "google")
f.write(str(p))
f.close()

以下是一些保存的观察结果:

将数据输出到文本文件

将数据保存为.csv 文件

以下程序首先获取 IBM 的价格数据,然后将其保存为.csv文件,路径为c:/temp

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import csv
f=open("c:/temp/c.csv","w")

ticker='c'
begdate=(2016,1,1)
enddate=(2017,1,9)
p = getData(ticker, begdate, enddate,asobject=True,adjusted=True)

writer = csv.writer(f)
writer.writerows(p)
f.close()

在前面的代码中,我们将quotes_historical_yahoo_ochl()函数重命名为getData,以便于使用。读者可以使用自己喜欢的名称。

将数据保存为 Excel 文件

以下程序首先获取 IBM 的价格数据,然后将其保存为.csv文件,路径为c:/temp

import pandas as pd
df=pd.read_csv("http://chart.yahoo.com/table.csv?s=IBM")
f= pd.ExcelWriter('c:/temp/ibm.xlsx')
df.to_excel(f, sheet_name='IBM')
f.save()

请注意,如果读者看到No module named openpyxl的错误信息,这意味着你需要首先安装该模块。以下截图展示了一些观察结果:

将数据保存为 Excel 文件

显然,我们很可能不会连接第一列,因为它只是无关的行列指示符:

import pandas as pd
df=pd.read_csv("http://chart.yahoo.com/table.csv?s=IBM")
f= pd.ExcelWriter('c:/temp/ibm.xlsx')
df.to_excel(f,index=False,sheet_name='IBM')
f.save() 

将数据保存为 pickle 数据集

以下程序首先生成一个简单的数组,包含三个值。我们将它们保存到名为tmp.bin的二进制文件中,路径为C:\temp\

>>>import pandas as pd 
>>>import numpy as np 
>>>np.random.seed(1234) 
>>> a = pd.DataFrame(np.random.randn(6,5))
>>>a.to_pickle('c:/temp/a.pickle') 

名为a的数据集如下所示:

将数据保存为 pickle 数据集

将数据保存为二进制文件

以下程序首先生成一个简单的数组,包含三个值。我们将它们保存到名为tmp.bin的二进制文件中,路径为C:\temp\

>>>import array 
>>>import numpy as np 
>>>outfile = "c:/temp/tmp.bin" 
>>>fileobj = open(outfile, mode='wb') 
>>>outvalues = array.array('f') 
>>>data=np.array([1,2,3]) 
>>>outvalues.fromlist(data.tolist()) 
>>>outvalues.tofile(fileobj) 
>>>fileobj.close()

从二进制文件读取数据

假设我们已经生成了一个名为C:\temp\tmp.bin的二进制文件,文件中包含三个数字:1、2 和 3。以下 Python 代码用于读取它们:

>>>import array 
>>>infile=open("c:/temp/tmp.bin", "rb") 
>>>s=infile.read() # read all bytes into a string 
>>>d=array.array("f", s) # "f" for float 
>>>print(d) 
>>>infile.close()

d的内容如下:

从二进制文件读取数据

简单字符串操作

对于 Python,我们可以直接将一个字符串赋值给变量,而无需事先定义:

>>> x="This is great"
>>> type(x)
<class 'str'>

用于将有效利率转换为其他利率的公式中,第二个输入值是一个字符串。例如,'s2a'

>>> type='s2a'
>>> type[0]
's'
>>> len(type)
3

len()函数显示字符串的长度,见以下代码:

>>>x='Hello World!'
>>>len(x)
13

这里是几种常用的子字符串选择方法:

string='Hello World!'

# find the length of the string
n_length=len(string)
print(n_length)

# the number of appearance of letter l
n=string.count('l') 
print(n) 

# find teh locatoin of work of 'World'
loc=string.index("World") 
print(loc) 

# number of spaces
n2=string.count(' ')
print(n2)

print(string[0]) # print the first letter 
print(string[0:1]) # print the first letter (same as above)
print(string[0:3]) # print the first three letters
print(string[:3]) # same as above 
print(string[-3:]) # print the last three letters
print(string[3:]) # ignore the first three 
print(string[:-3]) # except the last three

相应的输出如下所示:

简单字符串操作

很多时候,我们希望去除前后多余的空格。在这种情况下,可以使用三个函数,分别是strip()lstrip()rstrip()

string='Hello World!'

print(string.lower())
print(string.title())
print(string.capitalize())
print(string.swapcase())

string2=string.replace("World", "John")
print(string2)

# strip() would remove spaces before and the end of string
# lstrip() would remove spaces before and the end of string
# rstrip() would remove spaces before and the end of string
string3=' Hello World! '
print(string3)
print(string3.strip())
print(string3.lstrip())
print(string3.rstrip())

输出如下所示:

简单字符串操作

以下 Python 程序生成了圣经中所有单词的频率表:

from string import maketrans 
import pandas as pd 
word_freq = {}
infile="c:/temp/AV1611.txt"
word_list = open(infile, "r").read().split() 
ttt='!"#$%&()*+,./:;<=>?@[\\]^_`{|}~0123456789'
for word in word_list:
    word = word.translate(maketrans("",""),ttt )
    if word.startswith('-'): 
        word = word.replace('-','')
    if len(word): 
        word_freq[word] = word_freq.get(word, 0) + 1 
keys = sorted(word_freq.keys())
x=pd.DataFrame(keys) 
x.to_pickle('c:/temp/uniqueWordsBible.pkl')

有兴趣的读者可以从作者的网页下载 pickle 文件,地址为canisius.edu/~yany/python/uniqueWordsBible.pkl。输入x[0:10]后,我们可以看到前 10 个单词,见以下截图:

简单字符串操作

通过 Canopy 运行 Python

本节是可选的,特别是对于那些没有 Python 或通过 Anaconda 使用 Python 的读者。如果您想让 Python 编程更容易,安装另一个超强包是个不错的主意。在本节中,我们将讨论两个简单的任务:如何通过 Canopy 安装 Python 以及如何检查和安装各种 Python 模块。要安装 Python,请访问相关网页:store.enthought.com/downloads/#default。然后,您将看到以下屏幕:

Python 通过 Canopy

根据操作系统的不同,您可以下载 Canopy,例如 32 位的 Windows 版本。启动 Canopy 后,以下屏幕将出现:

Python 通过 Canopy

最常用的两个面板是编辑器包管理器。点击编辑器后,将弹出以下面板:

Python 通过 Canopy

显然,我们可以创建一个新文件,或者从现有程序中选择文件。让我们尝试最简单的一个;见下图。点击绿色按钮后,我们可以运行程序:

Python 通过 Canopy

或者,我们可以点击菜单栏中的运行,然后选择相应的操作。Canopy 最重要的优势之一是,它可以极其方便地安装各种 Python 模块。点击包管理器后,我们将看到以下屏幕:

Python 通过 Canopy

从左侧,我们可以看到已安装了 99 个包,还有 532 个可用包。假设名为statsmodels的 Python 模块没有预先安装。在左侧点击可用后,我们通过输入关键词来搜索该模块。找到该模块后,我们可以决定是否安装。通常,可能会有多个版本,见下图:

Python 通过 Canopy

参考文献

请参考以下文章:

  • Carhart, Mark M., 1997, 《共同基金表现的持久性》,《金融学杂志》52, 57-82。

  • Fama, Eugene 和 Kenneth R. French, 1993, 《股票和债券回报的共同风险因素》,《金融经济学杂志》33, 3056。

  • Fama, Eugene 和 Kenneth R. French, 1992, 《预期股票回报的横截面》,《金融学杂志》47, 427-465。

  • 字符串操作:www.pythonforbeginners.com/basics/string-manipulation-in-python

附录 A – 数据案例 #3 - 贝塔估计

目标:通过实际操作估算给定公司组的市场风险:

  1. 这些公司的 Alpha 和 Beta 值是多少?

  2. 评论您的结果。

  3. 基于您的月度回报率,S&P500 和无风险利率的年回报均值是多少?

  4. 如果预期的市场年回报率为 12.5%,预期的无风险年利率为 0.25%,那么这些公司的股本成本是多少?

  5. 投资组合的贝塔值是多少?

    计算工具:Python

    时间段:从 2011 年 2 月 1 日到 2016 年 12 月 31 日(过去五年)。

    技术细节:

    参考资料

序号 公司名称 股票代码 行业 股票数量
1 沃尔玛商店公司 WMT 大型零售商 1000
2 苹果公司 AAPL 计算机 2000
3 国际商业机器公司 IBM 计算机 1500
4 通用电气公司 GE 科技 3000
5 花旗集团 C 银行 1800

数据下载和处理的步骤:

  1. 股票月度价格数据来自雅虎财经 (finance.yahoo.com)。

  2. 计算月度回报数据。

  3. S&P500 被用作市场指数,其代码为^GSPC

  4. 我们使用法兰奇教授的月度数据集中的无风险利率作为我们的无风险利率。

  5. 在合并这些数据集时,请注意它们的日期顺序。

注意 1 – 如何下载数据?这里以 S&P500 为例(代码是^GSPC):

  1. 访问雅虎财经 (finance.yahoo.com)。

  2. 输入^GSPC

  3. 点击历史价格

  4. 选择起始日期和结束日期。点击获取价格

  5. 滚动到页面底部并点击下载到电子表格

  6. 给文件命名,例如sp500.csv

注意 2 – 如何下载月度无风险利率?

  1. 访问法兰奇教授的数据库:mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html

  2. 选择 Fama-French 3 因子模型,见下图:参考资料

以下截图给出了前几行和最后几行的数据:

参考资料

练习

  1. CAPM 是什么意思?它是一个线性模型吗?

  2. 一因子线性模型的特点是什么?

  3. 总风险和市场风险的定义是什么?你如何衡量它们?

  4. 解释以下两个方程式的相似性和区别:练习

  5. 一只股票的总风险与市场风险之间的关系是什么?

  6. 谁应该关注 CAPM,或者这个模型有什么用途?

  7. 如果股票 A 的市场风险高于股票 B,这是否意味着 A 的预期回报也更高?请解释。

  8. 你如何衡量不同类型的风险?

  9. 你如何预测预期的市场回报?

  10. 如果我们知道预期的市场风险溢价,如何预测一家公司股本成本?

  11. 以下的贝塔调整公式背后有什么逻辑?!练习

  12. 构建一个不等权重的投资组合,权重分别为 20%、10%、30%、10%、10%和 20%。股票列表包括沃尔玛(WMT)、国际商业机器公司(IBM)、花旗集团(C)、微软(MSFT)、谷歌(GOOG)和戴尔(DELL)。估算这些股票在 2001 年至 2016 年的月度投资组合回报。

  13. 查找 IBM 的贝塔值,访问雅虎财经,然后搜索 IBM,点击左侧的“关键统计数据”。finance.yahoo.com/q/ks?s=IBM+Key+Statistics

    下载 IBM 的历史价格数据并估算其贝塔系数并进行比较。

  14. 如果你使用五年的月度数据,DELL、IBM、GOOG 和 C 的总风险和市场风险是多少?

  15. 编写一个 Python 程序,估算以下 10 只股票的αβ。所使用的时间范围应为过去五年(2012 年 1 月 2 日到 2017 年 1 月 10 日),并使用来自雅虎财经和联邦储备网站的月度数据(用于无风险利率):

    公司名称 股票代码 行业
    1 家庭美元商店 FDO 零售
    2 沃尔玛商店 WMT 大型超市
    3 麦当劳 MCD 餐饮
    4 戴尔 DELL 计算机硬件
    5 国际商业机器公司 IBM 计算机
    6 微软 MSFT 软件
    7 通用电气 GE 综合企业
    8 谷歌 GOOG 网络服务
    9 苹果 AAPL 计算机硬件
    10 eBay EBAY 网络服务
  16. 从这一章中我们知道,可以调用p4f.dailyReturn函数来下载某个股票代码的历史数据,并指定一个时间范围;参考以下代码:

    import p4f
    x=dailyReturn("ibm",(2016,1,1),(2016,1,10))
    

    该函数如下所示:

    def dailyReturn(ticker,begdate,enddate):
        from scipy import stats 
        from matplotlib.finance import quotes_historical_yahoo_ochl
        p = quotes_historical_yahoo_ochl(ticker, begdate,    
              enddate,asobject=True,adjusted=True)
        return p.aclose[1:]/p.aclose[:-1]-1
    

    显然,第二种和第三种起始日期和结束日期的输入格式不够用户友好;参考dailyReturn("ibm",(2016,1,1),(2016,1,10))。修改程序使其更加用户友好,例如dailyReturn2("ibm", 20160101, 20160110)

  17. 从雅虎财经下载股票价格数据(只要可能),比如 DELL、IBM 和 MSFT。然后计算它们在若干十年中的波动率。例如,估算 IBM 在多个五年期的波动率。波动率的趋势如何?

  18. 市场指数之间(或其中)相关性是多少?例如,你可以下载标准普尔 500(其雅虎代码为^GSPC)和道琼斯工业平均指数(^DJI)的价格数据,时间范围为过去 10 年。然后估算它们的收益并计算相应的相关性。对结果进行评论。

  19. 哪五只股票在 2006 到 2010 年期间与 IBM 的相关性最强?(提示:没有唯一答案。你可以尝试十几只股票)。

  20. 2017 年 1 月 2 日,你的投资组合包含 2,000 股 IBM,1,500 股花旗集团,以及 500 股微软(MSFT)。这个投资组合的贝塔系数是多少?你可以使用过去五年的历史数据来运行 CAPM 模型。

  21. IBM 股票收益和微软(MSFT)的相关性是多少?

    提示

    你可以使用过去 10 年的历史数据来估算相关性。

  22. 找出以下代码中的问题并进行修正:

    from scipy import stats 
    from matplotlib.finance import quotes_historical_yahoo_ochl
    
    def dailyReturn(ticker,begdate=(1962,1,1),enddate=(2017,1,10)):
    p = quotes_historical_yahoo_ochl(ticker, begdate, enddate,asobject=True,adjusted=True)
    return p.aclose[1:]/p.aclose[:-1]-1
    
    retIBM=dailyReturn("wmt")
    retMkt=dailyReturn("^GSPC")
    
    outputs=stats.linregress(retIBM,retMkt) 
    print(outputs)
    
  23. 编写一个名为beta()的 Python 函数,利用过去五年的历史数据及标准普尔 500 指数,提供一个贝塔值及其显著性值,如 T 值或 P 值。

总结

资本资产定价模型CAPM)可能是资产定价中最广泛使用的模型。其流行背后有几个原因。首先,它非常简单。它只是一个单因子线性模型。其次,实现这个单因子模型相当容易。任何感兴趣的读者都可以下载上市公司和市场指数的历史价格数据,以计算它们的回报率,然后估算该股票的市场风险。第三,这个最简单的单因子资产定价模型可以作为其他更高级模型的基础,比如 Fama-French 三因子模型、Fama-French-Carhart 四因子模型和 Fama-French 五因子模型,这些将在下一章介绍。

第七章:多因子模型与绩效衡量

在第六章,资本资产定价模型中,我们讨论了最简单的单因子线性模型:CAPM。如前所述,这个单因子线性模型作为更高级和复杂模型的基准。在本章中,我们将重点讨论著名的 Fama-French 三因子模型、Fama-French-Carhart 四因子模型和 Fama-French 五因子模型。理解这些模型后,读者应该能够开发自己的多因子线性模型,例如通过添加国内生产总值GDP)、消费者价格指数CPI)、商业周期指标或其他变量作为额外的因子。此外,我们还将讨论绩效衡量标准,如夏普比率、特雷诺比率和詹森阿尔法。具体来说,本章将涵盖以下主题:

  • Fama-French 三因子模型介绍

  • Fama-French-Carhart 四因子模型

  • Fama-French 五因子模型

  • 其他多因子模型

  • 夏普比率和特雷诺比率

  • 下偏标准差和 Sortino 比率

  • 詹森阿尔法

  • 如何合并不同的数据集

Fama-French 三因子模型介绍

在讨论 Fama-French 三因子模型和其他模型之前,我们先看一下三因子线性模型的通用方程式:

Fama-French 三因子模型介绍

在这里,y是因变量,α是截距,x1x2x3是三个自变量,β1β2β3是三个系数,ε是随机因素。换句话说,我们尝试用三个自变量来解释一个因变量。与单因子线性模型相同,这个三因子线性模型的图形表示是一条直线,位于四维空间中,每个自变量的权重也是一个单位。在这里,我们将通过两个简单的例子来展示如何进行多因子线性回归。第一个例子中,我们有以下代码。数值没有特定的含义,读者也可以输入自己的数值:

from pandas.stats.api import ols
import pandas as pd
y = [0.065, 0.0265, -0.0593, -0.001,0.0346] 
x1 = [0.055, -0.09, -0.041,0.045,0.022]
x2 = [0.025, 0.10, 0.021,0.145,0.012]
x3=  [0.015, -0.08, 0.341,0.245,-0.022]
df=pd.DataFrame({"y":y,"x1":x1, 'x2':x2,'x3':x3})
result=ols(y=df['y'],x=df[['x1','x2','x3']])
print(result)

在前面的程序中,应用了pandas.stats.api.ols()函数。OLS代表最小二乘法。有关 OLS 模型的更多信息,我们可以使用help()函数;请参阅以下两行代码。为了简洁起见,输出结果在此未显示:

from pandas.stats.api import ols
help(ols) 

pandas DataFrame 用于构建我们的数据集。读者应注意{"y":y"x1":x1'x2':x2'x3':x3}的结构。它采用字典的数据格式。运行回归的结果如下所示:

Fama-French 三因子模型介绍

从输出结果来看,三因子模型首先列出:y 对应三个独立或可解释变量 x1x2x3。观察次数为 5,而自由度为 4。R2 的值为 0.96,调整后的 R2 为 0.84。R2 值反映了 x1x2x3 可以解释的 y 变动的百分比。由于调整后的 R2 考虑了独立变量的数量影响,因此更具意义。RMSE 代表 均方根误差,这个值越小,我们的模型越好。F 统计量和 p 值反映了我们线性模型的优度。F 值反映了整个模型的质量。F 值应与其临界 F 值进行比较,临界 F 值又取决于三个输入变量:置信水平、分子的自由度和分母的自由度。可以使用 scipy.stats.f.ppf() 函数来计算临界 F 值;请参见以下代码:

import scipy.stats as stats
alpha=0.05
dfNumerator=3
dfDenominator=1
f=stats.f.ppf(q=1-alpha, dfn=dfNumerator, dfd=dfDenominator)
print(f)
215.70734537

置信水平等于 1 减去 alpha,即在此案例中为 95%。置信水平越高,结果越可靠,例如 99% 而非 95%。最常用的置信水平有 90%、95% 和 99%。dfNumeratordfDenominator)是分子(分母)的自由度,这取决于样本大小。从前面的 OLS 回归结果来看,我们知道这两个值分别是 3 和 1。

根据前面的值,F=8.1 < 215.7(临界 F 值),我们应当接受原假设,即所有系数为零,也就是说模型的质量不佳。另一方面,P 值为 0.25,远高于临界值 0.05。这也意味着我们应该接受原假设。这是合理的,因为我们输入的这些值并没有实际意义。

在第二个例子中,使用了一个与 IBM 相关的 CSV 文件,该文件从 Yahoo! Finance 下载,数据集可以在canisius.edu/~yany/data/ibm.csv下载。或者,读者也可以访问finance.yahoo.com/下载 IBM 的历史数据。以下是前几行内容:

法玛-弗伦奇三因子模型介绍

Date 是日期变量,Open 是开盘价,High (Low) 是期间内的最高(最低)价,Close 是收盘价,Volume 是交易量,Adj.Close 是调整后的收盘价,已经考虑了股票拆分和股息分配。在下面的 Python 程序中,我们尝试用 OpenHighVolume 三个变量来解释 Adj.Close;请参见以下方程式:

法玛-弗伦奇三因子模型介绍

再次强调,这个 OLS 回归只是作为一个示范,展示如何运行三因子模型,它可能没有任何经济意义。这个示例的美妙之处在于,我们可以轻松获得数据并测试我们的 Python 程序:

import pandas as pd
import numpy as np
import statsmodels.api as sm
inFile='http://canisius.edu/~yany/data/ibm.csv'
df = pd.read_csv(inFile, index_col=0)
x = df[['Open', 'High', 'Volume']]
y = df['Adj.Close']
x = sm.add_constant(x)
result = sm.OLS(y, x).fit()
print(result.summary())

前三条命令导入了三个 Python 模块。x=sm.add_constant(x)这条命令将会为数据添加一列 1。如果没有这一行,我们会强制设置零截距。为了丰富运行三因子线性模型的经验,这次应用了不同的 OLS 函数。使用statsmodels.apilsm.OLS()函数的优势在于,我们可以获得更多关于结果的信息,例如赤池信息量准则AIC)、贝叶斯信息量准则BIC)、偏度和峰度。它们的定义将在下一章(第八章,时间序列分析)中讨论。运行前面的 Python 程序后的相应输出如下:

Fama-French 三因子模型简介

同样,我们将避免花时间解释结果,因为我们当前的目标是展示如何运行三因子回归。

Fama-French 三因子模型

回顾 CAPM 模型,它的形式如下:

Fama-French 三因子模型

在这里,E()是期望,E(Ri)是股票i的预期收益,Rf是无风险利率,E(Rmkt)是预期市场收益。例如,S&P500 指数可以作为市场指数。上面方程的斜率 (Fama-French 三因子模型) 是股票市场风险的度量。为了求出Fama-French 三因子模型的值,我们进行线性回归。Fama-French 三因子模型可以看作是 CAPM 模型的自然扩展,详见此处:

Fama-French 三因子模型

RiRfRmkt 的定义保持不变。SMB 是小盘股的投资组合收益减去大盘股的投资组合收益;HML 是高账面市值比的投资组合收益减去低账面市值比的投资组合收益。Fama/French 因子是通过基于市值和账面市值比形成的六个价值加权投资组合构建的。Small Minus BigSMB)是三个小盘股投资组合的平均收益减去三个大盘股投资组合的平均收益。根据市值(通过市场资本化计算,即流通股数乘以年末股价)来分类所有股票为两类,S(小型)和H(大型)。类似地,基于账面市值比来将所有股票分类为三组:H(高)、M(中)、L(低)。最终,我们可以得到以下六组:

按大小排序为两组
按账面市值比排序成三组
--- ---
SH BH
SM BM
SL BL

SMB 由以下六个投资组合构成:

Fama-French 三因子模型

当股本账面价值与市值比率较低(较高)时,这些股票被称为成长型(价值型)股票。因此,我们可以使用另一公式;见下:

Fama-French 三因子模型

高减低HML)是两组价值型投资组合的平均回报减去两组成长型投资组合的平均回报;见下方公式:

Fama-French 三因子模型

Rm-Rf,市场超额收益率,所有在美国注册并在 NYSE、AMEX 或 NASDAQ 上市的 CRSP 公司加权平均收益,这些公司在月初时具有 CRSP 股票代码 10 或 11,月初有良好的股票和价格数据,且有 t 月份的良好回报数据,减去 1 个月的国债利率(来源:Ibbotson Associates)。以下程序检索 Fama-French 月度因子,并生成一个.pickle格式的数据集。Fama-French 月度数据集的 pandas .pickle格式文件可以从www.canisius.edu/~yany/python/ffMonthly.pkl下载:

import pandas as pd
x=pd.read_pickle("c:/temp/ffMonthly.pkl")
print(x.head())
print(x.tail())

对应的输出如下所示:

Fama-French 三因子模型

接下来,我们展示如何使用 5 年期的月度数据运行 Fama-French 三因子回归。额外的步骤是首先下载历史价格数据。然后我们计算月度收益并将其转换为月度数据,最后与月度 Fama-French 三因子时间序列合并:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd
import scipy as sp
import statsmodels.api as sm

ticker='IBM'
begdate=(2012,1,1)
enddate=(2016,12,31)

p= getData(ticker, begdate, enddate,asobject=True, adjusted=True)
logret = sp.log(p.aclose[1:]/p.aclose[:-1])

ddate=[]
d0=p.date

for i in range(0,sp.size(logret)):
    x=''.join([d0[i].strftime("%Y"),d0[i].strftime("%m"),"01"])
    ddate.append(pd.to_datetime(x, format='%Y%m%d').date())

t=pd.DataFrame(logret,np.array(ddate),columns=[''RET''])
ret=sp.exp(t.groupby(t.index).sum())-1
ff=pd.read_pickle('c:/temp/ffMonthly.pkl')
final=pd.merge(ret,ff,left_index=True,right_index=True)
y=final[''RET'']
x=final[[''MKT_RF'',''SMB'',''HML'']]
x=sm.add_constant(x)
results=sm.OLS(y,x).fit()
print(results.summary())

在前面的程序中,起始日期为 2012 年 1 月 1 日,结束日期为 2016 年 12 月 31 日。获取每日价格数据后,我们估算每日收益率,然后将其转换为月度收益率。已上传 Fama-French 月度三因子时间序列,格式为 pandas 的.pickle。在前面的程序中,np.array(date,dtype=int64)的使用是为了确保两个索引具有相同的数据类型。对应的输出如下:

Fama-French 三因子模型

为了节省空间,我们将不讨论结果。

Fama-French-Carhart 四因子模型和 Fama-French 五因子模型

Jegadeesh 和 Titman(1993)展示了一种有利可图的动量交易策略:买入赢家,卖出输家。基本假设是,在短期内,比如 6 个月,赢家将继续是赢家,而输家将继续是输家。例如,我们可以根据过去 6 个月的累计总回报,将赢家与输家进行分类。假设我们处于 1965 年 1 月,首先估算过去 6 个月的总回报。然后根据总回报将它们从高到低排序,分为 10 个投资组合。排名前 10%(后 10%)被标记为赢家(输家)。我们做多赢家投资组合,做空输家投资组合,持仓期为 6 个月。下个月,即 1965 年 2 月,我们重复相同的操作。从 1965 年 1 月到 1989 年 12 月,Jegadeesh 和 Titman(1993)的实证结果表明,这种交易策略每月可以产生 0.95% 的回报。基于这一结果,Carhart(2000)将动量因素作为第四因子加入到 Fama-French 三因子模型中:

Fama-French-Carhart 四因子模型和 Fama-French 五因子模型

在这里,MOM 是动量因子。以下代码可以上传 ffcMonthly.pkl 并打印出前几行和后几行。该 Python 数据集可以从作者的网站下载:www.canisius.edu/~yany/python/ffcMonthly.pkl

import pandas as pd
x=pd.read_pickle("c:/temp/ffcMonthly.pkl")
print(x.head())
print(x.tail())

输出结果如下所示:

Fama-French-Carhart 四因子模型和 Fama-French 五因子模型

在 2015 年,Fama 和 French 开发了所谓的五因子模型;见下方公式:

Fama-French-Carhart 四因子模型和 Fama-French 五因子模型

在前面的方程中,RMW 是强盈利和弱盈利股票组合回报的差异,CMA 是低投资和高投资公司股票组合回报的差异,Fama 和 French 将其称为保守型和激进型。如果五个因子的暴露能够捕捉到所有预期回报的变化,那么所有证券和投资组合 i 的截距应该为零。再次说明,由于运行 Fama-French 五因子模型与运行 Fama-French 三因子模型非常相似,这里我们不展示如何运行五因子模型。相反,以下代码展示了一个名为 ffMonthly5.pkl 的 Python 数据集的前几行和后几行。该 Python 数据集可以从作者的网站下载:www.canisius.edu/~yany/python/ffMonthly5.pkl

import pandas as pd
x=pd.read_pickle("c:/temp/ffMonthly5.pkl")
print(x.head())
print(x.tail())

相应的输出结果如下所示:

Fama-French-Carhart 四因子模型和 Fama-French 五因子模型

按照相同的思路,对于日频数据,我们有几个数据集,分别是 ffDailyffcDailyffDaily5;有关更多细节,请参见 附录 A – 相关 Python 数据集列表

Dimson(1979)对贝塔值的调整实现

Dimson(1979)建议以下方法:

Dimson(1979)调整贝塔值的实现

最常用的k值是1。因此,我们得到以下方程:

Dimson(1979)调整贝塔值的实现

在运行基于前述方程的回归之前,先解释两个名为.diff().shift()的函数。这里,我们随机选择了五个价格。然后我们估算它们的价格差回报,并添加滞后和前向回报:

import pandas as pd
import scipy as sp

price=[10,11,12.2,14.0,12]
x=pd.DataFrame({'Price':price})
x['diff']=x.diff()
x['Ret']=x['Price'].diff()/x['Price'].shift(1)
x['RetLag']=x['Ret'].shift(1)
x['RetLead']=x['Ret'].shift(-1)
print(x)

输出如下所示:

Dimson(1979)调整贝塔值的实现

显然,价格时间序列是假定从最旧到最新的。差异定义为p(i) – p(i-1)。因此,第一个差异是NaN,即缺失值。我们来看第 4 期,即index=3。差异为1.8(14-12.2),回报率为(14-12.2)/12.2= 0.147541。滞后回报将是这一期之前的回报,即0.109091,而前向回报将是下一期的回报,即-0.142857。在下面的 Python 程序中,我们演示了如何运行之前的程序来分析 IBM 股票:

import pandas as pd
import numpy as np
from pandas.stats.api import ols

df=pd.read_pickle("c:/temp/yanMonthly.pkl")
sp500=df[df.index=='^GSPC']
sp500['retMkt']=sp500['VALUE'].diff()/sp500['VALUE'].shift(1)
sp500['retMktLag']=sp500['retMkt'].shift(1)
sp500['retMktLead']=sp500['retMkt'].shift(-1)

ibm=df[df.index=='IBM']
ibm['RET']=ibm['VALUE'].diff()/ibm['VALUE'].shift(1)
y=pd.DataFrame(ibm[['DATE','RET']])
x=pd.DataFrame(sp500[['DATE','retMkt','retMktLag','retMktLead']])
data=pd.merge(x,y)

result=ols(y=data['RET'],x=data[['retMkt','retMktLag','retMktLead']])
print(result)

输出如下所示:

Dimson(1979)调整贝塔值的实现

绩效度量

为了比较共同函数或单个股票的表现,我们需要一个绩效度量。在金融中,我们知道投资者应在风险和回报之间寻求平衡。说投资组合 A 比投资组合 B 更好可能不是一个好主意,因为前者去年给我们带来了 30%的回报,而后者仅带来了 8%的回报。显而易见的原因是我们不应忽视风险因素。因此,我们常常听到“风险调整回报”这个词。 在本节中,将讨论夏普比率、特雷诺比率、索提诺比率和詹森α。夏普比率是一个广泛使用的绩效度量,定义如下:

绩效度量

在这里,绩效度量是一个投资组合或股票的平均回报,绩效度量是无风险证券的平均回报,σ是超额投资组合(股票)回报的方差,VaR 是超额投资组合(股票)回报的方差。以下代码用于估算假设的无风险利率下的夏普比率:

import pandas as pd
import scipy as sp
df=pd.read_pickle("c:/temp/yanMonthly.pkl")
rf=0.01
ibm=df[df.index=='IBM']
ibm['RET']=ibm['VALUE'].diff()/ibm['VALUE'].shift(1)
ret=ibm['RET']
sharpe=sp.mean((ret)-rf)/sp.std(ret)
print(sharpe)

夏普比率为-0.00826559763423\。以下代码将直接从 Yahoo! Finance 下载每日数据,然后在不考虑无风险利率影响的情况下估算夏普比率:

import scipy as sp
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
begdate=(2012,1,1)
enddate=(2016,12,31)
def ret_f(ticker,begdate,enddate):
     p = getData(ticker,begdate, enddate,asobject=True,adjusted=True)
     return(p.aclose[1:]/p.aclose[:-1]-1)
y=ret_f('IBM',begdate,enddate)
sharpe=sp.mean(y)/sp.std(y)
print(sharpe)

结果是0.00686555838073。基于前面的代码,开发了一个 Python 程序,并提供了更多解释和两个示例;详情请参见附录 C。Sharpe 比率考虑的是总风险,因为标准差作为分母。这个衡量标准适用于当考虑的投资组合是公司或个人的所有财富时。在第六章,资本资产定价模型中,我们认为理性投资者在估计预期收益时,应仅考虑市场风险而非总风险。因此,当考虑的投资组合只是部分财富时,使用总风险并不合适。由于这一点,Treynor 建议使用 beta 作为分母:

绩效衡量

唯一的修改是将 sigma(总风险)替换为 beta(市场风险)。另一个反对在 Sharpe 比率中使用标准差的理由是,它考虑了均值上下两个方向的偏差。然而,我们知道投资者更担心下行风险(低于均值的偏差)。Sharpe 比率的第二个问题是,对于分子,我们将平均收益与无风险利率进行比较。然而,对于分母,偏差是基于均值回报的,而不是同样的无风险利率。为了克服这两个缺点,开发了所谓的下行部分标准差LPSD)。假设我们有 n 个回报和一个无风险利率Rf)。进一步假设有 m 个回报低于这个无风险利率。LPSD 在此定义:

绩效衡量

或者,我们有以下等效的公式:

绩效衡量

Sortino 比率在此定义:

绩效衡量

我们可以编写一个 Python 程序来估算 Sortino 比率;请参见以下代码。为了确保获得相同的一组随机数,sp.random.seed()函数中应使用相同的种子:

import scipy as sp
import numpy as np

mean=0.10;
Rf=0.02
std=0.20
n=100
sp.random.seed(12456)
x=sp.random.normal(loc=mean,scale=std,size=n)
print("std=", sp.std(x))

y=x[x-Rf<0]  
m=len(y)
total=0.0
for i in sp.arange(m):
    total+=(y[i]-Rf)**2

LPSD=total/(m-1)
print("y=",y)
print("LPSD=",LPSD)

相应的输出如下所示:

绩效衡量

从输出中,标准差为0.22,而 LPSD 值为0.045。对于共同基金经理来说,获得一个正的 alpha 值是非常重要的。因此,alpha 值或 Jensen's alpha 是一个绩效衡量标准。Jensen's alpha 定义为实际收益与预期收益之间的差异。其公式如下:

绩效衡量

如何合并不同的数据集

合并不同的数据集是一个常见的任务,比如将指数数据与股票数据等进行合并。因此,了解合并不同数据集的机制非常重要。这里讨论了pandas.merge()函数:

import pandas as pd
import scipy as s
x= pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                 'A': ['A0', 'A1', 'A2', 'A3'],
                 'B': ['B0', 'B1', 'B2', 'B3']})
y = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K6'],
                 'C': ['C0', 'C1', 'C2', 'C3'],
                 'D': ['D0', 'D1', 'D2', 'D3']})

xy的大小是43列,即四行三列;请参见以下代码:

print(sp.shape(x))
print(x)

输出如下所示:

如何合并不同的数据集

print(sp.shape(y))
print(y)

如何合并不同的数据集

假设我们打算基于名为key的变量进行合并,这是两个数据集共享的共同变量。由于该变量的共同值为 K0、K1 和 K2,最终结果应该有三行五列,因为 K3 和 K6 不是两个数据集的共同值;请参见这里显示的结果:

result = pd.merge(x,y, on='key')
print(result)

输出结果如下:

如何合并不同的数据集

由于key是两个数据集共享的,我们可以简单地忽略它;请参见以下代码。换句话说,resultresult2是相同的:

result2 = pd.merge(x,y)
print(result2)

pandas.merge()函数的完整含义如下:

pd.merge(left, right, how='inner', on=None, left_on=None, right_on = None,left_index=False, right_index=False, sort=True,suffixes=('_x', '_y'), copy=True, indicator=False)

对于前两个输入变量,left是第一个输入数据集,而right是第二个输入数据集。对于how=条件,我们有以下四种可能的情况:

how='inner' 含义 描述
内连接 INNER JOIN 使用两个框架的键的交集
外连接 FULL OUTER JOIN 使用两个框架的键的并集
左连接 LEFT OUTER JOIN 仅使用左框架的键
右连接 RIGHT OUTER JOIN 仅使用右框架的键

表 7.1 四种连接条件的含义:内连接、外连接、左连接和右连接

内连接的格式要求两个数据集必须有相同的项目。一个类比是来自有父母的家庭的学生。左连接基于左数据集。换句话说,我们的基准是第一个数据集(left)。一个类比是从只有妈妈的家庭中选择学生。右连接与左连接相反,也就是说,基准是第二个数据集(right)。外连接是包含两个数据集的完整数据集,就像所有家庭的学生一样:有父母的、只有妈妈的和只有爸爸的。

在以下示例中,第一个数据集有 4 年的数据。这些值没有特定的含义,读者可以使用自己的值。我们的共同变量是YEAR。对于第一个数据集,我们有 4 年的数据:2010201120122013。对于第二个数据集,我们有2011201320142015。显然,只有 2 年是重叠的。总共有 6 年的数据:

import pandas as pd
import scipy as sp
x= pd.DataFrame({'YEAR': [2010,2011, 2012, 2013],
'IBM': [0.2, -0.3, 0.13, -0.2],
'WMT': [0.1, 0, 0.05, 0.23]})
y = pd.DataFrame({'YEAR': [2011,2013,2014, 2015],
'C': [0.12, 0.23, 0.11, -0.1],
'SP500': [0.1,0.17, -0.05, 0.13]})

print(pd.merge(x,y, on='YEAR'))
print(pd.merge(x,y, on='YEAR',how='outer'))
print(pd.merge(x,y, on='YEAR',how='left'))
print(pd.merge(x,y, on='YEAR',how='right'))

四个输出结果如下:

如何合并不同的数据集

当两个数据集中的共同变量有不同的名称时,我们应该通过使用left_on='left_name'right_on='another_name'来指定它们的名称;请参见以下代码:

import pandas as pd
import scipy as sp
x= pd.DataFrame({'YEAR': [2010,2011, 2012, 2013],
                 'IBM': [0.2, -0.3, 0.13, -0.2],
                 'WMT': [0.1, 0, 0.05, 0.23]})
y = pd.DataFrame({'date': [2011,2013,2014, 2015],
                 'C': [0.12, 0.23, 0.11, -0.1],
                 'SP500': [0.1,0.17, -0.05, 0.13]})
print(pd.merge(x,y, left_on='YEAR',right_on='date'))

如果我们打算基于索引(行号)进行合并,我们指定left_index='True'right_index='True';请参见以下代码。从某种意义上讲,由于两个数据集都有四行数据,我们只是将它们逐行放在一起。真正的原因是,对于这两个数据集,没有特定的索引。做个对比,ffMonthly.pkl数据的索引是日期:

import pandas as pd
import scipy as sp
x= pd.DataFrame({'YEAR': [2010,2011, 2012, 2013],
                 'IBM': [0.2, -0.3, 0.13, -0.2],
                 'WMT': [0.1, 0, 0.05, 0.23]})
y = pd.DataFrame({'date': [2011,2013,2014, 2015],
                 'C': [0.12, 0.23, 0.11, -0.1],
                 'SP500': [0.1,0.17, -0.05, 0.13]})
print(pd.merge(x,y, right_index=True,left_index=True))

输出结果如下所示。再次强调,我们仅通过合并不同年份的数据来展示结果,而不考虑其经济含义:

如何合并不同的数据集

这里是另一个基于索引合并的示例,其中date作为两个数据集的索引:

import pandas as pd
ff=pd.read_pickle("c:/temp/ffMonthly.pkl")
print(ff.head(2))
mom=pd.read_pickle("c:/temp/ffMomMonthly.pkl")
print(mom.head(3))
x=pd.merge(ff,mom,left_index=True,right_index=True)
print(x.head(2))

两个数据集均可用,例如,canisius.edu/~yany/python/ffMonthly.pkl。输出结果如下所示:

如何合并不同的数据集

有时候,我们需要根据两个键(如股票IDdate)合并两个数据集;请参见这里的格式:

result = pd.merge(left, right, how='left', on=['key1', 'key2'])

我们通过输入一些假设的值来举一个例子:

import pandas as pd
x= pd.DataFrame({'ID': ['IBM', 'IBM', 'WMT', 'WMT'],
'date': [2010, 2011, 2010, 2011],
'SharesOut': [100, 40, 60, 90],
'Asset': [20, 30, 10, 30]})

y = pd.DataFrame({'ID': ['IBM', 'IBM', 'C', 'WMT'],
'date': [2010, 2014, 2010, 2010],
'Ret': [0.1, 0.2, -0.1,0.2],
'ROA': [0.04,-0.02,0.03,0.1]})

z= pd.merge(x,y, on=['ID', 'date'])

对于第一个数据集,我们有两只股票在2010年和2011年的流通股数据。第二个数据集包含三只股票在 2 年(2010年和2014年)中的年度回报率和资产回报率(ROA)数据。我们的目标是通过股票IDdate(年份)将这两个数据集合并。输出结果如下所示:

如何合并不同的数据集

在了解如何运行多因子回归和如何合并不同的数据集之后,读者将能够添加自己的因子。一个问题是,一些因子的频率可能不同,例如季度 GDP 而非月度数据。对于这种情况,我们可以使用各种方法来填充这些缺失的值;请参见以下示例:

import pandas as pd
GDP=pd.read_pickle("c:/temp/usGDPquarterly.pkl")
ff=pd.read_pickle("c:/temp/ffMonthly.pkl")
final=pd.merge(ff,GDP,left_index=True,right_index=True,how='left') 
tt=final['AdjustedGDPannualBillion']
GDP2=pd.Series(tt).interpolate()
final['GDP2']=GDP2

print(GDP.head())
print(ff.head())
print(final.tail(10))

输出结果如下所示:

如何合并不同的数据集

读者应当将这两个 GDP 时间序列与其影响进行对比。

附录 A – 相关 Python 数据集列表

这些数据集的前缀是canisius.edu/~yany/python。例如,对于ffMonthly.pkl,我们可以使用canisius.edu/~yany/python/ffMonthly.pkl

文件名 描述
ibm3factor.pkl IBM 的 FF 三因子模型的简单数据集
ffMonthly.pkl Fama-French 月度三因子数据
ffMomMonthly.pkl 月度动量因子
ffcMonthly.pkl Fama-French-Carhart 月度四因子数据
ffMonthly5.pkl Fama-French 月度五因子数据
yanMonthly.pkl 作者生成的月度数据集
ffDaily.pkl Fama-French-Carhart 日度四因子数据
ffcDaily.pkl Fama-French 日度五因子数据
ffDaily5.pkl Fama-French 月度四因子数据
usGDPquarterly.pkl 美国季度 GDP 数据
usDebt.pkl 美国国债水平
usCPImonthly.pkl 消费者价格指数(CPI)数据
tradingDaysMonthly.pkl 月度数据的交易日数据
tradingDaysDaily.pkl 日度数据的交易日数据
businessCycleIndicator.pkl 商业周期指标
businessCycleIndicator2.pkl 另一个商业周期指标
uniqueWordsBible.pkl 圣经中的所有唯一单词

一个示例代码如下所示:

import pandas as pd
x=pd.read_pickle("c:/temp/ffMonthly.pkl")
print(x.head())
print(x.tail())

输出结果如下所示:

附录 A – 相关 Python 数据集列表

附录 B – 生成 ffMonthly.pkl 的 Python 程序

以下程序用于生成名为 ffMonthly.pkl 的数据集:

import scipy as sp
import numpy as np
import pandas as pd
file=open("c:/temp/ffMonthly.txt","r")
data=file.readlines()
f=[]
index=[]
for i in range(4,sp.size(data)):
print(data[i].split())
t=data[i].split()
index.append(pd.to_datetime(t[0]+'01', format='%Y%m%d').date())
#index.append(int(t[0]))
for j in range(1,5):
k=float(t[j])
f.append(k/100)
n=len(f) 
f1=np.reshape(f,[n/4,4])
ff=pd.DataFrame(f1,index=index,columns=['MKT_RF','SMB','HML','Rf'])
ff.to_pickle("c:/temp/ffMonthly.pkl")

这里显示的是前几行和最后几行:

附录 B – 生成 ffMonthly.pkl 的 Python 程序

附录 C – Sharpe 比率的 Python 程序

def sharpeRatio(ticker,begdate=(2012,1,1),enddate=(2016,12,31)):
    Objective: estimate Sharpe ratio for stock
        ticker  : stock symbol 
        begdate : beginning date
        enddate : ending date

       Example #1: sharpeRatio("ibm")
                     0.0068655583807256159

       Example #2: date1=(1990,1,1)
                   date2=(2015,12,23)
                   sharpeRatio("ibm",date1,date2)
                     0.027831010497755326

    import scipy as sp
    from matplotlib.finance import quotes_historical_yahoo_ochl as getData
    p = getData(ticker,begdate, enddate,asobject=True,adjusted=True)
    ret=p.aclose[1:]/p.aclose[:-1]-1
    return sp.mean(ret)/sp.std(ret) 

附录 D – 数据案例 #4 – 哪个模型最佳,CAPM、FF3、FFC4、FF5,还是其他?

目前,我们有许多资产定价模型。最重要的模型包括 CAPM、Fama-French 三因子模型、Fama-French-Carhart 四因子模型或 Fama-French 五因子模型。本数据案例的目标包括以下内容:

  • 熟悉数据下载方法

  • 理解 T 值、F 值和调整后的 R2

  • 编写各种 Python 程序进行测试

这四个模型的定义 CAPM:

附录 D – 数据案例 #4 – 哪个模型最佳,CAPM、FF3、FFC4、FF5,还是其他?

Fama-French 三因子模型:

附录 D – 数据案例 #4 – 哪个模型最佳,CAPM、FF3、FFC4、FF5,还是其他?

Fama-French-Carhart 四因子模型:

附录 D – 数据案例 #4 – 哪个模型最佳,CAPM、FF3、FFC4、FF5,还是其他?

Fama-French 五因子模型:

附录 D – 数据案例 #4 – 哪个模型最佳,CAPM、FF3、FFC4、FF5,还是其他?

在前面的方程中,RMV 是具有强劲和薄弱盈利能力的多元化股票投资组合回报率的差异,CMA 是低投资和高投资公司股票多元化投资组合回报率的差异,Fama 和 French 将其称为保守型和激进型。如果五个因子的曝露能够捕捉到预期回报的所有变动,那么所有证券和投资组合的截距应该为零。数据来源如下:

若干问题:

  • 使用哪个标准?

  • 性能是否与时间段无关?

  • 样本内估计与样本外预测

参考文献

请参考以下文章:

  • Carhart, Mark M., 1997, On Persistence in Mutual Fund Performance, Journal of Finance 52, 57-82

  • Fama, Eugene 和 Kenneth R. French, 2015, A five-factor asset pricing model, Journal of Financial Economics 116, 1, 1-22

  • Fama, Eugene 和 Kenneth R. French, 1993, Common risk factors in the returns on stocks and bonds, Journal of Financial Economics 33, 3056

  • Fama, Eugene 和 Kenneth R. French, 1992, The cross-section of expected stock returns, Journal of Finance 47, 427-465

  • Jegadeesh, N., & Titman, S., 1993, Returns to buying winners and selling losers: Implications for stock market efficiency, Journal of Finance 48(1): 65–91

  • Sharpe, W. F., 1966, Mutual Fund Performance, Journal of Business 39 (S1), 119–138

  • Sharpe, William F., 1994, The Sharpe Ratio, The Journal of Portfolio Management 21 (1), 49–58

  • Sortino, F.A., Price, L.N., 1994, Performance measurement in a downside risk framework, Journal of Investing 3, 50–8

  • Treynor, Jack L., 1965, How to Rate Management of Investment Funds, Harvard Business Review 43, pp. 63–75

练习

  1. CAPM 和 Fama-French 三因子模型有什么不同?

  2. 在 Fama-French 三因子模型中,SMB 和 HML 的含义是什么?

  3. 在 Fama-French-Carhart 四因子模型中,MOM 代表什么?

  4. 在 Fama-French 五因子模型中,RMW 和 CMA 的含义是什么?

  5. 运行多因子模型时,R2 和调整后的 R2 有什么区别?

  6. 我们可以使用多少种 OLS 函数?请提供至少两个来自不同 Python 模块的函数。

  7. rolling_kurt函数包含在哪个模块中?如何使用这个函数?

  8. 基于从 Yahoo! Finance 下载的每日数据,通过运行 CAPM 和 Fama-French 三因子模型,查找 IBM 过去五年的结果。哪个模型更好?

  9. 什么是动量因子?如何运行 Fama-French-Carhart 四因子模型?请使用几个股票代码进行示例。

  10. Fama-French 五因子模型的定义是什么?如何在花旗集团运行它?该金融机构的股票代码是 C。

  11. 对以下股票代码进行回归分析:IBM, DELL, WMT, ^GSPC, C, A, AA 和 MOFT,分别基于 CAPM、FF3、FFC4 和 FF5 进行回归。哪个模型最好?讨论你用来比较的基准或标准。

  12. 编写一个 Python 程序,根据 Fama-French 三因子模型每年估算滚动贝塔值。用它来显示 IBM 从 1962 年到 2016 年的年度贝塔值。

  13. 更新以下 Python 数据集。原始数据集可以从作者的网页下载。例如,为了下载第一个数据集ffMonthly.pkl,请访问canisius.edu/~yany/python/ffMonthly.pkl

    ffMonthly.pkl Fama-French 每月三因子数据
    ffcMonthly.pkl Fama-French-Carhart 每月四因子数据
    ffMonthly5.pkl Fama-French 每月五因子数据
  14. 数据来源: mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html

  15. 更新以下 Python 数据集:

    usGDPannual.pkl 美国 GDP 年数据
    usCPImonthly.pkl CPI(消费者价格指数)月数据
  16. Fama-French SMH 可以视为一个投资组合。下载每日和每月的 SMB 数据。然后估计 10 年、20 年和 30 年期间的总回报。比较每对总回报之间的差异。例如,比较 1980 到 2000 年期间基于每日 SML 和每月 SML 的总回报。为什么它们会不同?

  17. 对市场回报做同样的事情并与 SML 进行比较。为什么市场的差异比 SML 投资组合的差异要小得多?

  18. 对 HML 做同样的事情并解释。

  19. 有多少种方法可以合并两个数据集?

  20. 如果我们有两个按股票代码和日期排序的数据集,如何合并它们?

  21. 编写一个函数来估计 Treynor 比率。函数的格式为treynorRatio(ticker, rf, begdate, enddate),其中 ticker 是股票代码,例如 IBM,rf是无风险利率,begdate是开始日期,enddate是结束日期。

  22. 随机选择 10 只股票,如 IBM、C、WMT、MSFT 等,运行 CAPM 测试它们的截距是否为零。

  23. 编写一个 Python 程序来计算 Sortino 比率。程序的格式为sortinoRatio(ticker,rf,begdate,enddate)

  24. 如何使用 Python 和 CRSP 数据复制 Jagadeesh 和 Tidman(1993)动量策略?(假设你的学校有 CRSP 订阅)。

  25. 当使用statsmodels.api.ols()函数进行线性回归时,如果省略以下这一行会有什么后果?

    x = sm.add_constant(x)
    
  26. 调试以下用于估计 LPSD 的程序:

    import scipy as sp
    import numpy as np
    mean=0.08;Rf=0.01;std=0.12;n=100
    x=sp.random.normal(loc=mean,scale=std,size=n)
    y=x[x-Rf<0]  
    m=len(y)
    for i in sp.arange(m):
        total=0.0
        total+=(y[i]-Rf)**2
    LPSD=total/(m-1)
    

总结

本章我们讨论了多因子线性模型。这些模型可以看作是 CAPM(单一因子线性模型)的简单扩展。这些多因子模型包括 Fama-French 三因子模型、Fama-French-Carhart 四因子模型和 Fama-French 五因子模型。

在下一章中,我们将讨论时间序列的各种特性。在金融和经济学中,我们的数据有很大一部分是时间序列格式的,如股价和国内生产总值GDP),或者股票的月度或日度历史价格。对于时间序列,存在许多问题,如如何从历史价格数据中估计回报,如何合并具有相同或不同频率的数据集,季节性,以及自相关的检测。理解这些特性对于我们的知识发展至关重要。

第八章:时间序列分析

在金融学和经济学中,我们的大量数据都是时间序列格式,例如股票价格和国内生产总值GDP)。从第四章,数据来源中可以看出,我们可以从 Yahoo!Finance 下载日、周和月的历史价格时间序列。从美联储经济数据图书馆FRED),我们可以提取许多历史时间序列数据,如 GDP。对于时间序列数据,存在许多问题,例如如何从历史价格数据估算回报,如何合并具有相同或不同频率的数据集,季节性,以及检测自相关。理解这些属性对于我们的知识发展至关重要。

本章将涵盖以下主题:

  • 时间序列分析简介

  • 设计一个好的日期变量,并按日期合并不同的数据集

  • 正态分布和正态性检验

  • 利率期限结构、52 周最高和最低交易策略

  • 回报估算和将日回报转换为月回报或年回报

  • T 检验、F 检验和自相关的 Durbin-Watson 检验

  • Fama-MacBeth 回归

  • Roll(1984)扩展,Amihud(2002)流动性不足指标,以及 Pastor 和 Stambaugh(2003)流动性度量

  • 一月效应和星期几效应

  • 从 Google Finance 和哈斯布鲁克教授的 TORQ 数据库(交易、订单、报告和报价)中提取高频数据

  • 介绍 CRSP(证券价格研究中心)数据库

时间序列分析简介

大多数金融数据都是时间序列格式,以下是几个示例。第一个示例展示了如何从 Yahoo!Finance 下载给定股票代码的历史每日股票价格数据,包括起始和结束日期:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
x = getData("IBM",(2016,1,1),(2016,1,21),asobject=True, adjusted=True)
print(x[0:4])

输出如下所示:

时间序列分析简介

数据类型是numpy.recarray,如type(x)所示。第二个示例打印了两个名为ffMonthly.pklusGDPquarterly.pkl的数据集中的前几个观察值,两个数据集都可以从作者网站获取,例如canisius.edu/~yany/python/ffMonthly.pkl

import pandas as pd
GDP=pd.read_pickle("c:/temp/usGDPquarterly.pkl")
ff=pd.read_pickle("c:/temp/ffMonthly.pkl")
print(GDP.head())
print(ff.head())

相关输出如下所示:

时间序列分析简介

有一个章节末的问题,旨在将离散数据与日常数据合并。以下程序从 Google Finance 提取每日价格数据:

import pandas_datareader.data as web
import datetime
ticker='MSFT'
begdate = datetime.datetime(2012, 1, 2)
enddate = datetime.datetime(2017, 1, 10)
a = web.DataReader(ticker, 'google',begdate,enddate)
print(a.head(3))
print(a.tail(2))

对应的输出如下所示:

时间序列分析简介

为了获取当前的股票报价,我们有以下程序。请注意,输出结果为 2017 年 1 月 21 日的数据:

import pandas_datareader.data as web
ticker='AMZN'
print(web.get_quote_yahoo(ticker))

时间序列分析简介

通过使用下列 Python 程序,可以提取 1947 年 1 月到 2016 年 6 月的国内生产总值GDP)数据:

import pandas_datareader.data as web
import datetime
begdate = datetime.datetime(1900, 1, 1)
enddate = datetime.datetime(2017, 1, 27)
x= web.DataReader("GDP", "fred", begdate,enddate)
print(x.head(2))
print(x.tail(3))

输出如下所示:

时间序列分析简介

基于日期变量合并数据集

为了更好地管理我们的时间序列,生成一个date变量是一个很好的主意。谈到这样的变量时,读者可以考虑年份(YYYY)、年份和月份(YYYYMM)或年份、月份和日期(YYYYMMDD)。对于仅包含年份、月份和日期的组合,我们可以有多种形式。以 2017 年 1 月 20 日为例,我们可以有 2017-1-20、1/20/2017、20Jan2017、20-1-2017 等。从某种意义上讲,真正的日期变量在我们看来应该是易于操作的。通常,真正的date变量采取年-月-日或其变种的其他形式。假设日期变量的值为 2000-12-31,在其值上加一天后,结果应为 2001-1-1。

使用 pandas.date_range() 生成一维时间序列

我们可以轻松使用pandas.date_range()函数生成我们的时间序列;请参见以下示例:

import pandas as pd
import scipy as sp
sp.random.seed(1257)
mean=0.10
std=0.2
ddate = pd.date_range('1/1/2016', periods=252) 
n=len(ddate)
rets=sp.random.normal(mean,std,n)
data = pd.DataFrame(rets, index=ddate,columns=['RET'])
print(data.head())

在前面的程序中,由于使用了sp.random.seed()函数,如果读者使用相同的种子,则应获得相同的输出。输出如下所示:

                 RET
2016-01-01  0.431031
2016-01-02  0.279193
2016-01-03  0.002549
2016-01-04  0.109546
2016-01-05  0.068252

为了更方便地处理时间序列数据,在以下程序中使用了pandas.read_csv()函数,参见以下代码:

import pandas as pd
url='http://canisius.edu/~yany/data/ibm.csv' 
x=pd.read_csv(url,index_
col=0,parse_dates=True)
print(x.head())

输出如下所示:

使用 pandas.date_range() 生成一维时间序列

为了查看日期格式,我们有以下代码:

>>>x[0:1]

使用 pandas.date_range() 生成一维时间序列

>>>x[0:1].index

使用 pandas.date_range() 生成一维时间序列

在以下程序中,应用了matplotlib.finance.quotes_historical_yahoo_ochl()函数:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
x = getData("IBM",(2016,1,1),(2016,1,21),asobject=True, adjusted=True)
print(x[0:4])

输出如下所示:

使用 pandas.date_range() 生成一维时间序列

请注意,索引是日期格式,参见以下代码。有关.strftime("%Y")的含义,请参见表 8.2

>>>x[0][0]
  datetime.date(2016, 1, 4)
>>>x[0][0].strftime("%Y")
 '2016'

这里有几种定义date变量的方法:

函数 描述 示例
pandas.date_range 1. 生成一系列日期 pd.date_range('1/1/2017', periods=252)
datetime.date 2. 一天 >>>from datetime import datetime``>>>datetime.date(2017,1,20)
datetime.date.today() 3. 获取今天的日期 >>>datetime.date.today()``datetime.date(2017, 1, 26)
datetime.now() 4. 获取当前时间 >>>from datetime import datetime``>>>datetime.now()``datetime.datetime(2017, 1, 26, 8, 58, 6, 420000)
relativedelta() 5. 向日期变量添加一定数量的天、月或年 >>>from datetime import datetime``>>>today=datetime.today().date()``>>>print(today)``2017-01-26``>>>print(today+relativedelta(days=31))``2017-02-26

date变量中提取年份、月份和日期在处理时间序列时非常常见——请参见以下使用strftime()函数的 Python 程序。相应的输出显示在右侧面板中。年份、月份和日期的结果格式是字符串:

import datetime
today=datetime.date.today()
year=today.strftime("%Y")
year2=today.strftime("%y")
month=today.strftime("%m")
day=today.strftime("%d")
print(year,month,day,year2)
('2017', '01', '24', '17')

以下表格总结了它的用法。欲了解更多详细信息,请查看链接:strftime.org/

函数 描述 示例
.strftime("%Y") 1. 四位数字的年份字符串 a=datetime.date(2017,1,2)``a.strftime("%Y")
.strftime("%y") 2. 两位数字的年份字符串 a.strftime("%y")
.strftime("%m") 3. 月份字符串 a.strftime("%m")
.strftime("%d") 4. 日期字符串 a.strftime("%d")

回报估算

有了价格数据,我们可以计算回报。此外,有时我们需要将日回报转换为周回报或月回报,或者将月回报转换为季回报或年回报。因此,理解如何估算回报及其转换是至关重要的。假设我们有以下四个价格:

>>>p=[1,1.1,0.9,1.05]

了解这些价格是如何排序的非常重要。如果第一个价格发生在第二个价格之前,那么第一个回报应该是(1.1-1)/1=10%。接下来,我们学习如何从一个n条记录的数组中提取前n-1条和后n-1条记录。要列出前n-1个价格,我们使用p[:-1],而对于最后三个价格,我们使用p[1:],如以下代码所示:

>>>print(p[:-1]) 
>>>print(p[1:]) 
[ 1\. 1.1 0.9] 
[ 1.1 0.9 1.05]

要估算回报,我们可以使用以下代码:

>>>ret=(p[1:]-p[:-1])/p[:-1] 
>>>print(ret )
[ 0.1 -0.18181818 0.16666667]

当给定两个价格 x1x2,并假设 x2x1 之后,我们可以使用 ret=(x2-x1)/x1。或者,我们可以使用 ret=x2/x1-1。因此,对于前面的例子,我们可以使用 ret=p[1:]/p[:-1]-1。显然,这种第二种方法可以避免某些输入错误。另一方面,如果价格顺序颠倒了,例如,第一个是最新价格,最后一个是最旧价格,那么我们必须以以下方式估算回报:

>>>ret=p[:-1]/p[1:]-1 
>>>print(ret )
[-0.09090909 0.22222222 -0.14285714] 
>>>

正如在第七章中提到的,多因子模型与绩效评估,我们可以使用.diff().shift()函数来估算回报。请参见以下代码:

import pandas as pd
import scipy as sp
p=[1,1.1,0.9,1.05] 
a=pd.DataFrame({'Price':p})
a['Ret']=a['Price'].diff()/a['Price'].shift(1)
print(a)

输出如下所示:

Price       Ret
0   1.00       NaN
1   1.10  0.100000
2   0.90 -0.181818
3   1.05  0.166667

以下代码演示了如何从 Yahoo!Finance 下载日价格数据并估算日回报:

>>>from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
>>>ticker='IBM' 
>>>begdate=(2013,1,1) 
>>>enddate=(2013,11,9) 
>>>x =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
>>>ret=x.aclose[1:]/x.aclose[:-1]-1

第一行从matplotlib.finance上传一个函数。我们使用tuple数据类型定义开始和结束日期。下载的历史日价格数据被赋值给x。为了验证我们的回报是否正确估算,我们可以打印几个价格到屏幕上。然后,我们可以手动验证一个或两个回报值,如以下代码所示:

>>>x.date[0:3] 
array([datetime.date(2013, 1, 2), datetime.date(2013, 1, 3), 
datetime.date(2013, 1, 4)], dtype=object) 
>>>x.aclose[0:3] 
array([ 192.61, 191.55, 190.3 ]) 
>>>ret[0:2] 
array([-0.00550335, -0.00652571]) 
>>>(191.55-192.61)/192.61 
-0.005503348735787354 
>>>

是的,最后的结果确认我们的第一个回报是正确估算的。

将日回报转换为月回报

有时,我们需要将日收益率转换为月度或年度收益率。以下是我们的操作步骤。首先,我们估算每日对数收益率。然后,我们对每个月内的所有日对数收益率进行求和,以计算出相应的月度对数收益率。最后一步是将月度对数收益率转换为月度百分比收益率。假设我们有 p0, p1, p2, …., p20 的价格数据,其中 p0 是上个月最后一个交易日的价格,p1 是本月第一个交易日的价格,p20 是本月最后一个交易日的价格。那么,本月的百分比收益率可以表示为:

将日收益率转换为月收益率

月度对数收益率的定义如下:

将日收益率转换为月收益率

月度百分比收益率与月度对数收益率之间的关系如下所示:

将日收益率转换为月收益率

每日对数收益率的定义类似如下:

将日收益率转换为月收益率

我们来看一下以下对数收益率的求和:

将日收益率转换为月收益率

根据之前的步骤,以下 Python 程序将日收益率转换为月收益率:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd 
#
ticker='IBM' 
begdate=(2013,1,1) 
enddate=(2013,11,9)
#
x = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
logret = np.log(x.aclose[1:]/x.aclose[:-1])
yyyymm=[]
d0=x.date
#
for i in range(0,np.size(logret)): 
    yyyymm.append(''.join([d0[i].strftime("%Y"),d0[i].strftime("%m")]))

y=pd.DataFrame(logret,yyyymm,columns=['retMonthly']) 
retMonthly=y.groupby(y.index).sum()

print(retMonthly.head())

输出结果如下:

将日收益率转换为月收益率

按日期合并数据集

以下程序将 IBM 的每日调整收盘价与每日 Fama-French 3 因子时间序列进行合并。ffMonthly.pkl 文件可在以下网址获取:canisius.edu/~yany/python/ffDaily.pkl

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np 
import pandas as pd 
ticker='IBM' 
begdate=(2016,1,2) 
enddate=(2017,1,9) 
x =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
myName=ticker+'_adjClose'
x2=pd.DataFrame(x['aclose'],x.date,columns=[myName]) 
ff=pd.read_pickle('c:/temp/ffDaily.pkl') 
final=pd.merge(x2,ff,left_index=True,right_index=True)
print(final.head())

输出结果如下:

            IBM_adjClose  MKT_RF     SMB     HML   RF
2016-01-04    130.959683 -0.0159 -0.0083  0.0053  0.0
2016-01-05    130.863362  0.0012 -0.0021  0.0000  0.0
2016-01-06    130.208315 -0.0135 -0.0013  0.0001  0.0
2016-01-07    127.983111 -0.0244 -0.0028  0.0012  0.0
2016-01-08    126.798264 -0.0111 -0.0047 -0.0004  0.0

理解插值技术

插值是一种在金融领域中非常常用的技术。在以下示例中,我们需要替换 2 和 6 之间的两个缺失值 NaN。我们使用 pandas.interpolate() 函数进行线性插值,来填补这两个缺失值:

import pandas as pd 
import numpy as np 
nn=np.nan
x=pd.Series([1,2,nn,nn,6]) 
print(x.interpolate())

输出结果如下:

0    1.000000
1    2.000000
2    3.333333
3    4.666667
4    6.000000
dtype: float64

上述方法是一种线性插值。实际上,我们可以估算一个Δ,并手动计算这些缺失值:

理解插值技术

这里,v2(v1) 是第二个(第一个)值,n 是这两个值之间的间隔数。对于前面的例子,Δ 为 (6-2)/3=1.33333。因此,下一个值将是 v1+Δ=2+1.33333=3.33333。通过这种方式,我们可以不断估算所有缺失的值。注意,如果有多个时期的值缺失,那么每个时期的Δ必须手动计算,以验证方法的正确性。从 Yahoo!Finance 的债券页面 finance.yahoo.com/bonds,我们可以获得以下信息:

到期日 收益率 昨日 上周 上月
3 个月 0.05 0.05 0.04 0.03
6 个月 0.08 0.07 0.07 0.06
2 年 0.29 0.29 0.31 0.33
3 年 0.57 0.54 0.59 0.61
5 年 1.34 1.32 1.41 1.39
10 年 2.7 2.66 2.75 2.66
30 年 3.8 3.78 3.85 3.72

表 8.3 期限结构利率

基于表格数据,我们有如下代码:

>>>import numpy as np
>>>import pandas as pd
>>>nn=np.nan
>>>x=pd.Series([0.29,0.57,nn,1.34,nn,nn,nn,nn,2.7])
>>>y=x.interpolate()
>>>print(y)
0 0.290
1 0.570
2 0.955
3 1.340
4 1.612
5 1.884
6 2.156
7 2.428
8 2.700
dtype: float64
>>>

合并不同频率的数据

以下 Python 程序合并了两个数据集:美国国内生产总值GDP)数据(按季度频率)和ffMonthlycanisius.edu/~yany/python/ffMonthly.pkl(按月度频率)。

前述的插值方法应用于 GDP 数据中缺失的月份。假设ffMonthly数据集已保存在c:/temp/目录中:

import pandas as pd
import pandas_datareader.data as web
import datetime
begdate = datetime.datetime(1900, 1, 1)
enddate = datetime.datetime(2017, 1, 27)
GDP= web.DataReader("GDP", "fred", begdate,enddate)
ff=pd.read_pickle("c:/temp/ffMonthly.pkl")
final=pd.merge(ff,GDP,left_index=True,right_index=True,how='left') 
tt=final['GDP']
GDP2=pd.Series(tt).interpolate()
final['GDP2']=GDP2

输出如下所示。由于 1947 年之前没有 GDP 数据,且ffMonthly时间序列从 1926 年 7 月开始,因此合并数据后的最后几项观测结果提供了更多的信息:

print(final.head())
print(final.tail(10))
        MKT_RF     SMB     HML      RF      GDP  GDP2
1926-07-01  0.0296 -0.0230 -0.0287  0.0022  NaN   NaN
1926-08-01  0.0264 -0.0140  0.0419  0.0025  NaN   NaN
1926-09-01  0.0036 -0.0132  0.0001  0.0023  NaN   NaN
1926-10-01 -0.0324  0.0004  0.0051  0.0032  NaN   NaN
1926-11-01  0.0253 -0.0020 -0.0035  0.0031  NaN   NaN
            MKT_RF     SMB     HML      RF      GDP          GDP2
2016-02-01 -0.0007  0.0083 -0.0048  0.0002      NaN  18337.766667
2016-03-01  0.0696  0.0086  0.0111  0.0002      NaN  18393.933333
2016-04-01  0.0092  0.0068  0.0325  0.0001  18450.1  18450.100000
2016-05-01  0.0178 -0.0027 -0.0179  0.0001      NaN  18525.166667
2016-06-01 -0.0005  0.0061 -0.0149  0.0002      NaN  18600.233333
2016-07-01  0.0395  0.0290 -0.0098  0.0002  18675.3  18675.300000
2016-08-01  0.0050  0.0094  0.0318  0.0002      NaN  18675.300000
2016-09-01  0.0025  0.0200 -0.0134  0.0002      NaN  18675.300000
2016-10-01 -0.0202 -0.0440  0.0415  0.0002      NaN  18675.300000
2016-11-01  0.0486  0.0569  0.0844  0.0001      NaN  18675.300000
2016-07-01  0.0395  0.0290 -0.0098  0.0002  18675.3  18675.300000
2016-08-01  0.0050  0.0094  0.0318  0.0002      NaN  18675.300000
2016-09-01  0.0025  0.0200 -0.0134  0.0002      NaN  18675.300000
2016-10-01 -0.0202 -0.0440  0.0415  0.0002      NaN  18675.300000
2016-11-01  0.0486  0.0569  0.0844  0.0001      NaN  18675.300000

对于第二个例子,我们合并了一个商业周期指标,名为businessCycle.pkl,其可在canisius.edu/~yany/python/businessCycle.pkl下载,该数据集按月度频率提供,且与 GDP(按季度频率)合并。请参见以下代码:

import pandas as pd
import pandas_datareader.data as web
import datetime
import scipy as sp
import numpy as np
cycle=pd.read_pickle("c:/temp/businessCycle.pkl")
begdate = datetime.datetime(1947, 1, 1)
enddate = datetime.datetime(2017, 1, 27)
GDP= web.DataReader("GDP", "fred", begdate,enddate)
final=pd.merge(cycle,GDP,left_index=True,right_index=True,how='right')

我们可以打印几行来查看结果:

print(cycle.head())
print(GDP.head())
print(final.head())
          cycle
date             
1926-10-01  1.000
1926-11-01  0.846
1926-12-01  0.692
1927-01-01  0.538
1927-02-01  0.385
1947-07-01  0.135  250.1
1947-10-01  0.297  260.3
1948-01-01  0.459  266.2
              GDP
DATE             
1947-01-01  243.1
1947-04-01  246.3
1947-07-01  250.1
1947-10-01  260.3
1948-01-01  266.2
            cycle    GDP
DATE                    
1947-01-01 -0.189  243.1
1947-04-01 -0.027  246.3

正态性检验

在金融学中,关于正态分布的知识非常重要,原因有二。首先,股票回报被假设为遵循正态分布。其次,良好的计量经济模型中的误差项应遵循零均值的正态分布。然而,在现实世界中,这可能不适用于股票。另一方面,股票或投资组合是否遵循正态分布,可以通过各种所谓的正态性检验来验证。Shapiro-Wilk 检验就是其中之一。对于第一个例子,随机数是从正态分布中抽取的。因此,检验应确认这些观测值遵循正态分布:

from scipy import stats 
import scipy as sp
sp.random.seed(12345)
mean=0.1
std=0.2
n=5000
ret=sp.random.normal(loc=0,scale=std,size=n)
print 'W-test, and P-value' 
print(stats.shapiro(ret))
W-test, and P-value
(0.9995986223220825, 0.4129064679145813)

假设我们的置信度为 95%,即 alpha=0.05。结果的第一个值是检验统计量,第二个值是其对应的 P 值。由于 P 值非常大,远大于 0.05,我们接受零假设,即回报符合正态分布。对于第二个例子,随机数是从均匀分布中抽取的:

from scipy import stats
import scipy as sp
sp.random.seed(12345)
n=5000
ret=sp.random.uniform(size=n)
print 'W-test, and P-value' 
print(stats.shapiro(ret)) 
W-test, and P-value
(0.9537619352340698, 4.078975800593137e-37)

由于 P 值接近零,我们拒绝零假设。换句话说,这些观测值不符合正态分布。第三个例子验证了 IBM 的回报是否符合正态分布。使用了 Yahoo! Finance 的过去五年的每日数据进行检验。零假设是 IBM 的每日回报来自于正态分布:

from scipy import stats 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
import numpy as np 

ticker='IBM' 
begdate=(2012,1,1) 
enddate=(2016,12,31) 

p =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
ret = (p.aclose[1:] - p.aclose[:-1])/p.aclose[1:] 
print 'ticker=',ticker,'W-test, and P-value' 
print(stats.shapiro(ret))
ticker= IBM W-test, and P-value
(0.9213278889656067, 4.387053202198418e-25)

由于此 P 值接近零,我们拒绝原假设。换句话说,我们得出结论:IBM 的日收益不服从正态分布。对于正态性检验,我们还可以应用安德森-达林检验,这是科尔莫哥洛夫-斯米尔诺夫检验的改进版,用于验证观测值是否服从特定分布。请参见以下代码:

print( stats.anderson(ret) )
AndersonResult(statistic=12.613658863646833, critical_values=array([ 0.574,  0.654,  0.785,  0.915,  1.089]), significance_level=array([ 15\. ,  10\. ,   5\. ,   2.5,   1\. ]))

在这里,我们有三组值:安德森-达林检验统计量、一组临界值和一组相应的置信水平,例如 15%、10%、5%、2.5%和 1%,如前面的输出所示。如果选择 1%的置信水平——第三组的最后一个值——临界值为 1.089,即第二组的最后一个值。由于我们的检验统计量是 12.61,远高于临界值 1.089,我们拒绝原假设。因此,我们的安德森-达林检验得出的结论与 Shapiro-Wilk 检验的结论相同。scipy.stats.anderson()检验的一个优点是,我们可以检验其他分布类型。应用help()函数后,我们将得到以下列表。默认的分布是用于正态性检验:

>>>from scipy import stats 
>>>help(stats.anderson)
anderson(x, dist='norm')
Anderson-Darling test for data coming from a particular distribution
dist : {'norm','expon','logistic','gumbel','extreme1'}, optional the type of distribution to test against.  The default is 'norm'  and 'extreme1' is a synonym for 'gumbel'

估算胖尾

正态分布的一个重要性质是,我们可以使用均值和标准差这两个参数,即前两个矩,来完全定义整个分布。对于某个证券的n个收益,其前四个矩如公式(1)所示。均值或平均值定义如下:

估算胖尾

它的(样本)方差由以下公式定义。标准差,即σ,是方差的平方根:

估算胖尾

偏度由以下公式定义,表示分布是偏向左侧还是右侧。对于对称分布,偏度为零:

估算胖尾

峰度反映了极端值的影响,因为它的幂次为四。它有两种定义方法,分别有和没有减去三;参见以下两个公式。公式(10B)中减去三的原因是,对于正态分布,根据公式(10A)计算的峰度为三:

估算胖尾

一些书籍通过称公式(10B)为超额峰度来区分这两个公式。然而,基于公式(10B)的许多函数仍被称为峰度。我们知道,标准正态分布具有零均值、单位标准差、零偏度和零峰度(基于公式 10B)。以下输出验证了这些事实:

from scipy import stats,random
import numpy as np
np.random.seed(12345)
ret = random.normal(0,1,500000)
print('mean    =', np.mean(ret))
print('std     =',np.std(ret))
print('skewness=',stats.skew(ret))
print('kurtosis=',stats.kurtosis(ret))

相关输出如下所示。请注意,由于应用了scipy.random.seed()函数,读者在使用相同的种子 12345 时应该得到相同的结果:

估算胖尾

均值、偏度和峰度都接近零,而标准差接近一。接下来,我们基于 S&P500 的每日回报估算其四个矩:

from scipy import stats
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
ticker='^GSPC'
begdate=(1926,1,1)
enddate=(2016,12,31)
p = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
ret = p.aclose[1:]/p.aclose[:-1]-1
print( 'S&P500  n       =',len(ret))
print( 'S&P500  mean    =',round(np.mean(ret),8))
print('S&P500  std     =',round(np.std(ret),8))
print('S&P500  skewness=',round(stats.skew(ret),8))
print('S&P500  kurtosis=',round(stats.kurtosis(ret),8))

这五个值的输出,包括观测数量,结果如下:

估算厚尾

这一结果与 Cook Pine Capital(2008)撰写的《厚尾风险研究》论文中的结果非常接近。基于相同的论点,我们得出结论,SP500 的每日回报是左偏的,也就是说,存在负偏度,并且具有厚尾(峰度为 20.81,而非零)。

T 检验和 F 检验

在金融学中,T 检验可以被视为最广泛使用的统计假设检验之一,若原假设成立,检验统计量将服从学生 t 分布。我们知道标准正态分布的均值为零。在以下程序中,我们从标准正态分布中生成 1,000 个随机数。然后,我们进行两个检验:检验均值是否为 0.5,以及检验均值是否为零:

>>>from scipy import stats 
>>>import numpy as np
>>>np.random.seed(1235) 
>>>x = stats.norm.rvs(size=10000) 
>>>print("T-value P-value (two-tail)") 
>>>print(stats.ttest_1samp(x,0.5)) 
>>>print(stats.ttest_1samp(x,0)) 
T-value P-value (two-tail)
Ttest_1sampResult(statistic=-49.763471231428966, pvalue=0.0)
Ttest_1sampResult(statistic=-0.26310321925083019, pvalue=0.79247644375164861)

对于第一个检验,我们测试时间序列的均值是否为 0.5,由于 T 值为 49.76 且 P 值为 0,故我们拒绝原假设。对于第二个检验,由于 T 值接近 -0.26 且 P 值为 0.79,我们接受原假设。在下面的程序中,我们测试 IBM 2013 年每日回报的均值是否为零:

from scipy import stats 
import scipy as sp
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
ticker='ibm' 
begdate=(2013,1,1) 
enddate=(2013,12,31) 
p=getData(ticker,begdate,enddate,asobject=True, adjusted=True) 
ret=p.aclose[1:]/p.aclose[:-1]-1
print(' Mean T-value P-value ' ) 
print(round(sp.mean(ret),5), stats.ttest_1samp(ret,0))
Mean T-value P-value 
(-4e-05, Ttest_1sampResult(statistic=-0.049698422671935881, pvalue=0.96040239593479948))

从之前的结果来看,我们知道 IBM 的平均每日回报率是 0.00004%。T 值为 -0.049,P 值为 0.96。因此,我们接受原假设,即每日平均回报在统计上与零相同。

方差齐性检验

接下来,我们测试 2012 到 2016 年期间,IBM 和 DELL 的两个方差是否相同。名为 sp.stats.bartlet() 的函数执行 Bartlett 方差齐性检验,原假设为所有输入样本来自方差相等的总体。输出结果为 T 值和 P 值:

import scipy as sp 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
begdate=(2012,1,1) 
enddate=(2016,12,31) 
def ret_f(ticker,begdate,enddate): 
    p = getData(ticker,begdate, enddate,asobject=True,adjusted=True) 
    return p.aclose[1:]/p.aclose[:-1]-1
y=ret_f('IBM',begdate,enddate) 
x=ret_f('DELL',begdate,enddate) 
print(sp.stats.bartlett(x,y)) 
BartlettResult(statistic=108.07747537504794, pvalue=2.5847436899908763e-25)

T 值为 108,P 值为 0,我们得出结论,这两只股票在 2012 到 2016 年期间的每日股票回报具有不同的方差,并且无论显著性水平如何,都会有显著差异。

测试一月效应

在本节中,我们使用 IBM 的数据来测试所谓的 一月效应,即一月的股票回报在统计上不同于其他月份的回报。首先,我们从 Yahoo! Finance 收集 IBM 的每日价格数据。然后,我们将每日回报转化为月度回报。之后,我们将所有的月度回报分为两组:一月的回报与其他月份的回报。

最后,我们测试组均值是否相等,代码如下所示:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
import numpy as np 
import scipy as sp 
import pandas as pd
from datetime import datetime 
ticker='IBM' 
begdate=(1962,1,1) 
enddate=(2016,12,31) 
x =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
logret = sp.log(x.aclose[1:]/x.aclose[:-1]) 
date=[] 
d0=x.date 
for i in range(0,sp.size(logret)): 
    t1=''.join([d0[i].strftime("%Y"),d0[i].strftime("%m"),"01"]) 
    date.append(datetime.strptime(t1,"%Y%m%d")) 

y=pd.DataFrame(logret,date,columns=['logret']) 
retM=y.groupby(y.index).sum() 
ret_Jan=retM[retM.index.month==1] 
ret_others=retM[retM.index.month!=1] 
print(sp.stats.ttest_ind(ret_Jan.values,ret_others.values)) 
Ttest_indResult(statistic=array([ 1.89876245]), pvalue=array([ 0.05803291]))
>>>

由于 T 值为 1.89,P 值为 0.058,若以 IBM 为例并选择 5%的显著性水平,我们得出结论:没有 1 月效应。需要注意的是:我们不应该以此结果进行普遍推论,因为它仅基于一只股票。关于星期效应,我们可以应用相同的程序来测试其存在。章节末尾的一个问题旨在基于相同的逻辑测试星期效应。

52 周最高和最低交易策略

一些投资者/研究人员认为,我们可以通过采用 52 周最高和最低交易策略来建立一个多头仓位,如果今天的价格接近过去 52 周的最高价,建立一个反向仓位,如果今天的价格接近其 52 周最低价。让我们随机选择一个日期:2016 年 12 月 31 日。以下 Python 程序展示了这个 52 周的范围和今天的仓位:

import numpy as np
from datetime import datetime 
from dateutil.relativedelta import relativedelta 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
ticker='IBM' 
enddate=datetime(2016,12,31)
#
begdate=enddate-relativedelta(years=1) 
p =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
x=p[-1] 
y=np.array(p.tolist())[:,-1] 
high=max(y) 
low=min(y) 
print(" Today, Price High Low, % from low ") 
print(x[0], x[-1], high, low, round((x[-1]-low)/(high-low)*100,2))

相应的输出如下所示:

52 周最高和最低交易策略

根据 52 周最高和最低交易策略,我们今天有更多的动机购买 IBM 的股票。这个例子仅仅是展示如何做出决策。并没有进行任何测试来验证这是否是一个有利可图的交易策略。如果读者有兴趣测试这个 52 周最高和最低交易策略,应该使用所有股票组成两个投资组合。更多细节请参见 George 和 Huang(2004)。

估算 Roll 的价差

流动性被定义为我们可以多快地处置我们的资产而不失去其内在价值。通常,我们使用价差来表示流动性。然而,我们需要高频数据来估算价差。稍后在本章中,我们将展示如何直接利用高频数据来估算价差。为了基于每日观察间接衡量价差,Roll(1984)显示我们可以通过以下方式基于价格变动的序列协方差来估算价差:

估算 Roll 的价差

这里,S是 Roll 价差,Pt是股票在某一天的收盘价,

估算 Roll 的价差

是 Pt-Pt-1,且

估算 Roll 的价差

, t是估算期内的平均股价。以下 Python 代码使用来自 Yahoo! Finance 的一年期每日价格数据来估算 IBM 的 Roll 价差:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import scipy as sp 
ticker='IBM' 
begdate=(2013,9,1) 
enddate=(2013,11,11) 
data= getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
p=data.aclose 
d=sp.diff(p)
cov_=sp.cov(d[:-1],d[1:]) 
if cov_[0,1]<0: 
    print("Roll spread for ", ticker, 'is', round(2*sp.sqrt(-cov_[0,1]),3)) 
else: 
    print("Cov is positive for ",ticker, 'positive', round(cov_[0,1],3))

相应的输出如下所示:

估算 Roll 的价差

因此,在那段时间里,IBM 的 Roll 价差为$1.136。请看下面 Roll 模型的主要假设,

估算 Roll 的价差

估算 Roll 的价差

.

它们之间的协方差为负值。当协方差值为正时,Roll 模型将失效。在现实中,这种情况可能会发生在许多案例中。通常,实践者采用两种方法:当价差为负时,我们直接忽略这些情况,或者使用其他方法来估算价差。第二种方法是将正协方差前加上负号。

估算 Amihud 的非流动性

根据 Amihud (2002) 的观点,流动性反映了订单流对价格的影响。他的非流动性度量定义如下:

估算 Amihud 的非流动性

其中,illiq(t) 是月份 t 的 Amihud 非流动性度量,Ri 是第 i 天的日收益,Pi 是第 i 天的收盘价,Vi 是第 i 天的日交易量。由于非流动性是流动性的倒数,非流动性值越低,基础证券的流动性越高。首先,我们来看一下逐项分解:

>>>x=np.array([1,2,3],dtype='float') 
>>>y=np.array([2,2,4],dtype='float') 
>>>np.divide(x,y) 
array([ 0.5 , 1\. , 0.75]) 
>>>

在下面的代码中,我们根据 2013 年 10 月的交易数据估算了 IBM 的 Amihud 非流动性。结果是 1.2110^-11。这个值看起来非常小,实际上绝对值并不重要,重要的是相对值。如果我们在同一时期内估算 WMT 的非流动性,我们会得到 1.5210^-11 的值。由于 1.21 小于 1.52,我们得出结论,IBM 比 WMT 更具流动性。这一相关性在下面的代码中得以表示:

import numpy as np 
import statsmodels.api as sm 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
begdate=(2013,10,1) 
enddate=(2013,10,30) 
ticker='IBM'                   # or WMT  
data= getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
p=np.array(data.aclose) 
dollar_vol=np.array(data.volume*p) 
ret=np.array((p[1:] - p[:-1])/p[1:]) 
illiq=np.mean(np.divide(abs(ret),dollar_vol[1:])) 
print("Aminud illiq for =",ticker,illiq) 
'Aminud illiq for =', 'IBM', 1.2117639237103875e-11)
 ('Aminud illiq for =', 'WMT', 1.5185471291382207e-11)

估算 Pastor 和 Stambaugh (2003) 的流动性度量

基于 Campbell、Grossman 和 Wang (1993) 的方法论和实证证据,Pastor 和 Stambaugh (2003) 设计了以下模型来衡量个股的流动性和市场流动性:

估算 Pastor 和 Stambaugh (2003) 的流动性度量

这里,yt 是超额股票收益,Rt-Rft 是第 t 天,Rt 是股票的回报,Rf,t 是无风险利率,x1,t 是市场回报,x2,t 是签名美元交易量:

估算 Pastor 和 Stambaugh (2003) 的流动性度量

pt 是股价,t 是交易量。回归基于每月的每日数据进行。换句话说,对于每个月,我们会得到一个 β2,它被定义为个股的流动性度量。以下代码估算了 IBM 的流动性。首先,我们下载了 IBM 和 S&P500 的每日价格数据,估算它们的日收益,并进行合并:

import numpy as np 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np 
import pandas as pd 
import statsmodels.api as sm 
ticker='IBM' 
begdate=(2013,1,1) 
enddate=(2013,1,31) 

data =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
ret = data.aclose[1:]/data.aclose[:-1]-1 
dollar_vol=np.array(data.aclose[1:])*np.array(data.volume[1:]) 
d0=data.date 

tt=pd.DataFrame(ret,index=d0[1:],columns=['ret']) 
tt2=pd.DataFrame(dollar_vol,index=d0[1:],columns=['dollar_vol']) 
ff=pd.read_pickle('c:/temp/ffDaily.pkl') 
tt3=pd.merge(tt,tt2,left_index=True,right_index=True) 
final=pd.merge(tt3,ff,left_index=True,right_index=True) 
y=final.ret[1:]-final.RF[1:] 
x1=final.MKT_RF[:-1] 
x2=np.sign(np.array(final.ret[:-1]-final.RF[:-1]))*np.array(final.dollar_vol[:-1]) 
x3=[x1,x2] 
n=np.size(x3) 
x=np.reshape(x3,[n/2,2]) 
x=sm.add_constant(x) 
results=sm.OLS(y,x).fit() 
print(results.params)

在之前的程序中,y 是 IBM 在 t+1 时刻的超额收益,x1t 时刻的市场超额收益,x2t 时刻的签名美元交易量。x2 前的系数就是 Pastor 和 Stambaugh 的流动性度量。相应的输出如下所示:

const    2.702020e-03
x1      -1.484492e-13
x2       6.390822e-12
dtype: float64

Fama-MacBeth 回归

首先,让我们通过使用 pandas.ols 函数来看 OLS 回归,如下所示:

from datetime import datetime 
import numpy as np 
import pandas as pd 
n = 252 
np.random.seed(12345) 
begdate=datetime(2013, 1, 2) 
dateRange = pd.date_range(begdate, periods=n) 
x0= pd.DataFrame(np.random.randn(n, 1),columns=['ret'],index=dateRange) 
y0=pd.Series(np.random.randn(n), index=dateRange) 
print pd.ols(y=y0, x=x0)

对于 Fama-MacBeth 回归,我们有以下代码:

import numpy as np 
import pandas as pd 
import statsmodels.api as sm
from datetime import datetime 
#
n = 252 
np.random.seed(12345) 
begdate=datetime(2013, 1, 2) 
dateRange = pd.date_range(begdate, periods=n) 
def makeDataFrame(): 
    data=pd.DataFrame(np.random.randn(n,7),columns=['A','B','C','D','E',' F','G'],
    index=dateRange) 
    return data 
#
data = { 'A': makeDataFrame(), 'B': makeDataFrame(), 'C': makeDataFrame() }
Y = makeDataFrame() 
print(pd.fama_macbeth(y=Y,x=data))

Durbin-Watson

Durbin-Watson 统计量与自相关有关。在我们运行回归后,误差项应当没有相关性,且均值为零。Durbin-Watson 统计量定义如下:

Durbin-Watson

在这里,et 是时间 t 的误差项,T 是误差项的总数。Durbin-Watson 统计量用于检验普通最小二乘回归的残差是否没有自相关,零假设是残差没有自相关,备择假设是残差遵循 AR1 过程。Durbin-Watson 统计量的值范围是 0 到 4。接近 2 的值表示没有自相关;接近 0 的值表示正自相关;接近 4 的值表示负自相关,见下表:

Durbin-Watson 测试 描述
Durbin-Watson 无自相关
向 0 靠近 正自相关
向 4 靠近 负自相关

表 8.3 Durbin-Watson 测试

以下 Python 程序通过使用 IBM 的日数据首先运行 CAPM。S&P500 被用作指数,时间段为 2012 年 1 月 1 日至 2016 年 12 月 31 日,为期 5 年的窗口。此处忽略无风险利率。对于回归的残差,运行 Durbin-Watson 测试以检查其自相关性:

import pandas as pd
from scipy import stats 
import statsmodels.formula.api as sm
import statsmodels.stats.stattools as tools 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
begdate=(2012,1,1)
enddate=(2016,12,31)
#
def dailyRet(ticker,begdate,enddate):
    p =getData(ticker, begdate, enddate,asobject=True,adjusted=True)
    return p.aclose[1:]/p.aclose[:-1]-1

retIBM=dailyRet('IBM',begdate,enddate)
retMkt=dailyRet('^GSPC',begdate,enddate)

df = pd.DataFrame({"Y":retIBM, "X": retMkt})
result = sm.ols(formula="Y ~X", data=df).fit()
print(result.params)
residuals=result.resid
print("Durbin Watson")
print(tools.durbin_watson(residuals))

输出结果如下:

Durbin-Watson

1.82 接近 2 的正值表示 IBM 的 CAPM 残差的自相关可能为零。我们可以得到更明确的答案。或者,我们只需输入命令 print(result.summary()),见下图:

Durbin-Watson

上述结果显示,观测值数量为 1,257,Durbin-Watson 测试值为 1.82。根据以下的上下界限(dL 和 dU):web.stanford.edu/~clint/bench/dwcrit.htm,我们可以得出结论,1.82 距离 2 仍然不够接近。因此,残差仍然存在正自相关。赤池信息量准则 (AIC) 是一个用于衡量给定数据集的统计模型相对质量的指标。其公式如下:

Durbin-Watson

在这里,k 是模型中需要估计的系数数量,L 是对数似然值。在前面的示例中,k=1L=4089.0。因此,AIC 将是 21-24089.9=8177.8。AIC 会测试这个模型是否在绝对意义上是好的。然而,给定几个候选模型时,首选模型是具有最小 AIC 值的模型。AIC 奖励拟合优度(通过似然函数评估),但它还包括一个惩罚项,该惩罚项是被估计参数数量(k)的递增函数。BIC 代表贝叶斯信息准则,其定义如下:

Durbin-Watson

这里,n 是观测值的数量,k 是需要估计的参数数量,包括截距项。Jarque–Bera 检验是一个拟合优度检验,用于检验我们的数据是否具有与正态分布匹配的偏度和峰度:

Durbin-Watson

这里,S 是偏度,C 是峰度。原假设是偏度为零和超额峰度为零的联合假设。根据前述结果,由于概率(JB)为零,我们拒绝原假设。

高频数据的 Python 应用

高频数据是指逐秒或逐毫秒的交易和报价数据。纽约证券交易所的交易和报价TAQ)数据库是一个典型的例子(www.nyxdata.com/data-products/daily-taq)。以下程序可用于从 Google Finance 获取高频数据:

import tempfile
import re, string 
import pandas as pd 
ticker='AAPL'                    # input a ticker 
f1="c:/temp/ttt.txt"             # ttt will be replace with above sticker
f2=f1.replace("ttt",ticker) 
outfile=open(f2,"w") 
#path="http://www.google.com/finance/getprices?q=ttt&i=300&p=10d&f=d,o, h,l,c,v" 
path="https://www.google.com/finance/getprices?q=ttt&i=300&p=10d&f=d,o,%20h,l,c,v"
path2=path.replace("ttt",ticker) 
df=pd.read_csv(path2,skiprows=8,header=None) 
fp = tempfile.TemporaryFile()
df.to_csv(fp) 
print(df.head())
fp.close()

在前面的程序中,我们有两个输入变量:tickerpath。在选择带有嵌入变量 tttpath 后,我们使用 string.replace() 函数将其替换为我们的 ticker。使用 .head().tail() 函数查看前五行和后五行,代码如下所示:

高频数据的 Python 应用

来自 Google 的当天高频数据的相关网页位于 www.google.com/finance/getprices?q=AAPL&i=300&p=10d&f=d,o,%20h,l,c,v,其标题(前 10 行)如下所示:

EXCHANGE%3DNASDAQ
MARKET_OPEN_MINUTE=570
MARKET_CLOSE_MINUTE=960
INTERVAL=300
COLUMNS=DATE,CLOSE,LOW,OPEN,VOLUME
DATA=
TIMEZONE_OFFSET=-300
a1484145000,118.75,118.7,118.74,415095
1,119.1975,118.63,118.73,1000362
2,119.22,119.05,119.2,661651
3,118.96,118.91,119.225,487105
4,118.91,118.84,118.97,399730
5,118.985,118.82,118.91,334648

以下程序的目标是添加时间戳:

import tempfile
import pandas as pd, numpy as np, datetime 
ticker='AAPL' 
path="https://www.google.com/finance/getprices?q=ttt&i=300&p=10d&f=d,o,%20h,l,c,v"
x=np.array(pd.read_csv(path.replace('ttt',ticker),skiprows=7,header=None)) 
#
date=[] 
for i in np.arange(0,len(x)): 
    if x[i][0][0]=='a': 
        t= datetime.datetime.fromtimestamp(int(x[i][0].replace('a',''))) 
        print ticker, t, x[i][1:] 
        date.append(t) 
    else: 
        date.append(t+datetime.timedelta(minutes =int(x[i][0]))) 

final=pd.DataFrame(x,index=date) 
final.columns=['a','CLOSE','LOW','OPEN','VOL'] 
del final['a'] 
fp = tempfile.TemporaryFile()
#final.to_csv('c:/temp/abc.csv'.replace('abc',ticker)) 
final.to_csv(fp) 
print(final.head())

运行程序后,我们可以观察到以下输出:

%run "c:\users\yany\appdata\local\temp\tmppuuqpb.py"
AAPL 2017-01-11 09:30:00 [118.75 118.7 118.74 415095L]
AAPL 2017-01-17 09:30:00 [118.27 118.22 118.34 665157L]
AAPL 2017-01-23 09:30:00 [119.96 119.95 120.0 506837L]

要查看前几行和后几行,我们可以使用 .head().tail() 函数,方法如下:

>>>final.head() 
                       CLOSE     LOW     OPEN      VOL
2017-01-11 09:30:00   118.75   118.7   118.74   415095
2017-01-11 09:31:00  119.198  118.63   118.73  1000362
2017-01-11 09:32:00   119.22  119.05    119.2   661651
2017-01-11 09:33:00   118.96  118.91  119.225   487105
2017-01-11 09:34:00   118.91  118.84   118.97   399730
>>>final.tail() 
                      CLOSE      LOW     OPEN     VOL
2017-01-23 20:05:00  121.86   121.78   121.79  343711
2017-01-  23 20:06:00  121.84  121.815   121.86  162673
2017-01-23 20:07:00  121.77   121.75   121.84  166523
2017-01-23 20:08:00   121.7   121.69   121.78   68754
2017-01-23 20:09:00  121.82  121.704  121.707  103578

由于 TAQ 数据库价格昂贵,可能大多数读者无法访问该数据。幸运的是,我们有一个名为交易、订单、报告和报价TORQ)的数据库。感谢 Hasbrouck 教授,数据库可以从 people.stern.nyu.edu/jhasbrou/Research/ 下载。

在同一网页上,我们还可以下载 TORQ 手册。基于 Hasbrouck 教授的二进制数据集,我们生成了几个对应的 pandas pickle 格式数据集。合并交易CT)数据集可以从 canisius.edu/~yany/python/TORQct.pkl 下载。将此数据集保存到 C:\temp 后,我们可以发出以下两行 Python 代码来获取它:

import pandas as pd
import pandas as pd
import scipy as sp
x=pd.read_pickle("c:/temp/TORQct.pkl")
print(x.head())
print(x.tail())
print(sp.shape(x))

要查看前几行和后几行,我们可以使用 .head().tail() 函数,方法如下:

date      time  price  siz  g127  tseq cond ex
symbol                                                    
AC      19901101  10:39:06   13.0  100     0  1587       N
AC      19901101  10:39:36   13.0  100     0     0       M
AC      19901101  10:39:38   13.0  100     0     0       M
AC      19901101  10:39:41   13.0  100     0     0       M
AC      19901101  10:41:38   13.0  300     0  1591       N
            date      time   price    siz  g127    tseq cond ex
symbol                                                         
ZNT     19910131  11:03:31  12.375   1000     0  237884       N
ZNT     19910131  12:47:21  12.500   6800     0  237887       N
ZNT     19910131  13:16:59  12.500  10000     0  237889       N
ZNT     19910131  14:51:52  12.500    100     0  237891       N
ZNT     19910131  14:52:27  12.500   3600     0       0    Z  T
(728849, 8)

由于 ticker 被用作索引,我们可以列出所有唯一的索引值,从而找出数据集中包含的股票名称,方法如下:

import numpy as np
import pandas as pd
ct=pd.read_pickle("c:/temp/TORQct.pkl")
print(np.unique(np.array(ct.index)))

输出如下所示:

['AC' 'ACN' 'ACS' 'ADU' 'AL' 'ALL' 'ALX' 'AMD' 'AMN' 'AMO' 'AR' 'ARX' 'ATE'
 'AYD' 'BA' 'BG' 'BMC' 'BRT' 'BZF' 'CAL' 'CL' 'CLE' 'CLF' 'CMH' 'CMI' 'CMY'
 'COA' 'CP' 'CPC' 'CPY' 'CU' 'CUC' 'CUE' 'CYM' 'CYR' 'DBD' 'DCN' 'DI' 'DLT'
 'DP' 'DSI' 'EFG' 'EHP' 'EKO' 'EMC' 'FBO' 'FDX' 'FFB' 'FLP' 'FMI' 'FNM'
 'FOE' 'FPC' 'FPL' 'GBE' 'GE' 'GFB' 'GLX' 'GMH' 'GPI' 'GRH' 'HAN' 'HAT'
 'HE' 'HF' 'HFI' 'HTR' 'IBM' 'ICM' 'IEI' 'IPT' 'IS' 'ITG' 'KFV' 'KR' 'KWD'
 'LOG' 'LPX' 'LUK' 'MBK' 'MC' 'MCC' 'MCN' 'MDP' 'MNY' 'MO' 'MON' 'MRT'
 'MTR' 'MX' 'NI' 'NIC' 'NNP' 'NSI' 'NSO' 'NSP' 'NT' 'OCQ' 'OEH' 'PCO' 'PEO'
 'PH' 'PIM' 'PIR' 'PLP' 'PMI' 'POM' 'PPL' 'PRI' 'RDA' 'REC' 'RPS' 'SAH'
 'SJI' 'SLB' 'SLT' 'SNT' 'SPF' 'SWY' 'T' 'TCI' 'TEK' 'TUG' 'TXI' 'UAM'
 'UEP' 'UMG' 'URS' 'USH' 'UTD' 'UWR' 'VCC' 'VRC' 'W' 'WAE' 'WBN' 'WCS'
 'WDG' 'WHX' 'WIN' 'XON' 'Y' 'ZIF' 'ZNT']

基于高频数据估算的价差

基于合并报价CQ)数据集,由 Hasbrouck 教授提供,我们生成一个以 pandas pickle 格式保存的数据集,可以从canisius.edu/~yany/python/TORQcq.pkl下载。假设以下数据位于C:\temp目录下:

import pandas as pd 
cq=pd.read_pickle("c:/temp/TORQcq.pkl") 
print(cq.head() )

输出结果如下所示:

           date      time     bid     ofr  bidsiz  ofrsiz  mode  qseq
symbol                                                                
AC      19901101   9:30:44  12.875  13.125      32       5    10    50
AC      19901101   9:30:47  12.750  13.250       1       1    12     0
AC      19901101   9:30:51  12.750  13.250       1       1    12     0
AC      19901101   9:30:52  12.750  13.250       1       1    12     0
AC      19901101  10:40:13  12.750  13.125       2       2    12     0
>>>cq.tail() 
            date      time     bid     ofr  bidsiz  ofrsiz  mode  qseq
symbol                                                                
ZNT     19910131  13:31:06  12.375  12.875       1       1    12     0
ZNT     1  9910131  13:31:06  12.375  12.875       1       1    12     0
ZNT     19910131  16:08:44  12.500  12.750       1       1     3    69
ZNT     19910131  16:08:49  12.375  12.875       1       1    12     0
ZNT     19910131  16:16:54  12.375  12.875       1       1     3     0

同样,我们可以使用unique()函数找出所有的股票代码。假设我们对以下代码中显示的MO股票代码感兴趣:

>>>x=cq[cq.index=='MO'] 
>>>x.head() 
            date     time     bid     ofr  bidsiz  ofrsiz  mode  qseq
symbol                                                               
MO      19901101  9:30:33  47.000  47.125     100       4    10    50
MO      19901101  9:30:35  46.750  47.375       1       1    12     0
MO      19901101  9:30:38  46.875  47.750       1       1    12     0
MO      19901101  9:30:40  46.875  47.250       1       1    12     0
MO      19901101  9:30:47  47.000  47.125     100       3    12    51

检查一些观测值是个好主意。从以下输出的第一行可以看出,价差应为 0.125(47.125-47.000):

>>>x.head().ofr-x.head().bid 
symbol 
MO 0.125 
MO 0.625 
MO 0.875 
MO 0.375 
MO 0.125 
dtype: float64 
>>>

要找到平均价差和平均相对价差,我们可以使用以下代码。完整程序如下所示:

import pandas as pd 
import scipy as sp
cq=pd.read_pickle('c:/temp/TORQcq.pkl') 
x=cq[cq.index=='MO'] 
spread=sp.mean(x.ofr-x.bid) 
rel_spread=sp.mean(2*(x.ofr-x.bid)/(x.ofr+x.bid)) 
print(round(spread,5) )
print(round(rel_spread,5) )
0.39671 
0.00788

在前面的例子中,我们没有处理或清理数据。通常,我们需要通过添加各种过滤器来处理数据,例如删除负价差的报价、bidsiz为零或ofrsiz为零的数据,才能估算价差并进行其他估计。

CRSP 简介

本书的重点是免费的公共数据。因此,我们只讨论一些金融数据库,因为部分读者可能来自具有有效订阅的学校。CRSP 就是其中之一。在本章中,我们仅提到三个 Python 数据集。

证券价格研究中心CRSP)。它包含了自 1926 年起所有在美国上市的股票的所有交易数据,如收盘价、交易量和流通股数。由于其数据质量高且历史悠久,学术研究人员和实务工作者广泛使用它。第一组数据称为crspInfo.pkl,请参见以下代码:

import pandas as pd
x=pd.read_pickle("c:/temp/crspInfo.pkl")
print(x.head(3))
print(x.tail(2))

相关输出如下所示:

   PERMNO  PERMCO     CUSIP                       FIRMNAME TICKER  EXCHANGE  \
0   10001    7953  36720410                GAS NATURAL INC   EGAS         2   
1   10002    7954  05978R10  BANCTRUST FINANCIAL GROUP INC   BTFG         3   
2   10003    7957  39031810     GREAT COUNTRY BK ASONIA CT   GCBK         3   
    BEGDATE   ENDDATE  
0  19860131  20151231  
1  19860131  20130228  
2  19860131  19951229  

       PERMNO  PERMCO     CUSIP               FIRMNAME TICKER  EXCHANGE  \
31216   93435   53452  82936G20  SINO CLEAN ENERGY INC   SCEI         3   
31217   93436   53453  88160R10       TESLA MOTORS INC   TSLA         3   
        BEGDATE   ENDDATE  
31216  20100630  20120531  
31217  20100630  20151231  

PERMNO是股票IDPERMCO是公司IDCUSIP是证券IDFIRMNAME是公司名称,即今天的名称,EXCHANGE是交易所代码,BEGDATE (ENDDATE)是数据的起始日期(结束日期)。第二组数据是市场指数,参见以下代码:

import pandas as pd
x=pd.read_pickle("c:/temp/indexMonthly.pkl")
print(x.head())
    DATE    VWRETD    VWRETX    EWRETD    EWRETX  SP500RET  SP500INDEX  \
0  19251231       NaN       NaN       NaN       NaN       NaN       12.46   
1  19260130  0.000561 -0.001390  0.023174  0.021395  0.022472       12.74   
2  19260227 -0.033040 -0.036580 -0.053510 -0.055540 -0.043950       12.18   
3  19260331 -0.064000 -0.070020 -0.096820 -0.101400 -0.059110       11.46   
4  19260430  0.037019  0.034031  0.032946  0.030121  0.022688       11.72   
   TOTALVAL  TOTALN     USEDVAL  USEDN  
0  27487487     503         NaN    NaN  
1  27624240     506  27412916.0  496.0  
2  26752064     514  27600952.0  500.0  
3  25083173     519  26683758.0  507.0  
4  25886743     521  24899755.0  512.0  

最后一组数据是关于月度股票的。

参考文献

请参考以下文章:

附录 A – 生成 GDP 数据集 usGDPquarterly2.pkl 的 Python 程序

第一个程序生成一个扩展名为 .pkl 的 Python 数据集:

import pandas_datareader.data as web
import datetime
begdate = datetime.datetime(1900, 1, 1)
enddate = datetime.datetime(2017, 1, 27)

x= web.DataReader("GDP", "fred", begdate,enddate)
x.to_pickle("c:/temp/ugGDPquarterly2.pkl")

为了检索数据集,我们使用 pandas.read_pickle() 函数。请参见以下代码:

import pandas as pd
a=pd.read_pickle("c:/temp/usGDPquarterly2.pkl")
print(a.head())
print(a.tail())

              GDP
DATE             
1947-01-01  243.1
1947-04-01  246.3
1947-07-01  250.1
1947-10-01  260.3
1948-01-01  266.2
                GDP
DATE               
2015-07-01  18141.9
2015-10-01  18222.8
2016-01-01  18281.6
2016-04-01  18450.1
2016-07-01  18675.3

附录 B – 0.05 显著性水平的 F 临界值

第一行是分母的自由度,第一列是分子的自由度:

附录 B – 0.05 显著性水平的 F 临界值

用于生成上表的程序关键部分如下:

import scipy.stats as stats
alpha=0.05
dfNumerator=5
dfDenominator=10
f=stats.f.ppf(q=1-alpha, dfn=dfNumerator, dfd=dfDenominator)
print(f)
3.32583453041

附录 C – 数据案例#4 - 哪个政党更好地管理经济?

在美国,人们已经看到了许多共和党和民主党潜在总统候选人之间的总统辩论。一个潜在选民常问的问题是,哪个党能够更好地管理经济?在这项期末项目中,我们试图提出这个问题:哪个党能够从股市表现角度更好地管理经济?根据www.enchantedlearning.com/history/us/pres/list.shtml网页,我们可以找到美国总统所属的政党:

总统 所属党派 时间段
附录 C – 数据案例#4 - 哪个政党更擅长管理经济?

因此,我们可以生成以下表格。PARTY 和 RANGE 变量来自网页,YEAR2 是 RANGE 中第二个数字减去 1,除了最后一行:

政党 范围 年份 1 年份 2
共和党 1923-1929 1923 1928
共和党 1929-1933 1929 1932
民主党 1933-1945 1933 1944
民主党 1945-1953 1945 1952
共和党 1953-1961 1953 1960
民主党 1961-1963 1961 1962
民主党 1963-1969 1963 1968
共和党 1969-1974 1969 1973
共和党 1974-1977 1974 1976
民主党 1977-1981 1977 1980
共和党 1981-1989 1981 1988
共和党 1989-1993 1989 1992
民主党 1993-2001 1993 2000
共和党 2001-2009 2001 2008
民主党 2009-2017 2009 2016

表 1:自 1923 年以来的政党与总统

  1. 获取每月股票数据。

  2. 根据 YEAR1 和 YEAR2 将回报分类为两组:共和党下和民主党下。

  3. 测试零假设:两个组的均值相等:附录 C – 数据案例#4 - 哪个政党更擅长管理经济?

  4. 讨论你的结果并回答以下问题:两党下的月均回报是否相等?根据前面的表格,读者可以将所有月均回报排序为两类:民主党下和共和党下。

    注意

    对于没有 CRSP 订阅的学校读者,他们可以从 Yahoo! Finance 下载 S&P500 市场指数。另一方面,对于有 CRSP 订阅的学校读者,他们可以使用市值加权市场回报VWRETD)和等权重市场指数EWRETD)。

练习

  1. 哪个模块包含名为 rolling_kurt 的函数?你如何使用这个函数?

  2. 基于从 Yahoo! Finance 下载的每日数据,检查沃尔玛的日回报是否符合正态分布。

  3. 基于 2016 年的每日回报,IBM 和 DELL 的均值回报是否相同?

    提示

    你可以使用 Yahoo! Finance 作为数据来源

  4. 根据历史数据,过去 10 年 IBM 和 DELL 分别分配了多少股息或发生了多少次股票拆分?

  5. 编写一个 Python 程序,用 3 年的滚动窗口估算一些股票(如 IBM、WMT、C 和 MSFT)的滚动 beta。

  6. 假设我们刚刚从联邦银行的数据库下载了主要利率,网址为:www.federalreserve.gov/releases/h15/data.htm。我们下载了金融 1 个月工作日的时间序列数据。编写一个 Python 程序来合并这些数据,使用:

    • 访问网页:mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html

    • 点击 Fama-French 因子,下载名为 F-F_Research_Data_Factors.zip 的月度因子文件。解压 .zip 文件并估算市场的月度回报。

    • 例如,对于 1926 年 7 月,市场回报 = 2.65/100 + 0.22/100。该文件由 CMPT_ME_BEME_RETS 使用 201212 CRSP 数据库创建。

  7. 从 Prof. French 的数据库下载月度和日度的 Fama-French 因子,网址为:mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html。假设你持有一个 SMB 投资组合。回答以下三个问题:

    • 从 1989 年 1 月 1 日到 2016 年 12 月 31 日,使用日数据的总回报是多少?

    • 从 1989 年 1 月 1 日到 2016 年 12 月 31 日,使用月数据的总回报是多少?

    • 它们相同吗?如果不同,解释导致它们差异的一些原因。

  8. 如何通过使用 Python 和 CRSP 数据来复制 Jagadeech 和 Tidman(1993)动量策略?[假设你的学校有 CRSP 订阅]。

  9. 编写一个 Python 程序来估算回报。函数的格式可以是 dailyRet(data,sorted=0)。其中 sorted 参数表示价格的排序方式。例如,默认值可以是从最旧到最新,而 sorted=1 则表示相反的排序。下面给出一个相关的 Python 程序:

    import pandas as pd
    import scipy as sp
    p=[1,1.1,0.9,1.05] 
    a=pd.DataFrame({'Price':p})
    a['Ret']=a['Price'].diff()/a['Price'].shift(1)
    print(a)
       Price       Ret
    0   1.00       NaN
    1   1.10  0.100000
    2   0.90 -0.181818
    3   1.05  0.166667
    

    注意

    注意,有两种排序方式:p1 在 p2 之前,或者 p1 在 p2 之后。

  10. 复制附录 B 中的 F 临界值表,对于 0.05 显著性水平,提供以下 Python 程序:

    import scipy.stats as stats
    alpha=0.05
    dfNumerator=5
    dfDenominator=10
    stats.f.ppf(q=1-alpha, dfn=dfNumerator, dfd=dfDenominator)
    
  11. 此外,为 0.01 和 0.10 的显著性水平生成类似的表格。

  12. 基于程序测试 1 月效应,编写一个 Python 程序来测试星期几效应。

  13. 生成一个商业周期指标。商业周期数据来自国家经济研究局(National Bureau of Economic Research)。原始起始日期为 1854 年 6 月,www.nber.org/cycles/cyclesmain.html。由于股市数据从 1926 年开始,我们可以删除 1923 年之前的数据。对于峰值,赋值为正 1,对于谷值,赋值为负 1。任何位于这些峰值和谷值之间的月份,我们进行线性插值,参见下面的 B 面板。P 代表峰值,T 代表谷值。T(t-1) 代表前一个谷值,P(t-1) 代表前一个峰值:

    收缩 扩张 周期
    峰值 (P) 谷值 (T) 从 P 到 T 从 T(t-1) 到 P 从 T(-1) 到 T
    1923 年 5 月(II) 1924 年 7 月(III) 14 22 36
    1926 年 10 月(III) 1927 年 11 月(IV) 13 27 40
    1929 年 8 月(III) 1933 年 3 月(I) 43 21 64
    1937 年 5 月(II) 1938 年 6 月(II) 13 50 63
    1945 年 2 月(I) 1945 年 10 月(IV) 8 80 88
    1948 年 11 月(IV) 1949 年 10 月(IV) 11 37 48
    1953 年 7 月(II) 1954 年 5 月(II) 10 45 55
    1957 年 8 月(III) 1958 年 4 月(II) 8 39 47
    1960 年 4 月(第二季度) 1961 年 2 月(第一季度) 10 24 34
    1969 年 12 月(第四季度) 1970 年 11 月(第四季度) 11 106 117
    1973 年 11 月(第四季度) 1975 年 3 月(第一季度) 16 36 52
    1980 年 1 月(第一季度) 1980 年 7 月(第三季度) 6 58 64
    1981 年 7 月(第三季度) 1982 年 11 月(第四季度) 16 12 28
    1990 年 7 月(第三季度) 1991 年 3 月(第一季度) 8 92 100
    2001 年 3 月(第一季度) 2001 年 11 月(第四季度) 8 120 128
    2007 年 12 月(第四季度) 2009 年 6 月(第二季度) 18 73 91
  14. 编写一个 Python 程序,下载日价格并估算日收益。然后将日收益转换为月收益。月收益的日期变量应为月末的最后一个交易日。可以使用以下 Python 数据集:canisius.edu/~yany/python/tradingDaysMonthly.pkl,参见以下代码:

    >>>import pandas as pd
    >>>x=pd.read_pickle("c:/temp/tradingDaysMonthly.pk")
    >>>print(x.head())
      tradingDays
    0  1925-12-31
    1  1926-01-30
    2  1926-02-27
    3  1926-03-31
    4  1926-04-30
    
  15. 编写一个 Python 程序,从历史日价格或历史月价格数据中生成季度收益。

总结

本章详细讨论了与时间序列相关的许多概念和问题。话题包括如何设计一个真实的日期变量,如何合并具有不同频率的数据集,如何从雅虎财经下载历史价格;此外,还讨论了不同的收益估算方法,如何估算 Roll(1984)价差,Amihud(2002)的流动性,Pastor 和 Stambaugh(2003)的流动性,如何从 Hasbrouck 教授的 TORQ 数据库(交易、订单、报告和报价)中检索高频数据。此外,还展示了来自 CRSP 的两个数据集。由于本书专注于公开的金融、经济和会计数据,我们可能会简单提到一些金融数据库。

在下一章中,我们讨论与投资组合理论相关的许多概念和理论,例如如何衡量投资组合风险,如何估算 2 只股票和 n 只股票的投资组合风险,如何使用夏普比率、特雷诺比率和索提诺比率等各种指标衡量风险与收益之间的权衡,如何根据这些指标(比率)最小化投资组合风险,如何设置目标函数,如何为一组给定的股票选择高效的投资组合,以及如何构建有效前沿。

第九章:投资组合理论

理解投资组合理论对于学习金融非常重要。众所周知,不要把所有的鸡蛋放在一个篮子里,也就是说,分散风险是一个非常好的主意。然而,很少有人知道这种著名谚语背后的隐含假设。在本章中,我们将讨论个别股票或投资组合的各种风险衡量指标,如夏普比率、特雷诺比率、索提诺比率,如何基于这些指标(比率)最小化投资组合风险,如何设置目标函数,如何为给定的股票选择高效投资组合,以及如何构建有效前沿。我们的重点是如何使用实际数据应用投资组合理论。例如,今天我们有 200 万美元的现金,计划购买 IBM 和沃尔玛的股票。如果我们将 30%的资金投资于第一只股票,剩下的投资于第二只股票,那么我们的投资组合风险是多少?我们能基于这两只股票构建出最小风险的投资组合吗?如果是 10 只或 500 只股票呢?本章将涵盖以下主题:

  • 投资组合理论简介

  • 两只股票的投资组合

  • N 只股票的投资组合

  • 相关性与多样化效应

  • 生成收益矩阵

  • 基于夏普比率、特雷诺比率和索提诺比率生成最优投资组合

  • 构建有效前沿

  • 莫迪利安尼和莫迪利安尼绩效衡量(M2 衡量)

投资组合理论简介

投资组合理论的关键字是多样化,而多样化的关键字是相关性。换句话说,相关性用于衡量两只股票或投资组合之间的共同运动程度。投资组合理论的目标是根据风险和收益最优地分配我们的资产。马科维茨(1952 年)认为,我们应只考虑证券收益分布的前两个时刻:均值和方差。对于金融市场,做出了一些重要假设,例如股市无效、典型投资者是理性的、套利机会不会持续太长时间。对于两只股票之间的偏好,对于给定的风险,理性投资者会偏好预期收益较高的股票;对于给定的收益,理性投资者偏好风险较低的股票。有时,单期投资组合优化被称为马科维茨投资组合优化。输入包括收益矩阵、方差和协方差矩阵,而输出是高效投资组合。通过连接众多高效投资组合,形成了有效前沿。在这里,我们从最简单的情境开始:一个由两只股票组成的投资组合。

两只股票的投资组合

显然,两只股票的投资组合是最简单的假设。假设这两只股票的权重分别是w1w2。投资组合的收益如下所示:

两只股票的投资组合

在这里,Rp,t 是在时间 t 的投资组合回报,w1w2)是股票 1(2)的权重,而 R1,tR2,t)是股票 1(2)在时间 t 的回报。当谈到预期回报或均值时,我们有一个类似的公式:

一个两只股票的投资组合

在这里,一个两只股票的投资组合 是均值或预期的投资组合回报,而一个两只股票的投资组合 一个两只股票的投资组合 是股票 1(2)的均值或预期回报。此类两只股票的投资组合的方差定义如下:

一个两只股票的投资组合

在这里,一个两只股票的投资组合 是投资组合的方差,一个两只股票的投资组合 是股票 1(2)的标准差。股票 1 的方差和标准差的定义如下:

一个两只股票的投资组合 一个两只股票的投资组合 是股票 1 和股票 2 之间的协方差(相关性)。它们在此定义如下:

一个两只股票的投资组合

对于协方差,如果它是正的,那么这两只股票通常会一起变动。另一方面,如果它是负的,它们通常会朝相反的方向变动。如果协方差为零,那么它们没有相关性。然而,如果我们知道一个两只股票的投资组合,我们不能断言 A 与 B 的相关性比 A 与 C 的相关性强,或反之亦然。另一方面,如果一个两只股票的投资组合,我们则可以断言 A 与 B 的相关性比 A 与 A 的相关性强。这表明相关性比协方差更有用。相关性的范围从-1 到 1。相关性值越低,分散化效应越强。当相关性为-1(1)时,称为完全负相关(完全正相关)。当两只股票(或投资组合)完全正相关时,就没有分散化。

假设两只股票的波动率(标准差)分别为 0.06 和 0.24,并且它们是完全负相关的。为了形成一个零风险投资组合,需要什么样的权重?有几种方法可以找到解决方案。

方法 1:我们可以手动找到一个解:将给定的值代入方程(3),并设其等于零,其中 x=x1x2=1-x

一个两只股票的投资组合

扩展并整理项后,我们将得到以下通用方程:

一个两只股票的投资组合

对于这种通用形式,如果根号内的项为正,即一个两只股票的投资组合,我们有以下两种解法:

一个两只股票的投资组合

基于一组 abc,我们得到了一个解 x=80%,即当 w1=0.80w2=0.2 时,前述两只股票组合将是无风险的。假设我们有一个方程 x2+6x+3=0,以下 Python 程序提供两个解:

import scipy as sp
a=1
b=6
c=3
inside=b**2-4*a*c
if inside>0:
    squared=sp.sqrt(inside)
print("x1=",(b+squared)/(2*a))
print("x2=",(b-squared)/(2*a)) 
('x1=', 5.4494897427831779)
('x2=', 0.55051025721682212)

方法 2:对于一对给定的标准差(或一对方差)以及它们之间的相关性,我们生成许多股票 1 的权重,例如 0、0.001、0.002、0.003 等。记住,w2=1-w1。通过应用公式(3),我们估计这个两只股票组合的方差。我们的最终解决方案将是实现最小组合方差的 w1w2,请参见以下代码:

import scipy as sp
sigma1=0.06
sigma2=0.24
var1=sigma1**2
var2=sigma2**2
rho=-1
n=1000
portVar=10   # assign a big number
tiny=1.0/n

for i in sp.arange(n):
    w1=i*tiny
    w2=1-w1
    var=w1**2*var1 +w2**2*var2+2*w1*w2*rho*sigma1*sigma2
    if(var<portVar):
        portVar=var
        finalW1=w1
    #print(vol)
print("min vol=",sp.sqrt(portVar), "w1=",finalW1) ('min vol=', ('min vol=', ('min vol=', 9.3132257461547852e-10, 'w1=', 0.80000000000000004)

首先,结果确认了我们之前的结果,其中 w1=0.8w2=0.2。在程序中,我们有 1000 对 w1w2。一个很小的值,称为 tiny,是 1/1000=0.001。第一对两个权重是 0.1% 和 99.9%。我们为我们的解变量分配一个非常大的数字,作为初始值。在这个程序中,portVar=10。其他大数字也完全有效,比如 100。这里的逻辑是:基于第一对 w1w2,我们估计组合方差。如果这个新的组合方差小于 portVar,我们就用这个新值替换 portVar 并记录 w1。如果新的组合方差大于 portVar,我们什么也不做。重复相同的过程,直到完成循环。这里有个类比。假设我们想在 1000 人中找到身高最高的人。假设我们有一个变量叫 tallestPerson,初始值为 0.1 英寸。由于每个人都会比这个值高,第一个人的身高将替代这个值。如果下一个人的身高比这个变量还高,我们就替换它。否则,我们就到下一个人。这个过程会一直重复,直到最后一个人。在效率方面,有一个小技巧,就是只需估算一次 var1var2

在金融领域,使用方差和标准差来表示风险是一种惯例,因为它们描述了不确定性。通常,我们使用收益的标准差来代表波动性。观察相关性对有效前沿的影响是一个不错的主意。首先,让我们学习如何生成一组相关的随机数。涉及两个步骤:

  1. 生成两个随机时间序列,x1x2,其相关性为零。

  2. 应用以下公式:一个两只股票组合

这里 ρ 是预定的两个时间序列之间的相关性。现在,y1y2 是具有预定相关性的。以下 Python 程序将实现前述方法:

import scipy as sp
sp.random.seed(123)
n=1000
rho=0.3
x1=sp.random.normal(size=n)
x2=sp.random.normal(size=n)
y1=x1
y2=rho*x1+sp.sqrt(1-rho**2)*x2
print(sp.corrcoef(y1,y2))
[[ 1\.          0.28505213]
 [ 0.28505213  1\.        ]]

优化 – 最小化

在讨论如何生成最优组合之前,有必要研究几个优化函数。在以下示例中,我们最小化目标函数 y:

优化 – 最小化

首先,让我们看一下该目标函数的图形,见以下代码:

import scipy as sp
import matplotlib.pyplot as plt
x=sp.arange(-5,5,0.01)
a=3.2
b=5.0
y=a+b*x**2
plt.plot(x,y)
plt.title("y= "+str(a)+"+"+str(b)+"x²")
plt.ylabel("y")
plt.xlabel("x")
plt.show()

图形如下所示:

优化 – 最小化

为了使程序更具通用性,生成了两个系数ab。显然,由于x的幂为 2,y只有在x为 0 时才最小化。最小化的 Python 代码如下:

from scipy.optimize import minimize
def myFunction(x):
    return (3.2+5*x**2)
x0=100
res = minimize(myFunction,x0,method='nelder-mead',options={'xtol':1e-8,'disp': True})

在前面的程序中,使用的主要函数是scipy.optimize.minimize()函数。第一个输入是我们的目标函数,在这个例子中是我们的 y 函数。第二个值是输入值,即初始值。由于y函数只有一个自变量x,因此x0是一个标量。第三个输入值是方法,我们有多个选择:NelderMead。下表列出了 11 种变量选择:

方法 描述
NelderMead 使用 Simplex 算法。该算法在许多应用中都具有鲁棒性。然而,如果可以信任数值导数计算,其他使用一阶和/或二阶导数信息的算法可能会因其更好的性能而被优先选择。
Powell 这是 Powell 方法的修改版,Powell 方法是一种共轭方向法。它沿着每个方向向量执行顺序的一维最小化,并在每次主最小化循环迭代时更新方向集。该函数不需要可微性,也不需要计算导数。
CG 使用 Polak 和 Ribiere 的非线性共轭梯度算法,这是 Fletcher-Reeves 方法的一个变种。仅使用一阶导数。
BFGS 使用布罗伊登、弗莱彻、戈尔德法布和香农(BFGS)的拟牛顿法。只使用一阶导数。即使对于非光滑优化,BFGS 也表现出良好的性能。该方法还返回海森矩阵逆的近似值,存储在 OptimizeResult 对象中的 hess_inv 字段。
NewtonCG 使用牛顿-共轭梯度(Newton-CG)算法(也称为截断牛顿法)。它使用共轭梯度法计算搜索方向。
LBFGSB 使用 help()函数来获取更多信息。
TNC [同上]
COBYLA [同上]
SLSQP [同上]
dogleg [同上]
trustncg [同上]

表 9.1 求解器类型

输出显示函数值为 3.2,这是通过将x赋值为0实现的。

优化成功终止:

优化 – 最小化

下一个例子使用scipy.optimize.brent()函数进行指数函数的最小化,见下方的目标函数代码:

优化 – 最小化

以下程序尝试最小化目标函数,即y

from scipy import optimize
import numpy as np
import matplotlib.pyplot as plt
# define a function 
a=3.4
b=2.0
c=0.8
def f(x):
    return a-b*np.exp(-(x - c)**2)

x=np.arange(-3,3,0.1)
y=f(x)
plt.title("y=a-b*exp(-(x-c)²)")
plt.xlabel("x")
plt.ylabel("y")
plt.plot(x,y)
plt.show()

# find the minimum
solution= optimize.brent(f) 
print(solution)

解决方案是0.799999999528,相关图形如下所示:

优化 – 最小化

在经济学和金融学中,有一个重要的概念叫做效用。设计这个概念的主要原因之一是,在许多情况下,我们无法量化某些影响因素,例如幸福感、意愿、风险偏好、健康、情绪等。例如,如果你的老板要求你在周五加班并承诺给你奖金,假设每小时的奖金是x美元,并且你对此感到满意。如果任务很紧急,老板可能会要求你工作更长时间。假设你还需要在周六工作,你认为相同的每小时x美元还会让你高兴吗?对于大多数员工来说,额外的奖金应该高于 x,因为他们会认为自己现在的付出已经不仅仅是一个周五的晚上了。通常,效用函数可以定义为收益与成本之间的差异。边际效益是我们投入的递减函数。这意味着,额外获得的一美元并不像之前的那一美元那样有价值。另一方面,边际成本将是你投入的递增函数。当你被要求做额外工作时,适当的货币激励应该更高。这里是一个效用函数:

优化 – 最小化

这里,U是效用函数,E(R)是预期的投资组合回报,我们可以用它的均值来近似,A是风险厌恶系数,σ2是投资组合的方差。当预期回报更高时,我们的效用也更高。反之,当我们投资组合的风险更高时,效用更低。关键在于A,它代表了风险承受能力。在相同的预期回报和风险水平下,风险厌恶的投资者(更高的 A 值)会体验到较低的效用。一般来说,目标是平衡收益(预期回报)与风险(方差)。

假设我们有一组股票,如国际商用机器公司IBM)、沃尔玛WMT)和花旗集团C)。基于前面的效用函数,我们应该根据不同的风险偏好选择哪只股票呢?以下是给出的代码:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd
import scipy as sp

tickers=('IBM','WMT','C')  # tickers
begdate=(2012,1,1)         # beginning date 
enddate=(2016,12,31)       # ending date
n=len(tickers)             # number of observations
A=1                        # risk preference

def ret_f(ticker,begdate,enddte):
    x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
    ret =x.aclose[1:]/x.aclose[:-1]-1
    return ret

def myUtilityFunction(ret,A=1):
    meanDaily=sp.mean(ret)
    varDaily=sp.var(ret)
    meanAnnual=(1+meanDaily)**252
    varAnnual=varDaily*252
    return meanAnnual- 0.5*A*varAnnual

for i in sp.arange(n):
    ret=ret_f(tickers[i],begdate,enddate)
    print(myUtilityFunction(ret,A))

在前面的程序中,均值和标准差都是年化的。252 的数值代表每年的交易天数。使用的时间段是从 2012 年 1 月 1 日到 2016 年 12 月 31 日,即五年的时间段。输出结果如下。再次提醒,结果是针对风险偏好 A=1 的投资者:

优化 – 最小化

基于效用的概念,投资者偏好效用值最高的股票。因此,我们应该选择最后一只股票。换句话说,如果我们必须选择一只股票作为投资,我们应该选择花旗集团。另一方面,当 A=10 时,也就是极度风险厌恶时,这三只股票的效用值如下:

优化 – 最小化

结果表明,这样的投资者应该选择第二只股票,也就是沃尔玛作为唯一的投资。这与我们的常识一致,见其对应的平均回报和风险水平:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd
import scipy as sp

tickers=('IBM','WMT','C')  # tickers
begdate=(2012,1,1)         # beginning date 
enddate=(2016,12,31)       # ending date
n=len(tickers)             # number of observations

def ret_f(ticker,begdate,enddte):
    x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
    ret =x.aclose[1:]/x.aclose[:-1]-1
    return ret

def meanVarAnnual(ret):
    meanDaily=sp.mean(ret)
    varDaily=sp.var(ret)
    meanAnnual=(1+meanDaily)**252
    varAnnual=varDaily*252
return meanAnnual, varAnnual

print("meanAnnual,      varAnnjal")
for i in sp.arange(n):
    ret=ret_f(tickers[i],begdate,enddate)
    print(meanVarAnnual(ret))

输出结果如下:

优化 – 最小化

在前面的程序中,生成了一个名为meanVarAnnual()的函数,计算年化的平均回报和年化波动率。我们来比较最后两只股票。第二只股票在同一时间比第三只股票风险更低;它的风险高于第三只股票。第二只股票的年平均回报下降了 12%,然而,它的方差下降了 63%。结果是效用增加了。

对于投资组合优化,或者马克维茨投资组合优化,我们的输入数据集包括:预期收益、标准差和相关矩阵。输出将是一个最优的投资组合。通过连接这些高效的投资组合,可以构建一个有效前沿。在本章其余部分,我们使用历史回报来代表预期回报,并使用历史相关性来代替预期相关性。

形成一个 n 只股票的投资组合

以下程序生成一个回报矩阵,包含三只股票和 S&P500:

import statsimport numpy as np
import pandas as pd
tickers=['IBM','dell','wmt']
path1='http://chart.yahoo.com/table.csv?s=^GSPC'
final=pd.read_csv(path1,usecols=[0,6],index_col=0)
final.columns=['^GSPC']
path2='http://chart.yahoo.com/table.csv?s=ttt'
for ticker in tickers:
    print ticker
    x = pd.read_csv(path2.replace('ttt',ticker),usecols=[0,6],index_col=0)
    x.columns=[ticker]
    final=pd.merge(final,x,left_index=True,right_index=True)

要显示前几行和最后几行,我们使用.head().tail()函数,如下所示:

>>>final.head()
              ^GSPC     IBM   dell    wmt
Date                                     
2013-10-18  1744.50  172.85  13.83  75.71
2013-10-17  1733.15  173.90  13.85  75.78
2013-10-16  1721.54  185.73  13.85  75.60
2013-10-15  1698.06  183.67  13.83  74.37
2013-10-14  1710.14  185.97  13.85  74.68
>>>final.tail()
             ^GSPC    IBM  dell   wmt
Date                                 
1988-08-23  257.09  17.38  0.08  2.83
1988-08-22  256.98  17.36  0.08  2.87
1988-08-19  260.24  17.67  0.09  2.94
1988-08-18  261.03  17.97  0.09  2.98
1988-08-17  260.77  17.97  0.09  2.98
>>>

在前面的程序中,我们首先提取 S&P500 数据。然后将股票数据与市场指数合并。使用的主要函数是pandas.merge()。请注意两个输入参数的含义:left_index=Trueright_index=True。它们表示这两个数据集是通过它们的索引进行合并的。在程序中,获取的是日频率数据。学术研究人员和专业人士通常更倾向于使用月度频率数据,其中一个原因是月度数据相比日度数据更少受到所谓的微观结构效应的影响。以下程序使用了月度数据。使用的 Python 数据是yanMonthly.pklcanisius.edu/~yany/python/yanMonthly.pkl。首先,我们打印出所包含的证券列表:

import pandas as pd
import scipy as sp
df=pd.read_pickle("c:/temp/yanMonthly.pkl")
print(sp.unique(df.index))
['000001.SS' 'A' 'AA' 'AAPL' 'BC' 'BCF' 'C' 'CNC' 'COH' 'CPI' 'DELL' 'GE'
 'GOLDPRICE' 'GV' 'GVT' 'HI' 'HML' 'HPS' 'HY' 'IBM' 'ID' 'IL' 'IN' 'INF'
 'ING' 'INY' 'IO' 'ISL' 'IT' 'J' 'JKD' 'JKE' 'JPC' 'KB' 'KCC' 'KFT' 'KIE'
 'KO' 'KOF' 'LBY' 'LCC' 'LCM' 'LF' 'LG' 'LM' 'M' 'MA' 'MAA' 'MD' 'MFL' 'MM'
 'MPV' 'MY' 'Mkt_Rf' 'NEV' 'NIO' 'NP' 'NU' 'NYF' 'OI' 'OPK' 'PAF' 'PFO'
 'PSJ' 'PZZA' 'Q' 'RH' 'RLV' 'Rf' 'Russ3000E_D' 'Russ3000E_X' 'S' 'SBR'
 'SCD' 'SEF' 'SI' 'SKK' 'SMB' 'STC' 'T' 'TA' 'TBAC' 'TEN' 'TK' 'TLT' 'TOK'
 'TR' 'TZE' 'UHS' 'UIS' 'URZ' 'US_DEBT' 'US_GDP2009dollar'
 'US_GDP2013dollar' 'V' 'VC' 'VG' 'VGI' 'VO' 'VV' 'WG' 'WIFI' 'WMT' 'WR'
 'XLI' 'XON' 'Y' 'YANG' 'Z' '^AORD' '^BSESN' '^CCSI' '^CSE' '^FCHI' '^FTSE'
 '^GSPC' '^GSPTSE' '^HSI' '^IBEX' '^ISEQ' '^JKSE' '^KLSE' '^KS11' '^MXX'
 '^NZ50' '^OMX' '^STI' '^STOXX50E' '^TWII']

要选择特定的证券,可以将数据集的索引与股票代码进行比较;请参见以下代码选择 IBM 的月度价格数据:

import scipy as sp
import pandas as pd
import numpy as np
n_stocks=10
x=pd.read_pickle('c:/temp/yanMonthly.pkl')
ibm=x[x.index=='IBM']
print(ibm.head(3))
print(ibm.tail(3))
          DATE  VALUE
NAME                 
IBM   19620131   2.36
IBM   19620228   2.34
          DATE   VALUE
NAME                  
IBM   20130930  185.18
IBM   20131031  179.21
IBM   20131104  180.27

以下程序先生成回报,然后使用股票代码作为对应的列名,而不是使用生成的术语,如return。原因是我们打算选择几只股票并将它们并排放置,即按日期排列:

import scipy as sp
import pandas as pd
import numpy as np
n_stocks=10
x=pd.read_pickle('c:/temp/yanMonthly.pkl')
def ret_f(ticker):
    a=x[x.index==ticker]
    p=sp.array(a['VALUE'])
    ddate=a['DATE']
    ret=p[1:]/p[:-1]-1
    output=pd.DataFrame(ret,index=ddate[1:])
    output.columns=[ticker]
    return output
ret=ret_f('IBM')
print(ret.head())
               IBM
DATE              
19620228 -0.008475
19620330 -0.008547
19620430 -0.146552
19620531 -0.136364
19620629 -0.134503

最后,我们可以从yanMonthly.pkl构建一个 n 只股票的回报矩阵:

import scipy as sp
import pandas as pd
import numpy as np
n_stocks=10
x=pd.read_pickle('c:/temp/yanMonthly.pkl')
x2=sp.unique(np.array(x.index))
x3=x2[x2<'ZZZZ']                       # remove all indices
sp.random.seed(1234567)
nonStocks=['GOLDPRICE','HML','SMB','Mkt_Rf','Rf','Russ3000E_D','US_DEBT','Russ3000E_X','US_GDP2009dollar','US_GDP2013dollar']
x4=list(x3)

for i in range(len(nonStocks)):
    x4.remove(nonStocks[i])
k=sp.random.uniform(low=1,high=len(x4),size=n_stocks)
y,s=[],[]

for i in range(n_stocks):
    index=int(k[i])
    y.append(index)
    s.append(x4[index])
final=sp.unique(y)
print(s)

def ret_f(ticker):
    a=x[x.index==ticker]
    p=sp.array(a['VALUE'])
    ddate=a['DATE']
    ret=p[1:]/p[:-1]-1
    output=pd.DataFrame(ret,index=ddate[1:])
    output.columns=[ticker]
    return output
final=ret_f(s[0])
for i in sp.arange(1,n_stocks):
    ret=ret_f(s[i])
    final=pd.merge(final,ret,left_index=True, right_index=True)

要从一组现有可用股票(其中有 n 个)中随机选择 m 只股票,请参见scipy.random.uniform(low=1,high=len(x4),size=n_stocks)命令。由于n_stocks的值为 10,我们从len(x4)中选择了 10 只股票。输出结果如下:

                IO         A        AA        KB      DELL        IN  \
DATE                                                                   
20110930 -0.330976 -0.152402 -0.252006 -0.206395 -0.048679 -0.115332   
20111031  0.610994  0.185993  0.124464  0.192002  0.117690  0.237730   
20111130 -0.237533  0.011535 -0.066794 -0.106274 -0.002616 -0.090458   
20111230  0.055077 -0.068422 -0.135992 -0.102006 -0.072131 -0.065395   
20120131  0.212072  0.215972  0.173964  0.209317  0.178092  0.230321   

               INF       IBM       SKK        BC  
DATE                                              
20110930 -0.228456  0.017222  0.227586 -0.116382  
20111031  0.142429  0.055822 -0.305243  0.257695  
20111130 -0.038058  0.022314 -0.022372  0.057484  
20111230  0.059345 -0.021882 -0.024262 -0.030140  
20120131  0.079202  0.047379 -0.142131  0.182020

在金融领域,构建有效前沿始终是一项具有挑战性的工作,尤其是在使用真实数据时。在这一部分,我们讨论了方差-协方差矩阵的估算及其优化、寻找最优投资组合以及使用从 Yahoo! Finance 下载的股票数据构建有效前沿。当给定回报矩阵时,我们可以估算其方差-协方差矩阵。对于一组给定的权重,我们可以进一步估算投资组合的方差。估算单只股票回报的方差和标准差的公式如下:

形成一个 n 股投资组合形成一个 n 股投资组合

这里,形成一个 n 股投资组合是均值,形成一个 n 股投资组合是第i期的股票回报,n是回报的数量。对于一个 n 股投资组合,我们有以下公式来估算其投资组合回报:

形成一个 n 股投资组合

这里,形成一个 n 股投资组合是投资组合回报,形成一个 n 股投资组合是第i只股票的权重,形成一个 n 股投资组合是第 i 只股票的回报。这对于投资组合的均值或预期投资组合回报是成立的,见下文:

形成一个 n 股投资组合形成一个 n 股投资组合

n 股投资组合的投资组合方差在此定义:

形成一个 n 股投资组合

这里,形成一个 n 股投资组合是投资组合方差,n是投资组合中的股票数量,形成一个 n 股投资组合是第 i 只股票的权重,形成一个 n 股投资组合是第i只股票和第j只股票之间的协方差。请注意,当ij相同时,形成一个 n 股投资组合就是方差,即:

形成一个 n 股投资组合

可以理解的是,2 只股票的投资组合只是 n 股投资组合的一个特例。同样,当回报矩阵和权重向量的值给定时,我们可以如下估算其方差-协方差矩阵和投资组合方差:

import numpy as np
ret=np.matrix(np.array([[0.1,0.2],[0.10,0.1071],[-0.02,0.25],[0.012,0.028],[0.06,0.262],[0.14,0.115]]))
print("return matrix")
print(ret)
covar=ret.T*ret
print("covar")
print(covar)
weight=np.matrix(np.array([0.4,0.6]))
print("weight ")
print(weight)
print("mean return")
print(weight*covar*weight.T)

使用的关键命令是ret.T*retret.T是回报矩阵的转置。由于回报矩阵是一个 6×2 的矩阵,其转置将是一个 2×6 的矩阵。因此,(2×6)与(6×2)的矩阵乘法结果将是(2×2)。相应的输出,如回报矩阵、协方差矩阵、权重和投资组合方差,列示如下:

return matrix
[[ 0.1     0.2   ]
 [ 0.1     0.1071]
 [-0.02    0.25  ]
 [ 0.012   0.028 ]
 [ 0.06    0.262 ]
 [ 0.14    0.115 ]]
covar
[[ 0.043744    0.057866  ]
 [ 0.057866    0.19662341]]
weight 
[[ 0.4  0.6]]
mean return
[[ 0.10555915]]

进行矩阵乘法的第二种方法是使用spcipy.dot()函数,见以下代码:

import numpy as np
ret=np.matrix(np.array([[0.1,0.2],[0.10,0.1071],[-0.02,0.25],[0.012,0.028],[0.06,0.262],[0.14,0.115]]))
covar=np.dot(ret.T,ret)
print("covar")
print(covar)

构建一个最优投资组合

在金融学中,我们处理的是风险与收益之间的权衡。一个广泛使用的标准是夏普比率,其定义如下:

构建最优投资组合

以下程序通过改变投资组合中股票的权重来最大化夏普比率。整个程序可以分为几个部分。输入区域非常简单,只需要输入一些股票代码以及起始和结束日期。接下来,我们定义四个函数,分别是将日收益转换为年收益、估算投资组合的方差、估算夏普比率,并在优化过程中估算最后一个(即第 n 个)权重,而前 n-1 个权重已经通过我们的优化程序估算出来:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd
import scipy as sp
from scipy.optimize import fmin
  1. 输入区域的代码:

    ticker=('IBM','WMT','C')   # tickers
    begdate=(1990,1,1)         # beginning date 
    enddate=(2012,12,31)       # ending date
    rf=0.0003                  # annual risk-free rate
    
  2. 定义一些函数的代码:

    # function 1: 
    def ret_annual(ticker,begdate,enddte):
        x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
        logret =sp.log(x.aclose[1:]/x.aclose[:-1])
        date=[]
        d0=x.date
        for i in range(0,sp.size(logret)):
            date.append(d0[i].strftime("%Y"))
        y=pd.DataFrame(logret,date,columns=[ticker])
        return sp.exp(y.groupby(y.index).sum())-1
    
    # function 2: estimate portfolio variance 
    def portfolio_var(R,w):
        cor = sp.corrcoef(R.T)
        std_dev=sp.std(R,axis=0)
        var = 0.0
        for i in xrange(n):
            for j in xrange(n):
                var += w[i]*w[j]*std_dev[i]*std_dev[j]*cor[i, j]
        return var
    
    # function 3: estimate Sharpe ratio
    def sharpe(R,w):
        var = portfolio_var(R,w)
        mean_return=sp.mean(R,axis=0)
        ret = sp.array(mean_return)
        return (sp.dot(w,ret) - rf)/sp.sqrt(var)
    
    # function 4: for given n-1 weights, return a negative sharpe ratio
    def negative_sharpe_n_minus_1_stock(w):
        w2=sp.append(w,1-sum(w))
        return -sharpe(R,w2)        # using a return matrix here!!!!!!
    
  3. 生成收益矩阵(年收益)的代码:

    n=len(ticker)              # number of stocks
    x2=ret_annual(*ticker[0],begdate,enddate) 
    for i in range(1,n):
        x_=ret_annual(ticker[i],begdate,enddate) 
        x2=pd.merge(x2,x_,left_index=True,right_index=True)
    
    # using scipy array format 
    R = sp.array(x2)
    print('Efficient porfolio (mean-variance) :ticker used')
    print(ticker)
    print('Sharpe ratio for an equal-weighted portfolio')
    equal_w=sp.ones(n, dtype=float) * 1.0 /n 
    print(equal_w)
    print(sharpe(R,equal_w))
    
    # for n stocks, we could only choose n-1 weights
    w0= sp.ones(n-1, dtype=float) * 1.0 /n 
    w1 = fmin(negative_sharpe_n_minus_1_stock,w0)
    final_w = sp.append(w1, 1 - sum(w1))
    final_sharpe = sharpe(R,final_w)
    print ('Optimal weights are ')
    print (final_w)
    print ('final Sharpe ratio is ')
    print(final_sharpe)
    

在步骤 2 中,我们将日收益估算为年收益。对于优化而言,最重要的函数是scipy.optimize.fmin()函数。该最小化函数的第一个输入是我们的目标函数negative_sharpe_n_minus_1。我们的目标是最大化夏普比率。由于这是一个最小化函数,它等同于最小化负的夏普比率。另一个问题是,我们需要 n 个权重来计算夏普比率。然而,由于 n 个权重的总和为 1,我们只有 n-1 个权重作为选择变量。从以下输出可以看出,如果我们使用一个简单的等权重策略,夏普比率为 0.63。另一方面,我们的最优投资组合的夏普比率为 0.67:

Efficient porfolio (mean-variance) :ticker used
('IBM', 'WMT', 'C')
Sharpe ratio for an equal-weighted portfolio
[ 0.33333333  0.33333333  0.33333333]
0.634728319263
Optimization terminated successfully.
         Current function value: -0.669758
         Iterations: 31
         Function evaluations: 60
Optimal weights are 
[ 0.49703463  0.31044168  0.19252369]
final Sharpe ratio is 
0.66975823926

构建具有 n 只股票的有效前沿

构建有效前沿一直是金融学教授面临的最困难的任务之一,因为这个任务涉及矩阵操作和约束优化过程。一个有效前沿能够生动地解释马科维茨投资组合理论。以下 Python 程序使用五只股票构建有效前沿:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import scipy as sp
from numpy.linalg import inv, pinv
  1. 输入区域的代码:

    begYear,endYear = 2001,2013
    stocks=['IBM','WMT','AAPL','C','MSFT']
    
  2. 定义两个函数的代码:

    def ret_monthly(ticker):  #  function 1
        x = getData(ticker,(begYear,1,1),(endYear,12,31),asobject=True,adjusted=True)
        logret=np.log(x.aclose[1:]/x.aclose[:-1]) 
        date=[]
        d0=x.date
        for i in range(0,np.size(logret)): 
            date.append(''.join([d0[i].strftime("%Y"),d0[i].strftime("%m")]))
        y=pd.DataFrame(logret,date,columns=[ticker]) 
        return y.groupby(y.index).sum()
    
    # function 2: objective function 
    def objFunction(W, R, target_ret):
        stock_mean=np.mean(R,axis=0) 
        port_mean=np.dot(W,stock_mean)          # portfolio mean 
        cov=np.cov(R.T)                         # var-cov matrix
        port_var=np.dot(np.dot(W,cov),W.T)     # portfolio variance 
        penalty = 2000*abs(port_mean-target_ret)# penalty 4 deviation 
        return np.sqrt(port_var) + penalty     # objective function
    
  3. 生成收益矩阵 R 的代码:

    R0=ret_monthly(stocks[0])                   # starting from 1st stock 
    n_stock=len(stocks)                         # number of stocks
    for i in xrange(1,n_stock):                 # merge with other stocks 
        x=ret_monthly(stocks[i]) 
        R0=pd.merge(R0,x,left_index=True,right_index=True)
        R=np.array(R0)
    
  4. 估算给定收益的最优投资组合的代码:

    out_mean,out_std,out_weight=[],[],[] 
    stockMean=np.mean(R,axis=0)
    for r in np.linspace(np.min(stockMean),np.max(stockMean),num=100):
        W = np.ones([n_stock])/n_stock    # starting from equal weights 
        b_ = [(0,1) 
        for i in range(n_stock)]          # bounds, here no short 
        c_ = ({'type':'eq', 'fun': lambda W: sum(W)-1\. })#constraint
        result=sp.optimize.minimize(objFunction,W,(R,r),method='SLSQP',constraints=c_, bounds=b_)
        if not result.success:            # handle error raise 
            BaseException(result.message)
        out_mean.append(round(r,4))       # 4 decimal places 
        std_=round(np.std(np.sum(R*result.x,axis=1)),6) 
        out_std.append(std_)
        out_weight.append(result.x)
    
  5. 绘制有效前沿的代码:

    plt.title('Efficient Frontier')
    plt.xlabel('Standard Deviation of the porfolio (Risk))') 
    plt.ylabel('Return of the portfolio') 
    plt.figtext(0.5,0.75,str(n_stock)+' stock are used: ') 
    plt.figtext(0.5,0.7,' '+str(stocks))
    plt.figtext(0.5,0.65,'Time period: '+str(begYear)+' ------ '+str(endYear)) 
    plt.plot(out_std,out_mean,'--')
    plt.show()
    

理解该程序的关键是其目标函数,在# 函数 2:目标函数标题下。我们的目标是,对于给定的目标投资组合的均值或预期值,我们将最小化投资组合的风险。命令行的第一部分np.sqrt(port_var) + penalty是投资组合方差。第一项没有歧义。现在,让我们转到第二项,称为惩罚项,它定义为投资组合均值与目标均值的绝对偏差乘以一个大数值。这是一种通过使用无约束优化过程来定义目标函数的非常流行的方式。另一种方式是应用带有约束条件的优化过程。输出图像如下所示:

构建具有 n 只股票的有效前沿

在之前的一个程序中,我们的目标函数是最大化夏普比率。从上一章我们知道,当考虑的投资组合不是我们所有的财富时,夏普比率可能不是一个好的衡量标准。作为夏普比率的修正,特雷诺比率定义如下:

构建含有 n 个股票的有效前沿

在这里,左侧是特雷诺比率,构建含有 n 个股票的有效前沿 是平均投资组合收益,构建含有 n 个股票的有效前沿 是无风险利率,构建含有 n 个股票的有效前沿 是投资组合的贝塔值。唯一的修改是,将西格玛(总风险)替换为贝塔(市场风险)。

在以下程序中,特雷诺比率将是我们的目标函数:

import matplotlib.pyplot as plt
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import numpy as np
import pandas as pd
import scipy as sp
from scipy.optimize import fmin

# Step 1: input area
ticker=('IBM','WMT','C')   # tickers
begdate=(1990,1,1)         # beginning date 
enddate=(2012,12,31)       # ending date
rf=0.0003                  # annual risk-free rate
betaGiven=(0.8,0.4,0.3)    # given beta's 

# Step 2: define a few functions

# function 1: 
def ret_annual(ticker,begdate,enddte):
    x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
    logret =sp.log(x.aclose[1:]/x.aclose[:-1])
    date=[]
    d0=x.date
    for i in range(0,sp.size(logret)):
        date.append(d0[i].strftime("%Y"))
    y=pd.DataFrame(logret,date,columns=[ticker])
    return sp.exp(y.groupby(y.index).sum())-1

# function 2: estimate portfolio beta 
def portfolioBeta(betaGiven,w):
    #print("betaGiven=",betaGiven,"w=",w)
    return sp.dot(betaGiven,w)
# function 3: estimate Treynor
def treynor(R,w):
    betaP=portfolioBeta(betaGiven,w)
    mean_return=sp.mean(R,axis=0)
    ret = sp.array(mean_return)
    return (sp.dot(w,ret) - rf)/betaP

# function 4: for given n-1 weights, return a negative Sharpe ratio
def negative_treynor_n_minus_1_stock(w):
    w2=sp.append(w,1-sum(w))
    return -treynor(R,w2)        # using a return matrix here!!!!!!

# Step 3: generate a return matrix (annul return)
n=len(ticker)                    # number of stocks
x2=ret_annual(ticker[0],begdate,enddate) 
for i in range(1,n):
    x_=ret_annual(ticker[i],begdate,enddate) 
    x2=pd.merge(x2,x_,left_index=True,right_index=True)
# using scipy array format 
R = sp.array(x2)
print('Efficient porfolio (Treynor ratio) :ticker used')
print(ticker)
print('Treynor ratio for an equal-weighted portfolio')
equal_w=sp.ones(n, dtype=float) * 1.0 /n 
print(equal_w)
print(treynor(R,equal_w))

# for n stocks, we could only choose n-1 weights
w0= sp.ones(n-1, dtype=float) * 1.0 /n 
w1 = fmin(negative_treynor_n_minus_1_stock,w0)
final_w = sp.append(w1, 1 - sum(w1))
final_treynor = treynor(R,final_w)
print ('Optimal weights are ')
print (final_w)
print ('final Sharpe ratio is ')
print(final_treynor)

输出如下所示:

构建含有 n 个股票的有效前沿

另一个反对在夏普比率中使用标准差的观点是,它考虑了上下两方面的偏差,低于均值和高于均值。然而,我们知道投资者更关注下行风险(低于均值的偏差)。夏普比率的第二个问题是,对于分子,我们将均值收益与无风险利率进行比较。然而,对于分母,偏差是基于均值收益,而不是相同的无风险利率。为了克服这两个缺点,提出了一种所谓的下行偏差标准差LPSD)。假设我们有 n 个收益和一个无风险利率Rf)。进一步假设,有 m 个收益低于该无风险利率。我们通过仅使用这 m 个收益来估算 LPSD,其定义如下:

构建含有 n 个股票的有效前沿

以下程序展示了如何为给定的收益集估算 LPSD:

import scipy as sp
import numpy as np
mean=0.15;
Rf=0.01
std=0.20
n=200
sp.random.seed(3412)
x=sp.random.normal(loc=mean,scale=std,size=n)
def LPSD_f(returns, Rf):
    y=returns[returns-Rf<0]  
    m=len(y)
    total=0.0
    for i in sp.arange(m):
        total+=(y[i]-Rf)**2
    return total/(m-1)
answer=LPSD_f(x,Rf)
print("LPSD=",answer)
('LPSD=', 0.022416749724544906)

类似于夏普比率和特雷诺比率,索提诺比率的定义如下:

构建含有 n 个股票的有效前沿

以下程序将最大化给定几只股票的索提诺比率:

import scipy as sp
import numpy as np
import pandas as pd
from scipy.optimize import fmin
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
# Step 1: input area
ticker=('IBM','WMT','C')   # tickers
begdate=(1990,1,1)         # beginning date 
enddate=(2012,12,31)       # ending date
rf=0.0003                  # annual risk-free rate
#
# Step 2: define a few functions
# function 1: 
def ret_annual(ticker,begdate,enddte):
    x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
    logret =sp.log(x.aclose[1:]/x.aclose[:-1])
    date=[]
    d0=x.date
    for i in range(0,sp.size(logret)):
        date.append(d0[i].strftime("%Y"))
    y=pd.DataFrame(logret,date,columns=[ticker])
    return sp.exp(y.groupby(y.index).sum())-1

# function 2: estimate LPSD
def LPSD_f(returns, Rf):
    y=returns[returns-Rf<0]  
    m=len(y)
    total=0.0
    for i in sp.arange(m):
        total+=(y[i]-Rf)**2
    return total/(m-1)

# function 3: estimate Sortino
def sortino(R,w):
    mean_return=sp.mean(R,axis=0)
    ret = sp.array(mean_return)
    LPSD=LPSD_f(R,rf)
    return (sp.dot(w,ret) - rf)/LPSD

# function 4: for given n-1 weights, return a negative sharpe ratio
def negative_sortino_n_minus_1_stock(w):
    w2=sp.append(w,1-sum(w))
    return -sortino(R,w2)        # using a return matrix here!!!!!!

# Step 3: generate a return matrix (annul return)
n=len(ticker)              # number of stocks
x2=ret_annual(ticker[0],begdate,enddate) 
for i in range(1,n):
    x_=ret_annual(ticker[i],begdate,enddate) 
    x2=pd.merge(x2,x_,left_index=True,right_index=True)

# using scipy array format 
R = sp.array(x2)
print('Efficient porfolio (mean-variance) :ticker used')
print(ticker)
print('Sortino ratio for an equal-weighted portfolio')
equal_w=sp.ones(n, dtype=float) * 1.0 /n 
print(equal_w)
print(sortino(R,equal_w))
# for n stocks, we could only choose n-1 weights
w0= sp.ones(n-1, dtype=float) * 1.0 /n 
w1 = fmin(negative_sortino_n_minus_1_stock,w0)
final_w = sp.append(w1, 1 - sum(w1))
final_sortino = sortino(R,final_w)
print ('Optimal weights are ')
print (final_w)
print ('final Sortino ratio is ')
print(final_sortino)

这是相应的输出:

构建含有 n 个股票的有效前沿

Modigliani 和 Modigliani(1997)提出了另一种绩效衡量方法。他们的基准是一个特定的市场指数。我们以标准普尔 500 指数为例。假设我们的投资组合相比标准普尔 500 市场指数具有更高的风险和更高的回报:

构建含有 n 个股票的有效前沿

这是他们的两步法:

  1. 通过为我们的原始投资组合设置两个权重 w 和(1-w)来形成一个新投资组合,该新投资组合的风险与标准普尔 500 市场指数相同:构建含有 n 个股票的有效前沿

    实际上,w的权重将由以下公式给出:

    构建一个由 n 只股票组成的有效前沿

  2. 使用以下公式计算投资组合的平均回报:构建一个由 n 只股票组成的有效前沿

最终的判断标准是这个新的风险调整后的投资组合回报是否大于或小于标准普尔 500 指数的平均回报。以下 Python 程序实现了这一点:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import scipy as sp

begdate=(2012,1,1)
enddate=(2016,12,31)
ticker='IBM'

def ret_f(ticker):  #  function 1
    x = getData(ticker,begdate,enddate,asobject=True,adjusted=True)
    ret=x.aclose[1:]/x.aclose[:-1]-1 
    ddate=x['date'][1:]
    y=pd.DataFrame(ret,columns=[ticker],index=ddate) 
    return y.groupby(y.index).sum()

a=ret_f(ticker)
b=ret_f("^GSPC")
c=pd.merge(a,b,left_index=True, right_index=True)
print(c.head())
mean=sp.mean(c)
print(mean)
cov=sp.dot(c.T,c)
print(cov)

输出结果如下:

构建一个由 n 只股票组成的有效前沿

有不同的加权方案来估算投资组合的回报。常用的有市值加权、等权重加权和价格加权。在估算某些指数时,市值加权也叫做市值加权法。例如,标准普尔 500 指数的回报是市值加权的,而道琼斯工业平均指数则是价格加权的。等权重加权是最简单的一种:

构建一个由 n 只股票组成的有效前沿

这里,构建一个由 n 只股票组成的有效前沿表示时间t时的投资组合回报,构建一个由 n 只股票组成的有效前沿表示股票i在时间t的回报,n是投资组合中的股票数量。这里有一个非常简单的例子,假设我们的投资组合中有两只股票。去年,股票 A 的回报为 20%,而股票 B 的回报为-10%,那么基于这两个值的等权重回报是多少?答案是 5%。对于市值加权指数,关键是权重构建一个由 n 只股票组成的有效前沿,请看下面的公式:

构建一个由 n 只股票组成的有效前沿

这里的vi是我们对第 i 只股票的投资金额,构建一个由 n 只股票组成的有效前沿是投资组合的总价值。假设我们有一个由 2 只股票组成的投资组合。去年,股票 A(B)的回报分别为 20%(-10%)。如果我们对股票 A 和 B 的投资比例为 90%与 10%,那么它们的市值加权回报是多少?答案是17% (0.90.2+0.1(-0.1))。对于像标准普尔 500 指数这样的市场指数,vi将是股票 i 的市值,所有 500 只股票市值的总和将是该指数投资组合的市场价值。在估算市值加权市场指数时,小市值股票的影响很小,因为它们的权重非常小。这里有一个简单的例子,使用yanMonthly.pkl文件,可以从canisius.edu/~yany/python/yanMonthly.pkl下载:

import scipy as sp
import pandas as pd
x=pd.read_pickle("c:/temp/yanMonthly.pkl")
def ret_f(ticker):
    a=x[x.index==ticker]
    p=sp.array(a['VALUE'])
    ddate=a['DATE'][1:]
    ret=p[1:]/p[:-1]-1
    out1=pd.DataFrame(p[1:],index=ddate)
    out2=pd.DataFrame(ret,index=ddate)
    output=pd.merge(out1,out2,left_index=True, right_index=True)
    output.columns=['Price_'+ticker,'Ret_'+ticker]
    return output
a=ret_f("IBM")
b=ret_f('WMT')
c=pd.merge(a,b,left_index=True, right_index=True)
print(c.head())

这是输出结果:

构建一个由 n 只股票组成的有效前沿

由于只有两只股票,我们可以手动计算几个不同权重方案的几天回报。我们以最后的观察数据,1973 年 1 月为例,假设我们有 100 股 IBM 和 200 股沃尔玛股票。等权月度回报为 -0.08 (0.04-0.2)/2)。对于价值加权回报,我们估算两个权重,假设我们使用前一个价格来估算这些权重。总值为 1007.04 + 2000.05= 714。因此 w1= 0.9859944 (704/714)w2=0.0140056。价值加权回报为 0.0366,即 0.98599440.04 + 0.0140056(-0.2)。对于价格加权投资组合,格式与价值加权类似。主要的区别是如何定义其权重:

构建一个有效的前沿面,含 n 个股票

这里,构建一个有效的前沿面,含 n 个股票i 股票的价格。从某种意义上说,价格加权投资组合可以看作是我们每只股票在投资组合中只有一股,且是同样的 2 只股票投资组合。去年,A 股票(B 股票)的回报分别是 20%(-10%)。如果 A 股票(B 股票)的价格分别为 10 美元(90 美元),那么价格加权投资组合的回报将是-7%,即 0.2(10/100)-0.1(90/100)。显然,价格较高的股票会有更高的权重。根据前述 IBM 和沃尔玛的结果,价格加权方案的两个权重是 0.9929478;即 7.04/(7.04+0.05)0.007052186。因此,当月的价格加权投资组合回报为 0.03830747,即 0.99294780.04 + 0.007052186(-0.2)

估算投资组合或指数收益时会有一些曲折。第一个曲折是收益是否包含股息和其他分红。例如,CRSP数据库有EWRETDEWRETXEWRETD定义为基于股票收益包括股息的等权市场回报,即总回报。EWRETX定义为不包括股息或其他分红的等权市场回报。类似地,对于价值加权收益,有VWRETDVWRETX。第二个曲折是,通常使用上一期的市值作为权重,而不是当前的市值。

参考文献

请参考以下文章:

  • Markowitz, Harry, 1952, 投资组合 选择, 金融杂志 8,77-91, onlinelibrary.wiley.com/doi/10.1111/j.1540-6261.1952.tb01525.x/full

  • Modigliani, Franco, 1997, 风险调整后的表现, 投资组合管理杂志, 45–54

  • Sharpe, William F., 1994, Sharpe 比率, 投资组合管理杂志 21 (1), 49–58

  • Sharpe, W. F., 1966, 共同基金表现, 商业杂志 39 (S1), 119–138

  • Scipy 手册, 数学优化:寻找函数的最小值, www.scipy-lectures.org/advanced/mathematical_optimization/

  • Sortino, F.A., Price, L.N.,1994, Performance measurement in a downside risk framework, Journal of Investing 3, 50–8

  • Treynor, Jack L., 1965, How to Rate Management of Investment Funds, Harvard Business Review 43, pp. 63–75

附录 A – 数据案例 #5 - 你更喜欢哪个行业投资组合?

请完成以下目标:

  1. 理解 49 个行业的定义。

  2. 学习如何从法国教授的数据库下载数据。

  3. 理解效用函数,见此处。

  4. 找出哪种行业适合不同类型的投资者。

  5. 学习如何绘制无差异曲线(仅针对一个最优投资组合)。

    步骤:

  6. 访问 Professor French's Data Librarymba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html

  7. 点击 49 Industry Portfolios 右侧的 CSV,查看以下截图:附录 A – 数据案例 #5 - 你更喜欢哪个行业投资组合?

  8. 估算价值加权和等权行业投资组合的收益和方差。

  9. 为三种类型的投资者(A=1、2 和 4)估算效用函数:附录 A – 数据案例 #5 - 你更喜欢哪个行业投资组合?

    这里 U 是效用函数,E(R) 是预期投资组合回报,我们可以使用其均值进行近似,A 是风险厌恶系数,σ2 是投资组合的方差。

  10. 选择一个结果,例如,为风险偏好为 1 的投资者绘制最优价值加权投资组合的无差异曲线。

  11. 对你的结果进行评论。

mba.tuck.dartmouth.edu/pages/faculty/ken.french/Data_Library/det_49_ind_port.html,我们可以找到这 49 个行业的定义。

附录 B – 数据案例 #6 - 复制 S&P500 月度回报

为完成这个数据案例,你所在的学校已经订阅了 CRSP 数据库。

目标:

  1. 理解等权和价值加权市场指数的概念。

  2. 编写 Python 程序来复制 S&P500 月度回报。

  3. 对你的结果进行评论。

    注意

    数据来源:CRSP

    sp500monthly.pkl

    sp500add.pkl

    stockMonthly.pkl

对于 sp500monthly.pkl,请查看以下几点观察:

import pandas as pd
x=pd.read_pickle("c:/temp/sp500monthly.pkl")
print(x.head())
print(x.tail())
      DATE    VWRETD    EWRETD    VWRETX    EWRETX  SP500INDEX  SP500RET   N
0  19251231       NaN       NaN       NaN       NaN       12.46       NaN  89
1  19260130 -0.001780  0.006457 -0.003980  0.003250       12.74  0.022472  89
2  19260227 -0.033290 -0.039970 -0.037870 -0.042450       12.18 -0.043950  89
3  19260331 -0.057700 -0.067910 -0.062000 -0.073270       11.46 -0.059110  89
4  19260430  0.038522  0.031441  0.034856  0.027121       11.72  0.022688  89
          DATE    VWRETD    EWRETD    VWRETX    EWRETX  SP500INDEX  SP500RET \
1076  20150831 -0.059940 -0.052900 -0.062280 -0.054850     1972.18 -0.062580   
1077  20150930 -0.024530 -0.033490 -0.026240 -0.035550     1920.03 -0.026440   
1078  20151030  0.083284  0.073199  0.081880  0.071983     2079.36  0.082983   
1079  20151130  0.003317  0.002952  0.000771  0.000438     2080.41  0.000505   
1080  20151231 -0.015180 -0.025550 -0.017010 -0.027650     2043.94 -0.017530

对于 sp500add.pkl,请查看以下几点观察:

import pandas as pd
x=pd.read_pickle("c:/temp/sp500add.pkl")
print(x.head())
print(x.tail())
  PERMNO  DATEADDED  DAYDELETED
0   10006   19570301    19840718
1   10030   19570301    19690108
2   10049   19251231    19321001
3   10057   19570301    19920702
4   10078   19920820    20100128
      PERMNO  DATEADDED  DAYDELETED
1847   93002   20140508    20151231
1848   93089   20151008    20151231
1849   93096   20121203    20151231
1850   93159   20120731    20151231
1851   93422   20100701    20150630

对于最后一个名为 stockMonthly.pkl 的数据集,查看其中的几条观察:

import pandas as pd
x=pd.read_pickle("c:/temp/stockMonthly.pkl")
print(x.head())
print(x.tail())

输出结果如下:

         Date    Return  Volume   Price  SharesOutStanding
permno                                                        
10000  1985-12-31       NaN     NaN     NaN                NaN
10000  1986-01-31       NaN  1771.0 -4.3750             3680.0
10000  1986-02-28 -0.257140   828.0 -3.2500             3680.0
10000  1986-03-31  0.365385  1078.0 -4.4375             3680.0
10000  1986-04-30 -0.098590   957.0 -4.0000             3793.0
             Date    Return     Volume     Price  SharesOutStanding
permno                                                             
93436  2014-08-29  0.207792  1149281.0  269.7000           124630.0
93436  2014-09-30 -0.100180  1329469.0  242.6799           125366.0
93436  2014-10-31 -0.004030  1521398.0  241.7000           125382.0
93436  2014-11-28  0.011667  1077170.0  244.5200           125382.0
93436  2014-12-31 -0.090420  1271222.0  222.4100           125382.0

练习

  1. 不要把所有的鸡蛋放在一个篮子里 这一说法背后的假设是什么?

  2. 风险的衡量标准是什么?

  3. 如何衡量两只股票回报之间的共同波动?

  4. 为什么在评估两只股票之间的共同波动时,相关性被认为比协方差更为优越?

  5. 对于两只股票 A 和 B,分别有两组(σA, σB)和(βA, βB),在比较它们的预期收益时,哪个组合更重要?

  6. 历史回报的方差和相关性是否总是具有相同的符号?

  7. 找出以下代码中的低效之处:

    import scipy as sp
    sigma1=0.02
    sigma2=0.05
    rho=-1
    n=1000
    portVar=10   # assign a big number
    tiny=1.0/n
    for i in sp.arange(n):
        w1=i*tiny
        w2=1-w1
        var=w1**2*sigma1**2 +w2**2*sigma2**2+2*w1*w2*rho*sigma1*sigma2
        if(var<portVar):
            portVar=var
            finalW1=w1
        #print(vol)
    print("min vol=",sp.sqrt(portVar), "w1=",finalW1)
    
  8. 对于给定的 σA、σB 和相关性(ρ),编写一个 Python 程序来测试我们是否有解。

    注意

    测试这个方程!练习

    练习

  9. 方差和相关性之间有什么区别?编写一个 Python 程序来找出给定回报集的结果。

  10. 这里定义了投资组合风险。相关性对投资组合风险的影响是什么?练习

  11. 对于多只股票,如 MSFTIBMWMT^GSPCCAAA,基于过去五年每月回报数据,估算它们的方差-协方差矩阵和相关性矩阵。哪两只股票的相关性最强?

  12. 基于最新的五年每月数据和每日数据,IBMWMT 之间的相关性是多少?它们是否相同?

  13. 为市场指数和几只股票生成一个方差-协方差矩阵。它们的股票代码是 CMSFTIBMWMTAAPLAFAIGAP^GSPC

  14. 股票之间的相关性是否随时间变化保持不变?

    提示

    你可以选择几只股票,然后估算它们在若干个五年窗口中的相关性。

  15. 市值较大的股票,是否比市值较小的股票之间有更强的相关性?

  16. 要形成一个投资组合,我们有以下三只股票可供选择:

    • 是否有可能形成一个零风险的 2 股票投资组合?

    • 这两只股票的权重是多少(以形成一个无风险投资组合)?

      股票 方差 股票 方差 股票 方差
      A 0.0026 B 0.0418 C 0.0296

这里给出了相应的相关性(系数)矩阵:

A B C
A 1.0 -1.0 0.0
B -1.0 1.0 0.7
C 0.0 0.7 1.0
  1. 在计算方差或标准差时,通常有两种定义,基于总体或基于样本。区别在于分母。如果是基于总体,我们有以下公式:练习练习

如果是基于样本,我们有以下公式:

  1. 了解 scipy.var()scipy.std() 函数是基于样本还是基于总体。

  2. 编写一个 Python 程序,通过使用你自己的权重和最新的 10 年数据,估算 20 只股票的预期投资组合回报率。

  3. 对于 50 只股票,选择至少五年的数据。估算每只股票的波动率,它们的平均值将是Exercises。然后形成几个等权重的 2 只股票投资组合并估算它们的波动率。它们的平均值将是我们的Exercises。继续这个过程,![

  4. 为行业找到一个合适的定义。从每个行业中选择七只股票,估算它们的相关矩阵。然后对另一个行业做相同的操作。对你的结果进行评论。

  5. 编写一个 Python 程序,使用 10 只股票估算最佳投资组合。

  6. 查找五个行业的相关性平均值,每个行业至少 10 只股票。

  7. 为了估算投资组合的波动率,我们有两个公式:一个是针对 2 只股票投资组合的公式,另一个是针对 n 只股票投资组合的公式。证明当 n 等于 2 时,我们可以展开公式来估算 n 只股票投资组合的波动率;最终得到的结果与 2 只股票投资组合的公式相同。

  8. 以下说法是否正确?请证明或反驳它。

    提示

    股票收益是无相关的。

  9. 下载一年的 IBM 日数据,并使用两种方法估算其夏普比率:其定义,并编写一个sharpe()函数在 Python 中。

  10. 更新yanMonthly.pklcanisius.edu/~yany/python/yanMonthly.pkl,查看以下的前几行和最后几行。请注意,对于股票,VALUE是每月的股票价格;对于法玛-弗伦奇因子,VALUE是它们的因子,即它们的每月投资组合回报:

    import pandas as pd
    x=pd.read_pickle('c:/temp/yanMonthly.pkl')
    print(x.head(2))
    print(x.tail(3))
                  DATE   VALUE
    NAME                       
    000001.SS  19901231  127.61
    000001.SS  19910131  129.97
               DATE    VALUE
    NAME                    
    ^TWII  20130930  8173.87
    ^TWII  20131031  8450.06
    ^TWII  20131122  8116.78
    
  11. 对于 Markowitz 的优化,仅使用前两个矩 moments。这是为什么?第三和第四矩的定义是什么?忽略这两个矩时会产生什么影响?如何将它们包含在内?

  12. 编写一个 Python 程序,估算 2012 年 1 月 2 日至 2013 年 12 月 31 日,10 只股票的等权重和市值加权每月回报。使用的数据是yanMonthly.pklcanisius.edu/~yany/python/yanMonthly.pkl。对于市值加权回报,权重为上月的股票数量与股票价格的乘积。

  13. 对于这个问题,假设你的学校已订阅证券价格研究中心CRSP)数据库。在 CRSP 中复制VWRETDEWRETD。请注意,应使用每月的 CRSP 数据集。以下是名为stockMonthly.pkl的数据集中的一些观测值:

    import pandas as pd
    x=pd.read_pickle("c:/temp/stockMonthly.pkl")
    print(x.head())
    print(x.tail())
    

    输出如下所示:

             Date    Return  Volume   Price  SharesOutStanding
    permno                                                        
    10000  1985-12-31       NaN     NaN     NaN                NaN
    10000  1986-01-31       NaN  1771.0 -4.3750             3680.0
    10000  1986-02-28 -0.257140   828.0 -3.2500             3680.0
    10000  1986-03-31  0.365385  1078.0 -4.4375             3680.0
    10000  1986-04-30 -0.098590   957.0 -4.0000             3793.0
                 Date    Return     Volume     Price  SharesOutStanding
    permno                                                             
    93436  2014-08-29  0.207792  1149281.0  269.7000           124630.0
    93436  2014-09-30 -0.100180  1329469.0  242.6799           125366.0
    93436  2014-10-31 -0.004030  1521398.0  241.7000           125382.0
    93436  2014-11-28  0.011667  1077170.0  244.5200           125382.0
    93436  2014-12-31 -0.090420  1271222.0  222.4100           125382.0
    
  14. 编写一个 Python 程序,完成 Modigliani 和 Modigliani(1997)的业绩测试。

  15. 对于多个绩效衡量标准,如夏普比率、特雷诺比率和索提诺比率,请参见此处,通过将它们进行比较,福利与成本被分开:习题习题习题

    另一方面,效用函数,参见以下公式,也通过选择它们的差值来平衡福利与成本:

    习题

    比较这两种方法。我们能否有一个更一般的形式来结合这两种方法?

  16. 估算法马-法伦奇 49 个行业的夏普比率、特雷诺比率和索提诺比率。无风险利率可以在finance.yahoo.com/bonds找到。或者,可以使用来自ffMonthly.pkl的无风险利率,canisius.edu/~yany/python/ffMonthly.pkl。使用的数据集是ff49industries.pkl,可以从canisius.edu/~yany/python/ff49industries.pkl下载。这里展示了几行:

    import pandas as pd
    x=pd.read_pickle("c:/temp/ff49industries.pkl")
    print(x.head(2))
              Agric    Food     Soda     Beer     Smoke    Toys     Fun    \
    192607     2.37     0.12   -99.99    -5.19     1.29     8.65     2.50   
    192608     2.23     2.68   -99.99    27.03     6.50    16.81    -0.76   
              Books    Hshld    Clths   ...       Boxes    Trans    Whlsl  \
    192607    50.21    -0.48     8.08   ...        7.70     1.94   -23.79   
    192608    42.98    -3.58    -2.51   ...       -2.38     4.88     5.39   
              Rtail    Meals    Banks    Insur    RlEst    Fin      Other  
    192607     0.07     1.87     4.61    -0.54     2.89    -4.85     5.20  
    192608    -0.75    -0.13    11.83     2.57     5.30    -0.57     6.76  
    [2 rows x 49 columns]
    

总结

本章首先解释了与投资组合理论相关的各种概念,例如成对股票和投资组合的协方差和相关性。之后,我们讨论了个别股票或投资组合的各种风险衡量标准,如夏普比率、特雷诺比率和索提诺比率,如何基于这些衡量标准(比率)最小化投资组合风险,如何建立目标函数,如何为给定的股票集选择有效的投资组合,以及如何构建有效前沿。

对于下一章,第十章,期权与期货,我们将首先解释一些基本概念。接着,我们将讨论著名的布莱克-斯科尔斯-默顿期权模型。此外,还将详细讨论涉及期权的各种交易策略。

第十章:期权与期货

在现代金融中,期权理论(包括期货和远期合约)及其应用发挥着重要作用。许多交易策略、公司激励计划和对冲策略都包含各种类型的期权。例如,许多高管激励计划都基于股票期权。假设一家位于美国的进口商刚刚从英国订购了一台机器,三个月后需支付 1000 万英镑。进口商面临货币风险(或汇率风险)。如果英镑对美元贬值,进口商将受益,因为他/她用更少的美元买入 1000 万英镑。相反,如果英镑对美元升值,那么进口商将遭受损失。进口商可以通过几种方式避免或减少这种风险:立即购买英镑、进入期货市场按今天确定的汇率买入英镑,或者购买一个有固定行使价格的认购期权。在本章中,我们将解释期权理论及其相关应用。特别地,以下主题将被涵盖:

  • 如何对冲货币风险以及市场普遍的短期下跌

  • 认购期权和认沽期权的支付和盈亏函数及其图形表示

  • 欧洲期权与美洲期权

  • 正态分布、标准正态分布和累积正态分布

  • 布莱克-斯科尔斯-默顿期权模型(有/无股息)

  • 各种交易策略及其可视化呈现,例如备兑认购期权、跨式期权、蝶式期权和日历差价期权

  • 德尔塔、伽马以及其他希腊字母

  • 认购期权和认沽期权的平价关系及其图形表示

  • 一步和两步二叉树模型的图形表示

  • 使用二叉树方法定价欧洲和美洲期权

  • 隐含波动率、波动率微笑和偏斜

期权理论是金融理论的重要组成部分。很难想象一位金融学学生无法理解它。然而,深入理解这一理论是非常具有挑战性的。许多金融专业的学生认为期权理论就像火箭科学一样复杂,因为它涉及到如何解决各种微分方程。为了满足尽可能多的读者需求,本章避免了复杂的数学推导。

一种期权将赋予期权买方在未来以今天确定的固定价格买入或卖出某物的权利。如果买方有权在未来购买某物,则称为认购期权。如果期权买方有权卖出某物,则称为认沽期权。由于每个交易涉及两方(买方和卖方),买方支付以获得某项权利,而卖方则收到现金流入以承担义务。与期权不同,期货合约赋予买卖双方权利和义务。与期权中买方向卖方支付初始现金流不同,期货合约通常没有初始现金流。远期合约与期货合约非常相似,只有少数例外。在本章中,这两种合约(期货和远期合约)没有做区分。远期合约比期货合约更容易分析。如果读者希望进行更深入的分析,应参考其他相关教材。

介绍期货

在讨论与期货相关的基本概念和公式之前,我们先回顾一下连续复利利率的概念。在第三章,货币的时间价值,我们学到以下公式可以用来估算给定现值的未来价值:

介绍期货

在这里,FV 是未来价值,PV 是现值,R 是有效期利率,n 是期数。例如,假设年利率APR)为 8%,按半年复利计算。如果我们今天存入 $100,两年后的未来价值是多少?以下代码显示了结果:

import scipy as ps
pv=100
APR=0.08
rate=APR/2.0
n=2
nper=n*2
fv=ps.fv(rate,nper,0,pv)
print(fv)

输出如下所示:

-116.985856

未来价值是 $116.99。在前面的程序中,有效半年利率为 4%,因为年利率为 8%,并按半年复利计算。在期权理论中,无风险利率和股息收益率定义为连续复利率。推导有效利率(或年利率)与连续复利率之间的关系是很容易的。估算给定现值的未来价值的第二种方法如下:

介绍期货

在这里,Rc 是连续复利率,T 是年份数。换句话说,在应用公式(1)时,我们可以有许多组合,例如年有效利率和年份数、有效月利率和月数等。然而,公式(2)并非如此,它只有一对:连续复利率和年份数。为了推导一个有效利率与其对应的连续复利率之间的关系,我们推荐以下简单方法:选择 $1 作为当前值,1 年作为投资期限。然后应用前两个公式并将其设为相等。假设我们知道有效半年利率为前述案例中的 4%。那么其等效的 Rc 是多少?

引入期货

我们将它们等同,得到以下方程:

引入期货

对前一个方程两边取自然对数,我们得到以下解:

引入期货

对前述方法进行简单的推广,我们得出以下公式,将有效利率转换为对应的连续复利利率:

引入期货

这里,m 是每年的复利频率:m=1, 2, 4, 12, 52, 365 分别对应年复利、半年复利、季复利、月复利、周复利和日复利。Reffective 是 APR 除以m。如果给定一个带有相关复利频率的 APR,我们可以使用以下等效转换公式:

引入期货

另一方面,从给定的连续利率推导出有效利率的公式是相当简单的:

引入期货

为了验证前面的方程,请查看以下代码:

import scipy as sp
Rc=2*log(1+0.04)
print(sp.exp(Rc/2)-1
0.040000000000000036

类似地,我们得出以下公式来估算从Rc计算得到的 APR:

引入期货

对于期货合约,我们以之前的例子为例,假设一个美国进口商将在三个月后支付 1000 万英镑。通常,汇率有两种表示方式:第一种货币与第二种货币的比值,或者相反的比值。我们假设美国为国内,英国为外国,汇率以美元/英镑表示。假设今天的汇率为 1 英镑 = 1.25 美元,国内利率为 1%,外国利率(在英国)为 2%。以下代码展示了我们今天需要多少英镑和美元:

import scipy as sp
amount=5
r_foreign=0.02
T=3./12.
exchangeRateToday=1.25
poundToday=5*sp.exp(-r_foreign*T)
print("Pound needed today=", poundToday)
usToday=exchangeRateToday*poundToday
print("US dollar needed today", usToday)
('Pound needed today=', 4.9750623959634117)
('US dollar needed today', 6.2188279949542649)

结果显示,为了满足三个月后支付 500 万英镑的需求,我们今天需要 497.5 万英镑,因为我们可以将 497.5 万英镑存入银行以赚取额外的利息(按 1%的利率)。如果进口商没有英镑,他们可以花费 621.88 万美元来购买今天所需的英镑。或者,进口商可以通过购买期货合约(或几个期货合约)来锁定一个固定的汇率,在三个月后购买英镑。此处给出的远期汇率(未来汇率)如下:

引入期货

这里,F 是期货价格(在本例中是今天确定的未来汇率),S0 是现货价格(在本例中是今天的汇率),Rd 是国内的连续复利无风险利率,Rf 是外国的连续复利存款利率,T 是年化期限。以下 Python 程序显示了今天的期货价格:

import scipy as sp
def futuresExchangeRate(s0,rateDomestic,rateForeign,T):
    futureEx=s0*sp.exp((rateDomestic-rateForeign)*T)
return futureEx

# input area

s0=1.25
rHome=0.01
rForeigh=0.02
T=3./12.
#
futures=futuresExchangeRate(s0,rHome,rForeigh,T)
print("futures=",futures)

输出如下:

('futures=', 1.246878902996825)

根据结果,三个月后的汇率应该是每英镑 1.2468789 美元。换句话说,美元应该会相对于英镑贬值。其原因基于两种利率。以下是基于无套利原则的逻辑。假设我们今天有 1.25 美元。我们有两种选择:将其存入美国银行享受 2%的利息,或将其兑换成 1 英镑并存入外资银行,享受 1%的利息。进一步假设,如果未来的汇率不是 1.246879,我们将面临套利机会。假设期货价格(汇率)为 1.26 美元,表示英镑相对于美元被高估了。套利者将低买高卖,也就是做空期货。假设我们有一个三个月到期的 1 英镑的义务。以下是套利策略:借入 1.25 美元(USD),并在三个月后以 1.26 美元的期货价格卖出 1 英镑。三个月后,我们的套利利润如下:

import scipy as sp
obligationForeign=1.0           # how much to pay in 3 months
f=1.26                          # future price
s0=1.25                         # today's exchange rate 
rHome=0.01
rForeign=0.02
T=3./12.
todayObligationForeign=obligationForeign*sp.exp(-rForeign*T)
usBorrow=todayObligationForeign*s0  
costDollarBorrow=usBorrow*sp.exp(rHome*T)
profit=f*obligationForeign-costDollarBorrow
print("profit in USD =", profit)

输出结果如下:

('profit in USD =', 0.013121097003174764)

利润为 0.15 美元。如果期货价格低于 1.246878902996825,套利者将采取相反的头寸,即做多期货合约。对于到期日前没有股息支付的股票,我们有以下期货价格:

介绍期货

这里,F是期货价格,S0是当前股票价格,Rf是持续复利的无风险利率,yield 是持续复利的股息收益率。对于已知到期日前的离散股息,我们有以下公式:

介绍期货

在这里,PV(D)是到期日前所有股息的现值。期货可以作为对冲工具或投机工具。假设某个共同基金经理担心市场可能在短期内出现负面波动。进一步假设他/她的投资组合与市场投资组合(如标准普尔 500 指数)正相关。因此,他/她应该做空标准普尔 500 指数的期货。以下是相关的公式:

介绍期货

这里,n是做多或做空的期货合约数量,βtarget是目标贝塔值,βp是当前投资组合的贝塔值,Vp是投资组合的价值,VF是一个期货合约的价值。如果n小于(大于)零,表示做空(做多)头寸。以下是一个例子。假设 John Doe 今天管理着一只价值 5000 万美元的投资组合,他的投资组合与标准普尔 500 指数的贝塔值为 1.10。他担心市场可能在未来六个月内下跌。由于交易成本,他/她无法出售其投资组合或其一部分。假设短期内他的目标贝塔值为零。每个标准普尔 500 指数点的价格是 250 美元。由于今天标准普尔 500 指数为 2297.41 点,一个期货合约的价值为 5,743,550 美元。John 应当做空(或做多)的合约数量如下:

import scipy as ps
# input area
todaySP500index=2297.42
valuePortfolio=50e6    
betaPortfolio=1.1
betaTarget=0
#
priceEachPoint=250  
contractFuturesSP500=todaySP500index*priceEachPoint
n=(betaTarget-betaPortfolio)*valuePortfolio/contractFuturesSP500
print("number of contracts SP500 futures=",n)

输出结果如下:

('number of contracts SP500 futures=', -95.75959119359979)

负值表示做空头寸。John Doe 应该做空 96 份 S&P500 期货合约。这与常识一致,因为投资组合与 S&P500 指数正相关。以下程序展示了当 S&P500 指数下跌 97 点时,是否对冲的盈亏情况:

# input area

import scipy as sp
sp500indexToday=2297.42
valuePortfolio=50e6    
betaPortfolio=1.1
betaTarget=0
sp500indexNmonthsLater=2200.0
#
priceEachPoint=250  
contractFuturesSP500=sp500indexToday*priceEachPoint
n=(betaTarget-betaPortfolio)*valuePortfolio/contractFuturesSP500
mySign=sp.sign(n)
n2=mySign*sp.ceil(abs(n))
print("number of contracts=",n2)
# hedging result
v1=sp500indexToday
v2=sp500indexNmonthsLater
lossFromPortfolio=valuePortfolio*(v2-v1)/v1
gainFromFutures=n2*(v2-v1)*priceEachPoint
net=gainFromFutures+lossFromPortfolio
print("loss from portfolio=", lossFromPortfolio)
print("gain from futures contract=", gainFromFutures)
print("net=", net)

相关输出如下所示:

('number of contracts=', -96.0)
('loss from portfolio=', -2120204.403200113)
('gain from futures contract=', 2338080.0000000019)
('net=', 217875.59679988865)

从最后三行可以知道,如果不对冲,投资组合的损失将为$212 万。另一方面,在做空 96 份 S&P500 期货合约后,在 S&P500 指数下跌 98 点的六个月后,净损失仅为$217,876。通过不同的潜在 S&P500 指数水平,我们可以找出其相关的对冲和不对冲结果。这样的对冲策略通常被称为投资组合保险。

看涨和看跌期权的回报和盈亏函数

一个期权赋予其买方在未来以预定价格(行使价)购买(看涨期权)或出售(看跌期权)某物给期权卖方的权利。例如,如果我们购买一个欧式看涨期权,以 X 美元(如$30)在三个月后获得某个股票,那么在到期日我们的回报将按照以下公式计算:

看涨和看跌期权的回报和盈亏函数

这里,看涨和看跌期权的回报和盈亏函数是到期日(T)的股票价格,行使价为 X(此例中 X=30)。假设三个月后股票价格为$25。我们不会行使看涨期权以$30 的价格购买股票,因为我们可以在公开市场上以$25 购买相同的股票。另一方面,如果股票价格为$40,我们将行使我们的权利以获取$10 的回报,即以$30 买入股票并以$40 卖出股票。以下程序展示了看涨期权的回报函数:

>>>def payoff_call(sT,x):
        return (sT-x+abs(sT-x))/2

应用payoff函数是直接的:

>>> payoff_call(25,30)
0
>>> payoff_call(40,30)
10

第一个输入变量,即到期时的股票价格T,也可以是一个数组:

>> import numpy as np
>> x=20
>> sT=np.arange(10,50,10)
>>> sT
array([10, 20, 30, 40])
>>> payoff_call(s,x)
array([  0.,   0.,  10.,  20.])
>>>

为了创建图形化展示,我们有以下代码:

import numpy as np
import matplotlib.pyplot as plt
s = np.arange(10,80,5)
x=30
payoff=(abs(s-x)+s-x)/2
plt.ylim(-10,50)
plt.plot(s,payoff)
plt.title("Payoff for a call (x=30)")
plt.xlabel("stock price")
plt.ylabel("Payoff of a call")
plt.show()

这里显示了图形:

看涨和看跌期权的回报和盈亏函数

看涨期权卖方的回报与买方相反。需要记住的是,这是一个零和游戏:你赢了,我输了。例如,一位投资者以$10 的行使价卖出三个看涨期权。当股价在到期时为$15 时,期权买方的回报是$15,而期权卖方的总损失也是$15。如果看涨期权的溢价(期权价格)为 c,则看涨期权买方的盈亏函数是其回报与初始投资(c)之间的差异。显然,期权溢价提前支付与到期日回报的现金流时间不同。在这里,我们忽略了货币的时间价值,因为到期通常相当短。

对于看涨期权买方:

看涨期权和看跌期权的盈亏函数

对于看涨期权卖方:

看涨期权和看跌期权的盈亏函数

以下图表展示了看涨期权买方和卖方的盈亏函数:

import scipy as sp
import matplotlib.pyplot as plt
s = sp.arange(30,70,5)
x=45;c=2.5
y=(abs(s-x)+s-x)/2 -c
y2=sp.zeros(len(s))
plt.ylim(-30,50)
plt.plot(s,y)
plt.plot(s,y2,'-.')
plt.plot(s,-y)
plt.title("Profit/Loss function")
plt.xlabel('Stock price')
plt.ylabel('Profit (loss)')
plt.annotate('Call option buyer', xy=(55,15), xytext=(35,20),
             arrowprops=dict(facecolor='blue',shrink=0.01),)
plt.annotate('Call option seller', xy=(55,-10), xytext=(40,-20),
             arrowprops=dict(facecolor='red',shrink=0.01),)
plt.show()

这里展示了一个图形表示:

看涨期权和看跌期权的盈亏函数

看跌期权赋予其买方在未来以预定价格 X 向看跌期权卖方出售证券(商品)的权利。其盈亏函数如下:

看涨期权和看跌期权的盈亏函数

这里,ST 是到期时的股票价格,X 是行使价格(执行价格)。对于看跌期权买方,盈亏函数如下:

看涨期权和看跌期权的盈亏函数

卖出看跌期权的盈亏函数正好相反:

看涨期权和看跌期权的盈亏函数

以下是看跌期权买方和卖方盈亏函数的相关程序和图形:

import scipy as sp
import matplotlib.pyplot as plt
s = sp.arange(30,70,5)
x=45;p=2;c=2.5
y=c-(abs(x-s)+x-s)/2 
y2=sp.zeros(len(s)) 
x3=[x, x]
y3=[-30,10]
plt.ylim(-30,50)
plt.plot(s,y) 
plt.plot(s,y2,'-.') 
plt.plot(s,-y) 
plt.plot(x3,y3)
plt.title("Profit/Loss function for a put option") 
plt.xlabel('Stock price')
plt.ylabel('Profit (loss)')
plt.annotate('Put option buyer', xy=(35,12), xytext=(35,45), arrowprops=dict(facecolor='red',shrink=0.01),)
plt.annotate('Put option seller', xy=(35,-10), xytext=(35,-25), arrowprops=dict(facecolor='blue',shrink=0.01),)
plt.annotate('Exercise price', xy=(45,-30), xytext=(50,-20), arrowprops=dict(facecolor='black',shrink=0.01),)
plt.show()

该图形如下:

看涨期权和看跌期权的盈亏函数

欧洲期权与美国期权

欧洲期权只能在到期日行使,而美国期权可以在到期日前或到期日当天任何时间行使。由于美国期权可以持有至到期,其价格(期权溢价)应该高于或等于相应的欧洲期权价格:

欧洲期权与美国期权对比

一个重要的区别是,对于欧洲期权,我们有一个封闭解,即布莱克-斯科尔斯-梅顿期权模型。然而,对于美国期权,我们没有封闭解。幸运的是,我们有几种方法可以定价美国期权。后续章节中,我们将展示如何使用二叉树方法,也称为 CRR 方法,来定价美国期权。

理解现金流、期权类型、权利和义务

我们知道,对于每个商业合同,我们有买方和卖方两个方面。期权合同也是如此。看涨期权买方将预付现金(现金流出)以获得一个权利。由于这是一个零和博弈,看涨期权卖方将享有预付现金流入,并承担相应的义务。

下表列出了这些头寸(买方或卖方)、初始现金流的方向(流入或流出)、期权买方的权利(买或卖)以及期权卖方的义务(即满足期权卖方的需求):

买方(多头头寸) 卖方(空头头寸) 欧洲期权 美国期权
看涨期权 以预定价格购买证券(商品)的权利 以预定价格出售证券(商品)的义务 只能在到期日行使 可以在到期日前或到期日当天任何时间行使
看跌期权 在预定价格下出售证券的权利 购买的义务
现金流 前期现金流出 前期现金流入

表 10.1 多头、空头头寸,初始现金流,以及权利与义务

非分红股票的 Black-Scholes-Merton 期权模型

Black-Scholes-Merton 期权模型是一个封闭式解,用于为一个没有分红支付的股票定价欧式期权。如果我们使用 非分红股票的 Black-Scholes-Merton 期权模型 或今天的价格,X 为执行价格,r 为连续复利的无风险利率,T 为到期年数,非分红股票的 Black-Scholes-Merton 期权模型 为股票的波动率,则欧式看涨期权 (c) 和看跌期权 (p) 的封闭式公式为:

非分红股票的 Black-Scholes-Merton 期权模型

在这里,N() 是累积分布标准正态分布。以下 Python 代码表示前述方程,用于评估一个欧式看涨期权:

from scipy import log,exp,sqrt,stats
def bs_call(S,X,T,r,sigma):
    d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    d2 = d1-sigma*sqrt(T)
return S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2)

在前面的程序中,stats.norm.cdf() 是累积正态分布,也就是 Black-Scholes-Merton 期权模型中的 N()。当前股票价格为 40 美元,行使价格为 42 美元,到期时间为六个月,无风险利率为 1.5%(连续复利),基础股票的波动率为 20%(连续复利)。基于前述代码,欧式看涨期权的价值为 1.56 美元:

>>>c=bs_call(40.,42.,0.5,0.015,0.2) 
>>>round(c,2)
1.56

生成我们自己的模块 p4f

我们可以将许多小的 Python 程序组合成一个程序,例如 p4f.py。例如,前面的 Python 程序中包含的 bs_call() 函数。这样的一组程序提供了多个好处。首先,当我们使用 bs_call() 函数时,不需要重新输入那五行代码。为了节省空间,我们仅展示了 p4f.py 中包含的几个函数。为了简洁起见,我们去除了每个函数中的所有注释。这些注释是为了帮助未来的用户在调用 help() 函数时,例如 help(bs_call())

def bs_call(S,X,T,rf,sigma):
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(rf+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    d2 = d1-sigma*sqrt(T)
    return S*stats.norm.cdf(d1)-X*exp(-rf*T)*stats.norm.cdf(d2)

def binomial_grid(n):
    import networkx as nx 
    import matplotlib.pyplot as plt 
    G=nx.Graph() 
    for i in range(0,n+1):     
        for j in range(1,i+2):         
            if i<n:             
                G.add_edge((i,j),(i+1,j))
                G.add_edge((i,j),(i+1,j+1)) 
    posG={}    #dictionary with nodes position 
    for node in G.nodes():     
        posG[node]=(node[0],n+2+node[0]-2*node[1]) 
    nx.draw(G,pos=posG)      

def delta_call(S,X,T,rf,sigma):
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(rf+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    return(stats.norm.cdf(d1))

def delta_put(S,X,T,rf,sigma):
    from scipy import log,exp,sqrt,stats
    d1=(log(S/X)+(rf+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    return(stats.norm.cdf(d1)-1)

要应用 Black-Scholes-Merton 看涨期权模型,我们只需使用以下代码:

>>>import p4f
>>>c=p4f.bs_call(40,42,0.5,0.015,0.2) 
>>>round(c,2)
1.56

第二个优点是节省空间并使编程更加简洁。在本章后续内容中,当我们使用一个名为 binomial_grid() 的函数时,这一点将变得更加明确。从现在开始,每当首次讨论某个函数时,我们会提供完整的代码。然而,当该程序再次使用且程序比较复杂时,我们会通过 p4f 间接调用它。要查找我们的工作目录,使用以下代码:

>>>import os
>>>print os.getcwd()

具有已知股息的欧式期权

假设我们知道在时间 T1(T1 < T)时分发的股息 d1,其中 T 为到期日。我们可以通过将 S0 替换为 S 来修改原始的 Black-Scholes-Merton 期权模型,其中 具有已知股息的欧式期权

具有已知股息的欧式期权

在前面的例子中,如果我们有已知的$1.5 股息将在一个月内支付,那么认购期权的价格是多少?

>>>import p4f
>>>s0=40
>>>d1=1.5
>>>r=0.015
>>>T=6/12
>>>s=s0-exp(-r*T*d1)
>>>x=42
>>>sigma=0.2 
>>>round(p4f.bs_call(s,x,T,r,sigma),2)
1.18

程序的第一行导入了名为p4f的模块,该模块包含了认购期权模型。结果显示,认购期权的价格为$1.18,低于之前的值($1.56)。这是可以理解的,因为标的股票在一个月内大约会下跌$1.5。由于这个原因,我们行使认购期权的机会会变小,也就是说,股票价格不太可能超过$42。前述论点适用于在 T 之前分配的已知股息,即已知股息的欧式期权

各种交易策略

在下表中,我们总结了几种常用的期权交易策略:

名称 描述 初始现金流方向 未来价格变动预期
看涨价差(认购期权) 买入一个认购期权(x1),卖出一个认购期权(x2)[x1 < x2] 支出 上涨
看涨价差(看跌期权) 买入一个看跌期权(x1),卖出一个看跌期权(x2)[x1 < x2] 进账 上涨
熊市价差(看跌期权) 买入一个看跌期权(x2),卖出一个看跌期权(x1)[x1 < x2] 支出 下跌
看跌价差(认购期权) 买入一个认购期权(x2),卖出一个认购期权(x1)[x1 < x2] 进账 下跌
交易策略 买入认购期权并卖出看跌期权(相同执行价格) 支出 上涨或下跌
条带策略 买入两个看跌期权和一个认购期权(相同的执行价格) 支出 下跌的概率 > 上涨的概率
踏带策略 买入两个认购期权和一个看跌期权(相同的执行价格) 支出 上涨的概率 > 下跌的概率
脱口而出策略 买入一个认购期权(x2)并买入一个看跌期权(x1)[x1 < x2] 支出 上涨或下跌
蝴蝶策略(认购期权) 买入两个认购期权(x1, x3),卖出两个认购期权(x2)[x2=(x1+x3)/2] 支出 保持在 x2 附近
蝴蝶策略(看跌期权) 买入两个看跌期权(x1, x3),卖出两个看跌期权(x2)[x2=(x1+x3)/2] 保持在 x2 附近
日历价差 卖出一个认购期权(T1),买入一个认购期权(T2),具有相同的执行价格,且 T1<T2 支出

表 10.2 各种交易策略

覆盖认购期权 – 持有股票并卖出认购期权

假设我们购买了 100 股 A 股票,每股价格为$10。那么,总成本为$1,000。如果我们同时写出一个认购期权合同,一个合同对应 100 股,价格为$20。那么,我们的总成本将减少$20。再假设行权价格为$12。以下是我们盈亏函数的图示:

import matplotlib.pyplot as plt 
import numpy as np
sT = np.arange(0,40,5) 
k=15;s0=10;c=2
y0=np.zeros(len(sT))
y1=sT-s0                    # stock only
y2=(abs(sT-k)+sT-k)/2-c     # long a call 
y3=y1-y2                    # covered-call 
plt.ylim(-10,30)
plt.plot(sT,y1) 
plt.plot(sT,y2) 
plt.plot(sT,y3,'red')
plt.plot(sT,y0,'b-.') 
plt.plot([k,k],[-10,10],'black')
plt.title('Covered call (long one share and short one call)') 
plt.xlabel('Stock price')
plt.ylabel('Profit (loss)')
plt.annotate('Stock only (long one share)', xy=(24,15),xytext=(15,20),arrowprops=dict(facecolor='blue',shrink=0.01),)
plt.annotate('Long one share, short a call', xy=(10,4), xytext=(9,25), arrowprops=dict(facecolor='red',shrink=0.01),)
plt.annotate('Exercise price= '+str(k), xy=(k+0.2,-10+0.5))
plt.show()

这里给出了一个图示,展示了仅持有股票、认购期权和覆盖认购期权的位置。显然,当股票价格低于$17(15 + 2)时,覆盖认购期权优于单纯持有股票:

覆盖认购期权 – 持有股票并卖出认购期权

跨式策略 – 买入认购期权和看跌期权,且行权价格相同

我们来看最简单的情形。一家公司面临下个月的不确定事件,问题在于我们不确定事件的方向,是好事还是坏事。为了利用这样的机会,我们可以同时购买看涨期权和看跌期权,且它们的行使价格相同。这意味着无论股票是上涨还是下跌,我们都将受益。进一步假设行使价格为 $30。此策略的收益如下所示:

import matplotlib.pyplot as plt 
import numpy as np
sT = np.arange(30,80,5)
x=50;    c=2; p=1
straddle=(abs(sT-x)+sT-x)/2-c + (abs(x-sT)+x-sT)/2-p 
y0=np.zeros(len(sT))
plt.ylim(-6,20) 
plt.xlim(40,70) 
plt.plot(sT,y0) 
plt.plot(sT,straddle,'r')
plt.plot([x,x],[-6,4],'g-.')
plt.title("Profit-loss for a Straddle") 
plt.xlabel('Stock price') 
plt.ylabel('Profit (loss)')
plt.annotate('Point 1='+str(x-c-p), xy=(x-p-c,0), xytext=(x-p-c,10),
arrowprops=dict(facecolor='red',shrink=0.01),) 
plt.annotate('Point 2='+str(x+c+p), xy=(x+p+c,0), xytext=(x+p+c,13),
arrowprops=dict(facecolor='blue',shrink=0.01),) 
plt.annotate('exercise price', xy=(x+1,-5))
plt.annotate('Buy a call and buy a put with the same exercise price',xy=(45,16))
plt.show()

Straddle – 购买具有相同行使价格的看涨期权和看跌期权

上图显示了无论股票如何波动,我们都会获利。我们会亏损吗?显然,当股票变化不大时,我们的预期未能实现。

使用看涨期权的蝶式交易

当购买两个行使价格为 x1x3 的看涨期权,并卖出两个行使价格为 x2 的看涨期权时,其中 x2=(x1+x2)/2,且期权到期日相同,标的股票也相同,我们称之为蝶式交易。其盈亏函数如下所示:

import matplotlib.pyplot as plt 
import numpy as np
sT = np.arange(30,80,5) 
x1=50;    c1=10
x2=55;    c2=7
x3=60;    c3=5
y1=(abs(sT-x1)+sT-x1)/2-c1 
y2=(abs(sT-x2)+sT-x2)/2-c2 
y3=(abs(sT-x3)+sT-x3)/2-c3 
butter_fly=y1+y3-2*y2 
y0=np.zeros(len(sT))
plt.ylim(-20,20) 
plt.xlim(40,70) 
plt.plot(sT,y0) 
plt.plot(sT,y1) 
plt.plot(sT,-y2,'-.') 
plt.plot(sT,y3)
plt.plot(sT,butter_fly,'r') 
plt.title("Profit-loss for a Butterfly") 
plt.xlabel('Stock price')
plt.ylabel('Profit (loss)')
plt.annotate('Butterfly', xy=(53,3), xytext=(42,4), arrowprops=dict(facecolor='red',shrink=0.01),)
plt.annotate('Buy 2 calls with x1, x3 and sell 2 calls with x2', xy=(45,16))
plt.annotate('    x2=(x1+x3)/2', xy=(45,14)) 
plt.annotate('    x1=50, x2=55, x3=60',xy=(45,12)) 
plt.annotate('    c1=10,c2=7, c3=5', xy=(45,10)) 
plt.show()

相关图表如下所示:

使用看涨期权的蝶式交易

输入值与期权值之间的关系

当标的股票的波动性增加时,其看涨期权和看跌期权的价值都会增加。其逻辑是,当股票变得更具波动性时,我们有更好的机会观察到极端值,也就是说,我们有更大的机会行使我们的期权。以下 Python 程序展示了这一关系:

import numpy as np
import p4f as pf
import matplotlib.pyplot as plt
s0=30
T0=0.5
sigma0=0.2
r0=0.05
x0=30
sigma=np.arange(0.05,0.8,0.05)
T=np.arange(0.5,2.0,0.5)
call_0=pf.bs_call(s0,x0,T0,r0,sigma0)
call_sigma=pf.bs_call(s0,x0,T0,r0,sigma)
call_T=pf.bs_call(s0,x0,T,r0,sigma0)
plt.title("Relationship between sigma and call, T and call")
plt.plot(sigma,call_sigma,'b')
plt.plot(T,call_T,'r')
plt.annotate('x=Sigma, y=call price', xy=(0.6,5), xytext=(1,6), arrowprops=dict(facecolor='blue',shrink=0.01),)
plt.annotate('x=T(maturity), y=call price', xy=(1,3), xytext=(0.8,1), arrowprops=dict(facecolor='red',shrink=0.01),)
plt.ylabel("Call premium")
plt.xlabel("Sigma (volatility) or T(maturity) ")
plt.show()

相应的图表如下所示:

输入值与期权值之间的关系

希腊字母

Delta Greeks 定义为期权对其标的证券价格的导数。看涨期权的 delta 定义如下:

Greeks

欧式看涨期权在没有分红的股票上的 delta 定义为:

Greeks

delta_call() 的程序非常简单。由于它包含在 p4f.py 中,我们可以轻松地调用它:

>>>>from p4f import *
>>> round(delta_call(40,40,1,0.1,0.2),4)
0.7257

欧式看跌期权在无分红的股票上的 delta 为:

Greeks

>>>>from p4f import *
>>> round(delta_put(40,40,1,0.1,0.2),4)
-0.2743

Gamma 是 delta 对价格的变化率,如下公式所示:

Greeks

对于欧式看涨期权(或看跌期权),其 gamma 如下所示,其中 Greeks

Greeks

欧式看涨期权和看跌期权的希腊字母的数学定义如下表所示:

Greeks

表 10.1 希腊字母的数学定义

请注意,在表格中,Greeks

显然,很少有人能记住这些公式。这里有一个非常简单的方法,基于它们的定义:

Greeks

表 10.2 估算希腊字母的简单方法

如何记住?

  • Delta:一阶导数

  • Gamma:二阶导数

  • Theta:时间(T)

  • Vega:波动性(V)

  • Rho:利率(R)

例如,根据 delta 的定义,我们知道它是c2 - c1s2 - s1的比率。因此,我们可以生成一个小的数值来生成这两个对;见以下代码:

from scipy import log,exp,sqrt,stats
tiny=1e-9
S=40
X=40
T=0.5
r=0.01
sigma=0.2

def bsCall(S,X,T,r,sigma):
    d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    d2 = d1-sigma*sqrt(T)
    return S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2)

def delta1(S,X,T,r,sigma):
    d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T))
    return stats.norm.cdf(d1)

def delta2(S,X,T,r,sigma):
    s1=S
    s2=S+tiny
    c1=bsCall(s1,X,T,r,sigma)
    c2=bsCall(s2,X,T,r,sigma)
    delta=(c2-c1)/(s2-s1)
    return delta

print("delta (close form)=", delta1(S,X,T,r,sigma))
print("delta (tiny number)=", delta2(S,X,T,r,sigma))
('delta (close form)=', 0.54223501331161406)
('delta (tiny number)=', 0.54223835949323917)

根据最后两个值,差异非常小。我们可以将此方法应用于其他希腊字母,参见章节末尾的问题。

看跌-看涨平价及其图示

让我们来看一个行使价格为$20,期限为三个月,风险自由利率为 5%的看涨期权。这个未来$20 的现值如下面所示:

>>>x=20*exp(-0.05*3/12)   
>>>round(x,2)
19.75
>>>

在三个月后,由一个看涨期权和今天$19.75 现金组成的投资组合的财富将是多少?如果股票价格低于$20,我们不会行使看涨期权,而是保留现金。如果股票价格高于$20,我们用$20 现金行使看涨期权以拥有股票。因此,我们的投资组合价值将是这两者中的最大值:三个月后的股票价格或$20,即max(s,20)

另一方面,假设有一个由股票和一个行使价格为$20 的看跌期权组成的投资组合。如果股票价格下跌$20,我们行使看跌期权并获得$20。如果股票价格高于$20,我们就直接持有股票。因此,我们的投资组合价值将是这两者中的最大值:三个月后的股票价格或$20,即max(s,20)

因此,对于这两个投资组合,我们都有相同的最终财富max(s,20)。根据无套利原理,这两个投资组合的现值应该相等。我们称之为看跌-看涨平价:

看跌-看涨平价及其图示

当股票在到期日之前有已知的股息支付时,我们有以下等式:

看跌-看涨平价及其图示

这里,D是所有股息支付在其到期日之前的现值(T)。以下 Python 程序提供了看跌-看涨平价的图示:

import pylab as pl 
import numpy as np 
x=10
sT=np.arange(0,30,5) 
payoff_call=(abs(sT-x)+sT-x)/2 
payoff_put=(abs(x-sT)+x-sT)/2 
cash=np.zeros(len(sT))+x

def graph(text,text2=''): 
    pl.xticks(())
    pl.yticks(())
    pl.xlim(0,30)
    pl.ylim(0,20) 
    pl.plot([x,x],[0,3])
    pl.text(x,-2,"X");
    pl.text(0,x,"X")
    pl.text(x,x*1.7, text, ha='center', va='center',size=10, alpha=.5) 
    pl.text(-5,10,text2,size=25)

pl.figure(figsize=(6, 4))
pl.subplot(2, 3, 1); graph('Payoff of call');       pl.plot(sT,payoff_call) 
pl.subplot(2, 3, 2); graph('cash','+');             pl.plot(sT,cash)
pl.subplot(2, 3, 3); graph('Porfolio A ','=');   pl.plot(sT,cash+payoff_call)
pl.subplot(2, 3, 4); graph('Payoff of put ');       pl.plot(sT,payoff_put) 
pl.subplot(2, 3, 5); graph('Stock','+');       pl.plot(sT,sT)
pl.subplot(2, 3, 6); graph('Portfolio B','=');   pl.plot(sT,sT+payoff_put) 
pl.show()

输出结果如下:

看跌-看涨平价及其图示

看跌-看涨比率代表了投资者对未来的共同预期。如果没有明显的趋势,也就是说,我们预期未来是正常的,那么看跌-看涨比率应该接近 1。另一方面,如果我们预期未来会更加光明,那么比率应该低于 1。

以下代码显示了这种类型的比率在多年的变化。首先,我们必须从 CBOE 下载数据。

执行以下步骤:

  1. 访问www.cboe.com/

  2. 点击菜单栏中的报价与数据

  3. 查找put call ratio,即,www.cboe.com/data/putcallratio.aspx

  4. 点击当前下的CBOE 总交易量和看涨/看跌比率(2006 年 11 月 1 日至今)

    注意

    对于数据,读者可以在canisius.edu/~yany/data/totalpc.csv下载。

以下代码显示了看跌-看涨比率的趋势:

import pandas as pd
import scipy as sp
from matplotlib.pyplot import *
infile='c:/temp/totalpc.csv'
data=pd.read_csv(infile,skiprows=2,index_col=0,parse_dates=True)
data.columns=('Calls','Puts','Total','Ratio') 
x=data.index
y=data.Ratio 
y2=sp.ones(len(y)) 
title('Put-call ratio') 
xlabel('Date') 
ylabel('Put-call ratio') 
ylim(0,1.5)
plot(x, y, 'b-')
plot(x, y2,'r') 
show()

相关图表如下所示:

看涨看跌期权平价及其图形展示

带趋势的短期看跌期权与看涨期权比率

基于前面的程序,我们可以选择一个带趋势的较短时期,如以下代码所示:

import scipy as sp
import pandas as pd
from matplotlib.pyplot import * 
import matplotlib.pyplot as plt 
from datetime import datetime 
import statsmodels.api as sm

data=pd.read_csv('c:/temp/totalpc.csv',skiprows=2,index_col=0,parse_dates=True)
data.columns=('Calls','Puts','Total','Ratio') 
begdate=datetime(2013,6, 1) 
enddate=datetime(2013,12,31)
data2=data[(data.index>=begdate) & (data.index<=enddate)] 
x=data2.index
y=data2.Ratio 
x2=range(len(x)) 
x3=sm.add_constant(x2) 
model=sm.OLS(y,x3) 
results=model.fit()

#print results.summary() 
alpha=round(results.params[0],3) 
slope=round(results.params[1],3) 
y3=alpha+sp.dot(slope,x2) 
y2=sp.ones(len(y))
title('Put-call ratio with a trend') 
xlabel('Date') 
ylabel('Put-call ratio') 
ylim(0,1.5)
plot(x, y, 'b-')
plt.plot(x, y2,'r-.')
plot(x,y3,'y+')
plt.figtext(0.3,0.35,'Trend: intercept='+str(alpha)+',slope='+str(slope)) 
show()

相应的图表如下所示:

带趋势的短期看跌期权与看涨期权比率

二项树及其图形展示

二项树方法是由 Cox、Ross 和 Robinstein 于 1979 年提出的。因此,它也被称为 CRR 方法。基于 CRR 方法,我们有以下两步方法。首先,我们绘制一棵树,例如以下的一步树。假设我们当前的股票价值是 S。那么,结果有两个,SuSd,其中u>1d<1,请参见以下代码:

import matplotlib.pyplot as plt 
plt.xlim(0,1) 
plt.figtext(0.18,0.5,'S')
plt.figtext(0.6,0.5+0.25,'Su')
plt.figtext(0.6,0.5-0.25,'Sd')

plt.annotate('',xy=(0.6,0.5+0.25), xytext=(0.1,0.5), arrowprops=dict(facecolor='b',shrink=0.01))
plt.annotate('',xy=(0.6,0.5-0.25), xytext=(0.1,0.5), arrowprops=dict(facecolor='b',shrink=0.01))
plt.axis('off')
plt.show()

图表如下所示:

二项树及其图形展示

显然,最简单的树是一棵一步树。假设今天的价格为 10 美元,行权价为 11 美元,且一个看涨期权将在六个月后到期。此外,假设我们知道价格将有两个结果:上涨(u=1.15)或下跌(d=0.9)。换句话说,最终的价格要么是 11 美元,要么是 9 美元。基于这些信息,我们有以下图表,展示了这种一步二项树的价格:

二项树及其图形展示

生成前面图表的代码如下所示。

这些代码基于pypi.python.org/pypi/PyFi的代码:

import networkx as nx
import matplotlib.pyplot as plt 
plt.figtext(0.08,0.6,"Stock price=$20") 
plt.figtext(0.75,0.91,"Stock price=$22") 
plt.figtext(0.75,0.87,"Option price=$1")
plt.figtext(0.75,0.28,"Stock price=$18") 
plt.figtext(0.75,0.24,"Option price=0") 
n=1
def binomial_grid(n): 
    G=nx.Graph()
    for i in range(0,n+1):
        for j in range(1,i+2): 
            if i<n:
                G.add_edge((i,j),(i+1,j))
                G.add_edge((i,j),(i+1,j+1))
    posG={}
    for node in G.nodes(): 
        posG[node]=(node[0],n+2+node[0]-2*node[1])
    nx.draw(G,pos=posG) 
binomial_grid(n)
plt.show()

在前面的程序中,我们生成了一个名为binomial_grid()的函数,因为我们将在本章后面多次调用此函数。由于我们事先知道会有两个结果,因此我们可以选择一个合适的股票和看涨期权组合,以确保我们的最终结果是确定的,即相同的终端值。假设我们选择合适的 delta 份额的基础证券,并加上一份看涨期权,以确保在一个时期结束时具有相同的终端值,即!二项树及其图形展示。

因此,二项树及其图形展示。这意味着,如果我们持有0.4股并卖空一份看涨期权,那么当股票上涨时,我们的最终财富将相同,即0.411.5-1 =3.6;而当股票下跌时,则为0.49=3.6。进一步假设,如果持续复利的无风险利率为 0.12%,则今天的投资组合的价值将相当于未来确定值的折现值,即0.410 – c=pv(3.6)*。也就是说,二项树及其图形展示。如果使用 Python,我们将得到以下结果:

>>>round(0.4*10-exp(-0.012*0.5)*3.6,2)
0.42
>>>

对于二步二项树,我们有以下代码:

import p4f
plt.figtext(0.08,0.6,"Stock price=$20")
plt.figtext(0.08,0.56,"call =7.43")
plt.figtext(0.33,0.76,"Stock price=$67.49")
plt.figtext(0.33,0.70,"Option price=0.93")
plt.figtext(0.33,0.27,"Stock price=$37.40")
plt.figtext(0.33,0.23,"Option price=14.96")
plt.figtext(0.75,0.91,"Stock price=$91.11")
plt.figtext(0.75,0.87,"Option price=0")
plt.figtext(0.75,0.6,"Stock price=$50")
plt.figtext(0.75,0.57,"Option price=2")
plt.figtext(0.75,0.28,"Stock price=$27.44")
plt.figtext(0.75,0.24,"Option price=24.56")
n=2
p4f.binomial_grid(n)

基于 CRR 方法,我们有以下程序:

  1. 绘制一个n步树。

  2. n步结束时,估算终端价格。

  3. 根据终端价格、行权、看涨或看跌期权,在每个节点计算期权值。

  4. 按照风险中性概率将其向后折现一步,即从第 n 步到第 n-1 步。

  5. 重复前一步骤,直到找到第 0 步的最终值。udp的公式如下所示:二项树及其图示

这里,u是向上波动,d是向下波动,二项树及其图示是标的证券的波动率,r 是无风险利率,二项树及其图示是步长,即二项树及其图示T是到期时间(以年为单位),n是步数,q是股息收益率,p 是向上波动的风险中性概率。binomial_grid()函数基于一步二项树图示中的函数。如前所述,该函数包含在名为p4fy.py的总主文件中。输出图形如下所示。一个明显的结果是,前面的 Python 程序非常简单且直接。接下来,我们使用一个两步二项树来解释整个过程。假设当前股票价格为 10 美元,行权价格为 10 美元,到期时间为三个月,步数为二,风险自由利率为 2%,标的证券的波动率为 0.2。以下 Python 代码将生成一个两步二项树:

import p4f
from math import sqrt,exp 
import matplotlib.pyplot as plt
s=10
r=0.02
sigma=0.2
T=3./12
x=10
n=2
deltaT=T/n
q=0 
u=exp(sigma*sqrt(deltaT))
d=1/u 
a=exp((r-q)*deltaT)
p=(a-d)/(u-d) 
su=round(s*u,2);
suu=round(s*u*u,2) 
sd=round(s*d,2)
sdd=round(s*d*d,2) 
sud=s

plt.figtext(0.08,0.6,'Stock '+str(s)) 
plt.figtext(0.33,0.76,"Stock price=$"+str(su)) 
plt.figtext(0.33,0.27,'Stock price='+str(sd)) 
plt.figtext(0.75,0.91,'Stock price=$'+str(suu)) 
plt.figtext(0.75,0.6,'Stock price=$'+str(sud)) 
plt.figtext(0.75,0.28,"Stock price="+str(sdd)) 
p4f.binomial_grid(n)
plt.show()

树形结构如下所示:

二项树及其图示

现在,我们使用风险中性概率将每个值向后折现一步。代码和图形如下所示:

import p4f
import scipy as sp
import matplotlib.pyplot as plt
s=10;x=10;r=0.05;sigma=0.2;T=3./12.;n=2;q=0    # q is dividend yield 
deltaT=T/n    # step
u=sp.exp(sigma*sp.sqrt(deltaT)) 
d=1/u
a=sp.exp((r-q)*deltaT) 
p=(a-d)/(u-d)
s_dollar='S=$'
c_dollar='c=$' 
p2=round(p,2)
plt.figtext(0.15,0.91,'Note: x='+str(x)+', r='+str(r)+', deltaT='+str(deltaT)+',p='+str(p2))
plt.figtext(0.35,0.61,'p')
plt.figtext(0.65,0.76,'p')
plt.figtext(0.65,0.43,'p')
plt.figtext(0.35,0.36,'1-p')
plt.figtext(0.65,0.53,'1-p')
plt.figtext(0.65,0.21,'1-p')

# at level 2 
su=round(s*u,2);
suu=round(s*u*u,2) 
sd=round(s*d,2);
sdd=round(s*d*d,2) 
sud=s
c_suu=round(max(suu-x,0),2) 
c_s=round(max(s-x,0),2) 
c_sdd=round(max(sdd-x,0),2) 
plt.figtext(0.8,0.94,'s*u*u') 
plt.figtext(0.8,0.91,s_dollar+str(suu)) 
plt.figtext(0.8,0.87,c_dollar+str(c_suu)) 
plt.figtext(0.8,0.6,s_dollar+str(sud)) 
plt.figtext(0.8,0.64,'s*u*d=s') 
plt.figtext(0.8,0.57,c_dollar+str(c_s)) 
plt.figtext(0.8,0.32,'s*d*d') 
plt.figtext(0.8,0.28,s_dollar+str(sdd)) 
plt.figtext(0.8,0.24,c_dollar+str(c_sdd))

# at level 1
c_01=round((p*c_suu+(1-p)*c_s)*sp.exp(-r*deltaT),2) 
c_02=round((p*c_s+(1-p)*c_sdd)*sp.exp(-r*deltaT),2)

plt.figtext(0.43,0.78,'s*u') 
plt.figtext(0.43,0.74,s_dollar+str(su)) 
plt.figtext(0.43,0.71,c_dollar+str(c_01)) 
plt.figtext(0.43,0.32,'s*d') 
plt.figtext(0.43,0.27,s_dollar+str(sd)) 
plt.figtext(0.43,0.23,c_dollar+str(c_02))
# at level 0 (today)

c_00=round(p*sp.exp(-r*deltaT)*c_01+(1-p)*sp.exp(-r*deltaT)*c_02,2) 
plt.figtext(0.09,0.6,s_dollar+str(s)) 
plt.figtext(0.09,0.56,c_dollar+str(c_00)) 
p4f.binomial_grid(n)

树形结构如下所示:

二项树及其图示

这里,我们解释图中显示的一些值。在最高节点(suu)处,由于终端股票价格为 11.52,行权价格为 10,故看涨期权的价值为 1.52(11.52-10)。类似地,在节点sud=s处,看涨期权的价值为 0,因为 10-10=0。对于看涨期权值为 0.8,我们进行如下验证:

>>>p
0.5266253390068362
>>>deltaT
0.125
>>>v=(p*1.52+(1-p)*0)*exp(-r*deltaT)
>>>round(v,2)
0.80
>>>

欧洲期权的二项树(CRR)方法

以下代码是用于使用二项树法定价欧洲期权的:

def binomialCallEuropean(s,x,T,r,sigma,n=100):
    from math import exp,sqrt 
    deltaT = T /n
    u = exp(sigma * sqrt(deltaT)) 
    d = 1.0 / u
    a = exp(r * deltaT)
    p = (a - d) / (u - d)
    v = [[0.0 for j in xrange(i + 1)]  for i in xrange(n + 1)] 
    for j in xrange(i+1):
        v[n][j] = max(s * u**j * d**(n - j) - x, 0.0) 
    for i in xrange(n-1, -1, -1):
        for j in xrange(i + 1):
            v[i][j]=exp(-r*deltaT)*(p*v[i+1][j+1]+(1.0-p)*v[i+1][j]) 
    return v[0][0]

为了应用这个函数,我们给它一组输入值。为了比较,基于Black-Scholes-Merton 期权模型的结果也在这里显示:

>>> binomialCallEuropean(40,42,0.5,0.1,0.2,1000) 
2.278194404573134
>>> bs_call(40,42,0.5,0.1,0.2) 
2.2777803294555348
>>>

美国期权的二项树(CRR)方法

与只能应用于欧洲期权的 Black-Scholes-Merton 期权模型不同,二项树(CRR 方法)可以用来定价美国期权。唯一的区别是我们必须考虑提前行权:

def binomialCallAmerican(s,x,T,r,sigma,n=100):
    from math import exp,sqrt
    import numpy as np
    deltaT = T /n
    u = exp(sigma * sqrt(deltaT)) 
    d = 1.0 / u
    a = exp(r * deltaT)
    p = (a - d) / (u - d)
    v = [[0.0 for j in np.arange(i + 1)] for i in np.arange(n + 1)] 
    for j in np.arange(n+1):
        v[n][j] = max(s * u**j * d**(n - j) - x, 0.0) 
    for i in np.arange(n-1, -1, -1):
        for j in np.arange(i + 1):
            v1=exp(-r*deltaT)*(p*v[i+1][j+1]+(1.0-p)*v[i+1][j]) 
            v2=max(v[i][j]-x,0)           # early exercise 
            v[i][j]=max(v1,v2)
    return v[0][0]

定价美国看涨期权与定价欧洲看涨期权的关键区别在于其提前行权的机会。在前面的程序中,最后几行反映了这一点。对于每个节点,我们估算两个值:v1是折现后的值,v2是提前行权的支付。如果使用相同的数值集来应用此二项树定价美国看涨期权,我们会得到以下值。可以理解,最终结果会高于欧洲看涨期权的对应值:

>>> call=binomialCallAmerican(40,42,0.5,0.1,0.2,1000)
>>> round(call,2)
2.28
>>>

对冲策略

在卖出欧洲看涨期权后,我们可以持有对冲策略同一只股票的股份来对冲我们的仓位。这被称为 delta 对冲。由于 delta对冲策略是标的股票(S)的一个函数,为了保持有效的对冲,我们必须不断地重新平衡我们的持仓。这就是动态对冲。一个投资组合的 delta 是该投资组合中各个证券的加权 delta。需要注意的是,当我们做空某个证券时,其权重将为负值:

对冲策略

假设一个美国进口商将在三个月后支付 1000 万英镑。他或她担心美元对英镑的潜在贬值。有几种方法可以对冲这种风险:现在购买英镑,进入期货合约以固定汇率在三个月后购买 1000 万英镑,或者购买以固定汇率为行权价格的看涨期权。第一种选择成本高,因为进口商今天并不需要英镑。进入期货合约也有风险,因为如果美元升值,进口商将面临额外的费用。另一方面,进入看涨期权将保证今天的最大汇率。同时,如果英镑贬值,进口商将获得收益。这种活动被称为对冲,因为我们采取了与我们的风险相反的立场。

对于货币期权,我们有以下方程:

对冲策略

这里,对冲策略是外币的美元汇率,对冲策略是国内无风险利率,对冲策略是外国的无风险利率。

隐含波动率

从前面的部分我们知道,对于一组输入变量——S(当前股票价格)、X(行权价格)、T(到期日,单位为年)、r(连续复利的无风险利率)以及 sigma(股票的波动率,即其收益的年化标准差)——我们可以根据 Black-Scholes-Merton 期权模型来估算看涨期权的价格。回想一下,为了定价欧洲看涨期权,我们有以下五行 Python 代码:

def bs_call(S,X,T,r,sigma):
    from scipy import log,exp,sqrt,stats
d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T))
d2 = d1-sigma*sqrt(T)
return S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2)

在输入一组五个数值后,我们可以按照以下方式估算看涨期权的价格:

>>>bs_call(40,40,0.5,0.05,0.25)
3.3040017284767735

另一方面,如果我们知道SXTrc,我们如何估算 sigma 呢?这里,sigma是我们的隐含波动率。换句话说,如果我们给定一组值,如 S=40、X=40、T=0.5、r=0.05 和 c=3.30,我们应该找出 sigma 的值,并且它应该等于 0.25。在本章中,我们将学习如何估算隐含波动率。实际上,计算隐含波动率的基本逻辑非常简单:试错法。让我们以之前的例子为例进行说明。我们有五个值——S=40X=40T=0.5r=0.05c=3.30。基本设计是,在输入 100 个不同的 sigma 值后,加上之前提到的四个输入值,我们将得到 100 个看涨期权价格。隐含波动率就是通过最小化估算的看涨期权价格与 3.30 之间的绝对差值来得到的 sigma 值。当然,我们可以增加试验的次数,以获得更高的精度,也就是更多的小数位。

另外,我们可以采用另一种转换标准:当估算的看涨期权价格与给定的看涨期权值之间的绝对差值小于某个临界值时停止,例如 1 美分,即|c-3.30|<0.01。由于随机选择 100 个或 1,000 个不同的 sigma 值并不是一个好主意,我们将系统地选择这些值,也就是通过循环来系统地选择这些 sigma 值。接下来,我们将讨论两种类型的循环:for 循环和 while 循环。基于欧洲看涨期权的隐含波动率函数。最终,我们可以编写一个基于欧洲看涨期权的隐含波动率估算函数。为了节省空间,我们从程序中移除所有注释和示例,如下所示:

def implied_vol_call(S,X,T,r,c):
    from scipy import log,exp,sqrt,stats
    for i in range(200):
        sigma=0.005*(i+1)
        d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T))
        d2 = d1-sigma*sqrt(T)
        diff=c-(S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2))
        if abs(diff)<=0.01:
            return i,sigma, diff

使用一组输入值,我们可以像下面这样轻松地应用之前的程序:

>>>implied_vol_call(40,40,0.5,0.05,3.3)
 (49, 0.25, -0.0040060797372882817)

类似地,我们可以基于欧洲看跌期权模型来估算隐含波动率。在以下程序中,我们设计了一个名为implied_vol_put_min()的函数。这个函数与之前的函数有几个区别。首先,当前的函数依赖于看跌期权,而不是看涨期权。因此,最后一个输入值是看跌期权的溢价,而不是看涨期权的溢价。其次,转换标准是估算的价格与给定的看跌期权价格之间的差值最小。在之前的函数中,转换标准是当绝对差值小于 0.01 时。在某种意义上,当前程序将保证输出隐含波动率,而之前的程序则不能保证输出:

def implied_vol_put_min(S,X,T,r,p):
    from scipy import log,exp,sqrt,stats 
    implied_vol=1.0
    min_value=100.0
    for i in xrange(1,10000): 
        sigma=0.0001*(i+1)
        d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T)) 
        d2 = d1-sigma*sqrt(T)
        put=X*exp(-r*T)*stats.norm.cdf(-d2)-S*stats.norm.cdf(-d1) 
        abs_diff=abs(put-p)
        if abs_diff<min_value: 
            min_value=abs_diff 
            implied_vol=sigma 
            k=i
        put_out=put
    print ('k, implied_vol, put, abs_diff') 
    return k,implied_vol, put_out,min_value

让我们使用一组输入值来估算隐含波动率。之后,我们将解释前面程序的逻辑。假设S=40X=40T=12个月、r=0.1,并且看跌期权价格为$1.50,如以下代码所示:

>>>implied_vol_put_min(40,40,1.,0.1,1.501)
k, implied_vol, put, abs_diff
(1999, 0.2, 12.751879946129757, 0.00036735530273501737)

隐含波动率为 20%。其逻辑是我们为一个变量 min_value 赋予一个较大的值,例如 100。对于第一个 sigma 值 0.0002,我们几乎得到零的卖出期权值。因此,绝对差值为 1.50,这小于 100。所以,我们的 min_value 变量将被 1.50 替换。我们继续这样做,直到完成循环。记录下来的最小值对应的 sigma 就是我们的隐含波动率。我们可以通过定义一些中间值来优化前面的程序。例如,在之前的程序中,我们估算了 ln(S/X) 10,000 次。实际上,我们定义一个新变量 log_S_over_X,只估算一次,然后在 10,000 次中使用这个值。对于 sigma*sigma/2.sigman*sqrt(T) 也是如此:

二分查找

为了估算隐含波动率,早期方法背后的逻辑是运行 100 次 Black-Scholes-Merton 期权模型,并选择能使估算期权价格与观察价格之间差异最小的 sigma 值。尽管这个逻辑易于理解,但这种方法效率不高,因为我们需要调用几百次 Black-Scholes-Merton 期权模型。为了估算少量的隐含波动率,这种方法不会带来问题。然而,在两种情况下,这种方法会遇到问题。首先,如果我们需要更高的精度,比如 sigma=0.25333,或者必须估算几百万个隐含波动率,那么我们需要优化方法。让我们看一个简单的例子。假设我们随机选择一个 1 到 5,000 之间的数。如果我们从 1 到 5,000 顺序地运行循环,需要多少步才能找到这个数?二分查找是 log(n) 的最坏情况,而线性查找则是 n 的最坏情况。因此,在 1 到 5,000 范围内搜索一个值时,线性查找在最坏情况下需要 5,000 步(平均 2,050 步),而二分查找在最坏情况下只需 12 步(平均 6 步)。以下是一个实现二分查找的 Python 程序:

def binary_search(x, target, my_min=1, my_max=None):
    if my_max is None:
       my_max = len(x) - 1
    while my_min <= my_max:
      mid = (my_min + my_max)//2
      midval = x[mid]
      if midval < target:
          my_min = my_mid + 1
      elif midval > target:
          my_max = mid - 1
      else:
          return mid
    raise ValueError

以下程序展示了其在搜索隐含波动率中的应用:

from scipy import log,exp,sqrt,stats
S=42;X=40;T=0.5;r=0.01;c=3.0
def bsCall(S,X,T,r,sigma):
    d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T)) 
    d2 = d1-sigma*sqrt(T)
    return S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2)
#
def impliedVolBinary(S,X,T,r,c):
    k=1
    volLow=0.001
    volHigh=1.0
    cLow=bsCall(S,X,T,r,volLow)
    cHigh=bsCall(S,X,T,r,volHigh)
    if cLow>c or cHigh<c:
        raise ValueError
    while k ==1:
        cLow=bsCall(S,X,T,r,volLow)
        cHigh=bsCall(S,X,T,r,volHigh)
        volMid=(volLow+volHigh)/2.0
        cMid=bsCall(S,X,T,r,volMid)
        if abs(cHigh-cLow)<0.01:
            k=2
        elif cMid>c:
            volHigh=volMid
        else:
            volLow=volMid
    return volMid, cLow, cHigh
#
print("Vol,     cLow,      cHigh")
print(impliedVolBinary(S,X,T,r,c))
Vol,     cLow,      cHigh
(0.16172778320312498, 2.998464657758511, 3.0039730848624977)

根据结果,隐含波动率为 16.17%。在前面的程序中,转换条件,即程序应该停止的条件,是两个看涨期权之间的差异。读者可以设置其他转换条件。为了避免无限循环,我们设置了一个屏幕条件:

    if cLow>c or cHigh<c:
        raise ValueError

从 Yahoo! 财经获取期权数据

有许多期权数据来源可以用于我们的投资、研究或教学。其中一个来源是 Yahoo! 财经。

为了检索 IBM 的期权数据,我们有以下程序:

  1. 访问 finance.yahoo.com

  2. 在搜索框中输入 IBM

  3. 点击导航栏中的 期权

相关页面是 finance.yahoo.com/quote/IBM/options?p=IBM。该网页的截图如下:

从 Yahoo! Finance 获取期权数据

波动率微笑与偏斜

显然,每只股票应有一个波动率值。然而,在估算隐含波动率时,不同的行权价格可能会提供不同的隐含波动率。更具体来说,基于虚值期权、平值期权和实值期权的隐含波动率可能会有显著差异。波动率微笑是指随着行权价格的变化,波动率先下降后上升,而波动率偏斜则表现为向下或向上倾斜。关键在于投资者情绪以及供求关系对波动率偏斜的根本影响。因此,这种微笑或偏斜提供了关于投资者(例如基金经理)是更倾向于卖出看涨期权还是看跌期权的信息,如以下代码所示:

import datetime
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.finance import quotes_historical_yahoo_ochl as getData

# Step 1: input area
infile="c:/temp/callsFeb2014.pkl"
ticker='IBM'
r=0.0003                          # estimate
begdate=datetime.date(2010,1,1)   # this is arbitrary 
enddate=datetime.date(2014,2,1)   # February 2014

# Step 2: define a function 
def implied_vol_call_min(S,X,T,r,c): 
    from scipy import log,exp,sqrt,stats 
    implied_vol=1.0
    min_value=1000
    for i in range(10000): 
        sigma=0.0001*(i+1)
        d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T)) 
        d2 = d1-sigma*sqrt(T)
        c2=S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2) 
        abs_diff=abs(c2-c)
        if abs_diff<min_value: 
            min_value=abs_diff 
            implied_vol=sigma 
            k=i
    return implied_vol

# Step 3: get call option data 
calls=pd.read_pickle(infile)
exp_date0=int('20'+calls.Symbol[0][len(ticker):9])  # find expiring date
p = getData(ticker, begdate,enddate,asobject=True, adjusted=True)
s=p.close[-1]                    # get current stock price 
y=int(exp_date0/10000)
m=int(exp_date0/100)-y*100
d=exp_date0-y*10000-m*100
exp_date=datetime.date(y,m,d)    # get exact expiring date 
T=(exp_date-enddate).days/252.0  # T in years

# Step 4: run a loop to estimate the implied volatility 
n=len(calls.Strike)   # number of strike
strike=[]             # initialization
implied_vol=[]        # initialization
call2=[]              # initialization
x_old=0               # used when we choose the first strike 

for i in range(n):
    x=calls.Strike[i]
    c=(calls.Bid[i]+calls.Ask[i])/2.0
    if c >0:
        print ('i=',i,'',    c='',c)
        if x!=x_old:
            vol=implied_vol_call_min(s,x,T,r,c)
            strike.append(x)
            implied_vol.append(vol)
            call2.append(c)
            print x,c,vol
            x_old=x

# Step 5: draw a smile 
plt.title('Skewness smile (skew)') 
plt.xlabel('Exercise Price') 
plt.ylabel('Implied Volatility')
plt.plot(strike,implied_vol,'o')
plt.show()

注意事项

请注意,.pickle数据集可以在 canisus.edu/~yan/python/callsFeb2014.pkl 下载。

与波动率微笑相关的图表如下:

波动率微笑与偏斜

参考文献

请参考以下文章:

附录 A – 数据案例 6:投资组合保险

投资组合保险是一种通过卖空股指期货来对冲股票投资组合市场风险的方法。当市场方向不确定或波动较大时,机构投资者常常使用这种对冲技术。假设你管理着一个价值 5000 万美元的行业投资组合。如果你预计未来三个月整个市场将非常波动——换句话说,市场可能会大幅下跌——此时我们可能有哪些选择?

  • 选项 #1:立即卖出股票,并在几个月后再买回来

  • 选项 #2:卖出 S&P500 指数期货

显然,第一个选择因交易成本而昂贵:

  1. 获取五个行业投资组合:

    1. 要获取 Fama-French 五行业投资组合,请访问 French 教授的数据图书馆。

    2. 请访问 mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html

    3. 搜索关键词Industry;见下方截图:附录 A – 数据案例 6:投资组合保险

    4. 下载数据并估算这五个行业的贝塔值。让我们看看当市场下跌一个点时会发生什么。今天的 S&P500 水平如下:附录 A – 数据案例 6:投资组合保险

    5. 如果市场下跌一个点,多头头寸(S&P500 期货合约)将损失 250 美元,而空头头寸将获得 250 美元。一个 S&P500 期货合约的规模是指数水平 *250。

    6. 如果我们想对冲我们的 5 美元投资组合,应该空头 n 个期货合约。具体说明见www3.canisius.edu/~yany/doc/sp500futures.pdf

    附录 A – 数据案例 6:投资组合保险

    这里,Vp是投资组合价值,βp是投资组合贝塔值,指数水平是 S&P500 指数水平。应用上述公式,我们应空头十个期货合约。假设三个月后指数为 2090.4,即下跌了十个点。由于我们知道贝塔值是衡量市场风险的指标,假设年化无风险利率为 1%,即三个月的利率为 0.25%。

  2. 通过应用以下线性回归来估计投资组合的贝塔值:附录 A – 数据案例 6:投资组合保险

  3. 确定市场大幅下跌的几个时刻。

    你可以使用一个名为 Business Cycle 的 Python 数据集:

    import pandas as pd
    x=pd.read_pickle("c:/temp/businessCycle.pkl")
    print(x.head())
    print(x.tail())
    date             
    1926-10-01  1.000
    1926-11-01  0.846
    1926-12-01  0.692
    1927-01-01  0.538
    1927-02-01  0.385
       cycle
    date             
    2009-02-01 -0.556
    2009-03-01 -0.667
    2009-04-01 -0.778
    2009-05-01 -0.889
    2009-06-01 -1.000
    

    提示

    注意,-1 表示经济处于深度衰退,而 1 表示经济在扩张。

  4. 估算有无对冲策略时的损失。你的投资组合损失是多少?如果你空头一个 S&P500 期货合约,收益是多少?

  5. 重复整个过程,假设我们持有 1,000 股 IBM,2,000 股 DELL,5,000 股 Citi Group 和 7,000 股 IBM。

    • 今天的总市值是多少?

    • 投资组合的贝塔值是多少?[注:你可以使用最新的五年月度数据来估算贝塔值]

    • 如果我们想通过使用 S&P500 期货合约来对冲投资组合,应该做多(做空)多少合约?

    • 如果市场下跌 5%,我们的投资组合损失是多少,套期保值头寸的收益是多少?

以下公式是通用公式:

附录 A – 数据案例 6:投资组合保险

这里,n是合约数,β是我们的目标贝塔值,VF是一个期货合约的价值。Vpβp在前文已定义。如果 n 为正(负),则表示多头(空头)头寸。在前面的 S&P500 期货使用案例中,VF=S&P500 指数水平 *250。

提示

通过使用 S&P500 期货来改变投资组合贝塔值以应对市场不利时机,考虑市场时机。

练习

  1. 如果年利率为 5%,按季度复利计算,其等效的连续复利利率是多少?

  2. 一个投资组合今天的价值为 477 万美元,β值为 0.88。如果投资组合经理解释市场将在未来三个月上涨,并且他/她打算在仅三个月内通过使用 S&P500 期货将投资组合的 β 值从 0.88 提高到 1.20,那么他/她应该做多少合约的多头或空头?如果 S&P500 指数上涨 70 点,他/她的盈亏是多少?如果 S&P500 下跌 50 点呢?

  3. 编写一个 Python 程序来定价一个看涨期权。

  4. 解释在编写复杂的 Python 程序时,“空壳方法”的含义。

  5. 解释在编写复杂的 Python 程序时,所谓的“注释掉所有代码”方法背后的逻辑。

  6. 解释当我们调试程序时,返回值的用途。

  7. 当我们编写 CND(累积分布标准正态分布)时,我们可以单独定义 a1、a2、a3、a4 和 a5。以下两种方法有何不同?

    • 当前方法:(a1,a2,a3,a4,a5)=(0.31938153,-0.356563782,1.781477937,-1.821255978,1.330274429)
  8. 一种替代方法:

    • a1=0.31938153

    • a2=-0.356563782

    • a3=1.781477937

    • a4=-1.821255978

    • a5=1.330274429

  9. 美式看涨期权和欧式看涨期权有什么区别?

  10. 在 Black-Scholes-Merton 期权模型中,rf 的单位是什么?

  11. 如果给定年利率为 3.4%,半年复利,我们应该在 Black-Scholes-Merton 期权模型中使用哪个 rf 值?

  12. 如何使用期权进行对冲?

  13. 在定价欧洲看涨期权时,如何处理预定的现金股息?

  14. 为什么美式看涨期权比欧式看涨期权更有价值?

  15. 假设你是一个共同基金经理,且你的投资组合的β值与市场高度相关。你担心市场会短期下跌,你可以采取什么措施来保护你的投资组合?

  16. 股票 A 的当前价格为 38.5 美元,看涨期权和看跌期权的行使价格均为 37 美元。如果无风险利率为 3.2%,到期时间为三个月,股票 A 的波动率为 0.25,那么欧洲看涨期权和看跌期权的价格是多少?

  17. 使用买卖平价验证上述解决方案。

  18. 在 9.11) 中,当看涨期权和看跌期权的行使价格不同,我们还能应用买卖平价吗?

  19. 对于一组输入值,如 S=40、X=40、T=3/12=0.25、r=0.05 和 sigma=0.20,使用 Black-Scholes-Merton 期权模型,我们可以估算看涨期权的价值。现在保持所有参数不变,除了 S(股票的当前价格);请展示看涨期权和 S 之间的关系,最好以图表形式展示。

  20. 在看涨期权模型中,有效年利率、半年有效利率和无风险利率的定义是什么?假设当前年化无风险利率为 5%,半年复利,应该使用哪个值作为 Black-Scholes-Merton 看涨期权模型的输入值?

  21. 当股票交易价格为 39 美元,行使价格为 40 美元,到期日为三个月,无风险利率为 3.5%,连续复利,年波动率为 0.15 时,期权的权利金是多少?

  22. 对于风险无关利率仍为每年 3.5% 但按半年复利的情况,重复之前的练习。

  23. 使用他人的程序有哪些优点和缺点?

  24. 你如何调试他人的程序?

  25. 编写一个 Python 程序,将任何给定的按每年 m 次复利计算的 APR 转换为连续复利利率。

  26. 你如何提高累积正态分布的准确性?

  27. APR 和 Rc(连续复利率)之间的关系是什么?

  28. 对于当前股价为 52.34 美元的股票,如果行使价格与当前股价相同,期满时间为六个月,年波动率为 0.16,风险无关利率为 3.1%,且按连续复利计算,计算其看涨期权的价格。

  29. 对于一组 SXTr 和 sigma,我们可以使用这 13 行 Python 代码来估算一个欧洲看涨期权。当当前股价 S 增加,而其他输入值不变时,看涨期权的价格会增加还是减少?为什么?

  30. 以图形方式展示前述结果。

  31. 当行使价格 X 增加时,看涨期权的价值会下降。这个说法对吗?为什么?

    • 如果其他输入值保持不变,股票的 sigma 增加时,看涨期权溢价会增加。这个说法对吗?为什么?
  32. 对于一组输入值 SXTr 和 sigma,我们可以使用本章中的代码来定价欧洲看涨期权,即 C。另一方面,如果我们观察到一个实际的看涨期权溢价(Cobs),并且拥有一组值 SXTr,我们可以估计出隐含波动率(sigma)。指定一个试错法来粗略估计隐含波动率(如果一个新手没有解答这个问题是完全可以的,因为我们会 dedicating 一整章来讨论如何做到这一点)。

  33. 根据所谓的看涨看跌平价公式,它表明一个到期时持有足够现金的看涨期权(X 美元)与持有一只标的股票的看跌期权是等价的——在此,两者的行使价格(X)和到期时间(T)相同,且均为欧洲期权——如果股票价格为 10 美元,行使价格为 11 美元,到期时间为六个月,风险无关利率为 2.9%,且按半年复利计算,那么该欧洲看跌期权的价格是多少?

总结

在本章中,我们首先解释了许多与投资组合理论相关的基本概念,例如协方差、相关性,如何计算二股票投资组合的方差以及 n 股票投资组合的方差公式。接着,我们讨论了针对个别股票或投资组合的各种风险衡量标准,如夏普比率、特雷诺比率、索提诺比率,如何基于这些衡量标准(比率)来最小化投资组合风险,如何设置目标函数,如何为给定的股票集选择一个有效的投资组合,以及如何构建有效前沿。

在下一章中,我们将讨论现代金融中最重要的理论之一:期权和期货。我们将从基本概念入手,例如看涨期权和看跌期权的收益函数。接着,我们将解释相关的应用,如各种交易策略、公司激励计划以及对冲策略,包括不同类型的期权和期货。

第十一章:风险价值(VaR)

在金融领域,理性投资者在隐性或显性地,总是考虑风险与回报之间的权衡。通常,衡量回报没有歧义。然而,在衡量风险方面,我们有许多不同的度量标准,如使用收益的方差和标准差来衡量总风险,个股的贝塔值,或投资组合贝塔值来衡量市场风险。在前几章中,我们知道总风险有两个组成部分:市场风险和公司特定风险。为了平衡回报的收益和风险的成本,可以应用多种度量方法,如夏普比率、特雷诺比率、索提诺比率和 M2 绩效度量(莫迪利安尼与莫迪利安尼绩效度量)。所有这些风险度量或比率都有一个共同的格式:即回报的收益(以风险溢价表示)和风险(以标准差、贝塔值或下偏标准差LPSD)表示)之间的权衡。另一方面,这些度量并未考虑概率分布。在本章中,将介绍一个新的风险度量——风险价值VaR),并通过使用现实世界的数据来应用它。特别地,将涵盖以下主题:

  • VaR 介绍

  • 正态分布的密度函数和累积分布函数回顾

  • 方法一—基于正态性假设估算 VaR

  • 从 1 天风险转换为 n 天风险,一天 VaR 与 n 天 VaR 的比较

  • 正态性检验

  • 偏度和峰度的影响

  • 通过包含偏度和峰度来修正 VaR 度量

  • 方法二—基于历史收益估算 VaR

  • 使用蒙特卡洛模拟将两种方法联系起来

  • 回测和压力测试

VaR 介绍

目前为止,我们有几种方法来评估个股或投资组合的风险,比如使用收益的方差和标准差来衡量总风险,或者用贝塔值来衡量投资组合或个股的市场风险。另一方面,许多 CEO 更倾向于使用一个简单的指标,称为风险价值VaR),它有一个简单的定义:

“在预定时间段内,具有一定置信水平的最大损失。”

从前面的定义来看,它有三个明确的因素,再加上一个隐含的因素。隐含因素或变量是我们当前的位置,或者说我们当前投资组合或个股的价值。前述陈述提供了未来可能的最大损失,这是第一个因素。第二个因素是在特定的时间段内。这两个因素是相当常见的。然而,最后一个因素是非常独特的:它带有置信水平或概率。以下是几个例子:

  • 示例 #1:在 2017 年 2 月 7 日,我们持有 300 股国际商业机器公司的股票,市值为$52,911。明天,即 2017 年 2 月 8 日,最大损失为$1,951,置信水平为 99%。

  • 示例 #2:我们今天的共同基金价值为 1000 万美元。在接下来的三个月里,基于 95%的置信水平,最大损失为 50 万美元。

  • 示例 #3:我们银行的价值为 2 亿美元。我们银行的 VaR 为 1000 万美元,具有 1%的概率,时间跨度为接下来的 6 个月。

通常,有两种方法来估算 VaR。第一种方法基于假设我们的证券或投资组合回报遵循正态分布,而第二种方法依赖于历史回报的排名。在讨论第一种方法之前,我们先复习一下正态分布的相关概念。正态分布的密度在这里定义:

VaR 简介

这里,f(x)是密度函数,x是输入变量,μ是均值,σ是标准差。可以使用一个叫做spicy.stats.norm.pdf()的函数来估算密度。该函数有三个输入值:xμσ。以下代码调用此函数,并根据前面的公式手动验证结果:

import scipy.stats as stats
from scipy import sqrt, exp,pi
d1=stats.norm.pdf(0,0.1,0.05)      
print("d1=",d1)
d2=1/sqrt(2*pi*0.05**2)*exp(-(0-0.1)**2/0.05**2/2)  # verify manually
print("d2=",d2) 
('d1=', 1.0798193302637611)
('d2=', 1.0798193302637611)

在前面的代码中,我们导入了sqrt()exp()函数以及π,以简化我们的代码。设置μ=0,σ=1,前面的正态分布密度函数简化为标准正态分布;请看其对应的密度函数:

VaR 简介

spicy.stats.norm.pdf()函数的第二个和第三个输入值的默认值分别为 0 和 1。换句话说,只需要一个输入值,它就代表了标准正态分布;请查看以下代码并了解如何手动验证:

from scipy import exp,sqrt,stats,pi
d1=stats.norm.pdf(0)
print("d1=",d1)
d2=1/sqrt(2*pi)           # verify manually
print("d2=",d2)
('d1=', 0.3989422804014327)
('d2=', 0.3989422804014327)

以下代码生成了标准正态分布的图形,其中spicy.stats.norm.pdf()函数只接受一个输入值:

import scipy as sp
import matplotlib.pyplot as plt
x = sp.arange(-3,3,0.1)
y=sp.stats.norm.pdf(x)
plt.title("Standard Normal Distribution")
plt.xlabel("X")
plt.ylabel("Y")
plt.plot(x,y)
plt.show()

下面是图形展示:

VaR 简介

对于 VaR 估算,通常我们会选择 95%和 99%两个置信水平。对于 95%(99%)置信水平,我们实际上关注的是左尾的 5%(1%)概率。以下图表展示了基于标准正态分布和 95%置信水平的 VaR 概念:

import scipy as sp
from matplotlib import pyplot as plt
z=-2.325       # user can change this number 
xStart=-3.8    # arrow line start x
yStart=0.2     # arrow line start x
xEnd=-2.5      # arrow line start x
yEnd=0.05      # arrow line start x
def f(t):
    return sp.stats.norm.pdf(t) 

plt.ylim(0,0.45)
x = sp.arange(-3,3,0.1) 
y1=f(x)
plt.plot(x,y1)
x2= sp.arange(-4,z,1/40.) 
sum=0
delta=0.05
s=sp.arange(-10,z,delta) 
for i in s:
    sum+=f(i)*delta

plt.annotate('area is '+str(round(sum,4)),xy=(xEnd,yEnd),xytext=(xStart,yStart), arrowprops=dict(facecolor='red',shrink=0.01))
plt.annotate('z= '+str(z),xy=(z,0.01)) 
plt.fill_between(x2,f(x2))
plt.show()

要生成图形,应用了三个函数。matplotlib.pyplot.annotate()函数的作用是生成一个文本或带有文本描述的箭头。str()函数将数字转换为字符串。matplotlib.pyplot.fill_between()将填充指定区域。输出的图形如下所示:

VaR 简介

基于正态分布假设,我们有以下一般形式来估算 VaR:

VaR 简介

在这里,VaR 是我们的风险价值,position 是我们投资组合的当前市场价值,μperiod 是预期的期间回报,z 是一个根据置信水平确定的临界值,σ 是我们投资组合的波动性。对于正态分布,z=2.33 对应于 99% 的置信水平,而 z=1.64 对应于 95% 的置信水平。由于我们可以使用 scipy.stats.norm.ppf() 来获取 z 值,因此前述方程可以重写为:

VaR 介绍

比较前述两个方程。仔细的读者应该注意到,z 前的符号是不同的。对于前面的方程,它有一个正号,而不是前一个方程中的负号。原因是,应用 scipy.stats.norm.ppf() 估算出的 z 值会是负数;请参见以下代码:

from scipy.stats import norm
confidence_level=0.99
z=norm.ppf(1-confidence_level)
print(z)
-2.32634787404

当时间周期较短,如 1 天时,我们可以忽略 μperiod 的影响。因此,我们有以下最简单的形式:

VaR 介绍

以下程序显示了一个假设的盈亏概率密度函数的 5% VaR:

import scipy as sp
import scipy as sp
from scipy.stats import norm
from matplotlib import pyplot as plt

confidence_level=0.95   # input 
z=norm.ppf(1-confidence_level) 
def f(t):
    return sp.stats.norm.pdf(t)
#
plt.ylim(0,0.5)
x = sp.arange(-7,7,0.1) 
ret=f(x)
plt.plot(x,ret)
x2= sp.arange(-4,z,1/40.) 
x3=sp.arange(z,4,1/40.)
sum=0
delta=0.05
s=sp.arange(-3,z,delta) 
for i in s:
    sum+=f(i)*delta
note1='Red area to the left of the'
note2='dotted red line reprsesents'
note3='5% of the total area'
#
note4='The curve represents a hypothesis'
note5='profit/loss density function. The'
note6='5% VaR is 1.64 standard deviation'
note7='from the mean, i.e.,zero'
#
note8='The blue area to the righ of the'
note9='red dotted line represents 95%'
note10='of the returns space'
# this is for the vertical line
plt.axvline(x=z, ymin=0.1, ymax = 1, linewidth=2,ls='dotted', color='r')
plt.figtext(0.14,0.5,note1)
plt.figtext(0.14,0.47,note2)
plt.figtext(0.14,0.44,note3)
#
plt.figtext(0.5,0.85,note4)
plt.figtext(0.5,0.82,note5)
plt.figtext(0.5,0.79,note6)
plt.figtext(0.5,0.76,note7)
plt.annotate("",xy=(-2.5,0.08),xytext=(-2.5,0.18), arrowprops=dict(facecolor='red',shrink=0.001))
#
plt.figtext(0.57,0.5,note8)
plt.figtext(0.57,0.47,note9)
plt.figtext(0.57,0.44,note10)
plt.annotate("",xy=(1.5,0.28),xytext=(4.5,0.28), arrowprops=dict(facecolor='blue',shrink=0.001))
#
plt.annotate('z= '+str(z),xy=(2.,0.1)) 
plt.fill_between(x2,f(x2), color='red')
plt.fill_between(x3,f(x3), color='blue')
plt.title("Visual presentation of VaR, 5% vs. 95%")
plt.show()

相关图表如下:

VaR 介绍

这是估算明天最大损失的最简单示例。假设我们在 2017 年 2 月 7 日拥有 1,000 股 IBM 的股票。以 99% 的置信水平,明天的最大损失是多少?为了估算每日回报的标准差,我们使用过去 5 年的数据。实际上,这是一个决策变量。我们可以使用 1 年的数据或多年的数据。每种方法都有其优缺点。基于较长时间段估算的标准差会更稳定,因为我们有更大的样本量。然而,远过去的一些信息肯定会过时:

import numpy as np
import pandas as pd
from scipy.stats import norm
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
# input area
ticker='IBM'              # input 1
n_shares=1000             # input 2
confidence_level=0.99     # input 3
begdate=(2012,2,7)        # input 4
enddate=(2017,2,7)        # input 5
#
z=norm.ppf(1-confidence_level) 
x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
print(x[0])
ret = x.aclose[1:]/x.aclose[:-1]-1
#
position=n_shares*x.close[0] 
std=np.std(ret)
#
VaR=position*z*std
print("Holding=",position, "VaR=", round(VaR,4), "tomorrow")
(datetime.date(2012, 2, 7), 2012, 2, 7, 734540.0, 167.75861437920275, 168.543152, 169.23178870104016, 167.34020198573538, 3433000.0, 168.543152)
('Holding=', 168543.152, 'VaR=', -4603.5087, 'tomorrow')

打印数据第一行的目的是为了显示收盘价确实是在 2017 年 2 月 7 日。我们的持仓价值为 168,543 美元,其 1 日 VaR 为 4,604 美元。第二个示例是关于 10 天期间的 VaR。要将每日回报的方差(标准差)转换为 n 天的方差(标准差),我们有以下公式:

VaR 介绍

例如,年波动率等于日波动率乘以 252 的平方根 VaR 介绍。为了将每日平均回报转换为 n 天的平均回报,我们有以下公式:

VaR 介绍

基于每日回报,我们有以下一般公式来估算 n 天 VaR 的置信水平:

VaR 介绍

以下代码显示了在 2016 年最后一天,持有 50 股沃尔玛股票,在 99% 置信水平下的 10 天 VaR:

import numpy as np
import pandas as pd
from scipy.stats import norm
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
ticker='WMT'            # input 1
n_shares=50             # input 2
confidence_level=0.99   # input 3
n_days=10               # input 4
begdate=(2012,1,1)      # input 5
enddate=(2016,12,31)    # input 6

z=norm.ppf(confidence_level) 

x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
ret = x.aclose[1:]/x.aclose[:-1]-1 
position=n_shares*x.close[0] 
VaR=position*z*np.std(ret)*np.sqrt(n_days)
print("Holding=",position, "VaR=", round(VaR,4), "in ", n_days, "Days")
('Holding=', 2650.3070499999999, 'VaR=', 205.0288, 'in ', 10, 'Days')

2016 年 12 月 31 日,我们的持仓价值为$2,650。我们在接下来的 10 天内的最大损失为$205,置信水平为 99%。在前面的程序中,我们基于日收益率估算了日均收益和标准差。然后我们将其转换为 10 天的平均收益和 10 天的波动率。另一方面,实际上我们可以直接计算 10 天的收益率。在 10 天收益率可用后,可以直接应用scipy.mean()scipy.std()函数。换句话说,我们不需要将日均收益和日标准差转换为 10 天均值和 10 天标准差。相关代码如下。为了节省空间,前 11 行没有重复:

x = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
logret = np.log(x.aclose[1:]/x.aclose[:-1])

# method 2: calculate 10 day returns 
ddate=[]
d0=x.date
for i in range(0,np.size(logret)): 
    ddate.append(int(i/nDays))
y=pd.DataFrame(logret,ddate,columns=['retNdays']) 
retNdays=y.groupby(y.index).sum()
#print(retNdays.head())
position=n_shares*x.close[0] 
VaR=position*z*np.std(retNdays)
print("Holding=",position, "VaR=", round(VaR,4), "in ", nDays, "Days")
('Holding=', 2650.3070499999999, 'VaR=', 209.1118, 'in ', 10, 'Days')

我们的新结果显示,VaR 为$209.11,相比之下,原值为$205.03。低估的百分比为-0.01951126,约为-2%。以下代码估算了 Fama-French 五个按市值加权的行业组合的 VaR,数据频率为月度。数据集可以在作者的官网获取,链接:canisius.edu/~yany/python/ff5VWindustryMonthly.pkl。这五个行业分别是消费品、制造业、高科技、健康和其他。以下是前几行和最后几行代码:

import pandas as pd
x=pd.read_pickle("c:/temp/ff5VWindustryMonthly.pkl")
print(x.head())
print(x.tail())
         CNSMR   MANUF   HITEC   HLTH    OTHER
192607  0.0543  0.0273  0.0183  0.0177  0.0216
192608  0.0276  0.0233  0.0241  0.0425  0.0438
192609  0.0216 -0.0044  0.0106  0.0069  0.0029
192610 -0.0390 -0.0242 -0.0226 -0.0057 -0.0285
192611  0.0370  0.0250  0.0307  0.0542  0.0211
         CNSMR   MANUF   HITEC   HLTH    OTHER
201608 -0.0101  0.0040  0.0068 -0.0323  0.0326
201609 -0.0143  0.0107  0.0202  0.0036 -0.0121
201610 -0.0252 -0.0231 -0.0141 -0.0743  0.0059
201611  0.0154  0.0539  0.0165  0.0137  0.1083
201612  0.0132  0.0158  0.0163  0.0084  0.0293

以下程序估算了将$1,000 投资于每个行业组合的 VaR,置信水平为 99%,计算的是下一个周期的风险。由于数据频率为月度,因此固定周期为下一个月:

import pandas as pd
import scipy as sp
from scipy.stats import norm
#
confidence_level=0.99   # input 
position=([1000,1000,1000,1000,1000])
z=norm.ppf(1-confidence_level)
x=pd.read_pickle("c:/temp/ff5VWindustryMonthly.pkl")
#
std=sp.std(x,axis=0)
mean=sp.mean(x,axis=0)
#
t=sp.dot(position,z)
VaR=t*std
#
# output area
print(sp.shape(x))
print("Position=",position)
print("VaR=")
print(VaR)
1086, 5)
('Position=', [1000, 1000, 1000, 1000, 1000])
VaR=
CNSMR   -122.952735
MANUF   -128.582446
HITEC   -129.918893
HLTH    -130.020356
OTHER   -149.851230
dtype: float64

对于这五个行业,VaR 分别为$122.95、$128.58、$129.92、$130.02 和$149.85,假设每个行业投资$1,000。通过比较这些值,我们可以看到,消费品行业的风险最低,而被定义为“其他”的行业具有最高的最大可能损失。

正态性检验

第一个估算 VaR 的方法是基于一个重要的假设,即个别股票或组合的收益符合正态分布。然而,在现实世界中,我们知道股票收益或组合收益不一定符合正态分布。以下程序通过使用 5 年的日数据,检验微软的收益是否满足这个假设:

from scipy import stats 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
import numpy as np 
#	
ticker='MSFT' 
begdate=(2012,1,1) 
enddate=(2016,12,31) 
#
p =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
ret = (p.aclose[1:] - p.aclose[:-1])/p.aclose[1:] 
print 'ticker=',ticker,'W-test, and P-value' 
print(stats.shapiro(ret))
print( stats.anderson(ret))
ticker= MSFT W-test, and P-value
(0.9130843877792358, 3.2116320877511604e-26)
AndersonResult(statistic=14.629260310763584, critical_values=array([ 0.574,  0.654,  0.785,  0.915,  1.089]), significance_level=array([ 15\. ,  10\. ,   5\. ,   2.5,   1\. ]))

我们的原假设是微软股票的日收益符合正态分布。根据前面的结果,我们拒绝原假设,因为 F 值远高于临界值 1.089(假设显著性水平为 1%)。即使我们基于单只股票拒绝了这个假设,也有人可能会认为组合的收益可能符合这一假设。下一个程序检验 S&P500 的日收益是否符合正态分布。S&P500 的股票代码是^GSPC,可以通过 Yahoo!Finance 获取:

import numpy as np 
from scipy import stats 
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
#
ticker='^GSPC'    # ^GSPC is for S&P500
begdate=(2012,1,1) 
enddate=(2016,12,31) 
#
p =getData(ticker, begdate, enddate,asobject=True, adjusted=True) 
ret = (p.aclose[1:] - p.aclose[:-1])/p.aclose[1:] 
print 'ticker=',ticker,'W-test, and P-value' 
print(stats.shapiro(ret))
print( stats.anderson(ret) )
ticker= ^GSPC W-test, and P-value
(0.9743353128433228, 3.7362179458122827e-14)
AndersonResult(statistic=8.6962226557502618, critical_values=array([ 0.574,  0.654,  0.785,  0.915,  1.089]), significance_level=array([ 15\. ,  10\. ,   5\. ,   2.5,   1\. ]))

从前面的结果来看,我们拒绝了 S&P500 的正态性假设。换句话说,由 S&P500 日收益表示的市场指数不符合正态分布。

偏度和峰度

基于正态性假设,VaR 估算只考虑前两个矩:均值和方差。如果股票回报真的遵循正态分布,这两个矩将完全定义它们的概率分布。从前面的章节可以知道,这并不成立。第一种修正方法是除了前两个矩之外,加入其他更高阶的矩。第三和第四阶矩分别称为偏度和峰度。对于一个具有 n 个回报的股票或投资组合,偏度通过以下公式估算:

偏度和峰度

在这里,偏度是偏度,Ri是第i个回报,偏度和峰度是平均回报,n是回报的数量,σ是回报的标准差。峰度反映了极端值的影响,因为四次方的值非常高。峰度通常通过以下公式估算:

偏度和峰度

对于标准正态分布,它的均值为零,方差为 1,偏度为零,峰度为 3。因此,有时峰度被定义为前面的公式减去 3:

偏度和峰度

一些教科书将这两种定义区分为峰度和超额峰度。然而,也有一些教科书简单地将前面的公式也标记为峰度。因此,当我们进行检验以查看时间序列的峰度是否为零时,我们必须知道使用的是哪个基准。以下程序生成了 500 万随机数来自标准差,并应用四个函数来估算这四个矩,即均值、标准差、偏度和峰度:

from scipy import stats,random
import numpy as np
np.random.seed(12345)
n=5000000   
#
ret = random.normal(0,1,n)
print('mean    =', np.mean(ret))
print('std     =',np.std(ret))
print('skewness=',stats.skew(ret))
print('kurtosis=',stats.kurtosis(ret))
('mean    =', 0.00035852273706422504)
('std     =', 0.99983435063933623)
('skewness=', -0.00040545999711941665)
('kurtosis=', -0.001162270913658947)

由于从标准正态分布中抽取的随机数的峰度接近零,scipy.stats.kurtosis()函数应基于公式(11)而不是公式(10)

修正 VaR

从前面的讨论中我们知道,基于假设,股票回报服从正态分布。因此,回报的偏度和峰度都假设为零。然而,在现实世界中,许多股票回报的偏度和超额峰度并不为零。因此,开发了修正 VaR,利用这四个矩而不仅仅是两个;请参见以下定义:

修正 VaR

这里,z是基于正态分布的值,S是偏度,K是峰度,t是一个中间变量,scipy.stats.ppf()函数为给定的置信水平提供一个 z 值。以下程序提供了基于正态性假设和基于前述公式(即使用所有四个矩)计算的两种 VaR。年末时持有的股票数量为 500 只,测试的股票是沃尔玛WMT)。1 天 VaR 的置信水平为 99%:

import numpy as np
import pandas as pd
from scipy.stats import stats,norm
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
ticker='WMT'            # input 1
n_shares=500            # input 2
confidence_level=0.99   # input 3
begdate=(2000,1,1)      # input 4
enddate=(2016,12,31)    # input 5
#
# Method I: based on the first two moments
z=abs(norm.ppf(1-confidence_level)) x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
ret = x.aclose[1:]/x.aclose[:-1]-1
position=n_shares*x.close[0] 
mean=np.mean(ret)
std=np.std(ret)
VaR1=position*(mean-z*std)
print("Holding=",round(position,2), "VaR1=", round(VaR1,2), "for 1 day ")
#
# Modified VaR: based on 4 moments
s=stats.skew(ret)
k=stats.kurtosis(ret)
t=z+1/6.*(z**2-1)*s+1/24.*(z**3-3*z)*k-1/36.*(2*z**3-5*z)*s**2
mVaR=position*(mean-t*std)
print("Holding=",round(position,2), "modified VaR=", round(mVaR,2), "for 1 day ")
('Holding=', 24853.46, 'VaR1=', -876.84, 'for 1 day ')
('Holding=', 24853.46, 'modified VaR=', -1500.41, 'for 1 day ')

根据最后两行,我们得出基于正态分布的 VaR 为 $876.84,修正后的 VaR 为 $1,500。两者之间的百分比差异为 42%。这一结果表明,忽略偏度和峰度将极大低估 VaR。

基于排序的历史收益 VaR

我们知道,股票收益不一定遵循正态分布。另一种方法是使用排序后的收益来评估 VaR。这种方法称为基于历史收益的 VaR。假设我们有一个名为 ret 的每日收益向量。我们将其从最小到最大排序。我们将排序后的收益向量称为 sorted_ret。对于给定的置信水平,一期 VaR 计算公式如下:

基于排序的历史收益 VaR

这里,position 是我们的财富(投资组合的价值),confidence 是置信水平,n 是收益的数量。len() 函数显示观察值的数量,int() 函数取输入值的整数部分。例如,如果收益向量的长度是 200,且置信水平为 99%,那么排序后的收益向量中第二个值(200*0.01),从最小到最大,再乘以我们的财富,就是我们的 VaR。显然,如果我们有更长的时间序列,也就是更多的收益观察值,我们的最终 VaR 将更加准确。比如拥有 500 股沃尔玛股票,99% 的置信水平下,第二天的最大损失是多少?首先,让我们看看几种排序数据的方法。第一种方法使用 numpy.sort() 函数:

import numpy as np
a = np.array([[1,-4],[9,10]])
b=np.sort(a)                
print("a=",a)
print("b=",b)
('a=', array([[ 1, -4],
       [ 9, 10]]))
('b=', array([[-4,  1],
       [ 9, 10]]))

这是使用 Python pandas 模块进行排序的第二种方法:

import pandas as pd
a = pd.DataFrame([[9,4],[9,2],[1,-1]],columns=['A','B'])
print(a)
# sort by A ascedning, then B descending 
b= a.sort_values(['A', 'B'], ascending=[1, 0])
print(b)
# sort by A and B, both ascedning 
c= a.sort_values(['A', 'B'], ascending=[1, 1])
print(c)

为了方便比较,这三个数据集并排放置。左侧面板显示原始数据集。中间面板显示按列 A 升序排序后,再按列 B 降序排序的结果。右侧面板显示按列 AB 都升序排序的结果:

基于排序的历史收益 VaR

接下来的两个程序比较了两种估算 VaR(风险价值)的方法:基于正态分布的方法和基于排序的方法。为了让我们的程序更易理解,时间段仅为 1 天:

#
z=norm.ppf(confidence_level) 
x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
ret = x.aclose[1:]/x.aclose[:-1]-1
#
position=n_shares*x.close[0] 
std=np.std(ret)
#
VaR=position*z*std
print("Holding=",position, "VaR=", round(VaR,4), "tomorrow")
('Holding=', 26503.070499999998, 'VaR=', 648.3579, 'tomorrow')

上面程序中使用的公式是 VaR=positionzsigma。结果告诉我们,持仓为 $26,503,1 天 VaR 为 $648,置信水平为 99%。以下程序基于排序估算同一股票的 VaR:

ret = np.array(x.aclose[1:]/x.aclose[:-1]-1)
ret2=np.sort(ret) 
#
position=n_shares*x.close[0] 
n=np.size(ret2)
leftTail=int(n*(1-confidence_level))
print(leftTail)
#
VaR2=position*ret2[leftTail]
print("Holding=",position, "VaR=", round(VaR2,4), "tomorrow")
('Holding=', 26503.070499999998, 'VaR=', -816.7344, 'tomorrow')

结果显示,1 天 VaR 为 $817。回想一下,基于正态分布的 VaR 为 $648。如果第二种方法更准确,第一种方法将我们的潜在损失低估了 20%。在风险评估中,这个差异非常巨大!以下代码是基于排序的 n 天期的 VaR:

ret = x.aclose[1:]/x.aclose[:-1]-1
position=n_shares*x.close[0] 
#
# Method 1: based on normality 
mean=np.mean(ret)
std=np.std(ret)
meanNdays=(1+mean)**nDays-1
stdNdays=std*np.sqrt(nDays)
z=norm.ppf(confidence_level) 
VaR1=position*z*stdNdays
print("Holding=",position, "VaR1=", round(VaR1,0), "in ", nDays, "Days")
#
# method 2: calculate 10 day returns 
ddate=[]
d0=x.date
for i in range(0,np.size(logret)): 
    ddate.append(int(i/nDays))
y=pd.DataFrame(logret,index=ddate,columns=['retNdays']) 
logRet=y.groupby(y.index).sum()
retNdays=np.exp(logRet)-1
# 
VaR2=position*z*np.std(retNdays)
print("Holding=",position, "VaR2=", round(VaR2,0), "in ", nDays, "Days")
# 
# Method III
ret2=np.sort(retNdays) 
n=np.size(ret2)
leftTail=int(n*(1-confidence_level))
print(leftTail)
#
VaR3=position*ret2[leftTail]
print("Holding=",position, "VaR=", round(VaR3,0), "in ",nDays, "Days")
('Holding=', 24853.456000000002, 'VaR1=', 2788.0, 'in ', 10, 'Days')
('Holding=', 24853.456000000002, 'VaR2=', 2223.0, 'in ', 10, 'Days')
4
('Holding=', 24853.456000000002, 'VaR=', 1301.0, 'in ', 10, 'Days')

在前面的程序中有两个技巧。第一个技巧是将每日对数收益率的总和转化为 10 天的对数收益率。然后,我们将对数收益率转换为百分比收益率。第二个技巧是如何生成 10 天的收益率。首先,我们使用int()函数生成组,即int(i/nDays)。由于nDays的值为 10,int(i/10)会生成 10 个零、10 个一、10 个二,依此类推。基于三种方法的 VaR 分别为$2,788、$2,223 和$1,301。显然,第三种方法存在一些问题。一个问题是,对于 n 天的周期,我们只有 428 个观察值,也就是说样本的大小可能太小。如果我们选择 99%的置信区间,则必须选择计算中的第四低收益。这肯定会导致一些问题。

模拟与 VaR

在前面的章节中,我们学习了两种估算单只股票或投资组合 VaR 的方法。第一种方法假设股票收益符合正态分布。第二种方法使用排序后的历史收益。那么这两种方法之间有什么联系呢?实际上,蒙特卡洛模拟可以作为它们之间的桥梁。首先,让我们来看一下基于正态假设的第一种方法。我们在 2016 年最后一天持有 500 股沃尔玛股票。如果明天的置信区间为 99%,那么明天的 VaR 是多少?

#
position=n_shares*x.close[0] 
mean=np.mean(ret)
std=np.std(ret)
#
VaR=position*(mean+z*std)
print("Holding=",position, "VaR=", round(VaR,4), "tomorrow")
('Holding=', 26503.070499999998, 'VaR=', -641.2911, 'tomorrow')

明天的 VaR 为$641.29,置信水平为 99%。下面是蒙特卡洛模拟的工作原理。首先,我们基于每日收益计算均值和标准差。由于假设股票收益符合正态分布,我们可以生成 5,000 个收益,均值和标准差相同。如果我们的置信水平为 99%,那么从按升序排列的收益中,位于第 50 个的收益将是我们的截断点,50000.01=50*。以下是代码:

#
position=n_shares*x.close[0] 
mean=np.mean(ret)
std=np.std(ret)
#
n_simulation=5000
sp.random.seed(12345) 
ret2=sp.random.normal(mean,std,n_simulation) 
ret3=np.sort(ret2) 
m=int(n_simulation*(1-confidence_level))
VaR=position*(ret3[m])
print("Holding=",position, "VaR=", round(VaR,4), "tomorrow")
('Holding=', 26503.070499999998, 'VaR=', -627.3443, 'tomorrow')

与基于公式的$641.29 相比,蒙特卡洛模拟给出的值为$627.34,差异相对较小。

投资组合的 VaR

在第九章中,投资组合理论展示了当我们将多只股票放入投资组合时,可以降低或消除公司特定的风险。估算 n 只股票投资组合收益率的公式如下:

投资组合的 VaR

这里的Rp,t是时刻t的投资组合收益,wi是股票i的权重,Ri,t是股票i在时刻t的收益。谈到预期收益或均值时,我们有一个非常相似的公式:

投资组合的 VaR

在这里,投资组合的 VaR是平均或预期的投资组合收益率,投资组合的 VaR是股票i的平均或预期收益率。这样一个 n 只股票投资组合的方差公式如下:

投资组合的 VaR

在这里,投资组合的 VaR是投资组合方差,σi,j 是股票 i 和股票 j 之间的协方差;请参见以下公式:

投资组合的 VaR

股票i与股票j之间的相关性ρi,j定义如下:

投资组合的 VaR

当股票之间没有完全正相关时,组合股票会降低我们的投资组合风险。以下程序显示,投资组合的 VaR 不仅仅是单个股票 VaR 的简单加总或加权 VaR:

from matplotlib.finance import quotes_historical_yahoo_ochl as getData

# Step 1: input area
tickers=('IBM','WMT','C')  # tickers
begdate=(2012,1,1)         # beginning date 
enddate=(2016,12,31)       # ending date
weight=(0.2,0.5,0.3)       # weights
confidence_level=0.99      # confidence level 
position=5e6               # total value
#
z=norm.ppf(confidence_level) 
# Step 2: define a function
def ret_f(ticker,begdate,enddte):
    x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
    ret=x.aclose[1:]/x.aclose[:-1]-1
    d0=x.date[1:]
    return pd.DataFrame(ret,index=d0,columns=[ticker])
# Step 3
n=np.size(tickers)
final=ret_f(tickers[0],begdate,enddate)
for i in np.arange(1,n):
    a=ret_f(tickers[i],begdate,enddate)
    if i>0:
        final=pd.merge(final,a,left_index=True,right_index=True)
#
# Step 4: get porfolio returns
portRet=sp.dot(final,weight)
portStd=sp.std(portRet)
portMean=sp.mean(portRet)
VaR=position*(portMean-z*portStd)
print("Holding=",position, "VaR=", round(VaR,2), "tomorrow")

# compare
total2=0.0
for i in np.arange(n):
    stock=tickers[i]
    ret=final[stock]
    position2=position*weight[i]
    mean=sp.mean(ret)
    std=sp.std(ret)
    VaR=position2*(mean-z*std)
    total2+=VaR
    print("For ", stock, "with a value of ", position2, "VaR=", round(VaR,2))
print("Sum of three VaR=",round(total2,2))
('Holding=', 5000000.0, 'VaR=', -109356.22, 'tomorrow')
('For ', 'IBM', 'with a value of ', 1000000.0, 'VaR=', -27256.67)
('For ', 'WMT', 'with a value of ', 2500000.0, 'VaR=', -60492.15)
('For ', 'C', 'with a value of ', 1500000.0, 'VaR=', -59440.77)
('Sum of three VaR=', -147189.59)

我们当前投资组合的 VaR 为$109,356。然而,基于权重计算的这三只股票的 VaR 总和为$147,190。这个结果验证了通过选择不同股票来实现的分散化效应。

回测与压力测试

在金融领域,压力测试可以看作是一种分析或模拟,旨在确定特定金融工具(如 VaR)在经济危机中的应对能力。由于估算 VaR 的首个方法基于股票回报服从正态分布的假设,其准确性取决于股票回报在现实中偏离这一假设的程度。模型验证是基于模型的风险管理实施的关键组成部分。也就是说,我们需要某种方法来确定所选择的模型是否准确且一致地执行。对于公司及其监管机构而言,这一步非常重要。根据 Lopez(2000 年)的研究,以下是表格:

名称 目标 方法
回测 将观察到的结果与模型的预期输出进行比较 预测评估确立了一个具有大量学术文献的经验问题
压力测试 在极端条件下展示模型的预期结果
  • 投影分析

  • 异常值分析

  • 情景分析与案例研究

|

表 11.1 回测与压力测试

假设我们仅使用 1 年的数据来估算 2017 年 2 月 7 日持有 1,000 股 IBM 的 1 天 VaR,置信度为 99%。程序如下所示:

#
position=n_shares*x.close[0] 
mean=np.mean(ret)
z=norm.ppf(1-confidence_level)
std=np.std(ret)
#
VaR=position*(mean+z*std)
print("Holding=",position, "VaR=", round(VaR,4), "tomorrow")
print("VaR/holding=",VaR/position)
(datetime.date(2016, 2, 8), 2016, 2, 8, 736002.0, 121.65280462310274, 122.598996, 123.11070921267809, 119.84731962624865, 7364000.0, 122.598996)
('Holding=', 122598.996, 'VaR=', -3186.5054, 'tomorrow')
('VaR/holding=', -0.025991284652254254)

根据之前的结果,我们的持仓为$122,599,下一天的最大损失为$3,187。请记住,置信度为 99%,这意味着在这一年期间,我们应预期约 2.5 次违例(0.01*252)。252 是指一年中的交易日数。以下程序显示了违例的次数:

VaR=-3186.5054            # from the previous program
position=122598.996       # from the previous program
#('Holding=', 122598.996, 'VaR=', -3186.5054, 'tomorrow')
#('VaR/holding=', -0.025991284652254254)
#
z=norm.ppf(1-confidence_level) 
x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
print("first day=",x[0])
ret = x.aclose[1:]/x.aclose[:-1]-1
#
cutOff=VaR/position 
n=len(ret)
ret2=ret[ret<=cutOff]
n2=len(ret2)
print("n2=",n2)
ratio=n2*1./(n*1.)
print("Ratio=", ratio)
('first day=', (datetime.date(2016, 2, 8), 2016, 2, 8, 736002.0, 121.65280462310274, 122.598996, 123.11070921267809, 119.84731962624865, 7364000.0, 122.598996))
('n2=', 4)
('Ratio=', 0.015873015873015872)

再次,我们期望根据模型看到 2.5 次违例。然而,实际违例次数为 4 次。基于 99%的置信度,我们预期回报低于-2.599%的情况应该约为 1%。不幸的是,基于 1 年的数据,这一比例为 1.58%。如果基于 55 年的历史数据,对于这只特定股票,回报低于该比例的频率超过了 2 倍,分别为 3.66%与 1%。这表明基础模型低估了潜在的最大损失。

预期损失

在前面的章节中,我们讨论了与 VaR 相关的许多问题,如其定义和如何估算。然而,VaR 的一个主要问题是它依赖于基础证券或投资组合的分布形态。如果正态分布的假设接近成立,那么 VaR 是一个合理的度量。否则,如果我们观察到胖尾现象,我们可能会低估最大损失(风险)。另一个问题是,VaR 触及后分布的形态被忽略了。如果我们有比正态分布描述的更胖的左尾,那么我们的 VaR 会低估真实风险。反之,如果左尾比正态分布更薄,我们的 VaR 则会高估真实风险。预期损失ES)是在 VaR 触及时的预期损失,其定义如下:

预期损失

这里,ES是预期损失,α是我们的显著性水平,如 1%或 5%。基于正态分布假设,对于我们的 Python 示例,我们有以下公式:

预期损失

预期损失可以通过以下方式估算:

预期损失

以下程序展示了如何从正态分布中生成回报,然后估算 VaR 和 ES:

import scipy as sp
import scipy.stats as stats
x = sp.arange(-3,3,0.01)
ret=stats.norm.pdf(x)
confidence=0.99
position=10000
z=stats.norm.ppf(1-confidence)
print("z=",z)
zES=-stats.norm.pdf(z)/(1-confidence)
print("zES=", zES)
std=sp.std(ret)
VaR=position*z*std
print("VaR=",VaR)
ES=position*zES*std
print("ES=",ES)

同样,我们可以推导出基于历史回报估计预期损失(Expected Shortfall,简称 ES)的公式。从某种意义上说,预期损失是基于低于 VaR 阈值的回报的平均损失。假设我们有 n 个回报观测值,预期损失可以定义如下:

预期损失

这里,ES是预期损失,position是我们的投资组合的价值,m是比给定置信水平指定的截断点更差的回报观测数,Ii是一个虚拟变量,当回报小于Rcutoff时其值为 1,否则为 0,Ri是第i个回报,Rcutoff是由给定置信水平确定的截断回报,n 是所有回报观测的总数,m 是小于截断回报的回报数。例如,如果我们有 1,000 个回报观测,且置信水平为 99%,则截断回报将是按从低到高排序的回报中的第 10 个观测值。预期损失将是这 10 个最差情境的平均损失。

假设在 2016 年最后一天,我们持有 500 股沃尔玛股票。假设我们关心的是次日最大损失,并且置信水平为 99%。根据历史回报的排名,VaR 和预期损失是多少?以下代码给出了答案:

x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
ret = np.array(x.aclose[1:]/x.aclose[:-1]-1)
ret2=np.sort(ret) 
#
position=n_shares*x.close[0] 
n=np.size(ret2)
m=int(n*(1-confidence_level))
print("m=",m)
#
sum=0.0
for i in np.arange(m):
    sum+=ret2[i]
ret3=sum/m
ES=position*ret3
print("Holding=",position, "Expected Shortfall=", round(ES,4), "tomorrow")
('m=', 12)
('Holding=', 26503.070499999998, 'Expected Shortfall=', -1105.1574, 'tomorrow')

由于有 11 个回报低于第 12 个回报,预期损失将是这些 12 个回报的平均值,乘以评估日的投资组合市值:

附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算

本数据集有三个目标:

  • 理解与 VaR 相关的概念和方法

  • 估算个股的 VaR

  • 估算投资组合的 VaR

问题是:在 99%的置信区间下,对于每只股票和一个等权重投资组合,10 天的 VaR 是多少?假设数据期间为 2012 年 2 月 7 日到 2017 年 2 月 7 日,并且你有 100 万美元的投资(公式 1 中的position):

i 公司名称 股票代码 行业
1 微软公司 MSFT 应用软件
2 苹果公司 AAPL 个人计算机
3 家得宝公司 HD 家装服务
4 花旗集团公司 C 大型银行
5 沃尔玛公司 WMT 折扣与杂货店
6 通用电气公司 GE 技术

具体步骤如下:

  1. 从 Yahoo! Finance 获取日数据。

  2. 估算日收益。

  3. 应用以下公式估算 VaR:附录 A – 数据案例 7 – 个股与投资组合的 VaR 估算

  4. 根据排序的历史收益估算 VaR。

  5. 如果可能,使用 VBA、R、SAS 或 Matlab 自动化该过程。

VaR 最常用的参数为 1%和 5%的概率(99%和 95%的置信水平),以及 1 天和 2 周的时间范围。基于正态性假设,我们得到以下通用公式:

附录 A – 数据案例 7 – 个股与投资组合的 VaR 估算

这里,position是我们投资组合的当前市场价值,µperiod是预期的周期收益,z是根据置信水平决定的截断点,σ是波动性。对于正态分布,z=2.33 对应 99%的置信水平,z=1.64 对应 95%的置信水平。当时间范围很短,如 1 天时,我们可以忽略µperiod的影响。因此,我们得到最简单的形式:

附录 A – 数据案例 7 – 个股与投资组合的 VaR 估算

根据正态性假设估算 VaR。

如果基础证券遵循正态分布,则 VaR 公式如下:

附录 A – 数据案例 7 – 个股与投资组合的 VaR 估算

对于 99%和 95%的置信水平,公式(5)变为以下公式:

置信水平 公式
99% 附录 A – 数据案例 7 – 个股与投资组合的 VaR 估算
95% 附录 A – 数据案例 7 – 个股与投资组合的 VaR 估算

n 天 VaR 的估算取决于如何计算 n 天的收益和标准差。变换基于以下公式,不同频率之间的方差关系:

附录 A – 数据案例 7 – 个股与投资组合的 VaR 估算

例如,年波动率等于每日波动率乘以 252 的平方根 附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算。基于日收益率,我们得出以下一般公式,用于 99%或 95%置信水平的 VaR 估算:

附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算

这里,附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算是预期的每日收益率,n 是天数,附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算是每日波动率,附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算是 n 天的波动率,confidence 是置信水平,如 99%或 95%,而 p 是头寸。如果我们不知道预期收益率,并且假设预期的均值收益率与实际的均值收益率相同,那么我们有以下公式:

附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算

对于 99%和 95%的置信水平,我们有以下内容:

附录 A – 数据案例 7 – 个别股票和投资组合的 VaR 估算

参考文献

请参考以下文章:

练习

  1. VaR 的最简单定义是什么?VaR、方差、标准差和贝塔值之间有什么区别?

  2. 假设我们有一个计划,形成一个两只股票的投资组合。置信水平为 99%,周期数为 10 天。如果第一只股票的 VaR 为 x,而第二只股票的 VaR 为 y,那么投资组合的 VaR 是否为加权个别股票的 VaR,即VaR(投资组合) = wAx + wBy,其中WA是股票A的权重,而wB是股票 B 的权重?请解释。

  3. IBM 的回报是否符合正态分布?它们的偏度和峰度值是否分别为零和三(过度峰度为零)?

  4. 正态分布的偏度和峰度值是多少?通过使用rnorm()生成 n 个随机数来支持你的结论。

  5. 编写一个 Python 函数,估算给定股票代码的均值、标准差、偏度和峰度;例如,moments4("ticker", begdate, enddate)

  6. 假设我们持有 134 股微软股票;今天的总价值是多少?在 95%置信水平下,明天的最大损失是多少?如果我们的持有期是 1 个月而不是 1 天,价值是多少?

  7. 使用月度收益代替日度收益重复 11.4 中的最后一个问题,答案是否与 11.4 中的不同?

  8. 我们的投资组合有 100 股 IBM 股票和 300 股微软股票。在 99%的置信水平下,持有期为 1 天时的 VaR 是多少?

  9. 为了估算戴尔公司 1 个月的 VaR,我们可以将日 VaR 转换为月 VaR,或者直接根据月度数据计算 VaR。两者有区别吗?

  10. 当我们估算 VaR 时,可以使用不同的时间周期,例如过去 1 年或过去 5 年。这会有区别吗?使用几个股票代码进行探索并评论结果。

  11. 评论不同的 VaR 方法,例如基于正态假设、历史收益和修正 VaR 的方法。

  12. 如果一个基金投资了 10%的 IBM,12%的谷歌,其余部分投资于沃尔玛,投资组合的波动率是多少?

  13. 如果 IBM 股票的权重为 10%,戴尔为 12%,沃尔玛为 20%,其余部分为长期 10 年期国债,那么投资组合的波动率是多少?

  14. 基于 11.11,如果投资组合的价值为 1000 万美元,那么在接下来的 6 个月内,99%置信水平下的 VaR 是多少?

  15. 使用 99%置信水平和 10 个交易日作为持有期,基于历史收益法估算 VaR:100 股 IBM,200 股花旗,200 股微软和 400 股沃尔玛。

  16. 基于正态分布假设的 VaR 通常小于基于历史收益的 VaR,这是真的吗?

    提示

    你可以使用滚动窗口对股票进行处理来显示你的结果(答案)。或者,你可以使用多只股票。

  17. 基于偏度的代码,编写一个 Python 函数来计算峰度。将你的函数与scipy.stats.kurtosis()函数进行比较。

  18. 如果我们的持有期不是 1 天,如何根据历史收益估算 VaR?公式是什么?

  19. 如果持有期为 2 周(10 个交易日),如何根据历史收益数据估算 VaR?

  20. 如果我们的持有 IBM、戴尔和沃尔玛的股票分别是 100 股、200 股和 500 股,99%的置信水平和 2 周的持有期下,最大可能损失(VaR)是多少?

  21. 编写一个 Python 程序,使用历史数据生成 VaR。函数的结构将是VaR_historical(ticker, confidence_level, n_days)

总结

在本章中,详细讨论了一种重要的风险度量方法——风险价值VaR)。为了估算单只股票或投资组合的 VaR,解释了两种最流行的方法:基于正态性假设的方法和基于历史收益排序的方法。此外,我们还讨论了修改版 VaR 方法,该方法除了考虑收益的前两阶矩,还考虑了第三阶和第四阶矩。在第十二章,蒙特卡罗模拟,我们解释了如何将模拟应用于金融领域,例如模拟股票价格波动和收益,复制 Black-Scholes-Merton 期权模型,以及定价一些复杂期权。

第十二章:蒙特卡洛模拟

蒙特卡洛模拟是金融领域中非常有用的工具。例如,因为我们可以通过从对数正态分布中抽取随机数来模拟股票价格,所以著名的布莱克-斯科尔斯-梅顿期权模型可以被复制。在第九章,投资组合理论中,我们学习到,通过向投资组合中添加更多股票,企业特定风险可以被减少或消除。通过模拟,我们可以更清晰地看到多样化效应,因为我们可以反复从 5000 只股票中随机选择 50 只股票。对于资本预算,我们可以模拟几十个具有不确定未来值的变量。对于这些情况,可以通过模拟生成许多可能的未来结果、事件以及各种类型的组合。在本章中,将涵盖以下主题:

  • 从正态分布、均匀分布和泊松分布中生成随机数

  • 使用蒙特卡洛模拟估算π值

  • 使用对数正态分布模拟股票价格变动

  • 构建高效投资组合与有效前沿

  • 通过模拟复制布莱克-斯科尔斯-梅顿期权模型

  • 定价一些复杂期权,例如带浮动行权价格的回顾期权

  • 自助法(有/无放回)

  • 长期预期回报预测

  • 效率、准蒙特卡洛模拟与 Sobol 序列

蒙特卡洛模拟的重要性

蒙特卡洛模拟,或称模拟,在金融领域发挥着非常重要的作用,具有广泛的应用。假设我们打算估算一个项目的净现值NPV)。未来存在许多不确定性,例如借款成本、最终产品价格、原材料等。对于少数几个变量,我们仍然能够轻松处理。然而,如果面对二十多个具有不确定未来值的变量,寻找解决方案就会成为一个难题。幸运的是,这时可以应用蒙特卡洛模拟。在第十章,期权与期货中,我们了解到,布莱克-斯科尔斯-梅顿期权模型背后的逻辑是股票回报的正态性假设。正因为如此,它们的封闭形式解可以通过模拟来复制。另一个例子是从 4500 只可用股票中随机选择 50 只。与传统期权(如布莱克-斯科尔斯-梅顿模型)不同,复杂期权没有封闭形式解。幸运的是,我们可以使用模拟来为其中一些期权定价。

从标准正态分布生成随机数

正态分布在金融中起着核心作用。一个主要原因是,许多金融理论(如期权理论及其相关应用)假设股票收益服从正态分布。第二个原因是,如果我们的计量经济学模型设计得当,那么模型中的误差项应该服从零均值的正态分布。生成标准正态分布中的 n 个随机数是一个常见任务。为此,我们有以下三行代码:

import scipy as sp
x=sp.random.standard_normal(size=10)
print(x)
[-0.98350472  0.93094376 -0.81167564 -1.83015626 -0.13873015  0.33408835
  0.48867499 -0.17809823  2.1223147   0.06119195]

SciPy/NumPy 中的基本随机数是通过 numpy.random 函数中的梅森旋转算法(Mersenne Twister PRNG)生成的。numpy.random 中的分布随机数是用 Cython/Pyrex 编写的,运行速度非常快。读者不可能得到与此处相同的10个随机数。我们很快会解释如何生成相同的一组随机数。或者,我们可以使用以下代码:

>>>import scipy as sp
>>>x=sp.random.normal(size=10)

这个程序等价于以下程序:

>>>import scipy as sp 
>>>x=sp.random.normal(0,1,10)

第一个输入是均值,第二个输入是标准差,最后一个输入是随机数的个数,也就是我们期望数据集的大小。对比前两个程序,显然均值和标准差的默认设置是01。我们可以使用 help() 函数来查看这三个输入变量的名称。为了节省空间,这里仅显示前几行:

>>>help(sp.random.normal) 
Help on built-in function normal:
normal(...) 
normal(loc=0.0, scale=1.0, size=None)

从正态分布中抽取随机样本

正态分布的概率密度函数最早由德·莫伊夫(De Moivre)推导出来,200 年后由高斯(Gauss)和拉普拉斯(Laplace)独立推导完成,通常称为钟形曲线,因为它的特征形状;参见下图:

从正态分布中抽取随机样本

标准正态分布的密度函数如下所示:

从正态分布中抽取随机样本

这里,f(x) 是标准正态分布的密度函数,x 是输入值,e 是指数函数,π3.1415926。以下是生成上述钟形曲线的代码:

import scipy as sp
import scipy.stats as stats
import matplotlib.pyplot as plt
x = sp.arange(-3,3,0.01)
y=stats.norm.pdf(x)
plt.plot(x,y)
plt.title("A standard normal distribution")
plt.xlabel('x')
plt.ylabel('y')
plt.show()

使用种子生成随机数

很多时候,用户希望能够反复生成相同的一组随机数。例如,当教授在讲解如何估计一组随机数的均值、标准差、偏度和峰度时,学生能够生成与教授完全相同的数值是非常有帮助的。另一个例子是,当我们在调试 Python 程序以模拟股票走势时,我们可能更希望得到相同的中间结果。对于这种情况,我们可以使用scipy.random.seed()函数,如下所示:

>>>import scipy as sp 
>>>sp.random.seed(12345) 
>>>x=sp.random.normal(0,1,20) 
>>>print x[0:5] 
[-0.20470766 0.47894334 -0.51943872 -0.5557303 1.96578057] 
>>>

这里,12345是种子。种子的值并不重要,关键是相同的种子会产生相同的随机数值。更一般的正态分布公式如下:

使用种子生成随机数

这里,f(x)是正态分布的密度函数,x是输入值,e是指数函数,μ是均值,σ是标准差。

正态分布的随机数

要从正态分布中生成n个随机数,我们有以下代码:

>>>impimport scipy as sp 
>>>sp.random.seed(12345) 
>>>mean=0.05
>>>std=0.1
>>>n=50
>>>x=sp.random.normal(mean,std,n) 
>>>print(x[0:5])
[ 0.02952923 0.09789433 -0.00194387 -0.00557303 0.24657806]
>>>

这个程序与前一个程序的区别在于,均值是0.05而不是0,而标准差是0.1而不是1

正态分布的直方图

在分析数据集属性的过程中,直方图被广泛使用。为了为从具有指定均值和标准差的正态分布中抽取的一组随机值生成直方图,我们有以下代码:

import scipy as sp 
import matplotlib.pyplot as plt 
sp.random.seed(12345) 
mean=0.1
std=0.2
n=1000
x=sp.random.normal(mean,std,n) 
plt.hist(x, 15, normed=True) 
plt.title("Histogram for random numbers drawn from a normal distribution")
plt.annotate("mean="+str(mean),xy=(0.6,1.5))
plt.annotate("std="+str(std),xy=(0.6,1.4))
plt.show()

结果图形如下所示:

正态分布的直方图

对数正态分布的图形展示

当股票回报率遵循正态分布时,其价格应当遵循对数正态分布。对数正态分布的定义如下:

对数正态分布的图形展示

这里,f(x;μ,σ)是对数正态分布的密度,ln()是自然对数函数。以下代码展示了三个不同的对数正态分布,分别使用了三组参数,如(0, 0.25)、(0, 0.5)和(0, 1.0)。第一个参数是均值(μ),第二个参数是标准差,见以下代码:

import scipy as sp
import numpy as np
import matplotlib.pyplot as plt 
from scipy import sqrt,exp,log,pi
#
x=np.linspace(0.001,3,200)
mu=0 
sigma0=[0.25,0.5,1]
color=['blue','red','green'] 
target=[(1.2,1.3),(1.7,0.4),(0.18,0.7)]
start=[(1.8,1.4),(1.9,0.6),(0.18,1.6)]
#
for i in sp.arange(len(sigma0)):
    sigma=sigma0[i]
    y=1/(x*sigma*sqrt(2*pi))*exp(-(log(x)-mu)**2/(2*sigma*sigma))
    plt.annotate('mu='+str(mu)+', sigma='+str(sigma),xy=target[i],xytext=start[i],arrowprops=dict(facecolor=color[i],shrink=0.01),) 
    plt.plot(x,y,color[i])
    plt.title('Lognormal distribution') 
    plt.xlabel('x')
    plt.ylabel('lognormal density distribution') 
#
plt.show()

这里展示了图形。显然,与正态分布的密度不同,对数正态分布的密度函数是非对称的:

对数正态分布的图形展示

从均匀分布中生成随机数

当从n个可用的股票中随机选择 m 只股票时,我们可以从均匀分布中抽取一组随机数。为了从均匀分布中生成 10 个介于 1 和 100 之间的随机数,我们有以下代码。为了保证相同的数值集合,使用了seed()函数:

>>>import scipy as sp 
>>>sp.random.seed(123345) 
>>>x=sp.random.uniform(low=1,high=100,size=10) 

同样,low、high 和 size 是三个输入名称。第一个指定最小值,第二个指定最大值,而 size 指定我们打算生成的随机数的数量。前五个数字如下所示:

>>>print(x[0:5])
[ 30.32749021 20.58006409 2.43703988 76.15661293 75.06929084]
>>>

下一个程序随机掷一个骰子,结果为 1、2、3、4、5 或 6:

import random
def rollDice():
    roll = random.randint(1,6)
    return roll
i =1
n=10
result=[]
random.seed(123)
while i<n:
    result.append(rollDice())
    i+=1
print(result)
[1, 1, 3, 1, 6, 1, 4, 2, 6]

在前一个程序中,应用了random.seed()函数。因此,任何读者都应得到最后一行显示的相同结果。

使用模拟估算π值

通过模拟估算π值是一个很好的练习。我们来画一个边长为 2R 的正方形。如果在正方形内部放置一个最大的圆形,那么其半径将是 R,表示为以下方程:

使用模拟估算π值

另一方面,正方形的面积是其边长的平方:

使用模拟估算π值

方程 (4) 除以方程 (5),我们得到以下结果:

使用仿真估算π值

重新组织后,我们得到以下方程:

使用仿真估算π值

换句话说,π的值将是4 Scircle/Square。在运行仿真时,我们从一个均匀分布中生成nxy,范围为 0 到 0.5。然后我们估算一个距离,该距离是xy*的平方和的平方根,即!使用仿真估算π值。

显然,当d小于 0.5(R 值)时,它会落入圆内。我们可以想象扔一个飞镖,飞镖落入圆内时,π的值将呈现以下形式:

使用仿真估算π值

下图展示了这些随机点在圆内和方形内的分布:

使用仿真估算π值

估算π值的 Python 程序如下所示:

import scipy as sp 
n=100000
x=sp.random.uniform(low=0,high=1,size=n) 
y=sp.random.uniform(low=0,high=1,size=n) 
dist=sp.sqrt(x**2+y**2) 
in_circle=dist[dist<=1] 
our_pi=len(in_circle)*4./n
print ('pi=',our_pi)
print('error (%)=', (our_pi-sp.pi)/sp.pi)

每次运行前面的代码时,估算的π值会发生变化,如下所示,且其估算的准确性取决于试验次数,即n

('pi=', 3.14168)
('error (%)=', 2.7803225891524895e-05)

从泊松分布生成随机数

为了研究私人信息的影响,Easley、Kiefer、O'Hara 和 Paperman(1996 年)设计了一个信息化交易概率PIN)度量方法,基于买方发起交易的每日数量和卖方发起交易的数量推导而来。他们模型的基本假设是,订单到达遵循泊松分布。以下代码展示了如何从泊松分布中生成n个随机数:

import numpy as np
import scipy as sp 
import matplotlib.pyplot as plt 
x=sp.random.poisson(lam=1, size=100) 
#plt.plot(x,'o') 
a = 5\. # shape 
n = 1000 
s = np.random.power(a, n) 
count, bins, ignored = plt.hist(s, bins=30) 
x = np.linspace(0, 1, 100) 
y = a*x**(a-1.) 
normed_y = n*np.diff(bins)[0]*y 
plt.title("Poisson distribution")
plt.ylabel("y")
plt.xlabel("x")
plt.plot(x, normed_y) 
plt.show()

该图如下所示:

从泊松分布生成随机数

从 n 个给定股票中随机选择 m 只股票

基于前面的程序,我们可以轻松地从 500 只可用证券中选择 20 只股票。如果我们打算研究随机选择股票数量对投资组合波动性的影响,这将是一个重要的步骤,如下所示的代码所示:

import scipy as sp 
n_stocks_available=500 
n_stocks=20 
sp.random.seed(123345) 
x=sp.random.uniform(low=1,high=n_stocks_available,size=n_stocks)
y=[] 
for i in range(n_stocks): 
    y.append(int(x[i])) 
#print y 
final=sp.unique(y) 
print(final) 
print(len(final))
[  8  31  61  99 124 148 155 172 185 205 226 275 301 334 356 360 374 379
 401 449]
20

在前面的程序中,我们从 500 个数字中选择 20 个数字。由于我们必须选择整数,可能最终会得到少于 20 个值,也就是说,一些整数在将实数转换为整数后可能会出现重复。一种解决方法是选择更多的数字,然后取前 20 个整数。另一种方法是使用randrange()randint()函数。在下一个程序中,我们从所有可用的股票中选择n只股票。首先,我们从canisius.edu/~yany/python/yanMonthly.pkl下载数据集。假设数据集位于C:/temp/目录下:

import scipy as sp
import numpy as np
import pandas as pd
#
n_stocks=10 
x=pd.read_pickle('c:/temp/yanMonthly.pkl') 
x2=sp.unique(np.array(x.index)) 
x3=x2[x2<'ZZZZ']                        # remove all indices 
sp.random.seed(1234567) 
nonStocks=['GOLDPRICE','HML','SMB','Mkt_Rf','Rf','Russ3000E_D','US_DEBT','Russ3000E_X','US_GDP2009dollar','US_GDP2013dollar'] 
x4=list(x3) 
#
for i in range(len(nonStocks)): 
    x4.remove(nonStocks[i]) 
#
k=sp.random.uniform(low=1,high=len(x4),size=n_stocks) 
y,s=[],[] 
for i in range(n_stocks): 
    index=int(k[i]) 
    y.append(index) 
    s.append(x4[index]) 
#
final=sp.unique(y) 
print(final) 
print(s)

在前面的程序中,我们移除了非股票数据项。这些非股票项是数据项的一部分。首先,我们加载一个名为yanMonthly.pickle的数据集,其中包括 200 多只股票、黄金价格、GDP、失业率、小盘减大盘SMB)、高估减低估HML)、无风险利率、价格率、市场超额收益率以及罗素指数。

pandas 的一种输出格式是.pkl.png。由于x.index会呈现每个观测值的所有索引,我们需要使用unique()函数来选择所有唯一的 ID。因为我们只考虑股票来构建我们的投资组合,所以我们必须移除所有市场指数和其他非股票证券,如 HML 和US_DEBT。因为所有股票市场指数都以插入符号(^)开头,所以我们使用小于 ZZZZ 的方式来移除它们。对于其他在 A 和 Z 之间的 ID,我们必须逐个移除它们。为此,我们使用.remove()函数,该函数适用于列表变量。最终输出如下:

从 n 个给定股票中随机选择 m 个股票

有/没有替换

假设我们拥有一只股票的历史数据,如价格和回报。显然,我们可以估计它们的均值、标准差和其他相关统计数据。那么,明年的预期年均值和风险是多少呢?最简单的,可能也是天真的方法是使用历史均值和标准差。更好的方法是构建年回报和风险的分布。这意味着我们必须找到一种方法,利用历史数据更有效地预测未来。在这种情况下,我们可以应用自助法(bootstrapping)方法。例如,对于一只股票,我们拥有过去 20 年的月度回报数据,即 240 个观测值。

为了估计明年 12 个月的回报,我们需要构建回报分布。首先,我们从历史回报集中随机选择 12 个回报(不放回),并估计它们的均值和标准差。我们重复这个过程 5000 次。最终输出将是我们的回报-标准差分布。基于这个分布,我们也可以估计其他属性。同样地,我们也可以在有替换的情况下进行。NumPy 中一个有用的函数是numpy.random.permutation()。假设我们有从 1 到 10 的 10 个数字(包含 1 和 10)。我们可以调用numpy.random.permutation()函数来重新洗牌,如下所示:

import numpy as np 
x=range(1,11) 
print(x) 
for i in range(5):
    y=np.random.permutation(x) 
#
print(y)

这段代码的输出如下所示:

有/没有替换

基于numpy.random.permutation()函数,我们可以定义一个函数,输入三个变量:数据、我们计划从数据中随机选择的观测值数量,以及是否选择有或没有替换的自助法,如下代码所示:

import numpy as np 
def boots_f(data,n_obs,replacement=None):
    n=len(data) 
    if (n<n_obs):
        print "n is less than n_obs" 
    else: 
        if replacement==None:
            y=np.random.permutation(data) 
            return y[0:n_obs] 
        else:
            y=[] 
    #
    for i in range(n_obs): 
        k=np.random.permutation(data) 
        y.append(k[0]) 
    return y

在之前的程序中指定的约束条件是,给定的观察次数应大于我们计划选择的随机回报次数。这对于无替换的自助法是成立的。对于有替换的自助法,我们可以放宽这个约束;请参考相关习题。

年度回报分布

估算年化回报分布并将其表示为图形是一个很好的应用。为了使我们的练习更有意义,我们下载了微软的每日价格数据。然后,我们估算了其每日回报,并将其转换为年回报。基于这些年回报,我们通过应用带替换的自助法进行 5,000 次模拟,从而生成其分布,如下代码所示:

import numpy as np 
import scipy as sp
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
# Step 1: input area
ticker='MSFT'          # input value 1 
begdate=(1926,1,1)      # input value 2 
enddate=(2013,12,31)    # input value 3 
n_simulation=5000       # input value 4
# Step 2: retrieve price data and estimate log returns
x=getData(ticker,begdate,enddate,asobject=True)
logret = sp.log(x.aclose[1:]/x.aclose[:-1])
# Step 3: estimate annual returns 
date=[]
d0=x.date
for i in range(0,sp.size(logret)): 
    date.append(d0[i].strftime("%Y"))
y=pd.DataFrame(logret,date,columns=['logret']) 
ret_annual=sp.exp(y.groupby(y.index).sum())-1 
ret_annual.columns=['ret_annual']
n_obs=len(ret_annual)
# Step 4: estimate distribution with replacement 
sp.random.seed(123577) 
final=sp.zeros(n_obs,dtype=float)
for i in range(0,n_obs):
    x=sp.random.uniform(low=0,high=n_obs,size=n_obs) 
    y=[]
    for j in range(n_obs): 
        y.append(int(x[j]))
        z=np.array(ret_annual)[y] 
    final[i]=sp.mean(z)
# step 5: graph
plt.title('Mean return distribution: number of simulations ='+str(n_simulation))
plt.xlabel('Mean return')
plt.ylabel('Frequency')
mean_annual=round(np.mean(np.array(ret_annual)),4) 
plt.figtext(0.63,0.8,'mean annual='+str(mean_annual)) 
plt.hist(final, 50, normed=True)
plt.show()

相应的图表如下所示:

年度回报分布

股票价格波动模拟

我们在前面的章节中提到,在金融领域,回报假定服从正态分布,而价格则服从对数正态分布。股票在时间 t+1 的价格是时间 t 股票价格、均值、标准差和时间间隔的函数,如下公式所示:

股票价格波动模拟

在此公式中,St + 1 是时间 t+1 的股票价格,ˆ μ 是预期股票回报,t_ 是时间间隔(T t n_= ),T 是时间(以年为单位),n 是步数,ε 是均值为零的分布项,σ 是标的股票的波动率。通过简单的操作,方程(4)可以推导出以下我们将在程序中使用的方程:

股票价格波动模拟

在一个风险中性世界中,投资者不要求承担风险的补偿。换句话说,在这样的世界里,任何证券(投资)的预期回报率都是无风险利率。因此,在风险中性世界中,前面的方程式变为以下方程式:

股票价格波动模拟

如果你想了解更多关于风险中性概率的内容,请参考《期权、期货及其他衍生品》第七版,约翰·赫尔,皮尔森,2009 年。模拟股票价格运动(路径)的 Python 代码如下:

import scipy as sp 
import matplotlib.pyplot as plt
# input area
stock_price_today = 9.15 # stock price at time zero 
T =1\.                    # maturity date (in years) 
n_steps=100\.             # number of steps 
mu =0.15                 # expected annual return 
sigma = 0.2              # annualized volatility
sp.random.seed(12345)    # fixed our seed 
n_simulation = 5         # number of simulations 
dt =T/n_steps 
#
S = sp.zeros([n_steps], dtype=float) 
x = range(0, int(n_steps), 1) 
for j in range(0, n_simulation): 
    S[0]= stock_price_today 
    for i in x[:-1]: 
        e=sp.random.normal() 
        S[i+1]=S[i]+S[i]*(mu-0.5*pow(sigma,2))*dt+sigma*S[i]*sp.sqrt(dt)*e; 
    plt.plot(x, S)
#
plt.figtext(0.2,0.8,'S0='+str(S[0])+',mu='+str(mu)+',sigma='+str(sigma)) 
plt.figtext(0.2,0.76,'T='+str(T)+', steps='+str(int(n_steps))) 
plt.title('Stock price (number of simulations = %d ' % n_simulation +')') 
plt.xlabel('Total number of steps ='+str(int(n_steps))) 
plt.ylabel('stock price') 
plt.show()

为了使我们的图表更具可读性,我们故意只选择了五次模拟。由于应用了scipy.random.seed()函数,你可以通过运行之前的代码来复制以下图表。图表如下所示:

股票价格波动模拟

期权到期日股票价格的图形展示

到目前为止,我们已经讨论了期权实际上是路径无关的,这意味着期权价格取决于终值。因此,在定价此类期权之前,我们需要知道终期股票价格。为了扩展之前的程序,我们有以下代码来估算一组给定值的终期股票价格:S0(初始股票价格),n_simulation(终期价格的数量),T(到期日,按年计算),n_steps(步骤数),mu(预期年股票回报率),sigma(波动率):

import scipy as sp 
import matplotlib.pyplot as plt
from scipy import zeros, sqrt, shape 
#input area
S0 = 9.15               # stock price at time zero 
T =1\.                   # years
n_steps=100\.            # number of steps 
mu =0.15                # expected annual return 
sigma = 0.2             # volatility (annual) 
sp.random.seed(12345)   # fix those random numbers 
n_simulation = 1000     # number of simulation 
dt =T/n_steps 
#
S = zeros([n_simulation], dtype=float) 
x = range(0, int(n_steps), 1) 
for j in range(0, n_simulation): 
    tt=S0 
    for i in x[:-1]: 
        e=sp.random.normal() 
        tt+=tt*(mu-0.5*pow(sigma,2))*dt+sigma*tt*sqrt(dt)*e; 
        S[j]=tt 
#
plt.title('Histogram of terminal price') 
plt.ylabel('Number of frequencies') 
plt.xlabel('Terminal price') 
plt.figtext(0.5,0.8,'S0='+str(S0)+',mu='+str(mu)+',sigma='+str(sigma)) 
plt.figtext(0.5,0.76,'T='+str(T)+', steps='+str(int(n_steps))) 
plt.figtext(0.5,0.72,'Number of terminal prices='+str(int(n_simulation))) 
plt.hist(S) 
plt.show()

我们模拟的终期价格的直方图如下所示:

期权到期日股票价格的图示

正如我们在第九章中提到的,投资组合理论,为了生成两个相关的随机时间序列,需要进行两个步骤:生成两个零相关的随机时间序列x1x2;然后应用以下公式:

期权到期日股票价格的图示

这里,ρ是这两个时间序列之间的预定相关性。现在,y1y2与预定的相关性相关。以下 Python 程序将实现上述方法:

import scipy as sp
sp.random.seed(123)
n=1000
rho=0.3
x1=sp.random.normal(size=n)
x2=sp.random.normal(size=n)
y1=x1
y2=rho*x1+sp.sqrt(1-rho**2)*x2
print(sp.corrcoef(y1,y2))
[[ 1\.          0.28505213]
 [ 0.28505213  1\.        ]]

使用模拟复制布莱克-斯科尔斯-默顿看涨期权

在知道终期价格后,如果给定行权价格,我们可以估算看涨期权的支付。使用无风险利率作为贴现率计算的这些贴现支付的均值将是我们的看涨期权价格。以下代码帮助我们估算看涨期权价格:

import scipy as sp 
from scipy import zeros, sqrt, shape 
#
S0 = 40\.              # stock price at time zero 
X= 40\.                # exercise price 
T =0.5                # years 
r =0.05               # risk-free rate 
sigma = 0.2           # annualized volatility 
n_steps=100          # number of steps 
#
sp.random.seed(12345) # fix those random numbers 
n_simulation = 5000   # number of simulation 
dt =T/n_steps 
call = sp.zeros([n_simulation], dtype=float) 
x = range(0, int(n_steps), 1) 
for j in range(0, n_simulation): 
    sT=S0 
    for i in x[:-1]: 
        e=sp.random.normal() 
        sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sqrt(dt)) 
        call[j]=max(sT-X,0) 
#
call_price=sp.mean(call)*sp.exp(-r*T) 
print('call price = ', round(call_price,3))

估算的看涨期权价格为$2.748。相同的逻辑适用于定价看跌期权。

异型期权 #1 – 使用蒙特卡洛模拟来定价平均值

到目前为止,我们在第九章中已经讨论了欧式期权和美式期权,投资组合理论。布莱克-斯科尔斯-默顿期权模型,也叫做普通期权。其特征之一是路径无关。另一方面,异型期权更复杂,因为它们可能有多个触发因素与其支付的确定相关。例如,一家炼油厂担心未来三个月内原油价格的波动。它们计划对潜在的原油价格跳跃进行对冲。公司可以购买看涨期权。然而,由于公司每天消耗大量的原油,自然更关心的是平均价格,而不仅仅是普通看涨期权所依赖的终期价格。对于这种情况,平均期权会更有效。平均期权是一种亚洲期权。对于平均期权,其支付是由一段预设时间内的基础价格的平均值决定的。平均值有两种类型:算术平均和几何平均。亚洲看涨期权(平均价格)的支付函数如下所示:

另类期权 #1 – 使用蒙特卡罗模拟法定价平均价格

这里给出了亚洲看跌期权(平均价格)的支付函数:

另类期权 #1 – 使用蒙特卡罗模拟法定价平均价格

亚洲期权是另类期权的一种基本形式。亚洲期权的另一个优势是,与欧洲和美式普通期权相比,它们的成本更低,因为平均值的波动要比终值的波动小得多。下面的 Python 程序是针对具有算术平均价格的亚洲期权的:

import scipy as sp
s0=40\.                 # today stock price 
x=40\.                  # exercise price 
T=0.5                  # maturity in years 
r=0.05                 # risk-free rate 
sigma=0.2              # volatility (annualized) 
sp.random.seed(123)    # fix a seed here 
n_simulation=100       # number of simulations 
n_steps=100\.           # number of steps
#	
dt=T/n_steps 
call=sp.zeros([n_simulation], dtype=float) 
for j in range(0, n_simulation): 
    sT=s0 
    total=0 
    for i in range(0,int(n_steps)): 
         e=sp.random.normal()
         sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sp.sqrt(dt)) 
         total+=sT 
         price_average=total/n_steps 
    call[j]=max(price_average-x,0) 
#
call_price=sp.mean(call)*sp.exp(-r*T) 
print('call price based on average price = ', round(call_price,3))
('call price based on average price = ', 1.699)

根据上述结果,这个平均价格看涨期权的权利金为$1.70。

另类期权 #2 – 使用蒙特卡罗模拟法定价障碍期权

与黑-肖尔斯-莫顿期权模型中的看涨和看跌期权不同,后者是路径无关的,障碍期权则是路径相关的。障碍期权在许多方面与普通期权类似,但存在触发条件。敲入期权的生命周期从无价值开始,除非标的股票达到预定的敲入障碍。而相反,敲出障碍期权从一开始就有效,只有在价格突破敲出障碍时才会变得无效。此外,如果障碍期权到期时没有激活,它可能变得一文不值,或者可能支付一部分权利金作为现金返还。四种障碍期权如下所示:

  • 向上敲出期权:在这种障碍期权中,价格从较低的障碍水平开始。如果它达到障碍,则被“敲出”。

  • 跌破障碍期权:在这种障碍期权中,价格从一个较高的障碍开始。如果价格达到障碍,则被“敲出”。

  • 向上敲入期权:在这种障碍期权中,价格从较低的障碍开始,必须达到该障碍才能激活。

  • 向下敲入期权:在这种障碍期权中,价格从较高的障碍开始,必须达到该障碍才能激活。

接下来的 Python 程序用于一个向上敲出障碍期权,类型为欧洲看涨期权:

import scipy as sp 
from scipy import log,exp,sqrt,stats 
#
def bsCall(S,X,T,r,sigma):
    d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T)) 
    d2 = d1-sigma*sqrt(T)
    return S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2)
#
def up_and_out_call(s0,x,T,r,sigma,n_simulation,barrier):
    n_steps=100\. 
    dt=T/n_steps 
    total=0 
    for j in sp.arange(0, n_simulation): 
        sT=s0 
        out=False
        for i in range(0,int(n_steps)): 
            e=sp.random.normal() 
            sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sp.sqrt(dt)) 
            if sT>barrier: 
               out=True 
        if out==False: 
            total+=bsCall(s0,x,T,r,sigma) 
    return total/n_simulation

基本设计是我们模拟股票价格波动n次,例如模拟 100 次。对于每次模拟,我们有 100 个步骤。每当股票价格达到障碍时,支付将为零。否则,支付将为一种普通的欧洲看涨期权。最终价值将是所有未被敲出的看涨期权价格的总和,再除以模拟次数,如下所示的代码:

s0=40\.              # today stock price 
x=40\.               # exercise price 
barrier=42          # barrier level 
T=0.5               # maturity in years 
r=0.05              # risk-free rate 
sigma=0.2           # volatility (annualized) 
n_simulation=100    # number of simulations 
sp.random.seed(12)  # fix a seed
#
result=up_and_out_call(s0,x,T,r,sigma,n_simulation,barrier) 
print('up-and-out-call = ', round(result,3))
('up-and-out-call = ', 0.937)

根据上述结果,我们知道这个向上敲出看涨期权的价格为$0.94。

喜欢使用模拟法计算 VaR 的两种方法

在上一章,第十一章,风险价值中,我们学习了可以使用两种方法来估算个股或投资组合的 VaR:这取决于正态性假设和基于历史回报排序的方法。蒙特卡罗模拟法能够将这两种方法结合起来,见下述代码:

import numpy as np
import numpy as np
import scipy as sp
import pandas as pd
from scipy.stats import norm
#
position=1e6              # portfolio value
std=0.2                   # volatility
mean=0.08                 # mean return
confidence=0.99           # confidence level
nSimulations=50000        # number of simulations
# Method I
z=norm.ppf(1-confidence)
VaR=position*(mean+z*std)
print("Holding=",position, "VaR=", round(VaR,2), "tomorrow")
#
# Method II: Monte Carlo simulaiton 
sp.random.seed(12345) 
ret2=sp.random.normal(mean,std,nSimulations) 
ret3=np.sort(ret2) 
m=int(nSimulations*(1-confidence))
VaR2=position*(ret3[m])
print("Holding=",position, "VaR2=", round(VaR2,2), "tomorrow")
('Holding=', 1000000.0, 'VaR=', -385270.0, 'tomorrow')
('Holding=', 1000000.0, 'VaR2=', -386113.0, 'tomorrow')

蒙特卡洛模拟的结果为$386,113,而基于公式计算的结果为$385,270(假设投资组合当前价值为 100 万美元)。

使用蒙特卡洛模拟进行资本预算

正如我们在本章开头提到的,当变量的数量有许多不同的值时,我们可以使用蒙特卡洛模拟来进行资本预算。我们的目标是通过对所有未来自由现金流进行折现,来估算给定预算的净现值(NPV):

使用蒙特卡洛模拟进行资本预算

这里,NPV 是某一提案的净现值,FCF0 将是零时点的自由现金流,FCFt 将是第 I 年末的自由现金流,R 是折现率。计算第 t 年末自由现金流的公式如下:

使用蒙特卡洛模拟进行资本预算

这里,FCTt 是第 t 年的自由现金流,Dt 是第 t 年的折旧,CaptExt 是第 t 年的净资本支出,NWC 是净营运资金,即流动资产减去流动负债,Δ表示变化。让我们看一个简单的例子。假设公司购买一项长期等效资产,总成本为 50 万美元,使用年限为五年:

项目 0 1 2 3 4 5
价格 0 28 28 28 28 28
单位 0 100000 100000 100000 100000 100000
销售 0 2800000 2800000 2800000 2800000 2800000
销售成本 0 840000 840000 840000 840000 840000
其他成本 0 100000 100000 100000 100000 100000
销售、一般管理和行政费用 15000 15000 15000 15000 15000 15000
研发费用 20000
折旧 1000000 1000000 1000000 1000000 1000000
息税前利润(EBIT) -35000 845000 845000 845000 845000 845000
税率 35% -12250 295750 295750 295750 295750 295750
净收入(NI) -47250 1140750 1140750 1140750 1140750 1140750
加上折旧 -47250 2140750 2140750 2140750 2140750 2140750

表 12.1 每年的现金流

我们有以下等效代码:

import scipy as sp
nYear=5                 # number of years
costEquipment=5e6       # 5 million 
n=nYear+1               # add year zero
price=28                # price of the product
units=100000            # estimate number of units sold 
otherCost=100000        # other costs
sellingCost=1500        # selling and administration cost 
R_and_D=200000          # Research and development
costRawMaterials=0.3    # percentage cost of raw materials
R=0.15                  # discount rate
tax=0.38                # corporate tax rate
#
sales=sp.ones(n)*price*units
sales[0]=0              # sales for 1st year is zero
cost1=costRawMaterials*sales
cost2=sp.ones(n)*otherCost
cost3=sp.ones(n)*sellingCost
cost4=sp.zeros(n)
cost4[0]=costEquipment
RD=sp.zeros(n)
RD[0]=R_and_D                     # assume R&D at time zero
D=sp.ones(n)*costEquipment/nYear  # straight line depreciation 
D[0]=0                            # no depreciation at time 0
EBIT=sales-cost1-cost2-cost3-cost4-RD-D
NI=EBIT*(1-tax)
FCF=NI+D                         # add back depreciation
npvProject=sp.npv(R,FCF)         # estimate NPV
print("NPV of project=",round(npvProject,0))
('NPV of project=', 1849477.0)

该项目的净现值(NPV)为$1,848,477。由于它是正值,如果我们的标准是基于 NPV 规则,那么我们应当接受该提案。现在,让我们加入一些不确定性。假设我们有三个不确定因素:价格、预计销售的产品单位数以及折现率,请参见以下代码:

import scipy as sp
import matplotlib.pyplot as plt
nYear=5                 # number of years
costEquipment=5e6       # 5 million 
n=nYear+1               # add year zero
otherCost=100000        # other costs
sellingCost=1500        # selling and administration cost 
R_and_D=200000          # Research and development
costRawMaterials=0.3    # percentage cost of raw materials
tax=0.38                # corporate tax rate
thousand=1e3            # unit of thousand 
million=1e6             # unit of million 
#
# three uncertainties: price, unit and discount rate
nSimulation=100         # number of simulation
lowPrice=10             # low price
highPrice=30            # high price
lowUnit=50*thousand     # low units expected to sell 
highUnit=200*thousand   # high units expected to sell 
lowRate=0.15            # lower discount rate
highRate=0.25           # high discount rate 
#
n2=nSimulation
sp.random.seed(123)
price0=sp.random.uniform(low=lowPrice,high=highPrice,size=n2)
units0=sp.random.uniform(low=lowUnit,high=highUnit,size=n2)
R0=sp.random.uniform(lowRate,highRate,size=n2)
#
npv=[]
for i in sp.arange(nSimulation):
    units=sp.ones(n)*units0[i]
    price=price0[i]
    R=R0[i]
    sales=units*price
    sales[0]=0              # sales for 1st year is zero
    cost1=costRawMaterials*sales
    cost2=sp.ones(n)*otherCost
    cost3=sp.ones(n)*sellingCost
    cost4=sp.zeros(n)
    cost4[0]=costEquipment
    RD=sp.zeros(n)
    RD[0]=R_and_D                     # assume R&D at time zero
    D=sp.ones(n)*costEquipment/nYear  # straight line depreciation 
    D[0]=0                            # no depreciation at time 0
    EBIT=sales-cost1-cost2-cost3-cost4-RD-D
    NI=EBIT*(1-tax)
    FCF=NI+D                          # add back depreciation
    npvProject=sp.npv(R,FCF)/million  # estimate NPV
    npv.append(npvProject)
print("mean NPV of project=",round(sp.mean(npv),0))
print("min  NPV of project=",round(min(npv),0))
print("max  NPV of project=",round(max(npv),0))
plt.title("NPV of the project: 3 uncertainties")
plt.xlabel("NPV (in million)")
plt.hist(npv, 50, range=[-3, 6], facecolor='blue', align='mid')
plt.show()

NPV 分布的直方图如下所示:

使用蒙特卡洛模拟进行资本预算

Python SimPy 模块

SimPy 是一个基于标准 Python 的基于过程的离散事件仿真框架。它的事件调度器基于 Python 的生成器,也可以用于异步网络通信或实现多智能体系统(包括模拟和真实通信)。SimPy 中的进程是简单的 Python 生成器函数,用于建模活跃的组件,如顾客、车辆或代理。SimPy 还提供了各种类型的共享资源,用于建模有限容量的拥堵点(如服务器、结账柜台和隧道)。从 3.1 版本开始,它还将提供监控功能,帮助收集有关资源和进程的统计数据:

import simpy
def clock(env, name, tick):
     while True:
         print(name, env.now)
         yield env.timeout(tick)
#
env = simpy.Environment()
env.process(clock(env, 'fast', 0.5))
env.process(clock(env, 'slow', 1))
env.run(until=2)
('fast', 0)
('slow', 0)
('fast', 0.5)
('slow', 1)
('fast', 1.0)
('fast', 1.5)

两种社会政策的比较——基本收入与基本工作

这个例子借鉴自 Stucchhio(2013)。在过去几十年的发展中,各国的财富持续积累,尤其是在发达国家中尤为明显。支持公平的基本论点之一是每个公民都应拥有基本的生活标准。基于这一论点,许多国家为其公民提供了大量福利,例如普及医疗、免费教育等。一项政策建议是基本收入,即每个公民每年都会获得一笔无附加条件的基本收入。例如,如果我们假设基本时薪为 7.50 美元,每周工作 40 小时,每年工作 50 周,那么基本收入应该为 15,000 美元。Zhong(2017)报告称,印度正在考虑通过普遍基本收入计划来对抗贫困。显而易见的优势是行政成本会相当低。此外,腐败侵占政府为穷人发放的资金的可能性也较小。2017 年,芬兰启动了一个试点项目,加拿大和荷兰的地方当局也宣布了相关实验。2016 年,瑞士选民拒绝了一项最低收入提案。

一种替代方案是所谓的基本工作,在这种工作中,政府保证为无法找到体面工作的人提供一份低薪工作。这些方法各有优缺点。基于一组假设,如时薪、每周工作小时数、每年工作周数、人口、劳动力等,Stucchhio(2013)比较了这两种提案的成本和收益。存在若干不确定性;请参阅下表中的列表:

政策 命令 描述
基本收入 unitAdmCost = norm(250,75) 每个人的行政费用
binom(nNonWorkers,tiny).rvs() 从二项分布中随机生成一个数字
nonWorkerMultiplier = uniform(-0.10, 0.15).rvs() 非劳动者的乘数
基本工作 unitAdmCost4disabled= norm(500,150).rvs() 每个残疾成年人所需的行政费用
unitAdmCost4worker = norm(5000, 1500).rvs() 每个工人的行政费用
nonWorkerMultiplier = uniform(-0.20, 0.25).rvs() 非工作者的乘数
hourlyProductivity = uniform(0.0,hourlyPay).rvs() 每小时生产力

表 12.2:两种提案的成本与收益

程序使用三种分布:正态分布、均匀分布和二项分布。uniform(a,b).rvs()命令生成一个在ab之间均匀分布的随机数。norm(mean,std).rvs()命令生成一个来自具有指定均值和标准差的正态分布的随机数。binom(n,k).rvs()命令生成一个来自二项分布的随机数,输入值为nk

import scipy as sp
import scipy.stats as stats
sp.random.seed(123)
u=stats.uniform(-1,1).rvs()
n=stats.norm(500,150).rvs()
b=stats.binom(10000,0.1).rvs()
x='random number from a '
print(x+"uniform distribution ",u)
print(x+" normal distribution ",n)
print(x+" binomial distribution ",b)
('random number from a uniform distribution ', -0.30353081440213836)
('random number from a  normal distribution ', 357.18541897080166)
('random number from a  binomial distribution', 1003)

Stucchhio 的 Python 程序,经过少许修改,显示如下:

from pylab import *
from scipy.stats import *
#input area
million=1e6                        # unit of million 
billion=1e9                        # unit of billion 
trillion=1e12                      # unit of trillion 
tiny=1e-7                          # a small number 
hourlyPay = 7.5                    # hourly wage
workingHoursPerWeek=40             # working hour per week                                
workingWeeksPerYear=50             # working weeks per year
nAdult           = 227*million     # number of adult
laborForce       = 154*million     # labor force
disabledAdults   =  21*million     # disability 
nSimulations     = 1024*32         # number of simulations 
#
basicIncome = hourlyPay*workingHoursPerWeek*workingWeeksPerYear
# define a few function
def geniusEffect(nNonWorkers):
    nGenious = binom(nNonWorkers,tiny).rvs()
    return nGenious* billion
#
def costBasicIncome():
    salaryCost= nAdult * basicIncome
    unitAdmCost = norm(250,75)
    nonWorkerMultiplier = uniform(-0.10, 0.15).rvs()
    nonWorker0=nAdult-laborForce-disabledAdults
    nNonWorker = nonWorker0*(1+nonWorkerMultiplier)
    marginalWorkerHourlyProductivity = norm(10,1)
    admCost = nAdult * unitAdmCost.rvs()
    unitBenefitNonWorker=40*52*marginalWorkerHourlyProductivity.rvs()
    benefitNonWorkers = 1 * (nNonWorker*unitBenefitNonWorker)
    geniusBenefit=geniusEffect(nNonWorker)
    totalCost=salaryCost + admCost - benefitNonWorkers-geniusBenefit
    return totalCost
#
def costBasicJob():
    unitAdmCost4disabled= norm(500,150).rvs()
    unitAdmCost4worker = norm(5000, 1500).rvs()
    nonWorkerMultiplier = uniform(-0.20, 0.25).rvs()
    hourlyProductivity = uniform(0.0, hourlyPay).rvs()
    cost4disabled=disabledAdults * (basicIncome + unitAdmCost4disabled)
    nBasicWorkers=((nAdult-disabledAdults-laborForce)*(1+nonWorkerMultiplier))
    annualCost=workingHoursPerWeek*workingWeeksPerYear*hourlyProductivity
    cost4workers=nBasicWorkers * (basicIncome+unitAdmCost4worker-annualCost)
    return cost4disabled + cost4workers
#
N = nSimulations
costBI = zeros(shape=(N,),dtype=float)
costBJ = zeros(shape=(N,),dtype=float)
for k in range(N):
    costBI[k] = costBasicIncome()
    costBJ[k] = costBasicJob()
#
def myPlot(data,myTitle,key):
    subplot(key)
    width = 4e12
    height=50*N/1024
    title(myTitle)
    #xlabel("Cost (Trillion = 1e12)")
    hist(data, bins=50)
    axis([0,width,0,height])
#
myPlot(costBI,"Basic Income",211)
myPlot(costBJ,"Basic Job",212)
show()

根据这里展示的图表,他得出结论,基本工作提案的成本低于基本收入提案。为了节省篇幅,我们不再详细阐述该程序。更多详细解释及相关假设,请参阅 Stucchhio(2013)发布的博客:

两种社会政策比较 – 基本收入与基本工作

通过模拟找到基于两只股票的有效前沿

以下程序旨在基于已知均值、标准差和相关性的两只股票生成有效前沿。我们只有六个输入值:两个均值,两个标准差,相关性(ρ)和模拟次数。为了生成相关的* y1 y2 时间序列,我们首先生成不相关的 x1 x2 *序列。然后,我们应用以下公式:

通过模拟找到基于两只股票的有效前沿

另一个重要的问题是如何构造一个目标函数来最小化。我们的目标函数是投资组合的标准差,外加一个惩罚项,该惩罚项定义为与目标投资组合均值的绝对偏差的缩放。

换句话说,我们最小化投资组合的风险以及投资组合回报与目标回报之间的偏差,代码如下所示:

import numpy as np 
import scipy as sp 
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime as dt 
from scipy.optimize import minimize
#
# Step 1: input area
mean_0=(0.15,0.25)   # mean returns for 2 stocks
std_0= (0.10,0.20)   # standard deviations for 2 stocks 
corr_=0.2       # correlation between 2 stocks
nSimulations=1000    # number of simulations 
#
# Step 2: Generate two uncorrelated time series 
n_stock=len(mean_0)
n=nSimulations
sp.random.seed(12345) # to get the same random numbers 
x1=sp.random.normal(loc=mean_0[0],scale=std_0[0],size=n) 
x2=sp.random.normal(loc=mean_0[1],scale=std_0[1],size=n) 
if(any(x1)<=-1.0 or any(x2)<=-1.0):
    print ('Error: return is <=-100%')
#
# Step 3: Generate two correlated time series 
index_=pd.date_range(start=dt(2001,1,1),periods=n,freq='d') 
y1=pd.DataFrame(x1,index=index_) 
y2=pd.DataFrame(corr_*x1+sp.sqrt(1-corr_**2)*x2,index=index_)
#
# step 4: generate a return matrix called R 
R0=pd.merge(y1,y2,left_index=True,right_index=True) 
R=np.array(R0)
#
# Step 5: define a few functions 
def objFunction(W, R, target_ret):
    stock_mean=np.mean(R,axis=0) 
    port_mean=np.dot(W,stock_mean)            # portfolio mean
    cov=np.cov(R.T)                           # var-covar matrix 
    port_var=np.dot(np.dot(W,cov),W.T)        # portfolio variance 
    penalty = 2000*abs(port_mean-target_ret)  # penalty 4 deviation
    return np.sqrt(port_var) + penalty        # objective function
#
# Step 6: estimate optimal portfolio for a given return 
out_mean,out_std,out_weight=[],[],[] 
stockMean=np.mean(R,axis=0)
#
for r in np.linspace(np.min(stockMean),np.max(stockMean),num=100): 
    W = sp.ones([n_stock])/n_stock             # start equal w
    b_ = [(0,1) for i in range(n_stock)]       # bounds
    c_ = ({'type':'eq', 'fun': lambda W: sum(W)-1\. })# constraint 
    result=minimize(objFunction,W,(R,r),method='SLSQP',constraints=c_,bounds=b_)
    if not result.success:                     # handle error 
        raise BaseException(result.message)
    out_mean.append(round(r,4))                # decimal places
    std_=round(np.std(np.sum(R*result.x,axis=1)),6) 
    out_std.append(std_) 
    out_weight.append(result.x)
#
# Step 7: plot the efficient frontier
plt.title('Simulation for an Efficient Frontier from given 2 stocks') 
plt.xlabel('Standard Deviation of the 2-stock Portfolio (Risk)') 
plt.ylabel('Return of the 2-stock portfolio')
plt.figtext(0.2,0.80,' mean = '+str(stockMean)) 
plt.figtext(0.2,0.75,' std  ='+str(std_0)) 
plt.figtext(0.2,0.70,' correlation ='+str(corr_))
plt.plot(np.array(std_0),np.array(stockMean),'o',markersize=8) 
plt.plot(out_std,out_mean,'--',linewidth=3)
plt.show()

输出如下所示:

通过模拟找到基于两只股票的有效前沿

构建带有 n 只股票的有效前沿

当股票数量* n 增加时,每对股票之间的相关性会显著增加。对于 n 只股票,我们有 n(n-1)/2 个相关性。例如,如果* n 是 10,则我们有 45 个相关性。由于这个原因,手动输入这些值并不是一个好主意。相反,我们通过从多个均匀分布中抽取随机数来生成均值、标准差和相关性。为了产生相关的回报,首先我们生成 n *个不相关的股票回报时间序列,然后应用 Cholesky 分解,具体如下:

import numpy as np
import scipy as sp
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime as dt
from scipy.optimize import minimize
#
# Step 1: input area
nStocks=20
sp.random.seed(1234)                        # produce the same random numbers 
n_corr=nStocks*(nStocks-1)/2                # number of correlation 
corr_0=sp.random.uniform(0.05,0.25,n_corr)  # generate correlations 
mean_0=sp.random.uniform(-0.1,0.25,nStocks) # means
std_0=sp.random.uniform(0.05,0.35,nStocks)  # standard deviation 
nSimulations=1000                           # number of simulations 
#
# Step 2: produce correlation matrix: Cholesky decomposition
corr_=sp.zeros((nStocks,nStocks))
for i in range(nStocks):
    for j in range(nStocks):
        if i==j:
            corr_[i,j]=1
        else:
            corr_[i,j]=corr_0[i+j]
U=np.linalg.cholesky(corr_)
#
# Step 3: Generate two uncorrelated time series 
R0=np.zeros((nSimulations,nStocks))
for i in range(nSimulations):
    for j in range(nStocks):
        R0[i,j]=sp.random.normal(loc=mean_0[j],scale=std_0[j],size=1)
if(R0.any()<=-1.0):
    print ('Error: return is <=-100%')
#
# Step 4: generate correlated return matrix: Cholesky     
R=np.dot(R0,U)
R=np.array(R)
#
# Step 5: define a few functions
def objFunction(W, R, target_ret): 
    stock_mean=np.mean(R,axis=0)  
    port_mean=np.dot(W,stock_mean)           # portfolio mean
    cov=np.cov(R.T)                          # var-covar matrix
    port_var=np.dot(np.dot(W,cov),W.T)       # portfolio variance
    penalty = 2000*abs(port_mean-target_ret) # penalty 4 deviation 
    return np.sqrt(port_var) + penalty       # objective function 
#
# Step 6: estimate optimal portfolo for a given return 
out_mean,out_std,out_weight=[],[],[] 
stockMean=np.mean(R,axis=0)    
#
for r in np.linspace(np.min(stockMean), np.max(stockMean), num=100):
    W = sp.ones([nStocks])/nStocks             # starting:equal w 
    b_ = [(0,1) for i in range(nStocks)]       # bounds
    c_ = ({'type':'eq', 'fun': lambda W: sum(W)-1\. })# constraint
    result=minimize(objFunction,W,(R,r),method='SLSQP',constraints=c_, bounds=b_)    
    if not result.success:                    # handle error
        raise BaseException(result.message) 
    out_mean.append(round(r,4))               # a few decimal places
    std_=round(np.std(np.sum(R*result.x,axis=1)),6)
    out_std.append(std_)
    out_weight.append(result.x) 
#
# Step 7: plot the efficient frontier
plt.title('Simulation for an Efficient Frontier: '+str(nStocks)+' stocks')
plt.xlabel('Standard Deviation of the Porfolio')
plt.ylabel('Return of the2-stock portfolio')
plt.plot(out_std,out_mean,'--',linewidth=3)
plt.show()

图表如下所示:

构建带有 n 只股票的有效前沿

当 n 是一个很大的数字时,模拟一个 n 股票组合是困难的。原因在于生成方差-协方差矩阵非常耗时,请看这里的协方差(相关性)数量:

构建包含 n 只股票的有效前沿

假设我们的投资组合中有 500 只股票。那么我们需要估算 124,750 对相关性。为了简化这个计算,我们可以应用资本资产定价模型(CAPM),见下式:

构建包含 n 只股票的有效前沿

这里 R[i,t] 是股票 i 在时间 t 的收益,α[i]β[i] 分别是股票 i 的截距和斜率,R[M,t] 是市场指数在时间 t 的收益,e[i], t 是时间 t 的误差项。由于单只股票的总风险包含两个成分:系统性风险和公司特定风险。因此,股票 i 的方差与市场指数的关系如下所示:

构建包含 n 只股票的有效前沿

股票 ij 之间的协方差如下所示:

构建包含 n 只股票的有效前沿

因此,我们可以将估算从 124,750 减少到仅 1,000。首先估算 500 个 β 值。然后我们应用之前的公式来估算协方差。类似地,估算股票 ij 之间的相关性的公式如下:

构建包含 n 只股票的有效前沿

长期收益预测

许多研究人员和实践者认为,如果基于过去收益的算术平均数来进行长期收益预测,会导致过高的估计;而如果基于几何平均数,则会导致过低的估计。Jacquier、Kane 和 Marcus(2003)建议使用以下加权方案,利用 80 年的历史收益来预测未来 25 年的收益:

长期收益预测

以下程序反映了上述方程:

import numpy as np
import pandas as pd
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
#
# input area
ticker='IBM'           # input value 1 
begdate=(1926,1,1)     # input value 2 
enddate=(2013,12,31)   # input value 3 
n_forecast=25          # input value 4
#
def geomean_ret(returns): 
    product = 1
    for ret in returns: 
        product *= (1+ret)
    return product ** (1.0/len(returns))-1
#
x=getData(ticker,begdate,enddate,asobject=True, adjusted=True)
logret = np.log(x.aclose[1:]/x.aclose[:-1]) 
date=[]
d0=x.date
for i in range(0,np.size(logret)):
    date.append(d0[i].strftime("%Y"))
#
y=pd.DataFrame(logret,date,columns=['logret'],dtype=float)
ret_annual=np.exp(y.groupby(y.index).sum())-1 
ret_annual.columns=['ret_annual']
n_history=len(ret_annual) 
a_mean=np.mean(np.array(ret_annual))
g_mean=geomean_ret(np.array(ret_annual))
w=n_forecast/n_history
future_ret=w*g_mean+(1-w)*a_mean
print('Arithmetric mean=',round(a_mean,3), 'Geomean=',round(g_mean,3),'forecast=',future_ret)

输出结果如下所示:

长期收益预测

效率、拟蒙特卡洛法和 Sobol 序列

当使用蒙特卡洛模拟来解决各种金融相关问题时,会生成一组随机数。当精度要求非常高时,我们必须生成大量的随机数。例如,在定价期权时,我们使用非常小的间隔或大量的步长来提高解决方案的精度。因此,蒙特卡洛模拟的效率在计算时间和成本方面至关重要。如果需要定价几千个期权,这一点尤其重要。一种提高效率的方法是应用更好的算法,即优化我们的代码。另一种方法是使用一些分布更均匀的特殊类型的随机数,这被称为准蒙特卡洛模拟。一个典型的例子是所谓的 Sobol 序列。Sobol 序列属于低差异序列,满足随机数的属性,但分布更加均匀:

import numpy as np
import matplotlib.pyplot as plt
np.random.seed(12345)
n=200
a = np.random.uniform(size=(n*2))
plt.scatter(a[:n], a[n:])
plt.show()

相关图表显示在左侧面板:

效率、准蒙特卡洛、和 Sobol 序列

另一方面,如果我们使用 Sobol 序列,这些随机数的分布会更加均匀;请参见前面的右侧面板。相关代码如下:

import sobol_seq
import scipy as sp
import matplotlib.pyplot as plt
a=[]
n=100
for i in sp.arange(2*n):
     t=sobol_seq.i4_sobol(1,i)
     a.append(t)
print(a[0:10])
x=sp.random.permutation(a[:n])
y=sp.random.permutation(a[n:])
plt.scatter(x,y,edgecolors='r')
plt.show()
[[array([ 0.]), 1], [array([ 0.5]), 2], [array([ 0.75]), 3], [array([ 0.25]), 4], [array([ 0.375]), 5], [array([ 0.875]), 6], [array([ 0.625]), 7], [array([ 0.125]), 8], [array([ 0.1875]), 9], [array([ 0.6875]), 10]]
>>>

对于一个类似的例子,但使用更复杂的 Python 代码,请参见 betatim.github.io/posts/quasi-random-numbers/.

附录 A – 数据案例#8 - 蒙特卡洛模拟与二十一点

二十一点是一种双人游戏,包括发牌员和玩家。这里,我们假设你是玩家。

规则#1:2 到 10 的牌按面值计分,而杰克、皇后和国王的点数为 10,A 的点数为 1 或 11(由玩家选择)。

术语:

  • 二十一点:一张 A 牌加任何一张值 10 点的牌

  • :玩家的赌注被发牌员收走

  • :玩家赢得与赌注相等的金额

  • 二十一点(自然牌):玩家赢得 1.5 倍赌注

  • 平局:玩家保持赌注,既不赢也不输

  • 步骤 1:发牌员抽两张牌,一张面朝上,而玩家抽两张牌(面朝上)

  • 步骤 2:玩家可以抽第三张牌

  • 胜或负:如果你的牌的点数少于 21 且大于发牌员的点数,你获胜。查看 www.pagat.com/banking/ blackjack.html

参考文献

请参考以下文章:

练习

  1. finance.yahoo.com,下载一些公司(如 IBM、WMT 和 C(花旗集团))过去五年的价格数据。测试它们的每日回报是否符合正态分布。

  2. 编写一个 Python 程序,使用scipy.permutation()函数,从过去五年的数据中随机选择 12 个月的回报,不放回。为了测试该程序,你可以使用花旗集团的数据,时间范围为 2012 年 1 月 2 日到 2016 年 12 月 31 日,数据来源于 Yahoo! Finance。

  3. 编写一个 Python 程序,使用给定的n个回报进行自助法抽样。每次选择m个回报,其中m>n

  4. 为了将均匀分布的随机数转换为正态分布,我们有以下公式:Exercises

    根据公式,生成 5,000 个正态分布的随机数;估算它们的均值、标准差,并进行测试。

  5. 假设当前股票价格为$10.25,过去五年的均值为$9.35,标准差为 4.24。编写一个 Python 程序,生成 1,000 个未来的股票价格。

  6. 下载过去 10 年内 10 只股票的价格数据。构建一个等权重投资组合,并对该组合的每日回报进行 Shapiro-Wilk 检验:

    公司名称 股票代码 戴尔公司 DELL
    国际商业机器公司 IBM 通用电气 GE
    微软 MSFT 谷歌 GOOG
    Family Dollar Stores FDO Apple AAPL
    Wal-Mart Stores WMT eBay EBAY
    麦当劳 MCD
  7. 前往 Yahoo! Finance 查找今天的 IBM 股票价格,然后下载其历史价格信息,估算过去五年的均值和标准差。生成未来一年每日价格的预测。

  8. 对于 20 个股票代码,下载并保存它们的每日价格为 20 个不同的 CSV 文件。编写一个 Python 程序,随机选择五只股票,估算它们的等权重投资组合回报和风险。

  9. 重复前一个程序,但将其保存为一个文件,而不是 20 个单独的 CSV 文件。

    提示

    生成一个额外的变量,称为 ticker。

  10. 班级里有 30 名学生。编写一个程序,从中随机选择七个学生。

  11. 测试提取ffMonthly.pklffDaily.pklffMonthly.csvffDaily.csv的时间差,并进行一些测试。

  12. 通常我们观察到投资组合的波动率与投资组合中股票数量之间存在负相关关系。编写一个程序,显示投资组合方差与其中股票数量之间的关系。

  13. 从标记为 1 到 10 的 10 个球中,抽取 1、2、3 和 4 的概率是多少?使用两种方法:a. 使用公式。b. 编写一个程序生成五个随机数。

  14. 编写一个程序生成 176 百万组 Mega Millions 游戏的组合。赢得(1、2、3、4、5)和(1)的机会是多少?

  15. 对于 Powerball 游戏,我们从 59 个标有 1 到 59 的白球中选择 5 个白球,并从 39 个标有 1 到 39 的红球中选择 1 个红球。编写一个程序随机选择这六个球。

  16. 从 20 只股票中选择 7 只,选择前七只股票的概率是多少?使用模拟来证明你的结果。

总结

在本章中,我们讨论了几种类型的分布:正态分布、标准正态分布、对数正态分布和泊松分布。由于假设股票遵循对数正态分布且回报遵循正态分布是期权理论的基石,因此蒙特卡洛模拟被用于定价欧式期权。在某些情况下,亚洲期权可能在对冲方面更为有效。奇异期权比标准期权更为复杂,因为前者没有封闭解,而后者可以通过 Black-Scholes-Merton 期权模型定价。定价这些奇异期权的一种方法是使用蒙特卡洛模拟。我们还讨论了定价亚洲期权和回溯期权的 Python 程序。

第十三章:信用风险分析

信用风险分析的目标是衡量潜在违约支付承诺金额的概率。信用评级反映了一个公司或债券的信用状况。公司的评级不同于其债券的评级,因为后者取决于债券的到期日以及某些特征,比如是否具有可赎回或可出售的选项。在第五章《债券与股票估值》中,我们学到了到期收益率YTM),或简称收益率,它与信用质量相关。信用质量越低,要求的回报率越高,即收益率越高。在本章中,我们将讨论与信用风险相关的许多基本概念,如信用评级、信用利差、一年期信用评级迁移矩阵、违约概率、违约损失率、回收率和 KMV 模型。具体来说,将涵盖以下主题:

  • 穆迪(Moody's)、标准普尔(Standard and Poor's)和惠誉(Fitch)的信用评级

  • 信用利差、一年期和五年期迁移矩阵

  • 利率的期限结构

  • 未来利率的模拟

  • 阿尔特曼 Z 评分预测企业破产

  • KMV 模型估算总资产及其波动性

  • 违约概率和违约距离

  • 信用违约掉期

信用风险分析简介

在本章中,我们将讨论与信用风险相关的基本概念,如信用评级、信用利差、一年期和五年期评级迁移矩阵、违约概率、回收率和违约损失率。信用利差,即债券收益率与基准收益率(无风险利率)之间的差异,反映了其信用风险或违约风险。例如,要估算一只 AA 评级债券在两年后的票息支付现值,折现率(收益率)将是无风险收益率(国债收益率)加上相应的利差。在分析公司或债券的信用状况时,有很多工具可供使用。第一个工具是信用评级,由信用评级机构提供,如穆迪(Moody's)或标普(Standard and Poor's)。其显著的优点是潜在用户可以花费更少的时间和精力评估公司或债券的信用风险。明显的缺点是,信用评级对于大多数用户而言是一个“黑匣子”。换句话说,用户无法复制一个信用评级。因此,想要揭示这种简单字母评级系统背后的逻辑(例如 AA 或 A1)是相当困难的。还有其他方式来评估公司(债券)的信用状况,例如利差,这是一个容易获取的数据。最量化的模型之一是所谓的 KMV 模型,它应用了我们在第十章中学到的期权理论来评估公司的信用风险。

信用评级

目前,美国有三大信用评级机构:穆迪、标准普尔和惠誉。它们的网站分别是 www.moodys.com/www.standardandpoors.com/en_US/web/guest/homewww.fitchratings.com/site/home。尽管它们的评级符号(字母)不同,但将一个评级机构的字母评级转换为另一个评级机构的评级是非常容易的。根据以下链接 www.quadcapital.com/Rating%20Agency%20Credit%20Ratings.pdf,生成了一个名为 creditRatigs3.pkl 的数据集,可以从作者的网站下载,网址是 http://canisius.edu/~yany/python/creditRatings3.pkl。假设它位于 C:/temp/ 目录下。

以下代码显示其内容:

import pandas as pd
x=pd.read_pickle("c:/temp/creditRatings3.pkl")
print(x)
       Moody's S&P Fitch  NAIC  InvestmentGrade
0      Aaa   AAA   AAA     1                1
1      Aa1   AA+   AA+     1                1
2      Aa2    AA    AA     1                1
3      Aa3   AA-   AA-     1                1
4       A1    A+    A+     1                1
5       A2     A     A     1                1
6       A3    A-    A-     1                1
7     Baa1  BBB+  BBB+     2                1
8     Baa2   BBB   BBB     2                1
9     Baa3  BBB-  BBB-     2                1
10     Ba1   BB+   BB+     3                0
11     Ba2    BB    BB     3                0
12     Ba3   BB-   BB-     3                0
13      B1    B+    B+     3                0
14      B2     B     B     3                0
15      B3    B-    B-     3                0

第一列是行号,没有特定含义。接下来的三列分别是 穆迪标准普尔惠誉 的信用评级。NAIC 代表 国家保险委员会协会。任何评级等于或高于 BBB 的被归类为投资级别,参见最后一列(变量),其值为 1 或 0。许多共同基金和养老金基金只能投资评级为投资级别的债券。

当一家公司今年获得 Aaa 评级时,它明年保持相同信用评级的概率是多少?根据以下表格,它明年保持 Aaa 评级的概率为 89%,穆迪(2007 年)。另一方面,降级的概率是 3%,即从 Aaa 降级为 Aa1。对于 B1 评级的债券,保持相同评级的概率是 65%。同时,它有 12% 的概率升级,9% 的概率降级。B1 评级债券的违约概率是 3%,请参见以下图表的最后一列,给出了这一年的信用评级迁移矩阵:

信用评级

一年期信用评级迁移矩阵

注意以下缩写:

  • WR 表示穆迪已撤回其评级

  • DEF 表示违约概率

同样,Aaa 评级公司在明年变成 Aa2 评级的概率是 3%。主对角线上的数值(从西北到东南)是明年保持相同评级的概率。主对角线下方的数值(左下三角和底部三角)是降级的概率,而主对角线上方的数值(右上三角)是升级的概率。最后一列提供了各种评级的违约概率。例如,Ba2 评级的债券违约概率为 1%,而 Caa3 评级的债券违约概率为 25%。可以使用名为 migration1year.pkl 的 Python 数据集,代码如下所示。数据集可以通过以下网址获取:http://canisius.edu/~yany/python/migration1year.pkl:

import pandas as pd
x=pd.read_pickle("c:/temp/migration1year.pkl")
print(x.head(1))
print(x.tail(1))
    Aaa   Aa1   Aa2  Aa3   A1   A2   A3  Baa1  Baa2  Baa3 ...   Ba3   B1  \
Aaa  0.89  0.03  0.03  0.0  0.0  0.0  0.0   0.0   0.0   0.0 ...   0.0  0.0   
      B2   B3  Caa1  Caa2  Caa3  Ca-C    WR  DEF  
Aaa  0.0  0.0   0.0   0.0   0.0   0.0  0.05  0.0  
[1 rows x 22 columns]
      Aaa  Aa1  Aa2  Aa3   A1   A2   A3  Baa1  Baa2  Baa3 ...   Ba3   B1   B2\
Ca-C  0.0  0.0  0.0  0.0  0.0  0.0  0.0   0.0   0.0   0.0 ...   0.0  0.0  0.0   
       B3  Caa1  Caa2  Caa3  Ca-C    WR  DEF  
Ca-C  0.0  0.01  0.01  0.01  0.35  0.13  0.2  
[1 rows x 22 columns]

以下表格展示了穆迪的 5 年过渡(迁移)矩阵。请注意DEF列(用于违约概率):

信用评级

穆迪的平均 5 年评级过渡矩阵(1920-1992)

来源:穆迪(2007 年)。

注意以下缩写:

  • WR 表示穆迪已撤销其评级

  • DEF 表示违约概率

一个名为migration5year.pkl的数据集已生成。该数据集可以在canisius.edu/~yany/python/migration5year.pkl下载。以下代码将打印其首尾行:

import pandas as pd
x=pd.read_pickle("c:/temp/migration5year.pkl")
print(x.head(1))
print(x.tail(1))
    Aaa   Aa1  Aa2   Aa3    A1    A2   A3  Baa1  Baa2  Baa3 ...   Ba3   B1  \
Aaa  0.56  0.07  0.1  0.03  0.01  0.01  0.0   0.0   0.0   0.0 ...   0.0  0.0   
      B2   B3  Caa1  Caa2  Caa3  Ca-C   WR  DEF  
Aaa  0.0  0.0   0.0   0.0   0.0   0.0  0.2  0.0  
[1 rows x 22 columns]
      Aaa  Aa1  Aa2  Aa3   A1   A2   A3  Baa1  Baa2  Baa3  ...   Ba3   B1  \
Ca-C  0.0  0.0  0.0  0.0  0.0  0.0  0.0   0.0   0.0   0.0  ...   0.0  0.0   
        B2    B3  Caa1  Caa2  Caa3  Ca-C    WR   DEF  
Ca-C  0.02  0.02  0.01  0.01  0.01  0.04  0.43  0.46  

评级与违约呈负相关。评级越高,违约概率越低。以下是累积历史违约率(以百分比表示):

违约率(%)
穆迪
评级类别 市政
Aaa/AAA 0.00
Aa/AA 0.06
A/A 0.03
Baa/BBB 0.13
Ba/BB 2.65
B/B 11.86
Caa-C/CCC-C 16.58
平均值
投资级 0.07
非投资级 4.29
所有 0.10

表 13.3 信用评级与 DP(违约概率)之间的关系

数据来自于网站monevator.com/bond-default-rating-probability/

例如,对于穆迪的Aaa级相关公司债券,其违约概率为 0.52%。标准普尔的对应违约概率为 0.60%。给定违约后的回收率是一个重要概念。债务的状态(资历)对回收率有重大影响。根据 Altman 和 Kishore(1997 年)的研究,我们得到了以下表格:

回收率(占面值的百分比)
高级担保债务 58%
高级-非担保债务 48%
高级-次级债务 35%
次级债务 32%
折扣债券和零息债券 21%

表 13.4 基于资历的回收率

担保债务是指由资产担保支付的债务。高级债务和次级债务是指优先级结构。另一方面,不同的行业有不同的回收率,这是由于它们各自的行业特征,如固定长期资产和无形资产的比例:

行业 平均回收率 观察数量
公用事业 70.5% 56
化工、石油、橡胶和塑料制品 62.7% 35
机械、仪器及相关产品 48.7% 36
服务业 - 商业和个人 46.2% 14
食品及相关产品 45.3% 18
批发和零售贸易 44.0% 12
多元化制造业 42.3% 20
赌场、酒店和娱乐 40.2% 21
建筑材料、金属和加工制品 38.8% 68
运输和运输设备 38.4% 52
通信、广播、电影制作 37.1% 65
印刷和出版 NA NA
金融机构 35.7% 66
建筑和房地产 35.3% 35
一般商品商店 33.2% 89
矿业和石油钻探 33.0% 45
纺织和服装产品 31.7% 31
木材、纸张和皮革制品 29.8% 11
住宿、医院和护理设施 26.5% 22
总计 41.0% 696

表 13.5 基于行业的回收率

请参见关于回收率的文章: Recovery Rates PDF

前面的表格是根据回收率从高到低排序的。对于印刷和出版行业,根据原始数据没有相关数据。违约损失LGD)等于 1 减去回收率

信用评级

在这里,我们通过一个假设性的例子来解释默认概率和回收率的使用,以计算债券的价格。假设一只一年期债券的面值为 100 美元,票息率为 6%,到期收益率(YTM)为 7%。我们有以下四种情况:

  • 情况 #1:无违约。今天的价格将是其折现后的未来现金流,(6+100)/(1+0.07)。

  • 情况 #2:确定违约并且无法回收任何金额。对于这种情况,其价格将为零。

  • 情况 #3:如果发生违约,我们将无法收回任何金额。

  • 情况 #4:如果发生违约,我们将收到某些金额。

以下表格总结了前面四种情况:

# 条件 违约概率 回收率 今天的价格
1 无违约 P=0, 回收率(NA) $99.07
2 100% 违约/无回收 P=100%,回收率=0 0
3 如果违约,无法回收任何金额 O<P<100%,回收率=0 $99.07 *(1-P)
4 如果违约,回收部分金额 O<P<100%,回收率>0 $99.07 1-P(1- ![信用评级)]

表 13.6 不同违约概率和回收率的四种情况

债券的价格是其所有预期未来现金流现值的总和:

信用评级

如果P是违约概率,我们有以下预期未来现金流:

信用评级

折现所有未来现金流可以得出债券的价格:

信用评级

假设根据穆迪评级,信用评级为 A。根据表 13.3,其违约率为 1.29%。进一步假设它是一家公用事业公司。因此,根据表 13.5,违约时的回收率为 70.5%。该债券的面值为 100 美元,要求回报率(YTM)为 5%。根据前述公式,若没有违约,一年期债券的价格将为 95.24 美元,即 100/(1+0.05)。我们这只债券的卖出价格,将在 1.29%的违约概率下为 94.88 美元,即95.24(1-0.0129(1-0.705))

信用利差

信用利差(违约风险溢价)反映了其违约风险。例如,要估算一个 AA 评级债券在两年后的票息支付现值,折现率(收益率)将是无风险利率加上相应的利差。对于给定的信用评级,可以通过使用历史数据来找到其信用利差。以下是一个典型的表格,显示了信用风险溢价(利差)与信用评级之间的关系,请参见下表:

我们感谢亚当·阿莫多尔教授在其网站上提供的数据集,people.stern.nyu.edu/adamodar/pc/datasets/

信用利差

基于信用评级的信用利差

除了前面表格中的最后一行外,利差的单位是基点,即百分之一的百分之一。例如,对于一个 A-(A 减)评级的五年期债券,其利差为 83.6 个基点。由于无风险利率为 1.582%(五年期国债利率),该债券的到期收益率(YTM)将是 2.418%,即 0.01582+83.6/100/100。根据前面的表格,我们生成了一个名为bondSpread2014.p的 Python 数据集,数据集可以在作者的网站上找到,canisius.edu/~yany/python/creditSpread2014.pkl

import pandas as pd
x=pd.read_pickle("c:/temp/creditSpread2014.pkl")
print(x.head())
print(x.tail())
  Rating     1     2     3     5     7    10     30
0  Aaa/AAA   5.0   8.0  12.0  18.0  28.0  42.0   65.0
1  Aa1/AA+  11.2  20.0  27.0  36.6  45.2  56.8   81.8
2   Aa2/AA  16.4  32.8  42.6  54.8  62.8  71.2   97.8
3  Aa3/AA-  21.6  38.6  48.6  59.8  67.4  75.2   99.2
4    A1/A+  26.2  44.0  54.2  64.6  71.4  78.4  100.2
               Rating        1        2        3        5        7       10  \
13              B1/B+  383.600  409.600  431.400  455.600  477.600  500.800   
14               B2/B  455.800  481.600  505.200  531.000  555.400  581.400   
15              B3/B-  527.800  553.800  579.400  606.400  633.600  661.800   
16           Caa/CCC+  600.000  626.000  653.000  682.000  712.000  743.000   
17  US Treasury Yield    0.132    0.344    0.682    1.582    2.284    2.892

经过仔细研究前面的表格后,我们会发现两个单调趋势。首先,利差是信用质量的递减函数。信用评级越低,其利差越高。其次,对于相同的信用评级,其利差每年都会增加。例如,对于 AAA 评级的债券,一年期的利差为 5 个基点,而五年期的利差为 18 个基点。

AAA 评级债券的收益率,Altman Z 分数

从前面的章节中,我们已经了解到,债券的收益率与同到期国债收益率之间的差额是违约风险溢价。为了获取AAAAA债券的收益率,我们使用以下代码。穆迪的Aaa公司债券收益率可以在 https://fred.stlouisfed.org/series/AAA 下载。数据集可以在canisius.edu/~yany/python/moodyAAAyield.p下载。请注意,.p格式的.png文件适用于.pickle格式:

import pandas as pd
x=pd.read_pickle("c:/temp/moodyAAAyield.p")
print(x.head())
print(x.tail())

输出如下所示:

AAA 评级债券的收益率,Altman Z 分数

请注意,名为moodyAAAyield.p的数据集中的第二列值是年化的。因此,如果我们想估计 1919 年 1 月的月度收益率(回报率),该收益率应为 0.4458333%,即 0.0535/12。

艾尔特曼 Z-score 广泛应用于金融领域的信用分析,用于预测公司破产的可能性。该分数是基于公司资产负债表和损益表的五个比率的加权平均值。对于上市公司,艾尔特曼(1968)提供了以下公式:

AAA 评级债券的收益率,艾尔特曼 Z-score

在此,X1X2X3X4X5 的定义列在下表中:

变量 定义
X1 息税前利润/总资产
X2 净销售额/总资产
X3 股本市场价值/总负债
X4 营运资金/总资产
X5 留存收益/总资产

表 13.8 Z-scores 估算中变量的定义

基于 Z-score 的范围,我们可以将上市公司分为以下四类。Eidlenan(1995)发现,Z-score 正确预测了 72%的破产事件,这些破产事件发生在事件发生前两年:

Z-score 范围 描述
> 3.0 安全
2.7 至 2.99 警戒状态
1.8 至 2.7 两年内破产的高概率
< 1.80 财务困境的概率非常高

艾尔特曼 Z-score 属于信用评分(方法)类别。另一方面,更先进的模型,例如 KMV 模型,是基于现代金融理论,如期权理论。

使用 KMV 模型估算总资产的市场价值及其波动性

KMV 代表KealhoferMcQuownVasicek,他们创办了一家公司,专注于衡量违约风险。KMV 方法是通过使用公司资产负债表信息和股市信息来估计公司违约概率的最重要方法之一。本节的目标是展示如何估算总资产(A)的市场价值及其相应的波动性(σA)。该结果将在本章后续使用。基本思路是将公司的股本视为一个看涨期权,其债务的账面价值视为行权价。让我们看一个最简单的例子。对于一家公司,如果其债务为$70,股本为$30,则总资产为$100,见下表:

100 70
30

假设总资产跳升至$110,债务保持不变。现在,股本的价值增加到$40。另一方面,如果资产下降到$90,股本将被评估为$20。由于股东是剩余索偿人,他们的价值满足以下表达式:

使用 KMV 模型估算总资产的市场价值及其波动性

这里,E 是股权的价值,A 是总资产,D 是总债务水平。回顾欧式看涨期权,我们有以下支付函数:

使用 KMV 模型估算总资产的市场价值及其波动率

这里,ST 是到期日的终端股票价格,T 是到期日,K 是执行价格,max() 是最大值函数。前两个公式之间的相似性表明,我们可以将股权视为以债务水平为执行价格的看涨期权。通过适当的符号表示,我们将得到以下公司的股权公式。KMV 模型在此定义:

使用 KMV 模型估算总资产的市场价值及其波动率

另一方面,以下是股权与总资产波动率之间的关系。在下面的公式中,我们有:

使用 KMV 模型估算总资产的市场价值及其波动率使用 KMV 模型估算总资产的市场价值及其波动率

由于 d1d2 是通过前面公式定义的,我们有两个关于两个未知数 (A使用 KMV 模型估算总资产的市场价值及其波动率) 的方程;请参见以下公式。因此,我们可以使用试错法或联立方程法来求解这两个未知数。最终,我们要解以下两个关于 A使用 KMV 模型估算总资产的市场价值及其波动率 的方程:

使用 KMV 模型估算总资产的市场价值及其波动率

我们应该注意前面公式中估算的 A(总资产市场价值),因为它不同于资产市场价值与债务账面价值的总和。

以下 Python 程序用于估算给定 E(股权)、D(债务)、T(到期日)、r(无风险利率)和股权波动率(sigmaE)下的总资产(A)及其波动率(sigmA)。该程序的基本逻辑是,我们输入大量的 (A, sigmaE) 配对数据,然后根据前面的公式估算 E 和 sigmaE。由于 E 和 sigmaE 都是已知的,我们可以估算出差异,diff4E=estimatedE – knownEdiff4sigmaE = estimatedSigmaE – knownSigmaE。最小化这两个绝对差值和的 (A, sigmaE) 配对即为我们的解:

import scipy as sp
import pandas as pd
import scipy.stats as stats
from scipy import log,sqrt,exp
# input area 
D=30\.            # debt
E=70\.            # equity 
T=1\.             # maturity 
r=0.07           # risk-free
sigmaE=0.4       # volatility of equity 
#
# define a function to siplify notations later 
def N(x):
    return stats.norm.cdf(x)
#
def KMV_f(E,D,T,r,sigmaE):
    n=10000
    m=2000
    diffOld=1e6     # a very big number
    for i in sp.arange(1,10):
        for j in sp.arange(1,m):
            A=E+D/2+i*D/n
            sigmaA=0.05+j*(1.0-0.001)/m
            d1 = (log(A/D)+(r+sigmaA*sigmaA/2.)*T)/(sigmaA*sqrt(T))
            d2 = d1-sigmaA*sqrt(T)
            diff4A= (A*N(d1)-D*exp(-r*T)*N(d2)-E)/A  # scale by assets
            diff4sigmaE= A/E*N(d1)*sigmaA-sigmaE     # a small number already
            diffNew=abs(diff4A)+abs(diff4sigmaE)
            if diffNew<diffOld:
               diffOld=diffNew
               output=(round(A,2),round(sigmaA,4),round(diffNew,5))
    return output
#
print("KMV=", KMV_f(D,E,T,r,sigmaE))
print("KMV=", KMV_f(D=65e3,E=110e3,T=1,r=0.01,sigmaE=0.2))

输出如下所示:

print("KMV=", KMV_f(D,E,T,r,sigmaE))

使用 KMV 模型估算总资产的市场价值及其波动率

请注意结果,因为债务的账面价值与股本的市场价值总和为 175,000,而我们估算的结果为 142,559。由于公司的股本是看涨期权,我们可以使用 Black-Scholes-Merton 模型来再次验证我们的结果。

利率期限结构

在 第五章《债券与股票估值》中,我们讨论了利率期限结构的概念。利率期限结构被定义为无风险利率与时间之间的关系。无风险利率通常被定义为无违约的国债利率。从许多来源,我们可以获取当前的利率期限结构。例如,在 2017 年 2 月 27 日,我们可以从 http://finance.yahoo.com/bonds 获取以下信息:

利率期限结构

绘制的利率期限结构可能更吸引眼球;请参见以下代码:

import matplotlib.pyplot as plt
time=[3./12.,6./12.,2.,3.,5.,10.,30.]
rate=[0.45,0.61,1.12,1.37,1.78,2.29,2.93]
plt.title("Term Structure of Interest Rate ")
plt.xlabel("Time (in years) ")
plt.ylabel("Risk-free rate (%)")
plt.plot(time,rate)
plt.show()

相关的图表如下所示:

利率期限结构

为了模拟未来的利率变化,我们可以应用所谓的 BIS 模型,使用以下公式。利率变化假设遵循正态分布;请参见以下公式:

利率期限结构

这里,Δ表示变化,R 是利率,s 是利率的标准差。以下是等效的公式:

利率期限结构

现在,我们有以下公式来调整我们的模拟:

利率期限结构

这里,z 是反累积分布函数。以下代码显示了 scipy.stat.norm.ppf() 函数和给定 RVq 时的百分位点函数(cdf 的反函数):

import scipy.stats as stats
#
cumulativeProb=0
print(stats.norm.ppf(cumulativeProb))
#
cumulativeProb=0.5
print(stats.norm.ppf(cumulativeProb))
#
cumulativeProb=0.99
print(stats.norm.ppf(cumulativeProb))

相关的三个输出如下所示:

利率期限结构

相关的 Python 代码如下所示:

import scipy as sp
import scipy.stats as stats
# input area
R0=0.09              # initial rate
s=0.182              # standard deviation of the risk-free rate
nSimulation=10       # number of simulations
sp.random.seed(123)  # fix the seed
#
num=sp.random.uniform(0,1,size=nSimulation)
z=stats.norm.ppf(num)
#
output=[]
def BIS_f(R,s,n):
    R=R0
    for i in sp.arange(0,n):
        deltaR=z[i]*s/sp.sqrt(2.)
        logR=sp.log(R)
        R=sp.exp(logR+deltaR)
        output.append(round(R,5))
    return output 
#
final=BIS_f(R0,s,nSimulation)
print(final)
[0.09616, 0.08942, 0.0812, 0.08256, 0.08897, 0.08678, 0.11326, 0.1205, 0.11976, 0.11561]

违约距离

违约距离 (DD) 由以下公式定义;这里 A 是总资产的市场价值,![违约距离

违约点 而言,目前没有理论指导如何选择理想的违约点。然而,我们可以将所有短期债务加上长期债务的一半作为我们的违约点。在得到资产的市场价值及其波动性后,我们可以使用前述公式来估算违约距离。A 和 违约距离 来自 公式(10) 的输出。另一方面,如果违约点等于 E,我们将得到以下公式:

违约距离

根据 Black-Scholes-Merton 看涨期权模型,DDDP(违约概率) 之间的关系如下:

违约距离

信用违约掉期

债权人可以购买一种所谓的信用违约掉期CDS)来在违约发生时进行保护。CDS 的买方向卖方支付一系列款项,作为交换,如果贷款发生违约,买方将获得赔偿。我们来看一个简单的例子。一个基金刚刚购买了 1 亿美元的公司债券,债券的到期时间为 15 年。如果发行公司没有发生违约,养老基金将每年享受利息支付,并在到期时拿回 1 亿美元本金。为了保护他们的投资,基金与一家金融机构签订了一份 15 年的 CDS 合约。根据债券发行公司的信用状况,约定的利差为 80 个基点,按年支付。这意味着每年,养老基金(CDS 买方)将在未来 10 年里支付给金融机构(CDS 卖方)80,000 美元。如果发生信用事件,CDS 卖方将根据其损失向 CDS 买方进行赔偿。如果合同规定的是实物结算,CDS 买方可以将债券以 1 亿美元的价格卖给 CDS 卖方。如果合同规定的是现金结算,CDS 卖方将支付Max($100m-X,0)给 CDS 买方,其中 X 是债券的市场价值。如果债券的市场价值为 7000 万美元,则 CDS 卖方将向 CDS 买方支付 3000 万美元。在上述案例中,利差或费用与发行公司违约的概率密切相关。违约概率越高,CDS 的利差越高。下表显示了这种关系:

CDS P CDS P CDS P CDS P CDS P CDS P CDS P
0 0.0% 100 7.8% 200 13.9% 300 19.6% 500 30.2% 500 30.2% 1000 54.1%
5 0.6% 105 8.1% 205 14.2% 310 20.2% 510 30.7% 525 31.4% 1025 55.2%
10 1.1% 110 8.4% 210 14.5% 320 20.7% 520 31.2% 550 32.7% 1050 56.4%
15 1.6% 115 8.7% 215 14.8% 330 21.2% 530 31.7% 575 33.9% 1075 57.5%
20 2.0% 120 9.1% 220 15.1% 340 21.8% 540 32.2% 600 35.2% 1100 58.6%
25 2.4% 125 9.4% 225 15.4% 350 22.3% 550 32.7% 625 36.4% 1125 59.7%
30 2.8% 130 9.7% 230 15.7% 360 22.9% 560 33.2% 650 37.6% 1150 60.9%
35 3.2% 135 10.0% 235 16.0% 370 23.4% 570 33.7% 675 38.8% 1175 62.0%
40 3.6% 140 10.3% 240 16.2% 380 23.9% 580 34.2% 700 40.0% 1200 63.1%
45 4.0% 145 10.6% 245 16.5% 390 24.5% 590 34.7% 725 41.2% 1225 64.2%
50 4.3% 150 10.9% 250 16.8% 400 25.0% 600 35.2% 750 42.4% 1250 65.3%
55 4.7% 155 11.2% 255 17.1% 410 25.5% 610 35.7% 775 43.6% 1275 66.4%
60 5.0% 160 11.5% 260 17.4% 420 26.0% 620 36.1% 800 44.8% 1300 67.5%
65 5.4% 165 11.8% 265 17.7% 430 26.6% 630 36.6% 825 46.0% 1325 68.6%
70 5.7% 170 12.1% 270 17.9% 440 27.1% 640 37.1% 850 47.2% 1350 69.7%
75 6.1% 175 12.4% 275 18.2% 450 27.6% 650 37.6% 875 48.3% 1375 70.7%
80 6.4% 180 12.7% 280 18.5% 460 28.1% 660 38.1% 900 49.5% 1400 71.8%
85 6.8% 185 13.0% 285 18.8% 470 28.6% 670 38.6% 925 50.6% 1425 72.9%
90 7.1% 190 13.3% 290 19.1% 480 29.1% 680 39.1% 950 51.8% 1450 74.0%
95 7.4% 195 13.6% 295 19.3% 490 29.6% 690 39.6% 975 52.9% 1475 75.1%
100 7.8% 200 13.9% 300 19.6% 500 30.2% 700 40.0% 1000 54.1% 1500 76.1%

表 13.9:违约概率和信用违约掉期。

违约概率估计的五年累计违约概率(P)

和五年期信用违约掉期(5Y CDS)

附录 A – 数据案例#8 - 使用 Z 分数预测破产

Altman 的 Z 分数用于预测公司破产的可能性。该分数是基于公司资产负债表和利润表的五个比率的加权平均值。对于上市公司,Altman(1968)提供了以下公式:

附录 A – 数据案例#8 - 使用 Z 分数预测破产

在这里,X1X2X3X4X5 的定义在下面的表格中给出:

变量 定义
X1 息税前利润/总资产
X2 净销售额/总资产
X3 市场价值的股本/总负债
X4 营运资金/总资产
X5 留存收益/总资产

根据 Z 分数的范围,我们可以将 20 家上市公司分为以下四类。Eidlenan(1995)发现,Z 分数能够准确预测事件发生前两年的 72%的破产情况:

Z 分数范围 描述
> 3.0 安全
2.7 到 2.99 处于警戒状态
1.8 到 2.7 两年内破产的可能性较大
< 1.80 财务困境的可能性非常高

参考文献

练习

  1. 美国有多少家信用评级机构?哪些是主要的机构?

  2. 风险的定义有几种?信用风险和市场风险有什么区别?

  3. 如何估计一个公司的总风险和市场风险?相关的数学公式是什么?

  4. 如何估计一个公司的信用风险?相关的数学公式是什么?

  5. 为什么债券的信用风险可能与其公司的信用评级不同?

  6. 如果一切条件相等,哪种债券的风险更高,是长期债券还是短期债券?

  7. 信用利差的定义是什么?为什么它有用?

  8. 利率期限结构的用途是什么?

  9. 对于 Altman 的 Z-score,X1X2X3X4X5的定义是什么?解释为什么 Z-score 越高,破产的概率越低:练习

  10. 确定 Z-score 的一个问题并找到解决方法。

  11. 一年期迁移(转移)矩阵是什么?

  12. 信用评级与违约概率之间有什么关系?

  13. 使用债券现值的概念,如何通过折现预计的未来现金流来推导方程式(1)?

  14. 信用迁移矩阵中(从西北到东南)的主对角线上的值是什么?

  15. 沃尔玛计划发行 5000 万美元(总面值)的公司债,每个债券的面值为 1000 美元。这些债券将在 10 年后到期,票面利率为 8%,每年支付一次利息。沃尔玛今天能筹集多少资金?如果沃尔玛将其信用评级提高一个等级,该公司能筹集多少额外资金?

  16. 下表展示了评级、违约风险(利差)与时间之间的关系。编写一个 Python 程序来插值缺失的利差,如从第 11 年到第 29 年的 S。Python 数据集可以从canisius.edu/~yany/python/creditSpread2014.p下载:

    import matplotlib.pyplot as plt
    import pandas as pd
    x=pd.read_pickle("c:/temp/creditSpread2014.p")
    print(x.head())
        Rating     1     2     3     5     7    10     30
    0  Aaa/AAA   5.0   8.0  12.0  18.0  28.0  42.0   65.0
    1  Aa1/AA+  11.2  20.0  27.0  36.6  45.2  56.8   81.8
    2   Aa2/AA  16.4  32.8  42.6  54.8  62.8  71.2   97.8
    3  Aa3/AA-  21.6  38.6  48.6  59.8  67.4  75.2   99.2
    4    A1/A+  26.2  44.0  54.2  64.6  71.4  78.4  100.2
    

总结

在本章中,我们从信用风险分析的基础知识开始,涵盖信用评级、信用利差、1 年期评级迁移矩阵、违约概率PD)、违约损失LGD)、利率期限结构、Altman Z 分数、KMV 模型、违约概率、违约距离和信用违约掉期。在第十章,期权与期货,讨论了一些基本的香草期权,如 Black-Scholes-Merton 期权及其相关应用。此外,在第十二章,蒙特卡洛模拟中,解释了两种特殊期权。

在下一章中,我们将讨论更多的特殊期权,因为它们对于缓解许多类型的金融风险非常有用。

第十四章:另类期权

在第十章,期权与期货中,我们讨论了著名的布莱克-斯科尔斯-梅顿期权模型以及涉及各种类型期权、期货和基础证券的各种交易策略。布莱克-斯科尔斯-梅顿的闭式解法适用于只能在到期日行使的欧式期权。美式期权可以在到期日之前或当天行使。通常,这些类型的期权被称为香草期权。另一方面,也存在各种类型的另类期权,它们具有各种特性,使其比常见的香草期权更为复杂。

例如,如果期权买方可以在到期日前的多个时刻行使权利,那么它就是一个百慕大期权。在第十二章,蒙特卡洛模拟中,讨论了两种类型的另类期权。许多另类期权(衍生品)可能有多个触发条件与其支付有关。另类期权还可能包括非标准的基础证券或工具,专为特定客户或特定市场开发。另类期权通常是场外交易OTC)。

在本章中,将涵盖以下主题:

  • 欧式、美式和百慕大期权

  • 简单选择期权

  • 喊叫期权、彩虹期权和双向期权

  • 平均价格期权

  • 障碍期权——上涨入场期权和上涨退出期权

  • 障碍期权——下跌入场期权和下跌退出期权

欧式、美式和百慕大期权

在第十章,期权与期货中,我们已经学习了对于欧式期权,期权买方只能在到期日行使权利,而对于美式期权买方,他们可以在到期日之前的任何时候行使权利。因此,美式期权的价值通常高于其对应的欧式期权。百慕大期权可以在几个预定的日期内行使一次或多次。因此,百慕大期权的价格应介于欧式期权和美式期权之间,前提是它们具有相同的特征,例如相同的到期日和相同的行权价格,见以下两个关于看涨期权的不等式:

欧式、美式和百慕大期权

这里是一个关于百慕大期权的例子。假设一家公司发行了一张 10 年期债券。七年后,公司可以在接下来的三年中的每年年底,选择赎回即退还债券。这种可赎回特性最终形成了一个嵌入式百慕大期权,其行使日期为第 8、9 和 10 年的 12 月。

首先,我们来看一下使用二项模型的美式看涨期权的 Python 程序:

def binomialCallAmerican(s,x,T,r,sigma,n=100):
    from math import exp,sqrt
    import numpy as np
    deltaT = T /n
    u = exp(sigma * sqrt(deltaT)) 
    d = 1.0 / u
    a = exp(r * deltaT)
    p = (a - d) / (u - d)
    v = [[0.0 for j in np.arange(i + 1)] for i in np.arange(n + 1)] 
    for j in np.arange(n+1):
        v[n][j] = max(s * u**j * d**(n - j) - x, 0.0) 
    for i in np.arange(n-1, -1, -1):
        for j in np.arange(i + 1):
            v1=exp(-r*deltaT)*(p*v[i+1][j+1]+(1.0-p)*v[i+1][j]) 
            v2=max(v[i][j]-x,0)         # early exercise 
            v[i][j]=max(v1,v2)
    return v[0][0]
#
s=40\.        # stock price today 
x=40\.        # exercise price
T=6./12      # maturity date ii years
tao=1/12     # when to choose
r=0.05       # risk-free rate
sigma=0.2    # volatility 
n=1000       # number of steps
#
price=binomialCallAmerican(s,x,T,r,sigma,n)
print("American call =", price)
('American call =', 2.7549263174936502)

这个美式看涨期权的价格是$2.75。为了修改之前的程序以仅满足少数几个行权价格,以下两行是关键:

            v2=max(v[i][j]-x,0)         # early exercise 
            v[i][j]=max(v1,v2)

这是一个适用于伯穆达看涨期权的 Python 程序。关键的不同点是名为 T2 的变量,它包含伯穆达期权可以行权的日期:

def callBermudan(s,x,T,r,sigma,T2,n=100):
    from math import exp,sqrt
    import numpy as np
    n2=len(T2)
    deltaT = T /n
    u = exp(sigma * sqrt(deltaT)) 
    d = 1.0 / u
    a = exp(r * deltaT)
    p = (a - d) / (u - d)
    v =[[0.0 for j in np.arange(i + 1)] for i in np.arange(n + 1)] 
    for j in np.arange(n+1):
        v[n][j] = max(s * u**j * d**(n - j) - x, 0.0) 
    for i in np.arange(n-1, -1, -1):
        for j in np.arange(i + 1):
            v1=exp(-r*deltaT)*(p*v[i+1][j+1]+(1.0-p)*v[i+1][j])
            for k in np.arange(n2):
                if abs(j*deltaT-T2[k])<0.01:
                    v2=max(v[i][j]-x,0)  # potential early exercise 
                else: 
                    v2=0
            v[i][j]=max(v1,v2)
    return v[0][0]
#
s=40\.                 # stock price today 
x=40\.                 # exercise price
T=6./12               # maturity date ii years
r=0.05                # risk-free rate
sigma=0.2             # volatility 
n=1000                # number of steps
T2=(3./12.,4./12.)    # dates for possible early exercise 
#
price=callBermudan(s,x,T,r,sigma,T2,n)
print("Bermudan call =", price)
('Bermudan call =', 2.7549263174936502)

选择权选项

对于选择权期权,它允许期权买方在期权到期之前的某个预定时间点选择是欧洲看涨期权还是欧洲看跌期权。对于一个简单的选择权期权,标的的看涨和看跌期权具有相同的到期日和行权价格。我们来看两个极端案例。期权买方必须在今天做出决策,即当他们进行此类购买时。这个选择权期权的价格应当是看涨和看跌期权的最大值,因为期权买方没有更多的信息。第二个极端情况是投资者可以在到期日做出决策。由于看涨和看跌期权具有相同的行权价格,如果看涨期权处于价内,看跌期权应该是价外的,反之亦然。因此,选择权期权的价格应当是看涨期权和看跌期权的总和。这相当于购买一个看涨期权和一个看跌期权,行权价格和到期日相同。在第十章《期权与期货》中,我们知道这样的交易策略被称为跨式期权(Straddle)。通过这种交易策略,我们押注标的资产会偏离当前的价格,但我们不确定其方向。

首先,我们来看一个简单的选择权期权的定价公式,假设看涨和看跌期权有相同的到期日和行权价格,并假设到期之前没有股息。一个简单的选择权期权的定价公式如下:

选择权选项

这里,Pchooer 是选择权的价格或溢价,call (T) 是一个到期日为 T 的欧洲看涨期权,put(τ) 将很快定义。对于第一个 call (T) 期权,我们有以下定价公式:

选择权选项

这里,call (T) 是看涨期权的溢价,S 是今天的价格,K 是行权价格,T 是到期日(年),σ 是波动率,N() 是累积标准正态分布。实际上,这与 Black-Scholes-Merton 看涨期权模型完全相同。put (τ) 的公式如下:

选择权选项

同样,put(τ) 是看跌期权的溢价,τ 是选择权买方可以做出决策的时刻。为了使 d1d2 与前面公式中的两个值有所区分,使用了 选择权选项选择权选项 来代替 d1d2。请注意,前面的公式与 Black-Scholes-Merton 看跌期权模型不同,因为我们有 T 和 τ 而不仅仅是 T。现在,我们来看一个极端情况:期权买方可以在到期日做出决策,即 τ=T。从前面的公式可以明显看出,选择权的价格将是这两种期权的总和:

选择期权

以下 Python 程序是针对选择期权的。为了节省空间,我们可以将看涨期权和看跌期权合并,见下文 Python 代码。为此,我们有两个时间变量输入,分别是Ttao

from scipy import log,exp,sqrt,stats 
def callAndPut(S,X,T,r,sigma,tao,type='C'):
    d1=(log(S/X)+r*T+0.5*sigma*sigma*tao)/(sigma*sqrt(tao)) 
    d2 = d1-sigma*sqrt(tao)
    if type.upper()=='C':
        c=S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2)
        return c
    else:
        p=X*exp(-r*T)*stats.norm.cdf(-d2)-S*stats.norm.cdf(-d1)
        return p
#
def chooserOption(S,X,T,r,sigma,tao):
    call_T=callAndPut(S,X,T,r,sigma,T)
    put_tao=callAndPut(S,X,T,r,sigma,tao,type='P')
    return call_T- put_tao
#
s=40\.        # stock price today 
x=40\.        # exercise price
T=6./12      # maturity date ii years
tao=1./12\.   # when to choose
r=0.05       # risk-free rate
sigma=0.2    # volatility 
#
price=chooserOption(s,x,T,r,sigma,tao)
print("price of a chooser option=",price)
('price of a chooser option=', 2.2555170735574421)

这个选择权的价格是$2.26。

呼叫期权

呼叫期权是一种标准的欧洲期权,不同之处在于,期权买方可以在到期日前对期权卖方呼叫,将最小支付设置为Sτ-X,其中是买方呼叫时的股票价格,X是行权价。行权价的设定可以与现货价格的特定关系相关,例如设定为现货价的 3%或 5%(高于或低于)。Python 代码如下:

def shoutCall(s,x,T,r,sigma,shout,n=100):
    from math import exp,sqrt
    import numpy as np
    deltaT = T /n
    u = exp(sigma * sqrt(deltaT)) 
    d = 1.0 / u
    a = exp(r * deltaT)
    p = (a - d) / (u - d)
    v =[[0.0 for j in np.arange(i + 1)] for i in np.arange(n + 1)] 
    for j in np.arange(n+1):
        v[n][j] = max(s * u**j * d**(n - j) - x, 0.0) 
    for i in np.arange(n-1, -1, -1):
        for j in np.arange(i + 1):
            v1=exp(-r*deltaT)*(p*v[i+1][j+1]+(1.0-p)*v[i+1][j]) 
            v2=max(v[i][j]-shout,0)   # shout  
            v[i][j]=max(v1,v2)
    return v[0][0]
#
s=40\.              # stock price today 
x=40\.              # exercise price
T=6./12            # maturity date ii years
tao=1/12           # when to choose
r=0.05             # risk-free rate
sigma=0.2          # volatility 
n=1000             # number of steps
shout=(1+0.03)*s   # shout out level 
#
price=shoutCall(s,x,T,r,sigma,shout,n)
print("Shout call =", price)

二元期权

二元期权,或资产无条件期权,是一种期权类型,其支付结构为:若期权到期时处于实值,则支付固定金额的补偿;若期权到期时处于虚值,则支付零。由于这个特性,我们可以应用蒙特卡洛模拟来找到解决方案。Python 代码如下:

import random
import scipy as sp
#
def terminalStockPrice(S, T,r,sigma):
    tao=random.gauss(0,1.0)
    terminalPrice=S * sp.exp((r - 0.5 * sigma**2)*T+sigma*sp.sqrt(T)*tao)
    return terminalPrice
#
def binaryCallPayoff(x, sT,payoff):
    if sT >= x:
        return payoff
    else:
        return 0.0
# input area 
S = 40.0            # asset price
x = 40.0            # exercise price 
T = 0.5             # maturity in years 
r = 0.01            # risk-free rate 
sigma = 0.2         # vol of 20%
fixedPayoff = 10.0  # payoff 
nSimulations =10000 # number of simulations 
#
payoffs=0.0
for i in xrange(nSimulations):
    sT = terminalStockPrice(S, T,r,sigma) 
    payoffs += binaryCallPayoff(x, sT,fixedPayoff)
#
price = sp.exp(-r * T) * (payoffs / float(nSimulations))
print('Binary options call= %.8f' % price)

请注意,由于前述程序未固定种子,因此每次运行时,用户应获得不同的结果。

彩虹期权

许多金融问题可以总结为或与多项资产的最大值或最小值相关。我们来看一个简单的例子:基于两个资产最大值或最小值的期权。这种类型的期权称为彩虹期权。由于涉及两项资产,我们需要熟悉所谓的二元正态分布。以下代码展示了其图形。原始代码可以在scipython.com/blog/visualizing-the-bivariate-gaussian-distribution/网站找到:

import numpy as np
from matplotlib import cm
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#
# input area
n   = 60                      # number of intervals
x   = np.linspace(-3, 3, n)   # x dimension
y   = np.linspace(-3, 4, n)   # y dimension 
x,y = np.meshgrid(x, y)       # grid 
#
# Mean vector and covariance matrix
mu = np.array([0., 1.])
cov= np.array([[ 1\. , -0.5], [-0.5,  1.5]])
#
# combine x and y into a single 3-dimensional array
pos = np.empty(x.shape + (2,))
pos[:, :, 0] = x
pos[:, :, 1] = y
#
def multiNormal(pos, mu, cov):
    n = mu.shape[0]
    Sigma_det = np.linalg.det(cov)
    Sigma_inv = np.linalg.inv(cov)
    n2 = np.sqrt((2*np.pi)**n * Sigma_det)
    fac=np.einsum('...k,kl,...l->...', pos-mu, Sigma_inv, pos-mu)
    return np.exp(-fac/2)/n2
#
z    = multiNormal(pos, mu, cov)
fig  = plt.figure()
ax   = fig.gca(projection='3d')
ax.plot_surface(x, y, z, rstride=3, cstride=3,linewidth=1, antialiased=True,cmap=cm.viridis)
cset = ax.contourf(x, y, z, zdir='z', offset=-0.15, cmap=cm.viridis)
ax.set_zlim(-0.15,0.2)
ax.set_zticks(np.linspace(0,0.2,5))
ax.view_init(27, -21)
plt.title("Bivariate normal distribtuion")
plt.ylabel("y values ")
plt.xlabel("x values")
plt.show()

图形如下:

彩虹期权

假设这两项资产的收益率遵循具有相关系数ρ的二元正态分布。为了简化我们的估算,我们假设在到期日前没有分红。对于基于两个资产最小值的看涨期权,其支付为:

彩虹期权

这里,彩虹期权是股票 1(2)的终值股价,T 是到期日(以年为单位)。基于两项资产最小值的看涨期权定价公式如下:

彩虹期权

这里,S1(S2)是股票 1(2)的当前股价,N2(a,b,ρ)是具有上限 a 和 b、资产间相关系数ρ的二元正态分布,K是行权价。d11d12d21d22ρ1ρ2的参数在此定义:

彩虹期权

首先,我们应学习这里描述的二元累积正态分布N2_f(d1,d2,rho)

def N2_f(d1,d2,rho):
    """cumulative bivariate standard normal distribution 
       d1: the first value
       d2: the second value
       rho: correlation

       Example1:
               print(N2_f(0,0,1.)) => 0.5
       Example2:
               print(N2_f(0,0,0)  => 0.25
     """
    import statsmodels.sandbox.distributions.extras as extras
    muStandardNormal=0.0    # mean of a standard normal distribution 
    varStandardNormal=1.0   # variance of standard normal distribution 
    upper=([d1,d2])         # upper bound for two values
    v=varStandardNormal     # simplify our notations
    mu=muStandardNormal     # simplify our notations
    covM=([v,rho],[rho,v])
    return extras.mvnormcdf(upper,mu,covM)
#

让我们看一些特殊情况。从单变量标准正态分布中,我们知道当输入值为0时,我们期望累积标准正态分布为0.5,因为基础正态分布是对称的。当两个时间序列完全正相关时,累积标准正态分布也应该为0.5,请参见前面的结果。另一方面,如果两个时间序列不相关,当输入都为零时,它们的累积标准正态分布期望为重叠,即,0.5 * 0.5 = 0.25。这一点通过调用之前的N2_f()函数得到验证。对于这一类期权,相关的 Python 程序如下:

from math import exp,sqrt,log
import statsmodels.sandbox.distributions.extras as extras
#
def dOne(s,k,r,sigma,T):
    #print(s,k,r,sigma,T)
    a=log(s/k)+(r-0.5*sigma**2)*T
    b=(sigma*sqrt(T))
    return a/b
#
def sigmaA_f(sigma1,sigma2,rho):
    return sqrt(sigma1**2-2*rho*sigma1*sigma2+sigma2**2)
#
def dTwo(d1,sigma,T):
    return d1+sigma*sqrt(T)
#
def rhoTwo(sigma1,sigma2,sigmaA,rho):
    return (rho*sigma2-sigma1)/sigmaA
#
def N2_f(d1,d2,rho):
    import statsmodels.sandbox.distributions.extras as extras
    muStandardNormal=0.0    # mean of a standard normal distribution 
    varStandardNormal=1.0   # variance of standard normal distribution 
    upper=([d1,d2])         # upper bound for two values
    v=varStandardNormal     # simplify our notations
    mu=muStandardNormal     # simplify our notations
    covM=([v,rho],[rho,v])
    return extras.mvnormcdf(upper,mu,covM)
#
def dOneTwo(s1,s2,sigmaA,T):
    a=log(s2/s1)-0.5*sigmaA**2*T
    b=sigmaA*sqrt(T)
    return a/b
#
def rainbowCallOnMinimum(s1,s2,k,T,r,sigma1,sigma2,rho):
    d1=dOne(s1,k,r,sigma1,T)
    d2=dOne(s2,k,r,sigma2,T)
    d11=dTwo(d1,sigma1,T)
    d22=dTwo(d2,sigma2,T)
    sigmaA=sigmaA_f(sigma1,sigma2,rho)
    rho1=rhoTwo(sigma1,sigma2,sigmaA,rho)
    rho2=rhoTwo(sigma2,sigma1,sigmaA,rho)
    d12=dOneTwo(s1,s2,sigmaA,T)
    d21=dOneTwo(s2,s1,sigmaA,T)
    #
    part1=s1*N2_f(d11,d12,rho1)
    part2=s2*N2_f(d21,d22,rho2)
    part3=k*exp(-r*T)*N2_f(d1,d2,rho)
    return part1 + part2 - part3
#
s1=100.
s2=95.
k=102.0
T=8./12.
r=0.08
rho=0.75
sigma1=0.15
sigma2=0.20
price=rainbowCallOnMinimum(s1,s2,k,T,r,sigma1,sigma2,rho)
print("price of call based on the minimum of 2 assets=",price)
('price of call based on the minimum of 2 assets=', 3.747423936156629)

另一种定价各种类型彩虹期权的方法是使用蒙特卡洛模拟。如我们在第十二章中提到的,蒙特卡洛模拟,我们可以生成两个相关的随机数时间序列。这个过程分为两步:生成两个零相关的随机时间序列x1x2;然后应用以下公式:

彩虹期权

在这里,ρ是这两个时间序列之间的预定相关性。现在,y1y2具有预定的相关性。以下 Python 程序将实现上述方法:

import scipy as sp
sp.random.seed(123)
n=1000
rho=0.3
x1=sp.random.normal(size=n)
x2=sp.random.normal(size=n)
y1=x1
y2=rho*x1+sp.sqrt(1-rho**2)*x2
print(sp.corrcoef(y1,y2))
[[ 1\.          0.28505213]
 [ 0.28505213  1\.        ]]

接下来,我们应用我们在第十二章中所学的同样技术,蒙特卡洛模拟,来定价两个资产最小值的彩虹期权看涨:

import scipy as sp 
from scipy import zeros, sqrt, shape 
#
sp.random.seed(123)  # fix our random numbers
s1=100\.              # stock price 1 
s2=95\.               # stock price 2
k=102.0              # exercise price
T=8./12\.             # maturity in years
r=0.08               # risk-free rate
rho=0.75             # correlation between 2
sigma1=0.15          # volatility for stock 1
sigma2=0.20          # volatility for stock 1
nSteps=100\.          # number of steps 
nSimulation=1000     # number of simulations 
#
# step 1: generate correlated random number
dt =T/nSteps 
call = sp.zeros([nSimulation], dtype=float) 
x = range(0, int(nSteps), 1) 
#
# step 2: call call prices 
for j in range(0, nSimulation): 
    x1=sp.random.normal(size=nSimulation)
    x2=sp.random.normal(size=nSimulation)
    y1=x1
    y2=rho*x1+sp.sqrt(1-rho**2)*x2
    sT1=s1
    sT2=s2 
    for i in x[:-1]: 
        e1=y1[i]
        e2=y2[i]
        sT1*=sp.exp((r-0.5*sigma1**2)*dt+sigma1*e1*sqrt(dt)) 
        sT2*=sp.exp((r-0.5*sigma2**2)*dt+sigma2*e2*sqrt(dt)) 
        minOf2=min(sT1,sT2)
        call[j]=max(minOf2-k,0) 
#
# Step 3: summation and discount back 
call=sp.mean(call)*sp.exp(-r*T) 
print('Rainbow call on minimum of 2 assets = ', round(call,3))
('Rainbow call on minimum of 2 assets = ', 4.127)

如果我们增加更多的资产,获得封闭式解将变得更加困难。这里我们展示如何使用蒙特卡洛模拟来定价基于最大终端股价的彩虹看涨期权。基本逻辑相当简单:生成三个终端股价,然后应用以下公式计算看涨期权收益:

彩虹期权

最终价格将是折现收益的平均值。关键是如何生成一组三个相关的随机数。这里,我们应用了著名的乔尔斯基分解。假设我们有一个相关矩阵叫做C,一个使得彩虹期权的乔尔斯基分解矩阵L。进一步假设无关的回报矩阵叫做U。现在,相关回报矩阵R = UL。以下是对应的 Python 代码:

import numpy as np
# input area
nSimulation=5000              # number of simulations
c=np.array([[1.0, 0.5, 0.3],  # correlation matrix
            [0.5, 1.0, 0.4],
            [0.3, 0.4, 1.0]])
np.random.seed(123)           # fix random numbers 
#
# generate uncorrelated random numbers
x=np.random.normal(size=3*nSimulation)
U=np.reshape(x,(nSimulation,3))
#
# Cholesky decomposition 
L=np.linalg.cholesky(c)
# generate correlated random numbers
r=np.dot(U,L)
#check the correlation matrix
print(np.corrcoef(r.T))
[[ 1\.          0.51826188  0.2760649 ]
 [ 0.51826188  1\.          0.35452286]
 [ 0.2760649   0.35452286  1\.        ]]

定价平均期权

在第十二章,蒙特卡罗模拟中,我们讨论了两种奇异期权。为了方便起见,我们也将它们包括在这一章中。因此,读者可能会看到一些重复的内容。欧洲期权和美式期权是路径无关的期权。这意味着期权的支付仅取决于终端股票价格和执行价格。路径相关期权的一个相关问题是到期日的市场操控。另一个问题是,一些投资者或对冲者可能更关注平均价格而非终端价格。

例如,一家炼油厂担心其主要原料——石油的价格波动,尤其是在接下来的三个月内。他们计划对冲原油价格的潜在跳动。该公司可能会购买看涨期权。然而,由于公司每天消耗大量的原油,它显然更关心的是平均价格,而不是仅仅依赖于普通看涨期权的终端价格。在这种情况下,平均期权会更加有效。平均期权是一种亚洲期权。对于平均期权,其支付取决于某一预定时间段内的基础资产价格的平均值。平均数有两种类型:算术平均数和几何平均数。以下给出了亚洲看涨期权(平均价格)的支付函数:

定价平均期权

这里给出了亚洲看跌期权(平均价格)的支付函数:

定价平均期权

亚洲期权是奇异期权的一种基本形式。亚洲期权的另一个优点是,相比于欧洲和美式的普通期权,它们的成本更低,因为平均价格的波动会比终端价格的小得多。以下 Python 程序适用于一个具有算术平均价格的亚洲期权:

import scipy as sp 
s0=30\.                 # today stock price 
x=32\.                  # exercise price 
T=3.0/12\.              # maturity in years 
r=0.025                # risk-free rate 
sigma=0.18             # volatility (annualized) 
sp.random.seed(123)    # fix a seed here 
n_simulation=1000      # number of simulations 
n_steps=500\.           # number of steps
#
dt=T/n_steps 
call=sp.zeros([n_simulation], dtype=float) 
for j in range(0, n_simulation): 
    sT=s0 
    total=0 
    for i in range(0,int(n_steps)): 
         e=sp.random.normal()
         sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sp.sqrt(dt)) 
         total+=sT 
         price_average=total/n_steps 
    call[j]=max(price_average-x,0) 
#
call_price=sp.mean(call)*sp.exp(-r*T) 
print('call price based on average price = ', round(call_price,3))
('call price based on average price = ', 0.12)

定价障碍期权

不同于 Black-Scholes-Merton 期权模型中的看涨和看跌期权,这些期权是路径无关的,障碍期权是路径相关的。障碍期权与普通期权在许多方面相似,唯一的区别是存在一个触发条件。敲入期权在基础资产触及预定的敲入障碍之前是无效的。相反,敲出障碍期权在其生命周期一开始是有效的,只有当价格突破敲出障碍价时,它才变得无效。此外,如果障碍期权到期时未激活,它可能一文不值,或者会按溢价的一定比例支付现金回扣。四种类型的障碍期权如下:

  • 上涨敲出:在这种障碍期权中,价格从障碍下方开始。如果价格触及障碍,则会被敲出。

  • 下跌敲出:在这种障碍期权中,价格从障碍上方开始。如果价格触及障碍,则会被敲出。

  • 敲入上涨:在这种障碍期权中,价格从障碍下方开始,必须触及障碍才能激活。

  • 下行入场:在这种障碍期权中,价格始于一个障碍点,并且必须达到该障碍点才能被激活。

以下 Python 程序适用于一个带有欧洲看涨期权的上行出场障碍期权:

import scipy as sp 
from scipy import log,exp,sqrt,stats 
#
def bsCall(S,X,T,r,sigma):
    d1=(log(S/X)+(r+sigma*sigma/2.)*T)/(sigma*sqrt(T)) 
    d2 = d1-sigma*sqrt(T)
    return S*stats.norm.cdf(d1)-X*exp(-r*T)*stats.norm.cdf(d2)
#
def up_and_out_call(s0,x,T,r,sigma,n_simulation,barrier):
    n_steps=100\. 
    dt=T/n_steps 
    total=0 
    for j in sp.arange(0, n_simulation): 
        sT=s0 
        out=False
        for i in range(0,int(n_steps)): 
            e=sp.random.normal() 
            sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sp.sqrt(dt)) 
            if sT>barrier: 
               out=True 
        if out==False: 
            total+=bsCall(s0,x,T,r,sigma) 
    return total/n_simulation 
#

基本设计是模拟股票运动 n 次,例如 100 次。对于每次模拟,我们有 100 步。每当股票价格达到障碍点时,收益为零。否则,收益将是一个普通的欧洲看涨期权。最终的值将是所有未被打击的看涨期权价格的总和,再除以模拟次数,如下代码所示:

s0=30\.              # today stock price 
x=30\.               # exercise price 
barrier=32          # barrier level 
T=6./12\.            # maturity in years 
r=0.05              # risk-free rate 
sigma=0.2           # volatility (annualized) 
n_simulation=100    # number of simulations 
sp.random.seed(12)  # fix a seed
#
result=up_and_out_call(s0,x,T,r,sigma,n_simulation,barrier) 
print('up-and-out-call = ', round(result,3))
('up-and-out-call = ', 0.93)

下述是下行入场看跌期权的 Python 代码:

def down_and_in_put(s0,x,T,r,sigma,n_simulation,barrier): 
    n_steps=100.
    dt=T/n_steps 
    total=0
    for j in range(0, n_simulation): 
        sT=s0
        in_=False
        for i in range(0,int(n_steps)): 
            e=sp.random.normal()
            sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sp.sqrt(dt)) 
            if sT<barrier:
                in_=True
            #print 'sT=',sT
            #print 'j=',j ,'out=',out if in_==True:
            total+=p4f.bs_put(s0,x,T,r,sigma) 
    return total/n_simulation
#

进出障碍平价

如果我们购买一个上行出场的欧洲看涨期权和一个上行入场的欧洲看涨期权,则应满足以下平价关系:

进出障碍平价

逻辑非常简单——如果股票价格触及障碍点,则第一个看涨期权失效,第二个看涨期权被激活。如果股票价格从未触及障碍点,第一个看涨期权保持有效,而第二个则永远不会被激活。无论哪种情况,其中一个期权是有效的。以下 Python 程序演示了这种情境:

def upCall(s,x,T,r,sigma,nSimulation,barrier):
    import scipy as sp
    import p4f 
    n_steps=100
    dt=T/n_steps 
    inTotal=0 
    outTotal=0
    for j in range(0, nSimulation): 
        sT=s
        inStatus=False 
        outStatus=True
        for i in range(0,int(n_steps)):
            e=sp.random.normal()
            sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sp.sqrt(dt)) 
            if sT>barrier:
                outStatus=False 
                inStatus=True
        if outStatus==True:
            outTotal+=p4f.bs_call(s,x,T,r,sigma) 
        else:
            inTotal+=p4f.bs_call(s,x,T,r,sigma) 
    return outTotal/nSimulation, inTotal/nSimulation
#

我们输入一组值来测试一个上行出场看涨期权与一个上行入场看涨期权的总和是否与一个普通看涨期权相同:

import p4f
s=40\.                 # today stock price 
x=40\.                 # exercise price 
barrier=42.0          # barrier level 
T=0.5                 # maturity in years 
r=0.05                # risk-free rate 
sigma=0.2             # volatility (annualized) 
nSimulation=500       # number of simulations 
#
upOutCall,upInCall=upCall(s,x,T,r,sigma,nSimulation,barrier) 
print 'upOutCall=', round(upOutCall,2),'upInCall=',round(upInCall,2) 
print 'Black-Scholes call', round(p4f.bs_call(s,x,T,r,sigma),2)

相关的输出如下所示:

upOutCall= 0.75 upInCall= 2.01
Black-Scholes call 2.76

上行出场与上行入场平价图

使用蒙特卡罗模拟来呈现这种平价是一个不错的主意。以下代码旨在实现这一点。为了让我们的模拟更清晰,我们故意选择了仅进行五次模拟:

import p4f
import scipy as sp
import matplotlib.pyplot as plt
#
s =9.25              # stock price at time zero
x =9.10              # exercise price
barrier=10.5         # barrier
T =0.5               # maturity date (in years)
n_steps=30           # number of steps
r =0.05              # expected annual return
sigma = 0.2          # volatility (annualized) 
sp.random.seed(125)  # seed()
n_simulation = 5     # number of simulations 
#
dt =T/n_steps
S = sp.zeros([n_steps], dtype=float) 
time_= range(0, int(n_steps), 1) 
c=p4f.bs_call(s,x,T,r,sigma) 
sp.random.seed(124)
outTotal, inTotal= 0.,0\. 
n_out,n_in=0,0

for j in range(0, n_simulation):
    S[0]= s
    inStatus=False
    outStatus=True
    for i in time_[:-1]:
        e=sp.random.normal()
        S[i+1]=S[i]*sp.exp((r-0.5*pow(sigma,2))*dt+sigma*sp.sqrt(dt)*e) 
        if S[i+1]>barrier:
            outStatus=False 
            inStatus=True
    plt.plot(time_, S) 
    if outStatus==True:
        outTotal+=c;n_out+=1 
    else:
        inTotal+=c;n_in+=1 
        S=sp.zeros(int(n_steps))+barrier 
        plt.plot(time_,S,'.-') 
        upOutCall=round(outTotal/n_simulation,3) 
        upInCall=round(inTotal/n_simulation,3) 
        plt.figtext(0.15,0.8,'S='+str(s)+',X='+str(x))
        plt.figtext(0.15,0.76,'T='+str(T)+',r='+str(r)+',sigma=='+str(sigma)) 
        plt.figtext(0.15,0.6,'barrier='+str(barrier))
        plt.figtext(0.40,0.86, 'call price  ='+str(round(c,3)))
        plt.figtext(0.40,0.83,'up_and_out_call ='+str(upOutCall)+' (='+str(n_out)+'/'+str(n_simulation)+'*'+str(round(c,3))+')') 
        plt.figtext(0.40,0.80,'up_and_in_call ='+str(upInCall)+' (='+str(n_in)+'/'+ str(n_simulation)+'*'+str(round(c,3))+')')
#
plt.title('Up-and-out and up-and-in parity (# of simulations = %d ' % n_simulation +')')
plt.xlabel('Total number of steps ='+str(int(n_steps))) 
plt.ylabel('stock price')
plt.show()

对应的图形如下所示。请注意,在前面的程序中,由于使用了种子,若使用相同的种子,不同的用户应该得到相同的图形:

上行出场与上行入场平价图

使用浮动行权价定价回望期权

回望期权取决于标的资产所经历的路径(历史)。因此,它们也被称为路径依赖的奇异期权。其中一种被称为浮动行权价。当行使价格为期权生命周期内达到的最低价格时,给定的看涨期权的收益函数如下所示:

使用浮动行权价定价回望期权

这是该回望期权的 Python 代码:

plt.show()
def lookback_min_price_as_strike(s,T,r,sigma,n_simulation): 
    n_steps=100
    dt=T/n_steps
    total=0
    for j in range(n_simulation): 
        min_price=100000\.  # a very big number 
        sT=s
        for i in range(int(n_steps)): 
            e=sp.random.normal()
            sT*=sp.exp((r-0.5*sigma*sigma)*dt+sigma*e*sp.sqrt(dt)) 
            if sT<min_price:
                min_price=sT
                #print 'j=',j,'i=',i,'total=',total 
                total+=p4f.bs_call(s,min_price,T,r,sigma)
    return total/n_simulation

记住,前面的函数需要两个模块。因此,我们必须在调用函数之前导入这些模块,如下代码所示:

import scipy as sp
import p4f
s=40\.             # today stock price
T=0.5               # maturity in years
r=0.05              # risk-free rate
sigma=0.2           # volatility (annualized)
n_simulation=1000   # number of simulations
result=lookback_min_price_as_strike(s,T,r,sigma,n_simulation)
print('lookback min price as strike = ', round(result,3))

一次运行的结果如下所示:

('lookback min price as strike = ', 53.31)t(

附录 A – 数据案例 7 – 对冲原油

假设一家炼油厂每天使用原油。因此,他们必须面对主要原材料——原油价格的不确定性风险。保护底线和确保生产顺利之间存在权衡;公司研究所有可能的结果,例如是否对冲油价,或者完全不对冲。假设总的年原油消耗量为 2000 万加仑。再次强调,公司每天都必须运营。比较以下几种策略,并指出其优缺点:

  • 不进行对冲

  • 使用期货

  • 使用期权

  • 使用外汇期权

存在几种策略,例如美国式期权;请参见下表中的规格。以下表格展示了部分原油期权合约的规格:

合约单位 在交易所交易的轻质甜油看跌(看涨)期权代表着在交易所交易的轻质甜油期货中持有空头(多头)仓位的期权。
最小价格波动 每桶$0.01。
价格报价 每桶以美元和美分计。
产品代码 CME Globex: LO, CME ClearPort: LO, 清算: LO。
上市合约 当前年份和接下来五个日历年份的每月合约,以及三个额外年份的 6 月和 12 月合约。新日历年的剩余每月合约将在当前年 12 月合约交易终止后加入。
交易终止 交易在相关期货合约的交易终止前三个工作日终止。
交易风格 美国式。
结算方式 可交割。
标的 轻质甜油期货。

表 1:原油期权合约的一些规格

如果我们使用期货进行对冲,我们有以下公式:

附录 A – 数据案例 7 – 对冲原油

N 是期货合约的数量,VA 是我们的投资组合价值(我们想要对冲的金额),β 是基于我们材料和标的工具的回归斜率(注意如果我们的材料与标的对冲工具相同,那么 β 等于 1),VF 是一个期货合约的价值:

参考文献

  • Clewlow,Les 和 Chris Strickland,1997 年《异型期权,技术前沿》,Thomaston Business Press

  • Kurtverstegen《模拟:模拟无相关和相关的随机变量》kurtverstegen.wordpress.com/2013/12/07/simulation/

  • 张,彼得,1998 年《异型期权》,世界科学出版社,第 2 版

练习

  1. 异型期权的定义是什么?

  2. 为什么有人说可赎回债券相当于普通债券加上一种 Bermudan 期权(发行公司是这个 Bermudan 期权的买方,而债券买方是卖方)?

  3. 编写一个 Python 程序,根据算术平均数定价一个亚洲平均价格看跌期权。

  4. 编写一个 Python 程序,根据几何平均数定价一个亚洲平均价格看跌期权。

  5. 编写一个 Python 程序,定价一个向上障碍期权(up-and-in call)。

  6. 编写一个 Python 程序,定价一个向下障碍期权(down-and-out put)。

  7. 编写一个 Python 程序,展示向下障碍期权和向下敲入期权的平价关系。

  8. 编写一个 Python 程序,使用 SciPy 中的permutation()函数,从过去五年的数据中随机选择 12 个月的回报,并且不放回。测试程序时,可以使用花旗银行的数据,时间段为 2009 年 1 月 1 日到 2014 年 12 月 31 日,数据来自 Yahoo Finance。

  9. 编写一个 Python 程序,使用 n 个给定的回报进行自助法。每次选择 m 个回报,其中 m>n。

  10. 在本章中,我们学习了一个简单的 chooser 期权的定价公式:Exercises

    这里,T 是到期日(以年为单位),τ是期权决定选择看涨还是看跌的时间。是否可以使用以下公式?

    Exercises

  11. 当股票支付连续复利的股息,股息收益率为δ时,我们有以下的 Chooser 期权定价公式:Exercises

    其中Pchooser是 chooser 期权的价格或溢价,call (T)是到期时间为 T 的欧洲看涨期权,put(τ)将在后续定义。对于第一个call (T)期权,我们有以下定价公式:

    Exercises

    其中call(T)是看涨期权价格或期权溢价,S是今天的价格,K是执行价格,T 是期权到期时间(以年为单位),σ是波动率,N()是累积分布函数。实际上,这正是 Black-Scholes-Merton 看涨期权模型。看跌期权(τ)的公式如下:

    Exercises

    编写一个相关的 Python 程序。

  12. 如果今天两只股票的价格分别是 $40 和 $55,这两只股票的收益率标准差分别为 0.1 和 0.2。它们的相关系数为 0.45。基于这两只股票终端股价的最大值,彩虹看涨期权的价格是多少?行权价为 $60,期限为六个月,无风险利率为 4.5%。

  13. 解释单变量累积标准正态分布与双变量累积标准正态分布之间的异同。对于单变量累积标准正态分布 N_f() 和双变量累积标准正态分布 N2_f(),我们有以下代码:

    def N_f(x):
        from scipy import stats
        return stats.norm.cdf(x)
    #
    def N2_f(x,y,rho):
        import statsmodels.sandbox.distributions.extras as extras
        muStandardNormal=0.0    # mean of a standard normal distribution 
        varStandardNormal=1.0   # variance of standard normal distribution 
        upper=([x,y])           # upper bound for two values
        v=varStandardNormal     # simplify our notations
        mu=muStandardNormal     # simplify our notations
        covM=([v,rho],[rho,v])
    return extras.mvnormcdf(upper,mu,covM) 
    
  14. 编写一个 Python 程序来定价基于两只相关资产终端价格最大值的看涨期权:练习

    注意

    S1S2d1d2d11d12d21d22N2() 函数的定义在本章中有说明。

  15. 基于蒙特卡洛模拟,编写一个 Python 程序来定价两只相关资产的最小终端价格的卖出期权。

  16. 在本章中,涉及美式期权和百慕大期权的两个程序,其输入集为 s=40x=40T=6./12r=0.05sigma=0.2n=1000T2=(3./12.,4./1);对于潜在的提前行使日期,提供相同的结果。为什么?

  17. 编写一个 Python 程序来定价百慕大卖出期权。

  18. 编写一个 Python 程序来定价基于五个资产最小终端价格的彩虹看涨期权。

总结

我们在第十章中讨论的期权,期权与期货 通常称为普通期权,它们有封闭形式的解,即 Black-Scholes-Merton 期权模型。除了这些普通期权外,还有许多异域期权存在。在本章中,我们讨论了几种类型的异域期权,如百慕大期权、简单选择期权、喊叫期权和二元期权、平均价格期权、上进期权、上出期权、下进期权和下出期权。对于欧式看涨期权,期权买方只能在到期日行使权利,而对于美式期权买方,他们可以在到期日前或到期日当天随时行使权利。百慕大期权可以在到期前的几个时间点行使。

在下一章,我们将讨论各种波动性度量方法,例如我们常用的标准差,下偏标准差LPSD)。将收益的标准差作为风险度量依据了一个关键假设,即股票收益服从正态分布。基于这一点,我们引入了几种正态性检验。此外,我们还通过图示展示了波动性聚集现象——高波动性通常会跟随一个高波动期,而低波动性则通常会跟随一个低波动期。为了解决这一现象,Angel(1982)提出了自回归条件异方差ARCH)过程,Bollerslev(1986)则提出了广义自回归条件异方差GARCH)过程,这些是 ARCH 过程的扩展。它们的图示及相关的 Python 程序将在下一章中讨论。

第十五章:波动率、隐含波动率、ARCH 和 GARCH

在金融中,我们知道风险被定义为不确定性,因为我们无法更准确地预测未来。基于价格服从对数正态分布且回报服从正态分布的假设,我们可以将风险定义为证券回报的标准差或方差。我们称之为传统的波动率(不确定性)定义。由于正态分布是对称的,它将以与负偏差相同的方式处理均值的正偏差。这与我们的传统认知相悖,因为我们通常将它们区分开来。为了解决这一问题,Sortino(1983)提出了下偏标准差。通常假设时间序列的波动率是常数,显然这是不准确的。另一个观察是波动率聚集性,意味着高波动率通常会跟随一个高波动率期,低波动率也通常会跟随一个低波动率期。为了模拟这种模式,Angel(1982)开发了自回归条件异方差ARCH)过程,而 Bollerslev(1986)将其扩展为广义自回归条件异方差GARCH)过程。本章将涵盖以下主题:

  • 传统的波动率度量——标准差——基于正态性假设

  • 正态性和厚尾检验

  • 下偏标准差和 Sortino 比率

  • 两个时期波动率等价性的检验

  • 异方差性检验,Breusch 和 Pagan

  • 波动率微笑和偏度

  • ARCH 模型

  • 模拟 ARCH(1)过程

  • GARCH 模型

  • GARCH 过程的模拟

  • 使用修改版garchSim()模拟 GARCH(p,q)过程

  • Glosten、Jagannathan 和 Runkle 提出的 GJR_GARCH 过程

传统的波动率度量——标准差

在大多数金融教科书中,我们使用回报的标准差作为风险度量。这基于一个关键假设,即对数回报服从正态分布。标准差和方差都可以用来衡量不确定性;前者通常被称为波动率。例如,如果我们说 IBM 的波动率为 20%,这意味着其年化标准差为 20%。以 IBM 为例,以下程序用于估计其年化波动率:

import numpy as np
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
ticker='IBM' 
begdate=(2009,1,1) 
enddate=(2013,12,31)
p =getData(ticker, begdate, enddate,asobject=True, adjusted=True)
ret = p.aclose[1:]/p.aclose[:-1]-1
std_annual=np.std(ret)*np.sqrt(252)
print('volatility (std)=',round(std_annual,4))
('volatility (std)=', 0.2093)

正态性检验

Shapiro-Wilk 检验是一个正态性检验。以下 Python 程序验证 IBM 的回报是否遵循正态分布。测试使用的是来自 Yahoo! Finance 的过去五年的每日数据。零假设是 IBM 的每日回报来自正态分布:

import numpy as np
from scipy import stats
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
#
ticker='IBM' 
begdate=(2009,1,1) 
enddate=(2013,12,31)
p =getData(ticker, begdate, enddate,asobject=True, adjusted=True)
ret = p.aclose[1:]/p.aclose[:-1]-1
#
print('ticker=',ticker,'W-test, and P-value') 
print(stats.shapiro(ret))
 ('ticker=', 'IBM', 'W-test, and P-value')
(0.9295020699501038, 7.266549629954468e-24)

结果的第一个值是检验统计量,第二个值是其对应的 P 值。由于这个 P 值非常接近零,我们拒绝原假设。换句话说,我们得出结论,IBM 的日收益不遵循正态分布。

对于正态性检验,我们也可以使用安德森-达林检验,这是一种科尔莫哥洛夫-斯米尔诺夫检验的改进方法,用于验证观测数据是否遵循特定分布。stats.anderson()函数提供了正态、指数、逻辑和 Gumbel(极值类型 I)分布的检验。默认的检验是正态分布检验。调用该函数并打印测试结果后,我们得到了以下结果:

print(stats.anderson(ret))
AndersonResult(statistic=inf, critical_values=array([ 0.574,  0.654,  0.785,  0.915,  1.089]), significance_level=array([ 15\. ,  10\. ,   5\. ,   2.5,   1\. ]))

在这里,我们有三组值:安德森-达林检验统计量、一组临界值和一组对应的置信水平,如 15%、10%、5%、2.5%和 1%,如前面的输出所示。如果我们选择 1%的置信水平——第三组的最后一个值——则临界值为 1.089,即第二组的最后一个值。由于我们的检验统计量是 14.73,远高于临界值 1.089,因此我们拒绝原假设。因此,我们的安德森-达林检验得出的结论与我们的 Shapiro-Wilk 检验相同。

估计肥尾

正态分布的一个重要性质是,我们可以使用均值和标准差,即前两个矩,来完全定义整个分布。对于 n 个证券收益,其前四个矩在方程式(1)中定义。均值或平均值定义如下:

估计肥尾

它的(样本)方差由以下公式定义。标准差,即σ,是方差的平方根:

估计肥尾

以下公式定义的偏度表示分布是偏向左还是偏向右。对于对称分布,其偏度为零:

估计肥尾

峰度反映了极值的影响,因为它的四次方特性。峰度有两种定义方式,一种是带有减三的定义,另一种是不带减三的定义;请参见以下两个方程。方程式(4B)中减去三的原因是,对于正态分布,基于方程式(4A)的峰度为三:

估计肥尾

有些书籍通过称方程式(4B)为超额峰度来区分这两个方程。然而,许多基于方程式(4B)的函数仍然被称为峰度。因为我们知道标准正态分布具有零均值、单位标准差、零偏度和零峰度(基于方程式 4B)。以下输出确认了这些事实:

import numpy as np
from scipy import stats, random
#
random.seed(12345)
ret=random.normal(0,1,50000)
print('mean =',np.mean(ret))
print('std =',np.std(ret))
print('skewness=',stats.skew(ret))
print('kurtosis=',stats.kurtosis(ret))
('mean =', -0.0018105809899753157)
('std =', 1.002778144574481)
('skewness=', -0.014974456637295455)
('kurtosis=', -0.03657086582842339)

均值、偏度和峰度都接近零,而标准差接近一。接下来,我们基于 S&P500 的日收益率估计四个矩,具体如下:

import numpy as np
from scipy import stats
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
#
ticker='^GSPC' 
begdate=(1926,1,1)
enddate=(2013,12,31)
p = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
ret = p.aclose[1:]/p.aclose[:-1]-1
print( 'S&P500    n    =',len(ret))
print( 'S&P500    mean    =',round(np.mean(ret),8)) 
print( 'S&P500    std    =',round(np.std(ret),8)) 
print( 'S&P500    skewness=',round(stats.skew(ret),8))
print( 'S&P500    kurtosis=',round(stats.kurtosis(ret),8))

以下是前面代码中提到的五个值的输出,包括观察次数:

('S&P500\tn\t=', 16102)
('S&P500\tmean\t=', 0.00033996)
('S&P500\tstd\t=', 0.00971895)
('S&P500\tskewness=', -0.65037674)
('S&P500\tkurtosis=', 21.24850493)

该结果与题为Cook Pine Capital 的脂尾风险研究的论文中的结果非常接近,论文的 PDF 版本可以从www.cookpinecapital.com/assets/pdfs/Study_of_Fat-tail_Risk.pdf下载。或者,可以从www3.canisius.edu/~yany/doc/Study_of_Fat-tail_Risk.pdf获得。使用相同的论点,我们得出结论:标准普尔 500 的每日收益呈现左偏,即负偏态,并且具有脂尾(峰度为 38.22,而非零)。

下偏标准差和 Sortino 比率

我们已经讨论过这个概念。然而,为了完整起见,本章再次提及它。使用收益的标准差作为风险度量存在一个问题,即正偏差也被视为坏的。第二个问题是偏差是相对于平均值而非固定基准(如无风险利率)。为了克服这些缺点,Sortino(1983)提出了下偏标准差,它被定义为相对于无风险利率的负超额收益条件下的平方偏差的平均值,如下公式所示:

下偏标准差和 Sortino 比率

因为我们在此公式中需要无风险利率,所以可以生成一个包含无风险利率作为时间序列的 Fama-French 数据集。首先,从mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html下载他们的日常因子数据。然后,解压文件并删除文本文件末尾的非数据部分。假设最终的文本文件保存在C:/temp/

import datetime
import numpy as np
import pandas as pd 
file=open("c:/temp/ffDaily.txt","r") 
data=file.readlines()
f=[]
index=[]
#
for i in range(5,np.size(data)): 
    t=data[i].split() 
    t0_n=int(t[0]) 
    y=int(t0_n/10000) 
    m=int(t0_n/100)-y*100 
    d=int(t0_n)-y*10000-m*100
    index.append(datetime.datetime(y,m,d)) 
    for j in range(1,5):
         k=float(t[j]) 
         f.append(k/100)
#
n=len(f) 
f1=np.reshape(f,[n/4,4])
ff=pd.DataFrame(f1,index=index,columns=['Mkt_Rf','SMB','HML','Rf'])
ff.to_pickle("c:/temp/ffDaily.pkl")

最终数据集的名称是ffDaily.pkl。最好是自己生成这个数据集。不过,也可以从canisius.edu/~yany/python/ffDaily.pkl下载该数据集。使用过去五年的数据(2009 年 1 月 1 日到 2013 年 12 月 31 日),我们可以估算 IBM 的 LPSD,如下所示:

import numpy as np
import pandas as pd 
from scipy import stats
from matplotlib.finance import quotes_historical_yahoo_ochl as getData 
#
ticker='IBM' 
begdate=(2009,1,1) 
enddate=(2013,12,31)
p =getData(ticker, begdate, enddate,asobject=True, adjusted=True)
ret = p.aclose[1:]/p.aclose[:-1]-1
date_=p.date
x=pd.DataFrame(data=ret,index=date_[1:],columns=['ret']) 
#
ff=pd.read_pickle('c:/temp/ffDaily.pkl') 
final=pd.merge(x,ff,left_index=True,right_index=True) 
#
k=final.ret-final.RF
k2=k[k<0] 
LPSD=np.std(k2)*np.sqrt(252)
print("LPSD=",LPSD)
print(' LPSD (annualized) for ', ticker, 'is ',round(LPSD,3))

以下输出显示 IBM 的 LPSD 为 14.8%,与上一节中显示的 20.9%差异较大:

('LPSD=', 0.14556051947047091)
(' LPSD (annualized) for ', 'IBM', 'is ', 0.146)

测试两个时期波动性是否相等

我们知道,1987 年 10 月股市大幅下跌。我们可以选择一只股票来测试 1987 年 10 月前后波动性。例如,我们可以使用福特汽车公司,股票代码为 F,来说明如何测试 1987 年股市崩盘前后的方差是否相等。在以下 Python 程序中,我们定义了一个名为ret_f()的函数,用于从 Yahoo! Finance 获取每日价格数据并估算其每日收益:

import numpy as np
import scipy as sp
import pandas as pd
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
# input area
ticker='F'            # stock
begdate1=(1982,9,1)   # starting date for period 1 
enddate1=(1987,9,1)   # ending date for period   1 
begdate2=(1987,12,1)  # starting date for period 2 
enddate2=(1992,12,1)  # ending   date for period 2
#
# define a function
def ret_f(ticker,begdate,enddate):
    p =getData(ticker, begdate, enddate,asobject=True, adjusted=True)
    ret = p.aclose[1:]/p.aclose[:-1]-1 
    date_=p.date
    return pd.DataFrame(data=ret,index=date_[1:],columns=['ret'])
#
# call the above function twice 
ret1=ret_f(ticker,begdate1,enddate1) 
ret2=ret_f(ticker,begdate2,enddate2)
#
# output
print('Std period #1    vs. std period #2') 
print(round(sp.std(ret1.ret),6),round(sp.std(ret2.ret),6)) 
print('T value ,    p-value ') 
print(sp.stats.bartlett(ret1.ret,ret2.ret))

以下截图中极高的 T 值和接近零的 p 值表明拒绝假设:在这两个时间段内,股票的波动率相同。相应的输出如下:

Std period #1   vs. std period #2
(0.01981, 0.017915)
T value ,       p-value 
BartlettResult(statistic=12.747107745102099, pvalue=0.0003565601014515915)

异方差性检验,Breusch 和 Pagan

Breusch 和 Pagan(1979)设计了一种检验方法,用于确认或拒绝回归残差同质性(即波动率为常数)的零假设。以下公式表示他们的逻辑。首先,我们进行yx的线性回归:

异方差性检验,Breusch 和 Pagan

这里,y是因变量,x是自变量,α是截距,β是回归系数,异方差性检验,Breusch 和 Pagan是误差项。在获得误差项(残差)后,我们运行第二次回归:

异方差性检验,Breusch 和 Pagan

假设运行先前回归得到的拟合值为t f,则 Breusch-Pagan(1979)检验的公式如下,并且它服从χ2分布,具有k自由度:

异方差性检验,Breusch 和 Pagan

以下示例来自一个名为lm.test(线性回归检验)的 R 包,其作者为 Hothorn 等人(2014)。我们生成了xy1y2的时间序列。自变量是x,因变量是y1y2。根据我们的设计,y1是同质的,即方差(标准差)为常数,而y2是非同质的(异方差的),即方差(标准差)不是常数。对于变量x,我们有以下 100 个值:

异方差性检验,Breusch 和 Pagan

然后,我们生成两个误差项,每个误差项包含 100 个随机值。对于error1,它的 100 个值来自标准正态分布,即均值为零、标准差为 1。对于error2,它的 100 个值来自均值为零、标准差为 2 的正态分布。y1y2时间序列定义如下:

异方差性检验,Breusch 和 Pagan

对于y2的奇数项,误差项来自error1,而对于偶数项,误差项来自error2。要获取与lm.test相关的 PDF 文件或 R 包的更多信息,按照以下六个步骤操作:

  1. 访问www.r-project.org

  2. 下载下点击CRAN,然后点击Packages

  3. 选择一个靠近的服务器。

  4. 点击屏幕左侧的Packages

  5. 选择一个列表并搜索lm.test

  6. 点击链接并下载与lm.test相关的 PDF 文件。

以下是相关的 Python 代码:

import numpy as np
import scipy as sp
import statsmodels.api as sm 
#
def breusch_pagan_test(y,x): 
    results=sm.OLS(y,x).fit() 
    resid=results.resid
    n=len(resid)
    sigma2 = sum(resid**2)/n 
    f = resid**2/sigma2 - 1
    results2=sm.OLS(f,x).fit() 
    fv=results2.fittedvalues 
    bp=0.5 * sum(fv**2) 
    df=results2.df_model
    p_value=1-sp.stats.chi.cdf(bp,df)
    return round(bp,6), df, round(p_value,7)
#
sp.random.seed(12345) 
n=100
x=[]
error1=sp.random.normal(0,1,n) 
error2=sp.random.normal(0,2,n) 
for i in range(n):
    if i%2==1:
        x.append(1) 
    else:
        x.append(-1)
#
y1=x+np.array(x)+error1 
y2=sp.zeros(n)
#
for i in range(n): 
    if i%2==1:
        y2[i]=x[i]+error1[i] 
    else:
        y2[i]=x[i]+error2[i]

print ('y1 vs. x (we expect to accept the null hypothesis)') 
bp=breusch_pagan_test(y1,x)
#
print('BP value,    df,    p-value') 
print 'bp =', bp 
bp=breusch_pagan_test(y2,x)
print ('y2 vs. x    (we expect to rject the null hypothesis)') 
print('BP value,    df,    p-value')
print('bp =', bp)

从使用y1x进行回归分析的结果来看,我们知道其残差值是齐次的,即方差或标准差是常数。因此,我们预计接受原假设。相反,对于y2x的回归分析,由于我们的设计,y2的误差项是异质的。因此,我们预计会拒绝原假设。相应的输出如下所示:

y1 vs. x (we expect to accept the null hypothesis)
BP value,       df,     p-value
bp = (0.596446, 1.0, 0.5508776)
y2 vs. x        (we expect to rject the null hypothesis)
BP value,       df,     p-value
('bp =', (17.611054, 1.0, 0.0))

波动率微笑与偏度

显然,每只股票应该只有一个波动率。然而,在估算隐含波动率时,不同的行使价格可能会给我们不同的隐含波动率。更具体地说,基于虚值期权、平值期权和实值期权的隐含波动率可能会有很大差异。波动率微笑是在行使价格范围内先下降后上升的形态,而波动率偏度则是向下或向上的倾斜。关键在于,投资者的情绪以及供需关系对波动率偏度有着根本的影响。因此,这种微笑或偏度为我们提供了有关投资者(如基金经理)是否偏好写入看涨期权或看跌期权的信息。首先,我们访问雅虎财经网站,下载看涨期权和看跌期权的数据:

  1. 访问finance.yahoo.com

  2. 输入一个代码,比如IBM

  3. 点击期权(Options)选项。

  4. 复制并粘贴看涨期权和期权的数据。

  5. 将它们分成两个文件。

如果读者使用的是 2017 年 3 月 17 日到期的数据,可以从作者的网站下载:canisius.edu/~yany/data/calls17march.txtcanisius.edu/~yany/data/puts17march.txt

看涨期权的 Python 程序如下所示:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
infile="c:/temp/calls17march.txt"
data=pd.read_table(infile,delimiter='\t',skiprows=1)
x=data['Strike']
y0=list(data['Implied Volatility'])
n=len(y0)
y=[]
for i in np.arange(n):
    a=float(y0[i].replace("%",""))/100.
    y.append(a)
    print(a)
#
plt.title("Volatility smile")
plt.figtext(0.55,0.80,"IBM calls")
plt.figtext(0.55,0.75,"maturity: 3/17/2017")
plt.ylabel("Volatility")
plt.xlabel("Strike Price")
plt.plot(x,y,'o')
plt.show()

在前面的程序中,输入文件用于看涨期权。这里展示的是波动率微笑的图形。另一个截图基于隐含波动率与行使(执行)价格之间的关系。程序与前面的程序完全相同,唯一不同的是输入文件。章节末尾有一个数据案例与前面的程序相关。接下来的图像是基于看涨期权数据的波动率微笑:

波动率微笑与偏度

基于看涨期权数据的波动率微笑

类似地,下一个波动率微笑图像是基于看跌期权数据:

波动率微笑与偏度

波动性聚集的图形呈现

其中一个观察结果被标注为波动性聚集,这意味着高波动性通常会跟随一个高波动性的时期,而低波动性通常会跟随一个低波动性的时期。以下程序通过使用 1988 到 2006 年的标准普尔 500 每日收益数据展示了这一现象。请注意,在以下代码中,为了在x轴上显示 1988 年,我们在 1988 年前添加了几个月的数据:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
ticker='^GSPC'
begdate=(1987,11,1)
enddate=(2006,12,31)
#
p = getData(ticker, begdate, enddate,asobject=True, adjusted=True)
x=p.date[1:] 
ret = p.aclose[1:]/p.aclose[:-1]-1
#
plt.title('Illustration of volatility clustering (S&P500)') 
plt.ylabel('Daily returns')
plt.xlabel('Date') 
plt.plot(x,ret)
plt.show()

该程序的灵感来自于马尔滕·P·维瑟绘制的图表;请参阅pure.uva.nl/ws/files/922823/67947_09.pdf。对应于前面代码的图表如下所示:

波动率聚类的图示

ARCH 模型

根据前述的论点,我们知道股票收益的波动性或方差不是恒定的。根据 ARCH 模型,我们可以使用前次估算的误差项来帮助我们预测下一期的波动性或方差。这个模型是由 2003 年诺贝尔经济学奖获得者罗伯特·F·恩格尔提出的。ARCH (q) 模型的公式如下所示:

ARCH 模型

这里,ARCH 模型是时刻 t 的方差,i是第 i 个系数,ARCH 模型t-i时期的平方误差项,q是误差项的阶数。当q为 1 时,我们得到最简单的 ARCH(1)过程,如下所示:

ARCH 模型

模拟 ARCH(1)过程

模拟一个 ARCH(1)过程以更好地理解波动率聚类是一个好主意,这意味着高波动期通常会跟随高波动期,而低波动期通常会跟随低波动期。以下代码反映了这一现象:

import scipy as sp 
import matplotlib.pyplot as plt
#
sp.random.seed(12345)
n=1000        # n is the number of observations
n1=100        # we need to drop the first several observations 
n2=n+n1       # sum of two numbers
#
a=(0.1,0.3)   # ARCH (1) coefficients alpha0 and alpha1, see Equation (3)
errors=sp.random.normal(0,1,n2) 
t=sp.zeros(n2)
t[0]=sp.random.normal(0,sp.sqrt(a[0]/(1-a[1])),1) 
for i in range(1,n2-1):
    t[i]=errors[i]*sp.sqrt(a[0]+a[1]*t[i-1]**2) 
    y=t[n1-1:-1] # drop the first n1 observations 
#
plt.title('ARCH (1) process')
x=range(n) 
plt.plot(x,y)
plt.show()

从下图可以看出,确实高波动期通常会跟随高波动,而低波动聚类也是如此:

模拟 ARCH(1)过程

GARCH 模型

广义自回归条件异方差 (GARCH) 是 ARCH 的一个重要扩展,由 Bollerslev(1986 年)提出。GARCH (p,q) 过程定义如下:

GARCH 模型

这里,GARCH 模型是时刻t的方差,q是误差项的阶数,p 是方差的阶数,GARCH 模型是常数,GARCH 模型是时刻t-i的误差项系数,GARCH 模型是时刻t-i的方差系数。显然,最简单的 GARCH 过程是当pq都设为 1 时,即 GARCH(1,1),其公式如下:

GARCH 模型

模拟 GARCH 过程

基于与 ARCH(1)相关的前一个程序,我们可以模拟一个 GARCH(1,1)过程,如下所示:

import scipy as sp 
import matplotlib.pyplot as plt
#
sp.random.seed(12345)
n=1000          # n is the number of observations
n1=100          # we need to drop the first several observations 
n2=n+n1         # sum of two numbers
#
a=(0.1,0.3)     # ARCH coefficient
alpha=(0.1,0.3)    # GARCH (1,1) coefficients alpha0 and alpha1, see Equation (3)
beta=0.2 
errors=sp.random.normal(0,1,n2) 
t=sp.zeros(n2)
t[0]=sp.random.normal(0,sp.sqrt(a[0]/(1-a[1])),1)
#
for i in range(1,n2-1): 
    t[i]=errors[i]*sp.sqrt(alpha[0]+alpha[1]*errors[i-1]**2+beta*t[i-1]**2)
#
y=t[n1-1:-1]    # drop the first n1 observations 
plt.title('GARCH (1,1) process')
x=range(n) 
plt.plot(x,y)
plt.show()

老实说,下面的图表在 ARCH(1)过程下与之前的图表非常相似。对应于前面代码的图表如下所示:

模拟 GARCH 过程

Fig15_04_garch.png

使用修改过的 garchSim()模拟 GARCH(p,q)过程

以下代码基于名为garchSim()的 R 函数,该函数包含在名为fGarch的 R 包中。fGarch的作者是 Diethelm Wuertz 和 Yohan Chalabi。要查找相关手册,执行以下步骤:

  1. 访问 www.r-project.org

  2. 下载下点击CRAN,然后点击

  3. 选择一个附近的服务器。

  4. 在屏幕左侧点击

  5. 选择一个列表并搜索fGarch

  6. 点击链接下载与fGarch相关的 PDF 文件。

基于 R 程序的 Python 程序如下所示:

import scipy as sp
import numpy as np
import matplotlib.pyplot as plt
#
sp.random.seed(12345) 
m=2
n=100              # n is the number of observations
nDrop=100          # we need to drop the first several observations 
delta=2
omega=1e-6 
alpha=(0.05,0.05)
#
beta=0.8 
mu,ma,ar=0.0,0.0,0.0
gamma=(0.0,0.0) 
order_ar=sp.size(ar) 
order_ma=sp.size(ma) 
order_beta=sp.size(beta)
#
order_alpha =sp.size(alpha) 
z0=sp.random.standard_normal(n+nDrop) 
deltainv=1/delta 
spec_1=np.array([2])
spec_2=np.array([2])
spec_3=np.array([2])
z = np.hstack((spec_1,z0)) 
t=np.zeros(n+nDrop)
h = np.hstack((spec_2,t)) 
y = np.hstack((spec_3,t)) 
eps0 = h**deltainv  * z
for i in range(m+1,n +nDrop+m-1):
    t1=sum(alpha[::-1]*abs(eps0[i-2:i]))    # reverse 
    alpha =alpha[::-1] 
    t2=eps0[i-order_alpha-1:i-1]
    t3=t2*t2 
    t4=np.dot(gamma,t3.T)
    t5=sum(beta* h[i-order_beta:i-1]) 
    h[i]=omega+t1-t4+ t5
    eps0[i] = h[i]**deltainv * z[i] 
    t10=ar * y[i-order_ar:i-1] 
    t11=ma * eps0[i -order_ma:i-1]
    y[i]=mu+sum(t10)+sum(t11)+eps0[i] 
    garch=y[nDrop+1:] 
    sigma=h[nDrop+1:]**0.5 
    eps=eps0[nDrop+1:] 
    x=range(1,len(garch)+1) 
#
plt.plot(x,garch,'r')
plt.plot(x,sigma,'b')
plt.title('GARCH(2,1) process')
plt.figtext(0.2,0.8,'omega='+str(omega)+', alpha='+str(alpha)+',beta='+str(beta))
plt.figtext(0.2,0.75,'gamma='+str(gamma)) 
plt.figtext(0.2,0.7,'mu='+str(mu)+', ar='+str(ar)+',ma='+str(ma)) 
plt.show()

在前面的程序中,omega 是方程(10)中的常数,alpha 与误差项相关,beta 与方差相关。alpha[a,b]中的 a 对应t-1,b 对应t-2。然而,eps0[t-2:i]表示的是t-2t-1alphaeps0项不一致,因此我们必须反转ab的顺序。这就是为什么我们使用alpha[::-1]的原因。由于一些值为零,如muarma,GARCH 的时间序列与eps是相同的。因此,我们只在下面的图表中展示了两个时间序列。GARCH 的高波动性与另一个标准差的时间序列:

使用修改后的 garchSim() 模拟 GARCH (p,q) 过程

Fig15_05_two.png

GJR_GARCH 由 Glosten、Jagannanthan 和 Runkle 提出

Glosten、Jagannanthan 和 Runkle(1993)在 GARCH 过程模型中引入了非对称性。GJR_GARCH(1,1,1)的格式如下:

Glosten、Jagannanthan 和 Runkle 的 GJR_GARCH

在这里,条件It-1=0,如果Glosten、Jagannanthan 和 Runkle 的 GJR_GARCH,并且It-1=1如果Glosten、Jagannanthan 和 Runkle 的 GJR_GARCH成立。以下代码基于 Kevin Sheppard 编写的代码:

import numpy as np
from numpy.linalg import inv
import matplotlib.pyplot as plt
from matplotlib.mlab import csv2rec
from scipy.optimize import fmin_slsqp 
from numpy import size, log, pi, sum, diff, array, zeros, diag, dot, mat, asarray, sqrt
#
def gjr_garch_likelihood(parameters, data, sigma2, out=None): 
    mu = parameters[0]
    omega = parameters[1] 
    alpha = parameters[2] 
    gamma = parameters[3] 
    beta = parameters[4]
    T = size(data,0)
    eps = data-mu
    for t in xrange(1,T):
        sigma2[t]=(omega+alpha*eps[t-1]**2+gamma*eps[t-1]**2*(eps[t- 1]<0)+beta*sigma2[t-1])
        logliks = 0.5*(log(2*pi) + log(sigma2) + eps**2/sigma2) 
    loglik = sum(logliks)
    if out is None: 
        return loglik
    else:
        return loglik, logliks, copy(sigma2)
#
def gjr_constraint(parameters,data, sigma2, out=None):
    alpha = parameters[2]
    gamma = parameters[3] 
    beta = parameters[4]
    return array([1-alpha-gamma/2-beta]) # Constraint alpha+gamma/2+beta<=1
#
def hessian_2sided(fun, theta, args): 
    f = fun(theta, *args)
    h = 1e-5*np.abs(theta) 
    thetah = theta + h
    h = thetah-theta 
    K = size(theta,0) 
    h = np.diag(h)
    fp = zeros(K) 
    fm = zeros(K)
    for i in xrange(K):
        fp[i] = fun(theta+h[i], *args) 
        fm[i] = fun(theta-h[i], *args)
        fpp = zeros((K,K))
        fmm = zeros((K,K)) 
    for i in xrange(K):
        for j in xrange(i,K):
            fpp[i,j] = fun(theta + h[i] + h[j], *args) 
            fpp[j,i] = fpp[i,j]
            fmm[i,j] = fun(theta-h[i]-h[j], *args) 
            fmm[j,i] = fmm[i,j]
            hh = (diag(h))
            hh = hh.reshape((K,1))
            hh = dot(hh,hh.T)
            H = zeros((K,K)) 
    for i in xrange(K):
        for j in xrange(i,K):
            H[i,j] = (fpp[i,j]-fp[i]-fp[j] + f+ f-fm[i]-fm[j] + fmm[i,j])/hh[i,j]/2
            H[j,i] = H[i,j]
    return H

我们可以通过包含所有初始值、约束和边界来编写一个名为GJR_GARCH()的函数,如下所示:

def GJR_GARCH(ret): 
    import numpy as np
    import scipy.optimize as op 
    startV=np.array([ret.mean(),ret.var()*0.01,0.03,0.09,0.90])
    finfo=np.finfo(np.float64)
    t=(0.0,1.0)
    bounds=[(-10*ret.mean(),10*ret.mean()),(finfo.eps,2*ret.var()),t,t,t] 
    T=np.size(ret,0)
    sigma2=np.repeat(ret.var(),T) 
    inV=(ret,sigma2)
    return op.fmin_slsqp(gjr_garch_likelihood,startV,f_ieqcons=gjr_constraint,bounds=bounds,args=inV)
#

为了复制我们的结果,我们可以使用random.seed()函数来固定通过从均匀分布生成一组随机数得到的回报:

sp.random.seed(12345) 
returns=sp.random.uniform(-0.2,0.3,100) 
tt=GJR_GARCH(returns)

以下表格给出了这五个输出的解释:

# 含义
1 描述优化器退出模式的消息
2 目标函数的最终值
3 迭代次数
4 函数评估
5 梯度评估

表 15.1 五个输出的定义

各种退出模式的描述列在下表中:

退出代码 描述
-1 需要评估梯度(g 和 a)
0 优化成功终止
1 需要评估函数(f 和 c)
2 独立变量比约束条件更多
3 LSQ 子问题中超过 3*n 次迭代
4 不兼容的约束条件
5 LSQ 子问题中的奇异矩阵 E
6 LSQ 子问题中的奇异矩阵 C
7 秩缺失的等式约束子问题 HFTI
8 线搜索的正方向导数
9 超过迭代限制

表 15.2 退出模式

为了展示我们的最终参数值,我们使用以下代码输出结果:

print(tt)
Optimization terminated successfully.    (Exit mode 0)
            Current function value: -54.0664733128
            Iterations: 12
            Function evaluations: 94
            Gradient evaluations: 12
[  7.73958251e-02   6.65706323e-03   0.00000000e+00   2.09662783e-12
   6.62024107e-01]

参考文献

正态分布的一个重要属性是我们可以使用均值和标准差。

Engle, Robert, 2002, 动态条件相关性 – 一类简单的多元 GARCH 模型即将发表于《商业与经济统计学杂志》pages.stern.nyu.edu/~rengle/dccfinal.pdf

附录 A – 数据案例 8 - 使用 VIX 看涨期权进行投资组合对冲

CBOE 波动率指数 (VIX) 基于 S&P500 指数 (SPX),这是美国股市的核心指数,通过对 SPX 的看跌和看涨期权在多个行使价格区间内的加权价格进行平均,来估计预期波动率。

通过提供复制波动率敞口的 SPX 期权投资组合脚本,这一新方法将 VIX 从一个抽象概念转变为实际的交易和对冲波动率的标准。

在 2014 年,CBOE 增强了 VIX 指数,包括了 SPX 周期期权系列的计算。SPX 周期期权的纳入,使得 VIX 指数能够使用最能精确匹配 VIX 指数预期波动率目标时间框架(即 30 天)的 S&P500 指数期权系列进行计算。使用到期日大于 23 天且小于 37 天的 SPX 期权,确保了 VIX 指数始终反映 S&P 500 波动率期限结构中两个点的插值。

参考文献

www.theoptionsguide.com/portfolio-hedging-using-vix-calls.aspx

www.cboe.com/micro/vix/historical.aspx

www.tickdata.com/tick-data-adds-vix-futures-data/

附录 B – 数据案例 8 - 波动率微笑及其含义

该数据案例有多个目标:

  • 理解隐含波动率的概念

  • 了解不同的行使(行权)价格会导致隐含波动率的不同。

  • 学会如何处理数据并生成相关图表

  • 波动率微笑有什么含义?

数据来源:Yahoo! Finance:

  1. 访问 finance.yahoo.com

  2. 输入一个股票代码,例如 IBM

  3. 点击 Options(期权)在中心位置。

  4. 复制并粘贴看涨期权和期权的数据。

  5. 将它们分成两个文件。

针对以下公司:

公司名称 股票代码 戴尔公司 DELL
国际商用机器公司 IBM 通用电气 GE
微软 MSFT 谷歌 GOOG
家庭美元商店 FDO 苹果 AAPL
沃尔玛商店 WMT eBay EBAY
麦当劳 MCD

注意,每只股票都有多个到期日;请参见以下截图:

附录 B – 数据案例 8 - 波动率微笑及其含义

这里展示了一个示例 Python 程序,输入文件可以从作者的网站下载:canisius.edu/~yany/data/calls17march.txt

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
infile="c:/temp/calls17march.txt"
data=pd.read_table(infile,delimiter='\t',skiprows=1)
x=data['Strike']
y0=list(data['Implied Volatility'])
n=len(y0)
y=[]
for i in np.arange(n):
    a=float(y0[i].replace("%",""))/100.
    y.append(a)
    print(a)
#
plt.title("Volatility smile")
plt.figtext(0.55,0.80,"IBM calls")
plt.figtext(0.55,0.75,"maturity: 3/17/2017")
plt.ylabel("Volatility")
plt.xlabel("Strike Price")
plt.plot(x,y,'o')
plt.show()

练习

  1. 什么是波动率的定义?

  2. 你如何衡量风险(波动率)?

  3. 与广泛使用的风险(标准差)定义相关的问题是什么?

  4. 你如何测试股票收益是否遵循正态分布?对于以下给定的一组股票,测试它们是否遵循正态分布:

    公司名称 股票代码 戴尔公司 DELL
    国际商业机器公司 IBM 通用电气 GE
    微软 MSFT 谷歌 GOOG
    家庭美元商店 FDO 苹果 AAPL
    沃尔玛 WMT eBay EBAY
    麦当劳 MCD
  5. 什么是下偏标准差?它的应用是什么?

  6. 选择五只股票,例如戴尔、IBM、微软、花旗集团和沃尔玛,并根据过去三年的日数据,将它们的标准差与 LPSD 进行比较。

  7. 股票的波动率是否在多年间保持不变?你可以选择国际商业机器公司IBM)和沃尔玛WMT)来验证你的假设。

  8. 什么是 ARCH (1) 过程?

  9. 什么是 GARCH (1,1) 过程?

  10. 将 GARCH (1,1) 过程应用于 IBM 和 WMT。

  11. 编写一个 Python 程序,展示结合了看涨和看跌期权的波动率微笑。

  12. 编写一个 Python 程序,通过不同到期日绘制波动率微笑。换句话说,将多个微笑放在一起。

  13. 使用 Breusch-Pagan (1979) 检验确认或拒绝 IBM 日收益率同质性的假设。

  14. 你如何测试股票的波动率是否恒定?

  15. 胖尾是什么意思?为什么我们应该关注胖尾?

  16. 你能编写一个 Python 程序来下载期权数据吗?

  17. 你如何下载所有到期日的数据?

总结

在本章中,我们集中讨论了几个问题,特别是波动率度量和 ARCH/GARCH 模型。对于波动率度量,首先我们讨论了广泛使用的标准差,它是基于正态性假设的。为了展示这种假设可能不成立,我们介绍了几种正态性检验方法,如 Shapiro-Wilk 检验和 Anderson-Darling 检验。为了展示许多股票的实际分布在基准正态分布下具有厚尾特征,我们生动地使用了各种图形来说明这一点。为了说明波动率可能不是常数,我们展示了用于比较两个时期方差的检验。接着,我们展示了一个 Python 程序来进行 Breusch-Pagan(1979)异方差性检验。ARCH 和 GARCH 模型广泛用于描述波动率随时间的演变。对于这些模型,我们模拟了它们的简单形式,如 ARCH(1)和 GARCH(1,1)过程。除了它们的图形表示外,还包括了 Kevin Sheppard 的 Python 代码,用于求解 GJR_GARCH(1,1,1)过程。

posted @ 2025-01-21 21:16  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报