使用 PyQt5 和 Echarts 打造股票数据看板

点击上方“编程派”,选择设为“设为星标”

优质文章,第一时间送达!

作者:BuyiXiao

出处:月小水长公众号

在一篇论文中,最吸引审稿人目光的莫过于枯燥的文字间精美的图表。在一份项目路演 ppt 中,酷炫的财务报表往往是打动投资人的最后一剂强心剂

作为数据分析最后也是最直接的一环,数据可视化的重要性不言而喻

数据可视化大致可分为两类,一类是 excel、powerBI 这类不需要写代码的,另一类是需要写代码的;而对于 Python 来说,数据可视化框架,我个人觉得大致可以分为以下两类(推荐程度从高到底)

1、如果对于数据交互性没有特殊要求的话,首推 matplotlib + seaborn ,其中 matplotlib 中成熟而强大的绘图 api 应有尽有,seaborn 相当于调色笔,修改 matplotlib 本身的主题、配色风格等;matplotlib 的另一大优势是可以结合 pandas 快速喂入数据。

2、如果对数据交互性要求高,需要点击图表查看数据,首推 pyecharts;如果还喜欢二次元可爱风的话,可以用 cutecharts, cutecharts 和 pycharts 均基于百度主导的 JavaScript 可视化框架 Echarts。

可以看到,cutecharts 绘制的图表比较 Q(显然不能做正式数据报表和论文图表),当鼠标悬停到天线原理这一列时,会显示出学生 A 和 B 的成绩,但是 cutecharts 生成的是一个 HTML 文件,需要用浏览器打开才能显示图表,而 Python 第三方 GUI 库 PyQt5 实现了浏览器组件 QtWebEngineWidgets,结合 Echarts 的 JavaScript API 就能不打开浏览器实现酷炫的数据交互效果。

实现效果

今天要讲的主题就是使用 PyQt5+Echarts 实现股票数据看板,股票数据采集自网上公开接口,考虑到网易财经历史数据全但有延时,Tushare 数据更新快颗粒度高但调用次数有限制,融合使用网易财经和 Tushare,爬虫这部分代码不是今天的主题,可以跳过,且所有代码均已上传,关注本公众号并在后台回复 股票 即可获得所有代码(包括爬虫+可视化)的下载链接。

界面布局

如上图所示,界面可细分为三大块,左上角的昨日股票涨跌行情饼状图,右上角的展示股票排行榜的 QTabWidget 表格,以及下方的某只股票 Open-Close-High-Low 折线图。上图中,考虑到计算量的问题,饼状图和表格的数据都是直接伪造的,只有股票的折线图数据是真实的。

整个界面继承自 QMainWindow,最外层的布局是竖直布局 QVBoxLayout,它包含界面上部分的 QHBoxLayout 和下方的 QHBoxLayout,并同时设置这两个 QHBoxLayout 的 拉伸因子为 1,这样就能够实现上下部分等分整个界面并大小随界面自适应改变,其语法格式是

vbox = QVBoxLayout()
vbox.addLayout(QHBoxLayout())
vbox.addLayout(QHBoxLayout())
# 第一个参数表示 vbox 中组件的序号,也就是添加顺序
# 第二个参数 表示组件在 vbox 中的权重
vbox.setStretch(0,1)
vbox.setStretch(1,1)

从小的方面来说,左上角和下部分的布局都是 PyQt5 中的 QtWebEngineWidgets 组件,它就像一个浏览器,通过 QtWebEngineWidgets 调用 Echarts 中的 API,就能在 PyQt5 的界面中显示 Echarts 各种各样的的图表。而右上角是一个 QTabWidget 组件,为了减少代码之间的耦合,我单独把它写成一个 RightTableView 类,

# -*- coding: utf-8 -*-
# author:           inspurer(月小水长)
# pc_type           lenovo
# create_time:      2019/12/18 21:54
# file_name:        rightview.py
# github            https://github.com/inspurer
# qq邮箱            2391527690@qq.com
# 微信公众号         月小水长(ID: inspurer)

import sys
from PyQt5.QtWidgets import QApplication,QWidget,QVBoxLayout,QTabWidget,QLabel,QTableWidget,QAbstractItemView,QTableWidgetItem
from PyQt5.QtCore import Qt

