模拟 FIX 交易服务器






4.92/5 (16投票s)
一个模拟 FIX 交易服务器,用于测试 FIX 交易客户端。
引言
在2013年炎热的夏天(嗯,对英国来说),我们Heathmill公司决定创建一个演示交易UI客户端,该客户端可以使用自动化交易(AT)规则。按照金融界的标准,我们希望客户端使用FIX协议进行通信,但是我们找不到一个模拟FIX服务器或引擎来测试我们的客户端。
如下所述,这个FIX服务器可用于测试FIX客户端应用程序。它处理来自客户端的传入FIX会话连接和订单,并使用基本的自动匹配行为来允许订单交易。本文主要介绍系统的领域模型、FIX消息处理以及自动匹配服务器的设计和实现。另一篇文章将介绍通过合成订单进行的自动化交易以及AT订单簿演示交易客户端本身。
这不是一篇关于交易的文章(我也没资格写这样的文章),所以我不会深入探讨交易的任何细节。如果您想了解高杠杆、裸卖空、艾略特波浪或任何类似风险,我建议您访问Investopedia作为入门。
代码
服务器使用C# 4.0和.NET 4,通过Visual Studio 2010编写。它使用NuGet获取第三方包(构建说明1)。
示例客户端
解决方案文件还在Heathmill.FixAT.ATOrderBook
项目中包含一个用于服务器的示例客户端。另有一篇Code Project文章专门讨论该客户端。
那么服务器是做什么的?
- 接受来自 FIX 客户端的连接
- 支持“直到取消为止”的限价订单
- 自动匹配订单以执行交易
- 向已连接的客户端发送订单和交易的执行报告
- 支持 FIX 4.2 和 FIX 4.4 会话
如果以上任何内容让您感到困惑,请不要担心,我将在文章的其余部分解释所用术语的更多细节。
什么是 FIX?
FIX 代表金融信息交换(Financial Information eXchange),是90年代设计的一种协议,旨在为电子交易系统创建一种通用语言。在一个非常简单的层面上,FIX 定义了一系列消息和消息交换工作流,用于表示金融信息和工作流。
有关 FIX 的更多详细信息,请参阅 维基百科文章 和 FIX 协议组织 网站。在介绍我们的领域模型之后,下面的“我们如何使用 FIX?”部分将详细介绍我们如何实际使用 FIX。
领域模型
在 Heathmill,我们是领域驱动设计的拥趸(另请参阅 Eric Evans 同名巨著2,其中对该主题进行了精彩的讨论),因此我们决定为我们的系统创建一个简单的领域模型。该领域以订单(Order)的概念以及订单如何排序和匹配为中心,并在 AT 订单簿客户端和服务器之间共享。
作为领域模型,本节也作为我们交易平台核心领域概念的介绍。如果您熟悉交易系统,那么您可以直接跳到下面的“我们如何使用 FIX?”部分或“服务器设计”部分,具体取决于您想了解或觉得有趣的内容。
Order
订单是任何交易平台的核心概念之一。订单代表某人愿意支付多少钱来买入或卖出特定数量的特定合约。例如,如果我愿意以每股38美元的价格购买1000股微软股票,那么我将向系统输入一个买入订单(也称为Bid订单),内容为{价格=38,数量=1000,合约=MSFT,方向=买入}。如果您愿意以39美元的价格出售500股微软股票,您将向系统输入一个卖出订单(也称为Ask订单或Offer订单),内容为{价格=39,数量=500,合约=MSFT,方向=卖出}。
订单有时用数量@价格的常见简写表示,例如,上例中的1000@38和500@39。系统的用户通常会看到他们感兴趣的每种合约的订单堆栈,其中订单堆栈是该合约的买入和卖出订单列表,按价格排序,以便最优价格位于堆栈顶部。
订单类型
FIX支持多种订单类型;由于此服务器仅用于测试目的,因此其支持的内容最好通过代码展示
// TODO Support the other types in QuickFix.Fields.OrdType
public enum OrderType
{
//Market,
Limit,
//Stop,
//StopLimit,
}
限价订单是指以指定价格或更优价格买入或卖出特定数量商品的订单。如果您对其他订单类型感到好奇,那么值得在 Investopedia 上查找它们。
由于服务器是为了测试和演示 AT 订单簿,以及它能够创建模仿现有服务器不支持的订单类型的合成订单,因此服务器上唯一支持的订单类型是“直到取消有效”的订单,没有隐藏数量,也没有“全部或无”选项。
实际上,像“全部或无”(All-or-None)或“立即或取消”(Immediate-or-Cancel)这样的订单行为很难创建合成版本,几乎肯定需要在服务器上实现。然而,其他订单类型,如“直到日期有效”(Good-til-Date)、市价订单(Market Orders)和止损/限价订单(Stop/Limit Orders),都是合成订单类型的候选。隐藏数量 / 冰山订单实际上是 Heathmill AT 订单簿客户端的示例合成订单类型。
契约
如上所述,每个订单都针对给定合约下单。在我们这里创建的系统中,合约简单地表示为一个符号(例如 MSFT、AAPL、EURUSD)。这对于基本的股票或外汇交易系统来说已经足够,但许多其他系统(商品、期权、股票衍生品等)拥有更为复杂的合约,涉及交割日期、行权价格、标的合约等。
对于我们所需的测试,简单的符号合约就足够了。扩展合约以包含合约到期、近月滚动、期权定价等复杂性(就像许多其他事情一样)留给读者作为练习。
订单排序
如上所述,订单通常会排序,以便“最佳”订单位于列表(“堆栈”)的顶部。对于买入订单,这是价格最高的订单(愿意支付最多的人),对于卖出订单,这是价格最低的订单(愿意以最少价格出售的人)。
价格相同的订单必须使用不同的标准进行排序,在我们的案例中,排序是按照上次更新时间更早、然后数量更大、最后是任意的平局决胜(内部ID)进行的。这是一种相当标准的交易平台订单排序方式,目的是奖励那些在市场上以该价格持有订单时间最长的交易者。
正如您可以想象的那样,在考虑订单的自动匹配时,订单排序非常重要,说到这里。
订单匹配
广义上说,交易平台上有两种进行交易的方式。第一种通常称为“点击交易”,即用户主动选择他们希望交易的订单(通常通过双击或右键单击,因此得名),服务器将订单从系统中移除并生成交易3。我们的服务器不支持点击交易,尽管即将发布的关于我们的AT订单簿客户端的文章展示了它使用了一个常见的技巧来规避服务器的限制,即在市场的另一侧添加一个匹配订单。
我们的服务器支持另一种主要的交易方式:自动匹配。当存在相同价格的买入订单(Bid)和卖出订单(Ask)时,服务器会自动生成交易:如果我准备支付38.50美元购买MSFT股票,而你准备以38.50美元出售你的MSFT股票,那么我们就有了一笔交易!
自动匹配(通常简称为匹配)还必须考虑在给定市场的一侧,在最优价格(又称订单簿顶部)存在多个订单的情况。如果在买方(Bid)侧有两个100@38.50的订单,而您输入一个200@38.50的卖方(Offer)订单,那么如果只交易其中一个买入订单,您会不高兴,因此匹配算法必须计算出市场两侧在订单簿顶部的总可用数量。
我们的服务器使用的匹配算法并不复杂;匹配中一些棘手的问题(例如“全部或无”订单、“立即或取消”订单)不在我们的系统中。它只是获取订单簿顶部的所有订单,计算哪一方的数量最少,然后轮流匹配每一方市场的订单,直到该数量被填满。
一如既往,真相尽在代码中,您可以通过查看StandardOrderMatcher.cs
中的代码来了解所有细节。
我们如何使用FIX?
Heathmill FIX AT 客户端和服务器通过 QuickFIX/n 库进行通信。QuickFIX/n 是 C++ QuickFIX 库的 .NET 本机版本,通过类型安全的消息处理和 FIX 的 Initiator / Acceptor 模式实现,支持 FIX 4.1、4.2、4.3、4.4 和 5.0。QuickFIX/n 通过套接字使用 TCP 进行通信。
我们的服务器实现了对 FIX 4.2 和 FIX 4.4 的支持,主要作为我们如何支持来自多个 FIX 版本的客户端连接的示例。
主应用程序类(ServerApplication
)是 QuickFix.IApplication
接口的实现,并继承自 QuickFix.MessageCracker
,这是 QuickFIX/n 推荐的。
为了抽象 FIX 和 FIX 版本控制的细节,并为了提高可测试性,我们在发送 FIX 消息时使用了外观模式(Facades)。我们还将 FIX 消息的生成和转换分离到一个兼容领域驱动设计的服务程序集 Heathmill.FixAT.Services
中。
Heathmill.FixAT.Services
此服务程序集处理 FIX 消息的生成以及领域对象与 FIX 字段和消息之间的转换。IFixMessageGenerator
接口提供了一种使调用代码与 FIX 版本无关的方式,而将例如 NewOrderSingle 消息转换为订单的详细工作由 TranslateFixMessages
和 TranslateFixFields
处理。
FIX 消息工作流
对于给定的金融工作流,存在一个 FIX 工作流,描述要发送哪些 FIX 消息以及发送顺序。在本节中,我们将介绍 Heathmill 系统支持的工作流,并展示每种情况下发送的 FIX 消息。在每种由交易员/客户端启动的工作流中,第一个用例图代表成功情况(例如,新订单被接受),而第二个用例图代表失败情况(例如,新订单因无效价格等原因被拒绝)。
这些用例展示了正在发送的消息类型以及区分消息的关键字段(例如,ExecutionReport 的 ExecType)。还有一系列其他字段需要填写各种消息(例如 LeavesQty、CumQty、AvgPrice),这些字段用于指示系统的状态。有关处理消息字段的完整详细信息,请参阅代码(例如 Fix44MessageGenerator
)。此外,fixwiki.org 是解释消息字段及其预期值的优秀资源。
用户添加订单
用户取消订单
用户更新订单
更新现有订单时,我们通过客户端的OrderCancel和NewOrderSingle来实现,因为我们没有时间扩展服务器来处理OrderCancelReplace消息。因此,目前它的实现方式与上面“用户取消订单”然后“用户添加订单”的工作流相同。
但是,如果时间允许,我们**应该**有的工作流程是
匹配时服务器填充订单
请注意,对于部分成交,应使用ExecType.PARTIAL_FILL
和OrdStatus.PARTIALLY_FILLED
。
ClOrdID
系统中所有订单在提交时都会由客户端分配一个标识符,称为 ClOrdID。取消订单时,也必须为该订单指定一个新的 ClOrdID。根据 fixwiki.org:
“由买方(机构、经纪商、中介等)分配的订单唯一标识符……在单个交易日内必须保证唯一性……”
由于我们的系统只是一个演示系统,客户端简单地使用一个基于订单类型,带有基线偏移的递增计数器。
ExecID
您可能会从代码中注意到,一些服务器生成的 FIX 消息具有一个 execID 字段。引用 fixwiki.org:
“由卖方(经纪商、交易所、ECN)分配的执行消息的唯一标识符”
在我们的案例中,这只是一个由服务器维护和递增的单调递增计数器。商业服务器需要确保此标识符确实是唯一的,并且(再次引用 fixwiki.org)
“必须在单个交易日或多日订单的生命周期内保证唯一性。”
在我们的服务器上,此 ID 不会持久化,因此服务器重启意味着 ID 将被重置,所以日内重置将是一件糟糕的事情。这又是生产与原型之间的区别!
服务器设计
服务器基于几种设计模式(命令、外观和策略)、存储库和中介,并巧妙地使用了队列、消息处理程序和任务。
主要的底层结构类在下面的类图中显示。为保持可读性,未显示方法、属性等。
架构相对简单:核心是一个带有命令处理器的输入命令队列和一个带有命令处理器的输出命令队列。传入的 FIX 消息由 IFixMessageHandler
处理,转换后的命令被放入输入队列。输入命令处理器使用 Task
异步处理输入命令,并将一个命令放入输出队列以指示结果。然后,输出命令处理器同步处理输出命令(例如 SendAcceptNewOrder
、SendRejectNewOrder
),并将适当的消息发送回客户端,以及必要时发送给任何其他已连接的客户端。
我将在下一节介绍处理这些命令的一些实际机制。
服务器实现:亮点回顾
服务器的代码量相当大,其中一些涉及任何此类系统都必须做的平凡内务管理任务(例如,存储订单然后再次查找它们)。如果我逐行讲解代码,可能会让您感到非常无聊,所以我们不如更详细地从代码层面探讨服务器中一些更重要的事情。
命令、队列和处理器
服务器中的主要处理机制是基于命令模式。命令派生自典型的命令接口 ICommand
namespace Heathmill.FixAT.Server.Commands
{
internal interface ICommand
{
void Execute();
}
}
每个派生的命令类在执行时都会执行所需的任何操作。作为一个例子,我们将在下面的“服务器接收新消息”部分中查看Commands.AddOrder
所做的事情。各种派生的命令是通过CommandFactory
创建的。
有两个队列:一个用于传入命令的输入队列和一个用于传出命令的输出队列。每个队列都是CommandQueue
的实例,它只是一个带有简单锁定的Queue<ICommand>
的包装器
internal class CommandQueue : ICommandQueue
{
private readonly Queue<ICommand> _queue = new Queue<ICommand>();
private readonly object _queueLock = new object();
public void Enqueue(ICommand command)
{
lock (_queueLock)
{
_queue.Enqueue(command);
}
}
public ICommand Dequeue()
{
lock (_queueLock)
{
return _queue.Count > 0 ? _queue.Dequeue() : null;
}
}
public void Clear()
{
lock (_queueLock)
{
_queue.Clear();
}
}
}
生产服务器无疑会需要一个稍微更重量级的排队机制,例如一个适当的生产者-消费者队列、一个阻塞的循环缓冲区或一些其他方法,允许控制队列的大小以及可能控制项目入队和出队的速度。
传入的 FIX 消息被转换为命令并放入输入命令队列。然后由输入 CommandProcessor
处理。CommandProcessor
创建一个长时间运行的任务,从输入队列中取出命令,然后使用提供给处理器的命令处理策略在 ICommand
对象上调用 Execute
internal class CommandProcessor
{
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ICommandQueue _commandQueue;
private readonly ICommandProcessingStrategy _processingStrategy;
public CommandProcessor(ICommandProcessingStrategy processingStrategy,
ICommandQueue commandQueue)
{
_processingStrategy = processingStrategy;
_commandQueue = commandQueue;
_cancellationTokenSource = new CancellationTokenSource();
var token = _cancellationTokenSource.Token;
var task = new Task(
() =>
{
while (!token.IsCancellationRequested)
{
var cmd = _commandQueue.Dequeue();
while (cmd != null)
{
_processingStrategy.ProcessCommand(cmd.Execute);
cmd = commandQueue.Dequeue();
}
Thread.Sleep(100);
}
},
token,
TaskCreationOptions.LongRunning);
task.Start();
}
public void Stop()
{
_cancellationTokenSource.Cancel();
}
}
输入队列的处理策略是一个异步的、基于任务的策略
internal class TaskBasedCommandProcessingStrategy : ICommandProcessingStrategy
{
private readonly TaskFactory _taskFactory = new TaskFactory();
public void ProcessCommand(Action processingFunction)
{
_taskFactory.StartNew(processingFunction);
}
}
请注意,这意味着服务器不是一个实时服务器:没有任何东西可以保证任何给定的输入命令将在特定时间段内执行。对于实时服务器,您需要某种任务调度器来确保任务在一定时间限制内完成,例如 Alexy Shelest 在其关于实时 WPF 交易 UI 的出色 Code Project 文章中所示。
当输入命令执行时,它们会根据其操作结果将一个任务放置在输出队列上(例如,如果 AddOrder
任务成功,则为 SendAcceptNewOrder
)。输出队列和输出命令处理器的工作方式与输入队列完全相同,只不过在这种情况下,命令处理策略是同步的
internal class SynchronousCommandProcessingStrategy : ICommandProcessingStrategy
{
public void ProcessCommand(Action processingFunction)
{
processingFunction();
}
}
放置在输出队列中的命令将传入 FIX 消息的结果反馈给发送方 FIX 会话(以及在适当情况下反馈给其他已连接的 FIX 会话),或者在订单匹配发生时发送通知。
当 FIX 客户端连接时
当 FIX 客户端连接时,它通过创建一个 FIX 会话来实现;所有与客户端的通信随后都通过此会话进行。
在 QuickFix.MessageCracker
类的基础上,并实现了 QuickFix.IApplication
,主应用程序类(ServerApplication
)接收传入的 FIX 会话,并根据会话使用的 FIX 版本分配一个消息处理程序。
public class ServerApplication : MessageCracker, IApplication
{
// ...
public void OnCreate(SessionID sessionID)
{
try
{
_sessionMediator.AddSession(sessionID, GetHandler(sessionID));
}
catch (FixATServerException e)
{
_messageCallback("ERROR: " + e.Message);
}
}
// ...
private IFixMessageHandler GetHandler(SessionID sessionID)
{
switch (sessionID.BeginString)
{
case BeginString.FIX42:
return _fix42MessageHandler;
case BeginString.FIX44:
return _fix44MessageHandler;
default:
throw new FixATServerException(
string.Format("FIX version {0} not supported by server",
sessionID.BeginString));
}
}
// ...
}
来自该 FIX 会话的传入消息随后由与该会话关联的 IFixMessageHandler
实例处理。
当会话连接时发生的另一个主要事件是发生在 ServerApplication.OnLogon
方法中
public void OnLogon(SessionID sessionID)
{
try
{
_sessionMediator.SessionLoggedIn(sessionID);
_sessionMediator.SendOrders(sessionID, _orderMediator.GetAllOrders());
}
catch (FixATServerException e)
{
_messageCallback("ERROR: " + e.Message);
}
}
除了通知 SessionMediator
会话已登录之外,它还将服务器上的所有订单发送给新连接的会话。新连接的客户端必须收到系统中现有的订单,否则交易员将只能与自己交易;您会明白,这对于交易平台来说非常重要!
服务器从客户端接收消息
这是几乎所有服务器的关键部分;那么当我们的服务器从客户端收到传入的 FIX 消息时会发生什么呢?我们将通过一个 FIX 4.4 客户端尝试通过 NewOrderSingle FIX 消息向系统添加新订单的案例。
接收消息并将命令放入输入队列
消息通过 ServerApplication 中的 QuickFix.IApplication.FromApp
方法接收
public void FromApp(Message message, SessionID sessionID)
{
_messageCallback("IN: " + message);
try
{
Crack(message, sessionID);
}
catch (UnsupportedMessageType)
{
_messageCallback(
string.Format("Unsupported message type: {0}", message.GetType()));
}
}
然后它会调用 QuickFix.MessageCracker.Crack
,该方法使用反射来调用正确的 OnMessage
重载,在此例中为
public void OnMessage(NewOrderSingle n, SessionID sessionID)
{
_fix44MessageHandler.OnMessage(n, sessionID);
}
Fix44MessageHandler.OnMessage
重载调用服务程序集将 FIX 消息转换为 OrderDetails
类,然后请求 MessageHandlerCommandFactory
(一个围绕常规 CommandFactory
的包装器,增加了便利方法并处理 FIX SessionID
)将 AddOrder
命令入队
internal class Fix44MessageHandler : IFixMessageHandler
{
// ...
public void OnMessage(NewOrderSingle n, SessionID sessionID)
{
var execID = _execIdGenerator();
try
{
var orderData = TranslateFixMessages.Translate(n);
_commandFactory.EnqueueAddOrder(_messageGenerator, sessionID, orderData, execID);
}
catch (QuickFIXException e)
{
var rejectMessage = "Unable to add order: " + e.Message;
var message = CreateFix44Message.CreateRejectNewOrderExecutionReport(n,
execID,
rejectMessage);
_fixFacade.SendToTarget(message, sessionID);
}
}
// ...
}
MessageHandlerCommandFactory.EnqueueAddOrder
随后查找内部会话 ID,使用 CommandFactory
创建一个 AddOrder
命令,并将其添加到输入队列中。
internal class MessageHandlerCommandFactory
{
// ...
public void EnqueueAddOrder(IFixMessageGenerator messageGenerator,
SessionID sessionID,
OrderData orderData,
string execID)
{
var internalSessionID = _sessionMediator.LookupInternalSessionID(sessionID);
var cmd = _commandFactory.CreateAddOrder(messageGenerator,
internalSessionID,
orderData,
execID);
_commandFactory.IncomingQueue.Enqueue(cmd);
}
// ...
}
所以我们现在输入队列中有一个 AddOrder
命令。在不久的将来,输入命令处理器将从输入队列中获取该命令,这就是我们重新开始行动的地方。
处理 AddOrder 命令
如上文“命令、队列和处理器”一节所示,输入命令处理器从输入命令队列中取出命令,并启动一个Task
来调用命令上的Execute
;AddOrder.Execute
包含
internal class AddOrder : ICommand
{
// ...
public void Execute()
{
try
{
var order = _orderMediator.AddOrder(// ... order details);
var successCmd = _commandFactory.CreateSendAcceptNewOrder(_sessionID, order);
_commandFactory.OutgoingQueue.Enqueue(successCmd);
// Kick off matching for the contract given we have a new order
var matchOrders = _commandFactory.CreateMatchOrders(_orderData.Symbol);
_commandFactory.IncomingQueue.Enqueue(matchOrders);
}
catch (FixATServerException e)
{
var rejectMessage = "Unable to add order: " + e.Message;
var rejectCmd = _commandFactory.CreateSendRejectNewOrder(// ...);
_commandFactory.OutgoingQueue.Enqueue(rejectCmd);
}
}
}
如您所见,这正是行动发生的地方。首先,它要求 OrderMediator
添加一个新订单。OrderMediator
在将订单添加到 OrderRepository
之前执行各种验证。如果任何验证失败,它将抛出 FixATServerException
,并且处理代码会将 SendRejectNewOrder
命令添加到输出队列。在这种情况下,我们将直接跳到下面的“处理输出命令”部分。但是,现在我们假设订单是有效的并成功添加到 OrderRepository
。
在订单成功添加的情况下,代码会做两件事:首先,它创建一个 SendAcceptNewOrder
命令到输出队列,表示成功;然后,它继续为订单的符号添加一个 MatchOrders
命令到**输入**队列。由于系统中有一个新订单,现在该符号存在新的可能匹配项,MatchOrders
命令将启动订单匹配。在下面的订单匹配部分之前,我们暂时不谈匹配。
所以我们现在输出队列中有一个 SendAcceptNewOrder
或 SendRejectNewOrder
命令,是时候跳到输出队列 CommandProcessor
接收我们新添加的命令的地方了。
处理输出命令
当输出 CommandProcessor
从队列中取出输出命令并执行它时,我们回到正题。乐观一点,让我们详细看看 SendAcceptNewOrder
的情况,我将在最后简要提及 SendRejectNewOrder
失败的情况
internal class SendAcceptNewOrder : ICommand
{
// ...
public void Execute()
{
_sessionMediator.NewOrderAccepted(_sessionID, _order);
}
}
那只是把它交给了 SessionMediator
,那么它做了什么呢?
internal class SessionMediator
{
// ...
public void NewOrderAccepted(FixSessionID ownerSessionID, IOrder order)
{
var orders = new List<IOrder> {order};
foreach (var sessionID in GetAllLoggedInSessions())
{
SendOrders(sessionID, orders);
}
}
// ...
public void SendOrders(FixSessionID sessionID, List orders)
{
var fixID = _sessionIDMap.GetBySecond(sessionID);
Action<IFixMessageHandler> messageSendF =
handler => handler.SendOrdersToSession(fixID, orders);
_sessionRepository.SendMessageToHandler(sessionID, messageSendF);
}
// ...
}
由于订单已添加到系统中,我们需要通知所有已连接的客户端,因此循环遍历所有已登录的会话。SendOrders
然后查找外部会话 ID (QuickFix.SessionID
) 并请求 SessionRepository
通过调用 SendMessageToHandler
并传递一个 Action
来调用 IFixMessageHandler.SendOrdersToSession
来向 FIX 会话发送消息。
internal class SessionRepository : ISessionRepository
{
private readonly object _lock = new object();
private readonly Dictionary<FixSessionID, SessionContext> _sessions =
new Dictionary<FixSessionID, SessionContext>();
public void AddSession(FixSessionID sessionID, IFixMessageHandler messageHandler)
{
lock (_lock)
{
_sessions.Add(sessionID, new SessionContext(messageHandler));
}
}
// ...
public void SendMessageToHandler(FixSessionID sessionID, Action<IFixMessageHandler> f)
{
lock (_lock)
{
var handler = _sessions[sessionID];
f(handler.MessageHandler);
}
}
}
如果您记忆力特别好(或者刚刚重读了“当 FIX 客户端连接时”一节),您可能会记得,当 FIX 会话连接时,它会根据所使用的 FIX 版本分配相应的 IFixMessageHandler
派生类。在这里,我们可以看到,当会话被添加到 AddSession
中的存储库时,这个 IFixMessageHandler
被存储在一个 Dictionary
中(通过一个 SessionContext
对象)并与会话的 FixSessionID
关联。现在所有需要做的事情就是为新订单生成 FIX 消息(一个带有 ExecType.NEW 的 ExecutionReport)并将其发送到 FIX 会话,这就是 SendOrdersToSession
所做的
internal class Fix44MessageHandler : IFixMessageHandler
{
// ...
private readonly IFixFacade _fixFacade;
private readonly IFixMessageGenerator _messageGenerator;
// ...
public void SendOrdersToSession(SessionID sessionID, IEnumerable<IOrder> orders)
{
foreach (var order in orders)
{
var msg =
_messageGenerator.CreateNewOrderExecutionReport(order, _execIdGenerator());
_fixFacade.SendToTarget(msg, sessionID);
}
}
// ...
}
以上是成功案例,如果订单验证失败并被拒绝,我们该怎么办?嗯,非常相似,只是 SendRejectNewOrder
命令会生成一个带有 ExecType.REJECTED 字段的 ExecutionReport,该报告仅发送给尝试添加订单的会话。
订单匹配
正如我们上面所看到的,当一个新订单成功添加到系统时,一个 MatchOrders
命令会被添加到输入队列中。当执行时,这个命令会触发给定符号的匹配。
这将触发 StandardOrderMatcher
自动匹配订单并生成一个 OrderMatch
对象列表,每个对象都详细说明了完全或部分匹配。然后,这些匹配被转换为 ExecutionReport FIX 消息并发送给已连接的客户端。为了使本文比《战争与和平》短,我们在此不再深入讨论,但如果您想自行研究,请从 MatchOrders.Execute
开始,沿着黄砖路前进。
单元测试
有一些单元测试,虽然没有我真正想要的那么多。领域覆盖率接近可容忍的水平,但系统其余部分的覆盖率不佳。在开发过程中特别有用的是订单排序测试和匹配测试,它们使得处理那些总是很棘手的匹配边界情况比手动测试容易得多。
毋庸置疑,对于商业系统,我强烈建议进行更广泛的单元测试、集成测试和系统测试。
回顾
正如引言中所述,该服务器是在2013年夏天创建的。现在是2014年春天,这是我几个月来第一次查看它。回顾代码时,我事后发现了一些事情。毫无疑问,还有许多其他可以改进的地方(代码中也有很多 TODO 建议),但在撰写本文时,我想到的是这些:
- 输入命令可能不应该直接将回复添加到输出队列中,应该有一个某种形式的 Mediator / Governor 类来控制将回复命令添加到输出队列。任务可以向此类指示成功或失败,或向其提供返回的
Command
对象,然后 Mediator / Governor 将适当的命令添加到队列中。- 此外,
AddOrder
命令绝对不应该将MatchOrders
命令添加到输入队列。这同样应该是AddOrder.Execute
指示成功时由 Mediator / Governor 完成的事情。
- 此外,
- 为什么输出队列是同步而非异步?我**认为**是为了确保客户端不会收到某些乱序的消息,例如在添加新订单并立即匹配的情况下,客户不会在收到订单添加消息之前收到订单已成交消息的风险。
- 如果依赖注入通过 DI 框架(例如 MEF、Castle Windsor)完成,而不是让
ServerApplication
到处创建和传递接口,那么代码会更整洁,也更容易创建测试。 - 为什么有些接口实现具有不同的命名约定?例如,对于
IFoo
,有些叫做StandardFoo
,而另一些则直接叫做Foo
。这肯定只是因为星期几和没有足够时间进行重构等风格问题。
结论
我们介绍了 Heathmill 模拟 FIX 交易服务器,并展示了系统使用的领域模型。我们简要讨论了系统如何使用 FIX 消息进行通信,然后查看了服务器的设计,并详细介绍了服务器代码库中的选定部分。
FIX 消息基于客户端发送 NewSingleOrder 和 OrderCancelRequest 消息,服务器回复各种形式的 ExecutionReports。
服务器基于命令模式,并在命令和 FIX 消息之间进行转换。它使用带有命令处理器的输入和输出队列来执行命令,并依赖排序的订单堆栈来自动匹配订单。
我们详细介绍了当 FIX 会话连接时、当客户端向服务器发送 NewOrderSingle FIX 消息时以及当服务器完全或部分匹配订单时使用的代码。
您可能会问:“我可以使用这个服务器来测试我的 FIX 客户端吗?”当然可以,请尽管使用!它就是为此而生的。
鉴于此,您很可能会问:“我可以使用这个服务器用于商业目的吗?”没有什么能阻止您。您应该这样做吗?绝对不应该!嗯,除非您喜欢非常愤怒的客户。
我已经用2个连接的客户端和一些基本工作流程测试了服务器,但它肯定还没有准备好投入生产;它被设计和编写用于测试,应该这样看待。它可能在更多客户端的情况下发生死锁,没有什么能阻止一个客户端每秒发送数千条消息并随着输入队列膨胀到巨大而耗尽服务器内存,在系统中只有几个订单的2个客户端之外的性能尚未测试,它不是实时的,它不持久化任何数据,没有权限管理,因此所有客户端都可以看到所有订单的所有细节,以及可能许多其他此类“功能”正等着给您带来惊喜和“乐趣”。
致谢
服务器代码使用了名为 Heathmill.FixAT.Utilities.BidirectionalDictionary
的类。这个类几乎完全基于Jon Skeet 的众多优秀 Stack Overflow 答案之一。我几乎可以肯定,DictionaryExtensions
中的至少一些方法也来自 Stack Overflow,但我记不清是从哪里来的(如果它们确实如此)。从统计学上讲,它们也很可能是 Jon Skeet 的,因为他自己可能回答了所有 C# Stack Overflow 问题中的 50% 以上……
脚注
- 构建解决方案
- 如果您以前没有使用过 NuGet,请使用 Visual Studio 扩展管理器(通过“工具”菜单或下载)安装 NuGet
- 恢复 NuGet 包
- 右键单击“解决方案资源管理器”中的解决方案,然后选择“启用 NuGet 包还原”
- 这将在解决方案目录下创建一个 .nuget 目录
- 右键单击“解决方案资源管理器”中的解决方案,然后选择“管理解决方案的 NuGet 包...”
- 点击右上角的“恢复”
- 更多信息请参阅NuGet 包还原页面
- 右键单击“解决方案资源管理器”中的解决方案,然后选择“启用 NuGet 包还原”
- 照常构建解决方案
- 领域驱动设计;Evans, Eric;Addison Wesley;ISBN-10: 0321125215;ISBN-13: 978-0321125217
- 点击交易远不止选择一个价格那么简单。大多数系统允许用户清扫成交量(即交易多个订单以达到特定数量)、同时批量处理多个订单或只交易订单数量的一部分。例如,交易前风险或信用系统可能会阻止某些交易对手相互交易,因此某些订单可能无法交易。商业交易平台在处理点击交易时需要考虑很多因素;值得庆幸的是,一个演示系统可以做一些简单得多的事情(在这种情况下,就是不支持它!)。
历史
- 2014-04-10 初始版本
- 2014-06-05 添加了 AT 订单簿客户端的代码作为示例 FIX 客户端,并添加了指向其 Code Project 文章的链接。