MATLAB 高級數據結構連載 5:使用containers.Map實時監控股票行情(1)
1. 引言
MATLAB 高級數據結構連載 4 containers.Map - MATLAB - 知乎專欄 一文對 containers.Map做了全面的介紹,這裡不再贅述。本文介紹containers.Map在股票實盤行情監控方面的GUI應用,比如我們期望設計如下界面:
Figure 3支股票、二個行情欄位的實盤行情監控界面上述界面為3支股票(50ETF-510050 、中國平安-601318 、中國人壽-601628)、二個行情欄位(最新價、成交量)的實時監控界面。第一列為股票代碼,第二列為最新價,第三列為成交量。該界面使用 MATLAB 語言編寫,調用了行情供應商 Wind 的行情訂閱介面 WSQ。不考慮供應商行情介面數據延遲的前提下,用MATLAB構造的界面監視行情,可以達到延遲肉眼不可辨(注1-1),再增加幾十支股票亦如此。
之所以介紹這樣一個案例,是因為筆者曾經認為,在金融工程領域,MATLAB 只在歷史數據分析、策略回測等事後分析工作有優勢;而實盤行情監控這種對時效性要求較高的工作,MATLAB 會因速度不夠快和單線程限制而無法勝任(這也是不少數據供應商在解釋他們沒有 MATLAB 介面時給出的理由)。事實上,筆者以前也曾按照常見的 Wind WSQ 介面調用方式,編寫過文章開頭圖片所示界面,果然(除介面本身的行情延遲外)存在嚴重的、由數據阻塞導致的行情延遲、行情丟失問題。但是,在學習面向對象程序設計和 containers.Map 數據結構以後,經 @iamxuxiao 老師指點,筆者發現,大多數情況下,MATLAB的程序速度慢,並非 MATLAB語言本身慢,而是筆者自己的程序設計問題。只要使用一定的編程思想和合理的數據結構,MATLAB 完全可達工業級應用,行情阻塞導致的數據延遲乃至丟失,均可有效避免。
本案例在設計上採用分離用戶界面(view)和模型(model)的模式(MVC 模式,注1-2),分別介紹 containers.Mapn於 model 層面和 view 層面的應用:
1. 使用 containers.Map 存儲實盤行情(model 層面,解決後台數據存儲問題)
2. 使用 containers.Map 存放 ui project(view 層面,解決界面組織和更新問題)
本案例最終的代碼採用了面向對象的代碼風格,但考慮到部分慣用 MATLAB 做金工分析的讀者可能並不熟悉「面向對象」的概念,後文在舉例時將盡量採用常見的、面向過程的代碼風格;也希望讀者不要糾結於語法,而是把關注重點放在 containers.Map 的應用上(更多關於「面向對象」見注1-3)。
另外需要強調的是,本案例所述設計也可應用於其它行情供應商的行情推送介面(如 CTP)。這裡之所以以 Wind 為例,是因為 Wind 有較成熟的 MATLAB 介面、應用更廣、免費試用版也更容易獲取。Wind 下載鏈接見注1-4。
2.Wind 實盤行情介面:WSQ
首先介紹 MATLAB 平台調用 Wind 實盤行情介面(WSQ)的方法。
在MATLAB中,可以使用以下語句啟用 Wind 插件(或可以稱做服務):
>> w = windmatlab;n
啟用該服務後,可用 w.wsq 訂閱多個合約、多個行情欄位的實盤行情:
>> w.wsq(合約1,合約2,..., 行情欄位1,行情欄位2, ..., @回調函數);n
例如,以下語句訂閱了中國人壽(601628)、中國平安(601318)、50ETF(510050)這三支股票的收盤價、成交量這兩個行情欄位:
>> w.wsq(601628.SH,601318.SH,510050.SH,RT_LAST,RT_VOL,@myCallBack);n
其中,".SH "是 (Wind 規定的)合約後綴,表示交易所,例如,".SH" 代表滬市,".CFE" 代表上期所,等。"RT_LAST" 是 (Wind 規定的)「最新價」行情欄位;"RT_VOL" 是「成交量」 .
WSQ 的第三個輸入「@回調函數」,是 Wind 每次行情更新後會自動調用的 MATLAB 函數的函數句柄(handle);在調用該回調函數時,Wind 會把更新了的相關行情數據作為該回調函數的輸入參數傳入。這些輸入參數的順序是固定的,依次為:
reqid(請求ID), isfinished, errorid(錯誤代碼), datas(行情數據), codes(合約代碼), fields(行情欄位), times(時間)n
更多關於回調函數的說明見注2-1,下面舉例說明 w.wsq 的使用。
例2-1. 編寫 ex_2_1.m,使其運行後,在 command window 中實時顯示最新的中國人壽(601628)、中國平安(601318)、50ETF(510050)這三支股票的最新價、成交量這兩個行情欄位。
function ex_2_1n global wn w = windmatlab;n w.wsq(601628.SH,601318.SH,510050.SH,...n RT_LAST,RT_VOL,@myCallBack);nendnnfunction myCallBack ( reqid, isfinished, errorid, datas, codes, fields, times )n disp([時間:, datestr(times,HH:MM:SS) ]);n for iter = 1:length(codes)n disp([合約代碼:,codes{iter}]);n for j = 1:length(fields)n disp([行情欄位:,fields{j}]); n disp([數據:,num2str(datas(j,iter))]) ; n endn endn disp ---------------------- % 相鄰兩次行情分割線nendn
上例中,每次訂閱的行情發生更新後,Wind 會調用一次回調函數 myCallBack,並把最新更新的 fields, codes, datas 等數據傳入 myCallBack,其中,datas 的第 j 行、第 i 列數據 datas(j,i) 是所訂閱的第 i 個合約、第 j 個欄位的最新數據。以下為部分運行結果:
註:
1. 由於 w 是 global 變數,因此可直接在 command window 中輸入以下語句來中止最新行情的更新(ctrl+C 是無法終止訂閱的):
>> global w; w.cancelRequest(0);n
2. 需要在 Wind 中修復過 MATLAB 插件,才可啟用該插件。步驟:打開 Wind -> 菜單欄「我的」 -> 「修復插件」 -> 「修復 Matlab 插件」。
3. 用 containers.Map 存儲實盤行情數據
上文介紹了 WSQ 介面的使用方法,現在考慮這個問題:在回調函數 myCallBack 中,使用什麼樣的數據結構儲存 Wind 推送過來的行情數據(主要為 datas) 。顯然,該數據結構應當滿足以下要求:
(1)可根據合約代碼(codes)和欄位名稱(fields)定位到所需更新的數據。
這樣既便於在 myCallBack 中用推送來的 fields 和 codes 定位數據位置進行變數賦值,又便於後續界面中定位所需更新的 ui project(即某合約某欄位對應的 edit 框)。一個自然的想法是,直接將 myCallback 的 datas 變數設為全局變數:
function myCallBack ( reqid, isfinished, errorid, datas, codes, fields, times )n global datasn ......n
並讓 MATLAB 記住 datas 每一行、每一列所對應的行情欄位名(fields)、合約代碼(codes)。例如,若例2-1三支股票兩個行情欄位與 datas 的位置對應如下表所示:
則 datas(2,3) 就是 50ETF 的成交量。這種方式對於那種每次推送來的 datas 的行列數(size)、位置對應關係(i,j)均一致的行情推送方式是有效的(比如部分 CTP 的 com 介面),但在例2-1中,我們已經看到,Wind 採用了更新推送的方式(只推送更新過行情的合約,並且不一定推送所有行情),因此,每次傳入 myCallBack 的 codes、fields、datas 的尺寸(size)都不定。這樣一來,對 datas 某行某列對應的是哪個行情欄位、哪個合約這個問題,就不那麼顯而易見了。datas 的形狀可變性對用來存儲它的數據結構提出了新的要求:(2)按行情欄位名(fields)、合約代碼(codes)定位 datas 對應數據的方式,不受 datas行列數(size)變化影響。
並且,該數據結構還應當:
(3)定位快。
這是因為當所訂閱的合約、行情欄位多時,行情推送和 MATLAB 後台運算可能發生衝突,從而發生行情阻塞,造成行情數據延遲,甚至發生行情數據丟失。
不能按照事先存儲的位置關係直接定位 datas 的行、列對應的 fields、codes,就只能按照傳入 myCallback 的 fields 變數(length 和 datas 變數的行數一致)和 codes 變數(length 和 datas 變數的列數一致)進行定位了。這兩個變數均為字元串形,如果通過比較字元串(strcmp)進行定位,勢必會影響到速度,而 containers.Map 由於可以以字元串為 key,且搜索時的計算複雜度為線性級別,因此,containers.Map 正是符合前述條件的數據結構。具體的應用如下例所示:
例3-1. 編寫 ex_3_1.m,使用兩個 containers.Map 型變數,儲存 例2-1 所示的三支股票、兩個行情欄位的實時行情數據。
function ex_3_1 n global wn global map_LAST map_VOLn w = windmatlab;n map_LAST = containers.Map(); % 生成儲存最新價的映射表n map_VOL = containers.Map(); % 生成儲存成交量的映射表n % 訂閱最新價n w.wsq(601628.SH,601318.SH,510050.SH,RT_LAST,@myCallBack_LAST); n % 訂閱成交量n w.wsq(601628.SH,601318.SH,510050.SH,RT_VOL,@myCallBack_VOL);n nendnnfunction myCallBack_LAST ( reqid, isfinished, errorid, datas, codes, fields, times ) % 最新價訂閱的回調函數n global map_LASTn for iter = 1:length(codes)n map_LAST(codes{iter}) = datas(iter); n endnendnnfunction myCallBack_VOL ( reqid, isfinished, errorid, datas, codes, fields, times ) % 成交量訂閱的回調函數n global map_VOLn for iter = 1:length(codes)n map_VOL(codes{iter}) = datas(iter);n endnendn
這個設計的關鍵是:用 Wind 推送來的合約代碼(codes )作為存放行情數據的映射表(map_LAST、map_VOL)的 key(鍵),用
映射表(『合約代碼』)n
的方式來定位推送來的 datas 對應的是哪個合約:
map_LAST(codes{iter}) = datas(iter);n
回調函數使用了 for 循環,可處理每次推送的 datas 尺寸不同的問題。
運行 ex_3_1.m ,MATLAB 即啟動訂閱,並將收集到的數據存放到相應的映射表中。查看某合約的成交量,可以這樣:
>> global map_LAST; map_LAST(合約代碼)n
查看某合約的最新價,可以這樣:
>> global map_VOL; map_VOL(合約代碼)n
下圖為運行結果:
相鄰兩次 map_VOL 取到的數據不一樣,這是因為行情數據(這裡對應成交量)發生了更新。需要注意的是,上例為兩個行情欄位(成交量、最新價)啟動了兩個訂閱,相應地,就有兩個回調函數。也可以只啟動一個訂閱(相應地,只有一個回調函數),此時,存放行情數據的映射表可以仍以股票代碼為 key,而其 keyvalue 可以是自定義的類,見第 5 節。
另外,結構體 struct 這一數據結構也可按字元串定位數據,但 struct 的「域」不能以數字開頭(而股票代碼都是數字開頭的),struct 的搜索速度也比映射錶慢,故不適用於本例。
文章開頭的實盤監控界面只用到了當前最新的行情數據,因此,本例無需將歷史數據保存在映射表中。若要存儲歷史數據,可以考慮把 myMap 的 keyvalue 設為 matrix,這是因為 containers.Map 的 keyvalue 可以是 any類型。
4 完整代碼:用 containers.Map 存儲實盤行情
上例代碼風格是面向過程的,這種風格不利於程序擴展,還使用了 global 變數。現將代碼轉換為面向對象的風格,方便不熟悉「面向對象」概念的讀者體會兩者使用上的差別:
代碼 用 containers.Map 存儲實盤行情.
classdef myMap < handle % 把 ex3_1.m 轉換成面向對象的風格(類)nn properties n map_LAST % 存儲「最新價」這一行情數據的映射表n map_VOL % 存儲「成交量」這一行情數據的映射表n CodeSets % 要訂閱的合約n wn endnn methods n function obj = myMap()n obj.w = windmatlab; n % 要訂閱的合約n obj.CodeSets = {601628.SH,601318.SH,510050.SH}; n obj.map_LAST = containers.Map();n obj.map_VOL = containers.Map();n % 佔位 n for iter = 1:length( obj.CodeSets )n obj.map_LAST( obj.CodeSets{iter} ) = [];n obj.map_VOL( obj.CodeSets{iter} ) = [];n end n endnn function startBooking(obj)n % 啟動訂閱n % strjoin 將 cell 型的 obj.CodeSets 轉換成 Wind n % 認可的合約代碼(集)字元串n % 結果將是 601628.SH,601318.SH,510050.SHn strCodes = strjoin(obj.CodeSets,,);nnn % 向 Wind 訂閱三支股票的最新價行清欄位n obj.w.wsq(strCodes,RT_LAST,@obj.myCallBack_LAST); nn % 向 Wind 訂閱三支股票的最新價行清欄位n obj.w.wsq(strCodes,RT_VOL,@obj.myCallBack_VOL); n endnn function stopBooking(obj)n % 取消訂閱n obj.w.cancelRequest(0);n endn function myCallBack_LAST( obj, reqid, isfinished, errorid, datas, codes, fields, times) n % 最新價訂閱的回調函數n for iter = 1:length(codes)n obj.map_LAST(codes{iter}) = datas(iter);n endn endnn function myCallBack_VOL( obj, reqid, isfinished, errorid, datas, codes, fields, times) n % 成交量訂閱的回調函數n for iter = 1:length(codes)n obj.map_VOL(codes{iter}) = datas(iter);n endn endn endnnendn
可以看到,上述代碼將 Wind 行情訂閱的訂閱(w.wsq) n封裝在了 function startBooking 中,將 w.cancelRequest 封裝在了 functionnstopBooking 中,這兩個 function 統稱 "myMap" 類的 methods (方法),這麼做的好處是:程序結構更清晰,並方便了外部調用。 此外,例 3-1 中的兩個映射表被封裝在了 properties(屬性)中,這可以避免過多的 global 彼此干擾。這就是面向對象的代碼風格,myMap.m 定義了 myMap 類。將上述代碼保存為 myMap.m ,即可使用,方法如下:
>> obj = myMap(); % 生成一個 myMap 類對象n>> obj.startBooking() % 啟動行情訂閱n
此時,MATLAB在後台啟用了 Wind 服務,連接了實盤行情,並實時地進行著行情數據的更新。在command window 中輸入如下語句,可以看到最新的行情數據:
>> obj.映射表名(合約代碼);n
例如:
5 改進設計:適用於多行情欄位
第四部分的 myMap.m 用兩個映射表(map_LAST, map_VOL)分別儲存最新價和成交量這兩個行情欄位,這樣的設計存在一個問題:如果想要添加新的行情欄位,就要添加新的 map_xxx 屬性。考慮到 containters.Map 的 keyvalue 可以是 any類型,我們可以定義一個新的類:myUnit,作為儲存行情映射表的 keyvalue, 該類的屬性包含所要監控的所有行情欄位。類定義文件如下:
classdef myUnit < handlenn properties n code % 合約代碼n RT_LAST % 最新價n RT_VOL % 成交量n endnn methodsn function obj = myUnit(code) % 構造函數n obj.code = code;n endn endnnendn
令 map_RMD 為以合約代碼為 key、myUnit 類對象為 keyvalue 的映射表,則這一個映射表即可囊括 RT_LAST、 RT_VOL 這兩個行情欄位。
這樣做的好處是:
- 添加一個行情欄位,只需要修改 myUnit.m 文件,為其添加一個行情欄位屬性,而無需修改 myMap.m 文件
- 可以直接用
tmph = map_RMD(合約代碼);tmph.行情欄位名n
來獲取某合約、某行情欄位的實時數據。
在需要訂閱較多行情欄位時,這樣的設計是便捷的。修改後的設計保存為 myMap2.m,完整代碼如下:
classdef myMap2 < handle % myMap 類的改進nn properties n map_RMD % 存儲所有行情欄位的映射表n CodeSets % 要訂閱的合約n w % Wind n endnn methods n function obj = myMap2() % 構造函數的函數名必須和類名一致n obj.w = windmatlab; n obj.CodeSets = {601628.SH,601318.SH,510050.SH}; n obj.map_RMD = containers.Map(); % 用一個映射表囊括兩個行情欄位n % 佔位 n for iter = 1:length( obj.CodeSets )n sCode = obj.CodeSets{iter};n obj.map_RMD( sCode ) = myUnit( sCode ); n % 映射表 map_RMD 的 keyvalue 是 myUnit 類(屬性為行情欄位)對象n end n endnn function startBooking(obj)n % 開始訂閱n strCodes = strjoin(obj.CodeSets,,);nn % 兩個行情欄位合用一個訂閱n obj.w.wsq(strCodes,RT_LAST,RT_VOL,@obj.myCallBack); n endnn function stopBooking(obj)n % ......( 和 myMap 一致)n endnn function myCallBack( obj, reqid, isfinished, errorid, datas, codes, fields, times) n % 最新價訂閱的回調函數n for iter = 1:length(codes)n tmph = obj.map_RMD(codes{iter});n for j = 1:length(fields)n eval( [tmph.,fields{j},=datas(j,iter);] )n endn endn endn endnendn
myMap2 的使用和 myMap 基本一致,差別只在於數據引用方式:
>> obj2 = myMap2(); % 生成 myMap2 類 對象n>> obj2.startBooking; % 啟動訂閱n
6 總結
至此,我們介紹了containers.Map(映射表)這個數據結構於實盤行情存儲上的應用,它的主要優勢是:
- 定位方便(可按照字元串形的股票 code 和行情 field 定位數據,並且允許 code 以數字開頭)
- 定位快(常數的複雜度)
下一篇,我們將介紹 containers.Map(映射表)在界面設計上的應用,我們將進一步體會到這個數據結構的優勢。
%%%%%%%%%%%%%%% 分割線 %%%%%%%%%%%%%%%
注
1-1 「如果不考慮供應商行情介面推送數據本身的延遲,該界面行情延遲肉眼不可辨。」
筆者也把同樣的設計應用於 CTP 行情介面,所生成的界面和同花順、Wind 實盤行情界面的行情同步(同步指:肉眼幾乎不可辨差別),故而得出了這個結論。
1-2 MVC 模式:指 model-view-control 分開的 GUI 設計模式,以下內容摘自《MATLAB 面向對象編程——從入門到設計模式》,pp. 122:
模型反映的是程序的邏輯,是相對穩定的,而視圖界面需要經常調整,界面的調整不應該影響到程序的模型。採用面向對象的思想,最顯然的做法是把界面和模型封裝到不同的類當中去,讓各個類各司其職。這叫做把界面的變化和模型解耦。還有一些和界面模型無關的功能,比如處理用戶的輸入,我們把它們歸類到第三個類中去,叫做控制器(Controller)類。這樣用三個基本的類來組織整個 GUI 程序就是模型-視圖-控制器(MVC)模式。
1-3 「面向過程」 vs 「面向對象」
簡單來說,形如例2-1、例3-1這種把所有代碼裝在一個(或多個)m文件里的程序,是面向過程的。它的缺點是顯而易見的:使用了 global 變數,代碼冗長不易讀。進一步的,若要再添加文章開頭所示界面上的一眾 edit 框,勢必導致代碼更冗長、更不易讀。此外,如果修改訂閱合約,就需要修改原 m 文件,或者建立新的 m 文件,這說明面向過程的代碼「復用性」不好。如果使用本文第四部分 myMap.m 這種面向對象的代碼風格(定義類文件),這些問題均可迎刃而解。面向對象的代碼風格優勢絕不止於此,myMap.m 也有諸多可優化之處,本文不展開了。
面向對象的系統學習可參考《MATLAB 面向對象編程——從入門到設計模式》一書。筆者認為,面向對象的代碼風格極有利於編寫「需要長期使用而且不斷更新的程序」(該書封底),值得一習。
1-4 下載和申請試用 Wind :Wind資訊--下載中心
2-1 關於「回調函數」:回調函數,本質上是觀察者模式的實現。MATLAB 的 GUI 控制項也有「回調函數」一說,它也是觀察者模式的實現,可參考《MATLAB 面向對象編程——從入門到設計模式》一書 17.1節。
另外,回調函數的介面通常是固定的,如果不清楚介面的參數是如何設置的(例如,介面說明文檔未寫明),可以用 varargin 代替輸入參數,並用 disp 將輸入參數輸出到 command window,以查看介面的內容,例如:
function myCallBack(varargin)n for iter = 1:length(varargin)n disp(varargin{iter});n endnendn
推薦閱讀:
※機器學習筆記16 —— 編程作業5線性回歸演算法的評估
※Matlab如何從曲線圖中提取原始數據
※已知f(x,y)=0以及z=g(x,y),如何用matlab求(x,z)的圖像?
※加速你的MATLAB開發(1): 自動生成MATLAB代碼