From e25787536aa3751ec88c540b641e2dba976e5799 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Sun, 6 Jan 2019 20:16:29 +0800 Subject: [PATCH] [Add] Add trader ui widget --- vnpy/trader/ui/__init__.py | 21 ++ vnpy/trader/ui/widget.py | 585 +++++++++++++++++++++++++++++++++++++ 2 files changed, 606 insertions(+) create mode 100644 vnpy/trader/ui/__init__.py create mode 100644 vnpy/trader/ui/widget.py diff --git a/vnpy/trader/ui/__init__.py b/vnpy/trader/ui/__init__.py new file mode 100644 index 00000000..709d9260 --- /dev/null +++ b/vnpy/trader/ui/__init__.py @@ -0,0 +1,21 @@ +import platform +import ctypes + +import qdarkstyle +from PyQt5 import QtWidgets + +from .mainwindow import MainWindow + + +def create_qapp(): + """ + Create Qt Application. + """ + qapp = QtWidgets.QApplication([]) + qapp.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) + + if 'Windows' in platform.uname(): + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + 'VN Trader') + + return qapp \ No newline at end of file diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py new file mode 100644 index 00000000..b692a038 --- /dev/null +++ b/vnpy/trader/ui/widget.py @@ -0,0 +1,585 @@ +""" +Basic widgets for VN Trader. +""" + +import csv +from typing import Any + +from PyQt5 import QtWidgets, QtGui, QtCore + +from vnpy.event import EventEngine, Event +from ..constant import DIRECTION_LONG, DIRECTION_SHORT, DIRECTION_NET +from ..engine import MainEngine +from ..event import (EVENT_TICK, EVENT_ORDER, EVENT_TRADE, EVENT_ACCOUNT, + EVENT_POSITION, EVENT_CONTRACT, EVENT_LOG) + +COLOR_LONG = QtGui.QColor("red") +COLOR_SHORT = QtGui.QColor("green") +COLOR_BID = QtGui.QColor(255, 174, 201) +COLOR_ASK = QtGui.QColor(160, 255, 160) +COLOR_BLACK = QtGui.QColor("black") + + +class BaseCell(QtWidgets.QTableWidgetItem): + """ + General cell used in tablewidgets. + """ + + def __init__(self, content: Any, data: Any): + """""" + super(BaseCel, self).__init__() + self.setTextAlignment(QtCore.Qt.AlignCenter) + self.set_content(content, data) + + def set_content(self, content: Any, data: Any): + """ + Set text content. + """ + self.setText(str(content)) + self._data = data + + def get_data(self): + """ + Get data object. + """ + return self._data + + +class DirectionCell(BaseCell): + """ + Cell used for showing direction data. + """ + + def __init__(self, content: str, data: Any): + """""" + super(DirectionCell, self).__init__(content, data) + + def set_content(self, content: Any, data: Any): + """ + Cell color is set according to direction. + """ + super(DirectionCell, self).set_content(content, data) + + if content == DIRECTION_SHORT: + self.setForeground(COLOR_SHORT) + else: + self.setForeground(COLOR_LONG) + + +class BidCell(BaseCell): + """ + Cell used for showing bid price and volume. + """ + + def __init__(self, content: Any, data: Any): + """""" + super(BidCell, self).__init__(content, data) + + self.setForeground(COLOR_BLACK) + self.setForeground(COLOR_BID) + + +class AskCell(BaseCell): + """ + Cell used for showing ask price and volume. + """ + + def __init__(self, content: Any, data: Any): + """""" + super(AskCell, self).__init__(content, data) + + self.setForeground(COLOR_BLACK) + self.setForeground(COLOR_ASK) + + +class PnlCell(BaseCell): + """ + Cell used for showing pnl data. + """ + + def __init__(self, content: Any, data: Any): + """""" + super(PnlCell, self).__init__(content, data) + + def set_content(self, content: Any, data: Any): + """ + Cell color is set based on whether pnl is + positive or negative. + """ + super(PnlCell, self).set_content(content, data) + + if content.startswith("-"): + self.setForeground(COLOR_SHORT) + else: + self.setForeground(COLOR_LONG) + + +class TimeCell(BaseCell): + """ + Cell used for showing time string from datetime object. + """ + + def __init__(self, content: Any, data: Any): + """""" + super(TimeCell, self).__init__(content, data) + + def set_content(self, content: Any, data: Any): + """ + Time format is 12:12:12.5 + """ + timestamp = content.strftime("%H:%M:%S") + + millisecond = int(content.microsecond / 1000) + if millisecond: + timestamp = f"{timestamp}.{millisecond}" + + self.setText(timestamp) + self._data = data + + +class BaseMonitor(QtWidgets.QTableWidget): + """ + Monitor data update in VN Trader. + """ + + event_type = "" + data_key = "" + sorting = False + headers = {} + + signal = QtCore.pyqtSignal(Event) + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super(BaseMonitor, self).__init__() + + self.main_engine = main_engine + self.event_engine = event_engine + self.cells = {} + + self.init_ui() + self.register_event() + + def init_ui(self): + """""" + self.init_table() + self.init_menu() + + def init_table(self): + """ + Initialize table. + """ + self.setColumnCount(len(self.headers)) + + labels = [d['display'] for d in self.headers.values()] + self.setHorizontalHeaderLabels(labels) + + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + self.setAlternatingRowColors(True) + self.setSortingEnabled(self.sorting) + + def init_menu(self): + """ + Create right click menu. + """ + self.menu = QtWidgets.QMenu(self) + + resize_action = QtWidgets.QAction("调整列宽", self) + resize_action.triggered.connect(self.resize_columns) + self.menu.addAction(resize_action) + + save_action = QtWidgets.QAction("保存数据", self) + save_action.triggered.connect(self.save_csv) + self.menu.addAction(save_action) + + def register_event(self): + """ + Register event handler into event engine. + """ + self.signal.connect(self.process_event) + self.event_engine.register(self.event_type, self.process_event) + + def process_event(self, event): + """ + Process new data from event and update into table. + """ + # Disable sorting to prevent unwanted error. + if self.sorting: + self.setSortingEnabled(False) + + # Update data into table. + data = event.data + + if not self.data_key: + self.insert_new_row(data) + else: + key = data.__getattribute__(self.data_key) + + if key in self.cells: + self.update_old_row(data) + else: + self.insert_new_row(data) + + # Enable sorting + if self.sorting: + self.setSortingEnabled(True) + + def insert_new_row(self, data): + """ + Insert a new row at the top of table. + """ + self.insertRow(0) + + row_cells = {} + for column, header in enumerate(self.headers.keys()): + setting = self.headers[header] + + content = data.__getattribute__(header) + cell = setting['cell'](content, data) + self.setItem(0, column, cell) + + if setting['update']: + row_cells[header] = cell + + if self.data_key: + key = data.__getattribute__(self.data_key) + self.cells[key] = row_cells + + def update_old_row(self, data): + """ + Update an old row in table. + """ + key = data.__getattribute__(self.data_key) + row_cells = self.cells[key] + + for header, cell in row_cells: + content = data.__getattribute__(header) + cell.set_content(content, data) + + def resize_columns(self): + """ + Resize all columns according to contents. + """ + self.horizontalHeader().resizeSections( + QtWidgets.QHeaderView.ResizeToContents) + + def save_csv(self): + """ + Save table data into a csv file + """ + path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "保存数据", "", + "CSV(*.csv)") + + if not path: + return + + with open(path, "wb") as f: + writer = csv.writer(f) + writer.writerow(self.headers.keys()) + + for row in range(self.rowCount()): + row_data = [] + for column in range(self.columnCount()): + item = self.item(row, column) + if item: + row_data.append(text_type(item.text())) + else: + row_data.append("") + writer.writerow(row_data) + + +class TickMonitor(BaseMonitor): + """ + Monitor for tick data. + """ + event_type = EVENT_TICK + data_key = "vt_symbol" + sorting = True + + headers = { + 'symbol': { + 'display': '代码', + 'cell': BaseCell, + 'update': False + }, + 'last_price': { + 'display': '最新价', + 'cell': BaseCell, + 'update': True + }, + 'volume': { + 'display': '成交量', + 'cell': BaseCell, + 'update': True + }, + 'open_price': { + 'display': '开盘价', + 'cell': BaseCell, + 'update': True + }, + 'high_price': { + 'display': '最高价', + 'cell': BaseCell, + 'update': True + }, + 'low_price': { + 'display': '最低价', + 'cell': BaseCell, + 'update': True + }, + 'bid_price_1': { + 'display': '买1价', + 'cell': BidCell, + 'update': True + }, + 'bid_volume_1': { + 'display': '买1量', + 'cell': BidCell, + 'update': True + }, + 'ask_price_1': { + 'display': '卖1价', + 'cell': AskCell, + 'update': True + }, + 'ask_volume_1': { + 'display': '卖1量', + 'cell': AskCell, + 'update': True + }, + 'time': { + 'display': '时间', + 'cell': TimeCell, + 'update': True + }, + 'gateway_name': { + 'display': '接口', + 'cell': BaseCell, + 'update': False + } + } + + +class LogMonitor(BaseMonitor): + """ + Monitor for log data. + """ + event_type = EVENT_LOG + data_key = "" + sorting = False + + headers = { + 'time': { + 'display': '时间', + 'cell': BaseCell, + 'update': False + }, + 'msg': { + 'display': '信息', + 'cell': BaseCell, + 'update': False + }, + 'gateway_name': { + 'display': '接口', + 'cell': BaseCell, + 'update': False + } + } + + def process_event(self, event): + """Resize row heights for diplaying long log message.""" + super(LogMonitor, self).process_event(event) + self.resizeRowToContents(0) + + +class TradeMonitor(BaseMonitor): + """ + Monitor for trade data. + """ + event_type = EVENT_TRADE + data_key = "" + sorting = True + + headers = { + 'tradeid': { + 'display': "成交号 ", + 'cell': BaseCell, + 'update': False + }, + 'orderid': { + 'display': '委托号', + 'cell': BaseCell, + 'update': False + }, + 'symbol': { + 'display': '代码', + 'cell': BaseCell, + 'update': False + }, + 'direction': { + 'display': '方向', + 'cell': DirectionCell, + 'update': False + }, + 'offset': { + 'display': '开平', + 'cell': BaseCell, + 'update': False + }, + 'price': { + 'display': '价格', + 'cell': BaseCell, + 'update': False + }, + 'volume': { + 'display': '数量', + 'cell': BaseCell, + 'update': False + }, + 'gateway_name': { + 'display': '接口', + 'cell': BaseCell, + 'update': False + } + } + + +class OrderMonitor(BaseMonitor): + """ + Monitor for order data. + """ + event_type = EVENT_ORDER + data_key = "vt_orderid" + sorting = True + + headers = { + 'orderid': { + 'display': '委托号', + 'cell': BaseCell, + 'update': False + }, + 'symbol': { + 'display': '代码', + 'cell': BaseCell, + 'update': False + }, + 'direction': { + 'display': '方向', + 'cell': DirectionCell, + 'update': False + }, + 'offset': { + 'display': '开平', + 'cell': BaseCell, + 'update': False + }, + 'price': { + 'display': '价格', + 'cell': BaseCell, + 'update': False + }, + 'volume': { + 'display': '总数量', + 'cell': BaseCell, + 'update': True + }, + 'traded': { + 'display': '已成交', + 'cell': BaseCell, + 'update': True + }, + 'status': { + 'display': '状态', + 'cell': BaseCell, + 'update': True + }, + 'time': { + 'display': '时间', + 'cell': BaseCell, + 'update': True + }, + 'gateway_name': { + 'display': '接口', + 'cell': BaseCell, + 'update': False + } + } + +class PositionMonitor(BaseMonitor): + """ + Monitor for position data. + """ + event_type = EVENT_POSITION + data_key = "vt_positionid" + sorting = True + + headers = { + 'symbol': { + 'display': '代码', + 'cell': BaseCell, + 'update': False + }, + 'direction': { + 'display': '方向', + 'cell': DirectionCell, + 'update': False + }, + 'volume': { + 'display': '数量', + 'cell': BaseCell, + 'update': True + }, + 'frozen': { + 'display': '冻结', + 'cell': BaseCell, + 'update': True + }, + 'price': { + 'display': '均价', + 'cell': BaseCell, + 'update': False + }, + 'pnl': { + 'display': '盈亏', + 'cell': PnlCell, + 'update': True + }, + 'gateway_name': { + 'display': '接口', + 'cell': BaseCell, + 'update': False + } + } + + +class AccountMonitor(BaseMonitor): + """ + Monitor for account data. + """ + event_type = EVENT_ACCOUNT + data_key = "vt_accountid" + sorting = True + + headers = { + 'accountid': { + 'display': '账号', + 'cell': BaseCell, + 'update': False + }, + 'balance': { + 'display': '余额', + 'cell': BaseCell, + 'update': True + }, + 'frozen': { + 'display': '冻结', + 'cell': BaseCell, + 'update': True + }, + 'available': { + 'display': '可用', + 'cell': BaseCell, + 'update': True + } + }