如何实现一个实时获取股票Tick数据并自动合成1分钟K线数据的框架 实盘用代码用大模型重构后分享 仅供学习

好的,下面是一个基于你提供的代码构建的教程,解释了如何实现一个实时获取股票Tick数据并自动合成1分钟K线数据的框架。


教程:构建实时股票数据获取与1分钟K线合成框架

目标: 本教程旨在指导你如何使用Python构建一个框架,该框架能够:

  1. 实时从指定数据源(如此处代码中的 spider_api)获取股票的逐笔成交数据(Tick数据)。
  2. 在内存中实时将Tick数据聚合成1分钟K线(包含开盘价、最高价、最低价、收盘价、成交量、成交额 - OHLCVA)。
  3. 利用Redis存储生成的1分钟K线数据,区分“已完成”的K线和“正在更新中”的当前K线。

核心技术栈:

  • Python: 主要编程语言。
  • Pandas: 用于高效处理和操作数据。
  • Threading: 用于在后台持续运行数据获取和处理任务,不阻塞主程序。
  • Redis: 一个内存数据库,用于快速存储和读取K线数据,便于其他应用或策略模块实时访问。
  • 自定义模块:
    • spider_api: 假设这是一个封装好的用于获取实时Tick数据的类或函数(你需要自行实现或替换为你实际使用的数据源接口,如 Tushare 的 WebSocket、或其他行情API)。
    • basic.*: 包含基础函数(如时间处理 disp)、数据路径配置、Redis连接等辅助功能。

步骤 1:环境准备与依赖

  1. 安装必要的库:
    pip install pandas numpy redis requests beautifulsoup4 tushare -i https://pypi.tuna.tsinghua.edu.cn/simple
    
  2. 准备自定义模块:
    • 确保你的项目结构中有 basic 文件夹,并且包含 basic.py, ts_token.py, data_path.py, redis_api.py 文件,且它们的功能符合代码中的调用(例如,redis_api 提供 Redis 连接,data_path 提供文件路径)。
    • 确保 spider_api.py 文件存在,并包含一个 spider_api 类,该类有一个 get_tick_mainG 方法,能够接收一个股票代码列表 listG 并返回一个包含 code, name, price, vol, amt 列的 Pandas DataFrame。 这是你需要根据你的实际数据源实现的关键部分。
    • 准备 stocklist.parquet 文件,存放股票代码列表。
  3. 启动Redis服务: 确保你的本地或远程Redis服务器正在运行,并且 redis_api 可以连接到它(代码中默认连接 DB 5)。

步骤 2:理解代码结构 (实时数据 类)

这个 实时数据 类是整个框架的核心。

# -*- coding: utf-8 -*-
"""
Created on Fri Dec 20 10:25:08 2024 # 注意:这个创建日期是未来的,仅为示例

@author: Administrator
"""

# 1. 导入库与模块
import warnings
warnings.filterwarnings("ignore") # 关闭警告
import sys
sys.path.append("..") # 假设 basic 和 spider_api 在上一级目录
import tushare as ts # 虽然导入了,但在这个实时类中似乎没直接用 Tushare 获取Tick
import pandas as pd
import numpy as np
import time
import warnings
import os
from bs4 import BeautifulSoup # 代码中未使用,可移除
import datetime
import requests # 代码中未使用,可移除
import re # 代码中未使用,可移除
import threading # 用于后台运行
import sys
from basic.basic import * # 导入基础函数,如 disp, tt, get_date_num_s
from basic.ts_token import * # 导入 token 相关,此处可能未使用
from basic.data_path import data_path, set_file_name # 导入数据路径配置
from spider_api import spider_api # 导入你的爬虫/API接口类
data_path=data_path+'stock/' # 设置股票数据的基础路径
sina=spider_api() # 实例化你的数据获取类
from basic.redis_api import redis_api # 导入Redis操作类
redisD=redis_api(dbG=5) # 连接到Redis的DB 5


