金融中的人工智能-全-

金融中的人工智能(全)

原文:annas-archive.org/md5/d3dc59f235378edd30befe32733e0af3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

对于每一种可以想象的投资策略,alpha 最终会趋向零吗?更根本地说,由于许多聪明人和更聪明的计算机,金融市场真的会变得完美吗?我们可以坐下来放松,假设所有资产都被正确定价了吗?

罗伯特·席勒(2015 年)

人工智能(AI)在 2010 年代崛起,被认为是 2020 年代主导技术。在技术创新、算法突破、大数据的可用性和计算能力的不断增加的推动下,许多行业正在经历由 AI 驱动的根本性变革。

虽然媒体和公众关注大多集中在游戏和自动驾驶汽车等领域的突破,但 AI 也已成为金融业的主要技术力量。然而,可以肯定的是,与诸如网络搜索或社交媒体等行业相比,金融 AI 仍处于初级阶段。

本书旨在涵盖与金融 AI 相关的若干重要方面。金融 AI 已经是一个广阔的主题,而一本书只能集中讨论选定的方面。因此,本书首先涵盖了基础知识(见第一部分和第二部分)。然后,它深入探讨了通过使用 AI,特别是神经网络来发现金融市场中的统计效率低下(见第三部分)。这种通过成功预测未来市场走势的 AI 算法体现的效率低下是通过算法交易来利用经济效率低下的前提(见第四部分)。系统地利用统计和经济效率低下,可能与金融中的一个已确立的理论和基石相矛盾:有效市场假说(EMH)。设计成功的交易机器人可以被认为是金融界的圣杯,而 AI 可能引领其道路。本书最后讨论了 AI 对金融行业的影响以及金融奇点的可能性(见第五部分)。此外,还有一个技术附录,展示如何基于纯 Python 代码从头构建神经网络,并提供了它们应用的额外示例(见第六部分)。

将 AI 应用于金融问题的问题与将 AI 应用于其他领域的问题并没有太大不同。在 2010 年代,AI 在玩街机游戏(如 1980 年代由 Atari 发布的游戏,参见 Mnih 等人 2013 年)以及象棋或围棋等棋类游戏中的一些重大突破,主要得益于应用强化学习(RL)。从在游戏环境中应用 RL 中学到的经验教训,今天被应用于诸如设计和建造自动驾驶车辆或改善医疗诊断等具有挑战性的问题。表格 P-1 比较了不同领域中 AI 和 RL 的应用情况。

表格 P-1. 不同领域中 AI 的比较

领域 代理 目标 方法 奖励 障碍 风险
街机游戏 AI 代理 (软件) 最大化游戏得分 虚拟游戏环境中的 RL 分数和得分 计划和延迟奖励
自动驾驶 自动驾驶汽车 (软件 + 汽车) 安全地从 A 地到 B 地驾驶 虚拟(游戏)环境中的 RL,现实世界测试驾驶 错误的惩罚 从虚拟世界过渡到现实世界 损坏财产,伤害人员
金融交易 交易机器人 (软件) 最大化长期绩效 虚拟交易环境中的 RL 金融回报 有效市场和竞争 金融损失

使用 AI 代理来玩街机游戏的美妙之处在于提供了一个完美的虚拟学习环境¹,并且没有任何风险。在自动驾驶车辆中,主要问题出现在从虚拟学习环境过渡到现实世界时——例如,像侠盗猎车手这样的电脑游戏——在这里,自动驾驶汽车在真实街道上导航,这些街道上有其他汽车和行人。这导致严重风险,比如汽车造成事故或伤害人员。

对于交易机器人来说,RL 也可以完全是虚拟的,即在模拟的金融市场环境中。由于交易机器人的故障而产生的主要风险包括金融损失以及在集合水平上由于交易机器人的群体行为可能导致的系统性风险。然而,总体来说,金融领域似乎是训练、测试和部署 AI 算法的理想场所。

鉴于该领域的快速发展,有兴趣且雄心勃勃的学生,只需准备好笔记本和互联网接入,就能成功地在金融交易环境中应用 AI。除了近年来硬件和软件的改进之外,主要归功于提供历史和实时金融数据,并允许通过程序化 API 执行金融交易的在线经纪商的兴起。

本书分为以下六个部分。

第一部分

第一部分讨论了人工智能的中心概念和算法,例如监督学习和神经网络(参见第一章)。它还讨论了超智能的概念,即 AI 代理具备人类水平智能甚至某些领域超人类水平智能的能力(参见第二章)。并非所有 AI 研究人员都认为在可预见的未来会出现超智能。然而,讨论这个想法为讨论整体 AI 以及特别是金融 AI 提供了有价值的框架。

第 II 部分

第二部分包括四章,讨论了传统的规范金融理论(参见第三章),以及该领域如何被数据驱动金融(参见第四章)和机器学习(ML)(参见第五章)所转变。数据驱动金融和机器学习共同促成了一种无模型、以 AI 为先的金融方法,如第六章所讨论。

第 III 部分

第三部分涉及通过应用深度学习、神经网络和强化学习发现金融市场中的统计效率低下问题。该部分涵盖了密集型神经网络(DNNs,参见第七章)、递归神经网络(RNNs,参见第八章)以及强化学习算法(RL,参见第九章),后者通常依赖于 DNNs 来表示和近似 AI 代理的最优策略。

第 IV 部分

第四部分讨论了如何通过算法交易利用统计效率低下的问题。主题包括矢量化回测(参见第十章)、基于事件的回测和风险管理(参见第十一章)以及执行和部署基于 AI 的算法交易策略(参见第十二章)。

第 V 部分

第五部分讨论了基于人工智能竞争在金融行业中引起的后果(参见第十三章)。它还讨论了金融奇点的可能性,即 AI 代理将统治我们知道的所有金融领域的时刻。在这个背景下的讨论侧重于作为交易机器人的人工金融智能,这些机器人能够始终产生高于任何人类或机构基准的交易利润(参见第十四章)。

第 VI 部分

附录包含了用于交互式神经网络训练的 Python 代码(见附录 A),基于纯 Python 代码从头开始实现的简单和浅层神经网络类(见附录 B),以及如何使用卷积神经网络(CNN)进行金融时间序列预测的示例(见附录 C)。

参考文献

前言中引用的论文和书籍:

  • Chollet, François. 2017. Deep Learning with Python. Shelter Island: Manning.

  • Hilpisch, Yves. 2018. Python for Finance: Mastering Data-Driven Finance. 2nd ed. Sebastopol: O’Reilly.

  • ⸻. 2020. Python for Algorithmic Trading: From Idea to Cloud Deployment. Sebastopol: O’Reilly.

  • Lee, Justina and Melissa Karsh. 2020. “Machine-Learning Hedge Fund Voleon Group Returns 7% in 2019.” Bloomberg, January 21, 2020. https://oreil.ly/TOQiv.

  • López de Prado, Marcos. 2018. Advances in Financial Machine Learning. Hoboken, NJ: John Wiley & Sons.

  • Mnih, Volodymyr et al. 2013. “Playing Atari with Deep Reinforcement Learning.” arXiv. December 19. https://oreil.ly/-pW-1.

  • Shiller, Robert. 2015. “The Mirage of the Financial Singularity.” Yale Insights. July 16. https://oreil.ly/VRkP3.

  • Silver, David et al. 2016. “Mastering the Game of Go with Deep Neural Networks and Tree Search.” Nature 529 (January): 484-489.

本书使用的约定

本书使用以下排版约定:

斜体

指示新术语、网址、电子邮件地址、文件名和文件扩展名。

常量宽度

用于程序清单,以及段落中引用程序元素(如变量或函数名称、数据库、数据类型、环境变量、语句和关键字)。

常量宽度粗体

显示用户应按字面输入的命令或其他文本。

常量宽度斜体

显示应由用户提供值或根据上下文确定值的文本。

提示

此元素表示提示或建议。

注意

此元素表示一般注释。

重要

此元素表示重要信息。

警告

此元素表示警告或注意事项。

使用代码示例

您可以在 Quant 平台上访问并执行伴随本书的代码,只需免费注册即可https://aiif.pqp.io

如果您有技术问题或者使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作。一般情况下,如果本书附带示例代码,则您可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则您无需联系我们获得许可。例如,编写一个使用本书中多个代码块的程序不需要许可。销售或分发 O’Reilly 书籍中的示例需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码合并到您产品的文档中需要许可。

我们感谢,但通常不要求署名。署名通常包括书名、作者、出版商和 ISBN。例如,本书可以归属为:“Artificial Intelligence in Finance by Yves Hilpisch (O’Reilly)。2021 年版权 Yves Hilpisch,978-1-492-05543-3。”

如果您觉得您对代码示例的使用超出了合理使用范围或上述许可,请随时联系我们 permissions@oreilly.com

O’Reilly Online Learning

注意

超过 40 年来,O’Reilly Media为企业提供技术和商业培训、知识和见解,帮助它们取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台为您提供即时访问直播培训课程、深入学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多家出版商的广泛文本和视频的访问权限。欲了解更多信息,请访问 http://oreilly.com

联系我们的方式

请就本书的评论和问题联系出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为本书建立了一个网页,其中列出了勘误表、示例和任何额外信息。您可以在 https://oreil.ly/ai-in-finance 上访问这个页面。

发送电子邮件至 bookquestions@oreilly.com 对本书进行评论或提出技术问题。

关于我们的书籍和课程的新闻和信息,请访问 http://oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

关注我们的 Twitter:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

我要感谢技术审阅人员—Margaret Maynard-Reid、Dr. Tim Nugent 和 Dr. Abdullah Karasan,在帮助我改进本书内容方面做得非常好。

Python 计算金融和算法交易证书课程的代表们也帮助改进了这本书。他们持续的反馈使我能够排除错误,并完善在线培训课程中使用的代码和笔记本,现在最终体现在这本书中。

同样的情况也适用于 Python Quants 团队和 AI Machine 团队的成员,特别是迈克尔·施韦德、拉马纳坦·拉马克里希纳穆尔蒂和普雷姆·杰巴塞兰,他们在许多方面支持我。他们是在撰写这本书时帮助我解决困难技术问题的人。

我还要感谢 O'Reilly Media 的整个团队,特别是米歇尔·史密斯、科尔宾·柯林斯、维多利亚·德罗斯和丹尼·埃尔凡鲍姆,在各种方式上帮助我完成这本书,并帮助我进行多方面的改进。

当然,所有剩下的错误都是我自己的责任。

此外,我还要感谢 Refinitiv 团队,特别是杰森·拉姆查达尼,他们提供持续的支持和对金融数据的访问。本书中使用的主要数据文件以某种方式从 Refinitiv 的数据 API 中获得,并提供给读者使用。

当然,今天所有利用人工智能和机器学习的人都受益于许多其他人的成就和贡献。因此,我们应该始终记住艾萨克·牛顿在 1675 年写道的话:“如果我看得更远,那是因为我站在巨人的肩膀上。” 在这个意义上,特别感谢所有为该领域做出贡献的研究人员和开源维护者。

最后,特别感谢我的家人,他们全年支持我从事业务和撰写书籍活动。特别感谢我的妻子桑德拉,她不知疲倦地照顾我们所有人,并为我们提供了一个我们非常喜爱的家和环境。我将这本书献给我可爱的妻子桑德拉和我美好的儿子亨利。

¹ 请参阅 Arcade Learning Environment

第一部分:机器智能

当今的算法交易程序相对简单,仅在有限程度上使用 AI。这肯定会改变。

Murray Shanahan(2015)

本部分讨论了人工智能(AI)的一般情况:人工指的是智能不是由生物机构展示,而是由机器展示,智能由 AI 研究者马克斯·泰格马克定义为“实现复杂目标的能力”。本部分介绍了 AI 领域的核心概念和算法,提供了最近主要突破的例子,并讨论了超智能的各个方面。它包括两个章节:

  • 第一章介绍了 AI 领域的一般概念、思想和定义。它还提供了几个 Python 示例,展示了不同算法如何在实践中应用。

  • 第二章讨论与人工通用智能(AGI)和超智能(SI)相关的概念和主题。这些类型的智能涉及到达到至少人类水平智能的 AI 代理,以及在某些领域具有超人类智能。

第一章:人工智能

这是计算机程序第一次在全尺寸围棋比赛中击败专业人类选手,这一壮举以前被认为至少还需十年时间。

David Silver 等(2016 年)

本章介绍了人工智能领域的一般概念、思想和定义,用于本书的目的。它还为不同类型的主要学习算法提供了实例。特别是,“算法” 从广泛的角度来看分类了数据类型、学习类型和通常在 AI 环境中遇到的问题。本章还介绍了无监督学习和强化学习的示例。“神经网络” 直接进入神经网络的世界,这不仅是本书后续章节的核心内容,而且被证明是当今 AI 最强大的算法之一。“数据的重要性” 讨论了在 AI 背景下数据量和多样性的重要性。

算法

本节介绍了与本书相关的人工智能领域的基本概念。它讨论了不同类型的数据、学习、问题和方法,这些可以归纳为通用术语AI。Alpaydin(2016 年)提供了一个非正式的介绍,以及许多本节中仅简要涉及的主题的概述和许多示例。

数据类型

数据通常具有两个主要组成部分:

特征

特征数据(或输入数据)是作为算法输入的数据。在金融背景下,例如可能是潜在债务人的收入和储蓄。

标签

标签数据(或输出数据)是作为相关输出给定的数据,例如通过监督学习算法学习的内容。在金融背景下,例如可能是潜在债务人的信用价值。

学习类型

主要有三种类型的学习算法:

监督学习(SL)

这些算法从给定的特征(输入)和标签(输出)数值样本数据集中学习。接下来的部分展示了这类算法的实例,如普通最小二乘回归(OLS)和神经网络。监督学习的目的是学习输入和输出值之间的关系。在金融领域,这类算法可能被训练用于预测潜在债务人是否信用良好。对于本书而言,这些是最重要的算法类型。

无监督学习(UL)

这些算法仅从给定的特征(输入)数值样本数据集中学习,通常目标是找出数据中的结构。它们被设计来学习输入数据集,例如通过一些引导参数。聚类算法属于这一类别。在金融背景下,这种算法可能会将股票聚类到某些组中。

强化学习(RL)

这些是通过试错学习的算法,通过在采取行动后获得奖励来更新最佳行动策略。它们根据所接收到的奖励和惩罚更新最佳行动策略。例如,此类算法用于需要持续采取行动并立即获得奖励的环境,例如在电脑游戏中。

因为后续章节详细介绍了监督学习,所以简要示例将说明无监督学习和强化学习。

无监督学习

简单地说,k-means 聚类算法n个观测值分成k个聚类。每个观测值属于其均值(中心)最近的聚类。以下 Python 代码生成了聚类的特征数据的样本数据。图 1-1 可视化了聚类的样本数据,还显示了这里使用的scikit-learn KMeans算法完美地识别了聚类。点的着色基于算法学习的内容。¹

In [1]: import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        np.set_printoptions(precision=4, suppress=True)

In [2]: from sklearn.cluster import KMeans
        from sklearn.datasets import make_blobs

In [3]: x, y = make_blobs(n_samples=100, centers=4,
                          random_state=500, cluster_std=1.25)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [4]: model = KMeans(n_clusters=4, random_state=0)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [5]: model.fit(x)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[5]: KMeans(n_clusters=4, random_state=0)

In [6]: y_ = model.predict(x)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [7]: y_  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[7]: array([3, 3, 1, 2, 1, 1, 3, 2, 1, 2, 2, 3, 2, 0, 0, 3, 2, 0, 2, 0, 0, 3,
               1, 2, 1, 1, 0, 0, 1, 3, 2, 1, 1, 0, 1, 3, 1, 3, 2, 2, 2, 1, 0, 0,
               3, 1, 2, 0, 2, 0, 3, 0, 1, 0, 1, 3, 1, 2, 0, 3, 1, 0, 3, 2, 3, 0,
               1, 1, 1, 2, 3, 1, 2, 0, 2, 3, 2, 0, 2, 2, 1, 3, 1, 3, 2, 2, 3, 2,
               0, 0, 0, 3, 3, 3, 3, 0, 3, 1, 0, 0], dtype=int32)

In [8]: plt.figure(figsize=(10, 6))
        plt.scatter(x[:, 0], x[:, 1], c=y_,  cmap='coolwarm');

1

创建一个带有聚类特征数据的样本数据集。

2

实例化一个KMeans模型对象,固定聚类数。

3

模型适配特征数据。

4

给定适配模型,生成预测结果。

5

预测结果是从 0 到 3 的数字,每个数字代表一个聚类。

aiif 0101

图 1-1. 聚类的无监督学习

一旦像KMeans这样的算法训练完成,它可以预测一个新的(尚未见过)特征值组合的聚类情况。假设这样的算法是在描述银行潜在和真实债务人的特征数据上进行训练的。它可以通过生成两个聚类来了解潜在债务人的信用状况。然后,新的潜在债务人可以被分类到某个聚类中:“有信用”还是“没有信用”。

强化学习

以下示例基于一个硬币抛掷游戏,该游戏使用一枚硬币,正面的概率为 80%,反面的概率为 20%。硬币抛掷游戏被大大偏向以强调学习的好处,与一个未经训练的基线算法相比。随机押注并平均分配在正反面的基线算法,在每轮 100 次押注游戏中平均获得约 50 的总奖励:

In [9]: ssp = [1, 1, 1, 1, 0]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [10]: asp = [1, 0]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [11]: def epoch():
             tr = 0
             for _ in range(100):
                 a = np.random.choice(asp)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 s = np.random.choice(ssp)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 if a == s:
                     tr += 1  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
             return tr

In [12]: rl = np.array([epoch() for _ in range(15)])  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         rl
Out[12]: array([53, 55, 50, 48, 46, 41, 51, 49, 50, 52, 46, 47, 43, 51, 52])

In [13]: rl.mean()  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
Out[13]: 48.93333333333333

1

状态空间(1 = 正面,0 = 反面)。

2

行动空间(1 = 押注正面,0 = 押注反面)。

3

从动作空间中随机选择一个动作。

4

从状态空间中随机选择一个状态。

5

如果赌注正确,则总奖励tr增加 1。

6

游戏进行多个时期;每个时期为 100 次赌注。

7

计算已播放的时期的平均总奖励。

强化学习试图从采取行动后观察到的内容中学习,通常基于奖励。为了简化问题,以下学习算法仅在每轮中跟踪观察到的状态,只要它们被附加到动作空间的list对象中。通过这种方式,算法学习了游戏中的偏差,尽管可能不完美。通过从更新后的动作空间中随机抽样,反映了偏差,因为自然地,赌注更容易更多地选择正面。随着时间的推移,平均而言,头部将被选择约 80%的时间。大约 65 的平均总奖励反映了与未知基线算法相比学习算法的改进:

In [14]: ssp = [1, 1, 1, 1, 0]

In [15]: def epoch():
             tr = 0
             asp = [0, 1]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             for _ in range(100):
                 a = np.random.choice(asp)
                 s = np.random.choice(ssp)
                 if a == s:
                     tr += 1
                 asp.append(s)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             return tr

In [16]: rl = np.array([epoch() for _ in range(15)])
         rl
Out[16]: array([64, 65, 77, 65, 54, 64, 71, 64, 57, 62, 69, 63, 61, 66, 75])

In [17]: rl.mean()
Out[17]: 65.13333333333334

1

在开始之前重置动作空间(over)

2

将观察到的状态添加到动作空间

任务类型

根据标签数据类型和手头问题的不同,学习的两种重要任务类型如下:

估计

估计(或近似,回归)是指标签数据为实值(连续)的情况;也就是说,它在技术上表示为浮点数。

分类

分类是指标签数据包含有限数量的类别或类别,通常由离散值(正整数)表示,技术上表示为整数。

以下部分提供了这两种任务类型的示例。

方法类型

在完成本节之前,可能需要进一步定义一些术语。本书遵循以下三个主要术语之间的常见区分:

人工智能(AI)

AI 包含了所有类型的学习(算法),如前所定义,以及一些其他类型(例如专家系统)。

机器学习(ML)

机器学习(ML)是根据算法和成功度量学习给定数据集的关系和其他信息的学科;例如,成功度量可能是均方误差(MSE),给定标签值和要估计的输出值与算法预测值。ML 是 AI 的一个子集。

深度学习(DL)

DL 包括所有基于神经网络的算法。术语深度通常仅在神经网络具有多个隐藏层时使用。DL 是机器学习的子集,因此也是 AI 的子集。

DL 已被证明在许多广泛的问题领域中非常有用。它适用于估计和分类任务,以及强化学习。在许多情况下,基于 DL 的方法表现优于其他算法,例如逻辑回归或基于核的方法,如支持向量机。² 这就是为什么本书主要侧重于 DL。所使用的 DL 方法包括密集神经网络(DNNs)、循环神经网络(RNNs)和卷积神经网络(CNNs)。更多细节将在后面的章节中出现,特别是在第三部分。

神经网络

前面的章节提供了人工智能算法的更广泛概述。本节展示了神经网络的适应性。一个简单的例子将说明神经网络与传统统计方法(例如普通最小二乘回归)相比的特点。该例子从数学开始,然后使用线性回归进行估计(或函数逼近),最后应用神经网络来完成估计。这里采用的方法是一种监督学习方法,任务是基于特征数据估计标签数据。本节还说明了神经网络在分类问题背景下的使用。

普通最小二乘回归

假设给出数学函数如下:

f : , y = 2 x 2 - 1 3 x 3

这样的函数将一个输入值x转换为一个输出值y。或者将一系列输入值x 1 , x 2 , ... , x N转换为一系列输出值y 1 , y 2 , ... , y N。以下 Python 代码将数学函数实现为 Python 函数,并创建一些输入和输出值。图 1-2 绘制了输出值与输入值的关系:

In [18]: def f(x):
             return 2 * x ** 2 - x ** 3 / 3  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [19]: x = np.linspace(-2, 4, 25)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         x  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[19]: array([-2.  , -1.75, -1.5 , -1.25, -1.  , -0.75, -0.5 , -0.25,  0.  ,
                 0.25,  0.5 ,  0.75,  1.  ,  1.25,  1.5 ,  1.75,  2.  ,  2.25,
                 2.5 ,  2.75,  3.  ,  3.25,  3.5 ,  3.75,  4.  ])

In [20]: y = f(x)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         y  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[20]: array([10.6667,  7.9115,  5.625 ,  3.776 ,  2.3333,  1.2656,  0.5417,
                 0.1302,  0.    ,  0.1198,  0.4583,  0.9844,  1.6667,  2.474 ,
                 3.375 ,  4.3385,  5.3333,  6.3281,  7.2917,  8.1927,  9.    ,
                 9.6823, 10.2083, 10.5469, 10.6667])

In [21]: plt.figure(figsize=(10, 6))
         plt.plot(x, y, 'ro');

1

数学函数作为 Python 函数

2

输入值

3

输出值

aiif 0102

图 1-2. 输出值与输入值

而在数学示例中,函数首先出现,然后是输入数据,最后是输出数据,在统计学习中,顺序不同。假设给出了前述输入值和输出值。它们代表样本(数据)。统计回归问题是找到一种尽可能好地近似输入值(也称为自变量)和输出值(也称为因变量)之间的功能关系的函数。

假设简单 OLS 线性回归。在这种情况下,假设输入和输出值之间的功能关系是线性的,问题是找到以下线性方程的最优参数 αβ

f ^ : , y ^ = α + β x

对于给定的输入值 x 1 , x 2 , ... , x N 和输出值 y 1 , y 2 , ... , y N,在这种情况下最优意味着它们最小化了真实输出值和近似输出值之间的均方误差(MSE):

min α,β 1 N n N y n -f ^(x n ) 2

对于简单线性回归情况,解 ( α , β ) 在闭合形式中是已知的,如下方程所示。变量上的条表示样本均值:

β = Cov(x,y) Var(x) α = y ¯ - β x ¯

以下 Python 代码计算了最优参数值,线性估计(近似)输出值,并在示例数据旁边绘制了线性回归线(参见 Figure 1-3)。在此处,线性回归方法在近似功能关系方面表现不佳。这一点由相对较高的 MSE 值确认:

In [22]: beta = np.cov(x, y, ddof=0)[0, 1] / np.var(x)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         beta  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[22]: 1.0541666666666667

In [23]: alpha = y.mean() - beta * x.mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         alpha  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[23]: 3.8625000000000003

In [24]: y_ = alpha + beta * x  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [25]: MSE = ((y - y_) ** 2).mean()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         MSE  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[25]: 10.721953125

In [26]: plt.figure(figsize=(10, 6))
         plt.plot(x, y, 'ro', label='sample data')
         plt.plot(x, y_, lw=3.0, label='linear regression')
         plt.legend();

1

计算最优参数 β

2

计算最优参数 α

3

计算估计输出值

4

计算近似值时的 MSE

aiif 0103

图 1-3. 样本数据和线性回归线

如何改善(降低)MSE 值,甚至可能达到 0,即“完美估计”?当然,OLS 回归不仅限于简单的线性关系。除了常数和线性项外,例如,可以轻松添加更高阶的单项式作为基础函数。为此,请参考图 1-4 中显示的回归结果和创建该图的以下代码。使用二次和三次单项式作为基础函数带来的改进是显而易见的,并且通过计算得到的 MSE 值也得到了数值确认。对于包括三次单项式在内的基础函数,估计是完美的,功能关系也完全恢复:

In [27]: plt.figure(figsize=(10, 6))
         plt.plot(x, y, 'ro', label='sample data')
         for deg in [1, 2, 3]:
             reg = np.polyfit(x, y, deg=deg)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             y_ = np.polyval(reg, x)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             MSE = ((y - y_) ** 2).mean()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             print(f'deg={deg} | MSE={MSE:.5f}')
             plt.plot(x, np.polyval(reg, x), label=f'deg={deg}')
         plt.legend();
         deg=1 | MSE=10.72195
         deg=2 | MSE=2.31258
         deg=3 | MSE=0.00000

In [28]: reg  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[28]: array([-0.3333,  2.    ,  0.    , -0.    ])

1

回归步骤

2

近似步骤

3

MSE 计算

4

最佳(“完美”)参数值

aiif 0104

图 1-4. 样本数据和 OLS 回归线

利用数学函数形式的知识进行近似,并相应地添加更多的基础函数到回归中,可以实现“完美逼近”。也就是说,OLS 回归分别恢复了原始函数的二次和三次部分的确切因素。

使用神经网络进行估计

然而,并非所有的关系都是这种类型。例如,神经网络 可以帮助。不详细讨论,神经网络可以近似广泛的功能关系。通常不需要了解关系形式的具体知识。

Scikit-learn

下面的 Python 代码使用了scikit-learnMLPRegressor类,它实现了用于估计的 DNN。有时也称为多层感知器(MLP)。³ 如图 1-5 和 MSE 所示,结果并非完美。但对于所用的简单配置来说,它们已经相当不错:

In [29]: from sklearn.neural_network import MLPRegressor

In [30]: model = MLPRegressor(hidden_layer_sizes=3 * [256],
                              learning_rate_init=0.03,
                              max_iter=5000)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [31]: model.fit(x.reshape(-1, 1), y)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[31]: MLPRegressor(hidden_layer_sizes=[256, 256, 256], learning_rate_init=0.03,
                      max_iter=5000)

In [32]: y_ = model.predict(x.reshape(-1, 1))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [33]: MSE = ((y - y_) ** 2).mean()
         MSE
Out[33]: 0.021662355744355866

In [34]: plt.figure(figsize=(10, 6))
         plt.plot(x, y, 'ro', label='sample data')
         plt.plot(x, y_, lw=3.0, label='dnn estimation')
         plt.legend();

1

实例化MLPRegressor对象

2

实现拟合或学习步骤

3

实现预测步骤

仅仅看一下图 1-4 和图 1-5 中的结果,人们可能会认为这些方法和方法实际上并没有太大的不同。然而,有一个值得强调的根本性差异。尽管 OLS 回归方法,如对简单线性回归明确显示的那样,是基于计算某些明确定义的量和参数的,但神经网络方法依赖于增量学习。这又意味着,一组参数,神经网络内的权重,首先是随机初始化的,然后根据神经网络输出与样本输出值之间的差异逐渐调整。这种方法让你可以增量地重新训练(更新)神经网络。

aiif 0105

图 1-5。样本数据和基于神经网络的估计

Keras

下一个例子使用了Keras深度学习包中的顺序模型⁴。该模型进行了 100 个时期的拟合或训练。该过程重复进行五轮。在每一轮之后,神经网络的近似值都会更新并绘制出来。图 1-6 显示了每一轮近似值的逐渐改善。这也反映在逐渐减少的 MSE 值中。最终结果并不完美,但考虑到模型的简单性,它还是相当不错的:

In [35]: import tensorflow as tf
         tf.random.set_seed(100)

In [36]: from keras.layers import Dense
         from keras.models import Sequential
         Using TensorFlow backend.

In [37]: model = Sequential()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.add(Dense(256, activation='relu', input_dim=1)) ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         model.add(Dense(1, activation='linear'))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         model.compile(loss='mse', optimizer='rmsprop')  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [38]: ((y - y_) ** 2).mean()
Out[38]: 0.021662355744355866

In [39]: plt.figure(figsize=(10, 6))
         plt.plot(x, y, 'ro', label='sample data')
         for _ in range(1, 6):
             model.fit(x, y, epochs=100, verbose=False)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
             y_ =  model.predict(x)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
             MSE = ((y - y_.flatten()) ** 2).mean()  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
             print(f'round={_} | MSE={MSE:.5f}')
             plt.plot(x, y_, '--', label=f'round={_}')  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
         plt.legend();
         round=1 | MSE=3.09714
         round=2 | MSE=0.75603
         round=3 | MSE=0.22814
         round=4 | MSE=0.11861
         round=5 | MSE=0.09029

1

实例化Sequential模型对象

2

添加具有修正线性单元(ReLU)激活的密集连接隐藏层⁵

3

添加具有线性激活的输出层

4

编译模型以供使用

5

对神经网络进行固定次数的训练

6

实现近似步骤

7

计算当前的 MSE

8

绘制当前的近似结果

粗略地说,可以说神经网络在估计方面几乎与 OLS 回归一样出色,后者提供了完美的结果。那么,为什么还要使用神经网络呢?一个更全面的答案可能需要在本书的后面给出,但一个稍微不同的例子可能会给出一些提示。

考虑一下之前的样本数据集,这是从一个明确定义的数学函数生成的,现在是一个随机样本数据集,其中特征和标签都是随机选择的。当然,这个例子是为了说明,并不允许深入解释。

aiif 0106

图 1-6。多轮训练后的样本数据和估计

以下代码生成随机样本数据集,并基于不同数量的单项式基函数创建 OLS 回归估计。图 1-7 可视化了结果。即使在示例中单项式数量最高的情况下,估计结果仍然不太理想。相应的均方误差(MSE)值相对较高:

In [40]: np.random.seed(0)
         x = np.linspace(-1, 1)
         y = np.random.random(len(x)) * 2 - 1

In [41]: plt.figure(figsize=(10, 6))
         plt.plot(x, y, 'ro', label='sample data')
         for deg in [1, 5, 9, 11, 13, 15]:
             reg = np.polyfit(x, y, deg=deg)
             y_ = np.polyval(reg, x)
             MSE = ((y - y_) ** 2).mean()
             print(f'deg={deg:2d} | MSE={MSE:.5f}')
             plt.plot(x, np.polyval(reg, x), label=f'deg={deg}')
         plt.legend();
         deg= 1 | MSE=0.28153
         deg= 5 | MSE=0.27331
         deg= 9 | MSE=0.25442
         deg=11 | MSE=0.23458
         deg=13 | MSE=0.22989
         deg=15 | MSE=0.21672

OLS 回归的结果并不令人意外。在这种情况下,OLS 回归假设可以通过有限数量的基础函数的适当组合来实现近似。由于样本数据集是随机生成的,因此 OLS 回归在这种情况下表现不佳。

aiif 0107

图 1-7. 随机样本数据和 OLS 回归线

神经网络怎么样?应用方式与以前一样简单,并显示了如图 1-8 所示的估算结果。虽然最终结果并非完美,但显然神经网络在从随机特征值估计随机标签值方面表现更好。然而,考虑到其架构,神经网络几乎有 200,000 个可训练参数(权重),这提供了相对高的灵活性,尤其是与仅使用最多 15+1 参数的 OLS 回归相比:

In [42]: model = Sequential()
         model.add(Dense(256, activation='relu', input_dim=1))
         for _ in range(3):
             model.add(Dense(256, activation='relu'))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.add(Dense(1, activation='linear'))
         model.compile(loss='mse', optimizer='rmsprop')

In [43]: model.summary()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         Model: "sequential_2"
         _________________________________________________________________
         Layer (type)                 Output Shape              Param #
         =================================================================
         dense_3 (Dense)              (None, 256)               512
         _________________________________________________________________
         dense_4 (Dense)              (None, 256)               65792
         _________________________________________________________________
         dense_5 (Dense)              (None, 256)               65792
         _________________________________________________________________
         dense_6 (Dense)              (None, 256)               65792
         _________________________________________________________________
         dense_7 (Dense)              (None, 1)                 257
         =================================================================
         Total params: 198,145
         Trainable params: 198,145
         Non-trainable params: 0
         _________________________________________________________________

In [44]: %%time
         plt.figure(figsize=(10, 6))
         plt.plot(x, y, 'ro', label='sample data')
         for _ in range(1, 8):
             model.fit(x, y, epochs=500, verbose=False)
             y_ =  model.predict(x)
             MSE = ((y - y_.flatten()) ** 2).mean()
             print(f'round={_} | MSE={MSE:.5f}')
             plt.plot(x, y_, '--', label=f'round={_}')
         plt.legend();
         round=1 | MSE=0.13560
         round=2 | MSE=0.08337
         round=3 | MSE=0.06281
         round=4 | MSE=0.04419
         round=5 | MSE=0.03329
         round=6 | MSE=0.07676
         round=7 | MSE=0.00431
         CPU times: user 30.4 s, sys: 4.7 s, total: 35.1 s
         Wall time: 13.6 s

1

添加多个隐藏层。

2

展示网络架构和可训练参数的数量。

aiif 0108

图 1-8. 随机样本数据和神经网络估算结果

使用神经网络进行分类

神经网络的另一个好处是,它们也可以轻松用于分类任务。考虑下面这段使用基于Keras的神经网络进行分类的 Python 代码。二进制特征数据和标签数据是随机生成的。建模上的主要调整是将输出层的激活函数从linear改为sigmoid。更多详细信息将在后续章节中讨论。分类并不完美。但是,它达到了很高的准确率。如何准确率,即正确结果与所有标签值的关系随着训练时期数的增加而改变,显示在图 1-9 中。准确率开始较低,然后逐步改善,尽管并非每个步骤都如此:

In [45]: f = 5
         n = 10

In [46]: np.random.seed(100)

In [47]: x = np.random.randint(0, 2, (n, f))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         x  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[47]: array([[0, 0, 1, 1, 1],
                [1, 0, 0, 0, 0],
                [0, 1, 0, 0, 0],
                [0, 1, 0, 0, 1],
                [0, 1, 0, 0, 0],
                [1, 1, 1, 0, 0],
                [1, 0, 0, 1, 1],
                [1, 1, 1, 0, 0],
                [1, 1, 1, 1, 1],
                [1, 1, 1, 0, 1]])

In [48]: y = np.random.randint(0, 2, n)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         y  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[48]: array([1, 1, 0, 0, 1, 1, 0, 1, 0, 1])

In [49]: model = Sequential()
         model.add(Dense(256, activation='relu', input_dim=f))
         model.add(Dense(1, activation='sigmoid'))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         model.compile(loss='binary_crossentropy', optimizer='rmsprop',
                      metrics=['acc'])  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [50]: model.fit(x, y, epochs=50, verbose=False)
Out[50]: <keras.callbacks.callbacks.History at 0x7fde09dd1cd0>

In [51]: y_ = np.where(model.predict(x).flatten() > 0.5, 1, 0)
         y_
Out[51]: array([1, 1, 0, 0, 0, 1, 0, 1, 0, 1], dtype=int32)

In [52]: y == y_  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[52]: array([ True,  True,  True,  True, False,  True,  True,  True,  True,
                 True])

In [53]: res = pd.DataFrame(model.history.history)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [54]: res.plot(figsize=(10, 6));  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

1

创建随机特征数据。

2

创建随机标签数据。

3

将输出层的激活函数定义为sigmoid

4

将损失函数定义为binary_crossentropy

5

将预测值与标签数据进行比较。

6

绘制每个训练步骤的损失函数和准确度值。

aiif 0109

图 1-9. 根据周期数的分类准确度和损失

本节中的示例展示了神经网络与 OLS 回归相比的一些基本特征:

问题不可知性

神经网络方法在估计和分类标签值时是不可知的,只要给定一组特征值。统计方法,如 OLS 回归,对于一小部分问题可能表现良好,但对其他问题则可能效果不佳甚至根本无法使用。

增量学习

在神经网络中,给定一个目标成功度量,最优权重是通过随机初始化和增量改进逐步学习的。这些增量改进通过考虑预测值与样本标签值之间的差异,并通过神经网络反向传播权重更新来实现。

通用逼近

存在强大的数学定理表明,神经网络(即使只有一个隐藏层)可以近似几乎任何函数。⁷

这些特征可能正是本书将神经网络置于所用算法核心位置的理由。第二章将讨论更多充分的理由。

神经网络

神经网络擅长学习输入和输出数据之间的关系。它们可应用于多种问题类型,例如在存在复杂关系的情况下的估计或分类问题,而传统的统计方法则不太适用。

数据的重要性

前一节末尾的例子表明,神经网络能够相当好地解决分类问题。具有一个隐藏层的神经网络在给定数据集上达到了很高的准确度,或称为样本内。然而,神经网络的预测能力如何?这在很大程度上取决于用于训练神经网络的数据量和种类的多样性。基于更大数据集的另一个数值例子将阐明这一点。

小数据集

考虑一个与分类示例中使用的相似的随机样本数据集,但具有更多的特征和更多的样本。人工智能中使用的大多数算法都是关于模式识别的。在以下 Python 代码中,二进制特征的数量定义了算法可以学习的可能模式的数量。鉴于标签数据也是二进制的,算法试图学习在给定某个模式时01更可能出现的情况,比如[0, 0, 1, 1, 1, 1, 0, 0, 0, 0]。因为所有数字都是以相等概率随机选择的,除了标签01不管观察到什么(随机)模式都是同等可能发生的外,没有太多可学习的东西。因此,基线预测算法应该在任何(随机)模式被呈现时准确率约为 50%:

In [55]: f = 10
         n = 250

In [56]: np.random.seed(100)

In [57]: x = np.random.randint(0, 2, (n, f))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         x[:4]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[57]: array([[0, 0, 1, 1, 1, 1, 0, 0, 0, 0],
                [0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
                [0, 1, 0, 0, 0, 1, 1, 1, 0, 0],
                [1, 0, 0, 1, 1, 1, 1, 1, 0, 0]])

In [58]: y = np.random.randint(0, 2, n)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         y[:4]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[58]: array([0, 1, 0, 0])

In [59]: 2 ** f  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[59]: 1024

1

特征数据

2

标签数据

3

模式数量

为了继续进行,原始数据被放入了一个pandasDataFrame对象中,这简化了某些操作和分析:

In [60]: fcols = [f'f{_}' for _ in range(f)]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         fcols  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[60]: ['f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9']

In [61]: data = pd.DataFrame(x, columns=fcols)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         data['l'] = y  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [62]: data.info()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         <class 'pandas.core.frame.DataFrame'>
         RangeIndex: 250 entries, 0 to 249
         Data columns (total 11 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   f0      250 non-null    int64
          1   f1      250 non-null    int64
          2   f2      250 non-null    int64
          3   f3      250 non-null    int64
          4   f4      250 non-null    int64
          5   f5      250 non-null    int64
          6   f6      250 non-null    int64
          7   f7      250 non-null    int64
          8   f8      250 non-null    int64
          9   f9      250 non-null    int64
          10  l       250 non-null    int64
         dtypes: int64(11)
         memory usage: 21.6 KB

1

为特征数据定义列名

2

将特征数据放入一个DataFrame对象中

3

将标签数据放入同一个DataFrame对象中

4

显示数据集的元信息

通过执行以下 Python 代码的结果,可以确定两个主要问题。首先,样本数据集中并不包含所有模式。其次,每个观察到的模式的样本量太小。即使不深入挖掘,也很明显没有分类算法能够真正有意义地学习所有可能的模式:

In [63]: grouped = data.groupby(list(data.columns))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [64]: freq = grouped['l'].size().unstack(fill_value=0)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [65]: freq['sum'] = freq[0] + freq[1]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [66]: freq.head(10)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[66]: l                              0  1  sum
         f0 f1 f2 f3 f4 f5 f6 f7 f8 f9
         0  0  0  0  0  0  0  1  1  1   0  1    1
                           1  0  1  0   1  1    2
                                    1   0  1    1
                        1  0  0  0  0   1  0    1
                                    1   0  1    1
                              1  1  1   0  1    1
                           1  0  0  0   0  1    1
                                 1  0   0  1    1
                     1  0  0  0  1  1   1  0    1
                           1  1  0  0   1  0    1

In [67]: freq['sum'].describe().astype(int)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[67]: count    227
         mean       1
         std        0
         min        1
         25%        1
         50%        1
         75%        1
         max        2
         Name: sum, dtype: int64

1

沿所有列对数据进行分组

2

展开标签列的分组数据

3

01的频率进行求和

4

显示给定某个模式的01的频率

5

提供频率总和的统计信息

以下 Python 代码使用了scikit-learn中的MLPClassifier模型。⁸ 该模型在整个数据集上进行了训练。神经网络在学习给定数据集内部关系方面的能力如何?从样本内准确率分数来看,这种能力相当高。事实上,这个准确率接近于 100%,这在很大程度上是由于相对较小的数据集给出了相对较高的神经网络容量所致:

In [68]: from sklearn.neural_network import MLPClassifier
         from sklearn.metrics import accuracy_score

In [69]: model = MLPClassifier(hidden_layer_sizes=[128, 128, 128],
                               max_iter=1000, random_state=100)

In [70]: model.fit(data[fcols], data['l'])
Out[70]: MLPClassifier(hidden_layer_sizes=[128, 128, 128], max_iter=1000,
                       random_state=100)

In [71]: accuracy_score(data['l'], model.predict(data[fcols]))
Out[71]: 0.952

但是,对于经过训练的神经网络的预测能力又如何呢?为此,给定的数据集可以分为训练和测试数据子集。模型仅在训练数据子集上进行训练,然后针对测试数据集测试其预测能力。与之前相同,训练好的神经网络在样本内(即训练数据集)的准确率非常高。然而,在测试数据集上,它比一个无信息基准算法差了超过 10 个百分点:

In [72]: split = int(len(data) * 0.7)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [73]: train = data[:split]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         test = data[split:]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [74]: model.fit(train[fcols], train['l'])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[74]: MLPClassifier(hidden_layer_sizes=[128, 128, 128], max_iter=1000,
                       random_state=100)

In [75]: accuracy_score(train['l'], model.predict(train[fcols]))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[75]: 0.9714285714285714

In [76]: accuracy_score(test['l'], model.predict(test[fcols]))  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[76]: 0.38666666666666666

1

将数据分割为traintest数据子集

2

仅在训练数据集上训练模型

3

报告了样本内的准确率(训练数据集)

4

报告了样本外的准确率(测试数据集)

简言之,仅在小数据集上训练的神经网络,由于识别出的两个主要问题区域,学习到了错误的关系。在样本内学习关系时,这些问题实际上并不重要。相反,数据集越小,通常越容易学习样本内的关系。然而,当使用训练好的神经网络进行样本外预测时,这些问题区域就变得非常重要。

更大的数据集

幸运的是,通常有一种明显的方法来摆脱这个问题情境:更大的数据集。面对现实世界中的问题,这种理论洞见可能同样正确。然而,从实际的角度来看,这样的更大数据集并不总是可用的,也不容易生成。然而,在本节示例的上下文中,确实可以轻松地创建一个更大的数据集。

以下 Python 代码显著增加了初始样本数据集中的样本数量。结果是,训练好的神经网络的预测准确率提高了超过 10 个百分点,达到约 50%,这是可以预期的,考虑到标签数据的性质。现在,它与无信息基准算法的一致性已经达到了:

In [77]: factor = 50

In [78]: big = pd.DataFrame(np.random.randint(0, 2, (factor * n, f)),
                            columns=fcols)

In [79]: big['l'] = np.random.randint(0, 2, factor * n)

In [80]: train = big[:split]
         test = big[split:]

In [81]: model.fit(train[fcols], train['l'])
Out[81]: MLPClassifier(hidden_layer_sizes=[128, 128, 128], max_iter=1000,
                       random_state=100)

In [82]: accuracy_score(train['l'], model.predict(train[fcols]))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[82]: 0.9657142857142857

In [83]: accuracy_score(test['l'], model.predict(test[fcols]))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[83]: 0.5043407707910751

1

样本内的预测准确率(训练数据集)

2

样本外的预测准确率(测试数据集)

如下所示,对可用数据的快速分析说明了预测准确性的提高。首先,现在数据集中包含所有可能的模式。其次,所有模式在数据集中的平均频率均超过 10。换句话说,神经网络基本上多次看到所有模式。这使得神经网络能够“学习”,即所有可能的模式对标签01同等可能。当然,这是一种相对复杂的学习方式,但它很好地说明了在神经网络背景下,相对较小的数据集经常可能太小的事实:

In [84]: grouped = big.groupby(list(data.columns))

In [85]: freq = grouped['l'].size().unstack(fill_value=0)

In [86]: freq['sum'] = freq[0] + freq[1]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [87]: freq.head(6)
Out[87]: l                               0  1  sum
         f0 f1 f2 f3 f4 f5 f6 f7 f8 f9
         0  0  0  0  0  0  0  0  0  0   10  9   19
                                    1    5  4    9
                                 1  0    2  5    7
                                    1    6  6   12
                              1  0  0    9  8   17
                                    1    7  4   11

In [88]: freq['sum'].describe().astype(int)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[88]: count    1024
         mean       12
         std         3
         min         2
         25%        10
         50%        12
         75%        15
         max        26
         Name: sum, dtype: int64

1

添加01值的频率

2

显示和值的汇总统计信息

数据量和多样性

在进行预测任务的神经网络背景下,用于训练神经网络的可用数据的数量和多样性对其预测性能至关重要。本节中的数值假设示例表明,同一神经网络在相对较小且数据不够丰富的数据集上训练,其预测性能要比在相对较大且数据更加多样化的数据集上训练的网络低超过 10 个百分点。考虑到人工智能从业者和公司通常为提高预测准确性而奋斗,这种差异可以被视为巨大的。

大数据

更大数据集和数据集之间的区别是什么?大数据这个术语已经用了十多年,有多种含义。对于本书的目的,可以说大数据集足够大——在数据量、多样性甚至速度方面——使得 AI 算法能够适当地训练,从而使算法在预测任务中表现优于基准算法。

之前使用的更大数据集在实际条件下仍然很小。然而,它足够实现指定的目标。数据集所需的数量和多样性主要由特征和标签数据的结构和特征驱动。

在这种情况下,假设一家零售银行实施基于神经网络的信用评分分类方法。根据内部数据,负责的数据科学家设计了 25 个分类特征,每个特征可以取 8 个不同的值。由此产生的模式数量是天文数字:

In [89]: 8 ** 25
Out[89]: 37778931862957161709568

很明显,没有单个数据集可以为神经网络提供接触每个模式的机会。⁹ 幸运的是,在实践中,对于神经网络根据常规、违约和/或拒绝的债务人的数据学习信用价值的情况,并不需要单个数据集对所有潜在债务人的信用价值进行“好”的预测。

这是由于多种原因。仅举几例,首先,并非每种模式在实践中都是相关的——有些模式可能根本不存在,可能是不可能的等等。其次,并非所有特征都可能同等重要,减少相关特征数量,从而减少可能的模式数量。第三,例如,特征编号为7的值为45可能根本没有任何差别,进一步减少了相关模式的数量。

结论

对于本书,人工智能(AI)涵盖了能够从数据中学习关系、规则、概率等的方法、技术、算法等。重点放在监督学习算法上,例如用于估计和分类的算法。关于算法,神经网络和深度学习方法处于核心位置。

这本书的核心主题是将神经网络应用于金融领域的核心问题之一:预测未来市场走势。更具体地说,问题可能是预测股票指数的方向或货币对的汇率。预测未来市场走向(即目标水平或价格是上升还是下降)可以很容易地转化为分类问题。

在深入讨论核心主题本身之前,下一章首先讨论与所谓的超智能技术奇点相关的选定主题。这些讨论将为接下来专注于金融和人工智能在金融领域应用的章节提供有用的背景。

参考文献

本章引用的书籍和论文:

  • Alpaydin,Ethem。2016。机器学习. MIT Press,Cambridge。

  • Chollet,Francois。2017。Python 深度学习. Shelter Island:Manning。

  • Goodfellow,Ian,Yoshua Bengio 和 Aaron Courville。2016。深度学习. Cambridge:MIT Press。http://deeplearningbook.org

  • Kratsios,Anastasis。2019。《通用逼近定理》。https://oreil.ly/COOdI

  • Silver,David 等。2016。《使用深度神经网络和树搜索掌握围棋》。Nature 529(一月):484-489。

  • Shanahan,Murray。2015。技术奇点. Cambridge:MIT Press。

  • Tegmark,Max。2017。生命 3.0:在人工智能时代的人类存在. United Kingdom:Penguin Random House。

  • VanderPlas,Jake。2017。Python 数据科学手册. Sebastopol:O’Reilly。

¹ 详细信息请参阅sklearn.cluster.KMeans,以及 VanderPlas (2017,第五章)。

² 详细信息请参阅 VanderPlas (2017,第五章)。

³ 详细信息请参阅sklearn.neural_network.MLPRegressor。有关更多背景信息,请参阅 Goodfellow 等人(2016,第六章)。

⁴ 详细信息请参阅 Chollet (2017, ch. 3)。

⁵ 有关Keras激活函数的详细信息,请参阅https://keras.io/activations

损失函数计算神经网络(或其他 ML 算法)的预测误差。对于二元分类问题,二元交叉熵是一种合适的损失函数,而对于估计问题,例如均方误差(MSE)则更合适。有关Keras损失函数的详细信息,请参阅https://keras.io/losses

⁷ 例如,参见 Kratsios (2019)。

⁸ 详细信息请参阅sklearn.neural_network.MLPClassifier

⁹ 如果有此类数据集可用,当前计算技术也无法对基于此数据集的神经网络进行建模和训练。在这个背景下,下一章讨论了硬件对 AI 的重要性。

第二章:超智能

事实上,通向超智能的路径有很多条,这应增加我们最终能达到这一目标的信心。如果一条路径被阻碍,我们仍然可以取得进展。

Nick Bostrom(2014 年)

技术奇点这一术语有多种定义。其使用至少可以追溯到 Vinge(1993 年)的文章,作者大胆地以此开始:

未来三十年内,我们将拥有技术手段创造超人类智能。不久之后,人类时代将结束。

本章和本书中,技术奇点指的是某些机器达到超人类智能或超智能的时间点——这基本上符合 Vinge(1993 年)最初的想法。这个概念和概念被 Kurzweil(2005 年)广泛阅读和引用的书籍进一步普及。Barrat(2013 年)提供了大量围绕这个主题的历史和轶事信息。Shanahan(2015 年)对其核心方面进行了非正式介绍和概述。表达技术奇点本身起源于物理学中奇点的概念。它指的是黑洞中心,其中物质高度集中,引力变得无限,传统物理定律崩溃。宇宙的起源,即所谓的大爆炸,也被称为奇点。

尽管技术奇点和超智能的一般思想和概念可能与应用于金融的人工智能没有明显和直接的关系,但更好地理解它们的背景、相关问题和潜在后果是有益的。在更狭窄的背景下,如金融领域中的人工智能,获得的见解也很重要。这些见解还有助于指导讨论关于人工智能如何在近期和长期重塑金融业的议题。

“成功故事” 着眼于人工智能领域最近的成功故事的一部分。其中包括 DeepMind 公司如何用神经网络解决玩 Atari 2600 游戏的问题。它还讲述了同一家公司如何解决超越人类专家水平的围棋问题的故事。该部分还叙述了国际象棋和计算机程序的故事。“硬件的重要性” 讨论了在这些最近成功故事的背景下硬件的重要性。“智能形式” 介绍了不同形式的智能,如人工狭义智能(ANI)、人工通用智能(AGI)和超级智能(SI)。“通往超级智能的途径” 讨论了通向超级智能的潜在途径,如整脑仿真(WBE),而“智能爆炸” 则是研究人员称之为智能爆炸的内容。“目标和控制” 提供了在超级智能背景下所谓的控制问题的讨论。最后,“潜在的结果” 简要讨论了一旦实现了超级智能后的潜在未来结果和情景。

成功故事

人工智能的许多思想和算法早在几十年前就已经存在了。在这几十年中,人们一方面抱有长期的希望,另一方面则感到绝望。Bostrom(2014 年,第一章)对这些时期进行了回顾。

到了 2020 年,可以肯定地说人工智能正处于一个充满希望(如果不是兴奋)的时期。其中一个原因是最近在将人工智能应用于即使在几年前看起来也不可能在未来几十年内被人工智能主导的领域和问题上取得了成功。这类成功故事的列表既长且迅速增长。因此,本节仅集中讨论了其中三个这样的故事。Gerrish(2018)提供了更广泛的选择和更详细的案例记录。

Atari

本小节首先讲述了 DeepMind 如何通过强化学习和神经网络掌握玩 Atari 2600 游戏的成功故事,然后通过具体的代码示例阐述了导致其成功的基本方法。

故事

第一个成功故事是关于在超人水平上玩 Atari 2600 游戏。¹ Atari 2600 视频电脑系统(VCS)于 1977 年发布,是 1980 年代最早的广泛使用的游戏机之一。从那个时期选出的流行游戏,如Space InvadersAsteroidsMissile Command,被视为经典,几十年后仍受复古游戏爱好者的喜爱。

DeepMind 在其团队发表的一篇论文(Mnih et al. 2013)中详细介绍了将强化学习应用于通过 AI 算法或所谓的 AI 代理来玩 Atari 2600 游戏的结果。该算法是 Q-learning 的变体,应用于卷积神经网络。² 该算法仅基于高维视觉输入(原始像素)进行训练,没有任何人类的指导或输入。原始项目专注于七款 Atari 2600 游戏,其中三款——PongEnduroBreakout——DeepMind 团队报告了 AI 代理的超越人类专家水平的表现。

从 AI 的角度来看,不仅是 DeepMind 团队实现了这样的结果,而且是如何实现的令人瞩目。首先,团队仅使用一个神经网络来学习和玩所有七款游戏。其次,没有提供任何人类指导或人工标记的数据,仅仅是基于视觉输入转化为特征数据的交互学习经验。³ 第三,采用的方法是强化学习,仅依赖于观察行动与结果(奖励)之间的关系,基本上与人类玩此类游戏的方式相同。

《Breakout》是 Atari 2600 游戏中的一个,DeepMind AI 代理实现了超越人类专家水平的表现,链接。在这个游戏中,目标是通过屏幕底部的挡板,让球反弹并直线穿过屏幕,摧毁屏幕顶部的一行行砖块。每当球击中砖块时,砖块被摧毁并使球反弹。球还会从左、右和顶部的墙壁反弹。如果球到达屏幕底部而未被挡板击中,玩家就会失去一条生命。

行动空间有三个元素,都与挡板相关:保持当前位置、向左移动和向右移动。状态空间由大小为 210 x 160 像素的游戏屏幕帧表示,并配有 128 色调色板。奖励由游戏分数表示,DeepMind 算法被设计为最大化这一分数。关于行动策略,算法学习在给定某个游戏状态时采取哪个行动最佳,以最大化游戏分数(总奖励)。

一个例子

此章节无法详细探讨 DeepMind 在Breakout和其他 Atari 2600 游戏中采用的方法。然而,OpenAI Gym 环境(参见https://gym.openai.com)允许展示类似但更简单的神经网络方法,适用于类似但同样更简单的游戏。

此部分的 Python 代码适用于 OpenAI Gym 的 CartPole 环境(参见http://bit.ly/aiif_cartpole)。⁴ 在这个环境中,需要将车移动到右边或左边,以平衡放在挡板上的直立杆。因此,动作空间类似于 Breakout 的动作空间。状态空间由四个物理数据点组成:车的位置、车的速度、杆的角度和杆的角速度(见 Figure 2-1)。如果在执行动作后,杆仍然保持平衡,则代理获得奖励 1。如果杆失去平衡,则游戏结束。如果代理达到总奖励 200,则被认为是成功的。⁵

aiif 0201

图 2-1. CartPole 环境的图形表示

以下代码首先实例化一个 CartPole 环境对象,然后检查动作和状态空间,执行随机动作,并捕获结果。当 done 变量为 False 时,AI 代理继续下一轮:

In [1]: import gym
        import numpy as np
        import pandas as pd
        np.random.seed(100)

In [2]: env = gym.make('CartPole-v0')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: env.seed(100)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[3]: [100]

In [4]: action_size = env.action_space.n  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        action_size  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[4]: 2

In [5]: [env.action_space.sample() for _ in range(10)]  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[5]: [1, 0, 0, 0, 1, 1, 0, 0, 0, 0]

In [6]: state_size = env.observation_space.shape[0]  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
        state_size  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[6]: 4

In [7]: state = env.reset()  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
        state  # [cart position, cart velocity, pole angle, pole angular velocity]
Out[7]: array([-0.01628537,  0.02379786, -0.0391981 , -0.01476447])

In [8]: state, reward, done, _ = env.step(env.action_space.sample())  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
        state, reward, done, _  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
Out[8]: (array([-0.01580941, -0.17074066, -0.03949338,  0.26529786]), 1.0, False, {})

1

实例化环境对象

2

为环境设置随机数种子

3

显示动作空间的大小

4

进行一些随机动作并收集它们

5

显示状态空间的大小

6

重置(初始化)环境并捕获状态

7

执行随机动作并将环境推进到下一个状态

下一步是根据随机动作来玩游戏,以生成足够大的数据集。然而,为了提高数据集的质量,仅收集了总奖励达到 110 或更高的游戏数据。为此,玩了几千场游戏,以收集足够的数据来训练神经网络:

In [9]: %%time
        data = pd.DataFrame()
        state = env.reset()
        length = []
        for run in range(25000):
            done = False
            prev_state = env.reset()
            treward = 1
            results = []
            while not done:
                action = env.action_space.sample()
                state, reward, done, _ = env.step(action)
                results.append({'s1': prev_state[0], 's2': prev_state[1],
                                's3': prev_state[2], 's4': prev_state[3],
                                'a': action, 'r': reward})
                treward += reward if not done else 0
                prev_state = state
            if treward >= 110:  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                data = data.append(pd.DataFrame(results))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                length.append(treward)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        CPU times: user 9.84 s, sys: 48.7 ms, total: 9.89 s
        Wall time: 9.89 s

In [10]: np.array(length).mean()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[10]: 119.75

In [11]: data.info()  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         <class 'pandas.core.frame.DataFrame'>
         Int64Index: 479 entries, 0 to 143
         Data columns (total 6 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   s1      479 non-null    float64
          1   s2      479 non-null    float64
          2   s3      479 non-null    float64
          3   s4      479 non-null    float64
          4   a       479 non-null    int64
          5   r       479 non-null    float64
         dtypes: float64(5), int64(1)
         memory usage: 26.2 KB

In [12]: data.tail()  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[12]:            s1        s2        s3        s4  a    r
         139  0.639509  0.992699 -0.112029 -1.548863  0  1.0
         140  0.659363  0.799086 -0.143006 -1.293131  0  1.0
         141  0.675345  0.606042 -0.168869 -1.048421  0  1.0
         142  0.687466  0.413513 -0.189837 -0.813148  1  1.0
         143  0.695736  0.610658 -0.206100 -1.159030  0  1.0

1

仅当随机代理的总奖励至少为 100 时…

2

…数据被收集…

3

…并记录总奖励。

4

所有随机游戏的平均总奖励。

5

查看 DataFrame 对象中收集的数据

装备有数据后,可以按以下方式训练神经网络。为分类设置一个神经网络。用表示状态数据的列作为特征,用表示采取的动作的列作为标签数据来训练它。鉴于数据集仅包括对给定状态成功的动作,神经网络学习了在给定状态下采取什么动作(标签):

In [13]: from pylab import plt
         plt.style.use('seaborn')
         %matplotlib inline

In [14]: import tensorflow as tf
         tf.random.set_seed(100)

In [15]: from keras.layers import Dense
         from keras.models import Sequential
         Using TensorFlow backend.

In [16]: model = Sequential()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.add(Dense(64, activation='relu',
                         input_dim=env.observation_space.shape[0]))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.add(Dense(1, activation='sigmoid'))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.compile(loss='binary_crossentropy',
                       optimizer='adam',
                       metrics=['acc'])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [17]: %%time
         model.fit(data[['s1', 's2', 's3', 's4']], data['a'],
                   epochs=25, verbose=False, validation_split=0.2)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         CPU times: user 1.02 s, sys: 166 ms, total: 1.18 s
         Wall time: 797 ms

Out[17]: <keras.callbacks.callbacks.History at 0x7ffa53685190>

In [18]: res = pd.DataFrame(model.history.history)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         res.tail(3)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[18]:     val_loss  val_acc      loss       acc
         22  0.660300  0.59375  0.646965  0.626632
         23  0.660828  0.59375  0.646794  0.621410
         24  0.659114  0.59375  0.645908  0.626632

1

仅使用一个隐藏层的神经网络。

2

模型是基于先前收集的数据进行训练的。

3

最后几步的每个训练步骤的指标如下所示。

经过训练的神经网络或 AI 代理可以根据其学习到的最佳动作来玩CartPole游戏。AI 代理在每次玩的 100 场比赛中都能获得最大的总奖励 200 分。这是基于相对较小的数据集和相对简单的神经网络:

In [20]: def epoch():
             done = False
             state = env.reset()
             treward = 1
             while not done:
                 action = np.where(model.predict(np.atleast_2d(state))[0][0] > \
                          0.5, 1, 0)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 state, reward, done, _ = env.step(action)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 treward += reward if not done else 0
             return treward

In [21]: res = np.array([epoch() for _ in range(100)])
         res ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[21]: array([200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200., 200., 200., 200., 200., 200., 200., 200., 200., 200., 200.,
                200.])

In [22]: res.mean()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[22]: 200.0

1

在给定状态和训练模型的情况下选择一个动作

2

根据学习的动作将环境向前推进一步

3

玩若干游戏并记录每场游戏的总奖励

4

计算所有游戏的平均总奖励

Arcade Learning Environment(ALE)与 OpenAI Gym 类似。它允许程序化地与仿真的 Atari 2600 游戏互动,执行动作,收集执行动作后的结果等。例如,学习玩Breakout这样的任务当然更为复杂,因为状态空间要大得多。然而,基本的方法与这里采取的方法相似,都有几种算法上的改进。

Go

棋盘游戏Go已有 2000 多年的历史。长期以来,它被认为是一种美与艺术的创作,因为它在原则上简单但仍然高度复杂,并且预计能抵挡几十年来的游戏 AI 代理的进展。打 Go 的玩家的实力用dan来衡量,与许多武术系统的毕业系统一致。例如,李世石,多年来的 Go 世界冠军,持有第 9 段。2014 年,博斯特罗姆提出:

近年来,打 Go 的程序每年以约 1 段的速度提升。如果这种改进速度持续下去,它们可能在大约十年内击败人类世界冠军。

再次,DeepMind 团队通过其 AlphaGo 算法在 Go 游戏中取得了突破性进展(请参阅DeepMind 官网的 AlphaGo 页面)。Silver 等人(2016 年)在其研究中如下描述了这一情况:

长期以来,围棋被视为人工智能中最具挑战性的经典游戏之一,因为其巨大的搜索空间和难以评估的棋局和着法。

团队成员使用了神经网络与蒙特卡洛树搜索算法的组合,在他们的论文中简要概述了这一方法。在介绍部分,团队回顾了他们 2015 年初期的成功:

我们的程序 AlphaGo 对其他围棋程序取得了 99.8%的胜率,并以 5 比 0 击败了人类欧洲围棋冠军[樊振东]。这是计算机程序首次在围棋的全尺寸比赛中击败人类职业选手,这一壮举此前被认为至少还需要十年的时间。

令人瞩目的是,这一里程碑是在领先的人工智能研究员尼克·博斯特罗姆预测可能需要另外十年才能达到那一水平的一年后实现的。然而,许多观察家评论说,当时的欧洲围棋冠军樊振东并不能真正被视为一个基准,因为世界围棋精英的水平要高得多。DeepMind 团队接受了这一挑战,并在 2016 年 3 月组织了一场五局三胜的比赛,对阵当时已获得 18 次世界围棋冠军的李世石,这无疑是对人类围棋顶尖水平的适当基准。(有关这场事件的大量背景信息可在AlphaGo Korea 网页上找到,甚至还有一部电影可供观看。)为此,DeepMind 团队进一步改进了 AlphaGo Fan 版本,推出了 AlphaGo Lee 版本。

关于比赛和 AlphaGo 李的故事已经有了详尽的记录,并且引起了全世界的关注。DeepMind 在其网页上写道:

AlphaGo 于 2016 年 3 月在韩国首尔以 4 比 1 的胜利获得了全球 2 亿多人次的观看。这一具有里程碑意义的成就领先于其时代十年。这场比赛让 AlphaGo 获得了 9 段的专业段位,这是最高的认证。这是计算机围棋选手首次获得这一荣誉。

直到那个时候,AlphaGo 在监督学习中使用了基于数百万人类专家对局的训练数据集等资源。团队的下一个版本 AlphaGo Zero 完全跳过了这种方法,而是完全依赖于强化学习和自我对弈,组合不同世代的训练过的、基于神经网络的人工智能代理来互相竞争。Silver 等人的文章(2017b)详细介绍了 AlphaGo Zero。在摘要中,研究人员总结道:

AlphaGo 成为自己的老师:一个神经网络被训练来预测 AlphaGo 自己的移动选择以及 AlphaGo 游戏的赢家。这个神经网络改善了树搜索的强度,导致更高质量的移动选择和更强的自我对弈。从零开始,我们的新程序 AlphaGo Zero 实现了超人类的表现,以 100 比 0 击败了先前发布的、击败冠军的 AlphaGo。

令人惊奇的是,一个神经网络训练方式与前一节中的CartPole示例并没有太大不同(即基于自我对弈),竟然能够打破象棋这样一个复杂的游戏,其可能的棋盘位置超过了宇宙中的原子数量。同样令人惊奇的是,几个世纪以来由人类玩家积累的象棋智慧对于达到这一里程碑根本不是必要的。

DeepMind 团队并没有止步于此。AlphaZero 旨在成为一个通用的游戏 AI 代理,能够学习不同的复杂棋盘游戏,如围棋、国际象棋和将棋。关于 AlphaZero,团队在 Silver(2017a)中总结道:

在本文中,我们将这种方法推广为一个名为 AlphaZero 的算法,该算法可以在许多具有挑战性的领域中实现超人类水平的表现。从随机游戏开始,除了游戏规则外,没有给予任何领域知识,AlphaZero 在 24 小时内在象棋、将棋(日本象棋)以及围棋的游戏中都实现了超人类水平的表现,并且在每种情况下都击败了世界冠军程序。

2017 年,DeepMind 再次取得了一个引人注目的里程碑:一个游戏 AI 代理,在不到 24 小时的自我对弈和训练后,在三个历史悠久的、深入研究的棋盘游戏中达到了超越人类专家水平。

国际象棋

当然,国际象棋是世界上最流行的棋盘游戏之一。自家用计算机出现以来,就有国际象棋游戏程序存在。例如,一个几乎完整的象棋引擎叫做ZX Chess,它只包含大约 672 字节的机器码,于 1983 年推出,用于 ZX-81 Spectrum 家用计算机。[⁶] 尽管它是一个缺少某些规则(比如易位)的不完整实现,但那时它是一个巨大的成就,今天对于计算机象棋爱好者仍然很迷人。ZX Chess作为最小的象棋程序的记录保持了 32 年,直到 2015 年才被BootChess以 487 字节打破。[⁷]

用如此小的代码基础编写一个计算机程序,它能够玩一种棋盘游戏,其可能的游戏排列比宇宙中的原子还多,几乎可以被认为是软件工程的天才。虽然与围棋在纯数字方面不那么复杂,但象棋可以被认为是最具挑战性的棋盘游戏之一,因为玩家需要数十年才能达到国际大师级别。

在 20 世纪 80 年代中期,即使是在比基础家用计算机 ZX-81 Spectrum 上更好的硬件上,专家级别的计算机国际象棋程序仍然遥不可及,并且没有那么多的约束条件。因此当时的领先国际象棋选手在与计算机对战时也感到自信。例如,加里·卡斯帕罗夫(2017 年)回忆起 1985 年的一次事件,当时他进行了 32 场同时对局:

1985 年 6 月 6 日是汉堡的一个愉快的日子…我所有 32 位对手都是计算机…当我获得了 32-0 的完美成绩时,这并不令人惊讶。

计算机国际象棋开发者和硬件专家从 IBM 公司花费了 12 年时间,直到一台名为深蓝的计算机能够击败当时的世界国际象棋冠军卡斯帕罗夫。在他写的书中,这场历史性的输给深蓝之后 20 年出版,他写道:

十二年后,我在纽约为我的国际象棋生涯而战。只对抗一台机器,一台价值 1000 万美元的 IBM 超级计算机,外号“深蓝”。

卡斯帕罗夫与深蓝总共进行了六场比赛。计算机以 3.5 比 2.5 的成绩获胜;赢一场比赛获得一个完整的分数,平局双方各得半分。尽管深蓝输掉了第一场比赛,但它在剩下的五场中赢了两场,另外三场以双方协议平局结束。有人指出,深蓝不应被视为人工智能形式,因为它主要依赖于一个庞大的硬件集群。这个硬件集群包括 30 个节点和 480 个 IBM 专为此事件设计的特殊国际象棋芯片,能够每秒分析约 2 亿个位置。从这个意义上说,深蓝主要依赖于蛮力技术,而不是像神经网络等现代人工智能算法。

自 1997 年以来,硬件和软件都取得了巨大进步。卡斯帕罗夫在他的书中提到现代智能手机上的国际象棋应用时总结道:

再跳到今天,即 2017 年,你可以在手机上下载任意数量的免费国际象棋应用程序,它们能与任何人类大师匹敌。

击败人类大师所需的硬件成本从 1000 万美元降低到约 100 美元(即降低了 100,000 倍)。然而,常规计算机和智能手机的国际象棋应用程序仍依赖几十年计算机国际象棋的积累智慧。它们包含大量为游戏设计的规则和策略,依赖开局的大型数据库,并且利用现代设备的增强计算能力和内存进行基本上以蛮力为基础的数百万国际象棋位置的评估。

这就是 AlphaZero 的作用所在。AlphaZero 掌握国际象棋的方法是基于强化学习和不同版本的 AI 代理自我对弈竞争。DeepMind 团队将传统的计算机国际象棋方法与 AlphaZero 进行了对比(见AlphaZero 研究论文):

传统的国际象棋引擎,包括世界计算机国际象棋冠军 Stockfish 和 IBM 开创性的 Deep Blue,依赖于数千条由强大的人类玩家手工制作的规则和启发式方法,试图在游戏中考虑到每一种可能性……AlphaZero 采用了一种完全不同的方法,用深度神经网络和通用算法替代这些手工制作的规则,这些算法对游戏除了基本规则以外一无所知。

鉴于 AlphaZero 的这种白纸面对的方法,与领先的传统国际象棋计算机程序相比,在几个小时的基于自我对弈训练后,其性能异常出色。AlphaZero 只需 9 小时或更少的时间就能掌握超过所有人类玩家和其他所有国际象棋程序的水平,包括 Stockfish 引擎,它曾一度主导了计算机国际象棋。在 2016 年的测试系列中,AlphaZero 以 155 场胜利(大部分时候执白棋),仅输了 6 场,其余为平局,击败了 Stockfish。

尽管 IBM 的 Deep Blue 能够每秒分析 2 亿个位置,现代国际象棋引擎如 Stockfish,在多核通用硬件上,可以每秒分析约 6000 万个位置。与此同时,AlphaZero 仅分析约 6 万个位置每秒。尽管每秒分析的位置数量少了 1000 倍,它仍然能够击败 Stockfish。人们可能倾向于认为 AlphaZero 确实表现出了某种形式的智能,这是纯粹的蛮力无法弥补的。考虑到人类大师可能基于经验、模式和直觉每秒分析几百个位置,AlphaZero 可能在专家级人类国际象棋玩家和基于蛮力方法、辅以手工制作规则和存储棋局知识的传统国际象棋引擎之间占据了一个甜蜜的点。人们可以推测,AlphaZero 获得了类似于人类模式识别、远见和直觉的东西,同时由于其比较适合该目的的硬件,具有更高的计算速度。

硬件的重要性

在过去的十年里,AI 研究人员和实践者在 AI 算法方面取得了巨大进展。如前一节所示,强化学习通常与神经网络结合,用于行动策略表示,在许多不同领域都证明了其有用和优越性。

然而,如果没有硬件方面的进步,最近的 AI 成就是不可能的。再次,DeepMind 以及其通过强化学习(RL)掌握围棋的努力提供了一些宝贵的见解。Table 2-1 概述了从 2015 年以来各个主要 AlphaGo 版本的硬件使用和功耗情况。⁸ AlphaGo 的实力不仅稳步增强,而且硬件要求及相关功耗也显著降低。⁹

表 2-1. AlphaGo 的 DeepMind 硬件

版本 年份 Elo 等级^(a) 硬件 功耗 [TDP]
AlphaGo 粉丝 2015 >3,000 176 GPU >40,000
AlphaGo 李 2016 >3,500 48 TPUs 10,000+
AlphaGo 大师 2016 >4,500 4 TPUs <2,000
AlphaGo Zero 2017 >5,000 4 TPUs <2,000
^(a) 有关世界顶级人类围棋选手的 Elo 等级,请参见 https://www.goratings.org/en

AI 领域首次进行的重要硬件推动来自 GPU。虽然最初开发用于为计算机游戏生成快速的高分辨率图形,现代 GPU 也可以用于许多其他目的。其中一个用途涉及线性代数(例如矩阵乘法形式),这是 AI 总体及神经网络特别重要的数学学科。

截至 2020 年中期,市场上最快的消费级 CPU 之一是 Intel 最新版本的 i9 处理器(具有 8 核心和最多 16 个并行线程)。¹⁰ 根据手头的基准任务,其速度大约为 1 TFLOPS 或略高(即每秒一万亿浮点运算)。

与此同时,市场上最快的消费级 GPU 之一是 Nvidia GTX 2080 Ti。它拥有 4,352 个 CUDA 核心,Nvidia 版本的 GPU 核心。这使得在线性代数操作的上下文中能够高度并行。这款 GPU 的速度可达 15 TFLOPS,大约比 Intel 最快的消费级 CPU 快 15 倍。GPU 在速度上长期以来一直快于 CPU。然而,一个主要的限制因素通常是 GPU 相对较小和专业化的内存。随着新型 GPU 模型(如 GTX 2080 Ti)的推出,这一问题显著得到缓解,它具备高达 11 GB 的快速 GDDR6 内存和高速数据传输的总线速度,可用于与 GPU 的数据交换。¹¹

在 2020 年中期,这类 GPU 的零售价格约为 1400 美元,比起 10 年前同等功率的硬件便宜了几个数量级。这一进展使得 AI 研究,例如,对于相对预算较小的个人学术研究者而言更加负担得起,相比之下,公司如 DeepMind 的预算要高得多。

另一个硬件趋势是促进 AI 方法和算法的进一步发展和采用:云中的 GPU 和 TPU。像 Scaleway 这样的云服务提供商提供可以按小时租用的云实例,并配备强大的 GPU(参见Scaleway GPU instances)。其他如 Google 已开发了专门用于 AI 的 TPU 芯片,类似于 GPU,使得线性代数运算更加高效(参见Google TPUs)。

总的来说,从 AI 的角度来看,硬件在过去几年中有了巨大的进步。总结起来,值得强调三个方面:

性能

GPU 和 TPU 提供具有高度并行体系结构的硬件,非常适合 AI 算法和神经网络。

成本

每 TFLOPS 计算能力的成本显著下降,使得更小的 AI 相关预算或者同样预算下更多的计算能力成为可能。

功率

功耗也在降低。同样的 AI 相关任务需要更少的功耗,通常也能更快地执行。

智能形式

AlphaGo Zero 是否智能?如果没有一个明确的智能定义,这很难说。AI 研究者 Max Tegmark(2017 年)将智能简洁地定义为“实现复杂目标的能力”。

这个定义足够一般化,以涵盖更具体的定义。根据这个定义,AlphaZero 是智能的,因为它能够实现一个复杂的目标,即在对弈人类玩家或其他 AI 代理的围棋或国际象棋比赛中获胜。当然,人类和动物总体上也因此被认为是智能的。

对于本书的目的,以下更具体的定义似乎是合适且足够精确。

人工狭窄智能(ANI)

This specifies an AI agent that exceeds human-expert-level capabilities and skills in a narrow field. AlphaZero can be considered an ANI in the fields of Go, chess, and shogi. An algorithmic stock-trading AI agent that realizes a net return of consistently 100% per year (per anno) on the invested capital could be considered an ANI.

通用人工智能(AGI)

This specifies an AI agent that reaches human-level intelligence in any field, such as chess, mathematics, text composition, or finance, and might exceed human-level intelligence in some other domains.

超级智能(SI)

This specifies an intellect or AI agent that exceeds human-level intelligence in any respect.

一个 ANI 具有在一个狭窄领域内达到复杂目标的能力,比任何人类都要高。一个 AGI 在各种领域中达成复杂目标的能力与任何人类相当。最后,一个超智能在几乎任何可想象的领域中,比任何人类,甚至是人类集体,都要显著地更擅长实现复杂目标。

前面给出的超智能定义与 Bostrom 在其名为《超智能》(2014 年)的书中提供的定义一致:

我们可以暂时定义超智能为在几乎所有感兴趣领域中,远远超过人类认知表现的任何智力

如前所定义,技术奇点是超智能存在的时间点。然而,哪些路径可能导致超智能?这是下一节的主题。

通往超智能

多年来,研究人员和实践者一直在争论是否可能创建超智能。关于技术奇点实现的估计从几年到几十年,几个世纪,甚至永远不可能。无论一个人是否相信超智能的可行性,讨论实现它的潜在路径都是有益的。

首先,以下是来自 Bostrom(2014 年,第二章)的一段较长引文,其中列出了一些通用考虑因素,这些因素可能适用于实现超智能的任何潜在途径:

然而,我们可以辨认出那种所需系统的一些通用特征。现在看来,学习能力将成为旨在实现通用智能的核心设计特征,而不是作为后来的扩展或唐突的附加物。处理不确定性和概率信息的能力也同样如此。从感官数据和内部状态中提取有用概念的能力,以及将获得的概念转化为用于逻辑和直觉推理的灵活组合表达,这些在现代 AI 旨在实现通用智能时,也很可能属于核心设计特征之一。

这些通用特征让人想起了 AlphaZero 的方法和能力,尽管像直观这样的术语可能需要定义才能适用于 AI 代理。但如何实际实现这些通用特征呢?Bostrom(2014 年,第二章)讨论了五条可能的路径,在接下来的子章节中进行了探讨。

网络和组织

实现超智能的第一条途径是通过涉及可能大量人类的网络和组织,以使他们的个体智能被放大并同步工作。团队,包括具有不同技能的人,是这种网络或组织的一个简单例子。在这种背景下经常提到的一个例子是美国政府为曼哈顿计划组建的领先专家团队,旨在建造核武器以决定性地结束第二次世界大战。

这条路径似乎有自然的限制,因为单个人类的个体能力和容量相对固定。进化也表明,人类在超过 150 人的网络和组织中协调起来存在困难。大公司通常比这小得多地形成了更小的团队、部门或群体。

另一方面,计算机和机器的网络,如互联网,往往能够基本无缝地工作,即使有数百万的计算节点。这样的网络今天至少能够组织人类的知识和其他数据(声音、图片、视频等)。当然,AI 算法已经帮助人类浏览所有这些知识和数据。然而,从今天的角度来看,一个超智能是否可能“自发”地从互联网中出现是值得怀疑的,可能需要专门的努力。

生物增强

如今,人类在提高个体认知和身体表现方面投入了大量努力。从更自然的方法,如更好的训练和学习方法,到涉及物质,如补充剂或智能甚至致幻药物,再到涉及特殊工具,人类今天比以往任何时候都更加系统和科学地努力提高个体的认知和身体表现。哈拉里(2015 年)将这种努力描述为智人寻求创造一个新而更好版本的自己,智神的探索。

然而,这种方法再次面临人类硬件基本固定的障碍。它已经在数十万年间进化,并且在可预见的未来可能会继续。但这将以相对缓慢的速度进行,并且仅在许多代人中。它也仅会在很小的程度上发生,因为如今自然选择对人类的作用大大减少,而自然选择是进化获得改善的动力。多明戈斯(2015 年,第五章)讨论了通过进化取得进展的中心方面。

在这个背景下,用泰格马克(2017 年,第一章)概述的生命版本来思考是很有帮助的:

  • 生命 1.0(生物学):具有基本固定硬件(生物体)和软件(基因)的生命形式。两者通过进化同时缓慢演化。例如细菌或昆虫。

  • 生命 2.0(文化):基本上具有固定且演化缓慢的硬件,但主要由设计和学习的软件组成(基因加上语言、知识、技能等)。一个例子是人类。

  • 生命 3.0(技术):具有设计和可调整硬件以及完全学习和进化的软件的生命形式。一个例子是使用计算机硬件、软件和 AI 算法创建的超级智能。

在机器超级智能中体现的技术生命,可用硬件的限制几乎完全消失。因此,与网络或生物增强之外的通往超级智能的路径,目前可能会证明更有前途。

大脑-机器混合体

在任何领域提升人类表现的混合方法在我们的生活中随处可见,并通过人类使用各种硬件和软件工具的象征。人类自其起源以来一直使用工具。今天,数十亿人携带着装有 Google Maps 的智能手机,使得即使在从未到访的地区和城市中,也能轻松导航。这是我们的祖先所没有的奢侈品,因此他们需要基于天空中看到的物体获取导航技能,或者使用远不如此精密的工具,比如指南针。

在国际象棋的背景下,例如,当计算机(如 Deep Blue)被证明优于人类时,并不意味着人类停止下棋。相反,计算机国际象棋程序性能的提升使它们成为每位大师系统提升游戏水平的必不可少的工具。人类大师和高速计算的国际象棋引擎形成了一个人机团队,在其他条件相同的情况下,比单独的人类表现更优。甚至有国际象棋比赛,人类在互相对弈的同时利用计算机来提出下一步的走法。

同样地,可以想象通过适当的接口直接连接人脑到机器,使得大脑能够与机器正确通信,交换数据并启动某些计算、分析或学习任务。听起来像是科幻小说的事情实际上是一个活跃的研究领域。例如,埃隆·马斯克(Elon Musk)是 Neuralink 这个专注于神经技术(常被称为这个领域)的初创公司的创始人。

总而言之,大脑-机器混合体似乎在实践上是可行的,并有可能显著超越人类智能。然而,它是否会导致超级智能并不明显。

整体脑仿真

另一种推荐的通往超智能的路径是首先完全模拟人脑,然后对其进行改进。这里的想法是通过现代脑部扫描以及生物和医学分析方法来绘制整个人脑的地图,以精确复制其神经元、突触等结构,并通过软件实现。该软件将在适当的硬件上运行。Domingos(2015 年,第四章)提供了关于人脑及其学习特性的背景信息。Kurzweil(2012 年)则在这个主题上提供了一本详尽的处理,提供了详细的背景信息,并勾画了实现整个大脑仿真(WBE,有时也称为上传)的方式。¹²

在一个较少雄心勃勃的层面上,神经网络确实实现了 WBE 试图实现的目标。正如其名称所暗示的那样,神经网络受到大脑的启发,因为它们在许多不同领域已经被证明是如此有用和成功,因此人们可能倾向于认为 WBE 确实可以被视为通向超智能的可行路径。然而,完整绘制出人类大脑所需的技术目前仅部分可用。即使绘制成功,也不清楚软件版本是否能够完成人脑能够完成的同样任务。

然而,如果 WBE 成功,那么人脑软件可以在比人体更强大和更快的硬件上运行,潜在地导致超智能。软件也可以轻松复制,并且可以以协调的方式组合大量仿真大脑,同样可能导致超智能。人脑软件也可以以人类由于生物限制而无法实现的方式进行增强。

人工智能

最后但同样重要的是,就本书的背景而言,人工智能本身可能导致超智能:算法(如神经网络)在标准或专门的硬件上运行,并根据可用或自创数据进行训练。有很多理由可以解释为什么大多数研究人员和实践者认为,如果超智能是可实现的,这条路径可能是最有可能的路径之一。

第一个主要原因是,从历史上看,人类在工程领域的成功通常是通过忽略自然和进化为解决某一问题所提出的方法。考虑飞机。它们的设计利用了对现代物理学、空气动力学、热力学等的现代理解,而不是试图模仿鸟类或昆虫的飞行方式。或者考虑计算器。当工程师们建造第一台计算器时,并没有分析人脑如何执行计算,也没有试图复制生物学的方法。他们依赖的是在技术硬件上实现的数学算法。在这两种情况下,更重要的是功能或能力本身(飞行,计算)。它能够提供的更高效,就越好。没有必要模仿自然。

第二个主要原因是,AI 的成功案例数量似乎在不断增加。例如,将神经网络应用于几年前似乎对 AI 优势免疫的领域,已被证明是 ANI 在许多领域的一个富有成效的路径。AlphaGo 演变为 AlphaZero 的例子,在短时间内掌握多个棋盘游戏,为通用化的进一步推进提供了希望。

第三个主要原因是,超智能(“奇点”)可能只会在观察到许多 ANI 甚至一些 AGI 之后才出现。鉴于 AI 在特定领域和领域的力量毋庸置疑,研究人员和企业都将继续专注于改进 AI 算法和硬件。例如,大型对冲基金将推动其努力以 AI 方法和代理生成α(基金相对于市场基准的超额收益)。他们中的许多人都有专门的团队致力于这些工作。各行各业的全球努力可能会共同产生超智能所需的进展。

人工智能

在通向超智能的所有可能路径中,AI 似乎是最有前途的一条。基于强化学习和神经网络的最新成功在该领域引发了另一波 AI 春天,经历了几次 AI 冬天后。现在甚至有人认为,超智能可能离我们想象的时间不远了几年前。该领域目前的特点是,进展比专家们短时间前最初预测的要快得多。

智能爆炸

早前提到的 Vinge(1993)的引用不仅描绘了技术奇点后人类的危险情景,还预测这种危险情景将会很快实现。为什么会这么快?

如果有一个超智能存在,那么工程师或超智能本身可以创建另一个超智能,甚至可能是一个更好的超智能,因为超智能在技术知识和技能方面比初始超智能的创造者更为优越。超智能的复制不会受到生物进化数百万年时间限制的约束。它只会受到新硬件技术装配过程的限制,而超智能可以自行改进并显著提升。软件可以迅速轻松地复制到新硬件上。资源可能也会限制复制。超智能可能会想出更好甚至是新的方法来挖掘和生产所需的资源。

这些以及类似的论点支持这样的观点,即一旦技术奇点达到,智能将会爆炸增长。这可能类似于大爆炸,它开始于一个(物理)奇点,从中已知的宇宙如同从爆炸中出现一样。

关于特定领域和 ANIs,类似的论点可能适用。假设一个算法交易的 AI 代理在市场上的表现比其他交易者和对冲基金要成功和一致得多。这样的 AI 代理将通过交易收益和吸引外部资金积累更多资金。这反过来又会增加可用于改进硬件、算法、学习方法等的预算,例如支付高于市场水平的薪水和奖励,以吸引在金融应用 AI 领域最聪明的头脑。

目标与控制

在普通的 AI 背景下,例如,当一个 AI 代理被认为是掌握了如CartPole游戏或更复杂的游戏(如国际象棋或围棋)时,目标通常是定义明确的:“至少获得 200 分”,“通过将军赢得国际象棋比赛”等等。但是超智能的目标是什么呢?

超智能与目标

对于具有超人类能力的超智能,其目标可能不像前面的例子那样简单和稳定。首先,超智能可能会为自己设定一个新目标,认为这比其最初的设定和编程的目标更合适。毕竟,它有能力以与其工程团队相同的方式做到这一点。总体而言,它将能够在任何方面重新编程自己。许多科幻小说和电影让我们相信,这种主要目标的变化通常对人类不利,这也是文格(1993 年)的假设。

即使假设超智能的主要目标可以以不可改变的方式编程和嵌入,或者超智能可能只是坚持其原始目标,问题也可能出现。独立于主要目标,波斯特罗姆(2014 年,第七章)认为,每个超智能都有五个工具性子目标:

自我保存

足够长时间的超智能生存是实现其主要目标所必需的。为此,超智能可能会采取不同措施,其中一些可能对人类有害,以确保其生存。

目标内容完整性

这指的是超智能将努力保留其当前主要目标的想法,因为这增加了其未来自身实现此目标的可能性。因此,当前和未来的主要目标很可能是相同的。考虑一个下棋 AI 代理,最初的目标是赢得一局棋。它可能会改变目标,以任何代价避免其皇后被捕获。这可能最终阻止它赢得比赛,因此这种目标变化将是不一致的。

认知增强

无论超智能的主要目标是什么,认知增强通常都会带来益处。因此,如果认为这有助于实现其主要目标,它可能会努力尽快并尽可能地增加自己的能力。认知增强因此是一个重要的工具性目标。

技术完善

另一个工具性目标是技术完善。根据《生命 3.0》的理解,超智能不会局限于当前的硬件或软件状态。它可能会努力存在于它设计和制造的更好硬件上,并利用它编码的改进软件。这通常会服务于它的主要目标,并可能加快其实现速度。例如,在金融行业,高频交易(HFT)是一个以追求技术优势为特征的领域。

资源获取

对于几乎任何主要目标,更多的资源通常会增加实现目标的概率和速度。当目标中隐含竞争的情况下,这尤为真实。考虑一个以尽可能快尽可能多地挖掘比特币为目标的 AI 代理。该 AI 代理拥有的硬件、能源等资源越多,对于实现其目标越有利。在这种情况下,它甚至可能采取非法手段从加密货币市场中获取(窃取)资源。

表面上看,工具性目标似乎不会构成威胁。毕竟,它们确保了 AI 代理的主要目标的实现。然而,正如 Bostrom(2014)广泛引用的例子所示,问题可能很容易出现。Bostrom 认为,例如,一个以最大化纸夹生产为目标的超智能可能对人类构成严重威胁。为了理解这一点,考虑在这样一个 AI 代理的背景下前面的工具性目标。

首先,它会尽一切手段保护自己,甚至使用武器对抗其创造者。其次,即使其自身的认知推理能力可能表明其主要目标并不真正合理,它可能会随着时间的推移坚持下去,以最大化实现其目标的机会。第三,认知增强肯定对实现其目标有价值。因此,它将尝试各种措施,可能其中许多措施会以牺牲和伤害人类为代价,以提高其能力。第四,对于自身和生产回形针而言,技术越先进越好,这对其主要目标越有利。因此,它将通过购买或窃取获取所有现有技术,并建立新技术来帮助其实现目标。最后,它拥有的资源越多,可以生产的回形针就越多——直到在地球资源耗尽时建立太空探索和采矿技术。在极端情况下,这样的超智能可能耗尽太阳系、银河甚至整个宇宙的资源。

工具性目标

假设任何形式的超智能都会有其独立于主要目标的工具性目标。这可能会导致一系列意想不到的后果,比如对以任何看似有前景的手段获取更多资源的贪得无厌。

该例子说明了关于 AI 代理目标的两个重要观点。首先,可能无法以一种完全清晰地反映制定目标人意图的方式为 AI 代理制定复杂的目标。例如,像“保护和保护人类物种”这样的高尚目标可能会导致杀死四分之三的人类,以确保其余四分之一的存活机会更高。超智能在对地球未来和人类物种进行数十亿次模拟后决定,这一措施最有可能实现其主要目标。其次,一个看似出于善意和无害的目标可能由于工具性目标而导致意想不到的后果。在回形针的例子中,一个问题是“尽可能多地生产”。在这里很容易修正,比如将数量明确为一百万。但即使如此,这可能只是部分修正,因为工具性目标,比如自我保护,可能会成为主要目标之一。

超智能与控制

如果在技术奇点之后存在不良甚至灾难性的后果,制定至少潜在可以控制超智能的措施至关重要。

第一组措施与主要目标的正确制定和设计相关。前一节在一定程度上讨论了这个方面。博斯特罗姆(2014 年,第九章)在“动机选择方法”一章中提供了更多细节。

第二组措施与控制超智能的能力有关。博斯特罗姆(2014 年,第九章)勾画了四种基本方法。

包装

这是一种将正在出现的超智能与外部世界分开的方法。例如,AI 代理可能没有连接到互联网。它也可能缺乏任何感官能力。人类互动也可能被排除在外。鉴于这种控制能力的方法,可能根本无法实现一大套有趣的目标。考虑一个算法交易的 AI 代理,它被设计为达到 ANI 水平。如果没有连接到外部世界,比如股票交易平台,AI 代理就无法实现其目标。

激励

AI 代理可能被编程为最大化其奖励功能,用于特意设计的(电子)奖励,奖励期望行为并惩罚不良行为。虽然这种间接方法在目标设计上更自由,但在很大程度上遭受直接制定目标类似的问题。

衰退

这种方法指的是有意限制 AI 代理的能力,比如硬件、计算速度或内存方面。然而,这是一项微妙的任务。太多的衰退会导致超智能永远不会出现。而太少的衰退则会导致随后的智能爆炸使这项措施变得过时。

绊脚石

这指的是应该帮助及早发现任何可疑或不良行为的措施,以便可以启动有针对性的对策。然而,这种方法遭遇了警报系统通知警方有入室盗窃的问题。即使监控摄像头的录像也可能无法帮助确定入室盗窃的是谁。

能力控制

总的来说,当超智能达到那个水平时,是否能够适当而系统地控制它似乎是值得质疑的。毕竟,它的超能力至少在原则上可以用来克服任何人类设计的控制机制。

潜在结果

除了文奇(1993 年)早期预言超智能的出现将意味着人类的末日之外,还有哪些可能的结果和情景是可以想象的?

越来越多的人工智能研究者和实践者警告说,未受控制的人工智能可能带来潜在威胁。在超智能出现之前,人工智能可能导致歧视、社会失衡、金融风险等问题。(在这一背景下,一个著名的人工智能批评者是特斯拉、SpaceX 和上述的神经链接等公司的创始人埃隆·马斯克。)因此,人工智能的伦理和治理成为研究者和从业者之间积极讨论的话题。为了简化问题,可以说这些人担心人工智能引发的反乌托邦。其他人,如雷·库兹韦尔(2005 年,2012 年),强调人工智能可能是通向乌托邦的唯一途径。

在这种情况下的问题是,即使是相对较低的反乌托邦结果的概率也足以令人担忧。正如前一节所示,考虑到现有技术状态,可能没有适当的控制机制可用。在这种背景下,毫不奇怪,在撰写本文时,42 个国家已经签署了关于人工智能发展的第一份国际协议。

正如 Murgia 和 Shrikanth(2019 年)在《金融时报》中报道的:

在上周的一次历史性步骤中,42 个国家联合起来支持了全球治理框架,以应对我们这个时代最强大的新兴技术之一——人工智能。

此次协议由包括美国、英国和日本在内的 OECD 国家以及非成员国家签署,正值各国政府在工业中应用人工智能的道德和实际后果刚刚开始认真对待的关键时刻……过去几年来,像谷歌、亚马逊、百度、腾讯和字节跳动等公司的人工智能的快速发展远远超过了该领域的监管,暴露出包括偏见人工智能决策、彻头彻尾的伪造和误导,以及自动化军事武器的危险在内的重大挑战。

乌托邦与反乌托邦

尽管是人工智能推动的乌托邦未来的坚定支持者,他们也必须承认,在技术奇点之后的反乌托邦未来是不能完全排除的。由于后果可能是灾难性的,反乌托邦的结果必须在关于人工智能和超智能的更广泛讨论中起作用。

那么在技术奇点之后,超智能的数量和情况如何?有三种基本情景似乎是可能的。

单一体

单一超智能出现并获得如此大的权力,以至于其他超智能甚至无法生存或出现。例如,谷歌主导搜索市场,并在该领域几乎达到垄断地位。超智能可能会在其出现后迅速在许多相关领域和行业达到可比的地位。

多极

多个超智能同时出现并在更长时间内共存。例如,对冲基金行业有几家大型参与者,可以被视为垄断,因为它们的市场份额合并起来。多个超智能也可以通过分治协议在一定时间内共存。

原子

在技术奇点之后不久,会出现大量的超智能。经济上,这种情景类似于完全竞争的市场。技术上,象棋的演变为这种情况提供了一个类比。1997 年,IBM 建造了一台机器来统治计算机和人类的象棋世界,而如今每部智能手机上的象棋应用程序都能胜过任何人类象棋选手。截至 2018 年,全球已有超过 30 亿部智能手机在使用。在这种情况下,值得注意的是,智能手机的最新硬件趋势是在常规 CPU 之外增加专用的 AI 芯片,不断增强这些小型设备的功能。

本节不会就技术奇点后的一个或多个潜在结果进行争论:反乌托邦、乌托邦、单体、多极或原子。相反,它提供了一个基本框架来思考超智能或其各自领域内强大的 ANIs 可能产生的潜在影响。

结论

近期的成功故事,比如 DeepMind 和 AlphaZero,导致了新的 AI 春天,带来了前所未有的希望,即超智能可能是可实现的。当前,AI 已经在不同领域的专家水平上开发出了远远超过人类的 ANIs。关于 AGIs 和超智能是否可能的问题仍在争论中。然而,至少不能排除通过某种路径——最近的经验表明 AI——确实可以实现。一旦技术奇点发生,也不能排除超智能可能对人类产生意外的、负面的甚至是灾难性的后果。因此,适当的目标和激励设计以及适当的控制机制可能对保持新兴的、越来越强大的 AI 代理在可见技术奇点之前就受到控制至关重要。一旦奇点达成,智能爆炸可能迅速将超智能的控制权夺出其创造者和赞助者的手中。

人工智能(AI)、机器学习、神经网络、超智能和技术奇点是当前或将来对人类生活任何领域至关重要的主题。今天,许多研究领域、许多行业以及人类生活的许多领域都因 AI、机器学习和深度学习而发生了根本性的变化。同样适用于金融和金融行业,尽管由于采用速度较慢,AI 对金融的影响可能还不那么显著。但是,正如后面的章节所论述的那样,AI 将从根本上和永久地改变金融和金融市场参与者的运作方式。

参考文献

本章引用的书籍和论文:

  • Barrat, James. 2013. Our Final Invention: Artificial Intelligence and The End of the Human Era. 纽约: St. Martin’s Press.

  • Bostrom, Nick. 2014. Superintelligence: Paths, Dangers, Strategies. 牛津: Oxford University Press.

  • Chollet, François. 2017. 使用 Python 进行深度学习。Shelter Island:Manning。

  • Domingos, Pedro. 2015. 大师算法:追求终极学习机器将改变我们的世界。英国:企鹅兰登书屋。

  • Doudna, Jennifer 和 Samuel H. Sternberg. 2017. 创世纪裂痕:控制进化的新力量。伦敦:博德利头出版社。

  • Gerrish, Sean. 2018. 智能机器如何思考。剑桥:麻省理工学院出版社。

  • Harari, Yuval Noah. 2015. 霍莫·德尤斯:未来简史。伦敦:哈维尔·塞克出版社。

  • Kasparov, Garry. 2017. 深度思考:机器智能的极限在哪里。伦敦:约翰·穆雷。

  • Kurzweil, Ray. 2005. 奇点临近:当人类超越生物学。纽约:企鹅集团。

  • ⸻. 2012. 如何创造心灵:揭示人类思维的秘密。纽约:企鹅集团。

  • Mnih, Volodymyr et al. 2013. “使用深度强化学习玩 Atari 游戏。” arXiv。2013 年 12 月 19 日。https://oreil.ly/HD20U

  • Murgia, Madhumita 和 Siddarth Shrikanth. 2019. “政府如何开始监管人工智能。” 金融时报,2019 年 5 月 30 日。

  • Silver, David et al. 2016. “使用深度神经网络和树搜索掌握围棋。” 自然,529(一月):484-489。

  • ⸻. 2017a. “通过自我对弈掌握象棋和将棋的一般强化学习算法。” arXiv。2017 年 12 月 5 日。https://oreil.ly/SBrWQ

  • ⸻. 2017b. “无需人类知识掌握围棋。” 自然,550(十月):354–359. https://oreil.ly/lB8DH

  • Shanahan, Murray. 2015. 技术奇点。剑桥:麻省理工学院出版社。

  • Tegmark, Max. 2017. 生命 3.0:在人工智能时代作为人类存在。英国:企鹅兰登书屋。

  • Vinge, Vernor. 1993. “关于奇点的文论。” https://oreil.ly/NaorT

¹ 关于背景和历史信息,请参阅 http://bit.ly/aiif_atari

² 详细信息请参阅 Mnih 等人(2013)。

³ 其他因素包括通过标准化 API 训练 AI 代理程序的 Arcade Learning Environment (ALE) 的可用性。

⁴ 第九章更详细地重新讨论了这个例子。

⁵ 更具体地说,如果 AI 代理在 100 场连续比赛中达到 195 或更高的平均总奖励,则认为其成功。

⁶ 参见 http://bit.ly/aiif_1k_chess,获取 1983 年 2 月 Your Computer 上原文文章的电子重印和原始代码的扫描。

⁷ 欲了解更多背景,请参见 http://bit.ly/aiif_bootchess

⁸ 欲了解更多信息,请参见:https://oreil.ly/im174

⁹ 在表中,GPU 指图形处理单元。TPU 指张量处理单元,是一种专门设计的计算机芯片,用于更有效地处理所谓的张量和张量操作。关于张量的更多信息,作为神经网络和深度学习的基本构建块,稍后在本书和 Chollet(2017 年,第二章)中有所涉及。TDP 指热设计功耗(详见 http://bit.ly/aiif_tdp)。

¹⁰ CPU 指的是中央处理单元,是任何标准桌面或笔记本电脑中的通用处理器。

¹¹ 2018 年 GDDR6 GPU 内存标准的描述,请参考 http://bit.ly/aiif_gddr6

¹² 2019 年 1 月,一部名为《复制品》的美国科幻惊悚片,由基努·里维斯主演,美国上映。该电影的主题是人脑的映射及其转移至机器甚至克隆和复制生成的其他人体。这部电影触及了人类数百年来超越人体并实现不朽(至少是在精神和灵魂方面)的愿望。即使 WBE 可能不会导致超级智能,但在理论上它可能是实现这种不朽的基础。

第二部分:金融与机器学习

如果有一个行业可以从真正采纳人工智能中获益匪浅,那就是投资管理。

安吉洛·卡韦洛(2020 年)

本部分包括四章内容。涵盖了理解为何数据驱动金融、人工智能和机器学习对金融理论和实践将产生长远影响的核心主题。

  • 第三章通过介绍几十年来一直被视为金融学基石的重要和流行的金融理论和模型,为后续章节铺垫。其中涵盖了均值-方差组合理论(MVP 理论)和资本资产定价模型(CAPM)等内容。

  • 第四章讨论了历史数据和实时金融数据的程序化可用性如何将金融学从理论驱动转变为数据驱动学科。

  • 第五章探讨了机器学习作为一种通用方法,大大抽象了特定算法的内容。

  • 第六章在一般层面上讨论了数据驱动金融与人工智能及机器学习的结合如何引领金融学的范式转变。

第三章:规范金融

CAPM 基于许多不切实际的假设。例如,假设投资者只关心单期组合收益的均值和方差是极端的。

Eugene Fama 和 Kenneth French(2004)

与基本粒子不同,涉及人类的科学更难以使用优雅的数学方法解决问题。

Alon Halevy 等(2009)

本章回顾了主要的规范金融理论和模型。简单来说,对于本书而言,规范理论 是基于假设(数学上的公理)并从相关假设中得出洞见、结果等的理论。另一方面,实证理论 则是基于观察、实验、数据、关系等,并根据可获得信息和得出的结果描述现象。Rubinstein(2006)详细讲述了本章所介绍的理论和模型的起源历史。

“不确定性和风险” 介绍了来自金融建模的中心概念,如不确定性、风险、交易资产等。“期望效用理论” 讨论了在不确定性下进行决策的主要经济范式:期望效用理论(EUT)。在其现代形式下,EUT 可追溯到 von Neumann 和 Morgenstern(1944)。“均值-方差组合理论” 根据 Markowitz(1952)介绍了均值-方差组合(MVP)理论。“资本资产定价模型” 分析了根据 Sharpe(1964)和 Lintner(1965)的资本资产定价模型(CAPM)。“套利定价理论” 概述了根据 Ross(1971, 1976)的套利定价理论(APT)。

本章的目的是以中心的规范金融理论形式为本书其余部分做铺垫。这一点很重要,因为几代经济学家、金融分析师、资产管理者、交易员、银行家、会计师和其他人员都接受过这些理论的培训。从这个意义上说,可以说金融作为理论和实践学科在很大程度上受到了这些理论的影响。

不确定性和风险

金融理论的核心是在不确定性和风险存在的情况下进行投资、交易和估值。本节在一定程度上正式介绍了与这些主题相关的中心概念。重点是从概率论中构建量化金融的基础概念。¹

定义

假设经济体仅在两个时间点观察活动:今天,t = 0 ,一年后,t = 1 。本章后面讨论的财务理论在很大程度上基于这样一个静态经济。²

t = 0 时,完全没有任何不确定性。在 t = 1 时,经济体可以处于有限数量 S 的可能状态 ω Ω = { ω 1 , ω 2 , ... , ω S }Ω 被称为状态空间,其基数为 | Ω | = S

Ω 中的代数 是具有以下特征的集合族:

  1. Ω

  2. 𝔼 𝔼 c

  3. 𝔼 1 , 𝔼 2 , . . . , 𝔼 I i=1 I 𝔼 i

𝔼 c 表示集合 𝔼 的补集。幂集 ( Ω ) 是最大的代数结构,而集合 = { , Ω }Ω 中最小的代数。代数是经济学中可观察事件的模型。在此背景下,经济体的单一状态 ω Ω 可解释为原子事件

概率将实数 0 p ω P ( { ω } ) 1 分配给状态 ω Ω 或实数 0 P ( 𝔼 ) 1 分配给事件 𝔼 。如果所有状态的概率都已知,则有 P ( 𝔼 ) = ω𝔼 p ω

概率测度 P : [ 0 , 1 ] 具有以下特征:

  1. 𝔼 : P ( 𝔼 ) 0

  2. P i=1 I 𝔼 i = i=1 I 𝔼 i for disjoint sets 𝔼 i

  3. P ( Ω ) = 1

三个元素 { Ω , , P } 组成一个概率空间。概率空间是模型经济中不确定性的形式表示。如果概率测度 P 是固定的,经济处于风险状态。如果所有经济主体都知晓,则经济具有对称信息

给定一个概率空间 { Ω , , P },一个随机变量是一个函数 S : Ω + , ω S ( ω ),是 - 可测的。这意味着对于每个 𝔼 { a , b [ : a , b , a < b },有以下:

S -1 ( 𝔼 ) { ω Ω : S ( ω ) 𝔼 }

如果 ( Ω ) ,则随机变量的期望定义如下:

𝐄 P ( S ) = ωΩ P ( ω ) · S ( ω )

否则,它由以下定义:

𝐄 P ( S ) = 𝔼 P ( 𝔼 ) · S ( 𝔼 )

一般来说,金融经济被假定为完美。这意味着,除其他事项外,不存在交易成本,可用资产有固定价格并且数量无限,所有事情都发生在光速下,并且参与者拥有完全对称的信息。

数值示例

现在假设一个简单的静态经济在风险 { Ω , , P } 下,并且符合以下条件:

  1. Ω { u , d }

  2. ( Ω )

  3. P { P ( { u } ) = 1 2 , P ( { d } ) = 1 2 }

交易资产

在经济中,有两种资产在交易。第一种是风险资产,股票,今天的某一固定价格为 S 0 = 10,而明天的不确定回报则是一个随机变量:

S 1 = S 1 u = 20 if ω = u S 1 d = 5 if ω = d

第二种是无风险资产,债券,今天的某一固定价格为 B 0 = 10,而明天的确定回报如下:

B 1 = B 1 u = 11 if ω = u B 1 d = 11 if ω = d

形式上,模型经济可以写成 2 = ( { Ω , , P } , 𝔸 ) ,其中 𝔸 表示今天的价格向量 M 0 = (S 0 ,B 0 ) T ,明天的市场支付矩阵如下:

M 1 = S 1 u B 1 u S 1 d B 1 d

套利定价

在这样的经济环境中,例如可以解决如何计算股票的欧式看涨期权的公平价值,行权价为 K = 14.5. 欧式看涨期权的无套利价值 C 0 是通过一个由股票和债券组成的投资组合 C 1 复制期权的支付 ϕ 来确定的。复制投资组合的价格也必须等于欧式看涨期权的价格。否则,将可能存在(无限)套利利润。在 Python 中,利用这样的复制论证很容易实现:^([3)

In [1]: import numpy as np

In [2]: S0 = 10  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        B0 = 10  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: S1 = np.array((20, 5))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        B1 = np.array((11, 11))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [4]: M0 = np.array((S0, B0)) ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        M0  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[4]: array([10, 10])

In [5]: M1 = np.array((S1, B1)).T  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
        M1  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[5]: array([[20, 11],
               [ 5, 11]])

In [6]: K = 14.5  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [7]: C1 = np.maximum(S1 - K, 0) ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
        C1  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[7]: array([5.5, 0. ])

In [8]: phi = np.linalg.solve(M1, C1)  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
        phi  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
Out[8]: array([ 0.36666667, -0.16666667])

In [9]: np.allclose(C1, np.dot(M1, phi))  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
Out[9]: True

In [10]: C0 = np.dot(M0, phi)  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
         C0  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
Out[10]: 2.0

1

今天的股票和债券价格。

2

明天股票和债券的不确定支付。

3

市场价格向量。

4

市场支付矩阵。

5

期权的行权价。

6

期权的不确定支付。

7

复制投资组合 ϕ

8

检查其支付是否与期权的支付相同。

9

复制投资组合的价格即为期权的无套利价格。

套利定价

正如前面的例子所示,套利定价理论可以被认为是具有一些最强大数学结果之一的金融理论,例如资产定价的基本定理(FTAP)。⁴ 其中一个原因是,例如期权的价格可以从其他可观察的市场参数推导出来,比如期权所涉及的股票的股价。在这种意义上,套利定价不关心如何首先提出公平的股票价格,而只是将其作为输入。因此,套利定价已经使用了少量且温和的假设,比如无套利,而这在许多其他金融理论中并非如此。请注意,甚至概率测度也不用来推导套利价格。

预期效用理论

预期效用理论(EUT)是金融理论的基石之一。自 1940 年代制定以来,它一直是建模不确定情况下决策制定的中心范式之一。⁵ 基本上每本介绍金融理论和投资理论的入门教材都介绍了 EUT。其中一个原因是金融中的其他核心结果可以从 EUT 范式中推导出来。

假设和结果

EUT 是一种公理化理论,可以追溯到冯·诺依曼和莫根斯特恩(1944 年)的开创性工作。公理化在这里意味着理论的主要结果可以仅从少数公理推导出来。关于公理化效用理论、不同变体和应用的综述,请参阅费什本(1968 年)。

公理和规范理论

Wolfram MathWorld上,您可以找到公理的以下定义:“公理是被视为不需证明而显然成立的命题。”

EUT 通常基于与代理人在面对不确定选择时的偏好相关的少数主要公理。尽管公理的定义可能暗示了其他情况,但并非所有经济学家都认为所有公理都是“不需证明而显然成立的”。

冯·诺依曼和莫根斯特恩(1944 年,第 25 页)就公理的选择发表评论:

公理的选择并非纯粹客观的任务。通常希望从公理中得出某些明确的目标——可以从公理中推导出一些具体的定理或定理——在这个意义上,问题是确切和客观的。但除此之外,总会有其他重要但不太确切的愿望:公理不应过于繁多,它们的体系应尽可能简单和透明,并且每个公理应具有直观的意义,可以直接判断其适用性。

在这个意义上,一组公理构成了(部分)被该理论建模的世界的规范理论。这组公理收集了应该事先满足而不是通过某些形式证明或类似方式得到的最小假设。在列出导致期望效用理论的一组公理之前,这里先谈谈代理的偏好本身,正式地,当面对不确定性选择时。

代理的偏好

假设一个具有偏好的代理面临投资于模型经济的两种交易资产 2的问题。例如,代理可以选择在未来支付A = ϕ A · M 1的投资组合ϕ A或在未来支付B = ϕ B · M 1的投资组合ϕ B之间进行选择。假定代理的偏好是定义在未来支付上而不是在投资组合上的。如果代理(强烈地)更喜欢支付A而不是B,则写作A B,在另一种情况下写作A B。如果代理对这两个支付持中立态度,则写作A B。鉴于这些描述,导致期望效用理论的一组可能公理如下:

完备性

代理能够相对排列所有的支付。以下情况必须成立之一:A BA B,或A B

传递性

如果有第三个投资组合 ϕ C ,未来收益为 C = ϕ C · M 1 ,则从 A BB C 可得 A C

连续性

如果 A B C ,那么存在一个数 α [ 0 , 1 ] ,使得 B α A + ( 1 - α ) C

独立性

A B 可得 α A + ( 1 - α ) C α B + ( 1 - α ) C 。同样地,从 A B 可得 α A + ( 1 - α ) C α B + ( 1 - α ) C

优势

如果 C 1 = α 1 A + ( 1 - α 1 ) CC 2 = α 2 A + ( 1 - α 2 ) C ,由此可得 A CC 1 C 2 ,则 α 1 > α 2

实用函数

实用函数是以数学和数值方式表示代理人偏好 的一种方法,该函数为某种回报分配数值。在这种情况下,绝对值并不重要,而是由这些值引发的排序才是关键。⁶ 假设 𝕏 表示代理人可以表达偏好的所有可能回报。则实用函数 U 定义如下:

U : 𝕏 + , x U ( x )

如果 U 表示一个代理人的偏好 ,那么以下关系成立:

A B U ( A ) > U ( B ) (strongly prefers) A B U ( A ) U ( B ) (weakly prefers) A B U ( A ) < U ( B ) (strongly does not prefer) A B U ( A ) U ( B ) (weakly does not prefer) A B U ( A ) = U ( B ) (is indifferent)

实用函数 U 仅确定到正线性变换。因此,如果 U 表示偏好 ,那么 V = a + b U ,其中 a , b > 0 也如此。关于实用函数,冯·诺伊曼和莫根斯特恩(1944,第 25 页)总结如下:“因此我们看到:如果这样的实用度数值化确实存在的话,那么它就是在一线性变换下的确定。也就是说,实用度是一个数,它们之间只相差一个线性变换。”

预期实用函数

冯·诺伊曼和莫根斯特恩(1944)表明,如果一个代理人的偏好 满足前述五条公理,则存在一个期望效用函数,其形式为:

U : 𝕏 + , x 𝐄 P ( u ( x ) ) = ω Ω P ( ω ) u ( x ( ω ) )

在这里,u : , x u ( x ) 是一个单调递增且与状态无关的函数,通常称为伯努利效用,如 u ( x ) = ln ( x ) , u ( x ) = x ,或 u ( x ) = x 2

简言之,期望效用函数 U 首先将某一状态下的支付 x ( ω ) 应用于函数 u ,然后使用给定状态发生的概率 P ( ω ) 来加权单个效用。在线性伯努利效用的特殊情况下 u ( x ) = x ,期望效用简单地是状态依赖支付的期望值,U ( x ) = 𝐄 P ( x )

风险厌恶

在金融领域,风险厌恶的概念非常重要。最常用的风险厌恶度量是绝对风险厌恶(ARA)的阿罗-普拉特度量,该度量可以追溯到普拉特(1964)。假设代理人的状态无关伯努利效用函数为 u ( x ) ,则 ARA 的阿罗-普拉特度量由以下定义:

A R A ( x ) = - u '' (x) u ' (x) , x 0

根据此度量标准,可以区分以下三种情况:

A R A ( x ) = - u '' (x) u ' (x) > 0 risk-averse = 0 risk-neutral < 0 risk-loving

在金融理论和模型中,通常假设风险规避和风险中立是适当的情形。在赌博中,也可能存在风险爱好者。

考虑之前提到的三个伯努利函数:u ( x ) = ln ( x ) , u ( x ) = x ,或者 u ( x ) = x 2 。可以轻松验证它们分别模拟了风险规避、风险中立和风险爱好的代理人。例如,考虑 u ( x ) = x 2

- u '' (x) u ' (x) = - 2 2x < 0 , x > 0 risk-loving

数值示例

在 Python 中可以很容易地演示 EUT 的应用。假设前一节中的示例模型经济体 2 。假设一个具有偏好 的代理人根据 EUT 在不同未来收益之间做出决策。代理人的伯努利效用由 u ( x ) = x 给出。在示例中,来自投资组合 ϕ A 的收益 A 1 被优先于来自投资组合 ϕ D 的收益 D 1

以下代码展示了此应用:

In [11]: def u(x):
             return np.sqrt(x)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [12]: phi_A = np.array((0.75, 0.25))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         phi_D = np.array((0.25, 0.75))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [13]: np.dot(M0, phi_A) == np.dot(M0, phi_D)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[13]: True

In [14]: A1 = np.dot(M1, phi_A)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         A1  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[14]: array([17.75,  6.5 ])

In [15]: D1 = np.dot(M1, phi_D)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         D1  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[15]: array([13.25,  9.5 ])

In [16]: P = np.array((0.5, 0.5))  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [17]: def EUT(x):
             return np.dot(P, u(x))  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)

In [18]: EUT(A1)  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
Out[18]: 3.381292321692286

In [19]: EUT(D1)  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
Out[19]: 3.3611309730623735

1

风险规避的伯努利效用函数

2

两个具有不同权重的投资组合

3

显示每个投资组合的设置成本是相同的

4

一个投资组合的不确定收益…

5

…以及另一个

6

概率测度

7

期望效用函数

8

两个不确定收益的效用值

在这种背景下的典型问题是根据代理人的固定预算w > 0 推导出最优组合(即,最大化预期效用)。以下 Python 代码模拟并精确解决了这个问题。代理人的可用预算中,大约 60%用于风险资产,大约 40%用于无风险资产。结果主要受伯努利效用函数的特定形式驱动:

In [20]: from scipy.optimize import minimize

In [21]: w = 10  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [22]: cons = {'type': 'eq', 'fun': lambda phi: np.dot(M0, phi) - w}  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [23]: def EUT_(phi):
             x = np.dot(M1, phi)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             return EUT(x)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [24]: opt = minimize(lambda phi: -EUT_(phi),  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                        x0=phi_A,  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                        constraints=cons)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [25]: opt  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
Out[25]:      fun: -3.385015999493397
              jac: array([-1.69249132, -1.69253424])
          message: 'Optimization terminated successfully.'
             nfev: 16
              nit: 4
             njev: 4
           status: 0
          success: True
                x: array([0.61122474, 0.38877526])

In [26]: EUT_(opt['x'])  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
Out[26]: 3.385015999493397

1

代理人的固定预算

2

用于minimize的预算约束⁷

3

定义在组合上的预期效用函数

4

最小化-EUT_(phi)最大化EUT_(phi)

5

优化的初始猜测

6

应用的预算约束

7

包括在x下的最佳组合的最佳结果

8

给定w = 10 的预算下的最优(最高)预期效用

平均-方差组合理论

平均-方差组合(MVP)理论,根据马科维茨(1952 年)的说法,是金融理论中的另一个基石。这是关于不确定性下投资的最早的理论之一,它仅关注统计指标来构建股票投资组合。 MVP 完全抽象了一个公司的基本面可能驱动其股票表现或者关于未来竞争力可能对公司增长前景重要的假设。基本上,唯一计算的输入数据是股票价格的时间序列及其导出的统计数据,如(历史)年化平均回报和(历史)年化回报的方差。

假设和结果

MVP 的中心假设,根据马科维茨(1952 年)的说法,是投资者关心预期回报和这些回报的方差:

我们接下来考虑投资者认为(或应该认为)预期回报是可取的,而回报的方差是不可取的。这一规则在投资行为方面有许多合理的观点,既是一种原则,也是一种关于投资行为的假设。

最大预期回报的组合未必是方差最小的组合。投资者可以通过承担方差来获得预期回报,或者通过放弃预期回报来降低方差。

这种对投资者偏好的处理方法与定义代理人直接通过支付的偏好和效用函数的方法有很大不同。MVP 更倾向于假设代理人的偏好和效用函数可以定义在投资组合预期产生的回报的第一和第二时刻上。

隐含假设的正态分布

总体上,MVP 理论集中于单期投资组合的风险和回报,与标准 EUT 不兼容。解决这个问题的一种方式是假设风险资产的回报服从正态分布,使得第一和第二时刻足以描述资产回报的完整分布。这在真实金融数据中几乎从未观察到,如下一章所示。另一种方式是假设特定的二次伯努利效用函数,如下一节所示。

投资组合统计数据

假设静态经济 N = ( { Ω , , P } , 𝔸 ) ,其中可交易资产集合 𝔸 包括 N 种风险资产,即 A 1 , A 2 , ... , A N 。其中 A 0 n 表示今天资产 n 的固定价格,A 1 n 表示其一年后的回报,资产 n 的(简单)净收益向量 r n 定义如下:

r n = A 1 n A 0 n - 1

对于所有未来可能发生的状态,资产 n预期回报为:

μ n = 1 |Ω| ω Ω r n ( ω )

因此,预期回报向量如下:

μ = μ 1 μ 2 μ N

一个投资组合(向量)ϕ = ϕ 1 ,ϕ 2 ,...,ϕ N T,其中ϕ n 0 n N ϕ n = 1,为投资组合中每个资产分配权重。⁸

投资组合的预期回报由投资组合权重向量与预期回报向量的点积给出:

μ phi = ϕ · μ

现在定义资产nm之间的协方差如下:

σ mn = ω Ω r m ( ω ) - μ m r n ( ω ) - μ n

然后,协方差矩阵如下给出:

Σ = σ 11 σ 12 ... σ 1n σ 21 σ 22 ... σ 2n σ n1 σ n2 ... σ nn

投资组合的预期方差随后由双重点积给出:

φ phi = ϕ T · Σ · ϕ

因此,投资组合的预期波动率如下:

σ phi = φ phi

Sharpe 比率

Sharpe(1966 年)引入了一个用于评估互惠基金和其他投资组合甚至单一风险资产风险调整绩效的指标。在其最简单的形式中,它将投资组合的(预期、实现)回报与其(预期、实现)波动性联系起来。形式上,Sharpe 比率可以如下定义:

π phi = μ phi σ phi

如果r表示无风险短期利率,则投资组合p h i风险溢价超额回报定义为μ phi - r。在 Sharpe 比率的另一个版本中,这个风险溢价是分子:

π phi = μ phi -r σ phi

如果无风险短期利率相对较低,则如果应用相同的无风险短期利率,两个版本不会产生太大的数值差异。特别是在根据 Sharpe 比率对不同投资组合进行排名时,两个版本应该生成相同的排名顺序,其他一切都相等。

数值示例

回到静态模型经济 2,可以通过使用 Python 轻松地再次说明 MVP 的基本概念。

投资组合统计

首先,这是投资组合预期回报的推导:

In [27]: rS = S1 / S0 - 1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         rS  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[27]: array([ 1. , -0.5])

In [28]: rB = B1 / B0 - 1  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         rB  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[28]: array([0.1, 0.1])

In [29]: def mu(rX):
             return np.dot(P, rX)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [30]: mu(rS)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[30]: 0.25

In [31]: mu(rB)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[31]: 0.10000000000000009

In [32]: rM = M1 / M0 - 1  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         rM  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[32]: array([[ 1. ,  0.1],
                [-0.5,  0.1]])

In [33]: mu(rM)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[33]: array([0.25, 0.1 ])

1

风险资产的回报向量

2

无风险资产的回报向量

3

预期收益函数

4

交易资产的预期回报

5

交易资产的回报矩阵

6

预期收益向量

第二,方差波动性,以及 协方差矩阵

In [34]: def var(rX):
             return ((rX - mu(rX)) ** 2).mean()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [35]: var(rS)
Out[35]: 0.5625

In [36]: var(rB)
Out[36]: 0.0

In [37]: def sigma(rX):
             return np.sqrt(var(rX))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [38]: sigma(rS)
Out[38]: 0.75

In [39]: sigma(rB)
Out[39]: 0.0

In [40]: np.cov(rM.T, aweights=P, ddof=0)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[40]: array([[0.5625, 0.    ],
                [0.    , 0.    ]])

1

方差函数

2

波动率函数

3

协方差矩阵

第三,投资组合预期回报投资组合预期方差投资组合预期波动率,以等权重投资组合为例:

In [41]: phi = np.array((0.5, 0.5))

In [42]: def mu_phi(phi):
             return np.dot(phi, mu(rM))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [43]: mu_phi(phi)
Out[43]: 0.17500000000000004

In [44]: def var_phi(phi):
             cv = np.cov(rM.T, aweights=P, ddof=0)
             return np.dot(phi, np.dot(cv, phi))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [45]: var_phi(phi)
Out[45]: 0.140625

In [46]: def sigma_phi(phi):
             return var_phi(phi) ** 0.5  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [47]: sigma_phi(phi)
Out[47]: 0.375

1

投资组合预期回报

2

投资组合预期方差

3

投资组合预期波动率

投资机会集

基于蒙特卡洛模拟的投资组合权重 ϕ ,可以在波动率-回报空间中可视化投资机会集(由以下代码片段生成)。

aiif 0301

图 3-1. 模拟预期投资组合波动率和回报(一个风险资产)

因为只有一个风险资产和一个无风险资产,机会集是一条直线:

In [48]: from pylab import plt, mpl
         plt.style.use('seaborn')
         mpl.rcParams['savefig.dpi'] = 300
         mpl.rcParams['font.family'] = 'serif'

In [49]: phi_mcs = np.random.random((2, 200))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [50]: phi_mcs = (phi_mcs / phi_mcs.sum(axis=0)).T  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [51]: mcs = np.array([(sigma_phi(phi), mu_phi(phi))
                         for phi in phi_mcs])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [52]: plt.figure(figsize=(10, 6))
         plt.plot(mcs[:, 0], mcs[:, 1], 'ro')
         plt.xlabel('expected volatility')
         plt.ylabel('expected return');

1

归一化到 1 的随机投资组合构成

2

随机组合的预期投资组合波动率和回报

现在考虑一个静态的三状态经济 3 ,其中 Ω = { u , m , d } 成立。三种状态等可能发生,P = 1 3 , 1 3 , 1 3 。可交易资产组合包括两个风险资产 ST,其固定价格为 S 0 = T 0 = 10,分别具有不确定的回报:

S 1 = 20 10 5

T 1 = 1 12 13

基于这些假设,以下 Python 代码重复 Monte Carlo 模拟并在图 3-2 中可视化结果。具有两个风险资产时,著名的 MVP“子弹”变得可见。

In [53]: P = np.ones(3) / 3  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         P  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[53]: array([0.33333333, 0.33333333, 0.33333333])

In [54]: S1 = np.array((20, 10, 5))

In [55]: T0 = 10
         T1 = np.array((1, 12, 13))

In [56]: M0 = np.array((S0, T0))
         M0
Out[56]: array([10, 10])

In [57]: M1 = np.array((S1, T1)).T
         M1
Out[57]: array([[20,  1],
                [10, 12],
                [ 5, 13]])

In [58]: rM = M1 / M0 - 1
         rM
Out[58]: array([[ 1. , -0.9],
                [ 0. ,  0.2],
                [-0.5,  0.3]])

In [59]: mcs = np.array([(sigma_phi(phi), mu_phi(phi))
                         for phi in phi_mcs])

In [60]: plt.figure(figsize=(10, 6))
         plt.plot(mcs[:, 0], mcs[:, 1], 'ro')
         plt.xlabel('expected volatility')
         plt.ylabel('expected return');

1

新的三状态概率测度

aiif 0302

图 3-2. 模拟预期投资组合波动性和回报(两个风险资产)

最小波动性和最大夏普比率

接下来,推导最小波动性(最小方差)和最大夏普比率投资组合。图 3-3 显示了这两个投资组合在风险-回报空间中的位置。

尽管风险资产 T 有负的预期回报,但它在最大化夏普比率投资组合中占有重要权重。这是由于降低投资组合风险的多样化效应,它降低了投资组合的预期回报:

In [61]: cons = {'type': 'eq', 'fun': lambda phi: np.sum(phi) - 1}

In [62]: bnds = ((0, 1), (0, 1))

In [63]: min_var = minimize(sigma_phi, (0.5, 0.5),
                            constraints=cons, bounds=bnds)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [64]: min_var
Out[64]:      fun: 0.07481322946910632
              jac: array([0.07426564, 0.07528945])
          message: 'Optimization terminated successfully.'
             nfev: 17
              nit: 4
             njev: 4
           status: 0
          success: True
                x: array([0.46511697, 0.53488303])

In [65]: def sharpe(phi):
             return mu_phi(phi) / sigma_phi(phi)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [66]: max_sharpe = minimize(lambda phi: -sharpe(phi), (0.5, 0.5),
                        constraints=cons, bounds=bnds)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [67]: max_sharpe
Out[67]:      fun: -0.2721654098971811
              jac: array([ 0.00012054, -0.00024174])
          message: 'Optimization terminated successfully.'
             nfev: 38
              nit: 9
             njev: 9
           status: 0
          success: True
                x: array([0.66731116, 0.33268884])

In [68]: plt.figure(figsize=(10, 6))
         plt.plot(mcs[:, 0], mcs[:, 1], 'ro', ms=5)
         plt.plot(sigma_phi(min_var['x']), mu_phi(min_var['x']),
                  '^', ms=12.5, label='minimum volatility')
         plt.plot(sigma_phi(max_sharpe['x']), mu_phi(max_sharpe['x']),
                  'v', ms=12.5, label='maximum Sharpe ratio')
         plt.xlabel('expected volatility')
         plt.ylabel('expected return')
         plt.legend();

1

最小化预期投资组合波动性

2

定义夏普比率函数,假设短期利率为 0

3

通过最小化其负值来最大化夏普比率

aiif 0303

图 3-3. 最小波动性和最大夏普比率投资组合

有效前沿

有效投资组合是指在给定预期风险(回报)下具有最大预期回报的投资组合。在图 3-3 中,所有那些预期回报低于最小风险投资组合的投资组合都是无效的。以下代码在风险-回报空间中推导出有效投资组合,并像图 3-4 中所示绘制它们。所有有效投资组合的集合被称为有效前沿,而代理人将仅选择位于有效前沿上的投资组合:

In [69]: cons = [{'type': 'eq', 'fun': lambda phi: np.sum(phi) - 1},
                {'type': 'eq', 'fun': lambda phi: mu_phi(phi) - target}]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [70]: bnds = ((0, 1), (0, 1))

In [71]: targets = np.linspace(mu_phi(min_var['x']), 0.16)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [72]: frontier = []
         for target in targets:
             phi_eff = minimize(sigma_phi, (0.5, 0.5),
                                constraints=cons, bounds=bnds)['x']  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             frontier.append((sigma_phi(phi_eff), mu_phi(phi_eff)))
         frontier = np.array(frontier)

In [73]: plt.figure(figsize=(10, 6))
         plt.plot(frontier[:, 0], frontier[:, 1], 'mo', ms=5,
                  label='efficient frontier')
         plt.plot(sigma_phi(min_var['x']), mu_phi(min_var['x']),
                  '^', ms=12.5, label='minimum volatility')
         plt.plot(sigma_phi(max_sharpe['x']), mu_phi(max_sharpe['x']),
                  'v', ms=12.5, label='maximum Sharpe ratio')
         plt.xlabel('expected volatility')
         plt.ylabel('expected return')
         plt.legend();

1

新约束修正了预期回报的目标水平。

2

生成目标预期回报的集合。

3

推导给定目标预期回报的最小波动性投资组合。

aiif 0304

图 3-4. 有效前沿

资本资产定价模型

资本资产定价模型(CAPM)是金融领域中最广为人知和应用广泛的模型之一。在其核心,它以线性方式将单只股票的预期回报与市场投资组合的预期回报关联起来,通常由广泛的股票指数(如标普 500)近似。该模型可以追溯到 Sharpe(1964)和 Lintner(1965)的开创性工作。Jones(2012,第九章)在 MVP 方面描述了 CAPM 如下:

资本市场理论是一种正面理论,它假设投资者的实际行为方式,而不是像现代投资组合理论(MVP)中那样假设投资者应该如何行动。将资本市场理论视为投资组合理论的延伸是合理的,但重要的是要理解,MVP 并不基于资本市场理论的有效性或无效性。

许多投资者感兴趣的特定均衡模型被称为资本资产定价模型,通常简称为 CAPM。它使我们能够评估个别证券的相关风险,以及评估风险与预期投资回报之间的关系。CAPM 之所以作为均衡模型具有吸引力,是因为其简单性及其推论。

假设与结果

假设来自前一节的静态模型经济 N = ( { Ω , , P } , 𝔸 ),其中N个交易资产和所有简化假设。在 CAPM 中,假设代理人根据 MVP 进行投资,仅关心一期内风险和回报的统计数据。

资本市场均衡中,所有可用资产都被所有代理人持有,市场清算。由于假设代理人使用 MVP 形成其有效投资组合,因此这意味着所有代理人必须持有相同的有效投资组合(在组成方面),因为对每个代理人来说可交易资产集是相同的。换句话说,市场组合(可交易资产集)必须位于有效前沿上。如果不是这种情况,市场就不能处于均衡状态。

如何获得资本市场均衡的机制?可交易资产的当前价格是确保市场清算的机制。如果代理人对可交易资产的需求不足,其价格将下降。如果需求高于供给,其价格将上升。如果价格设置正确,对每种可交易资产的需求和供给将相等。虽然 MVP 将可交易资产的价格视为给定,但 CAPM 是关于资产均衡价格的理论和模型,考虑其风险与回报特性,应当是

CAPM 假设存在(至少)一种无风险资产,每个代理人都可以投资任意金额,并获得无风险利率 r ¯ 。因此,在均衡状态下,每个代理人将持有市场组合和无风险资产的组合,这被称为 两基金分离定理 。⁹ 所有这类投资组合的集合称为 资本市场线 (CML)。图 3-5 示意地显示了 CML。位于市场组合右侧的投资组合仅在代理人被允许做空无风险资产和通过这种方式借钱时才能实现:

In [74]: plt.figure(figsize=(10, 6))
         plt.plot((0, 0.3), (0.01, 0.22), label='capital market line')
         plt.plot(0, 0.01, 'o', ms=9, label='risk-less asset')
         plt.plot(0.2, 0.15, '^', ms=9, label='market portfolio')
         plt.annotate('$(0, \\bar{r})$', (0, 0.01), (-0.01, 0.02))
         plt.annotate('$(\sigma_M, \mu_M)$', (0.2, 0.15), (0.19, 0.16))
         plt.xlabel('expected volatility')
         plt.ylabel('expected return')
         plt.legend();

aiif 0305

图 3-5. 资本市场线(CML)

如果 σ M , μ M 是市场组合的预期波动率和回报,那么将预期组合回报 μ 与预期波动率 σ 相关联的资本市场线由以下定义:

μ = r ¯ + μ M -r ¯ σ M σ

下面的表达式称为 市场风险价格

μ M -r ¯ σ M

它表达了在均衡状态下,需要多少更多的预期回报来承担一个单位的风险。

然后,资本资产定价模型(CAPM)将任何可交易风险资产的预期回报 n = 1 , 2 , ... , N 与市场组合的预期回报相关联,如下所示:

μ n = r ¯ + β n ( μ M - r ¯ )

在这里, β n 是由市场组合与风险资产 n 的协方差除以市场组合本身的方差定义的:

β n = σ M,n σ M 2

β n = 0 时,根据 CAPM 公式,预期回报为无风险利率。 β n 越高,风险资产的预期回报就越高。 β n 衡量的是 不可分散化风险 。这种类型的风险也称为 市场风险系统风险 。根据 CAPM,这是代理人唯一应该因此获得更高预期回报的风险。

数值示例

假设静态模型经济有来自之前的三种可能未来状态 3 = ( { Ω , , P } , 𝔸 ) 有以风险无关利率 r ¯ = 0.0025. 两种风险资产 S , T 的数量分别为 0.8 和 0.2。

资本市场线

Figure 3-6 显示了风险-回报空间中的有效边界、市场组合、无风险资产和结果资本市场线:

In [75]: phi_M = np.array((0.8, 0.2))

In [76]: mu_M = mu_phi(phi_M)
         mu_M
Out[76]: 0.10666666666666666

In [77]: sigma_M = sigma_phi(phi_M)
         sigma_M
Out[77]: 0.39474323581566567

In [78]: r = 0.0025

In [79]: plt.figure(figsize=(10, 6))
         plt.plot(frontier[:, 0], frontier[:, 1], 'm.', ms=5,
                  label='efficient frontier')
         plt.plot(0, r, 'o', ms=9, label='risk-less asset')
         plt.plot(sigma_M, mu_M, '^', ms=9, label='market portfolio')
         plt.plot((0, 0.6), (r, r + ((mu_M - r) / sigma_M) * 0.6),
                  'r', label='capital market line', lw=2.0)
         plt.annotate('$(0, \\bar{r})$', (0, r), (-0.015, r + 0.01))
         plt.annotate('$(\sigma_M, \mu_M)$', (sigma_M, mu_M),
                      (sigma_M - 0.025, mu_M + 0.01))
         plt.xlabel('expected volatility')
         plt.ylabel('expected return')
         plt.legend();

aiif 0306

图 3-6. 两种风险资产的资本市场线

最优投资组合

假设一个代理人的期望效用函数定义如下:

U : 𝕏 + , x 𝐄 P ( u ( x ) ) = 𝐄 P x - b 2 x 2

这里,b > 0 . 经过一些转换,期望效用函数可以表示为风险-回报组合:

U : + × + , ( σ , μ ) μ - b 2 ( σ 2 + μ 2 )

具体的二次效用函数

尽管 MVP 理论和 CAPM 都假设投资者只关心一个时期的投资组合风险和回报,但这种假设通常只与给定的伯努利效用函数的特定形式一致:二次效用。这种类型的伯努利函数几乎只在 MVP 理论的背景下提到和使用。除此之外,它的特定形式和特征通常被认为是不合适的。既不是资产回报正态分布的假设,也不是二次效用函数似乎是调和 EUT 与 MVP 理论和 CAPM 之间的不一致性的“优雅”方式。

代理人在 CML 上会选择什么样的投资组合?一个简单的效用最大化,在 Python 中实现,得出了答案。为此,固定参数b = 1

In [80]: def U(p):
             mu, sigma = p
             return mu - 1 / 2 * (sigma ** 2 + mu ** 2)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [81]: cons = {'type': 'eq',
                 'fun': lambda p: p[0] - (r + (mu_M - r) / sigma_M * p[1])}  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [82]: opt = minimize(lambda p: -U(p), (0.1, 0.3), constraints=cons)

In [83]: opt
Out[83]:      fun: -0.034885186826739426
              jac: array([-0.93256102,  0.24608851])
          message: 'Optimization terminated successfully.'
             nfev: 8
              nit: 2
             njev: 2
           status: 0
          success: True
                x: array([0.06743897, 0.2460885 ])

1

风险-回报空间中的效用函数

2

投资组合在 CML 上的条件

无差异曲线

通过视觉分析可以说明代理人的最优决策。为代理人确定一个效用水平,可以在风险-回报空间中绘制无差异曲线。当一个无差异曲线与 CML 相切时,找到了一个最优投资组合。任何其他无差异曲线(不触及 CML 或在 CML 上切割两次)都不能确定一个最优投资组合。

首先,这里有一些符号化 Python 代码,将风险-回报空间中的效用函数转化为固定效用水平v和固定参数值b之间的函数关系。图 3-7 展示了两条等效曲线。在这样的等效曲线上的每个( σ , μ )组合产生相同的效用;代理人对这样的投资组合是无差别的:

In [84]: from sympy import *
         init_printing(use_unicode=False, use_latex=False)

In [85]: mu, sigma, b, v = symbols('mu sigma b v')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [86]: sol = solve('mu - b / 2 * (sigma ** 2 + mu ** 2) - v', mu)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [87]: sol  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[87]:         _________________________     _________________________
                /    2      2                 /    2      2
          1 - \/  - b *sigma  - 2*b*v + 1   \/  - b *sigma  - 2*b*v + 1  + 1
         [--------------------------------, --------------------------------]
                         b                                 b

In [88]: u1 = sol[0].subs({'b': 1, 'v': 0.1})  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         u1
Out[88]:        ______________
               /            2
         1 - \/  0.8 - sigma

In [89]: u2 = sol[0].subs({'b': 1, 'v': 0.125})  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         u2
Out[89]:        _______________
               /             2
         1 - \/  0.75 - sigma

In [90]: f1 = lambdify(sigma, u1)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         f2 = lambdify(sigma, u2)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [91]: sigma_ = np.linspace(0.0, 0.5)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         u1_ = f1(sigma_)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         u2_ = f2(sigma_)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [92]: plt.figure(figsize=(10, 6))
         plt.plot(sigma_, u1_, label='$v=0.1$')
         plt.plot(sigma_, u2_, '--', label='$v=0.125$')
         plt.xlabel('expected volatility')
         plt.ylabel('expected return')
         plt.legend();

1

定义SymPy符号

2

解决μ的效用函数

3

用于b , v的数值值进行替代

4

根据生成的方程式生成可调用函数

5

指定用于评估函数的σ

6

评估两个不同效用水平的可调用函数

aiif 0307

图 3-7. 风险-回报空间中的等效曲线

接下来,需要将等效曲线与 CML 结合起来,以便直观地找出代理人的最佳投资组合选择。利用之前的数值优化结果,图 3-8 展示了最佳投资组合——即等效曲线与 CML 相切的点。图 3-8 显示,代理人确实选择了市场组合和无风险资产的混合:

In [93]: u = sol[0].subs({'b': 1, 'v': -opt['fun']})  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         u
Out[93]:        ____________________________
               /                          2
         1 - \/  0.930229626346521 - sigma

In [94]: f = lambdify(sigma, u)

In [95]: u_ = f(sigma_)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [96]: plt.figure(figsize=(10, 6))
         plt.plot(0, r, 'o', ms=9, label='risk-less asset')
         plt.plot(sigma_M, mu_M, '^', ms=9, label='market portfolio')
         plt.plot(opt['x'][1], opt['x'][0], 'v', ms=9, label='optimal portfolio')
         plt.plot((0, 0.5), (r, r + (mu_M - r) / sigma_M * 0.5),
                  label='capital market line', lw=2.0)
         plt.plot(sigma_, u_, '--', label='$v={}$'.format(-round(opt['fun'], 3)))
         plt.xlabel('expected volatility')
         plt.ylabel('expected return')
         plt.legend();

1

定义最优效用水平的等效曲线

2

求出用于绘制等效曲线的数值值

aiif 0308

图 3-8. CML 上的最优投资组合

此子节中讨论的主题通常在资本市场理论(CMT)下讨论。CAPM 是该理论的一部分,并将在下一章节中使用实际的金融时间序列数据进行说明。

套利定价理论

在 CAPM 的早期阶段,人们观察到并在金融文献中解决了其缺陷。CAPM 的主要泛化之一是套利定价理论(APT),如 Ross(1971)和 Ross(1976)所提出。Ross(1976)在其论文中如下定义:

本文的目的是深入研究 Ross (1971) 发展的资本资产定价套利模型。套利模型被提出作为对由 Sharpe、Lintner 和 Treynor 引入的均值方差资本资产定价模型的替代,后者已成为解释资本市场中风险资产观察现象的主要分析工具。

假设与结果

APT 是 CAPM 在多个风险因素上的泛化。从这个意义上说,APT 不假定市场组合是唯一相关的风险因素;相反,假设有多种类型的风险共同驱动股票的表现(预期收益)。这些风险因素可能包括大小、波动性、价值和动量。¹⁰ 除了这一主要差异外,该模型依赖于类似的假设,如市场完全、可以(无限制地)以相同的恒定利率进行借贷等等。

在其最初的动态版本中,如 Ross (1976) 所述,APT 的形式如下:

y t = a + B f t + ϵ t

在这里,y t 是时间 t 的观察变量向量 M ——比如,M 种不同股票的预期收益:

y t = y t 1 y t 2 y t M

a 是常量项的 M 向量:

a = a 1 a 2 a M

f t 是时间 tF 因子向量:

f t = f t 1 f t 2 f t F

B 是所谓的因子载荷的 M × F 矩阵:

B = b 11 b 12 ... b 1F b 21 b 22 ... b 2F b M1 b M2 ... b MF

最后,ϵ t 是足够独立的残差项向量 M

ϵ t = ϵ t 1 ϵ t 2 ϵ t M

Jones (2012, ch. 9) 描述了 CAPM 和 APT 之间的差异如下:

与 CAPM 或任何其他资产定价模型类似,APT 假设预期收益与风险之间存在关系。然而,它使用不同的假设和程序。非常重要的是,APT 不像 CAPM 那样对基础市场组合具有关键依赖性,后者预测只有市场风险影响预期收益。相反,APT 认识到多种类型的风险可能影响证券回报。

CAPM 和 APT 都是以线性方式将输出变量与相关的输入因素关联起来。从计量经济学的角度来看,这两个模型都是基于线性最小二乘(OLS)回归来实现的。虽然 CAPM 可以基于单变量线性 OLS 回归来实现,但 APT 需要多变量OLS 回归。

数值示例

下面的数值示例将 APT 表现为静态模型,尽管先前给出的公式是动态的。假设静态模型经济有来自上一节的三种可能的未来状态, 3 = ( { Ω , , P } , 𝔸 )。假设现在两个风险资产是经济中的相关风险因素,并引入第三个资产V,其未来回报如下:

V 1 = 12 15 7

虽然两个线性独立的向量,例如S 1 , T 1,无法构成 3的基础,但它们仍然可以用于 OLS 回归来近似回报V 1。以下 Python 代码实现了 OLS 回归:

In [97]: M1
Out[97]: array([[20,  1],
                [10, 12],
                [ 5, 13]])

In [98]: M0
Out[98]: array([10, 10])

In [99]: V1 = np.array((12, 15, 7))

In [100]: reg = np.linalg.lstsq(M1, V1, rcond=-1)[0]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
          reg  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[100]: array([0.6141665 , 0.50030531])

In [101]: np.dot(M1, reg)
Out[101]: array([12.78363525, 12.14532872,  9.57480155])

In [102]: np.dot(M1, reg) - V1  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[102]: array([ 0.78363525, -2.85467128,  2.57480155])

In [103]: V0 = np.dot(M0, reg)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
          V0  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[103]: 11.144718094850402

1

最优回归参数可以解释为因子载荷。

2

这两个因素不足以解释回报V 1;复制并非完美,残差值非零。

3

因子载荷可用于估算风险资产V的无套利价格V 0

很明显,这两个因素并不足以完全“解释”回报V 1。鉴于线性代数的标准结果,这并不令人意外。¹¹ 那么在模型经济中添加第三个风险因素U呢?假设第三个风险因素U由以下定义:

U 1 = 12 5 11

现在,三个风险因素一起可以完全(准确地)解释回报V 1

In [104]: U0 = 10
          U1 = np.array((12, 5, 11))

In [105]: M0_ = np.array((S0, T0, U0))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [106]: M1_ = np.concatenate((M1.T, np.array([U1,]))).T  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [107]: M1_  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[107]: array([[20,  1, 12],
                 [10, 12,  5],
                 [ 5, 13, 11]])

In [108]: np.linalg.matrix_rank(M1_)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[108]: 3

In [109]: reg = np.linalg.lstsq(M1_, V1, rcond=-1)[0]
          reg
Out[109]: array([ 0.9575179 ,  0.72553699, -0.65632458])

In [110]: np.allclose(np.dot(M1_, reg), V1)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[110]: True

In [111]: V0_ = np.dot(M0_, reg)
          V0_  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[111]: 10.267303102625307

1

增强型市场价格向量。

2

带有完整秩的增强型市场回报矩阵。

3

V 1 的精确复制。残差值为零。

4

风险资产V 的独特无套利价格。

此处的例子类似于“不确定性和风险”中提到的例子,即可以利用足够的风险因素(可交易资产)来推导出交易资产的无套利价格。APT 并不一定要求能够进行完美复制;其模型形式本身包含残差值。然而,如果可以进行完美复制,则残差项为零,就像前面提到的具有三个风险因素的例子一样。

结论

从 1940 年代到 1970 年代的一些早期理论和模型,特别是本章介绍的那些,仍然是金融教科书的核心内容,并且在金融实践中仍然被使用。其中一个原因是,这些大多数是规范性理论和模型对学生、学者和从业者同样具有很强的吸引力。它们在某种程度上“似乎很有道理”。使用 Python,可以轻松创建、分析和可视化这些模型的数值示例。

尽管像 MVP 和 CAPM 这样的理论和模型在学术上具有吸引力,易于实施且数学上优雅,令人惊讶的是它们今天仍然如此流行,原因有几个。首先,本章介绍的流行理论和模型几乎没有什么有意义的经验支持。其次,其中一些理论和模型在理论上甚至在多个方面存在一些不一致。第三,金融理论和建模领域持续进展,提供了可供选择的替代理论和模型。第四,现代计算和经验金融可以依赖几乎无限的数据来源和计算能力,使得简洁、简明和优雅的数学模型和结果越来越不重要。

下一章分析了本章介绍的一些理论和模型在真实金融数据基础上的应用。在量化金融的早期,数据是稀缺资源,而今天甚至学生都可以获取丰富的金融数据和开源工具,以便基于真实世界数据对金融理论和模型进行全面分析。经验金融一直是理论金融的重要姊妹学科。然而,金融理论通常在很大程度上推动了经验金融的发展。数据驱动的金融 这一新领域可能会导致理论和数据在金融中相对重要性的持久性转变。

参考文献

本章引用的书籍和论文:

  • Bender, Jennifer 等人,2013 年。《因子投资的基础》。MSCI 研究洞察http://bit.ly/aiif_factor_invest

  • Calvello, Angelo. 2020. “基金经理必须接受人工智能的颠覆。” 《金融时报》, 2020 年 1 月 15 日。http://bit.ly/aiif_ai_disrupt

  • Eichberger, Jürgen, 和 Ian R. Harper. 1997. Financial Economics. 纽约:牛津大学出版社。

  • Fishburn, Peter. 1968. “效用理论。” 管理科学 14 (5): 335-378。

  • Fama, Eugene F. 和 Kenneth R. French. 2004. “资本资产定价模型:理论与证据。” 经济展望 18 (3): 25-46。

  • Halevy, Alon, Peter Norvig, 和 Fernando Pereira. 2009. “数据的不合理有效性。” IEEE 智能系统, 专家意见。

  • Hilpisch, Yves. 2015. Python 衍生品分析:数据分析,模型,模拟,校准和对冲. Wiley Finance。

  • Jacod, Jean, 和 Philip Protter. 2004. 概率要点. 第 2 版。柏林:Springer。

  • Johnstone, David 和 Dennis Lindley. 2013. “均值-方差和预期效用:Borch 悖论。” 统计科学 28 (2): 223-237。

  • Jones, Charles P. 2012. 投资分析与管理. 第 12 版。霍博肯:John Wiley & Sons。

  • Karni, Edi. 2014. “预期效用和主观概率的公理基础。” 在 风险与不确定性经济学手册 中,编辑:Mark J. Machina 和 W. Kip Viscusi,1-39. 牛津:North Holland。

  • Lintner, John. 1965. “风险资产的估值和股票投资组合以及资本预算中的风险投资选择。” 经济与统计评论 47 (1): 13-37。

  • Markowitz, Harry. 1952. “Portfolio Selection.” Finance Journal 7 (1): 77-91.

  • Pratt, John W. 1964. “小规模和大规模中的风险厌恶。” 计量经济学 32 (1/2): 122-136。

  • Ross, Stephen A. 1971. “在大市场中任意偏好和分布下的组合与资本市场理论:均值-方差方法的普遍有效性。” 工作论文 No. 12-72,Rodney L. White 金融研究中心。

  • ⸻. 1976. “The Arbitrage Theory of Capital Asset Pricing.” 经济理论杂志 13: 341-360.

  • Rubinstein, Mark. 2006. 理论投资史—我的注释书目. 霍博肯:Wiley Finance。

  • Sharpe, William F. 1964. “资本资产价格:在风险条件下市场均衡理论。” Finance Journal 19 (3): 425-442。

  • ⸻. 1966. “共同基金绩效。” 商业杂志 39 (1): 119-138。

  • Varian, Hal R. 2010. 中级微观经济学:现代方法. 第 8 版。纽约和伦敦:W.W. Norton & Company。

  • von Neumann, John, 和 Oskar Morgenstern. 1944. Theory of Games and Economic Behavior. 普林斯顿:普林斯顿大学出版社。

¹ 参见 Jacod 和 Protter(2004 年)关于概率论的入门文本。

² 在一个动态的经济中,不确定性会逐渐在时间的推移中解决,比如,在今天和一年后的每一天。

³ 关于风险中性估值和套利估值的详细内容,请参阅 Hilpisch (2015, 第四章)。

⁴ 同样,请参阅 Hilpisch (2015, 第四章)及其所列参考文献。

⁵ 欲了解更多背景和详情,请参阅 Eichberger 和 Harper (1997, 第一章)或 Varian (2010, 第十二章)。

⁶ 一般称之为序数。街道上的房屋号码是序数的一个很好的例子。

⁷ 欲了解详情,请参阅http://bit.ly/aiif_minimize

⁸ 这些假设并非真正必要。例如,即使允许卖空,也不会显著改变分析。

⁹ 欲了解更多详情,请参阅 Jones (2012, 第九章)。

¹⁰ 欲了解实际应用中使用的因子的更多背景信息,请参阅 Bender 等人 (2013)。

¹¹ 当然,支付 V 1 可能(偶然地)位于两个因子支付向量 S 1 , T 1 的范围内。

第四章:数据驱动金融

如果人工智能是新的电力,那么大数据就是为发电机提供动力的石油。

李开复(2018)

如今,分析师们筛选非传统信息,如卫星图像和信用卡数据,或使用人工智能技术如机器学习和自然语言处理,从传统数据源如经济数据和财报转录中获得新的见解。

罗宾·威格尔斯沃斯(2019)

本章讨论了数据驱动金融的核心方面。在本书中,数据驱动金融被理解为主要由从数据中获取的见解驱动的金融背景(理论、模型、应用等)。

“科学方法”讨论了指导科学努力的普遍接受原则。“金融计量与回归”探讨了金融计量及相关主题。“数据可用性”阐明了通过编程接口可获得哪些类型(金融)数据,以及其质量和数量。“规范理论再探讨”重新审视了第三章中的规范金融理论,并基于真实的金融时间序列数据进行了分析。同时,基于真实的金融数据,“揭穿中心假设”驳斥了金融模型和理论中两种最常见的假设:收益正态性线性 关系

科学方法

科学方法指的是应该指导任何科学项目的一套普遍接受的原则。维基百科对科学方法的定义如下:

科学方法是一种自 17 世纪以来就标志着科学发展的经验主义知识获取方法。它涉及仔细观察,对所观察到的事物应用严格的怀疑,因为认知假设可能会扭曲一个人对观察的解释。它包括根据这些观察通过归纳形成假设;基于假设所作的实验和基于测量的测试所得出的演绎;以及根据实验结果对假设进行修正(或淘汰)。这些都是科学方法的原则,不同于适用于所有科学企业的一系列确定步骤。

根据这一定义,如第三章中所述的规范金融与科学方法形成鲜明对比。规范金融理论大多依赖于假设和公设,结合演绎作为主要的分析方法来得出其核心结果。

  • 预期效用理论(EUT)假设代理人在不管世界状态如何变化时都具有相同的效用函数,并在不确定条件下最大化预期效用。

  • 均值方差组合(MVP)理论描述了投资者在不确定条件下应该如何投资,假设只有一个期间内组合的预期收益和预期波动性起作用。

  • 资本资产定价模型(CAPM)假设只有不可分散的市场风险解释了一个期间内股票的预期收益和预期波动性。

  • 套利定价理论(APT)假设可以用一些可识别的风险因素解释股票随时间的预期收益和预期波动性;诚然,与其他理论相比,APT 的制定相当宽泛,允许广泛的解释。

上述规范金融理论的特征在于它们最初是在某些假设和公理的基础上仅使用“纸和笔”推导出来的,没有任何对真实世界数据或观察的求助。从历史的角度来看,许多这些理论在它们发布后很长一段时间才被真实世界数据严格测试。这主要可以通过随着时间的推移数据可用性的提高和计算能力的增强来解释。毕竟,数据和计算是在实践中应用统计方法的主要要素。将这些方法应用于金融市场数据的数学、统计学和金融交叉学科通常称为金融计量,这是下一节的主题。

金融计量与回归

采用Investopedia提供的定义适应计量经济学的定义,可以将金融计量定义如下:

[金融]计量是使用[金融]数据对统计和数学模型进行量化应用,以发展金融理论或测试现有金融假设,并从历史数据中预测未来趋势。它对真实世界的[金融]数据进行统计试验,然后将结果与正在测试的[金融]理论进行比较和对比。

Alexander(2008b)为金融计量领域提供了全面而广泛的介绍。该书的第二章涵盖了单因素和多因素模型,如 CAPM 和 APT。Alexander(2008b)是名为市场风险分析的四本书系列的一部分。系列的第一本书,Alexander(2008a),涵盖了 MVP 理论和 CAPM 等的理论背景概念、主题和方法。Campbell(2018)的书是另一部关于金融理论和相关计量经济研究的全面资源。

金融计量学中的一个主要工具是回归,无论是其一元还是多元形式。回归也是一般统计学习中的核心工具。传统数学和统计学习之间有什么区别?虽然对于这个问题没有普遍的答案(毕竟,统计学是数学的一个子领域),一个简单的例子应强调与本书内容相关的一个主要区别。

首先是标准的数学方法。假设数学函数如下所示:

f : + , x 2 + 1 2 x

给定多个值 x i ,其中 i 等于 1、2、...、n,可以通过上述定义得到 f 的函数值:

y i = f ( x i ) , i = 1 , 2 , ... , n

下面的 Python 代码基于一个简单的数值例子说明了这一点:

In [1]: import numpy as np

In [2]: def f(x):
            return 2 + 1 / 2 * x

In [3]: x = np.arange(-4, 5)
        x
Out[3]: array([-4, -3, -2, -1,  0,  1,  2,  3,  4])

In [4]: y = f(x)
        y
Out[4]: array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. ])

其次是统计学习中采用的方法。在前面的例子中,函数先给出,然后数据派生出来,而在统计学习中,这个顺序是颠倒的。在这里,通常给定数据,需要找到一个函数关系。在这个背景下,x 经常被称为自变量,而 y 则被称为因变量。因此,请考虑以下数据:

( x i , y i ) , i = 1 , 2 , ... , n

问题是找到例如参数 α , β ,使得:

f ^ ( x i ) α + β x i = y ^ i y i , i = 1 , 2 , ... , n

另一种写法是包括残差值 ϵ i

α + β x i + ϵ i = y i , i = 1 , 2 , ... , n

在普通最小二乘(OLS)回归的背景下,选择 α , β 使得近似值 y ^ i 与实际值 y i 之间的均方误差最小化问题如下:

min α,β 1 n i n (y ^ i -y i ) 2

简单的 OLS 回归的情况下,如前所述,最优解以封闭形式已知,如下所示:

β = Cov(x,y) Var(x) α = y ¯ - β x ¯

这里,Cov ( )代表协方差Var ( )代表方差,而x ¯ , y ¯代表x , y均值

回到前面的数值示例,这些见解可以用于导出最优参数α , β,在这种特定情况下,还可以恢复f ( x )的原始定义:

In [5]: x
Out[5]: array([-4, -3, -2, -1,  0,  1,  2,  3,  4])

In [6]: y
Out[6]: array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. ])

In [7]: beta = np.cov(x, y, ddof=0)[0, 1] / x.var()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        beta  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[7]: 0.49999999999999994

In [8]: alpha = y.mean() - beta * x.mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        alpha  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[8]: 2.0

In [9]: y_ = alpha + beta * x  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [10]: np.allclose(y_, y)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[10]: True

1

从协方差矩阵和方差派生出的β

2

β和均值派生出的α

3

给定α , β,估计值y ^ i , i = 1 , 2 , ... , n

4

检查y ^ i , y i值是否数值上相等

前面的例子以及第一章中的例子说明,将 OLS 回归应用于给定数据集通常很简单。OLS 回归已成为计量经济学和金融计量学中的中心工具还有更多原因。其中包括:

数百年历史

最小二乘法,特别是与回归结合使用,已经使用了 200 多年。¹

简单性

OLS 回归背后的数学易于理解,并且易于在编程中实现。

可伸缩性

基本上不存在 OLS 回归可以应用的数据大小限制。

灵活性

OLS 回归可应用于广泛的问题和数据集。

速度

OLS 回归的评估速度快,即使在较大的数据集上也是如此。

可用性

Python 及许多其他编程语言中提供了高效的实现。

然而,尽管 OLS 回归方法通常易于应用且直接,但该方法基于一些假设——大多数与残差有关——在实践中并不总是满足。

线性

该模型在其参数方面是线性的,包括系数和残差。

独立性

自变量之间不存在完全(高度)相关(无多重共线性)。

零均值

残差的均值(接近)为零。

无相关性

残差与自变量没有(强烈)相关。

同方差性

残差的标准偏差(几乎)是恒定的。

无自相关

残差之间没有(强烈)相关性。

在实践中,一般很容易测试特定数据集的假设有效性。

数据可用性

金融计量经济学受统计方法驱动,如回归分析,以及金融数据的可用性。从 1950 年代到 1990 年代,甚至到 21 世纪初,理论和实证金融研究主要依赖相对较小的数据集,主要由日终数据(EOD)组成。过去十年左右,数据的可用性发生了巨大变化,金融和其他数据的类型越来越多,粒度、数量和速度不断增加。

编程 API

关于数据驱动的金融,重要的不仅是数据的可用性,还包括如何访问和处理数据。长期以来,金融专业人士依赖于 Refinitiv 等公司的数据终端(见Eikon Terminal)或 Bloomberg 等公司的数据终端(见Bloomberg Terminal)。报纸、杂志、财务报告等已被此类终端替代为主要的金融信息来源。然而,这些终端提供的数据量和种类之多无法由单个用户或大批金融专业人士系统地消化。因此,数据驱动金融的主要突破在于通过允许使用计算机代码选择、检索和处理任意数据集的应用程序接口(API)编程可用性

本节其余部分专注于展示此类 API,使得即使是学者和零售投资者也能检索到各种不同的数据集。在提供这些示例之前,Table 4-1 提供了一般金融背景下相关数据类别的概述,以及典型示例。在该表中,结构化数据指的是通常以表格结构呈现的数值数据类型,而非结构化数据则指的是以标准文本形式存在的数据,通常除了标题或段落之外没有结构。替代数据指的是通常被视为金融数据的数据类型。

表 4-1. 相关的金融数据类型

时间 结构化数据 非结构化数据 替代数据
历史 价格,基本面 新闻,文本 Web,社交媒体,卫星
流媒体 价格,成交量 新闻,文件 Web,社交媒体,卫星,物联网

结构化历史数据

首先,将通过程序检索结构化历史数据类型。为此,以下 Python 代码使用Eikon 数据 API。²

要通过 Eikon 数据 API 访问数据,必须运行本地应用程序,例如Refinitiv Workspace,并且必须在 Python 级别上配置 API 访问:

In [11]: import eikon as ek
         import configparser

In [12]: c = configparser.ConfigParser()
         c.read('../aiif.cfg')
         ek.set_app_key(c['eikon']['app_id'])
         2020-08-04 10:30:18,059 P[14938] [MainThread 4521459136] Error on handshake
          port 9000 : ReadTimeout(ReadTimeout())

如果满足这些要求,则可以通过单个函数调用检索历史结构化数据。例如,以下 Python 代码检索一组符号和指定时间间隔的 EOD 数据:

In [14]: symbols = ['AAPL.O', 'MSFT.O', 'NFLX.O', 'AMZN.O']  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [15]: data = ek.get_timeseries(symbols,
                                  fields='CLOSE',
                                  start_date='2019-07-01',
                                  end_date='2020-07-01')  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [16]: data.info()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 254 entries, 2019-07-01 to 2020-07-01
         Data columns (total 4 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   AAPL.O  254 non-null    float64
          1   MSFT.O  254 non-null    float64
          2   NFLX.O  254 non-null    float64
          3   AMZN.O  254 non-null    float64
         dtypes: float64(4)
         memory usage: 9.9 KB

In [17]: data.tail()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[17]: CLOSE       AAPL.O  MSFT.O  NFLX.O   AMZN.O
         Date
         2020-06-25  364.84  200.34  465.91  2754.58
         2020-06-26  353.63  196.33  443.40  2692.87
         2020-06-29  361.78  198.44  447.24  2680.38
         2020-06-30  364.80  203.51  455.04  2758.82
         2020-07-01  364.11  204.70  485.64  2878.70

1

定义要检索数据的RICs(符号)列表³

2

检索RICs列表的 EODClose价格

3

显示返回的DataFrame对象的元信息

4

显示DataFrame对象的最终行

同样地,通过适当调整参数可以检索带有OHLC字段的一分钟条。

In [18]: data = ek.get_timeseries('AMZN.O',
                                  fields='*',
                                  start_date='2020-08-03',
                                  end_date='2020-08-04',
                                  interval='minute')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [19]: data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 911 entries, 2020-08-03 08:01:00 to 2020-08-04 00:00:00
         Data columns (total 6 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   HIGH    911 non-null    float64
          1   LOW     911 non-null    float64
          2   OPEN    911 non-null    float64
          3   CLOSE   911 non-null    float64
          4   COUNT   911 non-null    float64
          5   VOLUME  911 non-null    float64
         dtypes: float64(6)
         memory usage: 49.8 KB

In [20]: data.head()
Out[20]: AMZN.O                  HIGH      LOW     OPEN    CLOSE  COUNT  VOLUME
         Date
         2020-08-03 08:01:00  3190.00  3176.03  3176.03  3178.17   18.0   383.0
         2020-08-03 08:02:00  3183.02  3176.03  3180.00  3177.01   15.0   513.0
         2020-08-03 08:03:00  3179.91  3177.05  3179.91  3177.05    5.0    14.0
         2020-08-03 08:04:00  3184.00  3179.91  3179.91  3184.00    8.0   102.0
         2020-08-03 08:05:00  3184.91  3182.91  3183.30  3184.00   12.0   403.0

1

检索具有所有可用字段的一分钟条OHLC数据

可以从 Eikon 数据 API 检索到不仅结构化金融时间序列数据。基础数据也可以同时为多个RICs和多个不同的数据字段检索,如以下 Python 代码所示:

In [21]: data_grid, err = ek.get_data(['AAPL.O', 'IBM', 'GOOG.O', 'AMZN.O'],
                                      ['TR.TotalReturnYTD', 'TR.WACCBeta',
                                       'YRHIGH', 'YRLOW',
                                       'TR.Ebitda', 'TR.GrossProfit'])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [22]: data_grid
Out[22]:   Instrument  YTD Total Return      Beta   YRHIGH      YRLOW        EBITDA  \
         0     AAPL.O         49.141271  1.221249   425.66   192.5800  7.647700e+10
         1        IBM         -5.019570  1.208156   158.75    90.5600  1.898600e+10
         2     GOOG.O         10.278829  1.067084  1586.99  1013.5361  4.757900e+10
         3     AMZN.O         68.406897  1.338106  3344.29  1626.0318  3.025600e+10

            Gross Profit
         0   98392000000
         1   36488000000
         2   89961000000
         3  114986000000

1

为多个RICs和多个数据字段检索数据

程序化数据可用性

如今,基本上所有的结构化金融数据都以编程方式可用。在这种情况下,金融时间序列数据是最重要的例子。然而,其他结构化数据类型如基本数据也以相同方式可用,极大地简化了量化分析师、交易员、投资组合经理等的工作。

结构化流式数据

金融领域的许多应用需要实时的结构化数据,例如算法交易或市场风险管理。以下 Python 代码利用Oanda 交易平台的 API 实时流式传输了比特币价格(美元)的时间戳、买入报价和卖出报价:

In [23]: import tpqoa

In [24]: oa = tpqoa.tpqoa('../aiif.cfg')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [25]: oa.stream_data('BTC_USD', stop=5)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         2020-08-04T08:30:38.621075583Z 11298.8 11334.8
         2020-08-04T08:30:50.485678488Z 11298.3 11334.3
         2020-08-04T08:30:50.801666847Z 11297.3 11333.3
         2020-08-04T08:30:51.326269990Z 11296.0 11332.0
         2020-08-04T08:30:54.423973431Z 11296.6 11332.6

1

连接到 Oanda API

2

为特定符号流式传输了固定数量的 ticks

当然,打印流式传输的数据字段仅供说明。某些金融应用可能需要对检索到的数据进行复杂处理,并生成信号或统计数据,特别是在工作日和交易时间内,金融工具的价格 tick 数逐步增加,需要金融机构在实时或至少在“接近实时”(“近实时”)处理这类数据时具备强大的数据处理能力。

当查看苹果公司股票价格时,这一观察的重要性变得明显。可以计算出在 40 年的时间内,苹果公司股票大约有 252 · 40 = 10 , 080 EOD 收盘报价。以下代码仅检索了一个小时的苹果股票的tick 数据。检索到的数据集可能甚至对于给定时间间隔来说都不完整,其中包含了 50,000 条数据行,比 40 年交易期间积累的 EOD 报价多五倍:

In [26]: data = ek.get_timeseries('AAPL.O',
                                  fields='*',
                                  start_date='2020-08-03 15:00:00',
                                  end_date='2020-08-03 16:00:00',
                                  interval='tick')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [27]: data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 50000 entries, 2020-08-03 15:26:24.889000 to 2020-08-03
          15:59:59.762000
         Data columns (total 2 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   VALUE   49953 non-null  float64
          1   VOLUME  50000 non-null  float64
         dtypes: float64(2)
         memory usage: 1.1 MB

In [28]: data.head()
Out[28]: AAPL.O                    VALUE  VOLUME
         Date
         2020-08-03 15:26:24.889  439.06   175.0
         2020-08-03 15:26:24.889  439.08     3.0
         2020-08-03 15:26:24.890  439.08   100.0
         2020-08-03 15:26:24.890  439.08     5.0
         2020-08-03 15:26:24.899  439.10    35.0

1

检索苹果股票价格的 tick 数据

EOD 与 Tick 数据的对比

大多数今天仍然适用的金融理论起源于只有 EOD 数据可用时的情况。如今,金融机构甚至包括零售交易者和投资者都面临着不断涌入的实时数据流。苹果股票的例子说明,在一个交易小时内,单只股票可能会收到四倍于 40 年交易期间 EOD 数据数量的 ticks。这不仅挑战了金融市场中的参与者,还引发了现有金融理论是否能够适应这样的环境的疑问。

非结构化历史数据

在金融中,许多重要的数据来源仅提供非结构化数据,如财经新闻或公司文件。毫无疑问,机器在处理大量结构化数值数据方面比人类更快更好。然而,最近在自然语言处理(NLP)方面的进展也使得机器在处理财经新闻方面更快更好。例如,2020 年,数据服务提供商每天摄取大约 150 万篇新闻文章。显然,这么庞大的基于文本的数据无法被人类适当地处理。

幸运的是,这些天大部分非结构化数据也可以通过程序化 API 轻松获取。以下 Python 代码从 Eikon 数据 API 检索与特斯拉公司及其生产相关的多篇新闻文章。选择一篇文章并完整显示:

In [29]: news = ek.get_news_headlines('R:TSLA.O PRODUCTION',
                                  date_from='2020-06-01',
                                  date_to='2020-08-01',
                                  count=7
                                 )  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [30]: news
Out[30]:                                           versionCreated  \
         2020-07-29 11:02:31.276 2020-07-29 11:02:31.276000+00:00
         2020-07-28 00:59:48.000        2020-07-28 00:59:48+00:00
         2020-07-23 21:20:36.090 2020-07-23 21:20:36.090000+00:00
         2020-07-23 08:22:17.000        2020-07-23 08:22:17+00:00
         2020-07-23 07:08:48.000        2020-07-23 07:46:56+00:00
         2020-07-23 00:55:54.000        2020-07-23 00:55:54+00:00
         2020-07-22 21:35:42.640 2020-07-22 22:13:26.597000+00:00

                                                                          text  \
         2020-07-29 11:02:31.276  Tesla Launches Hiring Spree in China as It Pre...
         2020-07-28 00:59:48.000    Tesla hiring in Shanghai as production ramps up
         2020-07-23 21:20:36.090     Tesla speeds up Model 3 production in Shanghai
         2020-07-23 08:22:17.000  UPDATE 1-'Please mine more nickel,' Musk urges...
         2020-07-23 07:08:48.000  'Please mine more nickel,' Musk urges as Tesla...
         2020-07-23 00:55:54.000  USA-Tesla choisit le Texas pour la production ...
         2020-07-22 21:35:42.640  TESLA INC - THE REAL LIMITATION ON TESLA GROWT...

                                                                       storyId  \
         2020-07-29 11:02:31.276  urn:newsml:reuters.com:20200729:nCXG3W8s9X:1
         2020-07-28 00:59:48.000  urn:newsml:reuters.com:20200728:nL3N2EY3PG:8
         2020-07-23 21:20:36.090  urn:newsml:reuters.com:20200723:nNRAcf1v8f:1
         2020-07-23 08:22:17.000  urn:newsml:reuters.com:20200723:nL3N2EU1P9:1
         2020-07-23 07:08:48.000  urn:newsml:reuters.com:20200723:nL3N2EU0HH:1
         2020-07-23 00:55:54.000  urn:newsml:reuters.com:20200723:nL5N2EU03M:1
         2020-07-22 21:35:42.640  urn:newsml:reuters.com:20200722:nFWN2ET120:2

                                 sourceCode
         2020-07-29 11:02:31.276  NS:CAIXIN
         2020-07-28 00:59:48.000    NS:RTRS
         2020-07-23 21:20:36.090  NS:SOUTHC
         2020-07-23 08:22:17.000    NS:RTRS
         2020-07-23 07:08:48.000    NS:RTRS
         2020-07-23 00:55:54.000    NS:RTRS
         2020-07-22 21:35:42.640    NS:RTRS

In [31]: storyId = news['storyId'][1]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [32]: from IPython.display import HTML

In [33]: HTML(ek.get_news_story(storyId)[:1148])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[33]: <IPython.core.display.HTML object>
Jan 06, 2020

Tesla, Inc.TSLA registered record production and deliveries of 104,891 and
112,000 vehicles, respectively, in the fourth quarter of 2019.

Notably, the company's Model S/X and Model 3 reported record production and
deliveries in the fourth quarter. The Model S/X division recorded production
and delivery volume of 17,933 and 19,450 vehicles, respectively. The Model 3
division registered production of 86,958 vehicles, while 92,550 vehicles were
delivered.

In 2019, Tesla delivered 367,500 vehicles, reflecting an increase of 50%, year
over year, and nearly in line with the company's full-year guidance of 360,000
vehicles.

1

获取在参数范围内的多篇新闻文章的元数据

2

选择一个storyId以检索完整文本

3

检索所选文章的完整文本并显示

非结构化流数据

就像检索历史非结构化数据一样,程序化 API 可以用来实时或至少接近实时地流式传输非结构化新闻数据。例如,道琼斯的数据、新闻和分析平台 DNA 提供这样的 API。图 4-1 展示了一个网络应用程序的截图,该应用程序实时流传“商品和财经新闻”文章,并使用 NLP 技术进行处理。

aiif 0401

图 4-1. 基于 DNA 的新闻流应用程序(道琼斯)

新闻流应用程序具有以下主要功能:

完整文本

点击文章标题可以获取每篇文章的完整文本。

关键词总结

关键词总结被创建并显示在屏幕上。

情感分析

情感分数被计算并可视化为彩色箭头。点击箭头可以查看详细信息。

词云

创建词云总结位图,并在点击缩略图后显示(见图 4-2)。

aiif 0402

图 4-2. 在新闻流应用程序中显示的词云位图

替代数据

如今,金融机构,特别是对冲基金,系统地开采多种替代数据来源,以在交易和投资中获得优势。彭博社的一篇最新文章列出,其中包括以下替代数据来源:

  • 网络抓取数据

  • 众包数据

  • 信用卡和销售点(POS)系统

  • 社交媒体情感

  • 搜索趋势

  • 网络流量

  • 供应链数据

  • 能源生产数据

  • 消费者档案

  • 卫星图像/地理空间数据

  • 应用程序安装数

  • 海洋船舶追踪

  • 可穿戴设备、无人机、物联网(IoT)传感器

以下两个示例说明了替代数据的使用。第一个示例检索并处理苹果公司的新闻稿,其形式为 HTML 页面。以下 Python 代码使用了一组辅助函数,如 “Python Code” 中所示。在代码中,定义了一个 URL 列表,每个 URL 代表苹果公司的一个新闻稿的 HTML 页面。然后检索每个新闻稿的原始 HTML 代码。接着清理原始代码,并打印一个新闻稿的摘录:

In [34]: import nlp  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         import requests

In [35]: sources = [
             'https://nr.apple.com/dE0b1T5G3u',  # iPad Pro
             'https://nr.apple.com/dE4c7T6g1K',  # MacBook Air
             'https://nr.apple.com/dE4q4r8A2A',  # Mac Mini
         ]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [36]: html = [requests.get(url).text for url in sources]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [37]: data = [nlp.clean_up_text(t) for t in html]  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [38]: data[0][536:1001]  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[38]: ' display, powerful a12x bionic chip and face id introducing the new ipad pro
          with all-screen design and next-generation performance. new york apple today
          introduced the new ipad pro with all-screen design and next-generation
          performance, marking the biggest change to ipad ever. the all-new design
          pushes 11-inch and 12.9-inch liquid retina displays to the edges of ipad pro
          and integrates face id to securely unlock ipad with just a glance.1 the a12x
          bionic chip w'

1

导入 NLP 辅助函数

2

定义了三篇新闻稿的 URL

3

检索三篇新闻稿的原始 HTML 代码

4

清理原始 HTML 代码(例如,删除 HTML 标签)

5

打印一个新闻稿的摘录

当然,在本节中如此广泛地定义替代数据,意味着可以检索和处理无限量的数据用于金融目的。从本质上讲,这是像 Google LLC 的搜索引擎的业务。在金融背景下,明确指定要利用的非结构化替代数据源至关重要。

第二个示例涉及从社交网络 Twitter, Inc. 中检索数据。为此,Twitter 提供 API 访问其平台上的推文,前提是已经适当设置了 Twitter 账户。以下 Python 代码连接到 Twitter API,并从我的主页时间线和用户时间线分别检索并打印最近的五条推文:

In [39]: from twitter import Twitter, OAuth

In [40]: t = Twitter(auth=OAuth(c['twitter']['access_token'],
                                c['twitter']['access_secret_token'],
                                c['twitter']['api_key'],
                                c['twitter']['api_secret_key']),
                     retry=True)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [41]: l = t.statuses.home_timeline(count=5)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [42]: for e in l:
             print(e['text'])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         The Bank of England is effectively subsidizing polluting industries in its
          pandemic rescue program, a think tank sa… https://t.co/Fq5jl2CIcp
         Cool shared task: mining scientific contributions (by @SeeTedTalk @SoerenAuer
          and Jennifer D'Souza)
         https://t.co/dm56DMUrWm
         Twelve people were hospitalized in Wyoming on Monday after a hot air balloon
          crash, officials said.

         Three hot air… https://t.co/EaNBBRXVar
         President Trump directed controversial Pentagon pick into new role with
          similar duties after nomination failed https://t.co/ZyXpPcJkcQ
         Company announcement: Revolut launches Open Banking for its 400,000 Italian...
          https://t.co/OfvbgwbeJW #fintech

In [43]: l = t.statuses.user_timeline(screen_name='dyjh', count=5)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [44]: for e in l:
             print(e['text'])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         #Python for #AlgoTrading (focus on the process) &amp; #AI in #Finance (focus
          on prediction methods) will complement eac… https://t.co/P1s8fXCp42
         Currently putting finishing touches on #AI in #Finance (@OReillyMedia). Book
          going into production shortly. https://t.co/JsOSA3sfBL
         Chinatown Is Coming Back, One Noodle at a Time https://t.co/In5kXNeVc5
         Alt data industry balloons as hedge funds strive for Covid edge via @FT |
         "We remain of the view that alternative d… https://t.co/9HtUOjoEdz
         @Wolf_Of_BTC Just follow me on Twitter (or LinkedIn). Then you will notice for
          sure when it is out.

1

连接到 Twitter API

2

检索并打印主页时间线上最近的五条推文

3

从用户时间线检索并打印最近的五条推文

Twitter API 也允许根据搜索条件检索最近的推文并进行处理:

In [45]: d = t.search.tweets(q='#Python', count=7)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [46]: for e in d['statuses']:
             print(e['text'])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         RT @KirkDBorne: #AI is Reshaping Programming — Tips on How to Stay on Top:
          https://t.co/CFNu1i352C
         ——
         Courses:
         1: #MachineLearning — Jupyte…
         RT @reuvenmlerner: Today, a #Python student's code didn't print:

         x = 5
         if x == 5:
             print: ('yes!')

         There was a typo, namely : after pr…
         RT @GavLaaaaaaaa: Javascript Does Not Need a StringBuilder
          https://t.co/aS7NzHLO65 #programming #softwareengineering #bigdata
          #datascience…
         RT @CodeFlawCo: It is necessary to publish regular updates on Twitter
          #programmer #coder #developer #technology RT @pak_aims: Learning to C…
         RT @GavLaaaaaaaa: Javascript Does Not Need a StringBuilder
          https://t.co/aS7NzHLO65 #programming #softwareengineering #bigdata
          #datascience…

1

搜索带有“Python”标签的推文,并打印最近的五条

也可以收集一个 Twitter 用户的大量推文,并创建一个词云形式的摘要(参见 图 4-3)。以下 Python 代码再次使用了 NLP 辅助函数,如 “Python Code” 中所示:

In [47]: l = t.statuses.user_timeline(screen_name='elonmusk', count=50)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [48]: tl = [e['text'] for e in l]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [49]: tl[:5]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[49]: ['@flcnhvy @Lindw0rm @cleantechnica True',
          '@Lindw0rm @cleantechnica Highly likely down the road',
          '@cleantechnica True fact',
         '@NASASpaceflight Scrubbed for the day. A Raptor turbopump spin start valve
          didn’t open, triggering an automatic abo… https://t.co/QDdlNXFgJg',
          '@Erdayastronaut I’m in the Boca control room. Hop attempt in ~33 minutes.']

In [50]: wc = nlp.generate_word_cloud(' '.join(tl), 35,
                     name='../../images/ch04/musk_twitter_wc.png'
                     )  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

1

检索用户 elonmusk 的最近 50 条推文

2

将文本收集到一个 list 对象中

3

显示最后五条推文的摘录

4

生成一个词云摘要并展示它

aiif 0403

图 4-3. 作为更多推文的词云摘要

一旦金融从业者定义了超越结构化金融时间序列数据的“相关金融数据”,数据来源在容量、多样性和速度方面似乎是无限的。从 Twitter API 检索的推文方式几乎是近乎实时的,因为这些示例中访问的是最新的推文。因此,这些和类似的基于 API 的数据源为我们提供了源源不断的替代数据流,正如先前指出的那样,重要的是要明确指定我们要寻找的内容。否则,任何金融数据科学的努力可能会很容易地淹没在过多或太嘈杂的数据中。

规范性理论再审视

第三章介绍了规范性金融理论,如 MVP 理论或 CAPM 理论。相当长一段时间以来,学生和学者学习和研究这些理论更多或多或少地受到理论本身的约束。如前一节中讨论和说明的所有可用金融数据,再加上强大的开源数据分析软件——如 Python、NumPypandas等,使得将金融理论应用于现实世界测试变得相当容易和直接。不再需要小团队和更大规模的研究来完成这一过程。一本典型的笔记本、互联网接入和标准的 Python 环境就足够了。这正是本节要讨论的内容。然而,在深入探讨数据驱动的金融之前,下一小节简要讨论了 EUT 背景下的一些著名悖论,以及企业如何在实践中建模和预测个体行为。

预期效用与现实

在经济学中,风险描述的是决策者事先知道可能的未来状态及其概率的情况。这是金融和 EUT 背景下的标准假设。另一方面,模糊性描述了经济学中的情况,其中决策者事先不知道概率,甚至可能的未来状态。不确定性涵盖了这两种不同的决策情况。

有一个长期的传统,分析个体(“代理人”)在不确定性下的具体决策行为。无数研究和实验已经进行,观察和分析代理人在面对不确定性时的行为,与诸如 EUT 的理论预测相比。几个世纪以来,悖论在决策理论和研究中发挥着重要作用。

一个这样的悖论,即圣彼得堡悖论,首先引发了效用函数和 EUT 的发明。丹尼尔·贝努利在 1738 年提出了这个悖论及其解决方案。该悖论基于以下的硬币投掷游戏 G。在游戏中,一个(完美的)硬币被抛掷,可能无限次。如果第一次抛掷结果是正面,玩家将获得 1 个货币单位的回报。只要出现正面,就会继续抛掷。如果出现两次正面,玩家将额外获得 2 个单位的回报。如果出现三次,额外回报是 4 个单位,依此类推。这是一个风险情境,因为所有可能的未来状态及其相关的概率都是预先知道的。

这个游戏的期望回报是无穷大。这可以从以下的无限求和中看出,其中每个元素都严格为正:

𝐄 ( G ) = 1 2 · 1 + 1 4 · 2 + 1 8 · 4 + 1 16 · 8 + ... = k=1 1 2 k 2 k-1 = k=1 1 2 =

然而,面对这样一场游戏,一般的决策者只愿意支付有限的金额来参与游戏。这主要是因为相对较大的回报只发生在相对较小的概率下。考虑潜在的回报 W = 511

W = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256 = 511

获得这样的回报的概率非常低。确切地说,它只有 P ( x = W ) = 1 512 = 0.001953125。然而,获得这样的回报或更小回报的概率则相当高:

P ( x W ) = k=1 9 1 2 k = 0 . 998046875

换句话说,在 1000 次游戏中,有 998 次的回报是 511 或更小。因此,一个玩家可能不愿意押上超过 511 来玩这个游戏。走出这个悖论的方法是引入具有正但递减边际效用的效用函数。在圣彼得堡悖论的背景下,这意味着存在一个函数 u : + ,它为每一个正回报 x 分配一个实数值 u ( x )。正但递减的边际效用则正式转化为以下形式:

u x > 0 2 u x 2 < 0

正如第三章中所见,其中一个候选函数是 u ( x ) = ln ( x ) 其中:

u x = 1 x 2 u x 2 = - 1 x 2

预期效用有限,如下无限和的计算所示:

𝐄 u ( G ) = k=1 1 2 k u 2 k-1 = k=1 ln2 k-1 2 k = k=1 (k-1) 2 k · ln ( 2 ) = ln ( 2 ) <

ln ( 2 ) 的预期效用 = 0.693147 显然与无限的预期回报相比是一个非常小的数字。伯努利效用函数和 EUT 解决了圣彼得堡悖论。

其他悖论,如奥莱尔悖论(发表于 Allais (1953))涉及到 EUT 本身。该悖论基于一个包含四个不同游戏的实验,测试对象应该对其进行排名。 表格 4-2 展示了这四个游戏 ( A , B , A ' , B ' ) 。排名是针对两对 ( A , B )( A ' , B ' )独立公理 假设表格的第一行不应该对 ( A ' , B ' ) 的排序产生任何影响,因为两个游戏的回报是相同的。

表格 4-2. 奥莱尔悖论中的游戏

概率 游戏 A 游戏 B 游戏 A’ 游戏 B’
0.66 2,400 2,400 0 0
0.33 2,500 2,400 2,500 2,400
0.01 0 2,400 0 2,400

在实验中,大多数决策者将游戏排名如下:B AA ' B ' 。排名 B A 导致以下不等式,其中 u 1 u ( 2400 ) , u 2 u ( 2500 ) , u 3 u ( 0 )

u 1 > 0 . 66 · u 1 + 0 . 33 · u 2 + 0 . 01 · u 3 0 . 34 · u 1 > 0 . 33 · u 2 + 0 . 01 · u 3

排名 A ' B ' 又导致以下不等式:

0 . 33 · u 2 + 0 . 01 · u 3 > 0 . 33 · u 1 + 0 . 01 · u 1 0 . 34 · u 1 < 0 . 33 · u 2 + 0 . 01 · u 3

这些不等式显然互相矛盾,并导致奥莱悖论。一个可能的解释是,决策者普遍认为确定性比典型模型(如 EUT)预测的更高。大多数人可能更愿意确定地获得 100 万美元,而不是玩一个只有 5%赢得 1 亿美元概率的游戏,尽管根据 EUT 的决策者可以选择游戏而不是确定的金额。

另一个解释在于框架决策和决策者的心理。众所周知,如果手术“成功率为 95%”而不是“死亡率为 5%”,更多人会接受手术。简单地改变措辞可能导致与 EUT 等决策理论不一致的行为。

另一个着名的悖论是关于 EUT 在其主观形式中的缺点,根据 Savage(1954 年,1972 年),即埃尔斯伯格悖论,它可以追溯到 Ellsberg(1961 年)的开创性论文。它解释了许多现实世界决策情境中模棱两可性的重要性。该悖论的标准设定包括两个不同的罐子,每个罐子都包含 100 个球。对于罐子 1,已知它包含 50 个黑色球和 50 个红色球。对于罐子 2,只知道它包含黑色和红色球,但不知道比例。

测试对象可以在以下游戏选项中选择:

  • 游戏 1:红 1,黑 1 或中立

  • 游戏 2:红 2,黑 2 或中立

  • 游戏 3:红 1,红 2 或中立

  • 游戏 4:黑 1,黑 2 或中立

这里,“红 1”表示从罐子 1 抽出一个红球。通常,测试对象的回答如下:

  • 游戏 1:中立

  • 游戏 2:中立

  • 游戏 3:红 1

  • 游戏 4:黑 1

这一组决策——虽然不是唯一可观察到的决策,但是是一种常见的决策——展示了所谓的避免歧义。由于对于第二个瓮而言,黑球和红球的概率并不为人所知,决策者更倾向于风险而非歧义的情况。

阿莱和埃尔斯伯格的两个悖论表明,真实的测试对象往往与经济学中成熟的决策理论预测相反行事。换句话说,作为决策者的人类不能像仔细收集数据然后计算数值以在不确定性下做出决策的机器那样进行比较,无论是以风险还是歧义的形式。人类行为比大多数,如果不是全部,现有理论当前建议的更复杂。阅读 Sapolsky(2018)的 800 页书籍行为之后,清楚地了解到解释人类行为是多么困难和复杂。它涵盖了从生化过程到遗传学、人类进化、部落、语言、宗教等多个方面,以一种整合的方式。

如果标准的经济决策范式(如 EUT)不能很好地解释现实世界的决策制定,那么还有哪些替代方案?建立在阿莱和埃尔斯伯格悖论基础上的经济实验是学习决策者在特定控制情境中行为的一个良好起点。这些实验及其有时令人惊讶和悖论的结果确实激发了大量研究人员提出解决悖论的替代理论和模型。Fontaine 和 Leonard(2005)的书籍经济学历史中的实验讨论了实验在经济学中的历史角色。例如,有一整套文献讨论了由埃尔斯伯格悖论引起的问题。这些文献涉及非加法概率、Choquet 积分以及决策启发式,如最大化最小回报(“max-min”)或最小化最大损失(“min-max”)。这些替代方法在某些决策情境下被证明优于 EUT,但它们远非金融领域的主流。

在实践中,究竟什么被证明是有用的?毫不奇怪,答案在于数据和机器学习算法。互联网作为拥有数十亿用户的平台,产生了描述真实世界人类行为的大量数据,有时被称为显性偏好。在网络上生成的大数据规模比单个实验能够生成的数据量大几个数量级。亚马逊、Facebook、Google 和 Twitter 等公司通过记录用户行为(即他们的显性偏好),并利用基于这些数据训练的机器学习算法产生的见解,赚取了数十亿美元。

在这种情况下采用的默认机器学习方法是监督学习。 算法本身通常是理论无关和模型无关的;经常应用的是各种神经网络的变体。 因此,当今公司预测其用户或客户的行为时,往往采用无模型的机器学习算法。 传统的决策理论,如 EUT 或其后继者之一,通常根本不起作用。 这使得在 2020 年代初,这样的理论仍然是大多数经济和金融理论实践中的基石,有些令人惊讶。 甚至不用提大量详细介绍传统决策理论的金融教科书。 如果金融理论的最基本的基石似乎缺乏有意义的经验支持或实际效益,那么建立在其上的金融模型又如何呢? 后续章节和章节将更详细地讨论这一点。

数据驱动的行为预测

标准经济决策理论对许多人具有知识上的吸引力,即使对于那些在面对不确定性的具体决策时与理论预测相反的人也是如此。 另一方面,大数据和无模型、监督学习方法在预测用户和客户行为方面证明是有用且成功的。 在财务环境中,这可能意味着人们不应该真正担心金融代理人为什么以及如何做出决策。 人们应该更多地关注他们间接展示的偏好,这些偏好基于描述金融市场状态的特征数据(新信息)和反映金融代理人决策影响的标签数据(结果)。 这导致在金融市场决策制定中,不再是理论或模型驱动,而是数据驱动的观点。 金融代理人变成了可以通过复杂神经网络(例如)而不是简单的效用函数与假设的概率分布来更好地建模的数据处理生物。

平均-方差组合理论

假设一个数据驱动的投资者希望应用 MVP 理论来投资一篮子科技股,并希望通过一个与黄金相关的交易所交易基金(ETF)进行多样化。 可能,投资者会通过 API 访问相关的历史价格数据,或者通过交易平台或数据提供者。 为了使下面的分析可以重复进行,它依赖于存储在远程位置的 CSV 数据文件。 以下 Python 代码检索数据文件,根据投资者的目标选择一些符号,并计算价格时间序列数据的对数收益率。 Figure 4-4 比较了所选符号的归一化价格时间序列:

In [51]: import numpy as np
         import pandas as pd
         from pylab import plt, mpl
         from scipy.optimize import minimize
         plt.style.use('seaborn')
         mpl.rcParams['savefig.dpi'] = 300
         mpl.rcParams['font.family'] = 'serif'
         np.set_printoptions(precision=5, suppress=True,
                            formatter={'float': lambda x: f'{x:6.3f}'})

In [52]: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [53]: raw = pd.read_csv(url, index_col=0, parse_dates=True).dropna()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [54]: raw.info()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 2516 entries, 2010-01-04 to 2019-12-31
         Data columns (total 12 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   AAPL.O  2516 non-null   float64
          1   MSFT.O  2516 non-null   float64
          2   INTC.O  2516 non-null   float64
          3   AMZN.O  2516 non-null   float64
          4   GS.N    2516 non-null   float64
          5   SPY     2516 non-null   float64
          6   .SPX    2516 non-null   float64
          7   .VIX    2516 non-null   float64
          8   EUR=    2516 non-null   float64
          9   XAU=    2516 non-null   float64
          10  GDX     2516 non-null   float64
          11  GLD     2516 non-null   float64
         dtypes: float64(12)
         memory usage: 255.5 KB

In [55]: symbols = ['AAPL.O', 'MSFT.O', 'INTC.O', 'AMZN.O', 'GLD']  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [56]: rets = np.log(raw[symbols] / raw[symbols].shift(1)).dropna()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [57]: (raw[symbols] / raw[symbols].iloc[0]).plot(figsize=(10, 6));  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

1

从远程位置检索历史 EOD 数据

2

指定要投资的符号(RICs)

3

计算所有时间序列的对数收益率

4

绘制所选符号的标准化财务时间序列

aiif 0404

图 4-4. 标准化财务时间序列数据

数据驱动型投资者希望首先根据所有可用数据的整个期间内的等权重投资组合来设置性能基线。为此,以下 Python 代码定义了计算所选符号的投资组合回报、投资组合波动率和投资组合夏普比率的函数:

In [58]: weights = len(rets.columns) * [1 / len(rets.columns)]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [59]: def port_return(rets, weights):
             return np.dot(rets.mean(), weights) * 252  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [60]: port_return(rets, weights)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[60]: 0.15694764653018106

In [61]: def port_volatility(rets, weights):
             return np.dot(weights, np.dot(rets.cov() * 252 , weights)) ** 0.5  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [62]: port_volatility(rets, weights)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[62]: 0.16106507848480675

In [63]: def port_sharpe(rets, weights):
             return port_return(rets, weights) / port_volatility(rets, weights)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [64]: port_sharpe(rets, weights)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[64]: 0.97443622172255

1

等权重投资组合

2

投资组合回报

3

投资组合波动率

4

投资组合夏普比率(零短期利率)

投资者还希望分析通过将蒙特卡罗模拟应用于随机化投资组合权重来实现的投资组合风险和回报的组合——因此夏普比率——的大致可能性。不包括空头交易,并假设投资组合权重总和为 100%。以下 Python 代码实现了模拟并可视化结果(参见图 4-5):

In [65]: w = np.random.random((1000, len(symbols)))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         w = (w.T / w.sum(axis=1)).T  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [66]: w[:5]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[66]: array([[ 0.184,  0.157,  0.227,  0.353,  0.079],
                [ 0.207,  0.282,  0.258,  0.023,  0.230],
                [ 0.313,  0.284,  0.051,  0.340,  0.012],
                [ 0.238,  0.181,  0.145,  0.191,  0.245],
                [ 0.246,  0.256,  0.315,  0.181,  0.002]])

In [67]: pvr = [(port_volatility(rets[symbols], weights),
                 port_return(rets[symbols], weights))
                for weights in w]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         pvr = np.array(pvr)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [68]: psr = pvr[:, 1] / pvr[:, 0]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [69]: plt.figure(figsize=(10, 6))
         fig = plt.scatter(pvr[:, 0], pvr[:, 1],
                           c=psr, cmap='coolwarm')
         cb = plt.colorbar(fig)
         cb.set_label('Sharpe ratio')
         plt.xlabel('expected volatility')
         plt.ylabel('expected return')
         plt.title(' | '.join(symbols));

1

模拟投资组合权重总和为 100%

2

推导出的投资组合波动率和回报

3

计算得到的夏普比率

aiif 0405

图 4-5. 模拟投资组合波动性、回报和夏普比率

数据驱动型投资者现在希望对在 2011 年初设置的投资组合的性能进行回测。优化的投资组合构成是根据 2010 年可用的财务时间序列数据得出的。在 2012 年初,根据 2011 年的可用数据调整了投资组合构成,依此类推。为此,以下 Python 代码为每个相关年份推导出最大化夏普比率的投资组合权重:

In [70]: bnds = len(symbols) * [(0, 1),]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         bnds  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[70]: [(0, 1), (0, 1), (0, 1), (0, 1), (0, 1)]

In [71]: cons = {'type': 'eq', 'fun': lambda weights: weights.sum() - 1}  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [72]: opt_weights = {}
         for year in range(2010, 2019):
             rets_ = rets[symbols].loc[f'{year}-01-01':f'{year}-12-31']  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             ow = minimize(lambda weights: -port_sharpe(rets_, weights),
                           len(symbols) * [1 / len(symbols)],
                           bounds=bnds,
                           constraints=cons)['x']  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
             opt_weights[year] = ow  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [73]: opt_weights  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[73]: {2010: array([ 0.366,  0.000,  0.000,  0.056,  0.578]),
          2011: array([ 0.543,  0.000,  0.077,  0.000,  0.380]),
          2012: array([ 0.324,  0.000,  0.000,  0.471,  0.205]),
          2013: array([ 0.012,  0.305,  0.219,  0.464,  0.000]),
          2014: array([ 0.452,  0.115,  0.419,  0.000,  0.015]),
          2015: array([ 0.000,  0.000,  0.000,  1.000,  0.000]),
          2016: array([ 0.150,  0.260,  0.000,  0.058,  0.533]),
          2017: array([ 0.231,  0.203,  0.031,  0.109,  0.426]),
          2018: array([ 0.000,  0.295,  0.000,  0.705,  0.000])}

1

指定单个资产权重的界限

2

指定所有权重需要总和为 100%

3

为给定年份选择相关数据集

4

推导出最大化夏普比率的投资组合权重

5

将这些权重存储在dict对象中

对于相关年份衍生的最优投资组合构成,表明最小方差组合理论在其原始形式下往往导致(相对)极端情况,即一个或多个资产完全不被包括,甚至一个单一资产构成了投资组合的 100%。当然,可以通过设置每个考虑的资产的最小权重来积极避免这种情况。结果还表明,这种方法导致了投资组合的显著再平衡,受前一年的实现统计数据和相关性的驱动。

为了完成回测,以下代码将预期投资组合统计数据(从上一年的最优构成应用于上一年的数据)与当前年份的实现投资组合统计数据进行比较:

In [74]: res = pd.DataFrame()
         for year in range(2010, 2019):
             rets_ = rets[symbols].loc[f'{year}-01-01':f'{year}-12-31']
             epv = port_volatility(rets_, opt_weights[year])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             epr = port_return(rets_, opt_weights[year])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             esr = epr / epv  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             rets_ = rets[symbols].loc[f'{year + 1}-01-01':f'{year + 1}-12-31']
             rpv = port_volatility(rets_, opt_weights[year]) ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             rpr = port_return(rets_, opt_weights[year])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             rsr = rpr / rpv  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             res = res.append(pd.DataFrame({'epv': epv, 'epr': epr, 'esr': esr,
                                            'rpv': rpv, 'rpr': rpr, 'rsr': rsr},
                                           index=[year + 1]))

In [75]: res
Out[75]:            epv       epr       esr       rpv       rpr       rsr
         2011  0.157440  0.303003  1.924564  0.160622  0.133836  0.833235
         2012  0.173279  0.169321  0.977156  0.182292  0.161375  0.885256
         2013  0.202460  0.278459  1.375378  0.168714  0.166897  0.989228
         2014  0.181544  0.368961  2.032353  0.197798  0.026830  0.135645
         2015  0.160340  0.309486  1.930190  0.211368 -0.024560 -0.116194
         2016  0.326730  0.778330  2.382179  0.296565  0.103870  0.350242
         2017  0.106148  0.090933  0.856663  0.079521  0.230630  2.900235
         2018  0.086548  0.260702  3.012226  0.157337  0.038234  0.243004
         2019  0.323796  0.228008  0.704174  0.207672  0.275819  1.328147

In [76]: res.mean()
Out[76]: epv    0.190920
         epr    0.309689
         esr    1.688320
         rpv    0.184654
         rpr    0.123659
         rsr    0.838755
         dtype: float64

1

预期投资组合统计数据

2

实现的投资组合统计数据

图 4-6 比较了单年预期和实现的投资组合波动率。最小方差组合理论在预测投资组合波动率方面表现相当好。这也得到了两个时间序列之间相对较高的相关性的支持:

In [77]: res[['epv', 'rpv']].corr()
Out[77]:           epv       rpv
         epv  1.000000  0.765733
         rpv  0.765733  1.000000

In [78]: res[['epv', 'rpv']].plot(kind='bar', figsize=(10, 6),
                 title='Expected vs. Realized Portfolio Volatility');

aiif 0406

图 4-6. 预期与实现的投资组合波动率

然而,当比较预期和实现的投资组合回报时,结论却相反(见图 4-7)。最小方差组合理论在预测投资组合回报方面显然失败了,这也被两个时间序列之间的负相关性所证实:

In [79]: res[['epr', 'rpr']].corr()
Out[79]:           epr       rpr
         epr  1.000000 -0.350437
         rpr -0.350437  1.000000

In [80]: res[['epr', 'rpr']].plot(kind='bar', figsize=(10, 6),
                 title='Expected vs. Realized Portfolio Return');

aiif 0407

图 4-7. 预期与实现的投资组合回报

对于追求最大化投资组合夏普比率的数据驱动型投资者来说,与实际价值相比,理论的预测通常相差很大。两个时间序列之间的相关性甚至低于回报的相关性:

In [81]: res[['esr', 'rsr']].corr()
Out[81]:           esr       rsr
         esr  1.000000 -0.698607
         rsr -0.698607  1.000000

In [82]: res[['esr', 'rsr']].plot(kind='bar', figsize=(10, 6),
                 title='Expected vs. Realized Sharpe Ratio');

aiif 0408

图 4-8. 预期与实现的投资组合夏普比率

最小方差组合理论的预测能力

将最小方差组合理论应用于现实数据揭示了其实际上的缺陷。在没有额外限制的情况下,最优投资组合构成和再平衡可能会极端。在数值示例中,最小方差组合理论在投资组合回报和夏普比率方面的预测能力相当差。然而,投资者通常对风险调整后的绩效指标感兴趣,例如夏普比率,在该示例中,最小方差组合理论在此统计中的表现最差。

资本资产定价模型

可以采用类似的方法来对 CAPM 进行实际测试。假设之前的数据驱动技术投资者希望应用 CAPM 来推导之前四只技术股票的预期回报。以下 Python 代码首先计算了每只股票在给定年份的贝塔,然后根据其贝塔和市场组合的表现计算了下一年该股票的预期回报。市场组合由标普 500 股指来近似:

In [83]: r = 0.005  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [84]: market = '.SPX'  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [85]: rets = np.log(raw / raw.shift(1)).dropna()

In [86]: res = pd.DataFrame()

In [87]: for sym in rets.columns[:4]:
             print('\n' + sym)
             print(54 * '=')
             for year in range(2010, 2019):
                 rets_ = rets.loc[f'{year}-01-01':f'{year}-12-31']
                 muM = rets_[market].mean() * 252
                 cov = rets_.cov().loc[sym, market]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 var = rets_[market].var()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 beta = cov / var  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 rets_ = rets.loc[f'{year + 1}-01-01':f'{year + 1}-12-31']
                 muM = rets_[market].mean() * 252
                 mu_capm = r + beta * (muM - r)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 mu_real = rets_[sym].mean() * 252  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 res = res.append(pd.DataFrame({'symbol': sym,
                                                'mu_capm': mu_capm,
                                                'mu_real': mu_real},
                                               index=[year + 1]),
                                 sort=True)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 print('{} | beta: {:.3f} | mu_capm: {:6.3f} | mu_real: {:6.3f}'
                       .format(year + 1, beta, mu_capm, mu_real))  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

1

指定无风险短期利率

2

定义了市场组合

3

计算股票的贝塔

4

根据前一年的贝塔和当前年份市场组合的表现计算预期回报

5

计算当前年份股票的实际表现

6

收集并打印所有结果

上述代码提供了以下输出:

         AAPL.O
         ======================================================
         2011 | beta: 1.052 | mu_capm: -0.000 | mu_real:  0.228
         2012 | beta: 0.764 | mu_capm:  0.098 | mu_real:  0.275
         2013 | beta: 1.266 | mu_capm:  0.327 | mu_real:  0.053
         2014 | beta: 0.630 | mu_capm:  0.070 | mu_real:  0.320
         2015 | beta: 0.833 | mu_capm: -0.005 | mu_real: -0.047
         2016 | beta: 1.144 | mu_capm:  0.103 | mu_real:  0.096
         2017 | beta: 1.009 | mu_capm:  0.180 | mu_real:  0.381
         2018 | beta: 1.379 | mu_capm: -0.091 | mu_real: -0.071
         2019 | beta: 1.252 | mu_capm:  0.316 | mu_real:  0.621

         MSFT.O
         ======================================================
         2011 | beta: 0.890 | mu_capm:  0.001 | mu_real: -0.072
         2012 | beta: 0.816 | mu_capm:  0.104 | mu_real:  0.029
         2013 | beta: 1.109 | mu_capm:  0.287 | mu_real:  0.337
         2014 | beta: 0.876 | mu_capm:  0.095 | mu_real:  0.216
         2015 | beta: 0.955 | mu_capm: -0.007 | mu_real:  0.178
         2016 | beta: 1.249 | mu_capm:  0.113 | mu_real:  0.113
         2017 | beta: 1.224 | mu_capm:  0.217 | mu_real:  0.321
         2018 | beta: 1.303 | mu_capm: -0.086 | mu_real:  0.172
         2019 | beta: 1.442 | mu_capm:  0.364 | mu_real:  0.440

         INTC.O
         ======================================================
         2011 | beta: 1.081 | mu_capm: -0.000 | mu_real:  0.142
         2012 | beta: 0.842 | mu_capm:  0.108 | mu_real: -0.163
         2013 | beta: 1.081 | mu_capm:  0.280 | mu_real:  0.230
         2014 | beta: 0.883 | mu_capm:  0.096 | mu_real:  0.335
         2015 | beta: 1.055 | mu_capm: -0.008 | mu_real: -0.052
         2016 | beta: 1.009 | mu_capm:  0.092 | mu_real:  0.051
         2017 | beta: 1.261 | mu_capm:  0.223 | mu_real:  0.242
         2018 | beta: 1.163 | mu_capm: -0.076 | mu_real:  0.017
         2019 | beta: 1.376 | mu_capm:  0.347 | mu_real:  0.243

         AMZN.O
         ======================================================
         2011 | beta: 1.102 | mu_capm: -0.001 | mu_real: -0.039
         2012 | beta: 0.958 | mu_capm:  0.122 | mu_real:  0.374
         2013 | beta: 1.116 | mu_capm:  0.289 | mu_real:  0.464
         2014 | beta: 1.262 | mu_capm:  0.135 | mu_real: -0.251
         2015 | beta: 1.473 | mu_capm: -0.013 | mu_real:  0.778
         2016 | beta: 1.122 | mu_capm:  0.102 | mu_real:  0.104
         2017 | beta: 1.118 | mu_capm:  0.199 | mu_real:  0.446
         2018 | beta: 1.300 | mu_capm: -0.086 | mu_real:  0.251
         2019 | beta: 1.619 | mu_capm:  0.408 | mu_real:  0.207

图 4-9 比较了单只股票的预期回报(根据前一年的贝塔和当前年份市场组合的表现)与当前年份股票的实际回报。显然,以其原始形式而言,CAPM 并不能有效预测仅基于贝塔的股票表现:

In [88]: sym = 'AMZN.O'

In [89]: res[res['symbol'] == sym].corr()
Out[89]:           mu_capm   mu_real
         mu_capm  1.000000 -0.004826
         mu_real -0.004826  1.000000

In [90]: res[res['symbol'] == sym].plot(kind='bar',
                         figsize=(10, 6), title=sym);

aiif 0409

图 4-9. 单只股票的 CAPM 预测与实际股票回报的对比

图 4-10 比较了 CAPM 预测股票回报的平均值与实际回报的平均值。同样地,CAPM 在这里的表现并不理想。

易见的是,对于分析的股票而言,CAPM 的预测平均值变化不大;在 12.2% 到 14.4% 之间。然而,股票的实际平均回报显示出较高的变异性;在 9.4% 到 29.2% 之间。显然,仅靠市场组合表现和贝塔无法解释观察到的(技术)股票的回报:

In [91]: grouped = res.groupby('symbol').mean()
         grouped
Out[91]:          mu_capm   mu_real
         symbol
         AAPL.O  0.110855  0.206158
         AMZN.O  0.128223  0.259395
         INTC.O  0.117929  0.116180
         MSFT.O  0.120844  0.192655

In [92]: grouped.plot(kind='bar', figsize=(10, 6), title='Average Values');

aiif 0410

图 4-10. 多只股票的平均 CAPM 预测回报与实际回报的对比

CAPM 的预测能力

关于 CAPM 对股票未来表现的预测能力相对于市场组合,对某些股票来说是非常低甚至不存在的。其中一个原因可能是 CAPM 基于 MVP 理论相同的核心假设,即投资者仅关心(预期)回报和(预期)波动率的组合和/或股票。从建模角度来看,可以问,单一风险因子足以解释股票回报的变异性,还是股票回报与市场组合表现之间可能存在非线性关系。

套利定价理论

从先前的数值示例结果来看,CAPM 的预测能力似乎相当有限。一个合理的问题是市场组合的表现是否足以解释股票回报的变异性。APT 的答案是否定的——可能还有更多(甚至很多)因素共同解释股票回报的变异性。“套利定价理论” 正式描述了 APT 框架,该框架还依赖于因素与股票回报之间的线性关系。

数据驱动的投资者意识到 CAPM 并不足以可靠地预测股票相对市场组合的表现。因此,投资者决定向市场组合中添加三个可能影响股票表现的额外因素:

  • 市场波动率(以 VIX 指数表示,.VIX

  • 汇率(以 EUR/USD 汇率表示,EUR=

  • 商品价格(以金价表示,XAU=

下面的 Python 代码通过使用四个因素结合多元回归来实现简单的 APT 方法,以解释股票未来的表现与因素的关系:

In [93]: factors = ['.SPX', '.VIX', 'EUR=', 'XAU=']  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [94]: res = pd.DataFrame()

In [95]: np.set_printoptions(formatter={'float': lambda x: f'{x:5.2f}'})

In [96]: for sym in rets.columns[:4]:
             print('\n' + sym)
             print(71 * '=')
             for year in range(2010, 2019):
                 rets_ = rets.loc[f'{year}-01-01':f'{year}-12-31']
                 reg = np.linalg.lstsq(rets_[factors],
                                       rets_[sym], rcond=-1)[0]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 rets_ = rets.loc[f'{year + 1}-01-01':f'{year + 1}-12-31']
                 mu_apt = np.dot(rets_[factors].mean() * 252, reg)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 mu_real =  rets_[sym].mean() * 252  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 res = res.append(pd.DataFrame({'symbol': sym,
                                 'mu_apt': mu_apt, 'mu_real': mu_real},
                                  index=[year + 1]))
                 print('{} | fl: {} | mu_apt: {:6.3f} | mu_real: {:6.3f}'
                       .format(year + 1, reg.round(2), mu_apt, mu_real))

1

四个因素

2

多元回归

3

股票的 APT 预期回报

4

股票的实现回报

前述代码提供了以下输出:

         AAPL.O
         =======================================================================
         2011 | fl: [ 0.91 -0.04 -0.35  0.12] | mu_apt:  0.011 | mu_real:  0.228
         2012 | fl: [ 0.76 -0.02 -0.24  0.05] | mu_apt:  0.099 | mu_real:  0.275
         2013 | fl: [ 1.67  0.04 -0.56  0.10] | mu_apt:  0.366 | mu_real:  0.053
         2014 | fl: [ 0.53 -0.00  0.02  0.16] | mu_apt:  0.050 | mu_real:  0.320
         2015 | fl: [ 1.07  0.02  0.25  0.01] | mu_apt: -0.038 | mu_real: -0.047
         2016 | fl: [ 1.21  0.01 -0.14 -0.02] | mu_apt:  0.110 | mu_real:  0.096
         2017 | fl: [ 1.10  0.01 -0.15 -0.02] | mu_apt:  0.170 | mu_real:  0.381
         2018 | fl: [ 1.06 -0.03 -0.15  0.12] | mu_apt: -0.088 | mu_real: -0.071
         2019 | fl: [ 1.37  0.01 -0.20  0.13] | mu_apt:  0.364 | mu_real:  0.621

         MSFT.O
         =======================================================================
         2011 | fl: [ 0.98  0.01  0.02 -0.11] | mu_apt: -0.008 | mu_real: -0.072
         2012 | fl: [ 0.82  0.00 -0.03 -0.01] | mu_apt:  0.103 | mu_real:  0.029
         2013 | fl: [ 1.14  0.00 -0.07 -0.01] | mu_apt:  0.294 | mu_real:  0.337
         2014 | fl: [ 1.28  0.05  0.04  0.07] | mu_apt:  0.149 | mu_real:  0.216
         2015 | fl: [ 1.20  0.03  0.05  0.01] | mu_apt: -0.016 | mu_real:  0.178
         2016 | fl: [ 1.44  0.03 -0.17 -0.02] | mu_apt:  0.127 | mu_real:  0.113
         2017 | fl: [ 1.33  0.01 -0.14  0.00] | mu_apt:  0.216 | mu_real:  0.321
         2018 | fl: [ 1.10 -0.02 -0.14  0.22] | mu_apt: -0.087 | mu_real:  0.172
         2019 | fl: [ 1.51  0.01 -0.16 -0.02] | mu_apt:  0.378 | mu_real:  0.440

         INTC.O
         =======================================================================
         2011 | fl: [ 1.17  0.01  0.05 -0.13] | mu_apt: -0.010 | mu_real:  0.142
         2012 | fl: [ 1.03  0.04  0.01  0.03] | mu_apt:  0.122 | mu_real: -0.163
         2013 | fl: [ 1.06 -0.01 -0.10  0.01] | mu_apt:  0.267 | mu_real:  0.230
         2014 | fl: [ 0.96  0.02  0.36 -0.02] | mu_apt:  0.063 | mu_real:  0.335
         2015 | fl: [ 0.93 -0.01 -0.09  0.02] | mu_apt:  0.001 | mu_real: -0.052
         2016 | fl: [ 1.02  0.00 -0.05  0.06] | mu_apt:  0.099 | mu_real:  0.051
         2017 | fl: [ 1.41  0.02 -0.18  0.03] | mu_apt:  0.226 | mu_real:  0.242
         2018 | fl: [ 1.12 -0.01 -0.11  0.17] | mu_apt: -0.076 | mu_real:  0.017
         2019 | fl: [ 1.50  0.01 -0.34  0.30] | mu_apt:  0.431 | mu_real:  0.243

         AMZN.O
         =======================================================================
         2011 | fl: [ 1.02 -0.03 -0.18 -0.14] | mu_apt: -0.016 | mu_real: -0.039
         2012 | fl: [ 0.98 -0.01 -0.17 -0.09] | mu_apt:  0.117 | mu_real:  0.374
         2013 | fl: [ 1.07 -0.00  0.09  0.00] | mu_apt:  0.282 | mu_real:  0.464
         2014 | fl: [ 1.54  0.03  0.01 -0.08] | mu_apt:  0.176 | mu_real: -0.251
         2015 | fl: [ 1.26 -0.02  0.45 -0.11] | mu_apt: -0.044 | mu_real:  0.778
         2016 | fl: [ 1.06 -0.00 -0.15 -0.04] | mu_apt:  0.099 | mu_real:  0.104
         2017 | fl: [ 0.94 -0.02  0.12 -0.03] | mu_apt:  0.185 | mu_real:  0.446
         2018 | fl: [ 0.90 -0.04 -0.25  0.28] | mu_apt: -0.085 | mu_real:  0.251
         2019 | fl: [ 1.99  0.05 -0.37  0.12] | mu_apt:  0.506 | mu_real:  0.207

图 4-11 比较了一只股票的 APT 预测回报和其随时间变化的实际股票回报。与单因素 CAPM 相比,改进似乎几乎没有:

In [97]: sym = 'AMZN.O'

In [98]: res[res['symbol'] == sym].corr()
Out[98]:            mu_apt   mu_real
         mu_apt   1.000000 -0.098281
         mu_real -0.098281  1.000000

In [99]: res[res['symbol'] == sym].plot(kind='bar',
                         figsize=(10, 6), title=sym);

aiif 0411

图 4-11. 一只股票的 APT 预测与实现股票回报的对比

与单因素 CAPM 相比,由下面的片段产生的 图 4-12 显示出相同的情况,该片段比较了多只股票的平均 APT 预测。由于平均 APT 预测几乎没有变化,实现回报之间存在较大的平均差异:

In [100]: grouped = res.groupby('symbol').mean()
          grouped
Out[100]:           mu_apt   mu_real
          symbol
          AAPL.O  0.116116  0.206158
          AMZN.O  0.135528  0.259395
          INTC.O  0.124811  0.116180
          MSFT.O  0.128441  0.192655

In [101]: grouped.plot(kind='bar', figsize=(10, 6), title='Average Values');

当然,在这种情况下,风险因素的选择至关重要。数据驱动的投资者决定找出哪些风险因素通常被认为是股票的相关因素。在研究了 Bender 等人(2013)的论文后,投资者选择用新的一组替换原始的风险因素。特别是,投资者选择如 表格 4-3 中所示的集合。

aiif 0412

图 4-12. 多支股票的平均 APT 预测与实际股票回报的对比

表格 4-3. APT 的风险因素

因素 描述 RIC
市场 MSCI 世界总回报每日美元(PUS = 价格回报) .dMIWO00000GUS
大小 MSCI 世界等权重价格净指数 EOD .dMIWO0000ENUS
波动率 MSCI 世界最小波动率净收益 .dMIWO0000YNUS
价值 MSCI 世界价值加权总收益(NUS 为净收益) .dMIWO000PkGUS
风险 MSCI 世界风险加权总收益美元收盘价 .dMIWO000PlGUS
成长 MSCI 世界质量净收益美元 .MIWO0000vNUS
动量 MSCI 世界动量总收益指数美元收盘价 .dMIWO0000NGUS

下面的 Python 代码从远程位置检索相应的数据集,并可视化归一化时间序列数据(见图 4-13)。仔细一看就能发现,这些时间序列似乎高度正相关:

In [102]: factors = pd.read_csv('http://hilpisch.com/aiif_eikon_eod_factors.csv',
                                index_col=0, parse_dates=True) ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [103]: (factors / factors.iloc[0]).plot(figsize=(10, 6));  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

1

检索因子时间序列数据

2

对数据进行归一化并绘图

aiif 0413

图 4-13. 归一化因子时间序列数据

这一印象得到了以下计算及因子回报相关性矩阵的确认。所有相关因子的相关系数都在 0.75 或更高:

In [104]: start = '2017-01-01'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
          end = '2020-01-01'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [105]: retsd = rets.loc[start:end].copy()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
          retsd.dropna(inplace=True)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [106]: retsf = np.log(factors / factors.shift(1))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
          retsf = retsf.loc[start:end]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
          retsf.dropna(inplace=True)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
          retsf = retsf.loc[retsd.index].dropna()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [107]: retsf.corr()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[107]:               market      size  volatility     value      risk    growth  \
          market      1.000000  0.935867    0.845010  0.964124  0.947150  0.959038
          size        0.935867  1.000000    0.791767  0.965739  0.983238  0.835477
          volatility  0.845010  0.791767    1.000000  0.778294  0.865467  0.818280
          value       0.964124  0.965739    0.778294  1.000000  0.958359  0.864222
          risk        0.947150  0.983238    0.865467  0.958359  1.000000  0.858546
          growth      0.959038  0.835477    0.818280  0.864222  0.858546  1.000000
          momentum    0.928705  0.796420    0.819585  0.818796  0.825563  0.952956

                      momentum
          market      0.928705
          size        0.796420
          volatility  0.819585
          value       0.818796
          risk        0.825563
          growth      0.952956
          momentum    1.000000

1

定义了数据选择的开始和结束日期

2

选择相关的回报数据子集

3

计算并处理因子的对数收益率

4

展示了因子的相关性矩阵

下面的 Python 代码为原始股票推导因子载荷,但使用了新的因子。它们源自数据集的前半部分,并应用于预测第二半部分的股票回报,考虑到各个单一因子的表现。也计算了实现回报。这两个时间序列在图 4-14 中进行了比较。鉴于因子间高相关性,APT 方法的解释能力与 CAPM 相比并没有显著提高:

In [108]: res = pd.DataFrame()

In [109]: np.set_printoptions(formatter={'float': lambda x: f'{x:5.2f}'})

In [110]: split = int(len(retsf) * 0.5)
          for sym in rets.columns[:4]:
              print('\n' + sym)
              print(74 * '=')
              retsf_, retsd_ = retsf.iloc[:split], retsd.iloc[:split]
              reg = np.linalg.lstsq(retsf_, retsd_[sym], rcond=-1)[0]
              retsf_, retsd_ = retsf.iloc[split:], retsd.iloc[split:]
              mu_apt = np.dot(retsf_.mean() * 252, reg)
              mu_real =  retsd_[sym].mean() * 252
              res = res.append(pd.DataFrame({'mu_apt': mu_apt,
                              'mu_real': mu_real}, index=[sym,]),
                              sort=True)
              print('fl: {} | apt: {:.3f} | real: {:.3f}'
                    .format(reg.round(1), mu_apt, mu_real))

          AAPL.O
          ==========================================================================
          fl: [ 2.30  2.80 -0.70 -1.40 -4.20  2.00 -0.20] | apt: 0.115 | real: 0.301

          MSFT.O
          ==========================================================================
          fl: [ 1.50  0.00  0.10 -1.30 -1.40  0.80  1.00] | apt: 0.181 | real: 0.304

          INTC.O
          ==========================================================================
          fl: [-3.10  1.60  0.40  1.30 -2.60  2.50  1.10] | apt: 0.186 | real: 0.118

          AMZN.O
          ==========================================================================
          fl: [ 9.10  3.30 -1.00 -7.10 -3.10 -1.80  1.20] | apt: 0.019 | real: 0.050

In [111]: res.plot(kind='bar', figsize=(10, 6));

aiif 0414

图 4-14. 基于典型因子的 APT 预测回报与实现回报的比较

数据驱动的投资者并不愿完全忽略 APT。因此,可以进行额外的测试以更深入地了解 APT 的解释能力。为此,使用因子载荷来测试 APT 是否能正确解释股票价格随时间的变动。事实上,尽管 APT 不能准确预测绝对表现(误差超过 10 个百分点),但在大多数情况下,它能够准确预测股票价格运动的方向(见图 4-15)。预测值与实现回报之间的相关性也非常高,约为 85%。然而,分析使用的是实现的因子回报来生成 APT 预测,这在实际操作中是不可能的,因为这些数据在相关交易日前一天是不可用的:

In [112]: sym
Out[112]: 'AMZN.O'

In [113]: rets_sym = np.dot(retsf_, reg)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [114]: rets_sym = pd.DataFrame(rets_sym,
                                  columns=[sym + '_apt'],
                                  index=retsf_.index)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [115]: rets_sym[sym + '_real'] = retsd_[sym]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [116]: rets_sym.mean() * 252  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[116]: AMZN.O_apt     0.019401
          AMZN.O_real    0.050344
          dtype: float64

In [117]: rets_sym.std() * 252 ** 0.5  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[117]: AMZN.O_apt     0.270995
          AMZN.O_real    0.307653
          dtype: float64

In [118]: rets_sym.corr()  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[118]:              AMZN.O_apt  AMZN.O_real
          AMZN.O_apt     1.000000     0.832218
          AMZN.O_real    0.832218     1.000000

In [119]: rets_sym.cumsum().apply(np.exp).plot(figsize=(10, 6));

1

预测每日股票价格回报,考虑到实现的因子回报

2

将结果存储在DataFrame对象中,并添加列和索引数据

3

将实现的股票价格收益添加到DataFrame对象中

4

计算年化收益率

5

计算年化波动率

6

计算相关系数

aiif 0415

图 4-15. APT 预测的绩效与随时间的实际绩效(总体)

鉴于实现的因子回报,APT 有多准确地预测股票价格走势的方向?以下 Python 代码显示准确度分数略高于 75%:

In [120]: rets_sym['same'] = (np.sign(rets_sym[sym + '_apt']) ==
                              np.sign(rets_sym[sym + '_real']))

In [121]: rets_sym['same'].value_counts()
Out[121]: True     288
          False     89
          Name: same, dtype: int64

In [122]: rets_sym['same'].value_counts()[True] / len(rets_sym)
Out[122]: 0.7639257294429708

揭穿中心假设

前一节提供了许多数值实例,展示了流行的规范金融理论在实践中可能失败的方式。本节认为其中一个主要原因是这些流行金融理论的核心假设是无效的;也就是说,它们根本不描述金融市场的现实。分析的两个假设是正态分布的回报线性 关系

正态分布的回报

实际上,只有正态分布完全通过其第一(期望)和第二时刻(标准差)来指定。

样本数据集

为了说明,考虑以下 Python 代码生成的标准正态分布随机数集合。⁴ 图 4-16 显示了结果直方图的典型钟形曲线:

In [1]: import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        np.random.seed(100)
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'

In [2]: N = 10000

In [3]: snrn = np.random.standard_normal(N)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        snrn -= snrn.mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        snrn /= snrn.std()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [4]: round(snrn.mean(), 4)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[4]: -0.0

In [5]: round(snrn.std(), 4)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[5]: 1.0

In [6]: plt.figure(figsize=(10, 6))
        plt.hist(snrn, bins=35);

1

生成标准正态分布的随机数

2

修正第一时刻(期望)为 0.0

3

修正第二时刻(标准差)为 1.0

aiif 0416

图 4-16. 标准正态分布的随机数

现在考虑一组随机数,其共享相同的第一和第二时刻值,但与图 4-17 所示的完全不同的分布。尽管时刻相同,该分布仅由三个离散值组成:

In [7]: numbers = np.ones(N) * 1.5  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        split = int(0.25 * N)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        numbers[split:3 * split] = -1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        numbers[3 * split:4 * split] = 0  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [8]: numbers -= numbers.mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        numbers /= numbers.std()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [9]: round(numbers.mean(), 4)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[9]: 0.0

In [10]: round(numbers.std(), 4)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[10]: 1.0

In [11]: plt.figure(figsize=(10, 6))
         plt.hist(numbers, bins=35);

1

一组只有三个离散值的数字

2

修正第一时刻(期望)为 0.0

3

修正第二时刻(标准差)为 1.0

aiif 0417

图 4-17. 其第一和第二时刻分别为 0.0 和 1.0 的分布

第一和第二时刻

概率分布的第一和第二矩完全描述了正态分布。还有无数其他分布可能与正态分布共享前两个矩,而完全不同。

为了测试真实财务回报,考虑以下 Python 函数,它允许用户将数据可视化为直方图,并添加一个具有数据的前两个矩的正态分布的概率密度函数(PDF):

In [12]: import math
         import scipy.stats as scs
         import statsmodels.api as sm

In [13]: def dN(x, mu, sigma):
             ''' Probability density function of a normal random variable x.
             '''
             z = (x - mu) / sigma
             pdf = np.exp(-0.5 * z ** 2) / math.sqrt(2 * math.pi * sigma ** 2)
             return pdf

In [14]: def return_histogram(rets, title=''):
             ''' Plots a histogram of the returns.
             '''
             plt.figure(figsize=(10, 6))
             x = np.linspace(min(rets), max(rets), 100)
             plt.hist(np.array(rets), bins=50,
                      density=True, label='frequency')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             y = dN(x, np.mean(rets), np.std(rets))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             plt.plot(x, y, linewidth=2, label='PDF')  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             plt.xlabel('log returns')
             plt.ylabel('frequency/probability')
             plt.title(title)
             plt.legend()

1

绘制数据的直方图

2

绘制相应正态分布的 PDF

图 4-18 展示了直方图如何逼近标准正态分布随机数的概率密度函数(PDF):

In [15]: return_histogram(snrn)

aiif 0418

图 4-18. 标准正态分布数字的直方图和 PDF

相比之下,图 4-19 表明正态分布的概率密度函数与作为直方图显示的数据无关:

In [16]: return_histogram(numbers)

aiif 0419

图 4-19. 离散数字的直方图和正态 PDF

另一种比较正态分布与数据的方法是 Quantile-Quantile(Q-Q)图。如 图 4-20 所示,对于正态分布的数字,数字本身(大部分)位于 Q-Q 平面上的一条直线上:

In [17]: def return_qqplot(rets, title=''):
             ''' Generates a Q-Q plot of the returns.
 '''
             fig = sm.qqplot(rets, line='s', alpha=0.5)
             fig.set_size_inches(10, 6)
             plt.title(title)
             plt.xlabel('theoretical quantiles')
             plt.ylabel('sample quantiles')

In [18]: return_qqplot(snrn)

aiif 0420

图 4-20. 标准正态分布数字的 Q-Q 图

同样,离散数字的 Q-Q 图如 图 4-21 所示,与 图 4-20 中的 Q-Q 图完全不同:

In [19]: return_qqplot(numbers)

aiif 0421

图 4-21. 离散数字的 Q-Q 图

最后,人们也可以使用统计测试来检查一组数字是否符合正态分布。

以下 Python 函数实现了三个测试:

  • 正态偏斜测试。

  • 正态峰度测试。

  • 正态偏斜和峰度组合测试。

p 值低于 0.05 通常被认为是正态性的反指标;也就是说,拒绝数字符合正态分布的假设。从这个意义上说,与前面的图一样,两组数据的 p 值说明了问题:

In [20]: def print_statistics(rets):
             print('RETURN SAMPLE STATISTICS')
             print('---------------------------------------------')
             print('Skew of Sample Log Returns {:9.6f}'.format(
                         scs.skew(rets)))
             print('Skew Normal Test p-value   {:9.6f}'.format(
                         scs.skewtest(rets)[1]))
             print('---------------------------------------------')
             print('Kurt of Sample Log Returns {:9.6f}'.format(
                         scs.kurtosis(rets)))
             print('Kurt Normal Test p-value   {:9.6f}'.format(
                         scs.kurtosistest(rets)[1]))
             print('---------------------------------------------')
             print('Normal Test p-value        {:9.6f}'.format(
                         scs.normaltest(rets)[1]))
             print('---------------------------------------------')

In [21]: print_statistics(snrn)
         RETURN SAMPLE STATISTICS
         ---------------------------------------------
         Skew of Sample Log Returns  0.016793
         Skew Normal Test p-value    0.492685
         ---------------------------------------------
         Kurt of Sample Log Returns -0.024540
         Kurt Normal Test p-value    0.637637
         ---------------------------------------------
         Normal Test p-value         0.707334
         ---------------------------------------------

In [22]: print_statistics(numbers)
         RETURN SAMPLE STATISTICS
         ---------------------------------------------
         Skew of Sample Log Returns  0.689254
         Skew Normal Test p-value    0.000000
         ---------------------------------------------
         Kurt of Sample Log Returns -1.141902
         Kurt Normal Test p-value    0.000000
         ---------------------------------------------
         Normal Test p-value         0.000000
         ---------------------------------------------

真实的财务回报

以下 Python 代码从远程源检索 EOD 数据,如本章前面所做的那样,并计算数据集中所有金融时间序列的对数回报。图 4-22 表明标准普尔 500 股票指数的对数回报表示为直方图时,与具有样本期望值和标准偏差的正态 PDF 相比,峰值更高,尾部更厚。这两个见解是惯例事实,因为它们可以在不同的金融工具上一致观察到:

In [23]: raw = pd.read_csv('http://hilpisch.com/aiif_eikon_eod_data.csv',
                           index_col=0, parse_dates=True).dropna()

In [24]: rets = np.log(raw / raw.shift(1)).dropna()

In [25]: symbol = '.SPX'

In [26]: return_histogram(rets[symbol].values, symbol)

aiif 0422

图 4-22. 标准普尔 500 对数回报的频率分布和正态 PDF

当考虑到 S&P 500 对数回报的 Q-Q 图时,可以获得类似的见解,见图 4-23。特别是,Q-Q 图很好地可视化了大尾部(左侧的直线下方的点和右侧的直线上方的点):

In [27]: return_qqplot(rets[symbol].values, symbol)

aiif 0423

图 4-23。S&P 500 对数回报的 Q-Q 图

下面的 Python 代码对数据集中一组金融时间序列的真实财务回报的正态性进行了统计检验。真实的财务回报经常未能通过这些测试。因此,可以安全地得出结论,关于财务回报的正态性假设几乎不描述财务现实:

In [28]: symbols = ['.SPX', 'AMZN.O', 'EUR=', 'GLD']

In [29]: for sym in symbols:
             print('\n{}'.format(sym))
             print(45 * '=')
             print_statistics(rets[sym].values)

         .SPX
         =============================================
         RETURN SAMPLE STATISTICS
         ---------------------------------------------
         Skew of Sample Log Returns -0.497160
         Skew Normal Test p-value    0.000000
         ---------------------------------------------
         Kurt of Sample Log Returns  4.598167
         Kurt Normal Test p-value    0.000000
         ---------------------------------------------
         Normal Test p-value         0.000000
         ---------------------------------------------

         AMZN.O
         =============================================
         RETURN SAMPLE STATISTICS
         ---------------------------------------------
         Skew of Sample Log Returns  0.135268
         Skew Normal Test p-value    0.005689
         ---------------------------------------------
         Kurt of Sample Log Returns  7.344837
         Kurt Normal Test p-value    0.000000
         ---------------------------------------------
         Normal Test p-value         0.000000
         ---------------------------------------------

         EUR=
         =============================================
         RETURN SAMPLE STATISTICS
         ---------------------------------------------
         Skew of Sample Log Returns -0.053959
         Skew Normal Test p-value    0.268203
         ---------------------------------------------
         Kurt of Sample Log Returns  1.780899
         Kurt Normal Test p-value    0.000000
         ---------------------------------------------
         Normal Test p-value         0.000000
         ---------------------------------------------

         GLD
         =============================================
         RETURN SAMPLE STATISTICS
         ---------------------------------------------
         Skew of Sample Log Returns -0.581025
         Skew Normal Test p-value    0.000000
         ---------------------------------------------
         Kurt of Sample Log Returns  5.899701
         Kurt Normal Test p-value    0.000000
         ---------------------------------------------
         Normal Test p-value         0.000000
         ---------------------------------------------

正态性假设

尽管正态性假设对于许多现实世界的现象(如物理学)是一个很好的近似,但当涉及到财务回报时,这种假设是不合适的,甚至是危险的。几乎没有财务回报样本数据集能通过统计正态性测试。除了在其他领域证明有用之外,这种假设之所以出现在许多金融模型中的一个主要原因是,它会导致简洁而相对简单的数学模型、计算和证明。

线性关系

与金融模型和理论中正态性假设的“无处不在”相似,变量之间的线性关系似乎是另一个广泛应用的基准。本小节考虑了一个重要的基准,即 CAPM 中股票β和其预期(实现)回报之间的假定线性关系。一般来说,β值越高,给定正市场表现的预期回报也会越高——这是由β值本身确定的一种固定比例方式。

回顾前一节中对一组科技股票的β值、CAPM 预期回报和实现回报的计算,以下 Python 代码重复了这一过程以便于操作。这次,β值也被添加到结果的DataFrame对象中。

In [30]: r = 0.005

In [31]: market = '.SPX'

In [32]: res = pd.DataFrame()

In [33]: for sym in rets.columns[:4]:
             for year in range(2010, 2019):
                 rets_ = rets.loc[f'{year}-01-01':f'{year}-12-31']
                 muM = rets_[market].mean() * 252
                 cov = rets_.cov().loc[sym, market]
                 var = rets_[market].var()
                 beta = cov / var
                 rets_ = rets.loc[f'{year + 1}-01-01':f'{year + 1}-12-31']
                 muM = rets_[market].mean() * 252
                 mu_capm = r + beta * (muM - r)
                 mu_real = rets_[sym].mean() * 252
                 res = res.append(pd.DataFrame({'symbol': sym,
                                                'beta': beta,
                                                'mu_capm': mu_capm,
                                                'mu_real': mu_real},
                                               index=[year + 1]),
                                 sort=True)

以下分析计算了一个线性回归的R 2分数,其中β是自变量,给定市场组合表现的 CAPM 预期回报是因变量。 R 2指的是确定系数,它衡量了模型相对于基线预测器(简单平均值形式)的表现。线性回归只能解释预期的 CAPM 回报变异的约 10%,这是一个相当低的值,这也通过图 4-24 得到了确认:

In [34]: from sklearn.metrics import r2_score

In [35]: reg = np.polyfit(res['beta'], res['mu_capm'], deg=1)
         res['mu_capm_ols'] = np.polyval(reg, res['beta'])

In [36]: r2_score(res['mu_capm'], res['mu_capm_ols'])
Out[36]: 0.09272355783573516

In [37]: res.plot(kind='scatter', x='beta', y='mu_capm', figsize=(10, 6))
         x = np.linspace(res['beta'].min(), res['beta'].max())
         plt.plot(x, np.polyval(reg, x), 'g--', label='regression')
         plt.legend();

aiif 0424

图 4-24。预期的 CAPM 回报与β的关系(包括线性回归)

对于实现回报,线性回归的解释能力甚至更低,约为 4.5%(见图 4-25)。线性回归恢复了贝塔和股票回报之间的正相关关系——“贝塔越高,给定(正)市场组合表现越高的回报”,如回归线的正斜率所示。然而,它们只能解释观察到的股票回报总体变异的一小部分:

In [38]: reg = np.polyfit(res['beta'], res['mu_real'], deg=1)
         res['mu_real_ols'] = np.polyval(reg, res['beta'])

In [39]: r2_score(res['mu_real'], res['mu_real_ols'])
Out[39]: 0.04466919444752959

In [40]: res.plot(kind='scatter', x='beta', y='mu_real', figsize=(10, 6))
         x = np.linspace(res['beta'].min(), res['beta'].max())
         plt.plot(x, np.polyval(reg, x), 'g--', label='regression')
         plt.legend();

aiif 0425

图 4-25. 预期的 CAPM 回报与贝塔之间的关系(包括线性回归)

线性关系

与正态性假设一样,线性关系在物理世界中经常可以观察到。然而,在金融领域中,几乎没有情况是变量之间明显线性依赖的。从建模的角度看,线性关系像正态性假设一样,导致了优雅而相对简单的数学模型、计算和证明。此外,金融计量学中的标准工具 OLS 回归非常适合处理数据中的线性关系。这些是为什么正态性和线性关系经常被选择为金融模型和理论的便利构建块的主要原因。

Conclusions

数个世纪以来,科学一直被严格生成和分析数据所驱动。然而,金融曾以基于金融市场简化数学模型的规范理论为特征,依赖于回报的正态性和线性关系等假设。几乎所有(金融)数据的广泛和全面可用性导致了从“理论优先”转向“数据驱动”的金融焦点转移。基于真实金融数据的数个例子说明,许多流行的金融模型和理论在面对金融市场现实时难以生存。尽管优雅,它们可能过于简单,无法捕捉金融市场的复杂性、变化性和非线性特征。

参考文献

本章引用的书籍和论文:

  • Allais, M. 1953. “Le Comportement de l’Homme Rationnel devant le Risque: Critique des Postulats et Axiomes de l’Ecole Americaine.” Econometrica 21 (4): 503-546.

  • Alexander, Carol. 2008a. Quantitative Methods in Finance. Market Risk Analysis I, West Sussex: John Wiley & Sons.

  • ⸻。2008b. Practical Financial Econometrics. Market Risk Analysis II, West Sussex: John Wiley & Sons.

  • Bender, Jennifer 等。2013. “Factor Investing 的基础。” MSCI Research Insight. http://bit.ly/aiif_factor_invest.

  • Campbell, John Y. 2018. Financial Decisions and Markets: A Course in Asset Pricing. Princeton and Oxford: Princeton University Press.

  • Ellsberg, Daniel. 1961. “Risk, Ambiguity, and the Savage Axioms.” Quarterly Journal of Economics 75 (4): 643-669.

  • Fontaine, Philippe 和 Robert Leonard. 2005. The Experiment in the History of Economics. 伦敦和纽约:Routledge.

  • Kopf, Dan. 2015. “The Discovery of Statistical Regression.” Priceonomics, November 6, 2015. http://bit.ly/aiif_ols.

  • Lee, Kai-Fu. 2018. AI Superpowers: China, Silicon Valley, and the New World Order. 波士顿和纽约:Houghton Mifflin Harcourt.

  • Sapolsky, Robert M. 2018. Behave: The Biology of Humans at Our Best and Worst. 纽约:Penguin Books.

  • Savage, Leonard J. (1954) 1972. The Foundations of Statistics. 第 2 版。纽约:Dover Publications.

  • Wigglesworth, Robin. 2019. “How Investment Analysts Became Data Miners.” Financial Times, November 28, 2019. https://oreil.ly/QJGtd.

Python Code

下面的 Python 文件包含了一些辅助函数,用于简化 NLP 中的某些任务:

#
# NLP Helper Functions
#
# Artificial Intelligence in Finance
# (c) Dr Yves J Hilpisch
# The Python Quants GmbH
#
import re
import nltk
import string
import pandas as pd
from pylab import plt
from wordcloud import WordCloud
from nltk.corpus import stopwords
from nltk.corpus import wordnet as wn
from lxml.html.clean import Cleaner
from sklearn.feature_extraction.text import TfidfVectorizer
plt.style.use('seaborn')

cleaner = Cleaner(style=True, links=True, allow_tags=[''],
                  remove_unknown_tags=False)

stop_words = stopwords.words('english')
stop_words.extend(['new', 'old', 'pro', 'open', 'menu', 'close'])

def remove_non_ascii(s):
    ''' Removes all non-ascii characters.
 '''
    return ''.join(i for i in s if ord(i) < 128)

def clean_up_html(t):
    t = cleaner.clean_html(t)
    t = re.sub('[\n\t\r]', ' ', t)
    t = re.sub(' +', ' ', t)
    t = re.sub('<.*?>', '', t)
    t = remove_non_ascii(t)
    return t

def clean_up_text(t, numbers=False, punctuation=False):
    ''' Cleans up a text, e.g. HTML document,
 from HTML tags and also cleans up the
 text body.
 '''
    try:
        t = clean_up_html(t)
    except:
        pass
    t = t.lower()
    t = re.sub(r"what's", "what is ", t)
    t = t.replace('(ap)', '')
    t = re.sub(r"\'ve", " have ", t)
    t = re.sub(r"can't", "cannot ", t)
    t = re.sub(r"n't", " not ", t)
    t = re.sub(r"i'm", "i am ", t)
    t = re.sub(r"\'s", "", t)
    t = re.sub(r"\'re", " are ", t)
    t = re.sub(r"\'d", " would ", t)
    t = re.sub(r"\'ll", " will ", t)
    t = re.sub(r'\s+', ' ', t)
    t = re.sub(r"\\", "", t)
    t = re.sub(r"\'", "", t)
    t = re.sub(r"\"", "", t)
    if numbers:
        t = re.sub('[^a-zA-Z ?!]+', '', t)
    if punctuation:
        t = re.sub(r'\W+', ' ', t)
    t = remove_non_ascii(t)
    t = t.strip()
    return t

def nltk_lemma(word):
    ''' If one exists, returns the lemma of a word.
 I.e. the base or dictionary version of it.
 '''
    lemma = wn.morphy(word)
    if lemma is None:
        return word
    else:
        return lemma

def tokenize(text, min_char=3, lemma=True, stop=True,
             numbers=False):
    ''' Tokenizes a text and implements some
 transformations.
 '''
    tokens = nltk.word_tokenize(text)
    tokens = [t for t in tokens if len(t) >= min_char]
    if numbers:
        tokens = [t for t in tokens if t[0].lower()
                  in string.ascii_lowercase]
    if stop:
        tokens = [t for t in tokens if t not in stop_words]
    if lemma:
        tokens = [nltk_lemma(t) for t in tokens]
    return tokens

def generate_word_cloud(text, no, name=None, show=True):
    ''' Generates a word cloud bitmap given a
 text document (string).
 It uses the Term Frequency (TF) and
 Inverse Document Frequency (IDF)
 vectorization approach to derive the
 importance of a word -- represented
 by the size of the word in the word cloud.

 Parameters
 ==========
 text: str
 text as the basis
 no: int
 number of words to be included
 name: str
 path to save the image
 show: bool
 whether to show the generated image or not
 '''
    tokens = tokenize(text)
    vec = TfidfVectorizer(min_df=2,
                      analyzer='word',
                      ngram_range=(1, 2),
                      stop_words='english'
                     )
    vec.fit_transform(tokens)
    wc = pd.DataFrame({'words': vec.get_feature_names(),
                       'tfidf': vec.idf_})
    words = ' '.join(wc.sort_values('tfidf', ascending=True)['words'].head(no))
    wordcloud = WordCloud(max_font_size=110,
                      background_color='white',
                      width=1024, height=768,
                      margin=10, max_words=150).generate(words)
    if show:
        plt.figure(figsize=(10, 10))
        plt.imshow(wordcloud, interpolation='bilinear')
        plt.axis('off')
        plt.show()
    if name is not None:
        wordcloud.to_file(name)

def generate_key_words(text, no):
    try:
        tokens = tokenize(text)
        vec = TfidfVectorizer(min_df=2,
                      analyzer='word',
                      ngram_range=(1, 2),
                      stop_words='english'
                     )

        vec.fit_transform(tokens)
        wc = pd.DataFrame({'words': vec.get_feature_names(),
                       'tfidf': vec.idf_})
        words = wc.sort_values('tfidf', ascending=False)['words'].values
        words = [ a for a in words if not a.isnumeric()][:no]
    except:
        words = list()
    return words

¹ See, for example, Kopf (2015).

² 此数据服务仅通过付费订阅可用。

³ RIC 代表Reuters Instrument Code

⁴ 由NumPy的随机数生成器生成的数字是伪随机数,尽管在整本书中它们被引用为随机数

第五章:机器学习

数据主义认为宇宙由数据流组成,任何现象或实体的价值取决于其对数据处理的贡献……数据主义因此消除了动物(人类)和机器之间的障碍,并期望电子算法最终能够解读和超越生物化学算法。

Yuval Noah Harari(2015 年)

机器学习是科学方法的高级版本。它遵循生成、测试、丢弃或精炼假设的相同过程。但是,一个科学家可能花一生时间提出和测试几百个假设,而一个机器学习系统可以在一秒钟内完成相同的工作。机器学习自动化了发现过程。因此,它正像革新商业一样,革新了科学。

Pedro Domingos(2015)

本章讨论的是机器学习作为一个过程。尽管使用了特定的算法和特定的数据来进行说明,但本章讨论的概念和方法是普遍适用的。本章的目标是以简单易懂和易于可视化的方式呈现机器学习的最重要元素。本章的方法是实用和说明性的,避免了大部分技术细节。从这个意义上说,本章提供了后续更为现实的机器学习应用的一种蓝图。

“学习”简要讨论了“学习”的机器概念。“数据”导入并预处理后续章节中使用的样本数据。样本数据基于 EUR/USD 汇率的时间序列。“成功”实施 OLS 回归和神经网络估计,使用均方误差作为成功的度量标准。“容量”讨论了模型容量在估计问题中使模型更成功的作用。“评估”解释了模型评估在机器学习过程中的角色,通常基于验证数据子集。“偏差和方差”讨论了高偏差高方差模型在估计问题背景下的典型特征。“交叉验证”说明了交叉验证的概念,以避免由于过大的模型容量而导致的过拟合,其中之一。

VanderPlas(2017 年,第五章)讨论了与本章类似的主题,主要使用了scikit-learn Python 包。Chollet(2017 年,第四章)也提供了类似于这里提供的概述,但主要使用了Keras深度学习包。Goodfellow 等人(2016 年,第五章)对机器学习及其相关重要概念进行了更为技术化和数学化的概述。

学习

在更正式、更抽象的层面上,学习通过算法或计算机程序可以定义为 Mitchell(1997)中的方式:

据说计算机程序在某类任务T和性能度量P方面通过经验E来学习,如果它在任务T上的表现,由P来衡量,随着经验E而改善。

存在一类任务需要执行(例如,估计分类)。然后有性能度量,例如均方误差(MSE)或准确率比。然后有学习,以算法在任务上的经验改进的性能来衡量。手头的任务类别是根据给定数据集描述的一般性质,其中包括监督学习情况下的特征数据和标签数据,或无监督学习情况下仅包括特征数据。

学习任务与待学习的任务

在通过算法或计算机程序进行学习的定义中,重要的是要注意学习任务和待学习任务之间的差异。学习意味着学习如何(最好地)执行某个任务,如估计或分类。

数据

本节介绍了接下来要使用的样本数据集。样本数据基于 EUR/USD 汇率的真实金融时间序列创建而成。首先,从 CSV 文件导入数据,然后将数据重新采样为月度数据,并存储在一个Series对象中:

In [1]: import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        np.random.seed(100)
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'

In [2]: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: raw = pd.read_csv(url, index_col=0, parse_dates=True)['EUR=']  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [4]: raw.head()
Out[4]: Date
        2010-01-01    1.4323
        2010-01-04    1.4411
        2010-01-05    1.4368
        2010-01-06    1.4412
        2010-01-07    1.4318
        Name: EUR=, dtype: float64

In [5]: raw.tail()
Out[5]: Date
        2019-12-26    1.1096
        2019-12-27    1.1175
        2019-12-30    1.1197
        2019-12-31    1.1210
        2020-01-01    1.1210
        Name: EUR=, dtype: float64

In [6]: l = raw.resample('1M').last()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [7]: l.plot(figsize=(10, 6), title='EUR/USD monthly');

1

导入金融时间序列数据

2

将数据重新采样为月度时间间隔

图 5-1 显示了金融时间序列。

aiif 0501

图 5-1. EUR/USD 汇率作为时间序列(月度)

为了只有一个特征,以下 Python 代码创建了一个合成特征向量。这允许在二维中进行简单的可视化。当然,这个合成特征(自变量)对 EUR/USD 汇率(标签数据,因变量)没有任何解释能力。接下来,还将这些数据抽象为标签数据是顺序和时间性质的事实。本章中将样本数据集作为由一维特征向量和一维标签向量组成的通用数据集进行处理。图 5-2 展示了暗示一个估计问题是当前任务的样本数据集的可视化:

In [8]: l = l.values  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        l -= l.mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [9]: f = np.linspace(-2, 2, len(l))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [10]: plt.figure(figsize=(10, 6))
         plt.plot(f, l, 'ro')
         plt.title('Sample Data Set')
         plt.xlabel('features')
         plt.ylabel('labels');

1

将标签数据转换为一个ndarray对象。

2

逐元素从数据中减去均值

3

创建一个合成特征作为 ndarray 对象

aiif 0502

图 5-2. 样本数据集

成功

一般情况下,估计问题的成功度量是 MSE,如在第一章中使用的。根据 MSE,根据标签数据作为相关基准以及算法在暴露于数据集或其部分后的预测值进行评判。与第一章类似,本节及其后续节考虑了两种算法:OLS 回归和神经网络。

首先是 OLS 回归。应用是直接的,如下面的 Python 代码所示。回归结果在图 5-3 中展示,包括至五阶的单项式回归。计算得到的 MSE 也相应计算:

In [11]: def MSE(l, p):
             return np.mean((l - p) ** 2)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [12]: reg = np.polyfit(f, l, deg=5)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         reg  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[12]: array([-0.01910626, -0.0147182 ,  0.10990388,  0.06007211, -0.20833598,
                -0.03275423])

In [13]: p = np.polyval(reg, f)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [14]: MSE(l, p)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[14]: 0.0034166422957371025

In [15]: plt.figure(figsize=(10, 6))
         plt.plot(f, l, 'ro', label='sample data')
         plt.plot(f, p, '--', label='regression')
         plt.legend();

1

函数 MSE 计算均方误差。

2

OLS 回归模型拟合到包括五阶单项式为止。

3

给出最优参数的 OLS 回归模型预测。

4

给出预测值的 MSE 值。

aiif 0503

图 5-3. 样本数据和三次回归线

OLS 回归通常通过解析方法求解。因此,不存在迭代学习。然而,可以通过逐渐向算法暴露更多数据来模拟学习过程。以下 Python 代码实现了 OLS 回归和预测,从仅有的几个样本开始逐步增加数量,最终达到完整数据集的长度。回归步骤基于较小的子集实现,而预测步骤基于每种情况下的全部特征数据。一般而言,增加训练数据集时,MSE 明显下降:

In [16]: for i in range(10, len(f) + 1, 20):
             reg = np.polyfit(f[:i], l[:i], deg=3)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             p = np.polyval(reg, f)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             mse = MSE(l, p)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             print(f'{i:3d} | MSE={mse}')
          10 | MSE=248628.10681642237
          30 | MSE=731.9382249304651
          50 | MSE=12.236088505004465
          70 | MSE=0.7410590619743301
          90 | MSE=0.0057430617304093275
         110 | MSE=0.006492800939555582

1

基于数据子集的回归步骤

2

基于完整数据集的预测步骤

3

结果 MSE 值

其次是神经网络。对样本数据的应用再次是直接的,类似于第一章中的情况。图 5-4 展示了神经网络如何逼近样本数据:

In [17]: import tensorflow as tf
         tf.random.set_seed(100)

In [18]: from keras.layers import Dense
         from keras.models import Sequential
         Using TensorFlow backend.

In [19]: model = Sequential()
         model.add(Dense(256, activation='relu', input_dim=1))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.add(Dense(1, activation='linear')) ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.compile(loss='mse', optimizer='rmsprop')

In [20]: model.summary()
         Model: "sequential_1"
         _________________________________________________________________
         Layer (type)                 Output Shape              Param #
         =================================================================
         dense_1 (Dense)              (None, 256)               512
         _________________________________________________________________
         dense_2 (Dense)              (None, 1)                 257
         =================================================================
         Total params: 769
         Trainable params: 769
         Non-trainable params: 0
         _________________________________________________________________

In [21]: %time model.fit(f, l, epochs=1500, verbose=False)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         CPU times: user 5.89 s, sys: 761 ms, total: 6.66 s
         Wall time: 4.43 s

Out[21]: <keras.callbacks.callbacks.History at 0x7fc05d599d90>

In [22]: p = model.predict(f).flatten()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [23]: MSE(l, p)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[23]: 0.0020217512014360102

In [24]: plt.figure(figsize=(10, 6))
         plt.plot(f, l, 'ro', label='sample data')
         plt.plot(f, p, '--', label='DNN approximation')
         plt.legend();

1

神经网络是一个单隐藏层的浅层网络。

2

拟合步骤,具有相对较多的周期数。

3

预测步骤还会将 ndarray 对象展平。

4

DNN 预测的结果 MSE 值。

aiif 0504

图 5-4. 示例数据和神经网络近似

使用Keras包,在每个学习步骤后存储 MSE 值。图 5-5 显示了神经网络训练的时期增加时(从图中可以看出)MSE 值(“损失”)的平均减少:

In [25]: import pandas as pd

In [26]: res = pd.DataFrame(model.history.history)

In [27]: res.tail()
Out[27]:           loss
         1495  0.001547
         1496  0.001520
         1497  0.001456
         1498  0.001356
         1499  0.001325

In [28]: res.iloc[100:].plot(figsize=(10, 6))
         plt.ylabel('MSE')
         plt.xlabel('epochs');

aiif 0505

图 5-5. MSE 值与训练时期数量的关系

容量

模型或算法的容量定义了模型或算法基本可以学习的函数或关系类型。在仅基于单项式的 OLS 回归中,只有一个参数定义了模型的容量:最高单项式的次数。如果将该次数参数设为deg=3,OLS 回归模型可以学习常数、线性、二次或三次类型的函数关系。参数deg越高,OLS 回归模型的容量就越高。

以下 Python 代码从deg=1开始,每次增加两个单位的次数。随着次数参数的增加,MSE 值单调减少。图 5-6 展示了考虑的所有次数的回归线:

In [29]: reg = {}
         for d in range(1, 12, 2):
             reg[d] = np.polyfit(f, l, deg=d)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             p = np.polyval(reg[d], f)
             mse = MSE(l, p)
             print(f'{d:2d} | MSE={mse}')
          1 | MSE=0.005322474034260403
          3 | MSE=0.004353110724143185
          5 | MSE=0.0034166422957371025
          7 | MSE=0.0027389501772354025
          9 | MSE=0.001411961626330845
         11 | MSE=0.0012651237868752322

In [30]: plt.figure(figsize=(10, 6))
         plt.plot(f, l, 'ro', label='sample data')
         for d in reg:
             p = np.polyval(reg[d], f)
             plt.plot(f, p, '--', label=f'deg={d}')
         plt.legend();

1

不同deg值的回归步骤

aiif 0506

图 5-6. 不同最高次数的回归线

神经网络的容量取决于一些超参数。其中通常包括以下内容:

  • 隐藏层的数量

  • 每个隐藏层的隐藏单元数量

综合考虑这两个超参数,它们定义了神经网络中可训练参数(权重)的数量。前一节中的神经网络模型具有相对较少的可训练参数。例如,仅增加一个相同大小的层,可显著增加可训练参数的数量。虽然可能需要增加训练时期的数量,但容量更高的神经网络模型的 MSE 值显著减少,视觉上的拟合效果也更好,正如图 5-7 所示:

In [31]: def create_dnn_model(hl=1, hu=256):
             ''' Function to create Keras DNN model.

             Parameters
             ==========
             hl: int
                 number of hidden layers
             hu: int
                 number of hidden units (per layer)
             '''
             model = Sequential()
             for _ in range(hl):
                 model.add(Dense(hu, activation='relu', input_dim=1))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             model.add(Dense(1, activation='linear'))
             model.compile(loss='mse', optimizer='rmsprop')
             return model

In [32]: model = create_dnn_model(3)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [33]: model.summary()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         Model: "sequential_2"
         _________________________________________________________________
         Layer (type)                 Output Shape              Param #
         =================================================================
         dense_3 (Dense)              (None, 256)               512
         _________________________________________________________________
         dense_4 (Dense)              (None, 256)               65792
         _________________________________________________________________
         dense_5 (Dense)              (None, 256)               65792
         _________________________________________________________________
         dense_6 (Dense)              (None, 1)                 257
         =================================================================
         Total params: 132,353
         Trainable params: 132,353
         Non-trainable params: 0
         _________________________________________________________________

In [34]: %time model.fit(f, l, epochs=2500, verbose=False)
         CPU times: user 34.9 s, sys: 5.91 s, total: 40.8 s
         Wall time: 15.5 s

Out[34]: <keras.callbacks.callbacks.History at 0x7fc03fc18890>

In [35]: p = model.predict(f).flatten()

In [36]: MSE(l, p)
Out[36]: 0.00046612284916401614

In [37]: plt.figure(figsize=(10, 6))
         plt.plot(f, l, 'ro', label='sample data')
         plt.plot(f, p, '--', label='DNN approximation')
         plt.legend();

1

可能向神经网络添加许多层

2

深度神经网络具有三个隐藏层

3

摘要显示了可训练参数的增加数量(增加的容量)

aiif 0507

图 5-7. 示例数据和 DNN 近似(更高容量)

评估

在前几节中,分析侧重于估算算法在整个样本数据集上的性能。一般规则是,模型或算法的能力直接影响其在相同数据集上进行训练和评估时的性能。然而,在机器学习中,“简单且容易的情况”只是其中之一。更复杂和有趣的情况是,训练完成的模型或算法需要在其从未见过的数据上进行泛化。例如,这样的泛化可以是根据股票历史价格预测(估算)未来股票价格,或者根据现有债务人的数据对潜在债务人进行“信用良好”或“不良信用”的分类。

尽管在估算的上下文中经常自由使用“预测”这个术语,但在用于训练的特征数据集上,真正的预测可能意味着预测一些事先不知道并且从未见过的东西。再次强调,预测未来股票价格是在时间上的真正预测的一个很好的例子。

一般而言,给定数据集被划分为各具不同目的的子集:

训练数据集

这是用于算法训练的子集。

验证数据集

这是用于在训练期间验证算法性能的子集,而这个数据集与训练数据集不同。

测试数据集

这是在训练完成后仅对已训练算法进行测试的子集。

通过将(当前)训练过的算法应用于验证数据集获得的见解,可能会反映在训练本身上(例如通过调整模型的超参数)。另一方面,测试训练过的算法在测试数据集上的见解则不应该反映在训练本身或超参数中。

以下 Python 代码有些随意地选择了样本数据的 25%进行测试;模型或算法在训练(学习)完成之前不会见到这些数据。同样地,样本数据的 25%用于验证;这些数据用于在训练步骤中监视性能,可能在许多学习迭代中都会用到。剩余的 50%用于训练(学习)本身。¹ 鉴于样本数据集,将洗牌技术应用于随机填充所有样本数据子集是合理的:

In [38]: te = int(0.25 * len(f))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         va = int(0.25 * len(f))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [39]: np.random.seed(100)
         ind = np.arange(len(f))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         np.random.shuffle(ind)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [40]: ind_te = np.sort(ind[:te])  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         ind_va = np.sort(ind[te:te + va])  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         ind_tr = np.sort(ind[te + va:])  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [41]: f_te = f[ind_te]  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         f_va = f[ind_va]  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         f_tr = f[ind_tr]  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [42]: l_te = l[ind_te]  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         l_va = l[ind_va]  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         l_tr = l[ind_tr]  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

1

测试数据集样本数量

2

验证数据集样本数量

3

完整数据集的随机索引

4

对数据子集进行排序后的索引结果

5

得到的特征数据子集

6

得到的标签数据子集

随机抽样

对于既非顺序类也非时间性质的数据集,随机化训练、验证和测试数据集是一种常见且有用的技术。然而,当处理例如财务时间序列时,应避免对数据进行洗牌,因为这会破坏时间结构,并通过在训练中使用稍后的样本并在较早的样本上实施测试,引入先见性偏差。

基于训练和验证数据子集,以下 Python 代码实现了不同deg参数值的回归,并计算了对两个数据子集进行预测的 MSE 值。尽管训练数据集上的 MSE 值单调下降,但验证数据集上的 MSE 值通常会在某个参数值达到最小值后再次增加。这种现象表明了所谓的过拟合。图 5-8 显示了不同deg值的回归拟合,并比较了训练数据和验证数据集的拟合情况:

In [43]: reg = {}
         mse = {}
         for d in range(1, 22, 4):
             reg[d] = np.polyfit(f_tr, l_tr, deg=d)
             p = np.polyval(reg[d], f_tr)
             mse_tr = MSE(l_tr, p)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             p = np.polyval(reg[d], f_va)
             mse_va = MSE(l_va, p)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             mse[d] = (mse_tr, mse_va)
             print(f'{d:2d} | MSE_tr={mse_tr:7.5f} | MSE_va={mse_va:7.5f}')
          1 | MSE_tr=0.00574 | MSE_va=0.00492
          5 | MSE_tr=0.00375 | MSE_va=0.00273
          9 | MSE_tr=0.00132 | MSE_va=0.00243
         13 | MSE_tr=0.00094 | MSE_va=0.00183
         17 | MSE_tr=0.00060 | MSE_va=0.00153
         21 | MSE_tr=0.00046 | MSE_va=0.00837

In [44]: fig, ax = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
         ax[0].plot(f_tr, l_tr, 'ro', label='training data')
         ax[1].plot(f_va, l_va, 'go', label='validation data')
         for d in reg:
             p = np.polyval(reg[d], f_tr)
             ax[0].plot(f_tr, p, '--', label=f'deg={d} (tr)')
             p = np.polyval(reg[d], f_va)
             plt.plot(f_va, p, '--', label=f'deg={d} (va)')
         ax[0].legend()
         ax[1].legend();

1

训练数据集的 MSE 值

2

验证数据集的 MSE 值

aiif 0508

图 5-8. 包括回归拟合的训练和验证数据

使用Keras和神经网络模型,可以监控每个学习步骤的验证数据集性能。还可以使用回调函数在观察到训练数据集上的性能没有进一步改进时,提前停止模型训练。以下 Python 代码利用了这样的回调函数。图 5-9 显示了神经网络对训练和验证数据集的预测:

In [45]: from keras.callbacks import EarlyStopping

In [46]: model = create_dnn_model(2, 256)

In [47]: callbacks = EarlyStopping(monitor='loss',  ![1                                    patience=100,  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                                   restore_best_weights=True)]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [48]: %%time
         model.fit(f_tr, l_tr, epochs=3000, verbose=False,
                   validation_data=(f_va, l_va),  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                   callbacks=callbacks)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         CPU times: user 8.07 s, sys: 1.33 s, total: 9.4 s
         Wall time: 4.81 s

Out[48]: <keras.callbacks.callbacks.History at 0x7fc0438b47d0>

In [49]: fig, ax = plt.subplots(2, 1, sharex=True, figsize=(10, 8))
         ax[0].plot(f_tr, l_tr, 'ro', label='training data')
         p = model.predict(f_tr)
         ax[0].plot(f_tr, p, '--', label=f'DNN (tr)')
         ax[0].legend()
         ax[1].plot(f_va, l_va, 'go', label='validation data')
         p = model.predict(f_va)
         ax[1].plot(f_va, p, '--', label=f'DNN (va)')
         ax[1].legend();

1

基于训练数据 MSE 值停止学习。

2

只有在一定数量的时期内没有显示改进时才会停止。

3

当学习停止时,最佳权重会被恢复。

4

指定了验证数据子集。

5

回调函数被传递给fit()方法。

aiif 0509

图 5-9. 包括 DNN 预测的训练和验证数据

Keras允许分析模型在每个训练时期中在两个数据集上的 MSE 值的变化。图 5-10 显示,随着训练时期数量的增加,MSE 值平均下降,但不是单调的:

In [50]: res = pd.DataFrame(model.history.history)

In [51]: res.tail()
Out[51]:       val_loss      loss
         1375  0.000854  0.000544
         1376  0.000685  0.000473
         1377  0.001326  0.000942
         1378  0.001026  0.000867
         1379  0.000710  0.000500

In [52]: res.iloc[35::25].plot(figsize=(10, 6))
         plt.ylabel('MSE')
         plt.xlabel('epochs');

aiif 0510

图 5-10. DNN 模型在训练和验证数据集上的 MSE 值

在 OLS 回归的情况下,人们可能会选择一个高但不太高的度参数值,如deg=9。神经网络模型的参数化在训练结束时自动给出最佳模型配置。Figure 5-10 比较了两种模型的预测结果以及测试数据集。考虑到样本数据的性质,神经网络在测试数据集上表现稍好并不奇怪:

In [53]: p_ols = np.polyval(reg[5], f_te)
         p_dnn = model.predict(f_te).flatten()

In [54]: MSE(l_te, p_ols)
Out[54]: 0.0038960346771028356

In [55]: MSE(l_te, p_dnn)
Out[55]: 0.000705705678438721

In [56]: plt.figure(figsize=(10, 6))
         plt.plot(f_te, l_te, 'ro', label='test data')
         plt.plot(f_te, p_ols, '--', label='OLS prediction')
         plt.plot(f_te, p_dnn, '-.', label='DNN prediction');
         plt.legend();

aiif 0511

Figure 5-11. 测试数据及 OLS 回归和 DNN 模型的预测结果

偏差和方差

机器学习普遍存在的主要问题,特别是在应用于金融数据时,是过拟合问题。当模型在验证和测试数据上的表现比在训练数据上差时,就称为模型过拟合训练数据。以 OLS 回归为例,可以通过视觉和数值上的示例来说明这个问题。

以下 Python 代码使用较小的训练和验证子集实施了线性回归,以及一个高阶回归。如 Figure 5-12 所示,线性回归拟合在训练数据集上具有高偏差,预测和标签数据之间的绝对差异相对较大。高阶拟合显示了高方差,它精确地命中所有训练数据点,但拟合本身因为追求完美拟合而变化显著:

In [57]: f_tr = f[:20:2]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         l_tr = l[:20:2]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [58]: f_va = f[1:20:2]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         l_va = l[1:20:2]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [59]: reg_b = np.polyfit(f_tr, l_tr, deg=1)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [60]: reg_v = np.polyfit(f_tr, l_tr, deg=9, full=True)[0]  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [61]: f_ = np.linspace(f_tr.min(), f_va.max(), 75)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [62]: plt.figure(figsize=(10, 6))
         plt.plot(f_tr, l_tr, 'ro', label='training data')
         plt.plot(f_va, l_va, 'go', label='validation data')
         plt.plot(f_, np.polyval(reg_b, f_), '--', label='high bias')
         plt.plot(f_, np.polyval(reg_v, f_), '--', label='high variance')
         plt.ylim(-0.2)
         plt.legend(loc=2);

1

较小特征数据子集

2

较小标签数据子集

3

高偏差 OLS 回归(线性)

4

高方差 OLS 回归(更高阶)

5

用于绘图的扩大特征数据集

aiif 0512

Figure 5-12. 高偏差和高方差 OLS 回归拟合

Figure 5-12 显示,在这个例子中,高偏差拟合在训练数据上的表现比高方差拟合差。但是这里的高方差拟合过度拟合,其在验证数据上表现更差。可以通过比较所有情况下的性能指标来说明这一点。以下 Python 代码不仅计算了 MSE 值,还计算了R 2值:

In [63]: from sklearn.metrics import r2_score

In [64]: def evaluate(reg, f, l):
             p = np.polyval(reg, f)
             bias = np.abs(l - p).mean()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             var = p.var()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             msg = f'MSE={MSE(l, p):.4f} | R2={r2_score(l, p):9.4f} | '
             msg += f'bias={bias:.4f} | var={var:.4f}'
             print(msg)

In [65]: evaluate(reg_b, f_tr, l_tr)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         MSE=0.0026 | R2=   0.3484 | bias=0.0423 | var=0.0014

In [66]: evaluate(reg_b, f_va, l_va)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         MSE=0.0032 | R2=   0.4498 | bias=0.0460 | var=0.0014

In [67]: evaluate(reg_v, f_tr, l_tr)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         MSE=0.0000 | R2=   1.0000 | bias=0.0000 | var=0.0040

In [68]: evaluate(reg_v, f_va, l_va)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         MSE=0.8752 | R2=-149.2658 | bias=0.3565 | var=0.7539

1

模型偏差定义为平均绝对差异

2

模型方差定义为模型预测的方差

3

高偏差模型在训练数据上的表现

4

高偏差模型在验证数据上的表现

5

高方差模型在训练数据上的表现

6

高方差模型在验证数据上的表现

结果显示,高偏差模型在训练和验证数据集上的表现大致相当。相比之下,高方差模型在训练数据上表现完美,但在验证数据上表现非常糟糕。

交叉验证

避免过拟合的标准方法是交叉验证,在此期间会对多个训练和验证数据集进行测试。scikit-learn包提供了一种标准化的实现交叉验证的功能。cross_val_score函数可应用于任何scikit-learn模型对象。

以下代码在完整样本数据集上实现 OLS 回归方法,使用scikit-learn的多项式 OLS 回归模型。为最高多项式的不同度数实现了五折交叉验证。随着最高度数的增加,交叉验证得分平均变得更差。当使用前 20%数据用于验证(图 5-3 左侧的数据)或使用最后 20%数据时,特别糟糕的结果被观察到(图 5-3 右侧的数据)。类似地,最佳验证分数出现在样本数据集中间的 20%上:

In [69]: from sklearn.model_selection import cross_val_score
         from sklearn.preprocessing import PolynomialFeatures
         from sklearn.linear_model import LinearRegression
         from sklearn.pipeline import make_pipeline

In [70]: def PolynomialRegression(degree=None, **kwargs):
             return make_pipeline(PolynomialFeatures(degree),
                                 LinearRegression(**kwargs))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [71]: np.set_printoptions(suppress=True,
                 formatter={'float': lambda x: f'{x:12.2f}'})  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [72]: print('\nCross-validation scores')
         print(74 * '=')
         for deg in range(0, 10, 1):
             model = PolynomialRegression(deg)
             cvs = cross_val_score(model, f.reshape(-1, 1), l, cv=5)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             print(f'deg={deg} | ' + str(cvs.round(2)))

         Cross-validation scores
         ==========================================================================
         deg=0 | [       -6.07        -7.34        -0.09        -6.32        -8.69]
         deg=1 | [       -0.28        -1.40         0.16        -1.66        -4.62]
         deg=2 | [       -3.48        -2.45         0.19        -1.57       -12.94]
         deg=3 | [       -0.00        -1.24         0.32        -0.48       -43.62]
         deg=4 | [     -222.81        -2.88         0.37        -0.32      -496.61]
         deg=5 | [     -143.67        -5.85         0.49         0.12     -1241.04]
         deg=6 | [    -4038.96       -14.71         0.49        -0.33      -317.32]
         deg=7 | [    -9937.83       -13.98         0.64         0.22    -18725.61]
         deg=8 | [    -3514.36       -11.22        -0.15        -6.29   -298744.18]
         deg=9 | [    -7454.15        -0.91         0.15        -0.41    -13580.75]

1

创建多项式回归模型类

2

调整了numpy的默认打印设置

3

实现了五折交叉验证

Keras提供了包装类,用于将Keras模型对象与scikit-learn功能结合使用,例如cross_val_score函数。以下示例使用KerasRegressor类来包装神经网络模型,并对其应用交叉验证。与 OLS 回归交叉验证得分相比,这两个测试网络的交叉验证得分整体更好。在本例中,神经网络的容量并不起太大的作用:

In [73]: np.random.seed(100)
         tf.random.set_seed(100)
         from keras.wrappers.scikit_learn import KerasRegressor

In [74]: model = KerasRegressor(build_fn=create_dnn_model,
                               verbose=False, epochs=1000,
                               hl=1, hu=36)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [75]: %time cross_val_score(model, f, l, cv=5)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         CPU times: user 18.6 s, sys: 2.17 s, total: 20.8 s
         Wall time: 14.6 s

Out[75]: array([       -0.02,        -0.01,        -0.00,        -0.00,
                       -0.01])

In [76]: model = KerasRegressor(build_fn=create_dnn_model,
                               verbose=False, epochs=1000,
                               hl=3, hu=256)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [77]: %time cross_val_score(model, f, l, cv=5)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         CPU times: user 1min 5s, sys: 11.6 s, total: 1min 16s
         Wall time: 30.1 s

Out[77]: array([       -0.08,        -0.00,        -0.00,        -0.00,
                       -0.05])

1

具有容量的神经网络的包装类

2

具有容量的神经网络的交叉验证

3

具有容量的神经网络的包装类

4

具有容量的神经网络的交叉验证

避免过拟合

过拟合指的是模型在训练数据集上表现比在验证和测试数据集上表现要好得多,这在机器学习中一般以及在金融领域尤其要避免。适当的评估程序和分析,比如交叉验证,有助于预防过拟合并找到合适的模型容量。

结论

本章提供了一个机器学习过程的蓝图。所呈现的主要元素如下:

学习

机器学习到底意味着什么?

数据

使用什么原始数据以及哪些(预处理过的)特征和标签数据?

成功

鉴于数据间接定义的问题(估计、分类等),什么是成功的适当衡量标准?

容量

模型容量扮演着什么角色,以及针对手头问题,什么样的容量可能是合适的?

评估

鉴于训练模型的目的,如何评估模型的性能?

偏差和方差

针对手头问题,哪种模型更适合:那些具有相当高的偏差还是相当高的方差?

交叉验证

对于非序列型数据集,当在不同配置上进行交叉验证时,模型在训练和验证数据子集上的表现如何?

此蓝图松散应用于后续章节中的多个实际金融用例。有关机器学习作为过程的更多背景信息和细节,请参阅本章末尾列出的参考文献。

参考文献

本章引用的书籍和论文:

  • Chollet,François。2017。深度学习与 Python。Shelter Island:Manning。

  • Domingos,Pedro。2015。大师算法:寻找终极学习机器的探索如何重塑我们的世界。纽约:基础书籍。

  • Goodfellow,Ian,Yoshua Bengio 和 Aaron Courville。2016。深度学习。剑桥:麻省理工学院出版社。http://deeplearningbook.org

  • Harari,Yuval Noah。2015。人类大未来:明日简史。伦敦:Harvill Secker。

  • Mitchell,Tom M。1997。机器学习。纽约:麦格劳希尔。

  • VanderPlas,Jake。2017。Python 数据科学手册。Sebastopol:O’Reilly。

¹在这个上下文中提到的经验法则通常是“60%,20%,20%”,用于将给定数据集分割为训练、验证和测试数据子集。

第六章:AI 优先金融

计算接收信息并对其进行转换,实现数学家所称的函数……如果你拥有一个函数,输入全球所有的金融数据并输出最佳股票购买建议,你很快就会变得非常富有。

Max Tegmark (2017)

本章旨在将数据驱动的金融与前一章的机器学习方法结合起来。这仅代表了这一努力的开端,因为首次使用神经网络来发现统计上的非效率性。“有效市场”讨论了有效市场假说,并使用 OLS 回归根据金融时间序列数据加以说明。“基于收益数据的市场预测”首次应用神经网络和 OLS 回归来预测金融工具价格的未来走向(“市场走向”)。分析仅依赖于收益数据。“添加更多特征的市场预测”混合了更多特征,如典型的财务指标。在这一背景下,初步结果表明统计上的非效率性可能确实存在。这在“市场预测—日内交易”中得到了证实,与每日结束数据相比,这里使用的是日内数据。最后,“结论”讨论了大数据与人工智能在某些领域的有效性,并认为 AI 优先、无理论的金融可能是摆脱传统金融理论谬误的一种途径。

有效市场

其中一种得到最强实证支持的假设是有效市场假说(EMH)。它也被称为随机行走假说(RWH)。¹ 简单地说,该假说认为某一时间点上的金融工具价格反映了此时所有可用信息。如果 EMH 成立,讨论股票价格是否过高或过低将是毫无意义的。根据 EMH,股票价格始终恰好处于其合适水平,考虑了当前可用信息。

自从上世纪 60 年代提出和首次讨论有效市场理论(EMH)以来,已经付出了大量努力来完善和形式化这一理念。Jensen(1978)中提出的定义至今仍在使用。Jensen 将有效市场定义如下:

一个市场在信息集合θ t的条件下是有效的,如果根据信息集合θ t进行交易是无法获得经济利润的。这里的经济利润指的是扣除所有成本后的风险调整收益。

在这个背景下,Jensen 区分了三种市场效率形式:

弱式 EMH

在这种情况下,信息集θ t仅包括市场过去的价格和回报历史。

EMH 的半强形式

在这种情况下,信息集θ t被认为是所有公开可获得的信息,包括过去的价格和回报历史,以及财务报告、新闻文章、天气数据等等。

EMH 的强形式

当信息集θ t包括任何人可获得的所有信息时(即,甚至包括私人信息)。

无论假设了哪种形式,EMH 的影响都是深远的。在他关于 EMH 的开创性文章中,Fama(1965)得出了以下结论:

多年来,经济学家、统计学家和金融教师一直对股价行为的模型进行开发和测试。从这项研究中发展而来的一个重要模型是随机漫步理论。这个理论严重质疑了许多其他描述和预测股价行为的方法——这些方法在学术界之外颇受欢迎。例如,我们将在后面看到,如果随机漫步理论准确描述了现实,那么各种用于预测股价的“技术”或“图表”程序完全没有价值。

换句话说,如果 EMH 成立,那么任何为了实现超额市场回报而进行的研究或数据分析在实践中都应该是无用的。另一方面,一个价值数万亿美元的资产管理行业已经发展起来,承诺通过严格的研究和资本的积极管理实现这种超额市场回报。特别是,对冲基金行业是基于承诺提供alpha——即超额市场回报甚至独立于市场回报,至少在很大程度上是如此。近期Preqin的一项研究数据显示了实现这种承诺有多么困难。该研究报告称,2018 年 Preqin 全策略对冲基金指数下跌了-3.42%。研究覆盖的所有对冲基金中,近 40%在那一年经历了 5%或更大的损失。

如果股票价格(或任何其他金融工具的价格)遵循标准随机漫步,则其回报通常服从均值为零的正态分布。股票价格上涨的概率和下跌的概率均为 50%。在这种情况下,从最小二乘意义上讲,预测明天股票价格的最佳预测变量是今天的股票价格。这是由于随机漫步的马尔可夫性质,即未来股票价格的分布与价格过程的历史无关;它只依赖于当前价格水平。因此,在随机漫步的背景下,分析历史价格(或回报)对于预测未来价格是无用的。

在此背景下,可以实施一个半正式的有效市场检验。² 获取一个金融时间序列,将价格数据滞后多次,使用滞后价格数据作为 OLS 回归的特征数据,使用当前价格水平作为标签数据。这与依赖历史价格形态来预测未来价格的图表技术精神相似。

以下 Python 代码基于多个金融工具(可交易和不可交易的)的滞后价格数据实施了这样的分析。首先,导入数据及其可视化(见图 6-1):

In [1]: import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        pd.set_option('precision', 4)
        np.set_printoptions(suppress=True, precision=4)

In [2]: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: data = pd.read_csv(url, index_col=0, parse_dates=True).dropna()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [4]: (data / data.iloc[0]).plot(figsize=(10, 6), cmap='coolwarm');  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

1

将数据读入DataFrame对象中

2

绘制标准化的时间序列数据

aiif 0601

图 6-1. 标准化的时间序列数据(每日末)

其次,对所有金融时间序列的价格数据进行滞后,并存储在DataFrame对象中:

In [5]: lags = 7  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [6]: def add_lags(data, ric, lags):
            cols = []
            df = pd.DataFrame(data[ric])
            for lag in range(1, lags + 1):
                col = 'lag_{}'.format(lag)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                df[col] = df[ric].shift(lag)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                cols.append(col)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
            df.dropna(inplace=True)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
            return df, cols

In [7]: dfs = {}
        for sym in data.columns:
            df, cols = add_lags(data, sym, lags)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
            dfs[sym] = df  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)

In [8]: dfs[sym].head(7)  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
Out[8]:                GLD   lag_1   lag_2   lag_3   lag_4   lag_5   lag_6   lag_7
        Date
        2010-01-13  111.54  110.49  112.85  111.37  110.82  111.51  109.70  109.80
        2010-01-14  112.03  111.54  110.49  112.85  111.37  110.82  111.51  109.70
        2010-01-15  110.86  112.03  111.54  110.49  112.85  111.37  110.82  111.51
        2010-01-19  111.52  110.86  112.03  111.54  110.49  112.85  111.37  110.82
        2010-01-20  108.94  111.52  110.86  112.03  111.54  110.49  112.85  111.37
        2010-01-21  107.37  108.94  111.52  110.86  112.03  111.54  110.49  112.85
        2010-01-22  107.17  107.37  108.94  111.52  110.86  112.03  111.54  110.49

1

滞后数(以交易日计)

2

创建列名

3

滞后价格数据

4

将列名添加到list对象中

5

删除所有不完整的数据行

6

为每个财务时间序列创建滞后数据

7

将结果存储在dict对象中

8

展示滞后价格数据的示例

第三,准备好数据后,OLS 回归分析变得直观易行。图 6-2 展示了平均最优回归结果。毫无疑问,仅滞后一天的价格数据具有最高的解释力。其权重接近于 1,支持这样一个观点:金融工具明天价格的最佳预测者是今天的价格。对每个财务时间序列单独进行的回归结果也是如此:

In [9]: regs = {}
        for sym in data.columns:
            df = dfs[sym]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
            reg = np.linalg.lstsq(df[cols], df[sym], rcond=-1)[0]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
            regs[sym] = reg  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [10]: rega = np.stack(tuple(regs.values()))  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [11]: regd = pd.DataFrame(rega, columns=cols, index=data.columns)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [12]: regd  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[12]:          lag_1   lag_2   lag_3   lag_4   lag_5   lag_6   lag_7
         AAPL.O  1.0106 -0.0592  0.0258  0.0535 -0.0172  0.0060 -0.0184
         MSFT.O  0.8928  0.0112  0.1175 -0.0832 -0.0258  0.0567  0.0323
         INTC.O  0.9519  0.0579  0.0490 -0.0772 -0.0373  0.0449  0.0112
         AMZN.O  0.9799 -0.0134  0.0206  0.0007  0.0525 -0.0452  0.0056
         GS.N    0.9806  0.0342 -0.0172  0.0042 -0.0387  0.0585 -0.0215
         SPY     0.9692  0.0067  0.0228 -0.0244 -0.0237  0.0379  0.0121
         .SPX    0.9672  0.0106  0.0219 -0.0252 -0.0318  0.0515  0.0063
         .VIX    0.8823  0.0591 -0.0289  0.0284 -0.0256  0.0511  0.0306
         EUR=    0.9859  0.0239 -0.0484  0.0508 -0.0217  0.0149 -0.0055
         XAU=    0.9864  0.0069  0.0166 -0.0215  0.0044  0.0198 -0.0125
         GDX     0.9765  0.0096 -0.0039  0.0223 -0.0364  0.0379 -0.0065
         GLD     0.9766  0.0246  0.0060 -0.0142 -0.0047  0.0223 -0.0106

In [13]: regd.mean().plot(kind='bar', figsize=(10, 6));  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

1

获取当前时间序列的数据

2

实施回归分析。

3

将最优回归参数存储在dict对象中。

4

将最优结果合并为单个ndarray对象。

5

将结果放入DataFrame对象并展示。

6

可视化每个滞后期的平均最优回归参数(权重)。

aiif 0602

图 6-2。滞后价格的平均最优回归参数。

鉴于这种半正式分析,至少在其弱形式上,EMH 似乎有强有力的支持证据。值得注意的是,在此实施的 OLS 回归分析违反了几个假设。其中之一是假设特征在彼此间不相关,而实际上它们应该与标签数据高度相关。然而,滞后价格数据导致特征高度相关。以下 Python 代码展示了相关数据,显示所有特征之间几乎完美的相关性。这解释了为什么只需一个特征(“滞后 1”)就足以完成基于 OLS 回归方法的近似和预测。添加更多高度相关的特征并不会带来任何改进。另一个违反的基本假设是时间序列数据的平稳性,以下代码也对此进行了测试:³

In [14]: dfs[sym].corr()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[14]:           GLD   lag_1   lag_2   lag_3   lag_4   lag_5   lag_6   lag_7
         GLD    1.0000  0.9972  0.9946  0.9920  0.9893  0.9867  0.9841  0.9815
         lag_1  0.9972  1.0000  0.9972  0.9946  0.9920  0.9893  0.9867  0.9842
         lag_2  0.9946  0.9972  1.0000  0.9972  0.9946  0.9920  0.9893  0.9867
         lag_3  0.9920  0.9946  0.9972  1.0000  0.9972  0.9946  0.9920  0.9893
         lag_4  0.9893  0.9920  0.9946  0.9972  1.0000  0.9972  0.9946  0.9920
         lag_5  0.9867  0.9893  0.9920  0.9946  0.9972  1.0000  0.9972  0.9946
         lag_6  0.9841  0.9867  0.9893  0.9920  0.9946  0.9972  1.0000  0.9972
         lag_7  0.9815  0.9842  0.9867  0.9893  0.9920  0.9946  0.9972  1.0000

In [15]: from statsmodels.tsa.stattools import adfuller  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [16]: adfuller(data[sym].dropna())  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[16]: (-1.9488969577009954,
          0.3094193074034718,
          0,
          2515,
          {'1%': -3.4329527780962255,
           '5%': -2.8626898965523724,
           '10%': -2.567382133955709},
          8446.683102944744)

1

展示了滞后时间序列之间的相关性。

2

使用增广迪基-富勒检验稳定性。

综上所述,如果 EMH 成立,积极或算法投资组合管理或交易将毫无经济意义。简单地投资于股票或 MVP 意义上的高效投资组合,并长期 passively 持有投资,将会得到至少与不作任何努力相当的,如果不是更高的回报。根据 CAPM 和 MVP,投资者愿意承受的风险越高,预期回报也应该越高。事实上,正如 Copeland 等人(2005 年,第十章)指出的那样,CAPM 和 EMH 对金融市场形成了一个联合假设:如果 EMH 被拒绝,则 CAPM 也必须被拒绝,因为其推导假设 EMH 成立。

基于收益数据的市场预测。

正如第二章所示,机器学习(ML),特别是深度学习(DL)算法,在近年来在一些长期以来对标准统计或数学方法有抵抗力的领域中取得了突破。那么在金融市场呢?ML 和 DL 算法能否发现传统金融计量方法(如 OLS 回归)失效的地方?当然,对于这些问题目前还没有简单明了的答案。

然而,一些具体的例子可能会为可能的答案提供启示。为此,使用与前一节相同的数据,从价格数据中派生对数收益。其目的是比较 OLS 回归和神经网络在预测不同时间序列的下一日价格走势方面的表现。当前阶段的目标是发现统计无效经济无效的差异。当模型能够以一定优势(例如,预测准确率达到 55%或 60%的情况下)预测未来价格走势时,称为统计无效。只有当这种统计无效能够通过考虑交易成本等因素实现盈利的交易策略时,才称为经济无效。

分析的第一步是创建带有滞后对数收益数据的数据集。还对归一化的滞后对数收益数据进行了平稳性测试(已给出),并测试了特征之间的相关性(无相关性)。由于以下分析仅依赖于与时间序列相关的数据,因此它们处理的是弱式市场效率

In [17]: rets = np.log(data / data.shift(1))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [18]: rets.dropna(inplace=True)

In [19]: dfs = {}
         for sym in data:
             df, cols = add_lags(rets, sym, lags)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             mu, std = df[cols].mean(), df[cols].std()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             df[cols] = (df[cols] - mu) / std  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             dfs[sym] = df

In [20]: dfs[sym].head()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[20]:                GLD   lag_1   lag_2   lag_3   lag_4   lag_5   lag_6   lag_7
         Date
         2010-01-14  0.0044  0.9570 -2.1692  1.3386  0.4959 -0.6434  1.6613 -0.1028
         2010-01-15 -0.0105  0.4379  0.9571 -2.1689  1.3388  0.4966 -0.6436  1.6614
         2010-01-19  0.0059 -1.0842  0.4385  0.9562 -2.1690  1.3395  0.4958 -0.6435
         2010-01-20 -0.0234  0.5967 -1.0823  0.4378  0.9564 -2.1686  1.3383  0.4958
         2010-01-21 -0.0145 -2.4045  0.5971 -1.0825  0.4379  0.9571 -2.1680  1.3384

In [21]: adfuller(dfs[sym]['lag_1'])  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[21]: (-51.568251505825536,
          0.0,
          0,
          2507,
          {'1%': -3.4329610922579095,
           '5%': -2.8626935681060375,
           '10%': -2.567384088736619},
          7017.165474260225)

In [22]: dfs[sym].corr()  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[22]:           GLD   lag_1   lag_2       lag_3   lag_4       lag_5   lag_6   lag_7
         GLD    1.0000 -0.0297  0.0003  1.2635e-02 -0.0026 -5.9392e-03  0.0099 -0.0013
         lag_1 -0.0297  1.0000 -0.0305  8.1418e-04  0.0128 -2.8765e-03 -0.0053  0.0098
         lag_2  0.0003 -0.0305  1.0000 -3.1617e-02  0.0003  1.3234e-02 -0.0043 -0.0052
         lag_3  0.0126  0.0008 -0.0316  1.0000e+00 -0.0313 -6.8542e-06  0.0141 -0.0044
         lag_4 -0.0026  0.0128  0.0003 -3.1329e-02  1.0000 -3.1761e-02  0.0002  0.0141
         lag_5 -0.0059 -0.0029  0.0132 -6.8542e-06 -0.0318  1.0000e+00 -0.0323  0.0002
         lag_6  0.0099 -0.0053 -0.0043  1.4115e-02  0.0002 -3.2289e-02  1.0000 -0.0324
         lag_7 -0.0013  0.0098 -0.0052 -4.3869e-03  0.0141  2.1707e-04 -0.0324  1.0000

1

从价格数据中派生对数收益

2

滞后对数收益数据

3

对特征数据应用高斯归一化处理⁴

4

显示滞后收益数据的样本

5

测试时间序列数据的平稳性

6

显示特征数据的相关性

首先,实施 OLS 回归并生成回归结果的预测。分析是在完整数据集上实施的。它将展示 OLS 回归在样本内的表现如何。OLS 回归预测下一日价格走势的准确率略高于 50%,除了一个例外:

In [23]: from sklearn.metrics import accuracy_score

In [24]: %%time
         for sym in data:
             df = dfs[sym]
             reg = np.linalg.lstsq(df[cols], df[sym], rcond=-1)[0]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             pred = np.dot(df[cols], reg)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             acc = accuracy_score(np.sign(df[sym]), np.sign(pred))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             print(f'OLS | {sym:10s} | acc={acc:.4f}')
         OLS | AAPL.O     | acc=0.5056
         OLS | MSFT.O     | acc=0.5088
         OLS | INTC.O     | acc=0.5040
         OLS | AMZN.O     | acc=0.5048
         OLS | GS.N       | acc=0.5080
         OLS | SPY        | acc=0.5080
         OLS | .SPX       | acc=0.5167
         OLS | .VIX       | acc=0.5291
         OLS | EUR=       | acc=0.4984
         OLS | XAU=       | acc=0.5207
         OLS | GDX        | acc=0.5307
         OLS | GLD        | acc=0.5072
         CPU times: user 201 ms, sys: 65.8 ms, total: 267 ms
         Wall time: 60.8 ms

1

回归步骤

2

预测步骤

3

预测的准确性

其次,再次进行相同的分析,但这次使用scikit-learn中的神经网络作为学习和预测的模型。内样本预测准确率始终高于 50%,在某些情况下甚至高于 60%:

In [25]: from sklearn.neural_network import MLPRegressor

In [26]: %%time
         for sym in data.columns:
             df = dfs[sym]
             model = MLPRegressor(hidden_layer_sizes=[512],
                                  random_state=100,
                                  max_iter=1000,
                                  early_stopping=True,
                                  validation_fraction=0.15,
                                  shuffle=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             model.fit(df[cols], df[sym])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             pred = model.predict(df[cols])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             acc = accuracy_score(np.sign(df[sym]), np.sign(pred))  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
             print(f'MLP | {sym:10s} | acc={acc:.4f}')
         MLP | AAPL.O     | acc=0.6005
         MLP | MSFT.O     | acc=0.5853
         MLP | INTC.O     | acc=0.5766
         MLP | AMZN.O     | acc=0.5510
         MLP | GS.N       | acc=0.6527
         MLP | SPY        | acc=0.5419
         MLP | .SPX       | acc=0.5399
         MLP | .VIX       | acc=0.6579
         MLP | EUR=       | acc=0.5642
         MLP | XAU=       | acc=0.5522
         MLP | GDX        | acc=0.6029
         MLP | GLD        | acc=0.5259
         CPU times: user 1min 37s, sys: 6.74 s, total: 1min 44s
         Wall time: 14 s

1

模型实例化

2

模型拟合

3

预测步骤

4

准确度计算

第三,再次进行相同的分析,但使用Keras包中的神经网络。准确性结果与MLPRegressor相似,但平均准确率更高:

In [27]: import tensorflow as tf
         from keras.layers import Dense
         from keras.models import Sequential
         Using TensorFlow backend.

In [28]: np.random.seed(100)
         tf.random.set_seed(100)

In [29]: def create_model(problem='regression'):  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             model = Sequential()
             model.add(Dense(512, input_dim=len(cols),
                             activation='relu'))
             if problem == 'regression':
                 model.add(Dense(1, activation='linear'))
                 model.compile(loss='mse', optimizer='adam')
             else:
                 model.add(Dense(1, activation='sigmoid'))
                 model.compile(loss='binary_crossentropy', optimizer='adam')
             return model

In [30]: %%time
         for sym in data.columns[:]:
             df = dfs[sym]
             model = create_model()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             model.fit(df[cols], df[sym], epochs=25, verbose=False)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             pred = model.predict(df[cols])  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
             acc = accuracy_score(np.sign(df[sym]), np.sign(pred))  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
             print(f'DNN | {sym:10s} | acc={acc:.4f}')
         DNN | AAPL.O     | acc=0.6292
         DNN | MSFT.O     | acc=0.5981
         DNN | INTC.O     | acc=0.6073
         DNN | AMZN.O     | acc=0.5781
         DNN | GS.N       | acc=0.6196
         DNN | SPY        | acc=0.5829
         DNN | .SPX       | acc=0.6077
         DNN | .VIX       | acc=0.6392
         DNN | EUR=       | acc=0.5845
         DNN | XAU=       | acc=0.5881
         DNN | GDX        | acc=0.5829
         DNN | GLD        | acc=0.5666
         CPU times: user 34.3 s, sys: 5.34 s, total: 39.6 s
         Wall time: 23.1 s

1

模型创建函数

2

模型实例化

3

模型拟合

4

预测步骤

5

准确度计算

这个简单的例子表明,神经网络可以在预测下一天的价格走势方向上显著优于 OLS 回归。然而,当测试两种模型类型的外样本性能时,情况会如何变化?

为此,分析重复进行,但在前 80%的数据上实施训练(拟合)步骤,而在剩余的 20%上进行性能测试。首先实施 OLS 回归。外样本 OLS 回归显示与内样本类似的准确性水平—约为 50%:

In [31]: split = int(len(dfs[sym]) * 0.8)

In [32]: %%time
         for sym in data.columns:
             df = dfs[sym]
             train = df.iloc[:split]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             reg = np.linalg.lstsq(train[cols], train[sym], rcond=-1)[0]
             test = df.iloc[split:]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             pred = np.dot(test[cols], reg)
             acc = accuracy_score(np.sign(test[sym]), np.sign(pred))
             print(f'OLS | {sym:10s} | acc={acc:.4f}')
         OLS | AAPL.O     | acc=0.5219
         OLS | MSFT.O     | acc=0.4960
         OLS | INTC.O     | acc=0.5418
         OLS | AMZN.O     | acc=0.4841
         OLS | GS.N       | acc=0.4980
         OLS | SPY        | acc=0.5020
         OLS | .SPX       | acc=0.5120
         OLS | .VIX       | acc=0.5458
         OLS | EUR=       | acc=0.4482
         OLS | XAU=       | acc=0.5299
         OLS | GDX        | acc=0.5159
         OLS | GLD        | acc=0.5100
         CPU times: user 200 ms, sys: 60.6 ms, total: 261 ms
         Wall time: 61.7 ms

1

创建训练数据子集

2

创建测试数据子集

MLPRegressor模型在外样本中的表现要比内样本差得多,并且与 OLS 回归结果相似:

In [34]: %%time
         for sym in data.columns:
             df = dfs[sym]
             train = df.iloc[:split]
             model = MLPRegressor(hidden_layer_sizes=[512],
                                  random_state=100,
                                  max_iter=1000,
                                  early_stopping=True,
                                  validation_fraction=0.15,
                                  shuffle=False)
             model.fit(train[cols], train[sym])
             test = df.iloc[split:]
             pred = model.predict(test[cols])
             acc = accuracy_score(np.sign(test[sym]), np.sign(pred))
             print(f'MLP | {sym:10s} | acc={acc:.4f}')
         MLP | AAPL.O     | acc=0.4920
         MLP | MSFT.O     | acc=0.5279
         MLP | INTC.O     | acc=0.5279
         MLP | AMZN.O     | acc=0.4641
         MLP | GS.N       | acc=0.5040
         MLP | SPY        | acc=0.5259
         MLP | .SPX       | acc=0.5478
         MLP | .VIX       | acc=0.5279
         MLP | EUR=       | acc=0.4980
         MLP | XAU=       | acc=0.5239
         MLP | GDX        | acc=0.4880
         MLP | GLD        | acc=0.5000
         CPU times: user 1min 39s, sys: 4.98 s, total: 1min 44s
         Wall time: 13.7 s

对于来自KerasSequential模型,同样适用,其外样本数据显示的准确度值在 50%阈值上下浮动几个百分点:

In [35]: %%time
         for sym in data.columns:
             df = dfs[sym]
             train = df.iloc[:split]
             model = create_model()
             model.fit(train[cols], train[sym], epochs=50, verbose=False)
             test = df.iloc[split:]
             pred = model.predict(test[cols])
             acc = accuracy_score(np.sign(test[sym]), np.sign(pred))
             print(f'DNN | {sym:10s} | acc={acc:.4f}')
         DNN | AAPL.O     | acc=0.5179
         DNN | MSFT.O     | acc=0.5598
         DNN | INTC.O     | acc=0.4821
         DNN | AMZN.O     | acc=0.4920
         DNN | GS.N       | acc=0.5179
         DNN | SPY        | acc=0.4861
         DNN | .SPX       | acc=0.5100
         DNN | .VIX       | acc=0.5378
         DNN | EUR=       | acc=0.4661
         DNN | XAU=       | acc=0.4602
         DNN | GDX        | acc=0.4841
         DNN | GLD        | acc=0.5378
         CPU times: user 50.4 s, sys: 7.52 s, total: 57.9 s
         Wall time: 32.9 s

弱有效市场效率

尽管将其标记为弱有效市场效率可能暗示着其他情况,但从只有时间序列相关数据可以用于识别统计非效率的角度来看,它是最难的形式。对于半强有效形式,可以添加任何其他公开可用数据源以提高预测准确性。

基于本节选择的方法,市场至少在弱有效形式上似乎是有效的。仅仅基于 OLS 回归或神经网络分析历史回报模式可能不足以发现统计上的非效率。

在本节选择的方法中,有两个主要元素可以调整,以期改善预测结果:

特征

除了普通价格和收益数据外,还可以向数据添加其他特征,例如技术指标(例如简单移动平均线,或简称为 SMA)。希望在技术图表主义者的传统中,这些指标能够提高预测准确性。

条形长度

与使用每日结束数据不同,日内数据可能允许更高的预测准确性。在这里,希望更有可能在一天内发现统计效率低下,而不是在每天结束时,当所有市场参与者通常会最关注做出最后交易时,考虑到所有可用信息。

以下两个部分讨论了这些要素。

更多特征的市场预测

在交易中,长期以来一直有使用技术指标来生成基于观察到的模式的买入或卖出信号的传统。这些技术指标,基本上任何一种,也可以用作训练神经网络的特征。

以下 Python 代码使用简单移动平均线、滚动最小和最大值、动量和滚动波动率作为特征:

In [36]: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'

In [37]: data = pd.read_csv(url, index_col=0, parse_dates=True).dropna()

In [38]: def add_lags(data, ric, lags, window=50):
             cols = []
             df = pd.DataFrame(data[ric])
             df.dropna(inplace=True)
             df['r'] = np.log(df / df.shift())
             df['sma'] = df[ric].rolling(window).mean()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             df['min'] = df[ric].rolling(window).min()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             df['max'] = df[ric].rolling(window).max()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             df['mom'] = df['r'].rolling(window).mean()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
             df['vol'] = df['r'].rolling(window).std()  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
             df.dropna(inplace=True)
             df['d'] = np.where(df['r'] > 0, 1, 0)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
             features = [ric, 'r', 'd', 'sma', 'min', 'max', 'mom', 'vol']
             for f in features:
                 for lag in range(1, lags + 1):
                     col = f'{f}_lag_{lag}'
                     df[col] = df[f].shift(lag)
                     cols.append(col)
             df.dropna(inplace=True)
             return df, cols

In [39]: lags = 5

In [40]: dfs = {}
         for ric in data:
             df, cols = add_lags(data, ric, lags)
             dfs[ric] = df.dropna(), cols

1

简单移动平均线(SMA)

2

滚动最小值

3

滚动最大值

4

动量作为对数收益率的平均值

5

滚动波动率

6

方向作为二进制特征

技术指标作为特征

正如前面的例子所示,基本上任何用于投资或日内交易的传统技术指标都可以作为训练 ML 算法的特征。在这种意义上,人工智能和机器学习并不一定会使这些指标变得过时,相反,它们确实可以丰富基于 ML 的交易策略的推导。

样本内,当考虑到新特征并对其进行训练标准化时,MLPClassifier模型的表现要好得多。KerasSequential模型在训练的周期数中达到了约 70%的准确率。根据经验,通过增加训练周期数和/或神经网络的容量,这些准确率可以轻松提高:

In [41]: from sklearn.neural_network import MLPClassifier

In [42]: %%time
         for ric in data:
             model = MLPClassifier(hidden_layer_sizes=[512],
                                   random_state=100,
                                   max_iter=1000,
                                   early_stopping=True,
                                   validation_fraction=0.15,
                                   shuffle=False)
             df, cols = dfs[ric]
             df[cols] = (df[cols] - df[cols].mean()) / df[cols].std()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             model.fit(df[cols], df['d'])
             pred = model.predict(df[cols])
             acc = accuracy_score(df['d'], pred)
             print(f'IN-SAMPLE | {ric:7s} | acc={acc:.4f}')
         IN-SAMPLE | AAPL.O  | acc=0.5510
         IN-SAMPLE | MSFT.O  | acc=0.5376
         IN-SAMPLE | INTC.O  | acc=0.5607
         IN-SAMPLE | AMZN.O  | acc=0.5559
         IN-SAMPLE | GS.N    | acc=0.5794
         IN-SAMPLE | SPY     | acc=0.5729
         IN-SAMPLE | .SPX    | acc=0.5941
         IN-SAMPLE | .VIX    | acc=0.6940
         IN-SAMPLE | EUR=    | acc=0.5766
         IN-SAMPLE | XAU=    | acc=0.5672
         IN-SAMPLE | GDX     | acc=0.5847
         IN-SAMPLE | GLD     | acc=0.5567
         CPU times: user 1min 1s, sys: 4.5 s, total: 1min 6s
         Wall time: 9.05 s

In [43]: %%time
         for ric in data:
             model = create_model('classification')
             df, cols = dfs[ric]
             df[cols] = (df[cols] - df[cols].mean()) / df[cols].std()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             model.fit(df[cols], df['d'], epochs=50, verbose=False)
             pred = np.where(model.predict(df[cols]) > 0.5, 1, 0)
             acc = accuracy_score(df['d'], pred)
             print(f'IN-SAMPLE | {ric:7s} | acc={acc:.4f}')
         IN-SAMPLE | AAPL.O  | acc=0.7156
         IN-SAMPLE | MSFT.O  | acc=0.7156
         IN-SAMPLE | INTC.O  | acc=0.7046
         IN-SAMPLE | AMZN.O  | acc=0.6640
         IN-SAMPLE | GS.N    | acc=0.6855
         IN-SAMPLE | SPY     | acc=0.6696
         IN-SAMPLE | .SPX    | acc=0.6579
         IN-SAMPLE | .VIX    | acc=0.7489
         IN-SAMPLE | EUR=    | acc=0.6737
         IN-SAMPLE | XAU=    | acc=0.7143
         IN-SAMPLE | GDX     | acc=0.6826
         IN-SAMPLE | GLD     | acc=0.7078
         CPU times: user 1min 5s, sys: 7.06 s, total: 1min 12s
         Wall time: 44.3 s

1

规范化特征数据

这些改进能够转化为样本外预测准确性吗?以下 Python 代码重复了分析,这次使用了之前使用的训练和测试拆分。不幸的是,情况最好是复杂的。与仅依赖滞后收益数据作为特征的方法相比,并没有真正的改进。对于选定的工具,与 50%基准相比,预测准确率似乎有几个百分点的优势。然而,对于其他工具,准确率仍然低于 50%——正如对于MLPClassifier模型所示:

In [44]: def train_test_model(model):
             for ric in data:
                 df, cols = dfs[ric]
                 split = int(len(df) * 0.85)
                 train = df.iloc[:split].copy()
                 mu, std = train[cols].mean(), train[cols].std()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 train[cols] = (train[cols] - mu) / std
                 model.fit(train[cols], train['d'])
                 test = df.iloc[split:].copy()
                 test[cols] = (test[cols] - mu) / std
                 pred = model.predict(test[cols])
                 acc = accuracy_score(test['d'], pred)
                 print(f'OUT-OF-SAMPLE | {ric:7s} | acc={acc:.4f}')

In [45]: model_mlp = MLPClassifier(hidden_layer_sizes=[512],
                                   random_state=100,
                                   max_iter=1000,
                                   early_stopping=True,
                                   validation_fraction=0.15,
                                   shuffle=False)

In [46]: %time train_test_model(model_mlp)
         OUT-OF-SAMPLE | AAPL.O  | acc=0.4432
         OUT-OF-SAMPLE | MSFT.O  | acc=0.4595
         OUT-OF-SAMPLE | INTC.O  | acc=0.5000
         OUT-OF-SAMPLE | AMZN.O  | acc=0.5270
         OUT-OF-SAMPLE | GS.N    | acc=0.4838
         OUT-OF-SAMPLE | SPY     | acc=0.4811
         OUT-OF-SAMPLE | .SPX    | acc=0.5027
         OUT-OF-SAMPLE | .VIX    | acc=0.5676
         OUT-OF-SAMPLE | EUR=    | acc=0.4649
         OUT-OF-SAMPLE | XAU=    | acc=0.5514
         OUT-OF-SAMPLE | GDX     | acc=0.5162
         OUT-OF-SAMPLE | GLD     | acc=0.4946
         CPU times: user 44.9 s, sys: 2.64 s, total: 47.5 s
         Wall time: 6.37 s

1

训练数据集统计数据用于归一化。

优良的样本内表现和不太理想的样本外表现表明神经网络的过拟合可能起到关键作用。避免过拟合的一种方法是使用集成方法,将同一类型的多个训练模型组合起来,以产生更强大的元模型和更好的样本外预测。其中一种方法称为装袋scikit-learn中有这种方法的实现,即BaggingClassifier。使用多个估算器允许每个估算器训练,而不用将它们暴露于完整的训练数据集或所有特征。这应有助于避免过拟合。

以下 Python 代码实现了基于同一类型的多个基础估算器(MLPClassifier)的装袋方法。现在的预测准确率始终高于 50%。一些准确度值超过 55%,在这种情况下可以认为相当高。总体而言,装袋似乎至少在某种程度上避免了过拟合,并显著改善了预测:

In [47]: from sklearn.ensemble import BaggingClassifier

In [48]: base_estimator = MLPClassifier(hidden_layer_sizes=[256],
                                   random_state=100,
                                   max_iter=1000,
                                   early_stopping=True,
                                   validation_fraction=0.15,
                                   shuffle=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [49]: model_bag = BaggingClassifier(base_estimator=base_estimator,  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                                   n_estimators=35,  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                                   max_samples=0.25,  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                                   max_features=0.5,  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                                   bootstrap=False,  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                                   bootstrap_features=True,  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                                   n_jobs=8,  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
                                   random_state=100
                                  )

In [50]: %time train_test_model(model_bag)
         OUT-OF-SAMPLE | AAPL.O  | acc=0.5243
         OUT-OF-SAMPLE | MSFT.O  | acc=0.5703
         OUT-OF-SAMPLE | INTC.O  | acc=0.5027
         OUT-OF-SAMPLE | AMZN.O  | acc=0.5270
         OUT-OF-SAMPLE | GS.N    | acc=0.5243
         OUT-OF-SAMPLE | SPY     | acc=0.5595
         OUT-OF-SAMPLE | .SPX    | acc=0.5514
         OUT-OF-SAMPLE | .VIX    | acc=0.5649
         OUT-OF-SAMPLE | EUR=    | acc=0.5108
         OUT-OF-SAMPLE | XAU=    | acc=0.5378
         OUT-OF-SAMPLE | GDX     | acc=0.5162
         OUT-OF-SAMPLE | GLD     | acc=0.5432
         CPU times: user 2.55 s, sys: 494 ms, total: 3.05 s
         Wall time: 11.1 s

1

基础估算器

2

使用的估算器数量

3

每个估算器使用的训练数据的最大百分比

4

每个估算器使用的最大特征数量

5

是否引导(重用)数据

6

是否引导(重用)特征

7

并行作业数量

日终市场效率

效率市场假说可以追溯到 20 世纪 60 年代和 70 年代,那时日终数据基本上是唯一可用的时间序列数据。在那些日子里(至今如此),可以假设市场参与者在交易会话接近尾声时特别关注他们的头寸和交易。这对股票来说可能更为真实,但对货币来说可能稍微少一些,因为原则上它们是全天交易的。

市场预测盘中

本章尚未产生确凿的证据,但到目前为止实施的分析更倾向于认为市场在日终基础上是弱有效的。那么,关于盘中市场呢?是否有更一致的统计效率低下现象可以发现?为了回答这个问题,需要另一个数据集。以下 Python 代码使用一个数据集,其中包含与日终数据集相同的工具,但现在包含每小时的收盘价。由于交易时间可能因工具而异,数据集是不完整的。不过,这没有问题,因为分析是逐时间序列实施的。

按小时数据的技术实现与每日结束时分析所用的代码基本相同:

In [51]: url = 'http://hilpisch.com/aiif_eikon_id_data.csv'

In [52]: data = pd.read_csv(url, index_col=0, parse_dates=True)

In [53]: data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 5529 entries, 2019-03-01 00:00:00 to 2020-01-01 00:00:00
         Data columns (total 12 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   AAPL.O  3384 non-null   float64
          1   MSFT.O  3378 non-null   float64
          2   INTC.O  3275 non-null   float64
          3   AMZN.O  3381 non-null   float64
          4   GS.N    1686 non-null   float64
          5   SPY     3388 non-null   float64
          6   .SPX    1802 non-null   float64
          7   .VIX    2959 non-null   float64
          8   EUR=    5429 non-null   float64
          9   XAU=    5149 non-null   float64
          10  GDX     3173 non-null   float64
          11  GLD     3351 non-null   float64
         dtypes: float64(12)
         memory usage: 561.5 KB

In [54]: lags = 5

In [55]: dfs = {}
         for ric in data:
             df, cols = add_lags(data, ric, lags)
             dfs[ric] = df, cols

就一天内的市场预测准确度而言,单一神经网络的准确度再次分布在 50%左右,具有相对较大的波动。积木模型的表现更加一致,许多观察到的准确度值都略高于 50%的基准:

In [56]: %time train_test_model(model_mlp)
         OUT-OF-SAMPLE | AAPL.O  | acc=0.5420
         OUT-OF-SAMPLE | MSFT.O  | acc=0.4930
         OUT-OF-SAMPLE | INTC.O  | acc=0.5549
         OUT-OF-SAMPLE | AMZN.O  | acc=0.4709
         OUT-OF-SAMPLE | GS.N    | acc=0.5184
         OUT-OF-SAMPLE | SPY     | acc=0.4860
         OUT-OF-SAMPLE | .SPX    | acc=0.5019
         OUT-OF-SAMPLE | .VIX    | acc=0.4885
         OUT-OF-SAMPLE | EUR=    | acc=0.5130
         OUT-OF-SAMPLE | XAU=    | acc=0.4824
         OUT-OF-SAMPLE | GDX     | acc=0.4765
         OUT-OF-SAMPLE | GLD     | acc=0.5455
         CPU times: user 1min 4s, sys: 5.05 s, total: 1min 9s
         Wall time: 9.56 s

In [57]: %time train_test_model(model_bag)
         OUT-OF-SAMPLE | AAPL.O  | acc=0.5660
         OUT-OF-SAMPLE | MSFT.O  | acc=0.5431
         OUT-OF-SAMPLE | INTC.O  | acc=0.5072
         OUT-OF-SAMPLE | AMZN.O  | acc=0.5110
         OUT-OF-SAMPLE | GS.N    | acc=0.5020
         OUT-OF-SAMPLE | SPY     | acc=0.5120
         OUT-OF-SAMPLE | .SPX    | acc=0.4677
         OUT-OF-SAMPLE | .VIX    | acc=0.5092
         OUT-OF-SAMPLE | EUR=    | acc=0.5242
         OUT-OF-SAMPLE | XAU=    | acc=0.5255
         OUT-OF-SAMPLE | GDX     | acc=0.5085
         OUT-OF-SAMPLE | GLD     | acc=0.5374
         CPU times: user 2.64 s, sys: 439 ms, total: 3.08 s
         Wall time: 12.4 s

一天内市场的效率

即使市场在每日结束时弱有效,它们在一天之内也可能弱无效。这种统计上的无效可能是由于临时失衡、买卖压力、市场过度反应、技术驱动的买卖订单等引起的。核心问题在于,一旦发现这些统计上的无效,是否可以通过特定的交易策略有利地加以利用。

结论

在他们广为引用的文章“数据的不合理有效性”中,Halevy 等人(2009 年)指出经济学家们患有所谓的物理学嫉妒。他们所指的是无法像物理学家那样用数学上优雅的方式解释人类行为,即使是描述复杂的现实世界现象。其中一个例子是阿尔伯特·爱因斯坦可能最为人熟知的公式 E = m c 2,它将能量与物体的质量乘以光速的平方联系起来。

在经济学和金融领域,研究人员几十年来一直试图模仿物理学的方法,推导和证明简单而优雅的方程来解释经济和金融现象。但正如第三章和第四章一起展示的那样,许多最优雅的金融理论在现实金融世界中几乎没有支持性证据,这些理论的简化假设(如正态分布和线性关系)在现实中并不成立。

正如 Halevy 等人(2009 年)在他们的文章中解释的那样,可能存在某些领域(如自然语言及其遵循的规则),这些领域无法推导出简洁而优雅的理论。研究人员可能需要依赖由数据驱动的复杂理论和模型。尤其是对于语言而言,万维网代表着大数据的宝库。而在某些任务上,例如自然语言处理或人类水平的翻译,大数据似乎是训练机器学习和深度学习算法所必需的。

毕竟,金融可能与自然语言更相似,而非物理学。也许确实没有简单而优雅的公式来描述重要的金融现象,如货币汇率的日常变动或股票价格。⁵ 也许真相只能在如今以程序方式提供给金融研究人员和学者的大数据中找到。

本章介绍了揭示真相、发现金融圣杯的探索之始:证明市场实际上并不那么有效。本章相对简单的神经网络方法仅依赖于与时间序列相关的特征进行训练。标签简单明了:市场(金融工具的价格)是涨还是跌。目标是发现统计上的低效,以预测未来市场走向。这反过来代表了经济上利用这种低效的第一步,通过可实施的交易策略。

Agrawal 等人(2018)详细解释了,通过许多例子,预测本身只是一个方面。决策和实施规则详细说明了如何处理某种预测同样重要。在算法交易背景下也是如此:信号(预测)只是开始。难的是优化执行适当的交易,监控活跃交易,实施适当的风险措施—例如止损和止盈订单等。

在追求统计低效性的过程中,本章仅依赖于数据和神经网络。没有涉及理论,也没有假设市场参与者可能如何行动,或类似的推理。主要的建模工作是针对准备特征进行的,这当然代表了建模者认为重要的内容。在所采取的方法中的一个隐含假设是,可以仅基于时间序列相关数据来发现统计低效性。这就是说,市场甚至不是弱有效的—这是三种形式中最难证明的一种。

本书只依赖于金融数据,并将通用的机器学习和深度学习算法和模型应用于其中,这就是本书所称的AI 首先的金融。不需要理论,不需要建模人类行为,不假设分布或关系的本质—只有数据和算法。从这个意义上说,AI 首先的金融也可以标记为无理论无模型的金融

参考文献

本章引用的书籍和论文:

  • Agrawal, Ajay, Joshua Gans, and Avi Goldfarb. 2018. 预测机器:人工智能的简单经济学. 波士顿:哈佛商业评论出版社。

  • Copeland, Thomas, Fred Weston, and Kuldeep Shastri. 2005. 金融理论与公司政策. 第四版. 波士顿:皮尔逊出版社。

  • Fama, Eugene. 1965. “股市价格的随机漫步.” Financial Analysts Journal (9/10): 55-59.

  • Halevy, Alon, Peter Norvig, and Fernando Pereira. 2009. “数据的不合理有效性.” IEEE Intelligent Systems, 专家意见.

  • Hilpisch, Yves. 2018. Python for Finance: Mastering Data-Driven Finance. 第二版. Sebastopol: O’Reilly.

  • Jensen, Michael. 1978. “关于市场效率的一些异常证据.” Journal of Financial Economics 6 (2/3): 95-101.

  • Tegmark, Max. 2017. Life 3.0: Being Human in the Age of Artificial Intelligence. 英国: Penguin Random House.

  • Tsay, Ruey S. 2005. Financial Time Series Analysis. Hoboken: Wiley.

¹ 在本章和本书的目的中,这两个假设被视为相等,尽管随机漫步假说略强于有效市场假说。例如,参见 Copeland 等人 (2005, 第十章).

² 另见 Hilpisch (2018, 第十五章).

³ 有关金融时间序列中平稳性的详细信息,请参阅 Tsay (2005, 第 2.1 节)。Tsay 指出:“时间序列分析的基础是平稳性。”

⁴ 该方法的另一个术语是z-score 标准化

⁵ 当然,还有更简单的金融方面,可以通过简单公式建模。例如,如果相关的对数收益率是 r = 0.01,那么为两年期连续贴现因子 D 的推导如下 D ( r , T ) = exp ( - r T ) = exp ( -0.01 · 2 ) = 0.9802. 人工智能或机器学习在这里无法提供任何好处。

第三部分:统计效率

“市场上存在着模式,” Simons 告诉一位同事。“我知道我们可以找到它们。”¹

Gregory Zuckerman (2019)

本部分的主要目标是应用神经网络和强化学习来发现金融市场(数据)中的统计效率问题。本书中,“统计效率”是指当“预测器”(一般的模型或算法,特别是神经网络)比将上升和下降运动等概率分配给随机预测器更好地预测市场时发现的情况。在算法交易的背景下,拥有这样的预测器是产生超过市场回报的“阿尔法”或市场回报的前提条件。

本部分由三章组成,提供了与密集神经网络(DNNs)、循环神经网络(RNNs)和强化学习(RL)相关的更多背景、细节和示例:

  • 第七章更详细地涵盖了 DNN,并将其应用于预测金融市场运动的问题。历史数据用于生成滞后特征数据和生成二进制标签数据。然后使用这些数据集通过监督学习来训练 DNN。重点放在识别金融市场中的统计效率。在一些示例中,DNN 的样本外预测准确率超过 60%。

  • 第八章关于 RNNs,它们被设计来适应序列数据的特定性质,例如文本数据或时间序列数据。其想法是向网络中添加某种形式的记忆,将以前(历史)信息通过网络(层)传递。本章采取的方法与第七章中的方法接近,其目标是在金融市场数据中发现统计效率。如数值示例所示,RNNs 也可以达到样本外预测准确率超过 60%。

  • 第九章讨论了强化学习作为人工智能的主要成功案例之一。本章讨论了不同的强化学习代理应用于 OpenAI Gym 中的模拟物理环境和本章中开发的金融市场环境。在强化学习中,常用的算法是 Q-learning,本章对其进行了详细讨论,并应用于训练交易机器人。交易机器人展示了可观的样本外财务表现,这通常比仅仅预测准确性更重要。从这个意义上说,本章为第四部分搭建了自然的桥梁,该部分关注的是经济上的统计效率问题。

尽管卷积神经网络(CNNs)是一种非常重要的神经网络类型,在本部分没有详细讨论。附录 C 简明地展示了 CNNs 的应用。在许多情况下,CNNs 也可以应用于本书此部分应用于 DNNs 和 RNNs 问题的问题中。

本部分的方法是实用的,关于所应用的算法和技术略去了许多重要细节。这似乎是有道理的,因为有很多书籍形式和其他形式的良好资源可供查阅技术细节和背景信息。随后的章节在适当时会提供对一些精选、综合的资源的参考。

¹ Gregory Zuckerman. 2019. The Man Who Solved the Market. 纽约:企鹅兰登书屋。

第七章:密集神经网络

如果你试图根据股票市场上的最近价格历史预测股票的走势,你可能不太可能成功,因为价格历史并没有包含太多的预测信息。

François Chollet(2017)

本章讨论密集神经网络的重要方面。之前的章节已经使用了这种类型的神经网络。特别地,来自scikit-learnMLPClassifierMLPRegressor模型,以及用于分类和估计的KerasSequential模型都是密集神经网络(DNNs)。本章专注于Keras,因为它在建模 DNNs 时提供更多的自由度和灵活性。¹

“数据”介绍了其他本章节中使用的外汇(FX)数据集。“基线预测”在新数据集上生成基线内样本预测。在“归一化”中介绍了训练和测试数据的归一化。为了避免过度拟合,“Dropout”和“正则化”讨论了作为流行方法的 dropout 和正则化。Bagging,作为另一种避免过度拟合的方法,在“Bagging”中重新审视。最后,“优化器”比较了可以与Keras DNN 模型一起使用的不同优化器的性能。

尽管本章的开篇引用可能给人希望不大的理由,但本章及整个第三部分的主要目标是通过应用神经网络在金融市场(时间序列)中发现统计效率低下的现象。本章提供的数值结果,例如在某些情况下达到 60%甚至更高的预测准确率,表明至少有一些希望是合理的。

数据

第六章发现了统计效率低下的线索,其中包括 EUR/USD 货币对的日内价格序列。本章及其后续章节关注外汇(FX)作为一种资产类别,特别关注 EUR/USD 货币对。总体而言,与其他资产类别(如 VIX 波动率指数等波动性产品)相比,经济上利用 FX 的统计效率低下通常不那么复杂。对于 FX,通常也可以自由和全面地获取数据。以下数据集来自 Refinitiv Eikon Data API。数据集通过 API 检索。数据集包含开盘价、最高价、最低价和收盘价值。图 7-1 展示了收盘价格的可视化:

In [1]: import os
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        pd.set_option('precision', 4)
        np.set_printoptions(suppress=True, precision=4)
        os.environ['PYTHONHASHSEED'] = '0'

In [2]: url = 'http://hilpisch.com/aiif_eikon_id_eur_usd.csv'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: symbol = 'EUR_USD'

In [4]: raw = pd.read_csv(url, index_col=0, parse_dates=True)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [5]: raw.head()
Out[5]:                        HIGH     LOW    OPEN   CLOSE
        Date
        2019-10-01 00:00:00  1.0899  1.0897  1.0897  1.0899
        2019-10-01 00:01:00  1.0899  1.0896  1.0899  1.0898
        2019-10-01 00:02:00  1.0898  1.0896  1.0898  1.0896
        2019-10-01 00:03:00  1.0898  1.0896  1.0897  1.0898
        2019-10-01 00:04:00  1.0898  1.0896  1.0897  1.0898

In [6]: raw.info()
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 96526 entries, 2019-10-01 00:00:00 to 2019-12-31 23:06:00
        Data columns (total 4 columns):
         #   Column  Non-Null Count  Dtype
        ---  ------  --------------  -----
         0   HIGH    96526 non-null  float64
         1   LOW     96526 non-null  float64
         2   OPEN    96526 non-null  float64
         3   CLOSE   96526 non-null  float64
        dtypes: float64(4)
        memory usage: 3.7 MB

In [7]: data = pd.DataFrame(raw['CLOSE'].loc[:])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        data.columns = [symbol]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [8]: data = data.resample('1h', label='right').last().ffill()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [9]: data.info()
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 2208 entries, 2019-10-01 01:00:00 to 2020-01-01 00:00:00
        Freq: H
        Data columns (total 1 columns):
         #   Column   Non-Null Count  Dtype
        ---  ------   --------------  -----
         0   EUR_USD  2208 non-null   float64
        dtypes: float64(1)
        memory usage: 34.5 KB

In [10]: data.plot(figsize=(10, 6));  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

1

将数据读入DataFrame对象中

2

选择、重新采样并绘制收盘价图表

aiif 0701

图 7-1. EUR/USD 的中间收盘价格(盘中)

基线预测

基于新数据集,从 第六章 中的预测方法被重复使用。首先是创建滞后特征:

In [11]: lags = 5

In [12]: def add_lags(data, symbol, lags, window=20):  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             cols = []
             df = data.copy()
             df.dropna(inplace=True)
             df['r'] = np.log(df / df.shift())
             df['sma'] = df[symbol].rolling(window).mean()
             df['min'] = df[symbol].rolling(window).min()
             df['max'] = df[symbol].rolling(window).max()
             df['mom'] = df['r'].rolling(window).mean()
             df['vol'] = df['r'].rolling(window).std()
             df.dropna(inplace=True)
             df['d'] = np.where(df['r'] > 0, 1, 0)
             features = [symbol, 'r', 'd', 'sma', 'min', 'max', 'mom', 'vol']
             for f in features:
                 for lag in range(1, lags + 1):
                     col = f'{f}_lag_{lag}'
                     df[col] = df[f].shift(lag)
                     cols.append(col)
             df.dropna(inplace=True)
             return df, cols

In [13]: data, cols = add_lags(data, symbol, lags)

1

从第六章略微调整的函数

其次是查看标签数据。在分类中可能出现的一个主要问题是类别不平衡。这意味着,在二元标签的情况下,一个特定类别相对于另一个类别的频率可能较高。这可能导致神经网络简单地预测具有更高频率的类别,因为这已经可以导致低损失和高准确率值。通过应用适当的权重,可以确保在 DNN 训练步骤中两个类别获得相等的重要性:²

In [14]: len(data)
Out[14]: 2183

In [15]: c = data['d'].value_counts()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         c  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[15]: 0    1445
         1     738
         Name: d, dtype: int64

In [16]: def cw(df):  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             c0, c1 = np.bincount(df['d'])
             w0 = (1 / c0) * (len(df)) / 2
             w1 = (1 / c1) * (len(df)) / 2
             return {0: w0, 1: w1}

In [17]: class_weight = cw(data)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [18]: class_weight  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[18]: {0: 0.755363321799308, 1: 1.4789972899728998}

In [19]: class_weight[0] * c[0]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[19]: 1091.5

In [20]: class_weight[1] * c[1]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[20]: 1091.5

1

展示了两个类的频率

2

计算适当的权重以达到均衡权重

3

使用计算得出的权重,两个类别获得相等的权重

第三步是使用Keras创建 DNN 模型,并在完整数据集上训练模型。样本内基线表现约为 60%:

In [21]: import random
         import tensorflow as tf
         from keras.layers import Dense
         from keras.models import Sequential
         from keras.optimizers import Adam
         from sklearn.metrics import accuracy_score
         Using TensorFlow backend.

In [22]: def set_seeds(seed=100):
             random.seed(seed)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             np.random.seed(seed)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             tf.random.set_seed(seed)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [23]: optimizer = Adam(lr=0.001)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [24]: def create_model(hl=1, hu=128, optimizer=optimizer):
             model = Sequential()
             model.add(Dense(hu, input_dim=len(cols),
                             activation='relu'))  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
             for _ in range(hl):
                 model.add(Dense(hu, activation='relu'))  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
             model.add(Dense(1, activation='sigmoid'))  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
             model.compile(loss='binary_crossentropy',  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
                           optimizer=optimizer,  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
                           metrics=['accuracy'])  ![10](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/10.png)
             return model

In [25]: set_seeds()
         model = create_model(hl=1, hu=128)

In [26]: %%time
         model.fit(data[cols], data['d'], epochs=50,
                   verbose=False, class_weight=cw(data))
         CPU times: user 6.44 s, sys: 939 ms, total: 7.38 s
         Wall time: 4.07 s

Out[26]: <keras.callbacks.callbacks.History at 0x7fbfc2ee6690>

In [27]: model.evaluate(data[cols], data['d'])
         2183/2183 [==============================] - 0s 24us/step

Out[27]: [0.582192026280068, 0.6087952256202698]

In [28]: data['p'] = np.where(model.predict(data[cols]) > 0.5, 1, 0)

In [29]: data['p'].value_counts()
Out[29]: 1    1340
         0     843
         Name: p, dtype: int64

1

Python 随机数种子

2

NumPy 随机数种子

3

TensorFlow 随机数种子

4

默认优化器(参见https://oreil.ly/atpu8

5

第一层

6

附加层

7

输出层

8

损失函数(参见https://oreil.ly/cVGVf

9

要使用的优化器

10

要收集的额外指标

对于模型的样本外表现也是如此。它仍然高于 60%。这可以被认为已经相当不错:

In [30]: split = int(len(data) * 0.8)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [31]: train = data.iloc[:split].copy()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [32]: test = data.iloc[split:].copy()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [33]: set_seeds()
         model = create_model(hl=1, hu=128)

In [34]: %%time
         model.fit(train[cols], train['d'],
                   epochs=50, verbose=False,
                   validation_split=0.2, shuffle=False,
                   class_weight=cw(train))
         CPU times: user 4.72 s, sys: 686 ms, total: 5.41 s
         Wall time: 3.14 s

Out[34]: <keras.callbacks.callbacks.History at 0x7fbfc3231250>

In [35]: model.evaluate(train[cols], train['d'])  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         1746/1746 [==============================] - 0s 13us/step

Out[35]: [0.612861613500842, 0.5853379368782043]

In [36]: model.evaluate(test[cols], test['d'])  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         437/437 [==============================] - 0s 16us/step

Out[36]: [0.5946959675858714, 0.6247139573097229]

In [37]: test['p'] = np.where(model.predict(test[cols]) > 0.5, 1, 0)

In [38]: test['p'].value_counts()
Out[38]: 1    291
         0    146
         Name: p, dtype: int64

1

将整个数据集分割…

2

…进入训练数据集…

3

…以及测试数据集。

4

评估样本内表现。

5

评估样本外表现。

图 7-2 显示了训练和验证数据子集的准确率随训练时期的变化情况:

In [39]: res = pd.DataFrame(model.history.history)

In [40]: res[['accuracy', 'val_accuracy']].plot(figsize=(10, 6), style='--');

aiif 0702

图 7-2. 训练和验证准确率数值

本节的分析为使用Keras更为复杂的 DNNs 打下了基础。它展示了一个基准市场预测方法。接下来的部分添加了不同元素,这些元素主要旨在提高样本外模型性能,并避免模型对训练数据的过拟合。

标准化

在“基准预测”中,使用滞后特征。在第 6 章 中,特征数据通过减去每个特征的训练数据均值并除以训练数据的标准偏差进行标准化。这种标准化技术称为高斯标准化,在训练神经网络时往往(几乎总是)证明是一个重要的方面。如下面的 Python 代码及其结果所示,使用标准化后的特征数据能显著提高样本内表现。样本外表现也略有提高。然而,并不能保证通过特征标准化会提高样本外表现:

In [41]: mu, std = train.mean(), train.std()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [42]: train_ = (train - mu) / std  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [43]: set_seeds()
         model = create_model(hl=2, hu=128)

In [44]: %%time
         model.fit(train_[cols], train['d'],
                   epochs=50, verbose=False,
                   validation_split=0.2, shuffle=False,
                   class_weight=cw(train))
         CPU times: user 5.81 s, sys: 879 ms, total: 6.69 s
         Wall time: 3.53 s

Out[44]: <keras.callbacks.callbacks.History at 0x7fbfa51353d0>

In [45]: model.evaluate(train_[cols], train['d'])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         1746/1746 [==============================] - 0s 14us/step

Out[45]: [0.4253406366728084, 0.887170672416687]

In [46]: test_ = (test - mu) / std  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [47]: model.evaluate(test_[cols], test['d'])  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         437/437 [==============================] - 0s 24us/step

Out[47]: [1.1377735263422917, 0.681922197341919]

In [48]: test['p'] = np.where(model.predict(test_[cols]) > 0.5, 1, 0)

In [49]: test['p'].value_counts()
Out[49]: 0    281
         1    156
         Name: p, dtype: int64

1

计算所有训练特征的均值和标准偏差

2

根据高斯标准化对训练数据集进行标准化

3

评估样本内表现

4

根据高斯标准化对测试数据集进行标准化

5

评估样本外表现

经常出现的一个主要问题是过拟合。在图 7-3 中有一个令人印象深刻的可视化,显示训练准确率稳步提高而验证准确率缓慢下降:

In [50]: res = pd.DataFrame(model.history.history)

In [51]: res[['accuracy', 'val_accuracy']].plot(figsize=(10, 6), style='--');

aiif 0703

图 7-3. 训练和验证准确率数值(标准化特征数据)

避免过拟合的三种候选方法是dropout正则化装袋。接下来的部分将讨论这些方法。还将在本章后面讨论所选择优化器的影响。

Dropout

dropout 的概念是神经网络在训练阶段不应使用所有隐藏单元。与人类大脑的类比是,人类经常忘记先前学到的信息。这种做法可以说是保持人类大脑“开放思维”的一种方式。理想情况下,神经网络应该类似:DNN 中的连接不应过于强大,以避免过度拟合训练数据。

从技术上讲,Keras模型在隐藏层之间有额外的层来管理 dropout。主要参数是层中隐藏单元被丢弃的速率。这些丢弃通常以随机方式发生。这可以通过固定seed参数来避免。虽然样本内性能降低了,但样本外性能也略有下降。但是,两个性能指标之间的差异较小,这通常是一种理想情况:

In [52]: from keras.layers import Dropout

In [53]: def create_model(hl=1, hu=128, dropout=True, rate=0.3,
                          optimizer=optimizer):
             model = Sequential()
             model.add(Dense(hu, input_dim=len(cols),
                             activation='relu'))
             if dropout:
                 model.add(Dropout(rate, seed=100))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             for _ in range(hl):
                 model.add(Dense(hu, activation='relu'))
                 if dropout:
                     model.add(Dropout(rate, seed=100))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             model.add(Dense(1, activation='sigmoid'))
             model.compile(loss='binary_crossentropy', optimizer=optimizer,
                          metrics=['accuracy'])
             return model

In [54]: set_seeds()
         model = create_model(hl=1, hu=128, rate=0.3)

In [55]: %%time
         model.fit(train_[cols], train['d'],
                   epochs=50, verbose=False,
                   validation_split=0.15, shuffle=False,
                   class_weight=cw(train))
         CPU times: user 5.46 s, sys: 758 ms, total: 6.21 s
         Wall time: 3.53 s

Out[55]: <keras.callbacks.callbacks.History at 0x7fbfa6386550>

In [56]: model.evaluate(train_[cols], train['d'])
         1746/1746 [==============================] - 0s 20us/step

Out[56]: [0.4423361133190911, 0.7840778827667236]

In [57]: model.evaluate(test_[cols], test['d'])
         437/437 [==============================] - 0s 34us/step

Out[57]: [0.5875822428434883, 0.6430205702781677]

1

在每一层后添加 dropout

如图 7-4 所示,现在训练精度和验证精度不再像以前那样迅速分开:

In [58]: res = pd.DataFrame(model.history.history)

In [59]: res[['accuracy', 'val_accuracy']].plot(figsize=(10, 6), style='--');

aiif 0704

图 7-4. 训练和验证精度值(使用 dropout)

有意忘记

KerasSequential模型中,dropout 模拟了所有人都会经历的情况:忘记以前记住的信息。这是通过在训练期间关闭隐藏层的某些隐藏单元来实现的。实际上,这往往可以更大程度地避免将神经网络过拟合到训练数据上。

正则化

避免过拟合的另一种方法是正则化。通过正则化,神经网络中的大权重在损失(函数)的计算中会受到惩罚。这避免了深度神经网络中某些连接过于强大和占主导地位的情况。正则化可以通过Dense层中的参数来引入Keras DNN。根据所选择的正则化参数,训练和测试精度可以保持非常接近。通常使用两种正则化器,一种基于线性范数l1,另一种基于欧几里得范数l2。以下 Python 代码将正则化添加到模型创建函数中:

In [60]: from keras.regularizers import l1, l2

In [61]: def create_model(hl=1, hu=128, dropout=False, rate=0.3,
                          regularize=False, reg=l1(0.0005),
                          optimizer=optimizer, input_dim=len(cols)):
             if not regularize:
                 reg = None
             model = Sequential()
             model.add(Dense(hu, input_dim=input_dim,
                             activity_regularizer=reg,  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                             activation='relu'))
             if dropout:
                 model.add(Dropout(rate, seed=100))
             for _ in range(hl):
                 model.add(Dense(hu, activation='relu',
                                 activity_regularizer=reg))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 if dropout:
                     model.add(Dropout(rate, seed=100))
             model.add(Dense(1, activation='sigmoid'))
             model.compile(loss='binary_crossentropy', optimizer=optimizer,
                          metrics=['accuracy'])
             return model

In [62]: set_seeds()
         model = create_model(hl=1, hu=128, regularize=True)

In [63]: %%time
         model.fit(train_[cols], train['d'],
                   epochs=50, verbose=False,
                   validation_split=0.2, shuffle=False,
                   class_weight=cw(train))
         CPU times: user 5.49 s, sys: 1.05 s, total: 6.54 s
         Wall time: 3.15 s

Out[63]: <keras.callbacks.callbacks.History at 0x7fbfa6b8e110>

In [64]: model.evaluate(train_[cols], train['d'])
         1746/1746 [==============================] - 0s 15us/step

Out[64]: [0.5307255412568205, 0.7691867351531982]

In [65]: model.evaluate(test_[cols], test['d'])
         437/437 [==============================] - 0s 22us/step

Out[65]: [0.8428352184644826, 0.6590389013290405]

1

在每一层中添加正则化。

图 7-5 显示了在正则化下的训练和验证精度。两个性能指标比以前更接近:

In [66]: res = pd.DataFrame(model.history.history)

In [67]: res[['accuracy', 'val_accuracy']].plot(figsize=(10, 6), style='--');

aiif 0705

图 7-5. 训练和验证精度值(使用正则化)

当然,dropout 和正则化可以一起使用。其想法是,这两个措施结合起来可以更好地避免过拟合,并将样本内和样本外的精度值更接近。事实上,在这种情况下,两种措施之间的差异最小:

In [68]: set_seeds()
         model = create_model(hl=2, hu=128,
                              dropout=True, rate=0.3,  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                              regularize=True, reg=l2(0.001),  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                             )

In [69]: %%time
         model.fit(train_[cols], train['d'],
                   epochs=50, verbose=False,
                   validation_split=0.2, shuffle=False,
                   class_weight=cw(train))
         CPU times: user 7.06 s, sys: 958 ms, total: 8.01 s
         Wall time: 4.28 s

Out[69]: <keras.callbacks.callbacks.History at 0x7fbfa701cb50>

In [70]: model.evaluate(train_[cols], train['d'])
         1746/1746 [==============================] - 0s 18us/step

Out[70]: [0.5007762827004764, 0.7691867351531982]

In [71]: model.evaluate(test_[cols], test['d'])
         437/437 [==============================] - 0s 23us/step

Out[71]: [0.6191965124699835, 0.6864988803863525]

1

在模型创建中添加了 dropout。

2

在模型创建中添加了正则化。

图 7-6 显示了在将 dropout 与正则化结合使用时的训练和验证精度。训练时期内训练数据和验证数据的精度之间的差异仅平均为四个百分点:

In [72]: res = pd.DataFrame(model.history.history)

In [73]: res[['accuracy', 'val_accuracy']].plot(figsize=(10, 6), style='--');

aiif 0706

图 7-6. 训练和验证准确率值(带有 dropout 和正则化)

惩罚大权重

正则化通过惩罚神经网络中的大权重来避免过拟合。单个权重不能变得足够大以主导神经网络。惩罚使权重保持在可比较的水平上。

Bagging

Bagging 方法以避免过度拟合已经在第六章中使用,尽管仅用于scikit-learnMLPRegressor模型。还有一个包装器用于将Keras DNN 分类模型以scikit-learn的方式公开,即KerasClassifier类。以下 Python 代码结合了基于包装器的Keras DNN 建模与scikit-learn中的BaggingClassifier。样本内和样本外的性能指标相对较高,约为 70%。然而,结果受类别不平衡的影响,如前所述,并在这里反映在0预测的高频率中:

In [75]: from sklearn.ensemble import BaggingClassifier
         from keras.wrappers.scikit_learn import KerasClassifier

In [76]: max_features = 0.75

In [77]: set_seeds()
         base_estimator = KerasClassifier(build_fn=create_model,
                                 verbose=False, epochs=20, hl=1, hu=128,
                                 dropout=True, regularize=False,
                                 input_dim=int(len(cols) * max_features))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [78]: model_bag = BaggingClassifier(base_estimator=base_estimator,
                                   n_estimators=15,
                                   max_samples=0.75,
                                   max_features=max_features,
                                   bootstrap=True,
                                   bootstrap_features=True,
                                   n_jobs=1,
                                   random_state=100,
                                  )  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [79]: %time model_bag.fit(train_[cols], train['d'])
         CPU times: user 40 s, sys: 5.23 s, total: 45.3 s
         Wall time: 26.3 s

Out[79]: BaggingClassifier(base_estimator=<keras.wrappers.scikit_learn.KerasClassifier
          object at 0x7fbfa7cc7b90>,
         bootstrap_features=True, max_features=0.75, max_samples=0.75,
                           n_estimators=15, n_jobs=1, random_state=100)

In [80]: model_bag.score(train_[cols], train['d'])
Out[80]: 0.720504009163803

In [81]: model_bag.score(test_[cols], test['d'])
Out[81]: 0.6704805491990846

In [82]: test['p'] = model_bag.predict(test_[cols])

In [83]: test['p'].value_counts()
Out[83]: 0    408
         1     29
         Name: p, dtype: int64

1

基本估计器,在这里是KerasSequential模型,被实例化。

2

BaggingClassifier模型为一些相等的基本估计器实例化。

分布式学习

Bagging 在某种意义上将学习分布在多个神经网络(或其他模型)之间,例如每个神经网络仅看到训练数据集的某些部分和特征的选择。这避免了单个神经网络过度拟合完整的训练数据集。预测基于所有选择性训练的神经网络的集合。

优化器

Keras包提供了一些可以与Sequential模型结合使用的优化器(参见https://oreil.ly/atpu8)。不同的优化器可能在训练所需时间和预测准确性方面表现不同。以下 Python 代码使用不同的优化器并评估其性能。在所有情况下,都使用了Keras的默认参数设置。样本外的性能变化不大。然而,由于不同的优化器,样本内的性能变化很大:

In [84]: import time

In [85]: optimizers = ['sgd', 'rmsprop', 'adagrad', 'adadelta',
                       'adam', 'adamax', 'nadam']

In [86]: %%time
         for optimizer in optimizers:
             set_seeds()
             model = create_model(hl=1, hu=128,
                              dropout=True, rate=0.3,
                              regularize=False, reg=l2(0.001),
                              optimizer=optimizer
                             )  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             t0 = time.time()
             model.fit(train_[cols], train['d'],
                       epochs=50, verbose=False,
                       validation_split=0.2, shuffle=False,
                       class_weight=cw(train))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             t1 = time.time()
             t = t1 - t0
             acc_tr = model.evaluate(train_[cols], train['d'], verbose=False)[1]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             acc_te = model.evaluate(test_[cols], test['d'], verbose=False)[1]  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
             out = f'{optimizer:10s} | time[s]: {t:.4f} | in-sample={acc_tr:.4f}'
             out += f' | out-of-sample={acc_te:.4f}'
             print(out)
         sgd        | time[s]: 2.8092 | in-sample=0.6363 | out-of-sample=0.6568
         rmsprop    | time[s]: 2.9480 | in-sample=0.7600 | out-of-sample=0.6613
         adagrad    | time[s]: 2.8472 | in-sample=0.6747 | out-of-sample=0.6499
         adadelta   | time[s]: 3.2068 | in-sample=0.7279 | out-of-sample=0.6522
         adam       | time[s]: 3.2364 | in-sample=0.7365 | out-of-sample=0.6545
         adamax     | time[s]: 3.2465 | in-sample=0.6982 | out-of-sample=0.6476
         nadam      | time[s]: 4.1275 | in-sample=0.7944 | out-of-sample=0.6590
         CPU times: user 35.9 s, sys: 4.55 s, total: 40.4 s
         Wall time: 23.1 s

1

为给定的优化器实例化 DNN 模型

2

使用给定的优化器拟合模型

3

评估样本内表现

4

评估样本外表现

结论

本章深入探讨了深度神经网络(DNNs)的世界,并以Keras作为主要工具包。Keras在组合 DNNs 方面提供了高度灵活性。本章的结果表明,无论是样本内还是样本外的预测准确率都稳定在 60%以上。然而,预测准确率只是一个方面。必须有合适的交易策略可以经济地从预测或“信号”中获利。在算法交易背景下,这个至关重要的话题在第四部分中有详细讨论。接下来的两章首先说明了不同神经网络(递归神经网络和卷积神经网络)以及学习技术(强化学习)的使用。

参考文献

Keras是一个强大而全面的深度学习工具包,其主要后端是 TensorFlow。该项目也在快速发展中。请通过主项目页面保持最新。关于Keras的主要资源书籍如下:

  • Chollet, Francois. 2017. Python 深度学习. Shelter Island: Manning。

  • Goodfellow, Ian, Yoshua Bengio, and Aaron Courville. 2016. 深度学习. Cambridge: MIT Press. http://deeplearningbook.org

¹ 更多关于Keras包的详细信息和背景,请参见 Chollet (2017)。关于神经网络及其相关方法的全面处理,请参见 Goodfellow 等人(2016)。

² 查看这篇博客文章,讨论了使用Keras解决类别不平衡问题的解决方案。

第八章:循环神经网络

历史永远不会重复,但却会押韵。

马克·吐温(可能)

我的生活似乎是一连串的事件和意外。然而,当我回顾过去时,我看到了一种模式。

伯诺瓦·曼德布罗特

本章讨论的是循环神经网络(RNNs)。这种类型的网络专门设计用于学习顺序数据,例如文本或时间序列数据。本章的讨论采用与之前相同的实用方法,主要依赖于经过详细解释的 Python 示例,利用了 Keras。¹

“第一个示例”和“第二个示例”基于两个简单示例介绍了 RNNs,并使用样本数值数据进行说明。展示了将 RNNs 应用于预测顺序数据的方法。“金融价格序列”然后使用金融价格序列数据,并通过估计方法直接应用 RNN 方法来预测这样的序列。“金融收益序列”然后使用回报数据来预测金融工具价格的未来方向,也是通过估计方法。“金融特征”将金融特征添加到混合中—除了价格和回报数据外—以预测市场走向。本节介绍了三种不同的方法:通过浅层 RNN 进行估计和分类的预测,以及通过深层 RNN 进行分类的预测。

本章显示,将 RNNs 应用于金融时间序列数据可以在方向性市场预测的情况下在样本外获得高达 60% 以上的预测准确率。然而,所得结果不能完全跟上第七章中看到的结果。这可能令人惊讶,因为 RNNs 本应在金融时间序列数据中表现良好,而这正是本书的主要关注点。

第一个示例

为了说明循环神经网络(RNN)的训练和使用,考虑一个基于整数序列的简单示例。首先,进行一些导入和配置:

In [1]: import os
        import random
        import numpy as np
        import pandas as pd
        import tensorflow as tf
        from pprint import pprint
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        pd.set_option('precision', 4)
        np.set_printoptions(suppress=True, precision=4)
        os.environ['PYTHONHASHSEED'] = '0'

In [2]: def set_seeds(seed=100):  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
            random.seed(seed)
            np.random.seed(seed)
            tf.random.set_seed(seed)
        set_seeds()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

1

设置所有种子值的函数

第二个是将简单数据集转换为适当形状的简单数据集:

In [3]: a = np.arange(100)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        a
Out[3]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
               17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
               34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
               51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
               68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
               85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99])

In [4]: a = a.reshape((len(a), -1))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [5]: a.shape  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[5]: (100, 1)

In [6]: a[:5]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[6]: array([[0],
               [1],
               [2],
               [3],
               [4]])

1

样本数据

2

转换为二维

使用 TimeseriesGenerator,原始数据可以被转换为适合训练 RNN 的对象。其思想是使用一定数量的原始数据滞后值来训练模型,以预测序列中的下一个值。例如,0, 1, 2 是用于预测值 3(标签)的三个滞后值(特征)。同样,1, 2, 3 用于预测 4

In [7]: from keras.preprocessing.sequence import TimeseriesGenerator
        Using TensorFlow backend.

In [8]: lags = 3

In [9]: g = TimeseriesGenerator(a, a, length=lags, batch_size=5)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [10]: pprint(list(g)[0])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         (array([[[0],
                 [1],
                 [2]],

                [[1],
                 [2],
                 [3]],

                [[2],
                 [3],
                 [4]],

                [[3],
                 [4],
                 [5]],

                [[4],
                 [5],
                 [6]]]),
          array([[3],
                [4],
                [5],
                [6],
                [7]]))

1

TimeseriesGenerator 创建滞后连续数据的批次。

创建 RNN 模型与 DNN 类似。以下 Python 代码使用单隐藏层类型为SimpleRNN(Chollet 2017,第六章;另见Keras recurrent layers)。即使隐藏单元相对较少,可训练参数的数量也非常大。.fit_generator()方法以生成器对象为输入,例如使用TimeseriesGenerator创建的对象:

In [11]: from keras.models import Sequential
         from keras.layers import SimpleRNN, LSTM, Dense

In [12]: model = Sequential()
         model.add(SimpleRNN(100, activation='relu',
                             input_shape=(lags, 1)))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         model.add(Dense(1, activation='linear'))
         model.compile(optimizer='adagrad', loss='mse',
                       metrics=['mae'])

In [13]: model.summary()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         Model: "sequential_1"
         _________________________________________________________________
         Layer (type)                 Output Shape              Param #
         =================================================================
         simple_rnn_1 (SimpleRNN)     (None, 100)               10200
         _________________________________________________________________
         dense_1 (Dense)              (None, 1)                 101
         =================================================================
         Total params: 10,301
         Trainable params: 10,301
         Non-trainable params: 0
         _________________________________________________________________

In [14]: %%time
         model.fit_generator(g, epochs=1000, steps_per_epoch=5,
                             verbose=False)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         CPU times: user 17.4 s, sys: 3.9 s, total: 21.3 s
         Wall time: 30.8 s

Out[14]: <keras.callbacks.callbacks.History at 0x7f7f079058d0>

1

单隐藏层的类型为SimpleRNN

2

浅层 RNN 的总结。

3

基于生成器对象的 RNN 拟合。

当训练 RNN 时,性能指标可能表现出相对不稳定的行为(见图 8-1):

In [15]: res = pd.DataFrame(model.history.history)

In [16]: res.tail(3)
Out[16]:        loss     mae
         997  0.0001  0.0109
         998  0.0007  0.0211
         999  0.0001  0.0101

In [17]: res.iloc[10:].plot(figsize=(10, 6), style=['--', '--']);

aiif 0801

图 8-1. RNN 训练期间的性能指标

有了训练好的 RNN,以下 Python 代码生成样本内和样本外预测:

In [18]: x = np.array([21, 22, 23]).reshape((1, lags, 1))
         y = model.predict(x, verbose=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         int(round(y[0, 0]))
Out[18]: 24

In [19]: x = np.array([87, 88, 89]).reshape((1, lags, 1))
         y = model.predict(x, verbose=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         int(round(y[0, 0]))
Out[19]: 90

In [20]: x = np.array([187, 188, 189]).reshape((1, lags, 1))
         y = model.predict(x, verbose=False)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         int(round(y[0, 0]))
Out[20]: 190

In [21]: x = np.array([1187, 1188, 1189]).reshape((1, lags, 1))
         y = model.predict(x, verbose=False)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         int(round(y[0, 0]))
Out[21]: 1194

1

样本内预测

2

样本外预测

3

远期预测

即使对于远期预测,总体上在这种简单情况下结果是不错的。然而,在手头问题中,例如通过 OLS 回归应用可以完美解决。因此,考虑到 RNN 的性能,对于这样的问题进行训练所需的工作量是相当大的。

第二个示例

第一个示例说明了对一个简单问题的 RNN 进行训练,这个问题不仅可以通过 OLS 回归轻松解决,而且人类检查数据时也能解决。第二个示例稍微有些挑战性。输入数据通过二次项和三角函数项的转换,以及添加白噪声来进行变换。图 8-2 展示了在区间[ - 2 π , 2 π ]的结果序列:

In [22]: def transform(x):
             y = 0.05 * x ** 2 + 0.2 * x + np.sin(x) + 5  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             y += np.random.standard_normal(len(x)) * 0.2  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             return y

In [23]: x = np.linspace(-2 * np.pi, 2 * np.pi, 500)
         a = transform(x)

In [24]: plt.figure(figsize=(10, 6))
         plt.plot(x, a);

1

确定性转换

2

随机转换

aiif 0802

图 8-2. 样本序列数据

如前所述,原始数据被重新塑造,应用TimeseriesGenerator,并训练带有单隐藏层的 RNN:

In [25]: a = a.reshape((len(a), -1))

In [26]: a[:5]
Out[26]: array([[5.6736],
                [5.68  ],
                [5.3127],
                [5.645 ],
                [5.7118]])

In [27]: lags = 5

In [28]: g = TimeseriesGenerator(a, a, length=lags, batch_size=5)

In [29]: model = Sequential()
         model.add(SimpleRNN(500, activation='relu', input_shape=(lags, 1)))
         model.add(Dense(1, activation='linear'))
         model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])

In [30]: model.summary()
         Model: "sequential_2"
         _________________________________________________________________
         Layer (type)                 Output Shape              Param #
         =================================================================
         simple_rnn_2 (SimpleRNN)     (None, 500)               251000
         _________________________________________________________________
         dense_2 (Dense)              (None, 1)                 501
         =================================================================
         Total params: 251,501
         Trainable params: 251,501
         Non-trainable params: 0
         _________________________________________________________________

In [31]: %%time
         model.fit_generator(g, epochs=500,
                             steps_per_epoch=10,
                             verbose=False)
         CPU times: user 1min 6s, sys: 14.6 s, total: 1min 20s
         Wall time: 23.1 s

Out[31]: <keras.callbacks.callbacks.History at 0x7f7f09c11810>

下面的 Python 代码预测区间 - 6 π , 6 π 的序列值。这个区间是训练区间大小的三倍,并包含训练区间左右两侧的样本外预测。[Figure 8-3 显示模型表现相当良好,即使在样本外:

In [32]: x = np.linspace(-6 * np.pi, 6 * np.pi, 1000)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         d = transform(x)

In [33]: g_ = TimeseriesGenerator(d, d, length=lags, batch_size=len(d))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [34]: f = list(g_)[0][0].reshape((len(d) - lags, lags, 1))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [35]: y = model.predict(f, verbose=False)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [36]: plt.figure(figsize=(10, 6))
         plt.plot(x[lags:], d[lags:], label='data', alpha=0.75)
         plt.plot(x[lags:], y, 'r.', label='pred', ms=3)
         plt.axvline(-2 * np.pi, c='g', ls='--')
         plt.axvline(2 * np.pi, c='g', ls='--')
         plt.text(-15, 22, 'out-of-sample')
         plt.text(-2, 22, 'in-sample')
         plt.text(10, 22, 'out-of-sample')
         plt.legend();

1

扩大样本数据集

2

样本内 样本外预测

示例的简易性

前两个示例故意选择简单。例如,第二个示例可以通过允许在三角函数基础函数中使用 OLS 回归等更高效地解决。然而,对于金融时间序列数据等非平凡序列数据的 RNN 训练基本相同。在这种情况下,例如,OLS 回归通常无法与 RNN 的能力相匹敌。

aiif 0803

图 8-3. RNN 的样本内和样本外预测

金融价格序列

作为对金融时间序列数据的 RNN 的第一个应用,考虑日内 EUR/USD 报价。使用前两节介绍的方法,对金融时间序列的 RNN 训练是直接的。首先,导入和重新采样数据。数据也被归一化,并转换为适当的 ndarray 对象:

In [37]: url = 'http://hilpisch.com/aiif_eikon_id_eur_usd.csv'

In [38]: symbol = 'EUR_USD'

In [39]: raw = pd.read_csv(url, index_col=0, parse_dates=True)

In [40]: def generate_data():
             data = pd.DataFrame(raw['CLOSE'])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             data.columns = [symbol]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             data = data.resample('30min', label='right').last().ffill()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             return data

In [41]: data = generate_data()

In [42]: data = (data - data.mean()) / data.std()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [43]: p = data[symbol].values  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [44]: p = p.reshape((len(p), -1))  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

1

选择单列

2

重命名列

3

对数据进行重新采样

4

进行高斯归一化处理

5

将数据集重塑为二维

其次,基于生成器对象训练 RNN。函数 create_rnn_model() 允许创建带有 SimpleRNNLSTM(长短期记忆)层的 RNN(Chollet 2017, ch. 6; 另请参阅 Keras recurrent layers)。

In [45]: lags = 5

In [46]: g = TimeseriesGenerator(p, p, length=lags, batch_size=5)

In [47]: def create_rnn_model(hu=100, lags=lags, layer='SimpleRNN',
                                    features=1, algorithm='estimation'):
             model = Sequential()
             if layer is 'SimpleRNN':
                 model.add(SimpleRNN(hu, activation='relu',
                                     input_shape=(lags, features)))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             else:
                 model.add(LSTM(hu, activation='relu',
                                input_shape=(lags, features)))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             if algorithm == 'estimation':
                 model.add(Dense(1, activation='linear'))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 model.compile(optimizer='adam', loss='mse', metrics=['mae'])
             else:
                 model.add(Dense(1, activation='sigmoid'))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 model.compile(optimizer='adam', loss='mse', metrics=['accuracy'])
             return model

In [48]: model = create_rnn_model()

In [49]: %%time
         model.fit_generator(g, epochs=500, steps_per_epoch=10,
                             verbose=False)
         CPU times: user 20.8 s, sys: 4.66 s, total: 25.5 s
         Wall time: 11.2 s

Out[49]: <keras.callbacks.callbacks.History at 0x7f7ef6716590>

1

添加 SimpleRNN 层或 LSTM

2

添加用于 估计分类 的输出层

第三,生成样本内预测。如 Figure 8-4 所示,RNN 能够捕捉归一化金融时间序列数据的结构。基于这种可视化,预测精度似乎相当不错:

In [50]: y = model.predict(g, verbose=False)

In [51]: data['pred'] = np.nan
         data['pred'].iloc[lags:] = y.flatten()

In [52]: data[[symbol, 'pred']].plot(
                     figsize=(10, 6), style=['b', 'r-.'],
                     alpha=0.75);

aiif 0804

图 8-4. RNN 对金融价格序列的样本内预测(整个数据集)

然而,可视化结果表明,进一步检查后结果并不成立。图 8-5 放大并仅显示原始数据集和预测数据集的 50 个数据点。可以明显看出,RNN 的预测值基本上只是前一个滞后,按一定时间间隔移动。从视觉上看,预测线就是金融时间序列本身,向右移动了一个时间间隔。

In [53]: data[[symbol, 'pred']].iloc[50:100].plot(
                     figsize=(10, 6), style=['b', 'r-.'],
                     alpha=0.75);

aiif 0805

图 8-5. RNN 对金融价格序列的样本内预测(数据子集)。

RNN 与有效市场

基于 RNN 的金融价格序列预测结果与在第六章中用于说明有效市场假设(EMH)的 OLS 回归方法一致。在那里,说明了在最小二乘意义下,今天的价格是明天价格的最佳预测值。将 RNN 应用于价格数据并没有产生其他见解。

金融回报序列。

正如前面的分析所示,预测回报可能比预测价格更容易。因此,以下 Python 代码基于对数回报重复了前述分析:

In [54]: data = generate_data()

In [55]: data['r'] = np.log(data / data.shift(1))

In [56]: data.dropna(inplace=True)

In [57]: data = (data - data.mean()) / data.std()

In [58]: r = data['r'].values

In [59]: r = r.reshape((len(r), -1))

In [60]: g = TimeseriesGenerator(r, r, length=lags, batch_size=5)

In [61]: model = create_rnn_model()

In [62]: %%time
         model.fit_generator(g, epochs=500, steps_per_epoch=10,
                             verbose=False)
         CPU times: user 20.4 s, sys: 4.2 s, total: 24.6 s
         Wall time: 11.3 s

Out[62]: <keras.callbacks.callbacks.History at 0x7f7ef47a8dd0>

如图 8-6 所示,RNN 的预测在绝对意义上并不太好。然而,它们似乎在某种程度上正确地捕捉了市场方向(回报的符号)。

In [63]: y = model.predict(g, verbose=False)

In [64]: data['pred'] = np.nan
         data['pred'].iloc[lags:] = y.flatten()
         data.dropna(inplace=True)

In [65]: data[['r', 'pred']].iloc[50:100].plot(
                     figsize=(10, 6), style=['b', 'r-.'],
                     alpha=0.75);
         plt.axhline(0, c='grey', ls='--')

aiif 0806

图 8-6. RNN 对金融回报序列的样本内预测(数据子集)。

虽然图 8-6 仅提供了一个指示,但相对较高的准确率得分支持了 RNN 在回报序列上可能比在价格序列上表现更好的假设。

In [66]: from sklearn.metrics import accuracy_score

In [67]: accuracy_score(np.sign(data['r']), np.sign(data['pred']))
Out[67]: 0.6806532093445226

然而,为了得到一个真实的图像,需要进行训练-测试分割。样本外的准确率评分并不像样本内整体数据集那样高,但对于当前问题来说仍然很高。

In [68]: split = int(len(r) * 0.8)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [69]: train = r[:split]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [70]: test = r[split:]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [71]: g = TimeseriesGenerator(train, train, length=lags, batch_size=5)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [72]: set_seeds()
         model = create_rnn_model(hu=100)

In [73]: %%time
         model.fit_generator(g, epochs=100, steps_per_epoch=10, verbose=False)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         CPU times: user 5.67 s, sys: 1.09 s, total: 6.75 s
         Wall time: 2.95 s

Out[73]: <keras.callbacks.callbacks.History at 0x7f7ef5482dd0>

In [74]: g_ = TimeseriesGenerator(test, test, length=lags, batch_size=5)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [75]: y = model.predict(g_)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [76]: accuracy_score(np.sign(test[lags:]), np.sign(y))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[76]: 0.6708428246013668

1

将数据分割为训练和测试数据子集。

2

在训练数据上拟合模型。

3

在测试数据上测试模型。

金融特征。

RNN 的应用不仅限于原始价格或收益数据。还可以添加额外的特征来改善 RNN 的预测能力。以下 Python 代码向数据集添加了典型的金融特征:

In [77]: data = generate_data()

In [78]: data['r'] = np.log(data / data.shift(1))

In [79]: window = 20
         data['mom'] = data['r'].rolling(window).mean()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         data['vol'] = data['r'].rolling(window).std()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [80]: data.dropna(inplace=True)

1

添加了时间序列动量特征。

2

添加了滚动的波动率特征。

估计。

在估计情况下,样本外准确率可能会显著下降,这可能会有些意外。换句话说,在这种特定情况下,添加金融特征并没有观察到改进。

In [81]: split = int(len(data) * 0.8)

In [82]: train = data.iloc[:split].copy()

In [83]: mu, std = train.mean(), train.std()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [84]: train = (train - mu) / std  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [85]: test = data.iloc[split:].copy()

In [86]: test = (test - mu) / std  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [87]: g = TimeseriesGenerator(train.values, train['r'].values,
                                 length=lags, batch_size=5)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [88]: set_seeds()
         model = create_rnn_model(hu=100, features=len(data.columns),
                                  layer='SimpleRNN')

In [89]: %%time
         model.fit_generator(g, epochs=100, steps_per_epoch=10,
                             verbose=False)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         CPU times: user 5.24 s, sys: 1.08 s, total: 6.32 s
         Wall time: 2.73 s

Out[89]: <keras.callbacks.callbacks.History at 0x7f7ef313c950>

In [90]: g_ = TimeseriesGenerator(test.values, test['r'].values,
                                  length=lags, batch_size=5)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [91]: y = model.predict(g_).flatten()  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [92]: accuracy_score(np.sign(test['r'].iloc[lags:]), np.sign(y))  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[92]: 0.37299771167048057

1

计算训练数据的第一和第二时刻。

2

对训练数据应用高斯归一化

3

对测试数据应用高斯归一化,基于从训练数据中得出的统计数据

4

在训练数据上拟合模型

5

在测试数据上测试模型

分类

到目前为止的分析使用了Keras的 RNN 模型进行估计,以预测金融工具价格的未来走向。当前问题可能更适合直接设定为分类问题。以下 Python 代码处理二元标签数据,并直接预测价格走势的方向。这次还使用了 LSTM 层。即使隐藏单元较少且仅经过少数训练时期,样本外准确率也相当高。该方法再次通过适当调整类别权重考虑了类别不平衡。在这种情况下,预测准确率相当高,约为 65%:

In [93]: set_seeds()
         model = create_rnn_model(hu=50,
                     features=len(data.columns),
                     layer='LSTM',
                     algorithm='classification')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [94]: train_y = np.where(train['r'] > 0, 1, 0)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [95]: np.bincount(train_y)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[95]: array([2374, 1142])

In [96]: def cw(a):
             c0, c1 = np.bincount(a)
             w0 = (1 / c0) * (len(a)) / 2
             w1 = (1 / c1) * (len(a)) / 2
             return {0: w0, 1: w1}

In [97]: g = TimeseriesGenerator(train.values, train_y,
                                 length=lags, batch_size=5)

In [98]: %%time
         model.fit_generator(g, epochs=5, steps_per_epoch=10,
                             verbose=False, class_weight=cw(train_y))
         CPU times: user 1.25 s, sys: 159 ms, total: 1.41 s
         Wall time: 947 ms

Out[98]: <keras.callbacks.callbacks.History at 0x7f7ef43baf90>

In [99]: test_y = np.where(test['r'] > 0, 1, 0)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [100]: g_ = TimeseriesGenerator(test.values, test_y,
                                   length=lags, batch_size=5)

In [101]: y = np.where(model.predict(g_, batch_size=None) > 0.5, 1, 0).flatten()

In [102]: np.bincount(y)
Out[102]: array([492, 382])

In [103]: accuracy_score(test_y[lags:], y)
Out[103]: 0.6498855835240275

1

用于分类的 RNN 模型

2

二元训练标签

3

训练标签的类别频率

4

二元测试标签

深层 RNNs

最后,考虑深层 RNNs,它们是具有多个隐藏层的 RNNs。它们的创建与深度 DNNs 一样简单。唯一的要求是对于非最终隐藏层,参数return_sequences必须设置为True。以下 Python 函数用于创建深层 RNN,还允许添加Dropout层以潜在地避免过拟合。预测准确率与前一小节中看到的相当:

In [104]: from keras.layers import Dropout

In [105]: def create_deep_rnn_model(hl=2, hu=100, layer='SimpleRNN',
                                    optimizer='rmsprop', features=1,
                                    dropout=False, rate=0.3, seed=100):
              if hl <= 2: hl = 2  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
              if layer == 'SimpleRNN':
                  layer = SimpleRNN
              else:
                  layer = LSTM
              model = Sequential()
              model.add(layer(hu, input_shape=(lags, features),
                               return_sequences=True,
                              ))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
              if dropout:
                  model.add(Dropout(rate, seed=seed))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
              for _ in range(2, hl):
                  model.add(layer(hu, return_sequences=True))
                  if dropout:
                      model.add(Dropout(rate, seed=seed))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
              model.add(layer(hu))  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
              model.add(Dense(1, activation='sigmoid'))  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
              model.compile(optimizer=optimizer,
                            loss='binary_crossentropy',
                            metrics=['accuracy'])
              return model

In [106]: set_seeds()
          model = create_deep_rnn_model(
                      hl=2, hu=50, layer='SimpleRNN',
                      features=len(data.columns),
                      dropout=True, rate=0.3)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [107]: %%time
          model.fit_generator(g, epochs=200, steps_per_epoch=10,
                              verbose=False, class_weight=cw(train_y))
          CPU times: user 14.2 s, sys: 2.85 s, total: 17.1 s
          Wall time: 7.09 s

Out[107]: <keras.callbacks.callbacks.History at 0x7f7ef6428790>

In [108]: y = np.where(model.predict(g_, batch_size=None) > 0.5, 1, 0).flatten()

In [109]: np.bincount(y)
Out[109]: array([550, 324])

In [110]: accuracy_score(test_y[lags:], y)
Out[110]: 0.6430205949656751

1

至少确保两个隐藏层。

2

第一个隐藏层。

3

Dropout层。

4

最终隐藏层。

5

模型建立用于分类。

结论

本章介绍了使用Keras实现的 RNN,并展示了这些神经网络在金融时间序列数据中的应用。在 Python 层面上,使用 RNN 与使用 DNN 并没有太大区别。一个主要区别在于,训练和测试数据必须以顺序形式呈现给相应的方法。然而,TimeseriesGenerator函数的应用使得将顺序数据转换为Keras可处理的生成器对象变得简单。

本章的示例适用于金融价格序列和金融收益序列。此外,诸如时间序列动量等金融特征也可以轻松添加。所提供的模型创建功能允许使用SimpleRNNLSTM层以及不同的优化器,等等。它们还允许在浅层和深层神经网络的背景下建模估计和分类问题。

在预测市场方向时,样本外预测准确性对于分类示例相对较高,但对于估计示例来说并不那么高,甚至可能相当低。

参考文献

本章引用的书籍和论文:

  • Chollet,François。2017 年。Python 深度学习。Shelter Island:Manning。

  • Goodfellow,Ian,Yoshua Bengio 和 Aaron Courville。2016 年。深度学习。剑桥:MIT Press。http://deeplearningbook.org

¹ 对于 RNN 的技术细节,请参考 Goodfellow 等人(2016 年,第十章)。至于实际实现,请参考 Chollet(2017 年,第六章)。

第九章:强化学习

像人类一样,我们的代理通过自己学习,以实现成功的策略,从而获得最大的长期回报。这种通过奖励或惩罚进行试错学习的范式称为强化学习。¹

DeepMind(2016)

应用于第七章和第八章的学习算法属于监督学习范畴。这些方法要求提供一个数据集,其中包含特征和标签,使得算法能够学习特征与标签之间的关系,以便在估计或分类任务中成功。正如第一章的简单示例所说明的那样,强化学习(RL)的工作方式不同。首先,无需事先提供完整的特征和标签数据集。数据是通过学习代理与感兴趣的环境交互而生成的。本章详细介绍了 RL,并引入了基本概念,以及领域中使用的最流行算法之一:Q-learning(QL)。神经网络并未被 RL 算法所取代;它们在这一背景下通常也起着重要作用。

“基本概念” 解释了 RL 中的基本概念,如环境、状态和代理。“OpenAI Gym” 介绍了 OpenAI Gym 的 RL 环境套件,其中CartPole环境作为示例。在这个环境中,第二章简要介绍并讨论了代理必须学习如何通过移动车辆左右来平衡杆的问题。“蒙特卡洛代理”展示了如何通过降维和蒙特卡洛模拟来解决CartPole问题。通常,标准监督学习算法如深度神经网络(DNNs)一般不适用于解决CartPole这类问题,因为它们缺乏延迟奖励的概念。这个问题在“神经网络代理”中有所说明。“DQL 代理”讨论了一个显式考虑延迟奖励并能够解决CartPole问题的 QL 代理。同样的代理也应用于“简单金融 Gym”中的一个简单金融市场环境。尽管该代理在这种情况下表现不佳,但该示例显示 QL 代理也可以学习交易并成为所谓的交易机器人。为了改善 QL 代理的学习能力,“更好的金融 Gym”提出了一个改进的金融市场环境,除其他好处外,还允许使用多种类型的特征来描述环境的状态。基于这种改进的环境,“FQL 代理”介绍并应用了一个改进的金融 QL 代理,表现更佳,作为交易机器人。

基本概念

本节简要概述了强化学习中的基本概念。其中包括以下内容:

环境

环境定义了面临的问题。这可以是要玩的电脑游戏,也可以是要进行交易的金融市场。

状态

状态包含描述当前环境状态的所有相关参数。在电脑游戏中,这可能是整个屏幕及其所有像素。在金融市场中,这可能包括当前和历史价格水平或金融指标如移动平均线、宏观经济变量等。

代理

代理一词涵盖了 RL 算法中与环境交互并从中学习的所有元素。在游戏背景下,代理可能代表玩家进行游戏。在金融背景下,代理可以代表交易员在上涨或下跌的市场上下注。

动作

代理可以从允许的(有限的)一组操作中选择一个动作。在电脑游戏中,向左或向右移动可能是允许的动作,而在金融市场中,做多或做空可能是允许的操作。

步骤

针对代理的一个行动,环境的状态会被更新。这样的更新通常被称为一个步骤。步骤的概念足够广泛,可以涵盖两个步骤之间的异质和同质时间间隔。在电脑游戏中,通过相当短、同质的时间间隔模拟与游戏环境的实时交互(“游戏时钟”),而交易机器人与金融市场环境交互可能需要更长、异质的时间间隔来执行操作。

奖励

根据代理选择的行动,会给予一定的奖励(或惩罚)。在电脑游戏中,分数通常是一种典型的奖励。在金融背景下,利润(或亏损)是一种标准的奖励(或惩罚)。

目标

目标指定了代理试图最大化的内容。在电脑游戏中,一般是代理达到的分数。对于金融交易机器人,这可能是累积的交易利润。

策略

策略定义了在特定环境状态下代理应该采取的行动。在电脑游戏中,代表当前场景的所有像素构成的状态下,策略可能指定代理选择“向右移动”作为行动。一个交易机器人观察到连续三次价格上涨,可能根据其策略决定做空市场。

回合

一个回合是从环境的初始状态到达成功或观察到失败的一系列步骤。在游戏中,这是从游戏开始到获胜或失败。在金融世界中,例如,这是从年初到年底或破产。

Sutton 和 Barto(2018)详细介绍了 RL 领域。该书详细讨论了前述概念,并且通过大量具体示例进行了说明。以下章节再次选择了一种实用的、实施导向的 RL 方法。讨论的示例通过 Python 代码说明了所有前述概念。

OpenAI Gym

在大多数成功案例中,RL 在第二章中扮演了主导角色。这激发了对 RL 作为一种算法的广泛兴趣。OpenAI 是一个致力于推动 AI 研究的组织,特别是在 RL 领域。OpenAI 开发并开源了一套称为OpenAI Gym的环境套件,允许通过标准化 API 训练 RL 代理。

在众多环境中,有一个称为CartPole的环境(或游戏),模拟了一个经典的 RL 问题。一个杆竖立在一个小车上,目标是通过移动小车向左或向右来学习平衡杆子的策略。环境的状态由四个参数给出,描述以下物理测量:小车位置、小车速度、杆角度和杆末端的杆角速度。Figure 9-1 展示了环境的可视化。

aiif 0901

图 9-1. OpenAI Gym 的 CartPole 环境

考虑以下 Python 代码,实例化了一个 CartPole 环境对象,并检查观察空间。观察空间是环境状态的模型:

In [1]: import os
        import math
        import random
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        np.set_printoptions(precision=4, suppress=True)
        os.environ['PYTHONHASHSEED'] = '0'
In [2]: import gym

In [3]: env = gym.make('CartPole-v0')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [4]: env.seed(100)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        env.action_space.seed(100)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[4]: [100]

In [5]: env.observation_space  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[5]: Box(4,)

In [6]: env.observation_space.low.astype(np.float16)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[6]: array([-4.8  ,   -inf, -0.419,   -inf], dtype=float16)

In [7]: env.observation_space.high.astype(np.float16)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[7]: array([4.8  ,   inf, 0.419,   inf], dtype=float16)

In [8]: state = env.reset()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [9]: state  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[9]: array([-0.0163,  0.0238, -0.0392, -0.0148])

1

环境对象,带有固定的种子值

2

观察空间的最小值和最大值

3

重置环境

4

初始状态:小车位置、小车速度、杆角度、杆角速度

在以下环境中,允许的动作由动作空间描述。在这种情况下,有两种动作,分别用0(向左推车)和1(向右推车)表示:

In [10]: env.action_space  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[10]: Discrete(2)

In [11]: env.action_space.n  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[11]: 2

In [12]: env.action_space.sample()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[12]: 1

In [13]: env.action_space.sample()   ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[13]: 0

In [14]: a = env.action_space.sample()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         a  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[14]: 1

In [15]: state, reward, done, info = env.step(a)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         state, reward, done, info  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[15]: (array([-0.0158,  0.2195, -0.0395, -0.3196]), 1.0, False, {})

1

行动空间

2

从动作空间中随机采样随机动作

3

根据随机行动向前迈进

4

环境的新状态、奖励、成功/失败、额外信息

只要done=False,代理仍然在游戏中,并且可以选择另一个动作。当代理连续达到 200 步或总奖励达到 200 时(每步奖励为 1.0)即为成功。如果架在小车上的杆达到可能导致杆从小车上掉下的一定角度,则观察到失败。在这种情况下,将返回done=True

简单代理是指遵循完全随机策略的代理:无论观察到什么状态,代理都会选择一个随机动作。以下代码实现了这一点。在这种情况下,代理能够走多少步完全取决于它的运气。不会发生更新策略的学习形式:

In [16]: env.reset()
         for e in range(1, 200):
             a = env.action_space.sample()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             state, reward, done, info = env.step(a) ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             print(f'step={e:2d} | state={state} | action={a} | reward={reward}')
             if done and (e + 1) < 200:  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 print('*** FAILED ***')  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 break
         step= 1 | state=[-0.0423  0.1982  0.0256 -0.2476] | action=1 | reward=1.0
         step= 2 | state=[-0.0383  0.0028  0.0206  0.0531] | action=0 | reward=1.0
         step= 3 | state=[-0.0383  0.1976  0.0217 -0.2331] | action=1 | reward=1.0
         step= 4 | state=[-0.0343  0.0022  0.017   0.0664] | action=0 | reward=1.0
         step= 5 | state=[-0.0343  0.197   0.0184 -0.2209] | action=1 | reward=1.0
         step= 6 | state=[-0.0304  0.0016  0.0139  0.0775] | action=0 | reward=1.0
         step= 7 | state=[-0.0303  0.1966  0.0155 -0.2107] | action=1 | reward=1.0
         step= 8 | state=[-0.0264  0.0012  0.0113  0.0868] | action=0 | reward=1.0
         step= 9 | state=[-0.0264  0.1962  0.013  -0.2023] | action=1 | reward=1.0
         step=10 | state=[-0.0224  0.3911  0.009  -0.4908] | action=1 | reward=1.0
         step=11 | state=[-0.0146  0.5861 -0.0009 -0.7807] | action=1 | reward=1.0
         step=12 | state=[-0.0029  0.7812 -0.0165 -1.0736] | action=1 | reward=1.0
         step=13 | state=[ 0.0127  0.9766 -0.0379 -1.3714] | action=1 | reward=1.0
         step=14 | state=[ 0.0323  1.1722 -0.0654 -1.6758] | action=1 | reward=1.0
         step=15 | state=[ 0.0557  0.9779 -0.0989 -1.4041] | action=0 | reward=1.0
         step=16 | state=[ 0.0753  0.7841 -0.127  -1.1439] | action=0 | reward=1.0
         step=17 | state=[ 0.0909  0.5908 -0.1498 -0.8936] | action=0 | reward=1.0
         step=18 | state=[ 0.1028  0.7876 -0.1677 -1.2294] | action=1 | reward=1.0
         step=19 | state=[ 0.1185  0.9845 -0.1923 -1.5696] | action=1 | reward=1.0
         step=20 | state=[ 0.1382  0.7921 -0.2237 -1.3425] | action=0 | reward=1.0
         *** FAILED ***

In [17]: done
Out[17]: True

1

随机动作策略

2

向前迈出一步

3

如果少于 200 步,则失败

通过交互获取数据

在监督学习中,训练、验证和测试数据集被假定在训练开始之前存在,而在 RL 中,代理通过与环境的交互自己生成其数据。在许多情况下,例如在游戏中,这是一个巨大的简化。考虑象棋游戏:与其将成千上万个历史人类下棋游戏加载到计算机中,不如使用 RL 代理自己生成数千甚至数百万个游戏,例如通过与另一个象棋引擎或其另一个版本对弈。

蒙特卡罗代理

CartPole问题不一定需要完整的强化学习方法或一些神经网络来解决。本节提供了一个基于蒙特卡罗模拟的简单解决方案,该解决方案基于降维。在这种情况下,定义了一个特定的策略,该策略利用线性组合将环境状态的四个参数折叠为一个实值参数。² 以下 Python 代码实现了这个想法:

In [18]: np.random.seed(100)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [19]: weights = np.random.random(4) * 2 - 1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [20]: weights  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[20]: array([ 0.0868, -0.4433, -0.151 ,  0.6896])

In [21]: state = env.reset()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [22]: state  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[22]: array([-0.0347, -0.0103,  0.047 , -0.0315])

In [23]: s = np.dot(state, weights)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         s  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[23]: -0.02725361929630797

1

固定种子值的随机权重

2

环境的初始状态

3

状态和权重的点积

策略然后基于单个状态参数s的符号进行定义:

In [24]: if s < 0:
             a = 0
         else:
             a = 1

In [25]: a
Out[25]: 0

然后可以使用此策略来玩一个CartPole游戏的回合。由于应用的权重具有随机性质,因此其结果通常不会比前一节中的随机行动策略更好:

In [26]: def run_episode(env, weights):
             state = env.reset()
             treward = 0
             for _ in range(200):
                 s = np.dot(state, weights)
                 a = 0 if s < 0 else 1
                 state, reward, done, info = env.step(a)
                 treward += reward
                 if done:
                     break
             return treward

In [27]: run_episode(env, weights)
Out[27]: 41.0

因此,蒙特卡罗模拟被用来测试大量不同的权重。以下代码模拟了大量的权重,检查它们是否成功或失败,然后选择产生成功的权重:

In [28]: def set_seeds(seed=100):
             random.seed(seed)
             np.random.seed(seed)
             env.seed(seed)

In [29]: set_seeds()
         num_episodes = 1000

In [30]: besttreward = 0
         for e in range(1, num_episodes + 1):
             weights = np.random.rand(4) * 2 - 1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             treward = run_episode(env, weights)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             if treward > besttreward:  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 besttreward = treward  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 bestweights = weights  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 if treward == 200:
                     print(f'SUCCESS | episode={e}')
                     break
                 print(f'UPDATE  | episode={e}')
         UPDATE  | episode=1
         UPDATE  | episode=2
         SUCCESS | episode=13

In [31]: weights
Out[31]: array([-0.4282,  0.7048,  0.95  ,  0.7697])

1

随机权重。

2

这些权重的总奖励。

3

观察到改进了吗?

4

替换最佳总奖励。

5

替换最佳权重。

如果一个代理在连续的 100 个回合中的平均总奖励达到 195 或更高,则认为解决了CartPole问题。如下面的代码所示,蒙特卡洛代理确实达到了这个目标:

In [32]: res = []
         for _ in range(100):
             treward = run_episode(env, weights)
             res.append(treward)
         res[:10]
Out[32]: [200.0, 200.0, 200.0, 200.0, 200.0, 200.0, 200.0, 200.0, 200.0, 200.0]

In [33]: sum(res) / len(res)
Out[33]: 200.0

当然,这是一个强大的基准,其他更复杂的方法也在与之竞争。

神经网络代理

CartPole游戏也可以视为分类设置。环境的状态由四个特征值组成。给定特征值,正确的动作是标签。通过与环境的交互,神经网络代理可以收集由特征值和标签组成的数据集。在这个逐渐增长的数据集的基础上,可以训练神经网络来学习给定环境状态时的正确动作。在这种情况下,神经网络代表了策略。代理根据新的经验更新策略。

首先,导入一些库:

In [34]: import tensorflow as tf
         from keras.layers import Dense, Dropout
         from keras.models import Sequential
         from keras.optimizers import Adam, RMSprop
         from sklearn.metrics import accuracy_score
         Using TensorFlow backend.

In [35]: def set_seeds(seed=100):
             random.seed(seed)
             np.random.seed(seed)
             tf.random.set_seed(seed)
             env.seed(seed)
             env.action_space.seed(100)

其次是NNAgent类,它整合了代理的主要元素:策略的神经网络模型,根据策略选择动作,更新策略(训练神经网络),以及在多个回合中的学习过程。代理同时使用探索利用来选择动作。探索是指独立于当前策略的随机动作。利用是指根据当前策略得出的动作。这样做的想法是一定程度的探索可以确保更丰富的经验,从而提高代理的学习效果:

In [36]: class NNAgent:
             def __init__(self):
                 self.max = 0  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 self.scores = list()
                 self.memory = list()
                 self.model = self._build_model()

             def _build_model(self):  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 model = Sequential()
                 model.add(Dense(24, input_dim=4,
                                 activation='relu'))
                 model.add(Dense(1, activation='sigmoid'))
                 model.compile(loss='binary_crossentropy',
                               optimizer=RMSprop(lr=0.001))
                 return model

             def act(self, state):  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 if random.random() <= 0.5:
                     return env.action_space.sample()
                 action = np.where(self.model.predict(
                     state, batch_size=None)[0, 0] > 0.5, 1, 0)
                 return action

             def train_model(self, state, action):  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 self.model.fit(state, np.array([action,]),
                                epochs=1, verbose=False)

             def learn(self, episodes):  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 for e in range(1, episodes + 1):
                     state = env.reset()
                     for _ in range(201):
                         state = np.reshape(state, [1, 4])
                         action = self.act(state)
                         next_state, reward, done, info = env.step(action)
                         if done:
                             score = _ + 1
                             self.scores.append(score)
                             self.max = max(score, self.max)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                             print('episode: {:4d}/{} | score: {:3d} | max: {:3d}'
                                   .format(e, episodes, score, self.max), end='\r')
                             break
                         self.memory.append((state, action))
                         self.train_model(state, action)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                         state = next_state

1

最大的总奖励

2

用于策略的 DNN 分类模型

3

选择动作的方法(探索和利用)

4

更新策略的方法(训练神经网络)

5

从与环境交互中学习的方法

对于所示的配置,神经网络代理无法解决问题。最大的总奖励 200 甚至没有达到一次:

In [37]: set_seeds(100)
         agent = NNAgent()

In [38]: episodes = 500

In [39]: agent.learn(episodes)
         episode:  500/500 | score:  11 | max:  44
In [40]: sum(agent.scores) / len(agent.scores)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[40]: 13.682

1

所有回合的平均总奖励

这种方法似乎存在一些问题。其中一个主要的缺失元素是超越当前状态和待选择动作的概念。当前实施的方法并没有考虑到只有当代理在 200 个连续步骤中生存下来时才算成功。简单来说,代理避免采取错误的动作,但并没有学会赢得游戏。

分析收集到的状态(特征)和动作(标签)的历史表明,神经网络达到了大约 75%的准确率。

然而,这并不意味着像以前那样转化为一个获胜的政策:

In [41]: f = np.array([m[0][0] for m in agent.memory])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         f  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[41]: array([[-0.0163,  0.0238, -0.0392, -0.0148],
                [-0.0158,  0.2195, -0.0395, -0.3196],
                [-0.0114,  0.0249, -0.0459, -0.0396],
                ...,
                [ 0.0603,  0.9682, -0.0852, -1.4595],
                [ 0.0797,  1.1642, -0.1144, -1.7776],
                [ 0.103 ,  1.3604, -0.15  , -2.1035]])

In [42]: l = np.array([m[1] for m in agent.memory])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         l  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[42]: array([1, 0, 1, ..., 1, 1, 1])

In [43]: accuracy_score(np.where(agent.model.predict(f) > 0.5, 1, 0), l)
Out[43]: 0.7525626872733008

1

所有剧集的特征(状态)

2

所有剧集的标签(动作)

DQL 代理

Q 学习(QL)是一种算法,除了行动的即时奖励外,还考虑了延迟奖励。该算法由 Watkins(1989 年)和 Watkins 和 Dayan(1992 年)提出,并在 Sutton 和 Barto(2018 年,第六章)中详细解释。QL 解决了与神经网络代理遇到的超出即时下一个奖励的问题。

该算法大致如下工作。有一个动作值策略Q,为每个状态和动作组合分配一个值。值越高,从代理的角度来看,动作越好。如果代理使用策略Q来选择动作,则选择价值最高的动作。

如何推导出一个动作的价值?一个动作的价值由其直接奖励和下一个状态中最优动作的折扣值组成。以下是形式表达:

Q ( S t , A t ) = R t+1 + γ max a Q ( S t+1 , a )

这里,S t 是步骤(时间)t 的状态,A t 是在状态S t 下采取的动作,R t+1 是动作A t 的直接奖励,0 < γ < 1 是折扣因子,max a Q ( S t+1 , a ) 是给定当前策略Q 的最佳行动后延迟奖励的最大值。

在简单环境中,只有有限数量的可能状态,Q 例如可以被表示为表格,列出每个状态-动作组合对应的值。然而,在更有趣或复杂的环境中,比如CartPole环境,状态的数量对于Q来说过于庞大,无法全面地写出来。因此,Q通常被理解为一个函数

这就是神经网络发挥作用的地方。在现实设置和环境中,对于函数Q可能不存在闭合形式的解,或者基于动态规划可能太难推导。因此,QL 算法通常只针对近似。神经网络以其通用逼近能力,是实现Q的自然选择。

QL 的另一个关键元素是回放。QL 代理会定期重放若干经验(状态-动作组合)以更新策略函数Q。这可以显著改善学习效果。此外,在以下提供的 QL 代理——DQLAgent中,代理在学习过程中也会在探索和利用之间交替。这种交替是系统化的,代理从仅探索开始——开始时它可能还没有学到任何东西——然后缓慢但稳步地减少探索率ϵ直到达到最低水平:³

In [44]: from collections import deque
         from keras.optimizers import Adam, RMSprop

In [45]: class DQLAgent:
             def __init__(self, gamma=0.95, hu=24, opt=Adam,
                    lr=0.001, finish=False):
                 self.finish = finish
                 self.epsilon = 1.0  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 self.epsilon_min = 0.01  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 self.epsilon_decay = 0.995  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 self.gamma = gamma  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 self.batch_size = 32  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 self.max_treward = 0
                 self.averages = list()
                 self.memory = deque(maxlen=2000)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 self.osn = env.observation_space.shape[0]
                 self.model = self._build_model(hu, opt, lr)

             def _build_model(self, hu, opt, lr):
                 model = Sequential()
                 model.add(Dense(hu, input_dim=self.osn,
                                 activation='relu'))
                 model.add(Dense(hu, activation='relu'))
                 model.add(Dense(env.action_space.n, activation='linear'))
                 model.compile(loss='mse', optimizer=opt(lr=lr))
                 return model

             def act(self, state):
                 if random.random() <= self.epsilon:
                     return env.action_space.sample()
                 action = self.model.predict(state)[0]
                 return np.argmax(action)

             def replay(self):
                 batch = random.sample(self.memory, self.batch_size)  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
                 for state, action, reward, next_state, done in batch:
                     if not done:
                         reward += self.gamma * np.amax(
                             self.model.predict(next_state)[0])  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
                     target = self.model.predict(state)
                     target[0, action] = reward
                     self.model.fit(state, target, epochs=1,
                                    verbose=False)  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
                 if self.epsilon > self.epsilon_min:
                     self.epsilon *= self.epsilon_decay  ![10](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/10.png)

             def learn(self, episodes):
                 trewards = []
                 for e in range(1, episodes + 1):
                     state = env.reset()
                     state = np.reshape(state, [1, self.osn])
                     for _ in range(5000):
                         action = self.act(state)
                         next_state, reward, done, info = env.step(action)
                         next_state = np.reshape(next_state,
                                                 [1, self.osn])
                         self.memory.append([state, action, reward,
                                              next_state, done])  ![11](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/11.png)
                         state = next_state
                         if done:
                             treward = _ + 1
                             trewards.append(treward)
                             av = sum(trewards[-25:]) / 25
                             self.averages.append(av)
                             self.max_treward = max(self.max_treward, treward)
                             templ = 'episode: {:4d}/{} | treward: {:4d} | '
                             templ += 'av: {:6.1f} | max: {:4d}'
                             print(templ.format(e, episodes, treward, av,
                                                self.max_treward), end='\r')
                             break
                     if av > 195 and self.finish:
                         break
                     if len(self.memory) > self.batch_size:
                         self.replay()  ![12](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/12.png)
             def test(self, episodes):
                 trewards = []
                 for e in range(1, episodes + 1):
                     state = env.reset()
                     for _ in range(5001):
                         state = np.reshape(state, [1, self.osn])
                         action = np.argmax(self.model.predict(state)[0])
                         next_state, reward, done, info = env.step(action)
                         state = next_state
                         if done:
                             treward = _ + 1
                             trewards.append(treward)
                             print('episode: {:4d}/{} | treward: {:4d}'
                                   .format(e, episodes, treward), end='\r')
                             break
                 return trewards

1

初始探索率

2

最小探索率

3

探索率衰减率

4

延迟奖励的折现因子

5

回放的批次大小

6

deque集合用于有限历史记录

7

随机选择历史批次用于回放

8

状态-动作对的Q

9

更新神经网络以适应新的动作-值对

10

更新探索率

11

存储新数据

12

回放以基于过去经验更新策略

QL 代理表现如何? 如下所示的代码显示,它对CartPole达到了总奖励为 200 的获胜状态。图 9-2 显示了分数的移动平均值以及随时间的增长情况,尽管不是单调递增。相反,作为图 9-2 所示,代理的表现有时可能显著下降。在其他方面,始终进行的探索导致可能不一定会带来总奖励方面的良好结果但可能导致更新策略网络的有益体验的随机操作:

In [46]: episodes = 1000

In [47]: set_seeds(100)
         agent = DQLAgent(finish=True)

In [48]: agent.learn(episodes)
         episode:  400/1000 | treward:  200 | av:  195.4 | max:  200
In [49]: plt.figure(figsize=(10, 6))
         x = range(len(agent.averages))
         y = np.polyval(np.polyfit(x, agent.averages, deg=3), x)
         plt.plot(agent.averages, label='moving average')
         plt.plot(x, y, 'r--', label='trend')
         plt.xlabel('episodes')
         plt.ylabel('total reward')
         plt.legend();

aiif 0902

图 9-2. DQLAgentCartPole的平均总奖励

QL 代理是否解决了CartPole问题? 在这种特定情况下,根据 OpenAI Gym 的成功定义,它确实解决了这个问题:

In [50]: trewards = agent.test(100)
         episode:  100/100 | treward:  200
In [51]: sum(trewards) / len(trewards)
Out[51]: 200.0

简单的财务健身房

为了将 QL 方法转移到金融领域,本节提供了一个类,模仿 OpenAI Gym 环境,但适用于由金融时间序列数据表示的金融市场。其想法是,类似于CartPole环境,四个历史价格代表了金融市场的状态。当呈现状态时,代理可以决定是持有多头还是持有空头。在这种情况下,两个环境是可比较的,因为一个状态由四个参数给出,代理可以采取两种不同的行动。

为了模仿 OpenAI Gym API,需要两个辅助类——一个用于观察空间,另一个用于行动空间:

In [52]: class observation_space:
             def __init__(self, n):
                 self.shape = (n,)

In [53]: class action_space:
             def __init__(self, n):
                 self.n = n
             def seed(self, seed):
                 pass
             def sample(self):
                 return random.randint(0, self.n - 1)

以下 Python 代码定义了Finance类。它检索多个符号的每日历史价格。该类的主要方法是.reset().step().step()方法检查是否已采取正确的操作,相应地定义奖励,并检查成功或失败。当代理能够正确地贸易整个数据集时,就实现了成功。当然,可以以不同的方式定义成功(例如,当代理成功进行了 1,000 步的交易时即视为成功)。失败被定义为精度比低于 50%(总奖励除以总步数)。但是,这只是在一定数量的步骤之后进行检查,以避免此度量的高初始方差:

In [54]: class Finance:
             url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'
             def __init__(self, symbol, features):
                 self.symbol = symbol
                 self.features = features
                 self.observation_space = observation_space(4)
                 self.osn = self.observation_space.shape[0]
                 self.action_space = action_space(2)
                 self.min_accuracy = 0.475  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 self._get_data()
                 self._prepare_data()
             def _get_data(self):
                 self.raw = pd.read_csv(self.url, index_col=0,
                                        parse_dates=True).dropna()
             def _prepare_data(self):
                 self.data = pd.DataFrame(self.raw[self.symbol])
                 self.data['r'] = np.log(self.data / self.data.shift(1))
                 self.data.dropna(inplace=True)
                 self.data = (self.data - self.data.mean()) / self.data.std()
                 self.data['d'] = np.where(self.data['r'] > 0, 1, 0)
             def _get_state(self):
                 return self.data[self.features].iloc[
                     self.bar - self.osn:self.bar].values  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             def seed(self, seed=None):
                 pass
             def reset(self):  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 self.treward = 0
                 self.accuracy = 0
                 self.bar = self.osn
                 state = self.data[self.features].iloc[
                     self.bar - self.osn:self.bar]
                 return state.values
             def step(self, action):
                 correct = action == self.data['d'].iloc[self.bar]  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 reward = 1 if correct else 0  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 self.treward += reward  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 self.bar += 1  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
                 self.accuracy = self.treward / (self.bar - self.osn)  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
                 if self.bar >= len(self.data):  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
                     done = True
                 elif reward == 1:  ![10](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/10.png)
                     done = False
                 elif (self.accuracy < self.min_accuracy and
                       self.bar > self.osn + 10):  ![11](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/11.png)
                     done = True
                 else:  ![12](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/12.png)
                     done = False
                 state = self._get_state()
                 info = {}
                 return state, reward, done, info

1

定义所需的最小精度。

2

选择定义金融市场状态的数据。

3

将环境重置为其初始值。

4

检查代理是否选择了正确的行动(成功交易)。

5

定义代理接收的奖励。

6

将奖励添加到总奖励中。

7

将环境向前推进一步。

8

计算成功行动(交易)的准确性,考虑所有步骤(交易)。

9

如果智能体达到数据集的末尾,则视为成功。

10

如果智能体采取正确的行动,它可以继续前进。

11

在一些初始步骤之后,如果准确性降到最低水平以下,则该情节结束(失败)。

12

对于其余情况,智能体可以继续前进。

Finance 类的实例表现得像是 OpenAI Gym 的环境。特别是在这个基础案例中,该实例的行为与 CartPole 环境完全一致:

In [55]: env = Finance('EUR=', 'EUR=')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [56]: env.reset()
Out[56]: array([1.819 , 1.8579, 1.7749, 1.8579])

In [57]: a = env.action_space.sample()
         a
Out[57]: 0

In [58]: env.step(a)
Out[58]: (array([1.8579, 1.7749, 1.8579, 1.947 ]), 0, False, {})

1

指定用于定义表示状态数据的符号和特征类型(符号或对数收益)。

DQLAgent 是否能像为 CartPole 游戏开发的那样,在金融市场中进行交易学习?是的,可以,正如下面的代码所示。然而,尽管智能体在训练情节中平均改善了其交易技能,但结果并不十分令人印象深刻(参见 图 9-3):

In [59]: set_seeds(100)
         agent = DQLAgent(gamma=0.5, opt=RMSprop)

In [60]: episodes = 1000

In [61]: agent.learn(episodes)
         episode: 1000/1000 | treward: 2511 | av: 1012.7 | max: 2511
In [62]: agent.test(3)
         episode:    3/3 | treward: 2511
Out[62]: [2511, 2511, 2511]

In [63]: plt.figure(figsize=(10, 6))
         x = range(len(agent.averages))
         y = np.polyval(np.polyfit(x, agent.averages, deg=3), x)
         plt.plot(agent.averages, label='moving average')
         plt.plot(x, y, 'r--', label='regression')
         plt.xlabel('episodes')
         plt.ylabel('total reward')
         plt.legend();

aiif 0903

图 9-3. FinanceDQLAgent 的平均总奖励

通用 RL 智能体

本节为模仿 OpenAI Gym 环境 API 的金融市场环境提供了一个类。它还将 QL 智能体应用于新的金融市场环境,而无需对智能体本身进行任何更改。尽管智能体在这种新环境中的表现可能并不令人印象深刻,但它说明了本章介绍的 RL 方法相当通用。RL 智能体通常可以从其交互的不同环境中学习。这在某种程度上解释了为什么 DeepMind 的 AlphaZero 能够掌握围棋、国际象棋和将棋,如 第二章 中所讨论的那样。

更好的金融 Gym

前一节的想法是开发一个简单的类,允许在金融市场环境中进行强化学习。该节的主要目标是复制 OpenAI Gym 环境的 API。然而,并不需要将这样的环境限制在单一类型的特征来描述金融市场状态,也不需要仅使用四个滞后期。本节介绍了一个改进的 Finance 类,允许多个特征、灵活的滞后期数量以及用于基础数据集的特定起始点和结束点。这样做,除其他事项外,还允许将数据集的一部分用于学习,另一部分用于验证或测试。以下 Python 代码还允许使用杠杆。在考虑相对较小的绝对回报的分钟级数据时,这可能是有帮助的:

In [64]: class Finance:
             url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'
             def __init__(self, symbol, features, window, lags,
                          leverage=1, min_performance=0.85,
                          start=0, end=None, mu=None, std=None):
                 self.symbol = symbol
                 self.features = features  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 self.n_features = len(features)
                 self.window = window
                 self.lags = lags  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 self.leverage = leverage  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 self.min_performance = min_performance  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 self.start = start
                 self.end = end
                 self.mu = mu
                 self.std = std
                 self.observation_space = observation_space(self.lags)
                 self.action_space = action_space(2)
                 self._get_data()
                 self._prepare_data()
             def _get_data(self):
                 self.raw = pd.read_csv(self.url, index_col=0,
                                        parse_dates=True).dropna()
             def _prepare_data(self):
                 self.data = pd.DataFrame(self.raw[self.symbol])
                 self.data = self.data.iloc[self.start:]
                 self.data['r'] = np.log(self.data / self.data.shift(1))
                 self.data.dropna(inplace=True)
                 self.data['s'] = self.data[self.symbol].rolling(
                                                       self.window).mean()   ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 self.data['m'] = self.data['r'].rolling(self.window).mean()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 self.data['v'] = self.data['r'].rolling(self.window).std()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 self.data.dropna(inplace=True)
                 if self.mu is None:
                     self.mu = self.data.mean()  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                     self.std = self.data.std()  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 self.data_ = (self.data - self.mu) / self.std  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 self.data_['d'] = np.where(self.data['r'] > 0, 1, 0)
                 self.data_['d'] = self.data_['d'].astype(int)
                 if self.end is not None:
                     self.data = self.data.iloc[:self.end - self.start]
                     self.data_ = self.data_.iloc[:self.end - self.start]
             def _get_state(self):
                 return self.data_[self.features].iloc[self.bar -
                                         self.lags:self.bar]
             def seed(self, seed):
                 random.seed(seed)
                 np.random.seed(seed)
             def reset(self):
                 self.treward = 0
                 self.accuracy = 0
                 self.performance = 1
                 self.bar = self.lags
                 state = self.data_[self.features].iloc[self.bar-
                                 self.lags:self.bar]
                 return state.values
             def step(self, action):
                 correct = action == self.data_['d'].iloc[self.bar]
                 ret = self.data['r'].iloc[self.bar] * self.leverage  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 reward_1 = 1 if correct else 0
                 reward_2 = abs(ret) if correct else -abs(ret)  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
                 factor = 1 if correct else -1
                 self.treward += reward_1
                 self.bar += 1
                 self.accuracy = self.treward / (self.bar - self.lags)
                 self.performance *= math.exp(reward_2)  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
                 if self.bar >= len(self.data):
                     done = True
                 elif reward_1 == 1:
                     done = False
                 elif (self.performance < self.min_performance and
                       self.bar > self.lags + 5):
                     done = True
                 else:
                     done = False
                 state = self._get_state()
                 info = {}
                 return state.values, reward_1 + reward_2 * 5, done, info

1

用于定义状态的特征

2

要使用的滞后数

3

所需的最低总体性能

4

附加的金融特征(简单移动平均线、动量、滚动波动性)

5

数据的高斯归一化

6

步骤的杠杆收益率

7

步骤的基于回报的奖励

8

步骤后的总体性能

新的Finance类为金融市场环境的建模提供了更大的灵活性。以下代码展示了两个特征和五个滞后的示例:

In [65]: env = Finance('EUR=', ['EUR=', 'r'], 10, 5)

In [66]: a = env.action_space.sample()
         a
Out[66]: 0

In [67]: env.reset()
Out[67]: array([[ 1.7721, -1.0214],
                [ 1.5973, -2.4432],
                [ 1.5876, -0.1208],
                [ 1.6292,  0.6083],
                [ 1.6408,  0.1807]])

In [68]: env.step(a)
Out[68]: (array([[ 1.5973, -2.4432],
                 [ 1.5876, -0.1208],
                 [ 1.6292,  0.6083],
                 [ 1.6408,  0.1807],
                 [ 1.5725, -0.9502]]),
          1.0272827803740798,
          False,
          {})

FQL 代理

基于新的Finance环境,本节改进了简单的 DQL 代理以提高在金融市场环境中的性能。FQLAgent类能够处理多个特征和灵活的滞后数。它还将学习环境(learn_env)与验证环境(valid_env)区分开来。这允许在训练期间获得代理的样本外性能的更真实的图片。该类的基本结构和 RL/QL 学习方法对于DQLAgent类和FQLAgent类都是相同的:

In [69]: class FQLAgent:
             def __init__(self, hidden_units, learning_rate, learn_env, valid_env):
                 self.learn_env = learn_env
                 self.valid_env = valid_env
                 self.epsilon = 1.0
                 self.epsilon_min = 0.1
                 self.epsilon_decay = 0.98
                 self.learning_rate = learning_rate
                 self.gamma = 0.95
                 self.batch_size = 128
                 self.max_treward = 0
                 self.trewards = list()
                 self.averages = list()
                 self.performances = list()
                 self.aperformances = list()
                 self.vperformances = list()
                 self.memory = deque(maxlen=2000)
                 self.model = self._build_model(hidden_units, learning_rate)

             def _build_model(self, hu, lr):
                 model = Sequential()
                 model.add(Dense(hu, input_shape=(
                     self.learn_env.lags, self.learn_env.n_features),
                                 activation='relu'))
                 model.add(Dropout(0.3, seed=100))
                 model.add(Dense(hu, activation='relu'))
                 model.add(Dropout(0.3, seed=100))
                 model.add(Dense(2, activation='linear'))
                 model.compile(
                     loss='mse',
                     optimizer=RMSprop(lr=lr)
                 )
                 return model

             def act(self, state):
                 if random.random() <= self.epsilon:
                     return self.learn_env.action_space.sample()
                 action = self.model.predict(state)[0, 0]
                 return np.argmax(action)

             def replay(self):
                 batch = random.sample(self.memory, self.batch_size)
                 for state, action, reward, next_state, done in batch:
                     if not done:
                         reward += self.gamma * np.amax(
                             self.model.predict(next_state)[0, 0])
                     target = self.model.predict(state)
                     target[0, 0, action] = reward
                     self.model.fit(state, target, epochs=1,
                                    verbose=False)
                 if self.epsilon > self.epsilon_min:
                     self.epsilon *= self.epsilon_decay

             def learn(self, episodes):
                 for e in range(1, episodes + 1):
                     state = self.learn_env.reset()
                     state = np.reshape(state, [1, self.learn_env.lags,
                                                self.learn_env.n_features])
                     for _ in range(10000):
                         action = self.act(state)
                         next_state, reward, done, info = \
                                         self.learn_env.step(action)
                         next_state = np.reshape(next_state,
                                         [1, self.learn_env.lags,
                                          self.learn_env.n_features])
                         self.memory.append([state, action, reward,
                                              next_state, done])
                         state = next_state
                         if done:
                             treward = _ + 1
                             self.trewards.append(treward)
                             av = sum(self.trewards[-25:]) / 25
                             perf = self.learn_env.performance
                             self.averages.append(av)
                             self.performances.append(perf)
                             self.aperformances.append(
                                 sum(self.performances[-25:]) / 25)
                             self.max_treward = max(self.max_treward, treward)
                             templ = 'episode: {:2d}/{} | treward: {:4d} | '
                             templ += 'perf: {:5.3f} | av: {:5.1f} | max: {:4d}'
                             print(templ.format(e, episodes, treward, perf,
                                           av, self.max_treward), end='\r')
                             break
                     self.validate(e, episodes)
                     if len(self.memory) > self.batch_size:
                         self.replay()
             def validate(self, e, episodes):
                 state = self.valid_env.reset()
                 state = np.reshape(state, [1, self.valid_env.lags,
                                            self.valid_env.n_features])
                 for _ in range(10000):
                     action = np.argmax(self.model.predict(state)[0, 0])
                     next_state, reward, done, info = self.valid_env.step(action)
                     state = np.reshape(next_state, [1, self.valid_env.lags,
                                            self.valid_env.n_features])
                     if done:
                         treward = _ + 1
                         perf = self.valid_env.performance
                         self.vperformances.append(perf)
                         if e % 20 == 0:
                             templ = 71 * '='
                             templ += '\nepisode: {:2d}/{} | VALIDATION | '
                             templ += 'treward: {:4d} | perf: {:5.3f} | '
                             templ += 'eps: {:.2f}\n'
                             templ += 71 * '='
                             print(templ.format(e, episodes, treward,
                                                perf, self.epsilon))
                         break

以下 Python 代码显示了FQLAgent的性能明显优于解决CartPole问题的简单DQLAgent的性能。这个交易机器人似乎通过与金融市场环境的互动而相当一致地了解交易(见图 9-4):

In [70]: symbol = 'EUR='
         features = [symbol, 'r', 's', 'm', 'v']

In [71]: a = 0
         b = 2000
         c = 500

In [72]: learn_env = Finance(symbol, features, window=10, lags=6,
                          leverage=1, min_performance=0.85,
                          start=a, end=a + b, mu=None, std=None)

In [73]: learn_env.data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 2000 entries, 2010-01-19 to 2017-12-26
         Data columns (total 5 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   EUR=    2000 non-null   float64
          1   r       2000 non-null   float64
          2   s       2000 non-null   float64
          3   m       2000 non-null   float64
          4   v       2000 non-null   float64
         dtypes: float64(5)
         memory usage: 93.8 KB

In [74]: valid_env = Finance(symbol, features, window=learn_env.window,
                          lags=learn_env.lags, leverage=learn_env.leverage,
                          min_performance=learn_env.min_performance,
                          start=a + b, end=a + b + c,
                          mu=learn_env.mu, std=learn_env.std)

In [75]: valid_env.data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 500 entries, 2017-12-27 to 2019-12-20
         Data columns (total 5 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   EUR=    500 non-null    float64
          1   r       500 non-null    float64
          2   s       500 non-null    float64
          3   m       500 non-null    float64
          4   v       500 non-null    float64
         dtypes: float64(5)
         memory usage: 23.4 KB

In [76]: set_seeds(100)
         agent = FQLAgent(24, 0.0001, learn_env, valid_env)

In [77]: episodes = 61

In [78]: agent.learn(episodes)
         =======================================================================
         episode: 20/61 | VALIDATION | treward:  494 | perf: 1.169 | eps: 0.68
         =======================================================================
         =======================================================================
         episode: 40/61 | VALIDATION | treward:  494 | perf: 1.111 | eps: 0.45
         =======================================================================
         =======================================================================
         episode: 60/61 | VALIDATION | treward:  494 | perf: 1.089 | eps: 0.30
         =======================================================================
         episode: 61/61 | treward: 1994 | perf: 1.268 | av: 1615.1 | max: 1994
In [79]: agent.epsilon
Out[79]: 0.291602079838278

In [80]: plt.figure(figsize=(10, 6))
         x = range(1, len(agent.averages) + 1)
         y = np.polyval(np.polyfit(x, agent.averages, deg=3), x)
         plt.plot(agent.averages, label='moving average')
         plt.plot(x, y, 'r--', label='regression')
         plt.xlabel('episodes')
         plt.ylabel('total reward')
         plt.legend();

aiif 0904

图 9-4。FinanceFQLAgent的平均总奖励

对于训练和验证性能还出现了一个有趣的图像,如图 9-5 所示。训练性能显示出较高的方差,这是由于探索活动,除了目前最优政策的开发之外。相比之下,验证性能的方差要低得多,因为它仅依赖于目前最优政策的开发:

In [81]: plt.figure(figsize=(10, 6))
         x = range(1, len(agent.performances) + 1)
         y = np.polyval(np.polyfit(x, agent.performances, deg=3), x)
         y_ = np.polyval(np.polyfit(x, agent.vperformances, deg=3), x)
         plt.plot(agent.performances[:], label='training')
         plt.plot(agent.vperformances[:], label='validation')
         plt.plot(x, y, 'r--', label='regression (train)')
         plt.plot(x, y_, 'r-.', label='regression (valid)')
         plt.xlabel('episodes')
         plt.ylabel('gross performance')
         plt.legend();

aiif 0905

图 9-5。FQLAgent每一集的训练和验证性能

结论

本章讨论了强化学习作为人工智能提供的最成功算法类之一。在第二章讨论的大部分进展和成功案例都源于强化学习领域的改进。在这种情况下,神经网络并没有变得无用。相反,它们在逼近最优动作策略方面起着重要作用,通常以策略 Q 的形式表现,给定某个状态,为每个动作分配一个值。值越高,动作越好,同时考虑即时和延迟奖励。

当然,延迟奖励的包含在许多重要的背景中是相关的。在游戏背景中,通常有多种动作可供选择,选择承诺最高总奖励的动作是最优的,而不仅仅是最高的即时奖励。最终的总分是要被最大化的。在金融背景中也是如此。一般来说,长期的表现是交易和投资的适当目标,而不是可能带来增加破产风险的快速短期利润。

本章的例子还表明,强化学习方法在灵活性和普适性上非常强,可以同样适用于不同的设置。解决CartPole问题的 DQL 代理也可以学会如何在金融市场中交易,尽管效果不是很好。基于Finance环境和 FQL 代理的改进,FQL 交易机器人在样本内(训练数据)和样本外(验证数据)都展现出了可观的性能。

参考文献

本章引用的书籍和论文:

  • Sutton, Richard S. 和 Andrew G. Barto. 2018. 强化学习:一种介绍. 剑桥和伦敦:麻省理工学院出版社。

  • Watkins, Christopher. 1989. 从延迟奖励中学习. 剑桥大学博士论文。

  • Watkins, Christopher 和 Peter Dayan. 1992. “Q 学习。” 机器学习 8 (五月): 279-282.

¹ 参见深度强化学习

² 参见,例如,这篇博客文章

³ 实现与这篇博客文章中的类似。

第四部分:算法交易

成功意味着盈利和避免亏损。

马丁·兹威格

第 III 部分关注通过深度学习和强化学习技术发现金融市场中的统计低效率。相比之下,本部分关注于识别和利用一般情况下统计低效率是经济低效率的先决条件的经济低效率。利用经济低效率的工具是算法交易,即基于交易机器人生成的预测自动执行交易策略。

表格 IV-1 以简化的方式比较了训练和部署交易机器人与构建和部署自动驾驶汽车的问题。

表格 IV-1. 自动驾驶汽车与交易机器人的比较

步骤 自动驾驶汽车 交易机器人
训练 在虚拟和记录环境中训练 AI 使用模拟和真实历史数据训练 AI
风险管理 添加规则以避免碰撞、坠毁等 添加规则以避免大额损失,尽早获利等
部署 将 AI 与汽车硬件结合,将汽车部署在街道上,并进行监控 将 AI 与交易平台结合,部署交易机器人进行实际交易,并进行监控

本部分由三章组成,结构化沿着三个步骤进行,正如表格 IV-1 所示,通过交易机器人利用经济效率低下来实现—从交易策略的向量化回测开始,覆盖通过基于事件的回测分析风险管理措施,并在策略执行和部署的背景下讨论技术细节:

  • 第十章讲述了基于向量化回测的算法交易策略,例如基于 DNN 进行市场预测的策略。这种方法在对交易策略的经济潜力做出初步判断方面既高效又具有洞察力。它还可以评估交易成本对经济绩效的影响。

  • 第十一章涵盖了管理算法交易策略风险的核心方面,例如使用止损单或止盈单。除了向量化回测,本章介绍了基于事件的回测作为一种更灵活的方法,以评估交易策略的经济潜力。

  • 第十二章 主要讨论交易策略的执行。主题包括历史数据的获取、基于这些数据训练交易机器人、实时数据的流式传输以及订单的下达。介绍了将 Oanda 及其 API 作为适合算法交易的交易平台。还涵盖了以自动化方式部署基于人工智能的算法交易策略的基本方面。

算法交易策略

算法交易是一个广阔的领域,涵盖了不同类型的交易策略。例如,有些策略试图在执行大额订单时尽量减少市场影响(流动性算法),而另一些则试图尽可能地复制衍生品的回报(动态对冲/复制)。这些例子说明,并非所有算法交易策略的目标都是利用经济效率低下的机会。对于本书的目的而言,重点放在由交易机器人进行预测而产生的算法交易策略(例如以 DNN 代理或 RL 代理的形式)似乎是合适和有用的。

第十章:矢量化回测

特斯拉的首席执行官和连续技术企业家埃隆·马斯克表示,他的公司的汽车将能够在未来两年内被召唤并自主驾驶穿越美国,以接载他们的主人。

Samuel Gibbs(2016)

赚取大笔资金是通过站在股市主要波动的正确一侧来实现的。

Martin Zweig

矢量化回测 这个术语指的是一种技术方法,用于回测基于密集神经网络(DNN)进行市场预测等算法交易策略。 Hilpisch 的书籍(2018 年,第十五章;2020 年,第四章)涵盖了基于多个具体例子的矢量化回测。在这个上下文中,矢量化 指的是一种编程范式,严重依赖甚至完全依赖于矢量化代码(即在 Python 级别上没有任何循环的代码)。在一般情况下,使用 Numpypandas 等软件包的代码矢量化是良好的实践,并且在前几章中也已经广泛使用。矢量化代码的好处是更简洁、易于阅读的代码,以及在许多重要情况下更快的执行速度。另一方面,与基于事件的回测(例如在 第十一章 中介绍和使用的方法)相比,矢量化代码在回测交易策略的灵活性上可能不足。

拥有一个能够击败简单基准预测器的良好 AI 预测器是重要的,但通常不足以产生alpha(即超过市场回报,可能根据风险调整)。例如,对于基于预测的交易策略,重要的是正确预测大市场波动,而不仅仅是多数(可能相当小的)市场波动。矢量化回测是快速了解交易策略经济潜力的简便方法。

与自动驾驶车辆(AV)相比,矢量化回测就像是在虚拟环境中测试 AV 的 AI,只是为了看它在“一般情况下”表现如何,而不会有风险。然而,对于 AV 的 AI 来说,不仅在“平均情况下”表现良好是重要的,更重要的是看它如何掌握关键甚至极端情况。这样的 AI 应该平均造成“零伤亡”,而不是 0.1 或 0.5。对于金融 AI 而言,同样重要(即使不完全相同)的是正确预测大市场波动。而本章重点讨论金融 AI 代理(交易机器人)的纯性能, 第十一章 则深入探讨风险评估和标准风险度量的回测。

“基于 SMA 的回测策略”介绍了基于简单移动平均线的向量化回测示例,使用每日收盘数据。这样可以进行深入的可视化和更轻松的理解初始方法。“基于每日 DNN 的回测策略”在每日收盘数据上训练了一个 DNN,并对基于预测的策略进行了经济绩效的回测。“基于日内 DNN 的回测策略”然后使用日内数据进行相同的操作。在所有示例中,都包括了比例交易成本,以假定的买卖价差形式。

基于 SMA 的回测策略

本节介绍基于经典交易策略的向量化回测,该策略使用简单移动平均线(SMAs)作为技术指标。以下代码实现了必要的导入和配置,并获取了 EUR/USD 货币对的每日收盘数据:

In [1]: import os
        import math
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        pd.set_option('mode.chained_assignment', None)
        pd.set_option('display.float_format', '{:.4f}'.format)
        np.set_printoptions(suppress=True, precision=4)
        os.environ['PYTHONHASHSEED'] = '0'

In [2]: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: symbol = 'EUR='  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [4]: data = pd.DataFrame(pd.read_csv(url, index_col=0,
                                        parse_dates=True).dropna()[symbol])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [5]: data.info()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 2516 entries, 2010-01-04 to 2019-12-31
        Data columns (total 1 columns):
         #   Column  Non-Null Count  Dtype
        ---  ------  --------------  -----
         0   EUR=    2516 non-null   float64
        dtypes: float64(1)
        memory usage: 39.3 KB

1

获取 EUR/USD 的每日收盘数据

策略的思路如下。计算较短的 SMA1,例如 42 天,和较长的 SMA2,例如 258 天。每当 SMA1 大于 SMA2 时,在金融工具上建立多头头寸。每当 SMA1 小于 SMA2 时,在金融工具上建立空头头寸。由于本示例基于 EUR/USD,因此很容易实现多头或空头。

下面的 Python 代码以向量化方式计算 SMA 值,并将结果时间序列与原始时间序列一起进行可视化(参见 图 10-1):

In [6]: data['SMA1'] = data[symbol].rolling(42).mean()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [7]: data['SMA2'] = data[symbol].rolling(258).mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [8]: data.plot(figsize=(10, 6));  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

1

计算较短的 SMA1

2

计算较长的 SMA2

3

可视化了三个时间序列

使用了 SMA 时间序列数据后,再次以向量化方式计算出了相应的仓位。请注意,由于数据中存在前瞻偏差,所以需要将得出的仓位时间序列向后推移一天。这种推移是必要的,因为计算 SMA 包括当天的收盘价。因此,从一天的 SMA 值推导出的仓位需要应用到下一天的整个时间序列中。

aiif 1001

图 10-1. EUR/USD 和 SMAs 的时间序列数据

图 10-2 将得出的仓位作为其他时间序列的叠加显示:

In [9]: data.dropna(inplace=True)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [10]: data['p'] = np.where(data['SMA1'] > data['SMA2'], 1, -1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [11]: data['p'] = data['p'].shift(1)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [12]: data.dropna(inplace=True)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [13]: data.plot(figsize=(10, 6), secondary_y='p');  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

1

删除包含 NaN 值的行

2

根据同一日的 SMA 值推导出仓位值

3

将仓位值向后推移一天,以避免前瞻偏差

4

可视化从 SMAs 得出的仓位值

aiif 1002

图 10-2. EUR/USD、SMAs 和生成的头寸的时间序列数据

还缺少一个关键步骤:将头寸与金融工具的收益结合起来。由于头寸可以方便地通过 +1 表示多头头寸,-1 表示空头头寸,因此这一步骤可以简化为再次向量化地将 DataFrame 对象的两列相乘。与被动基准投资相比,基于 SMA 的交易策略表现出色,如图 10-3 所示:

In [14]: data['r'] = np.log(data[symbol] / data[symbol].shift(1))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [15]: data.dropna(inplace=True)

In [16]: data['s'] = data['p'] * data['r']  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [17]: data[['r', 's']].sum().apply(np.exp)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[17]: r   0.8640
         s   1.3773
         dtype: float64

In [18]: data[['r', 's']].sum().apply(np.exp) - 1  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[18]: r   -0.1360
         s    0.3773
         dtype: float64

In [19]: data[['r', 's']].cumsum().apply(np.exp).plot(figsize=(10, 6));  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

1

计算对数收益

2

计算策略收益

3

计算总体表现

4

计算净表现

5

可视化随时间的总体表现

aiif 1003

图 10-3. 被动基准投资和 SMA 策略的总体表现

到目前为止,表现数据并未考虑交易成本。当然,在评估交易策略的经济潜力时,这些成本是至关重要的因素。在当前设置中,可以轻松地将比例交易成本纳入计算中。其核心思想是确定交易发生的时机,并通过减少交易策略的表现来考虑相关的买卖价差。正如接下来的计算所示,并且如图 10-2 所示,交易策略并不经常改变头寸。因此,为了有效考虑交易成本的影响,假设其比通常的 EUR/USD 更高。在给定假设下,减去交易成本的净效果是几个百分点(见图 10-4):

In [20]: sum(data['p'].diff() != 0) + 2  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[20]: 10

In [21]: pc = 0.005  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [22]: data['s_'] = np.where(data['p'].diff() != 0,
                               data['s'] - pc, data['s'])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [23]: data['s_'].iloc[0] -= pc  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [24]: data['s_'].iloc[-1] -= pc  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [25]: data[['r', 's', 's_']][data['p'].diff() != 0]  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[25]:                  r       s      s_
         Date
         2011-01-12  0.0123  0.0123  0.0023
         2011-10-10  0.0198 -0.0198 -0.0248
         2012-11-07 -0.0034 -0.0034 -0.0084
         2014-07-24 -0.0001  0.0001 -0.0049
         2016-03-16  0.0102  0.0102  0.0052
         2016-11-10 -0.0018  0.0018 -0.0032
         2017-06-05 -0.0025 -0.0025 -0.0075
         2018-06-15  0.0035 -0.0035 -0.0085

In [26]: data[['r', 's', 's_']].sum().apply(np.exp)
Out[26]: r    0.8640
         s    1.3773
         s_   1.3102
         dtype: float64

In [27]: data[['r', 's', 's_']].sum().apply(np.exp) - 1
Out[27]: r    -0.1360
         s     0.3773
         s_    0.3102
         dtype: float64

In [28]: data[['r', 's', 's_']].cumsum().apply(np.exp).plot(figsize=(10, 6));

1

计算交易次数,包括进入和退出交易

2

修正比例交易成本(故意设置得很高)

3

调整交易成本对策略表现的影响

4

调整进入交易的策略表现

5

调整退出交易的策略表现

6

展示常规交易的调整后表现数值

aiif 1004

图 10-4. SMA 策略在交易成本前后的总体表现

关于交易策略的结果风险如何?对于基于方向预测且仅采取多头或空头头寸的交易策略,表达为波动率(对数收益标准差)的风险与被动基准投资完全相同:

In [29]: data[['r', 's', 's_']].std()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[29]: r    0.0054
         s    0.0054
         s_   0.0054
         dtype: float64

In [30]: data[['r', 's', 's_']].std() * math.sqrt(252)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[30]: r    0.0853
         s    0.0853
         s_   0.0855
         dtype: float64

1

每日波动率

2

年化波动率

向量化回测

向量化回测是回测基于预测的交易策略“纯”性能的强大高效方法。它还可以适应比例交易成本等。然而,它不适合包含典型的风险管理措施,如(追踪)止损订单或获利订单。这在第十一章中有所涉及。

日常基于 DNN 的策略回测

前一节基于简单易于可视化的交易策略提出了向量化回测的蓝图。相同的蓝图可以应用于基于深度神经网络的交易策略,只需进行最少的技术调整。以下训练了一个Keras DNN 模型,如第七章讨论的那样。使用的数据与前面的例子相同。然而,就像在第七章中一样,需要向DataFrame对象添加不同的特征及其滞后值:

In [31]: data = pd.DataFrame(pd.read_csv(url, index_col=0,
                                         parse_dates=True).dropna()[symbol])

In [32]: data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 2516 entries, 2010-01-04 to 2019-12-31
         Data columns (total 1 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   EUR=    2516 non-null   float64
         dtypes: float64(1)
         memory usage: 39.3 KB

In [33]: lags = 5

In [34]: def add_lags(data, symbol, lags, window=20):
             cols = []
             df = data.copy()
             df.dropna(inplace=True)
             df['r'] = np.log(df / df.shift(1))
             df['sma'] = df[symbol].rolling(window).mean()
             df['min'] = df[symbol].rolling(window).min()
             df['max'] = df[symbol].rolling(window).max()
             df['mom'] = df['r'].rolling(window).mean()
             df['vol'] = df['r'].rolling(window).std()
             df.dropna(inplace=True)
             df['d'] = np.where(df['r'] > 0, 1, 0)
             features = [symbol, 'r', 'd', 'sma', 'min', 'max', 'mom', 'vol']
             for f in features:
                 for lag in range(1, lags + 1):
                     col = f'{f}_lag_{lag}'
                     df[col] = df[f].shift(lag)
                     cols.append(col)
             df.dropna(inplace=True)
             return df, cols

In [35]: data, cols = add_lags(data, symbol, lags, window=20)

以下 Python 代码完成了额外的导入并定义了set_seeds()create_model()函数:

In [36]: import random
         import tensorflow as tf
         from keras.layers import Dense, Dropout
         from keras.models import Sequential
         from keras.regularizers import l1
         from keras.optimizers import Adam
         from sklearn.metrics import accuracy_score
         Using TensorFlow backend.

In [37]: def set_seeds(seed=100):
             random.seed(seed)
             np.random.seed(seed)
             tf.random.set_seed(seed)
         set_seeds()

In [38]: optimizer = Adam(learning_rate=0.0001)

In [39]: def create_model(hl=2, hu=128, dropout=False, rate=0.3,
                         regularize=False, reg=l1(0.0005),
                         optimizer=optimizer, input_dim=len(cols)):
             if not regularize:
                 reg = None
             model = Sequential()
             model.add(Dense(hu, input_dim=input_dim,
                          activity_regularizer=reg,
                          activation='relu'))
             if dropout:
                 model.add(Dropout(rate, seed=100))
             for _ in range(hl):
                 model.add(Dense(hu, activation='relu',
                              activity_regularizer=reg))
                 if dropout:
                     model.add(Dropout(rate, seed=100))
             model.add(Dense(1, activation='sigmoid'))
             model.compile(loss='binary_crossentropy',
                           optimizer=optimizer,
                           metrics=['accuracy'])
             return model

基于历史数据的顺序训练测试分割,以下 Python 代码首先基于归一化的特征数据训练 DNN 模型:

In [40]: split = '2018-01-01'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [41]: train = data.loc[:split].copy()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [42]: np.bincount(train['d'])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[42]: array([ 982, 1006])

In [43]: mu, std = train.mean(), train.std()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [44]: train_ = (train - mu) / std  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [45]: set_seeds()
         model = create_model(hl=2, hu=64)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [46]: %%time
         model.fit(train_[cols], train['d'],
                 epochs=20, verbose=False,
                 validation_split=0.2, shuffle=False)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         CPU times: user 2.93 s, sys: 574 ms, total: 3.5 s
         Wall time: 1.93 s

Out[46]: <keras.callbacks.callbacks.History at 0x7fc9392f38d0>

In [47]: model.evaluate(train_[cols], train['d'])  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         1988/1988 [==============================] - 0s 17us/step

Out[47]: [0.6745863538872549, 0.5925553441047668]

1

将数据分割为训练和测试数据

2

显示标签类别的频率

3

规范化训练特征数据

4

创建 DNN 模型

5

在训练数据上训练 DNN 模型

6

评估模型在训练数据上的表现

到目前为止,这基本上重复了第七章的核心方法。现在可以应用向量化回测来评估基于模型预测的 DNN 交易策略的经济表现(见图 10-5)。在这种情况下,向上的预测自然被解释为多头头寸,向下的预测则被解释为空头头寸:

In [48]: train['p'] = np.where(model.predict(train_[cols]) > 0.5, 1, 0)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [49]: train['p'] = np.where(train['p'] == 1, 1, -1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [50]: train['p'].value_counts()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[50]: -1    1098
          1     890
         Name: p, dtype: int64

In [51]: train['s'] = train['p'] * train['r']  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [52]: train[['r', 's']].sum().apply(np.exp)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[52]: r   0.8787
         s   5.0766
         dtype: float64

In [53]: train[['r', 's']].sum().apply(np.exp)  - 1  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[53]: r   -0.1213
         s    4.0766
         dtype: float64

In [54]: train[['r', 's']].cumsum().apply(np.exp).plot(figsize=(10, 6));  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

1

生成二元预测

2

将预测转换为位置数值

3

展示多头和空头头寸的数量

4

计算策略的表现数值

5

计算毛收益和净收益(样本内)

6

可视化时间内的毛收益表现(样本内)

aiif 1005

图 10-5. 被动基准投资和每日 DNN 策略的毛收益表现(样本内)

下面是针对测试数据集的同一计算序列。虽然样本内的超额表现显著,但样本外的数字并不那么令人印象深刻,但仍然令人信服(参见图 10-6):

In [55]: test = data.loc[split:].copy()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [56]: test_ = (test - mu) / std  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [57]: model.evaluate(test_[cols], test['d'])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         503/503 [==============================] - 0s 17us/step

Out[57]: [0.6933823573897421, 0.5407554507255554]

In [58]: test['p'] = np.where(model.predict(test_[cols]) > 0.5, 1, -1)

In [59]: test['p'].value_counts()
Out[59]: -1    406
          1     97
         Name: p, dtype: int64

In [60]: test['s'] = test['p'] * test['r']

In [61]: test[['r', 's']].sum().apply(np.exp)
Out[61]: r   0.9345
         s   1.2431
         dtype: float64

In [62]: test[['r', 's']].sum().apply(np.exp) - 1
Out[62]: r   -0.0655
         s    0.2431
         dtype: float64

In [63]: test[['r', 's']].cumsum().apply(np.exp).plot(figsize=(10, 6));

1

生成测试数据子集

2

标准化测试数据

3

在测试数据上评估模型的表现

基于深度神经网络的交易策略导致交易次数比基于 SMA 的策略更多。这使得在评估经济表现时,包括交易成本变得更加重要。

aiif 1006

图 10-6. 被动基准投资和每日 DNN 策略的毛收益表现(样本外)

下面的代码现在假设 EUR/USD 的实际报价价差为 1.2 个点(即货币单位的 0.00012)。¹ 为了简化计算,基于 EUR/USD 的平均收盘价计算比例交易成本pc的平均值(参见图 10-7):

In [64]: sum(test['p'].diff() != 0)
Out[64]: 147

In [65]: spread = 0.00012  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         pc = spread / data[symbol].mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         print(f'{pc:.6f}')
         0.000098

In [66]: test['s_'] = np.where(test['p'].diff() != 0,
                               test['s'] - pc, test['s'])

In [67]: test['s_'].iloc[0] -= pc

In [68]: test['s_'].iloc[-1] -= pc

In [69]: test[['r', 's', 's_']].sum().apply(np.exp)
Out[69]: r    0.9345
         s    1.2431
         s_   1.2252
         dtype: float64

In [70]: test[['r', 's', 's_']].sum().apply(np.exp) - 1
Out[70]: r    -0.0655
         s     0.2431
         s_    0.2252
         dtype: float64

In [71]: test[['r', 's', 's_']].cumsum().apply(np.exp).plot(figsize=(10, 6));

1

修正平均买卖价差

2

计算平均比例交易成本

aiif 1007

图 10-7. 每日 DNN 策略在交易成本(样本外)前后的毛收益表现

基于深度神经网络的交易策略,无论是在典型的交易成本前后都显示出很大的潜力。然而,当观察到更多交易时,类似的策略在当天是否经济可行?接下来的部分将分析基于深度神经网络的当天交易策略。

回测基于当天的深度神经网络策略

要在当天数据上训练和回测 DNN 模型,需要另一个数据集:

In [72]: url = 'http://hilpisch.com/aiif_eikon_id_eur_usd.csv'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [73]: symbol = 'EUR='  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [74]: data = pd.DataFrame(pd.read_csv(url, index_col=0,
                             parse_dates=True).dropna()['CLOSE'])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         data.columns = [symbol]

In [75]: data = data.resample('5min', label='right').last().ffill()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [76]: data.info()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 26486 entries, 2019-10-01 00:05:00 to 2019-12-31 23:10:00
         Freq: 5T
         Data columns (total 1 columns):
          #   Column  Non-Null Count  Dtype
         ---  ------  --------------  -----
          0   EUR=    26486 non-null  float64
         dtypes: float64(1)
         memory usage: 413.8 KB

In [77]: lags = 5

In [78]: data, cols = add_lags(data, symbol, lags, window=20)

1

检索 EUR/USD 的当天数据并选择收盘价格

2

将数据重采样为五分钟柱

前一部分的过程现在可以使用新数据集重复。首先,训练深度神经网络模型:

In [79]: split = int(len(data) * 0.85)

In [80]: train = data.iloc[:split].copy()

In [81]: np.bincount(train['d'])
Out[81]: array([16284,  6207])

In [82]: def cw(df):
             c0, c1 = np.bincount(df['d'])
             w0 = (1 / c0) * (len(df)) / 2
             w1 = (1 / c1) * (len(df)) / 2
             return {0: w0, 1: w1}

In [83]: mu, std = train.mean(), train.std()

In [84]: train_ = (train - mu) / std

In [85]: set_seeds()
         model = create_model(hl=1, hu=128,
                              reg=True, dropout=False)

In [86]: %%time
         model.fit(train_[cols], train['d'],
                   epochs=40, verbose=False,
                   validation_split=0.2, shuffle=False,
                   class_weight=cw(train))
         CPU times: user 40.6 s, sys: 5.49 s, total: 46 s
         Wall time: 25.2 s

Out[86]: <keras.callbacks.callbacks.History at 0x7fc91a6b2a90>

In [87]: model.evaluate(train_[cols], train['d'])
         22491/22491 [==============================] - 0s 13us/step

Out[87]: [0.5218664327576152, 0.6729803085327148]

样本内,表现看起来很有前途,如图 10-8 所示:

In [88]: train['p'] = np.where(model.predict(train_[cols]) > 0.5, 1, -1)

In [89]: train['p'].value_counts()
Out[89]: -1    11519
          1    10972
         Name: p, dtype: int64

In [90]: train['s'] = train['p'] * train['r']

In [91]: train[['r', 's']].sum().apply(np.exp)
Out[91]: r   1.0223
         s   1.6665
         dtype: float64

In [92]: train[['r', 's']].sum().apply(np.exp) - 1
Out[92]: r   0.0223
         s   0.6665
         dtype: float64

In [93]: train[['r', 's']].cumsum().apply(np.exp).plot(figsize=(10, 6));

aiif 1008

图 10-8. 被动基准投资和 DNN 日内策略的总体表现(样本内)

样本外,在未考虑交易成本的情况下,该策略看起来也是具有前景的。该策略似乎系统性地优于被动基准投资(见图 10-9):

In [94]: test = data.iloc[split:].copy()

In [95]: test_ = (test - mu) / std

In [96]: model.evaluate(test_[cols], test['d'])
         3970/3970 [==============================] - 0s 19us/step

Out[96]: [0.5226116042706168, 0.668513834476471]

In [97]: test['p'] = np.where(model.predict(test_[cols]) > 0.5, 1, -1)

In [98]: test['p'].value_counts()
Out[98]: -1    2273
          1    1697
         Name: p, dtype: int64

In [99]: test['s'] = test['p'] * test['r']

In [100]: test[['r', 's']].sum().apply(np.exp)
Out[100]: r   1.0071
          s   1.0658
          dtype: float64

In [101]: test[['r', 's']].sum().apply(np.exp) - 1
Out[101]: r   0.0071
          s   0.0658
          dtype: float64

In [102]: test[['r', 's']].cumsum().apply(np.exp).plot(figsize=(10, 6));

在纯经济表现方面的最终检验是加入交易成本后。该策略在相对较短的时间内进行了数百次交易。正如下文分析所示,基于标准零售买卖价差,基于 DNN 的策略是不可行的。

aiif 1009

图 10-9. 被动基准投资和 DNN 日内策略的总体表现(样本外)

将交易价差降低到专业高交易量交易者可能达到的水平,该策略仍然无法达到收支平衡,而是大部分利润都损失给了交易成本(见图 10-10):

In [103]: sum(test['p'].diff() != 0)
Out[103]: 1303

In [104]: spread = 0.00012  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
          pc_1 = spread / test[symbol]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [105]: spread = 0.00006  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
          pc_2 = spread / test[symbol]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [106]: test['s_1'] = np.where(test['p'].diff() != 0,
                                 test['s'] - pc_1, test['s'])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [107]: test['s_1'].iloc[0] -= pc_1.iloc[0]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
          test['s_1'].iloc[-1] -= pc_1.iloc[0]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [108]: test['s_2'] = np.where(test['p'].diff() != 0,
                                 test['s'] - pc_2, test['s'])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [109]: test['s_2'].iloc[0] -= pc_2.iloc[0]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
          test['s_2'].iloc[-1] -= pc_2.iloc[0]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [110]: test[['r', 's', 's_1', 's_2']].sum().apply(np.exp)
Out[110]: r     1.0071
          s     1.0658
          s_1   0.9259
          s_2   0.9934
          dtype: float64

In [111]: test[['r', 's', 's_1', 's_2']].sum().apply(np.exp) - 1
Out[111]: r      0.0071
          s      0.0658
          s_1   -0.0741
          s_2   -0.0066
          dtype: float64

In [112]: test[['r', 's', 's_1', 's_2']].cumsum().apply(
              np.exp).plot(figsize=(10, 6), style=['-', '-', '--', '--']);

1

假设零售级别的买卖价差

2

假设专业水平的买卖价差

aiif 1010

图 10-10. 高/低交易成本前后 DNN 日内策略的总体表现(样本外)

日内交易

本章讨论的形式中的日内算法交易在统计学上常常看起来很有吸引力。在样本内和样本外,DNN 模型在预测市场方向时达到了高准确率。不考虑交易成本的情况下,与被动基准投资相比,DNN 策略在样本内和样本外表现显著优于被动基准投资。然而,一旦将交易成本考虑在内,DNN 策略的表现显著下降,使其对于典型的零售买卖价差来说不可行,并且对于较低但高交易量买卖价差也不甚有吸引力。

结论

向量化回测被证明是一种有效和有价值的方法,用于回测基于人工智能的算法交易策略的表现。本章首先基于使用两个简单的简单移动平均线(SMA)从信号中派生的简单示例来解释这种方法的基本思想。这允许对策略和结果位置进行简单可视化。然后,结合 EOD 数据,通过回测基于 DNN 的交易策略(详细讨论见第七章),在交易成本之前和之后,都显示出在统计上发现的统计效率转化为经济效率,这意味着盈利的交易策略。当使用相同的向量化回测方法处理分钟数据时,与被动基准投资相比,DNN 策略也显示出显著的内外样本超额表现。在回测中增加交易成本说明,这些成本必须非常低,通常甚至大型专业交易者也无法达到这一水平,才能使交易策略在经济上可行。

参考文献

本章引用的书籍和论文:

  • Gibbs Samuel. 2016. “伊隆·马斯克:特斯拉汽车将在两年内能够在美国无司机行驶。” 卫报。2016 年 1 月 11 日。https://oreil.ly/C508Q

  • Hilpisch, Yves. 2018. Python 金融分析:数据驱动金融的掌握. 第 2 版。Sebastopol:O’Reilly。

  • ⸻。2020. Python 量化交易:从概念到云端部署. Sebastopol:O’Reilly。

¹ 例如,这是Oanda向零售交易者提供的典型点差。

第十一章:风险管理

在大规模部署自动驾驶车辆(AVs)方面,安全保障是一个重大障碍。

Majid Khonji 等人(2019 年)

有了更好的预测,判断的价值也会提升。毕竟,如果不知道你有多喜欢保持干燥,或者有多讨厌带伞,知道下雨的可能性也没什么帮助。

Ajay Agrawal 等人(2018 年)

通常情况下,向量化回测能够就基于预测的算法交易策略的经济潜力进行评估(即在其纯粹形式下)。实际应用中的大多数 AI 代理不仅仅包括预测模型。例如,自动驾驶车辆(AVs)的 AI 不是独立存在的,而是带有大量规则和启发式方法,限制了 AI 能够采取或可以采取的行动。在 AVs 的背景下,这主要涉及管理风险,如因碰撞或坠毁而产生的风险。

在财务背景下,AI 代理或交易机器人通常也不是原样部署的。相反,通常会使用一些标准的风险措施,如(追踪)止损订单或盈利订单。其理由显而易见。在金融市场上放置方向性赌注时,要避免过大的损失。同样,一旦达到某个利润水平,就应通过提前平仓来保护成功。这类风险措施的处理方式往往取决于人类判断,可能会受到相关数据和统计分析的形式支持。在 Agrawal 等人(2018 年)的书中概念上,这是一个重要的讨论点:AI 提供了改进的预测,但人类判断仍然在设定决策规则和行动边界方面发挥作用。

本章有三个主要目的。首先,它以训练过的深度 Q 学习代理产生的向量化和事件驱动方式回测算法交易策略。因此,这样的代理被称为交易机器人。其次,它评估了在实施这些策略的金融工具上相关的风险。第三,它使用本章介绍的事件驱动方法回测典型的风险措施,如止损订单。与向量化回测相比,事件驱动回测的主要优势是在建模和分析决策规则及风险管理措施方面具有更高的灵活性。换句话说,它允许我们关注那些在向量化编程方法中被推到背景中的细节。

“交易机器人”介绍并训练了基于金融 Q 学习代理的交易机器人,源自第九章。“矢量化回测”使用来自第十章的矢量化回测来评估交易机器人的(纯)经济表现。事件驱动的回测在“基于事件的回测”中进行介绍。首先讨论基类,然后基于基类实现和进行交易机器人的回测。在此背景下,还参考 Hilpisch(2020 年,第六章)。“风险评估”分析了设置风险管理规则所需的选定统计指标,如 最大回撤平均真实波幅(ATR)。“回测风险指标”然后回测主要风险指标对交易机器人表现的影响。

交易机器人

本节介绍基于金融 Q 学习代理 FQLAgent 的交易机器人,源自第九章。这是随后分析的交易机器人。一如既往,我们首先引入所需的库:

In [1]: import os
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        pd.set_option('mode.chained_assignment', None)
        pd.set_option('display.float_format', '{:.4f}'.format)
        np.set_printoptions(suppress=True, precision=4)
        os.environ['PYTHONHASHSEED'] = '0'

“金融环境”展示了一个 Python 模块,其中包含用于接下来的 Finance 类。“交易机器人”提供了一个 Python 模块,其中包含用于绘制训练和验证结果的 TradingBot 类及其一些辅助函数。这两个类与第九章介绍的类非常接近,因此在此处使用它们而无需进一步解释。

下面的代码在历史每日收盘数据(EOD)上训练交易机器人,包括用于验证的数据子集。图 11-1 显示了不同训练周期的平均总奖励:

In [2]: import finance
        import tradingbot
        Using TensorFlow backend.

In [3]: symbol = 'EUR='
        features = [symbol, 'r', 's', 'm', 'v']

In [4]: a = 0
        b = 1750
        c = 250

In [5]: learn_env = finance.Finance(symbol, features, window=20, lags=3,
                         leverage=1, min_performance=0.9, min_accuracy=0.475,
                         start=a, end=a + b, mu=None, std=None)

In [6]: learn_env.data.info()
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 1750 entries, 2010-02-02 to 2017-01-12
        Data columns (total 6 columns):
         #   Column  Non-Null Count  Dtype
        ---  ------  --------------  -----
         0   EUR=    1750 non-null   float64
         1   r       1750 non-null   float64
         2   s       1750 non-null   float64
         3   m       1750 non-null   float64
         4   v       1750 non-null   float64
         5   d       1750 non-null   int64
        dtypes: float64(5), int64(1)
        memory usage: 95.7 KB

In [7]: valid_env = finance.Finance(symbol, features=learn_env.features,
                                    window=learn_env.window,
                                    lags=learn_env.lags,
                                    leverage=learn_env.leverage,
                                    min_performance=0.0, min_accuracy=0.0,
                                    start=a + b, end=a + b + c,
                                    mu=learn_env.mu, std=learn_env.std)

In [8]: valid_env.data.info()
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 250 entries, 2017-01-13 to 2018-01-10
        Data columns (total 6 columns):
         #   Column  Non-Null Count  Dtype
        ---  ------  --------------  -----
         0   EUR=    250 non-null    float64
         1   r       250 non-null    float64
         2   s       250 non-null    float64
         3   m       250 non-null    float64
         4   v       250 non-null    float64
         5   d       250 non-null    int64
        dtypes: float64(5), int64(1)
        memory usage: 13.7 KB

In [9]: tradingbot.set_seeds(100)
        agent = tradingbot.TradingBot(24, 0.001, learn_env, valid_env)

In [10]: episodes = 61

In [11]: %time agent.learn(episodes)
         =======================================================================
         episode: 10/61 | VALIDATION | treward:  247 | perf: 0.936 | eps: 0.95
         =======================================================================
         =======================================================================
         episode: 20/61 | VALIDATION | treward:  247 | perf: 0.897 | eps: 0.86
         =======================================================================
         =======================================================================
         episode: 30/61 | VALIDATION | treward:  247 | perf: 1.035 | eps: 0.78
         =======================================================================
         =======================================================================
         episode: 40/61 | VALIDATION | treward:  247 | perf: 0.935 | eps: 0.70
         =======================================================================
         =======================================================================
         episode: 50/61 | VALIDATION | treward:  247 | perf: 0.890 | eps: 0.64
         =======================================================================
         =======================================================================
         episode: 60/61 | VALIDATION | treward:  247 | perf: 0.998 | eps: 0.58
         =======================================================================
         episode: 61/61 | treward:   17 | perf: 0.979 | av: 475.1 | max: 1747
         CPU times: user 51.4 s, sys: 2.53 s, total: 53.9 s
         Wall time: 47 s

In [12]: tradingbot.plot_treward(agent)

aiif 1101

图 11-1. 每个训练周期的平均总奖励

图 11-2 比较了交易机器人在训练数据集上的总体表现——由于在利用和探索之间交替而表现出相当大的差异——以及在仅利用开发数据集上的表现:

In [13]: tradingbot.plot_performance(agent)

aiif 1102

图 11-2. 训练集和验证集的总体表现

训练后的交易机器人在接下来的部分中用于回测。

矢量化回测

矢量化回测不能直接应用于交易机器人。第十章使用密集神经网络(DNNs)来说明该方法。在这种情况下,首先准备带有特征和标签子集的数据,然后将其一次性输入到 DNN 中以生成所有预测值。在强化学习(RL)的背景下,通过与环境的交互动作和逐步生成和收集数据。

为此,以下 Python 代码定义了 backtest 函数,该函数以 TradingBot 实例和 Finance 实例作为输入。它在原始 DataFrame 对象中生成了 Finance 环境列的交易机器人持有的仓位和相应的策略表现:

In [14]: def reshape(s):
             return np.reshape(s, [1, learn_env.lags,
                                   learn_env.n_features])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [15]: def backtest(agent, env):
             env.min_accuracy = 0.0
             env.min_performance = 0.0
             done = False
             env.data['p'] = 0  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             state = env.reset()
             while not done:
                 action = np.argmax(
                     agent.model.predict(reshape(state))[0, 0])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 position = 1 if action == 1 else -1  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 env.data.loc[:, 'p'].iloc[env.bar] = position  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 state, reward, done, info = env.step(action)
             env.data['s'] = env.data['p'] * env.data['r'] * learn_env.leverage  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

1

重塑单个特征-标签组合

2

生成一个用于持仓价值的列

3

推导出给定经过训练的 DNN 的最佳动作(预测)

4

推导出结果仓位(长/向上为 +1,空/向下为 –1)…

5

…并将其存储在相应的索引位置的相应列中

6

根据持仓价值计算策略对数收益率

配备了 backtest 函数,向量化的回测归结为几行 Python 代码,就像第 10 章中的那样。

图 11-3 比较了被动基准投资的总体表现与策略的总体表现:

In [16]: env = agent.learn_env  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [17]: backtest(agent, env)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [18]: env.data['p'].iloc[env.lags:].value_counts()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[18]:  1    961
         -1    786
         Name: p, dtype: int64

In [19]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[19]: r   0.7725
         s   1.5155
         dtype: float64

In [20]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp) - 1  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[20]: r   -0.2275
         s    0.5155
         dtype: float64

In [21]: env.data[['r', 's']].iloc[env.lags:].cumsum(
                 ).apply(np.exp).plot(figsize=(10, 6));

1

指定相关环境

2

生成所需的额外数据

3

计算多头和空头仓位的数量

4

计算被动基准投资(r)和策略(s)的总体表现…

5

…以及相应的净表现

aiif 1103

图 11-3. 被动基准投资和交易机器人的总体表现(样本内)

为了更真实地了解交易机器人的表现,以下 Python 代码创建了一个测试环境,其中包含交易机器人尚未见过的数据。图 11-4 显示了与被动基准投资相比,交易机器人的表现如何:

In [22]: test_env = finance.Finance(symbol, features=learn_env.features,
                                    window=learn_env.window,
                                    lags=learn_env.lags,
                                    leverage=learn_env.leverage,
                                    min_performance=0.0, min_accuracy=0.0,
                                    start=a + b + c, end=None,
                                    mu=learn_env.mu, std=learn_env.std)

In [23]: env = test_env

In [24]: backtest(agent, env)

In [25]: env.data['p'].iloc[env.lags:].value_counts()
Out[25]: -1    437
          1     56
         Name: p, dtype: int64

In [26]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp)
Out[26]: r   0.9144
         s   1.0992
         dtype: float64

In [27]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp) - 1
Out[27]: r   -0.0856
         s    0.0992
         dtype: float64

In [28]: env.data[['r', 's']].iloc[env.lags:].cumsum(
                     ).apply(np.exp).plot(figsize=(10, 6));

aiif 1104

图 11-4. 被动基准投资和交易机器人的总体表现(样本外)

没有任何风险措施的样本外表现似乎已经很有希望。然而,为了能够正确地评估交易策略的实际表现,应该包括风险措施。这就是基于事件的回测发挥作用的地方。

基于事件的回测

鉴于前一节的结果,没有任何风险措施的样本外表现似乎已经很有希望。然而,为了能够正确地分析风险措施,如移动止损订单,需要进行 基于事件的回测。本节介绍了这种替代方法来评估算法交易策略的表现。

“Backtesting Base Class”介绍了BacktestingBase类,可以灵活用于测试不同类型的定向交易策略。代码中对重要行进行了详细注释。该基础类提供以下方法:

get_date_price()

对于给定的bar(包含财务数据的DataFrame对象的索引值),返回相关的dateprice

print_balance()

对于给定的bar,打印交易机器人当前(现金)余额。

calculate_net_wealth()

对于给定的price,返回由当前(现金)余额和仪器持仓组成的净财富。

print_net_wealth()

对于给定的bar,打印交易机器人的净财富。

place_buy_order()place_sell_order()

对于给定的bar和给定的units或给定的amount,这些方法会下买单或卖单,并相应调整相关数量(例如考虑交易成本)。

close_out()

在给定的bar,此方法关闭开放头寸并计算和报告绩效统计数据。

以下 Python 代码说明了BacktestingBase类的实例如何根据一些简单步骤运行:

In [29]: import backtesting as bt

In [30]: bb = bt.BacktestingBase(env=agent.learn_env, model=agent.model,
                                 amount=10000, ptc=0.0001, ftc=1.0,
                                 verbose=True)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [31]: bb.initial_amount  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[31]: 10000

In [32]: bar = 100  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [33]: bb.get_date_price(bar)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[33]: ('2010-06-25', 1.2374)

In [34]: bb.env.get_state(bar)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[34]:               EUR=       r       s       m      v
         Date
         2010-06-22 -0.0242 -0.5622 -0.0916 -0.2022 1.5316
         2010-06-23  0.0176  0.6940 -0.0939 -0.0915 1.5563
         2010-06-24  0.0354  0.3034 -0.0865  0.6391 1.0890

In [35]: bb.place_buy_order(bar, amount=5000)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         2010-06-25 | buy 4040 units for 1.2374
         2010-06-25 | current balance = 4999.40

In [36]: bb.print_net_wealth(2 * bar)  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
         2010-11-16 | net wealth = 10450.17

In [37]: bb.place_sell_order(2 * bar, units=1000)  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
         2010-11-16 | sell 1000 units for 1.3492
         2010-11-16 | current balance = 6347.47

In [38]: bb.close_out(3 * bar)  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
         ==================================================
         2011-04-11 | *** CLOSING OUT ***
         2011-04-11 | sell 3040 units for 1.4434
         2011-04-11 | current balance = 10733.97
         2011-04-11 | net performance [%] = 7.3397
         2011-04-11 | number of trades [#] = 3
         ==================================================

1

实例化BacktestingBase对象。

2

查找initial_amount属性值。

3

固定bar的值。

4

检索bardateprice值。

5

检索barFinance环境状态。

6

使用amount参数下买单。

7

在稍后的时间点打印净财富(2 * bar)。

8

使用units参数在稍后的时间点下卖单。

9

更晚时候关闭剩余的多头头寸(3 * bar)。

继承自BacktestingBase类,TBBacktester类为交易机器人实现了基于事件的回测:

In [39]: class TBBacktester(bt.BacktestingBase):
             def _reshape(self, state):
                 ''' Helper method to reshape state objects.
                 '''
                 return np.reshape(state, [1, self.env.lags, self.env.n_features])
             def backtest_strategy(self):
                 ''' Event-based backtesting of the trading bot's performance.
                 '''
                 self.units = 0
                 self.position = 0
                 self.trades = 0
                 self.current_balance = self.initial_amount
                 self.net_wealths = list()
                 for bar in range(self.env.lags, len(self.env.data)):
                     date, price = self.get_date_price(bar)
                     if self.trades == 0:
                         print(50 * '=')
                         print(f'{date} | *** START BACKTEST ***')
                         self.print_balance(bar)
                         print(50 * '=')
                     state = self.env.get_state(bar)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                     action = np.argmax(self.model.predict(
                                 self._reshape(state.values))[0, 0])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                     position = 1 if action == 1 else -1  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                     if self.position in [0, -1] and position == 1:  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                         if self.verbose:
                             print(50 * '-')
                             print(f'{date} | *** GOING LONG ***')
                         if self.position == -1:
                             self.place_buy_order(bar - 1, units=-self.units)
                         self.place_buy_order(bar - 1,
                                              amount=self.current_balance)
                         if self.verbose:
                             self.print_net_wealth(bar)
                         self.position = 1
                     elif self.position in [0, 1] and position == -1:  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                         if self.verbose:
                             print(50 * '-')
                             print(f'{date} | *** GOING SHORT ***')
                         if self.position == 1:
                             self.place_sell_order(bar - 1, units=self.units)
                         self.place_sell_order(bar - 1,
                                               amount=self.current_balance)
                         if self.verbose:
                             self.print_net_wealth(bar)
                         self.position = -1
                     self.net_wealths.append((date,
                                              self.calculate_net_wealth(price)))  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 self.net_wealths = pd.DataFrame(self.net_wealths,
                                                 columns=['date', 'net_wealth'])  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 self.net_wealths.set_index('date', inplace=True)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 self.net_wealths.index = pd.DatetimeIndex(
                                                 self.net_wealths.index)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                 self.close_out(bar)

1

检索Finance环境的状态。

2

给定状态和model对象,生成最佳操作(预测)。

3

根据最佳操作(预测)确定最佳位置(多头/空头)。

4

如果满足条件,则进入多头头寸。

5

如果满足条件,则进入空头头寸。

6

收集随时间变化的净财富值并转换为DataFrame对象。

鉴于FinanceTradingBot实例已经准备好,TBBacktester类的应用非常简单。以下代码首先在学习环境数据上对交易机器人进行回测,分别在没有和有交易成本的情况下。图 11-5 在时间上进行了视觉对比:

In [40]: env = learn_env

In [41]: tb = TBBacktester(env, agent.model, 10000,
                           0.0, 0, verbose=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [42]: tb.backtest_strategy()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         ==================================================
         2010-02-05 | *** START BACKTEST ***
         2010-02-05 | current balance = 10000.00
         ==================================================
         ==================================================
         2017-01-12 | *** CLOSING OUT ***
         2017-01-12 | current balance = 14601.85
         2017-01-12 | net performance [%] = 46.0185
         2017-01-12 | number of trades [#] = 828
         ==================================================

In [43]: tb_ = TBBacktester(env, agent.model, 10000,
                            0.00012, 0.0, verbose=False)

In [44]: tb_.backtest_strategy()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         ==================================================
         2010-02-05 | *** START BACKTEST ***
         2010-02-05 | current balance = 10000.00
         ==================================================
         ==================================================
         2017-01-12 | *** CLOSING OUT ***
         2017-01-12 | current balance = 13222.08
         2017-01-12 | net performance [%] = 32.2208
         2017-01-12 | number of trades [#] = 828
         ==================================================

In [45]: ax = tb.net_wealths.plot(figsize=(10, 6))
         tb_.net_wealths.columns = ['net_wealth (after tc)']
         tb_.net_wealths.plot(ax=ax);

1

基于事件的样本内回测 不带 交易成本

2

基于事件的样本内回测 交易成本

aiif 1105

Figure 11-5. 交易机器人在样本内交易前后的总体表现(毛利)

图 11-6 比较了测试环境数据中交易机器人的总体表现时间序列,再次在扣除交易成本前后进行比较:

In [46]: env = test_env

In [47]: tb = TBBacktester(env, agent.model, 10000,
                           0.0, 0, verbose=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [48]: tb.backtest_strategy()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10936.79
         2019-12-31 | net performance [%] = 9.3679
         2019-12-31 | number of trades [#] = 186
         ==================================================

In [49]: tb_ = TBBacktester(env, agent.model, 10000,
                            0.00012, 0.0, verbose=False)

In [50]: tb_.backtest_strategy()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10695.72
         2019-12-31 | net performance [%] = 6.9572
         2019-12-31 | number of trades [#] = 186
         ==================================================

In [51]: ax = tb.net_wealths.plot(figsize=(10, 6))
         tb_.net_wealths.columns = ['net_wealth (after tc)']
         tb_.net_wealths.plot(ax=ax);

1

基于事件的样本外回测 不带 交易成本

2

基于事件的样本外回测 交易成本

aiif 1106

Figure 11-6. 交易机器人在样本外交易前后的总体表现(毛利)

基于事件的样本内回测与向量化回测在扣除交易成本前的表现有何不同?图 11-7 显示了随时间推移归一化的净财富相对总体表现的对比情况。由于采用了不同的技术方法,两个时间序列并不完全相同,但非常相似。主要的性能差异主要可以通过基于事件的回测假设每个仓位的金额相同来解释。向量化回测考虑了复利效应,导致报告的性能略高:

In [52]: ax = (tb.net_wealths / tb.net_wealths.iloc[0]).plot(figsize=(10, 6))
         tp = env.data[['r', 's']].iloc[env.lags:].cumsum().apply(np.exp)
         (tp / tp.iloc[0]).plot(ax=ax);

aiif 1107

Figure 11-7. 被动基准投资和交易机器人的总体表现(向量化和基于事件的回测)

性能差异

向量化和基于事件的回测的表现数据相近但并非完全相同。在第一种情况下,假设金融工具是完全可分的。同时还进行了持续复利。而在后一种情况下,只接受完整单位的金融工具进行交易,这更接近实际情况。净财富计算基于价格差异。例如,基于事件的代码并不会检查当前余额是否足以通过现金支付来进行某项交易,这是一个简化的假设。例如,不一定总是可以买入保证金。在BacktestingBase类中很容易添加这方面的代码调整。

风险评估

实施风险措施需要理解交易所选择金融工具的风险。因此,正确设置风险措施参数(如止损订单)之前,需要对基础工具的风险进行评估。有许多方法可用于测量金融工具的风险。例如,有非定向风险度量,如波动率或平均真实范围(ATR)。还有定向度量,如最大回撤或风险值(VaR)。

设置止损(SL)、移动止损(TSL)或止盈订单(TP)的目标水平时,常见做法是将这些水平与 ATR 值关联起来。¹ 以下 Python 代码计算了在训练和回测交易机器人的金融工具上的 ATR 绝对值和相对值(即 EUR/USD 汇率)。这些计算依赖于学习环境的数据,并使用典型的 14 天(柱)窗口长度。图 11-8 显示了这些计算值,随时间显著变化:

In [53]: data = pd.DataFrame(learn_env.data[symbol])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [54]: data.head()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[54]:              EUR=
         Date
         2010-02-02 1.3961
         2010-02-03 1.3898
         2010-02-04 1.3734
         2010-02-05 1.3662
         2010-02-08 1.3652

In [55]: window = 14  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [56]: data['min'] = data[symbol].rolling(window).min()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [57]: data['max'] = data[symbol].rolling(window).max()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [58]: data['mami'] = data['max'] - data['min']  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [59]: data['mac'] = abs(data['max'] - data[symbol].shift(1))  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [60]: data['mic'] = abs(data['min'] - data[symbol].shift(1))  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)

In [61]: data['atr'] = np.maximum(data['mami'], data['mac'])  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)

In [62]: data['atr'] = np.maximum(data['atr'], data['mic'])  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)

In [63]: data['atr%'] = data['atr'] / data[symbol]  ![10](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/10.png)

In [64]: data[['atr', 'atr%']].plot(subplots=True, figsize=(10, 6));

1

原始DataFrame对象中的工具价格列

2

用于计算的窗口长度

3

滚动最小值

4

滚动最大值

5

滚动最大值与最小值的差值

6

滚动最大值与前一天价格的绝对差值

7

滚动最小值与前一天价格的绝对差值

8

最大的最大-最小差值与最大-价格差值

9

前一个最大值与最小价格差的最大值(= ATR)

10

从 ATR 绝对值和价格计算的 ATR 值百分比

aiif 1108

图 11-8. 平均真实范围(ATR)的绝对(价格)和相对(%)值

以下代码显示了 ATR 的最终数值,包括绝对值和相对值。一个典型的规则可能是设置止损(SL)水平为入场价格减去x倍的 ATR。根据交易者或投资者的风险偏好,x可能小于 1 或大于 1。这就是人类判断或正式风险策略发挥作用的地方。如果x = 1,那么 SL 水平将设置在入场水平以下约 2%处:

In [65]: data[['atr', 'atr%']].tail()
Out[65]:               atr   atr%
         Date
         2017-01-06 0.0218 0.0207
         2017-01-09 0.0218 0.0206
         2017-01-10 0.0218 0.0207
         2017-01-11 0.0199 0.0188
         2017-01-12 0.0206 0.0194

然而,在此背景下,杠杆扮演了重要角色。例如,如果使用杠杆为 10,这在外汇交易中实际上是相当低的水平,那么 ATR 数值需要乘以杠杆。因此,对于假定的 ATR 因子为 1,之前的 SL 水平现在应该设置为约 20%而不仅仅是 2%。或者,当从整个数据集中取中位数的 ATR 值时,应将其设置为约 25%:

In [66]: leverage = 10

In [67]: data[['atr', 'atr%']].tail() * leverage
Out[67]:               atr   atr%
         Date
         2017-01-06 0.2180 0.2070
         2017-01-09 0.2180 0.2062
         2017-01-10 0.2180 0.2066
         2017-01-11 0.1990 0.1881
         2017-01-12 0.2060 0.1942

In [68]: data[['atr', 'atr%']].median() * leverage
Out[68]: atr    0.3180
         atr%   0.2481
         dtype: float64

将 SL 或 TP 水平与 ATR 相关联的基本思想是,应避免将它们设置得太低或太高。考虑一个杠杆为 10 倍的头寸,ATR 为 20%的情况。将 SL 水平仅设置为 3%或 5%,可能会减少头寸的财务风险,但会引入过早触发的止损风险,这是由于金融工具的典型波动。在某些范围内的这些“典型波动”通常被称为噪音。一般来说,SL 指令应保护免受大于典型价格波动(噪音)的不利市场波动影响。

对于止盈水平也是如此。如果设置得太高,比如设为三倍的 ATR 水平,可能无法确保获得可观的利润,而头寸可能会保持开放太久,直到之前的利润被吞噬。即使在此背景下可以使用正式分析和数学公式,但设置这些目标水平涉及到更多的艺术而非科学。在金融背景下,设置这些目标水平有相当大的自由度,可以依靠人类判断来解决。在其他情况下,例如对自动驾驶车辆,情况就不同了,因为无需人类判断指导 AI 避免与人类碰撞。

非正态性和非线性

保证金止损在投资者的资金或已投资的权益被用光时,会平仓。假设一个杠杆交易中有保证金止损的设定。例如,当杠杆为 10 倍时,保证金为 10%的权益。交易工具出现不利的波动,如 10%或更大,会消耗所有权益并触发平仓操作——损失 100%的权益。而若交易工具出现有利的波动,如下涨 25%,则会带来 150%的权益回报。即使交易工具的回报通常服从正态分布,杠杆和保证金止损会导致回报非正态分布,以及交易工具与交易头寸之间的非对称、非线性关系。

回测风险度量

对于金融工具的 ATR 有一个概念往往是实施风险措施的良好起点。为了能够正确地回测典型风险管理订单的效果,对BacktestingBase类进行了一些调整是有帮助的。以下 Python 代码展示了一个新的基类—BacktestBaseRM,它继承自BacktestingBase—用于跟踪前一次交易的入场价格以及自那次交易以来的最高和最低价格。这些数值用于计算事件驱动回测期间的相关绩效指标,与 SL、TSL 和 TP 订单相关:

#
# Event-Based Backtesting
# --Base Class (2)
#
# (c) Dr. Yves J. Hilpisch
#
from backtesting import *

class BacktestingBaseRM(BacktestingBase):

    def set_prices(self, price):
        ''' Sets prices for tracking of performance.
            To test for e.g. trailing stop loss hit.
        '''
        self.entry_price = price  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        self.min_price = price  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        self.max_price = price  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

    def place_buy_order(self, bar, amount=None, units=None, gprice=None):
        ''' Places a buy order for a given bar and for
            a given amount or number of units.
        '''
        date, price = self.get_date_price(bar)
        if gprice is not None:
            price = gprice
        if units is None:
            units = int(amount / price)
        self.current_balance -= (1 + self.ptc) * units * price + self.ftc
        self.units += units
        self.trades += 1
        self.set_prices(price)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
        if self.verbose:
            print(f'{date} | buy {units} units for {price:.4f}')
            self.print_balance(bar)

    def place_sell_order(self, bar, amount=None, units=None, gprice=None):
        ''' Places a sell order for a given bar and for
            a given amount or number of units.
        '''
        date, price = self.get_date_price(bar)
        if gprice is not None:
            price = gprice
        if units is None:
            units = int(amount / price)
        self.current_balance += (1 - self.ptc) * units * price - self.ftc
        self.units -= units
        self.trades += 1
        self.set_prices(price)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
        if self.verbose:
            print(f'{date} | sell {units} units for {price:.4f}')
            self.print_balance(bar)

1

设置最近交易的入场价格

2

设置自最近交易以来的最低价格

3

设置自最近交易以来的最高价格

4

设置执行交易后的相关价格

基于这个新的基类,“回测类” 提供了一个新的回测类,TBBacktesterRM,允许包含 SL、TSL 和 TP 订单。相关的代码部分将在以下子章节中讨论。在前一节中计算的大约为 2%的 ATR 水平上,回测示例的参数设置大致为此。

EUT 和风险测量

EUT、MVP 和 CAPM(见第三章和第四章)假设金融代理人了解金融工具收益的未来分布。MPT 和 CAPM 进一步假设收益呈正态分布,并且市场组合的收益与交易金融工具的收益之间存在线性关系。使用 SL、TSL 和 TP 订单以及与保证金止损组合使用的杠杆会导致“保证非正态”分布,并且导致与交易工具相关的非对称、非线性收益。

止损

第一个风险测量是 SL 订单。它固定了一个特定的价格水平或者更常见的是一个固定百分比值,当未杠杆头寸的入场价格为 100 时,SL 水平设置为 5%,那么长头寸在 95 时关闭,而短头寸在 105 时关闭。

以下 Python 代码是TBBacktesterRM类的相关部分,处理 SL 订单。对于 SL 订单,该类允许指定订单的价格水平是否保证。使用保证的 SL 价格水平可能导致过于乐观的绩效结果:²

# stop loss order
if sl is not None and self.position != 0:  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
    rc = (price - self.entry_price) / self.entry_price  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
    if self.position == 1 and rc < -self.sl:  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        print(50 * '-')
        if guarantee:
            price = self.entry_price * (1 - self.sl)
            print(f'*** STOP LOSS (LONG  | {-self.sl:.4f}) ***')
        else:
            print(f'*** STOP LOSS (LONG  | {rc:.4f}) ***')
        self.place_sell_order(bar, units=self.units, gprice=price)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
        self.wait = wait  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
        self.position = 0   ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
    elif self.position == -1 and rc > self.sl:  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
        print(50 * '-')
        if guarantee:
            price = self.entry_price * (1 + self.sl)
            print(f'*** STOP LOSS (SHORT | -{self.sl:.4f}) ***')
        else:
            print(f'*** STOP LOSS (SHORT | -{rc:.4f}) ***')
        self.place_buy_order(bar, units=-self.units, gprice=price)  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
        self.wait = wait  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
        self.position = 0  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

1

检查是否定义了 SL 以及仓位是否非中性

2

基于最后交易的入场价格计算表现

3

检查多头头寸是否给出了止损(SL)事件

4

关闭多头头寸,无论是当前价格还是保证价格水平

5

设置在下一笔交易发生之前等待的条数为wait

6

将头寸设置为中性

7

检查空头头寸是否给出了止损(SL)事件

8

关闭空头头寸,无论是当前价格还是保证价格水平

下面的 Python 代码对交易机器人的交易策略进行了回测,既不使用 SL 订单也不使用 SL 订单。对于给定的参数化,SL 订单对策略表现产生了负面影响:

In [69]: import tbbacktesterrm as tbbrm

In [70]: env = test_env

In [71]: tb = tbbrm.TBBacktesterRM(env, agent.model, 10000,
                                   0.0, 0, verbose=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [72]: tb.backtest_strategy(sl=None, tsl=None, tp=None, wait=5)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10936.79
         2019-12-31 | net performance [%] = 9.3679
         2019-12-31 | number of trades [#] = 186
         ==================================================

In [73]: tb.backtest_strategy(sl=0.0175, tsl=None, tp=None,
                              wait=5, guarantee=False)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         --------------------------------------------------
         *** STOP LOSS (SHORT | -0.0203) ***
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10717.32
         2019-12-31 | net performance [%] = 7.1732
         2019-12-31 | number of trades [#] = 188
         ==================================================

In [74]: tb.backtest_strategy(sl=0.017, tsl=None, tp=None,
                              wait=5, guarantee=True)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         --------------------------------------------------
         *** STOP LOSS (SHORT | -0.0170) ***
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10753.52
         2019-12-31 | net performance [%] = 7.5352
         2019-12-31 | number of trades [#] = 188
         ==================================================

1

实例化用于风险管理的回测类

2

对交易机器人的性能进行回测,不使用任何风险度量

3

对交易机器人的性能进行回测,使用 SL 订单(无保证)

4

对交易机器人的性能进行回测,使用带有保证的 SL 订单

滑动止损

与常规止损单相比,每当基础订单下达后观察到新高时,TSL 订单会进行调整。假设无杠杆长头寸的基础订单入场价格为 95,TSL 设置为 5%。如果工具价格达到 100 并回落至 95,则意味着 TSL 事件,头寸在入场价格水平关闭。如果价格达到 110 并回落至 104.5,则会再次触发 TSL 事件。

下面的 Python 代码是TBBacktesterRM类的相关部分,用于处理 TSL 订单。为了正确处理这样的订单,需要跟踪最高价格(高点)和最低价格(低点)。最高价格适用于多头头寸,而最低价格适用于空头头寸:

# trailing stop loss order
if tsl is not None and self.position != 0:
    self.max_price = max(self.max_price, price)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
    self.min_price = min(self.min_price, price)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
    rc_1 = (price - self.max_price) / self.entry_price  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
    rc_2 = (self.min_price - price) / self.entry_price  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
    if self.position == 1 and rc_1 < -self.tsl:  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
        print(50 * '-')
        print(f'*** TRAILING SL (LONG  | {rc_1:.4f}) ***')
        self.place_sell_order(bar, units=self.units)
        self.wait = wait
        self.position = 0
    elif self.position == -1 and rc_2 < -self.tsl:  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
        print(50 * '-')
        print(f'*** TRAILING SL (SHORT | {rc_2:.4f}) ***')
        self.place_buy_order(bar, units=-self.units)
        self.wait = wait
        self.position = 0

1

如有必要更新最高价格

2

如有必要更新最低价格

3

计算多头头寸的相关表现

4

计算空头头寸的相关表现

5

检查多头头寸是否给出了 TSL 事件

6

检查空头头寸是否给出了 TSL 事件

正如接下来展示的回测结果所示,使用给定参数化的 TSL 订单相比未设置 TSL 订单,显著降低了总体表现:

In [75]: tb.backtest_strategy(sl=None, tsl=0.015,
                              tp=None, wait=5)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0152) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0169) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0164) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0191) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0166) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0194) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0172) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0181) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0153) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0160) ***
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10577.93
         2019-12-31 | net performance [%] = 5.7793
         2019-12-31 | number of trades [#] = 201
         ==================================================

1

使用 TSL 订单回测交易机器人的表现

止盈

最后,有 TP 订单。 TP 订单关闭了达到一定利润水平的头寸。 假设无杠杆的多头头寸以 100 的价格开仓,TP 订单设置为 5%的水平。 如果价格达到 105,位置将关闭。

TBBacktesterRM类的以下代码最终显示了处理 TP 订单的部分。 鉴于 SL 和 TSL 订单代码的参考,TP 实施是直接的。 对于 TP 订单,还可以选择根据相关的高/低价格水平进行回测,这很可能会导致过于乐观的性能值:³

# take profit order
if tp is not None and self.position != 0:
    rc = (price - self.entry_price) / self.entry_price
    if self.position == 1 and rc > self.tp:
        print(50 * '-')
        if guarantee:
            price = self.entry_price * (1 + self.tp)
            print(f'*** TAKE PROFIT (LONG  | {self.tp:.4f}) ***')
        else:
            print(f'*** TAKE PROFIT (LONG  | {rc:.4f}) ***')
        self.place_sell_order(bar, units=self.units, gprice=price)
        self.wait = wait
        self.position = 0
    elif self.position == -1 and rc < -self.tp:
        print(50 * '-')
        if guarantee:
            price = self.entry_price * (1 - self.tp)
            print(f'*** TAKE PROFIT (SHORT | {self.tp:.4f}) ***')
        else:
            print(f'*** TAKE PROFIT (SHORT | {-rc:.4f}) ***')
        self.place_buy_order(bar, units=-self.units, gprice=price)
        self.wait = wait
        self.position = 0

对于给定的参数化,添加 TP 订单——没有保证——显著提高了与被动基准投资相比的交易机器人表现。 这一结果可能过于乐观,考虑到前面的考虑因素。 因此,在本例中,具有保证的 TP 订单导致了更现实的性能值:

In [76]: tb.backtest_strategy(sl=None, tsl=None, tp=0.015,
                              wait=5, guarantee=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0155) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0155) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0204) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0240) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0168) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0156) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0183) ***
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 11210.33
         2019-12-31 | net performance [%] = 12.1033
         2019-12-31 | number of trades [#] = 198
         ==================================================

In [77]: tb.backtest_strategy(sl=None, tsl=None, tp=0.015,
                              wait=5, guarantee=True)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0150) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0150) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0150) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0150) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0150) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0150) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0150) ***
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10980.86
         2019-12-31 | net performance [%] = 9.8086
         2019-12-31 | number of trades [#] = 198
         ==================================================

1

使用 TP 订单(保证)回测交易机器人的表现

2

使用 TP 订单(保证)回测交易机器人的表现

当然,SL / TSL 订单也可以与 TP 订单结合使用。 下面的 Python 代码的回测结果在这两种情况下都比没有风险措施的策略要差。 在管理风险方面,几乎没有免费的午餐:

In [78]: tb.backtest_strategy(sl=0.015, tsl=None,
                              tp=0.0185, wait=5)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         --------------------------------------------------
         *** STOP LOSS (SHORT | -0.0203) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0202) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0213) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0240) ***
         --------------------------------------------------
         *** STOP LOSS (SHORT | -0.0171) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0188) ***
         --------------------------------------------------
         *** STOP LOSS (SHORT | -0.0153) ***
         --------------------------------------------------
         *** STOP LOSS (SHORT | -0.0154) ***
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10552.00
         2019-12-31 | net performance [%] = 5.5200
         2019-12-31 | number of trades [#] = 201
         ==================================================

In [79]: tb.backtest_strategy(sl=None, tsl=0.02,
                              tp=0.02, wait=5)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         ==================================================
         2018-01-17 | *** START BACKTEST ***
         2018-01-17 | current balance = 10000.00
         ==================================================
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0235) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0202) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0250) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0227) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0240) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0216) ***
         --------------------------------------------------
         *** TAKE PROFIT (SHORT | 0.0241) ***
         --------------------------------------------------
         *** TRAILING SL (SHORT | -0.0206) ***
         ==================================================
         2019-12-31 | *** CLOSING OUT ***
         2019-12-31 | current balance = 10346.38
         2019-12-31 | net performance [%] = 3.4638
         2019-12-31 | number of trades [#] = 198
         ==================================================

1

使用 SL 和 TP 订单回测交易机器人的表现

2

使用 TSL 和 TP 订单回测交易机器人的表现

性能影响

风险措施具有其理由和好处。 然而,减少风险可能会以降低整体表现为代价。 另一方面,使用 TP 订单的回测示例显示了可以通过某个金融工具的 ATR 来考虑某个利润水平足够实现利润的事实来解释的性能改善。 任何希望看到更高利润的希望通常会因市场再次反转而破灭。

结论

本章有三个主题。 它在向量化和事件驱动的方式下样本外回测交易机器人(即经过训练的深度 Q 学习代理)的表现。 它还评估了以平均真实范围(ATR)指标形式衡量感兴趣的金融工具价格的典型变化的风险。 最后,本章讨论并回测了以停损(SL)、跟踪停损(TSL)和止盈(TP)订单形式的事件驱动典型风险措施。

类似于自主车辆(AVs),交易机器人很少仅基于其人工智能的预测而部署。为了避免较大的下行风险并提高(风险调整后的)性能,风险度量通常发挥作用。标准的风险度量,如本章所讨论的,几乎在每个交易平台上都可以找到,也适用于零售交易者。下一章将在 Oanda 交易平台的背景下说明这一点。基于事件的回测方法提供了适当回测这些风险度量效果的算法灵活性。尽管“降低风险”听起来很吸引人,但回测结果表明,降低风险通常是有成本的:与没有任何风险度量的纯策略相比,性能可能较低。然而,当精细调整时,结果还显示,例如 TP 订单也可以对性能产生积极影响。

参考文献

本章引用的书籍和论文:

  • Agrawal, Ajay, Joshua Gans, 和 Avi Goldfarb. 2018. 预测机器:人工智能的简单经济学. 波士顿:哈佛商业评论出版社。

  • Hilpisch, Yves. 2020. Python 量化交易:从构想到云部署. Sebastopol: O’Reilly。

  • Khonji, Majid, Jorge Dias, 和 Lakmal Seneviratne. 2019. “自主车辆的风险感知推理。” arXiv. 2019 年 10 月 6 日。https://oreil.ly/2Z6WR

Python 代码

财务环境(Finance Environment)

以下是财务(Finance)环境类的 Python 模块:

#
# Finance Environment
#
# (c) Dr. Yves J. Hilpisch
# Artificial Intelligence in Finance
#
import math
import random
import numpy as np
import pandas as pd

class observation_space:
    def __init__(self, n):
        self.shape = (n,)

class action_space:
    def __init__(self, n):
        self.n = n

    def sample(self):
        return random.randint(0, self.n - 1)

class Finance:
    intraday = False
    if intraday:
        url = 'http://hilpisch.com/aiif_eikon_id_eur_usd.csv'
    else:
        url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'

    def __init__(self, symbol, features, window, lags,
                 leverage=1, min_performance=0.85, min_accuracy=0.5,
                 start=0, end=None, mu=None, std=None):
        self.symbol = symbol
        self.features = features
        self.n_features = len(features)
        self.window = window
        self.lags = lags
        self.leverage = leverage
        self.min_performance = min_performance
        self.min_accuracy = min_accuracy
        self.start = start
        self.end = end
        self.mu = mu
        self.std = std
        self.observation_space = observation_space(self.lags)
        self.action_space = action_space(2)
        self._get_data()
        self._prepare_data()

    def _get_data(self):
        self.raw = pd.read_csv(self.url, index_col=0,
                               parse_dates=True).dropna()
        if self.intraday:
            self.raw = self.raw.resample('30min', label='right').last()
            self.raw = pd.DataFrame(self.raw['CLOSE'])
            self.raw.columns = [self.symbol]

    def _prepare_data(self):
        self.data = pd.DataFrame(self.raw[self.symbol])
        self.data = self.data.iloc[self.start:]
        self.data['r'] = np.log(self.data / self.data.shift(1))
        self.data.dropna(inplace=True)
        self.data['s'] = self.data[self.symbol].rolling(self.window).mean()
        self.data['m'] = self.data['r'].rolling(self.window).mean()
        self.data['v'] = self.data['r'].rolling(self.window).std()
        self.data.dropna(inplace=True)
        if self.mu is None:
            self.mu = self.data.mean()
            self.std = self.data.std()
        self.data_ = (self.data - self.mu) / self.std
        self.data['d'] = np.where(self.data['r'] > 0, 1, 0)
        self.data['d'] = self.data['d'].astype(int)
        if self.end is not None:
            self.data = self.data.iloc[:self.end - self.start]
            self.data_ = self.data_.iloc[:self.end - self.start]

    def _get_state(self):
        return self.data_[self.features].iloc[self.bar -
                                              self.lags:self.bar]

    def get_state(self, bar):
        return self.data_[self.features].iloc[bar - self.lags:bar]

    def seed(self, seed):
        random.seed(seed)
        np.random.seed(seed)

    def reset(self):
        self.treward = 0
        self.accuracy = 0
        self.performance = 1
        self.bar = self.lags
        state = self.data_[self.features].iloc[self.bar -
                                               self.lags:self.bar]
        return state.values

    def step(self, action):
        correct = action == self.data['d'].iloc[self.bar]
        ret = self.data['r'].iloc[self.bar] * self.leverage
        reward_1 = 1 if correct else 0
        reward_2 = abs(ret) if correct else -abs(ret)
        self.treward += reward_1
        self.bar += 1
        self.accuracy = self.treward / (self.bar - self.lags)
        self.performance *= math.exp(reward_2)
        if self.bar >= len(self.data):
            done = True
        elif reward_1 == 1:
            done = False
        elif (self.performance < self.min_performance and
              self.bar > self.lags + 15):
            done = True
        elif (self.accuracy < self.min_accuracy and
              self.bar > self.lags + 15):
            done = True
        else:
            done = False
        state = self._get_state()
        info = {}
        return state.values, reward_1 + reward_2 * 5, done, info

交易机器人

以下是基于金融 Q-learning 代理的TradingBot类的 Python 模块:

#
# Financial Q-Learning Agent
#
# (c) Dr. Yves J. Hilpisch
# Artificial Intelligence in Finance
#
import os
import random
import numpy as np
from pylab import plt, mpl
from collections import deque
import tensorflow as tf
from keras.layers import Dense, Dropout
from keras.models import Sequential
from keras.optimizers import Adam, RMSprop

os.environ['PYTHONHASHSEED'] = '0'
plt.style.use('seaborn')
mpl.rcParams['savefig.dpi'] = 300
mpl.rcParams['font.family'] = 'serif'

def set_seeds(seed=100):
    ''' Function to set seeds for all
 random number generators.
 '''
    random.seed(seed)
    np.random.seed(seed)
    tf.random.set_seed(seed)

class TradingBot:
    def __init__(self, hidden_units, learning_rate, learn_env,
                 valid_env=None, val=True, dropout=False):
        self.learn_env = learn_env
        self.valid_env = valid_env
        self.val = val
        self.epsilon = 1.0
        self.epsilon_min = 0.1
        self.epsilon_decay = 0.99
        self.learning_rate = learning_rate
        self.gamma = 0.5
        self.batch_size = 128
        self.max_treward = 0
        self.averages = list()
        self.trewards = []
        self.performances = list()
        self.aperformances = list()
        self.vperformances = list()
        self.memory = deque(maxlen=2000)
        self.model = self._build_model(hidden_units,
                             learning_rate, dropout)

    def _build_model(self, hu, lr, dropout):
        ''' Method to create the DNN model.
 '''
        model = Sequential()
        model.add(Dense(hu, input_shape=(
            self.learn_env.lags, self.learn_env.n_features),
            activation='relu'))
        if dropout:
            model.add(Dropout(0.3, seed=100))
        model.add(Dense(hu, activation='relu'))
        if dropout:
            model.add(Dropout(0.3, seed=100))
        model.add(Dense(2, activation='linear'))
        model.compile(
            loss='mse',
            optimizer=RMSprop(lr=lr)
        )
        return model

    def act(self, state):
        ''' Method for taking action based on
 a) exploration
 b) exploitation
 '''
        if random.random() <= self.epsilon:
            return self.learn_env.action_space.sample()
        action = self.model.predict(state)[0, 0]
        return np.argmax(action)

    def replay(self):
        ''' Method to retrain the DNN model based on
 batches of memorized experiences.
 '''
        batch = random.sample(self.memory, self.batch_size)
        for state, action, reward, next_state, done in batch:
            if not done:
                reward += self.gamma * np.amax(
                    self.model.predict(next_state)[0, 0])
            target = self.model.predict(state)
            target[0, 0, action] = reward
            self.model.fit(state, target, epochs=1,
                           verbose=False)
        if self.epsilon > self.epsilon_min:
            self.epsilon *= self.epsilon_decay

    def learn(self, episodes):
        ''' Method to train the DQL agent.
 '''
        for e in range(1, episodes + 1):
            state = self.learn_env.reset()
            state = np.reshape(state, [1, self.learn_env.lags,
                                       self.learn_env.n_features])
            for _ in range(10000):
                action = self.act(state)
                next_state, reward, done, info = self.learn_env.step(action)
                next_state = np.reshape(next_state,
                                        [1, self.learn_env.lags,
                                         self.learn_env.n_features])
                self.memory.append([state, action, reward,
                                    next_state, done])
                state = next_state
                if done:
                    treward = _ + 1
                    self.trewards.append(treward)
                    av = sum(self.trewards[-25:]) / 25
                    perf = self.learn_env.performance
                    self.averages.append(av)
                    self.performances.append(perf)
                    self.aperformances.append(
                        sum(self.performances[-25:]) / 25)
                    self.max_treward = max(self.max_treward, treward)
                    templ = 'episode: {:2d}/{} | treward: {:4d} | '
                    templ += 'perf: {:5.3f} | av: {:5.1f} | max: {:4d}'
                    print(templ.format(e, episodes, treward, perf,
                                       av, self.max_treward), end='\r')
                    break
            if self.val:
                self.validate(e, episodes)
            if len(self.memory) > self.batch_size:
                self.replay()
        print()

    def validate(self, e, episodes):
        ''' Method to validate the performance of the
 DQL agent.
 '''
        state = self.valid_env.reset()
        state = np.reshape(state, [1, self.valid_env.lags,
                                   self.valid_env.n_features])
        for _ in range(10000):
            action = np.argmax(self.model.predict(state)[0, 0])
            next_state, reward, done, info = self.valid_env.step(action)
            state = np.reshape(next_state, [1, self.valid_env.lags,
                                            self.valid_env.n_features])
            if done:
                treward = _ + 1
                perf = self.valid_env.performance
                self.vperformances.append(perf)
                if e % int(episodes / 6) == 0:
                    templ = 71 * '='
                    templ += '\nepisode: {:2d}/{} | VALIDATION | '
                    templ += 'treward: {:4d} | perf: {:5.3f} | eps: {:.2f}\n'
                    templ += 71 * '='
                    print(templ.format(e, episodes, treward,
                                       perf, self.epsilon))
                break

def plot_treward(agent):
    ''' Function to plot the total reward
 per training episode.
 '''
    plt.figure(figsize=(10, 6))
    x = range(1, len(agent.averages) + 1)
    y = np.polyval(np.polyfit(x, agent.averages, deg=3), x)
    plt.plot(x, agent.averages, label='moving average')
    plt.plot(x, y, 'r--', label='regression')
    plt.xlabel('episodes')
    plt.ylabel('total reward')
    plt.legend()

def plot_performance(agent):
    ''' Function to plot the financial gross
 performance per training episode.
 '''
    plt.figure(figsize=(10, 6))
    x = range(1, len(agent.performances) + 1)
    y = np.polyval(np.polyfit(x, agent.performances, deg=3), x)
    plt.plot(x, agent.performances[:], label='training')
    plt.plot(x, y, 'r--', label='regression (train)')
    if agent.val:
        y_ = np.polyval(np.polyfit(x, agent.vperformances, deg=3), x)
        plt.plot(x, agent.vperformances[:], label='validation')
        plt.plot(x, y_, 'r-.', label='regression (valid)')
    plt.xlabel('episodes')
    plt.ylabel('gross performance')
    plt.legend()

回测基类(Backtesting Base Class)

以下是基于事件回测的BacktestingBase类的 Python 模块:

#
# Event-Based Backtesting
# --Base Class (1)
#
# (c) Dr. Yves J. Hilpisch
# Artificial Intelligence in Finance
#

class BacktestingBase:
    def __init__(self, env, model, amount, ptc, ftc, verbose=False):
        self.env = env  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        self.model = model  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        self.initial_amount = amount  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        self.current_balance = amount  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        self.ptc = ptc   ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
        self.ftc = ftc   ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
        self.verbose = verbose  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
        self.units = 0  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
        self.trades = 0  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)

    def get_date_price(self, bar):
        ''' Returns date and price for a given bar.
        '''
        date = str(self.env.data.index[bar])[:10]  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
        price = self.env.data[self.env.symbol].iloc[bar]  ![10](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/10.png)
        return date, price

    def print_balance(self, bar):
        ''' Prints the current cash balance for a given bar.
        '''
        date, price = self.get_date_price(bar)
        print(f'{date} | current balance = {self.current_balance:.2f}')  ![11](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/11.png)

    def calculate_net_wealth(self, price):
        return self.current_balance + self.units * price  ![12](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/12.png)

    def print_net_wealth(self, bar):
        ''' Prints the net wealth for a given bar
            (cash + position).
        '''
        date, price = self.get_date_price(bar)
        net_wealth = self.calculate_net_wealth(price)
        print(f'{date} | net wealth = {net_wealth:.2f}')  ![13](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/13.png)

    def place_buy_order(self, bar, amount=None, units=None):
        ''' Places a buy order for a given bar and for
            a given amount or number of units.
        '''
        date, price = self.get_date_price(bar)
        if units is None:
            units = int(amount / price)  ![14](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/14.png)
            # units = amount / price ![14](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/14.png)
        self.current_balance -= (1 + self.ptc) * \
            units * price + self.ftc  ![15](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/15.png)
        self.units += units  ![16](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/16.png)
        self.trades += 1  ![17](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/17.png)
        if self.verbose:
            print(f'{date} | buy {units} units for {price:.4f}')
            self.print_balance(bar)

    def place_sell_order(self, bar, amount=None, units=None):
        ''' Places a sell order for a given bar and for
            a given amount or number of units.
        '''
        date, price = self.get_date_price(bar)
        if units is None:
            units = int(amount / price)  ![14](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/14.png)
            # units = amount / price ![14](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/14.png)
        self.current_balance += (1 - self.ptc) * \
            units * price - self.ftc  ![15](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/15.png)
        self.units -= units  ![16](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/16.png)
        self.trades += 1  ![17](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/17.png)
        if self.verbose:
            print(f'{date} | sell {units} units for {price:.4f}')
            self.print_balance(bar)

    def close_out(self, bar):
        ''' Closes out any open position at a given bar.
        '''
        date, price = self.get_date_price(bar)
        print(50 * '=')
        print(f'{date} | *** CLOSING OUT ***')
        if self.units < 0:
            self.place_buy_order(bar, units=-self.units)  ![18](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/18.png)
        else:
            self.place_sell_order(bar, units=self.units)  ![19](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/19.png)
        if not self.verbose:
            print(f'{date} | current balance = {self.current_balance:.2f}')
        perf = (self.current_balance / self.initial_amount - 1) * 100  ![20](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/20.png)
        print(f'{date} | net performance [%] = {perf:.4f}')
        print(f'{date} | number of trades [#] = {self.trades}')
        print(50 * '=')

1

与相关的财务(Finance)环境

2

自交易机器人的相关 DNN 模型

3

初始/当前余额

4

比例交易成本

5

固定交易成本

6

打印输出是否冗长

7

金融工具交易的初始数量

8

实施的初始交易数量

9

在给定某个柱状图时相关的日期

10

在某个柱状图上给定的相关金融工具价格

11

特定柱状图的日期当前余额的输出

12

从当前余额和金融工具头寸计算净财富(net wealth)

13

在某个条的日期净财富的输出

14

给定交易金额的交易单位数

15

交易对当前余额的影响及相关成本

16

调整持有单位的数量

17

调整已实施的交易数量

18

空头头寸的平仓…

19

…或多头头寸

20

考虑到初始金额和最终当前余额的净表现

回测类

以下是带有TBBacktesterRM类的 Python 模块,用于基于事件的回测,包括风险度量(止损、移动止损、止盈订单):

#
# Event-Based Backtesting
# --Trading Bot Backtester
# (incl. Risk Management)
#
# (c) Dr. Yves J. Hilpisch
#
import numpy as np
import pandas as pd
import backtestingrm as btr

class TBBacktesterRM(btr.BacktestingBaseRM):
    def _reshape(self, state):
        ''' Helper method to reshape state objects.
 '''
        return np.reshape(state, [1, self.env.lags, self.env.n_features])

    def backtest_strategy(self, sl=None, tsl=None, tp=None,
                          wait=5, guarantee=False):
        ''' Event-based backtesting of the trading bot's performance.
 Incl. stop loss, trailing stop loss and take profit.
 '''
        self.units = 0
        self.position = 0
        self.trades = 0
        self.sl = sl
        self.tsl = tsl
        self.tp = tp
        self.wait = 0
        self.current_balance = self.initial_amount
        self.net_wealths = list()
        for bar in range(self.env.lags, len(self.env.data)):
            self.wait = max(0, self.wait - 1)
            date, price = self.get_date_price(bar)
            if self.trades == 0:
                print(50 * '=')
                print(f'{date} | *** START BACKTEST ***')
                self.print_balance(bar)
                print(50 * '=')

            # stop loss order
            if sl is not None and self.position != 0:
                rc = (price - self.entry_price) / self.entry_price
                if self.position == 1 and rc < -self.sl:
                    print(50 * '-')
                    if guarantee:
                        price = self.entry_price * (1 - self.sl)
                        print(f'*** STOP LOSS (LONG  | {-self.sl:.4f}) ***')
                    else:
                        print(f'*** STOP LOSS (LONG  | {rc:.4f}) ***')
                    self.place_sell_order(bar, units=self.units, gprice=price)
                    self.wait = wait
                    self.position = 0
                elif self.position == -1 and rc > self.sl:
                    print(50 * '-')
                    if guarantee:
                        price = self.entry_price * (1 + self.sl)
                        print(f'*** STOP LOSS (SHORT | -{self.sl:.4f}) ***')
                    else:
                        print(f'*** STOP LOSS (SHORT | -{rc:.4f}) ***')
                    self.place_buy_order(bar, units=-self.units, gprice=price)
                    self.wait = wait
                    self.position = 0

            # trailing stop loss order
            if tsl is not None and self.position != 0:
                self.max_price = max(self.max_price, price)
                self.min_price = min(self.min_price, price)
                rc_1 = (price - self.max_price) / self.entry_price
                rc_2 = (self.min_price - price) / self.entry_price
                if self.position == 1 and rc_1 < -self.tsl:
                    print(50 * '-')
                    print(f'*** TRAILING SL (LONG  | {rc_1:.4f}) ***')
                    self.place_sell_order(bar, units=self.units)
                    self.wait = wait
                    self.position = 0
                elif self.position == -1 and rc_2 < -self.tsl:
                    print(50 * '-')
                    print(f'*** TRAILING SL (SHORT | {rc_2:.4f}) ***')
                    self.place_buy_order(bar, units=-self.units)
                    self.wait = wait
                    self.position = 0

            # take profit order
            if tp is not None and self.position != 0:
                rc = (price - self.entry_price) / self.entry_price
                if self.position == 1 and rc > self.tp:
                    print(50 * '-')
                    if guarantee:
                        price = self.entry_price * (1 + self.tp)
                        print(f'*** TAKE PROFIT (LONG  | {self.tp:.4f}) ***')
                    else:
                        print(f'*** TAKE PROFIT (LONG  | {rc:.4f}) ***')
                    self.place_sell_order(bar, units=self.units, gprice=price)
                    self.wait = wait
                    self.position = 0
                elif self.position == -1 and rc < -self.tp:
                    print(50 * '-')
                    if guarantee:
                        price = self.entry_price * (1 - self.tp)
                        print(f'*** TAKE PROFIT (SHORT | {self.tp:.4f}) ***')
                    else:
                        print(f'*** TAKE PROFIT (SHORT | {-rc:.4f}) ***')
                    self.place_buy_order(bar, units=-self.units, gprice=price)
                    self.wait = wait
                    self.position = 0

            state = self.env.get_state(bar)
            action = np.argmax(self.model.predict(
                self._reshape(state.values))[0, 0])
            position = 1 if action == 1 else -1
            if self.position in [0, -1] and position == 1 and self.wait == 0:
                if self.verbose:
                    print(50 * '-')
                    print(f'{date} | *** GOING LONG ***')
                if self.position == -1:
                    self.place_buy_order(bar - 1, units=-self.units)
                self.place_buy_order(bar - 1, amount=self.current_balance)
                if self.verbose:
                    self.print_net_wealth(bar)
                self.position = 1
            elif self.position in [0, 1] and position == -1 and self.wait == 0:
                if self.verbose:
                    print(50 * '-')
                    print(f'{date} | *** GOING SHORT ***')
                if self.position == 1:
                    self.place_sell_order(bar - 1, units=self.units)
                self.place_sell_order(bar - 1, amount=self.current_balance)
                if self.verbose:
                    self.print_net_wealth(bar)
                self.position = -1
            self.net_wealths.append((date, self.calculate_net_wealth(price)))
        self.net_wealths = pd.DataFrame(self.net_wealths,
                                        columns=['date', 'net_wealth'])
        self.net_wealths.set_index('date', inplace=True)
        self.net_wealths.index = pd.DatetimeIndex(self.net_wealths.index)
        self.close_out(bar)

¹ 要了解更多关于 ATR 指标的详细信息,请参阅ATR (1) InvestopediaATR (2) Investopedia

² 保证的止损订单可能仅适用于某些司法管辖区的某些经纪客户群体,例如零售投资者/交易者。

³ 止盈订单有一个固定的目标价格水平。因此,使用一个时间间隔的高价计算多头头寸的实现利润或使用低价计算空头头寸的实现利润是不现实的。

第十二章:执行和部署

在混合城市交通、大雨和雪、未铺装和未映射的道路以及无线接入不可靠的情况下,自动驾驶车辆可靠运行还需取得可观的进展。

Todd Litman(2020)

从事算法交易的投资公司应当建立有效的系统和风险控制措施,适合其经营的业务,以确保其交易系统具有弹性和足够的容量,受到适当的交易阈值和限制的约束,并防止发送错误订单或以其他方式使系统在可能创造或促成混乱市场的方式中运行。

MiFID II(第 17 条)

第十一章以基于历史数据的金融 Q 学习代理形式训练交易机器人。它介绍了基于事件的回测作为一种灵活的方法,足以考虑典型的风险措施,如跟踪止损订单或利润目标。然而,所有这些都异步发生在仅基于历史数据的沙盒环境中。与自动驾驶车辆(AV)一样,部署 AI 到现实世界存在问题。对于 AV 来说,这意味着将 AI 与车辆硬件结合,并在测试和公共道路上部署 AV。对于交易机器人来说,这意味着将交易机器人连接到交易平台,并部署以实现订单的自动执行。换句话说,算法方面已经很清晰——现在需要添加执行和部署来实现算法交易。

本章介绍了用于算法交易的Oanda交易平台。因此,重点放在平台的v20 API,而不是提供用户手动交易界面的应用程序上。为了简化代码,介绍并使用了包装器包tpqoa,它依赖于 Oanda 的v20 Python 包,并提供更符合 Python 风格的用户界面。

“Oanda 账户”详细介绍了使用 Oanda 的演示账户的先决条件。“数据检索”展示了如何从 API 检索历史和实时(流式)数据。“订单执行”处理买卖订单的执行,可能包括其他订单,如跟踪止损订单。“交易机器人”基于 Oanda 的历史分钟数据训练交易机器人,并以向量化方式进行性能回测。最后,“部署”展示了如何实时部署交易机器人和自动化部署。

Oanda 账户

本章的代码依赖于 Python 包装器包tpqoa。可以通过以下方式使用pip安装此包:

pip install --upgrade git+https://github.com/yhilpisch/tpqoa.git

要使用此包,仅需一个Oanda的演示账户即可。一旦账户开通,在登录后的账户页面生成一个访问令牌。然后将访问令牌和账户 ID(也可在账户页面找到)存储在配置文本文件中,如下所示:

[oanda]
account_id = XYZ-ABC-...
access_token = ZYXCAB...
account_type = practice

如果配置文件名为aiif.cfg,并且存储在当前工作目录中,则可以如下使用tpqoa包:

import tpqoa
api = tpqoa.tpqoa('aiif.cfg')

风险免责声明和披露

Oanda 是外汇(FX)和差价合约(CFD)交易的平台。这些工具涉及相当大的风险,特别是在使用杠杆交易时。强烈建议您在继续之前仔细阅读来自 Oanda 网站上所有相关的风险免责声明和披露(请查看适用司法管辖区)。

本章中呈现的所有代码和示例仅用于技术说明,不构成任何投资建议或类似内容。

数据检索

通常,首先进行一些 Python 导入和配置:

In [1]: import os
        import time
        import numpy as np
        import pandas as pd
        from pprint import pprint
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        pd.set_option('mode.chained_assignment', None)
        pd.set_option('display.float_format', '{:.5f}'.format)
        np.set_printoptions(suppress=True, precision=4)
        os.environ['PYTHONHASHSEED'] = '0'

根据账户的相关司法管辖权,Oanda 提供多种可交易的外汇和差价合约工具。以下 Python 代码检索给定账户的可用工具:

In [2]: import tpqoa  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: api = tpqoa.tpqoa('../aiif.cfg')  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [4]: ins = api.get_instruments()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [5]: ins[:5]  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[5]: [('AUD/CAD', 'AUD_CAD'),
         ('AUD/CHF', 'AUD_CHF'),
         ('AUD/HKD', 'AUD_HKD'),
         ('AUD/JPY', 'AUD_JPY'),
         ('AUD/NZD', 'AUD_NZD')]

1

导入tpqoa

2

给定账户凭据实例化一个 API 对象

3

检索可用工具列表的格式为(显示名称, 技术名称)

4

展示其中几个工具

Oanda 通过其 v20 API 提供丰富的历史数据。以下示例检索 EUR/USD 货币对的历史数据,粒度设置为D(即每日)。

图 12-1 绘制了收盘(卖出)价格:

In [6]: raw = api.get_history(instrument='EUR_USD',  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                              start='2018-01-01',  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                              end='2020-07-31',  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                              granularity='D',  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                              price='A')  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [7]: raw.info()
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 671 entries, 2018-01-01 22:00:00 to 2020-07-30 21:00:00
        Data columns (total 6 columns):
         #   Column    Non-Null Count  Dtype
        ---  ------    --------------  -----
         0   o         671 non-null    float64
         1   h         671 non-null    float64
         2   l         671 non-null    float64
         3   c         671 non-null    float64
         4   volume    671 non-null    int64
         5   complete  671 non-null    bool
        dtypes: bool(1), float64(4), int64(1)
        memory usage: 32.1 KB

In [8]: raw.head()
Out[8]:                           o       h       l       c  volume  complete
        time
        2018-01-01 22:00:00 1.20101 1.20819 1.20051 1.20610   35630      True
        2018-01-02 22:00:00 1.20620 1.20673 1.20018 1.20170   31354      True
        2018-01-03 22:00:00 1.20170 1.20897 1.20049 1.20710   35187      True
        2018-01-04 22:00:00 1.20692 1.20847 1.20215 1.20327   36478      True
        2018-01-07 22:00:00 1.20301 1.20530 1.19564 1.19717   27618      True

In [9]: raw['c'].plot(figsize=(10, 6));

1

指定工具…

2

…开始日期…

3

…结束日期…

4

…粒度(D = 每日)…

5

…和价格序列类型(A = 询价价)

aiif 1201

图 12-1. Oanda 提供的 EUR/USD 历史每日收盘价格

就如下面的代码所示,分钟数据和日数据一样容易检索和使用。图 12-2 可视化了分钟条(中间价)价格数据:

In [10]: raw = api.get_history(instrument='EUR_USD',
                               start='2020-07-01',
                               end='2020-07-31',
                               granularity='M1',  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                               price='M')   ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [11]: raw.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 30728 entries, 2020-07-01 00:00:00 to 2020-07-30 23:59:00
         Data columns (total 6 columns):
          #   Column    Non-Null Count  Dtype
         ---  ------    --------------  -----
          0   o         30728 non-null  float64
          1   h         30728 non-null  float64
          2   l         30728 non-null  float64
          3   c         30728 non-null  float64
          4   volume    30728 non-null  int64
          5   complete  30728 non-null  bool
         dtypes: bool(1), float64(4), int64(1)
         memory usage: 1.4 MB

In [12]: raw.tail()
Out[12]:                           o       h       l       c  volume  complete
         time
         2020-07-30 23:55:00 1.18724 1.18739 1.18718 1.18738      57      True
         2020-07-30 23:56:00 1.18736 1.18758 1.18722 1.18757      57      True
         2020-07-30 23:57:00 1.18756 1.18756 1.18734 1.18734      49      True
         2020-07-30 23:58:00 1.18736 1.18737 1.18713 1.18717      36      True
         2020-07-30 23:59:00 1.18718 1.18724 1.18714 1.18722      31      True

In [13]: raw['c'].plot(figsize=(10, 6));

1

指定粒度(M1 = 一分钟)…

2

…和价格序列类型(M = 中间价)

aiif 1202

图 12-2. Oanda 提供的 EUR/USD 历史一分钟条收盘价格

尽管历史数据很重要,例如用于训练和测试交易机器人,实时(流式)数据则是部署算法交易的必要条件。tpqoa允许通过单个方法调用同步流式传输所有可用工具的实时数据。该方法默认打印时间戳和买入/卖出价格。对于算法交易,可以根据需要调整此默认行为,如“部署”所示:

In [14]: api.stream_data('EUR_USD', stop=10)
         2020-08-13T12:07:09.735715316Z 1.18328 1.18342
         2020-08-13T12:07:16.245253689Z 1.18329 1.18343
         2020-08-13T12:07:16.397803785Z 1.18328 1.18342
         2020-08-13T12:07:17.240232521Z 1.18331 1.18346
         2020-08-13T12:07:17.358476854Z 1.18334 1.18348
         2020-08-13T12:07:17.778061207Z 1.18331 1.18345
         2020-08-13T12:07:18.016544856Z 1.18333 1.18346
         2020-08-13T12:07:18.144762415Z 1.18334 1.18348
         2020-08-13T12:07:18.689365678Z 1.18331 1.18345
         2020-08-13T12:07:19.148039139Z 1.18331 1.18345

执行订单

自动驾驶车辆的 AI 需要能够控制实体车辆。为此,它向车辆发送不同类型的信号,例如加速、刹车、左转或右转。交易机器人需要能够在交易平台上下订单。本节涵盖了不同类型的订单,例如市价订单和止损订单。

最基本的订单类型是市价订单。该订单允许以当前市场价格(即购买时的卖出价和销售时的买入价)买入或卖出金融工具。以下示例基于 20 倍杠杆和相对较小的订单规模。因此,流动性问题并不重要。通过 Oanda v20 API 执行订单时,API 会返回详细的订单对象。首先,下了一个买市场订单

In [15]: order = api.create_order('EUR_USD', units=25000,
                                  suppress=True, ret=True)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         pprint(order)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         {'accountBalance': '98553.3172',
          'accountID': '101-004-13834683-001',
          'batchID': '1625',
          'commission': '0.0',
          'financing': '0.0',
          'fullPrice': {'asks': [{'liquidity': '10000000', 'price': 1.18345}],
                        'bids': [{'liquidity': '10000000', 'price': 1.18331}],
                        'closeoutAsk': 1.18345,
                        'closeoutBid': 1.18331,
                        'type': 'PRICE'},
          'fullVWAP': 1.18345,
          'gainQuoteHomeConversionFactor': '0.840811914585',
          'guaranteedExecutionFee': '0.0',
          'halfSpreadCost': '1.4788',
          'id': '1626',
          'instrument': 'EUR_USD',
          'lossQuoteHomeConversionFactor': '0.849262285586',
          'orderID': '1625',
          'pl': '0.0',
          'price': 1.18345,
          'reason': 'MARKET_ORDER',
          'requestID': '78757241547812154',
          'time': '2020-08-13T12:07:19.434407966Z',
          'tradeOpened': {'guaranteedExecutionFee': '0.0',
                          'halfSpreadCost': '1.4788',
                          'initialMarginRequired': '832.5',
                          'price': 1.18345,
                          'tradeID': '1626',
                          'units': '25000.0'},
          'type': 'ORDER_FILL',
          'units': '25000.0',
          'userID': 13834683}

In [16]: def print_details(order):  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             details = (order['time'][:-7], order['instrument'], order['units'],
                        order['price'], order['pl'])
             return details

In [17]: print_details(order)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[17]: ('2020-08-13T12:07:19.434', 'EUR_USD', '25000.0', 1.18345, '0.0')

In [18]: time.sleep(1)

1

下单一个买市场订单并打印订单对象的详情

2

选择并显示订单的时间工具单位价格pl详情

其次,通过相同大小的卖市场订单关闭仓位。而第一笔交易由于其性质(在考虑交易成本之前)P&L 为零——通常第二笔交易具有非零 P&L:

In [19]: order = api.create_order('EUR_USD', units=-25000,
                                  suppress=True, ret=True)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         pprint(order)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         {'accountBalance': '98549.283',
          'accountID': '101-004-13834683-001',
          'batchID': '1627',
          'commission': '0.0',
          'financing': '0.0',
          'fullPrice': {'asks': [{'liquidity': '9975000', 'price': 1.18339}],
                        'bids': [{'liquidity': '10000000', 'price': 1.18326}],
                        'closeoutAsk': 1.18339,
                        'closeoutBid': 1.18326,
                        'type': 'PRICE'},
          'fullVWAP': 1.18326,
          'gainQuoteHomeConversionFactor': '0.840850994445',
          'guaranteedExecutionFee': '0.0',
          'halfSpreadCost': '1.3732',
          'id': '1628',
          'instrument': 'EUR_USD',
          'lossQuoteHomeConversionFactor': '0.849301758209',
          'orderID': '1627',
          'pl': '-4.0342',
          'price': 1.18326,
          'reason': 'MARKET_ORDER',
          'requestID': '78757241552009237',
          'time': '2020-08-13T12:07:20.586564454Z',
          'tradesClosed': [{'financing': '0.0',
                            'guaranteedExecutionFee': '0.0',
                            'halfSpreadCost': '1.3732',
                            'price': 1.18326,
                            'realizedPL': '-4.0342',
                            'tradeID': '1626',
                            'units': '-25000.0'}],
          'type': 'ORDER_FILL',
          'units': '-25000.0',
          'userID': 13834683}

In [20]: print_details(order) ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[20]: ('2020-08-13T12:07:20.586', 'EUR_USD', '-25000.0', 1.18326, '-4.0342')

In [21]: time.sleep(1)

1

下单一个卖市场订单并打印订单对象的详情

2

选择并显示订单的时间工具单位价格pl详情

限价订单

本章仅涵盖市价订单作为一种基本订单类型。市价订单在下单时以当前价格买入或卖出金融工具。相比之下,限价订单作为另一种主要的基本订单类型,允许以最低或最高价格下单。只有当达到最低/最高价格时才执行订单。在此之前不进行任何交易。

接下来,考虑同一组合交易的示例,但这次使用止损(SL)订单。SL 订单被视为单独的(限价)订单。以下 Python 代码下单并显示了 SL 订单对象的详情:

In [22]: order = api.create_order('EUR_USD', units=25000,
                                  sl_distance=0.005,  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                                  suppress=True, ret=True)

In [23]: print_details(order)
Out[23]: ('2020-08-13T12:07:21.740', 'EUR_USD', '25000.0', 1.18343, '0.0')

In [24]: sl_order = api.get_transaction(tid=int(order['id']) + 1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [25]: sl_order  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[25]: {'id': '1631',
          'time': '2020-08-13T12:07:21.740825489Z',
          'userID': 13834683,
          'accountID': '101-004-13834683-001',
          'batchID': '1629',
          'requestID': '78757241556206373',
          'type': 'STOP_LOSS_ORDER',
          'tradeID': '1630',
          'price': 1.17843,
          'distance': '0.005',
          'timeInForce': 'GTC',
          'triggerCondition': 'DEFAULT',
          'reason': 'ON_FILL'}

In [26]: (sl_order['time'], sl_order['type'], order['price'],
          sl_order['price'], sl_order['distance'])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[26]: ('2020-08-13T12:07:21.740825489Z',
          'STOP_LOSS_ORDER',
          1.18343,
          1.17843,
          '0.005')

In [27]: time.sleep(1)

In [28]: order = api.create_order('EUR_USD', units=-25000, suppress=True, ret=True)

In [29]: print_details(order)
Out[29]: ('2020-08-13T12:07:23.059', 'EUR_USD', '-25000.0', 1.18329, '-2.9725')

1

SL 距离以货币单位定义。

2

选择并显示 SL 订单对象数据。

3

选择并显示两个订单对象的一些相关细节。

移动止损(TSL)订单以相同的方式处理。唯一的区别是 TSL 订单没有固定的价格:

In [30]: order = api.create_order('EUR_USD', units=25000,
                                  tsl_distance=0.005,  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                                  suppress=True, ret=True)

In [31]: print_details(order)
Out[31]: ('2020-08-13T12:07:23.204', 'EUR_USD', '25000.0', 1.18341, '0.0')

In [32]: tsl_order = api.get_transaction(tid=int(order['id']) + 1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [33]: tsl_order  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[33]: {'id': '1637',
          'time': '2020-08-13T12:07:23.204457044Z',
          'userID': 13834683,
          'accountID': '101-004-13834683-001',
          'batchID': '1635',
          'requestID': '78757241564598562',
          'type': 'TRAILING_STOP_LOSS_ORDER',
          'tradeID': '1636',
          'distance': '0.005',
          'timeInForce': 'GTC',
          'triggerCondition': 'DEFAULT',
          'reason': 'ON_FILL'}

In [34]: (tsl_order['time'][:-7], tsl_order['type'],
          order['price'], tsl_order['distance'])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[34]: ('2020-08-13T12:07:23.204', 'TRAILING_STOP_LOSS_ORDER', 1.18341, '0.005')

In [35]: time.sleep(1)

In [36]: order = api.create_order('EUR_USD', units=-25000,
                                  suppress=True, ret=True)

In [37]: print_details(order)
Out[37]: ('2020-08-13T12:07:24.551', 'EUR_USD', '-25000.0', 1.1833, '-2.3355')

In [38]: time.sleep(1)

1

TSL 距离以货币单位定义。

2

选择并显示 TSL 订单对象数据。

3

选择并显示两个订单对象的一些相关细节。

最后,这是一个获利(TP)订单。该订单需要一个固定的 TP 目标价格。因此,以下代码使用先前订单的执行价格来定义相对价格的 TP 价格。除此以外的小差异外,处理方式与之前一样。

In [39]: tp_price = round(order['price'] + 0.01, 4)
         tp_price
Out[39]: 1.1933

In [40]: order = api.create_order('EUR_USD', units=25000,
                                  tp_price=tp_price,  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                                  suppress=True, ret=True)

In [41]: print_details(order)
Out[41]: ('2020-08-13T12:07:25.712', 'EUR_USD', '25000.0', 1.18344, '0.0')

In [42]: tp_order = api.get_transaction(tid=int(order['id']) + 1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [43]: tp_order  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[43]: {'id': '1643',
          'time': '2020-08-13T12:07:25.712531725Z',
          'userID': 13834683,
          'accountID': '101-004-13834683-001',
          'batchID': '1641',
          'requestID': '78757241572993078',
          'type': 'TAKE_PROFIT_ORDER',
          'tradeID': '1642',
          'price': 1.1933,
          'timeInForce': 'GTC',
          'triggerCondition': 'DEFAULT',
          'reason': 'ON_FILL'}

In [44]: (tp_order['time'][:-7], tp_order['type'],
          order['price'], tp_order['price'])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[44]: ('2020-08-13T12:07:25.712', 'TAKE_PROFIT_ORDER', 1.18344, 1.1933)

In [45]: time.sleep(1)

In [46]: order = api.create_order('EUR_USD', units=-25000,
                                  suppress=True, ret=True)

In [47]: print_details(order)
Out[47]: ('2020-08-13T12:07:27.020', 'EUR_USD', '-25000.0', 1.18332, '-2.5478')

1

TP 目标价格相对于先前执行价格定义。

2

选择并显示 TP 订单对象数据。

3

选择并显示两个订单对象的一些相关细节。

到目前为止,代码只处理单个订单的交易详情。然而,对于多个历史交易的概览也很有趣。为此,以下方法调用提供了本节中所有主要订单的概览数据,包括 P&L 数据:

In [48]: api.print_transactions(tid=int(order['id']) - 22)
          1626 | 2020-08-13T12:07:19.434407966Z |   EUR_USD |      25000.0 |      0.0
          1628 | 2020-08-13T12:07:20.586564454Z |   EUR_USD |     -25000.0 |  -4.0342
          1630 | 2020-08-13T12:07:21.740825489Z |   EUR_USD |      25000.0 |      0.0
          1633 | 2020-08-13T12:07:23.059178023Z |   EUR_USD |     -25000.0 |  -2.9725
          1636 | 2020-08-13T12:07:23.204457044Z |   EUR_USD |      25000.0 |      0.0
          1639 | 2020-08-13T12:07:24.551026466Z |   EUR_USD |     -25000.0 |  -2.3355
          1642 | 2020-08-13T12:07:25.712531725Z |   EUR_USD |      25000.0 |      0.0
          1645 | 2020-08-13T12:07:27.020414342Z |   EUR_USD |     -25000.0 |  -2.5478

另一个方法调用提供了账户详情的快照。显示的细节来自使用已经进行了相当长时间的 Oanda 演示账户进行技术测试的目的:

In [49]: api.get_account_summary()
Out[49]: {'id': '101-004-13834683-001',
          'alias': 'Primary',
          'currency': 'EUR',
          'balance': '98541.4272',
          'createdByUserID': 13834683,
          'createdTime': '2020-03-19T06:08:14.363139403Z',
          'guaranteedStopLossOrderMode': 'DISABLED',
          'pl': '-1248.5543',
          'resettablePL': '-1248.5543',
          'resettablePLTime': '0',
          'financing': '-210.0185',
          'commission': '0.0',
          'guaranteedExecutionFees': '0.0',
          'marginRate': '0.0333',
          'openTradeCount': 1,
          'openPositionCount': 1,
          'pendingOrderCount': 0,
          'hedgingEnabled': False,
          'unrealizedPL': '941.9536',
          'NAV': '99483.3808',
          'marginUsed': '380.83',
          'marginAvailable': '99107.2283',
          'positionValue': '3808.3',
          'marginCloseoutUnrealizedPL': '947.9546',
          'marginCloseoutNAV': '99489.3818',
          'marginCloseoutMarginUsed': '380.83',
          'marginCloseoutPercent': '0.00191',
          'marginCloseoutPositionValue': '3808.3',
          'withdrawalLimit': '98541.4272',
          'marginCallMarginUsed': '380.83',
          'marginCallPercent': '0.00383',
          'lastTransactionID': '1646'}

这结束了使用 Oanda 执行订单基础知识的讨论。所有元素现在都已准备好支持交易机器人的部署。本章剩余部分将在 Oanda 数据上训练交易机器人,并以自动化方式部署它。

交易机器人

第十一章详细介绍了如何训练深度 Q 学习交易机器人以及如何以矢量化和基于事件的方式进行回测。此节现在基于来自 Oanda 的历史数据重复了这方面的一些核心步骤。“Oanda 环境”提供了一个包含用于处理 Oanda 数据的环境类 OandaEnv 的 Python 模块。它可以与第十一章中的 Finance 类一样使用。

以下 Python 代码实例化了学习环境对象。在此步骤中,固定了驱动学习、验证和测试的主要数据相关参数。OandaEnv 类允许包含杠杆,这对于外汇和差价合约交易是典型的。杠杆放大了实现的回报,从而增加了利润潜力,但也增加了损失风险:

In [50]: import oandaenv as oe

In [51]: symbol = 'EUR_USD'

In [52]: date = '2020-08-11'

In [53]: features = [symbol, 'r', 's', 'm', 'v']

In [54]: %%time
         learn_env = oe.OandaEnv(symbol=symbol,
                           start=f'{date} 08:00:00',
                           end=f'{date} 13:00:00',
                           granularity='S30',  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                           price='M',  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                           features=features,  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                           window=20,  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                           lags=3,  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                           leverage=20,  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                           min_accuracy=0.4,  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
                           min_performance=0.85  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
                          )
         CPU times: user 23.1 ms, sys: 2.86 ms, total: 25.9 ms
         Wall time: 26.8 ms

In [55]: np.bincount(learn_env.data['d'])
Out[55]: array([299, 281])

In [56]: learn_env.data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 580 entries, 2020-08-11 08:10:00 to 2020-08-11 12:59:30
         Data columns (total 6 columns):
          #   Column   Non-Null Count  Dtype
         ---  ------   --------------  -----
          0   EUR_USD  580 non-null    float64
          1   r        580 non-null    float64
          2   s        580 non-null    float64
          3   m        580 non-null    float64
          4   v        580 non-null    float64
          5   d        580 non-null    int64
         dtypes: float64(5), int64(1)
         memory usage: 31.7 KB

1

设置数据的粒度为五秒钟

2

将价格类型设置为中间价格

3

定义要使用的特征集

4

定义滚动统计的窗口长度

5

指定滞后数

6

修正杠杆

7

设置所需的最低准确率

8

设置所需的最低性能

下一步,将实例化验证环境,依赖于学习环境的参数——除了时间间隔,由于显而易见的原因。图 12-3 显示了 EUR/USD 的收盘价格,作为学习、验证和测试环境中使用的(从左到右):

In [57]: valid_env = oe.OandaEnv(symbol=learn_env.symbol,
                           start=f'{date} 13:00:00',
                           end=f'{date} 14:00:00',
                           granularity=learn_env.granularity,
                           price=learn_env.price,
                           features=learn_env.features,
                           window=learn_env.window,
                           lags=learn_env.lags,
                           leverage=learn_env.leverage,
                           min_accuracy=0,
                           min_performance=0,
                           mu=learn_env.mu,
                           std=learn_env.std
                          )

In [58]: valid_env.data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 100 entries, 2020-08-11 13:10:00 to 2020-08-11 13:59:30
         Data columns (total 6 columns):
          #   Column   Non-Null Count  Dtype
         ---  ------   --------------  -----
          0   EUR_USD  100 non-null    float64
          1   r        100 non-null    float64
          2   s        100 non-null    float64
          3   m        100 non-null    float64
          4   v        100 non-null    float64
          5   d        100 non-null    int64
         dtypes: float64(5), int64(1)
         memory usage: 5.5 KB

In [59]: test_env = oe.OandaEnv(symbol=learn_env.symbol,
                           start=f'{date} 14:00:00',
                           end=f'{date} 17:00:00',
                           granularity=learn_env.granularity,
                           price=learn_env.price,
                           features=learn_env.features,
                           window=learn_env.window,
                           lags=learn_env.lags,
                           leverage=learn_env.leverage,
                           min_accuracy=0,
                           min_performance=0,
                           mu=learn_env.mu,
                           std=learn_env.std
                          )

In [60]: test_env.data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 340 entries, 2020-08-11 14:10:00 to 2020-08-11 16:59:30
         Data columns (total 6 columns):
          #   Column   Non-Null Count  Dtype
         ---  ------   --------------  -----
          0   EUR_USD  340 non-null    float64
          1   r        340 non-null    float64
          2   s        340 non-null    float64
          3   m        340 non-null    float64
          4   v        340 non-null    float64
          5   d        340 non-null    int64
         dtypes: float64(5), int64(1)
         memory usage: 18.6 KB

In [61]: ax = learn_env.data[learn_env.symbol].plot(figsize=(10, 6))
         plt.axvline(learn_env.data.index[-1], ls='--')
         valid_env.data[learn_env.symbol].plot(ax=ax, style='-.')
         plt.axvline(valid_env.data.index[-1], ls='--')
         test_env.data[learn_env.symbol].plot(ax=ax, style='-.');

aiif 1203

图 12-3. Oanda 提供的 EUR/USD 历史 30 秒钟收盘价(学习 = 左,验证 = 中,测试 = 右)

基于 Oanda 环境,可以对第十一章中的交易机器人进行训练和验证。以下 Python 代码执行此任务并可视化性能结果(参见 图 12-4):

In [62]: import sys
         sys.path.append('../ch11/')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [63]: import tradingbot  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         Using TensorFlow backend.

In [64]: tradingbot.set_seeds(100)
         agent = tradingbot.TradingBot(24, 0.001, learn_env=learn_env,
                                       valid_env=valid_env)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [65]: episodes = 31

In [66]: %time agent.learn(episodes)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         =======================================================================
         episode:  5/31 | VALIDATION | treward:   97 | perf: 1.004 | eps: 0.96
         =======================================================================
         =======================================================================
         episode: 10/31 | VALIDATION | treward:   97 | perf: 1.005 | eps: 0.91
         =======================================================================
         =======================================================================
         episode: 15/31 | VALIDATION | treward:   97 | perf: 0.986 | eps: 0.87
         =======================================================================
         =======================================================================
         episode: 20/31 | VALIDATION | treward:   97 | perf: 1.012 | eps: 0.83
         =======================================================================
         =======================================================================
         episode: 25/31 | VALIDATION | treward:   97 | perf: 0.995 | eps: 0.79
         =======================================================================
         =======================================================================
         episode: 30/31 | VALIDATION | treward:   97 | perf: 0.972 | eps: 0.75
         =======================================================================
         episode: 31/31 | treward:   16 | perf: 0.981 | av: 376.0 | max:  577
         CPU times: user 22.1 s, sys: 1.17 s, total: 23.3 s
         Wall time: 20.1 s

In [67]: tradingbot.plot_performance(agent)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

1

从第十一章导入tradingbot模块

2

基于 Oanda 数据对交易机器人进行训练和验证

3

可视化性能结果

如前两章所讨论的,训练和验证表现只是交易机器人表现的一个指标。

aiif 1204

图 12-4. Oanda 数据的交易机器人的训练和验证性能结果

下面的代码实现了用于测试环境的交易机器人性能的矢量化回测——再次使用与学习环境相同的参数,除了使用的时间间隔。代码利用了 Python 模块中提供的backtest()函数,该模块在“矢量化回测”中有介绍。报告的性能数字包括杠杆为 20。这对于被动基准投资的总体表现和交易机器人的表现都是如此,如图 12-5 所示:

In [68]: import backtest as bt

In [69]: env = test_env

In [70]: bt.backtest(agent, env)

In [71]: env.data['p'].iloc[env.lags:].value_counts()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[71]:  1    263
         -1     74
         Name: p, dtype: int64

In [72]: sum(env.data['p'].iloc[env.lags:].diff() != 0)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[72]: 25

In [73]: (env.data[['r', 's']].iloc[env.lags:] * env.leverage).sum(
                 ).apply(np.exp)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[73]: r   0.99966
         s   1.05910
         dtype: float64

In [74]: (env.data[['r', 's']].iloc[env.lags:] * env.leverage).sum(
                 ).apply(np.exp) - 1  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[74]: r   -0.00034
         s    0.05910
         dtype: float64

In [75]: (env.data[['r', 's']].iloc[env.lags:] * env.leverage).cumsum(
                 ).apply(np.exp).plot(figsize=(10, 6));  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

1

显示长仓和短仓的总数

2

显示实施该策略所需的交易次数

3

计算包括杠杆在内的总体表现

4

计算包括杠杆在内的净表现

5

可视化随时间推移的总体表现,包括杠杆

aiif 1205

图 12-5. 被动基准投资和交易机器人随时间的总体表现(包括杠杆)

简化的回测

本节中交易机器人的训练和回测是在不现实的假设条件下进行的。基于 30 秒钟的柱状图的交易策略可能会在短时间内导致大量交易。假设典型的交易成本(买卖价差),这样的策略通常在经济上不可行。更长的柱状图或更少交易的策略可能更现实。但是,为了在下一节中允许“快速”部署演示,训练和回测是故意在相对短的 30 秒钟柱状图上实施的。

部署

本节结合了前几节的主要元素,以自动化方式部署训练过的交易机器人。这类似于 AV 准备在街上部署的时刻。以下代码中介绍的OandaTradingBot类继承自tpqoa类,并添加了一些辅助函数和交易逻辑:

In [76]: import tpqoa

In [77]: class OandaTradingBot(tpqoa.tpqoa):
             def __init__(self, config_file, agent, granularity, units,
                          verbose=True):
                 super(OandaTradingBot, self).__init__(config_file)
                 self.agent = agent
                 self.symbol = self.agent.learn_env.symbol
                 self.env = agent.learn_env
                 self.window = self.env.window
                 if granularity is None:
                     self.granularity = agent.learn_env.granularity
                 else:
                     self.granularity = granularity
                 self.units = units
                 self.trades = 0
                 self.position = 0
                 self.tick_data = pd.DataFrame()
                 self.min_length = (self.agent.learn_env.window +
                                    self.agent.learn_env.lags)
                 self.pl = list()
                 self.verbose = verbose
             def _prepare_data(self):
                 self.data['r'] = np.log(self.data / self.data.shift(1))
                 self.data.dropna(inplace=True)
                 self.data['s'] = self.data[self.symbol].rolling(
                                                     self.window).mean()
                 self.data['m'] = self.data['r'].rolling(self.window).mean()
                 self.data['v'] = self.data['r'].rolling(self.window).std()
                 self.data.dropna(inplace=True)
                 # self.data_ = (self.data - self.env.mu) / self.env.std ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
                 self.data_ = (self.data - self.data.mean()) / self.data.std()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             def _resample_data(self):
                 self.data = self.tick_data.resample(self.granularity,
                                 label='right').last().ffill().iloc[:-1]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 self.data = pd.DataFrame(self.data['mid'])  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 self.data.columns = [self.symbol,]  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 self.data.index = self.data.index.tz_localize(None)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             def _get_state(self):
                 state = self.data_[self.env.features].iloc[-self.env.lags:]  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
                 return np.reshape(state.values, [1, self.env.lags,
                                                  self.env.n_features])  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
             def report_trade(self, time, side, order):
                 self.trades += 1
                 pl = float(order['pl'])  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 self.pl.append(pl)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
                 cpl = sum(self.pl)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
                 print('\n' + 75 * '=')
                 print(f'{time} | *** GOING {side} ({self.trades}) ***')
                 print(f'{time} | PROFIT/LOSS={pl:.2f} | CUMULATIVE={cpl:.2f}')
                 print(75 * '=')
                 if self.verbose:
                     pprint(order)
                     print(75 * '=')
             def on_success(self, time, bid, ask):
                 df = pd.DataFrame({'ask': ask, 'bid': bid,
                                    'mid': (bid + ask) / 2},
                                   index=[pd.Timestamp(time)])
                 self.tick_data = self.tick_data.append(df)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 self._resample_data()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
                 if len(self.data) > self.min_length:
                     self.min_length += 1
                     self._prepare_data()
                     state = self._get_state()  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                     prediction = np.argmax(
                         self.agent.model.predict(state)[0, 0])  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                     position = 1 if prediction == 1 else -1  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
                     if self.position in [0, -1] and position == 1:  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
                         order = self.create_order(self.symbol,
                                 units=(1 - self.position) * self.units,
                                         suppress=True, ret=True)
                         self.report_trade(time, 'LONG', order)
                         self.position = 1
                     elif self.position in [0, 1] and position == -1:  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
                         order = self.create_order(self.symbol,
                                 units=-(1 + self.position) * self.units,
                                         suppress=True, ret=True)
                         self.report_trade(time, 'SHORT', order)
                         self.position = -1

1

为了演示,使用实时数据统计进行归一化处理。¹

2

收集 tick 数据并将其重新采样到所需的粒度。

3

返回当前金融市场的状态。

4

收集每笔交易的盈亏数据。

5

计算所有交易的累积盈亏。

6

预测市场方向并推导信号(头寸)。

7

检查是否满足多头位(买入订单)的条件。

8

检查是否满足空头位(卖出订单)的条件。

这个类的应用非常直接。首先,实例化一个对象,主要输入是上一节中训练过的交易机器人agent。其次,需要启动要交易的工具的流。每当有新的 tick 数据到达时,都会调用.on_success()方法,其中包含处理 tick 数据和下订单的主要逻辑。为了加快速度,部署示例依赖于之前的回测一样,使用 30 秒钟的柱状图。在管理真实资金时,生产环境中可能更长的时间间隔可能是更好的选择——如果只是为了减少交易数量和因此的交易成本:

In [78]: otb = OandaTradingBot('../aiif.cfg', agent, '30s',
                               25000, verbose=False)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [79]: otb.tick_data.info()
         <class 'pandas.core.frame.DataFrame'>
         Index: 0 entries
         Empty DataFrame
In [80]: otb.stream_data(agent.learn_env.symbol, stop=1000)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

         ===========================================================================
         2020-08-13T12:19:32.320291893Z | *** GOING SHORT (1) ***
         2020-08-13T12:19:32.320291893Z | PROFIT/LOSS=0.00 | CUMULATIVE=0.00
         ===========================================================================

         ===========================================================================
         2020-08-13T12:20:00.083985447Z | *** GOING LONG (2) ***
         2020-08-13T12:20:00.083985447Z | PROFIT/LOSS=-6.80 | CUMULATIVE=-6.80
         ===========================================================================

         ===========================================================================
         2020-08-13T12:25:00.099901587Z | *** GOING SHORT (3) ***
         2020-08-13T12:25:00.099901587Z | PROFIT/LOSS=-7.86 | CUMULATIVE=-14.66
         ===========================================================================

In [81]: print('\n' + 75 * '=')
         print('*** CLOSING OUT ***')
         order = otb.create_order(otb.symbol,
                         units=-otb.position * otb.units,
                         suppress=True, ret=True)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         otb.report_trade(otb.time, 'NEUTRAL', order)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         if otb.verbose:
             pprint(order)
         print(75 * '=')

         ===========================================================================
         *** CLOSING OUT ***

         ===========================================================================
         2020-08-13T12:25:16.870357562Z | *** GOING NEUTRAL (4) ***
         2020-08-13T12:25:16.870357562Z | PROFIT/LOSS=-3.19 | CUMULATIVE=-17.84
         ===========================================================================
         ===========================================================================

1

实例化OandaTradingBot对象

2

启动实时数据和交易的流式传输

3

在检索到一定数量的 ticks 后关闭最终位置

在部署期间,利润和损失(P&L)数据被收集在pl属性中,这是一个list对象。一旦交易停止,可以分析 P&L 数据:

In [82]: pl = np.array(otb.pl)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [83]: pl  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[83]: array([ 0.    , -6.7959, -7.8594, -3.1862])

In [84]: pl.cumsum()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[84]: array([  0.    ,  -6.7959, -14.6553, -17.8415])

1

所有交易的 P&L 数据

2

累积 P&L 数据

简单的部署示例说明,人们可以使用少于 100 行的 Python 代码进行算法交易和自动化交易,使用深度 Q 学习交易机器人。主要前提是训练有素的交易机器人(即tradingbot类的实例)。此处有意省略了许多重要方面。例如,在生产环境中,可能希望持久化数据。还可能希望持久化订单对象。确保套接字连接仍然活动的措施也很重要(例如通过监视心跳)。总体而言,安全性、可靠性、日志记录和监控并未得到真正的关注。关于这方面的一些更多细节在 Hilpisch(2020)中提供。

Python 脚本中的“Oanda Trading Bot”展示了OandaTradingBot类的独立可执行版本。与 Jupyter Notebook 或 Jupyter Lab 等交互式环境相比,这代表了更稳健的部署选项的重要进展。脚本还包括为执行添加 SL、TSL 或 TP 订单的功能。脚本期望当前工作目录中有agent对象的 pickle 版本,以下 Python 代码将该对象 pickle 化以供脚本后续使用:

In [85]: import pickle

In [86]: pickle.dump(agent, open('trading.bot', 'wb'))

结论

本章讨论了算法交易策略执行和交易机器人部署的核心方面。Oanda 交易平台通过其 v20 API 直接或间接提供所有必要的功能来执行以下操作:

  • 检索历史数据

  • 训练和回测交易机器人(深度 Q 学习代理)

  • 实时数据流

  • 下达市场(和限价)订单

  • 使用 SL、TSL 和 TP 订单

  • 以自动化方式部署交易机器人

实施所有这些步骤的先决条件是在 Oanda 拥有一个演示账户,标准硬件和软件(仅开源),以及稳定的互联网连接。换句话说,与在公共街道上部署 AV(自动驾驶车辆)的培训、设计和建造相比,利用算法交易来利用经济效率低下的目的的门槛非常低。换句话说,金融领域在 AI 代理实际部署方面(如本章和前一章中所关注的交易机器人)与其他行业和领域相比具有显著优势。

参考资料

本章引用的书籍和论文:

  • Hilpisch, Yves. 2020 年。《Python 量化交易:从想法到云部署》。Sebastopol:O’Reilly。

  • Litman, Todd. 2020 年。《自动驾驶车辆实施预测》。维多利亚交通政策研究所https://oreil.ly/ds7YM

Python 代码

此部分包含主章节中使用和引用的代码。

Oanda 环境

以下是带有OandaEnv类的 Python 模块,用于基于历史 Oanda 数据训练交易机器人:

#
# Finance Environment
#
# (c) Dr. Yves J. Hilpisch
# Artificial Intelligence in Finance
#
#
import math
import tpqoa
import random
import numpy as np
import pandas as pd

class observation_space:
    def __init__(self, n):
        self.shape = (n,)

class action_space:
    def __init__(self, n):
        self.n = n

    def sample(self):
        return random.randint(0, self.n - 1)

class OandaEnv:
    def __init__(self, symbol, start, end, granularity, price,
                 features, window, lags, leverage=1,
                 min_accuracy=0.5, min_performance=0.85,
                 mu=None, std=None):
        self.symbol = symbol
        self.start = start
        self.end = end
        self.granularity = granularity
        self.price = price
        self.api = tpqoa.tpqoa('../aiif.cfg')
        self.features = features
        self.n_features = len(features)
        self.window = window
        self.lags = lags
        self.leverage = leverage
        self.min_accuracy = min_accuracy
        self.min_performance = min_performance
        self.mu = mu
        self.std = std
        self.observation_space = observation_space(self.lags)
        self.action_space = action_space(2)
        self._get_data()
        self._prepare_data()

    def _get_data(self):
        ''' Method to retrieve data from Oanda.
        '''
        self.fn = f'../../source/oanda/'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        self.fn += f'oanda_{self.symbol}_{self.start}_{self.end}_'  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        self.fn += f'{self.granularity}_{self.price}.csv'  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        self.fn = self.fn.replace(' ', '_').replace('-', '_').replace(':', '_')
        try:
            self.raw = pd.read_csv(self.fn, index_col=0, parse_dates=True)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        except:
            self.raw = self.api.get_history(self.symbol, self.start,
                                       self.end, self.granularity,
                                       self.price)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
            self.raw.to_csv(self.fn)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
        self.data = pd.DataFrame(self.raw['c'])  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
        self.data.columns = [self.symbol]  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)

    def _prepare_data(self):
        ''' Method to prepare additional time series data
            (such as features data).
        '''
        self.data['r'] = np.log(self.data / self.data.shift(1))
        self.data.dropna(inplace=True)
        self.data['s'] = self.data[self.symbol].rolling(self.window).mean()
        self.data['m'] = self.data['r'].rolling(self.window).mean()
        self.data['v'] = self.data['r'].rolling(self.window).std()
        self.data.dropna(inplace=True)
        if self.mu is None:
            self.mu = self.data.mean()
            self.std = self.data.std()
        self.data_ = (self.data - self.mu) / self.std
        self.data['d'] = np.where(self.data['r'] > 0, 1, 0)
        self.data['d'] = self.data['d'].astype(int)

    def _get_state(self):
        ''' Privat method that returns the state of the environment.
        '''
        return self.data_[self.features].iloc[self.bar -
                                    self.lags:self.bar].values

    def get_state(self, bar):
        ''' Method that returns the state of the environment.
        '''
        return self.data_[self.features].iloc[bar - self.lags:bar].values

    def reset(self):
        ''' Method to reset the environment.
        '''
        self.treward = 0
        self.accuracy = 0
        self.performance = 1
        self.bar = self.lags
        state = self._get_state()
        return state

    def step(self, action):
        ''' Method to step the environment forwards.
        '''
        correct = action == self.data['d'].iloc[self.bar]
        ret = self.data['r'].iloc[self.bar] * self.leverage
        reward_1 = 1 if correct else 0  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
        reward_2 = abs(ret) if correct else -abs(ret)  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
        reward = reward_1 + reward_2 * self.leverage  ![10](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/10.png)
        self.treward += reward_1
        self.bar += 1
        self.accuracy = self.treward / (self.bar - self.lags)
        self.performance *= math.exp(reward_2)
        if self.bar >= len(self.data):
            done = True
        elif reward_1 == 1:
            done = False
        elif (self.accuracy < self.min_accuracy and
              self.bar > self.lags + 15):
            done = True
        elif (self.performance < self.min_performance and
              self.bar > self.lags + 15):
            done = True
        else:
            done = False
        state = self._get_state()
        info = {}
        return state, reward, done, info

1

定义数据文件的路径

2

定义数据文件的文件名

3

如果存在相应的数据文件,则读取数据

4

如果不存在这样的文件,则检索 API 的数据

5

将数据写入CSV文件到磁盘

6

选择包含收盘价格的列

7

将列重命名为工具名称(符号)

8

正确预测的奖励

9

实现表现(回报)的奖励

10

预测和表现的综合奖励

矢量化回测

以下是带有助手函数backtest的 Python 模块,用于为深度 Q 学习交易机器人进行矢量化回测生成数据。此代码还用于第十一章:

#
# Vectorized Backtesting of
# Trading Bot (Financial Q-Learning Agent)
#
# (c) Dr. Yves J. Hilpisch
# Artificial Intelligence in Finance
#
import numpy as np
import pandas as pd
pd.set_option('mode.chained_assignment', None)

def reshape(s, env):
    return np.reshape(s, [1, env.lags, env.n_features])

def backtest(agent, env):
    done = False
    env.data['p'] = 0
    state = env.reset()
    while not done:
        action = np.argmax(
            agent.model.predict(reshape(state, env))[0, 0])
        position = 1 if action == 1 else -1
        env.data.loc[:, 'p'].iloc[env.bar] = position
        state, reward, done, info = env.step(action)
    env.data['s'] = env.data['p'] * env.data['r']

Oanda 交易机器人

以下是带有OandaTradingBot类和部署该类的 Python 脚本:

#
# Oanda Trading Bot
# and Deployment Code
#
# (c) Dr. Yves J. Hilpisch
# Artificial Intelligence in Finance
#
import sys
import tpqoa
import pickle
import numpy as np
import pandas as pd

sys.path.append('../ch11/')

class OandaTradingBot(tpqoa.tpqoa):
    def __init__(self, config_file, agent, granularity, units,
                 sl_distance=None, tsl_distance=None, tp_price=None,
                 verbose=True):
        super(OandaTradingBot, self).__init__(config_file)
        self.agent = agent
        self.symbol = self.agent.learn_env.symbol
        self.env = agent.learn_env
        self.window = self.env.window
        if granularity is None:
            self.granularity = agent.learn_env.granularity
        else:
            self.granularity = granularity
        self.units = units
        self.sl_distance = sl_distance
        self.tsl_distance = tsl_distance
        self.tp_price = tp_price
        self.trades = 0
        self.position = 0
        self.tick_data = pd.DataFrame()
        self.min_length = (self.agent.learn_env.window +
                           self.agent.learn_env.lags)
        self.pl = list()
        self.verbose = verbose
    def _prepare_data(self):
        ''' Prepares the (lagged) features data.
 '''
        self.data['r'] = np.log(self.data / self.data.shift(1))
        self.data.dropna(inplace=True)
        self.data['s'] = self.data[self.symbol].rolling(self.window).mean()
        self.data['m'] = self.data['r'].rolling(self.window).mean()
        self.data['v'] = self.data['r'].rolling(self.window).std()
        self.data.dropna(inplace=True)
        self.data_ = (self.data - self.env.mu) / self.env.std
    def _resample_data(self):
        ''' Resamples the data to the trading bar length.
 '''
        self.data = self.tick_data.resample(self.granularity,
                                label='right').last().ffill().iloc[:-1]
        self.data = pd.DataFrame(self.data['mid'])
        self.data.columns = [self.symbol,]
        self.data.index = self.data.index.tz_localize(None)
    def _get_state(self):
        ''' Returns the (current) state of the financial market.
 '''
        state = self.data_[self.env.features].iloc[-self.env.lags:]
        return np.reshape(state.values, [1, self.env.lags, self.env.n_features])
    def report_trade(self, time, side, order):
        ''' Reports trades and order details.
 '''
        self.trades += 1
        pl = float(order['pl'])
        self.pl.append(pl)
        cpl = sum(self.pl)
        print('\n' + 71 * '=')
        print(f'{time} | *** GOING {side} ({self.trades}) ***')
        print(f'{time} | PROFIT/LOSS={pl:.2f} | CUMULATIVE={cpl:.2f}')
        print(71 * '=')
        if self.verbose:
            pprint(order)
            print(71 * '=')
    def on_success(self, time, bid, ask):
        ''' Contains the main trading logic.
 '''
        df = pd.DataFrame({'ask': ask, 'bid': bid, 'mid': (bid + ask) / 2},
                          index=[pd.Timestamp(time)])
        self.tick_data = self.tick_data.append(df)
        self._resample_data()
        if len(self.data) > self.min_length:
            self.min_length += 1
            self._prepare_data()
            state = self._get_state()
            prediction = np.argmax(self.agent.model.predict(state)[0, 0])
            position = 1 if prediction == 1 else -1
            if self.position in [0, -1] and position == 1:
                order = self.create_order(self.symbol,
                        units=(1 - self.position) * self.units,
                        sl_distance=self.sl_distance,
                        tsl_distance=self.tsl_distance,
                        tp_price=self.tp_price,
                        suppress=True, ret=True)
                self.report_trade(time, 'LONG', order)
                self.position = 1
            elif self.position in [0, 1] and position == -1:
                order = self.create_order(self.symbol,
                        units=-(1 + self.position) * self.units,
                        sl_distance=self.sl_distance,
                        tsl_distance=self.tsl_distance,
                        tp_price=self.tp_price,
                        suppress=True, ret=True)
                self.report_trade(time, 'SHORT', order)
                self.position = -1

if __name__ == '__main__':
    agent = pickle.load(open('trading.bot', 'rb'))
    otb = OandaTradingBot('../aiif.cfg', agent, '5s',
                          25000, verbose=False)
    otb.stream_data(agent.learn_env.symbol, stop=1000)
    print('\n' + 71 * '=')
    print('*** CLOSING OUT ***')
    order = otb.create_order(otb.symbol,
                    units=-otb.position * otb.units,
                    suppress=True, ret=True)
    otb.report_trade(otb.time, 'NEUTRAL', order)
    if otb.verbose:
        pprint(order)
    print(71 * '=')

¹ 在特定情境中,这个小技巧可以更快地导致交易,考虑到所使用的数据。对于真实部署,学习环境数据的统计数据将用于标准化。

第五部分:展望

本书的这一部分作为尾声。它展望了人工智能在金融中广泛应用可能带来的后果。与全书一样,它主要关注交易领域,以保持讨论的集中。这最后部分包括两章:

  • 第十三章讨论了金融行业中基于人工智能竞争的各个方面,例如对金融教育的新要求或可能出现的竞争场景。

  • 第十四章考虑了财务奇点的前景以及人工财务智能的出现——通过算法交易持续产生超出人类或机构已知能力的利润的交易机器人。

这部分内容在很大程度上是推测性的,从高层次上讨论,并忽略了许多相关和有趣的细节。然而,它可以作为更深入讨论和分析其中涉及的重要主题的起点。

第十三章:基于 AI 的竞争

如今,人工智能系统运行的一个高风险且极具竞争力的环境是全球金融市场。

尼克·博斯特罗姆(2014)

金融服务公司越来越依赖人工智能,用它来自动化琐碎任务、分析数据、改善客户服务和遵守法规。

尼克·胡伯(2020)

本章讨论了基于系统化和战略性应用 AI 在金融行业中竞争的相关主题。“AI 与金融”作为 AI 可能对金融未来重要性的回顾和总结。“标准化的缺失”认为 AI 在金融中仍处于萌芽阶段,使得在许多情况下实施并不那么简单。然而,这也使得竞争环境对于金融参与者通过 AI 获得竞争优势的机会非常广泛。AI 在金融中的崛起要求重新思考和重塑金融教育和培训。传统金融课程已经无法满足当今的需求。“资源争夺”讨论了金融机构如何争夺必要的资源,以在金融领域大规模应用 AI。与许多其他领域一样,AI 专家往往是金融公司与科技公司、初创公司以及其他行业公司竞争的瓶颈。

“市场影响”解释了 AI 既是“微观阿尔法”的主要原因,又是其唯一解决方案——“微观阿尔法”像当今的黄金一样,仍然可以找到,但通常只在小规模和在许多情况下只能通过工业努力来开采。“竞争场景”讨论了金融行业未来可能出现垄断、寡头或完全竞争的原因和反对意见。最后,“风险、监管和监督”简要概述了 AI 在金融中引发的风险以及监管机构和行业监督机构面临的主要问题。

AI 与金融

本书主要关注 AI 在金融中的应用,特别是在预测金融时间序列方面。其目标是发现统计效率低下的情况,即 AI 算法在预测未来市场走势方面优于基准算法。这种统计效率低下是经济效率低下的基础。经济效率低下要求存在一种交易策略,能够利用统计效率低下的方式实现超过市场的回报。换句话说,存在一种由预测算法和执行算法组成的策略,可以产生阿尔法

当然,还有许多其他领域可以应用 AI 算法到金融中。例如:

信用评分

AI 算法可以用于为潜在借款人推导信用评分,从而支持信用决策甚至完全自动化它们。例如,Golbayani 等人(2020)应用基于神经网络的方法进行公司信用评级,而 Babaev 等人(2019)则在零售贷款申请的情境中使用了 RNN。

欺诈检测

AI 算法可以识别不寻常的模式(例如与信用卡相关的交易),从而防止欺诈未被发现甚至发生。Yousefi 等人(2019)对该主题的文献进行了调查。

交易执行

AI 算法可以学习如何最好地执行与大宗股票相关的交易,从而最小化市场影响和交易成本。Ning 等人(2020)的论文应用了双深度 Q 学习算法来学习最优交易执行策略。

衍生品套期保值

AI 算法可以被训练以最优地执行单个衍生工具或由这些工具组成的投资组合的套期保值交易。这种方法通常被称为深度套期保值。Buehler 等人(2019)应用了强化学习方法来实现深度套期保值。

投资组合管理

AI 算法可以用于组合和重新平衡金融工具组合,比如在长期退休储蓄计划的背景下。López de Prado(2020)最近的著作详细介绍了这个话题。

客户服务

AI 算法可以用于处理自然语言,比如在客户询问的情境中。因此,聊天机器人在金融领域已经像在许多其他行业一样变得非常流行。Yu 等人(2020)的论文讨论了基于流行的双向编码器表示来自变压器(BERT)模型的金融聊天机器人,该模型起源于 Google。

金融领域中 AI 的所有这些应用领域以及这里未列出的其他领域都受益于大量相关数据的程序化可用性。为什么我们可以期望机器学习、深度学习和强化学习算法的表现优于金融计量学中的传统方法,例如 OLS 回归?原因有很多:

大数据

虽然传统的统计方法通常可以处理较大的数据集,但它们在性能方面并不会因数据量的增加而受益太多。另一方面,基于神经网络的方法在针对相关性能指标的较大数据集上进行训练时往往会有巨大的好处。

不稳定性

金融市场与物理世界相比,不遵循恒定的规律。它们随着时间的推移而变化,有时变化迅速。例如,通过在线训练逐渐更新神经网络,AI 算法可以更容易地考虑这一点。

非线性

例如,OLS 回归假设特征与标签数据之间存在固有的线性关系。人工智能算法,如神经网络,通常更容易应对非线性关系。

非正态性

在金融计量学中,假设变量服从正态分布是普遍存在的。总体上,人工智能算法并不那么依赖这种约束性假设。

高维度

金融计量学的传统方法已经证明在低维度问题上非常有用。金融中的许多问题都可以归结为具有相对较低数量特征(自变量)的情境,比如一个(CAPM)或者可能更多一些。更先进的人工智能算法可以轻松处理高维度问题,甚至在需要时考虑到几百个不同的特征。

分类问题

传统金融计量学的工具箱主要基于对估计(回归)问题的方法。这些问题无疑构成了金融中的一个重要类别。但是,分类问题可能同样重要。机器和深度学习的工具箱为解决分类问题提供了大量选项。

非结构化数据

金融计量学的传统方法基本上只能处理结构化的数值数据。机器和深度学习算法还能够有效处理非结构化的文本数据。它们还能够同时高效处理结构化和非结构化数据。

尽管人工智能的应用在金融的许多领域仍处于萌芽阶段,但一些应用领域已经被证明从向以 AI 为先的金融范式转变中获益良多。因此,可以相对安全地预测,机器、深度和强化学习算法将显著重塑金融实践中的处理方式和方式。此外,人工智能已成为追求竞争优势的头号工具。

缺乏标准化

传统的规范金融(见 第三章)已经达到了高度的标准化。有许多不同形式水平的教材可以用于教授和解释完全相同的理论和模型。在这个背景下的两个例子是 Copeland 等人(2005)和 Jones(2012)。而这些理论和模型又通常依赖于前几十年发表的研究论文。

当 Black 和 Scholes(1973)以及 Merton(1973)发表他们的理论和模型,用封闭形式的分析公式定价欧式期权合同时,金融行业立即接受了该公式及其背后的理念作为基准。近 50 年来,虽然期间出现了许多改进的理论和模型,但 Black-Scholes-Merton 模型和公式仍被认为是期权定价的一个基准,如果不是唯一的基准。

另一方面,AI 首先的金融缺乏显著的标准化。每天都有大量研究论文发布(例如,http://arxiv.org)。这是因为传统的同行评审出版渠道通常速度太慢,跟不上 AI 领域的快速发展。研究人员急于尽快与公众分享他们的工作,通常是为了不被竞争团队超越。同行评审过程虽然在质量保证方面有其优点,但可能需要数月时间,期间研究成果无法发布。从这个意义上说,研究人员越来越信任社区处理评审,同时确保他们的发现能够早日获得认可。

几十年前,新的金融工作论文在同行评审和最终发表之前通常在专家间流传几年是司空见惯的事,而今天的研究环境则以更快的周转时间和研究人员愿意尽早发布未经彻底评审和他人测试的工作为特征。因此,对于正在应用于金融问题的众多 AI 算法,几乎没有任何标准或基准实现可用。

这些快速的研究发布周期在很大程度上受到 AI 算法易于应用于金融数据的影响。与几十年前经济计量研究的限制相比(例如有限的数据可用性和有限的计算能力),学生、研究人员和从业者几乎不需要更多的工具就能将 AI 的最新突破应用到金融领域。这在某种程度上是一种优势,但也常常导致“尽可能多地试探”的想法,希望能有所发现。

在某种程度上,投资者的热切和紧迫感也推动投资经理以更快的速度提出新的投资方法。这通常需要放弃传统的金融研究方法,转向更实用的方法。正如 Lopéz de Prado(2018)所说:

问题:数学证明可能需要数年、数十年甚至数个世纪。没有投资者会等那么长时间。

解决方案:使用实验数学。通过实验解决艰难的难题,而不是通过证明。

总体而言,缺乏标准化为单一金融参与者提供了充分的机会,在竞争环境中利用 AI 首先的金融优势。在 2020 年中期撰写本文时,感觉利用 AI 彻底改变金融方法的竞赛正在全速进行中。本章剩余部分讨论了超出本节和前节内容的基于 AI 的竞争重要方面。

教育与培训

进入金融领域和金融行业通常是通过在该领域的正规教育。典型的学位名称如下:

  • 金融硕士

  • 量化金融硕士

  • 金融计算硕士

  • 金融工程硕士

  • 量化企业风险管理硕士

事实上,今天所有这些学位都要求学生至少掌握一种编程语言,通常是 Python,以满足数据驱动金融的数据处理需求。在这方面,大学满足了行业对这些技能的需求。Murray(2019)指出:

随着公司在更多任务中使用人工智能,工作人员将不得不适应。

[T]Masters in Finance (MiF) 毕业生有机会。技术和金融知识的结合是一个甜蜜点。

或许需求最高的是使用人工智能来搜索市场和庞大数据集以识别潜在交易的量化投资者。

调整金融相关学位课程的不仅仅是大学,还有公司本身,他们也在为新员工和现有员工投入大量培训计划,以应对数据驱动和以人工智能为先的金融。Noonan(2018)描述了摩根大通这个全球最大银行之一的大规模培训努力:

摩根大通正在对数百名新投资银行家和资产经理进行强制性编码课程培训,这表明华尔街对技术技能的需求增加。

随着技术从人工智能交易到在线贷款平台塑造银行业未来,金融服务集团正在开发软件,帮助它们提高效率、创造创新产品并抵御来自初创企业和科技巨头的威胁。

今年的初级编程培训基于 Python 编程,这将帮助他们分析非常大的数据集并解释非结构化数据,如自由语言文本。明年,资产管理部门将扩展强制性技术培训,包括数据科学概念、机器学习和云计算。

简而言之,金融行业中越来越多的角色将需要精通编程、基础和高级数据科学概念、机器学习以及其他技术方面,如云计算。买方和卖方的大学和金融机构对这一趋势做出反应,通过调整课程和大量投资培训他们的员工。在这两种情况下,这是有效竞争——甚至是保持相关性并能够在金融景观中生存的问题——因为人工智能的重要性日益增加,已经永久改变了这一领域。

争夺资源

在金融领域大规模应用人工智能的过程中,金融市场的参与者们竞争获取最佳资源。四大主要资源至关重要:人力资源、算法、数据和硬件。

可能是最重要且同时也是最稀缺的资源是通晓 AI 技术,尤其是金融领域的专家。在这方面,金融机构与科技公司、金融科技(fintech)初创公司以及其他群体竞争获取顶尖人才。虽然银行通常愿意支付相对较高的薪水给这类专家,但科技公司的文化因素和例如初创公司的股票期权承诺可能会使他们难以吸引顶尖人才。因此,金融机构通常采取内部培养人才的方式。

许多机器学习和深度学习中的算法和模型可以被视为经过充分研究、测试和记录的标准算法。然而,在许多情况下,从一开始就不清楚如何在金融背景下最佳应用它们。这就是为什么金融机构在研究工作中投入重大资源的原因。对于许多较大的买方机构,如系统化对冲基金,投资和交易策略研究是其业务模型的核心。然而,如第十二章所示,执行和生产同样重要。在这种情况下,策略研究和部署当然都是高度技术性的学科。

算法如果没有数据往往毫无价值。同样,使用来自典型数据源(如交易所或数据服务提供商如 Refinitiv 或 Bloomberg)的“标准”数据的算法可能只有有限的价值。这是因为这类数据被市场上许多,如果不是所有相关参与者密集分析,很难甚至不可能识别出创造超额收益的机会或类似的竞争优势。因此,大型买方机构特别投资大量资源获取替代数据(参见“数据可用性”)。

当今人们认为替代数据有多么重要,这反映在买方机构和其他投资者对这一领域活跃公司的投资中。例如,2018 年一群投资公司向数据公司 Enigma 投资了 9500 万美元。Fortado(2018)描述了这笔交易及其基本原理如下:

对冲基金、银行和风险投资公司正在大举投资数据公司,希望通过这一业务获取更多利润,因为他们自身也在大量使用这些数据。

近年来,涌现出大量初创公司,它们遍历海量数据并将其出售给寻求优势的投资群体。

最近吸引投资者兴趣的是 Enigma,这是一家总部位于纽约的初创企业,它从包括量化巨头 Two Sigma、激进对冲基金 Third Point 以及风险投资公司 NEA 和 Glynn Capital 在内的多方资金中获得了 9500 万美元的资本,这项融资于周二宣布。

金融机构竞争的第四个资源是处理大数据的最佳硬件选择,实施基于传统和替代数据集的算法,并有效地将人工智能应用于金融领域。近年来,专门用于加快机器学习和深度学习工作的硬件创新巨大,更加能效和成本效益显著。尽管传统处理器如 CPU 在该领域中角色较小,但专用硬件如Nvidia 的 GPU,以及像Google 的 TPU初创企业 Graphcore 的 IPU等新选项已在人工智能领域占据主导地位。金融机构对新型专用硬件的兴趣,例如 Citadel 这样的最大对冲基金和市场制造商对 IPU 的研究努力有所体现。其努力详细记录在全面的研究报告 Jia 等人(2019)中,展示了专用硬件相对于替代选项的潜在优势。

在以人工智能为先的金融竞争中,金融机构每年投资数十亿美元用于人才、研究、数据和硬件。虽然大型机构似乎已经在该领域保持了领先地位,但较小或中型玩家将发现全面转向以人工智能为先的业务模式非常困难。

市场影响

数据科学、机器学习和深度学习算法在金融行业的日益广泛使用无疑对金融市场、投资和交易机会产生影响。正如本书中的许多例子所示,机器学习和深度学习方法能够发现传统计量经济学方法(如多元 OLS 回归)无法发现的统计和经济效率低下的现象。因此,可以认为新的和更好的分析方法使得发现产生 Alpha 收益的机会和策略变得更加困难。

比较金融市场当前情况与黄金采矿的情况,Lopéz de Prado(2018)如下描述:

十年前,相对普遍能够通过简单的数学工具(如计量经济学)发现宏观 alpha(即超额收益),但如今这种机会迅速趋近于零。无论个人的经验或知识如何,现今寻找宏观 alpha 的机会都非常渺茫。唯一真正的 alpha 是微观的,而要找到它则需要资本密集型的工业方法。就像黄金一样,微观 alpha 并不意味着总体利润减少。今天的微观 alpha 比历史上的宏观 alpha 丰富得多。可以赚大钱,但你需要使用重型机器学习工具。

在这种背景下,金融机构似乎必须采纳首先 AI 金融,以免落后甚至可能倒闭。这不仅适用于投资和交易,还包括其他领域。尽管银行在历史上一直与商业和零售债务人保持长期关系,并且自然地建立了他们做出明智信贷决策的能力,但如今 AI 使比赛更加公平,并几乎使长期关系变得毫无价值。因此,依赖 AI 的新进入者(如金融科技初创公司)往往可以以受控的、可行的方式迅速从现有者手中夺取市场份额。另一方面,这些发展激励现有者收购和合并年轻、创新的金融科技初创公司以保持竞争力。

竞争场景

展望未来,比如说三到五年后,由首先 AI 驱动的竞争格局可能会是什么样子?有三种场景可以想象:

垄断

一个金融机构通过在算法交易等领域实现重大、无与伦比的 AI 应用突破,达到了主导地位。例如,这种情况在互联网搜索中就很明显,谷歌全球市场份额约为 90%。

寡头垄断

较少数的金融机构能够利用首先 AI 来达到领先地位。例如,在对资产管理方面,对冲基金行业也存在寡头垄断,少数几家大型参与者主导了该领域。

完全竞争

所有金融市场的参与者都在类似的方式下从首先 AI 金融的进展中受益。在技术上看,这类似于当前的计算机国际象棋局面。一些国际象棋程序在标准硬件(如智能手机)上运行,对比当前世界冠军(本文撰写时的马格努斯·卡尔森)来说,它们显著更擅长下国际象棋。

很难预测哪种情景更有可能发生。可以找到各种论点并描述所有三种可能的路径。例如,支持垄断的一个论点可能是,例如在算法交易方面的重大突破可能导致快速显著的超额表现,通过再投资和新的资金流入积累更多资本。这反过来增加了可用的技术和研究预算,以保护竞争优势,并吸引本来难以获得的人才。整个周期是自我强化的,而谷歌在搜索领域的例子,结合核心在线广告业务,正是在这种情况下的一个很好的例子。

同样,有充分的理由预期寡头垄断。目前可以安全地假设,任何交易业务中的大型参与者都在研究和技术上投入巨资,AI 相关的倡议占据了预算的重要部分。就像在其他领域一样,比如推荐引擎——想想亚马逊的书籍、Netflix 的电影和 Spotify 的音乐——多家公司可能能够同时达到类似的突破。可以想象,当前领先的系统性交易商将能够利用 AI 优先金融来巩固其领先地位。

最后,多年来,许多技术已经变得无处不在。强大的国际象棋程序只是一个例子。其他可能是地图和导航系统或基于语音的个人助理。在完全竞争的情况下,大量金融参与者将竞争创造微不足道的 Alpha 机会,甚至可能无法产生与纯市场回报有所不同的回报。

与此同时,也有反对这三种情况的论点。当前的格局中有许多拥有相同手段和激励的参与者,可以利用人工智能在金融领域中。这使得只有一个单一参与者能够在投资管理中脱颖而出,并且占据与谷歌在搜索中相媲美的市场份额的可能性较小。同时,进行研究的小型、中型和大型参与者的数量以及算法交易中的低准入壁垒,使得很少有几个人能够确保可持续的竞争优势。反对完全竞争的论点是,在可预见的未来,大规模算法交易需要大量资本和其他资源。关于国际象棋,DeepMind 通过 AlphaZero 展示了即使一个领域几乎看似已经定型,仍然存在创新和显著改进的空间。

风险、监管和监督

一项简单的谷歌搜索显示,关于人工智能风险及其在一般情况下以及在金融服务行业中的监管问题,存在着活跃的讨论。¹ 在此背景下,本节不能涵盖所有相关方面,但至少可以处理几个重要的方面。

以下是金融中应用人工智能引入的一些风险:

隐私

金融是一个有严格隐私法的敏感领域。大规模使用 AI 需要使用至少部分来自客户的私密数据。这增加了私密数据可能泄露或以不适当方式使用的风险。当使用公开可用数据源(例如金融时间序列数据)时,这种风险显然是不存在的。

偏见

AI 算法很容易学习与数据相关的偏见,例如与零售或公司客户相关的偏见。例如,算法在评估潜在债务人的信用价值时只能像数据允许的那样优秀和客观。²再次强调,在处理市场数据时,学习偏见的问题实际上并不成问题。

不可解释性

在许多领域,重要的是决策可以被解释,有时需要详细说明并事后解释。这可能是法律要求,也可能是投资者希望理解为什么做出特定投资决策的原因。以投资和交易决策为例。如果基于大型神经网络的人工智能在算法上决定何时以及如何进行交易,详细解释为什么 AI 进行了这样的交易通常会非常困难,甚至不可能。研究人员正在积极而 intensively 地致力于“可解释的 AI”,但在这方面显然存在明显的限制。

群体效应

自 1987 年股市崩盘以来,金融交易中的群体效应风险显而易见。1987 年,在大规模合成复制看跌期权计划及止损订单的背景下,正反馈交易引发了向下螺旋。类似的群体效应也可在 2008 年的对冲基金崩盘中观察到,这是首次揭示了不同对冲基金实施类似策略的程度。至于 2010 年的闪电崩盘,有人指责算法交易,但证据似乎不明确。然而,在交易中更广泛使用人工智能可能带来类似的风险,当越来越多的机构采用已被证明有效的相似方法时。其他领域也容易受到这种效应的影响。信用决策机构可能会基于不同的数据集学习相同的偏见,并可能会使某些群体或个人根本无法获得信贷。

Alpha 消失

正如先前所述,人工智能在金融领域越来越广泛的应用可能会使市场上的 Alpha 消失。技术必须变得更加先进,数据可能会变得“更加替代”,以确保任何竞争优势。第十四章在潜在的金融奇点背景下更详细地探讨了这一点。

除了 AI 的典型风险之外,AI 引入了特定于金融领域的新风险。同时,对于立法者和监管者来说,跟上该领域的发展并全面评估源自以 AI 为先导的金融的个别和系统风险是困难的。这其中有几个原因:

知识产权

法律制定者和监管者需要像金融参与者本身一样,获得与金融 AI 相关的新知识。在这方面,他们与知名的金融机构和技术公司竞争,后者以远高于法律制定者和监管者的可能薪资水平聘请人才。

数据不足

在许多应用领域,监管者可以用来评估 AI 实际影响的数据简直少之又少,甚至可能根本不知道 AI 是否起到作用。即使已知道 AI 的作用并且数据可能可用,也很难将 AI 的影响与其他相关因素的影响分开。

缺乏透明度

尽管几乎所有金融机构都试图利用 AI 来确保或获取竞争优势,但单个机构在这方面的具体做法和实施方式很少被透明化。许多机构将他们在这一背景下的努力视为知识产权和他们自己的“秘密配方”。

模型验证

模型验证在许多金融领域是一个中心风险管理和监管工具。以欧式期权定价为例,基于 Black-Scholes-Merton(1973)期权定价模型。模型的特定实现所生成的价格可以通过使用 Cox 等人(1979)的二项式期权定价模型进行验证,反之亦然。然而,这在 AI 算法中通常大不相同。几乎没有一个模型可以基于简洁的参数集验证复杂 AI 算法的输出。可复制性可能是一个可以实现的目标(即,第三方可以验证输出,前提是能够精确复制所有涉及步骤)。但是,这又需要第三方,比如监管机构或审计师,能够获得相同的数据,拥有与金融机构使用的强大基础设施等。对于较大的 AI 项目,这似乎是不现实的。

难以规范

返回到期权定价示例,监管机构可以指定黑-斯科尔斯-默顿(1973 年)和科克斯等人(1979 年)的期权定价模型均可接受用于欧洲期权的定价。即使立法者和监管机构指定支持向量机(SVM)算法和神经网络都是“可接受的算法”,这仍然未明确这些算法如何训练、使用等等。在这种情况下很难更加具体。例如,监管机构是否应限制神经网络中的隐藏层和/或隐藏单元的数量?软件包又该如何使用?这类问题的清单似乎是无穷无尽的。因此,只会制定一般规则。

技术公司和金融机构通常更喜欢对人工智能监管采取更为宽松的态度——通常这是显而易见的原因。在布拉德肖(2019 年)中,谷歌 CEO 桑达尔·皮查伊谈到“智能”监管,并呼吁区分不同行业的处理方法:

谷歌首席执行官警告政客不要对人工智能进行膝反射式监管,认为现有规则可能足以管理这种新技术。

桑达尔·皮查伊表示,人工智能需要“智能监管”,既要促进创新,又要保护公民……“这是一种非常广泛的跨界技术,因此在特定垂直情况下更加重视[监管]非常重要,”皮查伊先生说。

另一方面,像埃隆·马斯克在马特约斯(2020 年)中那样,有支持更为严格的人工智能监管的知名支持者。

“记住我的话,”马斯克警告道。“人工智能比核武器更加危险。那么,为什么我们没有监管监督呢?”

金融领域人工智能的风险多种多样,立法者和监管机构面临的问题也是如此。尽管如此,可以预测,在许多司法管辖区内,一定会出台更加严格的规定和监督,特别是涉及金融领域的人工智能。

结论

本章讨论了在金融行业中使用人工智能来竞争的方面。在许多应用领域,其好处是显而易见的。然而,到目前为止,几乎没有建立任何标准,该领域似乎仍然对参与者寻求竞争优势敞开大门。由于数据科学、机器学习、深度学习等新技术和方法几乎渗透到任何金融学科中,因此金融教育和培训必须考虑到这一点。许多硕士课程已经调整了他们的课程设置,而大型金融机构则大量投资于培训新员工和现有员工所需的技能。除了人力资源外,金融机构还在该领域竞争其他资源,如替代数据。在金融市场上,由人工智能驱动的投资和交易使得识别可持续的阿尔法机会更加困难。另一方面,使用传统的计量经济学方法,可能今天无法识别和开发微小的阿尔法。

难以预测在人工智能主导下的金融行业的竞争性终结场景。从垄断到寡头垄断再到完全竞争的各种场景仍然是合理的。第十四章重新讨论了这个话题。AI 优先金融让研究人员、从业者和监管机构在适当地处理这些风险方面面临新的风险和挑战。在许多讨论中占据显著地位的一种风险是许多 AI 算法的黑盒特性。这种风险通常只能在今天最先进的可解释 AI 技术的一定程度上得到缓解。

参考文献:

本章引用的书籍、论文和文章:

  • Babaev, Dmitrii 等人。2019 年。《E.T.-RNN:将深度学习应用于信贷贷款申请》。https://oreil.ly/ZK5G8

  • Black, Fischer 和 Myron Scholes。1973 年。《期权定价和公司责任》。《政治经济学杂志》81(3):638–659。

  • Bradshaw, Tim。2019 年。《谷歌首席执行官桑达尔·皮查伊警告不要急于制定 AI 法规》。《金融时报》,2019 年 9 月 20 日。

  • Bostrom, Nick。2014 年。《超智能:路径、危险、策略》。牛津:牛津大学出版社。

  • Buehler, Hans 等人。2019 年。《深度对冲:利用强化学习在一般市场摩擦下对冲衍生品》。金融研究论文第 19-80 号。https://oreil.ly/_oDaO

  • Copeland, Thomas, Fred Weston 和 Kuldeep Shastri。2005 年。《财务理论与公司政策》。第 4 版。波士顿:皮尔逊。

  • Cox, John, Stephen Ross 和 Mark Rubinstein。1979 年。《期权定价:一种简化方法》。《金融经济学杂志》7(3):229–263。

  • Fortado, Lindsay。2018 年。《数据专家恩格玛吸引投资集团现金》。《金融时报》,2018 年 9 月 18 日。

  • Golbayani, Parisa, Dan Wang 和 Ionut Florescu。2020 年。《应用深度神经网络评估企业信用评级》。https://oreil.ly/U3eXF

  • Huber, Nick。2020 年。《人工智能在金融服务领域的潜力仅仅触及了表面》。《金融时报》,2020 年 7 月 1 日。

  • Jia, Zhe 等人。2019 年。《通过微基准测试剖析 Graphcore IPU 架构》。https://oreil.ly/3ZgTO

  • Jones, Charles P. 2012 年。《投资:分析与管理》。第 12 版。霍博肯:约翰·威利与儿子公司。

  • Klein, Aaron。2020 年。《减少 AI 在金融服务中的偏见》。布鲁金斯学会报告,2020 年 7 月 10 日。https://bit.ly/aiif_bias

  • López de Prado, Marcos。2018 年。《金融机器学习的进展》。霍博肯:威利金融。

  • ⸻。2020 年。《资产管理的机器学习》。剑桥:剑桥大学出版社。

  • Matyus, Allison。2020 年。《埃隆·马斯克警告所有 AI 必须受到监管,甚至在特斯拉》。《数字趋势》,2020 年 2 月 18 日。https://oreil.ly/JmAKZ

  • Merton, Robert C. 1973. “理性期权定价理论。” 《贝尔经济与管理科学期刊》 4 (春季):141–183。

  • Murray, Seb. 2019. “Tech 和 Finance 技能毕业生需求旺盛。” 《金融时报》,2019 年 6 月 17 日。

  • Ning, Brian,Franco Ho Ting Lin 和 Sebastian Jaimungal。2020. “双深度 Q 学习用于最优执行。” https://oreil.ly/BSBNV

  • Noonan, Laura。2018. “JPMorgan 对新员工的要求:编程课程。” 《金融时报》,2018 年 10 月 8 日。

  • Yousefi, Niloofar,Marie Alaghband 和 Ivan Garibay。2019. “关于机器学习技术和用户身份验证方法在信用卡欺诈检测中的综合调查。” https://oreil.ly/fFjAJ

  • Yu, Shi,Yuxin Chen 和 Hussain Zaidi。2020. “AVA:基于深度双向 Transformer 的金融服务聊天机器人。” https://oreil.ly/2NVNH

¹ 关于这些主题的简要概述,请参阅麦肯锡的这些文章:应对人工智能的风险降低机器学习和人工智能的风险

² 关于通过 AI 导致偏见及其解决方案的更多信息,请参阅 Klein (2020)。

第十四章:金融奇点

我们发现自己身处战略复杂性的丛林中,被浓雾的不确定性所包围。

Nick Bostrom (2014)

“大多数交易和投资角色将消失,随着时间的推移,可能大多数需要人类服务的角色都将被自动化。”斯金纳先生说。“最终你将得到的是主要由管理者和机器运行的银行。管理者决定机器需要做什么,然后机器就会执行。”

Nick Huber (2020)

人工智能在金融行业的竞争是否会导致金融奇点?这是本章讨论的主要问题。它从“概念与定义”开始,定义了诸如金融奇点人工财务智能(AFI)等表达。“利益所在”说明了在争夺 AFI 的竞赛中,可能积累的潜在财富。“通往金融奇点的路径”考虑了在第二章的背景下,可能导致 AFI 的路径。“正交技能与资源”认为有一些资源对于创造 AFI 的目标至关重要且正交。参与 AFI 竞争的任何人都将争夺这些资源。最后,“星际迷航还是星球大战”考虑到本章讨论的 AFI 是否只会使少数人受益还是整个人类受益。

概念与定义

表达金融奇点至少可以追溯到 2015 年希勒的博客文章。在这篇文章中,希勒写道:

每一种投资策略的 alpha 最终是否会趋近于零?更根本地,由于如此多的聪明人和更聪明的计算机,财务市场是否真的会变得完美,我们可以放心地坐下来,假设所有资产的定价都是正确的?

这种想象中的状态可能被称为金融奇点,类似于假设中的未来技术奇点,即计算机取代人类智能的情况。金融奇点意味着所有投资决策最好交给计算机程序,因为专家和他们的算法已经找出了驱动市场结果的因素,并将其简化为无缝系统。

更广义地说,可以将金融奇点定义为计算机和算法开始接管金融及其整个行业的时间点,包括银行、资产管理公司、交易所等,而人类则作为管理者、监管者和控制者,如果有的话。

另一方面,可以定义金融奇点——根据本书的关注点,这是一个时刻,从这一时刻起,一个交易机器人展示了一种持续预测金融市场运动的能力,达到了超人类和超机构水平,并具有前所未有的准确性。在这种意义上,这样的交易机器人将被描述为人工狭义智能(ANI),而不是人工通用智能(AGI)或超智能(见第二章)。

可以假设,建立这样一个以交易机器人形式存在的 AFI 比建立 AGI 甚至超智能要容易得多。这一点对 AlphaZero 也是成立的,因为构建一个超过任何人类或其他代理人在下围棋游戏中的 AI 代理更容易。因此,即使目前尚不清楚是否会有符合 AGI 或超智能标准的 AI 代理存在,但无论如何,更有可能会出现一个符合 ANI 或 AFI 标准的交易机器人。

接下来,重点放在一个被定义为 AFI 的交易机器人上,以尽可能具体地讨论并嵌入本书的背景。

究竟是什么在危险中?

追求 AFI 可能本身就是具有挑战性和令人兴奋的。然而,如同金融界的通常情况一样,很少有倡议是出于利他动机驱动的;相反,大多数是由财务激励驱动(也就是说,硬现金)。但在建立 AFI 的竞赛中究竟有什么风险?这个问题不能确定地或一般地回答,但一些简单的计算可以为这个问题提供一些启示。

要了解拥有 AFI 相对于较差交易策略的价值,考虑以下基准:

牛市策略

一种仅在预期价格上涨时对金融工具采取多头交易策略。

随机策略

一种为特定金融工具随机选择多头或空头位置的交易策略。

熊市策略

一种仅在预期价格下跌时对金融工具采取空头交易策略。

这些基准策略将与具有以下成功特征的 AFI 进行比较:

前 X%

AFI 在上升和下跌的前 X% 运动中预测正确,其余市场运动则随机预测。

X% 的 AFI

AFI 通过随机选择的市场运动的 X% 部分正确预测,其余市场运动则随机预测。

以下 Python 代码导入已知的时间序列数据集,其中包含多种金融工具的 EOD 数据。接下来的示例依赖于单一金融工具五年的 EOD 数据:

In [1]: import random
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'

In [2]: url = 'https://hilpisch.com/aiif_eikon_eod_data.csv'

In [3]: raw = pd.read_csv(url, index_col=0, parse_dates=True)

In [4]: symbol = 'EUR='

In [5]: raw['bull'] = np.log(raw[symbol] / raw[symbol].shift(1))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [6]: data = pd.DataFrame(raw['bull']).loc['2015-01-01':]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [7]: data.dropna(inplace=True)

In [8]: data.info()
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 1305 entries, 2015-01-01 to 2020-01-01
        Data columns (total 1 columns):
         #   Column  Non-Null Count  Dtype
        ---  ------  --------------  -----
         0   bull    1305 non-null   float64
        dtypes: float64(1)
        memory usage: 20.4 KB

1

牛市 基准回报(仅多头)

由于牛市策略已经由基础金融工具的对数回报定义,以下 Python 代码指定了另外两个基准策略,并为 AFI 策略的表现进行了推导。在这种情况下,考虑了多种 AFI 策略以说明 AFI 预测准确性提高的影响:

In [9]: np.random.seed(100)

In [10]: data['random'] = np.random.choice([-1, 1], len(data)) * data['bull']  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [11]: data['bear'] = -data['bull']  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [12]: def top(t):
             top = pd.DataFrame(data['bull'])
             top.columns = ['top']
             top = top.sort_values('top')
             n = int(len(data) * t)
             top['top'].iloc[:n] = abs(top['top'].iloc[:n])
             top['top'].iloc[n:] = abs(top['top'].iloc[n:])
             top['top'].iloc[n:-n] = np.random.choice([-1, 1],
                             len(top['top'].iloc[n:-n])) * top['top'].iloc[n:-n]
             data[f'{int(t * 100)}_top'] = top.sort_index()

In [13]: for t in [0.1, 0.15]:
             top(t)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [14]: def afi(ratio):
             correct = np.random.binomial(1, ratio, len(data))
             random = np.random.choice([-1, 1], len(data))
             strat = np.where(correct, abs(data['bull']), random * data['bull'])
             data[f'{int(ratio * 100)}_afi'] = strat

In [15]: for ratio in [0.51, 0.6, 0.75, 0.9]:
             afi(ratio)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

1

随机 基准回报

2

熊市 基准回报(仅做空)

3

X% top 策略的回报

4

X% AFI 策略的回报

使用引入的标准向量化回测方法,如第十章所介绍的(忽略交易成本),可以清楚地看出预测准确率显著提高在金融方面意味着什么。考虑到“90% AFI”,它在预测上并不完美,而是在所有情况的 10%中缺乏任何优势。假设 90%的准确性导致在五年内净回报几乎是投资资本的 100 倍(在交易成本之前)。以 75%的准确性,AFI 仍将返回几乎 50 倍的投资资本(见图 14-1)。这不包括杠杆,杠杆可以在这种预测准确率存在的情况下几乎无风险地增加:

In [16]: data.head()
Out[16]:                 bull    random      bear    10_top    15_top    51_afi  \
         Date
         2015-01-01  0.000413 -0.000413 -0.000413  0.000413 -0.000413  0.000413
         2015-01-02 -0.008464  0.008464  0.008464  0.008464  0.008464  0.008464
         2015-01-05 -0.005767 -0.005767  0.005767 -0.005767  0.005767 -0.005767
         2015-01-06 -0.003611 -0.003611  0.003611 -0.003611  0.003611  0.003611
         2015-01-07 -0.004299 -0.004299  0.004299  0.004299  0.004299  0.004299

                       60_afi    75_afi    90_afi
         Date
         2015-01-01  0.000413  0.000413  0.000413
         2015-01-02  0.008464  0.008464  0.008464
         2015-01-05  0.005767 -0.005767  0.005767
         2015-01-06  0.003611  0.003611  0.003611
         2015-01-07  0.004299  0.004299  0.004299

In [17]: data.sum().apply(np.exp)
Out[17]: bull       0.926676
         random     1.097137
         bear       1.079126
         10_top     9.815383
         15_top    21.275448
         51_afi    12.272497
         60_afi    22.103642
         75_afi    49.227314
         90_afi    98.176658
         dtype: float64

In [18]: data.cumsum().apply(np.exp).plot(figsize=(10, 6));

aiif 1401

图 14-1. 基准和理论 AFI 策略的毛收益随时间变化

分析表明,虽然我们做了几个简化假设,但情况很严峻。时间在这个背景下起着重要作用。在一个 10 年的时间段内重新实施相同的分析使得这些数字变得更加令人印象深刻——在交易环境中几乎是难以想象的。正如以下输出所示,“90% AFI”的毛收益将是投资资本的 16,000 多倍(在交易成本之前)。复利和再投资的效应是巨大的:

bull          0.782657
random        0.800253
bear          1.277698
10_top      165.066583
15_top     1026.275100
51_afi      206.639897
60_afi      691.751006
75_afi     2947.811043
90_afi    16581.526533
dtype: float64

通向金融奇点的路径

AFI 的出现将是一个非常具体的事件,发生在一个非常特定的环境中。例如,并不需要模仿人类大脑,因为 AGI 或超级智能并非主要目标。考虑到没有一个人似乎在金融市场上的交易中始终比其他人表现更出色,试图模仿人脑以实现 AFI 甚至可能是一条死胡同。也无需担心具象化的问题。AFI 可以作为软件存在,仅需连接到所需的数据和交易 API 的适当基础设施上。

另一方面,由于问题的本质,人工智能似乎是通往人工智能金融机构的一个有前途的路径:接受大量金融和其他数据,并生成关于未来价格走势的预测。这正是本书中介绍和应用的算法所关注的内容——特别是那些属于监督学习和强化学习类别的算法。

另一个选项可能是人类和机器智能的混合。虽然机器几十年来一直支持人类交易员,在许多情况下,角色已经发生了变化。人类通过提供理想的环境和最新的数据,在交易中支持机器,仅在极端情况下进行干预等。在许多情况下,机器已经完全独立地进行其算法交易决策。或者像文艺复兴技术公司创始人之一、最成功和最神秘的系统性交易对冲基金的吉姆·西蒙斯所说:“我们唯一的规则是我们永远不会覆盖计算机。”

尽管目前尚不清楚哪些路径可能导致超智能,但从今天的角度来看,人工智能最有可能铺平通往金融奇点和人工智能金融机构的道路。

正交技能与资源

第十三章讨论了在金融行业基于人工智能竞争背景下的资源竞争。在这一背景下,主要的四大资源包括人力资源(专家)、算法与软件、金融与替代数据,以及高性能硬件。在这种情况下,我们可以添加第五个资源,即获取其他资源所需的资本。

根据正交假设,获取这些正交技能和资源是明智的,甚至是必不可少的,无论 AFI 如何实现。参与构建 AFI 竞赛的金融机构将尽可能获取尽可能多的高质量资源,并合理地为清晰路径至少一条AFI 而定位自己尽可能有利。

在由人工智能驱动的金融世界中,这种行为和定位可能决定是繁荣、仅仅生存,还是离开市场。不能排除进展可能比预期快得多的可能性。尼克·博斯特罗姆在 2014 年预测,可能需要 10 年才能让人工智能在围棋比赛中击败世界冠军,基本上没有人预料到只需两年就能实现。主要的推动力是在应用强化学习到这类游戏中取得突破,至今仍受益于其他应用。金融领域也不能排除这类意外突破的可能性。

情景之前和之后

可以安全地假设,世界各地的每个主要金融机构和许多其他非金融实体目前都在研究并具有应用于金融的人工智能的实际经验。然而,并非所有金融行业的参与者都能同等地在第一个交易 AFI 到达之前做好准备。一些,如银行,受到监管要求的限制。其他一些则遵循不同的商业模式,例如交易所。另一些,如某些资产管理者,专注于提供低成本、商品化的投资产品,如 ETF,模仿更广泛市场指数的表现。换句话说,对每个金融机构来说,生成阿尔法并不是主要目标。

从外部看来,较大的对冲基金似乎是最有利于充分利用以人工智能为先的金融和 AI 驱动的算法交易的位置。总的来说,他们已经拥有了在这一领域中重要的许多所需资源:才华横溢且受过良好教育的人才,对交易算法的经验,几乎无限的传统和替代数据来源访问权限,以及可扩展的专业交易基础设施。如果缺少某些东西,大额的技术预算可以确保快速和有针对性的投资。

目前尚不清楚是否会有一个 AFI 首先出现,其它随后出现,或者是否可能同时出现几个 AFI。如果有几个 AFI 存在,我们可能会称之为多极或寡头垄断的情况。这些 AFI 可能主要会互相竞争,而“非-AFI”参与者可能会被边缘化。项目的发起者会努力获取优势,哪怕是微小的优势,因为这可能使一个 AFI 完全占据主导地位,并最终成为独占市场的单一者。

也可以想象从一开始就可能出现“赢家通吃”的情况。在这种情况下,一个单一的 AFI 出现并迅速达到了在金融交易中无法与任何其他竞争者匹敌的主导地位。这可能有几个原因。一个原因可能是第一个 AFI 产生了如此惊人的回报,以至于管理的资产以惊人的速度膨胀,导致预算不断增加,进而使其能够获取越来越多的相关资源。另一个原因可能是第一个 AFI 迅速达到了一个规模,在这个规模下,其行动可能对市场价格产生影响——例如,有能力操纵市场价格——从而使其成为金融市场的主要,甚至是唯一的推动力。

规定理论上可以防止一个人工智能金融机构(AFI)变得过于庞大或者获得过多的市场力量。主要问题在于这些法律在实践中是否可执行,以及它们需要如何设计才能达到预期效果。

Star Trek 或 Star Wars

对于许多人来说,金融行业代表了纯粹的资本主义形式:一个一切以贪婪为驱动的行业。这肯定是一个高度竞争的行业,毋庸置疑。特别是交易和投资管理经常以亿万富翁的管理者和所有者来象征,他们愿意进行大赌注,并与竞争对手直接竞争,以争取下一个大交易或交易。人工智能的出现为雄心勃勃的管理者提供了丰富的工具集,将竞争推向下一个水平,正如在 第十三章 中讨论的那样。

然而,问题在于 AI 优先的金融,可能会导致金融乌托邦或者金融世界,即 AFI,是否会导致金钱在一些人手中无限积累的系统性、不可避免?理论上说,这种积累的财富可能只服务于少数人,或者潜在地可能造福于人类。不幸的是,我们必须假设,只有导致 AFI 的项目的赞助者才会直接从本章设想中的 AFI 类型中获益。因为这样的 AFI 只能通过在金融市场中交易而非发明新产品、解决重要问题或者发展业务和产业来生成利润。换句话说,仅通过在金融市场中交易以获取利润的 AFI,参与的是一个零和游戏,并不会直接增加可分配的财富。

有人可能会争辩说,例如,投资于由 AFI 管理的基金的养老基金也会从其异常高的回报中受益。但这仍然只会使某个特定的群体受益,而非整个人类。同时也有人质疑,成功的 AFI 项目的赞助者是否愿意向外部投资者开放。在这方面的一个很好的例子是文艺复兴技术管理的 Medallion 基金,是历史上表现最佳的投资工具之一。文艺复兴于 1993 年关闭了 Medallion,该基金基本上是由机器独家运行的,不接受外部投资者。其出色的表现肯定会吸引大量额外资产。然而,特定的考虑因素,如某些策略的能力,在这种情况下也发挥了作用,类似的考虑也可能适用于 AFI。

因此,尽管人们可以期望超级智能帮助克服整个人类面临的基本问题——严重的疾病、环境问题、来自外太空的未知威胁等——但 AFI 更可能导致市场中的更不平等和更激烈的竞争。与 星际迷航 风格的世界相比,这不排除 AFI 可能更多地导致像 星球大战 风格的世界,其中充斥着激烈的贸易战和对现有资源的争夺。在撰写本文时,全球贸易战(例如美国和中国之间的贸易战)似乎比以往任何时候都更加激烈,技术和人工智能是重要的战场。

结论

本章从高层次的视角讨论了金融奇点和人工金融智能的概念。AFI 是一个缺乏超级智能许多能力和特征的 ANI。AFI 可以与 AlphaZero 相提并论,后者是一个用于下棋或围棋等棋类游戏的 ANI。AFI 将在金融工具交易游戏中表现出色。当然,与棋类游戏相比,金融交易涉及的风险要大得多。

与 AlphaZero 类似,AI 更有可能铺平通往 AFI 的道路,而不是通过仿真人脑等替代路径。即使道路尚不完全明确,尽管无法确定单个项目已经进展到何种程度,但有几个重要的资源至关重要,无论哪种路径最终胜出:专家、算法、数据、硬件和资本。大型成功的对冲基金似乎最有可能赢得 AFI 的竞赛。

即使在本章所描绘的情况下创建 AFI 可能是不可能的,但 AI 系统地引入金融领域肯定会促进创新,并在许多情况下加剧行业竞争。AI 不是一时的潮流,而是一种终将引领行业范式转变的趋势。

参考资料:

本章引用的书籍和论文:

  • Bostrom, Nick. 2014. 超级智能:路径、危险、策略。牛津:牛津大学出版社。

  • Huber, Nick. 2020. “AI‘只是触及金融服务潜力表面’。” 《金融时报》,2020 年 7 月 1 日。

  • Shiller, Robert. 2015. “金融奇点的幻象。” 耶鲁见解 (博客)。https://oreil.ly/cnWBh

第六部分:附录

本部分作为附录,提供了额外的材料,支持本书其他部分所呈现的内容、代码和示例。本部分包括三个附录:

  • 附录 A 涵盖了与神经网络相关的基本概念,如张量运算。

  • 附录 B 介绍了从头开始实现简单和浅层神经网络的类。

  • 附录 C 展示了使用Keras包应用卷积神经网络的方法。

附录 A. 交互式神经网络

本附录通过基础的 Python 代码探讨了神经网络的基本概念,涵盖了简单和浅层神经网络。其目标是帮助读者对重要概念有一个深入的理解和直观感受,这些概念在使用标准机器学习和深度学习包时往往被高级抽象的 API 所掩盖。

本附录包含以下部分:

  • “张量和张量操作”介绍了张量的基础知识和在其上实施的操作。

  • “简单神经网络”讨论简单神经网络,即只有输入层和输出层的神经网络。

  • “浅层神经网络”关注浅层神经网络,即只有一个隐藏层的神经网络。

张量和张量操作

除了实现多个导入和配置外,以下 Python 代码展示了本附录目的上相关的四种张量类型:标量、向量、矩阵和立方体张量。在 Python 中,张量通常表示为可能是多维 ndarray 对象。有关更多详细信息和示例,请参阅 Chollet(2017,第二章):

In [1]: import math
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        np.random.seed(1)
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        np.set_printoptions(suppress=True)

In [2]: t0 = np.array(10)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        t0  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[2]: array(10)

In [3]: t1 = np.array((2, 1))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
        t1  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[3]: array([2, 1])

In [4]: t2 = np.arange(10).reshape(5, 2)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
        t2  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[4]: array([[0, 1],
               [2, 3],
               [4, 5],
               [6, 7],
               [8, 9]])

In [5]: t3 = np.arange(16).reshape(2, 4, 2)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
        t3  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[5]: array([[[ 0,  1],
                [ 2,  3],
                [ 4,  5],
                [ 6,  7]],

               [[ 8,  9],
                [10, 11],
                [12, 13],
                [14, 15]]])

1

标量张量

2

向量张量

3

矩阵张量

4

立方体张量

在神经网络的背景下,张量上的几种数学操作非常重要,例如逐元素操作或点积:

In [6]: t2 + 1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[6]: array([[ 1,  2],
               [ 3,  4],
               [ 5,  6],
               [ 7,  8],
               [ 9, 10]])

In [7]: t2 + t2  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[7]: array([[ 0,  2],
               [ 4,  6],
               [ 8, 10],
               [12, 14],
               [16, 18]])

In [8]: t1
Out[8]: array([2, 1])

In [9]: t2
Out[9]: array([[0, 1],
               [2, 3],
               [4, 5],
               [6, 7],
               [8, 9]])

In [10]: np.dot(t2, t1)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[10]: array([ 1,  7, 13, 19, 25])

In [11]: t2[:, 0] * 2 + t2[:, 1] * 1  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[11]: array([ 1,  7, 13, 19, 25])

In [12]: np.dot(t1, t2.T)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[12]: array([ 1,  7, 13, 19, 25])

1

广播操作

2

逐元素操作

3

NumPy 函数进行点积

4

显式符号中的点积

简单神经网络

理解张量的基础后,考虑仅有输入层和输出层的简单神经网络。

估计

第一个问题是一个估计问题,其标签是实值的:

In [13]: features = 3  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [14]: samples = 5  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [15]: l0 = np.random.random((samples, features))  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         l0  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[15]: array([[0.417022  , 0.72032449, 0.00011437],
                [0.30233257, 0.14675589, 0.09233859],
                [0.18626021, 0.34556073, 0.39676747],
                [0.53881673, 0.41919451, 0.6852195 ],
                [0.20445225, 0.87811744, 0.02738759]])

In [16]: w = np.random.random((features, 1))  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         w  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[16]: array([[0.67046751],
                [0.4173048 ],
                [0.55868983]])

In [17]: l2 = np.dot(l0, w)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         l2  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[17]: array([[0.58025848],
                [0.31553474],
                [0.49075552],
                [0.91901616],
                [0.51882238]])

In [18]: y = l0[:, 0] * 0.5 + l0[:, 1]   ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         y = y.reshape(-1, 1)  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         y  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[18]: array([[0.9288355 ],
                [0.29792218],
                [0.43869083],
                [0.68860288],
                [0.98034356]])

1

特征数量

2

样本数量

3

随机输入层

4

随机权重

5

通过点积的输出层

6

待学习的标签

下面的 Python 代码逐步展示了一个学习过程,从计算误差到在更新权重后计算均方误差(MSE):

In [19]: e = l2 - y  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         e  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[19]: array([[-0.34857702],
                [ 0.01761256],
                [ 0.05206469],
                [ 0.23041328],
                [-0.46152118]])

In [20]: mse = (e ** 2).mean()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         mse  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[20]: 0.07812379019517127

In [21]: d = e * 1  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         d  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[21]: array([[-0.34857702],
                [ 0.01761256],
                [ 0.05206469],
                [ 0.23041328],
                [-0.46152118]])

In [22]: a = 0.01  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [23]: u = a * np.dot(l0.T, d)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         u  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[23]: array([[-0.0010055 ],
                [-0.00539194],
                [ 0.00167488]])

In [24]: w  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[24]: array([[0.67046751],
                [0.4173048 ],
                [0.55868983]])

In [25]: w -= u  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [26]: w  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[26]: array([[0.67147301],
                [0.42269674],
                [0.55701495]])

In [27]: l2 = np.dot(l0, w)  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)

In [28]: e = l2 - y  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)

In [29]: mse = (e ** 2).mean()  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
         mse  ![9](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/9.png)
Out[29]: 0.07681782193617318

1

估计误差

2

给定估计的均方误差值

3

反向传播(这里 d = e)¹

4

学习率

5

更新值

6

更新前后的权重

7

更新后的新输出层(估计)

8

更新后的新误差值

9

更新后的新均方误差值

为了提高估计精度,通常需要重复相同的步骤多次。在下面的代码中,学习率增加,并且该过程执行了数百次。最终的均方误差值非常低,估计非常好:

In [30]: a = 0.025  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [31]: w = np.random.random((features, 1))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         w  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[31]: array([[0.14038694],
                [0.19810149],
                [0.80074457]])

In [32]: steps = 800  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [33]: for s in range(1, steps + 1):
             l2 = np.dot(l0, w)
             e = l2 - y
             u = a * np.dot(l0.T, e)
             w -= u
             mse = (e ** 2).mean()
             if s % 50 == 0:
                 print(f'step={s:3d} | mse={mse:.5f}')
         step= 50 | mse=0.03064
         step=100 | mse=0.01002
         step=150 | mse=0.00390
         step=200 | mse=0.00195
         step=250 | mse=0.00124
         step=300 | mse=0.00092
         step=350 | mse=0.00074
         step=400 | mse=0.00060
         step=450 | mse=0.00050
         step=500 | mse=0.00041
         step=550 | mse=0.00035
         step=600 | mse=0.00029
         step=650 | mse=0.00024
         step=700 | mse=0.00020
         step=750 | mse=0.00017
         step=800 | mse=0.00014

In [34]: l2 - y  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[34]: array([[-0.01240168],
                [-0.01606065],
                [ 0.01274072],
                [-0.00087794],
                [ 0.01072845]])

In [35]: w  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[35]: array([[0.41907514],
                [1.02965827],
                [0.04421136]])

1

调整后的学习率

2

初始随机权重

3

学习步骤的数量

4

估计的残差误差

5

神经网络的最终权重

分类

第二个问题是一个二元整数值标签的分类问题。为了改善学习算法的性能,输出层的激活采用了sigmoid 函数。图 A-1 展示了 sigmoid 函数及其一阶导数,并将其与简单的阶跃函数进行了比较:

In [36]: def sigmoid(x, deriv=False):
             if deriv:
                 return sigmoid(x) * (1 - sigmoid(x))
             return 1 / (1 + np.exp(-x))

In [37]: x = np.linspace(-10, 10, 100)

In [38]: plt.figure(figsize=(10, 6))
         plt.plot(x, np.where(x > 0, 1, 0), 'y--', label='step function')
         plt.plot(x, sigmoid(x), 'r', label='sigmoid')
         plt.plot(x, sigmoid(x, True), '--', label='derivative')
         plt.legend();

aiif 1601

图 A-1. 阶跃函数、sigmoid 函数及其一阶导数

为了简化问题,分类问题基于随机二进制特征和二进制标签数据。除了不同的特征和标签数据外,只有输出层的激活与估计问题有所不同。更新神经网络权重的学习算法基本相同:

In [39]: features = 4
         samples = 5

In [40]: l0 = np.random.randint(0, 2, (samples, features))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         l0  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[40]: array([[1, 1, 1, 1],
                [0, 1, 1, 0],
                [0, 1, 0, 0],
                [1, 1, 1, 0],
                [1, 0, 0, 1]])

In [41]: w = np.random.random((features, 1))
         w
Out[41]: array([[0.42110763],
                [0.95788953],
                [0.53316528],
                [0.69187711]])

In [42]: l2 = sigmoid(np.dot(l0, w))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
         l2
Out[42]: array([[0.93112111],
                [0.81623654],
                [0.72269905],
                [0.87126189],
                [0.75268514]])

In [43]: l2.round()
Out[43]: array([[1.],
                [1.],
                [1.],
                [1.],
                [1.]])

In [44]: y = np.random.randint(0, 2, samples)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         y = y.reshape(-1, 1)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         y  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[44]: array([[1],
                [1],
                [0],
                [0],
                [0]])

In [45]: e = l2 - y
         e
Out[45]: array([[-0.06887889],
                [-0.18376346],
                [ 0.72269905],
                [ 0.87126189],
                [ 0.75268514]])

In [46]: mse = (e ** 2).mean()
         mse
Out[46]: 0.37728788783411127

In [47]: a = 0.02

In [48]: d = e * sigmoid(l2, True)  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         d
Out[48]: array([[-0.01396723],
                [-0.03906484],
                [ 0.15899479],
                [ 0.18119776],
                [ 0.16384833]])

In [49]: u = a * np.dot(l0.T, d)
         u
Out[49]: array([[0.00662158],
                [0.00574321],
                [0.00256331],
                [0.00299762]])

In [50]: w
Out[50]: array([[0.42110763],
                [0.95788953],
                [0.53316528],
                [0.69187711]])

In [51]: w -= u

In [52]: w
Out[52]: array([[0.41448605],
                [0.95214632],
                [0.53060197],
                [0.68887949]])

1

具有二进制特征的输入层

2

sigmoid 激活的输出层

3

二进制标签数据

4

通过一阶导数进行反向传播

与之前一样,需要进行更多次迭代的循环来获得准确的分类结果。根据随机数的选择,像下面的例子中可能会达到 100%的准确率:

In [53]: steps = 3001

In [54]: a = 0.025

In [55]: w = np.random.random((features, 1))
         w
Out[55]: array([[0.41253884],
                [0.03417131],
                [0.62402999],
                [0.66063573]])

In [56]: for s in range(1, steps + 1):
             l2 = sigmoid(np.dot(l0, w))
             e = l2 - y
             d = e * sigmoid(l2, True)
             u = a * np.dot(l0.T, d)
             w -= u
             mse = (e ** 2).mean()
             if s % 200 == 0:
                 print(f'step={s:4d} | mse={mse:.4f}')
         step= 200 | mse=0.1899
         step= 400 | mse=0.1572
         step= 600 | mse=0.1349
         step= 800 | mse=0.1173
         step=1000 | mse=0.1029
         step=1200 | mse=0.0908
         step=1400 | mse=0.0806
         step=1600 | mse=0.0720
         step=1800 | mse=0.0646
         step=2000 | mse=0.0583
         step=2200 | mse=0.0529
         step=2400 | mse=0.0482
         step=2600 | mse=0.0441
         step=2800 | mse=0.0405
         step=3000 | mse=0.0373

In [57]: l2
Out[57]: array([[0.71220474],
                [0.92308745],
                [0.16614971],
                [0.20193503],
                [0.17094583]])

In [58]: l2.round() == y
Out[58]: array([[ True],
                [ True],
                [ True],
                [ True],
                [ True]])

In [59]: w
Out[59]: array([[-3.86002022],
                [-1.61346536],
                [ 4.09895004],
                [ 2.28088807]])

浅层神经网络

前一节的神经网络仅由输入层和输出层组成。换句话说,输入层和输出层直接相连。浅层神经网络在输入层和输出层之间有一个隐藏层。考虑到这种结构,需要两组权重来连接神经网络中的三层。本节分析了用于估计和分类的浅层神经网络。

估计

如同前一节,首先解决估计问题。以下 Python 代码构建了具有三层和两组权重的神经网络。这第一个步骤序列通常称为前向传播。在这种情况下,输入层矩阵通常具有满秩,表明可以实现完美的估计结果:

In [60]: features = 5
         samples = 5

In [61]: l0 = np.random.random((samples, features))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         l0  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[61]: array([[0.29849529, 0.44613451, 0.22212455, 0.07336417, 0.46923853],
                [0.09617226, 0.90337017, 0.11949047, 0.52479938, 0.083623  ],
                [0.91686133, 0.91044838, 0.29893011, 0.58438912, 0.56591203],
                [0.61393832, 0.95653566, 0.26097898, 0.23101542, 0.53344849],
                [0.94993814, 0.49305959, 0.54060051, 0.7654851 , 0.04534573]])

In [62]: np.linalg.matrix_rank(l0)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[62]: 5

In [63]: units = 3  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [64]: w0 = np.random.random((features, units))  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
         w0  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
Out[64]: array([[0.13996612, 0.79240359, 0.02980136],
                [0.88312548, 0.54078819, 0.44798018],
                [0.89213587, 0.37758434, 0.53842469],
                [0.65229888, 0.36126102, 0.57100856],
                [0.63783648, 0.12631489, 0.69020459]])

In [65]: l1 = np.dot(l0, w0)  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
         l1  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
Out[65]: array([[0.98109007, 0.64743919, 0.69411448],
                [1.31351565, 0.81000928, 0.82927653],
                [1.94121167, 1.61435539, 1.32042417],
                [1.65444429, 1.25315104, 1.08742312],
                [1.57892999, 1.50576525, 1.00865941]])

In [66]: w1 = np.random.random((units, 1))  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
         w1  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)
Out[66]: array([[0.6477494 ],
                [0.35393909],
                [0.76323305]])

In [67]: l2 = np.dot(l1, w1)  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
         l2  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)
Out[67]: array([[1.39442565],
                [1.77045418],
                [2.83659354],
                [2.3451617 ],
                [2.32554234]])

In [68]: y = np.random.random((samples, 1))  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
         y  ![8](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/8.png)
Out[68]: array([[0.35653172],
                [0.75278835],
                [0.88134183],
                [0.01166919],
                [0.49810907]])

1

随机输入层

2

输入层矩阵的秩

3

隐藏单元的数量

4

给定featuresunits参数的第一组随机权重

5

给定输入层和权重的隐藏层

6

第二组随机权重

7

给定隐藏层和权重的输出层

8

随机标签数据

第二个步骤序列通常称为反向传播,与估计误差相关。两组权重将被更新,从输出层开始更新连接隐藏层和输出层之间的权重w1。随后,在考虑更新后的权重w1之后,将更新连接输入层和隐藏层之间的权重w0

In [69]: e2 = l2 - y  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         e2  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[69]: array([[1.03789393],
                [1.01766583],
                [1.95525171],
                [2.33349251],
                [1.82743327]])

In [70]: mse = (e2 ** 2).mean()
         mse
Out[70]: 2.9441152813655007

In [71]: d2 = e2 * 1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         d2  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[71]: array([[1.03789393],
                [1.01766583],
                [1.95525171],
                [2.33349251],
                [1.82743327]])

In [72]: a = 0.05

In [73]: u2 = a * np.dot(l1.T, d2)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         u2  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[73]: array([[0.64482837],
                [0.51643336],
                [0.42634283]])

In [74]: w1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[74]: array([[0.6477494 ],
                [0.35393909],
                [0.76323305]])

In [75]: w1 -= u2  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [76]: w1  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[76]: array([[ 0.00292103],
                [-0.16249427],
                [ 0.33689022]])

In [77]: e1 = np.dot(d2, w1.T)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [78]: d1 = e1 * 1  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [79]: u1 = a * np.dot(l0.T, d1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [80]: w0 -= u1  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [81]: w0  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[81]: array([[ 0.13918198,  0.8360247 , -0.06063583],
                [ 0.88220599,  0.59193836,  0.34193342],
                [ 0.89176585,  0.39816855,  0.49574861],
                [ 0.65175984,  0.39124762,  0.50883904],
                [ 0.63739741,  0.15074009,  0.63956519]])

1

更新过程的权重集w1

2

更新过程的权重集w0

以下 Python 代码实现了学习(即网络权重的更新)作为一个for循环,迭代次数更多。通过增加迭代次数,可以使估计结果任意精确:

In [82]: a = 0.015
         steps = 5000

In [83]: for s in range(1, steps + 1):
             l1 = np.dot(l0, w0)
             l2 = np.dot(l1, w1)
             e2 = l2 - y
             u2 = a * np.dot(l1.T, e2)
             w1 -= u2
             e1 = np.dot(e2, w1.T)
             u1 = a * np.dot(l0.T, e1)
             w0 -= u1
             mse = (e2 ** 2).mean()
             if s % 750 == 0:
                 print(f'step={s:5d} | mse={mse:.6f}')
         step=  750 | mse=0.039263
         step= 1500 | mse=0.009867
         step= 2250 | mse=0.000666
         step= 3000 | mse=0.000027
         step= 3750 | mse=0.000001
         step= 4500 | mse=0.000000

In [84]: l2
Out[84]: array([[0.35634333],
                [0.75275415],
                [0.88135507],
                [0.01179945],
                [0.49809208]])

In [85]: y
Out[85]: array([[0.35653172],
                [0.75278835],
                [0.88134183],
                [0.01166919],
                [0.49810907]])

In [86]: (l2 - y)
Out[86]: array([[-0.00018839],
                [-0.00003421],
                [ 0.00001324],
                [ 0.00013025],
                [-0.00001699]])

分类

接下来是分类问题。在这种情况下,实现与估计问题非常接近。但是,再次使用 Sigmoid 函数进行激活。以下 Python 代码首先生成随机样本数据:

In [87]: features = 5
         samples = 10
         units = 10

In [88]: np.random.seed(200)
         l0 = np.random.randint(0, 2, (samples, features))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         w0 = np.random.random((features, units))
         w1 = np.random.random((units, 1))
         y = np.random.randint(0, 2, (samples, 1))  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [89]: l0  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[89]: array([[0, 1, 0, 0, 0],
                [1, 0, 1, 1, 0],
                [1, 1, 1, 1, 0],
                [0, 0, 1, 1, 1],
                [1, 1, 1, 1, 0],
                [1, 1, 0, 1, 0],
                [0, 1, 0, 1, 0],
                [0, 1, 0, 0, 1],
                [0, 1, 1, 1, 1],
                [0, 0, 1, 0, 0]])

In [90]: y  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[90]: array([[1],
                [0],
                [1],
                [0],
                [1],
                [0],
                [0],
                [0],
                [1],
                [1]])

1

二进制特征数据(输入层)

2

二进制标签数据

学习算法的实现再次利用for循环来重复权重更新步骤,直到必要的次数。根据为特征和标签数据生成的随机数,经过足够的学习步骤后可以达到 100%的准确率:

In [91]: a = 0.1
         steps = 20000

In [92]: for s in range(1, steps + 1):
             l1 = sigmoid(np.dot(l0, w0))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             l2 = sigmoid(np.dot(l1, w1))  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
             e2 = l2 - y  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             d2 = e2 * sigmoid(l2, True)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             u2 = a * np.dot(l1.T, d2)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             w1 -= u2  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             e1 = np.dot(d2, w1.T)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             d1 = e1 * sigmoid(l1, True)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             u1 = a * np.dot(l0.T, d1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             w0 -= u1  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             mse = (e2 ** 2).mean()
             if s % 2000 == 0:
                 print(f'step={s:5d} | mse={mse:.5f}')
         step= 2000 | mse=0.00933
         step= 4000 | mse=0.02399
         step= 6000 | mse=0.05134
         step= 8000 | mse=0.00064
         step=10000 | mse=0.00013
         step=12000 | mse=0.00009
         step=14000 | mse=0.00007
         step=16000 | mse=0.00007
         step=18000 | mse=0.00012
         step=20000 | mse=0.00015

In [93]: acc = l2.round() == y  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
         acc  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[93]: array([[ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True]])

In [94]: sum(acc) / len(acc)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[94]: array([1.])

1

前向传播

2

反向传播

3

分类的准确性

参考资料

附录中引用的书籍:

  • Chollet, Francois. 2017. Python 深度学习. Shelter Island: Manning.

¹ 由于没有隐藏层,反向传播的导数值为 1。输出层和输入层直接连接。

附录 B. 神经网络类

基于附录 A 的基础,本附录提供了类似于scikit-learn等包的 API 的简单基于类的神经网络实现。该实现基于纯粹简单的 Python 代码,仅供示例和教学之用。本附录中介绍的类不能替代标准 Python 包中发现的健壮、高效和可伸缩的实现,如scikit-learnTensorFlow结合Keras

附录包括以下部分:

  • “激活函数”介绍了一个带有不同激活函数的 Python 函数。

  • “简单神经网络”介绍了一个用于简单神经网络的 Python 类。

  • “浅层神经网络”介绍了一个用于浅层神经网络的 Python 类。

  • “预测市场方向”将浅层神经网络类应用于金融数据。

本附录中的实现和示例都非常简单直接。这些 Python 类并不适合解决更大的估计或分类问题。其主要目的是展示从头开始易于理解的 Python 实现。

激活函数

附录 A 隐式或显式地使用了两个激活函数:线性函数和 Sigmoid 函数。Python 函数activation添加了relu(修正线性单元)和softplus函数作为选项。对于所有这些激活函数,也定义了第一导数:

In [1]: import math
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        np.set_printoptions(suppress=True)

In [2]: def activation(x, act='linear', deriv=False):
            if act == 'sigmoid':
                if deriv:
                    out = activation(x, 'sigmoid', False)
                    return out * (1 - out)
                return 1 / (1 + np.exp(-x))
            elif act == 'relu':
                if deriv:
                    return np.where(x > 0, 1, 0)
                return np.maximum(x, 0)
            elif act == 'softplus':
                if deriv:
                    return activation(x, act='sigmoid')
                return np.log(1 + np.exp(x))
            elif act == 'linear':
                if deriv:
                    return 1
                return x
            else:
                raise ValueError('Activation function not known.')

In [3]: x = np.linspace(-1, 1, 20)

In [4]: activation(x, 'sigmoid')
Out[4]: array([0.26894142, 0.29013328, 0.31228169, 0.33532221, 0.35917484,
               0.38374461, 0.40892261, 0.43458759, 0.46060812, 0.48684514,
               0.51315486, 0.53939188, 0.56541241, 0.59107739, 0.61625539,
               0.64082516, 0.66467779, 0.68771831, 0.70986672, 0.73105858])

In [5]: activation(x, 'sigmoid', True)
Out[5]: array([0.19661193, 0.20595596, 0.21476184, 0.22288122, 0.23016827,
               0.23648468, 0.24170491, 0.24572122, 0.24844828, 0.24982695,
               0.24982695, 0.24844828, 0.24572122, 0.24170491, 0.23648468,
               0.23016827, 0.22288122, 0.21476184, 0.20595596, 0.19661193])

简单神经网络

本节介绍了一个简单神经网络类,其 API 与标准 Python 包(特别是scikit-learnKeras)中的模型类似。考虑到类sinn的 Python 代码如下所示。它实现了一个简单的神经网络,并定义了两个主要方法.fit().predict().metrics()方法计算了估计的均方误差(MSE)和分类的准确度等典型性能指标。该类还实现了前向传播和反向传播步骤的两个方法:

In [6]: class sinn:
            def __init__(self, act='linear', lr=0.01, steps=100,
                         verbose=False, psteps=200):
                self.act = act
                self.lr = lr
                self.steps = steps
                self.verbose = verbose
                self.psteps = psteps
            def forward(self):
                ''' Forward propagation.
 '''
                self.l2 = activation(np.dot(self.l0, self.w), self.act)
            def backward(self):
                ''' Backward propagation.
 '''
                self.e = self.l2 - self.y
                d = self.e * activation(self.l2, self.act, True)
                u = self.lr * np.dot(self.l0.T, d)
                self.w -= u
            def metrics(self, s):
                ''' Performance metrics.
 '''
                mse = (self.e ** 2).mean()
                acc = float(sum(self.l2.round() == self.y) / len(self.y))
                self.res = self.res.append(
                    pd.DataFrame({'mse': mse, 'acc': acc}, index=[s,])
                )
                if s % self.psteps == 0 and self.verbose:
                        print(f'step={s:5d} | mse={mse:.6f}')
                        print(f'           | acc={acc:.6f}')
            def fit(self, l0, y, steps=None, seed=None):
                ''' Fitting step.
 '''
                self.l0 = l0
                self.y = y
                if steps is None:
                    steps = self.steps
                self.res = pd.DataFrame()
                samples, features = l0.shape
                if seed is not None:
                    np.random.seed(seed)
                self.w = np.random.random((features, 1))
                for s in range(1, steps + 1):
                    self.forward()
                    self.backward()
                    self.metrics(s)
            def predict(self, X):
                ''' Prediction step.
 '''
                return activation(np.dot(X, self.w), self.act)

估计

首先是可以通过回归技术解决的估计问题:

In [7]: features = 5
        samples = 5

In [8]: np.random.seed(10)
        l0 = np.random.standard_normal((samples, features))
        l0
Out[8]: array([[ 1.3315865 ,  0.71527897, -1.54540029, -0.00838385,  0.62133597],
               [-0.72008556,  0.26551159,  0.10854853,  0.00429143, -0.17460021],
               [ 0.43302619,  1.20303737, -0.96506567,  1.02827408,  0.22863013],
               [ 0.44513761, -1.13660221,  0.13513688,  1.484537  , -1.07980489],
               [-1.97772828, -1.7433723 ,  0.26607016,  2.38496733,  1.12369125]])

In [9]: np.linalg.matrix_rank(l0)
Out[9]: 5

In [10]: y = np.random.random((samples, 1))
         y
Out[10]: array([[0.8052232 ],
                [0.52164715],
                [0.90864888],
                [0.31923609],
                [0.09045935]])

In [11]: reg = np.linalg.lstsq(l0, y, rcond=-1)[0]  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [12]: reg  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[12]: array([[-0.74919308],
                [ 0.00146473],
                [-1.49864704],
                [-0.02498757],
                [-0.82793882]])

In [13]: np.allclose(np.dot(l0, reg), y)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[13]: True

1

通过回归的精确解决方案

sinn类应用于估计问题需要进行多次学习步骤的努力。但是,通过增加步骤的数量,可以使估计任意精确:

In [14]: model = sinn(lr=0.015, act='linear', steps=6000,
                     verbose=True, psteps=1000)

In [15]: %time model.fit(l0, y, seed=100)
         step= 1000 | mse=0.008086
                    | acc=0.000000
         step= 2000 | mse=0.000545
                    | acc=0.000000
         step= 3000 | mse=0.000037
                    | acc=0.000000
         step= 4000 | mse=0.000002
                    | acc=0.000000
         step= 5000 | mse=0.000000
                    | acc=0.000000
         step= 6000 | mse=0.000000
                    | acc=0.000000
         CPU times: user 5.23 s, sys: 29.7 ms, total: 5.26 s
         Wall time: 5.26 s

In [16]: model.predict(l0)
Out[16]: array([[0.80512489],
                [0.52144986],
                [0.90872498],
                [0.31919803],
                [0.09045743]])

In [17]: model.predict(l0) - y  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[17]: array([[-0.0000983 ],
                [-0.00019729],
                [ 0.0000761 ],
                [-0.00003806],
                [-0.00000191]])

1

神经网络估计的残差误差

分类

其次是一个分类问题,也可以用sinn类来解决。在这里,标准的回归技术通常无效。对于随机特征和标签集合,sinn模型达到了 100%的准确率。同样地,需要大量的重复学习步骤。图 B-1 显示了预测准确率随学习步骤数量变化的情况:

In [18]: features = 5
         samples = 10

In [19]: np.random.seed(3)
         l0 = np.random.randint(0, 2, (samples, features))
         l0
Out[19]: array([[0, 0, 1, 1, 0],
                [0, 0, 1, 1, 1],
                [0, 1, 1, 1, 0],
                [1, 1, 0, 0, 0],
                [0, 1, 1, 0, 0],
                [0, 1, 0, 0, 0],
                [0, 1, 0, 1, 1],
                [0, 1, 0, 0, 1],
                [1, 0, 0, 1, 0],
                [1, 0, 1, 1, 1]])

In [20]: np.linalg.matrix_rank(l0)
Out[20]: 5

In [21]: y = np.random.randint(0, 2, (samples, 1))
         y
Out[21]: array([[1],
                [0],
                [1],
                [0],
                [0],
                [1],
                [1],
                [1],
                [0],
                [0]])

In [22]: model = sinn(lr=0.01, act='sigmoid')  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [23]: %time model.fit(l0, y, 4000)
         CPU times: user 3.57 s, sys: 9.6 ms, total: 3.58 s
         Wall time: 3.59 s

In [24]: model.l2
Out[24]: array([[0.51118415],
                [0.34390898],
                [0.84733758],
                [0.07601979],
                [0.40505454],
                [0.84145926],
                [0.95592461],
                [0.72680243],
                [0.11219587],
                [0.00806003]])

In [25]: model.predict(l0).round() == y  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[25]: array([[ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True],
                [ True]])

In [26]: ax = model.res['acc'].plot(figsize=(10, 6),
                     title='Prediction Accuracy | Classification')
         ax.set(xlabel='steps', ylabel='accuracy');

1

sigmoid 函数用于激活

2

在这个特定数据集上表现完美

aiif 1601

图 B-1. 预测准确率与学习步骤数量的关系

浅层神经网络

本节应用shnn类,该类实现了具有一个隐藏层的浅层神经网络,用于估计和分类问题。该类结构类似于前一节中的sinn类:

In [27]: class shnn:
             def __init__(self, units=12, act='linear', lr=0.01, steps=100,
                          verbose=False, psteps=200, seed=None):
                 self.units = units
                 self.act = act
                 self.lr = lr
                 self.steps = steps
                 self.verbose = verbose
                 self.psteps = psteps
                 self.seed = seed
             def initialize(self):
                 ''' Initializes the random weights.
 '''
                 if self.seed is not None:
                     np.random.seed(self.seed)
                 samples, features = self.l0.shape
                 self.w0 = np.random.random((features, self.units))
                 self.w1 = np.random.random((self.units, 1))
             def forward(self):
                 ''' Forward propagation.
 '''
                 self.l1 = activation(np.dot(self.l0, self.w0), self.act)
                 self.l2 = activation(np.dot(self.l1, self.w1), self.act)
             def backward(self):
                 ''' Backward propagation.
 '''
                 self.e = self.l2 - self.y
                 d2 = self.e * activation(self.l2, self.act, True)
                 u2 = self.lr * np.dot(self.l1.T, d2)
                 self.w1 -= u2
                 e1 = np.dot(d2, self.w1.T)
                 d1 = e1 * activation(self.l1, self.act, True)
                 u1 = self.lr * np.dot(self.l0.T, d1)
                 self.w0 -= u1
             def metrics(self, s):
                 ''' Performance metrics.
 '''
                 mse = (self.e ** 2).mean()
                 acc = float(sum(self.l2.round() == self.y) / len(self.y))
                 self.res = self.res.append(
                     pd.DataFrame({'mse': mse, 'acc': acc}, index=[s,])
                 )
                 if s % self.psteps == 0 and self.verbose:
                         print(f'step={s:5d} | mse={mse:.5f}')
                         print(f'           | acc={acc:.5f}')
             def fit(self, l0, y, steps=None):
                 ''' Fitting step.
 '''
                 self.l0 = l0
                 self.y = y
                 if steps is None:
                     steps = self.steps
                 self.res = pd.DataFrame()
                 self.initialize()
                 self.forward()
                 for s in range(1, steps + 1):
                     self.backward()
                     self.forward()
                     self.metrics(s)
             def predict(self, X):
                 ''' Prediction step.
 '''
                 l1 = activation(np.dot(X, self.w0), self.act)
                 l2 = activation(np.dot(l1, self.w1), self.act)
                 return l2

估计

再次,估计问题排在首位。对于 5 个特征和 10 个样本,完美的回归解决方案不太可能存在。因此,相对于回归值,MSE 值相对较高:

In [28]: features = 5
         samples = 10

In [29]: l0 = np.random.standard_normal((samples, features))

In [30]: np.linalg.matrix_rank(l0)
Out[30]: 5

In [31]: y = np.random.random((samples, 1))

In [32]: reg = np.linalg.lstsq(l0, y, rcond=-1)[0]

In [33]: (np.dot(l0, reg)  - y)
Out[33]: array([[-0.10226341],
                [-0.42357164],
                [-0.25150491],
                [-0.30984143],
                [-0.85213261],
                [-0.13791373],
                [-0.52336502],
                [-0.50304204],
                [-0.7728686 ],
                [-0.3716898 ]])

In [34]: ((np.dot(l0, reg)  - y) ** 2).mean()
Out[34]: 0.23567187607888118

然而,基于shnn类的浅层神经网络估计表现相当良好,并显示出与回归值相比相对较低的 MSE 值:

In [35]: model = shnn(lr=0.01, units=16, act='softplus',
                      verbose=True, psteps=2000, seed=100)

In [36]: %time model.fit(l0, y, 8000)
         step= 2000 | mse=0.00205
                    | acc=0.00000
         step= 4000 | mse=0.00098
                    | acc=0.00000
         step= 6000 | mse=0.00043
                    | acc=0.00000
         step= 8000 | mse=0.00022
                    | acc=0.00000
         CPU times: user 8.15 s, sys: 69.2 ms, total: 8.22 s
         Wall time: 8.3 s

In [37]: model.l2 - y
Out[37]: array([[-0.00390976],
                [-0.00522077],
                [ 0.02053932],
                [-0.0042113 ],
                [-0.0006624 ],
                [-0.01001395],
                [ 0.01783203],
                [-0.01498316],
                [-0.0177866 ],
                [ 0.02782519]])

分类

分类示例将估计的数字应用四舍五入处理。浅层神经网络迅速收敛,以 100%的准确率预测标签(见图 B-2):

In [38]: model = shnn(lr=0.025, act='sigmoid', steps=200,
                      verbose=True, psteps=50, seed=100)

In [39]: l0.round()
Out[39]: array([[ 0., -1., -2.,  1., -0.],
                [-1., -2., -0., -0., -2.],
                [ 0.,  1., -1., -1., -1.],
                [-0.,  0., -1., -0., -1.],
                [ 1., -1.,  1.,  1., -1.],
                [ 1., -1.,  1., -2.,  1.],
                [-1., -0.,  1., -1.,  1.],
                [ 1.,  2., -1., -0., -0.],
                [-1.,  0.,  0.,  0.,  2.],
                [ 0.,  0., -0.,  1.,  1.]])

In [40]: np.linalg.matrix_rank(l0)
Out[40]: 5

In [41]: y.round()
Out[41]: array([[0.],
                [1.],
                [1.],
                [1.],
                [1.],
                [1.],
                [0.],
                [1.],
                [0.],
                [0.]])

In [42]: model.fit(l0.round(), y.round())
         step=   50 | mse=0.26774
                    | acc=0.60000
         step=  100 | mse=0.22556
                    | acc=0.60000
         step=  150 | mse=0.19939
                    | acc=0.70000
         step=  200 | mse=0.16924
                    | acc=1.00000

In [43]: ax = model.res.plot(figsize=(10, 6), secondary_y='mse')
         ax.get_legend().set_bbox_to_anchor((0.2, 0.5));

aiif 1602

图 B-2. 浅层神经网络(分类)的性能指标

预测市场方向

本节应用shnn类来预测 EUR/USD 汇率未来的走向。分析仅适用于样本内,以说明shnn在实际数据中的应用。详见第十章,了解更真实的基于预测策略的向量化回测设置实现。

以下 Python 代码导入了金融数据——10 年的 EOD 数据,并创建了滞后、归一化的对数收益率作为特征。标签数据是价格序列方向的二进制数据集:

In [44]: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'

In [45]: raw = pd.read_csv(url, index_col=0, parse_dates=True).dropna()

In [46]: sym = 'EUR='

In [47]: data = pd.DataFrame(raw[sym])

In [48]: lags = 5
         cols = []
         data['r'] = np.log(data / data.shift(1))
         data['d'] = np.where(data['r'] > 0, 1, 0)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         for lag in range(1, lags + 1):
             col = f'lag_{lag}'
             data[col] = data['r'].shift(lag)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
             cols.append(col)
         data.dropna(inplace=True)
         data[cols] = (data[cols] - data[cols].mean()) / data[cols].std()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)

In [49]: data.head()
Out[49]:               EUR=         r  d     lag_1     lag_2     lag_3     lag_4  \
         Date
         2010-01-12  1.4494 -0.001310  0  1.256582  1.177935 -1.142025  0.560551
         2010-01-13  1.4510  0.001103  1 -0.214533  1.255944  1.178974 -1.142118
         2010-01-14  1.4502 -0.000551  0  0.213539 -0.214803  1.256989  1.178748
         2010-01-15  1.4382 -0.008309  0 -0.079986  0.213163 -0.213853  1.256758
         2010-01-19  1.4298 -0.005858  0 -1.456028 -0.080289  0.214140 -0.214000

                        lag_5
         Date
         2010-01-12 -0.511372
         2010-01-13  0.560740
         2010-01-14 -1.141841
         2010-01-15  1.178904
         2010-01-19  1.256910

1

标签数据为市场方向

2

拖尾对数收益作为特征数据

3

特征数据的高斯归一化

数据预处理完成后,将浅层神经网络类shnn应用于监督分类问题非常简单。图 B-3 显示了样本内基于预测策略显著优于被动基准投资的情况:

In [50]: model = shnn(lr=0.0001, act='sigmoid', steps=10000,
                      verbose=True, psteps=2000, seed=100)

In [51]: y = data['d'].values.reshape(-1, 1)

In [52]: %time model.fit(data[cols].values, y)
         step= 2000 | mse=0.24964
                    | acc=0.51594
         step= 4000 | mse=0.24951
                    | acc=0.52390
         step= 6000 | mse=0.24945
                    | acc=0.52231
         step= 8000 | mse=0.24940
                    | acc=0.52510
         step=10000 | mse=0.24936
                    | acc=0.52430
         CPU times: user 9min 1s, sys: 40.9 s, total: 9min 42s
         Wall time: 1min 21s

In [53]: data['p'] = np.where(model.predict(data[cols]) > 0.5, 1, -1)  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [54]: data['p'].value_counts()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
Out[54]:  1    1257
         -1    1253
         Name: p, dtype: int64

In [55]: data['s'] = data['p'] * data['r']  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [56]: data[['r', 's']].sum().apply(np.exp)  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[56]: r    0.772411
         s    1.885677
         dtype: float64

In [57]: data[['r', 's']].cumsum().apply(np.exp).plot(figsize=(10, 6));  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

1

从预测值派生出位置值

2

从仓位价值和对数收益率计算策略收益

3

计算策略和基准投资的总体表现

4

显示策略和基准投资随时间的总体表现

aiif 1603

图 B-3. 基于预测的策略与被动基准投资的总体表现(样本内)

附录 C. 卷积神经网络

第三部分集中介绍了密集神经网络(DNNs)和递归神经网络(RNNs)作为两种标准的神经网络类型。DNNs 之所以迷人在于它们是良好的通用逼近器。例如,本书中的强化学习示例利用 DNNs 来逼近最优动作策略。另一方面,RNNs 专门设计用于处理序列数据,如时间序列数据,这在试图预测金融时间序列的未来值时非常有用。

卷积神经网络(CNNs)是另一种广泛应用的神经网络类型。在实践中它们非常成功,尤其在计算机视觉领域。CNNs 在诸如 ImageNet 挑战赛等多个标准测试和挑战中设立了新的基准;更多相关内容请参见《经济学人》(2016 年)或 Gerrish(2018 年)。计算机视觉在自动驾驶车辆、安全监控等领域尤为重要。

本附录简要展示了 CNN 在预测金融时间序列数据中的应用。有关 CNN 的详细信息,请参阅 Chollet(2017 年,第五章)和 Goodfellow 等人(2016 年,第九章)。

特征和标签数据

下面的 Python 代码首先处理所需的导入和定制。然后导入包含多种金融工具每日结束数据(EOD)的数据集。这个数据集在本书中的不同示例中都有使用:

In [1]: import os
        import math
        import numpy as np
        import pandas as pd
        from pylab import plt, mpl
        plt.style.use('seaborn')
        mpl.rcParams['savefig.dpi'] = 300
        mpl.rcParams['font.family'] = 'serif'
        os.environ['PYTHONHASHSEED'] = '0'

In [2]: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [3]: symbol = 'EUR='  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [4]: data = pd.DataFrame(pd.read_csv(url, index_col=0,
                                        parse_dates=True).dropna()[symbol])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)

In [5]: data.info()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
        <class 'pandas.core.frame.DataFrame'>
        DatetimeIndex: 2516 entries, 2010-01-04 to 2019-12-31
        Data columns (total 1 columns):
         #   Column  Non-Null Count  Dtype
        ---  ------  --------------  -----
         0   EUR=    2516 non-null   float64
        dtypes: float64(1)
        memory usage: 39.3 KB

1

检索并选择金融时间序列数据

接下来的步骤是生成特征数据,延迟数据,将其拆分为训练和测试数据集,最后基于训练数据集的统计数据进行归一化:

In [6]: lags = 5

In [7]: features = [symbol, 'r', 'd', 'sma', 'min', 'max', 'mom', 'vol']

In [8]: def add_lags(data, symbol, lags, window=20, features=features):
            cols = []
            df = data.copy()
            df.dropna(inplace=True)
            df['r'] = np.log(df / df.shift(1))
            df['sma'] = df[symbol].rolling(window).mean()  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
            df['min'] = df[symbol].rolling(window).min()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
            df['max'] = df[symbol].rolling(window).max()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
            df['mom'] = df['r'].rolling(window).mean()  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)
            df['vol'] = df['r'].rolling(window).std()  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)
            df.dropna(inplace=True)
            df['d'] = np.where(df['r'] > 0, 1, 0)
            for f in features:
                for lag in range(1, lags + 1):
                    col = f'{f}_lag_{lag}'
                    df[col] = df[f].shift(lag)
                    cols.append(col)
            df.dropna(inplace=True)
            return df, cols

In [9]: data, cols = add_lags(data, symbol, lags, window=20, features=features)

In [10]: split = int(len(data) * 0.8)

In [11]: train = data.iloc[:split].copy()  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [12]: mu, std = train[cols].mean(), train[cols].std()  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [13]: train[cols] = (train[cols] - mu) / std  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [14]: test = data.iloc[split:].copy()  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)

In [15]: test[cols] = (test[cols] - mu) / std  ![7](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/7.png)

1

简单移动平均特征

2

滚动最小值特征

3

滚动最大值特征

4

时间序列动量特征

5

滚动波动率特征

6

训练数据集的高斯归一化

7

测试数据集的高斯归一化

模型训练

CNN 的实现与 DNN 类似。以下 Python 代码首先处理来自Keras的导入以及设置所有相关随机数生成器种子值的函数定义:

In [16]: import random
         import tensorflow as tf
         from keras.models import Sequential
         from keras.layers import Dense, Conv1D, Flatten
         Using TensorFlow backend.

In [17]: def set_seeds(seed=100):
             random.seed(seed)
             np.random.seed(seed)
             tf.random.set_seed(seed)

以下 Python 代码实现并训练了一个简单的 CNN 模型。该模型的核心是适用于时间序列数据的一维卷积层(详见Keras 卷积层):

In [18]: set_seeds()
         model = Sequential()
         model.add(Conv1D(filters=96, kernel_size=5, activation='relu',
                          input_shape=(len(cols), 1)))
         model.add(Flatten())
         model.add(Dense(10, activation='relu'))
         model.add(Dense(1, activation='sigmoid'))

         model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy'])

In [19]: model.summary()
         Model: "sequential_1"
         _________________________________________________________________
         Layer (type)                 Output Shape              Param #
         =================================================================
         conv1d_1 (Conv1D)            (None, 36, 96)            576
         _________________________________________________________________
         flatten_1 (Flatten)          (None, 3456)              0
         _________________________________________________________________
         dense_1 (Dense)              (None, 10)                34570
         _________________________________________________________________
         dense_2 (Dense)              (None, 1)                 11
         =================================================================
         Total params: 35,157
         Trainable params: 35,157
         Non-trainable params: 0
         _________________________________________________________________

In [20]: %%time
         model.fit(np.atleast_3d(train[cols]), train['d'],
                   epochs=60, batch_size=48, verbose=False,
                   validation_split=0.15, shuffle=False)
         CPU times: user 10.1 s, sys: 1.87 s, total: 12 s
         Wall time: 4.78 s

Out[20]: <keras.callbacks.callbacks.History at 0x7ffe3f32b110>

Figure C-1 展示了训练和验证数据集在不同训练时期的性能指标:

In [21]: res = pd.DataFrame(model.history.history)

In [22]: res.tail(3)
Out[22]:     val_loss  val_accuracy      loss  accuracy
         57  0.699932      0.508361  0.635633  0.597165
         58  0.719671      0.501672  0.634539  0.598937
         59  0.729954      0.505017  0.634403  0.601890

In [23]: res.plot(figsize=(10, 6));

aiif 1701

Figure C-1. CNN 训练和验证的性能指标

测试模型

最后,下面的 Python 代码将训练好的模型应用于测试数据集。CNN 模型明显优于被动基准投资。然而,考虑到以典型(零售)买卖点差的形式存在的交易成本,它会吞噬大部分超额收益。Figure C-2 可视化了随时间的表现:

In [24]: model.evaluate(np.atleast_3d(test[cols]), test['d'])  ![1](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/1.png)
         499/499 [==============================] - 0s 25us/step

Out[24]: [0.7364848222665653, 0.5210421085357666]

In [25]: test['p'] = np.where(model.predict(np.atleast_3d(test[cols])) > 0.5, 1, 0)

In [26]: test['p'] = np.where(test['p'] > 0, 1, -1)  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)

In [27]: test['p'].value_counts()  ![2](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/2.png)
Out[27]: -1    478
          1     21
         Name: p, dtype: int64

In [28]: (test['p'].diff() != 0).sum()  ![3](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/3.png)
Out[28]: 41

In [29]: test['s'] = test['p'] * test['r']  ![4](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/4.png)

In [30]: ptc = 0.00012 / test[symbol]  ![5](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/5.png)

In [31]: test['s_'] = np.where(test['p'] != 0, test['s'] - ptc, test['s'])  ![6](https://gitee.com/OpenDocCN/ibooker-quant-zh/raw/master/docs/ai-fin/img/6.png)

In [32]: test[['r', 's', 's_']].sum().apply(np.exp)
Out[32]: r     0.931992
         s     1.086525
         s_    1.031307
         dtype: float64

In [33]: test[['r', 's', 's_']].cumsum().apply(np.exp).plot(figsize=(10, 6));

1

样本外准确率比例

2

根据预测的仓位(多头/空头)

3

基于仓位预测的交易数目

4

给定买卖点差的比例交易成本

5

在交易成本之前的策略表现

6

在交易成本之后的策略表现

aiif 1702

Figure C-2. 被动基准投资和 CNN 策略的总体表现(交易成本之前/之后)

资源

在本附录中引用的书籍和论文:

  • Chollet, François. 2017. Python 深度学习. Shelter Island: Manning.

  • 《经济学家》. 2016. “从不工作到神经网络。” 《经济学家》 特别报道, 2016 年 6 月 23 日. https://oreil.ly/6VvlS.

  • Gerrish, Sean. 2018. 智能机器如何思考. Cambridge: MIT Press.

  • Goodfellow, Ian, Yoshua Bengio, and Aaron Courville. 2016. 深度学习. Cambridge: MIT Press. http://deeplearningbook.org.

posted @ 2024-06-17 18:15  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报