背景
这次目标不是调用行情软件的界面,而是直接读取本机已经下载到本地的通达信、同花顺数据文件,把它们变成可复用的量化数据源。
核心用途有三个:
- 用通达信
gbbq股本变动文件,还原历史某一天的总股本、流通股本,再结合日线收盘价推导历史市值。 - 用通达信本地板块文件读取行业、概念、风格、指数板块成分股。
- 用同花顺本地板块文件读取概念板块、行业板块以及股票成分股。
需要特别注意:股本文件是历史事件表,适合做历史还原;概念/板块文件通常是当前快照,直接用于历史回测会有未来函数风险。
通达信股本文件 gbbq
本机文件位置:
D:\dev\tongdaxin(上证指数)\T0002\hq_cache\gbbq
D:\dev\tongdaxin(上证指数)\T0002\hq_cache\gbbq.map
gbbq 是通达信的股本变动、除权除息等事件文件。用 pytdx 的 GbbqReader 可以直接解析。
本机当前解析结果:
全部事件: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
完整流程:
- 读取
gbbq。 - 过滤目标日期之前的股本相关事件。
- 每只股票取最后一条股本事件。
- 读取目标日期或目标日期之前最近一个交易日的
.day收盘价。 - 用收盘价乘总股本或流通股本。
- 排序得到历史某一天的市值排名。
示例骨架:
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 年已经属于这个概念。用当前概念成分股去做历史回测,会引入未来信息。
实务上可以分三种用法:
- 如果只是做当前股票池、当前标签、当前分组分析,可以直接使用这些文件。
- 如果做严格历史回测,概念标签需要有历史版本快照,或者每天保存一次
block_conception.ini/block_gn.dat。 - 如果只是做粗略研究,可以接受这个偏差,但结果不能当作严格可交易回测。
推荐从现在开始定时归档:
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