MATLAB 高級數據結構連載 6:使用containers.Map實時監控股票行情(2)

1 引言

上一篇文章介紹了如何使用 containers.Map這一數據結構存儲 3 支股票(中國人壽、中國平安、50ETF)、二個行情欄位(最新價、成交量)的實盤行情數據,並將之封裝在了 myMap.m (類文件)中,它的使用方法為:

>> obj2 = myMap2(); % 生成一個 myMap2 類對象n>> obj2.startBooking() % 啟動行情訂閱n

此時,可使用

obj.map_RMD(合約代碼) n

獲取該合約所有行情欄位的最新行情數據(僅限交易時段),例如:

使用ncontainers.Map 儲存實盤行情的主要優點是:

  • 定位方便(可按照字元串形的股票 code 和行情欄位 field 定位數據,並且允許 code 以數字開頭)
  • 速度快(搜索為常數複雜度)

上述內容是 containers.Mapn於 model 層面的應用,解決了後台數據存儲的問題。本文進一步討論 containers.Map 在 view 層面的應用:如何用 containers.Map 構建如下圖所示的行情監控界面:

Figure : 3支股票、二個行情欄位的實盤行情監控界面

2 部件分析:單個合約的行情監控 edit(OneLine.m)

上述行情監控頁面的難點在於:當行情發生更新時,如何快速地把更新了的行情展示在對應的 edit 框里。為簡化問題,我們先看單個合約(這裡以 50ETF 為例)、兩個行情欄位的情況。

首先討論 edit 框 的建立。新建 figure(1),在其上建立兩個 editn框,分別用於展示 50ETF 的兩個行情欄位;將這兩個edit 框的 handle 分別保存為n"RT_LAST", "RT_VOL"(和 Wind 行情推送來的行情欄位名一致):

hFig = figure(1);clf(hFig);hFig.MenuBar = none;nRT_LAST = uicontrol(style,edit,parent,hFig,Position,[60 20 60 20],String,RT_LAST);nRT_VOL = uicontrol(style,edit,parent,hFig,Position,[0 20 60 20],String,RT_VOL);n

運行得到結果如下:

由於儲存行情數據使用的數據結構是映射表,可直接用 field 定位數據,因此,當行情發生更新時,可直接用:

tmph = 存儲 50ETF 所有實盤行情的 handle(是上一篇第 5 部分討論的 myUnit 類對象);n欄位名.String = tmph.欄位名n

來更新相應 edit 框的數據,具體代碼如下:

>> modelobj = myMap2(); % 生成 myMap2 類對象n>> modelobj.startBooking; % 啟動訂閱n>> tmph = modelobj.map_RMD(510050.SH); % 取出 50ETF 實盤行情的 handlen>> RT_LAST.String = tmph.RT_LAST; % 更新展示最新價的 edit 框n

(註:較早的 MATLAB 版本可能要 num2str, 且不能直接用 . 賦值)

完成更新後的界面是:

同理,可更新展示成交量的 edit 框:

>> RT_VOL.String = tmph.RT_VOL;n

更新後得到:

簡單起見,兩個更新統一用 eval 完成:

FieldSets = {RT_LAST,RT_VOL};nfor iter = 1:length(FieldSets)n sField = FieldSets{iter};n eval([sField,.String = tmph.,sField,;]);nendn

更新後得到:

(注意到上圖 ETF 價格和前一圖不一致,這是因為行情數據一直在更新。)

現令 OneLine.m 為展示單個合約所有行情欄位的類文件,將上述修改 edit 框為最新行情數據的代碼封裝在 update_RMD 方法中,供外部調用。OneLine 類的類文件代碼如下:

