通达信和同花顺本地数据文件用于量化回测

背景

这次目标不是调用行情软件的界面,而是直接读取本机已经下载到本地的通达信、同花顺数据文件,把它们变成可复用的量化数据源。

核心用途有三个:

  • 用通达信 gbbq 股本变动文件,还原历史某一天的总股本、流通股本,再结合日线收盘价推导历史市值。
  • 用通达信本地板块文件读取行业、概念、风格、指数板块成分股。
  • 用同花顺本地板块文件读取概念板块、行业板块以及股票成分股。

需要特别注意:股本文件是历史事件表,适合做历史还原;概念/板块文件通常是当前快照,直接用于历史回测会有未来函数风险。

通达信股本文件 gbbq

本机文件位置:

D:\dev\tongdaxin(上证指数)\T0002\hq_cache\gbbq
D:\dev\tongdaxin(上证指数)\T0002\hq_cache\gbbq.map

gbbq 是通达信的股本变动、除权除息等事件文件。用 pytdxGbbqReader 可以直接解析。

本机当前解析结果:

全部事件:196941 条
全部事件日期范围:19900301 到 20260611
股本相关事件:132627 条
股本相关事件日期范围:19901210 到 20260610

示例股票:

000001:股本记录从 19910403 到 20250630
600028:中国石化,股本记录从 20011108 到 20251230
601857:中国石油,股本记录从 20071105 到 20131108

字段含义

pytdx.reader.gbbq_reader.GbbqReader().get_df() 解析后的主要字段:

market
code
datetime
category
hongli_panqianliutong
peigujia_qianzongguben
songgu_qianzongguben
peigu_houzongguben

category 里,常见分类含义如下:

1  除权除息
2  送配股上市
3  非流通股上市
4  未知股本变动
5  股本变化
6  增发新股
7  股份回购
8  增发新股上市
9  转配股上市
10 可转债上市
11 扩缩股
12 非流通股缩股

做历史市值时,不要把 category=1 当作股本记录。category=1 是分红、送股、配股价等除权除息信息,字段语义和股本变化行不同。

对股本相关行,可以按下面方式理解:

hongli_panqianliutong      变动前流通股本
peigujia_qianzongguben    变动前总股本
songgu_qianzongguben      变动后流通股本
peigu_houzongguben        变动后总股本

单位通常是“万股”。例如中国石油的总股本字段 18302098.0,对应:

18302098 万股 = 183020980000 股

用 gbbq 还原历史股本

gbbq 是事件表,不是每日快照。要得到某只股票在某一天的股本,需要取这只股票在目标日期之前最后一条有效股本事件。

例如要还原 2023-01-31 的股本:

from pathlib import Path
from pytdx.reader.gbbq_reader import GbbqReader

tdx = Path(r"D:\dev\tongdaxin(上证指数)")
gbbq_path = tdx / "T0002" / "hq_cache" / "gbbq"

df = GbbqReader().get_df(str(gbbq_path))

share_categories = {2, 3, 4, 5, 6, 7, 8, 9, 11, 12}
target_date = 20230131

share_events = df[
    (df["category"].isin(share_categories))
    & (df["datetime"] <= target_date)
    & (df["songgu_qianzongguben"] > 0)
    & (df["peigu_houzongguben"] > 0)
].copy()

latest_share = (
    share_events
    .sort_values(["market", "code", "datetime"])
    .groupby(["market", "code"], as_index=False)
    .tail(1)
)

latest_share["float_shares"] = latest_share["songgu_qianzongguben"] * 10000
latest_share["total_shares"] = latest_share["peigu_houzongguben"] * 10000

通达信日线文件

通达信日线文件在:

D:\dev\tongdaxin(上证指数)\vipdoc\sh\lday
D:\dev\tongdaxin(上证指数)\vipdoc\sz\lday

文件名类似:

sh601857.day
sz000001.day

.day 文件每条记录 32 字节,小端格式:

date     int32  YYYYMMDD
open     int32  价格 * 100
high     int32  价格 * 100
low      int32  价格 * 100
close    int32  价格 * 100
amount   float
volume   int32
reserved int32

读取示例:

