PyQT + ECharts + Tushare : 他们加在一起能让股市多好看

3,323 阅读7分钟

一. 前言

前两天搞清楚了 Echarts 是怎么集成到 PyQT 中的 ,这一篇就来应用一下 ,看看能碰撞出什么样的火花。

这一篇之前有很多前置的知识点 :

只需要十分钟 ,快速体验一把 PyQT6 好不好用

Python : 用 TuShare 构建你自己的量化引擎

PyQt : 图表也能这么秀 ,无缝集成 ECharts

二. 基础图表

3.1 功能及图表的选择

image.png

这个图表是基于基础图形进行二次开发的成果,先来了解一下其中的核心参数 :

// 总共四个参数 : 日期 ,开盘价 ,收盘价 ,最低价 ,最高价
const data0 = splitData([  
  ['2013/6/13', 2190.1, 2148.35, 2126.22, 2190.1]
]);


// 用于构建日期和数据的关联关系 ,可以直接 Copy 过来
function splitData(rawData) {
  const categoryData = [];
  const values = [];
  for (var i = 0; i < rawData.length; i++) {
    categoryData.push(rawData[i].splice(0, 1)[0]);
    values.push(rawData[i]);
  }
  return {
    categoryData: categoryData,
    values: values
  };
}

// 计算移动平均线 , 比如计算5日平均线 :  calculateMA(5)
function calculateMA(dayCount) {
  var result = [];
  // 遍历 data0.values 中的所有数据点,data0.values 应该是一个二维数组
  // 其中每个子数组代表一个数据点,类似 [日期, 数据值]
  for (var i = 0, len = data0.values.length; i < len; i++) {
    if (i < dayCount) {
      result.push('-');
      continue;
    }
    var sum = 0;
    for (var j = 0; j < dayCount; j++) {
      sum += +data0.values[i - j][1];
    }
    result.push(sum / dayCount);
  }
  return result;
}


option = {
  title: {
    text: '上证指数', // 标题名称  
    left: 0  // 标题位置
  },
  tooltip: {
    trigger: 'axis', // 触发类型:坐标轴触发
    axisPointer: {
      type: 'cross'
    }
  },
  legend: {
    // 图例
    data: ['日K', 'MA5', 'MA10', 'MA20', 'MA30']
  },
  grid: { // 设置图表的网格区域(即坐标轴和图表的绘制区域)
    left: '10%',  // 左侧留白的空间
    right: '10%', // 右侧留白的空间 
    bottom: '15%' // 底部留白的空间
  },
  xAxis: {
    type: 'category',  // X轴类型为‘category’,即类目轴,表示数据是离散的类目
    data: data0.categoryData, // 按照日期分类
    boundaryGap: false,
    axisLine: { onZero: false },
    splitLine: { show: false },
    min: 'dataMin',
    max: 'dataMax'
  },
  yAxis: {
    scale: true, //Y轴启用缩放,使得坐标轴的最小值和最大值可以根据数据自动缩放
    splitArea: {
      show: true
    }
  },
  dataZoom: [  // 对图表中的数据进行缩放和平移操作
    {
      type: 'inside', // 使用内部缩放组件(可以直接在图中进行滑动缩放)
      start: 50, // 初始缩放起点(50%)
      end: 100   // 初始缩放终点(100%)
    },
    {
      show: true,  // 外部挂载的横向缩放
      type: 'slider',
      top: '90%', 
      start: 50,
      end: 100
    }
  ],
  series: [ // 定义了图表的主要数据和图表类型
    {
      name: '日K',
      type: 'candlestick', // 数据系列类型为‘candlestick’,表示绘制K线图
      data: data0.values,
      // 用于定义颜色风格 , 此处省略
      itemStyle: {
      },
      
      // 在图表中添加标记点,可以用来标记最大值、最小值
      markPoint: {
        label: {
          formatter: // 控制数据标签的显示样式
        },
        data: [
          // 这里定义了一个 Mark 标记点  
          {
            name: 'Mark',
            coord: ['2013/5/31', 2300], // 标记点的位置在这个点
            value: 2300,  // 标记点的值是2300
            itemStyle: { }  // 标记点的样式 
          },
          //........
        ],
        tooltip: {  } // 系列内部的提示框配置
      },
      // 在图表中添加标记线,可以用来标记平均值、指定的值
      markLine: {
        symbol: ['none', 'none'],
        data: [
          [
            {
              name: 'from lowest to highest',  // 定义了一段标记线
              type: 'min',   
              valueDim: 'lowest',
              symbol: 'circle',
              symbolSize: 10,
              label: { show: false },
              emphasis: {label: {show: false}
              }
            },
            //....
          ]
        ]
      }
    },
    {
      name: 'MA5',
      type: 'line',
      data: calculateMA(5),
      smooth: true,
      lineStyle: {
        opacity: 0.5
      }
    },
    // 省略 MA10 / MA20 / MA30
  ]
};



