好的,下面是一个基于你提供的代码构建的教程,解释了如何实现一个实时获取股票Tick数据并自动合成1分钟K线数据的框架。
教程:构建实时股票数据获取与1分钟K线合成框架
目标: 本教程旨在指导你如何使用Python构建一个框架,该框架能够:
- 实时从指定数据源(如此处代码中的
spider_api
)获取股票的逐笔成交数据(Tick数据)。 - 在内存中实时将Tick数据聚合成1分钟K线(包含开盘价、最高价、最低价、收盘价、成交量、成交额 - OHLCVA)。
- 利用Redis存储生成的1分钟K线数据,区分“已完成”的K线和“正在更新中”的当前K线。
核心技术栈:
- Python: 主要编程语言。
- Pandas: 用于高效处理和操作数据。
- Threading: 用于在后台持续运行数据获取和处理任务,不阻塞主程序。
- Redis: 一个内存数据库,用于快速存储和读取K线数据,便于其他应用或策略模块实时访问。
- 自定义模块:
spider_api
: 假设这是一个封装好的用于获取实时Tick数据的类或函数(你需要自行实现或替换为你实际使用的数据源接口,如 Tushare 的 WebSocket、或其他行情API)。basic.*
: 包含基础函数(如时间处理disp
)、数据路径配置、Redis连接等辅助功能。
步骤 1:环境准备与依赖
- 安装必要的库:
pip install pandas numpy redis requests beautifulsoup4 tushare -i https://pypi.tuna.tsinghua.edu.cn/simple
- 准备自定义模块:
- 确保你的项目结构中有
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
文件,存放股票代码列表。
- 确保你的项目结构中有
- 启动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:运行与理解数据流
-
运行脚本:
python your_script_name.py
脚本启动后,
__init__
会被调用,后台线程tick_run
开始运行。 -
数据流动过程:
tick_run
线程每秒检查一次时间。- 在交易时段内,调用
tick_handle
。 tick_handle
通过sina.get_tick_mainG
获取一批最新Tick数据。- 计算与上次数据的差值,得到
voladd
和amtadd
(增量成交)。 - 过滤掉
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
键 (会覆盖之前的值)。
- 判断Tick属于哪一分钟 (
bar_超时闭合
函数会在特定时间点(如11:31, 15:01)或检测到K线长时间未更新时,强制将内存中的K线视为完成,推送到_1m_close
列表,并清理内存。
-
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
)。
关键点与注意事项:
spider_api
的实现: 这是整个框架能工作的 前提。你需要确保spider_api().get_tick_mainG(listG)
能稳定、快速地返回所需的实时Tick数据 DataFrame。数据源的质量和速度直接影响K线合成的准确性和实时性。- 时间同步与精度: 服务器时间需要准确。Tick数据的时间戳精度也很重要。代码中将秒数截断来确定分钟边界,这在大多数情况下可行,但需注意极端情况(如恰好在
HH:MM:00.000
收到的Tick)。 - 性能:
- 监控的股票数量 (
listG
) 越多,get_tick_mainG
的负担越重,处理时间可能增加。 - Pandas 的
merge
操作在数据量大时可能有性能开销,可以考虑优化(如使用set_index
后subtract
或其他方法)。 - Redis 操作通常很快,但在极高并发下也需关注。
- 监控的股票数量 (
- 异常处理: 代码中只有顶层的
try...except
,建议在get_tick_mainG
调用、数据处理、Redis操作等关键步骤加入更细致的错误处理和日志记录,增强系统鲁棒性。 - 数据持久化: Redis 是内存数据库,重启会丢失数据(除非配置了持久化)。如果需要长期保存K线数据,应考虑定期将 Redis 中的
_1m_close
数据转存到磁盘文件(如 Parquet、CSV)或永久数据库(如 InfluxDB、MySQL)。 - 启动时的数据处理: 盘中启动时,如何处理历史累计成交量/额 (
vol
,amt
) 以正确计算增量,需要仔细考虑(代码中尝试用设为1来处理,可能需要根据数据源特性调整)。 - 资源消耗: 后台线程会持续运行,消耗CPU和内存。Redis 也会消耗内存。需要监控资源使用情况。
本教程提供了一个基础框架,你可以根据自己的具体需求(数据源、性能要求、错误处理级别等)进行修改和扩展。