BriefMaker – 一个处理实时市场数据的应用程序





5.00/5 (12投票s)
将过去和实时的股票市场撮合数据转换为称为 Briefs 的时间切片摘要。
- 下载源代码 - 685 KB
- 下载可执行文件 - 554 KB
- 下载 MS-SQL 启动数据库: 作为 Bak
- 在 GitHub 上查看最新代码
引言
BriefMaker
是一款 Windows 程序,它将流式市场事件转换为我称之为“摘要”(briefs)的时间段式摘要。摘要是更熟悉且更实用的数据。
流数据通常没有太大用处——它是一团随机的各种事件。我们通常希望将这种混乱转换为更合乎逻辑且有用的数据结构,其中可能包含最高价、最低价和平均价等内容。当我们查看股票价格历史表时,我们常常认为这种转换是理所当然的,而忽略了在后台,这些数据最初来自大量的市场事件,例如交易、买入价、卖出价等。转换过程将这些变化的事件数量转换为特定时间间隔的固定大小的精美摘要。其结果是更容易供人类和 AI 算法读取的数据。
下方左侧是流事件数据的样子。[78,2,2,24.43] 只是我对 在 78/256 秒内,股票 ID 为 #2,属性为 #2(买入价更新),新值为 24.43 的简写。BriefMaker 将此流数据转换为我称之为“摘要”的基于时间的快照,显示在右侧。右侧更容易阅读。
为什么命名为“Brief”?首先,“Brief”表示某件更长事物的简短版本。摘要是一段时间内大量事件的固定大小摘要。但从某种意义上说,摘要也不算太简短,因为它捕捉了比基本的低/高/收盘价/交易量更多信息,它还捕捉了指标、统计数据、计数等等。我想一个更准确的名称/定义可能是“一段时间内市场事件数据的固定大小扩展摘要”。
将事件数据转换为时间摘要(摘要)有一个缺点。通常,在对任何类型的数据进行摘要时,都会丢失一些无法恢复的数据。例如,无法从摘要中重现原始实时事件流——这是一条单行道。然而,摘要始终可以从事件数据中重新创建,因此,我建议保留原始数据(StreamMoments
)。如上所示,摘要可以非常详细地描述事件数据,我们可以调整摘要生成过程并重新创建所有摘要。
与实时行情相比,摘要有很多好处。例如,它们可以放入一个表格中,其中时间是一个轴,交易量是另一个轴。摘要更容易查看,并且包含有关每种符号在每个 6 秒时间间隔内发生情况的大部分重要信息。它包含诸如周期最高价、周期最低价、最新成交价、最新买入价、最新买入量、交易计数等数据。
背景
我需要表格状数据来训练我的 AI,因此构建了 BriefMaker
。我使用类似遗传算法的东西来预测股票。它基本上读取 X 行数据,然后尽最大努力预测下一行数据,以便进行实时交易。虽然不是必需的,但表格对此非常有效。然而,我几乎只有随机事件数据。
虽然事件数据也可以在表格中使用,但对每个事件运行算法的计算量会非常大。只有在使用非常低延迟的系统时,这种情况才有意义。如果我离交易所只有一英里,毫秒级都很重要,我可能会直接使用事件数据,但我住在西海岸,网络连接基本。
对于我的数据需求,我有两种选择。我可以继续使用现有的历史数据,或者我可以自己从流数据中构建。网上有很多股票历史数据可供选择,但它们缺乏我想要的高细节。大多数基本上是买入价、卖出价、最高价、最低价、最新价和交易量,分辨率为 5-60 秒。我想要关于这些数据更详细的描述,所以我认为我可以通过实时数据源生成自己的数据。这也是我从头开始构建此程序而不是使用现有数据的原因之一。
我最大的担忧之一是在转换过程中丢失隐藏数据。我希望 AI 算法能够访问尽可能多的信息。AI 在 CUDA 中使用,而在 CUDA 中,warp 大小为 32,因此拥有 32 个符号和 32 个属性可以很好地适应数据结构。起初,填充 32 个不同的属性字段很困难,但很快就难以将其保持在 32 个以下。最初,我还有更多统计字段,如方差、标准差、峰度,但为了其他描述性字段而放弃了这些。此外,对于仅 6 秒的交易,没有足够的数据来填充这些高级数据描述符。
因此,为了让整个 AI 预测算法能够很好地工作,我需要一些部分,其中一个问题是将事件数据转换为高度详细、固定大小、时间间隔的数据。经过一些尝试,BriefMaker 应运而生。
特点
以下是我认为 BriefMaker
的一些不错的功能。
- 详细摘要 - 捕获每只股票的 32 个不同方面,包括统计和指标类数据。
- 直接 WCF 连接,以提高实时数据性能。它可以与 MarketRecorder 一起使用,或定制以与其他程序配合使用。
- 断点续传 - 将从上一次中断点前稍早的时间点重新运行
StreamMoments
,以确保数据更加准确。另外,如果程序在创建摘要时关闭,重新启动后它将从中断点继续。 - 防止写入不完整的摘要 - 内置启动保护,防止在尚未收到重要值(如最新价格)时输出不完整的摘要。(请参见代码中的
waitingForData
) - 超范围检查 – 检查数据以确保值在可接受的范围内,例如最新价格应介于
数据流
BriefMaker
的数据流非常简单。它基本上一次读取表中的六条 SteamMoment
记录,然后将它们保存到摘要记录中。在读取完所有 StreamMoment
并全部赶上后,它可以选择通过 WCF 等待新的 StreamMoment
。请参阅 Interactive Brokers TWS MarketRecorder 项目,了解如何通过 WCF 发送数据的示例。
流到摘要的转换 - 几乎捕获所有内容
每当我们对股票或几乎任何其他东西的混乱事件数据进行摘要时,我们都会在转换过程中丢失数据。我们正在偏离市场上实际发生的情况。此项目之一的目标是尽可能少地丢失原始事件数据中的信息。对于股票预测算法,我希望它能够访问尽可能多的信息字段,以确保它能够发现市场中未被发现的模式。
在 BriefMaker
中,我试图收集有关每只股票在每 6 秒内发生的所有细节。从某些方面来说,摘要并不那么简短。摘要包含正常的内容,如高价、低价、最新价、卖出价、买入价,但它还捕获不同类型的交易量信息、统计数据(平均价格、众数价格、中位数价格)和指标数据(如 MACD、SMA、布林带)、Tick 计数、成交计数等。目标是尽可能多地捕获有关流的描述性信息。过去,我也有标准差和方差,但我为了其他类型的数据而放弃了它们。通常,对于给定的股票代码,在 6 秒的增量中没有足够的数据来获取这些。
以下是每只股票每六秒捕获的 32 个不同方面。通过如此多的数据查看方式,我们可以很好地描述该股票在每个 6 秒时间段内发生的情况。每 6 秒,每只股票,BriefMaker
会收集以下信息
ID Format Short Name Init. Value New Values Description
0 float volume_day (none) always replace total volume for the day
1 float volume_ths 0 sum volume for this 6-sec period (calculated)
2 float largTrdPrc price_last conditional replace price at largest volume trade
3 float price_high price_last conditional replace highest price in period
4 float price_loww price_last conditional replace lowest price in period
5 float price_last price_last always replace last trade price
6 float price_bidd price_bidd always replace last bid price
7 float price_askk price_askk always replace last ask price
8 float volume_bid volume_bid always replace last bid size
9 float volume_ask volume_bid always replace last ask size
10 float price_medn (none) calculated Statistics median for last price
11 float price_mean (none) calculated Statistics mean for last price
12 float price_mode (none) calculated Statistics mode for last price
13 float buyy_price (none) calculated A prediction of what the buy price.
14 float sell_price (none) calculated A prediction of what the sell price.
15 float largTrdVol 0 conditional replace The size of largest trade.
16 float prcModeCnt 0 sum Statistics mode price count
17 float vol_at_ask 0 always replace Volume at ask price
18 float vol_no_chg 0 always replace Volume with no last change in last size.
19 float vol_at_bid 0 always replace Volume at bid price
20 float BidUpTicks 0 sum How many times the bid went up.
21 float BidDnTicks 0 sum How many times the bid went down.
22 float sale_count 0 sum # of trades counted
23 float extIndex00 overwritten calculated 6 sec. calculated ATR of last trades
24 float extIndex01 overwritten calculated 6 sec. calculated CCI of last trades
25 float extIndex02 overwritten calculated 6 sec. calculated EMA of last trades
26 float extIndex03 overwritten calculated 6 sec. calculated Kama of last trades
27 float extIndex04 overwritten calculated 6 sec. calculated RSI of last trades
28 float extIndex05 overwritten calculated 6 sec. calculated SMA of last trades
29 float extIndex06 overwritten calculated 6 sec. calculated SarExt of last trades
30 float extIndex07 overwritten calculated 6 sec. calculated MACD of last trades
31 float extIndex08 overwritten calculated 6 sec. calculated Bollinger bands of lasts
格式:为简单起见,所有值都存储为 float
类型。浮点数的一个缺点是其 7.2 位有效数字,但此精度通常超出了数据的精度。
初始值:这是每个摘要开始时的值。通常,它们重置为零或从前一个摘要继承。某些值指定为“覆盖”,因为它们在摘要完成时会被覆盖,因此不需要初始值。
新值:这是新流事件的操作。
- 始终替换 - 将始终用新值替换当前值。
- 条件替换 - 仅当满足条件时才替换值。例如,只有当
price_high
是新的高价时才会被替换。 - 求和 - 将新值加到运行总数中。
- 计算 – 值在每个摘要完成后计算。例如,这可能是一个卖出价格数组,然后输入到一个公式中。
提取摘要
要为应用程序提取摘要,请使用类似以下的示例…
有关摘要结构的更多详细信息,请在此 处 查找。
BinaryReader reader = new BinaryReader(new MemoryStream(lastBrfImage));
// Layout: |--HDRs+Indexes(32)--|----------------Stocks(32x32)----------------|
int briefID = reader.ReadSingle();
float Day = reader.ReadSingle();
float Hour = reader.ReadSingle();
float Minute = reader.ReadSingle();
float Second = reader.ReadSingle();
float DayOfWeek = reader.ReadSingle();
float MinutesSinceOpen = reader.ReadSingle();
float SecondsSinceOpen = reader.ReadSingle();
float HoursSinceOpen = reader.ReadSingle();
float RemainingHours = reader.ReadSingle();
float RemainingMinutes = reader.ReadSingle();
float TICK-NASD 4 = reader.ReadSingle();
float VOL-NASD_0 = reader.ReadSingle();
float VOL-NASD_1 = reader.ReadSingle();
float VOL-NASD_2 = reader.ReadSingle();
float AD-NASD_1 = reader.ReadSingle();
float AD-NASD_2 = reader.ReadSingle();
float TICK-NYSE_4 = reader.ReadSingle();
float VOL-NYSE_0 = reader.ReadSingle();
float VOL-NYSE_1 = reader.ReadSingle();
float VOL-NYSE_2 = reader.ReadSingle();
float AD-NYSE_0 = reader.ReadSingle();
float AD-NYSE_1 = reader.ReadSingle();
float AD-NYSE_2 = reader.ReadSingle();
float INDU_1 = reader.ReadSingle();
float INDU_2 = reader.ReadSingle();
float INDU_4 = reader.ReadSingle();
for (int s = 0; s < symbCt; s++) // Read in each ticker
{
symbols[s].volume_day = reader.ReadSingle();
symbols[s].volume_ths = reader.ReadSingle();
symbols[s].largTrdPrc = reader.ReadSingle();
symbols[s].price_high = reader.ReadSingle();
symbols[s].price_loww = reader.ReadSingle();
symbols[s].price_last = reader.ReadSingle();
symbols[s].price_bidd = reader.ReadSingle();
symbols[s].price_askk = reader.ReadSingle();
symbols[s].volume_bid = reader.ReadSingle();
symbols[s].volume_ask = reader.ReadSingle();
symbols[s].price_medn = reader.ReadSingle();
symbols[s].price_mean = reader.ReadSingle();
symbols[s].price_mode = reader.ReadSingle();
symbols[s].buyy_price = reader.ReadSingle();
symbols[s].sell_price = reader.ReadSingle();
symbols[s].largTrdVol = reader.ReadSingle();
symbols[s].prcModeCnt = reader.ReadSingle();
symbols[s].vol_at_ask = reader.ReadSingle();
symbols[s].vol_no_chg = reader.ReadSingle();
symbols[s].vol_at_bid = reader.ReadSingle();
symbols[s].BidUpTicks = reader.ReadSingle();
symbols[s].BidDnTicks = reader.ReadSingle();
symbols[s].sale_count = reader.ReadSingle();
symbols[s].extIndex00 = reader.ReadSingle();
...
symbols[s].extIndex08 = reader.ReadSingle();
}
关注点
在此项目的开始阶段,我遇到了一些不必要的复杂性。我需要一个能够几乎同时从多个线程接收、准备和上传数据的系统。经过一些尝试,我想到了一枚硬币。硬币的一面可以接收 StreamMoment
s,而另一面可以完成摘要并将其上传到数据库。每 6 秒,硬币就会翻转,一些数据会被传递下去。这种跳出常规的思维方式使程序更容易编写、维护和理解。它也使多线程变得更加简单。
读取和存储数据
从数据库读取 StreamMoments 记录
该程序读取六条一秒钟的 StreamMoment
记录来生成一个摘要。由于大部分源数据在程序启动时存储在数据库中,因此…
- 查找并加载已写入 SQL 数据库的最新摘要。目标是将系统状态重新加载到上次中断的地方。
- 根据最新的摘要,程序然后开始重放该时刻之前不久的
StreamMoment
s。目标是再次将系统状态重新加载到上次中断的地方。 - 在重放这些
StreamMoment
s 之后,它会继续并开始处理还没有摘要的新StreamMoment
s。 - 在通过读取所有
StreamMoment
s 或当前时刻完成同步后,它会等待来自新数据库记录或直接来自 WCF 连接的新StreamMoment
s。添加 WCF 方法纯粹是为了降低处理实时数据的延迟。
有关 StreamMoment
s 格式的详细信息,请在此 处 查找。
将完成的摘要写入数据库
在 BriefMaker
使用六个 StreamMoments
创建了一个新摘要后,它将其写入 briefs
表。最初,我有大量的列(32 个符号 x 32 个属性),但这会占用大量性能/内存,所以我将其更改为以字节图像格式记录。这更有效率,但数据使用起来没那么方便。将数据存储在每个列中,方便报告,但速度非常慢,而且占用大量内存。
在 briefs
表中,只有两列
BriefID
:存储为 TinyTime6Sec
格式。这是我自己的格式,但通过简单的类型转换可以轻松将其转换为 DateTime
。这有点像 DateTime
,但可以装入一个 32 位整数。TinyTime6Sec
的值基本上是 2010 年 1 月 1 日以来 M-F 上午 8 点至下午 4 点之间 6 秒增量的数量。我创建这种时间格式是为了 (a) 保持数据/时间字段小巧,以及 (b) 创建一个没有间隙的连续范围,我可以将其用作 ID。例如,ID 489394 将指代市场交易时段中的某个 6 秒时间段,而 4893945 将是下一个 6 秒时间段。
需要注意的是,TinyTime6Sec
不考虑节假日。即使存在有效的 TinyTime6Sec ID
,也不意味着市场在该天开市。周末会被跳过,但周六或周日没有有效值。
BriefBytes
:这是存储摘要的地方。如前所述,出于性能原因,它以字节图像格式存储。每个字节图像的大小为 4224 字节。(32 个头信息 + (32 个股票 x 32 个属性)) * sizeof(float)。
字节布局如下
偏移量 | 存储的数据 | 偏移量 | 存储的数据(续) |
0 | briefID(int) | 64 | VOL-NYSE(2).askPrice |
4 | 日 | 68 | TICK-NYSE(3).lastPrice |
8 | 小时 | 72 | VOL-NYSE(4).bidSize |
12 | 分钟 | 76 | VOL-NYSE(4).bidPrice |
16 | 第二种 | 80 | VOL-NYSE(4).askPrice |
20 | DayOfWeek | 84 | AD-NYSE(5).bidSize |
24 | TotalMinutes | 88 | AD-NYSE(5).bidPrice |
28 | TotalSeconds | 92 | AD-NYSE(5).askPrice |
32 | HoursSinceOpened | 96 | DJIA(6).bidPrice |
36 | HoursRemaining | 100 | DJIA(6).askPrice |
40 | MinutesRemaining | 104 | DJIA(6).lastPrice |
44 | TICK-NASD(0).lastPrice | 108-127 | note used |
48 | VOL-NASD(1).bidSize | 128-255 | Ticker 1 (see table) |
52 | VOL-NASD(1).bidPrice | 256-383 | Ticker 2 (see table) |
56 | AD-NASD(1).askPrice | … | |
60 | VOL-NYSE(2).bidPrice | 4096-4223 | Ticker 32 |
查看摘要
包含一个小型查看器程序,以便轻松查看摘要。如果无法查看输出,运行 BriefMaker 将不会很有趣!
要查看摘要,请启动查看器应用程序,然后使用“股票图表”选项卡或“摘要原始数据”选项卡。两个选项卡都显示相同的数据,但视图不同——图表 vs 表格。这里只使用这两个选项卡。
摘要原始数据视图如下所示
而图表视图...
可执行文件包含在本页顶部的下载中,源代码可以在 这里 找到。
限制 / 缺点
在下载此项目之前,可能需要审查一些限制/令人不快之处。我想与大家分享这些,这样他们就不必下载项目并自己找出这些问题了。 =)
BriefMaker
目前大部分是硬编码以处理 32 个符号。要使用更多/更少,需要修改一些代码。这不会太难。BriefMaker
以专有的TinyTime6Sec
格式存储每个摘要的时间。这基本上是从 2010 年 1 月 1 日开始的 M-F 上午 8 点至下午 4 点的 6 秒间隔数。可以使用TinyTime6Sec
类轻松地将TinyTime6Sec
转换为DateTime
。- 无 Level II 市场数据
愿望清单
一些我想在未来添加的项目:(但不确定何时或是否会完成)
- 切换到
QLNet
库(使用QuantLib
)进行量化金融事务。这是一个比 TA-Lib 更新的项目。C# TA-Lib 是一个很棒的库,但不幸的是自 2007 年以来没有更新。 - 去除硬编码的“32 个符号”要求。
- 添加 Level II 数据。
安装说明
- 下载数据库,解压缩,然后使用 SQL Server 管理器,将其附加为
Focus
名称。 - 下载并解压缩代码或可执行文件。如果您下载代码,则需要生成项目。
- 打开 .config 文件,首先查看
BriefsConnectionString
。 您可能需要根据您的设置编辑此连接字符串。对于常规 SQL Server,通常是 "Data Source=.;Initial Catalog=Focus;Integrated Security=True
",对于 SQL Server Express,则使用 "Data Source=.\SQLEXPRESS;Initial Catalog=Focus;Integrated Security=True
"。同时查看BeginRecordTime
、EndRecordTime
和PreBeginBufferTime
。这些应设置为您当地的市场开/收盘时间。 - 现在运行
BriefMaker
。如果出现错误,您可以查看日志窗口或使用 Visual Studio 的调试器。 BriefMaker
完成后,启动 Viewer.exe 查看输出。同样,您可能需要在启动应用程序之前调整查看器的FocusConnectionString
。打开后,使用“股票图表”和“摘要原始数据”选项卡查看数据。其他选项卡不用于BriefMaker
。
要求
- .NET 4.5
- SQL Server 或 SQL Express (免费)
历史
- 2016 年 3 月 23 日:初始版本