From 2811ab1068d9e6184b1a366f7e21e7a37e7d8249 Mon Sep 17 00:00:00 2001 From: msincenselee Date: Wed, 26 Feb 2020 21:25:32 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=A2=9E=E5=BC=BA]=201.9x=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=E7=9A=84ui=5Fkline=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A?= =?UTF-8?q?=E5=9B=BE/=E5=A4=9A=E6=8C=87=E6=A0=87/=E9=99=84=E5=9B=BE?= =?UTF-8?q?=E6=8C=87=E6=A0=87/=E4=BA=A4=E6=98=93=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=AD=89=E5=9B=9E=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vnpy/trader/ui/kline/crosshair.py | 287 +++++ vnpy/trader/ui/kline/css.qss | 1342 +++++++++++++++++++++++ vnpy/trader/ui/kline/cta.ico | Bin 0 -> 186094 bytes vnpy/trader/ui/kline/kline.py | 1413 +++++++++++++++++++++++++ vnpy/trader/ui/kline/kline_widgets.py | 269 +++++ 5 files changed, 3311 insertions(+) create mode 100644 vnpy/trader/ui/kline/crosshair.py create mode 100644 vnpy/trader/ui/kline/css.qss create mode 100644 vnpy/trader/ui/kline/cta.ico create mode 100644 vnpy/trader/ui/kline/kline.py create mode 100644 vnpy/trader/ui/kline/kline_widgets.py diff --git a/vnpy/trader/ui/kline/crosshair.py b/vnpy/trader/ui/kline/crosshair.py new file mode 100644 index 00000000..a1aceec1 --- /dev/null +++ b/vnpy/trader/ui/kline/crosshair.py @@ -0,0 +1,287 @@ +# flake8: noqa + +import pyqtgraph as pg +import datetime as dt +import traceback +from qtpy import QtCore +from pyqtgraph.Point import Point +import pandas as pd + + +# 十字光标支持 +class Crosshair(QtCore.QObject): + """ + 此类给pg.PlotWidget()添加crossHair功能,PlotWidget实例需要初始化时传入 + """ + signal = QtCore.Signal(type(tuple([]))) + signalInfo = QtCore.Signal(float, float) + + def __init__(self, parent, master): + """Constructor""" + self.__view = parent # PlatWidget + self.master = master # KLineWidget + super(Crosshair, self).__init__() + + self.xAxis = 0 + self.yAxis = 0 + + self.datas = None + + self.yAxises = [0 for i in range(3)] + self.leftX = [0 for i in range(3)] + self.showHLine = [False for i in range(3)] + self.textPrices = [pg.TextItem('', anchor=(1, 1)) for i in range(3)] + # 取得 widget上的:主图/成交量/副图指标 + self.views = [parent.centralWidget.getItem(i + 1, 0) for i in range(3)] + self.rects = [self.views[i].sceneBoundingRect() for i in range(3)] + self.vLines = [pg.InfiniteLine(angle=90, movable=False) for i in range(3)] + self.hLines = [pg.InfiniteLine(angle=0, movable=False) for i in range(3)] + + # mid 在y轴动态跟随最新价显示最新价和最新时间 + self.__textDate = pg.TextItem('date', anchor=(1, 1)) # 文本:日期/时间 + self.__textInfo = pg.TextItem('lastBarInfo') # 文本:bar信息 + self.__text_main_indicators = pg.TextItem('lastIndicatorsInfo', anchor=(1, 0)) # 文本:主图指标 + self.__text_sub_indicators = pg.TextItem('lastSubIndicatorsInfo', anchor=(1, 0)) # 文本: 副图指标 + self.__textVolume = pg.TextItem('lastBarVolume', anchor=(1, 0)) # 文本: 成交量 + + self.__textDate.setZValue(2) + self.__textInfo.setZValue(2) + self.__text_main_indicators.setZValue(2) + self.__text_sub_indicators.setZValue(2) + self.__textVolume.setZValue(2) + self.__textInfo.border = pg.mkPen(color=(230, 255, 0, 255), width=1.2) + + for i in range(3): + self.textPrices[i].setZValue(2) + self.vLines[i].setPos(0) + self.hLines[i].setPos(0) + self.vLines[i].setZValue(0) + self.hLines[i].setZValue(0) + self.views[i].addItem(self.vLines[i]) + self.views[i].addItem(self.hLines[i]) + self.views[i].addItem(self.textPrices[i]) + + self.views[0].addItem(self.__textInfo, ignoreBounds=True) + self.views[0].addItem(self.__text_main_indicators, ignoreBounds=True) + + # 添加成交量的提示信息到 volume view + if self.master.display_vol: + self.views[1].addItem(self.__textVolume, ignoreBounds=True) + + # 添加 副图指标/日期信息到 副图 + if self.master.display_sub: + self.views[2].addItem(self.__textDate, ignoreBounds=True) + self.views[2].addItem(self.__text_sub_indicators, ignoreBounds=True) + else: + # 没有启用副图,日期信息放在主图 + self.views[0].addItem(self.__textDate, ignoreBounds=True) + + self.proxy = pg.SignalProxy(self.__view.scene().sigMouseMoved, rateLimit=360, slot=self.__mouseMoved) + # 跨线程刷新界面支持 + self.signal.connect(self.update) + self.signalInfo.connect(self.plotInfo) + + def update(self, pos): + """刷新界面显示""" + try: + xAxis, yAxis = pos + xAxis, yAxis = (self.xAxis, self.yAxis) if xAxis is None else (xAxis, yAxis) + self.moveTo(xAxis, yAxis) + except Exception as ex: + print(u'Crosshair.update() exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + + def __mouseMoved(self, evt): + """鼠标移动回调""" + try: + pos = evt[0] + self.rects = [self.views[i].sceneBoundingRect() for i in range(3)] + for i in range(3): + self.showHLine[i] = False + if i == 1 and not self.master.display_vol: + continue + + if i == 2 and not self.master.display_sub: + continue + + if self.rects[i].contains(pos): + mousePoint = self.views[i].vb.mapSceneToView(pos) + xAxis = mousePoint.x() + yAxis = mousePoint.y() + self.yAxises[i] = yAxis + self.showHLine[i] = True + self.moveTo(xAxis, yAxis) + except Exception as ex: + print(u'_mouseMove() exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + + def moveTo(self, xAxis, yAxis): + """ + 移动 + :param xAxis: + :param yAxis: + :return: + """ + try: + xAxis, yAxis = (self.xAxis, self.yAxis) if xAxis is None else (int(xAxis), yAxis) + self.rects = [self.views[i].sceneBoundingRect() for i in range(3)] + if not xAxis or not yAxis: + return + self.xAxis = xAxis + self.yAxis = yAxis + self.vhLinesSetXY(xAxis, yAxis) + self.plotInfo(xAxis, yAxis) + self.master.pi_volume.update() + except Exception as ex: + print(u'_mouseMove() exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + + def vhLinesSetXY(self, xAxis, yAxis): + """水平和竖线位置设置""" + for i in range(3): + self.vLines[i].setPos(xAxis) + if self.showHLine[i]: + self.hLines[i].setPos(yAxis if i == 0 else self.yAxises[i]) + self.hLines[i].show() + else: + self.hLines[i].hide() + + def plotInfo(self, xAxis, yAxis): + """ + 被嵌入的plotWidget在需要的时候通过调用此方法显示K线信息 + """ + if self.datas is None or xAxis >= len(self.datas): + return + + tickDatetime = None + openPrice = 0 + closePrice = 0 + highPrice = 0 + lowPrice = 0 + preClosePrice = 0 + volume = 0 + # open_interest = 0 + + try: + # 获取K线数据 + data = self.datas[xAxis] + lastdata = self.datas[xAxis - 1] + tickDatetime = pd.to_datetime(data['datetime']) + openPrice = data['open'] + closePrice = data['close'] + lowPrice = data['low'] + highPrice = data['high'] + volume = int(data['volume']) + # open_interest = int(data['open_interest']) + preClosePrice = lastdata['close'] + + except Exception as ex: + print(u'exception:{},trace:{}'.format(str(ex), traceback.format_exc())) + return + + if (isinstance(tickDatetime, dt.datetime)): + datetimeText = dt.datetime.strftime(tickDatetime, '%Y-%m-%d %H:%M:%S') + dateText = dt.datetime.strftime(tickDatetime, '%Y-%m-%d') + timeText = dt.datetime.strftime(tickDatetime, '%H:%M:%S') + else: + datetimeText = "" + dateText = "" + timeText = "" + + # 显示所有的主图技术指标 + html = u'
' + for indicator in self.master.main_indicator_data: + val = self.master.main_indicator_data[indicator][xAxis] + col = self.master.main_indicator_colors[indicator] + html += u'  %s:%.2f' % (col, indicator, val) + html += u'
' + self.__text_main_indicators.setHtml(html) + + # 显示所有的主图技术指标 + html = u'
' + for indicator in self.master.sub_indicator_data: + val = self.master.sub_indicator_data[indicator][xAxis] + col = self.master.sub_indicator_colors[indicator] + html += u'  %s:%.2f' % (col, indicator, val) + html += u'
' + self.__text_sub_indicators.setHtml(html) + + # 和上一个收盘价比较,决定K线信息的字符颜色 + cOpen = 'red' if openPrice > preClosePrice else 'green' + cClose = 'red' if closePrice > preClosePrice else 'green' + cHigh = 'red' if highPrice > preClosePrice else 'green' + cLow = 'red' if lowPrice > preClosePrice else 'green' + + self.__textInfo.setHtml( + u'
\ + 日期
\ + %s
\ + 时间
\ + %s
\ + 价格
\ + (开) %.3f
\ + (高) %.3f
\ + (低) %.3f
\ + (收) %.3f
\ + 成交量
\ + (量) %d
\ +
' \ + % (dateText, timeText, cOpen, openPrice, cHigh, highPrice, + cLow, lowPrice, cClose, closePrice, volume)) + self.__textDate.setHtml( + '
\ + %s\ +
' \ + % (datetimeText)) + + self.__textVolume.setHtml( + '
\ + VOL : %.3f\ +
' \ + % (volume)) + # 坐标轴宽度 + rightAxisWidth = self.views[0].getAxis('right').width() + bottomAxisHeight = self.views[2].getAxis('bottom').height() + offset = QtCore.QPointF(rightAxisWidth, bottomAxisHeight) + + # 各个顶点 + t1 = [None, None, None] + br = [None, None, None] + t1[0] = self.views[0].vb.mapSceneToView(self.rects[0].topLeft()) + br[0] = self.views[0].vb.mapSceneToView(self.rects[0].bottomRight() - offset) + + if self.master.display_vol: + t1[1] = self.views[1].vb.mapSceneToView(self.rects[1].topLeft()) + br[1] = self.views[1].vb.mapSceneToView(self.rects[1].bottomRight() - offset) + if self.master.display_sub: + t1[2] = self.views[2].vb.mapSceneToView(self.rects[2].topLeft()) + br[2] = self.views[2].vb.mapSceneToView(self.rects[2].bottomRight() - offset) + + # 显示价格 + for i in range(3): + if self.showHLine[i] and t1[i] is not None and br[i] is not None: + self.textPrices[i].setHtml( + '
\ + \ + %0.3f\ + \ +
' \ + % (yAxis if i == 0 else self.yAxises[i])) + self.textPrices[i].setPos(br[i].x(), yAxis if i == 0 else self.yAxises[i]) + self.textPrices[i].show() + else: + self.textPrices[i].hide() + + # 设置坐标 + self.__textInfo.setPos(t1[0]) + self.__text_main_indicators.setPos(br[0].x(), t1[0].y()) + if self.master.display_sub: + self.__text_sub_indicators.setPos(br[2].x(), t1[2].y()) + if self.master.display_vol: + self.__textVolume.setPos(br[1].x(), t1[1].y()) + + # 修改对称方式防止遮挡 + self.__textDate.anchor = Point((1, 1)) if xAxis > self.master.index else Point((0, 1)) + if br[2] is not None: + self.__textDate.setPos(xAxis, br[2].y()) + elif br[1] is not None: + self.__textDate.setPos(xAxis, br[1].y()) + else: + self.__textDate.setPos(xAxis, br[0].y()) diff --git a/vnpy/trader/ui/kline/css.qss b/vnpy/trader/ui/kline/css.qss new file mode 100644 index 00000000..acb366d8 --- /dev/null +++ b/vnpy/trader/ui/kline/css.qss @@ -0,0 +1,1342 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) <2013-2014> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +QToolTip +{ + border: 1px solid #76797C; + background-color: rgb(90, 102, 117);; + color: white; + padding: 5px; + opacity: 200; +} + +QWidget +{ + color: #eff0f1; + background-color: #31363b; + selection-background-color:#3daee9; + selection-color: #eff0f1; + background-clip: border; + border-image: none; + border: 0px transparent black; + outline: 0; +} + +QWidget:item:hover +{ + background-color: #3daee9; + color: #eff0f1; +} + +QWidget:item:selected +{ + background-color: #3daee9; +} + +QCheckBox +{ + spacing: 5px; + outline: none; + color: #eff0f1; + margin-bottom: 2px; +} + +QCheckBox:disabled +{ + color: #76797C; +} + +QCheckBox::indicator, +QGroupBox::indicator +{ + width: 18px; + height: 18px; +} +QGroupBox::indicator +{ + margin-left: 2px; +} + +QCheckBox::indicator:unchecked +{ + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QCheckBox::indicator:unchecked:hover, +QCheckBox::indicator:unchecked:focus, +QCheckBox::indicator:unchecked:pressed, +QGroupBox::indicator:unchecked:hover, +QGroupBox::indicator:unchecked:focus, +QGroupBox::indicator:unchecked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); +} + +QCheckBox::indicator:checked +{ + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QCheckBox::indicator:checked:hover, +QCheckBox::indicator:checked:focus, +QCheckBox::indicator:checked:pressed, +QGroupBox::indicator:checked:hover, +QGroupBox::indicator:checked:focus, +QGroupBox::indicator:checked:pressed +{ + border: none; + image: url(:/qss_icons/rc/checkbox_checked_focus.png); +} + + +QCheckBox::indicator:indeterminate +{ + image: url(:/qss_icons/rc/checkbox_indeterminate.png); +} + +QCheckBox::indicator:indeterminate:focus, +QCheckBox::indicator:indeterminate:hover, +QCheckBox::indicator:indeterminate:pressed +{ + image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); +} + +QCheckBox::indicator:checked:disabled, +QGroupBox::indicator:checked:disabled +{ + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +QCheckBox::indicator:unchecked:disabled, +QGroupBox::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QRadioButton +{ + spacing: 5px; + outline: none; + color: #eff0f1; + margin-bottom: 2px; +} + +QRadioButton:disabled +{ + color: #76797C; +} +QRadioButton::indicator +{ + width: 21px; + height: 21px; +} + +QRadioButton::indicator:unchecked +{ + image: url(:/qss_icons/rc/radio_unchecked.png); +} + + +QRadioButton::indicator:unchecked:hover, +QRadioButton::indicator:unchecked:focus, +QRadioButton::indicator:unchecked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_unchecked_focus.png); +} + +QRadioButton::indicator:checked +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked.png); +} + +QRadioButton::indicator:checked:hover, +QRadioButton::indicator:checked:focus, +QRadioButton::indicator:checked:pressed +{ + border: none; + outline: none; + image: url(:/qss_icons/rc/radio_checked_focus.png); +} + +QRadioButton::indicator:checked:disabled +{ + outline: none; + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QRadioButton::indicator:unchecked:disabled +{ + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + + +QMenuBar +{ + background-color: #31363b; + color: #eff0f1; +} + +QMenuBar::item +{ + background: transparent; +} + +QMenuBar::item:selected +{ + background: transparent; + border: 1px solid #76797C; +} + +QMenuBar::item:pressed +{ + border: 1px solid #76797C; + background-color: #3daee9; + color: #eff0f1; + margin-bottom:-1px; + padding-bottom:1px; +} + +QMenu +{ + border: 1px solid #76797C; + color: #eff0f1; + margin: 2px; +} + +QMenu::icon +{ + margin: 5px; +} + +QMenu::item +{ + padding: 5px 30px 5px 30px; + margin-left: 5px; + border: 1px solid transparent; /* reserve space for selection border */ +} + +QMenu::item:selected +{ + color: #eff0f1; +} + +QMenu::separator { + height: 2px; + background: lightblue; + margin-left: 10px; + margin-right: 5px; +} + +QMenu::indicator { + width: 18px; + height: 18px; +} + +/* non-exclusive indicator = check box style indicator + (see QActionGroup::setExclusive) */ +QMenu::indicator:non-exclusive:unchecked { + image: url(:/qss_icons/rc/checkbox_unchecked.png); +} + +QMenu::indicator:non-exclusive:unchecked:selected { + image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); +} + +QMenu::indicator:non-exclusive:checked { + image: url(:/qss_icons/rc/checkbox_checked.png); +} + +QMenu::indicator:non-exclusive:checked:selected { + image: url(:/qss_icons/rc/checkbox_checked_disabled.png); +} + +/* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ +QMenu::indicator:exclusive:unchecked { + image: url(:/qss_icons/rc/radio_unchecked.png); +} + +QMenu::indicator:exclusive:unchecked:selected { + image: url(:/qss_icons/rc/radio_unchecked_disabled.png); +} + +QMenu::indicator:exclusive:checked { + image: url(:/qss_icons/rc/radio_checked.png); +} + +QMenu::indicator:exclusive:checked:selected { + image: url(:/qss_icons/rc/radio_checked_disabled.png); +} + +QMenu::right-arrow { + margin: 5px; + image: url(:/qss_icons/rc/right_arrow.png) +} + + +QWidget:disabled +{ + color: #454545; + background-color: #31363b; +} + +QAbstractItemView +{ + alternate-background-color: #31363b; + color: #eff0f1; + border: 1px solid 3A3939; + border-radius: 2px; +} + +QWidget:focus, QMenuBar:focus +{ + border: 1px solid #3daee9; +} + +QTabWidget:focus, QCheckBox:focus, QRadioButton:focus, QSlider:focus +{ + border: none; +} + +QLineEdit +{ + background-color: #232629; + padding: 5px; + border-style: solid; + border: 1px solid #76797C; + border-radius: 2px; + color: #eff0f1; +} + +QGroupBox { + border:1px solid #76797C; + border-radius: 2px; + margin-top: 20px; + font-size:18px; + font-weight:bold; +} + +QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top center; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; + font-size:18px; + font-weight:bold; +} + +QAbstractScrollArea +{ + border-radius: 2px; + border: 1px solid #76797C; + background-color: transparent; +} + +QScrollBar:horizontal +{ + height: 15px; + margin: 3px 15px 3px 15px; + border: 1px transparent #2A2929; + border-radius: 4px; + background-color: #2A2929; +} + +QScrollBar::handle:horizontal +{ + background-color: #605F5F; + min-width: 5px; + border-radius: 4px; +} + +QScrollBar::add-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/right_arrow_disabled.png); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal +{ + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/left_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/right_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + + +QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on +{ + border-image: url(:/qss_icons/rc/left_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal +{ + background: none; +} + + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal +{ + background: none; +} + +QScrollBar:vertical +{ + background-color: #2A2929; + width: 15px; + margin: 15px 3px 15px 3px; + border: 1px transparent #2A2929; + border-radius: 4px; +} + +QScrollBar::handle:vertical +{ + background-color: #605F5F; + min-height: 5px; + border-radius: 4px; +} + +QScrollBar::sub-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/up_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical +{ + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/down_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on +{ + + border-image: url(:/qss_icons/rc/up_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on +{ + border-image: url(:/qss_icons/rc/down_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical +{ + background: none; +} + + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical +{ + background: none; +} + +QTextEdit +{ + background-color: #232629; + color: #eff0f1; + border: 1px solid #76797C; +} + +QPlainTextEdit +{ + background-color: #232629;; + color: #eff0f1; + border-radius: 2px; + border: 1px solid #76797C; +} + +QHeaderView::section +{ + background-color: #76797C; + color: #eff0f1; + padding: 5px; + border: 1px solid #76797C; +} + +QSizeGrip { + image: url(:/qss_icons/rc/sizegrip.png); + width: 12px; + height: 12px; +} + + +QMainWindow::separator +{ + background-color: #31363b; + color: white; + padding-left: 4px; + spacing: 2px; + border: 1px dashed #76797C; +} + +QMainWindow::separator:hover +{ + + background-color: #787876; + color: white; + padding-left: 4px; + border: 1px solid #76797C; + spacing: 2px; +} + + +QMenu::separator +{ + height: 1px; + background-color: #76797C; + color: white; + padding-left: 4px; + margin-left: 10px; + margin-right: 5px; +} + + +QFrame +{ + border-radius: 2px; + border: 1px solid #76797C; +} + +QFrame[frameShape="0"] +{ + border-radius: 2px; + border: 1px transparent #76797C; +} + +QStackedWidget +{ + border: 1px transparent black; +} + +QToolBar { + border: 1px transparent #393838; + background: 1px solid #31363b; + font-weight: bold; +} + +QToolBar::handle:horizontal { + image: url(:/qss_icons/rc/Hmovetoolbar.png); +} +QToolBar::handle:vertical { + image: url(:/qss_icons/rc/Vmovetoolbar.png); +} +QToolBar::separator:horizontal { + image: url(:/qss_icons/rc/Hsepartoolbar.png); +} +QToolBar::separator:vertical { + image: url(:/qss_icons/rc/Vsepartoolbar.png); +} +QToolButton#qt_toolbar_ext_button { + background: #58595a +} + +QPushButton +{ + color: #eff0f1; + background-color: #31363b; + border-width: 1px; + border-color: #76797C; + border-style: solid; + padding: 5px; + border-radius: 2px; + outline: none; +} + +QPushButton:disabled +{ + background-color: #31363b; + border-width: 1px; + border-color: #454545; + border-style: solid; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + border-radius: 2px; + color: #454545; +} + +QPushButton:focus { + background-color: #3daee9; + color: white; +} + +QPushButton:pressed +{ + background-color: #3daee9; + padding-top: -15px; + padding-bottom: -17px; +} + +QPushButton#blueButton{ + border-radius: 4px; + border: 2px solid rgb(41, 41, 41); + background-color: rgb(0, 112, 193); +} +QPushButton#blueButton:hover{ + border-color: rgb(45, 45, 45); +} +QPushButton#blueButton:pressed, QPushButton#buleButton:checked{ + border-color: rgb(0, 160, 230); +} + + +QPushButton#redButton{ + border-radius: 4px; + border: 2px solid rgb(41, 41, 41); + background-color: rgb(114, 26, 20); +} +QPushButton#redButton:hover{ + border-color: rgb(45, 45, 45); +} +QPushButton#redButton:pressed, QPushButton#buleButton:checked{ + border-color: rgb(0, 160, 230); +} + +QPushButton#greenButton{ + border-radius: 4px; + border: 2px solid rgb(41, 41, 41); + background-color: rgb(118, 197, 126); +} +QPushButton#greenButton:hover{ + border-color: rgb(45, 45, 45); +} +QPushButton#greenButton:pressed, QPushButton#buleButton:checked{ + border-color: rgb(0, 160, 230); +} + + +QComboBox +{ + selection-background-color: #3daee9; + border-style: solid; + border: 1px solid #76797C; + border-radius: 2px; + padding: 5px; + min-width: 75px; +} + +QPushButton:checked{ + background-color: #76797C; + border-color: #6A6969; +} + +QComboBox:hover,QPushButton:hover,QAbstractSpinBox:hover,QLineEdit:hover,QTextEdit:hover,QPlainTextEdit:hover,QAbstractView:h +over,QTreeView:hover +{ + border: 1px solid #3daee9; + color: #eff0f1; +} + +QComboBox:on +{ + padding-top: 3px; + padding-left: 4px; + selection-background-color: #4a4a4a; +} + +QComboBox QAbstractItemView +{ + background-color: #232629; + border-radius: 2px; + border: 1px solid #76797C; + selection-background-color: #3daee9; +} + +QComboBox::drop-down +{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 15px; + + border-left-width: 0px; + border-left-color: darkgray; + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); +} + +QComboBox::down-arrow:on, QComboBox::down-arrow:hover, +QComboBox::down-arrow:focus +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + +QAbstractSpinBox { + padding: 5px; + border: 1px solid #76797C; + background-color: #232629; + color: #eff0f1; + border-radius: 2px; + min-width: 75px; +} + +QAbstractSpinBox:up-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center right; +} + +QAbstractSpinBox:down-button +{ + background-color: transparent; + subcontrol-origin: border; + subcontrol-position: center left; +} + +QAbstractSpinBox::up-arrow,QAbstractSpinBox::up-arrow:disabled,QAbstractSpinBox::up-arrow:off { + image: url(:/qss_icons/rc/up_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::up-arrow:hover +{ + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QAbstractSpinBox::down-arrow,QAbstractSpinBox::down-arrow:disabled,QAbstractSpinBox::down-arrow:off +{ + image: url(:/qss_icons/rc/down_arrow_disabled.png); + width: 10px; + height: 10px; +} +QAbstractSpinBox::down-arrow:hover +{ + image: url(:/qss_icons/rc/down_arrow.png); +} + + +QLabel +{ + border: 0px solid black; +} + +QLabel#whiteLabel +{ + border: 0px solid black; + background:white; + color:rgb(100,100,100,250); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#whiteLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#blueLabel +{ + border: 0px solid black; + background:rgb(0, 112, 193); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#blueLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#redLabel +{ + border: 0px solid black; + background: rgb(114, 26, 20); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#redLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#greenLabel +{ + border: 0px solid black; + background: rgb(118, 197, 126); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#greenLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#orangeLabel +{ + border: 0px solid black; + background: rgb(255, 121, 0); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#orangeLabel:hover +{ + color:rgb(100,100,100,120); +} + +QLabel#purseLabel +{ + border: 0px solid black; + background: rgb(98, 43, 98); + font-size:15px; + font-weight:bold; + font-family:Roman times; +} + +QLabel#purseLabel:hover +{ + color:rgb(100,100,100,120); +} + +QTabWidget{ + border: 0px transparent black; +} + +QTabWidget::pane { + border: 1px solid #76797C; + padding: 5px; + margin: 0px; +} + +QTabBar +{ + qproperty-drawBase: 0; + left: 5px; /* move to the right by 5px */ + border-radius: 3px; +} + +QTabBar:focus +{ + border: 0px transparent black; +} + +QTabBar::close-button { + image: url(:/qss_icons/rc/close.png); + background: transparent; +} + +QTabBar::close-button:hover +{ + image: url(:/qss_icons/rc/close-hover.png); + background: transparent; +} + +QTabBar::close-button:pressed { + image: url(:/qss_icons/rc/close-pressed.png); + background: transparent; +} + +/* TOP TABS */ +QTabBar::tab:top { + color: #eff0f1; + border: 1px solid #76797C; + border-bottom: 1px transparent black; + background-color: #31363b; + padding: 5px; + min-width: 50px; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-bottom: 1px transparent black; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +QTabBar::tab:top:!selected:hover { + background-color: #3daee9; +} + +/* BOTTOM TABS */ +QTabBar::tab:bottom { + color: #eff0f1; + border: 1px solid #76797C; + border-top: 1px transparent black; + background-color: #31363b; + padding: 5px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + min-width: 50px; +} + +QTabBar::tab:bottom:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-top: 1px transparent black; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:bottom:!selected:hover { + background-color: #3daee9; +} + +/* LEFT TABS */ +QTabBar::tab:left { + color: #eff0f1; + border: 1px solid #76797C; + border-left: 1px transparent black; + background-color: #31363b; + padding: 5px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + min-height: 50px; +} + +QTabBar::tab:left:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-left: 1px transparent black; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} + +QTabBar::tab:left:!selected:hover { + background-color: #3daee9; +} + + +/* RIGHT TABS */ +QTabBar::tab:right { + color: #eff0f1; + border: 1px solid #76797C; + border-right: 1px transparent black; + background-color: #31363b; + padding: 5px; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + min-height: 50px; +} + +QTabBar::tab:right:!selected +{ + color: #eff0f1; + background-color: #54575B; + border: 1px solid #76797C; + border-right: 1px transparent black; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +QTabBar::tab:right:!selected:hover { + background-color: #3daee9; +} + +QTabBar QToolButton::right-arrow:enabled { + image: url(:/qss_icons/rc/right_arrow.png); + } + + QTabBar QToolButton::left-arrow:enabled { + image: url(:/qss_icons/rc/left_arrow.png); + } + +QTabBar QToolButton::right-arrow:disabled { + image: url(:/qss_icons/rc/right_arrow_disabled.png); + } + + QTabBar QToolButton::left-arrow:disabled { + image: url(:/qss_icons/rc/left_arrow_disabled.png); + } + + +QDockWidget { + background: #31363b; + border: 1px solid #403F3F; + titlebar-close-icon: url(:/qss_icons/rc/close.png); + titlebar-normal-icon: url(:/qss_icons/rc/undock.png); +} + +QDockWidget::close-button, QDockWidget::float-button { + border: 1px solid transparent; + border-radius: 2px; + background: transparent; +} + +QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background: rgba(255, 255, 255, 10); +} + +QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + padding: 1px -1px -1px 1px; + background: rgba(255, 255, 255, 10); +} + +QTreeView, QListView +{ + border: 1px solid #76797C; + background-color: #232629; +} + +QTreeView:branch:selected, QTreeView:branch:hover +{ + background: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:!adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item { + border-image: url(:/qss_icons/rc/transparent.png); +} + +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_closed.png); +} + +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings { + image: url(:/qss_icons/rc/branch_open.png); +} + +QTreeView::branch:has-children:!has-siblings:closed:hover, +QTreeView::branch:closed:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_closed-on.png); + } + +QTreeView::branch:open:has-children:!has-siblings:hover, +QTreeView::branch:open:has-children:has-siblings:hover { + image: url(:/qss_icons/rc/branch_open-on.png); + } + +QListView::item:!selected:hover, QTreeView::item:!selected:hover { + background: rgba(167,218,245, 0.3); + outline: 0; + color: #eff0f1 +} + +QListView::item:selected:hover, QTreeView::item:selected:hover { + background: #3daee9; + color: #eff0f1; +} + +QSlider::groove:horizontal { + border: 1px solid #565a5e; + height: 4px; + background: #565a5e; + margin: 0px; + border-radius: 2px; +} + +QSlider::handle:horizontal { + background: #232629; + border: 1px solid #565a5e; + width: 16px; + height: 16px; + margin: -8px 0; + border-radius: 9px; +} + +QSlider::groove:vertical { + border: 1px solid #565a5e; + width: 4px; + background: #565a5e; + margin: 0px; + border-radius: 3px; +} + +QSlider::handle:vertical { + background: #232629; + border: 1px solid #565a5e; + width: 16px; + height: 16px; + margin: 0 -8px; + border-radius: 9px; +} + +QToolButton { + background-color: transparent; + border: 1px transparent #76797C; + border-radius: 2px; + margin: 3px; + padding: 5px; +} + +QToolButton[popupMode="1"] { /* only for MenuButtonPopup */ + padding-right: 20px; /* make way for the popup button */ + border: 1px #76797C; + border-radius: 5px; +} + +QToolButton[popupMode="2"] { /* only for InstantPopup */ + padding-right: 10px; /* make way for the popup button */ + border: 1px #76797C; +} + + +QToolButton:hover, QToolButton::menu-button:hover { + background-color: transparent; + border: 1px solid #3daee9; + padding: 5px; +} + +QToolButton:checked, QToolButton:pressed, + QToolButton::menu-button:pressed { + background-color: #3daee9; + border: 1px solid #3daee9; + padding: 5px; +} + +/* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ +QToolButton::menu-indicator { + image: url(:/qss_icons/rc/down_arrow.png); + top: -7px; left: -2px; /* shift it a bit */ +} + +/* the subcontrols below are used only in the MenuButtonPopup mode */ +QToolButton::menu-button { + border: 1px transparent #76797C; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + /* 16px width + 4px for border = 20px allocated above */ + width: 16px; + outline: none; +} + +QToolButton::menu-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QToolButton::menu-arrow:open { + border: 1px solid #76797C; +} + +QPushButton::menu-indicator { + subcontrol-origin: padding; + subcontrol-position: bottom right; + left: 8px; +} + +QTableView +{ + border: 1px solid #76797C; + gridline-color: #31363b; + background-color: #232629; +} + + +QTableView, QHeaderView +{ + border-radius: 0px; +} + +QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { + background: #3daee9; + color: #eff0f1; +} + +QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { + background: #3daee9; + color: #eff0f1; +} + + +QHeaderView +{ + background-color: #31363b; + border: 1px transparent; + border-radius: 0px; + margin: 0px; + padding: 0px; + +} + +QHeaderView::section { + background-color: #31363b; + color: #eff0f1; + padding: 5px; + border: 1px solid #76797C; + border-radius: 0px; + text-align: center; +} + +QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one +{ + border-top: 1px solid #76797C; +} + +QHeaderView::section::vertical +{ + border-top: transparent; +} + +QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one +{ + border-left: 1px solid #76797C; +} + +QHeaderView::section::horizontal +{ + border-left: transparent; +} + + +QHeaderView::section:checked + { + color: white; + background-color: #334e5e; + } + + /* style the sort indicator */ +QHeaderView::down-arrow { + image: url(:/qss_icons/rc/down_arrow.png); +} + +QHeaderView::up-arrow { + image: url(:/qss_icons/rc/up_arrow.png); +} + + +QTableCornerButton::section { + background-color: #31363b; + border: 1px transparent #76797C; + border-radius: 0px; +} + +QToolBox { + padding: 5px; + border: 1px transparent black; +} + +QToolBox::tab { + color: #eff0f1; + background-color: #31363b; + border: 1px solid #76797C; + border-bottom: 1px transparent #31363b; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +QToolBox::tab:selected { /* italicize selected tabs */ + font: italic; + background-color: #31363b; + border-color: #3daee9; + } + +QStatusBar::item { + border: 0px transparent dark; + } + + +QFrame[height="3"], QFrame[width="3"] { + background-color: #76797C; +} + + +QSplitter::handle { + border: 1px dashed #76797C; +} + +QSplitter::handle:hover { + background-color: #787876; + border: 1px solid #76797C; +} + +QSplitter::handle:horizontal { + width: 1px; +} + +QSplitter::handle:vertical { + height: 1px; +} + +QProgressBar { + border: 1px solid #76797C; + border-radius: 5px; + text-align: center; +} + +QProgressBar::chunk { + background-color: #05B8CC; +} + + diff --git a/vnpy/trader/ui/kline/cta.ico b/vnpy/trader/ui/kline/cta.ico new file mode 100644 index 0000000000000000000000000000000000000000..1cc84063b099fa1bbf69cf5f1e31d551293d87fc GIT binary patch literal 186094 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 1: + self.onDoubleClick(event.pos()) + else: + self.pos = event.pos() + + event.accept() + + def mouseRelease(self, event): + """ + 重载方法mouseReleaseEvent(self,event),即鼠标点击事件方法 + :param event: + :return: + """ + if event.button() == QtCore.Qt.RightButton: + self.onRRelease(event.pos()) + elif event.button() == QtCore.Qt.LeftButton: + self.onLRelease(event.pos()) + self.releaseMouse() + + def wheelEvent(self, event): + """ + 重载方法wheelEvent(self,event),即滚轮事件方法 + :param event: + :return: + """ + try: + pos = event.angleDelta() + if pos.y() > 0: + self.onUp() + else: + self.onDown() + event.accept() + except Exception as ex: + print(u'wheelEvent exception:{},{}'.format(str(ex), traceback.format_exc())) + + def paintEvent(self, event): + """ + 重载方法paintEvent(self,event),即拖动事件方法 + :param event: + :return: + """ + self.onPaint() + event.accept() + + # PgDown键 + + def onNxt(self): + pass + + # PgUp键 + + def onPre(self): + pass + + # 向上键和滚轮向上 + + def onUp(self): + pass + + # 向下键和滚轮向下 + + def onDown(self): + pass + + # 向左键 + + def onLeft(self): + pass + + # 向右键 + + def onRight(self): + pass + + # 鼠标左单击 + + def onLClick(self, pos): + print('single left click') + + # 鼠标右单击 + + def onRClick(self, pos): + pass + + def onDoubleClick(self, pos): + print('double click') + + # 鼠标左释放 + + def onLRelease(self, pos): + pass + + # 鼠标右释放 + + def onRRelease(self, pos): + pass + + # 画图 + + def onPaint(self): + pass + + +# 选择缩放功能支持 +class CustomViewBox(pg.ViewBox): + + def __init__(self, *args, **kwds): + pg.ViewBox.__init__(self, *args, **kwds) + # 拖动放大模式 + # self.setMouseMode(self.RectMode) + + # 右键自适应 + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + self.autoRange() + + +class MyStringAxis(pg.AxisItem): + """ + 时间序列横坐标支持 + changelog: by 李来佳 + 增加时间与x轴的双向映射 + """ + + # 初始化 + + def __init__(self, xdict, *args, **kwargs): + pg.AxisItem.__init__(self, *args, **kwargs) + self.minVal = 0 + self.maxVal = 0 + # 序列 <= > 时间 + self.xdict = OrderedDict() + self.xdict.update(xdict) + # 时间 <=> 序列 + self.tdict = OrderedDict([(v, k) for k, v in xdict.items()]) + self.x_values = np.asarray(xdict.keys()) + self.x_strings = list(xdict.values()) + self.setPen(color=(255, 255, 255, 255), width=0.8) + self.setStyle(tickFont=QtGui.QFont("Roman times", 10, QtGui.QFont.Bold), autoExpandTextSpace=True) + + def update_xdict(self, xdict): + """ + 更新坐标映射表 + :param xdict: + :return: + """ + # 更新 x轴-时间映射 + self.xdict.update(xdict) + # 更新 时间-x轴映射 + tdict = dict([(v, k) for k, v in xdict.items()]) + self.tdict.update(tdict) + + # 重新生成x轴队列和时间字符串显示队列 + self.x_values = np.asarray(self.xdict.keys()) + self.x_strings = list(self.xdict.values()) + + def get_x_by_time(self, t_value): + """ + 通过 时间,找到匹配或最接近x轴 + :param t_value: datetime 类型时间 + :return: + """ + last_time = None + for t in self.x_strings: + if last_time is None: + last_time = t + continue + if t > t_value: + break + last_time = t + + x = self.tdict.get(last_time, 0) + return x + + def tickStrings(self, values, scale, spacing): + """ + 将原始横坐标转换为时间字符串,第一个坐标包含日期 + :param values: + :param scale: + :param spacing: + :return: + """ + strings = [] + for v in values: + vs = v * scale + if vs in self.x_values: + vstr = self.x_strings[np.abs(self.x_values - vs).argmin()] + vstr = vstr.strftime('%Y-%m-%d %H:%M:%S') + else: + vstr = "" + strings.append(vstr) + return strings + + +class CandlestickItem(pg.GraphicsObject): + """K线图形对象""" + + # 初始化 + + def __init__(self, data): + """初始化""" + pg.GraphicsObject.__init__(self) + # 数据格式: [ (time, open, close, low, high),...] + self.data = data + # 只重画部分图形,大大提高界面更新速度 + self.rect = None + self.picture = None + self.setFlag(self.ItemUsesExtendedStyleOption) + # 画笔和画刷 + w = 0.4 + self.offset = 0 + self.low = 0 + self.high = 1 + self.picture = QtGui.QPicture() + self.pictures = [] + self.bPen = pg.mkPen(color=(0, 240, 240, 255), width=w * 2) # 阴线画笔 + self.bBrush = pg.mkBrush((0, 240, 240, 255)) # 阴线主体 + self.rPen = pg.mkPen(color=(255, 60, 60, 255), width=w * 2) # 阳线画笔 + self.rBrush = pg.mkBrush((255, 60, 60, 255)) # 阳线主体 + self.rBrush.setStyle(QtCore.Qt.NoBrush) + # 刷新K线 + self.generatePicture(self.data) + + # 画K线 + + def generatePicture(self, data=None, redraw=False): + """重新生成图形对象""" + # 重画或者只更新最后一个K线 + if redraw: + self.pictures = [] + elif self.pictures: + self.pictures.pop() + w = 0.4 + bPen = self.bPen + bBrush = self.bBrush + rPen = self.rPen + rBrush = self.rBrush + low, high = (data[0]['low'], data[0]['high']) if len(data) > 0 else (0, 1) + for (t, open0, close0, low0, high0) in data: + # t 并不是时间,是序列 + if t >= len(self.pictures): + # 每一个K线创建一个picture + picture = QtGui.QPicture() + p = QtGui.QPainter(picture) + low, high = (min(low, low0), max(high, high0)) + + # 下跌蓝色(实心), 上涨红色(空心) + pen, brush, pmin, pmax = (bPen, bBrush, close0, open0) \ + if open0 > close0 else (rPen, rBrush, open0, close0) + p.setPen(pen) + p.setBrush(brush) + + # 画K线方块和上下影线 + if open0 == close0: + p.drawLine(QtCore.QPointF(t - w, open0), QtCore.QPointF(t + w, close0)) + else: + p.drawRect(QtCore.QRectF(t - w, open0, w * 2, close0 - open0)) + if pmin > low0: + p.drawLine(QtCore.QPointF(t, low0), QtCore.QPointF(t, pmin)) + if high0 > pmax: + p.drawLine(QtCore.QPointF(t, pmax), QtCore.QPointF(t, high0)) + + p.end() + # 添加到队列中 + self.pictures.append(picture) + + # 更新所有K线的最高/最低 + self.low, self.high = low, high + + # 手动重画 + + def update(self): + if not self.scene() is None: + self.scene().update() + + # 自动重画 + + def paint(self, painter, opt, w): + # 获取显示区域 + rect = opt.exposedRect + # 获取显示区域/数据的滑动最小值/最大值,即需要显示的数据最小值/最大值。 + xmin, xmax = (max(0, int(rect.left())), min(int(len(self.pictures)), int(rect.right()))) + + # 区域发生变化,或者没有最新图片(缓存),重画 + if not self.rect == (rect.left(), rect.right()) or self.picture is None: + # 更新显示区域 + self.rect = (rect.left(), rect.right()) + # 重画,并缓存为最新图片 + self.picture = self.createPic(xmin, xmax) + self.picture.play(painter) + + # 存在缓存,直接显示出来 + elif self.picture: + self.picture.play(painter) + + # 缓存图片 + + def createPic(self, xmin, xmax): + picture = QtGui.QPicture() + p = QtGui.QPainter(picture) + # 全部数据,[xmin~xmax]的k线,重画一次 + [pic.play(p) for pic in self.pictures[xmin:xmax]] + p.end() + return picture + + # 定义显示边界,x轴:0~K线数据长度;Y轴,最低值~最高值-最低值 + + def boundingRect(self): + return QtCore.QRectF(0, self.low, len(self.pictures), (self.high - self.low)) + + +######################################################################## +class KLineWidget(KeyWraper): + """用于显示价格走势图""" + + # 是否完成了历史数据的读取 + initCompleted = False + clsId = 0 + + def __init__(self, parent=None, display_vol=False, display_sub=False, **kargs): + """Constructor""" + self.parent = parent + super(KLineWidget, self).__init__(parent) + + # 当前序号 + self.index = None # 下标 + self.countK = 60 # 显示的K线数量范围 + + KLineWidget.clsId += 1 + self.windowId = str(KLineWidget.clsId) + + self.title = u'KLineWidget' + + # # 保存K线数据的列表和Numpy Array对象 + self.datas = [] # 'datetime','open','close','low','high','volume','openInterest + self.listBar = [] # 蜡烛图使用的Bar list :'time_int','open','close','low','high' + self.listVol = [] # 成交量(副图使用)的 volume list + + # 交易事务有关的线段 + self.list_trans = [] # 交易事务( {'start_time','end_time','tns_type','start_price','end_price','start_x','end_x','completed'} + self.list_trans_lines = [] + + # 交易记录相关的箭头 + self.list_trade_arrow = [] # 交易图标 list + self.x_t_trade_map = OrderedDict() # x 轴 与交易信号的映射 + self.t_trade_dict = OrderedDict() # t 时间的交易记录 + + # 标记相关 + self.list_markup = [] + self.x_t_markup_map = OrderedDict() # x轴与标记的映射 + self.t_markup_dict = OrderedDict() # t 时间的标记 + + # 所有K线上指标 + self.main_color_pool = deque(['red', 'green', 'yellow', 'white']) + self.main_indicator_data = {} # 主图指标数据(字典,key是指标,value是list) + self.main_indicator_colors = {} # 主图指标颜色(字典,key是指标,value是list + self.main_indicator_plots = {} # 主图指标的所有画布(字典,key是指标,value是plot) + + self.display_vol = display_vol + self.display_sub = display_sub + + # 所副图上信号图 + self.sub_color_pool = deque(['red', 'green', 'yellow', 'white']) + self.sub_indicator_data = {} + self.sub_indicator_colors = {} + self.sub_indicator_plots = {} + + # 初始化完成 + self.initCompleted = False + + # 调用函数 + self.initUi() + + # 通知上层时间切换点的回调函数 + self.relocate_notify_func = None + + # 初始化相关 + def initUi(self): + """ + 初始化界面 + leyout 如下: + ------------------———————— + \ 主图(K线/主图指标/交易信号 \ + \ \ + ----------------------------------- + \ 副图1(成交量) \ + ----------------------------------- + \ 副图2(持仓量/副图指标) \ + ----------------------------------- + + """ + self.setWindowTitle(u'K线工具') + # 主图 + self.pw = pg.PlotWidget() + # 界面布局 + self.lay_KL = pg.GraphicsLayout(border=(100, 100, 100)) + # self.lay_KL.setContentsMargins(10, 10, 10, 10) + self.lay_KL.setContentsMargins(5, 5, 5, 5) + self.lay_KL.setSpacing(0) + self.lay_KL.setBorder(color=(100, 100, 100, 250), width=0.4) + self.lay_KL.setZValue(0) + self.KLtitle = self.lay_KL.addLabel(u'') + self.pw.setCentralItem(self.lay_KL) + # 设置横坐标 + xdict = {} + self.axisTime = MyStringAxis(xdict, orientation='bottom') + # 初始化子图 + self.init_plot_main() + self.init_plot_volume() + self.init_plot_sub() + # 注册十字光标 + self.crosshair = Crosshair(self.pw, self) + # 设置界面 + self.vb = QtWidgets.QVBoxLayout() + self.vb.addWidget(self.pw) + self.setLayout(self.vb) + # 初始化完成 + self.initCompleted = True + + def create_plot_item(self, name): + """生成PlotItem对象""" + vb = CustomViewBox() + plotItem = pg.PlotItem(viewBox=vb, name=name, axisItems={'bottom': self.axisTime}) + plotItem.setMenuEnabled(False) + plotItem.setClipToView(True) + plotItem.hideAxis('left') + plotItem.showAxis('right') + plotItem.setDownsampling(mode='peak') + plotItem.setRange(xRange=(0, 1), yRange=(0, 1)) + plotItem.getAxis('right').setWidth(60) + plotItem.getAxis('right').setStyle(tickFont=QtGui.QFont("Roman times", 10, QtGui.QFont.Bold)) + plotItem.getAxis('right').setPen(color=(255, 255, 255, 255), width=0.8) + plotItem.showGrid(True, True) + plotItem.hideButtons() + return plotItem + + def init_plot_main(self): + """ + 初始化主图 + 1、添加 K线(蜡烛图) + :return: + """ + # 创建K线PlotItem + self.pi_main = self.create_plot_item('_'.join([self.windowId, 'Plot_Main'])) + + # 创建蜡烛图 + self.ci_candle = CandlestickItem(self.listBar) + + # 添加蜡烛图到主图 + self.pi_main.addItem(self.ci_candle) + self.pi_main.setMinimumHeight(200) + self.pi_main.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_main.hideAxis('bottom') + + # 添加主图到window layout + self.lay_KL.nextRow() + self.lay_KL.addItem(self.pi_main) + + def init_plot_volume(self): + """ + 初始化成交量副图 + :return: + """ + + # 创建plot item + self.pi_volume = self.create_plot_item('_'.join([self.windowId, 'Plot_Volume'])) + + if self.display_vol: + # 以蜡烛图(柱状图)的形式创建成交量图形对象 + self.ci_volume = CandlestickItem(self.listVol) + + # 副图1,添加成交量子图 + self.pi_volume.addItem(self.ci_volume) + self.pi_volume.setMaximumHeight(150) + self.pi_volume.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_volume.hideAxis('bottom') + else: + self.pi_volume.setMaximumHeight(1) + self.pi_volume.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_volume.hideAxis('bottom') + + # 添加副图1到window layout + self.lay_KL.nextRow() + self.lay_KL.addItem(self.pi_volume) + + def init_plot_sub(self): + """ + 初始化副图(只有一个图层) + :return: + """ + self.pi_sub = self.create_plot_item('_'.join([self.windowId, 'Plot_Sub'])) + + if self.display_sub: + # 副图的plot对象 + self.curve_sub = self.pi_sub.plot() + else: + self.pi_sub.setMaximumHeight(1) + self.pi_sub.setXLink('_'.join([self.windowId, 'Plot_Sub'])) + self.pi_sub.hideAxis('bottom') + + # 添加副图到窗体layer中 + self.lay_KL.nextRow() + self.lay_KL.addItem(self.pi_sub) + + # 画图相关 + + def plot_volume(self, redraw=False, xmin=0, xmax=-1): + """重画成交量子图""" + if self.initCompleted: + self.ci_volume.generatePicture(self.listVol[xmin:xmax], redraw) # 画成交量子图 + + def plot_kline(self, redraw=False, xmin=0, xmax=-1): + """重画K线子图""" + if self.initCompleted: + self.ci_candle.generatePicture(self.listBar[xmin:xmax], redraw) # 画K线 + + for indicator in list(self.main_indicator_data.keys()): + if indicator in self.main_indicator_plots: + self.main_indicator_plots[indicator].setData(self.main_indicator_data[indicator], + pen=self.main_indicator_colors[indicator][0], + name=indicator) + + def plot_sub(self, xmin=0, xmax=-1): + """重画持仓量子图""" + if self.initCompleted: + for indicator in list(self.sub_indicator_data.keys()): + # 调用该信号/指标画布(plotDataItem.setData()),更新数据,更新画笔颜色,更新名称 + if indicator in self.sub_indicator_plots: + self.sub_indicator_plots[indicator].setData(self.sub_indicator_data[indicator], + pen=self.sub_indicator_colors[indicator][0], + name=indicator) + + def add_indicator(self, indicator, is_main=True): + """ + 新增指标信号图 + :param indicator: 指标/信号的名称,如ma10, + :param is_main: 是否为主图 + :return: + """ + if is_main: + if indicator in self.main_indicator_plots: + self.pi_main.removeItem(self.main_indicator_plots[indicator]) # 存在该指标/信号,先移除原有画布 + + self.main_indicator_plots[indicator] = self.pi_main.plot() # 为该指标/信号,创建新的主图画布,登记字典 + self.main_indicator_colors[indicator] = self.main_color_pool[0] # 登记该指标/信号使用的颜色 + self.main_color_pool.append(self.main_color_pool.popleft()) # 调整剩余颜色 + if indicator not in self.main_indicator_data: + self.main_indicator_data[indicator] = [] + else: + if indicator in self.sub_indicator_plots: + self.pi_sub.removeItem(self.sub_indicator_plots[indicator]) # 若存在该指标/信号,先移除原有的附图画布 + self.sub_indicator_plots[indicator] = self.pi_sub.plot() # 为该指标/信号,创建新的主图画布,登记字典 + self.sub_indicator_colors[indicator] = self.sub_color_pool[0] # 登记该指标/信号使用的颜色 + self.sub_color_pool.append(self.sub_color_pool.popleft()) # 调整剩余颜色 + if indicator not in self.sub_indicator_data: + self.sub_indicator_data[indicator] = [] + + def plot_indicator(self, datas, is_main=True, clear=False): + """ + 刷新指标/信号图( 新数据) + :param datas: 所有数据 + :param is_main: 是否为主图 + :param clear: 是否要清除旧数据 + :return: + """ + if clear: + self.clear_indicator(is_main) # 清除主图/副图 + + if is_main: + for indicator in datas: + self.add_indicator(indicator, is_main) # 逐一添加主图信号/指标 + self.main_indicator_data[indicator] = datas[indicator] # 更新组件数据字典 + # 调用该信号/指标画布(plotDataItem.setData()),更新数据,更新画笔颜色,更新名称 + self.main_indicator_plots[indicator].setData(datas[indicator], + pen=self.main_indicator_colors[indicator][0], + name=indicator) + else: + for indicator in datas: + self.add_indicator(indicator, is_main) # 逐一增加子图指标/信号 + self.sub_indicator_data[indicator] = datas[indicator] # 更新组件数据字典 + # 调用该信号/指标画布(plotDataItem.setData()),更新数据,更新画笔颜色,更新名称 + self.sub_indicator_plots[indicator].setData(datas[indicator], + pen=self.sub_indicator_colors[indicator][0], name=indicator) + + def update_all(self): + """ + 手动更新所有K线图形,K线播放模式下需要 + """ + datas = self.datas + + if self.display_vol: + self.ci_volume.pictrue = None + self.ci_volume.update() + + self.ci_candle.pictrue = None + self.ci_candle.update() + + def update(view, low, high): + """ + 更新视图 + :param view: viewbox + :param low: + :param high: + :return: + """ + vRange = view.viewRange() + xmin = max(0, int(vRange[0][0])) + xmax = max(0, int(vRange[0][1])) + xmax = min(xmax, len(datas)) + if len(datas) > 0 and xmax > xmin: + ymin = min(datas[xmin:xmax][low]) + ymax = max(datas[xmin:xmax][high]) + view.setRange(yRange=(ymin, ymax)) + else: + view.setRange(yRange=(0, 1)) + + update(self.pi_main.getViewBox(), 'low', 'high') + update(self.pi_volume.getViewBox(), 'volume', 'volume') + + def plot_all(self, redraw=True, xMin=0, xMax=-1): + """ + 重画所有界面 + redraw :False=重画最后一根K线; True=重画所有 + xMin,xMax : 数据范围 + """ + + xMax = len(self.datas) if xMax < 0 else xMax + self.countK = xMax - xMin + self.index = int((xMax + xMin) / 2) # 设置当前索引所在位置为数据的中心点 + self.pi_sub.setLimits(xMin=xMin, xMax=xMax) + self.pi_main.setLimits(xMin=xMin, xMax=xMax) + self.plot_kline(redraw, xMin, xMax) # K线图 + + if self.display_vol: + self.pi_volume.setLimits(xMin=xMin, xMax=xMax) + self.plot_volume(redraw, xMin, xMax) # K线副图,成交量 + + self.plot_sub(0, len(self.datas)) # K线副图,持仓量 + self.refresh() + + def refresh(self): + """ + 刷新三个子图的显示范围 + """ + # 计算界面上显示数量的最小x/最大x + minutes = int(self.countK / 2) + xmin = max(0, self.index - minutes) + xmax = xmin + 2 * minutes + + # 更新主图/副图/成交量的 x范围 + self.pi_sub.setRange(xRange=(xmin, xmax)) + self.pi_main.setRange(xRange=(xmin, xmax)) + self.pi_volume.setRange(xRange=(xmin, xmax)) + + # 快捷键相关 + + def onNxt(self): + """跳转到下一个开平仓点""" + try: + if len(self.x_t_trade_map) > 0 and self.index is not None: + datalen = len(self.datas) + self.index += 1 + while self.index < datalen and self.index in self.x_t_trade_map: + self.index += 1 + self.refresh() + x = self.index + y = self.datas[x]['close'] + self.crosshair.signal.emit((x, y)) + except Exception as ex: + print(u'{} onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def onPre(self): + """跳转到上一个开平仓点""" + try: + if len(self.x_t_trade_map) > 0 and self.index: + self.index -= 1 + while self.index > 0 and self.index in self.x_t_trade_map: + self.index -= 1 + self.refresh() + x = self.index + y = self.datas[x]['close'] + self.crosshair.signal.emit((x, y)) + except Exception as ex: + print(u'{}.onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def onDown(self): + """放大显示区间""" + try: + self.countK = min(len(self.datas), int(self.countK * 1.2) + 1) + self.refresh() + if len(self.datas) > 0: + x = self.index - self.countK / 2 + 2 if int( + self.crosshair.xAxis) < self.index - self.countK / 2 + 2 else int(self.crosshair.xAxis) + x = self.index + self.countK / 2 - 2 if x > self.index + self.countK / 2 - 2 else x + x = int(x) + y = self.datas[x][2] + self.crosshair.signal.emit((x, y)) + print(u'onDown:countK:{},x:{},y:{},index:{}'.format(self.countK, x, y, self.index)) + except Exception as ex: + print(u'{}.onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def onUp(self): + """缩小显示区间""" + try: + # 减少界面显示K线数量 + self.countK = max(3, int(self.countK / 1.2) - 1) + self.refresh() + if len(self.datas) > 0: + x = self.index - int(self.countK / 2) + 2 if int(self.crosshair.xAxis) < self.index - int( + self.countK / 2) + 2 else int(self.crosshair.xAxis) + x = self.index + int(self.countK / 2) - 2 if x > self.index + (self.countK / 2) - 2 else x + x = int(x) + y = self.datas[x]['close'] + self.crosshair.signal.emit((x, y)) + print(u'onUp:countK:{},x:{},y:{},index:{}'.format(self.countK, x, y, self.index)) + except Exception as ex: + print(u'{}.onDown() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def onLeft(self): + """向左移动""" + try: + if len(self.datas) > 0 and int(self.crosshair.xAxis) > 2: + x = int(self.crosshair.xAxis) - 1 + y = self.datas[x]['close'] + if x <= self.index - self.countK / 2 + 2 and self.index > 1: + self.index -= 1 + self.refresh() + self.crosshair.signal.emit((x, y)) + + print(u'onLeft:countK:{},x:{},y:{},index:{}'.format(self.countK, x, y, self.index)) + except Exception as ex: + print(u'{}.onLeft() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def onRight(self): + """向右移动""" + try: + if len(self.datas) > 0 and int(self.crosshair.xAxis) < len(self.datas) - 1: + x = int(self.crosshair.xAxis) + 1 + y = self.datas[x]['close'] + if x >= self.index + int(self.countK / 2) - 2: + self.index += 1 + self.refresh() + self.crosshair.signal.emit((x, y)) + except Exception as ex: + print(u'{}.onLeft() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def onDoubleClick(self, pos): + """ + 鼠标双击事件 + :param pos: + :return: + """ + try: + if len(self.datas) > 0 and int(self.crosshair.xAxis) >= 0: + x = int(self.crosshair.xAxis) + time_value = self.axisTime.xdict.get(x, None) + self.index = x + + print(u'{} doubleclick: {},x:{},index:{}'.format(self.title, time_value, x, self.index)) + + if self.relocate_notify_func is not None and time_value is not None: + self.relocate_notify_func(self.windowId, time_value, self.countK) + except Exception as ex: + print(u'{}.onDoubleClick() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + def relocate(self, window_id, t_value, count_k): + """ + 重定位到最靠近t_value的x坐标 + :param window_id: + :param t_value: + :param count_k + :return: + """ + if self.windowId == window_id or count_k < 2: + return + + try: + x_value = self.axisTime.get_x_by_time(t_value) + self.countK = count_k + + if 0 < x_value <= len(self.datas): + self.index = x_value + x = self.index + y = self.datas[x]['close'] + self.refresh() + self.crosshair.signal.emit((x, y)) + print(u'{} reloacate to :{},{}'.format(self.title, x, y)) + except Exception as ex: + print(u'{}.relocate() exception:{},trace:{}'.format(self.title, str(ex), traceback.format_exc())) + + # 界面回调相关 + + def onPaint(self): + """界面刷新回调""" + view = self.pi_main.getViewBox() + vRange = view.viewRange() + xmin = max(0, int(vRange[0][0])) + xmax = max(0, int(vRange[0][1])) + self.index = int((xmin + xmax) / 2) + 1 + + def resignData(self, datas): + """更新数据,用于Y坐标自适应""" + self.crosshair.datas = datas + + def viewXRangeChanged(low, high, self): + vRange = self.viewRange() + xmin = max(0, int(vRange[0][0])) + xmax = max(0, int(vRange[0][1])) + xmax = min(xmax, len(datas)) + if len(datas) > 0 and xmax > xmin: + ymin = min(datas[xmin:xmax][low]) + ymax = max(datas[xmin:xmax][high]) + self.setRange(yRange=(ymin, ymax)) + else: + self.setRange(yRange=(0, 1)) + + view = self.pi_main.getViewBox() + view.sigXRangeChanged.connect(partial(viewXRangeChanged, 'low', 'high')) + + if self.display_vol: + view = self.pi_volume.getViewBox() + view.sigXRangeChanged.connect(partial(viewXRangeChanged, 'volume', 'volume')) + if self.display_sub: + view = self.pi_sub.getViewBox() + # view.sigXRangeChanged.connect(partial(viewXRangeChanged,'openInterest','openInterest')) + view.setRange(yRange=(0, 100)) + + # 数据相关 + + def clearData(self): + """清空数据""" + # 清空数据,重新画图 + self.time_index = [] + self.listBar = [] + self.listVol = [] + + self.list_trade_arrow = [] + self.x_t_trade_map = OrderedDict() + self.t_trade_dict = OrderedDict() + + self.list_trans = [] + self.list_trans_lines = [] + + self.list_markup = [] + self.x_t_markup_map = OrderedDict() + self.t_markup_dict = OrderedDict() + + # 清空主图指标 + self.main_indicator_data = {} + # 清空副图指标 + self.sub_indicator_data = {} + + self.datas = None + + def clear_indicator(self, main=True): + """清空指标图形""" + # 清空信号图 + if main: + for indicator in self.main_indicator_plots: + self.pi_main.removeItem(self.main_indicator_plots[indicator]) + self.main_indicator_data = {} + self.main_indicator_plots = {} + else: + for indicator in self.sub_indicator_plots: + self.pi_sub.removeItem(self.sub_indicator_plots[indicator]) + self.sub_indicator_data = {} + self.sub_indicator_plots = {} + + def onBar(self, bar, main_indicator_datas, sub_indicator_datas, nWindow=20, inited=False): + """ + 新增K线数据,K线播放模式 + + :param bar: dict + :param main_indicator_datas: + :param sub_indicator_datas: + :param nWindow: + :return: nWindow : 最大数据窗口 + """ + bar_datetime = bar.get('datetime', '') + try: + bar_datetime = datetime.strptime(bar_datetime, '%Y-%m-%d %H:%M:%S') + except: # noqa + bar_datetime = datetime.now() + bar_open = bar.get('open', 0) + bar_close = bar.get('close', 0) + bar_high = bar.get('high', 0) + bar_low = bar.get('low', 0) + bar_volume = bar.get('volume', 0) + bar_openInterest = bar.get('openInterest') + if bar_openInterest == np.inf or bar_openInterest == -np.inf: + bar_openInterest = np.random.randint(0, 3) + + # 是否需要更新K线 + newBar = False if self.datas and bar_datetime == self.datas[-1].datetime else True + nrecords = len(self.datas) if newBar else len(self.datas) - 1 + + recordVol = (nrecords, bar_volume, 0, 0, bar_volume) if bar_close < bar_open else ( + nrecords, 0, bar_volume, 0, bar_volume) + + if newBar and any(self.datas): + # 主图数据增加一项 + self.datas.resize(nrecords + 1, refcheck=0) + self.listBar.resize(nrecords + 1, refcheck=0) + # 成交量指标,增加一项 + self.listVol.resize(nrecords + 1, refcheck=0) + + # 主图指标,增加一项 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator, []) + indicator_data.append(0) + + # 副图指标,增加一行 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data.append(0) + + elif any(self.datas): + + # 主图指标,移除第一项 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator, []) + indicator_data.pop() + + # 副图指标,移除第一项 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data.pop() + + if any(self.datas): + self.datas[-1] = (bar_datetime, bar_open, bar_close, bar_low, bar_high, bar_volume, bar_openInterest) + self.listBar[-1] = (nrecords, bar_open, bar_close, bar_low, bar_high) + self.listVol[-1] = recordVol + + # 主图指标,更新最后记录 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator, []) + indicator_data[-1] = main_indicator_datas.get(indicator, 0) + + # 副图指标,更新最后记录 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data[-1] = sub_indicator_datas.get(indicator, 0) + + else: + self.datas = np.rec.array( + [(datetime, bar_open, bar_close, bar_low, bar_high, bar_volume, bar_openInterest)], + names=('datetime', 'open', 'close', 'low', 'high', 'volume', 'openInterest')) + self.listBar = np.rec.array([(nrecords, bar_open, bar_close, bar_low, bar_high)], + names=('time_int', 'open', 'close', 'low', 'high')) + self.listVol = np.rec.array([recordVol], names=('time_int', 'open', 'close', 'low', 'high')) + + # 主图指标,添加数据 + for indicator in list(self.main_indicator_data.keys()): + indicator_data = self.main_indicator_data.get(indicator, []) + indicator_data.append(main_indicator_datas.get(indicator, 0)) + + # 副图指标,添加数据 + for indicator in list(self.sub_indicator_data.keys()): + indicator_data = self.sub_indicator_data.get(indicator, []) + indicator_data.append(sub_indicator_datas.get(indicator, 0)) + + self.resignData(self.datas) + + self.axisTime.update_xdict({nrecords: bar_datetime}) + + if 'openInterest' in self.sub_indicator_data: + self.sub_indicator_data['openInterest'].append(bar_openInterest) + + self.resignData(self.datas) + nWindow0 = min(nrecords, nWindow) + xMax = nrecords + 2 + xMin = max(0, nrecords - nWindow0) + if inited: + self.plot_all(False, xMin, xMax) + if not newBar: + self.update_all() + self.index = 0 + self.crosshair.signal.emit((None, None)) + + def add_signal(self, t_value, direction, offset, price, volume): + """ + 增加信号 + :param t_value: + :param direction: + :param offset: + :param price: + :param volume: + :return: + """ + # 找到信号时间最贴近的bar x轴 + x = self.axisTime.get_x_by_time(t_value) + need_plot_arrow = False + + # 修正一下 信号时间,改为bar的时间 + if x not in self.x_t_trade_map: + bar_time = self.axisTime.xdict.get(x, t_value) + else: + # 如果存在映射,就更新 + bar_time = self.x_t_trade_map[x] + + trade_node = self.t_trade_dict.get(bar_time, None) + if trade_node is None: + # 当前时间无交易信号 + self.t_trade_dict[bar_time] = {'x': x, 'signals': [ + {'direction': direction, 'offset': offset, 'price': price, 'volume': volume}]} + self.x_t_trade_map[x] = bar_time + need_plot_arrow = True + else: + # match_signals = [t for t in trade_node['signals'] if t['direction'] == direction and t['offset'] == offset] + # if len(match_signals) == 0: + need_plot_arrow = True + trade_node['signals'].append({'direction': direction, 'offset': offset, 'price': price, 'volume': volume}) + self.x_t_trade_map[x] = bar_time + + # 需要显示图标 + if need_plot_arrow: + arrow = None + # 多信号 + if direction == Direction.LONG: + if offset == Offset.OPEN: + # buy + arrow = pg.ArrowItem(pos=(x, price), angle=135, brush=None, pen={'color': 'r', 'width': 1}, + tipAngle=30, baseAngle=20, tailLen=10, tailWidth=2) + # d = { + # "pos": (x, price), + # "data": 1, + # "size": 14, + # "pen": pg.mkPen((255, 255, 255)), + # "symbol": "t1", + # "brush": pg.mkBrush((255, 255, 0)) + # } + # arrow = pg.ScatterPlotItem() + # arrow.setData([d]) + else: + # cover + arrow = pg.ArrowItem(pos=(x, price), angle=0, brush=(255, 0, 0), pen=None, headLen=20, headWidth=20, + tailLen=10, tailWidth=2) + # 空信号 + elif direction == Direction.SHORT: + if offset == Offset.CLOSE: + # sell + arrow = pg.ArrowItem(pos=(x, price), angle=0, brush=(0, 255, 0), pen=None, headLen=20, headWidth=20, + tailLen=10, tailWidth=2) + else: + # short + arrow = pg.ArrowItem(pos=(x, price), angle=-135, brush=None, pen={'color': 'g', 'width': 1}, + tipAngle=30, baseAngle=20, tailLen=10, tailWidth=2) + if arrow: + self.pi_main.addItem(arrow) + self.list_trade_arrow.append(arrow) + + def add_trades(self, df_trades): + """ + 批量导入交易记录(vnpy回测中导出的trade.csv) + :param df_trades: + :return: + """ + if df_trades is None or len(df_trades) == 0: + print(u'dataframe is None or Empty', file=sys.stderr) + return + + for idx in df_trades.index: + # 时间 + trade_time = df_trades['time'].loc[idx] + if not isinstance(trade_time, datetime) and isinstance(trade_time, str): + trade_time = datetime.strptime(trade_time, '%Y-%m-%d %H:%M:%S') + + price = df_trades['price'].loc[idx] + direction = df_trades['direction'].loc[idx] + if direction.lower() in ['long', 'direction.long']: + direction = Direction.LONG + else: + direction = Direction.SHORT + offset = df_trades['offset'].loc[idx] + if offset.lower() in ['open', 'offset.open']: + offset = Offset.OPEN + else: + offset = Offset.CLOSE + + volume = df_trades['volume'].loc[idx] + + # 添加开仓信号 + self.add_signal(t_value=trade_time, direction=direction, offset=offset, price=price, + volume=volume) + + def add_signals(self, df_trade_list): + """ + 批量导入交易记录(vnpy回测中导出的trade_list.csv) + :param df_trade_list: + :return: + """ + if df_trade_list is None or len(df_trade_list) == 0: + print(u'dataframe is None or Empty', file=sys.stderr) + return + + for idx in df_trade_list.index: + # 开仓时间 + open_time = df_trade_list['open_time'].loc[idx] + if not isinstance(open_time, datetime) and isinstance(open_time, str): + open_time = datetime.strptime(open_time, '%Y-%m-%d %H:%M:%S') + + open_price = df_trade_list['open_price'].loc[idx] + direction = df_trade_list['direction'].loc[idx] + if direction.lower() == 'long': + open_direction = Direction.LONG + close_direction = Direction.SHORT + else: + open_direction = Direction.SHORT + close_direction = Direction.LONG + + close_time = df_trade_list['close_time'].loc[idx] + if not isinstance(close_time, datetime) and isinstance(close_time, str): + close_time = datetime.strptime(close_time, '%Y-%m-%d %H:%M:%S') + + close_price = df_trade_list['close_price'].loc[idx] + volume = df_trade_list['volume'].loc[idx] + + # 添加开仓信号 + self.add_signal(t_value=open_time, direction=open_direction, offset=Offset.OPEN, price=open_price, + volume=volume) + + # 添加平仓信号 + self.add_signal(t_value=close_time, direction=close_direction, offset=Offset.CLOSE, price=close_price, + volume=volume) + + def add_trans(self, tns_dict): + """ + 添加事务画线 + {'start_time','end_time','tns_type','start_price','end_price','start_x','end_x','completed'} + :return: + """ + if len(self.datas) == 0: + print(u'No datas exist', file=sys.stderr) + return + tns = copy.copy(tns_dict) + + completed = tns.get('completed', False) + end_price = tns.get('end_price', 0) + if not completed: + end_x = len(self.datas) - 1 + end_price = self.datas[end_x]['close'] + tns['end_x'] = end_x + tns['end_price'] = end_price + tns['completed'] = False + else: + tns['end_x'] = self.axisTime.get_x_by_time(tns['end_time']) + + tns['start_x'] = self.axisTime.get_x_by_time(tns['start_time']) + # 将上一个线段设置为True + if len(self.list_trans) > 0: + self.list_trans[-1]['completed'] = True + pos = np.array([[tns['start_x'], tns['start_price']], [tns['end_x'], tns['end_price']]]) + tns_type = tns.get('tns_type', None) + if tns_type == Direction.LONG: + pen = pg.mkPen({'color': 'r', 'width': 1}) + elif tns_type == Direction.SHORT: + pen = pg.mkPen({'color': 'g', 'width': 1}) + else: + pen = 'default' + tns_line = pg.GraphItem(pos=pos, adj=np.array([[0, 1]]), pen=pen) + self.pi_main.addItem(tns_line) + self.list_trans.append(tns) + + def add_trans_df(self, df_trans): + """ + 批量增加,多空的切换 + :param df_trans: + :return: + """ + if df_trans is None or len(df_trans) == 0: + print(u'dataframe is None or Empty', file=sys.stderr) + return + + for idx in df_trans.index: + if idx == 0: + continue + # 事务开始时间 + start_time = df_trans['datetime'].loc[idx - 1] + if not isinstance(start_time, datetime) and isinstance(start_time, str): + start_time = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') + + end_time = df_trans['datetime'].loc[idx] + if not isinstance(end_time, datetime) and isinstance(end_time, str): + end_time = datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S') + + tns_type = df_trans['direction'].loc[idx - 1] + tns_type = Direction.LONG if tns_type.lower() == 'long' else Direction.SHORT + + start_price = df_trans['price'].loc[idx - 1] + end_price = df_trans['price'].loc[idx] + start_x = self.axisTime.get_x_by_time(start_time) + end_x = self.axisTime.get_x_by_time(end_time) + + self.add_trans({'start_time': start_time, 'end_time': end_time, + 'start_price': start_price, 'end_price': end_price, + 'start_x': start_x, 'end_x': end_x, + 'tns_type': tns_type, 'completed': True}) + + def add_markup(self, t_value, price, txt): + """ + 添加标记 + :param t_value: 时间-》坐标x + :param price: 坐标y + :param txt: 文字 + :return: + """ + # 找到信号时间最贴近的bar x轴 + x = self.axisTime.get_x_by_time(t_value) + + # 修正一下 标记时间,改为bar的时间 + if x not in self.x_t_markup_map: + bar_time = self.axisTime.xdict.get(x, t_value) + else: + # 如果存在映射,就更新 + bar_time = self.x_t_markup_map[x] + + markup_node = self.t_markup_dict.get(bar_time, None) + if markup_node is None: + # 当前时间无标记 + markup_node = {'x': x, 'markup': [txt]} + self.t_markup_dict[bar_time] = markup_node + self.x_t_markup_map[x] = bar_time + else: + if '.' in txt: + txt_list = txt.split('.') + else: + txt_list = [txt] + + for t in txt_list: + if t in markup_node['markup']: + continue + markup_node['markup'].append(t) + + if 'textitem' in markup_node: + markup_node['textitem'].setText(';'.join(markup_node.get('markup', []))) + else: + textitem = pg.TextItem(markup_node['markup'][0]) + textitem.setPos(x, price) + markup_node['textitem'] = textitem + self.list_markup.append(textitem) + self.pi_main.addItem(textitem) + + def add_markups(self, df_markup, include_list=[], exclude_list=[]): + """ + 批量增加标记 + :param df_markup: Dataframe(datetime, price, markup) + :param include_list: 如果len(include_list)>0,只显示里面的内容 + :param exclude_list: 如果exclude_list里面存在,不显示 + :return: + """ + if df_markup is None or len(df_markup) == 0: + print(u'df_markup is None or Empty', file=sys.stderr) + return + + for idx in df_markup.index: + # 标记时间 + t_value = df_markup['datetime'].loc[idx] + if not isinstance(t_value, datetime) and isinstance(t_value, str): + t_value = datetime.strptime(t_value, '%Y-%m-%d %H:%M:%S') + + price = df_markup['price'].loc[idx] + markup_text = df_markup['markup'].loc[idx] + if '.' in markup_text: + markup_texts = markup_text.split('.') + else: + markup_texts = [markup_text] + + for txt in markup_texts: + if len(include_list) > 0 and markup_text not in include_list: + continue + + if len(exclude_list) > 0 and markup_text in exclude_list: + continue + + self.add_markup(t_value=t_value, price=price, txt=markup_text) + + def loadData(self, df_datas, main_indicators=[], sub_indicators=[]): + """ + 载入pandas.DataFrame数据 + :param df_datas:DataFrame数据格式,cols : datetime, open, close, low, high, ,,indicator,indicator2,indicator,,, + :param main_indicators: 主图的indicator list + :param sub_indicators: 副图的indicator list + :return: + """ + # 设置中心点时间 + self.index = 0 + # 绑定数据,更新横坐标映射,更新Y轴自适应函数,更新十字光标映射 + if 'open_interest' not in df_datas.columns: + df_datas['open_interest'] = 0 + df_datas['time_int'] = np.array(range(len(df_datas.index))) + self.datas = df_datas[['open', 'close', 'low', 'high', 'volume', 'open_interest']].to_records() + self.axisTime.xdict = {} + xdict = dict(enumerate(df_datas.index.tolist())) + self.axisTime.update_xdict(xdict) + self.resignData(self.datas) + # 更新画图用到的数据 + self.listBar = df_datas[['time_int', 'open', 'close', 'low', 'high']].to_records(False) + + # 成交量颜色和涨跌同步,K线方向由涨跌决定 + datas0 = pd.DataFrame() + datas0['open'] = df_datas.apply(lambda x: 0 if x['close'] >= x['open'] else x['volume'], axis=1) + datas0['close'] = df_datas.apply(lambda x: 0 if x['close'] < x['open'] else x['volume'], axis=1) + datas0['low'] = 0 + datas0['high'] = df_datas['volume'] + datas0['time_int'] = np.array(range(len(df_datas.index))) + self.listVol = datas0[['time_int', 'open', 'close', 'low', 'high']].to_records(False) + + for indicator in main_indicators: + list_indicator = list(df_datas[indicator]) + self.main_indicator_data[indicator] = list_indicator + for indicator in sub_indicators: + list_indicator = list(df_datas[indicator]) + self.sub_indicator_data[indicator] = list_indicator + + # 调用画图函数 + self.plot_all(redraw=True, xMin=0, xMax=len(self.datas)) + self.crosshair.signal.emit((None, None)) + print('finished load Data') diff --git a/vnpy/trader/ui/kline/kline_widgets.py b/vnpy/trader/ui/kline/kline_widgets.py new file mode 100644 index 00000000..cd1e17c5 --- /dev/null +++ b/vnpy/trader/ui/kline/kline_widgets.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +""" +基于uiKline扩展得widgets +华富资产/李来佳 +""" + +import os +import json +import traceback +import pandas as pd +from threading import Thread +from time import sleep +from pymongo import MongoClient +from pymongo.errors import ConnectionFailure, AutoReconnect +from vnpy.trader.ui.kline.kline import QtWidgets, KLineWidget +from vnpy.amqp.consumer import subscriber + + +class RenkoKline(QtWidgets.QWidget): + """ + 砖型图得显示 + 1、显示 + 2、数据加载:从Mongodb,从本地文件 + 3、定时数据更新 + 4、加载文件时,选择指标 + """ + + def __init__(self, parent=None, setting={}): + self.parent = parent + super(RenkoKline, self).__init__(parent) + + self.kline_name = setting.get('kline_name', 'kline_name') + + self.canvas = None + + self.main_indicators = sorted(setting.get('main_indicators', [])) + self.sub_indicators = sorted(setting.get('sub_indicators', [])) + + # 数据来自文件 + self.csv_file = setting.get('csv_file', None) + + # 数据来自mongodb + self.host = None + self.port = None + self.db = None + self.collection = None + + self.sub = None + live_setting = setting.get('live', {}) + if len(live_setting) > 0: + try: + self.sub = subscriber(host=live_setting.get('host', 'localhost'), + port=live_setting.get('port', 5672), + user=live_setting.get('user', 'admin'), + password=live_setting.get('pasword', 'admin'), + exchange=live_setting.get('exchange', 'x_fanout'), + routing_key=live_setting.get('routing_key', 'default')) + + self.sub.set_callback(self.on_bar_message) + except Exception as ex: + print(u'创建消息订阅失败:{}'.format(str(ex))) + traceback.print_exc() + + # MongoDB数据库相关 + self.dbClient = None # MongoDB客户端对象 + self.db_has_connected = False + + self.is_loaded = False + self.last_bar_dt = None + + self.initUI() + + if self.csv_file is not None and os.path.exists(self.csv_file): + self.datathread = Thread(self.load_csv) + self.datathread.start() + + elif 'mongo' in setting: + mongo_setting = setting.get('mongo') + self.host = mongo_setting.get('ip', 'localhost') + self.port = mongo_setting.get('port', 27017) + self.db = mongo_setting.get('db', None) + self.collection = mongo_setting.get('collection', None) + self.kline_name = self.collection + + if self.db is not None and self.collection is not None: + self.dbConnect() + self.datathread = Thread(target=self.load_db, args=()) + self.datathread.start() + + def initUI(self): + """ + 初始化界面 + :return: + """ + vbox = QtWidgets.QVBoxLayout() + self.canvas = KLineWidget(display_vol=False, display_sub=True) + self.canvas.show() + self.canvas.KLtitle.setText('{}'.format(self.kline_name), size='18pt') + + for indicator in self.main_indicators: + self.canvas.add_indicator(indicator=indicator, is_main=True) + + for indicator in self.sub_indicators: + self.canvas.add_indicator(indicator=indicator, is_main=False) + + vbox.addWidget(self.canvas) + self.setLayout(vbox) + + def load_csv(self): + """从文件加载renko数据""" + if self.canvas: + df = pd.read_csv(self.csv_file) + df = df.set_index(pd.DatetimeIndex(df['datetime'])) + self.canvas.loadData(df, main_indicators=self.main_indicators, sub_indicators=self.sub_indicators) + self.is_loaded = True + + if self.sub: + self.sub.start() + + def load_db(self): + """从数据库加载renko数据""" + try: + d = {} + qryData = self.dbQueryBySort(dbName=self.db, collectionName=self.collection, d=d, sortName='datetime', + sortType=1) + bars = [] + for data in qryData: + bar = { + 'datetime': data.get('datetime'), + 'open': data.get('open'), + 'close': data.get('close'), + 'high': data.get('high'), + 'low': data.get('low'), + 'openInterest': 0, + 'volume': data.get('volume', 0) + } + bars.append(bar) + self.last_bar_dt = data.get('datetime') + + print(u'一共从数据库加载{}根bar'.format(len(bars))) + df = pd.DataFrame(bars) + df = df.set_index(pd.DatetimeIndex(df['datetime'])) + self.canvas.loadData(df, main_indicators=self.main_indicators, sub_indicators=self.sub_indicators) + + self.is_loaded = True + if self.sub: + self.sub.start() + + except Exception as ex: + print(u'加载bar异常:{}'.format(str(ex))) + + def on_bar_message(self, chan, method_frame, _header_frame, body, userdata=None): + try: + str_bar = body.decode('utf-8') + bar = json.loads(str_bar) + bar_name = bar.get('bar_name') + + if bar_name == self.kline_name: + print(u'on_bar_message from rabbitmq :{}'.format(bar)) + self.canvas.on_bar(bar, [], []) + + except Exception as ex: + print(u'on_bar_message Exception:{}'.format(str(ex))) + traceback.print_exc() + + def dbConnect(self): + try: + self.dbClient = MongoClient(self.host, self.port, connectTimeoutMS=500) + except Exception as ex: + print(u'连接Mongodb:{} {} 异常:{}'.format(self.host, self.port, str(ex))) + + def dbQueryBySort(self, dbName, collectionName, d, sortName, sortType, limitNum=0): + """从MongoDB中读取数据,d是查询要求,sortName是排序的字段,sortType是排序类型 + 返回的是数据库查询的指针""" + try: + if self.dbClient: + db = self.dbClient[dbName] + collection = db[collectionName] + if limitNum > 0: + cursor = collection.find(d).sort(sortName, sortType).limit(limitNum) + else: + cursor = collection.find(d).sort(sortName, sortType) + if cursor: + return list(cursor) + else: + return [] + else: + print(u'数据库查询出错') + if self.db_has_connected: + self.writeLog(u'重新尝试连接数据库') + self.dbConnect() + except AutoReconnect as ex: + print(u'数据库连接断开重连:{}'.format(str(ex))) + sleep(1) + except ConnectionFailure: + self.dbClient = None + print(u'数据库连接断开') + if self.db_has_connected: + print(u'重新尝试连接数据库') + self.dbConnect() + except Exception as ex: + self.writeError(u'dbQueryBySort exception:{}'.format(str(ex))) + + return [] + + +######################################################################## +class MultiKlineWindow(QtWidgets.QMainWindow): + """多窗口显示K线 + 包括: + + """ + + def __init__(self, parent=None, settings=[]): + """Constructor""" + super(MultiKlineWindow, self).__init__(parent) + + self.settings = settings + self.kline_dict = {} + self.initUi() + + self.load_multi_kline() + + def initUi(self): + """初始化界面""" + self.setWindowTitle(u'多K线') + self.maximumSize() + self.mdi = QtWidgets.QMdiArea() + self.setCentralWidget(self.mdi) + + # 创建菜单 + menubar = self.menuBar() + file_menu = menubar.addMenu("File") + file_menu.addAction("Cascade") + file_menu.addAction("Tiled") + file_menu.triggered[QtWidgets.QAction].connect(self.windowaction) + + def windowaction(self, q): + if q.text() == "cascade": + self.mdi.cascadeSubWindows() + + if q.text() == "Cascade": + self.mdi.tileSubWindows() + + def load_multi_kline(self): + """加载多周期窗口""" + + try: + for setting in self.settings: + sub_window = QtWidgets.QMdiSubWindow() + sub_window.setWindowTitle(setting.get('kline_name')) + renko_kline = RenkoKline(parent=self, setting=setting) + sub_window.setWidget(renko_kline) + self.mdi.addSubWindow(renko_kline) + # renko_kline.loadData() + renko_kline.show() + + self.mdi.tileSubWindows() + + except Exception as ex: # noqa + traceback.print_exc() + QtWidgets.QMessageBox.warning(self, 'Exception', u' Exception', QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.NoButton) + + return + + def closeEvent(self, event): + """关闭窗口时的事件""" + os._exit(0)