跳至主要内容

R 中的算法交易教程

在本 R 教程中,您将进行网页抓取、调用金融 API,并使用 htmlwidget 制作交互式时间序列图表,以执行一个简单的算法交易策略
· 15分钟 阅读

使用 AI 探索

在本文中,我将展示如何使用 R 收集 loyal3 上列出的股票、从 Yahoo 获取历史数据,并执行一个简单的算法交易策略。过程中,您将学到一些网页抓取技巧、调用金融 API 的函数,以及使用 htmlwidget 制作交互式时间序列图表。

在本文中,交易算法被定义为一组触发买入或卖出事件的规则,而非预测模型或时间序列预测。这是最简单的一类交易算法。如果您希望用 R 更深入研究金融,我建议您学习 DataCamp 的R 中量化交易策略建模课程。

背景

2015 年,我开始在 loyal3 做一点投资。他们的服务很特别,也是开启投资之旅的好地方。loyal3 并不向投资者收取交易费用,而是向在其平台上市的公司收费。其理念是,喜欢某家公司服务的人也会购买其股票,并因此成为强有力的品牌拥护者。更吸引人的是,您可以购买零股。因此,您只需$10就能买入那只价格为$800的亚马逊股票的一小部分,并在每月末手头有点闲钱时再买入 $10 的份额。诚然,这其中存在一些摩擦成本,例如您需要在指定时间窗口交易,而且整个投资组合被限制在大约 70 只股票,但 loyal3 提供了一种有趣且低成本的方式来探索股票投资。您只需$10就能“真金白银”上阵!

需要说明的是,我也有常规的退休与投资账户,但我喜欢 loyal3 应用简洁的界面以及零费用。我发现自己查看 loyal3 这个“乐趣组合”的频率比共同基金更高,仅仅因为它操作简单、也很有趣,可以直接看到自己亲自挑选的股票表现。

algorithmic trading R
www.loyal3.com 上可选的股票

设置工作区

首先,将所需的库加载到环境中。如今我几乎总是用 rvest 做网页抓取。还有其他可用包(包括 RSelenium),但我喜欢 rvest 的易用性。

第二个包 pbapply 是可选的,因为它只是为apply 家族函数添加了进度条。由于您可能需要抓取数百个网页,进度条有助于估算时间。

接下来,TTR 是我刚开始探索的一个包。该库用于构建“技术交易规则”。尽管本文将介绍一个简单的交易算法,TTR 包还能进行更复杂的计算,值得学习。

dygraphs 库是一个快速、开源的 JavaScript 图表库的封装。它是 htmlwidgets 之一,使 R 的图表更具动态性,并成为 html 文件的一部分,而非静态图片。最后,lubridate 用于便捷地处理日期。

library(rvest)
library(pbapply)
library(TTR)
library(dygraphs)
library(lubridate)

数据收集

所有 loyal3 的股票都列在同一页面。要在构建交易算法前查询各股票的日价格,您需要先收集所有可用的股票代码。首先将 stock.list 声明为一个 URL 字符串。然后使用 read_html(),让您的 R 会话创建一个网络会话,并将该页面的全部 html 信息收集为 XML 节点集。页面的 CSS 中有一个叫“.company-name”的 ID。调用 html_nodes() 时将其作为参数,只选择与该节点相关的 XML 数据。最后,使用 html_text() 收集公司名称的实际文本值。

stock.list<-'https://www.loyal3.com/stocks'
stocks<-read_html(stock.list)
stocks.names<-html_nodes(stocks,'.company-name')
stocks.names<-html_text(stocks.names)

要查看 loyal3 上可选的股票,您可以在控制台打印 stocks.names 对象。它会以文本向量形式返回公司名称。

stocks.names 

为研究股价,您需要先获得股票代码。在 loyal3 网站上,您可以点击公司卡片,打开包含股票代码与其他公司信息的页面。

对 stocks 使用 html_nodes(),您可以抓取所有标记为“a.”的节点。在 HTML 中,<a> 标签定义超链接,用于在页面之间跳转。超链接标签中的“href”指向确切的 URL 地址。因此,如果传入“href”,html_attr() 会提取页面上所有链接的 URL。

经过一些手动检查,我发现页面上的第 54 到第 123 个链接代表我需要用来抓取股票代码信息的公司页面。最后一行使用 paste0() 将基础 URL 字符串 ’http://www.loyal3.com` 与特定的公司页面(如“/WALMART”)拼接。例如,http://www.loyal3.com/WALMART:

loyal.links<-html_nodes(stocks, "a")
loyal.links<-html_attr(loyal.links, "href")
stock.links<-paste0('http://www.loyal3.com',loyal.links[54:123])

在每个公司页面上都有简介、最近的收盘价以及股票代码。所有公司页面结构一致,因此可以使用自定义函数 get.ticker() 来提取股票代码。

