【前言】整合底層接口的各項功能到中層引擎中后,當我們開發頂層應用時(GUI或者策略算法)。只需知道中層引擎對外提供的主動API函數以及事件引擎中相關的事件類型和數據形式即可。
在GUI和策略算法這兩個主要類型的頂層應用中,先介紹GUI開發的原因是:目前國內支持用戶定制化開發GUI界面的量化平台少之又少,而包含一個比較全面的GUI開發教程的則據我所知還沒有。隨着國內越來越多的衍生品推出(期權、分級基金、未來的反向基金),很多新型的交易策略從全自動轉向了半自動,經常需要交易員的手動干預(啟動暫停策略、盤中微調參數等),以及投資組合層面的風險管理(期權希臘值、分級基金行業暴露等),這種情況下傳統上僅支持策略算法開發的量化交易平台變得越發難以滿足交易員的需求(包括作者本人),所以估計這方面的文章更能填補當前市場需求的空缺(笑...)。
我司的交易平台是沒有gui的,所以我可以基於此開發GUI的監控系統。
參考於系列文章:http://www.vnpy.org/basic-tutorial-8.html
一、PyQt
目前Python上主要的GUI開發工具包括:tkinter、PyQt、PyGTK和wxPython,筆者選擇PyQt的主要原因是:
-
Anaconda中已經包含(早期版本中包含的是LGPL協議的Pyside,穩定性不如PyQt)
-
另一個內置GUI庫tkinter的功能太弱
網上對這四款GUI開發工具比較分析的文章很多,有興趣的讀者可以自己搜搜看。
接下來的幾篇GUI開發教程會假設讀者已經對PyQt開發有了一定的了解,主要針對和量化交易平台開發相關的部分,需要補充基礎知識的讀者建議參考以下資源:
-
zetcode.com,上面的教程簡單明了,從頭到尾做一遍對PyQt的工作原理基本就有個全面的了解了
-
Rapid GUI Programming with Python and Qt,一本由Riverbank(PyQt的開發公司)員工推出的PyQt開發教程,非常細致全面,但是部分內容跳躍性太大,需要從頭到尾多看幾遍
-
也可以在遇到特定問題時搜百度或者StackOverflow,通常都能找到答案
PyQt版本 在Riverbank官網下載:
PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe Windows 32 bit installer
請特別注意這個版本的問題,從最近幾個月的經驗看,很多PyQt相關模塊運行時報錯就是因為下錯了版本(PyQt5、64位等等都不對)。
二、GUI組件
從功能上看,所有交易平台的GUI組件都可以分為兩類,數據監控(被動)和功能調用(主動),當然也有同時混合兩類功能的組件。
1、數據監控
行情監控組件,用於監控實時行情數據,每當API端有新的行情數據推送時立即進行更新。
2、功能調用
賬號登錄組件,用於調用中層引擎的登錄功能,傳入用戶名、密碼、服務器地址等參數。
3、 混合(交易下單)
交易下單組件,左側部分用於填入下單參數后調用中層引擎的下單功能發單,右側部分用於監控用戶輸入的合約代碼的實時行情(有行情推送時立即更新)。
三、數據監控組件
數據監控組件主要用於對交易平台中的各項數據實現實時更新或者手動更新的顯示監控,最常用的包括行情、報單、成交、持倉和日志等。監控組件中最常見的類型是表格,對應PyQt中的QTableWidget組件,表格中的單元格則使用QTableWidgetItem組件。對於某些特殊的數據監控也可以使用其他類型的組件,如上面交易下單組件中右側的部分,主要是使用標簽組件QLabel構成的。
針對不同的監控內容需要實現不同的數據更新方法,例如日志、成交類數據應該使用插入更新(即每條新的數據都應該插入新的一行),行情數據應該使用固定位置更新(即在表格中固定的單元格位置更新數據),以及主要針對持倉和報單數據的混合更新(即已經存在的數據直接在對應的位置更新,否則插入新的一行)。
下面以最基礎的日志監控為例介紹監控組件的實現原理,其他更為復雜的監控組件建議用戶直接閱讀vn.demo中demoMain.py的源代碼,大部分代碼作者都做了詳盡的注釋。
四、日志監控
日志監控組件主要用於輸出程序運行過程中有關當前程序運行狀態的信息。
整個實現代碼如下:
######################################################################## class LogMonitor(QtGui.QTableWidget): """用於顯示日志""" signal = QtCore.pyqtSignal(type(Event())) #---------------------------------------------------------------------- def __init__(self, eventEngine, parent=None): """Constructor""" super(LogMonitor, self).__init__(parent) self.__eventEngine = eventEngine self.initUi() self.registerEvent() #---------------------------------------------------------------------- def initUi(self): """初始化界面""" self.setWindowTitle(u'日志') self.setColumnCount(2) self.setHorizontalHeaderLabels([u'時間', u'日志']) self.verticalHeader().setVisible(False) # 關閉左邊的垂直表頭 self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 設為不可編輯狀態 # 自動調整列寬 self.horizontalHeader().setResizeMode(0, QtGui.QHeaderView.ResizeToContents) self.horizontalHeader().setResizeMode(1, QtGui.QHeaderView.Stretch) #---------------------------------------------------------------------- def registerEvent(self): """注冊事件監聽""" # Qt圖形組件的GUI更新必須使用Signal/Slot機制,否則有可能導致程序崩潰 # 因此這里先將圖形更新函數作為Slot,和信號連接起來 # 然后將信號的觸發函數注冊到事件驅動引擎中 self.signal.connect(self.updateLog) self.__eventEngine.register(EVENT_LOG, self.signal.emit) #---------------------------------------------------------------------- def updateLog(self, event): """更新日志""" # 獲取當前時間和日志內容 t = time.strftime('%H:%M:%S',time.localtime(time.time())) log = event.dict_['log'] # 在表格最上方插入一行 self.insertRow(0) # 創建單元格 cellTime = QtGui.QTableWidgetItem(t) cellLog = QtGui.QTableWidgetItem(log) # 將單元格插入表格 self.setItem(0, 0, cellTime) self.setItem(0, 1, cellLog)
接下來逐段講解:
1、對象初始化(init)
1 #---------------------------------------------------------------------- 2 def __init__(self, eventEngine, parent=None): 3 """Constructor""" 4 super(LogMonitor, self).__init__(parent) 5 self.__eventEngine = eventEngine 6 7 self.initUi() 8 self.registerEvent()
-
創建對象時,我們需要傳入程序中的事件驅動引擎對象eventEngine,以及該圖形組件所依附的母組件對象parent(一般可以留空)
-
把eventEngine對象的引用保存到__eventEngine上后,我們調用initUi方法初始化圖形組件的界面,以及registerEvent方法來向事件引擎中注冊該圖形組件的事件監聽函數。
2、初始化界面(initUi)
1 #---------------------------------------------------------------------- 2 def initUi(self): 3 """初始化界面""" 4 self.setWindowTitle(u'日志') 5 6 self.setColumnCount(2) 7 self.setHorizontalHeaderLabels([u'時間', u'日志']) 8 9 self.verticalHeader().setVisible(False) # 關閉左邊的垂直表頭 10 self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 設為不可編輯狀態 11 12 # 自動調整列寬 13 self.horizontalHeader().setResizeMode(0, QtGui.QHeaderView.ResizeToContents) 14 self.horizontalHeader().setResizeMode(1, QtGui.QHeaderView.Stretch)
-
首先設置該圖形組件左上方的標題欄內容為“日志”(日志監控組件)
-
我們希望顯示日志時,每行顯示該條日志的生成時間和具體的日志內容
-
由於QTableWidget本身比較類似於Excel表格,左側有垂直標題欄(默認用於顯示每行行號的表頭)且可以編輯,我們需要關閉這兩個功能
-
另外我們希望顯示日志生成時間的列的列寬可以調整為最小(只要能看見完整的時間就行),而把顯示日志內容的列設為拉升(即窗口有多寬都完全覆蓋)
3、注冊事件監聽(registerEvent)
這里需要稍微深入一下vn.py框架中的多線程工作機制:
-
整個框架在Python環境中主要包含兩個線程:主線程(運行Qt循環)和事件處理線程(運行EventEngine中的工作循環)
-
針對用戶是否需要使用GUI界面,主線程中運行的Qt循環可以選擇QApplication(帶GUI)或者QCoreApplication(純cmd)
-
Qt循環主要負責處理所有GUI相關的操作(控件繪制、信號處理等等),用戶不能在其他線程中直接改變GUI界面上的任何內容,否則可能會直接導致程序崩潰
-
當用戶希望在其他線程中對GUI進行操作時,必須依賴Qt提供的signal/slot機制,Qt循環的底層也運行着一個類似於EventEngine的事件處理機制,其他線程發出signal后會首先記錄到一個隊列中,然后由Qt對隊列中的signal任務進行循環處理(具體請參考Qt相關的資料)
-
事件處理線程的工作原理在之前的教程中已經專門介紹過了,這里不再重復,用戶只需記住所有Qt GUI組件的事件處理函數,都必須使用一個signal和該函數相連,並且在向事件引擎中注冊函數監聽時,將該signal的emit方法代替原本的事件處理函數進行注冊
######################################################################## class LogMonitor(QtGui.QTableWidget): """用於顯示日志""" signal = QtCore.pyqtSignal(type(Event()))
6、signal的創建需要放在類的構造中,而不能放在類的初始化函數里(Qt會直接報錯)
7、由於事件驅動引擎在調用監聽函數時會傳入事件對象本身作為參數,因此在創建signal時需要允許傳入一個類型為Event的參數
#---------------------------------------------------------------------- def registerEvent(self): """注冊事件監聽""" # Qt圖形組件的GUI更新必須使用Signal/Slot機制,否則有可能導致程序崩潰 # 因此這里先將圖形更新函數作為Slot,和信號連接起來 # 然后將信號的觸發函數注冊到事件驅動引擎中 self.signal.connect(self.updateLog) self.__eventEngine.register(EVENT_LOG, self.signal.emit)
8、首先我們把signal和事件處理函數updateLog連接.
9、然后將signal的emit方法注冊到事件驅動引擎中,監聽EVENT_LOG類型的事件
4、更新日志記錄(updateLog)
#---------------------------------------------------------------------- def updateLog(self, event): """更新日志""" # 獲取當前時間和日志內容 t = time.strftime('%H:%M:%S',time.localtime(time.time())) log = event.dict_['log'] # 在表格最上方插入一行 self.insertRow(0) # 創建單元格 cellTime = QtGui.QTableWidgetItem(t) cellLog = QtGui.QTableWidgetItem(log) # 將單元格插入表格 self.setItem(0, 0, cellTime) self.setItem(0, 1, cellLog)
- 感覺這段代碼的注釋已經足夠清楚,就不多廢話了
五、總結
盡管Qt庫本身使用C++開發,相比之下在Python中使用PyQt構建GUI程序更為快捷、簡便。基於Python動態語言的特性,在很多不是特別追求性能(GUI更新速度)的地方可以大幅減少用戶的代碼編寫量,並且降低出錯率。
請記住vn.py從開始就是一款專門為交易員設計的通用型交易平台開發框架(而不止是全自動的程序化交易),在金融市場上真正能幫助交易員賺錢的絕對不是多么復雜的程序算法,而是能夠完美實現交易員的交易策略並且越簡單越好的工具。