1. 引言

通达信是国内主流的股票分析软件之一,其本地数据文件包含了丰富的股票历史行情数据。对于量化交易研究者和数据分析师来说,能够直接读取这些本地数据文件,可以避免网络延迟、实现离线分析,并充分利用通达信软件积累的历史数据。本文将详细介绍如何使用Python读取通达信本地数据文件,包括日线、5分钟线等不同周期的数据。

2. 通达信数据文件结构

通达信的数据文件通常存储在软件的安装目录下,主要包含以下几种类型:

2.1 日线数据文件

  • 文件路径:T0002/hq_cache/*.day
  • 文件名格式:sh000001.day(上证指数)、sz000001.day(平安银行)
  • 文件结构:二进制格式,每条记录包含日期、开盘价、最高价、最低价、收盘价、成交量、成交额

2.2 5分钟线数据文件

  • 文件路径:T0002/hq_cache/*.lc5T0002/hq_cache/*.lc1
  • 文件名格式:sh000001.lc5(5分钟线)、sh000001.lc1(1分钟线)
  • 文件结构:二进制格式,记录更细粒度的分时数据

2.3 基础信息文件

  • 文件路径:T0002/hq_cache/tdxhy.cfgT0002/hq_cache/tdxzs.cfg
  • 内容:板块分类、指数信息等元数据

3. 环境准备与依赖安装

在开始读取通达信数据之前,需要确保Python环境已安装必要的库:

pip install pandas numpy struct

主要依赖说明:

  • pandas: 数据处理和分析的核心库
  • numpy: 数值计算,处理二进制数据转换
  • struct: Python标准库,用于解析二进制数据格式

4. 读取日线数据文件

4.1 日线数据结构解析

通达信的日线数据文件(.day)采用固定的二进制格式:

  • 每条记录32字节
  • 包含7个字段,每个字段4字节(32位整数)

数据结构定义:

# 日线数据记录结构
# 每32字节一条记录,包含:
# 1. 日期(YYYYMMDD格式的整数)
# 2. 开盘价(单位:0.01元)
# 3. 最高价(单位:0.01元)
# 4. 最低价(单位:0.01元)
# 5. 收盘价(单位:0.01元)
# 6. 成交量(单位:股)
# 7. 成交额(单位:0.01元)

4.2 Python实现代码

import struct
import pandas as pd
import numpy as np
from pathlib import Path
from datetime import datetime

def read_tdx_day_file(file_path):
    """
    读取通达信日线数据文件(.day格式)
    
    Args:
        file_path: 通达信日线文件路径,如 "sh000001.day"
        
    Returns:
        pandas.DataFrame: 包含日期、开盘、最高、最低、收盘、成交量、成交额的DataFrame
    """
    # 检查文件是否存在
    if not Path(file_path).exists():
        raise FileNotFoundError(f"文件不存在: {file_path}")
    
    # 定义数据结构格式
    # 每32字节一条记录,7个int32类型字段
    record_format = '7i'  # 7个32位有符号整数
    record_size = struct.calcsize(record_format)  # 7 * 4 = 28字节
    
    data_list = []
    
    try:
        with open(file_path, 'rb') as f:
            # 读取文件全部内容
            file_content = f.read()
            
            # 计算记录数量
            num_records = len(file_content) // record_size
            
            for i in range(num_records):
                # 计算当前记录的起始位置
                start_pos = i * record_size
                end_pos = start_pos + record_size
                
                # 解析二进制数据
                record_data = struct.unpack(record_format, file_content[start_pos:end_pos])
                
                # 提取各个字段
                date_int = record_data[0]
                open_price = record_data[1] / 100.0  # 转换为元
                high_price = record_data[2] / 100.0
                low_price = record_data[3] / 100.0
                close_price = record_data[4] / 100.0
                volume = record_data[5]  # 成交量(股)
                amount = record_data[6] / 100.0  # 成交额(元)
                
                # 转换日期格式
                date_str = str(date_int)
                if len(date_str) == 8:
                    trade_date = datetime.strptime(date_str, '%Y%m%d').date()
                else:
                    trade_date = None
                
                # 添加到列表
                data_list.append({
                    'date': trade_date,
                    'open': open_price,
                    'high': high_price,
                    'low': low_price,
                    'close': close_price,
                    'volume': volume,
                    'amount': amount
                })
        
        # 转换为DataFrame
        df = pd.DataFrame(data_list)
        
        # 按日期排序(通达信数据通常是倒序存储)
        if not df.empty and 'date' in df.columns:
            df = df.sort_values('date').reset_index(drop=True)
            
        return df
        
    except Exception as e:
        print(f"读取文件失败: {e}")
        return pd.DataFrame()

# 使用示例
if __name__ == "__main__":
    # 替换为你的通达信日线文件路径
    day_file = "T0002/hq_cache/sh000001.day"
    
    try:
        df_day = read_tdx_day_file(day_file)
        print(f"成功读取 {len(df_day)} 条日线数据")
        print(df_day.head())
        print("\n数据统计信息:")
        print(df_day.describe())
    except Exception as e:
        print(f"错误: {e}")

4.3 数据处理与验证

读取数据后,通常需要进行一些数据清洗和验证:

def validate_tdx_data(df):
    """验证通达信数据的完整性"""
    if df.empty:
        print("数据为空")
        return False
    
    # 检查必要列是否存在
    required_columns = ['date', 'open', 'high', 'low', 'close', 'volume']
    missing_columns = [col for col in required_columns if col not in df.columns]
    
    if missing_columns:
        print(f"缺少必要列: {missing_columns}")
        return False
    
    # 检查数据合理性
    # 1. 高价应不低于低价
    invalid_high_low = df[df['high'] < df['low']]
    if not invalid_high_low.empty:
        print(f"发现 {len(invalid_high_low)} 条高价低于低价的数据")
    
    # 2. 价格应为正数
    negative_prices = df[(df['open'] <= 0) | (df['close'] <= 0)]
    if not negative_prices.empty:
        print(f"发现 {len(negative_prices)} 条非正价格数据")
    
    # 3. 检查日期连续性
    if 'date' in df.columns:
        df_sorted = df.sort_values('date')
        date_diff = df_sorted['date'].diff().dt.days
        gaps = date_diff[date_diff > 1]
        if not gaps.empty:
            print(f"发现日期不连续,最大间隔: {gaps.max()} 天")
    
    return True

def enhance_tdx_data(df):
    """增强数据,添加技术指标"""
    if df.empty:
        return df
    
    # 添加涨跌幅
    df['pct_change'] = df['close'].pct_change() * 100
    
    # 添加移动平均线
    df['ma5'] = df['close'].rolling(window=5).mean()
    df['ma10'] = df['close'].rolling(window=10).mean()
    df['ma20'] = df['close'].rolling(window=20).mean()
    
    # 添加成交量移动平均
    df['volume_ma5'] = df['volume'].rolling(window=5).mean()
    
    # 添加价格区间
    df['range'] = df['high'] - df['low']
    df['range_pct'] = df['range'] / df['close'] * 100
    
    return df

5. 读取5分钟线数据

5.1 5分钟线数据结构

通达信的5分钟线文件(.lc5)结构与日线类似,但包含更多字段:

def read_tdx_minute_file(file_path, period='5min'):
    """
    读取通达信分钟线数据文件
    
    Args:
        file_path: 文件路径,如 "sh000001.lc5"
        period: 周期类型,'5min' 或 '1min'
        
    Returns:
        pandas.DataFrame: 包含时间、OHLCV等数据的DataFrame
    """
    # 检查文件是否存在
    if not Path(file_path).exists():
        raise FileNotFoundError(f"文件不存在: {file_path}")
    
    # 分钟线数据结构:每32字节一条记录
    # 包含:日期、时间、开盘、最高、最低、收盘、成交量、成交额
    record_format = '2i6i'  # 2个日期时间相关,6个价格成交量相关
    record_size = struct.calcsize(record_format)
    
    data_list = []
    
    try:
        with open(file_path, 'rb') as f:
            file_content = f.read()
            num_records = len(file_content) // record_size
            
            for i in range(num_records):
                start_pos = i * record_size
                end_pos = start_pos + record_size
                
                record_data = struct.unpack(record_format, file_content[start_pos:end_pos])
                
                # 解析日期和时间
                date_int = record_data[0]
                time_int = record_data[1]
                
                # 转换日期
                date_str = str(date_int)
                if len(date_str) == 8:
                    year = int(date_str[:4])
                    month = int(date_str[4:6])
                    day = int(date_str[6:8])
                else:
                    continue
                
                # 转换时间(HHMM格式)
                hour = time_int // 100
                minute = time_int % 100
                
                # 创建datetime对象
                dt = datetime(year, month, day, hour, minute)
                
                # 解析价格和成交量
                open_price = record_data[2] / 100.0
                high_price = record_data[3] / 100.0
                low_price = record_data[4] / 100.0
                close_price = record_data[5] / 100.0
                volume = record_data[6]
                amount = record_data[7] / 100.0
                
                data_list.append({
                    'datetime': dt,
                    'open': open_price,
                    'high': high_price,
                    'low': low_price,
                    'close': close_price,
                    'volume': volume,
                    'amount': amount,
                    'period': period
                })
        
        # 创建DataFrame
        df = pd.DataFrame(data_list)
        
        if not df.empty:
            df = df.sort_values('datetime').reset_index(drop=True)
            
        return df
        
    except Exception as e:
        print(f"读取分钟线文件失败: {e}")
        return pd.DataFrame()

6. 批量读取与数据管理

6.1 批量读取多个股票数据

import os
from concurrent.futures import ThreadPoolExecutor

def batch_read_tdx_data(data_dir, pattern="*.day", max_workers=4):
    """
    批量读取通达信数据文件
    
    Args:
        data_dir: 数据目录路径
        pattern: 文件匹配模式,如 "*.day"、"*.lc5"
        max_workers: 最大线程数
        
    Returns:
        dict: 股票代码到DataFrame的映射
    """
    data_dir = Path(data_dir)
    if not data_dir.exists():
        raise FileNotFoundError(f"目录不存在: {data_dir}")
    
    # 查找所有匹配的文件
    files = list(data_dir.glob(pattern))
    print(f"找到 {len(files)} 个数据文件")
    
    data_dict = {}
    
    def read_single_file(file_path):
        """读取单个文件"""
        try:
            if file_path.suffix == '.day':
                df = read_tdx_day_file(str(file_path))
            elif file_path.suffix in ['.lc5', '.lc1']:
                period = '5min' if file_path.suffix == '.lc5' else '1min'
                df = read_tdx_minute_file(str(file_path), period)
            else:
                return None
            
            # 从文件名提取股票代码
            stock_code = file_path.stem
            return stock_code, df
            
        except Exception as e:
            print(f"读取文件 {file_path.name} 失败: {e}")
            return None
    
    # 使用线程池并行读取
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        results = executor.map(read_single_file, files)
        
        for result in results:
            if result:
                stock_code, df = result
                if not df.empty:
                    data_dict[stock_code] = df
    
    return data_dict

def save_to_database(data_dict, db_path="tdx_data.db"):
    """将数据保存到SQLite数据库"""
    import sqlite3
    
    conn = sqlite3.connect(db_path)
    
    for stock_code, df in data_dict.items():
        if not df.empty:
            # 添加股票代码列
            df['code'] = stock_code
            
            # 保存到数据库
            table_name = f"stock_{stock_code}"
            df.to_sql(table_name, conn, if_exists='replace', index=False)
            print(f"已保存 {stock_code} 到表 {table_name}")
    
    conn.close()
    print(f"数据已保存到 {db_path}")

6.2 数据缓存与更新

import pickle
import hashlib
from datetime import datetime, timedelta

class TdxDataCache:
    """通达信数据缓存管理器"""
    
    def __init__(self, cache_dir="tdx_cache"):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(exist_ok=True)
    
    def get_cache_key(self, file_path, params=None):
        """生成缓存键"""
        # 基于文件路径和参数生成唯一键
        key_str = str(file_path)
        if params:
            key_str += str(params)
        
        # 添加文件修改时间
        file_mtime = os.path.getmtime(file_path) if os.path.exists(file_path) else 0
        key_str += str(file_mtime)
        
        return hashlib.md5(key_str.encode()).hexdigest()
    
    def load_from_cache(self, cache_key):
        """从缓存加载数据"""
        cache_file = self.cache_dir / f"{cache_key}.pkl"
        
        if cache_file.exists():
            # 检查缓存是否过期(默认1天)
            cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
            if cache_age < timedelta(days=1):
                try:
                    with open(cache_file, 'rb') as f:
                        return pickle.load(f)
                except:
                    pass
        
        return None
    
    def save_to_cache(self, cache_key, data):
        """保存数据到缓存"""
        cache_file = self.cache_dir / f"{cache_key}.pkl"
        
        try:
            with open(cache_file, 'wb') as f:
                pickle.dump(data, f)
            return True
        except:
            return False
    
    def read_with_cache(self, file_path, reader_func, **kwargs):
        """带缓存的读取"""
        cache_key = self.get_cache_key(file_path, kwargs)
        
        # 尝试从缓存加载
        cached_data = self.load_from_cache(cache_key)
        if cached_data is not None:
            print(f"从缓存加载数据: {file_path}")
            return cached_data
        
        # 重新读取
        print(f"重新读取数据: {file_path}")
        data = reader_func(file_path, **kwargs)
        
        # 保存到缓存
        if data is not None and not data.empty:
            self.save_to_cache(cache_key, data)
        
        return data

7. 常见问题与解决方案

7.1 文件路径问题

问题: 找不到通达信数据文件
解决方案:

def find_tdx_data_path():
    """自动查找通达信数据路径"""
    possible_paths = [
        # Windows 默认路径
        "C:/new_tdx/T0002/hq_cache",
        "D:/new_tdx/T0002/hq_cache",
        "C:/tdx/T0002/hq_cache",
        # 用户目录
        str(Path.home() / "tdx" / "T0002" / "hq_cache"),
    ]
    
    for path in possible_paths:
        if Path(path).exists():
            print(f"找到通达信数据路径: {path}")
            return path
    
    print("未找到通达信数据路径,请手动指定")
    return None

7.2 数据格式异常

问题: 读取的数据价格异常(如价格为0或负数)
解决方案:

def clean_tdx_data(df):
    """清洗通达信数据"""
    if df.empty:
        return df
    
    # 移除价格异常的数据
    df_clean = df.copy()
    
    # 1. 移除价格为0或负数的记录
    price_columns = ['open', 'high', 'low', 'close']
    for col in price_columns:
        df_clean = df_clean[df_clean[col] > 0]
    
    # 2. 移除高价低于低价的

更多推荐