65.9K
CodeProject 正在变化。 阅读更多。
Home

实时股票市场数据处理导论

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (63投票s)

2013年2月26日

CPOL

24分钟阅读

viewsIcon

337634

downloadIcon

57871

讨论股票市场交易的工作原理、可用的不同类型的市场数据,并提供包含处理市场数据源的示例代码

目录

更新

2013年3月4日

源代码包中包含了一个示例市场数据文件,我还提供了额外的市场和证券事件数据文件供下载:http://sourceforge.net/projects/sparkapi/files/MarketDataFiles/,供希望进一步实验的人使用。

2013年5月20日

添加了明确的说明,用于将“SparkAPI”项目切换为使用32位版本的本机spark.dll文件。

引言 

本文的目标是介绍开发利用实时股票市场数据(例如交易应用程序)所需的概念、术语和代码结构。它讨论了交易概念、可用的不同类型的市场数据,并提供了一个关于如何将数据源事件处理到市场对象模型中的实际示例。

本文面向希望了解基本金融市场数据处理的中高级开发人员。我建议那些已经熟悉交易术语的人跳到“市场数据”部分。

文章结构如下:

  • 股票市场和交易相关概念、规则和术语介绍
  • 市场数据讨论:不同类型、不同等级及其可用性
  • 代码演练,该代码从文件回放市场数据事件,并对其进行处理以生成可用于更高级别流程(如算法交易)的市场数据结构(例如证券、交易历史)。

市场概念与术语

以下部分解释了与交易和市场数据结构相关的基本术语和概念。它以股票市场为例,但通常适用于大多数交易市场(例如衍生品、商品等)。

订单与交易

当卖方同意以指定价格向买方转让指定数量股票的所有权时,即发生**交易**。买卖双方如何会面?他们使用一个名为**股票市场**的集中交易场所。人们聚集在一起,然后宣布他们买卖特定股票的意愿;我想以35.00美元购买500股BHP,我想以65.34美元出售2,000股RIO。这些被称为**订单**。买入订单也称为**买入报价**订单。卖出订单也称为**卖出报价**或**发盘**订单。

当买入订单的价格等于或高于当前可用的最低卖出订单价格时,发生交易。当卖出订单的价格等于或低于当前可用的最高买入订单价格时,发生交易。此过程也称为**撮合**,因为买入价和卖出价必须匹配或交叉才能发生交易。

如果订单提交到市场,但未撮合,会发生什么?它会进入一个名为**订单簿**的订单列表。订单将保留在那里,直到交易员取消它,或者它过期(例如,如果是日内订单,则在收盘时过期)。一些订单在与订单簿上可用的内容撮合后立即过期。这些被称为“立即或取消”(IOC)或“全部成交或立即取消”(FAK)订单。无论是否撮合,这些订单都不会进入订单簿。

订单簿

**订单簿**包含所有尚未撮合的买卖特定股票的报价。它就像股票的分类广告,每个人都可以看到所有的买卖报价。限价订单簿可以称为**簿**、**深度**或**队列**。

订单簿中的订单只有在其优先级最高(例如,它必须在订单簿的顶部)的情况下才能与传入订单撮合。这就像排队:您必须站在队伍的最前面才能得到服务。

订单簿中的订单按**价格-时间优先**排序。这意味着订单首先按价格排序,然后按提交时间排序。优先级最高的买入订单将是价格最高且最先提交的买入订单。优先级最高的卖出订单将是价格最低且最先提交的卖出订单。

以下是显示价格-时间优先的两个订单队列的示例订单簿:一个买入(出价)订单队列和一个卖出(要价)订单队列。时间指每个订单的提交时间

买入(出价)订单和卖出(要价)订单按优先级从上到下排列。要价订单按时间排序(最早的在前),因为它们价格相同。请注意,100股的买入订单的优先级高于19股的订单,尽管它提交得较晚,因为它价格较高。

交易示例

现在我将使用上面所示的示例订单簿逐步完成一次交易撮合。假设一个以23.34价格出售150股的卖出订单提交到市场(此订单的简写将是:卖出150@23.34)。新订单用黄色标记

