剖析一個用C++寫的行情交易系統

最近hen ci hen ci用C++寫完了一整套證券行情繫統,可是不是服務滬深交易所的,是給文交所用的。整個系統涵蓋了從DBF文件解析開始到客戶端展示這一整條邏輯。想來一年多沒有更新博客了,因此趁這個機會,把整個系統的架構和開發中遇到的問題寫下來,權當總結和分享。前端

首先要說明的是,整個系統的架構都是以當前業務爲出發點的,因此和目前網上看到的,比方說廣發自研的系統是確定有差異的,咱們就沒有合規一說。另外,從用戶規模和市場活躍程度來看,咱們也沒法和國內證券市場比較,因此和目前公開出來的系統結構相比也仍是有差別的。咱們根據自身人力資源限制和當前業務角度考慮,首要目標是但願總體架構要簡單,易於橫向擴展。由於一,人太少,平均下來,我就倆人;二,不懂業務,其中接手我服務端開發的仍是應屆畢業生。算法

這裏先把系統結構圖羅列一下:數據庫

你會看到裏面有不少讓人意想不到的東西,比方說SQLite!緩存

容我後續慢慢來講!安全

DBF文件解析

目前滬深L1數據更新的頻率是3秒。文交所這邊是1秒。總得來講解析DBF文件沒有太大的難度,就是要理解文件結構。DBF文件結構其實也是開放的,隨便查。這個程序沒有任何難度,不須要多線程,只有一個要求,就是解析文件越快越好。網絡

關於DBF麼,我就畫一個結構圖在這裏好了。方便你們查閱。session

行情數據庫程序

首先來張圖展現下行情數據庫程序的結構。多線程

從圖上看,咱們的行情程序分三大模塊:架構

  1. 業務驅動對象
  2. Logger
  3. Config Agent

Logger

也就是日誌。咱們這裏是直接用了Linux自帶的Syslog。固然了迴歸到代碼的話,是一個logger接口,而後在Linux上基於Syslog實現了這個logger接口。app

爲何選用Syslog?一是咱們沒有那麼多資源搞一個異步logger;二是以咱們目前的壓力來看,Syslog從各方面都知足咱們的要求。並且是獨立的進程,萬一發生不測,不影響咱們行情正常運行,無非就是沒有日誌了。

Config Agent

配置文件讀取對象。好像沒有神馬好說的。

業務驅動對象

咱們重點來講下這個業務驅動對象。

首先來講一說這個業務驅動對象究竟是用來幹啥的。

服務端程序在設計的時候,你確定會將業務進行層次劃分,這樣作的好處就是結構清晰,易於維護。每一層就是一個模塊,模塊之間規定好訪問接口,形象地說就是高內聚低耦合。

在咱們這裏,模塊之間的接口都是以boost:: signals2::signal來作的。

網絡層

網絡層只開放了5個接口:

其中shield是指DBF文件解析器。當連上了DBF解析器的時候,網絡層會向上層發送一個shield_connected_signal_t類型的信號,通常這個信號上一層都沒有訂閱。當DBF解析器推送數據過來的時候,網絡層解析成功後,就會發送一個shield_data_ready_signal_t信號。這個信號上次確定會訂閱的,否則程序就不用跑了。

另外,cache打頭的信號指的是從緩存程序發送過來的請求。和DBF的shield相似,基本上一目瞭然,顧名思義。

數據轉換層

這層其實無關緊要。這層的做用就是將網絡過來的二進制數據解碼成protobuf message對象,而後將protobuf message對象解碼成無第三方庫依賴的本地數據包,並根據數據包的類型,發送相應的信號給數據層。之因此有一個protobuf到本地包的轉換,主要是感謝Google,畢竟Google是出了名的喜歡棄坑。或者說,等哪天有了更好的數據包二進制序列號反序列化庫,只須要考慮將這一場替換掉,就萬事大吉了。固然,目前來看,這一層確定是我想多了!

數據層

數據層也就是咱們真正處理業務的模塊。這個模塊的特色是,宏觀上看簡單,微觀上看複雜。

宏觀上看無非就三件事情:

  1. 從DBF數據計算出各類週期行情數據
  2. 將最新的行情數據存盤
  3. 將最新的行情數據推送到前端緩存

微觀上覆雜怎麼說呢?複雜就複雜在計算週期行情數據。

行情數據計算

咱們給每個行情數據都指定了一個數據項ID。比方說開盤價咱們能夠用0xFFFFFFFF這個ID來表示,收盤價能夠用0xFFFFFFFE來表示。

除此之外,咱們還具體定義了週期ID。實時週期,一分鐘週期,五分鐘週期,十五分鐘週期,三十分鐘週期,六十分鐘週期,日週期,周月季年週期等。

DBF裏的數據就至關與實時週期數據。

因此咱們有多少數據要計算?顯而易見,數據項ID個數x週期個數!

