You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
502 lines
14 KiB
502 lines
14 KiB
"""
|
|
Basic widgets for SQ main window.
|
|
"""
|
|
|
|
import csv
|
|
from typing import Any
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
import pyqtgraph as pg
|
|
|
|
from ..common.datastruct import Event
|
|
from ..engine.iengine import EventEngine
|
|
from ..common.constant import Direction
|
|
|
|
from ..common.constant import EventType, OT2STR
|
|
|
|
|
|
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 VerticalTabBar(QtWidgets.QTabBar):
|
|
# def tabSizeHint(self,index):
|
|
# s = QtWidgets.QTabBar.tabSizeHint(self,index)
|
|
# s.transpose()
|
|
# return s
|
|
|
|
def paintEvent(self, event):
|
|
painter = QtWidgets.QStylePainter(self)
|
|
opt = QtWidgets.QStyleOptionTab()
|
|
for i in range(self.count()):
|
|
self.initStyleOption(opt, i)
|
|
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, opt)
|
|
painter.save()
|
|
|
|
s = opt.rect.size()
|
|
# s.transpose()
|
|
r = QtCore.QRect(QtCore.QPoint(), s)
|
|
r.moveCenter(opt.rect.center())
|
|
opt.rect = r
|
|
|
|
c = self.tabRect(i).center()
|
|
painter.translate(c)
|
|
painter.rotate(180)
|
|
painter.translate(-c)
|
|
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, opt)
|
|
painter.restore()
|
|
|
|
|
|
class QFloatTableWidgetItem (QtWidgets.QTableWidgetItem):
|
|
def __init__(self, value):
|
|
super().__init__(value)
|
|
|
|
def __lt__(self, other):
|
|
if (isinstance(other, QFloatTableWidgetItem)):
|
|
selfDataValue = float(self.text())
|
|
otherDataValue = float(other.text())
|
|
return selfDataValue < otherDataValue
|
|
else:
|
|
return QtWidgets.QTableWidgetItem.__lt__(self, other)
|
|
|
|
|
|
class BaseCell(QtWidgets.QTableWidgetItem):
|
|
"""
|
|
General cell used in tablewidgets.
|
|
"""
|
|
|
|
def __init__(self, content: Any, data: Any):
|
|
""""""
|
|
super().__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 EnumCell(BaseCell):
|
|
"""
|
|
Cell used for showing enum data.
|
|
"""
|
|
|
|
def __init__(self, content: str, data: Any):
|
|
""""""
|
|
super().__init__(content, data)
|
|
|
|
def set_content(self, content: Any, data: Any):
|
|
"""
|
|
Set text using enum.constant.value.
|
|
"""
|
|
if content:
|
|
super().set_content(content.value, data)
|
|
|
|
|
|
class OTCell(BaseCell):
|
|
"""
|
|
Cell used for showing ordertype data.
|
|
"""
|
|
|
|
def __init__(self, content: str, data: Any):
|
|
""""""
|
|
super().__init__(content, data)
|
|
|
|
def set_content(self, content: Any, data: Any):
|
|
"""
|
|
Set text using ot2str.
|
|
"""
|
|
if content:
|
|
text = OT2STR.get(content, '未知')
|
|
self.setText(text)
|
|
self._data = data
|
|
|
|
|
|
class DirectionCell(EnumCell):
|
|
"""
|
|
Cell used for showing direction data.
|
|
"""
|
|
|
|
def __init__(self, content: str, data: Any):
|
|
""""""
|
|
super().__init__(content, data)
|
|
|
|
def set_content(self, content: Any, data: Any):
|
|
"""
|
|
Cell color is set according to direction.
|
|
"""
|
|
super().set_content(content, data)
|
|
|
|
if content is 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().__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().__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().__init__(content, data)
|
|
|
|
def set_content(self, content: Any, data: Any):
|
|
"""
|
|
Cell color is set based on whether pnl is
|
|
positive or negative.
|
|
"""
|
|
super().set_content(content, data)
|
|
|
|
if str(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().__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 MsgCell(BaseCell):
|
|
"""
|
|
Cell used for showing msg data.
|
|
"""
|
|
|
|
def __init__(self, content: str, data: Any):
|
|
""""""
|
|
super().__init__(content, data)
|
|
self.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
|
|
|
|
|
|
class BaseMonitor(QtWidgets.QTableWidget):
|
|
"""
|
|
Monitor data update in VN Trader.
|
|
"""
|
|
|
|
event_type: EventType = EventType.HEADER
|
|
data_key = ""
|
|
sorting = False
|
|
headers = {}
|
|
|
|
signal = QtCore.pyqtSignal(Event)
|
|
|
|
def __init__(self, event_engine: EventEngine):
|
|
""""""
|
|
super().__init__()
|
|
|
|
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)
|
|
|
|
del_action = QtWidgets.QAction("删除数据", self)
|
|
del_action.triggered.connect(self.deleterows)
|
|
self.menu.addAction(del_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.signal.emit)
|
|
|
|
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.items():
|
|
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, "w") as f:
|
|
writer = csv.writer(f, lineterminator="\n")
|
|
|
|
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(str(item.text()))
|
|
else:
|
|
row_data.append("")
|
|
writer.writerow(row_data)
|
|
|
|
def deleterows(self):
|
|
rr = QtWidgets.QMessageBox.warning(self, "注意", "删除无法恢复!",
|
|
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
|
|
QtWidgets.QMessageBox.No)
|
|
if rr == QtWidgets.QMessageBox.Yes:
|
|
curow = self.currentRow()
|
|
selections = self.selectionModel()
|
|
selectedsList = selections.selectedRows()
|
|
rows = []
|
|
for r in selectedsList:
|
|
rows.append(r.row())
|
|
if len(rows) == 0 and curow >= 0:
|
|
rows.append(curow)
|
|
rows.reverse()
|
|
for i in rows:
|
|
cell = self.item(i, 0)
|
|
data = cell.get_data()
|
|
key = data.__getattribute__(self.data_key)
|
|
self.removeRow(i)
|
|
del self.cells[key]
|
|
|
|
def contextMenuEvent(self, event):
|
|
"""
|
|
Show menu with right click.
|
|
"""
|
|
self.menu.popup(QtGui.QCursor.pos())
|
|
|
|
|
|
class CandlestickItem(pg.GraphicsObject):
|
|
w = 0.35
|
|
bull_pen = pg.mkPen('r')
|
|
bear_pen = pg.mkPen('g')
|
|
bull_brush = pg.mkBrush('r') # pg.mkBrush('#00cc00')
|
|
bear_brush = pg.mkBrush('g') # pg.mkBrush('#fa0000')
|
|
|
|
def __init__(self, data):
|
|
pg.GraphicsObject.__init__(self)
|
|
self.data = data
|
|
self.generatePicture()
|
|
|
|
def generatePicture(self):
|
|
self.picture = QtGui.QPicture()
|
|
p = QtGui.QPainter(self.picture)
|
|
for t, bar in enumerate(self.data):
|
|
# t = 60*(bar.datetime.hour - 9) + bar.datetime.minute
|
|
if bar.open_price < bar.close_price:
|
|
p.setPen(self.bull_pen)
|
|
p.setBrush(self.bull_brush)
|
|
else:
|
|
p.setPen(self.bear_pen)
|
|
p.setBrush(self.bear_brush)
|
|
p.drawLine(QtCore.QPointF(t, bar.low_price),
|
|
QtCore.QPointF(t, bar.high_price))
|
|
p.drawRect(QtCore.QRectF(t - self.w, bar.open_price,
|
|
self.w * 2, bar.close_price - bar.open_price))
|
|
p.end()
|
|
self.update()
|
|
|
|
def on_bar(self, bar):
|
|
# self.data.append(bar)
|
|
self.generatePicture()
|
|
# p = self.p
|
|
# t = 60*(bar.datetime.hour - 9) + bar.datetime.minute
|
|
# if bar.open_price < bar.close_price:
|
|
# p.setPen(self.bull_pen)
|
|
# p.setBrush(self.bull_brush)
|
|
# else:
|
|
# p.setPen(self.bear_pen)
|
|
# p.setBrush(self.bear_brush)
|
|
# p.drawLine(QtCore.QPointF(t, bar.low_price), QtCore.QPointF(t, bar.high_price))
|
|
# p.drawRect(QtCore.QRectF(t - self.w, bar.open_price, self.w * 2, bar.close_price - bar.open_price))
|
|
# p.end()
|
|
# self.update()
|
|
|
|
def paint(self, p, *args):
|
|
p.drawPicture(0, 0, self.picture)
|
|
|
|
def boundingRect(self):
|
|
return QtCore.QRectF(self.picture.boundingRect())
|
|
|
|
|
|
class VolumeItem(pg.GraphicsObject):
|
|
w = 0.35
|
|
bull_pen = pg.mkPen('r')
|
|
bear_pen = pg.mkPen('g')
|
|
bull_brush = pg.mkBrush('r') # pg.mkBrush('#00cc00')
|
|
bear_brush = pg.mkBrush('g') # pg.mkBrush('#fa0000')
|
|
|
|
def __init__(self, data):
|
|
pg.GraphicsObject.__init__(self)
|
|
self.data = data
|
|
self.generatePicture()
|
|
|
|
def generatePicture(self):
|
|
self.picture = QtGui.QPicture()
|
|
p = QtGui.QPainter(self.picture)
|
|
for t, bar in enumerate(self.data):
|
|
# t = 60*(bar.datetime.hour - 9) + bar.datetime.minute
|
|
sign = 1
|
|
if bar.open_price < bar.close_price:
|
|
p.setPen(self.bull_pen)
|
|
p.setBrush(self.bull_brush)
|
|
else:
|
|
p.setPen(self.bear_pen)
|
|
p.setBrush(self.bear_brush)
|
|
sign = -1
|
|
p.drawRect(QtCore.QRectF(t - self.w, 0,
|
|
self.w * 2, bar.volume))
|
|
p.end()
|
|
self.update()
|
|
|
|
def on_bar(self, bar):
|
|
# self.data.append(bar)
|
|
self.generatePicture()
|
|
|
|
def paint(self, p, *args):
|
|
p.drawPicture(0, 0, self.picture)
|
|
|
|
def boundingRect(self):
|
|
return QtCore.QRectF(self.picture.boundingRect())
|
|
|