请注意,买入价和卖出价现在已经交叉。每个订单提交后,交易所会检查交叉价格,然后执行所需数量的撮合,以使市场恢复到未交叉状态。在这种情况下,交易所将150股卖出订单与23.34价格的100股买入订单撮合,生成一个100股以23.34价格的交易(100@23.34)。剩余的50股卖出订单现在是订单簿中优先级最高的卖出订单

市场行情

交易者通常不关心股票的整个订单簿,而只关心市场上当前最高的买入价和最低的卖出价,以及这些价格下可用的数量。所有这些信息合起来称为**市场报价**。

价差

当市场处于持续交易状态(未收盘或处于拍卖状态)时,买入价必须始终低于卖出价。如果它们交叉,就会发生交易。最低卖出价与最高买入价之间的差额称为**价差**。

买卖价格只能以指定的增量进行更改(例如,您不能以35.0001美元的价格买入)。股票的最小价格变化称为其**跳动点**。因此,每只股票的最小价差大小由其跳动点决定。股票股价越低,跳动点越小。例如,澳大利亚市场的跳动点为

  • 股价低于0.10美元为0.001美元
  • 股价低于2.00美元为0.005美元
  • 股价等于或大于2.00美元为0.01美元

市场数据

市场数据有几种不同的“等级”。数据质量由其粒度和细节决定。

粒度

粒度指的是数据的观测时间间隔。快照观测记录特定时刻(例如每日收盘价,或每天每分钟的市场报价)。基于事件的观测记录每次相关字段更改时(例如交易更新,订单簿更改)。

事件级别数据集优于快照数据集,因为快照视图总是可以从事件数据中派生出来,反之则不然。如果它们好得多,为什么并非所有数据集都以事件形式提供?有几个原因。它们存储大,编码更难,处理速度慢,并且并非所有人都需要这种详细程度。

详细信息

细节是指数据集中包含的信息。市场数据有三个详细级别:交易、报价和深度。交易和报价更新通常合称为一级 (L1) 数据。深度更新称为二级 (L2) 数据。

交易更新

最简单的细节级别以交易价格的形式出现(例如每日收盘价)。这些价格广泛可用,常被散户投资者用来选择股票进行较长投资期(例如数月、数年)的买入和持有。

以下是BHP在ASX上交易的每日收盘价样本

Date		Open	High	Low	Close	Volume
2013-02-05	37.80 	37.94 	37.68 	37.92 	5683782
2013-02-04	37.38 	37.61 	37.33 	37.48 	6140610
2013-02-03	37.50 	37.64 	37.42 	37.62 	6676410
2013-02-02	37.30 	37.30 	37.04 	37.17 	6936594
2013-02-01	37.25 	37.27 	36.95 	37.10 	13737522
2013-01-31	36.90 	37.22 	36.82 	37.16 	7174644
2013-01-30	37.00 	37.15 	36.86 	37.06 	9143136
2013-01-29	36.54 	36.85 	36.50 	36.58 	5569151

比每日收盘价高出一大截的是日内交易历史(也称为逐笔数据),它包含一系列记录,详细说明了股票发生的每笔交易。一个好的交易数据集将包含以下字段:

  • 代码 - 证券代码(例如 BHP)
  • 交易所 - 发生交易的交易所(例如 ASX、CXA)
  • 价格 - 交易价格
  • 数量 - 交易数量
  • 时间 - 交易日期和时间(如果数据质量高,将以毫秒或微秒为单位)
  • 交易类型(条件代码) - 交易类型(例如标准交易、场外交易报告、簿记目的交易)

以下是FMG(Fortescue Metals Group)于2011年9月30日在ASX上交易的日内交易记录样本