import struct
import pandas as pd
from pathlib import Path

def read_tdx_day(path: Path) -> pd.DataFrame:
    rows = []
    data = path.read_bytes()
    for offset in range(0, len(data), 32):
        chunk = data[offset: offset + 32]
        if len(chunk) < 32:
            continue
        date, open_, high, low, close, amount, volume, reserved = struct.unpack("<IIIIIfII", chunk)
        rows.append({
            "date": date,
            "open": open_ / 100,
            "high": high / 100,
            "low": low / 100,
            "close": close / 100,
            "amount": amount,
            "volume": volume,
        })
    return pd.DataFrame(rows)

推导历史市值

历史市值的核心公式:

总市值 = close * total_shares
流通市值 = close * float_shares

如果要用亿元表示:

总市值_亿元 = close * total_shares / 1e8
流通市值_亿元 = close * float_shares / 1e8

完整流程:

  1. 读取 gbbq
  2. 过滤目标日期之前的股本相关事件。
  3. 每只股票取最后一条股本事件。
  4. 读取目标日期或目标日期之前最近一个交易日的 .day 收盘价。
  5. 用收盘价乘总股本或流通股本。
  6. 排序得到历史某一天的市值排名。

示例骨架:

from pathlib import Path
import struct
import pandas as pd
from pytdx.reader.gbbq_reader import GbbqReader

tdx = Path(r"D:\dev\tongdaxin(上证指数)")
target_date = 20230131

def read_tdx_day(path: Path) -> pd.DataFrame:
    rows = []
    data = path.read_bytes()
    for offset in range(0, len(data), 32):
        chunk = data[offset: offset + 32]
        if len(chunk) < 32:
            continue
        date, open_, high, low, close, amount, volume, reserved = struct.unpack("<IIIIIfII", chunk)
        rows.append({
            "date": date,
            "close": close / 100,
            "amount": amount,
            "volume": volume,
        })
    return pd.DataFrame(rows)

def market_code_from_day_path(path: Path):
    stem = path.stem
    prefix = stem[:2]
    code = stem[2:]
    market = 1 if prefix == "sh" else 0
    return market, code

gbbq = GbbqReader().get_df(str(tdx / "T0002" / "hq_cache" / "gbbq"))
share_categories = {2, 3, 4, 5, 6, 7, 8, 9, 11, 12}

share_events = gbbq[
    (gbbq["category"].isin(share_categories))
    & (gbbq["datetime"] <= target_date)
    & (gbbq["songgu_qianzongguben"] > 0)
    & (gbbq["peigu_houzongguben"] > 0)
].copy()

shares = (
    share_events
    .sort_values(["market", "code", "datetime"])
    .groupby(["market", "code"], as_index=False)
    .tail(1)
    .copy()
)
shares["float_shares"] = shares["songgu_qianzongguben"] * 10000
shares["total_shares"] = shares["peigu_houzongguben"] * 10000

price_rows = []
for day_dir in [tdx / "vipdoc" / "sh" / "lday", tdx / "vipdoc" / "sz" / "lday"]:
    for path in day_dir.glob("*.day"):
        market, code = market_code_from_day_path(path)
        day = read_tdx_day(path)
        day = day[day["date"] <= target_date]
        if day.empty:
            continue
        last = day.sort_values("date").iloc[-1]
        price_rows.append({
            "market": market,
            "code": code,
            "trade_date": int(last["date"]),
            "close": float(last["close"]),
        })

prices = pd.DataFrame(price_rows)

result = prices.merge(
    shares[["market", "code", "datetime", "float_shares", "total_shares"]],
    on=["market", "code"],
    how="inner",
)
result["total_mv_yuan"] = result["close"] * result["total_shares"]
result["float_mv_yuan"] = result["close"] * result["float_shares"]
result["total_mv_yi"] = result["total_mv_yuan"] / 1e8
result["float_mv_yi"] = result["float_mv_yuan"] / 1e8

top100 = result.sort_values("total_mv_yuan", ascending=False).head(100)
print(top100[["market", "code", "trade_date", "close", "total_mv_yi", "float_mv_yi"]])

通达信板块文件

通达信本地板块文件主要在:

D:\dev\tongdaxin(上证指数)\T0002\hq_cache

常用文件:

tdxhy.cfg       行业分类,股票到行业代码
block_gn.dat    概念板块及成分股
block_fg.dat    风格板块及成分股
block_zs.dat    指数/板块及成分股
tdxzs.cfg       指数/板块配置
tdxzs3.cfg      指数/板块配置
tdxzsbase.cfg   指数/板块基础配置

读取通达信概念板块

block_gn.dat 可以用 pytdx.reader.block_reader.BlockReader 直接读取。

from pathlib import Path
from pytdx.reader.block_reader import BlockReader

tdx = Path(r"D:\dev\tongdaxin(上证指数)")
path = tdx / "T0002" / "hq_cache" / "block_gn.dat"

df = BlockReader().get_df(str(path))
print(df.head())

输出字段:

blockname   板块名
block_type  板块类型
code_index  成分股顺序
code        股票代码

本机当前 block_gn.dat 解析出 40653 条概念-股票关系。

读取通达信行业分类

tdxhy.cfg 是文本文件,每行大致类似:

0|000001|T1001|||X500102
0|000002|T110201|||X530101
1|600028|T010101|||X620101

可以按 | 分割:

from pathlib import Path
import pandas as pd

tdx = Path(r"D:\dev\tongdaxin(上证指数)")
path = tdx / "T0002" / "hq_cache" / "tdxhy.cfg"

rows = []
for line in path.read_text(encoding="gbk", errors="ignore").splitlines():
    parts = line.split("|")
    if len(parts) >= 3:
        rows.append({
            "market": parts[0],
            "code": parts[1],
            "tdx_industry": parts[2],
            "extra_industry": parts[5] if len(parts) > 5 else "",
        })

industry = pd.DataFrame(rows)

tdxhy.cfg 直接给的是行业代码,不一定带完整行业名称。要还原行业树和名称,还需要结合通达信其他行业配置文件,或者用同花顺 block_industry.ini 这种已经带名称和成分股的文件。

同花顺概念和行业文件

同花顺安装路径:

C:\同花顺软件\同花顺\hexin.exe

核心板块文件在:

C:\同花顺软件\同花顺\BlockUpdate

最有用的几个文件:

block_conception.ini    A 股概念板块
block_industry.ini      A 股行业板块
block_region.ini        地域板块
block_every_day.ini     每日/热点类板块
block_tree.ini          板块树

另外还有一个全集缓存:

C:\同花顺软件\同花顺\system\同花顺方案\StockBlock.ini

StockBlock.ini 内容更大,混合了概念、行业、指数、风格、自选、系统板块等。如果只需要概念,优先用 BlockUpdate\block_conception.ini,更干净。

本机当前解析结果:

block_conception.ini
概念名称数:396
有成分股的概念板块:388

block_industry.ini
行业名称数:356
有成分股的行业板块:257

StockBlock.ini
板块名称数:2880
有成分股的板块:2209

同花顺 ini 文件结构

block_conception.ini 是 GBK 编码的 ini 风格文件,核心 section 是两个:

[BLOCK_NAME_MAP_TABLE]
CBE8=氢能源
...

[BLOCK_STOCK_CONTEXT]
CBE8=33:000009,33:000027,17:600028,...
...

含义:

  • [BLOCK_NAME_MAP_TABLE]:板块 ID 到板块名称。
  • [BLOCK_STOCK_CONTEXT]:板块 ID 到成分股列表。
  • 33:000001 一般表示深市或普通 A 股代码。
  • 17:600000 一般表示沪市代码。
  • 实际做股票匹配时,可以先取冒号后面的六位代码。

读取同花顺概念成分股

from pathlib import Path
import pandas as pd