# 2. 定义核心类 `实时数据`
class 实时数据:

    def __init__(self,listG=[]):
        """
        初始化函数:
        - 读取必要的静态数据(如日期、股票列表)。
        - 初始化用于存储数据的变量(self.data, self.barM1)。
        - 确定要监控的股票列表(listG)。
        - 启动后台线程 `tick_run` 来持续处理数据。
        """
        # print("初始化 实时数据 类...") # 可以加一些打印方便调试
        # parquet_r 是假设 basic.py 中定义的读取 parquet 文件的函数
        # date_day=parquet_r(data_path+'list/','date_day.parquet') # 这行代码似乎没被使用,可注释掉
        self.stocklist=parquet_r(data_path+'list/','stocklist.parquet') # 加载股票列表
        self.data={} # 用于存储上一次获取的原始Tick数据,以计算增量
        self.barM1={} # 核心变量:用于存储每个股票当前正在合成的1分钟K线 {'股票代码': {'trade_time': 'YYYY-MM-DD HH:MM:00', 'open': o, 'high': h, 'low': l, 'close': c, 'vol': v, 'amt': a}}
        
        if len(listG)==0:
            # 如果没有指定股票列表,默认取stocklist中的前500个
            self.listG=list(self.stocklist['code'])[:500]
            print(f"使用默认股票列表,监控前 {len(self.listG)} 只股票。")
        else:
            # 使用传入的股票列表
            self.listG=listG
            print(f"使用指定股票列表,监控 {len(self.listG)} 只股票。")

        # 启动后台线程,daemon=True表示主线程退出时该线程也退出
        threading.Thread(target=self.tick_run, daemon=True).start()
        print("Tick数据处理线程已启动...")

    def tick_run(self):
        """
        后台运行的核心循环:
        - 每秒检查一次。
        - 判断当前是否为交易时间。
        - 如果是交易时间,调用 self.tick_handle() 处理数据。
        - 在非交易时间或特定时间点(如午休后、收盘后),调用 self.bar_超时闭合() 来确保最后一根K线被正确关闭和存储。
        """
        while True:
            time.sleep(1) # 每秒执行一次检查
            try:
                timeN=disp(strTime='时间') # 获取当前时间 HH:MM:SS (假设 disp 函数实现)
                # 判断是否在交易时段 (考虑了集合竞价结束后 和 午休时间)
                is_trade_time = (timeN >= '09:26:01' and timeN <= '11:29:59') or \
                                (timeN >= '13:00:00' and timeN <= '15:00:10') # 稍微延长收盘时间以捕获最后数据

                if is_trade_time :
                    # print(f"{timeN} 在交易时段,处理Tick数据...") # 调试信息
                    self.tick_handle() # 处理实时Tick数据
                else:
                    # print(f"{timeN} 非交易时段,跳过处理。") # 调试信息
                    pass

                # 检查是否需要强制闭合K线 (例如,午休结束或收盘后)
                timeNow=disp('time_')[:4] # 获取 HHMM 格式的时间
                if timeNow in ['1131', '1501']: # 在11:31或15:01检查
                     print(f"{timeN} 触发超时闭合检查...") # 调试信息
                     self.bar_超时闭合()

            except Exception as ex:
                # 记录或打印异常,防止线程意外终止
                print(f"tick_run 发生错误: {ex}")
                # 考虑加入更详细的错误日志记录

    def bar_超时闭合(self):
        """
        处理超时的K线闭合逻辑:
        - 主要用于处理交易时段结束(午休、收盘)后,那些还没来得及因为下一分钟到来而自动闭合的K线。
        - 检查 self.barM1 中每个股票的K线,如果其时间戳与当前时间相差超过一定阈值(90秒),
          则认为该分钟已结束,将其视为“已完成”K线推送到Redis的list中,并清空内存中的记录。
        """
        currentTime = disp('datetime') # 获取当前完整日期时间
        # print(f"执行超时闭合检查,当前时间: {currentTime}")
        symbols_to_clear = [] # 记录需要清理的symbol
        for symbol in self.barM1: # 遍历内存中所有正在合成的K线
            bart = self.barM1[symbol]
            if isinstance(bart, dict) and 'trade_time' in bart: # 确保 bart 是有效的 K 线字典
                # get_date_num_s 是假设 basic.py 中计算两个时间字符串秒数差的函数
                sAdd2 = get_date_num_s(bart['trade_time'], currentTime) # 计算K线时间戳与当前时间的秒差

                # 如果秒差大于90秒 (1分钟 + 30秒缓冲),认为该K线应该已经结束
                if sAdd2 > 60 + 30:
                    print(f"超时闭合 K线: {symbol} @ {bart['trade_time']}, 秒差: {sAdd2}")
                    # 构造要存入Redis的数据
                    contentG=[bart['trade_time'], bart['open'], bart['high'], bart['low'], bart['close'], bart['vol'], bart['amt']]
                    nameG=symbol+'_1m_close' # Redis Key: 股票代码_1m_close
                    redisD.rpush(nameG, contentG) # 推入Redis list (存储已完成的K线)
                    symbols_to_clear.append(symbol) # 标记此symbol待清理
            else:
                # 如果 bart 不是预期的字典格式,也标记清理,防止内存泄漏
                 symbols_to_clear.append(symbol)

        # 清理已闭合的K线在内存中的记录
        for symbol in symbols_to_clear:
            if symbol in self.barM1:
                self.barM1[symbol] = {} # 清空,或者使用 del self.barM1[symbol]
                # print(f"已清理内存中的超时K线: {symbol}")


    def tick_handle(self):
        """
        处理单次Tick数据获取和分发:
        - 调用 `spider_api` 的 `get_tick_mainG` 获取一批股票的最新Tick数据。
        - 与上次存储的Tick数据 (`self.data['tick']`) 对比,计算成交量和成交额的增量。
        - 过滤掉没有实际成交(增量为0)的Tick。
        - 将带有增量信息的新Tick传递给 `tick_to_bar1` 进行K线合成。
        """
        # print("执行 tick_handle...") # 调试信息
        t1 = tt() # 假设 tt() 是 basic.py 中用于计时的类或函数
        
        # 1. 获取实时Tick数据
        # !!! 关键:这里的 sina.get_tick_mainG 需要你根据实际情况实现 !!!
        tick = sina.get_tick_mainG(self.listG)
        if tick is None or tick.empty:
            # print("未能获取到Tick数据或数据为空。")
            return

        # 2. 提取所需列,并计算增量
        tick2 = tick[['code', 'name', 'price', 'vol', 'amt']].copy() # 保留副本以防修改原始数据

        if 'tick' not in self.data or self.data['tick'].empty:
            # 如果是第一次获取数据或内存中无数据
            tick2['voladd'] = tick2['vol']
            tick2['amtadd'] = tick2['amt']
            # 特殊处理:如果是盘中启动,且时间已过09:30,将初始增量设为1(或一个小数目),避免因巨大累计值导致K线异常
            if disp('time') > '09:30:00':
                 tick2['voladd'] = tick2['voladd'].apply(lambda x: 1 if pd.isna(x) or x==0 else x) # 避免0或NaN
                 tick2['amtadd'] = tick2['amtadd'].apply(lambda x: 1 if pd.isna(x) or x==0 else x)
        else:
            # 与上次数据合并,计算差值(增量)
            tick2t = self.data['tick'].copy()
            # 使用 merge 来匹配 code,然后计算差异
            merged = pd.merge(tick2, tick2t, on='code', suffixes=('', '_prev'), how='left')
            # 注意:vol 和 amt 是累计值,需要用当前值减去上一个值得到增量
            # fillna(0) 处理新上市或首次获取到的股票
            merged['voladd'] = merged['vol'] - merged['vol_prev'].fillna(0)
            merged['amtadd'] = merged['amt'] - merged['amt_prev'].fillna(0)
            # 将计算好的增量添加回 tick2
            tick2 = pd.merge(tick2, merged[['code', 'voladd', 'amtadd']], on='code', how='left')


        # 更新存储的上一次Tick数据
        self.data['tick'] = tick2[['code', 'name', 'price', 'vol', 'amt']].copy() # 只存当前状态,不存增量列

        # 3. 处理时间戳
        timeN = disp('time') # 当前时间 HH:MM:SS
        dateN = disp('date') # 当前日期 YYYY-MM-DD
        # 对时间进行规范化,处理开盘前和收盘后的情况
        if timeN <= '09:30:00':
            timeN = '09:30:00' # 早于9:30的Tick计入9:30的第一分钟
        # 注意:收盘后的处理需要小心,这里直接设为 14:59:59 可能导致15:00的最后一笔丢失
        # 更好的做法可能是允许 15:00:00,然后在 tick_to_bar1 中处理分钟边界
        # if timeN >= '15:00:00':
        #    timeN = '14:59:59' # 归入最后一分钟 (这个逻辑可能需要调整)
        
        tick2['trade_time'] = dateN + ' ' + timeN # 组合成 'YYYY-MM-DD HH:MM:SS'

        # 4. 过滤掉无成交的Tick,并将有效Tick传递给K线合成函数
        tick2G = tick2[tick2['voladd'] > 0].copy() # 只处理成交量增加的Tick

        if not tick2G.empty:
            # print(f"获取到 {len(tick2G)} 条有效Tick,进行K线合成...")
            self.tick_to_bar1(tick2G)
        # else:
            # print("本次获取无有效成交Tick。")

        # t1.get_p(f"tick_handle 完成, 有效Tick数={len(tick2G)}") # 打印耗时


    def tick_to_bar1(self,tick2G):
        """
        将有效的Tick数据聚合成1分钟K线:
        - 遍历每一条带有增量成交的Tick数据。
        - 根据Tick时间确定它属于哪一分钟(截断秒数)。
        - 检查 `self.barM1` 中是否已有该股票该分钟的K线记录:
            - 如果没有,根据当前Tick创建新的1分钟K线(O=H=L=C=当前价格, V=增量成交量, A=增量成交额)。
            - 如果有,但时间戳是上一分钟的,说明上一分钟的K线已完成:
                - 将上一分钟的完整K线数据推送到Redis的 `_1m_close` list 中。
                - 用当前Tick创建新的1分钟K线。
            - 如果有,且时间戳是当前分钟的,更新该K线:
                - 更新最高价(max(High, 当前价格))。
                - 更新最低价(min(Low, 当前价格))。
                - 更新收盘价(当前价格)。
                - 累加成交量和成交额。
        - 每次更新或创建K线后,将该股票当前分钟K线的 *最新状态* 推送到Redis的 `_1m_new` set 中 (覆盖旧值)。
        """
        # print(f"执行 tick_to_bar1,处理 {len(tick2G)} 条Tick...")
        for i in range(len(tick2G)): # 遍历所有有效Tick
            # 提取Tick信息
            row = tick2G.iloc[i]
            code = row['code']
            price = row['price']
            voladd = row['voladd']
            amtadd = row['amtadd']
            trade_time1 = row['trade_time'] # 'YYYY-MM-DD HH:MM:SS'

            # 计算所属分钟的时间戳 'YYYY-MM-DD HH:MM:00'
            trade_minute = trade_time1[:16] + ':00'
            symbol = code # 用 code 作为 K线字典的 key

            # --- 核心聚合逻辑 ---
            if symbol not in self.barM1 or not isinstance(self.barM1[symbol], dict) or not self.barM1[symbol]:
                # Case 1: 该股票的第一条Tick 或 内存中无记录/记录无效
                # 创建新的1分钟K线
                bar = {
                    'trade_time': trade_minute,
                    'open': price,
                    'high': price,
                    'low': price,
                    'close': price,
                    'vol': voladd,
                    'amt': amtadd
                }
                self.barM1[symbol] = bar
                # print(f"[{trade_minute}] 新建 K线: {symbol}, O:{price}, V:{voladd}")
            else:
                # Case 2: 内存中已存在该股票的K线记录
                bart = self.barM1[symbol] # 获取当前内存中的K线

                if trade_minute == bart['trade_time']:
                    # Case 2a: Tick属于当前内存中的K线同一分钟 -> 更新K线
                    bart['high'] = max(bart['high'], price)
                    bart['low'] = min(bart['low'], price)
                    bart['close'] = price # 最新价即为当前收盘价
                    bart['vol'] += voladd # 累加成交量
                    bart['amt'] += amtadd # 累加成交额
                    self.barM1[symbol] = bart # 更新回内存
                    # print(f"[{trade_minute}] 更新 K线: {symbol}, H:{bart['high']}, L:{bart['low']}, C:{price}, V+:{voladd}")
                else:
                    # Case 2b: Tick属于新的一分钟 -> 旧K线完成,新K线开始
                    # 1. 将已完成的旧K线推送到Redis (_1m_close list)
                    contentG = [bart['trade_time'], bart['open'], bart['high'], bart['low'], bart['close'], bart['vol'], bart['amt']]
                    nameG_close = symbol + '_1m_close'
                    redisD.rpush(nameG_close, contentG) # 追加到列表末尾
                    # print(f"[{bart['trade_time']}] 关闭 K线: {symbol}, 推送至 Redis List '{nameG_close}'")

                    # 2. 用当前Tick创建新的1分钟K线
                    bar = {
                        'trade_time': trade_minute,
                        'open': price,
                        'high': price,
                        'low': price,
                        'close': price,
                        'vol': voladd,
                        'amt': amtadd
                    }
                    self.barM1[symbol] = bar
                    # print(f"[{trade_minute}] 新建 K线 (因分钟切换): {symbol}, O:{price}, V:{voladd}")

            # --- 推送当前K线的最新状态到Redis (_1m_new set) ---
            # 无论新建还是更新,都将当前内存中K线的最新状态写入Redis set (覆盖)
            # 这使得外部可以实时获取到“正在进行中”的K线状态
            current_bar = self.barM1[symbol]
            if isinstance(current_bar, dict) and current_bar: # 确保是有效字典
                contentG_new = [current_bar['trade_time'], current_bar['open'], current_bar['high'], current_bar['low'], current_bar['close'], current_bar['vol'], current_bar['amt']]
                nameG_new = symbol + '_1m_new'
                redisD.set(nameG_new, contentG_new) # 使用 set 更新/覆盖最新状态
                # print(f"[{current_bar['trade_time']}] 更新 Redis Set '{nameG_new}' 最新状态")