數據項ID說實話,並很多!因此計算週期數據真的是至關的重體力!

 那麼計算行情數據到底應該是:

  1. 遍歷每個數據項,計算出這個數據項全部週期的數據
  2. 仍是先根據週期來,計算出每個週期下全部數據項的值

這個話題說到這裏,感受說不下去了。由於我最後付諸的行動不是這麼搞的。我不肯定個人算法是否是最優算法,可是我想應該八九不離十?

要把這個想法說清楚,估計仍是要真正寫一把,你才知道到前面提到的說法到底對不對。

我前先後後寫了大概至少兩遍。我最終的想法容我下來嗎慢慢說來。

先說一說咱們數據的特色。行情數據其實都是以時間爲順序的離散數據點!這個能理解吧?因此,咱們的行情數據在根據DBF文件計算的時候對於任意一個週期來講,無非遇到兩種狀況:

  1. 更新最新的這個時間點數據
  2. 生成一個最新時間點的數據

另外,在這兩個大類的情下還要考慮一個因素:

  • 更新當前最新數據點的時候是否和該週期前一個數據點有依賴關係?
  • 生成一個最新數據點的時候是否和該週期前一個數據點有依賴關係?

因此我在計算行情數據時,先區分是否生成一個新的數據點。而後根據週期來計算的。

這個代碼註釋可能不對,心照不宣就好。

關於行情計算這裏再說最後一點,前段時間發現一個八哥。若是DBF文件裏的某隻代碼/某個證券的昨收爲0時,這個時候,這隻代碼當天的昨收是0仍是其餘值?正確答案是,不是零,是上一個交易日的昨收。

數據存盤

終於說到數據存盤,說到SQLite了。

先不從業務角度考慮這個問題。當有大量數據要存儲的時候,通常的解決方案是什麼?沒有特殊要求的狀況下,多數會使用數據庫來解決這個問題。畢竟自研一套數據存儲工具,並且要作得好,並非一件簡單的事情。接着我說一說同花順和恆生,據我瞭解同花順和恆生都是本身設計的二進制文件格式來保存行情數據的(不要問我怎麼知道的,畢竟當初我是同花順的)。恆生的不知道,同花順我是有切身體會的。這個二進制文件常常會莫名其妙寫掛了。因此如今經典的統一版客戶端出現數據錯誤的時候,多半是文件已經寫壞了。你須要「從新初始化」!

基於上面的考慮,我果斷選擇了SQLite。好處就是事務性,不容易寫壞,而且易於經過第三方工具快速查閱。固然相比較自定義的二進制結構文件,劣勢是查詢數據的方式會慢一點,你得構造出一個SQL語句,而後解析返回的數據才能夠。不過這其實並非問題,大量頻繁的查詢操做咱們都定義在緩存裏,所有交由Redis來解決了。除了查詢歷史數據,咱們通常都不須要訪問數據庫。

數據庫另一個優點就是歷史庫。咱們能夠把好久遠的數據導到歷史庫裏,這比本身搞一個二進制文件,而後要支持分割歷史到歷史庫裏要方便,這種複雜的操做,我不是不相信本身寫代碼的能力,而是我以爲何須呢?

既然經驗證實存文件都沒有問題,那麼數據庫我選用SQLite應該不會出大問題,若是出問題了,我改用非嵌入式數據庫就好了。等性能問題即將出現的時候,profile一下,問題若出在SQLite,再改不遲。

線程分配/管理

接下來講一說線程分配管理模塊。這個模塊的設想是怎麼出來的呢?顯然,並非瞎想出來的。

咱們先來回顧一下咱們常常用到的幾個網絡庫,比方說Libevent,ASIO。這些類庫沒有在業務邏輯裏面本身搞線程池,說神馬本身建立一個線程而後跑。最多就是支持一下線程安全。該有鎖的地方配好一把鎖。因此,這個基本特性也告訴我,個人網絡層個人數據層這些,他們都不該該本身去分配線程。這些層級要作的就是要確保線程安全,這樣才能方便作橫向拓展。

要實現這個目的,就須要網絡層對象、數據層對象有一個dependence injection的構造函數,傳入一個libevent event base wrapper對象。一個對象只能跑在一個線程上。那麼一個客戶端session到時候就只在指定的一個(我但願是這樣,避免沒必要要的線程切換)或兩個(看網絡層和數據層這些是在同一條線程上跑仍是分開跑)線程上跑。在這樣的設計下,加之不一樣客戶端之間其實業務行爲都是獨立的,包括數據均可以是獨立的。那麼不一樣線程間的客戶session都是相互不影響的。有了這個分析結果,再配合thread_local,你在業務層面就不須要鎖了。因此接下來經過增長線程的方式能夠很輕鬆作到橫向的多線程擴展。是否是?

未完,待續,to be continued。。。

相關文章
相關標籤/搜索