在公司的网页中有一个名为“ticker-price”的表格。该函数会导航至公司页面,定位相应表格,并通过 html_text() 提取文本。最后,配合正则表达式 ^([[:alpha:]]*).* 与 \\1 使用 sub(),仅保留字母字符。这样会移除所有特殊字符(如 $)以及数字字符(如收盘价)。当函数读取 70 个页面时,只会收集股票代码。

 get.ticker<-function(url){
  x<-read_html(url)
  x<-html_node(x,'.ticker-price')
  x<-html_text(x)
  x<-sub("^([[:alpha:]]*).*", "\\1", x)
  return(x)
  } 
loyal3 stock page for Alibaba
loyal3 上的阿里巴巴股票页面,包含股票代码 BABA 的表格位于加粗文本下方

有了自定义函数后,使用 pblapply() 将其应用到每个包含公司页面的 stock.links。结果对象 stock.tickers 是一个由各公司股票代码组成的列表,每个元素对应一家公司的代码。

stock.tickers<-pblapply(stock.links,get.ticker)

将元素列表转换为扁平对象的一种方式是使用 do.call()。这里,您对每个列表元素应用 rbind,将其按行绑定为单个向量。最后,您将股票代码与公司名称信息构建为数据框。

stock.ticks<-do.call(rbind,stock.tickers)
stock.ticks<-data.frame(symbol=stock.ticks,name=stocks.names)

为保证分析一致,您可能希望限制每只股票抓取的历史信息量。Sys.Data() 函数会将日期对象存储为年、月、日。将年份作为整数使用,是从 start.date 对象减去特定时间长度的一种方法。

start.date<-Sys.Date()
end.date<-Sys.Date()-years(3)

要获取 Yahoo 财经数据,需要将日期对象转换为不带连字符的简单字符对象。对 start.date 和 end.date 使用全局替换函数 gsub() 会同时改变其类型并移除连字符。在 gsub() 中,首先传入要搜索的字符模式,然后是替换字符。本例中替换模式是引号内的空字符串。最后一个参数是要应用 gsub() 的对象。

start.date<-gsub('-','', start.date)
end.date<-gsub('-','', end.date)

TTR() 包中的 getYahooData() 函数接受股票代码以及起止日期。函数返回一个包含时间序列信息的数据框。每一行是一个日期,列包含该股票的“Open”、“High”、“Low”与“Closing”价格等信息。由于您要查询多家公司,可以使用 lapply() 或 pblapply()。传入公司代码向量、函数 getYahooData(),以及日期信息。在每次对一个股票代码调用 getYahooData() 时,会重复使用这些 date 参数。

 stocks.ts<-pblapply(stock.ticks$symbol,getYahooData,end.date, start.date)

为便于选择返回的列表 stocks.ts,您可以为列表元素添加名称。对 stocks.ts 使用 names,将名称设为原始的 $symbol 向量。

names(stocks.ts)<-stock.ticks$symbol

处理大型列表时,我喜欢先检查结果对象,确认结果符合预期。现在元素已有名称,您可以直接引用它们。以下示例中,您查看 AMC Entertainment Holdings(AMC)的前 6 行。对列表使用 head() 并引用 $AMC,将返回该股票时间序列的一部分:

head(stocks.ts$AMC)

查看股票数据

我在听财经新闻时,评论员常常会提到图表。尽管有高频交易和他人进行的主动管理,许多小型投资者仍会借助图表获取洞见。时间序列对象可以用 plot 快速展示。传入带命名元素的列表,例如 $AMC,再指定要显示的列,这里是 $Close。

plot(stocks.ts$AMZN$Close)
stocks plot

上面的图表是静态的,并不够有趣。

我们来用一个 JavaScript 库制作可探索的图表。在下面的代码片段中,您会看到“%>%”或管道操作符。管道是编写简洁代码的好方法。它会把对象传递给下一个函数,而无需像前文那样重复写对象名。

在这个示例中,您针对 Twitter 股票 $TWTR 创建一个 dygraph,并指定要绘制的列 $Close。在 dygraph 中,main 参数用于添加标题,标题内容写在引号内。使用“%>%”会将整个对象传递给下一个函数 dyRangeSelector()。您可以用 c() 指定默认的日期范围,包含起止日期字符串。生成的 HTML 对象是 Twitter 股票的动态时间序列,底部带有日期滑块。

请记住,如需更换显示的股票,只需修改 stocks.ts 列表中的股票代码以及图表标题。

dygraph(stocks.ts$TWTR$Close, main = "TWTR Stock Price") %>%
  dyRangeSelector(dateWindow = c("2013-12-18", "2016-12-30"))

basic dygraph for Twitter’s stock
这是 Twitter 股票的基础 dygraph 图

一个简单的交易策略:趋势跟随

高频交易者和对冲基金使用复杂的模型与基于规则的方法来执行交易。如果您想了解更多,建议访问 www.quantopian.com 获取更高级的方法。对于更简单的方法,可以从 www.Investopedia.com 上的相关页面入手。