3.2 Tushare 数据的返回

按照之前的相关代码 ,是可以拿到如下信息 :

import tushare as ts
import pandas as pd
import matplotlib.pyplot as plt

# 设置你的 Token (这里方便以后的Demo放在了TXT文件中)
with open('D:\\code\\python\\tushareToken.txt', 'r', encoding='utf-8') as file:
    token = file.read().strip() 
    ts.set_token(token)
pro = ts.pro_api()

# 获取历史交易数据
df = pro.daily(ts_code='600519.SH', start_date='20240810', end_date='20240915')
print(df)

image.png

  • 这里不仅提供了我们需要的每天的上下交易额 ,还提供了成交量 ,成交额 ,涨跌幅等信息

四. 组合各工具的数据

import sys
import os
import tushare as ts
import pandas as pd
from PyQt6.QtWidgets import QApplication, QMainWindow
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import QUrl


class EChartsWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('ECharts with PyQt6')
        self.setGeometry(100, 100, 1200, 800)

        # 获取股票数据
        self.stock_data = self.get_stock_data()

        # 创建 QWebEngineView 组件
        self.browser = QWebEngineView()

        # 获取当前目录的绝对路径
        current_dir = os.path.dirname(os.path.abspath(__file__))
        local_file_path = os.path.join(current_dir, 'demo.html')

        # 使用 QUrl.fromLocalFile() 加载本地 HTML 文件
        url = QUrl.fromLocalFile(local_file_path)

        # 加载 HTML 文件到 QWebEngineView 中
        self.browser.setUrl(url)

        # 页面加载完成后,执行 JavaScript 方法
        self.browser.loadFinished.connect(self.on_load_finished)

        # 设置浏览器为中央控件
        self.setCentralWidget(self.browser)

    def get_stock_data(self):
        # # 设置你的 Token (这里方便以后的Demo放在了TXT文件中)
        with open('D:\\code\\python\\tushareToken.txt', 'r', encoding='utf-8') as file:
            token = file.read().strip()
            ts.set_token(token)
        pro = ts.pro_api()

        # 获取某支股票的每日行情,示例为 '600519.SH'
        df = pro.daily(ts_code='600519.SH',
                       start_date='20240101', end_date='20241003')

        # 将日期设置为索引并按时间排序
        df.index = pd.to_datetime(df['trade_date'])
        df = df.sort_index()

        # 提取需要的列(日期、开盘、收盘、最高、最低)
        stock_data = df[['open', 'close', 'high', 'low']]

        # 生成 K 线图所需的数组
        kline_data = []
        for row in stock_data.itertuples():
            kline_data.append([row.open, row.close, row.low, row.high])

        # 日期
        dates = stock_data.index.strftime('%Y-%m-%d').tolist()

        # 生成关键点
        point_data = [{
            "name": 'highest value',
            "type": 'max',
            "valueDim": 'highest'
        },
            {
            "name": 'lowest value',
            "type": 'min',
            "valueDim": 'lowest'
        }]

        # 生成平均值
        markLine = self.get_markline_data(stock_data)

        return {
            'dates': dates,
            'kline_data': kline_data,
            'point_data': point_data,
            'markLine': markLine,
        }

    def get_markline_data(self, stock_data):
        # 计算收盘价的平均值
        average_price = stock_data['close'].mean()

        # 生成 markLine 数据
        markline_data = {
            'data': [
                {
                    'yAxis': average_price,  # 将 y 轴的值设置为均值
                    'name': '平均值',
                    'lineStyle': {
                        'color': '#FF0000',  # 线条颜色
                        'type': 'dashed',    # 线条类型(虚线)
                        'width': 2           # 线条宽度
                    },
                    'label': {
                        'formatter': f'平均值: {average_price:.2f}',  # 显示的文本
                        'position': 'end',    # 文本位置
                        'color': '#FF0000'    # 文本颜色
                    }
                }
            ]
        }
        return markline_data

    def on_load_finished(self):
        # 页面加载完成后,调用 JavaScript 的 setData 函数
        js_code = f"setData({self.stock_data['dates']}, {self.stock_data['kline_data']},{self.stock_data['point_data']},{self.stock_data['markLine']});"
        print(f"{js_code}")
        self.browser.page().runJavaScript(js_code, self.js_callback)

    def js_callback(self, result):
        print(f"JavaScript Result: {result}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = EChartsWindow()
    window.show()
    sys.exit(app.exec())

<!DOCTYPE html>
<html style="height: 100%">

<head>
    <meta charset="utf-8">
</head>

<body style="height: 100%; margin: 0">
    <div id="container" style="height: 100%"></div>
    <script src="echarts.min.js"></script>
    <script>
        var app = {};
        var dates = [];
        var klineData = [];
        var mainPointData = [];
        var markLineData = [];

        const upColor = '#ec0000';
        const upBorderColor = '#8A0000';
        const downColor = '#00da3c';
        const downBorderColor = '#008F28';
        
        // 接收 Python 端的数据 ,并且进行图表的绘制
        function setData(datesData, klineDataData, mainPoint, markLine) {
            dates = datesData;
            klineData = klineDataData;
            mainPointData = mainPoint;
            markLineData = markLine;
            drawChart();
        }
        
        // 处理不同的周线 ,月线信息
        function calculateMA(dayCount) {
            var result = [];
            for (var i = 0, len = klineData.length; i < len; i++) {
                if (i < dayCount) {
                    result.push('-');
                    continue;
                }
                var sum = 0;
                for (var j = 0; j < dayCount; j++) {
                    sum += +klineData[i - j][1];
                }
                result.push(sum / dayCount);
            }
            return result;
        }
        
        // 绘制图表
        function drawChart() {
            var dom = document.getElementById('container');
            var myChart = echarts.init(dom, null, {
                renderer: 'canvas',
                useDirtyRect: false
            });

            var option = {
                title: {
                    text: '上证指数',
                    left: 0
                },
                tooltip: {
                    trigger: 'axis',
                    axisPointer: {
                        type: 'cross'
                    }
                },
                legend: {
                    data: ['日K', 'MA5', 'MA10', 'MA20', 'MA30']
                },
                grid: {
                    left: '10%',
                    right: '10%',
                    bottom: '15%'
                },
                xAxis: {
                    type: 'category',
                    data: dates,
                    boundaryGap: false,
                    boundaryGap: false,
                    axisLine: { onZero: false },
                    splitLine: { show: false },
                    min: 'dataMin',
                    max: 'dataMax'
                },
                yAxis: {
                    scale: true,
                    splitArea: {
                        show: true
                    }
                },
                dataZoom: [
                    {
                        type: 'inside',
                        start: 50,
                        end: 100
                    },
                    {
                        show: true,
                        type: 'slider',
                        top: '90%',
                        start: 50,
                        end: 100
                    }
                ],
                series: [
                    {
                        type: 'candlestick',
                        name: '日K',
                        data: klineData,
                        itemStyle: {
                            color: upColor,
                            color0: downColor,
                            borderColor: upBorderColor,
                            borderColor0: downBorderColor
                        },
                        markPoint: {
                            label: {
                                normal: {
                                    formatter: function (param) {
                                        return param != null ? Math.round(param.value) : '';
                                    }
                                }
                            },
                            data: mainPointData
                        },
                        markLine: markLineData
                    },
                    {
                        name: 'MA5',
                        type: 'line',
                        data: calculateMA(5),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5
                        }
                    },
                    {
                        name: 'MA10',
                        type: 'line',
                        data: calculateMA(10),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5
                        }
                    },
                    {
                        name: 'MA20',
                        type: 'line',
                        data: calculateMA(20),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5
                        }
                    },
                    {
                        name: 'MA30',
                        type: 'line',
                        data: calculateMA(30),
                        smooth: true,
                        lineStyle: {
                            opacity: 0.5
                        }
                    }
                ]
            };

            myChart.setOption(option);
        }
    </script>
</body>

</html>

image.png

到了这里图表就算绘制完成了 ,后续还可以计算出一些核心的值 , 比如我这里就设置了 :

  • 设置一个平均线 ,表示平均股价应该在哪里
  • 设置最高点 ,最低点 ,可以标注出多个峰值

总结

    1. 拉取股票信息后 ,需要转换为 EChat 需要的格式
    1. Echat 的核心是一个 Options ,只要在通过一个 setData 把 Options 设置进去 ,就可以实现图表的展示
    • Option 可以整个作为一个模板放在 .json 文件中 ,通过 set 某个变量的方式注入值
    1. 关键能力在 QWebEngineView 上面 ,用来展示网页信息

关于性能方面 :

  • 尝试中 ,感觉并没有太大的延迟 ,主要的耗时还在接口调用股票信息上面
  • 可以把股票信息本地存储就行
  • 整体的流畅度也很高 ,没感觉到有卡顿现象,还是很好用的

最后的最后 ❤️❤️❤️👇👇👇