本文介绍如何使用PyQtGraph绘制类似东方财富PC软件上的K线图,我们要绘制的K线图包括蜡烛图、移动平均线、交易量柱状图、KDJ(或其它指标)曲线、十字光标、前后翻页等可视化功能,一图胜千言,先上笔者绘制的K线图:

图1 本文绘制的K线图

         图2为东方财富同一天的K线图:

图2 东方财富K线图

        对比可以发现,除了少了些指标曲线(例如MA30,MA60,MA120,MA250;MAVOL1)等,主要的显示功能不能说差不多,只能说一模一样!如果有读者有需要可自行添加所需要的指标曲线。

一. 创建K线图界面主框架

        首先我们在QT Creator中来创建K线图的主体窗口,界面上包含了:

  • 三个widget: Kline widget, volume widget, quota widget用来显示K线图,交易量柱状图,KDJ指标曲线图
  • 3个pushbutton:日线图、周线图、月线图:切换K线周期
  • 三个Label: MA data label, vol label, quota label
  • 5个label显示当前股票的当天股价信息
  • 2个label用于切换股票
  • 2个label:x label、y label用于显示十字光标出现时鼠标所在位置的股价与交易日期
  •         创建上述界面只需要在QtCreator中拖出对应的控件并设置好大小、位置以及样式即可,例如设置边框、背景色、前景色等,以kline widget距离,找到styleSheet:

            设置其属性:

    background-color: rgb(7, 7, 7);
    border: 1px solid;
    border-color: rgb(60, 60, 60);
    border-left-width: 0px;

            其它控件依次类推不再赘述。

    二. 创建K线图绘图类

    2.1 创建绘制蜡烛图的类

            创建一个名为CandlestickItem的类,继承子GraphicsObject类。重写paint, boundingRect方法,并返回绘制的蜡烛图:

    class CandlestickItem(pg.GraphicsObject):
        def __init__(self, data):
            pg.GraphicsObject.__init__(self)
            self.data = data
            self.generatePicture()
    
        def generatePicture(self):
            self.picture = QtGui.QPicture()
            p = QtGui.QPainter(self.picture)
            pg.setConfigOptions(leftButtonPan=False, antialias=False)
            w=0.25
            p.setPen(pg.mkPen('w'))
            p.setBrush(pg.mkBrush('w'))
            
            ma5_lines = GetQuotaLines(quota.MA(self.data['close']))
            p.drawLines(*tuple(ma5_lines))
            p.setPen(pg.mkPen('y'))
            ma10_lines = GetQuotaLines(quota.MA(self.data['close'], 10))
            p.drawLines(*tuple(ma10_lines))
            p.setPen(pg.mkPen(color_table['pink']))
            ma20_lines = GetQuotaLines(quota.MA(self.data['close'], 20))
            p.drawLines(*tuple(ma20_lines))
            # for (i, open, close, min, max) in self.data:
            for i in range(len(self.data['open'])):
                open, close, max, min = self.data['open'][i], self.data['close'][i], self.data['high'][i], self.data['low'][i]
                if open > close:
                    p.setPen(pg.mkPen(color_table['line_desc']))
                    p.setBrush(pg.mkBrush(color_table['line_desc']))
                    p.drawLine(QtCore.QPointF(i, min), QtCore.QPointF(i, max))
                    p.drawRect(QtCore.QRectF(i - w, open, w * 2, close - open))
    
                else:
                    p.setPen(pg.mkPen('r'))
                    if (max != close):
                        p.drawLine(QtCore.QPointF(i, max), QtCore.QPointF(i, close))
                    if (min != open):
                        p.drawLine(QtCore.QPointF(i, open), QtCore.QPointF(i, min))
                    if (close==open):
                        p.drawLine(QtCore.QPointF(i-w, open), QtCore.QPointF(i+w, open))
                    else:
                        p.drawLines(QtCore.QLineF(QtCore.QPointF(i-w, close), QtCore.QPointF(i-w, open)),
                                    QtCore.QLineF(QtCore.QPointF(i-w, open), QtCore.QPointF(i+w, open)),
                                    QtCore.QLineF(QtCore.QPointF(i+w, open), QtCore.QPointF(i+w, close)),
                                    QtCore.QLineF(QtCore.QPointF(i+w, close), QtCore.QPointF(i-w, close)))
            p.end()
    
        def paint(self, p, *args):
            p.drawPicture(0, 0, self.picture)
    
        def boundingRect(self):
            return QtCore.QRectF(self.picture.boundingRect())

            其中初始传入的参数data为pandas frame类型,至少包含'open', 'close', 'high', 'low'字段。

    2.2 设置K线图绘图的属性

            为了达到类似东方财富软件上的绘图效果,需对pyqtgraph进行一些设置。

    设置效果

    代码

    禁止鼠标拖动

    plt.setMouseEnabled(x=False, y=False)

    x轴多显示一些区域

    plt.setXRange(-1, width+1, padding=0)

    显示网格线

    plt.showGrid(x=False, y=True)

    y轴颜色

    plt.getAxis('left').setPen(QtGui.QColor(110,110,110))

    x轴颜色

    plt.getAxis('bottom').setPen(QtGui.QColor(110,110,110))

    y轴刻度颜色

    plt.getAxis('left').setTextPen(QtGui.QColor(110,110,110))

    x轴刻度颜色

    plt.getAxis('bottom').setTextPen(QtGui.QColor(110,110,110))

    y轴范围

    plt.setYRange(min, max, padding=0)

            设置完k线图的属性后,将CandlestickItem添加到plot中,具体代码如下:

    def _setPlotStyle(self, plt, width):
        plt.setMouseEnabled(x=False, y=False) #禁止轴向操作
        plt.setXRange(-1, width + 1, padding=0)
        plt.showGrid(x=False, y=True)
        plt.getAxis('left').setTextPen(color_table['klines'])
        plt.getAxis('left').setPen(color_table['klines'])
        plt.getAxis('bottom').setTextPen(color_table['klines'])
        plt.getAxis('bottom').setPen(color_table['klines'])
    
    def _setYRange(self, plt, high, low, y_margin=0.05):
        delta = high-low
        delta_t = delta*y_margin*0.5
        plt.setYRange(low-delta_t, high+delta_t, padding=0)
        
    def DrawKline(self, show_cursor=False, Xlabel = None, Ylabel = None):
        try:
            item = CandlestickItem(self.data_list)
            self.line_plt = pg.PlotWidget(axisItems={enableMenu=False)
            self._setPlotStyle(self.line_plt, self.t)
            self._setYRange(self.line_plt, data_high, data_low)
            self.line_plt.addItem(item)
            return self.line_plt
        except:
            return pg.PlotWidget()

    2.3 绘制y轴坐标

            使用pyqtgraph默认的坐标轴有几个缺点:坐标刻度不太美观、y轴坐标宽度不固定,与下方要绘制的交易量、指标曲线无法自动对齐x轴,因此我们自己设置y轴刻度并固定它们所有y轴刻度到相同字符宽度。

            经观察发现,东方财富的k线图y轴等分点并不是固定,但总结下来可以发现其刻度线所在的位置都是以1,2,5为基数的,即刻度线的值一定是0.1*10^n, 0.2*10^n, 0.5*10^n中的这些数,因此设计算法如下:

    # 根据最大值最小值将坐标轴划分成Level等份,尽量保证划分点为整数
    def stock_volue_grid(data_high, data_low, level=8):
        assert data_high >= data_low
        grid_value = np.array([0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0,
                        100.0, 200.0, 500.0, 1000.0, 2000.0], dtype=np.float)
        data_diff = data_high - data_low
        grid_diff = data_diff / level
        grid_level_diff = np.abs(grid_diff-grid_value)
        grid_min = np.min(grid_level_diff)
        index = grid_level_diff.tolist().index(grid_min)
        grid = grid_value[index]
        grid_data_high = int((data_high + grid)/ grid) * grid
        grid_data_low = int((data_low) / grid) * grid
        grid_len = int((grid_data_high - grid_data_low)/grid) + 1
        grid_list = [grid_data_low + grid * i for i in range(grid_len)]
        grid_dict = {}
        for x in grid_list:
            x_value = np.round(x, 2)
            grid_dict[x_value] = str(x_value).rjust(8)
        return grid_dict

            再通过pyqtgraph设置y轴刻度线:

    def DrawKline(self, show_cursor=False, Xlabel = None, Ylabel = None):
        try:
            data_high = np.max(self.data_list['high'])
            data_low = np.min(self.data_list['low'])
            y_grid_list=stock_volue_grid(data_high=data_high,data_low=data_low)
            item = CandlestickItem(self.data_list)
            x_stringaxis = pg.AxisItem(orientation='bottom')
            x_stringaxis.setTicks([{}.items()])
            y_stringaxis = pg.AxisItem(orientation='left')
            y_stringaxis.setTicks([y_grid_list.items()])
            self.line_plt = pg.PlotWidget(axisItems=
                {'bottom': x_stringaxis, 'left': y_stringaxis}, 
                enableMenu=False)
            self._setPlotStyle(self.line_plt, self.t)
            self._setYRange(self.line_plt, data_high, data_low)
            self.line_plt.addItem(item)
            return self.line_plt
        except:
            return pg.PlotWidget()

    2.4 绘制十字光标

            为了绘制十字光标,首先要获取鼠标所在的位置和鼠标移出k线图区域的事件。鼠标位置可通过pg.SignalProxy获取:

    pen = pg.mkPen(color_table['klines_cursor'], width=1, style=QtCore.Qt.SolidLine)
    self.vline = pg.InfiniteLine(angle=90, movable=False, pen=pen)  # 创建垂直线
    self.hline = pg.InfiniteLine(angle=0, movable=False, pen=pen)  # 创建水平线
    self.vline.setPos(-100)
    self.hline.setPos(-100)
    self.line_plt.addItem(self.vline, ignoreBounds=True)
    self.line_plt.addItem(self.hline, ignoreBounds=True)
    self.move_slot = pg.SignalProxy(self.line_plt.scene().sigMouseMoved, rateLimit=100, slot=self.PlotCursor)

            PlotCursor是鼠标在绘图区域移动所产生的事件,在该回调函数中设置十字光标的位置:

    def PlotCursor(self, event=None):
        pos = event[0]
        if self.line_plt.sceneBoundingRect().contains(pos):
            mousePoint=self.line_plt.plotItem.vb.mapSceneToView(pos)#转换坐标系
            if -1 < index < len(self.data_list['high']):
                # 设置垂直线条和水平线条的位置组成十字光标
                self.vline.setPos(mousePoint.x())
                self.hline.setPos(mousePoint.y())

            这样十字光标就能随着鼠标移动了,如果鼠标移出了绘图区则发生leaveEvent,下面注册该事件:

    def leaveEvent(self, a0):
        self.vline.setPos(-100)
        self.hline.setPos(-100)
    self.line_plt.leaveEvent = self.leaveEvent

            此时只有光标移动,还需要认为去读数,不太方便,下面设置x,y轴两个label同步移动并显示x,y轴坐标值。

    def PlotCursor(self, event=None):
        pos = event[0]
        if self.line_plt.sceneBoundingRect().contains(pos):
            mousePoint = self.line_plt.plotItem.vb.mapSceneToView(pos)#转换坐标系
            index = int(mousePoint.x() + 0.25)  # 鼠标所处的x轴坐标
            if -1 < index < len(self.data_list['high']):
                trade_date = self.data_list['trade_date'][index]
                # 设置垂直线条和水平线条的位置组成十字光标
                self.vline.setPos(mousePoint.x())
                self.hline.setPos(mousePoint.y())
                # 显示x, y坐标跟随鼠标移动并且居中
                if not self.ylabel is None:
                    self.ylabel.setText(str(round(mousePoint.y(), 2)))
                    self.ylabel.move(0,pos.y()-self.ylabel.geometry().height()/2)
                    if self.ylabel.isHidden():
                        self.ylabel.show()
                if not self.xlabel is None:
                    self.xlabel.setText(trade_date)
                    self.xlabel.move(pos.x() - self.xlabel.geometry().width()/2,
                                    self.xlabel.geometry().y())
                    if self.xlabel.isHidden():
                        self.xlabel.show()
    
    def leaveEvent(self, a0):
        self.vline.setPos(-100)
        self.hline.setPos(-100)
        if not self.ylabel is None:
            self.ylabel.hide()
        if not self.xlabel is None:
            self.xlabel.hide()

    三. 交易量与指标曲线

    3.1 交易量曲线

            同k线图绘制类一样,VolItem类继承自GraphicsObject类,使用QPainter绘制柱形图,需要注意的,drawRect绘制的矩形是填充的,如果要绘制一部分填充,一部分未填充,未填充矩形通过绘制线条实现。

    class VolItem(pg.GraphicsObject):
        def __init__(self, data):
            pg.GraphicsObject.__init__(self)
            self.data = data
            self.generatePicture()
        
        def generatePicture(self):
            self.picture = QtGui.QPicture()
            p = QtGui.QPainter(self.picture)
            pg.setConfigOptions(leftButtonPan=False, antialias=False)
            w=0.25
            for i in range(len(self.data['vol'])):
                open, close, vol = self.data['open'][i], self.data['close'][i], self.data['vol'][i]
                if open > close:
                    p.setPen(pg.mkPen(color_table['line_desc']))
                    p.setBrush(pg.mkBrush(color_table['line_desc']))
                    p.drawRect(QtCore.QRectF(i - w, 0, w * 2, vol))
    
                else:
                    p.setPen(pg.mkPen('r'))
                    p.drawLines(QtCore.QLineF(QtCore.QPointF(i-w, 0), QtCore.QPointF(i-w, vol)),
                                QtCore.QLineF(QtCore.QPointF(i-w, vol), QtCore.QPointF(i+w, vol)),
                                QtCore.QLineF(QtCore.QPointF(i+w, vol), QtCore.QPointF(i+w, 0)),
                                QtCore.QLineF(QtCore.QPointF(i+w, 0), QtCore.QPointF(i-w, 0)))
            p.end()
    
        def paint(self, p, *args):
            p.drawPicture(0, 0, self.picture)
    
        def boundingRect(self):
            return QtCore.QRectF(self.picture.boundingRect())

            交易量是正整数,纵轴从零到最大值等分成3或者4份即可,纵轴刻度获取如下:

    def stock_volume_grid(data_max, data_min=0, level = 3):
        assert data_max >= 0
        overflow = 1
        if data_max>10000:
            data_max = data_max/10000
            overflow = 10000
        grid = np.round(int(data_max - data_min) / level, 2)
        grid_list = [np.round(data_min+grid*i, 2) for i in range(1, level+1)]
        grid_dict = {}
        for x in grid_list:
            grid_dict[x*overflow] = str(x).rjust(8)
        return grid_dict

    3.2 KDJ指标曲线

            将KDJ每天的指标连接起来绘制多条直线就是KDJ曲线了。

    class QuotaItem(pg.GraphicsObject):
        # data's np.array list
        def __init__(self, data, color=None):
            pg.GraphicsObject.__init__(self)
            self.data = data
            if color is None:
                color = ['w' for x in range(len(data))]
            self.color = color
            self.generatePicture()
        
        def generatePicture(self):
            self.picture = QtGui.QPicture()
            p = QtGui.QPainter(self.picture)
            pg.setConfigOptions(leftButtonPan=False, antialias=False)
            for line_i in  range(len(self.data)):
                lines = GetQuotaLines(self.data[line_i])
                p.setPen(pg.mkPen(self.color[line_i]))
                p.drawLines(*tuple(lines))
            p.end()
    
        def paint(self, p, *args):
            p.drawPicture(0, 0, self.picture)
    
        def boundingRect(self):
            return QtCore.QRectF(self.picture.boundingRect())

    来源:亦梦云烟

    物联沃分享整理
    物联沃-IOTWORD物联网 » PyQT绘制股票K线图

    发表评论