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 +21 -0
- tdxdata-0.1.0/NOTICE.md +42 -0
- tdxdata-0.1.0/PKG-INFO +129 -0
- tdxdata-0.1.0/README.md +92 -0
- tdxdata-0.1.0/pyproject.toml +29 -0
- tdxdata-0.1.0/setup.cfg +4 -0
- tdxdata-0.1.0/src/tdxdata/__init__.py +14 -0
- tdxdata-0.1.0/src/tdxdata/cli.py +50 -0
- tdxdata-0.1.0/src/tdxdata/client.py +165 -0
- tdxdata-0.1.0/src/tdxdata/commands/__init__.py +1 -0
- tdxdata-0.1.0/src/tdxdata/commands/auth.py +23 -0
- tdxdata-0.1.0/src/tdxdata/commands/bars.py +83 -0
- tdxdata-0.1.0/src/tdxdata/commands/finance.py +96 -0
- tdxdata-0.1.0/src/tdxdata/commands/quotes.py +125 -0
- tdxdata-0.1.0/src/tdxdata/pool2.py +180 -0
- tdxdata-0.1.0/src/tdxdata/protocol.py +28 -0
- tdxdata-0.1.0/src/tdxdata/servers.txt +43 -0
- tdxdata-0.1.0/src/tdxdata.egg-info/PKG-INFO +129 -0
- tdxdata-0.1.0/src/tdxdata.egg-info/SOURCES.txt +20 -0
- tdxdata-0.1.0/src/tdxdata.egg-info/dependency_links.txt +1 -0
- tdxdata-0.1.0/src/tdxdata.egg-info/entry_points.txt +2 -0
- tdxdata-0.1.0/src/tdxdata.egg-info/top_level.txt +1 -0
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.
|
tdxdata-0.1.0/NOTICE.md
ADDED
|
@@ -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
|
+
> ⚠️ 仅供学习研究。行情数据版权归交易所所有;请遵守通达信服务条款,控制请求频率。
|
tdxdata-0.1.0/README.md
ADDED
|
@@ -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"]
|
tdxdata-0.1.0/setup.cfg
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tdxdata
|