Date		Time		Symbol	Exch	Price	Quantity	Type
20110930	11:14:24.475	FMG	ASX	4.62	1000	
20110930	11:14:24.475	FMG	ASX	4.62	5000		XT
20110930	11:14:24.475	FMG	ASX	4.62	249	
20110930	11:14:24.477	FMG	ASX	4.62	25722	
20110930	11:14:24.480	FMG	ASX	4.62	1518		XT
20110930	11:14:24.482	FMG	ASX	4.62	113		XT
20110930	11:14:25.046	FMG	ASX	4.62	2702	

注意:'XT' 标志表示该交易是交叉盘交易。这发生在同一经纪人执行交易双方的情况下(例如,一个客户通过经纪人买入,另一个客户通过经纪人卖出)。

日内交易记录常用于零售交易软件进行日内图表和技术分析。有时人们会尝试使用交易记录对交易策略进行回测(回测指使用历史数据评估表现)。如果您正在测试日内策略,请不要这样做,您的结果将毫无用处,因为交易价格并不总是反映您当时可以买入或卖出的实际价格。

谷歌财经显示的证券图表是使用日内交易记录生成的(x轴为交易时间,y轴为交易价格)。

报价更新

市场数据细节的下一步是包含市场报价更新。一个好的市场报价数据集将在每次发生变化时包含以下字段的记录:

  • 代码 - 证券代码(例如 BHP)
  • 交易所 - 报价来源交易所
  • 时间 - 报价更新时间
  • 买入价 - 市场最高买入价
  • 买入量 - 市场买入价可用的总数量
  • 卖出价 - 市场最低卖出价
  • 卖出量 - 市场卖出价可用的总数量

一些报价数据集将提供这些额外的字段,这些字段对于某些类型的分析很有用,但与回溯测试关系不大

  • 买入订单 - 市场买入价的订单数量
  • 卖出订单 - 市场卖出价的订单数量

以下是NAB(澳大利亚国民银行)2012年10月31日实时报价更新事件的样本

Date		Time		Update	Symbol	Exch	Side	Price	Quantity
2012-10-31	13:51:13.784	QUOTE	NAB	ASX	Bid	25.81 	15007
2012-10-31	13:51:14.615	QUOTE	NAB	ASX	Bid	25.82 	10
2012-10-31	13:51:14.633	QUOTE	NAB	ASX	Bid	25.81 	13623
2012-10-31	13:51:14.684	QUOTE	NAB	ASX	Ask	25.82 	2500
2012-10-31	13:52:09.168	QUOTE	NAB	ASX	Bid	25.80 	12223
2012-10-31	13:52:09.173	QUOTE	NAB	ASX	Ask	25.81 	1278
2012-10-31	13:52:39.750	QUOTE	NAB	ASX	Ask	25.80 	136
2012-10-31	13:52:39.754	QUOTE	NAB	ASX	Bid	25.79 	12656
2012-10-31	13:54:20.870	QUOTE	NAB	ASX	Ask	25.81 	10375
2012-10-31	13:54:20.878	QUOTE	NAB	ASX	Bid	25.80 	1098

高质量的市场报价数据集足以用于日内交易策略的回测,只要您只打算根据最佳市场价格进行交易,而不是在订单簿中发布订单并等待成交(暂不考虑数据源延迟、执行延迟和其他更高级的主题)。

深度更新

市场数据详细信息的最终级别是包含市场深度更新。深度更新包含特定证券订单簿中每个订单的每次更改记录。深度数据集通常可能受限。一些深度数据集只提供聚合价格视图、每个价格水平的总数量和订单数量。另一些则只提供前几个价格水平。

一个好的深度更新数据集将在每次其中一个字段更改时包含以下字段的记录:

  • 代码 - 证券代码(例如 BHP)
  • 交易所 - 更新来源交易所
  • 时间 - 订单更新时间
  • 订单位置唯一订单标识符 - 这用于标识哪个订单已更改(通过队列中的相对位置或通过订单ID)
  • 数量 - 订单数量
  • 更新类型 - 新建或进入(新订单已输入)、更新(数量已修改)或删除(订单已移除)

当您提交的订单将进入订单簿队列,而不是立即以最佳市场价格执行时,需要市场深度更新才能准确回测日内交易策略。

