介紹
match-trade超高效的交易所撮合引擎,採用倫敦外匯交易所LMAX開源的Disruptor框架,用Hazelcast進行分佈式內存存取,以及原子性操作。使用數據流的方式進行計算撮合序列,才用價格水平獨立撮合邏輯,實現高效大數據撮合。
優勢
- match-engine水平價格為獨立撮合邏輯,相比於訂單隊列為撮合隊列的交易引擎來說,價格區間越小時,性能越優越。
- match-engine不再對撮合薄進行排序,而是用並行流計算出最優撮合價格,進行撮合。
- match-engine每個價格下的訂單都是異步完成被撮合。獨立價格下訂單不影響下一個新發生的撮合。
- match-engine每個價格撮合都是獨立的,與下一個價格沒的關係,實現快速吃單。
- match-engine每個新的訂單經歷撮合處理器後,後續邏輯採用並行計算,能更快速反饋數據撮合結果。
- match-engine使用數據流反應式MQ消費,降低由MQ帶來的數據延遲。
- match-engine撤單走獨立的邏輯,不用和下單在一個處理序列。
技術選擇
- Disruptor: 號稱每秒鐘承載600萬訂單級別的無鎖並行計算框架,主要選擇原因還是並行計算。
- Hazelcast: 很好進行內存處理,很強原子性保障的操作能力。同時分佈式內存實現很簡單,能自動內存集群。據說火幣也在用。
- rocketmq: 消息可以做到0丟失,支持10億級別的消息堆積,不會因堆積導致性能下降,主要是經過雙11檢驗
- WebFlux: 它能夠充分利用多核 CPU 的硬件資源去處理大量的併發請求。
描述
用戶輸入包括:
- 創建新的委託單(NewOrder):一個新的委託單可以作為交易撮合引擎的輸入,引擎會嘗試將其與已有的 委託單進行撮合。
- 取消已有的委託單(CancelOrder):用戶也可以取消一個之前輸入的委託單,如果它還沒有執行的話,即開口訂單。
委託單:
- 限價委託單
限價委託單是在當前的加密貨幣交易環境中最常用的委託類型。這種委託單允許用戶指定一個價格,只有當撮合引擎找到同樣價格甚至更好價格的對手單時才執行交易。 - 市價委託單
市價委託單的撮合會完全忽略價格因素,而致力於有限完成指定數量的成交。市價委託單在交易委託賬本中有較高的優先級,在流動性充足的市場中市價單可以保證成交。不充足時,撮合完最後一條撤銷。 - 止損委託單
止損委託單盡在市場價格到達指定價位時才被激活,因此它的執行方式與市價委託單相反。一旦止損委託單激活,它們可以自動轉化為市價委託單或限價委託單。(未實現)
撮合流程
限價撮合:
市價撮合:
目前就實現這兩種訂單撮合
代碼執行流程圖(感謝讀者提供的圖)
訂單簿為撮合簿時代碼解析
這個是一個簡單流盤口計算demo
<code>//獲取匹配的訂單薄數據
IMap<long> outMap = hzInstance.getMap(HzltUtil.getMatchKey(coinTeam, isBuy));
/**
* -★
* -使用Java 8 Stream API中的並行流來計算最優
* -能快速的拿到撮合對象,不用排序取值,降低性能消耗
*/
Order outOrder = outMap.values().parallelStream().min(HzltUtil::compareOrder).get();
//這種方式最難的,就是整理盤口深度數據了
/**
* -★
\t * -獲取行情深度
\t *
\t * @param coinTeam 交易隊
\t * @param isBuy 是否是買
\t * @return List<depth>
\t */
\tpublic List<depth> getMarketDepth(String coinTeam, Boolean isBuy) {
\t\tList<depth> depths = new ArrayList<depth>();
\t\tIMap<long> map = hzInstance.getMap(HzltUtil.getMatchKey(coinTeam, isBuy));
\t\tif (map.size() > 0) {
\t\t\t/**
\t\t\t * -這個流:主要是安價格分組和統計,使用並行流快速歸集。
\t\t\t */
\t\t\tList<depth> list = map.entrySet().parallelStream().map(mo -> mo.getValue())
\t\t\t\t\t.collect(Collectors.groupingBy(Order::getPrice)).entrySet().parallelStream()
\t\t\t\t\t.map(ml -> new Depth(ml.getKey().toString(),
\t\t\t\t\t\t\tml.getValue().stream().map(o -> o.getUnFinishNumber()).reduce(BigDecimal.ZERO, BigDecimal::add)
\t\t\t\t\t\t\t\t\t.toString(),
\t\t\t\t\t\t\t"0", 1, coinTeam, isBuy))
\t\t\t\t\t.sorted((d1, d2) -> HzltUtil.compareTo(d1, d2)).collect(Collectors.toList());
\t\t\t/**
\t\t\t * -這個流:主要是盤口的累計計算,因涉及排序選擇串行流
\t\t\t */
\t\t\tlist.stream().reduce(new Depth("0", "0", "0", 1, coinTeam, isBuy), (one, two) -> {
\t\t\t\tone.setTotal((new BigDecimal(one.getTotal()).add(new BigDecimal(two.getNumber()))).toString());
\t\t\t\tdepths.add(new Depth(two.getPrice(), two.getNumber(), one.getTotal(), two.getPlatform(),
\t\t\t\t\t\ttwo.getCoinTeam(), two.getIsBuy()));
\t\t\t\treturn one;
\t\t\t});
\t\t} else {
\t\t\tDepth depth = new Depth("0", "0", "0", 1, coinTeam, isBuy);
\t\t\tdepths.add(depth);
\t\t}
\t\treturn depths;
\t}/<depth>/<long>/<depth>/<depth>/<depth>/<depth>/<long>/<code>
測試結果
在我8cpu,16G內存的開發win10系統上測試結果:
- Disruptor單生產者初始化10萬不能撮合的訂單耗時:約700毫秒
- Disruptor多生產者初始化10萬不能撮合的訂單耗時:約20秒
- 實際單吃完1-100價格內隨機數量的10萬訂單耗時:約400毫秒
閱讀更多 儒雅程序員 的文章