F--量化金融指南-一-

F# 量化金融指南(一)

原文:zh.annas-archive.org/md5/5c473d71ce4daf0e6aba8b62fa3e518f

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

F#是一种函数式编程语言,允许你为复杂问题编写简洁的代码。目前,它在金融领域被广泛使用。量化金融大量使用数学模型来模拟现实世界。如果你有兴趣使用 F#进行日常工作或量化金融研究,本书将是你的最佳选择。

本书涵盖了使用函数式编程进行量化金融所需了解的所有内容。使用函数式编程语言进行量化金融将使你能够更多地集中在模型本身,而非实现细节。教程和代码片段在整本书中总结为一个交易系统。

F#与.NET 一起提供了广泛的工具,帮助你从原型开发到生产环境中编写高质量和高效的代码。本书中的示例代码片段可以扩展为更大的代码块,并且在函数式语言中容易重用和测试。F#被认为是金融和交易相关应用程序的默认函数式语言之一。

本书内容

第一章,使用 Visual Studio 介绍 F#,让你了解 F#及其在函数式语言中的根基。你将学习如何在 Visual Studio 中使用 F#并编写你的第一个应用程序。

第二章,进一步了解 F#,教你更多关于 F#语言的知识,并阐述了这种范式语言的多面性。

第三章,财务数学与数值分析,介绍了本书中实施金融模型和算法所需的工具集。

第四章,入门数据可视化,介绍了使用 F#进行数据可视化和在 GUI 中显示信息的几种常见方式。

第五章,学习期权定价,教你关于期权、Black-Scholes 公式以及如何使用手头工具探索期权的方法。

第六章,探索波动性,深入探讨了 Black-Scholes 模型的世界,并教你了解隐含波动性。

第七章,入门订单类型和市场数据,以相当务实的方式接触金融,并实现了一个基本的订单管理系统。

第八章,设置交易系统项目,为项目奠定基础,并展示了如何连接 SQL Server 以及使用 LINQ 进行查询。

第九章,通过波动率赚取利润,研究了通过波动率波动和套利机会来赚钱的多种方式,并定义了本项目的交易策略。

第十章,将各部分拼接起来,展示了使用波动率套利策略和 FIX 4.2 的完整交易系统的最终步骤。

本书所需内容

除了对 F#和金融的兴趣外,你还需要一台安装了 Visual Studio 2012 的计算机。Visual Studio 2012 是推荐的 IDE,支持 F# 3.0。

本书适合谁阅读

本书适合任何有兴趣在金融领域编写 F#代码并采用定量方法的人。本书主要目的是为读者提供灵感,并通过大量有效的代码示例来说明金融概念和 F#作为函数式编程语言的应用。

在本书的最后,我们开发了一个简单的波动率套利交易系统。详细解释了订单和 FIX 协议,以及该策略背后的理论。这可以作为任何有兴趣基于期权和波动率开发自己交易系统的人的基础。

约定

在本书中,你将看到多种文本样式,用来区分不同类型的信息。以下是一些样式的示例,并解释了它们的含义。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入等如下所示:“首先,我们在构造函数中设置一个标志,WorkerSupportsCancellation = true,然后每次迭代计算时检查这个标志。”

代码块设置如下:

let rec getSecondLastElement = function
        | head :: tail :: [] -> head
        | head :: tail -> getSecondLastElement tail

当我们希望你关注代码块中的特定部分时,相关的行或项会以粗体显示:

SocketConnectPort=9878
SocketConnectHost=192.168.0.25
FileStorePath=temp

新术语重要词汇以粗体显示。例如,屏幕、菜单或对话框中显示的词汇,在文本中呈现为:“如果你运行这段代码,你将看到一个标题为在 F#中显示数据的表单,如下图所示。”

注意

警告或重要提示会以这样的框显示。

提示

小贴士和技巧如下所示。

读者反馈

我们总是欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢的部分。读者反馈对我们来说很重要,它帮助我们开发出更符合你需求的书籍。

如需向我们提供一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在某个主题上具有专业知识,并且有兴趣编写或参与撰写书籍,请参阅我们的作者指南:www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 图书的骄傲拥有者,我们为您提供了一些帮助,帮助您最大化利用您的购买。

下载示例代码

您可以从您的账户中下载所有已购买 Packt 图书的示例代码文件,网址是www.packtpub.com。如果您是在其他地方购买的本书,您可以访问www.packtpub.com/support,注册后将文件直接通过电子邮件发送给您。

勘误

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

盗版

互联网版权材料的盗版问题在所有媒体中都是一个持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上发现我们作品的任何非法复制品,请立即提供相关位置地址或网站名称,以便我们采取补救措施。

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

我们感谢您在保护我们的作者方面提供的帮助,以及帮助我们为您提供有价值内容的能力。

问题

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

第一章:使用 Visual Studio 介绍 F#

在本章中,你将了解 F# 的历史及其在其他编程语言中的根源。我们还将介绍 Visual Studio 及 F# 的基本语言构造。你将通过交互模式逐步原型化代码,变得更加熟悉如何在 F# 中将各个模块拼接起来构建程序。此外,本章将通过使用和评估代码的 读取-求值-打印循环REPL)来涵盖语言的基础。

在本章中,你将学习:

  • 如何在 Visual Studio 2012 中使用 F#

  • 如何使用 F# Interactive 以全新的探索方式编写代码

  • F# 的基础知识及如何编写你的第一个非玩具应用

  • 函数式编程如何让你更高效

介绍

在我们深入探讨语言本身之前,我们应该先讨论一下为什么我们需要它。F# 是一门强大的语言,这听起来可能像是陈词滥调,但它将多种编程范式结合在一起,实际提升生产力,并且原生支持 .NET 组件和库,以及 公共语言基础设施CLI)。函数式编程长期以来与学术界和专家紧密相关。F# 是为数不多的提供完整环境的语言之一,其成熟度足以让其顺利集成到组织中。

此外,F# 对并行编程有广泛的支持,其中包括异步和多线程等高级特性,这些特性作为语言构造实现,极大地简化了程序员的实现细节。在 F# 中,函数式编程范式是解决问题的主要哲学。其他范式,如面向对象编程和命令式编程,被优先作为补充和辅助来支持这一主要范式。它们的共存是基于兼容性和实际生产力的考虑。

入门 Visual Studio

我们将从介绍 Visual Studio 作为本书的主要工具开始。虽然可以使用独立的 F# 编译器和你喜欢的编辑器,但使用 Visual Studio 2012,你会更高效,我们将在本书中始终如一地使用它。

F# 自 2010 年以来就已成为 Visual Studio 的一部分。在本书中,我们将使用 Visual Studio 和 F# 的最新版本。这将使我们能够使用 Visual Studio 2012 和 F# 3.0 中最新的功能和改进。

F# 是开源的,这意味着你可以在任何支持的平台上使用它;它不依赖于 Microsoft 或 Visual Studio。在其他 IDE 中,如 MonoDevelop,也有良好的支持,它能在 Linux 和 Mac OS X 上运行。

注意

有关 F# 和 F# 软件基金会的更多信息,请访问 fsharp.org

创建一个新的 F# 项目

在 Visual Studio 中创建一个新的 F# 项目,本指南将使用该项目来探索基础知识,具体内容见下文各节。

在 Visual Studio 中创建一个新项目

通过以下步骤,我们可以在 Visual Studio 中创建一个新项目:

  1. 要创建你的第一个 F# 项目,打开 Visual Studio 12,并导航到文件 | 新建 | 项目,然后在菜单中选择新建项目在 Visual Studio 中创建新项目

  2. 现在你将看到新建项目窗口出现。选择左侧面板中的F#,然后选择F# 应用程序。你可以给它命名任何你喜欢的名称。最后,点击确定在 Visual Studio 中创建新项目

  3. 现在你已经创建了你的第一个 F# 应用程序,它将仅仅打印传递给它的参数。

理解程序模板

让我们简要看一下 Visual Studio 生成的程序模板。

理解程序模板

如果你运行这个程序,它将仅仅打印出传递给它的参数,你将看到一个终端窗口出现。

理解程序模板

上述截图中的[<EntryPoint>]函数是主函数,它告诉 Visual Studio 使用该函数作为程序可执行文件的入口点。我们暂时不深入讨论这个程序模板,但在接下来的三章中,我们将在构建交易系统时回到这个话题。

添加一个 F# 脚本文件

我们将使用 F# 脚本文件来代替更交互式地探索语言的基础知识,而是在查看标准程序模板之后进行操作。你可以将 F# 脚本文件视为笔记本,在其中你可以逐步探索可执行的代码:

  1. 通过右键点击代码编辑器右侧的解决方案资源管理器,添加 F# 脚本文件。

  2. 然后,导航到添加 | 新建项…,如以下截图所示:添加一个 F# 脚本文件

  3. 你可以为脚本文件命名任何你喜欢的名称,比如GettingStarted.fsx添加一个 F# 脚本文件

现在我们已经在 Visual Studio 中设置好了基本的项目结构,接下来让我们继续探索 F# Interactive。

理解 F# Interactive

F# Interactive 是一种交互式执行程序部分的方式。通过这种方式,程序员可以探索代码的不同部分及其行为。这样,你会对编写代码有更动态的感受,也更有趣。F# Interactive 是 F# 的 REPL,这意味着它会读取代码、评估它,然后输出结果。接着,它会反复执行这一过程。这就像命令行一样,代码被执行,结果展示给用户。

要在 F# Interactive 中执行代码,请查看以下步骤:

  1. 选择你感兴趣的源代码,然后按Alt + Enter

  2. 你可以写一行简单的代码,它将仅仅打印一个字符串到 REPL 的输出窗口:

    printfn "Hello World, from F"
    
  3. 你也可以右键点击选中的代码,选择在交互式中执行理解 F# 交互式

    在使用交互模式执行代码时,结果会显示在代码编辑器下方的F# 交互式评估窗口中。也可以输入代码片段到交互式窗口中,这样做有时更为合适,正如以下示例所展示的。

  4. F# 交互式窗口中输入以下代码并按Enter键:

    printfn "Hello World, from F#";;
    
  5. 这将在 REPL 中被评估为以下内容:

    > printfn "Hello World, from F#";;
    Hello World, from F#
    val it : unit = ()
    

    在代码行后使用双分号(;;)将终止输入,并允许你直接按Enter键,它们在你直接在终端窗口中输入时是必须的。

  6. 如果你想取消评估,可以右键点击并选择取消交互式评估,或者直接按Ctrl + Break理解 F# 交互式

语言概览

我们现在将开始使用 F# 进行函数式编程的旅程,并探索其在定量金融应用中的能力。

我们首先来看一下如何声明值,也就是如何将值绑定到名称,及其可变性和不可变性。

要初始化并创建一个值,请使用let关键字。let将右侧的值绑定到等号左侧的变量名。这是一个绑定操作符,很像数学中的操作。

let sum = 4 + 5
let newsum = sum + 3

let绑定也可以用来将函数绑定到名称上,正如我们将在接下来的章节中看到的那样。

解释可变性和不可变性

一旦变量被定义为具有特定值,它将一直保持该值。对此有一些例外情况,且可以使用遮蔽来覆盖同一作用域内先前的赋值。因此,数学中的变量是不可变的。同样,F#中的变量也是不可变的,但有一些例外。

不可变变量是 F# 中的默认值。它们很有用,因为它们是线程安全的,更容易推理。这也是你最近可能听到很多关于不可变性的原因之一。这个概念旨在解决并发编程中的最大问题和设计缺陷,包括共享的可变状态。如果值不会改变,那么就不需要保护它们,这也是推动并发编程中不可变性的原因之一。

如果你尝试修改不可变变量的值,你将遇到类似以下的消息:

let immutable = "I am immutable!"
immutable <- "Try to change it..."
… error FS0027: This value is not mutable

然而,有时需要拥有可变变量。在实际应用中,当某些全局状态被共享(例如计数器)时,经常会有这种需求。此外,面向对象编程和与其他 .NET 语言的互操作性使得使用可变性变得不可避免。

要创建一个可变变量,你只需在名称前面加上关键字mutable,如下面的代码所示:

let mutable name = firstname + lastname

要在创建变量后更改其值,请使用箭头操作符(),如下代码所示:

name ← "John Johnson"

这与其他语言稍有不同。但一旦你理解了这个概念,就能更好地理解它。事实上,它很可能会成为未来推理变量的主要方式之一。

原始类型

F# 看起来可能像是 JavaScript、Ruby 或 Python 等动态类型语言。实际上,F# 是像 C#、C++ 和 Java 一样的静态类型语言。它通过类型推断来确定正确的类型。类型推断是一种通过分析代码自动推导出类型的技术。这种方法在几乎所有情况下都能很好地工作。然而,有时程序员需要对编译器进行明确的说明。这可以通过类型注解实现,我们将在接下来的章节中进一步探讨这一概念。

让我们通过 REPL 探索一些 F# 内建类型。

> let anInt = 124;;

val anInt : int = 124

这意味着 F# 推断 anInt 的类型为 int。它仅仅通过推断左边的类型与右边赋值的类型一致。逻辑上,赋值操作符两边的类型必须相同,不是吗?

我们可以将分析扩展到浮点数,如下代码所示:

> let anFloat = 124.00;;

val anFloat : float = 124.0

因为有小数点符号,类型被确定为 float 类型。同样的规则适用于 double,如下代码所示:

> let anDouble : double = 1.23e10;;

val anDouble : double = 1.23e+10

对于其他类型,其工作方式与预期一致,如下所示:

> let myString = "This is a string";;

val myString : string = "This is a string"

除了 unit 类型,所有原始的内建类型都有对应的 .NET 类型。

以下表格展示了 F# 中最常见的原始类型:

类型 .NET 类型 描述
bool Boolean true 或 false
byte Byte 0 到 255
int Int32 -128 到 127
int64 Int64 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
char Char 0 到 18,446,744,073,709,551,615
string String Unicode 文本
decimal Decimal 浮动数据类型
unit - 缺少实际值
void Void 无类型或值
float Single 64 位浮点值
double Double 与上述相同

注意

更多信息及所有类型,可以访问 msdn.microsoft.com/en-us/library/dd233210.aspx.

语言中还有其他类型,这些类型将在下一章中详细讲解,例如列表、数组、序列、记录和区分联合。

解释类型推断

类型推导意味着编译器会根据程序员提供的表达式上下文信息,自动推断代码中表达式的类型。类型推导分析代码,如你在前面的章节中所看到的,确定程序员通常可以轻松识别的类型。这使得程序员无需显式定义每个变量的类型。在前面的章节中,我们看到的简单整数和浮点数赋值不需要定义类型就能理解代码。类型推导将使代码更易于编写,从而更易于阅读,省去了不必要的冗余。

解释函数

现在是时候了解函数了,它是 F#以及任何其他函数式编程语言中最基本且强大的构建块。函数式编程语言将函数视为一等构造,与面向对象编程不同,在面向对象编程中,对象和数据是第一类构造。这意味着在函数式编程中,函数根据输入生成数据,而不是依赖状态。在面向对象编程中,状态被封装到对象中并传递。在之前的代码片段中,函数的声明方式与变量声明方式相同,都是使用let绑定。请看以下代码片段:

let sum (x,y) =
	x + y
> sum (7, 7)

如果你尝试使用Alt + Enter来评估第一个sum函数,F#交互式会返回如下类似的函数代码:

val sum : x:int -> y:int -> int

这意味着sum是一个接受两个int类型值并返回一个int类型值的函数。编译器只需要知道最后一个类型就是返回类型。

let sum (x:float, y:float) =
	x + y

> sum(7.0, 7.0);;
val it : float = 14.0

让我们来看一个传递错误类型参数给函数的例子:

> sum(7, 7);;
...
error FS0001: This expression was expected to have type float
but here has type int

如修改版的sum函数所示,类型被显式声明为 float。这是一种提前告诉编译器,函数中将使用 float 类型的值的方式。sum 函数的第一个版本使用类型推导来计算xy的类型,并发现它们是int类型。

了解匿名函数

由于在 F#编程中创建小型辅助函数很常见,F#还提供了一个用于创建匿名函数的特殊语法。这些函数有时被称为 lambda,或 lambda 函数。定义匿名函数时,使用关键字fun。请看以下代码片段:

let square = (fun x → x * x)
> square 2
val it : int = 4

解释高阶函数

现在,平方函数可以单独使用,也可以作为其他函数或高阶函数的参数。请看以下平方函数:

let squareByFour f
	f 4
> squareByFour square

这里,平方函数作为参数传递给函数squareByFoursquareByFour是一个高阶函数,它接收另一个函数作为参数。高阶函数可以接受一个函数作为参数或返回一个函数,或者两者兼有。这是函数式编程中常用的技术,能够从现有函数构建新函数并重用它们。

柯里化

尽管柯里化有时被认为是编程语言的高级特性,但它与函数和高阶函数的关联使得它最具意义。这个概念一点也不复杂,一旦你看过几个例子,概念应该就很清楚了。

让我们来看一下下面的sum函数:

let sum x y =
	x + y

假设我们想要重用这个函数,但我们可能经常对某个固定的x值进行调用。这意味着我们有一个固定的x,假设是2,然后我们改变y参数。请看以下示例:

sum 2 3
sum 2 4
sum 2 5

不必每次都写出x参数,我们可以利用柯里化的概念。这意味着我们可以创建一个新的函数,在这种情况下第一个参数被固定。看一下下面的函数:

let sumBy2 y = 
	sum 2 y

> sumBy2 3;;
val it : int = 5

> sumBy2 4;;
val it : int = 5

> sumBy2 5;;
val it : int = 5

现在我们避免了重写一些参数,但这并不是主要原因。主要原因是能够控制参数并重用功能。更多关于柯里化的内容将在后续章节中介绍,但这里已经涵盖了与高阶函数相关的基础知识。

调查列表

在 F#中,列表非常有用,它们是最常用的构建块之一。它们是函数式语言中的基本构建块,通常可以替代其他类型或类。这是因为支持操作和创建列表的功能,以及能够嵌套列表,足以替代自定义类型。你可以将列表看作是同一类型值的序列。

F#中的列表如下:

  • 存储数据的一种强大方式

  • 不可变的值列表,支持任何类型

  • 常作为构建块使用

  • 存储数据的最佳方式之一

调查列表

这展示了 F#中的一个列表,具有头部和尾部,其中每个元素与下一个元素相连。

假设我们有一个简单的价格信息列表,这些信息以浮动点表示:

let prices = [45.0; 45.1; 44.9; 46.0]
> val prices : float list = [45.0; 45.1; 44.9; 46.0]

假设你想要一个值在 0 到 100 之间的列表,F#可以帮你实现,而不需要你手动编写。请看以下代码:

let range = [0 .. 100]
val range : int list =
  [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; ...]

如果我们只需要一个固定增量的简单范围,这样做是可以的。然而,有时你可能需要更小的增量,比如 0.1,范围是从 1.0 到 10.0。以下代码展示了如何实现:

let fineRange = [1.0 .. 0.1 .. 10.0]
val fineRange : float list =
[1.0; 1.1; 1.2; 1.3; 1.4; 1.5; 1.6; 1.7; 1.8; 1.9; 2.0; 2.1; 2.2; 2.3; 2.4; 2.5; 2.6; 2.7; 2.8; 2.9; 3.0; 3.1; 3.2; 3.3; 3.4; 3.5; 3.6; 3.7; 3.8; 3.9; 4.0; 4.1; 4.2; 4.3; 4.4; 4.5; 4.6; 4.7; 4.8; 4.9; 5.0; 5.1; 5.2; 5.3; 5.4; 5.5; 5.6; 5.7; 5.8; 5.9; 6.0; 6.1; 6.2; 6.3; 6.4; 6.5; 6.6; 6.7; 6.8; 6.9; 7.0; 7.1; 7.2; 7.3; 7.4; 7.5; 7.6; 7.7; 7.8; 7.9; 8.0; 8.1; 8.2; 8.3; 8.4; 8.5; 8.6; 8.7; 8.8; 8.9; 9.0; 9.1; 9.2; 9.3; 9.4; 9.5; 9.6; 9.7; 9.8; 9.9; 10.0]

列表可以是任何类型的,类型推断在这里也能正常工作。请看下面的代码:

> let myList = ["One"; "Two"; "Three"];;
val myList : string list = ["One"; "Two"; "Three"]

然而,如果你在列表中混合不同类型,编译器会对实际使用的类型产生困惑:

let myList = ["One"; "Two"; 3.0];; 
...
This expression was expected to have type
string but here has type float 

提示

下载示例代码

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

列表连接

列表连接在你想将多个列表合并时非常有用。可以使用@操作符来实现这一点。请看以下代码,其中使用了@操作符:

> let myNewList = [1;2;3] @ [4;5;6];;

val myNewList : int list = [1; 2; 3; 4; 5; 6]

> myNewList;;
val it : int list = [1; 2; 3; 4; 5; 6]

让我们看一下列表模块中最常用的一些函数:LengthHeadTailmapfilter

函数Length将简单地返回列表的长度:

> myNewList.Length;;
val it : int = 6

如果你想要列表的第一个元素,使用Head

> myNewList.Head;;
val it : int = 1

列表的其余部分,意味着除Head之外的所有其他元素,被定义为Tail

> myNewList.Tail;;
val it : int list = [2; 3; 4; 5; 6]

你还可以对列表做一些更有趣的事情,比如逐个计算所有元素的平方。请注意,它返回的是一个全新的列表,这是因为列表是不可变的。这是通过高阶函数完成的,其中List.map接受一个返回x*x值的 lambda 函数,如以下代码所示:

> List.map (fun x -> x * x) myNewList;;
val it : int list = [1; 4; 9; 16; 25; 36]

另一个有趣的函数是列表的filter函数,它将返回一个符合过滤条件的新列表:

> List.filter (fun x -> x < 4) myNewList;;
val it : int list = [1; 2; 3]

元组

元组是一组无名但有序的值。根据需要,值可以是不同类型的。你可以把它们看作是 C#中的元组类的更灵活版本。

// Tuple of two floats
(1.0, 2.0)

// Tuple of mixed representations of numbers
(1, 2.0, 3, '4', "four")

// Tuple of expressions
(1.0 + 2.0, 3, 4 + 5)

让我们在 REPL 中分析元组的类型信息。第一个元组具有以下类型信息:

> (1.0, 2.0);;
val it : float * float = (1.0, 2.0)

*符号用于分隔元组的类型元素。它只是一个包含两个浮点数的元组。下一个稍微复杂一点:

> (1, 2.0, 3, '4', "four");;
val it : int * float * int * char * string = (1, 2.0, 3, '4', "four")

但是类型推断没有任何疑问地弄清楚了。最后一个包含表达式:

> (1.0 + 2.0, 3, 4 + 5);;
val it : float * int * int = (3.0, 3, 9)

正如你所看到的,表达式在类型数据分析之前被求值。提取元组中的值可能是有用的,这可以通过简单的模式来完成:

let (a, b) = (1.0, 2.0)
printfn "%f %f" a b

如果你对第一个值不感兴趣,可以使用通配符字符(下划线)来简单地忽略它。通配符在 F#中广泛使用,例如在模式匹配中,在下一章将介绍这一点。

let (_, b) = (1.0, 2.0)
printfn "only b %2.2f" b

管道操作符

管道操作符使用得很频繁,它被定义为一个函数,该函数将操作符左边的值传递给右边的函数并应用它们。还有一个带有不同参数数量的管道操作符版本,关于它们的更多内容将在后面讲解。

管道前向操作符(|>)是最常见的管道操作符:

[0..100]|> List.filter (fun x -> x % 2 = 0)|> List.map (fun x -> x * 2)|> List.sum

这段代码首先创建一个从 0 到 100 的列表,如前面关于列表的部分所示。然后,列表被传递给 filter 函数,使用条件 lambda 函数。列表中的每个偶数值都会传递给下一个函数。map 函数会执行 lambda 函数,对每个数字进行平方。最后,所有的数字都会被求和,结果是:

val it : int = 5100

编写代码文档

编写代码文档是一个值得养成的好习惯。你还记得几周前你写的代码的细节吗?再想象一下,几年前你写的代码会是什么样的。这就是文档发挥作用的地方。仅仅对逻辑的一些提示就足够让你和你的同事理解背后的主要概念。

(*
This is a comment on multiple lines
*)

/// Single line comment, supporting XML-tags

// This is also a single line comment

你的第一个应用

第一个有用的应用将是这个金融领域的 Hello World,它将展示 F# 和函数式语言的一些强大而简单的概念和优点。

让我们通过一个简单但富有启发性的例子来开始我们的定量金融之旅,使用 Yahoo 财务数据。首先,我们将数据输入到代码中,以便熟悉基本概念。

首先,我们输入一些数据。在 F# 中,你可以像下面的代码那样在多行上声明一个字符串列表:

/// Sample stock data, from Yahoo Finance
let stockData = [
    "2013-06-06,51.15,51.66,50.83,51.52,9848400,51.52";
    "2013-06-05,52.57,52.68,50.91,51.36,14462900,51.36";
    "2013-06-04,53.74,53.75,52.22,52.59,10614700,52.59";
    "2013-06-03,53.86,53.89,52.40,53.41,13127900,53.41";
    "2013-05-31,54.70,54.91,53.99,54.10,12809700,54.10";
    "2013-05-30,55.01,55.69,54.96,55.10,8751200,55.10";
    "2013-05-29,55.15,55.40,54.53,55.05,8693700,55.05"
]

我们引入了一个用于按逗号分割字符串的函数;这将创建一个字符串数组。不要忘记在 F# Interactive 中使用 Alt + Enter 评估程序的各个部分。通过这样练习,可以减少错误数量,并且你会变得更加熟悉,理解涉及的类型。

stockData 值的类型没有显式声明,但如果你评估它,你应该看到它是 string list 类型:

val stockData : string list =
  ["2013-06-06,51.15,51.66,50.83,51.52,9848400,51.52";
   ...
   "2013-05-29,55.15,55.40,54.53,55.05,8693700,55.05"]

// Split row on commas
let splitCommas (l:string) =
    l.Split(',')

// Get the row with lowest trading volume
let lowestVolume =
    stockData
    |> List.map splitCommas
    |> List.minBy (fun x -> (int x.[5]))

评估表达式 lowestVolume 会解析 stockData 中的字符串,并提取出交易量最小的那一行,第六列。希望结果是包含日期 2013-05-29 的那一行,如下所示:

val lowestVolume : string [] =
  [|"2013-05-29"; "55.15"; "55.40"; "54.53"; "55.05"; "8693700";"55.05"|]

整个程序

以下是我们在上一部分开发的程序代码清单,即金融领域的 Hello World 程序。你可以自己尝试并根据需要做出更改:

/// Open the System.IO namespace
open System.IO

/// Sample stock data, from Yahoo Finance
let stockData = [
    "2013-06-06,51.15,51.66,50.83,51.52,9848400,51.52";
    "2013-06-05,52.57,52.68,50.91,51.36,14462900,51.36";
    "2013-06-04,53.74,53.75,52.22,52.59,10614700,52.59";
    "2013-06-03,53.86,53.89,52.40,53.41,13127900,53.41";
    "2013-05-31,54.70,54.91,53.99,54.10,12809700,54.10";
    "2013-05-30,55.01,55.69,54.96,55.10,8751200,55.10";
    "2013-05-29,55.15,55.40,54.53,55.05,8693700,55.05"
]

/// Split row on commas
let splitCommas (l:string) =
    l.Split(',')

/// Get the row with lowest trading volume
let lowestVolume =
    stockData
    |> List.map splitCommas
    |> List.minBy (fun x -> (int x.[5]))

理解程序

管道操作符使程序的逻辑非常直观。程序首先获取 stockData 列表,按逗号分割,然后选择特定的列并应用数学运算符。接着,它选择这些计算中的最大值,最后返回符合 minBy 条件的行的第一列。你可以把它看作是构建模块,其中每一部分都是一个独立的函数。将多个函数组合成强大的程序,是函数式编程背后的理念。

扩展示例程序

让我们扩展前面的程序,改为从文件中读取数据。由于在代码中显式声明数据在长远来看并不那么有用,因为数据会变化。在扩展的过程中,我们还将介绍异常及其在.NET 中的使用。

我们首先编写一个简单的函数来读取文件的所有内容,其中文件路径作为参数传递。参数的类型是字符串,如你在函数头部看到的类型注释。注释的使用通常是编译器无法自行推断类型时,或者作为程序员你想要明确指定或强制使用某种类型时。

/// Read a file into a string array
let openFile (name : string) =
    try
        let content = File.ReadAllLines(name)
        content |> Array.toList
    with
        | :? System.IO.FileNotFoundException as e -> printfn "Exception! %s " e.Message; ["empty"]

如果文件未找到,函数将捕获FileNotFoundException。在异常类型前还有一个新的运算符(:?)。这是一个类型测试运算符,如果值与指定的类型匹配,则返回 true,否则返回 false。

让我们修改前面的代码,使用从文件中加载的内容,而不是预先编码的股票价格。

/// Get the row with lowest trading volume, from file
let lowestVolume =
    openFile filePath
    |> List.map splitCommas
    |> Seq.skip 1
    |> Seq.minBy (fun x -> (int x.[5]))

代码需要进行一些小的修改,以便能够处理来自逗号分隔值CSV)文件的输入。与管道的输入一样,我们使用openFile函数的调用结果。然后,我们像之前一样按逗号进行拆分。需要有一种方法来跳过第一行;在 F#中这很容易实现,你只需插入Seq.skip n,其中 n 是要跳过的序列元素的数量。

printfn "Lowest volume, found in row: %A" lowestVolume

在这里,我们简单地使用printfn并格式化为%A,这样就可以接受任何内容并格式化输出(非常方便)。

让我们再看一个有用的字符串格式化器的例子:

> printfn "This works for lists too: %A" [1..5];;
This works for lists too: [1; 2; 3; 4; 5]
val it : unit = ()

整个程序

让我们看看整个程序的代码,这是我们在前一部分中看到的。

/// Open the System.IO namespace
open System.IO

let filePath = @" table.csv"

/// Split row on commas
let splitCommas (l:string) =
    l.Split(',')

/// Read a file into a string array
let openFile (name : string) =
    try
        let content = File.ReadAllLines(name)
        content |> Array.toList
    with
        | :? System.IO.FileNotFoundException as e -> printfn "Exception! %s " e.Message; ["empty"]

/// Get the row with lowest trading volume, from file
let lowestVolume =
    openFile filePath
    |> List.map splitCommas
    |> Seq.skip 1
    |> Seq.minBy (fun x -> (int x.[5]))

/// Use printfn with generic formatter, %A
printfn "Lowest volume, found in row: %A" lowestVolume

原型设计的力量

使用 Visual Studio 的交互模式,并能够通过原型设计以更小的构建块编写程序,是编写软件的一个好方法。你已经通过第一个应用程序使用了这种探索性的编程方式。

工作流程是逐步构建程序,而不是一次性运行所有代码。REPL 是一个完美的地方,可以尝试代码片段并实验 F#的不同方面。

功能性语言在量化金融中的应用

在前面的示例代码中,我们看到从文件中解析数据并提取各种信息是直接且简单的,产生的代码既易于阅读又易于理解。这是 F#的亮点之一,尤其在量化金融中,这一点尤为重要,因为在许多语言中,代码可能复杂且难以跟踪和理解。

让我们通过另一个例子来说明前述的说法。前一个示例应用中的 CSV 文件数据是按最新日期排序的。如果我们希望数据以更自然的方式排序,最早的日期排在前面,我们可以简单地通过以下方式反转整个列表:

/// Reverses the price data from the CSV-file
let reversePrices =
    openFile filePath
    |> List.map splitCommas
    |> List.rev

理解命令式代码和互操作性

假设我们对解析示例中股票价格的日期列感兴趣。整行数据大致如下所示:

 [|"2013-02-22"; "54.96"; "55.13"; "54.57"; "55.02"; "5087300"; "55.02"|]

我们对第一列感兴趣,索引为 0:

lowestVolume.[0];;	
val it : string = "2013-02-22"

我们可以利用 .NET 中 System.DateTime 命名空间下的日期和时间类:

> let dateTime = System.DateTime.ParseExact(lowestVolume.[0], "yyyy-mm-dd", null);;

val dateTime : System.DateTime = 2013-01-22 00:02:00

现在我们有了一个System.DateTime对象,它与 C# 以及其他 .NET 语言兼容,可以用来处理时间!

总结

在这一章,我们初步了解了如何使用 Visual Studio 编写 F# 编程。我们涵盖了语言的各种基础知识,并浅尝了函数式编程(functional programming),其中不可变性(immutability)扮演了关键角色。在整章中,我们通过展示一些 F# 语言特性并说明如何利用 .NET 框架(framework),让大家对 F# 编程有了初步的认识。在本章结束时,我们实现了一个简单的应用,展示了 F# 的强大功能和优雅语法。函数是任何函数式编程语言中的核心构件。从现有函数构建新函数是一种抽象复杂性的方式,并且能实现复用。

在下一章,我们将深入了解更多关于 F# 语言的细节。你将学习更多的数据结构,如列表(Lists)、序列(Sequences)和数组(Arrays)。你还将学习如何使用模块(modules)和命名空间(namespaces)来组织你的程序,这些内容在更大的程序中会变得非常有用。下一章还将介绍线程(threads)、线程池(thread pools)、使用 .NET 进行异步编程(asynchronous programming)以及 F# 语言特有的语言构造(language-specific constructs)。

第二章:进一步了解 F#

本章是关于 F# 语言本身的更详细内容。教程方法将继续使用 Visual Studio 进行演示,并结合本书后半部分涉及构建最终交易系统的语言细节。本章将涵盖大多数语言构建块,并通过解释和示例进行讲解。虽然本章内容较多,但要理解这里提供的内容,对于后续的学习至关重要。

本章你将学习:

  • 将程序结构化为模块和命名空间

  • 更多关于数据结构和类型的内容

  • 递归函数及其在函数式编程中的作用

  • 模式匹配

  • 结合函数式和面向对象的思想

  • F#的命令式部分

  • F#中的并行和异步编程模型

提示

下载 Microsoft Research 提供的 F# 3.0 语言规范,并在本章及本书其余部分的学习过程中一同查阅。该规范提供了许多有用的细节,并解答了你作为读者可能提出的疑问。

结构化你的 F# 程序

当你编写较大的程序时,能够将代码结构化为层次化的抽象层次变得至关重要。这使得构建大型程序、重用现有代码以及让其他人理解代码成为可能。在 F# 中,有命名空间、模块和面向对象的结合,以及类型和数据结构来实现这一点。在面向对象编程中,可以使函数和变量变为私有,并禁用外部访问。面向对象编程将在后续章节单独讲解。

正如你可能在 Visual Studio 中看到的,当你创建 F# 项目时,会有各种类型的源代码文件。根据文件的用途,它们有不同的扩展名。在本书中,我们使用 .fs.fsx 文件。前者是一个 F# 源代码文件,需编译后用于可执行程序中;后者 .fsx 是用于 F# 脚本和交互模式的原型开发。脚本非常适合快速原型开发和探索性开发,但不适合用于较大的程序。

接下来,我们将介绍在 F# 中用于将代码结构化成优雅、可维护且符合逻辑的结构的最常见和最有用的技术。

探讨模块

模块有助于组织和结构化相关代码。它们是将代码组织成更高抽象层次的简单而优雅的方式。可以将其视为值、类型和函数值等声明的命名集合。你已经在不自觉中使用了模块。所有文件会自动声明为与文件同名的模块。F# 交互模式也是如此,每次执行的内容都会被包装成一个独立的模块。

例如,考虑一个名为 application.fs 的文件,其内容如下:

let myval = 100

该文件将以与显式声明为模块相同的方式进行编译:

module application

let myval = 100

这意味着你不必在每个文件中显式声明模块来实现这一点。

在模块中使用函数和变量

模块看起来很像面向对象中的类。它们甚至有访问注解来指定访问其声明的成员的规则。模块和类之间的主要区别是,类可以被看作是定义新类型,而模块是功能的集合,且在写类之前可能不需要知道具体的细节。

以下示例演示了如何声明具有值和函数的模块及嵌套模块,以及如何访问这些成员:

module MainModule =
    let x = 2
    let y = 3
    module NestedModule =
        let f = 
            x + y

printfn "%A" MainModule.NestedModule.f

如你所见,模块可以嵌套,这使得程序员能够以优雅的方式组织代码。模块NestedModule中的f函数可以访问父模块中的xy值,而不必显式写出父模块的名称。

声明和使用模块的顺序至关重要,因为它们是按从上到下的顺序依次处理的。以下代码片段将在第一个let语句中找不到Module2

module Module1 =
    let x = Module2.Version() // Error: Not yet declared!

module Module2 =
    let Version() = 
        ""Version 1.0"

反转顺序后,错误得以解决:

module Module2 =
    let Version() = 
        ""Version 1.0"

module Module1 =
    let x = Module2.Version() // Now Module2 is found

模块的每个成员变量默认是公开的。这意味着每个成员都有一个由编译器设置的默认可访问性。让我们回到以下示例,并将函数Version的可访问性更改为 private:

module Module2 =
    let private Version() = 
        "Version 1.0"

module Module1 =
    let x = Module2.Version() // Error: Version is private!

如你所见,如果你将其输入到编辑器中,由于 private 标注会出现错误。这非常整洁。在这种情况下,使用 internal 注解可能更为合适,这意味着成员仅能在同一程序集内访问。

module Module2 =
    let internal Version() = 
        "Version 1.0"