数据可用性

市场数据有两种形式:实时数据和历史数据。交易需要实时市场数据源。历史数据集用于分析和回测。历史每日收盘价可从各种来源(如谷歌财经)免费公开获取。大多数数据和交易软件供应商可以提供指定时间窗口(例如6个月)的历史日内交易数据。例如,您可以从谷歌财经访问BHP(BHP Billiton)最近的历史每日收盘价。

实时日内交易数据也可以在互联网上免费获取,但通常会有延迟(20分钟是标准),以防止用户用它进行交易。无延迟的实时日内交易数据应可通过任何交易软件供应商以适中的价格获得。所有优秀的交易软件供应商都将通过其用户界面提供实时报价和交易数据。一些更高质量的供应商将通过API提供实时报价和交易(一级)日内数据(例如Interactive Brokers)。历史一级数据可能更难获取,但可通过一些供应商获得。

实时深度更新(二级)通常可以通过交易软件用户界面中的证券深度视图访问,但您看不到更新事件的详细信息,只是订单簿的最新版本。然而,通过API访问这些数据非常罕见。

对于散户投资者来说,获取历史二级更新记录几乎是不可能的,通常只有研究机构(如SIRCA)或交易机构(如投资银行、做市商、高频交易(HFT)团体)私下记录以供自己使用。

代码示例

下图展示了市场数据处理的基本流程

此代码示例关注前两层:接收事件和处理事件,使其成为可供高级流程使用的对象模型。

代码示例的以下部分结构如下:

  • 市场数据接口 - 讨论代码示例中使用的市场数据源
  • 读取事件 - 如何设置和执行从文件回放市场数据源
  • 事件处理 - 如何将事件处理成对象模型

市场数据接口

Spark API

虽然不同的数据源将有自己的格式(例如,通过API叠加的C结构,固定长度字符串中的FIX类消息),但它们都包含一组类似的信息。

一家名为Iguana2的澳大利亚公司创建了一款有趣的名为Spark API的产品,它提供了一个可编程访问的事件流,该事件流实质上等同于它从交易所接收到的数据。它支持访问澳大利亚和新西兰股票、认股权证和期权市场的数据源。事件流包括以下实时数据源:交易更新(L1)、报价更新(L1)、深度更新(L2)、交易所新闻、市场状态变化(例如盘前、开盘、拍卖、收盘等)以及报价基准变化(例如除息)。这是规格链接:http://iguana2.com/spark-api

该API的一个有用之处在于,它将来自不同交易所的市场数据源标准化并交错成统一的市场视图。例如,ASX市场数据接口通过一个名为Trade OI的基于C的组件传入,该组件基于纳斯达克的Genium INET技术。澳大利亚的二级交易所Chi-X通过固定长度字符串流提供市场数据,该流使用标签-值组合以半FIX式结构。

对于那些有兴趣学习如何针对交易所市场数据源进行编码的人来说,Spark API提供我遇到的最接近零售投资者可用的等效产品。通过我在Spark API SDK中编写的扩展,它可以在离线模式下运行历史数据文件,而无需连接到Spark服务器。

如果您有兴趣了解机构级市场数据源是什么样子,这里有一些我整理的机构供应商和交易所交易源规范链接:

数据供应商

交易所

Spark API SDK

Spark API SDK 是我编写的一个 C# 组件,旨在提供对 Spark API 的便捷访问,并平滑通过 .NET 访问本地 C 组件所带来的怪异之处。此外,它还包括处理和以对高级逻辑(如交易、订单、订单深度和证券)有用的形式表示事件流所需的类。

SparkApi C# 组件包含三个主要命名空间

  • Data - 包含所有执行查询、建立Spark API实时数据源以及加载和回放历史事件数据文件的逻辑
  • Market - 包含用于表示与市场相关的对象(例如交易、限价订单、订单深度和证券)的类。 
  • Common - 包含与文件管理、序列化、日志记录等相关的通用功能。

