From b17e98468b03cf73251b417f24142ad6f352c088 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Tue, 2 Jun 2026 12:30:24 +0800 Subject: [PATCH] feat: add offline CLI commands and bump version to v1.5.0 - New 'offline' command group with 8 subcommands: home, daily, min, ex-files, ex-daily, gbbq, financial, blocks - No network required, reads local TDX data files directly - Updated CLI examples and README with offline documentation - Added v1.5.0 changelog entry --- README.md | 44 ++++ examples/20_cli/cli_examples.sh | 65 ++++- pyproject.toml | 2 +- src/easy_tdx/cli/__init__.py | 6 +- src/easy_tdx/cli/cmd_offline.py | 414 ++++++++++++++++++++++++++++++++ 5 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 src/easy_tdx/cli/cmd_offline.py diff --git a/README.md b/README.md index 65fa37f..600109a 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,21 @@ easy-tdx ex quote-list HK_MAIN_BOARD --table # 港股商品列表 easy-tdx ex tick HK_MAIN_BOARD 00700 --table # 港股分时 ``` +### 离线数据(无需网络) + +从本地通达信安装目录直接读取数据文件,无需网络连接: + +```bash +easy-tdx offline home # 检测通达信安装目录 +easy-tdx offline daily SH 600000 --count 10 --table # A 股日线 +easy-tdx offline min SZ 000001 --type lc5 --table # 分钟线(5min/lc1/lc5) +easy-tdx offline ex-files --table # 列出扩展市场可用文件 +easy-tdx offline ex-daily 38#2_CPI --count 5 --table # 扩展市场日线(期货/港股/外盘) +easy-tdx offline gbbq C:\new_jyplug\T0002\hq_cache\gbbq --table # 股本变迁 +easy-tdx offline financial C:\new_jyplug\vipdoc\fin\gpcw20260331.dat # 历史财务 +easy-tdx offline blocks C:\new_jyplug\T0002\blocknew --table # 自定义板块 +``` + ## CLI 命令汇总 | 命令 | 说明 | @@ -244,6 +259,14 @@ easy-tdx ex tick HK_MAIN_BOARD 00700 --table # 港股分时 | `ex quote-list` | 扩展市场商品列表 | | `ex tick` | 扩展市场分时 | | `ex markets` | 列出可用扩展市场 | +| `offline home` | 检测通达信安装目录 | +| `offline daily` | A 股日线(本地 .day 文件) | +| `offline min` | 分钟线(本地 .5/.lc1/.lc5 文件) | +| `offline ex-files` | 列出扩展市场可用文件 | +| `offline ex-daily` | 扩展市场日线(期货/港股/外盘) | +| `offline gbbq` | 股本变迁数据 | +| `offline financial` | 历史财务数据 | +| `offline blocks` | 自定义板块数据 | ## Python API @@ -520,6 +543,14 @@ bars = read_daily_bars(filepath) 支持:日线、分钟线、扩展市场日线、板块、股本变迁、历史财务数据。 +v1.5.0 起可通过 CLI 直接使用: + +```bash +easy-tdx offline daily SH 600000 --count 10 --table +easy-tdx offline ex-files --table +easy-tdx offline ex-daily 29#A1801 --table +``` + ## 枚举参考 ### Period(K 线周期) @@ -716,6 +747,19 @@ ruff format --check src/ tests/ # format check ## Changelog +### 1.5.0 (2026-06-02) + +**离线数据 CLI 命令** — 新增 `offline` 命令组,无需网络即可通过 CLI 读取本地通达信数据文件。 + +- 新增 `offline home`:检测通达信安装目录 +- 新增 `offline daily`:A 股日线数据(.day 文件) +- 新增 `offline min`:分钟线数据(.5/.lc1/.lc5 文件,`--type` 指定格式) +- 新增 `offline ex-files`:列出扩展市场可用日线文件 +- 新增 `offline ex-daily`:扩展市场日线数据(期货/港股/外盘) +- 新增 `offline gbbq`:股本变迁数据 +- 新增 `offline financial`:历史财务数据 +- 新增 `offline blocks`:自定义板块数据 + ### 1.4.3 (2026-05-28) **30日乖离率信号指标** — 新增 BIAS_SIGNAL 指标,在标准乖离率基础上叠加短/长信号线,通过三者位置关系判断趋势方向和转折点。源自通达信经典指标。 diff --git a/examples/20_cli/cli_examples.sh b/examples/20_cli/cli_examples.sh index 6d93689..011c959 100644 --- a/examples/20_cli/cli_examples.sh +++ b/examples/20_cli/cli_examples.sh @@ -380,7 +380,70 @@ echo "=== 29. 分钟 K 线技术指标 ===" # 2025-05-15 14:25 1521.00 0.23 0.25 -0.04 # ...(共10条) -echo "=== 30. 常用指标快速参考 ===" +echo "=== 30. 离线数据 - 检测通达信目录 ===" +# 无需网络,直接读取本地通达信安装目录中的数据文件。 +# 需要本地已安装通达信并下载过对应数据。 +# easy-tdx offline home +# 输出: +# C:\new_jyplug + +echo "=== 31. 离线数据 - A 股日线 ===" +# 通过市场+代码自动定位 .day 文件,读取本地日线数据。 +# 参数: <市场> <代码> --count N --table +# easy-tdx offline daily SH 600000 --count 10 --table +# 输出: +# datetime open high low close vol amount +# 2026-05-26 9.10 9.32 9.09 9.27 1464374.54 1356643840 +# 2026-05-27 9.29 9.54 9.26 9.43 1345489.76 1269924736 +# 2026-05-28 9.42 9.46 9.14 9.21 836655.20 776320256 +# 2026-05-29 9.18 9.38 9.13 9.37 933303.04 869221248 +# 2026-06-01 9.32 9.35 9.20 9.32 750721.20 697403328 + +echo "=== 32. 离线数据 - 分钟线 ===" +# 读取本地分钟线数据,支持三种格式。 +# --type: 5min(.5) / lc1(1分钟) / lc5(5分钟) +# easy-tdx offline min SH 600000 --count 5 --table +# easy-tdx offline min SZ 000001 --type lc1 --count 10 --table +# 输出: +# datetime open high low close vol amount +# 2026-05-12 14:30 10.35 10.38 10.34 10.36 285400 295600000 +# 2026-05-12 14:35 10.36 10.40 10.35 10.38 312500 324000000 + +echo "=== 33. 离线数据 - 扩展市场文件列表 ===" +# 列出本地扩展市场(期货/港股/外盘)可用的日线数据文件。 +# easy-tdx offline ex-files --table +# 输出: +# filename size_kb +# 38#1_GDP.day 0.8 +# 38#2_CPI.day 9.9 +# 29#A1801.day 12.3 +# 31#00700.day 8.5 +# ...(共211个) + +echo "=== 34. 离线数据 - 扩展市场日线 ===" +# 读取扩展市场日线数据(期货/港股/外盘指数等)。 +# 参数: <文件名>(不含 .day 后缀) +# easy-tdx offline ex-daily 38#2_CPI --count 5 --table +# easy-tdx offline ex-daily 29#A1801 --table +# 输出: +# datetime open high low close vol settlement +# 2025-12-31 100.80 100.80 100.80 100.80 0 0.0 +# 2026-01-31 100.20 100.20 100.20 100.20 0 0.0 +# 2026-02-28 101.30 101.30 101.30 101.30 0 0.0 + +echo "=== 35. 离线数据 - 股本变迁 ===" +# 读取本地股本变迁文件(分红/送股/配股等历史记录)。 +# easy-tdx offline gbbq C:\new_jyplug\T0002\hq_cache\gbbq --count 10 --table + +echo "=== 36. 离线数据 - 历史财务 ===" +# 读取本地历史财务数据(gpcw*.dat / gpcw*.zip)。 +# easy-tdx offline financial C:\new_jyplug\vipdoc\fin\gpcw20260331.dat --count 5 --table + +echo "=== 37. 离线数据 - 自定义板块 ===" +# 读取本地自定义板块数据。 +# easy-tdx offline blocks C:\new_jyplug\T0002\blocknew --table + +echo "=== 38. 常用指标快速参考 ===" # MACD: easy-tdx indicator MACD -m SH -c 600519 --table # KDJ: easy-tdx indicator KDJ -m SZ -c 000001 --table # RSI: easy-tdx indicator RSI -m SH -c 600519 --table diff --git a/pyproject.toml b/pyproject.toml index 19e35b7..a2e8186 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.4.3" +version = "1.5.0" description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取" readme = "README.md" requires-python = ">=3.10" diff --git a/src/easy_tdx/cli/__init__.py b/src/easy_tdx/cli/__init__.py index 14c8008..1e9fe8e 100644 --- a/src/easy_tdx/cli/__init__.py +++ b/src/easy_tdx/cli/__init__.py @@ -10,17 +10,18 @@ from .cmd_board import belong_board, board_list, board_members, board_ranking, b from .cmd_capital import capital_flow from .cmd_ex import ex from .cmd_finance import f10, fund_flow -from .cmd_info import server_info, symbol_info from .cmd_indicator import indicator, indicator_list +from .cmd_info import server_info, symbol_info from .cmd_kline import kline from .cmd_monitor import market_stat, unusual +from .cmd_offline import offline from .cmd_quote import quote, quote_list from .cmd_tick import tick from .cmd_transaction import transaction @click.group() -@click.version_option(version="1.4.0", prog_name="easy-tdx") +@click.version_option(version="1.5.0", prog_name="easy-tdx") def cli() -> None: """easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。 @@ -62,3 +63,4 @@ cli.add_command(fund_flow) cli.add_command(ex) cli.add_command(indicator) cli.add_command(indicator_list) +cli.add_command(offline) diff --git a/src/easy_tdx/cli/cmd_offline.py b/src/easy_tdx/cli/cmd_offline.py new file mode 100644 index 0000000..59d7bb9 --- /dev/null +++ b/src/easy_tdx/cli/cmd_offline.py @@ -0,0 +1,414 @@ +"""离线本地数据读取命令(无需网络,读取本地通达信数据文件)。""" + +from __future__ import annotations + +import click + + +@click.group() +def offline() -> None: + """离线本地数据读取(无需网络,读取本地通达信数据文件)。 + + 需要本地已安装通达信并下载过对应数据。 + + 示例: + + easy-tdx offline home + + easy-tdx offline daily SZ 000001 --table + + easy-tdx offline min SH 600519 --type lc5 --table + + easy-tdx offline ex-files --table + + easy-tdx offline ex-daily 29#A1801 --table + """ + pass + + +@offline.command() +def home() -> None: + """检测并显示通达信安装目录。""" + from ..offline import detect_tdx_home + + tdx_home = detect_tdx_home() + if tdx_home is None: + click.echo("未检测到通达信安装目录,请设置 TDX_HOME 环境变量") + raise SystemExit(1) + click.echo(str(tdx_home)) + + +@offline.command() +@click.argument("market") +@click.argument("code") +@click.option("--count", default=0, type=int, help="返回条数(0=全部)") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def daily( + market: str, + code: str, + count: int, + use_table: bool, + output_fmt: str, +) -> None: + """读取 A 股日线数据(.day 文件)。 + + MARKET: 市场代码(SZ/SH) + CODE: 6 位股票代码 + + 示例: + + easy-tdx offline daily SZ 000001 --table + + easy-tdx offline daily SH 600519 --count 30 --table + """ + import pandas as pd + + from .output import print_error, print_output + from .parsers import parse_market + + fmt = "table" if use_table else output_fmt + + try: + from ..offline import find_daily_bar_file, read_daily_bars + + filepath = find_daily_bar_file(parse_market(market), code) + bars = read_daily_bars(filepath) + except Exception as e: + print_error(str(e)) + raise SystemExit(1) + + if not bars: + click.echo("未读取到数据,请确认通达信已下载该股票的日线数据") + raise SystemExit(0) + + rows = [ + { + "datetime": f"{b.year}-{b.month:02d}-{b.day:02d}", + "open": b.open, + "high": b.high, + "low": b.low, + "close": b.close, + "vol": b.vol, + "amount": b.amount, + } + for b in (bars[-count:] if count > 0 else bars) + ] + print_output(pd.DataFrame(rows), fmt) + + +@offline.command() +@click.argument("market") +@click.argument("code") +@click.option( + "--type", + "bar_type", + type=click.Choice(["5min", "lc1", "lc5"]), + default="5min", + help="分钟线类型: 5min(.5) / lc1(.lc1) / lc5(.lc5)", +) +@click.option("--count", default=0, type=int, help="返回条数(0=全部)") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def min( + market: str, + code: str, + bar_type: str, + count: int, + use_table: bool, + output_fmt: str, +) -> None: + """读取分钟线数据(.5 / .lc1 / .lc5 文件)。 + + MARKET: 市场代码(SZ/SH) + CODE: 6 位股票代码 + + 示例: + + easy-tdx offline min SZ 000001 --table + + easy-tdx offline min SH 600519 --type lc1 --count 100 --table + """ + import pandas as pd + + from .output import print_error, print_output + from .parsers import parse_market + + fmt = "table" if use_table else output_fmt + + try: + from ..offline import read_5min_bars, read_lc_min_bars + from ..offline.finders import find_5min_bar_file, find_lc1_bar_file, find_lc5_bar_file + + mkt = parse_market(market) + if bar_type == "5min": + filepath = find_5min_bar_file(mkt, code) + bars = read_5min_bars(filepath) + elif bar_type == "lc1": + filepath = find_lc1_bar_file(mkt, code) + bars = read_lc_min_bars(filepath) + else: + filepath = find_lc5_bar_file(mkt, code) + bars = read_lc_min_bars(filepath) + except Exception as e: + print_error(str(e)) + raise SystemExit(1) + + if not bars: + click.echo("未读取到数据,请确认通达信已下载该股票的分钟线数据") + raise SystemExit(0) + + rows = [ + { + "datetime": f"{b.year}-{b.month:02d}-{b.day:02d} {b.hour:02d}:{b.minute:02d}", + "open": b.open, + "high": b.high, + "low": b.low, + "close": b.close, + "vol": b.vol, + "amount": b.amount, + } + for b in (bars[-count:] if count > 0 else bars) + ] + print_output(pd.DataFrame(rows), fmt) + + +@offline.command("ex-files") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +def ex_files(use_table: bool) -> None: + """列出扩展市场可用的日线数据文件。 + + 文件位于 vipdoc/ds/lday/ 目录下。 + """ + import pandas as pd + + from ..offline.paths import detect_tdx_home + from .output import print_error, print_output + + fmt = "table" if use_table else "json" + + tdx_home = detect_tdx_home() + if tdx_home is None: + print_error("未检测到通达信安装目录,请设置 TDX_HOME 环境变量") + raise SystemExit(1) + + lday_dir = tdx_home / "vipdoc" / "ds" / "lday" + if not lday_dir.is_dir(): + print_error(f"扩展市场目录不存在: {lday_dir}") + raise SystemExit(1) + + files = sorted(lday_dir.glob("*.day")) + if not files: + click.echo("扩展市场目录为空,请在通达信中下载扩展市场数据") + raise SystemExit(0) + + rows = [{"filename": f.name, "size_kb": round(f.stat().st_size / 1024, 1)} for f in files] + print_output(pd.DataFrame(rows), fmt) + + +@offline.command("ex-daily") +@click.argument("filename") +@click.option("--count", default=0, type=int, help="返回条数(0=全部)") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def ex_daily( + filename: str, + count: int, + use_table: bool, + output_fmt: str, +) -> None: + """读取扩展市场日线数据(期货/港股/外盘)。 + + FILENAME: 文件名(如 29#A1801)或完整路径 + + 示例: + + easy-tdx offline ex-daily 29#A1801 --table + + easy-tdx offline ex-daily 12#A_IXIC --count 30 --table + """ + from pathlib import Path + + import pandas as pd + + from ..offline import read_ex_daily_bars + from ..offline.paths import detect_tdx_home + from .output import print_error, print_output + + fmt = "table" if use_table else output_fmt + + filepath = Path(filename) + if not filepath.is_file(): + tdx_home = detect_tdx_home() + if tdx_home is not None: + filepath = tdx_home / "vipdoc" / "ds" / "lday" / f"{filename}.day" + + try: + bars = read_ex_daily_bars(filepath) + except Exception as e: + print_error(str(e)) + raise SystemExit(1) + + if not bars: + click.echo("未读取到数据,请确认文件存在且已下载对应数据") + raise SystemExit(0) + + rows = [ + { + "datetime": f"{b.year}-{b.month:02d}-{b.day:02d}", + "open": b.open, + "high": b.high, + "low": b.low, + "close": b.close, + "vol": b.vol, + "settlement": b.settlement, + } + for b in (bars[-count:] if count > 0 else bars) + ] + print_output(pd.DataFrame(rows), fmt) + + +@offline.command() +@click.argument("filepath") +@click.option("--count", default=0, type=int, help="返回条数(0=全部)") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def gbbq( + filepath: str, + count: int, + use_table: bool, + output_fmt: str, +) -> None: + """读取股本变迁数据(gbbq 文件)。 + + FILEPATH: gbbq 文件路径 + + 示例: + + easy-tdx offline gbbq C:\\new_jyplug\\T0002\\hq_cache\\gbbq --table + """ + import pandas as pd + + from ..offline import read_gbbq + from .output import print_error, print_output + + fmt = "table" if use_table else output_fmt + + try: + records = read_gbbq(filepath) + except Exception as e: + print_error(str(e)) + raise SystemExit(1) + + if not records: + click.echo("未读取到数据") + raise SystemExit(0) + + rows = [ + { + "market": r.market, + "code": r.code, + "datetime": r.datetime, + "category": r.category, + "hongli_panqianliutong": r.hongli_panqianliutong, + "peigujia_qianzongguben": r.peigujia_qianzongguben, + "songgu_qianzongguben": r.songgu_qianzongguben, + "peigu_houzongguben": r.peigu_houzongguben, + } + for r in (records[-count:] if count > 0 else records) + ] + print_output(pd.DataFrame(rows), fmt) + + +@offline.command() +@click.argument("filepath") +@click.option("--count", default=0, type=int, help="返回条数(0=全部)") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def financial( + filepath: str, + count: int, + use_table: bool, + output_fmt: str, +) -> None: + """读取历史财务数据(gpcw*.dat / gpcw*.zip)。 + + FILEPATH: 财务数据文件路径 + + 示例: + + easy-tdx offline financial C:\\new_jyplug\\vipdoc\\sz\\gpcw.zip --count 5 --table + """ + import pandas as pd + + from ..offline import read_history_financial + from .output import print_error, print_output + + fmt = "table" if use_table else output_fmt + + try: + records = read_history_financial(filepath) + except Exception as e: + print_error(str(e)) + raise SystemExit(1) + + if not records: + click.echo("未读取到数据") + raise SystemExit(0) + + display = records[-count:] if count > 0 else records + rows = [ + { + "code": r.code, + "market": r.market.value, + "report_date": r.report_date, + } + for r in display + ] + print_output(pd.DataFrame(rows), fmt) + + +@offline.command() +@click.argument("block_dir") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def blocks( + block_dir: str, + use_table: bool, + output_fmt: str, +) -> None: + """读取自定义板块数据。 + + BLOCK_DIR: 自定义板块目录路径(如 C:\\new_jyplug\\T0002\\blocknew) + + 示例: + + easy-tdx offline blocks C:\\new_jyplug\\T0002\\blocknew --table + """ + import pandas as pd + + from ..offline import read_customer_blocks + from .output import print_error, print_output + + fmt = "table" if use_table else output_fmt + + try: + result = read_customer_blocks(block_dir) + except Exception as e: + print_error(str(e)) + raise SystemExit(1) + + if not result: + click.echo("未读取到板块数据") + raise SystemExit(0) + + rows = [ + { + "blockname": b.blockname, + "type": b.block_type, + "stock_count": len(b.codes), + "codes": ",".join(b.codes[:10]) + ("..." if len(b.codes) > 10 else ""), + } + for b in result + ] + print_output(pd.DataFrame(rows), fmt)