class RightTableView(QWidget):
    def __init__(self):
        super().__init__()
        self.mainLayout = QVBoxLayout()
        tabWidgets = QTabWidget()
        label = QLabel("前一日涨幅排名前十的股票详细信息")
        tabWidgets.addTab(label, "涨幅排名")
        label = QLabel("前一日成交量排名前十的股票详细信息")
        tabWidgets.addTab(label, "成交量排名")
        tabWidgets.currentChanged['int'].connect(self.tabClicked)   # 绑定标签点击时的信号与槽函数
        self.mainLayout.addWidget(tabWidgets)
        self.tableView = QTableWidget()
        self.table = QTableWidget(self)
        self.table.setColumnCount(6)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)  # 设置表格的选取方式是行选取
        self.table.setSelectionMode(QAbstractItemView.SingleSelection)  # 设置选取方式为单个选取
        self.table.setHorizontalHeaderLabels(["股票代码", "开盘", "收盘",'最高','最低','成交量'])  # 设置行表头
        self.mainLayout.addWidget(self.table)
        self.mainLayout.setStretch(0,1)
        self.mainLayout.setStretch(1,12)
        self.setLayout(self.mainLayout)
        self.updateView()

    def updateView(self):
        self.table.insertRow(0)

        stock_code = QTableWidgetItem("1001")
        stock_code.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)  
        stock_code.setTextAlignment(Qt.AlignCenter)

        stock_open = QTableWidgetItem("10.20")  
        stock_open.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)  
        stock_open.setTextAlignment(Qt.AlignCenter)

        stock_close = QTableWidgetItem("10.20")  
        stock_close.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)  
        stock_close.setTextAlignment(Qt.AlignCenter)

        stock_high = QTableWidgetItem("10.20")  
        stock_high.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) 
        stock_high.setTextAlignment(Qt.AlignCenter)

        stock_low = QTableWidgetItem("10.20")  
        stock_low.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)  

        stock_dealNum.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) 
        stock_dealNum.setTextAlignment(Qt.AlignCenter)

        self.table.setItem(0, 0, stock_code)
        self.table.setItem(0, 1, stock_open)
        self.table.setItem(0, 2, stock_close)
        self.table.setItem(0, 3, stock_high)
        self.table.setItem(0, 4, stock_low)
        self.table.setItem(0, 5, stock_dealNum)

    def tabClicked(self,index):
        '''
        tab 监听事件,在此添加业务逻辑
        '''
        print(index)

RightTableView 实现了 tab 的监听,切换不同的 tab 可根据 index 展示不同的数据,因此,在主模块中初始化 RightTableView 类的时候,应当给定 RightTableView 可能用到的所有数据,这样可以避免使用 Signal 信号来进行主模块和 RightTableView 模块的通信。

在主模块中,通过 from rightview import RightTableView
即可引入 RightTableView 布局,其中 rightview 是文件名,RightTableView 是类名,如改行代码爆红(实际上不影响运行),可在项目上右键 Mark Dircectory as -> Sources Root 解决之。

数据驱动

实际上,在三个小布局中,界面上部的两个布局的数据均是伪造的,因为这个数据的采集及计算太过耗时

在 basic 表中,我记录了 5000 支股票的基础信息:股票交易所、股票发行公司、股票代码,上部的两个布局需要这 5000 支股票的整体数据,即 5000 支股票昨日相较于前日的跌涨幅,5000 支股票的跌涨幅度的排名,作为一个客户端软件,我觉得一个操作所能忍受的时延是 3 s 以内,优于接口还有速率限制,粗略计算了一下,这个过程远远超过了 30 s,所以我觉得可行的办法是将这种采集和计算过程部署到服务器,通过设置定时任务执行,客户端每次打开只需要一个简单的 Get 请求即可立即渲染数据。

而下方的股票 Open-Close-High-Low 折线图所需数据的计算量比较小,可直接完成,用户输入股票发行公司,即可返回该公司发行股票的代码,(因为我们一般记住的是股票发行公司而不是股票代码,就行我们往往记住网站的域名而不是 ip 地址),如果数据库中不存在代码该股票的表(表名=发行公司_股票代码),就新建,并抓取指定日期的数据存入该表;如果表存在但是缺少用户想要的数据,则更新数据即可;这样设计的好处是尽可能减少平均操作时延。

