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 00000000..1cc84063
Binary files /dev/null and b/vnpy/trader/ui/kline/cta.ico differ
diff --git a/vnpy/trader/ui/kline/kline.py b/vnpy/trader/ui/kline/kline.py
new file mode 100644
index 00000000..04b19b49
--- /dev/null
+++ b/vnpy/trader/ui/kline/kline.py
@@ -0,0 +1,1413 @@
+# flake8: noqa
+"""
+Python K线模块,包含十字光标和鼠标键盘交互
+Support By 量投科技(http://www.quantdo.com.cn/)
+
+修改by:华富资产,李来佳
+20180515 change log:
+1.修改命名规则,将图形划分为 主图(main),副图1(volume),副图2:(sub),能够设置开关显示/关闭 两个副图。区分indicator/signal
+2.修改鼠标滚动得操作,去除focus,增加双击鼠标事件
+3.增加重定位接口,供上层界面调用,实现多周期窗口的时间轴同步。
+
+"""
+
+# Qt相关和十字光标
+import sys
+import traceback
+import copy
+import numpy as np
+import pandas as pd
+import pyqtgraph as pg
+
+from functools import partial
+from datetime import datetime
+from collections import deque, OrderedDict
+from qtpy import QtGui, QtCore, QtWidgets
+
+# 其他
+from vnpy.trader.ui.kline.crosshair import Crosshair
+from vnpy.trader.constant import Direction, Offset
+
+
+########################################################################
+# 键盘鼠标功能
+########################################################################
+class KeyWraper(QtWidgets.QWidget):
+ """键盘鼠标功能支持的窗体元类"""
+
+ # 初始化
+
+ def __init__(self, parent=None):
+ QtWidgets.QWidget.__init__(self, parent)
+
+ # 定时器(for 鼠标双击)
+ self.timer = QtCore.QTimer()
+ self.timer.setInterval(300)
+ self.timer.setSingleShot(True)
+ self.timer.timeout.connect(self.timeout)
+ self.click_count = 0 # 鼠标点击次数
+ self.pos = None # 鼠标点击的位置
+
+ # 激活鼠标跟踪功能
+ self.setMouseTracking(True)
+
+ def timeout(self):
+ """鼠标双击定时检查"""
+ if self.click_count == 1 and self.pos is not None:
+ self.onLClick(self.pos)
+
+ self.click_count = 0
+ self.pos = None
+
+ def keyPressEvent(self, event):
+ """
+ 重载方法keyPressEvent(self,event),即按键按下事件方法
+ :param event:
+ :return:
+ """
+ if event.key() == QtCore.Qt.Key_Up:
+ self.onUp()
+ elif event.key() == QtCore.Qt.Key_Down:
+ self.onDown()
+ elif event.key() == QtCore.Qt.Key_Left:
+ self.onLeft()
+ elif event.key() == QtCore.Qt.Key_Right:
+ self.onRight()
+ elif event.key() == QtCore.Qt.Key_PageUp:
+ self.onPre()
+ elif event.key() == QtCore.Qt.Key_PageDown:
+ self.onNxt()
+ event.accept()
+
+ def mousePressEvent(self, event):
+ """
+ 重载方法mousePressEvent(self,event),即鼠标点击事件方法
+ :param event:
+ :return:
+ """
+ if event.button() == QtCore.Qt.RightButton:
+ self.onRClick(event.pos())
+ elif event.button() == QtCore.Qt.LeftButton:
+ self.click_count += 1
+ if not self.timer.isActive():
+ self.timer.start()
+
+ if self.click_count > 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)