
在周期的最后一篇文章中 ,我们熟悉了交换订单的类型。 今天,我们将分析订单,应用程序的处理以及与贸易信息存储组织有关的问题。
供求关系
当然,您还记得经济过程中的供求定律,它显示了价格形成市场的机制:
同样的机制也可以进行交流。
订单簿是一个列表,在其中输入了买卖双方的限价订单,从而显示了对特定金融工具的当前兴趣。
如果将先前的图表相对于订单簿进行转换,则会得到如下所示的内容:
在这里,我们可以看到,当最大需求价格和最小要约价格相等时,便获得了市场价格。 点差是这些价格的差额。 这是重要的指标,因为它与工具的流动性相关。 点差越小,工具的流动性越强。 为了确保交易所交易框架内的流动性,他们经常对最大点差施加限制,超过此上限就可以停止交易。
创建应用
考虑一个应用程序的生命周期,从准入,交换到实现或取消。 为简单起见,我们将考虑外汇市场的情况。 一个特殊的过程负责处理订单的逻辑,我们称其为市场控制者。
因此,参与者创建了一个应用程序,该应用程序可以进行交换。 控制器必须确保参与者有足够的流动性来创建请求的订单类型。 信息源可以是内部会计服务或任何外部API。
为了立即执行该应用程序,市场上必须存在一个所谓的配对应用程序。
如果找到相反的顺序,则完全执行较小的顺序,部分执行较大的顺序。 当然,如果应用程序的交易指令允许部分执行。 在没有反订单的情况下,新订单将落入订单簿并在其类型的订单列表中占据位置。
由于只有待处理的订单属于订单簿,因此对于其他类型的订单,您需要选择列表。
在所有订单清单中,买方应按降序排序,卖方应按升序排序。 每一边的限价单清单的第一个元素分别形成供需的最佳价格。
另一个重要点是执行顺序。 控制器必须实现FIFO。 因此,如果两个出价的价格一致,则较早创建的出价应该更高。
在用户界面中,这本书看起来像是由一组价格水平组成的表格,其中列出了购买和出售的限价单。
为了增加视觉上的区别,买卖应用程序具有不同的颜色。
等级汇总
书本深度-价格水平的数量。 对于具有大量待定订单且相隔最小距离的活跃市场,在交易者终端中显示的深度可能非常大。 要评估整本书,您需要一个级别分组工具。
通过截取一个小数位并将其分组,我们可以在每一步中减少它们的数量。
执行和取消申请
在执行或取消书中的订单后,控制器必须通过删除此订单并通知对书中的更改感兴趣的所有人来更新书。
处理程序体系结构和扩展
给定所需的性能和可靠性,有必要确定扩展应用程序和存储系统的方法。
通常,交换使用垂直缩放。 用于处理应用程序和用户帐户的代码在单个整体中的一台计算机上执行。 这种方法显示出良好的性能,但有很大的局限性-无论如何,垂直扩展在处理器能力和存储容量方面都有局限性。
作为实验的一部分,我决定将市场处理水平扩展。 每个单独的工具都由其自己的过程处理。 进程自动在群集节点之间分配。 如果发生故障,市场将转移到另一个节点,而不会失去状态。
系统的公式非常简单:M个处理程序分布在集群的K个节点上,并使用L个数据存储。
类似的方案使您可以将系统扩展到大约150个节点。 每个市场控制器可以处理约30k RPS。
由于所有市场中的应用程序流都是不同的,并且取决于用户活动,因此市场可以分为几类:小型,中型和大型。 每个节点都有设置,可让您指定对其可以处理的市场数量的限制。 该向导自动在群集节点之间平均分配相同类型的市场。 如果集群组成发生变化,则会重新分配市场。 因此,实现了系统上的负载的大致均匀的分布。
交换管理界面中的节点示例视图:
资料储存
订单不断变化,必须保存在内存中。 对于MVP,我选择了带有WAL的Tarantool作为内存存储。 所有历史数据将记录在PostgreSQL中。
用于存储当前和历史数据的方案应对应于用于缩放处理程序代码的所选方案。 每个市场都可以使用自己的postgres和tarantool。 为此,请将PostgreSQL和tarantool对组合为一个实体-市场数据仓库。
设置市场时,管理员可以管理存储库。 为了保持灵活性,我们将指定一个唯一的连接池标识符,而不是访问特定的postgresql和tarantool实例。 平台支持这些池的接口。 因此,管理界面中的存储库如下所示:
设置市场时,管理员必须为每个市场指定至少一个商店。 如果指定少数几个,您将获得逻辑数据复制的市场。 此功能使您可以配置存储方案的可靠性和性能。
订单簿数据
Tarantool使用空间来组织存储的数据。 订单簿所需空间的声明如下:
book = { state = { name = 'book_state', id = 1, }, orders = { limit = { buy_orders = { name = 'limit_buy_orders', id = 10, }, sell_orders = { name = 'limit_sell_orders', id = 20, }, }, market = { buy_orders = { name = 'market_buy_orders', id = 30, }, sell_orders = { name = 'market_sell_orders', id = 40, }, }, ... }, orders_mapping = { name = 'orders_mapping', id = 50, }, }
由于多个市场可以将其数据存储在一个tarantool实例上,因此我们将向所有实体添加市场标识符。 该书的当前实现基于一次计数,多次给予的原理。 在书籍更新操作期间,分组会自动重新计数。 例如,我们向市场添加订单,价格的准确性为6,有6个可能的价格组+一片带有需要更新的原始订单数据的切片。
有很多orders_mapping订单用于发布活动客户订单列表。
借助tarantool数据模型,结合使用索引和各种采样迭代器,实现订单簿存储的lua代码仅需600行(以及初始化操作)。
历史数据
市场数据存储在每个市场的单独表中。 考虑一组基本表。
完成申请的历史
要保存处理应用程序的结果,请使用历史记录表。 它包括完全完成的申请,以及已取消但部分完成的申请。
CREATE TABLE public.history ( id uuid NOT NULL, ts timestamp without time zone NOT NULL DEFAULT now(), owner character varying(75) COLLATE pg_catalog."default" NOT NULL, order_type integer NOT NULL, order_side integer NOT NULL, price numeric(64,32) NOT NULL, qty numeric(64,32) NOT NULL, commission numeric(64,32) NOT NULL, opts jsonb NOT NULL, CONSTRAINT history_pkey PRIMARY KEY (id, ts) )
在此基础上,最终用户的发行是基于其投标的历史。
历史数据馈送
为了进行分析以及形成历史数据供稿,每次交易后,市场控制者必须保存有关此事件的信息。 要修复市场变化事件,请使用报价表:
CREATE TABLE public.ticks ( ts timestamp without time zone NOT NULL, bid numeric(64,32) NOT NULL, ask numeric(64,32) NOT NULL, last numeric(64,32) NOT NULL, bid_vol numeric(64,32), ask_vol numeric(64,32), last_vol numeric(64,32), opts jsonb DEFAULT '{}'::jsonb, CONSTRAINT ticks_pk PRIMARY KEY (ts) )
它存储交易后的价格和市场量,并且opts字段包含服务信息,例如交易中涉及的订单的描述。
图表数据提要
要建立交易图表,报价表就足够了。 它包含所谓的原始流,但是postgresql具有强大的分析功能,并允许您按需聚合数据。
当数据过多且电源不足时,问题就开始了。 要解决此问题,请创建一个包含预先计算的数据的表:
CREATE TABLE public.df ( t timestamp without time zone NOT NULL, r df_resolution NOT NULL DEFAULT '1m'::df_resolution, o numeric(64,32), h numeric(64,32), l numeric(64,32), c numeric(64,32), v numeric(64,32), CONSTRAINT df_pk PRIMARY KEY (t, r) )
在下一篇文章中,我们将讨论如何在Postgresql中使用时间序列,为df表准备数据以及如何构建图。
总结
我们弄清了组织订单簿和订单处理机制的要点,并且还使自己沉迷于使用市场数据的实践中。
选定的存储方案使您可以从所有市场的一家商店开始,并且随着项目的发展,将市场分配到不同的商店,使它们尽可能靠近市场处理器。