前言
無論是股票交易系統,還是數字貨币交易系統,都離不開撮合交易引擎,這是交易平台的心髒。同時,一個優秀的架構設計也會讓交易平台的運維和持續開發更加容易。本文基于對開源項目的深入研究,總結了數字貨币交易系統的架構設計。
本文參考了開源項目:https://gitee.com/cexchange/CoinExchange
關于撮合交易系統
撮合技術主要是從資料庫撮合技術向記憶體撮合技術發展,這是因為資料庫撮合技術越來越無法滿足金融交易對于高可靠性、高性能、強安全性、可擴充性以及易維護性的需求。金融(币币)交易撮合系統中包括以下幾個核心子產品:
- 使用者:終端使用者委托報價與數量,生成訂單發送至交易平台。
- 網關:負責收集使用者訂單,并将其派發給撮合引擎。
- 撮合引擎:交易系統中的核心部分,用于接收訂單并根據業務邏輯實作訂單 撮合同時生成交易記錄,随後給予使用者交易結果回報。
- 資料庫:用來存放交易過程中的訂單和交易記錄,實作資料持久化。
- 消息隊列:一般用于訂單消息的傳輸
關于技術選型
一個交易所平台的技術架構主要考慮安全性、分布式、易擴充、容錯性、低延時、高并發等特性,以及熔斷機制、服務注冊和發現、消息服務、服務網關、安全認證、記憶體資料庫、關系型資料庫等各種選項,最終形成了如下技術選型:
- 分布式基礎進行架構SpringCloud與Dubbo之間二選一,由于SpringCloud更加知名,SpringCloud的程式員更好招聘,有利于系統的長期運維更新,而且SpringCloud是基于SpringBoot開發,比較有親切感,是以選擇了SpringCloud, 其實由于阿裡系的強大影響,國内Dubbo使用更加廣泛,不同的團隊可以根據自己的情況選擇。
- 引入Hystrix斷路器作為容錯保護子產品,防止單個服務的故障,耗盡整個撮合系統容器的線程資源,避免分布式環境裡大量級聯失敗。對通過第三方用戶端通路依賴服務出現失敗、拒絕、逾時或短路時執行回退邏輯。
- 采用Eureka作為服務注冊與發現中心,實作中間層服務,以達到負載均衡和中間層服務故障轉移的目的。
- 服務網關Spring Cloud Gateway 與 Zuul 的選型,選擇了Zuul,因為名字短一些。
- 引入SpringCloud Security安全認證子產品用于建構安全的應用程式和服務,SpringCloud Security在Spring Boot和Spring Security OAuth2的基礎上,可以快速建立和實作常見的安全認證方式,如單點登入,令牌中繼和令牌交換等。
- 引入Redis作為記憶體資料庫,兼做系統資料緩存和記憶體計算。
- 使用MySQL作為關系資料庫,性能測試非常過關,而且對熟悉MYSQL的程式員非常友好。
- 消息隊列中間件MQ采用了Kafka, 具有超高性能展現。
關于交易所架構設計
基于SpringCloud開發基于微服務架構的交易平台,首先需要對SpringCloud的基礎架構有所了解,我們熟知的SpringCloud微服務架構如下圖所示:

由于篇幅關系,本文就不對SpringCloud的技術架構進行詳細解讀了。
在SpringCloud這個優秀的微服務架構基礎之上,如何建構一個交易系統呢?開源項目CoinExchange對交易所的架構做了如下架構設計:
将撮合交易引擎、API等拆分作為單獨的服務,基于SpringCloud建構了一個精簡的交易所架構。
部署圖如下:
關于撮合交易引擎
采用記憶體撮合的方式進行,以Kafka做撮合訂單資訊傳輸,MongoDB持久化訂單成交明細,MySQL記錄訂單總體成交。其中行情子產品主要負責訂單成交持久化、行情生成、行情推送等服務,包括:
- K線資料,間隔分别為:1分鐘、5分鐘、15分鐘、30分鐘、1小時、1天、1周、1月
- 所有交易對的市場深度(market depth)資料
- 所有交易對的最新價格
- 最近成交的交易對
記憶體撮合交易支援的模式
- 限價訂單與限價訂單撮合
- 市價訂單與限價訂單撮合
- 限價訂單與市價訂單撮合
- 市價訂單與市價訂單撮合
撮合邏輯過程如下圖所示:
示例代碼如下:
1 /**
2 * 限價委托單與限價隊列比對
3 * @param lpList 限價對手單隊列
4 * @param focusedOrder 交易訂單
5 */
6 public void matchLimitPriceWithLPList(TreeMap<BigDecimal,MergeOrder> lpList, ExchangeOrder focusedOrder,boolean canEnterList){
7 List<ExchangeTrade> exchangeTrades = new ArrayList<>();
8 List<ExchangeOrder> completedOrders = new ArrayList<>();
9 synchronized (lpList) {
10 Iterator<Map.Entry<BigDecimal,MergeOrder>> mergeOrderIterator = lpList.entrySet().iterator();
11 boolean exitLoop = false;
12 while (!exitLoop && mergeOrderIterator.hasNext()) {
13 Map.Entry<BigDecimal,MergeOrder> entry = mergeOrderIterator.next();
14 MergeOrder mergeOrder = entry.getValue();
15 Iterator<ExchangeOrder> orderIterator = mergeOrder.iterator();
16 //買入單需要比對的價格不大于委托價,否則退出
17 if (focusedOrder.getDirection() == ExchangeOrderDirection.BUY && mergeOrder.getPrice().compareTo(focusedOrder.getPrice()) > 0) {
18 break;
19 }
20 //賣出單需要比對的價格不小于委托價,否則退出
21 if (focusedOrder.getDirection() == ExchangeOrderDirection.SELL && mergeOrder.getPrice().compareTo(focusedOrder.getPrice()) < 0) {
22 break;
23 }
24 while (orderIterator.hasNext()) {
25 ExchangeOrder matchOrder = orderIterator.next();
26 //處理比對
27 ExchangeTrade trade = processMatch(focusedOrder, matchOrder);
28 exchangeTrades.add(trade);
29 //判斷比對單是否完成
30 if (matchOrder.isCompleted()) {
31 //目前比對的訂單完成交易,删除該訂單
32 orderIterator.remove();
33 completedOrders.add(matchOrder);
34 }
35 //判斷交易單是否完成
36 if (focusedOrder.isCompleted()) {
37 //交易完成
38 completedOrders.add(focusedOrder);
39 //退出循環
40 exitLoop = true;
41 break;
42 }
43 }
44 if(mergeOrder.size() == 0){
45 mergeOrderIterator.remove();
46 }
47 }
48 }
49 //如果還沒有交易完,訂單壓入清單中
50 if (focusedOrder.getTradedAmount().compareTo(focusedOrder.getAmount()) < 0 && canEnterList) {
51 addLimitPriceOrder(focusedOrder);
52 }
53 //每個訂單的比對批量推送
54 handleExchangeTrade(exchangeTrades);
55 if(completedOrders.size() > 0){
56 orderCompleted(completedOrders);
57 TradePlate plate = focusedOrder.getDirection() == ExchangeOrderDirection.BUY ? sellTradePlate : buyTradePlate;
58 sendTradePlateMessage(plate);
59 }
60 }
61
62 /**
63 * 限價委托單與市價隊列比對
64 * @param mpList 市價對手單隊列
65 * @param focusedOrder 交易訂單
66 */
67 public void matchLimitPriceWithMPList(LinkedList<ExchangeOrder> mpList,ExchangeOrder focusedOrder){
68 List<ExchangeTrade> exchangeTrades = new ArrayList<>();
69 List<ExchangeOrder> completedOrders = new ArrayList<>();
70 synchronized (mpList) {
71 Iterator<ExchangeOrder> iterator = mpList.iterator();
72 while (iterator.hasNext()) {
73 ExchangeOrder matchOrder = iterator.next();
74 ExchangeTrade trade = processMatch(focusedOrder, matchOrder);
75 logger.info(">>>>>"+trade);
76 if(trade != null){
77 exchangeTrades.add(trade);
78 }
79 //判斷比對單是否完成,市價單amount為成交量
80 if(matchOrder.isCompleted()){
81 iterator.remove();
82 completedOrders.add(matchOrder);
83 }
84 //判斷吃單是否完成,判斷成交量是否完成
85 if (focusedOrder.isCompleted()) {
86 //交易完成
87 completedOrders.add(focusedOrder);
88 //退出循環
89 break;
90 }
91 }
92 }
93 //如果還沒有交易完,訂單壓入清單中
94 if (focusedOrder.getTradedAmount().compareTo(focusedOrder.getAmount()) < 0) {
95 addLimitPriceOrder(focusedOrder);
96 }
97 //每個訂單的比對批量推送
98 handleExchangeTrade(exchangeTrades);
99 orderCompleted(completedOrders);
100 }
101
102
103 /**
104 * 市價委托單與限價對手單清單交易
105 * @param lpList 限價對手單清單
106 * @param focusedOrder 待交易訂單
107 */
108 public void matchMarketPriceWithLPList(TreeMap<BigDecimal,MergeOrder> lpList, ExchangeOrder focusedOrder){
109 List<ExchangeTrade> exchangeTrades = new ArrayList<>();
110 List<ExchangeOrder> completedOrders = new ArrayList<>();
111 synchronized (lpList) {
112 Iterator<Map.Entry<BigDecimal,MergeOrder>> mergeOrderIterator = lpList.entrySet().iterator();
113 boolean exitLoop = false;
114 while (!exitLoop && mergeOrderIterator.hasNext()) {
115 Map.Entry<BigDecimal,MergeOrder> entry = mergeOrderIterator.next();
116 MergeOrder mergeOrder = entry.getValue();
117 Iterator<ExchangeOrder> orderIterator = mergeOrder.iterator();
118 while (orderIterator.hasNext()) {
119 ExchangeOrder matchOrder = orderIterator.next();
120 //處理比對
121 ExchangeTrade trade = processMatch(focusedOrder, matchOrder);
122 if (trade != null) {
123 exchangeTrades.add(trade);
124 }
125 //判斷比對單是否完成
126 if (matchOrder.isCompleted()) {
127 //目前比對的訂單完成交易,删除該訂單
128 orderIterator.remove();
129 completedOrders.add(matchOrder);
130 }
131 //判斷焦點訂單是否完成
132 if (focusedOrder.isCompleted()) {
133 completedOrders.add(focusedOrder);
134 //退出循環
135 exitLoop = true;
136 break;
137 }
138 }
139 if(mergeOrder.size() == 0){
140 mergeOrderIterator.remove();
141 }
142 }
143 }
144 //如果還沒有交易完,訂單壓入清單中,市價買單按成交量算
145 if (focusedOrder.getDirection() == ExchangeOrderDirection.SELL&&focusedOrder.getTradedAmount().compareTo(focusedOrder.getAmount()) < 0
146 || focusedOrder.getDirection() == ExchangeOrderDirection.BUY&& focusedOrder.getTurnover().compareTo(focusedOrder.getAmount()) < 0) {
147 addMarketPriceOrder(focusedOrder);
148 }
149 //每個訂單的比對批量推送
150 handleExchangeTrade(exchangeTrades);
151 if(completedOrders.size() > 0){
152 orderCompleted(completedOrders);
153 TradePlate plate = focusedOrder.getDirection() == ExchangeOrderDirection.BUY ? sellTradePlate : buyTradePlate;
154 sendTradePlateMessage(plate);
155 }
156 }
關于區塊鍊錢包對接
每個币種對應不同的資料通路方式,大部分區塊鍊項目的錢包操作方式是相同的或十分相似的,比如BTC、LTC、BCH、BSV、BCD等比特币衍生币,其API操作方式幾乎一樣;再比如ETH,當你掌握一個合約币種的操作,其他基于ETH發行的數字貨币的操作方式幾乎一樣。是以,基本上當你花時間弄懂了一個,就懂了一堆币種。
本項目使用的錢包操作方案也是不同的,也盡可能的為大家展示了不同用法:
- 如BTC、USDT,使用的自建全節點,現在差不多需要300G硬碟空間;
- 如ETH,使用的是自建輕節點(參考文章),因為全節點需要硬碟空間太大;
- 如BCH、BSV等,使用的是第三方區塊鍊浏覽器擷取資料;
- 如XRP,官方就已經提供了通路區塊資料的接口(Ripple API GitHub位址)
一般而言,當交易所來往資金量不大的時候,你可以自己摸索,但是當交易所資金量大了以後,如果你對自己操作錢包不太放心,你也可以使用第三方的錢包服務,當然,這需要你與錢包服務商進行談判,付個年費什麼的。
下圖是關于交易平台充值邏輯的一個簡單時序圖:
總結
通過以上的說明及圖示,我們基本上對交易所的整體架構有了一定的認知。
感謝
最後感謝開源交易所項目給與我學習的機會!
Java開源交易平台項目:https://gitee.com/cexchange/CoinExchange