module Module1 =
    let x = Module2.Version() // Now it works again, Version is set to be internal

可用的修饰符有 public、internal 和 private。public 修饰符表示标注的函数或变量可以被所有代码访问,而 private 表示该函数或变量只能从封闭模块中访问。

命名空间

命名空间是模块、类和其他命名空间的层次化分类。首先,我们来看一下如何声明命名空间。命名空间必须是代码文件中的第一个声明。当你希望区分功能而不必在模块或类前面加上冗长的名称时,命名空间非常有用。它们还可以最大限度地减少代码与现有代码之间的命名冲突。以下是之前讨论的代码,已被添加到一个命名空间中:

namespace Namespace1.Library1

    module Module2 =
        let internal Version() = 
            "Version 1.0"

    module Module1 =
        let x = Module2.Version()

这将告诉编译器我们处于名为Namespace1.Library1的命名空间中。命名空间还强制程序员使用 F#、C#及其他.NET 语言程序员熟悉的模式。命名空间是开放的,这意味着许多源文件可以贡献到同一个命名空间中。在同一文件中也可以有多个命名空间:

namespace Namespace1.Library1

    module Module2 =
        let internal Version() = 
            "Version 1.0"

namespace Namespace1.Library2

    module Module1 =
        let x = Namespace1.Library1.Module2.Version()

上面的示例展示了如何使用命名空间以及如何访问命名空间中的模块,这需要使用完整限定名。如果在 F# 中使用 open 关键字,则无需使用完整限定名,它相当于 C# 中的 using。完整限定名指的是完整的名称,就像在最后的 let 语句中访问 Version 函数时所使用的名称。

对于命名空间,私有和公共修饰符也存在,并且与模块的工作方式相同,不同之处在于它们作用于代码中的命名空间级别。此外,还存在更细粒度的控制机制,例如 [<AutoOpen>],它会自动打开命名空间中的模块。当需要在命名空间内定义 let 语句以定义值时,这非常方便。

深入探讨数据结构

在前一章中,我们介绍了 F# 中的一些数据结构,并简单了解了它们的功能。在本节中,我们将更深入地探讨许多在程序中常用的数据结构和表达式。

以下是将要介绍的内容,并附有简短的描述,总结它们的主要特征:

  • 记录类型:记录类型用于表示数据,并通过组合命名值和类型将数据片段组合在一起。

  • 区分联合:区分联合用于表示异构数据,并支持可以是多个命名情况的数据。

  • 枚举:F# 中的枚举几乎与其他语言中的枚举相同,用于将标签映射到常量值。

  • 数组:数组是固定大小的集合,必须包含相同类型的值。大型常量数组可以编译为高效的二进制表示。

  • 列表:列表是有序的集合,包含相同类型的元素,通常实现为链表。

  • 序列:在 F# 中,序列是惰性求值的,表示为逻辑元素序列,其中所有元素必须具有相同的类型。它们特别适合表示一个大的有序数据集合,在该集合中,并非所有元素都会被使用。

  • 集合:集合是无序的容器,用于存储唯一的数据元素。集合不会保留插入时元素的顺序,也不允许重复元素。

  • 映射:映射是用于存储键/值对的关联容器。

  • 选项:选项是一种优雅的方式,用于封装可能存在或不存在的值。它是通过区分联合(discriminated union)来实现的。与检查空值不同,选项更为常用。

  • 字符串:字符串是字符的序列,和 .NET 中的字符串相同。

记录类型

记录类型用于表示数据,并通过组合命名值和类型将数据片段组合在一起。

假设我们有兴趣使用记录类型来建模一个开盘-最高-最低-收盘OHLC)柱状图,它可能如下所示:

type OHLC = 
    {
    o: float
    h: float
    l: float
    c: float
    }

然后,我们可以使用先前定义的记录来声明一个变量:

let ohclBar : OHLC = {o = 1.0; h = 2.0; l = 3.0; c = 4.0} 

让我们考虑另一个示例,在这个示例中,我们用bidaskmidpoint来建模一个报价。midpoint()函数将从bidask值计算出来,即两者的平均值。可以像这样使用记录类型和成员函数来实现:

type Quote =
    {
    bid : float
    ask : float
    }
    member this.midpoint() = (this.bid + this.ask) / 2.0

let q : Quote = {bid = 100.0; ask = 200.0} 
q.midpoint()

如你所见,它与第一个示例非常相似,唯一不同的是成员函数midpoint。成员函数可以访问记录类型的字段。

假设我们有兴趣在初始化后修改字段,只需将mutable关键字添加到感兴趣的字段即可:

type Quote =
    {
    mutable bid : float
    mutable ask : float
    }
    member this.midpoint() = (this.bid + this.ask) / 2.0

let q : Quote = {bid = 100.0; ask = 200.0} q.midpoint()
q.bid <- 150.0
q.midpoint()

这个示例与之前讨论的类似,但在这里我们可以将bid字段的值更改为150.0

让我们看看如何在模式匹配中使用记录类型,因为这是使用记录类型的最大原因之一:

let matchQuote (quote : Quote) =
match quote with
   | { bid = 0.0; ask = 0.0 } -> printfn "Both bid and ask is zero"
   | { bid = b; ask = a } -> printfn "bid: %f, ask: %f" b a

let q1 : Quote = {bid = 100.0; ask = 200.0}
let q2 : Quote = {bid = 0.0; ask = 0.0}

matchQuote q1
matchQuote q2

简而言之,记录类型是:

  • 用于表示数据

  • 在模式匹配中很有用

  • 用于将数据块组合在一起

  • 很像面向对象中的对象

  • F#的强大且有用的特性

  • 类型的命名值组合

  • 与类不同,因为它们作为属性暴露,并且没有构造函数

判别联合

判别联合用于表示异质数据,并支持可以是一组命名的情况的数据。判别联合表示一个有限且明确的选择集。判别联合通常是构建更复杂数据结构的首选工具,包括链表和各种树形结构。

让我们通过查看一个示例来研究判别联合的一些特性及其如何使用。在这里,我们定义一个类型OrderSide,它可以是买方或卖方。

type OrderSide =
    | Buy
    | Sell

let buy = Buy
let sell = Sell

let testOrderSide() =
    printfn "Buy: %A" buy
    printfn "Sell: %A" sell

testOrderSide()

这非常方便,使我们能够编写简洁优雅的代码,做到我们想要的而不需要任何样板代码。如果我们希望有一个函数能够切换订单的方向怎么办?在这种情况下,买方变成卖方,反之亦然。

type OrderSide =
    | Buy
    | Sell
let toggle1 = 
	match x with
	| Buy -> Sell
	| Sell -> Buy

let toggle2 = function
    | Buy -> Sell
    | Sell -> Buy

let buy = Buy
let sell = Sell

let testOrderSide() =
    printfn "Buy: %A" buy
    printfn "Sell: %A" sell
    printfn "Toggle Buy: %A" (toggle1 buy)
    printfn "Toggle Sell: %A" (toggle2 sell)

testOrderSide()

在这里,有两个版本的切换函数,toggle1toggle2。第一个版本使用 match-with 风格,而后者使用简写版本。简写版本有时很有用,因为它更简洁、更易读。

让我们通过引入递归字段来扩展对判别联合的分析。递归字段用于引用类型本身,并使你作为程序员能够定义更复杂的类型。这里是一个示例,我们定义一个可以是PutCall,或两者组合的选项。

type OptionT = 
    | Put of float
    | Call of float
    | Combine of OptionT * OptionT

递归判别联合的另一个示例是树形结构。树形结构用于表示层次化结构。

type Tree = 
    | Leaf of int
    | Node of tree * tree

let SimpleTree = 
    Node (
        Leaf 1, 
        Leaf 2
        )
// Iterate tree
let countLeaves tree =
    let rec loop sum = function
        | Leaf(_) -> sum + 1
        | Node(tree1, tree2) ->
            sum + (loop 0 tree1) + (loop 0 tree2)
    loop 0 tree

枚举

F#中的枚举与其他语言中的枚举几乎相同,用于将标签映射到常量值。枚举用于将标签与数字或预定义值关联。

在这里,我们定义一个类型RGB,将标签映射到值:

// Enumeration
type RGB = 
    | Red = 0
    | Green = 1
    | Blue = 2

我们使用枚举将一个值绑定到第一个颜色,红色:

let col1 : Color = Color.Red

总结枚举:

  • 它看起来像是判别联合类型,但它们允许值被指定为常量

  • 它们用于表示带有标签的常量

  • 它们只包含一个数据元素

  • 它们和判别联合一样安全,因为它们可以使用未映射的值创建

数组

数组是大小固定的可变集合,必须包含相同类型的值。大量相同类型的数组可以编译成高效的二进制表示。

在 F#中,数组是通过以下方式创建的:

let array1 = [| 1; 2; 3 |]

因为数组是可变的,所以可以像这样修改数组中的值:

array1.[0] <- 10

数组中的所有元素必须是相同类型的,否则编译器会报错。假设你创建了一个包含一个浮点数和两个整数的数组:

let array2 = [| 1.0; 2; 3 |] 

编译器会告诉你类型不一致。有时需要使用逻辑表达式初始化数据结构,这在此称为数组推导。对于数组,可以使用以下表达式来创建一个:

let array3 = [| for i in 1 .. 10 -> i * i |]

访问元素非常直接,并且每种数据结构的访问模式都相同。它看起来与其他编程语言非常相似,只是索引括号前有一个点:

array1.[0]

一个方便的特性是切片表示法,用于访问一系列元素:

array1.[0..2]
This can be shortened further, for example, if you select the elements from the beginning to the element with index 2:array1.[..2] 

同样的情况也适用于反向操作,选择从索引 2 到数组末尾的元素:

array1.[2..] 

让我们看两个数组初始化的例子。初始化一个全是零的数组非常有用。数组模块提供了一个函数来做到这一点。请看以下代码片段:

let arrayOfTenZeroes : int array = Array.zeroCreate 10

要创建一个完全空的数组,请参考以下代码片段:

let myEmptyArray = Array.empty

数组模块中有很多有用的函数,下面我们只会看一些。例如,连接两个数组可以通过以下方式完成:

printfn "%A" (Array.append [| 1; 2; 3|] [| 4; 5; 6|])

filter函数是一个常见的候选者,在很多场景中都非常有用。以下将展示它在数组上的应用:

printfn "%A" (Array.filter (fun elem -> elem % 2 = 0) [| 1 .. 10|])

提示

详情和示例请参考 MSDN 页面,了解如何在 F#中使用数组:msdn.microsoft.com/en-us/library/dd233214.aspx

数组模块中的有趣函数

在下面的表格中,列出了数组模块中最有用的函数。这个表格也可以作为简短的参考:

函数 描述
Array.length a 返回数组 a 的长度
Array.average a 计算数组 a 元素的平均值。它必须是 float 或 double 数据类型
Array.min a 查找数组 a 中元素的最小值
Array.max a 查找数组 a 中元素的最大值
Array.filter f a 过滤数组 a 中符合函数 f 谓词的元素
Array.find f a 返回列表 a 中第一个匹配函数 f 谓词的元素
Array.empty 返回一个空数组
Array.isEmpty a 指示数组 a 是否为空
Array.exists f a 检查数组 a 中是否存在与谓词 f 匹配的元素
Array.sort a 按升序排列数组 a 中的元素,若要使用谓词排序,请参见 sortBy
Array.zip a b 按升序排列数组 a 中的元素,若要使用谓词排序,请参见 sortBy
Array.map f a 对数组 a 中的每个元素调用函数 f,并生成一个新数组

列表

在这一节中,我们将更详细地探讨列表,并展示如何使用模块中的大多数函数。F# 中的列表实现为链表,并且是不可变的。它们适用于列表,并且也可以用于在列表与其他集合之间进行转换。

列表是有序的集合,包含相同类型的元素;你不能在同一个列表中混合不同的类型。首先,我们来看如何创建和初始化一个列表,然后我们逐个探讨模块函数。本节最后有一个表格,总结了所有的函数。

让我们使用 F# Interactive:

> let list1 = [1 .. 10];;

val list1 : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

这里我们使用一个范围来初始化一个列表。当你想要一个特定模式或由函数描述的序列时,列表表达式非常有用。

> let list2 = [ for i in 1 .. 10 -> i * i ];;

val list2 : int list = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]

现在我们将探讨列表中的两个有用运算符。我们将使用之前定义的两个列表:list1list2

> 10 :: [10];;
val it : int list = [10; 10]

> 10 :: list1;;
val it : int list = [10; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

之前使用的运算符 (::) 称为连接运算符或 cons 运算符。连接是一个高效的 O(1) 运算符,它将元素添加到列表的开头。它通过一个元素和一个列表构建一个新的列表。使用运算符也可以连接两个列表:

> [10] @ list1;;
val it : int list = [10; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

> list1 @ list2;;
val it : int list =
[1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 1; 4; 9; 16; 25; 36; 49; 64; 81; 100]

连接运算符 (@) 会有性能损耗,因为列表是不可变的,必须复制第一个列表,因此在性能要求较高的场合应避免使用。这个方法将找到列表的 n 项值:

> List.nth list1 3;;
val it : int = 4

要计算一个列表的平均值,可以使用 List.average 函数:

> let list3 = [10.0 .. 20.0];;

val list3 : float list =
  [10.0; 11.0; 12.0; 13.0; 14.0; 15.0; 16.0; 17.0; 18.0; 19.0; 20.0]

> List.average list3;;
val it : float = 15.0

我们必须在这里使用浮动数,否则编译器会报错:

List.average list1;;

List.average list1;;
-------------^^^^^

... error FS0001: The type 'int' does not support the operator 'DivideByInt'

列表的最小值和最大值分别由 List.minList.max 找到:

> List.min list1;;
val it : int = 1

> List.max list1;;
val it : int = 10

列表模块中有一个函数 List.append,等同于运算符 @,它将两个列表连接起来:

> List.append list1 list2;;
val it : int list =
  [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 1; 4; 9; 16; 25; 36; 49; 64; 81;100]

现在让我们研究三个函数:filterfindexists。它们接受一个谓词函数 f 和一个列表。谓词函数描述了一个条件:

> List.filter (fun elem -> elem > 10) list1;;
val it : int list = []

> List.filter (fun elem -> elem > 3) list1;;
val it : int list = [4; 5; 6; 7; 8; 9; 10]

> List.find (fun elem -> elem > 3) list1;;
val it : int = 4

> List.exists (fun elem -> elem > 3) list1;;
val it : bool = true

> List.exists (fun elem -> elem > 10) list1;;
val it : bool = false

zip 函数将使用提供作为参数的列表中的元素成对组合,创建一个新列表:

> List.zip list1 list2;;
val it : (int * int) list =
  [(1, 1); (2, 4); (3, 9); (4, 16); (5, 25); (6, 36); (7, 49); (8, 64); (9, 81); (10, 100)]

假设我们想要总结一个列表的元素。List.fold 函数非常有用:

> List.fold (+) 0 list1;;
val it : int = 55

结果是 55,符合预期。我们还可以使用 List.fold 计算几何和:

> List.fold (*) 1 list1;;
val it : int = 3628800

注意,我们使用 1 作为折叠的初始值,因为这是一个乘法操作。

模式匹配与列表

让我们看一下模式匹配与列表,这是一种常见的处理列表的方法。模式匹配为在列表上创建递归函数提供了强大的方式。如果我们想编写一个递归函数来计算列表的长度,可以使用模式匹配。

    // Get the length of a list
    let rec getLengthOfList l = function
        | [] -> printfn "Length of list: %d" l
        | head :: tail -> getLengthOfList (l+1) tail

    let myList = [1..10]
    getLengthOfList 0 myList

正如前面的简短示例所示,模式匹配使得代码易于阅读。第一个匹配是空列表,第二个使用cons运算符来匹配一个包含头部和尾部的列表。然后,尾部被用于递归函数all,在每次迭代中,长度l增加 1。我们再看一个例子。如果我们想获取列表中的倒数第二个元素,可以使用模式匹配。

    // Get the second last element of a list
    let rec getSecondLastElement = function
        | head :: tail :: [] -> head
        | head :: tail -> getSecondLastElement tail

    getSecondLastElement myList

在前面的示例中,我们可以看到如何使用模式匹配清晰地表达思想。由于列表是使用::运算符构造的,我们可以使用它来匹配任意模式。第一个模式将匹配head :: tail :: [],其中head是列表中的倒数第二个元素。

列表模块中的有趣函数

在下表中,列出了列表模块中最常用的函数。这个表也可以作为一个简短的参考:

函数 描述
List.nth a 返回列表a中的第 n 个元素
List.average a 计算列表a的平均值;元素必须是浮动或双精度数据类型
List.max a 找出列表a中元素的最大值
List.min a 找出列表a中元素的最小值
List.append a b 将两个列表ab连接起来
List.filter f a 从列表a中过滤出符合函数f谓词的元素
List.empty 返回一个空列表
List.length a 返回列表a的长度
List.find f a 返回列表a中第一个符合函数f谓词的元素
List.sort a 按升序排序列表a中的元素,见sortBy用于使用谓词
List.zip a b 按元素合并列表ab,并形成一个新列表
List.exists f a 检查列表a中是否有符合谓词f的元素
List.fold f s a 从左到右折叠列表a,使用函数f和初始值s
List.head a 返回列表a的头部
List.tail a 返回列表a的尾部
List.map f a 对列表a中的每个元素调用函数f,并形成一个新列表

序列

序列是由元素组成的逻辑系列,元素必须是相同类型的。它们特别适合表示大型有序数据集合,其中并非所有元素都需要使用。序列是惰性求值的,适用于大数据集合,因为并非所有元素都需要保存在内存中。序列表达式表示按需计算的数据序列。我们将像前几节一样探讨序列,使用 F# Interactive 来更好地理解它们,并且我们还将看到它们的模块函数是如何工作的。

让我们首先看看如何使用 F# Interactive 以多种方式初始化和创建序列:

> seq {1 .. 2}
val it : seq<int> = [1; 2]

> seq {1 .. 10}
val it : seq<int> = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

> seq {1 .. 10 .. 100}
val it : seq<int> = [1; 11; 21; 31; 41; 51; 61; 71; 81; 91]
> seq {for i in 1 .. 10 do yield i * i}
val it : seq<int> = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]

> seq {for i in 1 .. 10 -> i * i}
val it : seq<int> = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]

首先,我们显式定义元素来创建一个序列。然后,我们使用范围表达式。它们的工作方式与数组和列表相同。范围表达式也与其他集合类似。

这将找到序列的第n个值:

> Seq.nth 3 { 1 .. 10};;
val it : int = 4

请注意,在Seq.nth中,参数的顺序与列表模块中相同函数的顺序有所不同:

> Seq.average {0.0 .. 100.0};;
val it : float = 50.0

序列的最小值和最大值分别通过Seq.minSeq.max找到:

> Seq.min seq1;;
val it : int = 1

> Seq.max seq1;;
val it : int = 10

你还可以使用Seq.append函数连接两个序列:

> Seq.append seq1 seq1;;
val it : seq<int> = seq [1; 2; 3; 4; …]

这将创建一个空序列:

> Seq.empty;;
val it : seq<'a> = seq []

现在让我们研究三个函数:filterfindexists。它们接受一个谓词函数f和一个列表。谓词函数描述一个条件。它们与列表函数是相同的:

> Seq.filter (fun elem -> elem > 3) seq1;;
val it : seq<int> = seq [4; 5; 6; 7; ...]

> Seq.filter (fun elem -> elem > 3) seq1;;
val it : seq<int> = seq [4; 5; 6; 7; ...]

> Seq.find (fun elem -> elem > 3) seq1;;
val it : int = 4

> Seq.exists (fun elem -> elem > 3) seq1;;
val it : bool = true

> Seq.exists (fun elem -> elem > 10) seq1;;
val it : bool = false

序列的第一个元素(头部)通过调用Seq.head获得:

> Seq.head seq1;;
val it : int = 1

请注意,这里没有Seq.tail,这是因为序列的表示方式(它们是惰性构造的)。

序列模块中的有趣函数

在下表中,展示了序列模块中最有用的函数。这个表格也可以作为简短参考:

函数 描述
Seq.nth a 返回序列 a 的第 n 个元素
Seq.average a 计算序列 a 的平均值。元素必须是浮动或双精度数据类型
Seq.min a 查找序列 a 中元素的最大值
Seq.max a 查找序列 a 中元素的最小值
Seq.append a b 连接序列 a 和 b
Seq.filter f a 筛选出序列 a 中符合谓词 f 的元素
Seq.empty 返回一个空序列
Seq.find f a 返回序列 a 中第一个满足谓词 f 的元素
Seq.sort a 按升序排序序列 a 中的元素。若要使用谓词,请参见 sortBy
Seq.zip a b 按元素将列表 a 和 b 组合,形成一个新列表
Seq.length a 返回序列 a 的长度
Seq.exists f a 检查序列 a 中是否存在满足谓词 f 的元素
Seq.fold f s a 使用函数 f 和起始值 s 从左到右折叠序列 a
Seq.head 返回序列 a 的头部
Seq.map f a 对序列 a 中的每个元素调用函数 f,并形成一个新序列

集合

集合是无序的数据元素容器。集合不保留元素插入顺序,也不允许重复元素。

让我们创建一个包含三个整数元素的集合:

> let s1 = set [1; 2; 7];;

上述集合将具有以下类型:

val s1 : Set<int> = set [1; 2; 7]

类型推导在这里按预期工作。现在让我们考虑添加和检查s1中的元素:

> s1.Add(9);;
val it : Set<int> = set [1; 2; 7; 9]

注意,s1由于其不可变特性不会被修改。我们可以检查s1是否包含 9:

> Set.contains 1 s1;;
val it : bool = true

有时,从其他数据结构创建序列是有帮助的。在这种情况下,从一个序列创建:

> let s2 = Set.ofSeq [1..10];;
val s2 : Set<int> = set [1; 2; 3; 4; 5; 6; 7; 8; 9; ...]

它的工作方式与从数组中创建相同:

> let s3 = Set.ofArray([| for i in 1 .. 5 -> i * i |]);;
val s3 : Set<int> = set [1; 4; 9; 16; 25]

要获取集合的长度,或者换句话说,计算集合中元素的数量,可以运行以下代码:

> Set.count s1;;
val it : int = 3

fold 函数也出现在集合模块中,并且与其他集合一样使用:

> Set.fold (fun a b -> a + b) 0 s1;;
val it : int = 10

> Set.fold (fun a b -> a * b) 1 s1;;
val it : int = 14

这也可以使用加法的简写版本 (+) 和乘法的简写版本 (*) 来写:

> Set.fold (+) 0 s1;;
val it : int = 10

> Set.fold (*) 1 s1;;
val it : int = 14

对于 Set.exists 也是如此。该函数接受一个谓词和一个集合,如果有任何元素与该函数匹配,则返回 true

> Set.exists (fun elem -> elem = 2) s1;;
val it : bool = true

> Set.exists ((=) 4) s1;;val it : bool = false

Filterexists 的变体,它返回一个新的集合,包含匹配该函数的元素:

> Set.filter (fun elem -> elem > 1) s1;;
val it : Set<int> = set [2; 7]

> Set.filter (fun elem -> elem < 2) s1;;
val it : Set<int> = set [1]

一个有趣的函数是 partition 函数。它会将集合拆分,在本例中拆分为两个新集合:一个包含通过谓词的元素,另一个则不包含:

> Set.partition (fun elem -> elem < 2) s1;;
val it : Set<int> * Set<int> = (set [1], set [2; 7])

想一想,如果你在一个普通的命令式语言中写这个 partition 函数会怎么样。如果你问我,写法没有那么优雅。最后但同样重要的是,我们讲解 map 函数。这个函数此时应该对你来说很熟悉。我们使用 s1 的旧值,并简单地给每个元素加上 2:

> Set.map (fun elem -> elem + 2) s1;;
val it : Set<int> = set [3; 4; 9]

Set 模块中一些有趣的函数如下:

函数 描述
Set.count a 返回集合 a 中的元素数量
Set.empty 返回一个空集合
Set.fold f s a 从左到右折叠集合 a,使用函数 f 和初始值 s
Set.exists f a 检查集合 a 中是否存在符合谓词 f 的元素
Set.filter f a 过滤序列 a 中与谓词函数 f 匹配的元素
Set.partition f a 使用谓词函数 f 从集合 a 创建两个新的子集
Set.map 对集合 a 中的每个元素调用函数 f,并形成一个新的集合

映射

映射是一种特殊的集合,它包含关联的键/值对。它们是不可变的、无序的数据结构。它们不会保留插入元素的顺序,也不允许重复元素。

映射的创建方式与集合非常相似,唯一不同的是我们还需要一个键:

> let m1 = Map.empty.Add("Age", 27);;
val m1 : Map<string,int> = map [("Age", 27)]

现在,我们可以使用键 Age 来访问值:

> m1.["Age"];;
val it : int = 27

可以通过值的列表创建映射:

> let m2 = ["Year", 2009; "Month", 21; "Day", 3] |> Map.ofList;;
val m2 : Map<string,int> = map [("Day", 3); ("Month", 21); ("Year", 2009)]

由于映射与我们之前讨论的集合有所不同,我们将进一步了解 map 模块中一些更有趣的函数。

要使用谓词过滤一个映射,你可能会注意到与其他集合相比,这里有一些小的变化。要忽略键或值,可以像模式匹配一样用下划线(_)替代它:

> Map.filter (fun _ v -> v = 27) m1;;
val it : Map<string,int> = map [("Age", 27)]

Maps.exists 的工作方式几乎与过滤器相同:

> Map.exists (fun _ v -> v = 27) m1;;
val it : bool = true

使用谓词对映射进行分区是很有用的。这里我们通过一个固定值来实现:

> Map.partition (fun _ v -> v = 27) m1;;
val it : Map<string,int> * Map<string,int> = (map [("Age", 27)], map [])

另一个有用的映射模块函数是 Map.containsKey。这个函数检查映射中是否包含特定的键:

> Map.containsKey "Age" m1;;
val it : bool = true

> Map.containsKey "Ages" m1;;
val it : bool = false

映射模块中的有趣函数

在下表中,列出了 map 模块中最有用的函数。此表也可以作为一个简短的参考:

函数 描述
Map.add(k,v) 创建一个新映射,包含原映射的内容及新条目k, v
Map.empty 创建一个空映射
Map.filter f a 从映射a中过滤出符合函数f谓词的元素
Map.exists f a 检查集合a中是否有元素符合谓词f
Map.partition f a 使用谓词函数f将映射a划分为两个新映射
Map.containsKey k a 检查映射a中是否包含键k
Map.fold f s a 从左到右折叠映射a,使用函数f和起始值s
Map.find f a 返回映射a中第一个符合函数f谓词的元素

选项

选项是一种优雅的方式,用于封装一个可能存在也可能不存在的值。它们通过判别联合来实现。与其检查null值,不如使用选项。

这是一个例子,演示了我们如何使用整数选项和模式匹配进一步探讨它们:

let evalOption (o : int option) = 
    match o with
    | Some(a) -> printfn "Found value: %d" a
    | None -> printfn "None"
let some : int option = Some(1)
let none : int option = None

我们可以使用 F# Interactive 来研究选项的类型:

> evalOption some;;
Found value: 1
val it : unit = ()

> evalOption none;;
None
val it : unit = ()

第一个选项some包含一个整数值,符合预期。另一个选项none为空。这样,我们就不需要使用null值并通过条件检查它们。我们只需要传递选项值即可。F#中还提供了Nullable,明确表示值的缺失。

字符串

字符串应该已经对你很熟悉,并且它们在 F#中的工作方式与其他语言相同。更正式地说,字符串是字符的序列,并且与.NET 中的字符串兼容:

let str1 = "This is a string"
let str2 = @"This is a string with \ \ //"
let str3 = """ this is "another" string"""

printfn "%s" (str1.[0..2])

let str4 = "Hello, " + "world"
let str5 = str1 + str3

String.length str4
String.Compare (str1, "This is a string")
String.Compare (str1, "This is another string")

String.map (fun s -> Char.ToUpper s) str1

字符串模块中的有趣函数

在下表中,展示了字符串模块中最有用的函数。这张表也可以作为简短的参考。

函数 描述
String.length s 返回字符串s的长度
String.Empty 创建一个空字符串
String.map f s 将函数f应用到字符串s中的每个字符上
String.IsNullOrEmpty 判断字符串s是否为空或为null
String.IsNullOrWhiteSpace 判断字符串s是否为空或仅由空白字符组成
String.Copy s 创建一个新的字符串,其字符序列与字符串s相同
String.Concat s1 s2 将两个字符串s1s2连接成一个新的字符串
String.exists f s 检查字符串s中是否有字符符合谓词函数f
String.Compare s1 s1 比较两个字符串s1s2。如果它们相同,则返回0,否则根据比较结果返回-11

选择数据结构

由于可选的数据结构很多,在特定问题中选择哪个可能很困难。这里有一些规则可供遵循,以下是各个数据结构的主要特征的简要总结。

数组

如果你需要预先知道集合的大小,数组是高效的。这意味着数组的大小是固定的,如果你想更改大小,必须创建一个新数组并将元素复制过去。另一方面,随机访问非常快速;它可以在常数时间内完成。

列表

列表通过链表实现,链表是通过指针将项连接在一起的。这意味着遍历链表并不是特别高效,因为必须跟踪很多指针。另一方面,在链表的任何位置插入元素都非常快速。还值得一提的是,访问头部元素是一个常数时间操作。

集合

集合是通过二叉树实现的,其中同一个集合内不能定义多个相同的值。当你不关心顺序并且不允许重复时,集合非常有用。

映射

映射类似于集合,只不过它们扩展为使用键值对而不是原始值。当你知道键时,映射在查找值时非常高效。

更多函数式编程内容

在这里,我们将继续并在前一章的函数式编程基础上进行扩展。我们将考察一些 F# 语言中的更高级且同时非常实用的构造。

递归函数

递归是函数式编程中的一个基本构建块。许多问题可以通过递归的方式解决,再加上模式匹配,它构成了一个强大的工具包。

要定义一个递归表达式,使用关键字rec

让我们从著名的斐波那契数列开始,斐波那契数列的定义是递归序列中前两个数的和。前两个值分别设置为01,作为种子值:

let rec fib n =
   if n <= 2 then 1
   else fib (n - 1) + fib (n – 2)

递归是一种强大的解决问题的方式,通常在函数式语言中优先于循环结构。让我们来看三个递归函数,展示它的灵活性和强大功能:

let rec sum list =
   match list with
   | head :: tail -> head + sum tail
   | [] -> 0

这个函数将递归地对一个元素列表求和,使用列表参数上的匹配构造。列表在第一个匹配语句中被拆分为头部和尾部,然后函数再次使用尾部部分进行调用。如果列表为空,将返回零。最终,列表的和将被返回:

let rec len list =
   match list with
   | head :: tail -> 1 + len tail
   | [] -> 0

为了确定列表的长度,我们稍微修改了mysum函数,在遇到每个元素时,添加一个 1,而不是该元素的值。当然,我们之前已经看到过有一个内置函数可以完成此操作。内置函数map可以使用类似于这样的递归来构建:

let rec mymap f = function
    | [] -> []
    | x::xs -> f x::mymap f xs

理解这个函数将帮助你更好地理解内置函数以及函数式编程的一般概念。许多函数可以通过递归和模式匹配来定义。我们将在后面的部分学习更多关于模式匹配的内容。

尾递归

尾递归是一种优化递归并减轻回调堆栈负担的方法。每次调用函数时,都会在堆栈上分配一个新的堆栈帧。这最终会导致StackOverflowException。换句话说,当你预计会有数千次迭代时,就会使用尾递归。

尾递归可以描述为:

  • 一种优化技术

  • 一种减轻堆栈负担并确保没有堆栈溢出的方法

  • 有时更难理解和推理

为了说明尾递归的概念,我们将转换一个传统的递归定义的阶乘函数。阶乘,n!,是所有小于或等于 n 的正数的积。例如,4!定义为 4 * 3 * 2 * 1,即 24:

let rec factorial1 n =
    match n with
    | 0 | 1 -> 1
    | _ -> n * factorial1(n - 1)

let factorial2 n =
    let rec tailrecfact n acc =
        match n with
        | 0 -> acc
        | _ -> trecfact (n - 1) (acc * n)
    tailrecfact n 1

我们现在可以验证该函数是否为 4!返回正确的值,如下所示:

> factorial1 4;;
val it : int = 24

> factorial2 4;;
val it : int = 24

让我们尝试一个稍微大的阶乘。参数不必很大,但可能会导致一个较大的结果:

> factorial1 10;;
val it : int = 3628800

> factorial2 10;;
val it : int = 3628800

模式匹配

模式匹配用于控制流。它允许程序员查看一个值,将其与一系列条件进行测试,并根据条件是否满足执行某些计算。它匹配不同的模式:

let sampleMatcher value =
    match value with
    | 0 -> "Zero"
    | 1 -> "One"
    | _ -> "Greather than one"

sampleMatcher 0

前面的代码片段定义了一个函数,该函数将提供给函数的参数与不同的模式进行匹配。这说明了模式匹配的基本思想。我们可以进一步修改它。

不完全的模式匹配

让我们看一个不完全的模式匹配示例:

let sampleMatcher value =
    match value with
    | 0 -> "Zero"
    | 1 -> "One"    

前面的代码片段是一个不完全的模式匹配,因为我们没有考虑零或一以外的值。编译器会告诉你这一点:

warning FS0025: Incomplete pattern matches on this expression. For example, the value '2' may indicate a case not covered by the pattern(s).

如果我们考虑一个简单的字符串模式匹配器,这一点也成立:

let nameMatcher name =
    match name with
    | "John" -> "The name is John"
    | "Bob" -> "Hi Bob!"

这可以通过通配符操作符(_)来修复,像前面片段中解释的第一个例子一样:

let nameMatcher name =
    match name with
    | "John" -> "The name is John"
    | "Bob" -> "Hi Bob!"
    | _ -> "I don't know you!"

nameMatcher "John"
nameMatcher "Linda"

使用守卫

在命令式编程中,我们使用带有表达式的if语句来表达条件。这是通过模式匹配和守卫来实现的。守卫使用关键字when来指定条件。让我们看一个例子,看看在哪里可以用到它:

let sampleMatcher value =
    match value with
    | 0 -> "Zero"
    | 1 -> "One"    
    | x when x > 1 -> "Greather than one"
    | _ -> "Some strange value"
sampleMatcher 0
sampleMatcher 1
sampleMatcher 2
sampleMatcher -1

guard这个名字告诉我们它们的属性。它们基于条件来保护模式。

模式匹配在赋值和输入参数中的应用

模式匹配在与元组一起的赋值中也很有用,像这样:

> let (bid, ask) = (100.0, 110.0);;
val bid : float = 100.0
val ask : float = 110.0

正如你在前面的例子中看到的,模式匹配机制会将值分配给每个名称。这对于多个赋值很有用:

> let (x, y, z) = (3.0, 2.0, 4.0);;
val z : float = 4.0
val y : float = 2.0
val x : float = 3.0

也可以使用通配符来忽略赋值中的一个值:

> let (x, y, _) = (3.0, 2.0, 4.0);;
val y : float = 2.0
val x : float = 3.0

活跃模式

活跃模式允许程序员将临时值和对象封装在类似联合体的结构中,以便在模式匹配中使用。首先,你通过作用于数据的各种表达式定义输入数据的分区。每个分区可以有自己的自定义逻辑。

假设我们想验证一个订单是否有效。我们可以使用将在后续章节介绍的Order类。订单可以是有效的,可以是市场订单或限价订单,或者仅仅是无效的。如果你对它的实现或属性感兴趣,可以在本章进一步查看order类。

我们首先引入一个非常简单的主动模式,即判断一个数字是正数还是负数:

let (|Negative|Positive|) number =
    if number >= 0.0 then
        Positive
    else
        Negative

let TestNumber (number:float) =
    match number with
    | Positive -> printfn "%f is positive" number
    | Negative -> printfn "%f is negative" number

我们可以使用 F# Interactive 来探索TestNumber函数,尝试不同的浮点数:

> TestNumber 0.0;;
0.000000 is positive
val it : unit = ()

> TestNumber 16.0;;
16.000000 is positive
val it : unit = ()

> TestNumber -7.0;;
-7.000000 is negative
val it : unit = ()

接下来,我们来看一下验证订单的主动模式:

let (|Limit|Market|Invalid|) (order:Order) = 
    if order.Type = OrderType.Limit && order.Price > 0.0 then 
        Limit 
    else if order.Type = OrderType.Market && order.Price = 0.0 then
        Market
    else 
        Invalid

let TestOrder (order:Order) =
   match order with
   | Market -> printfn "Market order"
   | Limit -> printfn "Limit order"
   | Invalid -> printfn "Invalid order"

让我们用不同的订单值来调用TestOrder

> TestOrder (Order(Buy, Limit, 5.0));;
Limit order
val it : unit = ()

> TestOrder (Order(Sell, Market, 0.0));;
Market order
val it : unit = ()

> TestOrder (Order(Sell, Limit, 0.0));;
Invalid order
val it : unit = ()

> TestOrder (Order(Buy, Market, 2.0));;
Invalid order
val it : unit = ()

> TestOrder (Order(Sell, Invalid, 2.0));;
Invalid order
val it : unit = ()

代码已经可以正常工作,你现在了解了更多关于主动模式及其如何简化生活的内容。

