R-量化金融学习指南-一-
R 量化金融学习指南(一)
原文:
zh.annas-archive.org/md5/30052aed3cdb197eebabc0579f7ffc2a
译者:飞龙
前言
用 R 学习量化金融 解释了量化金融在统计语言 R 中的实际应用示例。本书的写作目的是将知识传递给那些有兴趣用 R 学习量化金融的人。在本书中,我们涵盖了从基础到高级的各种主题。特别是,我们介绍了统计学、时间序列和小波分析,以及它们在算法交易中的应用。我们还尽力解释了本书中机器学习、风险管理、优化和期权定价的一些应用。
本书内容概览
第一章,R 语言简介,解释了 R 语言的基本命令。内容从 R 及其包的安装开始,接着讲解数据类型、数据框和循环。本章还涵盖了如何编写和调用函数,以及如何将各种格式的数据文件导入 R。此章节旨在为读者提供 R 语言的基础理解。
第二章,统计建模,讲解了探索性分析,如常见分布、相关性、集中趋势测量、异常值检测等,以更好地理解数据。还讨论了数据的抽样、标准化/归一化,这些都帮助准备数据以进行分析。此外,本章还涉及假设检验和参数估计。
第三章,计量经济学与小波分析,介绍了简单和多变量线性回归模型,这些是每个分析的基础。本章还解释了方差分析(ANOVA)和特征选择,并对小波分析模型进行了构建。
第四章,时间序列建模,本章中作者展示了如何使用 ts、zoo 和 xts 将数据转换为时间序列,这些是预测模型的基础。然后,作者讲解了各种预测技术,如 AR、ARIMA、GARCH、VGARCH 等,并通过 R 语言进行演示。
第五章,算法交易,包括了一些来自算法交易领域的实时示例,如动量交易和配对交易,使用了多种方法。本章还涵盖了 CAPM、多因子模型和投资组合构建等内容。
第六章,使用机器学习进行交易,展示了如何使用资本市场数据建模机器学习算法。内容涵盖了有监督和无监督算法。
第七章,风险管理,在本章中,作者讨论了衡量市场和投资组合风险的技术。他还介绍了用于计算 VAR 的常见方法,并给出了银行领域中用于衡量信用风险的最佳实践示例。
第八章,优化,在本章中,作者展示了金融领域中一些优化技术的示例,如动态再平衡、前向测试、网格测试和遗传算法。
第九章,衍生品定价,R 在衍生品定价中的应用案例。本章涵盖了普通期权定价、奇异期权、债券定价、信用利差和信用违约掉期等内容。本章内容较为复杂,需要读者具备一定的衍生品基础知识。
本书所需的内容
首先,您需要确保 R 已安装在您的机器上。本书中的所有示例都已在 R 中实现,并可以在 R 控制台上执行。R 是一个开源平台,任何操作系统都可以免费下载并安装,网址为www.r-project.org/
。安装指南也可以在该网站找到。一旦您在机器上安装了 R,就可以直接前往第一章开始学习。每一章都会介绍所需的包,展示如何安装包,并指导读者如何将其加载到工作区。
本书适合谁阅读
本书的目的是向有兴趣学习 R 及其在分析中的应用的人传授知识。然而,我们选择了金融领域中的一些示例。本书覆盖了从基础到复杂的金融示例,并且涵盖了不同复杂度的 R 编程。本书不要求您具备 R 编程的先前知识,但希望您具备一定的数学分析概念知识。即使您已经熟悉 R,本书依然对您有帮助,因为它解释了来自数据分析行业的各种实际示例,特别是资本市场。
约定
在本书中,您将看到多种不同的文本样式,用于区分不同类型的信息。以下是这些样式的一些示例,并解释其含义。
文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户名等都会以如下方式显示:quantmod
包被使用了很多次。
代码块如下所示:
>getSymbols("^DJI",src="img/yahoo")
>dji<- DJI[,"DJI.Close"]
当我们希望将您的注意力集中在代码块的某个特定部分时,相关的行或项目会以粗体显示:
corr<- rollapply(data,252,correlation ,by.column=FALSE)
对于我们使用的任何 R 命令,我们都使用 >
,这意味着该命令已在命令提示符中输入,因为 >
表示命令提示符。
新术语和重要词汇以粗体显示。例如,您在屏幕上看到的单词,如菜单或对话框中的内容,将以这样的形式出现在文本中:“点击下一步按钮可以将您带到下一个屏幕。”
注意
警告或重要提示将以类似这样的框显示。
提示
小贴士和技巧将以这样的形式显示。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对本书的看法——您喜欢或不喜欢的部分。读者反馈对我们来说至关重要,因为它帮助我们开发出您真正能够从中受益的书籍。如果您想发送一般反馈,只需通过电子邮件发送到 feedback@packtpub.com,并在邮件主题中提及书名。如果您在某个主题上有专业知识,并且有兴趣编写或参与书籍的创作,请查看我们的作者指南:www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 图书的骄傲拥有者,我们提供了许多帮助您最大化购买价值的资源。
下载示例代码
您可以从您的账户中下载本书的示例代码文件,访问 www.packtpub.com
。如果您是从其他地方购买的本书,可以访问 www.packtpub.com/support
并注册,以便直接通过电子邮件将文件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持标签上。
-
点击代码下载与勘误表。
-
在搜索框中输入书名。
-
选择您希望下载代码文件的书籍。
-
从下拉菜单中选择您购买本书的来源。
-
点击代码下载。
文件下载完成后,请确保使用以下最新版本解压或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Learning-Quantitative-Finance-with-R
。我们还有其他代码包,来自我们丰富的书籍和视频目录,您可以在 github.com/PacktPublishing/
查阅!快去看看吧!
勘误表
尽管我们已尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书籍中发现错误——无论是文本错误还是代码错误——我们将非常感激您向我们报告。通过这样做,您可以帮助其他读者避免困扰,并帮助我们改进后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata
报告,选择您的书籍,点击勘误提交表单链接,并填写勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书名的勘误部分中的现有勘误列表。
要查看先前提交的勘误,请访问www.packtpub.com/books/content/support
,并在搜索框中输入书名。所需的信息将出现在勘误部分。
盗版
互联网上的版权材料盗版问题在所有媒体中普遍存在。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上遇到任何我们作品的非法复制品,请立即提供该地址或网站名称,以便我们采取措施。
请通过 copyright@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。
感谢您在保护我们作者权益以及帮助我们为您提供有价值内容方面的支持。
问题
如果您在本书的任何方面遇到问题,可以通过 questions@packtpub.com 联系我们,我们将尽力解决问题。
第一章:R 介绍
本章将讨论 R 的基本概念。这将为后续章节提供背景知识。我们不会对 R 中的每个概念进行详细讨论。本章面向那些没有 R 语言基础知识的人,或者是希望从事量化金融职业或希望使用 R 进行量化金融分析的初学者。本章将帮助你入门学习如何编写 R 程序,对于编写复杂程序,你可以参考其他书籍。
本章涵盖以下主题:
-
R 的需求
-
如何下载/安装 R
-
如何安装包
-
数据类型
-
不同数据类型的导入与导出
-
如何编写代码表达式
-
函数
-
如何执行 R 程序
-
循环(for, while, if, 和 if...else)
R 的需求
有许多统计包可以用于解决量化金融中的问题。但 R 并不是一个统计包,而是一种语言。R 是一种灵活且强大的语言,用于实现高质量的分析。
使用 R 并不需要成为程序员或计算机领域的专家。虽然掌握基本的编程知识确实有助于学习 R,但这并不是开始使用 R 的前提条件。
R 的一大优势是其包系统。它非常庞大。如果某个统计概念存在,那么很有可能 R 中已经有了相应的包。R 内置了许多用于统计学和量化金融的功能。
R 是可扩展的,并提供丰富的功能,鼓励量化金融开发者编写自己的工具或方法来解决分析问题。
R 中的图形和绘图功能无与伦比。R 与学术界有着紧密的关系。随着新研究的发布,由于其开源特性,通常会有与新研究相关的包被添加,这使得 R 能够随时跟进量化金融领域中新兴的概念。
R 是为处理数据而设计的,但在 R 出现时,大数据并未成为焦点。处理大数据时面临的额外挑战包括数据的多样性(如文本数据、度量数据等)、数据安全性、内存、CPU I/O 和 RSC 要求、多台机器等。为应对大数据挑战,R 采用了诸如映射-归约、内存处理、流数据处理、下采样、分块等技术。
此外,R 是免费的软件。其开发社区非常出色且易于接触,他们始终对开发新包以支持新概念感兴趣。互联网上有大量关于 R 各种包的文档可供参考。
因此,R 是一种成本效益高、易于学习的工具。它具有非常好的数据处理、图形和绘图能力。它是一款前沿工具,因为由于其开放性,金融领域的新概念通常伴随着新的 R 包出现。对于追求定量金融职业的人来说,学习 R 是时代的需求。
如何下载/安装 R
在这一部分,我们将讨论如何为各种平台(Windows、Linux 和 Mac)下载和安装 R。
打开你的浏览器并访问以下链接:cran.rstudio.com/
。
从给定的链接中,你可以根据可用的操作系统下载所需的版本。
对于 Windows 版本,点击下载 R for Windows,然后选择基础版本并下载Download R 3.3.1 for Windows,点击后选择你喜欢的语言选项。接下来,点击安装程序并按步骤完成,它会带你选择一些选项,例如以下内容:
-
设置向导。
-
许可协议。
-
选择你希望安装的文件夹位置。
-
选择组件。根据系统配置选择选项;如果你不知道系统配置,则选择所有选项。
-
如果你想自定义安装,选择该选项。
-
根据你的需求选择 R 启动选项和桌面快捷方式选项。
Windows 上的 R 下载和安装完成。
类似地,你点击 Linux 和 Mac 的安装程序,它将带你完成各种安装选项。
如何安装包
R 包是 R 函数、编译代码和示例数据的组合,其存储目录被称为库。默认情况下,安装 R 时会安装一组包,其他的包则需要在需要时添加。
这里给出了一个命令列表,可以检查系统中有哪些包:
>.libPaths()
上述命令用于获取或设置 R 知道的库树。它会给出如下结果:
"C:/Program Files/R/R-3.3.1/library"
完成后,执行以下命令,它将列出所有可用的包:
>library()
有两种方式来安装新包。
从 CRAN 直接安装
CRAN 代表综合 R 存档网络。它是一个全球范围的 FTP 服务器网络,用于存储 R 的相同、最新版本的代码和文档。
以下命令用于直接从 CRAN 网页安装包。你需要选择合适的镜像:
>install.packages("Package")
例如,如果你需要为 R 安装ggplot2
或forecast
包,命令如下:
>install.packages("ggplot2")
>install.packages("forecast")
手动安装包
手动下载所需的 R 包并将 ZIP 版本保存在你指定的位置(例如 /DATA/RPACKAGES/
)。
例如,如果我们想安装 ggplot2
,可以运行以下命令来安装并加载到当前的 R 环境中。类似地,其他包也可以被安装:
>install.packages("ggplot2", lib="/data/Rpackages/")
>library(ggplot2, lib.loc="/data/Rpackages/")
数据类型
在任何编程语言中,都需要使用各种变量存储不同的信息。变量是用于存储值的保留内存位置。因此,通过创建一个变量,你实际上是在保留内存中的一部分空间。你可能想要存储各种数据类型,例如字符、浮动点数、布尔值等。根据数据类型,操作系统会分配内存并决定可以在保留的内存中存储什么内容。
在 R 中,你遇到的所有事物都被称为对象。
R 有五种基本对象类型,也称为原子对象,其他所有对象都是基于这些原子对象构建的。现在,我们将为所有基本对象提供示例并验证它们的类型:
-
字符:
我们为一个变量分配一个字符值并验证它的类型:
>a <- "hello" >print(class(a))
产生的结果如下:
[1] "character"
-
数值:
我们为一个变量分配一个数值并验证它的类型:
>a <- 2.5 >print(class(a))
产生的结果如下:
[1] "numeric"
-
整数:
我们为一个变量分配一个整数值并验证它的类型:
>a <- 6L >print(class(a))
产生的结果如下:
[1] "integer"
-
复数:
我们为一个变量分配一个整数值并验证它的类型:
>a <- 1 + 2i >print(class(a))
产生的结果如下:
[1] "complex"
-
逻辑值(
True
/false
):我们为一个变量分配一个整数值并验证它的类型:
>a <- TRUE >print(class(a))
然后,产生的结果如下:
[1] "logical"
R 中的基本对象类型被称为向量,它们由相同类型的对象组成。它们不能同时包含两种不同类型的对象,例如既包含字符类型又包含数值类型的向量。
但列表是一个例外,它可以同时包含多种类型的对象。因此,一个列表可以同时包含字符、数值和另一个列表。
现在我们将讨论 R 中常见的数据类型,并为每种数据类型提供至少一个示例。
向量
向量已经被定义。如果我们想构建一个包含多个元素的向量,可以使用 c()
函数将这些元素组合成一个向量,例如:
>a<-"Quantitative"
>b<-"Finance"
>c(a,b)
这将产生以下结果:
[1] "Quantitative" "Finance"
类似地:
>Var<-c(1,2,3)
>Var
这将产生以下结果:
[1] 1 2 3
列表
列表是一个 R 对象,它包含多种类型的对象,例如向量甚至是列表。例如,让我们构建一个列表并使用代码打印出来:
#Create a List and print it
>List1 = list(c(4,5,6),"Hello", 24.5)
>print(List1)
当我们执行之前的命令时,它会产生以下结果:
[[1]]
[1] 4 5 6
[[2]]
[1] "Hello"
[[3]]
[1] 24.5
我们可以根据需求提取列表中的单个元素。
例如,在上述情况下,如果我们想提取第二个元素:
>print(List1[2])
执行上述代码后,R 会创建以下输出:
[[1]]
[1] "Hello"
可以使用 c()
函数合并两个列表;例如:
>list1 <- list(5,6,7)
>list2 <- list("a","b","c")
>Combined_list <-c(list1,list2)
>print(Combined_list)
执行上述命令后,我们会得到合并后的列表:
[[1]]
[1] 5
[[2]]
[1] 6
[[3]]
[1] 7
[[4]]
[1] "a"
[[5]]
[1] "b"
[[6]]
[1] "c"
矩阵
矩阵是一个二维的矩形数据集,它是通过向 matrix()
函数输入向量来创建的。
例如,创建一个包含两行三列的矩阵,并打印出来:
>M <- matrix(c(1,2,3,4,5,6), nrow = 2, ncol = 3)
>print(M)
当我们执行前面的代码时,它会生成以下结果:
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
数组(Arrays)
矩阵仅限于二维,但数组可以是任意维度的。array()
函数接受 dim
属性,用于创建所需的维度。
例如,创建一个数组并打印出来:
>a <- array(c(4,5),dim = c(3,3,2))
>print(a)
当我们执行前面的代码时,它会生成以下结果:
, , 1
[,1] [,2] [,3]
[1,] 4 5 4
[2,] 5 4 5
[3,] 4 5 4
, , 2
[,1] [,2] [,3]
[1,] 5 4 5
[2,] 4 5 4
[3,] 5 4 5
因子(Factors)
因子(Factors)是使用向量创建的 R 对象。它存储向量及向量中存在的不同元素作为标签。标签总是以字符形式存在,不论其原始形式是数字、字符还是布尔值。
因子是通过 factor()
函数创建的,级别的数量由 n 个级别给出;例如:
>a <-c(2,3,4,2,3)
>fact <-factor(a)
>print(fact)
>print(nlevels(fact))
当前面的代码执行时,它会生成以下结果:
[1] 2 3 4 2 3
Levels: 2 3 4
[1] 3
数据框(DataFrames)
数据框(DataFrames)是表格形式的数据对象,每一列可以是不同的类型,即数值型、字符型或逻辑型。每一列由一组具有相同长度的向量构成。
数据框(DataFrames)是通过 data.frame()
函数生成的;例如:
>data <-data.frame(
>+Name = c("Alex", "John", "Bob"),
>+Age = c(18,20,23),
>+Gender =c("M","M","M")
>+)
>print(data)
当前面的代码执行时,它会生成以下结果:
Name Age Gender
1 Alex 18 M
2 John 20 M
3 Bob 23 M
导入和导出不同的数据类型
在 R 中,我们可以读取存储在 R 环境外的文件。我们也可以将数据写入文件,这些文件可以由操作系统存储和访问。在 R 中,我们可以读取和写入不同格式的文件,如 CSV、Excel、TXT 等。本节将讨论如何读取和写入不同格式的文件。
读取文件所需的文件应存在于当前目录中。否则,应该将目录更改为所需的目标目录。
读取/写入文件的第一步是了解工作目录。你可以通过运行以下代码找到工作目录的路径:
>print (getwd())
这将提供当前工作目录的路径。如果这不是你想要的目录,请使用以下代码设置你自己的目标目录:
>setwd("")
例如,以下代码将文件夹 C:/Users
设置为工作目录:
>setwd("C:/Users")
如何读取和写入 CSV 格式文件
CSV 格式文件是一个文本文件,值由逗号分隔。让我们考虑一个包含股票市场数据的 CSV 文件,其内容如下:
日期 |
开盘 |
最高 |
最低 |
收盘 |
成交量 |
调整后收盘 |
---|---|---|---|---|---|---|
14-10-2016 | 2139.68 | 2149.19 | 2132.98 | 2132.98 | 3.23E+09 | 2132.98 |
13-10-2016 | 2130.26 | 2138.19 | 2114.72 | 2132.55 | 3.58E+09 | 2132.55 |
12-10-2016 | 2137.67 | 2145.36 | 2132.77 | 2139.18 | 2.98E+09 | 2139.18 |
11-10-2016 | 2161.35 | 2161.56 | 2128.84 | 2136.73 | 3.44E+09 | 2136.73 |
10-10-2016 | 2160.39 | 2169.6 | 2160.39 | 2163.66 | 2.92E+09 | 2163.66 |
要在 R 中读取前面的文件,首先将该文件保存在工作目录中,然后使用以下代码读取它(文件名为 Sample.csv
):
>data<-read.csv("Sample.csv")
>print(data)
当当前代码执行时,它将给出以下输出:
Date Open High Low Close Volume Adj.Close
1 14-10-2016 2139.68 2149.19 2132.98 2132.98 3228150000 2132.98
2 13-10-2016 2130.26 2138.19 2114.72 2132.55 3580450000 2132.55
3 12-10-2016 2137.67 2145.36 2132.77 2139.18 2977100000 2139.18
4 11-10-2016 2161.35 2161.56 2128.84 2136.73 3438270000 2136.73
5 10-10-2016 2160.39 2169.60 2160.39 2163.66 2916550000 2163.66
Read.csv
默认生成 DataFrame 格式的文件;可以通过运行以下代码来检查:
>print(is.data.frame(data))
现在,无论您想进行什么分析,都可以通过对 DataFrame 应用各种函数来执行分析,分析完成后,您可以使用以下代码写出所需的输出文件:
>write.csv(data,"result.csv")
>output <- read.csv("result.csv")
>print(output)
当前面的代码执行时,它会将输出文件以 CSV 格式写入工作目录文件夹。
XLSX
Excel 是最常见的数据存储文件格式,文件扩展名为 .xls
或 .xlsx
。
xlsx
软件包将用于在 R 环境中读取或写入 .xlsx
文件。
安装xlsx
软件包需要依赖 Java,因此需要在系统上安装 Java。可以使用以下命令安装xlsx
软件包:
>install.packages("xlsx")
当前一个命令执行时,它会要求选择最近的 CRAN 镜像,用户必须选择该镜像来安装软件包。我们可以通过执行以下命令来验证软件包是否已安装:
>any(grepl("xlsx",installed.packages()))
如果已成功安装,它将显示以下输出:
[1] TRUE
Loading required package: rJava
Loading required package: methods
Loading required package: xlsxjars
我们可以通过运行以下脚本加载 xlsx
库:
>library("xlsx")
现在让我们将前面的示例文件保存为 .xlsx
格式,并在 R 环境中读取它,可以通过执行以下代码实现:
>data <- read.xlsx("Sample.xlsx", sheetIndex = 1)
>print(data)
这将给出一个 DataFrame 输出,内容如下:
Date Open High Low Close Volume Adj.Close
1 2016-10-14 2139.68 2149.19 2132.98 2132.98 3228150000 2132.98
2 2016-10-13 2130.26 2138.19 2114.72 2132.55 3580450000 2132.55
3 2016-10-12 2137.67 2145.36 2132.77 2139.18 2977100000 2139.18
4 2016-10-11 2161.35 2161.56 2128.84 2136.73 3438270000 2136.73
5 2016-10-10 2160.39 2169.60 2160.39 2163.66 2916550000 2163.66
同样,您可以通过执行以下代码,将 R 文件写入 .xlsx
格式:
>output<-write.xlsx(data,"result.xlsx")
>output<- read.csv("result.csv")
>print(output)
网络数据或在线数据源
网络是当前数据的主要来源之一,我们希望直接将数据从 Web 表单带入 R 环境。R 支持这一功能:
URL <- "http://ichart.finance.yahoo.com/table.csv?s=^GSPC"
snp <- as.data.frame(read.csv(URL))
head(snp)
当前面的代码执行时,它将直接将 S&P500
指数的数据以 DataFrame 格式带入 R。这里通过使用 head()
函数展示了一部分数据:
Date Open High Low Close Volume Adj.Close
1 2016-10-14 2139.68 2149.19 2132.98 2132.98 3228150000 2132.98
2 2016-10-13 2130.26 2138.19 2114.72 2132.55 3580450000 2132.55
3 2016-10-12 2137.67 2145.36 2132.77 2139.18 2977100000 2139.18
4 2016-10-11 2161.35 2161.56 2128.84 2136.73 3438270000 2136.73
5 2016-10-10 2160.39 2169.60 2160.39 2163.66 2916550000 2163.66
6 2016-10-07 2164.19 2165.86 2144.85 2153.74 3619890000 2153.74
类似地,如果我们执行以下代码,它将把 DJI 指数数据带入 R 环境:其样本在这里显示:
>URL <- "http://ichart.finance.yahoo.com/table.csv?s=^DJI"
>dji <- as.data.frame(read.csv(URL))
>head(dji)
这将给出以下输出:
Date Open High Low Close Volume Adj.Close
1 2016-10-14 18177.35 18261.11 18138.38 18138.38 87050000 18138.38
2 2016-10-13 18088.32 18137.70 17959.95 18098.94 83160000 18098.94
3 2016-10-12 18132.63 18193.96 18082.09 18144.20 72230000 18144.20
4 2016-10-11 18308.43 18312.33 18061.96 18128.66 88610000 18128.66
5 2016-10-10 18282.95 18399.96 18282.95 18329.04 72110000 18329.04
6 2016-10-07 18295.35 18319.73 18149.35 18240.49 82680000 18240.49
请注意,在本书的其余部分,我们将主要使用 snp
和 dji
指数作为示例,这些将被称为 snp
和 dji
。
数据库
关系型数据库以规范化格式存储数据,要执行统计分析,我们需要编写复杂的高级查询。但 R 可以轻松连接到各种关系型数据库,如 MySQL、Oracle 和 SQL Server,并将数据表转换为 DataFrame。一旦数据转换为 DataFrame 格式,就可以利用所有可用的函数和软件包轻松进行统计分析。
在本节中,我们将以 MySQL 为例进行说明。
R 内置了一个软件包 RMySQL
,它提供了与数据库的连接功能;可以使用以下命令安装:
>install.packages("RMySQL")
安装完包后,我们可以创建一个连接对象,以便与数据库建立连接。它需要用户名、密码、数据库名和本地主机名作为输入。我们可以提供输入并使用以下命令连接到所需的数据库:
>mysqlconnection = dbConnect(MySQL(), user = '...', password = '...', dbname = '..',host = '.....')
当数据库连接时,我们可以通过执行以下命令列出数据库中存在的表:
>dbListTables(mysqlconnection)
我们可以使用dbSendQuery()
函数查询数据库,并通过fetch()
函数将结果返回给 R。然后,输出以 DataFrame 格式存储:
>result = dbSendQuery(mysqlconnection, "select * from <table name>")
>data.frame = fetch(result)
>print(data.fame)
当前面的代码执行时,它会返回所需的输出。
我们可以通过dbSendQuery()
发送查询,使用过滤子句查询、更新数据库表中的行、向数据库表中插入数据、创建表、删除表等。
如何编写代码表达式
在本节中,我们将讨论如何编写各种基本表达式,这些表达式是编写程序的核心元素。之后,我们将讨论如何创建用户定义的函数。
表达式
R 代码由一个或多个表达式组成。表达式是执行特定任务的指令。
例如,两个数字的加法可以通过以下表达式给出:
>4+5
它给出以下输出:
[1] 9
如果程序中有多个表达式,它们会按出现的顺序逐一执行。
现在我们将讨论基本的表达式类型。
常量表达式
最简单的表达式形式是常量值,可能是字符值或数值。
例如,100 是一个常量值的数值表达式。
Hello World
是常量表达式的字符形式表达式。
算术表达式
R 语言有标准的算术运算符,通过这些运算符可以编写算术表达式。
R 有以下算术运算符:
操作数 | 运算符 |
---|---|
+ |
加法 |
- |
减法 |
* |
乘法 |
/ |
除法 |
^ |
乘方 |
使用这些算术运算,可以生成算术表达式;例如:
4+5
4-5
4*5
R 遵循 BODMAS 规则。在创建任何算术表达式时,可以使用括号来避免歧义。
条件表达式
条件表达式比较两个值并返回一个逻辑值,形式为True
或False
。
R 有用于比较值的标准运算符,以及用于组合条件的运算符:
操作数 | 运算符 |
---|---|
== |
相等 |
>(>=) |
大于(大于等于) |
<(<=) |
小于(小于等于) |
!= |
不等式 |
&& |
逻辑与 |
|| |
逻辑或 |
! |
逻辑非 |
例如:
10>5
,执行时返回True
。
5>10
,执行时返回False
。
函数调用表达式
R 表达式中最常见和最有用的类型是调用函数。在 R 中有许多内置函数,用户也可以构建自己的函数。在本节中,我们将看到调用函数的基本结构。
函数调用由函数名和紧随其后的括号组成。在括号内,参数以逗号分隔。参数是提供必要信息给函数以执行所需任务的表达式。当我们讨论如何构造用户定义函数时,将提供一个示例。
符号和赋值
R 代码由关键字和符号组成。
符号是存储在 RAM 中的对象的标签,在程序执行时,它从内存中获取存储的值。
R 还存储了许多预定义符号的预定义值,这些值会根据程序的需要自动下载使用。
例如,date()
函数在执行时会返回今天的日期。
表达式的结果可以赋值给一个符号,赋值通过使用赋值运算符<-
来实现。
例如,表达式value <- 4 + 6
将符号 value 赋值为 10,并存储在内存中。
关键字
一些符号用于表示特殊值,不能重新赋值:
-
NA
:用于定义缺失或未知的值。 -
Inf
:用于表示无限大。例如,1/0 会产生无限大的结果。 -
NaN
:用于定义算术表达式的结果是未定义的。例如,0/0 会产生 NaN。 -
NULL
:用于表示空结果。 -
TRUE
和FALSE
:这是逻辑值,通常在值比较时生成。
变量命名
在编写 R 代码时,我们需要将各种信息存储在许多符号下。因此,我们需要有意义地命名这些符号,这样可以使代码更容易理解。符号应该是自解释的。写短符号名会使代码更难理解。
例如,如果我们用DateOfBirth
或DOB
来表示出生日期信息,那么第一个选项更好,因为它是自解释的。
函数
在本节中,我们将提供一些 R 中已经存在的内置函数的示例,并构造一个用于特定任务的用户定义函数。
函数是一组组合在一起执行特定任务的语句。
R 有许多内置函数,用户也可以定义自己的函数。
根据需求,在 R 中,解释器将控制权传递给函数对象,并传递完成任务所需的参数。任务完成后,函数将控制权返回给解释器。
定义函数的语法如下:
>function_name<-function(arg1, arg2,...){
>+function body
>+}
这里:
-
函数名:这是定义的函数的名称,并作为一个对象以该名称存储。
-
参数:参数是函数完成任务所需的必要信息。参数是可选的。
-
函数体:这是执行函数指定任务的一组语句。
-
返回值:返回值是函数的最后一个表达式,它作为函数执行的任务的输出值返回。
这里提供了几个内建函数的示例及其执行结果:
>print(mean(25:82))
[1] 53.5
>print(sum(41:68))
[1] 1526
现在我们将看看如何构建用户定义的函数。在这里,我们尝试计算给定序列的平方。
函数的名称是 findingSqrFunc
,并接受一个参数值,该值必须是整数:
>findingSqrFunc<-function(value){
>+for(j in 1:value){
>+sqr<-j²
>+print(sqr)
>+}
>+}
一旦前面的代码执行完毕,我们调用该函数:
>findingSqrFunc(4)
我们得到以下输出:
[1] 1
[1] 4
[1] 9
[1] 16
无参数调用函数
构造一个没有参数的函数:
>Function_test<-function(){
>+ for(i in 1:3){
>+ print(i*5)
>+ }
>+ }
>Function_test()
执行前面的无参数函数时,会打印以下输出:
[1] 5
[1] 10
[1] 15
带参数调用函数
函数的参数可以按照定义时的顺序提供。否则,参数必须按任何顺序提供,并分配给其名称。以下是创建和调用函数的步骤:
-
首先创建一个函数:
>Function_test<-function(a,b,c){ >+ result<-a*b+c >+ print(result) >+ }
-
按照相同的顺序提供参数名称来调用函数。它将得到以下输出:
>Function_test(2,3,4) [1] 10
-
通过任意顺序提供参数名称来调用函数:
>Function_test(c=4,b=3,a=4)
这会得到以下输出:
[1] 16
如何执行 R 程序
在本节中,我们将讨论执行 R 程序的不同方法。
如何通过 R 窗口运行已保存的文件
在 R 工作空间中运行程序,请按照以下步骤操作:
-
打开 R(双击桌面图标或从 开始 菜单中打开程序)。
-
点击 文件 并打开脚本。
-
选择你要运行的程序;它将出现在 R 编辑器窗口中。
-
右键点击并选择 全选(或按 Ctrl + A)。
-
右键点击并选择 运行行 或 选择项(或者按 Ctrl + R)。
-
输出将显示在 R 控制台窗口中。
如何源代码执行 R 脚本
请按照以下步骤执行源代码操作:
-
首先检查你的工作目录。可以通过以下代码检查:
>print(getwd())
-
执行前面的代码时,如果给出了指定文件夹的路径,那是正常的。否则,可以使用以下代码更改工作目录:
>setwd("D:/Rcode")
-
根据需要更改目标目录,然后使用以下代码运行所需的代码:
>Source('firstprogram.r')
例如,假设程序 firstprogram.r
包含以下代码:
a<-5
print(a)
执行源代码时,它将在控制台生成输出 5
。
当你希望告诉 R 执行多行代码而无需等待指令时,可以使用 source
函数来运行已保存的脚本。这被称为源代码执行。
最好将整个代码写在 Studio 编辑器中,然后保存并源代码执行整个脚本。如果你希望在源代码脚本中打印输出,请使用 print
函数以获取所需的输出。然而,在交互式编辑器中,你不需要写 print
。它会默认显示输出。
在其他操作系统中,运行程序的命令保持不变。
注释是程序的一部分,在实际程序执行时,解释器会忽略这些注释。
注释使用#
编写;例如:
#this is comment in my program.
循环(for、while、if 和 if...else)
循环是用于自动化多步骤过程的指令,通过组织动作的顺序并将需要重复的部分分组来实现。所有编程语言都提供了内置结构,允许指令或指令块的重复。在编程语言中,循环有两种类型。
决策是编程语言中的重要组成部分。在 R 语言中,可以通过使用条件语句if...else
来实现决策。以下是语法及示例:
让我们先讨论if
和else
条件语句,然后再讨论循环。
if 语句
让我们先看看if
和else
在 R 中的工作原理。if
子句的一般语法如下:
if (expression) {
statement
}
如果表达式正确,则执行语句,否则不执行任何操作。表达式可以是逻辑或数值向量。在数值向量的情况下,0
被视为False
,其他的都被视为True
,例如:
>x<-5
>if(x>0)
>+ {
>+ print(" I am Positive")
>+ }
当前面的代码执行时,它打印出I am Positive
。
if...else 语句
现在让我们看看if
和else
条件在 R 中的工作方式。以下是语法:
if(expression){
statement1
} else {
statement2
}
else
部分在if
部分为False
时评估,例如:
> x<--5
> if(x>0)
>+ {
>+ print(" I am Positive")
>+ }else
>+{
>+ print(" I am Negative")
>+}
当前面的代码执行时,它打印出I am Negative
。
for 循环
这些循环在定义的次数内执行,由计数器或索引控制,并在每个循环周期中递增。请查看以下for
循环构造的语法:
for (val in sequence) {
statement
}
这是一个示例:
>Var <- c(3,6,8,9,11,16)
>counter <- 0
>for (val in Var) {
>+ if(val %% 2 != 0) counter = counter+1
>+}
print(counter)
当前面的代码执行时,它计算向量c
中奇数的数量,即3
。
while 循环
while
循环是设置用于验证逻辑条件的循环。逻辑条件在循环构造的开始进行测试。以下是语法:
while (expression) {
statement
}
这里,首先评估表达式,如果它为真,则for
循环体会执行。以下是一个示例:
>Var <- c("Hello")
>counter <- 4
>while (counter < 7) {
>+ print(Var)
>+ counter = counter+ 1
>+}
这里,首先评估表达式,如果它为真,则循环体会执行,并且会一直执行直到表达式返回False
。
apply()
apply()
是 R 语言中的一个函数,用于对矩阵、向量或数组进行快速操作,可以在行、列上或同时对行列执行操作。现在让我们尝试使用apply
函数计算矩阵的行和之和。让我们执行以下代码:
> sample = matrix(c(1:10), nrow = 5 , ncol = 2)
> apply(sample, 1,sum)
它生成按行求和的结果。
sapply()
sapply()
对数据集(如列表或向量)进行操作,并对每个项目调用指定的函数。让我们执行以下代码检查示例:
> sapply(1:5, function(x) x³)
它计算从1
到5
的立方。
循环控制语句
有一些控制语句可以改变正常的执行顺序。break
和next
是循环控制语句,我们将在此简要讨论这些控制语句。
break
break
终止循环并将控制权交给循环中的下一个语句;例如:
>Vec <- c("Hello")
>counter <- 5
>repeat {
>+ print(Vec)
>+ counter <- counter + 1
>+ if(counter > 8) {
>+ break
>+ }
>+}
由于 break
语句的作用,当前述语句执行时,它会打印出 Hello
四次,然后退出循环。repeat
是另一种循环结构,它会一直执行,除非指定停止条件。
next
next
不会终止循环,而是跳过当前迭代并进入下一次迭代。请参见以下示例:
>Vec <- c(2,3,4,5,6)
>for ( i in Vec) {
>+ if (i == 4) {
>+ next
>+ }
>+ print(i)
>+}
在前面的示例中,当迭代到向量 Vec
的第三个元素时,控制流跳过当前迭代并返回到下一个迭代。因此,当前述语句执行时,它打印出向量元素 2
、3
、5
和 6
,并跳过 4
。
问题
-
R 中有哪些不同的原子对象?
-
R 中的向量是什么?
-
向量和列表有什么区别?
-
数组和矩阵有什么区别?
-
什么是数据框(DataFrame),它在 R 中的意义是什么?
-
如何在 R 中读取和写入 CSV 和 XLSX 文件?
-
如何在 R 中读取和写入股市数据?
-
解释如何将 R 连接到任何关系型数据库。
-
什么是函数,它在 R 中的意义是什么?
-
R 中的赋值运算符是什么?
-
如何在 R 中调用函数?
-
如何在 R 中调用脚本?
-
R 中的
for
循环和while
循环有什么区别?
总结
现在让我们回顾一下本章到目前为止学到的内容:
-
对于从事金融分析的分析师来说,学习 R 是非常重要的
-
安装 R 及其包
-
R 中的基本对象有字符、数字、整数、复数和逻辑型
-
R 中常用的数据类型包括列表、矩阵、数组、因子和数据框
-
从外部数据文件(如 CSV 和 XLSX)读取文件,特别是从 R 中的在线源和数据库读取
-
从 R 写入 CSV 和 XLSX 文件
-
编写不同类型的表达式,例如常量、算术、逻辑、符号、赋值等
-
编写用户定义的函数
-
调用用户定义函数和内建函数的方法
-
从控制台窗口运行 R 程序以及通过源文件运行保存的文件
-
使用 if 和 else 语句进行条件决策的用法
-
使用
for
和while
等循环
第二章:统计建模
本章我们将讨论统计建模,这是学习 R 中的定量金融的第一步,因为统计建模的概念是定量金融的驱动力。在开始本章之前,假设学习者已熟悉 R 中的基本编程,并且具备扎实的统计学知识。本章不讨论统计学概念,我们将讨论如何在 R 中进行统计建模。
本章内容包括以下主题:
-
概率分布
-
抽样
-
统计学
-
相关性
-
假设检验
-
参数估计
-
异常值检测
-
标准化
-
归一化
概率分布
概率分布决定了随机变量的值是如何分布的。例如,抛掷一系列硬币的所有可能结果集合形成了二项分布。大样本的数据群体的均值遵循正态分布,它是最常见且最有用的分布。
这些分布的特征是非常著名的,可以用来提取有关总体的推论。本章我们将讨论一些最常见的概率分布以及如何计算它们。
正态分布
正态分布是金融行业中最广泛使用的概率分布。它呈钟形曲线,均值、中位数和众数在正态分布中是相同的。它的表示方式是,其中
是均值,
是样本的方差。如果均值为 0,方差为 1,则该正态分布称为标准正态分布 N(1, 0)。
现在让我们讨论计算与正态分布相关的重要特征的主要功能。请注意,我们将在本章的所有计算中使用数据集DataChap2.csv
。以下表格显示了一个样本。假设在 R 中导入的数据集为Sampledata
。
在给定的样本中,Date
是数据捕获的时间。Open
、High
、Low
和Close
分别是当天的开盘价、最高价、最低价和收盘价。Adj.Close
是当天的调整价格,return
是根据今天和昨天的Adj.Close
价格计算的回报。Flag
和Sentiments
是为分析目的创建的虚拟变量:
Date |
Open |
High |
Low |
Close |
Volume |
Adj.Close |
Return |
Flag |
Sentiments |
---|---|---|---|---|---|---|---|---|---|
12/14/2016 | 198.74 | 203 | 196.76 | 198.69 | 4144600 | 198.69 | 0 | 1 | 好 |
12/13/2016 | 193.18 | 201.28 | 193 | 198.15 | 6816100 | 198.15 | 0.03 | 1 | 坏 |
12/12/2016 | 192.8 | 194.42 | 191.18 | 192.43 | 615800 | 192.43 | 0 | 1 | 好 |
12/9/2016 | 190.87 | 193.84 | 190.81 | 192.18 | 2719600 | 192.18 | 0 | 0 | 坏 |
2016/12/8 | 192.05 | 192.5 | 189.54 | 192.29 | 3187300 | 192.29 | 0 | 0 | 良好 |
norm
norm
返回正态分布的高度,函数的定义如下:
dnorm(x, mean, sd)
在这里,x
是数字的向量,sd
是标准差。
当我们执行以下代码时,它会生成展示所有点高度的图:
> y <- dnorm(Sampledata$Return, mean = mean(Sampledata$Return), sd =sd(Sampledata$Return, na.rm = FALSE))
> plot(Sampledata$Return,y)
图形表示如下:
图 2.1:展示正态分布高度的图
pnorm
pnorm
被称为累积分布函数,它给出小于给定值的随机变量的概率,其函数定义如下:
pnorm(x, mean, sd)
我们执行以下代码:
> pnorm(.02, mean = mean(Sampledata$Return), sd = sd(Sampledata$Return, na.rm = FALSE))
这将得出 0.159837
,可以解释为获得大于 2% 回报的概率为 16%。
qnorm
qnorm
接受概率值并返回一个数字,使得其累积值与该概率匹配,函数定义如下:
qnorm(x, mean, sd)
在这里,x
是概率值。
我们执行以下代码:
> qnorm(0.159837, mean = mean(Sampledata$Return), sd = +sd(Sampledata$Return, na.rm = FALSE),lower.tail=FALSE)
这将给出输出 0.02
,意味着对于大于等于 2% 的回报,概率为 16%。
rnorm
rnorm
用于生成其分布为正态分布的随机数。其定义如下:
qnorm(x, mean, sd)
在这里,x
是要生成的随机变量的数量。
如果我们运行以下代码,它将生成五个具有回报均值和标准差的随机值:
>rnorm(5, mean = mean(Sampledata$Return), sd = +sd(Sampledata$Return, na.rm = FALSE))
当这段代码执行时,它会生成五个具有指定均值和标准差的正态随机变量。
对数正态分布
在金融时间序列中,对数正态分布比正态分布起着更为关键的作用。和正态分布一样,我们将讨论对数正态分布的相同特性。
dlnorm
dlnorm
用于查找对数正态分布的密度函数。计算密度函数的一般语法如下:
dlnorm(x, meanlog, sdlog)
让我们找出样本数据体积的密度函数,可以通过执行以下代码来完成:
> y <- dlnorm(Sampledata$Volume, meanlog = mean(Sampledata$Volume), sdlog= sd(Sampledata$Volume, na.rm = FALSE))> plot(Sampledata$Volume,y)
图形表示如下:
图 2.2:展示对数正态分布密度函数的图
plnorm
plnorm
给出对数正态分布的累积分布函数。其一般语法如下:
>dlnorm(x, meanlog, sdlog)
现在让我们找出体积的 cdf
,可以通过以下代码来完成:
> y <- plnorm(Sampledata$Volume, meanlog = mean(Sampledata$Volume), sdlog= sd(Sampledata$Volume, na.r=FALSE))> plot(Sampledata$Volume,y)
这将给出 cdf
图,如下所示:
图 2.3:展示对数正态分布的累积分布函数的图
qlnorm
qlnorm
用于生成对数正态分布的 p
分位数,可以使用以下语法来完成:
qlnorm(p, mean, standard deviation)
rlnorm
rlnorm
生成一个具有给定均值和标准差的数据集。其语法如下:
rlnorm((n, mean , standard dev)
泊松分布
泊松分布是独立事件在一个区间内发生的概率分布。如果 是每个区间的平均发生次数,那么在给定区间内发生 x 次事件的概率由以下公式给出:
这里,x = 0, 1, 2, 3.....
如果平均每分钟有 10 只股票的回报率是正的,我们可以通过使用以下代码来计算在某一特定分钟内,15 只股票回报率为正的概率:
>ppois(15, lambda=10)
这将产生输出值 0.9512596
。
因此,15 只股票回报为正的下尾概率为 0.95。
同样,我们可以通过执行以下代码来找到上尾概率:
>ppois(15, lambda=10, lower=FALSE)
均匀分布
连续均匀分布是从 a 到 b 的连续区间中随机选择一个数字的概率分布。其密度函数如下所示:
F(x) = 1/(b-a)
这里 和
现在让我们生成 1
到 5
之间的 10
个随机数。可以通过执行以下代码来实现:
>runif(10, min=1, max=5)
这将生成以下输出:
3.589514 2.979528 3.454022 2.731393 4.416726 1.560019 4.592588 1.500221 4.067229 3.515988\.
极值理论
大多数常见的统计分布关注分布的中心部分,而不关心分布的尾部,尾部包含极端值/异常值。对于风险管理者而言,最大的挑战之一是开发能够处理罕见和极端事件的风险模型。极值理论(EVT)试图提供分布尾部区域的最佳可能估计。
估计极值的模型有两种类型,即适配广义极值分布(GEV)的区间最大值模型和适配广义帕累托分布(GPD)的超阈值模型(POT)。目前通常使用 POT,因此我们将在本章中给出 POT 的示例。我们将使用 POT 包中的数据集子集作为示例。
为了找到尾部分布,我们首先需要找到一个阈值点,这可以通过执行以下代码来完成:
> data(ardieres)
> abc<-ardieres[1:10000,]
> events <- clust(abc, u = 1.5, tim.cond = 8/365, clust.max = TRUE)
> par(mfrow = c(2, 2))
> mrlplot(events[, "obs"])
> diplot(events)
> tcplot(events[, "obs"], which = 1)
> tcplot(events[, "obs"], which = 2)
这将生成以下图形:
图 2.4:EVT 阈值选择分析
通过分析这些图形,我们可以设置阈值点,并估计 GPD 模型的参数。这是通过执行以下代码来完成的:
>obs <- events[,"obs"]
>ModelFit <- fitgpd(obs, thresh = 5, "pwmu")
>ModelFit
这将给出 GPD 模型的参数估计:
图 2.5:EVT 的 GPD 模型参数估计
抽样
在金融建模中,我们可能会有非常大的数据集,模型的建立会非常耗时。一旦模型建立完成,如果需要再次调整模型,由于数据量庞大,过程将变得更加耗时。因此,最好从总体数据中获取随机或按比例抽样的数据,这样模型的构建会更加容易且节省时间。因此,在本节中,我们将讨论如何从数据中选择随机样本和分层样本。这将在基于从总体数据中抽取的样本数据建立模型时发挥关键作用。
随机抽样
选择一个样本,其中总体中的每个观察值都有相同的概率。这可以通过两种方式完成,一种是无放回,另一种是有放回。
无放回的随机抽样可以通过执行以下代码完成:
> RandomSample <- Sampledata[sample(1:nrow(Sampledata), 10,
>+ replace=FALSE),]
这将生成如下输出:
图 2.6:显示无放回随机样本的表格
带放回的随机抽样可以通过执行以下代码完成。放回意味着某个观察值可以被多次抽取。因此,如果某个观察值被选中,它会再次放回总体中,之后可能再次被选中:
> RandomSample <- Sampledata[sample(1:nrow(Sampledata), 10,
>+ replace=TRUE),]
这将生成如下输出:
图 2.7:显示带放回的随机抽样的表格
分层抽样
在分层抽样中,我们将总体划分为不同的组,称为层。然后,从每个组中抽取一个概率样本(通常是简单随机样本)。分层抽样相较于简单随机抽样有几个优点。通过分层抽样,可以减少样本量以获得更高的精度。
现在让我们通过以下代码查看使用 Flag
和 Sentiments
时存在的组别数量:
>library(sampling)
>table(Sampledata$Flag,Sampledata$Sentiments)
输出如下:
图 2.8:显示不同组别频率的表格
现在你可以根据需求从不同的组中选择样本:
>Stratsubset=strata(Sampledata,c("Flag","Sentiments"),size=c(6,5, >+4,3), method="srswor")
> Stratsubset
输出如下:
图 2.9:显示分层抽样输出的表格
统计学
在给定的数据集中,我们尝试通过数据的集中位置来总结数据,这被称为集中趋势度量或汇总统计。衡量集中趋势的方式有多种,比如均值、中位数和众数。均值是最常用的集中趋势度量。在不同的情境下,我们使用不同的集中趋势度量。现在,我们将通过一个例子展示如何在 R 中计算不同的集中趋势度量。
均值
mean
是样本的等权重平均值。例如,我们可以通过执行以下代码来计算数据集 Sampledata
中 Volume
的均值,这将给出体积的算术平均值:
mean(Sampledata$Volume)
中位数
中位数是将矩阵按顺序排列后的中间值,可以通过执行以下代码计算:
median(Sampledata$Volume)
众数
众数是属性中出现频率最高的值。由于众数没有内置函数,因此我们将编写一个函数来计算众数:
findmode <- function(x) {
uniqx <- unique(x)
uniqx[which.max(tabulate(match(x, uniqx)))]
}
findmode(Sampledata$return)
执行上述代码会给出数据集的返回属性的众数。
概述
我们还可以通过执行以下代码来生成列的基本统计信息:
summary(Sampledata$Volume)
这将生成均值、中位数、最小值、最大值、第一四分位数(Q1)和第二四分位数(Q2)。
矩(Moment)
矩(Moment)给出了总体的特征,如方差、偏度等,可以通过以下代码计算。该代码给出了属性Volume
的第三阶矩。你可以改变阶数来获得相关的特征。但是,在此之前,我们需要安装e1071
包:
moment(Sampledata$Volume, order=3, center=TRUE)
峰度
峰度(Kurtosis)衡量数据相对于正态分布是否为厚尾或轻尾分布。具有高峰度的数据集往往具有厚尾或异常值,而具有低峰度的数据集则倾向于具有轻尾和较少的异常值。计算出的峰度值会与正态分布的峰度进行比较,并根据该比较结果进行解释。
Volume
的峰度(kurtosis)由以下代码给出:
kurtosis(Sampledata$Volume)
它给出了值5.777117
,这表明体积的分布呈现尖峰厚尾(leptokurtic)特征。
偏度
偏度是分布对称性的度量。如果数据值的均值小于中位数,则称分布为左偏;如果数据值的均值大于中位数,则称分布为右偏。
Volume
的偏度(skewness)可以通过以下 R 代码计算:
skewness(Sampledata$Volume)
这给出的结果是1.723744
,这意味着它是右偏分布。
注意
计算偏度(skewness)
和峰度(kurtosis)时,我们需要安装e1071
包。
相关性
相关性在量化金融中起着非常重要的作用。它不仅决定了金融属性之间的关系,还在预测金融工具的未来中起着至关重要的作用。相关性是两个金融属性之间线性关系的度量。现在让我们尝试使用Sampledata
在 R 中计算不同类型的相关性,Sampledata
用于识别预测金融模型的组成部分的顺序。
相关性可以通过以下代码计算。首先让我们对数据进行子集化,然后运行该函数来获得相关性:
x<-Sampledata[,2:5]
rcorr(x, type="pearson")
这将生成以下相关性矩阵,显示了股票不同日常价格之间的线性关系度量:
Open |
High |
Low |
Close |
|
---|---|---|---|---|
Open |
1 | 0.962062 | 0.934174 | 0.878553 |
High |
0.962062 | 1 | 0.952676 | 0.945434 |
Low |
0.934174 | 0.952676 | 1 | 0.960428 |
Close |
0.878553 | 0.945434 | 0.960428 | 1 |
自相关
自相关是序列与其过去或未来值之间的相关性,也称为序列相关性或滞后相关性。它在时间序列预测建模中起着关键作用。函数acf
计算自相关函数的估计值。
执行以下代码将给出序列与其滞后值的自相关:
acf(Sampledata$Volume)
图形如下所示:
图 2.10:显示序列自相关及其滞后值的图形
这将给出序列与其滞后值的自相关图形。函数如lag.max
、plot
等还有其他选项。
部分自相关
时间序列的部分自相关是其与滞后值的相关性,控制了时间序列在所有较短滞后期的值。它还用于时间序列建模,以识别预测技术的成分的顺序。它可以通过以下代码计算:
pacf(Sampledata$Volume)
它还包含其他选项,如你想使用和绘制多少滞后。前面的代码生成了以下图表:
图 2.11:显示具有滞后期的系列的部分自相关图
互相关
互相关是衡量两个序列相似度的一种方法,作为一个序列相对于另一个序列的位移函数。与acf
和pacf
一样,它在时间序列预测中也起着至关重要的作用。它可以通过以下函数计算:
ccf(Sampledata$Volume,Sampledata$High, main = "ccf plot")
当前面的代码执行时,它生成以下图表:
图 2.12:显示两个序列的互相关图
假设检验
假设检验用于基于观察样本的测量来拒绝或保留假设。我们将不深入探讨理论方面,而是讨论如何在 R 中实现假设检验的各种场景。
已知方差下的总体均值下尾检验
零假设由给出,其中
是假设的总体均值下限。
假设我们处于一个情境中,其中一个投资者假设自股票成立以来,日收益的均值大于$10。30 天日收益样本的平均值是$9.9。假设总体标准差为 0.011。我们能否在0.05
显著性水平下拒绝零假设?
现在让我们计算检验统计量z
,它可以通过以下代码在 R 中计算:
> xbar= 9.9
> mu0 = 10
> sig = 1.1
> n = 30
> z = (xbar-mu0)/(sig/sqrt(n))
> z
这里:
-
xbar
:样本均值 -
mu
:假设的值 -
sig
:总体标准差 -
n
:样本大小 -
z
:检验统计量
这给出了检验统计量z
的值:
[1] -0.4979296
现在让我们找出在0.05
显著性水平下的临界值。它可以通过以下代码计算:
> alpha = .05
> z.alpha = qnorm(1-alpha)
> -z.alpha
这给出了以下输出:
[1] -1.644854
由于检验统计量的值大于临界值,我们未能拒绝零假设,认为收益大于$10。
替代使用临界值检验,我们可以使用pnorm
函数来计算 P 值检验统计量的下尾。可以通过以下代码计算:
> pnorm(z)
这给出了以下输出:
[1] 0.3092668
由于 P 值大于0.05
,我们未能拒绝零假设。
已知方差下的总体均值上尾检验
原假设给定为 ,其中
是假设的总体均值的上界。
假设一个场景,投资者假设自股票上市以来的日收益均值最多为$5。30 天的日收益样本均值为$5.1。假设总体标准差为 0.25。我们能在0.05
显著性水平下拒绝原假设吗?
现在让我们计算检验统计量z
,可以通过以下 R 语言代码来计算:
> xbar= 5.1
> mu0 = 5
> sig = .25
> n = 30
> z = (xbar-mu0)/(sig/sqrt(n))
> z
这里:
-
xbar
:样本均值 -
mu0
:假设值 -
sig
:总体标准差 -
n
:样本大小 -
z
:检验统计量
它给出了2.19089
作为检验统计量的值。现在让我们计算在0.05
显著性水平下的临界值,以下代码给出了该值:
> alpha = .05
> z.alpha = qnorm(1-alpha)
> z.alpha
这给出了1.644854
,小于为检验统计量计算的值。因此,我们拒绝原假设。
此外,检验统计量的 P 值如下所示:
>pnorm(z, lower.tail=FALSE)
这给出了0.01422987
,小于0.05
,因此我们拒绝原假设。
知道方差的总体均值的双尾检验
原假设给定为 ,其中
是假设的总体均值值。
假设一个场景,去年一只股票的日收益均值为$2。今年 30 天的日收益样本均值为$1.5。假设总体标准差为 0.2。我们能在0.05
显著性水平下拒绝原假设,即今年的收益与去年的差异不显著吗?
现在让我们计算检验统计量z
,可以通过以下 R 语言代码来计算:
> xbar= 1.5
> mu0 = 2
> sig = .1
> n = 30
> z = (xbar-mu0)/(sig/sqrt(n))
> z
这给出了检验统计量的值为-27.38613
。
现在让我们尝试在0.05
显著性水平下找出临界值,用于与检验统计量进行比较。以下代码给出了该值:
>alpha = .05
>z.half.alpha = qnorm(1-alpha/2)
>c(-z.half.alpha, z.half.alpha)
这给出了-1.959964
,1.959964
的值。由于检验统计量的值不在区间(-1.959964
,1.959964
)之间,因此我们在0.05
显著性水平下拒绝原假设,即今年的收益与去年的差异不显著。
双尾 P 值统计量如下所示:
>2*pnorm(z)
这给出了一个小于0.05
的值,因此我们拒绝原假设。
在所有前述场景中,总体的方差是已知的,因此我们使用正态分布进行假设检验。然而,在接下来的场景中,我们不会给出总体的方差,因此我们将使用t
分布来进行假设检验。
未知方差的总体均值下尾检验
原假设为 ,其中
是假设的总体均值的下界。
假设一个场景,投资者假设某股票自成立以来的日收益率均值大于\(1。30 天的日收益率样本平均值为\).9。假设总体标准差为 0.01。我们能在.05
显著性水平下拒绝原假设吗?
在此场景下,我们可以通过执行以下代码计算检验统计量:
> xbar= .9
> mu0 = 1
> sig = .1
> n = 30
> t = (xbar-mu0)/(sig/sqrt(n))
> t
这里:
-
xbar
: 样本均值 -
mu0
: 假设值 -
sig
: 样本标准差 -
n
: 样本容量 -
t
: 检验统计量
这给出了检验统计量的值为-5.477226
。现在让我们计算.05
显著性水平下的临界值,计算代码如下:
> alpha = .05
> t.alpha = qt(1-alpha, df=n-1)
> -t.alpha
我们得到的值为-1.699127
。由于检验统计量的值小于临界值,因此我们拒绝原假设。
现在,我们可以使用与检验统计量相关的 P 值,而不是检验统计量的值,P 值如下所示:
>pt(t, df=n-1)
这导致值小于.05,因此我们可以拒绝原假设。
上尾检验的总体均值,方差未知
原假设为 ,其中
是假设的总体均值的上界。
假设一个场景,投资者假设某股票自成立以来的日收益率均值最多为$3。30 天的日收益率样本平均值为$3.1。假设总体标准差为.2
。我们能在.05
显著性水平下拒绝原假设吗?
现在让我们计算检验统计量t
,它可以通过以下 R 代码计算得出:
> xbar= 3.1
> mu0 = 3
> sig = .2
> n = 30
> t = (xbar-mu0)/(sig/sqrt(n))
> t
这里:
-
xbar
: 样本均值 -
mu0
: 假设值 -
sig
: 样本标准差 -
n
: 样本容量 -
t
: 检验统计量
这给出了检验统计量的值2.738613
。现在让我们找出与.05
显著性水平相关的临界值,计算代码如下:
> alpha = .05
> t.alpha = qt(1-alpha, df=n-1)
> t.alpha
由于临界值1.699127
小于检验统计量的值,因此我们拒绝原假设。
此外,检验统计量对应的值如下所示:
>pt(t, df=n-1, lower.tail=FALSE)
这个值小于.05
。因此,原假设被拒绝。
两尾检验的总体均值,方差未知
原假设为 ,其中
是假设的总体均值。
假设一个情景,去年某股票的日收益率均值为$2。今年 30 天的日收益率样本均值为$1.9。假设总体标准差为.1
。我们能否在显著性水平为.05
的情况下,拒绝“今年和去年收益率差异不显著”的原假设?
现在让我们计算检验统计量t
,可以通过在 R 中执行以下代码来计算:
> xbar= 1.9
> mu0 = 2
> sig = .1
> n = 30
> t = (xbar-mu0)/(sig/sqrt(n))
> t
这给出了-5.477226
作为检验统计量的值。现在让我们尝试找出用于比较的临界值范围,代码如下:
> alpha = .05
> t.half.alpha = qt(1-alpha/2, df=n-1)
> c(-t.half.alpha, t.half.alpha)
这给出了范围值(-2.04523
,2.04523
)。由于这是检验统计量的值,我们拒绝原假设的主张。
参数估计
在本节中,我们将讨论一些用于参数估计的算法。
最大似然估计
最大似然估计(MLE)是一种在给定数据集上估计模型参数的方法。
现在让我们尝试找到正态分布概率密度函数的参数估计。
让我们首先生成一系列随机变量,可以通过执行以下代码来完成:
> set.seed(100)
> NO_values <- 100
> Y <- rnorm(NO_values, mean = 5, sd = 1)
> mean(Y)
这给出了5.002913
。
> sd(Y)
这给出了1.02071
。
现在让我们为log
似然函数创建一个函数:
LogL <- function(mu, sigma) {
+ A = dnorm(Y, mu, sigma)
+ -sum(log(A))
+ }
现在让我们应用mle
函数来估计均值和标准差的参数:
> library(stats4)
> mle(LogL, start = list(mu = 2, sigma=2))
mu
和sigma
已给定初始值。
这给出的输出如下:
图 2.13:MLE 估计的输出
当尝试负值作为标准差时,会产生 NaN。
这可以通过提供相关选项来控制,如下所示。这将忽略在图 2.13中显示的输出中产生的警告信息:
> mle(LogL, start = list(mu = 2, sigma=2), method = "L-BFGS-B",
+ lower = c(-Inf, 0),
+ upper = c(Inf, Inf))
执行后,给出了最佳拟合结果,如下所示:
图 2.14:修正后的 MLE 估计输出
线性模型
在线性回归模型中,我们尝试根据自变量/预测变量预测因变量/响应变量。在该模型中,我们尝试拟合最佳的回归线,通过给定的点来确定。回归线的系数是使用统计软件估计的。回归线中的截距表示当预测变量为零时,因变量的均值。同时,响应变量随着预测变量每单位变化而按估计系数的因子增加。现在让我们尝试估计线性回归模型的参数,其中因变量是Adj.Close
,自变量是Sampledata
的Volume
。然后我们可以拟合线性模型如下:
> Y<-Sampledata$Adj.Close
> X<-Sampledata$Volume
> fit <- lm(Y ~ X)
> summary(fit)
执行前面的代码后,输出如下所示:
图 2.15:线性模型估计的输出
summary
显示了线性回归模型的参数估计。同样,我们可以为其他回归模型(如多元回归或其他形式的回归模型)估计参数。
异常值检测
异常值在任何分析中都非常重要,因为它们可能导致分析偏差。在 R 中有多种方法可以检测异常值,本节将讨论最常见的方法。
箱形图
让我们为Sampledata
的变量volume
构建一个boxplot
,这可以通过执行以下代码来完成:
> boxplot(Sampledata$Volume, main="Volume", boxwex=0.1)
图形如下所示:
图 2.16:用于异常值检测的箱形图
异常值是与其余数据距离较远的观测值。当查看前面的箱形图时,我们可以清楚地看到异常值,这些值位于箱形图的“胡须”外部。
LOF 算法
局部异常因子(LOF)用于识别基于密度的局部异常值。在 LOF 中,比较一个点的局部密度与其邻居的局部密度。如果该点的密度比其邻居所在区域的密度低,则将其视为异常值。让我们考虑一些来自Sampledata
的变量,并执行以下代码:
> library(DMwR)
> Sampledata1<- Sampledata[,2:4]
> outlier.scores <- lofactor(Sampledata1, k=4)
> plot(density(outlier.scores))
这里,k
是计算局部异常因子时使用的邻居数量。
图形如下所示:
图 2.17:使用 LOF 方法显示异常值的图
如果你想获得前五个异常值,请执行以下代码:
> order(outlier.scores, decreasing=T)[1:5]
这将输出带有行号的结果:
[1] 50 34 40 33 22
标准化
在统计学中,标准化起着至关重要的作用,因为我们有许多不同尺度的属性进行建模。为了进行比较,我们需要标准化这些变量,使其处于相同的尺度上。R 中通过scale()
函数对值进行居中处理并创建z
分数。该函数接受以下参数:
-
x
:一个数值对象 -
center
:如果TRUE
,则从该列的值中减去该列的均值(忽略 NA 值);如果FALSE
,则不执行居中操作 -
scale
:如果TRUE
,则将居中的列值除以该列的标准差(当center
也为TRUE
时;否则,使用均方根);如果FALSE
,则不执行缩放操作
如果我们想要将数据集中的Volume
数据居中,只需执行以下代码:
>scale(Sampledata$Volume, center=TRUE, scale=FALSE)
如果我们想要标准化数据集中的volume
数据,只需执行以下代码:
>scale(Sampledata$Volume, center=TRUE, scale=TRUE)
归一化
归一化通过使用minmax
概念来完成,以使各个属性处于相同的尺度上。其计算公式如下:
normalized = (x-min(x))/(max(x)-min(x))
如果我们想要归一化volume
变量,只需执行以下代码:
> normalized = (Sampledata$Volume-+min(Sampledata$Volume))/(max(Sampledata$Volume)-+min(Sampledata$Volume))
> normalized
问题
-
在 R 中构建正常、泊松和均匀分布的示例。
-
如何在 R 中进行随机和分层抽样?
-
中心趋势的不同度量方法是什么,如何在 R 中找到它们?
-
如何在 R 中计算峰度和偏度?
-
如何在 R 中进行假设检验,已知/未知总体方差的情况下?
-
如何在 R 中检测离群值?
-
如何在 R 中为线性模型和最大似然估计(MLE)进行参数估计?
-
什么是标准化和归一化,它们在 R 中如何执行?
总结
在本章中,我们讨论了金融领域中最常用的分布及其在 R 中的度量计算;采样(随机采样和分层采样);中心趋势的度量方法;时间序列模型选择中使用的相关性及其类型;假设检验(单尾/双尾)以及已知和未知方差的情况;离群值检测;参数估计;以及在 R 中对属性进行标准化/归一化,以便将属性转换为可比较的尺度。
在下一章中,将介绍在 R 中进行的与简单线性回归、多元线性回归、方差分析(ANOVA)、特征选择、变量排名、小波分析、快速傅里叶变换(FFT)和希尔伯特变换相关的分析。
第三章:计量经济学与小波分析
在金融分析中,我们需要一些技术来进行预测建模,以进行预测并找出不同目标变量的驱动因素。在本章中,我们将讨论回归类型以及如何在 R 中构建回归模型来建立预测模型。我们还将讨论如何实现变量选择方法以及回归分析中的其他相关方面。本章不包含理论描述,而是指导你如何在金融领域中使用 R 实现回归模型。回归分析可以用于对金融领域的横截面数据进行预测。我们还将涵盖数据的频率分析,以及如何通过快速傅里叶变换、波形变换、希尔伯特变换、哈尔变换等时间和频率域中的变换来去除数据中的噪声。
本章包含以下主题:
-
简单线性回归
-
多元线性回归
-
多重共线性
-
方差分析(ANOVA)
-
特征选择
-
逐步变量选择
-
变量排名
-
小波分析
-
快速傅里叶变换
-
希尔伯特变换
简单线性回归
在简单线性回归中,我们试图通过一个第二个变量(称为预测变量)来预测一个变量。我们要预测的变量称为因变量,表示为y,而自变量表示为x。在简单线性回归中,我们假设因变量和预测变量之间存在线性关系。
首先,我们需要绘制数据图表,以理解因变量和自变量之间的线性关系。这里,我们的数据包含两个变量:
-
YPrice
:因变量 -
XPrice
:预测变量
在这种情况下,我们试图通过XPrice
来预测Yprice
。StockXprice
是自变量,StockYprice
是因变量。对于每一个StockXprice
的元素,都有一个对应的StockYprice
元素,这意味着StockXprice
和StockYprice
之间存在一一映射关系。
以下几行用于分析的数据通过以下代码显示:
>head(Data)
StockYPrice |
StockXPrice |
|
---|---|---|
1 | 80.13 | 72.86 |
2 | 79.57 | 72.88 |
3 | 79.93 | 71.72 |
4 | 81.69 | 71.54 |
5 | 80.82 | 71 |
6 | 81.07 | 71.78 |
散点图
首先,我们将绘制y和x之间的散点图,以理解x和y之间的线性关系类型。执行以下代码时,得到以下散点图:
> YPrice = Data$StockYPrice
> XPrice = Data$StockXPrice
> plot(YPrice, XPrice, xlab="XPrice",
ylab="YPrice")
这里,我们的因变量是YPrice
,预测变量是Xprice
。请注意,这个示例仅用于说明目的:
图 3.1:两个变量的散点图
一旦我们检查了因变量与预测变量之间的关系,我们就会尝试通过这些点拟合一条最佳的直线,这些点表示所有给定预测变量的预测 Y 值。简单的线性回归用以下方程表示,它描述了因变量与预测变量之间的关系:
这里, 和
是参数,
是误差项。
也被称为截距,
是预测变量的系数;它是通过最小化误差项平方和
来获得的。所有统计软件都提供了估计系数的选项,R 也提供该功能。
我们可以使用 R 中的 lm
函数来拟合线性回归模型,如下所示:
> LinearR.lm = lm(YPrice ~ XPrice, data=Data)
这里,Data
是给定的输入数据,Yprice
和 Xprice
分别是因变量和预测变量。一旦我们拟合了模型,我们可以使用以下代码提取参数:
> coeffs = coefficients(LinearR.lm); coeffs
上述结果给出了截距和系数的值:
(Intercept) XPrice
92.7051345 -0.1680975
所以现在我们可以按如下方式写出我们的模型:
> YPrice = 92.7051345 + -0.1680975*(Xprice)
这可以为任何给定的 Xprice
提供预测值。
同样,我们可以执行以下代码,使用拟合的线性回归模型在任何其他数据(例如 OutofSampleData
)上获得预测值,方法如下:
> predict(LinearR.lm, OutofSampleData)
决定系数
我们已经拟合了模型,但现在我们需要测试模型拟合数据的效果。为此有几种可用的衡量标准,最主要的是决定系数。通过以下代码可以获得该系数:
> summary(LinearR.lm)$r.squared
根据定义,它是由自变量解释的因变量方差的比例,也称为 R2。
显著性检验
现在,我们需要检查线性回归模型中变量之间的关系是否显著,显著性水平为 0.05。
我们执行以下代码:
> summary(LinearR.lm)
它提供了线性回归模型的所有相关统计信息,如下所示:
图 3.2:线性回归模型总结
如果与 Xprice
相关的P 值小于 0.05,则预测变量在 0.05 的显著性水平上显著地解释了因变量。
线性回归模型的置信区间
预测值的一个重要问题是找到预测值周围的置信区间。所以让我们尝试找到拟合模型的预测值周围的 95% 置信区间。这可以通过执行以下代码来实现:
> Predictdata = data.frame(XPrice=75)
> predict(LinearR.lm, Predictdata, interval="confidence")
这里,我们正在估算给定 Xprice = 75
的预测值,然后我们尝试找到预测值周围的置信区间。
执行前述代码生成的输出如下图所示:
图 3.3:线性回归模型的置信区间预测
残差图
一旦拟合了模型,我们就将其与观测值进行比较,找出差异,这就是残差。然后,我们将残差与预测变量绘制在一起,以便直观地查看模型的表现。可以执行以下代码来获取残差图:
> LinearR.res = resid(LinearR.lm)
> plot(XPrice, LinearR.res,
ylab="Residuals", xlab="XPrice",
main="Residual Plot")
图 3.4:线性回归模型的残差图
我们还可以通过执行先前提到的代码来绘制标准化残差的残差图:
> LinearRSTD.res = rstandard(LinearR.lm)
> plot(XPrice, LinearRSTD.res,
ylab="Standardized Residuals", xlab="XPrice",
main="Residual Plot")
误差的正态分布
线性回归的假设之一是误差服从正态分布,拟合模型后,我们需要检查误差是否确实服从正态分布。
这可以通过执行以下代码来检查,并与理论正态分布进行比较:
> qqnorm(LinearRSTD.res,
ylab="Standardized Residuals",
xlab="Normal Scores",
main="Error Normal Distribution plot")
> qqline(LinearRSTD.res)
图 3.5:标准化残差的 QQ 图
summary
函数的更多细节可以在 R 文档中找到。以下命令将打开一个窗口,显示关于线性回归模型(即 lm()
)的完整信息。它还包含关于每个输入变量的信息,包括它们的数据类型、该函数返回的所有变量,以及如何提取输出变量,同时提供了相关示例:
> help(summary.lm)
多元线性回归
在多元线性回归中,我们试图通过多个预测变量来解释因变量。多元线性回归方程由以下公式给出:
这里 是多元线性回归的参数,可以通过最小化平方和来获得,这也称为最小二乘法(OLS)估计方法。
假设我们有一个因变量 StockYPrice
,并且我们尝试根据自变量 StockX1Price
、StockX2Price
、StockX3Price
和 StockX4Price
来预测它,这些自变量都存在于数据集 DataMR
中。
现在让我们拟合多元回归模型并获取多元回归的参数估计:
> MultipleR.lm = lm(StockYPrice ~ StockX1Price + StockX2Price + StockX3Price + StockX4Price, data=DataMR)
> summary(MultipleR.lm)
当我们执行前述代码时,它会在数据上拟合多元回归模型,并给出与多元回归相关的基本统计总结:
图 3.6:多元线性回归的总结
就像简单线性回归模型一样,lm
函数估计多元回归模型的系数,如前述总结所示,我们可以将预测方程写为如下形式:
> StockYPrice = 88.42137 +(-0.16625)*StockX1Price
+ (-0.00468) * StockX2Price + (.03497)*StockX3Price+ (.02713)*StockX4Price
对于任何给定的独立变量集,我们可以通过使用前面的方程来找到预测的因变量。
对于任何样本外数据,我们可以通过执行以下代码来获得预测:
> newdata = data.frame(StockX1Price=70, StockX2Price=90, StockX3Price=60, StockX4Price=80)
> predict(MultipleR.lm, newdata)
这会生成输出 80.63105 作为给定独立变量集的预测因变量值。
决定系数
为了检查模型的充分性,主要统计量是决定系数和调整决定系数,这些在汇总表中以 R 平方和调整 R 平方矩阵显示。
我们也可以使用以下代码获取它们:
> summary(MultipleR.lm)$r.squared
> summary(MultipleR.lm)$adj.r.squared
从汇总表中,我们可以看到哪些变量变得显著。如果汇总表中与变量相关的 P 值小于 0.05,则该变量显著,否则不显著。
置信区间
我们可以通过执行以下代码来找到多元回归模型预测值的 95% 置信区间预测区间:
> predict(MultipleR.lm, newdata, interval="confidence")
上述代码生成了以下输出:
图 3.7:多元回归模型的置信区间预测
多重共线性
如果预测变量之间存在相关性,那么我们需要检测多重共线性并加以处理。识别多重共线性至关重要,因为两个或更多变量之间存在相关性,这显示了这些变量之间的强依赖结构,而我们将相关变量作为独立变量使用,这最终会由于它们之间的关系而在预测中产生双重效应。如果我们处理多重共线性并只考虑那些不相关的变量,那么可以避免双重影响的问题。
我们可以通过执行以下代码来找到多重共线性:
> vif(MultipleR.lm)
这显示了预测变量的多重共线性表:
图 3.8:多元回归模型的 VIF 表
根据 VIF 的值,我们可以删除不相关的变量。
方差分析(ANOVA)
方差分析(ANOVA)用于确定三个或更多独立组之间的均值是否存在统计学上的显著差异。如果只有两个样本,我们可以使用 t 检验来比较样本的均值,但如果样本超过两个,可能会非常复杂。我们将研究定量因变量收益与单一定性独立变量股票之间的关系。我们有五个股票级别:stock1、stock2、... stock5。
我们可以通过箱线图研究五个级别的股票,并通过执行以下代码进行比较:
> DataANOVA = read.csv("C:/Users/prashant.vats/Desktop/Projects/BOOK R/DataAnova.csv")
>head(DataANOVA)
这显示了用于分析的一些数据行,以表格形式展示:
Returns |
Stock |
|
---|---|---|
1 | 1.64 | Stock1 |
2 | 1.72 | Stock1 |
3 | 1.68 | Stock1 |
4 | 1.77 | Stock1 |
5 | 1.56 | Stock1 |
6 | 1.95 | Stock1 |
>boxplot(DataANOVA$Returns ~ DataANOVA$Stock)
这会生成以下输出并绘制箱线图:
图 3.9:不同股票水平的箱形图
上述箱形图显示,股票水平较高的回报率较高。如果我们重复这个过程,很可能会得到不同的回报。也有可能所有股票水平的回报率都相似,我们仅仅看到的是一个回报集中的随机波动。假设在任何水平上都没有差异,这是我们的零假设。使用 ANOVA 方法,我们来测试这个假设的显著性:
> oneway.test(Returns ~ Stock, var.equal=TRUE)
执行上述代码将得到以下结果:
图 3.10:不同股票水平的 ANOVA 输出
由于P 值小于0.05,零假设被拒绝。不同股票水平的回报率并不相似。
特征选择
特征选择是金融模型构建中最具挑战性的部分之一。特征选择可以通过统计方法或借助领域知识来完成。在这里,我们将讨论金融领域中的几种统计特征选择方法。
移除无关特征
数据可能包含高度相关的特征,如果模型中没有高度相关的特征,模型表现会更好。Caret R 包提供了查找特征相关矩阵的方法,以下是一个示例。
以下是执行代码后用于相关分析和多重回归分析的几行数据:
>DataMR = read.csv("C:/Users/prashant.vats/Desktop/Projects/BOOK R/DataForMultipleRegression.csv")
>head(DataMR)
StockYPrice |
StockX1Price |
StockX2Price |
StockX3Price |
StockX4Price |
|
---|---|---|---|---|---|
1 | 80.13 | 72.86 | 93.1 | 63.7 | 83.1 |
2 | 79.57 | 72.88 | 90.2 | 63.5 | 82 |
3 | 79.93 | 71.72 | 99 | 64.5 | 82.8 |
4 | 81.69 | 71.54 | 90.9 | 66.7 | 86.5 |
5 | 80.82 | 71 | 90.7 | 60.7 | 80.8 |
6 | 81.07 | 71.78 | 93.1 | 62.9 | 84.2 |
上述输出显示了名为 StockYPrice
、StockX1Price
、StockX2Price
、StockX3Price
和 StockX4Price
的五个变量在 DataMR
数据集中。在这里,StockYPrice
是因变量,其它四个变量是自变量。依赖结构对于深入分析至关重要。
以下命令计算前四列之间的相关矩阵,这些列分别是StockYPrice
、StockX1Price
、StockX2Price
和StockX3Price
:
> correlationMatrix<- cor(DataMR[,1:4])
图 3.11:相关矩阵表
上述相关矩阵显示了哪些变量是高度相关的,因此,特征选择会确保高度相关的特征不会出现在模型中。
步骤式变量选择
我们可以在预测模型中使用步骤式变量选择(前向选择、后向选择、双向选择)进行特征选择,使用 stepAIC()
函数。
这可以通过执行以下代码来完成:
> MultipleR.lm = lm(StockYPrice ~
StockX1Price + StockX2Price + StockX3Price + StockX4Price,
data=DataMR)
> step <- stepAIC(MultipleR.lm, direction="both")
> step$anova
在这里,我们使用的是用于多重回归的输入数据集。也可以使用 leaps()
函数来进行全子集回归,该函数来自 leaps 包。
通过分类进行变量选择
我们可以使用分类技术,如决策树或随机森林,来获取最重要的预测因子。在这里,我们使用随机森林(提供了代码)来找到最相关的特征。以下示例中,数据集DataForMultipleRegression1
中的所有四个属性都已被选择,图表显示了不同子集大小的准确度,比较了所有子集:
>library(mlbench)
>library(caret)
>DataVI = read.csv("C:/Users/prashant.vats/Desktop/Projects/BOOK R/DataForMultipleRegression1.csv")
>head(DataVI)
它显示了用于分析的部分数据,如下表所示:
PortfolioYDirection |
StockX1Price |
StockX2Price |
StockX3Price |
StockX4Price |
|
---|---|---|---|---|---|
1 | 0 | 72.86 | 93.1 | 63.7 | 83.1 |
2 | 1 | 72.88 | 90.2 | 63.5 | 82 |
3 | 0 | 71.72 | 99 | 64.5 | 82.8 |
4 | 0 | 71.54 | 90.9 | 66.7 | 86.5 |
5 | 1 | 71 | 90.7 | 60.7 | 80.8 |
6 | 0 | 71.78 | 93.1 | 62.9 | 84.2 |
执行以下代码进行所需的分析:
>control<- rfeControl(functions=rfFuncs, method="cv", number=10)
>Output <- rfe(DataVI[,1:4], DataVI[,0:1], sizes=c(1:4), rfeControl=control)
>predictors(Output)
>plot(Output, type=c("g", "o"))
它生成了以下图表,展示了不同子集大小的准确度,比较了所有子集:
图 3.12:展示不同子集大小模型准确度的图表
我们提供了一些特征选择的示例。其他一些特征选择方法,如分类技术和预测建模中的信息值,也可以使用。
变量排名
在拟合回归/预测模型后,我们需要了解显著属性在比较尺度上的相对排名。这可以通过 Beta 参数估计来解释。Beta 或标准化系数是当所有变量处于相同尺度时得到的斜率,通常是在进行预测建模(回归)之前,将变量转换为 z 分数。Beta 系数允许比较预测变量的大致相对重要性,因此变量可以被排名,而这既不是非标准化系数也不是 P 值能够做到的。数据向量的缩放或标准化可以使用scale()
函数来完成。一旦创建了标准化变量,回归就会使用这些变量重新执行。结果系数即为 Beta 系数。
小波分析
时间序列信息并不总是足够的来深入了解数据。有时,数据的频率内容也包含关于数据的重要信息。在时域中,傅里叶变换(FT)捕捉数据的频率-幅度信息,但它无法显示这个频率发生在何时。对于平稳数据,所有频率成分在任何时间点都存在,但对于非平稳数据来说,情况并非如此。因此,FT 不适用于非平稳数据。小波变换(WT)具有同时提供时间和频率信息的能力,形式为时频分析。WT 对于分析金融时间序列非常重要,因为大多数金融时间序列都是非平稳的。在本章的剩余部分,我将帮助你了解如何在 R 中使用小波分析解决非平稳数据的问题。股票价格/指数数据需要某些技术或变换,以获得原始数据未能显示的进一步信息。本示例使用了 2010 年 1 月 1 日至 2015 年 12 月 31 日的道琼斯工业平均指数(DJIA)和标准普尔 500 指数(S&P500)的日收盘价。我将使用小波包来进行演示:
-
在开始使用小波变换之前,你需要安装名为
Wavelets
的包:> install.packages('wavelets')
-
一旦你安装了该包,或者你的机器上已经有了该包,那么你只需要将它加载到工作区:
> library(wavelets)
-
为了对数据有一个初步的了解,我们绘制了
dji
和snp
的时间序列及其收益率:> par(mfrow=c(2,1)) > plot(dji,type="l") > plot(ret_dji,type="l")
第一行用于将图形分割成一个两行一列的矩阵,这样图形中可以显示两个子图,接下来的两条命令绘制了道琼斯价格及其收益率系列,可以在图 3.13中看到:
图 3.13:道琼斯指数(DJI)的价格和收益率系列
dji
和snp
的时间序列是非平稳的。我们使用 head 和 tail 查看时间序列的开始和结束部分:
>head (dji)
DJI.Close
2010-01-04 10583.96
2010-01-05 10572.02
2010-01-06 10573.68
2010-01-07 10606.86
2010-01-08 10618.19
2010-01-11 10663.99
>tail (dji)
DJI.Close
2015-12-23 17602.61
2015-12-24 17552.17
2015-12-28 17528.27
2015-12-29 17720.98
2015-12-30 17603.87
2015-12-31 17425.03
现在我们对dji
数据应用离散小波变换(DWT),并使用不同的滤波器对其进行分解。它需要数据的格式为时间序列、矩阵或数据框。我们查看dji
变量的格式,它是xts
和zoo
对象。因此,我们需要将其转换为可接受的格式:
dji<- as.ts (dji)
现在它已经可以在离散小波变换的 R 函数中使用。我们还需要提供其他参数,如使用的滤波器类型以及希望将数据分解的层数:
model<- dwt (dji, filter="la8", n.levels=3)
它将输出保存在名为 model 的变量中。你可以在命令提示符下输入 model,它会显示输出结果:
>model
它生成的输出包含各种信息矩阵,如小波系数、尺度系数、使用的滤波器类型以及使用的层数。你还可以提取任何单独的信息。要提取小波系数,你需要在命令提示符下输入以下命令:
>model
它生成的输出包含各种信息矩阵,如小波系数、尺度系数、使用的滤波器类型以及使用的层数。你还可以提取任何单独的信息。要提取小波系数,你需要在命令提示符下输入以下命令:
>model@W # to extract wavelets coefficients
>model@V # to extract scaling coefficients
这些命令生成了小波和尺度系数的相对列表。要获得小波的单个分量,你需要提到以下内容:
> model@W$W1 # to extract first level of wavelet coefficients
> model@V$V1 # to extract first level of scaling coefficients
我们还可以使用绘图命令来可视化数据系列、小波和尺度系数:
> plot (model)
图 3.14将绘制价格及其各个级别系数,帮助我们清晰地可视化和理解数据:
图 3.14:时间序列、小波和尺度系数的图示
你还可以使用离散小波变换函数来进行haar
滤波器的分析:
model<- dwt (dji, filter="haar", n.levels=3)
> plot (model)
它将使用haar
滤波器绘制数据系列、小波和尺度系数。
要计算逆离散小波变换,你必须使用一个小波对象,该对象通过离散小波变换来定义。变量 model 是一个使用haar
滤波器的小波对象:
imodel<- idwt(model, fast=TRUE)
有时需要知道 R 对象的类别,例如,model
和imodel
。
我们可以使用以下命令来实现:
> class(model)
[1] "dwt"
attr(,"package")
[1] "wavelets"
> class(imodel)
[1] "ts"
变量imodel
是通过逆小波变换创建的,它生成一个原始的时间序列对象。
多分辨率分析(MRA)是另一种广泛应用的小波方法,适用于时间序列分析。金融市场产生大量数据,这些数据经过分析后用于生成算法交易信号。由于多分辨率小波分析能够帮助交易者专注于特定时间尺度上的交易模式,它正被越来越多地应用于这些数据集。以下示例中使用了 la8 滤波器,haar
滤波器也可以被替换为la8
:
> model <- mra(dji, filter="la8", n.levels=3)
对于市场数据的分析,最大重叠离散小波变换(MODWT)是首选方法。
作为示例,我考虑了将道琼斯指数时间序列dji
作为modwt
函数的输入:
> model <- modwt(dji, filter="la8", n.levels=5)
上述函数将时间序列分解为详细的小波和尺度系数,这些可以在图 3.15中看到。可以使用plot.modwt()
函数来绘制该modwt
输出:
>plot.modwt(model)
图 3.15:最大重叠离散小波变换的图示
时间序列中的一些跳跃可以表现为较小系数如W1和W2的跳跃,而平滑系数如W6则表现为围绕某个均值的波动。图 3.15中的小波和尺度系数清晰地展示了不同时间尺度下的价格数据。
小波分析在定量金融中提供了一个重要的工具,应用范围从短期预测到计算与特定时间尺度相关的方差。
快速傅里叶变换
快速傅里叶变换(FFT)用于计算离散时间序列的傅里叶变换。你需要通过以下代码安装相关的fft
包来实现 FFT:
install.packages('fft')
安装完包后,你必须使用以下代码将其加载到工作空间:
library(fft)
时间序列的快速傅里叶变换可以使用fft
计算,它接受实数或复数序列。
在以下示例中,dji
是一个实数时间序列:
> model<- fft(dji)
变量model
是一个变换后的序列,基本由复数构成,实部和虚部可以通过以下代码提取:
>rp = Re(model)
>ip = Im(model)
以下命令计算模型的绝对值:
>absmodel<- abs(model)
让我绘制这个图表,看看fft
的绝对值对我来说有什么信息:
>plot(absmodel)
图 3.16:FFT 建模序列绝对值的绘图
图 3.16显示了数据两端的尖峰。FFT 可以接受复杂的输入,当输入为实数时(如大多数实际情况)。对于大于N/2的频段,输出是冗余的,并且不会提供额外的谱信息。因此,我们可以去除大于N/2的频段值。这是由于缺乏归一化造成的。
结果需要根据样本大小进行归一化。由于输入数据是实数值,因此大于N/2的数据会被去除,我们通过N/2对数据进行归一化处理:
>norm_absmodel<- absmodel[1:(length(dji)/2)]
傅里叶变换序列的实部和虚部之间的角度可以按如下方式计算:
Angle = atan2(ip, rp)
有时分析时间序列的谱密度非常重要,可以使用以下代码在 R 中进行计算:
>spec_density<- spectrum(dji, method = c("pgram", "ar"))
它接受两种方法:周期图法和自回归法。你可以选择这两种方法中的任何一种。此函数返回估算谱密度的频率向量,以及频率处的估算谱密度向量。它还返回一些其他对多变量分析有用的参数,如相干性水平和多变量序列之间的相位。
希尔伯特变换
希尔伯特变换是另一种转换时间序列的技术,R 使用seewave
包来实现这一点。可以通过install.packages()
安装该包,并使用library()
命令加载到工作空间:
> model <- hilbert(dji, 1)
第一个参数是你希望转换的时间序列对象,第二个参数是波形的采样频率。在前面的示例中,我使用了dji
作为时间序列,并将采样频率设置为 1 来计算 Hilbert 变换。
如果你想了解模型的输出,你应该使用以下代码:
> summary(model)
V1
Length:2555
Class :complex
Mode :complex
前面的输出提到输入数据序列的长度为2555
,输出变量model
的类型为复数。
由于输出是复数形式,我们可以使用以下代码提取实部和虚部:
>rp<- Re(model)
>ip<- Im(model)
在这里,实部是原始时间序列,我们的例子中是dji
,而虚部是原始序列的 Hilbert 变换序列。ifreq()
函数返回相位或瞬时频率,具体取决于我们想要的输出:
>ifreq(dji,1,ylim=c(0,0.00001))
前面的代码将生成瞬时频率:
图 3.17:使用 Hilbert 变换的时间序列瞬时频率
然而,如果我们想生成相位,则必须在函数中明确提到PHASE=TRUE
:
>ifreq(dji, 1 ,phase="TRUE",ylim=c(-0.5,1))
图 3.18显示了相对于时间变化的相位。随着时间的推移,相位也随着时间的增加而增加:
图 3.18:使用 Hilbert 变换的时间序列相位
默认情况下,绘图功能是开启的。如果我们设置PLOT=FALSE
,则不会生成图形,只会在工作空间中生成变量:
> output = ifreq(dji, 1 ,plot=FALSE)
输出变量的形式是一个列表,其中包含瞬时频率和相位,可以使用以下方法提取:
>freq<- output$f
>phase<- output$p
有时我们分析一对时间序列,计算相位差比单变量序列的相位更为重要。因此,相位差可以通过简单地计算各个序列的相位,然后从一个序列中减去另一个序列的相位来计算:
>phase_difference<- phase1 - phase2
还有一个包,waveslim
,它将所有这些变换(例如离散小波变换、快速傅里叶变换和 Hilbert 变换)都集成在一个包中。实际上,还有许多其他包包含这些变换。你可以使用任何你感到舒适且易于使用的包。
问题
-
定义回归以及如何在 R 中实现回归。
-
如何在 R 中找到线性回归/多重回归的决定系数?
-
如何在 R 中找到拟合线性回归/多重回归的预测值的置信区间?
-
如何在 R 中检测多重回归中的多重共线性?
-
ANOVA 的意义是什么?你如何使用它比较两个线性回归模型的结果?
-
如何在 R 中进行多重线性回归的特征选择?
-
如何在 R 中为多重线性回归模型排序重要性属性?
-
如何安装
waveslim
包并将其加载到 R 工作空间中? -
如何绘制时间序列并提取时间序列的头部和尾部?
-
如何知道由
fft
函数创建的变量的类别? -
如何使用给定的滤波器使用 dwt 函数并进行逆 dwt 变换?
-
如何提取序列的实部和虚部?
-
如何使用快速傅里叶变换和希尔伯特变换?
摘要
回归是任何分析的基础,读者不能在不接触回归的情况下继续学习。在本章中,我介绍了线性回归和多元回归,以及它们在预测中的应用。R 函数lm()
用于实现简单线性回归和多元线性回归。我还介绍了显著性检验、残差计算和正态性图,这些方法使用 qq 图测试残差的正态性。方差分析(ANOVA)用于选择两个或更多样本的均值差异。多元线性回归涉及许多变量,每个变量的系数不同,从而使每个变量的重要性有所不同,并按此排名。逐步回归用于选择在回归分析中重要的变量。时间序列分析有时并不能代表完整的信息,因此需要进行频率分析,这可以通过小波变换、快速傅里叶变换和希尔伯特变换来实现。所有这些方法都可以在 R 中进行频率分析。我还解释了如何在必要时查看和绘制结果。
在下一章中,我将解释时间序列分析和预测技术。
第四章:时间序列建模
时间序列预测分析是量化金融中最重要的组成部分之一。R 软件提供了许多时间序列和预测包来支持时间序列分析。R 中有足够的包可以将等距和不等距的系列转换为时间序列。此外,R 中还有足够的包可以构建预测模型,如自回归积分滑动平均(ARIMA)和广义自回归条件异方差(EGARCH)。在本章中,我们将简要介绍如何将任何系列转换为时间序列并构建预测模型。
在本章中,我们将涵盖以下主题:
-
一般时间序列
-
将数据转换为时间序列
-
zoo
-
xts
-
线性滤波器
-
AR
-
MA
-
ARIMA
-
GARCH
-
EGARCH
-
VGARCH
-
动态条件相关性
一般时间序列
时间序列是通常在固定间隔收集的数据序列。许多领域的数据都是以时间序列的形式存储的,并需要进行分析以便未来的规划。
例如,在金融领域,我们有每日/每月的失业率、GDP、日汇率、股价等数据。因此,所有投资者或金融机构的工作人员都需要规划未来的战略,因此他们希望分析时间序列数据。因此,时间序列在金融领域起着至关重要的作用。
时间序列数据本质上是非常不可预测的,为了理解数据,我们需要将时间序列数据分解成多个组成部分,如下所示:
-
趋势:这是时间序列数据均值的长期波动模式。趋势可能是线性或非线性的,并且随着时间的推移不断变化。没有确定的方法可以精确识别趋势,但如果它表现出单调性,那么可以在一定可接受的误差范围内进行估算。
-
季节效应:这些是与周期性循环相关的波动。例如,某个产品的销售在每年的特定月份/季度可能会激增。季节性可以通过绘制序列并检查其图形来识别。
-
周期(Ct):除了季节性周期外,还有一些与商业周期相关的周期,这些周期在进行时间序列分析时需要考虑。
-
残差:时间序列由系统性模式和随机噪声(误差)组成,这使得识别模式变得困难。通常,时间序列技术涉及某些去噪方法,以便使模式更加突出。
在一些预测技术中,时间序列假设是平稳的。平稳性是必需的,因为在进行预测时,我们假设均值和方差是静态的,这将是未来预测分析所需要的。如果序列是非平稳的,那么我们需要对其进行差分以使其变为平稳,然后再继续分析。
将数据转换为时间序列
时间序列是一个数据点序列,其中每个数据点都与特定的时间相关联。
例如,股票的调整收盘价是股票在特定日期的收盘价格。时间序列数据存储在一个名为时间序列对象的 R 对象中,并通过使用 R 中的ts()
函数创建。
ts
的基本语法如下:
ts(data, start, end, frequency)
这里:
-
data
:它是包含数据值的向量或矩阵 -
start
:它是第一次观察的起始点或时间 -
end
:它是最后一次观察的时间点 -
frequency
:它是每单位时间的数据点数量
让我们考虑一个向量,代码如下:
> StockPrice<
-c(23.5,23.75,24.1,25.8,27.6,27,27.5,27.75,26,28,27,25.5)
> StockPrice
现在将其转换为时间序列对象,可以使用以下代码完成:
> StockPricets<- ts(StockPrice,start = c(2016,1),frequency = 12)
> StockPricets
输出如下:
图 4.1:显示时间序列对象的表格
让我们使用以下代码绘制此数据:
> plot(StockPricets)
这将生成以下输出图:
图 4.2:使用 ts 对象的时间序列图
ts()
函数中的频率参数确定数据的时间间隔:
-
频率 = 12 表示数据为月度级别
-
频率 = 4 表示数据为季度级别
-
频率 = 6 表示每小时有每 10 分钟的一个数据点
-
频率 = 5 表示数据为每日工作日级别
zoo
ts
对象在表示时间序列时有其局限性。它用于表示等间隔数据。它不能用于表示日常股票价格,因为股票价格在周一到周五之间是等间隔的,但从周五到周一,或者在工作日有市场假期时,就不相同。这类不等间隔的数据无法通过ts
对象表示。
zoo
是灵活的,完全能够处理不等间隔数据、等间隔数据和数值索引数据。
让我们首先安装并加载zoo
库。这可以通过执行以下代码完成:
> install.packages("zoo")
> library(zoo)
现在我们将讨论如何使用zoo
表示不同的时间序列场景。
请注意,我们将在所有示例中使用一个常见的数据集。
构造一个 zoo 对象
为了创建一个zoo
对象,必须提供有序的时间索引和数据。因此,我们将构造一个zoo
对象。
让我们首先导入我们样本数据集的几行,可以使用以下代码完成:
>StockData <- read.table("DataChap4.csv",header = TRUE, sep = ",",nrows=3)
这将产生以下输出:
Date |
Volume |
Adj.Close |
Return |
---|---|---|---|
12/14/2016 | 4144600 | 198.69 | 0.27 |
12/13/2016 | 6816100 | 198.15 | 2.97 |
12/12/2016 | 615800 | 192.43 | 0.13 |
现在让我们尝试将这个 DataFrame 转换为zoo
对象。这可以通过执行以下代码完成:
> dt = as.Date(StockData$Date, format="%m/%d/%Y")
>Stockdataz = zoo(x=cbind(StockData$Volume,StockData$Adj.Close), order.by=dt)
> colnames(Stockdataz) <- c("Volume","Adj.Close")
> Stockdataz
执行后,它生成以下zoo
对象:
Volume |
Adj.Close |
|
---|---|---|
12/12/2016 | 615800 | 192.43 |
12/13/2016 | 6816100 | 198.15 |
12/14/2016 | 4144600 | 198.69 |
使用 zoo 读取外部文件
函数 read.zoo
是一个包装器,可用于读取外部数据集,假设第一列是索引,其他列是数据。
现在让我们使用 zoo
读取一个数据集,该数据集的格式如下:
Date |
Volume |
Adj Close |
Return |
---|---|---|---|
12/14/2016 | 4144600 | 198.69 | 0.27 |
我们执行以下代码:
>StockData <- read.zoo("DataChap4.csv",header = TRUE, sep = ",",format="%m/%d/%Y")
这将给出如下格式的输出:
Volume |
Adj.Close |
Return |
|
---|---|---|---|
2016-12-14 | 4144600 | 198.69 | 0.27 |
zoo
对象的优势
以下是一些展示 zoo
对象优势的例子。
数据子集化
可以使用 window()
函数对索引进行子集化,方法是执行以下代码:
>window(StockData, start=as.Date("2016/11/1"), end=as.Date("2016/11/3"))
这将给出以下输出:
Volume |
Adj.Close |
Return |
|
---|---|---|---|
11/1/2016 | 7014900 | 190.79 | -3.51 |
11/2/2016 | 4208700 | 188.02 | -1.45 |
11/3/2016 | 2641400 | 187.42 | -0.32 |
合并 zoo 对象
让我们形成两个具有共同索引的 zoo
对象,然后将它们合并。可以通过执行以下代码完成此操作:
> StockData <- read.table("DataChap4.csv",header = TRUE, sep = ",",nrows=3)
> zVolume <-zoo(StockData[,2:2],as.Date(as.character(StockData[, 1]), format="%m/%d/%Y"))
> zAdj.Close <-zoo(StockData[,3:3],as.Date(as.character(StockData[, 1]), format="%m/%d/%Y"))
> cbind(zVolume, zAdj.Close)
最终输出如下表所示:
zVolume |
zAdj.Close |
|
---|---|---|
12/12/2016 | 615800 | 192.43 |
12/13/2016 | 6816100 | 198.15 |
12/14/2016 | 4144600 | 198.69 |
绘制 zoo 对象
你可以跨时间绘制你的数据。这里展示了一个示例:
>plot(StockData$Adj.Close)
这将生成以下图表:
图 4.3:使用 zoo 对象的时间序列图
zoo
对象的缺点
zoo
对象中的索引不能具有 Date
类变量,而 xts
对象的索引必须是已知且受支持的 time
或 Date
类。此外,在 zoo
中,不能添加任意属性,而在 xts
中可以添加。
xts
xts
是一种可扩展的时间序列对象,包含了 zoo
对象的所有特性。它由一个矩阵和时间为基础的索引构成。构建 xts
对象有两种方式:一种是调用 as.xts
,另一种是从头开始构建 xts
对象。
使用 as.xts 构造 xts 对象
让我们通过 zoo
读取几行示例数据,并通过执行以下代码构建 xts
对象:
> StockData <- read.zoo("DataChap4.csv",header = TRUE, sep = ",",format="%m/%d/%Y",nrows=3)
> matrix_xts <- as.xts(StockData,dateFormat='POSIXct')
> matrix_xts
这将给出以下输出:
Volume |
Adj.Close |
Return |
|
---|---|---|---|
12/12/2016 | 615800 | 192.43 | 0.13 |
12/13/2016 | 6816100 | 198.15 | 2.97 |
12/14/2016 | 4144600 | 198.69 | 0.27 |
xts
对象的组成可以通过以下代码给出:
> str(matrix_xts)
这将生成以下输出:
2016-12-12/2016-12-14 的 xts
对象包含如下内容:
Data: num [1:3, 1:3] 615800 6816100 4144600 192 198 ...
- attr(*, "dimnames")=List of 2
..$ : NULL
..$ : chr [1:3] "Volume" "Adj.Close" "Return"
Indexed by objects of class: [Date] TZ: UTC
xts Attributes:
List of 1
$ dateFormat: chr "POSIXct"
从头构建 xts 对象
我们首先形成一个矩阵和日期序列,确保它们具有相同的顺序,然后将其转换为 xts
对象。可以通过执行以下代码完成此操作:
> x<-matrix(5:8, ncol =2, nrow =2)
> dt<-as.Date(c("2016-02-02","2016-03-02"))
> xts_object<-xts(x,order.by=dt)
> colnames(xts_object) <- c("a","b")
> xts_object
这将给出 xts
对象,如下所示:
a |
b |
|
---|---|---|
2/2/2016 | 5 | 7 |
3/2/2016 | 6 | 8 |
xts
对象的特殊之处在于,它表现得像一个矩阵,并且每个观测值都关联了一个时间。子集将始终保持矩阵形式,且xts
对象的属性始终保留。而且,由于xts
是zoo
的子类,因此它具备zoo
库的所有功能。
线性滤波器
时间序列分析的第一步是将时间序列分解为趋势、季节性等成分。
从时间序列中提取趋势的一种方法是线性滤波器。
线性滤波器的一个基本示例是具有相等权重的移动平均。
线性滤波器的示例包括每周平均、每月平均等。
用于查找滤波器的函数如下所示:
Filter(x,filter)
这里,x
是时间序列数据,而filter
是用于计算移动平均的系数。
现在让我们将StockData
的Adj.Close
转换为时间序列,并计算每周和每月的移动平均并绘制出来。这可以通过执行以下代码完成:
> StockData <- read.zoo("DataChap4.csv",header = TRUE, sep = ",",format="%m/%d/%Y")
>PriceData<-ts(StockData$Adj.Close, frequency = 5)
> plot(PriceData,type="l")
> WeeklyMAPrice <- filter(PriceData,filter=rep(1/5,5))
> monthlyMAPrice <- filter(PriceData,filter=rep(1/25,25))
> lines(WeeklyMAPrice,col="red")
> lines(monthlyMAPrice,col="blue")
这生成了以下的图表:
图 4.4:使用线性滤波器的移动平均示例
AR
AR 代表自回归模型。其基本概念是未来的值依赖于过去的值,并通过加权平均的方式估算过去的值。AR 模型的阶数可以通过绘制时间序列的自相关函数和部分自相关函数来估算。时间序列的自相关函数衡量的是序列与其滞后值之间的相关性,而部分自相关函数则衡量的是时间序列与其滞后值之间的相关性,同时控制所有短期滞后值的影响。所以,首先让我们绘制序列的acf
和pcf
。我们先通过执行以下代码绘制acf
图:
> PriceData<-ts(StockData$Adj.Close, frequency = 5)
> acf(PriceData, lag.max = 10)
这生成了如下所示的自相关图:
图 4.5:价格的 acf 图
现在让我们通过执行以下代码绘制pacf
:
> pacf(PriceData, lag.max = 10)
这生成了部分自相关图,如下所示:
图 4.6:价格的 pacf 图
前面的图是所考虑序列的自相关和部分自相关图。现在让我们来确定 AR 的阶数。由于这里没有差分,并且acf
衰减缓慢,而pacf
在一个滞后后截断,因此 AR 的阶数是 1。类似地,如果pacf
在第二个滞后后截断,而acf
衰减缓慢,则 AR 的阶数是 2。
MA
MA 代表移动平均,在 MA 建模中,我们不考虑实际序列的过去值。我们考虑的是过去几期预测误差的移动平均。在确定 MA 的阶数时,我们也需要绘制acf
和pacf
。因此,让我们绘制StockData
的成交量的acf
和pacf
,以评估 MA 的阶数。acf
可以通过执行以下代码绘制:
> VolumeData<-ts(StockData$Volume, frequency = 5)
> acf(VolumeData, lag.max = 10)
这生成了以下的acf
图:
图 4.7:交易量的 acf 图
让我们通过执行以下代码绘制交易量的 pacf
图:
> pacf(VolumeData, lag.max = 10)
这生成了以下图:
图 4.8:交易量的 pacf 图
在评估前述图后,acf
在 lag1
后急剧下降,因此 MA 的阶数为 1。
ARIMA
ARIMA 代表 自回归积分滑动平均模型。通常,它由方程 ARIMA(p, d, q) 定义。
这里,
-
p 是自回归模型的阶数
-
d 是使序列平稳所需的阶数
-
q 是移动平均模型的阶数
ARIMA 的第一步是绘制序列,因为我们需要一个平稳序列来进行预测。
所以让我们先通过执行以下代码绘制序列图:
> PriceData<-ts(StockData$Adj.Close, frequency = 5)
> plot(PriceData)
这生成了以下图:
图 4.9:价格数据图
显然,检查后,序列似乎是非平稳的,因此我们需要通过差分使其平稳。这可以通过执行以下代码完成:
> PriceDiff <- diff(PriceData, differences=1)
> plot(PriceDiff)
这为差分序列生成了以下图:
图 4.10:差分价格数据图
这是一个平稳序列,因为其均值和方差似乎在时间上保持不变。此外,我们还可以通过 Dickey-Fuller
检验来检查序列的平稳性。因此,我们已经确定了我们 ARIMA 模型中的 d 值,即 1。现在让我们绘制差分序列的自相关函数和偏自相关函数,以确定 p 和 q 的值。
acf
图是通过执行以下代码生成的:
> acf(PriceDiff, lag.max = 10)
图 4.11:差分序列的 acf 图
pacf
图是通过执行以下代码生成的:
> pacf(PriceDiff, lag.max = 10)
这为差分序列生成了 pacf
图:
图 4.12:差分序列的 pacf 图
这清楚地表明 AR 和 MA 阶数分别为 0 和 1,因此最好的候选模型是 ARIMA(0,1,1)。
现在让我们估计所识别的 ARIMA 模型的系数,可以通过执行以下代码来完成:
>PriceArima <- arima(PriceData, order=c(0,1,1))
>PriceArima
这生成了所识别 ARIMA 模型的系数,如下所示:
图 4.13:ARIMA (0,1,1) 的拟合总结
现在让我们尝试预测并绘制预测结果,可以通过执行以下代码来完成:
> library(forecast)
> FutureForecast<-forecast.Arima(PriceArima,h=5)
> FutureForecast
这生成了以下输出:
图 4.14:带置信区间的未来预测图
现在通过执行以下代码绘制预测值及其置信区间:
> plot.forecast(FutureForecast)
这生成了以下图:
图 4.15:预测值及其置信区间图
可以通过执行以下代码检查模型的适应性:
>Box.test(FutureForecast$residuals, lag=20, type="Ljung-Box")
这生成了以下输出:
图 4.16:拟合模型的模型适应性检查统计量
由于 P 值大于 0.05,因此在滞后 1-20 中残差没有显著的自相关:
GARCH
GARCH代表广义自回归条件异方差。OLS 估计中的一个假设是误差的方差应为常数。然而,在金融时间序列数据中,某些时期的波动性相对较高,这会导致残差的强度增加,而且这些波动并非随机出现,而是由于自相关效应(也称为波动聚类),即高波动时期往往会集中在一起。这正是 GARCH 用于预测波动率的地方,这些预测可以用于预测模型中的残差。我们不会深入讨论,但我们将展示如何在 R 中执行 GARCH。
在 R 中有各种包可用于 GARCH 建模,我们将使用rugarch
包。
首先安装并加载rugarch
包,可以通过执行以下代码来完成:
>install.packages("rugarch")
>Library(rugarch)
>snp <- read.zoo("DataChap4SP500.csv",header = TRUE, sep = ",",format="%m/%d/%Y")
现在让我们为 GARCH 模型定义规格,并通过运行以下代码来尝试估计系数:
> gspec.ru <- ugarchspec(mean.model=list( armaOrder=c(0,0)), distribution="std")
> gfit.ru <- ugarchfit(gspec.ru, snp$Return)
> coef(gfit.ru)
这给出了以下输出:
图 4.17:GARCH 系数估计摘要
GARCH 建模的主要参数如下:
-
方差模型:包含方差模型规格的列表,特别是使用哪个 GARCH 模型以及 ARCH(q)和 GARCH(p)的阶数应是多少。
-
均值模型:包含均值模型规格的列表:arma 阶数,即自回归(AR)和移动平均(MA)阶数。
-
分布模型:用于创新的条件密度。有效的选择包括
norm
(正态分布)、snorm
(偏态正态分布)、std
(学生 t 分布)等。
现在我们可以根据需求生成预测,以下是相关代码:
> FutureForecast=ugarchforecast(gfit.ru, n.ahead = 5)
> FutureForecast
输出如下:
图 4.18:GARCH 模型预测
GARCH 模型有许多选项,我们可以根据需求使用。
EGARCH
EGARCH 代表指数 GARCH。EGARCH 是 GARCH 的一种改进形式,能够更好地模拟一些市场情景。
例如,负面冲击(事件、新闻等)往往对波动率的影响大于正面冲击。
该模型与传统的 GARCH 模型在结构上有所不同,因为它采用了方差的对数。
让我们通过一个例子来展示如何在 R 中执行 EGARCH。首先为 EGARCH 定义spec
并估计系数,可以通过在snp
数据上执行以下代码来完成:
> snp <- read.zoo("DataChap4SP500.csv",header = TRUE, sep = ",",format="%m/%d/%Y")
> egarchsnp.spec = ugarchspec(variance.model=list(model="eGARCH",garchOrder=c(1,1)),
+ mean.model=list(armaOrder=c(0,0)))
> egarchsnp.fit = ugarchfit(egarchsnp.spec, snp$Return)
> egarchsnp.fit
> coef(egarchsnp.fit)
这给出了以下系数:
图 4.19:EGARCH 的参数估计
现在让我们尝试进行预测,可以通过执行以下代码来完成:
> FutureForecast=ugarchforecast(egarchsnp.fit, n.ahead = 5)
> FutureForecast
这给出了以下输出:
图 4.20:EGARCH 预测结果
VGARCH
VGARCH 代表向量 GARCH 或多元 GARCH。在金融领域,假设金融波动性随着时间的推移在不同的资产和市场之间共同波动。通过多元建模框架认识到这一点,有助于建立比单变量模型更好的模型。它有助于在多个领域做出更好的决策工具,如资产定价、投资组合选择、期权定价以及对冲和风险管理。R 中有多种选项可以用来构建多元模式。
让我们考虑一个 R 中多元 GARCH 的示例,使用 S&P500
和 DJI 指数的去年的数据:
>install.packages("rmgarch")
>install.packages("PerformanceAnalytics")
>library(rmgarch)
>library(PerformanceAnalytics)
>snpdji <- read.zoo("DataChap4SPDJIRet.csv",header = TRUE, sep = ",",format="%m/%d/%Y")
>garch_spec = ugarchspec(mean.model = list(armaOrder = c(2,1)),variance.model = list(garchOrder = c(1,1), model = "sGARCH"), distribution.model = "norm")
> dcc.garch_spec = dccspec(uspec = multispec( replicate(2, garch_spec) ), dccOrder = c(1,1), distribution = "mvnorm")
> dcc_fit= dccfit(dcc.garch_spec,data = snpdji)
> fcst=dccforecast(dcc_.fit,n.ahead=5)
> fcst
这将给出以下输出:
图 4.21:多元 GARCH 的未来预测
动态条件相关性
多元 GARCH 模型是线性的,涉及数据的平方和交叉积,通常用于估计随时间变化的相关性。现在可以使用动态条件相关性(DCC)来估计,这是一种将单变量 GARCH 模型与用于相关性的简洁参数化模型结合的方法。已经观察到,在各种情境下它表现良好。这种方法具备单变量 GARCH 的灵活性,并且没有多元 GARCH 的复杂性。
现在让我们看看如何在 R 中执行 DCC。
首先,我们需要安装并加载 rmgarch
和 PerformanceAnalytics
包。可以通过执行以下代码来完成:
install.packages("rmgarch")
install.packages("PerformanceAnalytics")
library(rmgarch)
library(PerformanceAnalytics)
现在让我们考虑 S&P 500
和 DJI 指数的去年的回报,并尝试为这些回报计算 DCC。
现在让我们通过执行以下代码设置 DCC 的规范:
snpdji <- read.zoo("DataChap4SPDJIRet.csv",header = TRUE, sep = ",",format="%m/%d/%Y")
> garchspec = ugarchspec(mean.model = list(armaOrder = c(0,0)),
+ variance.model = list(garchOrder = c(1,1),
+ model = "sGARCH"), distribution.model = "norm")
>
> dcc.garchsnpdji.spec = dccspec(uspec = multispec( replicate(2, garchspec) ), dccOrder = c(1,1), distribution = "mvnorm")
现在让我们拟合模型,可以通过执行以下代码来完成:
> dcc_fit = dccfit(dcc.garchsnpdji.spec , data = snpdji, fit.control=list(scale=TRUE))
> dcc_fit
这将给出以下输出:
图 4.22:DCC 的拟合摘要
由于预测已在之前的主题中展示,因此在此不再讨论。
问题
-
请给出一个使用
ts()
函数将数据序列转换为时间序列的示例。 -
zoo
和xts
与ts()
函数有何不同?请给出构造xts
和zoo
对象的示例。 -
如何使用
zoo
读取文件? -
如何检查时间序列的平稳性?
-
如何在 R 中识别 AR(2) 模型?
-
如何在 R 中识别 MA(2) 模型?
-
提供一个给定模型的示例并在 R 中执行。
GARCH,
EGARCH,
VGARCH
-
如何在 R 中识别 ARIMA(1,1,1) 模型?
-
提供一个给定模型的示例并在 R 中执行。
总结
在本章中,我们讨论了如何将时间序列分解成其各个组件,如趋势、季节性、周期性和残差。此外,我还讨论了如何在 R 中将任何序列转换为时间序列,并如何执行各种预测模型,如线性滤波器、AR、MA、ARMA、ARIMA、GARCH、EGARCH、VGARCH 和 DCC,并进行预测。
在下一章中,将讨论使用 R 进行交易的不同概念,首先是趋势,其次是策略,再其次是使用三种不同方法的配对交易。还将讨论资本资产定价、多因子模型和投资组合构建。还将讨论用于构建交易策略的机器学习技术。
第五章:算法交易
算法交易被定义为使用预定义规则(称为算法)进行金融工具的买卖。交易者使用预测建模、时间序列建模和机器学习来预测资产的价格、回报或价格变动方向。
算法由量化交易员或量化研究员开发,并在历史数据上进行测试。算法在用于实时交易之前会经过严格的测试。如果技术指标完全自动化,则基于技术指标的交易也可以归入算法交易。然而,有时量化交易员也会使用基本数据,如市值、现金流、债务股本比率等,来定义算法规则。人们可以自由使用任何技术来定义算法规则。最近,投资或交易公司开始深入研究机器学习方法,以预测价格、回报或价格变动方向。
我将在下一章介绍基于机器学习的交易。
在本章中,我将介绍一些在行业中常用的交易策略及其实现。具体来说,我将涵盖以下主题:
-
动量或方向性交易
-
配对交易
-
资本资产定价模型
-
多因子模型
-
投资组合构建
为此,我将需要一些特定的 R 包,如quantmod
、tseries
、xts
、zoo
和PerformanceAnalytics
,你可以使用以下命令安装它们:
install.packages('package name')
一旦安装了某个包,你应该将其加载到工作区以使用其功能,为此,你需要在 R 代码中包含以下命令:
library('package name')
动量或方向性交易
动量交易是指在金融工具价格上涨或下跌时进行交易,换句话说,就是跟随趋势继续交易,就像历史上的赢家预期会继续赢,历史上的输家预期会继续输一样。你押注于金融工具的方向,目标是以低价买入,高价卖出。我不会介绍动量交易策略的优缺点以及不同类型的动量交易策略,这些留给交易者自己去设计。我将介绍如何在 R 中实现动量交易规则并使用历史数据进行回测。股票回报取决于多种因素,在本章后面,我将向你展示如何使用多因子模型来解释股票回报。
让我从简单的技术指标开始。
技术指标在quantmod
包中实现,所以我将使用quantmod
来处理:
> library('quantmod')
>getSymbols("^DJI",src="img/yahoo")
[1] "DJI"
> head(DJI)
我们必须首先将quantmod
包加载到 R 工作空间中,第一行代码解释了如何操作。接下来,我们从 Yahoo 数据仓库提取道琼斯指数(DJI)数据。数据包含多个列,例如DJI.Open
、DJI.High
、DJI.Low
、DJI.Close
等。可以使用head
(dji
)命令查看这些列。下一行展示了如何提取仅包含收盘价的数据并存储到新的variabledji
中:
>dji<- DJI[,"DJI.Close"]
> class(dji)
[1] "xts" "zoo"
前面的行显示了dji
类为xts
,并且zoo
表示dji
是按时间索引格式存储的,因此我使用以下命令提取dji
数据,在两个指定日期之间:
>dji<- dji[(index(dji) >= "2010-01-01" & index(dji) <= "2015-12-31"),]
Delt()
将原始收盘价转换为回报,默认情况下,它计算的是一个周期的回报:
>ret_dji<- Delt(dji,k=1)
你可以使用Delt()
函数计算任意周期的回报,使用参数k
来指定周期。然而,也有选项可以计算股票价格的正常回报或对数回报。在以下命令中,我使用了k=1:3
,意味着我们正在计算从滞后期1
到3
的dji
回报,步长为1
:
>ret_dji<- Delt(dji,k=1:3)
你可以使用head()
查看前述命令的输出。在以下结果中,Delt.1
、Delt.2
和Delt.3
分别是滞后期1
、2
和3
的回报:
> head(ret_dji)
Delt.1 Delt.2 Delt.3
2010-01-04 NA NA NA
2010-01-05 -0.0011281628 NA NA
2010-01-06 0.0001570331 -0.0009713069 NA
2010-01-07 0.0031380432 0.0032955691 0.002163688
2010-01-08 0.0010681840 0.0042095792 0.004367273
2010-01-11 0.0043133342 0.0053861256 0.008541071
前述输出中有几个NA
值,这是因为数据的起始点。在第一列中,第一个点没有任何参考值来计算回报。因此,第一个点将是NA
,从第二个点开始,我们得到回报值。在第二列中,我们需要计算当前数据点与前两个数据点的回报,对于前两个数据点,这是不可能的,因此它们是NA
,类似地,第三列中的前三个数据点也是NA
。
Delt()
函数有更多的参数,每个参数都有其特定的类型,这些类型对该函数是特定的。有时需要查看输出,了解它生成的是哪种回报以及输出的格式。如果你想了解更多有关该函数及其参数的内容和示例,可以使用以下命令,打开另一个窗口,详细解释该函数的所有细节:
> ? Delt
图 5.1中的道琼斯指数收盘价显示了一个明显的趋势。我们需要定义一组指标和规则,这些指标和规则能够在适当的时机生成信号,并有可能带来正向的投资回报。理解模型的泛化能力非常重要,因此我们应当将数据集分为两个较小的数据集,一个数据集包含 70-80%的数据,另一个数据集包含剩余的 20-30%的数据。第一个数据集称为训练集(in-sample dataset),第二个数据集称为测试集(out-sample dataset):
图 5.1:道琼斯指数收盘价和回报系列
为了回测我们的策略理念及其泛化能力,我们必须将数据集分成两个较小的数据集,分别称为样本内数据集和样本外数据集。这里我将定义四个日期。in_sd
定义了样本内数据开始的日期,in_ed
定义了样本内数据结束的日期。类似地,out_sd
和out_ed
分别定义了样本外数据开始和结束的日期。由于我们的数据是时间序列格式,所以日期是按顺序定义的,我们有兴趣建立一个基于历史数据的模型,并应用于实时数据,也就是说,数据集的日期必须晚于历史数据:
>in_sd<- "2010-01-01"
>in_ed<- "2014-12-31"
>out_sd<- "2015-01-01"
>out_ed<- "2015-12-31"
变量in_dji
和in_ret_dji
分别包含之前定义的样本内日期范围内的道琼斯指数收盘价和回报数据,而out_dji
和out_ret_dji
分别包含之前定义的样本外日期范围内的道琼斯指数收盘价和回报数据:
>in_dji<- dji[(index(dji) >= in_sd& index(dji) <= in_ed),]
>in_ret_dji<- ret_dji[(index(ret_dji) >= in_sd& index(ret_dji) <= in_ed),]
>out_dji<- dji[(index(dji) >= out_sd& index(dji) <= out_ed),]
>out_ret_dji<- ret_dji[(index(ret_dji) >= out_sd& index(ret_dji) <= out_ed),]
创建样本内和样本外数据集的目的是合理的,有助于控制人为偏差对参数估计的影响。我们应该使用样本内数据来回测我们的策略,估计最佳参数集,并评估其性能。最佳参数集必须应用于样本外数据,以理解规则和参数的泛化能力。如果在样本外数据上的表现与样本内数据非常相似,我们可以认为这些参数和规则集具有良好的泛化能力,可以用于实时交易。
我将使用移动平均收敛发散(MACD)和布林带指标来生成自动化交易信号。MACD 和布林带指标通过以下两行代码计算得出。我在这两个函数中使用了相同的参数值;不过,你可以使用你认为最适合数据集的参数。输出变量macd
包含 MACD 指标及其信号值;而输出变量bb
包含下轨、平均值、上轨和百分比布林带:
>macd<- MACD(in_dji, nFast =12, nSlow = 26, nSig = 9,maType="SMA", percent = FALSE)
> bb <- BBands(in_dji, n = 20, maType="SMA", sd = 2)
第一行创建了变量signal
并将其初始化为NULL
。在第二行,当dji
超过上轨布林带且macd
值高于其macd-signal
值时,我生成了买入信号(1
);当dji
低于下轨布林带且macd
小于其macd-signal
值时,生成了卖出信号(-1
);当信号为0
时,表示不在市场中:
> signal <- NULL
> signal <- ifelse(in_dji> bb[,'up'] &macd[,'macd'] >macd[,'signal'],1,ifelse(in_dji< bb[,'dn'] &macd[,'macd'] <macd[,'signal'],-1,0))
我已经为多头和空头都生成了信号;不过,你也可以仅实施多头或空头策略。你还可以修改此信号生成机制,使用任何你希望的其他退出标准。我们没有包含任何交易成本和滑点成本来计算其性能,因为这些策略并非直接用于交易。这些策略仅用于展示实现机制:
>trade_return<- in_ret_dji*lag(signal)
交易收益是通过道琼斯指数的收益和前一天的信号计算的。我将使用PerformanceAnalytics
包来计算策略表现的各种矩阵。
首先,你应该将此包加载到 R 工作空间中:
> library(PerformanceAnalytics)
>cumm_ret<- Return.cumulative(trade_return)
>annual_ret<- Return.annualized(trade_return)
策略的累积收益和年化收益可以通过前面两行代码进行计算。Chart.PerformanceSummary
在给定时间点绘制累积收益和日收益图,并显示回撤,如图 5.2所示:
>charts.PerformanceSummary(trade_return)
图 5.2:策略的累积收益、日收益和回撤
要了解更多关于交易收益的表现,你需要使用summary()
命令。summary()
将显示所有交易收益的最小值、第一个四分位数、中位数、均值、第三个四分位数和最大值。变量trade_return
中也有一些NA
,summary()
会显示NA
的数量。在下面的代码行中,我们首先将trade_return
转换为时间序列对象,因为它会生成特定格式的输出。输出显示了最小值、第一个四分位数、中位数、第三个四分位数、最大值和NA
。NA
的值为20
,这意味着trade_return
中有20
个NA
值:
> summary(as.ts(trade_return))
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
-0.039770 0 0 0.000062 0 0.055460 20
以下是一些计算策略在样本数据上表现的命令。
第一个命令是计算整个交易期间的最大回撤,我们可以看到0.1173028
表示最大回撤为 11.73%。第二个和第三个命令是计算交易收益的日标准差和年化标准差。接下来是策略收益的VaR
计算,最后两个命令分别计算策略的日度和年化夏普比率。
日度夏普比率为0.01621421
,年化夏普比率为0.2289401
。夏普比率有两个参数:Rf
和FUN
。Rf
是无风险利率,FUN
是分母。在夏普比率的计算中,我使用了FUN=StdDev
;它也可以是VaR
:
>maxDrawdown(trade_return)
0.1173028
>StdDev(trade_return)
StdDev0.00379632
>StdDev.annualized(trade_return)
Annualized Standard Deviation 0.06026471
>VaR(trade_return, p = 0.95)
>SharpeRatio(as.ts(trade_return), Rf = 0, p = 0.95, FUN = "StdDev")
StdDev Sharpe (Rf=0%, p=95%): 0.01621421
>SharpeRatio.annualized(trade_return, Rf = 0)
Annualized Sharpe Ratio (Rf=0%) 0.2289401
现在,如果我们发现样本数据的表现很好,那么我们可以将此策略应用于出样本数据,计算所有出样本数据的矩阵,并检查策略表现的一致性。接下来的两行代码用于计算出样本数据的移动平均线、趋同发散和布林带:
>macd<- MACD(out_dji, nFast = 7, nSlow = 12, nSig = 15,maType="SMA", percent = FALSE)
> bb <- BBands(out_dji, n = 20, maType="SMA", sd = 2)
接下来,我使用这些出样本指标并生成像我们为样本数据生成的信号:
>signal <- NULL
> signal <- ifelse(out_dji> bb[,'up'] &macd[,'macd'] >macd[,'signal'],1,ifelse(out_dji< bb[,'dn'] &macd[,'macd'] <macd[,'signal'],-1,0))
使用以下代码行计算出样本数据的交易收益及其相关指标。这些数据与样本数据类似:
>trade_return<- out_ret_dji*lag(signal)
>cumm_ret<- Return.cumulative(trade_return)
>annual_ret<- Return.annualized(trade_return)
>charts.PerformanceSummary(trade_return)
>maxdd<- maxDrawdown(trade_return)
>sd<- StdDev(trade_return)
>sda<- StdDev.annualized(trade_return)
>VaR(trade_return, p = 0.95)
>SharpeRatio(as.ts(trade_return), Rf = 0, p = 0.95, FUN = "StdDev")
>SharpeRatio.annualized(trade_return, Rf = 0)
我为一个特定的时间序列实现了这个策略,即道琼斯指数(DJI);但是,你也可以在其他股票上测试这个策略,并了解它在整个股票宇宙中的表现。如果你发现这个策略在大多数股票上表现较好,这表明这个思路具有一致性,并且可能在实时交易中也能有效。这里非常重要的一点是,即使某个特定策略在少数股票上表现良好,我们也不应忽视检查投资组合的方差。让我给你举个例子。我计算了道琼斯指数时间序列回报的方差:
>var(ret_dji,na.rm=T)
Delt.1.arithmetic 8.093402e-05
在前面的代码中,我使用了na.rm=T
来去除时间序列中的Nan
。现在我将另一个符号S&P 500
导入到工作空间中。接下来的代码将S&P 500
导入工作空间:
>getSymbols("GSPC",src="img/yahoo")
现在,我提取了S&P 500
的收盘价,并在两日期之间进行了精细化处理。
接下来,我计算了S&P 500
的回报:
>snp<- GSPC[,"GSPC.Close"]
>snp<- snp[(index(snp) >= "2010-01-01" & index(snp) <= "2015-12-31"),]
>ret_snp<- Delt(snp)
我还计算了S&P 500
系列回报的方差:
>var(ret_snp,na.rm=T)
Delt.1.arithmetic 8.590805e-05
现在我将两个时间序列的回报率合并,并计算这两个回报率之和的方差:
>var(ret_dji + ret_snp,na.rm=T)
Delt.1.arithmetic 0.000218383
我们得到以下结果:
Variance(ret_dji + ret_snp) ≠ Variance(ret_dji) + Variance(ret_snp)
正如我们所见,0.000218383
≠ 8.093402e-05
+ 8.590805e-05
。
造成这种差异的原因非常重要。如果我们回顾概率论的基础,我们会发现以下内容:
Variance (X + Y) = Variance(X) + Variance(Y) + 2 Covariance(X,Y) .......... (5.1)
Variance (X + Y) = Variance(X) + Variance(Y) + 2 ρσXσY................(5.2)
这里,ρ
是X
和Y
之间的相关性;σX
是 X 的标准差;σY
是 Y 的标准差。
我们使用以下命令计算ret_dji
、ret_snp
的标准差以及ret_dji
和ret_snp
之间的相关性:
>sd(ret_dji,na.rm=T)
[1] 0.00926866
>sd(ret_snp,na.rm=T)
[1] 0.008996333
>cor(ret_dji[!is.na(ret_dji)],ret_snp[!is.na(ret_snp)])
Delt.1.arithmetic
Delt.1.arithmetic 0.3090576
ret_dji
和ret_snp
之间的相关性是0.3090576
。现在我们将这些值代入方程 5.2,你会看到两边是相等的。这意味着,如果两只股票是正相关的,它们会导致投资组合的方差比两只股票的单独方差之和更高。如果我们能挑选出两只不相关的股票,也就是相关性 = 0,那么投资组合的方差将是两只个股方差的线性和;或者,如果我们选中了两只负相关的股票,那么投资组合的方差将小于两只股票的方差之和。
所以我们必须查看投资组合中股票的相关性矩阵,以确定哪些股票有助于最小化风险。由于投资组合中只有两只股票,我创建了一个名为port_ret
的数据框,其中包含 NAs,行数与数据点的数量相同,且有两列:
>port_ret<- data.frame(matrix(NA,dim(ret_dji)[1],2))
接下来的两条命令将ret_dji
复制到数据框的第一列,将ret_snp
复制到第二列:
>port_ret[,1] <- ret_dji
>port_ret[,2] <- ret_snp
现在我们可以计算投资组合中股票的相关性矩阵。以下代码计算股票 1 与股票 2 的相关性:
>cor(port_ret)
X1 X2
X1 1 NA
X2 NA 1
上述相关性矩阵中有NA
值,这是因为port_ret
数据框中某处存在NA
,因此我们需要从数据框中删除NA
,is.na()
帮助我们去除这些NA
。以下代码过滤掉port_ret
中的NA
,然后计算相关性矩阵:
>port_ret<- port_ret[!is.na(port_ret[,1]),]
>cor(port_ret)
X1 X2
X1 1.0000000 0.3090576
X2 0.3090576 1.0000000
两只股票之间的相关性是顺序无关的,这就是为什么对角线元素相同的原因。很少能找到一对不相关或完全相关的股票。更负的相关性表明更好的多样化。当相关性增加时,多样化变得不那么重要,因为随着相关性的增加,投资组合的方差也会增加。这就是为什么相关性是选择股票和控制投资组合风险的最重要标准之一。
配对交易
你应该熟悉多样化的概念。为了实现多样化,我们需要选择负相关的股票;然而,在配对交易中,我们可以选择正相关的股票,并在这两只股票中进行相反的交易。对于被低估的股票,进行买入操作;对于被高估的股票,进行卖空操作。
X - Y 投资组合的方差定义如下:
Variance (X -Y) = Variance(X) + Variance(Y) - 2 ρ σXσY................(5.3)
配对交易是一种市场中性策略,因为两只股票的差异与整体市场不相关或接近零相关。我将展示如何使用距离法开始配对交易。我将使用相同的两只时间序列,分别是道琼斯指数和标普 500 指数,来进行配对交易,并解释如何在 R 中实现基于距离的方法。
基于距离的配对交易
不同的时间序列可能具有不同的缩放,因此你需要先对序列进行归一化。我通过使用初始投资 1 来进行归一化,然后计算该投资的累积回报:
>ret_dji[1] <- 1
>ret_snp[1] <- 1
以下命令计算了从 1 开始的投资的累积回报。通过这种方式,我们可以追踪第一条序列与第二条序列的相对表现,最后一个命令是计算两条序列之间的差异:
>norm_dji<- apply(ret_dji,2,cumprod)
>norm_snp<- apply(ret_snp,2,cumprod)
图 5.3:道琼斯指数和标普 500 指数的归一化价格
计算累积回报的公式定义如下:
(Norm_dji)t = (norm_dji)t-1 * (1 + rt)
现在,我使用plot()
命令绘制了归一化价格norm_dji
,并通过type="l"
帮助连接图中的所有点,生成折线图。如果不使用这个命令,图中将只显示点,ylim=c(0.5,2)
用于缩放纵轴。我还使用了lines()
在同一图表上绘制了另一条序列,这样我们就可以在同一图中查看两条序列。ylab
用于标记 y 轴:
>plot(norm_dji,type="l",ylim=c(0.5,2) ,ylab="Normalized_Price")
>lines(norm_snp,col="red")
>legend('topright',c("DJI","S&P 500") , lty=1, col=c('black','red'), bty='o', cex=1)
legend
命令帮助在图表的右上角放置一个框,标明图中绘制的是 DJI 和S&P500
序列。参数lty
用于设置图中的线型;lty=1
表示实线。下一个图表用于绘制归一化价格之间的差异。
当你查看这个图表时,你会发现这两个系列的距离在 500 号索引之前会趋于接近,之后则会持续发散。由于这个配对并不经常会收敛,因此你不应该考虑将其用于配对交易。你需要寻找另一个历史数据中经常收敛和发散的配对,这意味着这两个系列在历史数据上有某些基本面的相似性。
我选择了埃克森美孚(XOM)和雪佛龙公司(CVX)作为示例。图 5.4展示了标准化价格序列及其差异,同时显示了为交易生成的信号。
正如在图 5.4中所看到的,标准化价格并没有长时间地远离彼此。这个配对似乎是基于距离的对冲交易的好选择。
我们像计算norm_dji
和norm_snp
一样计算norm_xom
和norm_cvx
,并使用以下命令将它们绘制出来:
> class(norm_xom)
[1] "matrix"
> class(norm_cvx)
[1] "matrix"
你需要查看这两个变量的类别。正如上面所看到的,这两个都是矩阵,并且必须是xts
、zoo
对象。所以接下来你需要做的就是将这些矩阵对象转换为xts
、zoo
对象:
norm_xom<- xts(norm_xom,index(ret_xom))
norm_cvx<- xts(norm_cvx,index(ret_cvx))
xts()
函数完成这个工作,将这两个对象都转换为xts
对象:
>par(mfrow=c(3,1))
> plot(norm_xom,type="l",ylim=c(0.5,2) ,ylab="Normalized_Price")
> lines(norm_cvx,col="red")
> legend('topright',c("XOM","CVX") , lty=1, col=c('black','red'), bty='o', cex=1)
> diff = norm_xom - norm_cvx
> plot(diff,type="l",ylab="Normalized_Price_difference")
标准化价格差异的均值和标准差可以按如下方式计算:
> me <- mean(diff)
>std<- sd(diff)
图 5.4:标准化价格序列、差异和交易信号
差异序列的上界(ub
)和下界(lb
)可以通过从均值中加减n倍标准差来计算:
>ub<- me + n * std
>lb<- me - n*std
寻找n的最优参数值并不是一件简单的事。我们要么得使用反复试验的方法来找到最优的n值,要么使用基于网格的参数优化器来找到最优值。
作为一个示例,这里我使用了n = 1
仅仅是为了演示。当差异值低于下轨时,会产生一个买入信号(1
);当差异值高于上轨时,会产生一个卖出信号(-1
);否则信号保持(0
)。
当差异值,即价差,超过上轨时,我们推测它会回归到均值,因为历史上它大多数时间都在这个范围内。同样,当价差低于下轨时,我们也推测它会回归到均值:
> n <- 1
> signal <- ifelse(diff > ub,1,ifelse(diff < lb,-1,0))
这里我使用了完整的差异值时间序列来计算均值和标准差,正如之前我计算me
和std
一样。你也可以在滚动窗口上动态地计算均值和标准差。这种动态的均值和标准差会改变信号的生成,进出场的频率也会更高。
动态的均值和标准差可以通过rollapply()
函数来计算。在rollapply()
中你应该定义数据集、长度和函数:
>me_dynamic<- rollapply(diff,10,mean)
>std_dynamic<- rollapply(diff,10,sd)
plot()
函数绘制了如图 5.4所示的信号。非零信号值表示我们参与了市场,零值意味着我们没有参与市场:
>plot(signal, type="l")
标准化价格的差异也叫做价差。由于我们是通过价差生成信号,因此我们将交易的是价差而非单一股票。所以,我们必须清楚地理解什么是价差交易。当我说买入时,意味着我在买入价差,这就意味着在 XOM 上做多,在 CVX 上做空,或者当我们收到卖出信号时,在 XOM 上做空,在 CVX 上做多。以下两行计算了价差和交易回报:
>spread_return<- ret_xom - ret_cvx
>trade_return<- spread_return*lag(signal) - cost
变量spread_return
表示回报价差,trade_return
表示交易回报,cost 表示执行交易活动的费用;包括交易成本、经纪费用和滑点。
本书的目的仅是教你 R 语言编程,而不是产生盈利的交易理念。因此,我将成本设为 0,但你在回测你的策略并将资金投入实际账户时,必须考虑适当的成本。
现在我们应用绩效测量命令来提取绩效摘要:
> summary(trade_return)
Min. 1st Qu. Median Mean 3rd Qu. Max.
-0.0330000 0.0000000 0.0000000 0.0002135 0.0000000 0.0373400
所有关键绩效指标可以通过以下命令计算。这些命令已经在动量交易部分使用过:
>cumm_ret<- Return.cumulative(trade_return)
>annual_ret<- Return.annualized(trade_return)
>charts.PerformanceSummary(trade_return)
>maxdd<- maxDrawdown(trade_return)
>sd<- StdDev(trade_return)
>sda<- StdDev.annualized(trade_return)
>VaR(trade_return, p = 0.95)
>SharpeRatio(as.ts(trade_return), Rf = 0, p = 0.95, FUN = "StdDev")
>SharpeRatio.annualized(trade_return, Rf = 0)
图 5.5展示了此基于距离的配对交易策略的累积表现、每日回报和每日最大回撤:
图 5.5:策略的累积回报、回报和每日最大回撤
在这里,我展示了一种实现基于距离的配对交易模型的实际方法。你应该将数据分为样本内和样本外数据集。参数优化应使用样本内数据进行,随后验证这些参数在样本外数据中的表现。我已经在动量交易部分向你展示了这一方法。
基于相关性的配对交易
另一种传统的配对交易方式是基于相关性。你需要选择一对历史上高度相关的股票,并且该对股票的价差与市场基准的相关性最小。每当你看到相关性强度减弱时,就意味着出现了交易机会。这也建立在均值回归的前提下,交易者会在看到相关性与其均值发生显著偏离,至少偏离n倍标准差时,押注相关性会回归到其均值。
市场中性策略可以通过两种不同的方式实现:
-
贝塔中性
-
美元中性
贝塔中性意味着价差的贝塔接近零;这可以通过选择两只贝塔几乎相同的股票或工具来实现。然而,美元中性意味着你只对市场有微弱的暴露,因为在做多股票的投资通过做空交易获得的资金被抵消。
实际上,即使我们在市场上的暴露较少,也不意味着我们没有风险或风险很小。风险需要妥善管理才能实现盈利的交易。接下来,我将向你展示如何实现基于相关性的配对交易模型。
首先,你需要创建一个数据框,其中包含 XOM 和 CVX 的收益,因为我将 XOM 和 CVX 作为我的配对股票。
第一列是 XOM,第二列是 CVX 的收益:
>data <- data.frame(matrix(NA,dim(ret_xom)[1],2))
>data[,1] <- ret_xom
>data[,2] <- ret_cvx
> class(data)
[1] "data.frame"
可以使用 class()
函数检查该数据的类型,结果显示该数据属于 data.frame
类型。你需要将其转换为 xts
对象,可以使用以下代码来实现:
> data <- xts(data,index(ret_xom))
> class(data)
[1] "xts" "zoo"
现在你可以检查数据的类型,它是 xts
和 zoo
类型。接下来,我创建了一个名为 correlation 的函数,接受一个参数 x
,该函数计算 x
的第一列和第二列之间的相关性并返回相关性:
>correlation <- function(x)
{
result <- cor(x[,1],x[,2])
return (result)
}
我使用了 rollapply()
函数,它基于滚动窗口的方式进行计算,窗口长度由此函数中的滚动窗口长度参数定义。在这里,我提供了四个参数:第一个参数是用于计算的数据,第二个是窗口长度,第三个是用于计算的函数,第四个是指示函数是否应对每一列单独计算。
我使用的数据长度为 252
,函数是前面定义的 correlation,by.column=FALSE
表示该函数不会对每一列单独应用。
所以这个过程会不断进行,并使用最后 10 个数据点来计算相关性:
>corr<- rollapply(data,252,correlation ,by.column=FALSE)
该策略持续监控两个历史相关证券的表现。当这两只证券之间的相关性暂时减弱时,即一只股票上涨而另一只股票下跌,配对交易策略将是做空表现较好的股票并做多表现较差的股票,赌注是这两者之间的价差最终会收敛。
配对之间的背离可能是由于暂时的供需变化、大量买入/卖出某一证券的订单、某一公司发布的重要新闻反应等原因引起的。
图 5.6 显示了在滚动窗口长度为 252
时,XOM 和 CVX 收益之间的相关性。你可以看到,几乎每次相关性都超过了 0.6,表明这个配对的高相关性几乎每天都在持续:
图 5.6:XOM 和 CVX 收益之间的相关性
通常,相关性大于 0.8 被视为强相关,小于 0.5 被视为弱相关。你还需要计算 XOM 和 CVX 的对冲比率,可以通过将 XOM 价格除以 CVX 价格来计算:
>hedge_ratio<- xom / cvx
然后,你需要计算对冲比率的均值和标准差,以及上下边界。在基于距离的模型中,我提出了使用静态均值和标准差的技术;然而,在这一部分,我展示了基于滚动窗口的均值和标准差,用于计算边界。由于均值和标准差将是时间的函数,因此上下边界也将随时间变化。我使用rollapply()
函数计算每 14 个数据点的滚动均值和标准差:
>roll_me<- rollapply(hedge_ratio,14,mean)
>roll_std<- rollapply(hedge_ratio,14,sd)
> n <- 1
>roll_ub<- roll_me + n * roll_std
>roll_lb<- roll_me - n * roll_std
如果你查看前面的两个命令,你会看到参数 n,这是一个任意值,应该进行优化。一旦你有了边界,你就可以进行信号生成,下面的代码可以实现这一操作:
> signal <- NULL
> signal <- ifelse(hedge_ratio> roll_ub,-1,ifelse(hedge_ratio< roll_lb,1,0))
>lagsignal<- Lag(signal,1)
> signal <- ifelse(lagsignal == -1 &hedge_ratio> roll_me,
-1,ifelse(lagsignal == 1 &hedge_ratio< roll_me,1,0))
当对冲比率超过上界时,它会生成一个卖出信号(-1
);当对冲比率低于下界时,会生成一个买入信号(1
)。然后,计算滞后1
时刻的信号,并在对冲比率穿越滚动均值时生成退出信号。卖出信号表示卖空1
单位的 XOM 并做多对冲比率与 CVX 的乘积;买入信号则表示买入1
单位的 XOM 并做空对冲比率与 CVX 的乘积。你应该使用以下命令计算价差回报和交易回报:
>spread_return<- ret_xom - ret_cvx
>trade_return<- spread_return*lag(signal) - cost
完成这些操作后,你需要分析这些信号的质量,因此你需要计算所有的交易回报指标,这些可以通过前面章节提到的命令来计算,特别是动量交易和基于距离的配对交易部分。你还需要使用样本内数据优化参数,并使用这些优化后的参数来测试样本外数据,真正评估策略在样本外数据上的表现。
基于协整的配对交易
基于协整的配对交易是配对交易领域的最新武器,近年来其使用速度非常快。
协整考虑了一系列价格与另一系列价格的回归。由于这些系列是非平稳的,如果这些系列没有协整关系,那么回归结果将是虚假的。当我们需要回归非平稳系列时,协整变得至关重要。你首先需要检查你所使用的时间序列是否是非平稳的。你需要将tseries
包加载到工作空间中,且本部分使用的数据是 2014 年 1 月 1 日到 2014 年 12 月 31 日的数据:
> library(tseries)
>adf.test(xom)
增广的Dickey-Fuller
检验:
data: xom
Dickey-Fuller = -1.4326, Lag order = 11, p-value = 0.8185
alternative hypothesis: stationary
你可以看到Dicky-Fuller
统计量为-1.4326
,高于-3.43
。这意味着该序列是非平稳的,你也可以检查该序列的第一差分:
> diff <- xom - Lag(xom,1)
>adf.test(diff[!is.na(diff)])
Augmented Dickey-Fuller Test
data: diff[!is.na(diff)]
Dickey-Fuller = -11.791, Lag order = 11, p-value = 0.01
alternative hypothesis: stationary
由于diff
包含NA
,你应该只考虑非 NA 值,并使用adf.test()
来检验单位根。使用时间序列的第一差分得到的Dickey-Fuller
统计量为-11.97
,小于-3.43
,这表明第一差分是平稳的,且表明 XOM 是 1 阶积分的,即O(1)
。
现在我要使用lm()
来拟合 XOM 和 CVX 的模型。lm()
对应于线性模型,它回归 XOM 相对于 CVX 的原始价格,lm()
中的0
表示没有截距的回归:
> model <- lm(xom ~ cvx + 0)
> model
Call:
lm(formula = xom ~ cvx + 0)
Coefficients:
cvx
0.8008
可以使用summary()
命令查看模型的摘要:
> summary(model)
Call:
lm(formula = xom ~ cvx + 0)
Residuals:
Min 1Q Median 3Q Max
-12.7667 -2.2833 0.4533 2.9224 13.9694
Coefficients:
Estimate Std. Error t value Pr(>|t|)
cvx 0.800802 0.001123 713.4 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 4.587 on 1509 degrees of freedom
Multiple R-squared: 0.997, Adjusted R-squared: 0.997
F-statistic: 5.09e+05 on 1 and 1509 DF, p-value: < 2.2e-16>
下一步是提取来自名为model
的变量的残差,并对其进行单位根测试,您可以使用以下命令来完成此操作。你可以看到输出中Dickey-Fuller
统计量为-2.6088
,大于-3.43
,这意味着存在单位根。在概率论中,单位根有一个重要特性需要验证。单位根的存在会导致推断问题,因为具有单位根的时间序列会膨胀,不会收敛,或者持续发散。非平稳的时间序列具有单位根,它们不会收敛。单位根的存在意味着 XOM 和 CVX 并未协整:
>adf.test(as.ts(model$residuals))
Augmented Dickey-Fuller Test
data: as.ts(model$residuals)
Dickey-Fuller = -2.6088, Lag order = 11, p-value = 0.3206
alternative hypothesis: stationary
移动公司(XOM)和对冲比率乘以 BP 公司(BP),以及它们的残差。如果你查看价格序列,你会发现两者之间有很强的相关性。你必须找到另一个协整的配对,所以让我们尝试找另一个配对。让我向你展示埃克森美孚(XOM)和 BP 公司(BP)之间的协整关系。
使用GetSymbols()
提取 XOM 和 BP 的收盘价,并使用这些数据回归以建立它们之间的关系:
> model <- lm(xom ~ bp + 0)
>adf.test(as.ts(model$residuals))
Augmented Dickey-Fuller Test
data: as.ts(model$residuals)
Dickey-Fuller = -3.9007, Lag order = 11, p-value = 0.01395
alternative hypothesis: stationary
这里,Dickey-Fuller
统计量为-3.9007
,小于 95%置信水平下的临界值(-3.43
),所以这没有单位根,这对是平稳的:
> par(mfrow=c(2,1))
> plot(dji,type="l")
> lines(snp*model$coefficients,col="red")
> plot(model$residuals,type="l")
图 5.7 显示了 XOM 和对冲比率乘以 BP 价格序列及其价差。plot()
和lines()
用于绘制该图。现在,由于残差是均值回归的,因此下一步是使用以下命令生成界限和信号:
>roll_me<- rollapply(model$residuals,14,mean)
>roll_std<- rollapply(model$residuals,14,sd)
> n <- 1
>roll_ub<- roll_me + n * roll_std
>roll_lb<- roll_me - n * roll_std
前面的两个命令有一个参数 n,它是任意的,应该进行优化:
> signal <- NULL
> signal <- ifelse(model$residuals> roll_ub,-1,ifelse(model$residuals< roll_lb,1,0))
>lagsignal<- Lag(signal,1)
>signal <- ifelse(lagsignal == -1 &model$residuals> roll_me,-1,ifelse(lagsignal == 1 &model$residuals< roll_me,1,0))
图 5.7 显示了埃克森美孚(XOM)和对冲比率乘以 BP 公司(BP)的价格序列以及它们的残差。如果你查看价格序列,你可以看到这两个序列之间的紧密关系,并且这两个序列不会长时间偏离太远。如果它们有偏离的话,很快它们就会恢复接近:
图 5.7:XOM 和对冲比率乘以 BP 的序列以及它们的价差序列
由于你已经生成了信号,接下来应该计算策略的表现,方法可以参考我在动量交易或基于距离的配对交易部分中提到的命令。
资本资产定价模型
资本资产定价模型(CAPM)帮助评估证券或投资组合对基准的风险贡献,风险通过贝塔值来衡量 ()。通过 CAPM 模型,我们可以估计单个证券或投资组合的预期超额回报,这与其贝塔值成正比:
这里:
-
E(Ri):证券的预期回报
-
E(Rm): 市场预期收益率
-
Ri: 证券的收益率
-
Rf: 无风险收益率
-
Rm: 基准或市场收益率
-
: 证券的贝塔值
CVX 与 DJI 通过线性模型回归,如公式 5.4 所示。
这里我在以下命令中使用了零作为无风险收益率:
>rf<- rep(0,length(dji))
>model <- lm((ret_cvx -rf) ~ (ret_dji -rf) )
> model
Call:
lm(formula = (ret_cvx - rf) ~ (ret_dji - rf))
Coefficients:
(Intercept) ret_dji
-0.0002013 1.1034521
你可以看到上面的结果中的截距项是 alpha(-0.0002013
),而 ret_dji
的系数是贝塔(1.1034521
)。不过,你也可以使用 PerformanceAnalytics
包,通过 CAPM.alpha()
和 CAPM.beta()
来计算 CAPM 的 alpha 和 beta。
以下命令展示了如何使用此方法,结果与之前相同:
>CAPM.beta(ret_cvx,ret_dji)
[1] 1.103452
>CAPM.alpha(ret_cvx,ret_dji)
[1] -0.0002013222
从 CAPM.beta()
获得的贝塔值与回归中的系数相同,而 CAPM.alpha()
与回归中的截距相同。你还可以看到收益率及其拟合线的散点图:
>plot(as.ts(ret_cvx),as.ts(ret_dji),xlab="CVX_ Return",ylab="DJI_Return")
>abline(model,col="red")
图 5.8 显示拟合线具有正斜率,这意味着收益率之间存在正相关。我们可以使用以下命令验证这一说法:
>cor(ret_cvx,ret_dji)
Delt.1.arithmetic
Delt.1.arithmetic 0.7881967
图 5.8: DJI 和 CVS 收益率的散点图及其拟合线
多因子模型
多因子模型可以用来分解收益并计算风险。因子是通过定价、基本面和分析师预期数据构建的。在这一部分,我将使用系统化投资工具箱。
gzcon()
函数创建一个连接并读取压缩格式的数据。创建连接后,我们还需要关闭连接。
以下命令解释了这个过程:
> con = gzcon(url('http://www.systematicportfolio.com/sit.gz', 'rb'))
> source(con)
> close(con)
以下函数用于从 money.cnn.com
获取道琼斯成分股数据,join()
来自系统化投资工具箱:
>dow.jones.components<- function(){
url = 'http://money.cnn.com/data/dow30/'
txt = join(readLines(url))
temp = gsub(pattern = '">', replacement = '<td>', txt, perl = TRUE)
temp = gsub(pattern = '</a>', replacement = '</td>', temp, perl = TRUE)
temp = extract.table.from.webpage(temp, 'Volume', has.header = T)
trim(temp[,'Company']) }
下一行代码调用了前面的函数,用于提取道琼斯成分股列表:
>tickers = dow.jones.components()
以下命令解释了如何提取所有公司在 tickers 列表中过去 80 个月的基本面数据。这些命令会花费几分钟时间提取数据,因此建议在提取数据后保存数据,之后应使用 load()
命令加载它:
>data.fund<- new.env()
> temp = paste(iif( nchar(tickers) <= 3, 'NYSE:', 'NASDAQ:'), tickers, sep='')
>for(i in 1:len(tickers)) data.fund[[tickers[i]]] = fund.data(temp[i], 80)
>save(data.fund, file='data.fund.Rdata')
# load(file='data.fund.Rdata')
下一组命令与之前的代码相同,但用于提取价格数据:
# get pricing data
>data <- new.env()
>getSymbols(tickers, src = 'yahoo', from = '1970-01-01', env = data, auto.assign = T)
>for(i in ls(data)) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T)
>save(data, file='data.Rdata')
#load(file='data.Rdata')
后续的函数会创建各种日期格式的日期变量:
>date.fund.data<- function(data){
quarter.end.date = as.Date(paste(data['quarter end date',], '/1', sep=''), '%Y/%m/%d')
quarterly.indicator = data['quarterly indicator',]
date.preliminary.data.loaded = as.Date(data['date preliminary data loaded',], '%Y-%m-%d') + 1
months = seq(quarter.end.date[1], tail(quarter.end.date,1)+365, by='1 month')
index = match(quarter.end.date, months)
quarter.end.date = months[ iif(quarterly.indicator == '4', index+3, index+2) + 1 ] - 1
fund.date = date.preliminary.data.loaded
fund.date[is.na(fund.date)] = quarter.end.date[is.na(fund.date)]
return(fund.date) }
现在你已经提取了价格和基本面数据,你应该使用这些数据来构建各种基本面因子,如每股收益、流通股数、市场市值、市值与账面价值比等。这个循环会为每个股票代码逐个计算基本面因子并创建一个列表:
> library(quantmod)
>for(i in tickers) {
fund = data.fund[[i]]
fund.date = date.fund.data(fund)
# Earnings per Share
EPS = get.fund.data('Diluted EPS from Total Operations', fund, fund.date, is.12m.rolling=T)
# Common Shares Outstanding
CSHO = get.fund.data('total common shares out', fund, fund.date)
# Common Equity
CEQ = get.fund.data('total equity', fund, fund.date)
# merge
data[[i]] = merge(data[[i]], EPS, CSHO, CEQ) }
接下来,我筛选了前面的数据,时间范围从 1995
年到 2011
年:
>bt.prep(data, align='keep.all', dates='1995::2011')
所有股票的价格可以通过以下命令提取,并且 NAN 可以用之前的值替换:
> prices = data$prices
> prices = bt.apply.matrix(prices, function(x) ifna.prev(x))
现在你需要使用基本因素和价格构建基本比率。我创建了三个比率;不过,你可以创建任何你想要考虑的比率。我创建了市值、市盈率比率和账面价值比率:
# Financial Ratios
>factors$TV = list()
# Market Value - capitalization
> CSHO = bt.apply(data, function(x) ifna.prev(x[, 'CSHO']))
> MKVAL = prices * CSHO
# Earnings / Price
> EPS = bt.apply(data, function(x) ifna.prev(x[, 'EPS']))
>factors$TV$EP = EPS / prices
# Book Value / Price
> CEQ = bt.apply(data, function(x) ifna.prev(x[, 'CEQ']))
>factors$TV$BP = CEQ / MKVAL
由于这些比率的量纲可能不同,在继续之前,我们不应该忘记对其进行标准化。我计算了 Z 分数来标准化这些数据:
# normalize (convert to z scores) cross sectional all Traditional Value factors
>for(i in names(factors$TV)) {
factors$TV[[i]] = (factors$TV[[i]] -
cap.weighted.mean(factors$TV[[i]], MKVAL)) /
apply(factors$TV[[i]], 1, sd, na.rm=T)
}
This is how we bind different data in multidimensional case
# compute the overall Traditional Value factor
>load.packages('abind')
> temp = abind(factors$TV, along = 3)
计算所有标准化因子的平均值:
>factors$TV$AVG = factors$TV[[1]]
>factors$TV$AVG[] = apply(temp, c(1,2), mean, na.rm=T)
到目前为止,我们有的是日数据,并且已经基于日数据创建了财务比率。你可以将其转换为任何你需要的频率。我将其转换为月频,并提取了每个月最后一天的数据:
# find month ends
>month.ends = endpoints(prices, 'months')
> prices = prices[month.ends,]
> n = ncol(prices)
>nperiods = nrow(prices)
这就是你应该如何计算月度回报及其 lag
:
> ret = prices / mlag(prices) - 1
>next.month.ret = mlag(ret, -1)
每个月最后一天的市值可以通过以下方式计算:
> MKVAL = MKVAL[month.ends,]
提取每个月最后一天的所有比率:
>for(j in 1:len(factors)) {
for(i in 1:len(factors[[j]])) {
factors[[j]][[i]] = factors[[j]][[i]][month.ends,]
}}
接下来你应该计算分位数,可以通过以下命令计算。我创建了五个分位数,并且使用盈利价格因子计算了每个分位数的下个月平均回报。分位数是通过按 EP 因子对股票进行月份排序来创建的:
> out = compute.quantiles(factors$TV$AVG, next.month.ret, plot=F)
> models = list()
>for(i in 1:5) {
data$weight[] = NA
data$weight[month.ends,] = iif(out$quantiles == i, out$weights, 0)
capital = 100000
data$weight[] = (capital / prices) * (data$weight)
models[[paste('Q',i,sep='')]] = bt.run(data, type='share', capital=capital) }
最高和最低极值非常极端,应该用来创建一个差距(Q5 - Q1)。这个差距的动态有助于设计和开发投资策略,即动量或均值回归:
# spread
>data$weight[] = NA
>data$weight[month.ends,] = iif(out$quantiles == 5, out$weights,
iif(out$quantiles == 1, -out$weights, 0))
> capital = 100000
>data$weight[] = (capital / prices) * (data$weight)
> models$Q5_Q1 = bt.run(data, type='share', capital=capital)
现在你应该运行横截面回归来估计 alpha 和投资组合负荷,这些可以通过以下命令计算:
>factors.avg = list()
>for(j in names(factors)) factors.avg[[j]] = factors[[j]]$AVG
>factors.avg = add.avg.factor(factors.avg)
>nperiods = nrow(next.month.ret)
> n =ncol(next.month.ret)
# create matrix for each factor
>factors.matrix = abind(factors.avg, along = 3)
>all.data = factors.matrix
> # betas
> beta = all.data[,1,] * NA
# append next.month.ret to all.data
>all.data = abind(next.month.ret, all.data, along = 3)
>dimnames(all.data)[[3]][1] = 'Ret'
# estimate betas (factor returns)
>for(t in 30:(nperiods-1)) {
temp = all.data[t:t,,]
x = temp[,-1]
y = temp[,1]
beta[(t+1),] = lm(y~x-1)$coefficients
}
# create Alpha return forecasts
> alpha = next.month.ret * NA
>for(t in 40:(nperiods-1)) {
# average betas over the last 6 months
coef = colMeans(beta[(t-5):t,],na.rm=T)
alpha[t,] = rowSums(all.data[t,,-1] * t(repmat(coef, 1,n)), na.rm=T) }
我们还可以使用这些 alpha 和 beta 来估计未来的投资组合回报。
投资组合构建
投资者的目标是减少风险并最大化投资回报,创建一个投资组合可以实现这一目标,前提是我们在构建时考虑了投资者的风险-回报特征。我将指导你如何创建一个有效前沿,帮助你根据预期回报衡量风险。为此,我将开始提取四个证券的数据。第一行代码创建一个新的环境来存储数据;接下来的几行是关于符号列表、数据起始日期,以及使用 getSymbols()
提取数据:
>stockData<- new.env()
> symbols <- c("MSFT","FB","GOOG","AAPL")
>start_date<- as.Date("2014-01-01")
>getSymbols(symbols, src="img/yahoo", env=stockData, from=start_date)
> x <- list()
接下来的 for 循环将个别股票数据存储在一个列表中,并计算当天的收益,以及一个包含所有投资组合股票收盘价的数据框:
>for (i in 1:length(symbols)) {
x[[i]] <- get(symbols[i], pos=stockData) # get data from stockData environment
x[[i]]$gl<-((Cl(x[[i]])-Op(x[[i]]))/Op(x[[i]]))*100 #Daily gain loss percentage
if(i==1)
data <- Cl(x[[i]])
else
data <- cbind(data,Cl(x[[i]])) }
可以使用以下命令计算每只股票的收益、平均收益以及协方差矩阵:
>data_ret<- apply(data,2,Delt)
>napos<- which(apply(data_ret,2,is.na))# Remove Na's
>avg_ret<- apply(data_ret[-napos,],2,mean)
>covariance_mat<- cov(data_ret,use='na')
我将使用以下权重来分配给投资组合:
> weights <- c(0.2,0.3,0.35,0.15)
现在你需要浏览链接 faculty.washington.edu/ezivot/econ424/portfolio.r
,并将这个 R 代码保存到文件 portfolio.R
中。你应该使用以下命令来访问在 portfolio.R
中开发的函数:
> source("portfolio.R")
为了计算投资组合的预期收益和标准差,我们需要收益、权重和协方差矩阵。现在我们已经拥有所有数据,并可以使用以下命令来生成投资组合的预期收益和风险:
>weightedport = getPortfolio(avg_ret,covariance_mat,weights)
>weightedport
Call:
getPortfolio(er = avg_ret, cov.mat = covariance_mat, weights = weights)
Portfolio expected return: 0.0004109398
Portfolio standard deviation: 0.01525882
Portfolio weights:
MSFT.CloseFB.CloseGOOG.CloseAAPL.Close
0.20 0.30 0.35 0.15
全球最小方差投资组合可以使用以下命令获得。你可以看到这里的投资组合权重与前一个命令中的权重不同,这一组权重有助于生成标准差较低的投资组合:
>minvar_port<- globalMin.portfolio(avg_ret, covariance_mat)
>minvar_port
Call:
globalMin.portfolio(er = avg_ret, cov.mat = covariance_mat)
Portfolio expected return: 0.0007211767
Portfolio standard deviation: 0.01349528
Portfolio weights:
MSFT.CloseFB.CloseGOOG.CloseAAPL.Close
0.5889 0.2415 0.1001 0.0696
现在假设你想生成一个预期收益为0.0002
的投资组合。以下命令将帮助生成该预期收益为0.0002
的投资组合的权重和标准差:
>rf<- 0.0002
>effcient_port<- efficient.portfolio(avg_ret, covariance_mat,rf)
>effcient_port
Call:
efficient.portfolio(er = avg_ret, cov.mat = covariance_mat, target.return = 2e-04)
Portfolio expected return: 2e-04
Portfolio standard deviation: 0.0169678
Portfolio weights:
MSFT.CloseFB.CloseGOOG.CloseAAPL.Close
0.4626 -0.1292 0.4184 0.2482
切线投资组合是一个风险资产组合,具有最高的夏普比率斜率。为了计算这一点,我使用了 tangency.portfolio()
函数:
>tangency_port<- tangency.portfolio(avg_ret,covariance_mat , rf)
>tangency_port
Call:
tangency.portfolio(er = avg_ret, cov.mat = covariance_mat, risk.free = 2e-04)
Portfolio expected return: 0.4942792
Portfolio standard deviation: 0.02226374
Portfolio weights:
MSFT.CloseFB.CloseGOOG.CloseAAPL.Close
0.8062 0.8797 -0.4480 -0.2378
我们已经计算了全球最小方差投资组合,另一个具有最大预期收益的投资组合可以视为第二个投资组合。分别将这些投资组合称为P1和P2。现在,对于任何 ,可以按以下方式构建另一个投资组合:
可以使用以下命令计算有效前沿。这将生成50
个投资组合,使用 ,范围从
-2
到 2
:
>efficient_frontier<- efficient.frontier(avg_ret, covariance_mat, alpha.min=-2,alpha.max=2, nport=50)
接下来,在图 5.9中,我绘制了有效前沿、最小方差和切线投资组合的红蓝点,以及切线与前沿的切点:
>plot(efficient_frontier, plot.assets=T)
>points(minvar_port$sd, minvar_port$er, col="blue")
>points(tangency_port$sd,tangency_port$er, col="red")
>tangenet_sharpe_ratio = (tangency_port$er - rf)/tangency_port$sd
>abline(a=rf, b=tangenet_sharpe_ratio)
图 5.9:投资组合的有效前沿与切线
问题
-
如何从雅虎财经导入股票数据到 R 工作空间?
-
如何使用移动平均交叉生成动量策略?
-
哪个包帮助计算策略的绩效指标?
-
如何计算一个由五只股票组成的投资组合的协方差矩阵?
-
从雅虎提取 MSFT 数据,并测试收盘价系列是否是非平稳的。
-
使用距离法生成交易信号,当价差回归均值时退出。
-
如何测试一对股票的协整性,并编写代码进行测试?
-
如何计算对冲比率,它如何帮助交易?
-
如何计算投资组合的贝塔值?请用一个例子展示。
-
如何使用基本面因子创建分位数和分位数差距?
-
编写代码计算投资组合的预期
收益
和标准差。 -
如何计算有效前沿并使用 R 命令绘制它?
总结
在本章中,我介绍了使用 R 进行交易的不同概念。 我从趋势跟踪策略开始,深入解释了交易信号是如何生成的,以及如何捕捉与其表现相关的各种参数。 动量策略之后是使用三种不同方法进行配对交易。 第一种方法是基于距离的配对交易,第二种是基于相关性的,第三种也是最后一种方法是基于协整的配对交易。 有时,通过投资组合进行交易以控制风险和回报比是很重要的,为此我涵盖了资本资产定价、多因子模型和投资组合构建。 我使用 Systematic Investor Toolbox 来实现投资组合理念。
在下一章中,我将解释使用机器学习算法的交易策略,这些策略越来越受欢迎。 机器学习算法会自动从历史市场行为中学习,并尝试模仿这种行为。
第六章:使用机器学习进行交易
在资本市场,基于机器学习的算法交易如今非常流行,许多公司正在投入大量精力研发机器学习算法,这些算法要么是专有的,要么是为客户提供的。机器学习算法是以一种能够不断学习并自动改变其行为的方式编写的。这有助于在市场中出现新模式时及时识别。有时候,资本市场中的模式是如此复杂,以至于人类无法捕捉到。即使人类设法找到了某个模式,也无法高效地识别它。模式的复杂性迫使人们寻求其他机制,以准确高效地识别这些复杂的模式。
在上一章中,你了解了动量、基于配对交易的算法交易和投资组合构建。在本章中,我将一步步解释一些在算法交易中使用的有监督和无监督的机器学习算法:
-
逻辑回归神经网络
-
神经网络
-
深度神经网络
-
K 均值算法
-
K 最近邻算法
-
支持向量机
-
决策树
-
随机森林
本章中使用的几个包包括quantmod
、nnet
、genalg
、caret
、PerformanceAnalytics
、deepnet
、h2o
、clue
、e1071
、randomForest
和party
。
逻辑回归神经网络
市场方向对于投资者或交易者来说非常重要。预测市场方向是一项非常具有挑战性的任务,因为市场数据充满了噪声。市场要么上涨,要么下跌,市场运动的性质是二元的。逻辑回归模型帮助我们通过二元行为拟合模型,并预测市场方向。逻辑回归是一种概率模型,它为每个事件分配概率。我假设你已经熟悉从 Yahoo 提取数据,因为你在前面的章节中已经学习过这一部分。这里我仍将使用quantmod
包。接下来的三条命令用于将该包加载到工作空间中,从yahoo
库导入数据,并仅提取数据中的收盘价:
>library("quantmod")
>getSymbols("^DJI",src="img/yahoo")
>dji<- DJI[,"DJI.Close"]
逻辑回归的输入数据是通过不同的指标构建的,如移动平均、标准差、相对强弱指数(RSI)、平滑异同移动平均线(MACD)、布林带等,这些指标在市场方向上具有一定的预测能力,即Up
(上涨)或Down
(下跌)。这些指标可以通过以下命令构建:
>avg10<- rollapply(dji,10,mean)
>avg20<- rollapply(dji,20,mean)
>std10<- rollapply(dji,10,sd)
>std20<- rollapply(dji,20,sd)
>rsi5<- RSI(dji,5,"SMA")
>rsi14<- RSI(dji,14,"SMA")
>macd12269<- MACD(dji,12,26,9,"SMA")
>macd7205<- MACD(dji,7,20,5,"SMA")
>bbands<- BBands(dji,20,"SMA",2)
以下命令用于创建方向变量,方向可以是Up
(上涨,1
)或Down
(下跌,0
)。当当前价格大于 20 天前的价格时,创建Up
方向;当当前价格小于 20 天前的价格时,创建Down
方向:
>direction<- NULL
>direction[dji> Lag(dji,20)] <- 1
>direction[dji< Lag(dji,20)] <- 0
现在我们需要绑定所有包含价格和指标的列,以下命令展示了如何操作:
>dji<-cbind(dji,avg10,avg20,std10,std20,rsi5,rsi14,macd12269,macd7205,bbands,direction)
可以使用dim()
计算dji
对象的维度。我对dji
使用了dim()
并将输出保存到dm()
中。dm()
存储了两个值:第一个值是行数,第二个值是列数。列名可以通过colnames()
提取。第三个命令用于提取最后一列的名称。接下来,我将列名替换为特定的名称Direction
:
>dm<- dim(dji)
>dm
[1] 2493 16
>colnames(dji)[dm[2]]
[1] "..11"
>colnames(dji)[dm[2]] <- "Direction"
>colnames(dji)[dm[2]]
[1] "Direction"
我们已经将道琼斯指数(DJI)数据提取到 R 工作空间中。现在,为了实现逻辑回归,我们应该将数据分为两部分。第一部分是样本内数据,第二部分是样本外数据。
样本内数据用于模型构建过程,样本外数据用于评估目的。这个过程还有助于控制模型中的方差和偏差。接下来的四行是样本内开始、样本内结束、样本外开始和样本外结束日期:
>issd<- "2010-01-01"
>ised<- "2014-12-31"
>ossd<- "2015-01-01"
>osed<- "2015-12-31"
以下两个命令用于获取日期的行号,即变量isrow
提取样本内日期范围的行号,osrow
提取样本外日期范围的行号:
>isrow<- which(index(dji) >= issd& index(dji) <= ised)
>osrow<- which(index(dji) >= ossd& index(dji) <= osed)
变量isdji
和osdji
分别是样本内和样本外数据集:
>isdji<- dji[isrow,]
>osdji<- dji[osrow,]
如果你查看样本内数据,即isdji
,你会发现每一列的尺度不同:有些列的尺度是 100,有些列的尺度是 10,000,还有些列的尺度是 1。尺度的不同可能会导致结果出现问题,因为较高尺度的变量会被赋予更高的权重。因此,在继续之前,你应该考虑对数据集进行标准化。我将使用以下公式:
使用apply()
可以查看每列的均值和标准差:
>isme<- apply(isdji,2,mean)
>isstd<- apply(isdji,2,sd)
使用以下命令生成一个维度与样本内数据相同的单位矩阵,该矩阵将用于标准化:
>isidn<- matrix(1,dim(isdji)[1],dim(isdji)[2])
使用公式 6.1 来标准化数据:
>norm_isdji<- (isdji - t(isme*t(isidn))) / t(isstd*t(isidn))
前面的行也标准化了direction
列,即最后一列。我们不希望方向被标准化,所以我再次将最后一列替换为变量方向,适用于样本内数据范围:
>dm<- dim(isdji)
>norm_isdji[,dm[2]] <- direction[isrow]
现在我们已经创建了构建模型所需的所有数据。你应该建立一个逻辑回归模型,它将帮助你根据样本内数据预测市场方向。首先,在这一步中,我创建了一个公式,其中方向作为因变量,所有其他列作为自变量。然后,我使用了广义线性模型,即glm()
,来拟合一个包含公式、族和数据集的模型:
>formula<- paste("Direction ~ .",sep="")
>model<- glm(formula,family="binomial",norm_isdji)
可以使用以下命令查看模型的摘要:
>summary(model)
接下来使用predict()
在相同的数据集上拟合值,以估计最佳拟合值:
>pred<- predict(model,norm_isdji)
一旦你拟合了这些值,你应该尝试使用以下命令将其转换为概率。这将把输出转换为概率形式,且输出值将在[0,1]的范围内:
>prob<- 1 / (1+exp(-(pred)))
图 6.1是使用以下命令绘制的。代码的第一行显示我们将图形分为两行一列,第一张图是模型预测结果,第二张图是概率:
>par(mfrow=c(2,1))
>plot(pred,type="l")
>plot(prob,type="l")
可以使用head()
查看变量的前几个值:
>head(prob)
2010-01-042010-01-05 2010-01-06 2010-01-07
0.8019197 0.4610468 0.7397603 0.9821293
下图显示了上述定义的变量pred
,它是一个实数,并且它在0
和1
之间的转换,表示概率,即prob
,使用之前的变换:
图 6.1:DJI 的预测与概率分布
由于概率值在(0,1)范围内,所以我们的向量prob
也是如此。现在,为了将其分类为两个类别之一,我考虑了当prob
大于0.5
时为Up
方向(1
),而当prob
小于0.5
时为Down
方向(0
)。这个赋值可以通过以下命令完成。prob > 0.5
生成一个布尔值,表示prob
大于0.5
的点,pred_direction[prob > 0.5]
将1
赋给所有这些点。类似地,接下来的语句展示了当概率小于或等于0.5
时赋值0
:
>pred_direction<- NULL
>pred_direction[prob> 0.5] <- 1
>pred_direction[prob<= 0.5] <- 0
一旦我们确定了预测方向,我们应该检查模型的准确性:我们的模型将Up
方向预测为Up
方向和Down
方向预测为Down
的准确程度。可能会出现一些情况,模型预测的方向与实际方向相反,例如预测为Down
时实际上是Up
,反之亦然。我们可以使用caret
包来计算confusionMatrix()
,它会输出一个矩阵。所有对角线上的元素是正确预测的,非对角线元素则是错误的或预测错的。我们应该尽量减少混淆矩阵中的非对角线元素:
>install.packages('caret')
>library(caret)
>matrix<- confusionMatrix(pred_direction,norm_isdji$Direction)
>matrix
Confusion Matrix and Statistics
Reference
Prediction 0 1
0 362 35
1 42 819
Accuracy : 0.9388 95% CI : (0.9241, 0.9514)
No Information Rate : 0.6789 P-Value [Acc>NIR] : <2e-16
Kappa : 0.859 Mcnemar's Test P-Value : 0.4941 Sensitivity : 0.8960 Specificity : 0.9590
PosPredValue : 0.9118 NegPred Value : 0.9512
Prevalence : 0.3211 Detection Rate : 0.2878
Detection Prevalence : 0.3156 Balanced Accuracy : 0.9275
前面的表格显示我们得到了 94%的正确预测,因为 362+819 = 1181 个正确预测占总数 1258(所有四个值的和)。在样本内数据上预测超过 80%通常被认为是好的预测;然而,80%并不是固定的,具体数值需要根据数据集和行业来决定。现在你已经实现了逻辑回归模型,并且预测了 94%的正确率,需要测试其泛化能力。应该使用样本外数据来测试这个模型的准确性。第一步是使用公式(6.1)标准化样本外数据。这里的均值和标准差应与样本内归一化时使用的相同:
>osidn<- matrix(1,dim(osdji)[1],dim(osdji)[2])
>norm_osdji<- (osdji - t(isme*t(osidn))) / t(isstd*t(osidn))
>norm_osdji[,dm[2]] <- direction[osrow]
接下来我们在样本外数据上使用predict()
并利用这个值计算概率:
>ospred<- predict(model,norm_osdji)
>osprob<- 1 / (1+exp(-(ospred)))
一旦为样本外数据确定了概率,你应该使用以下命令将其划分到Up
或Down
类别中。这里的ConfusionMatrix()
将为样本外数据生成一个矩阵:
>ospred_direction<- NULL
>ospred_direction[osprob> 0.5] <- 1
>ospred_direction[osprob<= 0.5] <- 0
>osmatrix<- confusionMatrix(ospred_direction,norm_osdji$Direction)
>osmatrix
Confusion Matrix and Statistics
Reference
Prediction 0 1
0 115 26
1 12 99
Accuracy : 0.8492 95% CI : (0.7989, 0.891)
该模型在外样本数据上显示了 85%的准确率。准确率的质量超出了本书的讨论范围,因此我不会讨论外样本准确率是否良好,或者改进此性能的技术。一个现实的交易模型还需要考虑交易成本和市场滑点,这会显著降低胜率。接下来要做的是利用预测的方向设计交易策略。我将在下一节中解释如何使用预测信号实现自动化交易策略。
神经网络
在上一节中,我使用了两个类别实现了一个模型。实际上,可能有交易者不愿意在市场处于震荡区间时进行交易。也就是说,我们必须在现有的两个类别基础上再添加一个类别,Nowhere
。现在我们有三个类别:Up
、Down
和Nowhere
。我将使用人工神经网络来预测Up
、Down
或Nowhere
方向。当交易者预测某一时刻会有看涨(看跌)趋势时,他们会买入(卖出),而在市场处于Nowhere
时则不会投资。本节将实现一个具有前馈反向传播的人工神经网络。神经网络需要输入和输出数据。收盘价以及从收盘价衍生出的指标是输入层节点,三个类别(Up
、Down
和Nowhere
)是输出层节点。然而,输入层节点的数量没有限制。我将使用一个包含价格和在逻辑回归中使用的指标的数据集。不过,使用相同的数据集并非强制要求。如果你想使用不同的指标,完全可以这么做。你还可以增加或减少数据集中的指标数量;这部分留给读者自行构建自己选择的数据集。我将继续使用与逻辑回归中相同的数据集,唯一不同的是方向。在本节中,我们将Nowhere
作为方向中的第三个维度,因此我必须重新计算方向参数,以便训练神经网络:
>getSymbols("^DJI",src="img/yahoo")
>dji<- DJI[,"DJI.Close"]
> ret <- Delt(dji)
>avg10<- rollapply(dji,10,mean)
>avg20<- rollapply(dji,20,mean)
>std10<- rollapply(dji,10,sd)
>std20<- rollapply(dji,20,sd)
>rsi5<- RSI(dji,5,"SMA")
>rsi14<- RSI(dji,14,"SMA")
>macd12269<- MACD(dji,12,26,9,"SMA")
>macd7205<- MACD(dji,7,20,5,"SMA")
>bbands<- BBands(dji,20,"SMA",2)
当过去 20 天的收益率大于(小于)2%(-2%)时,我将生成Up
(Down
)方向;当过去 20 天的收益率在-2%和 2%之间时,我将生成Nowhere
方向。
第一行生成一个名为 direction 的数据框,该数据框包含 NA 值,行数与dji
的行数相同,并且只有一列。第二个命令是过去 20 天的收益率。参数值 20 是神圣不可侵犯的;不过,你可以选择任何你想要的值。第三、第四和第五个命令基本上是根据条件分配Up
、Down
和NoWhere
方向:
>direction<- data.frame(matrix(NA,dim(dji)[1],1))
>lagret<- (dji - Lag(dji,20)) / Lag(dji,20)
>direction[lagret> 0.02] <- "Up"
>direction[lagret< -0.02] <- "Down"
>direction[lagret< 0.02 &lagret> -0.02] <- "NoWhere"
收盘价和指标通过以下命令行合并成一个变量,命名为dji
:
>dji<- cbind(dji,avg10,avg20,std10,std20,rsi5,rsi14,macd12269,macd7205,bbands)
神经网络的数据被分为三部分,即训练数据集、验证数据集和测试数据集。训练数据应用于训练神经网络;然而,验证数据应用于验证估计的参数,测试数据集则用于测量预测的准确性。我使用了以下date
变量来定义日期范围,并根据该范围提取数据:
>train_sdate<- "2010-01-01"
>train_edate<- "2013-12-31"
>vali_sdate<- "2014-01-01"
>vali_edate<- "2014-12-31"
>test_sdate<- "2015-01-01"
>test_edate<- "2015-12-31"
可以使用以下命令构建三个数据集的日期范围,其中train_sdate
和train_edate
分别定义训练期的开始和结束日期。同样,验证期和测试期的日期也需要使用。
函数which()
用于生成行号,其中日期大于等于开始日期并且小于等于结束日期:
>trainrow<- which(index(dji) >= train_sdate& index(dji) <= train_edate)
>valirow<- which(index(dji) >= vali_sdate& index(dji) <= vali_edate)
>testrow<- which(index(dji) >= test_sdate& index(dji) <= test_edate)
现在,使用前面的行号,你应该提取训练、验证和测试期间的数据:
>traindji<- dji[trainrow,]
>validji<- dji[valirow,]
>testdji<- dji[testrow,]
以下命令用于计算训练数据按列的均值和标准差。函数apply()
将数据作为第一个参数,方向作为第二个参数,表示我们希望应用某个特定函数,函数作为第三个参数提供:
>trainme<- apply(traindji,2,mean)
>trainstd<- apply(traindji,2,sd)
为了规范化三个数据集,我们需要创建三个单位矩阵,其维度与训练、验证和测试数据的维度相等。
以下命令能够很好地完成此任务:
>trainidn<- (matrix(1,dim(traindji)[1],dim(traindji)[2]))
>valiidn<- (matrix(1,dim(validji)[1],dim(validji)[2]))
>testidn<- (matrix(1,dim(testdji)[1],dim(testdji)[2]))
训练、验证和测试数据可以使用以下命令进行规范化。t()
用于转置数据框、矩阵或向量:
>norm_traindji<- (traindji - t(trainme*t(trainidn))) / t(trainstd*t(trainidn))
>norm_validji<- (validji - t(trainme*t(valiidn))) / t(trainstd*t(valiidn))
>norm_testdji<- (testdji - t(trainme*t(testidn))) / t(trainstd*t(testidn))
先前定义的规范化数据包含价格和指标值。我们还应使用以下命令定义训练、验证和测试期间的方向:
>traindir<- direction[trainrow,1]
>validir<- direction[valirow,1]
>testdir<- direction[testrow,1]
现在,我假设你的机器上已经安装了nnet()
包。如果没有,你应该使用install.package()
进行安装。安装完成后,应该使用以下命令将其加载到工作空间中:
>library(nnet)
以下代码行用于设置神经网络的种子,否则每次神经网络将以随机权重开始,输出结果会有所不同。我们应使用set.seed()
来确保每次运行此命令时输出相同。下一行解释了神经网络的拟合,其中第一个参数是所有规范化列的集合,第二个参数是训练期日期的目标向量,其中包含方向,第三个参数是隐藏层中的神经元数量,第四个参数是trace
,它在执行结束时打印输出。我将隐藏层神经元数设置为4
;但是,你应该优化此参数。我不希望在执行结束时打印输出,除非我明确要求,因此我使用trade=F
:
>set.seed(1)
>model<- nnet(norm_traindji,class.ind(traindir),size=4,trace=F)
在第二个参数中,您必须已经注意到使用了class.ind()
函数。该函数将三个类别转换为三列,每列对应一个类别,每列中相同类别的位置为1
,其他位置为0
。
您可以使用以下方式查看模型输出:
>model
a 15-4-3 network with 79 weights
nnet()
中有一些额外的参数,您可以根据需要进行设置。有关nnet()
的更多信息,您应该在命令提示符下输入以下命令:
> ? nnet
这解释了神经网络架构,15-4-3
表示三层结构;第一层(输入层)、第二层(隐藏层)和第三层(输出层)分别有15
、4
和3
个神经元,生成了79
个权重参数。您可以看到第一层的神经元数量等于norm_traindji
中的列数:
>dim(norm_traindji)
[1] 1006 15
您可以看到输出包含 15 列,这与输入数据特征的数量相同。这 15 列的数量与输入层的神经元数量相同。第二个参数是隐藏层中的神经元数量,这个数量作为输入提供给nnet()
(在我们的例子中是4
),最终参数是输出层中的神经元数量,它是3
,与方向(Up
、Down
和NoWhere
)的数量相同。您必须使用predict()
对验证数据集应用训练后的神经网络:
>vali_pred<- predict(model,norm_validji)
>head(vali_pred)
Down NoWhere Up
2014-01-02 01.336572e-01 1
2014-01-03 0 1.336572e-01 1
2014-01-06 0 1.336572e-01 1
2014-01-07 0 1.336572e-01 1
2014-01-08 0 8.666505e-02 1
2014-01-09 0 5.337864e-07 1
现在,我们需要根据上述信息确定预测的方向。我定义0.5
作为阈值,并选择值大于0.5
的方向。第一行创建了一个长度与vali_pred
相同的数据框。接下来的命令逐一对每个类别进行检查,并在vali_pred
大于0.5
的位置写入类别名称:
>vali_pred_class<- data.frame(matrix(NA,dim(vali_pred)[1],1))
>vali_pred_class[vali_pred[,"Down"] > 0.5,1] <- "Down"
>vali_pred_class[vali_pred[,"NoWhere"] > 0.5,1] <- "NoWhere"
>vali_pred_class[vali_pred[,"Up"] > 0.5,1] <- "Up"
现在我们将创建一个混淆矩阵来检查其准确性。首先,加载 caret 包到工作空间,并对验证数据集的预测类别和原始类别使用confusionMatrix()
:
>library(caret)
>matrix<- confusionMatrix(vali_pred_class[,1],validir)
>matrix
Confusion Matrix and Statistics
Reference
Prediction Down NoWhere Up
Down 33 3 0
NoWhere 6 125 8
Up 0 15 62
Overall Statistic
Accuracy : 0.873 95% CI : (0.8255, 0.9115)
No Information Rate : 0.5675 P-Value [Acc>NIR] : <2.2e-16
Kappa : 0.7811 Mcnemar'sTest P-Value : NA
Statistics by Class:
Class: Down Class: NoWhereClass: Up
Sensitivity 0.8462 0.8741 0.8857
Specificity 0.9859 0.8716 0.9176
PosPred Value 0.9167 0.8993 0.8052
NegPred Value 0.9722 0.8407 0.9543
Prevalence 0.1548 0.5675 0.2778
Detection Rate 0.1310 0.4960 0.2460
Detection Prevalence 0.1429 0.5516 0.3056
Balanced Accuracy 0.9160 0.8728 0.9016
如果您查看结果输出中的准确率水平,可以看到准确率为 87%,这个准确率相当不错。这 87%的准确率是针对在训练数据上训练的模型,且在验证数据上进行了测试。现在,我们还应该检查测试数据上的准确率,验证它的泛化能力。测试数据集的归一化已经完成,因此我直接进入predict()
命令:
>test_pred<- predict(model,norm_testdji)
测试数据的类别与验证数据相同:
>test_pred_class<- data.frame(matrix(NA,dim(test_pred)[1],1))
>test_pred_class[test_pred[,"Down"] > 0.5,1] <- "Down"
>test_pred_class[test_pred[,"NoWhere"] > 0.5,1] <- "NoWhere"
>test_pred_class[test_pred[,"Up"] > 0.5,1] <- "Up"
使用以下命令生成用于测试数据的ConfusionMatrix()
,可以看到测试数据集上的准确率为 82%;这一预测准确率与验证数据集上的预测准确率非常相似。与验证数据相比,结果始终保持良好:
>test_matrix<- confusionMatrix(test_pred_class[,1],testdir)
>test_matrix
Confusion Matrix and Statistics
Reference
Prediction Down NoWhere Up
Down 31 4 0
Nowhere 26 138 8
Up 0 6 38
Overall Statistics
Accuracy : 0.8247 95% CI : (0.7719, 0.8696)
在验证数据集和测试数据集中的准确性一致性表明其泛化能力,该模型具有较好的泛化能力。现在,既然我们已经得到了类别,接下来我们应该使用这些类别来生成信号。当人们预测上涨方向时,他们会买入;而当他们预测下跌
方向时,他们会卖出。所以,我根据相同的人类心理生成信号,以下命令为你完成了这一操作:
>signal<- ifelse(test_pred_class =="Up",1,ifelse(test_pred_class =="Down",-1,0))
Return ofdji closing price is calculated below
> ret<- ret[testrow]
交易收益按此处定义的方式计算。Lag()
函数应用于信号,因为上一时段生成的信号对交易收益有贡献。我假设成本为0
:
>cost<- 0
>trade_ret<- ret * Lag(signal)- cost
为了评估该策略的表现,我们需要加载相关包,并使用以下部分中定义的所有相关命令:
>library(PerformanceAnalytics)
>cumm_ret<- Return.cumulative(trade_ret)
>annual_ret<- Return.annualized(trade_ret)
以下命令生成图 6.2,展示了累计收益、每日收益和回撤。我们可以看到该策略的累计收益为负值。生成盈利策略超出了本书的范围。本书仅解释了如何使用 R 实现策略:
>charts.PerformanceSummary(trade_ret)
输出结果如下:
图 6.2:DJI 的累计收益、每日收益和回撤
深度神经网络
深度神经网络属于深度学习的广泛类别。与神经网络不同,深度神经网络包含多个隐藏层。隐藏层的数量因问题而异,需要进行优化。R 语言有许多包,如darch
、deepnet
、deeplearning
和h20
,可以创建深度网络。然而,我将特别使用deepnet
包,并应用于DJI数据。可以使用以下命令安装并加载deepnet
包:
>install.packages('deepnet')
>library(deepnet)
我将使用set.seed()
来生成均匀输出,dbn.dnn.train()
用于训练深度神经网络。参数hidden
用于设置隐藏层的数量以及每层的神经元数量。
在下面的示例中,我使用了一个包含三个隐藏层的结构,并在第一、第二和第三个隐藏层中分别使用了3
、4
和6
个神经元。class.ind()
再次用于将三个方向转换为列向量,每一列表示一个方向:
>set.seed(1)
>model<- dbn.dnn.train(norm_traindji,class.ind(traindir),hidden=c(3,4,6))
以下命令用于生成使用归一化验证数据集的三个类别的输出:
>nn.predict(model,norm_validji)
为了获得模型在验证数据集上的准确度,你还可以使用以下命令。我选择t=0.4
仅仅是为了展示结果。你应根据实际需求选择合适的值。如果其值大于0.4
,将会生成每一列对应某一方向的输出:
>nn.test(model,norm_validji,class.ind(validir),t=0.4)
[1] 0.7222222
H2o
是另一个可以用于深度神经网络学习的包。它是用 Java 实现的,能够利用 CPU 的多线程和多节点;然而,deepnet
是用 R 本身实现的,只使用单线程,并且没有灵活性来使用 CPU 的多线程和多节点。以下命令会安装并将其加载到工作空间:
>install.packages(h2o)
>library(h2o)
接下来,我将标准化的训练数据和方向结合成一个变量。我将标准化数据转换为数据框架,格式与原始数据的xts
、zoo
格式一致。由于标准化的训练数据是数值型的,如果我没有将其转换为数据框,那么添加字符型的traindir
会将traindir
转换为 NAs。为了避免这种情况,我在以下命令中使用了数据框,可以使用接下来的两个命令验证输入变量的类别:
>data<- cbind(as.data.frame(norm_traindji),traindir)
>class(norm_traindji)
[1] "xts" "zoo"
>class(traindir)
[1] "character"
一旦我完成了变量的创建,我就将其转换为h2o
对象,因为模型拟合需要输入数据是h2o
格式。在以下命令中,第一个参数是我要转换的变量,第二个参数是我们希望第一个参数转换成的类名称。
在以下命令中,我希望将数据转换为h2o
类型。也可以使用第二个命令进行验证:
>datah2o<- as.h2o(data,"h2o")
>class(datah2o)
[1] "H2OFrame"
我们查看了刚刚创建的h2o
类对象的维度,其中最后一列是方向向量,剩余的列是标准化的数据列:
>dim(datah2o)
[1] 1006 16
以下是h2o.deeplearning()
,它训练一个具有四个隐藏层架构的深度神经网络,每个隐藏层的神经元数分别为4
、5
、2
和7
。第一个参数是 1 到 15 列的向量,假设为输入数据,第二个参数 16 表示第 16 列作为输出,供深度神经网络进行训练。第三个参数是 datah2o,用于深度神经网络的拟合,第四个参数是 hidden。参数 hidden 在这里具有重要意义,表示隐藏层的总数,以下示例显示了四个隐藏层:第一个隐藏层有 4 个神经元,第二个隐藏层有 5 个神经元,第三个和第四个隐藏层分别有 2 和 7 个神经元:
> model <-h2o.deeplearning(1:15,16,training_frame=datah2o,hidden=c(4,5,2,7))
>vali_pred<- predict(model,as.h2o(norm_validji,"h2o"))
predict Down NoWhere Up
1 Up 8.774719e-06 0.05996300 0.9400282
2 Up 4.715592e-06 0.04561811 0.9543772
3 Up 8.522070e-06 0.06120060 0.9387909
4 Up 1.384947e-06 0.02668458 0.9733140
5 Up 3.698133e-06 0.04144544 0.9585509
6 Up 2.016126e-06 0.03151435 0.9684836
[252 rows x 4 columns]
由于vali_pred
是H2OFrame
格式,我们应该将其转换为数据框,以便应用以下操作:
>vali_pred<- as.data.frame(vali_pred)
>vali_pred_class<- data.frame(matrix(NA,dim(vali_pred)[1],1))
>vali_pred_class[vali_pred[,"Down"] > 0.5,1] <- "Down"
>vali_pred_class[vali_pred[,"NoWhere"] > 0.5,1] <- "NoWhere"
>vali_pred_class[vali_pred[,"Up"] > 0.5,1] <- "Up"
我使用了 caret 包和confusionMatrix()
来创建误分类矩阵:
>library(caret)
>vali_matrix<- confusionMatrix(vali_pred_class[,1],validir)
如同我们在验证数据集上所做的那样,如果准确率在期望的范围内,我们应该继续使用测试数据预测方向,并使用这些预测的方向生成交易信号,正如在神经网络部分中所生成的那样。为了生成信号和策略的表现,你应该使用神经网络部分中提到的命令。
K 均值算法
K-means 算法是一种无监督机器学习算法。无监督学习是另一种数据分类方式,因为它不需要数据的标签。实际上,很多情况下数据无法被标注,因此我们需要基于无监督学习来分类数据。无监督学习通过数据元素之间的相似性,将每个数据点分配到相应的聚类中。每个聚类包含一组在性质上相似的数据点。K-means 算法是最基本的无监督学习算法,它只需要输入数据和我们希望聚类的数量,并返回每个数据点的聚类标签向量。我使用了标准化数据和聚类数量。我使用了在逻辑回归中使用的内样本数据,将其分为三个聚类。
set.seed()
用于确保每次迭代输出相同;如果不使用 set.seed()
,输出会在每次运行时变化:
>clusters<- 3
>set.seed(1)
标准化后的内样本和外样本数据将方向(标签)作为最后一列,而无监督学习不需要这些标签。因此,我通过以下命令删除了这两个数据集中的最后一列:
>norm_isdji<- norm_isdji[,-dm[2]]
>norm_osdji<- norm_osdji[,-dm[2]]
现在我没有这些数据的标签,并运行 kmeans()
:
>model<- kmeans(norm_isdji,clusters)
model$cluser
返回每个数据点对应的聚类编号,head()
用于输出前几个数据点的聚类编号:
>head(model$cluster)
2010-01-04 2010-01-05 2010-01-06 2010-01-07 2010-01-08
3 3 3 3 3
上述命令显示前几个数据点属于聚类编号 3。同样,最终聚类的中心可以通过以下命令提取:
>model$center
每个聚类中的数据点数量可以通过以下命令提取:
>model$size
260 434 564
由于我们使用的是 k-means 聚类算法,它是无监督学习,性能或准确性可以通过平方和与总平方和的比值来计算。聚类间的平方和与总平方和可以通过以下命令提取:
>model$tot.withinss
9703.398
>model$totss
19129.26
这些值的比率表示聚类内的平方和与总平方和的比率。在我们的例子中,这个比率大约是 50.7%,如下所示,表示聚类内的平方和几乎占总平方和的一半。在一系列模型中,选择最小化该比率的模型。这是一个最小化问题:
>model$tot.withinss / model$totss
0.5072543
如果我们对算法的准确性满意,我们将继续使用这个拟合模型来预测外样本数据集的聚类,这可以通过 predict()
命令来实现:
>ospredict<- cl_predict(model,norm_osdji)
下一行使用 head()
提取外样本数据的前几个预测聚类编号:
>head(ospredict)
2 2 2 2 2 2
该算法将每个来自外部样本数据集的数据点分配到任一簇中,每个簇属于一个市场方向,即Up
(上涨)、Down
(下跌)和Nowhere
(无方向)。在开始之前,弄清楚哪个簇代表Up
,哪个代表Down
和Nowhere
非常重要。一旦你识别出每个簇是Up
、Down
还是Nowhere
,当数据点落入相关簇时,我们就可以进行相关交易。例如,在前述的情况下,前六个数据点的输出是 2,这意味着这些数据点位于同一个簇中,但我们不知道这是否是Up
簇、Down
簇,还是Nowhere
簇。你可以通过计算一个簇中数据点的平均价格来弄清楚这一点,如果平均值高于某个阈值,则可以将其视为Up
簇;如果平均价格低于某个阈值,则为Down
簇;如果平均价格位于第一个数据点的上下某个阈值范围内,则为Nowhere
簇。还有其他技术可以用来确定簇的类别;你可以使用任何你喜欢的方法。当数据点落入Up
簇时,我们进入多头交易;对于其他两个簇也是如此。我们应该通过研究每个簇来设计交易策略。行为识别至关重要,因为这将帮助我们设计交易策略。我们应该知道哪个簇代表Up
、Down
或Nowhere
方向。我们应该基于神经网络部分中提到的例子生成交易信号并返回。
K 最近邻算法
K 最近邻是另一种监督学习算法,帮助我们在 k 个类别中确定外样本数据的类别。K 值必须适当选择,否则可能增加方差或偏差,降低算法的泛化能力。我将Up
、Down
和Nowhere
作为需要在外样本数据中识别的三个类别。这是基于欧几里得距离的。对于外样本数据中的每个数据点,我们计算它与内样本数据中所有数据点的距离。每个数据点都有一个距离向量,选择与其足够接近的 K 个距离,并根据所有 K 个邻域的加权组合来决定数据点的最终类别:
>library(class)
R 中的 K 最近邻函数不需要训练数据中的标签值。因此,我将使用在逻辑回归部分创建的标准化内样本和标准化外样本数据,并删除标准化内样本和标准化外样本数据中的最后一列:
>norm_isdji<- norm_isdji[,-dm[2]]
>norm_osdji<- norm_osdji[,-dm[2]]
训练数据的标签是一个包含三个方向的向量,即Up
、Down
和Nowhere
,通过以下命令构造:
>lagret<- (dji - Lag(dji,20)) / Lag(dji,20)
lagret
是过去 20 个数据点的回报,并用于生成如神经网络部分中的三种方向:
>direction[lagret> 0.02] <- "Up"
>direction[lagret< -0.02] <- "Down"
>direction[lagret< 0.02 &lagret> -0.02] <- "NoWhere"
>isdir<- direction[isrow]
>osdir<- direction[osrow]
我选择了三个邻域,并固定了set.seed()
值,以便每次生成相同的输出:
>neighborhood<- 3
>set.seed(1)
>model<- knn(norm_isdji,norm_osdji,isdir,neighborhood)
knn()
模型有前三个必填参数,分别是归一化的样本内数据、归一化的样本外数据以及我们案例中的训练标签数据。第四个参数是可选的;我在此输入了 3。如果用户没有提供该值,R 将使用默认值 1。不过,3 并不是固定的,它需要通过多个邻域值进行优化。knn()
函数返回样本外数据的类别,可以通过以下命令检查:
>head(model)
[1]NoWhere Nowhere Nowhere Nowhere NoWhere
对模型使用Summary()
会生成每个类别中的数据点总数,正如以下命令所示。它分别生成了44
、172
和36
个数据点,分别对应Down
、Nowhere
和Up
类别:
>summary(model)
Down NoWhere Up
44 172 36
我们不确定准确度如何。我们需要通过以下命令进行准确度测试。confusionMatrix()
会生成正确和错误预测的计数矩阵:
>library(caret)
>matrix<- confusionMatrix(model,osdir)
>matrix
Confusion Matrix and Statistics
Reference
Prediction Down NoWhere Up
Down 32 12 0
NoWhere 26 133 13
Up 0 3 3
Overall Statistics
Accuracy : 0.7857 95% CI : (0.7298, 0.8347)
No Information Rate : 0.5873 P-Value [Acc>NIR] : 2.173e-11
我们还需要最小化非对角线元素,因为这些是错误分类的类别。可以通过以下命令提取对角线元素:
>diag(matrix$table)
Down NoWhere Up
32 133 33
我们可以使用一个for
循环,遍历从 1 到 30 的邻域,并找到每个值对应的准确度。我们可以选择最佳的 k 值,该值在其邻域中具有最高且一致的准确度。
以下几行代码做了说明。For
循环用于遍历从 1 到 30 的值。在for
循环内部,我为每个值拟合模型,即confusionMatrix()
为每个i
计算矩阵,并计算对角线元素和样本外数据中的总元素数。matrix$table
的所有元素之和等于样本外数据中的数据点数。错误分类数是通过从总点数中减去diag
来计算的,而准确度则通过将其除以数据点总数来得出:
> accuracy<- NULL
>for(i in c(1:30))
{
model<- knn(isdji,osdji,isdir,i)
matrix<- confusionMatrix(model,osdir)
diag<- sum(diag(matrix$table))
total<- sum(matrix$table)
accuracy[i] <- (total - diag) / total
}
我们可以使用head()
检查变量accuracy
的输出:
>head(accuracy)
0.4404762 0.4087302 0.3452381 0.4563492 0.4801587 0.4642857
以下命令plot()
生成图 6.3,解释了邻域值变化对准确度的影响。图 6.3清楚地解释了邻域的重要性,因为错误在k=14时最小,这被认为是最佳值。然而,k=15使错误激增,这意味着k=14在其邻域中并不稳定。k=12在其邻域中也被认为是稳定的,因此如何选择最佳值留给读者自行决定:
>plot(accuracy, type="l")
图 6.3:KNN 分类器的准确度水平
支持向量机
支持向量机是另一种可以用于分类和回归的监督学习算法。它能够通过核方法对数据进行线性和非线性分类。训练数据集中的每个数据点都有标签,因为它是监督学习,并且映射到输入特征空间,目的是将每个新数据点分类到某一类别。数据点是一个N维数,其中N是特征的数量,问题是使用N-1维超平面将这些数据分开,这被认为是一个线性分类器。可能有许多分类器可以分隔数据;然而,最佳分类器是那个在类别之间具有最大间隔的分类器。最大间隔超平面是指与每一侧最近的点具有最大距离的超平面,且对应的分类器称为最大间隔分类器。e1071
包具有与支持向量机相关的所有功能,因此我将首先使用以下命令安装它:
>install.packages("e1071",dependencies=TRUE)
安装完成后,我将使用以下命令将其加载到工作区:
>library(e1071)
我将使用与之前“章节”中相同的归一化内样本和外样本数据。svm()
函数还需要一些其他参数,例如支持向量机类型、核类型等。类型参数可以选择训练支持向量机以解决分类或回归问题;默认情况下,它会考虑分类问题。核类型有多种选择,如线性、多项式、径向和 sigmoid,线性核类型是默认参数。以下命令说明了仅使用前两个参数和剩余默认参数的支持向量机的使用:
>model<- svm(norm_isdji,as.factor(isdir))
svm()
函数的输出保存在变量model
中,可以通过在命令提示符中输入变量名model
来查看:
>model
Call:
svm.default(x = norm_isdji, y = as.factor(isdir))
Parameters:
SVM-Type: C-classification
SVM-Kernel: radial
cost: 1 gamma: 0.06666667
Number of Support Vectors: 505
前面的结果显示了拟合的支持向量机类型,以及用于拟合模型的核类型。predict()
帮助预测外部样本数据的方向:
>pred<- predict(model,norm_osdji)
可以使用以下命令查看前几个预测方向:
>head(pred)
1 2 3 4 5
NoWhere NoWhere NoWhere NoWhere NoWhere
table()
命令生成一个误分类矩阵,并清楚地显示总共 45 个误分类的数据点:
>table(pred, osdir)
osdir
pred Down NoWhere Up
Down 32 6 0
NoWhere 26 139 10
Up 0 3 36
如果你想查看支持向量机生成的向量,可以使用以下命令:
>model$SV
你也可以使用以下命令查看相应的索引值:
>model$index
可以使用以下命令查看前几个索引值:
>head(model$index)
[1] 1 4 5 11 12 34
可以使用以下命令访问相应的系数:
>model$coefs
决策树
基于树的学习算法是最好的监督学习方法之一。它们通常对结果具有稳定性,并且对样本外数据集具有很好的准确性和泛化能力。它们能够很好地映射线性和非线性关系。通常以变量树的形式表示,其结果为树中的节点变量,终端值则是决策规则。我将使用 party
包来实现决策树。首先需要安装并使用以下命令加载该包:
>install.packages("party")
>library(party)
ctree()
函数是用于拟合决策树的函数,它需要公式和数据作为必需参数,并且还有一些可选变量。标准化的样本内数据和标准化的样本外数据没有标签,所以我们必须将标签合并到数据中。
以下命令将标签绑定到标准化的样本内和样本外数据中,并为两个数据集的最后一列添加列名:
>norm_isdji<- cbind(norm_isdji,isdir)
>norm_osdji<- cbind(norm_osdji,osdir)
>colnames(norm_isdji)[dim(norm_isdji)[2]] <- "Direction"
>colnames(norm_osdji)[dim(norm_osdji)[2]] <- "Direction"
现在两个数据集都包含了标签数据,我们现在选择使用 ctree()
来拟合决策树。
第一个参数是公式,公式中包含 Direction
,即标签作为因变量,公式另一边的点号(.
)表示我们将所有其他变量视为自变量。
第二个参数是标准化的样本内数据:
>model<- ctree(Direction ~ .,norm_isdji)
你可以使用 print()
来查看拟合模型的输出。变量 model 是模型的输出,因此使用以下命令查看它包含的内容:
>print(model)
如果你想绘制模型,可以使用 plot()
来完成:
>plot(model)
你还可以使用 summary()
获取总结形式的输出:
>summary(model)
predict()
可以用于使用拟合的模型和样本外数据来估计标签。我计算了标准化样本外数据的维度,并将该数据(除了最后一列)传入 predict()
:
>dm<- dim(norm_osdji)
>pred<- predict(model,norm_osdji[,1:(dm[2]-1)])
使用 head()
可以查看 pred
的前几个值,如下所示:
>head(pred)
Direction
[1,] 2.040816
[2,] 2.040816
[3,] 2.040816
[4,] 2.040816
命令 plot()
为预测变量 pred
生成一个图表,如下图所示:
>plot(pred)
以下图表清楚地显示了三类数据:第一类位于 1.0 和 1.5 之间,第二类在 2.0 附近,第三类在 3.0 附近。数据点根据聚类和分离标准被清晰区分:
图 6.4:标准化样本外数据的预测值
随机森林
随机森林是最好的基于树的方法之一。随机森林是多个决策树的集合,每个决策树都有一定的权重。随机森林的决策是通过类似投票的方式决定的,大多数决策树的结果决定随机森林的结果。因此,我们开始使用 randomForest
包,可以使用以下命令进行安装并加载:
>install.packages("randomForest")
>library(randomForest)
我们还可以使用以下命令了解更多关于这个randomForest
包的信息,包括版本、发布日期、网址、包中实现的函数集等:
>library(help=randomForest)
随机森林适用于任何类型的问题,并且能很好地处理分类、回归和无监督问题。根据标签变量的类型,它会实现相关的决策树;例如,对于因子目标变量,它使用分类;对于数值型或整数型目标变量,它使用回归;当目标向量未定义或完全未知时,它使用无监督决策树。我将使用本章中使用的标签数据:用于模型构建的归一化内样本数据和用于模型验证的归一化外样本数据。你可以使用以下命令查看输入数据的列名:
>names(norm_isdji)
[1] "DJI.Close" "DJI.Close.1" "DJI.Close.2" "DJI.Close.3" "DJI.Close.4" "SMA" "SMA.1" "macd" "signal" "macd.1" "signal.1" "dn" "mavg" "up" "pctB"
以下命令帮助你了解输入数据中独立变量的数量。它显示我们的输入数据集将有 15 个独立变量,如之前所示:
>length(names(norm_isdji))
[1] 15
由于标签数据有三个类别:Up
、Down
和Nowhere
,我们应该构建一个分类随机森林。对于分类问题,randomForest()
函数将标签数据作为因子,因此首先我们应该检查标签数据的类型,可以使用以下命令进行检查,它显示标签数据是字符类型:
>class(isdir)
[1] "character"
接下来,我们应该将标签数据转换为因子类型,因为randomForest()
接受因子类型的标签数据来解决分类问题。以下两行代码将字符数据转换为因子:
>isdir<- as.factor(isdir)
>osdir<- as.factor(osdir)
现在,如果我们检查标签数据的类别,可以再次使用class()
函数,以下命令显示我们已将字符数据类型转换为因子:
>class(as.factor(isdir))
[1] "factor"
现在我们已经准备好内样本和外样本数据集,并将这些数据集输入到randomForest()
中,使用以下命令。函数中的第一个参数是归一化的内样本独立变量数据框,第二个参数是内样本标签,第三个参数是外样本独立变量数据框,第四个参数是外样本标签,第五个参数是用于随机森林模型构建的树木数量,我设置为500
:
>model<- randomForest(norm_isdji, y=as.factor(isdir),
xtest=norm_osdji, ytest=as.factor(osdir), ntree=500)
然而,还有许多其他参数可以根据需要使用。如果你想了解更多关于其他参数的信息,下面这行代码将打开一个新窗口,详细解释randomForest
函数的所有内容,包括输入变量、输入变量类型、输出变量、示例等:
>help(randomForest)
你可以使用以下命令查看模型输出。首先显示用于拟合模型的命令,然后是森林的类型,在我们的例子中是分类,因为标记数据是因子类型或由三类组成,接下来是树的数量,因为我们在先前的命令中提供了500
作为参数。它还计算了样本内和样本外的数据的混淆矩阵。样本内数据的错误率为11.76%
,样本外数据的错误率为21.03%
,这被认为是相当不错的。如果深入查看样本内和样本外的混淆矩阵,你还可以找到每个类别的单独误差率。在样本内混淆矩阵中,第四列包含的错误率分别是11.34%
、14.40%
和9.55%
,对应Down
、NoWhere
和Up
类别。同样,你也可以解释样本外的混淆矩阵:
>print(model)
Call:
randomForest(x = norm_isdji, y = as.factor(isdir),
xtest = norm_osdji, ytest = as.factor(osdir), ntree = 500)
Type of random forest: classification
Number of trees: 500
No. of variables tried at each split: 3
OOB estimate of error rate: 11.76%
Confusion matrix:
Down NoWhere Up class.error
Down 211 27 0 0.11344538
NoWhere 19 416 51 0.14403292
Up 0 51 483 0.09550562
Test set error rate: 21.03%
Confusion matrix:
Down NoWhere Up class.error
Down 26 32 0 0.55172414
NoWhere 6 138 4 0.06756757
Up 0 11 35 0.23913043
拟合模型以矩阵形式生成误差,如果你想深入了解错误矩阵,可以使用head()
查看矩阵格式。以下矩阵显示了样本内数据的错误率,接下来的三列分别显示了每个类别在 500 棵决策树中的误差:
>head(model$err.rate)
OOBDown NoWhere Up
[1,] 0.2159329 0.08791209 0.2967033 0.2009804
[2,] 0.1855263 0.16438356 0.2430556 0.1441718
[3,] 0.1911765 0.15508021 0.2320442 0.1712159
[4,] 0.1854991 0.16097561 0.2369077 0.1513158
[5,] 0.1901408 0.17129630 0.2534884 0.1428571
以下命令绘制了所有500
棵决策树的总体样本内和三类误差,你可以在图 6.5中看到,经过 100 棵决策树后,错误率没有显著下降:
>plot(model$err.rate[,1],type="l",ylim=c(0.05,0.3),ylab="Error")
>lines(model$err.rate[,2],col="red")
>lines(model$err.rate[,3],col="green")
>lines(model$err.rate[,4],col="blue")
绘图如下:
图 6.5:500 棵决策树的错误率
如果你想提取有助于控制误差的变量,可以根据MeanDecreaseGinni
选择这些变量。MeanDecreaseGinni
可以通过以下代码访问:
>value<- importance(model,type = 2)
>head(value)
MeanDecreaseGini
DJI.Close 22.09961
DJI.Close.1 18.55651
DJI.Close.2 16.87061
DJI.Close.3 27.23347
问题
-
什么是机器学习,它在资本市场中是如何应用的?简要说明。
-
什么是逻辑回归,它以何种形式生成输出?
-
编写一小段代码,使用神经网络处理任何股票的时间序列数据。
-
混淆矩阵如何解释模型的准确性?
-
如何标准化数据,为什么它在模型构建过程中很重要?
-
支持向量机与逻辑回归有何不同?
-
解释监督学习和无监督学习,并说明如何在算法交易中使用这些技术。
-
编写一小段代码,使用任意一个股票的收盘价实现 k 均值算法。
-
除了
confusionMatrix()
,还有什么函数可以计算分类和误分类矩阵? -
决策树和随机森林有什么区别,如何从随机森林中选择特征?
总结
本章介绍了针对资本市场实施的高级技术。我详细介绍了各种有监督和无监督学习,并附带了实例。本章特别使用了道琼斯指数的收盘价作为数据集,该数据集被划分为样本内数据和样本外数据。样本内数据用于模型构建,样本外数据用于模型验证。过拟合和欠拟合通常会质疑模型的泛化能力,可以通过混淆矩阵来理解。模型的准确性是通过confusionMatrix()
或table()
来定义的。
市场中存在各种类型的风险,在下一章中,我将解释如何计算与各种投资相关的风险,特别是市场风险、投资组合风险等。我还将解释蒙特卡洛模拟在风险计算中的应用、对冲技术以及信用风险,同时介绍巴塞尔协议的相关规定。
第七章:风险管理
本章我们将讨论与银行和金融领域相关的各种风险类型。银行和金融机构都面临风险,它们需要在实施监管规范的同时,开发风险识别和风险缓解机制,以保持竞争力和盈利能力。本章将讨论使用 R 语言衡量不同类型风险的各种技术。内容还包括与银行业务相关的风险,如信用风险、欺诈检测和巴塞尔协议等。
本章涵盖以下主题:
-
市场风险
-
投资组合风险
-
VaR
-
蒙特卡罗模拟
-
对冲
-
巴塞尔协议
-
信用风险
-
欺诈检测
市场风险
投资者因市场整体表现变化而遭遇损失的风险,称为市场风险。市场风险是一种系统性风险,无法通过多样化来应对。它可以通过对冲来减轻。由于经济衰退、政治不稳定、利率变化、自然灾害和恐怖袭击等原因产生的风险都是市场风险的例子。市场风险的衡量方法因银行、单个股票、投资组合等不同而有所不同。
让我们考虑如何衡量单一证券的市场风险。作为投资组合一部分的股票的市场风险被衡量为该证券对整体投资组合风险的贡献。单个股票的风险通过 beta 系数来衡量,它表示股票相对于市场的波动性。
让我们对 IBM 股票(作为因变量)和 GPSC 指数(作为自变量)进行回归分析,并尝试估算 beta 值。可以通过执行以下代码实现,该代码使用 2010 年至 2016 年期间 GPSC 和 IBM 的月度数据:
> GPSCMonthlyUrl<-'http://ichart.yahoo.com/table.csv?s=%5EGSPC&a=00&b=1&c=2010&d=00&e=1&f=2017&g=m'
> GPSCMonthlyData <- read.csv(GPSCMonthlyUrl)
> IBMMonthlyUrl<-'http://ichart.yahoo.com/table.csv?s=IBM&a=00&b=1&c=2010&d=00&e=1&f=2017&g=m'
> IBMMonthlyData <- read.csv(IBMMonthlyUrl)
> DateRange <- GPSCMonthlyData$Date == IBMMonthlyData$Date
> GPSCPrice<-GPSCMonthlyData$Close[DateRange]
> IBMPrice<-IBMMonthlyData$Close[DateRange]
> GPSCReturns <- ( GPSCPrice[1:(length(GPSCPrice) - 1)] - GPSCPrice[2:length(GPSCPrice)] ) / GPSCPrice[2:length(GPSCPrice)]
> IBMReturns <- ( IBMPrice[1:(length(IBMPrice) - 1)] - IBMPrice[2:length(IBMPrice)] ) / IBMPrice[2:length(IBMPrice)]
> betafit <- lm(IBMReturns ~ GPSCReturns)
> result <- summary(betafit)
> beta <- result$coefficients[2,1]
> print(beta)
它给出了 beta 的估算值,如下所示:
[1] 0.72390819
投资者和分析师使用的另一种技术是价值-at-风险(VaR)。这是一种在金融市场中非常常见的风险衡量方法。价值-at-风险方法是一个知名且成熟的风险管理方法,但它也有一些假设,限制了其正确性。例如,其中一个假设是,被测量的投资组合内容在给定期间内保持不变。因此,这种方法可能对短期投资有效,但对于长期投资的风险测量准确性较差,因为它更容易受到利率和货币政策变化的影响。我们将在后面讨论如何在 R 中计算 VaR 和 CVAR/ES。
投资组合风险
使用 R 语言,我们可以通过降低风险和优化投资组合来更好地管理投资组合。为了避免与投资组合分析相关的风险,必须进行投资组合多样化,并为投资组合中的各成分选择最佳权重。
让我们尝试找出由 IBM 和 FB 组成的投资组合的最佳权重,并使用 CAPM。首先,通过执行以下代码获取相关数据:
>GPSCMonthlyUrl<-'http://ichart.yahoo.com/table.csv?s=%5EGSPC&a=00&b=1&c=2015&d=00&e=1&f=2017&g=m'
>GPSCMonthlyData <- read.csv(GPSCMonthlyUrl)
>IBMMonthlyUrl<-'http://ichart.yahoo.com/table.csv?s=IBM&a=00&b=1&c=2015&d=00&e=1&f=2017&g=m'
>IBMMonthlyData <- read.csv(IBMMonthlyUrl)
>FBMonthlyUrl<-'http://ichart.yahoo.com/table.csv?s=FB&a=00&b=1&c=2015&d=00&e=1&f=2017&g=m'
>FBMonthlyData <- read.csv(FBMonthlyUrl)
>VMonthlyUrl<-'http://ichart.yahoo.com/table.csv?s=V&a=00&b=1&c=2015&d=00&e=1&f=2017&g=m'
VMonthlyData <- read.csv(VMonthlyUrl)
这是 2015 年至 2016 年间的月度数据。
现在,让我们通过执行以下代码,尝试找到上述数据的收盘价格收益:
> DateRange <- GPSCMonthlyData$Date
> GPSCPrice<-GPSCMonthlyData$Close[DateRange]
> IBMPrice<-IBMMonthlyData$Close[DateRange]
> FBPrice<-FBMonthlyData$Close[DateRange]
> VPrice<-VMonthlyData$Close[DateRange]
> GPSCReturns <- ( GPSCPrice[1:(length(GPSCPrice) - 1)] - GPSCPrice[2:length(GPSCPrice)] ) / GPSCPrice[2:length(GPSCPrice)]
> IBMReturns <- ( IBMPrice[1:(length(IBMPrice) - 1)] - IBMPrice[2:length(IBMPrice)] ) / IBMPrice[2:length(IBMPrice)]
> FBReturns <- ( FBPrice[1:(length(FBPrice) - 1)] - FBPrice[2:length(FBPrice)] ) / FBPrice[2:length(FBPrice)]
> VReturns <- ( VPrice[1:(length(VPrice) - 1)] - VPrice[2:length(VPrice)] ) / VPrice[2:length(VPrice)]
它生成所有系列的收益。
现在,让我们尝试找出所有系列的超额收益。超额收益由月度收益减去月度国债利率(设为 .0015
)给出。可以通过执行以下代码来完成:
> EGPSCReturns<- GPSCReturns-.0015
> EIBMReturns<- IBMReturns-.0015
> EFBReturns<- FBReturns-.0015
> EVReturns<- VReturns-.0015
接下来,计算所有系列的超额收益的均值和标准差。可以通过执行以下代码得到:
> MeanSD<-rbind(cbind("GPSC",mean(EGPSCReturns),sd(EGPSCReturns)),cbind("FB",mean(EFBReturns),sd(EFBReturns)),cbind("IBM",mean(EIBMReturns),sd(EIBMReturns)),cbind("V",mean(EVReturns),sd(EVReturns)))
> MeanSD
它生成以下输出:
图 7.1:所有考虑股票的均值和标准差
现在,让我们通过回归分析来找出所有股票超额收益的贝塔值,回归对象为 S&P 指数的超额收益。
这可以通过执行以下代码完成:
> lmIBM<- lm(IBMReturns ~ EGPSCReturns)
> summary(lmIBM)
它生成以下输出:
图 7.2:回归输出汇总
这表明 IBM 的贝塔值为 1.1035670
。
同样,我们可以找到 FB 和 V 的贝塔值。
所以,使用 CAPM,IBM 的超额预期收益如下所示:
Beta of IBM*expected excess return of GPSC
=1.1035670*(-.005955424) = -0.0065722094
根据单因子模型,IBM 的方差由以下公式给出:
图 7.3:单因子模型给出的方差
这里,e 是回归中产生的残差误差。
因此,通过对所有独立变量进行回归并计算超额收益和方差,我们得到以下结果:
IBM | FB | V | |
---|---|---|---|
方差 | 0.002906 | 0.002949 | 0.455695 |
贝塔值 | 1.103567 | 0.423458 | 3.74228 |
预期超额收益 | -0.00657 | -0.00252 | -0.02229 |
协方差矩阵可以使用以下公式计算:
图 7.4:协方差公式
它生成以下矩阵:
IBM |
FB |
V |
|
---|---|---|---|
IBM |
0.001378 |
0.000529 |
0.004673 |
FB |
0.000529 |
0.000203 |
0.001793 |
V |
0.004673 |
0.001793 |
0.015848 |
现在,让我们通过执行以下代码来找到投资组合的最佳权重:
> returns_avg<-matrix(c(-0.0180513406031643,-0.00357192217566396,0.12613583240944),nrow =1)
> covariance<-matrix(c(0.001378118,0.000528808,0.004673302,0.000528808,0.000202913,0.001793228,0.004673302,0.001793228,0.015847524),nrow=3)
> library(tseries)
> sol<-portfolio.optim(x=returns_avg,covmat=covariance, shorts=F)
> sol$pw
它生成最佳权重,如下表所示:
IBM |
FB |
V |
---|---|---|
0 |
0.70387703 |
0.29612297 |
VaR
风险价值(VaR)是风险管理中的一个指标,用来衡量可能发生的风险,它能衡量投资者投资组合可能遭遇的潜在损失。5% 信心水平下的 VaR 表示损失值 95% 的时间不会低于预测值,换句话说,损失将有 5% 的概率大于预测值。
计算风险价值有三种常见方法:
-
参数化 VaR
-
历史 VaR
-
蒙特卡洛 VaR
在本节中,我们将介绍前两种方法,第三种将在下一节介绍。
参数化 VaR
参数 VaR 也称为方差-协方差法,用于通过均值和标准差作为参数计算 VaR。
qnorm
用于通过参数法计算风险价值。它使用均值和标准差作为参数。一般语法如下:
qnorm(p,mean,sd)
这里,p
是所需的百分位数;mean
是样本的给定均值;sd
是样本的标准差。
假设某只股票的平均收益率为 2%,标准差为 4%,那么使用参数法,在 95%置信水平下,计算出一天期限的风险价值可以执行以下方法:
>mean = 2
>sigma = 4
>Alpha = .05
>Var_parametric = qnorm(alpha, mean, sigma)
>Var_parametric
执行后,它生成以下输出:
[1] -4.579
或者,我们可以使用以下代码通过参数法找到 VaR:
> Var_parametric = mean + sigma*qnorm(alpha,0,1)
> Var_parametric
它生成如下的输出:
[1] -4.579
假设我们有一天期限的 VaR,如果我们想将其转换到不同的期限,比如一个月,可以使用这里给出的公式进行转换:
这里,T是一个月中的天数。
期望损失(ES)也被称为风险条件值,是 VaR 的替代方法。ES 是风险价值和大于风险价值的损失之间的加权平均。通过 ES,我们试图量化 VaR。
上述示例的 ES/CVAR 可以使用以下示例计算:
alpha_z=qnorm(alpha)
ES_parametric = mean + sigma*(dnorm(alpha_z)/(1-alpha))
ES_parametric
它生成以下输出:
[1] 2.434
历史 VaR
主要假设是过去会重演。它不假设任何特定的分布类型。历史 VaR 是通过模拟或构建资产收益的累计分布函数(CDF)来估算的。通常,我们按常规时间间隔找到收益并进行排序,然后找出相关的百分位数。
现在让我们尝试在数据集上找到单独的 VaR,以及投资组合的 VaR。数据集是通过执行以下代码生成的:
>library(quantmod)
> symbollist = c("FB", "V","JNJ")
> getSymbols(symbollist, from ="2016-01-01", to = "2017-01-01")
> FB = FB[, "FB.Adjusted", drop=F]
> V = V[, "V.Adjusted", drop=F]
> JNJ = JNJ[, "JNJ.Adjusted", drop=F]
> FB_return = CalculateReturns(FB, method="log")
> V_return = CalculateReturns(V, method="log")
> JNJ_return = CalculateReturns(JNJ, method="log")
> FB_return = FB_return[-1,]
> V_return = V_return[-1,]
> JNJ_return = JNJ_return[-1,]
> FB_V_JNJ_return<-cbind(FB_return,V_return,JNJ_return)
> head(FB_V_JNJ_return)
它准备好历史 VaR 所需的数据集,并显示如下的数据样本:
图 7.5:收益计算输出
现在,让我们尝试通过运行以下代码估计个别股票的历史
VaR:
> HVAR<-VaR(FB_V_JNJ_return, p=0.95, method="historical")
> HVAR
它生成以下输出:
图 7.6:历史单独输出
类似地,让我们尝试估计 CVAR/ES,它可以使用以下代码计算:
> HCVAR<-ES(FB_V_JNJ_return, p=0.95, method="historical")
> HCVAR
执行后,它生成如下的输出:
图 7.7:历史 CVAR 单独输出
我们可以在VaR()
函数中找到许多选项。这里列出了最重要的选项:
-
R
:矩阵,xts 向量或数据框 -
p
:置信水平 -
method
:它使用四种方法——修正法、高斯法、历史法和核方法 -
portfolio_method
:它有三种选项——单一、成分和边际——定义是否进行单变量、成分或边际计算
现在让我们尝试计算投资组合的组件
VaR。如果没有指定权重,则默认为等权重。可以通过运行以下代码来获得:
> VaR(FB_V_JNJ_return, p=0.95,portfolio_method="component")
它生成以下输出:
图 7.8:通过组件法输出的历史 VaR
同样,我们可以通过执行以下代码来找到边际
VaR:
> VaR(FB_V_JNJ_return, p=0.95,portfolio_method="marginal")
它生成以下结果:
图 7.9:通过边际输出法输出的历史 VaR
蒙特卡罗模拟
蒙特卡罗模拟在风险管理中起着非常重要的作用。即使我们获得了与公司相关的所有风险信息,仍然无法预测和量化相关风险。通过蒙特卡罗模拟,我们可以生成所有可能的风险情景,并利用这些情景评估风险的影响,从而构建更好的风险缓解策略。
蒙特卡罗模拟是一种计算数学方法,允许用户创建一系列可能的结果情景,包括极端情形,并为每个结果分配相应的概率。这些可能的结果也被绘制在期望的分布线中,这可能更接近真实结果。可能的结果范围可用于风险分析,用于构建模型并得出推论。分析会重复进行,每次使用概率函数中不同的一组随机值来测试模型,从而构建一个稳健的模型。概率分布是描述风险分析中变量不确定性的更现实的方式。蒙特卡罗模拟使用布朗运动动力学的概念。现在我们将演示如何使用蒙特卡罗模拟构建一个具有给定分布的样本,并尝试估算 VaR。
假设我们要使用正态分布生成2000
个月的股票回报样本,正态分布的均值为mean.20
,标准差为sigma.25
,时间间隔为deltat
= .08333
(每月)。这可以通过以下代码实现:
> Sample_Size<-2000
> set.seed(2345)
> Z<-rnorm(Sample_Size)
> mean<-.20
> sigma<-.25
> deltat<-.08333
> returns<-mean*deltat+sigma*Z*sqrt(deltat)
> hist(returns, breaks = 50)
它生成以下显示的直方图:
图 7.10:MCM 生成的回报直方图
现在让我们尝试计算新构建样本的均值和标准差,可以通过执行以下代码来获得:
> Mean_new<-mean(returns)*12
> Mean_new
> std_new<-sd(returns)*(12)^(.5)
> std_new
它生成的均值为0.1821
,标准差为0.2439
,与用于构建样本的均值和标准差接近。因此,显然,新的样本符合正态分布。
上述系列的风险值可以使用以下代码计算:
VaR(returns, p = 0.95, method="historical")
它生成以下输出:
VaR -0.09665
对冲
对冲基本上是通过在市场上采取一定的头寸来降低风险。这是一种旨在通过使用看涨/看跌期权或期货空头卖出减少投资风险的策略。对冲的理念是通过降低可能的损失风险来减少投资组合的波动性。对冲尤其能保护小型企业免受灾难性或极端风险的影响,从而在困境中锁定成本。税法对从事对冲的企业也有优惠。对于进行对冲的公司来说,它就像一种保险,他们可以更独立地做出财务决策,而不必过多考虑风险。
现在,让我们考虑一些对冲场景:
-
汇率风险:也称为汇率波动风险,因一种货币相对于另一种货币的价格波动而发生。跨国经营的投资者或公司面临汇率风险,这可能导致盈亏。通过对冲,可以减少这种风险,从而避免因价格波动导致的损失。
-
空头对冲:假设一个农民计划种植 200,000 蒲式耳的玉米,并将在接下来的 2 个月内收获,随后交割。农民凭借经验知道,种植和收获的成本是每蒲式耳$5.30。交割月的玉米期货交易价格为每蒲式耳$5.70。农民希望锁定自己的销售价格,因此他通过卖出一些交割月期货进行空头对冲。每个玉米期货合约包括 5,000 蒲式耳,他需要卖出 40 份期货合约。为了保护交割月份的作物,他发现玉米价格下跌,现货价格为每蒲式耳$5.20,而交割月期货合约的价格也降至$5.40 每蒲式耳。
在现货市场出售玉米为他带来了$5.20*200,000 = $1,040,000。
总成本为$1,060,000,因此他可能会遭受$20,000的损失。
但是,由于他已经对冲了种植月份出售的部分玉米期货价值 = $5.70*200,000=$1,140,000。
交割月份买入的玉米期货价值 = $5.40*200,000=1,080,000。
所以期货市场的收益是1,140,000-1,080,000=$60,000。
所以整体利润是$60,000-$20,000=$40,000。
类似地,根据需要,投资者可以进行长期对冲。
对冲中最常用的工具是远期合约、期货合约和期权合约。
巴塞尔法规
巴塞尔银行监管委员会的主要目标是加深对关键监管问题的理解,以制定全球健康的银行监管。其主要目标是制定监管框架,以改善银行体系。目前,巴塞尔 III 已经制定,以弥补在 2007-2008 年金融危机中暴露的金融监管不足。巴塞尔 III 是一个全球自愿性的银行资本充足性、压力测试和市场流动性风险的监管框架。其目标是通过减少银行杠杆并增加银行流动性来加强银行的资本要求。实施巴塞尔 III 的目的是使银行业更加稳健,以便能够应对由金融和经济压力引发的冲击,改善风险管理和治理,并加强银行的透明度和信息披露。
R 社区开发了一个名为 SACCR 的库,旨在遵守巴塞尔 III 的监管规定。这个库包含了许多基于巴塞尔 III 标准化规范的方法,已实现所有出现在监管框架中的示例。例如,它根据巴塞尔规范计算违约敞口。
它使用函数 CalcEAD(RC,PFE)
来计算违约敞口。
在这里,RC
是替代成本,PFE
是预测的未来敞口。
因此,如果 RC
组件为 50
,PFE
为 400
,那么执行以下代码会计算违约敞口:
> CalcEAD(50,400)
它会生成输出 630
。
同样,这个库中还有其他基于巴塞尔 III 实现的函数。
信用风险
信用风险是与投资相关的风险,指借款人无法按时向贷款人偿还款项。这可能是由于借款人财务状况不佳引起的,且对贷款人构成风险。风险表现为贷款人因未能收回款项而遭受损失,从而导致现金流中断和催收成本增加。损失可能是完全的,也可能是部分的。贷款人可能面临多种场景下的损失,以下是其中的一些情形:
-
客户未支付抵押贷款、信用卡、信用额度或其他类型的贷款
-
商业/消费者未支付到期的交易发票
-
企业未支付员工到期的应得工资
-
商业/政府债券发行人未按期支付到期的利息或本金
-
保险公司未履行其到期的保单义务
-
银行未按时返还存款人的资金
它是一种通过理解银行资本和贷款损失准备金的充分性来减轻损失的做法。为了减少信用风险,贷款方需要建立机制,对潜在借款人进行信用检查。通常,银行使用两种指标来量化信用风险——预期损失和经济资本。预期损失是可能损失的价值与该损失发生的概率的乘积。经济资本是用来覆盖意外损失所需的资本额。在计算预期损失(EL)和经济资本(EC)的过程中,有三个风险参数至关重要:违约概率(PD)、违约损失率(LGD)和违约时的敞口(EAD)。违约概率的计算更为重要,因此我们将重点讨论它。
为了构建 PD 模型,我们将使用 R 中提供的德国信用数据子集。执行以下代码即可获得用于分析的数据:
> data(GermanCredit)
> LRData<-GermanCredit[,1:10]
在开始建模之前,我们需要了解数据,可以通过执行以下代码来实现:
> str(LRData)
它为我们提供了列类型及其包含的值,如下所示:
图 7.11:数据集的列描述
在这个例子中,我们的目标变量是Class
。Class = Good
表示非违约者,Class = bad
表示违约者。现在,为了了解所有数值变量的分布,我们可以计算与数值属性相关的所有基本统计数据。可以通过执行以下代码来实现:
> summary(LRData)
前述代码生成的输出样本如下所示:
图 7.12 数值变量的基本统计
现在,让我们通过执行以下代码准备数据进行建模:
> set.seed(100)
> library(caTools)
> res = sample.split(LRData$Class, 0.6)
> Train_data = subset(LRData, res == TRUE)
> Test_data=subset(LRData,res==FALSE)
前述代码生成了用于建模的Train
和Test
数据。
选择Train
和Test
数据的比例相当主观。现在我们可以进行基本统计,填补缺失值或异常值,并进行探索性分析(如信息价值分析和相关矩阵),以了解自变量与因变量之间的关系。
现在,让我们尝试在Train
数据上拟合模型,可以通过执行以下代码来实现:
> lgfit = glm(Class ~. , data=Train_data, family="binomial")
> summary(lgfit)
它生成如这里所示的模型摘要:
图 7.13:逻辑回归的输出摘要
如我们在摘要中看到的,通过 P 值可以识别模型中的显著和非显著属性。考虑到属性的显著性和多重共线性,我们可以迭代模型,找到最佳模型。在我们的案例中,让我们只使用显著属性重新运行模型。
这可以通过执行以下代码来实现:
> lgfit = glm(Class ~Duration+InstallmentRatePercentage+Age , data=Train_data, family="binomial")
> summary(lgfit)
它生成如下所示的摘要输出:
图 7.14:仅包含显著属性的逻辑回归输出摘要
输出摘要显示,模型中考虑的所有属性都是显著的。
逻辑回归中有许多统计数据可以用来检查模型的准确性,在这种情况下,我们将显示 ROC 曲线和混淆矩阵来进行准确性检查。
我们可以通过 KS 统计量计算分类的阈值,但在这里让我们假设阈值为 0.5
,并通过执行以下代码尝试为我们的 Train
样本打分:
> Train_data$predicted.risk = predict(lgfit, newdata=Train_data, type="response")
> table(Train_data$Class, as.numeric(Train_data$predicted.risk >= 0.5))
它生成了如下所示的混淆矩阵:
图 7.15:逻辑回归的混淆矩阵
现在,让我们通过执行以下代码来计算 auc
:
> library(ROCR)
> pred = prediction(Train_data$predicted.risk, Train_data$Class)
> as.numeric(performance(pred, "auc")@y.values)
它给出了 auc
的值,如下所示:
0.67925265
现在,让我们通过执行以下代码绘制 ROC 曲线:
> predict_Train = predict(lgfit, type="response")
> ROCpred = prediction(predict_Train, Train_data$Class)
> ROCperf = performance(ROCpred, "tpr", "fpr")
> plot(ROCperf)
它绘制了如图所示的 ROC 曲线:
图 7.16:ROC 曲线
我们可以使用在 Train_data
上创建的相同模型,并对 Test_data
进行评分,检查准确度指标是否在相同范围内,从而验证模型。
欺诈检测
识别欺诈交易是风险管理中最重要的组成部分之一。R 语言有许多函数和包可以用来发现欺诈交易,包括逻辑回归、决策树、随机森林等二分类技术。我们将再次使用 R 库中的德国信用数据子集。在本节中,我们将使用随机森林进行欺诈检测。与逻辑回归一样,我们可以进行基本的探索性分析以了解属性。这里我们不会进行基础的探索性分析,而是使用标记数据来训练模型,使用随机森林,然后尝试在验证数据上进行欺诈预测。
所以,分析所用的数据集可以通过执行以下代码来获取:
>data(GermanCredit)
>FraudData<-GermanCredit[,1:10]
> head(FraudData)
它生成了一些示例数据行:
图 7.17:用于欺诈分析的示例数据
类别将是数据集中的目标变量,类别为 "bad" 表示客户已实施欺诈。现在,让我们通过执行以下代码将前面的数据划分为 train
和 test
样本:
> len<-dim(FraudData)[1]
> train<- sample(1:len , 0.8*len)
> TrainData<-FraudData[train,]
> TestData<-FraudData[-train,]
它生成了 Train
和 Test
数据。
现在,让我们尝试使用随机森林技术在训练样本上构建分类模型。可以通过执行以下代码来完成:
> fraud_model <- randomForest(Class~.,data=TrainData,ntree=50,proximity=TRUE)
> print(fraud_model)
它生成以下输出:
图 7.18:逻辑回归随机森林的输出摘要
可以通过执行以下代码绘制树的误差:
> plot(fraud_model)
这是生成的输出:
图 7.19:随机森林树的误差图
可以通过执行以下代码来给出属性的相对重要性:
> importance(fraud_model)
它生成以下输出:
图 7.20:随机森林的变量比较摘要
现在,让我们通过执行以下代码对测试数据进行分类:
> TestPred<-predict(fraud_model,newdata=TestData)
> table(TestPred, TestData$Class)
它生成以下分类表:
图 7.21:随机森林的分类总结
负债管理
负债管理是金融机构保持适当流动性储备和分配资产的过程,目的是平衡未偿付负债(如存款、定期存单等),通过同时满足负债并增长净资产来获得最佳表现。银行面临着多种风险——资产管理风险、利率风险和汇率风险——通过负债管理,银行和金融机构试图最小化这些风险。
在本章中,我们讨论了多个主题,如市场风险、投资组合风险、信用风险、欺诈检测、投资组合的多样化、投资组合优化以及投资组合再平衡,这些内容可以帮助金融领域处理负债管理问题。
问题
-
什么是市场风险?R 如何帮助衡量市场风险?请举例说明。
-
衡量投资组合相关风险的方法有哪些?
-
衡量 VaR 的最常见方法有哪些?请构建一个投资组合并使用所有方法计算 VaR。
-
如何在 R 中计算 ES/CVAR?
-
使用蒙特卡洛方法构造一个样本,采用正态分布和对数正态分布,并计算它们的历史 VaR。
-
如何在 R 中为一个投资组合找到组件和边际 VaR?
-
什么是信用评分?你如何在 R 中执行信用评分?请构造一个例子并构建一个评分示例并进行验证。
-
如何识别欺诈行为?你如何在 R 中执行这些方法?
总结
在本章中,我们介绍了金融机构面临的各种风险类型,如市场风险、投资组合风险、VaR、蒙特卡洛模拟、对冲、巴塞尔协议、信用风险和欺诈检测。我们还讨论了如何利用 R 的优势来衡量不同类型的风险。在本章中,我们展示了如何使用 R 衡量市场、投资组合和信用风险,并展示了如何使用随机森林分类技术进行欺诈检测。
在下一章中,我们将介绍在交易算法和参数估计中使用的各种优化技术。将涵盖的优化技术包括动态再平衡、前向步进测试、网格测试和遗传算法。
第八章:优化
优化是一种从所有可行解中选择最佳解的方法。因此,优化的第一部分是根据给定的约束来制定问题,并应用高级分析方法以获得最佳解,从而帮助做出更好的决策。
优化模型在量化和计算金融中起着关键作用,它通过更高效、准确地解决复杂问题来推动这些领域的发展。与资产配置、风险管理、期权定价、波动率估计、投资组合优化和指数基金构建等相关的问题,可以通过使用优化技术,如非线性优化模型、二次规划公式和整数规划模型来解决。现在有许多商业和开源软件可用于解决这些问题,而 R 是其中一个被广泛偏好的选择,因为它是开源且高效的。
在本章中,我们将讨论一些优化技术以及如何使用 R 来解决这些问题。
本章涵盖以下主题:
-
动态再平衡
-
向前测试
-
网格测试
-
遗传算法
动态再平衡
动态再平衡是保持投资组合接近目标配置的一种过程,利用投资组合的自然现金流入和流出进行调整。再平衡涉及定期买卖投资组合中的资产,以保持原始期望的资产配置水平,并重新调整投资组合中各资产的比重。
让我们考虑一个例子。在一个投资组合中,目标资产配置为 40%的股票和 60%的债券。如果债券在这一期间表现良好,债券在投资组合中的比重可能会增加到 70%。然后,投资者将决定卖出一些债券,买入一些股票,以便将投资组合恢复到原始目标配置:40%的股票和 60%的债券。
现在,让我们看看如何在 R 中进行投资组合的再平衡。
周期性再平衡
让我们考虑来自 R 的数据:
>library(PerformanceAnalytics)
>data(edhec)
> data<-edhec["1999", 3:5]
> colnames(data) = c("DS","EM","EMN")
> data
它给出了以下数据集:
图 8.1:用于再平衡分析的数据集
现在让我们假设,在1998-12-31
,由上述工具组成的投资组合的权重如下所示:
> wts <- xts(matrix(c(.3,.3,.4),nrow=1,ncol=3), as.Date("1998-12-31"))
> colnames(wts)<-colnames(data)
> wts
它给出了 1998 年年底的权重如下:
图 8.2:初始权重分配
现在,如果我们想按月平衡权重,可以通过执行以下代码来实现:
> Return.portfolio(data,weights =wts, rebalance_on="months",verbose=TRUE)
这里,data
是输入数据;weights
是投资组合各个组件的定义权重;rebalance_on = True
表示按月加权调整投资组合回报;verbose = True
返回额外信息。
当我们执行前面的代码时,它生成一个输出列表,其中包括每个区间调整后的投资组合回报、每个资产的月度贡献、每个区间后资产的前后权重以及每个区间的前后投资组合价值。这样,它就提供了一个完整的图景,展示了在给定时间跨度内如何进行再平衡。
月度再平衡后的投资组合回报如下所示:
图 8.3:不同时间段的投资组合回报
每个资产的月度贡献如下所示:
图 8.4:每个资产的月度贡献
期初权重如下所示:
图 8.5:每个周期开始时的权重汇总
期末权重如下所示:
图 8.6:每个周期结束时的权重汇总
期初投资组合的价值如下所示:
图 8.7:每个周期开始时的投资组合价值
期末投资组合的价值如下所示:
图 8.8:每个周期结束时的投资组合价值
前向走测试
前向走测试在量化金融中用于识别交易策略中使用的最佳参数。该交易策略在一个特定时间窗口内的样本数据子集上进行优化。其余未使用的数据被保留用于测试目的。在一小段未使用的数据窗口上进行测试并记录结果。然后,训练窗口向前移动以包括测试窗口,并反复执行此过程,直到测试窗口不可用为止。
前向优化是一种金融方法,用于确定交易策略中最佳的参数。该交易策略使用样本数据在某个时间窗口内进行优化。剩余的数据保留用于样本外测试。在样本数据后面的一小部分保留数据上进行测试,记录结果。样本时间窗口向前移动,覆盖样本外测试的周期,并重复此过程。最终,所有记录的结果将用于评估交易策略。
网格测试
让我们考虑一个典型的分类问题。假设你有一个数据集,并将其划分为训练(T)数据集和验证(V)数据集。在这里,你试图解决一个优化问题,假设为 P,在这个问题中,目的是减少训练误差以及正则化项,其中优化问题是模型参数 m、训练样本 T 和一些超参数 和
的函数。通过求解给定的
和
,你可以得到参数 m 的值。现在可以将估算的参数应用于验证样本,以获得验证误差函数,并对其进行优化,以获得
和
的集合,从而最小化误差函数。但是,这个优化问题会非常昂贵,因为对于每一组
和
,你都需要优化目标函数,而该函数可能不是凸的、凹的或平滑的。
因此,我们对子集进行选择,选择了 和
,对于每一对选定的
和
,我们解决优化问题。这看起来像是空间中的一个网格,所以我们称之为网格搜索。因此,网格搜索主要用于调整模型。
让我们考虑使用随机森林技术的分类示例。现在,首先通过执行以下代码构建一个基准模型:
>library(randomForest)
>library(mlbench)
>library(caret)
>data(Shuttle)
>Analysis_Data<-head(Shuttle,10000)
>X <- Analysis_Data[,1:9]
>Y<- Analysis_Data[,10]
>control <- trainControl(method="repeatedcv", number=5, repeats=3)
>seed <- 4
>metric <- "Accuracy"
>set.seed(seed)
>Count_var <- sqrt(ncol(X))
>tunegrid <- expand.grid(.mtry=Count_var)
>rf_baseline <- train(Class~., data=Analysis_Data, method="rf", metric=metric, tuneGrid=tunegrid, trControl=control)
>print(rf_baseline)
它生成了随机森林模型的汇总输出:
图 8.9:随机森林的汇总输出
网格搜索意味着你已经给定了一组在参数值上彼此不同的模型,这些模型位于网格上。训练每个模型,并通过交叉验证评估它们,以选择最佳模型。
现在,让我们尝试应用网格搜索方法并检查准确性。可以通过执行以下代码来完成:
> control <- trainControl(method="repeatedcv", number=5, repeats=3, search="grid")
> set.seed(seed)
> tunegrid <- expand.grid(.mtry=c(1:8))
> rf_gridsearch_method <- train(Class~., data=Analysis_Data, method="rf", metric=metric, tuneGrid=tunegrid, trControl=control)
> print(rf_gridsearch_method)
它给出了以下具有更好估计的输出:
图 8.10:随机森林的网格搜索输出
现在,让我们绘制不同随机森林模型在不同属性集下的准确度,可以通过执行以下代码来完成:
> plot(rf_gridsearch_method)
这给出了在 R 中使用网格搜索调整后的随机森林参数:
图 8.11:不同随机森林模型的准确度图
它通过准确度比较了模型池。
遗传算法
遗传算法(GA)是一种基于搜索的优化技术,其基本原理来源于遗传学和自然选择理论。它用于解决研究和机器学习领域中的优化问题,这些问题是通过其他方法解决时非常困难且耗时的。
优化是寻找一个比所有其他备选方案更好的解决方案的过程。它将所有可能的解决方案作为搜索空间,然后找到最适合问题的解决方案。
在遗传算法中,可能的候选解构成种群,它们通过重组和变异产生新的后代,并且这个过程会在多个代中重复进行。每个可能的候选解都会根据目标函数赋予一个适应度值。适应度较高的候选解会优先进行重组和变异,以产生更适应问题的候选解。
遗传算法(GA)相关的一些最重要的术语如下:
-
种群:它是现有问题所有可能候选解的一个子集
-
染色体:染色体是给定问题的一个解
-
基因:基因是染色体中的一个元素位置
例如,假设投资组合中包含以下股票,并且按照此处提到的相同比例进行投资,那么如果按照提到的回报率进行投资,去年将获得以下回报。然后我们需要通过限制总权重为 1 来最大化投资组合的表现:
股票 | 回报 | 权重 |
---|---|---|
Stock1 |
10 |
.1 |
Stock2 |
11 |
.2 |
Stock3 |
15 |
.1 |
Stock4 |
20 |
.2 |
Stock5 |
12 |
.2 |
Stock6 |
13 |
.3 |
让我们尝试在 R 中使用遗传算法来解决这个问题。
首先定义输入数据需求,可以通过执行以下代码来完成:
>install.packages("genalg")
>library(genalg)
>library(ggplot2)
>InputDataset <- data.frame(Stocks = c("Stock1", "Stock2", "Stock3", "Stock4", "Stock5", "Stock6"), returns = c(10, 11, 15, 20, 12, 13), weight = c(.1, .2, .1, .2, .2, .3))
>WTlimit <- 1
>InputDataset
这将得到以下输出:
图 8.12:遗传算法的输入数据集
现在让我们设置如下代码所示的评估函数:
>evaluationFunc <- function(x) {
current_solution_returns <- x %*% InputDataset$returns
current_solution_weight <- x %*% InputDataset$weight
if (current_solution_weight > WTlimit)
return(0) else return(-current_solution_returns)
}
然后,我们来设计模型并执行它。可以通过执行以下代码来完成:
> GAmodel <- rbga.bin(size = 6, popSize = 100, iters = 50, mutationChance = 0.01,
+ elitism = T, evalFunc = evaluationFunc)
> cat(summary(GAmodel))
这里:
-
size
是染色体中基因的数量 -
popsize
是种群大小 -
iters
是迭代次数 -
mutationChance
是染色体变异的概率 -
elitism
是保留到下一代的染色体数量;默认情况下为 20% -
evalFunc
是用户提供的用于评估给定染色体的评价函数
执行后,将得到以下输出:
图 8.13:遗传算法模型的总结输出
它表示保留所有股票,除了 Stock2
,以获得最佳投资组合。
让我们考虑另一个遗传算法的例子。在这里,我们将尝试通过遗传算法和传统的 OLS 方法估计系数。
首先,我们使用以下代码考虑一个数据集:
>library(GA)
> data(economics)
> Data_Analysis<-data.frame(economics[,2:4])
>head(Data_Analysis)
这将得到以下数据集:
图 8.14:通过遗传算法进行参数估计的输入示例
现在,让我们尝试通过遗传算法估计 pce
,它由 pop
和 psavert
来表示。现在我们来创建一个评估线性回归的函数,代码如下所示:
>OLS_GA <- function(Data_Analysis, a0, a1, a2){
attach(Data_Analysis, warn.conflicts=F)
Y_hat <- a0 + a1*pop + a2*psavert
SSE = t(pce-Y_hat) %*% (pce-Y_hat)
detach(Data_Analysis)
return(SSE)
}
然后,让我们尝试通过遗传算法(GA)来估计系数,这可以通过执行以下代码来实现:
>ga.OLS_GA <- ga(type='real-valued', min=c(-100,-100,-100),
max=c(100, 100, 100), popSize=500, maxiter=500, names=c('intercept', 'pop', 'psavert'),
keepBest=T, fitness = function(a) -OLS(Data_Analysis, a[1],a[2], a[3]))
> summary(ga.OLS_GA)
这将给出以下输出:
图 8.15:遗传算法(GA)提供的总结输出参数估计
问题
-
优化在量化金融中的重要性是什么?
-
动态再平衡优化方法是什么?请举例说明如何在 R 中执行它。
-
如何使用网格搜索来微调分类模型?请提供一个 R 语言的示例。
-
如何在 R 中使用遗传算法优化交易算法?
-
如何在 R 中使用遗传算法估计模型系数?请提供一个示例。
摘要
在本章中,我们讨论了交易算法和参数估计中使用的各种优化技术。所涉及的优化技术包括动态再平衡、前向测试、网格测试和遗传算法。
在下一章中,将讨论使用foptions
、termstrc
、CreditMetrics
、credule
、GUIDE
和fExoticOptions
来定价期权、债券、信用利差、信用违约掉期、利率衍生品以及不同类型的外汇期权。
第九章:衍生品定价
算法交易和金融工程是金融领域计算密集度最高的两个部分。参与这些领域的人不仅是金融、数学和统计学的专家,而且还精通计算密集型软件。在前几章中,我们学习了算法交易。在本章中,我们将研究 R 语言中不同类型的衍生品定价技术,因为衍生品定价是金融工程中最为关键的部分。
衍生品价格取决于其标的资产的价值。我们将从一些基本的期权定价模型开始,随后讨论其他资产类别:
-
期权定价
-
隐含波动率
-
债券定价
-
信用利差
-
信用违约掉期
-
利率衍生品
-
异国期权
期权定价
二项模型适用于连续过程,而 Cox-Ross-Rubinstein 模型适用于离散过程。期权价格取决于股票价格、行使价格、利率、波动率和到期时间。我们将使用fOption
包来实现 Black-Scholes 模型。以下命令将安装并加载该包到工作区:
> install.packages("fOptions")
> library(fOptions)
Black-Scholes 模型
让我们考虑一个关于call
和put
期权的示例,使用 2015 年 6 月的假设数据,期权到期日为 2015 年 9 月,即到期时间为 3 个月。假设当前标的股票价格为 900 美元,行使价格为 950 美元,波动率为 22%,无风险利率为 2%。我们还需要设置持有成本(b
);在原始的 Black-Scholes 模型中(假设标的资产不支付股息),它等于无风险利率。
以下命令GBSOption()
使用所有其他参数计算call
期权价格。第一个参数是期权类型,即call
或put
,第二个是当前股票价格,第三个是行使价格,第四、第五、第六和第七个参数分别是到期时间、无风险利率、波动率和持有成本:
>model <- GBSOption(TypeFlag = "c", S = 900, X =950, Time = 1/4, r = 0.02,sigma = 0.22, b = 0.02)
通过在 R 控制台中输入model
,可以查看模型的输出,它包括标题(即 Black-Scholes 期权)、方法论、函数语法、所用参数,最后显示期权价格:
> model
Title:
Black Scholes Option Valuation
Call:
GBSOption(TypeFlag = "c", S = 900, X = 950, Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22)
Parameters:
Value:
TypeFlag c
S 900
X 950
Time 0.25
r 0.02
b 0.02
sigma 0.22
Option Price:
21.79275
Description:
Sat Dec 31 11:53:06 2016
如果你想找到期权价格,可以使用以下代码行:
> model@price
[1] 21.79275
如果你希望计算put
期权价格,可以将第一个参数改为p
。
以下命令将计算put
期权价格,并仅提取价格。由于我们使用了@price
,因此输出中不显示其他项:
> GBSOption(TypeFlag = "p", S = 900, X =950, Time = 1/4, r = 0.02, sigma = 0.22, b = 0.02)@price
[1] 67.05461
Cox-Ross-Rubinstein 模型
Cox-Ross-Rubinstein 模型假设标的资产价格遵循离散的二项过程。价格在每个周期内要么上涨,要么下跌。CRR 模型的重要特征是,向上波动的幅度与向下波动的幅度成反比,即u=1/d。如果价格先上涨再下跌,或者先下跌再上涨,两种情况的价格在两期后是相同的,如图 9.1所示。
以下两个命令使用二项模型计算call
和put
期权价格,并保持与连续案例中相同的其他所有参数,除了最后一个参数。最后一个参数是将时间划分为多少步来建模期权。我使用了n=3
,这意味着仅使用三个步骤来定价期权:
> CRRBinomialTreeOption(TypeFlag = "ce", S = 900, X = 950, Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22, n = 3)@price
[1] 20.33618
> CRRBinomialTreeOption(TypeFlag = "pe", S = 900, X = 950, Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22, n = 3)@price
[1] 65.59803
然而,尽管提供给 Black-Scholes 和二项模型的所有值都是相同的,但输出并不完全相同,对于call
和put
期权略有不同。如果我们对价格波动的路径或轨迹感兴趣,那么我们应该使用以下命令来计算call
期权价格的二项树,并将结果保存在名为model
的变量中。
第二个命令使用变量model
绘制二项树,这是通过BonimialTreePlot()
完成的;它还帮助将期权值放置到树中。第三和第四个是 x 轴和 y 轴标签,第五个是 x 轴的限制。
第三个命令用于设置图形标题:
>model<- BinomialTreeOption(TypeFlag = "ce", S = 900, X = 950,Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22, n = 3)
> BinomialTreePlot(model, dy = 1, xlab = "Time steps",ylab = "Options Value", xlim = c(0,4) ,ylim=c(-3,4))
> title(main = "Call Option Tree")
现在,如果你有兴趣计算put
期权的价值,那么你应该在BinomialTreeOption()
中使用第一个参数,也就是TypeFlag="pe"
。随着步数的增加,二项树结果将趋向于连续案例。可以通过使用for
循环并迭代 100 次来验证这一点。为此,我们可以定义一个带有一个参数的func
函数。
这是函数的定义:
> func <- function(n) {
pr <- CRRBinomialTreeOption(TypeFlag = "ce", S = 900, X = 950, Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22, n = n)@price
return(pr)}
在下图中,你可以看到期权价格,显示了每个节点处的价格:
图 9.1:每个节点处的看涨期权价值
最右侧的层有四个节点,价格分别是0、0、9.01和138.9(从下往上)。第三层有三个节点,价格分别是0、4.47和73.47,依此类推。
图 9.1表示三个时间步长的情况。然而,我们可以增加步数,随着步数的增加,期权价格开始收敛到公平价格。我们必须将func
函数放入从 1 到 100 的循环中。接下来,我们可以将其与从 1 到 100 的迭代和func
一起传递给sapply()
,这将产生一个包含 100 个期权价格的序列,步数不断增加。
以下代码展示了sapply()
的使用,图 9.2显示了 100 步长下的二项价格和 CRR 价格:
price <- sapply(1:100,func)
随着步数的增加,我们可以在下图中看到,CRR 价格趋向于 Black-Scholes 价格:
图 9.2:Black-Scholes 和 CRR 价格
希腊字母
希腊字母(Greeks)在期权定价中也非常重要。它有助于理解期权价格相对于不同因素(如标的价格、到期时间、无风险收益率和波动性)的变化。GBSGreeks()
函数可以计算希腊字母,其中第一个参数是我们感兴趣的希腊字母类型,第二个是期权类型,第三个是标的资产价格,第四个是行权价,第五个是到期时间,第六个是无风险利率,第七个和第八个分别是年化持有成本和年化波动率。使用以下代码,我们可以计算call
期权的delta
:
> GBSGreeks(Selection = "delta", TypeFlag = "c", S = 900, X = 950,Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22)
[1] 0.3478744
同样,你可以将第二个参数改为p
,这样你就可以得到put
期权的delta
。理解希腊字母非常重要,因为它展示了希腊字母如何随着其他市场参数的变化而变化。这有助于投资组合多样化和风险控制。Gamma 可以使用以下代码计算:
> GBSGreeks(Selection = 'gamma', TypeFlag = "c", S = 900, X = 950,Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22)
[1] 0.003733069
同样地,你可以计算call
和put
期权的 Vega、Rho 和 Theta,控制第一个和第二个参数。现在,假设你想计算一个跨式投资组合(straddle portfolio)的delta
,即一个包括相同标的资产、行权价和到期日的call
和put
期权组合,你应该分别计算call
和put
期权的delta
,然后将它们相加。
以下代码计算了call
和put
期权在价格范围从500
到1500
之间(步长为 1)的delta
:
>portfolio<- sapply(c('c', 'p'), function(otype)
sapply(500:1500, function(price) GBSGreeks(Selection = 'delta', TypeFlag = otype, S = price,X = 950, Time = 1/4, r = 0.02, b = 0.02, sigma = 0.22)))
该命令显示了call
和put
期权delta
的前几个值,分别列出。第一列是call
期权,第二列是put
期权的delta
:
> head(portfolio)
c p
[1,] 4.902164e-09 -1
[2,] 5.455563e-09 -1
[3,] 6.068198e-09 -1
[4,] 6.746050e-09 -1
[5,] 7.495664e-09 -1
我们使用plot()
函数绘制跨式投资组合的delta
,代码如下:
> plot(500:1500, rowSums(portfolio), type='l',xlab='underlying Price', ylab = 'Straddle Delta')
第一个参数是X轴,即标的资产价格;在我们的例子中,它的范围是从500
到1500
。第二个参数是Y轴,即call
和put delta
的总和,保持其他所有参数不变。下图展示了跨式投资组合的delta
,它介于-1 到 1 之间,呈 S 形曲线:
图 9.3:跨式投资组合的 Delta
隐含波动率
在期权交易中,我们计算了历史波动率和隐含波动率。历史波动率是过去一年内价格的偏差,而隐含波动率则是通过期权价格计算得出的,暗示了未来股票的波动性。隐含波动率在期权交易中至关重要,因为它提供了股票未来波动性的估算。欧式call
期权的隐含波动率可以通过EuropeanOptionImpliedVolatility()
来计算,如下所示的代码所示。第一个参数是期权类型,第二个是call
或put
价格,第三和第四个是标的资产的当前价格和期权的行权价,第五个是股息收益率,第六、第七和第八个参数分别是无风险收益率、到期时间(年)和波动率的初步猜测:
>iv <-EuropeanOptionImpliedVolatility("call", 11.10, 100, 100, 0.01, 0.03, 0.5,0.4)
> iv
[1] 0.3805953
同样,可以使用AmericanOptionImpliedVolatility()
计算美式期权的隐含波动率。
债券定价
债券是非常重要的金融工具,因为它们在特定时间按预定利率或当前市场利率提供现金流。债券帮助投资者创建良好分散的投资组合。必须精确计算债券价格、收益率和到期时间,以便更好地了解该工具。我们将使用termstrc
包来实现这一点。我们需要通过以下代码安装并加载到 R 工作空间中:
> install.packages('termstrc')
> library('termstrc')
我们将使用包中的数据govtbonds
,可以使用以下代码加载并查看该数据:
> data(govbonds)
> govbonds
This is a data set of coupon bonds for:
GERMANY AUSTRIA FRANCE ,
observed at 2008-01-30\.
变量govbonds
包含三个国家(德国、奥地利和法国)的债券数据。我们将使用德国的数据来计算债券价格,可以通过govbonds[[1]]
访问。以下两行代码生成现金流
和到期
矩阵:
> cashflow <- create_cashflows_matrix(govbonds[[1]])
> maturity <- create_maturities_matrix(govbonds[[1]])
接下来,我们将查看bond_prices()
的使用方法,该函数用于计算债券价格。beta
是另一个需要传递给bond_prices()
函数的参数。接下来,我们可以创建beta
变量并定义bond_prices()
。该函数的第一个参数是method
,它支持三种方法:
-
ns
表示 Nelson/Siegel 方法 -
dl
表示 Diebold/Li -
sv
表示 Svensson 方法
我们将选择ns
方法来处理以下代码:
> beta <- c(0.0323,-0.023,-0.0403,3.234)
> bp <- bond_prices(method="ns",beta,maturity,cashflow)
变量bp
包含现货利率、折现因子和债券价格。可以使用$
访问任何信息。例如,您可以使用以下代码获取债券价格:
> bp$bond_prices
债券收益率是投资者在债券投资中实现的回报,可以使用bond_yields()
计算。以下两行代码生成包含肮脏价格的现金流
和到期
矩阵:
> cashflow <- create_cashflows_matrix(govbonds[[1]],include_price=T)
> maturity <- create_maturities_matrix(govbonds[[1]],include_price=T)
以下代码以矩阵形式计算债券收益率和到期时间:
>by <- bond_yields(cashflow,maturity)
可以通过输入以下命令head()
来查看前述代码的输出,输出将有两列,第一列是到期年限,第二列是相应债券的债券收益率。债券名称作为输出的索引给出:
> head(by)
Maturity Yield
DE0001141414 0.04383562 0.03525805
DE0001137131 0.12054795 0.03424302
DE0001141422 0.19726027 0.03798065
DE0001137149 0.36986301 0.03773425
以下图表显示了不同到期时间的收益曲线或期限结构。收益率在非常短的到期期内波动,并随着到期时间的增加而持续上升:
图 9.4:收益曲线,即具有不同到期时间的期限结构
久期衡量了债券价格由其内部现金流
偿还所需的时间长度。对于投资者来说,这是一个非常重要的因素,因为久期较高的债券比久期较低的债券具有更高的风险和价格波动性。久期可以通过以下代码来计算:
> dur <- duration(cashflow,maturity, by[,"Yield"])
它返回所有债券的年限、修正久期以及投资组合中所有债券的权重。如果你想查看投资组合的组成,可以查看输出矩阵的第三列。第三列是你的投资权重,你会看到权重的总和等于1
:
> sum(dur[,3])
[1] 1
信用利差
信用风险是金融机构面临的主要问题之一。其主要原因是信用质量,信用利差值有助于根据信用质量理解信用风险。信用利差是机构交易中的一个重要概念,因为信用利差取决于债券的质量或评级。它是两个具有相似到期日但评级不同的债券之间的收益率差异。我们将使用CreditMetrics
包来实现这一点,可以通过以下两个命令安装并导入到 R 工作区:
> install.packages('CreditMetrics')
> library('CreditMetrics')
信用利差通过cm.cs()
计算,该函数只有两个参数。第一个参数是某个发行信用的机构或政府的一年期迁移矩阵,第二个参数是违约损失(LGD),即如果债务人违约时的最大损失。通常,AAA
评级的信用在顶部,视为安全,而D
评级在底部,视为违约。
以下代码是所有信用评级的向量:
> rc <- c("AAA", "AA", "A", "BBB", "BB", "B", "CCC", "D")
上述代码显示我们有八个评级。转移矩阵是从一个信用评级转换到另一个信用评级的概率。
这段代码生成了一个这样的概率矩阵,矩阵为八行八列,评级作为索引和列名:
> M <- matrix(c(90.81, 8.33, 0.68, 0.06, 0.08, 0.02, 0.01, 0.01,
+ 0.70, 90.65, 7.79, 0.64, 0.06, 0.13, 0.02, 0.01,
+ 0.09, 2.27, 91.05, 5.52, 0.74, 0.26, 0.01, 0.06,
+ 0.02, 0.33, 5.95, 85.93, 5.30, 1.17, 1.12, 0.18,
+ 0.03, 0.14, 0.67, 7.73, 80.53, 8.84, 1.00, 1.06,
+ 0.01, 0.11, 0.24, 0.43, 6.48, 83.46, 4.07, 5.20,
+ 0.21, 0, 0.22, 1.30, 2.38, 11.24, 64.86, 19.79,
+ 0, 0, 0, 0, 0, 0, 0, 100
+ )/100, 8, 8, dimnames = list(rc, rc), byrow = TRUE)
你可以通过在命令提示符中输入变量名,即M,
来查看该矩阵,如下所示:
> M
AAA AA A BBB BB B CCC D
AA 0.9081 0.0833 0.0068 0.0006 0.0008 0.0002 0.0001 0.0001
AA 0.0070 0.9065 0.0779 0.0064 0.0006 0.0013 0.0002 0.0001
A 0.0009 0.0227 0.9105 0.0552 0.0074 0.0026 0.0001 0.0006
BBB 0.0002 0.0033 0.0595 0.8593 0.0530 0.0117 0.0112 0.0018
BB 0.0003 0.0014 0.0067 0.0773 0.8053 0.0884 0.0100 0.0106
B 0.0001 0.0011 0.0024 0.0043 0.0648 0.8346 0.0407 0.0520
CCC 0.0021 0.0000 0.0022 0.0130 0.0238 0.1124 0.6486 0.1979
D 0.0000 0.0000 -0.0000 0.0000 0.0000 0.0000 0.0000 1.0000
你可以在上面的表格中看到,最后一列显示了所有评级债券的违约概率;这意味着AAA
、AA
和A
评级的债券违约的可能性非常小。然而,随着债券评级向CCC
恶化,违约概率增加。最后一行除了最后一项外,所有的值都是零,这意味着违约的债券没有提高评级的可能性。我们可以在以下代码中将违约损失(lgd
)参数设置为0.2
:
> lgd <- 0.2
由于我们已经设计了一个转移矩阵并设置了lgd
参数,现在我们可以使用cm.cs()
函数来生成信用利差。
以下代码实现此功能,并分别计算所有评级的信用利差:
> cm.cs(M, lgd)
AAA AA A BBB BB B CCC
0.0000200 0.0000200 0.000120 0.000360 0.002122 0.010454 0.040384
前述结果表明,信用利差对于信用评级较好的债券较小,随着我们向左移动或违约概率增加,信用利差增大。最右侧的评级CCC
的利差最高为 4%,因为根据上述矩阵M
,CCC
评级债券的违约概率为 19.79%。该信用的风险价值可以使用cm.CVaR()
计算,该函数有九个参数。前两个参数与cm.cs()
相同,即迁移矩阵和违约损失(lgd
),其余参数包括违约暴露、公司数量、模拟随机数的数量、无风险利率、相关矩阵、置信水平和公司评级。
所有参数设置如下:
> ead <- c(4000000, 1000000, 10000000) # Exposure at default
> N <- 3 # Number of companies
> n <- 50000 # Number of simulated random numbers
>r <- 0.03 # Risk free interest rate
> rating <- c("BBB", "AA", "B") # Rating of selected companies
> firmnames <- c("firm 1", "firm 2", "firm 3")
> alpha <- 0.99 # Confidence interval
# Correlation matrix
> rho <- matrix(c( 1, 0.4, 0.6, 0.4, 1, 0.5, 0.6, 0.5, 1), 3, 3, dimnames = list(firmnames, firmnames), byrow = TRUE)
信用风险价值可以在以下代码行中使用,其中的参数如前所定义:
> cm.CVaR(M, lgd, ead, N, n, r, rho, alpha, rating)
1%
3993485
这个信用 VaR 数值是年化的,这意味着在未来一年内,包含三家公司在内的投资组合有 1%的概率会损失这个数值。该输出基于模拟价格。模拟价格在每次运行时都会变化,因此cm.CVaR()
的输出每次执行前述代码时可能也会有所不同。ca.gain()
需要与cm.CVaR()
相同的所有参数,除了 alpha 外,用于计算模拟的利润和亏损,可以像下面的代码一样使用:
> pnl <- cm.gain(M, lgd, ead, N, n, r, rho, rating)
信用违约掉期
简而言之,信用违约掉期(CDS)用于将参考实体(公司或主权)的信用风险从一方转移到另一方。在标准的 CDS 合同中,一方从另一方购买信用保护,以覆盖资产面值因信用事件导致的损失。信用事件是一个法律定义的事件,通常包括破产、未能支付和重组。保护期持续至某个指定的到期日。为了支付这种保护,保护买方定期向保护卖方支付一系列费用,称为保费支付。这些保费支付的大小是根据报价的违约掉期差价计算的,该差价是基于保护的面值支付的。这些支付会一直进行,直到发生信用事件或到期,以先发生者为准。CDS 衍生品的发行方必须在出售之前定价。我们将使用credule
包来完成这项工作。
这两行代码用于安装该包并将其加载到 R 工作空间中:
> install.packages('credule')
> library('credule')
priceCDS()
根据收益率曲线和信用曲线计算不同到期日的多个 CDS 的利差。这里,我们将定义用于定价 CDS 的参数。收益率曲线向量在下方定义,并表示每条收益率曲线的期限(以年为单位):
>yct = c(1,2,3,4,5,7)
这里,向量定义为收益率曲线的折现率,它将成为定价 CDS 函数中的第二个参数:
> ycr = c(0.0050,0.0070,0.0080,0.0100, 0.0120,0.0150)
以下两行代码定义了信用曲线的期限(以年为单位)和该期限的生存概率:
>cct = c(1,3,5,7)
>ccsp = c(0.99,0.98,0.95,0.92)
接下来是 CDS 的到期时间(以年为单位),我们将对其进行定价:
>tenors = c(1,3,5,7)
这里定义了另一个参数,即违约情况下的回收率:
>r = 0.40
我们已成功定义了必需的参数。下一行将执行定价多个期限 CDS 的任务:
> priceCDS(yct,ycr,cct,ccsp,tenors,r)
tenor spread
1 0.006032713
3 0.004057761
5 0.006101041
7 0.007038156
前述函数返回一个数据框,其中第一列是期限,第二列是给定期限的信用违约掉期(CDS)的利差。如果你想使用引导法来了解信用违约掉期的概率分布,可以使用bootstrapCDS()
,它需要收益率曲线的期限、收益率曲线的利率、CDS 期限、CDS 利差和回收率。我们将定义cdsSpread
参数,因为其他所有参数已在上面定义,我们将在bootstrapCDS()
中使用该参数:
> cdsSpreads = c(0.0050,0.0070,0.0090,0.0110)
以下命令给出不同到期日的信用违约掉期利差的引导信用曲线:
> bootstrapCDS(yct,ycr,cct,ccsp,r)
tenor survprob hazrate
1 1 0.187640397 1.673228e+00
2 3 0.007953847 1.580436e+00
3 5 0.007953847 2.081668e-15
4 7 0.007953847 4.579670e-15
前述命令的输出会生成三列数据。第一列是期限,第二列是每个期限的存活概率,最后一列是每个期限的风险率,即泊松分布的强度。
利率衍生品
我们将使用GUIDE
包来计算利率衍生品的价格:
> install.packages("GUIDE")
> library(GUIDE)
以下命令会打开一个弹出窗口,要求输入所有参数。一旦你在弹出窗口中提供所需的参数,它将生成利率衍生品价格:
>irswapvalue()
弹出窗口中需要提供的参数如下:
-
名义金额:以小数形式输入
-
固定利率:以小数形式输入,例如,0.05 代表 5%
-
最后现货汇率:以小数形式输入,例如,0.05 代表 5%
-
首次支付月份:输入 3 表示 3 个月
-
现货汇率:用逗号分隔输入,例如,0.054, 0.056, 0.058
-
现货汇率的频率:从连续/季度/半年度/年度中选择
-
结算频率:从季度/半年度/年度中选择
另类期权
亚洲期权、障碍期权、二元期权和回溯期权是一些另类期权。另类期权与普通的美式或欧式期权,或称香草期权,非常不同,它们具有一些新特性,使得定价变得相当复杂。美式或欧式的看涨
或看跌
期权被认为是非另类或香草期权。另类期权复杂的原因有几个特性:
-
它的结算方式取决于期权到期时是否处于平价。可以通过现金或股票期权进行结算。
-
还可能涉及外汇汇率。
-
到期时的支付不仅取决于标的股票价格,还取决于合同期间多个时间点的价格。
我们将使用fExoticOptions
包来定价各种类型的另类期权。为此,以下两行代码用于安装并加载该包到 R 工作区:
> install.packages('fExoticOptions')
> library(fExoticOptions)
亚洲期权可以通过不同的方式定价。以下是一些定价亚洲期权的函数。GeometricAverageRateOption()
根据几何平均收益率期权定价亚洲期权。
这里是一个使用几何平均收益率期权定价亚洲看涨
期权的代码。第一个参数是看涨
("c"
)期权,其他参数是股票价格(110
),执行价格(120
),到期时间(0.5
,即 6 个月),无风险利率(3%),持有成本(5%)和波动率(10%):
> price <- GeometricAverageRateOption("c", 110, 120, 0.5, 0.03, 0.05, 0.1)
> price
Title:
Geometric Average Rate Option
Call:
GeometricAverageRateOption(TypeFlag = "c", S = 110, X = 120,
Time = 0.5, r = 0.03, b = 0.05, sigma = 0.1)
Parameters:
Value:
TypeFlag c
S 110
X 120
Time 0.5
r 0.03
b 0.05
sigma 0.1
Option Price:
0.06067219
Description:
Sun Jan 15 01:00:34 2017
你也可以通过将c
替换为p
来计算亚洲看跌
期权的价格。TurnbullWakemanAsianApproxOption()
也可以计算亚洲期权的价格,并基于 Turnbull 和 Wakeman 的近似方法。它除了使用之前函数中的参数外,还使用两个新参数(SA
),时间和tau
。SA
和时间是必须使用的。以下是代码:
> TurnbullWakemanAsianApproxOption(TypeFlag = "p", S = 100, SA = 102, X = 120, Time = 0.50, time = 0.25, tau = 0.0, r = 0.03, b = 0.05, sigma = 0.1)@price
[1] 18.54625
然而,另一种计算亚洲期权价格的技术是使用LevyAsianApproxOption()
,该方法使用 Levy 的近似方法来定价亚洲期权,并且与TurnbullWakemanAsianApproxOption()
中使用的所有参数相同,除了tau
:
> LevyAsianApproxOption(TypeFlag = "p", S = 100, SA = 102, X = 120, Time = 0.50, time = 0.25, r = 0.03, b = 0.05, sigma = 0.1)@price
[1] 18.54657
还有许多函数用于计算障碍期权的价格。标准障碍期权、双障碍期权和回顾型障碍期权是其中的一些。单障碍期权有四种类型。
类型标志cdi
表示向下敲入看涨期权,cui
表示向上敲入看涨期权,cdo
表示向下敲出看涨期权,cuo
表示向上敲出看涨期权。
以下命令使用StandardBarrierOption()
计算标准障碍期权的价格,该函数在GeometricAverageRateOption()
中需要两个额外的参数,这些额外的参数是障碍值和回扣值。
第一个参数是期权类型,在以下命令中是一个向下敲出的看涨期权:
> StandardBarrierOption(TypeFlag = "cdo", S = 100, X = 90, H = 95, K = 3, Time = 0.5, r = 0.08, b = 0.04, sigma = 0.25)@price
[1] 9.024584
在前面的命令中,H = 95
是障碍值,K=3
是回扣。双障碍期权需要下界(L
)和上界(U
),以及下界和上界的曲率,即delta1
和delta2
。它还具有四种类型的期权。
类型标志co
表示向上敲出向下敲出的看涨期权,ci
表示向上敲入向下敲入的看涨期权,po
表示向上敲出向下敲出的看跌期权,pi
表示向上敲入向下敲入的看跌期权。我们将在以下命令中使用co
类型期权,并结合其他参数计算双障碍期权的价格:
> DoubleBarrierOption(TypeFlag = "co", S = 100, X = 100, L = 50,U = 150, Time = 0.25, r = 0.10, b = 0.10, sigma = 0.15,delta1 = -0.1, delta2 = 0.1)@price
[1] 4.351415
如果是敲入障碍期权,期权会生效;如果是敲出障碍期权,期权将变得毫无价值。回顾型障碍期权是另一种类型的障碍期权,它也有四种类型,cuo
表示向上敲出看涨期权,cui
表示向上敲入看涨期权,pdo
表示向下敲出看跌期权,pdi
表示向下敲入看跌期权。期权的障碍监控期从时间零开始,到到期前的任意日期结束。如果在此期间障碍未被触发,则固定执行价的回顾型期权将在障碍期限结束时被触发。
以下代码行计算了回溯障碍,即cuo
期权价格:
> LookBarrierOption(TypeFlag = "cuo", S = 100, X = 100, H = 130,time1 = 0.25, Time2 = 1, r = 0.1, b = 0.1, sigma = 0.15)@price
[1] 17.05969
障碍期权是另一种另类期权,也被称为数字期权。与标准的欧式期权不同,二元期权的支付并不依赖于它的盈利多少,而是取决于是否到期时符合条件。期权的支付在期权创建时已确定,并基于到期日标的资产的价格。跳空期权、现金或无价值期权以及两资产现金或无价值期权是二元期权中的几种。跳空期权的支付受普通期权的常规因素影响,但也受到gap
行权价的影响,该gap
可能为正值或负值。以下命令使用GapOption()
计算跳空期权价格,函数中的X1
和X2
参数是创建跳空期权的两个行权价,其他参数与常规期权一样:
> GapOption(TypeFlag = "c", S = 50, X1 = 50, X2 = 57, Time = 0.5,r = 0.09, b = 0.09, sigma = 0.20)
CashOrNothingOption()
计算现金或无价值期权的价格,对于此期权,如果资产价格在行权价上方(看涨)或下方(看跌),到期时支付预定金额。该金额与路径无关。这些期权不需要支付行权价。行权价决定期权是否支付收益。现金或无价值的call
(看涨)或put
(看跌)期权的价值是固定现金支付的现值,乘以终端价格高于(低于)行权价的概率。以下代码行中,S
为股票价格,X
为行权价,K
为到期时若期权到期获利则支付的现金金额:
> CashOrNothingOption(TypeFlag = "p", S = 100, X = 80, K = 10,Time = 9/12, r = 0.06, b = 0, sigma = 0.35)
两资产现金或无价值期权是构建更复杂的另类期权的基础,共有四种类型的两资产现金或无价值期权。前两种情况是:两资产现金或无价值看涨期权,如果到期时第一资产的价格高于(低于)第一资产的行权价,且第二资产的价格也高于(低于)第二资产的行权价,则支付固定现金金额。另一种情况是在以下条件下出现:两资产现金或无价值看跌期权,如果到期时第一资产的价格低于(高于)第一资产的行权价,且第二资产的价格高于(低于)第二资产的行权价,则支付固定现金金额。
以下函数用于计算两资产现金或无价值期权价格:
> TwoAssetCashOrNothingOption(TypeFlag = "c", S1 = 100, S2 = 100,X1 = 110, X2 = 90, K = 10, Time = 0.5, r = 0.10, b1 = 0.05,b2 = 0.06, sigma1 = 0.20, sigma2 = 0.25, rho = 0.5)@price
[1] 2.49875
这里:
-
TypeFlag
: 期权类型,"c"
代表看涨期权,"p"
代表看跌期权 -
S1
,S2
:两资产的股票价格 -
X1
,X2
:两个行权价 -
K
:到期时支付的现金金额 -
Time
:到期时间 -
r
:无风险利率 -
b1
,b2
:两资产的持仓成本 -
sigma1
,sigma2
:两资产的波动率 -
rho
:两资产之间的相关性
关于所有其他类型的奇异期权还有更多的细节,想了解更多关于奇异期权的内容,你应该查阅名为fExoticOptions
的 PDF 文件,你可以从网上下载该文件。
问题
-
用于期权、债券和奇异期权定价的包有哪些?
-
你会使用哪些函数来计算使用 Black-Scholes 和 Cox-Ross-Rubinstein 方法的期权价格?
-
CRR 定价如何收敛到二项式定价,且你会使用哪个命令来计算希腊字母?
-
写一个命令来计算隐含波动率。
-
如何准备一个
cashflow
和maturity
矩阵,使其符合债券定价函数的要求,并且应使用哪个函数进行债券定价? -
处理信用利差和信用违约掉期的函数有哪些?
-
亚洲期权类型、障碍期权类型和数字期权类型有哪些?
-
你会使用哪些函数来处理第 7 问中的选项?
概述
本章仅在 R 中实现衍生品定价,并使用了多个包,如foptions
、termstrc
、CreditMetrics
、credule
、GUIDE
和fExoticOptions
,这些包用于定价期权、债券、信用利差、信用违约掉期以及利率衍生品,同时也涉及了不同类型的奇异期权。衍生品定价在衍生品交易中至关重要,因此学习它非常重要。
本章还涵盖了 Black-Scholes 和 Cox-Ross-Rubinstein 方法,以及期权的希腊字母和隐含波动率。它还解释了债券价格和收益率曲线,并使用了相关函数来解释信用利差、信用违约掉期和利率衍生品的定价。最后,本章介绍了各种类型的奇异期权,并使用了相关包中提供的数据和实现的函数。