再说这个用户输入股票发行公司,即可返回该公司发行股票的代码,乍一看就是一个 key-value 字典,为了减少数据库的操作,在程序初始化过程中,我们需要把 basic 表中的股票数据全部加载进内存,也就是放进字典里,但是由于一个公司可能发现很多股票,但是 Python 内置的字典 一个 key 只能对应一个 value ,我们很容易想到把 value 设计成一个列表,但是这样破环了字典的原子性,假如后面我们新加了一个需要,根据 value 反查 key,也就是说根据股票代码反查股票发行公司,如果设计成列表,这个反查耗时将是巨大的,考虑再三,我在不破坏字典 item 的原子性的前提下,实现了 value 可重复 dict,其本质是一个列表,列表元素为字典,核心思想是把键重复的item分散到不同字典,不过经过封装,对外操作和字典一样,下面是该可重复字典的实现

# -*- coding: utf-8 -*-
# author:           inspurer(月小水长)
# pc_type           lenovo
# create_time:      2019/12/2 12:25
# file_name:        myDict.py
# github            https://github.com/inspurer
# qq邮箱            2391527690@qq.com
# 微信公众号         月小水长(ID: inspurer)

class AllowKeyRepeatDict():
    '''
    自定义允许键重复的字典
    其本质是一个列表,列表元素为字典,核心思想是把键重复的item分散到不同字典
    封装后列表对外操作像字典
    '''
    def __init__(self):
        self.dictList = []

    def add(self,key,value):
        length = len(self.dictList)
        i = 0
        while i<length:
            if not self.dictList[i].get(key,None):
                self.dictList[i][key] = value
                return i
            i += 1
        newDict = {}
        newDict[key] = value
        self.dictList.append(newDict)
        return i

    def delete(self,key):
        '''
        :param key: 根据 key 删除所有 item
        '''
        length = len(self.dictList)
        for i in range(length):
            response = self.dictList[i].pop(key,None)
            if not response:
                break
        # 清除哪些空容器,注意从后往前删,否则会出现下标越界
        while length>0:
            if self.dictList[length-1]=={}:
                del self.dictList[length-1]
            length -= 1


    def query(self,key):
        '''
        :param key: 查询的健
        :return: 由于允许键重复,返回形式是一个列表
        '''
        result = []
        length = len(self.dictList)
        for i in range(length):
            response = self.dictList[i].get(key,None)
            if not response:
                return result
            result.append(response)
        return result

    def __str__(self):
        '''
        :return: 打印整个字典
        '''
        resStr = ''
        length = len(self.dictList)
        if length==0:
            return '该字典为空'
        for i in range(length):
            for k,v in self.dictList[i].items():
                aItem = 'key:{:<8}value:{:<8}\n'.format(k,v)
                resStr += aItem
        return  resStr



if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWin = RightTableView()
    mainWin.show()
    sys.exit(app.exec_())

设计模式

当 QtWebEngineWidgets 需要新建一个图表获取句柄时,它希望屏蔽掉新建的具体细节,我们可以设计一个函数对应一种图表来实现这个功能,但是
我们又不想每次新建图表时去找对应的函数,这个时候可以再设计一个代理函数,告诉这个代理函数我们需要怎样的图表即可获取相应图表的句柄。

# 代理函数
def getOptions(self,type):
    if type==None or type=='K':
        return self.createKlines()
    elif type=='Pie':
        return self.create_pie(v=[3000,600,5000])

# K 图表工具函数
def createKlines(self):
    overlap = Overlap()
    for quote in self.quote_data:
        line = Line(quote['title'])
        print(quote)
        line.add('open',quote['date'],quote['open'],is_smooth=True)
        line.add('close',quote['date'],quote['close'],is_smooth=True)
        line.add('high',quote['date'],quote['high'],is_smooth=True)
        line.add('low',quote['date'],quote['low'],is_smooth=True)

        overlap.add(line)

    snippet = TRANSLATOR.translate(overlap.options)
    options = snippet.as_snippet()
    return options
# 饼图工具函数
def create_pie(self, v):
    pie = Pie()
    pie.add("昨日行情",['涨','平','跌'], v, is_label_show=True)
    snippet = TRANSLATOR.translate(pie.options)
    options = snippet.as_snippet()
    return options                           

以上就是本次话题的所有内容,代码开源,关注本公众号并在后台回复 股票 即可获得所有代码。

回复下方「关键词」,获取优质资源

回复关键词「 pybook03」,立即获取主页君与小伙伴一起翻译的《Think Python 2e》电子版

回复关键词「入门资料」,立即获取主页君整理的 10 本 Python 入门书的电子版

回复关键词「m」,立即获取Python精选优质文章合集

回复关键词「book 数字」,将数字替换成 0 及以上数字,有惊喜好礼哦~

题图:pexels,CC0 授权。

好文章,我在看❤️

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值