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
This commit is contained in:
Justin Gu
2026-06-02 12:30:24 +08:00
parent 07c1eca5e8
commit b17e98468b
5 changed files with 527 additions and 4 deletions
+44
View File
@@ -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
```
## 枚举参考
### PeriodK 线周期)
@@ -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 指标,在标准乖离率基础上叠加短/长信号线,通过三者位置关系判断趋势方向和转折点。源自通达信经典指标。
+64 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+4 -2
View File
@@ -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)
+414
View File
@@ -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)