我曾經花了一週時間開發了一個股票模擬交易後臺程序,使用Node.js。代碼量很少,能完成基本功能。下面給大家介紹一下其實現步驟。
基本功能
- 開戶
- 搜索股票
- 掛單(多單、空單)
- 撤單(主動、被動)
- 成交(非撮合)
- 除權、除息
- 查詢
- 訂單狀態
- 持倉
- 今日委託
- 今日成交
- 歷史委託
- 歷史成交
- 掛單列表
- 賬戶詳情(總收益,收益率,總資產)
其中模擬交易和真實交易最大的不同是,真實交易採用撮合制,邏輯較爲複雜。模擬交易採用更簡單的即時成交機制,只要符合條件,訂單立即成交。
這個後臺程序一共就兩個js文件,一個用於處理成交,即判斷成交條件,寫數據庫。另一個處理其他邏輯。當然這裏面沒有提到獲取股票實時價格的問題,這是另一個系統完成,我們通過消息隊列實時獲取我們所關心的股票的價格,這是另一個話題了。
這個後臺程序以一個node.js進程的方式運行,一個10秒一次的定時器執行成交判斷。(真實交易所的撮合器也是10秒鐘一次)
此外有一個WebAPI Server接受來自客戶端的請求。所以總體架構,可以看成是一個微服務組成的系統。
數據庫設計
賬戶表
`Id` int(11) NOT NULL AUTO_INCREMENT COMMENT '模擬賬戶',
`MemberCode` varchar(20) DEFAULT '' COMMENT '用戶編號',
`AccountNo` varchar(255) DEFAULT NULL COMMENT '賬號',
`TranAmount` int(11) DEFAULT NULL COMMENT '模擬賬戶入資金額',
`CommissionLimit` decimal(20,4) DEFAULT '2.9900' COMMENT '最低佣金',
`CommissionRate` decimal(20,4) DEFAULT '0.0125' COMMENT '佣金比例',
`Cash` decimal(20,4) DEFAULT '0.0000' COMMENT '現金',
`UsableCash` decimal(20,4) DEFAULT '0.0000' COMMENT '可用資金',
`Status` tinyint(4) DEFAULT '1' COMMENT '賬號狀態:1正常',
`AccountType` tinyint(4) DEFAULT '1' COMMENT '賬號類型:1現金賬號,2保證金賬號',
`CreateTime` datetime DEFAULT NULL COMMENT '創建時間',
PRIMARY KEY (`Id`)
其中一個用戶可以對應多個賬戶,所以有一個AccountNo作爲區分。 TranAmount爲初始資金,用於重置賬戶。佣金字段用於模擬交易的手續費和稅費。可用資金字段是,當用戶掛單的時候有一部分資金處於凍結狀態,可用資金就是去除凍結資金的金額。
訂單表
`Id` int(11) NOT NULL AUTO_INCREMENT COMMENT '模擬交易訂單表',
`MemberCode` varchar(20) DEFAULT '' COMMENT '用戶編號',
`AccountNo` varchar(20) DEFAULT '' COMMENT '模擬賬號',
`SecuritiesType` varchar(10) DEFAULT '' COMMENT '股票類型:us,hk,sh,sz',
`SecuritiesNo` varchar(20) DEFAULT '' COMMENT '股票編號',
`CPrice` decimal(20,4) DEFAULT '0.0000' COMMENT '委託價',
`Price` decimal(20,4) DEFAULT '0.0000' COMMENT '價格',
`OrderQty` decimal(20,4) DEFAULT '0.0000' COMMENT '股票數據量',
`Side` char(1) DEFAULT '' COMMENT '交易類型:B買、S賣',
`OrdType` tinyint(4) DEFAULT '1' COMMENT '訂單類型:1市場訂單、2限價訂單、3止損訂單、4做空市場訂單、5做空限價訂單、6做空止損訂單',
`execType` tinyint(4) DEFAULT '1' COMMENT '執行類型:0新的,1成交、2取消、3拒絕',
`Commission` decimal(20,4) DEFAULT '2.9900' COMMENT '佣金',
`Reason` tinyint(4) DEFAULT '0' COMMENT '訂單拒絕理由:0正常、1資金不足、2倉位不足、3超時失效',
`Amount` decimal(20,4) DEFAULT '0.0000' COMMENT '金額',
`EndTime` datetime DEFAULT NULL COMMENT '訂單截止時間',
`CreateTime` datetime DEFAULT NULL COMMENT '訂單時間',
`TurnoverTime` datetime DEFAULT NULL COMMENT '成交時間',
PRIMARY KEY (`Id`)
這是最重要的兩張表,其他幾張表就不羅列詳細的內容,只做簡單說明
- 資產表(記錄浮動盈虧,持倉金額,各種時間範圍的收益率)
- 額外津貼記錄表(記錄除權,除息)
- 資金記錄表(記錄特殊資金變動)
- 倉位表
- 倉位記錄表(記錄倉位變化)
- 做空倉位記錄表
- 排行榜
掛單
掛單的核心就是向數據庫插入一條記錄,不過即便是簡潔的js代碼,也差不多寫了80行代碼。 首先就是一系列的判斷,是否可以創建訂單。
- 參數是否在取值範圍內。
- 市價單類型,判斷是否開市,未開盤時間段不能創建訂單。
- 賬戶異常狀態不能創建訂單。
- 如果是賣多單,或者買空單,則要把倉位數據取出來判斷,是否倉位夠扣。
- 如果是買多單,或者賣空單,則要計算扣除佣金(手續費)後可用資金夠不夠。
- 如果是限價單或者是止損單,則判斷價格設置是否在有效範圍內。 然後執行一個數據庫事務,插入一條訂單記錄,同時修改可交易倉位或者可用資金。
撤單
撤單比掛單簡單許多。主要步驟就是先判斷訂單是否存在,然後修改訂單狀態,同時修改可交易倉位或者可用資金。
模擬交易主進程
系統每隔10秒執行一次邏輯。
所有訂單緩存策略
如果每隔10秒鐘從數據庫讀取所有訂單的話,效率會很低,而且過多佔用數據庫IO資源。所以訂單數據都緩存在成交判斷的進程內存中。將來也可以升級爲使用redis等內存數據庫來存儲。 當有訂單創建的時候,通過消息隊列通知進程。當進程重啓的時候,從數據庫讀取數據進行初始化。
超時訂單處理
有些訂單一直沒有滿足成交條件,但已經超過交易時間,所以要進行處理。(訂單狀態設置爲拒絕)
成交判斷
未開盤則跳過。 根據訂單類型判斷是否達到成交條件
'訂單類型:1市場訂單、2限價訂單、3止損訂單、4做空市場訂單、5做空限價訂單、6做空止損訂單' Price:訂單設置的價格 price:當前股價 B:買入 S:賣出
let trigge = false
switch (OrdType) {
case 1:
trigge = true;
break;
case 2:
case 3:
trigge = Side == "BS" [OrdType - 2] ? (Price >= price) : (Price <= price)
break;
case 4:
trigge = true;
break;
case 5:
case 6:
trigge = Side == "BS" [6 - OrdType] ? (Price >= price) : (Price <= price)
break;
}
執行成交
最初是用程序執行的,後來爲了執行效率和數據一致性,採用存儲過程。 首先,我們需要查詢出賬戶的現金和可用資金,以及倉位信息。 如果是賣多或者買空(減少持倉,增加現金),我們計算出此時需要增加的金額,當然這個時候可能出現倉位不夠的情況,就拒絕訂單。 如果是買多或者賣空(增加持倉,減少現金),我們就需要計算此時需要扣除的金額,如果出現可用金額不足,就拒絕訂單。 最後,我們修改賬戶的實際金額和可用金額,寫入持倉記錄和現金變化記錄,修改訂單狀態爲已成交狀態。
信息查詢
普通數據庫查詢,這裏不多贅述了。
除權、除息
由於模擬交易系統無法第一時間自動得到除權和除息的消息,所以當需要進行除權和除息的操作的時候,可能用戶已經發生成交的訂單。這時候需要根據持倉記錄變更表進行一些計算,恢復正確的持倉,如果是除息就是根據現金記錄變更表,進行資金重新計算。最後我們把這次操作的日誌記錄下來。