feat: add extended market, offline data reader, rewrite README

- Add ExTdxClient/AsyncExTdxClient for futures, HK stocks, etc (port 7727)
- Add offline module: read daily bars, minute bars, blocks, gbbq, financials
  from local TDX installation directory (inspired by pytdx)
- Add examples 09 (file download) and 10 (offline data reading)
- Rewrite README with comprehensive API docs and code examples
- Add TdxFileNotFoundError and TdxOfflineError exceptions

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub
2026-05-21 23:03:08 +08:00
co-authored by Claude Opus 4.7
parent 321043f9a5
commit 9c5672b4d2
42 changed files with 10090 additions and 130 deletions
+80
View File
@@ -0,0 +1,80 @@
"""演示:板块数据读取(本地 + 网络自动回退)。
系统板块获取优先级:
1. 本地 .dat 文件(离线读取)
2. TDX 服务器在线获取(自动回退)
自定义板块仅支持本地读取。
"""
from pathlib import Path
from xmtdx import TdxClient
from xmtdx.models.finance import TdxBlock
from xmtdx.offline import detect_tdx_home, read_block_dat, read_customer_blocks
def _print_blocks(blocks: list[TdxBlock], title: str) -> None:
print(f"\n{title} ({len(blocks)} 个板块):")
for block in blocks[:5]:
codes_preview = ", ".join(block.codes[:5])
suffix = "..." if len(block.codes) > 5 else ""
print(f" {block.name} ({block.count}只): {codes_preview}{suffix}")
if len(blocks) > 5:
print(f" ... 还有 {len(blocks) - 5} 个板块")
home = detect_tdx_home()
# --- 系统板块 ---
print("=" * 60)
print("系统板块")
print("=" * 60)
vipdoc = Path(home) / "vipdoc" if home else None
block_names = ["block_zs.dat", "block_gn.dat", "block_fg.dat"]
block_labels = {
"block_zs.dat": "行业板块",
"block_gn.dat": "概念板块",
"block_fg.dat": "风格板块",
}
need_fetch = []
for name in block_names:
local_path = vipdoc / name if vipdoc else None
if local_path and local_path.is_file():
blocks = read_block_dat(local_path)
_print_blocks(blocks, f"{block_labels[name]} ({name}, 本地)")
else:
print(f"\n{block_labels[name]}: 本地文件不存在,将从服务器获取")
need_fetch.append(name)
# 本地没有的板块,通过网络获取
if need_fetch:
print(f"\n正在连接服务器获取 {len(need_fetch)} 个板块文件...")
with TdxClient.from_best_host() as c:
for name in need_fetch:
blocks = c.get_block_info(name)
_print_blocks(blocks, f"{block_labels[name]} ({name}, 网络)")
# --- 自定义板块 ---
print(f"\n{'=' * 60}")
print("自定义板块")
print("=" * 60)
if home:
blocknew_dir = Path(home) / "T0002" / "blocknew"
if blocknew_dir.is_dir():
blocks = read_customer_blocks(blocknew_dir)
if blocks:
print(f"\n{len(blocks)} 个自定义板块:")
for block in blocks[:10]:
codes_preview = ", ".join(block.codes[:5])
suffix = "..." if len(block.codes) > 5 else ""
print(f" {block.blockname} ({len(block.codes)}只): {codes_preview}{suffix}")
else:
print("未找到自定义板块数据")
else:
print(f"自定义板块目录不存在: {blocknew_dir}")
else:
print("需要本地通达信安装目录才能读取自定义板块")
+43
View File
@@ -0,0 +1,43 @@
"""演示:从本地通达信目录读取日线 K 线数据。
两种用法:
1. 直接指定 .day 文件路径
2. 通过 市场+代码 自动定位文件(需要设置 TDX_HOME 环境变量)
需要本地已安装通达信并下载过日线数据。
"""
from xmtdx.offline import detect_tdx_home, read_daily_bars, find_daily_bar_file
from xmtdx import Market
home = detect_tdx_home()
if home is None:
print("未检测到通达信安装目录,请设置 TDX_HOME 环境变量")
print("例如: set TDX_HOME=C:\\new_jyplug")
raise SystemExit(1)
print(f"通达信目录: {home}")
# --- 方式1: 通过 市场+代码 自动定位文件 ---
filepath = find_daily_bar_file(Market.SH, "600000")
print(f"\n文件路径: {filepath}")
bars = read_daily_bars(filepath)
if not bars:
print("未读取到数据,请确认通达信已下载该股票的日线数据")
raise SystemExit(0)
# 最近 10 个交易日
print(f"\n浦发银行 日线 (最近 {min(10, len(bars))} 个交易日):")
print(f"{'日期':>12s} {'开盘':>8s} {'最高':>8s} {'最低':>8s} {'收盘':>8s} {'成交量':>10s}")
for bar in bars[-10:]:
print(
f"{bar.year}-{bar.month:02d}-{bar.day:02d} "
f"{bar.open:>8.2f} {bar.high:>8.2f} "
f"{bar.low:>8.2f} {bar.close:>8.2f} "
f"{bar.vol:>10.0f}"
)
# --- 方式2: 直接指定文件路径 ---
# from pathlib import Path
# bars2 = read_daily_bars(Path(r"C:\new_jyplug\vipdoc\sz\lday\sz000001.day"))
+80
View File
@@ -0,0 +1,80 @@
"""演示:检测通达信安装目录与路径解析。
offline 模块的路径检测优先级:
1. TDX_HOME 环境变量
2. 平台常见路径猜测 (Windows: C:\\new_jyplug, C:\\new_tdx, D:\\... 等)
vipdoc 目录结构:
vipdoc/
├── sh/lday/ 上海日线 sh600000.day
├── sh/fzline/ 上海5分钟线 sh600000.5
├── sh/fzline/ 上海分钟线 sh600000.lc1 / .lc5
├── sz/lday/ 深圳日线 sz000001.day
├── sz/fzline/ 深圳5分钟线 sz000001.5
├── sz/fzline/ 深圳分钟线 sz000001.lc1 / .lc5
└── ds/ 扩展市场 29#A1801.day
"""
import os
from pathlib import Path
from xmtdx.offline import detect_tdx_home, resolve_vipdoc
from xmtdx.offline import find_daily_bar_file, find_5min_bar_file, find_lc1_bar_file
from xmtdx import Market
# --- 检测安装目录 ---
print("=" * 60)
print("通达信安装目录检测")
print("=" * 60)
home = detect_tdx_home()
if home:
print(f"检测到: {home}")
else:
print("未检测到,可通过以下方式指定:")
print(f" set TDX_HOME=C:\\new_jyplug")
# --- 手动指定路径 ---
print(f"\n{'=' * 60}")
print("手动指定 vipdoc 路径")
print("=" * 60)
if home:
vipdoc = resolve_vipdoc()
print(f"vipdoc 目录: {vipdoc}")
# 列出 vipdoc 子目录
if vipdoc.is_dir():
for d in sorted(vipdoc.iterdir()):
if d.is_dir():
files = list(d.rglob("*"))
print(f" {d.name}/ ({len(files)} 个文件)")
else:
print("(需要 TDX_HOME 才能自动解析)")
# --- 文件定位示例 ---
print(f"\n{'=' * 60}")
print("通过 市场+代码 定位文件")
print("=" * 60)
if home:
examples = [
("浦发银行 日线", lambda: find_daily_bar_file(Market.SH, "600000")),
("平安银行 日线", lambda: find_daily_bar_file(Market.SZ, "000001")),
("浦发银行 5分钟", lambda: find_5min_bar_file(Market.SH, "600000")),
("平安银行 1分钟", lambda: find_lc1_bar_file(Market.SZ, "000001")),
]
for label, finder in examples:
p = finder()
exists = "存在" if p.is_file() else "不存在"
print(f" {label}: {p} ({exists})")
else:
print("(需要 TDX_HOME)")
# --- 设置环境变量的方式 ---
print(f"\n{'=' * 60}")
print("如何设置 TDX_HOME")
print("=" * 60)
print(" Windows CMD: set TDX_HOME=C:\\new_jyplug")
print(" Windows PS: $env:TDX_HOME = 'C:\\new_jyplug'")
print(" Linux/macOS: export TDX_HOME=/opt/new_tdx")
+69
View File
@@ -0,0 +1,69 @@
"""演示:从本地通达信目录读取扩展市场日线数据。
扩展市场包括:期货、港股、外盘等。
文件位于 vipdoc/ds/ 目录下,如 29#A1801.day
需要本地已安装通达信并下载过扩展市场数据。
"""
from pathlib import Path
from xmtdx.offline import detect_tdx_home, read_ex_daily_bars
home = detect_tdx_home()
if home is None:
print("未检测到通达信安装目录,请设置 TDX_HOME 环境变量")
raise SystemExit(1)
vipdoc = Path(home) / "vipdoc" / "ds" / "lday"
# 列出 ds 目录下可用的 .day 文件
day_files = sorted(vipdoc.glob("*.day")) if vipdoc.is_dir() else []
if not day_files:
print(f"扩展市场目录为空或不存在: {vipdoc}")
print("请在通达信中下载扩展市场数据后再试")
raise SystemExit(0)
print(f"可用文件 ({len(day_files)} 个):")
for f in day_files[:10]:
print(f" {f.name}")
if len(day_files) > 10:
print(f" ... 还有 {len(day_files) - 10}")
# 读取第一个文件作为示例
sample = day_files[5]
"""
可用文件 (211 个):
12#A_IXIC.day
38#1_GDP.day
38#1_GDPI.day
38#1_MSR.day
38#2_CGPI.day
38#2_CPI.day
38#2_PPCI.day
38#2_PPI.day
38#2_PPPI.day
38#3_BCI.day
... 还有 201 个
读取: 38#2_CPI.day
共 250 条记录,最后 5 条:
日期 开盘 最高 最低 收盘 结算
2025-12-31 100.80 100.80 100.80 100.80 0.00
2026-01-31 100.20 100.20 100.20 100.20 0.00
2026-02-28 101.30 101.30 101.30 101.30 0.00
2026-03-31 101.00 101.00 101.00 101.00 0.00
2026-04-30 101.20 101.20 101.20 101.20 0.00
"""
print(f"\n读取: {sample.name}")
bars = read_ex_daily_bars(sample)
if bars:
print(f"{len(bars)} 条记录,最后 5 条:")
print(f" {'日期':>12s} {'开盘':>8s} {'最高':>8s} {'最低':>8s} {'收盘':>8s} {'结算':>8s}")
for bar in bars[-5:]:
print(
f" {bar.year}-{bar.month:02d}-{bar.day:02d} "
f"{bar.open:>8.2f} {bar.high:>8.2f} "
f"{bar.low:>8.2f} {bar.close:>8.2f} {bar.settlement:>8.2f}"
)
+53
View File
@@ -0,0 +1,53 @@
"""演示:从本地通达信目录读取股本变迁数据。
股本变迁文件包含分红、送股、配股、扩缩股等历史记录。
数据使用 XOR 加密存储,读取时会自动解密。
需要本地已安装通达信。
"""
from pathlib import Path
from xmtdx.offline import detect_tdx_home, read_gbbq
home = detect_tdx_home()
if home is None:
print("未检测到通达信安装目录,请设置 TDX_HOME 环境变量")
raise SystemExit(1)
gbbq_path = Path(home) / "T0002" / "hq_cache" / "gbbq"
if not gbbq_path.is_file():
# 尝试其他可能的路径
gbbq_path = Path(home) / "T0002" / "gbbq"
if not gbbq_path.is_file():
print(f"股本变迁文件不存在")
print(f" 尝试过: {Path(home) / 'T0002' / 'hq_cache' / 'gbbq'}")
print(f" 尝试过: {Path(home) / 'T0002' / 'gbbq'}")
print("请在通达信中确认 gbbq 文件的位置")
raise SystemExit(0)
print(f"读取: {gbbq_path}")
records = read_gbbq(gbbq_path)
if not records:
print("未读取到数据")
raise SystemExit(0)
print(f"{len(records)} 条股本变迁记录\n")
# 按代码分组统计
from collections import Counter
code_counts = Counter(r.code for r in records)
print(f"涉及 {len(code_counts)} 只股票")
# 显示前 20 条记录
print(f"\n前 20 条记录:")
print(f" {'市场':>4s} {'代码':>8s} {'日期':>10s} {'类别':>4s} {'红利/盘前流通':>12s} {'配股价/前总股本':>14s}")
for rec in records[:20]:
print(
f" {rec.market:>4d} {rec.code:>8s} {rec.datetime:>10d} "
f"{rec.category:>4d} {rec.hongli_panqianliutong:>12.4f} "
f"{rec.peigujia_qianzongguben:>14.4f}"
)
+68
View File
@@ -0,0 +1,68 @@
"""演示:从本地通达信目录读取历史财务数据。
支持两种文件格式:
- .dat 文件: 直接读取
- .zip 文件: 自动解压后读取(如 gpcw20260331.zip
文件可通过 TdxClient.get_financial_file_list() + download_file() 获取,
也可从 calc 服务器下载。
需要本地有 gpcw*.dat 或 gpcw*.zip 文件。
"""
from pathlib import Path
from xmtdx.offline import detect_tdx_home, read_history_financial
home = detect_tdx_home()
if home is None:
print("未检测到通达信安装目录,请设置 TDX_HOME 环境变量")
raise SystemExit(1)
# 常见的历史财务数据存放位置
candidates = [
Path(home) / "vipdoc" / "fin",
Path(home) / "T0002" / "fin",
Path.home() / "Downloads",
Path("."),
]
# 查找可用的财务数据文件
fin_files: list[Path] = []
for d in candidates:
if d.is_dir():
fin_files.extend(d.glob("gpcw*.dat"))
fin_files.extend(d.glob("gpcw*.zip"))
if not fin_files:
print("未找到历史财务数据文件 (gpcw*.dat 或 gpcw*.zip)")
print("\n获取方式:")
print(" 1. 使用 TdxClient.get_financial_file_list() 查询可用文件")
print(" 2. 使用 TdxClient.download_file() 下载到本地")
raise SystemExit(0)
print(f"找到 {len(fin_files)} 个财务数据文件:")
for f in fin_files:
print(f" {f}")
# 读取第一个文件
sample = fin_files[0]
print(f"\n读取: {sample.name}")
records = read_history_financial(sample)
if not records:
print("未读取到数据")
raise SystemExit(0)
print(f"{len(records)} 条记录")
print(f"\n前 10 条:")
print(f" {'代码':>8s} {'市场':>4s} {'报告期':>10s} {'字段数':>6s}")
for rec in records[:10]:
print(f" {rec.code:>8s} {rec.market.name:>4s} {rec.report_date:>10d} {len(rec.fields):>6d}")
# 展示一只股票的详细数据
if records:
rec = records[0]
print(f"\n{rec.code} ({rec.market.name}) 报告期 {rec.report_date} 的前 20 个字段:")
for i, val in enumerate(rec.fields[:20]):
print(f" 字段{i + 1:3d}: {val:>15.4f}")
+83
View File
@@ -0,0 +1,83 @@
"""演示:从本地通达信目录读取分钟 K 线数据。
支持三种文件格式:
- .5 文件: vipdoc/{sh,sz}/fzline/ (OHLC 为整数÷100)
- .lc1 文件: vipdoc/{sh,sz}/fzline/ (OHLC 为浮点数)
- .lc5 文件: vipdoc/{sh,sz}/fzline/ (OHLC 为浮点数)
需要本地已安装通达信并下载过分钟数据。
"""
from xmtdx.offline import (
detect_tdx_home,
read_5min_bars,
read_lc_min_bars,
find_5min_bar_file,
find_lc1_bar_file,
find_lc5_bar_file,
)
from xmtdx import Market
home = detect_tdx_home()
if home is None:
print("未检测到通达信安装目录,请设置 TDX_HOME 环境变量")
raise SystemExit(1)
"""
# --- .5 文件 (5 分钟线) ---
print("=" * 60)
print("5 分钟线 (.5 文件)")
print("=" * 60)
filepath = find_5min_bar_file(Market.SH, "600000")
bars = read_5min_bars(filepath)
if bars:
print(f"{len(bars)} 条记录,最后 5 条:")
for bar in bars[-5:]:
print(
f" {bar.year}-{bar.month:02d}-{bar.day:02d} "
f"{bar.hour:02d}:{bar.minute:02d} "
f"{bar.open:>7.2f}{bar.high:>7.2f} "
f"{bar.low:>7.2f}{bar.close:>7.2f}{bar.vol:>8.0f}"
)
else:
print("未读取到数据")
# --- .lc1 文件 (1 分钟线) ---
print(f"\n{'=' * 60}")
print("1 分钟线 (.lc1 文件)")
print("=" * 60)
filepath = find_lc1_bar_file(Market.SH, "600000")
bars = read_lc_min_bars(filepath)
if bars:
print(f"{len(bars)} 条记录,最后 5 条:")
for bar in bars[-5:]:
print(
f" {bar.year}-{bar.month:02d}-{bar.day:02d} "
f"{bar.hour:02d}:{bar.minute:02d} "
f"{bar.open:>7.2f}{bar.high:>7.2f} "
f"{bar.low:>7.2f}{bar.close:>7.2f}{bar.vol:>8.0f}"
)
else:
print("未读取到数据")
"""
# --- .lc5 文件 (5 分钟线) ---
print(f"\n{'=' * 60}")
print("5 分钟线 (.lc5 文件)")
print("=" * 60)
filepath = find_lc5_bar_file(Market.SZ, "002176")
bars = read_lc_min_bars(filepath)
if bars:
print(f"{len(bars)} 条记录,最后 5 条:")
for bar in bars[-5:]:
print(
f" {bar.year}-{bar.month:02d}-{bar.day:02d} "
f"{bar.hour:02d}:{bar.minute:02d} "
f"{bar.open:>7.2f}{bar.high:>7.2f} "
f"{bar.low:>7.2f}{bar.close:>7.2f}{bar.vol:>8.0f}"
)
else:
print("未读取到数据")