当输入的部分匹配时,会使用部分主动模式,这在需要将字符串解析为数字的情况下非常有用。让我们来看一个例子,单个部分主动模式的使用将使其更加清晰:

let (|Integer|_|) str =
   match System.Int32.TryParse(str) with
   | (true,num) -> Some(num)
   | _ -> None

let (|Double|_|) str =
   match System.Double.TryParse(str) with
   | (true,num) -> Some(num)
   | _ -> None

let testParse numberStr = 
    match numberStr with
    | Integer num -> printfn "Parsed an integer '%A'" num
    | Double num -> printfn "Parsed a double '%A'" num
    | _ -> printfn "Couldn't parse string: %A" numberStr

> testParse "10.0"
Parsed a double '10.0'
val it : unit = ()
> testParse "11"
Parsed an integer '11'
val it : unit = ()

> testParse "abc"
Couldn't parse string: "abc"
val it : unit = ()

testParse函数中的匹配操作里,部分主动模式会自动使用。

引入泛型

在本节中,我们将简要地介绍泛型以及如何定义泛型函数。F# 编译器能够判断一个函数是否可以是泛型的。这个函数与类型推断的结合是一个非常强大的组合,可以实现简洁且易于阅读的代码。不过,有时你可能想要明确指定自己的函数为泛型。如果它们是泛型的,它们将能够处理不同的类型。这样做可以减少为每个相关类型编写多个具有相同逻辑的函数的需要。

为了说明前面描述的概念,我们可以看一个基于三个参数创建列表的函数。这个函数是泛型的,因此可以在使用时指定类型:

let genericListMaker<'T>(x, y, z) = 
let list = new List<'T>()
   list.Add(x)
   list.Add(y)
   list.Add(z)
   list

让我们在 F# Interactive 中使用它:

> genericListMaker<int>(1, 2, 3);;val it : List<int> = seq [1; 2; 3]
> genericListMaker<float>(1.0, 2.0, 3.0);;val it : List<float> = seq [1.0; 2.0; 3.0]
> genericListMaker<string>("1", "2", "3");;val it : List<string> = seq ["1"; "2"; "3"]

首先,我们使用整数来创建一个列表,函数会为我们创建一个列表。其次,函数用于浮点数。最后但同样重要的是,我们使用普通的字符串。在所有情况下,无论类型如何,函数都以相同的方式工作。

惰性求值

惰性求值或惰性计算顾名思义是懒惰的。这意味着它们在最后时刻才会被处理,也就是在需要值的时候。这种求值方式有助于提升代码的性能。例如,序列使用了惰性求值。惰性求值还允许定义昂贵的计算,而这些计算在实际需要之前不会被求值。惰性表达式是泛型的,类型会在表达式求值时确定。

让我们来看看如何定义你自己的惰性构造:

let lazyListFolding =
  lazy 
  ( 
    let someList = [for i in 1 .. 10 -> i * 2]
    List.fold (+) 0 someList
  )

现在我们可以使用 F# Interactive 来评估这个函数:

> let forcedMultiply1 = lazyListFolding.Force();;

val forcedMultiply1 : int = 110

当你执行函数定义时,编译器会告诉你值尚未创建;它是惰性求值的,具体表现如下:

val lazyMultiply : Lazy<int> = Value is not created.

测量单位

测量单位是一种将有符号整数和浮动点数与单位关联的方法。单位可以描述重量、长度、体积和货币。测量单位的一个有用应用是货币和货币转换。测量单位用于验证涉及的类型,确保它们被正确使用。编译器在验证后会移除所有关于单位的信息,这些信息不会成为生成的可执行程序的一部分。

首先我们来看 F#中实现货币转换的常见方式。不能保证实际的汇率是正确的,或者计算使用了正确的单位:

/// Conversion rate representing 1 EUR in USD
let rateEurUsd = 1.28M
/// Converts amount in EUR to USD
let euroToUsds eur = eur * rateEurUsd

/// Convert 10000 EUR to USD
let usd = euroToUsds 10000.0M

在前面的代码片段中,没有一种可靠的方法来验证单位转换的正确性。每个值都只是被当作浮动点数来处理。验证所涉及的单位是否正确可能非常重要。1999 年 9 月底,火星气候探测器因为单位问题丧失。地面软件生成了磅秒(pound-seconds),而不是牛顿秒(newton-seconds),这导致航天器在火星大气层中蒸发。

同样地,对于货币来说,能够验证所使用单位的正确性非常重要。让我们将前面的代码修改为使用measure构造体的单位:

[<Measure>]
type USD

[<Measure>]
type EUR

let rateEurUsd = 1.28M<EUR/USD>

// Converts amount in EUR to USD
let euroToUsds (eur:decimal<EUR>) = eur * rateEurUsd

// Convert 10000 EUR to USD
let usd = euroToUsds 10000.0M<EUR>

在这里,编译器将使用提供的类型信息验证单位是否正确。确实非常方便!如果我们使用了错误的单位怎么办?我们修改代码,加入一个日元(YEN)的测量单位:

[<Measure>]
type USD

[<Measure>]
type EUR

[<Measure>]
type YEN

let rateEurUsd = 1.28M<EUR/USD>
// Converts amount in EUR to USD
let euroToUsds (eur:decimal<EUR>) = eur * rateEurUsd

// Convert 10000 EUR to USD
let usd = euroToUsds 10000.0M<YEN>

在 F# Interactive 中运行此代码将导致一个错误,错误信息如下:

error FS0001: Type mismatch. Expecting a
    decimal<EUR>    
but given a
    decimal<YEN>    
The unit of measure 'EUR' does not match the unit of measure 'YEN'

消息非常清晰地指出了哪里出错,这对于编写涉及不同单位的正确代码并处理和转换这些单位非常有帮助。

异步和并行编程

本节将介绍异步和并行编程,以及事件、线程池、后台工作线程和邮箱处理器(代理)。所有这些构造体的存在都有一个共同的目的:让程序员的生活更轻松。现代计算机的 CPU 能够并行执行多个线程,这为新的可能性打开了大门。这些可能性需要一个优秀的并发和并行编程工具包。F#是一个非常好的候选语言,它的设计原则之一就是能够很好地适应这些类型的场景。

事件

事件在你希望某个特定事件发生时执行某个函数时非常有用,这个事件可能会在未来某个时刻发生。这通常适用于 GUI 编程,在这种情况下,用户将以某种方式与界面进行交互。这个模式被称为事件驱动编程,其中事件推动程序的执行。下面的例子简单地说明了这一点:

open System.Windows.Forms

let form = new Form(Text="F# Events",
                    Visible = true,
                    TopMost = true)

form.Click.Add(fun evArgs -> System.Console.WriteLine("Click event handler"))
Application.Run(form)

这里创建了一个表单,一个常规的.NET 表单。构造函数的参数设置了标题、可见性为true,并指定其始终显示在顶部。然后,安装了一个click事件处理器,使用一个 lambda 函数作为事件处理程序。执行时,该函数将简单地向控制台输出一条文本消息。

在 F#中,你可以操作事件流。如果需要过滤某些事件或进行某些操作,这非常有用。事件是 F#中的一等公民,这使得它们可以像其他变量一样传递。我们来看看如何过滤事件。在下面的示例中,click事件根据其坐标进行过滤:

open System.Windows.Forms

let form = new Form(Text="F# Events",
                    Visible = true,
                    TopMost = true)

form.MouseDown 
|> Event.filter (fun args -> args.X < 50)
|> Event.map (fun args -> printfn "%d %d" args.X args.Y)

在这里,我们修改了代码以监听MouseDown事件,该事件在表单本身中被点击。然后,MouseDown事件根据其坐标进行过滤,坐标是事件参数的一部分。如果事件通过过滤器,则调用一个函数,将坐标打印到控制台。这个过滤过程非常有用,可以创建复杂的事件过滤器和逻辑,代码简洁易懂。

后台工作者

假设你编写了一个程序,其中需要进行计算。有时这些计算会运行很长时间。后台工作者是一个解决方案,当你希望执行长时间运行的任务并且这些任务在后台运行时。代码将在一个单独的线程中执行:

open System.Threading
open System.ComponentModel

let worker = new BackgroundWorker()
worker.DoWork.Add(fun args ->    
    for i in 1 .. 50 do
        // Simulates heavy calculation
        Thread.Sleep(1000)
        printfn "%A" i
)
worker.RunWorkerCompleted.Add(fun args ->
    printfn "Completed..."
)

worker.RunWorkerAsync()

这是一个说明性示例,展示了如何以最简单的方式使用后台工作者。工作者将执行一个模拟计算过程的函数,并在完成时通知我们。你可以通过使用Add函数安排任务按顺序执行。这里有两个任务按顺序执行,最后我们会在完成时收到通知:

open System.Threading
open System.ComponentModel

let worker = new BackgroundWorker()
worker.DoWork.Add(fun args ->    
    for i in 1 .. 50 do
        // Simulates heavy calculation
        Thread.Sleep(1000)
        printfn "A: %A" i
)

worker.DoWork.Add(fun args ->    
    for i in 1 .. 10 do
        // Simulates heavy calculation
        Thread.Sleep(500)
        printfn "B: %A" i
)

worker.RunWorkerCompleted.Add(fun args ->
    printfn "Completed..."
)

worker.RunWorkerAsync()

有时,我们希望能够取消Backgroundworker中的执行。为了实现这一点,我们需要对之前的代码做一些小的修改。首先,在构造函数中设置一个标志,WorkerSupportsCancellation = true,,然后在每次迭代计算时检查该标志:

open System.ComponentModel

let workerCancel = new BackgroundWorker(WorkerSupportsCancellation = true)
workerCancel.DoWork.Add(fun args ->
    printfn "apan %A" args
    for i in 1 .. 50 do
        if (workerCancel.CancellationPending = false) then
            Thread.Sleep(1000)
            printfn "%A" i
)

workerCancel.RunWorkerCompleted.Add(fun args ->
    printfn "Completed..."
)

workerCancel.RunWorkerAsync()

如果运行此代码,你将看不到任何取消操作。只是代码已准备好处理取消。要取消先前的执行,你需要运行以下代码:

workerCancel.CancelAsync()

使用 F#交互式运行主代码进行几次迭代,然后运行CancelAsync()函数。这将终止后台工作者。

线程

线程是任何现代软件的核心部分。在 F#中,线程本质上是.NET 线程,具备所有.NET 功能。如果你有其他.NET 语言的相关知识,下面的代码对你来说应该非常熟悉:

open System.Threading

let runMe() = 
    for i in 1 .. 10 do
        try
            Thread.Sleep(1000)
        with
            | :? System.Threading.ThreadAbortException as ex -> printfn "Exception %A" ex
        printfn "I'm still running..."

let thread = new Thread(runMe)
thread.Start()

我们通过将一个委托传递给Thread构造函数来创建一个线程。该线程将运行runMe函数。新的部分可能是异常处理的方式。它们是通过模式匹配来处理的。

可以创建很多线程,它们将并发执行。

open System.Threading

let runMe() = 
    for i in 1 .. 10 do
        try
            Thread.Sleep(1000)
        with
            | :? System.Threading.ThreadAbortException as ex -> printfn "Exception %A" ex
        printfn "I'm still running..."

let createThread() =
    let thread = new Thread(runMe)
    thread.Start()

createThread()
createThread()

在这里,创建了两个线程并且它们并发执行。有时输出会被干扰;这是因为它们没有同步机制。创建线程是相当昂贵的,如果创建和终止的线程很多,这个成本会变得明显。每个线程都占用一定的内存,如果线程的生命周期较短,最好使用线程池来提升性能。

线程池

如前面所述,创建线程是相当昂贵的。这是因为它通常涉及操作系统本身来处理任务。如果线程的生命周期较短,线程池就能派上用场。线程池会根据负载来创建和终止线程。当线程完成任务时,它们会排队等待下一个任务。F#使用.NET 的ThreadPool类。如果你曾使用过 C#或其他.NET 语言中的同类类,这个示例也应该很熟悉:

open System.Threading

let runMe(arg:obj) = 
    for i in 1 .. 10 do
        try
            Thread.Sleep(1000)
        with
            | :? System.Threading.ThreadAbortException as ex -> printfn "Exception %A" ex
        printfn "%A still running..." arg

ThreadPool.QueueUserWorkItem(new WaitCallback(runMe), "One")
ThreadPool.QueueUserWorkItem(new WaitCallback(runMe), "Two")
ThreadPool.QueueUserWorkItem(new WaitCallback(runMe), "Three")

在前面的代码中,我们将三个任务排入队列,由线程池执行。它们将被执行而无需再次排队。这是因为线程池最多会启动ThreadPool.GetMaxThreads()个线程,通常为 1024 个线程。超过这个数量时,它们将被排队。

异步编程

异步代码执行的请求不会立即完成。这意味着它们正在执行一些操作,这些操作将在未来某个时间完成,而不会阻塞当前线程。与其等待结果的可用性,不如发起多个请求,一旦结果可用就处理它。这样的编程方式被称为异步编程。程序不会因为结果尚未可用而被阻塞。相反,如前所述,程序会在结果准备好时收到通知。一个常见的应用场景是 IO 操作,在这种情况下,CPU 时间可以用于做更有意义的事情,而不是等待 IO 操作完成。异步编程中通常会涉及很多回调。历史上,异步编程在.NET 中是通过异步编程模型APM)来实现的。

提示

MSDN 有关于 APM 的详细页面,msdn.microsoft.com/en-us/library/ms228963.aspx.

在不详细讨论 APM 和异步回调的情况下,我们将简单介绍 F#中的异步工作流。这使我们能够编写异步代码,而无需显式的回调。

F# 异步工作流

要使用异步工作流,你只需将你想要异步执行的代码包裹在async块中。就是这么简单,但还没有涵盖全部真相。还有一件事,async块中的代码本身必须是异步的,才能利用异步工作流:

async { expression }

这里,表达式被包装在 async 块中。该表达式通过 Async.Start 设置为异步运行,这意味着它不会阻塞当前线程。如果当前线程是 GUI 线程,这尤其是非常重要的。

异步绑定

当你处理异步代码和表达式,并且需要将它们绑定到值时,必须使用 let 关键字的修改版 let!

这使得执行在绑定后可以继续进行,而不会阻塞当前线程。这是一种告知绑定值是异步的,并将在结果可用时稍后使用的方式。

请考虑以下代码:

myFunction1()
let! response = req.AsyncGetResponse()
myFunction2()

如果我们在这里不使用 let!(let bang)操作符,myFunction2 就必须等待异步请求的结果。我们其实并不需要立即得到结果,而是可以通过运行 myFunction2 来更好地利用 CPU,而不是做无效的等待。

使用异步工作流的示例

这个例子演示了使用并行构造的异步编程的一些概念。在这里,我们将使用 WebClient 类中的异步函数 AsyncDownloadString 从 Yahoo! 财经下载数据。这个函数接受一个 URL 并下载内容。在这个例子中,内容将是 CSV 格式,包含从 2010-01-01 到 2013-06-06 的每日 OHLC 价格。我们首先通过并行下载数据并统计每个股票符号获取的字节数:

open System.Net
open Microsoft.FSharp.Control.WebExtensions
/// Stock symbol and URL to Yahoo finance
let urlList = [ "MSFT", "http://ichart.finance.yahoo.com/table.csv?s=MSFT&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv" 
                "GOOG", "http://ichart.finance.yahoo.com/table.csv?s=GOOG&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv" 
                "EBAY", "http://ichart.finance.yahoo.com/table.csv?s=EBAY&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv"
                "AAPL", "http://ichart.finance.yahoo.com/table.csv?s=AAPL&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv"
                "ADBE", "http://ichart.finance.yahoo.com/table.csv?s=ADBE&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv"
              ]

/// Async fetch of CSV data
let fetchAsync(name, url:string) =
    async { 
        try 
            let uri = new System.Uri(url)
            let webClient = new WebClient()
            let! html = webClient.AsyncDownloadString(uri)
            printfn "Downloaded historical data for %s, received %d characters" name html.Length
        with
            | ex -> printfn "Exception: %s" ex.Message
    }

/// Helper function to run in async parallel
let runAll() =
    urlList
    |> Seq.map fetchAsync
    |> Async.Parallel 
    |> Async.RunSynchronously
    |> ignore

/// Get max closing price from 2010-01-01 for each stock
runAll()

现在我们已经看到如何并行下载数据,并验证其确实有效,我们将扩展这个例子,对数据执行一些有用的操作。代码几乎与之前的一样,唯一不同的是我们增加了一个函数 getMaxPrice,用于解析 CSV 并通过管道迭代序列。然后我们将提取每只股票在整个期间的最大收盘价。所有这些操作都将并行、异步进行:

open System.Net
open Microsoft.FSharp.Control.WebExtensions

/// Stock symbol and URL to Yahoo finance
let urlList = [ "MSFT", "http://ichart.finance.yahoo.com/table.csv?s=MSFT&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv" 
                "GOOG", "http://ichart.finance.yahoo.com/table.csv?s=GOOG&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv" 
                "EBAY", "http://ichart.finance.yahoo.com/table.csv?s=EBAY&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv"
                "AAPL", "http://ichart.finance.yahoo.com/table.csv?s=AAPL&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv"
                "ADBE", "http://ichart.finance.yahoo.com/table.csv?s=ADBE&d=6&e=6&f=2013&g=d&a=1&b=1&c=2010&ignore=.csv"
              ]

/// Parse CSV and extract max price
let getMaxPrice(data:string) =   
    let rows = data.Split('\n')
    rows
    |> Seq.skip 1
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun s -> float s.[4])    
    |> Seq.take (rows.Length - 2)
    |> Seq.max

/// Async fetch of CSV data
let fetchAsync(name, url:string) =
    async { 
        try 
            let uri = new System.Uri(url)
            let webClient = new WebClient()
            let! html = webClient.AsyncDownloadString(uri)            
            let maxprice = (getMaxPrice(html.ToString()))
            printfn "Downloaded historical data for %s, max closing price since 2010-01-01: %f" name maxprice
        with
            | ex -> printfn "Exception: %s" ex.Message
    }
/// Helper function to run in async parallel
let runAll() =
    urlList
    |> Seq.map fetchAsync
    |> Async.Parallel 
    |> Async.RunSynchronously
    |> ignore

/// Get max closing price from 2010-01-01 for each stock
runAll()

我们现在已经看过两个例子,或者说是同一个例子的两个版本。你可以根据需要实验并扩展这个例子,以便更深入地了解如何在 F# 中使用并行和异步构造。

使用 TPL 进行并行编程

在前一节中,我们已经讲解了 F# 中异步工作流的部分内容。在本节中,我们将重点介绍 .NET 的 TPL(任务并行库)。

提示

如果你有兴趣,还可以探索并行序列。

TPL 比异步工作流更有用,特别是当涉及的线程需要经常进行 CPU 密集型操作时。换句话说,如果 IO 工作较少,TPL 是更优的选择。TPL 是 .NET Framework 4 的一部分,可以从所有 .NET 语言中使用。

编写并行程序的最简单方法是用Parallel.ForParallel.ForEach替换 for 循环,这两个函数位于System.Threading命名空间中。这两个函数将使循环并行执行,而不是按顺序执行。当然,现实中并非如此简单。例如,如果当前的迭代依赖于其他迭代,那么这就变得很难,这就是所谓的循环依赖。

MailboxProcessor

MailboxProcessor 也称为代理。代理是支持并发应用程序的构造,它们无需过多了解实现细节。它们更适用于不使用共享内存的场景,例如在具有多个节点的分布式计算机系统中。

该代理因 Erlang 编程语言而出名,在该语言中它被称为“actor”(演员)。演员的概念已部分实现于其他编程语言的各种库中。一个主要的库是针对 Scala 的Akka,它现在是官方 Scala 发行版的一部分。

我们将通过查看一个跟踪最大值的示例来介绍代理。每当接收到一个消息时,代理会重新计算最大值。还可以发送重置消息。与代理的所有通信都是通过消息传递来处理的。消息通常是区分联合类型。通过一个示例演示后,这一点将会更加清晰:

open System

// Type for our agent
type Agent<'T> = MailboxProcessor<'T>

// Control messages to be sent to agent
type CounterMessage = 
    | Update of float
    | Reset

module Helpers =
    let genRandomNumber (n) =
        let rnd = new System.Random()
        float (rnd.Next(n, 100))

module MaxAgent =
    // Agent to keep track of max value and update GUI
    let sampleAgent = Agent.Start(fun inbox ->
        let rec loop max = async {
            let! msg = inbox.Receive()
            match msg with
            | Reset ->
                return! loop 0.0
            | Update value ->
                let max = Math.Max(max, value)

                Console.WriteLine("Max: " + max.ToString())

                do! Async.Sleep(1000)
                return! loop max
        } 
        loop 0.0)

let agent = MaxAgent.sampleAgent
let random = Helpers.genRandomNumber 5
agent.Post(Update random)

在这个例子中,我们使用模块来演示当程序变得更大时,如何结构化程序。首先,我们将MailboxProcessor重新定义为Agent,以简化代码。然后,模块 helpers 中包含一个生成随机数的函数。

最后,模块MaxAgent定义了Agent本身。Agent通过模式匹配响应接收到的消息。如果消息是重置消息,则值会被重置;否则,代理会更新最大值并等待一秒钟。

最后三行代码将创建代理并向其发送一个随机值。你可以重复执行最后两行代码,以发送多个随机值并更新代理。这将产生接近以下的输出:

Max: 15

val random : float = 15.0
val it : unit = ()

> 

val random : float = 43.0
val it : unit = ()

> Max: 43

val random : float = 90.0
val it : unit = ()

> Max: 90

让我们发送一个重置消息,看看会发生什么。像之前一样,通过使用 F# Interactive 来实现:

agent.Post(Reset)

现在发送一些带有随机值的更新:

let random = Helpers.genRandomNumber 5
agent.Post(Update random)

代理是强大的构造,充当状态机的角色。你向它们发送消息,它们根据内部逻辑改变状态。

命令式编程简要回顾

本节将介绍命令式编程和面向对象编程。要进行面向对象编程,通常离不开命令式编程。换句话说,就是可变状态。可变状态与纯函数式编程并不是一个好组合,事实上,纯函数式编程完全禁止使用可变状态。幸运的是,F# 并非纯函数式编程语言,因此可变状态是被允许的。有了这些知识,我们可以继续学习面向对象编程以及如何在 F# 中实现它。

面向对象编程

F# 是一种多范式语言,其中面向对象编程占有一部分。这使得该语言在处理对象时能够与其他 .NET 语言无缝互动。几乎所有现代编程语言中常见的特性,如命令式编程、面向对象编程、数据存储和操作,都能在 F# 中找到。F# 尝试将命令式编程和函数式编程结合起来,并在许多方面取得了成功。F# 中的对象可以拥有构造函数、方法、属性(getter 和 setter)以及字段。

类和对象是面向对象编程OOP)的基础。它们用于建模应用中的动作、过程和任何概念实体。除了模块,类是 F# 中表示和封装数据及相关功能的最有用的方式。

考虑一个类来表示交易系统中的订单。订单首先会有订单方向、订单类型和价格。在本节中,我们将继续扩展该类的功能,以探讨类和面向对象的原则:

type OrderSide =
    | Buy
    | Sell

type OrderType =
    | Market
    | Limit

type Order(s: OrderSide, t: OrderType, p: float) =
    member this.S = s
    member this.T = t
    member this.P = p

我们可以使用新定义的类型 order,并通过 F# Interactive 调查成员变量:

> let order = Order(Buy, Limit, 45.50);;

val order : Order

> order.S;;
val it : OrderSide = Buy
> order.T;;
val it : OrderType = Limit
> order.P;;
val it : float = 45.5

对象和成员

我们现在将扩展 order 类,增加一个功能,允许我们切换订单方向。订单方向必须是可变的,我们需要一个成员函数来执行实际的工作。执行此操作的函数将被命名为 toggleOrderSide,并使用模式匹配来处理判别联合 OrderSide

// Toggle order side
type Order(s: OrderSide, t: OrderType, p: float) =
    let mutable S = s
    member this.T = t
    member this.P = p

    member this.Side
        with get()  = S
        and  set(s) = S <- s

    member this.toggleOrderSide() =
        match S with
        | Buy -> S <- Sell
        | Sell -> S <- Buy

如前所示,我们使用 F# Interactive 来研究类的变化:

> let order = Order(Buy, Limit, 45.50);;

val order : Order

> order.Side;;
val it : OrderSide = Buy
> order.toggleOrderSide();;
val it : unit = ()
> order.Side;;
val it : OrderSide = Sell
> order.toggleOrderSide();;
val it : unit = ()
> order.Side;;
val it : OrderSide = Buy

这可能看起来像是一次学习很多新东西。但首先我们使用关键字 mutable 来指示值 S 是可变的。这是因为我们想要修改已创建对象的值。与之相对的是不可变(immutable),正如我们在上一章中讨论过的。模式匹配负责实际操作, 操作符用于将新值赋给可变变量。

提示

请查看 MSDN 上关于 F# 中值的文章:msdn.microsoft.com/en-us/library/dd233185.aspx

方法和属性

能够使用更好的名称(如 T 或 P)访问价格和订单类型字段是不是很好?给你的对象、类型和函数起个清晰简洁的名字始终是个好习惯。这样不仅你自己,而且其他程序员也能更好地理解代码背后的意图。

type Order(s: OrderSide, t: OrderType, p: float) =
    let mutable S = s
    member this.T = t
    member this.P = p

    member this.Side
        with get()  = S
        and  set(s) = S <- s

    member this.Type
        with get() = this.T

    member this.Price
        with get() = this.P

    member this.toggleOrderSide() =
        match S with
        | Buy -> S <- Sell
        | Sell -> S <- Buy

现在可以分别通过属性 sidetype 访问成员值了。属性只是 getter 和 setter 的另一种叫法。getter 和 setter 用于获取和修改值。你可以通过省略 setter 来定义只读成员,如 type 属性所示:

> order.Type;;
val it : OrderType = Limit

> order.Price;;
val it : float = 45.5

重载操作符

如果某个特定功能需要在不调用特定函数的情况下暴露,重载操作符可能会很有用:

type Order(s: OrderSide, t: OrderType, p: float) =
    let mutable S = s
    member this.T = t
    member this.P = p

    member this.Side
        with get()  = S
        and  set(s) = S <- s

    member this.Type
        with get() = this.T

    member this.Price
        with get() = this.P

    member this.toggleOrderSide() =
        S <- this.toggleOrderSide(S)

    member private this.toggleOrderSide(s: OrderSide) =
        match s with
        | Buy -> Sell
        | Sell -> Buy

    static member (~-) (o : Order) =
        Order(o.toggleOrderSide(o.Side), o.Type, o.Price)

看看这个示例中的最后一行;在这里我们定义了一个重载的运算符,即一元减法。该运算符用于切换订单对象的订单方。

我们可以使用 F# Interactive 来调查这个问题,以及它在订单对象定义之外如何在代码中使用:

> let order1 = Order(Buy, Limit, 50.00);;

val order1 : Order

> let order2 = -order1;;

val order2 : Order

> order1;;
val it : Order = FSI_0263+Order {P = 50.0;
                                 Price = 50.0;
                                 Side = Buy;
                                 T = Limit;
                                 Type = Limit;}
> order2;;
val it : Order = FSI_0263+Order {P = 50.0;
                                 Price = 50.0;
                                 Side = Sell;
                                 T = Limit;
                                 Type = Limit;}

如你所见,我们首先创建了 order1 对象,然后创建了 order2,它被定义为 -order1。这将调用 order 类中的重载运算符并切换订单方。比较最后两次输出的订单方,自己看看结果。

使用 XML 文档

XML 文档在你需要为代码自动生成文档时非常有用。如果你直接将文档放在三斜杠注释中,整个内容将作为摘要。另一种可能性是使用特定的 XML 标签来指定某段文本属于哪种类型的文档。这样做能让你作为程序员以更灵活的方式记录代码。

有用的 XML 标签

以下是一些有用的 XML 标签:

标签 描述
summary 摘要描述被注释的代码
Returns 指定返回的内容
Remark 对代码的备注或需要注意的事项
exception 指定可能从代码中抛出的异常
See also 允许你将文档链接到其他部分以获取更多详细信息

典型的 XML 文档

XML 文档由 Visual Studio 及其 IntelliSense 使用,用于提供有关代码的信息。让我们为之前使用过的一个函数添加一些 XML 文档,并研究它如何影响 IntelliSense:

/// <summary>Extracts the maximum closing price from the provided CSV string</summary>
///<param name="str">Unparsed CSV string.</param>
///<remarks>Will leave the two last lines unhandled, due to Yahoo specific conditions</remarks>
///<returns>The maximum closing price for the entire sequence.</returns>
let getMaxPrice(data:string) =   
    let rows = data.Split('\n')
    rows
    |> Seq.skip 1
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun s -> float s.[4])    
    |> Seq.take (rows.Length - 2)
    |> Seq.max

最终结果将类似于以下内容。你可以看到 IntelliSense 以及它如何在工具提示框的底部包含摘要:

典型的 XML 文档

摘要

本章我们更详细地探讨了 F# 语言及其各种特性。目标是更详细地介绍一些在本书中后续会使用到的语言相关部分。F# 是一门庞大的语言,并不是所有特性都需要一次性掌握。互联网上有很多资源可以探索,F# 3.0 规范就是其中之一。这里的一些示例相当大,包含了许多已涵盖的特性和方面。

如果你已经消化了这里提供的材料,你将为下一章做好充分的准备。接下来将介绍金融数学和数值分析。

第三章:财务数学与数值分析

在本章中,读者将了解基本的数值分析和 F#中的算法实现。我们将讨论整数和浮点数是如何实现的,并且还将研究它们各自的局限性。基础统计内容也将涉及,同时会学习 F#中现有的函数,并与自定义实现进行比较。

本章将建立数值分析的基础,后续在研究期权定价和波动性时可以用到这些基础。我们还将使用上一章中覆盖的部分功能,来实现用于汇总统计的数学函数,并展示它们在现实生活中的实用性。

在本章中,你将学习:

  • 在 F#中实现算法

  • 数值问题

  • 实现基本的财务公式

  • 曲线拟合与回归

  • F#中的矩阵与向量

理解数字表示

在这一部分,我们将展示数字是如何在计算机中表示为整数或浮点数的。数字是计算机和编程的基础。计算机中的一切都是通过二进制数字——即零和一——来表示的。如今,我们拥有 64 位计算机,使得我们能够在 CPU 中天真地使用 64 位表示整数和浮点数。接下来,我们将深入探讨整数和浮点数在以下两部分中的表示方式。

整数

当我们谈论整数,通常用 Z 表示时,我们具体指的是以机器精度表示的整数,这些整数在计算机中用一系列位来精确表示。同时,整数是指可以不带小数或分数部分写出的数字,按照惯例用 Z 表示。例如,0 表示为 000000...,1 表示为...000001,2 表示为...000010,依此类推。从这个模式可以看出,数字是以二的幂次方表示的。为了表示负数,数字范围被分为两半,并使用二的补码表示。

当我们谈论没有负数的整数表示,即从零开始的数字时,我们讨论的是无符号整数。

二的补码

二的补码是一种将二进制数字范围分为正负十进制数的方式。通过这种方式,计算机可以表示正数和负数。另一方面,这意味着对于二的补码来说,数字范围相对于无符号表示的范围是减半的。二的补码是表示带符号整数的主要方式。

二的补码

二的补码表示的整数可以被看作一个环,如前图所示。当最大允许的正值或负值增加时,会发生溢出。溢出意味着我们越过了正负数之间的界限。

下表显示了一些整数及其二进制补码表示:

十进制 二进制补码
127 0111 1111
64 0100 0000
1 0000 0001
0 0000 0000
-1 1111 1111
-64 1100 0000
-127 1000 0001
-128 1000 0000

如你所见,8 位有符号整数的范围是从-128 到-127。更一般地说:

二进制补码

浮点数

浮点数,表示为 R,表示需要小数来定义的量。另一种描述这些数值的方式是将它们视为沿着连续线的量值。这些数值在实际生活中非常重要,用来建模诸如经济、统计和物理量等事物。在计算机中,浮点数是通过 IEEE 754 标准表示的。

IEEE 754 浮点数标准

IEEE 754 浮点数标准通过尾数和指数来描述浮点数;请参见下图。

例如,一个 64 位的浮点数由以下位模式组成:

符号位 指数 尾数
1 位 11 位 52 位

IEEE 754 浮点数标准

以下表格展示了浮点数及其二进制表示的示例:

二进制表示 浮点数
0x0000000000000000 0.0
0x3ff0000000000000 1.0
0xc000000000000000 -2.0
0x4000000000000000 2.0
0x402E000000000000 15.0

F# Interactive 能够将浮点数的十六进制表示解码为浮点数:

> 0x402E000000000000LF;;
val it: float = 15.0

提示

在 F# Interactive 中尝试前述的二进制表示。

学习 F#中的数值类型

在 F#中,像大多数现代编程语言一样,存在多种数值类型。这样做的主要原因是让你作为程序员能够在任何给定情况下选择最合适的数值类型。有时候,64 位整数并不必要,8 位整数就足够表示小数字了。另一个方面是内存效率和消耗,即 64 位整数将消耗 8 倍于 8 位整数的内存。

以下是 F#代码中最常用的数值类型表格。这些类型分为两大类:整数和浮点数:

类型 描述 示例
byte 8 位无符号整数 10uy, 0xA0uy
sbyte 8 位有符号整数 10y
int16 16 位有符号整数 10s
uint16 16 位无符号整数 10us
int, int32 32 位有符号整数 10
uint32 32 位无符号整数 10u
int64 64 位有符号整数 10L
uint64 64 位无符号整数 10UL
nativeint 硬件大小的有符号整数 10n
unativeint 硬件大小的有符号整数 10un
single, float32 32 位 IEEE 754 浮点数 10.0f
double, float 64 位 IEEE 754 浮点数 10.0
decimal 高精度十进制 10.0M
bigint 任意精度整数 10I
complex 使用 64 位浮点数的复数 Complex(10.0, 10.0)

以下是如何使用整数后缀的一些示例:

> let smallestnteger = 10uy;;
val smallestnteger : byte = 10uy

> let smallerInteger = 10s;;
val smallerInteger : int16 = 10s

> let smallInteger = 10us;;
val smallInteger : uint16 = 10us

> let integer = 10L;;
val integer : int64 = 10L

算术运算符

算术运算符应该是你熟悉的;不过,为了保持一致性,我们将在本节中覆盖它们。这些运算符按预期工作,接下来的表格通过每个运算符的示例来说明这一点。值得注意的是,返回整数除法余数的余数运算符。

让我们通过一个示例更详细地查看这个问题。首先,我们尝试将 10 除以 2,余数正如预期的那样为 0:

> 10 % 2;;
val it : int = 0

如果我们尝试将 10 除以 3,我们得到的余数是 1,因为 3 x 3 = 9,而 10 – 9 = 1:

> 10 % 3;;
val it : int = 1

以下表格显示了算术运算符、示例和描述:

运算符 示例 描述
+ x + y 加法
- x - y 减法
* x * y 乘法
/ x / y 除法
% x % y 余数
- -x 一元负号

注意

算术运算符不会检查溢出。如果你想检查溢出,可以使用 Checked 模块。你可以在 msdn.microsoft.com/en-us/library/vstudio/ee340296.aspfx 中找到更多关于 Checked 模块的信息。

学习算术比较

算术比较用于比较两个数字之间的关系。了解以下表格中显示的所有运算符是很有用的:

运算符 示例 描述
< x < y 小于
<= x <= y 小于或等于
> x > y 大于
>= x >= y 大于或等于
(=) x = y 相等
<> x <> y 不等于
min min x y 最小值
max max x y 最大值

一些算术比较的示例如下:

> 5.0 = 5.0;;
val it : bool = true
> 1 < 4;;
val it : bool = true
> 1.0 > 3.0;;
val it : bool = false

另外值得注意的是,在 F# 中你不能比较不同类型的数字。为了做到这一点,你必须将其中一个数字转换,方法如下:

> 5.0 >= 10;;
  5.0 >= 10
  -------^^
stdin(10,8): error FS0001: This expression was expected to have type float but here has type int

数学运算符

以下数学运算符表涵盖了编程语言或其标准库中预期包含的最基本的数学函数:

运算符 示例 描述
abs abs x 重载绝对值
acos acos x 重载反余弦
asin asin x 重载反正弦
atan atan x 重载反正切
ceil ceil x 重载浮点数向上取整
cos cos x 重载余弦
exp exp x 重载指数
floor floor x 重载浮点数向下取整
log log x 重载自然对数
log10 log10 x 重载以 10 为底的对数
(**) x ** y 重载指数
pown pown x y 重载整数指数
round round x 重载四舍五入
sin sin x 重载正弦函数
sqrt sqrt x 重载平方根函数
tan tan x 重载正切函数

转换函数

F# 中没有隐式转换,因为转换必须通过转换例程手动完成。类型之间的转换必须显式地使用下表中描述的运算符进行:

运算符 示例 描述
byte byte x 重载转换为字节
sbyte sbyte x 重载转换为有符号字节
int16 int16 重载转换为 16 位整数
uint16 uint16 重载转换为无符号 16 位整数
int32, int Int32 x, int x 重载转换为 32 位整数
uint32 uint32 x 重载转换为无符号 32 位整数
int64 int64 x 重载转换为 64 位整数
uint64 uint64 x 重载转换为无符号 64 位整数
nativeint nativeint x 重载转换为本机整数
unativeint unativeint x 重载转换为无符号本机整数
float, double float x, double x 重载转换为 64 位 IEEE 浮动点数
float32, single float32 x, single x 重载转换为 32 位 IEEE 浮动点数
decimal decimal x 重载转换为 System.decimal 数字
char char x 重载转换为 System.Char 值
enum enum x 重载转换为类型化的枚举值