# 3. 程序入口
if __name__=='__main__':
    print("开始运行实时数据处理程序...")
    # 实例化类,__init__ 会自动启动后台线程
    # 你可以传入一个特定的股票列表,例如: real_time_processor = 实时数据(listG=['sh.600000', 'sz.000001'])
    real_time_processor = 实时数据()

    # 主线程可以做其他事情,或者仅仅保持运行
    print("主线程保持运行,等待后台线程处理数据...")
    try:
        while True:
            time.sleep(60) # 主线程可以每分钟打印一次状态或做其他检查
            print(f"主线程心跳 - {disp('datetime')}")
            # 这里可以添加代码来检查后台线程是否还在运行等
    except KeyboardInterrupt:
        print("接收到退出信号,程序即将关闭...")
        # 这里可以添加一些清理代码,如果需要的话

步骤 3:运行与理解数据流

  1. 运行脚本:

    python your_script_name.py
    

    脚本启动后,__init__ 会被调用,后台线程 tick_run 开始运行。

  2. 数据流动过程:

    • tick_run 线程每秒检查一次时间。
    • 在交易时段内,调用 tick_handle
    • tick_handle 通过 sina.get_tick_mainG 获取一批最新Tick数据。
    • 计算与上次数据的差值,得到 voladdamtadd(增量成交)。
    • 过滤掉 voladd <= 0 的Tick。
    • 将有效的Tick(tick2G)传递给 tick_to_bar1
    • tick_to_bar1 遍历每一条Tick:
      • 判断Tick属于哪一分钟 (trade_minute)。
      • 查找内存 (self.barM1) 中该股票对应的K线。
      • 如果分钟变化: 将内存中上一分钟的完整K线数据 rpush 到 Redis 的 [股票代码]_1m_close 列表。然后,用当前Tick创建新的K线存入内存。
      • 如果分钟未变: 更新内存中当前分钟K线的 H, L, C, V, A。
      • 每次处理完Tick: 将内存中该股票当前分钟K线的 最新 状态 set 到 Redis 的 [股票代码]_1m_new 键 (会覆盖之前的值)。
    • bar_超时闭合 函数会在特定时间点(如11:31, 15:01)或检测到K线长时间未更新时,强制将内存中的K线视为完成,推送到 _1m_close 列表,并清理内存。
  3. Redis 中的数据:

    • [股票代码]_1m_close (List类型): 存储该股票所有 已经完成 的1分钟K线。每条K线是一个包含 [trade_time, open, high, low, close, vol, amt] 的列表或元组。新的完成K线会被追加到列表末尾。你可以使用 LRANGE [key] 0 -1 查看所有历史K线,或 LINDEX [key] -1 获取最新的已完成K线。
    • [股票代码]_1m_new (String/Value类型): 存储该股票 当前正在合成中 的那一分钟K线的最新状态。每次该分钟有新Tick到来并被处理后,这个键的值就会被更新(覆盖)。你可以使用 GET [key] 来获取这个实时更新的K线状态。当这一分钟结束后,它的最终状态会被推入 _1m_close 列表,而这个 _1m_new 键会被下一分钟的新K线数据覆盖。