classdef OneLine < handle % 某個合約所有行情欄位的監控頁面。一行。nn propertiesn hLayn hRMD % 儲存某個合約實盤行情的 handle,為 map_RMD 的 某個 keyvalue。n RT_LAST % 展示行情欄位「最新價」的 edit 框n RT_VOL % 展示行情欄位「成交量」的 edit 框 n FieldSets = {RT_LAST,RT_VOL};n endnn methodsn function obj = OneLine(hLay,hRMD)n obj.hLay = hLay;n obj.hRMD = hRMD; n % 生成兩個行情欄位 edit 框n obj.buildUI;n % 刷新界面n obj.update_RMD;n endnn function buildUI(obj) % 生成界面n % 如果界面上有其它東西,先刪掉。n if ~isempty(obj.hLay.Children)n delete(obj.hLay.Children)n endn % 依次生成兩個 edit 框:n tmpLay = uiextras.HBox(Parent,obj.hLay);n uicontrol(style,edit,parent,tmpLay,String,obj.hRMD.code,fontsize,12,fontweight,bold);% 展示 coden obj.RT_LAST = uicontrol(style,edit,parent,tmpLay,String,RT_LAST);n obj.RT_VOL = uicontrol(style,edit,parent,tmpLay,String,RT_VOL);n endnn function update_RMD(obj) % 更新所有行情欄位的方法,供外部調用n for iter = 1:length( obj.FieldSets )n sField = obj.FieldSets{iter};n eval([obj.,sField,.String = obj.hRMD.,sField,;]);n endn endnn endnendn

上述代碼用到了 uiextras 插件,它是自動排列 edit 框的 MATLAB 插件(注2-1)。

於是,可用以下語句生成50ETF 的多行情欄位監控頁面:

OneLine( 圖像 handle, 50ETF 行情 handle) n

具體代碼如下:

>> hFig = figure(1);clf(hFig);hFig.MenuBar = none; n>> hRMD = modelobj.map_RMD(510050.SH);n>> viewobj = OneLine( hFig, hRMD);n

運行上述代碼,得到:

Figure 單個合約、所有行情欄位的行情監控頁面

此時,每調用一次 viewobj.update_RMD ,可將界面更新為最新的行情數據

這個設計存在一個問題:界面上的 edit 框,都是手動添加的;如果希望修改需監控的行情欄位,就要修改 OneLine.m 的 properties,並相應地修改 buildUI 方法,這是很不方便的,不利於代碼擴展和復用。上一篇已經強調過,containers.Map 的 keyvalue 可以是 any,於是,這個問題的解決方案可以是:用 containers.Map 儲存這些 edit 框n的 handle。

設該映射表名為 mapUI,將之設置為 OneLine 類的屬性。這樣一來,更改所需監控的行情欄位, 就只需要 buildUI 中更改 mapUI 的 key(行情欄位名)和 keyvalue(顯示該行情欄位的 edit 框)。

進一步的,我們可以令 mapUI 的 key 完全依賴於 hRMD(儲存單個合約所有行情欄位的 myUnit 對象)的屬性,這就完全解耦了 model 類和 view 類:負責指定所需監控的行情欄位的 myUnit 類一旦發生修改,負責展示的 OneLine 類(重新生成的對象)會相應發生更改,無需手動修改 OneLine 類的代碼。更新後的 buildUI 方法為:

function buildUI(obj) n % 如果界面上有其它東西,先刪掉。n if ~isempty(obj.hLay.Children)n delete(obj.hLay.Children)n endn % 依次生成 edit 框: n tmpLay = uiextras.HBox(Parent,obj.hLay);n obj.mapUI = containers.Map();n obj.FieldSets = fieldnames(obj.hRMD); % 獲得所有欄位名稱n for iter = 1:length(obj.FieldSets) n sField = obj.FieldSets{iter}; n obj.mapUI( sField ) = uicontrol(style,edit,parent,tmpLay,String,sField );n endnendn

此時,界面所生成的 edit 框,完全取決於nmyUnit 類(用 fieldnames 獲取該類的所有屬性——行情欄位名);修改了 myUnit 類,(重新生成的)界面就會自動進行調整。

更新後的類稱為 OneLine2, 它的使用和 OneLine 一致,這裡不再贅述;OneLine2 的完整代碼為:

classdef OneLine2 < handlen % 某個合約所有行情欄位的監控頁面。一行。n propertiesn hLayn hRMD % 儲存某個合約實盤行情的 handle,為 map_RMD 的 某個 keyvalue。n mapUI % 儲存所有 edit 框 handle 的映射表n FieldSetsn endn methodsn function obj = OneLine2(hLay,hRMD)n obj.hLay = hLay;n obj.hRMD = hRMD; n % 生成兩個行情欄位 edit 框n obj.buildUI;n % 刷新界面n obj.update_RMD;n endn function buildUI(obj) n % 如果界面上有其它東西,先刪掉。n if ~isempty(obj.hLay.Children)n delete(obj.hLay.Children)n endn % 依次生成 edit 框: n tmpLay = uiextras.HBox(Parent,obj.hLay);n obj.mapUI = containers.Map();n obj.FieldSets = fieldnames(obj.hRMD); % 獲得所有欄位名稱n for iter = 1:length(obj.FieldSets) n sField = obj.FieldSets{iter}; n obj.mapUI( sField ) = uicontrol(style,edit,parent,tmpLay,String,sField );n obj.update_RMD(sField); % 刷新界面n endn endn function update_RMD(obj,sField) % 更新 sField 這個行情欄位的方法,供外部調用n tmph = obj.mapUI( sField );n eval([tmph.String = obj.hRMD.,sField,;]);n endn endnendn

注意這裡將 update_RMD 方法修改為:只更新 sField 這個行情欄位。這是因為實際使用時,Wind 並不會每次都更新所有行情欄位。後文將沿用這個設計。

以上內容討論了 containers.Map 數據結構於單個合約行情監控界面設計(OneLine類)的應用:使用映射表 mapUI 儲存該合約所有行情欄位對應的 edit 框,該映射表的 key 是行情欄位名,keyvalue 是 edit 框n的 handle。我們已經看到,這個設計有一個好處:如果 model 類(本例對應nmyUnit 類)的行情欄位發生了改變,該 view 類會自動修改相應界面,無需手動修改nOneLine2.m

這個設計還有一個好處:由於映射表 key 的設置和 Wind 傳入回調函數(行情發生更新時 Wind 調用的 MATLAB 函數) 的 fields 變數的設置完全一致(都是形如 RT_LAST、RT_VOL 這樣的 Wind-Readable 行情欄位名),因此,可以直接用 Wind 傳入的 fields 定位界面上的哪一個 edit 框需要更新。

3n多個合約的行情監控(MultiLines.m)

現在考慮文章開頭的行情監控頁面:

Figure 3-1: 三隻股票、兩個行情欄位的行情監控頁面

可以看到,它的每一行是一個 OneLine 類對象。和第 2 節討論的 OneLine2 設計一樣,我們希望這個界面可以自由擴展:如果 modelobj 中訂閱的股票發生了改變,該界面(重新生成後)會相應進行修改,無需手動修改對應 view 類的代碼。於是,仿照第 2 節思路,我們可以用 containers.Map 存儲上圖每一行的 handle,該映射表的 key 是股票的合約代碼,與 model 類(本例對應 myMap2 類)的 map_RMD 屬性的 key 一致;該映射表的 keyvalue 是上圖每一行的 handle。將該映射表命名為 map_Lines, 生成整個界面的類文件完整代碼如下:

classdef MultiLines < handle % 所有合約的行情監控頁面,行數 = 所訂閱的合約數n propertiesn hFig % 用於顯示行情監控頁面的 figure 的 handlen modelobj % MyMap3 類 對象n map_Lines % 映射表:key 是股票的合約代碼, keyvalue 是 OneLine 類對象的 handlen endn methodsn function obj = MultiLines( hFig, modelobj )n obj.hFig = hFig;n obj.modelobj = modelobj;n % buildUIn obj.buildUI();n % update marked datan obj.update_RMD();n endn function buildUI(obj)n % 清理界面n if ~isempty(obj.hFig.Children)n delete(obj.hFig.Children);n endn % 使用 uiextras 插件,從上往下,依次生成各個合約的行情監控頁面n obj.map_Lines = containers.Map();n KeySets = obj.modelobj.map_RMD.keys;% 訂閱了的所有股票合約的合約代碼n HLay = uiextras.VBox(Parent,obj.hFig); % VBox:豎直排列n for iter = 1:length(KeySets)n sCode = KeySets{iter};n hRMD = obj.modelobj.map_RMD( sCode ); % 取出存儲 sCode 這個合約的 myUnit 對象n tmpLay = uiextras.Empty(Parent,HLay );n obj.map_Lines( KeySets{iter} ) = OneLine2( tmpLay,hRMD);n endn endn function update_RMD(obj,src,event)n % 待定n endn endnendn

