PyQT绘制股票K线图

        本文介绍如何使用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())

评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值