Python量化交易平台开发教程系列8-顶层GUI界面开发(2)

来源:互联网 发布:unity3d玻璃材质包 编辑:程序博客网 时间:2024/04/25 15:34

原创文章,转载请注明出处:用Python的交易员

前言

接上一篇,主要分为两块:展示动态语言特性简化GUI开发的组件以及功能调用组件。

动态语言的方便之处

########################################################################class AccountMonitor(QtGui.QTableWidget):    """用于显示账户"""    signal = QtCore.pyqtSignal(type(Event()))    dictLabels = OrderedDict()    dictLabels['AccountID'] = u'投资者账户'    dictLabels['PreBalance'] = u'昨结'    dictLabels['Withdraw'] = u'出金'    dictLabels['Deposit'] = u'入金'        dictLabels['FrozenCash'] = u'冻结资金'    dictLabels['FrozenMargin'] = u'冻结保证金'    dictLabels['FrozenCommission'] = u'冻结手续费'    dictLabels['FrozenTransferFee'] = u'冻结过户费'    dictLabels['FrozenStampTax'] = u'冻结印花税'    dictLabels['Commission'] = u'手续费'    dictLabels['TransferFee'] = u'过户费'    dictLabels['StampTax'] = u'印花税'        dictLabels['CurrMargin'] = u'当前保证金'    dictLabels['Available'] = u'可用资金'    dictLabels['WithdrawQuota'] = u'可取资金'    #----------------------------------------------------------------------    def __init__(self, eventEngine, parent=None):        """Constructor"""        super(AccountMonitor, self).__init__(parent)        self.__eventEngine = eventEngine        self.dictAccount = {}       # 用来保存账户对应的单元格        self.initUi()        self.registerEvent()    #----------------------------------------------------------------------    def initUi(self):        """"""        self.setWindowTitle(u'账户')        self.setColumnCount(len(self.dictLabels))        self.setHorizontalHeaderLabels(self.dictLabels.values())        self.verticalHeader().setVisible(False)                 # 关闭左边的垂直表头        self.setEditTriggers(QtGui.QTableWidget.NoEditTriggers) # 设为不可编辑状态    #----------------------------------------------------------------------    def registerEvent(self):        """"""        self.signal.connect(self.updateAccount)        self.__eventEngine.register(EVENT_ACCOUNT, self.signal.emit)    #----------------------------------------------------------------------    def updateAccount(self, event):        """"""        data = event.dict_['data']        accountid = data['AccountID']        # 如果之前已经收到过这个账户的数据, 则直接更新        if accountid in self.dictAccount:            d = self.dictAccount[accountid]            for label, cell in d.items():                cell.setText(str(data[label]))        # 否则插入新的一行,并更新        else:            self.insertRow(0)            d = {}            for col, label in enumerate(self.dictLabels.keys()):                cell = QtGui.QTableWidgetItem(str(data[label]))                self.setItem(0, col, cell)                d[label] = cell            self.dictAccount[accountid] = d

写过C++里Qt代码的人可能会有比较直观的感受,上面这段代码利用Python动态语言的特性偷了不少懒。

该账户监控组件AccountMonitor用于显示LTS账户相关的数据(可用资金、手续费、保证金等等)。在vn.lts的API封装中,通过onRspQryTradingAccount推送回来的账户数据已经被从C++结构体自动转换为了Python字典。

########################################################################class AccountMonitor(QtGui.QTableWidget):    """用于显示账户"""    signal = QtCore.pyqtSignal(type(Event()))    dictLabels = OrderedDict()    dictLabels['AccountID'] = u'投资者账户'    dictLabels['PreBalance'] = u'昨结'    dictLabels['Withdraw'] = u'出金'    dictLabels['Deposit'] = u'入金'        dictLabels['FrozenCash'] = u'冻结资金'    dictLabels['FrozenMargin'] = u'冻结保证金'    dictLabels['FrozenCommission'] = u'冻结手续费'    dictLabels['FrozenTransferFee'] = u'冻结过户费'    dictLabels['FrozenStampTax'] = u'冻结印花税'    dictLabels['Commission'] = u'手续费'    dictLabels['TransferFee'] = u'过户费'    dictLabels['StampTax'] = u'印花税'        dictLabels['CurrMargin'] = u'当前保证金'    dictLabels['Available'] = u'可用资金'    dictLabels['WithdrawQuota'] = u'可取资金'