这意味着后台永远不会有任何自动类型转换,这可能导致精度丢失。例如,数字不会从浮动点数转换为整数,仅仅为了适应你编写的代码。编译器会在转换前告诉你代码存在错误(它永远不会由编译器转换)。这方面的积极作用是,你总是清楚地知道数字的表示方式。

引入统计学

在本节中,我们将通过内置函数和简单的自定义函数来了解统计学。统计学在定量金融中被广泛应用。通常会分析更大的时间序列,F# 对数字序列有很好的支持;一些功能将在本节提到的示例中进行演示。

聚合统计

聚合统计是关于聚合数据的统计,例如从测量中收集的数字序列。了解这种集合的平均值是有用的;它告诉我们这些值的集中位置。minmax 值也很有用,可以帮助确定集合中的极值。

在 F# 中,Seq 模块内置了此功能。让我们通过示例查看如何在每种情况下使用它。

计算序列的和

考虑一个包含 100 个随机数的序列:

let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 100 -> rnd()]

我们可以使用管道运算符和模块函数Seq.sum来计算前述序列data的总和:

let sum = data |> Seq.sum

请注意,由于我们使用的是随机数生成器,结果sum会时常变化:

> sum;;
val it : float = 42.65793569

你可能会认为sum函数并不是最有用的,但有时它确实有用,了解它在模块库中的存在会节省你很多时间。

计算序列的平均值

对于这个例子,我们稍微修改一下随机种子函数,以生成 0 到 10 之间的 500 个数字:

let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 500 -> rnd() * 10.0]

由于随机函数的分布,这个序列的期望值是 5:

let avg = data |> Seq.average

由于我们是随机生成数字,因此数值可能会有所变化:

> avg;;
val it : float = 4.983808457

正如预期的那样,平均值几乎是 5。如果我们生成更多的数字,数值会越来越接近理论上的期望值 5:

let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 10000 -> rnd() * 10.0]

let avg = data |> Seq.average

> avg;;
val it : float = 5.006555917

计算序列的最小值

我们不通过迭代序列并使用某种循环结构与临时变量来跟踪最小值,而是倾向于使用 F#中的函数式方法。为了计算序列的最小值,我们使用模块函数Seq.min

let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 10 -> rnd() * 10.0]

val data : float list =[5.0530272; 6.389536232; 6.126554094; 7.276151291; 0.9457452972; 7.774030933; 7.654594368; 8.517372011; 3.924642724; 6.572755164]

let min = data |> Seq.min

> min;;
val it : float = 0.9457452972

这与前面的代码非常相似,不同的是我们生成了 10 个随机数并检查列表中的值。如果我们手动寻找最小值,并将其与 F#计算得出的值进行比较,会发现它们是匹配的。

计算序列的最大值

在以下示例中,我们将使用Seq.max来计算列表的最大值:

let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 5 -> rnd() * 100.0]

val data : float list =[7.586052086; 22.3457242; 76.95953826; 59.31953153; 33.53864822]

let max = data |> Seq.max

> max;;
val it : float = 76.95953826

计算序列的方差和标准差

到目前为止,我们已经使用现有的函数进行了统计分析。现在,让我们实现方差和标准差。

计算方差

让我们使用以下函数并计算骰子的方差:

let variance(values=
  let average = Seq.average values values
  |> Seq.map (fun x -> (1.0 / float (Seq.length values)) * (x - average) ** 2.0)
  |> Seq.sum

一个骰子有六个离散的结果,从 1 到 6,每个结果的概率相等。期望值是 3.5,计算公式为(1 + 2 + 3 + 4 + 5 + 6)/6。骰子的方差通过以下函数计算:

> variance [1.0 .. 6.0];;
val it : float = 2.916666667

计算标准差

我们从使用之前定义的variance函数来实现标准差函数开始。根据统计学,标准差是方差的平方根:

let stddev1(values:seq<float>) = sqrt(variance(values))

前述函数工作得很好,但为了展示序列的强大功能,我们将使用fold函数来实现标准差。fold函数将对每个元素应用给定的函数并累积结果。最后的0.0值意味着我们没有初始值。你可能记得在上一章的fold部分中提到过这一点。如果我们使用乘法进行折叠,则初始值应为1.0。最后,我们只需将总和传递给平方根函数sqrt,就完成了:

let stddev2(values) =
  let avg = Seq.average values
  values    
  |> Seq.fold (fun acc x -> acc + (1.0 / float (Seq.length values)) * (x -avg) ** 2.0) 0.0
  |> sqrt

让我们使用一些样本数据来验证:

> stddev1 [2.0; 4.0; 4.0; 4.0; 5.0; 5.0; 7.0; 9.0];;
val it : float = 2.0

> stddev2 [2.0; 4.0; 4.0; 4.0; 5.0; 5.0; 7.0; 9.0];;
val it : float = 2.0

现在,我们可以回过头来分析之前生成并使用的随机数据,这些数据出现在我们查看序列函数构建时:

let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 100 -> rnd() * 10.0]

let var = variance data
let std = stddev2 data

我们可以验证标准差的平方等于方差这一事实:

> std * std = var;;
val it : bool = true

查看一个示例应用

本节中我们将看到的示例应用是本章中各个部分的结合,简单地生成一个关于给定数据序列的统计信息输出:

/// Helpers to generate random numbers
let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 500 -> rnd() * 10.0]

/// Calculates the variance of a sequence
let variance(values:seq<float>) = values
  |> Seq.map (fun x -> (1.0 / float (Seq.length values)) * (x - (Seq.average values)) ** 2.0)
  |> Seq.sum

/// Calculates the standard deviation of a sequence
let stddev(values:seq<float>) = values    
  |> Seq.fold (fun acc x -> acc + (1.0 / float (Seq.length values)) * (x - (Seq.average values)) ** 2.0) 0.0
  |> sqrt

let avg = data |> Seq.average
let sum = data |> Seq.sum
let min = data |> Seq.min
let max = data |> Seq.max
let var = data |> variance
let std = data |> stddev

评估代码后,输出显示如下生成的随机序列的统计属性:

val avg : float = 5.150620541
val sum : float = 2575.310271
val min : float = 0.007285140458
val max : float = 9.988292227
val var : float = 8.6539651
val std : float = 2.941762244

这意味着该序列的平均值大约为 5.15,标准差大约为 2.94。基于这些事实,我们可以假设该序列按照某种已知分布(例如正态分布)重新构建其分布。

使用 Math.NET 库

如果你不想自己实现数值计算相关的函数,可以使用一个非常优秀的库——Math.NET。这是一个开源库,涵盖了线性代数、统计学等基础数学内容。

Math.NET 库由以下几个库组成:

  • Math.NET 数值库:数值计算

  • Math.NET 钕库:信号处理

  • Math.NET LINQ 代数:计算机代数

  • Math.NET 钇库:实验性网络计算代数

在本节中,我们将学习 Math.NET 数值库,并了解它如何帮助我们在 F# 编程中使用。首先,我们需要确保 Math.NET 已经安装在我们的系统中。

安装 Math.NET 库

可以通过内置的包管理器安装 Math.NET 库。

安装 Math.NET 库

  1. 通过进入视图 | 其他窗口 | 包管理器控制台来打开包管理器控制台

  2. 输入以下命令:

    Install-Package MathNet.Numerics
    
    
  3. 等待安装完成。

注意

你可以在项目网站上阅读更多关于 Math.NET 项目的内容:www.mathdotnet.com/

随机数生成简介

让我们首先看看生成随机数的不同方法。随机数在统计学和模拟中使用频繁,尤其是在蒙特卡洛模拟中。我们在开始研究 Math.NET 和如何生成随机数之前,需要了解一些基础理论。

伪随机数

在计算机和编程中,随机数通常指的是伪随机数。伪随机数看起来是随机的,但实际上并非如此。换句话说,只要已知算法的某些特性和所使用的种子值,它们就是确定性的。种子是生成随机数的算法输入。通常,种子的选择会是系统的当前时间或其他唯一值。

在 .NET 平台提供的 System.Random 类中的随机数生成器基于 Donald E. Knuth 提出的减法随机数生成算法。该算法如果使用相同的种子,将生成相同的数字序列。

梅森旋转

梅森旋转伪随机数生成器能够以高效的方式生成不那么确定的数字。这些特性使得该算法成为今天最流行的算法之一。以下是如何在 Math.NET 中使用该算法的示例:

open MathNet.Numerics.Random

let mersenneTwister = new MersenneTwister(42);
let a = mersenneTwister.NextDouble();

在 F# Interactive 中,我们可以使用梅森旋转生成一些数字:

> mersenneTwister.NextDouble();;
val it : float = 0.7965429842

> mersenneTwister.NextDouble();;
val it : float = 0.9507143116

> mersenneTwister.NextDouble();;
val it : float = 0.1834347877
> mersenneTwister.NextDouble();;
val it : float = 0.7319939383

> mersenneTwister.NextDouble();;
val it : float = 0.7796909974

概率分布

概率分布通常用于统计学和金融学。它们用于分析和分类一组样本,以研究其统计属性。

正态分布

正态分布是最常用的概率分布之一。

在 Math.NET 中,正态分布可以如下使用:

open MathNet.Numerics.Distributions

let normal = new Normal(0.0, 1.0)
let mean = normal.Mean
let variance = normal.Variance
let stddev = normal.StdDev

通过使用前面的示例,我们创建了一个均值为零,标准差为一的正态分布。我们还可以从分布中检索均值、方差和标准差:

> normal.Mean;;
val it : float = 0.0

> normal.Variance;;
val it : float = 1.0

> normal.StdDev;;
val it : float = 1.0

在这种情况下,均值和标准差与我们在 Normal 类的构造函数中指定的相同。也可以从分布中生成随机数字。我们可以使用前面的分布生成一些随机数字,这些数字基于已定义的属性:

> normal.Sample();;
val it : float = 0.4458429471

> normal.Sample();;
val it : float = 0.4411828389

> normal.Sample();;
val it : float = 0.9845689791

> normal.Sample();;
val it : float = -1.733795869

在 Math.NET 库中,还有其他一些分布,如:

  • 泊松分布

  • 对数正态

  • 埃尔朗分布

  • 二项分布

统计

在 Math.NET 中,还提供了对描述性统计的强大支持,可以用来确定样本集合的属性。这些样本可以是来自测量的数字,也可以是同一库生成的数字。

在这一部分,我们将查看一个示例,分析一个具有已知属性的样本集合,并了解 DescriptiveStatistics 类如何为我们提供帮助。

我们通过生成一些数据来进行分析:

let dist = new Normal(0.0, 1.0)
let samples = dist.Samples() |> Seq.take 1000 |> Seq.toList

注意从SeqList的转换;这是因为如果不进行转换,samples 将是一个惰性集合。这意味着该集合每次在程序中使用时都会生成不同的数字,而这正是我们在这种情况下不希望发生的。接下来,我们实例化 DescriptiveStatistics 类:

let statistics = new DescriptiveStatistics(samples)

它将处理之前创建的样本,并创建一个描述 samples 列表中数字统计属性的对象。现在,我们可以获取有关数据的有价值信息:

// Order Statistics
let maximum = statistics.Maximum
let minimum = statistics.Minimum

// Central Tendency
let mean = statistics.Mean

// Dispersion
let variance = statistics.Variance
let stdDev = statistics.StandardDeviation

如果我们更仔细地查看均值、方差和标准差,我们会发现它们与该集合的预期值相符:

> statistics.Mean;;
val it : float = -0.002646746232

> statistics.Variance;;
val it : float = 1.000011159

> statistics.StandardDeviation;;
val it : float = 1.00000558

线性回归

线性回归在统计学中广泛应用,用于分析样本数据。线性回归揭示了两个变量之间的关系。它目前不是 Math.NET 的一部分,但可以通过它实现。Math.NET 中回归是一个被请求的功能;希望在未来,库将本地支持这一功能。

使用最小二乘法

让我们看看线性回归中最常用的一种方法,最小二乘法。这是一种标准的方法,用于通过最小二乘法找到近似解。最小二乘法将优化总体解,以最小化误差的平方,这意味着它将找到最能拟合数据的解。

以下是使用 Math.NET 进行线性代数部分的 F#实现最小二乘法:

open System
open MathNet.Numerics
open MathNet.Numerics.LinearAlgebra
open MathNet.Numerics.LinearAlgebra.Double
open MathNet.Numerics.Distributions

/// Linear regression using least squares

let X = DenseMatrix.ofColumnsList 5 2 [ List.init 5 (fun i -> 1.0); [ 10.0; 20.0; 30.0; 40.0; 50.0 ] ] X
let y = DenseVector [| 8.0; 21.0; 32.0; 40.0; 49.0 |]
let p = X.QR().Solve(y)
printfn "X: %A" X
printfn "y: %s" (y.ToString())
printfn "p: %s" (p.ToString())

let (a, b) = (p.[0], p.[1])

自变量数据y和因变量数据x作为输入提供给求解器。你可以在这里使用xy之间的任何线性关系。回归系数将告诉我们回归线的特性,y = ax + b

使用多项式回归

在本节中,我们将探讨一种将多项式拟合到数据点的方法。当数据中的关系更适合用多项式来描述时,这种方法非常有用,例如二次或三次多项式。我们将在第六章,波动率探索中使用此方法,我们将拟合一个二次多项式到期权数据上,以构建一个波动率微笑图表。在本节中,我们将为这个用例奠定基础。

我们将继续使用 Math.NET 进行线性代数计算,以求解二次多项式的系数。

我们将从生成一些多项式的样本数据开始:

使用多项式回归

然后,我们从-10.010.0生成 x 值,步长为0.2,并使用这些 x 值和前面的方程加噪声来生成 y 值。为了实现这一点,我们使用了均值为零、标准差为 1.0 的正态分布:

let noise = (Normal.WithMeanVariance(0.0,0.5))
/// Sample points for x²-3x+5
let xdata = [-10.0 .. 0.2 .. 10.0]
let ydata = [for x in xdata do yield x ** 2.0 - 3.0*x + 5.0 + noise.Sample()]

接下来,我们使用 Math.NET 中的线性代数函数实现系数的最小二乘估算。在数学术语中,这可以表示为:

使用多项式回归

这意味着我们将使用矩阵 A,它存储 x 值和 y 向量,用来估算系数向量 c。让我们看一下下面的代码,看看这是如何实现的:

let N = xdata.Length
let order = 2

/// Generating a Vandermonde row given input v
let vandermondeRow v = [for x in [0..order] do yield v ** (float x)]

/// Creating Vandermonde rows for each element in the list
let vandermonde = xdata |> Seq.map vandermondeRow |> Seq.toList

/// Create the A Matrix
let A = vandermonde |> DenseMatrix.ofRowsList N (order + 1)
A.Transpose()

/// Create the Y Matrix
let createYVector order l = [for x in [0..order] do yield l]
let Y = (createYVector order ydata |> DenseMatrix.ofRowsList (order + 1) N).Transpose()

/// Calculate coefficients using least squares
let coeffs = (A.Transpose() * A).LU().Solve(A.Transpose() * Y).Column(0)

let calculate x = (vandermondeRow(x) |> DenseVector.ofList) * coeffs

let fitxs = [(Seq.min xdata).. 0.02 ..(Seq.max xdata)]
let fitys = fitxs |> List.map calculate
let fits = [for x in [(Seq.min xdata).. 0.2 ..(Seq.max xdata)] do yield (x, calculate x)]

系数向量中的值是倒序的,这意味着它们对应于一个拟合数据的多项式,但系数是倒转的:

> coeffs;;
val it = seq [4.947741224; -2.979584718; 1.001216438]

这些值与我们在前面代码中用作输入的多项式非常接近。下面是样本数据点与拟合曲线的图表。该图表使用 FSharpChart 制作,我们将在下一章中介绍它。

使用多项式回归

使用 Math.net 进行多项式回归

好奇的读者可以使用以下代码段来生成前面的图形:

open FSharp.Charting
open System.Windows.Forms.DataVisualization.Charting

fsi.AddPrinter(fun (ch:FSharp.Charting.ChartTypes.GenericChart) -> ch.ShowChart(); "FSharpCharting")
let chart = Chart.Combine [Chart.Point(List.zip xdata ydata ); Chart.Line(fits).WithTitle("Polynomial regression")]

学习根寻找算法

在本节中,我们将学习在数值分析中用于寻找函数根的不同方法。根寻找算法非常有用,当我们讨论波动率和隐含波动率时,我们将进一步了解它们的应用。

二分法

在本节中,我们将看一种使用二分法找到函数根的方法。此方法将在本书后续章节中用于通过数值方法找到给定某一市场价格的期权的隐含波动率。二分法使用迭代,并反复将一个区间二分为下一个迭代的范围。

以下函数在 F#中实现了二分法:

let rec bisect n N (f:float -> float) (a:float) (b:float) (t:float) : float =
  if n >= N then -1.0
  else
    let c = (a + b) / 2.0
    if f(c) = 0.0 || (b - a) / 2.0 < t then
      // Solution found
      c
    else
      if sign(f(c)) = sign(f(a)) then
        bisect (n + 1) N f c b t
      else    
        bisect (n + 1) N f a c t

看一个例子

现在,我们将看一个求解二次方程根的例子。方程 x² - x - 6 在下面的图中进行了绘制:

看一个例子

前面的二次方程的根可以在图中轻松看到。否则,也有解析方法可以求解;例如,配方法。方程的根是-2 和 3。

接下来,我们在 F#中创建一个匿名函数来描述我们感兴趣的用于求解根的函数:

let f = (fun x -> (x**2.0 - x - 6.0))

我们可以使用前面图中找到的根来测试上面的函数:

> f(-2.0);;
val it : float = 0.0

> f(3.0);;
val it : float = 0.0

结果如预期所示。现在,我们可以继续并使用 lambda 函数作为bisect函数的参数:

// First root, on the positive side
let first = bisect 0 25 f 0.0 10.0 0.01

// Second root, on the negative side
let second = bisect 0 25 f -10.0 0.0 0.01

前两个参数,025,用于跟踪迭代过程。我们传入0是因为我们希望从第 0 次迭代开始,然后迭代 25 次。下一个参数是我们在前面代码中定义的函数f。接下来的两个参数是限制条件,也就是我们可以在其中寻找根的范围。最后一个参数是用于迭代内部比较的精度值。

我们现在可以检查这两个变量,看看是否找到了根:

> first;;
val it : float = -2.001953125

> second;;
val it : float = 2.998046875

它们几乎与解析解-2 和 3 完全相同。这是数值分析中典型的情况。解几乎永远不会完全准确。每一步都会因为浮点数、四舍五入等原因引入一些不准确。

使用牛顿-拉夫森法寻找根

牛顿-拉夫森法,或简称牛顿法,通常比二分法收敛得更快。牛顿-拉夫森法还需要函数的导数,这在某些情况下可能是一个问题。特别是当没有解析解时,这个问题尤为突出。以下实现是二分法的修改版,使用函数的导数来判断是否找到了解。我们来看看牛顿法在 F#中的实现:

// Newton's Method
let rec newtonraphson n N (f:float -> float) (fprime:float -> float) (x0: float) (tol:float) : float =
  if n >= N then -1.0
  else        
    let d = fprime(x0)
    let newtonX = x0 - f(x0) / d    
    if abs(d) < tol then
      -1.0
    else
      if abs(newtonX - x0) < tol then
        newtonX   // Solution found
      else
        newtonraphson (n +1) N f fprime newtonX tol

使用前面的方法的一个缺点是,我们使用了一个固定点收敛标准,abs(newtonX - x0) < tol,这意味着当满足此标准时,我们可能离实际解还很远。

看一个例子

现在我们可以尝试求解二的平方根,预期结果是 1.41421。首先,我们需要函数本身,fun x -> (x**2.0 – 2.0)。我们还需要该函数的导数,x -> (2.0*x)

let f = (fun x -> (x**2.0 - 2.0))
let fprime = (fun x -> (2.0*x))
let sqrtOfTwo = newtonraphson 0 25 f fprime 1.0 10e-10    

现在,我们使用牛顿-拉弗森方法来求解方程 x² - 2 的根。使用 F#交互式环境,我们可以按如下方式进行研究:

> newtonraphson 0 25 f fprime 1.0 10e-10;;
val it : float = 1.414213562    

这是我们预期的答案,且该方法能有效求解根!注意,如果我们将起始值x01.0改为-1.0,我们将得到负根:

> newtonraphson 0 25 f fprime -1.0 10e-10;;
val it : float = -1.414213562

这也是方程的一个有效解,因此在使用此方法求解根时要注意这一点。绘制函数图像可能会有所帮助,正如我们在二分法部分所做的那样,以便更好地了解从哪里开始。

使用割线法求根

割线法不需要函数的导数,是牛顿-拉弗森方法的一种近似方法。它在迭代中使用有限差分近似。以下是 F#中的递归实现:

// Secant method
let rec secant n N (f:float -> float) (x0:float) (x1:float) (x2:float) : float =
  if n >= N then x0
  else
    let x = x1 - (f(x1))*((x1 - x0)/(f(x1) - f(x0)))
    secant (n + 1) N f x x0

看一个例子

让我们看一个例子,我们使用割线法来求解一个函数的一个根。我们将尝试求解612的正根,它是 25 的平方(25 x 25 = 625)略小的数字:

let f = (fun x -> (x**2.0 - 612.0))

> secant 0 10 f 0.0 10.0 30.0;;
val it : float = 24.73863375

总结

在这一章中,我们深入探讨了 F#与数值分析的结合,以及这两者如何因为语言的函数式语法而紧密配合。我们介绍了算法实现、基本的数值问题以及 Math.NET 库。阅读完这一章后,你将更加熟悉 F#和数值分析,并能够自己实现算法。在本章的结尾,我们介绍了一个使用二分法的例子。这个方法在我们稍后讨论 Black-Scholes 模型和隐含波动率时将非常有用。

在下一章中,我们将基于到目前为止学到的内容,扩展我们当前的知识,涉及数据可视化、基础 GUI 编程以及财务数据的绘制。

第四章:数据可视化入门

在本章中,你将学习如何开始进行数据可视化,并在 F# 中构建图形用户界面(GUI)。在定量金融中,能够绘制和可视化时间序列是至关重要的。F# 是一款很好的工具,我们将学习如何使用 F# 作为一个高级图形计算器,通过 F# Interactive 来实现。

本章的内容将在整本书中使用,凡是需要用户界面的地方都会用到。在本章中,你将学习:

  • 在 F# 和 .NET 中编程基本 GUI

  • 使用 Microsoft Charts 绘制数据

  • 绘制金融数据

  • 构建交互式 GUI

在 F# 中创建你的第一个 GUI

F# 利用 .NET 平台,GUI 编程也不例外。本节将使用来自 .NET 平台的所有类,我们将重点关注来自 System.Windows.Forms 命名空间的类。

可以在 F# Interactive 中使用相同的代码并动态修改 GUI。我们将在 显示数据 部分更详细地探讨这一点。

让我们来看一个例子,使用 .NET 表单和按钮。按钮将连接到一个事件处理程序,每次点击按钮时都会触发该事件处理程序。正如你在阅读代码时看到的,事件处理程序是高阶函数,从而产生了简洁紧凑的代码。

open System.Windows.Forms

let form = new Form(Text = "First F# form")
let button = new Button(Text = "Click me to close!", Dock = DockStyle.Fill)

button.Click.Add(fun _ -> Application.Exit() |> ignore)
form.Controls.Add(button)
form.Show()

上述代码的输出截图如下:

在 F# 中创建你的第一个 GUI

第一个由表单和按钮组成的 F# GUI 应用程序

组合界面

现在,我们已经看过了生成表单的第一个代码,并组成了一个由按钮构成的非常简单的界面。正如你可能已经注意到的,F# 没有像其他 .NET 语言那样的可视化设计器。F# 中有几种方式来组合界面:

  • 手动编写界面代码

  • 使用 C# 可视化设计器并将代码转换为 F#

  • 使用其他 .NET 语言构建库,并在 F# 中使用它

  • 构建你自己的可视化设计器以输出 F# 代码

在本书中,我们将主要使用第一种方式——手动编写界面代码。这可能看起来有些繁琐,但优点是可以完全控制布局。我们现在将看一个更大的例子,使用代理来跟踪最高数字,并且包含一个带按钮的用户界面。当用户点击按钮时,一个随机数字会被发送到代理(见下图)。然后,代理每秒输出最高数字。此外,例子还展示了如何以现实的方式使用命名空间和模块。这能帮助读者了解何时使用命名空间和模块,以及在程序变得更大时如何结构化代码。

组合界面

代理应用程序的形式,其中包含一个按钮来将值发送给代理。

提示

项目中文件的顺序如下:

  • 代理

  • GUI

  • 程序

否则,由于引用问题,你会看到一些错误。见下图,展示了 解决方案资源管理器,请注意文件顺序:

构建界面

更多关于代理

首先,我们从代理开始。这个代理与第二章中关于代理的部分中的代理非常相似,进一步学习 F#,只是做了一些修改并且使用了Agents命名空间。代码如下:

namespace Agents

    open System

    // Type for our agent
    type Agent<'T> = MailboxProcessor<'T>

    // Control messages to be sent to agent
    type CounterMessage = 
        | Update of float
        | Reset

    module Helpers =
        let genRandomNumber (n) =
            let rnd = new System.Random()
            float (rnd.Next(n, 100))

    module MaxAgent =
        // Agent to keep track of max value and update GUI
        let sampleAgent = Agent.Start(fun inbox ->
            let rec loop m = async {
                let! msg = inbox.Receive()
                match msg with
                | Reset ->
                    return! loop 0.0
                | Update value ->
                    let m = max m value

                    Console.WriteLine("Max: " + m.ToString())

                    do! Async.Sleep(1000)
                    return! loop m
            } 
            loop 0.0)

上面代码的截图如下:

更多关于代理

包含代理输出的控制台窗口

用户界面

用户界面被放置在 GUI 命名空间中。SampleForm继承自Form .NET 类。如果你熟悉其他 .NET 语言,你会看到一些常见的步骤。所有布局代码也是代码的一部分。如前所述,F#中没有可视化设计器。要使用System.Windows.Forms,你必须将一个同名的程序集添加为引用。代码如下:

namespace GUI

    open System
    open System.Drawing
    open System.Windows.Forms
    open Agents

    // User interface form
    type public SampleForm() as form =
        inherit Form()

        let valueLabel = new Label(Location=new Point(25,15))
        let startButton = new Button(Location=new Point(25,50))
        let sendButton = new Button(Location=new Point(25,75))
        let agent = MaxAgent.sampleAgent
        let initControls =
            valueLabel.Text <- "Sample Text"
            startButton.Text <- "Start"
            sendButton.Text <- "Send value to agent"
        do
            initControls

            form.Controls.Add(valueLabel)
            form.Controls.Add(startButton)

            form.Text <- "SampleApp F#"

            startButton.Click.AddHandler(new System.EventHandler
                (fun sender e -> form.eventStartButton_Click(sender, e)))

        // Event handler(s)
        member form.eventStartButton_Click(sender:obj, e:EventArgs) =
            let random = Helpers.genRandomNumber 5
            Console.WriteLine("Sending value to agent: " + random.ToString())
            agent.Post(Update random)
            ()

主应用程序

这是主应用程序的入口点。它被注释以告知运行时环境(.NET 平台)从何处开始。这是通过使用[<STAThread>]注解来完成的。在这里,我们简单地启动应用程序及其 GUI。SampleForm的代码如下:

// Main application entry point
namespace Program

    open System
    open System.Drawing
    open System.Windows.Forms

    open GUI

    module Main =
        [<STAThread>]
        do
            Application.EnableVisualStyles()
            Application.SetCompatibleTextRenderingDefault(false)
            let view = new SampleForm()
            Application.Run(view)

了解事件处理

事件驱动编程和用户事件是构建 GUI 的常见方式。F#中的事件处理器非常简单,lambda 函数易于阅读和理解。紧凑的代码总是更受欢迎,并且使得如维护和理解代码等任务对所有相关人员来说都更容易。

如果我们仔细观察之前用于事件处理的代码,你会看到我们首先使用一个 lambda 函数,并在 lambda 内部调用类的成员函数:

startButton.Click.AddHandler(new System.EventHandler
                (fun sender e -> form.eventStartButton_Click(sender, e)))

        // Event handler(s)
        member form.eventStartButton_Click(sender:obj, e:EventArgs) =
            let random = Helpers.genRandomNumber 5
            Console.WriteLine("Sending value to agent: " + random.ToString())
            agent.Post(Update random)
            ()

这只是一种使代码更具可读性和更容易理解的方式。当然,也可以将所有逻辑直接包含在 lambda 函数中;但这种方法更加简洁,尤其对于大型项目。

显示数据

显示和可视化数据对于更好地理解其特性至关重要。此外,数据在定量金融中处于核心地位。F#是一个用于数据分析和可视化的利器。大多数可视化和用户界面功能都来自.NET 平台。结合 F#的探索特性,特别是通过 F#交互式,这种组合变得非常高效和强大。

我们首先使用 F#交互式创建一个表单,该表单将显示传递给它的数据。这意味着我们将有一个可以在运行时更改内容的表单,而无需重新编译。表单中的控件也是可以互换的:

// The form
open System
open System.Drawing
open System.Windows.Forms

let form = new Form(Visible = true, Text = "Displaying data in F#",TopMost = true, Size = Drawing.Size(600,600))

let textBox = 
    new RichTextBox(Dock = DockStyle.Fill, Text = "This is a text box that we can feed data into", Font = new Font("Lucida Console",16.0f,FontStyle.Bold), ForeColor = Color.DarkBlue)

form.Controls.Add textBox

如果你运行这段代码,你会看到一个标题为在 F#中显示数据的表单,如以下截图所示:

显示数据

包含 RichTextBox 控件用于显示数据的窗口

我们需要一个函数将数据发送到窗口中的文本框并显示它。以下是完成此任务的函数:

let show x = 
   textBox.Text <- sprintf "%30A" x

现在,我们可以使用这个函数,它会将格式化的数据发送到我们的文本框(textBox)。这里有一些示例,展示了如何使用这个函数;如后续代码片段所示,利用管道函数是很有用的:

show (1,2)
show [ 0 .. 100 ]
show [ 0.0 .. 2.0 .. 100.0 ]
// Using the pipe operator
(1,2,3) |> show
[ 0 .. 99 ] |> show
[ for i in 0 .. 99 -> (i, i*i) ] |> show

如果你想清除文本框的内容,可以写:

textBox.Clear()

输出结果如下:

显示数据

这是表单的样子,内容来自前一个代码片段生成的内容

提示

尝试自己动手做做看,看看哪种工作流最适合你。

扩展表单以使用表格

现在我们已经看过如何使用 F# Interactive 并即时将数据传递到表单中,我们可以扩展这个概念,并使用表格,如下所示:

open System
open System.Drawing
open System.Windows.Forms

// The form
let form2 = new Form(Visible = true, Text = "Displaying data in F#", TopMost = true, Size = Drawing.Size(600,600))

// The grid
let data = new DataGridView(Dock = DockStyle.Fill, Text = "Data grid", Font = new Drawing.Font("Lucida Console", 10.0f), ForeColor = Drawing.Color.DarkBlue)

form2.Controls.Add(data)

// Some data
data.DataSource <- [| ("ORCL", 32.2000, 31.1000, 31.1200, 0.0100);
                      ("MSFT", 72.050, 72.3100, 72.4000, 0.0800);
                      ("EBAY", 58.250, 58.5200, 58.5100, 0.0100)|]

上述代码将把DataGridView添加到表单中,并为其添加一些样式。代码的最后几行将填充DataGridView,并加入一些示例数据。它最终将呈现出如下图所示的样子:

扩展表单以使用表格

将包含示例数据的 DataGridView 添加到表单中

让我们扩展这个例子,并使用代码一起设置列标题,并使用代码来操作集合:

open System
open System.Drawing
open System.Windows.Forms
open System.Collections.Generic

// The form
let form2 = new Form(Visible = true, Text = "Displaying data in F#", TopMost = true, Size = Drawing.Size(600,600))

// The grid
let data = new DataGridView(Dock = DockStyle.Fill, Text = "Data grid",Font = new Drawing.Font("Lucida Console", 10.0f), ForeColor = Drawing.Color.DarkBlue)

form2.Controls.Add(data)

// Generic list
let myList = new List<(string * float * float * float * float)>()

// Sample data
myList.Add(("ORCL", 32.2000, 31.1000, 31.1200, 0.0200))
myList.Add(("MSFT", 72.050, 72.3100, 72.4000, 0.0100))

data.DataSource <- myList.ToArray()

// Set column headers
do data.Columns.[0].HeaderText <- "Symb"
do data.Columns.[1].HeaderText <- "Last sale"
do data.Columns.[2].HeaderText <- "Bid"
do data.Columns.[3].HeaderText <- "Ask"
do data.Columns.[4].HeaderText <- "Spread"

do data.Columns.[0].Width <- 100

结果将看起来像下图所示的窗口:

扩展表单以使用表格

使用集合作为数据源的格式化 DataGridView

从 Yahoo! Finance 显示财务数据

现在,我们来看一个更大的示例应用程序,我们将在其中使用到目前为止介绍的概念,并将功能扩展到财务数据的可视化。在这里,我们将从 Yahoo! Finance 下载数据,并在同一个图表窗口中显示收盘价及其计算出的移动平均线。最终,应用程序的界面大致如下所示:

从 Yahoo! Finance 显示财务数据

一个示例应用程序,用于可视化来自 Yahoo! Finance 的数据

理解应用程序代码

该应用程序将使用前面章节中介绍的一些代码。如果你对某些内容不熟悉,请回去复习相关的主题。这里的主要构建模块是Systems.Windows.FormsSystem.Windows.Forms.DataVisualization.Charting。更多信息可以在 MSDN 在线获取:msdn.microsoft.com/en-us/library/system.windows.forms.datavisualization.charting.aspx

让我们来看一下提供上述功能所需的代码:

#r "System.Windows.Forms.DataVisualization.dll"

open System
open System.Net
open System.Windows.Forms
open System.Windows.Forms.DataVisualization.Charting
open Microsoft.FSharp.Control.WebExtensions

我们将首先创建一个图表,并通过设置styleChartAreas来初始化它:

// Create chart and form
let chart = new Chart(Dock = DockStyle.Fill)
let area = new ChartArea("Main")
chart.ChartAreas.Add(area)

然后创建并显示表单。之后,设置程序的标题,并将图表控件添加到表单中:

let mainForm = new Form(Visible = true, TopMost = true, 
                        Width = 700, Height = 500)

do mainForm.Text <- "Yahoo Finance data in F#"
mainForm.Controls.Add(chart)

然后,有一些代码来创建所需的两个图表系列,并为这两个系列设置样式,以便将它们区分开来。股票价格系列将是红色,移动平均线将是蓝色:

// Create series for stock price
let stockPrice = new Series("stockPrice")
do stockPrice.ChartType <- SeriesChartType.Line
do stockPrice.BorderWidth <- 2
do stockPrice.Color <- Drawing.Color.Red
chart.Series.Add(stockPrice)
// Create series for moving average
let movingAvg = new Series("movingAvg")
do movingAvg.ChartType <- SeriesChartType.Line
do movingAvg.BorderWidth <- 2
do movingAvg.Color <- Drawing.Color.Blue
chart.Series.Add(movingAvg)

// Syncronous fetching (just one stock here)

现在,获取数据的代码与上一章使用的相同,第三章,金融数学与数值分析

let fetchOne() =
    let uri = new System.Uri("http://ichart.finance.yahoo.com/table.csv?s=ORCL&d=9&e=23&f=2012&g=d&a=2&b=13&c=1986&ignore=.csv")
    let client = new WebClient()
    let html = client.DownloadString(uri)
    html

// Parse CSV
let getPrices() =
    let data = fetchOne()
    data.Split('\n')
    |> Seq.skip 1
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun s -> float s.[4])
    |> Seq.truncate 2500

这里有趣的部分是如何将数据添加到图表中。这是通过遍历时间序列并使用 series.Points.Add 方法完成的。这是一种优雅简洁的方式。ignore 操作符被用来简单地跳过 Points.Add 方法的结果,忽略它。

// Calc moving average
let movingAverage n (prices:seq<float>) =
    prices    
    |> Seq.windowed n
    |> Seq.map Array.sum
    |> Seq.map (fun a -> a / float n)    

// The plotting
let sp = getPrices()
do sp |> Seq.iter (stockPrice.Points.Add >> ignore)

let ma = movingAverage 100 sp
do ma |> Seq.iter (movingAvg.Points.Add >> ignore)

扩展应用程序以使用布林带

我们现在将扩展上一节中使用的应用程序,以使用布林带。布林带是移动平均的扩展,加入了两个带——一个上轨带和一个下轨带。带的值通常是 K 倍(其中 K=2.0)的移动标准差,高于和低于移动平均线。我们需要添加一个函数来计算移动标准差。我们可以使用上一章中的标准差,并将其与 Seq.windowed 函数一起使用,代码如下所示。在此示例中,我们还添加了图例,以指定哪个数据系列对应于哪个颜色。截图如下:

扩展应用程序以使用布林带

扩展了布林带功能的示例应用程序

代码看起来与前面示例中的代码几乎相同,除了上轨和下轨的计算,以及移动标准差的部分:

/// Another example with Bollinger Bands
#r "System.Windows.Forms.DataVisualization.dll" 

open System
open System.Net
open System.Windows.Forms
open System.Windows.Forms.DataVisualization.Charting
open Microsoft.FSharp.Control.WebExtensions
// Create chart and form
let chart = new Chart(Dock = DockStyle.Fill)
let area = new ChartArea("Main")
chart.ChartAreas.Add(area)

可以使用 chart.Legends.Add 很容易地将图例添加到图表中:

// Add legends
chart.Legends.Add(new Legend())

let mainForm = new Form(Visible = true, TopMost = true, 
                        Width = 700, Height = 500)

do mainForm.Text <- "Yahoo Finance data in F# - Bollinger Bands"
mainForm.Controls.Add(chart)

// Create series for stock price
let stockPrice = new Series("stockPrice")
do stockPrice.ChartType <- SeriesChartType.Line
do stockPrice.BorderWidth <- 2
do stockPrice.Color <- Drawing.Color.DarkGray
chart.Series.Add(stockPrice)

// Create series for moving average
let movingAvg = new Series("movingAvg")
do movingAvg.ChartType <- SeriesChartType.Line
do movingAvg.BorderWidth <- 2
do movingAvg.Color <- Drawing.Color.Blue
chart.Series.Add(movingAvg)

我们将需要两个新的数据系列,分别用于上轨带和下轨带:

// Create series for upper band
let upperBand = new Series("upperBand")
do upperBand.ChartType <- SeriesChartType.Line
do upperBand.BorderWidth <- 2
do upperBand.Color <- Drawing.Color.Red
chart.Series.Add(upperBand)

// Create series for lower band
let lowerBand = new Series("lowerBand")
do lowerBand.ChartType <- SeriesChartType.Line
do lowerBand.BorderWidth <- 2
do lowerBand.Color <- Drawing.Color.Green
chart.Series.Add(lowerBand)
// Syncronous fetching (just one stock here)
let fetchOne() =
    let uri = new System.Uri("http://ichart.finance.yahoo.com/table.csv?s=ORCL&d=9&e=23&f=2012&g=d&a=2&b=13&c=1986&ignore=.csv")
    let client = new WebClient()
    let html = client.DownloadString(uri)
    html

// Parse CSV
let getPrices() =
    let data = fetchOne()
    data.Split('\n')
    |> Seq.skip 1
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun s -> float s.[4])
    |> Seq.truncate 2500

// Calc moving average
let movingAverage n (prices:seq<float>) =
    prices    
    |> Seq.windowed n
    |> Seq.map Array.sum
    |> Seq.map (fun a -> a / float n)

计算移动标准差的代码是对上一章中使用的代码的修改,以适应 Seq.windowed 函数:

// Stddev
let stddev2(values:seq<float>) =
    let avg = Seq.average values
    values    
    |> Seq.fold (fun acc x -> acc + (1.0 / float (Seq.length values)) * (x - avg) ** 2.0) 0.0
    |> sqrt

let movingStdDev n (prices:seq<float>) =
    prices
    |> Seq.windowed n
    |> Seq.map stddev2

// The plotting
let sp = getPrices()
do sp |> Seq.iter (stockPrice.Points.Add >> ignore)
let ma = movingAverage 100 sp
do ma |> Seq.iter (movingAvg.Points.Add >> ignore)

本节内容相当有趣。在这里,我们将从移动标准差中加上或减去结果,并将其与移动平均相乘,从而形成上轨和下轨带:

// Bollinger bands, K = 2.0
let ub = movingStdDev 100 sp
// Upper
Seq.zip ub ma |> Seq.map (fun (a,b) -> b + 2.0 * a) |> Seq.iter (upperBand.Points.Add >> ignore)
// Lower
Seq.zip ub ma |> Seq.map (fun (a,b) -> b - 2.0 * a) |> Seq.iter (lowerBand.Points.Add >> ignore)

如果你愿意,可以扩展这个应用程序并实现其他技术指标。使用 F# Interactive 的好处在于,应用程序本身不需要重新启动即可显示新数据。换句话说,你可以使用movingAvg.Points.Add,并将数据点添加到图表中。

使用 FSharp.Charting

FsChart 是一个常用的 F# 图表库,作为 Microsoft Chart 控件的功能性包装器实现。这个控件可以帮你省去一些工作,因为不像前面使用 Microsoft Chart 控件的示例那样需要编写样板代码。FsChart 也被设计为与 F# 一起使用,并能更好地与 F# Interactive 集成。

可以通过在包管理器控制台输入以下命令来安装该库:

Install-Package FSharp.Charting

从股票价格创建蜡烛图

让我们来看一下用于显示与之前相同的股票(Oracle)的 K 线图的代码,数据来自 Yahoo! Finance。这次,设置图表所需的样板代码减少了。程序的主要部分包括下载、解析和转换数据:

open System
open System.Net
open FSharp.Charting
open Microsoft.FSharp.Control.WebExtensions
open System.Windows.Forms.DataVisualization.Charting

要使用FSharpCharting,首先需要按如下方式设置图表:

module FSharpCharting = 
    fsi.AddPrinter(fun (ch:FSharp.Charting.ChartTypes.GenericChart) ->ch.ShowChart(); "FSharpCharting")

// Syncronous fetching (just one stock here)
let fetchOne() =
    let uri = new System.Uri("http://ichart.finance.yahoo.com/table.csv?s=ORCL&d=9&e=23&f=2012&g=d&a=2&b=13&c=2012&ignore=.csv")
    let client = new WebClient()
    let html = client.DownloadString(uri)
    html

我们需要将数据从开盘、最高、最低、收盘的顺序重新排序为最高、最低、开盘、收盘的顺序。这在我们将字符串解析为浮点数时完成。此外,我们将日期作为第一个值包括进去。FSharpCharts将使用日期来排序蜡烛图。

// Parse CSV and re-arrange O,H,L,C - > H,L,O,C
let getOHLCPrices() =
    let data = fetchOne()
    data.Split('\n')
    |> Seq.skip 1
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun s -> s.[0], float s.[2], float s.[3], float s.[1], float s.[4])
    |> Seq.truncate 50

// Candlestick chart price range specified
let ohlcPrices = getOHLCPrices() |> Seq.toList
Chart.Candlestick(ohlcPrices).WithYAxis(Max = 34.0, Min = 30.0)

数据将被下载、解析,并显示在图表中,最终的结果将类似于以下截图:

从股票价格创建 K 线图

使用 FSharpCharts 显示 K 线图

创建条形图

在这个例子中,我们将学习如何绘制由 Math.NET 生成的分布的直方图。直方图用于可视化统计数据,并帮助我们了解其特性。我们将使用一个简单的正态分布,均值为零,标准差为一。

open System
open MathNet.Numerics
open MathNet.Numerics.Distributions
open MathNet.Numerics.Statistics
open FSharp.Charting

module FSharpCharting2 = fsi.AddPrinter(fun ch:FSharp.Charting.ChartTypes.GenericChart) -> ch.ShowChart(); "FSharpCharting")

接下来,我们将创建将在直方图中使用的正态分布:

let dist = new Normal(0.0, 1.0)
let samples = dist.Samples() |> Seq.take 10000 |> Seq.toList
let histogram = new Histogram(samples, 35);

不幸的是,Math.NET 和 FSharpCharting 并不直接兼容。我们需要将 Math.NET 的直方图转换为适用于Chart.Column函数:

let getValues =
    let bucketWidth = Math.Abs(histogram.LowerBound - histogram.UpperBound) / (float histogram.BucketCount)
    [0..(histogram.BucketCount-1)]
    |> Seq.map (fun i -> (histogram.Item(i).LowerBound + histogram.Item(i).UpperBound)/2.0, histogram.Item(i).Count)

Chart.Column getValues

如下截图所示,分布看起来非常像正态分布。你可以自己尝试更改桶的数量,看看随着桶数的变化这种行为如何变化。此外,你还可以增加使用的样本数量。

创建条形图

使用 FSharpCharts 显示直方图

总结

在本章中,我们探讨了 F#中的数据可视化,并学习了如何构建用户界面。我们已经学习了如何使用 F#在没有可视化设计器的情况下创建用户界面。当然,这种方法有利有弊。主要的好处是完全控制,并且没有隐藏的魔法。另一方面,当谈到大型 GUI 应用时,这可能会非常耗时。

在下一章中,我们将使用本章介绍的数据可视化工具来研究期权的一些有趣属性。我们将讨论期权的基础知识,以及如何使用 Black-Scholes 公式来定价期权。此外,Black-Scholes 公式将在 F#中实现,并将详细讨论。

第五章:学习期权定价

在本章中,你将学习如何使用布莱克-斯科尔斯公式和蒙特卡罗方法进行期权定价。我们将比较这两种方法,并看看它们在实际应用中最适合的场景。

在本章中,你将学习:

  • 布莱克-斯科尔斯期权定价公式

  • 如何使用蒙特卡罗方法定价期权

  • 欧洲期权、美式期权和另类期权

  • 如何使用来自 Yahoo! Finance 的真实市场数据进行期权定价

  • 在 F#中绘制希腊字母

  • 维纳过程和布朗运动的基础知识

  • 随机微分方程的基础知识

期权简介

期权有两种变体,认沽期权和认购期权。认购期权赋予期权持有者在执行价格下购买标的资产的权利,但没有义务。认沽期权赋予合同持有者卖出标的资产的权利,但没有义务。布莱克-斯科尔斯公式描述的是只能在到期日行使的欧洲期权,与例如美式期权不同。期权买方为此支付一定的溢价,以覆盖来自对方风险。期权已经变得非常流行,全球主要交易所都在使用,涵盖了大部分资产类别。

期权背后的理论可能很快变得复杂。在本章中,我们将讨论期权的基础知识,以及如何通过 F#编写的代码来探索它们。

查看合同规范

期权有多种变体,其中一些将在本节中简要介绍。期权的合同规范也将取决于其类型。通常,有一些属性对于所有期权来说或多或少都是通用的。通用规格如下:

  • 侧面

  • 数量

  • 行使价格

  • 到期日

  • 结算条款

合同规范或已知变量在我们评估期权时使用。

欧洲期权

欧洲期权是其他类型期权的基础形式,美式期权和另类期权就是其中的例子。本章将重点讨论欧洲期权。

美式期权

美式期权是在到期日或之前的任何交易日都可以行使的期权。

另类期权

另类期权属于期权的广义范畴,可能包括复杂的金融结构,也可能是其他工具的组合。

学习维纳过程

维纳过程与随机微分方程和波动性密切相关。维纳过程或几何布朗运动定义如下:

学习维纳过程

上述公式描述了带有漂移项µ和波动性σ的股票价格或标的资产的变化,以及维纳过程Wt。这个过程用于在布莱克-斯科尔斯模型中模拟价格变化。

我们将使用布朗运动或维纳过程来模拟市场数据,布朗运动或维纳过程在 F#中实现为序列。序列可以是无限的,只有使用的值会被评估,这非常适合我们的需求。我们将实现一个生成器函数,将维纳过程作为序列生成,如下所示:

// A normally distributed random generator
let normd = new Normal(0.0, 1.0)
let T = 1.0
let N = 500.0
let dt:float = T / N

/// Sequences represent infinite number of elements
// p -> probability mean
// s -> scaling factor
let W s = let rec loop x = seq { yield x; yield! loop (x + sqrt(dt)*normd.Sample()*s)}
  loop s;;

在这里,我们使用normd.Sample()中的随机函数。在查看实现之前,先解释一下参数和布朗运动背后的理论。参数T是用于创建离散时间增量dt的时间。请注意,dt假设有 500 个N:s,序列中有 500 个项目;当然,这并不总是如此,但在这里这样做是足够的。接下来,我们使用递归来创建序列,其中我们将增量加到前一个值(x+...),其中 x 对应于xt-1

我们可以轻松生成任意长度的序列,如下所示:

> Seq.take 50 (W 55.00);;
val it : seq<float> = seq [55.0; 56.72907873; 56.96071054; 58.72850048; ...]

在这里,我们创建了一个长度为50的序列。让我们绘制这个序列,以便更好地理解该过程,如下图所示:

学习维纳过程

从前面的序列生成器中生成的维纳过程

接下来,我们将查看以下代码,以生成前面截图中显示的图表:

open System
open System.Net
open System.Windows.Forms
open System.Windows.Forms.DataVisualization.Charting
open Microsoft.FSharp.Control.WebExtensions
open MathNet.Numerics.Distributions;

// A normally distributed random generator
let normd = new Normal(0.0, 1.0)

// Create chart and form
let chart = new Chart(Dock = DockStyle.Fill)
let area = new ChartArea("Main")
chart.ChartAreas.Add(area)

let mainForm = new Form(Visible = true, TopMost = true, Width = 700, Height = 500)
do mainForm.Text <- "Wiener process in F#"
mainForm.Controls.Add(chart)

// Create series for stock price
let wienerProcess = new Series("process")
do wienerProcess.ChartType <- SeriesChartType.Line
do wienerProcess.BorderWidth <- 2
do wienerProcess.Color <- Drawing.Color.Red
chart.Series.Add(wienerProcess)

let random = new System.Random()
let rnd() = random.NextDouble()
let T = 1.0
let N = 500.0
let dt:float = T / N

/// Sequences represent infinite number of elements
let W s = let rec loop x = seq { yield x; yield! loop (x + sqrt(dt)*normd.Sample()*s)}
  loop s;;

do (Seq.take 100 (W 55.00)) |> Seq.iter (wienerProcess.Points.Add >> ignore)

到这一阶段,代码的大部分对你来说应该是熟悉的,但有趣的部分是最后一行,我们可以简单地将序列中选定数量的元素输入到Seq.iter中,它将优雅且高效地绘制这些值。

学习布莱克-斯科尔斯公式

布莱克-斯科尔斯公式是由费舍尔·布莱克迈伦·斯科尔斯在 1970 年代开发的。布莱克-斯科尔斯公式是一个估算期权价格的随机偏微分方程。公式背后的主要思想是 delta 中性组合。他们创建了理论上的 delta 中性组合以减少其中的不确定性。

这是一个必要的步骤,以便能够得出分析公式,我们将在本节中讨论。以下是布莱克-斯科尔斯公式下所做的假设:

  • 没有套利

  • 可以以恒定的无风险利率借钱(在持有期权期间)

  • 可以购买、出售和列出基础资产的分数部分

  • 没有交易成本

  • 基础资产的价格遵循布朗运动,具有恒定的漂移和波动率

  • 从基础证券中不支付股息

两种变体中最简单的是call期权的变体。首先,使用累积分布函数将股价按d1作为参数进行缩放。然后,股价通过d2的累积分布函数缩放后的折扣行使价格来减少。换句话说,这是使用各自的概率缩放并折扣行使价格后的股价与行使价格的差值:

学习布莱克-斯科尔斯公式

put期权的公式稍微复杂一些,但遵循相同的原则,如下所示:

学习 Black-Scholes 公式

Black-Scholes 公式通常分为几个部分,其中d1d2是概率因子,用来描述股票价格与行使价格的关系概率:

学习 Black-Scholes 公式

前述公式中使用的参数总结如下:

  • N:累计分布函数

  • T:到期时间,以年为单位表示

  • S:股票价格或其他标的资产的价格

  • K:行使价格

  • r:无风险利率

  • σ:标的资产的波动率

在 F#中实现 Black-Scholes 模型

现在我们已经了解了 Black-Scholes 公式的基本原理及相关参数,接下来可以自己实现它。这里实现了累计分布函数,避免了依赖外部库,同时也说明了自己实现这个函数其实是很简单的。Black-Scholes 公式在 F#中的实现如下所示。它接受六个参数,第一个是一个用于判断是call还是put期权的标志。常量a1a5是用于数值实现的泰勒级数系数:

let pow x n = exp(n * log(x))

type PutCallFlag = Put | Call

/// Cumulative distribution function
let cnd x =
  let a1 =  0.31938153
  let a2 = -0.356563782
  let a3 =  1.781477937
  let a4 = -1.821255978
  let a5 =  1.330274429
  let pi = 3.141592654
  let l  = abs(x)
  let k  = 1.0 / (1.0 + 0.2316419 * l)
  let w  = (1.0-1.0/sqrt(2.0*pi)*exp(-l*l/2.0)*(a1*k+a2*k*k+a3*(pow k 3.0)+a4*(pow k 4.0)+a5*(pow k 5.0)))
  if x < 0.0 then 1.0 - w else w

/// Black-Scholes
// call_put_flag: Put | Call
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes call_put_flag s x t r v =
  let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))
  let d2=d1-v*sqrt(t)
  //let res = ref 0.0

  match call_put_flag with
  | Put -> x*exp(-r*t)*cnd(-d2)-s*cnd(-d1)
  | Call -> s*cnd(d1)-x*exp(-r*t)*cnd(d2)    

让我们使用black_scholes函数,并为callput期权提供一些数字。假设我们想知道一个期权的价格,该期权的标的资产是一只以 58.60 美元交易的股票,年波动率为 30%。无风险利率为 1%。我们可以使用之前定义的公式,根据 Black-Scholes 公式计算一个到期时间为六个月(0.5 年)的call期权的理论价格:

> black_scholes Call 58.60 60.0 0.5 0.01 0.3;;
val it : float = 4.465202269

我们只需通过更改函数的标志,即可得到put期权的值:

> black_scholes Put  58.60 60.0 0.5 0.01 0.3;;
val it : float = 5.565951021

有时,用天数而非年数来表示到期时间更加方便。我们可以为此目的引入一个辅助函数:

/// Convert the nr of days to years
let days_to_years d = (float d) / 365.25

请注意数字365.25,它包括了闰年的因素。虽然在我们的示例中不需要使用,但为了准确性,还是用了这个值。当我们知道以天为单位的时间时,现在可以使用这个函数:

> days_to_years 30;;
val it : float = 0.08213552361

我们使用之前的示例,但现在设定到期日为20天:

> black_scholes Call 58.60 60.0 (days_to_years 20) 0.01 0.3;;
val it : float = 1.065115482

> black_scholes Put 58.60 60.0 (days_to_years 20) 0.01 0.3;;
val it : float = 2.432270266

使用 Black-Scholes 和图表一起

有时,能够绘制期权的价格直到到期是很有用的。我们可以使用之前定义的函数,改变剩余时间并绘制出相应的值。在这个示例中,我们将创建一个程序,输出下方截图中的图表:

将 Black-Scholes 与图表一起使用

显示看涨期权和看跌期权价格随时间变化的图表

以下代码用于生成前述截图中的图表:

/// Plot price of option as function of time left to maturity
#r "System.Windows.Forms.DataVisualization.dll"

open System
open System.Net
open System.Windows.Forms
open System.Windows.Forms.DataVisualization.Charting
open Microsoft.FSharp.Control.WebExtensions

/// Create chart and form
let chart = new Chart(Dock = DockStyle.Fill)
let area = new ChartArea("Main")
chart.ChartAreas.Add(area)
chart.Legends.Add(new Legend())

let mainForm = new Form(Visible = true, TopMost = true, 
                        Width = 700, Height = 500)
do mainForm.Text <- "Option price as a function of time"
mainForm.Controls.Add(chart)

/// Create series for call option price
let optionPriceCall = new Series("Call option price")
do optionPriceCall.ChartType <- SeriesChartType.Line
do optionPriceCall.BorderWidth <- 2
do optionPriceCall.Color <- Drawing.Color.Red
chart.Series.Add(optionPriceCall)

/// Create series for put option price
let optionPricePut = new Series("Put option price")
do optionPricePut.ChartType <- SeriesChartType.Line
do optionPricePut.BorderWidth <- 2
do optionPricePut.Color <- Drawing.Color.Blue
chart.Series.Add(optionPricePut)

/// Calculate and plot call option prices
let opc = [for x in [(days_to_years 20)..(-(days_to_years 1))..0.0] do yield black_scholes Call 58.60 60.0 x 0.01 0.3]
do opc |> Seq.iter (optionPriceCall.Points.Add >> ignore)

/// Calculate and plot put option prices
let opp = [for x in [(days_to_years 20)..(-(days_to_years 1))..0.0] do yield black_scholes Put 58.60 60.0 x 0.01 0.3]
do opp |> Seq.iter (optionPricePut.Points.Add >> ignore)

上述代码只是前一章代码的修改版,新增了期权部分。此图表中有两条序列,一条代表看涨期权,另一条代表看跌期权。我们还为每个序列添加了图例。最后一部分是计算价格和实际绘图。列表推导用于简洁的代码,Black-Scholes 公式在每个到期日之前被调用,天数每步递减一天。

作为读者,你可以修改代码,绘制期权的不同方面,例如期权价格相对于基础股价上升的变化等等。

介绍希腊字母

希腊字母是对 Black-Scholes 公式中某一特定参数(如时间、利率、波动性或股价)的偏导数。希腊字母可以根据导数的阶数分为两类或更多类。在本节中,我们将讨论一阶和二阶希腊字母。

一阶希腊字母

在本节中,我们将通过下表展示一阶希腊字母:

名称 符号 描述
Delta Δ 期权价值相对于基础资产价格变化的变化率。
Vega ν 期权价值相对于基础资产波动性变化的变化率,称为波动率敏感度。
Theta Θ 期权价值相对于时间的变化率。随着时间的流逝,对时间的敏感度会衰减,这种现象被称为时间衰减。
Rho ρ 期权价值相对于利率变化的变化率。

二阶希腊字母

在本节中,我们将通过下表展示二阶希腊字母:

名称 符号 描述
Gamma Γ Delta 相对于基础资产价格变化的变化率。
Veta - Vega 关于时间的变化率。
Vera - Rho 相对于波动性的变化率。

提示

为了清晰起见,省略了一些二阶希腊字母;本书中将不会涉及这些内容。

在 F# 中实现希腊字母

让我们来实现希腊字母:Delta、Gamma、Vega、Theta 和 Rho。首先,我们看一下每个希腊字母的公式。在某些情况下,期权的看涨和看跌分别有不同的公式,具体如下面的表格所示:

在 F# 中实现希腊字母

我们需要累积分布函数的导数,实际上它是一个均值为 0、标准差为 1 的正态分布:

/// Normal distribution
open MathNet.Numerics.Distributions;

let normd = new Normal(0.0, 1.0)

Delta

Delta 是期权价格相对于基础资产价格变化的变化率:

/// Black-Scholes Delta
// call_put_flag: Put | Call
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes_delta call_put_flag s x t r v =
  let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))
  match call_put_flag with
  | Put -> cnd(d1) - 1.0
  | Call -> cnd(d1) 

Gamma

Gamma 是德尔塔相对于标的资产价格变化的变化率。这是标的资产价格的二阶导数。它衡量期权价格相对于标的资产价格的加速度:

/// Black-Scholes Gamma
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes_gamma s x t r v =
  let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))
  normd.Density(d1) / (s*v*sqrt(t)

Vega

Vega 是期权价值相对于标的资产波动率变化的变化率。它被称为波动率的敏感性:

/// Black-Scholes Vega
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes_vega s x t r v =
  let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))    
  s*normd.Density(d1)*sqrt(t)

Theta

Theta 是期权价值相对于时间的变化率。随着时间的推移,时间敏感性会衰减,这种现象被称为时间衰减:

/// Black-Scholes Theta
// call_put_flag: Put | Call
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes_theta call_put_flag s x t r v =
  let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))
  let d2=d1-v*sqrt(t)
  let res = ref 0.0
  match call_put_flag with
  | Put -> -(s*normd.Density(d1)*v)/(2.0*sqrt(t))+r*x*exp(-r*t)*cnd(-d2)
  | Call -> -(s*normd.Density(d1)*v)/(2.0*sqrt(t))-r*x*exp(-r*t)*cnd(d2) 

Rho

Rho 是期权价值相对于利率的变化率:

/// Black-Scholes Rho
// call_put_flag: Put | Call
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes_rho call_put_flag s x t r v =
  let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))
  let d2=d1-v*sqrt(t)
  let res = ref 0.0
  match call_put_flag with
  | Put -> -x*t*exp(-r*t)*cnd(-d2)
  | Call -> x*t*exp(-r*t)*cnd(d2) 

研究希腊字母的敏感性

现在我们已经实现了所有的希腊字母,我们将研究其中一些的敏感性,看看它们在标的股票价格变化时如何变化。

以下截图是一个包含四个希腊字母的表面图,其中时间和标的资产价格在变化。此图是在 MATLAB 中生成的,F# 中无法生成。我们将使用该图的二维版本来研究希腊字母,如下所示的截图:

研究希腊字母的敏感性

看涨期权的德尔塔、伽玛、θ和 rho 的表面图

在本节中,我们将从绘制一个call期权的德尔塔值开始,其中我们改变标的资产的价格。这样将产生如下所示的二维图:

.

研究希腊字母的敏感性

看涨期权德尔塔与标的资产价格的图示

上述截图中显示的结果将通过接下来展示的代码生成。我们将重用大部分代码,之前我们查看了看涨期权和看跌期权的价格。稍微修改过的版本将在下面的代码中呈现,其中标的资产的价格从 10.070.0:

/// Plot delta of call option as function of underlying price
#r "System.Windows.Forms.DataVisualization.dll"

open System
open System.Net
open System.Windows.Forms
open System.Windows.Forms.DataVisualization.Charting
open Microsoft.FSharp.Control.WebExtensions

/// Create chart and form
let chart = new Chart(Dock = DockStyle.Fill)
let area = new ChartArea("Main")
chart.ChartAreas.Add(area)
chart.Legends.Add(new Legend())
let mainForm = new Form(Visible = true, TopMost = true, Width = 700, Height = 500)
do mainForm.Text <- "Option delta as a function of underlying price"
mainForm.Controls.Add(chart)

/// Create series for call option delta
let optionDeltaCall = new Series("Call option delta")
do optionDeltaCall.ChartType <- SeriesChartType.Line
do optionDeltaCall.BorderWidth <- 2
do optionDeltaCall.Color <- Drawing.Color.Red
chart.Series.Add(optionDeltaCall)

/// Calculate and plot call delta
let opc = [for x in [10.0..1.0..70.0] do yield black_scholes_delta Call x 60.0 0.5 0.01 0.3]
do opc |> Seq.iter (optionDeltaCall.Points.Add >> ignore)

我们可以扩展代码来绘制所有四个希腊字母,如截图中所示的二维表面图。结果将是如下截图所示的图形:

研究希腊字母的敏感性

显示看涨期权的希腊字母与价格变化(x 轴)关系的图形

可视化四个希腊字母的代码列表

以下是用于生成前述图形的完整程序代码列表:

#r "System.Windows.Forms.DataVisualization.dll"

open System
open System.Net
open System.Windows.Forms
open System.Windows.Forms.DataVisualization.Charting
open Microsoft.FSharp.Control.WebExtensions

/// Create chart and form
let chart = new Chart(Dock = DockStyle.Fill)
let area = new ChartArea("Main")
chart.ChartAreas.Add(area)
chart.Legends.Add(new Legend())

let mainForm = new Form(Visible = true, TopMost = true, Width = 700, Height = 500)

do mainForm.Text <- "Option delta as a function of underlying price"
mainForm.Controls.Add(chart)

我们将为每个希腊字母创建一个系列,如下所示:

/// Create series for call option delta
let optionDeltaCall = new Series("Call option delta")
do optionDeltaCall.ChartType <- SeriesChartType.Line
do optionDeltaCall.BorderWidth <- 2
do optionDeltaCall.Color <- Drawing.Color.Red
chart.Series.Add(optionDeltaCall)

/// Create series for call option gamma
let optionGammaCall = new Series("Call option gamma")
do optionGammaCall.ChartType <- SeriesChartType.Line
do optionGammaCall.BorderWidth <- 2
do optionGammaCall.Color <- Drawing.Color.Blue
chart.Series.Add(optionGammaCall)

/// Create series for call option theta
let optionThetaCall = new Series("Call option theta")
do optionThetaCall.ChartType <- SeriesChartType.Line
do optionThetaCall.BorderWidth <- 2
do optionThetaCall.Color <- Drawing.Color.Green
chart.Series.Add(optionThetaCall)

/// Create series for call option vega
let optionVegaCall = new Series("Call option vega")
do optionVegaCall.ChartType <- SeriesChartType.Line
do optionVegaCall.BorderWidth <- 2
do optionVegaCall.Color <- Drawing.Color.Purple
chart.Series.Add(optionVegaCall)

接下来,我们将计算每个希腊字母的绘图值:

/// Calculate and plot call delta
let opd = [for x in [10.0..1.0..70.0] do yield black_scholes_delta Call x 60.0 0.5 0.01 0.3]
do opd |> Seq.iter (optionDeltaCall.Points.Add >> ignore)

/// Calculate and plot call gamma
let opg = [for x in [10.0..1.0..70.0] do yield black_scholes_gamma x 60.0 0.5 0.01 0.3]
do opg |> Seq.iter (optionGammaCall.Points.Add >> ignore)

/// Calculate and plot call theta
let opt = [for x in [10.0..1.0..70.0] do yield black_scholes_theta Call x 60.0 0.5 0.01 0.3]
do opt |> Seq.iter (optionThetaCall.Points.Add >> ignore)

/// Calculate and plot call vega
let opv = [for x in [10.0..1.0..70.0] do yield black_scholes_vega x 60.0 0.1 0.01 0.3]
do opv |> Seq.iter (optionVegaCall.Points.Add >> ignore)

蒙特卡洛方法

蒙特卡洛方法用于使用随机数进行数值积分,并研究大量样本的平均值。当没有封闭形式解时,蒙特卡洛方法特别有用。

在本节中,我们将研究最简单的情况,其中我们有路径依赖的欧洲期权。我们将使用随机漂移参数来进行数值积分采样。这将导致随机过程的各种平均值,构成了基础资产的运动。我们将分别使用 1,000 和 1,000,000 个样本进行计算,并比较结果。让我们深入研究以下代码:

/// Monte Carlo implementation

/// Convert the nr of days to years
let days_to_years d =
  (float d) / 365.25

/// Asset price at maturity for sample rnd
// s: stock price
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
// rnd: sample
let price_for_sample s t r v rnd =
  s*exp((r-v*v/2.0)*t+v*rnd*sqrt(t))

/// For each sample we run the monte carlo simulation
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
// samples: random samples as input to simulation
let monte_carlo s x t r v (samples:seq<float>) = samples
  |> Seq.map (fun rnd -> (price_for_sample s t r v rnd) - x)
  |> Seq.average

/// Generate sample sequence
let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 1000 -> rnd() * 1.0]

让我们用与put期权的 Black-Scholes 公式相同的数值来测试它:

  > black_scholes 'c' 58.60 60.0 0.5 0.01 0.3;;
  val it : float = 4.465202269

  /// Monte carlo for call option
  > monte_carlo 58.60 60.0 0.5 0.01 0.3 data
  val it : float = 4.243545757

这个结果接近准确;我们可以增加样本数,看看是否得到另一个值:

let random = new System.Random()
let rnd() = random.NextDouble()
let data = [for i in 1 .. 1000000 -> rnd() * 1.0]

/// Monte carlo for call option
> monte_carlo 58.60 60.0 0.5 0.01 0.3 data;;
val it : float = 4.146170039

上述代码使用以下公式来完成估算价格的任务。简而言之,这里的蒙特卡罗方法可以看作是随机选择漂移参数σ*rnd*sqrt(t)。所有这些生成的样本的平均值将代表期权到期时的估算值。实际上,蒙特卡罗方法并不用于欧洲期权,这与本节中所展示的情况相反。选择欧洲期权主要是出于简单性考虑,用以说明这些概念:

蒙特卡罗方法

用于估算资产到期时价格样本值的公式

总结

在本章中,我们使用著名的 Black-Scholes 公式结合蒙特卡罗方法研究了 F#中的期权定价。F#再次证明了其强大的功能,特别是在数值实现方面。这段代码几乎与数学公式完全相同,因此很容易实现,无需额外的繁琐步骤。本章中学到的知识将在下一章中用于深入探讨期权和波动性。

第六章:探索波动率

在本章中,您将学习波动率以及如何使用 F#中的数值方法来探索期权的特性。我们将使用上一章的代码,并结合第三章中讲解的数值方法,来求解布莱克-舒尔斯模型中的内在波动率,即隐含波动率,财务数学与数值分析

在本章中,您将学习:

  • 实际波动率和隐含波动率

  • 使用 F#计算实际波动率

  • 在布莱克-舒尔斯模型中求解隐含波动率

  • 使用数值方法分析期权

  • 德尔塔对冲

  • 简要介绍波动率套利

波动率介绍

在上一章,我们回顾了布莱克-舒尔斯模型的基础,适用于欧洲期权。在本章中,我们将继续探讨期权,并研究波动率以及如何使用 F#来帮助我们。波动率衡量的是价格变动的年化标准差,即金融工具价格波动的速率。较高的波动率意味着更大的分散,而较低的波动率则意味着更小的分散。波动率与方差相关,方差等于标准差的平方,正如之前所讲。

布莱克-舒尔斯模型假设股票价格的波动服从正态分布,但根据观察,现实中并非如此。实际情况中,分布更为厚尾,这意味着负向价格波动发生时往往较大,但正向波动更常见,且通常较小。

波动率介绍

图 1:由 Yahoo! Finance 提供。显示低波动率(9.5%年化)的标准普尔 500 指数和高波动率(31%年化)的苹果股票。

实际波动率

实际波动率是指在特定时间段内(通常为过去一个月或一年)观察到的波动率。实际波动率使用当前的市场价格和若干先前的观察数据。简而言之,它是当前和历史价格数据的对数收益率的标准差。

隐含波动率

隐含波动率是包含在期权价格中的波动率。如果我们使用布莱克-舒尔斯模型,我们需要提供几个输入参数:股票价格、行权价格、无风险利率、波动率和到期时间。根据这些输入,模型将输出一个理论价格,基于所做的假设。我们可以通过逆向使用布莱克-舒尔斯模型来获取隐含波动率。这意味着,如果期权在交易所以公平市场价格进行交易,我们可以从该期权的市场价格中提取波动率。这样做需要使用数值方法求解根值,这部分内容已经在数值分析章节中讲解过。

使用当前价格计算隐含波动性,将通过一个二分法求解器来解决 Black-Scholes 模型中的隐含波动性问题,我们将在本章的后续部分学习这一内容。

F#中的波动性探索

让我们来看一个 F#示例程序,它将涵盖波动性的一些方面,以及如何从真实市场数据中计算波动性。我们将查看来自 NASDAQ 的一些科技股,并计算它们的年化波动率。首先,我们需要定义一个函数来进行计算。年化波动率定义如下:

F#中的波动性探索

其中P是时间周期(以年为单位),F#中的波动性探索是年化波动率,F#中的波动性探索是时间周期P内的标准差。

这里我们使用P表示 F#中的波动性探索,这意味着我们可以将公式重写为:

F#中的波动性探索

我们首先使用计算标准差的函数,如第三章所述,金融数学与数值分析

/// Calculate the standard deviation
let stddev(values:seq<float>) =
    values
    |> Seq.fold (fun acc x -> acc + (1.0 / float (Seq.length values)) * (x - (Seq.average values)) ** 2.0) 0.0
    |> sqrt

然后我们需要一个函数来计算对数收益。这是使用Seq.pairwise函数来完成的,因为我们需要一个大小为 2 的窗口。这与使用Seq.windowed函数并设置大小为 2 是相同的。

/// Calculate logarithmic returns
let calcDailyReturns(prices:seq<float>) =
    prices
    |> Seq.pairwise
    |> Seq.map (fun (x, y) -> log (x / y))

最后但同样重要的是,我们有一个函数来从收益序列中计算年化波动率:

/// Annualized volatility
let annualVolatility(returns:seq<float>) =
    let sd = stddev(calcDailyReturns(returns))
    let days = Seq.length(returns)
    sd * sqrt(float days)

该函数使用之前描述的数学公式,将天数的平方与收益序列的标准差相乘。这可以解释为一个缩放因子。

这些函数是我们需要进行的主要构建块。下一步是重用这些功能以从 Yahoo! Finance 获取价格,稍作修改以使用前面的函数。接下来,我们介绍两个辅助函数。第一个是将数字格式化为字符串,如果数字小于十,则前面加上零。第二个函数是帮助我们构造需要从 Yahoo! Finance 请求数据的 URL:

let formatLeadingZero(number:int):String =
    String.Format("{0:00}", number)

/// Helper function to create the Yahoo-finance URL
let constructURL(symbol, fromDate:DateTime, toDate:DateTime) =
    let fm = formatLeadingZero(fromDate.Month-1)
    let fd = formatLeadingZero(fromDate.Day)
    let fy = formatLeadingZero(fromDate.Year)
    let tm = formatLeadingZero(toDate.Month-1)
    let td = formatLeadingZero(toDate.Day)
    let ty = formatLeadingZero(toDate.Year)
    "http://ichart.finance.yahoo.com/table.csv?s=" + symbol + "&d=" + tm + "&e=" + td + "&f=" + ty + "&g=d&a=" + fm + "&b=" + fd + "&c=" + fy + "&ignore=.csv"

/// Synchronous fetching (just one request)
let fetchOne symbol fromDate toDate =
    let url = constructURL(symbol, fromDate, toDate)
    let uri = new System.Uri(url)
    let client = new WebClient()
    let html = client.DownloadString(uri)
    html

