精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。
锐英源软件擅长股票和金融软件开发,对于各类平台技术也非常精通,想通过开源学习高难技术细节,也请关注锐英源,头条号:“软件技术及人才和养生”,QQ群:14372360。本文翻译自codeproject,是学习架构和复杂数据模型转化为代码的好案例,想学习请联系,学习要点有
1、多线程、队列和事件。
2、C语言库怎么和C#语言交互方法。
3、股票数据接口的形态。
本文的目的是介绍开发利用实时股票市场数据的应用程序(例如交易应用程序)所需的概念、术语和代码结构。它讨论了交易概念、不同类型的可用市场数据,并提供了一个关于如何将数据馈送事件处理到市场对象模型中的实际示例。
本文面向希望了解基本金融市场数据处理的中高级开发人员。我建议那些已经熟悉交易术语的人直接跳到市场数据部分。
这篇文章的结构如下:
以下部分解释了与交易和市场数据结构相关的基本术语和概念。它是根据股票(股票)市场制定的,但通常适用于大多数交易市场(例如衍生品、商品等)。
当卖方同意以指定价格将指定数量的股票的所有权转让给买方时,交易就发生了。买家和卖家如何见面?他们使用一个称为股票市场的集中市场。人们聚集在一起,然后宣布他们希望购买或出售特定股票;我想以 35.00 美元的价格购买 500 股 BHP 的股票,我想以 65.34 美元的价格出售 2,000 股 RIO 的股票。这些称为订单。买单也称为出价单。卖单也称为卖单或卖单。
当买单的价格等于或高于当前可用的最低价卖单时,交易就会发生。当卖单的价格等于或低于当前可用的最高价买单时,就会发生交易。此过程也称为匹配,因为买卖价格必须匹配或交叉才能发生交易。
如果向市场提交订单但不匹配怎么办?它被输入到称为订单簿的订单列表中。订单将保留在那里,直到交易者取消它或到期(例如,如果它是日订单,则为日终)。一些订单在与订单簿上的可用订单匹配后立即过期。这些被称为“立即或取消”(IOC) 或“填写并取消”(FAK) 订单。无论它们是否匹配,这些订单都不会进入订单簿。
订单簿包含买卖尚未匹配的特定股票的所有报价。就像股票的分类广告,每个人都能看到所有的买卖报价。限价订单簿可以简称为book、depth或queue。
如果账簿中的订单是最高优先级的订单(例如,它必须位于账簿顶部),则只能与传入订单匹配。这就像排队:您必须排在队伍的最前面才能得到服务。
订单簿中的订单按价格-时间优先排序。这意味着订单首先按价格排序,然后按提交时间排序。最高优先级买单将是最先提交的价格最高的买单。最高优先级的卖单将是最先提交的价格最低的卖单。
这是一个示例订单簿,显示了两个价格时间优先级的订单队列:一个队列用于买(买)单,一个队列用于卖(卖)订单。时间是指每笔订单的提交时间:
买(买)单和卖(卖)单从上到下依次排列。询价订单是按时间排序的订单(最早的),因为它们的价格相同。请注意,100 股的出价订单优先于 19 股的订单,即使它是较晚提交的,因为它的价格更高。
我现在将使用上面显示的示例订单簿逐步完成交易匹配。考虑在 23.34 的价格向市场提交 150 股的卖单(该订单的简写为:Sell 150@23.34)。新订单以黄色标记:
注意买入价和卖出价现在交叉了。提交每个订单后,交易所检查交叉价格,然后执行所需数量的匹配以使市场恢复到未交叉状态。在这种情况下,交易所将 150 股卖出与 100 股买入相匹配,价格为 23.34,产生 100 股的交易价格为 23.34 (100@23.34)。剩余的 50 股卖单现在是书中优先级最高的卖单:
交易者通常对股票的整个订单簿不感兴趣,而只对当前市场上的最高出价和最低要价以及这些价格下的可用数量感兴趣。所有这些信息一起被称为市场报价。
当市场处于连续交易状态时(未关闭或处于拍卖状态),买入价必须始终低于卖出价。如果它们交叉,就会发生交易。最低卖价和最高买价之间的差额称为点差。
买入和卖出的价格只能按指定的增量更改(例如,您不能以 $35.0001 的价格买入)。一只股票的最小价格变化称为它的最小变动幅度。因此,每只股票的最小价差大小由其报价单位决定。股票的股价越低,刻度尺寸就越小。例如,澳大利亚市场上的报价单尺寸为:
市场数据有几种不同的“等级”。数据质量由其粒度和细节决定。
粒度是指数据的观察时间间隔。快照观察记录特定时刻(例如每日收盘价,或一天中每一分钟的市场报价)。每次相关字段发生变化(例如交易更新、订单簿的变化)时,都会记录基于事件的观察结果。
事件级数据集优于快照数据集,因为快照视图始终可以从事件数据中派生出来,但反之则不然。如果它们好得多,为什么不以事件形式提供所有数据集?几个原因。它们存储起来很大,更难编码,处理起来更慢,而且并不是每个人都需要这种程度的细节。
详细信息是指数据集中包含哪些信息。市场数据详细信息分为三个级别:交易、报价和深度。结合在一起的交易和报价更新通常称为 1 级 (L1) 数据。深度更新被称为 2 级 (L2) 数据。
最简单的详细程度以交易价格的形式出现(例如每日收盘价)。这些是广泛可用的,散户投资者经常使用它们来选择要购买的股票并持有更长的投资期(例如数月,数年)。
以下是必和必拓在澳大利亚证券交易所交易的每日收盘价示例:
日期开高低收盘量 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
日收盘价的一大进步是盘中交易历史(也称为报价数据),其中包含一系列详细记录股票发生的每笔交易的记录。一个好的贸易数据集将包含以下字段:
以下是 2011 年 9 月 30 日在澳大利亚证券交易所交易的 FMG(Fortescue Metals Group)的盘中交易记录示例:
日期时间符号 Exch Price 数量类型 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' 标志表示交易是一个交叉点。当同一经纪人执行交易的双方时(例如,一个客户通过经纪人买入而另一个客户卖出),就会发生这种情况。
日内交易记录通常用于零售交易软件,用于日内图表和技术分析。有时人们会尝试使用交易记录来回测交易策略(回测意味着使用历史数据评估性能)。如果您正在测试日内策略,请不要这样做,您的结果将毫无用处,因为交易价格并不总是反映您当时可以买入或卖出的实际价格。
Google 财经显示的证券图表是使用日内交易记录生成的(x 轴为交易时间,y 轴为交易价格)。
市场数据细节的下一步是包含市场报价更新。一个好的行情数据集,每次有变动都会包含以下字段的记录:
一些报价数据集将提供这些附加字段,这些字段对某些类型的分析很有用,但与回溯测试不是特别相关:
一些报价数据集将提供这些附加字段,这些字段对某些类型的分析很有用,但与回溯测试不是特别相关:
以下是 2012 年 10 月 31 日 NAB(澳大利亚国民银行)的实时报价更新事件示例:
日期时间更新符号交易所方价格数量 2012-10-31 13:51:13.784 报价 NAB ASX 出价 25.81 15007 2012-10-31 13:51:14.615 报价 NAB ASX 出价 25.82 10 2012-10-31 13:51:14.633 报价 NAB ASX 出价 25.81 13623 2012-10-31 13:51:14.684 报价 NAB ASX 问价 25.82 2500 2012-10-31 13:52:09.168 报价 NAB ASX 出价 25.80 12223 2012-10-31 13:52:09.173 报价 NAB ASX 问价 25.81 1278 2012-10-31 13:52:39.750 报价 NAB ASX 问价 25.80 136 2012-10-31 13:52:39.754 报价 NAB ASX 出价 25.79 12656 2012-10-31 13:54:20.870 报价 NAB ASX 问价 25.81 10375 2012-10-31 13:54:20.878 报价 NAB ASX 出价 25.80 1098
一个高质量的市场报价数据集是回测日内交易策略所需要的,只要您只计划以最佳市场价格进行交易,而不是在订单簿中发布订单并等待填充(暂且不考虑数据馈送延迟、执行延迟和其他更高级的主题)。
市场数据细节的最后一层是包含市场深度更新。深度更新包含对特定证券的订单簿中每个订单的每次更改的记录。深度数据集通常是有限的。一些深度数据集仅提供聚合价格视图,每个价格级别的总数量和订单数量。其他人只会提供前几个价格水平。
一个好的深度更新数据集将包含以下字段的每次更改时的记录:
需要市场深度更新才能准确回溯测试日内交易策略,您提交的订单将进入订单簿队列,而不是立即以最佳市场价格执行。
市场数据有两种形式:实时数据和历史数据。交易需要实时市场数据馈送。历史数据集用于分析和回测。历史每日收盘价可从各种来源(例如 Google 财经)免费公开获得。大多数数据和交易软件供应商可以提供指定时间窗口(例如 6 个月)的历史日内交易数据。例如,您可以从Google 财经访问必和必拓 (BHP Billiton) 最近的历史每日收盘价。
实时盘中交易数据也可以在互联网上免费访问,但通常会延迟(标准为 20 分钟)以防止用户使用它进行交易。非延迟的实时盘中交易数据应该可以通过任何交易软件供应商以适中的价格获得。所有优秀的交易软件供应商都将通过其用户界面提供实时报价和交易数据。一些更高质量的供应商将通过 API(例如 Interactive Brokers)实时提供报价和交易(1 级)盘中数据。历史级别 1 数据可能更难获取,但可以通过某些供应商获得。
实时深度更新(2 级)通常可以使用安全深度视图通过交易软件用户界面访问,但您看不到更新事件的详细信息,只能看到订单簿的最新版本。但是,很少能够通过 API 访问这些数据。
历史Level 2更新记录作为散户投资者几乎不可能获得,一般仅由研究机构(如SIRCA)保存或由交易机构(如投资银行、做市商、高频交易(HFT)集团)私下记录供自己使用。
下图展示了行情数据处理的基本流程:
此代码示例与前两层有关;事件的接收和处理成一个对象模型,可以被更高级别的进程使用。
代码示例的以下部分结构如下:
虽然不同的数据源将有自己的格式(例如,通过 API 覆盖的 C 结构,固定长度字符串中的 FIX 之类的消息),但它们都包含一组相似的信息。
一家名为 Iguana2 的澳大利亚公司创建了一个名为 Spark API 的有趣产品,它提供了一个可通过编程方式访问的事件流,该事件流本质上等同于它从交易所接收到的内容。它支持访问澳大利亚和新西兰股票、权证和期权市场的数据源。事件流包括实时馈送:交易更新 (L1)、报价更新 (L1)、深度更新 (L2)、交易所新闻、市场状态变化(例如开盘前、开盘、拍卖、收盘等) ,并引用基数变化(例如除息)。这是规范的链接:http://iguana2.com/spark-api
API 的一个有用之处在于,它将来自不同交易所的市场数据馈送标准化并交织成一个统一的市场视图。例如,ASX 市场数据接口通过一个名为 Trade OI 的基于 C 的组件进入,该组件基于 NASDAQ 的 Genium INET 技术。澳大利亚的二级交易所 Chi-X 通过固定长度的字符串流提供市场数据,该字符串流使用类似半固定结构的标签值组合。
对于那些有兴趣学习针对交易所市场数据源进行编码的人,Spark API 提供了我遇到的最接近散户投资者可用的等效项。通过我在 Spark API SDK 中编写的扩展,它可以在离线模式下运行历史数据文件,而无需连接到 Spark 服务器。
如果您有兴趣了解机构级市场数据源是什么样的,这里有一些指向我编制的机构供应商和交易所交易源规范的链接:
Spark API SDK 是我编写的一个 C# 组件,用于提供对 Spark API 的轻松访问,并消除因通过 .NET 访问本机 C 组件而产生的怪癖。此外,它还包括处理事件源所需的类,并以一种对更高级别的逻辑(如交易、订单、订单深度和证券)有用的形式表示事件源。
SparkApi C# 组件包含三个主要命名空间:
在我们检查与市场数据处理相关的概念时,我将在以下部分中引用这些命名空间中的特定类。Spark API SDK 中的代码将用作如何访问和处理市场数据提要的示例。
更新:虽然源代码包中包含一个样本市场数据文件,但我已经为那些希望进一步试验的人提供了额外的市场和安全事件数据文件可供下载。
重要提示:虽然 SDK 中的 SparkAPI 组件引用“Spark.Net.dll”.NET 库来访问 Spark API,但“Spark.Net.dll”实际上是名为“spark.dll”的 C 库的互操作包装器。由于 C 库不是 COM 对象,因此不能直接引用它。下载中包含 32 位和 64 位版本的 spark.dll,但该解决方案默认设置为使用 64 位版本。如果您在 32 位机器上运行,请按照以下说明将 SparkAPI 项目切换为使用 32 位版本。
指示:
在 Spark API 项目中:
在此代码示例中,我们将从Spark.EventSpark API 支持的结构处理市场数据更新。
下面是一个类图,显示了Spark.EventC# 形式(而不是本机 C 形式)的结构:
以下字段与所有消息类型相关:
其他字段仅与某些事件类型相关:
股票市场相关的应用程序经常对价格进行比较操作,例如将激进的市场订单价格与订单簿中的限价进行比较,以确定是否发生交易。由于价格以美元和美分表示,因此价格通常在代码中表示为浮点数(float 或 double)。但是,使用浮点数进行比较很容易出错(例如 36.0400000001 != 36.04)并且会产生不可预测的结果。
为避免此问题,与股票市场相关的应用程序通常会在内部将价格转换为整数格式。这不仅确保了准确的比较操作,而且还减少了内存占用并加快了比较速度(整数运算在 CPU 上比浮点运算更快)。
通过将价格乘以比例因子,价格的低于美元部分以整数格式保留。例如,价格 34.25 将变为 342500,比例因子为 10,000。小数点后四位足以表示澳大利亚市场报价的有效范围,因为最小价格将是价格低于 0.10 美元的股票的中点交易,报价为 0.001(例如 Bid=0.081 Ask=0.082 Mid=0.0815 ).
四位小数的精度适用于市场价格,但是订单的平均执行价格等值的计算最好以双精度或小数类型存储,因为它们不需要精确比较。
处理 Spark 数据提要需要以下步骤:
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 位构建上生成内存不足异常。它也快得多。
从文件中读取的每一行都使用该方法解析为一个Spark.Event结构SparkAPI.Data.ApiEventReaderWriter.Parse(),然后传递给 StreamFromFile 命令中指定的事件处理方法。在从文件重放的情况下,这将是方法ApiEventFeedReplay.EventReceived(),它又调用该ApiEventFeedBase.RaiseEvent()方法。该ApiEventFeedBase.RaiseEvent()方法是重播和实时事件提要代码路径对齐的地方。所有 Feed 关联类 ( 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支持两种传播事件的机制:
字典Security查找允许事件源将事件更新通过管道传输到正确的安全性并忽略其余部分。
为了使这个示例更容易理解并通过调试逐步完成,我将整个市场数据处理序列保持在一个线程中。在实践中,市场数据事件处理和分析等不同任务通常分配给不同的线程或不同的进程。这是另一篇文章的主题。
如果您有兴趣,SDK 中有一个使用名为SparkAPI.Data.ApiEventFeedThreadedReplay. 这实现了生产者-消费者模式,其中生产者线程从文件中流式传输事件,将它们解析为结构并将它们添加到并发队列中。消费者线程使用阻塞集合使事件出列并执行进一步处理。
那么我们如何在一个有用的对象模型中表示所有这些市场数据呢?我们需要一个Security班级。
它包含以下属性:
与表示安全对象模型关联的类如下所示:
Security 类中最复杂的区域与更新LimitOrderBook类中的订单深度有关。我们需要为我们从中接收数据的每个地点维护当前订单深度。在澳大利亚,有两个交易场所:澳大利亚证券交易所 (ASX) 和 Chi-X Australia (CXA)。当Security类接收两个场所的事件时,我们将多个LimitOrderBook类存储在OrderBooks字典中,使用交易所 ID(例如 ASX、CXA)作为字典键。
一个LimitOrderBook类包含两个LimitOrderList类(Bid和Ask),分别代表出价和要价订单队列。是imitOrder> 通用集合的LimitOrderList包装器List<code><L。该类LimitOrder包含当前队列中每个订单的详细信息。出价(购买)队列按价格时间优先顺序排序,价格最高的订单排在队列顶部。询价(卖出)队列按价格时间优先顺序排序,价格最低的订单排在队列顶部。
一旦事件到达安全对象,就需要对其进行解释以更新安全数据对象和字段。这是处理事件的方法:
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(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 (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 (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 (eventFeedArgs.TimeStamp, quote)); } break; default: break; } }
交易、报价和市场状态更新只需要将事件结构中的信息转换为 C# 等效对象,然后更新相关属性(用于市场状态和报价)或列表(用于交易)。更新限价订单簿条目更为复杂,因此我们将对其进行详细研究。
在该LimitOrderBook.SubmitEvent()方法中,我们确定是否应该将其添加到 Bid 或 Ask 队列中:
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来定位订单。
这篇文章中有很多主题我觉得应该更详细地讨论,例如跨多个线程同步事件处理以及延迟对处理市场数据以进行回测的影响。还有一个问题是您如何处理市场数据,涵盖指标、交易策略和订单状态管理的复杂领域等领域。但是,我希望我已经介绍了所涉及的概念和数据结构,并为那些有兴趣进一步试验的人提供了一些代码示例和示例市场数据。