在下面的代码中,您将可视化一个简单的动量交易策略。基本思路是,计算某只股票价格的 200 日与 50 日移动平均。若在任意一天,50 日均线高于 200 日均线,您就买入或持有;若 200 日均线高于 50 日均线,您就卖出。这一策略称为趋势跟随。两条以时间为基础的均线间的正负关系,代表了该股票的动量。

TTR 包提供了 SMA() 用于计算简单移动平均。下面的代码片段展示了 Twitter 的 200 日与 50 日移动平均的前 6 个值。SMA() 的使用方式是传入某只股票的时间序列数据,并指定列(如 Close)。这是一条关于 TWTR 股票收盘价的单列向量。第二个参数是整数,表示用于计算移动平均的观测点数。如果不使用 head(),SMA() 会返回所有数值。

head(SMA(stocks.ts$TWTR$Close, 200))
head(SMA(stocks.ts$TWTR$Close, 50))

既然已经详细查看了移动平均函数,接下来需要将其应用到 70 只股票上。stocks.ts 是包含 70 个数据框的列表,每个数据框包含某只股票的数据。每个数据框的第 4 列存放我们要用于计算移动平均的收盘价。

自定义函数 mov.avgs() 接受单只股票的数据框来计算移动平均。第一行通过索引 [,4] 选择收盘价,创建 stock.close。接下来,函数用 ifelse 检查数据框的行数。具体来说,如果数据框的 nrow 小于 (2*260),函数就创建一个包含移动平均列但值为“NA”的数据框。

我选择这个数值是因为一年大约有 250 个交易日,这样可以检查时间序列是否达到 2 年或更长。loyal3 有时会参与 IPO,如果股票刚刚上市,就没有足够的数据来计算 200 日移动平均。而如果 nrow 大于 2*260,函数就会创建一个包含原始数据以及 200 日与 50 日移动平均(作为新列)的数据框。使用 colnames 声明列名。函数的最后部分使用 complete.cases 检查 200 日移动平均列是否有值。没有值的行会在最终结果中被删除。

mov.avgs<-function(stock.df){
  stock.close<-stock.df[,4]
  ifelse((nrow(stock.df)<(2*260)),
         x<-data.frame(stock.df, 'NA', 'NA'),
         x<-data.frame(stock.df, SMA(stock.close, 200), SMA(stock.close, 50)))
  colnames(x)<-c(names(stock.df), 'sma_200','sma_50')
  x<-x[complete.cases(x$sma_200),]
  return(x)
  }

有了这个 mov.avgs() 函数,您可以使用 pblapply() 为 70 个数据框逐一添加移动平均计算。

stocks.ts<-pblapply(stocks.ts, mov.avgs)

使用以下代码通过 dygraph 可视化某只股票的移动平均。再次强调,这段代码使用“%>%”运算符来传递对象。dygraph() 接受 stocks.ts$FOX 数据框。具体来说,使用 c('sma_200','sma_50') 通过列名对数据框进行索引。该对象会在接下来的两行传入 dySeries()。您可以通过列名引用,dySeries() 会分别为“sma_50”和“sma_200”绘制曲线。该对象再次被传递给 dyRangeSelector() 来调整选择器高度。最后,我添加了阴影区域,用以标示出您希望买入或持有的时间段,以及应该卖出或回避(视持仓而定)的时间段。

dygraph(stocks.ts$FOX[,c('sma_200','sma_50')],main = 'FOX Moving Averages') %>%
  dySeries('sma_50', label = 'sma 50') %>%
  dySeries('sma_200', label = 'sma 200') %>%
  dyRangeSelector(height = 30) %>%
  dyShading(from = '2016-4-28', to = '2016-7-27', color = '#CCEBD6') %>%
  dyShading(from = '2016-7-28', to = '2016-12-30', color = '#FFE6E6')

下面是最终的交互式时间序列结果。

fox moving averages
FOX 的移动平均,并用阴影区分买入/持有与卖出时段

结语

作为一名初露锋芒的算法交易者,您无需绘制全部 70 只股票。相反,您应当每天运行代码,并以编程方式识别符合基于规则的方法“当 50 日均线高于 200 日均线时买入”的股票。回看前面的图表,绿色区域是您会买入 FOX 股票的时段;红色区域代表您应当卖出并不再入场的时段。

由于图表具有交互性,您可以使用滑块调整可视范围。基于这个简单的算法交易方法,现在或许是买入 FOX 的好时机!2016 年 12 月 30 日这一天,50 日移动平均比 200 日移动平均高出 $0.01!

fox equity
FOX 股票的放大区间

当然,请记住所有投资都可能亏损。想要进一步学习金融与算法交易,请查看 DataCamp 的课程这里。

欢迎阅读我们的R 基础编程技能教程。

主题