/// Parse CSV
let getPrices stock fromDate toDate =
    let data = fetchOne stock fromDate toDate
    data.Trim().Split('\n')
    |> Seq.skip 1
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun s -> float s.[4])
    |> Seq.takeWhile (fun s -> s >= 0.0)

/// Returns a formatted string with volatility for a stock
let getAnnualizedVol stock fromStr toStr =
    let prices = getPrices stock (System.DateTime.Parse fromStr) (System.DateTime.Parse toStr)
    let vol = Math.Round(annualVolatility(prices) * 100.0, 2)
    sprintf "Volatility for %s is %.2f %%" stock vol 

让我们尝试使用 F# Interactive 对来自 NASDAQ 的几只股票进行操作:

> getAnnualizedVol "MSFT" "2013-01-01" "2013-08-29";;
val it : string = "Volatility for MSFT is 21.30 %"

> getAnnualizedVol "ORCL" "2013-01-01" "2013-08-29";;
val it : string = "Volatility for ORCL is 20.44 %"

> getAnnualizedVol "GOOG" "2013-01-01" "2013-08-29";;
val it : string = "Volatility for GOOG is 14.80 %"

> getAnnualizedVol "EBAY" "2013-01-01" "2013-08-29";;
val it : string = "Volatility for EBAY is 20.82 %"

> getAnnualizedVol "AAPL" "2013-01-01" "2013-08-29";;
val it : string = "Volatility for AAPL is 25.16 %"

> getAnnualizedVol "AMZN" "2013-01-01" "2013-08-29";;
val it : string = "Volatility for AMZN is 21.10 %"

> getAnnualizedVol "^GSPC" "2013-01-01" "2013-08-29";;
val it : string = "Volatility for ^GSPC is 9.15 %"

上述代码的结果可以总结在以下表格中:

股票代码 公司名称 年化波动率
MSFT 微软公司 21.30 百分比
ORCL 甲骨文公司 20.44 百分比
GOOG 谷歌公司 14.80 百分比
EBAY eBay 20.82 百分比
AAPL 苹果公司 25.16 百分比
AMZN 亚马逊 21.10 百分比
^GSPC 标准普尔 500 指数 9.15 百分比

从前面的表格中,我们可以看到并比较所选股票和 S&P 500 指数的年化波动性。从数据中可以清楚地看出,哪一只股票的尊重度最高,波动性最低。AAPL 和^GSPC 在本章介绍的图 1中进行了比较。有时,波动性能告诉你某个工具的投资风险。但请记住,这些数据是历史数据,无法解读该工具未来的价格变动。

完整应用程序

以下是前面程序的完整代码清单。你可以修改参数,以便连接 Yahoo! Finance 的 Web 服务,返回 CSV 数据。这些参数包括 a、b、c 作为from-date参数,以及 d、e、f 作为to-date参数,外加股票的符号,见下表:

参数 描述 示例
s 股票符号 MSFT
d 到月份 07
e 到日 29
f 到年份 2013
a 从月份 00
b 到日 1
c 到年份 2013

让我们看一个示例,其中我们从 Yahoo!下载了几只在 NASDAQ 上市的股票以及 S&P500 指数的数据。我们将查看 2013 年 1 月 1 日到 2013 年 8 月 2 日的时间跨度:

open System
open System.Net

/// Calculate the standard deviation
let stddev(values:seq<float>) =
    values
    |> Seq.fold (fun acc x -> acc + (1.0 / float (Seq.length values)) * (x - (Seq.average values)) ** 2.0) 0.0
    |> sqrt

/// Calculate logarithmic returns
let calcDailyReturns(prices:seq<float>) =
    prices
    |> Seq.pairwise
    |> Seq.map (fun (x, y) -> log (x / y))

/// Annualized volatility
let annualVolatility(returns:seq<float>) =
    let sd = stddev(calcDailyReturns(returns))
    let days = Seq.length(returns)
    sd * sqrt(float days)

let formatLeadingZero(number:int):String =
    String.Format("{0:00}", number)

/// Helper function to create the Yahoo-finance URL
let constructURL(symbol, fromDate:DateTime, toDate:DateTime) =
    let fm = formatLeadingZero(fromDate.Month-1)
    let fd = formatLeadingZero(fromDate.Day)
    let fy = formatLeadingZero(fromDate.Year)
    let tm = formatLeadingZero(toDate.Month-1)
    let td = formatLeadingZero(toDate.Day)
    let ty = formatLeadingZero(toDate.Year)
    "http://ichart.finance.yahoo.com/table.csv?s=" + symbol + "&d=" + tm + "&e=" + td + "&f=" + ty + "&g=d&a=" + fm + "&b=" + fd + "&c=" + fy + "&ignore=.csv"

/// Synchronous fetching (just one request)
let fetchOne symbol fromDate toDate =
    let url = constructURL(symbol, fromDate, toDate)
    let uri = new System.Uri(url)
    let client = new WebClient()
    let html = client.DownloadString(uri)
    html

/// Parse CSV
let getPrices stock fromDate toDate =
    let data = fetchOne stock fromDate toDate
    data.Trim().Split('\n')
    |> Seq.skip 1
    |> Seq.map (fun s -> s.Split(','))
    |> Seq.map (fun s -> float s.[4])
    |> Seq.takeWhile (fun s -> s >= 0.0)

/// Returns a formatted string with volatility for a stock
let getAnnualizedVol stock fromStr toStr =
    let prices = getPrices stock (System.DateTime.Parse fromStr) (System.DateTime.Parse toStr)
    let vol = Math.Round(annualVolatility(prices) * 100.0, 2)
    sprintf "Volatility for %s is %.2f %%" stock vol

getAnnualizedVol "MSFT" "2013-01-01" "2013-08-29"
// val it : string = "Volatility for MSFT is 21.30 %"

getAnnualizedVol "ORCL" "2013-01-01" "2013-08-29"
// val it : string = "Volatility for ORCL is 20.44 %"

getAnnualizedVol "GOOG" "2013-01-01" "2013-08-29"
// val it : string = "Volatility for GOOG is 14.80 %"

getAnnualizedVol "EBAY" "2013-01-01" "2013-08-29"
// val it : string = "Volatility for EBAY is 20.82 %"

getAnnualizedVol "AAPL" "2013-01-01" "2013-08-29"
// val it : string = "Volatility for AAPL is 25.16 %"

getAnnualizedVol "AMZN" "2013-01-01" "2013-08-29"
// val it : string = "Volatility for AMZN is 21.10 %"

getAnnualizedVol "^GSPC" "2013-01-01" "2013-08-29"
// val it : string = "Volatility for ^GSPC is 9.15 %"

在本节中,我们使用来自 Yahoo!财经的数据查看了一些工具的实际波动性。在下一节中,我们将讨论隐含波动性以及如何从 Black-Scholes 公式中提取这些信息。

学习隐含波动性

在这里,我们将使用第三章中介绍的二分法,金融数学与数值分析。这是一种寻找根的方法。隐含波动性是根,函数值为零,适用于 Black-Scholes 公式的不同输入参数。标的工具的波动性是 Black-Scholes 的输入,给出的期权当前价格与实际市场价格相同。

Vega告诉我们期权价格对标的资产波动性变化的敏感度。查看 Yahoo! Finance 并找到期权数据。将这些期权数据带入以下求解函数:

学习隐含波动性

图 2:2000 年 1 月 1 日到 2013 年 11 月 1 日 S&P500 指数期权的 VIX 指数

如前面的截图所示,VIX 指数是一个结合了 S&P 500 指数期权隐含波动性的指数。它可以被解读为未来波动性的指示。

求解隐含波动性

接下来,我们将使用一种方法来求解欧洲期权的隐含波动性。这可以通过使用二分法数值解法来求解根。

为了理解为什么我们使用二分法求解 Black-Scholes 方程的根值,我们需要一些工具。首先,我们重新回顾一下看涨期权和看跌期权价格作为估算波动率和一组参数(记作)函数的定义:

求解隐含波动率

为了提取隐含波动率,我们需要 Black-Scholes 公式的反函数。不幸的是,这个函数没有解析的反函数。相反,我们可以说,Black-Scholes 公式减去当前期权市场价格的隐含波动率,在此情况下的看涨期权价格为零。以下是求解隐含波动率,这是本节中研究的看涨期权的当前市场价格:

求解隐含波动率

这使我们能够使用数值根求解器来找到隐含波动率。以下是 F#中二分法求解器的实现。我们还将使用先前的 Black-Scholes 实现。

/// Solve for implied volatility

let pow x n = exp (n * log(x) )

type PutCallFlag = Put | Call

/// Cumulative distribution function
let cnd x =
    let a1 =  0.31938153
    let a2 = -0.356563782
    let a3 =  1.781477937
    let a4 = -1.821255978
    let a5 =  1.330274429
    let pi = 3.141592654
    let l  = abs(x)
    let k  = 1.0 / (1.0 + 0.2316419 * l)
    let w  = (1.0-1.0/sqrt(2.0*pi)*exp(-l*l/2.0)*(a1*k+a2*k*k+a3*(pow k 3.0)+a4*(pow k 4.0)+a5*(pow k 5.0)))
    if x < 0.0 then 1.0 - w else w

/// Black-Scholes
// call_put_flag: Call | Put
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes call_put_flag s x t r v =
    let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))
    let d2=d1-v*sqrt(t)
    //let res = ref 0.0

    match call_put_flag with
    | Put -> x*exp(-r*t)*cnd(-d2)-s*cnd(-d1)
    | Call -> s*cnd(d1)-x*exp(-r*t)*cnd(d2)    
/// Bisection solver
let rec bisect n N (f:float -> float) (a:float) (b:float) (t:float) : float =
    if n >= N then -1.0
    else
        let c = (a + b) / 2.0
        if f(c) = 0.0 || (b - a) / 2.0 < t then
            // Solution found
            c
        else
            if sign(f(c)) = sign(f(a)) then
                bisect (n + 1) N f c b t
            else    
                bisect (n + 1) N f a c t

Let's use it!

/// Calculate implied volatility for an option
bisect 0 25 (fun x -> (black_scholes Call 58.60 60.0 0.05475 0.0095 x) - 1.2753) 0.0 1.0 0.001

运行前面的代码将得到隐含波动率为 0.3408203125,约为 34.1%的波动率。请注意,我们还必须减去当前期权的市场价格(1.2753),因为我们是在求解根值。最后三个输入是起始值和停止值,0.01.0分别表示波动率的 0%和 100%。步长设置为 0.001,即 0.1%。测试这些值是否正确的一个简单方法是,首先使用实际波动率通过 Black-Scholes 公式计算期权的理论价格。

假设我们有一个执行价为 75.00 美元、股票价格为 73.00 美元、到期时间为 20 天(约 0.05475 年)、波动率为 15%、固定利率为 0.01%的看涨期权,这将导致期权价格为:

let option_price = black_scholes Call 73.00 75.0 (days_to_years 20) 0.01 0.15

我们现在可以使用这个价格来查看二分法是否有效,并求解隐含波动率。在这种情况下,我们可以预期隐含波动率将与我们输入到 Black-Scholes 公式中的波动率完全相同;即 15%:

> bisect 0 25 (fun x -> (black_scholes Call 73.00 75.0 (days_to_years 20) 0.01 x) - option_price) 0.0 1.0 0.001;;
val it : float = 0.1494140625

使用 Black-Scholes 进行 Delta 对冲

一个 Delta 中性组合是由期权和基础工具构成的。理论上,该组合对基础价格的小幅变动免疫。在谈到 Delta 对冲时,衍生品的对冲比率用于定义每个期权所需的基础价格量。Delta 对冲是一种交易策略,通过维持 Delta 中性组合来应对基础价格的小幅变化。

简单来说,我们来看一下如何在实践中操作。假设我们有 N 个衍生品。这需要进行对冲以防止价格波动。然后,我们需要购买基础股票以创建对冲。整个过程可以分为三个步骤:

  1. N 个衍生品需要进行 Delta 对冲

  2. 购买基础股票以保护衍生品

  3. 定期重新平衡对冲头寸

为了确定我们需要多少股票,我们使用期权的 delta(Δ)。这告诉我们标的资产价格变化时,期权价格变化的幅度。投资组合使用 使用布莱克-斯科尔斯的 delta 对冲 股票来实现 delta 中性;通常每个期权合约对应 100 股股票。

标的股票的价格不断波动,这导致期权价格的变化。这意味着我们还必须重新平衡我们的投资组合。这是由于期权的时间价值和标的价格的变化。

让我们使用 F# 来计算为某个特定期权进行 delta 对冲所需的股票数量:

/// Assumes 100 shares of stock per option
let nr_of_stocks_delta_hedge N =
    (black_scholes_delta 'c' 58.0 60.0 0.5 0.01 0.3) * 100.0 * (float N)

/// Calculate nr of shares needed to cover 100 call options
nr_of_stocks_delta_hedge 100

如果我们评估最后一行,我们将得到创建 delta 中性对冲所需的股票数量,使用 100 个看涨期权。

> nr_of_stocks_delta_hedge 100;;
val it : float = 4879.628104

答案是大约需要 4880 股股票来对冲这些看涨期权。

探索波动率微笑

波动率微笑是市场中常见的现象。这个现象通常可以通过布莱克-斯科尔斯公式中的假设来解释。布莱克-斯科尔斯假设期权生命周期内的波动率是恒定的。如果修正布莱克-斯科尔斯公式,考虑到波动率并非恒定,我们将得到一个平坦的波动率曲面。

此外,波动率微笑描述的是期权某一价格相对于相同行权价的波动率。波动率曲面通常指的是波动率微笑的三维图表,其中包括到期时间和内在价值。

内在价值是标的资产现货价格 S 和期权行权价格 K 之间的比率:

探索波动率微笑

接下来,我们将看看如何使用 F# 提供一个图表,其中波动率微笑是通过从真实市场数据的参数回归计算得出的。

以下数据来自瑞典 OMX 交易所的爱立信 B 类期权:

探索波动率微笑

图 3:认购权证的波动率微笑

正如你在下面的截图中看到的,微笑曲线来源于不同的执行价格隐含波动率:

探索波动率微笑

图 4:来自市场数据的波动率微笑点

我们可以使用多项式回归从图表中的点估算波动率微笑。这种方法在第三章《金融数学与数值分析》中介绍;我们将在那里构建的代码基础上进行扩展。该多项式的阶数为二,即二次多项式,将很好地描述预期的波动率微笑。

让我们看看一个应用程序,它将使用 Math.NET 和 FSharpChart 生成 图 5 中的图形:

open System.IO
open FSharp.Charting
open System.Windows.Forms.DataVisualization.Charting

open MathNet.Numerics
open MathNet.Numerics.LinearAlgebra
open MathNet.Numerics.LinearAlgebra.Double
open MathNet.Numerics.Distributions

let filePath = @"smile_data.csv"

/// Split row on commas
let splitCommas (l:string) =
    l.Split(',')

/// Read a file into a string array
let openFile (name : string) =
    try
        let content = File.ReadAllLines(name)
        content |> Array.toList
    with
        | :? System.IO.FileNotFoundException as e -> printfn "Exception! %s " e.Message; ["empty"]

/// Read the data from a CSV file and returns
/// a tuple of strike price and implied volatility%
let readVolatilityData =
    openFile filePath
    |> List.map splitCommas
    |> List.map (fun cols -> (cols.[2], cols.[3]))

/// Calculates moneyness and parses strings into numbers
let calcMoneyness spot list =
    list
    |> List.map (fun (strike, imp) -> (spot / (float strike), (float imp)))

现在我们已经将数据存储在一个元组中,我们将使用基础资产的现货价格,该价格在数据收集时为 83.2。mlist 是一个包含已计算每个元组的平值度的转换后元组列表:

let list = readVolatilityData
let mlist = calcMoneyness 83.2 list

/// Plot values using FSharpChart
fsi.AddPrinter(fun (ch:FSharp.Charting.ChartTypes.GenericChart) -> ch.ShowChart(); "FSharpChartingSmile")    

如果你想重现之前的图表,可以在 F# Interactive 中运行以下代码行:

Chart.Point(mlist)

最后的步骤是计算回归系数,并利用这些系数计算曲线上的点。然后我们将使用包含这些点和拟合曲线的组合图表:

/// Sample points
//let xdata = [ 0.0; 1.0; 2.0; 3.0; 4.0 ]
//let ydata = [ 1.0; 1.4; 1.6; 1.3; 0.9 ]

let xdata = mlist |> Seq.map (fun (x, _) -> x) |> Seq.toList
let ydata = mlist |> Seq.map (fun (_, y) -> y) |> Seq.toList

let N = xdata.Length
let order = 2

/// Generating a Vandermonde row given input v
let vandermondeRow v = [for x in [0..order] do yield v ** (float x)]

/// Creating Vandermonde rows for each element in the list
let vandermonde = xdata |> Seq.map vandermondeRow |> Seq.toList

/// Create the A Matrix
let A = vandermonde |> DenseMatrix.ofRowsList N (order + 1)
A.Transpose()

/// Create the Y Matrix
let createYVector order l = [for x in [0..order] do yield l]
let Y = (createYVector order ydata |> DenseMatrix.ofRowsList (order + 1) N).Transpose()

/// Calculate coefficients using least squares
let coeffs = (A.Transpose() * A).LU().Solve(A.Transpose() * Y).Column(0)

let calculate x = (vandermondeRow(x) |> DenseVector.ofList) * coeffs

let fitxs = [(Seq.min xdata).. 0.01 ..(Seq.max xdata)]
let fitys = fitxs |> List.map calculate
let fits = [for x in [(Seq.min xdata).. 0.01 ..(Seq.max xdata)] do yield (x, calculate x)]

这是生成带标题的组合图表的代码行。结果将如以下截图所示:

let chart = Chart.Combine [Chart.Point(mlist); Chart.Line(fits).WithTitle("Volatility Smile")]

探索波动率微笑

图 5:带有多项式回归曲线的波动率微笑

总结

在本章中,我们探讨了如何使用 F#来研究波动率的不同方面。波动率是金融学中一个有趣的维度,您可以快速深入到复杂的理论和模型中。在这里,拥有一个强大的工具,如 F#和 F# Interactive,非常有帮助。我们在本章中只是稍微触及了期权和波动率的表面。还有很多内容需要讨论,但超出了本书的范围。

本章中的大部分内容将用于我们将在接下来的章节中开发的交易系统。在下一章中,我们将开始研究我们交易系统的第一个务实构建模块:订单和订单类型。我们还将涵盖一些关于交易前风险的方面,以及 F#如何帮助我们建模这些内容。

第七章:开始使用订单类型和市场数据

在本章中,你将学习如何在 F#中建模订单类型、市场数据以及各种类型的市场数据源。

在本章中,你将学习:

  • 订单类型建模

  • 市场数据建模

  • 实现简单的交易前风险分析

  • 使用函数组合和柯里化

介绍订单

通常,订单是买方给卖方关于如何买卖金融工具的指令。这些订单在一定程度上是标准化的。在大多数情况下,订单和订单类型由所使用的经纪商和交易所决定。

订单类型

订单类型是交易系统和交易所中常见的订单种类。通常你会遇到市场订单、限价单和条件单。在条件单类别中,我们有止损单和其他具有特定执行条件的订单。

还有其他类型的订单,但它们被认为是合成订单,因为它们是前面所述订单类型的组合。订单类型可以是以下几种之一:

  • 市场订单

  • 限价单

  • 条件单和止损单

市场订单

市场订单是以当前市场价格在交易所执行的订单。这些订单将接受特定金融工具的当前买卖价格(即订单簿顶端价格)。此外,市场订单是此处展示的最简单的订单类型。由于执行价格的不确定性,市场订单不适用于需要更复杂风险管理的情况。这意味着它们在现实中的应用是相对有限的。

限价单

限价单是最常用的订单类型。顾名思义,它们是限定在一个固定价格范围内的订单。这意味着订单有一个限制价格,只有在该价格下,订单才会被执行。对于买单而言,这意味着存在一个上限,所有低于该限价的价格都会被接受。对于卖单,情况则相反,存在一个下限,所有高于该限价的价格都会被接受。

条件单和止损单

条件单,尤其是止损单,是在特定条件下会在交易所被激活的订单。止损单会在订单簿(即顶端订单)触及某一价格时被激活。当该条件满足时,订单将转换为市场订单。也存在一些在触发条件后会转为限价单的止损单。

订单属性

订单属性指的是描述订单应执行的操作及其条件的属性。下表展示了最基本的订单属性,并附有每个属性的描述和示例。

属性 描述 示例
订单方向 订单是买单还是卖单,包括卖空订单 买、卖
订单类型 订单种类 市场、限价和止损
订单状态 订单在执行流程中的状态 Created, New, Filled, Canceled 等
有效时间 订单的有效时长 GoodForDay, FillOrKill
数量 要购买或出售的特定工具的数量 10, 25 和 200
价格 订单的限价 26.50, 55.10
工具 订单中使用的工具 MSFT, GOOG, AAPL
止损价格 订单中使用的止损价格 26.50, 55.10
时间戳 订单创建的日期和时间 2013-09-14 11:30:44

接下来,我们将继续在第二章中开始的工作,进一步了解 F#,并扩展订单类,描述一些作为区分联合的属性。首先,我们将重新审视订单的一方:

/// Order side
type OrderSide =
    Buy | Sell | Sellshort

在这个上下文中,订单方向将有三种选择:买入、卖出和卖空。然后是订单类型,可以是买入、卖出、止损或止损限价。止损限价是止损订单,随后会转为限价订单。

/// Order type
type OrderType = 
    Market | Limit | Stop | StopLimit

区分联合是描述具有不同值的数据的优雅方式。值可以有很多个,但不会失去可读性。接下来,我们来看订单状态:

type OrderStatus = 
    Created | New | Filled | PartiallyFilled | DoneForDay | Cancelled| Replaced | PendingCancel | Stopped | Rejected | Suspended | PendingNew | Calculated | Expired

订单状态有很多值;它们来自 FIX 4.2 规范,熟悉 FIX 的大多数人都很了解这些值。它们在这里作为示例,将用于交易系统中。有效时间 (Tif) 是交易中最常用的选项:

type Tif = 
    GoodForDay | GoodTilCancelled | ImmediateOrCancel | FillorKill

我们将在这里引入一个新类型,与本节中介绍的功能一起使用。首先,我们将查看以下代码:

/// Validation result, ok or failed with message
type Result = Valid of Order | Error of string

请注意前面代码片段中使用的Result;它用于标识验证函数的结果,我们将在后面介绍。现在,你可以把它们看作表示不同返回类型的方式。

以下订单类是对我们在第二章中工作内容的扩展。它使用了一些可变性,这在 F# 中是允许的,因为它不是纯粹的函数式语言。可变性提供了更多的灵活性,并为更务实的解决方案做出了妥协。这里,我们使用私有成员字段和一些新字段:

/// Order class
type Order(side: OrderSide, t: OrderType, p: float, tif: Tif, q: int, i: string, sp: float) =
    // Init order with status created
    let mutable St = OrderStatus.Created
    let mutable S = side
    member private this.Ts = System.DateTime.Now
    member private this.T = t
    member private this.P = p
    member private this.tif = tif
    member private this.Q = q
    member private this.I = i
    member private this.Sp = sp

    member this.Status
        with get() = St
        and set(st) = St <- st

    member this.Side
        with get() = S
        and set(s) = S <- s

    member this.Timestamp
        with get() = this.Ts

    member this.Type
        with get() = this.T

    member this.Qty
        with get() = this.Q

    member this.Price
        with get() = this.P

    member this.Tif
        with get() = this.tif

    member this.Instrument
        with get() = this.I

    member this.StopPrice
        with get() = this.Sp

    member this.toggleOrderSide() =
        S <- this.toggleOrderSide(S)

    member private this.toggleOrderSide(s: OrderSide) =
        match s with
        | Buy -> Sell
        | Sell -> Buy
        | Sellshort -> Sellshort

    static member (~-) (o : Order) =
        Order(o.toggleOrderSide(o.Side), o.Type, o.Price, o.tif, o.Q,o.I, o.Sp)

上面的方法将切换订单的一方,这在我们想要改变订单方向而不创建新订单时非常有用。

上述类可以像 F# 中的其他类一样使用:

      // Limit buy order
let buyOrder = Order(OrderSide.Buy, OrderType.Limit, 54.50, Tif.FillorKill, 100, "MSFT", 0.0)       

字段如下,其中私有字段是隐藏的,属性的 getter 函数对应字段的名称:

val it : Order = FSI_0050+Order {Instrument = "MSFT";
                                 Price = 54.5;
                                 Qty = 100;
                                 Side = Buy;
                                 Status = Created;
                                 StopPrice = 0.0;
                                 Tif = FillorKill;
                                 Timestamp = 2013-09-14 12:01:57;
                                 Type = Limit;}

假设你想验证一个订单的正确性并避免简单的错误。例如,如果订单是限价单,我们可以检查该订单的价格是否大于零。此外,如果订单是止损单,则止损价格必须大于零,依此类推。让我们编写一个函数来执行这个订单:

/// Validates an order for illustrative purposes
let validateOrder (result:Result) : Result= 
    match resultwith
    | Error s -> Error s
    | Valid order ->
        let orderType = order.Type
        let orderPrice = order.Price
        let stopPrice = order.StopPrice    
        match orderType with
        | OrderType.Limit -> 
            match orderPrice with       
            | p when p > 0.0 -> Valid order
            | _ -> Error "Limit orders must have a price > 0"
        | OrderType.Market -> Valid order
        | OrderType.Stop -> 
            match stopPrice with        
            | p when p > 0.0 -> Valid order
            | _ -> Error "Stop orders must have price > 0"
        | OrderType.StopLimit ->
            match stopPrice with
            | p when p > 0.0 && orderPrice > 0.0 -> Valid order
            | _ -> Error "Stop limit orders must both price > 0 and stop price > 0"

该函数将接受一个封装在订单单子(order monad)中的订单对象。当我们查看如何在以下代码中使用它时,这一点将变得更加明确:

  1. 我们可以创建一些不同类型的订单,看看我们的验证函数是否有效。

    // Limit buy order
    let buyOrder = Order(OrderSide.Buy, OrderType.Limit, 54.50, Tif.FillorKill, 100, "MSFT", 0.0)
    
    // Limit buy order, no price
    let buyOrderNoPrice = Order(OrderSide.Buy, OrderType.Limit, 0.0, Tif.FillorKill, 100, "MSFT", 0.0)
    
    // Stop order that will be converted to limit order, no limit price
    let stopLimitNoPrice = Order(OrderSide.Buy, OrderType.StopLimit, 0.0, Tif.FillorKill, 100, "MSFT", 45.50)
    
    // Stop order that will be converted to market order
    let stopNoPrice = Order(OrderSide.Buy, OrderType.Stop, 0.0, Tif.FillorKill, 100, "MSFT", 45.50)
    
  2. 现在,我们将测试我们为前面创建的订单编写的验证函数:

    // Validate sample orders
    validateOrder (Valid buyOrder) // Ok
    validateOrder (Valid buyOrderNoPrice) // Failed
    validateOrder (Valid stopLimitNoPrice) // Failed
    validateOrder (Valid stopNoPrice) // Ok
    
  3. 假设我们还想要一个验证器来检查订单是否设置了交易品种。然后我们可以逐个运行这些测试。首先,我们需要一个函数来进行验证:

    let validateInstrument (result:Result) : Result =
        match result with
        | Error l -> Error l
        | Valid order ->
            let orderInstrument = order.Instrument
            match orderInstrument.Length with
            | l when l > 0 -> Valid order 
            | _ -> Error "Must specify order Instrument"
    
  4. 在 F# Interactive 中运行此代码将得到以下结果:

    > validateInstrument (Valid stopNoPriceNoInstrument);;
    val it : Result = Error "Must specify order Instrument"
    
  5. 现在,我们希望将两个验证器合并为一个验证器。这可以通过 F# 的函数组合来实现。函数组合使用 >> 操作符,可以将两个或更多的函数组合成一个新的函数。你可以把函数组合看作是将函数链式连接的一种方式。这在使用和重用较小的构建块时非常有用,支持模块化。

    /// Composite validator
    let validateOrderAndInstrument = validateOrder >> validateInstrument
    
  6. 现在可以像我们之前使用的那个函数一样使用此函数:

    // Stop order that will be converted to market order
    let stopNoPriceNoInstrument = Order(OrderSide.Buy, OrderType.Stop, 0.0, Tif.FillorKill, 100, "", 45.50)
    
    validateOrderAndInstrument (Valid stopNoPriceNoInstrument)
    

这是结合功能的一种非常强大的方式,在许多情况下都很有用。当我们稍后在处理交易前风险时,我们会再次讨论函数组合。

理解订单执行

让我们稍微看一下订单执行和订单流。订单执行是订单被执行的地方,通常是在交易所。对于算法交易,买方(交易员)通常会在交易所有自己的订单管理系统或订单执行引擎,靠近交易所的交易服务器。交易所的订单通常是限价单;当然也有其他类型。所有其他类型的订单都被视为合成订单或非原生订单。如果使用经纪商,你是看不到这些的。但对于高频交易而言,限价单被认为是唯一的原生订单类型。市价单实际上是以当前市场价格(来自订单簿顶部)设置的限价单。

下图展示了订单执行引擎与交易所之间的简单订单流。订单执行引擎位于买方一侧,并跟踪当前存在的订单及其状态。

理解订单执行

订单执行流程和订单状态更新

订单的状态由我们之前看到的区分联合体(discriminated union)表示:

type OrderStatus = 
    Created | New | Filled | PartiallyFilled | DoneForDay | Cancelled | Replaced | PendingCancel | Stopped | Rejected | Suspended | PendingNew | Calculated | Expired

当接收到特定订单的执行报告时,订单管理系统中的属性将会更新。首先,订单的状态是Created,然后如果被接受,它将具有订单状态New。接下来,它将具有来自前一个OrderStatus对象中的任何订单状态;例如FilledExpiredRejected等。

引入市场数据

市场数据是表示某个金融工具在交易所当前买卖报价的数据。市场数据可以是该工具的顶级报价、最优买/卖报价,或是包含多个深度层级的汇总书。通常,我们只查看最佳买卖报价,称为“报价”。市场数据也可以作为 OHLC 条形图或蜡烛图发送。这些条形图仅用于在图表中可视化价格信息或在较长时间跨度的交易中使用。这是一种简单的价格信息筛选方式。中点定义为买价和卖价的平均值。

Type Quote =
{ 
   bid : float
   ask : float
} member this.midpoint() = (this.bid + this.ask) / 2.0

让我们看看如何使用这个类型:

let q = {bid = 1.40; ask = 1.45} : Quote

我们可以按以下方式计算中点:

> q.midpoint();;
val it : float = 1.425

我们可以扩展Quote类型,添加一个内置函数来计算该报价的价差:

type Quote =
    {
        bid : float
        ask : float
    }
    member this.midpoint() = (this.bid + this.ask) / 2.0
    member this.spread() = abs(this.bid - this.ask)

let q2 = {bid = 1.42; ask = 1.48} : Quote

> q2.midpoint();;
val it : float = 1.45
> q2.spread();;
val it : float = 0.06

有时,在实际系统中,买价和卖价不会在同一个对象中发送,因为它们的更新不会同时发生。此时,最好将它们分开:

// Often data is just sent from the feed handler, as bid or ask
type LightQuote = 
    | Bid of float | Ask of float

let lqb = Bid 1.31
let lqa = Ask 1.32

实现简单的交易前风险分析

在本节中,我们将简要介绍一个简单交易系统的交易前风险。交易前风险是指在订单发送到交易所之前,所有被认为是风险的内容并进行分析的部分。通常,这些操作在交易引擎、订单执行引擎或订单管理系统中完成。在这里,我们将考虑一些基本的交易前风险衡量标准,例如订单大小和限价的最大距离(即相对于当前市场价格可能出现的高/低值)。

以下是交易前风险规则的示例:

  • 限制订单价格与当前市场价格的距离

  • 最大订单价值

  • 总风险敞口

  • 每单位时间发送的订单数量

验证订单

让我们深入一些代码,看看如何使用函数式编程和函数组合来实现交易前风险控制。我们将查看两个简单的交易前风险规则。第一个规则是针对总订单价值的,另一个规则是检查限价是否设定在当前市场价格的有利一侧。例如,这对于手动交易可能非常有用。

let orderValueMax = 25000.0; // Order value max of $25,000

/// A simple pre trade risk rule
let preTradeRiskRuleOne (result:Result) : Result =
    match result with
    | Error l -> Error l
    | Valid order ->
        let orderValue = (float order.Qty) * order.Price
        match orderValue with
        | v when orderValue > orderValueMax -> Error "Order value exceeded limit"
        | _ -> Valid order

我们将使用的下一个规则是currying,这是我们在第二章,深入学习 F#中讨论过的一个概念。Currying 是一种调用函数的方式,它将函数的部分参数保存起来,稍后再指定。以下是我们用于交易前风险分析的第二个规则:

// Using currying
let preTradeRiskRuleTwo (marketPrice:float) (result:Result) : Result =
    match result with
    | Error l -> Error l
    | Valid order ->
        let orderLimit = (float order.Qty) * order.Price
        match orderLimit with
        | v when orderLimit < marketPrice && order.Side = OrderSide.Buy -> Error "Order limit price below market price"
        | v when orderLimit > marketPrice && order.Side = OrderSide.Sell -> Error "Order limit price above market price"
        | _ -> Valid order

接下来,我们将像以前一样使用函数组合,从现有的规则中创建新规则:

let validateOrderAndInstrumentAndPreTradeRisk = validateOrderAndInstrument >> preTradeRiskRuleOne

let validateOrderAndInstrumentAndPreTradeRisk2 marketPrice = validateOrderAndInstrument >> preTradeRiskRuleOne >> (preTradeRiskRuleTwo marketPrice)

validateOrderAndInstrumentAndPreTradeRisk (Valid stopNoPriceNoInstrument)
validateOrderAndInstrumentAndPreTradeRisk (Valid buyOrderExceetsPreTradeRisk)
validateOrderAndInstrumentAndPreTradeRisk2 25.0 (Valid buyOrderBelowPricePreTradeRisk)

这个模式可能会迅速变得不太实用。然而,有一个优雅的技巧可以使用,我们首先在一个列表中指定函数,然后使用List.reduce和组合运算符来创建一个新的组合:

/// Chain using List.reduce
let preTradeRiskRules marketPrice = [
    preTradeRiskRuleOne
    (preTradeRiskRuleTwo marketPrice)
    ]

/// Create function composition using reduce, >>, composite operator
let preTradeComposite = List.reduce (>>) (preTradeRiskRules 25.0)

preTradeComposite (Valid buyOrderExceetsPreTradeRisk)
preTradeComposite (Valid buyOrderBelowPricePreTradeRisk)

介绍 FIX 和 QuickFIX/N

在这一部分,我们将了解 FIX 4.2 标准以及用于.NET 的 QuickFIX/N 库。FIX 是与经纪商和交易所通信的标准协议,代表金融信息交换(Financial Information eXchange)。它自 90 年代初以来就存在,使用基于 ASCII 的消息表示方式。还有其他替代方案,例如专有的 API 和协议。

使用 FIX 4.2

在这一部分,我们将重构现有的交易系统,以便使用 FIX 4.2 消息。

  1. 从以下网址下载 FIXimulator:fiximulator.org/

  2. 从以下网址下载 QuickFIX/n:www.quickfixn.org/download

  3. 从压缩包中提取文件到文件夹中。

  4. 通过运行fiximulator.bat启动 FIXimulator。

当你启动 FIXimulator 时,应用程序将显示如下图所示。在应用程序底部有一个状态栏,显示指示器。最左侧的指示器是客户端连接状态。第一步是成功连接到模拟器。

配置 QuickFIX 以使用模拟器

为了能够连接到 FIXimulator,你需要一个合适的配置文件config.cfg,它应放在项目的类路径中,并且在以下代码中的SessionSettings构造函数里有正确的路径:

config.cfg的内容如下:

[DEFAULT]
ConnectionType=initiator
ReconnectInterval=60
SenderCompID=TRADINGSYSTEM

[SESSION]
BeginString=FIX.4.2
TargetCompID=FIXIMULATOR
StartTime=00:00:00
EndTime=00:00:00
HeartBtInt=30
ReconnectInterval=10
SocketConnectPort=9878
SocketConnectHost=192.168.0.25
FileStorePath=temp
ValidateUserDefinedFields=N

ResetOnLogon=Y 
ResetOnLogout=Y 
ResetOnDisconnect=Y

你必须更改SocketConnectHost字段的值以适应你的设置,如果你在同一台机器上运行模拟器,那么这将是你的本地 IP 地址。

模拟器还有一个配置文件,即FIXimulator.cfg,其中TargetCompID的值必须更改为TRADINGSYSTEM

配置 QuickFIX 以使用模拟器

没有客户端连接

在客户端,我们将在程序中使用 QuickFIX 库并实现所需的方法,以便连接到模拟器。将以下代码添加到新文件FIX.fs中。我们将在最终项目中使用这个代码,因此建议在完成这个项目时,从 TradingSystem 项目开始,这个项目将在下一章开始。

namespace FIX

    open Systemopen System.Globalization
    open QuickFix
    open QuickFix.Transport
    open QuickFix.FIX42
    open QuickFix.Fields

要使用 QuickFIX,必须实现多个接口。ClientInitiator函数是消息处理的地方:

module FIX =
    type ClientInitiator() =
        interface IApplication with
            member this.OnCreate(sessionID : SessionID) : unit = printfn "OnCreate"
            member this.ToAdmin(msg : QuickFix.Message, sessionID : SessionID) : unit = printf "ToAdmin"
            member this.FromAdmin(msg : QuickFix.Message, sessionID : SessionID) : unit = printf "FromAdmin"
            member this.ToApp(msg : QuickFix.Message, sessionID : SessionID) : unit = printf "ToApp"
            member this.FromApp(msg : QuickFix.Message, sessionID : QuickFix.SessionID) : unit = printfn"FromApp -- %A" msg
            member this.OnLogout(sessionID : SessionID) : unit = printf "OnLogout"
            member this.OnLogon(sessionID : SessionID) : unit = printf "OnLogon"

ConsoleLog函数是另一个需要支持日志记录的接口。

type ConsoleLog() =
    interface ILog with	
        member this.Clear() : unit = printf "hello"
        member this.OnEvent(str : string) : unit = printfn "%s" str
        member this.OnIncoming(str : string) : unit = printfn "%s" str
        member this.OnOutgoing(str : string) : unit = printfn "%s" str

type ConsoleLogFactory(settings : SessionSettings) =
    interface ILogFactory with
        member this.Create(sessionID : SessionID) : ILog = new NullLog() :> ILog

最后,我们有FIXEngine本身。这是提供给系统其他部分的接口。它提供了启动、停止和发送订单的方法。

type FIXEngine() =
    let settings = new SessionSettings(@"conf\config.cfg")
    let application = new ClientInitiator()
    let storeFactory = FileStoreFactory(settings)
    let logFactory = new ConsoleLogFactory(settings)
    let messageFactory = new MessageFactory()
    let initiator = new SocketInitiator(application, storeFactory, settings)
    let currentID = initiator.GetSessionIDs() |> Seq.head
        member this.init() : unit =
            ()
        member this.start() : unit =
            initiator.Start()
        member this.stop() : unit =
            initiator.Stop()

现在,我们可以使用 F# Interactive 尝试前面的代码,并且我们将成功连接到模拟器。如果一切顺利,FIXimulator窗口中的客户端连接状态将变为绿色。

let fixEngine = new FIX.FIXEngine()
fixEngine.init()
fixEngine.start()

配置 QuickFIX 使用模拟器

当客户端连接时,客户端连接状态为绿色

让我们增加些趣味,尝试一些代码将订单发送到模拟器。如前所述,订单有各种属性。我们Order对象中的字段应该尽可能贴近 FIX 4.2 字段。我们首先尝试使用 QuickFIX 来表示订单,并向模拟器发送一个限价订单,看看一切是否按预期工作。为此,我们将向 FIXEngine 添加一个方法:

member this.sendTestLimitOrder() : unit =
   let fixOrder = new NewOrderSingle()
   fixOrder.Symbol <- new Symbol("ERICB4A115")
   fixOrder.ClOrdID <- new ClOrdID(DateTime.Now.Ticks.ToString())fixOrder.OrderQty <- new OrderQty(decimal 50)
   fixOrder.OrdType <- new OrdType('2'); // Limit order
   fixOrder.Side <- new Side('1');
   fixOrder.Price <- new Price(decimal 25.0);
   fixOrder.TransactTime <- new TransactTime();
   fixOrder.HandlInst <- new HandlInst('2');
   fixOrder.SecurityType <- new SecurityType("OPT"); // Option                
   fixOrder.Currency <- new Currency("USD");                
   // Send order to target
Session.SendToTarget(fixOrder, currentID) |> ignore

这个方法将仅用于测试目的,在我们稍后添加支持将内部 Order 对象转换为 QuickFIX 使用的表示时,它会作为一个有效的参考。你需要重新启动 F# Interactive 会话,以便看到我们之前对 FIXEngine 所做的更改。运行以下代码,希望你能将第一个订单发送到模拟器:

let fixEngine = new FIX.FIXEngine()
fixEngine.init()
fixEngine.start()
fixEngine.sendTestLimitOrder()

配置 QuickFIX 使用模拟器

当客户端连接时,客户端连接状态为绿色

让我们在FIXimulator中再待一会儿,看看我们得到了哪些有用的功能。

你可以在模拟器中选择订单标签页,接着你会看到一个新的视图,如下图所示:

配置 QuickFIX 使用模拟器

调查来自进单的订单属性

SenderCompID 字段对应的是订单的实际发送者,即TradingSystem。我们还可以看到订单的其他属性。在模拟器中,每个订单都有多个选择,例如:

  • 已确认

  • 已取消

  • 被拒绝

  • 已执行

我们需要一些支持来跟踪系统中的订单,并根据从对方(模拟器)接收到的消息改变它们的状态;这通过使用 ClientInitiator 函数来实现。这里有一个名为 FromApp 的方法,用来处理传入的消息;例如,修改系统中订单的状态。在订单执行管理的情况下,监听的消息是 ExecutionReport。顾名思义,它是执行报告。首先,我们将实现代码,仅打印出模拟器中最后一次订单执行的状态。然后我们将使用模拟器进行测试。

member this.FromApp(msg : QuickFix.Message, sessionID : QuickFix.SessionID) : unit =
   match msg with
   | :? ExecutionReport as report ->
          match report.OrdStatus.getValue() with
          | OrdStatus.NEW -> printfn "ExecutionReport (NEW) %A" report
          | OrdStatus.FILLED -> printfn "ExecutionReport (FILLED) %A" report
          | OrdStatus.PARTIALLY_FILLED -> printfn "ExecutionReport (PARTIALLY_FILLED) %A" report
          | OrdStatus.CANCELED -> printfn "ExecutionReport (CANCELED) %A" report
          | OrdStatus.REJECTED -> printfn "ExecutionReport (REJECTED) %A" report
          | OrdStatus.EXPIRED -> printfn "ExecutionReport (EXPIRED) %A" report
          | _ -> printfn "ExecutionReport (other) %A" report
   | _ -> ()

在前面的代码中,我们尝试将 msg 强制转换为 ExecutionReport 实例,如果成功,我们会对订单状态进行模式匹配。让我们一起在模拟器中试试这个。

  1. 使用 fixEngine.sendTestLimitOrder() 向模拟器发送测试订单。

  2. FIXimulator中,选择订单下接收到的订单。

  3. 按下确认按钮。

  4. F# Interactive 将输出执行报告,内容大致如下:

    ExecutionReport (NEW) seq [[6, 0]; [11, 1]; [14, 0]; [17, E1385238452777]; ...]
    

    这意味着我们已收到执行报告,订单状态现在是New

  5. 在模拟器中选择相同的订单并点击取消

    ExecutionReport (CANCELED) seq [[6, 0]; [11, 1]; [14, 0]; [17, E1385238572409]; ...]
    
  6. 我们将重复该过程,但这次我们将先确认订单,然后分两步执行,结果将产生 NewPartial fillFilled 执行报告。

  7. 再次发送测试订单。

  8. 确认订单。

  9. 执行订单,LastShares=50 和 LastPx=25。

  10. 再次执行,LastShares=50 和 LastPx=24。

  11. 观察 F# Interactive 中的输出。

注意执行报告到达的顺序。首先,我们执行了订单的一半,导致部分成交。然后,我们执行剩余的 50 份,完成整个订单。

ExecutionReport (PARTIALLY_FILLED) seq [[6, 25]; [11, 1]; [14, 50]; [17, E1385238734808]; ...]
ExecutionReport (FILLED) seq [[6, 24.5]; [11, 1]; [14, 100]; [17, E1385238882881]; ...]

在模拟器中,我们可以看到 AvgPx 列中的值为 24.5。这对应于整个订单的平均价格。接下来,我们将实现一个轻量级的订单管理器,帮助我们跟踪所有这些信息。

让我们看看修改后的 FromApp 回调函数代码,我们仅仅修改了与订单 ID 匹配的订单状态:

| OrdStatus.NEW -> 
   printfn "ExecutionReport (NEW) %A" report
   orders |> Seq.find (fun order -> order.Timestamp.ToString() = report.ClOrdID.getValue()) |> (fun order -> order.Status <- OrderStatus.New)

orders 列表是一个 BindingList,这是我们在 MVC 模型之外用于实验的声明,最终系统中将会定位在该模型中。

/// Use a list of NewOrderSingle as first step
let orders = new BindingList<Order>()
let fixEngine = new FIX.FIXEngine(orders)
fixEngine.init()
fixEngine.start()
let buyOrder1 = Order(OrderSide.Buy, OrderType.Limit, 24.50, Tif.GoodForDay, 100, "ERICB4A115", 0.0)
let buyOrder2 = Order(OrderSide.Buy, OrderType.Limit, 34.50, Tif.GoodForDay, 100, "ERICB4A116", 0.0)
let buyOrder3 = Order(OrderSide.Buy, OrderType.Limit, 44.50, Tif.GoodForDay, 100, "ERICB4A117", 0.0)
fixEngine.sendOrder(buyOrder1)
fixEngine.sendOrder(buyOrder2)
fixEngine.sendOrder(buyOrder3)

sendOrder 方法也稍作修改,以便处理我们的订单对象并将其转换为 QuickFIX 表示形式:

member this.sendOrder(order:Order) : unit =                
   let fixOrder = new NewOrderSingle()
   /// Convert to Order to NewOrderSingle
   fixOrder.Symbol <- new Symbol(order.Instrument)
   fixOrder.ClOrdID <- new ClOrdID(order.Timestamp.ToString())
   fixOrder.OrderQty <- new OrderQty(decimal order.Qty)
   fixOrder.OrdType <- new OrdType('2'); /// TODO
   fixOrder.Side <- new Side('1');
   fixOrder.Price <- new Price(decimal order.Price);
   fixOrder.TransactTime <- new TransactTime();
   fixOrder.HandlInst <- new HandlInst('2');
   fixOrder.SecurityType <- new SecurityType("OPT"); /// TODO      
   fixOrder.Currency <- new Currency("USD"); /// TODO
   /// Add to OMS
   orders.Add(order)
   /// Send order to target
   Session.SendToTarget(fixOrder, currentID) |> ignore

我们现在可以使用迄今为止编写的代码,通过模拟器测试简单的订单管理器。当三笔订单发送到模拟器时,输出将如下截图所示。选择第一笔订单并点击 确认。现在我们可以比较订单列表的内容,查看执行报告是否已处理,以及订单管理器是否完成了任务。

这是 orders 列表中的第一笔 Order

{Instrument = "ERICB4A115";
 OrderId = "635208412525809991";
 Price = 24.5;
 Qty = 100;
 Side = Buy;
 Status = Created;
 StopPrice = 0.0;
 Tif = GoodForDay;
 Timestamp = 2013-11-23 22:09:31;
 Type = Limit;}
…

如果一切顺利,这将更改为如下所示:

{Instrument = "ERICB4A115";
OrderId = "635208412525809991";
Price = 24.5;
Qty = 100;
Side = Buy;
Status = New;
StopPrice = 0.0;
Tif = GoodForDay;
Timestamp = 2013-11-23 22:09:31;
Type = Limit;}

配置 QuickFIX 使用模拟器

测试我们订单管理器的第一版

剩下的部分是包括对部分成交的支持,其中我们有未完成和已执行的数量以及平均价格。这是订单管理器要完善的最后部分。我们还将增强 ClientInitiator 函数的输出,以包含来自执行报告的这些字段。

以下是对代码进行的修改,用于处理订单管理部分:

member this.findOrder str = 
   try 
      Some (orders |> Seq.find (fun o -> o.Timestamp = str))
   with | _ as ex -> printfn "Exception: %A" ex.Message; None

member this.FromApp(msg : QuickFix.Message, sessionID : QuickFix.SessionID) : unit =
match msg with
| :? ExecutionReport as report ->
   let qty = report.CumQty
   let avg = report.AvgPx
   let sta = report.OrdStatus
   let oid = report.ClOrdID
   let lqty = report.LeavesQty
   let eqty = report.CumQty

let debug = fun str -> printfn "ExecutionReport (%s) # avg price: %s | qty: %s | status: %s | orderId: %s" str (avg.ToString()) (qty.ToString()) (sta.ToString()) (oid.ToString())

   match sta.getValue() with
   | OrdStatus.NEW ->                            
         match this.findOrder(oid.ToString()) with
         | Some(o) ->
               o.Status <- OrderStatus.New
         | _ -> printfn "ERROR: The order was not found in OMS"
         debug "NEW"

为了处理 Filled 状态,我们设置了平均价格、未完成数量和已执行数量:

   | OrdStatus.FILLED ->
         /// Update avg price, open price, ex price
         match this.findOrder(oid.ToString()) with
         | Some(o) ->
                o.Status <- OrderStatus.Filled
                o.AveragePrice <- double (avg.getValue())
                o.OpenQty <- int (lqty.getValue())
                o.ExecutedQty <- int (eqty.getValue())
          | _ -> printfn "ERROR: The order was not found in OMS"
          debug "FILLED"

为了处理 PartiallyFilled 状态,我们设置了平均价格、未完成数量和已执行数量:

   | OrdStatus.PARTIALLY_FILLED ->                   
         /// Update avg price, open price, ex price
         match this.findOrder(oid.ToString()) with
         | Some(o) ->
               o.Status <- OrderStatus.PartiallyFilled
               o.AveragePrice <- double (avg.getValue())
               o.OpenQty <- int (lqty.getValue())
               o.ExecutedQty <- int (eqty.getValue())
         | _ -> printfn "ERROR: The order was not found in OMS"
         debug "PARTIALLY_FILLED"

剩余的状态更新是直接的更新:

   | OrdStatus.CANCELED ->
          match this.findOrder(oid.ToString()) with
          | Some(o) ->
                o.Status <- OrderStatus.Cancelled
         | _ -> printfn "ERROR: The order was not found in OMS"
         debug "CANCELED"
   | OrdStatus.REJECTED ->                             
         match this.findOrder(oid.ToString()) with
         | Some(o) ->
                o.Status <- OrderStatus.Rejected
         | _ -> printfn "ERROR: The order was not found in OMS"
         debug "REJECTED"
   | OrdStatus.REPLACED ->
         match this.findOrder(oid.ToString()) with
         | Some(o) ->
               o.Status <- OrderStatus.Replaced                                
         | _ -> printfn "ERROR: The order was not found in OMS"
         debug "REPLACED"
   | OrdStatus.EXPIRED -> 
         printfn "ExecutionReport (EXPIRED) %A" report
   | _ -> printfn "ExecutionReport (other) %A" report
| _ -> ()

我们可以通过模拟器进行测试,像之前一样进行部分成交的测试运行,结果将导致 orders 列表中出现以下内容:

{AveragePrice = 0.0;
ExecutedQty = 0;
Instrument = "ERICB4A115";
OpenQty = 0;
Price = 24.5;
Qty = 100;
Side = Buy;
Status = Cancelled;
...

{AveragePrice = 23.0;
ExecutedQty = 100;
Instrument = "ERICB4A116";
OpenQty = 0;
Price = 34.5;
Qty = 100;
Side = Buy;
Status = Filled;
...

{AveragePrice = 24.5;
ExecutedQty = 100;
Instrument = "ERICB4A117";
OpenQty = 0;
Price = 44.5;
Qty = 100;
Side = Buy;
Status = Filled;
...

配置 QuickFIX 使用模拟器

订单管理器的最终迭代和测试

这些值将在交易系统的 GUI 数据网格中展示,我们将在下一章开始准备该部分。

总结

在本章中,我们介绍了关于订单和市场数据的几个概念。我们还探讨了如何在 F# 中利用 FIX 4.2 标准中提供的一些信息来建模订单和市场数据。这些事实在从事量化金融工作时非常有用,特别是在处理算法交易时。

本章的结果将在我们开始搭建交易系统时使用,搭建工作将在下一章开始。我们还将探讨如何为本章中提供的验证代码实现测试用例。

第八章:设置交易系统项目

在本章中,我们将设置交易系统,这个系统将在本书接下来的章节中开发。交易系统将总结到目前为止我们学到的内容。这也是一个很好的例子,展示了 F#与现有工具和库结合时的强大功能。我们将首先在 Visual Studio 中设置项目,然后添加测试所需的引用以及连接 Microsoft SQL Server 所需的引用。类型提供程序和语言集成查询LINQ)将在此简要介绍,更多细节将在下一章中讲解。

在本章中,我们将学习:

  • 更多关于自动化交易的信息

  • 测试驱动开发

  • 交易系统的需求

  • 设置项目

  • 连接到数据库

  • F#中的类型提供程序

解释自动化交易

自动化交易近年来变得越来越流行。大多数交易策略可以通过计算机实现交易。自动化交易策略有很多好处,交易策略可以通过历史数据进行回测。这意味着策略会在历史数据上运行,并且可以研究策略的表现。虽然本书不涉及回测,但此处开发的交易系统可以修改以支持回测。

自动化交易系统顾名思义,是在计算机上运行的自动化交易系统。它们通常由几个部分组成,如数据馈送处理器、订单管理系统和交易策略。通常,自动化交易系统会呈现出从市场数据到订单再到执行的管道,并跟踪状态和历史。规则会被编写成在市场数据进入系统时几乎实时地执行。这就像一个常规的控制系统,具有输入和输出。在接下来的章节中,我们将看看如何用 F#实现一个相当简单,但又非常强大的交易系统,概括我们在本书中学到的内容。

以下是自动化交易系统的组成部分:

  • 数据馈送处理器和市场数据适配器

  • 交易策略

  • 订单执行与订单管理

  • 持久化层(数据库)

  • 用于监控和人工交互的图形用户界面(GUI)

下面是展示自动化交易系统各部分的框图:

解释自动化交易

图 1:交易系统的典型框图

理解软件测试和测试驱动开发

在编写软件时,能够测试系统的功能至关重要。在软件开发中,有一种流行且有效的编写代码的方式,即测试驱动开发。此方法由测试驱动,测试在主要逻辑实现之前编写。换句话说,当你准备为系统编写测试用例时,你肯定已经有一些要求或者对软件的想法。在测试驱动开发中,测试将反映需求。这是一种将需求写入代码的方式,用于测试给定功能集的软件。测试实现为测试用例,测试用例被收集到测试套件中。测试最好通过工具进行自动化。拥有自动化测试将使开发人员每次对代码进行更改时都能重新运行测试。

在本章中,我们将重点讲解如何使用NUnit进行单元测试。

理解 NUnit 和 FsUnit

NUnit 是一个开源单元测试框架,适用于所有 .NET 语言,类似于 JUnit 对于 Java 的作用。NUnit 使程序员能够编写单元测试,并执行测试,以查看哪些测试成功,哪些失败。在我们的项目中,我们将使用 NUnit 及其外部工具来运行测试。在 F# 中,使用 FsUnit 进行测试的典型代码行如下所示:

> 1 |> should equal 1;;

系统的要求

在本节中,我们将讨论交易系统的一些主要要求。我们不会指定所有细节,因为其中一些需要分成多个部分。交易系统将使用一些库和工具,这些库和工具将在下一节中指定。

表格展示我们将开发的交易系统的一些最重要要求。它将是一个简单的系统,用于利用 S&P 500 指数期权和 CBOE 波动率指数(VIX)进行波动率套利交易。S&P 500 指数由 NYSE 或 NASDAQ 上市的最大 500 家公司组成,被视为美国股市的总体指标。VIX 是 S&P 500 指数期权隐含波动率的指数。

系统应该能够执行以下操作:

  • 将日志条目存储在 Microsoft SQL Server 数据库中

  • 将交易历史存储在 Microsoft SQL Server 数据库中

  • 从 Yahoo! Finance 下载报价

  • 管理订单

  • 使用 FIX 4.2 发送订单

  • 使用 FIX 4.2 连接到 FIX 模拟器

  • 执行用 F# 编写的交易策略

  • 通过一个基本的 GUI 控制,包含启动/停止按钮

  • 显示当前的仓位

  • 显示当前的盈亏

  • 显示最新的报价

  • 使用 MVC 模式

以下是使用的库和工具:

  • QuickFIX/N:.NET 的 FIX 协议

  • QuantLib:这是一个用于量化金融的库

  • LINQ

  • Microsoft SQL Server

  • Windows Forms

  • FSharpChart:这是 Microsoft Chart Controls 的 F# 友好封装

设置项目

在本节中,我们将在 Visual Studio 中设置解决方案。它将包含两个项目;一个是交易系统项目,另一个是测试项目。将两者分开有一些优势,并且会生成两个二进制文件。测试将通过 NUnit 程序运行,NUnit 是一个用于运行单元测试的独立程序。

接下来的步骤将在同一解决方案中创建两个项目:

  1. 创建一个新的 F#应用程序,命名为TradingSystem,如下图所示:设置项目

    图 2:向解决方案中添加新项目

  2. 向现有的TradingSystem解决方案中添加一个新项目。右键单击解决方案,如图 2所示,然后选择添加 | 新建项目...。创建另一个 F#应用程序,并将其命名为TradingSystem.Tests

  3. 接下来,我们必须将测试框架添加到TradingSystem.Tests项目中,如下图所示:设置项目

    图 3:包含两个项目的解决方案

  4. 现在我们已经设置了包含两个项目的解决方案。您可以在图 3中看到每个项目中的引用。在本章中,我们将向两个项目添加更多引用。接下来,我们将安装测试框架,首先是 NUnit,然后是 FsCheck。

安装 NUnit 和 FsUnit 框架

在本节中,我们将讨论如何安装 NUnit 和 FsUnit,并如何使用 F# Interactive 来验证一切是否按预期工作。

在 Visual Studio 2012 中安装 NUnit 和 FsUnit,按照以下步骤操作:

  1. 通过导航到视图 | 其他窗口 | 包管理器控制台,打开包管理器控制台。

  2. 在下拉菜单中选择TradingSystem.Tests作为默认项目。

  3. 输入Install-Package NUnit

  4. 输入Install-Package FsUnit

  5. 输入Add-BindingRedirect TradingSystem.Tests。下图显示了前述步骤的结果:安装 NUnit 和 FsUnit 框架

    最后一条命令确保Fsharp.Core是最新的,如果需要,它还会更新App.config

  6. 让我们在 F# Interactive 中尝试使用 NUnit 框架,了解它是什么,并检查一切是否已正确设置。将以下代码添加到 F#脚本文件中:

    #r @"[...]\TradingSystem\packages\FsUnit.1.2.1.0\Lib\Net40\FsUnit.NUnit.dll";;
    #r @"[...]\TradingSystem\packages\NUnit.2.6.2\lib\nunit.framework.dll";;
    
    open NUnit.Framework
    open FsUnit
    
    1 |> should equal 1
    
  7. 请注意,您需要找到这两个 DLL 的路径,因为它在不同的计算机上会有所不同。只需进入项目中的引用TradingSystem.Tests),在解决方案资源管理器中单击特定的框架,然后完整路径将在属性窗口中更新。使用此完整路径替换前面代码中的路径,两个 DLL 都要这样操作。

    最后,您可以测试前面的代码:

    > 1 |> should equal 1;;
    val it : unit = ()
    

这意味着我们的第一个测试框架已成功设置。接下来,我们将查看如何在代码中添加一些测试并通过 NUnit 运行它们。

正在连接到 Microsoft SQL Server

本章假设你已经在本地机器上运行了 Microsoft SQL Server 实例,或者它作为 Visual Studio 2012 的一部分安装,或者你在远程机器上有访问权限,如下图所示:

连接到 Microsoft SQL Server

连接到 Microsoft SQL Server 的步骤如下:

  1. 导航到视图 | 服务器资源管理器

  2. 右键点击数据连接,然后选择添加连接

  3. 选择Microsoft SQL Server (SqlClient) 作为数据源

  4. 如果你在本地安装了 Microsoft SQL Server,请选择本地机器。

  5. 选择使用 Windows 身份验证

  6. 指定数据库的名称。从现在开始,我们将把我们的数据库称为TradingSystem

  7. 为了测试设置是否成功,在左下角按下测试连接

  8. 确定

  9. 然后你会遇到一个对话框,询问你是否要创建它,按

  10. 现在我们已经有了项目所需的数据库,接下来我们将添加项目所需的表。为此,打开视图 | SQL Server 对象资源管理器。它将显示如下截图:连接到 Microsoft SQL Server

  11. 为了添加我们的表,我们将使用下一步中提供的 SQL 代码片段。右键点击TradingSystem数据库,然后点击新建查询...。粘贴以下代码来创建Log表。

  12. 以下是创建Log表的 SQL 代码:

    DROP TABLE LOG
    CREATE TABLE LOG
    (
      log_id int IDENTITY PRIMARY KEY,
      log_datetime datetime DEFAULT CURRENT_TIMESTAMP,
      log_level nvarchar(12) DEFAULT 'info',
      log_msg ntext
    )
    
  13. 这将在编辑器下方的 SQL 终端输出以下内容:

    Command(s) completed successfully.
    

我们将在接下来的类型提供程序和 LINQ 章节中使用这个表。

引入类型提供程序

类型提供程序是处理 XML 文档、SQL 数据库和 CSV 文件中结构化数据的强大方式。它们将 F# 的类型系统与结构化数据结合起来,而在静态类型语言中,这通常是一个繁琐的任务。通过使用类型提供程序,数据源的类型会自动转换为本地类型;这意味着数据会使用与数据源中相同的字段名进行解析和存储。这使得 Visual Studio 和 IntelliSense 可以帮助你编写代码,而无需每次都去数据源中查找字段名。

使用 LINQ 和 F#

LINQ 是 .NET 框架中的一项特性,自 Version 3.0 以来就得到了 F# 的支持。它用于提供强大的查询语法,可以与数据库、XML 文档、.NET 集合等一起使用。在本节中,我们将简要介绍 LINQ,并看看如何将它与 SQL 数据库一起使用。但首先,我们将先看一下 LINQ 与集合的配合使用:

  1. 首先我们需要一个值的列表:

    let values = [1..10]
    
  2. 然后我们可以构建我们的第一个 LINQ 查询,它将选择列表中的所有值:

    let query1 = query { for value in values do select value }
    query1 |> Seq.iter (fun value -> printfn "%d" value)
    
  3. 这并不是很有用,所以我们添加一个条件。我们现在选择所有大于5的值,这可以通过 where 子句来实现:

    let query2 = query { for value in values do
                            where (value > 5)
                            select value }
    query2 |> Seq.iter (fun value -> printfn "%d" value)
    
  4. 我们可以添加另一个条件,选择大于5且小于8的值:

    let query3 = query { for value in values do
                            where (value > 5 && value < 8)
                            select value }
    query3 |> Seq.iter (fun value -> printfn "%d" value)
    

这非常强大,特别是在处理来自 SQL 数据库等的数据时更为有用。在下一节中,我们将查看一个结合使用类型提供程序和 LINQ 的示例应用程序,用于插入和查询数据库。

解释使用类型提供程序和 LINQ 的示例代码

在本节中,我们将查看一个使用类型提供程序和 LINQ 的示例应用程序。我们将使用之前创建的 Log 表。这是测试 SQL 数据库及其读写权限是否正常工作的好方法:

open System
open System.Data
open System.Data.Linq
open Microsoft.FSharp.Data.TypeProviders
open Microsoft.FSharp.Linq

#r "System.Data.dll"
#r "FSharp.Data.TypeProviders.dll"
#r "System.Data.Linq.dll"

连接字符串在某些情况下需要更改;如果是这样,只需检查数据库属性中的 Connection 字符串值:

/// Copied from properties of database
type EntityConnection = SqlEntityConnection<ConnectionString="Data Source=(localdb)\Projects;Initial Catalog=TradingSystem;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False",Pluralize = true>
let context = EntityConnection.GetDataContext()

/// Format date according to ISO 8601
let iso_8601 (date:Nullable<DateTime>) =
    if date.HasValue then
        date.Value.ToString("s")
    else "2000-00-00T00:00:00"    

我们将使用 LINQ 来查询我们的表格。代码将在最终应用程序中进行修改和使用。这里有一个查询,它列出了 Log 表中的所有条目:

/// Query all LOG entries
let listAll() =
    query { for logentry in context.LOGs do
            select logentry }
    |> Seq.iter (fun logentry -> printfn "%s -- %d -- %s -- %s" (iso_8601 logentry.log_datetime) logentry.log_id logentry.log_level logentry.log_msg)

/// Insert a LOG entry
let addEntry(logLevel, logMsg) =
    let fullContext = context.DataContext
    let logTimestamp = DateTime.Now
    let newEntry = new EntityConnection.ServiceTypes.LOG(log_level = logLevel,
    log_msg = logMsg,
    log_datetime = Nullable(logTimestamp))
    fullContext.AddObject("LOGs", newEntry)
    fullContext.SaveChanges() |> printfn "Saved changes: %d object(s) modified."

我们可以使用 F# Interactive 来查看代码是否有效,通过添加日志条目并尝试获取它:

> addLogEntry("INFO", "Just a simple log entry");;
Saved changes: 1 object(s) modified.
val it : unit = ()

> listAll();;
2013-09-26T14:08:02 -- 1 -- INFO -- Just a simple log entry
val it : unit = ()

这似乎运行得不错,我们可以认为 SQL Server 设置已完成。接下来的两章我们将添加表格和功能。但目前这样已经可以了,设置已完成。

在 Visual Studio 中浏览数据库表格的数据也很方便。为此,右键点击 SQL Server 对象资源管理器 中的 Log 表,然后选择 查看数据。你将看到类似于以下截图的视图:

解释使用类型提供程序和 LINQ 的示例代码

创建项目中剩余的表格

SQL 数据库将用于存储我们系统的状态和历史记录,主要用于说明性目的。我们将存储日志信息和交易历史。为此,我们需要添加一个新表,方法与之前相同。

CREATE TABLE TRADEHISTORY
(
  tradehistory_id int IDENTITY PRIMARY KEY,
  tradehistory_datetime datetime DEFAULT CURRENT_TIMESTAMP,
  tradehistory_instrument nvarchar(12),
  tradehistory_qty int,
  tradehistory_type nvarchar(12),
  tradehistory_price float
)

如果你执行上述 SQL 代码,现在可以看到该表已添加到 SQL Server 对象资源管理器 中的 表格视图,如以下截图所示:

为我们的项目创建剩余的表格

沿着我们之前的步骤,我们将检查示例代码来查询和插入一条交易历史记录:

/// Query trading history
let listAllTrades() =
    query { for trade in context.TRADEHISTORies do select trade }
    |> Seq.iter (fun trade -> printfn "%A" (iso_8601 trade.tradehistory_datetime, trade.tradehistory_id, trade.tradehistory_instrument, trade.tradehistory_type, trade.tradehistory_price, trade.tradehistory_qty))

/// Insert a trade
let addTradeEntry(instrument, price, qty, otype) =
    let fullContext = context.DataContext
    let timestamp = DateTime.Now
    let newEntry = new EntityConnection.ServiceTypes.TRADEHISTORY(tradehistory_instrument = instrument,
                               tradehistory_qty = Nullable(qty),
                               tradehistory_type = otype,
                               tradehistory_price = Nullable(price),
                               tradehistory_datetime = Nullable(timestamp))
    fullContext.AddObject("TRADEHISTORies", newEntry)
    fullContext.SaveChanges() |> printfn "Saved changes: %d object(s) modified."

我们可以使用 F# Interactive 来查看代码是否有效,通过添加交易条目并尝试获取它:

> addTradeEntry("MSFT", 45.50, 100, "limit")
Saved changes: 1 object(s) modified.
val it : unit = ()

> listAllTrades()
("2013-11-24T20:40:57", 1, "MSFT", "limit", 45.5, 100)
val it : unit = ()

看起来运行得不错!

编写测试用例

在本节中,我们将查看可以为交易系统编写的一些测试用例。我们将结合 NUnit 和 NUnit 提供的图形用户界面来完成这项工作。以下截图显示了 NUnit 的主界面:

编写测试用例

图 4:NUnit 用户界面

在我们开始编写系统的真正测试之前,我们会编写一个简单的测试来验证我们的设置是否正确。NUnit 会在每次构建时自动重新运行可执行文件。我们首先在 TestOrderValidation 文件中编写一个简单的测试,然后再编写真正的测试:

[<Test>]
let OneIsEqualToOne() =
    1 |> should equal 1

这看起来很傻,但我们将能看到 NUnit 是否能检测到变化,且 NUnit 是否能检测到 .exe 文件中的测试用例。编写简单测试用例的步骤如下:

  1. 打开NUnit,然后导航到文件 | 打开项目...

  2. 选择 TradingSystem.Tests 中对应的 .exe 文件,该文件位于..\visual studio 2012\Projects\TradingSystem\TradingSystem.Tests\bin\Debug

  3. 按下运行按钮,应该会显示如下图:编写测试用例

    图 5:成功运行示例单元测试时的 NUnit

现在我们知道设置是正确的,让我们开始为我们的订单验证代码编写一些真正的单元测试:

namespace TradingSystem.Tests

open System
open NUnit.Framework
open FsUnit

open TradingSystem.Orders

module OrderTests =
    [<Test>]
    let ValidateBuyOrder() =
        let buyOrder = Order(OrderSide.Buy, OrderType.Limit, 54.50, Tif.FillorKill, 100, "MSFT", 0.0)
        validateOrder (Right buyOrder) |> should equal (Right buyOrder)

现在我们需要构建项目,然后 NUnit 应该能够检测到变化。如果一切正常,NUnit 图形界面应该显示如下截图:

编写测试用例

图 6:运行 ValidateBuyOrder 测试时的 NUnit

让我们为订单验证代码添加一些更多的测试:

module ValidateOrderTests =
    [<Test>]
    let ValidateBuyOrder() =
        let buyOrder = Order(OrderSide.Buy, OrderType.Limit, 54.50, Tif.FillorKill, 100, "MSFT", 0.0)
        validateOrder (Right buyOrder) |> should equal (Right buyOrder)

    [<Test>]
    let ValidateOrderNoPrice() =
        let buyOrderNoPrice = Order(OrderSide.Buy, OrderType.Limit, 0.0, Tif.FillorKill, 100, "MSFT", 0.0)
        validateOrder (Right buyOrderNoPrice) |> should equal (Left "Limit orders must have a price > 0")

    [<Test>]
    let ValidateStopLimitNoPrice() =
        let stopLimitNoPrice = Order(OrderSide.Buy, OrderType.StopLimit, 0.0, Tif.FillorKill, 100, "MSFT", 45.50)
        validateOrder (Right stopLimitNoPrice) |> should equal (Left "Stop limit orders must both price > 0 and stop price > 0")

    [<Test>]
    let ValidateStopNoPrice() =
        let stopNoPrice = Order(OrderSide.Buy, OrderType.Stop, 0.0, Tif.FillorKill, 100, "MSFT", 45.50)
        validateOrder (Right stopNoPrice) |> should equal (Right stopNoPrice)

请注意,模块名称已更改为ValidateOrderTests。我们将为验证工具和使用预交易风险的功能添加更多测试,所有测试都将在同一文件中。以下截图显示了验证订单的四个测试:

编写测试用例

NUnit 当运行四个测试时

设置的详细信息

以下是到目前为止在项目中使用的引用列表。使用此列表检查你的项目是否是最新的:

TradingSystemFSharp.Core

  • FSharp.Data.TypeProviders

  • mscorlib

  • System

  • System.Core

  • System.Data

  • System.Data.Linq

  • System.Numerics

TradingSystem.Tests

  • Fsharp.Core

  • FsUnit.NUnit

  • TradingSystem.Core

  • FSharp.Data.TypeProviders

  • mscorlib

  • nunit.framework

  • System

  • System.Core

  • System.Numerics

摘要

在本章中,我们设置了项目,项目由交易系统本身和测试组成。我们学习了如何使用 Visual Studio 处理我们的解决方案,并将其与 NUnit 和 FsUnit 集成。我们还了解了如何连接到 Microsoft SQL Server,以及如何在 LINQ 中编写查询并通过类型提供程序检索数据。

在接下来的两章中,我们将继续开发从本章构建的基础上的交易系统。

第九章:通过交易波动性获利

在本章中,我们将研究各种波动率交易策略。我们将涵盖方向性波动率交易和相对价值波动率套利。我们还会简要涉及期权和收益图,并使用 F#和 FSharpChart 来可视化它们。在本章中,您将学习:

  • 交易波动率

  • 波动率套利机会

  • 获取和计算策略所需的数据

  • 推导波动率套利背后的数学原理

交易波动性

交易波动性类似于交易大多数其他资产,不同之处在于波动率不能直接交易。波动率是通过期权、期货和 VIX 指数等方式间接交易的。因为波动率是资产的内在价值,所以不能直接交易。为了能够交易波动率,通常需要通过衍生品及其标的资产的对冲头寸或期权头寸来实现。

波动率交易通常分为两类:方向性交易和相对价值交易。方向性波动率交易意味着我们根据波动率的方向进行交易。如果波动率很高,我们可能会开始做空波动率。相对价值交易意味着我们发起两个交易,例如,我们在一个看涨期权中做多,在另一个看涨期权中做空。第一个看涨期权在波动率上可能被低估,而另一个则可能被略微高估。这两种相关资产被认为会回归均值,利润将被兑现。

在本章中,我们将简要介绍波动率交易及其盈利方式。

使用 FSharpCharts 绘制收益图

在本节中,我们将构建欧洲看涨期权和看跌期权的基本收益图。收益图有助于根据股票价格和期权的行权价格可视化理论收益。

看涨期权的收益函数定义如下:

使用 FSharpCharts 绘制收益图

看跌期权的收益函数定义如下:

使用 FSharpCharts 绘制收益图

让我们看看如何在 F#中实现这一点。我们首先定义看涨期权和看跌期权的收益函数:

/// Payoff for European call option
// s: stock price
// k: strike price of option
let payoffCall k s = max (s-k) 0.0

/// Payoff for European Put option
// s: stock price
// k: strike price of option
let payoffPut k s = max (k-s) 0.0

我们可以使用这些函数生成数据,输入 FSharpChart 并可视化数据:

/// Calculate the payoffs given payoff function
let payoff payoffFunction = [ for s in 0.0 .. 10.0 .. 100.0 -> s, payoffFunction s ]

我们从生成行权价格为50.0的看涨期权的收益图开始:

let callPayoff = payoff (payoffCall 50.0)
Chart.Line(callPayoff).WithTitle("Payoff - Call Option")

使用 FSharpCharts 绘制收益图

显示看涨期权的收益图

在前面的图表中,我们可以看到看涨期权的收益在股票价格达到50(期权的行权价格)之前为0。从那里开始,收益逐渐增加。我们仅绘制0100之间的值,因此此图中的最大收益为50

对于行权价格为50.0的看跌期权,过程是相同的:

let putPayoff = payoff (payoffPut 50.0)
Chart.Line(putPayoff).WithTitle("Payoff - Put Option")

使用 FSharpCharts 绘制收益图

显示看跌期权的收益图

看跌期权的收益图与我们之前看到的相反。在前面的图表中,收益将会下降,直到在期权的行权价时为零。这意味着,看跌期权在股票价格低于期权行权价时会盈利。

最后,我们创建一个组合图表:

/// Combined chart of payoff for call and put option
let chart = Chart.Combine [Chart.Line(callPayoff); Chart.Line(putPayoff).WithTitle("Payoff diagram")]

使用 FSharpCharts 绘制收益图

图 3:组合收益图

学习方向性交易策略

在波动性中进行方向性交易意味着根据波动性的方向进行交易。如果波动性较高,我们可能会启动一个空头波动性交易。在本节中,我们将首先看看如何使用期权策略进行波动性交易。然后,我们将使用期权和基础资产价格来交易波动性,进一步探讨 VIX 指数和德尔塔中性头寸。

使用期权交易波动性

一种交易波动性的方法是使用期权。我们将看看两种期权策略,用于交易波动性或基础期权的价格变化。

交易跨式期权

跨式期权头寸由两种期权组成:一份看跌期权和一份看涨期权。跨式期权适用于当你对基础市场的观点是中立时,也就是说,不对市场的长期走势进行投机。它还意味着跨式期权头寸适用于当你希望交易波动性时,无论市场的走势如何。

多头跨式期权

多头跨式期权交易是通过同时持有一个看涨期权和一个看跌期权的多头头寸,且两者具有相同的行权价和到期日。多头跨式期权适用于当你认为波动性较低,并且希望利用波动性潜在上升来获利时。

这个思路是,两种期权(看涨和看跌期权)将抵消对基础市场的暴露,除了期权基础资产的波动性。也就是说,跨式期权对波动性的变化非常敏感。更技术地说,它们各自的德尔塔接近 0.5 和-0.5,这意味着它们相互抵消。因为货币期权的德尔塔大约为看涨期权的 0.5 和看跌期权的-0.5。

让我们看看一些代码,来实现多头跨式期权的收益函数:

/// Payoff for long straddle
// s: stock price
// k: strike price of option
let longStraddle k s = 
    (payoffCall k s) +
    (payoffPut k s)

Chart.Line(payoff (longStraddle 50.0)).WithTitle("Payoff - Long straddle")

以下截图展示了多头跨式期权的收益函数线图:

多头跨式期权

组合收益图

空头跨式期权

空头跨式期权是多头跨式期权的对立面。在这里,我们通过对一个看涨期权和一个看跌期权进行空头操作,且这两者具有相同的行权价和到期日,来创建这个头寸。空头跨式期权用于交易资产波动性的下降,而不暴露于市场的其他方面。

我们空头跨式期权的收益函数代码是通过添加两个空头头寸来获得的(注意在(payoffCall k s)前有一个负号):

/// Payoff for Short straddle
// s: stock price
// k: strike price of option
let shortStraddle k s = 
    -(payoffCall k s) +
    -(payoffPut k s)

Chart.Line(payoff (shortStraddle 50.0)).WithTitle("Payoff - Short 
    straddle")

以下截图展示了空头跨式期权的收益函数线图:

空头跨式期权

组合收益图

交易蝶式价差

蝶式价差由三个腿组成,并有两种形式:长蝶式价差和短蝶式价差。

长蝶式价差

长蝶式头寸是通过卖出两个平值看涨期权并买入两个看涨期权来创建的:一个实值看涨期权和一个远期看涨期权。这两个看涨期权将作为卖空头寸的保险。

总结来说,你需要:

  • 卖空两个平值看涨期权

  • 买入一个实值看涨期权

  • 买入一个远期看涨期权

我们可以将前面的规则表示为 F#代码来生成盈亏:

/// Payoff for long butterfly
// s: stock price
// h: high price
// l: low price
let longButterfly l h s = 
    (payoffCall l s) +
    (payoffCall h s) -
    2.0 * (payoffCall ((l + h) / 2.0) s)

Chart.Line(payoff (longButterfly 20.0 80.0)).WithTitle("Payoff - Long butterfly")

这段代码将生成一个图表,显示长蝶式的盈亏:

长蝶式价差

长蝶式价差的盈亏图

短蝶式价差

短蝶式头寸是通过买入两个平值看涨期权并卖出一个实值看涨期权和一个远期看涨期权来创建的。这两个看涨期权将作为卖空头寸的保险。

总结来说,你必须:

  • 买入两个平值(或两个其他行权价中间的)看涨期权

  • 卖出一个实值看涨期权

  • 卖出一个远期看涨期权

我们可以将前面的规则表示为 F#代码来生成盈亏:

/// Payoff for short butterfly
// s: stock price
// h: high price
// l: low price
let shortButterfly l h s = 
    -(payoffCall l s) +
    -(payoffCall h s) -
    2.0 * -(payoffCall ((l + h) / 2.0) s)

Chart.Line(payoff (shortButterfly 20.0 80.0)).WithTitle("Payoff - Short butterfly")

这段代码将生成以下图表,显示短蝶式的盈亏:

短蝶式价差

短蝶式价差的盈亏图

交易 VIX

另一种选择,如果你有兴趣使用方向性交易策略进行波动率交易,是交易 VIX 指数。VIX 是一个结合了标准普尔 500 指数期权隐含波动率的指数。它可以被解释为未来 30 天波动率的预测。VIX 的预测能力与标准普尔 500 指数本身的历史回报相当。这意味着,VIX 提供的信息并不是波动率预测的灵丹妙药;它更适合用于波动率的方向性交易。以下是 VIX 指数与移动平均指标的图表截图:

交易 VIX

从 2000 年 1 月 1 日到 2013 年 11 月 1 日的 VIX 指数

交易德尔塔中性组合

德尔塔中性组合是通过期权和标的资产构建的。理论上,组合对标的价格的小幅变化(德尔塔中性)有抵抗力。另一方面,其他因素会改变组合的价值;这意味着我们必须定期进行再平衡。

在本节中,我们将主要扩展对德尔塔中性组合的分析,并研究如何利用它进行波动率交易。

以下图表显示了一个资产的实际波动率和隐含波动率:

交易德尔塔中性组合

一个资产的实际波动率和隐含波动率

在 Delta 对冲头寸中,我们应该使用哪种波动率?我们有两个选择,要么使用实际波动率,要么使用隐含波动率。

这实际上是一个相当棘手的问题,如果不研究两种波动率的市值MTM)盈亏,很难回答。简而言之,我们可以使用实际波动率,然后进行随机漫步,直到锁定的利润实现,即两个概率分布交叉。另一种选择是使用隐含波动率。使用隐含波动率将产生一个更合理的盈亏曲线,这通常从风险管理的角度来看更为可取,因为随机项在一定程度上得到了减少,利润将随着时间逐步实现,直到完全实现。

推导数学公式

在本节中,我们将研究交易 delta 中性投资组合所需的数学。

以下表格展示了市场中性投资组合的值:

推导数学公式

以下表格展示了下一天市场中性投资组合的值:

推导数学公式

使用隐含波动率对冲

在本节中,我们将推导出使用隐含波动率对冲的数学工具,以便能够监控市值盈亏。

以下是从当前日期到下一日期的市值盈亏:

使用隐含波动率对冲

这里,S 是股票价格,Γ 是 Black-Scholes 伽玛函数。

以下是直到套利交易结束时的理论利润:

使用隐含波动率对冲

我们将每一笔利润的折现值整合,直到交易结束,从而得到总的理论利润。

实现数学公式

使用 Math.NET,让我们实现前一节推导出的数学公式,感受公式与 F#之间的紧密联系:

/// Mark-to-market profit

/// Normal distribution
open MathNet.Numerics.Distributions;
let normd = new Normal(0.0, 1.0)

/// Black-Scholes Gamma
// s: stock price
// x: strike price of option
// t: time to expiration in years
// r: risk free interest rate
// v: volatility
let black_scholes_gamma s x t r v =
let d1=(log(s / x) + (r+v*v*0.5)*t)/(v*sqrt(t))
normd.Density(d1) / (s*v*sqrt(t))

let mark_to_market_profit s,x,t,r,v,vh = 0.5*(v*v - vh*vh)*S*S*gamma(s,x,t,r,v)

学习相对价值交易策略

相对价值波动率交易是指通过使用一些金融工具的对立头寸(如期权)来进行波动率交易,从中利用波动率的变化。通常,交易者会通过买入一个看涨期权并卖出一个看涨期权来进行交易,形成一个双腿交易。这类交易有很多变种,我们将主要关注通过期权交易波动率微笑的斜率。这将构成本书所用交易策略的基础。

交易微笑的斜率

首先,我们将回顾第六章,探索波动率,我们在该章节中研究了瑞典 OMX 交易所期权的微笑效应。波动率微笑是股市中观察到的一种现象。通过将期权的隐含波动率绘制在 y 轴上,将期权的内在价值绘制在 x 轴上,可以得到波动率微笑。

金融含义是标的资产现货价格S与期权执行价格K之间的比率:

交易微笑曲线的斜率

在下面的截图中,你将看到金利率 M 位于 x 轴,隐含波动率位于 y 轴:

交易微笑曲线的斜率

波动率微笑

前面的图表的一个缺点是我们在同一图表中绘制了多个到期日。我们需要改进这种方法,以便能更详细地研究波动率微笑。作为第一步,我们来修改代码:

open MathNet.Numerics
open MathNet.Numerics.LinearAlgebra
open MathNet.Numerics.LinearAlgebra.Double
open MathNet.Numerics.Distributions

let filePath = @"C:\Users\Gecemmo\Desktop\smile_data.csv"

/// Split row on commas
let splitCommas (l:string) =
    l.Split(',')

/// Read a file into a string array
let openFile (name : string) =
    try
        let content = File.ReadAllLines(name)
        content |> Array.toList
    with
        | :? System.IO.FileNotFoundException as e -> printfn "Exception! %s " e.Message; ["empty"]

第一个修改是在readVolatilityData函数中进行的,添加了一个date参数。该参数用于从 CSV 文件中过滤出与日期匹配的行:

/// Read the data from a CSV file and returns
/// a tuple of strike price and implied volatility%
// Filter for just one expiration date
let readVolatilityData date =
    openFile filePath
    |> List.map splitCommas
    |> List.filter (fun cols -> cols.[1] = date)
    |> List.map (fun cols -> (cols.[2], cols.[3]))

以下代码与我们之前使用的相同,但在下一步中,我们需要做一些小的修改:

/// 83.2
/// Calculates moneyness and parses strings into numbers
let calcMoneyness spot list =
    list
    |> List.map (fun (strike, imp) -> (spot / (float strike), (float imp)))

// Filter out one expiration date -- 2013-12-20
let list = readVolatilityData "2013-12-20"
let mlist = calcMoneyness 83.2 list

/// Plot values using FSharpChart
fsi.AddPrinter(fun (ch:FSharp.Charting.ChartTypes.GenericChart) -> ch.ShowChart(); "FSharpChartingSmile")    
Chart.Point(mlist)

以下是绘制数据点以及获得回归线的代码。正如我们在图表中所看到的,回归结果并不理想:

let xdata = mlist |> Seq.map (fun (x, _) -> x) |> Seq.toList
let ydata = mlist |> Seq.map (fun (_, y) -> y) |> Seq.toList

let N = xdata.Length
let order = 2

/// Generating a Vandermonde row given input v
let vandermondeRow v = [for x in [0..order] do yield v ** (float x)]

/// Creating Vandermonde rows for each element in the list
let vandermonde = xdata |> Seq.map vandermondeRow |> Seq.toList

/// Create the A Matrix
let A = vandermonde |> DenseMatrix.ofRowsList N (order + 1)
A.Transpose()

/// Create the Y Matrix
let createYVector order l = [for x in [0..order] do yield l]
let Y = (createYVector order ydata |> DenseMatrix.ofRowsList (order + 1) N).Transpose()

最后的步骤是计算回归系数,并使用这些系数计算曲线上的点。然后,我们将像在第六章中那样,使用点和拟合曲线的组合图:

/// Calculate coefficients using least squares
let coeffs = (A.Transpose() * A).LU().Solve(A.Transpose() * Y).Column(0)

let calculate x = (vandermondeRow(x) |> DenseVector.ofList) * coeffs

let fitxs = [(Seq.min xdata).. 0.01 ..(Seq.max xdata)]
let fitys = fitxs |> List.map calculate
let fits = [for x in [(Seq.min xdata).. 0.01 ..(Seq.max xdata)] do yield (x, calculate x)]

let chart = Chart.Combine [Chart.Point(mlist); Chart.Line(fits).WithTitle("Volatility Smile - 2nd degree polynomial")]

交易微笑曲线的斜率

2013 年 1 月 20 日到期的波动率微笑

相反,我们可以尝试拟合三次多项式并评估图形。我们只需将order值更改为2

let order = 2

交易微笑曲线的斜率

使用三次多项式拟合同一到期日的波动率微笑

这次的结果更有说服力。正如我们所看到的,不同期权之间确实存在一些不一致性。但这并不一定意味着在这种情况下存在套利机会。

我们如何利用相同到期日的期权波动率的不一致性呢?一种方法是研究隐含波动率随时间变化的走势。如果波动率在某种程度上是均值回归的,该回归会如何影响?

首先,我们将聚焦于问题和期权集合。x 轴表示期权的金利率。在第一次实验中,我们将研究金利率在0.501.5之间的范围。

我们编写了一些 F#代码来帮助我们:

// Filter on moneyness, 0.5 to 1.5
let mlist = calcMoneyness 83.2 list |> List.filter (fun (x, y) -> x > 0.5 && x < 1.5)

这只是对mlist的赋值进行修改,过滤x值。经过这种过滤和对二次多项式的调整,生成了以下图表:

交易微笑曲线的斜率

使用二次多项式拟合金利率在 0.5 到 1.5 之间的波动率微笑

我们假设斜率会发生变化,并且斜率会在某种程度上回归均值,这意味着我们可以在期权受该波动影响时,采取一个多头和一个空头的仓位。

定义交易策略

我们系统的交易策略将基于先前描述的相对价值波动率套利。这将使我们能够专门使用期权进行交易,具体来说,是使用价内的看涨期权。

首先,我们定义内在价值的“边缘”之间的斜率:内在价值的上限和下限。为了做这个,我们需要查看一个图表。对于前面的图表,通常是[0.5, 1.0]

为了得到更为数学化的斜率表达式,我们查看两个点并计算它们之间的斜率:

定义交易策略

这里,m是内在价值,σ(sigma)是从期权价格中得到的隐含波动率。斜率可以上升或下降,这意味着β会增加、减少,或者当然,两者都不会发生。让我们更仔细地看一下这两种情况。

案例 1 – 斜率增加

在斜率低于回归线(平均值)的情况下,我们可以假设斜率最终会回归。在斜率上升的情况下,斜率如下:

案例 1 – 斜率增加

这导致以下不等式,其中在时间0时的合成波动性低于未来某个时刻的波动性:

案例 1 – 斜率增加

我们可以通过创建一个多头看涨期权和一个空头看涨期权的组合来交易这个斜率增加的情况。波动性上升的差异将导致潜在的利润。这意味着我们需要考虑这两个期权的 Vega。如果与案例 1 – 斜率增加相关的期权的 Vega 比与案例 1 – 斜率增加相关的期权的 Vega 更高,那么这个头寸可能会亏损。

案例 2 – 斜率减小

就像在斜率增加的情况下一样,斜率减小的情况也一样成立。我们可以假设斜率将在某个稍后的时间点回归。这意味着时间点 1(t1)处的斜率减去时间点 0(t0)处的斜率小于零:

案例 2 – 斜率减小

这导致以下不等式,其中在时间点 0 时的合成波动性大于未来某个时刻的波动性:

案例 2 – 斜率减小

该交易通过一个空头看涨期权和一个多头看涨期权来启动。

定义进入规则

系统的进入规则将是:

  • 每当β的斜率小于回归线的斜率时,我们按照案例 1 启动交易。

  • 每当β的斜率大于回归线的斜率时,我们按照案例 2 启动交易。

定义退出规则

当案例 1 或 2 中的任何一个不再成立时,交易将会被平仓。这意味着斜率已经发生反转,我们可能会亏损。我们还会增加一个时间约束,规定交易的持续时间不能超过两天。这个时间限制当然可以调整,但这种行为通常适用于日内交易。

我们将在下一章实现此处定义的规则,在那里我们将整合各个部分,通过使用期权构建一个完整的交易系统。

摘要

在本章中,我们详细探讨了构建交易策略所需的理论。我们推导了一些在波动率交易中使用的数学工具,并讨论了这些工具如何应用于交易系统。一些概念已经在前面的章节中介绍过,这里仅作了稍微修改的版本。在下一章中,我们将把这些内容整合在一起,看看如何在图形用户界面(GUI)中展示交易系统的数据。

第十章:将各个部分组合在一起

本章介绍构建自动化交易系统的最后一步。我们将讨论如何重构系统,并根据新的需求进行修改。

本章内容包括:

  • 执行交易策略

  • 在 GUI 中展示信息

  • 交易系统的可能扩展

理解需求

我们在第八章《设置交易系统项目》中已介绍了一些要求,但让我们再次回顾它们,并看看系统将如何定义。自动化交易系统的关键在于,它需要能够处理市场数据,并基于这些数据做出决策。这些决策将被转换为 FIX 4.2 消息,并发送到 FIX 模拟器、实际的经纪商或股票交易所。在这个相对简单的设置中,市场数据将是来自 Yahoo! Finance 的每日数据,将每天下载并解析。

自动化交易系统应该能够:

  • 将日志条目存储在 Microsoft SQL Server 数据库中

  • 将交易历史存储在 Microsoft SQL Server 数据库中

  • 从 Yahoo! Finance 下载报价

  • 使用订单管理系统OMS)管理订单

  • 使用 FIX 4.2 发送订单

  • 通过 FIX 4.2 将交易系统连接到 FIX 模拟器

  • 执行用 F#编写的交易策略

  • 使用基本的 GUI 控制自己,带有开始/停止按钮

  • 显示当前持仓

  • 显示当前的盈亏P&L

  • 显示最新的报价

  • 使用 MVC 模式和INotifyPropertyChanged接口

以下是说明交易系统中数据流的图示:

理解需求

交易系统中的数据流

重新审视系统结构

我们将重新审视项目结构,并确保所有依赖项都已添加。以下是自动化交易系统的组成部分:

  • 数据源处理器和市场数据适配器

  • 交易策略

  • 订单执行与订单管理

  • 持久化层(数据库)

  • 用于监控系统的 GUI

我们需要两个新的依赖项。它们如下:

  • System.Windows.Forms

  • System.Drawing

我们需要System.Windows.Forms依赖项来创建我们的 GUI。它为 Windows 本身以及使用的控件提供支持。System.Drawing依赖项也需要提供基本的图形功能。以下是项目中所需引用的列表。您可以根据这个列表验证您自己的项目,以确保您拥有所有必要的依赖项。

交易系统被分为两个项目:TradingSystemTradingSystem.Tests

以下是TradingSystem所需的依赖项列表:

  • FSharp.Core

  • FSharp.Data.TypeProviders

  • mscorlib

  • NQuantLib

  • System

  • System.Core

  • System.Data

  • System.Data.Linq

  • System.Drawing

  • System.Numerics

  • System.Windows.Forms

以下是TradingSystem.Tests所需的依赖项列表:

  • Fsharp.Core

  • FsUnit.NUnit

  • TradingSystem.Core

  • FSharp.Data.TypeProviders

  • mscorlib

  • nunit.framework

  • System

  • System.Core

  • System.Numerics

理解模型-视图-控制器模式

在这一部分,我们将探讨 MVC 设计模式的概念。MVC 模式是在 Xerox PARC 提出的,并自 Smalltalk 早期就存在。它是一种高层次的设计模式,常用于 GUI 编程。我们稍后会更详细地讲解它,但在此做一个温和的介绍将使你在需要时能够更容易理解这一概念。

MVC 的主要思想是将模型与视图分离。视图仅仅是 GUI,与程序的用户进行交互。GUI 会处理点击的按钮和显示在屏幕上的数据。模型是程序中使用的数据,例如,财务数据。通常,我们希望将模型(数据)和视图(GUI)的代码分开。

前面图中描述的 MVC 模式是传统 MVC 模式的一个修改版本。主要的区别在于,在这种变体中,视图与模型之间没有直接的通信。这是一种更精细的 MVC 模式使用方式,其中视图不需要了解任何关于模型的信息。

模型

模型通常是应用程序的数据和状态。在这个案例中,模型将包含订单、市场数据和系统状态。

视图

视图是TradingSystemForm类,它将是除了标准 Windows 窗体组件外,唯一使用的 GUI 窗体。视图即为 GUI。

控制器

控制器负责将视图与模型连接起来。控制器通过一个模型实例进行初始化,并且视图会在程序执行过程中(Program.fs)添加到控制器中。以下图表是 MVC 模式中各部分关系的示意图:

控制器

MVC 模式,其中控制器同时知道模型和视图

在我们的例子中,控制器将负责在执行操作时更新视图。这意味着模型会通知控制器,然后控制器会更新视图。严格来说,这是经典 MVC 模式的略微修改版本,其中模型了解视图并通知视图,而不是控制器。

经典方法中的主要问题是紧耦合。通过将控制器作为中介,形成了一个复合模式。这是苹果公司流行库Cocoa中使用的相同策略。

使用框架执行交易策略

当数据下载完成/成功时,交易策略通过onMarketData执行(它会向代理发送一条消息)。如果发生任何错误,则会通知代理。所有操作都记录到 SQL 后端(SQL Server)。

交易策略将有六个可调用函数:

  • onInit:当策略初始化时,将调用此函数。

  • onStart:当策略开始时,将调用此函数。

  • onStop:当策略停止时,将调用此函数。

  • onMarketData:每当新的市场数据到达时,都会调用此函数。

  • onTradeExecution:每当执行交易时,都会调用此函数。

  • onError:每当发生错误时,都会调用此函数。

交易策略将作为一个独立的类型来实现,其中回调函数是由策略执行器调用的成员函数。策略执行器由接收来自系统消息的代理组成。开始和停止命令通过与 GUI 中按钮连接的两个事件处理程序发送。

让我们看看用于执行交易策略的框架的主要结构:

/// Agent.fs
namespace Agents

open System

  type TradingStrategy() =
    member this.onInit : unit = printfn "onInit"
    member this.onStart : unit = printfn "onStart"
    member this.onStop : unit = printfn "onStop"
    member this.onTradeIndication : unit = printfn "onTradeIndication"

  // Type for our agent
  type Agent<'T> = MailboxProcessor<'T>

我们需要控制消息用于与代理进行通信,这些消息被建模为标记联合。消息用于改变状态并在系统的各部分之间传递变化。这是必需的,因为代理运行在另一个线程中,传递消息是我们与之通信的方式。以下是一个示例:

// Control messages to be sent to agent
type SystemMessage =
  | Init
  | Start
  | Stop

type SystemState =
  | Created
  | Initialized
  | Started
  | Stopped

TradingAgent 模块将接收控制消息并采取适当的行动。以下是实现功能的代码,使用模式匹配调用交易策略中的相应方法:

module TradingAgent =
let tradingAgent (strategy:TradingStrategy) = Agent.Start(fun inbox ->
  let rec loop state = async {
    let! msg = inbox.Receive()
    match msg with
    | Init ->
    if state = Started then
      printfn "ERROR"
    else
      printfn "Init"
    strategy.onInit
    return! loop Initialized
    | Start ->
    if state = Started then
      printfn "ERROR"
    else
      printfn "Start"
    strategy.onStart
    return! loop Started
    | Stop ->
    if state = Stopped then
      printfn "ERROR"
    else
      printfn "Stop"
    strategy.onStop
    return! loop Stopped
  }
loop Created)

以下是用于控制交易系统的部分 GUI 代码。大部分代码使用 .NET 类,主要来自 Windows Forms 库。

提示

MSDN 上有很多关于 Windows Forms 的优秀资源,网址是:msdn.microsoft.com/en-us/library/ms229601(v=vs.110).aspx

// GUI.fs
namespace GUI

open System
open System.Drawing
open System.Windows.Forms
open Agents

// User interface form
type public TradingSystemForm() as form =
  inherit Form()

  let valueLabel = new Label(Location=new Point(25,15))
  let startButton = new Button(Location=new Point(25,50))
  let stopButton = new Button(Location=new Point(25,80))
  let sendButton = new Button(Location=new Point(25,75))

交易策略将被初始化并传递给代理。在初始化过程中,参数和其他常量值将被传递:

let ts = TradingStrategy()
let agent = TradingAgent.tradingagent ts

let initControls =
  valueLabel.Text <- "Ready"
  startButton.Text <- "Start"
  stopButton.Text <- "Stop"
  sendButton.Text <- "Send value to agent"
  do
    initControls

  form.Controls.Add(valueLabel)
  form.Controls.Add(startButton)
  form.Controls.Add(stopButton)

  form.Text <- "F# TradingSystem"

  startButton.Click.AddHandler(new System.EventHandler
    (fun sender e -> form.eventStartButton_Click(sender, e)))

  stopButton.Click.AddHandler(new System.EventHandler
    (fun sender e -> form.eventStopButton_Click(sender, e)))

  // Event handlers
  member form.eventStartButton_Click(sender:obj, e:EventArgs) =      

开始按钮被按下时,调用此事件处理程序并将两条消息发送给代理:

  agent.Post(Init)
  agent.Post(Start)
  ()

  member form.eventStopButton_Click(sender:obj, e:EventArgs) =
  agent.Post(Stop)
  ()

以下是用于在 Program.fs 中启动应用程序并查看 GUI 的代码:

/// Program.fs
namespace Program

open System
open System.Drawing
open System.Windows.Forms

open GUI

module Main =
  [<STAThread>]
do
  Application.EnableVisualStyles()
  Application.SetCompatibleTextRenderingDefault(false)
  let view = new TradingSystemForm()
  Application.Run(view)

构建 GUI

我们在上一节中使用的 GUI 对于我们的交易应用程序来说还不够,但它展示了如何使用 F# 组合一个 GUI 的基本方法。接下来,我们将添加所需的控件,并准备呈现模型中的信息。以下是一个示意图,显示了控件的位置和 GUI 的整体构思:

构建 GUI

交易系统 GUI 的示意图

我们来看看所需的代码。大部分代码比较直接,遵循上一节中 GUI 所使用的相同规则。DataGridView控件的一些属性已设置为自动调整宽度。标签也是如此,其中AutoSize属性被设置为true。最终的 GUI 将如下所示,参见以下代码之后的屏幕截图:

/// GUI code according to mock
namespace GUI

open System
open System.Drawing
open System.Windows.Forms
open Agents
open Model

open System.Net
open System.ComponentModel
  // User interface form
  type public TradingSystemForm() as form =
    inherit Form()

    let plLabel = new Label(Location=new Point(15,15))
    let plTextBox = new TextBox(Location=new Point(75,15))

    let startButton = new Button(Location=new Point(15,350))
    let stopButton = new Button(Location=new Point(95,350))
    let cancelButton = new Button(Location=new Point(780,350))
    let downloadButton = new Button(Location=new Point(780,15))

    let ordersLabel = new Label(Location=new Point(15,120))
    let dataGridView = new DataGridView(Location=new Point(0,140));

    let initControls =
      plLabel.Text <- "P/L"
      plLabel.AutoSize <- true

      startButton.Text <- "Start"
      stopButton.Text <- "Stop"
      cancelButton.Text <- "Cancel all orders"
      cancelButton.AutoSize <- true
      downloadButton.Text <- "Download data"
      downloadButton.AutoSize <- true

    do
      initControls

    form.Size <- new Size(900,480)

    form.Controls.Add(plLabel)
    form.Controls.Add(plTextBox)
    form.Controls.Add(ordersLabel)

    form.Controls.Add(startButton)
    form.Controls.Add(stopButton)

    form.Controls.Add(cancelButton)
    form.Controls.Add(downloadButton)

    dataGridView.Size <- new Size(900,200)
    dataGridView.RowHeadersWidthSizeMode <- DataGridViewRowHeadersWidthSizeMode.EnableResizing
    dataGridView.AutoSizeColumnsMode <- DataGridViewAutoSizeColumnsMode.AllCells

    form.Controls.Add(dataGridView)

    form.Text <- "F# TradingSystem"

构建 GUI

根据模拟,最终构建的 GUI 代码

在 GUI 中呈现信息

在这一部分,我们将研究如何在 GUI 中呈现定期更新的信息。我们将使用 MVC 模式来更新数据。

在.NET 中,通常在需要通知模型更新时会使用接口INotifyPropertyChanged。在本例中,我们将使用一个DataGridView控件和一个DataSource,其中DataSource由一个实现了INotifyPropertyChanged接口的自定义类型列表组成。

模型的更新由控制器处理,然后从DataSource本身更新 GUI。我们从查看订单列表开始,并展示如何在DataGridView控件中呈现该订单列表。将以下代码添加到GUI.fs文件中:

let initOrderList() =
  let modelList = new BindingList<Model.Order>()
  let buyOrder = Model.Order(Model.OrderSide.Buy, Model.OrderType.Limit, 54.50, Model.Tif.FillorKill, 100, "MSFT", 0.0)            
  modelList.Add(buyOrder)

  dataGridView.DataSource <- modelList
  dataGridView.Size <- new Size(900,200)
  dataGridView.RowHeadersWidthSizeMode <- DataGridViewRowHeadersWidthSizeMode.EnableResizing
  dataGridView.AutoSizeColumnsMode <- DataGridViewAutoSizeColumnsMode.AllCells

另外,在initControls函数下方添加以下函数调用:

initOrderList()

在 GUI 中呈现信息

使用DataGridView并填充订单项的 GUI

如你所见,一些单元格中的内容未按我们希望的方式显示。我们需要为它们添加一个自定义单元格格式化器,指定如何在 GUI 中呈现这些值。

initOrderList函数的末尾添加以下代码行:

dataGridView.CellFormatting.AddHandler(new System.Windows.Forms.DataGridViewCellFormattingEventHandler(fun sender e -> form.eventOrdersGrid_CellFromatting(sender, e)))

然后,我们需要实现eventOrdersGrid_CellFromatting函数,具体如下:

  member form.eventOrdersGrid_CellFromatting(sender:obj, e:DataGridViewCellFormattingEventArgs) =            
  match (sender :?> DataGridView).Columns.[e.ColumnIndex].DataPropertyName with
  | "Status" -> e.Value <- sprintf "%A" modelList.[e.RowIndex].Status
  | "Side" -> e.Value <- sprintf "%A" modelList.[e.RowIndex].Side
  | "Type" -> e.Value <- sprintf "%A" modelList.[e.RowIndex].Type
  | "Tif" -> e.Value <- sprintf "%A" modelList.[e.RowIndex].Tif
  | _ -> ()    

现在,当我们运行程序时,订单项的DataGridView控件将正确格式化单元格,如下图所示:

在 GUI 中呈现信息

使用自定义单元格格式化器的DataGridView的 GUI

为了完善 GUI,我们需要添加更新文本字段和处理按钮点击的功能。我们需要回调函数,从控制器调用这些函数以更新 GUI 中的文本字段:

// Functions to update GUI from controller

let updatePlTextBox(str:string) =
  plTextBox.Text <- str

接下来,我们需要为按钮添加事件处理程序。每个按钮将有其自己的事件处理程序,具体如下:

startButton.Click.AddHandler(new System.EventHandler
  (fun sender e -> form.eventStartButton_Click(sender, e)))

stopButton.Click.AddHandler(new System.EventHandler
  (fun sender e -> form.eventStopButton_Click(sender, e)))

cancelButton.Click.AddHandler(new System.EventHandler
  (fun sender e -> form.eventCancelButton_Click(sender, e)))

downloadButton.Click.AddHandler(new System.EventHandler
  (fun sender e -> form.eventDownloadButton_Click(sender, e)))

// Event handlers
member form.eventStartButton_Click(sender:obj, e:EventArgs) = Controller.startButtonPressed()
  Controller.testUpdateGUI(updateSP500TextBoxPrice)

member form.eventStopButton_Click(sender:obj, e:EventArgs) = Controller.stopButtonPressed()
  Controller.testUpdateGUI(updateSP500TextBoxPrice)

member form.eventCancelButton_Click(sender:obj, e:EventArgs) = Controller.cancelButtonPressed()
  Controller.testUpdateGUI(updateSP500TextBoxPrice)

member form.eventDownloadButton_Click(sender:obj, e:EventArgs) = Controller.downloadButtonPressed(updatePlTextBox, updateSP500TextBoxPrice, updateSP500TextBoxVol, updateVixTextBoxPrice, updateVixTextBoxVol)

添加对下载数据的支持

市场数据将从 Yahoo! Finance 每天拉取;我们将使用收盘价,并从中计算所需的数据。数据将在 GUI 中的下载数据按钮被点击时下载。以下是说明如何通过后台线程处理下载的代码:

let fetchOne(url:string) =
  let uri = new System.Uri(url)
let client = new WebClient()
let html = client.DownloadString(uri)
html

let downloadNewData(url1:string, url2:string) =
  let worker = new BackgroundWorker()
  worker.DoWork.Add(fun args ->
  printfn("starting background thread")
  let data = fetchOne(url)
  printfn "%A" data)
  worker.RunWorkerAsync()

交易系统将遵循这些步骤,从下载过程到数据解析:

  1. 从 Yahoo! Finance 下载数据。

  2. 解析数据并执行计算。

  3. 将数据存储在模型中。

考虑系统可能的扩展

在本节中,我们将讨论对我们已经开发的交易系统可能的扩展。这些想法可以为感兴趣的读者提供灵感。交易系统涉及金融和计算机科学多个领域的主题。这里开发的交易系统相当基础,主要用于示范目的。

改进数据馈送

此处使用的数据馈送并非实际的馈送;它更像是一个数据服务。数据馈送顾名思义:就是数据的馈送。数据馈送会为应用程序提供一个连续的市场数据流,并遵循发布-订阅模式。很难找到提供免费数据的馈送服务商。以下是一些值得关注的替代方案:

支持回测

回测在许多情况下是有用的,最基本的是验证交易逻辑的正确性。回测还可以提供一些有价值的见解,帮助了解交易策略的历史表现。在开发回测引擎时,你需要一个馈送适配器来使用历史数据,以及一个经纪适配器来跟踪已执行的订单以及盈亏情况。

这些数据用于计算诸如以下的统计信息:

  • 总交易次数

  • 胜利者与失败者的比例

  • 一笔交易的平均大小

  • 账户的总回报

  • 波动率与夏普比率;夏普比率是经过波动率调整的回报

扩展 GUI

此交易系统提供的图形用户界面(GUI)相当有限。GUI 可以轻松扩展,以支持更多功能,并使用 FSharpChart 等工具提供市场数据的图表。一个替代方案是使用 C#或其他具有内置可视化设计器的语言来开发 GUI,这会使事情变得更加容易。

本书中 GUI 使用 F#开发的主要原因是为了展示 F#的灵活性。当 F#有可视化设计器时,没有理由不在程序的大部分部分使用 F#。手动编写 GUI 代码是一件繁琐的事情,无论使用哪种语言。

转换为客户端-服务器架构

当前的架构更适合建模为客户端-服务器解决方案。在客户端-服务器解决方案中,数据馈送、策略执行和订单管理将驻留在服务器上,而用户界面则可以是本地应用程序或浏览器实现。这里有两种方法可以选择。第一种是使用消息队列与服务器进行通信,例如 Microsoft 消息队列。另一种是使用基于浏览器的 GUI,通过 WebSockets 和 RESTful 技术与服务器通信。

一些值得深入了解的有用技术包括:

  • Microsoft 消息队列MSMQ

  • ZeroMQ

  • WebSocket

  • RESTful

概述

在本章中,我们将书中学到的知识拼凑在一起,最终构建了一个用于波动率套利的交易系统。到目前为止,F# 编程语言和 .NET 框架的许多方面,以及外部库的使用,都已被讲解和覆盖。

posted @   绝不原创的飞龙  阅读(10)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示