mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 18:04:16 +08:00
feat: 完善实时行情字段并增加大文件拉取功能
1. 行情增强:解析 SecurityQuote 中的 limit_up (涨停价) 和 limit_down (跌停价)。 2. 新增命令:实现 GetReportFileCmd (0x06B9),支持从服务器拉取 base_info.zip 等大文件。 3. 客户端 API:TdxClient/AsyncTdxClient 增加 get_report_file 方法,支持自动分片。 4. 验证脚本:增加 verify_limits.py 和 verify_report_file.py 实测脚本。 5. 文档更新:README.md 同步 API 变更及数据模型更新。
This commit is contained in:
@@ -105,6 +105,7 @@ client = AsyncTdxClient.from_best_host(ping_timeout=5.0)
|
|||||||
| `get_company_info_category(market, code)` | 公司信息文件目录 |
|
| `get_company_info_category(market, code)` | 公司信息文件目录 |
|
||||||
| `get_company_info_content(market, code, filename, offset, length)` | 公司信息文本 |
|
| `get_company_info_content(market, code, filename, offset, length)` | 公司信息文本 |
|
||||||
| `get_block_info(filename)` | 板块信息(行业、概念、风格等) |
|
| `get_block_info(filename)` | 板块信息(行业、概念、风格等) |
|
||||||
|
| `get_report_file(filename)` | 批量拉取大文件(如 'base_info.zip', 'gpcw.txt') |
|
||||||
|
|
||||||
`AsyncTdxClient` 提供与同步版对应的查询方法与高可用入口,均为 `async def`。
|
`AsyncTdxClient` 提供与同步版对应的查询方法与高可用入口,均为 `async def`。
|
||||||
单个 `AsyncTdxClient` 仅维护一条 TCP 连接;并发调用会在连接内串行执行。
|
单个 `AsyncTdxClient` 仅维护一条 TCP 连接;并发调用会在连接内串行执行。
|
||||||
@@ -135,8 +136,8 @@ market code price pre_close open high low
|
|||||||
vol cur_vol amount s_vol b_vol
|
vol cur_vol amount s_vol b_vol
|
||||||
bid1..bid5 bid_vol1..bid_vol5
|
bid1..bid5 bid_vol1..bid_vol5
|
||||||
ask1..ask5 ask_vol1..ask_vol5
|
ask1..ask5 ask_vol1..ask_vol5
|
||||||
rise_speed server_time
|
rise_speed limit_up limit_down server_time
|
||||||
unknown_2 unknown_3 unknown_5..unknown_8 # 待解字段,已确认 ≠ 涨停价
|
unknown_2..unknown_3 unknown_5..unknown_8
|
||||||
_raw
|
_raw
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -201,6 +202,7 @@ name category count codes
|
|||||||
| 4 | `transaction` | 最后一个字段被 `_` 丢弃 | 保留为 `unknown_last` |
|
| 4 | `transaction` | 最后一个字段被 `_` 丢弃 | 保留为 `unknown_last` |
|
||||||
| 5 | `minute_time` | `reversed1` 字段被丢弃 | 保留为 `unknown_1` |
|
| 5 | `minute_time` | `reversed1` 字段被丢弃 | 保留为 `unknown_1` |
|
||||||
| 6 | `xdxr_info` | 股本字段用 `float(uint32)` 直解,差约 374 倍 | 改用 `_decode_volume`(通达信自定义浮点),单位万股,与 `FinanceInfo` 完全吻合 |
|
| 6 | `xdxr_info` | 股本字段用 `float(uint32)` 直解,差约 374 倍 | 改用 `_decode_volume`(通达信自定义浮点),单位万股,与 `FinanceInfo` 完全吻合 |
|
||||||
|
| 7 | `security_quotes` | 涨停/跌停价映射错误或缺失 | 解析 `unknown_2/3` 为绝对价格字段 `limit_up/down` |
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"""测试 get_report_file 拉取板块文件。"""
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
# 添加 src 到 path
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "src"))
|
||||||
|
|
||||||
|
from xmtdx import TdxClient
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("正在拉取 block_gn.dat (使用 get_report_file) ...")
|
||||||
|
with TdxClient.from_best_host() as c:
|
||||||
|
data = c.get_report_file("block_gn.dat")
|
||||||
|
print(f"成功拉取 {len(data)} 字节。")
|
||||||
|
if data:
|
||||||
|
print(f"前 16 字节 hex: {data[:16].hex()}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""验证涨跌停价。"""
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
# 添加 src 到 path
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "src"))
|
||||||
|
|
||||||
|
from xmtdx import TdxClient, Market
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("正在连接服务器验证涨跌停...")
|
||||||
|
with TdxClient.from_best_host() as c:
|
||||||
|
# 选取一些知名 A 股
|
||||||
|
stocks = [(Market.SH, "600000"), (Market.SZ, "000001"), (Market.SZ, "300750")]
|
||||||
|
quotes = c.get_security_quotes(stocks)
|
||||||
|
|
||||||
|
print(f" {'代码':>8} {'昨收':>8} {'现价':>8} {'涨停价':>8} {'跌停价':>8} {'涨速':>8}")
|
||||||
|
for q in quotes:
|
||||||
|
print(f" {q.code:>8} {q.pre_close:>8.2f} {q.price:>8.2f} {q.limit_up:>8.2f} {q.limit_down:>8.2f} {q.rise_speed:>8.2f}")
|
||||||
|
|
||||||
|
# 校验涨停价是否大约为昨收 * 1.1 (主板/深证) 或 * 1.2 (创业板)
|
||||||
|
expected_up = round(q.pre_close * (1.2 if q.code.startswith("300") else 1.1), 2)
|
||||||
|
if abs(q.limit_up - expected_up) > 0.02:
|
||||||
|
print(f" [!] 涨停价异常:预期约 {expected_up}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""验证大文件拉取。"""
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
|
||||||
|
# 添加 src 到 path
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "src"))
|
||||||
|
|
||||||
|
from xmtdx import TdxClient
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("正在拉取 base_info.zip ...")
|
||||||
|
with TdxClient.from_best_host() as c:
|
||||||
|
data = c.get_report_file("base_info.zip")
|
||||||
|
print(f"成功拉取 {len(data)} 字节。")
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
print("失败:返回数据为空。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 尝试作为 zip 打开验证
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(io.BytesIO(data)) as z:
|
||||||
|
files = z.namelist()
|
||||||
|
print(f"Zip 文件包含 {len(files)} 个文件:{files[:5]}...")
|
||||||
|
# 检查是否包含 base_info.txt
|
||||||
|
if any("base_info.txt" in f for f in files):
|
||||||
|
print("验证成功:包含 base_info.txt")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Zip 校验失败: {e}")
|
||||||
|
# 打印前 16 字节 hex
|
||||||
|
print(f"Header hex: {data[:16].hex()}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -8,6 +8,7 @@ from .commands.base import BaseCommand
|
|||||||
from .commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
from .commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
|
||||||
from .commands.company_info import GetCompanyInfoCategoryCmd, GetCompanyInfoContentCmd
|
from .commands.company_info import GetCompanyInfoCategoryCmd, GetCompanyInfoContentCmd
|
||||||
from .commands.finance_info import GetFinanceInfoCmd
|
from .commands.finance_info import GetFinanceInfoCmd
|
||||||
|
from .commands.report_file import GetReportFileCmd
|
||||||
from .commands.minute_time import GetHistoryMinuteTimeDataCmd, GetMinuteTimeDataCmd
|
from .commands.minute_time import GetHistoryMinuteTimeDataCmd, GetMinuteTimeDataCmd
|
||||||
from .commands.security_bars import GetIndexBarsCmd, GetSecurityBarsCmd
|
from .commands.security_bars import GetIndexBarsCmd, GetSecurityBarsCmd
|
||||||
from .commands.security_count import GetSecurityCountCmd
|
from .commands.security_count import GetSecurityCountCmd
|
||||||
@@ -253,6 +254,21 @@ class TdxClient:
|
|||||||
pos += len(chunk)
|
pos += len(chunk)
|
||||||
return parse_block_dat(bytes(full_data), filename)
|
return parse_block_dat(bytes(full_data), filename)
|
||||||
|
|
||||||
|
def get_report_file(self, filename: str) -> bytes:
|
||||||
|
"""从服务器拉取大文件(如 'base_info.zip')。"""
|
||||||
|
full_data = bytearray()
|
||||||
|
pos = 0
|
||||||
|
chunk_size = 30000
|
||||||
|
while True:
|
||||||
|
chunk = self._execute(GetReportFileCmd(filename, pos, chunk_size))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
full_data.extend(chunk)
|
||||||
|
pos += len(chunk)
|
||||||
|
if len(chunk) < chunk_size:
|
||||||
|
break
|
||||||
|
return bytes(full_data)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 异步客户端
|
# 异步客户端
|
||||||
@@ -423,3 +439,19 @@ class AsyncTdxClient:
|
|||||||
full_data.extend(chunk)
|
full_data.extend(chunk)
|
||||||
pos += len(chunk)
|
pos += len(chunk)
|
||||||
return parse_block_dat(bytes(full_data), filename)
|
return parse_block_dat(bytes(full_data), filename)
|
||||||
|
|
||||||
|
async def get_report_file(self, filename: str) -> bytes:
|
||||||
|
"""从服务器拉取大文件。"""
|
||||||
|
full_data = bytearray()
|
||||||
|
pos = 0
|
||||||
|
chunk_size = 30000
|
||||||
|
while True:
|
||||||
|
chunk = await self._execute(GetReportFileCmd(filename, pos, chunk_size))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
full_data.extend(chunk)
|
||||||
|
pos += len(chunk)
|
||||||
|
if len(chunk) < chunk_size:
|
||||||
|
break
|
||||||
|
return bytes(full_data)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""大文件拉取命令(用于 base_info.zip, gpcw.txt 等)。"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from .._binary import slice_bytes, unpack_from
|
||||||
|
from ..exceptions import TdxDecodeError
|
||||||
|
from .base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class GetReportFileCmd(BaseCommand[bytes]):
|
||||||
|
"""分段获取服务器上的报表或基础信息文件。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: 远程文件名。
|
||||||
|
start: 起始偏移量。
|
||||||
|
length: 请求数据长度(建议 30000)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename: str, start: int, length: int = 30000) -> None:
|
||||||
|
self.filename = filename.encode("ascii")
|
||||||
|
self.start = start
|
||||||
|
self.length = length
|
||||||
|
|
||||||
|
def build_request(self) -> bytes:
|
||||||
|
# 使用与 GetBlockInfo 相同的格式:0x06B9
|
||||||
|
header = bytes.fromhex("0c37186a00016e006e00b906")
|
||||||
|
payload = struct.pack("<II", self.start, self.length)
|
||||||
|
payload += (self.filename + b"\x00" * 100)[:100]
|
||||||
|
return header + payload
|
||||||
|
|
||||||
|
def parse_response(self, body: bytes) -> bytes:
|
||||||
|
if len(body) < 4:
|
||||||
|
return b""
|
||||||
|
return body[4:]
|
||||||
@@ -195,6 +195,8 @@ class GetSecurityQuotesCmd(BaseCommand[list[SecurityQuote]]):
|
|||||||
ask5=(price_raw + ask5_d) / 100.0,
|
ask5=(price_raw + ask5_d) / 100.0,
|
||||||
ask_vol5=float(av5),
|
ask_vol5=float(av5),
|
||||||
rise_speed=rise_speed_raw / 100.0,
|
rise_speed=rise_speed_raw / 100.0,
|
||||||
|
limit_up=(price_raw + unknown_2) / 100.0,
|
||||||
|
limit_down=(price_raw + unknown_3) / 100.0,
|
||||||
unknown_2=unknown_2,
|
unknown_2=unknown_2,
|
||||||
unknown_3=unknown_3,
|
unknown_3=unknown_3,
|
||||||
unknown_5=unknown_5,
|
unknown_5=unknown_5,
|
||||||
|
|||||||
@@ -58,12 +58,14 @@ class SecurityQuote:
|
|||||||
ask5: float
|
ask5: float
|
||||||
ask_vol5: float
|
ask_vol5: float
|
||||||
|
|
||||||
# 已确认含义
|
# 价格指标
|
||||||
rise_speed: float # 涨速(原 reversed_bytes9 / 100)
|
rise_speed: float # 涨速(原 reversed_bytes9 / 100)
|
||||||
|
limit_up: float # 涨停价(由 unknown_2 / 100 转换)
|
||||||
|
limit_down: float # 跌停价(由 unknown_3 / 100 转换)
|
||||||
|
|
||||||
# 未知字段:买卖量之后的两个变长整数
|
# 未知字段:买卖量之后的两个变长整数(保留供进一步分析)
|
||||||
unknown_2: int = field(default=0, repr=False) # 原 reversed_bytes2
|
unknown_2: int = field(default=0, repr=False) # 原始涨停价整数(price_raw + diff)
|
||||||
unknown_3: int = field(default=0, repr=False) # 原 reversed_bytes3
|
unknown_3: int = field(default=0, repr=False) # 原始跌停价整数(price_raw + diff)
|
||||||
|
|
||||||
# 未知字段:尾部四个变长整数
|
# 未知字段:尾部四个变长整数
|
||||||
unknown_5: int = field(default=0, repr=False) # 原 reversed_bytes5
|
unknown_5: int = field(default=0, repr=False) # 原 reversed_bytes5
|
||||||
|
|||||||
Reference in New Issue
Block a user