步骤 4:如何使用生成的数据

  • 实时监控当前K线: 应用程序可以定期(例如每秒)从 Redis 读取 [股票代码]_1m_new 键,获取当前分钟K线的实时动态(H, L, C, V, A 的变化)。
  • 获取已完成的历史K线: 当需要历史1分钟K线时(例如用于策略回测或图表绘制),可以从 Redis 的 [股票代码]_1m_close 列表读取。可以一次性读取全部 (LRANGE key 0 -1) 或只读取最近几条 (LRANGE key -N -1)。

关键点与注意事项:

  1. spider_api 的实现: 这是整个框架能工作的 前提。你需要确保 spider_api().get_tick_mainG(listG) 能稳定、快速地返回所需的实时Tick数据 DataFrame。数据源的质量和速度直接影响K线合成的准确性和实时性。
  2. 时间同步与精度: 服务器时间需要准确。Tick数据的时间戳精度也很重要。代码中将秒数截断来确定分钟边界,这在大多数情况下可行,但需注意极端情况(如恰好在 HH:MM:00.000 收到的Tick)。
  3. 性能:
    • 监控的股票数量 (listG) 越多,get_tick_mainG 的负担越重,处理时间可能增加。
    • Pandas 的 merge 操作在数据量大时可能有性能开销,可以考虑优化(如使用 set_indexsubtract 或其他方法)。
    • Redis 操作通常很快,但在极高并发下也需关注。
  4. 异常处理: 代码中只有顶层的 try...except,建议在 get_tick_mainG 调用、数据处理、Redis操作等关键步骤加入更细致的错误处理和日志记录,增强系统鲁棒性。
  5. 数据持久化: Redis 是内存数据库,重启会丢失数据(除非配置了持久化)。如果需要长期保存K线数据,应考虑定期将 Redis 中的 _1m_close 数据转存到磁盘文件(如 Parquet、CSV)或永久数据库(如 InfluxDB、MySQL)。
  6. 启动时的数据处理: 盘中启动时,如何处理历史累计成交量/额 (vol, amt) 以正确计算增量,需要仔细考虑(代码中尝试用设为1来处理,可能需要根据数据源特性调整)。
  7. 资源消耗: 后台线程会持续运行,消耗CPU和内存。Redis 也会消耗内存。需要监控资源使用情况。

本教程提供了一个基础框架,你可以根据自己的具体需求(数据源、性能要求、错误处理级别等)进行修改和扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值