- K-line: daily+ periods output 'date' only, minute periods output 'datetime' - Transactions (tick-by-tick): combine date param + hour/minute into 'datetime' - XdxrRecord, HistoricalFundFlow: year/month/day merged to 'date' - MinuteBar: rename unknown_1 to _unknown_1 (hidden from DataFrame) - MinuteBar: add datetime column computed from bar index (A-share 240-bar pattern) - get_minute_time_data: use history endpoint only (current-day endpoint broken in pytdx too) - Update all examples to reflect new DataFrame column names
easy-tdx
通达信(tdx)是中国使用最广泛的券商行情终端之一,其私有 TCP 协议长期缺乏官方 SDK。pytdx 率先完成了协议逆向与离线数据读取,为整个生态奠定了基础;mootdx 在此之上做了工程化封装,让更多开发者得以使用;xmtdx 进一步探索了现代 Python 接口设计。
easy-tdx 站在这些项目的肩膀上,从协议层重新实现:LEB128 价格编解码、自定义浮点成交量、帧解压缩与握手——每一层都有对应的离线 fixture 测试。commands 层不含 IO,与 transport 完全解耦;同步 + asyncio 双接口;strict mypy 通过;零运行时依赖;每条记录保留原始字节。覆盖标准行情、扩展市场(期货/港股/外盘)、离线本地数据读取、专业财务数据全场景。
感谢 rainx、mootdx 社区及 minionszyw 的开创性工作——没有他们,就不会有这个项目。
特性
- 零依赖:纯标准库,Python >= 3.10
- 同步 + asyncio 双接口:
TdxClient/AsyncTdxClient,commands 层不含任何 IO - 完整类型注解:strict
mypy+ruff通过 - 高可用传输:同步/异步均支持
ping_all()、from_best_host()、断线自动重连 - 修复 pytdx 已知 bug(见下文)
- 保留原始字节:每条数据记录含
_raw: bytes,未知字段以unknown_N命名而非丢弃 - 保活心跳机制:
AsyncTdxClient自动发送心跳包,确保长连接生产环境稳定性 - 扩展行情:
ExTdxClient/AsyncExTdxClient支持期货、港股、外盘等扩展市场(端口 7727) - 离线数据读取:从本地通达信安装目录直接读取日线、分钟线、财务、板块、股本变迁等数据,无需网络
- 专业财务数据:通过计算服务器下载历史财报 ZIP 文件并解析
安装
pip install -e . # 开发模式
pip install -e ".[dev]" # 含测试/类型检查工具
pip install -e ".[pandas]" # 含 pandas(可选)
快速开始
连接与基本查询
from easy_tdx import TdxClient, Market, KlineCategory
# 手动指定服务器
with TdxClient("180.153.18.170") as c:
count = c.get_security_count(Market.SH)
print(f"沪市证券总数: {count}")
# 自动优选最低延迟服务器
with TdxClient.from_best_host() as c:
bars = c.get_security_bars(Market.SH, "600000", KlineCategory.DAY, 0, 5)
for b in bars:
print(f"{b.year}-{b.month:02d}-{b.day:02d} "
f"开:{b.open:.2f} 高:{b.high:.2f} "
f"低:{b.low:.2f} 收:{b.close:.2f}")
asyncio
import asyncio
from easy_tdx import AsyncTdxClient, Market, KlineCategory
async def main():
async with AsyncTdxClient.from_best_host() as c:
bars = await c.get_security_bars(
Market.SH, "600000", KlineCategory.DAY, 0, 5
)
for bar in bars:
print(f"{bar.year}-{bar.month:02d}-{bar.day:02d} "
f"开:{bar.open:.2f} 高:{bar.high:.2f} "
f"低:{bar.low:.2f} 收:{bar.close:.2f}")
asyncio.run(main())
服务器测速
from easy_tdx import TdxClient
# 测速并排序
results = TdxClient.ping_all()
for host, latency in results:
print(f"{host} {latency * 1000:.0f} ms")
API 参考
连接管理
| 方法 | 说明 |
|---|---|
TdxClient(host, port=7709, timeout=15.0) |
指定服务器创建客户端 |
TdxClient.from_best_host(ping_timeout=5.0) |
自动选延迟最低的服务器 |
TdxClient.ping_all(timeout=5.0) |
并发测速,返回 [(host, seconds), ...] |
AsyncTdxClient / AsyncTdxClient.from_best_host() |
异步版,接口一一对应 |
内置服务器列表 KNOWN_HOSTS(8 台)和计算服务器 CALC_HOSTS(1 台)。
市场信息
with TdxClient.from_best_host() as c:
# 市场证券总数
count = c.get_security_count(Market.SH)
# 证券列表(分页,每页约 1000 条)
stocks = c.get_security_list(Market.SH, start=0)
# stocks[0].code / .name / .pre_close / .industry_tdx / .industry_sw
# 沪深 A 股完整列表(自动挂载行业信息,本地缓存 1 天)
all_stocks = c.get_security_list_all()
# 批量实时五档行情(最多 80 只/次)
quotes = c.get_security_quotes([
(Market.SH, "600000"), # 浦发银行
(Market.SH, "600519"), # 贵州茅台
(Market.SZ, "000001"), # 平安银行
(Market.SZ, "000858"), # 五粮液
])
# quotes[0].price / .pre_close / .open / .high / .low / .bid1..bid5 / .ask1..ask5
# 全市场涨跌统计
stat = c.get_market_stat()
# stat.up_count / .down_count / .neutral_count / .total_amount / .total_market_cap
K 线数据
from easy_tdx import Market, KlineCategory
with TdxClient.from_best_host() as c:
# 个股 K 线
bars = c.get_security_bars(Market.SZ, "002176", KlineCategory.DAY, 0, 100)
# 指数 K 线(常用指数代码:上证 "999999",深成 "399001",创业板 "399006")
bars = c.get_index_bars(Market.SH, "999999", KlineCategory.DAY, 0, 10)
K 线类别:
KlineCategory.MIN_1 MIN_5 MIN_15 MIN_30 MIN_60
KlineCategory.DAY WEEK MONTH YEAR
K 线字段:open close high low vol amount year month day hour minute _raw
分时数据
with TdxClient.from_best_host() as c:
# 今日分时(240 条)
bars = c.get_minute_time_data(Market.SH, "600000")
# 历史某日分时,date 为 YYYYMMDD 格式整数
bars = c.get_history_minute_time_data(Market.SH, "600000", 20250110)
分时字段:price vol unknown_1(原 pytdx 丢弃字段,保留供分析)_raw
逐笔成交
with TdxClient.from_best_host() as c:
# 当日逐笔成交(分页)
records = c.get_transaction_data(Market.SH, "600000", 0, 20)
# 历史逐笔成交
records = c.get_history_transaction_data(Market.SH, "600000", 20250110, 0, 20)
成交字段:hour minute price vol buyorsell(0=买, 1=卖, 2=中性, 8=集合竞价)unknown_last _raw
财务与公司信息
from easy_tdx import XDXR_CATEGORY_NAMES
with TdxClient.from_best_host() as c:
# 除权除息历史
records = c.get_xdxr_info(Market.SH, "600519")
# records[0].fenhong / .songzhuangu / .peigujia / .peigu
# 最新财务数据
info = c.get_finance_info(Market.SH, "600519")
# info.zong_guben / .liutong_guben / .jing_lirun / .zhuying_shouru / ...
# 涨跌停价计算
quotes = c.get_security_quotes([(Market.SH, "600519")])
limit_up, limit_down = c.get_price_limits(
Market.SH, "600519", "贵州茅台", quotes[0].pre_close
)
# 公司信息目录
categories = c.get_company_info_category(Market.SH, "600519")
for cat in categories:
print(cat.name, cat.filename, cat.start, cat.length)
# 公司信息内容
content = c.get_company_info_content(
Market.SH, "600519", cat.filename, cat.start, cat.length
)
板块信息
with TdxClient.from_best_host() as c:
# 行业/指数板块
blocks = c.get_block_info("block_zs.dat")
# 概念板块
blocks = c.get_block_info("block_gn.dat")
# 风格板块
blocks = c.get_block_info("block_fg.dat")
# blocks[0].name / .category / .count / .codes
资金流向
with TdxClient.from_best_host() as c:
# 当日资金流向(超大/大/中/小单)
flow = c.get_fund_flow(Market.SH, "600519")
# flow.super_in / .super_out / .large_in / .large_out / .main_net_inflow
# 历史日线资金流向序列
flows = c.get_history_fund_flow(Market.SH, "600519", 0, 10)
# flows[0].year / .month / .day / .super_in / .main_net_inflow
文件下载
from easy_tdx import CALC_HOSTS
with TdxClient.from_best_host() as c:
# 行情服务器可用的文件
data = c.get_report_file("tdxhy.cfg") # 行业映射配置
data = c.get_report_file("block_gn.dat") # 概念板块
# 计算服务器:专业财务数据
with TdxClient(CALC_HOSTS[0]) as calc:
file_list = calc.get_financial_file_list()
# file_list[0].filename / .filesize / .hash
zip_data = calc.get_financial_file("tdxfin/gpcw20260331.zip")
records = calc.get_financial_records("tdxfin/gpcw20260331.zip")
# records[0].market / .code / .report_date / .fields
扩展行情(期货、港股、外盘)
from easy_tdx import ExTdxClient
# 扩展行情服务器端口 7727
with ExTdxClient() as c:
markets = c.get_markets() # 可用市场列表
count = c.get_instrument_count() # 品种总数
instruments = c.get_instrument_info(0, 50) # 品种信息(分页)
quote = c.get_instrument_quote(market, code) # 单品种行情
# K 线(支持日期范围查询)
bars = c.get_instrument_bars(market, code, category, start, count)
bars = c.get_history_instrument_bars_range(market, code, date_start, date_end)
# 分时 / 逐笔
minute = c.get_minute_time_data(market, code)
trades = c.get_transaction_data(market, code, start, count)
AsyncExTdxClient 提供与同步版对应的 async def 方法。
离线数据读取
从本地通达信安装目录直接读取数据文件,无需网络连接。离线模块的路径检测优先级:
TDX_HOME环境变量- 平台常见路径猜测(Windows:
C:\new_jyplug、C:\new_tdx等)
# Windows
set TDX_HOME=C:\new_jyplug
# Linux/macOS
export TDX_HOME=/opt/new_tdx
日线 K 线
from easy_tdx.offline import detect_tdx_home, read_daily_bars, find_daily_bar_file
from easy_tdx import Market
home = detect_tdx_home()
# 通过 市场+代码 自动定位文件
filepath = find_daily_bar_file(Market.SH, "600000")
bars = read_daily_bars(filepath)
for bar in bars[-10:]:
print(f"{bar.year}-{bar.month:02d}-{bar.day:02d} "
f"开:{bar.open:.2f} 收:{bar.close:.2f} 量:{bar.vol:.0f}")
文件位于 vipdoc/{sh,sz}/lday/,如 sh600000.day。自动识别证券类型(A 股/B 股/指数/基金/债券)并应用对应的价格和成交量系数。
分钟 K 线
from easy_tdx.offline import (
read_5min_bars, read_lc_min_bars,
find_5min_bar_file, find_lc1_bar_file, find_lc5_bar_file,
)
from easy_tdx import Market
# .5 文件(OHLC 为整数 / 100)
filepath = find_5min_bar_file(Market.SH, "600000")
bars = read_5min_bars(filepath)
# .lc1 文件(1 分钟线,OHLC 为浮点数)
filepath = find_lc1_bar_file(Market.SH, "600000")
bars = read_lc_min_bars(filepath)
# .lc5 文件(5 分钟线,OHLC 为浮点数)
filepath = find_lc5_bar_file(Market.SZ, "002176")
bars = read_lc_min_bars(filepath)
文件位于 vipdoc/{sh,sz}/fzline/,如 sh600000.5、sh600000.lc1、sh600000.lc5。
扩展市场日线
from easy_tdx.offline import read_ex_daily_bars
# 期货、港股、外盘等扩展市场数据
# 文件位于 vipdoc/ds/lday/,如 29#A1801.day
bars = read_ex_daily_bars(r"C:\new_jyplug\vipdoc\ds\lday\38#2_CPI.day")
# bar.open / .high / .low / .close / .settlement / .vol
板块数据
from easy_tdx.offline import read_block_dat, read_customer_blocks
# 系统板块(本地 .dat 文件)
blocks = read_block_dat(r"C:\new_jyplug\vipdoc\block_zs.dat")
# blocks[0].name / .category / .count / .codes
# 自定义板块(blocknew 目录)
blocks = read_customer_blocks(r"C:\new_jyplug\T0002\blocknew")
# blocks[0].blockname / .codes
支持本地 .dat 文件离线读取,本地不存在时可通过 TdxClient.get_block_info() 在线获取。
股本变迁
from easy_tdx.offline import read_gbbq
records = read_gbbq(r"C:\new_jyplug\T0002\hq_cache\gbbq")
# records[0].market / .code / .datetime / .category / .hongli_panqianliutong / ...
gbbq 文件使用 XOR 加密存储,读取时自动解密。
历史财务数据
from easy_tdx.offline import read_history_financial
# 支持 .dat 和 .zip 文件(.zip 自动解压)
records = read_history_financial(r"C:\new_jyplug\vipdoc\fin\gpcw20260331.zip")
# records[0].code / .market / .report_date / .fields
文件可通过 TdxClient.get_financial_file_list() 查询可用文件,再用 get_financial_file() 下载到本地。
路径检测
from easy_tdx.offline import detect_tdx_home, resolve_vipdoc
# 自动检测通达信安装目录
home = detect_tdx_home()
# 解析 vipdoc 数据目录
vipdoc = resolve_vipdoc()
vipdoc 目录结构:
vipdoc/
├── sh/lday/ 上海日线 sh600000.day
├── sh/fzline/ 上海分钟线 sh600000.5 / .lc1 / .lc5
├── sz/lday/ 深圳日线 sz000001.day
├── sz/fzline/ 深圳分钟线 sz000001.5 / .lc1 / .lc5
├── ds/lday/ 扩展市场 29#A1801.day
└── fin/ 历史财务 gpcw*.dat / gpcw*.zip
完整 API 列表
TdxClient / AsyncTdxClient
| 方法 | 说明 |
|---|---|
get_security_count(market) |
市场证券总数 |
get_security_list(market, start) |
证券列表(每页约 1000 条) |
get_security_list_all() |
沪深 A 股完整列表(含行业映射,本地缓存 1 天) |
get_security_quotes([(market, code), ...]) |
批量实时五档行情(最多 80 只/次) |
get_price_limits(market, code, name, pre_close) |
计算涨跌停价 |
get_security_bars(market, code, category, start, count) |
个股 K 线 |
get_index_bars(market, code, category, start, count) |
指数 K 线 |
get_minute_time_data(market, code) |
今日分时(240 条) |
get_history_minute_time_data(market, code, date) |
历史分时 |
get_transaction_data(market, code, start, count) |
当日逐笔成交 |
get_history_transaction_data(market, code, date, start, count) |
历史逐笔成交 |
get_fund_flow(market, code) |
当日资金流向 |
get_history_fund_flow(market, code, start, count) |
历史日线资金流向 |
get_xdxr_info(market, code) |
除权除息历史 |
get_finance_info(market, code) |
最新财务数据 |
get_company_info_category(market, code) |
公司信息目录 |
get_company_info_content(market, code, filename, offset, length) |
公司信息文本 |
get_block_info(filename) |
板块信息 |
get_report_file(filename) |
下载服务器文件 |
get_market_stat() |
全市场涨跌统计 |
get_financial_file_list() |
计算服务器财务文件列表 |
get_financial_file(filename) |
下载财务文件 |
get_financial_records(filename) |
下载并解析财务记录 |
ExTdxClient / AsyncExTdxClient
| 方法 | 说明 |
|---|---|
get_markets() |
可用市场列表 |
get_instrument_count() |
品种总数 |
get_instrument_info(start, count) |
品种信息(分页) |
get_instrument_quote(market, code) |
单品种行情 |
get_instrument_quote_list(market, start, count) |
批量行情 |
get_instrument_bars(market, code, category, start, count) |
品种 K 线 |
get_history_instrument_bars_range(market, code, start, end) |
日期范围 K 线 |
get_minute_time_data(market, code) |
分时数据 |
get_history_minute_time_data(market, code, date) |
历史分时 |
get_transaction_data(market, code, start, count) |
逐笔成交 |
get_history_transaction_data(market, code, date, start, count) |
历史逐笔 |
easy_tdx.offline
| 函数 | 说明 |
|---|---|
detect_tdx_home() |
检测通达信安装目录 |
resolve_vipdoc(path) |
解析 vipdoc 数据目录 |
read_daily_bars(filepath) |
读取日线 .day 文件 |
find_daily_bar_file(market, code) |
定位日线文件路径 |
read_5min_bars(filepath) |
读取 .5 分钟线文件 |
read_lc_min_bars(filepath) |
读取 .lc1/.lc5 分钟线文件 |
find_5min_bar_file(market, code) |
定位 .5 文件路径 |
find_lc1_bar_file(market, code) |
定位 .lc1 文件路径 |
find_lc5_bar_file(market, code) |
定位 .lc5 文件路径 |
read_ex_daily_bars(filepath) |
读取扩展市场日线 |
read_block_dat(filepath) |
读取系统板块 .dat 文件 |
read_customer_blocks(block_dir) |
读取自定义板块目录 |
read_gbbq(filepath) |
读取股本变迁文件 |
read_history_financial(filepath) |
读取历史财务数据 |
数据模型
所有 dataclass 字段均有类型注解。每条记录附带 _raw: bytes(原始协议字节)。
SecurityBar(K 线)
open close high low vol amount
year month day hour minute
_raw
SecurityQuote(实时行情)
market code price pre_close open high low
vol cur_vol amount s_vol b_vol
bid1..bid5 bid_vol1..bid_vol5
ask1..ask5 ask_vol1..ask_vol5
server_time
_raw
limit_up / limit_down 默认为 None,涨跌停价应通过 get_price_limits() 计算。
SecurityInfo(证券列表)
market code name volunit decimal_point pre_close
industry_tdx industry_sw
MinuteBar(分时)
price vol unknown_1 _raw
TransactionRecord(逐笔成交)
hour minute price vol buyorsell unknown_last _raw
XdxrRecord(除权除息)
market code year month day category name
fenhong peigujia songzhuangu peigu suogu
xingquanjia fenshu
panqian_liutong panhou_liutong # 万股
qian_zongguben hou_zongguben # 万股
_raw
category == 1 时为现金分红/送转/配股,fenhong / songzhuangu / peigu 已归一化为每股口径。
复权公式
仅使用 category == 1 的 xdxr 记录:
factor = (pre_close - cash + rights * rights_price) / (1 + bonus + rights)
其中 cash = fenhong,bonus = songzhuangu,rights = peigu,rights_price = peigujia,pre_close 为事件前一日未复权收盘价。
- 前复权:事件日前的历史价格连续乘以各次
factor - 后复权:事件日后的价格连续除以各次
factor
FundFlow(资金流向)
super_in/out large_in/out medium_in/out small_in/out
main_net_inflow total_net_inflow
FinanceInfo(财务)
流通股本、总股本、各省份/行业代码、资产负债表及利润表主要科目(30 个 float 字段)。
CompanyInfoCategory(公司信息目录)
name filename start length
TdxBlock(板块信息)
name category count codes
已知限制
get_security_list(Market.BJ, start)当前不能稳定获取(服务器端问题),get_security_list_all()暂不纳入 BJlimit_up/limit_down在SecurityQuote中默认为None,涨跌停价应通过get_price_limits()计算
修复的 pytdx Bug
| # | 位置 | 问题 | 修复 |
|---|---|---|---|
| 1 | xdxr_info |
循环内始终读 body[:7],所有记录字段相同 |
改为从当前 pos 读取,pos 正确推进 |
| 2 | security_list |
GBK 解码截断时 crash | decode('gbk', errors='replace') |
| 3 | security_list |
pre_close 误当作整数价格 /100 |
恢复为通达信自定义浮点解码 |
| 4 | transaction |
最后一个字段被 _ 丢弃 |
保留为 unknown_last |
| 5 | minute_time |
reversed1 字段被丢弃 |
保留为 unknown_1 |
| 6 | xdxr_info |
股本字段用 float(uint32) 直解,差约 374 倍 |
改用 _decode_volume,单位万股,与 FinanceInfo 完全吻合 |
| 7 | security_quotes |
涨停/跌停价映射错误或缺失 | 停止使用不可信协议位,改由业务规则计算 |
架构
src/easy_tdx/
├── client.py # TdxClient / AsyncTdxClient(高层 API)
├── ex/
│ ├── client.py # ExTdxClient / AsyncExTdxClient(扩展行情)
│ └── models.py # 扩展行情数据模型
├── offline/ # 离线数据读取模块
│ ├── paths.py # 路径检测与解析
│ ├── daily_bar.py # 日线读取
│ ├── min_bar.py # 分钟线读取
│ ├── ex_daily_bar.py # 扩展市场日线
│ ├── block.py # 板块数据读取
│ ├── gbbq.py # 股本变迁(XOR 解密)
│ ├── history_financial.py # 历史财务数据
│ └── finders.py # 文件路径定位
├── transport/
│ ├── sync.py # TdxConnection(socket)+ ping_host / ping_all
│ └── async_.py # AsyncTdxConnection(asyncio)
├── commands/ # 每条命令:build_request() + parse_response(),无 IO
├── codec/ # price / volume / datetime / frame 编解码
└── models/ # 纯 dataclass,无业务逻辑
commands 层不依赖 transport,可独立单测。transport 层负责 TCP、握手、帧解压、分发。offline 层直接读取本地二进制文件,不依赖 transport。
协议说明
通达信使用私有二进制 TCP 协议:
- 帧格式:16 字节响应头(含 zipsize / unzipsize),body 按需 zlib 解压
- 价格编码:变长有符号整数(类 LEB128,bit8=继续,bit7=符号,首字节低 6 位 + 后续低 7 位)
- 成交量编码:4 字节自定义浮点(字节 3 = 指数,字节 0-2 = 精度),不可用于价格字段
- 握手:连接后必须顺序发送 3 条 setup 命令,响应丢弃
- 价格存储:整数 x 100,差分编码(相邻 tick 存 delta)
开发
# 单元测试(无需网络)
python -m pytest tests/unit/
# 集成测试(需要网络,默认跳过)
XMTDX_LIVE=1 python -m pytest tests/integration/
# 类型检查
mypy src/
# lint + format
ruff check src/ tests/
ruff format --check src/ tests/