tdxdata 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tdxdata-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tdxdata contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,42 @@
1
+ # Third-Party Notices
2
+
3
+ 本项目(tdxdata)为 MIT License,但部分解码逻辑参考/移植自第三方开源项目。依据各项目许可证要求,特此声明:
4
+
5
+ ## pytdx (MIT License)
6
+
7
+ - 仓库: https://github.com/rainx/pytdx (作者 rainx,已归档)
8
+ - 许可证: MIT — Copyright (c) rainx
9
+ - 借用内容:
10
+ - `src/tdxdata/commands/quotes.py` — `get_price()` 变长整数(带符号)解码函数
11
+ - `src/tdxdata/commands/bars.py` — 历史K线响应解码思路(日期/差分价格/vol/amount 布局与 `GetSecurityBars.parseResponse` 等价,tdxdata 独立实现请求构造)
12
+ - 用途: 旧命令(bars/quotes/finance)在 2026-09 新版协议服务器上仅需改版本标识 `08 64 01 01 → 18 7b 00 01` 即可原样复用,因此解码层沿用经过验证的 pytdx 实现,请求构造与协议适配为 tdxdata 独立逆向成果。
13
+
14
+ pytdx 的 MIT 许可证全文:
15
+
16
+ ```
17
+ MIT License
18
+
19
+ Copyright (c) rainx
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a copy
22
+ of this software and associated documentation files (the "Software"), to deal
23
+ in the Software without restriction, including without limitation the rights
24
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
+ copies of the Software, and to permit persons to whom the Software is
26
+ furnished to do so, subject to the following conditions:
27
+
28
+ The above copyright notice and this permission notice shall be included in all
29
+ copies or substantial portions of the Software.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ SOFTWARE.
38
+ ```
39
+
40
+ ## 协议逆向说明
41
+
42
+ TCP 7709 协议本身的版本标识、命令族(0c02 init / 0c03 tdxlevel / 0c09 snapshot / 0c1f finance / 0x010c 行情命令)、响应 zlib 流式压缩等为 tdxdata 通过抓包逆向所得(见 `docs/analysis_round1~3.md`),不属第三方代码。
tdxdata-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdxdata
3
+ Version: 0.1.0
4
+ Summary: TDX (通达信) 2026-09 新版行情协议客户端 — realtime A-share quotes
5
+ Author: tdxdata contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 tdxdata contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Keywords: tdx,通达信,a-share,stock,quotes,pytdx
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Topic :: Office/Business :: Financial :: Investment
32
+ Requires-Python: >=3.10
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ License-File: NOTICE.md
36
+ Dynamic: license-file
37
+
38
+ # tdxdata
39
+
40
+ 通达信(TDX)新版行情协议 Python 客户端 — 2026-09 协议逆向适配。
41
+
42
+ 老 pytdx / mootdx / xmtdx 的协议 2026-09-10 起被通达信服务器拒绝(服务端协议改动:版本标识 + 命令族 + zlib 流式压缩)。tdxdata 是逆向新协议后的独立实现。
43
+
44
+ **A-share (CN) realtime quote client for TDX's 2026-09 protocol.**
45
+
46
+ ## 快速开始
47
+
48
+ ```bash
49
+ pip install -e .
50
+ ```
51
+
52
+ ```python
53
+ from tdxdata import TdxClient
54
+
55
+ c = TdxClient() # 默认连 101.35.121.35(43 主站之一,见 servers.txt)
56
+ c.connect() # 0c02 init + 0c03 tdxlevel 认证
57
+ c.snap('600519') # 分时快照: 最新价 + 当日分时点
58
+ c.bars(9, '000059', 0, 100) # 历史K线: 9=日线 0=5min 8=1min 6=周 7=月
59
+ c.quotes([(0, '000059')]) # 盘口报价: 现价/昨收/高低/量/额/五档
60
+ c.finance('000059') # 财务: 流通/总股本/净利润/每股净资产
61
+ c.close()
62
+ ```
63
+
64
+ 全市场并发快照(多主站摊掉每请求 ~3s 延迟):
65
+
66
+ ```python
67
+ from tdxdata.pool2 import Pool
68
+
69
+ p = Pool() # 43 个主站 IP × 并发连接
70
+ r = p.snap('600519') # 单只: {'last': 1277.56, 'prices': [32点分时...]}
71
+ rows = p.snap_all(['000001', '600519']) # 并发全列表 → [(code, last, ok)]
72
+ ```
73
+
74
+ 支持深市 / 沪市 / 指数(market 自动推断,也可显式传):
75
+
76
+ ```python
77
+ p.snap('000001') # 平安银行(深 market=0)
78
+ p.snap('000001', market=1) # 上证指数(沪 market=1)
79
+ p.snap('688981', market=1) # 中芯国际(科创板)
80
+ ```
81
+
82
+ CLI 连通性自检:
83
+
84
+ ```bash
85
+ tdxdata-probe # 探测 600519 的 snap/bars/quotes/finance
86
+ tdxdata-probe --codes 000001,600519 --host 101.35.121.35
87
+ ```
88
+
89
+ ## 功能
90
+
91
+ | 接口 | 命令 | 状态 |
92
+ |---|---|---|
93
+ | 分时快照(最新价+32-51点分时) | 0c09 | ✅ 深沪深+指数 |
94
+ | 历史K线(日/5min/15/30/60/1min/周/月) | 0x010c (GetSecurityBars) | ✅ 单只任意历史段 |
95
+ | 盘口报价(现价/昨收/高低/量/额/五档) | 0x010c (GetSecurityQuotes) | ✅ |
96
+ | 财务(流通/总股本/净利润/每股净资产) | 0c1f | ✅ |
97
+ | 多主站并发(43 IP 轮询、坏 IP 剔除) | — | ✅ 全市场快照 ~12-15 分钟 |
98
+ | 批量快照(单包多只) | 0c01 身份会话 | ⚠️ 服务端需 0c01 会话,已放弃(单只循环+并发替代) |
99
+ | 全量历史K线(单请求批量全市场) | — | ❌ 新版客户端用本地文件+增量,服务器无此接口(单只循环可拉任意股票) |
100
+
101
+ ## 已知限制
102
+
103
+ - **每连接每请求固定 ~3s 延迟**(服务器行为)→ 用多连接并发摊掉,勿压单连接频率
104
+ - 单出口并发 >60 会触发出口级限流(热状态,需长冷却)→ 默认 43 连接
105
+ - 3 个主站 IP 连接即 reset(已从 servers.txt 剔除)
106
+ - 大数据量响应为 zlib 压缩流(小数据量明文),客户端已统一处理(`client.maybe_decompress`)
107
+ - 少数股票 5min 含 13:00 一根 bar(服务器行为,与旧 pytdx 主库一致)
108
+ - 无效代码(不存在的股票代码)返回未定义数据(bars 可能含乱码日期、quotes 可能重定向到其他代码),调用方需保证传入有效代码
109
+ - quotes 对基金(ETF)价格精度为 0.001 元 → 返回价 ×10(510300 实际 4.523 会显示 45.23,pytdx 同款行为);基金行情请用 snap/bars(float32 直读,精度正确)
110
+ - 市场自动判断规则:5/6/9 开头=沪(5 基金/6 A/9 B),0/1/2/3 开头=深(0 A/1 基金/2 B/3 创业板);北交所(4/8/92)暂不支持
111
+
112
+ ## 协议要点(完整版见 docs/protocol.md)
113
+
114
+ ```
115
+ TCP 7709 → 0c02(init) → 0c03(tdxlevel) → 数据请求
116
+ 单只快照 = 0c09 0041 0300 2700 2700 d10f [market] [code6] + 29B 后缀
117
+ market: 0=深市 1=沪市(同 code 不同 market = 不同标的!)
118
+ 历史K线 = 0x010c + 版本标识 18 7b 00 01(老 pytdx 命令仅改此标识即复用)
119
+ ```
120
+
121
+ ## 借用与版权
122
+
123
+ 解码层部分逻辑参考 pytdx(MIT, rainx)——详见 [NOTICE.md](NOTICE.md)。协议逆向过程见 [docs/](docs/)(protocol.md / extractable.md / analysis_round1~3.md)。
124
+
125
+ ## License
126
+
127
+ MIT — 详见 [LICENSE](LICENSE)。
128
+
129
+ > ⚠️ 仅供学习研究。行情数据版权归交易所所有;请遵守通达信服务条款,控制请求频率。
@@ -0,0 +1,92 @@
1
+ # tdxdata
2
+
3
+ 通达信(TDX)新版行情协议 Python 客户端 — 2026-09 协议逆向适配。
4
+
5
+ 老 pytdx / mootdx / xmtdx 的协议 2026-09-10 起被通达信服务器拒绝(服务端协议改动:版本标识 + 命令族 + zlib 流式压缩)。tdxdata 是逆向新协议后的独立实现。
6
+
7
+ **A-share (CN) realtime quote client for TDX's 2026-09 protocol.**
8
+
9
+ ## 快速开始
10
+
11
+ ```bash
12
+ pip install -e .
13
+ ```
14
+
15
+ ```python
16
+ from tdxdata import TdxClient
17
+
18
+ c = TdxClient() # 默认连 101.35.121.35(43 主站之一,见 servers.txt)
19
+ c.connect() # 0c02 init + 0c03 tdxlevel 认证
20
+ c.snap('600519') # 分时快照: 最新价 + 当日分时点
21
+ c.bars(9, '000059', 0, 100) # 历史K线: 9=日线 0=5min 8=1min 6=周 7=月
22
+ c.quotes([(0, '000059')]) # 盘口报价: 现价/昨收/高低/量/额/五档
23
+ c.finance('000059') # 财务: 流通/总股本/净利润/每股净资产
24
+ c.close()
25
+ ```
26
+
27
+ 全市场并发快照(多主站摊掉每请求 ~3s 延迟):
28
+
29
+ ```python
30
+ from tdxdata.pool2 import Pool
31
+
32
+ p = Pool() # 43 个主站 IP × 并发连接
33
+ r = p.snap('600519') # 单只: {'last': 1277.56, 'prices': [32点分时...]}
34
+ rows = p.snap_all(['000001', '600519']) # 并发全列表 → [(code, last, ok)]
35
+ ```
36
+
37
+ 支持深市 / 沪市 / 指数(market 自动推断,也可显式传):
38
+
39
+ ```python
40
+ p.snap('000001') # 平安银行(深 market=0)
41
+ p.snap('000001', market=1) # 上证指数(沪 market=1)
42
+ p.snap('688981', market=1) # 中芯国际(科创板)
43
+ ```
44
+
45
+ CLI 连通性自检:
46
+
47
+ ```bash
48
+ tdxdata-probe # 探测 600519 的 snap/bars/quotes/finance
49
+ tdxdata-probe --codes 000001,600519 --host 101.35.121.35
50
+ ```
51
+
52
+ ## 功能
53
+
54
+ | 接口 | 命令 | 状态 |
55
+ |---|---|---|
56
+ | 分时快照(最新价+32-51点分时) | 0c09 | ✅ 深沪深+指数 |
57
+ | 历史K线(日/5min/15/30/60/1min/周/月) | 0x010c (GetSecurityBars) | ✅ 单只任意历史段 |
58
+ | 盘口报价(现价/昨收/高低/量/额/五档) | 0x010c (GetSecurityQuotes) | ✅ |
59
+ | 财务(流通/总股本/净利润/每股净资产) | 0c1f | ✅ |
60
+ | 多主站并发(43 IP 轮询、坏 IP 剔除) | — | ✅ 全市场快照 ~12-15 分钟 |
61
+ | 批量快照(单包多只) | 0c01 身份会话 | ⚠️ 服务端需 0c01 会话,已放弃(单只循环+并发替代) |
62
+ | 全量历史K线(单请求批量全市场) | — | ❌ 新版客户端用本地文件+增量,服务器无此接口(单只循环可拉任意股票) |
63
+
64
+ ## 已知限制
65
+
66
+ - **每连接每请求固定 ~3s 延迟**(服务器行为)→ 用多连接并发摊掉,勿压单连接频率
67
+ - 单出口并发 >60 会触发出口级限流(热状态,需长冷却)→ 默认 43 连接
68
+ - 3 个主站 IP 连接即 reset(已从 servers.txt 剔除)
69
+ - 大数据量响应为 zlib 压缩流(小数据量明文),客户端已统一处理(`client.maybe_decompress`)
70
+ - 少数股票 5min 含 13:00 一根 bar(服务器行为,与旧 pytdx 主库一致)
71
+ - 无效代码(不存在的股票代码)返回未定义数据(bars 可能含乱码日期、quotes 可能重定向到其他代码),调用方需保证传入有效代码
72
+ - quotes 对基金(ETF)价格精度为 0.001 元 → 返回价 ×10(510300 实际 4.523 会显示 45.23,pytdx 同款行为);基金行情请用 snap/bars(float32 直读,精度正确)
73
+ - 市场自动判断规则:5/6/9 开头=沪(5 基金/6 A/9 B),0/1/2/3 开头=深(0 A/1 基金/2 B/3 创业板);北交所(4/8/92)暂不支持
74
+
75
+ ## 协议要点(完整版见 docs/protocol.md)
76
+
77
+ ```
78
+ TCP 7709 → 0c02(init) → 0c03(tdxlevel) → 数据请求
79
+ 单只快照 = 0c09 0041 0300 2700 2700 d10f [market] [code6] + 29B 后缀
80
+ market: 0=深市 1=沪市(同 code 不同 market = 不同标的!)
81
+ 历史K线 = 0x010c + 版本标识 18 7b 00 01(老 pytdx 命令仅改此标识即复用)
82
+ ```
83
+
84
+ ## 借用与版权
85
+
86
+ 解码层部分逻辑参考 pytdx(MIT, rainx)——详见 [NOTICE.md](NOTICE.md)。协议逆向过程见 [docs/](docs/)(protocol.md / extractable.md / analysis_round1~3.md)。
87
+
88
+ ## License
89
+
90
+ MIT — 详见 [LICENSE](LICENSE)。
91
+
92
+ > ⚠️ 仅供学习研究。行情数据版权归交易所所有;请遵守通达信服务条款,控制请求频率。
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tdxdata"
7
+ version = "0.1.0"
8
+ description = "TDX (通达信) 2026-09 新版行情协议客户端 — realtime A-share quotes"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "tdxdata contributors" }]
13
+ keywords = ["tdx", "通达信", "a-share", "stock", "quotes", "pytdx"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Office/Business :: Financial :: Investment",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.scripts]
23
+ tdxdata-probe = "tdxdata.cli:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ tdxdata = ["servers.txt"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ # -*- coding: utf-8 -*-
2
+ """tdxdata — 通达信 2026-09 新版行情协议客户端
3
+
4
+ 核心能力(全实测 2026-09-15):
5
+ - snap: 分时快照(最新价 + 32-51 点当日分时,深沪深+指数)
6
+ - bars: 历史K线(日/5min/15/30/60/1min/周/月) — 旧命令+新版本标识 18 7b 00 01
7
+ - quotes: 盘口报价(现价/昨收/高低/量/额/五档) — 旧格式原样可用
8
+ - finance: 财务(流通/总股本/净利润/每股净资产) — 0c1f
9
+ - 0c02/0c03 打底连接,无需 0c01 身份(批量接口需 0c01,已放弃)
10
+ """
11
+ from .client import TdxClient, market_of, parse_snap
12
+
13
+ __version__ = "0.1.0"
14
+ __all__ = ["TdxClient", "market_of", "parse_snap", "__version__"]
@@ -0,0 +1,50 @@
1
+ # -*- coding: utf-8 -*-
2
+ """tdxdata-probe CLI — 连通性自检 + 快速行情探测
3
+
4
+ 用法:
5
+ tdxdata-probe # 默认 101.35.121.35, 探测 600519
6
+ tdxdata-probe --host 1.2.3.4 --codes 000001,600519
7
+ tdxdata-probe --snap-only # 只测分时快照(不测 bars/quotes/finance)
8
+ """
9
+ import argparse, sys
10
+
11
+ from .client import TdxClient, market_of
12
+
13
+
14
+ def probe(host: str, codes, snap_only: bool) -> int:
15
+ c = TdxClient(host)
16
+ try:
17
+ c.connect()
18
+ except Exception as e:
19
+ print(f'[connect] FAIL {host}: {e}')
20
+ return 1
21
+ print(f'[connect] OK {host}')
22
+ for code in codes:
23
+ m = market_of(code)
24
+ s = c.snap(code, m)
25
+ print(f'[snap] {code} (m{m}): last={s.get("last")} ok={s.get("ok")} pts={len(s.get("prices") or [])}')
26
+ if snap_only:
27
+ continue
28
+ q = c.quotes([(m, code)])
29
+ q0 = q[0] if q else {}
30
+ print(f'[quote] {code}: 现价={q0.get("price")} 昨收={q0.get("last_close")} 高={q0.get("high")} 低={q0.get("low")}')
31
+ f = c.finance(code, m)
32
+ print(f'[fin] {code}: 流通={f.get("liutongguben", 0)/1e8:.2f}亿 净利={f.get("net_profit")}')
33
+ b = c.bars(9, code, 0, 2, m)
34
+ print(f'[bars] {code}: 日线{len(b)}根 最新={b[-1]["datetime"]} c={b[-1]["close"]}' if b else f'[bars] {code}: 无')
35
+ c.close()
36
+ return 0
37
+
38
+
39
+ def main():
40
+ ap = argparse.ArgumentParser(description='tdxdata 连通性自检')
41
+ ap.add_argument('--host', default='101.35.121.35')
42
+ ap.add_argument('--codes', default='600519', help='逗号分隔代码列表')
43
+ ap.add_argument('--snap-only', action='store_true')
44
+ args = ap.parse_args()
45
+ codes = [c.strip() for c in args.codes.split(',') if c.strip()]
46
+ sys.exit(probe(args.host, codes, args.snap_only))
47
+
48
+
49
+ if __name__ == '__main__':
50
+ main()
@@ -0,0 +1,165 @@
1
+ # -*- coding: utf-8 -*-
2
+ """tdxdata 统一客户端入口
3
+
4
+ 用法:
5
+ c = TdxClient('101.35.121.35')
6
+ c.connect() # 0c02 init + 0c03 tdxlevel(无需 0c01)
7
+ c.snap('600519') # 分时快照(最新价 + 32-51 点当日分时)
8
+ c.bars(9, '000059', 0, 10) # 历史K线(9=日线 0=5min 3=60min 6=周 7=月 8=1min)
9
+ c.quotes([(0, '000059')]) # 盘口报价(现价/昨收/高低/量/额/五档)
10
+ c.finance(0, '000059') # 财务(流通股本/总股本/净利润/每股净资产)
11
+ c.close()
12
+
13
+ 协议要点(2026-09 逆向):
14
+ - 服务器认 0c02/0c03 即可用普通命令;批量接口需 0c01 身份(放弃,用单只循环+多IP并发)
15
+ - 老 pytdx 命令(bars/quotes/finance)只改版本标识 18 7b 00 01 即可用
16
+ """
17
+ import socket, time
18
+
19
+ from .commands.auth import build_auth
20
+ from .commands.bars import build_bars_req, parse_bars_response
21
+ from .commands.finance import build_finance_req, parse_finance
22
+ from .commands.quotes import build_quotes_req, parse_quotes
23
+
24
+ DEFAULT_HOST = '101.35.121.35'
25
+ PORT = 7709
26
+ C02 = bytes.fromhex("0c0218940001030003000d0001")
27
+
28
+ SNAP_PREFIX = bytes.fromhex("0c090041030027002700d10f")
29
+ SNAP_SUFFIX = bytes.fromhex("0000000000000000000000000000000001001400000000010000000000")
30
+
31
+
32
+ def market_of(code: str) -> int:
33
+ """0=深市 1=沪市(000001+1=上证指数)
34
+ ⚠️ 5 开头是沪市基金(510300 沪深300ETF/588000 科创50 等),不能按深市处理"""
35
+ return 1 if code.startswith(('5', '6', '9')) else 0
36
+
37
+
38
+ def maybe_decompress(resp: bytes) -> bytes:
39
+ """服务器对大数据量响应返回 zlib 压缩(小数据量明文),统一解压"""
40
+ import zlib
41
+ i = resp.find(b'\x78\x9c')
42
+ if i >= 0:
43
+ try:
44
+ return resp[:i] + zlib.decompress(resp[i:])
45
+ except Exception:
46
+ pass
47
+ return resp
48
+
49
+
50
+ class TdxClient:
51
+ def __init__(self, host: str = DEFAULT_HOST, port: int = PORT):
52
+ self.host, self.port = host, port
53
+ self.sock = None
54
+
55
+ def connect(self):
56
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
57
+ self.sock.settimeout(4)
58
+ self.sock.connect((self.host, self.port))
59
+ self._send_wait(C02)
60
+ self._send_wait(build_auth())
61
+
62
+ def _send_wait(self, payload: bytes, secs: float = 1.2):
63
+ self.sock.sendall(payload)
64
+ time.sleep(0.08)
65
+ self._recv(secs)
66
+
67
+ def _recv(self, secs: float = 3.0) -> bytes:
68
+ buf = b''
69
+ end = time.time() + secs
70
+ got = None
71
+ while time.time() < end:
72
+ try:
73
+ ch = self.sock.recv(65536)
74
+ except socket.timeout:
75
+ break
76
+ if not ch:
77
+ break
78
+ buf += ch
79
+ if got is None:
80
+ got = time.time()
81
+ end = min(end, got + 0.5) # 首个数据后多等 0.5s 收残余 TCP 段(防半帧错位)
82
+ return buf
83
+
84
+ def _req(self, payload: bytes, secs: float = 3.0) -> bytes:
85
+ self.sock.sendall(payload)
86
+ return self._recv(secs)
87
+
88
+ # ---- 分时快照 ----
89
+ def snap(self, code: str, market: int = None):
90
+ import struct
91
+ m = market if market is not None else market_of(code)
92
+ req = SNAP_PREFIX + bytes([m, 0]) + code.encode() + SNAP_SUFFIX
93
+ r = self._req(req)
94
+ return parse_snap(r)
95
+
96
+ # ---- 历史 K 线 ----
97
+ def bars(self, category: int, code: str, start: int = 0, count: int = 100, market: int = None):
98
+ m = market if market is not None else market_of(code)
99
+ r = self._req(build_bars_req(category, m, code, start, count))
100
+ r = maybe_decompress(r)
101
+ if len(r) <= 18:
102
+ return []
103
+ return parse_bars_response(r[16:], category)
104
+
105
+ # ---- 盘口报价 ----
106
+ def quotes(self, items):
107
+ r = self._req(build_quotes_req(items))
108
+ return parse_quotes(maybe_decompress(r))
109
+
110
+ # ---- 财务 ----
111
+ def finance(self, code: str, market: int = None):
112
+ m = market if market is not None else market_of(code)
113
+ r = self._req(build_finance_req(m, code))
114
+ return parse_finance(maybe_decompress(r))
115
+
116
+ def close(self):
117
+ if self.sock:
118
+ try:
119
+ self.sock.close()
120
+ except Exception:
121
+ pass
122
+
123
+
124
+ def parse_snap(resp: bytes) -> dict:
125
+ import struct, zlib
126
+ if len(resp) < 60:
127
+ return {'ok': False, 'raw_len': len(resp)}
128
+ data = resp
129
+ compressed = False
130
+ i789 = resp.find(b'\x78\x9c')
131
+ if i789 >= 0:
132
+ try:
133
+ data = resp[:i789] + zlib.decompress(resp[i789:])
134
+ compressed = True
135
+ except Exception:
136
+ pass
137
+ if len(data) < 60:
138
+ return {'ok': False, 'raw_len': len(resp), 'compressed': compressed}
139
+ code = data[18:24].decode(errors='ignore')
140
+ market = data[17]
141
+ prices = []
142
+ i = 50
143
+ while i + 4 <= len(data):
144
+ v = struct.unpack('<f', data[i:i+4])[0]
145
+ if 0.1 < v < 100000:
146
+ prices.append(round(v, 3))
147
+ i += 4
148
+ else:
149
+ i += 2
150
+ return {'ok': True, 'code': code, 'market': market, 'prices': prices,
151
+ 'last': prices[-1] if prices else None, 'compressed': compressed}
152
+
153
+
154
+ if __name__ == '__main__':
155
+ c = TdxClient()
156
+ c.connect()
157
+ for code in ['000059', '600519']:
158
+ m = market_of(code)
159
+ s = c.snap(code)
160
+ q = c.quotes([(m, code)])
161
+ f = c.finance(code)
162
+ b = c.bars(9, code, 0, 3)
163
+ print(f'{code}: 分时最新={s.get("last")} 报价昨收={q[0]["last_close"] if q else "?"} '
164
+ f'财务流通={f.get("liutongguben", 0)/1e8:.2f}亿 日线{len(b)}根(最新c={b[-1]["close"] if b else "?"})')
165
+ c.close()
@@ -0,0 +1 @@
1
+ """$d 子包"""
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ """tdxlevel 认证命令(0c03,抓包格式)"""
3
+ import struct
4
+
5
+
6
+ def build_auth() -> bytes:
7
+ """构造 tdxlevel 认证请求(抓包原样 42B,实测才能过认证)。
8
+
9
+ 0c03 1899 0001 2000 2000 db0f 'tdxlevel' 00000000 <float 7.918> 11 00...00 05
10
+ ⚠️ 注意必须 42B(曾少 1B 导致服务器不响应)
11
+ """
12
+ return bytes.fromhex(
13
+ "0c031899000120002000db0f7464786c6576656c"
14
+ "000000295cf740110000000000000000000000000005"
15
+ )
16
+
17
+ def parse_auth_response(buf: bytes) -> dict:
18
+ """解析认证响应(b1cb7400 0c03... 195B)。"""
19
+ if len(buf) < 8 or buf[4:6] != b"\x0c\x03":
20
+ return {"ok": False, "raw": buf[:16].hex()}
21
+ # b300 b300 = 行情级别数量?(179)
22
+ level = struct.unpack("<H", buf[16:18])[0] if len(buf) >= 18 else 0
23
+ return {"ok": True, "level_hint": level, "raw_len": len(buf)}
@@ -0,0 +1,83 @@
1
+ # -*- coding: utf-8 -*-
2
+ """GetSecurityBars 历史K线接口 — 旧 pytdx 请求 + 新版本标识(18 7b 00 01)
3
+
4
+ 2026-09-15 实测: 日线/5min/15min/30min/60min/1min 全周期可用(000059 数据正确)
5
+ 关键: 老格式请求需把版本标识 0x01016408(字节 08 64 01 01) 换成 0x01007b18(字节 18 7b 00 01)
6
+ 解码逻辑参考 pytdx (MIT) rainx/pytdx — get_datetime/get_price/差分价格;请求构造 tdxdata 独立实现。
7
+
8
+ category: 0=5min 1=15min 2=30min 3=60min 4=日线(后复权) 5=? 6=周 7=月 8=1min 9=日线 10=季 11=年
9
+ """
10
+ import struct
11
+
12
+ def build_bars_req(category: int, market: int, code: str, start: int, count: int) -> bytes:
13
+ values = (0x10c, 0x01007b18, 0x1c, 0x1c, 0x052d, market, code.encode(), category, 1, start, count, 0, 0, 0)
14
+ return struct.pack("<HIHHHH6sHHHHIIH", *values)
15
+
16
+ # 推荐直接复用 pytdx.parser.get_security_bars.GetSecurityBarsCmd.parseResponse(响应格式未变)
17
+ def parse_bars_response(body: bytes, category: int):
18
+ """轻量解码(与 pytdx parseResponse 等价):日期 4B + 4×变长价格差分 + vol + amount"""
19
+ from tdxdata.commands.quotes import get_price
20
+ (ret_count,) = struct.unpack("<H", body[0:2])
21
+ pos = 2
22
+ out = []
23
+ pre = 0
24
+ for _ in range(ret_count):
25
+ if category < 4 or category in (7, 8):
26
+ # 分钟线: zipday(u16 高11位年-2004+低11位月日) + tminutes(u16 分钟)
27
+ zipday, tminutes = struct.unpack("<HH", body[pos:pos+4]); pos += 4
28
+ year = (zipday >> 11) + 2004
29
+ month = int((zipday % 2048) / 100)
30
+ day = (zipday % 2048) % 100
31
+ hour = int(tminutes / 60)
32
+ minute = tminutes % 60
33
+ else:
34
+ d = struct.unpack("<I", body[pos:pos+4])[0]; pos += 4
35
+ year, month, day = d // 10000, d // 100 % 100, d % 100
36
+ hour = minute = 0
37
+ od, pos = get_price(body, pos); cd, pos = get_price(body, pos)
38
+ hd, pos = get_price(body, pos); ld, pos = get_price(body, pos)
39
+ (vol_raw,) = struct.unpack("<I", body[pos:pos+4]); pos += 4
40
+ (amt_raw,) = struct.unpack("<I", body[pos:pos+4]); pos += 4
41
+ o = (od + pre) / 1000
42
+ c = (od + pre + cd) / 1000
43
+ h = (od + pre + hd) / 1000
44
+ l = (od + pre + ld) / 1000
45
+ pre = od + cd + pre
46
+ out.append({
47
+ 'datetime': f'{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}',
48
+ 'open': round(o, 3), 'close': round(c, 3), 'high': round(h, 3), 'low': round(l, 3),
49
+ 'vol': vol_raw, 'amount': amt_raw,
50
+ })
51
+ return out
52
+
53
+
54
+ if __name__ == '__main__':
55
+ import socket, time
56
+ HOST = '101.35.121.35'
57
+ C02 = bytes.fromhex("0c0218940001030003000d0001")
58
+ C03 = bytes.fromhex("0c031899000120002000db0f7464786c6576656c000000295cf740110000000000000000000000000005")
59
+ s = socket.socket(); s.settimeout(3)
60
+ s.connect((HOST, 7709))
61
+ for p in (C02, C03):
62
+ s.sendall(p); time.sleep(0.2)
63
+ try:
64
+ s.recv(65536)
65
+ except socket.timeout:
66
+ pass
67
+ for name, cat in [('日线', 9), ('5min', 0), ('周线', 6)]:
68
+ s.sendall(build_bars_req(cat, 0, '000059', 0, 3))
69
+ buf = b''
70
+ end = time.time() + 3
71
+ while time.time() < end:
72
+ try:
73
+ ch = s.recv(65536)
74
+ except socket.timeout:
75
+ break
76
+ if not ch:
77
+ break
78
+ buf += ch
79
+ bars = parse_bars_response(buf[16:], cat) if len(buf) > 20 else []
80
+ print(f'{name}: {len(bars)}根')
81
+ for b in bars:
82
+ print(f' {b["datetime"]} o={b["open"]} c={b["close"]} h={b["high"]} l={b["low"]}')
83
+ s.close()
@@ -0,0 +1,96 @@
1
+ # -*- coding: utf-8 -*-
2
+ """GetFinanceInfo(0c1f) 财务接口 — 旧 pytdx 格式在新服务器原样可用(2026-09-15 实测)
3
+
4
+ 请求 21B: 0c1f 1876 0001 0b00 0b00 1000 0100 + market + code6
5
+ 响应: 帧头 + count(2) + market(1) + code(6) + 流通股本(f32) + 省份(u16) + 行业(u16)
6
+ + 更新日期(u32 yyyymmdd) + IPO(u32) + 总股本..每股净资产(28×f32)
7
+ 单位: 股本=万股(×10000→股); 金额=千元(×1000→元); 股东人数/每股净资产=直接值
8
+ ⚠️ pytdx 把金额类按万元×10000(放大10倍,其 docstring 平安总资产 30 万亿即此 bug),本实现已修正
9
+ """
10
+ import struct
11
+
12
+ TMPL = bytes.fromhex("0c1f187600010b000b0010000100")
13
+
14
+ FIELDS = ['总股本', '国家股', '发起人法人股', '法人股', 'B股', 'H股', '职工股', '总资产', '流动资产',
15
+ '固定资产', '无形资产', '股东人数', '流动负债', '长期负债', '资本公积金', '净资产', '主营收入',
16
+ '主营利润', '应收帐款', '营业利润', '投资收益', '经营现金流', '总现金流', '存货', '利润总额',
17
+ '税后利润', '净利润', '未分利润', '每股净资产', '保留']
18
+ # 单位(2026-09-15 实测 600519/601988 对照财报):
19
+ # 股本类(国家股..职工股) = 万股 → ×10000 得股(liutongguben/zongguben 同)
20
+ # 金额类(总资产..未分利润) = 千元 → ×1000 得元(⚠️ 原照抄 pytdx ×10000 会把金额放大 10 倍,
21
+ # pytdx 官方 docstring 样例平安总资产 ×10000=30万亿 vs 真实 3.24万亿 即此 bug)
22
+ # 直接值: 股东人数(11) / 每股净资产(28) / 保留(29)
23
+ MONEY_IDX = {7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}
24
+ SHARE_IDX = {1, 2, 3, 4, 5, 6}
25
+
26
+ def build_finance_req(market: int, code: str) -> bytes:
27
+ return TMPL + bytes([market]) + code.encode()
28
+
29
+ def parse_finance(resp: bytes) -> dict:
30
+ if len(resp) < 40:
31
+ return {'ok': False, 'raw_len': len(resp)}
32
+ body = resp[16:]
33
+ cnt = struct.unpack('<H', body[0:2])[0]
34
+ if cnt < 1:
35
+ return {'ok': False, 'raw_len': len(resp)}
36
+ market = body[2]
37
+ code = body[3:9].decode(errors='ignore')
38
+ pos = 9
39
+ liutong = struct.unpack('<f', body[pos:pos+4])[0] * 10000
40
+ pos += 4
41
+ prov, ind = struct.unpack('<HH', body[pos:pos+4]); pos += 4
42
+ upd, ipo = struct.unpack('<II', body[pos:pos+8]); pos += 8
43
+ pos += 4 # 总股本(单独拿)
44
+ zongguben = struct.unpack('<f', body[pos-4:pos])[0] * 10000
45
+ rest = {}
46
+ for i in range(1, 30):
47
+ if pos + 4 <= len(body):
48
+ v = struct.unpack('<f', body[pos:pos+4])[0]
49
+ pos += 4
50
+ if i in (11, 28, 29): # 股东人数/每股净资产/保留 = 直接值
51
+ rest[FIELDS[i]] = round(v, 4)
52
+ elif i in SHARE_IDX: # 股本类: 万股 → 股
53
+ rest[FIELDS[i]] = round(v * 10000, 0)
54
+ else: # 金额类: 千元 → 元
55
+ rest[FIELDS[i]] = round(v * 1000, 2)
56
+ return {
57
+ 'ok': True, 'code': code, 'market': market,
58
+ 'liutongguben': liutong, # 股
59
+ 'zongguben': zongguben, # 股
60
+ 'province': prov, 'industry': ind,
61
+ 'updated_date': upd, 'ipo_date': ipo,
62
+ 'net_profit': rest.get('净利润'), # 元
63
+ 'bvps': rest.get('每股净资产'),
64
+ **rest,
65
+ }
66
+
67
+
68
+ if __name__ == '__main__':
69
+ import socket, time
70
+ HOST = '101.35.121.35'
71
+ C02 = bytes.fromhex("0c0218940001030003000d0001")
72
+ C03 = bytes.fromhex("0c031899000120002000db0f7464786c6576656c000000295cf740110000000000000000000000000005")
73
+ s = socket.socket(); s.settimeout(3)
74
+ s.connect((HOST, 7709))
75
+ for _ in range(3):
76
+ s.sendall(C02 if _ == 0 else C03)
77
+ time.sleep(0.2)
78
+ try:
79
+ s.recv(65536)
80
+ except socket.timeout:
81
+ pass
82
+ for code, m in [('000059', 0), ('600519', 1)]:
83
+ s.sendall(build_finance_req(m, code))
84
+ buf = b''
85
+ end = time.time() + 3
86
+ while time.time() < end:
87
+ try:
88
+ ch = s.recv(65536)
89
+ except socket.timeout:
90
+ break
91
+ if not ch:
92
+ break
93
+ buf += ch
94
+ r = parse_finance(buf)
95
+ print(f'{code}: 流通={r.get("liutongguben", 0)/1e8:.2f}亿股 总股本={r.get("zongguben", 0)/1e8:.2f}亿 净利润={r.get("net_profit")} 每股净资产={r.get("bvps")} 更新={r.get("updated_date")}')
96
+ s.close()
@@ -0,0 +1,125 @@
1
+ # -*- coding: utf-8 -*-
2
+ """GetSecurityQuotes 报价接口 — 旧 pytdx 格式在新服务器原样可用(2026-09-15 实测)
3
+
4
+ 变长整数解码 (get_price) 参考 pytdx (MIT License) rainx/pytdx — https://github.com/rainx/pytdx
5
+ 请求/响应框架为 tdxdata 独立实现。
6
+
7
+ 响应(1只,100B): 帧头18B + count(2) + market(1) + code(6) + active1(2)
8
+ + price_base(变长) + last_close_diff + open_diff + high_diff + low_diff (相对base的差分)
9
+ + 时间 + (-price) + vol + cur_vol + amount(u32) + s_vol + b_vol
10
+ + bid1/ask1/bv1/av1 ... bid5/ask5/bv5/av5
11
+ 价格单位: 变长整数 / 100 (分->元)
12
+ """
13
+ import struct
14
+
15
+ def get_price(data, pos):
16
+ """pytdx 变长整数编码(带符号)"""
17
+ pos_byte = 6
18
+ bdata = data[pos]
19
+ intdata = bdata & 0x3f
20
+ sign = bool(bdata & 0x40)
21
+ if bdata & 0x80:
22
+ while True:
23
+ pos += 1
24
+ bdata = data[pos]
25
+ intdata += (bdata & 0x7f) << pos_byte
26
+ pos_byte += 7
27
+ if not (bdata & 0x80):
28
+ break
29
+ pos += 1
30
+ return (-intdata if sign else intdata), pos
31
+
32
+ def build_quotes_req(items):
33
+ """items: [(market, code), ...]"""
34
+ n = len(items)
35
+ pkgdatalen = n * 7 + 12
36
+ hdr = struct.pack("<HIHHIIHH", 0x10c, 0x02006320, pkgdatalen, pkgdatalen, 0x5053e, 0, 0, n)
37
+ body = b''.join(struct.pack("<B6s", m, c.encode()) for m, c in items)
38
+ return hdr + body
39
+
40
+ def parse_quotes(resp: bytes):
41
+ """返回 [dict...]"""
42
+ out = []
43
+ # 找帧头偏移: b1cb7400 后 18B(0c01 报价帧), 或直接从 num_stock=1 处
44
+ for head_len in (16, 18, 20):
45
+ b = resp[head_len:]
46
+ if len(b) < 12:
47
+ continue
48
+ (num_stock,) = struct.unpack("<H", b[0:2])
49
+ if num_stock not in (1, 2, 5):
50
+ continue
51
+ pos = 2
52
+ for _ in range(num_stock):
53
+ if pos + 9 > len(b):
54
+ break
55
+ market, code, active1 = struct.unpack("<B6sH", b[pos:pos+9]); pos += 9
56
+ price_base, pos = get_price(b, pos)
57
+ lc_diff, pos = get_price(b, pos)
58
+ op_diff, pos = get_price(b, pos)
59
+ hi_diff, pos = get_price(b, pos)
60
+ lo_diff, pos = get_price(b, pos)
61
+ rev0, pos = get_price(b, pos)
62
+ rev1, pos = get_price(b, pos)
63
+ vol, pos = get_price(b, pos)
64
+ cur_vol, pos = get_price(b, pos)
65
+ if pos + 4 > len(b):
66
+ break
67
+ (amount_raw,) = struct.unpack("<I", b[pos:pos+4]); pos += 4
68
+ s_vol, pos = get_price(b, pos)
69
+ b_vol, pos = get_price(b, pos)
70
+ # ⚠️ 关键: pytdx 在 b_vol 后还有 reversed_bytes2/3 两个变长字段,
71
+ # 不跳过会导致 pos 偏移 → 多只错位、单只五档全错(曾漏此 2 字段)
72
+ _, pos = get_price(b, pos)
73
+ _, pos = get_price(b, pos)
74
+ bids, asks, bvs, avs = [], [], [], []
75
+ for _ in range(5):
76
+ bd, pos = get_price(b, pos); aq, pos = get_price(b, pos)
77
+ bv, pos = get_price(b, pos); av, pos = get_price(b, pos)
78
+ bids.append(bd); asks.append(aq); bvs.append(bv); avs.append(av)
79
+ # 每只记录尾部(pytdx 对齐): rev4(H) + rev5-8(4×变长) + rev9(h)+active2(H)
80
+ pos += 2
81
+ for _ in range(4):
82
+ _, pos = get_price(b, pos)
83
+ pos += 4
84
+ base = price_base
85
+ out.append({
86
+ 'market': market, 'code': code.decode(errors='ignore'),
87
+ 'price': base/100, 'last_close': (base+lc_diff)/100,
88
+ 'open': (base+op_diff)/100, 'high': (base+hi_diff)/100,
89
+ 'low': (base+lo_diff)/100,
90
+ 'vol': vol, 'cur_vol': cur_vol, 'amount': amount_raw,
91
+ 'bid': [(base + b) / 100 for b in bids], 'ask': [(base + a) / 100 for a in asks],
92
+ 'bid_vol': bvs, 'ask_vol': avs,
93
+ })
94
+ if out:
95
+ return out
96
+ return out
97
+
98
+
99
+ if __name__ == '__main__':
100
+ import socket, time
101
+ HOST = '101.35.121.35'
102
+ C02 = bytes.fromhex("0c0218940001030003000d0001")
103
+ C03 = bytes.fromhex("0c031899000120002000db0f7464786c6576656c000000295cf740110000000000000000000000000005")
104
+ s = socket.socket(); s.settimeout(3)
105
+ s.connect((HOST, 7709))
106
+ for p in (C02, C03):
107
+ s.sendall(p); time.sleep(0.2)
108
+ try:
109
+ s.recv(65536)
110
+ except socket.timeout:
111
+ pass
112
+ s.sendall(build_quotes_req([(0, '000059'), (1, '600519')]))
113
+ buf = b''
114
+ end = time.time() + 3
115
+ while time.time() < end:
116
+ try:
117
+ ch = s.recv(65536)
118
+ except socket.timeout:
119
+ break
120
+ if not ch:
121
+ break
122
+ buf += ch
123
+ for q in parse_quotes(buf):
124
+ print(f"{q['code']}: 现价={q['price']} 昨收={q['last_close']} 高={q['high']} 低={q['low']} 量={q['vol']} bid1={q['bid'][0]}")
125
+ s.close()
@@ -0,0 +1,180 @@
1
+ # -*- coding: utf-8 -*-
2
+ """并发池: 43 IP 并发,每连接顺序拉 N 只(3s/只),全市场快照 ~12-15 分钟"""
3
+ import socket, time, struct, os, threading, random, sys
4
+
5
+ PORT = 7709
6
+ C02 = bytes.fromhex("0c0218940001030003000d0001")
7
+ C03 = bytes.fromhex("0c031899000120002000db0f7464786c6576656c000000295cf740110000000000000000000000000005")
8
+ SN_PREFIX = bytes.fromhex("0c090041030027002700d10f")
9
+ SN_SUFFIX = bytes.fromhex("0000000000000000000000000000000001001400000000010000000000")
10
+
11
+ def load_servers():
12
+ p = os.path.join(os.path.dirname(__file__), 'servers.txt')
13
+ with open(p) as f:
14
+ return [l.strip() for l in f if l.strip()]
15
+
16
+ def snap_req(code: str, market: int) -> bytes:
17
+ return SN_PREFIX + bytes([market, 0]) + code.encode() + SN_SUFFIX
18
+
19
+ def parse_snap(resp: bytes) -> dict:
20
+ if len(resp) < 60:
21
+ return {'ok': False, 'raw_len': len(resp)}
22
+ data = resp
23
+ compressed = False
24
+ i789 = resp.find(b'\x78\x9c')
25
+ if i789 >= 0:
26
+ try:
27
+ import zlib
28
+ data = resp[:i789] + zlib.decompress(resp[i789:])
29
+ compressed = True
30
+ except Exception:
31
+ pass
32
+ if len(data) < 60:
33
+ return {'ok': False, 'raw_len': len(resp), 'compressed': compressed}
34
+ prices = []
35
+ i = 50
36
+ while i + 4 <= len(data):
37
+ v = struct.unpack('<f', data[i:i+4])[0]
38
+ if 0.1 < v < 100000:
39
+ prices.append(round(v, 3)); i += 4
40
+ else:
41
+ i += 2
42
+ return {'ok': True, 'last': prices[-1] if prices else None, 'prices': prices,
43
+ 'compressed': compressed, 'raw_len': len(resp)}
44
+
45
+ class Conn:
46
+ def __init__(self, host):
47
+ self.host = host
48
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
49
+ self.sock.settimeout(4)
50
+ self.sock.connect((host, PORT))
51
+ for payload, wait in ((C02, 1.5), (C03, 1.5)):
52
+ self.sock.sendall(payload)
53
+ time.sleep(0.05)
54
+ self._recv(wait)
55
+ def _recv(self, secs):
56
+ buf = b''
57
+ end = time.time() + secs
58
+ got = None
59
+ while time.time() < end:
60
+ try:
61
+ ch = self.sock.recv(65536)
62
+ except socket.timeout:
63
+ break
64
+ if not ch:
65
+ break
66
+ buf += ch
67
+ if got is None:
68
+ got = time.time()
69
+ end = min(end, got + 0.15)
70
+ return buf
71
+ def snap(self, code, market):
72
+ self.sock.sendall(snap_req(code, market))
73
+ r = self._recv(3.5)
74
+ return parse_snap(r)
75
+ def close(self):
76
+ try:
77
+ self.sock.close()
78
+ except Exception:
79
+ pass
80
+
81
+ def worker(host, codes, results, idx):
82
+ c = Conn(host)
83
+ mine = []
84
+ for code in codes:
85
+ m = 1 if code.startswith(('5', '6', '9')) else 0 # 5=沪基金 6=沪A 9=沪B
86
+ try:
87
+ r = c.snap(code, m)
88
+ mine.append((code, r.get('last'), r['ok']))
89
+ except Exception as e:
90
+ mine.append((code, None, False))
91
+ results[idx] = mine
92
+ c.close()
93
+
94
+
95
+ class Pool:
96
+ """多主站并发快照池 — 每连接一个服务器,顺序拉取分配到的代码。
97
+
98
+ 服务器每连接每请求固定 ~3s → 多连接并发摊掉延迟。
99
+ 用法:
100
+ p = Pool() # 默认 43 主站并发
101
+ p.snap('600519') # 单只(走第一台服务器)
102
+ rows = p.snap_all(codes) # 全列表并发,返回 [(code, last, ok)]
103
+ """
104
+
105
+ def __init__(self, n_conn: int = 43):
106
+ self.n_conn = n_conn
107
+ self.servers = load_servers()
108
+ if len(self.servers) < n_conn:
109
+ self.servers = (self.servers * (n_conn // len(self.servers) + 1))[:n_conn]
110
+
111
+ def snap(self, code: str, market: int = None) -> dict:
112
+ """单只快照(走第一台服务器;失败自动换下一台)"""
113
+ if market is None:
114
+ market = 1 if code.startswith(('5', '6', '9')) else 0
115
+ for host in self.servers[:3]:
116
+ try:
117
+ c = Conn(host)
118
+ r = c.snap(code, market)
119
+ c.close()
120
+ if r.get('ok') and r.get('last'):
121
+ return r
122
+ except Exception:
123
+ continue
124
+ return {'ok': False, 'code': code}
125
+
126
+ def snap_all(self, codes):
127
+ """并发拉整个代码列表。返回 [(code, last, ok), ...] 保持输入顺序。"""
128
+ results = {}
129
+ chunk = len(codes) // self.n_conn + 1
130
+ threads = []
131
+ for i in range(self.n_conn):
132
+ seg = codes[i * chunk:(i + 1) * chunk]
133
+ if not seg:
134
+ continue
135
+ t = threading.Thread(target=worker, args=(self.servers[i], seg, results, i))
136
+ threads.append(t)
137
+ t.start()
138
+ for t in threads:
139
+ t.join()
140
+ flat = [v for sub in results.values() for v in sub]
141
+ by_code = {c: (last, ok) for c, last, ok in flat}
142
+ return [(c, *by_code.get(c, (None, False))) for c in codes]
143
+
144
+ if __name__ == '__main__':
145
+ servers = load_servers()
146
+ # 确定存在的代码(深沪白马混合)
147
+ codes = ['000001', '000002', '000063', '000066', '000333', '000338', '000425', '000538', '000568', '000651',
148
+ '000661', '000725', '000768', '000776', '000783', '000792', '000858', '000876', '000895', '000963',
149
+ '002001', '002007', '002008', '002027', '002044', '002050', '002064', '002074', '002080', '002085',
150
+ '002110', '002129', '002142', '002153', '002179', '002202', '002230', '002236', '002241', '002271',
151
+ '002304', '002311', '002352', '002371', '002415', '300003', '300014', '300015', '300033', '300059',
152
+ '600000', '600004', '600009', '600010', '600011', '600015', '600016', '600018', '600019', '600028',
153
+ '600029', '600030', '600031', '600036', '600048', '600050', '600061', '600085', '600089', '600104',
154
+ '600109', '600111', '600115', '600150', '600153', '600160', '600176', '600183', '600196', '600201',
155
+ '600219', '600233', '600276', '600309', '600332', '600346', '600352', '600362', '600372', '600383',
156
+ '600390', '600406', '600426', '600438', '600519', '600547', '600570', '600585', '600588', '600690']
157
+ random.seed(1)
158
+ random.shuffle(codes)
159
+ n_threads = 12
160
+ chunk = len(codes) // n_threads + 1
161
+ results = {}
162
+ t0 = time.time()
163
+ threads = []
164
+ for i in range(n_threads):
165
+ seg = codes[i*chunk:(i+1)*chunk]
166
+ if not seg:
167
+ continue
168
+ t = threading.Thread(target=worker, args=(servers[i], seg, results, i))
169
+ threads.append(t)
170
+ t.start()
171
+ for t in threads:
172
+ t.join()
173
+ el = time.time() - t0
174
+ flat = [v for sub in results.values() for v in sub]
175
+ ok = sum(1 for _, last, okk in flat if okk and last)
176
+ fail = [(c, l) for c, l, okk in flat if not (okk and l)]
177
+ print(f'{len(flat)} 只 / {len(results)} 连接并发: {el:.0f}s, 成功 {ok}/{len(flat)}')
178
+ if fail:
179
+ print(f'失败 {len(fail)} 只: {[c for c, _ in fail[:10]]}')
180
+ print(f'吞吐: {ok/el:.2f} 只/s → 全市场 5000 只(46 连接) ≈ {5000/(ok/el)/60:.1f} 分钟')
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ """协议常量与命令码(2026-09-14 抓包逆向)"""
3
+
4
+ # TCP 端口(通达信标准行情服务器)
5
+ DEFAULT_PORT = 7709
6
+
7
+ # 版本标识(服务器校验关键字段,老库 0x01016408 已被拒)
8
+ # 抓包新客户端: 18 7b 00 01(小端 0x01007b18)
9
+ VERSION_FLAG = 0x01007B18
10
+ # 老库值(存档对照)
11
+ VERSION_FLAG_OLD = 0x01016408
12
+
13
+ # 帧/长度字段(抓包: 1a 01 1a 01;子类型 0b 00)
14
+ FRAME_LEN = 0x011A
15
+ SUBTYPE = 0x000B
16
+
17
+ # 命令族(抓包观察)
18
+ CMD_HEARTBEAT = 0x0C00 # 12B 心跳: 0c00...
19
+ CMD_INIT = 0x0C01 # 74B 加密初始化(会话上下文,用途待定)
20
+ CMD_AUTH = 0x0C03 # tdxlevel 认证
21
+ CMD_FILE_REQ = 0x0C04 # 文件下载请求(infoharbor_spec.cfg 等)
22
+ CMD_BARS = 0x0C07 # 数据请求(带明文 code)— ⚠️ 实际 bars 走 0x010c 命令族,此常量仅为抓包观察存档
23
+ CMD_SNAPSHOT = 0x0000 # 明文批量快照请求(00...c002 + code 列表)
24
+
25
+ # 认证负载见 commands/auth.py build_auth()(42B 正确版;此处不再存档防漂移)
26
+
27
+ # 帧头标记(响应)
28
+ FRAME_MARK = b"\xb1\xcb\x74\x00"
@@ -0,0 +1,43 @@
1
+ 101.33.225.16
2
+ 101.35.121.35
3
+ 101.42.164.241
4
+ 101.42.240.54
5
+ 101.43.159.194
6
+ 110.41.147.114
7
+ 110.41.2.72
8
+ 111.229.247.189
9
+ 111.230.186.52
10
+ 111.231.113.208
11
+ 116.205.163.254
12
+ 116.205.171.132
13
+ 116.205.183.150
14
+ 118.25.98.114
15
+ 119.97.185.59
16
+ 120.53.8.251
17
+ 121.36.225.169
18
+ 122.51.120.217
19
+ 122.51.232.182
20
+ 123.60.164.122
21
+ 123.60.70.228
22
+ 123.60.73.44
23
+ 123.60.84.66
24
+ 124.223.163.242
25
+ 124.70.133.119
26
+ 124.70.199.56
27
+ 124.71.187.122
28
+ 124.71.187.72
29
+ 124.71.9.153
30
+ 129.204.230.128
31
+ 150.158.160.2
32
+ 152.136.191.169
33
+ 159.75.29.111
34
+ 175.178.112.197
35
+ 175.178.128.227
36
+ 43.139.18.171
37
+ 43.139.95.83
38
+ 49.232.15.141
39
+ 62.234.50.143
40
+ 81.70.151.186
41
+ 81.71.32.47
42
+ 82.156.174.84
43
+ 82.156.214.79
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdxdata
3
+ Version: 0.1.0
4
+ Summary: TDX (通达信) 2026-09 新版行情协议客户端 — realtime A-share quotes
5
+ Author: tdxdata contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 tdxdata contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Keywords: tdx,通达信,a-share,stock,quotes,pytdx
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Topic :: Office/Business :: Financial :: Investment
32
+ Requires-Python: >=3.10
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ License-File: NOTICE.md
36
+ Dynamic: license-file
37
+
38
+ # tdxdata
39
+
40
+ 通达信(TDX)新版行情协议 Python 客户端 — 2026-09 协议逆向适配。
41
+
42
+ 老 pytdx / mootdx / xmtdx 的协议 2026-09-10 起被通达信服务器拒绝(服务端协议改动:版本标识 + 命令族 + zlib 流式压缩)。tdxdata 是逆向新协议后的独立实现。
43
+
44
+ **A-share (CN) realtime quote client for TDX's 2026-09 protocol.**
45
+
46
+ ## 快速开始
47
+
48
+ ```bash
49
+ pip install -e .
50
+ ```
51
+
52
+ ```python
53
+ from tdxdata import TdxClient
54
+
55
+ c = TdxClient() # 默认连 101.35.121.35(43 主站之一,见 servers.txt)
56
+ c.connect() # 0c02 init + 0c03 tdxlevel 认证
57
+ c.snap('600519') # 分时快照: 最新价 + 当日分时点
58
+ c.bars(9, '000059', 0, 100) # 历史K线: 9=日线 0=5min 8=1min 6=周 7=月
59
+ c.quotes([(0, '000059')]) # 盘口报价: 现价/昨收/高低/量/额/五档
60
+ c.finance('000059') # 财务: 流通/总股本/净利润/每股净资产
61
+ c.close()
62
+ ```
63
+
64
+ 全市场并发快照(多主站摊掉每请求 ~3s 延迟):
65
+
66
+ ```python
67
+ from tdxdata.pool2 import Pool
68
+
69
+ p = Pool() # 43 个主站 IP × 并发连接
70
+ r = p.snap('600519') # 单只: {'last': 1277.56, 'prices': [32点分时...]}
71
+ rows = p.snap_all(['000001', '600519']) # 并发全列表 → [(code, last, ok)]
72
+ ```
73
+
74
+ 支持深市 / 沪市 / 指数(market 自动推断,也可显式传):
75
+
76
+ ```python
77
+ p.snap('000001') # 平安银行(深 market=0)
78
+ p.snap('000001', market=1) # 上证指数(沪 market=1)
79
+ p.snap('688981', market=1) # 中芯国际(科创板)
80
+ ```
81
+
82
+ CLI 连通性自检:
83
+
84
+ ```bash
85
+ tdxdata-probe # 探测 600519 的 snap/bars/quotes/finance
86
+ tdxdata-probe --codes 000001,600519 --host 101.35.121.35
87
+ ```
88
+
89
+ ## 功能
90
+
91
+ | 接口 | 命令 | 状态 |
92
+ |---|---|---|
93
+ | 分时快照(最新价+32-51点分时) | 0c09 | ✅ 深沪深+指数 |
94
+ | 历史K线(日/5min/15/30/60/1min/周/月) | 0x010c (GetSecurityBars) | ✅ 单只任意历史段 |
95
+ | 盘口报价(现价/昨收/高低/量/额/五档) | 0x010c (GetSecurityQuotes) | ✅ |
96
+ | 财务(流通/总股本/净利润/每股净资产) | 0c1f | ✅ |
97
+ | 多主站并发(43 IP 轮询、坏 IP 剔除) | — | ✅ 全市场快照 ~12-15 分钟 |
98
+ | 批量快照(单包多只) | 0c01 身份会话 | ⚠️ 服务端需 0c01 会话,已放弃(单只循环+并发替代) |
99
+ | 全量历史K线(单请求批量全市场) | — | ❌ 新版客户端用本地文件+增量,服务器无此接口(单只循环可拉任意股票) |
100
+
101
+ ## 已知限制
102
+
103
+ - **每连接每请求固定 ~3s 延迟**(服务器行为)→ 用多连接并发摊掉,勿压单连接频率
104
+ - 单出口并发 >60 会触发出口级限流(热状态,需长冷却)→ 默认 43 连接
105
+ - 3 个主站 IP 连接即 reset(已从 servers.txt 剔除)
106
+ - 大数据量响应为 zlib 压缩流(小数据量明文),客户端已统一处理(`client.maybe_decompress`)
107
+ - 少数股票 5min 含 13:00 一根 bar(服务器行为,与旧 pytdx 主库一致)
108
+ - 无效代码(不存在的股票代码)返回未定义数据(bars 可能含乱码日期、quotes 可能重定向到其他代码),调用方需保证传入有效代码
109
+ - quotes 对基金(ETF)价格精度为 0.001 元 → 返回价 ×10(510300 实际 4.523 会显示 45.23,pytdx 同款行为);基金行情请用 snap/bars(float32 直读,精度正确)
110
+ - 市场自动判断规则:5/6/9 开头=沪(5 基金/6 A/9 B),0/1/2/3 开头=深(0 A/1 基金/2 B/3 创业板);北交所(4/8/92)暂不支持
111
+
112
+ ## 协议要点(完整版见 docs/protocol.md)
113
+
114
+ ```
115
+ TCP 7709 → 0c02(init) → 0c03(tdxlevel) → 数据请求
116
+ 单只快照 = 0c09 0041 0300 2700 2700 d10f [market] [code6] + 29B 后缀
117
+ market: 0=深市 1=沪市(同 code 不同 market = 不同标的!)
118
+ 历史K线 = 0x010c + 版本标识 18 7b 00 01(老 pytdx 命令仅改此标识即复用)
119
+ ```
120
+
121
+ ## 借用与版权
122
+
123
+ 解码层部分逻辑参考 pytdx(MIT, rainx)——详见 [NOTICE.md](NOTICE.md)。协议逆向过程见 [docs/](docs/)(protocol.md / extractable.md / analysis_round1~3.md)。
124
+
125
+ ## License
126
+
127
+ MIT — 详见 [LICENSE](LICENSE)。
128
+
129
+ > ⚠️ 仅供学习研究。行情数据版权归交易所所有;请遵守通达信服务条款,控制请求频率。
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ NOTICE.md
3
+ README.md
4
+ pyproject.toml
5
+ src/tdxdata/__init__.py
6
+ src/tdxdata/cli.py
7
+ src/tdxdata/client.py
8
+ src/tdxdata/pool2.py
9
+ src/tdxdata/protocol.py
10
+ src/tdxdata/servers.txt
11
+ src/tdxdata.egg-info/PKG-INFO
12
+ src/tdxdata.egg-info/SOURCES.txt
13
+ src/tdxdata.egg-info/dependency_links.txt
14
+ src/tdxdata.egg-info/entry_points.txt
15
+ src/tdxdata.egg-info/top_level.txt
16
+ src/tdxdata/commands/__init__.py
17
+ src/tdxdata/commands/auth.py
18
+ src/tdxdata/commands/bars.py
19
+ src/tdxdata/commands/finance.py
20
+ src/tdxdata/commands/quotes.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tdxdata-probe = tdxdata.cli:main
@@ -0,0 +1 @@
1
+ tdxdata