這裡的 update_RMD 方法沒有列出,將在第 4 節討論。

MultiLines 類的使用方法:

>> hFig = figure(1);clf(hFig);hFig.MenuBar = none;n>> modelobj = myMap2();modelobj.startBooking;n>> viewobj_super = MultiLines( hFig, modelobj );n

它可以生成本節開頭所示界面。

4. view 和nmodel 的聯動

回顧 Figure 3-1,它是使用 MultiLines 類生成的多個合約的行情監控界面。但是,該界面上的行情數據,只是界面生成那一時刻的行情數據,它並不會隨時間的變化而動態變化。那麼,如何實時地刷新界面呢?我們已經知道,可以調用 viewobj_super.update_RMD 對界面進行刷新,得到最新的行情數據,於是,我們可以用以下兩種方式動態刷新界面:

  1. 使用 timer,隔一段時間刷新一次界面。這麼做的好處是:行情的更新速度很快(可能在 ms 級),但是肉眼無法感知如此高頻的刷新,高頻刷新界面的代價也過大(界面刷新代價遠遠大於後台數值運算代價)。但本文討論的股票行情更新速度並沒有那麼快(秒級),因此我們採用第二種方式:
  2. 在行情推送的回調函數(本例對應 myMap2 類的 myCallBack 方法)中觸發界面的刷新

這就需要用到上一篇中提到的觀察者模式(參考資料見上一篇 ):行情發生更新後,負責後台數據儲存的 modelobj 對外發布通知,負責更新界面的 viewobj 接到通知後,調用 update_RMD 函數,進行界面刷新。具體來說,整個觀察者模式的實現是這樣的:

(1)由於我們希望每次行情更新後都能觸發界面刷新,因此 modelobj 對外發布通知的工作應當放在行情推送的回調函數(myMap2 類的 function myCallBack)中完成。MATLAB 發布通知用 "notify" , 通知名可以是任意的,這裡命名為nDataUpdated:

>> 發布者對象.notify(DataUpdated);n

(2)為了讓 modelobj 能夠發布通知,還需要為 myMap2 類定義 events(事件):

classdef ...n properties % 屬性n ... n endn events % 事件n DataUpdatedn endn methods % 方法n ... n endnendn

(3)接到通知的 viewobj 如何知道哪些合約的哪些欄位發生了更新呢?一種簡單的做法,是在nmodel 類中添加兩個屬性:updated_Fields、updated_Codesn,記錄更新了的合約代碼和行情欄位名稱。還有一種做法,是將更新信息包裝在通知中(注4-1)。本案例採用前一種方式。為示區別,將定義了 events、在 myCallBack 中添加了 notify、增加了更新信息屬性(updated_Fields、updated_Codes) 的 myMap2 類,重新寫為 myMap3 類:

classdef myMap3 < handle n n properties % 屬性n ... % 和 myMap2.m 一致n updated_Codes % 新增記錄最近更新的合約n updated_Fields % 新增記錄最近更新的行情欄位n endnn eventsn DataUpdated % 新增事件名n endnn methods % 方法n function obj = myMap3() n ... % 和 myMap2.m 一致n function startBooking(obj)n ... % 和 myMap2.m 一致n function stopBooking(obj)n ... % 和 myMap2.m 一致n function myCallBack( obj, reqid, isfinished, errorid, datas, codes, fields, times) n % 最新價訂閱的回調函數 ,和 myMap2.m 一致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 % 記錄更新了的合約代碼 code 和行情欄位 fieldn obj.updated_Fields = fields;n obj.updated_Codes = codes;n obj.notify(DataUpdated); % 發送通知n endn endnendn