首先通过原生API的.h头文件查询该结构体包含的字段内容,并选择觉得有用、需要显示出来的字段,使用Python内置的排序字典类OrderedDict创建标签显示的配置dictLabels,按照我们希望显示的顺序(从左到右)逐条输入到该字典中。字典的键是该字段的英文名称,值是该字段对应的中文名称。

    #----------------------------------------------------------------------    def initUi(self):        """"""        self.setWindowTitle(u'账户')        self.setColumnCount(len(self.dictLabels))        self.setHorizontalHeaderLabels(self.dictLabels.values())

下一步在初始化表格时,表头的列数可以直接取dictLabels的长度,表头的标签内容可以直接取dictLabels的值列表。

    #----------------------------------------------------------------------    def updateAccount(self, event):        """"""        data = event.dict_['data']        accountid = data['AccountID']        # 如果之前已经收到过这个账户的数据, 则直接更新        if accountid in self.dictAccount:            d = self.dictAccount[accountid]            for label, cell in d.items():                cell.setText(str(data[label]))        # 否则插入新的一行,并更新        else:            self.insertRow(0)            d = {}            for col, label in enumerate(self.dictLabels.keys()):                cell = QtGui.QTableWidgetItem(str(data[label]))                self.setItem(0, col, cell)                d[label] = cell            self.dictAccount[accountid] = d

在更新账户数据时,由于dictLabels中设置的需要显示的字段和收到的账户数据字典data中的键是一一对应的,我们可以直接对dictLabels中的键进行遍历读取data字典中对应的值,并更新到表格中。这段感觉比较难以用语言描述出来,直接读懂上面的代码可能更能明白其中的意思。

可以来比较下updateAccount函数的类C++的写法,也是作者在刚开始做GUI开发时使用的方法,现在回看起来真是繁琐。

    #----------------------------------------------------------------------    def updateAccount(self, event):        """"""        data = event.dict_['data']        accountid = data['AccountID']        # 如果之前已经收到过这个账户的数据, 则直接更新        if accountid in self.dictAccount:            d = self.dictAccount[accountid]            d['AccountID'].setText(str(data['AccountID']))            d['PreBalance'].setText(str(data['PreBalance']))            d['Deposit'].setText(str(data['Deposit']))            d['FrozenCash'].setText(str(data['FrozenCash']))            d['FrozenMargin'].setText(str(data['FrozenMargin']))            d['FrozenCommission'].setText(str(data['FrozenCommission']))            d['FrozenTransferFee'].setText(str(data['FrozenTransferFee']))            d['FrozenStampTax'].setText(str(data['FrozenStampTax']))            d['Commission'].setText(str(data['Commission']))            d['TransferFee'].setText(str(data['TransferFee']))            d['StampTax'].setText(str(data['StampTax']))            d['CurrMargin'].setText(str(data['CurrMargin']))            d['Available'].setText(str(data['Available']))            d['WithdrawQuota'].setText(str(data['WithdrawQuota']))        # 否则插入新的一行,并更新        else:            self.insertRow(0)            d = {}            cellAccountID = QtGui.QTableWidgetItem(str(data['AccountID']))            self.setItem(0, 0, cellAccountID)            d['AccountID'] = cell            cellPreBalance = QtGui.QTableWidgetItem(str(data['PreBalance']))            self.setItem(0, 0, cellPreBalance)            d['PreBalance'] = cell            ...            (后面的字段以此类推,这里请原谅作者实在不想写下去了,习惯遍历后复制粘贴都觉得麻烦)            self.dictAccount[accountid] = d

和上面的类C++写法相比,之前Python写法的优势就很明显了:

  • 代码行数显著减少(绝大部分都是重复内容),降低了coding时出错的概率,省下很多debug的时间

  • dictLabels可以直接作为配置来使用(类似于Java中使用很多配置文件),当需要增加或者移除某个字段时,用户只需更改dictLabels中的内容而无需改变initUi和updateAccount(这才是浪费无数debug时间的地方),同时修改的配置和本身的程序代码一样都是Python源代码文件(不像Java使用XML等其他文件进行配置)

当然也存在一个缺点:理论上list遍历的使用降低了性能(GUI的更新耗时更长),但是作者以一个实际用户的经验可以告诉大家,这个性能上的牺牲微乎其微,考虑到vn.py的应用场景,这个缺点几乎可以忽略。

(限于作者本身的C++水平,以上这段编程语言的比较可能有不正确之处,欢迎读者指出。)

功能调用组件