在接下来的部分中,我们将讨论与市场数据处理相关的概念时,我将引用这些命名空间中的特定类。Spark API SDK 中的代码将作为如何访问和处理市场数据源的示例。

更新:虽然源代码包中包含了一个示例市场数据文件,但我在此处提供了额外的市场和安全事件数据文件供那些希望进一步实验的人下载:这里

重要提示:尽管 SDK 中的 SparkAPI 组件引用了“Spark.Net.dll” .NET 库来访问 Spark API,“Spark.Net.dll”实际上是 C 库“spark.dll”的互操作包装器。由于 C 库不是 COM 对象,因此无法直接引用。下载中包含 32 位和 64 位版本的 spark.dll,但解决方案默认设置为使用 64 位版本。如果您在 32 位机器上运行,请按照以下说明将 SparkAPI 项目切换为使用 32 位版本。 

说明

在 Spark API 项目中: 

  1. 选择“引用”,然后删除“Spark.Net”引用。
  2. 右键单击“引用”并选择“添加引用”。
  3. 导航到 Spark API 二进制文件文件夹,选择正确的操作系统版本(32位或64位),然后选择“Spark.Net.dll”(示例中的默认位置是 \Assemblies\Spark)。
  4. 删除 SparkAPI 项目中的“spark.dll”文件。
  5. 右键点击“SparkAPI”项目,然后选择“添加->添加现有项...”
  6. 导航到Spark API二进制文件夹,选择正确的操作系统版本(32位或64位),然后选择“spark.dll”(示例中的默认位置是\Assemblies\Spark)。
  7. 右键单击 SparkAPI 项目中的文件,然后选择“属性”。
  8. 将属性“复制到输出目录”设置为“如果较新则复制”。 

Spark.Event 结构

在此代码示例中,我们将处理来自 Spark API 支持的 `Spark.Event` 结构的市场数据更新。