以上介紹了行情更新後, model 類(本例對應 myMap3 類)如何對外發通知,接下去介紹 view 類(本例對應 MultiLines 類)如何接通知、如何一接到通知就進行界面刷新的工作。

(4)為了能夠接收到 model 類的通知,view 類需要監聽該 DataUpdated 通知,監聽的方法是:

>> 發布者對象.addlistener( 事件名稱, @監聽回調函數)n

其中,「監聽回調函數」是每次接收到通知以後,MATLAB 會自動調用的函數。將監聽回調函數設置成 view 類的 update_RMD 方法,則 view 類一接到 model 類的通知,就會調用該方法,刷新界面。這樣一來,model 類和 view 類的聯動就完成了。

和 Wind wsq 的回調函數類似,監聽回調函數的輸入參數也是被固定了的(Wind WSQ 介面回調函數的輸入參數是被 windmatlab 固定的,監聽回調函數的輸入參數是被 MATLAB 固定了的),依次為:通知發送源 src,自定義事件(本例沒有自定義事件,這個參數不會被調用)。view 類監聽 model 類的工作,可以在 view 類的構造函數(constructor, 顧名思義,為生成該類對象時自動調用的函數)中完成:

classdef MultiLines < handle % 所有合約的行情監控頁面,行數 = 所訂閱的合約數n propertiesn ...n hListener % 存儲 listener n endn methodsn function obj = MultiLines( hFig, modelobj )n ...n % 觀察者模式:監聽 modelobj 的 DataUpdated 方法。n obj.hListener = obj.modelobj.addlistener( DataUpdated,@obj.update_RMD);n endn function buildUI(obj) ...n function update_RMD n function delete(obj)n delete(obj.hListener);n endn endnendn

這裡需要注意的是,view 對象被刪除時,listener 不會自動被清理,要自己完成清理的工作,該清理在上述 delete 方法中完成(注4-2)。

(5)界面對象收到通知後自動調用的監聽函數 —— MultiLines 的 update_RMD 方法——應該是這樣的:根據 modelobj 中記錄的更新信息(最近更新的合約代碼:updated_Codes, 最近更新的行情欄位: updated_Fields)來更新界面。

由於Figure 3-1的每一行都是一個 OneLine 對象,這些對象都有根據行情欄位名刷新界面相應 edit 框的方法(OneLine 的 update_RMD 方法),因此,整個行情監控頁面(viewobj_super)的刷新工作,可以通過調用這些子對象的 update_RMD 方法來完成。由於這些 OneLine 對象的 handle 都是映射表 map_Lines 的 keyvalue,因此這個調用是很簡明的:

function update_RMD(obj,src,event)n CodeSets = obj.modelobj.updated_Codes;n FieldSets = obj.modelobj.updated_Fields;n for iter = 1:length(CodeSets)n tmph = obj.map_Lines( CodeSets{iter} ); % 取出該合約代碼對應的那一行監控頁面的 handle(是 OneLine 類對象)n for j = 1:length(FieldSets)n tmph.update_RMD(FieldSets{j});% 調用 OneLine 對象的 update_RMD 方法。n endn endn endn

綜上,界面和後台數據的互動完成了,它們的順序是這樣的:

1. Wind 行情發生更新

2. MATLAB 調用向 Wind 訂閱行情時註冊的回調函數—— modelobj 的 myCallBack 方法——進行以下工作:

(1)n更新後台儲存的行情數據(modelobj.map_RMD )

(2)n向外發布通知 DataUpdated

3. 監聽該通知的界面對象, 調用監聽回調函數 —— viewobj_super 的 update_RMD 方法——根據 modelobj 的 updated_Fields、updated_Codes 屬性,刷新相應合約、相應行情欄位的 edit 框。

讀者可能會有一個疑問:為什麼要用觀察者模式實現界面的更新?直接在 model 類的 myCallBack 中調用 view 類的 update_RMD 方法不行么?不好。這是因為,這樣就耦合的Model和View對象,Model對象不應該知道View的細節,View對象也不應該把自己的內部細節暴露給外部的類,如果存在這樣的耦合,今後對View類的修改,比如對函數名的修改,還要同時更新Model處的代碼,這是不好的程序設計。