########################################################################class LoginWidget(QtGui.QDialog):    """登录"""    #----------------------------------------------------------------------    def __init__(self, mainEngine, parent=None):        """Constructor"""        super(LoginWidget, self).__init__()        self.__mainEngine = mainEngine        self.initUi()        self.loadData()    #----------------------------------------------------------------------    def initUi(self):        """初始化界面"""        self.setWindowTitle(u'登录')        # 设置组件        labelUserID = QtGui.QLabel(u'账号:')        labelMdPassword = QtGui.QLabel(u'行情密码:')        labelTdPassword = QtGui.QLabel(u'交易密码:')        labelMdAddress = QtGui.QLabel(u'行情服务器:')        labelTdAddress = QtGui.QLabel(u'交易服务器:')        self.editUserID = QtGui.QLineEdit()        self.editMdPassword = QtGui.QLineEdit()        self.editTdPassword = QtGui.QLineEdit()        self.editMdAddress = QtGui.QLineEdit()        self.editTdAddress = QtGui.QLineEdit()        self.editUserID.setMinimumWidth(200)        buttonLogin = QtGui.QPushButton(u'登录')        buttonCancel = QtGui.QPushButton(u'取消')        buttonLogin.clicked.connect(self.login)        buttonCancel.clicked.connect(self.close)        # 设置布局        buttonHBox = QtGui.QHBoxLayout()        buttonHBox.addStretch()        buttonHBox.addWidget(buttonLogin)        buttonHBox.addWidget(buttonCancel)        grid = QtGui.QGridLayout()        grid.addWidget(labelUserID, 0, 0)        grid.addWidget(labelMdPassword, 1, 0)        grid.addWidget(labelTdPassword, 2, 0)        grid.addWidget(labelMdAddress, 3, 0)        grid.addWidget(labelTdAddress, 4, 0)        grid.addWidget(self.editUserID, 0, 1)        grid.addWidget(self.editMdPassword, 1, 1)        grid.addWidget(self.editTdPassword, 2, 1)        grid.addWidget(self.editMdAddress, 3, 1)        grid.addWidget(self.editTdAddress, 4, 1)        grid.addLayout(buttonHBox, 5, 0, 1, 2)        self.setLayout(grid)    #----------------------------------------------------------------------    def login(self):        """登录"""        userid = str(self.editUserID.text())        mdPassword = str(self.editMdPassword.text())        tdPassword = str(self.editTdPassword.text())        mdAddress = str(self.editMdAddress.text())        tdAddress = str(self.editTdAddress.text())        brokerid = '2011'    # 这里因为LTS的BrokerID固定为2011        self.__mainEngine.login(userid, mdPassword, tdPassword, brokerid, mdAddress, tdAddress)        self.close()    #----------------------------------------------------------------------    def loadData(self):        """读取数据"""        f = shelve.open('setting.vn')        try:            setting = f['login']            userid = setting['userid']            mdPassword = setting['mdPassword']            tdPassword = setting['tdPassword']            mdAddress = setting['mdAddress']            tdAddress = setting['tdAddress']            self.editUserID.setText(userid)            self.editMdPassword.setText(mdPassword)            self.editTdPassword.setText(tdPassword)            self.editMdAddress.setText(mdAddress)            self.editTdAddress.setText(tdAddress)        except KeyError:            pass        f.close()    #----------------------------------------------------------------------    def saveData(self):        """保存数据"""        setting = {}        setting['userid'] = str(self.editUserID.text())        setting['mdPassword'] = str(self.editMdPassword.text())        setting['tdPassword'] = str(self.editTdPassword.text())        setting['mdAddress'] = str(self.editMdAddress.text())        setting['tdAddress'] = str(self.editTdAddress.text())        f = shelve.open('setting.vn')        f['login'] = setting        f.close()    #----------------------------------------------------------------------    def closeEvent(self, event):        """关闭事件处理"""        # 当窗口被关闭时,先保存登录数据,再关闭        self.saveData()        event.accept()
  1. 主动调用组件通常在工作原理上较为简单,用户只需在界面上放置所需的组件(按钮、下拉框等),并将组件的信号和中层引擎暴露的功能函数连接上(参考上面的initUi和login方法)

  2. 对于一些我们希望保存下来省去每次输入麻烦的信息,这里推荐Python的shelve库,可以直接将Python中的数据对象以二进制的方式保存在文件中(文件后缀名可以自行定义)

  3. 在登录组件第一次创建时,我们从硬盘上的setting.vn文件中尝试读取登录账号等设置内容,同时每次在关闭该窗口时closeEvent方法会被自动触发,保存当前文本框中的设置内容到文件中

总结

系列7和8两篇教程中已经基本覆盖了GUI界面开发的核心原理,剩下的更多细节以及开发中的一些奇技淫巧就留给读者自己去研究vn.py项目的源代码了,毕竟对于这套教程的目标读者,有能力去开发适合自己交易策略的量化平台才是最终目标:now it's time to get your hands dirty。

0 0
原创粉丝点击