def read_ths_block_ini(path: Path):
    text = path.read_text(encoding="gbk", errors="replace")
    section = None
    names = {}
    members = {}

    for raw_line in text.splitlines():
        line = raw_line.strip()
        if not line or line.startswith(";"):
            continue
        if line.startswith("[") and line.endswith("]"):
            section = line[1:-1]
            continue
        if "=" not in line:
            continue

        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip()

        if section == "BLOCK_NAME_MAP_TABLE" and value:
            names[key] = value
        elif section == "BLOCK_STOCK_CONTEXT" and value:
            codes = []
            for item in value.split(","):
                item = item.strip()
                if not item:
                    continue
                code = item.split(":")[-1]
                if len(code) == 6:
                    codes.append(code)
            members[key] = codes

    rows = []
    for block_id, codes in members.items():
        block_name = names.get(block_id, block_id)
        for i, code in enumerate(codes):
            rows.append({
                "block_id": block_id,
                "block_name": block_name,
                "code_index": i,
                "code": code,
            })

    return pd.DataFrame(rows)

ths = Path(r"C:\同花顺软件\同花顺")
concept = read_ths_block_ini(ths / "BlockUpdate" / "block_conception.ini")
industry = read_ths_block_ini(ths / "BlockUpdate" / "block_industry.ini")

print(concept.head())
print(industry.head())

查询某只股票的同花顺概念

def concepts_of(concept_df: pd.DataFrame, code: str):
    return (
        concept_df[concept_df["code"] == code]["block_name"]
        .drop_duplicates()
        .sort_values()
        .tolist()
    )

print(concepts_of(concept, "601857"))
print(concepts_of(concept, "600519"))
print(concepts_of(concept, "600028"))

本机当前样例:

601857:中国石油
氢能源、融资融券、中字头股票、证金持股、参股保险、央企国企改革、沪股通、一带一路、天然气、页岩气、俄乌冲突概念、碳交易、国企改革、同花顺中特估100、高股息精选

600519:贵州茅台
融资融券、超级品牌、证金持股、白酒概念、沪股通、同花顺漂亮100、国企改革、西部大开发

600028:中国石化
氢能源、融资融券、中字头股票、可燃冰、证金持股、央企国企改革、沪股通、一带一路、充电桩、煤化工概念、天然气、页岩气、国企改革、同花顺中特估100、高股息精选

行业样例:

601857 -> 石油加工
600519 -> 白酒Ⅲ
600028 -> 石油加工

回测时的未来函数问题

gbbq 是事件表,带日期,可以用于历史还原。只要过滤 datetime <= target_date,逻辑上可以避免未来函数。

但通达信和同花顺的概念板块文件,大多是当前缓存快照。例如今天文件里某只股票属于“氢能源”,不代表它在 2018 年已经属于这个概念。用当前概念成分股去做历史回测,会引入未来信息。

实务上可以分三种用法:

  1. 如果只是做当前股票池、当前标签、当前分组分析,可以直接使用这些文件。
  2. 如果做严格历史回测,概念标签需要有历史版本快照,或者每天保存一次 block_conception.ini / block_gn.dat
  3. 如果只是做粗略研究,可以接受这个偏差,但结果不能当作严格可交易回测。

推荐从现在开始定时归档:

data_snapshot/
  2026-06-15/
    tdx_gbbq
    tdx_block_gn.dat
    tdx_tdxhy.cfg
    ths_block_conception.ini
    ths_block_industry.ini

这样后续至少可以从归档日起构建无未来函数的概念板块历史。

小结

本地可直接复用的数据源:

通达信 gbbq:
  适合还原历史总股本、流通股本,再结合 .day 收盘价推导历史市值。

通达信 vipdoc/*.day:
  适合读取本地日线 OHLCV。

通达信 block_gn.dat / block_fg.dat / block_zs.dat:
  适合读取当前概念、风格、指数板块成分股。

通达信 tdxhy.cfg:
  适合读取股票到通达信行业代码的映射。

同花顺 block_conception.ini:
  适合读取当前同花顺概念板块及成分股。

同花顺 block_industry.ini:
  适合读取当前同花顺行业板块及成分股。

同花顺 StockBlock.ini:
  适合读取更全的股票-板块标签,但内容混杂,需要过滤。

对“2023 年 1 月市值最大的 100 个股票”这类任务,最稳的本地方案是:

通达信 gbbq 还原 2023-01-31 的总股本
通达信 .day 读取 2023-01-31 或之前最近交易日收盘价
总市值 = close * total_shares
按总市值排序取前 100
目录
build:   20260615