5 啟動/終止訂閱

為方便界面的使用,還應當設置一個 「啟動/終止訂閱」 的按鈕,該按鈕可以用 MATLAB 的 toggle button 來做,其 callback function 可以直接調用 modelobj 的 startBooking 和 stopBooking 方法,這裡不再展開。

6n總結

至此,本案例介紹完畢。這個案例採用 模型-界面 分離的設計模式(MVC),多處採用了ncontainers.Map(映射表)這一數據結構,使行情監控的實現變得很簡潔。具體來說,containers.Mapn於本案例的應用有:

1. 用 containers.Map 存儲多支股票、多個行情欄位的實盤行情數據

key:合約代碼

keyvalue:MyUnit 類對象

優點:(1)速度快(常數複雜度)。(2)定位方便:若 tmph = 映射表(合約代碼),

則 tmph.行情欄位 == 該合約該行情欄位的最新市場價格,例如:

>> modelobj = myMap3();modelobj.startBooking;n>> tmph = modelobj.map_RMD(510050.SH)nntmph = nn myUnit with properties:nn code: 510050.SHn RT_LAST: 2.293n RT_VOL: 248396001n

2. 用 containers.Map 存儲單個合約所有行情欄位 edit 框的 handle

key:行情欄位名(如 RT_LAST)

keyvalue:edit 框n的 handle

優點:(1)如果 model 類(本例對應nmyUnit 類)的行情欄位發生了改變,該 view 類(本處對應 OneLine2 類)會自動修改相應界面,無需手動修改 view 類的類文件。(2)因為映射表 key 的設置和 Wind 傳入回調函數的 fields 變數的設置一致,所以可以直接用 Wind 傳入的 fields 定位界面上需要更新的 edit 框。

3. 用 containers.Map 存儲所有合約的行情監控對象

key:股票的合約代碼

keyvalue:單個合約、所有行情欄位的行情監控頁面(圖2-1,為 OneLine 類對象)

優點:(1)如果 modelobj 中訂閱的股票發生了改變,該界面(重新生成後)會相應進行修改,無需手動修改代碼。(2)因為映射表以合約代碼為 key,所以容易根據 modelobj.updated_Codes(最近發生更新的合約代碼),定位需要更新的 OneLine 對象

%%%%%%%%%%%%%%%%%%分割線%%%%%%%%%%%%%%%

2-1 uiextras 的下載和簡要說明:GUI Layout toolbox的兩個版本的說明

4-1 自定義事件:以下內容摘自《MATLAB 面向對象編程——從入門到設計模式》 4.3節:

有時,發布者除了通知觀察著,還需要向觀察者發送一些數據,所以 MATLAB 還允許用戶自定義一個消息類來定製要傳遞的消息,並且該消息類必須繼承自 event.EventData 基類。

本例中,可以自定義 xx 類 < event.EventData(MATLAB 中,< 代表繼承), 將 updated_Codes 和 updated_Fields 包裝在該類中。此時,modelobj(myMap2 類對象) 發布通知時,可採用如下格式向外發送數據:

notify(DataUpdated, 該xx類對象)n

監聽回調函數(update_RMD)的第二個輸入參數 event,就是接收到的該 xx 類對象。

4-2 MATLAB 的 delete 方法,是銷毀對象時 MATLAB 會自動調用的方法。參見《MATLAB 面向對象編程——從入門到設計模式》一書3.3節:類的析構函數.


推薦閱讀:

MATLAB 高級數據結構連載 5:使用containers.Map實時監控股票行情(1)
機器學習筆記16 —— 編程作業5線性回歸演算法的評估
Matlab如何從曲線圖中提取原始數據
已知f(x,y)=0以及z=g(x,y),如何用matlab求(x,z)的圖像?
加速你的MATLAB開發(1): 自動生成MATLAB代碼

TAG:MATLAB | 面向对象编程 | 设计模式 |