tags: 量化 数据源 通达信 同花顺 回测 [TOC] ## 背景 这次目标不是调用行情软件的界面,而是直接读取本机已经下载到本地的通达信、同花顺数据文件,把它们变成可复用的量化数据源。 核心用途有三个: - 用通达信 `gbbq` 股本变动文件,还原历史某一天的总股本、流通股本,再结合日线收盘价推导历史市值。 - 用通达信本地板块文件读取行业、概念、风格、指数板块成分股。 - 用同花顺本地板块文件读取概念板块、行业板块以及股票成分股。 需要特别注意:股本文件是历史事件表,适合做历史还原;概念/板块文件通常是当前快照,直接用于历史回测会有未来函数风险。 ## 通达信股本文件 gbbq 本机文件位置: ```text D:\dev\tongdaxin(上证指数)\T0002\hq_cache\gbbq D:\dev\tongdaxin(上证指数)\T0002\hq_cache\gbbq.map ``` `gbbq` 是通达信的股本变动、除权除息等事件文件。用 `pytdx` 的 `GbbqReader` 可以直接解析。 本机当前解析结果: ```text 全部事件:196941 条 全部事件日期范围:19900301 到 20260611 股本相关事件:132627 条 股本相关事件日期范围:19901210 到 20260610 ``` 示例股票: ```text 000001:股本记录从 19910403 到 20250630 600028:中国石化,股本记录从 20011108 到 20251230 601857:中国石油,股本记录从 20071105 到 20131108 ``` ### 字段含义 `pytdx.reader.gbbq_reader.GbbqReader().get_df()` 解析后的主要字段: ```text market code datetime category hongli_panqianliutong peigujia_qianzongguben songgu_qianzongguben peigu_houzongguben ``` `category` 里,常见分类含义如下: ```text 1 除权除息 2 送配股上市 3 非流通股上市 4 未知股本变动 5 股本变化 6 增发新股 7 股份回购 8 增发新股上市 9 转配股上市 10 可转债上市 11 扩缩股 12 非流通股缩股 ``` 做历史市值时,不要把 `category=1` 当作股本记录。`category=1` 是分红、送股、配股价等除权除息信息,字段语义和股本变化行不同。 对股本相关行,可以按下面方式理解: ```text hongli_panqianliutong 变动前流通股本 peigujia_qianzongguben 变动前总股本 songgu_qianzongguben 变动后流通股本 peigu_houzongguben 变动后总股本 ``` 单位通常是“万股”。例如中国石油的总股本字段 `18302098.0`,对应: ```text 18302098 万股 = 183020980000 股 ``` ### 用 gbbq 还原历史股本 `gbbq` 是事件表,不是每日快照。要得到某只股票在某一天的股本,需要取这只股票在目标日期之前最后一条有效股本事件。 例如要还原 `2023-01-31` 的股本: ```python 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 ``` ## 通达信日线文件 通达信日线文件在: ```text D:\dev\tongdaxin(上证指数)\vipdoc\sh\lday D:\dev\tongdaxin(上证指数)\vipdoc\sz\lday ``` 文件名类似: ```text sh601857.day sz000001.day ``` `.day` 文件每条记录 32 字节,小端格式: ```text date int32 YYYYMMDD open int32 价格 * 100 high int32 价格 * 100 low int32 价格 * 100 close int32 价格 * 100 amount float volume int32 reserved int32 ``` 读取示例: ```python 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(" 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(" 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"]]) ``` ## 通达信板块文件 通达信本地板块文件主要在: ```text D:\dev\tongdaxin(上证指数)\T0002\hq_cache ``` 常用文件: ```text 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` 直接读取。 ```python 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()) ``` 输出字段: ```text blockname 板块名 block_type 板块类型 code_index 成分股顺序 code 股票代码 ``` 本机当前 `block_gn.dat` 解析出 `40653` 条概念-股票关系。 ### 读取通达信行业分类 `tdxhy.cfg` 是文本文件,每行大致类似: ```text 0|000001|T1001|||X500102 0|000002|T110201|||X530101 1|600028|T010101|||X620101 ``` 可以按 `|` 分割: ```python 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` 这种已经带名称和成分股的文件。 ## 同花顺概念和行业文件 同花顺安装路径: ```text C:\同花顺软件\同花顺\hexin.exe ``` 核心板块文件在: ```text C:\同花顺软件\同花顺\BlockUpdate ``` 最有用的几个文件: ```text block_conception.ini A 股概念板块 block_industry.ini A 股行业板块 block_region.ini 地域板块 block_every_day.ini 每日/热点类板块 block_tree.ini 板块树 ``` 另外还有一个全集缓存: ```text C:\同花顺软件\同花顺\system\同花顺方案\StockBlock.ini ``` `StockBlock.ini` 内容更大,混合了概念、行业、指数、风格、自选、系统板块等。如果只需要概念,优先用 `BlockUpdate\block_conception.ini`,更干净。 本机当前解析结果: ```text block_conception.ini 概念名称数:396 有成分股的概念板块:388 block_industry.ini 行业名称数:356 有成分股的行业板块:257 StockBlock.ini 板块名称数:2880 有成分股的板块:2209 ``` ### 同花顺 ini 文件结构 `block_conception.ini` 是 GBK 编码的 ini 风格文件,核心 section 是两个: ```text [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` 一般表示沪市代码。 - 实际做股票匹配时,可以先取冒号后面的六位代码。 ### 读取同花顺概念成分股 ```python 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()) ``` ### 查询某只股票的同花顺概念 ```python 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")) ``` 本机当前样例: ```text 601857:中国石油 氢能源、融资融券、中字头股票、证金持股、参股保险、央企国企改革、沪股通、一带一路、天然气、页岩气、俄乌冲突概念、碳交易、国企改革、同花顺中特估100、高股息精选 600519:贵州茅台 融资融券、超级品牌、证金持股、白酒概念、沪股通、同花顺漂亮100、国企改革、西部大开发 600028:中国石化 氢能源、融资融券、中字头股票、可燃冰、证金持股、央企国企改革、沪股通、一带一路、充电桩、煤化工概念、天然气、页岩气、国企改革、同花顺中特估100、高股息精选 ``` 行业样例: ```text 601857 -> 石油加工 600519 -> 白酒Ⅲ 600028 -> 石油加工 ``` ## 回测时的未来函数问题 `gbbq` 是事件表,带日期,可以用于历史还原。只要过滤 `datetime <= target_date`,逻辑上可以避免未来函数。 但通达信和同花顺的概念板块文件,大多是当前缓存快照。例如今天文件里某只股票属于“氢能源”,不代表它在 2018 年已经属于这个概念。用当前概念成分股去做历史回测,会引入未来信息。 实务上可以分三种用法: 1. 如果只是做当前股票池、当前标签、当前分组分析,可以直接使用这些文件。 2. 如果做严格历史回测,概念标签需要有历史版本快照,或者每天保存一次 `block_conception.ini` / `block_gn.dat`。 3. 如果只是做粗略研究,可以接受这个偏差,但结果不能当作严格可交易回测。 推荐从现在开始定时归档: ```text data_snapshot/ 2026-06-15/ tdx_gbbq tdx_block_gn.dat tdx_tdxhy.cfg ths_block_conception.ini ths_block_industry.ini ``` 这样后续至少可以从归档日起构建无未来函数的概念板块历史。 ## 小结 本地可直接复用的数据源: ```text 通达信 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 个股票”这类任务,最稳的本地方案是: ```text 通达信 gbbq 还原 2023-01-31 的总股本 通达信 .day 读取 2023-01-31 或之前最近交易日收盘价 总市值 = close * total_shares 按总市值排序取前 100 ```