下面是 `Spark.Event` 结构(C# 形式而非原生 C 形式)的类图

以下字段与所有消息类型相关

  • 时间 - Unix 时间(自1970年1月1日以来的秒数)
  • 时间纳秒 - 纳秒(目前因数据流压缩原因未填充)
  • 代码 - 证券代码(例如 BHP)(如果来自 Chi-X,则带有 _CX 后缀)
  • 交易所 - 证券交易所
  • 类型 - 事件类型代码(映射到上面显示的 Spark.EVENT_X 常量列表)

其他字段仅与某些事件类型相关

  • 交易更新(EVENT_TRADE, EVENT_CANCEL_TRADE)
    • 价格 - 交易价格的整数表示
    • 成交量 - 交易数量
    • 条件代码 - 交易类型(例如 XT, CX)
  • 深度更新(EVENT_AMEND_DEPTH, EVENT_NEW_DEPTH, EVENT_DELETE_DEPTH)
    • 价格 - 订单价格的整数表示
    • 数量 - 订单数量 
    • 标志 - 包含指示订单方向的位标志
    • 位置 - 订单在买入或卖出深度队列中的索引位置
  • 市场状态更新(EVENT_STATE_CHANGE)
    • 状态 - 市场状态类型(常量列表包含在Spark API中)

关于整数价格的注意事项

股票市场相关应用程序经常对价格执行比较操作,例如,将激进的市场订单价格与订单簿中的限价进行比较,以确定是否发生交易。由于价格以美元和美分表示,价格通常在代码中表示为浮点数(float 或 double)。然而,使用浮点数进行比较容易出错(例如 36.0400000001 != 36.04),并产生不可预测的结果。

为了避免这个问题,股票市场相关的应用程序通常在内部将价格转换为整数格式。这不仅确保了准确的比较操作,而且还减少了内存占用并加快了比较速度(整数操作在CPU上比浮点操作更快)。

价格的小数部分通过乘以比例因子以整数格式保留。例如,价格34.25在比例因子为10,000时将变为342500。四位小数足以表示澳大利亚市场有效跳动点价格范围,因为最小价格将是股价低于0.10美元且跳动点为0.001的股票的中间价交易(例如,买入价=0.081,卖出价=0.082,中间价=0.0815)。

四位小数的精度适用于市场价格,但是诸如订单平均执行价格等值的计算最好存储为双精度或十进制类型,因为它们不需要精确比较。

读取事件

处理 Spark 数据源需要以下步骤

  1. 初始化数据源连接并登录Spark服务器(从文件回放时不需要)
  2. 订阅指定证券或交易所的事件源
  3. 创建 Security 类的实例来处理市场数据事件

Spark API SDK 会自动为您完成大部分工作。

以下是一些示例代码,用于回放股票数据文件,处理证券中的事件,并将交易和报价更新写入控制台

public void Main()
{
 
    //Create an event feed using replay from file
    var replayManager = new SparkAPI.Data.ApiEventFeedReplay(@"Data\TestData\AHD_Event_20120426.txt");
 
    //Create a security to receive the event feed
    var security = new SparkAPI.Market.Security("AHD");
    replayManager.AddSecurity(security);
 
    //Add event handlers to write each trade and quote update to console
    security.OnTradeUpdate += (sender, args) => Console.WriteLine("TRADE\t" + args.Value.ToString());
    security.OnQuoteUpdate += (sender, args) => Console.WriteLine("QUOTE\t" + args.Value.ToString());
 
    //Initiate the event replay
    replayManager.Execute();
 
}

让我们深入了解一下发生了什么。

第一步是读取市场数据文件中的事件。显然,当您连接到通过 API 传递事件的市场数据服务器时,不需要此步骤。以下是 SDK 中可用的事件源结构,以类图形式显示

`ApiSecurityEventFeed` 和 `ApiMarketEventFeed` 类用于接收实时市场数据。由于我们是从文件回放,因此将使用 `ApiEventFeedReplay` 类。当调用 `ApiEventFeedReplay.Execute()` 方法时,它开始从事件文件流式传输行并将其解析为所需的数据结构

public override void Execute()
{
    var reader = new SparkAPI.Data.ApiEventReaderWriter();
    reader.StreamFromFile(FileName, EventRecieved);
}

`ApiEventReaderWriter` 类包含读写 Spark 事件到文件所需的所有逻辑。我们一次一个地从文件流式传输事件,而不是一次性全部读入内存,因为在处理之前将交易所的每个事件都加载到内存中会在 32 位构建上生成内存不足异常。它也快得多。

从文件读取的每一行都使用 `SparkAPI.Data.ApiEventReaderWriter.Parse()` 方法解析为 `Spark.Event` 结构,然后传递给 StreamFromFile 命令中指定的事件处理方法。在从文件回放的情况下,这将是 `ApiEventFeedReplay.EventReceived()` 方法,该方法又会调用 `ApiEventFeedBase.RaiseEvent()` 方法。`ApiEventFeedBase.RaiseEvent()` 方法是回放和实时事件馈送代码路径对齐的地方。所有与馈送相关的类(`ApiEventFeedReplay`、`ApiMarketEventFeed`、`ApiSecurityEventFeed`)都继承自 `ApiEventFeedBase`。

我们来看看它做了什么

internal void RaiseEvent(EventFeedArgs eventFeedArgs)
{
 
    //Raise direct event if feed handler is assigned
    if (OnEvent != null) OnEvent(this, eventFeedArgs);
 
    //Raise event for security if in the dictionary
    Security security;
    if (Securities.TryGetValue(eventFeedArgs.Symbol, out security))
    {
        security.EventReceived(this, eventFeedArgs);
    }
 
}

EventFeedArgs 包含对 Spark.Event 结构的引用、事件的时间戳以及符号和交易所标识符。ApiEventFeedBase 类支持两种传播事件的机制:

  1. 通过调用其自身的 `OnEvent` 事件直接触发,或者
  2. 通过调用任何已与事件源关联的证券的 `EventReceived()` 方法。

`Security` 字典查找允许事件源将事件更新传输到正确的证券并忽略其余部分。

关于多线程的注意事项

为了使这个示例更容易理解并通过调试逐步执行,我将整个市场数据处理序列保留在单个线程中。实际上,不同的任务(如市场数据事件处理和分析)通常分配给不同的线程或不同的进程。这是一个单独文章的主题。

如果您有兴趣,SDK中有一个使用多线程实现的事件回放源,名为`SparkAPI.Data.ApiEventFeedThreadedReplay`。它实现了生产者-消费者模式,其中生产者线程从文件流式传输事件,将它们解析为结构并添加到并发队列中。消费者线程使用阻塞集合将事件出队并执行进一步处理。

处理事件

那么我们如何以有用的对象模型来表示所有这些市场数据呢?我们需要一个 `Security` 类。

它包含以下属性

  • Symbol - 用于识别证券的唯一交易所代码(例如 BHP、NAB)
  • MarketState - 证券的当前市场状态(例如盘前、拍卖、开盘、收盘等)
  • Trades - 当天该股票发生的所有交易列表
  • OrderBooks - 证券的一组限价订单簿(深度),其中每个独立的交易场所都有自己的订单簿。

与表示证券对象模型相关的类如下所示

Security 类中最复杂的区域涉及更新 `LimitOrderBook` 类中的订单深度。我们需要维护从我们接收数据的每个场所的当前订单深度。在澳大利亚,有两个交易场所:澳大利亚证券交易所(ASX)和Chi-X Australia(CXA)。由于 `Security` 类接收来自这两个场所的事件,我们将多个 `LimitOrderBook` 类存储在 `OrderBooks` 字典中,使用交易所ID(例如 ASX,CXA)作为字典键。

`LimitOrderBook` 类包含两个 `LimitOrderList` 类(`Bid` 和 `Ask`),它们分别代表买入和卖出订单队列。`LimitOrderList` 是 `List`<`LimitOrder`> 泛型集合的包装器。`LimitOrder` 类包含队列中当前每个订单的详细信息。买入(买方)队列按价格-时间优先级排序,价格最高的订单位于队列顶部。卖出(卖方)队列按价格-时间优先级排序,价格最低的订单位于队列顶部。

一旦事件到达 `Security` 对象,它们就需要被解释以更新 `Security` 数据对象和字段。以下是处理事件的方法:

internal void EventReceived(object sender, EventFeedArgs eventFeedArgs)
{
 
    //Process event
    Spark.Event eventItem = eventFeedArgs.Event;
    switch (eventItem.Type)
    {
 
        //Depth update
        case Spark.EVENT_NEW_DEPTH:
        case Spark.EVENT_AMEND_DEPTH:
        case Spark.EVENT_DELETE_DEPTH:
 
            //Check if exchange order book exists and create if it doesn't
            LimitOrderBook orderBook;
            if (!OrderBooks.TryGetValue(eventFeedArgs.Exchange, out orderBook))
            {
                orderBook = new LimitOrderBook(eventFeedArgs.Symbol, eventFeedArgs.Exchange);
                OrderBooks.Add(eventFeedArgs.Exchange, orderBook);
            }
 
            //Submit update to appropriate exchange order book
            orderBook.SubmitEvent(eventItem);
            if (OnDepthUpdate != null) OnDepthUpdate(this, 
              new GenericEventArgs<LimitOrderBook>(eventFeedArgs.TimeStamp, orderBook));
            break;
 
        //Trade update
        case Spark.EVENT_TRADE:
 
            //Create and store trade record
            Trade trade = eventItem.ToTrade(eventFeedArgs.Symbol, 
              eventFeedArgs.Exchange, eventFeedArgs.TimeStamp);
            Trades.Add(trade);
            if (OnTradeUpdate != null) OnTradeUpdate(this, 
              new GenericEventArgs<Trade>(eventFeedArgs.TimeStamp, trade));
            break;
 
        //Trade cancel
        case Spark.EVENT_CANCEL_TRADE:
            
            //Find original trade in trade record and delete
            Trade cancelledTrade = eventItem.ToTrade(eventFeedArgs.TimeStamp);
            Trade originalTrade = Trades.Find(x => (x.TimeStamp == 
              cancelledTrade.TimeStamp && x.Price == 
              cancelledTrade.Price && x.Volume == cancelledTrade.Volume));
            if (originalTrade != null) Trades.Remove(originalTrade);
            break;
 
        //Market state update
        case Spark.EVENT_STATE_CHANGE:
            State = ApiFunctions.ConvertToMarketState(eventItem.State);
            if (OnMarketStateUpdate != null) OnMarketStateUpdate(this, 
              new GenericEventArgs<MarketState>(eventFeedArgs.TimeStamp, State));
            break;
 
        //Market quote update (change to best market bid-ask prices)
        case Spark.EVENT_QUOTE:
            if (OnQuoteUpdate != null)
            {
                LimitOrderBook depth = OrderBooks[eventFeedArgs.Exchange];
                MarketQuote quote = new MarketQuote(eventFeedArgs.Symbol, 
                  eventFeedArgs.Exchange, depth.BidPrice, depth.AskPrice, eventFeedArgs.TimeStamp);
                OnQuoteUpdate(this, new GenericEventArgs<MarketQuote>(eventFeedArgs.TimeStamp, quote));
            }
            break;
 
        default:
            break;
 
    }
}

交易、报价和市场状态更新只需将事件结构中的信息转换为 C# 等效对象,然后更新相关属性(对于市场状态和报价)或列表(对于交易)。更新限价订单簿条目更为复杂,因此我们将详细研究它。

在 `LimitOrderBook.SubmitEvent()` 方法中,我们确定是将其添加到买入队列还是卖出队列

public void SubmitEvent(Spark.Event eventItem)
{
    LimitOrderList list = (ApiFunctions.GetMarketSide(eventItem.Flags) == MarketSide.Bid) ? Bid : Ask;
    lock (_lock)
    {
        list.SubmitEvent(eventItem);
    }
}

提交事件时会使用锁,因为限价订单簿队列可能会被需要该信息的其他线程遍历。

一旦我们获得了对正确 `LimitOrderList` 对象的引用,就会调用它的 `SubmitEvent()` 方法

public void SubmitEvent(Spark.Event eventItem)
{
    switch (eventItem.Type)
    {
        case Spark.EVENT_NEW_DEPTH:
 
            //ENTER
            LimitOrder order = eventItem.ToLimitOrder();
            if (Count == 0)
            {
                Add(order);
            }
            else
            {
                Insert(eventItem.Position - 1, order);
            }
            break;
 
        case Spark.EVENT_AMEND_DEPTH:
 
            //AMEND
            this[eventItem.Position - 1].Volume = (int)eventItem.Volume;
            break;
 
        case Spark.EVENT_DELETE_DEPTH:
 
            //DELETE
            RemoveAt(eventItem.Position - 1);
            break;
 
        default:
            break;
 
    }
}

事件结构中的 Position 字段是确定操作应发生位置的关键。对于 ENTER 订单,它提供插入位置;对于 AMEND 或 DELETE 订单,它提供对正确订单的引用。请注意,Position 使用基于 1 的引用点,而不是基于 0 的引用点。

一些数据源可能不提供位置值,而是提供深度更新的唯一订单标识符。在这种情况下,您需要根据 ENTER 订单的时间-价格优先规则确定订单的正确位置,并通过哈希表查找使用订单 ID 来定位修改或删除订单。

最终想法

本文中有许多主题我觉得应该更详细地讨论,例如跨多个线程同步事件处理以及延迟对回测市场数据处理的影响。还有一个问题是您如何处理市场数据,这涵盖了指标、交易策略和复杂的订单状态管理领域。然而,我希望我已经对所涉及的概念和数据结构进行了介绍,并为有兴趣进一步实验的人提供了一些代码示例和示例市场数据。

欢迎随时发表任何评论、问题或建议。

历史

版本1.0 - 2013年2月27日 - 初始版本 版本1.1 - 2013年3月4日 - 小幅编辑,添加了额外事件数据文件的链接

© . All rights reserved.